create-playwright-ts-framework 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +66 -0
  3. package/dist/cli/generator.d.ts +2 -0
  4. package/dist/cli/generator.js +43 -0
  5. package/dist/cli/generator.js.map +1 -0
  6. package/dist/cli/index.d.ts +2 -0
  7. package/dist/cli/index.js +20 -0
  8. package/dist/cli/index.js.map +1 -0
  9. package/dist/cli/logger.d.ts +6 -0
  10. package/dist/cli/logger.js +8 -0
  11. package/dist/cli/logger.js.map +1 -0
  12. package/dist/cli/prompts.d.ts +2 -0
  13. package/dist/cli/prompts.js +13 -0
  14. package/dist/cli/prompts.js.map +1 -0
  15. package/dist/generators/project.generator.d.ts +2 -0
  16. package/dist/generators/project.generator.js +84 -0
  17. package/dist/generators/project.generator.js.map +1 -0
  18. package/dist/templates/allure/scripts/prepare-allure.mjs +9 -0
  19. package/dist/templates/allure/scripts/rename-allure-report.mjs +2 -0
  20. package/dist/templates/api/api/builders/RequestBuilder.ts +15 -0
  21. package/dist/templates/api/api/clients/ApiClient.ts +26 -0
  22. package/dist/templates/api/api/models/requests/UserRequest.ts +1 -0
  23. package/dist/templates/api/api/models/responses/UserResponse.ts +1 -0
  24. package/dist/templates/api/api/services/UserService.ts +11 -0
  25. package/dist/templates/api/api/validators/ApiResponseValidator.ts +13 -0
  26. package/dist/templates/bdd/bdd.config.ts +6 -0
  27. package/dist/templates/bdd/features/api/users.feature +4 -0
  28. package/dist/templates/bdd/features/ui/login.feature +5 -0
  29. package/dist/templates/bdd/steps/api/users.steps.ts +17 -0
  30. package/dist/templates/bdd/steps/ui/login.steps.ts +13 -0
  31. package/dist/templates/common/.env.example +6 -0
  32. package/dist/templates/common/.nvmrc +1 -0
  33. package/dist/templates/common/LICENSE +21 -0
  34. package/dist/templates/common/README.md.template +120 -0
  35. package/dist/templates/common/config/environments.ts +13 -0
  36. package/dist/templates/common/config/framework.config.ts +5 -0
  37. package/dist/templates/common/config/run.config.json +11 -0
  38. package/dist/templates/common/eslint.config.js +8 -0
  39. package/dist/templates/common/fixtures/testFixture.ts +9 -0
  40. package/dist/templates/common/package.json.template +27 -0
  41. package/dist/templates/common/playwright.config.ts +26 -0
  42. package/dist/templates/common/scripts/clean.mjs +2 -0
  43. package/dist/templates/common/scripts/run-tests.mjs +10 -0
  44. package/dist/templates/common/test-data/__ENV__/testdata.json +9 -0
  45. package/dist/templates/common/tsconfig.json +15 -0
  46. package/dist/templates/common/utils/Constants.ts +2 -0
  47. package/dist/templates/common/utils/DateHelper.ts +2 -0
  48. package/dist/templates/common/utils/EnvConfig.ts +49 -0
  49. package/dist/templates/common/utils/JsonHelper.ts +7 -0
  50. package/dist/templates/common/utils/Logger.ts +10 -0
  51. package/dist/templates/common/utils/TestData.ts +16 -0
  52. package/dist/templates/github/.github/workflows/playwright.yml +45 -0
  53. package/dist/templates/non-bdd/tests/api/users.spec.ts +13 -0
  54. package/dist/templates/non-bdd/tests/ui/login.spec.ts +11 -0
  55. package/dist/templates/ui/pages/BasePage.ts +11 -0
  56. package/dist/templates/ui/pages/LoginPage.ts +23 -0
  57. package/dist/types.d.ts +11 -0
  58. package/dist/types.js +2 -0
  59. package/dist/types.js.map +1 -0
  60. package/dist/utils/files.d.ts +2 -0
  61. package/dist/utils/files.js +25 -0
  62. package/dist/utils/files.js.map +1 -0
  63. package/dist/utils/project-name.d.ts +1 -0
  64. package/dist/utils/project-name.js +9 -0
  65. package/dist/utils/project-name.js.map +1 -0
  66. package/package.json +34 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 create-playwright-ts-framework contributors
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,66 @@
1
+ # Playwright + TypeScript Automation Framework Scaffold
2
+
3
+ `create-playwright-ts-framework` is an interactive, composable CLI generator for strict TypeScript Playwright projects. It can generate BDD (`playwright-bdd`) or standard Playwright Test projects with UI, API, or combined automation, environment-specific data, optional Allure reporting, and optional GitHub Actions.
4
+
5
+ > The package name is provisional. This repository is prepared for publication but has not been published.
6
+
7
+ ## Requirements
8
+
9
+ Node.js 22 or newer and npm 10 or newer are recommended for developing and packaging the generator.
10
+
11
+ ## Local CLI usage
12
+
13
+ ```bash
14
+ npm install
15
+ npm run build
16
+ npm link
17
+ create-playwright-ts-framework
18
+ ```
19
+
20
+ Or test without linking:
21
+
22
+ ```bash
23
+ node dist/cli/index.js
24
+ ```
25
+
26
+ The prompts choose project name, framework style, test type, Allure, GitHub Actions, and automatic dependency installation. Every project includes smoke and UAT data; runtime selection is centralized in `config/run.config.json` rather than asked during generation.
27
+
28
+ ## Generator architecture
29
+
30
+ ```text
31
+ src/
32
+ ├── cli/ prompts, presentation, process orchestration
33
+ ├── generators/ conditional project composition
34
+ ├── utils/ validation and filesystem utilities
35
+ ├── templates/
36
+ │ ├── common/ config, fixtures, data, scripts, docs
37
+ │ ├── bdd/ Gherkin, steps, playwright-bdd config
38
+ │ ├── non-bdd/ standard Playwright specs
39
+ │ ├── ui/ Page Object layer
40
+ │ ├── api/ request/client/service/model/validator layers
41
+ │ ├── allure/ runtime metadata
42
+ │ └── github/ CI workflow
43
+ └── types.ts
44
+ ```
45
+
46
+ Templates are copied and composed based on the selected capabilities; unused test layers are removed. Placeholder substitution is limited to configuration values and small conditional source fragments—project files remain individually maintainable templates.
47
+
48
+ ## Development
49
+
50
+ ```bash
51
+ npm run build
52
+ npm run typecheck
53
+ npm run lint
54
+ npm test
55
+ npm run format:check
56
+ ```
57
+
58
+ Generator tests create all six BDD/non-BDD × UI/API/UI+API combinations in temporary directories and assert inclusion/exclusion rules. See the README generated inside each project for framework commands, architecture, test data, secrets, Allure, and CI guidance.
59
+
60
+ ## Publishing checklist
61
+
62
+ Before a future release: confirm the final npm name, update repository/author metadata, run `npm pack --dry-run`, choose and tag a release version, authenticate with npm, and publish. Publishing is deliberately outside this phase.
63
+
64
+ ## License
65
+
66
+ MIT
@@ -0,0 +1,2 @@
1
+ import type { GeneratorOptions } from '../types.js';
2
+ export declare function runGenerator(options: GeneratorOptions): Promise<void>;
@@ -0,0 +1,43 @@
1
+ import ora from 'ora';
2
+ import { spawn } from 'node:child_process';
3
+ import { generateProject } from '../generators/project.generator.js';
4
+ function install(cwd) {
5
+ return new Promise((resolve, reject) => {
6
+ const child = spawn(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['install'], { cwd, stdio: 'inherit' });
7
+ const timeout = setTimeout(() => {
8
+ child.kill();
9
+ reject(new Error('npm install timed out after 3 minutes. The framework files were created; run npm install inside the project when network access is available.'));
10
+ }, 180_000);
11
+ child.on('error', error => { clearTimeout(timeout); reject(error); });
12
+ child.on('exit', code => {
13
+ clearTimeout(timeout);
14
+ if (code === 0)
15
+ resolve();
16
+ else
17
+ reject(new Error(`npm install exited with code ${String(code)}. The framework files were created; run npm install inside the project to retry.`));
18
+ });
19
+ });
20
+ }
21
+ export async function runGenerator(options) {
22
+ const spinner = ora('Creating framework').start();
23
+ try {
24
+ await generateProject(options);
25
+ spinner.succeed('Framework files created');
26
+ }
27
+ catch (error) {
28
+ spinner.fail('Framework creation failed');
29
+ throw error;
30
+ }
31
+ if (options.installDependencies) {
32
+ spinner.start('Installing dependencies (this may take a few minutes on the first run)');
33
+ try {
34
+ await install(options.destination);
35
+ spinner.succeed('Dependencies installed');
36
+ }
37
+ catch (error) {
38
+ spinner.fail('Dependency installation failed');
39
+ throw error;
40
+ }
41
+ }
42
+ }
43
+ //# sourceMappingURL=generator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generator.js","sourceRoot":"","sources":["../../src/cli/generator.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,KAAK,CAAC;AACtB,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAE3C,OAAO,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAErE,SAAS,OAAO,CAAC,GAAW;IAC1B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,SAAS,CAAC,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;QAC9G,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE;YAC9B,KAAK,CAAC,IAAI,EAAE,CAAC;YACb,MAAM,CAAC,IAAI,KAAK,CAAC,+IAA+I,CAAC,CAAC,CAAC;QACrK,CAAC,EAAE,OAAO,CAAC,CAAC;QACZ,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACtE,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE;YACtB,YAAY,CAAC,OAAO,CAAC,CAAC;YACtB,IAAI,IAAI,KAAK,CAAC;gBAAE,OAAO,EAAE,CAAC;;gBACrB,MAAM,CAAC,IAAI,KAAK,CAAC,gCAAgC,MAAM,CAAC,IAAI,CAAC,kFAAkF,CAAC,CAAC,CAAC;QACzJ,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,OAAyB;IAC1D,MAAM,OAAO,GAAG,GAAG,CAAC,oBAAoB,CAAC,CAAC,KAAK,EAAE,CAAC;IAClD,IAAI,CAAC;QACH,MAAM,eAAe,CAAC,OAAO,CAAC,CAAC;QAC/B,OAAO,CAAC,OAAO,CAAC,yBAAyB,CAAC,CAAC;IAC7C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC;QAC1C,MAAM,KAAK,CAAC;IACd,CAAC;IACD,IAAI,OAAO,CAAC,mBAAmB,EAAE,CAAC;QAChC,OAAO,CAAC,KAAK,CAAC,wEAAwE,CAAC,CAAC;QACxF,IAAI,CAAC;YAAC,MAAM,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;YAAC,OAAO,CAAC,OAAO,CAAC,wBAAwB,CAAC,CAAC;QAAC,CAAC;QACtF,OAAO,KAAK,EAAE,CAAC;YAAC,OAAO,CAAC,IAAI,CAAC,gCAAgC,CAAC,CAAC;YAAC,MAAM,KAAK,CAAC;QAAC,CAAC;IAChF,CAAC;AACH,CAAC"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ import { collectOptions } from './prompts.js';
3
+ import { runGenerator } from './generator.js';
4
+ import { logger } from './logger.js';
5
+ async function main() {
6
+ if (Number.parseInt(process.versions.node.split('.')[0] ?? '0', 10) < 20)
7
+ throw new Error('Node.js 20 or newer is required.');
8
+ logger.heading();
9
+ const options = await collectOptions();
10
+ await runGenerator(options);
11
+ logger.success('Framework successfully created.');
12
+ console.log(`\nNext:\n cd ${options.projectName}\n npm run test:smoke`);
13
+ }
14
+ main().catch((error) => {
15
+ logger.error(error instanceof Error ? error.message : String(error));
16
+ if (process.env.DEBUG && error instanceof Error)
17
+ console.error(error.stack);
18
+ process.exitCode = 1;
19
+ });
20
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,KAAK,UAAU,IAAI;IACjB,IAAI,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,EAAE,EAAE,CAAC,GAAG,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;IAC9H,MAAM,CAAC,OAAO,EAAE,CAAC;IACjB,MAAM,OAAO,GAAG,MAAM,cAAc,EAAE,CAAC;IACvC,MAAM,YAAY,CAAC,OAAO,CAAC,CAAC;IAC5B,MAAM,CAAC,OAAO,CAAC,iCAAiC,CAAC,CAAC;IAClD,OAAO,CAAC,GAAG,CAAC,iBAAiB,OAAO,CAAC,WAAW,wBAAwB,CAAC,CAAC;AAC5E,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IAC9B,MAAM,CAAC,KAAK,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACrE,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,IAAI,KAAK,YAAY,KAAK;QAAE,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC5E,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,6 @@
1
+ export declare const logger: {
2
+ heading: () => void;
3
+ info: (message: string) => void;
4
+ success: (message: string) => void;
5
+ error: (message: string) => void;
6
+ };
@@ -0,0 +1,8 @@
1
+ import chalk from 'chalk';
2
+ export const logger = {
3
+ heading: () => console.log(chalk.bold.cyan('\n🚀 Playwright TypeScript Framework Generator\n')),
4
+ info: (message) => console.log(chalk.blue('ℹ'), message),
5
+ success: (message) => console.log(chalk.green('✔'), message),
6
+ error: (message) => console.error(chalk.red('✖'), message),
7
+ };
8
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/cli/logger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,OAAO,EAAE,GAAS,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,kDAAkD,CAAC,CAAC;IACrG,IAAI,EAAE,CAAC,OAAe,EAAQ,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC;IACtE,OAAO,EAAE,CAAC,OAAe,EAAQ,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC;IAC1E,KAAK,EAAE,CAAC,OAAe,EAAQ,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC;CACzE,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { GeneratorOptions } from '../types.js';
2
+ export declare function collectOptions(cwd?: string): Promise<GeneratorOptions>;
@@ -0,0 +1,13 @@
1
+ import { confirm, input, select } from '@inquirer/prompts';
2
+ import { resolve } from 'node:path';
3
+ import { validateProjectName } from '../utils/project-name.js';
4
+ export async function collectOptions(cwd = process.cwd()) {
5
+ const projectName = await input({ message: 'Project name:', default: 'my-playwright-framework', validate: validateProjectName });
6
+ const framework = await select({ message: 'Select framework style:', choices: [{ name: 'BDD', value: 'bdd' }, { name: 'Non-BDD', value: 'non-bdd' }] });
7
+ const testingType = await select({ message: 'Select testing type:', choices: [{ name: 'UI', value: 'ui' }, { name: 'API', value: 'api' }, { name: 'UI + API', value: 'ui-api' }] });
8
+ const allure = await confirm({ message: 'Include Allure reporting?', default: true });
9
+ const githubActions = await confirm({ message: 'Include GitHub Actions?', default: true });
10
+ const installDependencies = await confirm({ message: 'Install dependencies automatically?', default: true });
11
+ return { projectName, destination: resolve(cwd, projectName), framework, testingType, allure, githubActions, installDependencies };
12
+ }
13
+ //# sourceMappingURL=prompts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompts.js","sourceRoot":"","sources":["../../src/cli/prompts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAE/D,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE;IACtD,MAAM,WAAW,GAAG,MAAM,KAAK,CAAC,EAAE,OAAO,EAAE,eAAe,EAAE,OAAO,EAAE,yBAAyB,EAAE,QAAQ,EAAE,mBAAmB,EAAE,CAAC,CAAC;IACjI,MAAM,SAAS,GAAG,MAAM,MAAM,CAAiB,EAAE,OAAO,EAAE,yBAAyB,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC,CAAC;IACxK,MAAM,WAAW,GAAG,MAAM,MAAM,CAAc,EAAE,OAAO,EAAE,sBAAsB,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC;IACjM,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,EAAE,OAAO,EAAE,2BAA2B,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;IACtF,MAAM,aAAa,GAAG,MAAM,OAAO,CAAC,EAAE,OAAO,EAAE,yBAAyB,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3F,MAAM,mBAAmB,GAAG,MAAM,OAAO,CAAC,EAAE,OAAO,EAAE,qCAAqC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7G,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,OAAO,CAAC,GAAG,EAAE,WAAW,CAAC,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,mBAAmB,EAAE,CAAC;AACrI,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { GeneratorOptions } from '../types.js';
2
+ export declare function generateProject(options: GeneratorOptions): Promise<void>;
@@ -0,0 +1,84 @@
1
+ import { access, mkdir, rm } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { copyTemplate, replaceTokens } from '../utils/files.js';
5
+ import { validateProjectName } from '../utils/project-name.js';
6
+ const templateRoot = join(dirname(fileURLToPath(import.meta.url)), '..', 'templates');
7
+ const environments = ['smoke', 'uat'];
8
+ const hasUi = (type) => type !== 'api';
9
+ const hasApi = (type) => type !== 'ui';
10
+ export async function generateProject(options) {
11
+ const validity = validateProjectName(options.projectName);
12
+ if (validity !== true)
13
+ throw new Error(validity);
14
+ if (!['bdd', 'non-bdd'].includes(options.framework))
15
+ throw new Error(`Unsupported framework style: ${String(options.framework)}`);
16
+ if (!['ui', 'api', 'ui-api'].includes(options.testingType))
17
+ throw new Error(`Unsupported testing type: ${String(options.testingType)}`);
18
+ try {
19
+ await access(options.destination);
20
+ throw new Error(`Destination already exists: ${options.destination}\nChoose a different project name or remove that existing folder first.`);
21
+ }
22
+ catch (error) {
23
+ if (error instanceof Error && error.message.startsWith('Destination already exists'))
24
+ throw error;
25
+ if (!(error instanceof Error) || !('code' in error) || error.code !== 'ENOENT')
26
+ throw error;
27
+ }
28
+ await mkdir(options.destination, { recursive: false });
29
+ try {
30
+ await copyTemplate(join(templateRoot, 'common'), options.destination);
31
+ await copyTemplate(join(templateRoot, options.framework), options.destination);
32
+ if (hasUi(options.testingType))
33
+ await copyTemplate(join(templateRoot, 'ui'), options.destination);
34
+ if (hasApi(options.testingType))
35
+ await copyTemplate(join(templateRoot, 'api'), options.destination);
36
+ if (options.allure)
37
+ await copyTemplate(join(templateRoot, 'allure'), options.destination);
38
+ if (options.githubActions)
39
+ await copyTemplate(join(templateRoot, 'github'), options.destination);
40
+ if (!hasUi(options.testingType)) {
41
+ await rm(join(options.destination, options.framework === 'bdd' ? 'features/ui' : 'tests/ui'), { recursive: true, force: true });
42
+ await rm(join(options.destination, 'steps/ui'), { recursive: true, force: true });
43
+ }
44
+ if (!hasApi(options.testingType)) {
45
+ await rm(join(options.destination, options.framework === 'bdd' ? 'features/api' : 'tests/api'), { recursive: true, force: true });
46
+ await rm(join(options.destination, 'steps/api'), { recursive: true, force: true });
47
+ }
48
+ for (const env of environments) {
49
+ const environmentData = join(options.destination, 'test-data', env);
50
+ await copyTemplate(join(templateRoot, 'common', 'test-data', '__ENV__'), environmentData);
51
+ await replaceTokens(environmentData, { ENV_NAME: env });
52
+ }
53
+ await rm(join(options.destination, 'test-data', '__ENV__'), { recursive: true, force: true });
54
+ const envUnion = environments.map(value => `'${value}'`).join(' | ');
55
+ await replaceTokens(options.destination, {
56
+ PROJECT_NAME: options.projectName,
57
+ FRAMEWORK: options.framework,
58
+ TESTING_TYPE: options.testingType,
59
+ ENVIRONMENTS_JSON: JSON.stringify(environments),
60
+ ENVIRONMENT_TYPE: envUnion,
61
+ DEFAULT_ENV: 'smoke',
62
+ SCOPE_SCRIPTS: `${hasUi(options.testingType) ? ',\n "test:ui": "cross-env TEST_SCOPE=ui npm test"' : ''}${hasApi(options.testingType) ? ',\n "test:api": "cross-env TEST_SCOPE=api npm test"' : ''}`,
63
+ ALLURE_REPORTER: options.allure ? ", ['allure-playwright', { outputFolder: 'allure-results' }]" : '',
64
+ ALLURE_DEPS: options.allure ? ',\n "allure-commandline": "^2.43.0",\n "allure-playwright": "^3.12.1"' : '',
65
+ ALLURE_SCRIPTS: options.allure ? ',\n "allure": "allure generate allure-results --clean -o allure-report",\n "allure:open": "allure open allure-report",\n "allure:report": "allure generate allure-results --clean --single-file -o reports && node scripts/rename-allure-report.mjs"' : '',
66
+ BDD_DEPS: options.framework === 'bdd' ? ',\n "playwright-bdd": "^9.2.1"' : '',
67
+ BDD_SCRIPT: options.framework === 'bdd' ? ',\n "bddgen": "bddgen"' : '',
68
+ PREPARE_ALLURE: options.allure ? "spawnSync(process.execPath, ['scripts/prepare-allure.mjs'], { stdio: 'inherit', env });\n" : '',
69
+ BDD_CONFIG_IMPORT: options.framework === 'bdd' ? "import { testDir } from './bdd.config.js';" : '',
70
+ TEST_DIR: options.framework === 'bdd' ? 'testDir' : "'./tests'",
71
+ BDD_PRE: options.framework === 'bdd' ? "const pre = spawnSync(runner, ['bddgen'], { stdio: 'inherit', env });\nif (pre.status !== 0) process.exit(pre.status ?? 1);\n" : '',
72
+ ENV_IMPORT: hasApi(options.testingType) ? "import { getEnvironmentConfig } from '../utils/EnvConfig.js';" : '',
73
+ BASE_TEST_IMPORT: options.framework === 'bdd' ? "import { test as base } from 'playwright-bdd';" : "import { test as base } from '@playwright/test';",
74
+ FIXTURE_IMPORTS: `${hasUi(options.testingType) ? "import { LoginPage } from '../pages/LoginPage.js';\n" : ''}${hasApi(options.testingType) ? "import { ApiClient } from '../api/clients/ApiClient.js';\nimport { UserService } from '../api/services/UserService.js';\n" : ''}`,
75
+ FIXTURE_TYPES: `${hasUi(options.testingType) ? ' app: { loginPage: LoginPage };\n' : ''}${hasApi(options.testingType) ? ' api: { userService: UserService };\n' : ''}`,
76
+ FIXTURE_VALUES: `${hasUi(options.testingType) ? " app: async ({ page }, use) => { await use({ loginPage: new LoginPage(page) }); },\n" : ''}${hasApi(options.testingType) ? " api: async ({ playwright }, use) => {\n const context = await playwright.request.newContext({ baseURL: getEnvironmentConfig().apiBaseUrl });\n await use({ userService: new UserService(new ApiClient(context)) });\n await context.dispose();\n },\n" : ''}`,
77
+ });
78
+ }
79
+ catch (error) {
80
+ await rm(options.destination, { recursive: true, force: true });
81
+ throw new Error(`Could not create framework: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
82
+ }
83
+ }
84
+ //# sourceMappingURL=project.generator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project.generator.js","sourceRoot":"","sources":["../../src/generators/project.generator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ const properties = {
3
+ Environment: process.env.TEST_ENV ?? '{{DEFAULT_ENV}}', Browser: process.env.BROWSER ?? 'chromium',
4
+ Framework: 'Playwright-TypeScript ({{FRAMEWORK}})', 'Test-Type': '{{TESTING_TYPE}}',
5
+ 'Node-Version': process.version, Execution: process.env.GITHUB_ACTIONS ? 'GitHub Actions' : 'Local',
6
+ 'Execution-Date': new Date().toISOString(), ...(process.env.GITHUB_RUN_ID ? { 'CI-Run-ID': process.env.GITHUB_RUN_ID } : {}),
7
+ };
8
+ await mkdir('allure-results', { recursive: true });
9
+ await writeFile('allure-results/environment.properties', Object.entries(properties).map(([key, value]) => `${key}=${value}`).join('\n'));
@@ -0,0 +1,2 @@
1
+ import { rename } from 'node:fs/promises';
2
+ await rename('reports/index.html', 'reports/allure-report.html');
@@ -0,0 +1,15 @@
1
+ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
2
+ export interface RequestDefinition { method: HttpMethod; headers: Record<string, string>; query: Record<string, string | number | boolean>; body?: unknown; form?: Record<string, string>; }
3
+
4
+ export class RequestBuilder {
5
+ private request: RequestDefinition = { method: 'GET', headers: {}, query: {} };
6
+ public withMethod(method: HttpMethod): this { this.request.method = method; return this; }
7
+ public withHeader(name: string, value: string): this { this.request.headers[name] = value; return this; }
8
+ public withHeaders(headers: Record<string, string>): this { Object.assign(this.request.headers, headers); return this; }
9
+ public withQueryParam(name: string, value: string | number | boolean): this { this.request.query[name] = value; return this; }
10
+ public withQueryParams(query: Record<string, string | number | boolean>): this { Object.assign(this.request.query, query); return this; }
11
+ public withBody(body: unknown): this { this.request.body = body; delete this.request.form; return this; }
12
+ public withFormBody(form: Record<string, string>): this { this.request.form = form; delete this.request.body; return this; }
13
+ public withBearerToken(token: string): this { return this.withHeader('Authorization', `Bearer ${token}`); }
14
+ public build(): Readonly<RequestDefinition> { return structuredClone(this.request); }
15
+ }
@@ -0,0 +1,26 @@
1
+ import type { APIRequestContext } from '@playwright/test';
2
+ import type { RequestDefinition } from '../builders/RequestBuilder.js';
3
+
4
+ export interface ActualResponse<T> { status: number; headers: Record<string, string>; body: T; responseTimeMs: number; }
5
+
6
+ export class ApiClient {
7
+ public constructor(private readonly context: APIRequestContext) {}
8
+ public async execute<T>(path: string, request: Readonly<RequestDefinition>): Promise<ActualResponse<T>> {
9
+ const started = performance.now();
10
+ const response = await this.context.fetch(path, {
11
+ method: request.method, headers: request.headers, params: request.query,
12
+ ...(request.body !== undefined ? { data: request.body } : {}),
13
+ ...(request.form !== undefined ? { form: request.form } : {}),
14
+ });
15
+ const text = await response.text();
16
+ const contentType = response.headers()['content-type'] ?? '';
17
+ let parsed: unknown;
18
+ if (!text) parsed = undefined;
19
+ else if (contentType.includes('json')) {
20
+ try { parsed = JSON.parse(text) as unknown; }
21
+ catch (error) { throw new Error(`API response declared JSON but could not be parsed: ${error instanceof Error ? error.message : String(error)}`, { cause: error }); }
22
+ } else parsed = text;
23
+ const body = parsed as T;
24
+ return { status: response.status(), headers: response.headers(), body, responseTimeMs: performance.now() - started };
25
+ }
26
+ }
@@ -0,0 +1 @@
1
+ export interface UserRequest { name: string; email: string; }
@@ -0,0 +1 @@
1
+ export interface UserResponse { id: number; name: string; email: string; }
@@ -0,0 +1,11 @@
1
+ import type { ActualResponse, ApiClient } from '../clients/ApiClient.js';
2
+ import { RequestBuilder } from '../builders/RequestBuilder.js';
3
+ import type { UserRequest } from '../models/requests/UserRequest.js';
4
+ import type { UserResponse } from '../models/responses/UserResponse.js';
5
+
6
+ export class UserService {
7
+ public constructor(private readonly client: ApiClient) {}
8
+ public getUsers(): Promise<ActualResponse<UserResponse[]>> { return this.client.execute('/users', new RequestBuilder().withMethod('GET').build()); }
9
+ public getUser(id: number): Promise<ActualResponse<UserResponse>> { return this.client.execute(`/users/${id}`, new RequestBuilder().withMethod('GET').build()); }
10
+ public createUser(user: UserRequest): Promise<ActualResponse<UserResponse>> { return this.client.execute('/users', new RequestBuilder().withMethod('POST').withBody(user).build()); }
11
+ }
@@ -0,0 +1,13 @@
1
+ import { expect } from '@playwright/test';
2
+ import type { ActualResponse } from '../clients/ApiClient.js';
3
+
4
+ export class ApiResponseValidator {
5
+ public static status(response: ActualResponse<unknown>, expected: number): void { expect(response.status).toBe(expected); }
6
+ public static notEmpty(value: unknown): void { expect(value).not.toBeNull(); expect(value).not.toEqual(''); }
7
+ public static propertyExists(value: unknown, property: string): void { expect(typeof value).toBe('object'); expect(value).toHaveProperty(property); }
8
+ public static propertyEquals(value: unknown, property: string, expected: unknown): void { expect(value).toHaveProperty(property, expected); }
9
+ public static array(value: unknown): asserts value is unknown[] { expect(Array.isArray(value)).toBe(true); }
10
+ public static responseTime(response: ActualResponse<unknown>, maximumMs: number): void { expect(response.responseTimeMs).toBeLessThanOrEqual(maximumMs); }
11
+ public static errorMessage(response: ActualResponse<unknown>, message: string): void { expect(response.body).toHaveProperty('message', message); }
12
+ public static unauthorized(response: ActualResponse<unknown>): void { expect(response.status).toBe(401); }
13
+ }
@@ -0,0 +1,6 @@
1
+ import { defineBddConfig } from 'playwright-bdd';
2
+ export const testDir = defineBddConfig({
3
+ features: 'features/**/*.feature',
4
+ steps: ['steps/**/*.ts', 'fixtures/testFixture.ts'],
5
+ outputDir: '.features-gen',
6
+ });
@@ -0,0 +1,4 @@
1
+ Feature: Users API
2
+ Scenario: Retrieve users
3
+ When I request the users collection
4
+ Then the users response is successful
@@ -0,0 +1,5 @@
1
+ Feature: Login
2
+ Scenario: Successful login
3
+ Given I navigate to the application
4
+ When I login with valid credentials
5
+ Then I should see the inventory page
@@ -0,0 +1,17 @@
1
+ import { createBdd } from 'playwright-bdd';
2
+ import type { ActualResponse } from '../../api/clients/ApiClient.js';
3
+ import type { UserResponse } from '../../api/models/responses/UserResponse.js';
4
+ import { ApiResponseValidator } from '../../api/validators/ApiResponseValidator.js';
5
+ import { test } from '../../fixtures/testFixture.js';
6
+ import { getTestData } from '../../utils/TestData.js';
7
+ import { getEnvironment } from '../../utils/EnvConfig.js';
8
+ const { When, Then } = createBdd(test);
9
+ let response: ActualResponse<UserResponse[]>;
10
+ When('I request the users collection', async ({ api }) => { response = await api.userService.getUsers(); });
11
+ Then('the users response is successful', () => {
12
+ const data = getTestData<{ environment: string; apiExpectedStatus: number }>();
13
+ if (data.environment !== getEnvironment()) throw new Error(`Loaded test data for ${data.environment}, expected ${getEnvironment()}.`);
14
+ ApiResponseValidator.status(response, data.apiExpectedStatus);
15
+ ApiResponseValidator.array(response.body);
16
+ ApiResponseValidator.notEmpty(response.body);
17
+ });
@@ -0,0 +1,13 @@
1
+ import { createBdd } from 'playwright-bdd';
2
+ import { test, expect } from '../../fixtures/testFixture.js';
3
+ import { getTestData } from '../../utils/TestData.js';
4
+ import { getEnvironment } from '../../utils/EnvConfig.js';
5
+ const { Given, When, Then } = createBdd(test);
6
+ interface LoginData { environment: string; validUser: { username: string; password: string }; }
7
+ Given('I navigate to the application', async ({ app }) => { await app.loginPage.open(); });
8
+ When('I login with valid credentials', async ({ app }) => {
9
+ const data = getTestData<LoginData>();
10
+ expect(data.environment).toBe(getEnvironment());
11
+ await app.loginPage.login(data.validUser.username, data.validUser.password);
12
+ });
13
+ Then('I should see the inventory page', async ({ app }) => { await expect(app.loginPage.inventoryHeading).toBeVisible(); });
@@ -0,0 +1,6 @@
1
+ TEST_ENV={{DEFAULT_ENV}}
2
+ APP_USERNAME=
3
+ APP_PASSWORD=
4
+ CLIENT_ID=
5
+ CLIENT_SECRET=
6
+ API_TOKEN=
@@ -0,0 +1 @@
1
+ 22
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 {{PROJECT_NAME}} contributors
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.
@@ -0,0 +1,120 @@
1
+ # Playwright TypeScript Framework Scaffold
2
+
3
+ A generated **{{FRAMEWORK}}** framework for **{{TESTING_TYPE}}** automation. It uses strict TypeScript, Playwright-native UI/API tooling, environment-aware test data, reusable fixtures, and optional Allure/CI integration.
4
+
5
+ ## Features
6
+
7
+ - Page Objects and API service/client layers
8
+ - Typed fixtures, request builders, responses, and validators
9
+ - Central `config/run.config.json` for environment, timeouts, headless mode, retries, workers, and parallel execution
10
+ - Automatically selected smoke/UAT JSON test data
11
+ - Cross-platform scripts, ESLint, Prettier, and strict TypeScript
12
+ - Secrets loaded from environment variables—never test-data JSON
13
+
14
+ ## Requirements
15
+
16
+ Node.js 22 or newer. Install Playwright's browser once with `npx playwright install`. Allure Commandline 2 report generation also requires a Java runtime (Java 8 or newer; a current LTS such as Java 17 or 21 is recommended).
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install
22
+ npx playwright install
23
+ ```
24
+
25
+ ## Create a Framework
26
+
27
+ This directory was produced by `create-playwright-ts-framework`. Re-run the generator in a different parent directory to create another framework.
28
+
29
+ ## BDD Framework
30
+
31
+ BDD projects keep Gherkin in `features/`, bindings in `steps/`, and use `playwright-bdd`. Run `npm run bddgen` explicitly when inspecting generated specs; `npm test` runs it automatically.
32
+
33
+ ## Non-BDD Framework
34
+
35
+ Non-BDD projects use standard Playwright specs under `tests/ui` and/or `tests/api`.
36
+
37
+ ## UI Automation
38
+
39
+ Page Objects live in `pages/`. `BasePage` owns common browser operations and concrete pages contain locators and business actions. Assertions remain in tests or steps.
40
+
41
+ ## API Automation
42
+
43
+ Tests call `UserService`; services build requests, `ApiClient` executes them, and `ApiResponseValidator` verifies typed `ActualResponse<T>` values. `RequestBuilder` supports all common HTTP verbs, headers, bearer tokens, query parameters, JSON, and form bodies.
44
+
45
+ ## Project Structure
46
+
47
+ ```text
48
+ config/ environment and framework configuration
49
+ fixtures/ typed Playwright fixtures
50
+ test-data/ environment-specific non-secret data
51
+ utils/ data, JSON, date, logging, and constants
52
+ pages/ UI Page Objects (when selected)
53
+ api/ API client/service architecture (when selected)
54
+ features/steps BDD scenarios and bindings (BDD)
55
+ tests/ Playwright specs (Non-BDD)
56
+ ```
57
+
58
+ ## Environment Configuration
59
+
60
+ Edit `config/run.config.json` to choose the default environment and execution settings. Override it with `npm run test -- --env=smoke`, `TEST_ENV=uat npm test`, or the cross-platform named scripts. Unknown environments produce a list of valid values. URLs are centralized in `config/environments.ts`.
61
+
62
+ ## Test Data Management
63
+
64
+ `getTestData<T>()` automatically loads `test-data/<active-environment>/testdata.json`; callers never pass an environment. Smoke execution reads `test-data/smoke/testdata.json`, while UAT reads `test-data/uat/testdata.json`. For named collections, use `getKeyedTestData<T>('Appointment')`, which returns `Record<string, T>`.
65
+
66
+ ## Running Tests
67
+
68
+ ```bash
69
+ npm test
70
+ npm run test:smoke
71
+ npm run test:uat
72
+ npm run test:ui
73
+ npm run test:api
74
+ npm run typecheck
75
+ npm run lint
76
+ ```
77
+
78
+ ## Allure Reporting
79
+
80
+ When enabled, tests write metadata and results to `allure-results/`. `npm run allure` creates `allure-report/`; `npm run allure:open` serves it. `npm run allure:report` uses Allure 2's supported `--single-file` mode and writes the portable `reports/allure-report.html`.
81
+
82
+ ## GitHub Actions
83
+
84
+ The optional workflow supports manual environment selection, runs tests, generates reports with `if: always()`, and uploads available artifacts. The commented cron example uses UTC.
85
+
86
+ ## Secrets
87
+
88
+ Copy `.env.example` to `.env` locally. Keep usernames, passwords, tokens, and OAuth credentials out of JSON and source control. Configure the matching GitHub repository secrets for CI. Authorization headers are not logged.
89
+
90
+ ## Adding Page Objects
91
+
92
+ Extend `BasePage`, use role/label/test-id locators, expose business actions, and add the page to `fixtures/testFixture.ts`.
93
+
94
+ ## Adding API Services
95
+
96
+ Create typed models, build requests with `RequestBuilder`, execute via `ApiClient`, and expose the service through the API fixture.
97
+
98
+ ## Adding Test Data
99
+
100
+ Add the same schema to every selected environment folder, then load it through a typed interface. Put secret values in environment variables instead.
101
+
102
+ ## Architecture
103
+
104
+ BDD: `Feature → Step Definition → Fixture → Page Object/API Service → Playwright → Validator → Allure`
105
+ Non-BDD: `Test Spec → Fixture → Page Object/API Service → Playwright → Validator → Allure`
106
+
107
+ ## Troubleshooting
108
+
109
+ - Browser missing: run `npx playwright install`.
110
+ - Unknown environment: choose one listed in the error.
111
+ - No Allure results: run tests first. If report generation says no Java runtime is available, install a current Java LTS and retry.
112
+ - CI failure: download the Playwright/Allure artifacts and inspect traces and HTML output.
113
+
114
+ ## Contributing
115
+
116
+ Run `npm run typecheck`, `npm run lint`, and `npm test` before opening a pull request. Keep additions typed and cross-platform.
117
+
118
+ ## License
119
+
120
+ MIT
@@ -0,0 +1,13 @@
1
+ export type Environment = {{ENVIRONMENT_TYPE}};
2
+
3
+ export interface EnvironmentConfig {
4
+ baseUrl: string;
5
+ apiBaseUrl: string;
6
+ }
7
+
8
+ export const environments: Record<Environment, EnvironmentConfig> = Object.fromEntries(
9
+ ({{ENVIRONMENTS_JSON}} as Environment[]).map(environment => [environment, {
10
+ baseUrl: 'https://www.saucedemo.com',
11
+ apiBaseUrl: 'https://jsonplaceholder.typicode.com',
12
+ }]),
13
+ ) as Record<Environment, EnvironmentConfig>;
@@ -0,0 +1,5 @@
1
+ export const frameworkConfig = {
2
+ framework: '{{FRAMEWORK}}',
3
+ testingType: '{{TESTING_TYPE}}',
4
+ timeout: 30_000,
5
+ } as const;
@@ -0,0 +1,11 @@
1
+ {
2
+ "environment": "smoke",
3
+ "timeout": 30000,
4
+ "actionTimeout": 10000,
5
+ "navigationTimeout": 30000,
6
+ "headless": true,
7
+ "retries": 0,
8
+ "ciRetries": 2,
9
+ "fullyParallel": true,
10
+ "workers": null
11
+ }
@@ -0,0 +1,8 @@
1
+ import eslint from '@eslint/js';
2
+ import tseslint from 'typescript-eslint';
3
+ export default tseslint.config(
4
+ { ignores: ['allure-*', 'reports', 'playwright-report', 'test-results', 'scripts/*.mjs', 'eslint.config.js'] },
5
+ eslint.configs.recommended,
6
+ ...tseslint.configs.recommendedTypeChecked,
7
+ { languageOptions: { parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname } } },
8
+ );
@@ -0,0 +1,9 @@
1
+ {{BASE_TEST_IMPORT}}
2
+ {{ENV_IMPORT}}
3
+ {{FIXTURE_IMPORTS}}
4
+ interface FrameworkFixtures {
5
+ {{FIXTURE_TYPES}}}
6
+
7
+ export const test = base.extend<FrameworkFixtures>({
8
+ {{FIXTURE_VALUES}}});
9
+ export { expect } from '@playwright/test';
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "engines": { "node": ">=22" },
7
+ "scripts": {
8
+ "test": "node scripts/run-tests.mjs",
9
+ "test:smoke": "cross-env TEST_ENV=smoke npm test",
10
+ "test:uat": "cross-env TEST_ENV=uat npm test"{{SCOPE_SCRIPTS}},
11
+ "typecheck": "tsc --noEmit",
12
+ "lint": "eslint .",
13
+ "format": "prettier --write .",
14
+ "clean": "node scripts/clean.mjs"{{BDD_SCRIPT}}{{ALLURE_SCRIPTS}}
15
+ },
16
+ "devDependencies": {
17
+ "@eslint/js": "^10.0.0",
18
+ "@playwright/test": "^1.63.0",
19
+ "@types/node": "^22.18.3",
20
+ "cross-env": "^7.0.3",
21
+ "dotenv": "^17.2.2",
22
+ "eslint": "^10.0.0",
23
+ "prettier": "^3.6.2",
24
+ "typescript": "^5.9.2",
25
+ "typescript-eslint": "^8.70.0"{{BDD_DEPS}}{{ALLURE_DEPS}}
26
+ }
27
+ }
@@ -0,0 +1,26 @@
1
+ import { defineConfig } from '@playwright/test';
2
+ import { getEnvironmentConfig, getRunConfig } from './utils/EnvConfig.js';
3
+ {{BDD_CONFIG_IMPORT}}
4
+
5
+ const environment = getEnvironmentConfig();
6
+ const runConfig = getRunConfig();
7
+ const scope = process.env.TEST_SCOPE;
8
+
9
+ export default defineConfig({
10
+ testDir: {{TEST_DIR}},
11
+ testMatch: scope ? `**/${scope}/**/*.spec.*` : '**/*.spec.*',
12
+ timeout: runConfig.timeout,
13
+ fullyParallel: runConfig.fullyParallel,
14
+ retries: process.env.CI ? runConfig.ciRetries : runConfig.retries,
15
+ ...(process.env.CI ? { workers: 1 } : runConfig.workers === null ? {} : { workers: runConfig.workers }),
16
+ reporter: [['html', { open: 'never' }], ['list']{{ALLURE_REPORTER}}],
17
+ use: {
18
+ baseURL: environment.baseUrl,
19
+ headless: process.env.HEADLESS === undefined ? runConfig.headless : process.env.HEADLESS !== 'false',
20
+ actionTimeout: runConfig.actionTimeout,
21
+ navigationTimeout: runConfig.navigationTimeout,
22
+ screenshot: 'only-on-failure',
23
+ video: 'retain-on-failure',
24
+ trace: 'on-first-retry',
25
+ },
26
+ });
@@ -0,0 +1,2 @@
1
+ import { rm } from 'node:fs/promises';
2
+ for (const path of ['playwright-report', 'test-results', 'allure-results', 'allure-report', 'reports', '.features-gen']) await rm(path, { recursive: true, force: true });
@@ -0,0 +1,10 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ const args = process.argv.slice(2);
3
+ const envArg = args.find(value => value.startsWith('--env='));
4
+ const filtered = args.filter(value => value !== envArg);
5
+ const env = { ...process.env, ...(envArg ? { TEST_ENV: envArg.slice(6) } : {}) };
6
+ {{PREPARE_ALLURE}}
7
+ const runner = process.platform === 'win32' ? 'npx.cmd' : 'npx';
8
+ {{BDD_PRE}}
9
+ const result = spawnSync(runner, ['playwright', 'test', ...filtered], { stdio: 'inherit', env });
10
+ process.exit(result.status ?? 1);
@@ -0,0 +1,9 @@
1
+ {
2
+ "environment": "{{ENV_NAME}}",
3
+ "apiExpectedStatus": 200,
4
+ "baseUrl": "https://www.saucedemo.com",
5
+ "validUser": { "username": "standard_user", "password": "secret_sauce" },
6
+ "Appointment": {
7
+ "GP": { "product-category": "GP", "product-identifier": "001", "procedure-identifier": "CONSULTATION" }
8
+ }
9
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "noUncheckedIndexedAccess": true,
8
+ "exactOptionalPropertyTypes": true,
9
+ "resolveJsonModule": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "types": ["node"]
13
+ },
14
+ "include": ["**/*.ts"]
15
+ }
@@ -0,0 +1,2 @@
1
+ export const DEFAULT_TIMEOUT = 30_000;
2
+ export const CONTENT_TYPE_JSON = 'application/json';
@@ -0,0 +1,2 @@
1
+ export function formatDate(date: Date, locale = 'en-GB'): string { return new Intl.DateTimeFormat(locale).format(date); }
2
+ export function isoDate(date = new Date()): string { return date.toISOString().slice(0, 10); }
@@ -0,0 +1,49 @@
1
+ import 'dotenv/config';
2
+ import { environments, type Environment, type EnvironmentConfig } from '../config/environments.js';
3
+ import runConfigJson from '../config/run.config.json' with { type: 'json' };
4
+
5
+ export interface RunConfig {
6
+ environment: Environment;
7
+ timeout: number;
8
+ actionTimeout: number;
9
+ navigationTimeout: number;
10
+ headless: boolean;
11
+ retries: number;
12
+ ciRetries: number;
13
+ fullyParallel: boolean;
14
+ workers: number | null;
15
+ }
16
+
17
+ function loadRunConfig(value: unknown): RunConfig {
18
+ if (typeof value !== 'object' || value === null) throw new Error('config/run.config.json must contain a JSON object.');
19
+ const config = value as Record<string, unknown>;
20
+ const numericKeys = ['timeout', 'actionTimeout', 'navigationTimeout', 'retries', 'ciRetries'] as const;
21
+ for (const key of numericKeys) {
22
+ if (typeof config[key] !== 'number' || config[key] < 0) throw new Error(`run.config.json: "${key}" must be a non-negative number.`);
23
+ }
24
+ if (typeof config.headless !== 'boolean' || typeof config.fullyParallel !== 'boolean') throw new Error('run.config.json: "headless" and "fullyParallel" must be boolean values.');
25
+ if (config.workers !== null && (typeof config.workers !== 'number' || config.workers < 1)) throw new Error('run.config.json: "workers" must be null or a positive number.');
26
+ if (typeof config.environment !== 'string' || !(config.environment in environments)) throw new Error(`run.config.json: unknown environment "${String(config.environment)}".`);
27
+ return config as unknown as RunConfig;
28
+ }
29
+
30
+ const runConfig = loadRunConfig(runConfigJson);
31
+
32
+ const available = Object.keys(environments) as Environment[];
33
+
34
+ export function getEnvironment(): Environment {
35
+ const argument = process.argv.find(value => value.startsWith('--env='))?.slice('--env='.length);
36
+ const requested = argument ?? process.env.TEST_ENV ?? runConfig.environment;
37
+ if (!available.includes(requested as Environment)) {
38
+ throw new Error(`Unknown TEST_ENV "${requested}".\n\nAvailable environments:\n${available.map(value => `- ${value}`).join('\n')}`);
39
+ }
40
+ return requested as Environment;
41
+ }
42
+
43
+ export function getRunConfig(): Readonly<RunConfig> {
44
+ return runConfig;
45
+ }
46
+
47
+ export function getEnvironmentConfig(): EnvironmentConfig {
48
+ return environments[getEnvironment()];
49
+ }
@@ -0,0 +1,7 @@
1
+ export function getValue<T>(value: unknown, path: string): T {
2
+ const result = path.split('.').reduce<unknown>((current, key) => {
3
+ if (typeof current !== 'object' || current === null || !(key in current)) throw new Error(`JSON path not found: ${path}`);
4
+ return (current as Record<string, unknown>)[key];
5
+ }, value);
6
+ return result as T;
7
+ }
@@ -0,0 +1,10 @@
1
+ type Level = 'INFO' | 'WARN' | 'ERROR' | 'API' | 'UI';
2
+ const write = (level: Level, message: string): void => console.log(`[${level}] ${message}`);
3
+ export const logger = {
4
+ info: (message: string): void => write('INFO', message),
5
+ warn: (message: string): void => write('WARN', message),
6
+ error: (message: string): void => write('ERROR', message),
7
+ api: (message: string): void => write('API', message),
8
+ ui: (message: string): void => write('UI', message),
9
+ };
10
+ export function maskSecret(value: string): string { return value.length < 5 ? '****' : `${value.slice(0, 2)}***${value.slice(-2)}`; }
@@ -0,0 +1,16 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { getEnvironment } from './EnvConfig.js';
4
+
5
+ export function getTestData<T>(): T {
6
+ const path = resolve(process.cwd(), 'test-data', getEnvironment(), 'testdata.json');
7
+ try { return JSON.parse(readFileSync(path, 'utf8')) as T; }
8
+ catch (error) { throw new Error(`Unable to load test data from ${path}: ${error instanceof Error ? error.message : String(error)}`, { cause: error }); }
9
+ }
10
+
11
+ export function getKeyedTestData<T>(group: string): Record<string, T> {
12
+ const data = getTestData<Record<string, unknown>>();
13
+ const value = data[group];
14
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`Test-data group "${group}" was not found.`);
15
+ return value as Record<string, T>;
16
+ }
@@ -0,0 +1,45 @@
1
+ name: Playwright tests
2
+ on:
3
+ push:
4
+ pull_request:
5
+ workflow_dispatch:
6
+ inputs:
7
+ environment:
8
+ description: Environment
9
+ required: true
10
+ default: smoke
11
+ type: choice
12
+ options: [smoke, uat]
13
+ # schedule:
14
+ # - cron: '0 6 * * 1' # UTC; customise for your project.
15
+ jobs:
16
+ test:
17
+ runs-on: ubuntu-latest
18
+ env:
19
+ TEST_ENV: ${{ inputs.environment || 'smoke' }}
20
+ APP_USERNAME: ${{ secrets.APP_USERNAME }}
21
+ APP_PASSWORD: ${{ secrets.APP_PASSWORD }}
22
+ CLIENT_ID: ${{ secrets.CLIENT_ID }}
23
+ CLIENT_SECRET: ${{ secrets.CLIENT_SECRET }}
24
+ API_TOKEN: ${{ secrets.API_TOKEN }}
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+ - uses: actions/setup-node@v4
28
+ with: { node-version: 22, cache: npm }
29
+ - run: npm ci
30
+ - run: npx playwright install --with-deps
31
+ - run: npm run test
32
+ - name: Generate Allure reports
33
+ if: always()
34
+ run: npm run allure:report --if-present
35
+ - name: Upload test artifacts
36
+ if: always()
37
+ uses: actions/upload-artifact@v4
38
+ with:
39
+ name: playwright-reports-${{ env.TEST_ENV }}
40
+ if-no-files-found: ignore
41
+ path: |
42
+ playwright-report/
43
+ allure-results/
44
+ allure-report/
45
+ reports/allure-report.html
@@ -0,0 +1,13 @@
1
+ import { test, expect } from '../../fixtures/testFixture.js';
2
+ import { ApiResponseValidator } from '../../api/validators/ApiResponseValidator.js';
3
+ import { getTestData } from '../../utils/TestData.js';
4
+ import { getEnvironment } from '../../utils/EnvConfig.js';
5
+ interface ApiTestData { environment: string; apiExpectedStatus: number; }
6
+ test('users can be retrieved through the service layer', async ({ api }) => {
7
+ const data = getTestData<ApiTestData>();
8
+ expect(data.environment).toBe(getEnvironment());
9
+ const response = await api.userService.getUsers();
10
+ ApiResponseValidator.status(response, data.apiExpectedStatus);
11
+ ApiResponseValidator.array(response.body);
12
+ ApiResponseValidator.notEmpty(response.body);
13
+ });
@@ -0,0 +1,11 @@
1
+ import { test, expect } from '../../fixtures/testFixture.js';
2
+ import { getTestData } from '../../utils/TestData.js';
3
+ import { getEnvironment } from '../../utils/EnvConfig.js';
4
+ interface LoginData { environment: string; validUser: { username: string; password: string }; }
5
+ test('user can log in', async ({ app }) => {
6
+ const data = getTestData<LoginData>();
7
+ expect(data.environment).toBe(getEnvironment());
8
+ await app.loginPage.open();
9
+ await app.loginPage.login(data.validUser.username, data.validUser.password);
10
+ await expect(app.loginPage.inventoryHeading).toBeVisible();
11
+ });
@@ -0,0 +1,11 @@
1
+ import type { Locator, Page } from '@playwright/test';
2
+
3
+ export class BasePage {
4
+ public constructor(protected readonly page: Page) {}
5
+ public async navigate(path = '/'): Promise<void> { await this.page.goto(path); }
6
+ public async click(locator: Locator): Promise<void> { await locator.click(); }
7
+ public async fill(locator: Locator, value: string): Promise<void> { await locator.fill(value); }
8
+ public async getText(locator: Locator): Promise<string> { return (await locator.textContent()) ?? ''; }
9
+ public async waitForElement(locator: Locator): Promise<void> { await locator.waitFor({ state: 'visible' }); }
10
+ public async isVisible(locator: Locator): Promise<boolean> { return locator.isVisible(); }
11
+ }
@@ -0,0 +1,23 @@
1
+ import type { Locator, Page } from '@playwright/test';
2
+ import { BasePage } from './BasePage.js';
3
+
4
+ export class LoginPage extends BasePage {
5
+ private readonly username: Locator;
6
+ private readonly password: Locator;
7
+ private readonly loginButton: Locator;
8
+ public readonly inventoryHeading: Locator;
9
+
10
+ public constructor(page: Page) {
11
+ super(page);
12
+ this.username = page.getByPlaceholder('Username');
13
+ this.password = page.getByPlaceholder('Password');
14
+ this.loginButton = page.getByRole('button', { name: 'Login' });
15
+ this.inventoryHeading = page.getByText('Products', { exact: true });
16
+ }
17
+ public async open(): Promise<void> { await this.navigate('/'); }
18
+ public async login(username: string, password: string): Promise<void> {
19
+ await this.fill(this.username, username);
20
+ await this.fill(this.password, password);
21
+ await this.click(this.loginButton);
22
+ }
23
+ }
@@ -0,0 +1,11 @@
1
+ export type FrameworkStyle = 'bdd' | 'non-bdd';
2
+ export type TestingType = 'ui' | 'api' | 'ui-api';
3
+ export interface GeneratorOptions {
4
+ projectName: string;
5
+ destination: string;
6
+ framework: FrameworkStyle;
7
+ testingType: TestingType;
8
+ allure: boolean;
9
+ githubActions: boolean;
10
+ installDependencies: boolean;
11
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export declare function copyTemplate(source: string, destination: string): Promise<void>;
2
+ export declare function replaceTokens(root: string, tokens: Record<string, string>): Promise<void>;
@@ -0,0 +1,25 @@
1
+ import { cp, mkdir, readdir, readFile, rename, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ export async function copyTemplate(source, destination) {
4
+ await mkdir(destination, { recursive: true });
5
+ await cp(source, destination, { recursive: true, force: true });
6
+ }
7
+ export async function replaceTokens(root, tokens) {
8
+ for (const entry of await readdir(root, { withFileTypes: true })) {
9
+ let target = join(root, entry.name);
10
+ if (entry.name.endsWith('.template')) {
11
+ const renamed = target.slice(0, -'.template'.length);
12
+ await rename(target, renamed);
13
+ target = renamed;
14
+ }
15
+ if (entry.isDirectory())
16
+ await replaceTokens(target, tokens);
17
+ else {
18
+ let content = await readFile(target, 'utf8');
19
+ for (const [token, value] of Object.entries(tokens))
20
+ content = content.replaceAll(`{{${token}}}`, value);
21
+ await writeFile(target, content);
22
+ }
23
+ }
24
+ }
25
+ //# sourceMappingURL=files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"files.js","sourceRoot":"","sources":["../../src/utils/files.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACnF,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,MAAc,EAAE,WAAmB;IACpE,MAAM,KAAK,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9C,MAAM,EAAE,CAAC,MAAM,EAAE,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AAClE,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,IAAY,EAAE,MAA8B;IAC9E,KAAK,MAAM,KAAK,IAAI,MAAM,OAAO,CAAC,IAAI,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;QACjE,IAAI,MAAM,GAAG,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QACpC,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;YACrC,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;YACrD,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;YAC9B,MAAM,GAAG,OAAO,CAAC;QACnB,CAAC;QACD,IAAI,KAAK,CAAC,WAAW,EAAE;YAAE,MAAM,aAAa,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;aACxD,CAAC;YACJ,IAAI,OAAO,GAAG,MAAM,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;YAC7C,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC;gBAAE,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,KAAK,KAAK,IAAI,EAAE,KAAK,CAAC,CAAC;YACzG,MAAM,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACnC,CAAC;IACH,CAAC;AACH,CAAC"}
@@ -0,0 +1 @@
1
+ export declare function validateProjectName(name: string): true | string;
@@ -0,0 +1,9 @@
1
+ const PACKAGE_NAME = /^[a-z0-9][a-z0-9._-]*$/;
2
+ export function validateProjectName(name) {
3
+ if (!name.trim())
4
+ return 'Project name is required.';
5
+ if (!PACKAGE_NAME.test(name))
6
+ return 'Use a lowercase npm-compatible project name without spaces or path separators.';
7
+ return true;
8
+ }
9
+ //# sourceMappingURL=project-name.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-name.js","sourceRoot":"","sources":["../../src/utils/project-name.ts"],"names":[],"mappings":"AAAA,MAAM,YAAY,GAAG,wBAAwB,CAAC;AAE9C,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;QAAE,OAAO,2BAA2B,CAAC;IACrD,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,gFAAgF,CAAC;IACtH,OAAO,IAAI,CAAC;AACd,CAAC"}
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "create-playwright-ts-framework",
3
+ "version": "0.1.0",
4
+ "description": "Interactive generator for production-ready Playwright TypeScript test frameworks",
5
+ "type": "module",
6
+ "bin": { "create-playwright-ts-framework": "dist/cli/index.js" },
7
+ "files": ["dist", "README.md", "LICENSE"],
8
+ "engines": { "node": ">=22" },
9
+ "scripts": {
10
+ "build": "tsc -p tsconfig.build.json && node scripts/copy-templates.mjs",
11
+ "typecheck": "tsc --noEmit",
12
+ "lint": "eslint .",
13
+ "format": "prettier --write .",
14
+ "format:check": "prettier --check .",
15
+ "test": "vitest run",
16
+ "prepack": "npm run build && npm test"
17
+ },
18
+ "keywords": ["playwright", "typescript", "testing", "bdd", "scaffold", "cli"],
19
+ "license": "MIT",
20
+ "dependencies": {
21
+ "@inquirer/prompts": "^7.8.4",
22
+ "chalk": "^5.6.2",
23
+ "ora": "^8.2.0"
24
+ },
25
+ "devDependencies": {
26
+ "@eslint/js": "^10.0.0",
27
+ "@types/node": "^22.18.3",
28
+ "eslint": "^10.0.0",
29
+ "prettier": "^3.6.2",
30
+ "typescript": "^5.9.2",
31
+ "typescript-eslint": "^8.70.0",
32
+ "vitest": "^5.0.0"
33
+ }
34
+ }