diff --git a/examples/chat/angular/e2e/fixtures/markdown.json b/examples/chat/angular/e2e/fixtures/markdown.json index 4fa49a08e..8c98155b0 100644 --- a/examples/chat/angular/e2e/fixtures/markdown.json +++ b/examples/chat/angular/e2e/fixtures/markdown.json @@ -12,6 +12,14 @@ "match": { "userMessage": "respond with a bullet list" }, "response": { "content": "Three things:\n\n- alpha\n- beta\n- gamma" } }, + { + "match": { + "userMessage": "Give me a blockquote with two lines, then a markdown table with columns issue, expected behavior, verification." + }, + "response": { + "content": "> First quoted line\n> Second quoted line\n\n| Issue | Expected behavior | Verification |\n| --- | --- | --- |\n| Blockquote nesting | Both quoted lines render inside one blockquote | Inspect for one `
` with no stray quoted paragraph |\n| Table streaming | Rows remain inside the same table while content streams | Inspect for one `` with body rows |\n| Parser recovery | Final DOM has no raw pipe paragraphs | Confirm no paragraph text contains `|` delimiters |" + } + }, { "match": { "userMessage": "respond with the markdown checklist kitchen sink" }, "response": { diff --git a/examples/chat/angular/e2e/markdown-surfaces.spec.ts b/examples/chat/angular/e2e/markdown-surfaces.spec.ts index b2f2f2ea1..d2bb6fc8d 100644 --- a/examples/chat/angular/e2e/markdown-surfaces.spec.ts +++ b/examples/chat/angular/e2e/markdown-surfaces.spec.ts @@ -62,6 +62,29 @@ test('markdown checklist matrix: rich markdown renders with escaped html', async await expect(bubble).toContainText(""); }); +test('blockquote followed by table stays in one rendered markdown surface', async ({ page }) => { + const bubble = await sendPromptAndWait( + page, + 'Give me a blockquote with two lines, then a markdown table with columns issue, expected behavior, verification.', + ); + + await expect(bubble.locator('blockquote')).toHaveCount(1); + await expect(bubble.locator('blockquote')).toContainText('First quoted line'); + await expect(bubble.locator('blockquote')).toContainText('Second quoted line'); + await expect(bubble.locator('table')).toHaveCount(1); + await expect(bubble.locator('thead th')).toHaveText([ + 'Issue', + 'Expected behavior', + 'Verification', + ]); + await expect(bubble.locator('tbody tr')).toHaveCount(3); + await expect.poll(async () => tableColumnsAlign(bubble)).toBe(true); + await expect( + bubble.locator('p').filter({ hasText: /\|/ }), + 'table rows should not render as raw pipe paragraphs', + ).toHaveCount(0); +}); + test('streaming markdown table: keeps in-progress rows inside one table', async ({ page }) => { const hygiene = attachBrowserHygiene(page); await sendPrompt(page, 'stream a markdown comparison table regression'); @@ -94,6 +117,7 @@ test('streaming markdown table: blockquote followed by table does not throw', as const bubble = await waitForFinalAssistant(page); await expect(bubble.locator('blockquote')).toBeVisible(); await expect(bubble.locator('blockquote')).toContainText('First line of the quote.'); + await expect(bubble.locator('blockquote')).toContainText('Second line of the quote.'); await expect(bubble.locator('table')).toHaveCount(1); await expect(bubble.locator('thead th')).toHaveText([ 'Issue', @@ -101,6 +125,11 @@ test('streaming markdown table: blockquote followed by table does not throw', as 'Verification', ]); await expect(bubble.locator('tbody tr')).toHaveCount(2); + await expect.poll(async () => tableColumnsAlign(bubble)).toBe(true); + await expect( + bubble.locator('p').filter({ hasText: /\|/ }), + 'table rows should not render as raw pipe paragraphs', + ).toHaveCount(0); expect(hygiene.consoleErrors).toEqual([]); expect(hygiene.failedRequests).toEqual([]); }); diff --git a/libs/chat/src/lib/styles/chat-sidenav.styles.spec.ts b/libs/chat/src/lib/styles/chat-sidenav.styles.spec.ts index d080cf823..63d245c16 100644 --- a/libs/chat/src/lib/styles/chat-sidenav.styles.spec.ts +++ b/libs/chat/src/lib/styles/chat-sidenav.styles.spec.ts @@ -94,6 +94,14 @@ describe('CHAT_SIDENAV_STYLES — drawer elevation + z-index token', () => { it('no longer declares the .chat-sidenav__scrim selector', () => { expect(normalized).not.toMatch(/\.chat-sidenav__scrim\s*\{/); }); + it('keeps the closed drawer panel inert until the drawer is open', () => { + expect(normalized).toMatch( + /:host\(\[data-mode="drawer"\]\) \.chat-sidenav\s*\{[^}]*pointer-events:\s*none\s*;/, + ); + expect(normalized).toMatch( + /:host\(\[data-mode="drawer"\]\[data-open="true"\]\) \.chat-sidenav\s*\{[^}]*pointer-events:\s*auto\s*;/, + ); + }); }); describe('CHAT_SIDENAV_STYLES — Archived disclosure', () => { diff --git a/libs/chat/src/lib/styles/chat-sidenav.styles.ts b/libs/chat/src/lib/styles/chat-sidenav.styles.ts index cc8c63bb1..3480b2ed9 100644 --- a/libs/chat/src/lib/styles/chat-sidenav.styles.ts +++ b/libs/chat/src/lib/styles/chat-sidenav.styles.ts @@ -65,13 +65,14 @@ export const CHAT_SIDENAV_STYLES = ` height: 100%; transition: transform 200ms ease; transform: translateX(-100%); - /* Re-enable pointer events on the panel itself (host is inert above). When - * closed the panel is translated off-screen so this is harmless; when open - * it covers the host box and stays fully interactive. */ - pointer-events: auto; + /* Keep the closed/closing panel inert. The host is inert above, and the + * panel can still be hit during its transform transition unless pointer + * events are restored only for the open state below. */ + pointer-events: none; } :host([data-mode="drawer"][data-open="true"]) .chat-sidenav { transform: translateX(0); + pointer-events: auto; } .chat-sidenav__header { flex-shrink: 0;