From f2da3e4ab4d2022e3edd77d8729ea213c0158abf Mon Sep 17 00:00:00 2001 From: Juha Paananen Date: Sun, 3 Mar 2024 21:06:09 +0200 Subject: [PATCH] Fix DB persistence issue - we need to store an initial document state update when there's nothing in the DB - Add test for crdt text sync in a new browser session without indexedDB state --- backend/src/board-state.ts | 7 +- backend/src/board-yjs-server.ts | 15 ++-- playwright/src/pages/BoardPage.ts | 34 ++++++-- playwright/src/pages/DashboardPage.ts | 14 ++-- playwright/src/tests/board.spec.ts | 96 ++++++++++++++-------- playwright/src/tests/collaboration.spec.ts | 6 +- playwright/src/tests/dashboard.spec.ts | 10 +-- playwright/src/tests/navigation.spec.ts | 11 ++- 8 files changed, 125 insertions(+), 68 deletions(-) diff --git a/backend/src/board-state.ts b/backend/src/board-state.ts index feb156744..7437ea1bf 100644 --- a/backend/src/board-state.ts +++ b/backend/src/board-state.ts @@ -108,8 +108,11 @@ export function updateBoards(boardState: ServerSideBoardState, appEvent: BoardHi export function updateBoardCrdt(id: Id, crdtUpdate: Uint8Array) { const boardState = maybeGetBoard(id) - if (!boardState) throw Error(`No state for board ${id}`) - boardState.recentCrdtUpdate = combineCrdtUpdates(boardState.recentCrdtUpdate, crdtUpdate) + if (!boardState) { + console.warn("CRDT update for board not loaded into memory", id) + } else { + boardState.recentCrdtUpdate = combineCrdtUpdates(boardState.recentCrdtUpdate, crdtUpdate) + } } export async function addBoard(board: Board, createToken?: boolean): Promise { diff --git a/backend/src/board-yjs-server.ts b/backend/src/board-yjs-server.ts index 536d75013..80ca92778 100644 --- a/backend/src/board-yjs-server.ts +++ b/backend/src/board-yjs-server.ts @@ -30,14 +30,19 @@ export function BoardYJSServer(ws: expressWs.Instance, path: string) { persistence: { bindState: async (docName, ydoc) => { const boardId = docName - await withDBClient(async (client) => { - console.log(`Loading CRDT updates from DB for board ${boardId}`) - const updates = await getBoardHistoryCrdtUpdates(client, boardId) + const updates = await withDBClient(async (client) => getBoardHistoryCrdtUpdates(client, boardId)) + + if (updates.length === 0) { + const initUpdate = Y.encodeStateAsUpdate(ydoc) + console.log(`Storing initial CRDT state to DB for board ${boardId}`) + updateBoardCrdt(boardId, initUpdate) + } else { + console.log(`Loaded ${updates.length} CRDT updates from DB for board ${boardId}`) for (const update of updates) { Y.applyUpdate(ydoc, update) } - console.log(`Loaded ${updates.length} CRDT updates from DB for board ${boardId}`) - }) + } + ydoc.on("update", (update: Uint8Array, origin: any, doc: Y.Doc) => { updateBoardCrdt(boardId, update) }) diff --git a/playwright/src/pages/BoardPage.ts b/playwright/src/pages/BoardPage.ts index 1ed9ed279..e041129ec 100644 --- a/playwright/src/pages/BoardPage.ts +++ b/playwright/src/pages/BoardPage.ts @@ -1,17 +1,17 @@ -import { Locator, Page, expect, selectors } from "@playwright/test" +import { Browser, Locator, Page, expect, selectors } from "@playwright/test" import { navigateToDashboard } from "./DashboardPage" import { sleep } from "../../../common/src/sleep" import { assertNotNull } from "../../../common/src/assertNotNull" -export async function navigateToBoard(page: Page, boardId: string) { +export async function navigateToBoard(page: Page, browser: Browser, boardId: string) { selectors.setTestIdAttribute("data-test") await page.goto("http://localhost:1337/b/" + boardId) - return BoardPage(page) + return BoardPage(page, browser) } -export async function navigateToNewBoard(page: Page, boardName?: string) { +export async function navigateToNewBoard(page: Page, browser: Browser, boardName?: string) { boardName = boardName || `Test board ${semiUniqueId()}` - const dashboard = await navigateToDashboard(page) + const dashboard = await navigateToDashboard(page, browser) return await dashboard.createNewBoard(boardName) } @@ -20,7 +20,7 @@ export const semiUniqueId = () => { return now.substring(now.length - 5) } -export function BoardPage(page: Page) { +export function BoardPage(page: Page, browser: Browser) { const board = page.locator(".online .board") const newNoteOnPalette = page.getByTestId("palette-new-note") const newTextOnPalette = page.getByTestId("palette-new-text") @@ -231,6 +231,28 @@ export function BoardPage(page: Page) { }, }, contextMenu: ContextMenu(page), + async deleteIndexedDb() { + await page.evaluate(async (boardId) => { + const request = indexedDB.deleteDatabase(`b/${boardId}`) + await new Promise((resolve, reject) => { + request.onsuccess = resolve + request.onerror = reject + }) + }, this.getBoardId()) + expect + .poll(async () => { + const databases = await page.evaluate(async (boardId) => { + return await indexedDB.databases() + }, this.getBoardId()) + return databases + }) + .not.toContain(expect.objectContaining({ name: `b/${this.getBoardId()}` })) + }, + async openBoardInNewBrowser() { + const boardId = this.getBoardId() + const newBoard = await navigateToBoard(await (await browser.newContext()).newPage(), browser, boardId) + return newBoard + }, } } diff --git a/playwright/src/pages/DashboardPage.ts b/playwright/src/pages/DashboardPage.ts index cb098627e..d502d2d05 100644 --- a/playwright/src/pages/DashboardPage.ts +++ b/playwright/src/pages/DashboardPage.ts @@ -1,29 +1,29 @@ -import { Page, selectors } from "@playwright/test" +import { Browser, Page, selectors } from "@playwright/test" import { BoardPage } from "./BoardPage" -export async function navigateToDashboard(page: Page) { +export async function navigateToDashboard(page: Page, browser: Browser) { selectors.setTestIdAttribute("data-test") await page.goto("http://localhost:1337") - return DashboardPage(page) + return DashboardPage(page, browser) } -export function DashboardPage(page: Page) { +export function DashboardPage(page: Page, browser: Browser) { return { async createNewBoard(name: string) { await page.getByPlaceholder("Enter board name").fill(name) await page.getByText("use collaborative text editor").click() await page.getByRole("button", { name: "Create" }).click() - const board = BoardPage(page) + const board = BoardPage(page, browser) await board.assertBoardName(name) return board }, async goToBoard(name: string) { await page.locator(".recent-boards li").filter({ hasText: name }).first().click() - return BoardPage(page) + return BoardPage(page, browser) }, async goToTutorialBoard() { await page.getByText("Tutorial Board").click() - return BoardPage(page) + return BoardPage(page, browser) }, } } diff --git a/playwright/src/tests/board.spec.ts b/playwright/src/tests/board.spec.ts index 67a2398e5..05132034a 100644 --- a/playwright/src/tests/board.spec.ts +++ b/playwright/src/tests/board.spec.ts @@ -3,26 +3,26 @@ import { sleep } from "../../../common/src/sleep" import { navigateToNewBoard, semiUniqueId } from "../pages/BoardPage" test.describe("Basic board functionality", () => { - test("Can create note by dragging from palette", async ({ page }) => { - const board = await navigateToNewBoard(page) + test("Create note by dragging from palette", async ({ page, browser }) => { + const board = await navigateToNewBoard(page, browser) const userPageNoteText = `note-${semiUniqueId()}` await board.createNoteWithText(100, 200, userPageNoteText) }) - test("Can create text by dragging from palette", async ({ page }) => { - const board = await navigateToNewBoard(page) + test("Create text by dragging from palette", async ({ page, browser }) => { + const board = await navigateToNewBoard(page, browser) const userPageNoteText = `note-${semiUniqueId()}` await board.createText(100, 200, userPageNoteText) }) - test("Can create container by dragging from palette", async ({ page }) => { - const board = await navigateToNewBoard(page) + test("Create container by dragging from palette", async ({ page, browser }) => { + const board = await navigateToNewBoard(page, browser) const userPageNoteText = `note-${semiUniqueId()}` await board.createArea(100, 200, userPageNoteText) }) - test("Dragging notes", async ({ page }) => { - const board = await navigateToNewBoard(page) + test("Drag notes", async ({ page, browser }) => { + const board = await navigateToNewBoard(page, browser) const monoids = await board.createNoteWithText(100, 200, "Monoids") const semigroups = await board.createNoteWithText(200, 200, "Semigroups") @@ -37,14 +37,12 @@ test.describe("Basic board functionality", () => { await board.assertItemPosition(monoids, 400, 300) await board.assertItemPosition(semigroups, 300, 200) }) - - // TODO: test persistence }) // TODO: test creating and modifying connections - test("Changing note color", async ({ page }) => { - const board = await navigateToNewBoard(page) + test("Change note color", async ({ page, browser }) => { + const board = await navigateToNewBoard(page, browser) const monoids = await board.createNoteWithText(100, 200, "Monoids") const colorsAndShapes = await board.contextMenu.openColorsAndShapes() await colorsAndShapes.selectColor("pink") @@ -52,8 +50,8 @@ test.describe("Basic board functionality", () => { }) test.describe("Duplicate items", () => { - test("Duplicate text by Ctrl+D", async ({ page }) => { - const board = await navigateToNewBoard(page) + test("Duplicate text by Ctrl+D", async ({ page, browser }) => { + const board = await navigateToNewBoard(page, browser) const monoids = await board.createText(100, 200, "Monoids") const functors = await board.createNoteWithText(300, 200, "Functors") await board.selectItems(monoids, functors) @@ -62,8 +60,8 @@ test.describe("Basic board functionality", () => { await expect(functors).toHaveCount(2) }) - test("Duplicating a container with child items", async ({ page }) => { - const board = await navigateToNewBoard(page) + test("Duplicate a container with child items", async ({ page, browser }) => { + const board = await navigateToNewBoard(page, browser) const container = await board.createArea(100, 200, "Container") const text = await board.createText(150, 250, "text") await board.selectItems(container) @@ -72,8 +70,8 @@ test.describe("Basic board functionality", () => { await expect(clonedText).toHaveText("text") }) - test("Duplicating deeper hierarchy", async ({ page }) => { - const board = await navigateToNewBoard(page) + test("Duplicate deeper hierarchy", async ({ page, browser }) => { + const board = await navigateToNewBoard(page, browser) const container = await board.createArea(100, 200, "Top") await board.dragSelectionBottomCorner(550, 550) const container2 = await board.createArea(110, 220, "Middle") @@ -85,9 +83,9 @@ test.describe("Basic board functionality", () => { }) }) - test.skip("Copy, paste and cut", async ({ page }) => { + test.skip("Copy, paste and cut", async ({ page, browser }) => { // TODO: not sure how to trigger native copy, paste events - const board = await navigateToNewBoard(page) + const board = await navigateToNewBoard(page, browser) const monoids = await board.createNoteWithText(100, 200, "Monoids") await page.keyboard.press("Control+c", {}) await board.clickOnBoard({ x: 500, y: 300 }) @@ -98,8 +96,8 @@ test.describe("Basic board functionality", () => { await expect(board.getNote("Monads")).toBeVisible() }) - test("Move items with arrow keys", async ({ page }) => { - const board = await navigateToNewBoard(page) + test("Move items with arrow keys", async ({ page, browser }) => { + const board = await navigateToNewBoard(page, browser) const monoids = await board.createNoteWithText(100, 200, "Monoids") const origPos = await board.getItemPosition(monoids) await test.step("Normally", async () => { @@ -116,8 +114,8 @@ test.describe("Basic board functionality", () => { }) }) - test("Deleting notes", async ({ page }) => { - const board = await navigateToNewBoard(page) + test("Delete notes", async ({ page, browser }) => { + const board = await navigateToNewBoard(page, browser) const monoids = await board.createNoteWithText(100, 200, "Monoids") await test.step("With delete key", async () => { @@ -132,8 +130,8 @@ test.describe("Basic board functionality", () => { }) }) - test("Aligning notes", async ({ page }) => { - const board = await navigateToNewBoard(page) + test("Align notes", async ({ page, browser }) => { + const board = await navigateToNewBoard(page, browser) const n1 = await board.createNoteWithText(250, 120, "ALIGN") const n2 = await board.createNoteWithText(450, 100, "ALL") const n3 = await board.createNoteWithText(320, 250, "THESE") @@ -145,27 +143,57 @@ test.describe("Basic board functionality", () => { expect(newCoordinates.every((c) => c.x === originalCoordinates[0].x)).toBeTruthy() }) - test("Can edit note text", async ({ page }) => { - const board = await navigateToNewBoard(page) + test("Edit note text", async ({ page, browser }) => { + const board = await navigateToNewBoard(page, browser) const monoids = await board.createNoteWithText(100, 200, "Monoids") const semigroups = await board.createNoteWithText(200, 200, "Semigroups") await test.step("Change text", async () => { await board.changeItemText(board.getNote("Monoids"), "Monads") await expect(board.getNote("Monads")).toBeVisible() - await board.changeItemText(board.getNote("Monads"), "Monoids") }) await test.step("Check persistence", async () => { await sleep(1000) // Time for persistence await page.reload() - await expect(monoids).toBeVisible() + await expect(board.getNote("Monads")).toBeVisible() await expect(semigroups).toBeVisible() }) + + await test.step("Check with new session", async () => { + const newBoard = await board.openBoardInNewBrowser() + await newBoard.userInfo.dismiss() + await expect(newBoard.getNote("Monads")).toBeVisible() + }) + }) + + test("Edit area text", async ({ page, browser }) => { + const board = await navigateToNewBoard(page, browser) + const monoids = await board.createArea(100, 200, "Monoids") + const semigroups = await board.createArea(500, 200, "Semigroups") + + await test.step("Change text", async () => { + await board.changeItemText(board.getArea("Monoids"), "Monads") + await expect(board.getArea("Monads")).toBeVisible() + }) + + await test.step("Check persistence", async () => { + await sleep(1000) // Time for persistence + await page.reload() + await expect(board.getArea("Monads")).toBeVisible() + await expect(semigroups).toBeVisible() + }) + + await test.step("Check with new session", async () => { + await board.deleteIndexedDb() + const newBoard = await board.openBoardInNewBrowser() + await newBoard.userInfo.dismiss() + await expect(newBoard.getArea("Monads")).toBeVisible() + }) }) - test("Resizing notes", async ({ page }) => { - const board = await navigateToNewBoard(page) + test("Resize notes", async ({ page, browser }) => { + const board = await navigateToNewBoard(page, browser) const monoids = await board.createNoteWithText(100, 200, "Monoids") await test.step("Can drag to resize items", async () => { @@ -175,8 +203,8 @@ test.describe("Basic board functionality", () => { }) }) - test("Selecting notes", async ({ page }) => { - const board = await navigateToNewBoard(page) + test("Select notes", async ({ page, browser }) => { + const board = await navigateToNewBoard(page, browser) const monoids = await board.createNoteWithText(150, 200, "Monoids") const semigroups = await board.createNoteWithText(250, 200, "SemiGroups") diff --git a/playwright/src/tests/collaboration.spec.ts b/playwright/src/tests/collaboration.spec.ts index 7f46eb562..9e839bb31 100644 --- a/playwright/src/tests/collaboration.spec.ts +++ b/playwright/src/tests/collaboration.spec.ts @@ -1,5 +1,5 @@ import { Browser, Page, expect, test } from "@playwright/test" -import { navigateToBoard, navigateToNewBoard, semiUniqueId } from "../pages/BoardPage" +import { navigateToNewBoard, semiUniqueId } from "../pages/BoardPage" test.describe("Two simultaneous users", () => { test("two anonymous users can see each other notes", async ({ page, browser }) => { @@ -59,10 +59,10 @@ test.describe("Two simultaneous users", () => { }) async function createBoardWithTwoUsers(page: Page, browser: Browser) { - const user1Page = await navigateToNewBoard(page, "Collab test board") + const user1Page = await navigateToNewBoard(page, browser, "Collab test board") const boardId = user1Page.getBoardId() - const user2Page = await navigateToBoard(await (await browser.newContext()).newPage(), boardId) + const user2Page = await user1Page.openBoardInNewBrowser() await user1Page.userInfo.dismiss() await user2Page.userInfo.dismiss() diff --git a/playwright/src/tests/dashboard.spec.ts b/playwright/src/tests/dashboard.spec.ts index 4ab8921fb..8a3b38559 100644 --- a/playwright/src/tests/dashboard.spec.ts +++ b/playwright/src/tests/dashboard.spec.ts @@ -3,8 +3,8 @@ import { navigateToBoard } from "../pages/BoardPage" import { navigateToDashboard } from "../pages/DashboardPage" test.describe("Dashboard", () => { - test("Creating a new board", async ({ page }) => { - const dashboard = await navigateToDashboard(page) + test("Creating a new board", async ({ page, browser }) => { + const dashboard = await navigateToDashboard(page, browser) const board = await dashboard.createNewBoard("My new board") await board.assertBoardName("My new board") await board.goToDashBoard() @@ -17,7 +17,7 @@ test.describe("Dashboard", () => { await test.step("Navigating to the new board by URL", async () => { const boardId = board.getBoardId() await board.goToDashBoard() - await navigateToBoard(page, boardId) + await navigateToBoard(page, browser, boardId) await board.assertBoardName("My new board") }) @@ -27,8 +27,8 @@ test.describe("Dashboard", () => { await board.assertBoardName("My new board") }) }) - test("Personal tutorial board", async ({ page }) => { - const dashboard = await navigateToDashboard(page) + test("Personal tutorial board", async ({ page, browser }) => { + const dashboard = await navigateToDashboard(page, browser) const board = await dashboard.goToTutorialBoard() await board.assertBoardName("My personal tutorial board") }) diff --git a/playwright/src/tests/navigation.spec.ts b/playwright/src/tests/navigation.spec.ts index 48ea513d0..95b3fcf71 100644 --- a/playwright/src/tests/navigation.spec.ts +++ b/playwright/src/tests/navigation.spec.ts @@ -1,16 +1,15 @@ -import { Browser, Page, chromium, test, expect } from "@playwright/test" -import { navigateToDashboard } from "../pages/DashboardPage" +import { expect, test } from "@playwright/test" import { navigateToBoard } from "../pages/BoardPage" test.describe("Navigation", () => { - test("Navigation to default board by URL", async ({ page }) => { - const board = await navigateToBoard(page, "default") + test("Navigation to default board by URL", async ({ page, browser }) => { + const board = await navigateToBoard(page, browser, "default") await board.assertBoardName("Test Board") expect(board.getBoardId()).toBe("default") }) - test("Navigation to non-existing board by URL", async ({ page }) => { - const board = await navigateToBoard(page, "non-existing-board-id") + test("Navigation to non-existing board by URL", async ({ page, browser }) => { + const board = await navigateToBoard(page, browser, "non-existing-board-id") await board.assertStatusMessage("Board not found. A typo, maybe?") }) })