@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.
- package/.claude_README.md +276 -0
- package/.claude_memory_MEMORY.md +11 -0
- package/.gitignore.snippet +21 -0
- package/PLUGIN.md +172 -0
- package/README-TESTING.md +227 -0
- package/cli.js +53 -0
- package/e2e-tests/.env.testing +29 -0
- package/e2e-tests/data/authenticationData.js +76 -0
- package/e2e-tests/data/testConfig.js +39 -0
- package/e2e-tests/features/playwright-bdd/@Modules/.gitkeep +0 -0
- package/e2e-tests/features/playwright-bdd/@Modules/@Authentication/authentication.feature +64 -0
- package/e2e-tests/features/playwright-bdd/@Modules/@Authentication/steps.js +27 -0
- package/e2e-tests/features/playwright-bdd/@Workflows/.gitkeep +0 -0
- package/e2e-tests/features/playwright-bdd/shared/auth.steps.js +74 -0
- package/e2e-tests/features/playwright-bdd/shared/common.steps.js +52 -0
- package/e2e-tests/features/playwright-bdd/shared/global-hooks.js +44 -0
- package/e2e-tests/features/playwright-bdd/shared/navigation.steps.js +47 -0
- package/e2e-tests/instructions.example.js +110 -0
- package/e2e-tests/instructions.js +9 -0
- package/e2e-tests/playwright/auth-storage/.auth/.gitkeep +0 -0
- package/e2e-tests/playwright/auth.setup.js +74 -0
- package/e2e-tests/playwright/fixtures.js +264 -0
- package/e2e-tests/playwright/generated/.gitkeep +0 -0
- package/e2e-tests/playwright/global.setup.js +49 -0
- package/e2e-tests/playwright/global.teardown.js +23 -0
- package/e2e-tests/playwright/test-data/.gitkeep +0 -0
- package/e2e-tests/utils/stepHelpers.js +404 -0
- package/e2e-tests/utils/testDataGenerator.js +38 -0
- package/install.sh +148 -0
- package/package.json +39 -0
- package/package.json.snippet +27 -0
- 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;
|
|
File without changes
|
|
@@ -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
|
+
});
|
|
File without changes
|
|
@@ -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
|
+
});
|