mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-07-06 05:23:56 -07:00
Playwright E2E suite (not working yet)
This commit is contained in:
parent
f534cd79b7
commit
68050b56bc
13 changed files with 425 additions and 16 deletions
48
webclient/e2e/helpers/seedLocalHost.ts
Normal file
48
webclient/e2e/helpers/seedLocalHost.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import type { Page } from '@playwright/test';
|
||||
import { E2E_WS_URL } from './servatrice';
|
||||
|
||||
// Points the running webclient at the local servatrice without touching any
|
||||
// webclient source. Strategy: navigate to the app once (which lets Dexie open
|
||||
// the Webatrice IndexedDB at schema v1 and seed DefaultHosts), then wipe the
|
||||
// `hosts` store and insert a single "Local E2E" row with lastSelected=true,
|
||||
// then reload so useKnownHosts re-reads. On a fresh Playwright context the
|
||||
// IDB is empty, so our seed is the only known host after reload — the Login
|
||||
// form auto-selects it and fires its test-connection against :4748.
|
||||
export async function seedLocalHostAndReload(page: Page, wsUrl: string = E2E_WS_URL): Promise<void> {
|
||||
const parsed = new URL(wsUrl);
|
||||
const host = parsed.hostname;
|
||||
const port = parsed.port || '4748';
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
await page.evaluate(async ({ host, port }) => {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const open = indexedDB.open('Webatrice');
|
||||
open.onerror = () => reject(open.error ?? new Error('failed to open Webatrice DB'));
|
||||
open.onsuccess = () => {
|
||||
const db = open.result;
|
||||
if (!db.objectStoreNames.contains('hosts')) {
|
||||
db.close();
|
||||
reject(new Error('hosts object store missing — did Dexie finish opening?'));
|
||||
return;
|
||||
}
|
||||
const tx = db.transaction('hosts', 'readwrite');
|
||||
const store = tx.objectStore('hosts');
|
||||
store.clear();
|
||||
store.add({
|
||||
name: 'Local E2E',
|
||||
host,
|
||||
port,
|
||||
localHost: host,
|
||||
localPort: port,
|
||||
editable: false,
|
||||
lastSelected: true,
|
||||
});
|
||||
tx.oncomplete = () => { db.close(); resolve(); };
|
||||
tx.onerror = () => { db.close(); reject(tx.error ?? new Error('hosts write failed')); };
|
||||
};
|
||||
});
|
||||
}, { host, port });
|
||||
|
||||
await page.reload();
|
||||
}
|
||||
37
webclient/e2e/helpers/servatrice.ts
Normal file
37
webclient/e2e/helpers/servatrice.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { WebSocket } from 'ws';
|
||||
|
||||
// The e2e docker-compose.yml publishes servatrice on localhost:4748.
|
||||
export const E2E_WS_URL = 'ws://localhost:4748';
|
||||
|
||||
// Opens a WS to servatrice and resolves once the server sends *anything* back
|
||||
// (ServerIdentification is the first frame). Used by global-setup to wait out
|
||||
// the `sleep 10` in the docker-compose entrypoint before any spec runs.
|
||||
export async function waitForServatriceReady(timeoutMs = 60_000): Promise<void> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
const ok = await tryConnect(E2E_WS_URL).catch(() => false);
|
||||
if (ok) {
|
||||
return;
|
||||
}
|
||||
await sleep(1000);
|
||||
}
|
||||
throw new Error(`servatrice did not become ready at ${E2E_WS_URL} within ${timeoutMs}ms`);
|
||||
}
|
||||
|
||||
function tryConnect(url: string): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ws = new WebSocket(url);
|
||||
const done = (value: boolean, err?: Error) => {
|
||||
try { ws.removeAllListeners(); ws.close(); } catch { /* ignore */ }
|
||||
err ? reject(err) : resolve(value);
|
||||
};
|
||||
ws.once('message', () => done(true));
|
||||
ws.once('error', (err) => done(false, err));
|
||||
ws.once('close', () => done(false));
|
||||
});
|
||||
}
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((r) => setTimeout(r, ms));
|
||||
}
|
||||
47
webclient/e2e/helpers/testUser.ts
Normal file
47
webclient/e2e/helpers/testUser.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { randomBytes } from 'node:crypto';
|
||||
import { expect, type Page } from '@playwright/test';
|
||||
|
||||
export interface TestUser {
|
||||
userName: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
// Servatrice [users] rules (see webclient/e2e/docker/servatrice-e2e.ini):
|
||||
// - 4-12 chars, lowercase/uppercase/numerics + "_.-", no leading punctuation
|
||||
// - "admin" is blacklisted
|
||||
// "e2e_" + 6 hex chars = 10 chars — fits comfortably and is unique per call.
|
||||
export function generateUniqueUser(): TestUser {
|
||||
return {
|
||||
userName: 'e2e_' + randomBytes(3).toString('hex'),
|
||||
password: 'TestPw123!',
|
||||
};
|
||||
}
|
||||
|
||||
export async function registerViaUi(page: Page, user: TestUser): Promise<void> {
|
||||
await page.getByRole('button', { name: /register/i }).first().click();
|
||||
|
||||
const dialog = page.getByRole('dialog');
|
||||
await expect(dialog).toBeVisible();
|
||||
|
||||
await dialog.locator('input[name="userName"]').fill(user.userName);
|
||||
await dialog.locator('input[name="password"]').fill(user.password);
|
||||
await dialog.locator('input[name="passwordConfirm"]').fill(user.password);
|
||||
|
||||
await dialog.getByRole('button', { name: /register/i }).click();
|
||||
|
||||
// With requireemailactivation=false the server returns RegistrationAccepted
|
||||
// and the dialog closes. If activation were required an AccountActivation
|
||||
// dialog would replace it — that's out of scope for this spec.
|
||||
await expect(dialog).toBeHidden();
|
||||
}
|
||||
|
||||
export async function loginViaUi(page: Page, user: TestUser): Promise<void> {
|
||||
await page.locator('input[name="userName"]').fill(user.userName);
|
||||
await page.locator('input[name="password"]').fill(user.password);
|
||||
|
||||
const loginButton = page.locator('button.loginForm-submit');
|
||||
// The Login button is gated on a successful test-connection (fired
|
||||
// automatically when the host loads). Wait it out rather than racing it.
|
||||
await expect(loginButton).toBeEnabled();
|
||||
await loginButton.click();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue