summaryrefslogtreecommitdiff
path: root/crates/chat-server/tests/progress.ts
diff options
context:
space:
mode:
authorDoan Luu Duc Tai <doan@luuduct.ai>2026-10-31 12:25:30 -0500
committerDoan Luu Duc Tai <doan@luuduct.ai>2026-10-31 12:25:30 -0500
commit9139398d465f5899cc31bc4cdd54daa7392ca550 (patch)
treedebf0b15a969e8c64e3b3597f4eb7953b7f047e1 /crates/chat-server/tests/progress.ts
parentef61fc5aca6eca613a055c507a060b70bd537a37 (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.ts127
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});
+}