From ef1ab56f513838c3d026047ba375ad92f210e76d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 18 Jan 2025 19:20:49 +0200 Subject: [PATCH 01/26] chore(i18n): translate two more messages in Romanian --- translations/ro/server.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/translations/ro/server.json b/translations/ro/server.json index 34a547c5a..2f25db39d 100644 --- a/translations/ro/server.json +++ b/translations/ro/server.json @@ -244,5 +244,9 @@ }, "notes": { "new-note": "Notiță nouă" + }, + "backend_log": { + "log-does-not-exist": "Fișierul de loguri de backend „{{ fileName }}” nu există (încă).", + "reading-log-failed": "Nu s-a putut citi fișierul de loguri de backend „{{fileName}}”." } } From 5899ba9a5798c5762d95cf48911a44e5066c4e32 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 18 Jan 2025 19:37:09 +0200 Subject: [PATCH 02/26] Revert "feat(options_init): enable horizontal layout by default for new users" This reverts commit 637d1df0f597849d8c4bfc3ad53b6863bf665ba3. --- src/services/options_init.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/services/options_init.ts b/src/services/options_init.ts index 7e7434d2d..f4a98cf23 100644 --- a/src/services/options_init.ts +++ b/src/services/options_init.ts @@ -63,7 +63,6 @@ async function initNotSyncedOptions(initialized: boolean, opts: NotSyncedOpts = optionService.createOption("lastSyncedPush", "0", false); optionService.createOption("theme", "next", false); - optionService.createOption("layoutOrientation", "horizontal", false); optionService.createOption("syncServerHost", opts.syncServerHost || "", false); optionService.createOption("syncServerTimeout", "120000", false); @@ -149,11 +148,9 @@ const defaultOptions: DefaultOption[] = [ { name: "textNoteEditorType", value: "ckeditor-balloon", isSynced: true }, { name: "textNoteEditorMultilineToolbar", value: "false", isSynced: true }, - // Appearance + // HTML import configuration { name: "layoutOrientation", value: "vertical", isSynced: false }, { name: "backgroundEffects", value: "false", isSynced: false }, - - // HTML import configuration { name: "allowedHtmlTags", value: JSON.stringify([ From 658317799dfa812a5cacfcfb0e769265c7d6ad1f Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 18 Jan 2025 19:38:41 +0200 Subject: [PATCH 03/26] feat(options_init): enable fixed editing toolbar by default for new users --- src/services/options_init.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/options_init.ts b/src/services/options_init.ts index f4a98cf23..26f2d943f 100644 --- a/src/services/options_init.ts +++ b/src/services/options_init.ts @@ -63,6 +63,7 @@ async function initNotSyncedOptions(initialized: boolean, opts: NotSyncedOpts = optionService.createOption("lastSyncedPush", "0", false); optionService.createOption("theme", "next", false); + optionService.createOption("textNoteEditorType", "ckeditor-classic", true); optionService.createOption("syncServerHost", opts.syncServerHost || "", false); optionService.createOption("syncServerTimeout", "120000", false); From 0ccf91721d71060808c0a21c67b8ce885cae51a1 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Sat, 18 Jan 2025 19:12:50 +0100 Subject: [PATCH 04/26] test(services/data_dir): simplify getPlatformAppDataDir use the new available mocks to make tests a tiny bit more simpler :-) --- spec-es6/data_dir.spec.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/spec-es6/data_dir.spec.ts b/spec-es6/data_dir.spec.ts index ded078cb8..57abac8d3 100644 --- a/spec-es6/data_dir.spec.ts +++ b/spec-es6/data_dir.spec.ts @@ -61,27 +61,28 @@ describe("data_dir.ts unit tests", async () => { }; describe("#getPlatformAppDataDir()", () => { - type TestCaseGetPlatformAppDataDir = [description: string, fnValue: Parameters, expectedValueFn: (val: ReturnType) => boolean]; + type TestCaseGetPlatformAppDataDir = [description: string, fnValue: Parameters, expectedValue: string | null, osHomedirMockValue: string | null]; + const testCases: TestCaseGetPlatformAppDataDir[] = [ - ["w/ unsupported OS it should return 'null'", ["aix", undefined], (val) => val === null], + ["w/ unsupported OS it should return 'null'", ["aix", undefined], null, null], - ["w/ win32 and no APPDATA set it should return 'null'", ["win32", undefined], (val) => val === null], + ["w/ win32 and no APPDATA set it should return 'null'", ["win32", undefined], null, null], - ["w/ win32 and set APPDATA it should return set 'APPDATA'", ["win32", "AppData"], (val) => val === "AppData"], + ["w/ win32 and set APPDATA it should return set 'APPDATA'", ["win32", "AppData"], "AppData", null], - ["w/ linux it should return '/.local/share'", ["linux", undefined], (val) => val !== null && val.endsWith("/.local/share")], + ["w/ linux it should return '~/.local/share'", ["linux", undefined], "/home/mock/.local/share", "/home/mock"], - ["w/ linux and wrongly set APPDATA it should ignore APPDATA and return /.local/share", ["linux", "FakeAppData"], (val) => val !== null && val.endsWith("/.local/share")], + ["w/ linux and wrongly set APPDATA it should ignore APPDATA and return '~/.local/share'", ["linux", "FakeAppData"], "/home/mock/.local/share", "/home/mock"], - ["w/ darwin it should return /Library/Application Support", ["darwin", undefined], (val) => val !== null && val.endsWith("/Library/Application Support")] + ["w/ darwin it should return '~/Library/Application Support'", ["darwin", undefined], "/Users/mock/Library/Application Support", "/Users/mock"] ]; testCases.forEach((testCase) => { - const [testDescription, value, isExpected] = testCase; + const [testDescription, fnValues, expected, osHomedirMockValue] = testCase; return it(testDescription, () => { - const actual = getPlatformAppDataDir(...value); - const result = isExpected(actual); - expect(result).toBeTruthy(); + mockFn.osHomedirMock.mockReturnValue(osHomedirMockValue); + const actual = getPlatformAppDataDir(...fnValues); + expect(actual).toEqual(expected); }); }); }); From 49246ee456dbe0b58430b19951d3a8a7fdc6d298 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Sat, 18 Jan 2025 20:15:29 +0100 Subject: [PATCH 05/26] chore(tests): move tests next to actual file --- .../app/services}/attribute_parser.spec.ts | 2 +- {spec-es6 => src/services}/data_dir.spec.ts | 8 +++---- .../services}/sanitize_attribute_name.spec.ts | 2 +- .../search/services/handle_parens.spec.ts | 4 ++-- .../services/search/services/lex.spec.ts | 2 +- .../services/search/services/parse.spec.ts | 24 +++++++++---------- .../utils.formatDownloadTitle.spec.ts | 2 +- 7 files changed, 22 insertions(+), 22 deletions(-) rename {spec-es6 => src/public/app/services}/attribute_parser.spec.ts (98%) rename {spec-es6 => src/services}/data_dir.spec.ts (98%) rename {spec-es6 => src/services}/sanitize_attribute_name.spec.ts (92%) rename spec/search/parens.spec.ts => src/services/search/services/handle_parens.spec.ts (74%) rename spec/search/lexer.spec.ts => src/services/search/services/lex.spec.ts (99%) rename spec/search/parser.spec.ts => src/services/search/services/parse.spec.ts (92%) rename spec-es6/utils/formatDownloadTitle.spec.ts => src/services/utils.formatDownloadTitle.spec.ts (96%) diff --git a/spec-es6/attribute_parser.spec.ts b/src/public/app/services/attribute_parser.spec.ts similarity index 98% rename from spec-es6/attribute_parser.spec.ts rename to src/public/app/services/attribute_parser.spec.ts index 25892ec75..d8dcf09d5 100644 --- a/spec-es6/attribute_parser.spec.ts +++ b/src/public/app/services/attribute_parser.spec.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import attributeParser from "../src/public/app/services/attribute_parser.ts"; +import attributeParser from "./attribute_parser.js"; describe("Lexing", () => { diff --git a/spec-es6/data_dir.spec.ts b/src/services/data_dir.spec.ts similarity index 98% rename from spec-es6/data_dir.spec.ts rename to src/services/data_dir.spec.ts index ded078cb8..8192b8059 100644 --- a/spec-es6/data_dir.spec.ts +++ b/src/services/data_dir.spec.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; -import type { getTriliumDataDir as getTriliumDataDirType, getDataDirs as getDataDirsType, getPlatformAppDataDir as getPlatformAppDataDirType } from "../src/services/data_dir"; +import type { getTriliumDataDir as getTriliumDataDirType, getDataDirs as getDataDirsType, getPlatformAppDataDir as getPlatformAppDataDirType } from "./data_dir.js"; describe("data_dir.ts unit tests", async () => { let getTriliumDataDir: typeof getTriliumDataDirType; @@ -42,9 +42,9 @@ describe("data_dir.ts unit tests", async () => { }); // import function to test now, after creating the mocks - ({ getTriliumDataDir } = await import("../src/services/data_dir.ts")); - ({ getPlatformAppDataDir } = await import("../src/services/data_dir.ts")); - ({ getDataDirs } = await import("../src/services/data_dir.ts")); + ({ getTriliumDataDir } = await import("./data_dir.js")); + ({ getPlatformAppDataDir } = await import("./data_dir.js")); + ({ getDataDirs } = await import("./data_dir.js")); // helper to reset call counts const resetAllMocks = () => { diff --git a/spec-es6/sanitize_attribute_name.spec.ts b/src/services/sanitize_attribute_name.spec.ts similarity index 92% rename from spec-es6/sanitize_attribute_name.spec.ts rename to src/services/sanitize_attribute_name.spec.ts index 73cb5303d..dffee7ae8 100644 --- a/spec-es6/sanitize_attribute_name.spec.ts +++ b/src/services/sanitize_attribute_name.spec.ts @@ -1,5 +1,5 @@ import { expect, describe, it } from "vitest"; -import sanitizeAttributeName from "../src/services/sanitize_attribute_name"; +import sanitizeAttributeName from "./sanitize_attribute_name.js"; // fn value, expected value const testCases: [fnValue: string, expectedValue: string][] = [ diff --git a/spec/search/parens.spec.ts b/src/services/search/services/handle_parens.spec.ts similarity index 74% rename from spec/search/parens.spec.ts rename to src/services/search/services/handle_parens.spec.ts index 2cfae9e6e..be495a90c 100644 --- a/spec/search/parens.spec.ts +++ b/src/services/search/services/handle_parens.spec.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from "vitest"; -import handleParens from "../../src/services/search/services/handle_parens.js"; -import type { TokenStructure } from "../../src/services/search/services/types.js"; +import handleParens from "./handle_parens.js"; +import type { TokenStructure } from "./types.js"; describe("Parens handler", () => { it("handles parens", () => { diff --git a/spec/search/lexer.spec.ts b/src/services/search/services/lex.spec.ts similarity index 99% rename from spec/search/lexer.spec.ts rename to src/services/search/services/lex.spec.ts index 9f4a57482..06680af12 100644 --- a/spec/search/lexer.spec.ts +++ b/src/services/search/services/lex.spec.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import lex from "../../src/services/search/services/lex.js"; +import lex from "./lex.js"; describe("Lexer fulltext", () => { it("simple lexing", () => { diff --git a/spec/search/parser.spec.ts b/src/services/search/services/parse.spec.ts similarity index 92% rename from spec/search/parser.spec.ts rename to src/services/search/services/parse.spec.ts index 244fe62d2..6bde8756a 100644 --- a/spec/search/parser.spec.ts +++ b/src/services/search/services/parse.spec.ts @@ -1,16 +1,16 @@ import { describe, it, expect } from "vitest"; -import AndExp from "../../src/services/search/expressions/and.js"; -import AttributeExistsExp from "../../src/services/search/expressions/attribute_exists.js"; -import type Expression from "../../src/services/search/expressions/expression.js"; -import LabelComparisonExp from "../../src/services/search/expressions/label_comparison.js"; -import NotExp from "../../src/services/search/expressions/not.js"; -import NoteContentFulltextExp from "../../src/services/search/expressions/note_content_fulltext.js"; -import NoteFlatTextExp from "../../src/services/search/expressions/note_flat_text.js"; -import OrExp from "../../src/services/search/expressions/or.js"; -import OrderByAndLimitExp from "../../src/services/search/expressions/order_by_and_limit.js"; -import PropertyComparisonExp from "../../src/services/search/expressions/property_comparison.js"; -import SearchContext from "../../src/services/search/search_context.js"; -import { default as parseInternal, type ParseOpts } from "../../src/services/search/services/parse.js"; +import AndExp from "../../search/expressions/and.js"; +import AttributeExistsExp from "../../search/expressions/attribute_exists.js"; +import type Expression from "../../search/expressions/expression.js"; +import LabelComparisonExp from "../../search/expressions/label_comparison.js"; +import NotExp from "../../search/expressions/not.js"; +import NoteContentFulltextExp from "../../search/expressions/note_content_fulltext.js"; +import NoteFlatTextExp from "../../search/expressions/note_flat_text.js"; +import OrExp from "../../search/expressions/or.js"; +import OrderByAndLimitExp from "../../search/expressions/order_by_and_limit.js"; +import PropertyComparisonExp from "../../search/expressions/property_comparison.js"; +import SearchContext from "../../search/search_context.js"; +import { default as parseInternal, type ParseOpts } from "./parse.js"; describe("Parser", () => { it("fulltext parser without content", () => { diff --git a/spec-es6/utils/formatDownloadTitle.spec.ts b/src/services/utils.formatDownloadTitle.spec.ts similarity index 96% rename from spec-es6/utils/formatDownloadTitle.spec.ts rename to src/services/utils.formatDownloadTitle.spec.ts index d0c263646..0cc259e16 100644 --- a/spec-es6/utils/formatDownloadTitle.spec.ts +++ b/src/services/utils.formatDownloadTitle.spec.ts @@ -1,5 +1,5 @@ import { expect, describe, it } from "vitest"; -import { formatDownloadTitle } from "../../src/services/utils.ts"; +import { formatDownloadTitle } from "./utils.js"; const testCases: [fnValue: Parameters, expectedValue: ReturnType][] = [ // empty fileName tests From f803bd614081d225fa36d38014d459c270dff5ee Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Sat, 18 Jan 2025 20:16:02 +0100 Subject: [PATCH 06/26] chore(tests): move becca_mocking to `spec/support` --- spec/{search => support}/becca_mocking.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename spec/{search => support}/becca_mocking.ts (100%) diff --git a/spec/search/becca_mocking.ts b/spec/support/becca_mocking.ts similarity index 100% rename from spec/search/becca_mocking.ts rename to spec/support/becca_mocking.ts From 627b2faf0bc7ae709ce15789ec55981c85fffe6d Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Sat, 18 Jan 2025 20:20:30 +0100 Subject: [PATCH 07/26] chore(tests): move remaining valid tests next to actual files --- .../services/search/services}/search.spec.ts | 14 +++++++------- .../services}/search/value_extractor.spec.ts | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) rename {spec/search => src/services/search/services}/search.spec.ts (98%) rename {spec => src/services}/search/value_extractor.spec.ts (93%) diff --git a/spec/search/search.spec.ts b/src/services/search/services/search.spec.ts similarity index 98% rename from spec/search/search.spec.ts rename to src/services/search/services/search.spec.ts index 7df796e01..ce9acb3b0 100644 --- a/spec/search/search.spec.ts +++ b/src/services/search/services/search.spec.ts @@ -1,11 +1,11 @@ import { describe, it, expect, beforeEach, } from "vitest"; -import searchService from "../../src/services/search/services/search.js"; -import BNote from "../../src/becca/entities/bnote.js"; -import BBranch from "../../src/becca/entities/bbranch.js"; -import SearchContext from "../../src/services/search/search_context.js"; -import dateUtils from "../../src/services/date_utils.js"; -import becca from "../../src/becca/becca.js"; -import becca_mocking from "./becca_mocking.js"; +import searchService from "./search.js"; +import BNote from "../../../becca/entities/bnote.js"; +import BBranch from "../../../becca/entities/bbranch.js"; +import SearchContext from "../search_context.js"; +import dateUtils from "../../date_utils.js"; +import becca from "../../../becca/becca.js"; +import becca_mocking from "../../../../spec/support/becca_mocking.js"; describe("Search", () => { let rootNote: any; diff --git a/spec/search/value_extractor.spec.ts b/src/services/search/value_extractor.spec.ts similarity index 93% rename from spec/search/value_extractor.spec.ts rename to src/services/search/value_extractor.spec.ts index 89a2c1389..f7de5a281 100644 --- a/spec/search/value_extractor.spec.ts +++ b/src/services/search/value_extractor.spec.ts @@ -1,8 +1,8 @@ import { describe, it, expect, beforeEach } from "vitest"; -import becca_mocking from "./becca_mocking.js"; -import ValueExtractor from "../../src/services/search/value_extractor.js"; -import becca from "../../src/becca/becca.js"; -import SearchContext from "../../src/services/search/search_context.js"; +import becca_mocking from "../../../spec/support/becca_mocking.js"; +import ValueExtractor from "./value_extractor.js"; +import becca from "../../becca/becca.js"; +import SearchContext from "./search_context.js"; const dsc = new SearchContext(); From 09e0805d164079e6db3c11eb1e02bb63cfbe8c1b Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Sat, 18 Jan 2025 20:22:33 +0100 Subject: [PATCH 08/26] chore(tests): remove playwright demo spec as discussed here: https://github.com/orgs/TriliumNext/discussions/925#discussioncomment-11870786 --- tests-examples/demo-todo-app.spec.ts | 416 --------------------------- 1 file changed, 416 deletions(-) delete mode 100644 tests-examples/demo-todo-app.spec.ts diff --git a/tests-examples/demo-todo-app.spec.ts b/tests-examples/demo-todo-app.spec.ts deleted file mode 100644 index eb4ec9896..000000000 --- a/tests-examples/demo-todo-app.spec.ts +++ /dev/null @@ -1,416 +0,0 @@ -import { test, expect, type Page } from "@playwright/test"; - -test.beforeEach(async ({ page }) => { - await page.goto("https://demo.playwright.dev/todomvc"); -}); - -const TODO_ITEMS = ["buy some cheese", "feed the cat", "book a doctors appointment"] as const; - -test.describe("New Todo", () => { - test("should allow me to add todo items", async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder("What needs to be done?"); - - // Create 1st todo. - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press("Enter"); - - // Make sure the list only has one todo item. - await expect(page.getByTestId("todo-title")).toHaveText([TODO_ITEMS[0]]); - - // Create 2nd todo. - await newTodo.fill(TODO_ITEMS[1]); - await newTodo.press("Enter"); - - // Make sure the list now has two todo items. - await expect(page.getByTestId("todo-title")).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - - await checkNumberOfTodosInLocalStorage(page, 2); - }); - - test("should clear text input field when an item is added", async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder("What needs to be done?"); - - // Create one todo item. - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press("Enter"); - - // Check that input is empty. - await expect(newTodo).toBeEmpty(); - await checkNumberOfTodosInLocalStorage(page, 1); - }); - - test("should append new items to the bottom of the list", async ({ page }) => { - // Create 3 items. - await createDefaultTodos(page); - - // create a todo count locator - const todoCount = page.getByTestId("todo-count"); - - // Check test using different methods. - await expect(page.getByText("3 items left")).toBeVisible(); - await expect(todoCount).toHaveText("3 items left"); - await expect(todoCount).toContainText("3"); - await expect(todoCount).toHaveText(/3/); - - // Check all items in one call. - await expect(page.getByTestId("todo-title")).toHaveText(TODO_ITEMS); - await checkNumberOfTodosInLocalStorage(page, 3); - }); -}); - -test.describe("Mark all as completed", () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test.afterEach(async ({ page }) => { - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test("should allow me to mark all items as completed", async ({ page }) => { - // Complete all todos. - await page.getByLabel("Mark all as complete").check(); - - // Ensure all todos have 'completed' class. - await expect(page.getByTestId("todo-item")).toHaveClass(["completed", "completed", "completed"]); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - }); - - test("should allow me to clear the complete state of all items", async ({ page }) => { - const toggleAll = page.getByLabel("Mark all as complete"); - // Check and then immediately uncheck. - await toggleAll.check(); - await toggleAll.uncheck(); - - // Should be no completed classes. - await expect(page.getByTestId("todo-item")).toHaveClass(["", "", ""]); - }); - - test("complete all checkbox should update state when items are completed / cleared", async ({ page }) => { - const toggleAll = page.getByLabel("Mark all as complete"); - await toggleAll.check(); - await expect(toggleAll).toBeChecked(); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - - // Uncheck first todo. - const firstTodo = page.getByTestId("todo-item").nth(0); - await firstTodo.getByRole("checkbox").uncheck(); - - // Reuse toggleAll locator and make sure its not checked. - await expect(toggleAll).not.toBeChecked(); - - await firstTodo.getByRole("checkbox").check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - - // Assert the toggle all is checked again. - await expect(toggleAll).toBeChecked(); - }); -}); - -test.describe("Item", () => { - test("should allow me to mark items as complete", async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder("What needs to be done?"); - - // Create two items. - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press("Enter"); - } - - // Check first item. - const firstTodo = page.getByTestId("todo-item").nth(0); - await firstTodo.getByRole("checkbox").check(); - await expect(firstTodo).toHaveClass("completed"); - - // Check second item. - const secondTodo = page.getByTestId("todo-item").nth(1); - await expect(secondTodo).not.toHaveClass("completed"); - await secondTodo.getByRole("checkbox").check(); - - // Assert completed class. - await expect(firstTodo).toHaveClass("completed"); - await expect(secondTodo).toHaveClass("completed"); - }); - - test("should allow me to un-mark items as complete", async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder("What needs to be done?"); - - // Create two items. - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press("Enter"); - } - - const firstTodo = page.getByTestId("todo-item").nth(0); - const secondTodo = page.getByTestId("todo-item").nth(1); - const firstTodoCheckbox = firstTodo.getByRole("checkbox"); - - await firstTodoCheckbox.check(); - await expect(firstTodo).toHaveClass("completed"); - await expect(secondTodo).not.toHaveClass("completed"); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - await firstTodoCheckbox.uncheck(); - await expect(firstTodo).not.toHaveClass("completed"); - await expect(secondTodo).not.toHaveClass("completed"); - await checkNumberOfCompletedTodosInLocalStorage(page, 0); - }); - - test("should allow me to edit an item", async ({ page }) => { - await createDefaultTodos(page); - - const todoItems = page.getByTestId("todo-item"); - const secondTodo = todoItems.nth(1); - await secondTodo.dblclick(); - await expect(secondTodo.getByRole("textbox", { name: "Edit" })).toHaveValue(TODO_ITEMS[1]); - await secondTodo.getByRole("textbox", { name: "Edit" }).fill("buy some sausages"); - await secondTodo.getByRole("textbox", { name: "Edit" }).press("Enter"); - - // Explicitly assert the new text value. - await expect(todoItems).toHaveText([TODO_ITEMS[0], "buy some sausages", TODO_ITEMS[2]]); - await checkTodosInLocalStorage(page, "buy some sausages"); - }); -}); - -test.describe("Editing", () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test("should hide other controls when editing", async ({ page }) => { - const todoItem = page.getByTestId("todo-item").nth(1); - await todoItem.dblclick(); - await expect(todoItem.getByRole("checkbox")).not.toBeVisible(); - await expect( - todoItem.locator("label", { - hasText: TODO_ITEMS[1] - }) - ).not.toBeVisible(); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test("should save edits on blur", async ({ page }) => { - const todoItems = page.getByTestId("todo-item"); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).fill("buy some sausages"); - await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).dispatchEvent("blur"); - - await expect(todoItems).toHaveText([TODO_ITEMS[0], "buy some sausages", TODO_ITEMS[2]]); - await checkTodosInLocalStorage(page, "buy some sausages"); - }); - - test("should trim entered text", async ({ page }) => { - const todoItems = page.getByTestId("todo-item"); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).fill(" buy some sausages "); - await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).press("Enter"); - - await expect(todoItems).toHaveText([TODO_ITEMS[0], "buy some sausages", TODO_ITEMS[2]]); - await checkTodosInLocalStorage(page, "buy some sausages"); - }); - - test("should remove the item if an empty text string was entered", async ({ page }) => { - const todoItems = page.getByTestId("todo-item"); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).fill(""); - await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).press("Enter"); - - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test("should cancel edits on escape", async ({ page }) => { - const todoItems = page.getByTestId("todo-item"); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).fill("buy some sausages"); - await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).press("Escape"); - await expect(todoItems).toHaveText(TODO_ITEMS); - }); -}); - -test.describe("Counter", () => { - test("should display the current number of todo items", async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder("What needs to be done?"); - - // create a todo count locator - const todoCount = page.getByTestId("todo-count"); - - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press("Enter"); - - await expect(todoCount).toContainText("1"); - - await newTodo.fill(TODO_ITEMS[1]); - await newTodo.press("Enter"); - await expect(todoCount).toContainText("2"); - - await checkNumberOfTodosInLocalStorage(page, 2); - }); -}); - -test.describe("Clear completed button", () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - }); - - test("should display the correct text", async ({ page }) => { - await page.locator(".todo-list li .toggle").first().check(); - await expect(page.getByRole("button", { name: "Clear completed" })).toBeVisible(); - }); - - test("should remove completed items when clicked", async ({ page }) => { - const todoItems = page.getByTestId("todo-item"); - await todoItems.nth(1).getByRole("checkbox").check(); - await page.getByRole("button", { name: "Clear completed" }).click(); - await expect(todoItems).toHaveCount(2); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test("should be hidden when there are no items that are completed", async ({ page }) => { - await page.locator(".todo-list li .toggle").first().check(); - await page.getByRole("button", { name: "Clear completed" }).click(); - await expect(page.getByRole("button", { name: "Clear completed" })).toBeHidden(); - }); -}); - -test.describe("Persistence", () => { - test("should persist its data", async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder("What needs to be done?"); - - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press("Enter"); - } - - const todoItems = page.getByTestId("todo-item"); - const firstTodoCheck = todoItems.nth(0).getByRole("checkbox"); - await firstTodoCheck.check(); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - await expect(firstTodoCheck).toBeChecked(); - await expect(todoItems).toHaveClass(["completed", ""]); - - // Ensure there is 1 completed item. - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - // Now reload. - await page.reload(); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - await expect(firstTodoCheck).toBeChecked(); - await expect(todoItems).toHaveClass(["completed", ""]); - }); -}); - -test.describe("Routing", () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - // make sure the app had a chance to save updated todos in storage - // before navigating to a new view, otherwise the items can get lost :( - // in some frameworks like Durandal - await checkTodosInLocalStorage(page, TODO_ITEMS[0]); - }); - - test("should allow me to display active items", async ({ page }) => { - const todoItem = page.getByTestId("todo-item"); - await page.getByTestId("todo-item").nth(1).getByRole("checkbox").check(); - - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole("link", { name: "Active" }).click(); - await expect(todoItem).toHaveCount(2); - await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test("should respect the back button", async ({ page }) => { - const todoItem = page.getByTestId("todo-item"); - await page.getByTestId("todo-item").nth(1).getByRole("checkbox").check(); - - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - await test.step("Showing all items", async () => { - await page.getByRole("link", { name: "All" }).click(); - await expect(todoItem).toHaveCount(3); - }); - - await test.step("Showing active items", async () => { - await page.getByRole("link", { name: "Active" }).click(); - }); - - await test.step("Showing completed items", async () => { - await page.getByRole("link", { name: "Completed" }).click(); - }); - - await expect(todoItem).toHaveCount(1); - await page.goBack(); - await expect(todoItem).toHaveCount(2); - await page.goBack(); - await expect(todoItem).toHaveCount(3); - }); - - test("should allow me to display completed items", async ({ page }) => { - await page.getByTestId("todo-item").nth(1).getByRole("checkbox").check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole("link", { name: "Completed" }).click(); - await expect(page.getByTestId("todo-item")).toHaveCount(1); - }); - - test("should allow me to display all items", async ({ page }) => { - await page.getByTestId("todo-item").nth(1).getByRole("checkbox").check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole("link", { name: "Active" }).click(); - await page.getByRole("link", { name: "Completed" }).click(); - await page.getByRole("link", { name: "All" }).click(); - await expect(page.getByTestId("todo-item")).toHaveCount(3); - }); - - test("should highlight the currently applied filter", async ({ page }) => { - await expect(page.getByRole("link", { name: "All" })).toHaveClass("selected"); - - //create locators for active and completed links - const activeLink = page.getByRole("link", { name: "Active" }); - const completedLink = page.getByRole("link", { name: "Completed" }); - await activeLink.click(); - - // Page change - active items. - await expect(activeLink).toHaveClass("selected"); - await completedLink.click(); - - // Page change - completed items. - await expect(completedLink).toHaveClass("selected"); - }); -}); - -async function createDefaultTodos(page: Page) { - // create a new todo locator - const newTodo = page.getByPlaceholder("What needs to be done?"); - - for (const item of TODO_ITEMS) { - await newTodo.fill(item); - await newTodo.press("Enter"); - } -} - -async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) { - return await page.waitForFunction((e) => { - return JSON.parse(localStorage["react-todos"]).length === e; - }, expected); -} - -async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) { - return await page.waitForFunction((e) => { - return JSON.parse(localStorage["react-todos"]).filter((todo: any) => todo.completed).length === e; - }, expected); -} - -async function checkTodosInLocalStorage(page: Page, title: string) { - return await page.waitForFunction((t) => { - return JSON.parse(localStorage["react-todos"]) - .map((todo: any) => todo.title) - .includes(t); - }, title); -} From 67ee3c30fbd07354670ed8d4d139e54206bd03c4 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Sat, 18 Jan 2025 20:24:43 +0100 Subject: [PATCH 09/26] chore(tests): remove useless test stub --- spec/etapi/notes.spec.ts | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 spec/etapi/notes.spec.ts diff --git a/spec/etapi/notes.spec.ts b/spec/etapi/notes.spec.ts deleted file mode 100644 index 0d4772bf7..000000000 --- a/spec/etapi/notes.spec.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { describe, it } from "vitest"; - -describe.todo("Notes", () => { - it("zzz", () => {}); -}); From 121e7b33ecb4a2e726bc011d1fb871ba71289796 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Sat, 18 Jan 2025 20:32:40 +0100 Subject: [PATCH 10/26] chore(tests): comment out "todo" code in `attribute_parser.spec` we now have more stricter TS rules being applied to the spec files, so `tsc` fails with these now. and since this is set to TODO, because of outdated code, let's comment out, instead of using //@ts-expect-error --- src/public/app/services/attribute_parser.spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/public/app/services/attribute_parser.spec.ts b/src/public/app/services/attribute_parser.spec.ts index d8dcf09d5..7e83c53a4 100644 --- a/src/public/app/services/attribute_parser.spec.ts +++ b/src/public/app/services/attribute_parser.spec.ts @@ -41,7 +41,9 @@ describe("Lexing", () => { }); describe.todo("Parser", () => { + /* #TODO it("simple label", () => { + const attrs = attributeParser.parse(["#token"].map((t: any) => ({ text: t }))); expect(attrs.length).toEqual(1); @@ -85,6 +87,7 @@ describe.todo("Parser", () => { expect(attrs[0].name).toEqual("token"); expect(attrs[0].value).toEqual("NFi2gL4xtPxM"); }); + */ }); describe("error cases", () => { From c9619e1a1bd4d941b6e1226f374402b58ce50cd4 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Sat, 18 Jan 2025 20:45:01 +0100 Subject: [PATCH 11/26] chore(tests): fix type error in `data_dir.spec` --- src/services/data_dir.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/services/data_dir.spec.ts b/src/services/data_dir.spec.ts index 8192b8059..7ba31ea29 100644 --- a/src/services/data_dir.spec.ts +++ b/src/services/data_dir.spec.ts @@ -287,7 +287,7 @@ describe("data_dir.ts unit tests", async () => { const result = getDataDirs(`${mockValuePrefix}_TRILIUM_DATA_DIR`); for (const key in result) { - expect(result[key]).toEqual(`${mockValuePrefix}_${key}`); + expect(result[key as keyof typeof result]).toEqual(`${mockValuePrefix}_${key}`); } }); @@ -302,7 +302,7 @@ describe("data_dir.ts unit tests", async () => { const result = getDataDirs(mockDataDir); for (const key in result) { - expect(result[key].startsWith(mockDataDir)).toBeTruthy(); + expect(result[key as keyof typeof result].startsWith(mockDataDir)).toBeTruthy(); } mockFn.pathJoinMock.mockReset(); @@ -335,7 +335,7 @@ describe("data_dir.ts unit tests", async () => { if (typeof changeAttemptResult === "string") { // if it didn't throw above: assert that it did not change the value of it or any other keys of the object for (const key in result) { - expect(result[key].startsWith(mockDataDirBase)).toBeTruthy(); + expect(result[key as keyof typeof result].startsWith(mockDataDirBase)).toBeTruthy(); } } else { expect(changeAttemptResult).toBeInstanceOf(TypeError); From 6443b2ea66f44aa1075407fadcbb56229904d9da Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 19 Jan 2025 00:46:36 +0000 Subject: [PATCH 12/26] fix(deps): update dependency better-sqlite3 to v11.8.1 --- dump-db/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dump-db/package-lock.json b/dump-db/package-lock.json index fdccf601a..8c04cbd8b 100644 --- a/dump-db/package-lock.json +++ b/dump-db/package-lock.json @@ -464,9 +464,9 @@ "license": "MIT" }, "node_modules/better-sqlite3": { - "version": "11.8.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.8.0.tgz", - "integrity": "sha512-aKv9s2dir7bsEX5RIjL9HHWB9uQ+f6Vch5B4qmeAOop4Y9OYHX+PNKLr+mpv6+d8L/ZYh4l7H8zPuVMbWkVMLw==", + "version": "11.8.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.8.1.tgz", + "integrity": "sha512-9BxNaBkblMjhJW8sMRZxnxVTRgbRmssZW0Oxc1MPBTfiR+WW21e2Mk4qu8CzrcZb1LwPCnFsfDEzq+SNcBU8eg==", "hasInstallScript": true, "dependencies": { "bindings": "^1.5.0", @@ -1516,9 +1516,9 @@ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, "better-sqlite3": { - "version": "11.8.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.8.0.tgz", - "integrity": "sha512-aKv9s2dir7bsEX5RIjL9HHWB9uQ+f6Vch5B4qmeAOop4Y9OYHX+PNKLr+mpv6+d8L/ZYh4l7H8zPuVMbWkVMLw==", + "version": "11.8.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.8.1.tgz", + "integrity": "sha512-9BxNaBkblMjhJW8sMRZxnxVTRgbRmssZW0Oxc1MPBTfiR+WW21e2Mk4qu8CzrcZb1LwPCnFsfDEzq+SNcBU8eg==", "requires": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" From 66ad3779df364179cbe4322c187bad1022062081 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 19 Jan 2025 00:47:03 +0000 Subject: [PATCH 13/26] fix(deps): update react monorepo --- package-lock.json | 23 +++++++++++++---------- package.json | 4 ++-- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index f441eb0d3..251313c5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@mermaid-js/layout-elk": "0.1.7", "@mind-elixir/node-menu": "1.0.3", "@triliumnext/express-partial-content": "1.0.1", - "@types/react-dom": "18.3.1", + "@types/react-dom": "18.3.5", "archiver": "7.0.1", "async-mutex": "0.5.0", "autocomplete.js": "0.38.1", @@ -134,7 +134,7 @@ "@types/mime-types": "2.1.4", "@types/multer": "1.4.12", "@types/node": "22.10.7", - "@types/react": "18.3.1", + "@types/react": "18.3.18", "@types/safe-compare": "1.1.2", "@types/sanitize-html": "2.13.0", "@types/sax": "1.2.7", @@ -3918,6 +3918,7 @@ "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", + "dev": true, "license": "MIT" }, "node_modules/@types/qs": { @@ -3935,9 +3936,10 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.1.tgz", - "integrity": "sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==", + "version": "18.3.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", + "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -3945,12 +3947,12 @@ } }, "node_modules/@types/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", + "version": "18.3.5", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz", + "integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==", "license": "MIT", - "dependencies": { - "@types/react": "*" + "peerDependencies": { + "@types/react": "^18.0.0" } }, "node_modules/@types/readdir-glob": { @@ -6683,6 +6685,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, "license": "MIT" }, "node_modules/cytoscape": { diff --git a/package.json b/package.json index 044343879..f20a670aa 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "@mermaid-js/layout-elk": "0.1.7", "@mind-elixir/node-menu": "1.0.3", "@triliumnext/express-partial-content": "1.0.1", - "@types/react-dom": "18.3.1", + "@types/react-dom": "18.3.5", "archiver": "7.0.1", "async-mutex": "0.5.0", "autocomplete.js": "0.38.1", @@ -176,7 +176,7 @@ "@types/mime-types": "2.1.4", "@types/multer": "1.4.12", "@types/node": "22.10.7", - "@types/react": "18.3.1", + "@types/react": "18.3.18", "@types/safe-compare": "1.1.2", "@types/sanitize-html": "2.13.0", "@types/sax": "1.2.7", From 5ddd88ee550583c37937faf58cc89bff5fd43f74 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 19 Jan 2025 04:47:01 +0000 Subject: [PATCH 14/26] fix(deps): update dependency better-sqlite3 to v11.8.1 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index f441eb0d3..f0a10fec9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "async-mutex": "0.5.0", "autocomplete.js": "0.38.1", "axios": "1.7.9", - "better-sqlite3": "11.8.0", + "better-sqlite3": "11.8.1", "bootstrap": "5.3.3", "boxicons": "2.1.4", "cheerio": "1.0.0", @@ -5140,9 +5140,9 @@ "license": "MIT" }, "node_modules/better-sqlite3": { - "version": "11.8.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.8.0.tgz", - "integrity": "sha512-aKv9s2dir7bsEX5RIjL9HHWB9uQ+f6Vch5B4qmeAOop4Y9OYHX+PNKLr+mpv6+d8L/ZYh4l7H8zPuVMbWkVMLw==", + "version": "11.8.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.8.1.tgz", + "integrity": "sha512-9BxNaBkblMjhJW8sMRZxnxVTRgbRmssZW0Oxc1MPBTfiR+WW21e2Mk4qu8CzrcZb1LwPCnFsfDEzq+SNcBU8eg==", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 044343879..61b5c4e6e 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "async-mutex": "0.5.0", "autocomplete.js": "0.38.1", "axios": "1.7.9", - "better-sqlite3": "11.8.0", + "better-sqlite3": "11.8.1", "bootstrap": "5.3.3", "boxicons": "2.1.4", "cheerio": "1.0.0", From d56752e5a90d9d3b642ecc2f511ac35ee01d200e Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Sun, 19 Jan 2025 12:27:43 +0100 Subject: [PATCH 15/26] fix(utils/dynamicRequire): pass moduleName explicitly as string fixes the Webpack warning: "WARNING in ./src/public/app/services/utils.ts 249:15-34 Critical dependency: the request of a dependency is an expression" as we now explicitly pass the moduleName as string and not "expression" source: https://stackoverflow.com/questions/42908116/ --- src/public/app/services/utils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/public/app/services/utils.ts b/src/public/app/services/utils.ts index ff59d7170..7c7e47b31 100644 --- a/src/public/app/services/utils.ts +++ b/src/public/app/services/utils.ts @@ -308,7 +308,9 @@ function dynamicRequire(moduleName: string) { if (typeof __non_webpack_require__ !== "undefined") { return __non_webpack_require__(moduleName); } else { - return require(moduleName); + // explicitly pass as string and not as expression to suppress webpack warning + // 'Critical dependency: the request of a dependency is an expression' + return require(`${moduleName}`); } } From fb0dda982f75e969418f473b2b688cff558efa9d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 19 Jan 2025 23:30:07 +0200 Subject: [PATCH 16/26] fix(mindmap): restore direction --- src/public/app/widgets/type_widgets/mind_map.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/public/app/widgets/type_widgets/mind_map.ts b/src/public/app/widgets/type_widgets/mind_map.ts index 3bd72836a..ef8fdaf27 100644 --- a/src/public/app/widgets/type_widgets/mind_map.ts +++ b/src/public/app/widgets/type_widgets/mind_map.ts @@ -179,7 +179,6 @@ export default class MindMapWidget extends TypeWidget { return; } - this.#initLibrary(); await this.#loadData(note); } @@ -191,16 +190,18 @@ export default class MindMapWidget extends TypeWidget { const blob = await note.getBlob(); const content = blob?.getJsonContent() || MindElixir.new(NEW_TOPIC_NAME); - if (this.mind) { - this.mind.refresh(content); - this.mind.toCenter(); + if (!this.mind) { + this.#initLibrary(content.direction ?? MindElixir.LEFT); } + + this.mind.refresh(content); + this.mind.toCenter(); } - #initLibrary() { + #initLibrary(direction: unknown) { const mind = new MindElixir({ el: this.$content[0], - direction: MindElixir.LEFT + direction }); mind.install(nodeMenu); From 9820e8aa12b0e5e2311e28fd4d0dda272568f0cf Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 19 Jan 2025 23:34:57 +0200 Subject: [PATCH 17/26] fix(mindmap): use dynamic import instead of static one --- src/public/app/entities/fblob.ts | 2 +- .../app/widgets/type_widgets/mind_map.ts | 21 ++++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/public/app/entities/fblob.ts b/src/public/app/entities/fblob.ts index fc00e1d1f..739027d4d 100644 --- a/src/public/app/entities/fblob.ts +++ b/src/public/app/entities/fblob.ts @@ -27,7 +27,7 @@ export default class FBlob { /** * @throws Error in case of invalid JSON */ - getJsonContent(): unknown { + getJsonContent(): T | null { if (!this.content || !this.content.trim()) { return null; } diff --git a/src/public/app/widgets/type_widgets/mind_map.ts b/src/public/app/widgets/type_widgets/mind_map.ts index ef8fdaf27..91aabda79 100644 --- a/src/public/app/widgets/type_widgets/mind_map.ts +++ b/src/public/app/widgets/type_widgets/mind_map.ts @@ -1,6 +1,6 @@ import TypeWidget from "./type_widget.js"; import utils from "../../services/utils.js"; -import MindElixir, { type MindElixirCtor } from "mind-elixir"; +import type { MindElixirCtor } from "mind-elixir"; import nodeMenu from "@mind-elixir/node-menu"; import type FNote from "../../entities/fnote.js"; import type { EventData } from "../../components/app_context.js"; @@ -141,11 +141,16 @@ const TPL = ` `; +interface MindmapModel { + direction: number; +} + export default class MindMapWidget extends TypeWidget { private $content!: JQuery; private triggeredByUserOperation?: boolean; private mind?: ReturnType; + private MindElixir: any; // TODO: Fix type static getType() { return "mindMap"; @@ -188,25 +193,27 @@ export default class MindMapWidget extends TypeWidget { async #loadData(note: FNote) { const blob = await note.getBlob(); - const content = blob?.getJsonContent() || MindElixir.new(NEW_TOPIC_NAME); + const content = blob?.getJsonContent(); if (!this.mind) { - this.#initLibrary(content.direction ?? MindElixir.LEFT); + await this.#initLibrary(content?.direction ?? this.MindElixir.LEFT); } - this.mind.refresh(content); + this.mind.refresh(content ?? this.MindElixir.new(NEW_TOPIC_NAME)); this.mind.toCenter(); } - #initLibrary(direction: unknown) { - const mind = new MindElixir({ + async #initLibrary(direction: unknown) { + this.MindElixir = (await import("mind-elixir")).default; + + const mind = new this.MindElixir({ el: this.$content[0], direction }); mind.install(nodeMenu); this.mind = mind; - mind.init(MindElixir.new(NEW_TOPIC_NAME)); + mind.init(this.MindElixir.new(NEW_TOPIC_NAME)); // TODO: See why the typeof mindmap is not correct. mind.bus.addListener("operation", (operation: { name: string }) => { this.triggeredByUserOperation = true; From b47dc13ff1106bb94b037f3de7b95b2dbea33b13 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 19 Jan 2025 23:38:20 +0200 Subject: [PATCH 18/26] fix(mindmap): regression on new mindmap --- src/public/app/widgets/type_widgets/mind_map.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/public/app/widgets/type_widgets/mind_map.ts b/src/public/app/widgets/type_widgets/mind_map.ts index 91aabda79..37ca7545b 100644 --- a/src/public/app/widgets/type_widgets/mind_map.ts +++ b/src/public/app/widgets/type_widgets/mind_map.ts @@ -196,19 +196,19 @@ export default class MindMapWidget extends TypeWidget { const content = blob?.getJsonContent(); if (!this.mind) { - await this.#initLibrary(content?.direction ?? this.MindElixir.LEFT); + await this.#initLibrary(content?.direction); } this.mind.refresh(content ?? this.MindElixir.new(NEW_TOPIC_NAME)); this.mind.toCenter(); } - async #initLibrary(direction: unknown) { + async #initLibrary(direction?: number) { this.MindElixir = (await import("mind-elixir")).default; const mind = new this.MindElixir({ el: this.$content[0], - direction + direction: direction ?? this.MindElixir.LEFT }); mind.install(nodeMenu); From 7f15f8a7de9428736bff1489887b74d2e3a573dc Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 19 Jan 2025 23:48:30 +0200 Subject: [PATCH 19/26] fix(mindmap): save direction as soon as it is changed (closes #986) --- src/public/app/widgets/type_widgets/mind_map.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/public/app/widgets/type_widgets/mind_map.ts b/src/public/app/widgets/type_widgets/mind_map.ts index 37ca7545b..afa34df06 100644 --- a/src/public/app/widgets/type_widgets/mind_map.ts +++ b/src/public/app/widgets/type_widgets/mind_map.ts @@ -175,6 +175,11 @@ export default class MindMapWidget extends TypeWidget { } }); + // Save the mind map if the user changes the layout direction. + this.$content.on("click", ".mind-elixir-toolbar.lt", () => { + this.spacedUpdate.scheduleUpdate(); + }); + super.doRender(); } From 1405e22f89eed332ae30ffe5cea1d676bac78a2a Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Sun, 19 Jan 2025 11:27:19 +0100 Subject: [PATCH 20/26] test(import/mime): add tests --- src/services/import/mime.spec.ts | 146 +++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 src/services/import/mime.spec.ts diff --git a/src/services/import/mime.spec.ts b/src/services/import/mime.spec.ts new file mode 100644 index 000000000..bc2497ecc --- /dev/null +++ b/src/services/import/mime.spec.ts @@ -0,0 +1,146 @@ +import { describe, it, expect } from "vitest"; +import mimeService from "./mime.js"; + +type TestCase any, W> = [desc: string, fnParams: Parameters, expected: W]; + +describe("#getMime", () => { + // prettier-ignore + const testCases: TestCase[] = [ + [ + "Dockerfile should be handled correctly", + ["Dockerfile"], "text/x-dockerfile" + ], + + [ + "File extension that is defined in EXTENSION_TO_MIME", + ["test.py"], "text/x-python" + ], + + [ + "File extension that is not defined in EXTENSION_TO_MIME should use mimeTypes.lookup", + ["test.zip"], "application/zip" + ], + + [ + "unknown MIME type not recognized by mimeTypes.lookup", + ["test.fake"], false + ], + ]; + + testCases.forEach((testCase) => { + const [testDesc, fnParams, expected] = testCase; + it(`${testDesc}: '${fnParams} should return '${expected}'`, () => { + const actual = mimeService.getMime(...fnParams); + expect(actual).toEqual(expected); + }); + }); +}); + +describe("#getType", () => { + // prettier-ignore + const testCases: TestCase[] = [ + [ + "w/ no import options set and mime type empty – it should return 'file'", + [{}, ""], "file" + ], + + [ + "w/ no import options set and non-text or non-code mime type – it should return 'file'", + [{}, "application/zip"], "file" + ], + + [ + "w/ import options set and an image mime type – it should return 'image'", + [{}, "image/jpeg"], "image" + ], + + [ + "w/ image mime type and codeImportedAsCode: true – it should still return 'image'", + [{codeImportedAsCode: true}, "image/jpeg"], "image" + ], + + [ + "w/ image mime type and textImportedAsText: true – it should still return 'image'", + [{textImportedAsText: true}, "image/jpeg"], "image" + ], + + [ + "w/ codeImportedAsCode: true and a mime type that is in CODE_MIME_TYPES – it should return 'code'", + [{codeImportedAsCode: true}, "text/css"], "code" + ], + + [ + "w/ codeImportedAsCode: false and a mime type that is in CODE_MIME_TYPES – it should return 'file' not 'code'", + [{codeImportedAsCode: false}, "text/css"], "file" + ], + + [ + "w/ textImportedAsText: true and 'text/html' mime type – it should return 'text'", + [{textImportedAsText: true}, "text/html"], "text" + ], + + [ + "w/ textImportedAsText: true and 'text/markdown' mime type – it should return 'text'", + [{textImportedAsText: true}, "text/markdown"], "text" + ], + + [ + "w/ textImportedAsText: true and 'text/x-markdown' mime type – it should return 'text'", + [{textImportedAsText: true}, "text/x-markdown"], "text" + ], + + [ + "w/ textImportedAsText: false and 'text/x-markdown' mime type – it should return 'file'", + [{textImportedAsText: false}, "text/x-markdown"], "file" + ], + + [ + "w/ textImportedAsText: false and 'text/html' mime type – it should return 'file'", + [{textImportedAsText: false}, "text/html"], "file" + ], + + ] + + testCases.forEach((testCase) => { + const [desc, fnParams, expected] = testCase; + it(desc, () => { + const actual = mimeService.getType(...fnParams); + expect(actual).toEqual(expected); + }); + }); +}); + +describe("#normalizeMimeType", () => { + // prettier-ignore + const testCases: TestCase[] = [ + + [ + "empty mime should return undefined", + [""], undefined + ], + [ + "a mime that's defined in CODE_MIME_TYPES should return the same mime", + ["text/x-python"], "text/x-python" + ], + [ + "a mime (with capitalization inconsistencies) that's defined in CODE_MIME_TYPES should return the same mime in lowercase", + ["text/X-pYthOn"], "text/x-python" + ], + [ + "a mime that's non defined in CODE_MIME_TYPES should return undefined", + ["application/zip"], undefined + ], + [ + "a mime that's defined in CODE_MIME_TYPES with a 'rewrite rule' should return the rewritten mime", + ["text/markdown"], "text/x-markdown" + ] + ]; + + testCases.forEach((testCase) => { + const [desc, fnParams, expected] = testCase; + it(desc, () => { + const actual = mimeService.normalizeMimeType(...fnParams); + expect(actual).toEqual(expected); + }); + }); +}); From 815929c3768545c57be9bca6c5fe7831cb1e4d41 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Sun, 19 Jan 2025 14:50:59 +0100 Subject: [PATCH 21/26] refactor(import/mime): split CODE_MIME_TYPES Record into two separate objects CODE_MIME_TYPES -> as a Set -> as we only care about the existance of those types CODE_MIME_TYPES_OVERRIDE -> as a Map with those keys and the "overwrite" values as associated value -> this way we don't have to unnecessarily store additional boolean values for everything *but* those hand ful of mime types -> also I've sorted the items alphabetically, while I was at it --- src/services/import/mime.ts | 88 +++++++++++++++++++------------------ 1 file changed, 45 insertions(+), 43 deletions(-) diff --git a/src/services/import/mime.ts b/src/services/import/mime.ts index 10463c242..8fe97a5ea 100644 --- a/src/services/import/mime.ts +++ b/src/services/import/mime.ts @@ -4,45 +4,48 @@ import mimeTypes from "mime-types"; import path from "path"; import type { TaskData } from "../task_context_interface.js"; -const CODE_MIME_TYPES: Record = { - "text/plain": true, - "text/x-csrc": true, - "text/x-c++src": true, - "text/x-csharp": true, - "text/x-clojure": true, - "text/css": true, - "text/x-dockerfile": true, - "text/x-erlang": true, - "text/x-feature": true, - "text/x-go": true, - "text/x-groovy": true, - "text/x-haskell": true, - "text/html": true, - "message/http": true, - "text/x-java": true, - "application/javascript": "application/javascript;env=frontend", - "application/x-javascript": "application/javascript;env=frontend", - "application/json": true, - "text/x-kotlin": true, - "text/x-stex": true, - "text/x-lua": true, +const CODE_MIME_TYPES = new Set([ + "application/json", + "message/http", + "text/css", + "text/html", + "text/plain", + "text/x-clojure", + "text/x-csharp", + "text/x-c++src", + "text/x-csrc", + "text/x-dockerfile", + "text/x-erlang", + "text/x-feature", + "text/x-go", + "text/x-groovy", + "text/x-haskell", + "text/x-java", + "text/x-kotlin", + "text/x-lua", + "text/x-markdown", + "text/xml", + "text/x-objectivec", + "text/x-pascal", + "text/x-perl", + "text/x-php", + "text/x-python", + "text/x-ruby", + "text/x-rustsrc", + "text/x-scala", + "text/x-sh", + "text/x-sql", + "text/x-stex", + "text/x-swift", + "text/x-yaml" +]); + +const CODE_MIME_TYPES_OVERRIDE = new Map([ + ["application/javascript", "application/javascript;env=frontend"], + ["application/x-javascript", "application/javascript;env=frontend"], // possibly later migrate to text/markdown as primary MIME - "text/markdown": "text/x-markdown", - "text/x-markdown": true, - "text/x-objectivec": true, - "text/x-pascal": true, - "text/x-perl": true, - "text/x-php": true, - "text/x-python": true, - "text/x-ruby": true, - "text/x-rustsrc": true, - "text/x-scala": true, - "text/x-sh": true, - "text/x-sql": true, - "text/x-swift": true, - "text/xml": true, - "text/x-yaml": true -}; + ["text/markdown", "text/x-markdown"] +]); // extensions missing in mime-db const EXTENSION_TO_MIME: Record = { @@ -85,7 +88,7 @@ function getType(options: TaskData, mime: string) { if (options.textImportedAsText && (mime === "text/html" || ["text/markdown", "text/x-markdown"].includes(mime))) { return "text"; - } else if (options.codeImportedAsCode && mime in CODE_MIME_TYPES) { + } else if (options.codeImportedAsCode && CODE_MIME_TYPES.has(mime)) { return "code"; } else if (mime.startsWith("image/")) { return "image"; @@ -96,12 +99,11 @@ function getType(options: TaskData, mime: string) { function normalizeMimeType(mime: string) { mime = mime ? mime.toLowerCase() : ""; - const mappedMime = CODE_MIME_TYPES[mime]; - if (mappedMime === true) { + if (CODE_MIME_TYPES.has(mime)) { return mime; - } else if (typeof mappedMime === "string") { - return mappedMime; + } else if (CODE_MIME_TYPES_OVERRIDE.get(mime)) { + return CODE_MIME_TYPES_OVERRIDE.get(mime); } return undefined; From 91ae4b629e7084528c43a6d2f3738866e530c917 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Mon, 20 Jan 2025 08:18:35 +0100 Subject: [PATCH 22/26] refactor(import/mime): simplify normalizeMimeType --- src/services/import/mime.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/services/import/mime.ts b/src/services/import/mime.ts index 8fe97a5ea..5476da78c 100644 --- a/src/services/import/mime.ts +++ b/src/services/import/mime.ts @@ -40,7 +40,7 @@ const CODE_MIME_TYPES = new Set([ "text/x-yaml" ]); -const CODE_MIME_TYPES_OVERRIDE = new Map([ +const CODE_MIME_TYPES_OVERRIDE = new Map([ ["application/javascript", "application/javascript;env=frontend"], ["application/x-javascript", "application/javascript;env=frontend"], // possibly later migrate to text/markdown as primary MIME @@ -98,15 +98,12 @@ function getType(options: TaskData, mime: string) { } function normalizeMimeType(mime: string) { - mime = mime ? mime.toLowerCase() : ""; + const mimeLc = mime.toLowerCase(); - if (CODE_MIME_TYPES.has(mime)) { - return mime; - } else if (CODE_MIME_TYPES_OVERRIDE.get(mime)) { - return CODE_MIME_TYPES_OVERRIDE.get(mime); - } - - return undefined; + //prettier-ignore + return CODE_MIME_TYPES.has(mimeLc) + ? mimeLc + : CODE_MIME_TYPES_OVERRIDE.get(mimeLc); } export default { From 6a0edb68de88b0be543e54bd97a5cd78c2cb7ef9 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Mon, 20 Jan 2025 08:22:31 +0100 Subject: [PATCH 23/26] refactor(import/mime): simplify getType --- src/services/import/mime.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/services/import/mime.ts b/src/services/import/mime.ts index 5476da78c..a1b092556 100644 --- a/src/services/import/mime.ts +++ b/src/services/import/mime.ts @@ -84,16 +84,20 @@ function getMime(fileName: string) { } function getType(options: TaskData, mime: string) { - mime = mime ? mime.toLowerCase() : ""; + const mimeLc = mime?.toLowerCase(); - if (options.textImportedAsText && (mime === "text/html" || ["text/markdown", "text/x-markdown"].includes(mime))) { - return "text"; - } else if (options.codeImportedAsCode && CODE_MIME_TYPES.has(mime)) { - return "code"; - } else if (mime.startsWith("image/")) { - return "image"; - } else { - return "file"; + switch (true) { + case options.textImportedAsText && ["text/html", "text/markdown", "text/x-markdown"].includes(mimeLc): + return "text"; + + case options.codeImportedAsCode && CODE_MIME_TYPES.has(mimeLc): + return "code"; + + case mime.startsWith("image/"): + return "image"; + + default: + return "file"; } } From 4e59f58ce683d2f6edd0e3df1d94aef1c5f8f4a9 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Mon, 20 Jan 2025 08:34:22 +0100 Subject: [PATCH 24/26] refactor(import/mime): simplify getMime --- src/services/import/mime.ts | 51 ++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/src/services/import/mime.ts b/src/services/import/mime.ts index a1b092556..9930c9010 100644 --- a/src/services/import/mime.ts +++ b/src/services/import/mime.ts @@ -48,39 +48,38 @@ const CODE_MIME_TYPES_OVERRIDE = new Map([ ]); // extensions missing in mime-db -const EXTENSION_TO_MIME: Record = { - ".c": "text/x-csrc", - ".cs": "text/x-csharp", - ".clj": "text/x-clojure", - ".erl": "text/x-erlang", - ".hrl": "text/x-erlang", - ".feature": "text/x-feature", - ".go": "text/x-go", - ".groovy": "text/x-groovy", - ".hs": "text/x-haskell", - ".lhs": "text/x-haskell", - ".http": "message/http", - ".kt": "text/x-kotlin", - ".m": "text/x-objectivec", - ".py": "text/x-python", - ".rb": "text/x-ruby", - ".scala": "text/x-scala", - ".swift": "text/x-swift" -}; +const EXTENSION_TO_MIME = new Map([ + [".c", "text/x-csrc"], + [".cs", "text/x-csharp"], + [".clj", "text/x-clojure"], + [".erl", "text/x-erlang"], + [".hrl", "text/x-erlang"], + [".feature", "text/x-feature"], + [".go", "text/x-go"], + [".groovy", "text/x-groovy"], + [".hs", "text/x-haskell"], + [".lhs", "text/x-haskell"], + [".http", "message/http"], + [".kt", "text/x-kotlin"], + [".m", "text/x-objectivec"], + [".py", "text/x-python"], + [".rb", "text/x-ruby"], + [".scala", "text/x-scala"], + [".swift", "text/x-swift"] +]); /** @returns false if MIME is not detected */ function getMime(fileName: string) { - if (fileName.toLowerCase() === "dockerfile") { + const fileNameLc = fileName?.toLowerCase(); + + if (fileNameLc === "dockerfile") { return "text/x-dockerfile"; } - const ext = path.extname(fileName).toLowerCase(); + const ext = path.extname(fileNameLc); + const mimeFromExt = EXTENSION_TO_MIME.get(ext); - if (ext in EXTENSION_TO_MIME) { - return EXTENSION_TO_MIME[ext]; - } - - return mimeTypes.lookup(fileName); + return mimeFromExt || mimeTypes.lookup(fileNameLc); } function getType(options: TaskData, mime: string) { From 4be675c4e126503b755cb4a59bf84f8c84ce9b97 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Mon, 20 Jan 2025 08:34:52 +0100 Subject: [PATCH 25/26] test(import/mime): add additional test case for getMime --- src/services/import/mime.spec.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/services/import/mime.spec.ts b/src/services/import/mime.spec.ts index bc2497ecc..acebd94f7 100644 --- a/src/services/import/mime.spec.ts +++ b/src/services/import/mime.spec.ts @@ -16,6 +16,11 @@ describe("#getMime", () => { ["test.py"], "text/x-python" ], + [ + "File extension with inconsisten capitalization that is defined in EXTENSION_TO_MIME", + ["test.gRoOvY"], "text/x-groovy" + ], + [ "File extension that is not defined in EXTENSION_TO_MIME should use mimeTypes.lookup", ["test.zip"], "application/zip" From ca8146413a81c8fa83cd42178777ac34ed2ee8c7 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Mon, 20 Jan 2025 18:57:43 +0100 Subject: [PATCH 26/26] test(data_dir): fix flaky getPlatformAppDataDir test on Windows Delete the provided process.env.APPDATA on Windows, so that we can use our own values (one of which is "undefined", which was causing the getPlatformAppDataDir to fallback to the "real" process.env.APPDATA -> causing failing test, when run on Windows --- src/services/data_dir.spec.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/services/data_dir.spec.ts b/src/services/data_dir.spec.ts index 37958b185..4c94cbe94 100644 --- a/src/services/data_dir.spec.ts +++ b/src/services/data_dir.spec.ts @@ -77,6 +77,11 @@ describe("data_dir.ts unit tests", async () => { ["w/ darwin it should return '~/Library/Application Support'", ["darwin", undefined], "/Users/mock/Library/Application Support", "/Users/mock"] ]; + beforeEach(() => { + // make sure OS does not set its own process.env.APPDATA, so that we can use our own supplied value + delete process.env.APPDATA; + }); + testCases.forEach((testCase) => { const [testDescription, fnValues, expected, osHomedirMockValue] = testCase; return it(testDescription, () => {