test: add Playwright E2E tests for canvas mount, node navigation, transcription
Three spec files with beforeAll skip guards: - Skip if VITE_TEST_TEACHER_EMAIL/PASSWORD not set - Skip if dev server at 192.168.0.251:13000 unreachable Tests exit 0 (3 skipped) when env is unconfigured. Requires: live dev server + real teacher credentials to run live.
This commit is contained in:
parent
bf592886c6
commit
79e7d4df9c
18
playwright.config.ts
Normal file
18
playwright.config.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './src/__tests__/e2e',
|
||||||
|
testMatch: '**/*.spec.js',
|
||||||
|
timeout: 30000,
|
||||||
|
expect: { timeout: 5000 },
|
||||||
|
fullyParallel: false,
|
||||||
|
retries: 1,
|
||||||
|
use: {
|
||||||
|
baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://192.168.0.251:13000',
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
||||||
|
],
|
||||||
|
webServer: undefined,
|
||||||
|
});
|
||||||
47
src/__tests__/e2e/canvas-mount.spec.js
Normal file
47
src/__tests__/e2e/canvas-mount.spec.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { test, expect, request } from '@playwright/test';
|
||||||
|
|
||||||
|
const BASE = process.env.PLAYWRIGHT_BASE_URL || 'http://192.168.0.251:13000';
|
||||||
|
const TEST_EMAIL = process.env.VITE_TEST_TEACHER_EMAIL;
|
||||||
|
const TEST_PASSWORD = process.env.VITE_TEST_TEACHER_PASSWORD;
|
||||||
|
|
||||||
|
async function serverReachable() {
|
||||||
|
try {
|
||||||
|
const ctx = await request.newContext();
|
||||||
|
const res = await ctx.get(BASE, { timeout: 3000 }).catch(() => null);
|
||||||
|
await ctx.dispose();
|
||||||
|
return Boolean(res);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login(page) {
|
||||||
|
await page.goto(`${BASE}/login`);
|
||||||
|
await page.getByLabel('Email').fill(TEST_EMAIL);
|
||||||
|
await page.getByLabel('Password').fill(TEST_PASSWORD);
|
||||||
|
await page.getByRole('button', { name: 'Login' }).click();
|
||||||
|
await page.waitForURL((url) => /\/dashboard|\/node\//.test(url.pathname), { timeout: 15000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function skipIfUnconfigured() {
|
||||||
|
if (!TEST_EMAIL || !TEST_PASSWORD) test.skip(true, 'VITE_TEST_TEACHER_EMAIL / VITE_TEST_TEACHER_PASSWORD not set');
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe('Canvas mount', () => {
|
||||||
|
test.beforeAll(async () => {
|
||||||
|
skipIfUnconfigured();
|
||||||
|
const reachable = await serverReachable();
|
||||||
|
test.skip(!reachable, `Dev server not reachable at ${BASE}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('login and show lesson canvas within 5s', async ({ page }) => {
|
||||||
|
await login(page);
|
||||||
|
const canvasVisible = await page
|
||||||
|
.locator('[data-testid="tldraw-canvas"], .tl-container')
|
||||||
|
.waitFor({ timeout: 10000 })
|
||||||
|
.then(() => true)
|
||||||
|
.catch(() => false);
|
||||||
|
expect(canvasVisible).toBe(true);
|
||||||
|
await expect(page.locator('[class*="error-state"], [data-testid="error"]').first()).toBeHidden({ timeout: 2000 }).catch(() => {});
|
||||||
|
});
|
||||||
|
});
|
||||||
39
src/__tests__/e2e/canvas-mount.spec.ts
Normal file
39
src/__tests__/e2e/canvas-mount.spec.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
/// <reference types="@playwright/test" />
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
const BASE = process.env.PLAYWRIGHT_BASE_URL || 'http://192.168.0.251:13000';
|
||||||
|
|
||||||
|
test.describe('Canvas mount', () => {
|
||||||
|
test('login and show lesson canvas', async ({ page }) => {
|
||||||
|
test.setTimeout(60000);
|
||||||
|
await page.goto(`${BASE}/login`);
|
||||||
|
const email = process.env.VITE_TEST_TEACHER_EMAIL || 'teacher@test';
|
||||||
|
const password = process.env.VITE_TEST_TEACHER_PASSWORD || 'password';
|
||||||
|
|
||||||
|
await page.getByLabel('Email').fill(email);
|
||||||
|
await page.getByLabel('Password').fill(password);
|
||||||
|
await page.getByRole('button', { name: /sign in|log in/i }).click();
|
||||||
|
await page.waitForURL((url: URL) => /\/dashboard|\/node\//.test(url.pathname), { timeout: 12000 });
|
||||||
|
|
||||||
|
async function pingOk() {
|
||||||
|
try {
|
||||||
|
const r = await page.request.get(new URL('/api/health', BASE).toString(), { timeout: 4000 }).catch(
|
||||||
|
() => null as any
|
||||||
|
);
|
||||||
|
return Boolean(r && (r as any).ok());
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!(await pingOk())) test.skip(true, 'Dev server not reachable at ' + BASE);
|
||||||
|
|
||||||
|
const canvasVisible = page
|
||||||
|
.locator('[data-testid="tldraw-canvas"], .tl-container')
|
||||||
|
.waitFor({ timeout: 8000 })
|
||||||
|
.then(() => true)
|
||||||
|
.catch(() => false);
|
||||||
|
|
||||||
|
expect((await canvasVisible)).toBe(true);
|
||||||
|
await expect(page.getByText(/error|failed to load/i).first()).toBeHidden();
|
||||||
|
});
|
||||||
|
});
|
||||||
54
src/__tests__/e2e/node-navigation.spec.js
Normal file
54
src/__tests__/e2e/node-navigation.spec.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { test, expect, request } from '@playwright/test';
|
||||||
|
|
||||||
|
const BASE = process.env.PLAYWRIGHT_BASE_URL || 'http://192.168.0.251:13000';
|
||||||
|
const TEST_EMAIL = process.env.VITE_TEST_TEACHER_EMAIL;
|
||||||
|
const TEST_PASSWORD = process.env.VITE_TEST_TEACHER_PASSWORD;
|
||||||
|
|
||||||
|
async function serverReachable() {
|
||||||
|
try {
|
||||||
|
const ctx = await request.newContext();
|
||||||
|
const res = await ctx.get(BASE, { timeout: 3000 }).catch(() => null);
|
||||||
|
await ctx.dispose();
|
||||||
|
return Boolean(res);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login(page) {
|
||||||
|
await page.goto(`${BASE}/login`);
|
||||||
|
await page.getByLabel('Email').fill(TEST_EMAIL);
|
||||||
|
await page.getByLabel('Password').fill(TEST_PASSWORD);
|
||||||
|
await page.getByRole('button', { name: 'Login' }).click();
|
||||||
|
await page.waitForURL((url) => /\/dashboard|\/node\//.test(url.pathname), { timeout: 15000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function skipIfUnconfigured() {
|
||||||
|
if (!TEST_EMAIL || !TEST_PASSWORD) test.skip(true, 'VITE_TEST_TEACHER_EMAIL / VITE_TEST_TEACHER_PASSWORD not set');
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe('Node navigation', () => {
|
||||||
|
test.beforeAll(async () => {
|
||||||
|
skipIfUnconfigured();
|
||||||
|
const reachable = await serverReachable();
|
||||||
|
test.skip(!reachable, `Dev server not reachable at ${BASE}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('canvas remounts cleanly on node navigation', async ({ page }) => {
|
||||||
|
const errors = [];
|
||||||
|
page.on('pageerror', err => errors.push(err.message));
|
||||||
|
await login(page);
|
||||||
|
await page.locator('[data-testid="tldraw-canvas"], .tl-container').waitFor({ timeout: 10000 });
|
||||||
|
const navLinks = page.locator('nav a, [data-testid="nav-node"]');
|
||||||
|
const count = await navLinks.count();
|
||||||
|
if (count > 1) {
|
||||||
|
await navLinks.nth(1).click();
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
const hasCanvas = await page.locator('[data-testid="tldraw-canvas"], .tl-container')
|
||||||
|
.waitFor({ timeout: 8000 }).then(() => true).catch(() => false);
|
||||||
|
expect(hasCanvas).toBe(true);
|
||||||
|
}
|
||||||
|
const criticalErrors = errors.filter(e => /uncaught|undefined is not/i.test(e));
|
||||||
|
expect(criticalErrors).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
47
src/__tests__/e2e/node-navigation.spec.ts
Normal file
47
src/__tests__/e2e/node-navigation.spec.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
/// <reference types="@playwright/test" />
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
const BASE = process.env.PLAYWRIGHT_BASE_URL || 'http://192.168.0.251:13000';
|
||||||
|
|
||||||
|
test.describe('Node navigation', () => {
|
||||||
|
test('canvas remounts on node change', async ({ page }) => {
|
||||||
|
test.setTimeout(60000);
|
||||||
|
|
||||||
|
async function serverReachable() {
|
||||||
|
try {
|
||||||
|
const res = await page.request.get(new URL('/api/health', BASE).toString(), { timeout: 4000 }).catch(
|
||||||
|
() => null as any
|
||||||
|
);
|
||||||
|
return Boolean(res && (res as any).ok());
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!(await serverReachable())) test.skip(true, 'Dev server not reachable at ' + BASE);
|
||||||
|
|
||||||
|
const consoleMessages: string[] = [];
|
||||||
|
page.on('console', (msg: any) => consoleMessages.push((msg.text() as string) || ''));
|
||||||
|
page.on('pageerror', (err: any) => consoleMessages.push('pageerror: ' + ((err.message as string) || 'unknown')));
|
||||||
|
|
||||||
|
await page.goto(`${BASE}/login`);
|
||||||
|
const email = process.env.VITE_TEST_TEACHER_EMAIL || 'teacher@test';
|
||||||
|
const password = process.env.VITE_TEST_TEACHER_PASSWORD || 'password';
|
||||||
|
await page.getByLabel('Email').fill(email);
|
||||||
|
await page.getByLabel('Password').fill(password);
|
||||||
|
await page.getByRole('button', { name: /sign in|log in/i }).click();
|
||||||
|
await page.waitForURL((url: URL) => /\/dashboard|\/node\//.test(url.pathname), { timeout: 12000 });
|
||||||
|
|
||||||
|
const canvas = page.locator('[data-testid="tldraw-canvas"], .tl-container');
|
||||||
|
if ((await canvas.count()) === 0) await canvas.first().waitFor({ timeout: 8000 });
|
||||||
|
await canvas.first().waitFor({ state: 'visible', timeout: 8000 });
|
||||||
|
|
||||||
|
const nodes = page.locator('a[href*="/node/"]').first();
|
||||||
|
if ((await nodes.count()) === 0) test.skip(true, 'No node navigation links found');
|
||||||
|
await nodes.click();
|
||||||
|
await page.waitForURL((url: URL) => /\/node\/\d+/.test(url.pathname), { timeout: 12000 });
|
||||||
|
|
||||||
|
await expect(canvas.first()).toBeVisible();
|
||||||
|
const errorTexts = consoleMessages.some((m) => /error|failed/i.test(m));
|
||||||
|
expect(errorTexts).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
54
src/__tests__/e2e/transcription-toggle.spec.js
Normal file
54
src/__tests__/e2e/transcription-toggle.spec.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { test, expect, request } from '@playwright/test';
|
||||||
|
|
||||||
|
const BASE = process.env.PLAYWRIGHT_BASE_URL || 'http://192.168.0.251:13000';
|
||||||
|
const TEST_EMAIL = process.env.VITE_TEST_TEACHER_EMAIL;
|
||||||
|
const TEST_PASSWORD = process.env.VITE_TEST_TEACHER_PASSWORD;
|
||||||
|
|
||||||
|
async function serverReachable() {
|
||||||
|
try {
|
||||||
|
const ctx = await request.newContext();
|
||||||
|
const res = await ctx.get(BASE, { timeout: 3000 }).catch(() => null);
|
||||||
|
await ctx.dispose();
|
||||||
|
return Boolean(res);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login(page) {
|
||||||
|
await page.goto(`${BASE}/login`);
|
||||||
|
await page.getByLabel('Email').fill(TEST_EMAIL);
|
||||||
|
await page.getByLabel('Password').fill(TEST_PASSWORD);
|
||||||
|
await page.getByRole('button', { name: 'Login' }).click();
|
||||||
|
await page.waitForURL((url) => /\/dashboard|\/node\//.test(url.pathname), { timeout: 15000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function skipIfUnconfigured() {
|
||||||
|
if (!TEST_EMAIL || !TEST_PASSWORD) test.skip(true, 'VITE_TEST_TEACHER_EMAIL / VITE_TEST_TEACHER_PASSWORD not set');
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe('Transcription toggle', () => {
|
||||||
|
test.beforeAll(async () => {
|
||||||
|
skipIfUnconfigured();
|
||||||
|
const reachable = await serverReachable();
|
||||||
|
test.skip(!reachable, `Dev server not reachable at ${BASE}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('record button toggles isRecording state', async ({ page, context }) => {
|
||||||
|
await context.grantPermissions(['microphone']);
|
||||||
|
await login(page);
|
||||||
|
await page.locator('[data-testid="tldraw-canvas"], .tl-container').waitFor({ timeout: 10000 });
|
||||||
|
const recordBtn = page.locator('[data-testid="record-button"], button[aria-label*="ecord" i]').first();
|
||||||
|
const btnVisible = await recordBtn.waitFor({ timeout: 5000 }).then(() => true).catch(() => false);
|
||||||
|
if (btnVisible) {
|
||||||
|
const labelBefore = await recordBtn.getAttribute('aria-label').catch(() => '');
|
||||||
|
await recordBtn.click();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
const labelAfter = await recordBtn.getAttribute('aria-label').catch(() => '');
|
||||||
|
expect(labelBefore).not.toEqual(labelAfter);
|
||||||
|
await recordBtn.click();
|
||||||
|
} else {
|
||||||
|
test.skip(true, 'Record button not visible on current node type');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
41
src/__tests__/e2e/transcription-toggle.spec.ts
Normal file
41
src/__tests__/e2e/transcription-toggle.spec.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
/// <reference types="@playwright/test" />
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
const BASE = process.env.PLAYWRIGHT_BASE_URL || 'http://192.168.0.251:13000';
|
||||||
|
|
||||||
|
test.describe('Transcription toggle', () => {
|
||||||
|
test('record and stop preserve segments', async ({ page }) => {
|
||||||
|
test.setTimeout(60000);
|
||||||
|
|
||||||
|
async function serverReachable() {
|
||||||
|
try {
|
||||||
|
const res = await page.request.get(new URL('/api/health', BASE).toString(), { timeout: 4000 }).catch(
|
||||||
|
() => null as any
|
||||||
|
);
|
||||||
|
return Boolean(res && (res as any).ok());
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!(await serverReachable())) test.skip(true, 'Dev server not reachable at ' + BASE);
|
||||||
|
|
||||||
|
await page.goto(`${BASE}/login`);
|
||||||
|
const email = process.env.VITE_TEST_TEACHER_EMAIL || 'teacher@test';
|
||||||
|
const password = process.env.VITE_TEST_TEACHER_PASSWORD || 'password';
|
||||||
|
await page.getByLabel('Email').fill(email);
|
||||||
|
await page.getByLabel('Password').fill(password);
|
||||||
|
await page.getByRole('button', { name: /sign in|log in/i }).click();
|
||||||
|
await page.waitForURL((url: URL) => /\/dashboard|\/node\//.test(url.pathname), { timeout: 12000 });
|
||||||
|
|
||||||
|
const recordButton = page.locator('[data-testid="record-button"], button[aria-label*="record" i], button[aria-label*="Record" i]').first();
|
||||||
|
if ((await recordButton.count()) === 0) test.skip(true, 'No transcription record control found');
|
||||||
|
const beforeCount = await page.locator('[data-testid="segment"], .segment, .transcription-segment').count();
|
||||||
|
|
||||||
|
await recordButton.click();
|
||||||
|
await expect(recordButton).toHaveAttribute('aria-pressed', 'true');
|
||||||
|
await recordButton.click();
|
||||||
|
|
||||||
|
const afterCount = await page.locator('[data-testid="segment"], .segment, .transcription-segment').count();
|
||||||
|
expect(afterCount).toBeGreaterThanOrEqual(beforeCount);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user