@toolstackhq/create-qa-patterns 1.0.14 → 1.0.15
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/README.md +5 -0
- package/index.js +252 -1076
- package/lib/args.js +139 -0
- package/lib/constants.js +115 -0
- package/lib/interactive.js +131 -0
- package/lib/local-env.js +65 -0
- package/lib/metadata.js +329 -0
- package/lib/output.js +326 -0
- package/lib/prereqs.js +72 -0
- package/lib/scaffold.js +120 -0
- package/lib/templates.js +40 -0
- package/package.json +5 -3
- package/templates/cypress-template/.env.example +2 -2
- package/templates/cypress-template/.github/workflows/cypress-tests.yml +2 -2
- package/templates/cypress-template/README.md +10 -6
- package/templates/cypress-template/allurerc.mjs +1 -1
- package/templates/cypress-template/config/environments.ts +13 -11
- package/templates/cypress-template/config/runtime-config.ts +17 -12
- package/templates/cypress-template/config/secret-manager.ts +1 -1
- package/templates/cypress-template/config/test-env.ts +3 -3
- package/templates/cypress-template/cypress/e2e/ui-journey.cy.ts +12 -10
- package/templates/cypress-template/cypress/support/app-config.ts +5 -5
- package/templates/cypress-template/cypress/support/commands.ts +7 -7
- package/templates/cypress-template/cypress/support/data/data-factory.ts +6 -4
- package/templates/cypress-template/cypress/support/data/id-generator.ts +1 -1
- package/templates/cypress-template/cypress/support/data/seeded-faker.ts +2 -2
- package/templates/cypress-template/cypress/support/e2e.ts +2 -2
- package/templates/cypress-template/cypress/support/pages/login-page.ts +4 -4
- package/templates/cypress-template/cypress/support/pages/people-page.ts +10 -10
- package/templates/cypress-template/cypress.config.ts +9 -9
- package/templates/cypress-template/demo-apps/ui-demo-app/public/styles.css +1 -1
- package/templates/cypress-template/demo-apps/ui-demo-app/src/server.js +44 -41
- package/templates/cypress-template/demo-apps/ui-demo-app/src/store.js +31 -3
- package/templates/cypress-template/demo-apps/ui-demo-app/src/templates.js +5 -5
- package/templates/cypress-template/eslint.config.mjs +53 -45
- package/templates/cypress-template/package.json +6 -5
- package/templates/cypress-template/scripts/ensure-local-env.mjs +36 -0
- package/templates/cypress-template/scripts/generate-allure-report.mjs +16 -10
- package/templates/cypress-template/scripts/run-cypress.mjs +33 -24
- package/templates/cypress-template/scripts/run-tests.sh +1 -0
- package/templates/cypress-template/tsconfig.json +7 -1
- package/templates/playwright-template/.env.example +6 -6
- package/templates/playwright-template/.github/workflows/playwright-tests.yml +14 -5
- package/templates/playwright-template/README.md +6 -5
- package/templates/playwright-template/allurerc.mjs +1 -1
- package/templates/playwright-template/components/flash-message.ts +2 -2
- package/templates/playwright-template/config/environments.ts +16 -14
- package/templates/playwright-template/config/runtime-config.ts +17 -12
- package/templates/playwright-template/config/secret-manager.ts +1 -1
- package/templates/playwright-template/config/test-env.ts +3 -3
- package/templates/playwright-template/data/factories/data-factory.ts +6 -4
- package/templates/playwright-template/data/generators/id-generator.ts +1 -1
- package/templates/playwright-template/data/generators/seeded-faker.ts +2 -2
- package/templates/playwright-template/demo-apps/api-demo-server/src/server.js +9 -9
- package/templates/playwright-template/demo-apps/api-demo-server/src/store.js +1 -1
- package/templates/playwright-template/demo-apps/ui-demo-app/public/styles.css +1 -1
- package/templates/playwright-template/demo-apps/ui-demo-app/src/server.js +44 -41
- package/templates/playwright-template/demo-apps/ui-demo-app/src/store.js +31 -3
- package/templates/playwright-template/demo-apps/ui-demo-app/src/templates.js +5 -5
- package/templates/playwright-template/eslint.config.mjs +40 -40
- package/templates/playwright-template/fixtures/test-fixtures.ts +27 -12
- package/templates/playwright-template/lint/architecture-plugin.cjs +36 -31
- package/templates/playwright-template/package.json +7 -6
- package/templates/playwright-template/pages/base-page.ts +4 -4
- package/templates/playwright-template/pages/login-page.ts +9 -9
- package/templates/playwright-template/pages/people-page.ts +21 -17
- package/templates/playwright-template/playwright.config.ts +22 -19
- package/templates/playwright-template/reporters/structured-reporter.ts +11 -8
- package/templates/playwright-template/scripts/ensure-local-env.mjs +37 -0
- package/templates/playwright-template/scripts/generate-allure-report.mjs +16 -10
- package/templates/playwright-template/scripts/run-tests.sh +1 -0
- package/templates/playwright-template/tests/api-people.spec.ts +8 -6
- package/templates/playwright-template/tests/ui-journey.spec.ts +13 -8
- package/templates/playwright-template/tsconfig.json +3 -11
- package/templates/playwright-template/utils/logger.ts +12 -8
- package/templates/playwright-template/utils/test-step.ts +5 -5
- package/templates/wdio-template/.env.example +14 -0
- package/templates/wdio-template/.github/workflows/wdio-tests.yml +46 -0
- package/templates/wdio-template/README.md +241 -0
- package/templates/wdio-template/allurerc.mjs +10 -0
- package/templates/wdio-template/components/README.md +5 -0
- package/templates/wdio-template/components/flash-message.ts +16 -0
- package/templates/wdio-template/config/README.md +5 -0
- package/templates/wdio-template/config/environments.ts +40 -0
- package/templates/wdio-template/config/runtime-config.ts +53 -0
- package/templates/wdio-template/config/secret-manager.ts +29 -0
- package/templates/wdio-template/config/test-env.ts +9 -0
- package/templates/wdio-template/data/README.md +9 -0
- package/templates/wdio-template/data/factories/README.md +6 -0
- package/templates/wdio-template/data/factories/data-factory.ts +36 -0
- package/templates/wdio-template/data/generators/README.md +5 -0
- package/templates/wdio-template/data/generators/id-generator.ts +18 -0
- package/templates/wdio-template/data/generators/seeded-faker.ts +14 -0
- package/templates/wdio-template/demo-apps/ui-demo-app/public/styles.css +120 -0
- package/templates/wdio-template/demo-apps/ui-demo-app/src/server.js +152 -0
- package/templates/wdio-template/demo-apps/ui-demo-app/src/store.js +71 -0
- package/templates/wdio-template/demo-apps/ui-demo-app/src/templates.js +121 -0
- package/templates/wdio-template/eslint.config.mjs +86 -0
- package/templates/wdio-template/lint/architecture-plugin.cjs +123 -0
- package/templates/wdio-template/package-lock.json +11058 -0
- package/templates/wdio-template/package.json +44 -0
- package/templates/wdio-template/pages/README.md +6 -0
- package/templates/wdio-template/pages/base-page.ts +15 -0
- package/templates/wdio-template/pages/login-page.ts +27 -0
- package/templates/wdio-template/pages/people-page.ts +54 -0
- package/templates/wdio-template/reporters/README.md +5 -0
- package/templates/wdio-template/reporters/structured-reporter.ts +78 -0
- package/templates/wdio-template/scripts/README.md +5 -0
- package/templates/wdio-template/scripts/ensure-local-env.mjs +36 -0
- package/templates/wdio-template/scripts/generate-allure-report.mjs +72 -0
- package/templates/wdio-template/scripts/run-tests.sh +7 -0
- package/templates/wdio-template/scripts/run-wdio.mjs +114 -0
- package/templates/wdio-template/tests/README.md +7 -0
- package/templates/wdio-template/tests/ui-journey.spec.ts +52 -0
- package/templates/wdio-template/tsconfig.json +22 -0
- package/templates/wdio-template/utils/README.md +5 -0
- package/templates/wdio-template/utils/logger.ts +60 -0
- package/templates/wdio-template/utils/test-step.ts +20 -0
- package/templates/wdio-template/wdio.conf.ts +58 -0
- package/tests/args.test.js +58 -0
- package/tests/local-env.test.js +70 -0
- package/tests/metadata.test.js +147 -0
- package/tests/templates.test.js +44 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@toolstackhq/wdio-template",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Production-ready WebdriverIO automation framework template with deterministic test patterns.",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"ensure:env": "node ./scripts/ensure-local-env.mjs",
|
|
8
|
+
"test": "node ./scripts/ensure-local-env.mjs && node ./scripts/run-wdio.mjs",
|
|
9
|
+
"test:smoke": "node ./scripts/ensure-local-env.mjs && node ./scripts/run-wdio.mjs --mochaOpts.grep @smoke",
|
|
10
|
+
"test:regression": "node ./scripts/ensure-local-env.mjs && node ./scripts/run-wdio.mjs --mochaOpts.grep @regression",
|
|
11
|
+
"test:critical": "node ./scripts/ensure-local-env.mjs && node ./scripts/run-wdio.mjs --mochaOpts.grep @critical",
|
|
12
|
+
"demo:ui": "node ./scripts/ensure-local-env.mjs && node ./demo-apps/ui-demo-app/src/server.js",
|
|
13
|
+
"lint": "eslint .",
|
|
14
|
+
"typecheck": "tsc --noEmit",
|
|
15
|
+
"report:allure": "node ./scripts/generate-allure-report.mjs",
|
|
16
|
+
"report:allure:open": "allure open reports/allure",
|
|
17
|
+
"ci": "bash ./scripts/run-tests.sh"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@faker-js/faker": "^8.4.1",
|
|
21
|
+
"dotenv": "^16.4.5",
|
|
22
|
+
"express": "^4.21.0",
|
|
23
|
+
"zod": "^3.23.8"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@eslint/js": "^9.12.0",
|
|
27
|
+
"@types/node": "^22.7.4",
|
|
28
|
+
"@typescript-eslint/eslint-plugin": "^8.8.1",
|
|
29
|
+
"@typescript-eslint/parser": "^8.8.1",
|
|
30
|
+
"@wdio/allure-reporter": "^9.0.0",
|
|
31
|
+
"@wdio/cli": "^9.0.0",
|
|
32
|
+
"@wdio/globals": "^9.0.0",
|
|
33
|
+
"@wdio/local-runner": "^9.0.0",
|
|
34
|
+
"@wdio/mocha-framework": "^9.0.0",
|
|
35
|
+
"@wdio/reporter": "^9.0.0",
|
|
36
|
+
"@wdio/spec-reporter": "^9.0.0",
|
|
37
|
+
"@wdio/types": "^9.0.0",
|
|
38
|
+
"allure": "^3.3.1",
|
|
39
|
+
"expect-webdriverio": "^5.4.3",
|
|
40
|
+
"eslint": "^9.12.0",
|
|
41
|
+
"typescript": "^5.6.2",
|
|
42
|
+
"webdriverio": "^9.0.0"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Base page object for WebdriverIO pages.
|
|
2
|
+
import { browser } from '@wdio/globals';
|
|
3
|
+
|
|
4
|
+
import type { Logger } from '../utils/logger';
|
|
5
|
+
|
|
6
|
+
export class BasePage {
|
|
7
|
+
constructor(
|
|
8
|
+
protected readonly baseUrl: string,
|
|
9
|
+
protected readonly logger: Logger
|
|
10
|
+
) {}
|
|
11
|
+
|
|
12
|
+
async open(pathname: string): Promise<void> {
|
|
13
|
+
await browser.url(new globalThis.URL(pathname, this.baseUrl).toString());
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Page object for the login screen and its user actions.
|
|
2
|
+
import { $ } from '@wdio/globals';
|
|
3
|
+
|
|
4
|
+
import type { Logger } from '../utils/logger';
|
|
5
|
+
|
|
6
|
+
import { BasePage } from './base-page';
|
|
7
|
+
|
|
8
|
+
export class LoginPage extends BasePage {
|
|
9
|
+
constructor(baseUrl: string, logger: Logger) {
|
|
10
|
+
super(baseUrl, logger);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async goto(): Promise<void> {
|
|
14
|
+
this.logger.info('page.goto', { pageObject: 'LoginPage', page: 'login' });
|
|
15
|
+
await this.open('/login');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async login(username: string, password: string): Promise<void> {
|
|
19
|
+
this.logger.info('login.submit', {
|
|
20
|
+
pageObject: 'LoginPage',
|
|
21
|
+
username
|
|
22
|
+
});
|
|
23
|
+
await $('#username').setValue(username);
|
|
24
|
+
await $('#password').setValue(password);
|
|
25
|
+
await $('button=Sign in').click();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Page object for the people screen used in the starter WDIO workflow.
|
|
2
|
+
import { $ } from '@wdio/globals';
|
|
3
|
+
|
|
4
|
+
import type { PersonRecord } from '../data/factories/data-factory';
|
|
5
|
+
import type { Logger } from '../utils/logger';
|
|
6
|
+
|
|
7
|
+
import { BasePage } from './base-page';
|
|
8
|
+
|
|
9
|
+
export class PeoplePage extends BasePage {
|
|
10
|
+
constructor(baseUrl: string, logger: Logger) {
|
|
11
|
+
super(baseUrl, logger);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async waitForReady(): Promise<void> {
|
|
15
|
+
await $('h1=People').waitForDisplayed();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async getWelcomeMessage(): Promise<string> {
|
|
19
|
+
return $('[data-testid="welcome-message"]').getText();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async addPerson(person: PersonRecord): Promise<void> {
|
|
23
|
+
this.logger.info('person.create', {
|
|
24
|
+
pageObject: 'PeoplePage',
|
|
25
|
+
personId: person.personId
|
|
26
|
+
});
|
|
27
|
+
await $('#personId').setValue(person.personId);
|
|
28
|
+
await $('#name').setValue(person.name);
|
|
29
|
+
await $('#role').setValue(person.role);
|
|
30
|
+
await $('#email').setValue(person.email);
|
|
31
|
+
await $('button=Add person').click();
|
|
32
|
+
await $('[data-testid="flash-message"]').waitForDisplayed();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async getFlashMessage(): Promise<string> {
|
|
36
|
+
return $('[data-testid="flash-message"]').getText();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async getPersonSummary(
|
|
40
|
+
personId: string
|
|
41
|
+
): Promise<{ name: string; role: string; email: string } | null> {
|
|
42
|
+
const row = await $(`[data-testid="person-row-${personId}"]`);
|
|
43
|
+
if (!(await row.isExisting())) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const cells = await row.$$('td');
|
|
48
|
+
return {
|
|
49
|
+
name: await cells[0].getText(),
|
|
50
|
+
role: await cells[1].getText(),
|
|
51
|
+
email: await cells[2].getText()
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Custom reporter that writes machine-readable WebdriverIO lifecycle events to disk.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import reporter from '@wdio/reporter';
|
|
6
|
+
|
|
7
|
+
type ReporterOptions = {
|
|
8
|
+
outputFile?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default class StructuredReporter extends reporter {
|
|
12
|
+
private readonly outputFile: string;
|
|
13
|
+
|
|
14
|
+
constructor(options: ReporterOptions = {}) {
|
|
15
|
+
super({ ...options, stdout: false });
|
|
16
|
+
this.outputFile = path.resolve(
|
|
17
|
+
process.cwd(),
|
|
18
|
+
options.outputFile ?? 'reports/logs/wdio-events.jsonl'
|
|
19
|
+
);
|
|
20
|
+
fs.mkdirSync(path.dirname(this.outputFile), { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
onRunnerStart(runnerStats: { cid?: string; specs?: string[] }): void {
|
|
24
|
+
this.writeEvent({
|
|
25
|
+
event: 'run.started',
|
|
26
|
+
cid: runnerStats.cid ?? 'unknown',
|
|
27
|
+
specCount: runnerStats.specs?.length ?? 0
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
onTestPass(testStats: { title: string; duration?: number }): void {
|
|
32
|
+
this.writeTestEvent('passed', testStats);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
onTestFail(testStats: {
|
|
36
|
+
title: string;
|
|
37
|
+
duration?: number;
|
|
38
|
+
errors?: Array<{ message?: string }>;
|
|
39
|
+
}): void {
|
|
40
|
+
this.writeTestEvent('failed', testStats, {
|
|
41
|
+
error: testStats.errors?.[0]?.message
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
onTestSkip(testStats: { title: string; duration?: number }): void {
|
|
46
|
+
this.writeTestEvent('skipped', testStats);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
onRunnerEnd(runnerStats: { failures?: number }): void {
|
|
50
|
+
this.writeEvent({
|
|
51
|
+
event: 'run.finished',
|
|
52
|
+
status: runnerStats.failures ? 'failed' : 'passed'
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private writeTestEvent(
|
|
57
|
+
status: string,
|
|
58
|
+
testStats: { title: string; duration?: number },
|
|
59
|
+
extra: Record<string, unknown> = {}
|
|
60
|
+
): void {
|
|
61
|
+
this.writeEvent({
|
|
62
|
+
event: 'test.finished',
|
|
63
|
+
title: testStats.title,
|
|
64
|
+
tags: testStats.title.match(/@\w+/g) ?? [],
|
|
65
|
+
status,
|
|
66
|
+
durationMs: testStats.duration ?? 0,
|
|
67
|
+
...extra
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private writeEvent(payload: Record<string, unknown>): void {
|
|
72
|
+
fs.appendFileSync(
|
|
73
|
+
this.outputFile,
|
|
74
|
+
`${JSON.stringify({ timestamp: new Date().toISOString(), ...payload })}\n`,
|
|
75
|
+
'utf8'
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
const currentProcess = globalThis.process;
|
|
8
|
+
const envPath = path.resolve(currentProcess.cwd(), '.env');
|
|
9
|
+
|
|
10
|
+
if (fs.existsSync(envPath)) {
|
|
11
|
+
currentProcess.exit(0);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const projectSlug = path
|
|
15
|
+
.basename(currentProcess.cwd())
|
|
16
|
+
.toLowerCase()
|
|
17
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
18
|
+
.replace(/^-+|-+$/g, '')
|
|
19
|
+
.slice(0, 12);
|
|
20
|
+
const username = `${projectSlug || 'local'}-${crypto.randomBytes(3).toString('hex')}`;
|
|
21
|
+
const password = `${crypto.randomBytes(9).toString('base64url')}A1!`;
|
|
22
|
+
|
|
23
|
+
const envContents = [
|
|
24
|
+
'TEST_ENV=dev',
|
|
25
|
+
'TEST_RUN_ID=local',
|
|
26
|
+
'DEV_UI_BASE_URL=http://127.0.0.1:3000',
|
|
27
|
+
`DEV_APP_USERNAME=${username}`,
|
|
28
|
+
`DEV_APP_PASSWORD=${password}`,
|
|
29
|
+
`UI_DEMO_USERNAME=${username}`,
|
|
30
|
+
`UI_DEMO_PASSWORD=${password}`
|
|
31
|
+
].join('\n');
|
|
32
|
+
|
|
33
|
+
fs.writeFileSync(envPath, `${envContents}\n`, 'utf8');
|
|
34
|
+
currentProcess.stdout.write(
|
|
35
|
+
`Generated local .env with demo credentials for ${username}\n`
|
|
36
|
+
);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Builds a local Allure report from raw test results after a test run completes.
|
|
2
|
+
import { AllureReport, readConfig } from '@allurereport/core';
|
|
3
|
+
import { readdir, rm, stat } from 'node:fs/promises';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
import process from 'node:process';
|
|
6
|
+
|
|
7
|
+
const cwd = process.cwd();
|
|
8
|
+
const resultsDir = resolve(cwd, 'allure-results');
|
|
9
|
+
const outputDir = resolve(cwd, 'reports/allure');
|
|
10
|
+
|
|
11
|
+
const collectResultFiles = async () => {
|
|
12
|
+
const entries = (await readdir(resultsDir)).sort();
|
|
13
|
+
const files = [];
|
|
14
|
+
|
|
15
|
+
for (const entry of entries) {
|
|
16
|
+
const filePath = join(resultsDir, entry);
|
|
17
|
+
const entryStat = await stat(filePath);
|
|
18
|
+
|
|
19
|
+
if (entryStat.isFile()) {
|
|
20
|
+
files.push(filePath);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return files;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const generateReport = async () => {
|
|
28
|
+
try {
|
|
29
|
+
await stat(resultsDir);
|
|
30
|
+
} catch {
|
|
31
|
+
process.stdout.write(
|
|
32
|
+
'Skipping Allure report generation because allure-results does not exist.\n'
|
|
33
|
+
);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const files = await collectResultFiles();
|
|
38
|
+
|
|
39
|
+
if (files.length === 0) {
|
|
40
|
+
process.stdout.write(
|
|
41
|
+
'Skipping Allure report generation because no result files were found.\n'
|
|
42
|
+
);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
await rm(outputDir, { force: true, recursive: true });
|
|
47
|
+
|
|
48
|
+
const config = await readConfig(cwd, 'allurerc.mjs', { output: outputDir });
|
|
49
|
+
const report = new AllureReport(config);
|
|
50
|
+
|
|
51
|
+
await report.start();
|
|
52
|
+
|
|
53
|
+
for (const file of files) {
|
|
54
|
+
await report.readFile(file);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await report.done();
|
|
58
|
+
|
|
59
|
+
process.stdout.write(
|
|
60
|
+
'Allure report generated at reports/allure/index.html\n'
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
generateReport().catch((error) => {
|
|
65
|
+
if (error instanceof Error) {
|
|
66
|
+
process.stderr.write(`${error.message}\n`);
|
|
67
|
+
} else {
|
|
68
|
+
process.stderr.write(`${String(error)}\n`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
process.exit(1);
|
|
72
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Starts the local demo app when needed, then launches the WebdriverIO runner.
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
|
|
8
|
+
import dotenv from 'dotenv';
|
|
9
|
+
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
const cwd = process.cwd();
|
|
12
|
+
const healthUrl = 'http://127.0.0.1:3000/health';
|
|
13
|
+
const environment = process.env.TEST_ENV ?? 'dev';
|
|
14
|
+
const environmentDefaults = {
|
|
15
|
+
dev: 'http://127.0.0.1:3000',
|
|
16
|
+
staging: 'https://staging-ui.example.internal',
|
|
17
|
+
prod: 'https://ui.example.internal'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
dotenv.config({ path: path.resolve(cwd, '.env') });
|
|
21
|
+
dotenv.config({
|
|
22
|
+
path: path.resolve(cwd, `.env.${environment}`),
|
|
23
|
+
override: true
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const uiBaseUrl =
|
|
27
|
+
process.env[`${environment.toUpperCase()}_UI_BASE_URL`] ??
|
|
28
|
+
process.env.UI_BASE_URL ??
|
|
29
|
+
environmentDefaults[environment] ??
|
|
30
|
+
environmentDefaults.dev;
|
|
31
|
+
|
|
32
|
+
const shouldAutoStartDemoApp =
|
|
33
|
+
environment === 'dev' &&
|
|
34
|
+
uiBaseUrl === environmentDefaults.dev &&
|
|
35
|
+
process.env.WDIO_DISABLE_LOCAL_DEMO_APP !== 'true';
|
|
36
|
+
|
|
37
|
+
function getCommandName(command) {
|
|
38
|
+
return process.platform === 'win32' ? `${command}.cmd` : command;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function spawnCommand(command, commandArgs, options = {}) {
|
|
42
|
+
return spawn(getCommandName(command), commandArgs, {
|
|
43
|
+
cwd,
|
|
44
|
+
stdio: 'inherit',
|
|
45
|
+
...options
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function waitForHealthcheck(url, timeoutMs = 30_000) {
|
|
50
|
+
const start = Date.now();
|
|
51
|
+
|
|
52
|
+
while (Date.now() - start < timeoutMs) {
|
|
53
|
+
try {
|
|
54
|
+
const response = await globalThis.fetch(url);
|
|
55
|
+
if (response.ok) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// Service is not ready yet.
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await new Promise((resolve) => globalThis.setTimeout(resolve, 500));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
throw new Error(`Timed out waiting for ${url}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function killChild(child) {
|
|
69
|
+
if (!child || child.killed) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
child.kill('SIGTERM');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function run() {
|
|
77
|
+
let demoAppProcess;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
if (shouldAutoStartDemoApp) {
|
|
81
|
+
demoAppProcess = spawnCommand('npm', ['run', 'demo:ui']);
|
|
82
|
+
await waitForHealthcheck(healthUrl);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const wdioProcess = spawnCommand('npx', [
|
|
86
|
+
'wdio',
|
|
87
|
+
'run',
|
|
88
|
+
'./wdio.conf.ts',
|
|
89
|
+
...args
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
const exitCode = await new Promise((resolve) => {
|
|
93
|
+
wdioProcess.on('close', resolve);
|
|
94
|
+
wdioProcess.on('error', () => resolve(1));
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (exitCode !== 0) {
|
|
98
|
+
process.exit(Number(exitCode) || 1);
|
|
99
|
+
}
|
|
100
|
+
} finally {
|
|
101
|
+
killChild(demoAppProcess);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
106
|
+
process.on(signal, () => process.exit(1));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
run().catch((error) => {
|
|
110
|
+
process.stderr.write(
|
|
111
|
+
`${error instanceof Error ? error.message : String(error)}\n`
|
|
112
|
+
);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Starter WDIO scenario that demonstrates the preferred spec style for this template.
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import { loadRuntimeConfig } from '../config/runtime-config';
|
|
5
|
+
import { DataFactory } from '../data/factories/data-factory';
|
|
6
|
+
import { LoginPage } from '../pages/login-page';
|
|
7
|
+
import { PeoplePage } from '../pages/people-page';
|
|
8
|
+
import { createLogger } from '../utils/logger';
|
|
9
|
+
import { StepLogger } from '../utils/test-step';
|
|
10
|
+
|
|
11
|
+
describe('UI starter journey', () => {
|
|
12
|
+
it('login and add one person @smoke @critical', async () => {
|
|
13
|
+
const appConfig = loadRuntimeConfig();
|
|
14
|
+
const logger = createLogger({
|
|
15
|
+
test: 'ui-journey.spec.ts > UI starter journey > login and add one person @smoke @critical'
|
|
16
|
+
});
|
|
17
|
+
const stepLogger = new StepLogger(logger.child({ scope: 'steps' }));
|
|
18
|
+
const dataFactory = new DataFactory(appConfig.testRunId);
|
|
19
|
+
const loginPage = new LoginPage(
|
|
20
|
+
appConfig.uiBaseUrl,
|
|
21
|
+
logger.child({ pageObject: 'LoginPage' })
|
|
22
|
+
);
|
|
23
|
+
const peoplePage = new PeoplePage(
|
|
24
|
+
appConfig.uiBaseUrl,
|
|
25
|
+
logger.child({ pageObject: 'PeoplePage' })
|
|
26
|
+
);
|
|
27
|
+
const person = dataFactory.person();
|
|
28
|
+
|
|
29
|
+
await stepLogger.run('Sign in to the demo app', async () => {
|
|
30
|
+
await loginPage.goto();
|
|
31
|
+
await loginPage.login(
|
|
32
|
+
appConfig.credentials.username,
|
|
33
|
+
appConfig.credentials.password
|
|
34
|
+
);
|
|
35
|
+
await peoplePage.waitForReady();
|
|
36
|
+
assert.match(
|
|
37
|
+
await peoplePage.getWelcomeMessage(),
|
|
38
|
+
new RegExp(appConfig.credentials.username)
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
await stepLogger.run('Add one person and verify the list', async () => {
|
|
43
|
+
await peoplePage.addPerson(person);
|
|
44
|
+
assert.match(await peoplePage.getFlashMessage(), /Person added/);
|
|
45
|
+
assert.deepEqual(await peoplePage.getPersonSummary(person.personId), {
|
|
46
|
+
name: person.name,
|
|
47
|
+
role: person.role,
|
|
48
|
+
email: person.email
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "esnext",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"lib": ["ES2022", "DOM"],
|
|
7
|
+
"types": ["node", "mocha", "@wdio/globals/types"],
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"baseUrl": "."
|
|
13
|
+
},
|
|
14
|
+
"include": ["**/*.ts"],
|
|
15
|
+
"exclude": [
|
|
16
|
+
"node_modules",
|
|
17
|
+
"reports",
|
|
18
|
+
"allure-results",
|
|
19
|
+
"allure-report",
|
|
20
|
+
"test-results"
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Structured JSON logger used by tests, pages, and helper classes.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
type LogLevel = 'info' | 'error';
|
|
6
|
+
|
|
7
|
+
type LogContext = Record<string, string | number | boolean | null | undefined>;
|
|
8
|
+
|
|
9
|
+
const LOG_FILE = path.resolve(process.cwd(), 'reports/logs/execution.log');
|
|
10
|
+
|
|
11
|
+
function ensureLogDirectory(): void {
|
|
12
|
+
fs.mkdirSync(path.dirname(LOG_FILE), { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function serialize(
|
|
16
|
+
level: LogLevel,
|
|
17
|
+
message: string,
|
|
18
|
+
context?: LogContext
|
|
19
|
+
): string {
|
|
20
|
+
return JSON.stringify({
|
|
21
|
+
timestamp: new Date().toISOString(),
|
|
22
|
+
level,
|
|
23
|
+
message,
|
|
24
|
+
...context
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class Logger {
|
|
29
|
+
constructor(private readonly context: LogContext = {}) {
|
|
30
|
+
ensureLogDirectory();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
child(context: LogContext): Logger {
|
|
34
|
+
return new Logger({
|
|
35
|
+
...this.context,
|
|
36
|
+
...context
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
info(message: string, context?: LogContext): void {
|
|
41
|
+
this.write('info', message, context);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
error(message: string, context?: LogContext): void {
|
|
45
|
+
this.write('error', message, context);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private write(level: LogLevel, message: string, context?: LogContext): void {
|
|
49
|
+
const line = serialize(level, message, {
|
|
50
|
+
...this.context,
|
|
51
|
+
...context
|
|
52
|
+
});
|
|
53
|
+
fs.appendFileSync(LOG_FILE, `${line}\n`, 'utf8');
|
|
54
|
+
console.log(line);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function createLogger(context: LogContext = {}): Logger {
|
|
59
|
+
return new Logger(context);
|
|
60
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Logger } from './logger';
|
|
2
|
+
|
|
3
|
+
export class StepLogger {
|
|
4
|
+
constructor(private readonly logger: Logger) {}
|
|
5
|
+
|
|
6
|
+
async run<T>(message: string, callback: () => Promise<T>): Promise<T> {
|
|
7
|
+
this.logger.info('step.started', { step: message });
|
|
8
|
+
try {
|
|
9
|
+
const result = await callback();
|
|
10
|
+
this.logger.info('step.passed', { step: message });
|
|
11
|
+
return result;
|
|
12
|
+
} catch (error) {
|
|
13
|
+
this.logger.error('step.failed', {
|
|
14
|
+
step: message,
|
|
15
|
+
error: error instanceof Error ? error.message : String(error)
|
|
16
|
+
});
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|