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" diff --git a/package-lock.json b/package-lock.json index f441eb0d3..428c93da9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,12 +16,12 @@ "@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", "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", @@ -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": { @@ -5140,9 +5142,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": { @@ -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..a492db6fb 100644 --- a/package.json +++ b/package.json @@ -61,12 +61,12 @@ "@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", "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", @@ -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", 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", () => {}); -}); 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 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/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..7e83c53a4 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", () => { @@ -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", () => { 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}`); } } diff --git a/src/public/app/widgets/type_widgets/mind_map.ts b/src/public/app/widgets/type_widgets/mind_map.ts index 3bd72836a..afa34df06 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"; @@ -170,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(); } @@ -179,7 +189,6 @@ export default class MindMapWidget extends TypeWidget { return; } - this.#initLibrary(); await this.#loadData(note); } @@ -189,23 +198,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.mind.refresh(content); - this.mind.toCenter(); + if (!this.mind) { + await this.#initLibrary(content?.direction); } + + this.mind.refresh(content ?? this.MindElixir.new(NEW_TOPIC_NAME)); + this.mind.toCenter(); } - #initLibrary() { - const mind = new MindElixir({ + async #initLibrary(direction?: number) { + this.MindElixir = (await import("mind-elixir")).default; + + const mind = new this.MindElixir({ el: this.$content[0], - direction: MindElixir.LEFT + direction: direction ?? this.MindElixir.LEFT }); 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; diff --git a/spec-es6/data_dir.spec.ts b/src/services/data_dir.spec.ts similarity index 90% rename from spec-es6/data_dir.spec.ts rename to src/services/data_dir.spec.ts index ded078cb8..4c94cbe94 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 = () => { @@ -61,27 +61,33 @@ 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"] ]; + 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, 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); }); }); }); @@ -287,7 +293,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 +308,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 +341,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); diff --git a/src/services/import/mime.spec.ts b/src/services/import/mime.spec.ts new file mode 100644 index 000000000..acebd94f7 --- /dev/null +++ b/src/services/import/mime.spec.ts @@ -0,0 +1,151 @@ +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 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" + ], + + [ + "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); + }); + }); +}); diff --git a/src/services/import/mime.ts b/src/services/import/mime.ts index 10463c242..9930c9010 100644 --- a/src/services/import/mime.ts +++ b/src/services/import/mime.ts @@ -4,107 +4,109 @@ 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 = { - ".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) { - 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 && mime in CODE_MIME_TYPES) { - 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"; } } function normalizeMimeType(mime: string) { - mime = mime ? mime.toLowerCase() : ""; - const mappedMime = CODE_MIME_TYPES[mime]; + const mimeLc = mime.toLowerCase(); - if (mappedMime === true) { - return mime; - } else if (typeof mappedMime === "string") { - return mappedMime; - } - - return undefined; + //prettier-ignore + return CODE_MIME_TYPES.has(mimeLc) + ? mimeLc + : CODE_MIME_TYPES_OVERRIDE.get(mimeLc); } export default { diff --git a/src/services/options_init.ts b/src/services/options_init.ts index 7e7434d2d..26f2d943f 100644 --- a/src/services/options_init.ts +++ b/src/services/options_init.ts @@ -63,7 +63,7 @@ async function initNotSyncedOptions(initialized: boolean, opts: NotSyncedOpts = optionService.createOption("lastSyncedPush", "0", false); optionService.createOption("theme", "next", false); - optionService.createOption("layoutOrientation", "horizontal", false); + optionService.createOption("textNoteEditorType", "ckeditor-classic", true); optionService.createOption("syncServerHost", opts.syncServerHost || "", false); optionService.createOption("syncServerTimeout", "120000", false); @@ -149,11 +149,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([ 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/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(); 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 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); -} 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}}”." } }