openspec-playwright 0.3.41 → 0.3.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/templates/auth.setup.ts +15 -10
- package/templates/playwright.config.ts +18 -4
- package/templates/seed.spec.ts +6 -3
package/package.json
CHANGED
package/templates/auth.setup.ts
CHANGED
|
@@ -2,32 +2,35 @@
|
|
|
2
2
|
// This file authenticates once and saves the session state to .auth/user.json
|
|
3
3
|
// All subsequent tests reuse this state — no repeated logins needed.
|
|
4
4
|
//
|
|
5
|
-
// Setup:
|
|
5
|
+
// Setup (only if your app requires login):
|
|
6
6
|
// 1. Set credentials: export E2E_USERNAME=xxx E2E_PASSWORD=yyy
|
|
7
|
-
// 2.
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// 2. Enable auth: export E2E_AUTH_REQUIRED=true
|
|
8
|
+
// 3. Choose method: export E2E_AUTH_METHOD=api # or ui
|
|
9
|
+
// 4. Run: npx playwright test --project=setup
|
|
10
10
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// For multi-user testing, add more setup blocks with different storageState paths.
|
|
11
|
+
// If E2E_AUTH_REQUIRED is not true, auth setup is skipped so public apps
|
|
12
|
+
// can run seed/smoke tests without credentials.
|
|
14
13
|
|
|
15
14
|
import { test as setup, expect } from '@playwright/test';
|
|
16
15
|
import { existsSync } from 'fs';
|
|
17
16
|
|
|
17
|
+
const authRequired = process.env.E2E_AUTH_REQUIRED === 'true';
|
|
18
|
+
const authMethod = process.env.E2E_AUTH_METHOD || 'api';
|
|
19
|
+
|
|
18
20
|
// ─── API Login (preferred) ───────────────────────────────────────────────────
|
|
19
21
|
// If your app has a login API, configure it in tests/playwright/credentials.yaml:
|
|
20
22
|
// api: /api/auth/login
|
|
21
|
-
// Then this block runs automatically.
|
|
22
23
|
|
|
23
24
|
setup('authenticate via API', async ({ page }) => {
|
|
25
|
+
setup.skip(!authRequired || authMethod !== 'api', 'API auth not required/enabled');
|
|
26
|
+
|
|
24
27
|
const baseUrl = process.env.BASE_URL || 'http://localhost:3000';
|
|
25
28
|
const username = process.env.E2E_USERNAME;
|
|
26
29
|
const password = process.env.E2E_PASSWORD;
|
|
27
30
|
|
|
28
31
|
if (!username || !password) {
|
|
29
32
|
throw new Error(
|
|
30
|
-
'E2E_USERNAME and E2E_PASSWORD environment variables are required.\n' +
|
|
33
|
+
'E2E_USERNAME and E2E_PASSWORD environment variables are required when E2E_AUTH_REQUIRED=true.\n' +
|
|
31
34
|
'Set them with: export E2E_USERNAME=xxx E2E_PASSWORD=yyy'
|
|
32
35
|
);
|
|
33
36
|
}
|
|
@@ -51,13 +54,15 @@ setup('authenticate via API', async ({ page }) => {
|
|
|
51
54
|
// If no login API, use UI login. Update selectors to match your login page.
|
|
52
55
|
|
|
53
56
|
setup('authenticate via UI', async ({ page }) => {
|
|
57
|
+
setup.skip(!authRequired || authMethod !== 'ui', 'UI auth not required/enabled');
|
|
58
|
+
|
|
54
59
|
const baseUrl = process.env.BASE_URL || 'http://localhost:3000';
|
|
55
60
|
const username = process.env.E2E_USERNAME;
|
|
56
61
|
const password = process.env.E2E_PASSWORD;
|
|
57
62
|
|
|
58
63
|
if (!username || !password) {
|
|
59
64
|
throw new Error(
|
|
60
|
-
'E2E_USERNAME and E2E_PASSWORD environment variables are required.\n' +
|
|
65
|
+
'E2E_USERNAME and E2E_PASSWORD environment variables are required when E2E_AUTH_REQUIRED=true.\n' +
|
|
61
66
|
'Set them with: export E2E_USERNAME=xxx E2E_PASSWORD=yyy'
|
|
62
67
|
);
|
|
63
68
|
}
|
|
@@ -41,13 +41,13 @@ function findNpmRoot(projectRoot: string, maxDepth = 5): string {
|
|
|
41
41
|
return search(projectRoot, 0) ?? projectRoot;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
const projectRoot = findProjectRoot(
|
|
44
|
+
const projectRoot = findProjectRoot(process.cwd());
|
|
45
45
|
const npmRoot = findNpmRoot(projectRoot);
|
|
46
46
|
|
|
47
47
|
// ─── BASE_URL: prefer env, then seed.spec.ts, then default ───
|
|
48
48
|
const seedSpec = join(projectRoot, 'tests', 'playwright', 'seed.spec.ts');
|
|
49
49
|
let baseUrl = process.env.BASE_URL || 'http://localhost:3000';
|
|
50
|
-
if (existsSync(seedSpec)) {
|
|
50
|
+
if (!process.env.BASE_URL && existsSync(seedSpec)) {
|
|
51
51
|
const content = readFileSync(seedSpec, 'utf-8');
|
|
52
52
|
const m = content.match(/BASE_URL\s*=\s*process\.env\.BASE_URL\s*\|\|\s*['"]([^'"]+)['"]/);
|
|
53
53
|
if (m) baseUrl = m[1];
|
|
@@ -59,13 +59,27 @@ const npmPkg = join(npmRoot, 'package.json');
|
|
|
59
59
|
if (existsSync(npmPkg)) {
|
|
60
60
|
const pkg = JSON.parse(readFileSync(npmPkg, 'utf-8'));
|
|
61
61
|
const scripts = pkg.scripts ?? {};
|
|
62
|
-
|
|
62
|
+
const scriptName = scripts['dev:all']
|
|
63
|
+
? 'dev:all'
|
|
64
|
+
: scripts.dev
|
|
65
|
+
? 'dev'
|
|
66
|
+
: scripts.start
|
|
67
|
+
? 'start'
|
|
68
|
+
: scripts.serve
|
|
69
|
+
? 'serve'
|
|
70
|
+
: scripts.preview
|
|
71
|
+
? 'preview'
|
|
72
|
+
: 'dev';
|
|
73
|
+
devCmd = `npm run ${scriptName}`;
|
|
63
74
|
// Prefix with cd if npmRoot differs from projectRoot
|
|
64
75
|
if (npmRoot !== projectRoot) {
|
|
65
76
|
devCmd = `cd ${npmRoot} && ${devCmd}`;
|
|
66
77
|
}
|
|
67
78
|
}
|
|
68
79
|
|
|
80
|
+
const authStatePath = join(projectRoot, 'playwright', '.auth', 'user.json');
|
|
81
|
+
const storageState = existsSync(authStatePath) ? authStatePath : undefined;
|
|
82
|
+
|
|
69
83
|
export default defineConfig({
|
|
70
84
|
testDir: join(projectRoot, 'tests', 'playwright'),
|
|
71
85
|
outputDir: join(projectRoot, 'tests', 'playwright', 'test-results'),
|
|
@@ -99,7 +113,7 @@ export default defineConfig({
|
|
|
99
113
|
name: 'chromium',
|
|
100
114
|
use: {
|
|
101
115
|
...devices['Desktop Chrome'],
|
|
102
|
-
storageState
|
|
116
|
+
storageState,
|
|
103
117
|
},
|
|
104
118
|
dependencies: ['setup'],
|
|
105
119
|
// teardown: 'teardown', // Uncomment when teardown project is enabled
|
package/templates/seed.spec.ts
CHANGED
|
@@ -49,9 +49,12 @@ test.describe('Application smoke tests', () => {
|
|
|
49
49
|
}
|
|
50
50
|
};
|
|
51
51
|
page.on('console', handler);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
try {
|
|
53
|
+
await page.reload();
|
|
54
|
+
await page.waitForLoadState('networkidle');
|
|
55
|
+
} finally {
|
|
56
|
+
page.off('console', handler);
|
|
57
|
+
}
|
|
55
58
|
// Filter out known non-critical errors
|
|
56
59
|
const criticalErrors = errors.filter(
|
|
57
60
|
(e) => !e.includes('favicon') && !e.includes('404')
|