// Copyright (C) 2026 Doan Luu Duc Tai // // 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 . import assert from 'node:assert/strict'; import { createServer } 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 } from '../../../.tools/browser/node_modules/playwright/index.mjs'; 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; let configuration; let configReady; let signalConfig; let origin; const errors = []; 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; signalConfig(); return; } const assets = { '/':'crates/chat-server/web/index.html','/app.js':'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(done => server.listen(0,'127.0.0.1',done)); origin = `http://127.0.0.1:${server.address().port}`; let browser; let context; 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) { 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,part) { await page.waitForFunction(value => document.getElementById('status').textContent.includes(value),part); return page.locator('#status').textContent(); } async function complete(page,offset) { transfer.end(bytes.subarray(offset)); await waitStatus(page,'Connecting to chat'); await configReady; configuration.setHeader('Content-Type','application/json'); configuration.end(JSON.stringify({origin,owner:''})); 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); 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) { const state = JSON.parse(await readFile(join(profile,'extensions.json'),'utf8')); assert.ok(state.addons.find(addon => addon.id === 'jid1-KtlZuoiikVfFew@jetpack')?.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}); }