240 lines
8.2 KiB
TypeScript
240 lines
8.2 KiB
TypeScript
import { test, expect, type Page } from '@playwright/test'
|
|
import { createFixtureVaultCopy, openFixtureVault, removeFixtureVaultCopy } from '../helpers/fixtureVault'
|
|
import { openCommandPalette, executeCommand } from './helpers'
|
|
|
|
let tempVaultDir: string
|
|
|
|
test.beforeEach(async ({ page }, testInfo) => {
|
|
testInfo.setTimeout(90_000)
|
|
tempVaultDir = createFixtureVaultCopy()
|
|
await openFixtureVault(page, tempVaultDir)
|
|
})
|
|
|
|
test.afterEach(async () => {
|
|
removeFixtureVaultCopy(tempVaultDir)
|
|
})
|
|
|
|
async function openNote(page: Page, title: string) {
|
|
await page.locator('[data-testid="note-list-container"]').getByText(title, { exact: true }).click()
|
|
await expect(page.locator('.bn-editor')).toBeVisible({ timeout: 5_000 })
|
|
}
|
|
|
|
async function openRawMode(page: Page) {
|
|
await openCommandPalette(page)
|
|
await executeCommand(page, 'Toggle Raw')
|
|
await expect(page.locator('.cm-content')).toBeVisible({ timeout: 5_000 })
|
|
}
|
|
|
|
async function openBlockNoteMode(page: Page) {
|
|
await openCommandPalette(page)
|
|
await executeCommand(page, 'Toggle Raw')
|
|
await expect(page.locator('.bn-editor')).toBeVisible({ timeout: 5_000 })
|
|
}
|
|
|
|
async function getRawEditorContent(page: Page): Promise<string> {
|
|
return page.evaluate(() => {
|
|
type CodeMirrorHost = Element & {
|
|
cmTile?: {
|
|
view?: {
|
|
state: {
|
|
doc: {
|
|
toString(): string
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const el = document.querySelector('.cm-content')
|
|
if (!el) return ''
|
|
const view = (el as CodeMirrorHost).cmTile?.view
|
|
if (view) return view.state.doc.toString() as string
|
|
return el.textContent ?? ''
|
|
})
|
|
}
|
|
|
|
async function roundTripThroughAnotherNote(page: Page) {
|
|
await openNote(page, 'Note C')
|
|
await openNote(page, 'Note B')
|
|
}
|
|
|
|
async function selectWord(page: Page, blockIndex: number, word: string) {
|
|
const block = page.locator('.bn-block-content').nth(blockIndex)
|
|
await expect(block).toBeVisible({ timeout: 5_000 })
|
|
|
|
const selected = await block.evaluate((element, targetWord) => {
|
|
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT)
|
|
|
|
while (walker.nextNode()) {
|
|
const node = walker.currentNode
|
|
const content = node.textContent ?? ''
|
|
const index = content.indexOf(targetWord)
|
|
|
|
if (index === -1) continue
|
|
|
|
const selection = window.getSelection()
|
|
if (!selection) return false
|
|
|
|
const range = document.createRange()
|
|
range.setStart(node, index)
|
|
range.setEnd(node, index + targetWord.length)
|
|
selection.removeAllRanges()
|
|
selection.addRange(range)
|
|
document.dispatchEvent(new Event('selectionchange'))
|
|
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}, word)
|
|
|
|
expect(selected).toBe(true)
|
|
await expect(page.locator('.bn-formatting-toolbar')).toBeVisible({ timeout: 5_000 })
|
|
}
|
|
|
|
async function openBlockTypeMenu(page: Page) {
|
|
const blockTypeButton = page.getByRole('button', { name: 'Paragraph' })
|
|
await blockTypeButton.focus()
|
|
await page.keyboard.press('Enter')
|
|
}
|
|
|
|
async function assertSlashMenuBlockCommandPersists(page: Page, options: {
|
|
query: string
|
|
optionName: RegExp
|
|
insertedText: string
|
|
rawAssertion: (raw: string) => void
|
|
blockContentType: string
|
|
}) {
|
|
await openNote(page, 'Note B')
|
|
await page.locator('.bn-block-content').nth(1).click()
|
|
await page.keyboard.type(options.query)
|
|
await expect(page.getByRole('option', { name: options.optionName })).toBeVisible()
|
|
await page.keyboard.press('Enter')
|
|
await page.keyboard.type(options.insertedText)
|
|
await page.waitForTimeout(700)
|
|
|
|
await roundTripThroughAnotherNote(page)
|
|
await openRawMode(page)
|
|
|
|
const raw = await getRawEditorContent(page)
|
|
options.rawAssertion(raw)
|
|
|
|
await openBlockNoteMode(page)
|
|
await expect(
|
|
page.locator(
|
|
`.bn-block-content[data-content-type="${options.blockContentType}"]`,
|
|
).first(),
|
|
).toContainText(options.insertedText)
|
|
}
|
|
|
|
async function assertKeyboardBoldPersists(page: Page, options: {
|
|
selectionText: string
|
|
rawIncludes: string
|
|
rawExcludes?: string
|
|
}) {
|
|
await openNote(page, 'Note B')
|
|
await selectWord(page, 1, options.selectionText)
|
|
await page.keyboard.press('Meta+b')
|
|
await page.waitForTimeout(700)
|
|
|
|
await roundTripThroughAnotherNote(page)
|
|
await openRawMode(page)
|
|
|
|
const raw = await getRawEditorContent(page)
|
|
expect(raw).toContain(options.rawIncludes)
|
|
if (options.rawExcludes) {
|
|
expect(raw).not.toContain(options.rawExcludes)
|
|
}
|
|
}
|
|
|
|
const slashMenuPersistenceScenarios = [
|
|
{
|
|
name: 'slash menu block commands persist bullet lists',
|
|
query: '/bul',
|
|
optionName: /Bullet List/i,
|
|
insertedText: 'Persisted bullet',
|
|
rawAssertion: (raw: string) => {
|
|
expect(raw).toContain('- Persisted bullet')
|
|
},
|
|
blockContentType: 'bulletListItem',
|
|
},
|
|
{
|
|
name: 'slash menu block commands persist code blocks',
|
|
query: '/code',
|
|
optionName: /Code Block/i,
|
|
insertedText: 'const persistedAnswer = 42',
|
|
rawAssertion: (raw: string) => {
|
|
expect(raw).toMatch(/```[\w-]*\nconst persistedAnswer = 42/m)
|
|
},
|
|
blockContentType: 'codeBlock',
|
|
},
|
|
] as const
|
|
|
|
test('toolbar only exposes audited markdown-safe formatting controls', async ({ page }) => {
|
|
await openNote(page, 'Note B')
|
|
await selectWord(page, 1, 'referenced')
|
|
|
|
await expect(page.getByRole('button', { name: 'Paragraph' })).toBeVisible()
|
|
await expect(page.locator('.bn-formatting-toolbar [data-test="bold"]')).toBeVisible()
|
|
await expect(page.locator('.bn-formatting-toolbar [data-test="italic"]')).toBeVisible()
|
|
await expect(page.locator('.bn-formatting-toolbar [data-test="code"]')).toBeVisible()
|
|
await expect(page.locator('.bn-formatting-toolbar [data-test="strike"]')).toBeVisible()
|
|
await expect(page.locator('.bn-formatting-toolbar [data-test="createLink"]')).toBeVisible()
|
|
|
|
await openBlockTypeMenu(page)
|
|
await expect(page.getByRole('menuitem', { name: 'Heading 1' })).toBeVisible()
|
|
await expect(page.getByRole('menuitem', { name: 'Heading 6' })).toBeVisible()
|
|
await expect(page.getByRole('menuitem', { name: 'Quote' })).toBeVisible()
|
|
await expect(page.getByRole('menuitem', { name: 'Bullet List' })).toBeVisible()
|
|
await expect(page.getByRole('menuitem', { name: 'Numbered List' })).toBeVisible()
|
|
await expect(page.getByRole('menuitem', { name: 'Checklist' })).toBeVisible()
|
|
await expect(page.getByRole('menuitem', { name: 'Code Block' })).toBeVisible()
|
|
await expect(page.getByRole('menuitem', { name: 'Toggle List' })).toHaveCount(0)
|
|
|
|
await expect(page.locator('.bn-formatting-toolbar [data-test="underline"]')).toHaveCount(0)
|
|
await expect(page.locator('.bn-formatting-toolbar [data-test="colors"]')).toHaveCount(0)
|
|
await expect(page.locator('.bn-formatting-toolbar [data-test="alignTextLeft"]')).toHaveCount(0)
|
|
await expect(page.locator('.bn-formatting-toolbar [data-test="alignTextCenter"]')).toHaveCount(0)
|
|
await expect(page.locator('.bn-formatting-toolbar [data-test="alignTextRight"]')).toHaveCount(0)
|
|
})
|
|
|
|
test('supported inline formatting persists after note switches when applied from keyboard', async ({ page }) => {
|
|
await assertKeyboardBoldPersists(page, {
|
|
selectionText: 'referenced',
|
|
rawIncludes: 'This is Note B, **referenced** by Alpha Project.',
|
|
})
|
|
})
|
|
|
|
test('bold formatting keeps trailing whitespace outside markdown markers', async ({ page }) => {
|
|
await assertKeyboardBoldPersists(page, {
|
|
selectionText: 'referenced ',
|
|
rawIncludes: 'This is Note B, **referenced** by Alpha Project.',
|
|
rawExcludes: '**referenced **',
|
|
})
|
|
})
|
|
|
|
test('toolbar block-type commands persist numbered lists', async ({ page }) => {
|
|
await openNote(page, 'Note B')
|
|
await selectWord(page, 1, 'This')
|
|
await openBlockTypeMenu(page)
|
|
await page.getByRole('menuitem', { name: 'Numbered List' }).click()
|
|
await page.waitForTimeout(700)
|
|
|
|
await roundTripThroughAnotherNote(page)
|
|
await openRawMode(page)
|
|
|
|
const raw = await getRawEditorContent(page)
|
|
expect(raw).toContain('1. This is Note B, referenced by Alpha Project.')
|
|
|
|
await openBlockNoteMode(page)
|
|
await expect(page.locator('.bn-block-content[data-content-type="numberedListItem"]').first()).toContainText(
|
|
'This is Note B, referenced by Alpha Project.',
|
|
)
|
|
})
|
|
|
|
for (const scenario of slashMenuPersistenceScenarios) {
|
|
test(scenario.name, async ({ page }) => {
|
|
await assertSlashMenuBlockCommandPersists(page, scenario)
|
|
})
|
|
}
|