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
|
# Test pipeline
|
||||||
data/test/
|
data/test/
|
||||||
frontend/test-results/
|
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