129 lines
5.9 KiB
TypeScript
129 lines
5.9 KiB
TypeScript
import { readFileSync } from 'node:fs'
|
|
import { resolve } from 'node:path'
|
|
import { describe, expect, it } from 'vitest'
|
|
import { compileStyle, parse } from 'vue/compiler-sfc'
|
|
import { adminWorkbenchChartTheme } from './adminWorkbenchChartTheme'
|
|
|
|
const read = (path: string) => readFileSync(resolve(process.cwd(), 'src', path), 'utf8')
|
|
const tokens = read('styles/adminWorkbenchTokens.css')
|
|
const styles = read('styles/adminWorkbench.css')
|
|
const page = read('modules/self-operated/pages/OverviewView.vue')
|
|
const listPages = [
|
|
'modules/self-operated/components/AnalyticsView.vue',
|
|
'modules/self-operated/components/InventoryTurnoverView.vue',
|
|
]
|
|
const consumers = [
|
|
...listPages,
|
|
'modules/self-operated/pages/InventoryView.vue',
|
|
'modules/self-operated/components/SkuTrend.vue',
|
|
'modules/self-operated/components/ChartDetailsDialog.vue',
|
|
'modules/self-operated/pages/CollectionView.vue',
|
|
'modules/self-operated/components/CollectionAccountMonitor.vue',
|
|
'modules/self-operated/components/CollectionProgressGauge.vue',
|
|
'modules/self-operated/components/CollectionTaskProgress.vue',
|
|
]
|
|
const root = (source: string) => {
|
|
const result = compileStyle({ source, filename: 'workbench.css', id: 'workbench-contract' })
|
|
expect(result.errors).toEqual([])
|
|
return result.rawResult!.root
|
|
}
|
|
|
|
describe('Admin workbench opt-in styles', () => {
|
|
it('keeps canvas chart colors aligned with the public CSS palette', () => {
|
|
const declarations = new Map<string, string>()
|
|
root(tokens).walkRules(rule => {
|
|
if (rule.selector !== '.admin-workbench') return
|
|
rule.walkDecls(declaration => { declarations.set(declaration.prop, declaration.value) })
|
|
})
|
|
for (const name of ['primary', 'text', 'muted', 'border', 'surface'] as const) {
|
|
expect(adminWorkbenchChartTheme.colors[name]).toBe(declarations.get(`--workbench-${name}`))
|
|
}
|
|
expect(adminWorkbenchChartTheme.colors.primarySoft).toBe(declarations.get('--workbench-accent-soft'))
|
|
})
|
|
|
|
it('keeps lazy-loaded styles scoped to opted-in pages and dialog roots', () => {
|
|
for (const source of [tokens, styles]) {
|
|
root(source).walkRules(rule => {
|
|
if (rule.parent?.type === 'atrule' && rule.parent.name.endsWith('keyframes')) return
|
|
for (const selector of rule.selectors) {
|
|
expect(selector).toMatch(/^\.admin-workbench(?:\s|\.|:|\[|$)/)
|
|
expect(selector).not.toMatch(/:global|:root/)
|
|
if (selector.includes('.el-dialog')) expect(selector).toMatch(/^\.admin-workbench\.is-dialog\.el-dialog/)
|
|
}
|
|
})
|
|
root(source).walkDecls(declaration => {
|
|
expect(declaration.prop).not.toMatch(/^--admin-/)
|
|
expect(declaration.important).toBeFalsy()
|
|
})
|
|
}
|
|
})
|
|
|
|
it('resolves every workbench variable from the public tokens instead of page defaults', () => {
|
|
const declared = new Set<string>()
|
|
root(tokens).walkDecls(declaration => {
|
|
if (declaration.prop.startsWith('--workbench-')) declared.add(declaration.prop)
|
|
})
|
|
const pageStyles = [page, ...consumers.map(read)].flatMap(source => parse(source).descriptor.styles.map(style => style.content)).join('\n')
|
|
for (const source of [styles, pageStyles]) {
|
|
root(source).walkDecls(declaration => {
|
|
expect(declaration.prop).not.toMatch(/^--workbench-/)
|
|
})
|
|
for (const match of source.matchAll(/var\((--workbench-[\w-]+)/g)) {
|
|
expect(declared.has(match[1]!)).toBe(true)
|
|
}
|
|
}
|
|
})
|
|
|
|
it('opts list cards and teleported dialogs in on their actual rendering containers', () => {
|
|
for (const file of listPages) {
|
|
const source = read(file)
|
|
expect(source).toContain("import '@/styles/adminWorkbench.css'")
|
|
const template = parse(source).descriptor.template!.ast!
|
|
type TemplateElement = Extract<(typeof template.children)[number], { type: 1 }>
|
|
const classesOf = (node?: TemplateElement) => node?.props.flatMap(prop => prop.type === 6 && prop.name === 'class' ? prop.value?.content.split(/\s+/) ?? [] : []) ?? []
|
|
let tables = 0
|
|
const visit = (nodes: typeof template.children, ancestors: TemplateElement[]) => {
|
|
for (const node of nodes) {
|
|
if (node.type !== 1) continue
|
|
if (node.tag === 'AdminPage') expect(classesOf(node)).toEqual(expect.arrayContaining(['admin-density', 'admin-workbench']))
|
|
if (node.tag === 'DataTable') {
|
|
expect(classesOf(ancestors.at(-1))).toContain('workbench-table')
|
|
expect(classesOf(node)).not.toContain('workbench-table')
|
|
tables++
|
|
}
|
|
if (node.tag === 'AppDialog') expect(classesOf(node)).toEqual(expect.arrayContaining(['admin-density-dialog', 'admin-workbench', 'is-dialog']))
|
|
visit(node.children, [...ancestors, node])
|
|
}
|
|
}
|
|
visit(template.children, [])
|
|
expect(tables).toBe(1)
|
|
}
|
|
})
|
|
|
|
it('preserves complete metric values and the existing toolbar layout ownership', () => {
|
|
root(styles).walkRules(rule => {
|
|
if (rule.selector.includes('.workbench-metric')) {
|
|
rule.walkDecls(declaration => {
|
|
expect(['text-overflow', 'line-clamp', '-webkit-line-clamp']).not.toContain(declaration.prop)
|
|
if (declaration.prop === 'overflow') expect(declaration.value).not.toBe('hidden')
|
|
expect(declaration.prop).not.toBe('height')
|
|
})
|
|
}
|
|
if (rule.selector.includes('.data-table__toolbar')) {
|
|
rule.walkDecls(declaration => {
|
|
if (rule.selector.includes('.workbench-table.is-compact-toolbar')) {
|
|
expect(['min-height', 'padding-top', 'padding-bottom']).toContain(declaration.prop)
|
|
} else {
|
|
expect(declaration.prop).not.toMatch(/^(?:padding|height|min-height|display|gap)/)
|
|
}
|
|
})
|
|
}
|
|
if (rule.selector.includes('.is-dialog')) {
|
|
rule.walkDecls(declaration => {
|
|
expect(declaration.prop).not.toMatch(/^(?:width|height|min-height|max-height|padding|margin|overflow)/)
|
|
})
|
|
}
|
|
})
|
|
})
|
|
})
|