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 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
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { generator } from '../src/generator.js';
4
+
5
+ // Run the generator
6
+ generator().catch((error) => {
7
+ console.error('Unexpected error:', error);
8
+ process.exit(1);
9
+ });
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
+ }
@@ -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,5 @@
1
+ /**
2
+ * Central configuration export for {{EXPERIMENT_NAME}} tests
3
+ */
4
+ export { experimentConfig } from './experiment.config.js';
5
+ export { qaLinksConfig, validateUrls } from './qa-links.config.js';
@@ -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
+ }