experiment-e2e-generator 1.0.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/CHANGELOG.md +44 -0
- package/LICENSE +21 -0
- package/README.md +382 -0
- package/bin/cli.js +9 -0
- package/package.json +31 -0
- package/src/file-operations.js +91 -0
- package/src/generator.js +99 -0
- package/src/package-updater.js +73 -0
- package/src/prompts.js +88 -0
- package/src/utils.js +162 -0
- package/templates/playwright.config.js +29 -0
- package/templates/tests/config/experiment.config.js +28 -0
- package/templates/tests/config/index.js +5 -0
- package/templates/tests/config/qa-links.config.js +17 -0
- package/templates/tests/e2e/experiment-name/experiment.spec.js +53 -0
- package/templates/tests/fixtures/test-fixtures.js +51 -0
- package/templates/tests/utils/test-helpers.js +114 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.0.0] - 2026-02-02
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Initial release of @sogody/experiment-e2e-generator
|
|
12
|
+
- Interactive CLI for generating Playwright E2E test infrastructure
|
|
13
|
+
- Complete template system with placeholder replacement
|
|
14
|
+
- Auto-detection of experiment names from project structure
|
|
15
|
+
- Safe package.json updating (preserves existing configuration)
|
|
16
|
+
- Generated test structure includes:
|
|
17
|
+
- `playwright.config.js` - Multi-browser configuration
|
|
18
|
+
- `tests/config/` - Experiment and URL configuration files
|
|
19
|
+
- `tests/e2e/` - Sample test suites for control and experiment
|
|
20
|
+
- `tests/fixtures/` - Custom Playwright fixtures
|
|
21
|
+
- `tests/utils/` - Reusable helper functions
|
|
22
|
+
- Pre-flight validation checks
|
|
23
|
+
- Warning system for existing test directories
|
|
24
|
+
- Comprehensive documentation and examples
|
|
25
|
+
- Support for environment variables (CONTROL_URL, EXPERIMENT_URL, etc.)
|
|
26
|
+
- Adobe Target preview token integration
|
|
27
|
+
- Organized folder structure following best practices
|
|
28
|
+
|
|
29
|
+
### Template Variables
|
|
30
|
+
- `{{EXPERIMENT_NAME}}` - Original experiment name
|
|
31
|
+
- `{{EXPERIMENT_NAME_KEBAB}}` - Kebab-case formatted name
|
|
32
|
+
- `{{BASE_URL}}` - Base URL for tests
|
|
33
|
+
- `{{MARKET}}` - Market code (uppercase)
|
|
34
|
+
|
|
35
|
+
### Dependencies
|
|
36
|
+
- chalk ^5.6.2 - Terminal styling
|
|
37
|
+
- fs-extra ^11.3.3 - File system utilities
|
|
38
|
+
- prompts ^2.4.2 - Interactive CLI prompts
|
|
39
|
+
|
|
40
|
+
### Requirements
|
|
41
|
+
- Node.js >= 16.15.1
|
|
42
|
+
- Existing package.json in project root
|
|
43
|
+
|
|
44
|
+
[1.0.0]: https://github.com/sogody/experiment-e2e-generator/releases/tag/v1.0.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aldi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
# @sogody/experiment-e2e-generator
|
|
2
|
+
|
|
3
|
+
A CLI tool to scaffold Playwright E2E testing infrastructure for Experiment Framework projects. Generate a complete test setup with configuration, fixtures, utilities, and sample tests in seconds.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🎭 **Complete Playwright Setup** - Generates all necessary configuration and structure
|
|
8
|
+
- 📁 **Organized Structure** - Tests, configs, fixtures, and utilities properly organized
|
|
9
|
+
- 🔧 **Smart Configuration** - Auto-detects project settings and experiment names
|
|
10
|
+
- ✨ **Template System** - Customizable templates with placeholder replacement
|
|
11
|
+
- 📦 **Safe Updates** - Merges dependencies without overwriting existing `package.json`
|
|
12
|
+
- 🎯 **Best Practices** - Follows Playwright and A/B testing best practices
|
|
13
|
+
|
|
14
|
+
## Installation & Usage
|
|
15
|
+
|
|
16
|
+
No installation required! Use `npx` to run directly:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx @sogody/experiment-e2e-generator
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Requirements
|
|
23
|
+
|
|
24
|
+
- Node.js >= 16.15.1
|
|
25
|
+
- Existing `package.json` in project root
|
|
26
|
+
- Project must be an Experiment Framework project
|
|
27
|
+
|
|
28
|
+
## What It Generates
|
|
29
|
+
|
|
30
|
+
Running the generator creates the following structure:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
your-project/
|
|
34
|
+
├── playwright.config.js # Playwright configuration
|
|
35
|
+
├── tests/
|
|
36
|
+
│ ├── config/
|
|
37
|
+
│ │ ├── index.js # Central config export
|
|
38
|
+
│ │ ├── experiment.config.js # Experiment-specific settings
|
|
39
|
+
│ │ └── qa-links.config.js # Test URLs (control & experiment)
|
|
40
|
+
│ ├── e2e/
|
|
41
|
+
│ │ └── your-experiment/
|
|
42
|
+
│ │ └── your-experiment.spec.js # Sample test suite
|
|
43
|
+
│ ├── fixtures/
|
|
44
|
+
│ │ └── test-fixtures.js # Custom test fixtures
|
|
45
|
+
│ └── utils/
|
|
46
|
+
│ └── test-helpers.js # Reusable helper functions
|
|
47
|
+
└── package.json # Updated with Playwright dependencies
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Interactive Setup
|
|
51
|
+
|
|
52
|
+
The generator will prompt you for:
|
|
53
|
+
|
|
54
|
+
1. **Experiment Name** - Your experiment's name (auto-detected if possible)
|
|
55
|
+
2. **Base URL** - The base URL for tests (e.g., `https://www.samsung.com`)
|
|
56
|
+
3. **Market Code** - Primary market (e.g., `NL`, `BE`, `US`)
|
|
57
|
+
|
|
58
|
+
### Example Session
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
$ npx @sogody/experiment-e2e-generator
|
|
62
|
+
|
|
63
|
+
🎭 Experiment E2E Test Generator
|
|
64
|
+
|
|
65
|
+
Running pre-flight checks...
|
|
66
|
+
|
|
67
|
+
Please provide the following information:
|
|
68
|
+
|
|
69
|
+
? What is your experiment name? › My Awesome Experiment
|
|
70
|
+
? Base URL for tests › https://www.samsung.com
|
|
71
|
+
? Primary market code › NL
|
|
72
|
+
|
|
73
|
+
📁 Generating test files...
|
|
74
|
+
|
|
75
|
+
✓ playwright.config.js
|
|
76
|
+
✓ tests/config/index.js
|
|
77
|
+
✓ tests/config/experiment.config.js
|
|
78
|
+
✓ tests/config/qa-links.config.js
|
|
79
|
+
✓ tests/fixtures/test-fixtures.js
|
|
80
|
+
✓ tests/utils/test-helpers.js
|
|
81
|
+
✓ tests/e2e/my-awesome-experiment/my-awesome-experiment.spec.js
|
|
82
|
+
|
|
83
|
+
📦 Updating package.json...
|
|
84
|
+
|
|
85
|
+
✓ Added Playwright dependencies to devDependencies
|
|
86
|
+
✓ Added "test:e2e" script
|
|
87
|
+
|
|
88
|
+
✓ Test structure generated successfully!
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## After Generation
|
|
92
|
+
|
|
93
|
+
### 1. Install Dependencies
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
yarn install
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Or with npm:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
npm install
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 2. Configure Test URLs
|
|
106
|
+
|
|
107
|
+
Update the URLs in `tests/config/qa-links.config.js`:
|
|
108
|
+
|
|
109
|
+
```javascript
|
|
110
|
+
export const qaLinksConfig = {
|
|
111
|
+
controlUrl: process.env.CONTROL_URL || 'https://www.samsung.com/nl/control-page/',
|
|
112
|
+
experimentUrl: process.env.EXPERIMENT_URL || 'https://www.samsung.com/nl/experiment-page/',
|
|
113
|
+
};
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
You can set these via environment variables or update them directly in the file.
|
|
117
|
+
|
|
118
|
+
### 3. Customize Test Selectors
|
|
119
|
+
|
|
120
|
+
Edit your test file at `tests/e2e/your-experiment/your-experiment.spec.js`:
|
|
121
|
+
|
|
122
|
+
```javascript
|
|
123
|
+
// Update selectors to match your experiment component
|
|
124
|
+
const experimentComponent = page.locator('[data-experiment="your-experiment"]');
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### 4. Run Tests
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
# Run all tests
|
|
131
|
+
yarn test:e2e
|
|
132
|
+
|
|
133
|
+
# Run in headed mode
|
|
134
|
+
yarn playwright test --headed
|
|
135
|
+
|
|
136
|
+
# Run specific test file
|
|
137
|
+
yarn playwright test tests/e2e/your-experiment
|
|
138
|
+
|
|
139
|
+
# Run with UI mode
|
|
140
|
+
yarn playwright test --ui
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Generated Files Overview
|
|
144
|
+
|
|
145
|
+
### `playwright.config.js`
|
|
146
|
+
|
|
147
|
+
Main Playwright configuration with:
|
|
148
|
+
- Multi-browser support (Chromium, Firefox, WebKit)
|
|
149
|
+
- HTML reporter
|
|
150
|
+
- Screenshot and trace on failure
|
|
151
|
+
- Optimized for CI/CD
|
|
152
|
+
|
|
153
|
+
### `tests/config/experiment.config.js`
|
|
154
|
+
|
|
155
|
+
Experiment-specific settings:
|
|
156
|
+
- Experiment name and market
|
|
157
|
+
- Timeout configurations
|
|
158
|
+
- Environment variables
|
|
159
|
+
- Adobe Target integration (if needed)
|
|
160
|
+
|
|
161
|
+
### `tests/config/qa-links.config.js`
|
|
162
|
+
|
|
163
|
+
Test URL management:
|
|
164
|
+
- Control and experiment URLs
|
|
165
|
+
- URL validation
|
|
166
|
+
- Environment variable support
|
|
167
|
+
|
|
168
|
+
### `tests/e2e/your-experiment/your-experiment.spec.js`
|
|
169
|
+
|
|
170
|
+
Sample test suite with:
|
|
171
|
+
- **Control tests** - Verify experiment doesn't appear
|
|
172
|
+
- **Experiment tests** - Verify experiment appears and functions
|
|
173
|
+
- Proper test structure following best practices
|
|
174
|
+
|
|
175
|
+
### `tests/fixtures/test-fixtures.js`
|
|
176
|
+
|
|
177
|
+
Custom Playwright fixtures:
|
|
178
|
+
- Experiment context with utilities
|
|
179
|
+
- Adobe Target preview support
|
|
180
|
+
- URL building helpers
|
|
181
|
+
|
|
182
|
+
### `tests/utils/test-helpers.js`
|
|
183
|
+
|
|
184
|
+
Reusable helper functions:
|
|
185
|
+
- Network idle waiting
|
|
186
|
+
- Element stability checks
|
|
187
|
+
- Viewport utilities
|
|
188
|
+
- Screenshot helpers
|
|
189
|
+
- Retry with backoff
|
|
190
|
+
|
|
191
|
+
## Testing Strategy
|
|
192
|
+
|
|
193
|
+
The generated tests follow A/B testing best practices:
|
|
194
|
+
|
|
195
|
+
### Control Group Tests
|
|
196
|
+
Verify that the experiment component **does not appear** in the control variant:
|
|
197
|
+
|
|
198
|
+
```javascript
|
|
199
|
+
test.describe('Your Experiment - Control', () => {
|
|
200
|
+
test('should not display experiment component', async ({ page }) => {
|
|
201
|
+
await page.goto(qaLinksConfig.controlUrl);
|
|
202
|
+
await expect(experimentComponent).not.toBeVisible();
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Experiment Group Tests
|
|
208
|
+
Verify that the experiment component **appears and functions correctly**:
|
|
209
|
+
|
|
210
|
+
```javascript
|
|
211
|
+
test.describe('Your Experiment - Experiment', () => {
|
|
212
|
+
test('should display experiment component', async ({ page }) => {
|
|
213
|
+
await page.goto(qaLinksConfig.experimentUrl);
|
|
214
|
+
await expect(experimentComponent).toBeVisible();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('should handle user interactions correctly', async ({ page }) => {
|
|
218
|
+
await page.goto(qaLinksConfig.experimentUrl);
|
|
219
|
+
// Test interactions...
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Environment Variables
|
|
225
|
+
|
|
226
|
+
You can use environment variables for dynamic configuration:
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
# Set test URLs
|
|
230
|
+
export CONTROL_URL=https://www.samsung.com/nl/control
|
|
231
|
+
export EXPERIMENT_URL=https://www.samsung.com/nl/experiment
|
|
232
|
+
|
|
233
|
+
# Set Adobe Target preview token (if applicable)
|
|
234
|
+
export ADOBE_PREVIEW_TOKEN=your-token-here
|
|
235
|
+
|
|
236
|
+
# Set test environment
|
|
237
|
+
export TEST_ENV=qa
|
|
238
|
+
|
|
239
|
+
# Run tests
|
|
240
|
+
yarn test:e2e
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Best Practices
|
|
244
|
+
|
|
245
|
+
### Locator Strategy
|
|
246
|
+
|
|
247
|
+
The generated tests follow Playwright's recommended locator hierarchy:
|
|
248
|
+
|
|
249
|
+
1. **getByRole** - Preferred for accessibility
|
|
250
|
+
2. **getByText** - For visible text content
|
|
251
|
+
3. **getByLabel** - For form fields
|
|
252
|
+
4. **data attributes** - Only when necessary
|
|
253
|
+
|
|
254
|
+
### Test Structure
|
|
255
|
+
|
|
256
|
+
- Use `test.describe` blocks for logical grouping
|
|
257
|
+
- Keep tests independent and isolated
|
|
258
|
+
- Use `test.beforeEach` for common setup
|
|
259
|
+
- Avoid sharing state between tests
|
|
260
|
+
|
|
261
|
+
### Assertions
|
|
262
|
+
|
|
263
|
+
- Use web-first assertions that auto-wait
|
|
264
|
+
- Prefer `toBeVisible()` over checking element existence
|
|
265
|
+
- Use `toHaveText()` instead of `textContent()`
|
|
266
|
+
|
|
267
|
+
## Troubleshooting
|
|
268
|
+
|
|
269
|
+
### Tests directory already exists
|
|
270
|
+
|
|
271
|
+
If you see a warning about existing tests, the generator will ask for confirmation before overwriting. Make sure to back up any important test files first.
|
|
272
|
+
|
|
273
|
+
### Package.json not found
|
|
274
|
+
|
|
275
|
+
The generator must be run from your project root where `package.json` exists. Navigate to your project directory first:
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
cd your-project
|
|
279
|
+
npx @sogody/experiment-e2e-generator
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Playwright installation fails
|
|
283
|
+
|
|
284
|
+
If Playwright installation fails, try manually installing:
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
yarn add -D @playwright/test playwright
|
|
288
|
+
yarn playwright install
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## Advanced Usage
|
|
292
|
+
|
|
293
|
+
### Custom Test URLs per Environment
|
|
294
|
+
|
|
295
|
+
Use a `.env` file (add it to `.gitignore`):
|
|
296
|
+
|
|
297
|
+
```env
|
|
298
|
+
# .env
|
|
299
|
+
TEST_ENV=staging
|
|
300
|
+
CONTROL_URL=https://staging.samsung.com/nl/control
|
|
301
|
+
EXPERIMENT_URL=https://staging.samsung.com/nl/experiment
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Then use a package like `dotenv`:
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
yarn add -D dotenv
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Running Tests in CI/CD
|
|
311
|
+
|
|
312
|
+
The generated `playwright.config.js` is CI-ready with:
|
|
313
|
+
- Automatic retry on failure
|
|
314
|
+
- Single worker for stability
|
|
315
|
+
- Built-in reporters
|
|
316
|
+
|
|
317
|
+
Example GitHub Actions workflow:
|
|
318
|
+
|
|
319
|
+
```yaml
|
|
320
|
+
name: E2E Tests
|
|
321
|
+
on: [push, pull_request]
|
|
322
|
+
jobs:
|
|
323
|
+
test:
|
|
324
|
+
runs-on: ubuntu-latest
|
|
325
|
+
steps:
|
|
326
|
+
- uses: actions/checkout@v3
|
|
327
|
+
- uses: actions/setup-node@v3
|
|
328
|
+
with:
|
|
329
|
+
node-version: 18
|
|
330
|
+
- run: yarn install --frozen-lockfile
|
|
331
|
+
- run: yarn playwright install --with-deps
|
|
332
|
+
- run: yarn test:e2e
|
|
333
|
+
env:
|
|
334
|
+
CONTROL_URL: ${{ secrets.CONTROL_URL }}
|
|
335
|
+
EXPERIMENT_URL: ${{ secrets.EXPERIMENT_URL }}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Package.json Updates
|
|
339
|
+
|
|
340
|
+
The generator safely updates your `package.json`:
|
|
341
|
+
|
|
342
|
+
### Added Dependencies
|
|
343
|
+
|
|
344
|
+
```json
|
|
345
|
+
{
|
|
346
|
+
"devDependencies": {
|
|
347
|
+
"@playwright/test": "^1.40.0",
|
|
348
|
+
"playwright": "^1.40.0"
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Added Scripts
|
|
354
|
+
|
|
355
|
+
```json
|
|
356
|
+
{
|
|
357
|
+
"scripts": {
|
|
358
|
+
"test:e2e": "playwright test"
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Existing dependencies and scripts are preserved.
|
|
364
|
+
|
|
365
|
+
## Documentation & Resources
|
|
366
|
+
|
|
367
|
+
- [Playwright Documentation](https://playwright.dev/docs/intro)
|
|
368
|
+
- [Playwright Best Practices](https://playwright.dev/docs/best-practices)
|
|
369
|
+
- [Playwright API Reference](https://playwright.dev/docs/api/class-playwright)
|
|
370
|
+
- [Samsung Design System](https://design.samsung.com/)
|
|
371
|
+
|
|
372
|
+
## Support & Contributing
|
|
373
|
+
|
|
374
|
+
For issues, questions, or contributions, please contact the Sogody team.
|
|
375
|
+
|
|
376
|
+
## License
|
|
377
|
+
|
|
378
|
+
MIT
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
**Made with ❤️ by Sogody**
|
package/bin/cli.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "experiment-e2e-generator",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI tool to generate Playwright e2e tests for Experiment Framework projects",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"experiment-e2e-generator": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"templates",
|
|
12
|
+
"src"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"playwright",
|
|
16
|
+
"e2e",
|
|
17
|
+
"testing",
|
|
18
|
+
"experiment-framework",
|
|
19
|
+
"generator"
|
|
20
|
+
],
|
|
21
|
+
"author": "Aldisogody",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=v16.15.1"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"chalk": "^5.6.2",
|
|
28
|
+
"fs-extra": "^11.3.3",
|
|
29
|
+
"prompts": "^2.4.2"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import { copyTemplateFile, ensureDir, toKebabCase } from './utils.js';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
const TEMPLATES_DIR = path.join(__dirname, '..', 'templates');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generate all test files from templates
|
|
12
|
+
* @param {string} targetDir - Target project directory
|
|
13
|
+
* @param {Object} config - Configuration object with user inputs
|
|
14
|
+
*/
|
|
15
|
+
export async function generateTestFiles(targetDir, config) {
|
|
16
|
+
const { experimentName, baseUrl, market } = config;
|
|
17
|
+
const experimentNameKebab = toKebabCase(experimentName);
|
|
18
|
+
|
|
19
|
+
// Template variables
|
|
20
|
+
const variables = {
|
|
21
|
+
EXPERIMENT_NAME: experimentName,
|
|
22
|
+
EXPERIMENT_NAME_KEBAB: experimentNameKebab,
|
|
23
|
+
BASE_URL: baseUrl,
|
|
24
|
+
MARKET: market.toUpperCase(),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Define file mappings: [source, destination]
|
|
28
|
+
const fileMappings = [
|
|
29
|
+
// Root level playwright config
|
|
30
|
+
['playwright.config.js', 'playwright.config.js'],
|
|
31
|
+
|
|
32
|
+
// Config files
|
|
33
|
+
['tests/config/index.js', 'tests/config/index.js'],
|
|
34
|
+
['tests/config/experiment.config.js', 'tests/config/experiment.config.js'],
|
|
35
|
+
['tests/config/qa-links.config.js', 'tests/config/qa-links.config.js'],
|
|
36
|
+
|
|
37
|
+
// Fixtures
|
|
38
|
+
['tests/fixtures/test-fixtures.js', 'tests/fixtures/test-fixtures.js'],
|
|
39
|
+
|
|
40
|
+
// Utils
|
|
41
|
+
['tests/utils/test-helpers.js', 'tests/utils/test-helpers.js'],
|
|
42
|
+
|
|
43
|
+
// Test spec (with dynamic folder name)
|
|
44
|
+
[
|
|
45
|
+
'tests/e2e/experiment-name/experiment.spec.js',
|
|
46
|
+
`tests/e2e/${experimentNameKebab}/${experimentNameKebab}.spec.js`,
|
|
47
|
+
],
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
// Copy and process each template file
|
|
51
|
+
for (const [source, dest] of fileMappings) {
|
|
52
|
+
const sourcePath = path.join(TEMPLATES_DIR, source);
|
|
53
|
+
const destPath = path.join(targetDir, dest);
|
|
54
|
+
|
|
55
|
+
await copyTemplateFile(sourcePath, destPath, variables);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
testsDir: path.join(targetDir, 'tests'),
|
|
60
|
+
experimentDir: path.join(targetDir, 'tests', 'e2e', experimentNameKebab),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if tests directory already exists
|
|
66
|
+
* @param {string} targetDir - Target project directory
|
|
67
|
+
* @returns {Promise<boolean>}
|
|
68
|
+
*/
|
|
69
|
+
export async function testsDirectoryExists(targetDir) {
|
|
70
|
+
const testsPath = path.join(targetDir, 'tests');
|
|
71
|
+
return await fs.pathExists(testsPath);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get list of generated files for display
|
|
76
|
+
* @param {string} experimentName - Experiment name
|
|
77
|
+
* @returns {Array<string>} - List of file paths
|
|
78
|
+
*/
|
|
79
|
+
export function getGeneratedFilesList(experimentName) {
|
|
80
|
+
const experimentNameKebab = toKebabCase(experimentName);
|
|
81
|
+
|
|
82
|
+
return [
|
|
83
|
+
'playwright.config.js',
|
|
84
|
+
'tests/config/index.js',
|
|
85
|
+
'tests/config/experiment.config.js',
|
|
86
|
+
'tests/config/qa-links.config.js',
|
|
87
|
+
'tests/fixtures/test-fixtures.js',
|
|
88
|
+
'tests/utils/test-helpers.js',
|
|
89
|
+
`tests/e2e/${experimentNameKebab}/${experimentNameKebab}.spec.js`,
|
|
90
|
+
];
|
|
91
|
+
}
|
package/src/generator.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { validateProjectDirectory, pathExists } from './utils.js';
|
|
4
|
+
import { getUserInput, confirmAction } from './prompts.js';
|
|
5
|
+
import { generateTestFiles, testsDirectoryExists, getGeneratedFilesList } from './file-operations.js';
|
|
6
|
+
import { updatePackageJson, isPlaywrightInstalled } from './package-updater.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Main generator function
|
|
10
|
+
*/
|
|
11
|
+
export async function generator() {
|
|
12
|
+
const cwd = process.cwd();
|
|
13
|
+
|
|
14
|
+
console.log(chalk.blue.bold('\n🎭 Experiment E2E Test Generator\n'));
|
|
15
|
+
|
|
16
|
+
// Step 1: Pre-flight checks
|
|
17
|
+
console.log(chalk.gray('Running pre-flight checks...'));
|
|
18
|
+
|
|
19
|
+
const validation = await validateProjectDirectory(cwd);
|
|
20
|
+
if (!validation.isValid) {
|
|
21
|
+
console.error(chalk.red(`\n✗ ${validation.error}\n`));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Check if tests directory already exists
|
|
26
|
+
const testsExist = await testsDirectoryExists(cwd);
|
|
27
|
+
if (testsExist) {
|
|
28
|
+
console.log(chalk.yellow('\n⚠ Warning: tests/ directory already exists'));
|
|
29
|
+
const shouldContinue = await confirmAction(
|
|
30
|
+
'Do you want to overwrite existing test files?'
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
if (!shouldContinue) {
|
|
34
|
+
console.log(chalk.gray('\nOperation cancelled.\n'));
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Check if Playwright is already installed
|
|
40
|
+
const playwrightInstalled = await isPlaywrightInstalled(cwd);
|
|
41
|
+
if (playwrightInstalled) {
|
|
42
|
+
console.log(chalk.gray('✓ Playwright is already installed'));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Step 2: Get user input
|
|
46
|
+
console.log(chalk.blue('\nPlease provide the following information:\n'));
|
|
47
|
+
const config = await getUserInput(cwd);
|
|
48
|
+
|
|
49
|
+
// Step 3: Generate files
|
|
50
|
+
console.log(chalk.blue('\n📁 Generating test files...\n'));
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const { testsDir, experimentDir } = await generateTestFiles(cwd, config);
|
|
54
|
+
|
|
55
|
+
const generatedFiles = getGeneratedFilesList(config.experimentName);
|
|
56
|
+
generatedFiles.forEach(file => {
|
|
57
|
+
console.log(chalk.green(` ✓ ${file}`));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Step 4: Update package.json
|
|
61
|
+
console.log(chalk.blue('\n📦 Updating package.json...\n'));
|
|
62
|
+
|
|
63
|
+
const packageResult = await updatePackageJson(cwd);
|
|
64
|
+
if (packageResult.updated) {
|
|
65
|
+
packageResult.changes.forEach(change => {
|
|
66
|
+
console.log(chalk.green(` ✓ ${change}`));
|
|
67
|
+
});
|
|
68
|
+
} else {
|
|
69
|
+
console.log(chalk.gray(' ℹ No package.json updates needed'));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Step 5: Display completion message
|
|
73
|
+
console.log(chalk.green.bold('\n✓ Test structure generated successfully!\n'));
|
|
74
|
+
|
|
75
|
+
console.log(chalk.blue('Next steps:\n'));
|
|
76
|
+
console.log(chalk.white(' 1. Install dependencies:'));
|
|
77
|
+
console.log(chalk.gray(' yarn install\n'));
|
|
78
|
+
|
|
79
|
+
console.log(chalk.white(' 2. Update test URLs in:'));
|
|
80
|
+
console.log(chalk.gray(' tests/config/qa-links.config.js\n'));
|
|
81
|
+
|
|
82
|
+
console.log(chalk.white(' 3. Customize test selectors in:'));
|
|
83
|
+
console.log(chalk.gray(` tests/e2e/${config.experimentName.toLowerCase().replace(/\s+/g, '-')}/${config.experimentName.toLowerCase().replace(/\s+/g, '-')}.spec.js\n`));
|
|
84
|
+
|
|
85
|
+
console.log(chalk.white(' 4. Run tests:'));
|
|
86
|
+
console.log(chalk.gray(' yarn test:e2e\n'));
|
|
87
|
+
|
|
88
|
+
console.log(chalk.blue('📖 Documentation:'));
|
|
89
|
+
console.log(chalk.gray(' https://playwright.dev/docs/intro\n'));
|
|
90
|
+
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error(chalk.red('\n✗ Error generating test files:\n'));
|
|
93
|
+
console.error(chalk.red(error.message));
|
|
94
|
+
if (error.stack) {
|
|
95
|
+
console.error(chalk.gray('\n' + error.stack));
|
|
96
|
+
}
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { mergePackageJson, formatPackageJson } from './utils.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Update package.json with Playwright dependencies and scripts
|
|
7
|
+
* @param {string} targetDir - Target project directory
|
|
8
|
+
* @returns {Promise<{updated: boolean, changes: Array<string>}>}
|
|
9
|
+
*/
|
|
10
|
+
export async function updatePackageJson(targetDir) {
|
|
11
|
+
const packageJsonPath = path.join(targetDir, 'package.json');
|
|
12
|
+
const changes = [];
|
|
13
|
+
|
|
14
|
+
// Read existing package.json
|
|
15
|
+
const existingPackageJson = await fs.readJson(packageJsonPath);
|
|
16
|
+
|
|
17
|
+
// Define additions
|
|
18
|
+
const additions = {
|
|
19
|
+
devDependencies: {
|
|
20
|
+
'@playwright/test': '^1.40.0',
|
|
21
|
+
'playwright': '^1.40.0',
|
|
22
|
+
},
|
|
23
|
+
scripts: {},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Check if devDependencies need to be added
|
|
27
|
+
const existingDevDeps = existingPackageJson.devDependencies || {};
|
|
28
|
+
const needsPlaywright = !existingDevDeps['@playwright/test'];
|
|
29
|
+
|
|
30
|
+
if (needsPlaywright) {
|
|
31
|
+
changes.push('Added Playwright dependencies to devDependencies');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check if test script needs to be added
|
|
35
|
+
const existingScripts = existingPackageJson.scripts || {};
|
|
36
|
+
if (!existingScripts['test:e2e']) {
|
|
37
|
+
additions.scripts['test:e2e'] = 'playwright test';
|
|
38
|
+
changes.push('Added "test:e2e" script');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Only update if there are changes
|
|
42
|
+
if (changes.length === 0) {
|
|
43
|
+
return { updated: false, changes: [] };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Merge and write back
|
|
47
|
+
const updatedPackageJson = mergePackageJson(existingPackageJson, additions);
|
|
48
|
+
const formattedContent = formatPackageJson(updatedPackageJson);
|
|
49
|
+
await fs.writeFile(packageJsonPath, formattedContent, 'utf-8');
|
|
50
|
+
|
|
51
|
+
return { updated: true, changes };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check if Playwright is already installed
|
|
56
|
+
* @param {string} targetDir - Target project directory
|
|
57
|
+
* @returns {Promise<boolean>}
|
|
58
|
+
*/
|
|
59
|
+
export async function isPlaywrightInstalled(targetDir) {
|
|
60
|
+
try {
|
|
61
|
+
const packageJsonPath = path.join(targetDir, 'package.json');
|
|
62
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
63
|
+
|
|
64
|
+
const deps = {
|
|
65
|
+
...(packageJson.dependencies || {}),
|
|
66
|
+
...(packageJson.devDependencies || {}),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return !!deps['@playwright/test'] || !!deps['playwright'];
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/prompts.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import prompts from 'prompts';
|
|
2
|
+
import { detectExperimentName } from './utils.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get user input through interactive prompts
|
|
6
|
+
* @param {string} cwd - Current working directory
|
|
7
|
+
* @returns {Promise<Object>} - User responses
|
|
8
|
+
*/
|
|
9
|
+
export async function getUserInput(cwd) {
|
|
10
|
+
// Try to detect experiment name
|
|
11
|
+
const detectedName = await detectExperimentName(cwd);
|
|
12
|
+
|
|
13
|
+
const questions = [
|
|
14
|
+
{
|
|
15
|
+
type: 'text',
|
|
16
|
+
name: 'experimentName',
|
|
17
|
+
message: 'What is your experiment name?',
|
|
18
|
+
initial: detectedName || '',
|
|
19
|
+
validate: (value) => {
|
|
20
|
+
if (!value.trim()) {
|
|
21
|
+
return 'Experiment name is required';
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
type: 'text',
|
|
28
|
+
name: 'baseUrl',
|
|
29
|
+
message: 'Base URL for tests (e.g., https://www.samsung.com)',
|
|
30
|
+
initial: 'https://www.samsung.com',
|
|
31
|
+
validate: (value) => {
|
|
32
|
+
if (!value.trim()) {
|
|
33
|
+
return 'Base URL is required';
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
new URL(value);
|
|
37
|
+
return true;
|
|
38
|
+
} catch {
|
|
39
|
+
return 'Please enter a valid URL';
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
type: 'text',
|
|
45
|
+
name: 'market',
|
|
46
|
+
message: 'Primary market code (e.g., NL, BE)',
|
|
47
|
+
initial: 'NL',
|
|
48
|
+
validate: (value) => {
|
|
49
|
+
if (!value.trim()) {
|
|
50
|
+
return 'Market code is required';
|
|
51
|
+
}
|
|
52
|
+
if (value.length > 5) {
|
|
53
|
+
return 'Market code should be short (e.g., NL, BE, UK)';
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const response = await prompts(questions, {
|
|
61
|
+
onCancel: () => {
|
|
62
|
+
console.log('\nOperation cancelled by user');
|
|
63
|
+
process.exit(0);
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return response;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Ask user for confirmation before overwriting
|
|
72
|
+
* @param {string} message - Confirmation message
|
|
73
|
+
* @returns {Promise<boolean>}
|
|
74
|
+
*/
|
|
75
|
+
export async function confirmAction(message) {
|
|
76
|
+
const response = await prompts({
|
|
77
|
+
type: 'confirm',
|
|
78
|
+
name: 'value',
|
|
79
|
+
message,
|
|
80
|
+
initial: false,
|
|
81
|
+
}, {
|
|
82
|
+
onCancel: () => {
|
|
83
|
+
return false;
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return response.value;
|
|
88
|
+
}
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Convert string to kebab-case
|
|
6
|
+
* @param {string} str - Input string
|
|
7
|
+
* @returns {string} - kebab-case string
|
|
8
|
+
*/
|
|
9
|
+
export function toKebabCase(str) {
|
|
10
|
+
return str
|
|
11
|
+
.trim()
|
|
12
|
+
.toLowerCase()
|
|
13
|
+
.replace(/[\s_]+/g, '-')
|
|
14
|
+
.replace(/[^\w-]+/g, '')
|
|
15
|
+
.replace(/--+/g, '-')
|
|
16
|
+
.replace(/^-+/, '')
|
|
17
|
+
.replace(/-+$/, '');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Replace template variables in content
|
|
22
|
+
* @param {string} content - Template content
|
|
23
|
+
* @param {Object} variables - Variables to replace
|
|
24
|
+
* @returns {string} - Content with replaced variables
|
|
25
|
+
*/
|
|
26
|
+
export function replaceTemplateVars(content, variables) {
|
|
27
|
+
let result = content;
|
|
28
|
+
|
|
29
|
+
Object.entries(variables).forEach(([key, value]) => {
|
|
30
|
+
const placeholder = new RegExp(`{{${key}}}`, 'g');
|
|
31
|
+
result = result.replace(placeholder, value);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check if a path exists
|
|
39
|
+
* @param {string} targetPath - Path to check
|
|
40
|
+
* @returns {Promise<boolean>}
|
|
41
|
+
*/
|
|
42
|
+
export async function pathExists(targetPath) {
|
|
43
|
+
try {
|
|
44
|
+
await fs.access(targetPath);
|
|
45
|
+
return true;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Safely merge package.json dependencies
|
|
53
|
+
* @param {Object} existing - Existing package.json content
|
|
54
|
+
* @param {Object} additions - New dependencies to add
|
|
55
|
+
* @returns {Object} - Merged package.json
|
|
56
|
+
*/
|
|
57
|
+
export function mergePackageJson(existing, additions) {
|
|
58
|
+
const merged = { ...existing };
|
|
59
|
+
|
|
60
|
+
// Merge devDependencies
|
|
61
|
+
if (additions.devDependencies) {
|
|
62
|
+
merged.devDependencies = {
|
|
63
|
+
...merged.devDependencies,
|
|
64
|
+
...additions.devDependencies,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Merge scripts (don't overwrite existing)
|
|
69
|
+
if (additions.scripts) {
|
|
70
|
+
merged.scripts = {
|
|
71
|
+
...merged.scripts,
|
|
72
|
+
...additions.scripts,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return merged;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Attempt to detect experiment name from project structure
|
|
81
|
+
* @param {string} cwd - Current working directory
|
|
82
|
+
* @returns {Promise<string|null>} - Detected experiment name or null
|
|
83
|
+
*/
|
|
84
|
+
export async function detectExperimentName(cwd) {
|
|
85
|
+
try {
|
|
86
|
+
// Try to read package.json
|
|
87
|
+
const packageJsonPath = path.join(cwd, 'package.json');
|
|
88
|
+
if (await pathExists(packageJsonPath)) {
|
|
89
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
90
|
+
if (packageJson.name) {
|
|
91
|
+
// Extract experiment name from package name
|
|
92
|
+
const name = packageJson.name.replace(/^@[^/]+\//, '');
|
|
93
|
+
return name;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Try to find experiment folder structure
|
|
98
|
+
const srcPath = path.join(cwd, 'src');
|
|
99
|
+
if (await pathExists(srcPath)) {
|
|
100
|
+
const componentsPath = path.join(srcPath, 'components');
|
|
101
|
+
if (await pathExists(componentsPath)) {
|
|
102
|
+
const components = await fs.readdir(componentsPath);
|
|
103
|
+
if (components.length > 0) {
|
|
104
|
+
// Use first component name as experiment name
|
|
105
|
+
return components[0].replace(/\.(jsx?|tsx?)$/, '');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} catch (error) {
|
|
110
|
+
// Ignore detection errors
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Format package.json content with proper indentation
|
|
118
|
+
* @param {Object} content - Package.json object
|
|
119
|
+
* @returns {string} - Formatted JSON string
|
|
120
|
+
*/
|
|
121
|
+
export function formatPackageJson(content) {
|
|
122
|
+
return JSON.stringify(content, null, 2) + '\n';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Validate that current directory is a valid project
|
|
127
|
+
* @param {string} cwd - Current working directory
|
|
128
|
+
* @returns {Promise<{isValid: boolean, error?: string}>}
|
|
129
|
+
*/
|
|
130
|
+
export async function validateProjectDirectory(cwd) {
|
|
131
|
+
// Check if package.json exists
|
|
132
|
+
const packageJsonPath = path.join(cwd, 'package.json');
|
|
133
|
+
if (!await pathExists(packageJsonPath)) {
|
|
134
|
+
return {
|
|
135
|
+
isValid: false,
|
|
136
|
+
error: 'No package.json found in current directory. Please run this command from your project root.',
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return { isValid: true };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Create directory if it doesn't exist
|
|
145
|
+
* @param {string} dirPath - Directory path
|
|
146
|
+
*/
|
|
147
|
+
export async function ensureDir(dirPath) {
|
|
148
|
+
await fs.ensureDir(dirPath);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Copy file with template variable replacement
|
|
153
|
+
* @param {string} sourcePath - Source file path
|
|
154
|
+
* @param {string} destPath - Destination file path
|
|
155
|
+
* @param {Object} variables - Template variables
|
|
156
|
+
*/
|
|
157
|
+
export async function copyTemplateFile(sourcePath, destPath, variables) {
|
|
158
|
+
const content = await fs.readFile(sourcePath, 'utf-8');
|
|
159
|
+
const processedContent = replaceTemplateVars(content, variables);
|
|
160
|
+
await ensureDir(path.dirname(destPath));
|
|
161
|
+
await fs.writeFile(destPath, processedContent, 'utf-8');
|
|
162
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Playwright configuration for {{EXPERIMENT_NAME}} experiment tests
|
|
5
|
+
* @see https://playwright.dev/docs/test-configuration
|
|
6
|
+
*/
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
testDir: './tests',
|
|
9
|
+
fullyParallel: true,
|
|
10
|
+
reporter: 'html',
|
|
11
|
+
use: {
|
|
12
|
+
trace: 'on-first-retry',
|
|
13
|
+
screenshot: 'only-on-failure',
|
|
14
|
+
},
|
|
15
|
+
projects: [
|
|
16
|
+
{
|
|
17
|
+
name: 'chromium',
|
|
18
|
+
use: { ...devices['Desktop Chrome'] },
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'firefox',
|
|
22
|
+
use: { ...devices['Desktop Firefox'] },
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'webkit',
|
|
26
|
+
use: { ...devices['Desktop Safari'] },
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Experiment configuration for {{EXPERIMENT_NAME}}
|
|
3
|
+
* Contains experiment-specific settings and environment variables
|
|
4
|
+
*/
|
|
5
|
+
export const experimentConfig = {
|
|
6
|
+
name: '{{EXPERIMENT_NAME}}',
|
|
7
|
+
market: '{{MARKET}}',
|
|
8
|
+
baseUrl: '{{BASE_URL}}',
|
|
9
|
+
|
|
10
|
+
// Experiment variants
|
|
11
|
+
variants: {
|
|
12
|
+
control: 'control',
|
|
13
|
+
experiment: 'experiment',
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
// Timeout configurations (in milliseconds)
|
|
17
|
+
timeouts: {
|
|
18
|
+
navigation: 30000,
|
|
19
|
+
element: 5000,
|
|
20
|
+
api: 10000,
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
// Test environment
|
|
24
|
+
environment: process.env.TEST_ENV || 'qa',
|
|
25
|
+
|
|
26
|
+
// Adobe Target preview token (if applicable)
|
|
27
|
+
adobePreviewToken: process.env.ADOBE_PREVIEW_TOKEN || '',
|
|
28
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const qaLinksConfig = {
|
|
2
|
+
controlUrl: process.env.CONTROL_URL || '{{BASE_URL}}/{{MARKET}}/control-page/',
|
|
3
|
+
experimentUrl: process.env.EXPERIMENT_URL || '{{BASE_URL}}/{{MARKET}}/experiment-page/',
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Validates that required URLs are configured
|
|
8
|
+
* @throws {Error} if required URLs are missing
|
|
9
|
+
*/
|
|
10
|
+
export function validateUrls() {
|
|
11
|
+
if (!qaLinksConfig.controlUrl || !qaLinksConfig.experimentUrl) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
'Missing required URLs. Please set CONTROL_URL and EXPERIMENT_URL environment variables ' +
|
|
14
|
+
'or update the URLs in tests/config/qa-links.config.js'
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test';
|
|
2
|
+
import { qaLinksConfig, validateUrls } from '../../config/index.js';
|
|
3
|
+
|
|
4
|
+
test.describe('{{EXPERIMENT_NAME}} - Control', () => {
|
|
5
|
+
test.beforeEach(async ({ page }) => {
|
|
6
|
+
validateUrls();
|
|
7
|
+
await page.goto(qaLinksConfig.controlUrl);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test('should not display experiment component', async ({ page }) => {
|
|
11
|
+
// TODO: Update this selector to match your experiment component
|
|
12
|
+
const experimentComponent = page.locator('[data-experiment="{{EXPERIMENT_NAME_KEBAB}}"]');
|
|
13
|
+
|
|
14
|
+
// Verify the experiment component is not visible in control
|
|
15
|
+
await expect(experimentComponent).not.toBeVisible();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('should display baseline content', async ({ page }) => {
|
|
19
|
+
// TODO: Add assertions to verify baseline/control content
|
|
20
|
+
// Example:
|
|
21
|
+
// await expect(page.getByRole('heading', { name: 'Control Heading' })).toBeVisible();
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test.describe('{{EXPERIMENT_NAME}} - Experiment', () => {
|
|
26
|
+
test.beforeEach(async ({ page }) => {
|
|
27
|
+
validateUrls();
|
|
28
|
+
await page.goto(qaLinksConfig.experimentUrl);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('should display experiment component', async ({ page }) => {
|
|
32
|
+
// TODO: Update this selector to match your experiment component
|
|
33
|
+
const experimentComponent = page.locator('[data-experiment="{{EXPERIMENT_NAME_KEBAB}}"]');
|
|
34
|
+
|
|
35
|
+
// Verify the experiment component is visible
|
|
36
|
+
await expect(experimentComponent).toBeVisible();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('should have correct experiment styling', async ({ page }) => {
|
|
40
|
+
// TODO: Add assertions to verify experiment-specific styling or content
|
|
41
|
+
// Example:
|
|
42
|
+
// const experimentButton = page.getByRole('button', { name: 'Experiment CTA' });
|
|
43
|
+
// await expect(experimentButton).toBeVisible();
|
|
44
|
+
// await expect(experimentButton).toHaveCSS('background-color', 'rgb(0, 119, 200)');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('should handle user interactions correctly', async ({ page }) => {
|
|
48
|
+
// TODO: Add test for user interactions with the experiment component
|
|
49
|
+
// Example:
|
|
50
|
+
// await page.getByRole('button', { name: 'Click Me' }).click();
|
|
51
|
+
// await expect(page.getByText('Success!')).toBeVisible();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { test as base } from '@playwright/test';
|
|
2
|
+
import { experimentConfig } from '../config/index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Custom test fixtures for {{EXPERIMENT_NAME}} experiment
|
|
6
|
+
* Extend Playwright's base test with custom fixtures and utilities
|
|
7
|
+
*/
|
|
8
|
+
export const test = base.extend({
|
|
9
|
+
/**
|
|
10
|
+
* Custom fixture for experiment context
|
|
11
|
+
* Provides experiment-specific configuration and utilities
|
|
12
|
+
*/
|
|
13
|
+
experimentContext: async ({}, use) => {
|
|
14
|
+
const context = {
|
|
15
|
+
name: experimentConfig.name,
|
|
16
|
+
market: experimentConfig.market,
|
|
17
|
+
baseUrl: experimentConfig.baseUrl,
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Helper to construct URLs with experiment parameters
|
|
21
|
+
* @param {string} path - URL path
|
|
22
|
+
* @param {Object} params - Additional query parameters
|
|
23
|
+
*/
|
|
24
|
+
buildUrl: (path, params = {}) => {
|
|
25
|
+
const url = new URL(path, experimentConfig.baseUrl);
|
|
26
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
27
|
+
url.searchParams.append(key, value);
|
|
28
|
+
});
|
|
29
|
+
return url.toString();
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
await use(context);
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Custom fixture for Adobe Target preview (if applicable)
|
|
38
|
+
*/
|
|
39
|
+
adobePreview: async ({ page }, use) => {
|
|
40
|
+
if (experimentConfig.adobePreviewToken) {
|
|
41
|
+
// Add Adobe preview token to page context
|
|
42
|
+
await page.addInitScript((token) => {
|
|
43
|
+
window.adobePreviewToken = token;
|
|
44
|
+
}, experimentConfig.adobePreviewToken);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
await use(page);
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export { expect } from '@playwright/test';
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test helper utilities for {{EXPERIMENT_NAME}} experiment
|
|
3
|
+
* Reusable functions for common test operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Wait for network idle state
|
|
8
|
+
* Useful for waiting for async operations to complete
|
|
9
|
+
* @param {Page} page - Playwright page object
|
|
10
|
+
* @param {number} timeout - Maximum wait time in milliseconds
|
|
11
|
+
*/
|
|
12
|
+
export async function waitForNetworkIdle(page, timeout = 5000) {
|
|
13
|
+
await page.waitForLoadState('networkidle', { timeout });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Wait for a specific element to be stable (no animations)
|
|
18
|
+
* @param {Locator} locator - Playwright locator
|
|
19
|
+
* @param {number} timeout - Maximum wait time in milliseconds
|
|
20
|
+
*/
|
|
21
|
+
export async function waitForStable(locator, timeout = 3000) {
|
|
22
|
+
await locator.waitFor({ state: 'visible', timeout });
|
|
23
|
+
// Wait a bit for any CSS transitions/animations to complete
|
|
24
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get computed style of an element
|
|
29
|
+
* @param {Locator} locator - Playwright locator
|
|
30
|
+
* @param {string} property - CSS property name
|
|
31
|
+
* @returns {Promise<string>} - Computed style value
|
|
32
|
+
*/
|
|
33
|
+
export async function getComputedStyle(locator, property) {
|
|
34
|
+
return await locator.evaluate((el, prop) => {
|
|
35
|
+
return window.getComputedStyle(el).getPropertyValue(prop);
|
|
36
|
+
}, property);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if element is in viewport
|
|
41
|
+
* @param {Locator} locator - Playwright locator
|
|
42
|
+
* @returns {Promise<boolean>}
|
|
43
|
+
*/
|
|
44
|
+
export async function isInViewport(locator) {
|
|
45
|
+
return await locator.evaluate((el) => {
|
|
46
|
+
const rect = el.getBoundingClientRect();
|
|
47
|
+
return (
|
|
48
|
+
rect.top >= 0 &&
|
|
49
|
+
rect.left >= 0 &&
|
|
50
|
+
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
|
51
|
+
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Scroll element into view smoothly
|
|
58
|
+
* @param {Locator} locator - Playwright locator
|
|
59
|
+
*/
|
|
60
|
+
export async function scrollIntoView(locator) {
|
|
61
|
+
await locator.evaluate((el) => {
|
|
62
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
63
|
+
});
|
|
64
|
+
// Wait for scroll to complete
|
|
65
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Take a screenshot with a descriptive name
|
|
70
|
+
* @param {Page} page - Playwright page object
|
|
71
|
+
* @param {string} name - Screenshot name
|
|
72
|
+
* @returns {Promise<Buffer>}
|
|
73
|
+
*/
|
|
74
|
+
export async function takeScreenshot(page, name) {
|
|
75
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
76
|
+
return await page.screenshot({
|
|
77
|
+
path: `screenshots/${name}-${timestamp}.png`,
|
|
78
|
+
fullPage: true,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Wait for specific text to appear on page
|
|
84
|
+
* @param {Page} page - Playwright page object
|
|
85
|
+
* @param {string} text - Text to wait for
|
|
86
|
+
* @param {number} timeout - Maximum wait time in milliseconds
|
|
87
|
+
*/
|
|
88
|
+
export async function waitForText(page, text, timeout = 5000) {
|
|
89
|
+
await page.waitForSelector(`text=${text}`, { timeout });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Retry an action with exponential backoff
|
|
94
|
+
* @param {Function} action - Async function to retry
|
|
95
|
+
* @param {number} maxRetries - Maximum number of retry attempts
|
|
96
|
+
* @param {number} initialDelay - Initial delay in milliseconds
|
|
97
|
+
*/
|
|
98
|
+
export async function retryWithBackoff(action, maxRetries = 3, initialDelay = 1000) {
|
|
99
|
+
let lastError;
|
|
100
|
+
|
|
101
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
102
|
+
try {
|
|
103
|
+
return await action();
|
|
104
|
+
} catch (error) {
|
|
105
|
+
lastError = error;
|
|
106
|
+
if (i < maxRetries - 1) {
|
|
107
|
+
const delay = initialDelay * Math.pow(2, i);
|
|
108
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
throw lastError;
|
|
114
|
+
}
|