web: fix all tests

This commit is contained in:
Abdullah Atta
2024-04-20 16:42:04 +05:00
parent 0e4cd087d4
commit f95c068bc1
38 changed files with 414 additions and 185 deletions

View File

@@ -25,7 +25,7 @@ test("delete the last note of a color", async ({ page }) => {
await app.goto();
const notes = await app.goToNotes();
const note = await notes.createNote(NOTE);
await note?.contextMenu.color("red");
await note?.contextMenu.newColor({ title: "red", color: "#ff0000" });
await app.navigation.findItem("red");
await note?.contextMenu.moveToTrash();
@@ -34,15 +34,29 @@ test("delete the last note of a color", async ({ page }) => {
expect(await app.getRouteHeader()).toBe("Trash");
});
test("remove color", async ({ page }) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
const note = await notes.createNote(NOTE);
await note?.contextMenu.newColor({ title: "red", color: "#ff0000" });
const colorItem = await app.navigation.findItem("red");
await colorItem?.removeColor();
expect(await app.navigation.findItem("red")).toBeUndefined();
expect(await note?.contextMenu.isColored("red")).toBe(false);
});
test("rename color", async ({ page }) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
const note = await notes.createNote(NOTE);
await note?.contextMenu.color("red");
await note?.contextMenu.newColor({ title: "red", color: "#ff0000" });
const colorItem = await app.navigation.findItem("red");
await colorItem?.renameColor("priority-33");
expect(await app.navigation.findItem("priority-33"));
expect(await app.navigation.findItem("priority-33")).toBeDefined();
});

View File

