feat(tests): add playwright config, globalSetup, reset fixture, migrate superadmin spec
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -27,3 +27,4 @@ release/
|
||||
# Test pipeline
|
||||
data/test/
|
||||
frontend/test-results/
|
||||
frontend/playwright-report/
|
||||
|
||||
40
frontend/playwright.config.ts
Normal file
40
frontend/playwright.config.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
28
frontend/tests/fixtures.ts
Normal file
28
frontend/tests/fixtures.ts
Normal 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 };
|
||||
47
frontend/tests/global-setup.ts
Normal file
47
frontend/tests/global-setup.ts
Normal 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;
|
||||
40
frontend/tests/superadmin.spec.ts
Normal file
40
frontend/tests/superadmin.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user