Skip to content

Commit cf4c1a9

Browse files
committed
E2E test harness
1 parent 7274406 commit cf4c1a9

File tree

15 files changed

+9477
-2308
lines changed

15 files changed

+9477
-2308
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ out/
2727
build
2828
dist
2929

30+
# Playwright
31+
**/playwright-report
32+
**/test-results
3033

3134
# Debug
3235
npm-debug.log*

package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
11
{
22
"name": "htmldocs",
33
"private": true,
4+
"type": "module",
45
"scripts": {
56
"build": "turbo run build --filter=./packages/* --filter=!htmldocs-starter",
67
"dev": "turbo dev",
78
"lint": "turbo lint",
89
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
910
"version": "changeset version && pnpm install --no-frozen-lockfile",
10-
"publish": "pnpm run build && pnpm publish -r"
11+
"publish": "pnpm run build && pnpm publish -r",
12+
"test": "turbo test"
1113
},
1214
"devDependencies": {
1315
"@htmldocs/eslint-config": "workspace:*",
1416
"@htmldocs/typescript-config": "workspace:*",
1517
"prettier": "^3.2.5",
16-
"turbo": "latest"
18+
"turbo": "latest",
19+
"vitest": "^1.3.1",
20+
"@vitest/coverage-v8": "^1.3.1",
21+
"happy-dom": "^13.3.8"
1722
},
1823
"packageManager": "pnpm@8.9.0",
1924
"publishConfig": {

packages/e2e-tests/.env.example

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Base URL for the application
2+
TEST_BASE_URL=http://localhost:3000
3+
4+
# Test user credentials (required)
5+
TEST_USER_EMAIL=test@example.com
6+
TEST_USER_PASSWORD=your-test-password
7+
8+
# Optional test data
9+
TEST_TEAM_ID=your-test-team-id
10+
11+
# Test timeouts (optional, defaults shown)
12+
TEST_AUTH_TIMEOUT_MS=300000 # 5 minutes
13+
TEST_POLLING_INTERVAL_MS=1000 # 1 second

packages/e2e-tests/package.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "e2e-tests",
3+
"version": "1.0.0",
4+
"description": "End-to-end tests for HTML Docs",
5+
"scripts": {
6+
"test": "playwright test",
7+
"test:ui": "playwright test --ui",
8+
"test:debug": "playwright test --debug",
9+
"install:browsers": "playwright install chromium",
10+
"report": "playwright show-report"
11+
},
12+
"keywords": [],
13+
"author": "",
14+
"license": "ISC",
15+
"devDependencies": {
16+
"@playwright/test": "^1.50.1",
17+
"@types/node-fetch": "^2.6.12",
18+
"dotenv": "^16.4.7",
19+
"expect": "^29.7.0",
20+
"node-fetch": "^2.7.0"
21+
}
22+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { defineConfig, devices } from '@playwright/test';
2+
import dotenv from 'dotenv';
3+
import path from 'path';
4+
5+
// Load environment variables from .env file
6+
dotenv.config({ path: path.join(__dirname, '.env') });
7+
8+
export default defineConfig({
9+
testDir: './tests',
10+
timeout: 60000,
11+
expect: {
12+
timeout: 10000,
13+
},
14+
fullyParallel: true,
15+
forbidOnly: !!process.env.CI,
16+
retries: process.env.CI ? 2 : 0,
17+
workers: process.env.CI ? 1 : undefined,
18+
reporter: 'html',
19+
use: {
20+
baseURL: process.env.TEST_BASE_URL || 'http://localhost:3000',
21+
trace: 'on-first-retry',
22+
video: 'on-first-retry',
23+
},
24+
projects: [
25+
{
26+
name: 'chromium',
27+
use: { ...devices['Desktop Chrome'] },
28+
},
29+
],
30+
});
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { test, expect, type Page, type Browser } from '@playwright/test';
2+
import { waitForAuthCompletion, validateApiKey, login } from './helpers/auth';
3+
import path from 'path';
4+
import { promisify } from 'util';
5+
import { exec } from 'child_process';
6+
7+
const execAsync = promisify(exec);
8+
9+
test.describe('CLI Login Flow', () => {
10+
const baseUrl = process.env.TEST_BASE_URL || 'http://localhost:3000';
11+
let authPage: Page;
12+
13+
test.beforeAll(async ({ browser }: { browser: Browser }) => {
14+
// Create a new context and page for authentication
15+
const context = await browser.newContext();
16+
authPage = await context.newPage();
17+
await login(authPage);
18+
});
19+
20+
test.afterAll(async () => {
21+
await authPage.close();
22+
});
23+
24+
test('complete login flow with team selection', async () => {
25+
// Find the CLI binary path relative to the test file
26+
const cliPath = path.resolve(__dirname, '../../../packages/htmldocs/dist/cli/index.mjs');
27+
28+
// Execute the CLI login command with --headless to get the auth URL
29+
const { stdout } = await execAsync(`node ${cliPath} login --headless`, {
30+
env: {
31+
...process.env,
32+
API_URL: baseUrl,
33+
},
34+
});
35+
36+
// Get the authorization URL from stdout
37+
const authUrl = stdout.trim();
38+
expect(authUrl).toContain(`${baseUrl}/authorize`);
39+
expect(authUrl).toContain('callback=');
40+
console.log('authUrl', authUrl);
41+
42+
// Navigate to the auth URL using the already authenticated page
43+
await authPage.goto(authUrl);
44+
45+
// Verify we're on the auth page
46+
await expect(authPage).toHaveURL(/.*\/authorize/);
47+
48+
// Click the create token button
49+
await authPage.getByRole('button', { name: /create new api token/i }).click();
50+
51+
// Wait for success message
52+
await expect(authPage.getByText('Token Created Successfully')).toBeVisible();
53+
await expect(authPage.getByText('You can safely close this window now.')).toBeVisible();
54+
55+
// Extract session ID from the URL
56+
const url = new URL(authUrl);
57+
const callback = url.searchParams.get('callback');
58+
const decodedData = JSON.parse(Buffer.from(callback!, 'base64').toString());
59+
const sessionId = decodedData.session_id;
60+
61+
// Wait for auth completion and verify
62+
const authResult = await waitForAuthCompletion(sessionId);
63+
expect(authResult.apiKey).toBeTruthy();
64+
expect(authResult.teamId).toBeTruthy();
65+
66+
// Validate API key format
67+
const isValidApiKey = await validateApiKey(authResult.apiKey!);
68+
expect(isValidApiKey).toBe(true);
69+
});
70+
});

packages/e2e-tests/tests/fixtures.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { test as base, type Page } from '@playwright/test';
2+
import { login } from './helpers/auth';
3+
4+
// Declare the types of your fixtures
5+
type AuthFixtures = {
6+
authedPage: Page;
7+
};
8+
9+
// Extend the base test with authenticated page
10+
export const test = base.extend<AuthFixtures>({
11+
authedPage: async ({ page }, use: (page: Page) => Promise<void>) => {
12+
// Login before running the test
13+
await login(page);
14+
15+
// Use the authenticated page in the test
16+
await use(page);
17+
},
18+
});
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { expect, type Page } from '@playwright/test';
2+
import fetch from 'node-fetch';
3+
4+
export interface AuthSession {
5+
sessionId: string;
6+
apiKey?: string;
7+
teamId?: string;
8+
}
9+
10+
export async function login(page: Page): Promise<void> {
11+
const email = process.env.TEST_USER_EMAIL;
12+
const password = process.env.TEST_USER_PASSWORD;
13+
14+
if (!email || !password) {
15+
throw new Error('TEST_USER_EMAIL and TEST_USER_PASSWORD must be set in .env');
16+
}
17+
18+
// Navigate to login page
19+
await page.goto('/auth/login');
20+
21+
// Fill in login form
22+
await page.getByLabel(/email/i).fill(email);
23+
await page.getByLabel(/password/i).fill(password);
24+
await page.getByRole('button', { name: /sign in/i }).click();
25+
26+
// Wait for navigation and verify we're logged in
27+
await page.waitForURL(/.*\/document/);
28+
}
29+
30+
export const waitForAuthCompletion = async (sessionId: string, maxAttempts = 30): Promise<AuthSession> => {
31+
const baseUrl = process.env.TEST_BASE_URL || 'http://localhost:3000';
32+
let attempts = 0;
33+
34+
while (attempts < maxAttempts) {
35+
const response = await fetch(`${baseUrl}/api/auth/check-status?session_id=${sessionId}`);
36+
const data = await response.json();
37+
38+
if (data.status === 'completed' && data.team_id && data.api_key) {
39+
return {
40+
sessionId,
41+
apiKey: data.api_key,
42+
teamId: data.team_id,
43+
};
44+
} else if (data.status === 'error') {
45+
throw new Error(`Authentication failed: ${data.message}`);
46+
}
47+
48+
await new Promise(resolve => setTimeout(resolve, 1000));
49+
attempts++;
50+
}
51+
52+
throw new Error('Authentication timed out');
53+
};
54+
55+
export const validateApiKey = async (apiKey: string): Promise<boolean> => {
56+
// Add validation logic here based on your API requirements
57+
return apiKey.startsWith('tk_') && apiKey.length > 20;
58+
};

packages/htmldocs/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@
4545
"dev": "next dev",
4646
"start": "next start",
4747
"lint": "next lint",
48-
"tsc": "tsc"
48+
"tsc": "tsc",
49+
"test": "vitest",
50+
"test:coverage": "vitest run --coverage"
4951
},
5052
"dependencies": {
5153
"@babel/core": "^7.24.6",

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy