TestPlanIt Reporter Reporter
@testplanit/wdio-reporter is a 3rd party package, for more information please see GitHub | npm
WebdriverIO reporter and service for TestPlanIt - report test results directly to your TestPlanIt instance.
This package includes:
- Reporter - Tracks test execution in worker processes and reports results to TestPlanIt
- Service - Manages the test run lifecycle in the main process, ensuring all workers report to a single test run
Installation
npm install @testplanit/wdio-reporter
# or
pnpm add @testplanit/wdio-reporter
# or
yarn add @testplanit/wdio-reporter
Quick Start
1. Generate an API Token
- Log into your TestPlanIt instance
- Go to Settings > API Tokens
- Click Generate New Token
- Copy the token (it starts with
tpi_)
2. Configure the Reporter and Service
Add both the service and reporter to your wdio.conf.js or wdio.conf.ts:
// 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} {time}',
captureScreenshots: true,
}]
],
reporters: [
['@testplanit/wdio-reporter', {
domain: 'https://testplanit.example.com',
apiToken: process.env.TESTPLANIT_API_TOKEN,
projectId: 1,
}]
],
// ... rest of config
}
Note: The service is recommended when running with
maxInstances > 1. It creates a single test run before workers start, eliminating race conditions. Without the service, the reporter can still manage test runs on its own using file-based coordination (oneReport: true).
Service vs Reporter
| Aspect | Service | Reporter |
|---|---|---|
| Process | Main WDIO process | Each worker process |
| Timing | Runs once before/after all workers | Runs in each worker |
| Test run creation | Creates in onPrepare | Fallback: creates if no service |
| Result reporting | - | Reports each test result |
| Screenshot capture | Optional (captureScreenshots) | - |
| Screenshot upload | - | Uploads in onRunnerEnd |
| Run completion | Completes in onComplete | Skips if service-managed |
Sharing One Run Across Sharded, Parallel or Retried Executions
The service and oneReport both collapse a single WebdriverIO execution into one
test run. Neither spans separate executions: their shared state lives in a file
in the OS temp directory, so it cannot reach a second CI agent, and it is reset
once a run's workers have all finished. A suite split into shards, spread across
agents, or rerun in retry waves therefore produces one run per invocation — often
several runs with the same name.
To collect all of them in a single run, create the run in the pipeline and let every invocation attach to it:
RUN_ID=$(testplanit run create --project 9 --name "Web Regression Tests - DEV #984" --type MOCHA)
export TESTPLANIT_RUN_ID="$RUN_ID"
# Every shard, agent and retry wave attaches to $TESTPLANIT_RUN_ID
pnpm web:bs --spec ./test/specs/shard-1/**
pnpm web:bs --spec ./test/specs/shard-2/**
# ...deferred retries, other agents...
testplanit run complete --id "$RUN_ID"
testplanit is the @testplanit/cli
package (npm i -g @testplanit/cli). It reads TESTPLANIT_URL and
TESTPLANIT_API_TOKEN, or credentials stored once with
testplanit config set --url ... --token ...; run testplanit run --help for
the full list of options.
Nothing else in the config has to change. Both the service and the reporter read
TESTPLANIT_RUN_ID, so the recommended service + reporter setup works as-is —
the service reports into the pinned run instead of creating one in onPrepare,
and leaves it open in onComplete.
What Changes When a Run Is Externally Managed
A run supplied through TESTPLANIT_RUN_ID or the testRunId option is
externally managed. For such a run the reporter:
- Never creates a run. If the run cannot be read, the failure is logged and results are still attached to the given ID rather than to a replacement run.
- Never completes it, regardless of
completeRunOnFinish— the pipeline closes it withtestplanit run completeonce every invocation has finished. A shard that completed the run would push the ones behind it onto a new run. - Never discards it. The recovery paths that start a fresh run when the shared state is exhausted, completed or deleted do not apply.
- Never changes its settings.
configId,milestoneId,stateIdandtagIdsare ignored, since those belong to whoever created the run. Case creation options (parentFolderId,templateId, and the rest) still apply.
The service behaves the same way, with one addition: runLinks and runMetadata
describe the run as a whole, so it applies them only to runs it created —
otherwise every shard would duplicate the links and the last one would overwrite
the metadata. runAttachments are per-execution artifacts and still upload from
each shard.
Suites Within the Run
Each execution creates its own JUnit suite under the shared run, named
{suite} - {browser}/{platform} - {spec} by default so shards are
distinguishable. Results roll up at the run level across every suite. Override
the naming with testSuiteName, which accepts the same placeholders as
runName. The service's launcher process runs before any browser exists, so its
testSuiteName also resolves {env:VAR} — name shards from the pipeline, for
example testSuiteName: 'Shard {env:SHARD_ID}'.
Resolution Order
The first of these that yields a run wins:
testRunIdgiven as a numberTESTPLANIT_RUN_ID(ignored unless it is a positive integer, so an unresolved shell variable falls through instead of failing)testRunIdgiven as a name, looked up by exact match- the
oneReportshared-state file - a new run
Options 1–3 are externally managed. With none of them set, behaviour is
unchanged: oneReport still dedupes workers within one execution, and the run is
created and completed as before.
Linking Test Cases
Embed TestPlanIt case IDs in your test titles using brackets (configurable via caseIdPattern):
describe('Authentication', () => {
it('[12345] should login with valid credentials', async () => {
// This test will be linked to case ID 12345
});
it('[12346] [12347] should show error for invalid password', async () => {
// This test will be linked to multiple cases: 12346 and 12347
});
it('should redirect to dashboard after login', async () => {
// No case ID - will be skipped unless autoCreateTestCases is enabled
});
});
Custom Case ID Patterns
The caseIdPattern option accepts a regex with a capturing group for the numeric ID:
// Default: brackets - "[12345] should work"
caseIdPattern: /\[(\d+)\]/g
// C-prefix: "C12345 should work"
caseIdPattern: /C(\d+)/g
// TC- prefix: "TC-12345 should work"
caseIdPattern: /TC-(\d+)/g
// JIRA-style: "TEST-12345 should work"
caseIdPattern: /TEST-(\d+)/g
Matching Cases by a Custom Field
caseIdPattern treats the number it captures as a literal TestPlanIt case ID. If your titles instead carry a legacy external identifier — e.g. an ID left over from a previous test manager — that was backfilled onto your migrated cases as a custom field, use matchByCustomField to resolve the existing case by that field's value:
reporters: [
['@testplanit/wdio-reporter', {
domain: 'https://testplanit.example.com',
apiToken: process.env.TESTPLANIT_API_TOKEN,
projectId: 1,
matchByCustomField: {
fieldName: 'External ID', // custom field display name
// idPattern: /^(\d+)/ // default: bare leading number in the title
},
// Optional fallback for titles with no match:
autoCreateTestCases: true,
parentFolderId: 10,
templateId: 1,
}]
]
For a test titled "89434 Verify 'Relevance' is the default sort order", the reporter extracts 89434, finds the case whose External ID field equals 89434, and attaches the result directly to that case — regardless of its source (typically MANUAL). No new case or link is created. If that case isn't already flagged automated, the reporter flips it to automated (skipping the write when it already is).
This strategy is opt-in and runs before name/create resolution. On no match — or if the field doesn't exist on the project — it falls through to the standard flow without error. It is independent of caseIdPattern; an explicit caseIdPattern match still takes precedence.
Reporter Options
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
domain | string | Yes | - | Base URL of your TestPlanIt instance |
apiToken | string | Yes | - | API token for authentication |
projectId | number | Yes | - | Project ID to report results to |
testRunId | number | string | No | $TESTPLANIT_RUN_ID | Existing test run ID or name to append results to. A run supplied here is never created or completed by the reporter — see Sharing One Run Across Sharded, Parallel or Retried Executions |
runName | string | No | '{suite} - {date} {time}' | Name for new test runs. Supports placeholders: {date}, {time}, {browser}, {platform}, {spec}, {suite} |
testSuiteName | string | No | runName | Name of the JUnit suite created for this invocation. Same placeholders as runName. Defaults to '{suite} - {browser}/{platform} - {spec}' when the run is externally managed |
testRunType | string | No | Auto-detected | Test framework type: 'REGULAR', 'MOCHA', 'CUCUMBER', etc. Auto-detected from WDIO config |
configId | number | string | No | - | Configuration ID or name for the test run |
milestoneId | number | string | No | - | Milestone ID or name for the test run |
stateId | number | string | No | - | Workflow state ID or name for the test run |
tagIds | (number | string)[] | No | - | Tags to apply (IDs or names). Non-existent tags are created automatically |
caseIdPattern | RegExp | string | No | /\[(\d+)\]/g | Regex to extract case IDs from test titles. Must include a capturing group |
matchByCustomField | { fieldName: string; idPattern?: RegExp | string } | No | - | Resolve an existing case by a custom field value parsed from the title (default idPattern: /^(\d+)/), before the name/create fallback. See Matching Cases by a Custom Field |
autoCreateTestCases | boolean | No | false | Auto-create test cases matched by suite name + test title |
captureSteps | boolean | No | true | Capture a Cucumber scenario's Given/When/Then as the case's Steps. Cucumber only; silent no-op for Mocha/Jasmine |
overwriteSteps | boolean | No | false | Replace an existing Cucumber case's steps on each run (destructive: discards manual edits). Cucumber only |
createFolderHierarchy | boolean | No | false | Create nested folders based on suite structure. Requires autoCreateTestCases and parentFolderId |
parentFolderId | number | string | No | - | Parent folder for auto-created cases (ID or name) |
templateId | number | string | No | - | Template for auto-created cases (ID or name) |
uploadScreenshots | boolean | No | true | Upload intercepted screenshots |
includeStackTrace | boolean | No | true | Include stack traces in results |
excludeSkipped | boolean | No | false | Don't report skipped tests to TestPlanIt |
completeRunOnFinish | boolean | No | true | Mark test run as completed when done |
oneReport | boolean | No | true | Combine parallel workers from the same spec file into a single test run. Does not persist across spec file batches — use the service for that |
timeout | number | No | 30000 | API request timeout in ms |
maxRetries | number | No | 3 | Number of retries for failed requests |
verbose | boolean | No | false | Enable verbose logging |
Tip: Options like
configId,milestoneId,stateId,parentFolderId, andtemplateIdaccept either numeric IDs or string names. When a string is provided, the system looks up the resource by exact name match.