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.
- package/LICENSE +21 -0
- package/README.md +66 -0
- package/dist/cli/generator.d.ts +2 -0
- package/dist/cli/generator.js +43 -0
- package/dist/cli/generator.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +20 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/logger.d.ts +6 -0
- package/dist/cli/logger.js +8 -0
- package/dist/cli/logger.js.map +1 -0
- package/dist/cli/prompts.d.ts +2 -0
- package/dist/cli/prompts.js +13 -0
- package/dist/cli/prompts.js.map +1 -0
- package/dist/generators/project.generator.d.ts +2 -0
- package/dist/generators/project.generator.js +84 -0
- package/dist/generators/project.generator.js.map +1 -0
- package/dist/templates/allure/scripts/prepare-allure.mjs +9 -0
- package/dist/templates/allure/scripts/rename-allure-report.mjs +2 -0
- package/dist/templates/api/api/builders/RequestBuilder.ts +15 -0
- package/dist/templates/api/api/clients/ApiClient.ts +26 -0
- package/dist/templates/api/api/models/requests/UserRequest.ts +1 -0
- package/dist/templates/api/api/models/responses/UserResponse.ts +1 -0
- package/dist/templates/api/api/services/UserService.ts +11 -0
- package/dist/templates/api/api/validators/ApiResponseValidator.ts +13 -0
- package/dist/templates/bdd/bdd.config.ts +6 -0
- package/dist/templates/bdd/features/api/users.feature +4 -0
- package/dist/templates/bdd/features/ui/login.feature +5 -0
- package/dist/templates/bdd/steps/api/users.steps.ts +17 -0
- package/dist/templates/bdd/steps/ui/login.steps.ts +13 -0
- package/dist/templates/common/.env.example +6 -0
- package/dist/templates/common/.nvmrc +1 -0
- package/dist/templates/common/LICENSE +21 -0
- package/dist/templates/common/README.md.template +120 -0
- package/dist/templates/common/config/environments.ts +13 -0
- package/dist/templates/common/config/framework.config.ts +5 -0
- package/dist/templates/common/config/run.config.json +11 -0
- package/dist/templates/common/eslint.config.js +8 -0
- package/dist/templates/common/fixtures/testFixture.ts +9 -0
- package/dist/templates/common/package.json.template +27 -0
- package/dist/templates/common/playwright.config.ts +26 -0
- package/dist/templates/common/scripts/clean.mjs +2 -0
- package/dist/templates/common/scripts/run-tests.mjs +10 -0
- package/dist/templates/common/test-data/__ENV__/testdata.json +9 -0
- package/dist/templates/common/tsconfig.json +15 -0
- package/dist/templates/common/utils/Constants.ts +2 -0
- package/dist/templates/common/utils/DateHelper.ts +2 -0
- package/dist/templates/common/utils/EnvConfig.ts +49 -0
- package/dist/templates/common/utils/JsonHelper.ts +7 -0
- package/dist/templates/common/utils/Logger.ts +10 -0
- package/dist/templates/common/utils/TestData.ts +16 -0
- package/dist/templates/github/.github/workflows/playwright.yml +45 -0
- package/dist/templates/non-bdd/tests/api/users.spec.ts +13 -0
- package/dist/templates/non-bdd/tests/ui/login.spec.ts +11 -0
- package/dist/templates/ui/pages/BasePage.ts +11 -0
- package/dist/templates/ui/pages/LoginPage.ts +23 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/files.d.ts +2 -0
- package/dist/utils/files.js +25 -0
- package/dist/utils/files.js.map +1 -0
- package/dist/utils/project-name.d.ts +1 -0
- package/dist/utils/project-name.js +9 -0
- package/dist/utils/project-name.js.map +1 -0
- 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,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,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,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,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,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,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,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 @@
|
|
|
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,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,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,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,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
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -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 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -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
|
+
}
|