|
1 | | -import { |
2 | | - postcssIsolateStyles, |
3 | | - splitSelectorPseudo |
4 | | -} from 'node/postcss/isolateStyles' |
5 | | - |
6 | | -// helper to run plugin transform on selector |
7 | | -function apply( |
8 | | - prefixPlugin: ReturnType<typeof postcssIsolateStyles>, |
9 | | - selector: string |
10 | | -) { |
11 | | - // `prepare` is available on the runtime plugin but missing from the types, thus cast to `any` |
12 | | - const { Rule } = (prefixPlugin as any).prepare({ |
13 | | - root: { source: { input: { file: 'foo/base.css' } } } |
14 | | - }) |
15 | | - const rule = { selectors: [selector] } |
16 | | - Rule(rule, { result: {} }) |
17 | | - return rule.selectors[0] |
| 1 | +import { postcssIsolateStyles } from 'node/postcss/isolateStyles' |
| 2 | +import postcss from 'postcss' |
| 3 | + |
| 4 | +const INPUT_CSS = ` |
| 5 | +/* simple classes */ |
| 6 | +.example { color: red; } |
| 7 | +.class-a { color: coral; } |
| 8 | +.class-b { color: deepskyblue; } |
| 9 | +
|
| 10 | +/* escaped colon in class */ |
| 11 | +.baz\\:not\\(.bar\\) { display: block; } |
| 12 | +.disabled\\:opacity-50:disabled { opacity: .5; } |
| 13 | +
|
| 14 | +/* pseudos (class + element) */ |
| 15 | +.button:hover { color: pink; } |
| 16 | +.button:focus:hover { color: hotpink; } |
| 17 | +.item::before { content: '•'; } |
| 18 | +::first-letter { color: pink; } |
| 19 | +::before { content: ''; } |
| 20 | +
|
| 21 | +/* universal + :not */ |
| 22 | +* { background-color: red; } |
| 23 | +*:not(.b) { text-transform: uppercase; } |
| 24 | +
|
| 25 | +/* combinators */ |
| 26 | +.foo:hover .bar { background: blue; } |
| 27 | +ul > li.active { color: green; } |
| 28 | +a + b ~ c { color: orange; } |
| 29 | +
|
| 30 | +/* ids + attribute selectors */ |
| 31 | +#wow { color: yellow; } |
| 32 | +[data-world] .d { padding: 10px 20px; } |
| 33 | +
|
| 34 | +/* :root and chained tags */ |
| 35 | +:root { --bs-blue: #0d6efd; } |
| 36 | +:root .a { --bs-green: #bada55; } |
| 37 | +html { margin: 0; } |
| 38 | +body { padding: 0; } |
| 39 | +html body div { color: blue; } |
| 40 | +
|
| 41 | +/* grouping with commas */ |
| 42 | +.a, .b { color: red; } |
| 43 | +
|
| 44 | +/* multiple repeated groups to ensure stability */ |
| 45 | +.a, .b { color: coral; } |
| 46 | +.a { animation: glow 1s linear infinite alternate; } |
| 47 | +
|
| 48 | +/* nested blocks */ |
| 49 | +.foo { |
| 50 | + svg { display: none; } |
| 51 | + .bar { display: inline; } |
18 | 52 | } |
19 | 53 |
|
20 | | -describe('node/postcss/isolateStyles', () => { |
21 | | - const plugin = postcssIsolateStyles() |
| 54 | +/* standalone pseudos */ |
| 55 | +:first-child { color: pink; } |
| 56 | +:hover { color: blue; } |
| 57 | +:active { color: red; } |
22 | 58 |
|
23 | | - test('splitSelectorPseudo skips escaped colon', () => { |
24 | | - const input = '.foo\\:bar' |
25 | | - const [selector, pseudo] = splitSelectorPseudo(input) |
26 | | - expect(selector).toBe(input) |
27 | | - expect(pseudo).toBe('') |
28 | | - }) |
| 59 | +/* keyframes (should be ignored) */ |
| 60 | +@keyframes fade { |
| 61 | + from { opacity: 0; } |
| 62 | + to { opacity: 1; } |
| 63 | +} |
| 64 | +@-webkit-keyframes glow { |
| 65 | + from { color: coral; } |
| 66 | + to { color: red; } |
| 67 | +} |
| 68 | +@-moz-keyframes glow { |
| 69 | + from { color: coral; } |
| 70 | + to { color: red; } |
| 71 | +} |
| 72 | +@-o-keyframes glow { |
| 73 | + from { color: coral; } |
| 74 | + to { color: red; } |
| 75 | +} |
| 76 | +` |
29 | 77 |
|
30 | | - test('splitSelectorPseudo splits on pseudo selectors', () => { |
31 | | - const input = '.button:hover' |
32 | | - const [selector, pseudo] = splitSelectorPseudo(input) |
33 | | - expect(selector).toBe('.button') |
34 | | - expect(pseudo).toBe(':hover') |
| 78 | +describe('node/postcss/isolateStyles', () => { |
| 79 | + test('transforms selectors and skips keyframes', () => { |
| 80 | + const out = run(INPUT_CSS) |
| 81 | + expect(out.css).toMatchSnapshot() |
35 | 82 | }) |
36 | 83 |
|
37 | | - it('postcssIsolateStyles inserts :not(...) in the right place', () => { |
38 | | - const input = '.disabled\\:opacity-50:disabled' |
39 | | - const result = apply(plugin, input) |
40 | | - expect(result).toBe( |
41 | | - '.disabled\\:opacity-50:not(:where(.vp-raw, .vp-raw *)):disabled' |
42 | | - ) |
| 84 | + test('idempotent (running twice produces identical CSS)', () => { |
| 85 | + const first = run(INPUT_CSS).css |
| 86 | + const second = run(first).css |
| 87 | + expect(second).toBe(first) |
43 | 88 | }) |
44 | 89 | }) |
| 90 | + |
| 91 | +function run(css: string, from = 'src/styles/vp-doc.css') { |
| 92 | + return postcss([postcssIsolateStyles()]).process(css, { from }) |
| 93 | +} |
0 commit comments