Skip to content

Commit 439fee1

Browse files
committed
test: add whatwg cross-tests with browsers
1 parent 931a8b1 commit 439fee1

File tree

4 files changed

+33007
-6
lines changed

4 files changed

+33007
-6
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"test:spidermonkey": "exodus-test --engine=spidermonkey:bundle",
2828
"test:hermes": "exodus-test --engine=hermes:bundle",
2929
"test:quickjs": "exodus-test --engine=quickjs:bundle",
30-
"test:xs": "exodus-test --engine=xs:bundle",
30+
"test:xs": "EXODUS_TEST_IGNORE='tests/whatwg.browser.test.js' exodus-test --engine=xs:bundle",
3131
"test:engine262": "exodus-test --engine=engine262:bundle",
3232
"test:deno": "exodus-test --engine=deno:pure",
3333
"test:bun": "exodus-test --engine=bun:pure",

tests/whatwg.browser.test.js

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import '@exodus/bytes/encoding.js'
2+
import { percentEncodeAfterEncoding } from '@exodus/bytes/whatwg.js'
3+
import { keccakprg } from '@noble/hashes/sha3-addons.js'
4+
import { describe, test, before, after } from 'node:test'
5+
import { labels } from './encoding/fixtures/encodings.cjs'
6+
7+
// The test uses https:// URL query, which is special
8+
const specialquery = ' "#\'<>' // https://url.spec.whatwg.org/#special-query-percent-encode-set
9+
10+
const invalid = new Set(['replacement', 'utf-16le', 'utf-16be']) // https://encoding.spec.whatwg.org/#get-an-encoder
11+
12+
const { window, document } = globalThis
13+
14+
const range = (length, start) => Array.from({ length }, (_, i) => String.fromCodePoint(start + i))
15+
const strings = [
16+
...range(256, 0x20).filter((x) => x !== ' ' && x !== '#'), // we directly set to href
17+
...range(256, 0)
18+
.filter((x) => x !== '#' && x !== '\t' && x !== '\n' && x !== '\r')
19+
.map((x) => `${x}*`),
20+
...range(256, 0)
21+
.filter((x) => x !== '#' && x !== '\t' && x !== '\n' && x !== '\r')
22+
.map((x) => `*${x}*`),
23+
24+
String.fromCodePoint(0xfe_ff),
25+
String.fromCodePoint(0xff_fd),
26+
String.fromCodePoint(0xff_fe),
27+
String.fromCodePoint(0xff_ff),
28+
String.fromCodePoint(0x1_00_00),
29+
String.fromCodePoint(0x2_f8_a6), // max big5
30+
String.fromCodePoint(0x2_f8_a7),
31+
String.fromCodePoint(0x1_10_00),
32+
33+
String.fromCodePoint(42, 0x1_00_00, 0x1_10_00, 42),
34+
String.fromCodePoint(42, 0x1_00_00, 44, 0x1_10_00, 42),
35+
String.fromCodePoint(42, 0x1_00_00, 0x1_10_00, 42),
36+
String.fromCodePoint(42, 0x1_00_00, 44, 0x1_10_00, 42),
37+
38+
String.fromCharCode(0x20, 0x22, 0x3c, 0x3e, 0x60),
39+
String.fromCharCode(0x20, 0x22, 0x24, 0x3c, 0x3e),
40+
String.fromCharCode(0x3f, 0x5e, 0x60, 0x7b, 0x7d),
41+
String.fromCharCode(0x2f, 0x3a, 0x3b, 0x3d, 0x40, 0x5b, 0x5c, 0x5d, 0x7c),
42+
String.fromCharCode(0x24, 0x25, 0x26, 0x2b, 0x2c),
43+
String.fromCharCode(0x21, 0x27, 0x28, 0x29, 0x7e),
44+
45+
String.fromCharCode(0x61, 0x62, 0xd8_00, 0x77, 0x78),
46+
String.fromCharCode(0xd8_00, 0xd8_00),
47+
String.fromCharCode(0x61, 0x62, 0xdf_ff, 0x77, 0x78),
48+
String.fromCharCode(0xdf_ff, 0xd8_00),
49+
50+
range(0x2_00, 0x24).join(''), // from # + 1
51+
range(0x2_00, 0xf6_00).join(''), // user-defined
52+
range(0x2_00, 0xff_00).join(''),
53+
range(0x20_00, 0x24).join(''),
54+
range(0x20_00, 0xf0_00).join(''),
55+
range(0x20_00, 0xf_f0_00).join(''),
56+
'hello' + range(0x20_00, 0xf0_00).join('') + 'abc',
57+
]
58+
59+
const fixedPRG = keccakprg() // We don't add any entropy, so it spills out predicatable results
60+
for (let i = 1; i <= 32; i++) {
61+
const u8 = fixedPRG.randomBytes(1024)
62+
const u16 = new Uint16Array(u8.buffer, u8.byteOffset, u8.byteLength / 2)
63+
const u32 = new Uint32Array(u8.buffer, u8.byteOffset, u8.byteLength / 4)
64+
const chunk = [
65+
String.fromCharCode.apply(String, u8),
66+
String.fromCharCode.apply(String, u16),
67+
String.fromCodePoint(...u32.map((x) => x % 0x11_00_00)),
68+
].map(
69+
(x) =>
70+
x
71+
.trim()
72+
.replaceAll(/[\t\n\r#]/g, '')
73+
.replaceAll(/[\x00-\x20]+$/g, '') // eslint-disable-line no-control-regex
74+
)
75+
strings.push(...chunk)
76+
}
77+
78+
// Passes on Chromium, Servo. Webkit is incorrect. Firefox somewhy fails on CI only
79+
const skip =
80+
!document ||
81+
!window ||
82+
process.env.EXODUS_TEST_PLATFORM === 'webkit' ||
83+
process.env.EXODUS_TEST_PLATFORM === 'firefox'
84+
85+
describe('percent-encode after encoding matches browser', { skip }, () => {
86+
let handle
87+
const onmessage = (event) => handle(event.data)
88+
const iframe = document.createElement('iframe')
89+
90+
before(() => {
91+
window.addEventListener('message', onmessage)
92+
document.body.append(iframe)
93+
})
94+
95+
after(() => {
96+
window.removeEventListener('message', onmessage)
97+
iframe.remove()
98+
})
99+
100+
for (const encoding of labels) {
101+
if (invalid.has(encoding)) continue
102+
test(encoding, async (t) => {
103+
let ok = 0
104+
const loaded = new Promise((resolve) => (handle = resolve))
105+
const html = `
106+
<!DOCTYPE html>
107+
<script>
108+
var a = document.createElement('a');
109+
window.parent.postMessage('', '*');
110+
window.addEventListener('message', (e) => {
111+
a.href = 'https://example.com/?' + e.data
112+
window.parent.postMessage(a.search.slice(1), '*')
113+
})
114+
</script>`
115+
iframe.src = `data:text/html;charset=${encoding},${encodeURI(html)}`
116+
await loaded
117+
118+
for (const str of strings) {
119+
const promise = new Promise((resolve) => (handle = resolve))
120+
iframe.contentWindow.postMessage(str, '*')
121+
const actual = percentEncodeAfterEncoding(encoding, str, specialquery)
122+
t.assert.strictEqual(actual, await promise, `${encoding} #${ok + 1}`)
123+
ok++
124+
}
125+
126+
t.assert.strictEqual(ok, strings.length)
127+
})
128+
}
129+
})
130+
131+
// Ensures that behavior mathches everywhere with snapshots
132+
// Combined with the above check, we know that snapshots match reference browser platforms
133+
describe('percent-encode after encoding matches snapshot', () => {
134+
for (const encoding of labels) {
135+
if (invalid.has(encoding)) continue
136+
test(encoding, async (t) => {
137+
const res = []
138+
for (const str of strings) res.push(percentEncodeAfterEncoding(encoding, str, specialquery))
139+
if (t.assert.snapshot) {
140+
t.assert.snapshot(res)
141+
} else {
142+
t.skip('Snapshots are not supported')
143+
}
144+
})
145+
}
146+
})

0 commit comments

Comments
 (0)