test(ui-sidebar-right): pin the settling slide styles

Check that the slide rules list the same docked items as the opening effect and that the settling rule overrides the open rule. Name the shared selector list on both sides and describe the opening step in the module header.
This commit is contained in:
Turtle committed 2026-10-21 23:32:12 +08:00
1 parent 3acbd08867
commit f2a52f6f3c
4 files changed
+60 -7

No files matched your search

@@ -48,7 +48,8 @@
}
/* Only docked Grid items slide. Their common ancestor remains untransformed
and without a stacking context, so fixed floating children escape. */
and without a stacking context, so fixed floating children escape. The
selector lists match `DOCKED_ITEMS` (SidebarRight.tsx). */
.panel :global([data-dockkit-host='dock']),
.panel :global([data-dockkit-empty]),
.panel :global([data-dockkit-divider]) {
@@ -8,8 +8,9 @@
*
* Docked content stays mounted while collapsed, translated off the frame's right
* edge, so opening and closing are one gesture in both presentations: a slide
* from and to that edge. Normal presentation moves the frame's tracks with
* the panel. A fullscreen opening reserves its underlying track only after
* from and to that edge. An item the opening commit mounts, such as the first
* tab of an empty column, takes the closed style once so it joins that slide.
* Normal presentation moves the frame's tracks with the panel. A fullscreen opening reserves its underlying track only after
* the panel covers the frame, without animating those hidden columns.
*
* The panel has no header of its own: its two controls — presentation switch
@@ -295,10 +296,10 @@ function PanelChrome({ sessionId, fullscreen, actions, t, shortcuts, toggleFulls
)
}
/** The docked Grid items whose transform slides with the panel (`SidebarRight.module.css`). */
const DOCKED_ITEMS = "[data-dockkit-host='dock'], [data-dockkit-empty], [data-dockkit-divider]"
/** The docked Grid items whose transform slides with the panel; the slide rules in `SidebarRight.module.css` list the same selectors. */
export const DOCKED_ITEMS = "[data-dockkit-host='dock'], [data-dockkit-empty], [data-dockkit-divider]"
/** Whether an animation is a docked item's running open or close slide. */
/** Whether an animation is a running transform transition: the panel's open or close slide. */
function isSlide(animation: Animation): boolean {
return 'transitionProperty' in animation && animation.transitionProperty === 'transform'
&& animation.playState !== 'finished' && animation.playState !== 'idle'
@@ -557,7 +557,7 @@ describe('RightbarSeat presentation', () => {
expect(h.frame.closeRightbar).toHaveBeenCalled()
})
it('starts an item the opening mounts from the closed style, leaving a sliding item alone', async () => {
it('starts an item mounted by the opening commit from the closed style and leaves a reversing item alone', async () => {
const h = await mountSeat()
const settled: { readonly item: Element; readonly open: boolean }[] = []
const getComputedStyle = window.getComputedStyle.bind(window)
@@ -0,0 +1,51 @@
/** Right sidebar slide style contracts shared with the panel's opening effect. */
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { DOCKED_ITEMS } from '../src/client/shell/SidebarRight.tsx'
const css = readFileSync(fileURLToPath(new URL('../src/client/shell/SidebarRight.module.css', import.meta.url)), 'utf8')
.replace(/\/\*[\s\S]*?\*\//g, ' ')
/** Each top-level rule in source order: its selector list and normalized declarations. */
const rules = [...css.matchAll(/([^{}]+)\{([^{}]*)\}/g)].map(([, selectorList = '', body = '']) => ({
selectors: selectorList.split(',').map(value => value.trim()),
declarations: new Map(body.split(';').filter(part => part.includes(':')).map((part) => {
const colon = part.indexOf(':')
return [part.slice(0, colon).trim(), part.slice(colon + 1).trim().replace(/\s+/g, ' ')] as const
})),
}))
/**
* The index of the rule whose selector list is exactly the given selectors.
* @param selectors - expected selector list in order.
* @returns the rule's source position.
*/
function ruleIndex(selectors: readonly string[]): number {
const index = rules.findIndex(rule => rule.selectors.join() === selectors.join())
expect(index).not.toBe(-1)
return index
}
describe('SidebarRight.module.css', () => {
const items = DOCKED_ITEMS.split(',').map(value => `:global(${value.trim()})`)
it('slides exactly the docked items the opening effect settles', () => {
expect(rules[ruleIndex(items.map(item => `.panel ${item}`))]?.declarations.get('transform'))
.toBe('translateX(var(--dsh-sidebar-width))')
expect(rules[ruleIndex(items.map(item => `.panel[data-sidebar-right-open] ${item}`))]?.declarations.get('transform'))
.toBe('none')
})
it('overrides the open slide with the closed style while an item settles', () => {
const open = ruleIndex(items.map(item => `.panel[data-sidebar-right-open] ${item}`))
const settling = ruleIndex(['.panel[data-sidebar-right-open] :global([data-sidebar-right-settling])'])
// Both selectors have the same specificity, so the later rule wins.
expect(settling).toBeGreaterThan(open)
expect(Object.fromEntries(rules[settling]!.declarations)).toEqual({
transform: 'translateX(var(--dsh-sidebar-width))',
visibility: 'hidden',
transition: 'none',
})
})
})