Просмотр исходного кода

test(web): guard fitting table geometry across interaction

07akioni 2 недель назад
Родитель
Сommit
8ab3579e06

+ 42 - 4
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 FILL_MARKER = 'MWT_FILL_C1'
 const WIDE_MARKER = 'MWT_WIDE_C01'
 const WIDE_MARKER = 'MWT_WIDE_C01'
 const LONG_CELL_MARKER = 'MWT_LONGCELL_F1'
 const LONG_CELL_MARKER = 'MWT_LONGCELL_F1'
+const SHORT_MARKER = 'MWT_SHORT_C1'
 const MARKERS = [FILL_MARKER, WIDE_MARKER, LONG_CELL_MARKER]
 const MARKERS = [FILL_MARKER, WIDE_MARKER, LONG_CELL_MARKER]
 /** Golden-facing names, in {@link MARKERS} order. */
 /** Golden-facing names, in {@link MARKERS} order. */
 const TABLE_NAMES = ['fill', 'wide', 'long-cell']
 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 LONG_TOKEN = 'workspace/deepseek-harness/packages/client/ui-primitives/src/markdown/render.tsx/'.repeat(3)
 const CJK_SENTENCE = '这个单元格包含一段较长的中文说明,用来验证长内容在窄列宽下按最小可读宽度换行而不是把列压缩到无法阅读。'
 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 {
 function tablesMarkdown(): string {
   const wideHeader = [WIDE_MARKER, ...Array.from({ length: 11 }, (_, i) => `C${String(i + 2).padStart(2, '0')}`)]
   const wideHeader = [WIDE_MARKER, ...Array.from({ length: 11 }, (_, i) => `C${String(i + 2).padStart(2, '0')}`)]
   const wideRow = (row: number): string[] =>
   const wideRow = (row: number): string[] =>
     Array.from({ length: 12 }, (_, i) => `v${String(row)}${String(i + 1).padStart(2, '0')}`)
     Array.from({ length: 12 }, (_, i) => `v${String(row)}${String(i + 1).padStart(2, '0')}`)
   return [
   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 |`,
     `| ${FILL_MARKER} | Current approach | Proposed approach |`,
     '| --- | --- | --- |',
     '| --- | --- | --- |',
@@ -103,7 +111,7 @@ function tablesMarkdown(): string {
   ].join('\n')
   ].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 {
 function wideTableFixture(): string {
   const session = Session.create(SessionId('markdown-wide-table-source'))
   const session = Session.create(SessionId('markdown-wide-table-source'))
   const eventTimeOrigin = new Date().setHours(12, 0, 0, 0)
   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 () => {
   beforeAll(async () => {
     scaffold = await launchWebScaffold({})
     scaffold = await launchWebScaffold({})
     await seedSession(scaffold, wideTableFixture(), SEED_ID)
     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)
     page = await newEnglishPage(browser)
     tripwire = watchConsole(page)
     tripwire = watchConsole(page)
     await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
     await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
@@ -393,6 +402,35 @@ describe('web e2e: markdown tables fill the column, wide ones break out and scro
     expect(tripwire.pageErrors).toEqual([])
     expect(tripwire.pageErrors).toEqual([])
   }, 120_000)
   }, 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 () => {
   it('keeps the fill/scroll relations under page zoom', async () => {
     onTestFailed(() => saveFailureShot(page, 'web-e2e-markdown-wide-table-zoom'))
     onTestFailed(() => saveFailureShot(page, 'web-e2e-markdown-wide-table-zoom'))
     await sweep()
     await sweep()

+ 3 - 3
packages/client/ui-primitives/src/markdown/MarkdownText.module.css

@@ -198,9 +198,9 @@
    scrollbar STYLES (neither hover-conditioned `::-webkit-scrollbar*` rules
    scrollbar STYLES (neither hover-conditioned `::-webkit-scrollbar*` rules
    nor a :hover `scrollbar-color` change reaches the painted bar), so the
    nor a :hover `scrollbar-color` change reaches the painted bar), so the
    toggle is `overflow-x` itself — a layout change repaints reliably. The
    toggle is `overflow-x` itself — a layout change repaints reliably. The
-   resting padding matches the themed bar's height. `scroll` reserves that
-   height even when the table fits, so removing the padding cannot shift
-   the content below it.
+   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
    Wheel and trackpad scrolling need the pointer over the table, which is
    already the hover that re-enables scrolling. */
    already the hover that re-enables scrolling. */
 .tableScroll:global(.md-table-wide) {
 .tableScroll:global(.md-table-wide) {