Skip to content

Commit d6af4cc

Browse files
alexander-akaitclaude
authored andcommitted
test(css): hold every lowering to the whole computed style, not the probes
The lowering tier compared the properties each fixture names. It now compares every property the CSSOM reports for those elements, so a rewrite reaching one nobody thought to probe is one the tier sees. Two differences it turned up are real and now stated rather than hidden: - a folded color serializes differently. Chromium keeps the space a color was written in, so `color(srgb .2 .4 .6)` computes as itself where `#369` computes as `rgb(51, 102, 153)` — the same color, a different string for a script to read. Values are compared as the color they paint, which is what the rewrite preserves. - lowering `light-dark()` writes `--webpack-light` and `--webpack-dark`, which a script enumerating computed custom properties would see where the source has none. The fixtures naming that lowering now say so. The corpus tier already reads every `.css` under `configCases` and compares the CSSOM rule by rule — conditions evaluated rather than compared as text, blocks computed by the engine — and every `style=""` as the declarations it parses to. The queries and shorthand slots this session learned to shorten had no corpus entry, so they were held by unit tests alone; they have one now.
1 parent 524c217 commit d6af4cc

5 files changed

Lines changed: 129 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"webpack": minor
3+
---
4+
5+
Drop a CSS declaration, a shorthand slot or a query condition that says what another already says.

