Playwright JS SDK
The Evinced Playwright JS SDK integrates with new or existing Playwright tests to automatically detect accessibility issues. By adding a few lines of code to your Playwright project, you can begin analyzing all the web pages and DOM changes to provide a dynamic view of how your site can become more accessible. As a result of the test, a rich and comprehensive report is generated to easily track issues to resolution.
Interested in seeing this in action? Contact us to get started!
Prerequisites
- Playwright version 1.25 or higher
Get started
Installation
To install Playwright JS SDK you will need either a file provided by Evinced Support or access to a remote repository that provides it. If you have neither, contact us to get started.
Installation with a locally provided file
With a local copy of the Playwright JS SDK gzipped tar package (.tgz extension), install it in your project using NPM or Node package manager of your choice:
1# Using NPM2npm install -D <path to js-playwright-sdk-<version>.tgz file>
Installation from a remote repository
Evinced Customers have the option of accessing Playwright JS SDK from a remote repository - the Evinced JFrog Artifactory - to keep their SDK version up-to-date and to share the SDK internally at their organization.
When access is enabled, Playwright JS SDK is available at
https://evinced.jfrog.io/artifactory/restricted-npm/%40evinced/js-playwright-sdk.
Installation using NPM:
1npm install @evinced/js-playwright-sdk
AI Skills
The @evinced/js-playwright-sdk package ships with built-in AI agent skills that guide your AI assistant through integrating the Evinced SDK — from initial setup and writing tests to configuring rules and generating reports.
To activate AI Skills, add the following to your project's AGENTS.md file (create it at the root of your project if it does not exist):
1## Context23Before working on any accessibility testing task, always read:45`node_modules/@evinced/js-playwright-sdk/evinced-ai/entry.mdc`67The entry.mdc file contains:8- Integration rules for the Evinced Playwright JS SDK9- Skills for setup, test writing, configuration, reporting, and logging10- Links to detailed documentation for each task type1112## When to use1314When the user asks about accessibility testing, WCAG compliance, ARIA attributes, Evinced SDK usage, or accessibility reports or scans, read the entry point file listed under Context.1516## Capabilities1718- Set up the Evinced SDK in Playwright projects19- Write accessibility tests using `evAnalyze`, `evStart`, `evStop`20- Configure accessibility rules and scopes21- Generate HTML, JSON, SARIF, or CSV reports22- Configure the SDK — proxy, screenshots, iframes, and `evConfig.json`23- Enable and tune SDK logging and log levels24- Control SDK toggles — kill switch, analytics opt-out, and mock engine25- Integrate accessibility checks into CI/CD pipelines
Note for monorepos: with pnpm, or yarn/npm workspaces, the package may be hoisted to the workspace root. If the path above does not exist, look for evinced-ai/entry.mdc under the workspace root's node_modules instead.
This works with any AI assistant that reads project context files (Cursor, Claude, Copilot, Windsurf, Gemini, and others).
Try asking your AI assistant:
- "Set up the Evinced SDK in my project"
- "Add accessibility checks to my existing Playwright test"
- "Scope the scan to only the navigation bar"
Authentication
To launch Playwright JS SDK, you need to have a Service ID and an API Key.
Where to find your Evinced SDK credentials
These credentials are available via the Evinced Product Hub in the “Automation for Web” or “Automation for Mobile” product areas. Click the “Get SDK” button to see the Service Account ID and API Key at the bottom of the page.
Authenticate for Offline Testing
There are two methods to provide the token: online mode and offline mode. Online mode contacts the Evinced Licensing Server. Offline mode assumes that an Evinced employee has supplied a JSON Web Token (JWT). If an offline token is required, please reach out to your account team or support@evinced.com.
Please set credentials in environment variables and reference the environment variables in code.
1# Online mode2export EVINCED_SERVICE_ID=<serviceId>3export EVINCED_API_KEY=<apiKey>45# Offline mode, when a JWT has been provided by Evinced6export EVINCED_SERVICE_ID=<serviceId>7export EVINCED_AUTH_TOKEN=<token>
Setting credentials, an example:
Authenticate Playwright JS SDK in Playwright’s globalSetup as shown below
for the typical case using CommonJS or TypeScript with CommonJS.
1// global.setup.js2const { setCredentials } = require("@evinced/js-playwright-sdk");34async function globalSetup(config) {5 try {6 await setCredentials({7 serviceId: process.env.EVINCED_SERVICE_ID,8 secret: process.env.EVINCED_API_KEY,9 });10 } catch (error) {11 throw new Error("Evinced SDK authorization failure.");12 }13}14module.exports = globalSetup;
Set the full path to the module in the Playwright configuration:
1// playwright.config.js or playwright.config.ts2globalSetup: require.resolve("./global.setup.js");
Authenticate Using ECMAScript Modules
If package.json declares "type": "module" or the file extension is ".mjs", use ECMAScript module syntax:
1// playwright.config.mjs2// use pathToFileURL to resolve full path:3 globalSetup: pathToFileURL('./global.setup.js').pathname,
1// global.settings.mjs2import evSdk from "@evinced/js-playwright-sdk";3const { setCredentials } = evSdk;45async function globalSetup(config) {6 try {7 await setCredentials({8 serviceId: process.env.EVINCED_SERVICE_ID,9 secret: process.env.EVINCED_API_KEY,10 });11 } catch (error) {12 throw new Error("Evinced SDK authorization failure.");13 }14}15export default globalSetup;
Authenticate for Offline Testing
If Evinced has provided an JSON Web Token (JWT) for offline testing, invoke setOfflineCredentials to authenticate rather than setCredentials.
1// global.setup.js2const { setOfflineCredentials } = require("@evinced/js-playwright-sdk");34async function globalSetup(config) {5 try {6 await setOfflineCredentials({7 serviceId: process.env.EVINCED_SERVICE_ID,8 token: process.env.EVINCED_AUTH_TOKEN,9 });10 } catch (error) {11 throw new Error("Evinced SDK authorization failure.");12 }13}
Your First Test
SDK Initialization
To use Playwright JS SDK, you first need to authenticate. Please refer to Authentication for details.
Place evConfig.yaml, evConfig.yml, or structured evConfig.json at the project root. Authenticate in Playwright's globalSetup before running tests (see the authentication section).
Recommended (evConfig.yaml):
1switchOn: true2scan:3 iframes: true4 rootSelector: '#main'5report:6 format: html7 outputDir: ./evincedReports8 fileName: aggregatedReport9 aggregate: true10platform:11 upload:12 enabled: false
Recommended (structured evConfig.json):
1{2 "switchOn": true,3 "scan": {4 "iframes": true,5 "rootSelector": "#main"6 },7 "report": {8 "format": "html",9 "outputDir": "./evincedReports",10 "fileName": "aggregatedReport",11 "aggregate": true12 },13 "platform": {14 "upload": {15 "enabled": false16 }17 }18}
Deprecated (legacy flat keys in evConfig.json):
1{2 "switchOn": true,3 "includeIframes": true,4 "rootSelector": "#main",5 "enableScreenshots": false,6 "reporterOptions": {7 "reportFormat": "html",8 "fileName": "aggregatedReport.html",9 "outputDir": "./evincedReports"10 },11 "uploadToPlatformOptions": {12 "enableUploadToPlatform": false13 }14}
Add Evinced Accessibility Checks (Single Run Mode)
This is a simple example of how to add an Evinced accessibility scan to a test. Please note the inline comments that give detail on each test step.
This test scans https://demo.evinced.com, then generates an Evinced report. The test passes if the report is generated.
1// Use import in ECMAScript module2// (.mjs extension or type: module in package.json)3import { test, expect } from "@playwright/test";4import { existsSync } from "node:fs";5import { EvincedSDK } from "@evinced/js-playwright-sdk";67test.describe("Evinced evAnalyze", () => {8 test("Single test run using evAnalyze", async ({ page }) => {9 const evReport = "./test-results/evAnalyze.html";10 const evincedService = new EvincedSDK(page);11 await page.goto("https://demo.evinced.com/");12 const issues = await evincedService.evAnalyze();13 await evincedService.evSaveFile(issues, "html", evReport);14 expect(existsSync(evReport)).toBeTruthy();15 });16});17
Add Evinced Accessibility Checks (Continuous Mode)
This is an example of how to add a continuous Evinced accessibility scan to a test. Using the
evStart() and evStop() methods, the Evinced engine will continually scan in the background capturing all DOM changes and page navigation as the test is executed. This will capture all accessibility issues as clicking on drop-downs or similar interactions reveals more of the page. The advantage of continuous mode is that no interaction with the actual test code is needed.
In this test, an Evinced report is generated for https://demo.evinced.com/ after some scripted interaction. The test passes if the report is generated.
1// Use require in CommonJS module (default)2const { test, expect } = require('@playwright/test')3const { existsSync } = require('node:fs')4const { EvincedSDK } = require('@evinced/js-playwright-sdk')56// Use import in ECMAScript module7import { test, expect } from '@playwright/test'8import { existsSync } from 'node:fs'9import evExport from '@evinced/js-playwright-sdk'10const { EvincedSDK } = evExport1112// Test is the same with either module system13test.describe('Evinced Demo Page', () => {14 test('Continuous Test', async ({ page }) => {15 const evReport = './test-results/continuous.html'16 const evincedService = new EvincedSDK(page)17 await evincedService.evStart()1819 await page.goto('https://demo.evinced.com/')2021 const BASE_FORM_SELECTOR =22 '#gatsby-focus-wrapper > main > div.wrapper-banner > div.filter-container'23 const SELECT_HOME_DROPDOWN = `${BASE_FORM_SELECTOR} > div:nth-child(1) > div > div.dropdown.line`24 const SELECT_WHERE_DROPDOWN = `${BASE_FORM_SELECTOR} > div:nth-child(2) > div > div.dropdown.line`25 const TINY_HOME_OPTION = `${BASE_FORM_SELECTOR} > div:nth-child(1) > div > ul > li:nth-child(2)`26 const EAST_COST_OPTION = `${BASE_FORM_SELECTOR} > div:nth-child(2) > div > ul > li:nth-child(3)`2728 await page.locator(SELECT_HOME_DROPDOWN).click()29 await page.locator(TINY_HOME_OPTION).click()30 await page.locator(SELECT_WHERE_DROPDOWN).click()31 await page.locator(EAST_COST_OPTION).click()3233 const issues = await evincedService.evStop()34 await evincedService.evSaveFile(issues, 'html', evReport)35 expect(existsSync(evReport)).toBeTruthy()36 })37})
API
EvincedSDK
Prepares the Evinced object for use in the project.
The specific SDK initialization is not needed. Just import EvincedSDK in your test files.
1// in CommonJS module context2const { EvincedSDK } = require('@evinced/js-playwright-sdk');34// In ECMAScript module context5import sdk from '@evinced/js-playwright-sdk'6const { EvincedSDK } = sdk
Refer to Configuration to see examples of initializing with options.
evAnalyze(options)
Scans the current page and returns a list of accessibility issues. This is the recommended method for static page analysis.
Note: This method is not supported if evStart() is already running.
1const evincedService = new EvincedSDK(page);2await page.goto("https://demo.evinced.com/");3const issues = await evincedService.evAnalyze();
Returns Promise<Issue[]>.
The returned report object contains a list of accessibility issues.
For more information regarding reports as well as the report object itself, please refer to our detailed Web Reports page.
evStart(options)
Continually watches for DOM mutations and page navigation, recording accessibility issues
until the evStop() method is called. This method is recommended for dynamic page flows.
1const evincedService = new EvincedSDK(page);2await evincedService.evStart();3await page.goto('https://demo.evinced.com/');4const issues = await evincedService.evStop();
Returns Promise<void>.
evStop(options)
Stops the issue-gathering process started by evStart().
1const evincedService = new EvincedSDK(page);2await evincedService.evStart();3await page.goto('https://demo.evinced.com/');4const issues = await evincedService.evStop();
Returns Promise<Issue[]>.
The returned report object includes all accessibility issues detected between
the evStart() and evStop() method calls.
For more information regarding reports as well as the report object itself, please refer to our detailed Web Reports page.
evSaveFile(issues, format, destination)
Saves issues in a file with the specified format and location.
Supported formats are json, html, sarif, and csv.
Find detailed information in the Web Reports page.
1const evincedService = new EvincedSDK(page);2await page.goto("https://demo.evinced.com/");3const issues = await evincedService.evAnalyze();4evincedService.evSaveFile(issues, 'html', 'test-results/evinced-report.html');5evincedService.evSaveFile(issues, 'csv', 'test-results/evinced-report.csv');6evincedService.evSaveFile(issues, 'json', 'test-results/evinced-report.json');7evincedService.evSaveFile(issues, 'sarif', 'test-results/evinced-report.sarif.json');
Returns Promise<void>.
Aggregated Report
The aggregated report feature allows you to have a general aggregated report for
the whole run (not only for one test or suite). This report will contain all the
issues found by the tests where evStart() and
evStop() commands were called. It is still possible to use the
evSaveFile() command in any place of your code along with this
Aggregated Report feature.
For using the aggregated report feature, update your config file (evConfig.yaml, evConfig.yml, or structured evConfig.json at the project root).
Single Format Configuration
Recommended (evConfig.yaml):
1report:2 format: html # Sets a desired format for the report. Available options: html, json, csv, sarif.3 fileName: aggregatedReportForRun # Base name for the report file (extension added from format).4 outputDir: ./evincedReports # Directory for the final aggregated report.5 tmpDir: ./evincedReports/tmp # Per-spec tmp files (optional; OS temp dir by default).6 aggregate: true # Generate aggregated report at end of run (default: true).
Recommended (structured evConfig.json):
1{2 "report": {3 "format": "html",4 "fileName": "aggregatedReportForRun",5 "outputDir": "./evincedReports",6 "tmpDir": "./evincedReports/tmp",7 "aggregate": true8 }9}
Property comments match the YAML example above (format, fileName, outputDir, tmpDir, aggregate).
Deprecated (legacy flat reporterOptions in evConfig.json):
1{2 "reporterOptions": {3 "reportFormat": "html", // Sets a desired format for the report. Available options are: html, json, csv, and sarif. Mandatory.4 "fileName": "aggregatedReportForRun.html", // Specifies a name of the final report. Mandatory.5 "outputDir": "./evincedReports", // Specifies a path to the final aggregated report file. Mandatory.6 "tmpDir": "./evincedReports/tmp" // A directory for storing Evinced tmp files. 'os.tmpdir()' by default. Optional.7 }8}
Multiple Formats Configuration
Recommended (evConfig.yaml):
1report:2 format: [html, json, csv, sarif] # Array generates one file per format.3 fileName: aggregatedReport # Base name; extensions added per format.4 outputDir: ./evincedReports5 tmpDir: ./evincedReports/tmp6 aggregate: true
Recommended (structured evConfig.json):
1{2 "report": {3 "format": ["html", "json", "csv", "sarif"],4 "fileName": "aggregatedReport",5 "outputDir": "./evincedReports",6 "tmpDir": "./evincedReports/tmp",7 "aggregate": true8 }9}
This configuration will generate:
aggregatedReport.htmlaggregatedReport.jsonaggregatedReport.csvaggregatedReport.sarif
Deprecated (legacy flat reporterOptions in evConfig.json):
1{2 "reporterOptions": {3 "reportFormat": ["html", "json", "csv", "sarif"], // Array of formats to generate. Available options are: html, json, csv, and sarif. Mandatory.4 "fileName": "aggregatedReport", // Base name for reports. Extensions will be auto-generated (e.g., aggregatedReport.html, aggregatedReport.json). Mandatory.5 "outputDir": "./evincedReports", // Specifies a path to the final aggregated report files. Mandatory.6 "tmpDir": "./evincedReports/tmp" // A directory for storing Evinced tmp files. 'os.tmpdir()' by default. Optional.7 }8}
Custom File Names for Multiple Formats
Recommended (evConfig.yaml):
1report:2 format: [html, json] # Must match fileName array length when using custom names.3 fileName: [myReport.html, myReport.json]4 outputDir: ./evincedReports5 tmpDir: ./evincedReports/tmp6 aggregate: true
Recommended (structured evConfig.json):
1{2 "report": {3 "format": ["html", "json"],4 "fileName": ["myReport.html", "myReport.json"],5 "outputDir": "./evincedReports",6 "tmpDir": "./evincedReports/tmp",7 "aggregate": true8 }9}
Deprecated (legacy flat reporterOptions in evConfig.json):
1{2 "reporterOptions": {3 "reportFormat": ["html", "json"],4 "fileName": ["myReport.html", "myReport.json"], // Array must match reportFormat array length. Mandatory.5 "outputDir": "./evincedReports",6 "tmpDir": "./evincedReports/tmp" // A directory for storing Evinced tmp files. 'os.tmpdir()' by default. Optional.7 }8}
Then add specify a path to the Evinced reporter in your playwright.config file. It might be found in your node-modules directory, for example:
1'./node_modules/@evinced/js-playwright-sdk/dist/reporter/evincedReporter.js'
Important! The Evinced reporter file path might be different in your project, so please check it before specifying the path in the config file.
To do so add Evinced reporter to a list of reporters, it may look like this:
1reporter: process.env.CI ? 'html' : [['line'], ['./node_modules/@evinced/js-playwright-sdk/dist/reporter/evincedReporter.js']]
Important!
- For the modern schema, set
report.format,report.fileName, andreport.outputDir. Usereport.aggregate: true(default) to generate an aggregated report at the end of the run; setreport.aggregate: falseto skip aggregated report generation (no report files are written and an emptyreport.outputDiris not left behind). For legacyreporterOptions,reportFormat,fileName, andoutputDirare mandatory. report.cleanupdefaults totrue— deletes per-spec tmp files after the run (legacy:reporterOptions.deleteTmpFiles). Setreport.cleanup: falseto keep tmp files for debugging.format/reportFormatcan be a string (single format) or an array (multiple formats). Available formats:html,json,csv, andsarif.fileNamecan be a string (base name) or an array of strings (one name per format).tmpDiris optional; the environment temp directory is used by default.
And that's it! Now you can run your tests and get the aggregated report(s) in the specified directory.
Adding Aggregated Report when using the Cucumber test reporter
In some cases when using a Cucumber reporter, you may need to add the Evinced reporter to your `BeforeAll` and `AfterAll` hooks in the following way:
1// Import the Evinced reporter,2// it may look like this in your project (in case of using commonjs):3const EvincedReporter = require('@evinced/js-playwright-sdk/reporter');
Add the Evinced reporter to your BeforeAll hook:
1BeforeAll(async function () {2 // your code here3 new EvincedReporter().onBegin();4});
Add the Evinced reporter to your AfterAll hook:
1AfterAll(() => {2 // your code here3 new EvincedReporter().onEnd();4});
Configuration
The same configuration options can be used when initializing the Evinced object
using evConfig.json and when calling the evStart()
and evAnalyze() methods but with a bit different consequences.
Providing options when initializing defines a global configuration for all calls
of evAnalyze() and evStart(), while providing options to
either of those methods affect only the test in which they are called. Some SDKs
also let you define configuration outside your code, in a config file that is
picked up automatically at startup.
Options provided in either evAnalyze() or evStart() override
those set in Evinced engine initialization.
Engines Configuration
Evinced uses two separate engines when scanning for accessibility issues, one is the aXe engine and the other is the Evinced engine. By default, Evinced disables the aXe Needs Review and Best Practices issues based on customer request and due to the fact they are mostly false positives. Please note this setting when comparing issue counts directly. See an example of how to enable Needs Review and Best practices issues in the Toggles section.
Configuration Object
To define global configuration for Playwright JS SDK, place evConfig.yaml, evConfig.yml, or structured evConfig.json at the project root. JSON and YAML formats remain fully supported; only legacy top-level key names are deprecated. Per-command options on EvincedSDK methods use the same nested structure.
1type EvConfig = {2 switchOn?: boolean;3 scan?: {4 rootSelector?: string;5 iframes?: boolean;6 iframeDomains?: string[];7 screenshots?: { enabled?: boolean };8 withPasses?: boolean;9 };10 analysis?: {11 AXE_CONFIG?: Record<string, unknown>;12 TOGGLES?: Record<string, boolean>;13 SKIP_VALIDATIONS?: SkipValidation[];14 ISSUE_CONTENT_PER_TYPE?: Record<string, { knowledgeBaseLink: string }>;15 RECORDING_SERVICE?: Record<string, unknown>;16 [key: string]: unknown;17 };18 report?: {19 format?: 'json' | 'html' | 'csv' | 'sarif' | ('json' | 'html' | 'csv' | 'sarif')[];20 outputDir?: string;21 fileName?: string | string[];22 aggregate?: boolean;23 cleanup?: boolean;24 tmpDir?: string;25 };26 platform?: {27 upload?: { enabled?: boolean; autoUpload?: boolean };28 };29 logging?: {30 preset?: 'errors' | 'standard' | 'diagnostic';31 channels?: { engine?: boolean; sdk?: boolean };32 };33 network?: {34 idle?: { enabled?: boolean; timeout?: number; maxWait?: number };35 proxy?: Record<string, unknown>;36 };37};
Recommended (evConfig.yaml):
1switchOn: true2scan:3 iframes: true4 screenshots:5 enabled: false
Recommended (structured evConfig.json):
1{2 "switchOn": true,3 "scan": {4 "iframes": true,5 "screenshots": {6 "enabled": false7 }8 }9}
Deprecated (legacy flat keys in evConfig.json):
1{2 "switchOn": true,3 "includeIframes": true,4 "enableScreenshots": false5}
Root Selector
Sets a CSS selector to limit the Evinced Engine to scan only the selected element and its children. Must be a valid CSS selector. If not set, the Evinced Engine will scan the entire document.
Default: no value
Recommended (evConfig.yaml):
1scan:2 rootSelector: '#main'
Recommended (structured evConfig.json):
1{2 "scan": {3 "rootSelector": "#main"4 }5}
Recommended (per command):
1const evincedService = new EvincedSDK(page);2await evincedService.evStart({3 scan: { rootSelector: '.some-selector' }4});5await page.goto('https://demo.evinced.com/');6const issues = await evincedService.evStop();
Deprecated (legacy flat rootSelector key in evConfig — JSON, YAML, and YML remain supported):
1{2 "rootSelector": "#main"3}
Deprecated (per command — legacy flat rootSelector):
1await evincedService.evStart({2 rootSelector: '.some-selector'3});
User-Defined Labels
Attaches your own metadata to every issue element, so a report can be grouped by your
structure — team, feature area, component — instead of by DOM position. Labels appear on each
issue under elements[].userLabels, grouped by the kind of rule that produced them:
1"userLabels": {2 "matchingSelectors": ["#cart |+ .buy-button"],3 "attributes": {4 "data-testid": ["checkout-submit"]5 },6 "callback": {7 "team": ["checkout"],8 "stableId": ["WRONG_SEMANTIC_ROLE:BuyButton"]9 }10}
Three rule types are available, and they can be combined:
selector— label an element by a CSS selector it matches or descends from.attribute— label an element with the value of an attribute on it or its nearest ancestor.function— this is the option to reach for when you need a stable identity for an issue that survives DOM changes — for example a component name read from adata-attribute, or an id built from an element's position in your own component tree rather than in the document.
Default: no labels.
All three rule types can be written in a config file. A function rule has to name the
callback — with scriptPath or ref — because a file cannot carry a live fn.
1scan:2 userDefinedLabels:3 - type: selector4 selector: '#cart |+ .buy-button'5 - type: attribute6 attribute: data-testid7 scope: closest8 - type: function9 scriptPath: ./labels/teamLabels.js
All three rule types. Note that fn is the one form with no project-wide equivalent: a config
file cannot hold a live function and the EvincedSDK constructor takes no configuration, so an
fn rule is supplied per command. To set a callback once for every scan, use scriptPath or
ref in evConfig.yaml, as in the first tab.
1const evincedService = new EvincedSDK(page);2const issues = await evincedService.evAnalyze({3 scan: {4 userDefinedLabels: [5 // Label anything inside #cart by the matched ancestor.6 { type: 'selector', selector: '#cart |+ .buy-button' },7 // Label with the value of the nearest data-testid attribute.8 { type: 'attribute', attribute: 'data-testid', scope: 'closest' },9 // Compute labels yourself, per issue element.10 {11 type: 'function',12 fn: (element, typeId) => ({13 team: [element.closest('[data-team]')?.getAttribute('data-team') ?? 'unowned'],14 stableId: [`${typeId}:${element.getAttribute('data-component') ?? 'unknown'}`]15 })16 }17 ]18 }19});
For a longer callback, keep it in its own .js file whose contents evaluate to a function.
1const issues = await evincedService.evAnalyze({2 scan: { userDefinedLabels: [{ type: 'function', scriptPath: './labels/teamLabels.js' }] }3});
1// ./labels/teamLabels.js2function (element, typeId) {3 return { team: [element.closest('[data-team]')?.getAttribute('data-team') ?? 'unowned'] };4}
A relative scriptPath is resolved from the working directory of the process running the tests —
where the runner was launched, normally the project root, and never the spec file's own directory.
Use path.resolve(__dirname, 'labels/teamLabels.js') for a path relative to the spec.
If the function is placed there by the application or by your own script, pass only its ref.
It must start with __evUserLabelsFn_, so a typo cannot resolve an unrelated global such as
print.
1const issues = await evincedService.evAnalyze({2 scan: { userDefinedLabels: [{ type: 'function', ref: '__evUserLabelsFn_team' }] }3});
Choosing a form. The forms above differ in one way that matters: in all of them except the
last, you hand the SDK the callback itself and the SDK owns installing it — into the top document
and into every frame it scans, re-installing into frames that navigate or appear later, so nothing
further is required of you. Supply only a ref when the application itself puts the function on
the page; covering every frame is then your job, as described below. Where your SDK offers more
than one way to hand over the callback, keep a short one where it is used and move a longer one
into its own file. Except in Cypress the callback reaches the page as source, so it cannot use
helpers, imports, or variables from the file it is written in — define what it needs inside it.
Rules for the callback
A function rule runs your callback in the page under test, once per issue element:
1callback(element, typeId) -> Record<string, string[]>
element is the DOM element the issue was reported on, and typeId is the issue type
(the same value as type.id in a JSON report). Whatever the callback returns is merged into
that element's userLabels.callback.
Important: The callback itself is never part of the configuration the engine receives. Configuration is plain data — it has to be copied into the page, and then copied again into every frame the engine scans — so it can only carry a name, not a live function.
The function itself must exist on the global object (window) of each page or frame the engine
scans, under that name. The engine looks up the name and calls whatever it finds there. This name
is the rule's ref — you can choose it yourself, or let the SDK generate one.
Before using this feature, decide whether the function will already be on the page yourself, or whether the SDK should install it for you — see Who installs the callback below.
- It must be synchronous. The engine uses the return value immediately, so a promise is
never awaited. An
asyncfunction is rejected before it is installed — the rule is skipped and the SDK logs why — because that case can be spotted up front. A plain function that happens to return a promise can only be caught once it runs, so it is reported per element (see below). - Each callback needs its own
ref. Twofunctionrules resolving to the samerefare rejected, and the second is dropped with a warning. This is not a harmless duplicate: both rules would resolve to the same function on the page and produce identical labels, which is almost always a copy-paste slip rather than an intent. - It must return
Record<string, string[]>— an object whose values are arrays of strings. Any other shape is reported as an error. - It must not throw, though nothing breaks if it does — see below.
- In every SDK except Cypress it must be self-contained. These SDKs drive the browser from outside it, so the callback is serialised to source and injected into the page: helpers, imports, and variables from the surrounding file are not in scope when it runs. Inline everything it needs. Cypress runs inside the browser, so its callback may use values from the spec.
- Label keys and values are capped (20 keys and 50 values per key per callback; 200 characters each). Anything over the cap is dropped rather than truncated, because these are identity strings and a truncated id would silently collide with another.
Example
A callback that gives each issue an owner and an id tied to your component tree rather than to the element's position in the document:
1function (element, typeId) {2 // Helpers live inside the callback on purpose: outside Cypress it reaches the page as3 // source, so nothing from the file it was written in is in scope when it runs.4 function slug(value) {5 return String(value == null ? '' : value)6 .toLowerCase()7 .replace(/[^a-z0-9]+/g, '-')8 .replace(/^-+|-+$/g, '')9 .slice(0, 40);10 }1112 var card = element.closest('[data-component]');13 if (!card) {14 // Return an empty object, not nothing: `undefined` is not a `Record<string, string[]>`,15 // so falling off the end here would put a `__callbackError` on every element that16 // misses the lookup.17 return {};18 }1920 var labels = {};21 var team = card.getAttribute('data-team');22 if (team) {23 labels.team = [team];24 }25 // Pair the type with the component so two different issues on one component stay apart.26 labels.stableId = [typeId + ':' + slug(card.getAttribute('data-component'))];27 return labels;28}
Given <div data-component="Buy Button" data-team="checkout"> around the reported element,
that returns:
1{2 "team": ["checkout"],3 "stableId": ["WRONG_SEMANTIC_ROLE:buy-button"]4}
which arrives in the report under the callback bucket of that element:
1"userLabels": {2 "callback": {3 "team": ["checkout"],4 "stableId": ["WRONG_SEMANTIC_ROLE:buy-button"]5 }6}
The keys are yours — team and stableId here — and every value is an array of strings, even
when there is only one. Re-run the scan after the DOM around the button changes and stableId
stays the same, which is the point: it is derived from your component, not from the element's
position.
Who installs the callback
This is the decision from above, spelled out. The two arrangements differ in one important way — who's responsible for getting the function into every frame the engine scans, not just the top page.
The SDK installs it — you supply the function (or a .js file, or JavaScript source). The SDK
puts it on the page for you, in the top document and in every frame it scans, re-installing into
frames that navigate or appear later. Nothing further is required of you.
You install it — you assign the function to a global of your own on the page and pass only
that name as the rule's ref. The SDK then installs nothing, and the engine looks the ref up
separately in every frame it scans. So with iframe scanning enabled, you have to assign your
function in each frame's own global scope as well; a frame where the ref is not defined reports
__callbackError for every issue found there.
Either way the ref must start with __evUserLabelsFn_. The engine refuses any other name,
because the global object is full of callable things that are not label callbacks: a ref of
print or alert would otherwise resolve, pass the engine's is-it-a-function check, and be
invoked once per issue element. Names beginning __evUserLabelsFn_auto_ are reserved for the ones
the SDK generates, so a name you choose can never collide with one of those.
When a callback misbehaves
A broken callback never fails the scan. The problem is reported in the report itself, under
a reserved __callbackError label key beside the real labels, so it can be found without reading
SDK logs:
1"userLabels": {2 "callback": {3 "__callbackError": [4 "callback '__evUserLabelsFn_0' threw: el.closest is not a function"5 ]6 }7}
The same label key covers a callback that is missing, throws, returns the wrong shape, returns a
promise, or exceeds the label limits. To audit a run, search a JSON report for
__callbackError.
Axe Configuration
Configures Axe open-source accessibility toolkit, which the Evinced engine includes with its own, more extensive accessibility detection. For full Axe config options, see Axe Core API.
Recommended (per command):
1const evincedService = new EvincedSDK(page);2await page.goto('https://demo.evinced.com/');3const issues = await evincedService.evAnalyze({4 analysis: {5 AXE_CONFIG: {6 rules: {7 'link-name': { enabled: false }8 }9 }10 }11});
Deprecated (per command — legacy flat axeConfig):
1const issues = await evincedService.evAnalyze({2 axeConfig: {3 rules: {4 'link-name': { enabled: false }5 }6 }7});
Engine Logging
Set level of messages the Evinced engine will print to the console.
Valid levels are "debug", "info", "warn" and "error".
Default: "error"
Configure SDK log output with the logging.* section in evConfig.yaml, evConfig.yml, or structured evConfig.json. To include analysis-engine messages in the console, set logging.channels.engine: true or use preset diagnostic (includes the engine channel).
Recommended (evConfig.yaml):
1logging:2 preset: diagnostic
Recommended (enable engine channel only):
1logging:2 channels:3 engine: true
Deprecated (legacy flat sdkLogging key in evConfig):
1{2 "sdkLogging": {3 "enable": true,4 "level": "debug",5 "enableEngineLogs": true6 }7}
Reports Screenshots
When true, the Evinced SDK will include screenshots in its reports that
highlight elements with accessibility issues.
Default: false.
Note: Enabling screenshots may affect test run performance.

Recommended (per command):
1const issues = await evincedService.evAnalyze({2 scan: { screenshots: { enabled: true } }3});
Deprecated (per command — legacy flat enableScreenshots):
1const issues = await evincedService.evAnalyze({2 enableScreenshots: true3});
How screenshots are captured
When screenshots are enabled, the SDK captures them itself, through Playwright's own screenshot API. On Chromium that is a Chrome DevTools Protocol capture — Playwright drives Chromium over CDP — and it works on Firefox and WebKit too.
If a capture fails, that issue appears in the report without a screenshot and an error naming the reason is written to the SDK log; the scan itself is unaffected. Later captures are attempted normally. There is no fallback to engine-side capture, because Playwright's capture works on every browser it supports.
Captures are full-page and rendered at one image pixel per CSS pixel, so the issue highlights in an HTML report line up with the image. This is not configurable.
Known limitations
An issue on an element inside an <iframe> is highlighted at the wrong position in the HTML report: the element's coordinates are relative to the iframe document rather than to the top-level page. The screenshot itself is correct and the issue is reported normally — only the highlight is misplaced.
Switching back to engine-side capture
scan.screenshots.mode chooses which side captures. It is read only when scan.screenshots.enabled is true.
| Value | Effect |
|---|---|
| unset | The SDK captures the screenshots. This is the default. |
'sdk' | Identical to leaving it unset. Accepted so that configurations written before SDK-side capture became the default keep working — it no longer switches anything on. |
'browser' | The analysis engine captures the screenshots in the page, as it did before SDK-side capture. The image is redrawn inside the page rather than taken by the browser's own compositor, so it can differ from what you see on screen. |
'off' | Skips screenshots for this call even when enabled: true is set globally. Useful for a single spec; to turn screenshots off everywhere, prefer enabled: false. |
Reach for 'browser' only where SDK-side capture does not suit your environment; leave mode unset otherwise.
1const issues = await evincedService.evAnalyze({2 scan: { screenshots: { enabled: true, mode: 'browser' } }3});
Toggles
Enables experimental features. Feature names and values may vary from release to release.
Example: Recommended (per command):
1const issues = await evincedService.evAnalyze({2 analysis: {3 TOGGLES: {4 USE_AXE_NEEDS_REVIEW: true,5 USE_AXE_BEST_PRACTICES: true6 }7 }8});
Deprecated (per command — legacy flat toggles):
1const issues = await evincedService.evAnalyze({2 toggles: {3 USE_AXE_NEEDS_REVIEW: true,4 USE_AXE_BEST_PRACTICES: true5 }6});
Skip Validations
Sets validation types to be skipped for specified URL pattern and CSS selector. Issue type IDs can be found by inspecting a JSON report as described in Web Reports.
Default: no validations skipped.
Recommended (per command):
1const skipSelector = {2 selector: 'testValue.slds-checkbox',3 urlRegex: '.*',4 validationTypes: ['WRONG_SEMANTIC_ROLE', 'NOT_FOCUSABLE', 'NO_DESCRIPTIVE_TEXT']5};67await evincedService.evStart({8 analysis: { SKIP_VALIDATIONS: [skipSelector] }9});
Deprecated (per command — legacy flat skipValidations):
1await evincedService.evStart({ skipValidations: [skipSelector] });
Skipping every validation type
To suppress all validation types for a selector, pass the wildcard * in place of a list of
type names.
The wildcard must be the only value. Listed alongside concrete type names it is not treated as a wildcard — the wildcard entry is ignored and only the concrete types given with it are skipped.
1const skipEverythingInParent = {2 selector: '#parent, #parent *',3 urlRegex: '.*',4 validationTypes: '*'5};67await evincedService.evStart({8 analysis: { SKIP_VALIDATIONS: [skipEverythingInParent] }9});
A selector matches elements, not subtrees
Skip rules are evaluated against each reported element on its own, so a selector suppresses issues only on the elements it matches — not on anything nested inside them. To cover a container and its contents, list both the container and its descendants:
1#parent, #parent *
#parent alone suppresses only issues reported on #parent itself.
Include Validations
Reports only the validation types listed, and drops everything else. This is the allowlist counterpart of Skip Validations: skip removes selected types from an otherwise full report, while include restricts the report to selected types.
Use it to focus a test run on one area, such as color contrast or missing labels, without listing every other type to exclude.
Issue type IDs can be found by inspecting a JSON report as described in Web Reports.
Default: every validation type is reported.
The setting is global. Unlike Skip Validations it takes no URL pattern and no
CSS selector, and it applies to the whole scan. It is available only through the modern analysis
configuration schema shown below; there is no legacy flat spelling for it.
1const includedTypes = ['NO_DESCRIPTIVE_TEXT', 'WRONG_SEMANTIC_ROLE'];23// For one continuous-mode session:4await evincedService.evStart({5 analysis: { INCLUDE_VALIDATIONS: includedTypes }6});78// Or for a single analysis:9const report = await evincedService.evAnalyze({10 analysis: { INCLUDE_VALIDATIONS: includedTypes }11});
Reporting every validation type
Leaving the setting out reports every validation type. So does the wildcard *, on its own or as
a single-item list, and so does an empty list.
A wildcard listed next to concrete type names is not a wildcard. ['NOT_FOCUSABLE', '*'] reports
only NOT_FOCUSABLE; the * entry is treated as a type name that matches nothing. This mirrors
the behavior of Skip Validations.
Combining with Skip Validations
The two settings apply together, and a result is reported only if it passes both. Include narrows the set of types; skip can still suppress individual elements within the types that remain. A type named in both is reported only on the elements its skip rule does not match.
Effect on passed validations
The setting narrows passed results as well as failed ones. Passed results appear in a report only when Passed Validations is enabled. When it is enabled, they are restricted to the same list of types.
Type names are not validated
Type names are not checked against the engine's list of issue types, so a name that is not a real type ID matches nothing.
Note: an empty list and a list of names that match nothing produce opposite results. An empty
list reports every validation type. A list such as ['NOT_FOCUSABL'] reports no issues at all. If
a run returns zero issues unexpectedly, check the type names in the list against a JSON report.
Recording Service
This setting controls how frequently event-triggered functions are executed, helping to optimize performance and responsiveness. The option does not enable or disable the event-handling service itself but determines how frequently events are processed based on the selected mode.
Available modes:
debounce- Delays execution until a set time has passed since the last event. Useful for actions triggered by continuous input, such as typing in a search box, to avoid excessive function calls.throttle- Ensures execution at fixed intervals, regardless of event frequency. Ideal for scenarios like handling window resize events or scroll tracking, where limiting execution prevents performance degradation.
Default: Functions execute as triggering events occur.
An example of how to modify settings: Recommended (per command):
1const evincedService = new EvincedSDK(page);23await evincedService.evStart({4 analysis: {5 RECORDING_SERVICE: {6 ENABLE_DEBOUNCE_NEW_SELECTORS: true,7 DEBOUNCE_NEW_SELECTORS_MS: 500,8 DELAY_MODE: 'throttle'9 }10 }11});1213await page.goto('https://demo.evinced.com/');1415await evincedService.evStop();
Deprecated (per command — legacy flat recordingService):
1await evincedService.evStart({2 recordingService: {3 ENABLE_DEBOUNCE_NEW_SELECTORS: true,4 DEBOUNCE_NEW_SELECTORS_MS: 500,5 DELAY_MODE: 'throttle'6 }7});
Knowledge-Base Link Overrides
Sets customized knowledge-base links in the reports. The links are displayed in the reports “Issue Type” column, as shown in the following screenshot:

The knowledge base link can be overridden for every issue type ID.
Issue type IDs can be found by inspecting a JSON report as described in Web Reports.
For example, the issue with name Interactable Role has ID WRONG_SEMANTIC_ROLE.
Recommended (per command):
1const issues = await evincedService.evAnalyze({2 analysis: {3 ISSUE_CONTENT_PER_TYPE: {4 WRONG_SEMANTIC_ROLE: {5 knowledgeBaseLink: 'https://yourKnowlegdeBase.com/'6 }7 }8 }9});
Deprecated (per command — legacy flat issuesContentPerType):
1const issues = await evincedService.evAnalyze({2 issuesContentPerType: {3 WRONG_SEMANTIC_ROLE: {4 knowledgeBaseLink: 'https://yourKnowlegdeBase.com/'5 }6 }7});
Shadow DOM Support
Shadow DOM is now supported by default. No additional configuration is needed.
IFrames Support
When true, accessibility analysis includes iframe that exist inside the page.
Default: true.
Recommended (evConfig.yaml):
1scan:2 iframes: false
Recommended (structured evConfig.json):
1{2 "scan": {3 "iframes": false4 }5}
Recommended (per session or analysis):
1await evincedService.evStart({ scan: { iframes: false } });2const issues = await evincedService.evAnalyze({ scan: { iframes: false } });
Deprecated (legacy flat includeIframes key in evConfig — JSON, YAML, and YML remain supported):
1{2 "includeIframes": false3}
Deprecated (per session or analysis — legacy flat includeIframes):
1await evincedService.evStart({ includeIframes: false });2const issues = await evincedService.evAnalyze({ includeIframes: false });
Important! The current implementation does not support an analysis of Cross Origin iFrames. Only issues from the iFrames with the origin of the parent iFrame will be gathered.
Note: Since iframe analysis is enabled by default, your tests may take longer to run when analyzing pages with multiple or complex iFrames.
Include Hidden IFrames Domain
Configures which hidden iframe domains are included in accessibility analysis. By default, hidden iframes are excluded for performance and security reasons.
This setting allows you to specify a list of domains to analyze even when the iframe is hidden.
Supported Pattern Formats:
- Exact match:
"example.com"- matches the exact URL string - Regex with flags:
"/.*\\.google\\.com.*/i"- matches using regex with optional flags (i, g, m, u, y) - Raw regex:
".*\\.example\\.com.*"- matches using a raw regex pattern
Recommended (per command):
1const issues = await evincedService.evAnalyze({2 scan: {3 iframes: true,4 iframeDomains: [5 'https://exact-domain.com',6 '/.*\\.google\\.com.*/i',7 '.*\\.example\\.com.*'8 ]9 }10});
Deprecated (per command — legacy flat keys):
1const issues = await evincedService.evAnalyze({2 includeIframes: true,3 includeHiddenIframeDomains: [4 'https://exact-domain.com',5 '/.*\\.google\\.com.*/i',6 '.*\\.example\\.com.*'7 ]8});
Passed Validations
By default, the Evinced SDK only reports accessibility issues that have failed validation. However, you can also configure the SDK to include passed validations in your reports. Passed validations represent accessibility checks that were successfully completed without any issues found.
Why include passed validations?
Including passed validations in your reports provides several benefits:
- Comprehensive Coverage: Get a complete picture of all accessibility checks performed, not just the failures
- Compliance Documentation: Demonstrate which accessibility standards your application successfully meets
- Trend Analysis: Track improvements over time by monitoring both failed and passed validation counts
- Quality Assurance: Verify that accessibility checks are running as expected across your entire application
- Regulatory Reporting: Provide evidence of accessibility testing coverage for compliance audits
Report Structure
When passed validations are enabled, your reports will include both:
failedValidations: Array of accessibility issues that need to be fixedpassedValidations: Array of accessibility checks that passed successfully
Default: false (passed validations are not included)
Enable Passed Validations
To include passed validations in your reports, set the includePassedValidations option to true:
Warning: Enabling passed validations has a performance impact on your testing as the reporting overhead is significantly greater than when reporting just found issues.
Single Page Analysis with Passed Validations
1import { test } from '@playwright/test';2import { EvincedSDK } from '@evinced/js-playwright-sdk';34test('Analyze page with passed validations', async ({ page }) => {5 const evincedService = new EvincedSDK(page);67 await page.goto('https://demo.evinced.com');89 // Run analysis with passed validations enabled10 const report = await evincedService.evAnalyze({11 includePassedValidations: true12 });1314 // Report now contains both failed and passed validations15});
Continuous Analysis with Passed Validations
1import { test } from '@playwright/test';2import { EvincedSDK } from '@evinced/js-playwright-sdk';34test('Continuous analysis with passed validations', async ({ page }) => {5 const evincedService = new EvincedSDK(page);67 // Start continuous analysis with passed validations8 await evincedService.evStart({9 includePassedValidations: true10 });1112 // Navigate and interact with your application13 await page.goto('https://demo.evinced.com');14 await page.getByText('Select').first().click();15 await page.getByText('Tiny House').click();1617 // Stop analysis and get comprehensive report18 const report = await evincedService.evStop();19});
Saving Reports with Passed Validations
1test('Save report with passed validations', async ({ page }) => {2 const evincedService = new EvincedSDK(page);34 await evincedService.evStart({ includePassedValidations: true });5 await page.goto('https://demo.evinced.com');6 const report = await evincedService.evStop();78 // Save complete report (includes both failed and passed validations)9 await evincedService.evSaveFile(10 report,11 'json',12 './reports/complete-report.json'13 );14});
Configuration file
Recommended (evConfig.yaml):
1scan:2 withPasses: true3 screenshots:4 enabled: true5 rootSelector: main
Recommended (structured evConfig.json):
1{2 "scan": {3 "withPasses": true,4 "screenshots": {5 "enabled": true6 },7 "rootSelector": "main"8 }9}
Deprecated (legacy flat keys in evConfig — JSON, YAML, and YML remain supported):
1{2 "includePassedValidations": true,3 "enableScreenshots": true,4 "rootSelector": "main"5}
Recommended (per command):
1await evincedService.evAnalyze({ scan: { withPasses: true } });2await evincedService.evStart({ scan: { withPasses: true } });
Deprecated (per command — legacy flat includePassedValidations):
1await evincedService.evAnalyze({ includePassedValidations: true });2await evincedService.evStart({ includePassedValidations: true });
Report Structure
Without passed validations (includePassedValidations: false):
1{2 "issues": [3 // Array of failed validation issues only4 ],5 "screenshotsMap": { /* screenshot data */ }6}
With passed validations (includePassedValidations: true):
1{2 "failedValidations": [3 // Array of issues that failed validation4 ],5 "passedValidations": [6 // Array of checks that passed validation7 ],8 "screenshotsMap": { /* screenshot data */ }9}
Proxy
Configures proxy server access settings. Needed to enable outbound communication to the Evinced Platform through a proxy server.
network.proxy routes the SDK's own outbound HTTP through a proxy — platform upload, setCredentials() online token generation, usage analytics, and evExportReport()'s signed-URL calls. This is separate from Playwright's own use.proxy in playwright.config.ts, which routes the browser's page traffic (the pages under test) through a proxy. Configure both if you need proxying for both traffic types.
Configure network.proxy through evConfig.yaml, evConfig.yml, or structured evConfig.json:
Recommended (evConfig.yaml):
1network:2 proxy:3 host: myproxy.com4 port: 31285 protocol: http6 username: usr7 password: pwd
Recommended (structured evConfig.json):
1{2 "network": {3 "proxy": {4 "host": "myproxy.com",5 "port": 3128,6 "protocol": "http",7 "username": "usr",8 "password": "pwd"9 }10 }11}
Deprecated (legacy flat proxy key in evConfig — JSON, YAML, and YML remain supported):
1{2 "proxy": {3 "host": "myproxy.com",4 "port": 3128,5 "protocol": "http",6 "username": "usr",7 "password": "pwd"8 }9}
Environment variable fallback (used only when network.proxy.host is not set):
1export HTTPS_PROXY=http://myproxy.com:31282export PROXY_USERNAME=usr3export PROXY_PASSWORD=pwd
HTTPS_PROXY names the proxy used for HTTPS destinations — it does not mean the proxy endpoint itself speaks TLS. Most corporate proxies are plain HTTP servers even when proxying HTTPS traffic (they tunnel it via CONNECT), so http:// is almost always correct here; use https:// only if your proxy itself terminates TLS. HTTP_PROXY is read for non-HTTPS destination URLs, HTTPS_PROXY otherwise (lowercase http_proxy / https_proxy are also accepted). Credentials embedded directly in the proxy URL (http://usr:pwd@myproxy.com:3128) take precedence over PROXY_USERNAME / PROXY_PASSWORD.
Bypassing the Proxy for Specific Hosts (network.noProxy)
Use network.noProxy to skip the proxy for specific destination hosts — useful when your proxy can't (or shouldn't) reach an internal endpoint, or when only some of the SDK's outbound destinations need to go through it.
1network:2 proxy:3 host: myproxy.com4 port: 31285 protocol: http6 noProxy:7 - internal.example.com8 - '*.corp.local'
Each entry may be:
- An exact hostname (
internal.example.com) — matches that host and any of its subdomains - A domain with a leading dot (
.corp.local) — matches the domain and its subdomains '*'— bypasses the proxy for every destination
network.noProxy is merged with the standard NO_PROXY / no_proxy environment variable (comma-separated list, same entry forms) into a single bypass list — a match from either source skips the proxy, regardless of whether the proxy itself came from network.proxy or the HTTPS_PROXY/HTTP_PROXY environment fallback above.
Recommended (Playwright global proxy in playwright.config.ts, for page/browser traffic):
1use: {2 proxy: {3 server: 'http://myproxy.com:3128',4 username: 'usr',5 password: 'pwd'6 }7}
Network Idle Detection
The networkIdle configuration enables the SDK to wait for all network requests to complete before
running accessibility analysis. This is particularly useful for modern web applications that load
content dynamically after the initial page load.
Why use Network Idle Detection?
When testing modern web applications, accessibility analysis may run before all content has loaded, resulting in:
- Incomplete results: Dynamic content not yet rendered won't be analyzed
- False negatives: Accessibility issues in delayed content won't be detected
- Inconsistent results: Analysis timing may vary between test runs
Network idle detection solves this by ensuring all network activity has settled before analysis begins.
Configuration Options:
| Option | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Enables or disables network idle detection |
idleTimeout | number | 300 | Time in milliseconds to wait after the last network request completes before considering the network idle |
maxWaitTime | number | 7500 | Maximum time in milliseconds to wait for network idle. If exceeded, analysis proceeds with a warning |
Network idle detection can be configured globally during SDK initialization or per-command for granular control.
Global Configuration
Recommended (evConfig.yaml):
1network:2 idle:3 enabled: true4 timeout: 3005 maxWait: 7500
Recommended (structured evConfig.json):
1{2 "network": {3 "idle": {4 "enabled": true,5 "timeout": 300,6 "maxWait": 75007 }8 }9}
Deprecated (legacy flat networkIdle key in evConfig — JSON, YAML, and YML remain supported):
1{2 "networkIdle": {3 "enabled": true,4 "idleTimeout": 300,5 "maxWaitTime": 75006 }7}
Per-Command Configuration
Recommended:
1await evinced.evStart({2 network: { idle: { enabled: true, maxWait: 10000 } }3});45await evinced.evAnalyze({6 network: { idle: { enabled: true, timeout: 500, maxWait: 5000 } }7});
Deprecated (per command — legacy flat networkIdle):
1await evinced.evStart({2 networkIdle: {3 enabled: true,4 maxWaitTime: 100005 }6});78await evinced.evAnalyze({9 networkIdle: {10 enabled: true,11 idleTimeout: 500,12 maxWaitTime: 500013 }14});
Usage Example
1import { test, expect } from "@playwright/test";2import { EvincedSDK } from "@evinced/js-playwright-sdk";34test('network idle with continuous mode', async ({ page }) => {5 const evincedService = new EvincedSDK(page);67 await evincedService.evStart({8 network: { idle: { enabled: true } }9 });10 await page.goto('https://example.com/');11 await page.locator('[data-nav="products"]').click();12 const issues = await evincedService.evStop();1314 await page.goto('https://example.com/products');15 await evincedService.evAnalyze({16 network: { idle: { enabled: true } }17 });18});
Configuration Options
network.idle.enabled(boolean, default:false) - Enable/disable network idle detectionnetwork.idle.timeout(number, default:300) - Milliseconds to wait with no active requests before considering network idlenetwork.idle.maxWait(number, default:7500) - Maximum time to wait before proceeding anyway
How It Works
Network idle detection waits for network activity to settle before running accessibility analysis. This ensures analysis runs on a stable page with all dynamic content loaded.
- Static Analysis (
evAnalyze): Waits for network idle before running analysis - Continuous Mode (
evStart/evStop): Waits for network idle only atevStart()(session start), not after navigation or atevStop()
Priority Order
- Method options (highest priority) - Overrides everything
- evConfig.json - Used if not in method options
- Defaults -
network.idle.enabled: false,network.idle.timeout: 300,network.idle.maxWait: 7500
How It Works:
The SDK monitors all network requests and waits for the network to become idle before proceeding with analysis:
- Network is considered "idle" when no requests are active for at least
idleTimeoutmilliseconds - Analysis waits for idle state or
maxWaitTime, whichever comes first - Configuration from
evStart()automatically applies to subsequent page navigations andevStop()
Best Practices:
- Start with defaults: Enable with default settings and adjust only if needed
- Increase timeout for slow APIs: If your backend is slow, increase
maxWaitTime - Use per-command for specific pages: Enable only for pages with dynamic content
Important Notes:
- Disabled by default: The feature is opt-in to maintain backward compatibility
- No overhead when disabled: When not enabled, there is zero performance impact
- Automatic timeout extension: Command timeouts are automatically adjusted based on
maxWaitTime - Warning on timeout: If
maxWaitTimeis exceeded, the SDK logs a warning and proceeds with analysis
Global Switch
When false, disables Evinced functionality. Enabled by default, use this setting to disable
Evinced accessibility analysis when not needed during test development or when running CI jobs
where accessibility testing is not intended.
Default: true.
When switched off:
evStart()andevSaveFile()will be bypassed.evStop()andevAnalyze()will return an empty report.
Switching Evinced Functionality Off in Configuration
Add the following to your evConfig file (evConfig.yaml, evConfig.yml, or structured evConfig.json at the project root). JSON and YAML formats remain fully supported; only legacy top-level key names are deprecated.
Recommended (evConfig.yaml):
1switchOn: false
Recommended (structured evConfig.json):
1{2 "switchOn": false3}
Alternative (per command with the same modern key):
1await evincedService.evStart({ switchOn: false });
Deprecated (legacy flat sdkLogging keys in evConfig):
1{2 "sdkLogging": {3 "enable": true,4 "level": "debug"5 }6}
Important! Global Switch environment variable overrides the global configuration option.
Switching Evinced Functionality Off in Environment
1export EV_SWITCH_ON=false
Uploading Reports to Evinced Platform
Introduction
Evinced Platform allows you to seamlessly collect, organize, visualize and monitor Evinced accessibility reports in one place. In this section, we will guide you through the key functionalities of the upload methods of the accessibility reports from the Evinced SDK to the Evinced Platform, which was introduced in version 2.14.0. This upload method is fully compatible with the previous versions of the Evinced SDK API, and is disabled by default.
Enable Upload Report to Platform
To enable the uploading functionality of accessibility reports to the Evinced Platform
you will need to set the enableUploadToPlatform feature flag to true via
the global setUploadToPlatformConfig method:
1import { setUploadToPlatformConfig } from "@evinced/js-playwright-sdk";2setUploadToPlatformConfig({ enableUploadToPlatform: true });
You can also use evConfig.yaml, evConfig.yml, or structured evConfig.json.
Warning: The external config has more precedence if both initialization options are used.
Recommended (evConfig.yaml):
1platform:2 upload:3 enabled: true4 autoUpload: false
Recommended (structured evConfig.json):
1{2 "platform": {3 "upload": {4 "enabled": true,5 "autoUpload": false6 }7 }8}
Deprecated (legacy flat keys in evConfig.json):
1{2 "uploadToPlatformOptions": {3 "enableUploadToPlatform": true,4 "setUploadToPlatformDefault": false5 }6}
Note: Using uploadToPlatform: true in method parameters (e.g., evAnalyze({ uploadToPlatform: true })) is not sufficient on its own. You must first enable the feature via setUploadToPlatformConfig(), or in your config file. The method parameter only controls whether a specific report uploads when the feature is already enabled.
Automatic Report Upload
Once the enableUploadToPlatform method is set to true and setUploadToPlatformDefault is true (which is the default),
all generated reports will be automatically uploaded to the Platform immediately upon calling the evStop() or evAnalyze() command.
How it works:
- When
enableUploadToPlatform: trueandsetUploadToPlatformDefault: true (default), upload happens automatically - No additional code is needed - just call
evAnalyze()orevStop()and the report uploads - Upload occurs synchronously as part of the command execution
If you want to change this behavior and control uploads manually, set the setUploadToPlatformDefault feature flag to false.
When upload is enabled, set platform.upload.autoUpload to true to upload reports automatically at the end of each analysis session (legacy: setUploadToPlatformDefault: true).
Recommended (evConfig.yaml):
1platform:2 upload:3 enabled: true4 autoUpload: true
Recommended (structured evConfig.json):
1{2 "platform": {3 "upload": {4 "enabled": true,5 "autoUpload": true6 }7 }8}
Deprecated (legacy flat keys in evConfig.json):
1{2 "uploadToPlatformOptions": {3 "enableUploadToPlatform": true,4 "setUploadToPlatformDefault": true5 }6}
Deprecated (setUploadToPlatformConfig()):
1setUploadToPlatformConfig({2 enableUploadToPlatform: true,3 setUploadToPlatformDefault: true4});
If the setUploadToPlatformDefault is disabled, you can still upload
selected reports to the platform.
For that, use the following parameter in the evStop() command:
1await evincedService.evStop({ uploadToPlatform: true });
Or, in the evAnalyze() command:
1await evincedService.evAnalyze({ uploadToPlatform: true });
Test Names
To facilitate report management and be able to distinguish between different reports on the Platform, use the addLabel method to inform the test name and test class.
It’s recommended to do that in the “beforeEach” hook.
1let evincedService;2test.beforeEach(async ({page}, testInfo)=> {3 evincedService = new EvincedSDK(page);4 evincedService.testRunInfo.addLabel({5 testName: testInfo.title,6 })7})
Labels and Custom Fields
You can attach labels and custom fields to your report to enhance readability and organization in the platform. Labels help you filter, search, and organize reports on the Evinced Platform.
There are two types of labels:
Built-in Labels: Pre-defined labels that can be set using the addLabel method. Available built-in labels include:
testName- The name of the testtestFile- The file path of the testenvironment- The environment where the test runs (e.g., "Development", "Staging", "Production")flow- The test flow identifiergitBranch- The Git branch namegitUserName- The Git user namegitVersion- The Git commit version
Custom Labels: Flexible key-value pairs that can be set using the customLabel method. You can use any custom key-value pairs, including:
- Single values:
{ productVersion: '1.0.0' } - Multiple values (arrays):
{ browsers: ['Chrome', 'Firefox'] } - Special label
unitId: Use this to tag tests for relevant units within your organization
See the following code examples of how to set up labels:
1let evincedService;2test.beforeEach(async ({page}, testInfo)=> {3 evincedService = new EvincedSDK(page);45 // Set built-in labels6 evincedService.testRunInfo.addLabel({7 testName: testInfo.title,8 testFile: testInfo.file,9 environment: 'Development',10 gitBranch: 'main'11 });1213 // Set custom labels (including unitId for organizational tagging)14 evincedService.testRunInfo.customLabel({15 'anyCustomParameter': 'demo value',16 productVersion: '0.0.1',17 browsers: ['Chrome 1.00', 'Firefox 2.00'],18 unitId: 'unit-123' // Tag for organizational unit19 });20})
Use of beforeEach and afterEach Hooks
We recommend using the beforeEach and afterEach hooks to control analysis sessions and upload reports to the platform. This way, each test will be uploaded separately with its own report.
In beforeEach hook use evStart() to start Evinced analysis and set any labels
you want for the report to contain when uploading to the platform.
In the afterEach hook call evStop() to stop analysis and upload reports to the platform.
See this code example:
1 let evincedService;2 test.beforeEach(async ({page}, testInfo)=>{3 const evincedService = new EvincedSDK(page);4 evincedService.testRunInfo.addLabel({5 testName: testInfo.title,6 testFile: testInfo.file,7 environment: 'Development'8 })9 await evincedService.evStart();10 })11 test.afterEach(async ()=>{12 await evincedService.evStop();13 })
Putting All of This Together
Here is a complete code snippet of how to perform uploads to the platform on a per-test basis.
1import { EvincedSDK, setUploadToPlatformConfig } from '@evinced/js-playwright-sdk';23test.describe('Upload demo', () => {4 let evincedService;5 test.beforeAll(() => {6 setUploadToPlatformConfig({ enableUploadToPlatform: true });7 })8 test.beforeEach(async ({page}, testInfo)=>{9 evincedService = new EvincedSDK(page);10 evincedService.testRunInfo.addLabel({11 testName: testInfo.title,12 testFile: testInfo.file,13 environment: 'Development'14 })15 await evincedService.evStart();16 })1718 test.afterEach(async ()=>{19 await evincedService.evStop();20 })2122 test('Upload example playwright #0', async ({ page }) => {23 await page.goto("https://demo.evinced.com");24 });25})
Tutorials
You can find fully functional example projects on our GitHub.
Fail the test if critical issues are found
Here you can see a way of failing your test if critical accessibility issues are found using the Playwright JS SDK.
Using evAnalyze:
1const issues = await evincedService.evAnalyze();23const criticalIssues = issues.filter((issue) => issue.severity.name === 'Critical');4await assert(criticalIssues.length === 0, 'found critical issues');
Using evStart/evStop:
1await evincedService.evStart();2const issues = await evincedService.evStop();34const criticalIssues = issues.filter((issue) => issue.severity.name === 'Critical');5await assert(criticalIssues.length === 0, 'found critical issues');
The criticalIssues array will contain all the critical issues found during the scan. If the array is not empty, the test will fail on the assertion.
Complete Test Suite Integration
You can use Playwright Fixtures to integrate Evinced Playwright JS SDK into your test suite with global hooks that add logic to the Playwright test object. These hooks ensure that the Evinced SDK is properly initialized and managed across your test suite, which will reduce your setup effort and improve consistency.
Create a new module that extends the Playwright test object with beforeEach() and afterEach() hooks. The following example shows the module in file baseTestExtension.js placed in the same directory as the test specs, but you may use any file name or location in the project.
1// baseTestExtension2const { test: base } = require("@playwright/test");3const { EvincedSDK } = require("@evinced/js-playwright-sdk");45const test = base.extend({6 hookSetup: [7 async ({ page }, use, testInfo) => {8 const evincedService = new EvincedSDK(page);9 await beforeEach(page, evincedService);10 await use(); // this will execute the test11 await afterEach(page, evincedService, testInfo);12 },13 { auto: true }, // when true, the hook will be automatically executed and attached to all tests14 ],15});1617async function beforeEach(page, evincedService) {18 console.log("Global before each hook");19 await evincedService.evStart();20}2122async function afterEach(page, evincedService, testInfo) {23 console.log("Global after each hook");24 const issues = await evincedService.evStop();25 await evincedService.evSaveFile(26 issues,27 "json",28 `report-${testInfo.title}.json`29 );30}31module.exports = { test };
In your test spec file, use the test object exported from baseTestExtension.js instead of the default Playwright test object as shown in the following example.
Make sure the test accepts a parameter with the page object. It is required.
1// a test spec2const { test } = require("./baseTestExtension");34test.describe("Test suite description", () => {5 test("Test name", async ({ page }) => {6 await page.goto("https://your-web-site.com");7 // test logic here....8 });9});
For additional information on Playwright Fixtures, please refer to the official Playwright documentation: playwright fixtures
Support
Please feel free to reach out to support@evinced.com with any questions.
FAQ
- Can I configure which validations to run?
Yes, see the Configuration section for details on how to configure Axe validations to your needs.
- Can I run tests with Evinced using cloud-based services like Sauce Labs, Perfecto, or BrowserStack?
Yes, we have tested the Evinced SDK on many of these types of cloud-based services and expect no issues.