|
|
@@ -106,6 +106,10 @@ interface ListMetrics {
|
|
|
overflows: boolean
|
|
|
/** Border-box width minus client width: the space the scrollbar takes out of the content area. */
|
|
|
band: number
|
|
|
+ /** Distance from the scrollbar's right edge to the sidebar edge. */
|
|
|
+ scrollbarEdgeOffset: number
|
|
|
+ /** Distance from the first row background's right edge to the sidebar edge. */
|
|
|
+ rowEdgeInset: number
|
|
|
/** Client-area right edge in viewport coordinates (`clientWidth` excludes the scrollbar band). */
|
|
|
clientRight: number
|
|
|
/** Border-box right edge in viewport coordinates. */
|
|
|
@@ -133,6 +137,8 @@ function measureList(page: Page): Promise<ListMetrics> {
|
|
|
if (list === null) throw new Error('sidebar session list not in the DOM')
|
|
|
const time = list.querySelector<HTMLElement>('[class*="time"]')
|
|
|
if (time === null) throw new Error('no row relative-time element in the sidebar list')
|
|
|
+ const row = list.querySelector<HTMLElement>('[role="treeitem"]')
|
|
|
+ if (row === null) throw new Error('no row in the sidebar list')
|
|
|
// Each indirection variable is resolved through its own throwaway probe
|
|
|
// appended to the list: `var()` substitution then happens where the list
|
|
|
// sits in the cascade, which is the claim, and `color` normalizes whatever
|
|
|
@@ -167,6 +173,9 @@ function measureList(page: Page): Promise<ListMetrics> {
|
|
|
const style = getComputedStyle(list)
|
|
|
const pseudoWidth = getComputedStyle(list, '::-webkit-scrollbar').width
|
|
|
const barWidth = pseudoWidth === 'auto' ? 15 : Number.parseFloat(pseudoWidth)
|
|
|
+ const listRect = list.getBoundingClientRect()
|
|
|
+ const sidebarEdge = list.parentElement?.getBoundingClientRect().right
|
|
|
+ if (sidebarEdge === undefined) throw new Error('sidebar session list has no layout parent')
|
|
|
return {
|
|
|
gutter: style.scrollbarGutter,
|
|
|
width: pseudoWidth,
|
|
|
@@ -177,9 +186,11 @@ function measureList(page: Page): Promise<ListMetrics> {
|
|
|
token: resolve('--dsh-scrollbar-thumb'),
|
|
|
hoverToken: resolve('--dsh-scrollbar-thumb-hover'),
|
|
|
overflows: list.scrollHeight > list.clientHeight,
|
|
|
- band: list.getBoundingClientRect().width - list.clientWidth,
|
|
|
- clientRight: list.getBoundingClientRect().left + list.clientWidth,
|
|
|
- borderRight: list.getBoundingClientRect().right,
|
|
|
+ band: listRect.width - list.clientWidth,
|
|
|
+ scrollbarEdgeOffset: sidebarEdge - listRect.right,
|
|
|
+ rowEdgeInset: sidebarEdge - row.getBoundingClientRect().right,
|
|
|
+ clientRight: listRect.left + list.clientWidth,
|
|
|
+ borderRight: listRect.right,
|
|
|
timeRight: time.getBoundingClientRect().right,
|
|
|
// The bar is drawn in the rightmost `barWidth` of the border box, whether
|
|
|
// or not that space was reserved. Its width comes from the sheet where the
|
|
|
@@ -188,7 +199,28 @@ function measureList(page: Page): Promise<ListMetrics> {
|
|
|
// absent. Taking the UA width as the fallback is what keeps the assertion
|
|
|
// honest: assuming 0 there would report no occlusion precisely in the
|
|
|
// state that has it.
|
|
|
- timeCoveredBy: Math.max(0, time.getBoundingClientRect().right - (list.getBoundingClientRect().right - barWidth)),
|
|
|
+ timeCoveredBy: Math.max(0, time.getBoundingClientRect().right - (listRect.right - barWidth)),
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Measure only overflow and row inset, which remain observable when every
|
|
|
+ * session is hidden under a collapsed workspace group.
|
|
|
+ * @param page - the page under test.
|
|
|
+ * @returns the list overflow state and first row's trailing inset.
|
|
|
+ */
|
|
|
+function measureRowInset(page: Page): Promise<Pick<ListMetrics, 'overflows' | 'rowEdgeInset'>> {
|
|
|
+ return page.evaluate(() => {
|
|
|
+ const list = document.querySelector<HTMLElement>('[role="tree"][aria-label="Sessions"]')
|
|
|
+ if (list === null) throw new Error('sidebar session list not in the DOM')
|
|
|
+ const row = list.querySelector<HTMLElement>('[role="treeitem"]')
|
|
|
+ if (row === null) throw new Error('no row in the sidebar list')
|
|
|
+ const sidebarEdge = list.parentElement?.getBoundingClientRect().right
|
|
|
+ if (sidebarEdge === undefined) throw new Error('sidebar session list has no layout parent')
|
|
|
+ return {
|
|
|
+ overflows: list.scrollHeight > list.clientHeight,
|
|
|
+ rowEdgeInset: sidebarEdge - row.getBoundingClientRect().right,
|
|
|
}
|
|
|
})
|
|
|
}
|
|
|
@@ -222,6 +254,8 @@ function renderGeometry(light: ListMetrics, dark: ListMetrics): string {
|
|
|
`- --dsh-scrollbar-thumb-hover: ${metrics.hoverToken}`,
|
|
|
`- list overflows: ${String(metrics.overflows)}`,
|
|
|
`- reserved band: ${String(metrics.band)}px`,
|
|
|
+ `- scrollbar inset from the sidebar edge: ${String(metrics.scrollbarEdgeOffset)}px`,
|
|
|
+ `- row background inset from the sidebar edge: ${String(metrics.rowEdgeInset)}px`,
|
|
|
`- relative time covered by the bar: ${String(metrics.timeCoveredBy)}px`,
|
|
|
`- relative time ends inside the content area: ${String(metrics.timeRight <= metrics.clientRight)}`,
|
|
|
`- content area ends before the border box: ${String(metrics.clientRight < metrics.borderRight)}`,
|
|
|
@@ -299,6 +333,8 @@ describe('web e2e: sidebar session list scrollbar (reserved gutter / themed thum
|
|
|
// drawn over it. Removing the declaration makes it exactly 0. The value
|
|
|
// itself is not pinned — it tracks `scrollbar-width` and the platform.
|
|
|
expect(metrics.band).toBeGreaterThan(0)
|
|
|
+ expect(metrics.scrollbarEdgeOffset).toBe(2)
|
|
|
+ expect(metrics.rowEdgeInset).toBe(12)
|
|
|
// The reported symptom, stated directly: no part of the row's relative time
|
|
|
// lies under the bar. Measures 7 on clean master — the `h` of `1h` is the
|
|
|
// covered part. Unlike the client-edge comparison below it does not go
|
|
|
@@ -317,6 +353,20 @@ describe('web e2e: sidebar session list scrollbar (reserved gutter / themed thum
|
|
|
expect(tripwire.pageErrors).toEqual([])
|
|
|
}, 60_000)
|
|
|
|
|
|
+ it('keeps the row background inset when overflow disappears', async () => {
|
|
|
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-sidebar-scrollbar-stable-inset'))
|
|
|
+ expect(await measureRowInset(page)).toEqual({ overflows: true, rowEdgeInset: 12 })
|
|
|
+ const bucket = page.getByText('Ungrouped', { exact: true }).locator('..').locator('..')
|
|
|
+ await bucket.click()
|
|
|
+ try {
|
|
|
+ await expect.poll(async () => (await measureRowInset(page)).overflows, { timeout: 10_000 }).toBe(false)
|
|
|
+ expect(await measureRowInset(page)).toEqual({ overflows: false, rowEdgeInset: 12 })
|
|
|
+ } finally {
|
|
|
+ await expandSeededSessions(page)
|
|
|
+ }
|
|
|
+ expect(tripwire.pageErrors).toEqual([])
|
|
|
+ }, 60_000)
|
|
|
+
|
|
|
it('renders the themed thumb through the WebKit path in both palettes', async () => {
|
|
|
onTestFailed(() => saveFailureShot(page, 'web-e2e-sidebar-scrollbar-theme'))
|
|
|
const light = await measureList(page)
|