feat(tests): add playwright config, globalSetup, reset fixture, migrate superadmin spec

This commit is contained in:
2026-04-29 04:22:11 +02:00
parent 412dc01ac2
commit 8ea3d57239
5 changed files with 156 additions and 0 deletions

1
.gitignore vendored
View File

@@ -27,3 +27,4 @@ release/
# Test pipeline
data/test/
frontend/test-results/
frontend/playwright-report/

View File

@@ -0,0 +1,40 @@
import { defineConfig } from '@playwright/test';
import * as fs from 'fs';
import { fileURLToPath } from 'url';
import * as path from 'path';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Load TT_BASE_URL from data/test/.env if not already set
const envFile = path.resolve(__dirname, '../data/test/.env');
if (fs.existsSync(envFile)) {
for (const line of fs.readFileSync(envFile, 'utf-8').split('\n')) {
const eq = line.indexOf('=');
if (eq > 0) {
const key = line.slice(0, eq).trim();
if (!process.env[key]) process.env[key] = line.slice(eq + 1).trim();
}
}
}
const baseURL = process.env.TT_BASE_URL ?? 'http://127.0.0.1:3000';
export default defineConfig({
globalSetup: './tests/global-setup.ts',
testDir: './tests',
workers: 1,
reporter: [
['list'],
['html', { open: 'never', outputFolder: 'playwright-report' }],
],
use: {
baseURL,
storageState: 'tests/.auth/admin.json',
},
webServer: {
command: 'make -C .. test-up',
url: `${baseURL}/health`,
reuseExistingServer: true,
timeout: 60_000,
},
});

View File

@@ -0,0 +1,28 @@
import { test as base, expect } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
function getBaseURL(): string {
const envFile = path.resolve(__dirname, '../../../data/test/.env');
if (fs.existsSync(envFile)) {
for (const line of fs.readFileSync(envFile, 'utf-8').split('\n')) {
if (line.startsWith('TT_BASE_URL=')) return line.slice('TT_BASE_URL='.length).trim();
}
}
return process.env.TT_BASE_URL ?? 'http://127.0.0.1:3000';
}
// Extends base test with a beforeEach that resets DB to clean seed state
export const test = base.extend<{ page: Parameters<Parameters<typeof base>[1]>[0]['page'] }>({
page: async ({ page }, use) => {
const baseURL = getBaseURL();
const res = await page.request.post(`${baseURL}/__test__/reset`);
if (!res.ok()) throw new Error(`DB reset failed: ${res.status()}`);
await use(page);
},
});
export { expect };

View File

@@ -0,0 +1,47 @@
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
async function globalSetup() {
// Read env vars written by scripts/test-env.sh (make test-up calls it first)
const envFile = path.resolve(__dirname, '../../../data/test/.env');
if (!fs.existsSync(envFile)) {
throw new Error('data/test/.env not found — run "make test-up" first');
}
for (const line of fs.readFileSync(envFile, 'utf-8').split('\n')) {
const eq = line.indexOf('=');
if (eq > 0) process.env[line.slice(0, eq).trim()] = line.slice(eq + 1).trim();
}
const baseURL = process.env.TT_BASE_URL!;
// Obtain admin JWT via the login API
const res = await fetch(`${baseURL}/api/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'admin@tutortool.com', password: 'admin' }),
});
if (!res.ok) throw new Error(`Login failed: ${res.status} ${await res.text()}`);
const { token, is_superadmin } = await res.json() as { token: string; is_superadmin: boolean };
// Write Playwright storage state with localStorage pre-populated
const authDir = path.resolve(__dirname, '.auth');
fs.mkdirSync(authDir, { recursive: true });
const storageState = {
cookies: [],
origins: [
{
origin: baseURL,
localStorage: [
{ name: 'token', value: token },
{ name: 'is_superadmin', value: String(is_superadmin) },
],
},
],
};
fs.writeFileSync(path.join(authDir, 'admin.json'), JSON.stringify(storageState, null, 2));
}
export default globalSetup;

View File

@@ -0,0 +1,40 @@
import { test, expect } from './fixtures';
test.describe('Superadmin CRUD & UI Consistency', () => {
test('should show superadmin navigation and theme consistency', async ({ page }) => {
await page.goto('/admin');
const tutorsLink = page.locator('nav >> text=Tutor:innen');
await expect(tutorsLink).toBeVisible();
const mainContainer = page.locator('.paper-bg');
await expect(mainContainer).toBeVisible();
const header = page.locator('.serif').first();
await expect(header).toBeVisible();
const fontFamily = await header.evaluate(el => window.getComputedStyle(el).fontFamily);
expect(fontFamily).toContain('Source Serif');
});
test('should allow superadmin to navigate to tutors and see the list', async ({ page }) => {
await page.goto('/admin');
await page.click('nav >> text=Tutor:innen');
await expect(page).toHaveURL(/\/admin\/tutors/);
const tableHeader = page.locator('th >> text=Name / E-Mail');
await expect(tableHeader).toBeVisible();
await expect(page.locator('text=Demo Admin')).toBeVisible();
});
test('should allow superadmin to see course assignment UI', async ({ page }) => {
await page.goto('/admin');
await page.click('nav >> text=Kurse');
await expect(page).toHaveURL(/\/admin\/courses/);
await expect(page.locator('text=Neuen Kurs anlegen')).toBeVisible();
const tutorSelect = page.locator('select >> text=+ Hinzufügen').first();
await expect(tutorSelect).toBeVisible();
});
});