mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-08-09 17:52:32 +08:00
Merge branch 'develop' of https://github.com/TriliumNext/Notes into style/next/forms
This commit is contained in:
commit
84c03c214e
12
dump-db/package-lock.json
generated
12
dump-db/package-lock.json
generated
@ -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"
|
||||
|
31
package-lock.json
generated
31
package-lock.json
generated
@ -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": {
|
||||
|
@ -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",
|
||||
|
@ -1,5 +0,0 @@
|
||||
import { describe, it } from "vitest";
|
||||
|
||||
describe.todo("Notes", () => {
|
||||
it("zzz", () => {});
|
||||
});
|
@ -27,7 +27,7 @@ export default class FBlob {
|
||||
/**
|
||||
* @throws Error in case of invalid JSON
|
||||
*/
|
||||
getJsonContent(): unknown {
|
||||
getJsonContent<T>(): T | null {
|
||||
if (!this.content || !this.content.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
@ -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", () => {
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 = `
|
||||
</div>
|
||||
`;
|
||||
|
||||
interface MindmapModel {
|
||||
direction: number;
|
||||
}
|
||||
|
||||
export default class MindMapWidget extends TypeWidget {
|
||||
|
||||
private $content!: JQuery<HTMLElement>;
|
||||
private triggeredByUserOperation?: boolean;
|
||||
private mind?: ReturnType<MindElixirCtor["new"]>;
|
||||
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<MindmapModel>();
|
||||
|
||||
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;
|
||||
|
@ -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<typeof getPlatformAppDataDir>, expectedValueFn: (val: ReturnType<typeof getPlatformAppDataDir>) => boolean];
|
||||
type TestCaseGetPlatformAppDataDir = [description: string, fnValue: Parameters<typeof getPlatformAppDataDir>, 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);
|
151
src/services/import/mime.spec.ts
Normal file
151
src/services/import/mime.spec.ts
Normal file
@ -0,0 +1,151 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import mimeService from "./mime.js";
|
||||
|
||||
type TestCase<T extends (...args: any) => any, W> = [desc: string, fnParams: Parameters<T>, expected: W];
|
||||
|
||||
describe("#getMime", () => {
|
||||
// prettier-ignore
|
||||
const testCases: TestCase<typeof mimeService.getMime, string | false>[] = [
|
||||
[
|
||||
"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<typeof mimeService.getType, string>[] = [
|
||||
[
|
||||
"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<typeof mimeService.normalizeMimeType, string | undefined>[] = [
|
||||
|
||||
[
|
||||
"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);
|
||||
});
|
||||
});
|
||||
});
|
@ -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<string, boolean | string> = {
|
||||
"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<string, string>([
|
||||
["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<string, string> = {
|
||||
".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<string, string>([
|
||||
[".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 {
|
||||
|
@ -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([
|
||||
|
@ -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][] = [
|
@ -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", () => {
|
@ -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", () => {
|
@ -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", () => {
|
@ -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;
|
@ -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();
|
||||
|
@ -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<typeof formatDownloadTitle>, expectedValue: ReturnType<typeof formatDownloadTitle>][] = [
|
||||
// empty fileName tests
|
@ -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);
|
||||
}
|
@ -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}}”."
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user