diff --git a/mobile/src/components/custom-key-modal-save-refusal.test.tsx b/mobile/src/components/custom-key-modal-save-refusal.test.tsx index a147da0e081..20a0ebe0d88 100644 --- a/mobile/src/components/custom-key-modal-save-refusal.test.tsx +++ b/mobile/src/components/custom-key-modal-save-refusal.test.tsx @@ -129,11 +129,10 @@ beforeEach(() => { // Page allowlisted custom keys reject over-size/unseeded writes (rulings 33.4/33.6). Modal must catch void-call rejection or page faults and drops generation. describe('adding a custom key when the store refuses the write', () => { - it('does not let the refusal escape as an unhandled rejection', async () => { + it('keeps a refused addition out of callbacks and mirrored storage', async () => { store.refuse = true const before = mirrored() - const unhandled = vi.fn() - process.on('unhandledRejection', unhandled) + const onKeysChanged = vi.fn() const onClose = vi.fn() let renderer: ReturnType | null = null @@ -149,12 +148,10 @@ describe('adding a custom key when the store refuses the write', () => { await Promise.resolve() await Promise.resolve() }) - process.off('unhandledRejection', unhandled) - expect(unhandled).not.toHaveBeenCalled() + // Do not report unstored key as added to accessory row. expect(onKeysChanged).not.toHaveBeenCalled() expect(onClose).not.toHaveBeenCalled() - // Restore mirror after refused save: saveCustomKeys notes map before persistence, and next init must not include rejected value. expect(mirrored()).toBe(before) }) }) diff --git a/mobile/src/components/drag-reorder-positions.test.ts b/mobile/src/components/drag-reorder-positions.test.ts index 8c8d0769b5e..2105a96db02 100644 --- a/mobile/src/components/drag-reorder-positions.test.ts +++ b/mobile/src/components/drag-reorder-positions.test.ts @@ -1,41 +1,16 @@ -import { describe, expect, it } from 'vitest' - +import { expect, it } from 'vitest' import { dragReorderLayout, dragReorderTarget, dragReorderPositionsFromKeys, - moveDragReorderKey, - orderedKeysFromDragReorderPositions + moveDragReorderKey } from './drag-reorder-positions' -describe('drag reorder positions', () => { - it('shifts intermediate rows up when dragging a row earlier', () => { - const positions = dragReorderPositionsFromKeys(['a', 'b', 'c', 'd']) - expect(orderedKeysFromDragReorderPositions(moveDragReorderKey(positions, 'd', 1))).toEqual([ - 'a', - 'd', - 'b', - 'c' - ]) - }) -}) - -it('projects a thousand variable-height rows and all row lookups with linear reads', () => { - const keys = Array.from({ length: 1000 }, (_, index) => `row-${index}`) - let reads = 0 - const positions = new Proxy(dragReorderPositionsFromKeys(keys), { - get(target, key) { - if (typeof key === 'string') { - reads++ - return target[key] - } - return undefined - } - }) - const sizes = Object.fromEntries(keys.map((key, index) => [key, index % 2 ? 20 : 100])) +it('lays out variable-height rows when targeting, reordering and resizing', () => { + const positions = dragReorderPositionsFromKeys(['row-0', 'row-1', 'row-2', 'row-3']) + const sizes = { 'row-0': 100, 'row-1': 20, 'row-2': 100, 'row-3': 20 } const layout = dragReorderLayout(positions, sizes) - expect(keys.map((key) => layout.tops[key]).at(-1)).toBe(59980) - expect(reads).toBe(1000) + expect(layout.tops['row-3']).toBe(220) expect(dragReorderTarget(positions, sizes, layout, 'row-0', 35)).toBe(1) const moved = moveDragReorderKey(positions, 'row-0', 2) expect(dragReorderLayout(moved, sizes).keys.slice(0, 3)).toEqual(['row-1', 'row-2', 'row-0']) diff --git a/mobile/src/components/html-preview-inert-links.test.ts b/mobile/src/components/html-preview-inert-links.test.ts index b0f636f0b2e..bff57ed1988 100644 --- a/mobile/src/components/html-preview-inert-links.test.ts +++ b/mobile/src/components/html-preview-inert-links.test.ts @@ -30,29 +30,10 @@ describe('an artifact rendered for a shell that cannot open a link', () => { expect(inert(ARTIFACT).querySelectorAll('a[href], area[href]')).toHaveLength(0) }) - it('keeps the text, the headings and the images the author wrote', () => { - const doc = inert(ARTIFACT) - expect(doc.getElementById('marker')?.textContent).toBe('text') - expect(doc.getElementById('top')?.textContent).toBe('tap') - expect(doc.getElementById('blank')?.textContent).toBe('window') - expect(doc.getElementById('mapped')?.getAttribute('src')).toBe('x.png') - expect(doc.title).toBe('A') - // The elements are still there and still in order: this is a hidden affordance, not a deletion. - expect([...doc.querySelectorAll('a')].map((one) => one.id)).toEqual([ - 'top', - 'blank', - 'root', - 'empty', - 'frag', - 'named', - 'svglink' - ]) - }) - // Preserve authored doctype bytes/identifiers; srcdoc inherits embedder’s mode regardless, so test document.doctype rather than CSS1Compat. it('keeps the doctype whole, identifiers and all', () => { const doctypeOf = (html: string) => htmlPreviewWithInertLinks(html).split('>')[0] ?? '' - expect(doctypeOf(ARTIFACT).toLowerCase()).toBe('x') ).toBe(' { ).toContain( '' ) - // Leave missing doctype absent rather than inventing author markup. - expect(htmlPreviewWithInertLinks('x').toLowerCase()).not.toContain( - ' { expect(out).toContain('
no newline
') }) - it('leaves an href inside a comment where a regex pass would have found it', () => { - // Use parser rather than regex so href inside comment remains untouched artifact text. - const doc = inert( - '' + - 'r' - ) - expect(doc.body.innerHTML).toContain('href="https://example.com/x"') - // The control: a link that is rendered did lose its href. - expect(doc.getElementById('real')?.hasAttribute('href')).toBe(false) - }) - // Declarative shadowroot templates become rendered content at frame parse, but DOMParser leaves templates intact on Chromium 147/WebKit 26.4/happy-dom. // Traverse template.content recursively: document querySelectorAll misses links that would otherwise become dead anchors under sandbox. it('reaches a link inside a declarative shadow root, and inside one nested in it', () => { diff --git a/mobile/src/components/html-preview-link-affordance.test.tsx b/mobile/src/components/html-preview-link-affordance.test.tsx index 751854df369..271727165bb 100644 --- a/mobile/src/components/html-preview-link-affordance.test.tsx +++ b/mobile/src/components/html-preview-link-affordance.test.tsx @@ -29,8 +29,7 @@ vi.mock('lucide-react-native', () => ({ Eye: () => null })) -const { MobileHtmlPreview, MOBILE_HTML_PREVIEW_SANDBOX, MOBILE_HTML_PREVIEW_SEALED_SANDBOX } = - await import('./MobileHtmlPreview') +const { MobileHtmlPreview } = await import('./MobileHtmlPreview') const ARTIFACT = 'tap' @@ -76,19 +75,10 @@ describe('the frame the preview mounts for each answer the shell gives', () => { it('hands over an artifact with no link, in a frame that cannot navigate, when it cannot', () => { const frame = frameOf(false) // Both fences follow the one answer, which is the wiring this file exists for. - expect(frame?.sandbox).toBe(MOBILE_HTML_PREVIEW_SEALED_SANDBOX) + expect(frame?.sandbox).toBe('') expect(frame?.srcDoc).not.toContain('href') // The author's text is still there, so this is a hidden affordance and not a deletion. expect(frame?.srcDoc).toContain('tap') expect(frame?.srcDoc).toContain('id="x"') }) - - it('seals the frame with a sandbox that grants nothing, rather than dropping the attribute', () => { - // Require empty sandbox attribute: absent/undefined grants every capability, while empty grants none. - expect(MOBILE_HTML_PREVIEW_SEALED_SANDBOX).toBe('') - expect(frameOf(false)?.sandbox).toBe('') - expect(MOBILE_HTML_PREVIEW_SANDBOX.split(' ')).toContain( - 'allow-top-navigation-by-user-activation' - ) - }) }) diff --git a/mobile/src/components/markdown-file-path-detection.test.ts b/mobile/src/components/markdown-file-path-detection.test.ts index 9ecd9824b4c..fa42d518b48 100644 --- a/mobile/src/components/markdown-file-path-detection.test.ts +++ b/mobile/src/components/markdown-file-path-detection.test.ts @@ -22,20 +22,6 @@ describe('detectFilePathSegments', () => { ]) }) - it('detects POSIX absolute paths', () => { - expect(detectFilePathSegments('Wrote /Users/me/wt/src/app.tsx today')).toEqual([ - { type: 'text', value: 'Wrote ' }, - { type: 'file', value: '/Users/me/wt/src/app.tsx', path: '/Users/me/wt/src/app.tsx' }, - { type: 'text', value: ' today' } - ]) - expect(detectFilePathSegments('/repo/src/index.ts')).toEqual([ - { type: 'file', value: '/repo/src/index.ts', path: '/repo/src/index.ts' } - ]) - expect(detectFilePathSegments('/root.ts')).toEqual([ - { type: 'file', value: '/root.ts', path: '/root.ts' } - ]) - }) - it('detects paths with :line and :line:col suffixes', () => { expect(detectFilePathSegments('see src/foo.ts:42 here')).toEqual([ { type: 'text', value: 'see ' }, @@ -69,15 +55,6 @@ describe('detectFilePathSegments', () => { expect(detectFilePathSegments('https://example.com/path/file.ts')).toEqual([ { type: 'text', value: 'https://example.com/path/file.ts' } ]) - expect(detectFilePathSegments('see https://example.com/path/file.ts:42 now')).toEqual([ - { type: 'text', value: 'see https://example.com/path/file.ts:42 now' } - ]) - }) - - it('does not match protocol-relative URLs', () => { - expect(detectFilePathSegments('load //cdn.example.com/lib/app.js')).toEqual([ - { type: 'text', value: 'load //cdn.example.com/lib/app.js' } - ]) }) it('detects scoped-package file paths with a segment-leading @', () => { @@ -90,15 +67,6 @@ describe('detectFilePathSegments', () => { }, { type: 'text', value: ' here' } ]) - expect( - detectFilePathSegments('node_modules/@scope/pkg/file.ts').filter((s) => s.type === 'file') - ).toEqual([ - { - type: 'file', - value: 'node_modules/@scope/pkg/file.ts', - path: 'node_modules/@scope/pkg/file.ts' - } - ]) }) it('does not match emails or git URLs with a mid-token @', () => { @@ -118,11 +86,7 @@ describe('detectFilePathSegments', () => { }) describe('isFilePathCodeSpan', () => { - it('accepts POSIX absolute paths and :line citations', () => { - expect(isFilePathCodeSpan('/Users/me/wt/src/app.tsx')).toBe(true) - expect(isFilePathCodeSpan('src/foo.ts:42')).toBe(true) - expect(isFilePathCodeSpan('src/foo.ts:42:7')).toBe(true) + it('accepts a bare file name with a line citation', () => { expect(isFilePathCodeSpan('MobileNativeChatComposer.tsx:23')).toBe(true) - expect(isFilePathCodeSpan(String.raw`C:\repo\Main.tsx:12`)).toBe(true) }) }) diff --git a/mobile/src/components/markdown-href-routing.test.ts b/mobile/src/components/markdown-href-routing.test.ts deleted file mode 100644 index cc80494bfd9..00000000000 --- a/mobile/src/components/markdown-href-routing.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { routeMarkdownHref } from './markdown-href-routing' - -describe('routeMarkdownHref', () => { - it('carries a #L fragment as a :line suffix', () => { - expect(routeMarkdownHref('docs/plan.md#L42')).toEqual({ - kind: 'file', - pathText: 'docs/plan.md:42' - }) - expect(routeMarkdownHref('docs/plan.md?plain=1#line-7')).toEqual({ - kind: 'file', - pathText: 'docs/plan.md:7' - }) - expect(routeMarkdownHref('docs/plan.md#usage')).toEqual({ - kind: 'file', - pathText: 'docs/plan.md' - }) - }) - - it('routes file: URIs to the file opener', () => { - expect(routeMarkdownHref('file:///Users/me/wt/src/app.tsx')).toEqual({ - kind: 'file', - pathText: '/Users/me/wt/src/app.tsx' - }) - expect(routeMarkdownHref('file:///Users/me/wt/src/app.tsx#L12')).toEqual({ - kind: 'file', - pathText: '/Users/me/wt/src/app.tsx:12' - }) - expect(routeMarkdownHref('file:///C:/repo/src/index.ts')).toEqual({ - kind: 'file', - pathText: 'C:/repo/src/index.ts' - }) - }) -}) diff --git a/mobile/src/components/mobile-markdown-growing-tree.test.ts b/mobile/src/components/mobile-markdown-growing-tree.test.ts index 24833e3d5ec..25e77d46075 100644 --- a/mobile/src/components/mobile-markdown-growing-tree.test.ts +++ b/mobile/src/components/mobile-markdown-growing-tree.test.ts @@ -40,25 +40,6 @@ describe('text growing a character at a time', () => { ]) }) - it('keeps a fenced block whole across its blank lines', () => { - expect(grown('```\na\n\nb\n```\n\nafter')).toEqual([ - { - type: 'code', - lang: null, - value: 'a\n\nb', - closed: true, - position: { start: { line: 1, column: 1 }, end: { line: 5, column: 1 } } - }, - paragraph('after') - ]) - }) - - it('makes one math block once a display formula closes past a blank line', () => { - expect(grown('$$\nx\n\ny\n$$')).toEqual([ - { type: 'math', value: 'x\n\ny', source: '$$\nx\n\ny\n$$' } - ]) - }) - it('ends a quote at a display formula that closes past a blank line', () => { expect(grown('> a\n$$\n\nb\n$$')).toEqual([ { type: 'blockquote', children: [paragraph('a')] }, diff --git a/mobile/src/components/mobile-markdown-math.test.ts b/mobile/src/components/mobile-markdown-math.test.ts index 63dd18c5e67..f8e4ed37d2d 100644 --- a/mobile/src/components/mobile-markdown-math.test.ts +++ b/mobile/src/components/mobile-markdown-math.test.ts @@ -31,21 +31,16 @@ describe('math in mobile Markdown follows desktop chat', () => { ['I:\\color{#1a7f37}{\\Huge{\\mathbf{+}}}'], '\u200b37' ], - ['prices', '$10 and $20', [], '$10 and $20'], - ['shell variables', 'echo $HOME and $PATH', [], 'echo $HOME and $PATH'], [ 'a price with operators', 'Keep $5 + 2$ typed, render $5+2$', ['I:5+2'], 'Keep $5 + 2$ typed, render ' ], - ['a price then a formula', '$5 $x$', ['I:x'], '$5 '], ['a block that interrupts a paragraph', 'para\n$$\nx=1\n$$\nafter', ['D:x=1'], 'paraafter'], ['display formulas alone in a paragraph', '$$a$$ $$b$$', ['D:a', 'D:b'], ''], - ['a display opener among prose', '$$x$$ tail', ['I:x'], ' tail'], ['inline and bracket delimiters', '\\(a\\) and \\[b\\]', ['I:a', 'I:b'], ' and '], ['a citation', '\\[1, 2\\]', [], '[1, 2]'], - ['an unclosed block', '$$\nunclosed', [], '$$\nunclosed'], ['a formula in a link label', '[$x$](http://a)', [], '$x$'], ['a closed math fence', '```math\nx^2\n```', ['D:x^2'], ''] ])('%s', (_name, source, math, text) => { diff --git a/mobile/src/components/mobile-webview-editor-web-fallbacks.test.tsx b/mobile/src/components/mobile-webview-editor-web-fallbacks.test.tsx index bd233480e01..18ca2c7d9fc 100644 --- a/mobile/src/components/mobile-webview-editor-web-fallbacks.test.tsx +++ b/mobile/src/components/mobile-webview-editor-web-fallbacks.test.tsx @@ -106,36 +106,4 @@ describe('the rich markdown editor on the page', () => { }) expect(onChange.mock.calls).toEqual([['edited']]) }) - - it('keeps the document and toolbar read-only until editing is enabled', () => { - const host = document.createElement('div') - const onChange = vi.fn() - const editor = (editable: boolean) => - createElement(MobileRichMarkdownEditor, { - content: 'body', - editable, - onChange - }) - const renderer = render(editor(false), host) - const surface = host.querySelector('#editor')! - const buttons = findHosts(renderer, 'Pressable') - expect(buttons.length).toBeGreaterThan(0) - expect(buttons.filter((node) => node.props.disabled !== true)).toEqual([]) - expect(surface.getAttribute('contenteditable')).toBe('false') - act(() => { - surface.dispatchEvent(new Event('input', { bubbles: true })) - }) - expect(onChange).not.toHaveBeenCalled() - - act(() => renderer.update(editor(true))) - const enabled = findHosts(renderer, 'Pressable') - expect(enabled).toHaveLength(buttons.length) - expect(enabled.filter((node) => node.props.disabled === true)).toEqual([]) - expect(surface.getAttribute('contenteditable')).toBe('true') - act(() => { - surface.innerHTML = '

accepted

' - surface.dispatchEvent(new Event('input', { bubbles: true })) - }) - expect(onChange.mock.calls).toEqual([['accepted']]) - }) }) diff --git a/mobile/src/components/pr-sidebar/pr-comment-severity.test.ts b/mobile/src/components/pr-sidebar/pr-comment-severity.test.ts index b9f88792998..c2fb0272c5b 100644 --- a/mobile/src/components/pr-sidebar/pr-comment-severity.test.ts +++ b/mobile/src/components/pr-sidebar/pr-comment-severity.test.ts @@ -4,7 +4,6 @@ import { prCommentSeverity } from './pr-comment-severity' describe('prCommentSeverity', () => { it.each([ ['_⚠️ Potential issue_ | _🟠 Major_\n\n**Guard the null case.**', 'major'], - ['_⚠️ Potential issue_ | _🔴 Critical_', 'critical'], ['_🧹 Nitpick_ | _🔵 Trivial_', 'trivial'], ['**Minor**: rename this.', 'minor'], ['_Major_', 'major'] @@ -14,11 +13,8 @@ describe('prCommentSeverity', () => { it.each([ 'This is a minor race in the poller.', - 'Critical path: the merge button.', '_Majority_', - 'Bumps `semver_major_bump` in ci', - 'Set `ALLOW_MINOR_UPDATES` to true', - '' + 'Set `ALLOW_MINOR_UPDATES` to true' ])('ignores the word outside a severity marker in %j', (body) => { expect(prCommentSeverity(body)).toBeNull() }) diff --git a/mobile/src/components/rich-markdown/editor-content.test.ts b/mobile/src/components/rich-markdown/editor-content.test.ts index ccd608d4694..4a47d29caac 100644 --- a/mobile/src/components/rich-markdown/editor-content.test.ts +++ b/mobile/src/components/rich-markdown/editor-content.test.ts @@ -47,7 +47,6 @@ describe('the editor document content', () => { `data:image/png;base64,${btoa('images/diagram.png')}` ) ) - expect(loadImageSource).toHaveBeenCalledExactlyOnceWith('images/diagram.png') expect(editor.editor.querySelectorAll('img')[1]?.getAttribute('src')).toBe( 'https://example.com/remote.png' ) @@ -76,16 +75,6 @@ describe('the editor document content', () => { ]) }) - it('writes a ticked checkbox back into the markup the next serialization reads', () => { - const editor = editorDocument() - editor.handle.setMarkdown('- [ ] Open', 1) - const checkbox = editor.editor.querySelector('input')! - checkbox.checked = true - checkbox.dispatchEvent(new Event('change', { bubbles: true })) - expect(editor.editor.querySelector('li')!.getAttribute('data-checked')).toBe('true') - expect(editor.posted).toEqual([{ type: 'change', markdown: '- [x] Open', generation: 1 }]) - }) - it('reports one change for a checkbox tap, not one per event the tap raises', () => { // Assert click/input/change all bubble for one tick, then prove only one report; missing events cannot satisfy count accidentally. const editor = editorDocument() @@ -99,20 +88,4 @@ describe('the editor document content', () => { expect(editor.editor.querySelector('li')!.getAttribute('data-checked')).toBe('true') expect(editor.posted).toEqual([{ type: 'change', markdown: '- [x] Open', generation: 1 }]) }) - - it('reports one change for inline code over a selection, not one per emitting step', async () => { - // The wrap helper emitted and `runCommand` emits after every command: two identical changes. - const editor = editorDocument() - editor.handle.setMarkdown('one two', 1) - const text = editor.editor.querySelector('p')!.firstChild! - const range = document.createRange() - range.setStart(text, 0) - range.setEnd(text, 3) - const selection = window.getSelection()! - selection.removeAllRanges() - selection.addRange(range) - await editor.handle.runCommand('inlineCode') - expect(editor.editor.innerHTML).toBe('

one two

') - expect(editor.posted).toEqual([{ type: 'change', markdown: '`one` two', generation: 1 }]) - }) }) diff --git a/mobile/src/components/rich-markdown/editor-inline-code-blocks.test.ts b/mobile/src/components/rich-markdown/editor-inline-code-blocks.test.ts index 7b7cd4a3e9b..462cc2b8ddf 100644 --- a/mobile/src/components/rich-markdown/editor-inline-code-blocks.test.ts +++ b/mobile/src/components/rich-markdown/editor-inline-code-blocks.test.ts @@ -33,9 +33,7 @@ function select(range: Range) { } it.each([ - ['first\n\nsecond\n\nthird', '`first`\n\n`second`\n\n`third`', ['first', 'second', 'third']], ['# Heading\n\n> Quoted', '# `Heading`\n\n> `Quoted`', ['Heading', 'Quoted']], - ['- first\n- second', '- `first`\n- `second`', ['first', 'second']], ['- [ ] first\n- [x] second', '- [ ] `first`\n- [x] `second`', ['first', 'second']], [ 'left ![pixel](https://example.com/pixel.png) right', diff --git a/mobile/src/components/rich-markdown/editor-selection.test.ts b/mobile/src/components/rich-markdown/editor-selection.test.ts index 4d3b442eeeb..400e1188d60 100644 --- a/mobile/src/components/rich-markdown/editor-selection.test.ts +++ b/mobile/src/components/rich-markdown/editor-selection.test.ts @@ -113,7 +113,6 @@ describe('the editor document caret, across a keyboard dismissal', () => { it('restores the pre-dismissal caret so a command does not insert at the end', async () => { const editor = runtime({ caret: 'paragraph-3' }) editor.handle.dismissKeyboard() - expect(editor.selectedContainer()).toBe(null) await editor.handle.runCommand('bold') expect(editor.selectedContainer()).toBe('paragraph-3') expect(editor.focused()).toBe(true) diff --git a/mobile/src/components/rich-markdown/markdown-code-block-dom.test.ts b/mobile/src/components/rich-markdown/markdown-code-block-dom.test.ts index dfe13edc950..360ffe58999 100644 --- a/mobile/src/components/rich-markdown/markdown-code-block-dom.test.ts +++ b/mobile/src/components/rich-markdown/markdown-code-block-dom.test.ts @@ -13,7 +13,6 @@ afterEach(() => { }) it.each([ - ['first
second', 'first\nsecond'], ['first

second', 'first\n\nsecond'], ['prefix middle suffix', 'prefix middle suffix'], ['last line

', 'last line'] diff --git a/mobile/src/components/rich-markdown/markdown-code-delimiters.test.ts b/mobile/src/components/rich-markdown/markdown-code-delimiters.test.ts index f272b6656b0..a80ea2a887b 100644 --- a/mobile/src/components/rich-markdown/markdown-code-delimiters.test.ts +++ b/mobile/src/components/rich-markdown/markdown-code-delimiters.test.ts @@ -26,11 +26,8 @@ function mount() { } it.each([ - ['a`b', '``a`b``'], ['a``b', '```a``b```'], ['`edge', '`` `edge ``'], - ['edge`', '`` edge` ``'], - ['`', '`` ` ``'], [' surrounded ', '` surrounded `'], [' ', '` `'] ])('preserves selected inline code %j through the host and a reload', async (literal, saved) => { diff --git a/mobile/src/components/rich-markdown/markdown-code-fence-allocation.test.ts b/mobile/src/components/rich-markdown/markdown-code-fence-allocation.test.ts deleted file mode 100644 index 43488553521..00000000000 --- a/mobile/src/components/rich-markdown/markdown-code-fence-allocation.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -// @vitest-environment happy-dom -import { afterEach, describe, expect, it, vi } from 'vitest' -import { quietRichMarkdownEditorHost } from './rich-markdown-editor-host.test-support' -import { MOBILE_MARKDOWN_EDIT_MAX_BYTES } from '../../../../src/shared/mobile-markdown-document' -import { createRichMarkdownEditorDocument } from './create-rich-markdown-editor-document' -import { RICH_MARKDOWN_EDITOR_MARKUP } from './document-markup' - -import type { MobileRichMarkdownEditorMessage } from '../mobile-rich-markdown-editor-contract' - -afterEach(() => { - vi.restoreAllMocks() - document.body.innerHTML = '' -}) - -describe('code-block fence sizing', () => { - it('serializes an admitted editable document without collecting every backtick run', () => { - document.body.innerHTML = RICH_MARKDOWN_EDITOR_MARKUP - const posted: MobileRichMarkdownEditorMessage[] = [] - const editorDocument = createRichMarkdownEditorDocument( - quietRichMarkdownEditorHost({ - postToHost: (message) => posted.push(message), - keyboardInsetSource: () => null - }) - ) - try { - const code = '`a'.repeat((MOBILE_MARKDOWN_EDIT_MAX_BYTES - 40) / 2) - const markdown = `\`\`\`ts\n${code}\n\`\`\`` - expect(markdown.length).toBeLessThan(MOBILE_MARKDOWN_EDIT_MAX_BYTES) - editorDocument.send.setMarkdown(markdown, 7) - editorDocument.send.setEditable(true) - const editor = document.getElementById('editor') - const codeElement = editor?.querySelector('code') - if (!editor || !codeElement) { - throw new Error('The loaded Markdown document has no code block') - } - codeElement.textContent = code + 'a' - posted.length = 0 - - const match = String.prototype.match - let largestMatchArray = 0 - vi.spyOn(String.prototype, 'match').mockImplementation(function (this: string, pattern) { - const matches = match.call(this, pattern) - if (matches) { - largestMatchArray = Math.max(largestMatchArray, matches.length) - } - return matches - }) - editor.dispatchEvent(new Event('input')) - - const expected = `\`\`\`ts\n${code}a\n\`\`\`` - expect(expected.length).toBeLessThan(MOBILE_MARKDOWN_EDIT_MAX_BYTES) - expect(posted).toEqual([{ type: 'change', markdown: expected, generation: 7 }]) - expect(largestMatchArray).toBeLessThan(1024) - } finally { - editorDocument.stop() - } - }) -}) diff --git a/mobile/src/components/rich-markdown/markdown-code-literals.test.ts b/mobile/src/components/rich-markdown/markdown-code-literals.test.ts index 19687646113..f972fb42b39 100644 --- a/mobile/src/components/rich-markdown/markdown-code-literals.test.ts +++ b/mobile/src/components/rich-markdown/markdown-code-literals.test.ts @@ -31,7 +31,6 @@ it.each([ editorDocument.send.setMarkdown(original, 7) const surface = document.getElementById('editor')! expect(surface.querySelector('code')?.textContent).toBe(codeText) - expect(editorDocument.send.currentMarkdown()).toBe(original) surface.lastElementChild!.append('!') surface.dispatchEvent( @@ -60,7 +59,6 @@ it('preserves text after formatting the gap between inline code spans', async () await editorDocument.send.runCommand('inlineCode') - expect(editorDocument.send.currentMarkdown()).toBe('`old middle tail`') expect(messages.filter((message) => message.type === 'change')).toEqual([ { type: 'change', generation: 7, markdown: '`old middle tail`' } ]) @@ -68,24 +66,23 @@ it('preserves text after formatting the gap between inline code spans', async () expect(document.querySelector('#editor code')?.textContent).toBe('old middle tail') }) -it.each([ - ['a```b', '````a```b````', 'a```b'], - [' leftright ', '` leftright `', ' leftright '] -])('preserves adjacent literal code through save and reopen: %s', (html, markdown, text) => { - document.body.innerHTML = RICH_MARKDOWN_EDITOR_MARKUP - const editorDocument = createRichMarkdownEditorDocument(quietRichMarkdownEditorHost()) - stopDocuments.push(editorDocument.stop) - const surface = document.getElementById('editor')! - surface.innerHTML = `

${html}

` +it.each([['a```b', '````a```b````', 'a```b']])( + 'preserves adjacent literal code through save and reopen: %s', + (html, markdown, text) => { + document.body.innerHTML = RICH_MARKDOWN_EDITOR_MARKUP + const editorDocument = createRichMarkdownEditorDocument(quietRichMarkdownEditorHost()) + stopDocuments.push(editorDocument.stop) + const surface = document.getElementById('editor')! + surface.innerHTML = `

${html}

` - expect(editorDocument.send.currentMarkdown()).toBe(markdown) - editorDocument.send.setMarkdown(markdown, 8) - expect(surface.querySelector('code')?.textContent).toBe(text) - expect(editorDocument.send.currentMarkdown()).toBe(markdown) -}) + expect(editorDocument.send.currentMarkdown()).toBe(markdown) + editorDocument.send.setMarkdown(markdown, 8) + expect(surface.querySelector('code')?.textContent).toBe(text) + expect(editorDocument.send.currentMarkdown()).toBe(markdown) + } +) it.each([ - ['inline code', '

x\u00a0 z\u00a0 after

', '`x z ` after'], ['an inline code line break', '

first
second

', '`first second`'], ['a code block', '
\u00a0 \u00a0if x:
', '```\n if x:\n```'] ])('saves browser-created spacing in %s as ordinary spaces', (_name, html, markdown) => { diff --git a/mobile/src/components/rich-markdown/markdown-empty-tasks.test.ts b/mobile/src/components/rich-markdown/markdown-empty-tasks.test.ts index 3dad7a19afb..20c0f5a2ea6 100644 --- a/mobile/src/components/rich-markdown/markdown-empty-tasks.test.ts +++ b/mobile/src/components/rich-markdown/markdown-empty-tasks.test.ts @@ -28,8 +28,6 @@ function mount(markdown: string) { } it.each([ - ['- [ ]', '- [ ]', [false]], - ['- [x]', '- [x]', [true]], ['- [ ] ', '- [ ]', [false]], ['- [ ] Parent\n - [x]', '- [ ] Parent\n - [x]', [false, true]] ])( diff --git a/mobile/src/components/rich-markdown/markdown-list-block-boundaries.test.ts b/mobile/src/components/rich-markdown/markdown-list-block-boundaries.test.ts index 9f596557563..b2ff2d58cbe 100644 --- a/mobile/src/components/rich-markdown/markdown-list-block-boundaries.test.ts +++ b/mobile/src/components/rich-markdown/markdown-list-block-boundaries.test.ts @@ -6,10 +6,7 @@ import { quietRichMarkdownEditorHost } from './rich-markdown-editor-host.test-su import type { MobileRichMarkdownEditorMessage } from '../mobile-rich-markdown-editor-contract' const stopDocuments: (() => void)[] = [] -const lists = [ - ['bullet', '- First\n- Next\n\nOutside', 'li'], - ['task', '- [ ] First\n- [x] Next\n\nOutside', 'li > div'] -] as const +const lists = [['task', '- [ ] First\n- [x] Next\n\nOutside', 'li > div']] as const function surface(source: string) { document.body.innerHTML = RICH_MARKDOWN_EDITOR_MARKUP @@ -71,10 +68,7 @@ for (const [kind, source, selector] of lists) { it.each([ ['

First

Second

', 'First
Second'], ['

First


Second

', 'First

Second'], - ['

First

\n

Second

', 'First
Second'], - ['

First\n

Second

', 'First
Second'], ['First
Second
Third', 'First
Second
Third'], - ['First
Second
', 'First
Second'], ['First Second', '**First** *Second*'] ])('keeps existing breaks, empty lines and inline spaces without duplication: %j', (html, text) => { const { editorDocument, editor } = surface('- First\n- Next') diff --git a/mobile/src/components/rich-markdown/markdown-list-linebreaks.test.ts b/mobile/src/components/rich-markdown/markdown-list-linebreaks.test.ts index ebcc69ac183..de228bdf4ed 100644 --- a/mobile/src/components/rich-markdown/markdown-list-linebreaks.test.ts +++ b/mobile/src/components/rich-markdown/markdown-list-linebreaks.test.ts @@ -30,7 +30,6 @@ afterEach(() => { for (const [kind, source, selector] of lists) { it.each([ - ['First
Second', 'First
Second'], ['First
Second
', '**First
Second**'], ['First
Second', '`First`
`Second`'] ])(`keeps ${kind} item soft break inside its list through host save: %s`, (html, text) => { @@ -107,7 +106,6 @@ it.each([ it.each([ ['First
', 'First'], - ['

First

', 'First'], ['

First

', '**First**'], ['First
', 'First\n - Child'], ['

First

', 'First

'], diff --git a/mobile/src/components/rich-markdown/markdown-quote-blocks.test.ts b/mobile/src/components/rich-markdown/markdown-quote-blocks.test.ts index 9241627292f..e4c1bb99bce 100644 --- a/mobile/src/components/rich-markdown/markdown-quote-blocks.test.ts +++ b/mobile/src/components/rich-markdown/markdown-quote-blocks.test.ts @@ -103,21 +103,3 @@ it('keeps quoted code literal rather than interpreting its block markers', () => expect(editor.querySelectorAll('blockquote').length).toBe(1) expect(editorDocument.send.currentMarkdown()).toBe(markdown) }) - -it('keeps task state and read-only controls inside quotes', () => { - const { editorDocument, editor } = surface() - const markdown = '> - [x] Done\n> - [ ] Open' - editorDocument.send.setEditable(false) - editorDocument.send.setMarkdown(markdown, 8) - - expect( - Array.from(editor.querySelectorAll('input')).map((input) => ({ - checked: input.checked, - disabled: input.disabled - })) - ).toEqual([ - { checked: true, disabled: true }, - { checked: false, disabled: true } - ]) - expect(editorDocument.send.currentMarkdown()).toBe(markdown) -}) diff --git a/mobile/src/components/rich-markdown/markdown-round-trip.test.ts b/mobile/src/components/rich-markdown/markdown-round-trip.test.ts index 9e12092a039..4afc6e9da8e 100644 --- a/mobile/src/components/rich-markdown/markdown-round-trip.test.ts +++ b/mobile/src/components/rich-markdown/markdown-round-trip.test.ts @@ -107,32 +107,6 @@ describe('the editor document, from markdown and back', () => { ) }) - it('round-trips every block the toolbar can produce', () => { - const markdown = [ - '# Title', - '', - 'Body with **bold**, *italic*, ~~strike~~ and `code`.', - '', - '> Quoted line', - '', - '| a | b |', - '| --- | --- |', - '| 1 | 2 |', - '', - '```ts', - 'const x = 1', - '```', - '', - '---', - '', - '[docs](https://example.com/docs)', - '', - '![alt](https://example.com/a.png)' - ].join('\n') - - expect(currentMarkdown(surface(markdown).scope)).toBe(markdown) - }) - it('makes progress on a marker with nothing after it, rather than reading it forever', () => { // Bare block markers pass isBlockStart but item/heading readers require text; ensure index advances as plain text instead of looping forever. const render = (markdown: string) => @@ -153,15 +127,6 @@ describe('the editor document, from markdown and back', () => { expect(render('- ok')).toBe('') }) - it('round-trips a code block that holds a fence of its own', () => { - // Choose closing fence longer than any inner backtick run or reader ends code at its own content. - const markdown = ['````', '```', 'nested', '```', '````'].join('\n') - const { scope, html } = surface(markdown) - - expect(html).toContain('
```\nnested\n```
') - expect(currentMarkdown(scope)).toBe(markdown) - }) - it('round-trips a table cell that holds a pipe, and the backslash that hid it', () => { // Escape cell backslashes before pipes so literal pipe remains distinct from row separator. const markdown = ['| a \\| b | c\\\\d |', '| --- | --- |', '| 1 \\| 2 | 3 |'].join('\n') diff --git a/mobile/src/components/rich-markdown/markdown-table-linebreaks.test.ts b/mobile/src/components/rich-markdown/markdown-table-linebreaks.test.ts index 07a6f594418..784e9778ff4 100644 --- a/mobile/src/components/rich-markdown/markdown-table-linebreaks.test.ts +++ b/mobile/src/components/rich-markdown/markdown-table-linebreaks.test.ts @@ -32,7 +32,6 @@ function cellTexts(editor: HTMLElement) { } it.each([ - ['First
Second', 'First
Second'], ['First
Second
', '**First
Second**'], ['First
Second', '`First`
`Second`'] ])('keeps a cell line break in its own table row through host save: %s', (html, cell) => { @@ -71,20 +70,17 @@ it.each([ expect(editor.querySelector('td')?.querySelector('br')).toBe(null) }) -it.each(['
', '
', 'a
b', '
'])( - 'keeps typed table markup literal: %s', - (text) => { - const { editorDocument, editor } = surface() - editor.querySelector('td')!.textContent = text - const markdown = editorDocument.send.currentMarkdown() - editorDocument.send.setMarkdown(markdown, 8) +it.each(['a
b', '
'])('keeps typed table markup literal: %s', (text) => { + const { editorDocument, editor } = surface() + editor.querySelector('td')!.textContent = text + const markdown = editorDocument.send.currentMarkdown() + editorDocument.send.setMarkdown(markdown, 8) - expect(editor.querySelector('td')?.textContent).toBe(text) - expect(editor.querySelector('td')?.children.length).toBe(0) - expect(editor.querySelectorAll('tr').length).toBe(2) - expect(editorDocument.send.currentMarkdown()).toBe(markdown) - } -) + expect(editor.querySelector('td')?.textContent).toBe(text) + expect(editor.querySelector('td')?.children.length).toBe(0) + expect(editor.querySelectorAll('tr').length).toBe(2) + expect(editorDocument.send.currentMarkdown()).toBe(markdown) +}) it('keeps a typed break literal when the browser splits it across text nodes', () => { const { editorDocument, editor } = surface() diff --git a/mobile/src/components/rich-markdown/page-stylesheet.test.ts b/mobile/src/components/rich-markdown/page-stylesheet.test.ts index ecf974867a8..fbf587dd23f 100644 --- a/mobile/src/components/rich-markdown/page-stylesheet.test.ts +++ b/mobile/src/components/rich-markdown/page-stylesheet.test.ts @@ -1,25 +1,9 @@ import { describe, expect, it } from 'vitest' import { scopeDocumentStyleToHost } from '../../style-scoping/document-style-scoping' -import { richMarkdownEditorStyle } from './document-style' -// Scope all editor selectors to mount host: native :root/*/html/body rules otherwise restyle whole RN Web app after editor unmount. -// Move globals onto host, including theme variables/color/font/box model; unlike terminal, editor owns these page rules rather than delegating to app seam. const PREFIX = '.orca-rich-markdown-document-host' describe('the editor stylesheet on the page', () => { - it('moves the document’s own rules onto the host rather than dropping them', () => { - const scoped = scopeDocumentStyleToHost(richMarkdownEditorStyle(), PREFIX) - // Keep theme variables on host or correctly scoped selectors still render unstyled. - expect(scoped).toContain('--foreground:') - const rootRules = scoped - .split('}') - .filter((rule) => rule.includes('--foreground:') || rule.includes('min-height: 100%')) - expect(rootRules.length).toBeGreaterThan(0) - for (const rule of rootRules) { - expect(rule.trimStart().startsWith(PREFIX)).toBe(true) - } - }) - it('refuses a document-level selector it cannot move, rather than scoping it wrongly', () => { // Refuse compound host-global selector rather than silently rewriting body p into mismatching .host body p or changed .host p. expect(() => scopeDocumentStyleToHost('body p { color: red; }', PREFIX)).toThrow( diff --git a/mobile/src/components/rich-markdown/rich-markdown-host-seams.test.ts b/mobile/src/components/rich-markdown/rich-markdown-host-seams.test.ts deleted file mode 100644 index 67adb946b67..00000000000 --- a/mobile/src/components/rich-markdown/rich-markdown-host-seams.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -// @vitest-environment happy-dom -import { afterEach, describe, expect, it, vi } from 'vitest' -import { quietRichMarkdownEditorHost } from './rich-markdown-editor-host.test-support' -import { createRichMarkdownEditorDocument } from './create-rich-markdown-editor-document' -import { RICH_MARKDOWN_EDITOR_MARKUP } from './document-markup' - -import type { RichMarkdownEditorDocument } from './document-host-seams' - -// Page supplies messages, URL prompt and keyboard seams: shell owns ReactNativeWebView, window.prompt returns null, and screen already measures keyboard; test remaining live-window defaults. -const startedDocuments: RichMarkdownEditorDocument[] = [] - -afterEach(() => { - while (startedDocuments.length > 0) { - startedDocuments.pop()!.stop() - } - vi.unstubAllGlobals() -}) - -describe('the editor document host seams, once the page sets them', () => { - it('takes the caret from the selection the host names, not the window one', async () => { - // Save/restore only host selection: overlapping fields share window, and global selection belongs to whichever other field holds focus. - document.body.innerHTML = RICH_MARKDOWN_EDITOR_MARKUP - const editor = document.getElementById('editor')! - editor.innerHTML = '

text

' - const ranges: Range[] = [] - const windowSelection = window.getSelection()! - windowSelection.removeAllRanges() - // WebKit's own behaviour, and the reason the document saves a caret at all. - editor.addEventListener('blur', () => { - ranges.length = 0 - }) - - const hostSelection: Selection = Object.create(windowSelection) - Object.defineProperty(hostSelection, 'rangeCount', { get: () => ranges.length }) - hostSelection.getRangeAt = (index: number) => ranges[index]! - hostSelection.removeAllRanges = () => { - ranges.length = 0 - } - hostSelection.addRange = (range: Range) => { - ranges.push(range) - } - - const caret = document.createRange() - caret.selectNodeContents(document.getElementById('only')!) - caret.collapse(true) - ranges.push(caret) - - const started = createRichMarkdownEditorDocument( - quietRichMarkdownEditorHost({ - getSelection: () => hostSelection, - getDocument: () => hostDocument([]), - keyboardInsetSource: () => null, - postToHost: () => {} - }) - ) - startedDocuments.push(started) - - // Saved out of the host's selection, which the blur then empties. - editor.focus() - started.send.dismissKeyboard() - expect(ranges).toEqual([]) - // And restored into the host's selection rather than the window's. - await started.send.runCommand('bold') - expect( - ranges.map((range) => { - const container = range.commonAncestorContainer - return (container instanceof Element ? container : container.parentElement)?.id - }) - ).toEqual(['only']) - expect(windowSelection.rangeCount).toBe(0) - }) -}) - -// Host supplies real document with recorded execCommand because happy-dom lacks it; prove no global command lookup. -function hostDocument(commands: [string, boolean, string | undefined][]): Document { - const page: Document = Object.create(document) - page.execCommand = (command: string, showUI?: boolean, value?: string) => { - commands.push([command, showUI === true, value]) - return true - } - return page -}