A Playwright reporter that publishes test results to Azure DevOps Test Plans.
Install the package:
npm install @alex_neo/playwright-azure-reporterAdd the reporter to your playwright.config.ts:
import { defineConfig } from '@playwright/test';
import type { AzureReporterOptions } from '@alex_neo/playwright-azure-reporter';
export default defineConfig({
reporter: [
['list'],
[
'@alex_neo/playwright-azure-reporter',
{
orgUrl: 'https://dev.azure.com/your-organization-name',
token: process.env.AZURE_TOKEN,
planId: 44,
projectName: 'SampleProject',
publishTestResultsMode: 'testRun',
} as AzureReporterOptions,
],
],
});Each test must reference an existing Azure DevOps test case ID. See Defining Test Case IDs for all supported formats.
You must link each test to an existing test case in Azure DevOps before running tests. There are three ways to do this.
Wrap test case IDs in square brackets inside the test name:
[1] Test name- single test case[1,2,3] Test name- multiple test cases[1, 2, 3] Test name [4] another [5][6] combined- mixed formats
test.describe('Test suite', () => {
test('[1] First Test', () => {
expect(true).toBe(true);
});
test('[10,11,12] Multiple test cases', () => {
expect(true).toBe(true);
});
});Use Playwright's tag syntax with the format @[ID]:
test.describe('Test suite', () => {
test('Test name', {
tag: ['@[1]', '@smoke', '@slow'],
}, () => {
expect(true).toBe(true);
});
});The @ prefix is required by Playwright. Non-ID tags (like @smoke) are ignored by the reporter.
Use Playwright's annotation API with testCaseIdZone: 'annotation':
test('Test case', {
annotation: { type: 'TestCase', description: '12345' },
}, () => {
expect(true).toBe(true);
});See Test Case ID Matching for advanced matching patterns with annotations.
| Option | Type | Description |
|---|---|---|
orgUrl |
string |
Full URL for your organization. Example: https://dev.azure.com/your-organization-name |
projectName |
string |
Name of your Azure DevOps project |
planId |
number |
ID of the test plan (from the test plan URL: planId=4) |
Note: Some APIs (e.g. ProfileApi) must be hit at the deployment level:
https://vssps.dev.azure.com/{yourorgname}
| Option | Type | Default | Description |
|---|---|---|---|
isDisabled |
boolean |
false |
Disable the reporter entirely |
environment |
string |
undefined |
Prefix for test run names (e.g. 'QA') |
logging |
boolean |
false |
Enable debug logging |
testRunTitle |
string |
'Playwright Test Run' |
Title for newly created test runs |
testRunConfig |
object |
undefined |
Extra data passed when creating a test run. See API docs |
The reporter supports three authentication methods:
{
token: 'your-personal-access-token',
authType: 'pat' // Optional, this is the default
}{
token: 'your-access-token',
authType: 'accessToken'
}{
token: 'not-used', // Token field is required but ignored for managedIdentity
authType: 'managedIdentity',
applicationIdURI: '499b84ac-1321-427f-aa17-267ca6975798/.default'
}When to use each:
- PAT: Personal Access Token from Azure DevOps. Suitable for most use cases.
- Access Token: OAuth access token. Suitable for CI/CD environments with token-based authentication.
- Managed Identity: Azure environments with managed identity. Uses
DefaultAzureCredentialsupporting Azure CLI, managed identity, environment variables, etc.
For more examples, see Authentication Examples.
| Scope | Access | Purpose |
|---|---|---|
| Test Management | Read & Write | Creating/updating test runs, publishing results, uploading attachments |
| Work Items | Read & Write | Marking test cases as automated (only if autoMarkTestCasesAsAutomated is enabled) |
| Project and Team | Read | Accessing project information |
Alternatively, use a token with Full access scope.
Note: If your token lacks Work Items write permissions but
autoMarkTestCasesAsAutomatedis enabled, the reporter will log errors for automation updates but continue publishing test results normally.
Control how and when test results are sent to Azure DevOps with publishTestResultsMode:
| Mode | Default | Description |
|---|---|---|
'testResult' |
Yes | Results published after each test completes, in parallel with test execution |
'testRun' |
Results batched and published at the end of the test run | |
'testRunADO' |
Updates existing results in an Azure DevOps test run (requires isExistingTestRun: true and testRunId) |
Note: In
testRunmode, if the same test case ID appears in multiple tests, only the last result is kept.
To publish results to an existing test run instead of creating a new one:
{
publishTestResultsMode: 'testRunADO',
isExistingTestRun: true,
testRunId: 12345, // or set AZURE_PW_TEST_RUN_ID env variable
}isExistingTestRun[boolean] - Publish to an existing test run without creating/completing it. Default:false.testRunId[number] - ID of the existing test run. Can also be set viaAZURE_PW_TEST_RUN_IDenv variable. Reporter options take precedence over the env variable.
Note: When using
isExistingTestRun, the test run is not completed automatically. You must complete it manually.
Control how retry attempts are published with publishRetryResults:
| Mode | Description |
|---|---|
'all' |
Every retry attempt is published as a separate test result (default) |
'last' |
Only the final attempt is published. Intermediate failures are skipped |
{
publishRetryResults: 'last',
// works with Playwright's retries setting
}| Option | Type | Default | Description |
|---|---|---|---|
uploadAttachments |
boolean |
false |
Upload attachments (screenshots/video) after each test |
attachmentsType |
(string|RegExp)[] |
['screenshot'] |
Filter which attachments to upload by type or name pattern |
uploadLogs |
boolean |
false |
Upload stdout/stderr logs (independent of uploadAttachments) |
A callback to map tests to specific test configurations (e.g. by browser):
import { TestCase } from '@playwright/test/reporter';
import { TestPoint } from 'azure-devops-node-api/interfaces/TestInterfaces';
{
testPointMapper: async (testCase: TestCase, testPoints: TestPoint[]) => {
switch (testCase.parent.project()?.use.browserName) {
case 'chromium':
return testPoints.filter((tp) => tp.configuration.id === '3');
case 'firefox':
return testPoints.filter((tp) => tp.configuration.id === '4');
case 'webkit':
return testPoints.filter((tp) => tp.configuration.id === '5');
default:
throw new Error('invalid test configuration!');
}
},
testRunConfig: {
// Get configuration IDs from:
// https://dev.azure.com/{organization}/{project}/_apis/test/configurations
configurationIds: [3, 4, 5],
},
}Important: Define
testRunConfig.configurationIdsand/ortestPointMapper, otherwise results are published for all configurations.
Restricts test point resolution to a specific suite. Useful when different suites (e.g. smoke, integration) contain the same test case IDs. Can also be set via AZURE_PW_ROOT_SUITE_ID env variable. Reporter options take precedence.
Automation Tests
- Smoke Tests (suiteId: 5)
- Test 1 (caseId: 1)
- Test 2 (caseId: 2)
- Integration Tests (suiteId: 6)
- Test 1 (caseId: 1)
- Test 2 (caseId: 2)
Without rootSuiteId, running smoke tests would also match test points in Integration Tests. Set rootSuiteId: 5 to scope results to Smoke Tests only.
A string, RegExp, or array of either, used to extract test case IDs. Default: /\[([\d,\s]+)\]/
Examples with test titles:
| Test Title | Matcher | Extracted IDs |
|---|---|---|
Test case @tag1=123 |
/@tag1=(\d+)/ |
['123'] |
Test case @TC=123 [@TC=456] |
/@TC=(\d+)/ |
['123', '456'] |
Test case test123 TEST456 |
[/[a-z]+(\d+)/, /[A-Z]+(\d+)/] |
['123', '456'] |
If an invalid matcher is provided (e.g. a number), an error is thrown: "Invalid testCaseIdMatcher. Must be a string or RegExp.".
Specifies where to look for test case IDs: 'title' (default) or 'annotation'.
'title'- Extracts IDs from the test title and tags'annotation'- Extracts IDs from test annotations only
Important: When using
testCaseIdZone: 'annotation', you must also definetestCaseIdMatcher.
When using annotation zone, the matcher works in two steps:
- First matcher matches the annotation type (e.g.
/(TestCase)/) - Subsequent matchers extract IDs from the annotation description
Annotation examples:
Simple description:
// testCaseIdZone: 'annotation', testCaseIdMatcher: /(TestCase)/
test('Test case', {
annotation: { type: 'TestCase', description: '12345' },
}, () => { /* ... */ });
// Extracted: ['12345']Azure DevOps URLs:
// testCaseIdZone: 'annotation', testCaseIdMatcher: [/(Test Case)/, /\/(\d+)/]
test('Test case', {
annotation: {
type: 'Test Case',
description: 'https://dev.azure.com/org/project/_workitems/edit/12345, https://dev.azure.com/org/project/_workitems/edit/54321',
},
}, () => { /* ... */ });
// Extracted: ['12345', '54321']Bracketed format:
// testCaseIdZone: 'annotation', testCaseIdMatcher: [/(Test Case)/, /\[([\d,\s]+)\]/]
test('Test case', {
annotation: { type: 'Test Case', description: '[12345, 67890]' },
}, () => { /* ... */ });
// Extracted: ['12345', '67890']Generate a report of test cases that don't match the test plan. Helps identify missing, misconfigured, or unassigned test cases.
{
testCaseSummary: {
enabled: true,
outputPath: './test-case-summary.md',
consoleOutput: true,
publishToRun: true,
},
}| Sub-option | Type | Default | Description |
|---|---|---|---|
enabled |
boolean |
false |
Enable summary generation |
outputPath |
string |
undefined |
File path for the summary report |
consoleOutput |
boolean |
true |
Print summary to console |
publishToRun |
boolean |
false |
Upload summary as a Markdown attachment to the ADO test run |
Output channels work independently - enable any combination:
consoleOutput |
outputPath |
publishToRun |
Result |
|---|---|---|---|
| true | - | - | Console only |
| true | set | - | Console + file |
| - | - | true | ADO attachment only |
| true | set | true | All three channels |
When all test cases match, a success message is logged instead.
Note: When
publishToRun: true, the attachment is only uploaded if unmatched test points exist. Key summary lines are always logged regardless of theloggingsetting to ensure CI visibility.
Example output:
# Test Case Summary
Found 1 test(s) with test case IDs that don't match the test plan:
## Tests with No Matching Test Points (1)
- **[777] Test with file output**
- File: tests/example.spec.ts:3
- Test Case IDs: [777]
## Recommendations
- Verify test case IDs exist in Azure DevOps test plan 4
- Check that test cases are assigned to configurations: [10, 20] (Firefox, Safari)
- Ensure test cases are included in the test plan suite structureAutomatically update Azure DevOps test case work items to reflect their automation status when tests are executed.
{
autoMarkTestCasesAsAutomated: {
enabled: true,
updateAutomatedTestName: true,
updateAutomatedTestStorage: true,
},
}| Sub-option | Type | Default | Description |
|---|---|---|---|
enabled |
boolean |
false |
Enable automatic marking |
updateAutomatedTestName |
boolean |
true |
Set AutomatedTestName field |
updateAutomatedTestStorage |
boolean |
true |
Set AutomatedTestStorage field |
automatedTestNameFormat |
string |
'title' |
'title' or 'titleWithParent' (includes parent suite name) |
automatedTestStoragePath |
boolean | function |
false |
false: filename only, true: full path, function: custom callback |
automatedTestType |
function |
undefined |
Callback to set AutomatedTestType (e.g. 'Unit Test', 'E2E Test') |
What it does:
- Checks each test case's automation status in Azure DevOps
- If "Not Automated": sets
AutomationStatusto "Automated", updates name/storage/type fields, generates a newAutomatedTestIdGUID - If already "Automated": optionally updates name and storage fields if they differ
Advanced example:
{
autoMarkTestCasesAsAutomated: {
enabled: true,
updateAutomatedTestName: true,
updateAutomatedTestStorage: true,
automatedTestNameFormat: 'titleWithParent',
automatedTestStoragePath: (testCase) => {
// Use relative path from project root
const parts = testCase.location.file.split('/');
const idx = parts.indexOf('my-project');
return idx >= 0 ? parts.slice(idx + 1).join('/') : testCase.location.file;
},
automatedTestType: (testCase) => {
if (testCase.location.file.includes('/e2e/')) return 'End-to-End Test';
if (testCase.location.file.includes('/integration/')) return 'Integration Test';
return 'Functional Test';
},
},
}Tip: Use
automatedTestNameFormat: 'titleWithParent'to prevent name collisions across suites. UseautomatedTestStoragePathcallback to avoid exposing absolute paths.
Note: Requires Work Items write permissions. Works in all publishing modes (
testResult,testRun,testRunADO).
| Variable | Description |
|---|---|
AZURE_PW_TEST_RUN_ID |
Set automatically after the test run is created. Available as process.env.AZURE_PW_TEST_RUN_ID. In testResult mode it's set at the start; in testRun mode at the end; in testRunADO mode it contains the existing run ID. |
AZURE_PW_ROOT_SUITE_ID |
Alternative to the rootSuiteId option |
AZUREPWDEBUG |
Enable debug logging: '1' = enabled, '0' = disabled (default) |
- script: npx playwright test
displayName: 'Run Playwright tests'
name: 'playwright'
env:
CI: 'true'
AZUREPWDEBUG: '1'
# Access the test run ID from subsequent steps (since v1.10.0)
- script: echo $(playwright.AZURE_PW_TEST_RUN_ID)
displayName: 'Print test run id'For detailed information about stable releases, beta releases, and version management, see RELEASES.md.