diff --git a/src/renderer/src/components/editor/details-markdown-html.test.ts b/src/renderer/src/components/editor/details-markdown-html.test.ts
index c4e1493a198..47e404318b8 100644
--- a/src/renderer/src/components/editor/details-markdown-html.test.ts
+++ b/src/renderer/src/components/editor/details-markdown-html.test.ts
@@ -2,11 +2,25 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
import {
extractDetailsSummaryHtml,
isEditableDetailsHtmlBlock,
+ matchDetailsHtmlBlock,
parseDetailsAttributes,
parseToggleHeadingVariant,
type DetailsHtmlBlock
} from './details-markdown-html'
+function nestedToggles(depth: number): string {
+ let html = '\nleaf
\n\nBody\n\n '
+ for (let level = depth - 1; level > 0; level -= 1) {
+ html = `\nlevel ${level}
\n\n${html}\n\n `
+ }
+ return html
+}
+
+function isEditableHtml(html: string): boolean {
+ const block = matchDetailsHtmlBlock(html, 0)
+ return block !== null && isEditableDetailsHtmlBlock(block)
+}
+
afterEach(() => {
vi.restoreAllMocks()
})
@@ -34,8 +48,7 @@ describe('details markdown html', () => {
const block: DetailsHtmlBlock = {
raw: '',
openingAttributes: '',
- inner: `${'Heading line\n'.repeat(1_000)}
Body
`,
- hasNestedDetails: false
+ inner: `${'Heading line\n'.repeat(1_000)}Body
`
}
expect(isEditableDetailsHtmlBlock(block)).toBe(true)
@@ -61,17 +74,51 @@ describe('details markdown html', () => {
const editableHeading5: DetailsHtmlBlock = {
raw: '',
openingAttributes: ' data-orca-toggle="heading-5"',
- inner: 'ToggleBody
',
- hasNestedDetails: false
+ inner: 'ToggleBody
'
}
const unsupportedHeading6: DetailsHtmlBlock = {
raw: '',
openingAttributes: ' data-orca-toggle="heading-6"',
- inner: 'ToggleBody
',
- hasNestedDetails: false
+ inner: 'ToggleBody
'
}
expect(isEditableDetailsHtmlBlock(editableHeading5)).toBe(true)
expect(isEditableDetailsHtmlBlock(unsupportedHeading6)).toBe(false)
})
+
+ it('accepts nested toggles whose descendants are all editable', () => {
+ expect(isEditableHtml(nestedToggles(2))).toBe(true)
+ expect(isEditableHtml(nestedToggles(16))).toBe(true)
+ })
+
+ it('rejects a toggle whose nested toggle is not itself editable', () => {
+ const block: DetailsHtmlBlock = {
+ raw: '',
+ openingAttributes: ' class="orca-details"',
+ inner: 'OuterInner
Body
'
+ }
+
+ expect(isEditableDetailsHtmlBlock(block)).toBe(false)
+ })
+
+ it('rejects nesting past the recursion guard instead of recursing without bound', () => {
+ expect(isEditableHtml(nestedToggles(17))).toBe(false)
+ expect(isEditableHtml(nestedToggles(400))).toBe(false)
+ })
+
+ it('rejects unbalanced and non-toggle tags that start with details', () => {
+ const unbalanced: DetailsHtmlBlock = {
+ raw: '',
+ openingAttributes: '',
+ inner: 'OuterInner
'
+ }
+ const lookalike: DetailsHtmlBlock = {
+ raw: '',
+ openingAttributes: '',
+ inner: 'Outer
Body'
+ }
+
+ expect(isEditableDetailsHtmlBlock(unbalanced)).toBe(false)
+ expect(isEditableDetailsHtmlBlock(lookalike)).toBe(false)
+ })
})
diff --git a/src/renderer/src/components/editor/details-markdown-html.ts b/src/renderer/src/components/editor/details-markdown-html.ts
index a2f4238b658..248e2303637 100644
--- a/src/renderer/src/components/editor/details-markdown-html.ts
+++ b/src/renderer/src/components/editor/details-markdown-html.ts
@@ -34,7 +34,6 @@ export type DetailsHtmlBlock = {
raw: string
openingAttributes: string
inner: string
- hasNestedDetails: boolean
}
export type DetailsSummaryHtml = {
@@ -145,7 +144,6 @@ export function matchDetailsHtmlBlock(content: string, start: number): DetailsHt
const fenceRanges = markdownFenceRanges(content)
let depth = 0
- let hasNestedDetails = false
for (;;) {
const tagMatch = detailsTagPattern.exec(content)
@@ -167,14 +165,10 @@ export function matchDetailsHtmlBlock(content: string, start: number): DetailsHt
return {
raw: content.slice(start, closingEnd),
openingAttributes: openingMatch[0].replace(/^ $/u, ''),
- inner: content.slice(start + openingMatch[0].length, tagMatch.index),
- hasNestedDetails
+ inner: content.slice(start + openingMatch[0].length, tagMatch.index)
}
}
} else {
- if (depth > 0) {
- hasNestedDetails = true
- }
depth += 1
}
}
@@ -267,11 +261,37 @@ function isHtmlTagNamePart(code: number): boolean {
)
}
-export function isEditableDetailsHtmlBlock(block: DetailsHtmlBlock): boolean {
- if (block.hasNestedDetails) {
- return false
- }
+// Why: nested toggles validate recursively and each level rescans its own body,
+// so a pathological file would otherwise blow the stack or go quadratic.
+const MAX_DETAILS_NESTING_LEVELS = 16
+// Removes nested toggles that are themselves editable, so the caller's raw-tag
+// scan sees only the body's own markup. Null when a nested toggle can't be one.
+function stripEditableNestedDetails(bodyHtml: string, nestingLevel: number): string | null {
+ let result = ''
+ let index = 0
+
+ for (;;) {
+ const nestedStart = indexOfAsciiIgnoreCase(bodyHtml, '= MAX_DETAILS_NESTING_LEVELS) {
+ return null
+ }
+
+ const nested = matchDetailsHtmlBlock(bodyHtml, nestedStart)
+ if (!nested || !isEditableDetailsHtmlBlock(nested, nestingLevel + 1)) {
+ return null
+ }
+
+ result += bodyHtml.slice(index, nestedStart)
+ index = nestedStart + nested.raw.length
+ }
+}
+
+export function isEditableDetailsHtmlBlock(block: DetailsHtmlBlock, nestingLevel = 1): boolean {
if (!hasOnlySupportedDetailsAttributes(block.openingAttributes)) {
return false
}
@@ -289,7 +309,11 @@ export function isEditableDetailsHtmlBlock(block: DetailsHtmlBlock): boolean {
return false
}
- const bodyHtml = block.inner.slice(summary.rawLength)
+ const bodyHtml = stripEditableNestedDetails(block.inner.slice(summary.rawLength), nestingLevel)
+ if (bodyHtml === null) {
+ return false
+ }
+
if (!hasOnlyPlainParagraphAndBreakTags(bodyHtml)) {
return false
}
diff --git a/src/renderer/src/components/editor/markdown-round-trip.test.ts b/src/renderer/src/components/editor/markdown-round-trip.test.ts
index fabdc6b4d43..3f896f80e38 100644
--- a/src/renderer/src/components/editor/markdown-round-trip.test.ts
+++ b/src/renderer/src/components/editor/markdown-round-trip.test.ts
@@ -186,9 +186,74 @@ describe('rich markdown round trip', () => {
expect(roundTripMarkdown(input)).toBe(input.trimEnd())
})
- it('preserves nested details blocks as passthrough html', () => {
+ it('reopens nested toggles as editable details blocks', () => {
+ expect(
+ roundTripMarkdown(
+ 'Outer
Inner
Body
\n'
+ )
+ ).toBe(
+ [
+ '',
+ 'Outer
',
+ '',
+ '',
+ 'Inner
',
+ '',
+ 'Body',
+ '',
+ ' ',
+ '',
+ ' '
+ ].join('\n')
+ )
+ })
+
+ it('round-trips an orca-authored nested toggle unchanged', () => {
+ const input = [
+ '',
+ '08/26/2026
',
+ '',
+ '',
+ 'goals
',
+ '',
+ '- Read X post',
+ ' - Collab',
+ '',
+ ' ',
+ '',
+ '- after inner',
+ '',
+ ' ',
+ ''
+ ].join('\n')
+ expect(roundTripMarkdown(input)).toBe(input.trimEnd())
+ })
+
+ it('keeps nested toggle bodies editable rather than inert raw html', () => {
+ const markdown = markdownAfterTextReplace(
+ [
+ '',
+ 'Outer
',
+ '',
+ '',
+ 'Inner
',
+ '',
+ 'Body',
+ '',
+ ' ',
+ '',
+ ' ',
+ ''
+ ].join('\n'),
+ 'Body',
+ 'Edited body'
+ )
+ expect(markdown).toContain('Edited body')
+ })
+
+ it('preserves a nested toggle that is not itself editable as passthrough html', () => {
const input =
- 'Outer
Inner
Body
\n'
+ 'Outer
Inner
Body
\n'
expect(roundTripMarkdown(input)).toBe(input.trimEnd())
})
diff --git a/tests/e2e/helpers/markdown-editor-fixture.ts b/tests/e2e/helpers/markdown-editor-fixture.ts
new file mode 100644
index 00000000000..b9b71a3bb81
--- /dev/null
+++ b/tests/e2e/helpers/markdown-editor-fixture.ts
@@ -0,0 +1,139 @@
+import { randomUUID } from 'node:crypto'
+import { mkdir, rm, writeFile } from 'node:fs/promises'
+import path from 'node:path'
+import type { Locator, Page } from '@stablyai/playwright-test'
+import { expect } from '@stablyai/playwright-test'
+
+const MARKDOWN_HYDRATION_TIMEOUT_MS = 25_000
+
+export type ActiveWorktreeContext = {
+ worktreeId: string
+ rootPath: string
+}
+
+export type ActiveEditorFile = {
+ filePath: string
+}
+
+export async function getActiveWorktreeContext(page: Page): Promise {
+ return page.evaluate(() => {
+ const store = window.__store
+ if (!store) {
+ throw new Error('window.__store is not available')
+ }
+
+ const state = store.getState()
+ const worktreeId = state.activeWorktreeId
+ if (!worktreeId) {
+ throw new Error('No active worktree is selected')
+ }
+
+ const worktree = Object.values(state.worktreesByRepo)
+ .flat()
+ .find((entry) => entry.id === worktreeId)
+ if (!worktree) {
+ throw new Error(`Active worktree was not found in store: ${worktreeId}`)
+ }
+
+ return { worktreeId, rootPath: worktree.path }
+ })
+}
+
+export async function createMarkdownFixture(
+ context: ActiveWorktreeContext,
+ directoryName: string,
+ slug: string,
+ workerIndex: number,
+ initialMarkdown: string
+): Promise {
+ const directory = path.join(context.rootPath, directoryName)
+ await mkdir(directory, { recursive: true })
+
+ const filePath = path.join(directory, `${slug}-${workerIndex}-${Date.now()}-${randomUUID()}.md`)
+ await writeFile(filePath, initialMarkdown, 'utf8')
+
+ return filePath
+}
+
+export async function cleanupMarkdownFixture(filePath: string | null): Promise {
+ if (!filePath) {
+ return
+ }
+
+ try {
+ await rm(filePath, { force: true })
+ } catch {
+ // Best-effort cleanup must not hide the editor regression assertion.
+ }
+}
+
+export async function openMarkdownFixture(
+ page: Page,
+ context: ActiveWorktreeContext,
+ filePath: string
+): Promise {
+ const relativePath = path.relative(context.rootPath, filePath)
+
+ await page.evaluate(
+ ({ filePath, relativePath, worktreeId }) => {
+ const store = window.__store
+ if (!store) {
+ throw new Error('window.__store is not available')
+ }
+
+ store.getState().openFile({
+ filePath,
+ relativePath,
+ worktreeId,
+ language: 'markdown',
+ mode: 'edit'
+ })
+ },
+ { filePath, relativePath, worktreeId: context.worktreeId }
+ )
+
+ let activeFile: ActiveEditorFile | null = null
+ await expect
+ .poll(
+ async () => {
+ activeFile = await page.evaluate(() => {
+ const store = window.__store
+ if (!store) {
+ return null
+ }
+
+ const state = store.getState()
+ const file = state.openFiles.find((entry) => entry.id === state.activeFileId)
+ return file ? { filePath: file.filePath } : null
+ })
+ return activeFile?.filePath ?? null
+ },
+ {
+ timeout: 5_000,
+ message: `Active editor file did not become ${filePath}`
+ }
+ )
+ .toBe(filePath)
+
+ if (!activeFile) {
+ throw new Error(`Active editor file was not available after opening ${filePath}`)
+ }
+
+ return activeFile
+}
+
+export async function waitForRichMarkdownEditor(page: Page): Promise {
+ const editor = page.locator('.rich-markdown-editor')
+ await expect(editor).toBeVisible({ timeout: MARKDOWN_HYDRATION_TIMEOUT_MS })
+ return editor
+}
+
+export async function closeActiveEditorTab(page: Page, filePath: string): Promise {
+ const fileName = path.basename(filePath)
+ const tab = page.locator('[data-tab-id]').filter({ hasText: fileName }).last()
+ await tab.getByRole('button', { name: 'Close tab' }).click()
+
+ await expect(page.locator('.editor-header-path').filter({ hasText: fileName })).toHaveCount(0, {
+ timeout: 10_000
+ })
+}
diff --git a/tests/e2e/helpers/markdown-nested-toggle.ts b/tests/e2e/helpers/markdown-nested-toggle.ts
new file mode 100644
index 00000000000..6ea15a8c68b
--- /dev/null
+++ b/tests/e2e/helpers/markdown-nested-toggle.ts
@@ -0,0 +1,163 @@
+import type { Page } from '@stablyai/playwright-test'
+import { expect } from '@stablyai/playwright-test'
+
+const TOGGLE_RENDER_TIMEOUT_MS = 10_000
+
+export const NESTED_TOGGLE_FIXTURE_DIRECTORY = '.orca-e2e-markdown-nested-toggle'
+
+export const NESTED_TOGGLE_BODY_TEXT = 'Get back to ppl'
+
+export const NESTED_TOGGLE_MARKDOWN = [
+ '# 08/27/2026',
+ '',
+ '',
+ '08/26/2026
',
+ '',
+ '',
+ 'goals
',
+ '',
+ '- Get back to ppl',
+ '',
+ ' ',
+ '',
+ '- after inner toggle',
+ '',
+ ' ',
+ ''
+].join('\n')
+
+// An inner toggle carrying unsupported attributes cannot become an editable
+// node, so the whole block must still fall back to byte-preserving passthrough.
+export const UNSUPPORTED_NESTED_TOGGLE_MARKDOWN = [
+ '',
+ 'outer
',
+ '',
+ '',
+ 'inner
',
+ '',
+ 'Body',
+ '',
+ ' ',
+ '',
+ ' ',
+ ''
+].join('\n')
+
+export type RenderedToggles = {
+ toggleCount: number
+ nestedToggleCount: number
+ summaries: string[]
+ variants: (string | null)[]
+ passthroughBlockCount: number
+}
+
+export async function readRenderedToggles(page: Page): Promise {
+ return page.evaluate(() => {
+ const editor = document.querySelector('.rich-markdown-editor')
+ const toggles = Array.from(editor?.querySelectorAll('[data-type="details"]') ?? [])
+
+ return {
+ toggleCount: toggles.length,
+ nestedToggleCount:
+ editor?.querySelectorAll('[data-type="details"] [data-type="details"]').length ?? 0,
+ summaries: toggles.map(
+ (toggle) => toggle.querySelector('summary')?.textContent?.trim() ?? ''
+ ),
+ variants: toggles.map((toggle) => toggle.getAttribute('data-orca-toggle')),
+ passthroughBlockCount: editor?.querySelectorAll('[data-raw-markdown-html-block]').length ?? 0
+ }
+ })
+}
+
+export async function expectEditableNestedToggles(page: Page): Promise {
+ await expect
+ .poll(async () => readRenderedToggles(page), {
+ timeout: TOGGLE_RENDER_TIMEOUT_MS,
+ message: 'Nested toggles did not render as editable toggle nodes'
+ })
+ .toEqual({
+ toggleCount: 2,
+ nestedToggleCount: 1,
+ summaries: ['08/26/2026', 'goals'],
+ variants: ['heading-3', null],
+ passthroughBlockCount: 0
+ })
+}
+
+export async function expectPassthroughFallback(page: Page): Promise {
+ const passthrough = page.locator('.rich-markdown-editor [data-raw-markdown-html-block]')
+ await expect(passthrough).toHaveCount(1, { timeout: TOGGLE_RENDER_TIMEOUT_MS })
+ await expect(passthrough).toContainText('')
+}
+
+async function selectionIsInsideNestedToggleBody(page: Page): Promise {
+ return page.evaluate(() => {
+ const editor = document.querySelector('.rich-markdown-editor') as
+ | (Element & { editor?: { state?: { selection?: { from?: number; empty?: boolean } } } })
+ | null
+ const paragraph = editor?.querySelector(
+ '[data-type="details"] [data-type="details"] [data-type="detailsContent"] p'
+ ) as (Element & { pmViewDesc?: { posAtStart?: number; posAtEnd?: number } }) | null
+ const selection = editor?.editor?.state?.selection
+ const from = selection?.from
+ const start = paragraph?.pmViewDesc?.posAtStart
+ const end = paragraph?.pmViewDesc?.posAtEnd
+
+ if (
+ !selection?.empty ||
+ typeof from !== 'number' ||
+ typeof start !== 'number' ||
+ typeof end !== 'number'
+ ) {
+ return false
+ }
+
+ return from >= start && from <= end
+ })
+}
+
+export async function placeCaretInNestedToggleBody(page: Page): Promise {
+ // Real input only: click the body text itself, then extend to end of line.
+ await page
+ .locator('.rich-markdown-editor')
+ .getByText(NESTED_TOGGLE_BODY_TEXT, { exact: true })
+ .click()
+ await page.keyboard.press('End')
+
+ await expect
+ .poll(async () => selectionIsInsideNestedToggleBody(page), {
+ timeout: 3_000,
+ message: 'Clicking the nested toggle body text did not place the caret inside it'
+ })
+ .toBe(true)
+}
+
+export async function expectSentinelInsideNestedToggle(
+ page: Page,
+ sentinel: string
+): Promise {
+ const nestedBody = page.locator(
+ '.rich-markdown-editor [data-type="details"] [data-type="details"] [data-type="detailsContent"]'
+ )
+ await expect(nestedBody).toContainText(sentinel, { timeout: TOGGLE_RENDER_TIMEOUT_MS })
+}
+
+export function expectFileKeepsNesting(fileContents: string, sentinel: string): void {
+ const outerOpen = fileContents.indexOf('')
+ const firstClose = fileContents.indexOf(' ')
+ const sentinelAt = fileContents.indexOf(sentinel)
+
+ expect({
+ openTagCount: fileContents.split('').length - 1,
+ innerIsNestedInOuter: outerOpen !== -1 && innerOpen > outerOpen,
+ // The edited line has to stay inside the inner toggle, before its close.
+ sentinelInsideInner: sentinelAt !== -1 && sentinelAt > innerOpen && sentinelAt < firstClose
+ }).toEqual({
+ openTagCount: 2,
+ closeTagCount: 2,
+ innerIsNestedInOuter: true,
+ sentinelInsideInner: true
+ })
+}
diff --git a/tests/e2e/helpers/markdown-ordered-list-exit.ts b/tests/e2e/helpers/markdown-ordered-list-exit.ts
index 30e0258c4ea..5a026e78e53 100644
--- a/tests/e2e/helpers/markdown-ordered-list-exit.ts
+++ b/tests/e2e/helpers/markdown-ordered-list-exit.ts
@@ -1,16 +1,20 @@
-import { randomUUID } from 'node:crypto'
-import { mkdir, rm, writeFile } from 'node:fs/promises'
-import path from 'node:path'
-import type { Locator, Page } from '@stablyai/playwright-test'
+import type { Page } from '@stablyai/playwright-test'
import { expect } from '@stablyai/playwright-test'
+import {
+ createMarkdownFixture as createMarkdownFixtureIn,
+ type ActiveWorktreeContext
+} from './markdown-editor-fixture'
-const MARKDOWN_HYDRATION_TIMEOUT_MS = 25_000
const DRAFT_SERIALIZATION_TIMEOUT_MS = 10_000
+const FIXTURE_DIRECTORY = '.orca-e2e-markdown-ordered-list'
-export type ActiveWorktreeContext = {
- worktreeId: string
- rootPath: string
-}
+export {
+ cleanupMarkdownFixture,
+ getActiveWorktreeContext,
+ openMarkdownFixture,
+ waitForRichMarkdownEditor,
+ type ActiveWorktreeContext
+} from './markdown-editor-fixture'
export type MatrixRow = {
name: string
@@ -20,120 +24,13 @@ export type MatrixRow = {
run: (page: Page, sentinel: string) => Promise
}
-type ActiveEditorFile = {
- filePath: string
-}
-
-export async function getActiveWorktreeContext(page: Page): Promise {
- return page.evaluate(() => {
- const store = window.__store
- if (!store) {
- throw new Error('window.__store is not available')
- }
-
- const state = store.getState()
- const worktreeId = state.activeWorktreeId
- if (!worktreeId) {
- throw new Error('No active worktree is selected')
- }
-
- const worktree = Object.values(state.worktreesByRepo)
- .flat()
- .find((entry) => entry.id === worktreeId)
- if (!worktree) {
- throw new Error(`Active worktree was not found in store: ${worktreeId}`)
- }
-
- return { worktreeId, rootPath: worktree.path }
- })
-}
-
export async function createMarkdownFixture(
context: ActiveWorktreeContext,
slug: string,
workerIndex: number,
initialMarkdown: string
): Promise {
- const directory = path.join(context.rootPath, '.orca-e2e-markdown-ordered-list')
- await mkdir(directory, { recursive: true })
-
- const filePath = path.join(directory, `${slug}-${workerIndex}-${Date.now()}-${randomUUID()}.md`)
- await writeFile(filePath, initialMarkdown, 'utf8')
-
- return filePath
-}
-
-export async function cleanupMarkdownFixture(filePath: string | null): Promise {
- if (!filePath) {
- return
- }
-
- try {
- await rm(filePath, { force: true })
- } catch {
- // Best-effort cleanup must not hide the editor regression assertion.
- }
-}
-
-export async function openMarkdownFixture(
- page: Page,
- context: ActiveWorktreeContext,
- filePath: string
-): Promise {
- const relativePath = path.relative(context.rootPath, filePath)
-
- await page.evaluate(
- ({ filePath, relativePath, worktreeId }) => {
- const store = window.__store
- if (!store) {
- throw new Error('window.__store is not available')
- }
-
- store.getState().openFile({
- filePath,
- relativePath,
- worktreeId,
- language: 'markdown',
- mode: 'edit'
- })
- },
- { filePath, relativePath, worktreeId: context.worktreeId }
- )
-
- let activeFile: ActiveEditorFile | null = null
- await expect
- .poll(
- async () => {
- activeFile = await page.evaluate(() => {
- const store = window.__store
- if (!store) {
- return null
- }
-
- const state = store.getState()
- const file = state.openFiles.find((entry) => entry.id === state.activeFileId)
- return file ? { filePath: file.filePath } : null
- })
- return activeFile?.filePath ?? null
- },
- {
- timeout: 5_000,
- message: `Active editor file did not become ${filePath}`
- }
- )
- .toBe(filePath)
-
- if (!activeFile) {
- throw new Error(`Active editor file was not available after opening ${filePath}`)
- }
-
- return activeFile
-}
-
-export async function waitForRichMarkdownEditor(page: Page): Promise {
- const editor = page.locator('.rich-markdown-editor')
- await expect(editor).toBeVisible({ timeout: MARKDOWN_HYDRATION_TIMEOUT_MS })
- return editor
+ return createMarkdownFixtureIn(context, FIXTURE_DIRECTORY, slug, workerIndex, initialMarkdown)
}
export async function expectSentinelParagraphOutsideOrderedList(
diff --git a/tests/e2e/markdown-nested-toggle.spec.ts b/tests/e2e/markdown-nested-toggle.spec.ts
new file mode 100644
index 00000000000..537e6b65775
--- /dev/null
+++ b/tests/e2e/markdown-nested-toggle.spec.ts
@@ -0,0 +1,113 @@
+import { readFileSync } from 'node:fs'
+import { test, expect } from './helpers/orca-app'
+import {
+ cleanupMarkdownFixture,
+ closeActiveEditorTab,
+ createMarkdownFixture,
+ getActiveWorktreeContext,
+ openMarkdownFixture,
+ waitForRichMarkdownEditor
+} from './helpers/markdown-editor-fixture'
+import {
+ expectEditableNestedToggles,
+ expectFileKeepsNesting,
+ expectPassthroughFallback,
+ expectSentinelInsideNestedToggle,
+ NESTED_TOGGLE_FIXTURE_DIRECTORY,
+ NESTED_TOGGLE_MARKDOWN,
+ placeCaretInNestedToggleBody,
+ UNSUPPORTED_NESTED_TOGGLE_MARKDOWN
+} from './helpers/markdown-nested-toggle'
+import { waitForActiveWorktree, waitForSessionReady } from './helpers/store'
+
+test.describe('Markdown nested toggle regression', () => {
+ test.beforeEach(async ({ orcaPage }) => {
+ await waitForSessionReady(orcaPage)
+ await waitForActiveWorktree(orcaPage)
+ })
+
+ test('a nested toggle on disk reopens as editable toggles', async ({ orcaPage }, testInfo) => {
+ const context = await getActiveWorktreeContext(orcaPage)
+ let filePath: string | null = null
+
+ try {
+ filePath = await createMarkdownFixture(
+ context,
+ NESTED_TOGGLE_FIXTURE_DIRECTORY,
+ 'nested-toggle-reopen',
+ testInfo.workerIndex,
+ NESTED_TOGGLE_MARKDOWN
+ )
+ await openMarkdownFixture(orcaPage, context, filePath)
+ await waitForRichMarkdownEditor(orcaPage)
+
+ await expectEditableNestedToggles(orcaPage)
+ } finally {
+ await cleanupMarkdownFixture(filePath)
+ }
+ })
+
+ test('editing a nested toggle survives save and reopen', async ({ orcaPage }, testInfo) => {
+ const context = await getActiveWorktreeContext(orcaPage)
+ const sentinel = `editedInsideNestedToggle${Date.now()}`
+ let filePath: string | null = null
+
+ try {
+ filePath = await createMarkdownFixture(
+ context,
+ NESTED_TOGGLE_FIXTURE_DIRECTORY,
+ 'nested-toggle-edit',
+ testInfo.workerIndex,
+ NESTED_TOGGLE_MARKDOWN
+ )
+ await openMarkdownFixture(orcaPage, context, filePath)
+ await waitForRichMarkdownEditor(orcaPage)
+ await expectEditableNestedToggles(orcaPage)
+
+ await placeCaretInNestedToggleBody(orcaPage)
+ await orcaPage.keyboard.type(` ${sentinel}`)
+ await expectSentinelInsideNestedToggle(orcaPage, sentinel)
+
+ // Save through the real shortcut and assert the bytes that landed on disk.
+ await orcaPage.keyboard.press('ControlOrMeta+S')
+ const savedPath = filePath
+ await expect
+ .poll(() => readFileSync(savedPath, 'utf8'), { timeout: 10_000 })
+ .toContain(sentinel)
+ expectFileKeepsNesting(readFileSync(savedPath, 'utf8'), sentinel)
+
+ // The reported bug only appeared on reopen, so close the tab and parse
+ // the saved file again from scratch.
+ await closeActiveEditorTab(orcaPage, savedPath)
+ await openMarkdownFixture(orcaPage, context, savedPath)
+ await waitForRichMarkdownEditor(orcaPage)
+ await expectEditableNestedToggles(orcaPage)
+ await expectSentinelInsideNestedToggle(orcaPage, sentinel)
+ } finally {
+ await cleanupMarkdownFixture(filePath)
+ }
+ })
+
+ test('a nested toggle that cannot be represented stays raw passthrough', async ({
+ orcaPage
+ }, testInfo) => {
+ const context = await getActiveWorktreeContext(orcaPage)
+ let filePath: string | null = null
+
+ try {
+ filePath = await createMarkdownFixture(
+ context,
+ NESTED_TOGGLE_FIXTURE_DIRECTORY,
+ 'nested-toggle-unsupported',
+ testInfo.workerIndex,
+ UNSUPPORTED_NESTED_TOGGLE_MARKDOWN
+ )
+ await openMarkdownFixture(orcaPage, context, filePath)
+ await waitForRichMarkdownEditor(orcaPage)
+
+ await expectPassthroughFallback(orcaPage)
+ } finally {
+ await cleanupMarkdownFixture(filePath)
+ }
+ })
+})