@@ -40,10 +40,10 @@ for (const item of [
await note?.locator.hover();
await page.mouse.down();
await navigationItem?.locator.hover();
await navigationItem?.locator.hover();
await page.waitForTimeout(1000);
await page.keyboard.press("Escape");
expect(await app.getRouteHeader()).toBe(item.title);
await expect(app.routeHeader).toHaveText(item.title);
});
}
@@ -96,8 +96,10 @@ test(`drag & drop note over a notebook should get assigned to the notebook`, asy
await note?.locator.hover();
await page.mouse.down();
await navigationItem?.locator.hover();
await navigationItem?.locator.hover();
await page.waitForTimeout(1000);
await notebook?.locator.hover();
await notebook?.locator.hover();
await page.mouse.up();
const { notes: notebookNotes } = (await notebook?.openNotebook()) || {};
@@ -120,8 +122,10 @@ test(`drag & drop note over a tag should get assigned to the tag`, async ({
await note?.locator.hover();
await page.mouse.down();
await navigationItem?.locator.hover();
await navigationItem?.locator.hover();
await page.waitForTimeout(1000);
await tag?.locator.hover();
await tag?.locator.hover();
await page.mouse.up();
const tagNotes = await tag?.open();
@@ -198,8 +202,10 @@ test(`drag & drop note over a nested notebook should get assigned to the noteboo
await note?.locator.hover();
await page.mouse.down();
await navigationItem?.locator.hover();
await navigationItem?.locator.hover();
await page.waitForTimeout(1000);
await nestedNotebook?.locator.hover();
await nestedNotebook?.locator.hover();
await page.mouse.up();
await nestedNotebook?.click();
@@ -232,10 +238,13 @@ test(`drag & hover over a nested notebook should navigate inside`, async ({
await note?.locator.hover();
await page.mouse.down();
await navigationItem?.locator.hover();
await navigationItem?.locator.hover();
await page.waitForTimeout(1000);
await notebook?.locator.hover();
await notebook?.locator.hover();
await page.waitForTimeout(1000);
await nestedNotebook?.locator.hover();
await nestedNotebook?.locator.hover();
await page.waitForTimeout(1000);
await page.keyboard.press("Escape");

View File

@@ -1 +1 @@
Test note 1 (9) Test note 1 (8) Test note 1 (7) Test note 1 (6) Test note 1 (5) Test note 1 (4) Test note 1 (3) Test note 1 (2) Test note 1 (1) Test note 1 (0) 53ad8e4e40ebebd0f400498d
53ad8e4e40ebebd0f400498dTest note 1 (0) Test note 1 (1) Test note 1 (2) Test note 1 (3) Test note 1 (4) Test note 1 (5) Test note 1 (6) Test note 1 (7) Test note 1 (8) Test note 1 (9)

View File

@@ -1 +1 @@
Test note 2 (9)Test note 2 (8)Test note 2 (7)Test note 2 (6)Test note 2 (5)Test note 2 (4)Test note 2 (3)Test note 2 (2)Test note 2 (1)Test note 2 (0)f054d19e9a2f46eff7b9bb25
f054d19e9a2f46eff7b9bb25Test note 2 (0)Test note 2 (1)Test note 2 (2)Test note 2 (3)Test note 2 (4)Test note 2 (5)Test note 2 (6)Test note 2 (7)Test note 2 (8)Test note 2 (9)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View File

@@ -1 +1 @@
An edit I madeThis is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1
This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1An edit I made

View File

@@ -75,7 +75,7 @@ export class AuthModel {
}
await this.page
.locator(getTestId("sync-status-syncing"))
.locator(getTestId("sync-status-synced"))
.waitFor({ state: "visible" });
}
}

View File

@@ -33,9 +33,6 @@ export class EditorModel {
private readonly wordCountText: Locator;
private readonly dateEditedText: Locator;
private readonly searchButton: Locator;
private readonly previewCancelButton: Locator;
private readonly previewRestoreButton: Locator;
private readonly previewNotice: Locator;
constructor(page: Page) {
this.page = page;
@@ -54,11 +51,6 @@ export class EditorModel {
this.wordCountText = page.locator(getTestId("editor-word-count"));
this.dateEditedText = page.locator(getTestId("editor-date-edited"));
this.searchButton = page.locator(getTestId("Search"));
this.previewNotice = page.locator(getTestId("preview-notice"));
this.previewCancelButton = page.locator(getTestId("preview-notice-cancel"));
this.previewRestoreButton = page.locator(
getTestId("preview-notice-restore")
);
}
async waitForLoading(title?: string, content?: string) {
@@ -126,7 +118,7 @@ export class EditorModel {
async setContent(text: string) {
await this.editAndWait(async () => {
await this.content.focus();
await this.content.type(text);
await this.content.pressSequentially(text);
});
}
@@ -228,16 +220,6 @@ export class EditorModel {
return await this.exitFullscreenButton.isVisible();
}
async cancelPreview() {
await this.previewCancelButton.click();
await this.previewNotice.waitFor({ state: "hidden" });
}
async restoreSession() {
await this.previewRestoreButton.click();
await this.previewNotice.waitFor({ state: "hidden" });
}
async getWordCount() {
return parseInt(
(await this.wordCountText.allInnerTexts())

View File

@@ -34,6 +34,7 @@ export class NavigationMenuModel {
async findItem(title: string) {
for await (const item of this.iterateList()) {
const menuItem = new NavigationItemModel(item);
if (!(await menuItem.locator.isVisible())) continue;
if ((await menuItem.getTitle()) === title) return menuItem;
}
}
@@ -87,10 +88,15 @@ class NavigationItemModel {
async renameColor(alias: string) {
await this.menu.open(this.locator);
await this.menu.clickOnItem("rename");
await this.menu.clickOnItem("rename-color");
await fillItemDialog(this.page, { title: alias });
}
async removeColor() {
await this.menu.open(this.locator);
await this.menu.clickOnItem("remove-color");
}
async removeShortcut() {
await this.menu.open(this.locator);
await this.menu.clickOnItem("remove-shortcut");

View File

@@ -47,8 +47,14 @@ export class NoteItemModel extends BaseItemModel {
async openLockedNote(password: string) {
if (!(await this.contextMenu.isLocked())) return;
await this.page.locator(getTestId("unlock-note-password")).fill(password);
await this.page.locator(getTestId("unlock-note-submit")).click();
await this.page
.locator(".active")
.locator(getTestId("unlock-note-password"))
.fill(password);
await this.page
.locator(".active")
.locator(getTestId("unlock-note-submit"))
.click();
const title = await this.getTitle();
await this.editor.waitForLoading(title);

View File

@@ -21,20 +21,16 @@ import { Locator, Page } from "@playwright/test";
import { downloadAndReadFile, getTestId } from "../utils";
import { ContextMenuModel } from "./context-menu.model";
import { ToggleModel } from "./toggle.model";
import { Notebook } from "./types";
import { Color, Notebook } from "./types";
import {
confirmDialog,
fillColorDialog,
fillNotebookDialog,
fillPasswordDialog,
iterateList
} from "./utils";
import {
FS,
ZipReader,
BlobReader,
TextWriter,
Uint8ArrayReader
} from "@zip.js/zip.js";
import { ZipReader, TextWriter, Uint8ArrayReader } from "@zip.js/zip.js";
import { SessionHistoryItemModel } from "./session-history-item-model";
abstract class BaseProperties {
protected readonly page: Page;
@@ -234,6 +230,20 @@ export class NoteContextMenuModel extends BaseProperties {
await this.close();
}
async uncolor(color: string) {
await this.open();
await this.menu.clickOnItem("colors");
await new ToggleModel(this.page, `menu-button-${color}`).off();
await this.close();
}
async newColor(color: Color) {
await this.open();
await this.menu.clickOnItem("colors");
await new ToggleModel(this.page, `menu-button-new-color`).on();
await fillColorDialog(this.page, color);
}
async moveToTrash() {
await this.open();
await Promise.all([
@@ -336,41 +346,3 @@ export class NoteContextMenuModel extends BaseProperties {
return this.menu.title();
}
}
class SessionHistoryItemModel {
private readonly title: Locator;
private readonly page: Page;
private readonly previewNotice: Locator;
private readonly locked: Locator;
constructor(
private readonly properties: NotePropertiesModel,
private readonly locator: Locator
) {
this.page = locator.page();
this.title = locator.locator(getTestId("title"));
this.previewNotice = this.page.locator(getTestId("preview-notice"));
this.locked = locator.locator(getTestId("locked"));
}
async getTitle() {
return await this.title.textContent();
}
async preview(password?: string) {
await this.properties.open();
const isLocked = await this.locked.isVisible();
await this.locator.click();
if (password && isLocked) {
await fillPasswordDialog(this.page, password);
}
await this.previewNotice.waitFor();
await this.properties.close();
}
async isLocked() {
await this.properties.open();
const state = await this.locked.isVisible();
await this.properties.close();
return state;
}
}

View File

@@ -0,0 +1,54 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Locator, Page } from "@playwright/test";
import { getTestId } from "../utils";
import { NotePropertiesModel } from "./note-properties.model";
import { SessionHistoryPreviewModel } from "./session-history-preview-model";
export class SessionHistoryItemModel {
private readonly title: Locator;
private readonly page: Page;
private readonly locked: Locator;
constructor(
private readonly properties: NotePropertiesModel,
private readonly locator: Locator
) {
this.page = locator.page();
this.title = locator.locator(getTestId("title"));
this.locked = locator.locator(getTestId("locked"));
}
async getTitle() {
return await this.title.textContent();
}
async open() {
await this.properties.open();
await this.locator.click();
return new SessionHistoryPreviewModel(this.locator);
}
async isLocked() {
await this.properties.open();
const state = await this.locked.isVisible();
await this.properties.close();
return state;
}
}

View File

@@ -0,0 +1,58 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Locator, Page } from "@playwright/test";
import { getTestId } from "../utils";
import { NotePropertiesModel } from "./note-properties.model";
export class SessionHistoryPreviewModel {
private readonly page: Page;
private readonly diffViewer: Locator;
readonly firstEditor: Locator;
readonly secondEditor: Locator;
private readonly restoreButton: Locator;
constructor(locator: Locator) {
this.page = locator.page();
this.diffViewer = this.page.locator(`.active${getTestId("diff-viewer")}`);
this.firstEditor = this.diffViewer.locator(getTestId("first-editor"));
this.secondEditor = this.diffViewer.locator(getTestId("second-editor"));
this.restoreButton = this.diffViewer.locator(getTestId("restore-session"));
}
async unlock(password: string) {
await this.diffViewer.waitFor();
if (password) {
await this.firstEditor
.locator(getTestId("unlock-note-password"))
.fill(password);
await this.firstEditor.locator(getTestId("unlock-note-submit")).click();
await this.secondEditor
.locator(getTestId("unlock-note-password"))
.fill(password);
await this.secondEditor.locator(getTestId("unlock-note-submit")).click();
}
}
async restore() {
await this.diffViewer.waitFor();
await this.restoreButton.click();
}
}

View File

@@ -27,6 +27,11 @@ export type Item = {
title: string;
};
export type Color = {
title: string;
color: string;
};
export type PriceItem = { label: string; value: string };
export type OrderByOptions = "asc" | "desc";

View File

@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Reminder } from "@notesnook/core/dist/types";
import { Locator, Page } from "@playwright/test";
import { getTestId } from "../utils";
import { Item, Notebook } from "./types";
import { Color, Item, Notebook } from "./types";
import dayjs from "dayjs";
export async function* iterateList(list: Locator) {
@@ -103,6 +103,20 @@ export async function fillItemDialog(page: Page, item: Item) {
await confirmDialog(dialog);
}
export async function fillColorDialog(page: Page, item: Color) {
const dialog = page.locator(getTestId("new-color-dialog"));
const titleInput = dialog.locator(getTestId("title-input"));
await titleInput.waitFor({ state: "visible" });
await titleInput.fill(item.title);
const colorInput = dialog.locator(getTestId("color-input"));
await colorInput.waitFor({ state: "visible" });
await colorInput.fill(item.color);
await confirmDialog(dialog);
}
export async function fillPasswordDialog(page: Page, password: string) {
const dialog = page.locator(getTestId("password-dialog"));
await dialog.locator(getTestId("password")).fill(password);

View File

@@ -21,29 +21,34 @@ import { test, expect, Page } from "@playwright/test";
import { AppModel } from "./models/app.model";
import { NOTE, PASSWORD } from "./utils";
test.setTimeout(60 * 1000);
async function createSession(page: Page, locked = false) {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
const note = await notes.createNote(NOTE);
let note = await notes.createNote(NOTE);
if (locked) await note?.contextMenu.lock(PASSWORD);
if (locked) {
await note?.contextMenu.lock(PASSWORD);
await note?.openLockedNote(PASSWORD);
}
const edits = ["Some edited text.", "Some more edited text."];
await notes.newNote();
locked ? await note?.openLockedNote(PASSWORD) : await note?.openNote();
for (const edit of edits) {
await notes.editor.setContent(edit);
await notes.newNote();
await page.waitForTimeout(600);
await page.reload().catch(console.error);
await notes.waitForItem(NOTE.title);
note = await notes.findNote(NOTE);
locked ? await note?.openLockedNote(PASSWORD) : await note?.openNote();
}
const contents = [
`${edits[1]}${edits[0]}${NOTE.content}`,
`${edits[0]}${NOTE.content}`
`${NOTE.content}${edits[0]}${edits[1]}`,
`${NOTE.content}${edits[0]}`
];
await notes.editor.waitForLoading(NOTE.title, contents[0]);
return {
note,
@@ -76,31 +81,23 @@ for (const type of sessionTypes) {
test(`switching ${type} sessions should change editor content`, async ({
page
}) => {
const { note, notes, contents } = await createSession(page, isLocked);
const { note, contents } = await createSession(page, isLocked);
const history = await note?.properties.getSessionHistory();
await history?.at(1)?.preview(PASSWORD);
await notes.editor.waitForLoading(NOTE.title, contents[1]);
const content1 = await notes.editor.getContent("text");
await history?.at(0)?.preview(PASSWORD);
await notes.editor.waitForLoading(NOTE.title, contents[0]);
const content0 = await notes.editor.getContent("text");
let preview = await history?.at(1)?.open();
if (type === "locked") await preview?.unlock(PASSWORD);
expect(content1).toBe(contents[1]);
expect(content0).toBe(contents[0]);
});
await expect(preview!.firstEditor.locator(".ProseMirror")).toHaveText(
contents[1]
);
await note?.click();
await note?.properties.close();
preview = await history?.at(0)?.open();
if (type === "locked") await preview?.unlock(PASSWORD);
test(`cancelling ${type} session restore should bring editor content back to original`, async ({
page
}) => {
const { note, notes, contents } = await createSession(page, isLocked);
const history = await note?.properties.getSessionHistory();
await history?.at(1)?.preview(PASSWORD);
await notes.editor.waitForLoading(NOTE.title, contents[1]);
await notes.editor.cancelPreview();
expect(await notes.editor.getContent("text")).toBe(contents[0]);
await expect(preview!.firstEditor.locator(".ProseMirror")).toHaveText(
contents[0]
);
});
test(`restoring a ${type} session should change note's content`, async ({
@@ -108,14 +105,13 @@ for (const type of sessionTypes) {
}) => {
const { note, notes, contents } = await createSession(page, isLocked);
const history = await note?.properties.getSessionHistory();
await history?.at(1)?.preview(PASSWORD);
await notes.editor.waitForLoading(NOTE.title, contents[1]);
const preview = await history?.at(1)?.open();
if (type === "locked") await preview?.unlock(PASSWORD);
await notes.editor.restoreSession();
await preview?.restore();
expect(await notes.editor.getContent("text")).toBe(contents[1]);
await notes.newNote();
isLocked ? await note?.openLockedNote(PASSWORD) : await note?.openNote();
await page.waitForTimeout(1000);
if (type === "locked") await note?.openLockedNote(PASSWORD);
expect(await notes.editor.getContent("text")).toBe(contents[1]);
});
}

View File

@@ -165,10 +165,12 @@ for (const actor of actors) {
await app.goto();
const notes = await app.goToNotes();
const note = await notes.createNote(NOTE);
await note?.contextMenu.newColor({ title: "red", color: "#ff0000" });
await note?.contextMenu.uncolor("red");
await note?.[actor].color("red");
const coloredNotes = await app.goToColor("Red");
const coloredNotes = await app.goToColor("red");
const coloredNote = await coloredNotes.findNote(NOTE);
expect(coloredNote).toBeDefined();
expect(await coloredNote?.contextMenu.isColored("red")).toBe(true);

View File

@@ -8,23 +8,44 @@
content="This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1"
/>
<title>Test 1 - Notesnook</title>
<meta name="created-on" content="xxx" />
<meta name="last-edited-on" content="xxx" />
<meta name="created-at" content="xxx" />
<meta name="updated-at" content="xxx" />
<link rel="stylesheet" href="https://app.notesnook.com/assets/editor-styles.css?d=0">
<style>
img {
max-width: 100% !important;
height: auto !important;
border-radius: 5px;
.image-container {
display: block;
}
.image-container.align-right {
display: flex;
justify-content: end;
}
.image-container.align-center {
display: flex;
justify-content: center;
}
.image-container.float {
float: left;
}
.image-container.float.align-right {
float: right;
}
body {
background-color: transparent !important;
color: #202124;
font-family: "Open Sans", "Noto Sans", Frutiger, Calibri, Myriad, Arial, Ubuntu, Helvetica, -apple-system, BlinkMacSystemFont, sans-serif;
}
.math-block {
padding-top: 20px;
padding-bottom: 20px;
}
h1,
h2,
@@ -158,6 +179,6 @@
</head>
<body>
<h1>Test 1</h1>
<p data-spacing="double">This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1</p>
<p data-block-id="xxx" data-spacing="double">This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1</p>
</body>
</html>

View File

@@ -2,6 +2,3 @@
This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1
---

View File

@@ -1,7 +1,3 @@
Test 1
----------
This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1
----------
Tags:
This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1

View File

@@ -90,7 +90,7 @@
},
"devDependencies": {
"@babel/core": "^7.22.5",
"@playwright/test": "^1.36.2",
"@playwright/test": "^1.43.1",
"@swc/core": "1.3.61",
"@trpc/server": "10.38.3",
"@types/babel__core": "^7.20.1",
@@ -31937,7 +31937,7 @@
},
"../desktop": {
"name": "@notesnook/desktop",
"version": "3.0.14-beta",
"version": "3.0.0",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
@@ -37020,11 +37020,12 @@
}
},
"node_modules/@playwright/test": {
"version": "1.39.0",
"version": "1.43.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.43.1.tgz",
"integrity": "sha512-HgtQzFgNEEo4TE22K/X7sYTYNqEMMTZmFS8kTq6m8hXj+m1D8TgwgIbumHddJa9h4yl4GkKb8/bgAl2+g7eDgA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.39.0"
"playwright": "1.43.1"
},
"bin": {
"playwright": "cli.js"
@@ -40290,6 +40291,20 @@
"devOptional": true,
"license": "ISC"
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"license": "MIT",
@@ -43128,11 +43143,12 @@
"license": "MIT"
},
"node_modules/playwright": {
"version": "1.39.0",
"version": "1.43.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.43.1.tgz",
"integrity": "sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.39.0"
"playwright-core": "1.43.1"
},
"bin": {
"playwright": "cli.js"
@@ -43145,9 +43161,10 @@
}
},
"node_modules/playwright-core": {
"version": "1.39.0",
"version": "1.43.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.43.1.tgz",
"integrity": "sha512-EI36Mto2Vrx6VF7rm708qSnesVQKbxEWvPrfA1IPY6HgczBplDx7ENtx+K2n4kJ41sLLkuGfmb0ZLSSXlDhqPg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},

View File

@@ -88,7 +88,7 @@
},
"devDependencies": {
"@babel/core": "^7.22.5",
"@playwright/test": "^1.36.2",
"@playwright/test": "^1.43.1",
"@swc/core": "1.3.61",
"@trpc/server": "10.38.3",
"@types/babel__core": "^7.20.1",

View File

@@ -66,9 +66,23 @@ const config: PlaywrightTestConfig = {
{
name: "Chromium",
use: {
browserName: "chromium"
browserName: "chromium",
permissions: ["notifications"]
}
}
// {
// name: "Firefox",
// use: {
// browserName: "firefox",
// permissions: ["notifications"]
// }
// },we
// {
// name: "WebKit",
// use: {
// browserName: "webkit"
// }
// }
]
};

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useLayoutEffect, useState } from "react";
import { useLayoutEffect, useRef, useState } from "react";
import { Flex, Text, Button } from "@theme-ui/components";
import { Copy, Restore } from "../icons";
import ContentToggle from "./content-toggle";
@@ -39,11 +39,11 @@ function DiffViewer(props: DiffViewerProps) {
const { session } = props;
const [selectedContent, setSelectedContent] = useState(-1);
const [content, setContent] = useState(session.content);
const [conflictedContent, setConflictedContent] = useState(
content?.conflicted
);
const root = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
setConflictedContent((c) => {
@@ -52,11 +52,21 @@ function DiffViewer(props: DiffViewerProps) {
});
}, [session]);
useLayoutEffect(() => {
const element = root.current;
element?.classList.add("active");
return () => {
element?.classList.remove("active");
};
}, []);
if (!conflictedContent || !content) return null;
return (
<Flex
ref={root}
className="diffviewer"
data-test-id="diff-viewer"
sx={{
flex: "1 1 auto",
flexDirection: "column",
@@ -81,6 +91,7 @@ function DiffViewer(props: DiffViewerProps) {
<>
<Button
variant="secondary"
data-test-id="restore-session"
onClick={async () => {
const { closeSessions, openSession } =
useEditorStore.getState();
@@ -138,6 +149,7 @@ function DiffViewer(props: DiffViewerProps) {
>
<Flex
className="firstEditor"
data-test-id="first-editor"
sx={{
flex: "1 1 auto",
flexDirection: "column",
@@ -219,6 +231,7 @@ function DiffViewer(props: DiffViewerProps) {
</Flex>
<Flex
className="secondEditor"
data-test-id="second-editor"
sx={{
flex: "1 1 auto",
flexDirection: "column",

View File

@@ -36,7 +36,8 @@ import {
NewEditorSession,
ReadonlyEditorSession,
EditorSession,
DocumentPreview
DocumentPreview,
LockedEditorSession
} from "../../stores/editor-store";
import {
useStore as useAppStore,
@@ -130,28 +131,7 @@ export default function TabsView() {
}
>
{session.type === "locked" ? (
<UnlockView
buttonTitle="Open note"
subtitle="Please enter the password to unlock this note."
title={session.note.title}
unlock={async (password) => {
const note = await db.vault.open(session.id, password);
if (!note || !note.content)
throw new Error("note with this id does not exist.");
useEditorStore.getState().addSession({
type: "default",
locked: true,
id: session.id,
note: session.note,
saveState: SaveState.Saved,
sessionId: `${Date.now()}`,
pinned: session.pinned,
preview: session.preview,
content: note.content
});
}}
/>
<UnlockNoteView session={session} />
) : session.type === "conflicted" || session.type === "diff" ? (
<DiffViewer session={session} />
) : (
@@ -748,3 +728,52 @@ function restoreSelection(editor: IEditor, id: string) {
position: Config.get(`${id}:selection`, { from: 0, to: 0 })
});
}
type UnlockNoteViewProps = { session: LockedEditorSession };
function UnlockNoteView(props: UnlockNoteViewProps) {
const { session } = props;
const root = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
const element = root.current;
element?.classList.add("active");
return () => {
element?.classList.remove("active");
};
}, []);
return (
<div
style={{
display: "flex",
overflow: "hidden",
alignItems: "center",
flex: 1
}}
ref={root}
>
<UnlockView
buttonTitle="Open note"
subtitle="Please enter the password to unlock this note."
title={session.note.title}
unlock={async (password) => {
const note = await db.vault.open(session.id, password);
if (!note || !note.content)
throw new Error("note with this id does not exist.");
useEditorStore.getState().addSession({
type: "default",
locked: true,
id: session.id,
note: session.note,
saveState: SaveState.Saved,
sessionId: `${Date.now()}`,
pinned: session.pinned,
preview: session.preview,
content: note.content
});
}}
/>
</div>
);
}

View File

@@ -150,6 +150,7 @@ function ListContainer(props: ListContainerProps) {
else selectItem(id);
},
bulkSelect: async (indices) => {
console.log(indices.length, items.length);
const ids =
indices.length === items.length
? await items.ids()

View File

@@ -596,7 +596,7 @@ function colorsToMenuItems(
const isChecked = !!noteColor && noteColor.id === color.id;
return {
type: "button",
key: color.id,
key: color.title,
title: color.title,
icon: Circle.path,
styles: { icon: { color: color.colorCode } },

View File

@@ -43,19 +43,18 @@ function Toggle(props: ToggleProps) {
"& label": { width: "auto", flexShrink: 0 }
}}
data-test-id={props.testId}
onClick={() => onToggle(!isOn)}
>
<Text
variant="body"
sx={{ alignItems: "center", color: "paragraph", display: "flex" }}
data-test-id={`toggle-state-${isOn ? "on" : "off"}`}
onClick={() => onToggle(!isOn)}
>
<ToggleIcon size={13} sx={{ flexShrink: 0, mr: 1 }} />
{label}
</Text>
<Switch
sx={{ m: 0, bg: isOn ? "accent" : "icon-secondary" }}
onClick={() => onToggle(!isOn)}
checked={isOn}
/>
</Flex>

View File

@@ -24,6 +24,7 @@ import { Perform } from "../common/dialog-controller";
import { useRef } from "react";
import tinycolor from "tinycolor2";
import { db } from "../common/db";
import { showToast } from "../utils/toast";
type CreateColorDialogProps = {
onClose: Perform;
@@ -53,6 +54,10 @@ function CreateColorDialog(props: CreateColorDialogProps) {
const form = Object.fromEntries(
new FormData(e.target as HTMLFormElement).entries()
) as { color: string; title: string };
if (!tinycolor(form.color, { format: "hex" }).isValid()) {
showToast("error", "Please enter a valid hex color (e.g. #ffffff)");
return;
}
const colorId = await db.colors.add({
colorCode: form.color,
title: form.title

View File

@@ -30,6 +30,7 @@ export function useIsUserPremium() {
}
export function isUserPremium(user?: User) {
return true;
if (IS_TESTING) return true;
if (!user) user = userstore.get().user;
if (!user) return false;

View File

@@ -130,7 +130,7 @@ export function useKeyboardListNavigation(
},
"Mod-a": () => {
resetSelection();
for (let i = 0; i < length; ++i) select(i);
bulkSelect(new Array(length).fill(0).map((_, i) => i));
return true;
},
"Shift-ArrowUp": () => {

View File

@@ -75,7 +75,7 @@ async function renderApp() {
const serviceWorkerWhitelist: Routes[] = ["default"];
async function initializeServiceWorker() {
if (!IS_DESKTOP_APP) {
if (!IS_DESKTOP_APP && !IS_TESTING) {
logger.info("Initializing service worker...");
const serviceWorker = await import("./service-worker-registration");

View File

@@ -140,12 +140,14 @@ class KeyStore extends BaseStore<KeyStore> {
) {
super(setState, get);
this.#metadataStore = isFeatureSupported("indexedDB")
? new IndexedDBKVStore(`${dbName}-metadata`, "metadata")
: new MemoryKVStore();
this.#secretStore = isFeatureSupported("indexedDB")
? new IndexedDBKVStore(`${dbName}-secrets`, "secrets")
: new MemoryKVStore();
this.#metadataStore =
isFeatureSupported("indexedDB") && isFeatureSupported("clonableCryptoKey")
? new IndexedDBKVStore(`${dbName}-metadata`, "metadata")
: new MemoryKVStore();
this.#secretStore =
isFeatureSupported("indexedDB") && isFeatureSupported("clonableCryptoKey")
? new IndexedDBKVStore(`${dbName}-secrets`, "secrets")
: new MemoryKVStore();
}
activeCredentials = () => this.get().credentials.filter((c) => c.active);

View File

@@ -20,7 +20,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
const FEATURE_CHECKS = {
opfs: false,
cache: false,
indexedDB: false
indexedDB: false,
clonableCryptoKey: false
};
async function isOPFSSupported() {
@@ -67,11 +68,26 @@ async function isIndexedDBSupported() {
);
}
async function isCryptoKeyClonable() {
const key = await window.crypto.subtle.generateKey(
{ name: "AES-KW", length: 256 },
false,
["wrapKey", "unwrapKey"]
);
try {
structuredClone(key);
FEATURE_CHECKS.clonableCryptoKey = true;
} catch {
FEATURE_CHECKS.clonableCryptoKey = false;
}
}
export async function initializeFeatureChecks() {
await Promise.allSettled([
isOPFSSupported(),
isCacheSupported(),
isIndexedDBSupported()
isIndexedDBSupported(),
isCryptoKeyClonable()
]);
}

View File

@@ -106,7 +106,7 @@ function Notebook(props: NotebookProps) {
direction="vertical"
autoSaveId={`notebook-panel-sizes:${rootId}`}
>
<Panel>
<Panel style={{ display: "flex" }}>
<Notes
header={
<NotebookHeader

View File

@@ -69,7 +69,7 @@ export function Menu(props: MenuProps) {
const item = items[focusIndex];
if (!item || !subMenuRef.current) return;
const menuItemElement = document.getElementById(`${item.key}-menu-item`);
const menuItemElement = document.getElementById(`menu-item-${item.key}`);
if (!menuItemElement) return;
if (!isSubmenuOpen) {

View File

@@ -54,7 +54,7 @@ export function MenuButton(props: MenuButtonProps) {
onMouseLeave={onMouseLeave}
>
<Button
id={`${key}-menu-item`}
id={`menu-item-${key}`}
data-test-id={`menu-button-${key}`}
key={key}
ref={itemRef}