Skip to content

Commit c72f68f

Browse files
committed
test harness
1 parent 7274406 commit c72f68f

15 files changed

+9472
-2295
lines changed

Diff for: .gitignore

+3
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*

Diff for: package.json

+7-2
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": "[email protected]",
1924
"publishConfig": {

Diff for: packages/e2e-tests/.env.example

+13
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=[email protected]
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

Diff for: packages/e2e-tests/package.json

+22
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+
}

Diff for: packages/e2e-tests/playwright.config.ts

+30
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+
});

Diff for: packages/e2e-tests/tests/cli-login.spec.ts

+70
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+
});

Diff for: packages/e2e-tests/tests/fixtures.ts

+18
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+
});

Diff for: packages/e2e-tests/tests/helpers/auth.ts

+58
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+
};

Diff for: packages/htmldocs/package.json

+3-1
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)