Skip to content

Commit

Permalink
Fix DB persistence issue - we need to store an initial document state…
Browse files Browse the repository at this point in the history
… update when there's nothing in the DB

- Add test for crdt text sync in a new browser session without indexedDB state
  • Loading branch information
raimohanska committed Mar 3, 2024
1 parent 7a28055 commit f2da3e4
Show file tree
Hide file tree
Showing 8 changed files with 125 additions and 68 deletions.
7 changes: 5 additions & 2 deletions backend/src/board-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ServerSideBoardState> {
Expand Down
15 changes: 10 additions & 5 deletions backend/src/board-yjs-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
Expand Down
34 changes: 28 additions & 6 deletions playwright/src/pages/BoardPage.ts
Original file line number Diff line number Diff line change
@@ -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)
}

Expand All @@ -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")
Expand Down Expand Up @@ -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
},
}
}

Expand Down
14 changes: 7 additions & 7 deletions playwright/src/pages/DashboardPage.ts
Original file line number Diff line number Diff line change
@@ -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)
},
}
}
96 changes: 62 additions & 34 deletions playwright/src/tests/board.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -37,23 +37,21 @@ 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")
await board.assertItemColor(monoids, "rgb(253, 196, 231)")
})

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)
Expand All @@ -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)
Expand All @@ -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")
Expand All @@ -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 })
Expand All @@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -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")
Expand All @@ -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 () => {
Expand All @@ -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")

Expand Down
6 changes: 3 additions & 3 deletions playwright/src/tests/collaboration.spec.ts
Original file line number Diff line number Diff line change
@@ -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 }) => {
Expand Down Expand Up @@ -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()
Expand Down
10 changes: 5 additions & 5 deletions playwright/src/tests/dashboard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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")
})

Expand All @@ -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")
})
Expand Down
Loading

0 comments on commit f2da3e4

Please sign in to comment.