@specwright/plugin 0.1.0

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.
Files changed (32) hide show
  1. package/.claude_README.md +276 -0
  2. package/.claude_memory_MEMORY.md +11 -0
  3. package/.gitignore.snippet +21 -0
  4. package/PLUGIN.md +172 -0
  5. package/README-TESTING.md +227 -0
  6. package/cli.js +53 -0
  7. package/e2e-tests/.env.testing +29 -0
  8. package/e2e-tests/data/authenticationData.js +76 -0
  9. package/e2e-tests/data/testConfig.js +39 -0
  10. package/e2e-tests/features/playwright-bdd/@Modules/.gitkeep +0 -0
  11. package/e2e-tests/features/playwright-bdd/@Modules/@Authentication/authentication.feature +64 -0
  12. package/e2e-tests/features/playwright-bdd/@Modules/@Authentication/steps.js +27 -0
  13. package/e2e-tests/features/playwright-bdd/@Workflows/.gitkeep +0 -0
  14. package/e2e-tests/features/playwright-bdd/shared/auth.steps.js +74 -0
  15. package/e2e-tests/features/playwright-bdd/shared/common.steps.js +52 -0
  16. package/e2e-tests/features/playwright-bdd/shared/global-hooks.js +44 -0
  17. package/e2e-tests/features/playwright-bdd/shared/navigation.steps.js +47 -0
  18. package/e2e-tests/instructions.example.js +110 -0
  19. package/e2e-tests/instructions.js +9 -0
  20. package/e2e-tests/playwright/auth-storage/.auth/.gitkeep +0 -0
  21. package/e2e-tests/playwright/auth.setup.js +74 -0
  22. package/e2e-tests/playwright/fixtures.js +264 -0
  23. package/e2e-tests/playwright/generated/.gitkeep +0 -0
  24. package/e2e-tests/playwright/global.setup.js +49 -0
  25. package/e2e-tests/playwright/global.teardown.js +23 -0
  26. package/e2e-tests/playwright/test-data/.gitkeep +0 -0
  27. package/e2e-tests/utils/stepHelpers.js +404 -0
  28. package/e2e-tests/utils/testDataGenerator.js +38 -0
  29. package/install.sh +148 -0
  30. package/package.json +39 -0
  31. package/package.json.snippet +27 -0
  32. package/playwright.config.ts +143 -0
