@toolstackhq/create-qa-patterns 1.0.0 → 1.0.2
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 +29 -10
- package/index.js +460 -1
- package/package.json +4 -3
- package/templates/playwright-template/.env.example +17 -0
- package/templates/playwright-template/.github/workflows/playwright-tests.yml +126 -0
- package/templates/playwright-template/README.md +234 -0
- package/templates/playwright-template/allurerc.mjs +10 -0
- package/templates/playwright-template/components/flash-message.ts +16 -0
- package/templates/playwright-template/config/environments.ts +41 -0
- package/templates/playwright-template/config/runtime-config.ts +53 -0
- package/templates/playwright-template/config/secret-manager.ts +28 -0
- package/templates/playwright-template/config/test-env.ts +9 -0
- package/templates/playwright-template/data/README.md +9 -0
- package/templates/playwright-template/data/factories/data-factory.ts +33 -0
- package/templates/playwright-template/data/generators/id-generator.ts +17 -0
- package/templates/playwright-template/data/generators/seeded-faker.ts +13 -0
- package/templates/playwright-template/docker/Dockerfile +21 -0
- package/templates/playwright-template/eslint.config.mjs +66 -0
- package/templates/playwright-template/fixtures/test-fixtures.ts +43 -0
- package/templates/playwright-template/lint/architecture-plugin.cjs +118 -0
- package/templates/playwright-template/package-lock.json +4724 -0
- package/templates/playwright-template/package.json +34 -0
- package/templates/playwright-template/pages/base-page.ts +24 -0
- package/templates/playwright-template/pages/login-page.ts +22 -0
- package/templates/playwright-template/pages/people-page.ts +39 -0
- package/templates/playwright-template/playwright.config.ts +46 -0
- package/templates/playwright-template/reporters/structured-reporter.ts +61 -0
- package/templates/playwright-template/scripts/generate-allure-report.mjs +57 -0
- package/templates/playwright-template/scripts/run-tests.sh +6 -0
- package/templates/playwright-template/tests/api-people.spec.ts +28 -0
- package/templates/playwright-template/tests/ui-journey.spec.ts +30 -0
- package/templates/playwright-template/tsconfig.json +31 -0
- package/templates/playwright-template/utils/logger.ts +55 -0
- package/templates/playwright-template/utils/test-step.ts +22 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@toolstackhq/playwright-template",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Production-ready Playwright automation framework template with deterministic test patterns.",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "playwright test",
|
|
8
|
+
"test:smoke": "playwright test --grep @smoke",
|
|
9
|
+
"test:regression": "playwright test --grep @regression",
|
|
10
|
+
"test:critical": "playwright test --grep @critical",
|
|
11
|
+
"lint": "eslint .",
|
|
12
|
+
"typecheck": "tsc --noEmit",
|
|
13
|
+
"report:playwright": "playwright show-report reports/html",
|
|
14
|
+
"report:allure": "node ./scripts/generate-allure-report.mjs",
|
|
15
|
+
"report:allure:open": "allure open reports/allure",
|
|
16
|
+
"ci": "bash ./scripts/run-tests.sh"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@faker-js/faker": "^8.4.1",
|
|
20
|
+
"dotenv": "^16.4.5",
|
|
21
|
+
"zod": "^3.23.8"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@playwright/test": "^1.53.0",
|
|
25
|
+
"@types/node": "^22.7.4",
|
|
26
|
+
"@typescript-eslint/eslint-plugin": "^8.8.1",
|
|
27
|
+
"@typescript-eslint/parser": "^8.8.1",
|
|
28
|
+
"@eslint/js": "^9.12.0",
|
|
29
|
+
"allure": "^3.3.1",
|
|
30
|
+
"allure-playwright": "^3.6.0",
|
|
31
|
+
"eslint": "^9.12.0",
|
|
32
|
+
"typescript": "^5.6.2"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Page } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
import { FlashMessage } from "../components/flash-message";
|
|
4
|
+
import type { Logger } from "../utils/logger";
|
|
5
|
+
|
|
6
|
+
export abstract class BasePage {
|
|
7
|
+
readonly flashMessage: FlashMessage;
|
|
8
|
+
|
|
9
|
+
constructor(
|
|
10
|
+
protected readonly page: Page,
|
|
11
|
+
private readonly baseUrl: string,
|
|
12
|
+
protected readonly logger: Logger
|
|
13
|
+
) {
|
|
14
|
+
this.flashMessage = new FlashMessage(page);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
protected buildUrl(pathname: string): string {
|
|
18
|
+
return new URL(pathname, this.baseUrl).toString();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async getWelcomeMessage(): Promise<string> {
|
|
22
|
+
return (await this.page.getByTestId("welcome-message").textContent()) ?? "";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Page } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
import type { Logger } from "../utils/logger";
|
|
4
|
+
import { BasePage } from "./base-page";
|
|
5
|
+
|
|
6
|
+
export class LoginPage extends BasePage {
|
|
7
|
+
constructor(page: Page, baseUrl: string, logger: Logger) {
|
|
8
|
+
super(page, baseUrl, logger);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async goto(): Promise<void> {
|
|
12
|
+
this.logger.info("page.goto", { page: "login" });
|
|
13
|
+
await this.page.goto(this.buildUrl("/login"));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async login(username: string, password: string): Promise<void> {
|
|
17
|
+
this.logger.info("login.submit", { username });
|
|
18
|
+
await this.page.getByLabel("Username").fill(username);
|
|
19
|
+
await this.page.getByLabel("Password").fill(password);
|
|
20
|
+
await this.page.getByRole("button", { name: "Sign in" }).click();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Page } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
import type { PersonRecord } from "../data/factories/data-factory";
|
|
4
|
+
import type { Logger } from "../utils/logger";
|
|
5
|
+
import { BasePage } from "./base-page";
|
|
6
|
+
|
|
7
|
+
export class PeoplePage extends BasePage {
|
|
8
|
+
constructor(page: Page, baseUrl: string, logger: Logger) {
|
|
9
|
+
super(page, baseUrl, logger);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async waitForReady(): Promise<void> {
|
|
13
|
+
await this.page.getByRole("heading", { level: 1, name: "People", exact: true }).waitFor();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async addPerson(person: PersonRecord): Promise<void> {
|
|
17
|
+
this.logger.info("person.create", { personId: person.personId });
|
|
18
|
+
await this.page.getByLabel("Person ID").fill(person.personId);
|
|
19
|
+
await this.page.getByLabel("Name").fill(person.name);
|
|
20
|
+
await this.page.getByLabel("Role").fill(person.role);
|
|
21
|
+
await this.page.getByLabel("Email").fill(person.email);
|
|
22
|
+
await this.page.getByRole("button", { name: "Add person" }).click();
|
|
23
|
+
await this.page.waitForLoadState("networkidle");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async getPersonSummary(personId: string): Promise<{ name: string; role: string; email: string } | null> {
|
|
27
|
+
const row = this.page.getByTestId(`person-row-${personId}`);
|
|
28
|
+
if (!(await row.count())) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const cells = row.getByRole("cell");
|
|
33
|
+
return {
|
|
34
|
+
name: (await cells.nth(0).textContent()) ?? "",
|
|
35
|
+
role: (await cells.nth(1).textContent()) ?? "",
|
|
36
|
+
email: (await cells.nth(2).textContent()) ?? ""
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { defineConfig, devices } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
import { loadRuntimeConfig } from "./config/runtime-config";
|
|
4
|
+
|
|
5
|
+
const runtimeConfig = loadRuntimeConfig();
|
|
6
|
+
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
testDir: "./tests",
|
|
9
|
+
fullyParallel: false,
|
|
10
|
+
forbidOnly: Boolean(process.env.CI),
|
|
11
|
+
retries: process.env.CI ? 1 : 0,
|
|
12
|
+
workers: process.env.CI ? 2 : undefined,
|
|
13
|
+
timeout: 45_000,
|
|
14
|
+
expect: {
|
|
15
|
+
timeout: 10_000
|
|
16
|
+
},
|
|
17
|
+
outputDir: "test-results",
|
|
18
|
+
reporter: [
|
|
19
|
+
["list"],
|
|
20
|
+
["html", { open: "never", outputFolder: "reports/html" }],
|
|
21
|
+
// Keep the HTML reporter as the default path most users expect.
|
|
22
|
+
// Remove the Allure line below if you prefer to stay with Playwright's built-in reporters only.
|
|
23
|
+
["allure-playwright", { resultsDir: "allure-results" }],
|
|
24
|
+
["./reporters/structured-reporter.ts", { outputFile: "reports/logs/playwright-events.jsonl" }]
|
|
25
|
+
],
|
|
26
|
+
use: {
|
|
27
|
+
baseURL: runtimeConfig.uiBaseUrl,
|
|
28
|
+
trace: "retain-on-failure",
|
|
29
|
+
screenshot: "only-on-failure",
|
|
30
|
+
video: "retain-on-failure",
|
|
31
|
+
headless: !process.env.PWDEBUG
|
|
32
|
+
},
|
|
33
|
+
metadata: {
|
|
34
|
+
environment: runtimeConfig.testEnv,
|
|
35
|
+
testRunId: runtimeConfig.testRunId,
|
|
36
|
+
apiBaseUrl: runtimeConfig.apiBaseUrl
|
|
37
|
+
},
|
|
38
|
+
projects: [
|
|
39
|
+
{
|
|
40
|
+
name: "chromium",
|
|
41
|
+
use: {
|
|
42
|
+
...devices["Desktop Chrome"]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
FullConfig,
|
|
6
|
+
FullResult,
|
|
7
|
+
Reporter,
|
|
8
|
+
Suite,
|
|
9
|
+
TestCase,
|
|
10
|
+
TestResult
|
|
11
|
+
} from "@playwright/test/reporter";
|
|
12
|
+
|
|
13
|
+
type ReporterOptions = {
|
|
14
|
+
outputFile?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
class StructuredReporter implements Reporter {
|
|
18
|
+
private outputFile = path.resolve(process.cwd(), "reports/logs/playwright-events.jsonl");
|
|
19
|
+
|
|
20
|
+
constructor(options?: ReporterOptions) {
|
|
21
|
+
if (options?.outputFile) {
|
|
22
|
+
this.outputFile = path.resolve(process.cwd(), options.outputFile);
|
|
23
|
+
}
|
|
24
|
+
fs.mkdirSync(path.dirname(this.outputFile), { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
onBegin(config: FullConfig, suite: Suite): void {
|
|
28
|
+
this.write({
|
|
29
|
+
event: "run.started",
|
|
30
|
+
projectCount: config.projects.length,
|
|
31
|
+
testCount: suite.allTests().length
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
onTestEnd(test: TestCase, result: TestResult): void {
|
|
36
|
+
this.write({
|
|
37
|
+
event: "test.finished",
|
|
38
|
+
title: test.title,
|
|
39
|
+
tags: test.title.match(/@\w+/g) ?? [],
|
|
40
|
+
status: result.status,
|
|
41
|
+
durationMs: result.duration
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
onEnd(result: FullResult): void {
|
|
46
|
+
this.write({
|
|
47
|
+
event: "run.finished",
|
|
48
|
+
status: result.status
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private write(payload: Record<string, unknown>): void {
|
|
53
|
+
fs.appendFileSync(
|
|
54
|
+
this.outputFile,
|
|
55
|
+
`${JSON.stringify({ timestamp: new Date().toISOString(), ...payload })}\n`,
|
|
56
|
+
"utf8"
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default StructuredReporter;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { AllureReport, readConfig } from "@allurereport/core";
|
|
2
|
+
import { readdir, rm, stat } from "node:fs/promises";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
|
|
6
|
+
const cwd = process.cwd();
|
|
7
|
+
const resultsDir = resolve(cwd, "allure-results");
|
|
8
|
+
const outputDir = resolve(cwd, "reports/allure");
|
|
9
|
+
|
|
10
|
+
const collectResultFiles = async () => {
|
|
11
|
+
const entries = (await readdir(resultsDir)).sort();
|
|
12
|
+
const files = [];
|
|
13
|
+
|
|
14
|
+
for (const entry of entries) {
|
|
15
|
+
const filePath = join(resultsDir, entry);
|
|
16
|
+
const entryStat = await stat(filePath);
|
|
17
|
+
|
|
18
|
+
if (entryStat.isFile()) {
|
|
19
|
+
files.push(filePath);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return files;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const generateReport = async () => {
|
|
27
|
+
const files = await collectResultFiles();
|
|
28
|
+
|
|
29
|
+
if (files.length === 0) {
|
|
30
|
+
throw new Error("No Allure result files found in allure-results. Run the tests before generating a report.");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
await rm(outputDir, { force: true, recursive: true });
|
|
34
|
+
|
|
35
|
+
const config = await readConfig(cwd, "allurerc.mjs", { output: outputDir });
|
|
36
|
+
const report = new AllureReport(config);
|
|
37
|
+
|
|
38
|
+
await report.start();
|
|
39
|
+
|
|
40
|
+
for (const file of files) {
|
|
41
|
+
await report.readFile(file);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
await report.done();
|
|
45
|
+
|
|
46
|
+
process.stdout.write("Allure report generated at reports/allure/index.html\n");
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
generateReport().catch((error) => {
|
|
50
|
+
if (error instanceof Error) {
|
|
51
|
+
process.stderr.write(`${error.message}\n`);
|
|
52
|
+
} else {
|
|
53
|
+
process.stderr.write(`${String(error)}\n`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
process.exit(1);
|
|
57
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { expect, test } from "../fixtures/test-fixtures";
|
|
2
|
+
|
|
3
|
+
test.describe("API starter flow", () => {
|
|
4
|
+
test("create and list one person @regression", async ({
|
|
5
|
+
appConfig,
|
|
6
|
+
dataFactory,
|
|
7
|
+
request,
|
|
8
|
+
stepLogger
|
|
9
|
+
}) => {
|
|
10
|
+
const person = dataFactory.person();
|
|
11
|
+
|
|
12
|
+
await stepLogger.run("Create one person through the API", async () => {
|
|
13
|
+
const response = await request.post(`${appConfig.apiBaseUrl}/people`, {
|
|
14
|
+
data: person
|
|
15
|
+
});
|
|
16
|
+
expect(response.ok()).toBeTruthy();
|
|
17
|
+
expect(await response.json()).toMatchObject(person);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
await stepLogger.run("List people and verify the new record", async () => {
|
|
21
|
+
const response = await request.get(`${appConfig.apiBaseUrl}/people`);
|
|
22
|
+
expect(response.ok()).toBeTruthy();
|
|
23
|
+
|
|
24
|
+
const people = await response.json();
|
|
25
|
+
expect(people).toContainEqual(expect.objectContaining({ personId: person.personId }));
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { expect, test } from "../fixtures/test-fixtures";
|
|
2
|
+
|
|
3
|
+
test.describe("UI starter journey", () => {
|
|
4
|
+
test("login and add one person @smoke @critical", async ({
|
|
5
|
+
appConfig,
|
|
6
|
+
loginPage,
|
|
7
|
+
peoplePage,
|
|
8
|
+
dataFactory,
|
|
9
|
+
stepLogger
|
|
10
|
+
}) => {
|
|
11
|
+
const person = dataFactory.person();
|
|
12
|
+
|
|
13
|
+
await stepLogger.run("Sign in to the demo app", async () => {
|
|
14
|
+
await loginPage.goto();
|
|
15
|
+
await loginPage.login(appConfig.credentials.username, appConfig.credentials.password);
|
|
16
|
+
await peoplePage.waitForReady();
|
|
17
|
+
expect(await peoplePage.getWelcomeMessage()).toContain(appConfig.credentials.username);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
await stepLogger.run("Add one person and verify the list", async () => {
|
|
21
|
+
await peoplePage.addPerson(person);
|
|
22
|
+
expect(await peoplePage.flashMessage.getText()).toContain("Person added");
|
|
23
|
+
expect(await peoplePage.getPersonSummary(person.personId)).toEqual({
|
|
24
|
+
name: person.name,
|
|
25
|
+
role: person.role,
|
|
26
|
+
email: person.email
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"lib": [
|
|
7
|
+
"ES2022",
|
|
8
|
+
"DOM"
|
|
9
|
+
],
|
|
10
|
+
"types": [
|
|
11
|
+
"node",
|
|
12
|
+
"@playwright/test"
|
|
13
|
+
],
|
|
14
|
+
"strict": true,
|
|
15
|
+
"esModuleInterop": true,
|
|
16
|
+
"resolveJsonModule": true,
|
|
17
|
+
"skipLibCheck": true,
|
|
18
|
+
"baseUrl": "."
|
|
19
|
+
},
|
|
20
|
+
"include": [
|
|
21
|
+
"**/*.ts"
|
|
22
|
+
],
|
|
23
|
+
"exclude": [
|
|
24
|
+
"node_modules",
|
|
25
|
+
"reports",
|
|
26
|
+
"allure-results",
|
|
27
|
+
"allure-report",
|
|
28
|
+
"test-results",
|
|
29
|
+
"playwright-report"
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
type LogLevel = "info" | "error";
|
|
5
|
+
|
|
6
|
+
type LogContext = Record<string, string | number | boolean | null | undefined>;
|
|
7
|
+
|
|
8
|
+
const LOG_FILE = path.resolve(process.cwd(), "reports/logs/execution.log");
|
|
9
|
+
|
|
10
|
+
function ensureLogDirectory(): void {
|
|
11
|
+
fs.mkdirSync(path.dirname(LOG_FILE), { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function serialize(level: LogLevel, message: string, context?: LogContext): string {
|
|
15
|
+
return JSON.stringify({
|
|
16
|
+
timestamp: new Date().toISOString(),
|
|
17
|
+
level,
|
|
18
|
+
message,
|
|
19
|
+
...context
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class Logger {
|
|
24
|
+
constructor(private readonly context: LogContext = {}) {
|
|
25
|
+
ensureLogDirectory();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
child(context: LogContext): Logger {
|
|
29
|
+
return new Logger({
|
|
30
|
+
...this.context,
|
|
31
|
+
...context
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
info(message: string, context?: LogContext): void {
|
|
36
|
+
this.write("info", message, context);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
error(message: string, context?: LogContext): void {
|
|
40
|
+
this.write("error", message, context);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private write(level: LogLevel, message: string, context?: LogContext): void {
|
|
44
|
+
const line = serialize(level, message, {
|
|
45
|
+
...this.context,
|
|
46
|
+
...context
|
|
47
|
+
});
|
|
48
|
+
fs.appendFileSync(LOG_FILE, `${line}\n`, "utf8");
|
|
49
|
+
console.log(line);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createLogger(context: LogContext = {}): Logger {
|
|
54
|
+
return new Logger(context);
|
|
55
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { test } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
import type { Logger } from "./logger";
|
|
4
|
+
|
|
5
|
+
export class StepLogger {
|
|
6
|
+
constructor(private readonly logger: Logger) {}
|
|
7
|
+
|
|
8
|
+
async run<T>(message: string, callback: () => Promise<T>): Promise<T> {
|
|
9
|
+
this.logger.info("step.started", { step: message });
|
|
10
|
+
try {
|
|
11
|
+
const result = await test.step(message, callback);
|
|
12
|
+
this.logger.info("step.passed", { step: message });
|
|
13
|
+
return result;
|
|
14
|
+
} catch (error) {
|
|
15
|
+
this.logger.error("step.failed", {
|
|
16
|
+
step: message,
|
|
17
|
+
error: error instanceof Error ? error.message : String(error)
|
|
18
|
+
});
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|