Skip to content

Commit 43bcfbd

Browse files
test(css): detect prefix windows the compat data discards
The generator keeps a prefixed spelling only while it precedes the unprefixed one, so a browser that lost unprefixed support at an engine change gets no window at all. Opera 15 became Chromium 28 and needed `-webkit-` again after Presto 12.1 had shipped these unprefixed; the test lists those 22 spellings so the gap is a decision rather than an oversight, and fails if it closes or a second browser joins it.
1 parent 836722c commit 43bcfbd

1 file changed

Lines changed: 183 additions & 0 deletions

File tree

‎test/CssPrefixWindows.unittest.js‎

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
"use strict";
2+
3+
/** @typedef {{ version_added?: string | boolean | null, version_removed?: string | boolean | null, prefix?: string, alternative_name?: string, flags?: EXPECTED_ANY[] }} BcdSupport */
4+
/** @typedef {{ __compat?: { support: { [browser: string]: BcdSupport | BcdSupport[] } } }} BcdNode */
5+
6+
const bcd =
7+
/** @type {{ css: { properties: { [name: string]: BcdNode } } }} */ (
8+
/** @type {unknown} */ (require("@mdn/browser-compat-data"))
9+
);
10+
const { PREFIXED_PROPERTIES, PREFIX_BROWSERS } = require("../lib/css/data");
11+
12+
/**
13+
* The browserslist name each BCD key folds onto, for the browsers the prefix
14+
* tables carry windows for.
15+
* @type {Map<string, string[]>}
16+
*/
17+
const BCD_TO_BROWSERSLIST = new Map([
18+
["chrome", ["chrome"]],
19+
["chrome_android", ["and_chr"]],
20+
["edge", ["edge"]],
21+
["firefox", ["firefox"]],
22+
["firefox_android", ["and_ff"]],
23+
["ie", ["ie", "ie_mob"]],
24+
["opera", ["opera"]],
25+
["opera_android", ["op_mob"]],
26+
["safari", ["safari"]],
27+
["safari_ios", ["ios_saf"]],
28+
["samsunginternet_android", ["samsung"]],
29+
["webview_android", ["android"]]
30+
]);
31+
32+
// The one browser whose engine change took its unprefixed support away again:
33+
// Presto 12.1 shipped these unprefixed, then Opera 15 became Chromium 28 and
34+
// needed `-webkit-`. Nothing in BCD marks the discontinuity.
35+
const ENGINE_SWITCHES = new Map([
36+
["opera", { at: 15, before: "-o-", after: "-webkit-" }]
37+
]);
38+
39+
/**
40+
* @param {string | boolean | null | undefined} version a BCD version
41+
* @returns {number | null} it as a number, or `null` when it says nothing
42+
*/
43+
const versionOf = (version) => {
44+
if (version === true) return 0;
45+
if (typeof version !== "string") return null;
46+
const parsed = Number.parseFloat(version.replace(/^≤/, ""));
47+
return Number.isNaN(parsed) ? null : parsed;
48+
};
49+
50+
/**
51+
* The generator keeps a prefixed spelling only while it precedes the unprefixed
52+
* one. This re-runs that decision to collect what it discarded, so a window the
53+
* shipped table is missing is visible rather than silent.
54+
* @returns {{ engineSwitch: string[], sameVersion: number, foreignAlias: number }} what the shipped tables omit, by cause
55+
*/
56+
const discardedWindows = () => {
57+
/** @type {string[]} */
58+
const engineSwitch = [];
59+
let sameVersion = 0;
60+
let foreignAlias = 0;
61+
62+
for (const [property, spellings] of PREFIXED_PROPERTIES) {
63+
const compat = bcd.css.properties[property];
64+
if (compat === undefined || compat.__compat === undefined) continue;
65+
/** @type {Map<string, Set<string>>} */
66+
const shipped = new Map();
67+
for (const [spelling, windows] of spellings) {
68+
shipped.set(spelling, new Set(windows.map(([browser]) => browser)));
69+
}
70+
71+
for (const [key, raw] of Object.entries(compat.__compat.support)) {
72+
const browsers = BCD_TO_BROWSERSLIST.get(key);
73+
if (browsers === undefined) continue;
74+
const entries = Array.isArray(raw) ? raw : [raw];
75+
76+
let unprefixedFrom = null;
77+
for (const entry of entries) {
78+
if (
79+
!entry ||
80+
entry.prefix ||
81+
entry.alternative_name ||
82+
entry.version_removed ||
83+
entry.flags
84+
) {
85+
continue;
86+
}
87+
const added = versionOf(entry.version_added);
88+
if (
89+
added !== null &&
90+
(unprefixedFrom === null || added < unprefixedFrom)
91+
) {
92+
unprefixedFrom = added;
93+
}
94+
}
95+
if (unprefixedFrom === null) continue;
96+
97+
for (const entry of entries) {
98+
if (!entry || entry.flags || !entry.prefix) continue;
99+
const prefixedFrom = versionOf(entry.version_added);
100+
if (prefixedFrom === null || prefixedFrom < unprefixedFrom) continue;
101+
const spelling = entry.prefix + property;
102+
103+
for (const browser of browsers) {
104+
const windows = shipped.get(spelling);
105+
if (windows !== undefined && windows.has(browser)) continue;
106+
const change = ENGINE_SWITCHES.get(browser);
107+
if (prefixedFrom === unprefixedFrom) {
108+
// BCD records one version for both spellings, so which the
109+
// engine actually needed is not recoverable from it.
110+
sameVersion++;
111+
} else if (
112+
change !== undefined &&
113+
unprefixedFrom < change.at &&
114+
prefixedFrom >= change.at &&
115+
entry.prefix === change.after
116+
) {
117+
engineSwitch.push(
118+
`${spelling} @ ${browser} (unprefixed ${unprefixedFrom}, prefixed again ${prefixedFrom})`
119+
);
120+
} else {
121+
// A prefix that was never this browser's own, adopted late for
122+
// web compatibility — Firefox reads `-webkit-` spellings it
123+
// never needed. Dropping those is right.
124+
foreignAlias++;
125+
}
126+
}
127+
}
128+
}
129+
}
130+
131+
engineSwitch.sort();
132+
return { engineSwitch, sameVersion, foreignAlias };
133+
};
134+
135+
describe("CSS prefix windows", () => {
136+
const discarded = discardedWindows();
137+
138+
it("maps every browser the prefix tables carry windows for", () => {
139+
const mapped = new Set();
140+
for (const names of BCD_TO_BROWSERSLIST.values()) {
141+
for (const name of names) mapped.add(name);
142+
}
143+
for (const browser of PREFIX_BROWSERS) {
144+
expect(mapped.has(browser)).toBe(true);
145+
}
146+
});
147+
148+
// Opera 15/16 read `-webkit-`, so a target of those versions is served only
149+
// the unprefixed spelling it cannot use. Narrow enough to leave, listed here
150+
// so it is a decision rather than an oversight — and so closing it, or a
151+
// second browser joining it, fails this test instead of passing unseen.
152+
it("has one known gap, and it is Opera's engine change", () => {
153+
expect(discarded.engineSwitch).toEqual([
154+
"-webkit-background-clip @ opera (unprefixed 10.5, prefixed again 15)",
155+
"-webkit-background-origin @ opera (unprefixed 10.5, prefixed again 15)",
156+
"-webkit-background-size @ opera (unprefixed 10, prefixed again 15)",
157+
"-webkit-border-bottom-left-radius @ opera (unprefixed 10.5, prefixed again 15)",
158+
"-webkit-border-bottom-right-radius @ opera (unprefixed 10.5, prefixed again 15)",
159+
"-webkit-border-top-left-radius @ opera (unprefixed 10.5, prefixed again 15)",
160+
"-webkit-border-top-right-radius @ opera (unprefixed 10.5, prefixed again 15)",
161+
"-webkit-column-rule @ opera (unprefixed 11.1, prefixed again 15)",
162+
"-webkit-column-rule-color @ opera (unprefixed 11.1, prefixed again 15)",
163+
"-webkit-column-rule-style @ opera (unprefixed 11.1, prefixed again 15)",
164+
"-webkit-column-rule-width @ opera (unprefixed 11.1, prefixed again 15)",
165+
"-webkit-column-width @ opera (unprefixed 11.1, prefixed again 15)",
166+
"-webkit-flex-basis @ opera (unprefixed 12.1, prefixed again 15)",
167+
"-webkit-flex-direction @ opera (unprefixed 12.1, prefixed again 15)",
168+
"-webkit-flex-flow @ opera (unprefixed 12.1, prefixed again 15)",
169+
"-webkit-flex-grow @ opera (unprefixed 12.1, prefixed again 15)",
170+
"-webkit-flex-shrink @ opera (unprefixed 12.1, prefixed again 15)",
171+
"-webkit-transition @ opera (unprefixed 12.1, prefixed again 15)",
172+
"-webkit-transition-delay @ opera (unprefixed 12.1, prefixed again 15)",
173+
"-webkit-transition-duration @ opera (unprefixed 12.1, prefixed again 15)",
174+
"-webkit-transition-property @ opera (unprefixed 12.1, prefixed again 15)",
175+
"-webkit-transition-timing-function @ opera (unprefixed 12.1, prefixed again 15)"
176+
]);
177+
});
178+
179+
it("discards the rest for a reason that is not an engine change", () => {
180+
expect(discarded.sameVersion).toBeGreaterThan(0);
181+
expect(discarded.foreignAlias).toBeGreaterThan(0);
182+
});
183+
});

0 commit comments

Comments
 (0)