feat: publish HoloLake model-native living system source
This commit is contained in:
parent
6ad10edde1
commit
c395dd3a99
2467 changed files with 615073 additions and 0 deletions
|
|
@ -0,0 +1,498 @@
|
|||
import fs from 'fs'
|
||||
import { test, expect, type Page } from '@playwright/test'
|
||||
import { APP_COMMAND_IDS } from '../../src/hooks/appCommandCatalog'
|
||||
import { createFixtureVaultCopy, openFixtureVaultTauri, removeFixtureVaultCopy } from '../helpers/fixtureVault'
|
||||
import { triggerMenuCommand, triggerShortcutCommand } from './testBridge'
|
||||
|
||||
function markdownFiles(vaultPath: string): string[] {
|
||||
return fs.readdirSync(vaultPath).filter((name) => name.endsWith('.md')).sort()
|
||||
}
|
||||
|
||||
function slugifyTitle(title: string): string {
|
||||
return title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '')
|
||||
}
|
||||
|
||||
interface FileExpectation {
|
||||
vaultPath: string
|
||||
filename: string
|
||||
}
|
||||
|
||||
interface FileContentExpectation extends FileExpectation {
|
||||
text: string
|
||||
}
|
||||
|
||||
interface EmptyTitleHeadingState {
|
||||
contentType: string | null
|
||||
placeholder: string | null
|
||||
}
|
||||
|
||||
const REACT_UPDATE_LOOP_FRAGMENTS = [
|
||||
'Maximum update depth',
|
||||
'React error #185',
|
||||
'#185',
|
||||
]
|
||||
|
||||
function collectReactUpdateLoopErrors(page: Page): string[] {
|
||||
const errors: string[] = []
|
||||
const collect = (message: string) => {
|
||||
if (REACT_UPDATE_LOOP_FRAGMENTS.some((fragment) => message.includes(fragment))) {
|
||||
errors.push(message)
|
||||
}
|
||||
}
|
||||
|
||||
page.on('pageerror', (error) => collect(error.message))
|
||||
page.on('console', (message) => {
|
||||
if (message.type() === 'error') collect(message.text())
|
||||
})
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
async function createUntitledNote(page: Page): Promise<void> {
|
||||
await page.locator('body').click()
|
||||
await triggerMenuCommand(page, 'file-new-note')
|
||||
await expect(page.locator('.bn-editor')).toBeVisible({ timeout: 5_000 })
|
||||
await expect(page.getByTestId('breadcrumb-filename-trigger')).toContainText(/untitled-note-\d+(?:-\d+)?/i, {
|
||||
timeout: 5_000,
|
||||
})
|
||||
await expectStableEmptyTitleHeading(page)
|
||||
}
|
||||
|
||||
async function writeNewHeading(page: Page, title: string): Promise<void> {
|
||||
await page.keyboard.type(title)
|
||||
await page.keyboard.press('Enter')
|
||||
}
|
||||
|
||||
async function writeNewHeadingAndBody(page: Page, title: string, body: string): Promise<void> {
|
||||
await page.keyboard.type(title, { delay: 20 })
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.type(body, { delay: 20 })
|
||||
}
|
||||
|
||||
async function expectRenamedFile({ vaultPath, filename }: FileExpectation): Promise<void> {
|
||||
await expect(async () => {
|
||||
expect(markdownFiles(vaultPath)).toContain(filename)
|
||||
}).toPass({ timeout: 10_000 })
|
||||
}
|
||||
|
||||
async function expectFileMissing({ vaultPath, filename }: FileExpectation): Promise<void> {
|
||||
await expect(async () => {
|
||||
expect(markdownFiles(vaultPath)).not.toContain(filename)
|
||||
}).toPass({ timeout: 10_000 })
|
||||
}
|
||||
|
||||
async function expectFileContentContains({ vaultPath, filename, text }: FileContentExpectation): Promise<void> {
|
||||
await expect(async () => {
|
||||
const content = fs.readFileSync(`${vaultPath}/${filename}`, 'utf-8')
|
||||
expect(content).toContain(text)
|
||||
}).toPass({ timeout: 10_000 })
|
||||
}
|
||||
|
||||
async function expectActiveFilename(page: Page, filenameStem: string): Promise<void> {
|
||||
await expect(page.getByTestId('breadcrumb-filename-trigger')).toContainText(filenameStem, { timeout: 10_000 })
|
||||
}
|
||||
|
||||
async function expectEditorFocused(page: Page): Promise<void> {
|
||||
await expect.poll(async () => page.evaluate(() => {
|
||||
const active = document.activeElement as HTMLElement | null
|
||||
return Boolean(active?.isContentEditable || active?.closest('[contenteditable="true"]'))
|
||||
}), {
|
||||
timeout: 5_000,
|
||||
}).toBe(true)
|
||||
}
|
||||
|
||||
async function readEmptyTitleHeadingState(page: Page): Promise<EmptyTitleHeadingState> {
|
||||
return page.evaluate(() => {
|
||||
const firstBlock = document.querySelector('.bn-block-content') as HTMLElement | null
|
||||
const inlineHeading = firstBlock?.querySelector('.bn-inline-content') as HTMLElement | null
|
||||
return {
|
||||
contentType: firstBlock?.getAttribute('data-content-type') ?? null,
|
||||
placeholder: inlineHeading ? getComputedStyle(inlineHeading, '::before').content : null,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function selectionInsideEmptyTitleHeading(page: Page): Promise<boolean> {
|
||||
return page.evaluate(() => {
|
||||
const firstBlock = document.querySelector('.bn-block-content') as HTMLElement | null
|
||||
const selection = window.getSelection()
|
||||
const anchorNode = selection?.anchorNode ?? null
|
||||
const anchorElement = anchorNode instanceof Element ? anchorNode : anchorNode?.parentElement ?? null
|
||||
return Boolean(selection?.rangeCount && anchorElement && firstBlock?.contains(anchorElement))
|
||||
})
|
||||
}
|
||||
|
||||
async function expectReadyEmptyTitleHeading(page: Page): Promise<void> {
|
||||
await expectEditorFocused(page)
|
||||
await expect.poll(() => readEmptyTitleHeadingState(page), {
|
||||
timeout: 5_000,
|
||||
}).toEqual({
|
||||
contentType: 'heading',
|
||||
placeholder: '"Title"',
|
||||
})
|
||||
await expect.poll(() => selectionInsideEmptyTitleHeading(page), { timeout: 5_000 }).toBe(true)
|
||||
}
|
||||
|
||||
async function expectStableEmptyTitleHeading(page: Page): Promise<void> {
|
||||
await expectReadyEmptyTitleHeading(page)
|
||||
await page.waitForTimeout(300)
|
||||
await expectReadyEmptyTitleHeading(page)
|
||||
}
|
||||
|
||||
async function activeSelectionBlockType(page: Page): Promise<string | null> {
|
||||
return page.evaluate(() => {
|
||||
const anchorNode = window.getSelection()?.anchorNode
|
||||
if (!anchorNode) return null
|
||||
|
||||
const anchorElement = anchorNode instanceof Element ? anchorNode : anchorNode.parentElement
|
||||
if (!anchorElement) return null
|
||||
|
||||
return anchorElement.closest('.bn-block-content')?.getAttribute('data-content-type') ?? null
|
||||
})
|
||||
}
|
||||
|
||||
async function getRawEditorContent(page: Page): Promise<string> {
|
||||
return page.evaluate(() => {
|
||||
const host = document.querySelector('[data-testid="raw-editor-codemirror"]') as (Element & {
|
||||
__cmView?: { state: { doc: { toString: () => string } } }
|
||||
}) | null
|
||||
const view = host?.__cmView
|
||||
if (!view) throw new Error('CodeMirror view is missing')
|
||||
return view.state.doc.toString()
|
||||
})
|
||||
}
|
||||
|
||||
async function setRawEditorContent(page: Page, content: string): Promise<void> {
|
||||
await page.evaluate((nextContent) => {
|
||||
const host = document.querySelector('[data-testid="raw-editor-codemirror"]') as (Element & {
|
||||
__cmView?: {
|
||||
state: { doc: { toString: () => string } }
|
||||
dispatch: (spec: { changes: { from: number; to: number; insert: string } }) => void
|
||||
}
|
||||
}) | null
|
||||
const view = host?.__cmView
|
||||
if (!view) throw new Error('CodeMirror view is missing')
|
||||
view.dispatch({
|
||||
changes: { from: 0, to: view.state.doc.toString().length, insert: nextContent },
|
||||
})
|
||||
}, content)
|
||||
}
|
||||
|
||||
async function openRawEditor(page: Page): Promise<void> {
|
||||
await triggerShortcutCommand(page, APP_COMMAND_IDS.editToggleRawEditor)
|
||||
await expect(page.getByTestId('raw-editor-codemirror')).toBeVisible({ timeout: 5_000 })
|
||||
}
|
||||
|
||||
async function openRichEditor(page: Page): Promise<void> {
|
||||
await triggerShortcutCommand(page, APP_COMMAND_IDS.editToggleRawEditor)
|
||||
await expect(page.locator('.bn-editor')).toBeVisible({ timeout: 5_000 })
|
||||
}
|
||||
|
||||
async function expectTitleHeadingText(page: Page, title: string): Promise<void> {
|
||||
await expect(page.locator('.bn-editor [data-content-type="heading"]').first()).toContainText(title, {
|
||||
timeout: 5_000,
|
||||
})
|
||||
}
|
||||
|
||||
async function dispatchRichHeadingPaste(page: Page, text: string): Promise<void> {
|
||||
await page.evaluate((textFragment) => {
|
||||
const target = document.querySelector<HTMLElement>(
|
||||
[
|
||||
'.bn-editor [data-content-type="heading"][data-level="1"] .bn-inline-content',
|
||||
'.bn-editor [data-content-type="heading"]:not([data-level]) .bn-inline-content',
|
||||
'.bn-editor h1',
|
||||
].join(', '),
|
||||
)
|
||||
if (!target) throw new Error('Title heading paste target was not found')
|
||||
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData('text/html', '<h1>Rich <em>Paste</em> Payload</h1><table><tr><td>structured</td></tr></table>')
|
||||
clipboardData.setData('text/plain', textFragment)
|
||||
|
||||
const event = new Event('paste', { bubbles: true, cancelable: true })
|
||||
Object.defineProperty(event, 'clipboardData', { value: clipboardData })
|
||||
|
||||
if (target.dispatchEvent(event)) {
|
||||
throw new Error('Rich title-heading paste was not intercepted')
|
||||
}
|
||||
}, text)
|
||||
}
|
||||
|
||||
async function clickEditorChromeBelowLastBlock(page: Page): Promise<void> {
|
||||
const point = await page.evaluate(() => {
|
||||
const container = document.querySelector('.editor__blocknote-container')?.getBoundingClientRect()
|
||||
const blocks = Array.from(document.querySelectorAll('.bn-block-outer'))
|
||||
const lastBlock = blocks.at(-1)?.getBoundingClientRect()
|
||||
if (!container || !lastBlock) return null
|
||||
|
||||
return {
|
||||
x: Math.min(container.right - 24, Math.max(container.left + 24, lastBlock.left + 40)),
|
||||
y: Math.min(container.bottom - 24, lastBlock.bottom + 18),
|
||||
}
|
||||
})
|
||||
|
||||
expect(point).not.toBeNull()
|
||||
await page.mouse.click(point!.x, point!.y)
|
||||
}
|
||||
|
||||
let tempVaultDir: string
|
||||
|
||||
test.beforeEach(async ({ page }, testInfo) => {
|
||||
testInfo.setTimeout(90_000)
|
||||
tempVaultDir = createFixtureVaultCopy()
|
||||
await openFixtureVaultTauri(page, tempVaultDir)
|
||||
})
|
||||
|
||||
test.afterEach(async () => {
|
||||
removeFixtureVaultCopy(tempVaultDir)
|
||||
})
|
||||
|
||||
test('new-note H1 auto-rename keeps the editor usable and leaves no untitled duplicates', async ({ page }) => {
|
||||
const errors: string[] = []
|
||||
page.on('pageerror', (err) => {
|
||||
errors.push(err.message)
|
||||
})
|
||||
|
||||
const titles = [
|
||||
'Fresh Focus Title',
|
||||
'Rapid Rename 2',
|
||||
'Rapid Rename 3',
|
||||
'Rapid Rename 4',
|
||||
'Rapid Rename 5',
|
||||
]
|
||||
|
||||
for (const [index, title] of titles.entries()) {
|
||||
await createUntitledNote(page)
|
||||
if (index === 0) {
|
||||
await writeNewHeadingAndBody(page, title, 'Body continues while rename is pending.')
|
||||
} else {
|
||||
await writeNewHeading(page, title)
|
||||
}
|
||||
await expectActiveFilename(page, slugifyTitle(title))
|
||||
await expectRenamedFile({ vaultPath: tempVaultDir, filename: `${slugifyTitle(title)}.md` })
|
||||
await expectEditorFocused(page)
|
||||
await expectFileContentContains({
|
||||
vaultPath: tempVaultDir,
|
||||
filename: `${slugifyTitle(title)}.md`,
|
||||
text: `# ${title}`,
|
||||
})
|
||||
|
||||
if (index === 0) {
|
||||
await expectFileContentContains({
|
||||
vaultPath: tempVaultDir,
|
||||
filename: 'fresh-focus-title.md',
|
||||
text: 'Body continues while rename is pending.',
|
||||
})
|
||||
await page.keyboard.type(' focus-probe')
|
||||
await expectFileContentContains({
|
||||
vaultPath: tempVaultDir,
|
||||
filename: 'fresh-focus-title.md',
|
||||
text: 'focus-probe',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const files = markdownFiles(tempVaultDir)
|
||||
expect(files).toContain('fresh-focus-title.md')
|
||||
expect(files.filter((name) => name.startsWith('untitled-note-'))).toEqual([])
|
||||
expect(files.filter((name) => /^rapid-rename-\d+\.md$/.test(name))).toHaveLength(4)
|
||||
expect(errors).toEqual([])
|
||||
})
|
||||
|
||||
test('new-note short title typing and rich heading paste stay in the H1 until Enter', async ({ page }) => {
|
||||
const errors = collectReactUpdateLoopErrors(page)
|
||||
const titleStart = 'Obsi'
|
||||
const title = 'Obsidian'
|
||||
const richPasteSuffix = ' Rich Paste Payload'
|
||||
const pastedTitle = `${title}${richPasteSuffix}`
|
||||
const bodyText = 'Body starts only after intentional Enter.'
|
||||
|
||||
await createUntitledNote(page)
|
||||
await page.keyboard.type(titleStart, { delay: 80 })
|
||||
await expectTitleHeadingText(page, titleStart)
|
||||
await expectEditorFocused(page)
|
||||
await expect.poll(() => activeSelectionBlockType(page), { timeout: 5_000 }).toBe('heading')
|
||||
|
||||
await page.waitForTimeout(1_000)
|
||||
await expectTitleHeadingText(page, titleStart)
|
||||
await expect.poll(() => activeSelectionBlockType(page), { timeout: 5_000 }).toBe('heading')
|
||||
|
||||
await page.keyboard.type('dian', { delay: 80 })
|
||||
await expectTitleHeadingText(page, title)
|
||||
await page.keyboard.press('End')
|
||||
await dispatchRichHeadingPaste(page, richPasteSuffix)
|
||||
await expectTitleHeadingText(page, pastedTitle)
|
||||
await expect.poll(() => activeSelectionBlockType(page), { timeout: 5_000 }).toBe('heading')
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.type(bodyText, { delay: 35 })
|
||||
|
||||
await expectActiveFilename(page, slugifyTitle(pastedTitle))
|
||||
await expectFileContentContains({
|
||||
vaultPath: tempVaultDir,
|
||||
filename: `${slugifyTitle(pastedTitle)}.md`,
|
||||
text: `# ${pastedTitle}`,
|
||||
})
|
||||
await expectFileContentContains({
|
||||
vaultPath: tempVaultDir,
|
||||
filename: `${slugifyTitle(pastedTitle)}.md`,
|
||||
text: bodyText,
|
||||
})
|
||||
expect(errors).toEqual([])
|
||||
})
|
||||
|
||||
test('@smoke new-note typing stays focused through initial save settlement', async ({ page }) => {
|
||||
const title = 'Creation Focus Guard'
|
||||
const bodyText = 'Body keeps accepting text while creation writes and saves settle.'
|
||||
|
||||
await createUntitledNote(page)
|
||||
await page.keyboard.type(title, { delay: 35 })
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.type(bodyText, { delay: 35 })
|
||||
await page.waitForTimeout(1_000)
|
||||
|
||||
await expectEditorFocused(page)
|
||||
await page.keyboard.type(' Still focused.')
|
||||
await expectActiveFilename(page, slugifyTitle(title))
|
||||
await expectFileContentContains({
|
||||
vaultPath: tempVaultDir,
|
||||
filename: `${slugifyTitle(title)}.md`,
|
||||
text: 'Still focused.',
|
||||
})
|
||||
})
|
||||
|
||||
test('new-note editor mode roundtrip stays editable after auto-rename', async ({ page }) => {
|
||||
const errors: string[] = []
|
||||
page.on('pageerror', (err) => {
|
||||
errors.push(err.message)
|
||||
})
|
||||
const title = 'Mode Switch Crash Guard'
|
||||
const filename = `${slugifyTitle(title)}.md`
|
||||
const rawLine = 'Raw mode edit after auto-rename.'
|
||||
const richLine = 'Rich editor still accepts typing after the raw-mode return.'
|
||||
|
||||
await createUntitledNote(page)
|
||||
await writeNewHeadingAndBody(page, title, 'Initial body before the raw-mode round trip.')
|
||||
await expectActiveFilename(page, slugifyTitle(title))
|
||||
await expectRenamedFile({ vaultPath: tempVaultDir, filename })
|
||||
|
||||
await openRawEditor(page)
|
||||
const rawContent = await getRawEditorContent(page)
|
||||
await setRawEditorContent(page, `${rawContent}\n\n${rawLine}`)
|
||||
await page.waitForTimeout(650)
|
||||
|
||||
await openRichEditor(page)
|
||||
await expect(page.locator('.bn-editor')).toContainText(rawLine, { timeout: 5_000 })
|
||||
await page.locator('.bn-editor').click()
|
||||
await page.keyboard.type(richLine)
|
||||
|
||||
await expectFileContentContains({
|
||||
vaultPath: tempVaultDir,
|
||||
filename,
|
||||
text: rawLine,
|
||||
})
|
||||
await expectFileContentContains({
|
||||
vaultPath: tempVaultDir,
|
||||
filename,
|
||||
text: richLine,
|
||||
})
|
||||
expect(errors).toEqual([])
|
||||
})
|
||||
|
||||
test('new-note H1 auto-rename preserves body typing and cursor while rename lands', async ({ page }) => {
|
||||
const errors: string[] = []
|
||||
page.on('pageerror', (err) => {
|
||||
errors.push(err.message)
|
||||
})
|
||||
|
||||
await createUntitledNote(page)
|
||||
await page.keyboard.type('Cursor Stable Rename', { delay: 30 })
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// Let the initial untitled save settle so the rename timer can fire mid-body typing.
|
||||
await page.waitForTimeout(700)
|
||||
|
||||
const bodyText = 'Body keeps flowing through the rename without losing the caret or freezing.'
|
||||
await page.keyboard.type(bodyText, { delay: 70 })
|
||||
|
||||
await expectActiveFilename(page, 'cursor-stable-rename')
|
||||
await expectRenamedFile({ vaultPath: tempVaultDir, filename: 'cursor-stable-rename.md' })
|
||||
await expectFileContentContains({
|
||||
vaultPath: tempVaultDir,
|
||||
filename: 'cursor-stable-rename.md',
|
||||
text: bodyText,
|
||||
})
|
||||
await expectEditorFocused(page)
|
||||
await expect.poll(() => activeSelectionBlockType(page), { timeout: 5_000 }).toBe('paragraph')
|
||||
|
||||
await page.keyboard.type(' Still typing after rename.')
|
||||
await expectFileContentContains({
|
||||
vaultPath: tempVaultDir,
|
||||
filename: 'cursor-stable-rename.md',
|
||||
text: 'Still typing after rename.',
|
||||
})
|
||||
await expectEditorFocused(page)
|
||||
await expect(errors).toEqual([])
|
||||
})
|
||||
|
||||
test('fresh-note Enter stays stable after autosave and editor chrome clicks', async ({ page }) => {
|
||||
const errors: string[] = []
|
||||
page.on('pageerror', (err) => {
|
||||
errors.push(err.message)
|
||||
})
|
||||
const title = 'Enter Selection Guard'
|
||||
const filename = 'enter-selection-guard.md'
|
||||
const firstLine = 'First paragraph before chrome clicks.'
|
||||
const secondLine = 'Second paragraph after editor chrome click.'
|
||||
const thirdLine = 'Third paragraph after another chrome click.'
|
||||
|
||||
await createUntitledNote(page)
|
||||
await page.keyboard.type(title, { delay: 35 })
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.type(firstLine, { delay: 35 })
|
||||
|
||||
await expectActiveFilename(page, 'enter-selection-guard')
|
||||
await expectRenamedFile({ vaultPath: tempVaultDir, filename })
|
||||
await page.waitForTimeout(900)
|
||||
|
||||
await clickEditorChromeBelowLastBlock(page)
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.type(secondLine, { delay: 35 })
|
||||
|
||||
await clickEditorChromeBelowLastBlock(page)
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.type(thirdLine, { delay: 35 })
|
||||
|
||||
await expect(page.locator('.error-boundary')).toHaveCount(0)
|
||||
await expectEditorFocused(page)
|
||||
expect(errors).toEqual([])
|
||||
await expectFileContentContains({ vaultPath: tempVaultDir, filename, text: firstLine })
|
||||
await expectFileContentContains({ vaultPath: tempVaultDir, filename, text: secondLine })
|
||||
await expectFileContentContains({ vaultPath: tempVaultDir, filename, text: thirdLine })
|
||||
})
|
||||
|
||||
test('@smoke new-note H1 auto-rename does not recreate the untitled file when a buffered save lands after rename', async ({ page }) => {
|
||||
const title = 'Late Save Guard'
|
||||
const lateBody = 'Body typed right before rename'
|
||||
|
||||
await createUntitledNote(page)
|
||||
const untitledStem = (await page.getByTestId('breadcrumb-filename-trigger').textContent())?.trim()
|
||||
expect(untitledStem).toMatch(/^untitled-note-\d+(?:-\d+)?$/i)
|
||||
|
||||
await writeNewHeading(page, title)
|
||||
await page.waitForTimeout(2_600)
|
||||
await page.keyboard.type(lateBody)
|
||||
|
||||
await expectActiveFilename(page, 'late-save-guard')
|
||||
await expectRenamedFile({ vaultPath: tempVaultDir, filename: 'late-save-guard.md' })
|
||||
await expectFileContentContains({
|
||||
vaultPath: tempVaultDir,
|
||||
filename: 'late-save-guard.md',
|
||||
text: lateBody,
|
||||
})
|
||||
|
||||
await page.waitForTimeout(800)
|
||||
await expectFileMissing({
|
||||
vaultPath: tempVaultDir,
|
||||
filename: `${untitledStem}.md`,
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue