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,335 @@
|
|||
import { test, expect, type Locator, type Page } from '@playwright/test'
|
||||
import { createFixtureVaultCopy, openFixtureVault, removeFixtureVaultCopy } from '../helpers/fixtureVault'
|
||||
import { executeCommand, openCommandPalette } from './helpers'
|
||||
|
||||
const MIXED_RICH_TEXT_CONTENT = `---
|
||||
title: Note B
|
||||
type: Note
|
||||
status: Active
|
||||
---
|
||||
|
||||
# Note B
|
||||
|
||||
> 인용문
|
||||
|
||||
## 섹션
|
||||
|
||||
- 첫 번째 항목
|
||||
- 두 번째 항목
|
||||
|
||||
Plain paragraph after the list.
|
||||
`
|
||||
|
||||
const NUMBERED_REENTRY_CONTENT = `---
|
||||
title: Note B
|
||||
type: Note
|
||||
status: Active
|
||||
---
|
||||
|
||||
# Note B
|
||||
|
||||
Intro paragraph before the list.
|
||||
|
||||
1. First numbered item
|
||||
2. Second numbered item
|
||||
|
||||
Plain paragraph after the numbered list.
|
||||
`
|
||||
|
||||
const PROCEDURE_NOTE_CONTENT = `---
|
||||
title: Procedure
|
||||
type: Procedure
|
||||
status: Active
|
||||
---
|
||||
|
||||
# Procedure
|
||||
|
||||
Procedures are long-running processes tied to a [[responsibility|Responsibility]].
|
||||
- Status: Active
|
||||
- Owner: The person responsible
|
||||
|
||||
Plain follow-up paragraph after the procedure checklist.
|
||||
`
|
||||
|
||||
const NESTED_PROCEDURE_NOTE_CONTENT = `---
|
||||
title: Note B
|
||||
type: Procedure
|
||||
status: Active
|
||||
---
|
||||
|
||||
# Note B
|
||||
|
||||
Procedures are long-running processes tied to a responsibility.
|
||||
|
||||
- Prepare release
|
||||
1. Validate nested numbered content
|
||||
2. Notify the owner
|
||||
- Record outcome
|
||||
|
||||
Plain follow-up paragraph after the nested procedure checklist.
|
||||
`
|
||||
|
||||
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 toggleRawMode(page: Page, visibleSelector: string) {
|
||||
await openCommandPalette(page)
|
||||
await executeCommand(page, 'Toggle Raw')
|
||||
await expect(page.locator(visibleSelector)).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')
|
||||
const view = (el as CodeMirrorHost | null)?.cmTile?.view
|
||||
return view?.state.doc.toString() ?? el?.textContent ?? ''
|
||||
})
|
||||
}
|
||||
|
||||
async function setRawEditorContent(page: Page, content: string) {
|
||||
await page.evaluate((nextContent) => {
|
||||
type CodeMirrorHost = Element & {
|
||||
cmTile?: {
|
||||
view?: {
|
||||
state: {
|
||||
doc: {
|
||||
length: number
|
||||
}
|
||||
}
|
||||
dispatch(update: { changes: { from: number; to: number; insert: string } }): void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const el = document.querySelector('.cm-content')
|
||||
const view = (el as CodeMirrorHost | null)?.cmTile?.view
|
||||
if (!view) throw new Error('CodeMirror view not found')
|
||||
view.dispatch({
|
||||
changes: { from: 0, to: view.state.doc.length, insert: nextContent },
|
||||
})
|
||||
}, content)
|
||||
}
|
||||
|
||||
function collectEditorCrashSignals(page: Page) {
|
||||
const messages: string[] = []
|
||||
|
||||
page.on('pageerror', (error) => {
|
||||
messages.push(error.message)
|
||||
})
|
||||
|
||||
page.on('console', (message) => {
|
||||
if (message.type() === 'error') {
|
||||
messages.push(message.text())
|
||||
}
|
||||
})
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
function expectNoBlockContainerCrash(messages: string[]) {
|
||||
expect(messages.filter((message) => (
|
||||
message.includes('Invalid content for node blockContainer') ||
|
||||
message.includes('RangeError')
|
||||
))).toEqual([])
|
||||
}
|
||||
|
||||
async function loadRawContentInRichEditor(page: Page, content: string) {
|
||||
await openNote(page, 'Note B')
|
||||
await toggleRawMode(page, '.cm-content')
|
||||
await setRawEditorContent(page, content)
|
||||
await page.waitForTimeout(700)
|
||||
await toggleRawMode(page, '.bn-editor')
|
||||
}
|
||||
|
||||
async function appendMarkerToBlock(page: Page, block: Locator, marker: string) {
|
||||
await expect(block).toBeVisible({ timeout: 5_000 })
|
||||
await block.click()
|
||||
await page.keyboard.press('End')
|
||||
await page.keyboard.insertText(` ${marker}`)
|
||||
await page.waitForTimeout(900)
|
||||
}
|
||||
|
||||
async function reopenNoteB(page: Page) {
|
||||
await openNote(page, 'Note C')
|
||||
await openNote(page, 'Note B')
|
||||
}
|
||||
|
||||
async function expectListContentEditableAfterReopen(page: Page, options: {
|
||||
content: string
|
||||
markerPrefix: string
|
||||
primary: {
|
||||
selector: string
|
||||
text: string
|
||||
}
|
||||
secondary: {
|
||||
selector: string
|
||||
text: string
|
||||
}
|
||||
}) {
|
||||
const crashSignals = collectEditorCrashSignals(page)
|
||||
const markerId = Date.now()
|
||||
const primaryMarker = `${options.markerPrefix}-primary-${markerId}`
|
||||
const secondaryMarker = `${options.markerPrefix}-secondary-${markerId}`
|
||||
const reentryMarker = `${options.markerPrefix}-reentry-${markerId}`
|
||||
|
||||
await loadRawContentInRichEditor(page, options.content)
|
||||
|
||||
const primaryBlock = page.locator(options.primary.selector, {
|
||||
hasText: options.primary.text,
|
||||
}).first()
|
||||
await appendMarkerToBlock(page, primaryBlock, primaryMarker)
|
||||
|
||||
const secondaryBlock = page.locator(options.secondary.selector, {
|
||||
hasText: options.secondary.text,
|
||||
}).first()
|
||||
await appendMarkerToBlock(page, secondaryBlock, secondaryMarker)
|
||||
|
||||
await reopenNoteB(page)
|
||||
await appendMarkerToBlock(page, primaryBlock, reentryMarker)
|
||||
|
||||
expectNoBlockContainerCrash(crashSignals)
|
||||
|
||||
await toggleRawMode(page, '.cm-content')
|
||||
const raw = await getRawEditorContent(page)
|
||||
expect(raw).toContain(primaryMarker)
|
||||
expect(raw).toContain(secondaryMarker)
|
||||
expect(raw).toContain(reentryMarker)
|
||||
expectNoBlockContainerCrash(crashSignals)
|
||||
}
|
||||
|
||||
const listReopenScenarios = [
|
||||
{
|
||||
name: 'numbered list content stays editable after autosave and re-entry',
|
||||
content: NUMBERED_REENTRY_CONTENT,
|
||||
markerPrefix: 'numbered',
|
||||
primary: {
|
||||
selector: '.bn-block-content[data-content-type="numberedListItem"]',
|
||||
text: 'First numbered item',
|
||||
},
|
||||
secondary: {
|
||||
selector: '.bn-block-content',
|
||||
text: 'Plain paragraph after the numbered list.',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'nested procedure list content stays editable after save and reopen',
|
||||
content: NESTED_PROCEDURE_NOTE_CONTENT,
|
||||
markerPrefix: 'nested',
|
||||
primary: {
|
||||
selector: '.bn-block-content[data-content-type="numberedListItem"]',
|
||||
text: 'Validate nested numbered content',
|
||||
},
|
||||
secondary: {
|
||||
selector: '.bn-block-content[data-content-type="bulletListItem"]',
|
||||
text: 'Record outcome',
|
||||
},
|
||||
},
|
||||
] as const
|
||||
|
||||
test('mixed rich-text blocks with Korean list content stay editable after action clicks', async ({ page }) => {
|
||||
const crashSignals = collectEditorCrashSignals(page)
|
||||
|
||||
await openNote(page, 'Note B')
|
||||
await toggleRawMode(page, '.cm-content')
|
||||
await setRawEditorContent(page, MIXED_RICH_TEXT_CONTENT)
|
||||
await page.waitForTimeout(700)
|
||||
await toggleRawMode(page, '.bn-editor')
|
||||
|
||||
const bulletBlock = page.locator('.bn-block-content', { hasText: '첫 번째 항목' }).first()
|
||||
await expect(bulletBlock).toBeVisible({ timeout: 5_000 })
|
||||
|
||||
await page.locator('.bn-block-content', { hasText: '인용문' }).first().click()
|
||||
await page.keyboard.press('End')
|
||||
await page.keyboard.insertText(' 추가')
|
||||
await bulletBlock.hover()
|
||||
await expect(page.locator('.bn-side-menu').first()).toBeVisible({ timeout: 5_000 })
|
||||
await page.locator('.bn-side-menu').first().click({ force: true })
|
||||
await page.keyboard.press('Escape')
|
||||
await bulletBlock.click()
|
||||
await page.keyboard.press('End')
|
||||
await page.keyboard.insertText(' 계속')
|
||||
await page.waitForTimeout(700)
|
||||
|
||||
expectNoBlockContainerCrash(crashSignals)
|
||||
|
||||
await toggleRawMode(page, '.cm-content')
|
||||
const raw = await getRawEditorContent(page)
|
||||
expect(raw).toContain('> 인용문 추가')
|
||||
expect(raw).toContain('- 첫 번째 항목 계속')
|
||||
expectNoBlockContainerCrash(crashSignals)
|
||||
})
|
||||
|
||||
for (const scenario of listReopenScenarios) {
|
||||
test(scenario.name, async ({ page }) => {
|
||||
await expectListContentEditableAfterReopen(page, scenario)
|
||||
})
|
||||
}
|
||||
|
||||
test('Procedure prose and adjacent bullet-list content stay editable through keydown edits', async ({ page }) => {
|
||||
const crashSignals = collectEditorCrashSignals(page)
|
||||
const markerId = Date.now()
|
||||
const paragraphMarker = `procedure-paragraph-${markerId}`
|
||||
const ownerMarker = `procedure-owner-${markerId}`
|
||||
const cadenceMarker = `procedure-cadence-${markerId}`
|
||||
|
||||
await openNote(page, 'Note B')
|
||||
await toggleRawMode(page, '.cm-content')
|
||||
await setRawEditorContent(page, PROCEDURE_NOTE_CONTENT)
|
||||
await page.waitForTimeout(700)
|
||||
await toggleRawMode(page, '.bn-editor')
|
||||
|
||||
const procedureParagraph = page.locator('.bn-block-content', {
|
||||
hasText: 'Procedures are long-running processes',
|
||||
}).first()
|
||||
await expect(procedureParagraph).toBeVisible({ timeout: 5_000 })
|
||||
await procedureParagraph.click()
|
||||
await page.keyboard.press('End')
|
||||
await page.keyboard.insertText(` ${paragraphMarker}`)
|
||||
await page.waitForTimeout(700)
|
||||
|
||||
const ownerItem = page.locator('.bn-block-content[data-content-type="bulletListItem"]', {
|
||||
hasText: 'Owner: The person responsible',
|
||||
}).first()
|
||||
await expect(ownerItem).toBeVisible({ timeout: 5_000 })
|
||||
await ownerItem.click()
|
||||
await page.keyboard.press('End')
|
||||
await page.keyboard.insertText(` ${ownerMarker}`)
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.insertText(`Cadence: Weekly ${cadenceMarker}`)
|
||||
await page.waitForTimeout(900)
|
||||
|
||||
expectNoBlockContainerCrash(crashSignals)
|
||||
|
||||
await toggleRawMode(page, '.cm-content')
|
||||
const raw = await getRawEditorContent(page)
|
||||
expect(raw).toContain(paragraphMarker)
|
||||
expect(raw).toContain(`- Owner: The person responsible ${ownerMarker}`)
|
||||
expect(raw).toContain(`- Cadence: Weekly ${cadenceMarker}`)
|
||||
expectNoBlockContainerCrash(crashSignals)
|
||||
})
|
||||
Loading…
Reference in a new issue