diff --git a/.github/workflows/semantic-release.yml b/.github/workflows/semantic-release.yml index 6d97554c..a97b42a0 100644 --- a/.github/workflows/semantic-release.yml +++ b/.github/workflows/semantic-release.yml @@ -3,9 +3,10 @@ name: Semantic Release on: push: branches: [main] - # Skip running on release commits (already created) - paths-ignore: - - 'testplanit/CHANGELOG.md' + # Only run when the main application code changes — not for docs, packages, or CLI + paths: + - 'testplanit/**' + - '!testplanit/CHANGELOG.md' permissions: contents: write diff --git a/.gitignore b/.gitignore index 1b94d50e..062f84a4 100644 --- a/.gitignore +++ b/.gitignore @@ -7,8 +7,7 @@ .yarn/install-state.gz # testing -/coverage -testplanit/coverage/ +**/coverage/ # next.js **/.next/ diff --git a/docs/blog/2025-12-15-webdriverio-reporter.md b/docs/blog/2025-12-15-webdriverio-reporter.md index 5d994545..f3d0ce76 100644 --- a/docs/blog/2025-12-15-webdriverio-reporter.md +++ b/docs/blog/2025-12-15-webdriverio-reporter.md @@ -145,7 +145,7 @@ runName: `Build #${process.env.GITHUB_RUN_NUMBER} - {browser} - {date}`, ## Documentation -For complete configuration options, examples, and advanced features like custom case ID patterns, milestone/configuration associations, and retry handling, see the [WebdriverIO Reporter documentation](/docs/sdk/webdriverio-reporter). +For complete configuration options, examples, and advanced features like custom case ID patterns, milestone/configuration associations, and retry handling, see the [WebdriverIO Reporter documentation](/docs/sdk/wdio-overview). ## Get Started diff --git a/docs/docs/faq.md b/docs/docs/faq.md index 4bdb17f1..272c9945 100644 --- a/docs/docs/faq.md +++ b/docs/docs/faq.md @@ -60,7 +60,7 @@ Yes, TestPlanIt supports importing test cases from CSV files. For specific tools TestPlanIt doesn't run automated tests directly, but it integrates with your existing automation frameworks. You can push automated test results to TestPlanIt using: - The [TestPlanIt SDK](/docs/sdk/) for programmatic access -- The [WebdriverIO reporter](/docs/sdk/webdriverio-reporter) for WebdriverIO integration +- The [WebdriverIO reporter](/docs/sdk/wdio-overview) for WebdriverIO integration - The REST API for custom integrations ### Can I integrate with Jira/GitHub/Azure DevOps? diff --git a/docs/docs/sdk/api-client.md b/docs/docs/sdk/api-client.md index 7928bcac..b1f61b97 100644 --- a/docs/docs/sdk/api-client.md +++ b/docs/docs/sdk/api-client.md @@ -435,6 +435,6 @@ reportTestResults(); ## Related Resources -- [WebdriverIO Reporter](./webdriverio-reporter.md) - Automatic reporting for WebdriverIO tests +- [WebdriverIO Reporter](./wdio-overview.md) - Automatic reporting for WebdriverIO tests - [API Tokens](../api-tokens.md) - Managing API tokens - [API Reference](../api-reference.md) - Full REST API documentation diff --git a/docs/docs/sdk/index.md b/docs/docs/sdk/index.md index b904d153..e0f02268 100644 --- a/docs/docs/sdk/index.md +++ b/docs/docs/sdk/index.md @@ -13,7 +13,7 @@ TestPlanIt provides official npm packages to integrate with your test automation | Package | Description | npm | |---------|-------------|-----| | [`@testplanit/api`](./api-client.md) | Official JavaScript/TypeScript API client | [![npm](https://img.shields.io/npm/v/@testplanit/api)](https://www.npmjs.com/package/@testplanit/api) | -| [`@testplanit/wdio-reporter`](./webdriverio-reporter.md) | WebdriverIO reporter | [![npm](https://img.shields.io/npm/v/@testplanit/wdio-reporter)](https://www.npmjs.com/package/@testplanit/wdio-reporter) | +| [`@testplanit/wdio-reporter`](./wdio-overview.md) | WebdriverIO reporter | [![npm](https://img.shields.io/npm/v/@testplanit/wdio-reporter)](https://www.npmjs.com/package/@testplanit/wdio-reporter) | ## Architecture diff --git a/docs/docs/sdk/wdio-ci-cd.md b/docs/docs/sdk/wdio-ci-cd.md new file mode 100644 index 00000000..42c55dd4 --- /dev/null +++ b/docs/docs/sdk/wdio-ci-cd.md @@ -0,0 +1,261 @@ +--- +title: CI/CD & Advanced Usage +--- + +# CI/CD & Advanced Usage + +## CI/CD Integration + +### GitHub Actions + +```yaml +name: E2E Tests + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm ci + + - name: Run E2E tests + env: + TESTPLANIT_API_TOKEN: ${{ secrets.TESTPLANIT_API_TOKEN }} + run: npx wdio run wdio.conf.js +``` + +### GitLab CI + +```yaml +e2e-tests: + image: node:20 + script: + - npm ci + - npx wdio run wdio.conf.js + variables: + TESTPLANIT_API_TOKEN: $TESTPLANIT_API_TOKEN +``` + +### Jenkins + +```groovy +pipeline { + agent any + environment { + TESTPLANIT_API_TOKEN = credentials('testplanit-api-token') + } + stages { + stage('E2E Tests') { + steps { + sh 'npm ci' + sh 'npx wdio run wdio.conf.js' + } + } + } +} +``` + +### Dynamic Run Names with Build Info + +Include CI build information in your test run names: + +```javascript +// wdio.conf.js +const buildNumber = process.env.GITHUB_RUN_NUMBER + || process.env.CI_PIPELINE_ID + || process.env.BUILD_NUMBER + || 'local'; + +const branch = process.env.GITHUB_REF_NAME + || process.env.CI_COMMIT_REF_NAME + || process.env.GIT_BRANCH + || 'unknown'; + +export const config = { + reporters: [ + ['@testplanit/wdio-reporter', { + domain: 'https://testplanit.example.com', + apiToken: process.env.TESTPLANIT_API_TOKEN, + projectId: 1, + runName: `Build #${buildNumber} - ${branch} - {browser}`, + }] + ], +}; +``` + +## Handling Test Retries + +The reporter tracks retry attempts and reports them to TestPlanIt: + +```javascript +// wdio.conf.js +export const config = { + specFileRetries: 1, // Retry failed spec files + specFileRetriesDelay: 0, + reporters: [ + ['@testplanit/wdio-reporter', { + domain: 'https://testplanit.example.com', + apiToken: process.env.TESTPLANIT_API_TOKEN, + projectId: 1, + }] + ], +}; +``` + +Each retry attempt is recorded with its attempt number, so you can see the full history of a flaky test. + +## Debugging + +Enable verbose logging to troubleshoot issues: + +```javascript +// wdio.conf.js +export const config = { + reporters: [ + ['@testplanit/wdio-reporter', { + domain: 'https://testplanit.example.com', + apiToken: process.env.TESTPLANIT_API_TOKEN, + projectId: 1, + verbose: true, // Enables detailed logging + }] + ], +}; +``` + +You'll see detailed output: + +``` +[TestPlanIt] Initializing reporter... +[TestPlanIt] Status mapping: passed -> 1 +[TestPlanIt] Status mapping: failed -> 2 +[TestPlanIt] Status mapping: skipped -> 3 +[TestPlanIt] Creating test run: E2E Tests - 2024-01-15 14:30:00 +[TestPlanIt] Created test run with ID: 123 +[TestPlanIt] Test passed: should login successfully (Case IDs: 12345) +[TestPlanIt] Added case to run: 456 +[TestPlanIt] Created test result: 789 +``` + +## Complete Example + +Here's a complete configuration with all features: + +```javascript +// wdio.conf.js +import { TestPlanItService } from '@testplanit/wdio-reporter'; + +export const config = { + specs: ['./test/specs/**/*.js'], + + capabilities: [{ + browserName: 'chrome', + 'goog:chromeOptions': { + args: ['--headless', '--disable-gpu'] + } + }], + + framework: 'mocha', + + mochaOpts: { + ui: 'bdd', + timeout: 60000 + }, + + services: [ + [TestPlanItService, { + // Required + domain: process.env.TESTPLANIT_URL || 'https://testplanit.example.com', + apiToken: process.env.TESTPLANIT_API_TOKEN, + projectId: parseInt(process.env.TESTPLANIT_PROJECT_ID || '1'), + + // Test run configuration + runName: `E2E Tests - Build #${process.env.BUILD_NUMBER || 'local'} - {date}`, + configId: 5, // Chrome configuration + milestoneId: 10, // Current sprint + + // Screenshots + captureScreenshots: true, + + // Debugging + verbose: process.env.DEBUG === 'true', + }] + ], + + reporters: [ + 'spec', // Keep the spec reporter for console output + ['@testplanit/wdio-reporter', { + // Required + domain: process.env.TESTPLANIT_URL || 'https://testplanit.example.com', + apiToken: process.env.TESTPLANIT_API_TOKEN, + projectId: parseInt(process.env.TESTPLANIT_PROJECT_ID || '1'), + + // Case ID parsing (default matches [1234] format) + caseIdPattern: /\[(\d+)\]/g, + + // Auto-create cases (optional) + autoCreateTestCases: true, + parentFolderId: 100, + templateId: 1, + + // Result options + uploadScreenshots: true, + includeStackTrace: true, + + // Debugging + verbose: process.env.DEBUG === 'true', + }] + ], +}; +``` + +## TypeScript Support + +The package includes full TypeScript definitions: + +```typescript +import type { + TestPlanItReporterOptions, + TestPlanItServiceOptions, +} from '@testplanit/wdio-reporter'; + +const serviceOptions: TestPlanItServiceOptions = { + domain: 'https://testplanit.example.com', + apiToken: process.env.TESTPLANIT_API_TOKEN!, + projectId: 1, + runName: 'TypeScript Tests - {date}', + captureScreenshots: true, +}; + +const reporterOptions: TestPlanItReporterOptions = { + domain: 'https://testplanit.example.com', + apiToken: process.env.TESTPLANIT_API_TOKEN!, + projectId: 1, + autoCreateTestCases: true, + parentFolderId: 10, + templateId: 1, +}; +``` + +## Compatibility + +| WebdriverIO Version | Supported | +| -------------------- | ----------- | +| 9.x | Yes | +| 8.x | Yes | + +Requires Node.js 18 or later. + +## Related Resources + +- [API Client](./api-client.md) - Direct API access for custom integrations +- [SDK Overview](./index.md) - Architecture and package overview +- [API Tokens](../api-tokens.md) - Creating and managing API tokens diff --git a/docs/docs/sdk/wdio-configuration.md b/docs/docs/sdk/wdio-configuration.md new file mode 100644 index 00000000..aca4c54c --- /dev/null +++ b/docs/docs/sdk/wdio-configuration.md @@ -0,0 +1,113 @@ +--- +title: Configuration Options +--- + +# Configuration Options + +These options apply to the **reporter**. If you're using the [Launcher Service](./wdio-launcher-service.md), see [Choosing Your Setup](./wdio-overview.md#choosing-your-setup) for which options apply where. + +## Required + +| Option | Type | Description | +| -------- | -------------------------- | ------------- | +| `domain` | `string` | Base URL of your TestPlanIt instance | +| `apiToken` | `string` | API token for authentication (starts with `tpi_`) | +| `projectId` | `number` | Project ID where results will be reported (find this on the [Project Overview](../user-guide/project-overview.md) page) | + +## Optional + +| Option | Type | Default | Description | +| -------- | -------------------------- | --------- | ------------- | +| `testRunId` | `number \| string` | - | Existing test run to add results to (ID or name). If set, `runName` is ignored | +| `runName` | `string` | `'{suite} - {date} {time}'` | Name for new test runs (ignored if `testRunId` is set). Supports placeholders | +| `testRunType` | `string` | Auto-detected | Test framework type. Auto-detected from WebdriverIO config (`mocha` → `'MOCHA'`, `cucumber` → `'CUCUMBER'`, others → `'REGULAR'`). Override manually if needed. | +| `configId` | `number \| string` | - | Configuration for the test run (ID or name) | +| `milestoneId` | `number \| string` | - | Milestone for the test run (ID or name) | +| `stateId` | `number \| string` | - | Workflow state for the test run (ID or name) | +| `caseIdPattern` | `RegExp \| string` | `/\[(\d+)\]/g` | Regex pattern for extracting case IDs from test titles | +| `autoCreateTestCases` | `boolean` | `false` | Auto-create test cases if they don't exist | +| `createFolderHierarchy` | `boolean` | `false` | Create folder hierarchy based on Mocha suite structure (requires `autoCreateTestCases` and `parentFolderId`) | +| `parentFolderId` | `number \| string` | - | Folder for auto-created test cases (ID or name) | +| `templateId` | `number \| string` | - | Template for auto-created test cases (ID or name) | +| `tagIds` | `(number \| string)[]` | - | Tags to apply to the test run (IDs or names). Tags that don't exist are created automatically | +| `uploadScreenshots` | `boolean` | `true` | Upload intercepted screenshots to TestPlanIt (requires screenshot capture — see [Screenshot Uploads](./wdio-screenshots.md)) | +| `includeStackTrace` | `boolean` | `true` | Include stack traces for failures | +| `completeRunOnFinish` | `boolean` | `true` | Mark run as complete when tests finish | +| `oneReport` | `boolean` | `true` | Combine parallel workers from the same spec file into a single test run. Does not persist across spec file batches — use the [Launcher Service](./wdio-launcher-service.md) for that | +| `timeout` | `number` | `30000` | API request timeout in ms | +| `maxRetries` | `number` | `3` | Retry attempts for failed requests | +| `verbose` | `boolean` | `false` | Enable debug logging | + +## Run Name Placeholders + +Customize your test run names with these placeholders: + +| Placeholder | Description | Example | +| ------------- | ------------- | --------- | +| `{suite}` | Root suite name (first describe block) | `Login Tests` | +| `{spec}` | Spec file name (without extension) | `login` | +| `{date}` | Current date in ISO format | `2024-01-15` | +| `{time}` | Current time | `14:30:00` | +| `{browser}` | Browser name from capabilities | `chrome` | +| `{platform}` | Platform/OS name | `darwin`, `linux`, `win32` | + +The default run name is `'{suite} - {date} {time}'`, which uses the root describe block name to identify your test runs. + +```javascript +// wdio.conf.js +export const config = { + reporters: [ + ['@testplanit/wdio-reporter', { + domain: 'https://testplanit.example.com', + apiToken: process.env.TESTPLANIT_API_TOKEN, + projectId: 1, + // Default: '{suite} - {date} {time}' + // Custom example: + runName: 'E2E Tests - {browser} - {date} {time}', + }] + ], +}; +``` + +## Appending to Existing Test Runs + +Add results to an existing test run instead of creating a new one: + +```javascript +// wdio.conf.js +export const config = { + reporters: [ + ['@testplanit/wdio-reporter', { + domain: 'https://testplanit.example.com', + apiToken: process.env.TESTPLANIT_API_TOKEN, + projectId: 1, + testRunId: 456, // Add results to this existing run + }] + ], +}; +``` + +This is useful for: +- Aggregating results from multiple CI jobs +- Running tests in parallel across machines +- Re-running failed tests without creating new runs + +## Associating with Configurations and Milestones + +Track test results against specific configurations (browser/OS combinations) and milestones: + +```javascript +// wdio.conf.js +export const config = { + reporters: [ + ['@testplanit/wdio-reporter', { + domain: 'https://testplanit.example.com', + apiToken: process.env.TESTPLANIT_API_TOKEN, + projectId: 1, + configId: 5, // e.g., "Chrome / macOS" + milestoneId: 10, // e.g., "Sprint 15" + stateId: 2, // e.g., "In Progress" workflow state + }] + ], +}; +``` diff --git a/docs/docs/sdk/wdio-launcher-service.md b/docs/docs/sdk/wdio-launcher-service.md new file mode 100644 index 00000000..3edd60d3 --- /dev/null +++ b/docs/docs/sdk/wdio-launcher-service.md @@ -0,0 +1,79 @@ +--- +title: Launcher Service +--- + +# Launcher Service + +If you run multiple spec files and want all of them to report to a **single test run**, use the `TestPlanItService` launcher service. It creates the test run once before any workers start and completes it after all workers finish — regardless of `maxInstances` or how many batches your specs run in. + +:::info Why not `oneReport`? +The `oneReport` option only combines parallel workers that overlap in execution time (e.g., workers from the same spec file). When you have more spec files than `maxInstances`, workers run in batches — once the first batch finishes, `oneReport` completes the run before the next batch starts. The launcher service solves this by managing the run lifecycle in the main process, outside of any worker. +::: + +## Setup + +Add `TestPlanItService` to the `services` array alongside the reporter in `reporters`: + +```javascript +// wdio.conf.js +import { TestPlanItService } from '@testplanit/wdio-reporter'; + +export const config = { + services: [ + [TestPlanItService, { + domain: 'https://testplanit.example.com', + apiToken: process.env.TESTPLANIT_API_TOKEN, + projectId: 1, + runName: 'E2E Tests - {date}', + }] + ], + reporters: [ + ['@testplanit/wdio-reporter', { + domain: 'https://testplanit.example.com', + apiToken: process.env.TESTPLANIT_API_TOKEN, + projectId: 1, + // Reporter-specific options (case linking, screenshots, etc.) + autoCreateTestCases: true, + parentFolderId: 10, + templateId: 1, + // Do NOT set runName or testRunId here — the service handles it + }] + ], +}; +``` + +The service creates the test run and writes a shared state file. Each reporter worker detects this file on startup and reports results to the pre-created run instead of creating its own. + +## Service Configuration Options + +The service requires the same `domain`, `apiToken`, and `projectId` as the reporter. Additional options: + +| Option | Type | Default | Description | +| -------- | ------ | --------- | ------------- | +| `runName` | `string` | `'Automated Tests - {date} {time}'` | Name for the test run. Supports `{date}`, `{time}`, and `{platform}` placeholders | +| `testRunType` | `string` | `'MOCHA'` | Test framework type (`REGULAR`, `JUNIT`, `MOCHA`, `CUCUMBER`, etc.) | +| `configId` | `number \| string` | - | Configuration for the test run (ID or name) | +| `milestoneId` | `number \| string` | - | Milestone for the test run (ID or name) | +| `stateId` | `number \| string` | - | Workflow state for the test run (ID or name) | +| `tagIds` | `(number \| string)[]` | - | Tags to apply to the test run (IDs or names) | +| `completeRunOnFinish` | `boolean` | `true` | Mark run as completed when all workers finish | +| `captureScreenshots` | `boolean` | `false` | Automatically capture a screenshot when a test fails (uploaded by the reporter — see [Screenshot Uploads](./wdio-screenshots.md)) | +| `timeout` | `number` | `30000` | API request timeout in ms | +| `maxRetries` | `number` | `3` | Retry attempts for failed requests | +| `verbose` | `boolean` | `false` | Enable debug logging | + +:::note +The `{browser}`, `{spec}`, and `{suite}` placeholders are **not available** in the service context because the service runs before any workers start. Use `{date}`, `{time}`, and `{platform}` instead. +::: + +## How It Works + +1. **`onPrepare`** (before workers): The service creates a test run and JUnit test suite via the API, then writes a shared state file to the system temp directory. +2. **Workers run**: Each reporter worker reads the shared state file, finds the pre-created test run ID, and reports results to it. Workers skip creating or completing their own runs. +3. **`onComplete`** (after all workers): The service completes the test run (if `completeRunOnFinish` is `true`) and deletes the shared state file. + +If the service fails during `onPrepare`, the error is thrown and the WDIO session is aborted. If it fails during `onComplete`, the error is logged but does not affect the test exit code. + +## What the Service Overrides + +When both the service and reporter are used, the service takes over the test run lifecycle. See [Choosing Your Setup](./wdio-overview.md#choosing-your-setup) for the full breakdown of which reporter options are ignored vs. still apply. diff --git a/docs/docs/sdk/wdio-overview.md b/docs/docs/sdk/wdio-overview.md new file mode 100644 index 00000000..3d2f6a5c --- /dev/null +++ b/docs/docs/sdk/wdio-overview.md @@ -0,0 +1,165 @@ +--- +sidebar_label: 'WebdriverIO Reporter' +title: WebdriverIO Reporter (@testplanit/wdio-reporter) +--- + +# WebdriverIO Reporter + +`@testplanit/wdio-reporter` is an official WebdriverIO reporter that automatically sends test results to your TestPlanIt instance. It supports linking tests to existing test cases, automatic test case creation, screenshot uploads, and more. + +## Installation + +```bash +npm install @testplanit/wdio-reporter @testplanit/api +# or +pnpm add @testplanit/wdio-reporter @testplanit/api +# or +yarn add @testplanit/wdio-reporter @testplanit/api +``` + +## Quick Start + +Add the reporter to your WebdriverIO configuration: + +```javascript +// wdio.conf.js +export const config = { + reporters: [ + ['@testplanit/wdio-reporter', { + domain: 'https://testplanit.example.com', + apiToken: process.env.TESTPLANIT_API_TOKEN, + projectId: 1, + }] + ], +}; +``` + +Run your tests: + +```bash +npx wdio run wdio.conf.js +``` + +After your tests complete, you'll see a summary: + +```text +[TestPlanIt] Results Summary +[TestPlanIt] ═══════════════════════════════════════════════════════ +[TestPlanIt] Test Run ID: 123 +[TestPlanIt] Duration: 45.2s +[TestPlanIt] +[TestPlanIt] Test Results: +[TestPlanIt] ✓ Passed: 15 +[TestPlanIt] ✗ Failed: 2 +[TestPlanIt] ○ Skipped: 1 +[TestPlanIt] Total: 18 +[TestPlanIt] +[TestPlanIt] View results: https://testplanit.example.com/projects/runs/1/123 +[TestPlanIt] ═══════════════════════════════════════════════════════ +``` + +When `autoCreateTestCases` is enabled, additional stats are shown: + +```text +[TestPlanIt] Test Cases: +[TestPlanIt] Found (existing): 12 +[TestPlanIt] Created (new): 6 +``` + +Screenshot upload stats appear when screenshots are captured: + +```text +[TestPlanIt] Screenshots: +[TestPlanIt] Uploaded: 2 +``` + +## Choosing Your Setup + +This package provides two components: a **reporter** and a **launcher service**. Use them together or separately depending on your needs. + +### Reporter Only + +The simplest setup. Each worker creates its own test run, reports results, and completes the run when it finishes. Parallel workers running the same spec file are combined via the `oneReport` option. + +**Best for:** Single spec files, small test suites, or setups where one run per spec file is acceptable. + +```javascript +export const config = { + reporters: [ + ['@testplanit/wdio-reporter', { + domain: 'https://testplanit.example.com', + apiToken: process.env.TESTPLANIT_API_TOKEN, + projectId: 1, + }] + ], +}; +``` + +### Service + Reporter (recommended for multi-spec) + +The service creates a single test run before any workers start and completes it after all workers finish. Each reporter worker detects the pre-created run and reports results to it. This guarantees all spec files — across all batches — land in one test run. + +**Best for:** Multiple spec files, CI pipelines, any setup where you want a single consolidated test run for your entire test suite. + +```javascript +import { TestPlanItService } from '@testplanit/wdio-reporter'; + +export const config = { + services: [ + [TestPlanItService, { + domain: 'https://testplanit.example.com', + apiToken: process.env.TESTPLANIT_API_TOKEN, + projectId: 1, + runName: 'E2E Tests - {date}', + captureScreenshots: true, + }] + ], + reporters: [ + ['@testplanit/wdio-reporter', { + domain: 'https://testplanit.example.com', + apiToken: process.env.TESTPLANIT_API_TOKEN, + projectId: 1, + autoCreateTestCases: true, + parentFolderId: 10, + templateId: 1, + }] + ], +}; +``` + +### What the service overrides + +When both are used, the service takes over the test run lifecycle. The following reporter options are **ignored** because the service handles them: + +| Reporter option | Ignored because | +| ----------------- | ----------------- | +| `runName` | Service creates the run with its own `runName` | +| `testRunId` | Service provides the test run ID automatically | +| `oneReport` | Not needed — the service already ensures a single run | +| `completeRunOnFinish` | Service controls when the run is completed | +| `configId`, `milestoneId`, `stateId`, `tagIds` | Set these on the service instead | +| `testRunType` | Set on the service instead | + +The following reporter options **still apply** when using the service: + +| Reporter option | Purpose | +| ----------------- | --------- | +| `autoCreateTestCases` | Find or create test cases by name | +| `parentFolderId`, `templateId` | Control where and how test cases are created | +| `createFolderHierarchy` | Mirror suite structure as folders | +| `caseIdPattern` | Parse case IDs from test titles | +| `uploadScreenshots` | Upload intercepted screenshots | +| `includeStackTrace` | Include stack traces for failures | +| `timeout`, `maxRetries`, `verbose` | API and logging behavior per worker | + +:::tip +When using the service, set run-level options (`runName`, `configId`, `milestoneId`, etc.) on the **service** and test-level options (`autoCreateTestCases`, `caseIdPattern`, `uploadScreenshots`, etc.) on the **reporter**. +::: + +## Next Steps + +- [Configuration Options](./wdio-configuration.md) — Full reference for reporter options +- [Linking & Auto-Creating Test Cases](./wdio-test-cases.md) — Case ID patterns and auto-creation +- [Launcher Service](./wdio-launcher-service.md) — Single test run across all spec files +- [Screenshot Uploads](./wdio-screenshots.md) — Capturing and uploading screenshots +- [CI/CD & Advanced Usage](./wdio-ci-cd.md) — CI integration, retries, debugging, complete examples diff --git a/docs/docs/sdk/wdio-screenshots.md b/docs/docs/sdk/wdio-screenshots.md new file mode 100644 index 00000000..1b74b21d --- /dev/null +++ b/docs/docs/sdk/wdio-screenshots.md @@ -0,0 +1,67 @@ +--- +title: Screenshot Uploads +--- + +# Screenshot Uploads + +The reporter can upload screenshots to TestPlanIt when `uploadScreenshots` is enabled (the default). However, **the reporter does not automatically capture screenshots** - it intercepts screenshots taken by your WebdriverIO configuration and uploads them. + +## Using the Launcher Service (recommended) + +If you're using the [Launcher Service](./wdio-launcher-service.md), set `captureScreenshots: true` on the service. It handles the `afterTest` hook for you — no extra configuration needed: + +```javascript +// wdio.conf.js +import { TestPlanItService } from '@testplanit/wdio-reporter'; + +export const config = { + services: [ + [TestPlanItService, { + domain: 'https://testplanit.example.com', + apiToken: process.env.TESTPLANIT_API_TOKEN, + projectId: 1, + captureScreenshots: true, // Automatically capture screenshots on failure + }] + ], + reporters: [ + ['@testplanit/wdio-reporter', { + domain: 'https://testplanit.example.com', + apiToken: process.env.TESTPLANIT_API_TOKEN, + projectId: 1, + uploadScreenshots: true, // Upload captured screenshots (default) + }] + ], +}; +``` + +## Using the afterTest hook manually + +Without the launcher service, configure WebdriverIO to capture screenshots on failure using the `afterTest` hook: + +```javascript +// wdio.conf.js +export const config = { + afterTest: async function(test, context, { error, result, duration, passed }) { + if (!passed) { + // Take a screenshot - the reporter will intercept and upload it + await browser.takeScreenshot(); + } + }, + reporters: [ + ['@testplanit/wdio-reporter', { + domain: 'https://testplanit.example.com', + apiToken: process.env.TESTPLANIT_API_TOKEN, + projectId: 1, + uploadScreenshots: true, // Upload intercepted screenshots to TestPlanIt + }] + ], +}; +``` + +## How it works + +1. A screenshot is captured when a test fails (either by the service or your `afterTest` hook) +2. The reporter intercepts the screenshot data from the WebdriverIO command +3. When the test result is reported, the screenshot is uploaded as an attachment + +**Note:** Using `browser.saveScreenshot('./path/to/file.png')` also works - the reporter intercepts the screenshot data before it's saved to disk. diff --git a/docs/docs/sdk/wdio-test-cases.md b/docs/docs/sdk/wdio-test-cases.md new file mode 100644 index 00000000..f2df2b5e --- /dev/null +++ b/docs/docs/sdk/wdio-test-cases.md @@ -0,0 +1,175 @@ +--- +title: Linking & Auto-Creating Test Cases +--- + +# Linking & Auto-Creating Test Cases + +## Linking Tests to Test Cases + +Link your automated tests to existing TestPlanIt test cases by including case IDs in your test titles. By default, the reporter looks for case IDs in square brackets like `[1234]`: + +```javascript +describe('User Authentication', () => { + it('[12345] should login with valid credentials', async () => { + // This test links to TestPlanIt case #12345 + await LoginPage.login('user@example.com', 'password'); + await expect(DashboardPage.heading).toBeDisplayed(); + }); + + it('[12346] [12347] should show error for invalid password', async () => { + // This test links to BOTH case #12346 and #12347 + await LoginPage.login('user@example.com', 'wrongpassword'); + await expect(LoginPage.errorMessage).toHaveText('Invalid credentials'); + }); + + it('should logout successfully', async () => { + // No case ID - will be skipped unless autoCreateTestCases is enabled + // With autoCreateTestCases: true, this links to or creates a case named "should logout successfully" + await DashboardPage.logout(); + }); +}); +``` + +### Custom Case ID Patterns + +The `caseIdPattern` option accepts a regular expression to match case IDs in your test titles. The pattern **must include a capturing group** `(\d+)` to extract the numeric ID. + +```javascript +// wdio.conf.js +export const config = { + reporters: [ + ['@testplanit/wdio-reporter', { + domain: 'https://testplanit.example.com', + apiToken: process.env.TESTPLANIT_API_TOKEN, + projectId: 1, + // Choose a pattern that matches your test naming convention: + caseIdPattern: /C(\d+)/g, // Matches: C12345 + }] + ], +}; +``` + +### When No Case ID Is Found + +If the pattern doesn't match any case ID in a test title, the behavior depends on the `autoCreateTestCases` setting: + +| `autoCreateTestCases` | Behavior | +| ----------------------- | ---------- | +| `false` (default) | The test result is **skipped** and not reported to TestPlanIt. A warning is logged if `verbose` is enabled. | +| `true` | The reporter looks up or creates a test case by matching on the test name and suite (className). See [Auto-Creating Test Cases](#auto-creating-test-cases). | + +This means if you're using case IDs exclusively (without auto-creation), tests without valid case IDs in their titles won't appear in your TestPlanIt results. + +### Common Pattern Examples + +| Pattern | Matches | Example Test Title | +| --------- | --------- | ------------------- | +| `/\[(\d+)\]/g` (default) | `[1234]` | `[1234] should load the page` | +| `/C(\d+)/g` | `C1234` | `C1234 should load the page` | +| `/TC-(\d+)/g` | `TC-1234` | `TC-1234 should load the page` | +| `/TEST-(\d+)/g` | `TEST-1234` | `TEST-1234 should load the page` | +| `/CASE-(\d+)/g` | `CASE-1234` | `CASE-1234 should load the page` | +| `/^(\d+)\s/g` | Plain number at start | `1234 should load the page` | +| `/#(\d+)/g` | `#1234` | `#1234 should load the page` | + +### Using Pattern as String + +You can also pass the pattern as a string (useful for JSON config files): + +```javascript +// wdio.conf.js +export const config = { + reporters: [ + ['@testplanit/wdio-reporter', { + domain: 'https://testplanit.example.com', + apiToken: process.env.TESTPLANIT_API_TOKEN, + projectId: 1, + caseIdPattern: 'TC-(\\d+)', // Note: double backslash in strings + }] + ], +}; +``` + +## Auto-Creating Test Cases + +Automatically create test cases in TestPlanIt for tests without case IDs: + +```javascript +// wdio.conf.js +export const config = { + reporters: [ + ['@testplanit/wdio-reporter', { + domain: 'https://testplanit.example.com', + apiToken: process.env.TESTPLANIT_API_TOKEN, + projectId: 1, + autoCreateTestCases: true, + parentFolderId: 10, // Required: folder for new cases + templateId: 1, // Required: template for new cases + }] + ], +}; +``` + +When `autoCreateTestCases` is enabled: +- Tests with case IDs still link to existing cases +- Tests without case IDs are looked up by name and suite (className) +- If a matching case is found, results are linked to it +- If no match is found, a new case is created in TestPlanIt +- The test title becomes the case name +- The suite name becomes the case's `className` for grouping + +This means on first run, test cases are created automatically. On subsequent runs, the same test cases are reused based on matching name and suite. + +### Creating Folder Hierarchies + +When you have nested Mocha suites (describe blocks), you can automatically create a matching folder structure in TestPlanIt: + +```javascript +// wdio.conf.js +export const config = { + reporters: [ + ['@testplanit/wdio-reporter', { + domain: 'https://testplanit.example.com', + apiToken: process.env.TESTPLANIT_API_TOKEN, + projectId: 1, + autoCreateTestCases: true, + parentFolderId: 10, // Root folder for created hierarchy + templateId: 1, + createFolderHierarchy: true, // Enable folder hierarchy creation + }] + ], +}; +``` + +With `createFolderHierarchy` enabled, nested describe blocks create nested folders: + +```javascript +// test/specs/login.spec.js +describe('Authentication', () => { // Creates folder: "Authentication" + describe('Login', () => { // Creates folder: "Authentication > Login" + describe('@smoke', () => { // Creates folder: "Authentication > Login > @smoke" + it('should login with valid credentials', async () => { + // Test case placed in "Authentication > Login > @smoke" folder + }); + }); + }); +}); +``` + +This creates: + +```text +parentFolderId (e.g., "Automated Tests") +└── Authentication + └── Login + └── @smoke + └── "should login with valid credentials" (test case) +``` + +**Requirements:** + +- `autoCreateTestCases` must be `true` +- `parentFolderId` must be set (this becomes the root of the hierarchy) +- `templateId` must be set for new test cases + +Folder paths are cached during the test run to avoid redundant API calls, making large test suites efficient. diff --git a/docs/docs/sdk/webdriverio-reporter.md b/docs/docs/sdk/webdriverio-reporter.md deleted file mode 100644 index 78da0e04..00000000 --- a/docs/docs/sdk/webdriverio-reporter.md +++ /dev/null @@ -1,629 +0,0 @@ ---- -sidebar_position: 3 -title: WebdriverIO Reporter (@testplanit/wdio-reporter) ---- - -# WebdriverIO Reporter - -`@testplanit/wdio-reporter` is an official WebdriverIO reporter that automatically sends test results to your TestPlanIt instance. It supports linking tests to existing test cases, automatic test case creation, screenshot uploads, and more. - -## Installation - -```bash -npm install @testplanit/wdio-reporter @testplanit/api -# or -pnpm add @testplanit/wdio-reporter @testplanit/api -# or -yarn add @testplanit/wdio-reporter @testplanit/api -``` - -## Quick Start - -Add the reporter to your WebdriverIO configuration: - -```javascript -// wdio.conf.js -export const config = { - reporters: [ - ['@testplanit/wdio-reporter', { - domain: 'https://testplanit.example.com', - apiToken: process.env.TESTPLANIT_API_TOKEN, - projectId: 1, - }] - ], -}; -``` - -Run your tests: - -```bash -npx wdio run wdio.conf.js -``` - -After your tests complete, you'll see a summary: - -```text -[TestPlanIt] Results Summary -[TestPlanIt] ═══════════════════════════════════════════════════════ -[TestPlanIt] Test Run ID: 123 -[TestPlanIt] Duration: 45.2s -[TestPlanIt] -[TestPlanIt] Test Results: -[TestPlanIt] ✓ Passed: 15 -[TestPlanIt] ✗ Failed: 2 -[TestPlanIt] ○ Skipped: 1 -[TestPlanIt] Total: 18 -[TestPlanIt] -[TestPlanIt] View results: https://testplanit.example.com/projects/runs/1/123 -[TestPlanIt] ═══════════════════════════════════════════════════════ -``` - -When `autoCreateTestCases` is enabled, additional stats are shown: - -```text -[TestPlanIt] Test Cases: -[TestPlanIt] Found (existing): 12 -[TestPlanIt] Created (new): 6 -``` - -Screenshot upload stats appear when screenshots are captured: - -```text -[TestPlanIt] Screenshots: -[TestPlanIt] Uploaded: 2 -``` - -## Configuration Options - -### Required - -| Option | Type | Description | -|--------|--------------------------|-------------| -| `domain` | `string` | Base URL of your TestPlanIt instance | -| `apiToken` | `string` | API token for authentication (starts with `tpi_`) | -| `projectId` | `number` | Project ID where results will be reported (find this on the [Project Overview](../user-guide/project-overview.md) page) | - -### Optional - -| Option | Type | Default | Description | -|--------|--------------------------|---------|-------------| -| `testRunId` | `number \| string` | - | Existing test run to add results to (ID or name). If set, `runName` is ignored | -| `runName` | `string` | `'{suite} - {date} {time}'` | Name for new test runs (ignored if `testRunId` is set). Supports placeholders | -| `testRunType` | `string` | Auto-detected | Test framework type. Auto-detected from WebdriverIO config (`mocha` → `'MOCHA'`, `cucumber` → `'CUCUMBER'`, others → `'REGULAR'`). Override manually if needed. | -| `configId` | `number \| string` | - | Configuration for the test run (ID or name) | -| `milestoneId` | `number \| string` | - | Milestone for the test run (ID or name) | -| `stateId` | `number \| string` | - | Workflow state for the test run (ID or name) | -| `caseIdPattern` | `RegExp \| string` | `/\[(\d+)\]/g` | Regex pattern for extracting case IDs from test titles | -| `autoCreateTestCases` | `boolean` | `false` | Auto-create test cases if they don't exist | -| `createFolderHierarchy` | `boolean` | `false` | Create folder hierarchy based on Mocha suite structure (requires `autoCreateTestCases` and `parentFolderId`) | -| `parentFolderId` | `number \| string` | - | Folder for auto-created test cases (ID or name) | -| `templateId` | `number \| string` | - | Template for auto-created test cases (ID or name) | -| `tagIds` | `(number \| string)[]` | - | Tags to apply to the test run (IDs or names). Tags that don't exist are created automatically | -| `uploadScreenshots` | `boolean` | `true` | Upload intercepted screenshots to TestPlanIt (requires `afterTest` hook to capture them) | -| `includeStackTrace` | `boolean` | `true` | Include stack traces for failures | -| `completeRunOnFinish` | `boolean` | `true` | Mark run as complete when tests finish | -| `oneReport` | `boolean` | `true` | Use single test run for all specs | -| `timeout` | `number` | `30000` | API request timeout in ms | -| `maxRetries` | `number` | `3` | Retry attempts for failed requests | -| `verbose` | `boolean` | `false` | Enable debug logging | - -## Linking Tests to Test Cases - -Link your automated tests to existing TestPlanIt test cases by including case IDs in your test titles. By default, the reporter looks for case IDs in square brackets like `[1234]`: - -```javascript -describe('User Authentication', () => { - it('[12345] should login with valid credentials', async () => { - // This test links to TestPlanIt case #12345 - await LoginPage.login('user@example.com', 'password'); - await expect(DashboardPage.heading).toBeDisplayed(); - }); - - it('[12346] [12347] should show error for invalid password', async () => { - // This test links to BOTH case #12346 and #12347 - await LoginPage.login('user@example.com', 'wrongpassword'); - await expect(LoginPage.errorMessage).toHaveText('Invalid credentials'); - }); - - it('should logout successfully', async () => { - // No case ID - will be skipped unless autoCreateTestCases is enabled - // With autoCreateTestCases: true, this links to or creates a case named "should logout successfully" - await DashboardPage.logout(); - }); -}); -``` - -### Custom Case ID Patterns - -The `caseIdPattern` option accepts a regular expression to match case IDs in your test titles. The pattern **must include a capturing group** `(\d+)` to extract the numeric ID. - -```javascript -// wdio.conf.js -export const config = { - reporters: [ - ['@testplanit/wdio-reporter', { - domain: 'https://testplanit.example.com', - apiToken: process.env.TESTPLANIT_API_TOKEN, - projectId: 1, - // Choose a pattern that matches your test naming convention: - caseIdPattern: /C(\d+)/g, // Matches: C12345 - }] - ], -}; -``` - -#### When No Case ID Is Found - -If the pattern doesn't match any case ID in a test title, the behavior depends on the `autoCreateTestCases` setting: - -| `autoCreateTestCases` | Behavior | -|-----------------------|----------| -| `false` (default) | The test result is **skipped** and not reported to TestPlanIt. A warning is logged if `verbose` is enabled. | -| `true` | The reporter looks up or creates a test case by matching on the test name and suite (className). See [Auto-Creating Test Cases](#auto-creating-test-cases). | - -This means if you're using case IDs exclusively (without auto-creation), tests without valid case IDs in their titles won't appear in your TestPlanIt results. - -#### Common Pattern Examples - -| Pattern | Matches | Example Test Title | -|---------|---------|-------------------| -| `/\[(\d+)\]/g` (default) | `[1234]` | `[1234] should load the page` | -| `/C(\d+)/g` | `C1234` | `C1234 should load the page` | -| `/TC-(\d+)/g` | `TC-1234` | `TC-1234 should load the page` | -| `/TEST-(\d+)/g` | `TEST-1234` | `TEST-1234 should load the page` | -| `/CASE-(\d+)/g` | `CASE-1234` | `CASE-1234 should load the page` | -| `/^(\d+)\s/g` | Plain number at start | `1234 should load the page` | -| `/#(\d+)/g` | `#1234` | `#1234 should load the page` | - -#### Using Pattern as String - -You can also pass the pattern as a string (useful for JSON config files): - -```javascript -// wdio.conf.js -export const config = { - reporters: [ - ['@testplanit/wdio-reporter', { - domain: 'https://testplanit.example.com', - apiToken: process.env.TESTPLANIT_API_TOKEN, - projectId: 1, - caseIdPattern: 'TC-(\\d+)', // Note: double backslash in strings - }] - ], -}; -``` - -## Run Name Placeholders - -Customize your test run names with these placeholders: - -| Placeholder | Description | Example | -|-------------|-------------|---------| -| `{suite}` | Root suite name (first describe block) | `Login Tests` | -| `{spec}` | Spec file name (without extension) | `login` | -| `{date}` | Current date in ISO format | `2024-01-15` | -| `{time}` | Current time | `14:30:00` | -| `{browser}` | Browser name from capabilities | `chrome` | -| `{platform}` | Platform/OS name | `darwin`, `linux`, `win32` | - -The default run name is `'{suite} - {date} {time}'`, which uses the root describe block name to identify your test runs. - -```javascript -// wdio.conf.js -export const config = { - reporters: [ - ['@testplanit/wdio-reporter', { - domain: 'https://testplanit.example.com', - apiToken: process.env.TESTPLANIT_API_TOKEN, - projectId: 1, - // Default: '{suite} - {date} {time}' - // Custom example: - runName: 'E2E Tests - {browser} - {date} {time}', - }] - ], -}; -``` - -## Auto-Creating Test Cases - -Automatically create test cases in TestPlanIt for tests without case IDs: - -```javascript -// wdio.conf.js -export const config = { - reporters: [ - ['@testplanit/wdio-reporter', { - domain: 'https://testplanit.example.com', - apiToken: process.env.TESTPLANIT_API_TOKEN, - projectId: 1, - autoCreateTestCases: true, - parentFolderId: 10, // Required: folder for new cases - templateId: 1, // Required: template for new cases - }] - ], -}; -``` - -When `autoCreateTestCases` is enabled: -- Tests with case IDs still link to existing cases -- Tests without case IDs are looked up by name and suite (className) -- If a matching case is found, results are linked to it -- If no match is found, a new case is created in TestPlanIt -- The test title becomes the case name -- The suite name becomes the case's `className` for grouping - -This means on first run, test cases are created automatically. On subsequent runs, the same test cases are reused based on matching name and suite. - -### Creating Folder Hierarchies - -When you have nested Mocha suites (describe blocks), you can automatically create a matching folder structure in TestPlanIt: - -```javascript -// wdio.conf.js -export const config = { - reporters: [ - ['@testplanit/wdio-reporter', { - domain: 'https://testplanit.example.com', - apiToken: process.env.TESTPLANIT_API_TOKEN, - projectId: 1, - autoCreateTestCases: true, - parentFolderId: 10, // Root folder for created hierarchy - templateId: 1, - createFolderHierarchy: true, // Enable folder hierarchy creation - }] - ], -}; -``` - -With `createFolderHierarchy` enabled, nested describe blocks create nested folders: - -```javascript -// test/specs/login.spec.js -describe('Authentication', () => { // Creates folder: "Authentication" - describe('Login', () => { // Creates folder: "Authentication > Login" - describe('@smoke', () => { // Creates folder: "Authentication > Login > @smoke" - it('should login with valid credentials', async () => { - // Test case placed in "Authentication > Login > @smoke" folder - }); - }); - }); -}); -``` - -This creates: - -```text -parentFolderId (e.g., "Automated Tests") -└── Authentication - └── Login - └── @smoke - └── "should login with valid credentials" (test case) -``` - -**Requirements:** - -- `autoCreateTestCases` must be `true` -- `parentFolderId` must be set (this becomes the root of the hierarchy) -- `templateId` must be set for new test cases - -Folder paths are cached during the test run to avoid redundant API calls, making large test suites efficient. - -## Appending to Existing Test Runs - -Add results to an existing test run instead of creating a new one: - -```javascript -// wdio.conf.js -export const config = { - reporters: [ - ['@testplanit/wdio-reporter', { - domain: 'https://testplanit.example.com', - apiToken: process.env.TESTPLANIT_API_TOKEN, - projectId: 1, - testRunId: 456, // Add results to this existing run - }] - ], -}; -``` - -This is useful for: -- Aggregating results from multiple CI jobs -- Running tests in parallel across machines -- Re-running failed tests without creating new runs - -## Screenshot Uploads - -The reporter can upload screenshots to TestPlanIt when `uploadScreenshots` is enabled (the default). However, **the reporter does not automatically capture screenshots** - it intercepts screenshots taken by your WebdriverIO configuration and uploads them. - -You must configure WebdriverIO to capture screenshots on failure using the `afterTest` hook: - -```javascript -// wdio.conf.js -export const config = { - afterTest: async function(test, context, { error, result, duration, passed }) { - if (!passed) { - // Take a screenshot - the reporter will intercept and upload it - await browser.takeScreenshot(); - } - }, - reporters: [ - ['@testplanit/wdio-reporter', { - domain: 'https://testplanit.example.com', - apiToken: process.env.TESTPLANIT_API_TOKEN, - projectId: 1, - uploadScreenshots: true, // Upload intercepted screenshots to TestPlanIt - }] - ], -}; -``` - -**How it works:** - -1. Your `afterTest` hook calls `browser.takeScreenshot()` when a test fails -2. The reporter intercepts the screenshot data from the WebdriverIO command -3. When the test result is reported, the screenshot is uploaded as an attachment - -**Note:** Using `browser.saveScreenshot('./path/to/file.png')` also works - the reporter intercepts the screenshot data before it's saved to disk. - -## Associating with Configurations and Milestones - -Track test results against specific configurations (browser/OS combinations) and milestones: - -```javascript -// wdio.conf.js -export const config = { - reporters: [ - ['@testplanit/wdio-reporter', { - domain: 'https://testplanit.example.com', - apiToken: process.env.TESTPLANIT_API_TOKEN, - projectId: 1, - configId: 5, // e.g., "Chrome / macOS" - milestoneId: 10, // e.g., "Sprint 15" - stateId: 2, // e.g., "In Progress" workflow state - }] - ], -}; -``` - -## CI/CD Integration - -### GitHub Actions - -```yaml -name: E2E Tests - -on: [push, pull_request] - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Install dependencies - run: npm ci - - - name: Run E2E tests - env: - TESTPLANIT_API_TOKEN: ${{ secrets.TESTPLANIT_API_TOKEN }} - run: npx wdio run wdio.conf.js -``` - -### GitLab CI - -```yaml -e2e-tests: - image: node:20 - script: - - npm ci - - npx wdio run wdio.conf.js - variables: - TESTPLANIT_API_TOKEN: $TESTPLANIT_API_TOKEN -``` - -### Jenkins - -```groovy -pipeline { - agent any - environment { - TESTPLANIT_API_TOKEN = credentials('testplanit-api-token') - } - stages { - stage('E2E Tests') { - steps { - sh 'npm ci' - sh 'npx wdio run wdio.conf.js' - } - } - } -} -``` - -### Dynamic Run Names with Build Info - -Include CI build information in your test run names: - -```javascript -// wdio.conf.js -const buildNumber = process.env.GITHUB_RUN_NUMBER - || process.env.CI_PIPELINE_ID - || process.env.BUILD_NUMBER - || 'local'; - -const branch = process.env.GITHUB_REF_NAME - || process.env.CI_COMMIT_REF_NAME - || process.env.GIT_BRANCH - || 'unknown'; - -export const config = { - reporters: [ - ['@testplanit/wdio-reporter', { - domain: 'https://testplanit.example.com', - apiToken: process.env.TESTPLANIT_API_TOKEN, - projectId: 1, - runName: `Build #${buildNumber} - ${branch} - {browser}`, - }] - ], -}; -``` - -## Handling Test Retries - -The reporter tracks retry attempts and reports them to TestPlanIt: - -```javascript -// wdio.conf.js -export const config = { - specFileRetries: 1, // Retry failed spec files - specFileRetriesDelay: 0, - reporters: [ - ['@testplanit/wdio-reporter', { - domain: 'https://testplanit.example.com', - apiToken: process.env.TESTPLANIT_API_TOKEN, - projectId: 1, - }] - ], -}; -``` - -Each retry attempt is recorded with its attempt number, so you can see the full history of a flaky test. - -## Debugging - -Enable verbose logging to troubleshoot issues: - -```javascript -// wdio.conf.js -export const config = { - reporters: [ - ['@testplanit/wdio-reporter', { - domain: 'https://testplanit.example.com', - apiToken: process.env.TESTPLANIT_API_TOKEN, - projectId: 1, - verbose: true, // Enables detailed logging - }] - ], -}; -``` - -You'll see detailed output: - -``` -[TestPlanIt] Initializing reporter... -[TestPlanIt] Status mapping: passed -> 1 -[TestPlanIt] Status mapping: failed -> 2 -[TestPlanIt] Status mapping: skipped -> 3 -[TestPlanIt] Creating test run: E2E Tests - 2024-01-15 14:30:00 -[TestPlanIt] Created test run with ID: 123 -[TestPlanIt] Test passed: should login successfully (Case IDs: 12345) -[TestPlanIt] Added case to run: 456 -[TestPlanIt] Created test result: 789 -``` - -## Complete Example - -Here's a complete configuration with all features: - -```javascript -// wdio.conf.js -export const config = { - specs: ['./test/specs/**/*.js'], - - capabilities: [{ - browserName: 'chrome', - 'goog:chromeOptions': { - args: ['--headless', '--disable-gpu'] - } - }], - - framework: 'mocha', - - mochaOpts: { - ui: 'bdd', - timeout: 60000 - }, - - reporters: [ - 'spec', // Keep the spec reporter for console output - ['@testplanit/wdio-reporter', { - // Required - domain: process.env.TESTPLANIT_URL || 'https://testplanit.example.com', - apiToken: process.env.TESTPLANIT_API_TOKEN, - projectId: parseInt(process.env.TESTPLANIT_PROJECT_ID || '1'), - - // Test run configuration - runName: `E2E Tests - Build #${process.env.BUILD_NUMBER || 'local'} - {browser}`, - configId: 5, // Chrome configuration - milestoneId: 10, // Current sprint - - // Case ID parsing (default matches [1234] format) - caseIdPattern: /\[(\d+)\]/g, - - // Auto-create cases (optional) - autoCreateTestCases: false, - parentFolderId: 100, - templateId: 1, - - // Result options - uploadScreenshots: true, - includeStackTrace: true, - - // Behavior - completeRunOnFinish: true, - oneReport: true, - - // API options - timeout: 30000, - maxRetries: 3, - - // Debugging - verbose: process.env.DEBUG === 'true', - }] - ], - - // Capture screenshots on failure - afterTest: async function(test, context, { passed }) { - if (!passed) { - const timestamp = Date.now(); - await browser.saveScreenshot(`./screenshots/failure-${timestamp}.png`); - } - }, -}; -``` - -## TypeScript Support - -The package includes full TypeScript definitions: - -```typescript -import type { TestPlanItReporterOptions } from '@testplanit/wdio-reporter'; - -const reporterOptions: TestPlanItReporterOptions = { - domain: 'https://testplanit.example.com', - apiToken: process.env.TESTPLANIT_API_TOKEN!, - projectId: 1, - runName: 'TypeScript Tests - {date}', - verbose: true, -}; -``` - -## Compatibility - -| WebdriverIO Version | Supported | -|--------------------|-----------| -| 9.x | ✅ | -| 8.x | ✅ | - -Requires Node.js 18 or later. - -## Related Resources - -- [API Client](./api-client.md) - Direct API access for custom integrations -- [SDK Overview](./index.md) - Architecture and package overview -- [API Tokens](../api-tokens.md) - Creating and managing API tokens diff --git a/docs/sidebars.ts b/docs/sidebars.ts index 5e763918..fca59119 100644 --- a/docs/sidebars.ts +++ b/docs/sidebars.ts @@ -216,7 +216,21 @@ const sidebars: SidebarsConfig = { }, items: [ 'sdk/api-client', // @testplanit/api package - 'sdk/webdriverio-reporter', // @testplanit/wdio-reporter package + { + type: 'category', + label: 'WebdriverIO Reporter', + link: { + type: 'doc', + id: 'sdk/wdio-overview', + }, + items: [ + 'sdk/wdio-configuration', // Configuration options reference + 'sdk/wdio-test-cases', // Linking & auto-creating test cases + 'sdk/wdio-launcher-service', // Launcher service for single test run + 'sdk/wdio-screenshots', // Screenshot uploads + 'sdk/wdio-ci-cd', // CI/CD, retries, debugging, complete example + ], + }, ], }, // Add other categories or items here if needed in the future diff --git a/packages/wdio-testplanit-reporter/dist/index.d.mts b/packages/wdio-testplanit-reporter/dist/index.d.mts index bef3956e..9d6aeadc 100644 --- a/packages/wdio-testplanit-reporter/dist/index.d.mts +++ b/packages/wdio-testplanit-reporter/dist/index.d.mts @@ -168,6 +168,119 @@ interface TestPlanItReporterOptions extends Reporters.Options { */ oneReport?: boolean; } +/** + * Configuration options for the TestPlanIt WDIO launcher service. + * + * The service runs in the main WDIO process and manages the test run lifecycle: + * - Creates the test run before any workers start (onPrepare) + * - Completes the test run after all workers finish (onComplete) + * + * This ensures all spec files across all worker batches report to a single test run, + * regardless of `maxInstances` or execution order. + * + * @example + * ```javascript + * // wdio.conf.js + * import { TestPlanItService } from '@testplanit/wdio-reporter'; + * + * export const config = { + * services: [ + * [TestPlanItService, { + * domain: 'https://testplanit.example.com', + * apiToken: process.env.TESTPLANIT_API_TOKEN, + * projectId: 1, + * runName: 'Automated Tests - {date}', + * }] + * ], + * reporters: [ + * ['@testplanit/wdio-reporter', { + * domain: 'https://testplanit.example.com', + * apiToken: process.env.TESTPLANIT_API_TOKEN, + * projectId: 1, + * autoCreateTestCases: true, + * parentFolderId: 10, + * templateId: 1, + * }] + * ] + * } + * ``` + */ +interface TestPlanItServiceOptions { + /** + * The base URL of your TestPlanIt instance + * @example 'https://testplanit.example.com' + */ + domain: string; + /** + * API token for authentication + * Generate this from TestPlanIt: Settings > API Tokens + * Should start with 'tpi_' + */ + apiToken: string; + /** + * The project ID in TestPlanIt where results will be reported + */ + projectId: number; + /** + * Name for the test run. + * Supports placeholders: + * - {date} - Current date (YYYY-MM-DD) + * - {time} - Current time (HH:MM:SS) + * - {platform} - Platform/OS name + * + * Note: {browser}, {spec}, and {suite} are NOT available since the service + * runs before any workers start. They will be replaced with fallback values. + * + * @default 'Automated Tests - {date} {time}' + */ + runName?: string; + /** + * Test run type to indicate the test framework being used. + * @default 'MOCHA' + */ + testRunType?: 'REGULAR' | 'JUNIT' | 'TESTNG' | 'XUNIT' | 'NUNIT' | 'MSTEST' | 'MOCHA' | 'CUCUMBER'; + /** + * Configuration to associate with the test run (ID or name). + * If a string is provided, the system will look up the configuration by exact name match. + */ + configId?: number | string; + /** + * Milestone to associate with the test run (ID or name). + * If a string is provided, the system will look up the milestone by exact name match. + */ + milestoneId?: number | string; + /** + * Workflow state for the test run (ID or name). + * If a string is provided, the system will look up the state by exact name match. + */ + stateId?: number | string; + /** + * Tags to apply to the test run (IDs or names). + * If strings are provided, the system will look up each tag by exact name match. + * Tags that don't exist will be created automatically. + */ + tagIds?: (number | string)[]; + /** + * Whether to mark the test run as completed when all workers finish + * @default true + */ + completeRunOnFinish?: boolean; + /** + * Request timeout in milliseconds + * @default 30000 + */ + timeout?: number; + /** + * Number of retries for failed API requests + * @default 3 + */ + maxRetries?: number; + /** + * Enable verbose logging for debugging + * @default false + */ + verbose?: boolean; +} /** * Internal test result tracked by the reporter */ @@ -326,6 +439,8 @@ declare class TestPlanItReporter extends WDIOReporter { private currentTestUid; private currentCid; private pendingScreenshots; + /** When true, the TestPlanItService manages the test run lifecycle */ + private managedByService; /** * WebdriverIO uses this getter to determine if the reporter has finished async operations. * The test runner will wait for this to return true before terminating. @@ -340,40 +455,6 @@ declare class TestPlanItReporter extends WDIOReporter { * Log an error (always logs, not just in verbose mode) */ private logError; - /** - * Get the path to the shared state file for oneReport mode. - * Uses a file in the temp directory with a name based on the project ID. - */ - private getSharedStateFilePath; - /** - * Read shared state from file (for oneReport mode). - * Returns null if: - * - File doesn't exist - * - File is stale (older than 4 hours) - * - Previous run completed (activeWorkers === 0) - */ - private readSharedState; - /** - * Write shared state to file (for oneReport mode). - * Uses a lock file to prevent race conditions. - * Only writes the testRunId if the file doesn't exist yet (first writer wins). - * Updates testSuiteId if not already set. - */ - private writeSharedState; - /** - * Delete shared state file (cleanup after run completes). - */ - private deleteSharedState; - /** - * Increment the active worker count in shared state. - * Called when a worker starts using the shared test run. - */ - private incrementWorkerCount; - /** - * Decrement the active worker count in shared state. - * Returns true if this was the last worker (count reached 0). - */ - private decrementWorkerCount; /** * Track an async operation to prevent the runner from terminating early. * The operation is added to pendingOperations and removed when complete. @@ -456,4 +537,91 @@ declare class TestPlanItReporter extends WDIOReporter { getState(): ReporterState; } -export { type ReporterState, TestPlanItReporter, type TestPlanItReporterOptions, type TrackedTestResult, TestPlanItReporter as default }; +/** + * WebdriverIO Launcher Service for TestPlanIt. + * + * Manages the test run lifecycle in the main WDIO process: + * - onPrepare: Creates the test run and JUnit test suite ONCE before any workers start + * - onComplete: Completes the test run ONCE after all workers finish + * + * This ensures all spec files across all worker batches report to a single test run, + * regardless of `maxInstances` or execution order. + * + * @example + * ```javascript + * // wdio.conf.js + * import { TestPlanItService } from '@testplanit/wdio-reporter'; + * + * export const config = { + * services: [ + * [TestPlanItService, { + * domain: 'https://testplanit.example.com', + * apiToken: process.env.TESTPLANIT_API_TOKEN, + * projectId: 1, + * runName: 'E2E Tests - {date}', + * }] + * ], + * reporters: [ + * ['@testplanit/wdio-reporter', { + * domain: 'https://testplanit.example.com', + * apiToken: process.env.TESTPLANIT_API_TOKEN, + * projectId: 1, + * autoCreateTestCases: true, + * parentFolderId: 10, + * templateId: 1, + * }] + * ] + * } + * ``` + * + * @packageDocumentation + */ + +/** + * WebdriverIO Launcher Service for TestPlanIt. + * + * Creates a single test run before any workers start and completes it + * after all workers finish. Workers read the shared state file to find + * the pre-created test run and report results to it. + */ +declare class TestPlanItService { + private options; + private client; + private verbose; + private testRunId?; + private testSuiteId?; + constructor(serviceOptions: TestPlanItServiceOptions); + /** + * Log a message if verbose mode is enabled + */ + private log; + /** + * Log an error (always logs, not just in verbose mode) + */ + private logError; + /** + * Format run name with available placeholders. + * Note: {browser}, {spec}, and {suite} are NOT available in the service context + * since it runs before any workers start. + */ + private formatRunName; + /** + * Resolve string option IDs to numeric IDs using the API client. + */ + private resolveIds; + /** + * onPrepare - Runs once in the main process before any workers start. + * + * Creates the test run and JUnit test suite, then writes shared state + * so all worker reporters can find and use the pre-created run. + */ + onPrepare(): Promise; + /** + * onComplete - Runs once in the main process after all workers finish. + * + * Completes the test run and cleans up the shared state file. + */ + onComplete(exitCode: number): Promise; +} + +export { type ReporterState, TestPlanItReporter, type TestPlanItReporterOptions, TestPlanItService, type TestPlanItServiceOptions, type TrackedTestResult, TestPlanItReporter as default }; diff --git a/packages/wdio-testplanit-reporter/dist/index.d.ts b/packages/wdio-testplanit-reporter/dist/index.d.ts index bef3956e..9d6aeadc 100644 --- a/packages/wdio-testplanit-reporter/dist/index.d.ts +++ b/packages/wdio-testplanit-reporter/dist/index.d.ts @@ -168,6 +168,119 @@ interface TestPlanItReporterOptions extends Reporters.Options { */ oneReport?: boolean; } +/** + * Configuration options for the TestPlanIt WDIO launcher service. + * + * The service runs in the main WDIO process and manages the test run lifecycle: + * - Creates the test run before any workers start (onPrepare) + * - Completes the test run after all workers finish (onComplete) + * + * This ensures all spec files across all worker batches report to a single test run, + * regardless of `maxInstances` or execution order. + * + * @example + * ```javascript + * // wdio.conf.js + * import { TestPlanItService } from '@testplanit/wdio-reporter'; + * + * export const config = { + * services: [ + * [TestPlanItService, { + * domain: 'https://testplanit.example.com', + * apiToken: process.env.TESTPLANIT_API_TOKEN, + * projectId: 1, + * runName: 'Automated Tests - {date}', + * }] + * ], + * reporters: [ + * ['@testplanit/wdio-reporter', { + * domain: 'https://testplanit.example.com', + * apiToken: process.env.TESTPLANIT_API_TOKEN, + * projectId: 1, + * autoCreateTestCases: true, + * parentFolderId: 10, + * templateId: 1, + * }] + * ] + * } + * ``` + */ +interface TestPlanItServiceOptions { + /** + * The base URL of your TestPlanIt instance + * @example 'https://testplanit.example.com' + */ + domain: string; + /** + * API token for authentication + * Generate this from TestPlanIt: Settings > API Tokens + * Should start with 'tpi_' + */ + apiToken: string; + /** + * The project ID in TestPlanIt where results will be reported + */ + projectId: number; + /** + * Name for the test run. + * Supports placeholders: + * - {date} - Current date (YYYY-MM-DD) + * - {time} - Current time (HH:MM:SS) + * - {platform} - Platform/OS name + * + * Note: {browser}, {spec}, and {suite} are NOT available since the service + * runs before any workers start. They will be replaced with fallback values. + * + * @default 'Automated Tests - {date} {time}' + */ + runName?: string; + /** + * Test run type to indicate the test framework being used. + * @default 'MOCHA' + */ + testRunType?: 'REGULAR' | 'JUNIT' | 'TESTNG' | 'XUNIT' | 'NUNIT' | 'MSTEST' | 'MOCHA' | 'CUCUMBER'; + /** + * Configuration to associate with the test run (ID or name). + * If a string is provided, the system will look up the configuration by exact name match. + */ + configId?: number | string; + /** + * Milestone to associate with the test run (ID or name). + * If a string is provided, the system will look up the milestone by exact name match. + */ + milestoneId?: number | string; + /** + * Workflow state for the test run (ID or name). + * If a string is provided, the system will look up the state by exact name match. + */ + stateId?: number | string; + /** + * Tags to apply to the test run (IDs or names). + * If strings are provided, the system will look up each tag by exact name match. + * Tags that don't exist will be created automatically. + */ + tagIds?: (number | string)[]; + /** + * Whether to mark the test run as completed when all workers finish + * @default true + */ + completeRunOnFinish?: boolean; + /** + * Request timeout in milliseconds + * @default 30000 + */ + timeout?: number; + /** + * Number of retries for failed API requests + * @default 3 + */ + maxRetries?: number; + /** + * Enable verbose logging for debugging + * @default false + */ + verbose?: boolean; +} /** * Internal test result tracked by the reporter */ @@ -326,6 +439,8 @@ declare class TestPlanItReporter extends WDIOReporter { private currentTestUid; private currentCid; private pendingScreenshots; + /** When true, the TestPlanItService manages the test run lifecycle */ + private managedByService; /** * WebdriverIO uses this getter to determine if the reporter has finished async operations. * The test runner will wait for this to return true before terminating. @@ -340,40 +455,6 @@ declare class TestPlanItReporter extends WDIOReporter { * Log an error (always logs, not just in verbose mode) */ private logError; - /** - * Get the path to the shared state file for oneReport mode. - * Uses a file in the temp directory with a name based on the project ID. - */ - private getSharedStateFilePath; - /** - * Read shared state from file (for oneReport mode). - * Returns null if: - * - File doesn't exist - * - File is stale (older than 4 hours) - * - Previous run completed (activeWorkers === 0) - */ - private readSharedState; - /** - * Write shared state to file (for oneReport mode). - * Uses a lock file to prevent race conditions. - * Only writes the testRunId if the file doesn't exist yet (first writer wins). - * Updates testSuiteId if not already set. - */ - private writeSharedState; - /** - * Delete shared state file (cleanup after run completes). - */ - private deleteSharedState; - /** - * Increment the active worker count in shared state. - * Called when a worker starts using the shared test run. - */ - private incrementWorkerCount; - /** - * Decrement the active worker count in shared state. - * Returns true if this was the last worker (count reached 0). - */ - private decrementWorkerCount; /** * Track an async operation to prevent the runner from terminating early. * The operation is added to pendingOperations and removed when complete. @@ -456,4 +537,91 @@ declare class TestPlanItReporter extends WDIOReporter { getState(): ReporterState; } -export { type ReporterState, TestPlanItReporter, type TestPlanItReporterOptions, type TrackedTestResult, TestPlanItReporter as default }; +/** + * WebdriverIO Launcher Service for TestPlanIt. + * + * Manages the test run lifecycle in the main WDIO process: + * - onPrepare: Creates the test run and JUnit test suite ONCE before any workers start + * - onComplete: Completes the test run ONCE after all workers finish + * + * This ensures all spec files across all worker batches report to a single test run, + * regardless of `maxInstances` or execution order. + * + * @example + * ```javascript + * // wdio.conf.js + * import { TestPlanItService } from '@testplanit/wdio-reporter'; + * + * export const config = { + * services: [ + * [TestPlanItService, { + * domain: 'https://testplanit.example.com', + * apiToken: process.env.TESTPLANIT_API_TOKEN, + * projectId: 1, + * runName: 'E2E Tests - {date}', + * }] + * ], + * reporters: [ + * ['@testplanit/wdio-reporter', { + * domain: 'https://testplanit.example.com', + * apiToken: process.env.TESTPLANIT_API_TOKEN, + * projectId: 1, + * autoCreateTestCases: true, + * parentFolderId: 10, + * templateId: 1, + * }] + * ] + * } + * ``` + * + * @packageDocumentation + */ + +/** + * WebdriverIO Launcher Service for TestPlanIt. + * + * Creates a single test run before any workers start and completes it + * after all workers finish. Workers read the shared state file to find + * the pre-created test run and report results to it. + */ +declare class TestPlanItService { + private options; + private client; + private verbose; + private testRunId?; + private testSuiteId?; + constructor(serviceOptions: TestPlanItServiceOptions); + /** + * Log a message if verbose mode is enabled + */ + private log; + /** + * Log an error (always logs, not just in verbose mode) + */ + private logError; + /** + * Format run name with available placeholders. + * Note: {browser}, {spec}, and {suite} are NOT available in the service context + * since it runs before any workers start. + */ + private formatRunName; + /** + * Resolve string option IDs to numeric IDs using the API client. + */ + private resolveIds; + /** + * onPrepare - Runs once in the main process before any workers start. + * + * Creates the test run and JUnit test suite, then writes shared state + * so all worker reporters can find and use the pre-created run. + */ + onPrepare(): Promise; + /** + * onComplete - Runs once in the main process after all workers finish. + * + * Completes the test run and cleans up the shared state file. + */ + onComplete(exitCode: number): Promise; +} + +export { type ReporterState, TestPlanItReporter, type TestPlanItReporterOptions, TestPlanItService, type TestPlanItServiceOptions, type TrackedTestResult, TestPlanItReporter as default }; diff --git a/packages/wdio-testplanit-reporter/dist/index.js b/packages/wdio-testplanit-reporter/dist/index.js index 16043427..62ef597f 100644 --- a/packages/wdio-testplanit-reporter/dist/index.js +++ b/packages/wdio-testplanit-reporter/dist/index.js @@ -33,6 +33,112 @@ var fs__namespace = /*#__PURE__*/_interopNamespace(fs); var path__namespace = /*#__PURE__*/_interopNamespace(path); var os__namespace = /*#__PURE__*/_interopNamespace(os); +// src/reporter.ts +var STALE_THRESHOLD_MS = 4 * 60 * 60 * 1e3; +function getSharedStateFilePath(projectId) { + const fileName = `.testplanit-reporter-${projectId}.json`; + return path__namespace.join(os__namespace.tmpdir(), fileName); +} +function acquireLock(lockPath, maxAttempts = 10) { + for (let i = 0; i < maxAttempts; i++) { + try { + fs__namespace.writeFileSync(lockPath, process.pid.toString(), { flag: "wx" }); + return true; + } catch { + } + } + return false; +} +function releaseLock(lockPath) { + try { + fs__namespace.unlinkSync(lockPath); + } catch { + } +} +function withLock(projectId, callback) { + const filePath = getSharedStateFilePath(projectId); + const lockPath = `${filePath}.lock`; + if (!acquireLock(lockPath)) { + return void 0; + } + try { + return callback(filePath); + } finally { + releaseLock(lockPath); + } +} +function readSharedState(projectId) { + const filePath = getSharedStateFilePath(projectId); + try { + if (!fs__namespace.existsSync(filePath)) { + return null; + } + const content = fs__namespace.readFileSync(filePath, "utf-8"); + const state = JSON.parse(content); + const createdAt = new Date(state.createdAt); + const staleThreshold = new Date(Date.now() - STALE_THRESHOLD_MS); + if (createdAt < staleThreshold) { + deleteSharedState(projectId); + return null; + } + return state; + } catch { + return null; + } +} +function writeSharedState(projectId, state) { + withLock(projectId, (filePath) => { + fs__namespace.writeFileSync(filePath, JSON.stringify(state, null, 2)); + }); +} +function writeSharedStateIfAbsent(projectId, state) { + return withLock(projectId, (filePath) => { + if (fs__namespace.existsSync(filePath)) { + const content = fs__namespace.readFileSync(filePath, "utf-8"); + const existingState = JSON.parse(content); + if (!existingState.testSuiteId && state.testSuiteId) { + existingState.testSuiteId = state.testSuiteId; + fs__namespace.writeFileSync(filePath, JSON.stringify(existingState, null, 2)); + } + return existingState; + } + fs__namespace.writeFileSync(filePath, JSON.stringify(state, null, 2)); + return state; + }); +} +function deleteSharedState(projectId) { + const filePath = getSharedStateFilePath(projectId); + try { + if (fs__namespace.existsSync(filePath)) { + fs__namespace.unlinkSync(filePath); + } + } catch { + } +} +function incrementWorkerCount(projectId) { + withLock(projectId, (filePath) => { + if (fs__namespace.existsSync(filePath)) { + const content = fs__namespace.readFileSync(filePath, "utf-8"); + const state = JSON.parse(content); + state.activeWorkers = (state.activeWorkers || 0) + 1; + fs__namespace.writeFileSync(filePath, JSON.stringify(state, null, 2)); + } + }); +} +function decrementWorkerCount(projectId) { + const result = withLock(projectId, (filePath) => { + if (fs__namespace.existsSync(filePath)) { + const content = fs__namespace.readFileSync(filePath, "utf-8"); + const state = JSON.parse(content); + state.activeWorkers = Math.max(0, (state.activeWorkers || 1) - 1); + fs__namespace.writeFileSync(filePath, JSON.stringify(state, null, 2)); + return state.activeWorkers === 0; + } + return false; + }); + return result ?? false; +} + // src/reporter.ts var TestPlanItReporter = class extends WDIOReporter__default.default { client; @@ -46,6 +152,8 @@ var TestPlanItReporter = class extends WDIOReporter__default.default { currentTestUid = null; currentCid = null; pendingScreenshots = /* @__PURE__ */ new Map(); + /** When true, the TestPlanItService manages the test run lifecycle */ + managedByService = false; /** * WebdriverIO uses this getter to determine if the reporter has finished async operations. * The test runner will wait for this to return true before terminating. @@ -125,204 +233,6 @@ var TestPlanItReporter = class extends WDIOReporter__default.default { ${error.stack}` : ""; console.error(`[TestPlanIt] ERROR: ${message}`, errorMsg, stack); } - /** - * Get the path to the shared state file for oneReport mode. - * Uses a file in the temp directory with a name based on the project ID. - */ - getSharedStateFilePath() { - const fileName = `.testplanit-reporter-${this.reporterOptions.projectId}.json`; - return path__namespace.join(os__namespace.tmpdir(), fileName); - } - /** - * Read shared state from file (for oneReport mode). - * Returns null if: - * - File doesn't exist - * - File is stale (older than 4 hours) - * - Previous run completed (activeWorkers === 0) - */ - readSharedState() { - const filePath = this.getSharedStateFilePath(); - try { - if (!fs__namespace.existsSync(filePath)) { - return null; - } - const content = fs__namespace.readFileSync(filePath, "utf-8"); - const state = JSON.parse(content); - const createdAt = new Date(state.createdAt); - const fourHoursAgo = new Date(Date.now() - 4 * 60 * 60 * 1e3); - if (createdAt < fourHoursAgo) { - this.log("Shared state file is stale (older than 4 hours), starting fresh"); - this.deleteSharedState(); - return null; - } - if (state.activeWorkers === 0) { - this.log("Previous test run completed (activeWorkers=0), starting fresh"); - this.deleteSharedState(); - return null; - } - return state; - } catch (error) { - this.log("Failed to read shared state file:", error); - return null; - } - } - /** - * Write shared state to file (for oneReport mode). - * Uses a lock file to prevent race conditions. - * Only writes the testRunId if the file doesn't exist yet (first writer wins). - * Updates testSuiteId if not already set. - */ - writeSharedState(state) { - const filePath = this.getSharedStateFilePath(); - const lockPath = `${filePath}.lock`; - try { - let lockAcquired = false; - for (let i = 0; i < 10; i++) { - try { - fs__namespace.writeFileSync(lockPath, process.pid.toString(), { flag: "wx" }); - lockAcquired = true; - break; - } catch { - const sleepMs = 50 * Math.pow(2, i) + Math.random() * 50; - const start = Date.now(); - while (Date.now() - start < sleepMs) { - } - } - } - if (!lockAcquired) { - this.log("Could not acquire lock for shared state file"); - return; - } - try { - if (fs__namespace.existsSync(filePath)) { - const existingContent = fs__namespace.readFileSync(filePath, "utf-8"); - const existingState = JSON.parse(existingContent); - if (!existingState.testSuiteId && state.testSuiteId) { - existingState.testSuiteId = state.testSuiteId; - fs__namespace.writeFileSync(filePath, JSON.stringify(existingState, null, 2)); - this.log("Updated shared state file with testSuiteId:", state.testSuiteId); - } else { - this.log("Shared state file already exists with testSuiteId, not overwriting"); - } - return; - } - fs__namespace.writeFileSync(filePath, JSON.stringify(state, null, 2)); - this.log("Wrote shared state file:", filePath); - } finally { - try { - fs__namespace.unlinkSync(lockPath); - } catch { - } - } - } catch (error) { - this.log("Failed to write shared state file:", error); - } - } - /** - * Delete shared state file (cleanup after run completes). - */ - deleteSharedState() { - const filePath = this.getSharedStateFilePath(); - try { - if (fs__namespace.existsSync(filePath)) { - fs__namespace.unlinkSync(filePath); - this.log("Deleted shared state file"); - } - } catch (error) { - this.log("Failed to delete shared state file:", error); - } - } - /** - * Increment the active worker count in shared state. - * Called when a worker starts using the shared test run. - */ - incrementWorkerCount() { - const filePath = this.getSharedStateFilePath(); - const lockPath = `${filePath}.lock`; - try { - let lockAcquired = false; - for (let i = 0; i < 10; i++) { - try { - fs__namespace.writeFileSync(lockPath, process.pid.toString(), { flag: "wx" }); - lockAcquired = true; - break; - } catch { - const sleepMs = 50 * Math.pow(2, i) + Math.random() * 50; - const start = Date.now(); - while (Date.now() - start < sleepMs) { - } - } - } - if (!lockAcquired) { - this.log("Could not acquire lock to increment worker count"); - return; - } - try { - if (fs__namespace.existsSync(filePath)) { - const content = fs__namespace.readFileSync(filePath, "utf-8"); - const state = JSON.parse(content); - state.activeWorkers = (state.activeWorkers || 0) + 1; - fs__namespace.writeFileSync(filePath, JSON.stringify(state, null, 2)); - this.log("Incremented worker count to:", state.activeWorkers); - } - } finally { - try { - fs__namespace.unlinkSync(lockPath); - } catch { - } - } - } catch (error) { - this.log("Failed to increment worker count:", error); - } - } - /** - * Decrement the active worker count in shared state. - * Returns true if this was the last worker (count reached 0). - */ - decrementWorkerCount() { - const filePath = this.getSharedStateFilePath(); - const lockPath = `${filePath}.lock`; - try { - let lockAcquired = false; - for (let i = 0; i < 10; i++) { - try { - fs__namespace.writeFileSync(lockPath, process.pid.toString(), { flag: "wx" }); - lockAcquired = true; - break; - } catch { - const sleepMs = 50 * Math.pow(2, i) + Math.random() * 50; - const start = Date.now(); - while (Date.now() - start < sleepMs) { - } - } - } - if (!lockAcquired) { - this.log("Could not acquire lock to decrement worker count"); - return false; - } - try { - if (fs__namespace.existsSync(filePath)) { - const content = fs__namespace.readFileSync(filePath, "utf-8"); - const state = JSON.parse(content); - state.activeWorkers = Math.max(0, (state.activeWorkers || 1) - 1); - fs__namespace.writeFileSync(filePath, JSON.stringify(state, null, 2)); - this.log("Decremented worker count to:", state.activeWorkers); - if (state.activeWorkers === 0) { - this.log("This is the last worker"); - return true; - } - } - } finally { - try { - fs__namespace.unlinkSync(lockPath); - } catch { - } - } - } catch (error) { - this.log("Failed to decrement worker count:", error); - } - return false; - } /** * Track an async operation to prevent the runner from terminating early. * The operation is added to pendingOperations and removed when complete. @@ -357,47 +267,59 @@ ${error.stack}` : ""; this.log("Fetching status mappings..."); await this.fetchStatusMappings(); if (this.reporterOptions.oneReport && !this.state.testRunId) { - const sharedState = this.readSharedState(); + const sharedState = readSharedState(this.reporterOptions.projectId); if (sharedState) { - this.state.testRunId = sharedState.testRunId; - this.state.testSuiteId = sharedState.testSuiteId; - this.log(`Using shared test run from file: ${sharedState.testRunId}`); - try { - const testRun = await this.client.getTestRun(this.state.testRunId); - if (testRun.isDeleted) { - this.log(`Shared test run ${testRun.id} is deleted, starting fresh`); - this.state.testRunId = void 0; - this.state.testSuiteId = void 0; - this.deleteSharedState(); - } else if (testRun.isCompleted) { - this.log(`Shared test run ${testRun.id} is already completed, starting fresh`); + if (sharedState.managedByService) { + this.state.testRunId = sharedState.testRunId; + this.state.testSuiteId = sharedState.testSuiteId; + this.managedByService = true; + this.log(`Using service-managed test run: ${sharedState.testRunId}`); + } else { + this.state.testRunId = sharedState.testRunId; + this.state.testSuiteId = sharedState.testSuiteId; + this.log(`Using shared test run from file: ${sharedState.testRunId}`); + if (sharedState.activeWorkers === 0) { + this.log("Previous test run completed (activeWorkers=0), starting fresh"); + deleteSharedState(this.reporterOptions.projectId); this.state.testRunId = void 0; this.state.testSuiteId = void 0; - this.deleteSharedState(); } else { - this.log(`Validated shared test run: ${testRun.name} (ID: ${testRun.id})`); - this.incrementWorkerCount(); + try { + const testRun = await this.client.getTestRun(this.state.testRunId); + if (testRun.isDeleted) { + this.log(`Shared test run ${testRun.id} is deleted, starting fresh`); + this.state.testRunId = void 0; + this.state.testSuiteId = void 0; + deleteSharedState(this.reporterOptions.projectId); + } else if (testRun.isCompleted) { + this.log(`Shared test run ${testRun.id} is already completed, starting fresh`); + this.state.testRunId = void 0; + this.state.testSuiteId = void 0; + deleteSharedState(this.reporterOptions.projectId); + } else { + this.log(`Validated shared test run: ${testRun.name} (ID: ${testRun.id})`); + incrementWorkerCount(this.reporterOptions.projectId); + } + } catch { + this.log("Shared test run no longer exists, will create new one"); + this.state.testRunId = void 0; + this.state.testSuiteId = void 0; + deleteSharedState(this.reporterOptions.projectId); + } } - } catch { - this.log("Shared test run no longer exists, will create new one"); - this.state.testRunId = void 0; - this.state.testSuiteId = void 0; - this.deleteSharedState(); } } } - if (!this.state.testRunId) { + if (!this.state.testRunId && !this.managedByService) { if (this.reporterOptions.oneReport) { await this.createTestRun(); this.log(`Created test run with ID: ${this.state.testRunId}`); - this.writeSharedState({ + const finalState = writeSharedStateIfAbsent(this.reporterOptions.projectId, { testRunId: this.state.testRunId, testSuiteId: this.state.testSuiteId, createdAt: (/* @__PURE__ */ new Date()).toISOString(), activeWorkers: 1 - // First worker }); - const finalState = this.readSharedState(); if (finalState && finalState.testRunId !== this.state.testRunId) { this.log(`Another worker created test run first, switching from ${this.state.testRunId} to ${finalState.testRunId}`); this.state.testRunId = finalState.testRunId; @@ -407,7 +329,7 @@ ${error.stack}` : ""; await this.createTestRun(); this.log(`Created test run with ID: ${this.state.testRunId}`); } - } else if (!this.reporterOptions.oneReport) { + } else if (this.state.testRunId && !this.reporterOptions.oneReport && !this.managedByService) { try { const testRun = await this.client.getTestRun(this.state.testRunId); this.log(`Using existing test run: ${testRun.name} (ID: ${testRun.id})`); @@ -544,7 +466,7 @@ ${error.stack}` : ""; throw new Error("Cannot create JUnit test suite without a test run ID"); } if (this.reporterOptions.oneReport) { - const sharedState = this.readSharedState(); + const sharedState = readSharedState(this.reporterOptions.projectId); if (sharedState?.testSuiteId) { this.state.testSuiteId = sharedState.testSuiteId; this.log("Using shared JUnit test suite from file:", sharedState.testSuiteId); @@ -566,14 +488,12 @@ ${error.stack}` : ""; this.state.testSuiteId = testSuite.id; this.log("Created JUnit test suite with ID:", testSuite.id); if (this.reporterOptions.oneReport) { - this.writeSharedState({ + const finalState = writeSharedStateIfAbsent(this.reporterOptions.projectId, { testRunId: this.state.testRunId, testSuiteId: this.state.testSuiteId, createdAt: (/* @__PURE__ */ new Date()).toISOString(), activeWorkers: 1 - // Will be merged/updated by writeSharedState }); - const finalState = this.readSharedState(); if (finalState && finalState.testSuiteId !== this.state.testSuiteId) { this.log(`Another worker created test suite first, switching from ${this.state.testSuiteId} to ${finalState.testSuiteId}`); this.state.testSuiteId = finalState.testSuiteId; @@ -1027,15 +947,17 @@ ${error.stack}` : ""; await Promise.allSettled(uploadPromises); this.pendingScreenshots.clear(); } - if (this.reporterOptions.completeRunOnFinish) { + if (this.managedByService) { + this.log("Skipping test run completion (managed by TestPlanItService)"); + } else if (this.reporterOptions.completeRunOnFinish) { if (this.reporterOptions.oneReport) { - const isLastWorker = this.decrementWorkerCount(); + const isLastWorker = decrementWorkerCount(this.reporterOptions.projectId); if (isLastWorker) { const completeRunOp = (async () => { try { await this.client.completeTestRun(this.state.testRunId, this.reporterOptions.projectId); this.log("Test run completed (last worker):", this.state.testRunId); - this.deleteSharedState(); + deleteSharedState(this.reporterOptions.projectId); } catch (error) { this.logError("Failed to complete test run:", error); } @@ -1058,7 +980,7 @@ ${error.stack}` : ""; await completeRunOp; } } else if (this.reporterOptions.oneReport) { - this.decrementWorkerCount(); + decrementWorkerCount(this.reporterOptions.projectId); } const stats = this.state.stats; const duration = ((Date.now() - stats.startTime.getTime()) / 1e3).toFixed(1); @@ -1107,6 +1029,193 @@ ${error.stack}` : ""; return this.state; } }; +var TestPlanItService = class { + options; + client; + verbose; + testRunId; + testSuiteId; + constructor(serviceOptions) { + if (!serviceOptions.domain) { + throw new Error("TestPlanIt service: domain is required"); + } + if (!serviceOptions.apiToken) { + throw new Error("TestPlanIt service: apiToken is required"); + } + if (!serviceOptions.projectId) { + throw new Error("TestPlanIt service: projectId is required"); + } + this.options = { + completeRunOnFinish: true, + runName: "Automated Tests - {date} {time}", + testRunType: "MOCHA", + timeout: 3e4, + maxRetries: 3, + verbose: false, + ...serviceOptions + }; + this.verbose = this.options.verbose ?? false; + this.client = new api.TestPlanItClient({ + baseUrl: this.options.domain, + apiToken: this.options.apiToken, + timeout: this.options.timeout, + maxRetries: this.options.maxRetries + }); + } + /** + * Log a message if verbose mode is enabled + */ + log(message, ...args) { + if (this.verbose) { + console.log(`[TestPlanIt Service] ${message}`, ...args); + } + } + /** + * Log an error (always logs, not just in verbose mode) + */ + logError(message, error) { + const errorMsg = error instanceof Error ? error.message : String(error ?? ""); + console.error(`[TestPlanIt Service] ERROR: ${message}`, errorMsg); + } + /** + * Format run name with available placeholders. + * Note: {browser}, {spec}, and {suite} are NOT available in the service context + * since it runs before any workers start. + */ + formatRunName(template) { + const now = /* @__PURE__ */ new Date(); + const date = now.toISOString().split("T")[0]; + const time = now.toTimeString().split(" ")[0]; + const platform = process.platform; + return template.replace("{date}", date).replace("{time}", time).replace("{platform}", platform).replace("{browser}", "unknown").replace("{spec}", "unknown").replace("{suite}", "Tests"); + } + /** + * Resolve string option IDs to numeric IDs using the API client. + */ + async resolveIds() { + const projectId = this.options.projectId; + const resolved = {}; + if (typeof this.options.configId === "string") { + const config = await this.client.findConfigurationByName(projectId, this.options.configId); + if (!config) { + throw new Error(`Configuration not found: "${this.options.configId}"`); + } + resolved.configId = config.id; + this.log(`Resolved configuration "${this.options.configId}" -> ${config.id}`); + } else if (typeof this.options.configId === "number") { + resolved.configId = this.options.configId; + } + if (typeof this.options.milestoneId === "string") { + const milestone = await this.client.findMilestoneByName(projectId, this.options.milestoneId); + if (!milestone) { + throw new Error(`Milestone not found: "${this.options.milestoneId}"`); + } + resolved.milestoneId = milestone.id; + this.log(`Resolved milestone "${this.options.milestoneId}" -> ${milestone.id}`); + } else if (typeof this.options.milestoneId === "number") { + resolved.milestoneId = this.options.milestoneId; + } + if (typeof this.options.stateId === "string") { + const state = await this.client.findWorkflowStateByName(projectId, this.options.stateId); + if (!state) { + throw new Error(`Workflow state not found: "${this.options.stateId}"`); + } + resolved.stateId = state.id; + this.log(`Resolved workflow state "${this.options.stateId}" -> ${state.id}`); + } else if (typeof this.options.stateId === "number") { + resolved.stateId = this.options.stateId; + } + if (this.options.tagIds && this.options.tagIds.length > 0) { + resolved.tagIds = await this.client.resolveTagIds(projectId, this.options.tagIds); + this.log(`Resolved tags: ${resolved.tagIds.join(", ")}`); + } + return resolved; + } + /** + * onPrepare - Runs once in the main process before any workers start. + * + * Creates the test run and JUnit test suite, then writes shared state + * so all worker reporters can find and use the pre-created run. + */ + async onPrepare() { + this.log("Preparing test run..."); + this.log(` Domain: ${this.options.domain}`); + this.log(` Project ID: ${this.options.projectId}`); + try { + deleteSharedState(this.options.projectId); + const resolved = await this.resolveIds(); + const runName = this.formatRunName(this.options.runName ?? "Automated Tests - {date} {time}"); + this.log(`Creating test run: "${runName}" (type: ${this.options.testRunType})`); + const testRun = await this.client.createTestRun({ + projectId: this.options.projectId, + name: runName, + testRunType: this.options.testRunType, + configId: resolved.configId, + milestoneId: resolved.milestoneId, + stateId: resolved.stateId, + tagIds: resolved.tagIds + }); + this.testRunId = testRun.id; + this.log(`Created test run with ID: ${this.testRunId}`); + this.log("Creating JUnit test suite..."); + const testSuite = await this.client.createJUnitTestSuite({ + testRunId: this.testRunId, + name: runName, + time: 0, + tests: 0, + failures: 0, + errors: 0, + skipped: 0 + }); + this.testSuiteId = testSuite.id; + this.log(`Created JUnit test suite with ID: ${this.testSuiteId}`); + const sharedState = { + testRunId: this.testRunId, + testSuiteId: this.testSuiteId, + createdAt: (/* @__PURE__ */ new Date()).toISOString(), + activeWorkers: 0, + // Not used in service-managed mode + managedByService: true + }; + writeSharedState(this.options.projectId, sharedState); + this.log("Wrote shared state file for workers"); + console.log(`[TestPlanIt Service] Test run created: "${runName}" (ID: ${this.testRunId})`); + } catch (error) { + this.logError("Failed to prepare test run:", error); + deleteSharedState(this.options.projectId); + throw error; + } + } + /** + * onComplete - Runs once in the main process after all workers finish. + * + * Completes the test run and cleans up the shared state file. + */ + async onComplete(exitCode) { + this.log(`All workers finished (exit code: ${exitCode})`); + try { + if (this.testRunId && this.options.completeRunOnFinish) { + this.log(`Completing test run ${this.testRunId}...`); + await this.client.completeTestRun(this.testRunId, this.options.projectId); + this.log("Test run completed successfully"); + } + if (this.testRunId) { + console.log("\n[TestPlanIt Service] \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"); + console.log(`[TestPlanIt Service] Test Run ID: ${this.testRunId}`); + if (this.options.completeRunOnFinish) { + console.log("[TestPlanIt Service] Status: Completed"); + } + console.log(`[TestPlanIt Service] View: ${this.options.domain}/projects/runs/${this.options.projectId}/${this.testRunId}`); + console.log("[TestPlanIt Service] \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n"); + } + } catch (error) { + this.logError("Failed to complete test run:", error); + } finally { + deleteSharedState(this.options.projectId); + this.log("Cleaned up shared state file"); + } + } +}; Object.defineProperty(exports, "TestPlanItClient", { enumerable: true, @@ -1117,6 +1226,7 @@ Object.defineProperty(exports, "TestPlanItError", { get: function () { return api.TestPlanItError; } }); exports.TestPlanItReporter = TestPlanItReporter; +exports.TestPlanItService = TestPlanItService; exports.default = TestPlanItReporter; //# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/packages/wdio-testplanit-reporter/dist/index.js.map b/packages/wdio-testplanit-reporter/dist/index.js.map index 91389d07..e48409b8 100644 --- a/packages/wdio-testplanit-reporter/dist/index.js.map +++ b/packages/wdio-testplanit-reporter/dist/index.js.map @@ -1 +1 @@ -{"version":3,"sources":["../src/reporter.ts"],"names":["WDIOReporter","TestPlanItClient","path","os","fs"],"mappings":"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","file":"index.js","sourcesContent":["import WDIOReporter, { type RunnerStats, type SuiteStats, type TestStats, type AfterCommandArgs } from '@wdio/reporter';\nimport { TestPlanItClient } from '@testplanit/api';\nimport type { NormalizedStatus, JUnitResultType } from '@testplanit/api';\nimport type { TestPlanItReporterOptions, TrackedTestResult, ReporterState } from './types.js';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport * as os from 'os';\n\n/**\n * Shared state file for oneReport mode.\n * Contains the test run ID shared across all worker instances.\n */\ninterface SharedState {\n testRunId: number;\n testSuiteId?: number;\n createdAt: string;\n /** Number of active workers using this test run */\n activeWorkers: number;\n}\n\n/**\n * WebdriverIO Reporter for TestPlanIt\n *\n * Reports test results directly to your TestPlanIt instance.\n *\n * @example\n * ```javascript\n * // wdio.conf.js\n * export const config = {\n * reporters: [\n * ['@testplanit/wdio-reporter', {\n * domain: 'https://testplanit.example.com',\n * apiToken: process.env.TESTPLANIT_API_TOKEN,\n * projectId: 1,\n * runName: 'E2E Tests - {date}',\n * }]\n * ]\n * }\n * ```\n */\nexport default class TestPlanItReporter extends WDIOReporter {\n private client: TestPlanItClient;\n private reporterOptions: TestPlanItReporterOptions;\n private state: ReporterState;\n private currentSuite: string[] = [];\n private initPromise: Promise | null = null;\n private pendingOperations: Set> = new Set();\n private reportedResultCount = 0;\n private detectedFramework: string | null = null;\n private currentTestUid: string | null = null;\n private currentCid: string | null = null;\n private pendingScreenshots: Map = new Map();\n\n /**\n * WebdriverIO uses this getter to determine if the reporter has finished async operations.\n * The test runner will wait for this to return true before terminating.\n */\n get isSynchronised(): boolean {\n return this.pendingOperations.size === 0;\n }\n\n constructor(options: TestPlanItReporterOptions) {\n super(options);\n\n this.reporterOptions = {\n caseIdPattern: /\\[(\\d+)\\]/g,\n autoCreateTestCases: false,\n createFolderHierarchy: false,\n uploadScreenshots: true,\n includeStackTrace: true,\n completeRunOnFinish: true,\n oneReport: true,\n timeout: 30000,\n maxRetries: 3,\n verbose: false,\n ...options,\n };\n\n // Validate required options\n if (!this.reporterOptions.domain) {\n throw new Error('TestPlanIt reporter: domain is required');\n }\n if (!this.reporterOptions.apiToken) {\n throw new Error('TestPlanIt reporter: apiToken is required');\n }\n if (!this.reporterOptions.projectId) {\n throw new Error('TestPlanIt reporter: projectId is required');\n }\n\n // Initialize API client\n this.client = new TestPlanItClient({\n baseUrl: this.reporterOptions.domain,\n apiToken: this.reporterOptions.apiToken,\n timeout: this.reporterOptions.timeout,\n maxRetries: this.reporterOptions.maxRetries,\n });\n\n // Initialize state - testRunId will be resolved during initialization\n this.state = {\n testRunId: typeof this.reporterOptions.testRunId === 'number' ? this.reporterOptions.testRunId : undefined,\n resolvedIds: {},\n results: new Map(),\n caseIdMap: new Map(),\n testRunCaseMap: new Map(),\n folderPathMap: new Map(),\n statusIds: {},\n initialized: false,\n stats: {\n testCasesFound: 0,\n testCasesCreated: 0,\n testCasesMoved: 0,\n foldersCreated: 0,\n resultsPassed: 0,\n resultsFailed: 0,\n resultsSkipped: 0,\n screenshotsUploaded: 0,\n screenshotsFailed: 0,\n apiErrors: 0,\n apiRequests: 0,\n startTime: new Date(),\n },\n };\n }\n\n /**\n * Log a message if verbose mode is enabled\n */\n private log(message: string, ...args: unknown[]): void {\n if (this.reporterOptions.verbose) {\n console.log(`[TestPlanIt] ${message}`, ...args);\n }\n }\n\n /**\n * Log an error (always logs, not just in verbose mode)\n */\n private logError(message: string, error?: unknown): void {\n const errorMsg = error instanceof Error ? error.message : String(error ?? '');\n const stack = error instanceof Error && error.stack ? `\\n${error.stack}` : '';\n console.error(`[TestPlanIt] ERROR: ${message}`, errorMsg, stack);\n }\n\n /**\n * Get the path to the shared state file for oneReport mode.\n * Uses a file in the temp directory with a name based on the project ID.\n */\n private getSharedStateFilePath(): string {\n const fileName = `.testplanit-reporter-${this.reporterOptions.projectId}.json`;\n return path.join(os.tmpdir(), fileName);\n }\n\n /**\n * Read shared state from file (for oneReport mode).\n * Returns null if:\n * - File doesn't exist\n * - File is stale (older than 4 hours)\n * - Previous run completed (activeWorkers === 0)\n */\n private readSharedState(): SharedState | null {\n const filePath = this.getSharedStateFilePath();\n try {\n if (!fs.existsSync(filePath)) {\n return null;\n }\n const content = fs.readFileSync(filePath, 'utf-8');\n const state: SharedState = JSON.parse(content);\n\n // Check if state is stale (older than 4 hours)\n const createdAt = new Date(state.createdAt);\n const fourHoursAgo = new Date(Date.now() - 4 * 60 * 60 * 1000);\n if (createdAt < fourHoursAgo) {\n this.log('Shared state file is stale (older than 4 hours), starting fresh');\n this.deleteSharedState();\n return null;\n }\n\n // Check if previous run completed (no active workers)\n // This ensures each new test execution creates a new test run\n if (state.activeWorkers === 0) {\n this.log('Previous test run completed (activeWorkers=0), starting fresh');\n this.deleteSharedState();\n return null;\n }\n\n return state;\n } catch (error) {\n this.log('Failed to read shared state file:', error);\n return null;\n }\n }\n\n /**\n * Write shared state to file (for oneReport mode).\n * Uses a lock file to prevent race conditions.\n * Only writes the testRunId if the file doesn't exist yet (first writer wins).\n * Updates testSuiteId if not already set.\n */\n private writeSharedState(state: SharedState): void {\n const filePath = this.getSharedStateFilePath();\n const lockPath = `${filePath}.lock`;\n\n try {\n // Simple lock mechanism - try to create lock file exclusively\n let lockAcquired = false;\n for (let i = 0; i < 10; i++) {\n try {\n fs.writeFileSync(lockPath, process.pid.toString(), { flag: 'wx' });\n lockAcquired = true;\n break;\n } catch {\n // Lock exists, wait and retry with exponential backoff\n const sleepMs = 50 * Math.pow(2, i) + Math.random() * 50;\n // Use setTimeout-based sleep since Atomics.wait requires SharedArrayBuffer\n const start = Date.now();\n while (Date.now() - start < sleepMs) {\n // Busy wait (not ideal but works synchronously)\n }\n }\n }\n\n if (!lockAcquired) {\n this.log('Could not acquire lock for shared state file');\n return;\n }\n\n try {\n // Check if file already exists\n if (fs.existsSync(filePath)) {\n // Read existing state and merge - only update testSuiteId if not set\n const existingContent = fs.readFileSync(filePath, 'utf-8');\n const existingState: SharedState = JSON.parse(existingContent);\n\n // Only update if the testSuiteId is missing and we have one to add\n if (!existingState.testSuiteId && state.testSuiteId) {\n existingState.testSuiteId = state.testSuiteId;\n fs.writeFileSync(filePath, JSON.stringify(existingState, null, 2));\n this.log('Updated shared state file with testSuiteId:', state.testSuiteId);\n } else {\n this.log('Shared state file already exists with testSuiteId, not overwriting');\n }\n return;\n }\n fs.writeFileSync(filePath, JSON.stringify(state, null, 2));\n this.log('Wrote shared state file:', filePath);\n } finally {\n // Release lock\n try {\n fs.unlinkSync(lockPath);\n } catch {\n // Ignore lock removal errors\n }\n }\n } catch (error) {\n this.log('Failed to write shared state file:', error);\n }\n }\n\n /**\n * Delete shared state file (cleanup after run completes).\n */\n private deleteSharedState(): void {\n const filePath = this.getSharedStateFilePath();\n try {\n if (fs.existsSync(filePath)) {\n fs.unlinkSync(filePath);\n this.log('Deleted shared state file');\n }\n } catch (error) {\n this.log('Failed to delete shared state file:', error);\n }\n }\n\n /**\n * Increment the active worker count in shared state.\n * Called when a worker starts using the shared test run.\n */\n private incrementWorkerCount(): void {\n const filePath = this.getSharedStateFilePath();\n const lockPath = `${filePath}.lock`;\n\n try {\n // Acquire lock\n let lockAcquired = false;\n for (let i = 0; i < 10; i++) {\n try {\n fs.writeFileSync(lockPath, process.pid.toString(), { flag: 'wx' });\n lockAcquired = true;\n break;\n } catch {\n const sleepMs = 50 * Math.pow(2, i) + Math.random() * 50;\n const start = Date.now();\n while (Date.now() - start < sleepMs) {\n // Busy wait\n }\n }\n }\n\n if (!lockAcquired) {\n this.log('Could not acquire lock to increment worker count');\n return;\n }\n\n try {\n if (fs.existsSync(filePath)) {\n const content = fs.readFileSync(filePath, 'utf-8');\n const state: SharedState = JSON.parse(content);\n state.activeWorkers = (state.activeWorkers || 0) + 1;\n fs.writeFileSync(filePath, JSON.stringify(state, null, 2));\n this.log('Incremented worker count to:', state.activeWorkers);\n }\n } finally {\n try {\n fs.unlinkSync(lockPath);\n } catch {\n // Ignore\n }\n }\n } catch (error) {\n this.log('Failed to increment worker count:', error);\n }\n }\n\n /**\n * Decrement the active worker count in shared state.\n * Returns true if this was the last worker (count reached 0).\n */\n private decrementWorkerCount(): boolean {\n const filePath = this.getSharedStateFilePath();\n const lockPath = `${filePath}.lock`;\n\n try {\n // Acquire lock\n let lockAcquired = false;\n for (let i = 0; i < 10; i++) {\n try {\n fs.writeFileSync(lockPath, process.pid.toString(), { flag: 'wx' });\n lockAcquired = true;\n break;\n } catch {\n const sleepMs = 50 * Math.pow(2, i) + Math.random() * 50;\n const start = Date.now();\n while (Date.now() - start < sleepMs) {\n // Busy wait\n }\n }\n }\n\n if (!lockAcquired) {\n this.log('Could not acquire lock to decrement worker count');\n return false;\n }\n\n try {\n if (fs.existsSync(filePath)) {\n const content = fs.readFileSync(filePath, 'utf-8');\n const state: SharedState = JSON.parse(content);\n state.activeWorkers = Math.max(0, (state.activeWorkers || 1) - 1);\n fs.writeFileSync(filePath, JSON.stringify(state, null, 2));\n this.log('Decremented worker count to:', state.activeWorkers);\n\n if (state.activeWorkers === 0) {\n this.log('This is the last worker');\n return true;\n }\n }\n } finally {\n try {\n fs.unlinkSync(lockPath);\n } catch {\n // Ignore\n }\n }\n } catch (error) {\n this.log('Failed to decrement worker count:', error);\n }\n return false;\n }\n\n /**\n * Track an async operation to prevent the runner from terminating early.\n * The operation is added to pendingOperations and removed when complete.\n * WebdriverIO checks isSynchronised and waits until all operations finish.\n */\n private trackOperation(operation: Promise): void {\n this.pendingOperations.add(operation);\n operation.finally(() => {\n this.pendingOperations.delete(operation);\n });\n }\n\n /**\n * Initialize the reporter (create test run, fetch statuses)\n */\n private async initialize(): Promise {\n // If already initialized successfully, return immediately\n if (this.state.initialized) return;\n\n // If we have a previous error, throw it again to prevent retrying\n if (this.state.initError) {\n throw this.state.initError;\n }\n\n // If initialization is in progress, wait for it\n if (this.initPromise) return this.initPromise;\n\n this.initPromise = this.doInitialize();\n return this.initPromise;\n }\n\n private async doInitialize(): Promise {\n try {\n // Log initialization start (only happens when we have results to report)\n this.log('Initializing reporter...');\n this.log(` Domain: ${this.reporterOptions.domain}`);\n this.log(` Project ID: ${this.reporterOptions.projectId}`);\n this.log(` oneReport: ${this.reporterOptions.oneReport}`);\n\n // Resolve any string IDs to numeric IDs\n this.log('Resolving option IDs...');\n await this.resolveOptionIds();\n\n // Fetch status mappings\n this.log('Fetching status mappings...');\n await this.fetchStatusMappings();\n\n // Handle oneReport mode - check for existing shared state\n if (this.reporterOptions.oneReport && !this.state.testRunId) {\n const sharedState = this.readSharedState();\n if (sharedState) {\n this.state.testRunId = sharedState.testRunId;\n this.state.testSuiteId = sharedState.testSuiteId;\n this.log(`Using shared test run from file: ${sharedState.testRunId}`);\n // Validate the shared test run still exists, is not completed, and is not deleted\n try {\n const testRun = await this.client.getTestRun(this.state.testRunId);\n if (testRun.isDeleted) {\n // Test run was soft-deleted\n this.log(`Shared test run ${testRun.id} is deleted, starting fresh`);\n this.state.testRunId = undefined;\n this.state.testSuiteId = undefined;\n this.deleteSharedState();\n } else if (testRun.isCompleted) {\n // Test run was already completed (from a previous execution)\n this.log(`Shared test run ${testRun.id} is already completed, starting fresh`);\n this.state.testRunId = undefined;\n this.state.testSuiteId = undefined;\n this.deleteSharedState();\n } else {\n this.log(`Validated shared test run: ${testRun.name} (ID: ${testRun.id})`);\n // Increment worker count since we're joining an existing run\n this.incrementWorkerCount();\n }\n } catch {\n // Shared test run no longer exists, clear state and create new one\n this.log('Shared test run no longer exists, will create new one');\n this.state.testRunId = undefined;\n this.state.testSuiteId = undefined;\n this.deleteSharedState();\n }\n }\n }\n\n // Create or validate test run\n if (!this.state.testRunId) {\n // In oneReport mode, use atomic write to prevent race conditions\n if (this.reporterOptions.oneReport) {\n // Create the test run first\n await this.createTestRun();\n this.log(`Created test run with ID: ${this.state.testRunId}`);\n\n // Try to write shared state - this will fail if another worker already wrote\n this.writeSharedState({\n testRunId: this.state.testRunId!,\n testSuiteId: this.state.testSuiteId,\n createdAt: new Date().toISOString(),\n activeWorkers: 1, // First worker\n });\n\n // Re-check shared state to see if we won the race\n const finalState = this.readSharedState();\n if (finalState && finalState.testRunId !== this.state.testRunId) {\n // Another worker created a test run first - use theirs instead\n this.log(`Another worker created test run first, switching from ${this.state.testRunId} to ${finalState.testRunId}`);\n this.state.testRunId = finalState.testRunId;\n this.state.testSuiteId = finalState.testSuiteId;\n }\n } else {\n await this.createTestRun();\n this.log(`Created test run with ID: ${this.state.testRunId}`);\n }\n } else if (!this.reporterOptions.oneReport) {\n // Only validate if not using oneReport (already validated above)\n // Validate existing test run\n try {\n const testRun = await this.client.getTestRun(this.state.testRunId);\n this.log(`Using existing test run: ${testRun.name} (ID: ${testRun.id})`);\n } catch (error) {\n throw new Error(`Test run ${this.state.testRunId} not found or not accessible`);\n }\n }\n\n this.state.initialized = true;\n this.log('Reporter initialized successfully');\n } catch (error) {\n this.state.initError = error instanceof Error ? error : new Error(String(error));\n this.logError('Failed to initialize reporter:', error);\n throw error;\n }\n }\n\n /**\n * Resolve option names to numeric IDs\n */\n private async resolveOptionIds(): Promise {\n const projectId = this.reporterOptions.projectId;\n\n // Resolve testRunId if it's a string\n if (typeof this.reporterOptions.testRunId === 'string') {\n const testRun = await this.client.findTestRunByName(projectId, this.reporterOptions.testRunId);\n if (!testRun) {\n throw new Error(`Test run not found: \"${this.reporterOptions.testRunId}\"`);\n }\n this.state.testRunId = testRun.id;\n this.state.resolvedIds.testRunId = testRun.id;\n this.log(`Resolved test run \"${this.reporterOptions.testRunId}\" -> ${testRun.id}`);\n }\n\n // Resolve configId if it's a string\n if (typeof this.reporterOptions.configId === 'string') {\n const config = await this.client.findConfigurationByName(projectId, this.reporterOptions.configId);\n if (!config) {\n throw new Error(`Configuration not found: \"${this.reporterOptions.configId}\"`);\n }\n this.state.resolvedIds.configId = config.id;\n this.log(`Resolved configuration \"${this.reporterOptions.configId}\" -> ${config.id}`);\n } else if (typeof this.reporterOptions.configId === 'number') {\n this.state.resolvedIds.configId = this.reporterOptions.configId;\n }\n\n // Resolve milestoneId if it's a string\n if (typeof this.reporterOptions.milestoneId === 'string') {\n const milestone = await this.client.findMilestoneByName(projectId, this.reporterOptions.milestoneId);\n if (!milestone) {\n throw new Error(`Milestone not found: \"${this.reporterOptions.milestoneId}\"`);\n }\n this.state.resolvedIds.milestoneId = milestone.id;\n this.log(`Resolved milestone \"${this.reporterOptions.milestoneId}\" -> ${milestone.id}`);\n } else if (typeof this.reporterOptions.milestoneId === 'number') {\n this.state.resolvedIds.milestoneId = this.reporterOptions.milestoneId;\n }\n\n // Resolve stateId if it's a string\n if (typeof this.reporterOptions.stateId === 'string') {\n const state = await this.client.findWorkflowStateByName(projectId, this.reporterOptions.stateId);\n if (!state) {\n throw new Error(`Workflow state not found: \"${this.reporterOptions.stateId}\"`);\n }\n this.state.resolvedIds.stateId = state.id;\n this.log(`Resolved workflow state \"${this.reporterOptions.stateId}\" -> ${state.id}`);\n } else if (typeof this.reporterOptions.stateId === 'number') {\n this.state.resolvedIds.stateId = this.reporterOptions.stateId;\n }\n\n // Resolve parentFolderId if it's a string\n if (typeof this.reporterOptions.parentFolderId === 'string') {\n let folder = await this.client.findFolderByName(projectId, this.reporterOptions.parentFolderId);\n if (!folder) {\n // If createFolderHierarchy is enabled, create the parent folder\n if (this.reporterOptions.createFolderHierarchy) {\n this.log(`Parent folder \"${this.reporterOptions.parentFolderId}\" not found, creating it...`);\n folder = await this.client.createFolder({\n projectId,\n name: this.reporterOptions.parentFolderId,\n });\n this.log(`Created parent folder \"${this.reporterOptions.parentFolderId}\" -> ${folder.id}`);\n } else {\n throw new Error(`Folder not found: \"${this.reporterOptions.parentFolderId}\"`);\n }\n }\n this.state.resolvedIds.parentFolderId = folder.id;\n this.log(`Resolved folder \"${this.reporterOptions.parentFolderId}\" -> ${folder.id}`);\n } else if (typeof this.reporterOptions.parentFolderId === 'number') {\n this.state.resolvedIds.parentFolderId = this.reporterOptions.parentFolderId;\n }\n\n // Resolve templateId if it's a string\n if (typeof this.reporterOptions.templateId === 'string') {\n const template = await this.client.findTemplateByName(projectId, this.reporterOptions.templateId);\n if (!template) {\n throw new Error(`Template not found: \"${this.reporterOptions.templateId}\"`);\n }\n this.state.resolvedIds.templateId = template.id;\n this.log(`Resolved template \"${this.reporterOptions.templateId}\" -> ${template.id}`);\n } else if (typeof this.reporterOptions.templateId === 'number') {\n this.state.resolvedIds.templateId = this.reporterOptions.templateId;\n }\n\n // Resolve tagIds if they contain strings\n if (this.reporterOptions.tagIds && this.reporterOptions.tagIds.length > 0) {\n this.state.resolvedIds.tagIds = await this.client.resolveTagIds(projectId, this.reporterOptions.tagIds);\n this.log(`Resolved tags: ${this.state.resolvedIds.tagIds.join(', ')}`);\n }\n }\n\n /**\n * Fetch status ID mappings from TestPlanIt\n */\n private async fetchStatusMappings(): Promise {\n const statuses: NormalizedStatus[] = ['passed', 'failed', 'skipped', 'blocked'];\n\n for (const status of statuses) {\n const statusId = await this.client.getStatusId(this.reporterOptions.projectId, status);\n if (statusId) {\n this.state.statusIds[status] = statusId;\n this.log(`Status mapping: ${status} -> ${statusId}`);\n }\n }\n\n if (!this.state.statusIds.passed || !this.state.statusIds.failed) {\n throw new Error('Could not find required status mappings (passed/failed) in TestPlanIt');\n }\n }\n\n /**\n * Map test status to JUnit result type\n */\n private mapStatusToJUnitType(status: 'passed' | 'failed' | 'skipped' | 'pending'): JUnitResultType {\n switch (status) {\n case 'passed':\n return 'PASSED';\n case 'failed':\n return 'FAILURE';\n case 'skipped':\n case 'pending':\n return 'SKIPPED';\n default:\n return 'FAILURE';\n }\n }\n\n /**\n * Create the JUnit test suite for this test run\n */\n private async createJUnitTestSuite(): Promise {\n if (this.state.testSuiteId) {\n return; // Already created (either from shared state or previous call)\n }\n\n if (!this.state.testRunId) {\n throw new Error('Cannot create JUnit test suite without a test run ID');\n }\n\n // In oneReport mode, check if another worker has already created a suite\n if (this.reporterOptions.oneReport) {\n const sharedState = this.readSharedState();\n if (sharedState?.testSuiteId) {\n this.state.testSuiteId = sharedState.testSuiteId;\n this.log('Using shared JUnit test suite from file:', sharedState.testSuiteId);\n return;\n }\n }\n\n const runName = this.formatRunName(this.reporterOptions.runName || '{suite} - {date} {time}');\n\n this.log('Creating JUnit test suite...');\n\n const testSuite = await this.client.createJUnitTestSuite({\n testRunId: this.state.testRunId,\n name: runName,\n time: 0, // Will be updated incrementally\n tests: 0,\n failures: 0,\n errors: 0,\n skipped: 0,\n });\n\n this.state.testSuiteId = testSuite.id;\n this.log('Created JUnit test suite with ID:', testSuite.id);\n\n // Update shared state with suite ID if in oneReport mode\n if (this.reporterOptions.oneReport) {\n this.writeSharedState({\n testRunId: this.state.testRunId,\n testSuiteId: this.state.testSuiteId,\n createdAt: new Date().toISOString(),\n activeWorkers: 1, // Will be merged/updated by writeSharedState\n });\n\n // Re-check to handle race condition - if another worker wrote first, use their suite\n const finalState = this.readSharedState();\n if (finalState && finalState.testSuiteId !== this.state.testSuiteId) {\n this.log(`Another worker created test suite first, switching from ${this.state.testSuiteId} to ${finalState.testSuiteId}`);\n this.state.testSuiteId = finalState.testSuiteId;\n }\n }\n }\n\n /**\n * Map WebdriverIO framework name to TestPlanIt test run type\n */\n private getTestRunType(): TestPlanItReporterOptions['testRunType'] {\n // If explicitly set by user, use that\n if (this.reporterOptions.testRunType) {\n return this.reporterOptions.testRunType;\n }\n\n // Auto-detect from WebdriverIO framework config\n if (this.detectedFramework) {\n const framework = this.detectedFramework.toLowerCase();\n if (framework === 'mocha') return 'MOCHA';\n if (framework === 'cucumber') return 'CUCUMBER';\n // jasmine and others map to REGULAR\n return 'REGULAR';\n }\n\n // Default fallback\n return 'MOCHA';\n }\n\n /**\n * Create a new test run\n */\n private async createTestRun(): Promise {\n const runName = this.formatRunName(this.reporterOptions.runName || '{suite} - {date} {time}');\n const testRunType = this.getTestRunType();\n\n this.log('Creating test run:', runName, '(type:', testRunType + ')');\n\n const testRun = await this.client.createTestRun({\n projectId: this.reporterOptions.projectId,\n name: runName,\n testRunType,\n configId: this.state.resolvedIds.configId,\n milestoneId: this.state.resolvedIds.milestoneId,\n stateId: this.state.resolvedIds.stateId,\n tagIds: this.state.resolvedIds.tagIds,\n });\n\n this.state.testRunId = testRun.id;\n this.log('Created test run with ID:', testRun.id);\n }\n\n /**\n * Format the run name with placeholders\n */\n private formatRunName(template: string): string {\n const now = new Date();\n const date = now.toISOString().split('T')[0];\n const time = now.toTimeString().split(' ')[0];\n const browser = this.state.capabilities?.browserName || 'unknown';\n const platform = this.state.capabilities?.platformName || process.platform;\n\n // Get spec file name from currentSpec (e.g., \"/path/to/test.spec.ts\" -> \"test.spec.ts\")\n let spec = 'unknown';\n if (this.currentSpec) {\n const parts = this.currentSpec.split('/');\n spec = parts[parts.length - 1] || 'unknown';\n // Remove common extensions for cleaner names\n spec = spec.replace(/\\.(spec|test)\\.(ts|js|mjs|cjs)$/, '');\n }\n\n // Get the root suite name (first describe block)\n const suite = this.currentSuite[0] || 'Tests';\n\n return template\n .replace('{date}', date)\n .replace('{time}', time)\n .replace('{browser}', browser)\n .replace('{platform}', platform)\n .replace('{spec}', spec)\n .replace('{suite}', suite);\n }\n\n /**\n * Parse case IDs from test title using the configured pattern\n * @example With default pattern: \"[1761] [1762] should load the page\" -> [1761, 1762]\n * @example With C-prefix pattern: \"C12345 C67890 should load the page\" -> [12345, 67890]\n */\n private parseCaseIds(title: string): { caseIds: number[]; cleanTitle: string } {\n const pattern = this.reporterOptions.caseIdPattern || /\\[(\\d+)\\]/g;\n const regex = typeof pattern === 'string' ? new RegExp(pattern, 'g') : new RegExp(pattern.source, 'g');\n const caseIds: number[] = [];\n let match: RegExpExecArray | null;\n\n while ((match = regex.exec(title)) !== null) {\n // Find the first capturing group that has a value (supports patterns with multiple groups)\n for (let i = 1; i < match.length; i++) {\n if (match[i]) {\n caseIds.push(parseInt(match[i], 10));\n break;\n }\n }\n }\n\n // Remove matched patterns from title\n const cleanTitle = title.replace(regex, '').trim().replace(/\\s+/g, ' ');\n\n return { caseIds, cleanTitle };\n }\n\n /**\n * Get the full suite path as a string\n */\n private getFullSuiteName(): string {\n return this.currentSuite.join(' > ');\n }\n\n /**\n * Create a unique key for a test case\n */\n private createCaseKey(suiteName: string, testName: string): string {\n return `${suiteName}::${testName}`;\n }\n\n // ============================================================================\n // WebdriverIO Reporter Hooks\n // ============================================================================\n\n onRunnerStart(runner: RunnerStats): void {\n this.log('Runner started:', runner.cid);\n this.state.capabilities = runner.capabilities as WebdriverIO.Capabilities;\n\n // Auto-detect the test framework from WebdriverIO config\n // This is accessed via runner.config.framework (e.g., 'mocha', 'cucumber', 'jasmine')\n const config = runner.config as { framework?: string } | undefined;\n if (config?.framework) {\n this.detectedFramework = config.framework;\n this.log('Detected framework:', this.detectedFramework);\n }\n\n // Don't initialize here - wait until we have actual test results to report\n // This avoids creating empty test runs for specs with no matching tests\n }\n\n onSuiteStart(suite: SuiteStats): void {\n if (suite.title) {\n this.currentSuite.push(suite.title);\n this.log('Suite started:', this.getFullSuiteName());\n }\n }\n\n onSuiteEnd(suite: SuiteStats): void {\n if (suite.title) {\n this.log('Suite ended:', this.getFullSuiteName());\n this.currentSuite.pop();\n }\n }\n\n onTestStart(test: TestStats): void {\n this.log('Test started:', test.title);\n // Track the current test for screenshot association\n const { cleanTitle } = this.parseCaseIds(test.title);\n const suiteName = this.getFullSuiteName();\n const fullTitle = suiteName ? `${suiteName} > ${cleanTitle}` : cleanTitle;\n this.currentTestUid = `${test.cid}_${fullTitle}`;\n this.currentCid = test.cid;\n }\n\n /**\n * Capture screenshots from WebdriverIO commands\n */\n onAfterCommand(commandArgs: AfterCommandArgs): void {\n // Check if this is a screenshot command\n if (!this.reporterOptions.uploadScreenshots) {\n return;\n }\n\n // WebdriverIO uses 'takeScreenshot' as the command name or '/screenshot' endpoint\n const isScreenshotCommand =\n commandArgs.command === 'takeScreenshot' ||\n commandArgs.command === 'saveScreenshot' ||\n commandArgs.endpoint?.includes('/screenshot');\n\n if (!isScreenshotCommand) {\n return;\n }\n\n this.log(`Screenshot command detected: ${commandArgs.command}, endpoint: ${commandArgs.endpoint}`);\n\n // For saveScreenshot, the result is the file path, not base64 data\n // We need to handle both takeScreenshot (returns base64) and saveScreenshot (saves to file)\n const result = commandArgs.result as Record | string | undefined;\n const resultValue = (typeof result === 'object' && result !== null ? result.value : result) ?? result;\n\n if (!resultValue) {\n this.log('No result value in screenshot command');\n return;\n }\n\n // The result should be base64-encoded screenshot data\n const screenshotData = resultValue as string;\n if (typeof screenshotData !== 'string') {\n this.log(`Screenshot result is not a string: ${typeof screenshotData}`);\n return;\n }\n\n // Check if this looks like a file path rather than base64 data\n // File paths start with / (Unix) or drive letter like C:\\ (Windows)\n // Base64 PNG data starts with \"iVBORw0KGgo\" (PNG header)\n const looksLikeFilePath =\n screenshotData.startsWith('/') ||\n /^[A-Za-z]:[\\\\\\/]/.test(screenshotData) ||\n screenshotData.startsWith('./') ||\n screenshotData.startsWith('../');\n\n if (looksLikeFilePath) {\n this.log(`Screenshot result appears to be a file path: ${screenshotData.substring(0, 100)}`);\n return;\n }\n\n // Store the screenshot associated with the current test\n if (this.currentTestUid) {\n const buffer = Buffer.from(screenshotData, 'base64');\n const existing = this.pendingScreenshots.get(this.currentTestUid) || [];\n existing.push(buffer);\n this.pendingScreenshots.set(this.currentTestUid, existing);\n this.log('Captured screenshot for test:', this.currentTestUid, `(${buffer.length} bytes)`);\n } else {\n this.log('No current test UID to associate screenshot with');\n }\n }\n\n onTestPass(test: TestStats): void {\n this.handleTestEnd(test, 'passed');\n }\n\n onTestFail(test: TestStats): void {\n this.handleTestEnd(test, 'failed');\n }\n\n onTestSkip(test: TestStats): void {\n this.handleTestEnd(test, 'skipped');\n }\n\n /**\n * Handle test completion\n */\n private handleTestEnd(test: TestStats, status: 'passed' | 'failed' | 'skipped'): void {\n const { caseIds, cleanTitle } = this.parseCaseIds(test.title);\n const suiteName = this.getFullSuiteName();\n const suitePath = [...this.currentSuite]; // Copy the current suite hierarchy\n const fullTitle = suiteName ? `${suiteName} > ${cleanTitle}` : cleanTitle;\n const uid = `${test.cid}_${fullTitle}`;\n\n // Calculate duration from timestamps for reliability\n // WebdriverIO's test.duration can be inconsistent in some versions\n const startTime = new Date(test.start).getTime();\n const endTime = test.end ? new Date(test.end).getTime() : Date.now();\n const durationMs = endTime - startTime;\n\n // Format WebdriverIO command output if available\n let commandOutput: string | undefined;\n if (test.output && test.output.length > 0) {\n commandOutput = test.output\n .map((o) => {\n const parts: string[] = [];\n if (o.method) parts.push(`[${o.method}]`);\n if (o.endpoint) parts.push(o.endpoint);\n if (o.result !== undefined) {\n const resultStr = typeof o.result === 'string' ? o.result : JSON.stringify(o.result);\n // Truncate long results\n parts.push(resultStr.length > 200 ? resultStr.substring(0, 200) + '...' : resultStr);\n }\n return parts.join(' ');\n })\n .join('\\n');\n }\n\n const result: TrackedTestResult = {\n caseId: caseIds[0], // Primary case ID\n suiteName,\n suitePath,\n testName: cleanTitle,\n fullTitle,\n originalTitle: test.title,\n status,\n duration: durationMs,\n errorMessage: test.error?.message,\n stackTrace: this.reporterOptions.includeStackTrace ? test.error?.stack : undefined,\n startedAt: new Date(test.start),\n finishedAt: new Date(endTime),\n browser: this.state.capabilities?.browserName,\n platform: this.state.capabilities?.platformName || process.platform,\n screenshots: [],\n retryAttempt: test.retries || 0,\n uid,\n specFile: this.currentSpec,\n commandOutput,\n };\n\n this.state.results.set(uid, result);\n this.log(`Test ${status}:`, cleanTitle, caseIds.length > 0 ? `(Case IDs: ${caseIds.join(', ')})` : '');\n\n // Report result asynchronously - track operation so WebdriverIO waits for completion\n const reportPromise = this.reportResult(result, caseIds);\n this.trackOperation(reportPromise);\n }\n\n /**\n * Report a single test result to TestPlanIt\n */\n private async reportResult(result: TrackedTestResult, caseIds: number[]): Promise {\n try {\n // Check if this result can be reported BEFORE initializing\n // This prevents creating empty test runs for tests without case IDs\n if (caseIds.length === 0 && !this.reporterOptions.autoCreateTestCases) {\n console.warn(`[TestPlanIt] WARNING: Skipping \"${result.testName}\" - no case ID found and autoCreateTestCases is disabled. Set autoCreateTestCases: true to automatically find or create test cases by name.`);\n return;\n }\n\n // Now we know this result can be reported, so initialize if needed\n await this.initialize();\n\n if (!this.state.testRunId) {\n this.logError('No test run ID available, skipping result');\n return;\n }\n\n // Create JUnit test suite if not already created\n await this.createJUnitTestSuite();\n\n if (!this.state.testSuiteId) {\n this.logError('No test suite ID available, skipping result');\n return;\n }\n\n // Get or create repository case\n let repositoryCaseId: number | undefined;\n const caseKey = this.createCaseKey(result.suiteName, result.testName);\n\n // DEBUG: Always log key info about this test\n this.log('DEBUG: Processing test:', result.testName);\n this.log('DEBUG: suiteName:', result.suiteName);\n this.log('DEBUG: suitePath:', JSON.stringify(result.suitePath));\n this.log('DEBUG: caseIds from title:', JSON.stringify(caseIds));\n this.log('DEBUG: autoCreateTestCases:', this.reporterOptions.autoCreateTestCases);\n this.log('DEBUG: createFolderHierarchy:', this.reporterOptions.createFolderHierarchy);\n\n if (caseIds.length > 0) {\n // Use the provided case ID directly as repository case ID\n repositoryCaseId = caseIds[0];\n this.log('DEBUG: Using case ID from title:', repositoryCaseId);\n } else if (this.reporterOptions.autoCreateTestCases) {\n // Check cache first\n if (this.state.caseIdMap.has(caseKey)) {\n repositoryCaseId = this.state.caseIdMap.get(caseKey);\n this.log('DEBUG: Found in cache:', caseKey, '->', repositoryCaseId);\n } else {\n // Determine the target folder ID\n let folderId = this.state.resolvedIds.parentFolderId;\n const templateId = this.state.resolvedIds.templateId;\n\n this.log('DEBUG: Initial folderId (parentFolderId):', folderId);\n this.log('DEBUG: templateId:', templateId);\n\n if (!folderId || !templateId) {\n this.logError('autoCreateTestCases requires parentFolderId and templateId');\n return;\n }\n\n // Create folder hierarchy based on suite structure if enabled\n this.log('DEBUG: Checking folder hierarchy - createFolderHierarchy:', this.reporterOptions.createFolderHierarchy, 'suitePath.length:', result.suitePath.length);\n if (this.reporterOptions.createFolderHierarchy && result.suitePath.length > 0) {\n const folderPathKey = result.suitePath.join(' > ');\n this.log('DEBUG: Will create folder hierarchy for path:', folderPathKey);\n\n // Check folder cache first\n if (this.state.folderPathMap.has(folderPathKey)) {\n folderId = this.state.folderPathMap.get(folderPathKey)!;\n this.log('Using cached folder ID for path:', folderPathKey, '->', folderId);\n } else {\n // Create the folder hierarchy\n this.log('Creating folder hierarchy:', result.suitePath.join(' > '));\n this.log('DEBUG: Calling findOrCreateFolderPath with projectId:', this.reporterOptions.projectId, 'suitePath:', JSON.stringify(result.suitePath), 'parentFolderId:', this.state.resolvedIds.parentFolderId);\n const folder = await this.client.findOrCreateFolderPath(\n this.reporterOptions.projectId,\n result.suitePath,\n this.state.resolvedIds.parentFolderId\n );\n folderId = folder.id;\n this.state.folderPathMap.set(folderPathKey, folderId);\n this.log('Created/found folder:', folder.name, '(ID:', folder.id + ')');\n }\n } else {\n this.log('DEBUG: Skipping folder hierarchy - createFolderHierarchy:', this.reporterOptions.createFolderHierarchy, 'suitePath.length:', result.suitePath.length);\n }\n\n this.log('DEBUG: Final folderId for test case:', folderId);\n\n const { testCase, action } = await this.client.findOrCreateTestCase({\n projectId: this.reporterOptions.projectId,\n folderId,\n templateId,\n name: result.testName,\n className: result.suiteName || undefined,\n source: 'API',\n automated: true,\n });\n\n // Track statistics based on action\n if (action === 'found') {\n this.state.stats.testCasesFound++;\n } else if (action === 'created') {\n this.state.stats.testCasesCreated++;\n } else if (action === 'moved') {\n this.state.stats.testCasesMoved++;\n }\n\n repositoryCaseId = testCase.id;\n this.state.caseIdMap.set(caseKey, repositoryCaseId);\n this.log(`${action === 'found' ? 'Found' : action === 'created' ? 'Created' : 'Moved'} test case:`, testCase.id, testCase.name, 'in folder:', folderId);\n }\n } else {\n this.log('DEBUG: autoCreateTestCases is false, not creating test case');\n }\n\n if (!repositoryCaseId) {\n this.log('No repository case ID, skipping result');\n return;\n }\n\n // Get or create test run case\n let testRunCaseId: number | undefined;\n const runCaseKey = `${this.state.testRunId}_${repositoryCaseId}`;\n\n if (this.state.testRunCaseMap.has(runCaseKey)) {\n testRunCaseId = this.state.testRunCaseMap.get(runCaseKey);\n } else {\n const testRunCase = await this.client.findOrAddTestCaseToRun({\n testRunId: this.state.testRunId,\n repositoryCaseId,\n });\n testRunCaseId = testRunCase.id;\n this.state.testRunCaseMap.set(runCaseKey, testRunCaseId);\n this.log('Added case to run:', testRunCaseId);\n }\n\n // Get status ID for the JUnit result\n const statusId = this.state.statusIds[result.status] || this.state.statusIds.failed!;\n\n // Map status to JUnit result type\n const junitType = this.mapStatusToJUnitType(result.status);\n\n // Build error message/content for failed tests\n let message: string | undefined;\n let content: string | undefined;\n\n if (result.errorMessage) {\n message = result.errorMessage;\n }\n if (result.stackTrace) {\n content = result.stackTrace;\n }\n\n // Create the JUnit test result\n // WebdriverIO provides duration in milliseconds, JUnit expects seconds\n const durationInSeconds = result.duration / 1000;\n const junitResult = await this.client.createJUnitTestResult({\n testSuiteId: this.state.testSuiteId,\n repositoryCaseId,\n type: junitType,\n message,\n content,\n statusId,\n time: durationInSeconds,\n executedAt: result.finishedAt,\n file: result.specFile,\n systemOut: result.commandOutput,\n });\n\n this.log('Created JUnit test result:', junitResult.id, '(type:', junitType + ')');\n this.reportedResultCount++;\n\n // Store the JUnit result ID for deferred screenshot upload\n // Screenshots taken in afterTest hook won't be available yet, so we upload them in onRunnerEnd\n result.junitResultId = junitResult.id;\n\n // Update reporter stats (suite stats are calculated by backend from JUnitTestResult rows)\n if (result.status === 'failed') {\n this.state.stats.resultsFailed++;\n } else if (result.status === 'skipped') {\n this.state.stats.resultsSkipped++;\n } else {\n this.state.stats.resultsPassed++;\n }\n } catch (error) {\n this.state.stats.apiErrors++;\n this.logError(`Failed to report result for ${result.testName}:`, error);\n }\n }\n\n /**\n * Called when the entire test session ends\n */\n async onRunnerEnd(runner: RunnerStats): Promise {\n // If no tests were tracked and no initialization was started, silently skip\n // This handles specs with no matching tests (all filtered out by grep, etc.)\n if (this.state.results.size === 0 && !this.initPromise) {\n this.log('No test results to report, skipping');\n return;\n }\n\n this.log('Runner ended, waiting for initialization and pending results...');\n\n // Wait for initialization to complete (might still be in progress)\n if (this.initPromise) {\n try {\n await this.initPromise;\n } catch {\n // Error already captured in state.initError\n }\n }\n\n // Wait for any remaining pending operations\n // (WebdriverIO waits via isSynchronised, but we also wait here for safety)\n await Promise.allSettled([...this.pendingOperations]);\n\n // Check if initialization failed\n if (this.state.initError) {\n console.error('\\n[TestPlanIt] FAILED: Reporter initialization failed');\n console.error(` Error: ${this.state.initError.message}`);\n console.error(' No results were reported to TestPlanIt.');\n console.error(' Please check your configuration and API connectivity.');\n return;\n }\n\n // If no test run was created (no reportable results), silently skip\n if (!this.state.testRunId) {\n this.log('No test run created, skipping summary');\n return;\n }\n\n // If no results were actually reported to TestPlanIt, silently skip\n // This handles the case where tests ran but none had valid case IDs\n if (this.reportedResultCount === 0) {\n this.log('No results were reported to TestPlanIt, skipping summary');\n return;\n }\n\n // Upload any pending screenshots\n // Screenshots are uploaded here (deferred) because afterTest hooks run after onTestFail/onTestPass,\n // so screenshots taken in afterTest wouldn't be available during reportResult\n if (this.reporterOptions.uploadScreenshots && this.pendingScreenshots.size > 0) {\n this.log(`Uploading screenshots for ${this.pendingScreenshots.size} test(s)...`);\n\n // Create upload promises for all screenshots and track them\n // This ensures WebdriverIO waits for uploads to complete (via isSynchronised)\n const uploadPromises: Promise[] = [];\n\n for (const [uid, screenshots] of this.pendingScreenshots.entries()) {\n const result = this.state.results.get(uid);\n if (!result?.junitResultId) {\n this.log(`Skipping screenshots for ${uid} - no JUnit result ID`);\n continue;\n }\n\n this.log(`Uploading ${screenshots.length} screenshot(s) for test:`, result.testName);\n for (let i = 0; i < screenshots.length; i++) {\n const uploadPromise = (async () => {\n try {\n // Create a meaningful file name: testName_status_screenshot#.png\n // Sanitize test name for filename (remove special chars, limit length)\n const sanitizedTestName = result.testName\n .replace(/[^a-zA-Z0-9_-]/g, '_')\n .substring(0, 50);\n const fileName = `${sanitizedTestName}_${result.status}_${i + 1}.png`;\n\n // Build a descriptive note with test context\n const noteParts: string[] = [];\n noteParts.push(`Test: ${result.testName}`);\n if (result.suiteName) {\n noteParts.push(`Suite: ${result.suiteName}`);\n }\n noteParts.push(`Status: ${result.status}`);\n if (result.browser) {\n noteParts.push(`Browser: ${result.browser}`);\n }\n if (result.errorMessage) {\n // Truncate error message if too long\n const errorPreview = result.errorMessage.length > 200\n ? result.errorMessage.substring(0, 200) + '...'\n : result.errorMessage;\n noteParts.push(`Error: ${errorPreview}`);\n }\n const note = noteParts.join('\\n');\n\n this.log(`Starting upload of ${fileName} (${screenshots[i].length} bytes) to JUnit result ${result.junitResultId}...`);\n await this.client.uploadJUnitAttachment(\n result.junitResultId!,\n screenshots[i],\n fileName,\n 'image/png',\n note\n );\n this.state.stats.screenshotsUploaded++;\n this.log(`Uploaded screenshot ${i + 1}/${screenshots.length} for ${result.testName}`);\n } catch (uploadError) {\n this.state.stats.screenshotsFailed++;\n const errorMessage = uploadError instanceof Error ? uploadError.message : String(uploadError);\n const errorStack = uploadError instanceof Error ? uploadError.stack : undefined;\n this.logError(`Failed to upload screenshot ${i + 1}:`, errorMessage);\n if (errorStack) {\n this.logError('Stack trace:', errorStack);\n }\n }\n })();\n\n // Track this operation so WebdriverIO waits for it\n this.trackOperation(uploadPromise);\n uploadPromises.push(uploadPromise);\n }\n }\n\n // Wait for all uploads to complete before proceeding\n await Promise.allSettled(uploadPromises);\n\n // Clear all pending screenshots\n this.pendingScreenshots.clear();\n }\n\n // Note: JUnit test suite statistics (tests, failures, errors, skipped, time) are NOT updated here.\n // The backend calculates these dynamically from JUnitTestResult rows in the summary API.\n // This ensures correct totals when multiple workers/spec files report to the same test run.\n\n // Complete the test run if configured\n // In oneReport mode, decrement worker count and only complete when last worker finishes\n // Track this operation to prevent WebdriverIO from terminating early\n if (this.reporterOptions.completeRunOnFinish) {\n if (this.reporterOptions.oneReport) {\n // Decrement worker count and check if we're the last worker\n const isLastWorker = this.decrementWorkerCount();\n if (isLastWorker) {\n const completeRunOp = (async () => {\n try {\n await this.client.completeTestRun(this.state.testRunId!, this.reporterOptions.projectId);\n this.log('Test run completed (last worker):', this.state.testRunId);\n // Clean up shared state file\n this.deleteSharedState();\n } catch (error) {\n this.logError('Failed to complete test run:', error);\n }\n })();\n this.trackOperation(completeRunOp);\n await completeRunOp;\n } else {\n this.log('Skipping test run completion (waiting for other workers to finish)');\n }\n } else {\n const completeRunOp = (async () => {\n try {\n await this.client.completeTestRun(this.state.testRunId!, this.reporterOptions.projectId);\n this.log('Test run completed:', this.state.testRunId);\n } catch (error) {\n this.logError('Failed to complete test run:', error);\n }\n })();\n this.trackOperation(completeRunOp);\n await completeRunOp;\n }\n } else if (this.reporterOptions.oneReport) {\n // Even if not completing, decrement worker count\n this.decrementWorkerCount();\n }\n\n // Print summary\n const stats = this.state.stats;\n const duration = ((Date.now() - stats.startTime.getTime()) / 1000).toFixed(1);\n const totalResults = stats.resultsPassed + stats.resultsFailed + stats.resultsSkipped;\n const totalCases = stats.testCasesFound + stats.testCasesCreated + stats.testCasesMoved;\n\n console.log('\\n[TestPlanIt] ═══════════════════════════════════════════════════════');\n console.log('[TestPlanIt] Results Summary');\n console.log('[TestPlanIt] ═══════════════════════════════════════════════════════');\n console.log(`[TestPlanIt] Test Run ID: ${this.state.testRunId}`);\n console.log(`[TestPlanIt] Duration: ${duration}s`);\n console.log('[TestPlanIt]');\n console.log('[TestPlanIt] Test Results:');\n console.log(`[TestPlanIt] ✓ Passed: ${stats.resultsPassed}`);\n console.log(`[TestPlanIt] ✗ Failed: ${stats.resultsFailed}`);\n console.log(`[TestPlanIt] ○ Skipped: ${stats.resultsSkipped}`);\n console.log(`[TestPlanIt] Total: ${totalResults}`);\n\n if (this.reporterOptions.autoCreateTestCases && totalCases > 0) {\n console.log('[TestPlanIt]');\n console.log('[TestPlanIt] Test Cases:');\n console.log(`[TestPlanIt] Found (existing): ${stats.testCasesFound}`);\n console.log(`[TestPlanIt] Created (new): ${stats.testCasesCreated}`);\n if (stats.testCasesMoved > 0) {\n console.log(`[TestPlanIt] Moved (restored): ${stats.testCasesMoved}`);\n }\n }\n\n if (this.reporterOptions.uploadScreenshots && (stats.screenshotsUploaded > 0 || stats.screenshotsFailed > 0)) {\n console.log('[TestPlanIt]');\n console.log('[TestPlanIt] Screenshots:');\n console.log(`[TestPlanIt] Uploaded: ${stats.screenshotsUploaded}`);\n if (stats.screenshotsFailed > 0) {\n console.log(`[TestPlanIt] Failed: ${stats.screenshotsFailed}`);\n }\n }\n\n if (stats.apiErrors > 0) {\n console.log('[TestPlanIt]');\n console.log(`[TestPlanIt] ⚠ API Errors: ${stats.apiErrors}`);\n }\n\n console.log('[TestPlanIt]');\n console.log(`[TestPlanIt] View results: ${this.reporterOptions.domain}/projects/runs/${this.reporterOptions.projectId}/${this.state.testRunId}`);\n console.log('[TestPlanIt] ═══════════════════════════════════════════════════════\\n');\n }\n\n /**\n * Get the current state (for debugging)\n */\n getState(): ReporterState {\n return this.state;\n }\n}\n"]} \ No newline at end of file +{"version":3,"sources":["../src/shared.ts","../src/reporter.ts","../src/service.ts"],"names":["path","os","fs","WDIOReporter","TestPlanItClient"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Shared state utilities for coordinating between the TestPlanIt WDIO service\n * and reporter instances running in separate worker processes.\n *\n * Uses a file in the OS temp directory to share state (test run ID, test suite ID)\n * between the main process (service) and worker processes (reporters).\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport * as os from 'os';\n\n/**\n * Shared state file for coordinating between WDIO workers and the launcher service.\n *\n * When `managedByService` is true, the TestPlanItService controls the test run lifecycle\n * (creation in onPrepare, completion in onComplete). Workers must not manage the run lifecycle.\n *\n * When `managedByService` is false/absent, the reporter manages it (legacy oneReport mode).\n */\nexport interface SharedState {\n testRunId: number;\n testSuiteId?: number;\n createdAt: string;\n /** Number of active workers using this test run (only used when managedByService is false) */\n activeWorkers: number;\n /** When true, the TestPlanItService controls run creation/completion. Workers must not manage the run lifecycle. */\n managedByService?: boolean;\n}\n\n/** Maximum age of a shared state file before it is considered stale (4 hours) */\nconst STALE_THRESHOLD_MS = 4 * 60 * 60 * 1000;\n\n/**\n * Get the path to the shared state file for a given project.\n * Uses the OS temp directory with a project-specific filename.\n */\nexport function getSharedStateFilePath(projectId: number): string {\n const fileName = `.testplanit-reporter-${projectId}.json`;\n return path.join(os.tmpdir(), fileName);\n}\n\n/**\n * Acquire a simple file-based lock using exclusive file creation.\n * Retries with exponential backoff up to `maxAttempts` times.\n */\nfunction acquireLock(lockPath: string, maxAttempts = 10): boolean {\n for (let i = 0; i < maxAttempts; i++) {\n try {\n fs.writeFileSync(lockPath, process.pid.toString(), { flag: 'wx' });\n return true;\n } catch {\n const sleepMs = 50 * Math.pow(2, i) + Math.random() * 50;\n const start = Date.now();\n while (Date.now() - start < sleepMs) {\n // Busy wait\n }\n }\n }\n return false;\n}\n\n/**\n * Release a file-based lock.\n */\nfunction releaseLock(lockPath: string): void {\n try {\n fs.unlinkSync(lockPath);\n } catch {\n // Ignore lock removal errors\n }\n}\n\n/**\n * Execute a callback while holding the lock on the shared state file.\n * Returns the callback's return value, or undefined if the lock could not be acquired.\n */\nexport function withLock(projectId: number, callback: (filePath: string) => T): T | undefined {\n const filePath = getSharedStateFilePath(projectId);\n const lockPath = `${filePath}.lock`;\n\n if (!acquireLock(lockPath)) {\n return undefined;\n }\n\n try {\n return callback(filePath);\n } finally {\n releaseLock(lockPath);\n }\n}\n\n/**\n * Read shared state from file.\n * Returns null if file doesn't exist, is stale (>4 hours), or contains invalid JSON.\n *\n * Note: Does NOT check `activeWorkers === 0` — that logic differs between\n * service-managed mode and legacy oneReport mode and is handled by the caller.\n */\nexport function readSharedState(projectId: number): SharedState | null {\n const filePath = getSharedStateFilePath(projectId);\n try {\n if (!fs.existsSync(filePath)) {\n return null;\n }\n const content = fs.readFileSync(filePath, 'utf-8');\n const state: SharedState = JSON.parse(content);\n\n // Check if state is stale\n const createdAt = new Date(state.createdAt);\n const staleThreshold = new Date(Date.now() - STALE_THRESHOLD_MS);\n if (createdAt < staleThreshold) {\n deleteSharedState(projectId);\n return null;\n }\n\n return state;\n } catch {\n return null;\n }\n}\n\n/**\n * Write shared state to file atomically (uses lock).\n */\nexport function writeSharedState(projectId: number, state: SharedState): void {\n withLock(projectId, (filePath) => {\n fs.writeFileSync(filePath, JSON.stringify(state, null, 2));\n });\n}\n\n/**\n * Write shared state to file, but only if no file already exists (first writer wins).\n * If the file already exists, optionally updates the testSuiteId if not yet set.\n * Returns the final state (either the written state or the existing state).\n */\nexport function writeSharedStateIfAbsent(projectId: number, state: SharedState): SharedState | undefined {\n return withLock(projectId, (filePath) => {\n if (fs.existsSync(filePath)) {\n // File already exists — read existing state\n const content = fs.readFileSync(filePath, 'utf-8');\n const existingState: SharedState = JSON.parse(content);\n\n // Only update if the testSuiteId is missing and we have one to add\n if (!existingState.testSuiteId && state.testSuiteId) {\n existingState.testSuiteId = state.testSuiteId;\n fs.writeFileSync(filePath, JSON.stringify(existingState, null, 2));\n }\n return existingState;\n }\n\n // First writer — write the full state\n fs.writeFileSync(filePath, JSON.stringify(state, null, 2));\n return state;\n });\n}\n\n/**\n * Delete shared state file.\n */\nexport function deleteSharedState(projectId: number): void {\n const filePath = getSharedStateFilePath(projectId);\n try {\n if (fs.existsSync(filePath)) {\n fs.unlinkSync(filePath);\n }\n } catch {\n // Ignore deletion errors\n }\n}\n\n/**\n * Atomically increment the active worker count in the shared state file.\n */\nexport function incrementWorkerCount(projectId: number): void {\n withLock(projectId, (filePath) => {\n if (fs.existsSync(filePath)) {\n const content = fs.readFileSync(filePath, 'utf-8');\n const state: SharedState = JSON.parse(content);\n state.activeWorkers = (state.activeWorkers || 0) + 1;\n fs.writeFileSync(filePath, JSON.stringify(state, null, 2));\n }\n });\n}\n\n/**\n * Atomically decrement the active worker count in the shared state file.\n * Returns true if this was the last worker (count reached 0).\n */\nexport function decrementWorkerCount(projectId: number): boolean {\n const result = withLock(projectId, (filePath) => {\n if (fs.existsSync(filePath)) {\n const content = fs.readFileSync(filePath, 'utf-8');\n const state: SharedState = JSON.parse(content);\n state.activeWorkers = Math.max(0, (state.activeWorkers || 1) - 1);\n fs.writeFileSync(filePath, JSON.stringify(state, null, 2));\n return state.activeWorkers === 0;\n }\n return false;\n });\n return result ?? false;\n}\n","import WDIOReporter, { type RunnerStats, type SuiteStats, type TestStats, type AfterCommandArgs } from '@wdio/reporter';\nimport { TestPlanItClient } from '@testplanit/api';\nimport type { NormalizedStatus, JUnitResultType } from '@testplanit/api';\nimport type { TestPlanItReporterOptions, TrackedTestResult, ReporterState } from './types.js';\nimport {\n readSharedState,\n writeSharedStateIfAbsent,\n deleteSharedState,\n incrementWorkerCount,\n decrementWorkerCount,\n} from './shared.js';\n\n/**\n * WebdriverIO Reporter for TestPlanIt\n *\n * Reports test results directly to your TestPlanIt instance.\n *\n * @example\n * ```javascript\n * // wdio.conf.js\n * export const config = {\n * reporters: [\n * ['@testplanit/wdio-reporter', {\n * domain: 'https://testplanit.example.com',\n * apiToken: process.env.TESTPLANIT_API_TOKEN,\n * projectId: 1,\n * runName: 'E2E Tests - {date}',\n * }]\n * ]\n * }\n * ```\n */\nexport default class TestPlanItReporter extends WDIOReporter {\n private client: TestPlanItClient;\n private reporterOptions: TestPlanItReporterOptions;\n private state: ReporterState;\n private currentSuite: string[] = [];\n private initPromise: Promise | null = null;\n private pendingOperations: Set> = new Set();\n private reportedResultCount = 0;\n private detectedFramework: string | null = null;\n private currentTestUid: string | null = null;\n private currentCid: string | null = null;\n private pendingScreenshots: Map = new Map();\n /** When true, the TestPlanItService manages the test run lifecycle */\n private managedByService = false;\n\n /**\n * WebdriverIO uses this getter to determine if the reporter has finished async operations.\n * The test runner will wait for this to return true before terminating.\n */\n get isSynchronised(): boolean {\n return this.pendingOperations.size === 0;\n }\n\n constructor(options: TestPlanItReporterOptions) {\n super(options);\n\n this.reporterOptions = {\n caseIdPattern: /\\[(\\d+)\\]/g,\n autoCreateTestCases: false,\n createFolderHierarchy: false,\n uploadScreenshots: true,\n includeStackTrace: true,\n completeRunOnFinish: true,\n oneReport: true,\n timeout: 30000,\n maxRetries: 3,\n verbose: false,\n ...options,\n };\n\n // Validate required options\n if (!this.reporterOptions.domain) {\n throw new Error('TestPlanIt reporter: domain is required');\n }\n if (!this.reporterOptions.apiToken) {\n throw new Error('TestPlanIt reporter: apiToken is required');\n }\n if (!this.reporterOptions.projectId) {\n throw new Error('TestPlanIt reporter: projectId is required');\n }\n\n // Initialize API client\n this.client = new TestPlanItClient({\n baseUrl: this.reporterOptions.domain,\n apiToken: this.reporterOptions.apiToken,\n timeout: this.reporterOptions.timeout,\n maxRetries: this.reporterOptions.maxRetries,\n });\n\n // Initialize state - testRunId will be resolved during initialization\n this.state = {\n testRunId: typeof this.reporterOptions.testRunId === 'number' ? this.reporterOptions.testRunId : undefined,\n resolvedIds: {},\n results: new Map(),\n caseIdMap: new Map(),\n testRunCaseMap: new Map(),\n folderPathMap: new Map(),\n statusIds: {},\n initialized: false,\n stats: {\n testCasesFound: 0,\n testCasesCreated: 0,\n testCasesMoved: 0,\n foldersCreated: 0,\n resultsPassed: 0,\n resultsFailed: 0,\n resultsSkipped: 0,\n screenshotsUploaded: 0,\n screenshotsFailed: 0,\n apiErrors: 0,\n apiRequests: 0,\n startTime: new Date(),\n },\n };\n }\n\n /**\n * Log a message if verbose mode is enabled\n */\n private log(message: string, ...args: unknown[]): void {\n if (this.reporterOptions.verbose) {\n console.log(`[TestPlanIt] ${message}`, ...args);\n }\n }\n\n /**\n * Log an error (always logs, not just in verbose mode)\n */\n private logError(message: string, error?: unknown): void {\n const errorMsg = error instanceof Error ? error.message : String(error ?? '');\n const stack = error instanceof Error && error.stack ? `\\n${error.stack}` : '';\n console.error(`[TestPlanIt] ERROR: ${message}`, errorMsg, stack);\n }\n\n /**\n * Track an async operation to prevent the runner from terminating early.\n * The operation is added to pendingOperations and removed when complete.\n * WebdriverIO checks isSynchronised and waits until all operations finish.\n */\n private trackOperation(operation: Promise): void {\n this.pendingOperations.add(operation);\n operation.finally(() => {\n this.pendingOperations.delete(operation);\n });\n }\n\n /**\n * Initialize the reporter (create test run, fetch statuses)\n */\n private async initialize(): Promise {\n // If already initialized successfully, return immediately\n if (this.state.initialized) return;\n\n // If we have a previous error, throw it again to prevent retrying\n if (this.state.initError) {\n throw this.state.initError;\n }\n\n // If initialization is in progress, wait for it\n if (this.initPromise) return this.initPromise;\n\n this.initPromise = this.doInitialize();\n return this.initPromise;\n }\n\n private async doInitialize(): Promise {\n try {\n // Log initialization start (only happens when we have results to report)\n this.log('Initializing reporter...');\n this.log(` Domain: ${this.reporterOptions.domain}`);\n this.log(` Project ID: ${this.reporterOptions.projectId}`);\n this.log(` oneReport: ${this.reporterOptions.oneReport}`);\n\n // Resolve any string IDs to numeric IDs\n this.log('Resolving option IDs...');\n await this.resolveOptionIds();\n\n // Fetch status mappings\n this.log('Fetching status mappings...');\n await this.fetchStatusMappings();\n\n // Handle oneReport mode - check for existing shared state\n if (this.reporterOptions.oneReport && !this.state.testRunId) {\n const sharedState = readSharedState(this.reporterOptions.projectId);\n if (sharedState) {\n if (sharedState.managedByService) {\n // Service manages the run — just use the IDs, skip all lifecycle management\n this.state.testRunId = sharedState.testRunId;\n this.state.testSuiteId = sharedState.testSuiteId;\n this.managedByService = true;\n this.log(`Using service-managed test run: ${sharedState.testRunId}`);\n } else {\n // Legacy oneReport mode — validate and join the existing run\n this.state.testRunId = sharedState.testRunId;\n this.state.testSuiteId = sharedState.testSuiteId;\n this.log(`Using shared test run from file: ${sharedState.testRunId}`);\n\n // In legacy mode, skip runs where all workers have finished\n if (sharedState.activeWorkers === 0) {\n this.log('Previous test run completed (activeWorkers=0), starting fresh');\n deleteSharedState(this.reporterOptions.projectId);\n this.state.testRunId = undefined;\n this.state.testSuiteId = undefined;\n } else {\n // Validate the shared test run still exists, is not completed, and is not deleted\n try {\n const testRun = await this.client.getTestRun(this.state.testRunId);\n if (testRun.isDeleted) {\n this.log(`Shared test run ${testRun.id} is deleted, starting fresh`);\n this.state.testRunId = undefined;\n this.state.testSuiteId = undefined;\n deleteSharedState(this.reporterOptions.projectId);\n } else if (testRun.isCompleted) {\n this.log(`Shared test run ${testRun.id} is already completed, starting fresh`);\n this.state.testRunId = undefined;\n this.state.testSuiteId = undefined;\n deleteSharedState(this.reporterOptions.projectId);\n } else {\n this.log(`Validated shared test run: ${testRun.name} (ID: ${testRun.id})`);\n incrementWorkerCount(this.reporterOptions.projectId);\n }\n } catch {\n this.log('Shared test run no longer exists, will create new one');\n this.state.testRunId = undefined;\n this.state.testSuiteId = undefined;\n deleteSharedState(this.reporterOptions.projectId);\n }\n }\n }\n }\n }\n\n // Create or validate test run (skip if service-managed)\n if (!this.state.testRunId && !this.managedByService) {\n // In oneReport mode, use atomic write to prevent race conditions\n if (this.reporterOptions.oneReport) {\n // Create the test run first\n await this.createTestRun();\n this.log(`Created test run with ID: ${this.state.testRunId}`);\n\n // Try to write shared state - first writer wins\n const finalState = writeSharedStateIfAbsent(this.reporterOptions.projectId, {\n testRunId: this.state.testRunId!,\n testSuiteId: this.state.testSuiteId,\n createdAt: new Date().toISOString(),\n activeWorkers: 1,\n });\n\n // Check if another worker wrote first\n if (finalState && finalState.testRunId !== this.state.testRunId) {\n this.log(`Another worker created test run first, switching from ${this.state.testRunId} to ${finalState.testRunId}`);\n this.state.testRunId = finalState.testRunId;\n this.state.testSuiteId = finalState.testSuiteId;\n }\n } else {\n await this.createTestRun();\n this.log(`Created test run with ID: ${this.state.testRunId}`);\n }\n } else if (this.state.testRunId && !this.reporterOptions.oneReport && !this.managedByService) {\n // Validate existing test run (only when not using oneReport or service)\n try {\n const testRun = await this.client.getTestRun(this.state.testRunId);\n this.log(`Using existing test run: ${testRun.name} (ID: ${testRun.id})`);\n } catch (error) {\n throw new Error(`Test run ${this.state.testRunId} not found or not accessible`);\n }\n }\n\n this.state.initialized = true;\n this.log('Reporter initialized successfully');\n } catch (error) {\n this.state.initError = error instanceof Error ? error : new Error(String(error));\n this.logError('Failed to initialize reporter:', error);\n throw error;\n }\n }\n\n /**\n * Resolve option names to numeric IDs\n */\n private async resolveOptionIds(): Promise {\n const projectId = this.reporterOptions.projectId;\n\n // Resolve testRunId if it's a string\n if (typeof this.reporterOptions.testRunId === 'string') {\n const testRun = await this.client.findTestRunByName(projectId, this.reporterOptions.testRunId);\n if (!testRun) {\n throw new Error(`Test run not found: \"${this.reporterOptions.testRunId}\"`);\n }\n this.state.testRunId = testRun.id;\n this.state.resolvedIds.testRunId = testRun.id;\n this.log(`Resolved test run \"${this.reporterOptions.testRunId}\" -> ${testRun.id}`);\n }\n\n // Resolve configId if it's a string\n if (typeof this.reporterOptions.configId === 'string') {\n const config = await this.client.findConfigurationByName(projectId, this.reporterOptions.configId);\n if (!config) {\n throw new Error(`Configuration not found: \"${this.reporterOptions.configId}\"`);\n }\n this.state.resolvedIds.configId = config.id;\n this.log(`Resolved configuration \"${this.reporterOptions.configId}\" -> ${config.id}`);\n } else if (typeof this.reporterOptions.configId === 'number') {\n this.state.resolvedIds.configId = this.reporterOptions.configId;\n }\n\n // Resolve milestoneId if it's a string\n if (typeof this.reporterOptions.milestoneId === 'string') {\n const milestone = await this.client.findMilestoneByName(projectId, this.reporterOptions.milestoneId);\n if (!milestone) {\n throw new Error(`Milestone not found: \"${this.reporterOptions.milestoneId}\"`);\n }\n this.state.resolvedIds.milestoneId = milestone.id;\n this.log(`Resolved milestone \"${this.reporterOptions.milestoneId}\" -> ${milestone.id}`);\n } else if (typeof this.reporterOptions.milestoneId === 'number') {\n this.state.resolvedIds.milestoneId = this.reporterOptions.milestoneId;\n }\n\n // Resolve stateId if it's a string\n if (typeof this.reporterOptions.stateId === 'string') {\n const state = await this.client.findWorkflowStateByName(projectId, this.reporterOptions.stateId);\n if (!state) {\n throw new Error(`Workflow state not found: \"${this.reporterOptions.stateId}\"`);\n }\n this.state.resolvedIds.stateId = state.id;\n this.log(`Resolved workflow state \"${this.reporterOptions.stateId}\" -> ${state.id}`);\n } else if (typeof this.reporterOptions.stateId === 'number') {\n this.state.resolvedIds.stateId = this.reporterOptions.stateId;\n }\n\n // Resolve parentFolderId if it's a string\n if (typeof this.reporterOptions.parentFolderId === 'string') {\n let folder = await this.client.findFolderByName(projectId, this.reporterOptions.parentFolderId);\n if (!folder) {\n // If createFolderHierarchy is enabled, create the parent folder\n if (this.reporterOptions.createFolderHierarchy) {\n this.log(`Parent folder \"${this.reporterOptions.parentFolderId}\" not found, creating it...`);\n folder = await this.client.createFolder({\n projectId,\n name: this.reporterOptions.parentFolderId,\n });\n this.log(`Created parent folder \"${this.reporterOptions.parentFolderId}\" -> ${folder.id}`);\n } else {\n throw new Error(`Folder not found: \"${this.reporterOptions.parentFolderId}\"`);\n }\n }\n this.state.resolvedIds.parentFolderId = folder.id;\n this.log(`Resolved folder \"${this.reporterOptions.parentFolderId}\" -> ${folder.id}`);\n } else if (typeof this.reporterOptions.parentFolderId === 'number') {\n this.state.resolvedIds.parentFolderId = this.reporterOptions.parentFolderId;\n }\n\n // Resolve templateId if it's a string\n if (typeof this.reporterOptions.templateId === 'string') {\n const template = await this.client.findTemplateByName(projectId, this.reporterOptions.templateId);\n if (!template) {\n throw new Error(`Template not found: \"${this.reporterOptions.templateId}\"`);\n }\n this.state.resolvedIds.templateId = template.id;\n this.log(`Resolved template \"${this.reporterOptions.templateId}\" -> ${template.id}`);\n } else if (typeof this.reporterOptions.templateId === 'number') {\n this.state.resolvedIds.templateId = this.reporterOptions.templateId;\n }\n\n // Resolve tagIds if they contain strings\n if (this.reporterOptions.tagIds && this.reporterOptions.tagIds.length > 0) {\n this.state.resolvedIds.tagIds = await this.client.resolveTagIds(projectId, this.reporterOptions.tagIds);\n this.log(`Resolved tags: ${this.state.resolvedIds.tagIds.join(', ')}`);\n }\n }\n\n /**\n * Fetch status ID mappings from TestPlanIt\n */\n private async fetchStatusMappings(): Promise {\n const statuses: NormalizedStatus[] = ['passed', 'failed', 'skipped', 'blocked'];\n\n for (const status of statuses) {\n const statusId = await this.client.getStatusId(this.reporterOptions.projectId, status);\n if (statusId) {\n this.state.statusIds[status] = statusId;\n this.log(`Status mapping: ${status} -> ${statusId}`);\n }\n }\n\n if (!this.state.statusIds.passed || !this.state.statusIds.failed) {\n throw new Error('Could not find required status mappings (passed/failed) in TestPlanIt');\n }\n }\n\n /**\n * Map test status to JUnit result type\n */\n private mapStatusToJUnitType(status: 'passed' | 'failed' | 'skipped' | 'pending'): JUnitResultType {\n switch (status) {\n case 'passed':\n return 'PASSED';\n case 'failed':\n return 'FAILURE';\n case 'skipped':\n case 'pending':\n return 'SKIPPED';\n default:\n return 'FAILURE';\n }\n }\n\n /**\n * Create the JUnit test suite for this test run\n */\n private async createJUnitTestSuite(): Promise {\n if (this.state.testSuiteId) {\n return; // Already created (either from shared state or previous call)\n }\n\n if (!this.state.testRunId) {\n throw new Error('Cannot create JUnit test suite without a test run ID');\n }\n\n // In oneReport mode, check if another worker has already created a suite\n if (this.reporterOptions.oneReport) {\n const sharedState = readSharedState(this.reporterOptions.projectId);\n if (sharedState?.testSuiteId) {\n this.state.testSuiteId = sharedState.testSuiteId;\n this.log('Using shared JUnit test suite from file:', sharedState.testSuiteId);\n return;\n }\n }\n\n const runName = this.formatRunName(this.reporterOptions.runName || '{suite} - {date} {time}');\n\n this.log('Creating JUnit test suite...');\n\n const testSuite = await this.client.createJUnitTestSuite({\n testRunId: this.state.testRunId,\n name: runName,\n time: 0, // Will be updated incrementally\n tests: 0,\n failures: 0,\n errors: 0,\n skipped: 0,\n });\n\n this.state.testSuiteId = testSuite.id;\n this.log('Created JUnit test suite with ID:', testSuite.id);\n\n // Update shared state with suite ID if in oneReport mode\n if (this.reporterOptions.oneReport) {\n const finalState = writeSharedStateIfAbsent(this.reporterOptions.projectId, {\n testRunId: this.state.testRunId,\n testSuiteId: this.state.testSuiteId,\n createdAt: new Date().toISOString(),\n activeWorkers: 1,\n });\n\n // Check if another worker wrote first — use their suite\n if (finalState && finalState.testSuiteId !== this.state.testSuiteId) {\n this.log(`Another worker created test suite first, switching from ${this.state.testSuiteId} to ${finalState.testSuiteId}`);\n this.state.testSuiteId = finalState.testSuiteId;\n }\n }\n }\n\n /**\n * Map WebdriverIO framework name to TestPlanIt test run type\n */\n private getTestRunType(): TestPlanItReporterOptions['testRunType'] {\n // If explicitly set by user, use that\n if (this.reporterOptions.testRunType) {\n return this.reporterOptions.testRunType;\n }\n\n // Auto-detect from WebdriverIO framework config\n if (this.detectedFramework) {\n const framework = this.detectedFramework.toLowerCase();\n if (framework === 'mocha') return 'MOCHA';\n if (framework === 'cucumber') return 'CUCUMBER';\n // jasmine and others map to REGULAR\n return 'REGULAR';\n }\n\n // Default fallback\n return 'MOCHA';\n }\n\n /**\n * Create a new test run\n */\n private async createTestRun(): Promise {\n const runName = this.formatRunName(this.reporterOptions.runName || '{suite} - {date} {time}');\n const testRunType = this.getTestRunType();\n\n this.log('Creating test run:', runName, '(type:', testRunType + ')');\n\n const testRun = await this.client.createTestRun({\n projectId: this.reporterOptions.projectId,\n name: runName,\n testRunType,\n configId: this.state.resolvedIds.configId,\n milestoneId: this.state.resolvedIds.milestoneId,\n stateId: this.state.resolvedIds.stateId,\n tagIds: this.state.resolvedIds.tagIds,\n });\n\n this.state.testRunId = testRun.id;\n this.log('Created test run with ID:', testRun.id);\n }\n\n /**\n * Format the run name with placeholders\n */\n private formatRunName(template: string): string {\n const now = new Date();\n const date = now.toISOString().split('T')[0];\n const time = now.toTimeString().split(' ')[0];\n const browser = this.state.capabilities?.browserName || 'unknown';\n const platform = this.state.capabilities?.platformName || process.platform;\n\n // Get spec file name from currentSpec (e.g., \"/path/to/test.spec.ts\" -> \"test.spec.ts\")\n let spec = 'unknown';\n if (this.currentSpec) {\n const parts = this.currentSpec.split('/');\n spec = parts[parts.length - 1] || 'unknown';\n // Remove common extensions for cleaner names\n spec = spec.replace(/\\.(spec|test)\\.(ts|js|mjs|cjs)$/, '');\n }\n\n // Get the root suite name (first describe block)\n const suite = this.currentSuite[0] || 'Tests';\n\n return template\n .replace('{date}', date)\n .replace('{time}', time)\n .replace('{browser}', browser)\n .replace('{platform}', platform)\n .replace('{spec}', spec)\n .replace('{suite}', suite);\n }\n\n /**\n * Parse case IDs from test title using the configured pattern\n * @example With default pattern: \"[1761] [1762] should load the page\" -> [1761, 1762]\n * @example With C-prefix pattern: \"C12345 C67890 should load the page\" -> [12345, 67890]\n */\n private parseCaseIds(title: string): { caseIds: number[]; cleanTitle: string } {\n const pattern = this.reporterOptions.caseIdPattern || /\\[(\\d+)\\]/g;\n const regex = typeof pattern === 'string' ? new RegExp(pattern, 'g') : new RegExp(pattern.source, 'g');\n const caseIds: number[] = [];\n let match: RegExpExecArray | null;\n\n while ((match = regex.exec(title)) !== null) {\n // Find the first capturing group that has a value (supports patterns with multiple groups)\n for (let i = 1; i < match.length; i++) {\n if (match[i]) {\n caseIds.push(parseInt(match[i], 10));\n break;\n }\n }\n }\n\n // Remove matched patterns from title\n const cleanTitle = title.replace(regex, '').trim().replace(/\\s+/g, ' ');\n\n return { caseIds, cleanTitle };\n }\n\n /**\n * Get the full suite path as a string\n */\n private getFullSuiteName(): string {\n return this.currentSuite.join(' > ');\n }\n\n /**\n * Create a unique key for a test case\n */\n private createCaseKey(suiteName: string, testName: string): string {\n return `${suiteName}::${testName}`;\n }\n\n // ============================================================================\n // WebdriverIO Reporter Hooks\n // ============================================================================\n\n onRunnerStart(runner: RunnerStats): void {\n this.log('Runner started:', runner.cid);\n this.state.capabilities = runner.capabilities as WebdriverIO.Capabilities;\n\n // Auto-detect the test framework from WebdriverIO config\n // This is accessed via runner.config.framework (e.g., 'mocha', 'cucumber', 'jasmine')\n const config = runner.config as { framework?: string } | undefined;\n if (config?.framework) {\n this.detectedFramework = config.framework;\n this.log('Detected framework:', this.detectedFramework);\n }\n\n // Don't initialize here - wait until we have actual test results to report\n // This avoids creating empty test runs for specs with no matching tests\n }\n\n onSuiteStart(suite: SuiteStats): void {\n if (suite.title) {\n this.currentSuite.push(suite.title);\n this.log('Suite started:', this.getFullSuiteName());\n }\n }\n\n onSuiteEnd(suite: SuiteStats): void {\n if (suite.title) {\n this.log('Suite ended:', this.getFullSuiteName());\n this.currentSuite.pop();\n }\n }\n\n onTestStart(test: TestStats): void {\n this.log('Test started:', test.title);\n // Track the current test for screenshot association\n const { cleanTitle } = this.parseCaseIds(test.title);\n const suiteName = this.getFullSuiteName();\n const fullTitle = suiteName ? `${suiteName} > ${cleanTitle}` : cleanTitle;\n this.currentTestUid = `${test.cid}_${fullTitle}`;\n this.currentCid = test.cid;\n }\n\n /**\n * Capture screenshots from WebdriverIO commands\n */\n onAfterCommand(commandArgs: AfterCommandArgs): void {\n // Check if this is a screenshot command\n if (!this.reporterOptions.uploadScreenshots) {\n return;\n }\n\n // WebdriverIO uses 'takeScreenshot' as the command name or '/screenshot' endpoint\n const isScreenshotCommand =\n commandArgs.command === 'takeScreenshot' ||\n commandArgs.command === 'saveScreenshot' ||\n commandArgs.endpoint?.includes('/screenshot');\n\n if (!isScreenshotCommand) {\n return;\n }\n\n this.log(`Screenshot command detected: ${commandArgs.command}, endpoint: ${commandArgs.endpoint}`);\n\n // For saveScreenshot, the result is the file path, not base64 data\n // We need to handle both takeScreenshot (returns base64) and saveScreenshot (saves to file)\n const result = commandArgs.result as Record | string | undefined;\n const resultValue = (typeof result === 'object' && result !== null ? result.value : result) ?? result;\n\n if (!resultValue) {\n this.log('No result value in screenshot command');\n return;\n }\n\n // The result should be base64-encoded screenshot data\n const screenshotData = resultValue as string;\n if (typeof screenshotData !== 'string') {\n this.log(`Screenshot result is not a string: ${typeof screenshotData}`);\n return;\n }\n\n // Check if this looks like a file path rather than base64 data\n // File paths start with / (Unix) or drive letter like C:\\ (Windows)\n // Base64 PNG data starts with \"iVBORw0KGgo\" (PNG header)\n const looksLikeFilePath =\n screenshotData.startsWith('/') ||\n /^[A-Za-z]:[\\\\\\/]/.test(screenshotData) ||\n screenshotData.startsWith('./') ||\n screenshotData.startsWith('../');\n\n if (looksLikeFilePath) {\n this.log(`Screenshot result appears to be a file path: ${screenshotData.substring(0, 100)}`);\n return;\n }\n\n // Store the screenshot associated with the current test\n if (this.currentTestUid) {\n const buffer = Buffer.from(screenshotData, 'base64');\n const existing = this.pendingScreenshots.get(this.currentTestUid) || [];\n existing.push(buffer);\n this.pendingScreenshots.set(this.currentTestUid, existing);\n this.log('Captured screenshot for test:', this.currentTestUid, `(${buffer.length} bytes)`);\n } else {\n this.log('No current test UID to associate screenshot with');\n }\n }\n\n onTestPass(test: TestStats): void {\n this.handleTestEnd(test, 'passed');\n }\n\n onTestFail(test: TestStats): void {\n this.handleTestEnd(test, 'failed');\n }\n\n onTestSkip(test: TestStats): void {\n this.handleTestEnd(test, 'skipped');\n }\n\n /**\n * Handle test completion\n */\n private handleTestEnd(test: TestStats, status: 'passed' | 'failed' | 'skipped'): void {\n const { caseIds, cleanTitle } = this.parseCaseIds(test.title);\n const suiteName = this.getFullSuiteName();\n const suitePath = [...this.currentSuite]; // Copy the current suite hierarchy\n const fullTitle = suiteName ? `${suiteName} > ${cleanTitle}` : cleanTitle;\n const uid = `${test.cid}_${fullTitle}`;\n\n // Calculate duration from timestamps for reliability\n // WebdriverIO's test.duration can be inconsistent in some versions\n const startTime = new Date(test.start).getTime();\n const endTime = test.end ? new Date(test.end).getTime() : Date.now();\n const durationMs = endTime - startTime;\n\n // Format WebdriverIO command output if available\n let commandOutput: string | undefined;\n if (test.output && test.output.length > 0) {\n commandOutput = test.output\n .map((o) => {\n const parts: string[] = [];\n if (o.method) parts.push(`[${o.method}]`);\n if (o.endpoint) parts.push(o.endpoint);\n if (o.result !== undefined) {\n const resultStr = typeof o.result === 'string' ? o.result : JSON.stringify(o.result);\n // Truncate long results\n parts.push(resultStr.length > 200 ? resultStr.substring(0, 200) + '...' : resultStr);\n }\n return parts.join(' ');\n })\n .join('\\n');\n }\n\n const result: TrackedTestResult = {\n caseId: caseIds[0], // Primary case ID\n suiteName,\n suitePath,\n testName: cleanTitle,\n fullTitle,\n originalTitle: test.title,\n status,\n duration: durationMs,\n errorMessage: test.error?.message,\n stackTrace: this.reporterOptions.includeStackTrace ? test.error?.stack : undefined,\n startedAt: new Date(test.start),\n finishedAt: new Date(endTime),\n browser: this.state.capabilities?.browserName,\n platform: this.state.capabilities?.platformName || process.platform,\n screenshots: [],\n retryAttempt: test.retries || 0,\n uid,\n specFile: this.currentSpec,\n commandOutput,\n };\n\n this.state.results.set(uid, result);\n this.log(`Test ${status}:`, cleanTitle, caseIds.length > 0 ? `(Case IDs: ${caseIds.join(', ')})` : '');\n\n // Report result asynchronously - track operation so WebdriverIO waits for completion\n const reportPromise = this.reportResult(result, caseIds);\n this.trackOperation(reportPromise);\n }\n\n /**\n * Report a single test result to TestPlanIt\n */\n private async reportResult(result: TrackedTestResult, caseIds: number[]): Promise {\n try {\n // Check if this result can be reported BEFORE initializing\n // This prevents creating empty test runs for tests without case IDs\n if (caseIds.length === 0 && !this.reporterOptions.autoCreateTestCases) {\n console.warn(`[TestPlanIt] WARNING: Skipping \"${result.testName}\" - no case ID found and autoCreateTestCases is disabled. Set autoCreateTestCases: true to automatically find or create test cases by name.`);\n return;\n }\n\n // Now we know this result can be reported, so initialize if needed\n await this.initialize();\n\n if (!this.state.testRunId) {\n this.logError('No test run ID available, skipping result');\n return;\n }\n\n // Create JUnit test suite if not already created\n await this.createJUnitTestSuite();\n\n if (!this.state.testSuiteId) {\n this.logError('No test suite ID available, skipping result');\n return;\n }\n\n // Get or create repository case\n let repositoryCaseId: number | undefined;\n const caseKey = this.createCaseKey(result.suiteName, result.testName);\n\n // DEBUG: Always log key info about this test\n this.log('DEBUG: Processing test:', result.testName);\n this.log('DEBUG: suiteName:', result.suiteName);\n this.log('DEBUG: suitePath:', JSON.stringify(result.suitePath));\n this.log('DEBUG: caseIds from title:', JSON.stringify(caseIds));\n this.log('DEBUG: autoCreateTestCases:', this.reporterOptions.autoCreateTestCases);\n this.log('DEBUG: createFolderHierarchy:', this.reporterOptions.createFolderHierarchy);\n\n if (caseIds.length > 0) {\n // Use the provided case ID directly as repository case ID\n repositoryCaseId = caseIds[0];\n this.log('DEBUG: Using case ID from title:', repositoryCaseId);\n } else if (this.reporterOptions.autoCreateTestCases) {\n // Check cache first\n if (this.state.caseIdMap.has(caseKey)) {\n repositoryCaseId = this.state.caseIdMap.get(caseKey);\n this.log('DEBUG: Found in cache:', caseKey, '->', repositoryCaseId);\n } else {\n // Determine the target folder ID\n let folderId = this.state.resolvedIds.parentFolderId;\n const templateId = this.state.resolvedIds.templateId;\n\n this.log('DEBUG: Initial folderId (parentFolderId):', folderId);\n this.log('DEBUG: templateId:', templateId);\n\n if (!folderId || !templateId) {\n this.logError('autoCreateTestCases requires parentFolderId and templateId');\n return;\n }\n\n // Create folder hierarchy based on suite structure if enabled\n this.log('DEBUG: Checking folder hierarchy - createFolderHierarchy:', this.reporterOptions.createFolderHierarchy, 'suitePath.length:', result.suitePath.length);\n if (this.reporterOptions.createFolderHierarchy && result.suitePath.length > 0) {\n const folderPathKey = result.suitePath.join(' > ');\n this.log('DEBUG: Will create folder hierarchy for path:', folderPathKey);\n\n // Check folder cache first\n if (this.state.folderPathMap.has(folderPathKey)) {\n folderId = this.state.folderPathMap.get(folderPathKey)!;\n this.log('Using cached folder ID for path:', folderPathKey, '->', folderId);\n } else {\n // Create the folder hierarchy\n this.log('Creating folder hierarchy:', result.suitePath.join(' > '));\n this.log('DEBUG: Calling findOrCreateFolderPath with projectId:', this.reporterOptions.projectId, 'suitePath:', JSON.stringify(result.suitePath), 'parentFolderId:', this.state.resolvedIds.parentFolderId);\n const folder = await this.client.findOrCreateFolderPath(\n this.reporterOptions.projectId,\n result.suitePath,\n this.state.resolvedIds.parentFolderId\n );\n folderId = folder.id;\n this.state.folderPathMap.set(folderPathKey, folderId);\n this.log('Created/found folder:', folder.name, '(ID:', folder.id + ')');\n }\n } else {\n this.log('DEBUG: Skipping folder hierarchy - createFolderHierarchy:', this.reporterOptions.createFolderHierarchy, 'suitePath.length:', result.suitePath.length);\n }\n\n this.log('DEBUG: Final folderId for test case:', folderId);\n\n const { testCase, action } = await this.client.findOrCreateTestCase({\n projectId: this.reporterOptions.projectId,\n folderId,\n templateId,\n name: result.testName,\n className: result.suiteName || undefined,\n source: 'API',\n automated: true,\n });\n\n // Track statistics based on action\n if (action === 'found') {\n this.state.stats.testCasesFound++;\n } else if (action === 'created') {\n this.state.stats.testCasesCreated++;\n } else if (action === 'moved') {\n this.state.stats.testCasesMoved++;\n }\n\n repositoryCaseId = testCase.id;\n this.state.caseIdMap.set(caseKey, repositoryCaseId);\n this.log(`${action === 'found' ? 'Found' : action === 'created' ? 'Created' : 'Moved'} test case:`, testCase.id, testCase.name, 'in folder:', folderId);\n }\n } else {\n this.log('DEBUG: autoCreateTestCases is false, not creating test case');\n }\n\n if (!repositoryCaseId) {\n this.log('No repository case ID, skipping result');\n return;\n }\n\n // Get or create test run case\n let testRunCaseId: number | undefined;\n const runCaseKey = `${this.state.testRunId}_${repositoryCaseId}`;\n\n if (this.state.testRunCaseMap.has(runCaseKey)) {\n testRunCaseId = this.state.testRunCaseMap.get(runCaseKey);\n } else {\n const testRunCase = await this.client.findOrAddTestCaseToRun({\n testRunId: this.state.testRunId,\n repositoryCaseId,\n });\n testRunCaseId = testRunCase.id;\n this.state.testRunCaseMap.set(runCaseKey, testRunCaseId);\n this.log('Added case to run:', testRunCaseId);\n }\n\n // Get status ID for the JUnit result\n const statusId = this.state.statusIds[result.status] || this.state.statusIds.failed!;\n\n // Map status to JUnit result type\n const junitType = this.mapStatusToJUnitType(result.status);\n\n // Build error message/content for failed tests\n let message: string | undefined;\n let content: string | undefined;\n\n if (result.errorMessage) {\n message = result.errorMessage;\n }\n if (result.stackTrace) {\n content = result.stackTrace;\n }\n\n // Create the JUnit test result\n // WebdriverIO provides duration in milliseconds, JUnit expects seconds\n const durationInSeconds = result.duration / 1000;\n const junitResult = await this.client.createJUnitTestResult({\n testSuiteId: this.state.testSuiteId,\n repositoryCaseId,\n type: junitType,\n message,\n content,\n statusId,\n time: durationInSeconds,\n executedAt: result.finishedAt,\n file: result.specFile,\n systemOut: result.commandOutput,\n });\n\n this.log('Created JUnit test result:', junitResult.id, '(type:', junitType + ')');\n this.reportedResultCount++;\n\n // Store the JUnit result ID for deferred screenshot upload\n // Screenshots taken in afterTest hook won't be available yet, so we upload them in onRunnerEnd\n result.junitResultId = junitResult.id;\n\n // Update reporter stats (suite stats are calculated by backend from JUnitTestResult rows)\n if (result.status === 'failed') {\n this.state.stats.resultsFailed++;\n } else if (result.status === 'skipped') {\n this.state.stats.resultsSkipped++;\n } else {\n this.state.stats.resultsPassed++;\n }\n } catch (error) {\n this.state.stats.apiErrors++;\n this.logError(`Failed to report result for ${result.testName}:`, error);\n }\n }\n\n /**\n * Called when the entire test session ends\n */\n async onRunnerEnd(runner: RunnerStats): Promise {\n // If no tests were tracked and no initialization was started, silently skip\n // This handles specs with no matching tests (all filtered out by grep, etc.)\n if (this.state.results.size === 0 && !this.initPromise) {\n this.log('No test results to report, skipping');\n return;\n }\n\n this.log('Runner ended, waiting for initialization and pending results...');\n\n // Wait for initialization to complete (might still be in progress)\n if (this.initPromise) {\n try {\n await this.initPromise;\n } catch {\n // Error already captured in state.initError\n }\n }\n\n // Wait for any remaining pending operations\n // (WebdriverIO waits via isSynchronised, but we also wait here for safety)\n await Promise.allSettled([...this.pendingOperations]);\n\n // Check if initialization failed\n if (this.state.initError) {\n console.error('\\n[TestPlanIt] FAILED: Reporter initialization failed');\n console.error(` Error: ${this.state.initError.message}`);\n console.error(' No results were reported to TestPlanIt.');\n console.error(' Please check your configuration and API connectivity.');\n return;\n }\n\n // If no test run was created (no reportable results), silently skip\n if (!this.state.testRunId) {\n this.log('No test run created, skipping summary');\n return;\n }\n\n // If no results were actually reported to TestPlanIt, silently skip\n // This handles the case where tests ran but none had valid case IDs\n if (this.reportedResultCount === 0) {\n this.log('No results were reported to TestPlanIt, skipping summary');\n return;\n }\n\n // Upload any pending screenshots\n // Screenshots are uploaded here (deferred) because afterTest hooks run after onTestFail/onTestPass,\n // so screenshots taken in afterTest wouldn't be available during reportResult\n if (this.reporterOptions.uploadScreenshots && this.pendingScreenshots.size > 0) {\n this.log(`Uploading screenshots for ${this.pendingScreenshots.size} test(s)...`);\n\n // Create upload promises for all screenshots and track them\n // This ensures WebdriverIO waits for uploads to complete (via isSynchronised)\n const uploadPromises: Promise[] = [];\n\n for (const [uid, screenshots] of this.pendingScreenshots.entries()) {\n const result = this.state.results.get(uid);\n if (!result?.junitResultId) {\n this.log(`Skipping screenshots for ${uid} - no JUnit result ID`);\n continue;\n }\n\n this.log(`Uploading ${screenshots.length} screenshot(s) for test:`, result.testName);\n for (let i = 0; i < screenshots.length; i++) {\n const uploadPromise = (async () => {\n try {\n // Create a meaningful file name: testName_status_screenshot#.png\n // Sanitize test name for filename (remove special chars, limit length)\n const sanitizedTestName = result.testName\n .replace(/[^a-zA-Z0-9_-]/g, '_')\n .substring(0, 50);\n const fileName = `${sanitizedTestName}_${result.status}_${i + 1}.png`;\n\n // Build a descriptive note with test context\n const noteParts: string[] = [];\n noteParts.push(`Test: ${result.testName}`);\n if (result.suiteName) {\n noteParts.push(`Suite: ${result.suiteName}`);\n }\n noteParts.push(`Status: ${result.status}`);\n if (result.browser) {\n noteParts.push(`Browser: ${result.browser}`);\n }\n if (result.errorMessage) {\n // Truncate error message if too long\n const errorPreview = result.errorMessage.length > 200\n ? result.errorMessage.substring(0, 200) + '...'\n : result.errorMessage;\n noteParts.push(`Error: ${errorPreview}`);\n }\n const note = noteParts.join('\\n');\n\n this.log(`Starting upload of ${fileName} (${screenshots[i].length} bytes) to JUnit result ${result.junitResultId}...`);\n await this.client.uploadJUnitAttachment(\n result.junitResultId!,\n screenshots[i],\n fileName,\n 'image/png',\n note\n );\n this.state.stats.screenshotsUploaded++;\n this.log(`Uploaded screenshot ${i + 1}/${screenshots.length} for ${result.testName}`);\n } catch (uploadError) {\n this.state.stats.screenshotsFailed++;\n const errorMessage = uploadError instanceof Error ? uploadError.message : String(uploadError);\n const errorStack = uploadError instanceof Error ? uploadError.stack : undefined;\n this.logError(`Failed to upload screenshot ${i + 1}:`, errorMessage);\n if (errorStack) {\n this.logError('Stack trace:', errorStack);\n }\n }\n })();\n\n // Track this operation so WebdriverIO waits for it\n this.trackOperation(uploadPromise);\n uploadPromises.push(uploadPromise);\n }\n }\n\n // Wait for all uploads to complete before proceeding\n await Promise.allSettled(uploadPromises);\n\n // Clear all pending screenshots\n this.pendingScreenshots.clear();\n }\n\n // Note: JUnit test suite statistics (tests, failures, errors, skipped, time) are NOT updated here.\n // The backend calculates these dynamically from JUnitTestResult rows in the summary API.\n // This ensures correct totals when multiple workers/spec files report to the same test run.\n\n // Complete the test run if configured\n // When managedByService is true, the service handles completion in onComplete — skip entirely\n // In legacy oneReport mode, decrement worker count and only complete when last worker finishes\n if (this.managedByService) {\n this.log('Skipping test run completion (managed by TestPlanItService)');\n } else if (this.reporterOptions.completeRunOnFinish) {\n if (this.reporterOptions.oneReport) {\n // Decrement worker count and check if we're the last worker\n const isLastWorker = decrementWorkerCount(this.reporterOptions.projectId);\n if (isLastWorker) {\n const completeRunOp = (async () => {\n try {\n await this.client.completeTestRun(this.state.testRunId!, this.reporterOptions.projectId);\n this.log('Test run completed (last worker):', this.state.testRunId);\n deleteSharedState(this.reporterOptions.projectId);\n } catch (error) {\n this.logError('Failed to complete test run:', error);\n }\n })();\n this.trackOperation(completeRunOp);\n await completeRunOp;\n } else {\n this.log('Skipping test run completion (waiting for other workers to finish)');\n }\n } else {\n const completeRunOp = (async () => {\n try {\n await this.client.completeTestRun(this.state.testRunId!, this.reporterOptions.projectId);\n this.log('Test run completed:', this.state.testRunId);\n } catch (error) {\n this.logError('Failed to complete test run:', error);\n }\n })();\n this.trackOperation(completeRunOp);\n await completeRunOp;\n }\n } else if (this.reporterOptions.oneReport) {\n // Even if not completing, decrement worker count in legacy mode\n decrementWorkerCount(this.reporterOptions.projectId);\n }\n\n // Print summary\n const stats = this.state.stats;\n const duration = ((Date.now() - stats.startTime.getTime()) / 1000).toFixed(1);\n const totalResults = stats.resultsPassed + stats.resultsFailed + stats.resultsSkipped;\n const totalCases = stats.testCasesFound + stats.testCasesCreated + stats.testCasesMoved;\n\n console.log('\\n[TestPlanIt] ═══════════════════════════════════════════════════════');\n console.log('[TestPlanIt] Results Summary');\n console.log('[TestPlanIt] ═══════════════════════════════════════════════════════');\n console.log(`[TestPlanIt] Test Run ID: ${this.state.testRunId}`);\n console.log(`[TestPlanIt] Duration: ${duration}s`);\n console.log('[TestPlanIt]');\n console.log('[TestPlanIt] Test Results:');\n console.log(`[TestPlanIt] ✓ Passed: ${stats.resultsPassed}`);\n console.log(`[TestPlanIt] ✗ Failed: ${stats.resultsFailed}`);\n console.log(`[TestPlanIt] ○ Skipped: ${stats.resultsSkipped}`);\n console.log(`[TestPlanIt] Total: ${totalResults}`);\n\n if (this.reporterOptions.autoCreateTestCases && totalCases > 0) {\n console.log('[TestPlanIt]');\n console.log('[TestPlanIt] Test Cases:');\n console.log(`[TestPlanIt] Found (existing): ${stats.testCasesFound}`);\n console.log(`[TestPlanIt] Created (new): ${stats.testCasesCreated}`);\n if (stats.testCasesMoved > 0) {\n console.log(`[TestPlanIt] Moved (restored): ${stats.testCasesMoved}`);\n }\n }\n\n if (this.reporterOptions.uploadScreenshots && (stats.screenshotsUploaded > 0 || stats.screenshotsFailed > 0)) {\n console.log('[TestPlanIt]');\n console.log('[TestPlanIt] Screenshots:');\n console.log(`[TestPlanIt] Uploaded: ${stats.screenshotsUploaded}`);\n if (stats.screenshotsFailed > 0) {\n console.log(`[TestPlanIt] Failed: ${stats.screenshotsFailed}`);\n }\n }\n\n if (stats.apiErrors > 0) {\n console.log('[TestPlanIt]');\n console.log(`[TestPlanIt] ⚠ API Errors: ${stats.apiErrors}`);\n }\n\n console.log('[TestPlanIt]');\n console.log(`[TestPlanIt] View results: ${this.reporterOptions.domain}/projects/runs/${this.reporterOptions.projectId}/${this.state.testRunId}`);\n console.log('[TestPlanIt] ═══════════════════════════════════════════════════════\\n');\n }\n\n /**\n * Get the current state (for debugging)\n */\n getState(): ReporterState {\n return this.state;\n }\n}\n","/**\n * WebdriverIO Launcher Service for TestPlanIt.\n *\n * Manages the test run lifecycle in the main WDIO process:\n * - onPrepare: Creates the test run and JUnit test suite ONCE before any workers start\n * - onComplete: Completes the test run ONCE after all workers finish\n *\n * This ensures all spec files across all worker batches report to a single test run,\n * regardless of `maxInstances` or execution order.\n *\n * @example\n * ```javascript\n * // wdio.conf.js\n * import { TestPlanItService } from '@testplanit/wdio-reporter';\n *\n * export const config = {\n * services: [\n * [TestPlanItService, {\n * domain: 'https://testplanit.example.com',\n * apiToken: process.env.TESTPLANIT_API_TOKEN,\n * projectId: 1,\n * runName: 'E2E Tests - {date}',\n * }]\n * ],\n * reporters: [\n * ['@testplanit/wdio-reporter', {\n * domain: 'https://testplanit.example.com',\n * apiToken: process.env.TESTPLANIT_API_TOKEN,\n * projectId: 1,\n * autoCreateTestCases: true,\n * parentFolderId: 10,\n * templateId: 1,\n * }]\n * ]\n * }\n * ```\n *\n * @packageDocumentation\n */\n\nimport { TestPlanItClient } from '@testplanit/api';\nimport type { TestPlanItServiceOptions } from './types.js';\nimport {\n writeSharedState,\n deleteSharedState,\n type SharedState,\n} from './shared.js';\n\n/**\n * WebdriverIO Launcher Service for TestPlanIt.\n *\n * Creates a single test run before any workers start and completes it\n * after all workers finish. Workers read the shared state file to find\n * the pre-created test run and report results to it.\n */\nexport default class TestPlanItService {\n private options: TestPlanItServiceOptions;\n private client: TestPlanItClient;\n private verbose: boolean;\n private testRunId?: number;\n private testSuiteId?: number;\n\n constructor(serviceOptions: TestPlanItServiceOptions) {\n // Validate required options\n if (!serviceOptions.domain) {\n throw new Error('TestPlanIt service: domain is required');\n }\n if (!serviceOptions.apiToken) {\n throw new Error('TestPlanIt service: apiToken is required');\n }\n if (!serviceOptions.projectId) {\n throw new Error('TestPlanIt service: projectId is required');\n }\n\n this.options = {\n completeRunOnFinish: true,\n runName: 'Automated Tests - {date} {time}',\n testRunType: 'MOCHA',\n timeout: 30000,\n maxRetries: 3,\n verbose: false,\n ...serviceOptions,\n };\n\n this.verbose = this.options.verbose ?? false;\n\n this.client = new TestPlanItClient({\n baseUrl: this.options.domain,\n apiToken: this.options.apiToken,\n timeout: this.options.timeout,\n maxRetries: this.options.maxRetries,\n });\n }\n\n /**\n * Log a message if verbose mode is enabled\n */\n private log(message: string, ...args: unknown[]): void {\n if (this.verbose) {\n console.log(`[TestPlanIt Service] ${message}`, ...args);\n }\n }\n\n /**\n * Log an error (always logs, not just in verbose mode)\n */\n private logError(message: string, error?: unknown): void {\n const errorMsg = error instanceof Error ? error.message : String(error ?? '');\n console.error(`[TestPlanIt Service] ERROR: ${message}`, errorMsg);\n }\n\n /**\n * Format run name with available placeholders.\n * Note: {browser}, {spec}, and {suite} are NOT available in the service context\n * since it runs before any workers start.\n */\n private formatRunName(template: string): string {\n const now = new Date();\n const date = now.toISOString().split('T')[0];\n const time = now.toTimeString().split(' ')[0];\n const platform = process.platform;\n\n return template\n .replace('{date}', date)\n .replace('{time}', time)\n .replace('{platform}', platform)\n .replace('{browser}', 'unknown')\n .replace('{spec}', 'unknown')\n .replace('{suite}', 'Tests');\n }\n\n /**\n * Resolve string option IDs to numeric IDs using the API client.\n */\n private async resolveIds(): Promise<{\n configId?: number;\n milestoneId?: number;\n stateId?: number;\n tagIds?: number[];\n }> {\n const projectId = this.options.projectId;\n const resolved: {\n configId?: number;\n milestoneId?: number;\n stateId?: number;\n tagIds?: number[];\n } = {};\n\n if (typeof this.options.configId === 'string') {\n const config = await this.client.findConfigurationByName(projectId, this.options.configId);\n if (!config) {\n throw new Error(`Configuration not found: \"${this.options.configId}\"`);\n }\n resolved.configId = config.id;\n this.log(`Resolved configuration \"${this.options.configId}\" -> ${config.id}`);\n } else if (typeof this.options.configId === 'number') {\n resolved.configId = this.options.configId;\n }\n\n if (typeof this.options.milestoneId === 'string') {\n const milestone = await this.client.findMilestoneByName(projectId, this.options.milestoneId);\n if (!milestone) {\n throw new Error(`Milestone not found: \"${this.options.milestoneId}\"`);\n }\n resolved.milestoneId = milestone.id;\n this.log(`Resolved milestone \"${this.options.milestoneId}\" -> ${milestone.id}`);\n } else if (typeof this.options.milestoneId === 'number') {\n resolved.milestoneId = this.options.milestoneId;\n }\n\n if (typeof this.options.stateId === 'string') {\n const state = await this.client.findWorkflowStateByName(projectId, this.options.stateId);\n if (!state) {\n throw new Error(`Workflow state not found: \"${this.options.stateId}\"`);\n }\n resolved.stateId = state.id;\n this.log(`Resolved workflow state \"${this.options.stateId}\" -> ${state.id}`);\n } else if (typeof this.options.stateId === 'number') {\n resolved.stateId = this.options.stateId;\n }\n\n if (this.options.tagIds && this.options.tagIds.length > 0) {\n resolved.tagIds = await this.client.resolveTagIds(projectId, this.options.tagIds);\n this.log(`Resolved tags: ${resolved.tagIds.join(', ')}`);\n }\n\n return resolved;\n }\n\n /**\n * onPrepare - Runs once in the main process before any workers start.\n *\n * Creates the test run and JUnit test suite, then writes shared state\n * so all worker reporters can find and use the pre-created run.\n */\n async onPrepare(): Promise {\n this.log('Preparing test run...');\n this.log(` Domain: ${this.options.domain}`);\n this.log(` Project ID: ${this.options.projectId}`);\n\n try {\n // Clean up any stale shared state from a previous run\n deleteSharedState(this.options.projectId);\n\n // Resolve string IDs to numeric IDs\n const resolved = await this.resolveIds();\n\n // Format the run name\n const runName = this.formatRunName(this.options.runName ?? 'Automated Tests - {date} {time}');\n\n // Create the test run\n this.log(`Creating test run: \"${runName}\" (type: ${this.options.testRunType})`);\n const testRun = await this.client.createTestRun({\n projectId: this.options.projectId,\n name: runName,\n testRunType: this.options.testRunType,\n configId: resolved.configId,\n milestoneId: resolved.milestoneId,\n stateId: resolved.stateId,\n tagIds: resolved.tagIds,\n });\n this.testRunId = testRun.id;\n this.log(`Created test run with ID: ${this.testRunId}`);\n\n // Create the JUnit test suite\n this.log('Creating JUnit test suite...');\n const testSuite = await this.client.createJUnitTestSuite({\n testRunId: this.testRunId,\n name: runName,\n time: 0,\n tests: 0,\n failures: 0,\n errors: 0,\n skipped: 0,\n });\n this.testSuiteId = testSuite.id;\n this.log(`Created JUnit test suite with ID: ${this.testSuiteId}`);\n\n // Write shared state file for workers to read\n const sharedState: SharedState = {\n testRunId: this.testRunId,\n testSuiteId: this.testSuiteId,\n createdAt: new Date().toISOString(),\n activeWorkers: 0, // Not used in service-managed mode\n managedByService: true,\n };\n writeSharedState(this.options.projectId, sharedState);\n this.log('Wrote shared state file for workers');\n\n // Always print this so users can see the run was created\n console.log(`[TestPlanIt Service] Test run created: \"${runName}\" (ID: ${this.testRunId})`);\n } catch (error) {\n this.logError('Failed to prepare test run:', error);\n // Clean up shared state on failure so reporters fall back to self-managed mode\n deleteSharedState(this.options.projectId);\n throw error;\n }\n }\n\n /**\n * onComplete - Runs once in the main process after all workers finish.\n *\n * Completes the test run and cleans up the shared state file.\n */\n async onComplete(exitCode: number): Promise {\n this.log(`All workers finished (exit code: ${exitCode})`);\n\n try {\n if (this.testRunId && this.options.completeRunOnFinish) {\n this.log(`Completing test run ${this.testRunId}...`);\n await this.client.completeTestRun(this.testRunId, this.options.projectId);\n this.log('Test run completed successfully');\n }\n\n // Print summary\n if (this.testRunId) {\n console.log('\\n[TestPlanIt Service] ══════════════════════════════════════════');\n console.log(`[TestPlanIt Service] Test Run ID: ${this.testRunId}`);\n if (this.options.completeRunOnFinish) {\n console.log('[TestPlanIt Service] Status: Completed');\n }\n console.log(`[TestPlanIt Service] View: ${this.options.domain}/projects/runs/${this.options.projectId}/${this.testRunId}`);\n console.log('[TestPlanIt Service] ══════════════════════════════════════════\\n');\n }\n } catch (error) {\n // Don't re-throw — failing onComplete would hide the actual test results\n this.logError('Failed to complete test run:', error);\n } finally {\n // Always clean up shared state\n deleteSharedState(this.options.projectId);\n this.log('Cleaned up shared state file');\n }\n }\n}\n"]} \ No newline at end of file diff --git a/packages/wdio-testplanit-reporter/dist/index.mjs b/packages/wdio-testplanit-reporter/dist/index.mjs index 3303a2f9..c66a85ea 100644 --- a/packages/wdio-testplanit-reporter/dist/index.mjs +++ b/packages/wdio-testplanit-reporter/dist/index.mjs @@ -5,6 +5,112 @@ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; +// src/reporter.ts +var STALE_THRESHOLD_MS = 4 * 60 * 60 * 1e3; +function getSharedStateFilePath(projectId) { + const fileName = `.testplanit-reporter-${projectId}.json`; + return path.join(os.tmpdir(), fileName); +} +function acquireLock(lockPath, maxAttempts = 10) { + for (let i = 0; i < maxAttempts; i++) { + try { + fs.writeFileSync(lockPath, process.pid.toString(), { flag: "wx" }); + return true; + } catch { + } + } + return false; +} +function releaseLock(lockPath) { + try { + fs.unlinkSync(lockPath); + } catch { + } +} +function withLock(projectId, callback) { + const filePath = getSharedStateFilePath(projectId); + const lockPath = `${filePath}.lock`; + if (!acquireLock(lockPath)) { + return void 0; + } + try { + return callback(filePath); + } finally { + releaseLock(lockPath); + } +} +function readSharedState(projectId) { + const filePath = getSharedStateFilePath(projectId); + try { + if (!fs.existsSync(filePath)) { + return null; + } + const content = fs.readFileSync(filePath, "utf-8"); + const state = JSON.parse(content); + const createdAt = new Date(state.createdAt); + const staleThreshold = new Date(Date.now() - STALE_THRESHOLD_MS); + if (createdAt < staleThreshold) { + deleteSharedState(projectId); + return null; + } + return state; + } catch { + return null; + } +} +function writeSharedState(projectId, state) { + withLock(projectId, (filePath) => { + fs.writeFileSync(filePath, JSON.stringify(state, null, 2)); + }); +} +function writeSharedStateIfAbsent(projectId, state) { + return withLock(projectId, (filePath) => { + if (fs.existsSync(filePath)) { + const content = fs.readFileSync(filePath, "utf-8"); + const existingState = JSON.parse(content); + if (!existingState.testSuiteId && state.testSuiteId) { + existingState.testSuiteId = state.testSuiteId; + fs.writeFileSync(filePath, JSON.stringify(existingState, null, 2)); + } + return existingState; + } + fs.writeFileSync(filePath, JSON.stringify(state, null, 2)); + return state; + }); +} +function deleteSharedState(projectId) { + const filePath = getSharedStateFilePath(projectId); + try { + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + } catch { + } +} +function incrementWorkerCount(projectId) { + withLock(projectId, (filePath) => { + if (fs.existsSync(filePath)) { + const content = fs.readFileSync(filePath, "utf-8"); + const state = JSON.parse(content); + state.activeWorkers = (state.activeWorkers || 0) + 1; + fs.writeFileSync(filePath, JSON.stringify(state, null, 2)); + } + }); +} +function decrementWorkerCount(projectId) { + const result = withLock(projectId, (filePath) => { + if (fs.existsSync(filePath)) { + const content = fs.readFileSync(filePath, "utf-8"); + const state = JSON.parse(content); + state.activeWorkers = Math.max(0, (state.activeWorkers || 1) - 1); + fs.writeFileSync(filePath, JSON.stringify(state, null, 2)); + return state.activeWorkers === 0; + } + return false; + }); + return result ?? false; +} + // src/reporter.ts var TestPlanItReporter = class extends WDIOReporter { client; @@ -18,6 +124,8 @@ var TestPlanItReporter = class extends WDIOReporter { currentTestUid = null; currentCid = null; pendingScreenshots = /* @__PURE__ */ new Map(); + /** When true, the TestPlanItService manages the test run lifecycle */ + managedByService = false; /** * WebdriverIO uses this getter to determine if the reporter has finished async operations. * The test runner will wait for this to return true before terminating. @@ -97,204 +205,6 @@ var TestPlanItReporter = class extends WDIOReporter { ${error.stack}` : ""; console.error(`[TestPlanIt] ERROR: ${message}`, errorMsg, stack); } - /** - * Get the path to the shared state file for oneReport mode. - * Uses a file in the temp directory with a name based on the project ID. - */ - getSharedStateFilePath() { - const fileName = `.testplanit-reporter-${this.reporterOptions.projectId}.json`; - return path.join(os.tmpdir(), fileName); - } - /** - * Read shared state from file (for oneReport mode). - * Returns null if: - * - File doesn't exist - * - File is stale (older than 4 hours) - * - Previous run completed (activeWorkers === 0) - */ - readSharedState() { - const filePath = this.getSharedStateFilePath(); - try { - if (!fs.existsSync(filePath)) { - return null; - } - const content = fs.readFileSync(filePath, "utf-8"); - const state = JSON.parse(content); - const createdAt = new Date(state.createdAt); - const fourHoursAgo = new Date(Date.now() - 4 * 60 * 60 * 1e3); - if (createdAt < fourHoursAgo) { - this.log("Shared state file is stale (older than 4 hours), starting fresh"); - this.deleteSharedState(); - return null; - } - if (state.activeWorkers === 0) { - this.log("Previous test run completed (activeWorkers=0), starting fresh"); - this.deleteSharedState(); - return null; - } - return state; - } catch (error) { - this.log("Failed to read shared state file:", error); - return null; - } - } - /** - * Write shared state to file (for oneReport mode). - * Uses a lock file to prevent race conditions. - * Only writes the testRunId if the file doesn't exist yet (first writer wins). - * Updates testSuiteId if not already set. - */ - writeSharedState(state) { - const filePath = this.getSharedStateFilePath(); - const lockPath = `${filePath}.lock`; - try { - let lockAcquired = false; - for (let i = 0; i < 10; i++) { - try { - fs.writeFileSync(lockPath, process.pid.toString(), { flag: "wx" }); - lockAcquired = true; - break; - } catch { - const sleepMs = 50 * Math.pow(2, i) + Math.random() * 50; - const start = Date.now(); - while (Date.now() - start < sleepMs) { - } - } - } - if (!lockAcquired) { - this.log("Could not acquire lock for shared state file"); - return; - } - try { - if (fs.existsSync(filePath)) { - const existingContent = fs.readFileSync(filePath, "utf-8"); - const existingState = JSON.parse(existingContent); - if (!existingState.testSuiteId && state.testSuiteId) { - existingState.testSuiteId = state.testSuiteId; - fs.writeFileSync(filePath, JSON.stringify(existingState, null, 2)); - this.log("Updated shared state file with testSuiteId:", state.testSuiteId); - } else { - this.log("Shared state file already exists with testSuiteId, not overwriting"); - } - return; - } - fs.writeFileSync(filePath, JSON.stringify(state, null, 2)); - this.log("Wrote shared state file:", filePath); - } finally { - try { - fs.unlinkSync(lockPath); - } catch { - } - } - } catch (error) { - this.log("Failed to write shared state file:", error); - } - } - /** - * Delete shared state file (cleanup after run completes). - */ - deleteSharedState() { - const filePath = this.getSharedStateFilePath(); - try { - if (fs.existsSync(filePath)) { - fs.unlinkSync(filePath); - this.log("Deleted shared state file"); - } - } catch (error) { - this.log("Failed to delete shared state file:", error); - } - } - /** - * Increment the active worker count in shared state. - * Called when a worker starts using the shared test run. - */ - incrementWorkerCount() { - const filePath = this.getSharedStateFilePath(); - const lockPath = `${filePath}.lock`; - try { - let lockAcquired = false; - for (let i = 0; i < 10; i++) { - try { - fs.writeFileSync(lockPath, process.pid.toString(), { flag: "wx" }); - lockAcquired = true; - break; - } catch { - const sleepMs = 50 * Math.pow(2, i) + Math.random() * 50; - const start = Date.now(); - while (Date.now() - start < sleepMs) { - } - } - } - if (!lockAcquired) { - this.log("Could not acquire lock to increment worker count"); - return; - } - try { - if (fs.existsSync(filePath)) { - const content = fs.readFileSync(filePath, "utf-8"); - const state = JSON.parse(content); - state.activeWorkers = (state.activeWorkers || 0) + 1; - fs.writeFileSync(filePath, JSON.stringify(state, null, 2)); - this.log("Incremented worker count to:", state.activeWorkers); - } - } finally { - try { - fs.unlinkSync(lockPath); - } catch { - } - } - } catch (error) { - this.log("Failed to increment worker count:", error); - } - } - /** - * Decrement the active worker count in shared state. - * Returns true if this was the last worker (count reached 0). - */ - decrementWorkerCount() { - const filePath = this.getSharedStateFilePath(); - const lockPath = `${filePath}.lock`; - try { - let lockAcquired = false; - for (let i = 0; i < 10; i++) { - try { - fs.writeFileSync(lockPath, process.pid.toString(), { flag: "wx" }); - lockAcquired = true; - break; - } catch { - const sleepMs = 50 * Math.pow(2, i) + Math.random() * 50; - const start = Date.now(); - while (Date.now() - start < sleepMs) { - } - } - } - if (!lockAcquired) { - this.log("Could not acquire lock to decrement worker count"); - return false; - } - try { - if (fs.existsSync(filePath)) { - const content = fs.readFileSync(filePath, "utf-8"); - const state = JSON.parse(content); - state.activeWorkers = Math.max(0, (state.activeWorkers || 1) - 1); - fs.writeFileSync(filePath, JSON.stringify(state, null, 2)); - this.log("Decremented worker count to:", state.activeWorkers); - if (state.activeWorkers === 0) { - this.log("This is the last worker"); - return true; - } - } - } finally { - try { - fs.unlinkSync(lockPath); - } catch { - } - } - } catch (error) { - this.log("Failed to decrement worker count:", error); - } - return false; - } /** * Track an async operation to prevent the runner from terminating early. * The operation is added to pendingOperations and removed when complete. @@ -329,47 +239,59 @@ ${error.stack}` : ""; this.log("Fetching status mappings..."); await this.fetchStatusMappings(); if (this.reporterOptions.oneReport && !this.state.testRunId) { - const sharedState = this.readSharedState(); + const sharedState = readSharedState(this.reporterOptions.projectId); if (sharedState) { - this.state.testRunId = sharedState.testRunId; - this.state.testSuiteId = sharedState.testSuiteId; - this.log(`Using shared test run from file: ${sharedState.testRunId}`); - try { - const testRun = await this.client.getTestRun(this.state.testRunId); - if (testRun.isDeleted) { - this.log(`Shared test run ${testRun.id} is deleted, starting fresh`); - this.state.testRunId = void 0; - this.state.testSuiteId = void 0; - this.deleteSharedState(); - } else if (testRun.isCompleted) { - this.log(`Shared test run ${testRun.id} is already completed, starting fresh`); + if (sharedState.managedByService) { + this.state.testRunId = sharedState.testRunId; + this.state.testSuiteId = sharedState.testSuiteId; + this.managedByService = true; + this.log(`Using service-managed test run: ${sharedState.testRunId}`); + } else { + this.state.testRunId = sharedState.testRunId; + this.state.testSuiteId = sharedState.testSuiteId; + this.log(`Using shared test run from file: ${sharedState.testRunId}`); + if (sharedState.activeWorkers === 0) { + this.log("Previous test run completed (activeWorkers=0), starting fresh"); + deleteSharedState(this.reporterOptions.projectId); this.state.testRunId = void 0; this.state.testSuiteId = void 0; - this.deleteSharedState(); } else { - this.log(`Validated shared test run: ${testRun.name} (ID: ${testRun.id})`); - this.incrementWorkerCount(); + try { + const testRun = await this.client.getTestRun(this.state.testRunId); + if (testRun.isDeleted) { + this.log(`Shared test run ${testRun.id} is deleted, starting fresh`); + this.state.testRunId = void 0; + this.state.testSuiteId = void 0; + deleteSharedState(this.reporterOptions.projectId); + } else if (testRun.isCompleted) { + this.log(`Shared test run ${testRun.id} is already completed, starting fresh`); + this.state.testRunId = void 0; + this.state.testSuiteId = void 0; + deleteSharedState(this.reporterOptions.projectId); + } else { + this.log(`Validated shared test run: ${testRun.name} (ID: ${testRun.id})`); + incrementWorkerCount(this.reporterOptions.projectId); + } + } catch { + this.log("Shared test run no longer exists, will create new one"); + this.state.testRunId = void 0; + this.state.testSuiteId = void 0; + deleteSharedState(this.reporterOptions.projectId); + } } - } catch { - this.log("Shared test run no longer exists, will create new one"); - this.state.testRunId = void 0; - this.state.testSuiteId = void 0; - this.deleteSharedState(); } } } - if (!this.state.testRunId) { + if (!this.state.testRunId && !this.managedByService) { if (this.reporterOptions.oneReport) { await this.createTestRun(); this.log(`Created test run with ID: ${this.state.testRunId}`); - this.writeSharedState({ + const finalState = writeSharedStateIfAbsent(this.reporterOptions.projectId, { testRunId: this.state.testRunId, testSuiteId: this.state.testSuiteId, createdAt: (/* @__PURE__ */ new Date()).toISOString(), activeWorkers: 1 - // First worker }); - const finalState = this.readSharedState(); if (finalState && finalState.testRunId !== this.state.testRunId) { this.log(`Another worker created test run first, switching from ${this.state.testRunId} to ${finalState.testRunId}`); this.state.testRunId = finalState.testRunId; @@ -379,7 +301,7 @@ ${error.stack}` : ""; await this.createTestRun(); this.log(`Created test run with ID: ${this.state.testRunId}`); } - } else if (!this.reporterOptions.oneReport) { + } else if (this.state.testRunId && !this.reporterOptions.oneReport && !this.managedByService) { try { const testRun = await this.client.getTestRun(this.state.testRunId); this.log(`Using existing test run: ${testRun.name} (ID: ${testRun.id})`); @@ -516,7 +438,7 @@ ${error.stack}` : ""; throw new Error("Cannot create JUnit test suite without a test run ID"); } if (this.reporterOptions.oneReport) { - const sharedState = this.readSharedState(); + const sharedState = readSharedState(this.reporterOptions.projectId); if (sharedState?.testSuiteId) { this.state.testSuiteId = sharedState.testSuiteId; this.log("Using shared JUnit test suite from file:", sharedState.testSuiteId); @@ -538,14 +460,12 @@ ${error.stack}` : ""; this.state.testSuiteId = testSuite.id; this.log("Created JUnit test suite with ID:", testSuite.id); if (this.reporterOptions.oneReport) { - this.writeSharedState({ + const finalState = writeSharedStateIfAbsent(this.reporterOptions.projectId, { testRunId: this.state.testRunId, testSuiteId: this.state.testSuiteId, createdAt: (/* @__PURE__ */ new Date()).toISOString(), activeWorkers: 1 - // Will be merged/updated by writeSharedState }); - const finalState = this.readSharedState(); if (finalState && finalState.testSuiteId !== this.state.testSuiteId) { this.log(`Another worker created test suite first, switching from ${this.state.testSuiteId} to ${finalState.testSuiteId}`); this.state.testSuiteId = finalState.testSuiteId; @@ -999,15 +919,17 @@ ${error.stack}` : ""; await Promise.allSettled(uploadPromises); this.pendingScreenshots.clear(); } - if (this.reporterOptions.completeRunOnFinish) { + if (this.managedByService) { + this.log("Skipping test run completion (managed by TestPlanItService)"); + } else if (this.reporterOptions.completeRunOnFinish) { if (this.reporterOptions.oneReport) { - const isLastWorker = this.decrementWorkerCount(); + const isLastWorker = decrementWorkerCount(this.reporterOptions.projectId); if (isLastWorker) { const completeRunOp = (async () => { try { await this.client.completeTestRun(this.state.testRunId, this.reporterOptions.projectId); this.log("Test run completed (last worker):", this.state.testRunId); - this.deleteSharedState(); + deleteSharedState(this.reporterOptions.projectId); } catch (error) { this.logError("Failed to complete test run:", error); } @@ -1030,7 +952,7 @@ ${error.stack}` : ""; await completeRunOp; } } else if (this.reporterOptions.oneReport) { - this.decrementWorkerCount(); + decrementWorkerCount(this.reporterOptions.projectId); } const stats = this.state.stats; const duration = ((Date.now() - stats.startTime.getTime()) / 1e3).toFixed(1); @@ -1079,7 +1001,194 @@ ${error.stack}` : ""; return this.state; } }; +var TestPlanItService = class { + options; + client; + verbose; + testRunId; + testSuiteId; + constructor(serviceOptions) { + if (!serviceOptions.domain) { + throw new Error("TestPlanIt service: domain is required"); + } + if (!serviceOptions.apiToken) { + throw new Error("TestPlanIt service: apiToken is required"); + } + if (!serviceOptions.projectId) { + throw new Error("TestPlanIt service: projectId is required"); + } + this.options = { + completeRunOnFinish: true, + runName: "Automated Tests - {date} {time}", + testRunType: "MOCHA", + timeout: 3e4, + maxRetries: 3, + verbose: false, + ...serviceOptions + }; + this.verbose = this.options.verbose ?? false; + this.client = new TestPlanItClient({ + baseUrl: this.options.domain, + apiToken: this.options.apiToken, + timeout: this.options.timeout, + maxRetries: this.options.maxRetries + }); + } + /** + * Log a message if verbose mode is enabled + */ + log(message, ...args) { + if (this.verbose) { + console.log(`[TestPlanIt Service] ${message}`, ...args); + } + } + /** + * Log an error (always logs, not just in verbose mode) + */ + logError(message, error) { + const errorMsg = error instanceof Error ? error.message : String(error ?? ""); + console.error(`[TestPlanIt Service] ERROR: ${message}`, errorMsg); + } + /** + * Format run name with available placeholders. + * Note: {browser}, {spec}, and {suite} are NOT available in the service context + * since it runs before any workers start. + */ + formatRunName(template) { + const now = /* @__PURE__ */ new Date(); + const date = now.toISOString().split("T")[0]; + const time = now.toTimeString().split(" ")[0]; + const platform = process.platform; + return template.replace("{date}", date).replace("{time}", time).replace("{platform}", platform).replace("{browser}", "unknown").replace("{spec}", "unknown").replace("{suite}", "Tests"); + } + /** + * Resolve string option IDs to numeric IDs using the API client. + */ + async resolveIds() { + const projectId = this.options.projectId; + const resolved = {}; + if (typeof this.options.configId === "string") { + const config = await this.client.findConfigurationByName(projectId, this.options.configId); + if (!config) { + throw new Error(`Configuration not found: "${this.options.configId}"`); + } + resolved.configId = config.id; + this.log(`Resolved configuration "${this.options.configId}" -> ${config.id}`); + } else if (typeof this.options.configId === "number") { + resolved.configId = this.options.configId; + } + if (typeof this.options.milestoneId === "string") { + const milestone = await this.client.findMilestoneByName(projectId, this.options.milestoneId); + if (!milestone) { + throw new Error(`Milestone not found: "${this.options.milestoneId}"`); + } + resolved.milestoneId = milestone.id; + this.log(`Resolved milestone "${this.options.milestoneId}" -> ${milestone.id}`); + } else if (typeof this.options.milestoneId === "number") { + resolved.milestoneId = this.options.milestoneId; + } + if (typeof this.options.stateId === "string") { + const state = await this.client.findWorkflowStateByName(projectId, this.options.stateId); + if (!state) { + throw new Error(`Workflow state not found: "${this.options.stateId}"`); + } + resolved.stateId = state.id; + this.log(`Resolved workflow state "${this.options.stateId}" -> ${state.id}`); + } else if (typeof this.options.stateId === "number") { + resolved.stateId = this.options.stateId; + } + if (this.options.tagIds && this.options.tagIds.length > 0) { + resolved.tagIds = await this.client.resolveTagIds(projectId, this.options.tagIds); + this.log(`Resolved tags: ${resolved.tagIds.join(", ")}`); + } + return resolved; + } + /** + * onPrepare - Runs once in the main process before any workers start. + * + * Creates the test run and JUnit test suite, then writes shared state + * so all worker reporters can find and use the pre-created run. + */ + async onPrepare() { + this.log("Preparing test run..."); + this.log(` Domain: ${this.options.domain}`); + this.log(` Project ID: ${this.options.projectId}`); + try { + deleteSharedState(this.options.projectId); + const resolved = await this.resolveIds(); + const runName = this.formatRunName(this.options.runName ?? "Automated Tests - {date} {time}"); + this.log(`Creating test run: "${runName}" (type: ${this.options.testRunType})`); + const testRun = await this.client.createTestRun({ + projectId: this.options.projectId, + name: runName, + testRunType: this.options.testRunType, + configId: resolved.configId, + milestoneId: resolved.milestoneId, + stateId: resolved.stateId, + tagIds: resolved.tagIds + }); + this.testRunId = testRun.id; + this.log(`Created test run with ID: ${this.testRunId}`); + this.log("Creating JUnit test suite..."); + const testSuite = await this.client.createJUnitTestSuite({ + testRunId: this.testRunId, + name: runName, + time: 0, + tests: 0, + failures: 0, + errors: 0, + skipped: 0 + }); + this.testSuiteId = testSuite.id; + this.log(`Created JUnit test suite with ID: ${this.testSuiteId}`); + const sharedState = { + testRunId: this.testRunId, + testSuiteId: this.testSuiteId, + createdAt: (/* @__PURE__ */ new Date()).toISOString(), + activeWorkers: 0, + // Not used in service-managed mode + managedByService: true + }; + writeSharedState(this.options.projectId, sharedState); + this.log("Wrote shared state file for workers"); + console.log(`[TestPlanIt Service] Test run created: "${runName}" (ID: ${this.testRunId})`); + } catch (error) { + this.logError("Failed to prepare test run:", error); + deleteSharedState(this.options.projectId); + throw error; + } + } + /** + * onComplete - Runs once in the main process after all workers finish. + * + * Completes the test run and cleans up the shared state file. + */ + async onComplete(exitCode) { + this.log(`All workers finished (exit code: ${exitCode})`); + try { + if (this.testRunId && this.options.completeRunOnFinish) { + this.log(`Completing test run ${this.testRunId}...`); + await this.client.completeTestRun(this.testRunId, this.options.projectId); + this.log("Test run completed successfully"); + } + if (this.testRunId) { + console.log("\n[TestPlanIt Service] \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"); + console.log(`[TestPlanIt Service] Test Run ID: ${this.testRunId}`); + if (this.options.completeRunOnFinish) { + console.log("[TestPlanIt Service] Status: Completed"); + } + console.log(`[TestPlanIt Service] View: ${this.options.domain}/projects/runs/${this.options.projectId}/${this.testRunId}`); + console.log("[TestPlanIt Service] \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n"); + } + } catch (error) { + this.logError("Failed to complete test run:", error); + } finally { + deleteSharedState(this.options.projectId); + this.log("Cleaned up shared state file"); + } + } +}; -export { TestPlanItReporter, TestPlanItReporter as default }; +export { TestPlanItReporter, TestPlanItService, TestPlanItReporter as default }; //# sourceMappingURL=index.mjs.map //# sourceMappingURL=index.mjs.map \ No newline at end of file diff --git a/packages/wdio-testplanit-reporter/dist/index.mjs.map b/packages/wdio-testplanit-reporter/dist/index.mjs.map index 08ab06e9..82ab849d 100644 --- a/packages/wdio-testplanit-reporter/dist/index.mjs.map +++ b/packages/wdio-testplanit-reporter/dist/index.mjs.map @@ -1 +1 @@ -{"version":3,"sources":["../src/reporter.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["import WDIOReporter, { type RunnerStats, type SuiteStats, type TestStats, type AfterCommandArgs } from '@wdio/reporter';\nimport { TestPlanItClient } from '@testplanit/api';\nimport type { NormalizedStatus, JUnitResultType } from '@testplanit/api';\nimport type { TestPlanItReporterOptions, TrackedTestResult, ReporterState } from './types.js';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport * as os from 'os';\n\n/**\n * Shared state file for oneReport mode.\n * Contains the test run ID shared across all worker instances.\n */\ninterface SharedState {\n testRunId: number;\n testSuiteId?: number;\n createdAt: string;\n /** Number of active workers using this test run */\n activeWorkers: number;\n}\n\n/**\n * WebdriverIO Reporter for TestPlanIt\n *\n * Reports test results directly to your TestPlanIt instance.\n *\n * @example\n * ```javascript\n * // wdio.conf.js\n * export const config = {\n * reporters: [\n * ['@testplanit/wdio-reporter', {\n * domain: 'https://testplanit.example.com',\n * apiToken: process.env.TESTPLANIT_API_TOKEN,\n * projectId: 1,\n * runName: 'E2E Tests - {date}',\n * }]\n * ]\n * }\n * ```\n */\nexport default class TestPlanItReporter extends WDIOReporter {\n private client: TestPlanItClient;\n private reporterOptions: TestPlanItReporterOptions;\n private state: ReporterState;\n private currentSuite: string[] = [];\n private initPromise: Promise | null = null;\n private pendingOperations: Set> = new Set();\n private reportedResultCount = 0;\n private detectedFramework: string | null = null;\n private currentTestUid: string | null = null;\n private currentCid: string | null = null;\n private pendingScreenshots: Map = new Map();\n\n /**\n * WebdriverIO uses this getter to determine if the reporter has finished async operations.\n * The test runner will wait for this to return true before terminating.\n */\n get isSynchronised(): boolean {\n return this.pendingOperations.size === 0;\n }\n\n constructor(options: TestPlanItReporterOptions) {\n super(options);\n\n this.reporterOptions = {\n caseIdPattern: /\\[(\\d+)\\]/g,\n autoCreateTestCases: false,\n createFolderHierarchy: false,\n uploadScreenshots: true,\n includeStackTrace: true,\n completeRunOnFinish: true,\n oneReport: true,\n timeout: 30000,\n maxRetries: 3,\n verbose: false,\n ...options,\n };\n\n // Validate required options\n if (!this.reporterOptions.domain) {\n throw new Error('TestPlanIt reporter: domain is required');\n }\n if (!this.reporterOptions.apiToken) {\n throw new Error('TestPlanIt reporter: apiToken is required');\n }\n if (!this.reporterOptions.projectId) {\n throw new Error('TestPlanIt reporter: projectId is required');\n }\n\n // Initialize API client\n this.client = new TestPlanItClient({\n baseUrl: this.reporterOptions.domain,\n apiToken: this.reporterOptions.apiToken,\n timeout: this.reporterOptions.timeout,\n maxRetries: this.reporterOptions.maxRetries,\n });\n\n // Initialize state - testRunId will be resolved during initialization\n this.state = {\n testRunId: typeof this.reporterOptions.testRunId === 'number' ? this.reporterOptions.testRunId : undefined,\n resolvedIds: {},\n results: new Map(),\n caseIdMap: new Map(),\n testRunCaseMap: new Map(),\n folderPathMap: new Map(),\n statusIds: {},\n initialized: false,\n stats: {\n testCasesFound: 0,\n testCasesCreated: 0,\n testCasesMoved: 0,\n foldersCreated: 0,\n resultsPassed: 0,\n resultsFailed: 0,\n resultsSkipped: 0,\n screenshotsUploaded: 0,\n screenshotsFailed: 0,\n apiErrors: 0,\n apiRequests: 0,\n startTime: new Date(),\n },\n };\n }\n\n /**\n * Log a message if verbose mode is enabled\n */\n private log(message: string, ...args: unknown[]): void {\n if (this.reporterOptions.verbose) {\n console.log(`[TestPlanIt] ${message}`, ...args);\n }\n }\n\n /**\n * Log an error (always logs, not just in verbose mode)\n */\n private logError(message: string, error?: unknown): void {\n const errorMsg = error instanceof Error ? error.message : String(error ?? '');\n const stack = error instanceof Error && error.stack ? `\\n${error.stack}` : '';\n console.error(`[TestPlanIt] ERROR: ${message}`, errorMsg, stack);\n }\n\n /**\n * Get the path to the shared state file for oneReport mode.\n * Uses a file in the temp directory with a name based on the project ID.\n */\n private getSharedStateFilePath(): string {\n const fileName = `.testplanit-reporter-${this.reporterOptions.projectId}.json`;\n return path.join(os.tmpdir(), fileName);\n }\n\n /**\n * Read shared state from file (for oneReport mode).\n * Returns null if:\n * - File doesn't exist\n * - File is stale (older than 4 hours)\n * - Previous run completed (activeWorkers === 0)\n */\n private readSharedState(): SharedState | null {\n const filePath = this.getSharedStateFilePath();\n try {\n if (!fs.existsSync(filePath)) {\n return null;\n }\n const content = fs.readFileSync(filePath, 'utf-8');\n const state: SharedState = JSON.parse(content);\n\n // Check if state is stale (older than 4 hours)\n const createdAt = new Date(state.createdAt);\n const fourHoursAgo = new Date(Date.now() - 4 * 60 * 60 * 1000);\n if (createdAt < fourHoursAgo) {\n this.log('Shared state file is stale (older than 4 hours), starting fresh');\n this.deleteSharedState();\n return null;\n }\n\n // Check if previous run completed (no active workers)\n // This ensures each new test execution creates a new test run\n if (state.activeWorkers === 0) {\n this.log('Previous test run completed (activeWorkers=0), starting fresh');\n this.deleteSharedState();\n return null;\n }\n\n return state;\n } catch (error) {\n this.log('Failed to read shared state file:', error);\n return null;\n }\n }\n\n /**\n * Write shared state to file (for oneReport mode).\n * Uses a lock file to prevent race conditions.\n * Only writes the testRunId if the file doesn't exist yet (first writer wins).\n * Updates testSuiteId if not already set.\n */\n private writeSharedState(state: SharedState): void {\n const filePath = this.getSharedStateFilePath();\n const lockPath = `${filePath}.lock`;\n\n try {\n // Simple lock mechanism - try to create lock file exclusively\n let lockAcquired = false;\n for (let i = 0; i < 10; i++) {\n try {\n fs.writeFileSync(lockPath, process.pid.toString(), { flag: 'wx' });\n lockAcquired = true;\n break;\n } catch {\n // Lock exists, wait and retry with exponential backoff\n const sleepMs = 50 * Math.pow(2, i) + Math.random() * 50;\n // Use setTimeout-based sleep since Atomics.wait requires SharedArrayBuffer\n const start = Date.now();\n while (Date.now() - start < sleepMs) {\n // Busy wait (not ideal but works synchronously)\n }\n }\n }\n\n if (!lockAcquired) {\n this.log('Could not acquire lock for shared state file');\n return;\n }\n\n try {\n // Check if file already exists\n if (fs.existsSync(filePath)) {\n // Read existing state and merge - only update testSuiteId if not set\n const existingContent = fs.readFileSync(filePath, 'utf-8');\n const existingState: SharedState = JSON.parse(existingContent);\n\n // Only update if the testSuiteId is missing and we have one to add\n if (!existingState.testSuiteId && state.testSuiteId) {\n existingState.testSuiteId = state.testSuiteId;\n fs.writeFileSync(filePath, JSON.stringify(existingState, null, 2));\n this.log('Updated shared state file with testSuiteId:', state.testSuiteId);\n } else {\n this.log('Shared state file already exists with testSuiteId, not overwriting');\n }\n return;\n }\n fs.writeFileSync(filePath, JSON.stringify(state, null, 2));\n this.log('Wrote shared state file:', filePath);\n } finally {\n // Release lock\n try {\n fs.unlinkSync(lockPath);\n } catch {\n // Ignore lock removal errors\n }\n }\n } catch (error) {\n this.log('Failed to write shared state file:', error);\n }\n }\n\n /**\n * Delete shared state file (cleanup after run completes).\n */\n private deleteSharedState(): void {\n const filePath = this.getSharedStateFilePath();\n try {\n if (fs.existsSync(filePath)) {\n fs.unlinkSync(filePath);\n this.log('Deleted shared state file');\n }\n } catch (error) {\n this.log('Failed to delete shared state file:', error);\n }\n }\n\n /**\n * Increment the active worker count in shared state.\n * Called when a worker starts using the shared test run.\n */\n private incrementWorkerCount(): void {\n const filePath = this.getSharedStateFilePath();\n const lockPath = `${filePath}.lock`;\n\n try {\n // Acquire lock\n let lockAcquired = false;\n for (let i = 0; i < 10; i++) {\n try {\n fs.writeFileSync(lockPath, process.pid.toString(), { flag: 'wx' });\n lockAcquired = true;\n break;\n } catch {\n const sleepMs = 50 * Math.pow(2, i) + Math.random() * 50;\n const start = Date.now();\n while (Date.now() - start < sleepMs) {\n // Busy wait\n }\n }\n }\n\n if (!lockAcquired) {\n this.log('Could not acquire lock to increment worker count');\n return;\n }\n\n try {\n if (fs.existsSync(filePath)) {\n const content = fs.readFileSync(filePath, 'utf-8');\n const state: SharedState = JSON.parse(content);\n state.activeWorkers = (state.activeWorkers || 0) + 1;\n fs.writeFileSync(filePath, JSON.stringify(state, null, 2));\n this.log('Incremented worker count to:', state.activeWorkers);\n }\n } finally {\n try {\n fs.unlinkSync(lockPath);\n } catch {\n // Ignore\n }\n }\n } catch (error) {\n this.log('Failed to increment worker count:', error);\n }\n }\n\n /**\n * Decrement the active worker count in shared state.\n * Returns true if this was the last worker (count reached 0).\n */\n private decrementWorkerCount(): boolean {\n const filePath = this.getSharedStateFilePath();\n const lockPath = `${filePath}.lock`;\n\n try {\n // Acquire lock\n let lockAcquired = false;\n for (let i = 0; i < 10; i++) {\n try {\n fs.writeFileSync(lockPath, process.pid.toString(), { flag: 'wx' });\n lockAcquired = true;\n break;\n } catch {\n const sleepMs = 50 * Math.pow(2, i) + Math.random() * 50;\n const start = Date.now();\n while (Date.now() - start < sleepMs) {\n // Busy wait\n }\n }\n }\n\n if (!lockAcquired) {\n this.log('Could not acquire lock to decrement worker count');\n return false;\n }\n\n try {\n if (fs.existsSync(filePath)) {\n const content = fs.readFileSync(filePath, 'utf-8');\n const state: SharedState = JSON.parse(content);\n state.activeWorkers = Math.max(0, (state.activeWorkers || 1) - 1);\n fs.writeFileSync(filePath, JSON.stringify(state, null, 2));\n this.log('Decremented worker count to:', state.activeWorkers);\n\n if (state.activeWorkers === 0) {\n this.log('This is the last worker');\n return true;\n }\n }\n } finally {\n try {\n fs.unlinkSync(lockPath);\n } catch {\n // Ignore\n }\n }\n } catch (error) {\n this.log('Failed to decrement worker count:', error);\n }\n return false;\n }\n\n /**\n * Track an async operation to prevent the runner from terminating early.\n * The operation is added to pendingOperations and removed when complete.\n * WebdriverIO checks isSynchronised and waits until all operations finish.\n */\n private trackOperation(operation: Promise): void {\n this.pendingOperations.add(operation);\n operation.finally(() => {\n this.pendingOperations.delete(operation);\n });\n }\n\n /**\n * Initialize the reporter (create test run, fetch statuses)\n */\n private async initialize(): Promise {\n // If already initialized successfully, return immediately\n if (this.state.initialized) return;\n\n // If we have a previous error, throw it again to prevent retrying\n if (this.state.initError) {\n throw this.state.initError;\n }\n\n // If initialization is in progress, wait for it\n if (this.initPromise) return this.initPromise;\n\n this.initPromise = this.doInitialize();\n return this.initPromise;\n }\n\n private async doInitialize(): Promise {\n try {\n // Log initialization start (only happens when we have results to report)\n this.log('Initializing reporter...');\n this.log(` Domain: ${this.reporterOptions.domain}`);\n this.log(` Project ID: ${this.reporterOptions.projectId}`);\n this.log(` oneReport: ${this.reporterOptions.oneReport}`);\n\n // Resolve any string IDs to numeric IDs\n this.log('Resolving option IDs...');\n await this.resolveOptionIds();\n\n // Fetch status mappings\n this.log('Fetching status mappings...');\n await this.fetchStatusMappings();\n\n // Handle oneReport mode - check for existing shared state\n if (this.reporterOptions.oneReport && !this.state.testRunId) {\n const sharedState = this.readSharedState();\n if (sharedState) {\n this.state.testRunId = sharedState.testRunId;\n this.state.testSuiteId = sharedState.testSuiteId;\n this.log(`Using shared test run from file: ${sharedState.testRunId}`);\n // Validate the shared test run still exists, is not completed, and is not deleted\n try {\n const testRun = await this.client.getTestRun(this.state.testRunId);\n if (testRun.isDeleted) {\n // Test run was soft-deleted\n this.log(`Shared test run ${testRun.id} is deleted, starting fresh`);\n this.state.testRunId = undefined;\n this.state.testSuiteId = undefined;\n this.deleteSharedState();\n } else if (testRun.isCompleted) {\n // Test run was already completed (from a previous execution)\n this.log(`Shared test run ${testRun.id} is already completed, starting fresh`);\n this.state.testRunId = undefined;\n this.state.testSuiteId = undefined;\n this.deleteSharedState();\n } else {\n this.log(`Validated shared test run: ${testRun.name} (ID: ${testRun.id})`);\n // Increment worker count since we're joining an existing run\n this.incrementWorkerCount();\n }\n } catch {\n // Shared test run no longer exists, clear state and create new one\n this.log('Shared test run no longer exists, will create new one');\n this.state.testRunId = undefined;\n this.state.testSuiteId = undefined;\n this.deleteSharedState();\n }\n }\n }\n\n // Create or validate test run\n if (!this.state.testRunId) {\n // In oneReport mode, use atomic write to prevent race conditions\n if (this.reporterOptions.oneReport) {\n // Create the test run first\n await this.createTestRun();\n this.log(`Created test run with ID: ${this.state.testRunId}`);\n\n // Try to write shared state - this will fail if another worker already wrote\n this.writeSharedState({\n testRunId: this.state.testRunId!,\n testSuiteId: this.state.testSuiteId,\n createdAt: new Date().toISOString(),\n activeWorkers: 1, // First worker\n });\n\n // Re-check shared state to see if we won the race\n const finalState = this.readSharedState();\n if (finalState && finalState.testRunId !== this.state.testRunId) {\n // Another worker created a test run first - use theirs instead\n this.log(`Another worker created test run first, switching from ${this.state.testRunId} to ${finalState.testRunId}`);\n this.state.testRunId = finalState.testRunId;\n this.state.testSuiteId = finalState.testSuiteId;\n }\n } else {\n await this.createTestRun();\n this.log(`Created test run with ID: ${this.state.testRunId}`);\n }\n } else if (!this.reporterOptions.oneReport) {\n // Only validate if not using oneReport (already validated above)\n // Validate existing test run\n try {\n const testRun = await this.client.getTestRun(this.state.testRunId);\n this.log(`Using existing test run: ${testRun.name} (ID: ${testRun.id})`);\n } catch (error) {\n throw new Error(`Test run ${this.state.testRunId} not found or not accessible`);\n }\n }\n\n this.state.initialized = true;\n this.log('Reporter initialized successfully');\n } catch (error) {\n this.state.initError = error instanceof Error ? error : new Error(String(error));\n this.logError('Failed to initialize reporter:', error);\n throw error;\n }\n }\n\n /**\n * Resolve option names to numeric IDs\n */\n private async resolveOptionIds(): Promise {\n const projectId = this.reporterOptions.projectId;\n\n // Resolve testRunId if it's a string\n if (typeof this.reporterOptions.testRunId === 'string') {\n const testRun = await this.client.findTestRunByName(projectId, this.reporterOptions.testRunId);\n if (!testRun) {\n throw new Error(`Test run not found: \"${this.reporterOptions.testRunId}\"`);\n }\n this.state.testRunId = testRun.id;\n this.state.resolvedIds.testRunId = testRun.id;\n this.log(`Resolved test run \"${this.reporterOptions.testRunId}\" -> ${testRun.id}`);\n }\n\n // Resolve configId if it's a string\n if (typeof this.reporterOptions.configId === 'string') {\n const config = await this.client.findConfigurationByName(projectId, this.reporterOptions.configId);\n if (!config) {\n throw new Error(`Configuration not found: \"${this.reporterOptions.configId}\"`);\n }\n this.state.resolvedIds.configId = config.id;\n this.log(`Resolved configuration \"${this.reporterOptions.configId}\" -> ${config.id}`);\n } else if (typeof this.reporterOptions.configId === 'number') {\n this.state.resolvedIds.configId = this.reporterOptions.configId;\n }\n\n // Resolve milestoneId if it's a string\n if (typeof this.reporterOptions.milestoneId === 'string') {\n const milestone = await this.client.findMilestoneByName(projectId, this.reporterOptions.milestoneId);\n if (!milestone) {\n throw new Error(`Milestone not found: \"${this.reporterOptions.milestoneId}\"`);\n }\n this.state.resolvedIds.milestoneId = milestone.id;\n this.log(`Resolved milestone \"${this.reporterOptions.milestoneId}\" -> ${milestone.id}`);\n } else if (typeof this.reporterOptions.milestoneId === 'number') {\n this.state.resolvedIds.milestoneId = this.reporterOptions.milestoneId;\n }\n\n // Resolve stateId if it's a string\n if (typeof this.reporterOptions.stateId === 'string') {\n const state = await this.client.findWorkflowStateByName(projectId, this.reporterOptions.stateId);\n if (!state) {\n throw new Error(`Workflow state not found: \"${this.reporterOptions.stateId}\"`);\n }\n this.state.resolvedIds.stateId = state.id;\n this.log(`Resolved workflow state \"${this.reporterOptions.stateId}\" -> ${state.id}`);\n } else if (typeof this.reporterOptions.stateId === 'number') {\n this.state.resolvedIds.stateId = this.reporterOptions.stateId;\n }\n\n // Resolve parentFolderId if it's a string\n if (typeof this.reporterOptions.parentFolderId === 'string') {\n let folder = await this.client.findFolderByName(projectId, this.reporterOptions.parentFolderId);\n if (!folder) {\n // If createFolderHierarchy is enabled, create the parent folder\n if (this.reporterOptions.createFolderHierarchy) {\n this.log(`Parent folder \"${this.reporterOptions.parentFolderId}\" not found, creating it...`);\n folder = await this.client.createFolder({\n projectId,\n name: this.reporterOptions.parentFolderId,\n });\n this.log(`Created parent folder \"${this.reporterOptions.parentFolderId}\" -> ${folder.id}`);\n } else {\n throw new Error(`Folder not found: \"${this.reporterOptions.parentFolderId}\"`);\n }\n }\n this.state.resolvedIds.parentFolderId = folder.id;\n this.log(`Resolved folder \"${this.reporterOptions.parentFolderId}\" -> ${folder.id}`);\n } else if (typeof this.reporterOptions.parentFolderId === 'number') {\n this.state.resolvedIds.parentFolderId = this.reporterOptions.parentFolderId;\n }\n\n // Resolve templateId if it's a string\n if (typeof this.reporterOptions.templateId === 'string') {\n const template = await this.client.findTemplateByName(projectId, this.reporterOptions.templateId);\n if (!template) {\n throw new Error(`Template not found: \"${this.reporterOptions.templateId}\"`);\n }\n this.state.resolvedIds.templateId = template.id;\n this.log(`Resolved template \"${this.reporterOptions.templateId}\" -> ${template.id}`);\n } else if (typeof this.reporterOptions.templateId === 'number') {\n this.state.resolvedIds.templateId = this.reporterOptions.templateId;\n }\n\n // Resolve tagIds if they contain strings\n if (this.reporterOptions.tagIds && this.reporterOptions.tagIds.length > 0) {\n this.state.resolvedIds.tagIds = await this.client.resolveTagIds(projectId, this.reporterOptions.tagIds);\n this.log(`Resolved tags: ${this.state.resolvedIds.tagIds.join(', ')}`);\n }\n }\n\n /**\n * Fetch status ID mappings from TestPlanIt\n */\n private async fetchStatusMappings(): Promise {\n const statuses: NormalizedStatus[] = ['passed', 'failed', 'skipped', 'blocked'];\n\n for (const status of statuses) {\n const statusId = await this.client.getStatusId(this.reporterOptions.projectId, status);\n if (statusId) {\n this.state.statusIds[status] = statusId;\n this.log(`Status mapping: ${status} -> ${statusId}`);\n }\n }\n\n if (!this.state.statusIds.passed || !this.state.statusIds.failed) {\n throw new Error('Could not find required status mappings (passed/failed) in TestPlanIt');\n }\n }\n\n /**\n * Map test status to JUnit result type\n */\n private mapStatusToJUnitType(status: 'passed' | 'failed' | 'skipped' | 'pending'): JUnitResultType {\n switch (status) {\n case 'passed':\n return 'PASSED';\n case 'failed':\n return 'FAILURE';\n case 'skipped':\n case 'pending':\n return 'SKIPPED';\n default:\n return 'FAILURE';\n }\n }\n\n /**\n * Create the JUnit test suite for this test run\n */\n private async createJUnitTestSuite(): Promise {\n if (this.state.testSuiteId) {\n return; // Already created (either from shared state or previous call)\n }\n\n if (!this.state.testRunId) {\n throw new Error('Cannot create JUnit test suite without a test run ID');\n }\n\n // In oneReport mode, check if another worker has already created a suite\n if (this.reporterOptions.oneReport) {\n const sharedState = this.readSharedState();\n if (sharedState?.testSuiteId) {\n this.state.testSuiteId = sharedState.testSuiteId;\n this.log('Using shared JUnit test suite from file:', sharedState.testSuiteId);\n return;\n }\n }\n\n const runName = this.formatRunName(this.reporterOptions.runName || '{suite} - {date} {time}');\n\n this.log('Creating JUnit test suite...');\n\n const testSuite = await this.client.createJUnitTestSuite({\n testRunId: this.state.testRunId,\n name: runName,\n time: 0, // Will be updated incrementally\n tests: 0,\n failures: 0,\n errors: 0,\n skipped: 0,\n });\n\n this.state.testSuiteId = testSuite.id;\n this.log('Created JUnit test suite with ID:', testSuite.id);\n\n // Update shared state with suite ID if in oneReport mode\n if (this.reporterOptions.oneReport) {\n this.writeSharedState({\n testRunId: this.state.testRunId,\n testSuiteId: this.state.testSuiteId,\n createdAt: new Date().toISOString(),\n activeWorkers: 1, // Will be merged/updated by writeSharedState\n });\n\n // Re-check to handle race condition - if another worker wrote first, use their suite\n const finalState = this.readSharedState();\n if (finalState && finalState.testSuiteId !== this.state.testSuiteId) {\n this.log(`Another worker created test suite first, switching from ${this.state.testSuiteId} to ${finalState.testSuiteId}`);\n this.state.testSuiteId = finalState.testSuiteId;\n }\n }\n }\n\n /**\n * Map WebdriverIO framework name to TestPlanIt test run type\n */\n private getTestRunType(): TestPlanItReporterOptions['testRunType'] {\n // If explicitly set by user, use that\n if (this.reporterOptions.testRunType) {\n return this.reporterOptions.testRunType;\n }\n\n // Auto-detect from WebdriverIO framework config\n if (this.detectedFramework) {\n const framework = this.detectedFramework.toLowerCase();\n if (framework === 'mocha') return 'MOCHA';\n if (framework === 'cucumber') return 'CUCUMBER';\n // jasmine and others map to REGULAR\n return 'REGULAR';\n }\n\n // Default fallback\n return 'MOCHA';\n }\n\n /**\n * Create a new test run\n */\n private async createTestRun(): Promise {\n const runName = this.formatRunName(this.reporterOptions.runName || '{suite} - {date} {time}');\n const testRunType = this.getTestRunType();\n\n this.log('Creating test run:', runName, '(type:', testRunType + ')');\n\n const testRun = await this.client.createTestRun({\n projectId: this.reporterOptions.projectId,\n name: runName,\n testRunType,\n configId: this.state.resolvedIds.configId,\n milestoneId: this.state.resolvedIds.milestoneId,\n stateId: this.state.resolvedIds.stateId,\n tagIds: this.state.resolvedIds.tagIds,\n });\n\n this.state.testRunId = testRun.id;\n this.log('Created test run with ID:', testRun.id);\n }\n\n /**\n * Format the run name with placeholders\n */\n private formatRunName(template: string): string {\n const now = new Date();\n const date = now.toISOString().split('T')[0];\n const time = now.toTimeString().split(' ')[0];\n const browser = this.state.capabilities?.browserName || 'unknown';\n const platform = this.state.capabilities?.platformName || process.platform;\n\n // Get spec file name from currentSpec (e.g., \"/path/to/test.spec.ts\" -> \"test.spec.ts\")\n let spec = 'unknown';\n if (this.currentSpec) {\n const parts = this.currentSpec.split('/');\n spec = parts[parts.length - 1] || 'unknown';\n // Remove common extensions for cleaner names\n spec = spec.replace(/\\.(spec|test)\\.(ts|js|mjs|cjs)$/, '');\n }\n\n // Get the root suite name (first describe block)\n const suite = this.currentSuite[0] || 'Tests';\n\n return template\n .replace('{date}', date)\n .replace('{time}', time)\n .replace('{browser}', browser)\n .replace('{platform}', platform)\n .replace('{spec}', spec)\n .replace('{suite}', suite);\n }\n\n /**\n * Parse case IDs from test title using the configured pattern\n * @example With default pattern: \"[1761] [1762] should load the page\" -> [1761, 1762]\n * @example With C-prefix pattern: \"C12345 C67890 should load the page\" -> [12345, 67890]\n */\n private parseCaseIds(title: string): { caseIds: number[]; cleanTitle: string } {\n const pattern = this.reporterOptions.caseIdPattern || /\\[(\\d+)\\]/g;\n const regex = typeof pattern === 'string' ? new RegExp(pattern, 'g') : new RegExp(pattern.source, 'g');\n const caseIds: number[] = [];\n let match: RegExpExecArray | null;\n\n while ((match = regex.exec(title)) !== null) {\n // Find the first capturing group that has a value (supports patterns with multiple groups)\n for (let i = 1; i < match.length; i++) {\n if (match[i]) {\n caseIds.push(parseInt(match[i], 10));\n break;\n }\n }\n }\n\n // Remove matched patterns from title\n const cleanTitle = title.replace(regex, '').trim().replace(/\\s+/g, ' ');\n\n return { caseIds, cleanTitle };\n }\n\n /**\n * Get the full suite path as a string\n */\n private getFullSuiteName(): string {\n return this.currentSuite.join(' > ');\n }\n\n /**\n * Create a unique key for a test case\n */\n private createCaseKey(suiteName: string, testName: string): string {\n return `${suiteName}::${testName}`;\n }\n\n // ============================================================================\n // WebdriverIO Reporter Hooks\n // ============================================================================\n\n onRunnerStart(runner: RunnerStats): void {\n this.log('Runner started:', runner.cid);\n this.state.capabilities = runner.capabilities as WebdriverIO.Capabilities;\n\n // Auto-detect the test framework from WebdriverIO config\n // This is accessed via runner.config.framework (e.g., 'mocha', 'cucumber', 'jasmine')\n const config = runner.config as { framework?: string } | undefined;\n if (config?.framework) {\n this.detectedFramework = config.framework;\n this.log('Detected framework:', this.detectedFramework);\n }\n\n // Don't initialize here - wait until we have actual test results to report\n // This avoids creating empty test runs for specs with no matching tests\n }\n\n onSuiteStart(suite: SuiteStats): void {\n if (suite.title) {\n this.currentSuite.push(suite.title);\n this.log('Suite started:', this.getFullSuiteName());\n }\n }\n\n onSuiteEnd(suite: SuiteStats): void {\n if (suite.title) {\n this.log('Suite ended:', this.getFullSuiteName());\n this.currentSuite.pop();\n }\n }\n\n onTestStart(test: TestStats): void {\n this.log('Test started:', test.title);\n // Track the current test for screenshot association\n const { cleanTitle } = this.parseCaseIds(test.title);\n const suiteName = this.getFullSuiteName();\n const fullTitle = suiteName ? `${suiteName} > ${cleanTitle}` : cleanTitle;\n this.currentTestUid = `${test.cid}_${fullTitle}`;\n this.currentCid = test.cid;\n }\n\n /**\n * Capture screenshots from WebdriverIO commands\n */\n onAfterCommand(commandArgs: AfterCommandArgs): void {\n // Check if this is a screenshot command\n if (!this.reporterOptions.uploadScreenshots) {\n return;\n }\n\n // WebdriverIO uses 'takeScreenshot' as the command name or '/screenshot' endpoint\n const isScreenshotCommand =\n commandArgs.command === 'takeScreenshot' ||\n commandArgs.command === 'saveScreenshot' ||\n commandArgs.endpoint?.includes('/screenshot');\n\n if (!isScreenshotCommand) {\n return;\n }\n\n this.log(`Screenshot command detected: ${commandArgs.command}, endpoint: ${commandArgs.endpoint}`);\n\n // For saveScreenshot, the result is the file path, not base64 data\n // We need to handle both takeScreenshot (returns base64) and saveScreenshot (saves to file)\n const result = commandArgs.result as Record | string | undefined;\n const resultValue = (typeof result === 'object' && result !== null ? result.value : result) ?? result;\n\n if (!resultValue) {\n this.log('No result value in screenshot command');\n return;\n }\n\n // The result should be base64-encoded screenshot data\n const screenshotData = resultValue as string;\n if (typeof screenshotData !== 'string') {\n this.log(`Screenshot result is not a string: ${typeof screenshotData}`);\n return;\n }\n\n // Check if this looks like a file path rather than base64 data\n // File paths start with / (Unix) or drive letter like C:\\ (Windows)\n // Base64 PNG data starts with \"iVBORw0KGgo\" (PNG header)\n const looksLikeFilePath =\n screenshotData.startsWith('/') ||\n /^[A-Za-z]:[\\\\\\/]/.test(screenshotData) ||\n screenshotData.startsWith('./') ||\n screenshotData.startsWith('../');\n\n if (looksLikeFilePath) {\n this.log(`Screenshot result appears to be a file path: ${screenshotData.substring(0, 100)}`);\n return;\n }\n\n // Store the screenshot associated with the current test\n if (this.currentTestUid) {\n const buffer = Buffer.from(screenshotData, 'base64');\n const existing = this.pendingScreenshots.get(this.currentTestUid) || [];\n existing.push(buffer);\n this.pendingScreenshots.set(this.currentTestUid, existing);\n this.log('Captured screenshot for test:', this.currentTestUid, `(${buffer.length} bytes)`);\n } else {\n this.log('No current test UID to associate screenshot with');\n }\n }\n\n onTestPass(test: TestStats): void {\n this.handleTestEnd(test, 'passed');\n }\n\n onTestFail(test: TestStats): void {\n this.handleTestEnd(test, 'failed');\n }\n\n onTestSkip(test: TestStats): void {\n this.handleTestEnd(test, 'skipped');\n }\n\n /**\n * Handle test completion\n */\n private handleTestEnd(test: TestStats, status: 'passed' | 'failed' | 'skipped'): void {\n const { caseIds, cleanTitle } = this.parseCaseIds(test.title);\n const suiteName = this.getFullSuiteName();\n const suitePath = [...this.currentSuite]; // Copy the current suite hierarchy\n const fullTitle = suiteName ? `${suiteName} > ${cleanTitle}` : cleanTitle;\n const uid = `${test.cid}_${fullTitle}`;\n\n // Calculate duration from timestamps for reliability\n // WebdriverIO's test.duration can be inconsistent in some versions\n const startTime = new Date(test.start).getTime();\n const endTime = test.end ? new Date(test.end).getTime() : Date.now();\n const durationMs = endTime - startTime;\n\n // Format WebdriverIO command output if available\n let commandOutput: string | undefined;\n if (test.output && test.output.length > 0) {\n commandOutput = test.output\n .map((o) => {\n const parts: string[] = [];\n if (o.method) parts.push(`[${o.method}]`);\n if (o.endpoint) parts.push(o.endpoint);\n if (o.result !== undefined) {\n const resultStr = typeof o.result === 'string' ? o.result : JSON.stringify(o.result);\n // Truncate long results\n parts.push(resultStr.length > 200 ? resultStr.substring(0, 200) + '...' : resultStr);\n }\n return parts.join(' ');\n })\n .join('\\n');\n }\n\n const result: TrackedTestResult = {\n caseId: caseIds[0], // Primary case ID\n suiteName,\n suitePath,\n testName: cleanTitle,\n fullTitle,\n originalTitle: test.title,\n status,\n duration: durationMs,\n errorMessage: test.error?.message,\n stackTrace: this.reporterOptions.includeStackTrace ? test.error?.stack : undefined,\n startedAt: new Date(test.start),\n finishedAt: new Date(endTime),\n browser: this.state.capabilities?.browserName,\n platform: this.state.capabilities?.platformName || process.platform,\n screenshots: [],\n retryAttempt: test.retries || 0,\n uid,\n specFile: this.currentSpec,\n commandOutput,\n };\n\n this.state.results.set(uid, result);\n this.log(`Test ${status}:`, cleanTitle, caseIds.length > 0 ? `(Case IDs: ${caseIds.join(', ')})` : '');\n\n // Report result asynchronously - track operation so WebdriverIO waits for completion\n const reportPromise = this.reportResult(result, caseIds);\n this.trackOperation(reportPromise);\n }\n\n /**\n * Report a single test result to TestPlanIt\n */\n private async reportResult(result: TrackedTestResult, caseIds: number[]): Promise {\n try {\n // Check if this result can be reported BEFORE initializing\n // This prevents creating empty test runs for tests without case IDs\n if (caseIds.length === 0 && !this.reporterOptions.autoCreateTestCases) {\n console.warn(`[TestPlanIt] WARNING: Skipping \"${result.testName}\" - no case ID found and autoCreateTestCases is disabled. Set autoCreateTestCases: true to automatically find or create test cases by name.`);\n return;\n }\n\n // Now we know this result can be reported, so initialize if needed\n await this.initialize();\n\n if (!this.state.testRunId) {\n this.logError('No test run ID available, skipping result');\n return;\n }\n\n // Create JUnit test suite if not already created\n await this.createJUnitTestSuite();\n\n if (!this.state.testSuiteId) {\n this.logError('No test suite ID available, skipping result');\n return;\n }\n\n // Get or create repository case\n let repositoryCaseId: number | undefined;\n const caseKey = this.createCaseKey(result.suiteName, result.testName);\n\n // DEBUG: Always log key info about this test\n this.log('DEBUG: Processing test:', result.testName);\n this.log('DEBUG: suiteName:', result.suiteName);\n this.log('DEBUG: suitePath:', JSON.stringify(result.suitePath));\n this.log('DEBUG: caseIds from title:', JSON.stringify(caseIds));\n this.log('DEBUG: autoCreateTestCases:', this.reporterOptions.autoCreateTestCases);\n this.log('DEBUG: createFolderHierarchy:', this.reporterOptions.createFolderHierarchy);\n\n if (caseIds.length > 0) {\n // Use the provided case ID directly as repository case ID\n repositoryCaseId = caseIds[0];\n this.log('DEBUG: Using case ID from title:', repositoryCaseId);\n } else if (this.reporterOptions.autoCreateTestCases) {\n // Check cache first\n if (this.state.caseIdMap.has(caseKey)) {\n repositoryCaseId = this.state.caseIdMap.get(caseKey);\n this.log('DEBUG: Found in cache:', caseKey, '->', repositoryCaseId);\n } else {\n // Determine the target folder ID\n let folderId = this.state.resolvedIds.parentFolderId;\n const templateId = this.state.resolvedIds.templateId;\n\n this.log('DEBUG: Initial folderId (parentFolderId):', folderId);\n this.log('DEBUG: templateId:', templateId);\n\n if (!folderId || !templateId) {\n this.logError('autoCreateTestCases requires parentFolderId and templateId');\n return;\n }\n\n // Create folder hierarchy based on suite structure if enabled\n this.log('DEBUG: Checking folder hierarchy - createFolderHierarchy:', this.reporterOptions.createFolderHierarchy, 'suitePath.length:', result.suitePath.length);\n if (this.reporterOptions.createFolderHierarchy && result.suitePath.length > 0) {\n const folderPathKey = result.suitePath.join(' > ');\n this.log('DEBUG: Will create folder hierarchy for path:', folderPathKey);\n\n // Check folder cache first\n if (this.state.folderPathMap.has(folderPathKey)) {\n folderId = this.state.folderPathMap.get(folderPathKey)!;\n this.log('Using cached folder ID for path:', folderPathKey, '->', folderId);\n } else {\n // Create the folder hierarchy\n this.log('Creating folder hierarchy:', result.suitePath.join(' > '));\n this.log('DEBUG: Calling findOrCreateFolderPath with projectId:', this.reporterOptions.projectId, 'suitePath:', JSON.stringify(result.suitePath), 'parentFolderId:', this.state.resolvedIds.parentFolderId);\n const folder = await this.client.findOrCreateFolderPath(\n this.reporterOptions.projectId,\n result.suitePath,\n this.state.resolvedIds.parentFolderId\n );\n folderId = folder.id;\n this.state.folderPathMap.set(folderPathKey, folderId);\n this.log('Created/found folder:', folder.name, '(ID:', folder.id + ')');\n }\n } else {\n this.log('DEBUG: Skipping folder hierarchy - createFolderHierarchy:', this.reporterOptions.createFolderHierarchy, 'suitePath.length:', result.suitePath.length);\n }\n\n this.log('DEBUG: Final folderId for test case:', folderId);\n\n const { testCase, action } = await this.client.findOrCreateTestCase({\n projectId: this.reporterOptions.projectId,\n folderId,\n templateId,\n name: result.testName,\n className: result.suiteName || undefined,\n source: 'API',\n automated: true,\n });\n\n // Track statistics based on action\n if (action === 'found') {\n this.state.stats.testCasesFound++;\n } else if (action === 'created') {\n this.state.stats.testCasesCreated++;\n } else if (action === 'moved') {\n this.state.stats.testCasesMoved++;\n }\n\n repositoryCaseId = testCase.id;\n this.state.caseIdMap.set(caseKey, repositoryCaseId);\n this.log(`${action === 'found' ? 'Found' : action === 'created' ? 'Created' : 'Moved'} test case:`, testCase.id, testCase.name, 'in folder:', folderId);\n }\n } else {\n this.log('DEBUG: autoCreateTestCases is false, not creating test case');\n }\n\n if (!repositoryCaseId) {\n this.log('No repository case ID, skipping result');\n return;\n }\n\n // Get or create test run case\n let testRunCaseId: number | undefined;\n const runCaseKey = `${this.state.testRunId}_${repositoryCaseId}`;\n\n if (this.state.testRunCaseMap.has(runCaseKey)) {\n testRunCaseId = this.state.testRunCaseMap.get(runCaseKey);\n } else {\n const testRunCase = await this.client.findOrAddTestCaseToRun({\n testRunId: this.state.testRunId,\n repositoryCaseId,\n });\n testRunCaseId = testRunCase.id;\n this.state.testRunCaseMap.set(runCaseKey, testRunCaseId);\n this.log('Added case to run:', testRunCaseId);\n }\n\n // Get status ID for the JUnit result\n const statusId = this.state.statusIds[result.status] || this.state.statusIds.failed!;\n\n // Map status to JUnit result type\n const junitType = this.mapStatusToJUnitType(result.status);\n\n // Build error message/content for failed tests\n let message: string | undefined;\n let content: string | undefined;\n\n if (result.errorMessage) {\n message = result.errorMessage;\n }\n if (result.stackTrace) {\n content = result.stackTrace;\n }\n\n // Create the JUnit test result\n // WebdriverIO provides duration in milliseconds, JUnit expects seconds\n const durationInSeconds = result.duration / 1000;\n const junitResult = await this.client.createJUnitTestResult({\n testSuiteId: this.state.testSuiteId,\n repositoryCaseId,\n type: junitType,\n message,\n content,\n statusId,\n time: durationInSeconds,\n executedAt: result.finishedAt,\n file: result.specFile,\n systemOut: result.commandOutput,\n });\n\n this.log('Created JUnit test result:', junitResult.id, '(type:', junitType + ')');\n this.reportedResultCount++;\n\n // Store the JUnit result ID for deferred screenshot upload\n // Screenshots taken in afterTest hook won't be available yet, so we upload them in onRunnerEnd\n result.junitResultId = junitResult.id;\n\n // Update reporter stats (suite stats are calculated by backend from JUnitTestResult rows)\n if (result.status === 'failed') {\n this.state.stats.resultsFailed++;\n } else if (result.status === 'skipped') {\n this.state.stats.resultsSkipped++;\n } else {\n this.state.stats.resultsPassed++;\n }\n } catch (error) {\n this.state.stats.apiErrors++;\n this.logError(`Failed to report result for ${result.testName}:`, error);\n }\n }\n\n /**\n * Called when the entire test session ends\n */\n async onRunnerEnd(runner: RunnerStats): Promise {\n // If no tests were tracked and no initialization was started, silently skip\n // This handles specs with no matching tests (all filtered out by grep, etc.)\n if (this.state.results.size === 0 && !this.initPromise) {\n this.log('No test results to report, skipping');\n return;\n }\n\n this.log('Runner ended, waiting for initialization and pending results...');\n\n // Wait for initialization to complete (might still be in progress)\n if (this.initPromise) {\n try {\n await this.initPromise;\n } catch {\n // Error already captured in state.initError\n }\n }\n\n // Wait for any remaining pending operations\n // (WebdriverIO waits via isSynchronised, but we also wait here for safety)\n await Promise.allSettled([...this.pendingOperations]);\n\n // Check if initialization failed\n if (this.state.initError) {\n console.error('\\n[TestPlanIt] FAILED: Reporter initialization failed');\n console.error(` Error: ${this.state.initError.message}`);\n console.error(' No results were reported to TestPlanIt.');\n console.error(' Please check your configuration and API connectivity.');\n return;\n }\n\n // If no test run was created (no reportable results), silently skip\n if (!this.state.testRunId) {\n this.log('No test run created, skipping summary');\n return;\n }\n\n // If no results were actually reported to TestPlanIt, silently skip\n // This handles the case where tests ran but none had valid case IDs\n if (this.reportedResultCount === 0) {\n this.log('No results were reported to TestPlanIt, skipping summary');\n return;\n }\n\n // Upload any pending screenshots\n // Screenshots are uploaded here (deferred) because afterTest hooks run after onTestFail/onTestPass,\n // so screenshots taken in afterTest wouldn't be available during reportResult\n if (this.reporterOptions.uploadScreenshots && this.pendingScreenshots.size > 0) {\n this.log(`Uploading screenshots for ${this.pendingScreenshots.size} test(s)...`);\n\n // Create upload promises for all screenshots and track them\n // This ensures WebdriverIO waits for uploads to complete (via isSynchronised)\n const uploadPromises: Promise[] = [];\n\n for (const [uid, screenshots] of this.pendingScreenshots.entries()) {\n const result = this.state.results.get(uid);\n if (!result?.junitResultId) {\n this.log(`Skipping screenshots for ${uid} - no JUnit result ID`);\n continue;\n }\n\n this.log(`Uploading ${screenshots.length} screenshot(s) for test:`, result.testName);\n for (let i = 0; i < screenshots.length; i++) {\n const uploadPromise = (async () => {\n try {\n // Create a meaningful file name: testName_status_screenshot#.png\n // Sanitize test name for filename (remove special chars, limit length)\n const sanitizedTestName = result.testName\n .replace(/[^a-zA-Z0-9_-]/g, '_')\n .substring(0, 50);\n const fileName = `${sanitizedTestName}_${result.status}_${i + 1}.png`;\n\n // Build a descriptive note with test context\n const noteParts: string[] = [];\n noteParts.push(`Test: ${result.testName}`);\n if (result.suiteName) {\n noteParts.push(`Suite: ${result.suiteName}`);\n }\n noteParts.push(`Status: ${result.status}`);\n if (result.browser) {\n noteParts.push(`Browser: ${result.browser}`);\n }\n if (result.errorMessage) {\n // Truncate error message if too long\n const errorPreview = result.errorMessage.length > 200\n ? result.errorMessage.substring(0, 200) + '...'\n : result.errorMessage;\n noteParts.push(`Error: ${errorPreview}`);\n }\n const note = noteParts.join('\\n');\n\n this.log(`Starting upload of ${fileName} (${screenshots[i].length} bytes) to JUnit result ${result.junitResultId}...`);\n await this.client.uploadJUnitAttachment(\n result.junitResultId!,\n screenshots[i],\n fileName,\n 'image/png',\n note\n );\n this.state.stats.screenshotsUploaded++;\n this.log(`Uploaded screenshot ${i + 1}/${screenshots.length} for ${result.testName}`);\n } catch (uploadError) {\n this.state.stats.screenshotsFailed++;\n const errorMessage = uploadError instanceof Error ? uploadError.message : String(uploadError);\n const errorStack = uploadError instanceof Error ? uploadError.stack : undefined;\n this.logError(`Failed to upload screenshot ${i + 1}:`, errorMessage);\n if (errorStack) {\n this.logError('Stack trace:', errorStack);\n }\n }\n })();\n\n // Track this operation so WebdriverIO waits for it\n this.trackOperation(uploadPromise);\n uploadPromises.push(uploadPromise);\n }\n }\n\n // Wait for all uploads to complete before proceeding\n await Promise.allSettled(uploadPromises);\n\n // Clear all pending screenshots\n this.pendingScreenshots.clear();\n }\n\n // Note: JUnit test suite statistics (tests, failures, errors, skipped, time) are NOT updated here.\n // The backend calculates these dynamically from JUnitTestResult rows in the summary API.\n // This ensures correct totals when multiple workers/spec files report to the same test run.\n\n // Complete the test run if configured\n // In oneReport mode, decrement worker count and only complete when last worker finishes\n // Track this operation to prevent WebdriverIO from terminating early\n if (this.reporterOptions.completeRunOnFinish) {\n if (this.reporterOptions.oneReport) {\n // Decrement worker count and check if we're the last worker\n const isLastWorker = this.decrementWorkerCount();\n if (isLastWorker) {\n const completeRunOp = (async () => {\n try {\n await this.client.completeTestRun(this.state.testRunId!, this.reporterOptions.projectId);\n this.log('Test run completed (last worker):', this.state.testRunId);\n // Clean up shared state file\n this.deleteSharedState();\n } catch (error) {\n this.logError('Failed to complete test run:', error);\n }\n })();\n this.trackOperation(completeRunOp);\n await completeRunOp;\n } else {\n this.log('Skipping test run completion (waiting for other workers to finish)');\n }\n } else {\n const completeRunOp = (async () => {\n try {\n await this.client.completeTestRun(this.state.testRunId!, this.reporterOptions.projectId);\n this.log('Test run completed:', this.state.testRunId);\n } catch (error) {\n this.logError('Failed to complete test run:', error);\n }\n })();\n this.trackOperation(completeRunOp);\n await completeRunOp;\n }\n } else if (this.reporterOptions.oneReport) {\n // Even if not completing, decrement worker count\n this.decrementWorkerCount();\n }\n\n // Print summary\n const stats = this.state.stats;\n const duration = ((Date.now() - stats.startTime.getTime()) / 1000).toFixed(1);\n const totalResults = stats.resultsPassed + stats.resultsFailed + stats.resultsSkipped;\n const totalCases = stats.testCasesFound + stats.testCasesCreated + stats.testCasesMoved;\n\n console.log('\\n[TestPlanIt] ═══════════════════════════════════════════════════════');\n console.log('[TestPlanIt] Results Summary');\n console.log('[TestPlanIt] ═══════════════════════════════════════════════════════');\n console.log(`[TestPlanIt] Test Run ID: ${this.state.testRunId}`);\n console.log(`[TestPlanIt] Duration: ${duration}s`);\n console.log('[TestPlanIt]');\n console.log('[TestPlanIt] Test Results:');\n console.log(`[TestPlanIt] ✓ Passed: ${stats.resultsPassed}`);\n console.log(`[TestPlanIt] ✗ Failed: ${stats.resultsFailed}`);\n console.log(`[TestPlanIt] ○ Skipped: ${stats.resultsSkipped}`);\n console.log(`[TestPlanIt] Total: ${totalResults}`);\n\n if (this.reporterOptions.autoCreateTestCases && totalCases > 0) {\n console.log('[TestPlanIt]');\n console.log('[TestPlanIt] Test Cases:');\n console.log(`[TestPlanIt] Found (existing): ${stats.testCasesFound}`);\n console.log(`[TestPlanIt] Created (new): ${stats.testCasesCreated}`);\n if (stats.testCasesMoved > 0) {\n console.log(`[TestPlanIt] Moved (restored): ${stats.testCasesMoved}`);\n }\n }\n\n if (this.reporterOptions.uploadScreenshots && (stats.screenshotsUploaded > 0 || stats.screenshotsFailed > 0)) {\n console.log('[TestPlanIt]');\n console.log('[TestPlanIt] Screenshots:');\n console.log(`[TestPlanIt] Uploaded: ${stats.screenshotsUploaded}`);\n if (stats.screenshotsFailed > 0) {\n console.log(`[TestPlanIt] Failed: ${stats.screenshotsFailed}`);\n }\n }\n\n if (stats.apiErrors > 0) {\n console.log('[TestPlanIt]');\n console.log(`[TestPlanIt] ⚠ API Errors: ${stats.apiErrors}`);\n }\n\n console.log('[TestPlanIt]');\n console.log(`[TestPlanIt] View results: ${this.reporterOptions.domain}/projects/runs/${this.reporterOptions.projectId}/${this.state.testRunId}`);\n console.log('[TestPlanIt] ═══════════════════════════════════════════════════════\\n');\n }\n\n /**\n * Get the current state (for debugging)\n */\n getState(): ReporterState {\n return this.state;\n }\n}\n"]} \ No newline at end of file +{"version":3,"sources":["../src/shared.ts","../src/reporter.ts","../src/service.ts"],"names":["TestPlanItClient"],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * Shared state utilities for coordinating between the TestPlanIt WDIO service\n * and reporter instances running in separate worker processes.\n *\n * Uses a file in the OS temp directory to share state (test run ID, test suite ID)\n * between the main process (service) and worker processes (reporters).\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport * as os from 'os';\n\n/**\n * Shared state file for coordinating between WDIO workers and the launcher service.\n *\n * When `managedByService` is true, the TestPlanItService controls the test run lifecycle\n * (creation in onPrepare, completion in onComplete). Workers must not manage the run lifecycle.\n *\n * When `managedByService` is false/absent, the reporter manages it (legacy oneReport mode).\n */\nexport interface SharedState {\n testRunId: number;\n testSuiteId?: number;\n createdAt: string;\n /** Number of active workers using this test run (only used when managedByService is false) */\n activeWorkers: number;\n /** When true, the TestPlanItService controls run creation/completion. Workers must not manage the run lifecycle. */\n managedByService?: boolean;\n}\n\n/** Maximum age of a shared state file before it is considered stale (4 hours) */\nconst STALE_THRESHOLD_MS = 4 * 60 * 60 * 1000;\n\n/**\n * Get the path to the shared state file for a given project.\n * Uses the OS temp directory with a project-specific filename.\n */\nexport function getSharedStateFilePath(projectId: number): string {\n const fileName = `.testplanit-reporter-${projectId}.json`;\n return path.join(os.tmpdir(), fileName);\n}\n\n/**\n * Acquire a simple file-based lock using exclusive file creation.\n * Retries with exponential backoff up to `maxAttempts` times.\n */\nfunction acquireLock(lockPath: string, maxAttempts = 10): boolean {\n for (let i = 0; i < maxAttempts; i++) {\n try {\n fs.writeFileSync(lockPath, process.pid.toString(), { flag: 'wx' });\n return true;\n } catch {\n const sleepMs = 50 * Math.pow(2, i) + Math.random() * 50;\n const start = Date.now();\n while (Date.now() - start < sleepMs) {\n // Busy wait\n }\n }\n }\n return false;\n}\n\n/**\n * Release a file-based lock.\n */\nfunction releaseLock(lockPath: string): void {\n try {\n fs.unlinkSync(lockPath);\n } catch {\n // Ignore lock removal errors\n }\n}\n\n/**\n * Execute a callback while holding the lock on the shared state file.\n * Returns the callback's return value, or undefined if the lock could not be acquired.\n */\nexport function withLock(projectId: number, callback: (filePath: string) => T): T | undefined {\n const filePath = getSharedStateFilePath(projectId);\n const lockPath = `${filePath}.lock`;\n\n if (!acquireLock(lockPath)) {\n return undefined;\n }\n\n try {\n return callback(filePath);\n } finally {\n releaseLock(lockPath);\n }\n}\n\n/**\n * Read shared state from file.\n * Returns null if file doesn't exist, is stale (>4 hours), or contains invalid JSON.\n *\n * Note: Does NOT check `activeWorkers === 0` — that logic differs between\n * service-managed mode and legacy oneReport mode and is handled by the caller.\n */\nexport function readSharedState(projectId: number): SharedState | null {\n const filePath = getSharedStateFilePath(projectId);\n try {\n if (!fs.existsSync(filePath)) {\n return null;\n }\n const content = fs.readFileSync(filePath, 'utf-8');\n const state: SharedState = JSON.parse(content);\n\n // Check if state is stale\n const createdAt = new Date(state.createdAt);\n const staleThreshold = new Date(Date.now() - STALE_THRESHOLD_MS);\n if (createdAt < staleThreshold) {\n deleteSharedState(projectId);\n return null;\n }\n\n return state;\n } catch {\n return null;\n }\n}\n\n/**\n * Write shared state to file atomically (uses lock).\n */\nexport function writeSharedState(projectId: number, state: SharedState): void {\n withLock(projectId, (filePath) => {\n fs.writeFileSync(filePath, JSON.stringify(state, null, 2));\n });\n}\n\n/**\n * Write shared state to file, but only if no file already exists (first writer wins).\n * If the file already exists, optionally updates the testSuiteId if not yet set.\n * Returns the final state (either the written state or the existing state).\n */\nexport function writeSharedStateIfAbsent(projectId: number, state: SharedState): SharedState | undefined {\n return withLock(projectId, (filePath) => {\n if (fs.existsSync(filePath)) {\n // File already exists — read existing state\n const content = fs.readFileSync(filePath, 'utf-8');\n const existingState: SharedState = JSON.parse(content);\n\n // Only update if the testSuiteId is missing and we have one to add\n if (!existingState.testSuiteId && state.testSuiteId) {\n existingState.testSuiteId = state.testSuiteId;\n fs.writeFileSync(filePath, JSON.stringify(existingState, null, 2));\n }\n return existingState;\n }\n\n // First writer — write the full state\n fs.writeFileSync(filePath, JSON.stringify(state, null, 2));\n return state;\n });\n}\n\n/**\n * Delete shared state file.\n */\nexport function deleteSharedState(projectId: number): void {\n const filePath = getSharedStateFilePath(projectId);\n try {\n if (fs.existsSync(filePath)) {\n fs.unlinkSync(filePath);\n }\n } catch {\n // Ignore deletion errors\n }\n}\n\n/**\n * Atomically increment the active worker count in the shared state file.\n */\nexport function incrementWorkerCount(projectId: number): void {\n withLock(projectId, (filePath) => {\n if (fs.existsSync(filePath)) {\n const content = fs.readFileSync(filePath, 'utf-8');\n const state: SharedState = JSON.parse(content);\n state.activeWorkers = (state.activeWorkers || 0) + 1;\n fs.writeFileSync(filePath, JSON.stringify(state, null, 2));\n }\n });\n}\n\n/**\n * Atomically decrement the active worker count in the shared state file.\n * Returns true if this was the last worker (count reached 0).\n */\nexport function decrementWorkerCount(projectId: number): boolean {\n const result = withLock(projectId, (filePath) => {\n if (fs.existsSync(filePath)) {\n const content = fs.readFileSync(filePath, 'utf-8');\n const state: SharedState = JSON.parse(content);\n state.activeWorkers = Math.max(0, (state.activeWorkers || 1) - 1);\n fs.writeFileSync(filePath, JSON.stringify(state, null, 2));\n return state.activeWorkers === 0;\n }\n return false;\n });\n return result ?? false;\n}\n","import WDIOReporter, { type RunnerStats, type SuiteStats, type TestStats, type AfterCommandArgs } from '@wdio/reporter';\nimport { TestPlanItClient } from '@testplanit/api';\nimport type { NormalizedStatus, JUnitResultType } from '@testplanit/api';\nimport type { TestPlanItReporterOptions, TrackedTestResult, ReporterState } from './types.js';\nimport {\n readSharedState,\n writeSharedStateIfAbsent,\n deleteSharedState,\n incrementWorkerCount,\n decrementWorkerCount,\n} from './shared.js';\n\n/**\n * WebdriverIO Reporter for TestPlanIt\n *\n * Reports test results directly to your TestPlanIt instance.\n *\n * @example\n * ```javascript\n * // wdio.conf.js\n * export const config = {\n * reporters: [\n * ['@testplanit/wdio-reporter', {\n * domain: 'https://testplanit.example.com',\n * apiToken: process.env.TESTPLANIT_API_TOKEN,\n * projectId: 1,\n * runName: 'E2E Tests - {date}',\n * }]\n * ]\n * }\n * ```\n */\nexport default class TestPlanItReporter extends WDIOReporter {\n private client: TestPlanItClient;\n private reporterOptions: TestPlanItReporterOptions;\n private state: ReporterState;\n private currentSuite: string[] = [];\n private initPromise: Promise | null = null;\n private pendingOperations: Set> = new Set();\n private reportedResultCount = 0;\n private detectedFramework: string | null = null;\n private currentTestUid: string | null = null;\n private currentCid: string | null = null;\n private pendingScreenshots: Map = new Map();\n /** When true, the TestPlanItService manages the test run lifecycle */\n private managedByService = false;\n\n /**\n * WebdriverIO uses this getter to determine if the reporter has finished async operations.\n * The test runner will wait for this to return true before terminating.\n */\n get isSynchronised(): boolean {\n return this.pendingOperations.size === 0;\n }\n\n constructor(options: TestPlanItReporterOptions) {\n super(options);\n\n this.reporterOptions = {\n caseIdPattern: /\\[(\\d+)\\]/g,\n autoCreateTestCases: false,\n createFolderHierarchy: false,\n uploadScreenshots: true,\n includeStackTrace: true,\n completeRunOnFinish: true,\n oneReport: true,\n timeout: 30000,\n maxRetries: 3,\n verbose: false,\n ...options,\n };\n\n // Validate required options\n if (!this.reporterOptions.domain) {\n throw new Error('TestPlanIt reporter: domain is required');\n }\n if (!this.reporterOptions.apiToken) {\n throw new Error('TestPlanIt reporter: apiToken is required');\n }\n if (!this.reporterOptions.projectId) {\n throw new Error('TestPlanIt reporter: projectId is required');\n }\n\n // Initialize API client\n this.client = new TestPlanItClient({\n baseUrl: this.reporterOptions.domain,\n apiToken: this.reporterOptions.apiToken,\n timeout: this.reporterOptions.timeout,\n maxRetries: this.reporterOptions.maxRetries,\n });\n\n // Initialize state - testRunId will be resolved during initialization\n this.state = {\n testRunId: typeof this.reporterOptions.testRunId === 'number' ? this.reporterOptions.testRunId : undefined,\n resolvedIds: {},\n results: new Map(),\n caseIdMap: new Map(),\n testRunCaseMap: new Map(),\n folderPathMap: new Map(),\n statusIds: {},\n initialized: false,\n stats: {\n testCasesFound: 0,\n testCasesCreated: 0,\n testCasesMoved: 0,\n foldersCreated: 0,\n resultsPassed: 0,\n resultsFailed: 0,\n resultsSkipped: 0,\n screenshotsUploaded: 0,\n screenshotsFailed: 0,\n apiErrors: 0,\n apiRequests: 0,\n startTime: new Date(),\n },\n };\n }\n\n /**\n * Log a message if verbose mode is enabled\n */\n private log(message: string, ...args: unknown[]): void {\n if (this.reporterOptions.verbose) {\n console.log(`[TestPlanIt] ${message}`, ...args);\n }\n }\n\n /**\n * Log an error (always logs, not just in verbose mode)\n */\n private logError(message: string, error?: unknown): void {\n const errorMsg = error instanceof Error ? error.message : String(error ?? '');\n const stack = error instanceof Error && error.stack ? `\\n${error.stack}` : '';\n console.error(`[TestPlanIt] ERROR: ${message}`, errorMsg, stack);\n }\n\n /**\n * Track an async operation to prevent the runner from terminating early.\n * The operation is added to pendingOperations and removed when complete.\n * WebdriverIO checks isSynchronised and waits until all operations finish.\n */\n private trackOperation(operation: Promise): void {\n this.pendingOperations.add(operation);\n operation.finally(() => {\n this.pendingOperations.delete(operation);\n });\n }\n\n /**\n * Initialize the reporter (create test run, fetch statuses)\n */\n private async initialize(): Promise {\n // If already initialized successfully, return immediately\n if (this.state.initialized) return;\n\n // If we have a previous error, throw it again to prevent retrying\n if (this.state.initError) {\n throw this.state.initError;\n }\n\n // If initialization is in progress, wait for it\n if (this.initPromise) return this.initPromise;\n\n this.initPromise = this.doInitialize();\n return this.initPromise;\n }\n\n private async doInitialize(): Promise {\n try {\n // Log initialization start (only happens when we have results to report)\n this.log('Initializing reporter...');\n this.log(` Domain: ${this.reporterOptions.domain}`);\n this.log(` Project ID: ${this.reporterOptions.projectId}`);\n this.log(` oneReport: ${this.reporterOptions.oneReport}`);\n\n // Resolve any string IDs to numeric IDs\n this.log('Resolving option IDs...');\n await this.resolveOptionIds();\n\n // Fetch status mappings\n this.log('Fetching status mappings...');\n await this.fetchStatusMappings();\n\n // Handle oneReport mode - check for existing shared state\n if (this.reporterOptions.oneReport && !this.state.testRunId) {\n const sharedState = readSharedState(this.reporterOptions.projectId);\n if (sharedState) {\n if (sharedState.managedByService) {\n // Service manages the run — just use the IDs, skip all lifecycle management\n this.state.testRunId = sharedState.testRunId;\n this.state.testSuiteId = sharedState.testSuiteId;\n this.managedByService = true;\n this.log(`Using service-managed test run: ${sharedState.testRunId}`);\n } else {\n // Legacy oneReport mode — validate and join the existing run\n this.state.testRunId = sharedState.testRunId;\n this.state.testSuiteId = sharedState.testSuiteId;\n this.log(`Using shared test run from file: ${sharedState.testRunId}`);\n\n // In legacy mode, skip runs where all workers have finished\n if (sharedState.activeWorkers === 0) {\n this.log('Previous test run completed (activeWorkers=0), starting fresh');\n deleteSharedState(this.reporterOptions.projectId);\n this.state.testRunId = undefined;\n this.state.testSuiteId = undefined;\n } else {\n // Validate the shared test run still exists, is not completed, and is not deleted\n try {\n const testRun = await this.client.getTestRun(this.state.testRunId);\n if (testRun.isDeleted) {\n this.log(`Shared test run ${testRun.id} is deleted, starting fresh`);\n this.state.testRunId = undefined;\n this.state.testSuiteId = undefined;\n deleteSharedState(this.reporterOptions.projectId);\n } else if (testRun.isCompleted) {\n this.log(`Shared test run ${testRun.id} is already completed, starting fresh`);\n this.state.testRunId = undefined;\n this.state.testSuiteId = undefined;\n deleteSharedState(this.reporterOptions.projectId);\n } else {\n this.log(`Validated shared test run: ${testRun.name} (ID: ${testRun.id})`);\n incrementWorkerCount(this.reporterOptions.projectId);\n }\n } catch {\n this.log('Shared test run no longer exists, will create new one');\n this.state.testRunId = undefined;\n this.state.testSuiteId = undefined;\n deleteSharedState(this.reporterOptions.projectId);\n }\n }\n }\n }\n }\n\n // Create or validate test run (skip if service-managed)\n if (!this.state.testRunId && !this.managedByService) {\n // In oneReport mode, use atomic write to prevent race conditions\n if (this.reporterOptions.oneReport) {\n // Create the test run first\n await this.createTestRun();\n this.log(`Created test run with ID: ${this.state.testRunId}`);\n\n // Try to write shared state - first writer wins\n const finalState = writeSharedStateIfAbsent(this.reporterOptions.projectId, {\n testRunId: this.state.testRunId!,\n testSuiteId: this.state.testSuiteId,\n createdAt: new Date().toISOString(),\n activeWorkers: 1,\n });\n\n // Check if another worker wrote first\n if (finalState && finalState.testRunId !== this.state.testRunId) {\n this.log(`Another worker created test run first, switching from ${this.state.testRunId} to ${finalState.testRunId}`);\n this.state.testRunId = finalState.testRunId;\n this.state.testSuiteId = finalState.testSuiteId;\n }\n } else {\n await this.createTestRun();\n this.log(`Created test run with ID: ${this.state.testRunId}`);\n }\n } else if (this.state.testRunId && !this.reporterOptions.oneReport && !this.managedByService) {\n // Validate existing test run (only when not using oneReport or service)\n try {\n const testRun = await this.client.getTestRun(this.state.testRunId);\n this.log(`Using existing test run: ${testRun.name} (ID: ${testRun.id})`);\n } catch (error) {\n throw new Error(`Test run ${this.state.testRunId} not found or not accessible`);\n }\n }\n\n this.state.initialized = true;\n this.log('Reporter initialized successfully');\n } catch (error) {\n this.state.initError = error instanceof Error ? error : new Error(String(error));\n this.logError('Failed to initialize reporter:', error);\n throw error;\n }\n }\n\n /**\n * Resolve option names to numeric IDs\n */\n private async resolveOptionIds(): Promise {\n const projectId = this.reporterOptions.projectId;\n\n // Resolve testRunId if it's a string\n if (typeof this.reporterOptions.testRunId === 'string') {\n const testRun = await this.client.findTestRunByName(projectId, this.reporterOptions.testRunId);\n if (!testRun) {\n throw new Error(`Test run not found: \"${this.reporterOptions.testRunId}\"`);\n }\n this.state.testRunId = testRun.id;\n this.state.resolvedIds.testRunId = testRun.id;\n this.log(`Resolved test run \"${this.reporterOptions.testRunId}\" -> ${testRun.id}`);\n }\n\n // Resolve configId if it's a string\n if (typeof this.reporterOptions.configId === 'string') {\n const config = await this.client.findConfigurationByName(projectId, this.reporterOptions.configId);\n if (!config) {\n throw new Error(`Configuration not found: \"${this.reporterOptions.configId}\"`);\n }\n this.state.resolvedIds.configId = config.id;\n this.log(`Resolved configuration \"${this.reporterOptions.configId}\" -> ${config.id}`);\n } else if (typeof this.reporterOptions.configId === 'number') {\n this.state.resolvedIds.configId = this.reporterOptions.configId;\n }\n\n // Resolve milestoneId if it's a string\n if (typeof this.reporterOptions.milestoneId === 'string') {\n const milestone = await this.client.findMilestoneByName(projectId, this.reporterOptions.milestoneId);\n if (!milestone) {\n throw new Error(`Milestone not found: \"${this.reporterOptions.milestoneId}\"`);\n }\n this.state.resolvedIds.milestoneId = milestone.id;\n this.log(`Resolved milestone \"${this.reporterOptions.milestoneId}\" -> ${milestone.id}`);\n } else if (typeof this.reporterOptions.milestoneId === 'number') {\n this.state.resolvedIds.milestoneId = this.reporterOptions.milestoneId;\n }\n\n // Resolve stateId if it's a string\n if (typeof this.reporterOptions.stateId === 'string') {\n const state = await this.client.findWorkflowStateByName(projectId, this.reporterOptions.stateId);\n if (!state) {\n throw new Error(`Workflow state not found: \"${this.reporterOptions.stateId}\"`);\n }\n this.state.resolvedIds.stateId = state.id;\n this.log(`Resolved workflow state \"${this.reporterOptions.stateId}\" -> ${state.id}`);\n } else if (typeof this.reporterOptions.stateId === 'number') {\n this.state.resolvedIds.stateId = this.reporterOptions.stateId;\n }\n\n // Resolve parentFolderId if it's a string\n if (typeof this.reporterOptions.parentFolderId === 'string') {\n let folder = await this.client.findFolderByName(projectId, this.reporterOptions.parentFolderId);\n if (!folder) {\n // If createFolderHierarchy is enabled, create the parent folder\n if (this.reporterOptions.createFolderHierarchy) {\n this.log(`Parent folder \"${this.reporterOptions.parentFolderId}\" not found, creating it...`);\n folder = await this.client.createFolder({\n projectId,\n name: this.reporterOptions.parentFolderId,\n });\n this.log(`Created parent folder \"${this.reporterOptions.parentFolderId}\" -> ${folder.id}`);\n } else {\n throw new Error(`Folder not found: \"${this.reporterOptions.parentFolderId}\"`);\n }\n }\n this.state.resolvedIds.parentFolderId = folder.id;\n this.log(`Resolved folder \"${this.reporterOptions.parentFolderId}\" -> ${folder.id}`);\n } else if (typeof this.reporterOptions.parentFolderId === 'number') {\n this.state.resolvedIds.parentFolderId = this.reporterOptions.parentFolderId;\n }\n\n // Resolve templateId if it's a string\n if (typeof this.reporterOptions.templateId === 'string') {\n const template = await this.client.findTemplateByName(projectId, this.reporterOptions.templateId);\n if (!template) {\n throw new Error(`Template not found: \"${this.reporterOptions.templateId}\"`);\n }\n this.state.resolvedIds.templateId = template.id;\n this.log(`Resolved template \"${this.reporterOptions.templateId}\" -> ${template.id}`);\n } else if (typeof this.reporterOptions.templateId === 'number') {\n this.state.resolvedIds.templateId = this.reporterOptions.templateId;\n }\n\n // Resolve tagIds if they contain strings\n if (this.reporterOptions.tagIds && this.reporterOptions.tagIds.length > 0) {\n this.state.resolvedIds.tagIds = await this.client.resolveTagIds(projectId, this.reporterOptions.tagIds);\n this.log(`Resolved tags: ${this.state.resolvedIds.tagIds.join(', ')}`);\n }\n }\n\n /**\n * Fetch status ID mappings from TestPlanIt\n */\n private async fetchStatusMappings(): Promise {\n const statuses: NormalizedStatus[] = ['passed', 'failed', 'skipped', 'blocked'];\n\n for (const status of statuses) {\n const statusId = await this.client.getStatusId(this.reporterOptions.projectId, status);\n if (statusId) {\n this.state.statusIds[status] = statusId;\n this.log(`Status mapping: ${status} -> ${statusId}`);\n }\n }\n\n if (!this.state.statusIds.passed || !this.state.statusIds.failed) {\n throw new Error('Could not find required status mappings (passed/failed) in TestPlanIt');\n }\n }\n\n /**\n * Map test status to JUnit result type\n */\n private mapStatusToJUnitType(status: 'passed' | 'failed' | 'skipped' | 'pending'): JUnitResultType {\n switch (status) {\n case 'passed':\n return 'PASSED';\n case 'failed':\n return 'FAILURE';\n case 'skipped':\n case 'pending':\n return 'SKIPPED';\n default:\n return 'FAILURE';\n }\n }\n\n /**\n * Create the JUnit test suite for this test run\n */\n private async createJUnitTestSuite(): Promise {\n if (this.state.testSuiteId) {\n return; // Already created (either from shared state or previous call)\n }\n\n if (!this.state.testRunId) {\n throw new Error('Cannot create JUnit test suite without a test run ID');\n }\n\n // In oneReport mode, check if another worker has already created a suite\n if (this.reporterOptions.oneReport) {\n const sharedState = readSharedState(this.reporterOptions.projectId);\n if (sharedState?.testSuiteId) {\n this.state.testSuiteId = sharedState.testSuiteId;\n this.log('Using shared JUnit test suite from file:', sharedState.testSuiteId);\n return;\n }\n }\n\n const runName = this.formatRunName(this.reporterOptions.runName || '{suite} - {date} {time}');\n\n this.log('Creating JUnit test suite...');\n\n const testSuite = await this.client.createJUnitTestSuite({\n testRunId: this.state.testRunId,\n name: runName,\n time: 0, // Will be updated incrementally\n tests: 0,\n failures: 0,\n errors: 0,\n skipped: 0,\n });\n\n this.state.testSuiteId = testSuite.id;\n this.log('Created JUnit test suite with ID:', testSuite.id);\n\n // Update shared state with suite ID if in oneReport mode\n if (this.reporterOptions.oneReport) {\n const finalState = writeSharedStateIfAbsent(this.reporterOptions.projectId, {\n testRunId: this.state.testRunId,\n testSuiteId: this.state.testSuiteId,\n createdAt: new Date().toISOString(),\n activeWorkers: 1,\n });\n\n // Check if another worker wrote first — use their suite\n if (finalState && finalState.testSuiteId !== this.state.testSuiteId) {\n this.log(`Another worker created test suite first, switching from ${this.state.testSuiteId} to ${finalState.testSuiteId}`);\n this.state.testSuiteId = finalState.testSuiteId;\n }\n }\n }\n\n /**\n * Map WebdriverIO framework name to TestPlanIt test run type\n */\n private getTestRunType(): TestPlanItReporterOptions['testRunType'] {\n // If explicitly set by user, use that\n if (this.reporterOptions.testRunType) {\n return this.reporterOptions.testRunType;\n }\n\n // Auto-detect from WebdriverIO framework config\n if (this.detectedFramework) {\n const framework = this.detectedFramework.toLowerCase();\n if (framework === 'mocha') return 'MOCHA';\n if (framework === 'cucumber') return 'CUCUMBER';\n // jasmine and others map to REGULAR\n return 'REGULAR';\n }\n\n // Default fallback\n return 'MOCHA';\n }\n\n /**\n * Create a new test run\n */\n private async createTestRun(): Promise {\n const runName = this.formatRunName(this.reporterOptions.runName || '{suite} - {date} {time}');\n const testRunType = this.getTestRunType();\n\n this.log('Creating test run:', runName, '(type:', testRunType + ')');\n\n const testRun = await this.client.createTestRun({\n projectId: this.reporterOptions.projectId,\n name: runName,\n testRunType,\n configId: this.state.resolvedIds.configId,\n milestoneId: this.state.resolvedIds.milestoneId,\n stateId: this.state.resolvedIds.stateId,\n tagIds: this.state.resolvedIds.tagIds,\n });\n\n this.state.testRunId = testRun.id;\n this.log('Created test run with ID:', testRun.id);\n }\n\n /**\n * Format the run name with placeholders\n */\n private formatRunName(template: string): string {\n const now = new Date();\n const date = now.toISOString().split('T')[0];\n const time = now.toTimeString().split(' ')[0];\n const browser = this.state.capabilities?.browserName || 'unknown';\n const platform = this.state.capabilities?.platformName || process.platform;\n\n // Get spec file name from currentSpec (e.g., \"/path/to/test.spec.ts\" -> \"test.spec.ts\")\n let spec = 'unknown';\n if (this.currentSpec) {\n const parts = this.currentSpec.split('/');\n spec = parts[parts.length - 1] || 'unknown';\n // Remove common extensions for cleaner names\n spec = spec.replace(/\\.(spec|test)\\.(ts|js|mjs|cjs)$/, '');\n }\n\n // Get the root suite name (first describe block)\n const suite = this.currentSuite[0] || 'Tests';\n\n return template\n .replace('{date}', date)\n .replace('{time}', time)\n .replace('{browser}', browser)\n .replace('{platform}', platform)\n .replace('{spec}', spec)\n .replace('{suite}', suite);\n }\n\n /**\n * Parse case IDs from test title using the configured pattern\n * @example With default pattern: \"[1761] [1762] should load the page\" -> [1761, 1762]\n * @example With C-prefix pattern: \"C12345 C67890 should load the page\" -> [12345, 67890]\n */\n private parseCaseIds(title: string): { caseIds: number[]; cleanTitle: string } {\n const pattern = this.reporterOptions.caseIdPattern || /\\[(\\d+)\\]/g;\n const regex = typeof pattern === 'string' ? new RegExp(pattern, 'g') : new RegExp(pattern.source, 'g');\n const caseIds: number[] = [];\n let match: RegExpExecArray | null;\n\n while ((match = regex.exec(title)) !== null) {\n // Find the first capturing group that has a value (supports patterns with multiple groups)\n for (let i = 1; i < match.length; i++) {\n if (match[i]) {\n caseIds.push(parseInt(match[i], 10));\n break;\n }\n }\n }\n\n // Remove matched patterns from title\n const cleanTitle = title.replace(regex, '').trim().replace(/\\s+/g, ' ');\n\n return { caseIds, cleanTitle };\n }\n\n /**\n * Get the full suite path as a string\n */\n private getFullSuiteName(): string {\n return this.currentSuite.join(' > ');\n }\n\n /**\n * Create a unique key for a test case\n */\n private createCaseKey(suiteName: string, testName: string): string {\n return `${suiteName}::${testName}`;\n }\n\n // ============================================================================\n // WebdriverIO Reporter Hooks\n // ============================================================================\n\n onRunnerStart(runner: RunnerStats): void {\n this.log('Runner started:', runner.cid);\n this.state.capabilities = runner.capabilities as WebdriverIO.Capabilities;\n\n // Auto-detect the test framework from WebdriverIO config\n // This is accessed via runner.config.framework (e.g., 'mocha', 'cucumber', 'jasmine')\n const config = runner.config as { framework?: string } | undefined;\n if (config?.framework) {\n this.detectedFramework = config.framework;\n this.log('Detected framework:', this.detectedFramework);\n }\n\n // Don't initialize here - wait until we have actual test results to report\n // This avoids creating empty test runs for specs with no matching tests\n }\n\n onSuiteStart(suite: SuiteStats): void {\n if (suite.title) {\n this.currentSuite.push(suite.title);\n this.log('Suite started:', this.getFullSuiteName());\n }\n }\n\n onSuiteEnd(suite: SuiteStats): void {\n if (suite.title) {\n this.log('Suite ended:', this.getFullSuiteName());\n this.currentSuite.pop();\n }\n }\n\n onTestStart(test: TestStats): void {\n this.log('Test started:', test.title);\n // Track the current test for screenshot association\n const { cleanTitle } = this.parseCaseIds(test.title);\n const suiteName = this.getFullSuiteName();\n const fullTitle = suiteName ? `${suiteName} > ${cleanTitle}` : cleanTitle;\n this.currentTestUid = `${test.cid}_${fullTitle}`;\n this.currentCid = test.cid;\n }\n\n /**\n * Capture screenshots from WebdriverIO commands\n */\n onAfterCommand(commandArgs: AfterCommandArgs): void {\n // Check if this is a screenshot command\n if (!this.reporterOptions.uploadScreenshots) {\n return;\n }\n\n // WebdriverIO uses 'takeScreenshot' as the command name or '/screenshot' endpoint\n const isScreenshotCommand =\n commandArgs.command === 'takeScreenshot' ||\n commandArgs.command === 'saveScreenshot' ||\n commandArgs.endpoint?.includes('/screenshot');\n\n if (!isScreenshotCommand) {\n return;\n }\n\n this.log(`Screenshot command detected: ${commandArgs.command}, endpoint: ${commandArgs.endpoint}`);\n\n // For saveScreenshot, the result is the file path, not base64 data\n // We need to handle both takeScreenshot (returns base64) and saveScreenshot (saves to file)\n const result = commandArgs.result as Record | string | undefined;\n const resultValue = (typeof result === 'object' && result !== null ? result.value : result) ?? result;\n\n if (!resultValue) {\n this.log('No result value in screenshot command');\n return;\n }\n\n // The result should be base64-encoded screenshot data\n const screenshotData = resultValue as string;\n if (typeof screenshotData !== 'string') {\n this.log(`Screenshot result is not a string: ${typeof screenshotData}`);\n return;\n }\n\n // Check if this looks like a file path rather than base64 data\n // File paths start with / (Unix) or drive letter like C:\\ (Windows)\n // Base64 PNG data starts with \"iVBORw0KGgo\" (PNG header)\n const looksLikeFilePath =\n screenshotData.startsWith('/') ||\n /^[A-Za-z]:[\\\\\\/]/.test(screenshotData) ||\n screenshotData.startsWith('./') ||\n screenshotData.startsWith('../');\n\n if (looksLikeFilePath) {\n this.log(`Screenshot result appears to be a file path: ${screenshotData.substring(0, 100)}`);\n return;\n }\n\n // Store the screenshot associated with the current test\n if (this.currentTestUid) {\n const buffer = Buffer.from(screenshotData, 'base64');\n const existing = this.pendingScreenshots.get(this.currentTestUid) || [];\n existing.push(buffer);\n this.pendingScreenshots.set(this.currentTestUid, existing);\n this.log('Captured screenshot for test:', this.currentTestUid, `(${buffer.length} bytes)`);\n } else {\n this.log('No current test UID to associate screenshot with');\n }\n }\n\n onTestPass(test: TestStats): void {\n this.handleTestEnd(test, 'passed');\n }\n\n onTestFail(test: TestStats): void {\n this.handleTestEnd(test, 'failed');\n }\n\n onTestSkip(test: TestStats): void {\n this.handleTestEnd(test, 'skipped');\n }\n\n /**\n * Handle test completion\n */\n private handleTestEnd(test: TestStats, status: 'passed' | 'failed' | 'skipped'): void {\n const { caseIds, cleanTitle } = this.parseCaseIds(test.title);\n const suiteName = this.getFullSuiteName();\n const suitePath = [...this.currentSuite]; // Copy the current suite hierarchy\n const fullTitle = suiteName ? `${suiteName} > ${cleanTitle}` : cleanTitle;\n const uid = `${test.cid}_${fullTitle}`;\n\n // Calculate duration from timestamps for reliability\n // WebdriverIO's test.duration can be inconsistent in some versions\n const startTime = new Date(test.start).getTime();\n const endTime = test.end ? new Date(test.end).getTime() : Date.now();\n const durationMs = endTime - startTime;\n\n // Format WebdriverIO command output if available\n let commandOutput: string | undefined;\n if (test.output && test.output.length > 0) {\n commandOutput = test.output\n .map((o) => {\n const parts: string[] = [];\n if (o.method) parts.push(`[${o.method}]`);\n if (o.endpoint) parts.push(o.endpoint);\n if (o.result !== undefined) {\n const resultStr = typeof o.result === 'string' ? o.result : JSON.stringify(o.result);\n // Truncate long results\n parts.push(resultStr.length > 200 ? resultStr.substring(0, 200) + '...' : resultStr);\n }\n return parts.join(' ');\n })\n .join('\\n');\n }\n\n const result: TrackedTestResult = {\n caseId: caseIds[0], // Primary case ID\n suiteName,\n suitePath,\n testName: cleanTitle,\n fullTitle,\n originalTitle: test.title,\n status,\n duration: durationMs,\n errorMessage: test.error?.message,\n stackTrace: this.reporterOptions.includeStackTrace ? test.error?.stack : undefined,\n startedAt: new Date(test.start),\n finishedAt: new Date(endTime),\n browser: this.state.capabilities?.browserName,\n platform: this.state.capabilities?.platformName || process.platform,\n screenshots: [],\n retryAttempt: test.retries || 0,\n uid,\n specFile: this.currentSpec,\n commandOutput,\n };\n\n this.state.results.set(uid, result);\n this.log(`Test ${status}:`, cleanTitle, caseIds.length > 0 ? `(Case IDs: ${caseIds.join(', ')})` : '');\n\n // Report result asynchronously - track operation so WebdriverIO waits for completion\n const reportPromise = this.reportResult(result, caseIds);\n this.trackOperation(reportPromise);\n }\n\n /**\n * Report a single test result to TestPlanIt\n */\n private async reportResult(result: TrackedTestResult, caseIds: number[]): Promise {\n try {\n // Check if this result can be reported BEFORE initializing\n // This prevents creating empty test runs for tests without case IDs\n if (caseIds.length === 0 && !this.reporterOptions.autoCreateTestCases) {\n console.warn(`[TestPlanIt] WARNING: Skipping \"${result.testName}\" - no case ID found and autoCreateTestCases is disabled. Set autoCreateTestCases: true to automatically find or create test cases by name.`);\n return;\n }\n\n // Now we know this result can be reported, so initialize if needed\n await this.initialize();\n\n if (!this.state.testRunId) {\n this.logError('No test run ID available, skipping result');\n return;\n }\n\n // Create JUnit test suite if not already created\n await this.createJUnitTestSuite();\n\n if (!this.state.testSuiteId) {\n this.logError('No test suite ID available, skipping result');\n return;\n }\n\n // Get or create repository case\n let repositoryCaseId: number | undefined;\n const caseKey = this.createCaseKey(result.suiteName, result.testName);\n\n // DEBUG: Always log key info about this test\n this.log('DEBUG: Processing test:', result.testName);\n this.log('DEBUG: suiteName:', result.suiteName);\n this.log('DEBUG: suitePath:', JSON.stringify(result.suitePath));\n this.log('DEBUG: caseIds from title:', JSON.stringify(caseIds));\n this.log('DEBUG: autoCreateTestCases:', this.reporterOptions.autoCreateTestCases);\n this.log('DEBUG: createFolderHierarchy:', this.reporterOptions.createFolderHierarchy);\n\n if (caseIds.length > 0) {\n // Use the provided case ID directly as repository case ID\n repositoryCaseId = caseIds[0];\n this.log('DEBUG: Using case ID from title:', repositoryCaseId);\n } else if (this.reporterOptions.autoCreateTestCases) {\n // Check cache first\n if (this.state.caseIdMap.has(caseKey)) {\n repositoryCaseId = this.state.caseIdMap.get(caseKey);\n this.log('DEBUG: Found in cache:', caseKey, '->', repositoryCaseId);\n } else {\n // Determine the target folder ID\n let folderId = this.state.resolvedIds.parentFolderId;\n const templateId = this.state.resolvedIds.templateId;\n\n this.log('DEBUG: Initial folderId (parentFolderId):', folderId);\n this.log('DEBUG: templateId:', templateId);\n\n if (!folderId || !templateId) {\n this.logError('autoCreateTestCases requires parentFolderId and templateId');\n return;\n }\n\n // Create folder hierarchy based on suite structure if enabled\n this.log('DEBUG: Checking folder hierarchy - createFolderHierarchy:', this.reporterOptions.createFolderHierarchy, 'suitePath.length:', result.suitePath.length);\n if (this.reporterOptions.createFolderHierarchy && result.suitePath.length > 0) {\n const folderPathKey = result.suitePath.join(' > ');\n this.log('DEBUG: Will create folder hierarchy for path:', folderPathKey);\n\n // Check folder cache first\n if (this.state.folderPathMap.has(folderPathKey)) {\n folderId = this.state.folderPathMap.get(folderPathKey)!;\n this.log('Using cached folder ID for path:', folderPathKey, '->', folderId);\n } else {\n // Create the folder hierarchy\n this.log('Creating folder hierarchy:', result.suitePath.join(' > '));\n this.log('DEBUG: Calling findOrCreateFolderPath with projectId:', this.reporterOptions.projectId, 'suitePath:', JSON.stringify(result.suitePath), 'parentFolderId:', this.state.resolvedIds.parentFolderId);\n const folder = await this.client.findOrCreateFolderPath(\n this.reporterOptions.projectId,\n result.suitePath,\n this.state.resolvedIds.parentFolderId\n );\n folderId = folder.id;\n this.state.folderPathMap.set(folderPathKey, folderId);\n this.log('Created/found folder:', folder.name, '(ID:', folder.id + ')');\n }\n } else {\n this.log('DEBUG: Skipping folder hierarchy - createFolderHierarchy:', this.reporterOptions.createFolderHierarchy, 'suitePath.length:', result.suitePath.length);\n }\n\n this.log('DEBUG: Final folderId for test case:', folderId);\n\n const { testCase, action } = await this.client.findOrCreateTestCase({\n projectId: this.reporterOptions.projectId,\n folderId,\n templateId,\n name: result.testName,\n className: result.suiteName || undefined,\n source: 'API',\n automated: true,\n });\n\n // Track statistics based on action\n if (action === 'found') {\n this.state.stats.testCasesFound++;\n } else if (action === 'created') {\n this.state.stats.testCasesCreated++;\n } else if (action === 'moved') {\n this.state.stats.testCasesMoved++;\n }\n\n repositoryCaseId = testCase.id;\n this.state.caseIdMap.set(caseKey, repositoryCaseId);\n this.log(`${action === 'found' ? 'Found' : action === 'created' ? 'Created' : 'Moved'} test case:`, testCase.id, testCase.name, 'in folder:', folderId);\n }\n } else {\n this.log('DEBUG: autoCreateTestCases is false, not creating test case');\n }\n\n if (!repositoryCaseId) {\n this.log('No repository case ID, skipping result');\n return;\n }\n\n // Get or create test run case\n let testRunCaseId: number | undefined;\n const runCaseKey = `${this.state.testRunId}_${repositoryCaseId}`;\n\n if (this.state.testRunCaseMap.has(runCaseKey)) {\n testRunCaseId = this.state.testRunCaseMap.get(runCaseKey);\n } else {\n const testRunCase = await this.client.findOrAddTestCaseToRun({\n testRunId: this.state.testRunId,\n repositoryCaseId,\n });\n testRunCaseId = testRunCase.id;\n this.state.testRunCaseMap.set(runCaseKey, testRunCaseId);\n this.log('Added case to run:', testRunCaseId);\n }\n\n // Get status ID for the JUnit result\n const statusId = this.state.statusIds[result.status] || this.state.statusIds.failed!;\n\n // Map status to JUnit result type\n const junitType = this.mapStatusToJUnitType(result.status);\n\n // Build error message/content for failed tests\n let message: string | undefined;\n let content: string | undefined;\n\n if (result.errorMessage) {\n message = result.errorMessage;\n }\n if (result.stackTrace) {\n content = result.stackTrace;\n }\n\n // Create the JUnit test result\n // WebdriverIO provides duration in milliseconds, JUnit expects seconds\n const durationInSeconds = result.duration / 1000;\n const junitResult = await this.client.createJUnitTestResult({\n testSuiteId: this.state.testSuiteId,\n repositoryCaseId,\n type: junitType,\n message,\n content,\n statusId,\n time: durationInSeconds,\n executedAt: result.finishedAt,\n file: result.specFile,\n systemOut: result.commandOutput,\n });\n\n this.log('Created JUnit test result:', junitResult.id, '(type:', junitType + ')');\n this.reportedResultCount++;\n\n // Store the JUnit result ID for deferred screenshot upload\n // Screenshots taken in afterTest hook won't be available yet, so we upload them in onRunnerEnd\n result.junitResultId = junitResult.id;\n\n // Update reporter stats (suite stats are calculated by backend from JUnitTestResult rows)\n if (result.status === 'failed') {\n this.state.stats.resultsFailed++;\n } else if (result.status === 'skipped') {\n this.state.stats.resultsSkipped++;\n } else {\n this.state.stats.resultsPassed++;\n }\n } catch (error) {\n this.state.stats.apiErrors++;\n this.logError(`Failed to report result for ${result.testName}:`, error);\n }\n }\n\n /**\n * Called when the entire test session ends\n */\n async onRunnerEnd(runner: RunnerStats): Promise {\n // If no tests were tracked and no initialization was started, silently skip\n // This handles specs with no matching tests (all filtered out by grep, etc.)\n if (this.state.results.size === 0 && !this.initPromise) {\n this.log('No test results to report, skipping');\n return;\n }\n\n this.log('Runner ended, waiting for initialization and pending results...');\n\n // Wait for initialization to complete (might still be in progress)\n if (this.initPromise) {\n try {\n await this.initPromise;\n } catch {\n // Error already captured in state.initError\n }\n }\n\n // Wait for any remaining pending operations\n // (WebdriverIO waits via isSynchronised, but we also wait here for safety)\n await Promise.allSettled([...this.pendingOperations]);\n\n // Check if initialization failed\n if (this.state.initError) {\n console.error('\\n[TestPlanIt] FAILED: Reporter initialization failed');\n console.error(` Error: ${this.state.initError.message}`);\n console.error(' No results were reported to TestPlanIt.');\n console.error(' Please check your configuration and API connectivity.');\n return;\n }\n\n // If no test run was created (no reportable results), silently skip\n if (!this.state.testRunId) {\n this.log('No test run created, skipping summary');\n return;\n }\n\n // If no results were actually reported to TestPlanIt, silently skip\n // This handles the case where tests ran but none had valid case IDs\n if (this.reportedResultCount === 0) {\n this.log('No results were reported to TestPlanIt, skipping summary');\n return;\n }\n\n // Upload any pending screenshots\n // Screenshots are uploaded here (deferred) because afterTest hooks run after onTestFail/onTestPass,\n // so screenshots taken in afterTest wouldn't be available during reportResult\n if (this.reporterOptions.uploadScreenshots && this.pendingScreenshots.size > 0) {\n this.log(`Uploading screenshots for ${this.pendingScreenshots.size} test(s)...`);\n\n // Create upload promises for all screenshots and track them\n // This ensures WebdriverIO waits for uploads to complete (via isSynchronised)\n const uploadPromises: Promise[] = [];\n\n for (const [uid, screenshots] of this.pendingScreenshots.entries()) {\n const result = this.state.results.get(uid);\n if (!result?.junitResultId) {\n this.log(`Skipping screenshots for ${uid} - no JUnit result ID`);\n continue;\n }\n\n this.log(`Uploading ${screenshots.length} screenshot(s) for test:`, result.testName);\n for (let i = 0; i < screenshots.length; i++) {\n const uploadPromise = (async () => {\n try {\n // Create a meaningful file name: testName_status_screenshot#.png\n // Sanitize test name for filename (remove special chars, limit length)\n const sanitizedTestName = result.testName\n .replace(/[^a-zA-Z0-9_-]/g, '_')\n .substring(0, 50);\n const fileName = `${sanitizedTestName}_${result.status}_${i + 1}.png`;\n\n // Build a descriptive note with test context\n const noteParts: string[] = [];\n noteParts.push(`Test: ${result.testName}`);\n if (result.suiteName) {\n noteParts.push(`Suite: ${result.suiteName}`);\n }\n noteParts.push(`Status: ${result.status}`);\n if (result.browser) {\n noteParts.push(`Browser: ${result.browser}`);\n }\n if (result.errorMessage) {\n // Truncate error message if too long\n const errorPreview = result.errorMessage.length > 200\n ? result.errorMessage.substring(0, 200) + '...'\n : result.errorMessage;\n noteParts.push(`Error: ${errorPreview}`);\n }\n const note = noteParts.join('\\n');\n\n this.log(`Starting upload of ${fileName} (${screenshots[i].length} bytes) to JUnit result ${result.junitResultId}...`);\n await this.client.uploadJUnitAttachment(\n result.junitResultId!,\n screenshots[i],\n fileName,\n 'image/png',\n note\n );\n this.state.stats.screenshotsUploaded++;\n this.log(`Uploaded screenshot ${i + 1}/${screenshots.length} for ${result.testName}`);\n } catch (uploadError) {\n this.state.stats.screenshotsFailed++;\n const errorMessage = uploadError instanceof Error ? uploadError.message : String(uploadError);\n const errorStack = uploadError instanceof Error ? uploadError.stack : undefined;\n this.logError(`Failed to upload screenshot ${i + 1}:`, errorMessage);\n if (errorStack) {\n this.logError('Stack trace:', errorStack);\n }\n }\n })();\n\n // Track this operation so WebdriverIO waits for it\n this.trackOperation(uploadPromise);\n uploadPromises.push(uploadPromise);\n }\n }\n\n // Wait for all uploads to complete before proceeding\n await Promise.allSettled(uploadPromises);\n\n // Clear all pending screenshots\n this.pendingScreenshots.clear();\n }\n\n // Note: JUnit test suite statistics (tests, failures, errors, skipped, time) are NOT updated here.\n // The backend calculates these dynamically from JUnitTestResult rows in the summary API.\n // This ensures correct totals when multiple workers/spec files report to the same test run.\n\n // Complete the test run if configured\n // When managedByService is true, the service handles completion in onComplete — skip entirely\n // In legacy oneReport mode, decrement worker count and only complete when last worker finishes\n if (this.managedByService) {\n this.log('Skipping test run completion (managed by TestPlanItService)');\n } else if (this.reporterOptions.completeRunOnFinish) {\n if (this.reporterOptions.oneReport) {\n // Decrement worker count and check if we're the last worker\n const isLastWorker = decrementWorkerCount(this.reporterOptions.projectId);\n if (isLastWorker) {\n const completeRunOp = (async () => {\n try {\n await this.client.completeTestRun(this.state.testRunId!, this.reporterOptions.projectId);\n this.log('Test run completed (last worker):', this.state.testRunId);\n deleteSharedState(this.reporterOptions.projectId);\n } catch (error) {\n this.logError('Failed to complete test run:', error);\n }\n })();\n this.trackOperation(completeRunOp);\n await completeRunOp;\n } else {\n this.log('Skipping test run completion (waiting for other workers to finish)');\n }\n } else {\n const completeRunOp = (async () => {\n try {\n await this.client.completeTestRun(this.state.testRunId!, this.reporterOptions.projectId);\n this.log('Test run completed:', this.state.testRunId);\n } catch (error) {\n this.logError('Failed to complete test run:', error);\n }\n })();\n this.trackOperation(completeRunOp);\n await completeRunOp;\n }\n } else if (this.reporterOptions.oneReport) {\n // Even if not completing, decrement worker count in legacy mode\n decrementWorkerCount(this.reporterOptions.projectId);\n }\n\n // Print summary\n const stats = this.state.stats;\n const duration = ((Date.now() - stats.startTime.getTime()) / 1000).toFixed(1);\n const totalResults = stats.resultsPassed + stats.resultsFailed + stats.resultsSkipped;\n const totalCases = stats.testCasesFound + stats.testCasesCreated + stats.testCasesMoved;\n\n console.log('\\n[TestPlanIt] ═══════════════════════════════════════════════════════');\n console.log('[TestPlanIt] Results Summary');\n console.log('[TestPlanIt] ═══════════════════════════════════════════════════════');\n console.log(`[TestPlanIt] Test Run ID: ${this.state.testRunId}`);\n console.log(`[TestPlanIt] Duration: ${duration}s`);\n console.log('[TestPlanIt]');\n console.log('[TestPlanIt] Test Results:');\n console.log(`[TestPlanIt] ✓ Passed: ${stats.resultsPassed}`);\n console.log(`[TestPlanIt] ✗ Failed: ${stats.resultsFailed}`);\n console.log(`[TestPlanIt] ○ Skipped: ${stats.resultsSkipped}`);\n console.log(`[TestPlanIt] Total: ${totalResults}`);\n\n if (this.reporterOptions.autoCreateTestCases && totalCases > 0) {\n console.log('[TestPlanIt]');\n console.log('[TestPlanIt] Test Cases:');\n console.log(`[TestPlanIt] Found (existing): ${stats.testCasesFound}`);\n console.log(`[TestPlanIt] Created (new): ${stats.testCasesCreated}`);\n if (stats.testCasesMoved > 0) {\n console.log(`[TestPlanIt] Moved (restored): ${stats.testCasesMoved}`);\n }\n }\n\n if (this.reporterOptions.uploadScreenshots && (stats.screenshotsUploaded > 0 || stats.screenshotsFailed > 0)) {\n console.log('[TestPlanIt]');\n console.log('[TestPlanIt] Screenshots:');\n console.log(`[TestPlanIt] Uploaded: ${stats.screenshotsUploaded}`);\n if (stats.screenshotsFailed > 0) {\n console.log(`[TestPlanIt] Failed: ${stats.screenshotsFailed}`);\n }\n }\n\n if (stats.apiErrors > 0) {\n console.log('[TestPlanIt]');\n console.log(`[TestPlanIt] ⚠ API Errors: ${stats.apiErrors}`);\n }\n\n console.log('[TestPlanIt]');\n console.log(`[TestPlanIt] View results: ${this.reporterOptions.domain}/projects/runs/${this.reporterOptions.projectId}/${this.state.testRunId}`);\n console.log('[TestPlanIt] ═══════════════════════════════════════════════════════\\n');\n }\n\n /**\n * Get the current state (for debugging)\n */\n getState(): ReporterState {\n return this.state;\n }\n}\n","/**\n * WebdriverIO Launcher Service for TestPlanIt.\n *\n * Manages the test run lifecycle in the main WDIO process:\n * - onPrepare: Creates the test run and JUnit test suite ONCE before any workers start\n * - onComplete: Completes the test run ONCE after all workers finish\n *\n * This ensures all spec files across all worker batches report to a single test run,\n * regardless of `maxInstances` or execution order.\n *\n * @example\n * ```javascript\n * // wdio.conf.js\n * import { TestPlanItService } from '@testplanit/wdio-reporter';\n *\n * export const config = {\n * services: [\n * [TestPlanItService, {\n * domain: 'https://testplanit.example.com',\n * apiToken: process.env.TESTPLANIT_API_TOKEN,\n * projectId: 1,\n * runName: 'E2E Tests - {date}',\n * }]\n * ],\n * reporters: [\n * ['@testplanit/wdio-reporter', {\n * domain: 'https://testplanit.example.com',\n * apiToken: process.env.TESTPLANIT_API_TOKEN,\n * projectId: 1,\n * autoCreateTestCases: true,\n * parentFolderId: 10,\n * templateId: 1,\n * }]\n * ]\n * }\n * ```\n *\n * @packageDocumentation\n */\n\nimport { TestPlanItClient } from '@testplanit/api';\nimport type { TestPlanItServiceOptions } from './types.js';\nimport {\n writeSharedState,\n deleteSharedState,\n type SharedState,\n} from './shared.js';\n\n/**\n * WebdriverIO Launcher Service for TestPlanIt.\n *\n * Creates a single test run before any workers start and completes it\n * after all workers finish. Workers read the shared state file to find\n * the pre-created test run and report results to it.\n */\nexport default class TestPlanItService {\n private options: TestPlanItServiceOptions;\n private client: TestPlanItClient;\n private verbose: boolean;\n private testRunId?: number;\n private testSuiteId?: number;\n\n constructor(serviceOptions: TestPlanItServiceOptions) {\n // Validate required options\n if (!serviceOptions.domain) {\n throw new Error('TestPlanIt service: domain is required');\n }\n if (!serviceOptions.apiToken) {\n throw new Error('TestPlanIt service: apiToken is required');\n }\n if (!serviceOptions.projectId) {\n throw new Error('TestPlanIt service: projectId is required');\n }\n\n this.options = {\n completeRunOnFinish: true,\n runName: 'Automated Tests - {date} {time}',\n testRunType: 'MOCHA',\n timeout: 30000,\n maxRetries: 3,\n verbose: false,\n ...serviceOptions,\n };\n\n this.verbose = this.options.verbose ?? false;\n\n this.client = new TestPlanItClient({\n baseUrl: this.options.domain,\n apiToken: this.options.apiToken,\n timeout: this.options.timeout,\n maxRetries: this.options.maxRetries,\n });\n }\n\n /**\n * Log a message if verbose mode is enabled\n */\n private log(message: string, ...args: unknown[]): void {\n if (this.verbose) {\n console.log(`[TestPlanIt Service] ${message}`, ...args);\n }\n }\n\n /**\n * Log an error (always logs, not just in verbose mode)\n */\n private logError(message: string, error?: unknown): void {\n const errorMsg = error instanceof Error ? error.message : String(error ?? '');\n console.error(`[TestPlanIt Service] ERROR: ${message}`, errorMsg);\n }\n\n /**\n * Format run name with available placeholders.\n * Note: {browser}, {spec}, and {suite} are NOT available in the service context\n * since it runs before any workers start.\n */\n private formatRunName(template: string): string {\n const now = new Date();\n const date = now.toISOString().split('T')[0];\n const time = now.toTimeString().split(' ')[0];\n const platform = process.platform;\n\n return template\n .replace('{date}', date)\n .replace('{time}', time)\n .replace('{platform}', platform)\n .replace('{browser}', 'unknown')\n .replace('{spec}', 'unknown')\n .replace('{suite}', 'Tests');\n }\n\n /**\n * Resolve string option IDs to numeric IDs using the API client.\n */\n private async resolveIds(): Promise<{\n configId?: number;\n milestoneId?: number;\n stateId?: number;\n tagIds?: number[];\n }> {\n const projectId = this.options.projectId;\n const resolved: {\n configId?: number;\n milestoneId?: number;\n stateId?: number;\n tagIds?: number[];\n } = {};\n\n if (typeof this.options.configId === 'string') {\n const config = await this.client.findConfigurationByName(projectId, this.options.configId);\n if (!config) {\n throw new Error(`Configuration not found: \"${this.options.configId}\"`);\n }\n resolved.configId = config.id;\n this.log(`Resolved configuration \"${this.options.configId}\" -> ${config.id}`);\n } else if (typeof this.options.configId === 'number') {\n resolved.configId = this.options.configId;\n }\n\n if (typeof this.options.milestoneId === 'string') {\n const milestone = await this.client.findMilestoneByName(projectId, this.options.milestoneId);\n if (!milestone) {\n throw new Error(`Milestone not found: \"${this.options.milestoneId}\"`);\n }\n resolved.milestoneId = milestone.id;\n this.log(`Resolved milestone \"${this.options.milestoneId}\" -> ${milestone.id}`);\n } else if (typeof this.options.milestoneId === 'number') {\n resolved.milestoneId = this.options.milestoneId;\n }\n\n if (typeof this.options.stateId === 'string') {\n const state = await this.client.findWorkflowStateByName(projectId, this.options.stateId);\n if (!state) {\n throw new Error(`Workflow state not found: \"${this.options.stateId}\"`);\n }\n resolved.stateId = state.id;\n this.log(`Resolved workflow state \"${this.options.stateId}\" -> ${state.id}`);\n } else if (typeof this.options.stateId === 'number') {\n resolved.stateId = this.options.stateId;\n }\n\n if (this.options.tagIds && this.options.tagIds.length > 0) {\n resolved.tagIds = await this.client.resolveTagIds(projectId, this.options.tagIds);\n this.log(`Resolved tags: ${resolved.tagIds.join(', ')}`);\n }\n\n return resolved;\n }\n\n /**\n * onPrepare - Runs once in the main process before any workers start.\n *\n * Creates the test run and JUnit test suite, then writes shared state\n * so all worker reporters can find and use the pre-created run.\n */\n async onPrepare(): Promise {\n this.log('Preparing test run...');\n this.log(` Domain: ${this.options.domain}`);\n this.log(` Project ID: ${this.options.projectId}`);\n\n try {\n // Clean up any stale shared state from a previous run\n deleteSharedState(this.options.projectId);\n\n // Resolve string IDs to numeric IDs\n const resolved = await this.resolveIds();\n\n // Format the run name\n const runName = this.formatRunName(this.options.runName ?? 'Automated Tests - {date} {time}');\n\n // Create the test run\n this.log(`Creating test run: \"${runName}\" (type: ${this.options.testRunType})`);\n const testRun = await this.client.createTestRun({\n projectId: this.options.projectId,\n name: runName,\n testRunType: this.options.testRunType,\n configId: resolved.configId,\n milestoneId: resolved.milestoneId,\n stateId: resolved.stateId,\n tagIds: resolved.tagIds,\n });\n this.testRunId = testRun.id;\n this.log(`Created test run with ID: ${this.testRunId}`);\n\n // Create the JUnit test suite\n this.log('Creating JUnit test suite...');\n const testSuite = await this.client.createJUnitTestSuite({\n testRunId: this.testRunId,\n name: runName,\n time: 0,\n tests: 0,\n failures: 0,\n errors: 0,\n skipped: 0,\n });\n this.testSuiteId = testSuite.id;\n this.log(`Created JUnit test suite with ID: ${this.testSuiteId}`);\n\n // Write shared state file for workers to read\n const sharedState: SharedState = {\n testRunId: this.testRunId,\n testSuiteId: this.testSuiteId,\n createdAt: new Date().toISOString(),\n activeWorkers: 0, // Not used in service-managed mode\n managedByService: true,\n };\n writeSharedState(this.options.projectId, sharedState);\n this.log('Wrote shared state file for workers');\n\n // Always print this so users can see the run was created\n console.log(`[TestPlanIt Service] Test run created: \"${runName}\" (ID: ${this.testRunId})`);\n } catch (error) {\n this.logError('Failed to prepare test run:', error);\n // Clean up shared state on failure so reporters fall back to self-managed mode\n deleteSharedState(this.options.projectId);\n throw error;\n }\n }\n\n /**\n * onComplete - Runs once in the main process after all workers finish.\n *\n * Completes the test run and cleans up the shared state file.\n */\n async onComplete(exitCode: number): Promise {\n this.log(`All workers finished (exit code: ${exitCode})`);\n\n try {\n if (this.testRunId && this.options.completeRunOnFinish) {\n this.log(`Completing test run ${this.testRunId}...`);\n await this.client.completeTestRun(this.testRunId, this.options.projectId);\n this.log('Test run completed successfully');\n }\n\n // Print summary\n if (this.testRunId) {\n console.log('\\n[TestPlanIt Service] ══════════════════════════════════════════');\n console.log(`[TestPlanIt Service] Test Run ID: ${this.testRunId}`);\n if (this.options.completeRunOnFinish) {\n console.log('[TestPlanIt Service] Status: Completed');\n }\n console.log(`[TestPlanIt Service] View: ${this.options.domain}/projects/runs/${this.options.projectId}/${this.testRunId}`);\n console.log('[TestPlanIt Service] ══════════════════════════════════════════\\n');\n }\n } catch (error) {\n // Don't re-throw — failing onComplete would hide the actual test results\n this.logError('Failed to complete test run:', error);\n } finally {\n // Always clean up shared state\n deleteSharedState(this.options.projectId);\n this.log('Cleaned up shared state file');\n }\n }\n}\n"]} \ No newline at end of file diff --git a/packages/wdio-testplanit-reporter/src/index.ts b/packages/wdio-testplanit-reporter/src/index.ts index de09b709..c3cd2afa 100644 --- a/packages/wdio-testplanit-reporter/src/index.ts +++ b/packages/wdio-testplanit-reporter/src/index.ts @@ -22,7 +22,8 @@ */ export { default, default as TestPlanItReporter } from './reporter.js'; -export type { TestPlanItReporterOptions, TrackedTestResult, ReporterState } from './types.js'; +export { default as TestPlanItService } from './service.js'; +export type { TestPlanItReporterOptions, TestPlanItServiceOptions, TrackedTestResult, ReporterState } from './types.js'; // Re-export useful types from the API package export { TestPlanItClient, TestPlanItError } from '@testplanit/api'; diff --git a/packages/wdio-testplanit-reporter/src/reporter.test.ts b/packages/wdio-testplanit-reporter/src/reporter.test.ts index 95155b1c..036f6ed6 100644 --- a/packages/wdio-testplanit-reporter/src/reporter.test.ts +++ b/packages/wdio-testplanit-reporter/src/reporter.test.ts @@ -126,16 +126,23 @@ vi.mock("@testplanit/api", () => { }; }); -// Mock fs module -vi.mock("fs", () => ({ - existsSync: vi.fn().mockReturnValue(false), // No shared state file exists by default - readFileSync: vi.fn().mockReturnValue(Buffer.from("fake-image-data")), - writeFileSync: vi.fn(), - unlinkSync: vi.fn(), +// Mock shared state utilities +vi.mock("./shared.js", () => ({ + readSharedState: vi.fn().mockReturnValue(null), + writeSharedState: vi.fn(), + writeSharedStateIfAbsent: vi.fn(), + deleteSharedState: vi.fn(), + incrementWorkerCount: vi.fn(), + decrementWorkerCount: vi.fn().mockReturnValue(false), })); // Import after mocks are set up import TestPlanItReporter from "./reporter.js"; +import { + readSharedState, + incrementWorkerCount, + decrementWorkerCount, +} from "./shared.js"; describe("TestPlanItReporter", () => { let reporter: TestPlanItReporter; @@ -474,3 +481,158 @@ describe("caseIdPattern edge cases", () => { expect(result.cleanTitle).toBe("should work"); }); }); + +describe("service-managed mode", () => { + const defaultOptions = { + domain: "https://testplanit.example.com", + apiToken: "tpi_test_token", + projectId: 1, + }; + + const mockedReadSharedState = vi.mocked(readSharedState); + const mockedIncrementWorkerCount = vi.mocked(incrementWorkerCount); + const mockedDecrementWorkerCount = vi.mocked(decrementWorkerCount); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("should not be managed by service before initialization", () => { + mockedReadSharedState.mockReturnValue({ + testRunId: 500, + testSuiteId: 600, + createdAt: new Date().toISOString(), + activeWorkers: 0, + managedByService: true, + }); + + const reporter = new TestPlanItReporter(defaultOptions); + // Before initialization, the flag should be false + expect((reporter as any).managedByService).toBe(false); + }); + + it("should adopt service-managed testRunId and testSuiteId after initialization", async () => { + mockedReadSharedState.mockReturnValue({ + testRunId: 500, + testSuiteId: 600, + createdAt: new Date().toISOString(), + activeWorkers: 0, + managedByService: true, + }); + + const reporter = new TestPlanItReporter(defaultOptions); + // Trigger initialization via the private method + await (reporter as any).initialize(); + + expect((reporter as any).managedByService).toBe(true); + const state = reporter.getState(); + expect(state.testRunId).toBe(500); + expect(state.testSuiteId).toBe(600); + expect(state.initialized).toBe(true); + }); + + it("should not create a test run when service-managed", async () => { + mockedReadSharedState.mockReturnValue({ + testRunId: 500, + testSuiteId: 600, + createdAt: new Date().toISOString(), + activeWorkers: 0, + managedByService: true, + }); + + const reporter = new TestPlanItReporter(defaultOptions); + const client = (reporter as any).client; + const createTestRunSpy = vi.spyOn(client, "createTestRun"); + + await (reporter as any).initialize(); + + expect(createTestRunSpy).not.toHaveBeenCalled(); + }); + + it("should not increment worker count when service-managed", async () => { + mockedReadSharedState.mockReturnValue({ + testRunId: 500, + testSuiteId: 600, + createdAt: new Date().toISOString(), + activeWorkers: 0, + managedByService: true, + }); + + const reporter = new TestPlanItReporter(defaultOptions); + await (reporter as any).initialize(); + + expect(mockedIncrementWorkerCount).not.toHaveBeenCalled(); + }); + + it("should skip test run completion on runner end when service-managed", async () => { + mockedReadSharedState.mockReturnValue({ + testRunId: 500, + testSuiteId: 600, + createdAt: new Date().toISOString(), + activeWorkers: 0, + managedByService: true, + }); + + const reporter = new TestPlanItReporter({ + ...defaultOptions, + completeRunOnFinish: true, + }); + await (reporter as any).initialize(); + + const client = (reporter as any).client; + const completeTestRunSpy = vi.spyOn(client, "completeTestRun"); + + // Simulate runner end + const runnerStats = { + cid: "0-0", + capabilities: { browserName: "chrome" }, + specs: ["/test/specs/login.spec.js"], + } as unknown as import("@wdio/reporter").RunnerStats; + reporter.onRunnerStart(runnerStats); + await (reporter as any).onRunnerEnd(runnerStats); + + // completeTestRun should NOT have been called + expect(completeTestRunSpy).not.toHaveBeenCalled(); + // decrementWorkerCount should NOT have been called + expect(mockedDecrementWorkerCount).not.toHaveBeenCalled(); + }); + + it("should still report results when service-managed", async () => { + mockedReadSharedState.mockReturnValue({ + testRunId: 500, + testSuiteId: 600, + createdAt: new Date().toISOString(), + activeWorkers: 0, + managedByService: true, + }); + + const reporter = new TestPlanItReporter({ + ...defaultOptions, + autoCreateTestCases: true, + parentFolderId: 1, + templateId: 1, + }); + + // Track a test result + const testStats = { + type: "test", + title: "should work", + fullTitle: "Suite > should work", + uid: "test-uid-svc", + cid: "0-0", + state: "passed", + duration: 100, + start: new Date(), + end: new Date(), + retries: 0, + } as import("@wdio/reporter").TestStats; + + reporter.onTestPass(testStats); + const state = reporter.getState(); + expect(state.results.size).toBe(1); + }); +}); diff --git a/packages/wdio-testplanit-reporter/src/reporter.ts b/packages/wdio-testplanit-reporter/src/reporter.ts index 9108a34b..2001a577 100644 --- a/packages/wdio-testplanit-reporter/src/reporter.ts +++ b/packages/wdio-testplanit-reporter/src/reporter.ts @@ -2,21 +2,13 @@ import WDIOReporter, { type RunnerStats, type SuiteStats, type TestStats, type A import { TestPlanItClient } from '@testplanit/api'; import type { NormalizedStatus, JUnitResultType } from '@testplanit/api'; import type { TestPlanItReporterOptions, TrackedTestResult, ReporterState } from './types.js'; -import * as fs from 'fs'; -import * as path from 'path'; -import * as os from 'os'; - -/** - * Shared state file for oneReport mode. - * Contains the test run ID shared across all worker instances. - */ -interface SharedState { - testRunId: number; - testSuiteId?: number; - createdAt: string; - /** Number of active workers using this test run */ - activeWorkers: number; -} +import { + readSharedState, + writeSharedStateIfAbsent, + deleteSharedState, + incrementWorkerCount, + decrementWorkerCount, +} from './shared.js'; /** * WebdriverIO Reporter for TestPlanIt @@ -50,6 +42,8 @@ export default class TestPlanItReporter extends WDIOReporter { private currentTestUid: string | null = null; private currentCid: string | null = null; private pendingScreenshots: Map = new Map(); + /** When true, the TestPlanItService manages the test run lifecycle */ + private managedByService = false; /** * WebdriverIO uses this getter to determine if the reporter has finished async operations. @@ -140,242 +134,6 @@ export default class TestPlanItReporter extends WDIOReporter { console.error(`[TestPlanIt] ERROR: ${message}`, errorMsg, stack); } - /** - * Get the path to the shared state file for oneReport mode. - * Uses a file in the temp directory with a name based on the project ID. - */ - private getSharedStateFilePath(): string { - const fileName = `.testplanit-reporter-${this.reporterOptions.projectId}.json`; - return path.join(os.tmpdir(), fileName); - } - - /** - * Read shared state from file (for oneReport mode). - * Returns null if: - * - File doesn't exist - * - File is stale (older than 4 hours) - * - Previous run completed (activeWorkers === 0) - */ - private readSharedState(): SharedState | null { - const filePath = this.getSharedStateFilePath(); - try { - if (!fs.existsSync(filePath)) { - return null; - } - const content = fs.readFileSync(filePath, 'utf-8'); - const state: SharedState = JSON.parse(content); - - // Check if state is stale (older than 4 hours) - const createdAt = new Date(state.createdAt); - const fourHoursAgo = new Date(Date.now() - 4 * 60 * 60 * 1000); - if (createdAt < fourHoursAgo) { - this.log('Shared state file is stale (older than 4 hours), starting fresh'); - this.deleteSharedState(); - return null; - } - - // Check if previous run completed (no active workers) - // This ensures each new test execution creates a new test run - if (state.activeWorkers === 0) { - this.log('Previous test run completed (activeWorkers=0), starting fresh'); - this.deleteSharedState(); - return null; - } - - return state; - } catch (error) { - this.log('Failed to read shared state file:', error); - return null; - } - } - - /** - * Write shared state to file (for oneReport mode). - * Uses a lock file to prevent race conditions. - * Only writes the testRunId if the file doesn't exist yet (first writer wins). - * Updates testSuiteId if not already set. - */ - private writeSharedState(state: SharedState): void { - const filePath = this.getSharedStateFilePath(); - const lockPath = `${filePath}.lock`; - - try { - // Simple lock mechanism - try to create lock file exclusively - let lockAcquired = false; - for (let i = 0; i < 10; i++) { - try { - fs.writeFileSync(lockPath, process.pid.toString(), { flag: 'wx' }); - lockAcquired = true; - break; - } catch { - // Lock exists, wait and retry with exponential backoff - const sleepMs = 50 * Math.pow(2, i) + Math.random() * 50; - // Use setTimeout-based sleep since Atomics.wait requires SharedArrayBuffer - const start = Date.now(); - while (Date.now() - start < sleepMs) { - // Busy wait (not ideal but works synchronously) - } - } - } - - if (!lockAcquired) { - this.log('Could not acquire lock for shared state file'); - return; - } - - try { - // Check if file already exists - if (fs.existsSync(filePath)) { - // Read existing state and merge - only update testSuiteId if not set - const existingContent = fs.readFileSync(filePath, 'utf-8'); - const existingState: SharedState = JSON.parse(existingContent); - - // Only update if the testSuiteId is missing and we have one to add - if (!existingState.testSuiteId && state.testSuiteId) { - existingState.testSuiteId = state.testSuiteId; - fs.writeFileSync(filePath, JSON.stringify(existingState, null, 2)); - this.log('Updated shared state file with testSuiteId:', state.testSuiteId); - } else { - this.log('Shared state file already exists with testSuiteId, not overwriting'); - } - return; - } - fs.writeFileSync(filePath, JSON.stringify(state, null, 2)); - this.log('Wrote shared state file:', filePath); - } finally { - // Release lock - try { - fs.unlinkSync(lockPath); - } catch { - // Ignore lock removal errors - } - } - } catch (error) { - this.log('Failed to write shared state file:', error); - } - } - - /** - * Delete shared state file (cleanup after run completes). - */ - private deleteSharedState(): void { - const filePath = this.getSharedStateFilePath(); - try { - if (fs.existsSync(filePath)) { - fs.unlinkSync(filePath); - this.log('Deleted shared state file'); - } - } catch (error) { - this.log('Failed to delete shared state file:', error); - } - } - - /** - * Increment the active worker count in shared state. - * Called when a worker starts using the shared test run. - */ - private incrementWorkerCount(): void { - const filePath = this.getSharedStateFilePath(); - const lockPath = `${filePath}.lock`; - - try { - // Acquire lock - let lockAcquired = false; - for (let i = 0; i < 10; i++) { - try { - fs.writeFileSync(lockPath, process.pid.toString(), { flag: 'wx' }); - lockAcquired = true; - break; - } catch { - const sleepMs = 50 * Math.pow(2, i) + Math.random() * 50; - const start = Date.now(); - while (Date.now() - start < sleepMs) { - // Busy wait - } - } - } - - if (!lockAcquired) { - this.log('Could not acquire lock to increment worker count'); - return; - } - - try { - if (fs.existsSync(filePath)) { - const content = fs.readFileSync(filePath, 'utf-8'); - const state: SharedState = JSON.parse(content); - state.activeWorkers = (state.activeWorkers || 0) + 1; - fs.writeFileSync(filePath, JSON.stringify(state, null, 2)); - this.log('Incremented worker count to:', state.activeWorkers); - } - } finally { - try { - fs.unlinkSync(lockPath); - } catch { - // Ignore - } - } - } catch (error) { - this.log('Failed to increment worker count:', error); - } - } - - /** - * Decrement the active worker count in shared state. - * Returns true if this was the last worker (count reached 0). - */ - private decrementWorkerCount(): boolean { - const filePath = this.getSharedStateFilePath(); - const lockPath = `${filePath}.lock`; - - try { - // Acquire lock - let lockAcquired = false; - for (let i = 0; i < 10; i++) { - try { - fs.writeFileSync(lockPath, process.pid.toString(), { flag: 'wx' }); - lockAcquired = true; - break; - } catch { - const sleepMs = 50 * Math.pow(2, i) + Math.random() * 50; - const start = Date.now(); - while (Date.now() - start < sleepMs) { - // Busy wait - } - } - } - - if (!lockAcquired) { - this.log('Could not acquire lock to decrement worker count'); - return false; - } - - try { - if (fs.existsSync(filePath)) { - const content = fs.readFileSync(filePath, 'utf-8'); - const state: SharedState = JSON.parse(content); - state.activeWorkers = Math.max(0, (state.activeWorkers || 1) - 1); - fs.writeFileSync(filePath, JSON.stringify(state, null, 2)); - this.log('Decremented worker count to:', state.activeWorkers); - - if (state.activeWorkers === 0) { - this.log('This is the last worker'); - return true; - } - } - } finally { - try { - fs.unlinkSync(lockPath); - } catch { - // Ignore - } - } - } catch (error) { - this.log('Failed to decrement worker count:', error); - } - return false; - } - /** * Track an async operation to prevent the runner from terminating early. * The operation is added to pendingOperations and removed when complete. @@ -425,61 +183,73 @@ export default class TestPlanItReporter extends WDIOReporter { // Handle oneReport mode - check for existing shared state if (this.reporterOptions.oneReport && !this.state.testRunId) { - const sharedState = this.readSharedState(); + const sharedState = readSharedState(this.reporterOptions.projectId); if (sharedState) { - this.state.testRunId = sharedState.testRunId; - this.state.testSuiteId = sharedState.testSuiteId; - this.log(`Using shared test run from file: ${sharedState.testRunId}`); - // Validate the shared test run still exists, is not completed, and is not deleted - try { - const testRun = await this.client.getTestRun(this.state.testRunId); - if (testRun.isDeleted) { - // Test run was soft-deleted - this.log(`Shared test run ${testRun.id} is deleted, starting fresh`); - this.state.testRunId = undefined; - this.state.testSuiteId = undefined; - this.deleteSharedState(); - } else if (testRun.isCompleted) { - // Test run was already completed (from a previous execution) - this.log(`Shared test run ${testRun.id} is already completed, starting fresh`); + if (sharedState.managedByService) { + // Service manages the run — just use the IDs, skip all lifecycle management + this.state.testRunId = sharedState.testRunId; + this.state.testSuiteId = sharedState.testSuiteId; + this.managedByService = true; + this.log(`Using service-managed test run: ${sharedState.testRunId}`); + } else { + // Legacy oneReport mode — validate and join the existing run + this.state.testRunId = sharedState.testRunId; + this.state.testSuiteId = sharedState.testSuiteId; + this.log(`Using shared test run from file: ${sharedState.testRunId}`); + + // In legacy mode, skip runs where all workers have finished + if (sharedState.activeWorkers === 0) { + this.log('Previous test run completed (activeWorkers=0), starting fresh'); + deleteSharedState(this.reporterOptions.projectId); this.state.testRunId = undefined; this.state.testSuiteId = undefined; - this.deleteSharedState(); } else { - this.log(`Validated shared test run: ${testRun.name} (ID: ${testRun.id})`); - // Increment worker count since we're joining an existing run - this.incrementWorkerCount(); + // Validate the shared test run still exists, is not completed, and is not deleted + try { + const testRun = await this.client.getTestRun(this.state.testRunId); + if (testRun.isDeleted) { + this.log(`Shared test run ${testRun.id} is deleted, starting fresh`); + this.state.testRunId = undefined; + this.state.testSuiteId = undefined; + deleteSharedState(this.reporterOptions.projectId); + } else if (testRun.isCompleted) { + this.log(`Shared test run ${testRun.id} is already completed, starting fresh`); + this.state.testRunId = undefined; + this.state.testSuiteId = undefined; + deleteSharedState(this.reporterOptions.projectId); + } else { + this.log(`Validated shared test run: ${testRun.name} (ID: ${testRun.id})`); + incrementWorkerCount(this.reporterOptions.projectId); + } + } catch { + this.log('Shared test run no longer exists, will create new one'); + this.state.testRunId = undefined; + this.state.testSuiteId = undefined; + deleteSharedState(this.reporterOptions.projectId); + } } - } catch { - // Shared test run no longer exists, clear state and create new one - this.log('Shared test run no longer exists, will create new one'); - this.state.testRunId = undefined; - this.state.testSuiteId = undefined; - this.deleteSharedState(); } } } - // Create or validate test run - if (!this.state.testRunId) { + // Create or validate test run (skip if service-managed) + if (!this.state.testRunId && !this.managedByService) { // In oneReport mode, use atomic write to prevent race conditions if (this.reporterOptions.oneReport) { // Create the test run first await this.createTestRun(); this.log(`Created test run with ID: ${this.state.testRunId}`); - // Try to write shared state - this will fail if another worker already wrote - this.writeSharedState({ + // Try to write shared state - first writer wins + const finalState = writeSharedStateIfAbsent(this.reporterOptions.projectId, { testRunId: this.state.testRunId!, testSuiteId: this.state.testSuiteId, createdAt: new Date().toISOString(), - activeWorkers: 1, // First worker + activeWorkers: 1, }); - // Re-check shared state to see if we won the race - const finalState = this.readSharedState(); + // Check if another worker wrote first if (finalState && finalState.testRunId !== this.state.testRunId) { - // Another worker created a test run first - use theirs instead this.log(`Another worker created test run first, switching from ${this.state.testRunId} to ${finalState.testRunId}`); this.state.testRunId = finalState.testRunId; this.state.testSuiteId = finalState.testSuiteId; @@ -488,9 +258,8 @@ export default class TestPlanItReporter extends WDIOReporter { await this.createTestRun(); this.log(`Created test run with ID: ${this.state.testRunId}`); } - } else if (!this.reporterOptions.oneReport) { - // Only validate if not using oneReport (already validated above) - // Validate existing test run + } else if (this.state.testRunId && !this.reporterOptions.oneReport && !this.managedByService) { + // Validate existing test run (only when not using oneReport or service) try { const testRun = await this.client.getTestRun(this.state.testRunId); this.log(`Using existing test run: ${testRun.name} (ID: ${testRun.id})`); @@ -652,7 +421,7 @@ export default class TestPlanItReporter extends WDIOReporter { // In oneReport mode, check if another worker has already created a suite if (this.reporterOptions.oneReport) { - const sharedState = this.readSharedState(); + const sharedState = readSharedState(this.reporterOptions.projectId); if (sharedState?.testSuiteId) { this.state.testSuiteId = sharedState.testSuiteId; this.log('Using shared JUnit test suite from file:', sharedState.testSuiteId); @@ -679,15 +448,14 @@ export default class TestPlanItReporter extends WDIOReporter { // Update shared state with suite ID if in oneReport mode if (this.reporterOptions.oneReport) { - this.writeSharedState({ + const finalState = writeSharedStateIfAbsent(this.reporterOptions.projectId, { testRunId: this.state.testRunId, testSuiteId: this.state.testSuiteId, createdAt: new Date().toISOString(), - activeWorkers: 1, // Will be merged/updated by writeSharedState + activeWorkers: 1, }); - // Re-check to handle race condition - if another worker wrote first, use their suite - const finalState = this.readSharedState(); + // Check if another worker wrote first — use their suite if (finalState && finalState.testSuiteId !== this.state.testSuiteId) { this.log(`Another worker created test suite first, switching from ${this.state.testSuiteId} to ${finalState.testSuiteId}`); this.state.testSuiteId = finalState.testSuiteId; @@ -1323,19 +1091,20 @@ export default class TestPlanItReporter extends WDIOReporter { // This ensures correct totals when multiple workers/spec files report to the same test run. // Complete the test run if configured - // In oneReport mode, decrement worker count and only complete when last worker finishes - // Track this operation to prevent WebdriverIO from terminating early - if (this.reporterOptions.completeRunOnFinish) { + // When managedByService is true, the service handles completion in onComplete — skip entirely + // In legacy oneReport mode, decrement worker count and only complete when last worker finishes + if (this.managedByService) { + this.log('Skipping test run completion (managed by TestPlanItService)'); + } else if (this.reporterOptions.completeRunOnFinish) { if (this.reporterOptions.oneReport) { // Decrement worker count and check if we're the last worker - const isLastWorker = this.decrementWorkerCount(); + const isLastWorker = decrementWorkerCount(this.reporterOptions.projectId); if (isLastWorker) { const completeRunOp = (async () => { try { await this.client.completeTestRun(this.state.testRunId!, this.reporterOptions.projectId); this.log('Test run completed (last worker):', this.state.testRunId); - // Clean up shared state file - this.deleteSharedState(); + deleteSharedState(this.reporterOptions.projectId); } catch (error) { this.logError('Failed to complete test run:', error); } @@ -1358,8 +1127,8 @@ export default class TestPlanItReporter extends WDIOReporter { await completeRunOp; } } else if (this.reporterOptions.oneReport) { - // Even if not completing, decrement worker count - this.decrementWorkerCount(); + // Even if not completing, decrement worker count in legacy mode + decrementWorkerCount(this.reporterOptions.projectId); } // Print summary diff --git a/packages/wdio-testplanit-reporter/src/service.test.ts b/packages/wdio-testplanit-reporter/src/service.test.ts new file mode 100644 index 00000000..7c0e5ff6 --- /dev/null +++ b/packages/wdio-testplanit-reporter/src/service.test.ts @@ -0,0 +1,399 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +// Mock the API client - must use class syntax for `new` to work +const mockClientInstance = { + createTestRun: vi.fn().mockResolvedValue({ id: 100, name: 'Test Run' }), + createJUnitTestSuite: vi.fn().mockResolvedValue({ id: 200, name: 'Test Suite' }), + completeTestRun: vi.fn().mockResolvedValue({ id: 100, isCompleted: true }), + findConfigurationByName: vi.fn().mockResolvedValue({ id: 10, name: 'Config' }), + findMilestoneByName: vi.fn().mockResolvedValue({ id: 20, name: 'Milestone' }), + findWorkflowStateByName: vi.fn().mockResolvedValue({ id: 30, name: 'State' }), + resolveTagIds: vi.fn().mockResolvedValue([1, 2, 3]), +}; + +vi.mock('@testplanit/api', () => { + return { + TestPlanItClient: class MockTestPlanItClient { + constructor() { + return mockClientInstance; + } + }, + }; +}); + +// Mock shared state utilities +vi.mock('./shared.js', () => ({ + writeSharedState: vi.fn(), + deleteSharedState: vi.fn(), + readSharedState: vi.fn().mockReturnValue(null), +})); + +import TestPlanItService from './service.js'; +import { writeSharedState, deleteSharedState } from './shared.js'; + +const mockedWriteSharedState = vi.mocked(writeSharedState); +const mockedDeleteSharedState = vi.mocked(deleteSharedState); + +describe('TestPlanItService', () => { + const defaultOptions = { + domain: 'https://testplanit.example.com', + apiToken: 'tpi_test_token', + projectId: 1, + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('constructor', () => { + it('should create service with valid options', () => { + const service = new TestPlanItService(defaultOptions); + expect(service).toBeDefined(); + }); + + it('should throw if domain is missing', () => { + expect(() => { + new TestPlanItService({ ...defaultOptions, domain: '' }); + }).toThrow('domain is required'); + }); + + it('should throw if apiToken is missing', () => { + expect(() => { + new TestPlanItService({ ...defaultOptions, apiToken: '' }); + }).toThrow('apiToken is required'); + }); + + it('should throw if projectId is missing', () => { + expect(() => { + new TestPlanItService({ ...defaultOptions, projectId: 0 }); + }).toThrow('projectId is required'); + }); + + it('should use default values for optional fields', () => { + const service = new TestPlanItService(defaultOptions); + // Just verify it doesn't throw — defaults are applied internally + expect(service).toBeDefined(); + }); + }); + + describe('onPrepare', () => { + it('should create test run and JUnit test suite', async () => { + const service = new TestPlanItService(defaultOptions); + await service.onPrepare(); + + // Should have called createTestRun and createJUnitTestSuite + const clientInstance = mockClientInstance; + expect(clientInstance.createTestRun).toHaveBeenCalledWith( + expect.objectContaining({ + projectId: 1, + }) + ); + expect(clientInstance.createJUnitTestSuite).toHaveBeenCalledWith( + expect.objectContaining({ + testRunId: 100, + }) + ); + }); + + it('should write shared state with managedByService: true', async () => { + const service = new TestPlanItService(defaultOptions); + await service.onPrepare(); + + expect(mockedWriteSharedState).toHaveBeenCalledWith( + 1, + expect.objectContaining({ + testRunId: 100, + testSuiteId: 200, + managedByService: true, + activeWorkers: 0, + }) + ); + }); + + it('should clean up stale shared state before creating run', async () => { + const service = new TestPlanItService(defaultOptions); + await service.onPrepare(); + + // deleteSharedState should be called before writeSharedState + const deleteCallOrder = mockedDeleteSharedState.mock.invocationCallOrder[0]; + const writeCallOrder = mockedWriteSharedState.mock.invocationCallOrder[0]; + expect(deleteCallOrder).toBeLessThan(writeCallOrder); + }); + + it('should resolve string configId', async () => { + const service = new TestPlanItService({ + ...defaultOptions, + configId: 'My Config', + }); + await service.onPrepare(); + + const clientInstance = mockClientInstance; + expect(clientInstance.findConfigurationByName).toHaveBeenCalledWith(1, 'My Config'); + expect(clientInstance.createTestRun).toHaveBeenCalledWith( + expect.objectContaining({ + configId: 10, + }) + ); + }); + + it('should resolve string milestoneId', async () => { + const service = new TestPlanItService({ + ...defaultOptions, + milestoneId: 'Sprint 1', + }); + await service.onPrepare(); + + const clientInstance = mockClientInstance; + expect(clientInstance.findMilestoneByName).toHaveBeenCalledWith(1, 'Sprint 1'); + expect(clientInstance.createTestRun).toHaveBeenCalledWith( + expect.objectContaining({ + milestoneId: 20, + }) + ); + }); + + it('should format run name with placeholders', async () => { + const service = new TestPlanItService({ + ...defaultOptions, + runName: 'Tests - {date}', + }); + await service.onPrepare(); + + const clientInstance = mockClientInstance; + const callArg = clientInstance.createTestRun.mock.calls[0][0]; + expect(callArg.name).toMatch(/Tests - \d{4}-\d{2}-\d{2}/); + }); + + it('should replace unavailable placeholders with fallbacks', async () => { + const service = new TestPlanItService({ + ...defaultOptions, + runName: '{browser} - {spec} - {suite}', + }); + await service.onPrepare(); + + const clientInstance = mockClientInstance; + const callArg = clientInstance.createTestRun.mock.calls[0][0]; + expect(callArg.name).toBe('unknown - unknown - Tests'); + }); + + it('should throw when string configId is not found', async () => { + mockClientInstance.findConfigurationByName.mockResolvedValueOnce(null); + + const service = new TestPlanItService({ + ...defaultOptions, + configId: 'Nonexistent Config', + }); + + await expect(service.onPrepare()).rejects.toThrow('Configuration not found: "Nonexistent Config"'); + }); + + it('should throw when string milestoneId is not found', async () => { + mockClientInstance.findMilestoneByName.mockResolvedValueOnce(null); + + const service = new TestPlanItService({ + ...defaultOptions, + milestoneId: 'Nonexistent Milestone', + }); + + await expect(service.onPrepare()).rejects.toThrow('Milestone not found: "Nonexistent Milestone"'); + }); + + it('should throw when string stateId is not found', async () => { + mockClientInstance.findWorkflowStateByName.mockResolvedValueOnce(null); + + const service = new TestPlanItService({ + ...defaultOptions, + stateId: 'Nonexistent State', + }); + + await expect(service.onPrepare()).rejects.toThrow('Workflow state not found: "Nonexistent State"'); + }); + + it('should resolve string stateId', async () => { + const service = new TestPlanItService({ + ...defaultOptions, + stateId: 'In Progress', + }); + await service.onPrepare(); + + expect(mockClientInstance.findWorkflowStateByName).toHaveBeenCalledWith(1, 'In Progress'); + expect(mockClientInstance.createTestRun).toHaveBeenCalledWith( + expect.objectContaining({ + stateId: 30, + }) + ); + }); + + it('should pass through numeric configId directly', async () => { + const service = new TestPlanItService({ + ...defaultOptions, + configId: 42, + }); + await service.onPrepare(); + + expect(mockClientInstance.findConfigurationByName).not.toHaveBeenCalled(); + expect(mockClientInstance.createTestRun).toHaveBeenCalledWith( + expect.objectContaining({ + configId: 42, + }) + ); + }); + + it('should pass through numeric milestoneId directly', async () => { + const service = new TestPlanItService({ + ...defaultOptions, + milestoneId: 55, + }); + await service.onPrepare(); + + expect(mockClientInstance.findMilestoneByName).not.toHaveBeenCalled(); + expect(mockClientInstance.createTestRun).toHaveBeenCalledWith( + expect.objectContaining({ + milestoneId: 55, + }) + ); + }); + + it('should pass through numeric stateId directly', async () => { + const service = new TestPlanItService({ + ...defaultOptions, + stateId: 77, + }); + await service.onPrepare(); + + expect(mockClientInstance.findWorkflowStateByName).not.toHaveBeenCalled(); + expect(mockClientInstance.createTestRun).toHaveBeenCalledWith( + expect.objectContaining({ + stateId: 77, + }) + ); + }); + + it('should resolve tagIds', async () => { + const service = new TestPlanItService({ + ...defaultOptions, + tagIds: ['tag1', 'tag2'], + }); + await service.onPrepare(); + + expect(mockClientInstance.resolveTagIds).toHaveBeenCalledWith(1, ['tag1', 'tag2']); + expect(mockClientInstance.createTestRun).toHaveBeenCalledWith( + expect.objectContaining({ + tagIds: [1, 2, 3], + }) + ); + }); + + it('should not resolve tagIds when empty', async () => { + const service = new TestPlanItService({ + ...defaultOptions, + tagIds: [], + }); + await service.onPrepare(); + + expect(mockClientInstance.resolveTagIds).not.toHaveBeenCalled(); + }); + + it('should clean up shared state and re-throw on API failure', async () => { + // Temporarily make createTestRun fail + mockClientInstance.createTestRun.mockRejectedValueOnce(new Error('API error')); + + const service = new TestPlanItService(defaultOptions); + + await expect(service.onPrepare()).rejects.toThrow('API error'); + // deleteSharedState called twice: once at start (cleanup), once on error + expect(mockedDeleteSharedState).toHaveBeenCalledTimes(2); + }); + }); + + describe('onComplete', () => { + it('should complete test run when completeRunOnFinish is true', async () => { + const service = new TestPlanItService({ + ...defaultOptions, + completeRunOnFinish: true, + }); + await service.onPrepare(); + mockClientInstance.completeTestRun.mockClear(); + + await service.onComplete(0); + expect(mockClientInstance.completeTestRun).toHaveBeenCalledWith(100, 1); + }); + + it('should not complete test run when completeRunOnFinish is false', async () => { + const service = new TestPlanItService({ + ...defaultOptions, + completeRunOnFinish: false, + }); + await service.onPrepare(); + mockClientInstance.completeTestRun.mockClear(); + + await service.onComplete(0); + expect(mockClientInstance.completeTestRun).not.toHaveBeenCalled(); + }); + + it('should always delete shared state file', async () => { + const service = new TestPlanItService(defaultOptions); + await service.onPrepare(); + mockedDeleteSharedState.mockClear(); + + await service.onComplete(0); + expect(mockedDeleteSharedState).toHaveBeenCalledWith(1); + }); + + it('should not throw on API failure', async () => { + const service = new TestPlanItService(defaultOptions); + await service.onPrepare(); + + mockClientInstance.completeTestRun.mockRejectedValueOnce(new Error('Network error')); + + // Should not throw + await expect(service.onComplete(0)).resolves.toBeUndefined(); + }); + + it('should handle case where onPrepare was never called', async () => { + const service = new TestPlanItService(defaultOptions); + // No onPrepare call — testRunId is undefined + await expect(service.onComplete(1)).resolves.toBeUndefined(); + }); + }); + + describe('afterTest', () => { + const mockTakeScreenshot = vi.fn().mockResolvedValue('base64data'); + + beforeEach(() => { + (globalThis as Record).browser = { takeScreenshot: mockTakeScreenshot }; + }); + + afterEach(() => { + delete (globalThis as Record).browser; + }); + + it('should capture screenshot on failure when captureScreenshots is enabled', async () => { + const service = new TestPlanItService({ ...defaultOptions, captureScreenshots: true }); + await service.afterTest({}, {}, { passed: false }); + expect(mockTakeScreenshot).toHaveBeenCalled(); + }); + + it('should not capture screenshot on pass', async () => { + const service = new TestPlanItService({ ...defaultOptions, captureScreenshots: true }); + await service.afterTest({}, {}, { passed: true }); + expect(mockTakeScreenshot).not.toHaveBeenCalled(); + }); + + it('should not capture screenshot when captureScreenshots is disabled', async () => { + const service = new TestPlanItService(defaultOptions); + await service.afterTest({}, {}, { passed: false }); + expect(mockTakeScreenshot).not.toHaveBeenCalled(); + }); + + it('should not throw when screenshot capture fails', async () => { + mockTakeScreenshot.mockRejectedValueOnce(new Error('No browser')); + const service = new TestPlanItService({ ...defaultOptions, captureScreenshots: true }); + await expect(service.afterTest({}, {}, { passed: false })).resolves.toBeUndefined(); + }); + }); +}); diff --git a/packages/wdio-testplanit-reporter/src/service.ts b/packages/wdio-testplanit-reporter/src/service.ts new file mode 100644 index 00000000..70417bbc --- /dev/null +++ b/packages/wdio-testplanit-reporter/src/service.ts @@ -0,0 +1,317 @@ +/** + * WebdriverIO Launcher Service for TestPlanIt. + * + * Manages the test run lifecycle in the main WDIO process: + * - onPrepare: Creates the test run and JUnit test suite ONCE before any workers start + * - onComplete: Completes the test run ONCE after all workers finish + * + * This ensures all spec files across all worker batches report to a single test run, + * regardless of `maxInstances` or execution order. + * + * @example + * ```javascript + * // wdio.conf.js + * import { TestPlanItService } from '@testplanit/wdio-reporter'; + * + * export const config = { + * services: [ + * [TestPlanItService, { + * domain: 'https://testplanit.example.com', + * apiToken: process.env.TESTPLANIT_API_TOKEN, + * projectId: 1, + * runName: 'E2E Tests - {date}', + * }] + * ], + * reporters: [ + * ['@testplanit/wdio-reporter', { + * domain: 'https://testplanit.example.com', + * apiToken: process.env.TESTPLANIT_API_TOKEN, + * projectId: 1, + * autoCreateTestCases: true, + * parentFolderId: 10, + * templateId: 1, + * }] + * ] + * } + * ``` + * + * @packageDocumentation + */ + +import { TestPlanItClient } from '@testplanit/api'; +import type { TestPlanItServiceOptions } from './types.js'; +import { + writeSharedState, + deleteSharedState, + type SharedState, +} from './shared.js'; + +/** + * WebdriverIO Launcher Service for TestPlanIt. + * + * Creates a single test run before any workers start and completes it + * after all workers finish. Workers read the shared state file to find + * the pre-created test run and report results to it. + */ +export default class TestPlanItService { + private options: TestPlanItServiceOptions; + private client: TestPlanItClient; + private verbose: boolean; + private testRunId?: number; + private testSuiteId?: number; + + constructor(serviceOptions: TestPlanItServiceOptions) { + // Validate required options + if (!serviceOptions.domain) { + throw new Error('TestPlanIt service: domain is required'); + } + if (!serviceOptions.apiToken) { + throw new Error('TestPlanIt service: apiToken is required'); + } + if (!serviceOptions.projectId) { + throw new Error('TestPlanIt service: projectId is required'); + } + + this.options = { + completeRunOnFinish: true, + runName: 'Automated Tests - {date} {time}', + testRunType: 'MOCHA', + timeout: 30000, + maxRetries: 3, + verbose: false, + ...serviceOptions, + }; + + this.verbose = this.options.verbose ?? false; + + this.client = new TestPlanItClient({ + baseUrl: this.options.domain, + apiToken: this.options.apiToken, + timeout: this.options.timeout, + maxRetries: this.options.maxRetries, + }); + } + + /** + * Log a message if verbose mode is enabled + */ + private log(message: string, ...args: unknown[]): void { + if (this.verbose) { + console.log(`[TestPlanIt Service] ${message}`, ...args); + } + } + + /** + * Log an error (always logs, not just in verbose mode) + */ + private logError(message: string, error?: unknown): void { + const errorMsg = error instanceof Error ? error.message : String(error ?? ''); + console.error(`[TestPlanIt Service] ERROR: ${message}`, errorMsg); + } + + /** + * Format run name with available placeholders. + * Note: {browser}, {spec}, and {suite} are NOT available in the service context + * since it runs before any workers start. + */ + private formatRunName(template: string): string { + const now = new Date(); + const date = now.toISOString().split('T')[0]; + const time = now.toTimeString().split(' ')[0]; + const platform = process.platform; + + return template + .replace('{date}', date) + .replace('{time}', time) + .replace('{platform}', platform) + .replace('{browser}', 'unknown') + .replace('{spec}', 'unknown') + .replace('{suite}', 'Tests'); + } + + /** + * Resolve string option IDs to numeric IDs using the API client. + */ + private async resolveIds(): Promise<{ + configId?: number; + milestoneId?: number; + stateId?: number; + tagIds?: number[]; + }> { + const projectId = this.options.projectId; + const resolved: { + configId?: number; + milestoneId?: number; + stateId?: number; + tagIds?: number[]; + } = {}; + + if (typeof this.options.configId === 'string') { + const config = await this.client.findConfigurationByName(projectId, this.options.configId); + if (!config) { + throw new Error(`Configuration not found: "${this.options.configId}"`); + } + resolved.configId = config.id; + this.log(`Resolved configuration "${this.options.configId}" -> ${config.id}`); + } else if (typeof this.options.configId === 'number') { + resolved.configId = this.options.configId; + } + + if (typeof this.options.milestoneId === 'string') { + const milestone = await this.client.findMilestoneByName(projectId, this.options.milestoneId); + if (!milestone) { + throw new Error(`Milestone not found: "${this.options.milestoneId}"`); + } + resolved.milestoneId = milestone.id; + this.log(`Resolved milestone "${this.options.milestoneId}" -> ${milestone.id}`); + } else if (typeof this.options.milestoneId === 'number') { + resolved.milestoneId = this.options.milestoneId; + } + + if (typeof this.options.stateId === 'string') { + const state = await this.client.findWorkflowStateByName(projectId, this.options.stateId); + if (!state) { + throw new Error(`Workflow state not found: "${this.options.stateId}"`); + } + resolved.stateId = state.id; + this.log(`Resolved workflow state "${this.options.stateId}" -> ${state.id}`); + } else if (typeof this.options.stateId === 'number') { + resolved.stateId = this.options.stateId; + } + + if (this.options.tagIds && this.options.tagIds.length > 0) { + resolved.tagIds = await this.client.resolveTagIds(projectId, this.options.tagIds); + this.log(`Resolved tags: ${resolved.tagIds.join(', ')}`); + } + + return resolved; + } + + /** + * onPrepare - Runs once in the main process before any workers start. + * + * Creates the test run and JUnit test suite, then writes shared state + * so all worker reporters can find and use the pre-created run. + */ + async onPrepare(): Promise { + this.log('Preparing test run...'); + this.log(` Domain: ${this.options.domain}`); + this.log(` Project ID: ${this.options.projectId}`); + + try { + // Clean up any stale shared state from a previous run + deleteSharedState(this.options.projectId); + + // Resolve string IDs to numeric IDs + const resolved = await this.resolveIds(); + + // Format the run name + const runName = this.formatRunName(this.options.runName ?? 'Automated Tests - {date} {time}'); + + // Create the test run + this.log(`Creating test run: "${runName}" (type: ${this.options.testRunType})`); + const testRun = await this.client.createTestRun({ + projectId: this.options.projectId, + name: runName, + testRunType: this.options.testRunType, + configId: resolved.configId, + milestoneId: resolved.milestoneId, + stateId: resolved.stateId, + tagIds: resolved.tagIds, + }); + this.testRunId = testRun.id; + this.log(`Created test run with ID: ${this.testRunId}`); + + // Create the JUnit test suite + this.log('Creating JUnit test suite...'); + const testSuite = await this.client.createJUnitTestSuite({ + testRunId: this.testRunId, + name: runName, + time: 0, + tests: 0, + failures: 0, + errors: 0, + skipped: 0, + }); + this.testSuiteId = testSuite.id; + this.log(`Created JUnit test suite with ID: ${this.testSuiteId}`); + + // Write shared state file for workers to read + const sharedState: SharedState = { + testRunId: this.testRunId, + testSuiteId: this.testSuiteId, + createdAt: new Date().toISOString(), + activeWorkers: 0, // Not used in service-managed mode + managedByService: true, + }; + writeSharedState(this.options.projectId, sharedState); + this.log('Wrote shared state file for workers'); + + // Always print this so users can see the run was created + console.log(`[TestPlanIt Service] Test run created: "${runName}" (ID: ${this.testRunId})`); + } catch (error) { + this.logError('Failed to prepare test run:', error); + // Clean up shared state on failure so reporters fall back to self-managed mode + deleteSharedState(this.options.projectId); + throw error; + } + } + + /** + * afterTest - Runs in each worker process after each test. + * + * Captures a screenshot on test failure when `captureScreenshots` is enabled. + * The screenshot is intercepted and uploaded by the reporter automatically. + */ + async afterTest( + _test: Record, + _context: Record, + result: { error?: Error; passed: boolean }, + ): Promise { + if (!this.options.captureScreenshots || result.passed) { + return; + } + + try { + // `browser` is a WDIO global available in worker processes + await (globalThis as Record).browser?.takeScreenshot(); + } catch (error) { + this.log('Failed to capture screenshot:', error); + } + } + + /** + * onComplete - Runs once in the main process after all workers finish. + * + * Completes the test run and cleans up the shared state file. + */ + async onComplete(exitCode: number): Promise { + this.log(`All workers finished (exit code: ${exitCode})`); + + try { + if (this.testRunId && this.options.completeRunOnFinish) { + this.log(`Completing test run ${this.testRunId}...`); + await this.client.completeTestRun(this.testRunId, this.options.projectId); + this.log('Test run completed successfully'); + } + + // Print summary + if (this.testRunId) { + console.log('\n[TestPlanIt Service] ══════════════════════════════════════════'); + console.log(`[TestPlanIt Service] Test Run ID: ${this.testRunId}`); + if (this.options.completeRunOnFinish) { + console.log('[TestPlanIt Service] Status: Completed'); + } + console.log(`[TestPlanIt Service] View: ${this.options.domain}/projects/runs/${this.options.projectId}/${this.testRunId}`); + console.log('[TestPlanIt Service] ══════════════════════════════════════════\n'); + } + } catch (error) { + // Don't re-throw — failing onComplete would hide the actual test results + this.logError('Failed to complete test run:', error); + } finally { + // Always clean up shared state + deleteSharedState(this.options.projectId); + this.log('Cleaned up shared state file'); + } + } +} diff --git a/packages/wdio-testplanit-reporter/src/shared.test.ts b/packages/wdio-testplanit-reporter/src/shared.test.ts new file mode 100644 index 00000000..6c508349 --- /dev/null +++ b/packages/wdio-testplanit-reporter/src/shared.test.ts @@ -0,0 +1,352 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +// We need to test the actual module, so we mock fs at a low level +vi.mock('fs', () => ({ + existsSync: vi.fn(), + readFileSync: vi.fn(), + writeFileSync: vi.fn(), + unlinkSync: vi.fn(), +})); + +import { + getSharedStateFilePath, + readSharedState, + writeSharedState, + writeSharedStateIfAbsent, + deleteSharedState, + incrementWorkerCount, + decrementWorkerCount, + withLock, + type SharedState, +} from './shared.js'; + +const mockedFs = vi.mocked(fs); + +describe('shared utilities', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('getSharedStateFilePath', () => { + it('should return path in os.tmpdir() with projectId', () => { + const result = getSharedStateFilePath(42); + expect(result).toBe(path.join(os.tmpdir(), '.testplanit-reporter-42.json')); + }); + + it('should handle different projectId values', () => { + const result1 = getSharedStateFilePath(1); + const result2 = getSharedStateFilePath(999); + expect(result1).toContain('-1.json'); + expect(result2).toContain('-999.json'); + expect(result1).not.toBe(result2); + }); + }); + + describe('readSharedState', () => { + it('should return null when file does not exist', () => { + mockedFs.existsSync.mockReturnValue(false); + expect(readSharedState(1)).toBeNull(); + }); + + it('should return parsed state when file exists and is valid', () => { + const state: SharedState = { + testRunId: 123, + testSuiteId: 456, + createdAt: new Date().toISOString(), + activeWorkers: 2, + }; + mockedFs.existsSync.mockReturnValue(true); + mockedFs.readFileSync.mockReturnValue(JSON.stringify(state)); + + const result = readSharedState(1); + expect(result).toEqual(state); + }); + + it('should return null and delete file when state is stale (>4 hours)', () => { + const staleDate = new Date(Date.now() - 5 * 60 * 60 * 1000); // 5 hours ago + const state: SharedState = { + testRunId: 123, + createdAt: staleDate.toISOString(), + activeWorkers: 1, + }; + mockedFs.existsSync.mockReturnValue(true); + mockedFs.readFileSync.mockReturnValue(JSON.stringify(state)); + + const result = readSharedState(1); + expect(result).toBeNull(); + expect(mockedFs.unlinkSync).toHaveBeenCalled(); + }); + + it('should return null when file contains invalid JSON', () => { + mockedFs.existsSync.mockReturnValue(true); + mockedFs.readFileSync.mockReturnValue('not valid json'); + + const result = readSharedState(1); + expect(result).toBeNull(); + }); + + it('should return state even when activeWorkers is 0', () => { + // readSharedState no longer checks activeWorkers — caller handles this + const state: SharedState = { + testRunId: 123, + createdAt: new Date().toISOString(), + activeWorkers: 0, + }; + mockedFs.existsSync.mockReturnValue(true); + mockedFs.readFileSync.mockReturnValue(JSON.stringify(state)); + + const result = readSharedState(1); + expect(result).toEqual(state); + }); + + it('should return state with managedByService flag', () => { + const state: SharedState = { + testRunId: 123, + testSuiteId: 456, + createdAt: new Date().toISOString(), + activeWorkers: 0, + managedByService: true, + }; + mockedFs.existsSync.mockReturnValue(true); + mockedFs.readFileSync.mockReturnValue(JSON.stringify(state)); + + const result = readSharedState(1); + expect(result).toEqual(state); + expect(result?.managedByService).toBe(true); + }); + }); + + describe('writeSharedState', () => { + it('should write state to file', () => { + // Lock file doesn't exist (can acquire) + mockedFs.writeFileSync.mockImplementation(() => {}); + + const state: SharedState = { + testRunId: 123, + createdAt: new Date().toISOString(), + activeWorkers: 1, + }; + + writeSharedState(1, state); + + // Should have written lock file and then state file + expect(mockedFs.writeFileSync).toHaveBeenCalled(); + }); + }); + + describe('writeSharedStateIfAbsent', () => { + it('should write state when file does not exist', () => { + mockedFs.writeFileSync.mockImplementation(() => {}); + mockedFs.existsSync.mockReturnValue(false); + + const state: SharedState = { + testRunId: 123, + createdAt: new Date().toISOString(), + activeWorkers: 1, + }; + + const result = writeSharedStateIfAbsent(1, state); + expect(result).toEqual(state); + }); + + it('should return existing state when file already exists', () => { + const existingState: SharedState = { + testRunId: 100, + testSuiteId: 200, + createdAt: new Date().toISOString(), + activeWorkers: 1, + }; + + mockedFs.writeFileSync.mockImplementation(() => {}); + mockedFs.existsSync.mockReturnValue(true); + mockedFs.readFileSync.mockReturnValue(JSON.stringify(existingState)); + + const newState: SharedState = { + testRunId: 999, + createdAt: new Date().toISOString(), + activeWorkers: 1, + }; + + const result = writeSharedStateIfAbsent(1, newState); + expect(result?.testRunId).toBe(100); // Should return existing, not new + }); + + it('should update testSuiteId if not set in existing state', () => { + const existingState: SharedState = { + testRunId: 100, + createdAt: new Date().toISOString(), + activeWorkers: 1, + // No testSuiteId + }; + + mockedFs.writeFileSync.mockImplementation(() => {}); + mockedFs.existsSync.mockReturnValue(true); + mockedFs.readFileSync.mockReturnValue(JSON.stringify(existingState)); + + const newState: SharedState = { + testRunId: 100, + testSuiteId: 456, + createdAt: new Date().toISOString(), + activeWorkers: 1, + }; + + const result = writeSharedStateIfAbsent(1, newState); + expect(result?.testSuiteId).toBe(456); + }); + }); + + describe('deleteSharedState', () => { + it('should delete file when it exists', () => { + mockedFs.existsSync.mockReturnValue(true); + deleteSharedState(1); + expect(mockedFs.unlinkSync).toHaveBeenCalled(); + }); + + it('should do nothing when file does not exist', () => { + mockedFs.existsSync.mockReturnValue(false); + deleteSharedState(1); + expect(mockedFs.unlinkSync).not.toHaveBeenCalled(); + }); + }); + + describe('incrementWorkerCount', () => { + it('should increment activeWorkers by 1', () => { + const state: SharedState = { + testRunId: 123, + createdAt: new Date().toISOString(), + activeWorkers: 1, + }; + + mockedFs.writeFileSync.mockImplementation(() => {}); + mockedFs.existsSync.mockReturnValue(true); + mockedFs.readFileSync.mockReturnValue(JSON.stringify(state)); + + incrementWorkerCount(1); + + // Find the write call that writes the updated state (not the lock file) + const writeCalls = mockedFs.writeFileSync.mock.calls; + const stateWriteCall = writeCalls.find( + (call) => typeof call[1] === 'string' && call[1].includes('"activeWorkers": 2') + ); + expect(stateWriteCall).toBeTruthy(); + }); + }); + + describe('decrementWorkerCount', () => { + it('should decrement activeWorkers by 1 and return false when count > 0', () => { + const state: SharedState = { + testRunId: 123, + createdAt: new Date().toISOString(), + activeWorkers: 2, + }; + + mockedFs.writeFileSync.mockImplementation(() => {}); + mockedFs.existsSync.mockReturnValue(true); + mockedFs.readFileSync.mockReturnValue(JSON.stringify(state)); + + const result = decrementWorkerCount(1); + expect(result).toBe(false); + }); + + it('should return true when count reaches 0', () => { + const state: SharedState = { + testRunId: 123, + createdAt: new Date().toISOString(), + activeWorkers: 1, + }; + + mockedFs.writeFileSync.mockImplementation(() => {}); + mockedFs.existsSync.mockReturnValue(true); + mockedFs.readFileSync.mockReturnValue(JSON.stringify(state)); + + const result = decrementWorkerCount(1); + expect(result).toBe(true); + }); + + it('should not go below 0', () => { + const state: SharedState = { + testRunId: 123, + createdAt: new Date().toISOString(), + activeWorkers: 0, + }; + + mockedFs.writeFileSync.mockImplementation(() => {}); + mockedFs.existsSync.mockReturnValue(true); + mockedFs.readFileSync.mockReturnValue(JSON.stringify(state)); + + const result = decrementWorkerCount(1); + expect(result).toBe(true); // 0 -> 0, still returns true + + const writeCalls = mockedFs.writeFileSync.mock.calls; + const stateWriteCall = writeCalls.find( + (call) => typeof call[1] === 'string' && call[1].includes('"activeWorkers": 0') + ); + expect(stateWriteCall).toBeTruthy(); + }); + + it('should return false when file does not exist', () => { + mockedFs.writeFileSync.mockImplementation(() => {}); + mockedFs.existsSync.mockReturnValue(false); + + const result = decrementWorkerCount(1); + expect(result).toBe(false); + }); + }); + + describe('withLock', () => { + it('should return undefined when lock cannot be acquired', () => { + // Make writeFileSync always throw for the lock file (simulates lock contention) + mockedFs.writeFileSync.mockImplementation((_path, _data, options) => { + if (options && typeof options === 'object' && 'flag' in options && options.flag === 'wx') { + throw new Error('EEXIST: file already exists'); + } + }); + + // Speed up busy-wait by making Date.now() jump forward each call + let time = 1000; + vi.spyOn(Date, 'now').mockImplementation(() => { + time += 100000; // Jump far ahead each call to exit busy-wait instantly + return time; + }); + + try { + const callback = vi.fn().mockReturnValue('should not be called'); + const result = withLock(1, callback); + + expect(result).toBeUndefined(); + expect(callback).not.toHaveBeenCalled(); + } finally { + vi.mocked(Date.now).mockRestore(); + } + }); + + it('should execute callback when lock is acquired', () => { + mockedFs.writeFileSync.mockImplementation(() => {}); + + const callback = vi.fn().mockReturnValue('success'); + const result = withLock(1, callback); + + expect(result).toBe('success'); + expect(callback).toHaveBeenCalled(); + }); + + it('should release lock even when callback throws', () => { + mockedFs.writeFileSync.mockImplementation(() => {}); + + const callback = vi.fn().mockImplementation(() => { + throw new Error('callback error'); + }); + + expect(() => withLock(1, callback)).toThrow('callback error'); + // unlinkSync should be called to release the lock + expect(mockedFs.unlinkSync).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/wdio-testplanit-reporter/src/shared.ts b/packages/wdio-testplanit-reporter/src/shared.ts new file mode 100644 index 00000000..f0a501ac --- /dev/null +++ b/packages/wdio-testplanit-reporter/src/shared.ts @@ -0,0 +1,202 @@ +/** + * Shared state utilities for coordinating between the TestPlanIt WDIO service + * and reporter instances running in separate worker processes. + * + * Uses a file in the OS temp directory to share state (test run ID, test suite ID) + * between the main process (service) and worker processes (reporters). + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; + +/** + * Shared state file for coordinating between WDIO workers and the launcher service. + * + * When `managedByService` is true, the TestPlanItService controls the test run lifecycle + * (creation in onPrepare, completion in onComplete). Workers must not manage the run lifecycle. + * + * When `managedByService` is false/absent, the reporter manages it (legacy oneReport mode). + */ +export interface SharedState { + testRunId: number; + testSuiteId?: number; + createdAt: string; + /** Number of active workers using this test run (only used when managedByService is false) */ + activeWorkers: number; + /** When true, the TestPlanItService controls run creation/completion. Workers must not manage the run lifecycle. */ + managedByService?: boolean; +} + +/** Maximum age of a shared state file before it is considered stale (4 hours) */ +const STALE_THRESHOLD_MS = 4 * 60 * 60 * 1000; + +/** + * Get the path to the shared state file for a given project. + * Uses the OS temp directory with a project-specific filename. + */ +export function getSharedStateFilePath(projectId: number): string { + const fileName = `.testplanit-reporter-${projectId}.json`; + return path.join(os.tmpdir(), fileName); +} + +/** + * Acquire a simple file-based lock using exclusive file creation. + * Retries with exponential backoff up to `maxAttempts` times. + */ +function acquireLock(lockPath: string, maxAttempts = 10): boolean { + for (let i = 0; i < maxAttempts; i++) { + try { + fs.writeFileSync(lockPath, process.pid.toString(), { flag: 'wx' }); + return true; + } catch { + const sleepMs = 50 * Math.pow(2, i) + Math.random() * 50; + const start = Date.now(); + while (Date.now() - start < sleepMs) { + // Busy wait + } + } + } + return false; +} + +/** + * Release a file-based lock. + */ +function releaseLock(lockPath: string): void { + try { + fs.unlinkSync(lockPath); + } catch { + // Ignore lock removal errors + } +} + +/** + * Execute a callback while holding the lock on the shared state file. + * Returns the callback's return value, or undefined if the lock could not be acquired. + */ +export function withLock(projectId: number, callback: (filePath: string) => T): T | undefined { + const filePath = getSharedStateFilePath(projectId); + const lockPath = `${filePath}.lock`; + + if (!acquireLock(lockPath)) { + return undefined; + } + + try { + return callback(filePath); + } finally { + releaseLock(lockPath); + } +} + +/** + * Read shared state from file. + * Returns null if file doesn't exist, is stale (>4 hours), or contains invalid JSON. + * + * Note: Does NOT check `activeWorkers === 0` — that logic differs between + * service-managed mode and legacy oneReport mode and is handled by the caller. + */ +export function readSharedState(projectId: number): SharedState | null { + const filePath = getSharedStateFilePath(projectId); + try { + if (!fs.existsSync(filePath)) { + return null; + } + const content = fs.readFileSync(filePath, 'utf-8'); + const state: SharedState = JSON.parse(content); + + // Check if state is stale + const createdAt = new Date(state.createdAt); + const staleThreshold = new Date(Date.now() - STALE_THRESHOLD_MS); + if (createdAt < staleThreshold) { + deleteSharedState(projectId); + return null; + } + + return state; + } catch { + return null; + } +} + +/** + * Write shared state to file atomically (uses lock). + */ +export function writeSharedState(projectId: number, state: SharedState): void { + withLock(projectId, (filePath) => { + fs.writeFileSync(filePath, JSON.stringify(state, null, 2)); + }); +} + +/** + * Write shared state to file, but only if no file already exists (first writer wins). + * If the file already exists, optionally updates the testSuiteId if not yet set. + * Returns the final state (either the written state or the existing state). + */ +export function writeSharedStateIfAbsent(projectId: number, state: SharedState): SharedState | undefined { + return withLock(projectId, (filePath) => { + if (fs.existsSync(filePath)) { + // File already exists — read existing state + const content = fs.readFileSync(filePath, 'utf-8'); + const existingState: SharedState = JSON.parse(content); + + // Only update if the testSuiteId is missing and we have one to add + if (!existingState.testSuiteId && state.testSuiteId) { + existingState.testSuiteId = state.testSuiteId; + fs.writeFileSync(filePath, JSON.stringify(existingState, null, 2)); + } + return existingState; + } + + // First writer — write the full state + fs.writeFileSync(filePath, JSON.stringify(state, null, 2)); + return state; + }); +} + +/** + * Delete shared state file. + */ +export function deleteSharedState(projectId: number): void { + const filePath = getSharedStateFilePath(projectId); + try { + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + } catch { + // Ignore deletion errors + } +} + +/** + * Atomically increment the active worker count in the shared state file. + */ +export function incrementWorkerCount(projectId: number): void { + withLock(projectId, (filePath) => { + if (fs.existsSync(filePath)) { + const content = fs.readFileSync(filePath, 'utf-8'); + const state: SharedState = JSON.parse(content); + state.activeWorkers = (state.activeWorkers || 0) + 1; + fs.writeFileSync(filePath, JSON.stringify(state, null, 2)); + } + }); +} + +/** + * Atomically decrement the active worker count in the shared state file. + * Returns true if this was the last worker (count reached 0). + */ +export function decrementWorkerCount(projectId: number): boolean { + const result = withLock(projectId, (filePath) => { + if (fs.existsSync(filePath)) { + const content = fs.readFileSync(filePath, 'utf-8'); + const state: SharedState = JSON.parse(content); + state.activeWorkers = Math.max(0, (state.activeWorkers || 1) - 1); + fs.writeFileSync(filePath, JSON.stringify(state, null, 2)); + return state.activeWorkers === 0; + } + return false; + }); + return result ?? false; +} diff --git a/packages/wdio-testplanit-reporter/src/types.ts b/packages/wdio-testplanit-reporter/src/types.ts index 22f7246b..b624f933 100644 --- a/packages/wdio-testplanit-reporter/src/types.ts +++ b/packages/wdio-testplanit-reporter/src/types.ts @@ -188,6 +188,141 @@ export interface TestPlanItReporterOptions extends Reporters.Options { oneReport?: boolean; } +/** + * Configuration options for the TestPlanIt WDIO launcher service. + * + * The service runs in the main WDIO process and manages the test run lifecycle: + * - Creates the test run before any workers start (onPrepare) + * - Completes the test run after all workers finish (onComplete) + * + * This ensures all spec files across all worker batches report to a single test run, + * regardless of `maxInstances` or execution order. + * + * @example + * ```javascript + * // wdio.conf.js + * import { TestPlanItService } from '@testplanit/wdio-reporter'; + * + * export const config = { + * services: [ + * [TestPlanItService, { + * domain: 'https://testplanit.example.com', + * apiToken: process.env.TESTPLANIT_API_TOKEN, + * projectId: 1, + * runName: 'Automated Tests - {date}', + * }] + * ], + * reporters: [ + * ['@testplanit/wdio-reporter', { + * domain: 'https://testplanit.example.com', + * apiToken: process.env.TESTPLANIT_API_TOKEN, + * projectId: 1, + * autoCreateTestCases: true, + * parentFolderId: 10, + * templateId: 1, + * }] + * ] + * } + * ``` + */ +export interface TestPlanItServiceOptions { + /** + * The base URL of your TestPlanIt instance + * @example 'https://testplanit.example.com' + */ + domain: string; + + /** + * API token for authentication + * Generate this from TestPlanIt: Settings > API Tokens + * Should start with 'tpi_' + */ + apiToken: string; + + /** + * The project ID in TestPlanIt where results will be reported + */ + projectId: number; + + /** + * Name for the test run. + * Supports placeholders: + * - {date} - Current date (YYYY-MM-DD) + * - {time} - Current time (HH:MM:SS) + * - {platform} - Platform/OS name + * + * Note: {browser}, {spec}, and {suite} are NOT available since the service + * runs before any workers start. They will be replaced with fallback values. + * + * @default 'Automated Tests - {date} {time}' + */ + runName?: string; + + /** + * Test run type to indicate the test framework being used. + * @default 'MOCHA' + */ + testRunType?: 'REGULAR' | 'JUNIT' | 'TESTNG' | 'XUNIT' | 'NUNIT' | 'MSTEST' | 'MOCHA' | 'CUCUMBER'; + + /** + * Configuration to associate with the test run (ID or name). + * If a string is provided, the system will look up the configuration by exact name match. + */ + configId?: number | string; + + /** + * Milestone to associate with the test run (ID or name). + * If a string is provided, the system will look up the milestone by exact name match. + */ + milestoneId?: number | string; + + /** + * Workflow state for the test run (ID or name). + * If a string is provided, the system will look up the state by exact name match. + */ + stateId?: number | string; + + /** + * Tags to apply to the test run (IDs or names). + * If strings are provided, the system will look up each tag by exact name match. + * Tags that don't exist will be created automatically. + */ + tagIds?: (number | string)[]; + + /** + * Whether to mark the test run as completed when all workers finish + * @default true + */ + completeRunOnFinish?: boolean; + + /** + * Automatically capture a screenshot when a test fails. + * The screenshot is taken via the WDIO `afterTest` hook and is + * automatically uploaded by the reporter when `uploadScreenshots` + * is enabled (the default). + * @default false + */ + captureScreenshots?: boolean; + + /** + * Request timeout in milliseconds + * @default 30000 + */ + timeout?: number; + + /** + * Number of retries for failed API requests + * @default 3 + */ + maxRetries?: number; + + /** + * Enable verbose logging for debugging + * @default false + */ + verbose?: boolean; +} + /** * Internal test result tracked by the reporter */