diff options
| author | Doan Luu Duc Tai <doan@luuduct.ai> | 2026-10-31 12:25:30 -0500 |
|---|---|---|
| committer | Doan Luu Duc Tai <doan@luuduct.ai> | 2026-10-31 12:25:30 -0500 |
| commit | 9139398d465f5899cc31bc4cdd54daa7392ca550 (patch) | |
| tree | debf0b15a969e8c64e3b3597f4eb7953b7f047e1 /crates/chat-server/tests/progress.ts | |
| parent | ef61fc5aca6eca613a055c507a060b70bd537a37 (diff) | |
Migrate browser code and tests to strict TypeScript
Diffstat (limited to 'crates/chat-server/tests/progress.ts')
| -rw-r--r-- | crates/chat-server/tests/progress.ts | 127 |
1 files changed, 127 insertions, 0 deletions
diff --git a/crates/chat-server/tests/progress.ts b/crates/chat-server/tests/progress.ts new file mode 100644 index 0000000..59c939d --- /dev/null +++ b/crates/chat-server/tests/progress.ts @@ -0,0 +1,127 @@ +// Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai> +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +import assert from 'node:assert/strict'; +import { createServer, type ServerResponse } from 'node:http'; +import { readFile, writeFile, mkdir, mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +import { chromium, firefox, type BrowserContext, type Page } from 'playwright'; +import { libreJSAddon, required, serverOrigin } from '../../../tests/browser-support.js'; + +const libre = process.env['CHAT_LIBREJS'] === '1'; +const profile = await mkdtemp(join(tmpdir(),'stack-chat-progress-')); +const bytes = await readFile(resolve('target/wasm32-wasip1/release/stack-chat-crypto.wasm')); +let mode = 'known'; +let transfer: ServerResponse | undefined; +let configuration: ServerResponse | undefined; +let configReady: Promise<void> | undefined; +let signalConfig: (() => void) | undefined; +let origin = ''; +const errors: string[] = []; +const server = createServer(async (request,response) => { + response.setHeader('Cache-Control','no-store'); + if (request.url === '/crypto.wasm') { + if (mode === 'failure') { response.writeHead(503); response.end(); return; } + response.setHeader('Content-Type','application/wasm'); + if (mode !== 'unknown') response.setHeader('Content-Length',bytes.length); + transfer = response; + response.write(bytes.subarray(0,1048576)); + return; + } + if (request.url === '/api') { configuration = response; required(signalConfig, 'Missing configuration signal')(); return; } + const assets: Record<string, string> = { '/':'crates/chat-server/web/index.html','/app.js':'var/build/typescript/crates/chat-server/web/app.js','/style.css':'crates/chat-server/web/style.css' }; + const path = assets[request.url || '']; + if (!path) { response.writeHead(404); response.end(); return; } + const mime = request.url === '/' ? 'text/html' : request.url?.endsWith('.js') ? 'text/javascript' : 'text/css'; + response.setHeader('Content-Type',mime); + let source = await readFile(resolve(path),'utf8'); + if (request.url === '/') source = source.replaceAll('{{SITE_ORIGIN}}',origin); + response.end(source); +}); +await new Promise<void>(done => server.listen(0,'127.0.0.1',done)); +origin = serverOrigin(server); +let browser; +let context: BrowserContext; +if (libre) { + await mkdir(join(profile,'extensions')); + const addon = await readFile(resolve('.tools/librejs.xpi')); + await writeFile(join(profile,'extensions','jid1-KtlZuoiikVfFew@jetpack.xpi'),addon); + context = await firefox.launchPersistentContext(profile,{headless:true,firefoxUserPrefs:{'extensions.autoDisableScopes':0,'extensions.enabledScopes':15,'extensions.startupScanScopes':15,'extensions.update.enabled':false}}); + browser = {close:() => context.close()}; +} else { + browser = await chromium.launch({headless:true,channel:'chromium'}); + context = await browser.newContext(); +} +async function open(selected: 'known' | 'unknown' | 'failure') { + mode = selected; + configuration = undefined; + configReady = new Promise(done => { signalConfig = done; }); + const page = await context.newPage(); + page.on('pageerror',error => errors.push(error.message)); + await page.clock.install(); + await page.goto(origin); + return page; +} +async function waitStatus(page: Page, part: string) { + await page.waitForFunction(value => document.getElementById('status')?.textContent?.includes(value),part); + return await page.locator('#status').textContent() || ''; +} +async function complete(page: Page, offset: number) { + required(transfer, 'Missing transfer').end(bytes.subarray(offset)); + await waitStatus(page,'Connecting to chat'); + await configReady; + required(configuration, 'Missing configuration').setHeader('Content-Type','application/json'); + required(configuration, 'Missing configuration').end(JSON.stringify({origin,owner:'',protocol:'stack-chat/v1'})); + await page.locator('#owner-choice').waitFor({state:'visible'}); + assert.equal(await page.locator('#status').textContent(),''); + assert.equal(await page.evaluate(async () => (await wasm).byteLength),bytes.length); + await page.close(); +} +try { + const known = await open('known'); + assert.match(await waitStatus(known,'1.0 /'),/\[#{3}-{17}\] 19%/); + await known.clock.fastForward(44000); + required(transfer, 'Missing transfer').write(bytes.subarray(1048576,2097152)); + assert.match(await waitStatus(known,'2.0 /'),/39%/); + await known.screenshot({path:resolve('.tools/chat-download-progress.png'),fullPage:true}); + await known.clock.fastForward(44000); + assert.ok(!(await known.locator('#status').textContent() || '').includes('stalled')); + await complete(known,2097152); + const unknown = await open('unknown'); + assert.equal(await waitStatus(unknown,'1.0 MiB received'),'Downloading cryptographic module... 1.0 MiB received'); + await complete(unknown,1048576); + const failure = await open('failure'); + await waitStatus(failure,'HTTP 503'); + assert.equal(await failure.locator('#owner-choice').isVisible(),false); + await failure.close(); + const stalled = await open('known'); + await waitStatus(stalled,'1.0 /'); + await stalled.clock.fastForward(45001); + await waitStatus(stalled,'stalled for 45 seconds'); + assert.match(await stalled.locator('#status').textContent() || '',/reload/); + assert.equal(await stalled.locator('#owner-choice').isVisible(),false); + await stalled.close(); + assert.deepEqual(errors,[]); + if (libre) { + assert.ok(libreJSAddon(await readFile(join(profile,'extensions.json'),'utf8')).active); + } + console.log(`PASS: ${libre ? 'Firefox/LibreJS' : 'Chromium'} streamed percentage/bar/MiB, unknown length, exact byte recovery, slow continuous transfer, idle timeout, HTTP failure, and connection phase.`); +} finally { + await browser.close(); + server.closeAllConnections(); + await new Promise(done => server.close(done)); + await rm(profile,{recursive:true,force:true}); +} |
