Merge pull request #4133 from deepseek-harness/07akioni/fix-table-hover-height

fix(web): prevent short markdown tables from shifting on hover
This commit is contained in:
07akioni authored and GitHub committed 2026-09-14 14:03:49 +08:00
commit 49fcf87cb2
2 files changed
+48 -9

No files matched your search

+43 -5
View File
@@ -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()
@@ -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;
}