import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import { createServer } from 'node:http'; import process from 'node:process'; const { chromium } = await import( process.env.PLAYWRIGHT_MODULE ?? 'playwright' ); const actions = []; const server = createServer(async (request, response) => { const url = new URL(request.url, 'http://localhost'); if (url.pathname.startsWith('/action/')) { actions.push(url); response.writeHead(204).end(); return; } const file = url.pathname === '/' ? 'index.html' : url.pathname.slice(1); if ( ![ 'index.html', 'main.js', 'server.js', 'locales/en.js', 'styles.css', 'mark.svg', ].includes(file) ) { response.writeHead(404).end(); return; } response.setHeader( 'Content-Type', file.endsWith('.js') ? 'text/javascript' : file.endsWith('.css') ? 'text/css' : file.endsWith('.svg') ? 'image/svg+xml' : 'text/html', ); response.end(await readFile(new URL(`../ui/${file}`, import.meta.url))); }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); const browser = await chromium.launch({ channel: 'chrome', headless: true }); const page = await browser.newPage(); const state = (phase, ready = false, server = 'https://kanbangit.de/') => page.evaluate( ({ phase, ready, server }) => { window.updateLauncher( new URLSearchParams({ phase, ready: String(ready), server, }).toString(), ); }, { phase, ready, server }, ); const latestAction = async (name) => { await page.waitForTimeout(100); assert.equal(actions.at(-1)?.pathname, `/action/${name}`); }; try { await page.goto(`http://127.0.0.1:${server.address().port}/`); assert.equal(await page.locator('#workspace-settings').isVisible(), false); await state('connecting'); await page.waitForTimeout(500); assert.equal(await page.locator('#workspace-settings').isVisible(), false); assert.equal(await page.locator('#startup-loading').isVisible(), true); await page.reload(); await state('idle'); for (const width of [320, 390, 1440]) { await page.setViewportSize({ width, height: 900 }); for (const theme of ['light', 'dark', 'system']) { await page.getByLabel('Appearance').selectOption(theme); assert.equal( await page.evaluate( () => document.documentElement.scrollWidth <= innerWidth, ), true, ); assert.equal( await page .getByRole('button', { name: 'Open workspace', exact: true, }) .isVisible(), true, ); } } await page.getByLabel('Server address').fill('http://example.com/'); await page.getByLabel('Server address').press('Enter'); assert.equal( await page.getByLabel('Server address').getAttribute('aria-invalid'), 'true', ); await page.getByLabel('Server address').fill('https://first.example.com/'); await page.getByLabel('Server address').press('Enter'); await latestAction('open'); assert.equal( actions.at(-1).searchParams.get('server'), 'https://first.example.com/', ); await state('connecting', false, 'https://first.example.com/'); assert.equal(await page.locator('.open-workspace').isDisabled(), true); await state('connectionError', false, 'https://first.example.com/'); assert.equal( await page.getByLabel('Server address').inputValue(), 'https://first.example.com/', ); await page.getByRole('button', { name: 'Try again', exact: true }).click(); await latestAction('open'); await state('connected', true, 'https://first.example.com/'); await page .getByRole('button', { name: 'Return to workspace', exact: true }) .click(); await latestAction('resume'); await page .getByLabel('Server address') .fill('https://second.example.com/app'); await page .getByRole('button', { name: 'Open workspace', exact: true }) .click(); assert.equal( await page .getByRole('button', { name: 'Switch server', exact: true }) .isVisible(), true, ); await page.getByRole('button', { name: 'Cancel', exact: true }).click(); await latestAction('resume'); await page.getByLabel('Server address').press('Enter'); await page .getByRole('button', { name: 'Switch server', exact: true }) .click(); await latestAction('open'); assert.equal( actions.at(-1).searchParams.get('server'), 'https://second.example.com/app/', ); console.log( 'Launcher browser tests passed: responsive themes, keyboard, validation, offline retry, resume and confirmed switching.', ); } finally { await browser.close(); server.close(); }