Add basic e2e tests for admin panel

This commit is contained in:
Constantin Graf
2026-09-22 13:52:15 +02:00
parent 9ea774c1ac
commit 2072223f19
4 changed files with 133 additions and 0 deletions

View File

@@ -6,6 +6,7 @@ APP_DEBUG=true
APP_URL=http://localhost
APP_FORCE_HTTPS=false
APP_ENABLE_REGISTRATION=true
SUPER_ADMINS=admin@example.com
# Logging
LOG_CHANNEL=stack

75
e2e/admin-panel.spec.ts Normal file
View File

@@ -0,0 +1,75 @@
import { adminTest as test, expect } from '../playwright/fixtures';
import { PLAYWRIGHT_BASE_URL, TEST_USER_PASSWORD } from '../playwright/config';
/**
* Index pages of all Filament resources that ship with the core application.
* Resources provided by extensions are intentionally left out, because
* extensions are not installed in CI.
*/
const resourcePages = [
{ path: '/admin/time-entries', heading: 'Time Entries' },
{ path: '/admin/projects', heading: 'Projects' },
{ path: '/admin/tasks', heading: 'Tasks' },
{ path: '/admin/clients', heading: 'Clients' },
{ path: '/admin/tags', heading: 'Tags' },
{ path: '/admin/reports', heading: 'Reports' },
{ path: '/admin/users', heading: 'Users' },
{ path: '/admin/organizations', heading: 'Organizations' },
{ path: '/admin/organization-invitations', heading: 'Invitations' },
{ path: '/admin/project-members', heading: 'Project Members' },
{ path: '/admin/tokens', heading: 'Tokens' },
{ path: '/admin/failed-jobs', heading: 'Failed Jobs' },
{ path: '/admin/audits', heading: 'Audits' },
];
test.describe('Admin Panel Access', () => {
test.use({ storageState: undefined });
test('test that the admin panel redirects guests to the login page', async ({ page }) => {
await page.goto(PLAYWRIGHT_BASE_URL + '/admin');
await expect(page).toHaveURL(PLAYWRIGHT_BASE_URL + '/login');
});
test('test that the admin panel is forbidden for users that are no super admin', async ({
page,
}) => {
const email = `john+${Date.now()}_${Math.floor(Math.random() * 10000)}@doe.com`;
await page.goto(PLAYWRIGHT_BASE_URL + '/register');
await page.getByLabel('Name').fill('John Doe');
await page.getByLabel('Email').fill(email);
await page.getByLabel('Password', { exact: true }).fill(TEST_USER_PASSWORD);
await page.getByLabel('Confirm Password').fill(TEST_USER_PASSWORD);
await page.getByLabel('I agree to the Terms of').click();
await page.getByRole('button', { name: 'Register' }).click();
await expect(page.getByTestId('dashboard_view')).toBeVisible();
const response = await page.goto(PLAYWRIGHT_BASE_URL + '/admin');
expect(response?.status()).toBe(403);
});
});
test.describe('Admin Panel', () => {
test('test that the admin dashboard loads with all widgets', async ({ page }) => {
await page.goto(PLAYWRIGHT_BASE_URL + '/admin');
await expect(page.getByRole('heading', { name: 'Dashboard', level: 1 })).toBeVisible();
// The widgets are loaded lazily via Livewire, so they only show up once
// their content has been rendered.
await expect(page.getByText('Total real users')).toBeVisible();
await expect(page.getByText('Placeholder users')).toBeVisible();
await expect(page.getByText('Active users in the last seven days')).toBeVisible();
await expect(page.getByText('User Registrations', { exact: true })).toBeVisible();
await expect(page.getByText('Time Entries Created', { exact: true })).toBeVisible();
await expect(page.getByText('Time Entries Imported', { exact: true })).toBeVisible();
});
for (const { path, heading } of resourcePages) {
test(`test that the admin page ${path} loads`, async ({ page }) => {
const response = await page.goto(PLAYWRIGHT_BASE_URL + path);
expect(response?.status()).toBe(200);
await expect(page.getByRole('heading', { name: heading, level: 1 })).toBeVisible();
});
}
});

24
e2e/utils/admin.ts Normal file
View File

@@ -0,0 +1,24 @@
import { expect } from '@playwright/test';
import type { Page } from '@playwright/test';
import { PLAYWRIGHT_BASE_URL } from '../../playwright/config';
/**
* The seeded user that is allowed to access the Filament admin panel.
*
* Panel access is granted by email address via the `SUPER_ADMINS` config, so
* the admin panel tests cannot use the throwaway users created by the default
* fixture and rely on the seeded user instead.
*/
export const SUPER_ADMIN_EMAIL = 'admin@example.com';
export const SEEDED_USER_PASSWORD = 'password';
/**
* Log in as the seeded super admin user via the regular application login.
*/
export async function loginAsSuperAdmin(page: Page): Promise<void> {
await page.goto(PLAYWRIGHT_BASE_URL + '/login');
await page.getByLabel('Email').fill(SUPER_ADMIN_EMAIL);
await page.getByLabel('Password').fill(SEEDED_USER_PASSWORD);
await page.getByRole('button', { name: 'Log in' }).click();
await expect(page.getByTestId('dashboard_view')).toBeVisible({ timeout: 15000 });
}

View File

@@ -1,8 +1,11 @@
import fs from 'node:fs';
import path from 'node:path';
import { test as baseTest } from '@playwright/test';
import type { Page } from '@playwright/test';
import { PLAYWRIGHT_BASE_URL, TEST_USER_PASSWORD } from './config';
import { type TestContext, setupTestContext } from '../e2e/utils/api';
import { setupAdminUser, setupEmployeeUser } from '../e2e/utils/members';
import { loginAsSuperAdmin } from '../e2e/utils/admin';
export * from '@playwright/test';
export type { TestContext };
@@ -116,3 +119,33 @@ export const test = baseTest.extend<
await closeAdmin();
},
});
/**
* Authentication fixture for the Filament admin panel.
*
* The admin panel can only be accessed by the seeded super admin user, so the
* throwaway users of the default fixture cannot be used. Logging in is rate
* limited (5 attempts per minute and email address), therefore the session is
* created once per worker and reused by all tests of that worker.
*/
export const adminTest = baseTest.extend<object, { adminStorageState: string }>({
adminStorageState: [
async ({ browser }, use, workerInfo) => {
const fileName = path.resolve(
workerInfo.project.outputDir,
`.auth/admin-${workerInfo.workerIndex}.json`
);
if (!fs.existsSync(fileName)) {
const context = await browser.newContext({ storageState: undefined });
await loginAsSuperAdmin(await context.newPage());
await context.storageState({ path: fileName });
await context.close();
}
await use(fileName);
},
{ scope: 'worker' },
],
storageState: ({ adminStorageState }, use) => use(adminStorageState),
});