153 lines
5.1 KiB
TypeScript
153 lines
5.1 KiB
TypeScript
import { expect, test, type Page } from '@playwright/test'
|
|
import fs from 'fs'
|
|
import path from 'path'
|
|
import {
|
|
createFixtureVaultCopy,
|
|
openFixtureVault,
|
|
removeFixtureVaultCopy,
|
|
} from '../helpers/fixtureVault'
|
|
import { executeCommand, openCommandPalette, sendShortcut } from './helpers'
|
|
|
|
const CODE_COPY_NOTE_RELATIVE_PATH = path.join('note', 'fenced-code-copy.md')
|
|
const CODE_COPY_NOTE_TITLE = 'Fenced Code Copy'
|
|
const JSON_SNIPPET = '{\n "id": "Demo",\n "enabled": true\n}'
|
|
const TYPESCRIPT_SNIPPET = 'const answer = 42\nconsole.log(answer)'
|
|
const CJK_SNIPPET = 'const label = "中文測試"\nconsole.log(label)'
|
|
const RICH_CODE_SELECTOR = '.bn-block-content[data-content-type="codeBlock"] pre code'
|
|
|
|
function writeCodeCopyFixtureNote(tempVaultDir: string) {
|
|
const notePath = path.join(tempVaultDir, CODE_COPY_NOTE_RELATIVE_PATH)
|
|
fs.mkdirSync(path.dirname(notePath), { recursive: true })
|
|
fs.writeFileSync(notePath, `---
|
|
Is A: Note
|
|
Status: Active
|
|
---
|
|
|
|
# ${CODE_COPY_NOTE_TITLE}
|
|
|
|
Copy this JSON exactly:
|
|
|
|
\`\`\`json
|
|
${JSON_SNIPPET}
|
|
\`\`\`
|
|
|
|
Copy this TypeScript exactly:
|
|
|
|
\`\`\`ts
|
|
${TYPESCRIPT_SNIPPET}
|
|
\`\`\`
|
|
|
|
Copy this CJK text exactly:
|
|
|
|
\`\`\`ts
|
|
${CJK_SNIPPET}
|
|
\`\`\`
|
|
`)
|
|
}
|
|
|
|
async function clearClipboard(page: Page) {
|
|
await page.evaluate(() => navigator.clipboard.writeText(''))
|
|
}
|
|
|
|
async function readClipboard(page: Page) {
|
|
return page.evaluate(() => navigator.clipboard.readText())
|
|
}
|
|
|
|
async function copySelectedText(page: Page) {
|
|
await clearClipboard(page)
|
|
await sendShortcut(page, 'c', ['Control'])
|
|
return readClipboard(page)
|
|
}
|
|
|
|
async function expectRichCodeBlockButtonCopy(page: Page, blockIndex: number, expectedText: string) {
|
|
await clearClipboard(page)
|
|
const codeBlock = page.locator('.bn-block-content[data-content-type="codeBlock"]').nth(blockIndex)
|
|
await expect(codeBlock).toBeVisible()
|
|
await codeBlock.hover()
|
|
const copyButton = page.getByRole('button', { name: 'Copy code to clipboard' })
|
|
await expect(copyButton).toBeVisible()
|
|
await copyButton.click()
|
|
await expect.poll(() => readClipboard(page)).toBe(expectedText)
|
|
}
|
|
|
|
async function selectRichCodeBlock(page: Page, blockIndex: number) {
|
|
await page.locator(RICH_CODE_SELECTOR).nth(blockIndex).waitFor({ timeout: 10_000 })
|
|
await page.evaluate(({ selector, index }) => {
|
|
const code = document.querySelectorAll(selector)[index]
|
|
if (!code) throw new Error(`Missing rendered code block ${index}`)
|
|
|
|
const range = document.createRange()
|
|
range.selectNodeContents(code)
|
|
const selection = window.getSelection()
|
|
selection?.removeAllRanges()
|
|
selection?.addRange(range)
|
|
}, { selector: RICH_CODE_SELECTOR, index: blockIndex })
|
|
}
|
|
|
|
async function selectRawEditorText(page: Page, text: string) {
|
|
await expect(page.locator('.cm-content')).toBeVisible({ timeout: 10_000 })
|
|
await page.evaluate((expectedText) => {
|
|
const content = document.querySelector('.cm-content') as (HTMLElement & {
|
|
cmTile?: {
|
|
view?: {
|
|
state: { doc: { toString: () => string } }
|
|
focus: () => void
|
|
dispatch: (transaction: { selection: { anchor: number; head: number } }) => void
|
|
}
|
|
}
|
|
}) | null
|
|
const view = content?.cmTile?.view
|
|
if (!view) throw new Error('CodeMirror view is not available')
|
|
|
|
const documentText = view.state.doc.toString()
|
|
const start = documentText.indexOf(expectedText)
|
|
if (start === -1) throw new Error(`Raw editor text not found: ${expectedText}`)
|
|
|
|
view.focus()
|
|
view.dispatch({ selection: { anchor: start, head: start + expectedText.length } })
|
|
}, text)
|
|
}
|
|
|
|
test.describe('Fenced code copy', () => {
|
|
let tempVaultDir: string
|
|
|
|
test.setTimeout(30_000)
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.context().grantPermissions(['clipboard-read', 'clipboard-write'])
|
|
tempVaultDir = createFixtureVaultCopy()
|
|
writeCodeCopyFixtureNote(tempVaultDir)
|
|
await openFixtureVault(page, tempVaultDir)
|
|
})
|
|
|
|
test.afterEach(() => {
|
|
removeFixtureVaultCopy(tempVaultDir)
|
|
})
|
|
|
|
test('copies rich code blocks from the button and selections without corruption', async ({ page }) => {
|
|
const noteList = page.locator('[data-testid="note-list-container"]')
|
|
const noteItem = noteList.getByText(CODE_COPY_NOTE_TITLE, { exact: true })
|
|
await expect(noteItem).toBeVisible({ timeout: 10_000 })
|
|
await noteItem.click()
|
|
await expect(page.locator(RICH_CODE_SELECTOR)).toHaveCount(3, { timeout: 10_000 })
|
|
|
|
await expectRichCodeBlockButtonCopy(page, 0, JSON_SNIPPET)
|
|
await expectRichCodeBlockButtonCopy(page, 1, TYPESCRIPT_SNIPPET)
|
|
await expectRichCodeBlockButtonCopy(page, 2, CJK_SNIPPET)
|
|
|
|
await selectRichCodeBlock(page, 0)
|
|
await expect.poll(() => copySelectedText(page)).toBe(JSON_SNIPPET)
|
|
|
|
await selectRichCodeBlock(page, 1)
|
|
await expect.poll(() => copySelectedText(page)).toBe(TYPESCRIPT_SNIPPET)
|
|
|
|
await selectRichCodeBlock(page, 2)
|
|
await expect.poll(() => copySelectedText(page)).toBe(CJK_SNIPPET)
|
|
|
|
await openCommandPalette(page)
|
|
await executeCommand(page, 'Toggle Raw')
|
|
await selectRawEditorText(page, JSON_SNIPPET)
|
|
await expect.poll(() => copySelectedText(page)).toBe(JSON_SNIPPET)
|
|
})
|
|
})
|