Merge remote-tracking branch 'origin/develop' into test_simplify-data-dir

This commit is contained in:
Elian Doran 2025-01-19 23:13:56 +02:00
commit 55ce7aac10
No known key found for this signature in database
23 changed files with 95 additions and 489 deletions

Binary file not shown.

View File

@ -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
View File

@ -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": {

View File

@ -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",

View File

@ -1,5 +0,0 @@
import { describe, it } from "vitest";
describe.todo("Notes", () => {
it("zzz", () => {});
});

View File

@ -1528,7 +1528,9 @@ class BNote extends AbstractBeccaEntity<BNote> {
}
isLaunchBarConfig() {
return this.type === "launcher" || ["_lbRoot", "_lbAvailableLaunchers", "_lbVisibleLaunchers"].includes(this.noteId);
return this.type === "launcher"
|| ["_lbRoot", "_lbAvailableLaunchers", "_lbVisibleLaunchers"].includes(this.noteId)
|| ["_lbMobileRoot", "_lbMobileAvailableLaunchers", "_lbMobileVisibleLaunchers"].includes(this.noteId);
}
isOptions() {

View File

@ -34,8 +34,8 @@ export default class LauncherContextMenu implements SelectMenuItemEventListener<
const isVisibleRoot = note?.noteId === "_lbVisibleLaunchers";
const isAvailableRoot = note?.noteId === "_lbAvailableLaunchers";
const isVisibleItem = parentNoteId === "_lbVisibleLaunchers";
const isAvailableItem = parentNoteId === "_lbAvailableLaunchers";
const isVisibleItem = (parentNoteId === "_lbVisibleLaunchers" || parentNoteId === "_lbMobileVisibleLaunchers");
const isAvailableItem = (parentNoteId === "_lbAvailableLaunchers" || parentNoteId === "_lbMobileAvailableLaunchers");
const isItem = isVisibleItem || isAvailableItem;
const canBeDeleted = !note?.noteId.startsWith("_"); // fixed notes can't be deleted
const canBeReset = !canBeDeleted && note?.isLaunchBarConfig();

View File

@ -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", () => {

View File

@ -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}`);
}
}

View File

@ -1636,12 +1636,24 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
}
}
moveShortcutToVisibleCommand({ node, selectedOrActiveBranchIds }) {
branchService.moveToParentNote(selectedOrActiveBranchIds, "_lbVisibleLaunchers");
moveLauncherToVisibleCommand({ selectedOrActiveBranchIds }) {
this.#moveLaunchers(selectedOrActiveBranchIds, "_lbVisibleLaunchers", "_lbMobileVisibleLaunchers");
}
moveShortcutToAvailableCommand({ node, selectedOrActiveBranchIds }) {
branchService.moveToParentNote(selectedOrActiveBranchIds, "_lbAvailableLaunchers");
moveLauncherToAvailableCommand({ selectedOrActiveBranchIds }) {
this.#moveLaunchers(selectedOrActiveBranchIds, "_lbAvailableLaunchers", "_lbMobileAvailableLaunchers");
}
#moveLaunchers(selectedOrActiveBranchIds, desktopParent, mobileParent) {
const desktopLaunchersToMove = selectedOrActiveBranchIds.filter((branchId) => !branchId.startsWith("_lbMobile"));
if (desktopLaunchersToMove) {
branchService.moveToParentNote(desktopLaunchersToMove, "_lbRoot_" + desktopParent);
}
const mobileLaunchersToMove = selectedOrActiveBranchIds.filter((branchId) => branchId.startsWith("_lbMobile"));
if (mobileLaunchersToMove) {
branchService.moveToParentNote(mobileLaunchersToMove, "_lbMobileRoot_" + mobileParent);
}
}
addNoteLauncherCommand({ node }) {

View File

@ -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 = () => {
@ -288,7 +288,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}`);
}
});
@ -303,7 +303,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();
@ -336,7 +336,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);

View File

@ -63,6 +63,7 @@ async function initNotSyncedOptions(initialized: boolean, opts: NotSyncedOpts =
optionService.createOption("lastSyncedPush", "0", false);
optionService.createOption("theme", "next", false);
optionService.createOption("textNoteEditorType", "ckeditor-classic", true);
optionService.createOption("syncServerHost", opts.syncServerHost || "", false);
optionService.createOption("syncServerTimeout", "120000", false);

View File

@ -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][] = [

View File

@ -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", () => {

View File

@ -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", () => {

View File

@ -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", () => {

View File

@ -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;

View File

@ -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();

View File

@ -213,7 +213,7 @@ function resetLauncher(noteId: string) {
if (note?.isLaunchBarConfig()) {
if (note) {
if (noteId === "_lbRoot") {
if (noteId === "_lbRoot" || noteId === "_lbMobileRoot") {
// deleting hoisted notes are not allowed, so we just reset the children
for (const childNote of note.getChildNotes()) {
childNote.deleteNote();

View File

@ -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

View File

@ -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);
}

View File

@ -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}}”."
}
}