mirror of
https://github.com/oxynote/oxynote.git
synced 2026-10-18 03:19:19 +02:00
453 lines
13 KiB
TypeScript
453 lines
13 KiB
TypeScript
import { expect, test } from "@playwright/test"
|
|
import {
|
|
contentEditor,
|
|
contentPane,
|
|
createDocument,
|
|
documentPersisted,
|
|
editorText,
|
|
openBlockMenu,
|
|
openDocumentActions,
|
|
openSlashMenu,
|
|
readModeToggle,
|
|
sidebarDocument,
|
|
sidebarDocumentRow,
|
|
titleEditor,
|
|
waitForEditor,
|
|
} from "../helpers/editor"
|
|
import { t } from "../helpers/i18n"
|
|
import { visit } from "../helpers/page"
|
|
import { signUpWithWorkspace } from "../helpers/workspace"
|
|
|
|
test.describe("editor", () => {
|
|
test("creates a page and opens it empty", async ({ page, request }) => {
|
|
await signUpWithWorkspace(page, request)
|
|
|
|
await createDocument(page)
|
|
|
|
await expect
|
|
.poll(() => editorText(titleEditor(page)))
|
|
.toBe(t("editor.new-document-name"))
|
|
await expect(
|
|
contentEditor(page).locator("[data-placeholder]"),
|
|
).toHaveAttribute(
|
|
"data-placeholder",
|
|
t("editor.placeholders.content.paragraph"),
|
|
)
|
|
await expect(page).toHaveTitle(
|
|
t("general.document-page-title", {
|
|
suffix: t("editor.new-document-name"),
|
|
}),
|
|
)
|
|
})
|
|
|
|
test("renames a page from its title", async ({ page, request }) => {
|
|
await signUpWithWorkspace(page, request)
|
|
await createDocument(page)
|
|
|
|
await titleEditor(page).click()
|
|
await page.keyboard.press("ControlOrMeta+A")
|
|
await page.keyboard.type("Release Notes")
|
|
|
|
// the name is pushed everywhere it is shown: the URL slug, the
|
|
// sidebar row, the breadcrumb and the tab title
|
|
await expect(page).toHaveURL(/Release-Notes-[a-z0-9]{20}$/)
|
|
await expect(sidebarDocument(page, "Release Notes")).toBeVisible()
|
|
await expect(
|
|
page
|
|
.getByLabel("breadcrumb")
|
|
.getByRole("link", { name: "Release Notes" }),
|
|
).toBeVisible()
|
|
await expect(page).toHaveTitle(
|
|
t("general.document-page-title", { suffix: "Release Notes" }),
|
|
)
|
|
})
|
|
|
|
test("switches the page to read mode", async ({ page, request }) => {
|
|
await signUpWithWorkspace(page, request)
|
|
await createDocument(page)
|
|
|
|
await readModeToggle(page).click()
|
|
|
|
await expect(contentEditor(page)).toHaveAttribute(
|
|
"contenteditable",
|
|
"false",
|
|
)
|
|
await expect(titleEditor(page)).toHaveAttribute("contenteditable", "false")
|
|
await expect(readModeToggle(page)).toHaveAccessibleName(
|
|
t("editor.navbar.toggle-read-mode"),
|
|
)
|
|
})
|
|
|
|
test("switches the page back to edit mode", async ({ page, request }) => {
|
|
await signUpWithWorkspace(page, request)
|
|
await createDocument(page)
|
|
await readModeToggle(page).click()
|
|
await expect(contentEditor(page)).toHaveAttribute(
|
|
"contenteditable",
|
|
"false",
|
|
)
|
|
|
|
await readModeToggle(page).click()
|
|
|
|
await expect(contentEditor(page)).toHaveAttribute("contenteditable", "true")
|
|
await expect(titleEditor(page)).toHaveAttribute("contenteditable", "true")
|
|
await expect(readModeToggle(page)).toHaveAccessibleName(
|
|
t("editor.navbar.toggle-edit-mode"),
|
|
)
|
|
})
|
|
|
|
test("keeps read mode across a reload", async ({ page, request }) => {
|
|
await signUpWithWorkspace(page, request)
|
|
await createDocument(page)
|
|
const url = page.url()
|
|
await readModeToggle(page).click()
|
|
await expect(contentEditor(page)).toHaveAttribute(
|
|
"contenteditable",
|
|
"false",
|
|
)
|
|
|
|
await visit(page, url)
|
|
|
|
await waitForEditor(page)
|
|
await expect(contentEditor(page)).toHaveAttribute(
|
|
"contenteditable",
|
|
"false",
|
|
)
|
|
})
|
|
|
|
test("keeps content across a reload", async ({ page, request }) => {
|
|
await signUpWithWorkspace(page, request)
|
|
await createDocument(page)
|
|
const url = page.url()
|
|
|
|
await contentEditor(page).click()
|
|
await page.keyboard.type("Persist me")
|
|
await documentPersisted(page)
|
|
|
|
await visit(page, url)
|
|
|
|
await waitForEditor(page)
|
|
await expect.poll(() => editorText(contentEditor(page))).toBe("Persist me")
|
|
})
|
|
|
|
test.describe("markdown shortcuts", () => {
|
|
const CASES = [
|
|
{ name: "a heading", typed: "# Title", selector: "h1", text: "Title" },
|
|
{
|
|
name: "a bulleted list",
|
|
typed: "- item",
|
|
selector: "ul > li",
|
|
text: "item",
|
|
},
|
|
{
|
|
name: "a numbered list",
|
|
typed: "1. first",
|
|
selector: "ol > li",
|
|
text: "first",
|
|
},
|
|
{
|
|
name: "bold text",
|
|
typed: "**strong**",
|
|
selector: "strong",
|
|
text: "strong",
|
|
},
|
|
{
|
|
name: "italic text",
|
|
typed: "*slanted*",
|
|
selector: "em",
|
|
text: "slanted",
|
|
},
|
|
]
|
|
|
|
for (const c of CASES) {
|
|
test(`turns ${c.typed} into ${c.name}`, async ({ page, request }) => {
|
|
await signUpWithWorkspace(page, request)
|
|
await createDocument(page)
|
|
|
|
await contentEditor(page).click()
|
|
await page.keyboard.type(c.typed)
|
|
|
|
await expect(contentEditor(page).locator(c.selector)).toHaveText(c.text)
|
|
})
|
|
}
|
|
})
|
|
|
|
test("inserts a code block from the slash menu", async ({
|
|
page,
|
|
request,
|
|
}) => {
|
|
await signUpWithWorkspace(page, request)
|
|
await createDocument(page)
|
|
|
|
await contentEditor(page).click()
|
|
const menu = await openSlashMenu(page)
|
|
await page.keyboard.type("Code")
|
|
|
|
// typing narrows the menu down to the one matching command
|
|
await expect(menu.getByRole("button")).toHaveCount(1)
|
|
await expect(menu.getByRole("button")).toContainText("Code Block")
|
|
|
|
await page.keyboard.press("Enter")
|
|
await page.keyboard.type("const answer = 42")
|
|
|
|
await expect(contentEditor(page).locator("pre code")).toContainText(
|
|
"const answer = 42",
|
|
)
|
|
})
|
|
|
|
test("deletes a page after confirming", async ({ page, request }) => {
|
|
await signUpWithWorkspace(page, request)
|
|
await createDocument(page)
|
|
const name = t("editor.new-document-name")
|
|
|
|
await openDocumentActions(page, name)
|
|
await page
|
|
.getByRole("menuitem", {
|
|
name: t("sidebar.item-dropdown-menu-buttons.delete-page"),
|
|
})
|
|
.click()
|
|
|
|
const dialog = page.getByRole("dialog")
|
|
await expect(dialog).toContainText(
|
|
t("editor.document-deletion-modal.title"),
|
|
)
|
|
await dialog
|
|
.getByRole("button", {
|
|
name: t("editor.document-deletion-modal.confirm-button"),
|
|
})
|
|
.click()
|
|
|
|
// the toast comes first, and not only because it is the server's
|
|
// own confirmation that the delete landed: it dismisses itself
|
|
// after a few seconds, so waiting on the sidebar ahead of it
|
|
// leaves this racing a message that may already be gone. The
|
|
// deletion is a request, an invalidation and a tree refetch, so
|
|
// it gets longer than the default to complete.
|
|
await expect(
|
|
page.getByText(
|
|
t("editor.document-deletion-modal.deletion-success", { name }),
|
|
),
|
|
).toBeVisible({ timeout: 15_000 })
|
|
await expect(sidebarDocument(page, name)).toHaveCount(0)
|
|
// the welcome page is the only one left, so that is where the
|
|
// user lands
|
|
await expect(page).toHaveURL(/Welcome-to-Oxynote-[a-z0-9]{20}$/)
|
|
})
|
|
|
|
test("keeps the title across a reload", async ({ page, request }) => {
|
|
await signUpWithWorkspace(page, request)
|
|
await createDocument(page)
|
|
|
|
await titleEditor(page).click()
|
|
await page.keyboard.press("ControlOrMeta+A")
|
|
await page.keyboard.type("Sticky Title")
|
|
await expect(page).toHaveURL(/Sticky-Title-[a-z0-9]{20}$/)
|
|
const url = page.url()
|
|
await documentPersisted(page)
|
|
|
|
await visit(page, url)
|
|
|
|
await waitForEditor(page)
|
|
await expect.poll(() => editorText(titleEditor(page))).toBe("Sticky Title")
|
|
})
|
|
|
|
test("keeps a code block across a reload", async ({ page, request }) => {
|
|
await signUpWithWorkspace(page, request)
|
|
await createDocument(page)
|
|
const url = page.url()
|
|
|
|
await contentEditor(page).click()
|
|
const menu = await openSlashMenu(page)
|
|
await page.keyboard.type("Code")
|
|
await expect(menu.getByRole("button")).toHaveCount(1)
|
|
await page.keyboard.press("Enter")
|
|
await page.keyboard.type("const persisted = true")
|
|
await documentPersisted(page)
|
|
|
|
await visit(page, url)
|
|
|
|
await waitForEditor(page)
|
|
await expect(contentEditor(page).locator("pre code")).toContainText(
|
|
"const persisted = true",
|
|
)
|
|
})
|
|
|
|
test("duplicates a page with its content", async ({ page, request }) => {
|
|
await signUpWithWorkspace(page, request)
|
|
await createDocument(page)
|
|
const url = page.url()
|
|
await contentEditor(page).click()
|
|
await page.keyboard.type("Copy me over")
|
|
// the duplicate is built from the stored content, so the text has
|
|
// to reach the server first
|
|
await documentPersisted(page)
|
|
|
|
await page
|
|
.getByRole("button", { name: t("editor.navbar.open-document-options") })
|
|
.click()
|
|
await page
|
|
.getByRole("menuitem", {
|
|
name: t("editor.navbar.document-options.duplicate.title"),
|
|
})
|
|
.click()
|
|
|
|
// the copy's name is the original's plus a timestamp, and the app
|
|
// navigates straight to it
|
|
await expect(page).not.toHaveURL(url)
|
|
await waitForEditor(page)
|
|
await expect
|
|
.poll(() => editorText(titleEditor(page)))
|
|
.toMatch(/^New Page \(/)
|
|
await expect
|
|
.poll(() => editorText(contentEditor(page)))
|
|
.toBe("Copy me over")
|
|
})
|
|
|
|
test("navigates between pages from the sidebar", async ({
|
|
page,
|
|
request,
|
|
}) => {
|
|
await signUpWithWorkspace(page, request)
|
|
await createDocument(page)
|
|
await titleEditor(page).click()
|
|
await page.keyboard.press("ControlOrMeta+A")
|
|
await page.keyboard.type("Second Page")
|
|
await expect(page).toHaveURL(/Second-Page-[a-z0-9]{20}$/)
|
|
|
|
await sidebarDocument(page, "Welcome to Oxynote!").click()
|
|
|
|
await expect(page).toHaveURL(/Welcome-to-Oxynote-[a-z0-9]{20}$/)
|
|
await waitForEditor(page)
|
|
await expect
|
|
.poll(() => editorText(titleEditor(page)))
|
|
.toBe("Welcome to Oxynote!")
|
|
|
|
await sidebarDocument(page, "Second Page").click()
|
|
|
|
await expect(page).toHaveURL(/Second-Page-[a-z0-9]{20}$/)
|
|
await waitForEditor(page)
|
|
await expect.poll(() => editorText(titleEditor(page))).toBe("Second Page")
|
|
})
|
|
|
|
test("nests a sub page under its parent", async ({ page, request }) => {
|
|
await signUpWithWorkspace(page, request)
|
|
await createDocument(page)
|
|
const parent = t("editor.new-document-name")
|
|
|
|
await openDocumentActions(page, parent)
|
|
await page
|
|
.getByRole("menuitem", {
|
|
name: t("sidebar.item-dropdown-menu-buttons.add-sub-page"),
|
|
})
|
|
.click()
|
|
|
|
const parentRow = sidebarDocumentRow(page, parent).first()
|
|
await expect(parentRow).toHaveAttribute("data-item-children", "1")
|
|
|
|
// the child list is collapsed until the parent is expanded
|
|
await parentRow.hover()
|
|
await parentRow
|
|
.getByRole("button", {
|
|
name: t("sidebar.item-collapse-trigger-button.open-screen-reader-hint"),
|
|
})
|
|
.click()
|
|
|
|
const child = parentRow.locator(
|
|
'[data-sidebar="menu-badge"] a[data-sidebar="menu-button"]',
|
|
)
|
|
await expect(child).toHaveText(parent)
|
|
await expect(child).not.toHaveAttribute("data-optimistic-insert", "")
|
|
})
|
|
|
|
test("drops the linked block when a block is dragged", async ({
|
|
page,
|
|
request,
|
|
}) => {
|
|
await page.context().grantPermissions(["clipboard-read", "clipboard-write"])
|
|
await signUpWithWorkspace(page, request)
|
|
await createDocument(page)
|
|
await contentEditor(page).click()
|
|
await page.keyboard.type("First")
|
|
await page.keyboard.press("Enter")
|
|
await page.keyboard.type("Second")
|
|
const first = contentEditor(page).getByText("First", { exact: true })
|
|
const second = contentEditor(page).getByText("Second", { exact: true })
|
|
await openBlockMenu(page, contentPane(page), second)
|
|
await page
|
|
.getByRole("menuitem", {
|
|
name: t("editor.drag-handle.options.copy-link"),
|
|
})
|
|
.click()
|
|
await expect
|
|
.poll(() => page.evaluate(() => navigator.clipboard.readText()), {
|
|
message: "the block link reaches the clipboard",
|
|
})
|
|
.toContain("#")
|
|
const link = new URL(
|
|
await page.evaluate(() => navigator.clipboard.readText()),
|
|
)
|
|
await documentPersisted(page)
|
|
await visit(page, link.href)
|
|
const highlight = page.locator(".editor-scroll-highlight")
|
|
await expect(highlight).toBeVisible({ timeout: 15_000 })
|
|
|
|
const target = await second.boundingBox()
|
|
expect(target).not.toBeNull()
|
|
|
|
await first.hover()
|
|
await contentPane(page)
|
|
.locator('.z-drag-handle [data-slot="dropdown-menu-trigger"]', {
|
|
has: page.locator('[class*="dots-line"]'),
|
|
})
|
|
.hover()
|
|
await page.mouse.down()
|
|
await page.mouse.move(
|
|
(target?.x ?? 0) + 20,
|
|
(target?.y ?? 0) + (target?.height ?? 0) - 2,
|
|
{ steps: 10 },
|
|
)
|
|
await page.mouse.up()
|
|
|
|
await expect.poll(() => editorText(contentEditor(page))).toBe("SecondFirst")
|
|
await expect(page).toHaveURL(link.href.replace(link.hash, ""))
|
|
await expect(highlight).toHaveCount(0)
|
|
})
|
|
|
|
test("drops the linked block when the document is typed in", async ({
|
|
page,
|
|
request,
|
|
}) => {
|
|
await page.context().grantPermissions(["clipboard-read", "clipboard-write"])
|
|
await signUpWithWorkspace(page, request)
|
|
await createDocument(page)
|
|
await contentEditor(page).click()
|
|
await page.keyboard.type("Linked")
|
|
const block = contentEditor(page).getByText("Linked", { exact: true })
|
|
await openBlockMenu(page, contentPane(page), block)
|
|
await page
|
|
.getByRole("menuitem", {
|
|
name: t("editor.drag-handle.options.copy-link"),
|
|
})
|
|
.click()
|
|
await expect
|
|
.poll(() => page.evaluate(() => navigator.clipboard.readText()), {
|
|
message: "the block link reaches the clipboard",
|
|
})
|
|
.toContain("#")
|
|
const link = new URL(
|
|
await page.evaluate(() => navigator.clipboard.readText()),
|
|
)
|
|
await documentPersisted(page)
|
|
await visit(page, link.href)
|
|
const highlight = page.locator(".editor-scroll-highlight")
|
|
await expect(highlight).toBeVisible({ timeout: 15_000 })
|
|
|
|
// a click would drop the linked block by itself
|
|
await contentEditor(page).focus()
|
|
await page.keyboard.type("!")
|
|
|
|
await expect(page).toHaveURL(link.href.replace(link.hash, ""))
|
|
await expect(highlight).toHaveCount(0)
|
|
})
|
|
})
|