Files
orca/config/scripts/check-has-restyle-scope.test.mjs
T
NeilandNaufaldi 19f6c10c3f perf(renderer): drop group-has-[:focus-visible] rules that restyle whole subtrees (#36098)
Fixes #34312. Takes over naufaldi/orca#1 by @naufaldi, with the main.css part dropped because #34932 already covers it.

- SshTargetCard and the reasoning chevron now use the scoped `[[class~='group/x']:has(:focus-visible)_&]` shape. Tailwind's `group-has-*` compiles to a non-subject `:has()` whose subject is `*`, and that rule made Blink restyle whole subtrees on every mutation. This change is the entire measured win: 19.4 → 1.6 ms, 12.5 → 3.6 ms and 8.0 → 2.4 ms per mutation.
- rich-markdown-editor.css drops a redundant `:has()` on the TOC header, as defence in depth.
- Adds a guard, check-has-restyle-scope.mjs. It compiles the renderer's Tailwind CSS, adds plain stylesheets and the listed vendor stylesheets, and fails on any non-subject `:has()` whose subject has no class, id or attribute. Three pdf.js rules are baselined after being measured as harmless. It runs in lint, check:changed and PR CI in about 1.5 s.

Co-authored-by: Naufaldi <naufaldi.rafif@gmail.com>
2026-10-29 14:31:45 -07:00

69 lines
2.9 KiB
JavaScript

import path from 'node:path'
import { describe, expect, it } from 'vitest'
import { compileTailwind, findHasRestyleProblems } from './check-has-restyle-scope.mjs'
// Compiled shapes (the Tailwind spelling that emits each is noted); each restyles the whole anchor subtree.
// Rules need a declaration: lightningcss drops empty ones before the check sees them.
const subtreeWide = [
'.a:is(:where(.group\\/row):has(:is(:focus-visible)) *)', // group-has-[…]/row
'.a:is(:where(.group\\/x):is(:has(:focus-visible)) *)', // group-[:has(…)]/x
'.a:is(:where(.peer):is(:has(.x)) ~ *)', // peer-[:has(…)]
'.a:is(:where(.peer):has(:checked) ~ *)', // peer-has-checked
'.a:not(:is(:where(.group):has(:is(.x)) *))', // not-group-has-[…]
'.a:is(:where(.group):not(:has(:is(.x))) *)', // group-not-has-[…]
':is(.a:has(.x) > *)', // [&:has(x)]:*:
':is(.a:has(.x) *)', // [&:has(x)]:**:
':is(.a:has(:is(.x)) *)', // has-[x]:**:
'.row:has(.x) .a *', // [.row:has(x)_&_*]
'.a:has(:is(.x)) svg', // has-[x]:[&_svg]:
'.a:has(.x) svg', // [&:has(x)_svg]
':is(.anchor:has(.trigger) .scoped) *', // [&:is(.anchor:has(.trigger)_.scoped)_*]
'.a:nth-child(2n of .g:has(.x) *)',
'.zz:has(.a + .b) span',
'.foo:has(.a ~ .b) *',
'.zz:has(:focus-visible) span',
'.zz:has(.x) ::before',
':is(body:has(.modal)) span'
]
const scoped = [
"[class~='group/row']:has(:focus-visible) .a",
'.card:has([data-slot="card-action"])',
'.x .comment-md-p > strong:first-child:has(+ br) + br',
'.zz:has(~ .y) span',
'body:has(.qq) .yy',
':is(body:has(.modal)) .x',
':is(.anchor:has(.trigger) .scoped) .ok',
'.a:is(:where(.group\\/row):hover *)',
'.anchor:has(.x) :where(.scoped), .anchor:has(.x) :is(.scoped, #target)',
':host(.dark) .card:has(.x)',
"body > :is([data-slot='dialog-content']):has(~ [data-slot='dialog-overlay'])"
]
describe('findHasRestyleProblems', () => {
it.each(subtreeWide)('flags %s', (selector) => {
expect(findHasRestyleProblems(`${selector} { color: red }`)).toHaveLength(1)
})
it.each(scoped)('allows %s', (selector) => {
expect(findHasRestyleProblems(`${selector} { color: red }`)).toEqual([])
})
it('sees rules through nesting, @layer and @media', () => {
expect(findHasRestyleProblems('.zz:has(.x) { & * { color: red } }')).toHaveLength(1)
expect(
findHasRestyleProblems('@layer u { @media (hover: hover) { .zz:has(.x) i { color: red } } }')
).toHaveLength(1)
})
it('flags a group-has-* utility through the real Tailwind compile', async () => {
const css = await compileTailwind(
"@import 'tailwindcss/utilities' source(none);\n@source inline('group-has-[:focus-visible]/row:opacity-100');",
{ base: path.resolve(import.meta.dirname, '../../src/renderer/src/assets') }
)
expect(findHasRestyleProblems(css)).toEqual([
'.group-has-\\[\\:focus-visible\\]\\/row\\:opacity-100:is(:where(.group\\/row):has(:focus-visible) *)'
])
})
})