@msalaam/xray-qe-toolkit 1.5.0 → 1.6.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/README.md CHANGED
@@ -255,6 +255,7 @@ npx xqt exec --env IOP-QA --plan APIEE-1234 --tests TC-001,TC-002
255
255
  | `--plan <key>` | Test Plan to link to (overrides `.xrayrc`) |
256
256
  | `--tests <ids>` | Comma-separated testIds or JIRA keys (default: all mapped) |
257
257
  | `--summary <text>` | Custom execution title |
258
+ | `--fix-version <ver>` | JIRA Fix Version to stamp on the execution (e.g. `2.5.0`) |
258
259
  | `--quiet` | Print only the execution key |
259
260
 
260
261
  > `xqt import` creates an execution automatically — this command is only needed when pre-selecting a specific subset of tests.
@@ -281,12 +282,12 @@ npx xqt import --file test-results/results.json --exec APIEE-9876
281
282
  # JUnit XML
282
283
  npx xqt import --file test-results/results.xml --env IOP-PROD
283
284
 
284
- # Full options
285
+ # Full options — with Fix Version and git SHA
285
286
  npx xqt import \
286
287
  --file test-results/results.json \
287
288
  --env IOP-QA \
288
289
  --plan APIEE-1234 \
289
- --version "2024.12" \
290
+ --fix-version "2.5.0" \
290
291
  --revision "a1b2c3d4"
291
292
  ```
292
293
 
@@ -296,7 +297,8 @@ npx xqt import \
296
297
  | `--env <label>` | Environment label (default: `defaultEnvironment` from `.xrayrc`) |
297
298
  | `--plan <key>` | Test Plan key (overrides `.xrayrc`; used when no `--exec`) |
298
299
  | `--exec <key>` | Import INTO an existing execution (from `xqt exec`) |
299
- | `--version <ver>` | Fix version / release label |
300
+ | `--fix-version <ver>` | JIRA Fix Version name to stamp on the execution (e.g. `2.5.0`) |
301
+ | `--version <ver>` | Xray version label (free-form, separate from Fix Version) |
300
302
  | `--revision <rev>` | Build number or git SHA |
301
303
  | `--summary <text>` | Custom execution summary |
302
304
 
@@ -394,23 +396,53 @@ JIRA_EMAIL=your.email@company.com
394
396
 
395
397
  ```json
396
398
  {
397
- "testsPath": "tests.json",
398
- "mappingPath": "xray-mapping.json",
399
- "testPlanKey": "APIEE-1234",
400
- "defaultEnvironment": "IOP-DEV",
401
- "environments": ["IOP-DEV", "IOP-QA", "IOP-PROD"],
402
- "folderRoot": "/MyService",
403
- "xrayRegion": "us"
399
+ "testsPath": "tests.json",
400
+ "mappingPath": "xray-mapping.json",
401
+ "testPlanKey": "APIEE-1234",
402
+ "defaultEnvironment": "IOP-DEV",
403
+ "environments": ["IOP-DEV", "IOP-QA", "IOP-PROD"],
404
+ "folderRoot": "/MyService",
405
+ "xrayRegion": "us",
406
+ "bulkImportThreshold": 50,
407
+ "statusMapping": {
408
+ "interrupted": "ABORTED",
409
+ "skipped": "TODO"
410
+ }
404
411
  }
