Skip to content

Commit 0f45dc8

Browse files
committed
test: add TextEncoding suites for TextEncoder/TextDecoder and atob/btoa
Every suite feature-detects its globals and reports pending when they are absent, so runtimes without the encodings API keep passing unchanged. The utf-16le, utf-16be and windows-1252 sub-suites additionally probe the constructor for support, since encoding coverage differs per runtime.
1 parent 0baab7c commit 0f45dc8

4 files changed

Lines changed: 960 additions & 0 deletions

File tree

TextEncoding/Base64.js

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
// Suite for the WHATWG base64 globals, atob() and btoa().
2+
//
3+
// The suite gates itself on the API being present, so it can sit in
4+
// runAllTests() on every runtime and report a visible pending spec where the
5+
// base64 globals do not exist yet, rather than being wired in per-runtime.
6+
//
7+
// A runtime that DOES implement them must keep an unguarded canary in its own
8+
// suite asserting the globals are there (on iOS:
9+
// TestRunner/app/tests/RuntimeImplementedAPIs.js). Without one, this gate would
10+
// quietly turn a regression that removed the API into a skipped suite.
11+
//
12+
// Failures are asserted by `.name === "InvalidCharacterError"` rather than by
13+
// `instanceof DOMException`: runtimes without a DOMException throw a plain
14+
// Error carrying that name.
15+
16+
var globalObject = typeof globalThis !== "undefined" ? globalThis : global;
17+
18+
if (typeof globalObject.atob === "undefined" || typeof globalObject.btoa === "undefined") {
19+
describe("base64 globals", function () {
20+
it("is skipped: this runtime does not implement atob/btoa", function () {
21+
pending();
22+
});
23+
});
24+
return;
25+
}
26+
27+
function captureThrown(fn) {
28+
try {
29+
fn();
30+
} catch (e) {
31+
return e;
32+
}
33+
return null;
34+
}
35+
36+
function expectThrowsNamed(fn, name) {
37+
var thrown = captureThrown(fn);
38+
expect(thrown).not.toBeNull();
39+
expect(thrown && thrown.name).toBe(name);
40+
return thrown;
41+
}
42+
43+
function expectInvalidCharacter(fn) {
44+
return expectThrowsNamed(fn, "InvalidCharacterError");
45+
}
46+
47+
function binaryString(list) {
48+
var text = "";
49+
for (var i = 0; i < list.length; i++) {
50+
text += String.fromCharCode(list[i]);
51+
}
52+
return text;
53+
}
54+
55+
function everyByte() {
56+
var list = [];
57+
for (var i = 0; i < 256; i++) {
58+
list.push(i);
59+
}
60+
return list;
61+
}
62+
63+
describe("btoa", function () {
64+
it("is a function on the global", function () {
65+
expect(typeof btoa).toBe("function");
66+
expect(btoa.name).toBe("btoa");
67+
expect(btoa.length).toBe(1);
68+
});
69+
70+
it("encodes the empty string as the empty string", function () {
71+
expect(btoa("")).toBe("");
72+
});
73+
74+
it("encodes hello", function () {
75+
expect(btoa("hello")).toBe("aGVsbG8=");
76+
});
77+
78+
it("pads each input length as the base64 alphabet requires", function () {
79+
var inputs = ["f", "fo", "foo", "foob", "fooba", "foobar"];
80+
var encoded = inputs.map(function (input) {
81+
return btoa(input);
82+
});
83+
84+
expect(encoded).toEqual(["Zg==", "Zm8=", "Zm9v", "Zm9vYg==", "Zm9vYmE=", "Zm9vYmFy"]);
85+
});
86+
87+
it("encodes code units across the whole byte range", function () {
88+
expect(btoa("\u0000")).toBe("AA==");
89+
expect(btoa("\u00FF")).toBe("/w==");
90+
expect(btoa("\u0000\u0000\u0000")).toBe("AAAA");
91+
expect(btoa("\u00FF\u00FF\u00FF")).toBe("////");
92+
// Covers the two alphabet entries that are not alphanumeric.
93+
expect(btoa("\u00FB\u00FF\u00BF")).toBe("+/+/");
94+
});
95+
96+
it("coerces its argument to a string", function () {
97+
expect(btoa(123)).toBe(btoa("123"));
98+
expect(btoa(true)).toBe(btoa("true"));
99+
expect(btoa(null)).toBe(btoa("null"));
100+
});
101+
102+
it("throws InvalidCharacterError for a code unit above U+00FF", function () {
103+
expectInvalidCharacter(function () {
104+
btoa("\u0100");
105+
});
106+
expectInvalidCharacter(function () {
107+
btoa("a\u20ACb");
108+
});
109+
expectInvalidCharacter(function () {
110+
btoa("\uD83D\uDE00");
111+
});
112+
});
113+
});
114+
115+
describe("atob", function () {
116+
it("is a function on the global", function () {
117+
expect(typeof atob).toBe("function");
118+
expect(atob.name).toBe("atob");
119+
expect(atob.length).toBe(1);
120+
});
121+
122+
it("decodes the empty string as the empty string", function () {
123+
expect(atob("")).toBe("");
124+
});
125+
126+
it("decodes hello", function () {
127+
expect(atob("aGVsbG8=")).toBe("hello");
128+
});
129+
130+
it("decodes each padded length", function () {
131+
var inputs = ["Zg==", "Zm8=", "Zm9v", "Zm9vYg==", "Zm9vYmE=", "Zm9vYmFy"];
132+
var decoded = inputs.map(function (input) {
133+
return atob(input);
134+
});
135+
136+
expect(decoded).toEqual(["f", "fo", "foo", "foob", "fooba", "foobar"]);
137+
});
138+
139+
it("returns one code unit per decoded byte", function () {
140+
var high = atob("/w==");
141+
expect(high.length).toBe(1);
142+
expect(high.charCodeAt(0)).toBe(0xFF);
143+
144+
var zero = atob("AA==");
145+
expect(zero.length).toBe(1);
146+
expect(zero.charCodeAt(0)).toBe(0);
147+
148+
expect(atob("+/+/")).toBe("\u00FB\u00FF\u00BF");
149+
});
150+
151+
it("ignores ASCII whitespace anywhere in the input", function () {
152+
expect(atob("aGV s\tbG8=")).toBe("hello");
153+
expect(atob(" aGVsbG8= ")).toBe("hello");
154+
expect(atob("aGVs\nbG8\r=")).toBe("hello");
155+
expect(atob("a\fGVsbG8=")).toBe("hello");
156+
});
157+
158+
it("accepts input with the padding left off", function () {
159+
expect(atob("aGVsbG8")).toBe("hello");
160+
expect(atob("Zg")).toBe("f");
161+
expect(atob("Zm8")).toBe("fo");
162+
});
163+
164+
it("throws InvalidCharacterError when the length is one past a multiple of four", function () {
165+
expectInvalidCharacter(function () {
166+
atob("a");
167+
});
168+
expectInvalidCharacter(function () {
169+
atob("aGVsb");
170+
});
171+
// Whitespace is removed before the length is checked, so this is the
172+
// same one-past case rather than a whitespace rejection.
173+
expectInvalidCharacter(function () {
174+
atob(" a ");
175+
});
176+
});
177+
178+
it("throws InvalidCharacterError for a character outside the alphabet", function () {
179+
expectInvalidCharacter(function () {
180+
atob("a*bc");
181+
});
182+
expectInvalidCharacter(function () {
183+
atob("a=bc");
184+
});
185+
expectInvalidCharacter(function () {
186+
atob("aGV\u00E9bG8=");
187+
});
188+
// The base64url alphabet is not accepted.
189+
expectInvalidCharacter(function () {
190+
atob("-_-_");
191+
});
192+
});
193+
});
194+
195+
describe("base64 round trips", function () {
196+
it("round-trips text through btoa and back", function () {
197+
var samples = ["", "a", "ab", "abc", "hello world", "\u0000\u0001\u0002", "\u00FF\u00FE\u00FD"];
198+
var roundTripped = samples.map(function (sample) {
199+
return atob(btoa(sample));
200+
});
201+
202+
expect(roundTripped).toEqual(samples);
203+
});
204+
205+
it("round-trips a binary string covering every byte value", function () {
206+
var source = binaryString(everyByte());
207+
var decoded = atob(btoa(source));
208+
209+
expect(decoded.length).toBe(256);
210+
expect(decoded).toBe(source);
211+
});
212+
213+
it("round-trips every input length across one four-byte group", function () {
214+
var lengths = [1, 2, 3, 4, 5];
215+
var roundTripped = lengths.map(function (length) {
216+
var source = binaryString(everyByte().slice(0, length));
217+
return atob(btoa(source)) === source;
218+
});
219+
220+
expect(roundTripped).toEqual([true, true, true, true, true]);
221+
});
222+
});

0 commit comments

Comments
 (0)