@slashgear/gdpr-cookie-scanner 3.3.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,241 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { classifyButtonText } from "../../src/scanner/consent-modal.js";
3
+
4
+ // ── English ───────────────────────────────────────────────────────────────────
5
+
6
+ describe("classifyButtonText — en", () => {
7
+ it("accept", () => {
8
+ expect(classifyButtonText("Accept all", "en")).toBe("accept");
9
+ expect(classifyButtonText("Agree", "en")).toBe("accept");
10
+ expect(classifyButtonText("OK", "en")).toBe("accept");
11
+ expect(classifyButtonText("I accept", "en")).toBe("accept");
12
+ });
13
+ it("reject", () => {
14
+ expect(classifyButtonText("Reject all", "en")).toBe("reject");
15
+ expect(classifyButtonText("Decline", "en")).toBe("reject");
16
+ expect(classifyButtonText("No thanks", "en")).toBe("reject");
17
+ expect(classifyButtonText("Refuse", "en")).toBe("reject");
18
+ });
19
+ it("preferences", () => {
20
+ expect(classifyButtonText("Manage cookies", "en")).toBe("preferences");
21
+ expect(classifyButtonText("Customize", "en")).toBe("preferences");
22
+ expect(classifyButtonText("Settings", "en")).toBe("preferences");
23
+ });
24
+ it("close", () => {
25
+ expect(classifyButtonText("Close", "en")).toBe("close");
26
+ });
27
+ it("unknown", () => {
28
+ expect(classifyButtonText("Learn more", "en")).toBe("unknown");
29
+ });
30
+ });
31
+
32
+ // ── French ────────────────────────────────────────────────────────────────────
33
+
34
+ describe("classifyButtonText — fr", () => {
35
+ it("accept", () => {
36
+ expect(classifyButtonText("Tout accepter", "fr")).toBe("accept");
37
+ expect(classifyButtonText("J'accepte", "fr")).toBe("accept");
38
+ expect(classifyButtonText("Valider", "fr")).toBe("accept");
39
+ });
40
+ it("reject", () => {
41
+ expect(classifyButtonText("Tout refuser", "fr")).toBe("reject");
42
+ expect(classifyButtonText("Non merci", "fr")).toBe("reject");
43
+ expect(classifyButtonText("Refus", "fr")).toBe("reject");
44
+ expect(classifyButtonText("Tout rejeter", "fr")).toBe("reject");
45
+ });
46
+ it("preferences", () => {
47
+ expect(classifyButtonText("Paramètres", "fr")).toBe("preferences");
48
+ expect(classifyButtonText("Personnaliser", "fr")).toBe("preferences");
49
+ expect(classifyButtonText("Préférences", "fr")).toBe("preferences");
50
+ });
51
+ it("also recognises English labels on a French page (en fallback)", () => {
52
+ expect(classifyButtonText("Accept all", "fr")).toBe("accept");
53
+ expect(classifyButtonText("Reject all", "fr")).toBe("reject");
54
+ });
55
+ });
56
+
57
+ // ── German ────────────────────────────────────────────────────────────────────
58
+
59
+ describe("classifyButtonText — de", () => {
60
+ it("accept", () => {
61
+ expect(classifyButtonText("Alle akzeptieren", "de")).toBe("accept");
62
+ expect(classifyButtonText("Zustimmen", "de")).toBe("accept");
63
+ expect(classifyButtonText("Einverstanden", "de")).toBe("accept");
64
+ });
65
+ it("reject", () => {
66
+ expect(classifyButtonText("Ablehnen", "de")).toBe("reject");
67
+ expect(classifyButtonText("Alle ablehnen", "de")).toBe("reject");
68
+ expect(classifyButtonText("Nein danke", "de")).toBe("reject");
69
+ });
70
+ it("preferences", () => {
71
+ expect(classifyButtonText("Einstellungen", "de")).toBe("preferences");
72
+ expect(classifyButtonText("Anpassen", "de")).toBe("preferences");
73
+ expect(classifyButtonText("Mehr Optionen", "de")).toBe("preferences");
74
+ });
75
+ });
76
+
77
+ // ── Spanish ───────────────────────────────────────────────────────────────────
78
+
79
+ describe("classifyButtonText — es", () => {
80
+ it("accept", () => {
81
+ expect(classifyButtonText("Aceptar todo", "es")).toBe("accept");
82
+ expect(classifyButtonText("Aceptar", "es")).toBe("accept");
83
+ });
84
+ it("reject", () => {
85
+ expect(classifyButtonText("Rechazar todo", "es")).toBe("reject");
86
+ expect(classifyButtonText("Rechazar", "es")).toBe("reject");
87
+ expect(classifyButtonText("No gracias", "es")).toBe("reject");
88
+ });
89
+ it("preferences", () => {
90
+ expect(classifyButtonText("Configurar", "es")).toBe("preferences");
91
+ expect(classifyButtonText("Preferencias", "es")).toBe("preferences");
92
+ expect(classifyButtonText("Opciones", "es")).toBe("preferences");
93
+ });
94
+ });
95
+
96
+ // ── Italian ───────────────────────────────────────────────────────────────────
97
+
98
+ describe("classifyButtonText — it", () => {
99
+ it("accept", () => {
100
+ expect(classifyButtonText("Accetta tutto", "it")).toBe("accept");
101
+ expect(classifyButtonText("Accetta", "it")).toBe("accept");
102
+ expect(classifyButtonText("Acconsento", "it")).toBe("accept");
103
+ });
104
+ it("reject", () => {
105
+ expect(classifyButtonText("Rifiuta tutto", "it")).toBe("reject");
106
+ expect(classifyButtonText("Rifiuta", "it")).toBe("reject");
107
+ expect(classifyButtonText("No grazie", "it")).toBe("reject");
108
+ });
109
+ it("preferences", () => {
110
+ expect(classifyButtonText("Impostazioni", "it")).toBe("preferences");
111
+ expect(classifyButtonText("Gestisci", "it")).toBe("preferences");
112
+ expect(classifyButtonText("Preferenze", "it")).toBe("preferences");
113
+ });
114
+ });
115
+
116
+ // ── Dutch ─────────────────────────────────────────────────────────────────────
117
+
118
+ describe("classifyButtonText — nl", () => {
119
+ it("accept", () => {
120
+ expect(classifyButtonText("Alles accepteren", "nl")).toBe("accept");
121
+ expect(classifyButtonText("Akkoord", "nl")).toBe("accept");
122
+ });
123
+ it("reject", () => {
124
+ expect(classifyButtonText("Alles weigeren", "nl")).toBe("reject");
125
+ expect(classifyButtonText("Weigeren", "nl")).toBe("reject");
126
+ expect(classifyButtonText("Nee bedankt", "nl")).toBe("reject");
127
+ });
128
+ it("preferences", () => {
129
+ expect(classifyButtonText("Instellingen", "nl")).toBe("preferences");
130
+ expect(classifyButtonText("Voorkeuren", "nl")).toBe("preferences");
131
+ expect(classifyButtonText("Aanpassen", "nl")).toBe("preferences");
132
+ });
133
+ });
134
+
135
+ // ── Polish ────────────────────────────────────────────────────────────────────
136
+
137
+ describe("classifyButtonText — pl", () => {
138
+ it("accept", () => {
139
+ expect(classifyButtonText("Zaakceptuj wszystkie", "pl")).toBe("accept");
140
+ expect(classifyButtonText("Zaakceptuj", "pl")).toBe("accept");
141
+ expect(classifyButtonText("Zgadzam się", "pl")).toBe("accept");
142
+ });
143
+ it("reject", () => {
144
+ expect(classifyButtonText("Odrzuć wszystkie", "pl")).toBe("reject");
145
+ expect(classifyButtonText("Odrzuć", "pl")).toBe("reject");
146
+ expect(classifyButtonText("Nie zgadzam się", "pl")).toBe("reject");
147
+ });
148
+ it("preferences", () => {
149
+ expect(classifyButtonText("Ustawienia", "pl")).toBe("preferences");
150
+ expect(classifyButtonText("Dostosuj", "pl")).toBe("preferences");
151
+ expect(classifyButtonText("Preferencje", "pl")).toBe("preferences");
152
+ });
153
+ });
154
+
155
+ // ── Portuguese ────────────────────────────────────────────────────────────────
156
+
157
+ describe("classifyButtonText — pt", () => {
158
+ it("accept", () => {
159
+ expect(classifyButtonText("Aceitar tudo", "pt")).toBe("accept");
160
+ expect(classifyButtonText("Aceitar", "pt")).toBe("accept");
161
+ expect(classifyButtonText("Concordo", "pt")).toBe("accept");
162
+ });
163
+ it("reject", () => {
164
+ expect(classifyButtonText("Rejeitar tudo", "pt")).toBe("reject");
165
+ expect(classifyButtonText("Recusar", "pt")).toBe("reject");
166
+ expect(classifyButtonText("Não obrigado", "pt")).toBe("reject");
167
+ });
168
+ it("preferences", () => {
169
+ expect(classifyButtonText("Configurações", "pt")).toBe("preferences");
170
+ expect(classifyButtonText("Preferências", "pt")).toBe("preferences");
171
+ expect(classifyButtonText("Opções", "pt")).toBe("preferences");
172
+ });
173
+ });
174
+
175
+ // ── Unknown / missing language → all patterns tested ─────────────────────────
176
+
177
+ describe("classifyButtonText — null (unknown language)", () => {
178
+ it("still classifies common English labels", () => {
179
+ expect(classifyButtonText("Accept all", null)).toBe("accept");
180
+ expect(classifyButtonText("Reject all", null)).toBe("reject");
181
+ });
182
+ it("still classifies labels from any supported language", () => {
183
+ expect(classifyButtonText("Alle akzeptieren", null)).toBe("accept");
184
+ expect(classifyButtonText("Tout refuser", null)).toBe("reject");
185
+ expect(classifyButtonText("Rifiuta tutto", null)).toBe("reject");
186
+ expect(classifyButtonText("Odrzuć", null)).toBe("reject");
187
+ });
188
+ it("unknown language code falls back to all patterns", () => {
189
+ expect(classifyButtonText("Accept all", "ro")).toBe("accept");
190
+ expect(classifyButtonText("Alle akzeptieren", "ro")).toBe("accept");
191
+ });
192
+ });
193
+
194
+ // ── BCP 47 subtag normalisation (sanity) ─────────────────────────────────────
195
+
196
+ describe("classifyButtonText — full BCP 47 tags should be pre-normalised", () => {
197
+ // The caller (detectConsentModal) splits on "-" and lowercases before passing.
198
+ // These tests verify "de" (already normalised) matches correctly.
199
+ it("de matches German patterns", () => {
200
+ expect(classifyButtonText("Ablehnen", "de")).toBe("reject");
201
+ });
202
+ it("fr matches French patterns", () => {
203
+ expect(classifyButtonText("Tout accepter", "fr")).toBe("accept");
204
+ });
205
+ });
206
+
207
+ // ── Whitespace normalisation ──────────────────────────────────────────────────
208
+
209
+ describe("classifyButtonText — whitespace normalisation", () => {
210
+ it("collapses embedded newlines (common in CMP HTML templates)", () => {
211
+ expect(classifyButtonText("Tout\nrefuser", "fr")).toBe("reject");
212
+ expect(classifyButtonText("Accept\nall", "en")).toBe("accept");
213
+ });
214
+
215
+ it("collapses embedded tabs", () => {
216
+ expect(classifyButtonText("Reject\tall", "en")).toBe("reject");
217
+ expect(classifyButtonText("Tout\taccepter", "fr")).toBe("accept");
218
+ });
219
+
220
+ it("replaces non-breaking spaces (\\u00A0 /  ) with regular spaces", () => {
221
+ expect(classifyButtonText("Tout\u00A0refuser", "fr")).toBe("reject");
222
+ expect(classifyButtonText("Accept\u00A0all", "en")).toBe("accept");
223
+ expect(classifyButtonText("Alle\u00A0ablehnen", "de")).toBe("reject");
224
+ });
225
+
226
+ it("collapses multiple consecutive spaces", () => {
227
+ expect(classifyButtonText("Reject all", "en")).toBe("reject");
228
+ expect(classifyButtonText("Tout accepter", "fr")).toBe("accept");
229
+ });
230
+
231
+ it("strips leading and trailing whitespace", () => {
232
+ expect(classifyButtonText(" Accept all ", "en")).toBe("accept");
233
+ expect(classifyButtonText("\nTout refuser\n", "fr")).toBe("reject");
234
+ });
235
+
236
+ it("handles mixed whitespace characters in a single label", () => {
237
+ // Real-world CMP buttons sometimes have: icon +   + text + newline
238
+ expect(classifyButtonText("\n Tout\u00A0refuser\t", "fr")).toBe("reject");
239
+ expect(classifyButtonText(" Accept\n\tall ", "en")).toBe("accept");
240
+ });
241
+ });
@@ -0,0 +1,172 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ computeContrastRatio,
4
+ parseRgb,
5
+ relativeLuminance,
6
+ } from "../../src/scanner/consent-modal.js";
7
+
8
+ // ── parseRgb ──────────────────────────────────────────────────────────────────
9
+
10
+ describe("parseRgb", () => {
11
+ it("parses rgb() string", () => {
12
+ expect(parseRgb("rgb(255, 255, 255)")).toEqual([255, 255, 255]);
13
+ expect(parseRgb("rgb(0, 0, 0)")).toEqual([0, 0, 0]);
14
+ expect(parseRgb("rgb(100, 150, 200)")).toEqual([100, 150, 200]);
15
+ });
16
+
17
+ it("parses rgba() string, alpha channel is ignored", () => {
18
+ expect(parseRgb("rgba(0, 0, 0, 0.5)")).toEqual([0, 0, 0]);
19
+ expect(parseRgb("rgba(255, 255, 255, 1)")).toEqual([255, 255, 255]);
20
+ expect(parseRgb("rgba(100, 150, 200, 0.8)")).toEqual([100, 150, 200]);
21
+ });
22
+
23
+ it("parses rgba() with no spaces between values", () => {
24
+ expect(parseRgb("rgba(100,150,200,1)")).toEqual([100, 150, 200]);
25
+ expect(parseRgb("rgb(0,128,255)")).toEqual([0, 128, 255]);
26
+ });
27
+
28
+ it("parses fully transparent rgba(…,0) — alpha is ignored, RGB is still extracted", () => {
29
+ // Alpha-zero is treated the same as the opaque colour; callers bear responsibility
30
+ // for deciding whether to composite against a background.
31
+ expect(parseRgb("rgba(0, 0, 0, 0)")).toEqual([0, 0, 0]);
32
+ expect(parseRgb("rgba(255, 255, 255, 0)")).toEqual([255, 255, 255]);
33
+ });
34
+
35
+ it("returns null for named colours (not yet supported)", () => {
36
+ expect(parseRgb("white")).toBeNull();
37
+ expect(parseRgb("black")).toBeNull();
38
+ expect(parseRgb("red")).toBeNull();
39
+ expect(parseRgb("transparent")).toBeNull();
40
+ });
41
+
42
+ it("returns null for hex colours (not yet supported)", () => {
43
+ expect(parseRgb("#ffffff")).toBeNull();
44
+ expect(parseRgb("#000000")).toBeNull();
45
+ expect(parseRgb("#fff")).toBeNull();
46
+ expect(parseRgb("#000")).toBeNull();
47
+ });
48
+
49
+ it("returns null for empty string", () => {
50
+ expect(parseRgb("")).toBeNull();
51
+ });
52
+
53
+ it("returns null for arbitrary unrecognised strings", () => {
54
+ expect(parseRgb("hsl(0, 0%, 100%)")).toBeNull();
55
+ expect(parseRgb("oklch(1 0 0)")).toBeNull();
56
+ });
57
+ });
58
+
59
+ // ── relativeLuminance ─────────────────────────────────────────────────────────
60
+
61
+ describe("relativeLuminance", () => {
62
+ it("returns 0 for black", () => {
63
+ expect(relativeLuminance([0, 0, 0])).toBe(0);
64
+ });
65
+
66
+ it("returns 1 for white", () => {
67
+ expect(relativeLuminance([255, 255, 255])).toBe(1);
68
+ });
69
+
70
+ it("returns the WCAG luminance for primary colours", () => {
71
+ // WCAG 2.x: L = 0.2126·R + 0.7152·G + 0.0722·B (after linearisation)
72
+ expect(relativeLuminance([255, 0, 0])).toBeCloseTo(0.2126, 4);
73
+ expect(relativeLuminance([0, 255, 0])).toBeCloseTo(0.7152, 4);
74
+ expect(relativeLuminance([0, 0, 255])).toBeCloseTo(0.0722, 4);
75
+ });
76
+
77
+ it("uses the linear (÷12.92) branch for very dark channels (s ≤ 0.04045)", () => {
78
+ // rgb(10,10,10): s = 10/255 ≈ 0.0392 which is ≤ 0.04045 → linear = s/12.92
79
+ const lum = relativeLuminance([10, 10, 10]);
80
+ expect(lum).toBeGreaterThan(0);
81
+ expect(lum).toBeLessThan(0.01);
82
+ });
83
+
84
+ it("uses the gamma branch for mid-to-bright channels (s > 0.04045)", () => {
85
+ // rgb(128,128,128): s ≈ 0.502 → linear = ((s+0.055)/1.055)^2.4
86
+ const lum = relativeLuminance([128, 128, 128]);
87
+ expect(lum).toBeGreaterThan(0.2);
88
+ expect(lum).toBeLessThan(0.22);
89
+ });
90
+
91
+ it("luminance is symmetric — grey RGB channels give equal weights", () => {
92
+ // For equal R/G/B channels the contributions must sum correctly
93
+ const lum = relativeLuminance([200, 200, 200]);
94
+ const single = relativeLuminance([200, 0, 0]);
95
+ // luminance(200,200,200) ≈ luminance(200,0,0) × (0.2126+0.7152+0.0722)/0.2126
96
+ // i.e. the sum of channel weights = 1
97
+ expect(lum / single).toBeCloseTo(1 / 0.2126, 1);
98
+ });
99
+ });
100
+
101
+ // ── computeContrastRatio ──────────────────────────────────────────────────────
102
+
103
+ describe("computeContrastRatio", () => {
104
+ it("returns 21 for pure black on pure white (maximum contrast)", () => {
105
+ expect(computeContrastRatio("rgb(0, 0, 0)", "rgb(255, 255, 255)")).toBe(21);
106
+ });
107
+
108
+ it("is symmetric — order of fg/bg does not change the result", () => {
109
+ const fwb = computeContrastRatio("rgb(255, 255, 255)", "rgb(0, 0, 0)");
110
+ const bww = computeContrastRatio("rgb(0, 0, 0)", "rgb(255, 255, 255)");
111
+ expect(fwb).toBe(bww);
112
+ expect(fwb).toBe(21);
113
+ });
114
+
115
+ it("returns 1 for identical colours (minimum contrast)", () => {
116
+ expect(computeContrastRatio("rgb(255, 255, 255)", "rgb(255, 255, 255)")).toBe(1);
117
+ expect(computeContrastRatio("rgb(0, 0, 0)", "rgb(0, 0, 0)")).toBe(1);
118
+ expect(computeContrastRatio("rgb(128, 64, 32)", "rgb(128, 64, 32)")).toBe(1);
119
+ });
120
+
121
+ it("result is always ≥ 1 for any two parseable colours", () => {
122
+ const pairs: [string, string][] = [
123
+ ["rgb(255, 0, 0)", "rgb(0, 0, 255)"],
124
+ ["rgb(200, 200, 200)", "rgb(100, 100, 100)"],
125
+ ["rgb(50, 200, 50)", "rgb(200, 50, 200)"],
126
+ ];
127
+ for (const [fg, bg] of pairs) {
128
+ const r = computeContrastRatio(fg, bg);
129
+ expect(r).not.toBeNull();
130
+ expect(r!).toBeGreaterThanOrEqual(1);
131
+ }
132
+ });
133
+
134
+ it("result is always ≤ 21 (maximum possible WCAG contrast)", () => {
135
+ expect(computeContrastRatio("rgb(0, 0, 0)", "rgb(255, 255, 255)")).toBe(21);
136
+ });
137
+
138
+ it("result is rounded to at most 2 decimal places", () => {
139
+ const ratio = computeContrastRatio("rgb(100, 100, 100)", "rgb(200, 200, 200)");
140
+ expect(ratio).not.toBeNull();
141
+ // The value must equal its own 2-decimal rounded form
142
+ expect(ratio).toBe(parseFloat(ratio!.toFixed(2)));
143
+ });
144
+
145
+ it("returns null when fg colour is unparseable", () => {
146
+ expect(computeContrastRatio("white", "rgb(0, 0, 0)")).toBeNull();
147
+ expect(computeContrastRatio("#fff", "rgb(0, 0, 0)")).toBeNull();
148
+ expect(computeContrastRatio("transparent", "rgb(0, 0, 0)")).toBeNull();
149
+ });
150
+
151
+ it("returns null when bg colour is unparseable", () => {
152
+ expect(computeContrastRatio("rgb(0, 0, 0)", "black")).toBeNull();
153
+ expect(computeContrastRatio("rgb(0, 0, 0)", "#000000")).toBeNull();
154
+ });
155
+
156
+ it("returns null when both colours are unparseable", () => {
157
+ expect(computeContrastRatio("white", "black")).toBeNull();
158
+ expect(computeContrastRatio("#fff", "#000")).toBeNull();
159
+ expect(computeContrastRatio("transparent", "transparent")).toBeNull();
160
+ });
161
+
162
+ it("transparent rgba(…,0) is treated as its opaque counterpart — alpha is ignored", () => {
163
+ // This is a known limitation: parseRgb discards alpha.
164
+ // rgba(0,0,0,0) is parsed as black, so contrast with white equals 21.
165
+ expect(computeContrastRatio("rgba(0, 0, 0, 0)", "rgb(255, 255, 255)")).toBe(21);
166
+ });
167
+
168
+ it("accepts rgba() inputs for fg and bg", () => {
169
+ const ratio = computeContrastRatio("rgba(0, 0, 0, 1)", "rgba(255, 255, 255, 1)");
170
+ expect(ratio).toBe(21);
171
+ });
172
+ });