From 1e829bb6193208b9e4ec31f9d2441e14138eeb9b Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Fri, 31 Mar 2023 06:19:34 +0100 Subject: [PATCH 1/7] refactor name of Settings interface for clarity --- src/main.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main.ts b/src/main.ts index 1600796..41c8aea 100644 --- a/src/main.ts +++ b/src/main.ts @@ -36,7 +36,7 @@ enum HighlightOrder { TIME = "the time that highlights are updated", } -interface Settings { +interface OmnivoreSettings { apiKey: string; filter: string; syncAt: string; @@ -51,7 +51,8 @@ interface Settings { dateSavedFormat: string; filename: string; } -const DEFAULT_SETTINGS: Settings = { + +const DEFAULT_SETTINGS: OmnivoreSettings = { dateHighlightedFormat: "yyyy-MM-dd HH:mm:ss", dateSavedFormat: "yyyy-MM-dd HH:mm:ss", apiKey: "", @@ -102,7 +103,7 @@ date_published: {{{datePublished}}} }; export default class OmnivorePlugin extends Plugin { - settings: Settings; + settings: OmnivoreSettings; async onload() { await this.loadSettings(); From 0120ea0bc97ad5ae60336b7229c31b1a58865644 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Fri, 31 Mar 2023 06:20:12 +0100 Subject: [PATCH 2/7] export default settings and settings interface for testing --- src/main.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.ts b/src/main.ts index 41c8aea..364cbdc 100644 --- a/src/main.ts +++ b/src/main.ts @@ -36,7 +36,7 @@ enum HighlightOrder { TIME = "the time that highlights are updated", } -interface OmnivoreSettings { +export interface OmnivoreSettings { apiKey: string; filter: string; syncAt: string; @@ -52,7 +52,7 @@ interface OmnivoreSettings { filename: string; } -const DEFAULT_SETTINGS: OmnivoreSettings = { +export const DEFAULT_SETTINGS: OmnivoreSettings = { dateHighlightedFormat: "yyyy-MM-dd HH:mm:ss", dateSavedFormat: "yyyy-MM-dd HH:mm:ss", apiKey: "", From 8c7e02afa327898eb41e35691428015c10f1fa01 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Fri, 31 Mar 2023 06:26:10 +0100 Subject: [PATCH 3/7] extract DEFAULT_SETTINGS for testability --- src/DEFAULT_SETTINGS.ts | 51 +++++++++++++++++++++++++++++++++++++++++ src/main.ts | 51 +---------------------------------------- 2 files changed, 52 insertions(+), 50 deletions(-) create mode 100644 src/DEFAULT_SETTINGS.ts diff --git a/src/DEFAULT_SETTINGS.ts b/src/DEFAULT_SETTINGS.ts new file mode 100644 index 0000000..72706ce --- /dev/null +++ b/src/DEFAULT_SETTINGS.ts @@ -0,0 +1,51 @@ +import { OmnivoreSettings } from "./main"; + +export const DEFAULT_SETTINGS: OmnivoreSettings = { + dateHighlightedFormat: "yyyy-MM-dd HH:mm:ss", + dateSavedFormat: "yyyy-MM-dd HH:mm:ss", + apiKey: "", + filter: "HIGHLIGHTS", + syncAt: "", + customQuery: "", + template: `--- +id: {{{id}}} +title: {{{title}}} +{{#author}} +author: {{{author}}} +{{/author}} +{{#labels.length}} +tags: +{{#labels}} - {{{name}}} +{{/labels}} +{{/labels.length}} +date_saved: {{{dateSaved}}} +{{#datePublished}} +date_published: {{{datePublished}}} +{{/datePublished}} +--- + +# {{{title}}} +#Omnivore + +[Read on Omnivore]({{{omnivoreUrl}}}) +[Read Original]({{{originalUrl}}}) + +{{#highlights.length}} +## Highlights + +{{#highlights}} +> {{{text}}} [⤴️]({{{highlightUrl}}}) {{#labels}} #{{name}} {{/labels}} +{{#note}} + +{{{note}}} +{{/note}} + +{{/highlights}} +{{/highlights.length}}`, + highlightOrder: "LOCATION", + syncing: false, + folder: "Omnivore/{{date}}", + folderDateFormat: "yyyy-MM-dd", + endpoint: "https://api-prod.omnivore.app/api/graphql", + filename: "{{{title}}}" +}; diff --git a/src/main.ts b/src/main.ts index 364cbdc..904b583 100644 --- a/src/main.ts +++ b/src/main.ts @@ -24,6 +24,7 @@ import { parseDateTime, replaceIllegalChars, } from "./util"; +import { DEFAULT_SETTINGS } from "./DEFAULT_SETTINGS"; enum Filter { ALL = "import all my articles", @@ -52,56 +53,6 @@ export interface OmnivoreSettings { filename: string; } -export const DEFAULT_SETTINGS: OmnivoreSettings = { - dateHighlightedFormat: "yyyy-MM-dd HH:mm:ss", - dateSavedFormat: "yyyy-MM-dd HH:mm:ss", - apiKey: "", - filter: "HIGHLIGHTS", - syncAt: "", - customQuery: "", - template: `--- -id: {{{id}}} -title: {{{title}}} -{{#author}} -author: {{{author}}} -{{/author}} -{{#labels.length}} -tags: -{{#labels}} - {{{name}}} -{{/labels}} -{{/labels.length}} -date_saved: {{{dateSaved}}} -{{#datePublished}} -date_published: {{{datePublished}}} -{{/datePublished}} ---- - -# {{{title}}} -#Omnivore - -[Read on Omnivore]({{{omnivoreUrl}}}) -[Read Original]({{{originalUrl}}}) - -{{#highlights.length}} -## Highlights - -{{#highlights}} -> {{{text}}} [⤴️]({{{highlightUrl}}}) {{#labels}} #{{name}} {{/labels}} -{{#note}} - -{{{note}}} -{{/note}} - -{{/highlights}} -{{/highlights.length}}`, - highlightOrder: "LOCATION", - syncing: false, - folder: "Omnivore/{{date}}", - folderDateFormat: "yyyy-MM-dd", - endpoint: "https://api-prod.omnivore.app/api/graphql", - filename: "{{{title}}}", -}; - export default class OmnivorePlugin extends Plugin { settings: OmnivoreSettings; From 89ec89d724f2944a3266a9c9b751050456a30afb Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Fri, 31 Mar 2023 07:17:11 +0100 Subject: [PATCH 4/7] migrate formatDate to function rather than const containing function --- src/util.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util.ts b/src/util.ts index 89ae509..8c6f0ab 100644 --- a/src/util.ts +++ b/src/util.ts @@ -289,6 +289,6 @@ export const replaceIllegalChars = (str: string): string => { return str.replace(ILLEGAL_CHAR_REGEX, REPLACEMENT_CHAR); }; -export const formatDate = (date: string, format: string): string => { +export function formatDate(date: string, format: string): string { return DateTime.fromISO(date).toFormat(format); }; From 15aca68859312b299d01b84c87bef7f412653e0e Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Fri, 31 Mar 2023 07:37:31 +0100 Subject: [PATCH 5/7] add formatDate test --- src/__tests__/formatDate.ts | 134 ++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 src/__tests__/formatDate.ts diff --git a/src/__tests__/formatDate.ts b/src/__tests__/formatDate.ts new file mode 100644 index 0000000..c39fed8 --- /dev/null +++ b/src/__tests__/formatDate.ts @@ -0,0 +1,134 @@ +import { DEFAULT_SETTINGS } from "../DEFAULT_SETTINGS"; +import { formatDate } from "../util"; + +const jsDate = new Date("2023-02-18 13:02:08.169"); +const apiDate = jsDate.toISOString(); // API returns ISO 8601 date strings + +type testCase = { + format: string, + date: string + expected: string +} + + +const luxonHierarchicalFormatWithTime = { + date: apiDate, + expected: "2023/2023-02/2023-02-18/130208", + format: "yyyy/yyyy-MM/yyyy-MM-dd/HHmmss" +}; +const luxonHierarchicalFormat = { + date: apiDate, + expected: "2023/2023-02/2023-02-18", + format: "yyyy/yyyy-MM/yyyy-MM-dd" +}; +const defaultDateHighlightedFormatTestCase: testCase = { + date: apiDate, + expected: "2023-02-18 13:02:08", + format: DEFAULT_SETTINGS.dateHighlightedFormat +}; +const defaultDateSavedFormatTestCase: testCase = { + date: apiDate, + expected: "2023-02-18 13:02:08", + format: DEFAULT_SETTINGS.dateSavedFormat +}; +const defaultFolderDateFormatTestCase: testCase = { + date: apiDate, + expected: "2023-02-18", + format: DEFAULT_SETTINGS.folderDateFormat +}; +const testCases: testCase[] = [ + defaultDateHighlightedFormatTestCase, + defaultDateSavedFormatTestCase, + defaultFolderDateFormatTestCase, + luxonHierarchicalFormat, + luxonHierarchicalFormatWithTime +]; +describe("ensure default formats are as expected", () => { + test("dateHighlightedFormat", () => { + expect(DEFAULT_SETTINGS.dateHighlightedFormat).toBe("yyyy-MM-dd HH:mm:ss"); + }); + test("dateSavedFormat", () => { + expect(DEFAULT_SETTINGS.dateSavedFormat).toBe("yyyy-MM-dd HH:mm:ss"); + }); + test("folderDateFormat", () => { + expect(DEFAULT_SETTINGS.folderDateFormat).toBe("yyyy-MM-dd"); + }); +}); + +describe("formatDate on known formats", () => { + test.each(testCases)("should correctly format %s", (testCase) => { + const result = formatDate(testCase.date, testCase.format); + expect(result).toBe(testCase.expected); + }); +}); + +const { DateTime } = require("luxon"); + +function generateRandomISODateStrings(quantity: number) { + const randomISODateStrings = []; + const timeZones: any[] = Intl.DateTimeFormat().resolvedOptions().timeZone.split(","); + + for (let i = 0; i < quantity; i++) { + let date = new Date( + Date.UTC( + Math.floor(Math.random() * (2038 - 1970) + 1970), + Math.floor(Math.random() * 12), + Math.floor(Math.random() * 28), + Math.floor(Math.random() * 24), + Math.floor(Math.random() * 60), + Math.floor(Math.random() * 60), + Math.floor(Math.random() * 1000) + ) + ); + + // Randomly select a timezone from the available time zones + const randomTimeZone = timeZones[Math.floor(Math.random() * timeZones.length)]; + + // Convert the generated date to the randomly selected timezone + // const dateTimeWithZone = DateTime.fromJSDate(date, { zone: randomTimeZone }).toUTC(); + const jsDateTimeWithZone = new Date(date.toLocaleString("en-US", { timeZone: randomTimeZone })); + const luxonDate = DateTime.fromJSDate(jsDateTimeWithZone); + randomISODateStrings.push(luxonDate.toISO()); + } + + return randomISODateStrings; +} + +describe("formatDate on random dates", () => { + test.each(generateRandomISODateStrings(100))("should correctly format %s", (date) => { + const result = formatDate(date, "yyyy-MM-dd HH:mm:ss"); + // test with regex to ensure the format is correct + expect(result).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/); + }); +}); + +function getCasesWithRandomDates(testFormats: string[], quantity = 10) { + return testFormats.flatMap(luxonFormat => + generateRandomISODateStrings(quantity).map(date => ({ date, luxonFormat })) + ); +} + +describe("round trip on random dates", () => { + const testFormats = [ + defaultDateHighlightedFormatTestCase.format, + defaultDateSavedFormatTestCase.format, + defaultFolderDateFormatTestCase.format + ]; + // generate permutations of testCases.formats and 10 generated each + const casesWithRandomDates = getCasesWithRandomDates(testFormats); + test.each(casesWithRandomDates)("should be unchanged after round trip %s", (testCase) => { + const result = formatDate(testCase.date, testCase.luxonFormat); + const result2 = formatDate(result, testCase.luxonFormat); + expect(result2).toBe(result); + }); + + const atypicalFormats = [ + luxonHierarchicalFormat.format, + luxonHierarchicalFormatWithTime.format + ]; + test.each(getCasesWithRandomDates(atypicalFormats))("should be unchanged after round trip with atypical format %s", (testCase) => { + const formattedDate = formatDate(testCase.date, testCase.luxonFormat); + const parsedDate = DateTime.fromFormat(formattedDate, testCase.luxonFormat); + expect(parsedDate.isValid).toBe(true); + }); +}); From 923fb9ad8d396eafa41566135084926918df81d0 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Fri, 31 Mar 2023 09:38:17 +0100 Subject: [PATCH 6/7] handle bad dates being passed to formatDate --- src/util.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/util.ts b/src/util.ts index 8c6f0ab..3971d83 100644 --- a/src/util.ts +++ b/src/util.ts @@ -290,5 +290,9 @@ export const replaceIllegalChars = (str: string): string => { }; export function formatDate(date: string, format: string): string { - return DateTime.fromISO(date).toFormat(format); -}; + if (isNaN(Date.parse(date))) { + throw new Error(`Invalid date: ${date}`); + } + return DateTime.fromJSDate(new Date(date)).toFormat(format); +} + From cec080c4eef1a710f169f9ee6c1658c26ae91d6e Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Fri, 31 Mar 2023 09:42:09 +0100 Subject: [PATCH 7/7] forget test file extension --- src/__tests__/{formatDate.ts => formatDate.spec.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/__tests__/{formatDate.ts => formatDate.spec.ts} (100%) diff --git a/src/__tests__/formatDate.ts b/src/__tests__/formatDate.spec.ts similarity index 100% rename from src/__tests__/formatDate.ts rename to src/__tests__/formatDate.spec.ts