‎test/configCases/css/minimize-values/__snapshots__/ConfigCacheTest.snap‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ Array [
5555
"/*! webpack: ...with whitespace on both sides */@media screen and (width>=100px){.t45{color:red}}",
5656
"/*! webpack: a Media Queries Level 4 range */@media (width>=40rem){.t46{color:red}}",
5757
"/*! webpack: ...on both sides of the feature */@media (400px<=width<=700px){.t47{color:red}}",
58+
"/*! webpack: \`all and (…)\` matches what the condition alone matches, so the type
59+
is six bytes saying nothing */@media (width>=100px){.t47a{color:red}}",
60+
"/*! declined: \`all\` on its own is the whole query, and \`only\` before it is there
61+
to hide the rule from an engine reading CSS 2's grammar */@media all{.t47b{color:red}}@media only all and (width>=100px){.t47c{color:red}}@media not all and (width>=100px){.t47d{color:red}}",
62+
"/*! webpack: an operand a condition already states, whichever rule states it */@media (monochrome){.t47e{color:red}}@supports (display:grid){.t47f{color:red}}@supports ((display:grid) or (display:flex)){.t47g{color:red}}",
63+
"/*! declined: a chain mixing the two operators is no flat list of either's, and
64+
two queries are not two operands of anything */@supports (display:grid) and (display:flex) or (display:table){.t47h{color:red}}@media (monochrome),(monochrome){.t47i{color:red}}",
5865
"/*! webpack: a container query */@container card (width>400px){.t48{color:red}}",
5966
"/*! webpack: an @supports condition */@supports (display:grid){.t49{color:red}}",
6067
"/*! webpack: ...on a custom property */@supports (--x:1){.t50{color:red}}",
@@ -196,6 +203,10 @@ Array [
196203
"/*! declined: ...but only then */.t96{transform:matrix3d(20,20,0,0,40,40,0,0,0,0,1,5,80,80,0,1)}",
197204
"/*! webpack: a 2D pair of 1 leaves the z scale alone */.t97{transform:scaleZ(1.5)}",
198205
"/*! webpack: a zero time keeps a unit, and \`s\` is the shorter one it can carry */.t98{transition-duration:0s}",
206+
"/*! webpack: a shorthand slot holding its own initial goes, and a slot taking a
207+
length or a color is read by the classes a sibling's own value is in — every
208+
slot holding its initial leaves the shortest of them saying all of them */.t98a{border:none}.t98b{column-rule:none}.t98c{border:solid red}",
209+
"/*! declined: a sibling filling the slot keeps the initial there */.t98d{border:medium 1px}.t98e{border:currentcolor red}",
199210
"/*! webpack: a component spelling the property's own initial says nothing beside
200211
another — omitting its group leaves exactly that keyword */.t99{grid-auto-flow:dense}",
201212
"/*! declined: the other alternative of the group is not the initial */.t99a{grid-auto-flow:column dense}",

‎test/configCases/css/minimize-values/__snapshots__/ConfigTest.snap‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ Array [
5555
"/*! webpack: ...with whitespace on both sides */@media screen and (width>=100px){.t45{color:red}}",
5656
"/*! webpack: a Media Queries Level 4 range */@media (width>=40rem){.t46{color:red}}",
5757
"/*! webpack: ...on both sides of the feature */@media (400px<=width<=700px){.t47{color:red}}",
58+
"/*! webpack: \`all and (…)\` matches what the condition alone matches, so the type
59+
is six bytes saying nothing */@media (width>=100px){.t47a{color:red}}",
60+
"/*! declined: \`all\` on its own is the whole query, and \`only\` before it is there
61+
to hide the rule from an engine reading CSS 2's grammar */@media all{.t47b{color:red}}@media only all and (width>=100px){.t47c{color:red}}@media not all and (width>=100px){.t47d{color:red}}",
62+
"/*! webpack: an operand a condition already states, whichever rule states it */@media (monochrome){.t47e{color:red}}@supports (display:grid){.t47f{color:red}}@supports ((display:grid) or (display:flex)){.t47g{color:red}}",
63+
"/*! declined: a chain mixing the two operators is no flat list of either's, and
64+
two queries are not two operands of anything */@supports (display:grid) and (display:flex) or (display:table){.t47h{color:red}}@media (monochrome),(monochrome){.t47i{color:red}}",
5865
"/*! webpack: a container query */@container card (width>400px){.t48{color:red}}",
5966
"/*! webpack: an @supports condition */@supports (display:grid){.t49{color:red}}",
6067
"/*! webpack: ...on a custom property */@supports (--x:1){.t50{color:red}}",
@@ -196,6 +203,10 @@ Array [
196203
"/*! declined: ...but only then */.t96{transform:matrix3d(20,20,0,0,40,40,0,0,0,0,1,5,80,80,0,1)}",
197204
"/*! webpack: a 2D pair of 1 leaves the z scale alone */.t97{transform:scaleZ(1.5)}",
198205
"/*! webpack: a zero time keeps a unit, and \`s\` is the shorter one it can carry */.t98{transition-duration:0s}",
206+
"/*! webpack: a shorthand slot holding its own initial goes, and a slot taking a
207+
length or a color is read by the classes a sibling's own value is in — every
208+
slot holding its initial leaves the shortest of them saying all of them */.t98a{border:none}.t98b{column-rule:none}.t98c{border:solid red}",
209+
"/*! declined: a sibling filling the slot keeps the initial there */.t98d{border:medium 1px}.t98e{border:currentcolor red}",
199210
"/*! webpack: a component spelling the property's own initial says nothing beside
200211
another — omitting its group leaves exactly that keyword */.t99{grid-auto-flow:dense}",
201212
"/*! declined: the other alternative of the group is not the initial */.t99a{grid-auto-flow:column dense}",

‎test/configCases/css/minimize-values/style.css‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,22 @@
122122
@media (width >= 40rem) { .t46 { color: red } }
123123
/*! webpack: ...on both sides of the feature */
124124
@media (400px <= width <= 700px) { .t47 { color: red } }
125+
/*! webpack: `all and (…)` matches what the condition alone matches, so the type
126+
is six bytes saying nothing */
127+
@media all and (min-width: 100px) { .t47a { color: red } }
128+
/*! declined: `all` on its own is the whole query, and `only` before it is there
129+
to hide the rule from an engine reading CSS 2's grammar */
130+
@media all { .t47b { color: red } }
131+
@media only all and (min-width: 100px) { .t47c { color: red } }
132+
@media not all and (min-width: 100px) { .t47d { color: red } }
133+
/*! webpack: an operand a condition already states, whichever rule states it */
134+
@media (monochrome) and (monochrome) { .t47e { color: red } }
135+
@supports (display: grid) and (display: grid) { .t47f { color: red } }
136+
@supports ((display: grid) or (display: flex)) or ((display: grid) or (display: flex)) { .t47g { color: red } }
137+
/*! declined: a chain mixing the two operators is no flat list of either's, and
138+
two queries are not two operands of anything */
139+
@supports (display: grid) and (display: flex) or (display: table) { .t47h { color: red } }
140+
@media (monochrome), (monochrome) { .t47i { color: red } }
125141
/*! webpack: a container query */
126142
@container card (width > 400px) { .t48 { color: red } }
127143
/*! webpack: an @supports condition */
@@ -429,6 +445,15 @@
429445
.t97 { transform: scale3d(1, 1, 1.5) }
430446
/*! webpack: a zero time keeps a unit, and `s` is the shorter one it can carry */
431447
.t98 { transition-duration: 0ms }
448+
/*! webpack: a shorthand slot holding its own initial goes, and a slot taking a
449+
length or a color is read by the classes a sibling's own value is in — every
450+
slot holding its initial leaves the shortest of them saying all of them */
451+
.t98a { border: medium none currentcolor }
452+
.t98b { column-rule: medium none currentcolor }
453+
.t98c { border: medium solid red }
454+
/*! declined: a sibling filling the slot keeps the initial there */
455+
.t98d { border: medium 1px }
456+
.t98e { border: currentcolor red }
432457
/*! webpack: a component spelling the property's own initial says nothing beside
433458
another — omitting its group leaves exactly that keyword */
434459
.t99 { grid-auto-flow: row dense }

‎test/syntaxEquivalence.spectest.js‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1181,6 +1181,7 @@ describe("wpt css token adjacency", () => {
11811181
* @property {string[]=} schemes the color schemes to read it under
11821182
* @property {string[]=} directions the writing directions to read it under
11831183
* @property {string[]=} differs properties this lowering changes on purpose
1184+
* @property {string[]=} introduces custom properties the lowering writes that the source has none of, which a script enumerating them would see
11841185
* @property {string[]=} numeric properties whose value the rewrite reaches by arithmetic, held to `numericallyEqual` rather than to the same text
11851186
* @property {string[]} produces text the rewrite leaves, so a comparison of two sheets neither of which was rewritten cannot pass for one
11861187
* @property {string=} reference what the source means, where the engine reads no spelling of it — `:lang(en, fr)` is one Chromium has never taken, so the rewrite is held to the pair of rules that state the same thing rather than to an engine's reading of the original
@@ -1189,6 +1190,9 @@ describe("wpt css token adjacency", () => {
11891190
const LOWERING_FIXTURES = [
11901191
{
11911192
name: "light-dark()",
1193+
// The pair the lowering writes, which a script enumerating the computed
1194+
// custom properties would see where the source has none.
1195+
introduces: ["--webpack-light", "--webpack-dark"],
11921196
produces: [
11931197
"var(--webpack-light,#aaa) var(--webpack-dark,#444)",
11941198
":where(:root){--webpack-light:initial",
@@ -1211,6 +1215,7 @@ const LOWERING_FIXTURES = [
12111215
},
12121216
{
12131217
name: "light-dark() with no color-scheme, which is the light one",
1218+
introduces: ["--webpack-light", "--webpack-dark"],
12141219
produces: ["var(--webpack-light,#aaa)", ":where(:root){"],
12151220
css: "button{background-color:light-dark(#aaa,#444)}",
12161221
browsers: ["chrome 100"],
@@ -1579,6 +1584,53 @@ describe("a lowering computes as the spelling it replaces", () => {
15791584
}, probes)
15801585
);
15811586

1587+
/**
1588+
* Every computed property of every element a fixture probes, under one
1589+
* stylesheet — the whole of what the CSSOM reports rather than the handful of
1590+
* properties the fixture names, so a rewrite reaching a property nobody
1591+
* thought to probe is one this still sees.
1592+
* @param {import("puppeteer-core").Page} page the page to read from
1593+
* @param {string} css the stylesheet
1594+
* @param {string} html the document
1595+
* @param {string[]} selectors the elements to read
1596+
* @returns {Promise<string[]>} one `property:value` list per element
1597+
*/
1598+
const readEveryProperty = (page, css, html, selectors) =>
1599+
page.setContent(`<style>${css}</style>${html}`).then(() =>
1600+
page.evaluate((asked) => {
1601+
const canvas = document.createElement("canvas");
1602+
const context = /** @type {CanvasRenderingContext2D} */ (
1603+
canvas.getContext("2d", { willReadFrequently: true })
1604+
);
1605+
// A color is read as the color it is rather than as the text the
1606+
// engine serializes it to: `color(srgb .2 .4 .6)` and `#369` are one
1607+
// color written two ways, and which of them a computed value carries
1608+
// is what the rewrite is free to change.
1609+
const painted = (value) => {
1610+
context.fillStyle = "#010203";
1611+
context.fillStyle = value;
1612+
if (context.fillStyle === "#010203") return value;
1613+
context.clearRect(0, 0, 1, 1);
1614+
context.fillRect(0, 0, 1, 1);
1615+
return [...context.getImageData(0, 0, 1, 1).data].join(",");
1616+
};
1617+
return asked.map((selector) => {
1618+
const element = document.querySelector(selector);
1619+
if (element === null) return "no such element";
1620+
const style = getComputedStyle(element);
1621+
/** @type {string[]} */
1622+
const out = [];
1623+
for (let at = 0; at < style.length; at++) {
1624+
const property = style.item(at);
1625+
out.push(
1626+
`${property}:${painted(style.getPropertyValue(property))}`
1627+
);
1628+
}
1629+
return out.sort().join("\n");
1630+
});
1631+
}, selectors)
1632+
);
1633+
15821634
it.each(LOWERING_FIXTURES.map((fixture) => [fixture.name, fixture]))(
15831635
"%s",
15841636
async (_name, fixture) => {
@@ -1624,6 +1676,31 @@ describe("a lowering computes as the spelling it replaces", () => {
16241676
direction,
16251677
computed: before
16261678
});
1679+
// ...and the same elements, read whole: what the fixture names is
1680+
// where the rewrite is, and this is everything else the CSSOM
1681+
// says about them. A property the rewrite is allowed to move —
1682+
// or one it reaches by arithmetic — is left to the probes above.
1683+
const loose = new Set([
1684+
...(fixture.differs || []),
1685+
...(fixture.numeric || []),
1686+
...(fixture.introduces || [])
1687+
]);
1688+
const whole = [...new Set(asked.map(([selector]) => selector))];
1689+
const [wholeBefore, wholeAfter] = [
1690+
await readEveryProperty(
1691+
page,
1692+
fixture.reference || fixture.css,
1693+
html,
1694+
whole
1695+
),
1696+
await readEveryProperty(page, lowered, html, whole)
1697+
];
1698+
const kept = (/** @type {string} */ text) =>
1699+
text
1700+
.split("\n")
1701+
.filter((line) => !loose.has(line.slice(0, line.indexOf(":"))))
1702+
.join("\n");
1703+
expect(wholeAfter.map(kept)).toEqual(wholeBefore.map(kept));
16271704
}
16281705
}
16291706
} finally {

0 commit comments

Comments
 (0)