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/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
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