snapshot of https://github.com/raimohanska/ourboard.git (master, 2026-09-23; blobs > 131072B omitted)
This commit is contained in:
86
playwright/src/pages/BoardApi.ts
Normal file
86
playwright/src/pages/BoardApi.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Page, expect, test } from "@playwright/test"
|
||||
|
||||
export function BoardApi(page: Page) {
|
||||
return {
|
||||
async getBoard(accessToken: string, boardId: string) {
|
||||
const response = await page.request.get(`/api/v1/board/${boardId}`, {
|
||||
headers: {
|
||||
API_TOKEN: accessToken,
|
||||
},
|
||||
})
|
||||
return response.json()
|
||||
},
|
||||
async getBoardHierarchy(accessToken: string, boardId: string) {
|
||||
const response = await page.request.get(`/api/v1/board/${boardId}/hierarchy`, {
|
||||
headers: {
|
||||
API_TOKEN: accessToken,
|
||||
},
|
||||
})
|
||||
return response.json()
|
||||
},
|
||||
async getBoardHistory(accessToken: string, boardId: string) {
|
||||
const response = await page.request.get(`/api/v1/board/${boardId}/history`, {
|
||||
headers: {
|
||||
API_TOKEN: accessToken,
|
||||
},
|
||||
})
|
||||
return response.json()
|
||||
},
|
||||
async getBoardCsv(accessToken: string, boardId: string) {
|
||||
const response = await page.request.get(`/api/v1/board/${boardId}/csv`, {
|
||||
headers: {
|
||||
API_TOKEN: accessToken,
|
||||
},
|
||||
})
|
||||
return response.text()
|
||||
},
|
||||
async createNote(accessToken: string, boardId: string, text: string, attributes?: object) {
|
||||
return await test.step("Add item " + text, async () => {
|
||||
const response = await page.request.post(`/api/v1/board/${boardId}/item`, {
|
||||
data: {
|
||||
type: "note",
|
||||
text,
|
||||
color: "#000000",
|
||||
...attributes,
|
||||
},
|
||||
headers: {
|
||||
API_TOKEN: accessToken,
|
||||
},
|
||||
})
|
||||
expect(response.status()).toEqual(200)
|
||||
return await response.json()
|
||||
})
|
||||
},
|
||||
async createBoard(data: any) {
|
||||
const { id, accessToken } = await test.step("Create board", async () => {
|
||||
const response = await page.request.post("/api/v1/board", {
|
||||
data,
|
||||
})
|
||||
return await response.json()
|
||||
})
|
||||
return { id, accessToken }
|
||||
},
|
||||
async updateBoard(accessToken: string, boardId: string, data: any) {
|
||||
await test.step("Update board", async () => {
|
||||
const response = await page.request.put(`/api/v1/board/${boardId}`, {
|
||||
data,
|
||||
headers: {
|
||||
API_TOKEN: accessToken,
|
||||
},
|
||||
})
|
||||
expect(response.status()).toEqual(200)
|
||||
})
|
||||
},
|
||||
async updateItem(accessToken: string, boardId: string, itemId: string, data: any) {
|
||||
await test.step("Update item", async () => {
|
||||
const response = await page.request.put(`/api/v1/board/${boardId}/item/${itemId}`, {
|
||||
data,
|
||||
headers: {
|
||||
API_TOKEN: accessToken,
|
||||
},
|
||||
})
|
||||
expect(response.status()).toEqual(200)
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
375
playwright/src/pages/BoardPage.ts
Normal file
375
playwright/src/pages/BoardPage.ts
Normal file
@@ -0,0 +1,375 @@
|
||||
import { Browser, Locator, Page, expect, selectors, test } from "@playwright/test"
|
||||
import { DashboardPage, navigateToDashboard } from "./DashboardPage"
|
||||
import { sleep } from "../../../common/src/sleep"
|
||||
import { assertNotNull } from "../../../common/src/assertNotNull"
|
||||
|
||||
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, browser)
|
||||
}
|
||||
|
||||
export type NewBoardOptions = Partial<{
|
||||
boardName: string
|
||||
useCRDT: boolean
|
||||
}>
|
||||
|
||||
export const navigateToNewBoard = (page: Page, browser: Browser, options?: NewBoardOptions) =>
|
||||
test.step("Create new board", async () => {
|
||||
const dashboard = await navigateToDashboard(page, browser)
|
||||
return await dashboard.createNewBoard(options)
|
||||
})
|
||||
|
||||
export const semiUniqueId = () => {
|
||||
const now = String(Date.now())
|
||||
return now.substring(now.length - 5)
|
||||
}
|
||||
|
||||
export type BoardPage = ReturnType<typeof BoardPage>
|
||||
export function BoardPage(page: Page, browser: Browser) {
|
||||
const board = page.locator(".online .board")
|
||||
const boardName = page.locator("#board-name").locator(`[contentEditable]`)
|
||||
const newNoteOnPalette = page.getByTestId("palette-new-note")
|
||||
const newTextOnPalette = page.getByTestId("palette-new-text")
|
||||
const newContainerOnPalette = page.getByTestId("palette-new-container")
|
||||
const scrollContainer = page.locator(`div [class="scroll-container"]`)
|
||||
async function waitForThrottle() {
|
||||
await sleep(50) // for UI throttling to take effect
|
||||
}
|
||||
|
||||
async function moveMouseTo(newPos: { x: number; y: number }) {
|
||||
const clientPos = await itemToClientPos(newPos)
|
||||
await page.mouse.move(clientPos.clientX, clientPos.clientY, { steps: 10 })
|
||||
await waitForThrottle()
|
||||
}
|
||||
|
||||
async function itemToClientPos(itemPos: { x: number; y: number }) {
|
||||
const scrollContainer = page.locator(`div [class="scroll-container"]`)
|
||||
const scPos = assertNotNull(await scrollContainer.boundingBox())
|
||||
return { clientX: itemPos.x + scPos.x, clientY: itemPos.y + scPos.y }
|
||||
}
|
||||
|
||||
async function clientToElementPos(clientPos: { clientX: number; clientY: number }) {
|
||||
const scrollContainer = page.locator(`div [class="scroll-container"]`)
|
||||
const scPos = assertNotNull(await scrollContainer.boundingBox())
|
||||
return { x: Math.round(clientPos.clientX - scPos.x), y: Math.round(clientPos.clientY - scPos.y) }
|
||||
}
|
||||
|
||||
async function getElementPosition(item: Locator) {
|
||||
const scPos = assertNotNull(await scrollContainer.boundingBox())
|
||||
const clientPos = assertNotNull(await item.boundingBox())
|
||||
return await clientToElementPos({
|
||||
clientX: clientPos.x + clientPos.width / 2,
|
||||
clientY: clientPos.y + clientPos.height / 2,
|
||||
})
|
||||
}
|
||||
|
||||
async function getElementSize(item: Locator) {
|
||||
const { width, height } = assertNotNull(await item.boundingBox())
|
||||
return { width, height }
|
||||
}
|
||||
|
||||
async function createNew(paletteItem: Locator, x: number, y: number) {
|
||||
await expect(paletteItem).toBeVisible()
|
||||
await paletteItem.dispatchEvent("dragstart")
|
||||
await paletteItem.dispatchEvent("dragover")
|
||||
await moveMouseTo({ x, y })
|
||||
await paletteItem.dispatchEvent("dragend")
|
||||
}
|
||||
|
||||
async function dragElementOnBoard(element: Locator, x: number, y: number) {
|
||||
return await test.step(`Drag item to (${x}, ${y})`, async () => {
|
||||
const itemPos = await getElementPosition(element)
|
||||
await moveMouseTo(itemPos)
|
||||
await element.dispatchEvent("dragstart", await itemToClientPos(itemPos))
|
||||
await element.dispatchEvent("drag")
|
||||
page.locator(`.online .board`).dispatchEvent("dragover", await itemToClientPos(itemPos))
|
||||
await moveMouseTo({ x, y })
|
||||
page.locator(`.online .board`).dispatchEvent("dragover", await itemToClientPos({ x, y }))
|
||||
await waitForThrottle()
|
||||
await element.dispatchEvent("drag")
|
||||
await element.dispatchEvent("dragend")
|
||||
})
|
||||
}
|
||||
|
||||
async function selectText(el: Locator, text: string) {
|
||||
await test.step("Select text " + text, async () => {
|
||||
// below code selects the given word from the line. text is the word I want to //select
|
||||
await el.evaluate((element, text: string) => {
|
||||
const selection = window.getSelection()!
|
||||
const content = (element as HTMLElement).innerText
|
||||
const range = document.createRange()
|
||||
const index = content.indexOf(text)
|
||||
if (index === -1) {
|
||||
throw Error(`Text ${text} not found in ${content}`)
|
||||
}
|
||||
const textNode = element.firstChild!
|
||||
console.log("Textnode", textNode.textContent)
|
||||
range.setStart(textNode, index)
|
||||
console.log("Length", (textNode.textContent as any).length)
|
||||
range.setEnd(textNode, index + text.length)
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(range)
|
||||
}, text)
|
||||
})
|
||||
}
|
||||
|
||||
async function selectAll(el: Locator) {
|
||||
const textToSelect = (await el.textContent()) ?? ""
|
||||
selectText(el, textToSelect)
|
||||
}
|
||||
|
||||
return {
|
||||
page,
|
||||
board,
|
||||
scrollContainer,
|
||||
newNoteOnPalette,
|
||||
newTextOnPalette,
|
||||
newContainerOnPalette,
|
||||
cloneButton: page.locator(".tool.duplicate"),
|
||||
getBoardId() {
|
||||
return assertNotNull(page.url().split("/").pop())
|
||||
},
|
||||
async assertBoardName(name: string) {
|
||||
await expect(boardName).toHaveText(name)
|
||||
},
|
||||
async getBoardName() {
|
||||
return assertNotNull(await boardName.textContent())
|
||||
},
|
||||
async renameBoard(name: string) {
|
||||
await test.step("Rename board", async () => {
|
||||
await boardName.click()
|
||||
await selectAll(boardName)
|
||||
await boardName.pressSequentially(name)
|
||||
await boardName.press("Enter")
|
||||
await this.assertBoardName(name)
|
||||
})
|
||||
},
|
||||
async assertStatusMessage(message: string) {
|
||||
await expect(page.locator(".board-status-message")).toHaveText(message)
|
||||
},
|
||||
async cloneBoard() {
|
||||
await page.getByTitle("Make a copy").click()
|
||||
},
|
||||
async goToDashBoard() {
|
||||
await page.getByRole("link", { name: "All boards" }).click()
|
||||
return DashboardPage(page, browser)
|
||||
},
|
||||
async createNoteWithText(x: number, y: number, text: string) {
|
||||
return await test.step("Create note " + text, async () => {
|
||||
await createNew(this.newNoteOnPalette, x, y)
|
||||
const elt = page.locator(".note.selected .editable")
|
||||
await expect(elt).toBeVisible()
|
||||
await elt.fill(text)
|
||||
await page.keyboard.press("Escape")
|
||||
await expect(this.getNote(text)).toBeVisible()
|
||||
const result = this.getNote(text)
|
||||
await expect(result).toHaveText(text)
|
||||
return result
|
||||
})
|
||||
},
|
||||
async createText(x: number, y: number, text: string) {
|
||||
return await test.step("Create text " + text, async () => {
|
||||
await createNew(this.newTextOnPalette, x, y)
|
||||
await this.getText("HELLO")
|
||||
.locator(".text")
|
||||
.click({ position: { x: 5, y: 5 } })
|
||||
await page.keyboard.press("Delete")
|
||||
await page.keyboard.press("Delete")
|
||||
await page.keyboard.press("Delete")
|
||||
await page.keyboard.press("Delete")
|
||||
await page.keyboard.press("Delete")
|
||||
await page.keyboard.type(`${text}`)
|
||||
await expect(this.getText(text)).toBeVisible()
|
||||
const result = this.getText(text)
|
||||
await expect(result).toHaveText(text)
|
||||
return result
|
||||
})
|
||||
},
|
||||
async createArea(x: number, y: number, text: string) {
|
||||
return await test.step("Create area " + text, async () => {
|
||||
await createNew(this.newContainerOnPalette, x, y)
|
||||
await this.getArea("Unnamed area").locator(".text").dblclick()
|
||||
await page.locator("*:focus").fill(text)
|
||||
await expect(this.getArea(text)).toBeVisible()
|
||||
const result = this.getArea(text)
|
||||
await expect(result).toHaveText(text)
|
||||
return result
|
||||
})
|
||||
},
|
||||
async dragItem(item: Locator, x: number, y: number) {
|
||||
await dragElementOnBoard(item, x, y)
|
||||
},
|
||||
async dragSelectionBottomCorner(x: number, y: number) {
|
||||
const bottomCorner = board.locator(".corner-resize-drag.bottom.right")
|
||||
await dragElementOnBoard(bottomCorner, x, y)
|
||||
},
|
||||
getNote(name: string) {
|
||||
return page.locator(`.board > .note`).filter({ hasText: name })
|
||||
},
|
||||
getText(name: string) {
|
||||
return page.locator(`.board > .text`).filter({ hasText: name })
|
||||
},
|
||||
getArea(name: string) {
|
||||
return page.locator(`.board > .container`).filter({ hasText: name })
|
||||
},
|
||||
async assertItemPosition(item: Locator, x: number, y: number) {
|
||||
return await test.step(`Assert item position at (${x}, ${y})`, async () => {
|
||||
const pos = await getElementPosition(item)
|
||||
expect(pos.x).toBeGreaterThan(x - 5)
|
||||
expect(pos.x).toBeLessThan(x + 5)
|
||||
expect(pos.y).toBeGreaterThan(y - 5)
|
||||
expect(pos.y).toBeLessThan(y + 5)
|
||||
})
|
||||
},
|
||||
async assertItemSize(item: Locator, width: number, height: number) {
|
||||
return await test.step(`Assert item size (${width}, ${height})`, async () => {
|
||||
const size = await getElementSize(item)
|
||||
expect(size.width).toBeGreaterThan(width - 5)
|
||||
expect(size.width).toBeLessThan(width + 5)
|
||||
expect(size.height).toBeGreaterThan(height - 5)
|
||||
expect(size.height).toBeLessThan(height + 5)
|
||||
})
|
||||
},
|
||||
async getItemPosition(item: Locator) {
|
||||
return await getElementPosition(item)
|
||||
},
|
||||
async assertItemColor(item: Locator, color: string) {
|
||||
await expect(item.locator(".shape")).toHaveCSS("background-color", color)
|
||||
},
|
||||
|
||||
async changeItemText(item: Locator, text: string) {
|
||||
await item.click()
|
||||
await item.locator(".text").dblclick()
|
||||
await page.keyboard.type(text)
|
||||
},
|
||||
async assertSelected(item: Locator, selected: boolean = true) {
|
||||
if (selected) {
|
||||
await expect(item).toHaveClass(/selected/)
|
||||
} else {
|
||||
await expect(item).not.toHaveClass(/selected/)
|
||||
}
|
||||
},
|
||||
async assertLocked(item: Locator, locked: boolean = true) {
|
||||
if (locked) {
|
||||
await expect(item).toHaveClass(/locked/)
|
||||
} else {
|
||||
await expect(item).not.toHaveClass(/locked/)
|
||||
}
|
||||
},
|
||||
async clickOnBoard(position: { x: number; y: number }) {
|
||||
await scrollContainer.click({ position })
|
||||
},
|
||||
async dragOnBoard(
|
||||
from: { x: number; y: number },
|
||||
to: { x: number; y: number },
|
||||
options: { altKey?: boolean } = {},
|
||||
) {
|
||||
const startDrag: DragEventInit = { ...(await itemToClientPos(from)), ...options }
|
||||
const endDrag: DragEventInit = { ...(await itemToClientPos(to)), ...options }
|
||||
|
||||
await moveMouseTo(from)
|
||||
await this.board.dispatchEvent("dragstart", startDrag)
|
||||
await this.board.dispatchEvent("drag", startDrag)
|
||||
await this.board.dispatchEvent("dragover", startDrag)
|
||||
|
||||
await this.board.dispatchEvent("dragover", endDrag)
|
||||
await waitForThrottle()
|
||||
await this.board.dispatchEvent("drag", endDrag)
|
||||
await this.board.dispatchEvent("dragend", endDrag)
|
||||
},
|
||||
async selectItems(...items: Locator[]) {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
await this.assertLocked(items[i], false)
|
||||
if (i === 0) {
|
||||
await items[i].click({ position: { x: 5, y: 5 } })
|
||||
} else {
|
||||
await items[i].click({ modifiers: ["Shift"], position: { x: 5, y: 5 } })
|
||||
}
|
||||
await this.assertSelected(items[i], true)
|
||||
}
|
||||
},
|
||||
async setNickname(nickname: string) {
|
||||
await page
|
||||
.locator(".user-info .icon")
|
||||
.or(page.locator(".user-info .nickname"))
|
||||
.first()
|
||||
.click({ force: true })
|
||||
await page.locator(".user-info .nickname input").fill(nickname)
|
||||
await page.locator(".user-info button").click()
|
||||
},
|
||||
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 () => {
|
||||
try {
|
||||
const databases = await page.evaluate(async (boardId) => {
|
||||
return await indexedDB.databases()
|
||||
}, this.getBoardId())
|
||||
return databases
|
||||
} catch (e) {
|
||||
console.warn("indexedDB.databases call failed. This is not supported in Firefox")
|
||||
return []
|
||||
}
|
||||
})
|
||||
.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
|
||||
},
|
||||
async setOfflineMode(offline: boolean) {
|
||||
await page.evaluate((offline) => {
|
||||
;(window as any).forceOffline.set(offline)
|
||||
}, offline)
|
||||
if (offline) {
|
||||
await expect(page.locator(".offline-status")).toBeVisible()
|
||||
} else {
|
||||
await expect(page.locator(".offline-status")).not.toBeVisible()
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function ContextMenu(page: Page) {
|
||||
const contextMenu = page.locator(".context-menu")
|
||||
return {
|
||||
async openColorsAndShapes() {
|
||||
await contextMenu.locator(".colors-shapes .icon").click()
|
||||
return {
|
||||
async selectColor(color: string) {
|
||||
await page.locator(`.submenu .colors .icon.${color}`).click()
|
||||
},
|
||||
}
|
||||
},
|
||||
async openHorizontalAlign() {
|
||||
await contextMenu.locator(".icon-group.align .icon").first().click()
|
||||
const submenu = page.locator(`.submenu.alignment.x`)
|
||||
return {
|
||||
left: submenu.getByTitle("Align left"),
|
||||
center: submenu.getByTitle("Align center"),
|
||||
right: submenu.getByTitle("Align right"),
|
||||
}
|
||||
},
|
||||
async openVerticalAlign() {
|
||||
await contextMenu.locator(".icon-group.align .icon").nth(1).click()
|
||||
const submenu = page.locator(`.submenu.alignment.y`)
|
||||
return {
|
||||
top: submenu.getByTitle("Align top"),
|
||||
middle: submenu.getByTitle("Align middle"),
|
||||
bottom: submenu.getByTitle("Align bottom"),
|
||||
}
|
||||
},
|
||||
async toggleContentsHidden() {
|
||||
await contextMenu.locator(".visibility .icon").click()
|
||||
},
|
||||
}
|
||||
}
|
||||
37
playwright/src/pages/DashboardPage.ts
Normal file
37
playwright/src/pages/DashboardPage.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Browser, Page, selectors, test } from "@playwright/test"
|
||||
import { BoardPage, semiUniqueId, NewBoardOptions } from "./BoardPage"
|
||||
|
||||
export async function navigateToDashboard(page: Page, browser: Browser) {
|
||||
selectors.setTestIdAttribute("data-test")
|
||||
await page.goto("http://localhost:1337")
|
||||
return DashboardPage(page, browser)
|
||||
}
|
||||
|
||||
export function DashboardPage(page: Page, browser: Browser) {
|
||||
return {
|
||||
async createNewBoard(options?: NewBoardOptions) {
|
||||
return await test.step("Create new board", async () => {
|
||||
const name = options?.boardName ?? `Test board ${semiUniqueId()}`
|
||||
const useCRDT = options?.useCRDT ?? true
|
||||
|
||||
await page.getByPlaceholder("Enter board name").fill(name)
|
||||
if (useCRDT) {
|
||||
await page.getByText("use collaborative text editor").click()
|
||||
}
|
||||
await page.getByRole("button", { name: "Create" }).click()
|
||||
const board = BoardPage(page, browser)
|
||||
// TODO: this is flaky in at least "Switching between boards"
|
||||
await board.assertBoardName(name)
|
||||
return board
|
||||
})
|
||||
},
|
||||
async goToBoard(name: string) {
|
||||
await page.locator(".recent-boards li").filter({ hasText: name }).first().click()
|
||||
return BoardPage(page, browser)
|
||||
},
|
||||
async goToTutorialBoard() {
|
||||
await page.getByText("Tutorial Board").click()
|
||||
return BoardPage(page, browser)
|
||||
},
|
||||
}
|
||||
}
|
||||
139
playwright/src/tests/accessPolicy.spec.ts
Normal file
139
playwright/src/tests/accessPolicy.spec.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { Page, expect, test } from "@playwright/test"
|
||||
import { sleep } from "../../../common/src/sleep"
|
||||
import { navigateToBoard } from "../pages/BoardPage"
|
||||
|
||||
async function loginAsTester(page: Page) {
|
||||
await test.step("Login as tester", async () => {
|
||||
await page.request.get("/test-callback")
|
||||
})
|
||||
}
|
||||
|
||||
async function logout(page: Page) {
|
||||
await test.step("Logout", async () => {
|
||||
await page.request.get("/logout")
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: test creating accessPolicy through UI
|
||||
// TODO: test changing accessPolicy through UI
|
||||
|
||||
test.describe("Board access policy", () => {
|
||||
test("Create board with accessPolicy using API", async ({ page, browser }) => {
|
||||
await test.step("With empty accessPolicy", async () => {
|
||||
const board = await test.step("Create board and navigate", async () => {
|
||||
const response = await page.request.post("/api/v1/board", {
|
||||
data: {
|
||||
name: "API restricted board",
|
||||
accessPolicy: {
|
||||
allowList: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
const { id, accessToken } = await response.json()
|
||||
return await navigateToBoard(page, browser, id)
|
||||
})
|
||||
|
||||
await test.step("Verify no UI access", async () => {
|
||||
await board.assertStatusMessage("This board is for authorized users only. Click here to sign in.")
|
||||
await loginAsTester(page)
|
||||
await page.reload()
|
||||
await board.assertStatusMessage("Sorry, access denied. Click here to sign in with another account.")
|
||||
await logout(page)
|
||||
})
|
||||
})
|
||||
|
||||
await test.step("With non-empty accessPolicy", async () => {
|
||||
const board = await test.step("Create board and navigate", async () => {
|
||||
const response = await page.request.post("/api/v1/board", {
|
||||
data: {
|
||||
name: "API restricted board",
|
||||
accessPolicy: {
|
||||
allowList: [{ email: "ourboardtester@test.com" }],
|
||||
},
|
||||
},
|
||||
})
|
||||
const { id, accessToken } = await response.json()
|
||||
return await navigateToBoard(page, browser, id)
|
||||
})
|
||||
|
||||
await test.step("Verify restricted access", async () => {
|
||||
await board.assertStatusMessage("This board is for authorized users only. Click here to sign in.")
|
||||
|
||||
await loginAsTester(page)
|
||||
|
||||
await page.reload()
|
||||
await board.assertBoardName("API restricted board")
|
||||
})
|
||||
|
||||
await test.step("Rename board through UI", async () => {
|
||||
await board.renameBoard("API restricted board renamed")
|
||||
await sleep(1000)
|
||||
await page.reload()
|
||||
await board.assertBoardName("API restricted board renamed")
|
||||
})
|
||||
|
||||
await test.step("Verify restricted access", async () => {
|
||||
await logout(page)
|
||||
await page.reload()
|
||||
await board.assertStatusMessage("This board is for authorized users only. Click here to sign in.")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test("Create restricted board with public read access using API", async ({ page, browser }) => {
|
||||
const { id, accessToken } = await test.step("Create board and navigate", async () => {
|
||||
const response = await page.request.post("/api/v1/board", {
|
||||
data: {
|
||||
name: "API board with public read",
|
||||
accessPolicy: {
|
||||
publicRead: true,
|
||||
allowList: [{ email: "ourboardtester@test.com" }],
|
||||
},
|
||||
crdt: true,
|
||||
},
|
||||
})
|
||||
return await response.json()
|
||||
})
|
||||
|
||||
await loginAsTester(page)
|
||||
const board = await navigateToBoard(page, browser, id)
|
||||
|
||||
await test.step("Create content as authorized user", async () => {
|
||||
await board.assertBoardName("API board with public read")
|
||||
await board.createNoteWithText(100, 200, "Test note")
|
||||
await board.createArea(100, 400, "Test area with CRDT")
|
||||
})
|
||||
|
||||
await test.step("Verify read-only access", async () => {
|
||||
await logout(page)
|
||||
await page.reload()
|
||||
await expect(board.getNote("Test note")).toBeVisible()
|
||||
await expect(board.getArea("Test area with CRDT")).toBeVisible()
|
||||
|
||||
await board.changeItemText(board.getNote("Test note"), "Updated note")
|
||||
await board.changeItemText(board.getArea("Test area with CRDT"), "I should not be able to do this")
|
||||
|
||||
await expect(board.getArea("I should not be able to do this")).not.toBeVisible()
|
||||
})
|
||||
|
||||
await test.step("Remove public read access", async () => {
|
||||
const newAccessPolicy = {
|
||||
allowList: [{ email: "ourboardtester@test.com" }],
|
||||
publicRead: false,
|
||||
}
|
||||
const response = await page.request.put(`/api/v1/board/${id}`, {
|
||||
data: {
|
||||
name: "Updated board name",
|
||||
accessPolicy: newAccessPolicy,
|
||||
},
|
||||
headers: {
|
||||
API_TOKEN: accessToken,
|
||||
},
|
||||
})
|
||||
expect(response.status()).toEqual(200)
|
||||
|
||||
await page.reload()
|
||||
await board.assertStatusMessage("This board is for authorized users only. Click here to sign in.")
|
||||
})
|
||||
})
|
||||
})
|
||||
221
playwright/src/tests/api.spec.ts
Normal file
221
playwright/src/tests/api.spec.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
import { Page, expect, test } from "@playwright/test"
|
||||
import { Note } from "../../../common/src/domain"
|
||||
import { sleep } from "../../../common/src/sleep"
|
||||
import { navigateToBoard, semiUniqueId } from "../pages/BoardPage"
|
||||
import { BoardApi } from "../pages/BoardApi"
|
||||
|
||||
test.describe("API endpoints", () => {
|
||||
test("Create and update board", async ({ page, browser }) => {
|
||||
const Api = BoardApi(page)
|
||||
const { id, accessToken } = await Api.createBoard({ name: "API test board" })
|
||||
|
||||
const board = await navigateToBoard(page, browser, id)
|
||||
|
||||
await test.step("Check board name", async () => {
|
||||
await board.assertBoardName("API test board")
|
||||
const userPageNoteText = `note-${semiUniqueId()}`
|
||||
await board.createNoteWithText(100, 200, userPageNoteText)
|
||||
await board.createArea(100, 400, "API notes")
|
||||
await board.createArea(550, 400, "More API notes")
|
||||
})
|
||||
|
||||
await test.step("Set board name", async () => {
|
||||
await Api.updateBoard(accessToken, id, {
|
||||
name: "Updated board name",
|
||||
})
|
||||
await board.assertBoardName("Updated board name")
|
||||
})
|
||||
|
||||
const item = await Api.createNote(accessToken, id, "API note", { container: "API notes" })
|
||||
|
||||
await expect(board.getNote("API note")).toBeVisible()
|
||||
|
||||
await test.step("Update item", async () => {
|
||||
await Api.updateItem(accessToken, id, item.id, {
|
||||
type: "note",
|
||||
text: "Updated item",
|
||||
color: "#000000",
|
||||
})
|
||||
await expect(board.getNote("Updated item")).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step("Change item container", async () => {
|
||||
await board.assertItemPosition(board.getNote("Updated item"), 163, 460)
|
||||
await Api.updateItem(accessToken, id, item.id, {
|
||||
type: "note",
|
||||
text: "Updated item",
|
||||
color: "#000000",
|
||||
container: "More API notes",
|
||||
})
|
||||
await sleep(1000)
|
||||
await board.assertItemPosition(board.getNote("Updated item"), 613, 460)
|
||||
})
|
||||
|
||||
const itemWithCoordinates = await test.step("Create new item with position and size", async () => {
|
||||
const itemWithCoordinates = await Api.createNote(accessToken, id, "API New note", {
|
||||
x: 100,
|
||||
y: 200,
|
||||
width: 15,
|
||||
height: 30,
|
||||
})
|
||||
|
||||
const noteElement = board.getNote("API New note")
|
||||
await expect(noteElement).toBeVisible()
|
||||
const style = await noteElement.getAttribute("style")
|
||||
expect(style).toContain("transform: translate(100em, 200em)")
|
||||
expect(style).toContain("width: 15em")
|
||||
expect(style).toContain("height: 30em")
|
||||
return itemWithCoordinates
|
||||
})
|
||||
|
||||
await test.step("Update item position and size", async () => {
|
||||
await Api.updateItem(accessToken, id, itemWithCoordinates.id, {
|
||||
x: 20,
|
||||
y: 20,
|
||||
type: "note",
|
||||
text: "Updated new item",
|
||||
color: "#000000",
|
||||
width: 10,
|
||||
height: 10,
|
||||
})
|
||||
const noteElement = board.getNote("Updated new item")
|
||||
await expect(noteElement).toBeVisible()
|
||||
const style = await noteElement.getAttribute("style")
|
||||
expect(style).toContain("transform: translate(20em, 20em)")
|
||||
expect(style).toContain("width: 10em")
|
||||
expect(style).toContain("height: 10em")
|
||||
})
|
||||
|
||||
await test.step("Get board state", async () => {
|
||||
expect
|
||||
.poll(async () => await Api.getBoard(accessToken, id))
|
||||
.toEqual({
|
||||
board: {
|
||||
id,
|
||||
name: "Updated board name",
|
||||
width: 800,
|
||||
height: 600,
|
||||
serial: expect.anything(),
|
||||
connections: [],
|
||||
items: expect.anything(),
|
||||
},
|
||||
})
|
||||
|
||||
const content = await Api.getBoard(accessToken, id)
|
||||
expect(Object.values(content.board.items)).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
type: "container",
|
||||
text: "API notes",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
type: "note",
|
||||
text: "Updated item",
|
||||
}),
|
||||
]),
|
||||
)
|
||||
})
|
||||
|
||||
await test.step("Get board state hierarchy", async () => {
|
||||
expect
|
||||
.poll(async () => await Api.getBoardHierarchy(accessToken, id))
|
||||
.toEqual({
|
||||
board: {
|
||||
id,
|
||||
name: "Updated board name",
|
||||
width: 800,
|
||||
height: 600,
|
||||
serial: expect.anything(),
|
||||
connections: [],
|
||||
items: expect.anything(),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
await test.step("Get board history", async () => {
|
||||
const history = (await Api.getBoardHistory(accessToken, id)).history
|
||||
expect(history.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
await test.step("Get board as CSV", async () => {
|
||||
// CSV only includes items that are in containers
|
||||
expect(await Api.getBoardCsv(accessToken, id)).toEqual("More API notes,Updated item\n")
|
||||
})
|
||||
|
||||
await test.step("Set accessPolicy", async () => {
|
||||
await Api.updateBoard(accessToken, id, {
|
||||
name: "Updated board name",
|
||||
accessPolicy: {
|
||||
allowList: [],
|
||||
publicRead: false,
|
||||
publicWrite: false,
|
||||
},
|
||||
})
|
||||
await page.reload()
|
||||
await board.assertStatusMessage("This board is for authorized users only. Click here to sign in.")
|
||||
|
||||
expect((await Api.getBoard(accessToken, id)).board.accessPolicy).toEqual({
|
||||
allowList: [],
|
||||
publicRead: false,
|
||||
publicWrite: false,
|
||||
})
|
||||
})
|
||||
|
||||
await test.step("Update accessPolicy", async () => {
|
||||
const newAccessPolicy = {
|
||||
allowList: [{ email: "ourboardtester@test.com" }],
|
||||
publicRead: true,
|
||||
publicWrite: true,
|
||||
}
|
||||
await Api.updateBoard(accessToken, id, {
|
||||
name: "Updated board name",
|
||||
accessPolicy: newAccessPolicy,
|
||||
})
|
||||
await page.reload()
|
||||
await expect(board.getNote("Updated item")).toBeVisible()
|
||||
|
||||
expect((await Api.getBoard(accessToken, id)).board.accessPolicy).toEqual(newAccessPolicy)
|
||||
})
|
||||
})
|
||||
|
||||
test("Get board contents with CRDT text", async ({ page, browser }) => {
|
||||
const Api = BoardApi(page)
|
||||
const { id, accessToken } = await Api.createBoard({ name: "API test board", crdt: true })
|
||||
|
||||
const board = await navigateToBoard(page, browser, id)
|
||||
await board.createText(100, 200, "CRDT text")
|
||||
await board.createNoteWithText(100, 400, "Simple note")
|
||||
|
||||
await test.step("Get board state", async () => {
|
||||
const content = await Api.getBoard(accessToken, id)
|
||||
expect(Object.values(content.board.items)).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
type: "text",
|
||||
text: "CRDT text",
|
||||
textAsDelta: [{ insert: "CRDT text" }],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
type: "note",
|
||||
text: "Simple note",
|
||||
}),
|
||||
]),
|
||||
)
|
||||
expect((Object.values(content.board.items)[1] as Note).textAsDelta).toBeUndefined()
|
||||
})
|
||||
|
||||
await test.step("Get board hierarchy", async () => {
|
||||
const content = await Api.getBoardHierarchy(accessToken, id)
|
||||
|
||||
expect(Object.values(content.board.items)).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
type: "text",
|
||||
text: "CRDT text",
|
||||
textAsDelta: [{ insert: "CRDT text" }],
|
||||
}),
|
||||
]),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
333
playwright/src/tests/board.spec.ts
Normal file
333
playwright/src/tests/board.spec.ts
Normal file
@@ -0,0 +1,333 @@
|
||||
import { Browser, Page, expect, test } from "@playwright/test"
|
||||
import { sleep } from "../../../common/src/sleep"
|
||||
import { BoardPage, navigateToNewBoard, semiUniqueId } from "../pages/BoardPage"
|
||||
import { BoardApi } from "../pages/BoardApi"
|
||||
|
||||
test.describe("Basic board functionality", () => {
|
||||
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)
|
||||
})
|
||||
|
||||
testWithBothBoardTypes("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)
|
||||
})
|
||||
|
||||
testWithBothBoardTypes("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("Create note by double clicking on board", async ({ page, browser }) => {
|
||||
const board = await navigateToNewBoard(page, browser)
|
||||
await board.board.dblclick({ position: { x: 200, y: 200 } })
|
||||
await expect(board.getNote("HELLO")).toBeVisible()
|
||||
|
||||
await test.step("Also inside an Area", async () => {
|
||||
await board.createArea(300, 200, "Container")
|
||||
await board.board.dblclick({ position: { x: 350, y: 250 } })
|
||||
await expect(board.getNote("HELLO")).toHaveCount(2)
|
||||
})
|
||||
})
|
||||
|
||||
testWithBothBoardTypes("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")
|
||||
|
||||
await test.step("Drag to new position", async () => {
|
||||
await board.dragItem(monoids, 300, 300)
|
||||
await board.assertItemPosition(monoids, 300, 300)
|
||||
})
|
||||
|
||||
const area = await board.createArea(450, 100, "Container")
|
||||
await test.step("Drag multiple items", async () => {
|
||||
await board.selectItems(monoids, semigroups)
|
||||
await board.dragItem(monoids, 600, 300)
|
||||
await board.assertItemPosition(monoids, 600, 300)
|
||||
await board.assertItemPosition(semigroups, 500, 200)
|
||||
})
|
||||
|
||||
await test.step("Drag area to move contained items", async () => {
|
||||
await board.dragItem(area, 300, 300)
|
||||
await board.assertItemPosition(area, 300, 300)
|
||||
await board.assertItemPosition(monoids, 240, 360)
|
||||
await board.assertItemPosition(semigroups, 140, 260)
|
||||
})
|
||||
})
|
||||
|
||||
// TODO: test creating and modifying connections
|
||||
|
||||
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", () => {
|
||||
testWithBothBoardTypes("Duplicate text by Ctrl+D", async ({ board }) => {
|
||||
const monoids = await board.createText(100, 200, "Monoids")
|
||||
const functors = await board.createNoteWithText(300, 200, "Functors")
|
||||
await board.selectItems(monoids, functors)
|
||||
await monoids.press("Control+d")
|
||||
await expect(monoids).toHaveCount(2)
|
||||
await expect(functors).toHaveCount(2)
|
||||
})
|
||||
|
||||
testWithBothBoardTypes("Duplicate a container with child items", async ({ board }) => {
|
||||
const container = await board.createArea(100, 200, "Container")
|
||||
const text = await board.createText(150, 250, "text")
|
||||
await board.selectItems(container)
|
||||
await container.press("Control+d")
|
||||
const clonedText = board.getText("text").nth(1)
|
||||
await expect(clonedText).toHaveText("text")
|
||||
})
|
||||
|
||||
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")
|
||||
const text = await board.createText(150, 250, "Bottom")
|
||||
await board.selectItems(container)
|
||||
await container.press("Control+d")
|
||||
const clonedText = board.getText("Bottom").nth(1)
|
||||
await expect(clonedText).toHaveText("Bottom")
|
||||
})
|
||||
})
|
||||
|
||||
test.skip("Copy, paste and cut", async ({ page, browser }) => {
|
||||
// TODO: not sure how to trigger native copy, paste events
|
||||
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 })
|
||||
await page.keyboard.press("Control+v")
|
||||
|
||||
await board.changeItemText(board.getNote("Monoids"), "Monads")
|
||||
await expect(board.getNote("Monoids")).toBeVisible()
|
||||
await expect(board.getNote("Monads")).toBeVisible()
|
||||
})
|
||||
|
||||
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 () => {
|
||||
await page.keyboard.press("ArrowRight")
|
||||
expect(await board.getItemPosition(monoids)).toEqual({ x: origPos.x + 14, y: origPos.y })
|
||||
})
|
||||
await test.step("Faster with shift", async () => {
|
||||
await page.keyboard.press("Shift+ArrowLeft")
|
||||
expect(await board.getItemPosition(monoids)).toEqual({ x: origPos.x - 125, y: origPos.y })
|
||||
})
|
||||
await test.step("Slower with alt", async () => {
|
||||
await page.keyboard.press("Alt+ArrowDown")
|
||||
expect(await board.getItemPosition(monoids)).toEqual({ x: origPos.x - 125, y: origPos.y + 1 })
|
||||
})
|
||||
})
|
||||
|
||||
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 () => {
|
||||
await page.keyboard.press("Delete")
|
||||
await expect(monoids).not.toBeVisible()
|
||||
})
|
||||
|
||||
await test.step("With backspace key", async () => {
|
||||
const functors = await board.createNoteWithText(100, 200, "Functors")
|
||||
await page.keyboard.press("Delete")
|
||||
await expect(functors).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
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")
|
||||
await board.createNoteWithText(300, 450, "BUT NOT THIS")
|
||||
await board.selectItems(n1, n2, n3)
|
||||
const originalCoordinates = await Promise.all([n1, n2, n3].map((n) => board.getItemPosition(n)))
|
||||
await (await board.contextMenu.openHorizontalAlign()).left.click()
|
||||
const newCoordinates = await Promise.all([n1, n2, n3].map((n) => board.getItemPosition(n)))
|
||||
const expectedX = originalCoordinates[0].x
|
||||
for (let i = 1; i < newCoordinates.length; i++) {
|
||||
expect(newCoordinates[i].x).toEqual(expectedX)
|
||||
}
|
||||
})
|
||||
|
||||
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 test.step("Check persistence", async () => {
|
||||
await sleep(1000) // Time for persistence
|
||||
await page.reload()
|
||||
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.setNickname("User 2")
|
||||
await expect(newBoard.getNote("Monads")).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
testWithBothBoardTypes("Edit area text", async ({ board, page }) => {
|
||||
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.setNickname("User 2")
|
||||
await expect(newBoard.getArea("Monads")).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
/*
|
||||
testWithBothBoardTypes("Simulate typing lot of text", async ({ board, page }) => {
|
||||
const textArea = await board.createText(100, 200, "Initial text")
|
||||
await board.selectItems(textArea)
|
||||
await board.dragSelectionBottomCorner(600, 600)
|
||||
const item = board.board.locator(".text").first()
|
||||
await board.changeItemText(item, "New chapter in life\n")
|
||||
|
||||
await test.step("Change text", async () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await item.pressSequentially("New chapter in life\n")
|
||||
await sleep(100)
|
||||
}
|
||||
})
|
||||
|
||||
// This is just to check stored data size - compare the legacy approach vs CRDT storage.
|
||||
//
|
||||
// Results:
|
||||
|
||||
// Legacy: 2159b JSON, 1 event row, 45 events
|
||||
// CRDT: 904b CRDT + 740b JSON, 1 event row, 2 events
|
||||
|
||||
// SQL query: select coalesce(sum(pg_column_size(crdt_update)), 0) as crdt_size, sum(pg_column_size(events)) as json_size, count(*) as rows, sum(last_serial - first_serial - 1) as event_count from board_event where board_id ='2bc90b97-8304-4cd6-8954-b87878db6ff3';
|
||||
})
|
||||
*/
|
||||
|
||||
testWithBothBoardTypes("Hide area contents", async ({ page, browser }) => {
|
||||
const board = await navigateToNewBoard(page, browser)
|
||||
const area = await board.createArea(100, 200, "Area")
|
||||
const text = await board.createNoteWithText(150, 250, "text")
|
||||
await board.selectItems(area)
|
||||
await board.contextMenu.toggleContentsHidden()
|
||||
await expect(text).not.toBeVisible()
|
||||
|
||||
await test.step("Check persistence", async () => {
|
||||
await sleep(1000) // Time for persistence
|
||||
await page.reload()
|
||||
await expect(text).not.toBeVisible()
|
||||
})
|
||||
|
||||
await test.step("Check with new session", async () => {
|
||||
await board.deleteIndexedDb()
|
||||
const newBoard = await board.openBoardInNewBrowser()
|
||||
await newBoard.setNickname("User 2")
|
||||
await expect(newBoard.getNote("text")).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
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 () => {
|
||||
await board.selectItems(monoids)
|
||||
await board.dragSelectionBottomCorner(550, 550)
|
||||
await board.assertItemSize(monoids, 380, 380)
|
||||
})
|
||||
})
|
||||
|
||||
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")
|
||||
|
||||
async function resetSelection() {
|
||||
await board.clickOnBoard({ x: 100, y: 100 })
|
||||
await board.assertSelected(monoids, false)
|
||||
}
|
||||
|
||||
await test.step("Can select note by dragging on board with ALT pressed", async () => {
|
||||
await resetSelection()
|
||||
await board.dragOnBoard({ x: 100, y: 100 }, { x: 300, y: 300 }, { altKey: true })
|
||||
await board.assertSelected(monoids)
|
||||
})
|
||||
|
||||
await test.step("Can select notes with SHIFT key", async () => {
|
||||
await resetSelection()
|
||||
await board.selectItems(monoids, semigroups)
|
||||
await board.assertSelected(monoids)
|
||||
await board.assertSelected(semigroups)
|
||||
})
|
||||
})
|
||||
testWithBothBoardTypes("Clone the board", async ({ board, page }) => {
|
||||
const semigroups = await board.createArea(500, 200, "Semigroups")
|
||||
const functors = await board.createNoteWithText(200, 200, "Functors")
|
||||
await sleep(1000)
|
||||
await board.cloneBoard()
|
||||
await board.assertBoardName("Clone the board copy")
|
||||
await expect(semigroups).toBeVisible()
|
||||
await expect(functors).toBeVisible()
|
||||
|
||||
await test.step("Check persistence", async () => {
|
||||
await page.reload()
|
||||
await expect(semigroups).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step("Check with new session", async () => {
|
||||
await board.deleteIndexedDb()
|
||||
const newBoard = await board.openBoardInNewBrowser()
|
||||
await newBoard.setNickname("User 2")
|
||||
await expect(newBoard.getArea("Semigroups")).toBeVisible()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function testWithBothBoardTypes(
|
||||
name: string,
|
||||
runTest: (options: { board: BoardPage; page: Page; browser: Browser }) => Promise<void>,
|
||||
) {
|
||||
test(`${name}`, async ({ page, browser }) => {
|
||||
const board = await navigateToNewBoard(page, browser, { boardName: name })
|
||||
await runTest({ board, page, browser })
|
||||
})
|
||||
test(`${name} (legacy board)`, async ({ page, browser }) => {
|
||||
const board = await navigateToNewBoard(page, browser, { useCRDT: false, boardName: name })
|
||||
await runTest({ board, page, browser })
|
||||
})
|
||||
}
|
||||
147
playwright/src/tests/collaboration.spec.ts
Normal file
147
playwright/src/tests/collaboration.spec.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { Browser, Page, expect, test } from "@playwright/test"
|
||||
import { navigateToNewBoard, semiUniqueId } from "../pages/BoardPage"
|
||||
import { sleep } from "../../../common/src/sleep"
|
||||
|
||||
test.describe("Two simultaneous users", () => {
|
||||
test("Two anonymous users can see each other notes", async ({ page, browser }) => {
|
||||
const { user1Page: userPage, user2Page } = await createBoardWithTwoUsers(page, browser)
|
||||
// create 2 notes, one on each page
|
||||
const userPageNoteText = `note-${semiUniqueId()}`
|
||||
await userPage.createNoteWithText(100, 200, userPageNoteText)
|
||||
const anotherUserPageNoteText = `another-${semiUniqueId()}`
|
||||
await user2Page.createNoteWithText(500, 200, anotherUserPageNoteText)
|
||||
|
||||
await expect(user2Page.getNote(userPageNoteText)).toBeVisible()
|
||||
await expect(userPage.getNote(anotherUserPageNoteText)).toBeVisible()
|
||||
})
|
||||
|
||||
test("Change board name", async ({ page, browser }) => {
|
||||
const { user1Page: userPage, user2Page } = await createBoardWithTwoUsers(page, browser)
|
||||
// create 2 notes, one on each page
|
||||
await userPage.renameBoard("Renamed board")
|
||||
await user2Page.assertBoardName("Renamed board")
|
||||
})
|
||||
|
||||
const onTopPart = { position: { x: 9, y: 15 } } as const
|
||||
|
||||
test("Users can collaboratively edit a text area", async ({ page, browser }) => {
|
||||
const { user1Page, user2Page } = await createBoardWithTwoUsers(page, browser)
|
||||
await user1Page.createArea(100, 200, "initialText")
|
||||
await test.step("Both users edit text", async () => {
|
||||
await user1Page.getArea("initialText").click(onTopPart)
|
||||
await user2Page.getArea("initialText").press("ArrowDown")
|
||||
await user1Page.getArea("initialText").pressSequentially("User1Text")
|
||||
await user2Page.getArea("initialText").dblclick(onTopPart)
|
||||
await user2Page.getArea("initialText").press("ArrowDown")
|
||||
await user2Page.getArea("initialText").pressSequentially("User2Text")
|
||||
await expect(user1Page.getArea("User1Text")).toBeVisible()
|
||||
await expect(user2Page.getArea("User1Text")).toBeVisible()
|
||||
await expect(user1Page.getArea("User2Text")).toBeVisible()
|
||||
await expect(user2Page.getArea("User2Text")).toBeVisible()
|
||||
})
|
||||
await test.step("User 1 duplicates text area", async () => {
|
||||
await user1Page.cloneButton.click()
|
||||
})
|
||||
await test.step("Text changes to new area do not affect old area", async () => {
|
||||
const oldArea = user1Page.getArea("User2Text").first()
|
||||
const newArea = user1Page.getArea("User2Text").last()
|
||||
await newArea.press("Escape")
|
||||
await user1Page.dragItem(newArea, 300, 300)
|
||||
await newArea.dblclick(onTopPart)
|
||||
await newArea.press("ArrowDown")
|
||||
await newArea.pressSequentially("NewText")
|
||||
await expect(newArea).toContainText("NewText")
|
||||
await user1Page.assertSelected(newArea, true)
|
||||
await newArea.press("Escape")
|
||||
await newArea.press("Escape")
|
||||
await user1Page.assertSelected(newArea, false)
|
||||
await expect(oldArea).not.toContainText("NewText")
|
||||
await expect(user2Page.getArea("NewText")).toBeVisible()
|
||||
})
|
||||
await test.step("Deleting the new area does not affect the old area", async () => {
|
||||
const newAreaUser2 = user2Page.getArea("NewText")
|
||||
await user2Page.assertSelected(newAreaUser2, false)
|
||||
await user2Page.selectItems(newAreaUser2)
|
||||
await newAreaUser2.press("Delete")
|
||||
await expect(user2Page.getArea("NewText")).not.toBeVisible()
|
||||
await expect(user1Page.getArea("NewText")).not.toBeVisible()
|
||||
await expect(user2Page.getArea("User2Text")).toBeVisible()
|
||||
await expect(user1Page.getArea("User2Text")).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test("Offline changes are synced on re-connection", async ({ page, browser }) => {
|
||||
const { user1Page, user2Page } = await createBoardWithTwoUsers(page, browser)
|
||||
await user1Page.createNoteWithText(100, 400, "basetext")
|
||||
await sleep(1000)
|
||||
await user1Page.setOfflineMode(true)
|
||||
await user2Page.setOfflineMode(true)
|
||||
|
||||
await sleep(1000)
|
||||
await user1Page.createArea(100, 200, "user 1 offline text")
|
||||
await user2Page.createArea(500, 200, "user 2 offline text")
|
||||
|
||||
await test.step("Verify that changes are not synced while offline", async () => {
|
||||
await sleep(1000)
|
||||
await expect(user1Page.getArea("user 2 offline text")).not.toBeVisible()
|
||||
})
|
||||
|
||||
await test.step("Reload page to verify offline persistence", async () => {
|
||||
await user1Page.page.reload()
|
||||
await expect(user1Page.getArea("user 1 offline text")).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step("Reconnect and verify that changes are synced", async () => {
|
||||
await user1Page.setOfflineMode(false)
|
||||
await user2Page.setOfflineMode(false)
|
||||
await sleep(1000)
|
||||
await expect(user1Page.getArea("user 2 offline text")).toBeVisible()
|
||||
await expect(user2Page.getArea("user 1 offline text")).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test("Offline changes are synced on re-connection (first event offline)", async ({ page, browser }) => {
|
||||
const user1Page = await navigateToNewBoard(page, browser, { boardName: "Collab test board" })
|
||||
|
||||
await user1Page.setOfflineMode(true)
|
||||
|
||||
await sleep(1000)
|
||||
await user1Page.createArea(100, 200, "user 1 offline text")
|
||||
|
||||
await test.step("Reload page to verify offline persistence", async () => {
|
||||
await user1Page.page.reload()
|
||||
await expect(user1Page.getArea("user 1 offline text")).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test("Switching between boards", async ({ page, browser }) => {
|
||||
const { user1Page, user2Page } = await createBoardWithTwoUsers(page, browser)
|
||||
|
||||
const boardName1 = await user1Page.getBoardName()
|
||||
const monoids = await user1Page.createText(100, 200, "Monoids")
|
||||
const dashboard = await user1Page.goToDashBoard()
|
||||
await dashboard.createNewBoard({ boardName: "Another board" })
|
||||
const semigroups = await user1Page.createArea(500, 200, "Semigroups")
|
||||
await user1Page.goToDashBoard()
|
||||
await dashboard.goToBoard(boardName1)
|
||||
await expect(monoids).toBeVisible()
|
||||
await user2Page.createNoteWithText(100, 400, "User 2 note")
|
||||
await expect(user1Page.getNote("User 2 note")).toBeVisible()
|
||||
await user2Page.createText(300, 200, "User 2 text")
|
||||
await expect(user1Page.getText("User 2 text")).toBeVisible()
|
||||
})
|
||||
|
||||
async function createBoardWithTwoUsers(page: Page, browser: Browser) {
|
||||
const user1Page = await navigateToNewBoard(page, browser, { boardName: "Collab test board" })
|
||||
await user1Page.setNickname("User 1")
|
||||
|
||||
const boardId = user1Page.getBoardId()
|
||||
const user2Page = await user1Page.openBoardInNewBrowser()
|
||||
await user2Page.setNickname("User 2")
|
||||
|
||||
await user1Page.assertBoardName("Collab test board")
|
||||
await user2Page.assertBoardName("Collab test board")
|
||||
|
||||
return { user1Page, user2Page, boardId }
|
||||
}
|
||||
})
|
||||
35
playwright/src/tests/dashboard.spec.ts
Normal file
35
playwright/src/tests/dashboard.spec.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { test } from "@playwright/test"
|
||||
import { navigateToBoard } from "../pages/BoardPage"
|
||||
import { navigateToDashboard } from "../pages/DashboardPage"
|
||||
|
||||
test.describe("Dashboard", () => {
|
||||
test("Creating a new board", async ({ page, browser }) => {
|
||||
const dashboard = await navigateToDashboard(page, browser)
|
||||
const board = await dashboard.createNewBoard({ boardName: "My new board" })
|
||||
await board.assertBoardName("My new board")
|
||||
await board.goToDashBoard()
|
||||
|
||||
await test.step("Do it again to make sure it works", async () => {
|
||||
await dashboard.createNewBoard({ boardName: "My new board" })
|
||||
await board.assertBoardName("My new board")
|
||||
})
|
||||
|
||||
await test.step("Navigating to the new board by URL", async () => {
|
||||
const boardId = board.getBoardId()
|
||||
await board.goToDashBoard()
|
||||
await navigateToBoard(page, browser, boardId)
|
||||
await board.assertBoardName("My new board")
|
||||
})
|
||||
|
||||
await test.step("Navigating to the new board from the dashboard", async () => {
|
||||
await board.goToDashBoard()
|
||||
await dashboard.goToBoard("My new board")
|
||||
await board.assertBoardName("My new board")
|
||||
})
|
||||
})
|
||||
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")
|
||||
})
|
||||
})
|
||||
15
playwright/src/tests/navigation.spec.ts
Normal file
15
playwright/src/tests/navigation.spec.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { expect, test } from "@playwright/test"
|
||||
import { navigateToBoard } from "../pages/BoardPage"
|
||||
|
||||
test.describe("Navigation", () => {
|
||||
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, browser }) => {
|
||||
const board = await navigateToBoard(page, browser, "non-existing-board-id")
|
||||
await board.assertStatusMessage("Board not found. A typo, maybe?")
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user