agents-cli-automation 1.0.7 → 1.0.9
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/src/templates/playwright-agent-ts.md +661 -1344
|
@@ -1,1419 +1,736 @@
|
|
|
1
|
-
name: Create Playwright Framework - UI Testing
|
|
2
|
-
description: Creates a production-ready Playwright UI automation framework with all dependencies, fixtures, and test configurations properly set up.
|
|
3
|
-
argument-hint: "framework requirements, e.g., 'JavaScript with UI tests'"
|
|
4
|
-
|
|
5
1
|
---
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
For Playwright
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
2
|
+
name: automation.md
|
|
3
|
+
description: Describe what this custom agent does and when to use it.
|
|
4
|
+
argument-hint: The inputs this agent expects, e.g., "a task to implement" or "a question to answer".
|
|
5
|
+
tools:
|
|
6
|
+
- read
|
|
7
|
+
- create_file
|
|
8
|
+
- apply_patch
|
|
9
|
+
- run_in_terminal
|
|
10
|
+
- file_search
|
|
11
|
+
- grep_search
|
|
12
|
+
- manage_todo_list
|
|
13
|
+
---
|
|
14
|
+
This agent documents and automates reproducing the repository layout, creating a minimal, scalable Playwright-based UI test framework (targeting https://www.saucedemo.com/ by default), and providing recommended developer commands so a new contributor can recreate the workspace and run tests. When to use - When a new contributor needs to scaffold a local copy of this project structure, generate minimal UI test scaffolding (Playwright + page objects + sample tests), and install tooling to run tests and Playwright flows against Saucedemo. Inputs (argument-hint) - task (required): one of scaffold, verify, describe, create-sample-tests. - targetPath (optional): filesystem path where scaffolding should be created. Defaults to repository root. - installDeps (optional boolean): when true, agent will install Node dependencies and Playwright browsers. - force (optional boolean): when true, allow overwriting placeholder files (use with caution). - uiTarget (optional): URL to target for UI tests; defaults to https://www.saucedemo.com/. Capabilities / Behavior - Read the repo layout and create any missing directories listed in the standard layout. - Create placeholder files for missing source files (short header + file path + TODO notes). - Create Playwright scaffolding when requested, including: - a playwright.config.ts sample if missing, - a tests/ui/saucedemo.spec.ts sample test that logs in and asserts inventory load, - a ui/page page-object for the login and inventory pages, - package.json script recommendations for test, test:ui, and install:browsers. - Optionally install Node dependencies and Playwright browsers when installDeps=true. - Run verification commands (npm test, npx playwright test --list, npx playwright test) and return concise logs. Prerequisites - Node.js (v16+) and npm (or pnpm) installed. - Internet access for package installation and Playwright browser download. Representative scaffolded layout
|
|
15
|
+
package.json (sample scripts)
|
|
16
|
+
playwright.config.ts (generated if missing)
|
|
17
|
+
tests/
|
|
18
|
+
ui/
|
|
19
|
+
saucedemo.spec.ts # sample UI test
|
|
20
|
+
api/
|
|
21
|
+
schd-group-create.api.spec.ts
|
|
22
|
+
core/
|
|
23
|
+
api/
|
|
24
|
+
auth/
|
|
25
|
+
db/
|
|
26
|
+
ui/
|
|
27
|
+
page/
|
|
28
|
+
LoginPage.ts
|
|
29
|
+
InventoryPage.ts
|
|
30
|
+
features/
|
|
31
|
+
steps/
|
|
32
|
+
utils/
|
|
33
|
+
readJson.ts
|
|
34
|
+
playwright-report/
|
|
35
|
+
screenshots/
|
|
36
|
+
test-results/
|
|
37
|
+
Scaffold steps (task: scaffold) 1. Validate targetPath exists or create it. 2. Create the repository directory tree (only new directories created). 3. For each file present in the source repo, preserve content. For missing, create placeholders containing a header, responsibilities, and TODO notes. 4. When createSample=true or task=create-sample-tests, add Playwright scaffolding: - playwright.config.ts (simple config using chromium and testDir tests/ui). - tests/ui/saucedemo.spec.ts (sample test: navigate to uiTarget, perform login using standard Saucedemo credentials from sample data, assert inventory list visible). - ui/page/LoginPage.ts and ui/page/InventoryPage.ts page objects. - Update or create package.json scripts: test, test:ui, install:browsers. 5. If installDeps=true, run npm ci (or npm install) then npx playwright install (use --with-deps where appropriate). 6. Run verification: npm run test:ui -- --list or npx playwright test --list and optionally execute the sample test. Suggested commands (PowerShell / cross-platform)
|
|
38
|
+
powershell
|
|
39
|
+
# install dependencies
|
|
40
|
+
npm ci
|
|
41
|
+
# install Playwright browsers
|
|
42
|
+
npx playwright install
|
|
43
|
+
# run unit/api tests
|
|
44
|
+
npm test
|
|
45
|
+
# run Playwright E2E tests
|
|
46
|
+
npm run test:ui
|
|
47
|
+
Sample tests/ui/saucedemo.spec.ts behavior (generated when requested) - Open uiTarget (defaults to https://www.saucedemo.com/). - Use LoginPage to enter standard_user / secret_sauce and submit. - Wait for InventoryPage to display and assert at least one product is visible. Outputs - Created directories and placeholder files in targetPath. - Generated Playwright UI scaffolding and a runnable sample test when requested. - Verification logs for dependency install and sample test runs. - A summary list of placeholders and recommended edits (e.g., secrets, environment overrides). Safety & constraints - The agent will not overwrite existing non-placeholder files unless force=true is set. - The agent will not embed secrets into scaffolded files; secrets must be provided separately or via env vars. Example usages - Scaffold without installing deps: { "task": "scaffold", "installDeps": false } - Scaffold + install deps + create samples: { "task": "scaffold", "installDeps": true, "createSample": true } - Create only sample UI tests: { "task": "create-sample-tests", "targetPath": ".", "uiTarget": "https://www.saucedemo.com/" } - Describe repo: { "task": "describe" } Maintainer notes - Keep the generated playwright.config.ts and sample tests minimal — encourage contributors to migrate tests into the repository's existing structure and page objects. - When adding real credentials, instruct users to use environment variables or a secure secrets store. --- Automation agent specification end. Included file templates (drop-in ready) Below are copy-paste-ready templates for the key files you can generate from this agent to produce a runnable Playwright UI framework targeting https://www.saucedemo.com/. 1) package.json (merge with your existing package.json or use as a template)
|
|
48
|
+
json
|
|
49
49
|
{
|
|
50
|
+
"name": "poc_bbc",
|
|
51
|
+
"version": "1.0.0",
|
|
52
|
+
"type": "commonjs",
|
|
50
53
|
"scripts": {
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"test
|
|
54
|
-
"test:
|
|
55
|
-
"
|
|
56
|
-
"test:bdd": "npx @cucumber/cucumber-js",
|
|
57
|
-
"test:bdd:report": "npx @cucumber/cucumber-js --format json:cucumber-report.json"
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
### 4. TypeScript Configuration (`tsconfig.json`)
|
|
63
|
-
```json
|
|
64
|
-
{
|
|
65
|
-
"compilerOptions": {
|
|
66
|
-
"target": "ES2020",
|
|
67
|
-
"lib": ["ES2020"],
|
|
68
|
-
"module": "ESNext",
|
|
69
|
-
"moduleResolution": "node",
|
|
70
|
-
"esModuleInterop": true,
|
|
71
|
-
"allowSyntheticDefaultImports": true,
|
|
72
|
-
"strict": true,
|
|
73
|
-
"skipLibCheck": true,
|
|
74
|
-
"forceConsistentCasingInFileNames": true,
|
|
75
|
-
"resolveJsonModule": true,
|
|
76
|
-
"noEmit": true,
|
|
77
|
-
"declaration": true,
|
|
78
|
-
"declarationMap": true,
|
|
79
|
-
"sourceMap": true,
|
|
80
|
-
"baseUrl": ".",
|
|
81
|
-
"paths": {
|
|
82
|
-
"@pages/*": ["tests/pages/*"],
|
|
83
|
-
"@utils/*": ["tests/utils/*"],
|
|
84
|
-
"@fixtures/*": ["tests/fixtures/*"],
|
|
85
|
-
"@data/*": ["tests/data/*"]
|
|
86
|
-
}
|
|
87
|
-
},
|
|
88
|
-
"include": ["tests/**/*"],
|
|
89
|
-
"exclude": ["node_modules"]
|
|
90
|
-
}
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
### 5. Project Structure (Scalable with BDD & TypeScript)
|
|
94
|
-
```
|
|
95
|
-
project/
|
|
96
|
-
├── tests/
|
|
97
|
-
│ ├── fixtures/
|
|
98
|
-
│ │ └── base.fixture.ts # Shared fixtures (TypeScript)
|
|
99
|
-
│ ├── pages/
|
|
100
|
-
│ │ ├── BasePage.ts # Base page object (TypeScript)
|
|
101
|
-
│ │ ├── LoginPage.ts # Login page object (TypeScript)
|
|
102
|
-
│ │ ├── InventoryPage.ts # Inventory page object (TypeScript)
|
|
103
|
-
│ │ └── FormPage.ts # Form page object (TypeScript)
|
|
104
|
-
│ ├── utils/ # Utility functions (TypeScript)
|
|
105
|
-
│ │ ├── dataReader.ts
|
|
106
|
-
│ │ ├── formHelper.ts
|
|
107
|
-
│ │ └── testData.ts
|
|
108
|
-
│ ├── data/ # Test data files
|
|
109
|
-
│ │ ├── users.json
|
|
110
|
-
│ │ ├── formData.csv
|
|
111
|
-
│ │ └── config.yaml
|
|
112
|
-
│ ├── features/ # BDD Gherkin scenarios
|
|
113
|
-
│ │ ├── login.feature
|
|
114
|
-
│ │ └── shopping.feature
|
|
115
|
-
│ ├── steps/ # BDD Step definitions (TypeScript)
|
|
116
|
-
│ │ ├── loginSteps.ts
|
|
117
|
-
│ │ └── shoppingSteps.ts
|
|
118
|
-
│ └── specs/
|
|
119
|
-
│ ├── saucedemo.spec.ts # Traditional tests (TypeScript)
|
|
120
|
-
│ └── formTest.spec.ts
|
|
121
|
-
├── cucumber.js # Cucumber config
|
|
122
|
-
├── playwright.config.ts # Playwright config (TypeScript)
|
|
123
|
-
├── tsconfig.json # TypeScript config
|
|
124
|
-
└── package.json
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
### 6. Cucumber Configuration (`cucumber.js` - for BDD)
|
|
128
|
-
```javascript
|
|
129
|
-
module.exports = {
|
|
130
|
-
default: {
|
|
131
|
-
paths: ['tests/features/**/*.feature'],
|
|
132
|
-
require: ['tests/steps/**/*.ts'],
|
|
133
|
-
requireModule: ['ts-node/register'],
|
|
134
|
-
format: ['progress', 'html:cucumber-report.html'],
|
|
135
|
-
parallel: 4,
|
|
136
|
-
worldParameters: {
|
|
137
|
-
appUrl: 'https://www.saucedemo.com/',
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
};
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
### 7. Playwright Configuration (`playwright.config.ts`)
|
|
144
|
-
```typescript
|
|
145
|
-
import { defineConfig, devices, type PlaywrightTestOptions } from '@playwright/test';
|
|
146
|
-
|
|
147
|
-
export default defineConfig<PlaywrightTestOptions>({
|
|
148
|
-
testDir: './tests/specs',
|
|
149
|
-
fullyParallel: true,
|
|
150
|
-
workers: process.env.CI ? 1 : 4,
|
|
151
|
-
retries: 1,
|
|
152
|
-
timeout: 30000,
|
|
153
|
-
expect: {
|
|
154
|
-
timeout: 5000,
|
|
54
|
+
"dbtest": "npx playwright test --project=db",
|
|
55
|
+
"uitest": "npx bddgen & npx playwright test --project=ui --headed",
|
|
56
|
+
"test": "npx playwright test",
|
|
57
|
+
"test:ui": "npx playwright test --project=ui-plain",
|
|
58
|
+
"install:browsers": "npx playwright install"
|
|
155
59
|
},
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
trace: 'on-first-retry',
|
|
162
|
-
},
|
|
163
|
-
webServer: {
|
|
164
|
-
command: 'npm start',
|
|
165
|
-
url: 'http://localhost:3000',
|
|
166
|
-
reuseExistingServer: !process.env.CI,
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@playwright/test": "^1.58.2",
|
|
62
|
+
"playwright-bdd": "^8.4.2",
|
|
63
|
+
"ts-node": "^10.9.2",
|
|
64
|
+
"typescript": "^5.9.3"
|
|
167
65
|
},
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
66
|
+
"dependencies": {
|
|
67
|
+
"dotenv": "^17.2.3",
|
|
68
|
+
"mssql": "^12.2.0"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
2) playwright.config.ts (the repo already has a BDD config; this snippet adds a plain UI project)
|
|
72
|
+
typescript
|
|
73
|
+
import 'dotenv/config';
|
|
74
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
75
|
+
import { defineBddConfig } from 'playwright-bdd';
|
|
76
|
+
|
|
77
|
+
export const bddConfig = defineBddConfig({
|
|
78
|
+
features: ['tests/ui/features/**/*.feature'],
|
|
79
|
+
steps: ['tests/ui/steps/**/*.steps.ts','tests/fixtures/pages.fixture.ts'],
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export default defineConfig({
|
|
83
|
+
testDir: './tests',
|
|
84
|
+
timeout: 20 * 1000,
|
|
85
|
+
expect: { timeout: 10 * 1000 },
|
|
86
|
+
retries: process.env.CI ? 1 : 0,
|
|
87
|
+
reporter: [['html', { open: 'never' }], ['list']],
|
|
88
|
+
use: { trace: 'retain-on-failure', screenshot: 'only-on-failure', video: 'retain-on-failure' },
|
|
89
|
+
projects: [
|
|
90
|
+
{
|
|
91
|
+
name: 'ui',
|
|
92
|
+
testDir: './.features-gen',
|
|
93
|
+
testMatch: '**/*.feature.spec.*',
|
|
94
|
+
use: { ...devices['Desktop Chrome'], headless: true, baseURL: process.env.UI_BASE_URL },
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'api',
|
|
98
|
+
testMatch: /.*\.api\.spec\.ts/,
|
|
99
|
+
use: { browserName: undefined, baseURL: process.env.API_BASE_URL, extraHTTPHeaders: { 'Content-Type': 'application/json' } },
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'db',
|
|
103
|
+
testMatch: /.*\.db\.spec\.ts/,
|
|
104
|
+
use: { browserName: undefined },
|
|
105
|
+
},
|
|
106
|
+
// Plain UI tests (non-BDD)
|
|
107
|
+
{
|
|
108
|
+
name: 'ui-plain',
|
|
109
|
+
testDir: './tests/ui',
|
|
110
|
+
use: { ...devices['Desktop Chrome'], headless: true, baseURL: process.env.UI_BASE_URL || 'https://www.saucedemo.com/' },
|
|
111
|
+
},
|
|
171
112
|
],
|
|
172
113
|
});
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
```typescript
|
|
177
|
-
import fs from 'fs';
|
|
178
|
-
import path from 'path';
|
|
179
|
-
import csv from 'csv-parser';
|
|
180
|
-
import yaml from 'js-yaml';
|
|
181
|
-
|
|
182
|
-
const DATA_DIR = path.join(process.cwd(), 'tests', 'data');
|
|
183
|
-
|
|
184
|
-
interface DataRecord {
|
|
185
|
-
[key: string]: string | number | boolean;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export class DataReader {
|
|
189
|
-
// Read JSON file
|
|
190
|
-
static readJSON<T = DataRecord>(filename: string): T {
|
|
191
|
-
const filePath = path.join(DATA_DIR, filename);
|
|
192
|
-
const data = fs.readFileSync(filePath, 'utf-8');
|
|
193
|
-
return JSON.parse(data) as T;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Read YAML file
|
|
197
|
-
static readYAML<T = DataRecord>(filename: string): T {
|
|
198
|
-
const filePath = path.join(DATA_DIR, filename);
|
|
199
|
-
const data = fs.readFileSync(filePath, 'utf-8');
|
|
200
|
-
return yaml.load(data) as T;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Read CSV file (returns array of objects)
|
|
204
|
-
static async readCSV(filename: string): Promise<DataRecord[]> {
|
|
205
|
-
const filePath = path.join(DATA_DIR, filename);
|
|
206
|
-
return new Promise<DataRecord[]>((resolve, reject) => {
|
|
207
|
-
const records: DataRecord[] = [];
|
|
208
|
-
fs.createReadStream(filePath)
|
|
209
|
-
.pipe(csv())
|
|
210
|
-
.on('data', (data: DataRecord) => records.push(data))
|
|
211
|
-
.on('end', () => resolve(records))
|
|
212
|
-
.on('error', reject);
|
|
213
|
-
});
|
|
214
|
-
}
|
|
114
|
+
3) ui/page/LoginPage.ts (page object)
|
|
115
|
+
typescript
|
|
116
|
+
import { Page } from '@playwright/test';
|
|
215
117
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
return undefined;
|
|
118
|
+
export class LoginPage {
|
|
119
|
+
readonly page: Page;
|
|
120
|
+
constructor(page: Page) { this.page = page; }
|
|
121
|
+
async goto() { await this.page.goto('/'); }
|
|
122
|
+
async login(username: string, password: string) {
|
|
123
|
+
await this.page.fill('#user-name', username);
|
|
124
|
+
await this.page.fill('#password', password);
|
|
125
|
+
await this.page.click('#login-button');
|
|
226
126
|
}
|
|
227
127
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
### 9. Form Helper Utility (`tests/utils/formHelper.ts`) - TypeScript
|
|
231
|
-
```typescript
|
|
128
|
+
4) ui/page/InventoryPage.ts (page object)
|
|
129
|
+
typescript
|
|
232
130
|
import { Page, Locator } from '@playwright/test';
|
|
233
131
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
132
|
+
export class InventoryPage {
|
|
133
|
+
readonly page: Page;
|
|
134
|
+
readonly items: Locator;
|
|
135
|
+
constructor(page: Page) { this.page = page; this.items = page.locator('.inventory_item'); }
|
|
136
|
+
async isLoaded(): Promise<boolean> { return await this.items.first().isVisible(); }
|
|
137
|
+
getItems(): Locator { return this.items; }
|
|
238
138
|
}
|
|
139
|
+
5) tests/ui/saucedemo.spec.ts (plain Playwright test)
|
|
140
|
+
typescript
|
|
141
|
+
import { test, expect } from '@playwright/test';
|
|
142
|
+
import { LoginPage } from '../../ui/page/LoginPage';
|
|
143
|
+
import { InventoryPage } from '../../ui/page/InventoryPage';
|
|
144
|
+
|
|
145
|
+
test.describe('Saucedemo smoke', () => {
|
|
146
|
+
test('login and inventory visible', async ({ page, baseURL }) => {
|
|
147
|
+
const login = new LoginPage(page);
|
|
148
|
+
const inventory = new InventoryPage(page);
|
|
149
|
+
await page.goto(baseURL || 'https://www.saucedemo.com/');
|
|
150
|
+
await login.login('standard_user', 'secret_sauce');
|
|
151
|
+
await expect(page.locator('.inventory_list')).toBeVisible();
|
|
152
|
+
await expect(inventory.getItems().first()).toBeVisible();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
6) .env.example (environment variables sample)
|
|
156
|
+
text
|
|
157
|
+
UI_BASE_URL=https://www.saucedemo.com/
|
|
158
|
+
API_BASE_URL=http://localhost:3000
|
|
159
|
+
Usage notes - The templates above are ready to paste into files. The agent's scaffold task may create them automatically when createSample=true. - To run locally after creating files:
|
|
160
|
+
powershell
|
|
161
|
+
npm ci
|
|
162
|
+
npm run install:browsers
|
|
163
|
+
npm run test:ui
|
|
164
|
+
- The test uses Saucedemo public credentials: standard_user / secret_sauce. If you want, I can now: (choose one) - Generate these files automatically in the repo (I will create the files listed above). - Or leave this agent file as the canonical template and not modify the filesystem. Repository file list and embedded key file contents Below is a repository file list (captured from the workspace) followed by the full contents of selected key files (UI, tests, core helpers). Use these as drop-in references when running the agent's create/scaffold actions. File list (workspace snapshot):
|
|
165
|
+
c:\poc_bbc\workflows\schd-group\api\schd-group.api.ts
|
|
166
|
+
c:\poc_bbc\workflows\schd-group\invariants\db.invariants.ts
|
|
167
|
+
c:\poc_bbc\workflows\schd-group\invariants\api.invariants.ts
|
|
168
|
+
c:\poc_bbc\workflows\schd-group\invariants\permission.invariants.ts
|
|
169
|
+
c:\poc_bbc\core\permission\permission.types.ts
|
|
170
|
+
c:\poc_bbc\core\permission\permission.schd-group.ts
|
|
171
|
+
c:\poc_bbc\workflows\schd-group\db\schd-group.queries.ts
|
|
172
|
+
c:\poc_bbc\core\auth\roles.ts
|
|
173
|
+
c:\poc_bbc\core\db\queries.ts
|
|
174
|
+
c:\poc_bbc\core\db\connection.ts
|
|
175
|
+
c:\poc_bbc\tsconfig.json
|
|
176
|
+
c:\poc_bbc\core\api\apiClient.ts
|
|
177
|
+
c:\poc_bbc\playwright.config.ts
|
|
178
|
+
c:\poc_bbc\package.json
|
|
179
|
+
c:\poc_bbc\package-lock.json
|
|
180
|
+
c:\poc_bbc\bitbucket-pipelines.yml
|
|
181
|
+
c:\poc_bbc\.gitignore
|
|
182
|
+
c:\poc_bbc\.github\agents\automation.md.agent.md
|
|
183
|
+
c:\poc_bbc\tests\utils\readJson.ts
|
|
184
|
+
c:\poc_bbc\tests\utils\formFiller.ts
|
|
185
|
+
c:\poc_bbc\tests\fixtures\test.fixture.ts
|
|
186
|
+
c:\poc_bbc\tests\fixtures\pages.fixture.ts
|
|
187
|
+
c:\poc_bbc\tests\types\formField.ts
|
|
188
|
+
c:\poc_bbc\tests\ui\steps\scheduling-groups.steps.ts
|
|
189
|
+
c:\poc_bbc\tests\ui\steps\booking.steps.ts
|
|
190
|
+
c:\poc_bbc\tests\ui\features\scheduling-groups.feature
|
|
191
|
+
c:\poc_bbc\tests\ui\features\booking.feature
|
|
192
|
+
c:\poc_bbc\tests\ui\page\HomePage.ts
|
|
193
|
+
c:\poc_bbc\tests\db\01-connection.db.spec.ts
|
|
194
|
+
c:\poc_bbc\tests\api\schd-group-create.api.spec.ts
|
|
195
|
+
c:\poc_bbc\.bitbucket\README.md
|
|
196
|
+
c:\poc_bbc\.bitbucket\pull_request_template.md
|
|
197
|
+
c:\poc_bbc\tests\data\facilityFormData.json
|
|
198
|
+
Embedded key file contents 1) package.json
|
|
199
|
+
json
|
|
200
|
+
{
|
|
201
|
+
"name": "poc_bbc",
|
|
202
|
+
"version": "1.0.0",
|
|
203
|
+
"description": "",
|
|
204
|
+
"main": "index.js",
|
|
205
|
+
"scripts": {
|
|
206
|
+
"dbtest": "npx playwright test --project=db",
|
|
207
|
+
"uitest": "npx bddgen & npx playwright test --project=ui --headed"
|
|
239
208
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
await this.page.click(`text="${optionText}"`);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Select radio button by label text
|
|
261
|
-
async selectRadioButton(labelText: string): Promise<void> {
|
|
262
|
-
const label = this.page.locator(`label:has-text("${labelText}")`);
|
|
263
|
-
const radioId = await label.getAttribute('for');
|
|
264
|
-
if (radioId) {
|
|
265
|
-
await this.page.locator(`#${radioId}`).click();
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Check checkbox by label text
|
|
270
|
-
async checkCheckbox(labelText: string): Promise<void> {
|
|
271
|
-
const label = this.page.locator(`label:has-text("${labelText}")`);
|
|
272
|
-
const checkboxId = await label.getAttribute('for');
|
|
273
|
-
if (checkboxId) {
|
|
274
|
-
const checkbox = this.page.locator(`#${checkboxId}`);
|
|
275
|
-
const isChecked = await checkbox.isChecked();
|
|
276
|
-
if (!isChecked) {
|
|
277
|
-
await checkbox.click();
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Uncheck checkbox by label text
|
|
283
|
-
async uncheckCheckbox(labelText: string): Promise<void> {
|
|
284
|
-
const label = this.page.locator(`label:has-text("${labelText}")`);
|
|
285
|
-
const checkboxId = await label.getAttribute('for');
|
|
286
|
-
if (checkboxId) {
|
|
287
|
-
const checkbox = this.page.locator(`#${checkboxId}`);
|
|
288
|
-
const isChecked = await checkbox.isChecked();
|
|
289
|
-
if (isChecked) {
|
|
290
|
-
await checkbox.click();
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Select multiple checkboxes
|
|
296
|
-
async checkMultiple(labels: string[]): Promise<void> {
|
|
297
|
-
for (const label of labels) {
|
|
298
|
-
await this.checkCheckbox(label);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// Fill form with object data
|
|
303
|
-
async fillForm(formData: Record<string, FormFieldConfig>): Promise<void> {
|
|
304
|
-
for (const field of Object.values(formData)) {
|
|
305
|
-
if (field.type === 'text' && field.selector && typeof field.value === 'string') {
|
|
306
|
-
await this.fillInput(field.selector, field.value);
|
|
307
|
-
} else if (field.type === 'textarea' && field.selector && typeof field.value === 'string') {
|
|
308
|
-
await this.fillTextArea(field.selector, field.value);
|
|
309
|
-
} else if (field.type === 'dropdown' && field.selector && typeof field.value === 'string') {
|
|
310
|
-
await this.selectDropdown(field.selector, field.value);
|
|
311
|
-
} else if (field.type === 'radio' && typeof field.value === 'string') {
|
|
312
|
-
await this.selectRadioButton(field.value);
|
|
313
|
-
} else if (field.type === 'checkbox' && field.value) {
|
|
314
|
-
if (Array.isArray(field.value)) {
|
|
315
|
-
await this.checkMultiple(field.value);
|
|
316
|
-
} else if (typeof field.value === 'string') {
|
|
317
|
-
await this.checkCheckbox(field.value);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// Verify form value
|
|
324
|
-
async getInputValue(selector: string): Promise<string | null> {
|
|
325
|
-
return await this.page.inputValue(selector);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Verify radio button is selected
|
|
329
|
-
async isRadioSelected(labelText: string): Promise<boolean> {
|
|
330
|
-
const label = this.page.locator(`label:has-text("${labelText}")`);
|
|
331
|
-
const radioId = await label.getAttribute('for');
|
|
332
|
-
return radioId ? await this.page.locator(`#${radioId}`).isChecked() : false;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Verify checkbox is checked
|
|
336
|
-
async isCheckboxChecked(labelText: string): Promise<boolean> {
|
|
337
|
-
const label = this.page.locator(`label:has-text("${labelText}")`);
|
|
338
|
-
const checkboxId = await label.getAttribute('for');
|
|
339
|
-
return checkboxId ? await this.page.locator(`#${checkboxId}`).isChecked() : false;
|
|
209
|
+
|
|
210
|
+
},
|
|
211
|
+
"keywords": [],
|
|
212
|
+
"author": "",
|
|
213
|
+
"license": "ISC",
|
|
214
|
+
"type": "commonjs",
|
|
215
|
+
"devDependencies": {
|
|
216
|
+
"@playwright/test": "^1.58.2",
|
|
217
|
+
"@types/mssql": "^9.1.9",
|
|
218
|
+
"@types/node": "^25.2.3",
|
|
219
|
+
"playwright-bdd": "^8.4.2",
|
|
220
|
+
"ts-node": "^10.9.2",
|
|
221
|
+
"typescript": "^5.9.3"
|
|
222
|
+
},
|
|
223
|
+
"dependencies": {
|
|
224
|
+
"dotenv": "^17.2.3",
|
|
225
|
+
"mssql": "^12.2.0"
|
|
340
226
|
}
|
|
341
227
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
import {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
interface FormData {
|
|
358
|
-
scenario: string;
|
|
359
|
-
firstName: string;
|
|
360
|
-
lastName: string;
|
|
361
|
-
email: string;
|
|
362
|
-
country: string;
|
|
363
|
-
acceptTerms: string;
|
|
364
|
-
newsletter: string;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
interface Config {
|
|
368
|
-
app: {
|
|
369
|
-
url: string;
|
|
370
|
-
timeout: number;
|
|
371
|
-
};
|
|
372
|
-
users: {
|
|
373
|
-
[key: string]: {
|
|
374
|
-
username: string;
|
|
375
|
-
password: string;
|
|
376
|
-
};
|
|
377
|
-
};
|
|
378
|
-
forms: {
|
|
379
|
-
[key: string]: {
|
|
380
|
-
[field: string]: string;
|
|
381
|
-
};
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
export class TestDataManager {
|
|
386
|
-
// Load user data from JSON
|
|
387
|
-
static loadUsers(): { users: User[] } {
|
|
388
|
-
return DataReader.readJSON<{ users: User[] }>('users.json');
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// Load form data from CSV
|
|
392
|
-
static async loadFormData(): Promise<FormData[]> {
|
|
393
|
-
return await DataReader.readCSV('formData.csv') as unknown as FormData[];
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// Load config from YAML
|
|
397
|
-
static loadConfig(): Config {
|
|
398
|
-
return DataReader.readYAML<Config>('config.yaml');
|
|
399
|
-
}
|
|
228
|
+
2) playwright.config.ts
|
|
229
|
+
typescript
|
|
230
|
+
import 'dotenv/config';
|
|
231
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
232
|
+
import { defineBddConfig } from 'playwright-bdd';
|
|
233
|
+
|
|
234
|
+
// BDD config for UI tests
|
|
235
|
+
export const bddConfig = defineBddConfig({
|
|
236
|
+
features: ['tests/ui/features/**/*.feature'],
|
|
237
|
+
steps: [
|
|
238
|
+
'tests/ui/steps/**/*.steps.ts',
|
|
239
|
+
'tests/fixtures/pages.fixture.ts',
|
|
240
|
+
],
|
|
241
|
+
});
|
|
400
242
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
243
|
+
export default defineConfig({
|
|
244
|
+
testDir: './tests',
|
|
245
|
+
timeout: 20 * 1000,
|
|
246
|
+
expect: {
|
|
247
|
+
timeout: 10 * 1000,
|
|
248
|
+
},
|
|
249
|
+
retries: process.env.CI ? 1 : 0,
|
|
250
|
+
reporter: [
|
|
251
|
+
['html', { open: 'never' }],
|
|
252
|
+
['list'],
|
|
253
|
+
],
|
|
254
|
+
use: {
|
|
255
|
+
trace: 'retain-on-failure',
|
|
256
|
+
screenshot: 'only-on-failure',
|
|
257
|
+
video: 'retain-on-failure'
|
|
258
|
+
},
|
|
406
259
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
260
|
+
projects: [
|
|
261
|
+
// =======================
|
|
262
|
+
// UI BDD TESTS
|
|
263
|
+
// =======================
|
|
264
|
+
{
|
|
265
|
+
name: 'ui',
|
|
266
|
+
testDir: './.features-gen', // folder where Playwright-BDD generates .feature.spec.ts
|
|
267
|
+
testMatch: '**/*.feature.spec.*', // match .ts or .js files
|
|
268
|
+
use: {
|
|
269
|
+
...devices['Desktop Chrome'],
|
|
270
|
+
headless: true,
|
|
271
|
+
baseURL: process.env.UI_BASE_URL,
|
|
272
|
+
},
|
|
414
273
|
|
|
415
|
-
|
|
274
|
+
},
|
|
416
275
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
"users": [
|
|
276
|
+
// =======================
|
|
277
|
+
// API TESTS
|
|
278
|
+
// =======================
|
|
421
279
|
{
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
280
|
+
name: 'api',
|
|
281
|
+
testMatch: /.*\.api\.spec\.ts/,
|
|
282
|
+
use: {
|
|
283
|
+
browserName: undefined,
|
|
284
|
+
baseURL: process.env.API_BASE_URL,
|
|
285
|
+
extraHTTPHeaders: {
|
|
286
|
+
'Content-Type': 'application/json',
|
|
287
|
+
},
|
|
288
|
+
},
|
|
428
289
|
},
|
|
429
|
-
{
|
|
430
|
-
"id": "2",
|
|
431
|
-
"username": "problem_user",
|
|
432
|
-
"password": "secret_sauce",
|
|
433
|
-
"email": "problem@example.com",
|
|
434
|
-
"firstName": "Jane",
|
|
435
|
-
"lastName": "Smith"
|
|
436
|
-
}
|
|
437
|
-
]
|
|
438
|
-
}
|
|
439
|
-
```
|
|
440
|
-
|
|
441
|
-
**`tests/data/formData.csv`**
|
|
442
|
-
```csv
|
|
443
|
-
scenario,firstName,lastName,email,country,acceptTerms,newsletter
|
|
444
|
-
registration_valid,John,Doe,john@example.com,USA,true,true
|
|
445
|
-
registration_minimal,Jane,Smith,jane@example.com,Canada,true,false
|
|
446
|
-
registration_eu,Pierre,Martin,pierre@example.com,France,true,true
|
|
447
|
-
```
|
|
448
|
-
|
|
449
|
-
**`tests/data/config.yaml`**
|
|
450
|
-
```yaml
|
|
451
|
-
app:
|
|
452
|
-
url: https://www.saucedemo.com/
|
|
453
|
-
timeout: 30000
|
|
454
|
-
|
|
455
|
-
users:
|
|
456
|
-
standard:
|
|
457
|
-
username: standard_user
|
|
458
|
-
password: secret_sauce
|
|
459
|
-
admin:
|
|
460
|
-
username: admin
|
|
461
|
-
password: admin123
|
|
462
|
-
|
|
463
|
-
forms:
|
|
464
|
-
registration:
|
|
465
|
-
firstName: '#firstName'
|
|
466
|
-
lastName: '#lastName'
|
|
467
|
-
email: '#email'
|
|
468
|
-
country: '.country-dropdown'
|
|
469
|
-
acceptTerms: '#terms'
|
|
470
|
-
newsletter: '#newsletter'
|
|
471
|
-
|
|
472
|
-
```
|
|
473
|
-
|
|
474
|
-
### 10. Enhanced Page Object with Form Helper (`tests/pages/FormPage.js`)
|
|
475
|
-
```javascript
|
|
476
|
-
import { BasePage } from './BasePage';
|
|
477
|
-
import { FormHelper } from '../utils/formHelper.js';
|
|
478
|
-
import { TestDataManager } from '../utils/testData.js';
|
|
479
|
-
|
|
480
|
-
export class FormPage extends BasePage {
|
|
481
|
-
constructor(page) {
|
|
482
|
-
super(page);
|
|
483
|
-
this.formHelper = new FormHelper(page);
|
|
484
|
-
this.config = TestDataManager.loadConfig();
|
|
485
|
-
}
|
|
486
290
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
},
|
|
496
|
-
lastName: {
|
|
497
|
-
type: 'text',
|
|
498
|
-
selector: this.config.forms.registration.lastName,
|
|
499
|
-
value: formData.lastName
|
|
500
|
-
},
|
|
501
|
-
email: {
|
|
502
|
-
type: 'text',
|
|
503
|
-
selector: this.config.forms.registration.email,
|
|
504
|
-
value: formData.email
|
|
505
|
-
},
|
|
506
|
-
country: {
|
|
507
|
-
type: 'dropdown',
|
|
508
|
-
selector: this.config.forms.registration.country,
|
|
509
|
-
value: formData.country
|
|
510
|
-
},
|
|
511
|
-
acceptTerms: {
|
|
512
|
-
type: 'checkbox',
|
|
513
|
-
value: 'I accept the terms'
|
|
291
|
+
// =======================
|
|
292
|
+
// DB TESTS (NO BROWSER)
|
|
293
|
+
// =======================
|
|
294
|
+
{
|
|
295
|
+
name: 'db',
|
|
296
|
+
testMatch: /.*\.db\.spec\.ts/,
|
|
297
|
+
use: {
|
|
298
|
+
browserName: undefined,
|
|
514
299
|
},
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
for (const field of Object.values(formConfig)) {
|
|
522
|
-
if (field.value === null) continue;
|
|
523
|
-
|
|
524
|
-
if (field.type === 'text') {
|
|
525
|
-
await this.formHelper.fillInput(field.selector, field.value);
|
|
526
|
-
} else if (field.type === 'dropdown') {
|
|
527
|
-
await this.formHelper.selectDropdown(field.selector, field.value);
|
|
528
|
-
} else if (field.type === 'checkbox') {
|
|
529
|
-
await this.formHelper.checkCheckbox(field.value);
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
```
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
});
|
|
303
|
+
3) core/api/apiClient.ts
|
|
304
|
+
typescript
|
|
305
|
+
import type { APIRequestContext, APIResponse } from "@playwright/test";
|
|
535
306
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
export class FormPage extends BasePage {
|
|
544
|
-
readonly formHelper: FormHelper;
|
|
545
|
-
private config: any;
|
|
546
|
-
|
|
547
|
-
constructor(page: Page) {
|
|
548
|
-
super(page);
|
|
549
|
-
this.formHelper = new FormHelper(page);
|
|
550
|
-
this.config = TestDataManager.loadConfig();
|
|
551
|
-
}
|
|
307
|
+
export const apiGet = (
|
|
308
|
+
request: APIRequestContext,
|
|
309
|
+
url: string,
|
|
310
|
+
): Promise<APIResponse> => {
|
|
311
|
+
return request.get(url);
|
|
312
|
+
};
|
|
552
313
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
314
|
+
export const apiPost = <T>(
|
|
315
|
+
request: APIRequestContext,
|
|
316
|
+
url: string,
|
|
317
|
+
payload?: T,
|
|
318
|
+
): Promise<APIResponse> => {
|
|
319
|
+
return request.post(url, { data: payload });
|
|
320
|
+
};
|
|
556
321
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
lastName: {
|
|
564
|
-
type: 'text' as const,
|
|
565
|
-
selector: this.config.forms.registration.lastName,
|
|
566
|
-
value: formData.lastName
|
|
567
|
-
},
|
|
568
|
-
email: {
|
|
569
|
-
type: 'text' as const,
|
|
570
|
-
selector: this.config.forms.registration.email,
|
|
571
|
-
value: formData.email
|
|
572
|
-
},
|
|
573
|
-
country: {
|
|
574
|
-
type: 'dropdown' as const,
|
|
575
|
-
selector: this.config.forms.registration.country,
|
|
576
|
-
value: formData.country
|
|
577
|
-
},
|
|
578
|
-
acceptTerms: {
|
|
579
|
-
type: 'checkbox' as const,
|
|
580
|
-
value: 'I accept the terms'
|
|
581
|
-
},
|
|
582
|
-
newsletter: {
|
|
583
|
-
type: 'checkbox' as const,
|
|
584
|
-
value: formData.newsletter === 'true' ? 'Subscribe to newsletter' : null
|
|
585
|
-
}
|
|
586
|
-
};
|
|
587
|
-
|
|
588
|
-
for (const field of Object.values(formConfig)) {
|
|
589
|
-
if (field.value === null) continue;
|
|
590
|
-
|
|
591
|
-
if (field.type === 'text') {
|
|
592
|
-
await this.formHelper.fillInput(field.selector!, field.value as string);
|
|
593
|
-
} else if (field.type === 'dropdown') {
|
|
594
|
-
await this.formHelper.selectDropdown(field.selector!, field.value as string);
|
|
595
|
-
} else if (field.type === 'checkbox') {
|
|
596
|
-
await this.formHelper.checkCheckbox(field.value as string);
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
```
|
|
602
|
-
|
|
603
|
-
### 12. Fixtures Setup (`tests/fixtures/base.fixture.ts`) - TypeScript
|
|
604
|
-
```typescript
|
|
605
|
-
import { test as base, expect } from '@playwright/test';
|
|
606
|
-
import { LoginPage } from '@pages/LoginPage';
|
|
607
|
-
import { InventoryPage } from '@pages/InventoryPage';
|
|
608
|
-
import { FormPage } from '@pages/FormPage';
|
|
609
|
-
|
|
610
|
-
type AuthFixtures = {
|
|
611
|
-
loginPage: LoginPage;
|
|
612
|
-
inventoryPage: InventoryPage;
|
|
613
|
-
formPage: FormPage;
|
|
614
|
-
authenticatedUser: { page: any; loginPage: LoginPage };
|
|
322
|
+
export const apiPut = <T>(
|
|
323
|
+
request: APIRequestContext,
|
|
324
|
+
url: string,
|
|
325
|
+
payload?: T,
|
|
326
|
+
): Promise<APIResponse> => {
|
|
327
|
+
return request.put(url, { data: payload });
|
|
615
328
|
};
|
|
616
329
|
|
|
617
|
-
export const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
330
|
+
export const apiDelete = (
|
|
331
|
+
request: APIRequestContext,
|
|
332
|
+
url: string,
|
|
333
|
+
): Promise<APIResponse> => {
|
|
334
|
+
return request.delete(url);
|
|
335
|
+
};
|
|
336
|
+
4) core/db/connection.ts
|
|
337
|
+
typescript
|
|
338
|
+
import sql from 'mssql';
|
|
339
|
+
|
|
340
|
+
const config: sql.config = {
|
|
341
|
+
server: process.env.DB_HOST!,
|
|
342
|
+
database: process.env.DB_NAME!,
|
|
343
|
+
user: process.env.DB_USER,
|
|
344
|
+
password: process.env.DB_PASSWORD,
|
|
345
|
+
options: {
|
|
346
|
+
trustServerCertificate: true,
|
|
621
347
|
},
|
|
348
|
+
};
|
|
622
349
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
350
|
+
export async function getDbPool(): Promise<sql.ConnectionPool> {
|
|
351
|
+
return sql.connect(config);
|
|
352
|
+
}
|
|
353
|
+
5) core/db/queries.ts
|
|
354
|
+
typescript
|
|
355
|
+
import type { ConnectionPool } from 'mssql';
|
|
356
|
+
|
|
357
|
+
export async function listTables(db: ConnectionPool) {
|
|
358
|
+
const result = await db.request().query(`
|
|
359
|
+
SELECT TABLE_SCHEMA, TABLE_NAME
|
|
360
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
361
|
+
WHERE TABLE_TYPE = 'BASE TABLE'
|
|
362
|
+
`);
|
|
363
|
+
|
|
364
|
+
return result.recordset;
|
|
365
|
+
}
|
|
366
|
+
6) core/auth/roles.ts
|
|
367
|
+
typescript
|
|
368
|
+
export enum UserRole {
|
|
369
|
+
SYSTEM_ADMIN = 'SYSTEM_ADMIN',
|
|
370
|
+
AREA_ADMIN = 'AREA_ADMIN',
|
|
371
|
+
VIEW_ONLY = 'VIEW_ONLY',
|
|
372
|
+
}
|
|
373
|
+
7) core/permission/permission.types.ts
|
|
374
|
+
typescript
|
|
375
|
+
export enum Scope {
|
|
376
|
+
ANY = 'ANY',
|
|
377
|
+
OWN_AREA = 'OWN_AREA',
|
|
378
|
+
NONE = 'NONE',
|
|
379
|
+
}
|
|
380
|
+
8) core/permission/permission.schd-group.ts
|
|
381
|
+
typescript
|
|
382
|
+
import { UserRole } from '../auth/roles';
|
|
383
|
+
import { Scope } from './permission.types';
|
|
384
|
+
|
|
385
|
+
export type SchedulingGroupPermissions = {
|
|
386
|
+
view: Scope;
|
|
387
|
+
create: Scope;
|
|
388
|
+
edit: Scope;
|
|
389
|
+
delete: Scope;
|
|
390
|
+
};
|
|
627
391
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
392
|
+
export const schedulingGroupPermissions: Record<
|
|
393
|
+
UserRole,
|
|
394
|
+
SchedulingGroupPermissions
|
|
395
|
+
> = {
|
|
396
|
+
[UserRole.SYSTEM_ADMIN]: {
|
|
397
|
+
view: Scope.ANY,
|
|
398
|
+
create: Scope.ANY,
|
|
399
|
+
edit: Scope.ANY,
|
|
400
|
+
delete: Scope.ANY,
|
|
631
401
|
},
|
|
632
402
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
403
|
+
[UserRole.AREA_ADMIN]: {
|
|
404
|
+
view: Scope.OWN_AREA,
|
|
405
|
+
create: Scope.OWN_AREA,
|
|
406
|
+
edit: Scope.OWN_AREA,
|
|
407
|
+
delete: Scope.NONE,
|
|
637
408
|
},
|
|
638
|
-
});
|
|
639
|
-
|
|
640
|
-
export { expect };
|
|
641
|
-
```
|
|
642
409
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
async getLocator(selector: string): Locator {
|
|
671
|
-
return this.page.locator(selector);
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
async waitForElement(selector: string, timeout: number = 5000): Promise<void> {
|
|
675
|
-
await this.page.waitForSelector(selector, { timeout });
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
async isVisible(selector: string): Promise<boolean> {
|
|
679
|
-
return await this.page.locator(selector).isVisible();
|
|
680
|
-
}
|
|
410
|
+
[UserRole.VIEW_ONLY]: {
|
|
411
|
+
view: Scope.OWN_AREA,
|
|
412
|
+
create: Scope.NONE,
|
|
413
|
+
edit: Scope.NONE,
|
|
414
|
+
delete: Scope.NONE,
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
9) workflows/schd-group/api/schd-group.api.ts
|
|
418
|
+
typescript
|
|
419
|
+
import { APIRequestContext } from '@playwright/test';
|
|
420
|
+
import {
|
|
421
|
+
apiGet,
|
|
422
|
+
apiPost,
|
|
423
|
+
apiPut,
|
|
424
|
+
apiDelete,
|
|
425
|
+
} from '../../../core/api/apiClient';
|
|
426
|
+
|
|
427
|
+
export function createSchedulingGroup(
|
|
428
|
+
api: APIRequestContext,
|
|
429
|
+
payload: {
|
|
430
|
+
area: string;
|
|
431
|
+
groupName: string;
|
|
432
|
+
allocationsMenu?: boolean;
|
|
433
|
+
notes?: string;
|
|
434
|
+
}
|
|
435
|
+
) {
|
|
436
|
+
return apiPost(api, '/scheduling-groups', payload);
|
|
681
437
|
}
|
|
682
|
-
```
|
|
683
|
-
|
|
684
|
-
### 14. Page Object Model (`tests/pages/LoginPage.ts`) - TypeScript
|
|
685
|
-
```typescript
|
|
686
|
-
import { BasePage } from './BasePage';
|
|
687
|
-
|
|
688
|
-
export class LoginPage extends BasePage {
|
|
689
|
-
private readonly usernameField = '[data-test="username"]';
|
|
690
|
-
private readonly passwordField = '[data-test="password"]';
|
|
691
|
-
private readonly loginButton = '[data-test="login-button"]';
|
|
692
|
-
private readonly appUrl = 'https://www.saucedemo.com/';
|
|
693
438
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
async isLoaded(): Promise<boolean> {
|
|
706
|
-
return await this.page.locator(this.usernameField).isVisible();
|
|
707
|
-
}
|
|
439
|
+
export function editSchedulingGroup(
|
|
440
|
+
api: APIRequestContext,
|
|
441
|
+
id: number,
|
|
442
|
+
payload: {
|
|
443
|
+
area?: string;
|
|
444
|
+
groupName?: string;
|
|
445
|
+
allocationsMenu?: boolean;
|
|
446
|
+
notes?: string;
|
|
447
|
+
}
|
|
448
|
+
) {
|
|
449
|
+
return apiPut(api, `/scheduling-groups/${id}`, payload);
|
|
708
450
|
}
|
|
709
|
-
```
|
|
710
|
-
|
|
711
|
-
### 15. Inventory Page (`tests/pages/InventoryPage.ts`) - TypeScript
|
|
712
|
-
```typescript
|
|
713
|
-
import { BasePage } from './BasePage';
|
|
714
|
-
|
|
715
|
-
export class InventoryPage extends BasePage {
|
|
716
|
-
private readonly cartBadge = '[data-test="shopping-cart-badge"]';
|
|
717
|
-
private readonly addToCartBtn = '[data-test="add-to-cart-sauce-labs-backpack"]';
|
|
718
|
-
private readonly menuButton = '[data-test="bm-menu-button"]';
|
|
719
|
-
private readonly logoutLink = '[data-test="logout-sidebar-link"]';
|
|
720
|
-
|
|
721
|
-
async addProductToCart(): Promise<void> {
|
|
722
|
-
await this.click(this.addToCartBtn);
|
|
723
|
-
}
|
|
724
451
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
return await this.getText(this.cartBadge);
|
|
732
|
-
}
|
|
452
|
+
export function deleteSchedulingGroup(
|
|
453
|
+
api: APIRequestContext,
|
|
454
|
+
id: number
|
|
455
|
+
) {
|
|
456
|
+
return apiDelete(api, `/scheduling-groups/${id}`);
|
|
457
|
+
}
|
|
733
458
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
459
|
+
export function listSchedulingGroups(
|
|
460
|
+
api: APIRequestContext
|
|
461
|
+
) {
|
|
462
|
+
return apiGet(api, '/scheduling-groups');
|
|
463
|
+
}
|
|
464
|
+
10) workflows/schd-group/db/schd-group.queries.ts
|
|
465
|
+
typescript
|
|
466
|
+
import sql from 'mssql';
|
|
467
|
+
|
|
468
|
+
export class SchedulingGroupQueries {
|
|
469
|
+
static async getByName(
|
|
470
|
+
db: sql.ConnectionPool,
|
|
471
|
+
name: string
|
|
472
|
+
) {
|
|
473
|
+
const result = await db
|
|
474
|
+
.request()
|
|
475
|
+
.input('name', sql.VarChar, name)
|
|
476
|
+
.query(`
|
|
477
|
+
SELECT *
|
|
478
|
+
FROM scheduling_groups
|
|
479
|
+
WHERE scheduling_group_name = @name
|
|
480
|
+
`);
|
|
481
|
+
|
|
482
|
+
return result.recordset[0];
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
static async getById(
|
|
486
|
+
db: sql.ConnectionPool,
|
|
487
|
+
id: number
|
|
488
|
+
) {
|
|
489
|
+
const result = await db
|
|
490
|
+
.request()
|
|
491
|
+
.input('id', sql.Int, id)
|
|
492
|
+
.query(`
|
|
493
|
+
SELECT *
|
|
494
|
+
FROM scheduling_groups
|
|
495
|
+
WHERE id = @id
|
|
496
|
+
`);
|
|
497
|
+
|
|
498
|
+
return result.recordset[0];
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
static async exists(
|
|
502
|
+
db: sql.ConnectionPool,
|
|
503
|
+
name: string
|
|
504
|
+
): Promise<boolean> {
|
|
505
|
+
const record = await this.getByName(db, name);
|
|
506
|
+
return !!record;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
static async listByArea(
|
|
510
|
+
db: sql.ConnectionPool,
|
|
511
|
+
area: string
|
|
512
|
+
) {
|
|
513
|
+
const result = await db
|
|
514
|
+
.request()
|
|
515
|
+
.input('area', sql.VarChar, area)
|
|
516
|
+
.query(`
|
|
517
|
+
SELECT *
|
|
518
|
+
FROM scheduling_groups
|
|
519
|
+
WHERE area = @area
|
|
520
|
+
`);
|
|
521
|
+
|
|
522
|
+
return result.recordset;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
static async getHistory(
|
|
526
|
+
db: sql.ConnectionPool,
|
|
527
|
+
groupId: number
|
|
528
|
+
) {
|
|
529
|
+
const result = await db
|
|
530
|
+
.request()
|
|
531
|
+
.input('id', sql.Int, groupId)
|
|
532
|
+
.query(`
|
|
533
|
+
SELECT *
|
|
534
|
+
FROM scheduling_group_history
|
|
535
|
+
WHERE scheduling_group_id = @id
|
|
536
|
+
ORDER BY amended_date DESC
|
|
537
|
+
`);
|
|
538
|
+
|
|
539
|
+
return result.recordset;
|
|
738
540
|
}
|
|
739
541
|
}
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
import {
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
await loginPage.login('standard_user', 'secret_sauce');
|
|
750
|
-
expect(page.url()).toContain('inventory.html');
|
|
751
|
-
});
|
|
752
|
-
|
|
753
|
-
test('Add product to cart', async ({ authenticatedUser, inventoryPage }) => {
|
|
754
|
-
await inventoryPage.addProductToCart();
|
|
755
|
-
const cartCount = await inventoryPage.getCartCount();
|
|
756
|
-
expect(cartCount).toBe('1');
|
|
757
|
-
});
|
|
758
|
-
|
|
759
|
-
test('Logout from inventory', async ({ authenticatedUser, inventoryPage, page }) => {
|
|
760
|
-
await inventoryPage.logout();
|
|
761
|
-
expect(page.url()).toBe('https://www.saucedemo.com/');
|
|
762
|
-
});
|
|
763
|
-
});
|
|
764
|
-
```
|
|
765
|
-
|
|
766
|
-
### 17. Form Test with Data (`tests/specs/formTest.spec.ts`)
|
|
767
|
-
```typescript
|
|
768
|
-
import { test, expect } from '@fixtures/base.fixture';
|
|
769
|
-
import { TestDataManager } from '@utils/testData';
|
|
770
|
-
|
|
771
|
-
test.describe('Form Registration Tests with TypeScript', () => {
|
|
772
|
-
test('Register with JSON user data', async ({ formPage, page }) => {
|
|
773
|
-
const user = TestDataManager.getUserByUsername('standard_user');
|
|
774
|
-
expect(user).toBeDefined();
|
|
775
|
-
|
|
776
|
-
await page.goto('https://example.com/register');
|
|
777
|
-
await formPage.formHelper.fillInput('#firstName', user!.firstName);
|
|
778
|
-
await formPage.formHelper.fillInput('#lastName', user!.lastName);
|
|
779
|
-
await formPage.formHelper.fillInput('#email', user!.email);
|
|
780
|
-
|
|
781
|
-
await page.click('button[type="submit"]');
|
|
782
|
-
await expect(page).toHaveURL('**/success');
|
|
783
|
-
});
|
|
784
|
-
|
|
785
|
-
test.describe.configure({ mode: 'parallel' });
|
|
786
|
-
|
|
787
|
-
test('Register with CSV test data - Valid', async ({ formPage, page }) => {
|
|
788
|
-
await page.goto('https://example.com/register');
|
|
789
|
-
await formPage.fillRegistrationForm('registration_valid');
|
|
790
|
-
await page.click('button[type="submit"]');
|
|
791
|
-
await expect(page).toHaveURL('**/success');
|
|
792
|
-
});
|
|
793
|
-
|
|
794
|
-
test('Register with CSV test data - Minimal', async ({ formPage, page }) => {
|
|
795
|
-
await page.goto('https://example.com/register');
|
|
796
|
-
await formPage.fillRegistrationForm('registration_minimal');
|
|
797
|
-
await page.click('button[type="submit"]');
|
|
798
|
-
await expect(page).toHaveURL('**/success');
|
|
799
|
-
});
|
|
800
|
-
});
|
|
801
|
-
```
|
|
802
|
-
|
|
803
|
-
### 18. Run Tests
|
|
804
|
-
```bash
|
|
805
|
-
# Run all tests in parallel (4 workers)
|
|
806
|
-
npm test
|
|
807
|
-
|
|
808
|
-
# Run specific test file
|
|
809
|
-
npm test tests/specs/saucedemo.spec.ts
|
|
810
|
-
|
|
811
|
-
# Run tests with UI (single worker)
|
|
812
|
-
npm run test:headed
|
|
813
|
-
|
|
814
|
-
# Debug mode
|
|
815
|
-
npm run test:debug
|
|
816
|
-
|
|
817
|
-
# Run with custom worker count
|
|
818
|
-
npm test -- --workers=2
|
|
819
|
-
|
|
820
|
-
# Run BDD scenarios
|
|
821
|
-
npm run test:bdd
|
|
822
|
-
|
|
823
|
-
# Run BDD scenarios with JSON report
|
|
824
|
-
npm run test:bdd:report
|
|
825
|
-
|
|
826
|
-
# Run specific BDD feature
|
|
827
|
-
npx @cucumber/cucumber-js tests/features/login.feature
|
|
828
|
-
```
|
|
829
|
-
|
|
830
|
-
### 19. BDD Feature File (`tests/features/login.feature`)
|
|
831
|
-
```gherkin
|
|
832
|
-
Feature: Login to SauceDemo
|
|
833
|
-
As a user
|
|
834
|
-
I want to login to the application
|
|
835
|
-
So that I can access the inventory
|
|
836
|
-
|
|
837
|
-
Background:
|
|
838
|
-
Given I navigate to the SauceDemo application
|
|
839
|
-
|
|
840
|
-
Scenario: User logs in with valid credentials
|
|
841
|
-
When I login with username "standard_user" and password "secret_sauce"
|
|
842
|
-
Then I should see the inventory page
|
|
843
|
-
And the page title should contain "Inventory"
|
|
844
|
-
|
|
845
|
-
Scenario: User logs in and adds product to cart
|
|
846
|
-
When I login with username "standard_user" and password "secret_sauce"
|
|
847
|
-
And I add "Sauce Labs Backpack" to cart
|
|
848
|
-
Then the cart count should show "1"
|
|
849
|
-
And the product should be in the cart
|
|
850
|
-
|
|
851
|
-
Scenario: User can logout
|
|
852
|
-
When I login with username "standard_user" and password "secret_sauce"
|
|
853
|
-
And I click the menu button
|
|
854
|
-
And I click logout
|
|
855
|
-
Then I should be back on the login page
|
|
856
|
-
```
|
|
857
|
-
|
|
858
|
-
### 20. BDD Step Definitions with TypeScript (`tests/steps/loginSteps.ts`)
|
|
859
|
-
```typescript
|
|
860
|
-
import { Given, When, Then, Before, After } from '@cucumber/cucumber';
|
|
861
|
-
import { chromium, Browser, Page } from '@playwright/test';
|
|
862
|
-
import { LoginPage } from '@pages/LoginPage';
|
|
863
|
-
import { InventoryPage } from '@pages/InventoryPage';
|
|
864
|
-
|
|
865
|
-
let browser: Browser;
|
|
866
|
-
let page: Page;
|
|
867
|
-
let loginPage: LoginPage;
|
|
868
|
-
let inventoryPage: InventoryPage;
|
|
869
|
-
|
|
870
|
-
Before(async function() {
|
|
871
|
-
browser = await chromium.launch();
|
|
872
|
-
page = await browser.newPage();
|
|
873
|
-
loginPage = new LoginPage(page);
|
|
874
|
-
inventoryPage = new InventoryPage(page);
|
|
875
|
-
});
|
|
876
|
-
|
|
877
|
-
After(async function() {
|
|
878
|
-
await page.close();
|
|
879
|
-
await browser.close();
|
|
880
|
-
});
|
|
881
|
-
|
|
882
|
-
Given('I navigate to the SauceDemo application', async function() {
|
|
883
|
-
await loginPage.goto();
|
|
884
|
-
});
|
|
885
|
-
|
|
886
|
-
When('I login with username {string} and password {string}', async function(username: string, password: string) {
|
|
887
|
-
await loginPage.login(username, password);
|
|
888
|
-
});
|
|
542
|
+
11) tests/fixtures/pages.fixture.ts
|
|
543
|
+
typescript
|
|
544
|
+
import { test as bddTest } from 'playwright-bdd';
|
|
545
|
+
import { expect } from '@playwright/test';
|
|
546
|
+
import { HomePage } from '../ui/page/HomePage';
|
|
547
|
+
|
|
548
|
+
export type PageFixtures = {
|
|
549
|
+
homePage: HomePage;
|
|
550
|
+
};
|
|
889
551
|
|
|
890
|
-
Then('I should see the inventory page', async function() {
|
|
891
|
-
await page.waitForURL('**/inventory.html');
|
|
892
|
-
});
|
|
893
552
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
}
|
|
553
|
+
export const test = bddTest.extend<PageFixtures>({
|
|
554
|
+
homePage: async ({ page }, use) => {
|
|
555
|
+
const homePage = new HomePage(page);
|
|
556
|
+
await use(homePage);
|
|
557
|
+
},
|
|
899
558
|
});
|
|
900
559
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
560
|
+
export { expect };
|
|
561
|
+
12) tests/fixtures/test.fixture.ts
|
|
562
|
+
typescript
|
|
563
|
+
import {
|
|
564
|
+
test as base,
|
|
565
|
+
expect,
|
|
566
|
+
request,
|
|
567
|
+
APIRequestContext,
|
|
568
|
+
} from '@playwright/test';
|
|
569
|
+
import sql from 'mssql';
|
|
570
|
+
import { getDbPool } from '../../core/db/connection';
|
|
571
|
+
|
|
572
|
+
type TestFixtures = {
|
|
573
|
+
apiAsSystemAdmin: APIRequestContext;
|
|
574
|
+
apiAsAreaAdmin: APIRequestContext;
|
|
575
|
+
db: sql.ConnectionPool;
|
|
576
|
+
};
|
|
905
577
|
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
});
|
|
578
|
+
export const test = base.extend<TestFixtures>({
|
|
579
|
+
apiAsSystemAdmin: async ({}, use) => {
|
|
580
|
+
const api = await request.newContext({
|
|
581
|
+
baseURL: process.env.API_BASE_URL,
|
|
582
|
+
storageState: 'storage/system-admin.json',
|
|
583
|
+
});
|
|
912
584
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
}
|
|
585
|
+
await use(api);
|
|
586
|
+
await api.dispose();
|
|
587
|
+
},
|
|
916
588
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
589
|
+
apiAsAreaAdmin: async ({}, use) => {
|
|
590
|
+
const api = await request.newContext({
|
|
591
|
+
baseURL: process.env.API_BASE_URL,
|
|
592
|
+
storageState: 'storage/area-admin.json',
|
|
593
|
+
});
|
|
920
594
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
}
|
|
924
|
-
```
|
|
925
|
-
});
|
|
595
|
+
await use(api);
|
|
596
|
+
await api.dispose();
|
|
597
|
+
},
|
|
926
598
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
}
|
|
932
|
-
```
|
|
933
|
-
|
|
934
|
-
### 17. BDD Feature File (`tests/features/login.feature`)
|
|
935
|
-
```gherkin
|
|
936
|
-
Feature: Login to SauceDemo
|
|
937
|
-
As a user
|
|
938
|
-
I want to login to the application
|
|
939
|
-
So that I can access the inventory
|
|
940
|
-
|
|
941
|
-
Background:
|
|
942
|
-
Given I navigate to the SauceDemo application
|
|
943
|
-
|
|
944
|
-
Scenario: User logs in with valid credentials
|
|
945
|
-
When I login with username "standard_user" and password "secret_sauce"
|
|
946
|
-
Then I should see the inventory page
|
|
947
|
-
And the page title should contain "Inventory"
|
|
948
|
-
|
|
949
|
-
Scenario: User logs in and adds product to cart
|
|
950
|
-
When I login with username "standard_user" and password "secret_sauce"
|
|
951
|
-
And I add "Sauce Labs Backpack" to cart
|
|
952
|
-
Then the cart count should show "1"
|
|
953
|
-
And the product should be in the cart
|
|
954
|
-
|
|
955
|
-
Scenario: User can logout
|
|
956
|
-
When I login with username "standard_user" and password "secret_sauce"
|
|
957
|
-
And I click the menu button
|
|
958
|
-
And I click logout
|
|
959
|
-
Then I should be back on the login page
|
|
960
|
-
```
|
|
961
|
-
|
|
962
|
-
### 18. BDD Shopping Feature (`tests/features/shopping.feature`)
|
|
963
|
-
```gherkin
|
|
964
|
-
Feature: Shopping Cart
|
|
965
|
-
As a user
|
|
966
|
-
I want to manage my shopping cart
|
|
967
|
-
So that I can purchase items
|
|
968
|
-
|
|
969
|
-
Background:
|
|
970
|
-
Given I am logged into the application
|
|
971
|
-
|
|
972
|
-
Scenario: Add single item to cart
|
|
973
|
-
When I add "Sauce Labs Backpack" to cart
|
|
974
|
-
Then the cart count should show "1"
|
|
975
|
-
|
|
976
|
-
Scenario: Add multiple items to cart
|
|
977
|
-
When I add "Sauce Labs Backpack" to cart
|
|
978
|
-
And I add "Sauce Labs Bike Light" to cart
|
|
979
|
-
Then the cart count should show "2"
|
|
980
|
-
|
|
981
|
-
Scenario: Remove item from cart
|
|
982
|
-
When I add "Sauce Labs Backpack" to cart
|
|
983
|
-
And I remove "Sauce Labs Backpack" from cart
|
|
984
|
-
Then the cart should be empty
|
|
985
|
-
```
|
|
986
|
-
|
|
987
|
-
### 19. BDD Step Definitions (`tests/steps/loginSteps.js`)
|
|
988
|
-
```javascript
|
|
989
|
-
import { Given, When, Then, Before, After } from '@cucumber/cucumber';
|
|
990
|
-
import { chromium } from 'playwright';
|
|
991
|
-
import { LoginPage } from '../pages/LoginPage';
|
|
992
|
-
import { InventoryPage } from '../pages/InventoryPage';
|
|
993
|
-
|
|
994
|
-
let browser;
|
|
995
|
-
let page;
|
|
996
|
-
let loginPage;
|
|
997
|
-
let inventoryPage;
|
|
998
|
-
|
|
999
|
-
Before(async function() {
|
|
1000
|
-
browser = await chromium.launch();
|
|
1001
|
-
page = await browser.newPage();
|
|
1002
|
-
loginPage = new LoginPage(page);
|
|
1003
|
-
inventoryPage = new InventoryPage(page);
|
|
599
|
+
db: async ({}, use) => {
|
|
600
|
+
const pool = await getDbPool();
|
|
601
|
+
await use(pool);
|
|
602
|
+
await pool.close();
|
|
603
|
+
},
|
|
1004
604
|
});
|
|
1005
605
|
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
}
|
|
606
|
+
export { expect };
|
|
607
|
+
13) tests/ui/steps/booking.steps.ts
|
|
608
|
+
typescript
|
|
609
|
+
import { createBdd } from 'playwright-bdd';
|
|
610
|
+
import {test} from '@fixtures/pages.fixture'
|
|
1010
611
|
|
|
1011
|
-
Given('I navigate to the SauceDemo application', async function() {
|
|
1012
|
-
await loginPage.goto();
|
|
1013
|
-
});
|
|
1014
612
|
|
|
1015
|
-
|
|
1016
|
-
await loginPage.login(username, password);
|
|
1017
|
-
});
|
|
613
|
+
const { Given, When, Then } = createBdd(test);
|
|
1018
614
|
|
|
1019
|
-
|
|
1020
|
-
await
|
|
1021
|
-
|
|
615
|
+
Given('user opens the Allocate application', async ({ homePage }) => {
|
|
616
|
+
await homePage.open();
|
|
617
|
+
await homePage.isBbcMenuLoaded();
|
|
1022
618
|
|
|
1023
|
-
Then('the page title should contain {string}', async function(title) {
|
|
1024
|
-
const pageTitle = await page.title();
|
|
1025
|
-
if (!pageTitle.includes(title)) {
|
|
1026
|
-
throw new Error(`Expected title to contain "${title}" but got "${pageTitle}"`);
|
|
1027
|
-
}
|
|
1028
619
|
});
|
|
620
|
+
When('user create new facility', async ({homePage}) => {
|
|
1029
621
|
|
|
1030
|
-
|
|
1031
|
-
const addBtn = '[data-test="add-to-cart-sauce-labs-backpack"]';
|
|
1032
|
-
await page.click(addBtn);
|
|
1033
|
-
});
|
|
622
|
+
await homePage.createFacility()
|
|
1034
623
|
|
|
1035
|
-
|
|
1036
|
-
const cartCount = await inventoryPage.getCartCount();
|
|
1037
|
-
if (cartCount !== count) {
|
|
1038
|
-
throw new Error(`Expected cart count "${count}" but got "${cartCount}"`);
|
|
1039
|
-
}
|
|
1040
|
-
});
|
|
624
|
+
await homePage.verifyFacilityAdded()
|
|
1041
625
|
|
|
1042
|
-
|
|
1043
|
-
await page.click('[data-test="bm-menu-button"]');
|
|
1044
|
-
});
|
|
626
|
+
await homePage.assertViewFacility()
|
|
1045
627
|
|
|
1046
|
-
|
|
1047
|
-
|
|
628
|
+
await homePage.deleteFacility()
|
|
629
|
+
|
|
1048
630
|
});
|
|
631
|
+
14) tests/ui/steps/scheduling-groups.steps.ts
|
|
632
|
+
typescript
|
|
633
|
+
|
|
634
|
+
import { createBdd } from 'playwright-bdd';
|
|
635
|
+
import { test,expect } from '@fixtures/pages.fixture';
|
|
636
|
+
|
|
637
|
+
const { Given, When, Then } = createBdd(test);
|
|
638
|
+
|
|
639
|
+
Given('user switches to System Admin role', async ({homePage}) => {
|
|
640
|
+
await homePage.open();
|
|
641
|
+
const isBbcMenuLoaded = await homePage.isBbcMenuLoaded();
|
|
642
|
+
expect(isBbcMenuLoaded).toBe(true);
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
// When('user navigates to Scheduling Groups page', async ({homePage}) => {
|
|
646
|
+
// // From: tests\ui\features\scheduling-groups.feature:16:5
|
|
647
|
+
// });
|
|
648
|
+
|
|
649
|
+
// When('user creates a scheduling group {string} in {string} with allocations menu {string}', async ({}, arg: string, arg1: string, arg2: string) => {
|
|
650
|
+
// // Step: And user creates a scheduling group "Auto_SystemAdmin_" in "Manchester" with allocations menu "Yes"
|
|
651
|
+
// // From: tests\ui\features\scheduling-groups.feature:17:5
|
|
652
|
+
// });
|
|
653
|
+
|
|
654
|
+
// Then('user can see the new scheduling group in the list', async ({}) => {
|
|
655
|
+
// // Step: Then user can see the new scheduling group in the list
|
|
656
|
+
// // From: tests\ui\features\scheduling-groups.feature:18:5
|
|
657
|
+
// });
|
|
658
|
+
15) tests/ui/features/booking.feature
|
|
659
|
+
feature
|
|
660
|
+
@smoke
|
|
661
|
+
Feature: Booking navigation
|
|
662
|
+
|
|
663
|
+
Scenario: Navigate to Facility Catalogue
|
|
664
|
+
Given user opens the Allocate application
|
|
665
|
+
When user create new facility
|
|
666
|
+
16) tests/ui/features/scheduling-groups.feature
|
|
667
|
+
feature
|
|
668
|
+
@smoke @scheduling-groups
|
|
669
|
+
Feature: Scheduling Groups Management
|
|
670
|
+
|
|
671
|
+
@system-admin
|
|
672
|
+
Scenario: System Admin - Create a new Scheduling Group in any area
|
|
673
|
+
Given user opens the Allocate application
|
|
674
|
+
And user switches to System Admin role
|
|
675
|
+
When user navigates to Scheduling Groups page
|
|
676
|
+
And user creates a scheduling group "Auto_SystemAdmin_" in "Area"
|
|
677
|
+
Then user can see the new scheduling group in the list
|
|
678
|
+
17) tests/ui/page/HomePage.ts (already present in repo)
|
|
679
|
+
typescript
|
|
680
|
+
<content from existing file - see repo for full source>
|
|
681
|
+
18) tests/api/schd-group-create.api.spec.ts
|
|
682
|
+
typescript
|
|
683
|
+
import { test, expect } from "../../tests/fixtures/test.fixture";
|
|
684
|
+
import { createSchedulingGroup } from "../../workflows/schd-group/api/schd-group.api";
|
|
685
|
+
import { SchedulingGroupQueries } from "../../workflows/schd-group/db/schd-group.queries";
|
|
686
|
+
import { assertGroupExists } from "../../workflows/schd-group/invariants/db.invariants";
|
|
687
|
+
|
|
688
|
+
test("System Admin can create Scheduling Group in any Area", async ({apiAsSystemAdmin,db}) => {
|
|
689
|
+
const payload = {
|
|
690
|
+
area: "Manchester",
|
|
691
|
+
groupName: `Auto_${Date.now()}`,
|
|
692
|
+
};
|
|
1049
693
|
|
|
1050
|
-
Then('I should be back on the login page', async function() {
|
|
1051
|
-
await page.waitForURL('https://www.saucedemo.com/');
|
|
1052
|
-
});
|
|
1053
|
-
```
|
|
1054
|
-
|
|
1055
|
-
### 20. BDD Shopping Steps (`tests/steps/shoppingSteps.js`)
|
|
1056
|
-
```javascript
|
|
1057
|
-
import { Given, When, Then } from '@cucumber/cucumber';
|
|
1058
|
-
import { LoginPage } from '../pages/LoginPage';
|
|
1059
|
-
|
|
1060
|
-
let page;
|
|
1061
|
-
let loginPage;
|
|
1062
|
-
|
|
1063
|
-
Given('I am logged into the application', async function() {
|
|
1064
|
-
const { page: gherkinPage } = this;
|
|
1065
|
-
page = gherkinPage;
|
|
1066
|
-
loginPage = new LoginPage(page);
|
|
1067
|
-
await loginPage.goto();
|
|
1068
|
-
await loginPage.login('standard_user', 'secret_sauce');
|
|
1069
|
-
});
|
|
1070
694
|
|
|
1071
|
-
|
|
1072
|
-
const
|
|
1073
|
-
|
|
1074
|
-
});
|
|
695
|
+
//api
|
|
696
|
+
const res = await createSchedulingGroup(apiAsSystemAdmin, payload);
|
|
697
|
+
expect(res.status()).toBe(201);
|
|
1075
698
|
|
|
1076
|
-
|
|
1077
|
-
const cartBadge = page.locator('[data-test="shopping-cart-badge"]');
|
|
1078
|
-
const isHidden = await cartBadge.isHidden().catch(() => true);
|
|
1079
|
-
if (!isHidden) {
|
|
1080
|
-
throw new Error('Cart is not empty');
|
|
1081
|
-
}
|
|
1082
|
-
});
|
|
699
|
+
const record = await SchedulingGroupQueries.getByName(db, payload.groupName);
|
|
1083
700
|
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
if (!badge) {
|
|
1087
|
-
throw new Error('Product not found in cart');
|
|
1088
|
-
}
|
|
701
|
+
//db
|
|
702
|
+
assertGroupExists(record);
|
|
1089
703
|
});
|
|
1090
|
-
```
|
|
1091
|
-
|
|
1092
|
-
### 21. Run Tests
|
|
1093
|
-
```bash
|
|
1094
|
-
# Run all tests in parallel (4 workers)
|
|
1095
|
-
npm test
|
|
1096
|
-
|
|
1097
|
-
# Run specific test file
|
|
1098
|
-
npm test tests/specs/saucedemo.spec.js
|
|
1099
|
-
|
|
1100
|
-
# Run tests with UI (single worker)
|
|
1101
|
-
npm run test:headed
|
|
1102
|
-
|
|
1103
|
-
# Debug mode
|
|
1104
|
-
npm run test:debug
|
|
1105
|
-
|
|
1106
|
-
# Run with custom worker count
|
|
1107
|
-
npm test -- --workers=2
|
|
1108
|
-
|
|
1109
|
-
# Run BDD scenarios
|
|
1110
|
-
npm run test:bdd
|
|
1111
|
-
|
|
1112
|
-
# Run BDD scenarios with JSON report
|
|
1113
|
-
npm run test:bdd:report
|
|
1114
704
|
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
### 22. Example Test Using Utilities (`tests/specs/formTest.spec.js`)
|
|
1120
|
-
```javascript
|
|
1121
|
-
import { test, expect } from '../fixtures/base.fixture';
|
|
1122
|
-
import { FormPage } from '../pages/FormPage';
|
|
1123
|
-
import { TestDataManager } from '../utils/testData';
|
|
1124
|
-
|
|
1125
|
-
test.describe('Form Registration with Test Data', () => {
|
|
1126
|
-
let formPage;
|
|
1127
|
-
|
|
1128
|
-
test.beforeEach(async ({ page }) => {
|
|
1129
|
-
formPage = new FormPage(page);
|
|
1130
|
-
await page.goto('https://example.com/register');
|
|
1131
|
-
});
|
|
1132
|
-
|
|
1133
|
-
test('Register with JSON user data', async ({ page }) => {
|
|
1134
|
-
const user = TestDataManager.getUserByUsername('standard_user');
|
|
1135
|
-
|
|
1136
|
-
await formPage.formHelper.fillInput('#firstName', user.firstName);
|
|
1137
|
-
await formPage.formHelper.fillInput('#lastName', user.lastName);
|
|
1138
|
-
await formPage.formHelper.fillInput('#email', user.email);
|
|
1139
|
-
|
|
1140
|
-
await page.click('button[type="submit"]');
|
|
1141
|
-
await expect(page).toHaveURL('**/success');
|
|
1142
|
-
});
|
|
1143
|
-
|
|
1144
|
-
test('Register with CSV test scenarios', async ({ page }) => {
|
|
1145
|
-
await formPage.fillRegistrationForm('registration_valid');
|
|
1146
|
-
|
|
1147
|
-
await page.click('button[type="submit"]');
|
|
1148
|
-
await expect(page).toHaveURL('**/success');
|
|
705
|
+
test("Scheduling Group Name is mandatory", async ({ apiAsSystemAdmin }) => {
|
|
706
|
+
const res = await apiAsSystemAdmin.post("/scheduling-groups", {
|
|
707
|
+
data: { area: "London" },
|
|
1149
708
|
});
|
|
1150
709
|
|
|
1151
|
-
|
|
1152
|
-
const formConfig = {
|
|
1153
|
-
firstName: {
|
|
1154
|
-
type: 'text',
|
|
1155
|
-
selector: '#firstName',
|
|
1156
|
-
value: 'John'
|
|
1157
|
-
},
|
|
1158
|
-
country: {
|
|
1159
|
-
type: 'dropdown',
|
|
1160
|
-
selector: '.country-select',
|
|
1161
|
-
value: 'United States'
|
|
1162
|
-
},
|
|
1163
|
-
gender: {
|
|
1164
|
-
type: 'radio',
|
|
1165
|
-
value: 'Male'
|
|
1166
|
-
},
|
|
1167
|
-
interests: {
|
|
1168
|
-
type: 'checkbox',
|
|
1169
|
-
value: ['Sports', 'Gaming', 'Reading']
|
|
1170
|
-
},
|
|
1171
|
-
terms: {
|
|
1172
|
-
type: 'checkbox',
|
|
1173
|
-
value: 'I accept terms'
|
|
1174
|
-
}
|
|
1175
|
-
};
|
|
1176
|
-
|
|
1177
|
-
// Use helper to fill entire form
|
|
1178
|
-
for (const field of Object.values(formConfig)) {
|
|
1179
|
-
if (field.type === 'text') {
|
|
1180
|
-
await formPage.formHelper.fillInput(field.selector, field.value);
|
|
1181
|
-
} else if (field.type === 'dropdown') {
|
|
1182
|
-
await formPage.formHelper.selectDropdown(field.selector, field.value);
|
|
1183
|
-
} else if (field.type === 'radio') {
|
|
1184
|
-
await formPage.formHelper.selectRadioButton(field.value);
|
|
1185
|
-
} else if (field.type === 'checkbox') {
|
|
1186
|
-
if (Array.isArray(field.value)) {
|
|
1187
|
-
await formPage.formHelper.checkMultiple(field.value);
|
|
1188
|
-
} else {
|
|
1189
|
-
await formPage.formHelper.checkCheckbox(field.value);
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
await page.click('button[type="submit"]');
|
|
1195
|
-
await expect(page).toHaveURL('**/success');
|
|
1196
|
-
});
|
|
1197
|
-
|
|
1198
|
-
test('Verify form values after filling', async ({ page }) => {
|
|
1199
|
-
await formPage.formHelper.fillInput('#firstName', 'John');
|
|
1200
|
-
await formPage.formHelper.checkCheckbox('I accept terms');
|
|
1201
|
-
|
|
1202
|
-
const firstName = await formPage.formHelper.getInputValue('#firstName');
|
|
1203
|
-
const isTermsChecked = await formPage.formHelper.isCheckboxChecked('I accept terms');
|
|
1204
|
-
|
|
1205
|
-
expect(firstName).toBe('John');
|
|
1206
|
-
expect(isTermsChecked).toBe(true);
|
|
1207
|
-
});
|
|
710
|
+
expect(res.status()).toBe(400);
|
|
1208
711
|
});
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
Feature: Registration with Test Data
|
|
1214
|
-
As a new user
|
|
1215
|
-
I want to register using predefined test data
|
|
1216
|
-
So that I can access the application
|
|
1217
|
-
|
|
1218
|
-
Scenario: Register with JSON test data
|
|
1219
|
-
Given I navigate to the registration page
|
|
1220
|
-
When I fill the form with user data "standard_user"
|
|
1221
|
-
And I accept the terms and conditions
|
|
1222
|
-
And I submit the form
|
|
1223
|
-
Then I should see the success message
|
|
1224
|
-
|
|
1225
|
-
Scenario Outline: Register with CSV test scenarios
|
|
1226
|
-
Given I navigate to the registration page
|
|
1227
|
-
When I fill first name with "<firstName>"
|
|
1228
|
-
And I fill last name with "<lastName>"
|
|
1229
|
-
And I fill email with "<email>"
|
|
1230
|
-
And I select country "<country>"
|
|
1231
|
-
And I check "<acceptTerms>"
|
|
1232
|
-
And I submit the form
|
|
1233
|
-
Then I should see the success message
|
|
1234
|
-
|
|
1235
|
-
Examples:
|
|
1236
|
-
| firstName | lastName | email | country | acceptTerms |
|
|
1237
|
-
| John | Doe | john@test.com | USA | true |
|
|
1238
|
-
| Jane | Smith | jane@test.com | Canada | true |
|
|
1239
|
-
| Pierre | Martin | pierre@test.fr | France | true |
|
|
1240
|
-
```
|
|
1241
|
-
|
|
1242
|
-
## Developer Quick Start
|
|
1243
|
-
|
|
1244
|
-
### Using Test Data & Form Helpers
|
|
1245
|
-
**Step 1:** Create test data files in `tests/data/` (JSON, CSV, YAML)
|
|
1246
|
-
**Step 2:** Use `DataReader` to load test data:
|
|
1247
|
-
```javascript
|
|
1248
|
-
const users = DataReader.readJSON('users.json');
|
|
1249
|
-
const formData = await DataReader.readCSV('formData.csv');
|
|
1250
|
-
const config = DataReader.readYAML('config.yaml');
|
|
1251
|
-
```
|
|
1252
|
-
|
|
1253
|
-
**Step 3:** Use `FormHelper` for complex form interactions:
|
|
1254
|
-
```javascript
|
|
1255
|
-
const formHelper = new FormHelper(page);
|
|
1256
|
-
await formHelper.fillInput('#name', 'John');
|
|
1257
|
-
await formHelper.selectDropdown('.country', 'USA');
|
|
1258
|
-
await formHelper.checkMultiple(['Option1', 'Option2']);
|
|
1259
|
-
```
|
|
1260
|
-
|
|
1261
|
-
**Step 4:** Create page objects that combine both:
|
|
1262
|
-
```javascript
|
|
1263
|
-
const formPage = new FormPage(page);
|
|
1264
|
-
await formPage.fillRegistrationForm('registration_valid');
|
|
1265
|
-
```
|
|
1266
|
-
|
|
1267
|
-
### Traditional Playwright Tests
|
|
1268
|
-
**Step 1:** Copy project structure from Section 4
|
|
1269
|
-
**Step 2:** Create page objects in `tests/pages/` (copy LoginPage & InventoryPage)
|
|
1270
|
-
**Step 3:** Import fixtures in your tests: `import { test, expect } from '../fixtures/base.fixture'`
|
|
1271
|
-
**Step 4:** Use fixtures in tests: `test('my test', async ({ loginPage, authenticatedUser }) => { ... })`
|
|
1272
|
-
**Step 5:** Run: `npm test`
|
|
1273
|
-
|
|
1274
|
-
### BDD Scenarios
|
|
1275
|
-
**Step 1:** Create `.feature` files in `tests/features/` with Gherkin scenarios
|
|
1276
|
-
**Step 2:** Create step definitions in `tests/steps/` (copy loginSteps & shoppingSteps)
|
|
1277
|
-
**Step 3:** Map steps to page objects (reuse LoginPage, InventoryPage)
|
|
1278
|
-
**Step 4:** Configure `cucumber.js` for parallel execution
|
|
1279
|
-
**Step 5:** Run: `npm run test:bdd`
|
|
1280
|
-
|
|
1281
|
-
## Verification Checklist
|
|
1282
|
-
✅ Chromium browser installed (`npx playwright install chromium`)
|
|
1283
|
-
✅ TypeScript configured (`typescript @types/node` installed + `tsconfig.json` created)
|
|
1284
|
-
✅ All dependencies installed (`npm install` + `@cucumber/cucumber` + `csv-parser`, `js-yaml`, `@types/js-yaml`)
|
|
1285
|
-
✅ TypeScript paths configured in `tsconfig.json` (@pages/*, @utils/*, @fixtures/*, @data/*)
|
|
1286
|
-
✅ Playwright config in TypeScript (`playwright.config.ts` with latest imports)
|
|
1287
|
-
✅ Folder structure created (all `.ts` files: `tests/pages/`, `tests/fixtures/`, `tests/specs/`, `tests/features/`, `tests/steps/`, `tests/utils/`, `tests/data/`)
|
|
1288
|
-
✅ Base fixtures created with TypeScript typing (`base.fixture.ts`)
|
|
1289
|
-
✅ Cucumber configuration created (`cucumber.js` with ts-node support)
|
|
1290
|
-
✅ Page Object Model classes in TypeScript with type safety
|
|
1291
|
-
✅ Step definitions in TypeScript with proper typing
|
|
1292
|
-
✅ Feature files created (login.feature, shopping.feature)
|
|
1293
|
-
✅ Utilities created in TypeScript (dataReader.ts, formHelper.ts, testData.ts with interfaces)
|
|
1294
|
-
✅ Test data files created (users.json, formData.csv, config.yaml)
|
|
1295
|
-
✅ Enhanced page objects in TypeScript (FormPage.ts with utilities integrated)
|
|
1296
|
-
✅ Modern ES module imports throughout (import/export)
|
|
1297
|
-
✅ Parallel execution enabled (`fullyParallel: true` & Cucumber parallel: 4)
|
|
1298
|
-
✅ Tests execute in parallel against single Chromium browser
|
|
1299
|
-
✅ Both Playwright & BDD tests configured with full TypeScript support
|
|
1300
|
-
|
|
1301
|
-
## Best Practices for Scalability & TypeScript
|
|
1302
|
-
✅ **Use Fixtures** - Pre-set up authenticated users, page objects (base.fixture.ts with types)
|
|
1303
|
-
✅ **Page Object Model** - Encapsulate selectors & actions in TypeScript classes
|
|
1304
|
-
✅ **Type Safety** - Full TypeScript typing for all utilities, pages, and tests
|
|
1305
|
-
✅ **Test Data Management** - Use JSON, CSV, YAML with DataReader (fully typed)
|
|
1306
|
-
✅ **Form Helpers** - Reusable FormHelper for all form interactions (typed interfaces)
|
|
1307
|
-
✅ **BDD for Non-Technical Stakeholders** - Write scenarios in Gherkin (Feature files)
|
|
1308
|
-
✅ **Parallel Execution** - Tests & scenarios run simultaneously (4 workers by default)
|
|
1309
|
-
✅ **Minimal Developer Effort** - Write only test logic, reuse fixtures, pages, steps & utilities
|
|
1310
|
-
✅ **Chromium Only** - Single browser reduces flakiness & speeds up execution
|
|
1311
|
-
✅ **Retry Failed Tests** - Automatic retries improve reliability
|
|
1312
|
-
✅ **Capture Failures** - Screenshots & videos on failures for debugging
|
|
1313
|
-
✅ **Dual Approach** - Mix traditional tests & BDD scenarios based on needs
|
|
1314
|
-
✅ **Module Path Aliases** - Use @pages/, @utils/, @fixtures/, @data/ for clean imports
|
|
1315
|
-
✅ **Latest Playwright** - Uses latest Playwright APIs with modern module syntax
|
|
1316
|
-
✅ **Parameterized Tests** - Use CSV data for scenario outlines (data-driven testing)
|
|
1317
|
-
✅ **Form Helpers** - Reusable FormHelper for all form interactions (text, dropdown, radio, checkbox)
|
|
1318
|
-
✅ **BDD for Non-Technical Stakeholders** - Write scenarios in Gherkin (Feature files)
|
|
1319
|
-
✅ **Parallel Execution** - Tests & scenarios run simultaneously (4 workers by default)
|
|
1320
|
-
✅ **Minimal Developer Effort** - Write only test logic, reuse fixtures, pages, steps & utilities
|
|
1321
|
-
✅ **Chromium Only** - Single browser reduces flakiness & speeds up execution
|
|
1322
|
-
✅ **Retry Failed Tests** - Automatic retries improve reliability
|
|
1323
|
-
✅ **Capture Failures** - Screenshots & videos on failures for debugging
|
|
1324
|
-
✅ **Dual Approach** - Mix traditional tests & BDD scenarios based on needs
|
|
1325
|
-
✅ **Parameterized Tests** - Use CSV data for scenario outlines (data-driven testing)
|
|
1326
|
-
✅ **Full TypeScript Support** - All code written in TypeScript with strict mode enabled
|
|
1327
|
-
✅ **Path Aliases** - Clean imports using @pages/, @utils/, @fixtures/, @data/ prefixes
|
|
1328
|
-
|
|
1329
|
-
## How to Use Test Data & Utilities (TypeScript)
|
|
1330
|
-
|
|
1331
|
-
### 1. Loading Test Data
|
|
1332
|
-
```typescript
|
|
1333
|
-
import { TestDataManager } from '@utils/testData';
|
|
1334
|
-
|
|
1335
|
-
// Get user by username (fully typed)
|
|
1336
|
-
const user = TestDataManager.getUserByUsername('standard_user');
|
|
1337
|
-
|
|
1338
|
-
// Get form data by scenario
|
|
1339
|
-
const formData = await TestDataManager.getFormDataByScenario('registration_valid');
|
|
1340
|
-
|
|
1341
|
-
// Load all config with type safety
|
|
1342
|
-
const config = TestDataManager.loadConfig();
|
|
1343
|
-
```
|
|
1344
|
-
|
|
1345
|
-
### 2. Form Interactions with Type Safety
|
|
1346
|
-
```typescript
|
|
1347
|
-
import { FormHelper } from '@utils/formHelper';
|
|
1348
|
-
import { Page } from '@playwright/test';
|
|
1349
|
-
|
|
1350
|
-
const formHelper = new FormHelper(page);
|
|
1351
|
-
|
|
1352
|
-
// Text input
|
|
1353
|
-
await formHelper.fillInput('#firstName', 'John');
|
|
1354
|
-
|
|
1355
|
-
// Dropdown (non-select)
|
|
1356
|
-
await formHelper.selectDropdown('.country-dropdown', 'USA');
|
|
1357
|
-
|
|
1358
|
-
// Radio button
|
|
1359
|
-
await formHelper.selectRadioButton('Male');
|
|
1360
|
-
|
|
1361
|
-
// Single checkbox
|
|
1362
|
-
await formHelper.checkCheckbox('I accept terms');
|
|
1363
|
-
|
|
1364
|
-
// Multiple checkboxes
|
|
1365
|
-
await formHelper.checkMultiple(['Option1', 'Option2', 'Option3']);
|
|
1366
|
-
|
|
1367
|
-
// Get and verify values (fully typed)
|
|
1368
|
-
const firstName = await formHelper.getInputValue('#firstName');
|
|
1369
|
-
const isChecked = await formHelper.isCheckboxChecked('I accept terms');
|
|
1370
|
-
```
|
|
712
|
+
19) tests/db/01-connection.db.spec.ts
|
|
713
|
+
typescript
|
|
714
|
+
import { test, expect } from '../../tests/fixtures/test.fixture';
|
|
715
|
+
import { listTables } from '../../core/db/queries';
|
|
1371
716
|
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
import { FormPage } from '@pages/FormPage';
|
|
1376
|
-
|
|
1377
|
-
test.describe('Data-Driven Form Tests', () => {
|
|
1378
|
-
const testCases = [
|
|
1379
|
-
{ scenario: 'registration_valid' as const },
|
|
1380
|
-
{ scenario: 'registration_minimal' as const },
|
|
1381
|
-
{ scenario: 'registration_eu' as const }
|
|
1382
|
-
];
|
|
1383
|
-
|
|
1384
|
-
testCases.forEach((testCase) => {
|
|
1385
|
-
test(`Register with ${testCase.scenario}`, async ({ page, formPage }: any) => {
|
|
1386
|
-
await formPage.fillRegistrationForm(testCase.scenario);
|
|
1387
|
-
// Assert...
|
|
1388
|
-
});
|
|
1389
|
-
});
|
|
717
|
+
test('Tables exist', async ({ db }) => {
|
|
718
|
+
const tables = await listTables(db);
|
|
719
|
+
expect(tables.length).toBeGreaterThan(0);
|
|
1390
720
|
});
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
- Writing complex technical validations
|
|
1408
|
-
- Need direct access to page objects with full typing
|
|
1409
|
-
- Testing internal APIs or utilities
|
|
1410
|
-
- Data-driven test variations
|
|
1411
|
-
- Using TypeScript for type safety
|
|
1412
|
-
|
|
1413
|
-
**Use BDD (`npm run test:bdd`)** when:
|
|
1414
|
-
- Collaborating with non-technical stakeholders
|
|
1415
|
-
- Business logic needs clear documentation
|
|
1416
|
-
- Multiple teams working on same scenarios
|
|
1417
|
-
- Acceptance criteria need to be testable
|
|
1418
|
-
|
|
1419
|
-
**Note:** This setup uses Chromium only with parallel execution, full TypeScript support with strict mode, and modern ES module imports. Both Playwright & BDD tests can run simultaneously with complete type safety. All utilities and page objects are fully typed. Module path aliases (@pages/, @utils/, etc.) provide clean imports. Latest Playwright features and APIs are used throughout. If you see "Missing script: start" error, add the `start` script to your package.json before running tests.
|
|
721
|
+
20) tests/utils/readJson.ts
|
|
722
|
+
typescript
|
|
723
|
+
import fs from 'fs';
|
|
724
|
+
import path from 'path';
|
|
725
|
+
import { FormField } from '../types/formField';
|
|
726
|
+
|
|
727
|
+
export async function readJSON(fileName: string): Promise<Record<string, FormField>> {
|
|
728
|
+
const filePath = path.resolve(__dirname, `../data/${fileName}`);
|
|
729
|
+
const rawData = fs.readFileSync(filePath, 'utf-8');
|
|
730
|
+
const data: Record<string, FormField> = JSON.parse(rawData);
|
|
731
|
+
return data;
|
|
732
|
+
}
|
|
733
|
+
21) tests/utils/formFiller.ts (full implementation included in repo)
|
|
734
|
+
typescript
|
|
735
|
+
<content from existing file - see repo for full source>
|
|
736
|
+
22) tests/types/formField.ts (included above) 23) tests/data/facilityFormData.json (included above) --- Notes - I embedded the full contents of many small files above; for very large source files (like formFiller.ts and HomePage.ts) I included a placeholder pointer to the existing file since the full content is already present in the repository and shown earlier in this document. If you'd like, I can paste their complete contents here as well. - Next I can: (choose) - Auto-generate the Playwright UI scaffold files from the templates (create the files in the repo). - Leave this agent file as the single source of templates and not modify the filesystem.
|