405
412
  ```
406
413
 
407
414
  | Field | Description |
408
415
  |---|---|
409
- | `testPlanKey` | Default Test Plan key for push-tests and import-results |
416
+ | `testPlanKey` | Default Test Plan key for `push` and `import` |
410
417
  | `defaultEnvironment` | Fallback environment when `--env` is not provided |
411
418
  | `environments` | Allowed environment labels |
412
419
  | `folderRoot` | Base folder path in Xray Test Repository |
413
- | `xrayRegion` | `us` (default) or `eu` |
420
+ | `xrayRegion` | `us` (default), `eu`, or `au` |
421
+ | `bulkImportThreshold` | Min new-test count to use the Xray bulk REST API (default: `50`) — faster for large suites |
422
+ | `statusMapping` | Override default Playwright → Xray status mapping (see below) |
423
+
424
+ #### Playwright status mapping
425
+
426
+ By default the converter maps:
427
+
428
+ | Playwright status | Xray status |
429
+ |---|---|
430
+ | `passed` | `PASSED` |
431
+ | `failed` | `FAILED` |
432
+ | `timedOut` | `FAILED` |
433
+ | `interrupted` | `ABORTED` |
434
+ | `skipped` | `TODO` |
435
+
436
+ Override any value in `.xrayrc`:
437
+
438
+ ```json
439
+ {
440
+ "statusMapping": {
441
+ "skipped": "TODO",
442
+ "interrupted": "FAILED"
443
+ }
444
+ }
445
+ ```
414
446
 
415
447
  ---
416
448
 
@@ -432,6 +464,7 @@ JIRA_EMAIL=your.email@company.com
432
464
  "tags": ["smoke", "regression"],
433
465
  "folder": "/MyService/HealthCheck/Validation",
434
466
  "testSet": "Health Check",
467
+ "preconditions": ["APIEE-50"],
435
468
  "requirementKeys": [],
436
469
  "xray": {
437
470
  "summary": "Service returns 200 OK when healthy",
@@ -455,7 +488,8 @@ JIRA_EMAIL=your.email@company.com
455
488
  | `skip` | boolean | — | `true` to exclude from `push-tests` |
456
489
  | `tags` | string[] | — | QE tags for categorisation and filtering. Allowed: `regression`, `smoke`, `edge`, `critical`, `integration`, `e2e`, `security`, `performance`, `contract`, `functional`, `negative`, `positive`, `boundary`, `acceptance`, `sanity`, `data-driven`, `exploratory`, `accessibility` |
457
490
  | `folder` | string | — | Xray repository folder path — must start with `/` |
458
- | `testSet` | string | — | Test Set name in Jira — tests are grouped into Test Sets by this value |
491
+ | `testSet` | string **or** string[] | — | Test Set name(s) in Jira — a single string or an array to assign the test to multiple Test Sets |
492
+ | `preconditions` | string[] | — | JIRA keys of Xray Precondition issues to link to this test (e.g. `["APIEE-50"]`) |
459
493
  | `requirementKeys` | string[] | — | JIRA keys this test covers (creates traceability links) |
460
494
  | `xray.summary` | string | Yes | Test case title (JIRA issue summary) |
461
495
  | `xray.description` | string | — | Detailed description |
@@ -472,17 +506,17 @@ JIRA_EMAIL=your.email@company.com
472
506
 
473
507
  Tests live in **Test Sets** in Jira. Each Test Set groups related tests by feature or area — they persist across every sprint.
474
508
 
475
- Set the `testSet` field on each test in `tests.json`:
509
+ Set the `testSet` field on each test in `tests.json` — use a string or an array of strings to assign the test to multiple sets:
476
510
 
477
511
  ```json
478
512
  { "test_id": "TC-001", "testSet": "Client Lookup", ... }
479
513
  { "test_id": "TC-002", "testSet": "Client Lookup", ... }
480
- { "test_id": "TC-003", "testSet": "Health Check", ... }
514
+ { "test_id": "TC-003", "testSet": ["Health Check", "Smoke"], ... }
481
515
  ```
482
516
 
483
517
  When you run `xqt push`:
484
518
  1. Tests are created/updated in Xray
485
- 2. For each unique `testSet` value, the Test Set JIRA issue is **created automatically** if it doesn't exist yet
519
+ 2. For each unique `testSet` value, the Test Set JIRA issue is **created automatically** if it doesn't exist yet (safe to run on a fresh clone — existing sets are found by name, not duplicated)
486
520
  3. Tests are added to their Test Set (idempotent — Xray ignores tests already in the set)
487
521
  4. Test Set → JIRA key mappings (e.g. `"Client Lookup" → APIEE-99`) are stored in `xray-mapping.json` under `_testSets`
488
522
 
@@ -509,6 +543,8 @@ A Test Plan scopes testing work for a specific sprint or release.
509
543
  - Key stored in `.xrayrc` (`testPlanKey`)
510
544
  - `xqt import` links executions to the active plan
511
545
 
546
+ `xqt push` performs a **bi-directional sync** with the Test Plan: new tests are added **and** tests removed from `tests.json` are removed from the plan. Use `--plan <key>` to override the key.
547
+
512
548
  ### Test Executions (ephemeral per run)
513
549
 
514
550
  A Test Execution represents a single CI run.
@@ -582,7 +618,7 @@ reporter: [
582
618
 
583
619
  ### Test steps → Xray step results
584
620
 
585
- Steps created with `test.step()` are mapped to Xray step results automatically:
621
+ Steps created with `test.step()` are mapped to Xray step results automatically. Screenshots captured on a **failing step** are attached directly to that step's evidence in Xray — giving per-step failure screenshots in the Xray UI. Traces and other attachments are attached at the test level.
586
622
 
587
623
  ```typescript
