angular/packages/compiler/test/shadow_css/shadow_css_spec.ts
Matthew Beck 6de4955124 fix(compiler): remove dedicated support for legacy shadow DOM selectors
Remove `_shadowDOMSelectorsRe` and `_convertShadowDOMSelectors` from `shadow_css.ts` so that `::shadow`, `::content`, `/shadow-deep/`, and `/shadow/` are no longer treated specially or stripped from user CSS. Instead, they are naturally scoped like standard selectors. Also remove the legacy failing test from `shadow_css_spec.ts`.
2026-05-12 14:24:29 -07:00

393 lines
17 KiB
TypeScript

/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {shim} from './utils';
describe('ShadowCss', () => {
it('should handle empty string', () => {
expect(shim('', 'contenta')).toEqualCss('');
});
it('should add an attribute to every rule', () => {
const css = 'one {color: red;}two {color: red;}';
const expected = 'one[contenta] {color:red;}two[contenta] {color:red;}';
expect(shim(css, 'contenta')).toEqualCss(expected);
});
it('should handle invalid css', () => {
const css = 'one {color: red;}garbage';
const expected = 'one[contenta] {color:red;}garbage';
expect(shim(css, 'contenta')).toEqualCss(expected);
});
it('should add an attribute to every selector', () => {
const css = 'one, two {color: red;}';
const expected = 'one[contenta], two[contenta] {color:red;}';
expect(shim(css, 'contenta')).toEqualCss(expected);
});
it('should support newlines in the selector and content ', () => {
const css = `
one,
two {
color: red;
}
`;
const expected = `
one[contenta],
two[contenta] {
color: red;
}
`;
expect(shim(css, 'contenta')).toEqualCss(expected);
});
it('should support newlines in the same selector and content ', () => {
const selector = `.foo:not(
.bar) {
background-color:
green;
}`;
expect(shim(selector, 'contenta', 'a-host')).toEqualCss(
'.foo[contenta]:not( .bar) { background-color:green;}',
);
});
it('should handle complicated selectors', () => {
expect(shim('one::before {}', 'contenta')).toEqualCss('one[contenta]::before {}');
expect(shim('one two {}', 'contenta')).toEqualCss('one[contenta] two[contenta] {}');
expect(shim('one > two {}', 'contenta')).toEqualCss('one[contenta] > two[contenta] {}');
expect(shim('one + two {}', 'contenta')).toEqualCss('one[contenta] + two[contenta] {}');
expect(shim('one ~ two {}', 'contenta')).toEqualCss('one[contenta] ~ two[contenta] {}');
expect(shim('.one.two > three {}', 'contenta')).toEqualCss(
'.one.two[contenta] > three[contenta] {}',
);
expect(shim('one[attr="value"] {}', 'contenta')).toEqualCss('one[attr="value"][contenta] {}');
expect(shim('one[attr=value] {}', 'contenta')).toEqualCss('one[attr=value][contenta] {}');
expect(shim('one[attr^="value"] {}', 'contenta')).toEqualCss('one[attr^="value"][contenta] {}');
expect(shim('one[attr$="value"] {}', 'contenta')).toEqualCss('one[attr$="value"][contenta] {}');
expect(shim('one[attr*="value"] {}', 'contenta')).toEqualCss('one[attr*="value"][contenta] {}');
expect(shim('one[attr|="value"] {}', 'contenta')).toEqualCss('one[attr|="value"][contenta] {}');
expect(shim('one[attr~="value"] {}', 'contenta')).toEqualCss('one[attr~="value"][contenta] {}');
expect(shim('one[attr="va lue"] {}', 'contenta')).toEqualCss('one[attr="va lue"][contenta] {}');
expect(shim('one[attr] {}', 'contenta')).toEqualCss('one[attr][contenta] {}');
expect(shim('[is="one"] {}', 'contenta')).toEqualCss('[is="one"][contenta] {}');
expect(shim('[attr] {}', 'contenta')).toEqualCss('[attr][contenta] {}');
});
it('should transform :host with attributes', () => {
expect(shim(':host [attr] {}', 'contenta', 'hosta')).toEqualCss('[hosta] [attr][contenta] {}');
expect(shim(':host(create-first-project) {}', 'contenta', 'hosta')).toEqualCss(
'create-first-project[hosta] {}',
);
expect(shim(':host[attr] {}', 'contenta', 'hosta')).toEqualCss('[attr][hosta] {}');
expect(shim(':host[attr]:where(:not(.cm-button)) {}', 'contenta', 'hosta')).toEqualCss(
'[attr][hosta]:where(:not(.cm-button)) {}',
);
});
it('should handle escaped sequences in selectors', () => {
expect(shim('one\\/two {}', 'contenta')).toEqualCss('one\\/two[contenta] {}');
expect(shim('one\\:two {}', 'contenta')).toEqualCss('one\\:two[contenta] {}');
expect(shim('one\\\\:two {}', 'contenta')).toEqualCss('one\\\\[contenta]:two {}');
expect(shim('.one\\:two {}', 'contenta')).toEqualCss('.one\\:two[contenta] {}');
expect(shim('.one\\:\\fc ber {}', 'contenta')).toEqualCss('.one\\:\\fc ber[contenta] {}');
expect(shim('.one\\:two .three\\:four {}', 'contenta')).toEqualCss(
'.one\\:two[contenta] .three\\:four[contenta] {}',
);
expect(shim('div:where(.one) {}', 'contenta', 'hosta')).toEqualCss(
'div[contenta]:where(.one) {}',
);
expect(shim('div:where() {}', 'contenta', 'hosta')).toEqualCss('div[contenta]:where() {}');
expect(shim(':where(a):where(b) {}', 'contenta', 'hosta')).toEqualCss(
':where(a[contenta]):where(b[contenta]) {}',
);
expect(shim('*:where(.one) {}', 'contenta', 'hosta')).toEqualCss('*[contenta]:where(.one) {}');
expect(shim('*:where(.one) ::ng-deep .foo {}', 'contenta', 'hosta')).toEqualCss(
'*[contenta]:where(.one) .foo {}',
);
});
it('should handle pseudo functions correctly', () => {
// :where()
expect(shim(':where(.one) {}', 'contenta', 'hosta')).toEqualCss(':where(.one[contenta]) {}');
expect(shim(':where(div.one span.two) {}', 'contenta', 'hosta')).toEqualCss(
':where(div.one[contenta] span.two[contenta]) {}',
);
expect(shim(':where(.one) .two {}', 'contenta', 'hosta')).toEqualCss(
':where(.one[contenta]) .two[contenta] {}',
);
expect(shim(':where(:host) {}', 'contenta', 'hosta')).toEqualCss(':where([hosta]) {}');
expect(shim(':where(:host) .one {}', 'contenta', 'hosta')).toEqualCss(
':where([hosta]) .one[contenta] {}',
);
expect(shim(':where(.one) :where(:host) {}', 'contenta', 'hosta')).toEqualCss(
':where(.one) :where([hosta]) {}',
);
expect(shim(':where(.one :host) {}', 'contenta', 'hosta')).toEqualCss(
':where(.one [hosta]) {}',
);
expect(shim('div :where(.one) {}', 'contenta', 'hosta')).toEqualCss(
'div[contenta] :where(.one[contenta]) {}',
);
expect(shim(':host :where(.one .two) {}', 'contenta', 'hosta')).toEqualCss(
'[hosta] :where(.one[contenta] .two[contenta]) {}',
);
expect(shim(':where(.one, .two) {}', 'contenta', 'hosta')).toEqualCss(
':where(.one[contenta], .two[contenta]) {}',
);
expect(shim(':where(.one > .two) {}', 'contenta', 'hosta')).toEqualCss(
':where(.one[contenta] > .two[contenta]) {}',
);
expect(shim(':where(> .one) {}', 'contenta', 'hosta')).toEqualCss(
':where( > .one[contenta]) {}',
);
expect(shim(':where(:not(.one) ~ .two) {}', 'contenta', 'hosta')).toEqualCss(
':where([contenta]:not(.one) ~ .two[contenta]) {}',
);
expect(shim(':where([foo]) {}', 'contenta', 'hosta')).toEqualCss(':where([foo][contenta]) {}');
// :is()
expect(shim('div:is(.foo) {}', 'contenta', 'a-host')).toEqualCss('div[contenta]:is(.foo) {}');
expect(shim(':is(.dark :host) {}', 'contenta', 'a-host')).toEqualCss(':is(.dark [a-host]) {}');
expect(shim(':is(.dark) :is(:host) {}', 'contenta', 'a-host')).toEqualCss(
':is(.dark) :is([a-host]) {}',
);
expect(shim(':host:is(.foo) {}', 'contenta', 'a-host')).toEqualCss('[a-host]:is(.foo) {}');
expect(shim(':is(.foo) {}', 'contenta', 'a-host')).toEqualCss(':is(.foo[contenta]) {}');
expect(shim(':is(.foo, .bar, .baz) {}', 'contenta', 'a-host')).toEqualCss(
':is(.foo[contenta], .bar[contenta], .baz[contenta]) {}',
);
expect(shim(':is(.foo, .bar) :host {}', 'contenta', 'a-host')).toEqualCss(
':is(.foo, .bar) [a-host] {}',
);
// :is() and :where()
expect(
shim(
':is(.foo, .bar) :is(.baz) :where(.one, .two) :host :where(.three:first-child) {}',
'contenta',
'a-host',
),
).toEqualCss(
':is(.foo, .bar) :is(.baz) :where(.one, .two) [a-host] :where(.three[contenta]:first-child) {}',
);
expect(shim(':where(:is(a)) {}', 'contenta', 'hosta')).toEqualCss(
':where(:is(a[contenta])) {}',
);
expect(shim(':where(:is(a, b)) {}', 'contenta', 'hosta')).toEqualCss(
':where(:is(a[contenta], b[contenta])) {}',
);
expect(shim(':where(:host:is(.one, .two)) {}', 'contenta', 'hosta')).toEqualCss(
':where([hosta]:is(.one, .two)) {}',
);
expect(shim(':where(:host :is(.one, .two)) {}', 'contenta', 'hosta')).toEqualCss(
':where([hosta] :is(.one[contenta], .two[contenta])) {}',
);
expect(shim(':where(:is(a, b) :is(.one, .two)) {}', 'contenta', 'hosta')).toEqualCss(
':where(:is(a[contenta], b[contenta]) :is(.one[contenta], .two[contenta])) {}',
);
expect(
shim(
':where(:where(a:has(.foo), b) :is(.one, .two:where(.foo > .bar))) {}',
'contenta',
'hosta',
),
).toEqualCss(
':where(:where(a[contenta]:has(.foo), b[contenta]) :is(.one[contenta], .two[contenta]:where(.foo > .bar))) {}',
);
expect(shim(':where(.two):first-child {}', 'contenta', 'hosta')).toEqualCss(
'[contenta]:where(.two):first-child {}',
);
expect(shim(':first-child:where(.two) {}', 'contenta', 'hosta')).toEqualCss(
'[contenta]:first-child:where(.two) {}',
);
expect(shim(':where(.two):nth-child(3) {}', 'contenta', 'hosta')).toEqualCss(
'[contenta]:where(.two):nth-child(3) {}',
);
expect(shim('table :where(td, th):hover { color: lime; }', 'contenta', 'hosta')).toEqualCss(
'table[contenta] [contenta]:where(td, th):hover { color:lime;}',
);
// :nth
expect(shim(':nth-child(3n of :not(p, a), :is(.foo)) {}', 'contenta', 'hosta')).toEqualCss(
'[contenta]:nth-child(3n of :not(p, a), :is(.foo)) {}',
);
expect(shim('li:nth-last-child(-n + 3) {}', 'contenta', 'a-host')).toEqualCss(
'li[contenta]:nth-last-child(-n + 3) {}',
);
expect(shim('dd:nth-last-of-type(3n) {}', 'contenta', 'a-host')).toEqualCss(
'dd[contenta]:nth-last-of-type(3n) {}',
);
expect(shim('dd:nth-of-type(even) {}', 'contenta', 'a-host')).toEqualCss(
'dd[contenta]:nth-of-type(even) {}',
);
// complex selectors
expect(shim(':host:is([foo],[foo-2])>div.example-2 {}', 'contenta', 'a-host')).toEqualCss(
'[a-host]:is([foo],[foo-2]) > div.example-2[contenta] {}',
);
expect(shim(':host:is([foo], [foo-2]) > div.example-2 {}', 'contenta', 'a-host')).toEqualCss(
'[a-host]:is([foo], [foo-2]) > div.example-2[contenta] {}',
);
expect(shim(':host:has([foo],[foo-2])>div.example-2 {}', 'contenta', 'a-host')).toEqualCss(
'[a-host]:has([foo],[foo-2]) > div.example-2[contenta] {}',
);
// :has()
expect(shim('div:has(a) {}', 'contenta', 'hosta')).toEqualCss('div[contenta]:has(a) {}');
expect(shim('div:has(a) :host {}', 'contenta', 'hosta')).toEqualCss('div:has(a) [hosta] {}');
expect(shim(':has(a) :host :has(b) {}', 'contenta', 'hosta')).toEqualCss(
':has(a) [hosta] [contenta]:has(b) {}',
);
expect(shim('div:has(~ .one) {}', 'contenta', 'hosta')).toEqualCss(
'div[contenta]:has(~ .one) {}',
);
// Unlike `:is()` or `:where()` the attribute selector isn't placed inside
// of `:has()`. That is deliberate, `[contenta]:has(a)` would select all
// `[contenta]` with `a` inside, while `:has(a[contenta])` would select
// everything that contains `a[contenta]`, targeting elements outside of
// encapsulated scope.
expect(shim(':has(a) :has(b) {}', 'contenta', 'hosta')).toEqualCss(
'[contenta]:has(a) [contenta]:has(b) {}',
);
expect(shim(':has(a, b) {}', 'contenta', 'hosta')).toEqualCss('[contenta]:has(a, b) {}');
expect(shim(':has(a, b:where(.foo), :is(.bar)) {}', 'contenta', 'hosta')).toEqualCss(
'[contenta]:has(a, b:where(.foo), :is(.bar)) {}',
);
expect(
shim(':has(a, b:where(.foo), :is(.bar):first-child):first-letter {}', 'contenta', 'hosta'),
).toEqualCss('[contenta]:has(a, b:where(.foo), :is(.bar):first-child):first-letter {}');
expect(
shim(':where(a, b:where(.foo), :has(.bar):first-child) {}', 'contenta', 'hosta'),
).toEqualCss(
':where(a[contenta], b[contenta]:where(.foo), [contenta]:has(.bar):first-child) {}',
);
expect(shim(':has(.one :host, .two) {}', 'contenta', 'hosta')).toEqualCss(
'[contenta]:has(.one [hosta], .two) {}',
);
expect(shim(':has(.one, :host) {}', 'contenta', 'hosta')).toEqualCss(
'[contenta]:has(.one, [hosta]) {}',
);
});
it('should handle :host inclusions inside pseudo-selectors selectors', () => {
expect(shim('.header:not(.admin) {}', 'contenta', 'hosta')).toEqualCss(
'.header[contenta]:not(.admin) {}',
);
expect(shim('.header:is(:host > .toolbar, :host ~ .panel) {}', 'contenta', 'hosta')).toEqualCss(
'.header[contenta]:is([hosta] > .toolbar, [hosta] ~ .panel) {}',
);
expect(
shim('.header:where(:host > .toolbar, :host ~ .panel) {}', 'contenta', 'hosta'),
).toEqualCss('.header[contenta]:where([hosta] > .toolbar, [hosta] ~ .panel) {}');
expect(shim('.header:not(.admin, :host.super .header) {}', 'contenta', 'hosta')).toEqualCss(
'.header[contenta]:not(.admin, .super[hosta] .header) {}',
);
expect(
shim('.header:not(.admin, :host.super .header, :host.mega .header) {}', 'contenta', 'hosta'),
).toEqualCss('.header[contenta]:not(.admin, .super[hosta] .header, .mega[hosta] .header) {}');
expect(shim('.one :where(.two, :host) {}', 'contenta', 'hosta')).toEqualCss(
'.one :where(.two[contenta], [hosta]) {}',
);
expect(shim('.one :where(:host, .two) {}', 'contenta', 'hosta')).toEqualCss(
'.one :where([hosta], .two[contenta]) {}',
);
expect(shim(':is(.foo):is(:host):is(.two) {}', 'contenta', 'hosta')).toEqualCss(
':is(.foo):is([hosta]):is(.two[contenta]) {}',
);
expect(shim(':where(.one, :host .two):first-letter {}', 'contenta', 'hosta')).toEqualCss(
'[contenta]:where(.one, [hosta] .two):first-letter {}',
);
expect(shim(':first-child:where(.one, :host .two) {}', 'contenta', 'hosta')).toEqualCss(
'[contenta]:first-child:where(.one, [hosta] .two) {}',
);
expect(
shim(':where(.one, :host .two):nth-child(3):is(.foo, a:where(.bar)) {}', 'contenta', 'hosta'),
).toEqualCss('[contenta]:where(.one, [hosta] .two):nth-child(3):is(.foo, a:where(.bar)) {}');
});
it('should handle escaped selector with space (if followed by a hex char)', () => {
// When esbuild runs with optimization.minify
// selectors are escaped: .über becomes .\fc ber.
// The space here isn't a separator between 2 selectors
expect(shim('.\\fc ber {}', 'contenta')).toEqual('.\\fc ber[contenta] {}');
expect(shim('.\\fc ker {}', 'contenta')).toEqual('.\\fc[contenta] ker[contenta] {}');
expect(shim('.pr\\fc fung {}', 'contenta')).toEqual('.pr\\fc fung[contenta] {}');
});
it('should leave calc() unchanged', () => {
const styleStr = 'div {height:calc(100% - 55px);}';
const css = shim(styleStr, 'contenta');
expect(css).toEqualCss('div[contenta] {height:calc(100% - 55px);}');
});
it('should shim rules with quoted content', () => {
const styleStr = 'div {background-image: url("a.jpg"); color: red;}';
const css = shim(styleStr, 'contenta');
expect(css).toEqualCss('div[contenta] {background-image:url("a.jpg"); color:red;}');
});
it('should handle when quoted content contains a closing parenthesis', () => {
// Regression test for https://github.com/angular/angular/issues/65137
expect(shim('p { background-image: url(")") } p { color: red }', 'contenta')).toEqualCss(
'p[contenta] { background-image: url(")") } p[contenta] { color: red }',
);
});
it('should shim rules with an escaped quote inside quoted content', () => {
const styleStr = 'div::after { content: "\\"" }';
const css = shim(styleStr, 'contenta');
expect(css).toEqualCss('div[contenta]::after { content:"\\""}');
});
it('should shim rules with curly braces inside quoted content', () => {
const styleStr = 'div::after { content: "{}" }';
const css = shim(styleStr, 'contenta');
expect(css).toEqualCss('div[contenta]::after { content:"{}"}');
});
it('should keep retain multiline selectors', () => {
// This is needed as shifting in line number will cause sourcemaps to break.
const styleStr = '.foo,\n.bar { color: red;}';
const css = shim(styleStr, 'contenta');
expect(css).toEqual('.foo[contenta], \n.bar[contenta] { color: red;}');
});
describe('comments', () => {
// Comments should be kept in the same position as otherwise inline sourcemaps break due to
// shift in lines.
it('should remove inline comments without adding extra lines', () => {
expect(shim('/* b {} */ b {}', 'contenta')).toBe(' b[contenta] {}');
});
it('should preserve internal newlines from multiline comments', () => {
expect(shim('/* b {}\n */ b {}', 'contenta')).toBe('\n b[contenta] {}');
});
it('should remove multiple inline comments without adding extra lines', () => {
expect(shim('/* b {} */ b {} /* a {} */ a {}', 'contenta')).toBe(
' b[contenta] {} a[contenta] {}',
);
});
it('should keep sourceMappingURL comments', () => {
expect(shim('b {} /*# sourceMappingURL=data:x */', 'contenta')).toBe(
'b[contenta] {} /*# sourceMappingURL=data:x */',
);
expect(shim('b {}/* #sourceMappingURL=data:x */', 'contenta')).toBe(
'b[contenta] {}/* #sourceMappingURL=data:x */',
);
});
it('should handle adjacent comments', () => {
expect(shim('/* comment 1 */ /* comment 2 */ b {}', 'contenta')).toBe(' b[contenta] {}');
});
});
});