Files
deepseek-harness/packages/client/ui-theme/tests/client-styles.client.spec.ts
T

158 lines
6.5 KiB
TypeScript

// @vitest-environment jsdom
/** Dynamic ui-theme entry owns the global styles in dependency order. */
import { Context } from '@deepseek-ai/cordis'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { installThemeStyles } from '../src/client/styles.ts'
const PLUGIN_ID = '@deepseek-ai/dsh-client-ui-theme'
const HAIRLINE_WIDTH = '--dsh-hairline-width'
const contexts = new Set<Context>()
let previousWidth: string
let previousPriority: string
beforeEach(() => {
previousWidth = document.documentElement.style.getPropertyValue(HAIRLINE_WIDTH)
previousPriority = document.documentElement.style.getPropertyPriority(HAIRLINE_WIDTH)
})
afterEach(async () => {
for (const ctx of contexts) await ctx.fiber.dispose()
contexts.clear()
document.head.querySelectorAll(`style[data-plugin="${PLUGIN_ID}"]`).forEach((node) => { node.remove() })
if (previousWidth === '') document.documentElement.style.removeProperty(HAIRLINE_WIDTH)
else document.documentElement.style.setProperty(HAIRLINE_WIDTH, previousWidth, previousPriority)
vi.restoreAllMocks()
vi.unstubAllGlobals()
})
async function mountStyles() {
const ctx = new Context()
contexts.add(ctx)
const fiber = ctx.plugin({ apply(scope) { installThemeStyles(scope) } })
await fiber.await()
return fiber
}
describe('ui-theme client styles', () => {
it('mounts every global sheet in dependency order and removes them on dispose', async () => {
const fiber = await mountStyles()
const styles = [...document.head.querySelectorAll<HTMLStyleElement>(`style[data-plugin="${PLUGIN_ID}"]`)]
expect(styles.map(style => style.dataset.pluginCss)).toEqual([
`${PLUGIN_ID}/base.css`,
`${PLUGIN_ID}/corner-shape.css`,
`${PLUGIN_ID}/design-platform.css`,
`${PLUGIN_ID}/focus.css`,
`${PLUGIN_ID}/onboarding.css`,
`${PLUGIN_ID}/scrollbar.css`,
`${PLUGIN_ID}/gradient-shadow-text.css`,
`${PLUGIN_ID}/shiki.css`,
])
await fiber.dispose()
expect(document.head.querySelectorAll(`style[data-plugin="${PLUGIN_ID}"]`)).toHaveLength(0)
expect(document.documentElement.style.getPropertyValue(HAIRLINE_WIDTH)).toBe(previousWidth)
})
it('calculates the stroke when resolution notifications are unavailable', async () => {
vi.stubGlobal('matchMedia', undefined)
vi.stubGlobal('devicePixelRatio', 2)
await mountStyles()
expect(document.documentElement.style.getPropertyValue(HAIRLINE_WIDTH)).toBe('0.5px')
})
it('mounts and releases styles without publishing a DPR width for a detached document', async () => {
const detached = document.implementation.createHTMLDocument('')
vi.stubGlobal('document', detached)
try {
const fiber = await mountStyles()
expect(detached.defaultView).toBeNull()
expect(detached.head.querySelectorAll(`style[data-plugin="${PLUGIN_ID}"]`)).toHaveLength(8)
expect(detached.documentElement.style.getPropertyValue(HAIRLINE_WIDTH)).toBe('')
await fiber.dispose()
expect(detached.head.querySelectorAll(`style[data-plugin="${PLUGIN_ID}"]`)).toHaveLength(0)
} finally {
vi.unstubAllGlobals()
}
})
it('acquires nothing without a browser document', () => {
const ctx = new Context()
contexts.add(ctx)
vi.stubGlobal('document', undefined)
try {
installThemeStyles(ctx)
} finally {
vi.unstubAllGlobals()
}
expect(document.head.querySelectorAll(`style[data-plugin="${PLUGIN_ID}"]`)).toHaveLength(0)
expect(document.documentElement.style.getPropertyValue(HAIRLINE_WIDTH)).toBe(previousWidth)
})
describe('DPR-aware hairline width', () => {
let queries: MediaQueryList[]
beforeEach(() => {
queries = []
vi.stubGlobal('devicePixelRatio', 3)
vi.stubGlobal('matchMedia', (media: string): MediaQueryList => {
const events = new EventTarget()
const query: MediaQueryList = {
media, matches: true, onchange: null,
// oxlint-disable-next-line typescript/no-deprecated -- The platform interface requires both legacy members.
addListener: vi.fn(), removeListener: vi.fn(),
addEventListener: events.addEventListener.bind(events),
removeEventListener: events.removeEventListener.bind(events),
dispatchEvent: events.dispatchEvent.bind(events),
}
queries.push(query)
return query
})
})
it.each([
{ dpr: 1, width: 1 },
{ dpr: 1.25, width: 0.8 },
{ dpr: 2, width: 0.5 },
{ dpr: 3, width: 1 / 3 },
{ dpr: 4, width: 0.5 },
])('matches a 0.5px border at DPR $dpr', async ({ dpr, width }) => {
vi.stubGlobal('devicePixelRatio', dpr)
await mountStyles()
expect(document.documentElement.style.getPropertyValue(HAIRLINE_WIDTH)).toBe(`${width}px`)
expect(queries.map(query => query.media)).toEqual([`(resolution: ${dpr}dppx)`])
})
it('updates one document value through successive DPR changes and detaches old queries', async () => {
await mountStyles()
vi.stubGlobal('devicePixelRatio', 2)
queries[0]!.dispatchEvent(new Event('change'))
expect(document.documentElement.style.getPropertyValue(HAIRLINE_WIDTH)).toBe('0.5px')
vi.stubGlobal('devicePixelRatio', 1)
queries[1]!.dispatchEvent(new Event('change'))
expect(document.documentElement.style.getPropertyValue(HAIRLINE_WIDTH)).toBe('1px')
expect(queries.map(query => query.media)).toEqual(['(resolution: 3dppx)', '(resolution: 2dppx)', '(resolution: 1dppx)'])
queries[0]!.dispatchEvent(new Event('change'))
expect(queries).toHaveLength(3)
})
it('restores the prior declaration and releases its listener before reinstalling', async () => {
const root = document.documentElement
root.style.setProperty(HAIRLINE_WIDTH, '0.75px', 'important')
const first = await mountStyles()
expect(root.style.getPropertyPriority(HAIRLINE_WIDTH)).toBe('')
await first.dispose()
expect(root.style.getPropertyValue(HAIRLINE_WIDTH)).toBe('0.75px')
expect(root.style.getPropertyPriority(HAIRLINE_WIDTH)).toBe('important')
queries[0]!.dispatchEvent(new Event('change'))
expect(queries).toHaveLength(1)
vi.stubGlobal('devicePixelRatio', 2)
const second = await mountStyles()
expect(root.style.getPropertyValue(HAIRLINE_WIDTH)).toBe('0.5px')
await second.dispose()
queries[1]!.dispatchEvent(new Event('change'))
expect(queries).toHaveLength(2)
expect(root.style.getPropertyValue(HAIRLINE_WIDTH)).toBe('0.75px')
})
})
})