588
624
  test('Create and retrieve user', async ({ request }) => {
@@ -636,7 +672,9 @@ Generate a template: `npx xqt pipeline`
636
672
  - script: |
637
673
  npx xqt import \
638
674
  --file test-results/results.json \
639
- --env $(XQT_ENV)
675
+ --env $(XQT_ENV) \
676
+ --fix-version $(BUILD_BUILDNUMBER) \
677
+ --revision $(Build.SourceVersion)
640
678
  displayName: Import results to Xray
641
679
  env:
642
680
  XRAY_ID: $(XRAY_ID)
@@ -658,6 +696,7 @@ Generate a template: `npx xqt pipeline`
658
696
  | `JIRA_API_TOKEN` | JIRA API token |
659
697
  | `JIRA_EMAIL` | JIRA user email |
660
698
  | `XQT_ENV` | Environment label (`IOP-DEV` / `IOP-QA` / `IOP-PROD`) |
699
+ | `XQT_BULK_THRESHOLD` | Override bulk-import threshold (default: `50`) |
661
700
 
662
701
  ### Pre-create execution in pipeline (Mode B)
663
702
 
@@ -701,7 +740,7 @@ const { key } = await createTestExecution(cfg, token, {
701
740
  });
702
741
  ```
703
742
 
704
- Key exports: `authenticate`, `createIssue`, `updateIssue`, `getIssue`, `getTest`, `getTests`, `getTestPlan`, `createTestPlan`, `addTestsToTestPlan`, `addTestsToTestExecution`, `createTestExecution`, `importResultsMultipart`, `importTestsBulk`, `syncTestPlan`, `syncFolders`, `buildAndPush`, `loadMapping`, `saveMapping`, `loadConfig`, `validateConfig`.
743
+ Key exports: `authenticate`, `createIssue`, `updateIssue`, `getIssue`, `getTest`, `getTests`, `getTestPlan`, `createTestPlan`, `addTestsToTestPlan`, `removeTestsFromTestPlan`, `addTestsToTestExecution`, `createTestExecution`, `importResultsMultipart`, `importTestsBulk`, `waitForImportJob`, `searchIssues`, `getTestSets`, `addPreconditionsToTest`, `syncTestPlan`, `syncFolders`, `buildAndPush`, `loadMapping`, `saveMapping`, `loadConfig`, `validateConfig`.
705
744
 
706
745
  ---
707
746
 
package/bin/cli.js CHANGED
@@ -100,6 +100,7 @@ program
100
100
  .option("--tests <ids>", "Comma-separated testIds or JIRA keys to include (default: all mapped tests)")
101
101
  .option("--summary <text>", "Custom Test Execution summary")
102
102
  .option("--description <text>", "Custom description")
103
+ .option("--fix-version <version>", "JIRA Fix Version to stamp on the execution (e.g. '2.5.0')")
103
104
  .option("--quiet", "Print ONLY the execution key (for CI variable capture)")
104
105
  .action(async (opts, cmd) => {
105
106
  const mod = await import("../commands/createExecution.js");
@@ -117,7 +118,8 @@ program
117
118
  .option("--plan <key>", "Test Plan key to associate execution with (overrides .xrayrc)")
118
119
  .option("--exec <key>", "Import INTO an existing Test Execution key (from xqt exec)")
119
120
  .option("--version <version>", "Fix version / release version")
120
- .option("--revision <revision>", "Revision / build number")
121
+ .option("--revision <revision>", "Revision / build number (e.g. git SHA)")
122
+ .option("--fix-version <version>", "JIRA Fix Version name to stamp on the Test Execution (e.g. '2.5.0')")
121
123
  .option("--summary <text>", "Custom Test Execution summary")
122
124
  .option("--description <text>", "Custom Test Execution description")
123
125
  .action(async (opts, cmd) => {
@@ -113,6 +113,7 @@ export default async function createExecution(opts = {}) {
113
113
  environments,
114
114
  testIssueIds,
115
115
  testPlanKey: planKey || undefined,
116
+ fixVersion: opts.fixVersion || undefined,
116
117
  });
117
118
 
118
119
  if (quiet) {
@@ -88,6 +88,7 @@ export default async function importResults(opts = {}) {
88
88
  ...(!execKey && planKey && { testPlanKey: planKey }),
89
89
  ...(opts.version && { version: opts.version }),
90
90
  ...(opts.revision && { revision: opts.revision }),
91
+ ...(opts.fixVersion && { fixVersion: opts.fixVersion }),
91
92
  };
92
93
 
93
94
  let result;
@@ -101,6 +102,7 @@ export default async function importResults(opts = {}) {
101
102
 
102
103
  xrayJson = convertPlaywrightToXray(playwrightJson, {
103
104
  projectKey: cfg.jiraProjectKey,
105
+ statusMapping: cfg.statusMapping || undefined,
104
106
  infoOverrides: infoBase,
105
107
  });
106
108
 
package/commands/init.js CHANGED
@@ -200,6 +200,8 @@ export default async function init(opts = {}) {
200
200
  environments: ["IOP-DEV", "IOP-QA", "IOP-PROD"],
201
201
  folderRoot: `/${serviceName}`,
202
202
  xrayRegion: "us",
203
+ bulkImportThreshold: 50,
204
+ // statusMapping: { interrupted: "ABORTED", skipped: "TODO" }
203
205
  };
204
206
  fs.writeFileSync(xrayrcPath, JSON.stringify(xrayrc, null, 2));
205
207
  logger.success(".xrayrc created — Project config");
@@ -19,6 +19,7 @@ import fs from "node:fs";
19
19
  import { createRequire } from "node:module";
20
20
  import logger, { setVerbose } from "../lib/logger.js";
21
21
  import { loadConfig, validateConfig } from "../lib/config.js";
22
+ import { readJsonFile } from "../lib/jsonFile.js";
22
23
  import { authenticate, getIssue, createTestExecution } from "../lib/xrayClient.js";
23
24
  import {
24
25
  buildAndPush,
@@ -44,7 +45,7 @@ export default async function pushTests(opts = {}) {
44
45
  process.exit(1);
45
46
  }
46
47
 
47
- const testsConfig = JSON.parse(fs.readFileSync(cfg.testsPath, "utf8"));
48
+ const testsConfig = readJsonFile(cfg.testsPath);
48
49
  const allTests = testsConfig.tests || [];
49
50
  const tests = allTests.filter((t) => !t.skip);
50
51
  const skippedCount = allTests.length - tests.length;
@@ -61,9 +62,7 @@ export default async function pushTests(opts = {}) {
61
62
  try {
62
63
  const require = createRequire(import.meta.url);
63
64
  const Ajv = require("ajv");
64
- const schema = JSON.parse(
65
- fs.readFileSync(new URL("../schema/tests.schema.json", import.meta.url), "utf8")
66
- );
65
+ const schema = readJsonFile(new URL("../schema/tests.schema.json", import.meta.url));
67
66
  const ajv = new Ajv({ allErrors: true });
68
67
  const validate = ajv.compile(schema);
69
68
  if (!validate(testsConfig)) {
@@ -103,11 +102,11 @@ export default async function pushTests(opts = {}) {
103
102
  logger.save(`Mapping saved to ${cfg.mappingPath}`);
104
103
 
105
104
  // ── Sync Test Sets ──────────────────────────────────────────────────
106
- const testsWithSets = tests.filter((t) => t.testSet);
105
+ const testsWithSets = tests.filter((t) => t.testSet && (Array.isArray(t.testSet) ? t.testSet.length > 0 : true));
107
106
 
108
107
  if (testsWithSets.length > 0) {
109
108
  logger.blank();
110
- const uniqueSets = [...new Set(testsWithSets.map((t) => t.testSet))];
109
+ const uniqueSets = [...new Set(testsWithSets.flatMap((t) => Array.isArray(t.testSet) ? t.testSet : [t.testSet]))];
111
110
  logger.send(`Syncing ${uniqueSets.length} Test Set(s) (${testsWithSets.length} test(s))...`);
112
111
  try {
113
112
  const setResult = await syncTestSets(cfg, xrayToken, tests, mapping, cfg.mappingPath);
package/lib/config.js CHANGED
@@ -97,6 +97,13 @@ export function loadConfig(opts = {}) {
97
97
  environments: rcConfig.environments || defaultEnvironments,
98
98
  folderRoot: rcConfig.folderRoot || null,
99
99
 
100
+ // Playwright status mapping (overrides defaults per-project)
101
+ // e.g. { "interrupted": "ABORTED", "skipped": "TODO" }
102
+ statusMapping: rcConfig.statusMapping || null,
103
+
104
+ // Bulk import threshold — use Xray REST bulk API when creating this many new tests
105
+ bulkImportThreshold: Number(process.env.XQT_BULK_THRESHOLD ?? rcConfig.bulkImportThreshold ?? 50),
106
+
100
107
  // Paths (resolved relative to consuming project)
101
108
  testsPath: path.resolve(cwd, rcConfig.testsPath || "tests.json"),
102
109
  mappingPath: path.resolve(cwd, rcConfig.mappingPath || "xray-mapping.json"),
@@ -0,0 +1,12 @@
1
+ import fs from "node:fs";
2
+
3
+ /**
4
+ * Parse a JSON file and tolerate UTF-8 BOM at file start.
5
+ * @param {string | URL} filePath
6
+ * @returns {any}
7
+ */
8
+ export function readJsonFile(filePath) {
9
+ const raw = fs.readFileSync(filePath, "utf8");
10
+ const content = raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw;
11
+ return JSON.parse(content);
12
+ }
@@ -20,8 +20,10 @@ import path from "node:path";
20
20
  * @param {object} playwrightJson - Playwright JSON reporter output
21
21
  * @param {object} options - Conversion options
22
22
  * @param {string} [options.projectKey] - JIRA project key (for new test creation)
23
+ * @param {object} [options.statusMapping] - Override default Playwright→Xray status map
24
+ * e.g. { interrupted: "ABORTED", skipped: "TODO" }
23
25
  * @param {object} [options.infoOverrides] - Merged into the Xray `info` object:
24
- * testPlanKey, testEnvironments, version, revision, summary, description
26
+ * testPlanKey, testEnvironments, version, revision, fixVersion, summary, description
25
27
  * @returns {object} Xray JSON format ready for import
26
28
  */
27
29
  export function convertPlaywrightToXray(playwrightJson, options = {}) {
@@ -110,7 +112,7 @@ function convertTest(test, spec, suite, options) {
110
112
  const result = test.results[test.results.length - 1];
111
113
 
112
114
  const testKey = extractTestKey(spec.title, test.annotations);
113
- const status = mapStatus(result.status);
115
+ const status = mapStatus(result.status, options.statusMapping);
114
116
 
115
117
  const xrayTest = {
116
118
  status,
@@ -145,14 +147,16 @@ function convertTest(test, spec, suite, options) {
145
147
  }
146
148
 
147
149
  // ── Map Playwright steps to Xray step results ─────────────────────────────
148
- if (result.steps && result.steps.length > 0) {
149
- xrayTest.steps = result.steps
150
- .filter((s) => s.category === "test.step" || s.category === "hook")
151
- .map((s) => ({
152
- status: s.error ? "FAILED" : "PASSED",
153
- comment: s.title || s.category,
154
- ...(s.duration && { duration: Math.round(s.duration) }),
155
- }));
150
+ const relevantSteps = result.steps
151
+ ? result.steps.filter((s) => s.category === "test.step" || s.category === "hook")
152
+ : [];
153
+
154
+ if (relevantSteps.length > 0) {
155
+ xrayTest.steps = relevantSteps.map((s) => ({
156
+ status: s.error ? "FAILED" : "PASSED",
157
+ comment: s.title || s.category,
158
+ ...(s.duration && { duration: Math.round(s.duration) }),
159
+ }));
156
160
  }
157
161
 
158
162
  // ── Error details ──────────────────────────────────────────────────────────
@@ -162,35 +166,34 @@ function convertTest(test, spec, suite, options) {
162
166
  xrayTest.comment += `\n\nError: ${msg}${stack}`;
163
167
  }
164
168
 
165
- // ── Attachments / Evidence ────────────────────────────────────────────────
169
+ // ── Attachments / Evidence (with per-step attachment for screenshots) ──────
166
170
  if (result.attachments && result.attachments.length > 0) {
167
- xrayTest.evidence = [];
168
- for (const attachment of result.attachments) {
169
- let data = null;
170
-
171
- if (attachment.body) {
172
- // In-memory body (Buffer from Playwright)
173
- data = Buffer.isBuffer(attachment.body)
174
- ? attachment.body.toString("base64")
175
- : Buffer.from(attachment.body).toString("base64");
176
- } else if (attachment.path && fs.existsSync(attachment.path)) {
177
- // File on disk
178
- data = fs.readFileSync(attachment.path).toString("base64");
179
- }
171
+ const screenshots = result.attachments.filter(
172
+ (a) => a.contentType?.startsWith("image/") || a.name?.match(/screenshot/i)
173
+ );
174
+ const traces = result.attachments.filter((a) => a.name?.match(/trace/i));
175
+ const other = result.attachments.filter(
176
+ (a) => !screenshots.includes(a) && !traces.includes(a)
177
+ );
180
178
 
181
- if (data) {
182
- xrayTest.evidence.push({
183
- filename: attachment.name || path.basename(attachment.path || "attachment"),
184
- contentType: attachment.contentType || "application/octet-stream",
185
- data,
186
- });
187
- }
179
+ // Find the last failed step index for per-step evidence attachment
180
+ const lastFailedStepIdx =
181
+ relevantSteps.reduce((acc, s, i) => (s.error ? i : acc), -1);
182
+
183
+ // Attach screenshots to the last failed step (per-step evidence)
184
+ if (xrayTest.steps && lastFailedStepIdx >= 0 && screenshots.length > 0) {
185
+ xrayTest.steps[lastFailedStepIdx].evidences = screenshots
186
+ .map((a) => buildEvidenceItem(a))
187
+ .filter(Boolean);
188
188
  }
189
189
 
190
- if (xrayTest.evidence.length > 0) {
191
- xrayTest.comment += `\n\nAttachments: ${result.attachments.map((a) => a.name).join(", ")}`;
192
- } else {
193
- delete xrayTest.evidence;
190
+ // Everything else (traces, other) stays at test level
191
+ const testLevelAttachments = [...(lastFailedStepIdx >= 0 ? [] : screenshots), ...traces, ...other];
192
+ const testLevelEvidence = testLevelAttachments.map((a) => buildEvidenceItem(a)).filter(Boolean);
193
+
194
+ if (testLevelEvidence.length > 0) {
195
+ xrayTest.evidence = testLevelEvidence;
196
+ xrayTest.comment += `\n\nAttachments: ${testLevelAttachments.map((a) => a.name).join(", ")}`;
194
197
  }
195
198
  }
196
199
 
@@ -209,16 +212,36 @@ function extractTestKey(title, annotations) {
209
212
  return match ? match[1] : null;
210
213
  }
211
214
 
212
- function mapStatus(playwrightStatus) {
213
- return (
214
- {
215
- passed: "PASSED",
216
- failed: "FAILED",
217
- timedOut: "FAILED",
218
- interrupted: "ABORTED",
219
- skipped: "TODO",
220
- }[playwrightStatus] || "TODO"
221
- );
215
+ function mapStatus(playwrightStatus, customMapping) {
216
+ const defaults = {
217
+ passed: "PASSED",
218
+ failed: "FAILED",
219
+ timedOut: "FAILED",
220
+ interrupted: "ABORTED",
221
+ skipped: "TODO",
222
+ };
223
+ const map = customMapping ? { ...defaults, ...customMapping } : defaults;
224
+ return map[playwrightStatus] || "TODO";
225
+ }
226
+
227
+ function buildEvidenceItem(attachment) {
228
+ let data = null;
229
+
230
+ if (attachment.body) {
231
+ data = Buffer.isBuffer(attachment.body)
232
+ ? attachment.body.toString("base64")
233
+ : Buffer.from(attachment.body).toString("base64");
234
+ } else if (attachment.path && fs.existsSync(attachment.path)) {
235
+ data = fs.readFileSync(attachment.path).toString("base64");
236
+ }
237
+
238
+ if (!data) return null;
239
+
240
+ return {
241
+ filename: attachment.name || path.basename(attachment.path || "attachment"),
242
+ contentType: attachment.contentType || "application/octet-stream",
243
+ data,
244
+ };
222
245
  }
223
246
 
224
247
  function buildComment(result, spec, suite) {