Skip to content

Commit

Permalink
css-has-pseudo: fix handling of stylesheets loaded after the polyfill…
Browse files Browse the repository at this point in the history
… is started (#1533)
  • Loading branch information
romainmenke authored Dec 13, 2024
1 parent cd4287d commit f7d3906
Show file tree
Hide file tree
Showing 14 changed files with 174 additions and 205 deletions.
4 changes: 4 additions & 0 deletions plugins/css-has-pseudo/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changes to CSS Has Pseudo

### Unreleased (patch)

- Fix handling of stylesheets loaded after the polyfill was first started.

### 7.0.1

_October 23, 2024_
Expand Down
2 changes: 1 addition & 1 deletion plugins/css-has-pseudo/dist/browser-global.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion plugins/css-has-pseudo/dist/browser-global.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion plugins/css-has-pseudo/dist/browser.cjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion plugins/css-has-pseudo/dist/browser.cjs.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion plugins/css-has-pseudo/dist/browser.mjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion plugins/css-has-pseudo/dist/browser.mjs.map

Large diffs are not rendered by default.

67 changes: 41 additions & 26 deletions plugins/css-has-pseudo/src/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default function cssHasPseudo(document, options) {
options.observedAttributes = [];
}

options.observedAttributes = options.observedAttributes.filter((x) => {
options.observedAttributes = options.observedAttributes.filter(function(x) {
return (typeof x === 'string');
});

Expand All @@ -61,13 +61,24 @@ export default function cssHasPseudo(document, options) {

// observe DOM modifications that affect selectors
if ('MutationObserver' in self) {
const mutationObserver = new MutationObserver((mutationsList) => {
mutationsList.forEach(mutation => {
[].forEach.call(mutation.addedNodes || [], node => {
const mutationObserver = new MutationObserver(function(mutationsList) {
mutationsList.forEach(function(mutation) {
[].forEach.call(mutation.addedNodes || [], function(node) {
// walk stylesheets to collect observed css rules
if (node.nodeType === 1 && node.sheet) {
if (node.nodeType !== 1) {
return;
}

if (node.sheet) {
walkStyleSheet(node.sheet);
return;
}

node.addEventListener('load', function (e) {
if (e.target && e.target.sheet) {
walkStyleSheet(e.target.sheet);
}
});
});

// transform observed css rules
Expand Down Expand Up @@ -130,7 +141,7 @@ export default function cssHasPseudo(document, options) {

// Not all of these elements have all of these properties.
// But the code above checks if they exist first.
['checked', 'selected', 'readOnly', 'required'].forEach((property) => {
['checked', 'selected', 'readOnly', 'required'].forEach(function(property) {
[
'HTMLButtonElement',
'HTMLFieldSetElement',
Expand All @@ -142,7 +153,7 @@ export default function cssHasPseudo(document, options) {
'HTMLProgressElement',
'HTMLSelectElement',
'HTMLTextAreaElement',
].forEach((elementName) => {
].forEach(function(elementName) {
if (elementName in self && self[elementName].prototype) {
observeProperty(self[elementName].prototype, property);
}
Expand All @@ -162,28 +173,30 @@ export default function cssHasPseudo(document, options) {
cancelAnimationFrame(transformObservedItemsThrottledBusy);
}

transformObservedItemsThrottledBusy = requestAnimationFrame(() => {
transformObservedItemsThrottledBusy = requestAnimationFrame(function() {
transformObservedItems();
});
}

// transform observed css rules
function transformObservedItems() {
observedItems.forEach((item) => {
observedItems.forEach(function(item) {
const nodes = [];

let matches = [];
try {
matches = document.querySelectorAll(item.selector);
} catch (e) {
if (options.debug) {
// eslint-disable-next-line no-console
console.error(e);
if (item.selector) {
try {
matches = document.querySelectorAll(item.selector);
} catch (e) {
if (options.debug) {
// eslint-disable-next-line no-console
console.error(e);
}
return;
}
return;
}

[].forEach.call(matches, (element) => {
[].forEach.call(matches, function(element) {
// memorize the node
nodes.push(element);

Expand All @@ -198,7 +211,7 @@ export default function cssHasPseudo(document, options) {
});

// remove the encoded attribute from all nodes that no longer match them
item.nodes.forEach(node => {
item.nodes.forEach(function(node) {
if (nodes.indexOf(node) === -1) {
node.removeAttribute(item.attributeName);

Expand All @@ -216,7 +229,7 @@ export default function cssHasPseudo(document, options) {
function cleanupObservedCssRules() {
[].push.apply(
observedItems,
observedItems.splice(0).filter((item) => {
observedItems.splice(0).filter(function(item) {
return item.rule.parentStyleSheet &&
item.rule.parentStyleSheet.ownerNode &&
document.documentElement.contains(item.rule.parentStyleSheet.ownerNode);
Expand All @@ -228,7 +241,7 @@ export default function cssHasPseudo(document, options) {
function walkStyleSheet(styleSheet) {
try {
// walk a css rule to collect observed css rules
[].forEach.call(styleSheet.cssRules || [], (rule, index) => {
[].forEach.call(styleSheet.cssRules || [], function(rule, index) {
if (rule.selectorText) {
rule.selectorText = rule.selectorText.replace(/\.js-has-pseudo\s/g, '');

Expand All @@ -246,12 +259,14 @@ export default function cssHasPseudo(document, options) {

for (let i = 0; i < hasSelectors.length; i++) {
const hasSelector = hasSelectors[i];
observedItems.push({
rule: rule,
selector: hasSelector,
attributeName: encodeCSS(hasSelector),
nodes: [],
});
if (hasSelector) {
observedItems.push({
rule: rule,
selector: hasSelector,
attributeName: encodeCSS(hasSelector),
nodes: [],
});
}
}
} catch (e) {
if (options.debug) {
Expand Down
83 changes: 83 additions & 0 deletions plugins/css-has-pseudo/test/_browser-stylesheet-loading.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title></title>
<link rel="help" href="https://drafts.csswg.org/selectors/#relational">
<!-- Included only to trigger CORS errors -->
<!-- This stylesheet is not accessible in JS and must not cause cascading failures -->
<link rel="stylesheet" href="http://localhost:8081/test/basic.expect.css">

<script src="http://localhost:8082/dist/browser-global.js"></script>
<script>cssHasPseudo(document, { observedAttributes: ['attrname', 'a_test_attr', 'c_test_attr'], debug: false, hover: true, forcePolyfill: window.location.hash === '#force-polyfill' });</script>

<!-- This is the real test stylesheet and has correct CORS attributes and http headers -->
<link id="the-stylesheet" rel="stylesheet" href="http://localhost:8081/test/browser-stylesheet-loading.expect.css" crossorigin="anonymous">
</head>
<body>
<p id="hello-world">Hello World</p>

<script type="module">
function rafP(callback) {
return new Promise((resolve) => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
callback();
resolve();
});
});
});
}

self.runTest = async function runTest() {
const testLoadingAfterJSResult = await testLoadingAfterJS();

return testLoadingAfterJSResult;
}

async function testLoadingAfterJS() {
function testColor(test_name, color) {
var actual = getComputedStyle(document.getElementById('hello-world')).color;
if (actual !== color) {
throw new Error(test_name + ': div#hello-world.color; expected ' + color + ' but got ' + actual);
}
}

const green = 'rgb(0, 128, 0)';
const black = 'rgb(0, 0, 0)';

await rafP(() => {
testColor(`Text is green when loading a stylesheet after initializing the polyfill`, green);
});

document.getElementById('the-stylesheet').remove();

await rafP(() => {
testColor(`Text is green when loading a stylesheet after initializing the polyfill`, black);
});

var head = document.head;
var link = document.createElement("link");

link.type = "text/css";
link.rel = "stylesheet";
link.href = "http://localhost:8081/test/browser-stylesheet-loading.expect.css?delay=50";
link.crossOrigin = "anonymous";

const loadWaiter = new Promise((resolve) => {
link.addEventListener('load', resolve);
});

head.appendChild(link);

await loadWaiter;

await rafP(() => {
testColor(`Text is green when loading a stylesheet after initializing the polyfill`, green);
});

return true;
}
</script>
</body>
</html>
25 changes: 25 additions & 0 deletions plugins/css-has-pseudo/test/_browser.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ const requestListener = async function (req, res) {
res.writeHead(200);
res.end(await fs.readFile('test/_browser.html', 'utf8'));
break;
case '/stylesheet-loading':
res.setHeader('Content-type', 'text/html');
res.writeHead(200);
res.end(await fs.readFile('test/_browser-stylesheet-loading.html', 'utf8'));
break;
case '/test/basic.expect.css':
// Stylesheet WITHOUT CORS headers
res.setHeader('Content-type', 'text/css');
Expand All @@ -29,6 +34,15 @@ const requestListener = async function (req, res) {
res.writeHead(200);
res.end(await fs.readFile('test/browser.expect.css', 'utf8'));
break;
case '/test/browser-stylesheet-loading.expect.css':
await new Promise(resolve => setTimeout(resolve, parseInt(parsedUrl.searchParams.get('delay') ?? '0', 10)));

// Stylesheet WITH CORS headers
res.setHeader('Content-type', 'text/css');
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:8080');
res.writeHead(200);
res.end(await fs.readFile('test/browser-stylesheet-loading.expect.css', 'utf8'));
break;
case '/dist/browser-global.js':
res.setHeader('Content-type', 'text/javascript');
res.writeHead(200);
Expand Down Expand Up @@ -99,6 +113,17 @@ if (!process.env.DEBUG) {
throw new Error('Test failed, expected "window.runTest()" to return true');
}
}

{
await page.goto('http://localhost:8080/stylesheet-loading#force-polyfill');
const result = await page.evaluate(async () => {
// eslint-disable-next-line no-undef
return await window.runTest();
});
if (!result) {
throw new Error('Test failed, expected "window.runTest()" to return true');
}
}
} finally {
await browser.close();

Expand Down
9 changes: 9 additions & 0 deletions plugins/css-has-pseudo/test/_tape.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ postcssTape(plugin, { skipPackageNameCheck: true })({
},
'browser': {
message: 'prepare CSS for chrome test',
options: {
preserve: false,
},
},
'browser-stylesheet-loading': {
message: 'prepare CSS for chrome test',
options: {
preserve: false,
},
},
'plugin-order-logical:before': {
message: 'works with other plugins that modify selectors',
Expand Down
3 changes: 3 additions & 0 deletions plugins/css-has-pseudo/test/browser-stylesheet-loading.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
body:has(p) {
color: green;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.js-has-pseudo [csstools-has-2q-33-2s-3d-1m-2w-2p-37-14-34-15]:not(does-not-exist):not(does-not-exist) {
color: green;
}
Loading

0 comments on commit f7d3906

Please sign in to comment.