@@ -0,0 +1,227 @@
1
+ # E2E Testing Framework
2
+
3
+ ## Overview
4
+
5
+ **playwright-bdd** (v8+) BDD-style end-to-end testing with AI-powered test generation and self-healing.
6
+
7
+ - Gherkin `.feature` files compiled to Playwright specs via `bddgen`
8
+ - Path-based tag scoping for module isolation
9
+ - Default parallel execution, `@serial-execution` opt-out with browser reuse
10
+ - 3-layer test data persistence (`page.testData` → `featureDataCache` → scoped JSON)
11
+ - `processDataTable` / `validateExpectations` utilities for declarative form handling
12
+ - 8 AI agents + 7 Claude Code skills for automated test generation and healing
13
+
14
+ ## Quick Start
15
+
16
+ ```bash
17
+ # 1. Install dependencies + Playwright browsers
18
+ pnpm install && pnpx playwright install
19
+
20
+ # 2. Set credentials in .env
21
+ TEST_USER_EMAIL=your-email@example.com
22
+ TEST_USER_PASSWORD=your-password
23
+
24
+ # 3. Start dev server
25
+ pnpm dev
26
+
27
+ # 4. Run authentication tests (out-of-the-box)
28
+ pnpm test:bdd:auth
29
+
30
+ # 5. View report
31
+ pnpm report:playwright
32
+ ```
33
+
34
+ **Never run `npx playwright test` directly** — it skips `bddgen` and uses stale specs.
35
+
36
+ ## Project Configuration
37
+
38
+ | Project | Purpose | Behavior |
39
+ | ------------------ | --------------------- | -------------------------------------------------------- |
40
+ | `setup` | Auth session creation | Runs first, creates auth session |
41
+ | `auth-tests` | Login/logout tests | Clean browser state, no storageState |
42
+ | `serial-execution` | Stateful features | `@serial-execution` tag, workers: 1, browser reused |
43
+ | `main-e2e` | Everything else | `fullyParallel: true` (default), storageState from setup |
44
+
45
+ ## Directory Structure
46
+
47
+ ```
48
+ e2e-tests/
49
+ ├── features/playwright-bdd/
50
+ │ ├── @Modules/ ← Single-module tests
51
+ │ │ └── @Authentication/ ← Out-of-the-box auth tests (7 scenarios)
52
+ │ ├── @Workflows/ ← Cross-module workflow tests
53
+ │ │ └── @YourWorkflow/ ← Precondition → consumers pattern
54
+ │ │ ├── @0-Precondition/ ← Serial, creates shared data
55
+ │ │ ├── @1-Consumer/ ← Loads predata, runs in parallel
56
+ │ │ └── @2-Consumer/
57
+ │ └── shared/ ← Globally scoped steps (no @ prefix)
58
+ │ ├── auth.steps.js
59
+ │ ├── navigation.steps.js
60
+ │ ├── common.steps.js
61
+ │ └── global-hooks.js ← Auto-loaded, NEVER import manually
62
+ ├── playwright/
63
+ │ ├── fixtures.js ← Custom fixtures + browser reuse (import from HERE)
64
+ │ ├── auth.setup.js ← Two-step localStorage login
65
+ │ ├── auth-storage/.auth/ ← Saved auth state (gitignored)
66
+ │ ├── generated/ ← Seed files from exploration
67
+ │ └── test-data/ ← Scoped JSON files (auto-created)
68
+ ├── utils/
69
+ │ ├── stepHelpers.js ← processDataTable, validateExpectations, FIELD_TYPES
70
+ │ └── testDataGenerator.js ← Faker-based value generation
71
+ ├── data/
72
+ │ ├── authenticationData.js ← Credentials from env vars (never hardcoded)
73
+ │ └── testConfig.js ← Routes, timeouts, baseUrl
74
+ ├── instructions.js ← Agent pipeline config (add your entries)
75
+ ├── instructions.example.js ← Example configs (text, Jira, CSV, workflow)
76
+ └── .env.testing ← Environment variable template
77
+ ```
78
+
79
+ ## Running Tests
80
+
81
+ ```bash
82
+ ## Predefined scripts
83
+ pnpm test:bdd # All tests except auth
84
+ pnpm test:bdd:all # Everything including auth
85
+ pnpm test:bdd:auth # Authentication tests only
86
+ pnpm test:bdd:serial # Serial execution tests only
87
+ pnpm test:bdd:debug # Debug mode (PWDEBUG)
88
+
89
+ ## Run by tag
90
+ pnpm bddgen && npx playwright test --project setup --project main-e2e --grep @your-tag
91
+ pnpm bddgen && npx playwright test --project setup --project serial-execution --grep @your-serial-tag
92
+
93
+ ## Run by file
94
+ pnpm bddgen && npx playwright test --project setup --project main-e2e \
95
+ ".features-gen/e2e-tests/features/playwright-bdd/@Modules/@YourModule/feature.spec.js"
96
+
97
+ ## Run headed
98
+ pnpm bddgen && npx playwright test --project setup --project main-e2e --grep @tag --headed
99
+
100
+ ## Run single scenario by title
101
+ pnpm bddgen && npx playwright test --project setup --project main-e2e -g "Scenario title"
102
+
103
+ ## Reports
104
+ pnpm report:playwright # View HTML report
105
+ pnpm test:clean # Clean reports + .features-gen + test-data
106
+ ```
107
+
108
+ **Tip:** Use `--project serial-execution` for `@serial-execution` features, `--project auth-tests` for `@Authentication`, `--project main-e2e` for everything else.
109
+
110
+ ## Writing Tests
111
+
112
+ ### Feature Files
113
+
114
+ ```gherkin
115
+ @module-name
116
+ Feature: Module Description
117
+ Background:
118
+ Given I am logged in
119
+ When I navigate to "PageName"
120
+
121
+ Scenario: Happy path
122
+ When I fill the form with:
123
+ | Field Name | Value | Type |
124
+ | Name | <gen_test_data> | SharedGenerated |
125
+ | Email | <gen_test_data> | SharedGenerated |
126
+ And I click the submit button
127
+ Then I should see the success message
128
+ ```
129
+
130
+ ### Step Definitions (with processDataTable)
131
+
132
+ ```javascript
133
+ import { When, Then, expect } from '../../../../playwright/fixtures.js';
134
+ import { FIELD_TYPES, processDataTable, validateExpectations } from '../../../../utils/stepHelpers.js';
135
+
136
+ const FIELD_CONFIG = {
137
+ Name: { type: FIELD_TYPES.FILL, testID: 'user-name' },
138
+ Email: { type: FIELD_TYPES.FILL, testID: 'user-email' },
139
+ };
140
+
141
+ const VALIDATION_CONFIG = {
142
+ Name: { type: FIELD_TYPES.TEXT_VISIBLE, testID: 'display-name' },
143
+ Email: { type: FIELD_TYPES.TEXT_VISIBLE, testID: 'display-email' },
144
+ };
145
+
146
+ const fieldMapping = { Name: 'name', Email: 'email' };
147
+
148
+ When('I fill the form with:', async ({ page }, dataTable) => {
149
+ await processDataTable(page, dataTable, { mapping: fieldMapping, fieldConfig: FIELD_CONFIG });
150
+ });
151
+
152
+ Then('I should see the details:', async ({ page }, dataTable) => {
153
+ await validateExpectations(page, dataTable, { mapping: fieldMapping, validationConfig: VALIDATION_CONFIG });
154
+ });
155
+ ```
156
+
157
+ ### Data Table Placeholders
158
+
159
+ | Placeholder | Used in | Meaning |
160
+ | ------------------ | --------------- | ------------------------------ |
161
+ | `<gen_test_data>` | Form fill steps | Generate faker value, cache it |
162
+ | `<from_test_data>` | Assertion steps | Read cached value |
163
+ | Static value | Both | Use as-is |
164
+
165
+ ### When to Use `@serial-execution`
166
+
167
+ Add when:
168
+
169
+ - `<gen_test_data>` in Scenario A and `<from_test_data>` in Scenario B
170
+ - Scenarios rely on UI state from previous scenarios
171
+ - Workflow precondition/consumer pattern
172
+
173
+ No tag needed (parallel by default) when:
174
+
175
+ - Each scenario is self-contained
176
+ - Scenarios only READ predata independently
177
+
178
+ ### Adding a New Module
179
+
180
+ 1. Create: `e2e-tests/features/playwright-bdd/@Modules/@YourModule/`
181
+ 2. Add: `your-feature.feature` + `steps.js`
182
+ 3. Import: `import { When, Then } from '../../../../playwright/fixtures.js'`
183
+ 4. For data tables: `import { FIELD_TYPES, processDataTable } from '../../../../utils/stepHelpers.js'`
184
+ 5. Run: `pnpm test:bdd`
185
+
186
+ ### Adding a Workflow
187
+
188
+ 1. Create: `e2e-tests/features/playwright-bdd/@Workflows/@YourWorkflow/`
189
+ 2. `@0-Precondition/` — serial, creates data (`@precondition @cross-feature-data @serial-execution`)
190
+ 3. `@1-Consumer/` — loads predata via `Given I load predata from "workflow-name"` (`@workflow-consumer`)
191
+
192
+ ## Agent Pipeline
193
+
194
+ | Command | What it does |
195
+ | ---------------------- | ------------------------------------------------------------------------------ |
196
+ | `/e2e-automate` | Full 10-phase pipeline: config → explore → plan → approve → generate → execute |
197
+ | `/e2e-plan <page>` | Explore a page, discover selectors, generate test plan |
198
+ | `/e2e-generate <plan>` | Generate .feature + steps.js from a plan |
199
+ | `/e2e-heal` | Run tests, diagnose failures, auto-heal |
200
+ | `/e2e-run` | Quick test execution with optional filters |
201
+ | `/e2e-validate` | Validate seed file tests before BDD generation |
202
+ | `/e2e-process <input>` | Process Jira/files into test plan markdown |
203
+
204
+ Configure in `e2e-tests/instructions.js` (see `instructions.example.js` for examples).
205
+
206
+ ## Troubleshooting
207
+
208
+ | Problem | Fix |
209
+ | ------------------------------------ | -------------------------------------------------------------------------- |
210
+ | "Step not found" after editing steps | `rm -rf .features-gen/ && pnpm test:bdd` |
211
+ | Auth failures | `rm -f e2e-tests/playwright/auth-storage/.auth/user.json && pnpm test:bdd` |
212
+ | Cross-module steps not visible | Move from `@Module/steps.js` to `shared/` |
213
+ | Duplicate hook errors | Never import `global-hooks.js` manually |
214
+ | Browser launch failures | `pnpx playwright install` |
215
+
216
+ ## Environment Variables
217
+
218
+ | Variable | Default | Description |
219
+ | ------------------------ | ----------------------- | ------------------------- |
220
+ | `BASE_URL` | `http://localhost:5173` | Application URL |
221
+ | `TEST_USER_EMAIL` | — | Login email (required) |
222
+ | `TEST_USER_PASSWORD` | — | Login password (required) |
223
+ | `HEADLESS` | `true` | Headless browser mode |
224
+ | `TEST_TIMEOUT` | `90000` | Test timeout (ms) |
225
+ | `ENABLE_SCREENSHOTS` | `true` | Screenshot on failure |
226
+ | `ENABLE_VIDEO_RECORDING` | `false` | Video on failure |
227
+ | `ENABLE_TRACING` | `false` | Playwright traces |
package/cli.js ADDED
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { execSync } = require('child_process');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+
7
+ const command = process.argv[2];
8
+ const targetDir = process.argv[3] || process.cwd();
9
+
10
+ if (command === 'init') {
11
+ const installScript = path.join(__dirname, 'install.sh');
12
+
13
+ if (!fs.existsSync(installScript)) {
14
+ console.error('Error: install.sh not found in plugin package.');
15
+ process.exit(1);
16
+ }
17
+
18
+ console.log(`\n Specwright E2E Plugin Installer\n`);
19
+ console.log(` Target: ${targetDir}\n`);
20
+
21
+ try {
22
+ execSync(`bash "${installScript}" "${targetDir}"`, { stdio: 'inherit' });
23
+ console.log(`\n Done! Next steps:\n`);
24
+ console.log(` 1. pnpm install`);
25
+ console.log(` 2. npx playwright install`);
26
+ console.log(` 3. cp e2e-tests/instructions.example.js e2e-tests/instructions.js`);
27
+ console.log(` 4. pnpm test:bdd:auth\n`);
28
+ } catch (err) {
29
+ console.error('Installation failed:', err.message);
30
+ process.exit(1);
31
+ }
32
+ } else {
33
+ console.log(`
34
+ @specwright/plugin — AI-powered E2E test automation
35
+
36
+ Usage:
37
+ npx @specwright/plugin init [target-dir]
38
+
39
+ This installs the Playwright BDD framework, Claude Code agents,
40
+ and shared step definitions into your project.
41
+
42
+ Options:
43
+ init [dir] Install plugin into target directory (default: current dir)
44
+
45
+ After install:
46
+ pnpm install Install dependencies
47
+ npx playwright install Install browsers
48
+ pnpm test:bdd:auth Verify setup
49
+ claude → /e2e-automate Generate tests with AI
50
+
51
+ More info: https://github.com/SanthoshDhandapani/specwright
52
+ `);
53
+ }
@@ -0,0 +1,29 @@
1
+ # Base Environment Configuration
2
+ BASE_ENV=qat
3
+ BASE_URL=http://localhost:5173
4
+ NODE_ENV=test
5
+
6
+ # Browser Configuration
7
+ HEADLESS=true
8
+ BROWSER=chromium
9
+ CHROME_ARGS="--no-sandbox,--disable-dev-shm-usage"
10
+
11
+ # Reports Configuration
12
+ CUCUMBER_REPORT_PATH="reports/cucumber-bdd/report.json"
13
+ CODEGEN_OUTPUT_PATH="e2e-tests/playwright/generated"
14
+
15
+ # Test Configuration
16
+ TEST_TIMEOUT=120000
17
+
18
+ # Visual Testing Configuration
19
+ ENABLE_SCREENSHOTS=true
20
+ ENABLE_VIDEO_RECORDING=true
21
+ RETAIN_VIDEO_ON_SUCCESS=false
22
+ ENABLE_TRACING=false
23
+
24
+ # Test Credentials (REQUIRED — set your own values)
25
+ # TEST_USER_EMAIL=your-test-email@example.com
26
+ # TEST_USER_PASSWORD=your-test-password
27
+
28
+ # Build Environment
29
+ VITE_BUILD_ENVIRONMENT=testing
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Authentication test data — TEMPLATE
3
+ *
4
+ * Update the locators below to match YOUR app's login form.
5
+ * Credentials are read from environment variables ONLY.
6
+ * Set TEST_USER_EMAIL and TEST_USER_PASSWORD in your .env file.
7
+ */
8
+
9
+ const getCredentials = () => {
10
+ const email = process.env.TEST_USER_EMAIL;
11
+ const password = process.env.TEST_USER_PASSWORD;
12
+
13
+ if (!email || !password) {
14
+ throw new Error('E2E credentials not configured. Set TEST_USER_EMAIL and TEST_USER_PASSWORD in your .env file.');
15
+ }
16
+
17
+ return { email, password };
18
+ };
19
+
20
+ export const authenticationData = {
21
+ baseUrl: process.env.BASE_URL || 'http://localhost:5173',
22
+
23
+ environment: process.env.BASE_ENV || '',
24
+
25
+ validCredentials: getCredentials(),
26
+
27
+ invalidCredentials: {
28
+ email: 'invalid@email.com',
29
+ password: 'invalid_password',
30
+ },
31
+
32
+ // ┌──────────────────────────────────────────────────────────────┐
33
+ // │ UPDATE THESE LOCATORS to match your app's login form │
34
+ // │ The testId values should match data-testid attributes │
35
+ // │ in your login page components. │
36
+ // └──────────────────────────────────────────────────────────────┘
37
+ locators: {
38
+ emailInput: {
39
+ testId: 'loginEmail', // Update: your email input data-testid
40
+ label: 'Email',
41
+ },
42
+ emailSubmitButton: {
43
+ testId: 'loginEmailSubmit', // Update: your email submit button data-testid
44
+ label: 'Continue',
45
+ },
46
+ passwordInput: {
47
+ testId: 'loginPassword', // Update: your password input data-testid
48
+ label: 'Password',
49
+ },
50
+ loginSubmitButton: {
51
+ testId: 'loginSubmit', // Update: your login submit button data-testid
52
+ label: 'Sign In',
53
+ },
54
+ errorMessage: {
55
+ selector: '[data-testid="loginError"]', // Update: your error message selector
56
+ errorText: 'invalid email or password',
57
+ },
58
+ },
59
+
60
+ timeouts: {
61
+ login: 60000,
62
+ loadState: 50000,
63
+ elementWait: 10000,
64
+ },
65
+
66
+ // Two-factor authentication (remove if your app doesn't use 2FA)
67
+ twoFactor: {
68
+ code: '99999999',
69
+ locators: {
70
+ codeInput: { testId: 'twoFactorCodeInput' },
71
+ proceedButton: { testId: 'twfProceed' },
72
+ },
73
+ },
74
+ };
75
+
76
+ export default authenticationData;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Test configuration — TEMPLATE
3
+ *
4
+ * Update the routes below to match YOUR app's URL structure.
5
+ */
6
+ import { authenticationData } from './authenticationData.js';
7
+
8
+ export const testConfig = {
9
+ baseUrl: authenticationData.baseUrl,
10
+
11
+ credentials: {
12
+ ...authenticationData.validCredentials,
13
+ },
14
+
15
+ // ┌──────────────────────────────────────────────────────────────┐
16
+ // │ UPDATE THESE ROUTES to match your app's URL structure │
17
+ // │ These are used by shared/navigation.steps.js for │
18
+ // │ `Given I am on the "PageName" page` steps. │
19
+ // └──────────────────────────────────────────────────────────────┘
20
+ routes: {
21
+ Home: '/home',
22
+ SignIn: '/signin',
23
+ // Add your app's routes here:
24
+ // Dashboard: '/dashboard',
25
+ // Settings: '/settings',
26
+ // Users: '/users',
27
+ },
28
+
29
+ timeouts: {
30
+ standard: 30000,
31
+ long: 60000,
32
+ element: 10000,
33
+ loadState: 60000,
34
+ navigation: 50000,
35
+ networkIdle: 15000,
36
+ },
37
+ };
38
+
39
+ export default testConfig;
@@ -0,0 +1,64 @@
1
+ @authentication @serial-execution
2
+ Feature: Authentication
3
+ As a user of the application
4
+ I want to be able to log in and log out
5
+ So that I can securely access the application
6
+
7
+ @login-form
8
+ Scenario: Verify login form displays email input
9
+ Given I am on the sign-in page
10
+ Then I should see the heading "Welcome to FourKites!"
11
+ And the element with test ID "loginEmail" should be visible
12
+ And the element with test ID "loginEmailSubmit" should be disabled
13
+
14
+ @login-success
15
+ Scenario: Successful login flow
16
+ Given I am on the sign-in page
17
+ When I enter valid email
18
+ And I click the email submit button
19
+ Then the element with test ID "loginPassword" should be visible
20
+ When I enter valid password
21
+ And I click the login submit button
22
+ And I handle 2FA if prompted
23
+ Then I should be redirected to "/home"
24
+
25
+ @login-invalid-email
26
+ Scenario: Login with invalid email format
27
+ Given I am on the sign-in page
28
+ When I enter email "invalid-email"
29
+ And I click the email submit button
30
+ Then I should see the text "Enter a valid email address."
31
+
32
+ @login-invalid-credentials
33
+ Scenario: Login with invalid credentials
34
+ Given I am on the sign-in page
35
+ When I enter email "nonexistent@example.com"
36
+ And I click the email submit button
37
+ Then the element with test ID "loginPassword" should be visible
38
+ When I enter password "WrongPassword123"
39
+ And I click the login submit button
40
+ Then I should see the text "Email does not exist on FourKites"
41
+
42
+ @login-empty-email
43
+ Scenario: Empty email validation
44
+ Given I am on the sign-in page
45
+ Then the element with test ID "loginEmailSubmit" should be disabled
46
+
47
+ @logout
48
+ Scenario: Logout flow
49
+ Given I am on the sign-in page
50
+ When I enter valid email
51
+ And I click the email submit button
52
+ And I enter valid password
53
+ And I click the login submit button
54
+ And I handle 2FA if prompted
55
+ Then I should be redirected to "/home"
56
+ When I click the user menu button
57
+ And I click the button "Logout"
58
+ Then I should be redirected to "/signin"
59
+
60
+ @unauthenticated-access
61
+ Scenario: Unauthenticated access protection
62
+ Given I clear browser storage
63
+ When I navigate to "/home"
64
+ Then I should be redirected to "/signin"
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Authentication module-specific steps.
3
+ * Co-located with authentication.feature — scoped to @Modules AND @Authentication.
4
+ *
5
+ * Reusable steps (login, navigation, assertions) live in shared/.
6
+ * Only module-specific steps that are NOT reusable belong here.
7
+ */
8
+ import { When, Then, expect } from '../../../../playwright/fixtures.js';
9
+
10
+ When('I click the user menu button', async ({ page }) => {
11
+ const userMenuButton = page.getByTestId('user-menu-button');
12
+ await userMenuButton.waitFor({ state: 'visible', timeout: 10000 });
13
+ await userMenuButton.click();
14
+ });
15
+
16
+ Then('the password field should be visible', async ({ page }) => {
17
+ const passwordInput = page.getByTestId('loginPassword');
18
+ await expect(passwordInput).toBeVisible();
19
+ });
20
+
21
+ Then('the Go Back button should be visible', async ({ page }) => {
22
+ await expect(page.getByRole('button', { name: 'Go Back' })).toBeVisible();
23
+ });
24
+
25
+ Then('the Forgot Password button should be visible', async ({ page }) => {
26
+ await expect(page.getByRole('button', { name: 'Forgot Password?' })).toBeVisible();
27
+ });
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Shared authentication steps — reusable across all modules.
3
+ * Lives in shared/ (no @-prefix) so it is globally scoped.
4
+ */
5
+ import { Given, When, Then, expect } from '../../../playwright/fixtures.js';
6
+
7
+ Given('I am logged in', async ({ page, authData }) => {
8
+ // When using storageState from auth.setup, the user is already authenticated.
9
+ // In serial-execution mode, skip navigation if the page is already on an
10
+ // authenticated route to preserve cross-scenario state (e.g., submitted forms).
11
+ const currentUrl = page.url();
12
+ if (currentUrl && currentUrl !== 'about:blank' && !currentUrl.includes('/signin')) {
13
+ // Already on an authenticated page — no need to navigate away
14
+ return;
15
+ }
16
+ await page.goto('/home');
17
+ await page.waitForLoadState('networkidle', { timeout: authData.timeouts.loadState });
18
+ });
19
+
20
+ Given('I am on the sign-in page', async ({ page, authData }) => {
21
+ await page.goto('/signin');
22
+ await page.waitForLoadState('networkidle', { timeout: authData.timeouts.loadState });
23
+ });
24
+
25
+ When('I enter email {string}', async ({ page, authData }, email) => {
26
+ const emailInput = page.getByTestId(authData.locators.emailInput.testId);
27
+ await emailInput.waitFor({ state: 'visible', timeout: authData.timeouts.elementWait });
28
+ await emailInput.fill(email);
29
+ });
30
+
31
+ When('I enter valid email', async ({ page, authData }) => {
32
+ const emailInput = page.getByTestId(authData.locators.emailInput.testId);
33
+ await emailInput.waitFor({ state: 'visible', timeout: authData.timeouts.elementWait });
34
+ await emailInput.fill(authData.validCredentials.email);
35
+ });
36
+
37
+ When('I click the email submit button', async ({ page, authData }) => {
38
+ const emailSubmit = page.getByTestId(authData.locators.emailSubmitButton.testId);
39
+ await emailSubmit.click();
40
+ });
41
+
42
+ When('I enter password {string}', async ({ page, authData }, password) => {
43
+ const passwordInput = page.getByTestId(authData.locators.passwordInput.testId);
44
+ await passwordInput.waitFor({ state: 'visible', timeout: authData.timeouts.elementWait });
45
+ await passwordInput.fill(password);
46
+ });
47
+
48
+ When('I enter valid password', async ({ page, authData }) => {
49
+ const passwordInput = page.getByTestId(authData.locators.passwordInput.testId);
50
+ await passwordInput.waitFor({ state: 'visible', timeout: authData.timeouts.elementWait });
51
+ await passwordInput.fill(authData.validCredentials.password);
52
+ });
53
+
54
+ When('I click the login submit button', async ({ page, authData }) => {
55
+ const loginSubmit = page.getByTestId(authData.locators.loginSubmitButton.testId);
56
+ await loginSubmit.click();
57
+ });
58
+
59
+ When('I handle 2FA if prompted', async ({ page, authData }) => {
60
+ try {
61
+ const twoFactorInput = page.getByTestId(authData.twoFactor.locators.codeInput.testId);
62
+ await twoFactorInput.waitFor({ state: 'visible', timeout: 5000 });
63
+ await twoFactorInput.fill(authData.twoFactor.code);
64
+ const proceedButton = page.getByTestId(authData.twoFactor.locators.proceedButton.testId);
65
+ await proceedButton.click();
66
+ } catch {
67
+ // No 2FA prompt — expected for most test accounts
68
+ }
69
+ });
70
+
71
+ Then('I should be redirected to {string}', async ({ page, authData }, urlPath) => {
72
+ await page.waitForURL(`**${urlPath}**`, { timeout: authData.timeouts.login });
73
+ await page.waitForLoadState('networkidle', { timeout: authData.timeouts.loadState });
74
+ });
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Shared common steps — generic assertions and utilities.
3
+ * Lives in shared/ (no @-prefix) so it is globally scoped.
4
+ */
5
+ import { Given, When, Then, Before, expect } from '../../../playwright/fixtures.js';
6
+
7
+ Then('I should see the heading {string}', async ({ page }, headingText) => {
8
+ await expect(page.getByRole('heading', { name: headingText }).first()).toBeVisible();
9
+ });
10
+
11
+ Then('I should see a tab {string} that is active', async ({ page }, tabName) => {
12
+ const tab = page.getByRole('tab', { name: tabName });
13
+ await expect(tab).toBeVisible();
14
+ await expect(tab).toHaveAttribute('aria-selected', 'true');
15
+ });
16
+
17
+ When('I click the tab {string}', async ({ page }, tabName) => {
18
+ const tab = page.getByRole('tab', { name: tabName });
19
+ await tab.click();
20
+ });
21
+
22
+ When('I click the tab with test ID {string}', async ({ page }, testId) => {
23
+ await page.getByTestId(testId).click();
24
+ });
25
+
26
+ Then('the tab with test ID {string} should be active', async ({ page }, testId) => {
27
+ const tab = page.getByTestId(testId);
28
+ await expect(tab).toHaveAttribute('aria-selected', 'true');
29
+ });
30
+
31
+ Given('I clear browser storage', async ({ page, testConfig }) => {
32
+ // Navigate to the app origin first so localStorage/sessionStorage are accessible.
33
+ // On about:blank or cross-origin pages, browsers throw SecurityError when
34
+ // accessing storage APIs.
35
+ const currentUrl = page.url();
36
+ if (!currentUrl || currentUrl === 'about:blank' || !currentUrl.startsWith('http')) {
37
+ const baseUrl = testConfig?.baseUrl || process.env.BASE_URL || 'http://localhost:5173';
38
+ await page.goto(baseUrl, { waitUntil: 'commit' });
39
+ }
40
+
41
+ await page.evaluate(() => {
42
+ localStorage.clear();
43
+ sessionStorage.clear();
44
+ });
45
+ // Clear cookies
46
+ const context = page.context();
47
+ await context.clearCookies();
48
+ });
49
+
50
+ Then('the page should have title containing {string}', async ({ page }, titlePart) => {
51
+ await expect(page).toHaveTitle(new RegExp(titlePart));
52
+ });