diff --git a/apps/web/tests/markdown-wide-table.e2e.ts b/apps/web/tests/markdown-wide-table.e2e.ts index 09f980ab5c..45d889c6d4 100644 --- a/apps/web/tests/markdown-wide-table.e2e.ts +++ b/apps/web/tests/markdown-wide-table.e2e.ts @@ -56,6 +56,7 @@ const TAIL_MARKER = 'MWT_TABLES_DONE' const FILL_MARKER = 'MWT_FILL_C1' const WIDE_MARKER = 'MWT_WIDE_C01' const LONG_CELL_MARKER = 'MWT_LONGCELL_F1' +const SHORT_MARKER = 'MWT_SHORT_C1' const MARKERS = [FILL_MARKER, WIDE_MARKER, LONG_CELL_MARKER] /** Golden-facing names, in {@link MARKERS} order. */ const TABLE_NAMES = ['fill', 'wide', 'long-cell'] @@ -76,13 +77,20 @@ const SENTENCE = 'This cell carries one full sentence so the unwrapped table is const LONG_TOKEN = 'workspace/deepseek-harness/packages/client/ui-primitives/src/markdown/render.tsx/'.repeat(3) const CJK_SENTENCE = '这个单元格包含一段较长的中文说明,用来验证长内容在窄列宽下按最小可读宽度换行而不是把列压缩到无法阅读。' -/** The assistant markdown: one 3-column fill, one 12-column wide, one long-cell table. */ +/** The assistant markdown includes fitting and overflowing wide tables. */ function tablesMarkdown(): string { const wideHeader = [WIDE_MARKER, ...Array.from({ length: 11 }, (_, i) => `C${String(i + 2).padStart(2, '0')}`)] const wideRow = (row: number): string[] => Array.from({ length: 12 }, (_, i) => `v${String(row)}${String(i + 1).padStart(2, '0')}`) return [ - 'Three markdown tables exercise the wide-table layout rules.', + 'Markdown tables exercise the wide-table layout rules.', + '', + `| ${SHORT_MARKER} | C2 | C3 | C4 |`, + '| --- | --- | --- | --- |', + '| 1 | 2 | 3 | 4 |', + '| 5 | 6 | 7 | 8 |', + '', + 'The paragraph after the short table stays in place.', '', `| ${FILL_MARKER} | Current approach | Proposed approach |`, '| --- | --- | --- |', @@ -103,7 +111,7 @@ function tablesMarkdown(): string { ].join('\n') } -/** Build one closed, invariant-checked session fixture carrying the three tables. */ +/** Build one closed, invariant-checked session fixture carrying the tables. */ function wideTableFixture(): string { const session = Session.create(SessionId('markdown-wide-table-source')) const eventTimeOrigin = new Date().setHours(12, 0, 0, 0) @@ -257,7 +265,8 @@ describe('web e2e: markdown tables fill the column, wide ones break out and scro beforeAll(async () => { scaffold = await launchWebScaffold({}) await seedSession(scaffold, wideTableFixture(), SEED_ID) - browser = await chromium.launch() + // The geometry assertions include the space occupied by native scrollbars. + browser = await chromium.launch({ ignoreDefaultArgs: ['--hide-scrollbars'] }) page = await newEnglishPage(browser) tripwire = watchConsole(page) await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) @@ -386,13 +395,42 @@ describe('web e2e: markdown tables fill the column, wide ones break out and scro // Resting hidden overflow keeps the scroll position reachable and intact. expect(await wide.evaluate(element => element.scrollLeft)).toBeGreaterThanOrEqual(0) await wide.hover() - await expect.poll(overflowState, { timeout: 5_000 }).toBe('auto 0px') + await expect.poll(overflowState, { timeout: 5_000 }).toBe('scroll 0px') // Pointer leaves: the bar rests hidden again. await page.mouse.move(4, 4) await expect.poll(overflowState, { timeout: 5_000 }).toBe('hidden 8px') expect(tripwire.pageErrors).toEqual([]) }, 120_000) + it('keeps a fitting wide table and its following paragraph stationary during interaction', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-markdown-short-table-height')) + await settleAt(1680) + const short = page.locator('[class*="tableScroll"]', { hasText: SHORT_MARKER }) + await short.evaluate((element) => { element.scrollIntoView({ block: 'center', behavior: 'instant' }) }) + await page.mouse.move(4, 4) + await short.evaluate((element) => { element.blur() }) + expect(await short.evaluate(element => element.classList.contains('md-table-wide'))).toBe(true) + expect(await short.evaluate(element => element.scrollWidth - element.clientWidth)).toBeLessThanOrEqual(1) + const position = () => short.evaluate((element) => { + const following = element.nextElementSibling + if (following === null) throw new Error('short table has no following paragraph') + return { + height: element.getBoundingClientRect().height, + followingTop: following.getBoundingClientRect().top, + } + }) + const resting = await position() + await short.hover() + await expect.poll(position).toEqual(resting) + await page.mouse.move(4, 4) + await short.focus() + expect(await short.evaluate(element => document.activeElement === element)).toBe(true) + await expect.poll(position).toEqual(resting) + await short.evaluate((element) => { element.blur() }) + await expect.poll(position).toEqual(resting) + expect(tripwire.pageErrors).toEqual([]) + }, 120_000) + it('keeps the fill/scroll relations under page zoom', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-markdown-wide-table-zoom')) await sweep() diff --git a/packages/client/ui-primitives/src/markdown/MarkdownText.module.css b/packages/client/ui-primitives/src/markdown/MarkdownText.module.css index d29c50c99a..652affcf85 100644 --- a/packages/client/ui-primitives/src/markdown/MarkdownText.module.css +++ b/packages/client/ui-primitives/src/markdown/MarkdownText.module.css @@ -198,10 +198,11 @@ scrollbar STYLES (neither hover-conditioned `::-webkit-scrollbar*` rules nor a :hover `scrollbar-color` change reaches the painted bar), so the toggle is `overflow-x` itself — a layout change repaints reliably. The - resting padding matches the themed bar's height, so on an overflowing - table the appearing bar exactly replaces it and nothing below shifts. + resting padding matches the themed bar's height on the WebKit scrollbar + path. There, `scroll` reserves that height even when the table fits. + Other engines and overlay scrollbars may reserve a different height. Wheel and trackpad scrolling need the pointer over the table, which is - already the hover that re-enables `auto`. */ + already the hover that re-enables scrolling. */ .tableScroll:global(.md-table-wide) { overflow-x: hidden; padding-bottom: var(--dsh-scrollbar-width, 8px); @@ -209,7 +210,7 @@ .tableScroll:global(.md-table-wide):hover, .tableScroll:global(.md-table-wide):focus-visible { - overflow-x: auto; + overflow-x: scroll; padding-bottom: 0; }