Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions e2e/plugin-axe-e2e/mocks/fixtures/auth/_package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "auth-setup-fixture",
"type": "module"
}
9 changes: 9 additions & 0 deletions e2e/plugin-axe-e2e/mocks/fixtures/auth/axe-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { Page } from 'playwright-core';

export default async function setup(page: Page): Promise<void> {
await page.goto('http://localhost:8080/login');
await page.fill('#username', 'testuser');
await page.fill('#password', 'testpass');
await page.click('button[type="submit"]');
await page.waitForURL('**/dashboard');
}
10 changes: 10 additions & 0 deletions e2e/plugin-axe-e2e/mocks/fixtures/auth/code-pushup.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import axePlugin from '@code-pushup/axe-plugin';
import type { CoreConfig } from '@code-pushup/models';

export default {
plugins: [
axePlugin('http://localhost:8080/dashboard', {
setupScript: './axe-setup.ts',
}),
],
} satisfies CoreConfig;
24 changes: 24 additions & 0 deletions e2e/plugin-axe-e2e/mocks/fixtures/auth/server/dashboard.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dashboard</title>
</head>
<body>
<h1>Dashboard</h1>
<p>Welcome to the protected dashboard!</p>

<!-- Intentional accessibility issue: table header without data cells -->
<table>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Role</th>
</tr>
</thead>
<tbody></tbody>
</table>
</body>
</html>
22 changes: 22 additions & 0 deletions e2e/plugin-axe-e2e/mocks/fixtures/auth/server/login.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Login</title>
</head>
<body>
<h1>Login</h1>
<form method="POST" action="/login">
<label>
Username
<input type="text" id="username" name="username" required />
</label>
<label>
Password
<input type="password" id="password" name="password" required />
</label>
<button type="submit">Sign In</button>
</form>
</body>
</html>
100 changes: 100 additions & 0 deletions e2e/plugin-axe-e2e/mocks/fixtures/auth/server/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { readFile } from 'node:fs/promises';
import {
type IncomingMessage,
type Server,
type ServerResponse,
createServer,
} from 'node:http';
import { join } from 'node:path';
import { text } from 'node:stream/consumers';

const SESSION_COOKIE = 'session=authenticated';

function getCookie(req: IncomingMessage, name: string): string | undefined {
const cookies = req.headers.cookie?.split(';').map(c => c.trim()) ?? [];
const cookie = cookies.find(c => c.startsWith(`${name}=`));
return cookie?.split('=')[1];
}

function isAuthenticated(req: IncomingMessage): boolean {
return getCookie(req, 'session') === 'authenticated';
}

async function handleRequest(
req: IncomingMessage,
res: ServerResponse,
serverDir: string,
): Promise<void> {
const url = req.url ?? '/';
const method = req.method ?? 'GET';

if (url === '/login' && method === 'GET') {
const html = await readFile(join(serverDir, 'login.html'), 'utf-8');
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(html);
return;
}

if (url === '/login' && method === 'POST') {
const body = await text(req);
const params = new URLSearchParams(body);
const username = params.get('username');
const password = params.get('password');

if (username === 'testuser' && password === 'testpass') {
res.writeHead(302, {
Location: '/dashboard',
'Set-Cookie': `${SESSION_COOKIE}; Path=/; HttpOnly`,
});
res.end();
} else {
res.writeHead(401, { 'Content-Type': 'text/plain' });
res.end('Invalid credentials');
}
return;
}

if (url === '/dashboard') {
if (!isAuthenticated(req)) {
res.writeHead(302, { Location: '/login' });
res.end();
return;
}
const html = await readFile(join(serverDir, 'dashboard.html'), 'utf-8');
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(html);
return;
}

res.writeHead(302, { Location: '/login' });
res.end();
}

export function createAuthServer(serverDir: string): Server {
return createServer((req, res) => {
handleRequest(req, res, serverDir).catch(error => {
console.error('Server error:', error);
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Internal Server Error');
});
});
}

export function startServer(server: Server, port: number): Promise<void> {
return new Promise((resolve, reject) => {
server.on('error', reject);
server.listen(port, () => resolve());
});
}

export function stopServer(server: Server): Promise<void> {
return new Promise((resolve, reject) => {
server.close(err => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
66 changes: 66 additions & 0 deletions e2e/plugin-axe-e2e/tests/auth.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { cp } from 'node:fs/promises';
import type { Server } from 'node:http';
import path from 'node:path';
import type { Report } from '@code-pushup/models';
import { nxTargetProject } from '@code-pushup/test-nx-utils';
import {
E2E_ENVIRONMENTS_DIR,
TEST_OUTPUT_DIR,
restoreNxIgnoredFiles,
teardownTestFolder,
} from '@code-pushup/test-utils';
import { executeProcess, readJsonFile } from '@code-pushup/utils';
import {
createAuthServer,
startServer,
stopServer,
} from '../mocks/fixtures/auth/server/server.js';

describe('PLUGIN collect with setupScript authentication', () => {
const PORT = 8080;
const fixturesDir = path.join(
'e2e',
nxTargetProject(),
'mocks',
'fixtures',
'auth',
);
const testFileDir = path.join(
E2E_ENVIRONMENTS_DIR,
nxTargetProject(),
TEST_OUTPUT_DIR,
'auth',
);

let server: Server;

beforeAll(async () => {
await cp(fixturesDir, testFileDir, { recursive: true });
await restoreNxIgnoredFiles(testFileDir);
server = createAuthServer(path.join(testFileDir, 'server'));
await startServer(server, PORT);
});

afterAll(async () => {
await stopServer(server);
await teardownTestFolder(testFileDir);
});

it('should analyze authenticated page using setupScript', async () => {
const { code } = await executeProcess({
command: 'npx',
args: ['@code-pushup/cli', 'collect'],
cwd: testFileDir,
});

expect(code).toBe(0);

const report = await readJsonFile<Report>(
path.join(testFileDir, '.code-pushup', 'report.json'),
);

expect(report.plugins[0]!.audits).toSatisfyAny(
audit => audit.score < 1 && audit.slug === 'th-has-data-cells',
);
});
});
8 changes: 7 additions & 1 deletion e2e/plugin-axe-e2e/tests/collect.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ import {
import { executeProcess, readJsonFile } from '@code-pushup/utils';

describe('PLUGIN collect report with axe-plugin NPM package', () => {
const fixturesDir = path.join('e2e', nxTargetProject(), 'mocks', 'fixtures');
const fixturesDir = path.join(
'e2e',
nxTargetProject(),
'mocks',
'fixtures',
'collect',
);
const testFileDir = path.join(
E2E_ENVIRONMENTS_DIR,
nxTargetProject(),
Expand Down
3 changes: 2 additions & 1 deletion e2e/plugin-axe-e2e/tsconfig.test.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"vitest.e2e.config.ts",
"tests/**/*.e2e.test.ts",
"tests/**/*.d.ts",
"mocks/**/*.ts"
"mocks/**/*.ts",
"../../testing/test-setup/src/vitest.d.ts"
]
}
4 changes: 3 additions & 1 deletion packages/plugin-axe/src/lib/runner/setup.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import type { Page } from 'playwright-core';
import { z } from 'zod/v4';
import {
Expand Down Expand Up @@ -41,7 +42,8 @@ export async function loadSetupScript(
const validModule = await logger.task(
`Loading setup script from ${absolutePath}`,
async () => {
const module: unknown = await import(absolutePath);
const url = pathToFileURL(absolutePath).toString();
const module: unknown = await import(url);
const validated = await validateAsync(setupScriptModuleSchema, module, {
filePath: absolutePath,
});
Expand Down