Skip to content

Commit 7fbba29

Browse files
committed
Add tests
1 parent 4ec6e1f commit 7fbba29

3 files changed

Lines changed: 336 additions & 0 deletions

File tree

test/browser.js

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
// browser.test.js
2+
import http from 'node:http'
3+
import { spawn } from 'node:child_process'
4+
import { once } from 'node:events'
5+
import fs from 'node:fs/promises'
6+
7+
const PORT = 5173
8+
9+
function html(mode = '') {
10+
const forceFallback = mode === 'fallback'
11+
12+
return /* html */ `
13+
<!doctype html>
14+
<html>
15+
<body>
16+
<script>
17+
${
18+
forceFallback
19+
? `
20+
delete globalThis.CompressionStream
21+
delete globalThis.DecompressionStream
22+
delete Uint8Array.prototype.toBase64
23+
delete Uint8Array.fromBase64
24+
`
25+
: ''
26+
}
27+
</script>
28+
29+
<script type="module">
30+
import { zipurl, unzipurl } from './index.js?mode=${mode}'
31+
32+
const results = []
33+
34+
async function test(name, fn) {
35+
try {
36+
await fn()
37+
results.push({ name, ok: true })
38+
} catch (err) {
39+
results.push({ name, ok: false, error: err?.message || String(err) })
40+
}
41+
}
42+
43+
// --- tests ---
44+
45+
await test('basic', async () => {
46+
const input = 'hello world'
47+
const out = await unzipurl(await zipurl(input))
48+
if (out !== input) throw new Error('mismatch')
49+
})
50+
51+
await test('unicode', async () => {
52+
const input = '你好 🌏🚀 café üñîçødê'
53+
const out = await unzipurl(await zipurl(input))
54+
if (out !== input) throw new Error('mismatch')
55+
})
56+
57+
await test('empty', async () => {
58+
const input = ''
59+
const out = await unzipurl(await zipurl(input))
60+
if (out !== input) throw new Error('mismatch')
61+
})
62+
63+
await test('long string', async () => {
64+
const input = 'abc123 '.repeat(10000)
65+
const out = await unzipurl(await zipurl(input))
66+
if (out !== input) throw new Error('mismatch')
67+
})
68+
69+
await test('url-safe', async () => {
70+
const z = await zipurl('test + / =')
71+
if (!/^[A-Za-z0-9\\-_]+$/.test(z)) {
72+
throw new Error('not url safe')
73+
}
74+
})
75+
76+
await test('compression effectiveness', async () => {
77+
const input = 'aaaaaaabbbbbbbcccccccddddddd'.repeat(100)
78+
const z = await zipurl(input)
79+
if (z.length >= input.length) {
80+
throw new Error('not compressed')
81+
}
82+
})
83+
84+
await test('invalid input throws', async () => {
85+
let threw = false
86+
try {
87+
await unzipurl('invalid!!base64')
88+
} catch {
89+
threw = true
90+
}
91+
if (!threw) throw new Error('did not throw')
92+
})
93+
94+
await test('multiple roundtrips (stability)', async () => {
95+
for (let i = 0; i < 10; i++) {
96+
const input = 'loop-' + i + '-' + Math.random()
97+
const out = await unzipurl(await zipurl(input))
98+
if (out !== input) throw new Error('mismatch at ' + i)
99+
}
100+
})
101+
102+
// --- report ---
103+
104+
await fetch('/report?mode=${mode}', {
105+
method: 'POST',
106+
headers: { 'content-type': 'application/json' },
107+
body: JSON.stringify(results)
108+
})
109+
110+
window.close()
111+
</script>
112+
</body>
113+
</html>
114+
`
115+
}
116+
117+
let phase = 'native'
118+
let failures = 0
119+
120+
const server = http.createServer(async (req, res) => {
121+
const url = new URL(req.url || '/', `http://localhost:${PORT}`)
122+
123+
if (url.pathname === '/') {
124+
res.setHeader('content-type', 'text/html')
125+
res.end(html(phase))
126+
return
127+
}
128+
129+
if (url.pathname === '/index.js') {
130+
const code = await fs.readFile(new URL('../index.js', import.meta.url))
131+
res.setHeader('content-type', 'application/javascript')
132+
res.end(code)
133+
return
134+
}
135+
136+
if (url.pathname === '/report' && req.method === 'POST') {
137+
let body = ''
138+
for await (const chunk of req) body += chunk
139+
140+
const results = JSON.parse(body)
141+
const mode = url.searchParams.get('mode')
142+
143+
console.log(`\nBrowser tests (${mode}):\n`)
144+
145+
let failed = 0
146+
for (const r of results) {
147+
if (r.ok) {
148+
console.log('✓', r.name)
149+
} else {
150+
failed++
151+
console.error('✗', r.name, '-', r.error)
152+
}
153+
}
154+
155+
failures += failed
156+
res.end('ok')
157+
158+
if (phase === 'native') {
159+
phase = 'fallback'
160+
openBrowser()
161+
} else {
162+
server.close()
163+
setTimeout(() => process.exit(failures ? 1 : 0), 200)
164+
}
165+
166+
return
167+
}
168+
169+
res.statusCode = 404
170+
res.end()
171+
})
172+
173+
await new Promise((resolve) => server.listen(PORT, /** @type {any} */ (resolve)))
174+
175+
function openBrowser() {
176+
const url = `http://localhost:${PORT}`
177+
178+
const cmd =
179+
process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open'
180+
181+
const args = process.platform === 'win32' ? ['/c', 'start', url] : [url]
182+
183+
spawn(cmd, args, { stdio: 'ignore', detached: true })
184+
}
185+
186+
console.log('Running browser tests (native + fallback)...')
187+
openBrowser()
188+
189+
await once(server, 'close')

