mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
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`.
393 lines
17 KiB
TypeScript
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] {}');
|
|
});
|
|
});
|
|
});
|