From 9c2a5eabf3cd948441f376ed93999b70ad90a9ba Mon Sep 17 00:00:00 2001 From: lridge Date: Thu, 18 Dec 2025 15:06:55 -0500 Subject: [PATCH 1/2] add dotenv for testing against cloud; add a cloud playwright config, add ability to set test key with annotations, add ability to handle multiple test case annotations --- .env.example | 1 + .gitignore | 5 ++++ package-lock.json | 34 +++++++++++++++++++---- package.json | 7 +++-- playwright.cloud.config.ts | 40 +++++++++++++++++++++++++++ playwright.config.ts | 2 +- src/cloud.ts | 56 +++++++++++++++++++++++++------------- src/index.ts | 30 ++++++++++++++------ src/server.ts | 31 ++++++++++++++------- tests/annotated.test.ts | 29 ++++++++++++++++++++ 10 files changed, 187 insertions(+), 48 deletions(-) create mode 100644 .env.example create mode 100644 playwright.cloud.config.ts create mode 100644 tests/annotated.test.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..dc4308f --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +ZEPHYR_AUTHORIZATION_TOKEN=MySecretTokenValueHere \ No newline at end of file diff --git a/.gitignore b/.gitignore index 30be650..f793097 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,11 @@ yarn-debug.log* yarn-error.log* lerna-debug.log* +# Dotenv environment variables file +.env + +test-results/ + # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json diff --git a/package-lock.json b/package-lock.json index d820ad8..d839ce6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ }, "devDependencies": { "@playwright/test": "^1.36.2", + "dotenv": "^17.2.3", "prettier": "^3.0.1", "release-it": "^19.0.2", "typescript": "^5.1.6" @@ -780,6 +781,19 @@ } } }, + "node_modules/c12/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -1058,9 +1072,9 @@ "license": "MIT" }, "node_modules/dotenv": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", - "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -3384,6 +3398,14 @@ "perfect-debounce": "^1.0.0", "pkg-types": "^2.1.0", "rc9": "^2.1.2" + }, + "dependencies": { + "dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true + } } }, "call-bind-apply-helpers": { @@ -3562,9 +3584,9 @@ "dev": true }, "dotenv": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", - "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", "dev": true }, "dunder-proto": { diff --git a/package.json b/package.json index 65bd120..2b3bdf3 100644 --- a/package.json +++ b/package.json @@ -37,15 +37,16 @@ "homepage": "https://github.com/elaichenkov/playwright-zephyr#readme", "devDependencies": { "@playwright/test": "^1.36.2", + "dotenv": "^17.2.3", "prettier": "^3.0.1", "release-it": "^19.0.2", "typescript": "^5.1.6" }, "dependencies": { + "@types/adm-zip": "^0.5.0", + "adm-zip": "^0.5.10", "axios": "^1.4.0", "picocolors": "^1.0.0", - "table": "^6.8.1", - "@types/adm-zip": "^0.5.0", - "adm-zip": "^0.5.10" + "table": "^6.8.1" } } diff --git a/playwright.cloud.config.ts b/playwright.cloud.config.ts new file mode 100644 index 0000000..f16fd3f --- /dev/null +++ b/playwright.cloud.config.ts @@ -0,0 +1,40 @@ +// playwright.config.ts +import { PlaywrightTestConfig } from '@playwright/test'; +import * as dotenv from 'dotenv'; + +dotenv.config({ path: '.env' }); + +const config: PlaywrightTestConfig = { + reporter: [['list'], ['./src/cloud', { + runName: `testrun-${new Date().getTime()}`, + authorizationToken: process.env.ZEPHYR_AUTHORIZATION_TOKEN, + projectKey: 'QE' + }]], + use: { + screenshot: 'only-on-failure' + }, + projects: [ + { + name: 'Chrome', + use: { + browserName: 'chromium', + channel: 'chrome', + }, + }, + { + name: 'Safari', + use: { + browserName: 'webkit', + viewport: { width: 1200, height: 750 }, + } + }, + { + name: 'Firefox', + use: { + browserName: 'firefox', + viewport: { width: 800, height: 600 }, + } + }, + ], +}; +export default config; \ No newline at end of file diff --git a/playwright.config.ts b/playwright.config.ts index 7c646e7..1ac2051 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -2,7 +2,7 @@ import { PlaywrightTestConfig } from '@playwright/test'; const config: PlaywrightTestConfig = { - reporter: [['list'], ['./src/zephyr.reporter.ts', { + reporter: [['list'], ['./src', { host: '', runName: '', user: '', diff --git a/src/cloud.ts b/src/cloud.ts index a7e7384..d238e82 100644 --- a/src/cloud.ts +++ b/src/cloud.ts @@ -13,7 +13,8 @@ export default class ZephyrReporter implements Reporter { private zephyrService!: ZephyrService; private testResults: ZephyrTestResult[] = []; private projectKey!: string; - private testCaseKeyPattern = /\[(.*?)\]/; + private readonly testCaseKeyPattern = /\[(.*?)\]/; + private readonly ansiRegex = /\x1b\[[0-9;]*m/g; private options: ZephyrOptions; constructor(options: ZephyrOptions) { @@ -27,27 +28,39 @@ export default class ZephyrReporter implements Reporter { } onTestEnd(test: TestCase, result: TestResult) { + let testCaseIds: string[] | undefined; if (test.title.match(this.testCaseKeyPattern) && test.title.match(this.testCaseKeyPattern)!.length > 1) { const [, testCaseId] = test.title.match(this.testCaseKeyPattern)!; - const testCaseKey = `${this.projectKey}-${testCaseId}`; - const status = convertStatus(result.status); - const comment = result.error - ? `❌ Error Message:
${result.error?.message?.replaceAll( - '\n', - '
', - )}


🧱 Stack Trace:
${result.error?.stack?.replaceAll( - '\n', - '
', - )}
` - : undefined; + if(testCaseId) testCaseIds = [testCaseId]; + } else if(test.annotations.some(annotation => annotation.type === 'zephyrTestId')){ + testCaseIds = test.annotations + .filter(annotation => annotation.type === 'zephyrTestId') + .map(annotation => annotation.description || ''); + } + + if(testCaseIds) { + for (const testCaseId of testCaseIds) { + + const testCaseKey = `${this.projectKey}-${testCaseId}`; + const status = convertStatus(result.status); + const comment = result.error + ? `❌ Error Message:
${this.stripAnsiCodes(result.error?.message)?.replaceAll( + '\n', + '
', + )}


🧱 Stack Trace:
${this.stripAnsiCodes(result.error?.stack)?.replaceAll( + '\n', + '
', + )}
` + : undefined; - this.testResults.push({ - result: status, - testCase: { - key: testCaseKey, - comment, - }, - }); + this.testResults.push({ + result: status, + testCase: { + key: testCaseKey, + comment, + }, + }); + } } } @@ -64,4 +77,9 @@ export default class ZephyrReporter implements Reporter { console.log(gray(`[zephyr reporter]: There's no Zephyr test case id in this spec file`)); } } + + private stripAnsiCodes(str: string | undefined): string | undefined { + return str?.replaceAll(this.ansiRegex, ''); + } + } diff --git a/src/index.ts b/src/index.ts index cfb2987..5931722 100644 --- a/src/index.ts +++ b/src/index.ts @@ -32,17 +32,29 @@ class ZephyrReporter implements Reporter { } onTestEnd(test: TestCase, result: TestResult) { + let testCaseIds: string[] | undefined; if (test.title.match(this.testCaseKeyPattern) && test.title.match(this.testCaseKeyPattern)!.length > 1) { - const [, projectName] = test.titlePath(); const [, testCaseId] = test.title.match(this.testCaseKeyPattern)!; - const testCaseKey = `${this.projectKey}-${testCaseId}`; - const status = convertPwStatusToZephyr(result.status); - this.testResults.push({ - testCaseKey, - status, - environment: this.environment ?? projectName ?? 'Playwright', - executionDate: new Date().toISOString(), - }); + if(testCaseId) testCaseIds = [testCaseId]; + } else if(test.annotations.some(annotation => annotation.type === 'zephyrTestId')){ + testCaseIds = test.annotations + .filter(annotation => annotation.type === 'zephyrTestId') + .map(annotation => annotation.description || ''); + } + + if(testCaseIds) { + for (const testCaseId of testCaseIds) { + const [, projectName] = test.titlePath(); + const testCaseKey = `${this.projectKey}-${testCaseId}`; + const status = convertPwStatusToZephyr(result.status); + + this.testResults.push({ + testCaseKey, + status, + environment: this.environment ?? projectName ?? 'Playwright', + executionDate: new Date().toISOString(), + }); + } } } diff --git a/src/server.ts b/src/server.ts index a0ae7c0..c3e1316 100644 --- a/src/server.ts +++ b/src/server.ts @@ -32,18 +32,29 @@ class ZephyrReporter implements Reporter { } onTestEnd(test: TestCase, result: TestResult) { + let testCaseIds: string[] | undefined; if (test.title.match(this.testCaseKeyPattern) && test.title.match(this.testCaseKeyPattern)!.length > 1) { - const [, projectName] = test.titlePath(); const [, testCaseId] = test.title.match(this.testCaseKeyPattern)!; - const testCaseKey = `${this.projectKey}-${testCaseId}`; - const status = convertPwStatusToZephyr(result.status); - - this.testResults.push({ - testCaseKey, - status, - environment: this.environment ?? projectName ?? 'Playwright Test', - executionDate: new Date().toISOString(), - }); + if(testCaseId) testCaseIds = [testCaseId]; + } else if(test.annotations.some(annotation => annotation.type === 'zephyrTestId')){ + testCaseIds = test.annotations + .filter(annotation => annotation.type === 'zephyrTestId') + .map(annotation => annotation.description || ''); + } + + if(testCaseIds) { + for (const testCaseId of testCaseIds) { + const [, projectName] = test.titlePath(); + const testCaseKey = `${this.projectKey}-${testCaseId}`; + const status = convertPwStatusToZephyr(result.status); + + this.testResults.push({ + testCaseKey, + status, + environment: this.environment ?? projectName ?? 'Playwright Test', + executionDate: new Date().toISOString(), + }); + } } } diff --git a/tests/annotated.test.ts b/tests/annotated.test.ts new file mode 100644 index 0000000..f3f94de --- /dev/null +++ b/tests/annotated.test.ts @@ -0,0 +1,29 @@ +import { test, expect } from "@playwright/test"; + +test('annotated test', { + annotation: [{ type: 'zephyrTestId', description: 'T1' }] +}, async ({page}) => { + await page.goto('https://playwright.dev/'); + const title = page.locator('.navbar__inner .navbar__title'); + await expect(title).toHaveText('Playwright'); +}) + +test('Can annotate tests with multiple Zephyr IDs', { + annotation: [ + { type: 'zephyrTestId', description: 'T2' }, + { type: 'zephyrTestId', description: 'T3' } + ] +}, async ({page}) => { + await page.goto('https://playwright.dev/'); + const title = page.locator('.navbar__inner .navbar__title'); + await expect(title).toHaveText('Playwright'); + } +) + +test.fail('Failed test with Zephyr ID', { + annotation: [{ type: 'zephyrTestId', description: 'T1' }] +}, async ({page}) => { + await page.goto('https://playwright.dev/'); + const title = page.locator('.navbar__inner .navbar__title'); + await expect(title).toHaveText('Playright'); // Intentional typo to cause failure +}) \ No newline at end of file From c82794b256c751166670a8b2ee83fde024475b4f Mon Sep 17 00:00:00 2001 From: lridge Date: Thu, 18 Dec 2025 15:15:42 -0500 Subject: [PATCH 2/2] update readme --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 7956292..2cf4cf0 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,19 @@ test('[J79] basic test', async ({ page }) => { }); ``` +You can also use annotations to set the test id, note this supports multiple test ids. +```typescript +test('An annotated test', { + annotation: [ + { type: 'zephyrTestId', description: 'T2' }, + { type: 'zephyrTestId', description: 'T3' } + ] +}, async ({page}){ + await page.goto('https://playwright.dev/'); + const title = page.locator('.navbar__inner .navbar__title'); + await expect(title).toHaveText('Playwright'); +}); + Then run your tests with `npx playwright test` command and you'll see the result in console: ```sh