test/cli.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import test from 'node:test'
2+
import assert from 'node:assert/strict'
3+
import { spawn } from 'node:child_process'
4+
import { once } from 'node:events'
5+
6+
/**
7+
* @typedef {Object} RunOptions
8+
* @property {string[]} [args]
9+
* @property {string} [input]
10+
*/
11+
12+
/**
13+
* @typedef {Object} RunResult
14+
* @property {number} code
15+
* @property {string} stdout
16+
* @property {string} stderr
17+
*/
18+
19+
/**
20+
* Run a CLI file with optional args or stdin input.
21+
*
22+
* @param {string} file
23+
* @param {RunOptions} [options]
24+
* @returns {Promise<RunResult>}
25+
*/
26+
const run = async (file, options = {}) => {
27+
const { args = [], input = '' } = options
28+
29+
const ps = spawn(process.execPath, [file, ...args], {
30+
stdio: ['pipe', 'pipe', 'pipe']
31+
})
32+
33+
/** @type {string} */
34+
let stdout = ''
35+
36+
/** @type {string} */
37+
let stderr = ''
38+
39+
ps.stdout.setEncoding('utf8')
40+
ps.stderr.setEncoding('utf8')
41+
42+
ps.stdout.on('data', (/** @type {string} */ d) => {
43+
stdout += d
44+
})
45+
46+
ps.stderr.on('data', (/** @type {string} */ d) => {
47+
stderr += d
48+
})
49+
50+
if (input) ps.stdin.write(input)
51+
ps.stdin.end()
52+
53+
const [code] = await once(ps, 'close')
54+
55+
return {
56+
code: /** @type {number} */ (code),
57+
stdout: stdout.trim(),
58+
stderr: stderr.trim()
59+
}
60+
}
61+
62+
const ZIP = new URL('../bin/zipurl.js', import.meta.url).pathname
63+
const UNZIP = new URL('../bin/unzipurl.js', import.meta.url).pathname
64+
65+
console.log(`\nCLI tests:\n`)
66+
67+
test('CLI (argv): zip -> unzip roundtrip', async () => {
68+
const url = 'https://example.com/hello?x=1'
69+
70+
const zipRes = await run(ZIP, { args: [url] })
71+
assert.equal(zipRes.code, 0)
72+
assert.ok(zipRes.stdout.length > 0)
73+
74+
const unzipRes = await run(UNZIP, { args: [zipRes.stdout] })
75+
assert.equal(unzipRes.code, 0)
76+
assert.equal(unzipRes.stdout, url)
77+
})
78+
79+
test('CLI (stdin): zip -> unzip roundtrip', async () => {
80+
const url = 'https://example.com/hello?x=1'
81+
82+
const zipRes = await run(ZIP, { input: url })
83+
assert.equal(zipRes.code, 0)
84+
85+
const unzipRes = await run(UNZIP, { input: zipRes.stdout })
86+
assert.equal(unzipRes.stdout, url)
87+
})
88+
89+
test('CLI: empty input should fail', async () => {
90+
const res = await run(ZIP)
91+
assert.equal(res.code, 1)
92+
assert.match(res.stderr, /No arguments/)
93+
})

test/node.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import test from 'node:test'
2+
import assert from 'node:assert/strict'
3+
import { zipurl, unzipurl } from '../index.js'
4+
5+
console.log(`\nNode.js tests:\n`)
6+
7+
test('roundtrip: basic string', async () => {
8+
const input = 'hello world'
9+
const zipped = await zipurl(input)
10+
const output = await unzipurl(zipped)
11+
assert.equal(output, input)
12+
})
13+
14+
test('roundtrip: unicode', async () => {
15+
const input = '你好 🌏🚀 café üñîçødê'
16+
const zipped = await zipurl(input)
17+
const output = await unzipurl(zipped)
18+
assert.equal(output, input)
19+
})
20+
21+
test('roundtrip: empty string', async () => {
22+
const input = ''
23+
const zipped = await zipurl(input)
24+
const output = await unzipurl(zipped)
25+
assert.equal(output, input)
26+
})
27+
28+
test('roundtrip: long string', async () => {
29+
const input = 'abc123 '.repeat(10_000)
30+
const zipped = await zipurl(input)
31+
const output = await unzipurl(zipped)
32+
assert.equal(output, input)
33+
})
34+
35+
test('output is url-safe', async () => {
36+
const input = 'test url safe + / = check'
37+
const zipped = await zipurl(input)
38+
39+
assert.match(zipped, /^[A-Za-z0-9\-_]+$/)
40+
})
41+
42+
test('compression actually reduces size (typical case)', async () => {
43+
const input = 'aaaaaaabbbbbbbcccccccddddddd'.repeat(100)
44+
const zipped = await zipurl(input)
45+
46+
// base64 expands ~33%, but gzip should still win here
47+
assert.ok(zipped.length < input.length)
48+
})
49+
50+
test('invalid input throws', async () => {
51+
await assert.rejects(() => unzipurl('invalid!!base64'), {
52+
name: /Error|TypeError/
53+
})
54+
})

0 commit comments

Comments
 (0)