# How to integrate with Asana
Source: https://docs.qawolf.com/qawolf/Asana-2a15b2a994fb805287acdba6373e6758
Connect QA Wolf to Asana to automatically create and sync bug report issues in your Asana workspace.
Select **Workspace settings** from the `Workspace name` dropdown above the flows list.
Click **Integrations** from the left-side navigation bar.
Click **Enable** next to Asana.
Click **Connect** on the modal.
Enable **Bug reports** and select the **Workspace** and **Project** in which bug reports are going to be created.
* Click **Disconnect** to disable the integration.
# Create flows with AI
Source: https://docs.qawolf.com/qawolf/Automate-flows-using-AI-2d35b2a994fb8084b524cfc9aca7c358
Use AI flow creation to create, edit, and debug flows from plain-language or voice instructions.
AI flow creation lets you create, edit, and debug flows by describing what you want in plain language or by speaking your instructions. You don't need to know the flow syntax to get started — the AI writes and updates the code for you.
If you'd prefer to write flows manually, see [Create flows in the IDE](/qawolf/Create-a-new-flow-2c15b2a994fb802193a5c05e0219eb59).
## Start a flow
Two entry points on the **Flows** tab hand a new flow to the agent:
* **Create flow** — the button above the flows list
* **Create flow** — the large purple button on the **All flows** page
Both open the AI chat panel in flow mode. Working with the agent one prompt at a time is [guided creation](/qawolf/getting-the-most-out-of-automate#guided-creation).
The icon next to **Create flow** opens a drawer with **Outline flows**, which starts an outlining session across a whole application at once — see [Create a coverage outline](/qawolf/Map-your-app-s-workflows-2a45b2a994fb80ca8a87c8fdc2a443fc).
## Describe the flow you want
Either **Create flow** button opens the AI chat panel in flow mode, offering **Create a flow** and **What should I test?** to start from.
### Type a description
Type a plain-language description of what you want the flow to do into the chat box and hit enter. For example: *"Log in with a valid email and password and assert that the dashboard loads."*
### Speak a description
Click the icon and speak your instructions.
The agent will create the required groups and flows if they don't already exist.
## What happens next
The agent generates the flow code and opens it in the editor. From here you can:
* **Run it immediately** using the **Run** button to see it execute in the browser panel
* **Review the code** if you want to understand or manually adjust what was generated
* **Ask the AI to change it** by typing a follow-up instruction in the chat panel
## Complete flows automatically
Select **Autonomously complete this test** to have the AI generate Playwright code for the flow automatically. While it runs, you can multitask in other flow tabs.
## Debug a failing flow
When a flow fails, click **Fix with AI** in the banner at the top of the editor. Describe the problem or let the AI interpret the error automatically.
For manual debugging, see [Debug tests & flows](/qawolf/Debug-tests-and-flows-2c15b2a994fb805c9e92eec7a4a3e83e).
## Import a third-party library
In the AI chat panel, ask the agent to import the package you need. It will handle the entire process automatically.
# How to integrate with Azure DevOps Boards
Source: https://docs.qawolf.com/qawolf/Azure-DevOps-Boards-29c5b2a994fb81b09697f2f1e4f9fdf1
Connect QA Wolf to Azure DevOps Boards to automatically create and sync bug report issues using OAuth.
Go to `https://dev.azure.com/ORGANIZATION_NAME/_settings/organizationPolicy` — **ORGANIZATION\_NAME** is your Azure DevOps Organization name.
**Because** QA Wolf connects to Azure DevOps using OAuth, you must enable **Third-party application access via OAuth**.
If **Third-party application access via OAuth** is disabled, an Azure DevOps organization owner must enable it before QA Wolf can be authorized. If you are not an organization owner, contact your Azure DevOps administrator.
Select **Workspace settings** from the **Workspace name** dropdown above the flows list.
Click **Integrations** from the left-side navigation bar.
Click **Enable** next to Azure DevOps.
Click on the connect button.
Select the **Azure DevOps** service.
Enter the Azure DevOps **Organization name** and click **Continue**.
Click **Configuration**, then select the target project from the dropdown.
* Click **Disconnect** at any time to disable the integration.
# Overview
Source: https://docs.qawolf.com/qawolf/CI-CD-Overview
QA Wolf integrates with your CI pipeline to run tests automatically. Choose the outcome that matches your use case.
Run tests when a new build is deployed to an environment.
Run tests against a new mobile build artifact from your CI pipeline.
# Choose the right CI environment to execute your tests
Source: https://docs.qawolf.com/qawolf/Choose-the-right-CI-environment-to-execute-your-tests-29c5b2a994fb8031a3ddc1355eb4e7be
QA Wolf supports testing across any CI environment, so you can align testing goals with your existing release process.
Match your testing goals to the environment you run your tests in.
### Staging: When test stability matters most
Use your **Staging** (QA or Test) environment for the best balance between reliability and safety.
**What this gives you**
* Stable builds with predictable data.
* Space for complete end-to-end and regression testing.
* Safe configuration for allowlisted email domains or test credentials.
Staging may lag behind active development. If releases are infrequent, regressions might not surface until later in testing.
### Development: Get rapid feedback during test development
Run tests in your **Development** environment to identify regressions early.
**What this gives you**
* Immediate feedback after merges.
* Quick iteration and faster bug turnaround.
* Tight integration with the development process.
Development environments can be unstable or inconsistent. Failing tests here often point to temporary issues rather than actual defects.
### Production: Verify the real user experience
Run tests in **Production** when you need complete confidence in real-world behavior.
**What this gives you**
* Testing against the exact production configuration.
* Real performance and reliability data.
* Confirmation that core user flows work correctly in the live environment.
Bugs discovered here have already reached users. Some flows (such as payments) may trigger real transactions or pose a financial risk.
### Preview environments: Validate changes before merging code
Ask QA Wolf to help you enable **PR testing** to run your flows automatically on each pull request in ephemeral preview environments.
**What this gives you**
* Early, automated feedback in your VCS.
* Confidence that merges won't break mainline tests.
* Faster, safer release cycles.
PR testing requires dedicated, isolated environments. Shared resources or reused credentials can cause flaky results.
QA Wolf recommends using [release candidates (RCs)](https://www.qawolf.com/blog/rcs-over-prs-how-rc-testing-aligns-velocity-and-quality) for PR testing.
# Clear a release
Source: https://docs.qawolf.com/qawolf/Clear-a-release-2c55b2a994fb808fa5ddfa157a89adcd
Resolving failures is about deliberately clearing a release, not about achieving 100% passing results.
An environment enters **Needs investigation** when one or more flows fail in a run. An environment enters **Failed** status when one or more flows mapped to high-priority bug reports fail. These states indicate that one or more runs require resolution before the release should proceed.
Failed flows and environment status provide a release signal. If you want runs in *Failed* status to block releases, ask your Customer Success Manager about our CI Greenlight feature.
A run that contains failed flows remains in **Investigating** or **Failed** as long as any failed flow has not been resolved. A failed flow is considered resolved when one of the following occurs:
* The flow passes on a subsequent attempt, including when a temporary environmental issue resolves.
* The flow is fixed, and the updated version is applied to the run.
* The underlying application issue causing the failure is fixed.
* A maintenance report is filed, preventing the flow from being included in runs until it is fixed.
A run transitions to *Completed* only after all failed flows have been resolved. Once all runs are completed, the environment exits the *Needs investigation* or *Failed* status and can be considered ready for release.
## Reattempt a failure caused by a flake
Reattempting is appropriate when all failures in the run are believed to be caused by a flake or a temporary environment glitch.
Open the **Runs** tab and click the run that is in **Investigating** or **Failed** status to open the Investigation view.
Click the icon to open the context menu.
Choose one of the following options depending on where you are in your investigation:
* **Reattempt with latest version** — runs only the failed flows. Use this when you want to quickly check that a suspected temporary glitch has resolved.
* **Duplicate run** — a full retry of all flows, including up to three attempts. Use this after you have validated that the temporary problem has resolved but want to check whether it was masking deeper issues.
## Bypass a failing flow to clear a release
Bypassing a failed flow is appropriate when the failure is understood and should not block the release — for example, an understood failure in a third-party dependency. Bypassing does not fix the flow or the underlying issue. Instead, it records that the failure has been reviewed and does not require further investigation for the current release.
Open the **Runs** tab and select a run in **Investigating** or **Failed** status.
Select the failed run that contains the flow that doesn't need further investigation.
Click **Diagnose** in the center panel.
Select **Do not investigate**.
Once a failed flow is marked as **Do not investigate**, it is considered resolved for the run. When all failed flows in the run have either passed or been bypassed, the run transitions to **Completed**.
You can also find **Diagnose** by hovering over the failing flow to show the icon, then clicking it to show the icon. **Diagnose** will appear below the flow list.
## Repair a broken test to unblock a release
From the **Runs** tab, select the environment from the list on the upper left.
In the center panel, select a run from the **Investigating** list.
In the Investigation view, click **Edit code** to open the flow for changes.
Click **Run code**.
When the flow fails, click **Fix with AI** in the banner at the top of the editor. Describe the problem or let the AI interpret the error automatically.
You can also repair the test manually by modifying the code yourself.
When satisfied with the changes, review and publish your changes. You **must** publish your changes to enable **Use as reattempt**.
Click **Use as reattempt**. The flow should pass and will move to the **Completed** section of the Runs tab.
## Clear the release when there's a bug or broken test that can't be fixed immediately
Some failures represent real issues but are not practical to fix during the current release. In these cases, the appropriate action is to document the work required and allow the release to proceed.
Follow-up work generally falls into two categories:
**For bugs** — [file a bug report](/qawolf/Manage-bug-reports-2c75b2a994fb80308ddcc7cc60703c2f). When you file a bug report, the severity is used to display warnings about the safety of a given release.
**For flow maintenance** — [file a maintenance report](/qawolf/Manage-maintenance-reports-2d55b2a994fb80728909f48b1023f7fa). This prevents the flow from being executed in runs until it is fixed, which resolves the investigation state without requiring an immediate fix.
# Create flows in the IDE
Source: https://docs.qawolf.com/qawolf/Create-a-new-flow-2c15b2a994fb802193a5c05e0219eb59
Create flow files and folders and write test code manually in the QA Wolf IDE.
If you'd prefer to have AI create and edit flows for you, see [Create flows with AI](/qawolf/Automate-flows-using-AI-2d35b2a994fb8084b524cfc9aca7c358).
## Create a folder or flow
Two entry points on the **Flows** tab create a flow in the IDE.
**From the flows list**
Click in the toolbar, then select **New flow** or **New folder**.
**From the IDE**
From the **Flows** tab, hover over the left sidebar to expand it, then click **IDE**.
Navigate to the location in the file tree where you want to create the new file or folder.
Click the icon in the toolbar.
Click **New Folder** or **New Flow**.
Use clear, descriptive names that reflect your app's feature areas — this helps both you and AI keep flows organized.
## Write test code
Select a flow file from the tree to open it in the code editor. The editor prefills new flows with [AAA](https://www.qawolf.com/blog/intro-to-aaa) (Arrange-Act-Assert) comments as a starting structure.
For a full explanation of flow structure and imports, see [Anatomy of a flow](/qawolf/anatomy-of-a-qa-wolf-test-mobile-edition).
QA Wolf lints your code as you type. See [Debug tests & flows](/qawolf/Debug-tests-and-flows-2c15b2a994fb805c9e92eec7a4a3e83e) if you encounter errors.
## Publish your changes
When ready, click **Review**.
Review your changes in the side-by-side diff.
Enter a clear description of the change in the **Adding a comment** field.
Click **Publish changes** to confirm. The publish dialog will close when your changes have been saved.
# How to verify and debug your test
Source: https://docs.qawolf.com/qawolf/Debug-tests-and-flows-2c15b2a994fb805c9e92eec7a4a3e83e
Run your test code in a live browser, identify failures, and use debugging tools to fix issues.
## Run your flow
Click **Run** to execute the flow. A live browser appears in the right panel where you can watch the code execute in real time.
When the flow completes successfully, a green **Flow passed** banner appears at the top of the editor.
## Debug a failure
If something goes wrong, a red **Flow failed** banner appears at the top of the editor and the failed line is highlighted in red.
You have two options for fixing it:
**Fix with AI** — click **Fix with AI** in the banner to let the agent diagnose and repair the failure automatically. You can also describe the problem in the AI chat panel or paste the error directly from the **Logs** tab.
**Run selected lines** — reset the app state in the browser, select the lines you want to test, and click **Run**. This lets you iterate on a specific section without re-running the entire flow.
## Use the debugging panels
The bottom-right panel has four tabs that provide different views of your flow's execution.
**Problems** — the default tab when you open a flow. Appears when QA Wolf detects issues such as naming conflicts. Affected files and folders are marked with a yellow dot in the file tree. Resolve any problems listed before running or publishing to avoid confusing output.
**Logs** — shows a timestamped execution log. When a flow fails, the error message here is the fastest way to understand what went wrong. You can paste the log output directly into the AI chat panel to get a fix.
**Elements** — lets you enter a locator or CSS selector to highlight matching elements in the live browser. Useful for verifying selectors while writing or debugging a flow. See [Element picker](/qawolf/Use-the-Elements-picker-2c35b2a994fb80a681baca4f6acf3179) for more details.
**Details** — shows tags and readiness status for the current flow. Both fields can be edited directly from this tab. After a run, switches to show individual run steps with timing, useful for identifying slow or hung steps.
## Check run history
The **Run History** tab shows a pass/fail chart and the result of the most recent run.
Run History only shows results from scheduled runs. Manual runs from the editor or Flows tab do not appear here. See [How Runs work](/qawolf/Run-a-flow-or-suite-2a25b2a994fb8014b164d7ad2efd5ddel) for a full explanation of run types.
## Manage tags and readiness status
The **Details** tab shows the tags and readiness status assigned to the current flow. Both fields can be edited directly from this tab using the icon next to each field.
After a run, the tab switches to display individual run steps with timing, which is useful for identifying slow or hung steps.
# Diagnose the cause of a failing flow
Source: https://docs.qawolf.com/qawolf/Diagnose-the-cause-of-a-failing-flow-2c65b2a994fb8034ae2bd0def1f8dfe5
Learn how to classify flow failures as flakes, bugs, or broken tests using the Investigation view.
## The different reasons flows fail
When a flow fails, the next step is to determine why. Identifying the cause determines the appropriate resolution path and helps avoid unnecessary fixes or retries.
Failures generally fall into one of the following categories:
* **Flakes** — failures caused by infrastructure outages, network instability, or failed dependent services. These issues often resolve without changes to the flow or the application.
* **Bugs** — failures that reveal incorrect or unexpected behavior in the application under test. These indicate a product issue rather than a problem with the flow itself.
* **Broken tests** — failures caused by flow logic, bad selectors, incorrect assertions, or assumptions that no longer match the application. These may be simple to correct or may require more substantial maintenance.
Correctly identifying the cause helps you determine whether the failing flow should be retried, fixed, deferred, or accepted to clear the release.
## Use the Investigation view to diagnose a failure
The **Investigation view** is where you diagnose flows that failed during a scheduled or event-based run. It brings together everything you need to understand what happened in a single run and decide what to do next.
You reach the Investigation view by opening a run from the **Runs** tab when that run is in **Investigating** or **Failed** status. The view opens with the most relevant information already selected, so you can start reviewing immediately.
### Find failing flows
Navigate to the Investigation view. The left panel separates **Failed**, **Running**, and **Passed** flows.
Click the **Failed** tab to see just those flows.
### Identify the failing line of code
Navigate to the failing flows in the Investigation view.
Select a flow. The point of failure is highlighted.
### Compare attempts in a single run
If a flow was retried multiple times during a run, you can switch between attempts to determine whether the failure was consistent or occurred only under certain conditions.
### Review what happened in the browser during the most recent run
Recorded video shows exactly what happened in the browser during the selected attempt, which is often the fastest way to understand unexpected behavior.
### Compare previous runs
To review video of up to the last 100 runs of a flow, including all attempts, use the flow's **Run History** tab.
### Access Playwright traces
Navigate to the failing flow from the Investigation view.
Click the **Trace** tab on the right panel.
## Use Run History, Activity, and Notes to diagnose failures
Some issues only become clear when you look beyond a single run. **Run History** shows the last 100 runs for a flow, including each attempt. Reviewing this history helps you spot patterns such as intermittent failures, regressions after changes, or flows that consistently fail in the same place.
You'll typically access these tools from the Investigation view or the **Run History** tab in the IDE.
Run History is also available in the IDE — select a flow and click the **Run History** tab in the right panel.
### Focus on a specific run
Hover over the vertical bars in the **Run History** timeline.
As you move across the bars, the selected bar expands to show the icon, and the timestamp below the timeline updates to match that run.
Click the icon to open details for the selected run.
### Review change history
The **Activity** tab shows changes made to a flow over time, including readiness updates and maintenance reports. This helps explain when a flow was modified or temporarily removed from runs and why.
### Record and review notes
The **Notes** tab lets you record context about a flow, such as known issues, edge cases, or investigation findings. Notes are saved per flow and can help guide future diagnosis and maintenance.
# Configure environments
Source: https://docs.qawolf.com/qawolf/Environment-settings-2cd5b2a994fb8014ba8bd7aa25f3ed0f
Create and manage environments in QA Wolf with separate variables, concurrency settings, and VPN access.
## Environments
In the QA Wolf platform, an [environment](/qawolf/Glossary-29c5b2a994fb806d93f2ce4554a2fcfc) corresponds to a specific version of your app, such as development, staging, or production.
Each environment can be configured with separate variables (e.g., base URL, users), concurrency rates, and other settings relevant to the application version you're testing. Flows and tests are unique to each environment and are not shared across environments.
## Create an environment
Choose **Workspace settings** from the `Workspace name` dropdown above the flows list.
Click **Environments** in the left navigation bar.
Click **Create environment** in the upper-right corner. Give it a unique name and click **Create**.
Replace the existing Environment Name and press Enter.
## Change your default environment
Both the **Flows** and **Runs** tabs require a default environment. Set this to the environment you use most often for running and debugging tests.
Open the `Workspace name` dropdown and click **Workspace settings**.
Go to **General**.
Under **Default environments**, choose the environment you want the **Flows** and **Runs** tabs to use by default from the **Default team environment** dropdown.
If your team is using PR testing environments, select the default environment from the dropdown labeled **Default team PR Testing base environment**.
Contact your QA Wolf Customer Success Manager to assist with PR testing configuration.
## Limit the environment's concurrency
By default, QA Wolf executes all flows in a run concurrently. You can change this by capping concurrency per environment.
Open the `Workspace name` dropdown and click **Workspace settings**.
Click **Environments** in the left navigation bar.
Scroll to **Run Concurrency Limit** and select the maximum number of flows to be executed at the same time.
Click **Save**.
## Create and manage environment variables
In the QA Wolf platform, environment variables are encrypted constants scoped to a specific environment.
You can use the same variable across environments by reusing the same name in each environment and setting the appropriate value for that environment (for example, `BASE_URL` in Staging vs Production). QA Wolf doesn't enforce cross-environment consistency—this pattern relies on naming conventions.
Open the `Workspace name` dropdown and click **Workspace settings**.
Click **Environments** in the left navigation bar.
Click the **Environment variables** tab.
These can also be updated from the **Flows** tab.
# Integrate a mobile build with Fastlane
Source: https://docs.qawolf.com/qawolf/Fastlane-2db5b2a994fb802186a3d88573899c78
Upload Android and iOS build artifacts and trigger QA Wolf test runs from your existing Fastlane lanes using the QA Wolf plugin.
## When to use Fastlane
This guide is for teams that already use Fastlane to build Android or iOS apps. If your mobile builds are defined as Fastlane lanes, the QA Wolf Fastlane plugin lets you upload build artifacts and trigger test runs within your existing mobile automation. This is the recommended option for mobile teams that use Fastlane today.
If you do not use Fastlane, use the QA Wolf CI SDK instead. QA Wolf does not set up Fastlane or create build lanes—this guide assumes Fastlane is already installed and that it is producing a mobile build artifact.
## Before you begin
Before configuring the Fastlane integration, make sure you have the following in place:
* A working Fastlane setup for your Android or iOS app.
* A Fastlane lane that produces a mobile build artifact (APK, AAB, or IPA).
* A QA Wolf API key stored as a secret in your CI environment.
* Artifact naming conventions are defined for your environments. See [Artifact naming conventions](#artifact-naming-conventions) below.
Before mobile test runs can execute, QA Wolf must enable mobile triggers for your workspace. QA Wolf will handle this and may ask you for:
* Which environments you want to test.
* Whether PR testing is enabled.
* The artifact naming conventions you are using.
* The upload and trigger method you chose.
Until this step is complete, CI jobs can upload artifacts and send deployment notifications, but mobile test runs will not start automatically.
## How Fastlane works with QA Wolf
The Fastlane integration uses a QA Wolf Fastlane plugin to perform two actions:
Upload a mobile build artifact to QA Wolf.
Notify QA Wolf of a deployment event so a test run can be triggered.
You can upload builds without triggering runs, which is useful during initial setup or validation.
The artifact name you provide is used to associate the build with the correct environment. QA Wolf automatically applies the file extension.
## Install & configure Fastlane for QA Wolf
Add the [QA Wolf Fastlane plugin](https://github.com/qawolf/fastlane-plugin-qawolf) to your project if it is not already installed. This is typically done in your **Fastfile** or plugin configuration, depending on how your project manages Fastlane plugins. Follow the instructions on the `README` to install.
Make sure your CI environment exports the **QAWOLF\_API\_KEY** environment variable so Fastlane can authenticate with QA Wolf.
### Find the QAWOLF\_API\_KEY
Open the `Workspace name` dropdown in QA Wolf and click **Workspace Settings**.
Choose **Integrations**.
Generate your **QAWOLF\_API\_KEY** by clicking the icon to the right of **API Key** under **API Access**.
### Add the QAWOLF\_API\_KEY secret
Store `QAWOLF_API_KEY` as a secret in your CI system and ensure it is exported as an environment variable in the job that runs Fastlane. The exact steps depend on your CI provider — refer to your CI system's documentation for storing secrets.
## Artifact naming conventions
Mobile build artifacts must follow consistent naming conventions so QA Wolf can correctly associate each build with the right environment and make failures easier to diagnose.
The artifact name is used to identify:
* Which environment the build belongs to
* Whether the build is tied to a pull request
* Which build was used for a given test run
### Static environments
Static environments are long-lived environments such as staging or release environments.
**Format**
```text theme={null}
-
```
**Example**
```text theme={null}
app-staging
```
Use the same basename every time a build is generated for the same environment.
### PR (ephemeral) environments
PR environments are short-lived and tied to a specific pull request. These are only relevant if PR testing is enabled.
**Format**
```text theme={null}
---pr
```
**Example**
```text theme={null}
app-myorg-myrepo-pr123
```
Including the organization, repository, and pull request number ensures each build can be traced back to the correct change and environment.
QA Wolf applies the file extension (.apk, .aab, or .ipa) automatically based on the uploaded artifact. You only need to provide the basename.
## Upload a mobile build artifact
Once your Fastlane lane produces a build artifact, you can upload it to QA Wolf.
Add the `upload_to_qawolf` action to your lane and provide the artifact basename that matches your naming conventions. If this step completes successfully, the artifact has been uploaded and is available for use in test runs.
```ruby theme={null}
lane :qawolf do
# Build your app
gradle(
task: "assemble",
build_type: "Release"
)
# Upload the artifact to QA Wolf
upload_to_qawolf(
executable_file_basename: "app-staging"
)
end
```
## Trigger a test run
After uploading a build artifact, you can notify QA Wolf that a new deployment is ready for testing.
Add the `notify_deploy_qawolf` action to your lane. This step is optional and is required only if you want a test run to start automatically.
The environment key must match the value configured by QA Wolf for your workspace.
If mobile triggers have not yet been enabled, this step will complete without starting a test run.
```ruby theme={null}
notify_deploy_qawolf(
executable_environment_key: "RUN_INPUT_PATH"
)
```
## Verify the integration
Run the Fastlane lane in CI.
Confirm the artifact upload step completes successfully.
Confirm the deploy notification step runs without errors.
Once mobile triggers are enabled, check the **Runs** tab to see the triggered test run.
If no run appears, the most common cause is a missing or incomplete QA Wolf platform configuration.
## Troubleshooting and common issues
* **Artifact uploads succeed, but no runs start:** Mobile triggers may not be enabled yet. Contact QA Wolf to complete platform configuration.
* **Artifact not found during test execution:** Verify the artifact basename matches the expected naming conventions.
* **Authentication errors:** Confirm QAWOLF\_API\_KEY is set correctly in your CI environment.
* **Incorrect environment used:** Verify the environment key passed to notify\_deploy\_qawolf matches the configured value.
# Full Service FAQ
Source: https://docs.qawolf.com/qawolf/Full-Service-FAQs-29c5b2a994fb8000b442fa8aed05c9e5
Answers to common questions about QA Wolf full service, including test ownership, coverage, and how the service works.
## General
### What is QA Wolf's full service?
QA Wolf's full service means we don't just provide the platform — we also create, maintain, and run your end-to-end tests for you.
Our team writes Playwright and Appium tests, expands coverage over time, fixes broken tests as your product changes, and ensures tests run reliably on every pull request, schedule, or trigger you choose.
You still own the code and can modify it at any time. We simply handle the ongoing work required to keep end-to-end coverage healthy.
With full service, QA Wolf:
* Writes and expands your end-to-end tests
* Maintains tests as your app evolves
* Runs tests in parallel across our infrastructure
* Investigates failures and fixes test issues
* Provides clear artifacts like videos, logs, and traces for every run
The goal is simple: you get high end-to-end coverage without needing to build and operate a large internal test automation team.
### Who will we be working with from QA Wolf?
Every customer gets a dedicated team of Wolves to make sure that they're successful:
* **Customer Success Manager.** Strategic owner on QA Wolf's side for our partnership, helping ensure we are aligned on your goals and helping you get the most out of our service.
* **QA Lead.** Primary point of contact for technical implementation, including your test plan, implementation, and testing of new features.
* **QA Team.** Responsible for implementing tests, investigating failures, maintaining tests, and reporting bugs.
* **Account Executive.** Your AE will guide you through contracting, security reviews, and any other internal procedures your company requires.
## Implementation
### In what order does QA Wolf build tests?
Unless there are specific high-priority tests that need to be done first, we generally start with the most complex flows. These take longer to build and often require more collaboration with your team, so tackling them early helps speed up the rest of the process.
Test creation isn't linear — we don't build one test after another in sequence. Because we focus on the hardest flows first, your suite will come together unevenly at first and then accelerate as simpler tests are added.
If you have critical flows you'd like prioritized earlier, let your QA Lead know. They can adjust the build plan to align with your release schedule.
### How can I see how many flows your team has built?
Open the Flows tab.
Select **All Flows** from the left sidebar. The total number of groups and flows appears on the right panel at the top.
#### How can I see the number of active flows your team has built?
Set your filter using the icon.
Hover over your team name in **All Flows** above to see the icon, then click it to get the icon. Below the list to the right, the number to the right of the icon shows the total number of flows, and the number to the right of the icon shows the total number of tests that meet the filter criteria.
### When will our tests start running?
Tests start running as soon as they're built, and our goal is to deliver value fast — beginning with your most business-critical flows.
To keep things moving:
* Make sure your team is available for the **Product Tour** meeting.
* Approve the **coverage outline** as soon as it's ready.
Engage with us early, so we prioritize the right flows and start catching issues within the first few days of onboarding.
### What happens when a test fails?
When tests fail, a QA Wolf engineer investigates the issue and determines whether it is a bug in the application or the test.
If there's a bug in the application, we will file a bug report through your messaging system (e.g., Slack, Teams) and issue tracker (e.g., Jira, Linear).
If there's a problem with the test, it can usually be resolved on the fly. However, some tests will need to be quarantined for more substantial maintenance work.
You can monitor the [status of failed tests](/qawolf/Interpret-run-results-2d55b2a994fb80199754e4b2d3ce0318) in the Runs tab.
### How can we prevent planned application changes from blocking a release?
One of the most common release blockers is a flow that requires maintenance before it can proceed. Most maintenance is simple and happens immediately, but sometimes a flow requires a more comprehensive update. When that happens, the affected flows are disabled and placed in **maintenance mode** until they can be fixed.
If you're planning changes that will affect more than a few pages or significantly alter the DOM, let your QA Lead know ahead of time. They can usually update or refactor the flows in advance so runs stay smooth.
When in doubt, just shoot us a quick message — even a heads-up helps prevent coverage gaps or blocked runs.
### How does QA Wolf handle flaky tests?
As a customer, you'll never have to deal with flaky tests, which are tests that fail one or more times before eventually passing. Since flaky tests create noise that would otherwise slow your release velocity, our dedicated QA team will handle everything and ensure you see only real, human-verified bugs. That's our Zero Flake Guarantee.
To keep things moving as quickly as possible, any test that fails is automatically re-run. That's because anything from a simple network hiccup to a slow email server could cause a flake. After multiple consecutive failures, a human steps in to investigate the root cause.
### How often will our test suite run?
By default, your suite will run once a day, which is where most companies start. As their deployment processes mature, they increase testing frequency.
We also support PR testing for [GitHub](/qawolf/PR-testing-for-GitHub-Integrations-2db5b2a994fb80a0aa94f3297d1f60c3) and [GitLab](/qawolf/PR-testing-for-GitLab-Integrations-2db5b2a994fb8075b13dd9d5c2e6be8c) through our [Scheduled Runs](/qawolf/Scheduling-flows-29c5b2a994fb80789f3fea32feae8f92). You can ask your QA Lead to increase or decrease your test runs to best fit your process.
Unlimited test runs are included in your contract, so we encourage running your suite as frequently as it makes sense for your team and process.
# Onboarding to QA Wolf Full Service
Source: https://docs.qawolf.com/qawolf/Full-Service-The-First-Three-Months-29c5b2a994fb81bb9e10e4182639f294
What to expect during QA Wolf full service onboarding, including timelines, meetings, and setup steps.
QA Wolf Full Service pairs your team with a dedicated QA engineering team that builds, runs, and maintains your automated test suite. If you're evaluating QA Wolf, [book a demo](https://www.qawolf.com/book-a-demo) to talk through your coverage goals.
## Onboarding
Onboarding with QA Wolf is easy, but a few things need to be set up before we can begin developing a Test Plan and building your automated test suite.
### Timeline
1 to 2 weeks after contract start if all blockers are cleared.
### Onboarding to-dos
Connect your Slack, Teams, or Discord account with QA Wolf.
Send test environment credentials to your Account Executive, QA Engineering Lead, or Customer Success Manager. The environment needs to be stable and reachable before onboarding can begin.
* Read: [Preparing an environment for QA Wolf test runs](/qawolf/Test-environments-2dc5b2a994fb800e8ce4fe36ee34764a)
* Read: [Which environment should we run tests on?](/qawolf/Choose-the-right-CI-environment-to-execute-your-tests-29c5b2a994fb8031a3ddc1355eb4e7be)
Identify a Champion (day-to-day) point of contact for QA Wolf who can clear blockers and make decisions about your test suite.
### Your QA team
* Read: [Who will we be working with from QA Wolf?](/qawolf/Full-Service-FAQs-29c5b2a994fb8000b442fa8aed05c9e5#who-will-we-be-working-with-from-qa-wolf)
### Kick-off meeting
**When**
Within 5 business days of the contract start date.
**Agenda**
* Introductions.
* Review coverage goals.
* Set expectations for the process.
* Align on milestones.
**Attending**
| From your team | From our team |
| :------------------------------------------------------------------------------- | :----------------------- |
| Point of contact during the sales process | Customer Success Manager |
| Champion (day-to-day contact) who will be most hands-on working with us | QA Lead |
| Any other team members you'd like to include (engineers, product managers, etc.) | |
**Action items**
| Your responsibilities | Our responsibilities |
| :------------------------------------------------- | :---------------------------------------------- |
| Connect a shared Slack, Teams, or Discord channel. | Learn about QA priorities and onboarding goals. |
| Share your environment credentials. | |
### Product tour meeting
**When**
Within 5 business days of the contract start date.
**Agenda**
* Introductions.
* Walk through your app.
* Identify testing priorities.
* Identify areas to avoid.
**Attending**
| From your team | From our team |
| :----------------------------------------------------------- | :------------------ |
| Product or engineering manager for each testable feature set | QA Engineering Lead |
| | Test outliner |
**Action items**
| Your responsibilities | Our responsibilities |
| :---------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------- |
| Internally align on the product(s) that QA Wolf will be working with and what areas are the highest priority to build coverage for. | Record the product tour. |
| | Take detailed notes for test plan creation. |
| | Ask clarifying questions about priorities and product functionality. |
## Implementation
Once we understand your application and testing priorities, we're ready to start outlining a Test Plan and coding up your automated test suite.
### Timeline
* Planning phase: 2 to 4 weeks after the contract start date if all blockers are cleared.
* Test creation phase: The next 2 to 3 months after the Test Plan is approved.
### Test planning to-dos
1. **Send over any existing test plans or internal priority lists.** Anything you have is great: Google Sheets, a TestRail matrix, or just a list of priority features. If you don't have anything, don't worry — we'll work with you to determine priorities and coverage areas.
2. **Export and send your existing E2E tests — if you have any.** We can convert that code to QA Wolf outlines and test code, or avoid covered product areas if you plan to continue running and maintaining them internally.
### First Test Plan review meeting
**When**
Within 15 business days of the contract start date.
**Agenda**
* Introductions.
* Review the test plan.
* Re-confirm priorities.
* Provide feedback on the test plan's coverage areas.
* Confirm the scope of work.
**Attending**
| From your team | From our team |
| :----------------------------------------------------------- | :------------------ |
| Product or engineering manager for each testable feature set | QA Engineering Lead |
| | Test outliner |
**Action items**
| Your responsibilities | Our responsibilities |
| :-------------------------------------------------------------------- | :----------------------------------------------------------------- |
| Answer questions from the test outliners on Slack, Teams, or Discord. | Prepare detailed outlines for all tests within budget. |
| | Field questions about the test plan and outlining process. |
| | Respond to outline feedback and changes in your team's priorities. |
### Final Test Plan review
**When**
Within 20 business days of the contract start date.
**Agenda**
* Introductions.
* Recap feedback from the first review.
* Review changes/updates to the test plan.
* Review the AAA framework.
* Finalize the scope of work.
**Attending**
| From your team | From our team |
| :----------------------------------------------------------- | :------------------ |
| Product or engineering manager for each testable feature set | QA Engineering Lead |
| | Test outliner |
**Action items**
| Your responsibilities | Our responsibilities |
| :------------------------------------- | :------------------------------------------------------------------------------------------------------------------------ |
| Identify any remaining gaps or issues. | Convert all approved outlines to the [AAA framework](https://www.qawolf.com/blog/intro-to-aaa) in preparation for coding. |
| Approve the scope of work. | Update outlines and priorities based on feedback from the first review. |
### Common test planning blockers
* Your stakeholders aren't aligned on testing goals and priorities, which prevents us from finalizing a Test Plan.
* Your team is unresponsive when QA Wolf reports possible bugs or asks for product clarification.
* Your team is unavailable to review the test plan.
### Common test creation blockers
* Your test environment is too unstable to run end-to-end tests reliably. Read: [Preparing an environment for QA Wolf test runs](/qawolf/Test-environments-2dc5b2a994fb800e8ce4fe36ee34764a).
# How to integrate GitHub/GitHub Actions
Source: https://docs.qawolf.com/qawolf/GitHub-GitHub-Actions-2a15b2a994fb800699f6dc99584915d0
Connect QA Wolf to GitHub Actions to automatically trigger test runs when you deploy code from your repository.
If your repositories are hosted on GitHub.com or GitHub Enterprise Cloud, this is the recommended setup. The integration connects directly to your QA Wolf environments using GitHub Actions to notify QA Wolf of new deployments.
If your team self-hosts GitHub Enterprise Server without internet access, or has disabled GitHub Actions by policy, use the [QA Wolf CI SDK](/qawolf/Integrate-with-the-QA-Wolf-SDK-2db5b2a994fb8043be61f06f025d9c63) instead.
Make sure you have:
* Access to your QA Wolf workspace.
* Admin access to your GitHub repository.
## Enable the GitHub integration in QA Wolf
Open the `Workspace name` dropdown in QA Wolf and click **Workspace Settings**.
Choose **Integrations** and then click **Enable** next to **GitHub**. This opens GitHub, where you'll be prompted to sign in if you have not already done so.
Your user cannot be a member of multiple GitHub orgs.
Choose the repositories QA Wolf should access.
Click **Install**. Once installed, QA Wolf can create PR comments.
### Find the QAWOLF\_API\_KEY
Open the `Workspace name` dropdown in QA Wolf and click **Workspace Settings**.
Choose **Integrations**.
Generate your **QAWOLF\_API\_KEY** by clicking the icon to the right of **API Key** under **API Access**.
### Add the QAWOLF\_API\_KEY secret
Return to GitHub, go to your repository's **Settings** tab, click **Secrets and variables**, and then **Actions**.
Type `QAWOLF_API_KEY` in **Name** and paste your API key into **Secret**.
Click **Add secret** to save.
This key allows your workflow to authenticate with QA Wolf when the Action runs.
## Add the GitHub Actions workflow
QA Wolf's official GitHub Action, [Notify QA Wolf on Deploy](https://github.com/marketplace/actions/notify-qa-wolf-on-deploy), notifies QA Wolf whenever a new deployment is ready for testing.
This must be done for every repository you want connected to CI/CD.
Open your repository on GitHub. Click the **Actions** tab.
If you already have actions, click **New workflow**.
Choose **Set up a workflow yourself**.
Copy the example below into your workflow file (usually `.github/workflows/deploy.yml`). Replace the values in `deployment-type` and `deployment-url` with your actual values.
To get the value for `deployment-type`, reach out to QA Wolf and we will provide this value for you.
```yaml theme={null}
- name: Notify QA Wolf
uses: qawolf/notify-qawolf-on-deploy-action@v1
with:
qawolf-api-key: ${{ secrets.QAWOLF_API_KEY }}
deployment-type: "staging"
deployment-url: "https://staging.example.com"
```
## Verify the integration
Push a new commit to your main branch and wait for your deployment to complete.
Open QA Wolf and confirm a new run appears under the expected environment.
# How to integrate with GitLab
Source: https://docs.qawolf.com/qawolf/GitLab-2a15b2a994fb805d9d14ff27726065ae
Connect QA Wolf to GitLab CI/CD to automatically trigger test runs when you deploy code from your repository.
If your repositories are hosted on GitLab.com or on a GitLab Enterprise Server instance with internet access, this is the recommended setup. The integration connects directly to your QA Wolf environments.
If your GitLab instance does not allow outbound HTTPS connections (for example, an air-gapped GitLab Enterprise installation), use the QA Wolf CI SDK instead.
Make sure you have:
* Admin or Maintainer access to the GitLab project.
* Access to your QA Wolf workspace.
* At least one QA Wolf environment already configured.
* A QA Wolf API key.
## Enable the GitLab integration in QA Wolf
Open the `Workspace name` dropdown in QA Wolf and click **Workspace Settings**.
Click **Integrations**, then click **Enable** next to **GitLab**. This opens GitLab, where you'll be prompted to sign in if you have not already done so. Follow the prompts to connect your GitLab account or group.
You'll be asked to provide a **Group Access Token** with the **Maintainer** role and **API** scope.
The Maintainer role is required because Developer tokens cannot create pipeline checks on protected branches.
Once connected, QA Wolf can set commit statuses and link test runs to GitLab commits.
### Find the QAWOLF\_API\_KEY
Open the `Workspace name` dropdown in QA Wolf and click **Workspace Settings**.
Choose **Integrations** and then click the icon to the right of **API Key** under **API Access**.
### Add the QAWOLF\_API\_KEY secret
Open your GitLab project.
Go to **Settings → CI/CD → Variables**.
Click **Add variable**.
Name the variable `QAWOLF_API_KEY`, paste your API key, and save.
You can also define additional variables, such as a deployment URL, if your pipeline uses them.
## Add the deploy notification to GitLab CI/CD
QA Wolf provides a public API endpoint that your GitLab pipeline can call upon successful deployment. Add a job to your `.gitlab-ci.yml` that runs after your deploy step completes successfully.
```yml expandable theme={null}
stages:
- build
- deploy
- notify
deploy:
stage: deploy
script:
- ./deploy.sh
environment:
name: staging
notify_qawolf:
stage: notify
script:
- |
curl -X POST "https://app.qawolf.com/api/webhooks/deploy_success" \
-H "Authorization: Bearer $QAWOLF_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"branch\": \"$CI_COMMIT_REF_NAME\",
\"deployment_type\": \"staging\",
\"deployment_url\": \"$QA_WOLF_DEPLOY_URL\",
\"sha\": \"$CI_COMMIT_SHA\",
\"hosting_service\": \"GitLab\"
}"
when: on_success
needs: ["deploy"]
```
This job tells QA Wolf that a new deployment is ready for testing.
Run your GitLab CI/CD pipeline to trigger a deployment.
Wait for the pipeline to complete successfully.
Open QA Wolf and confirm a new run appears under the expected environment.
## Verify the integration
Push a new commit to your main branch and wait for your deployment to complete.
Open QA Wolf and confirm a new run appears under the expected environment.
## Related
* [webhooks/deploy\_success](/qawolf/deploy-success)
* [REST API](/qawolf/rest-overview)
# Glossary
Source: https://docs.qawolf.com/qawolf/Glossary-29c5b2a994fb806d93f2ce4554a2fcfc
Definitions of key QA Wolf terms including workspaces, environments, groups, flows, runs, coverage outlines, and coverage maps.
## Workspace
A **workspace** is the top-level container in the QA Wolf platform. All environments, groups, and related settings live inside a workspace.
Use workspaces for separate applications, such as the web, iOS, and Android versions of your product. Or when different business units are responsible for completely separate test suites.
## Environment
Under each workspace is an **environment**, which corresponds to a specific version of your app, such as development, staging, or production.
Each environment can be [configured](/qawolf/Environment-settings-2cd5b2a994fb8014ba8bd7aa25f3ed0f) with separate variables (e.g., base URL, users, etc.), concurrency rates, and other settings relevant to the version of the application you're testing. Flows and tests are unique for each environment and not shared between environments.
## Flow
A **flow** is a sequence of tests that validates a user journey in your application.
Flows are the primary unit of end-to-end testing in QA Wolf. Flows can be run individually, scheduled, and tagged.
## Test
A **test** verifies a specific behavior or step in your application. Multiple tests combine to form a flow that validates a complete user journey.
**Example: QA Wolf application hierarchy**
| Item | Example |
| :---------- | :---------------------- |
| Workspace | Wolf Corp |
| Environment | Staging |
| Folder | accounts |
| Flow | happy-path-ios.flow\.js |
## Coverage outline
A **coverage outline** is the set of groups and flow stubs that define what needs to be tested in an environment. Outlining is the act of producing one: you point the agent at your application, it explores, and it proposes the groups and flows that represent the main areas and user journeys. An outline records scope — it does not contain test code until the flows are created.
## Coverage map
The **coverage map** is the visual representation of a coverage outline. It shows the flows in an environment and how they are organized into groups, so you can see the shape of your coverage and find gaps in it.
## Run
A [run](/qawolf/Run-a-flow-or-suite-2a25b2a994fb8014b164d7ad2efd5ddel) is an execution of one or more flows. Each flow is attempted up to three times per run if it fails. Runs can be started manually or invoked automatically by a schedule. The status and results of every flow in a run are recorded and reported back.
## Attempt
Within a run, a flow may be attempted multiple times. As a tester, you will occasionally have flows that flake — attempting to run a failed flow multiple times ensures that the failure is not a temporary fluke.
QA Wolf will attempt to run a flow up to three times. Each attempt has its own video, logs, and results, allowing you to isolate issues precisely.
## Schedule
A schedule defines when and how often runs occur automatically. Scheduling is limited to flows. Individual tests can be run manually for debugging, but they cannot be scheduled independently.
Use schedules to execute flows at specific times (midnight) or at recurring intervals (every hour), or when a build is deployed.
# Integrate a mobile build with GitHub Actions
Source: https://docs.qawolf.com/qawolf/Integrate-a-mobile-build-with-GitHub-Actions-2db5b2a994fb8027a901d4238b02fb8f
Use the QA Wolf GitHub Actions to upload mobile build artifacts and trigger test runs directly from your existing workflows.
## When to use GitHub Actions
This guide is for teams that use **GitHub Actions** as their CI system and want a simple, declarative way to upload mobile builds and trigger test runs without writing custom scripts. Use GitHub Actions if your repository already builds mobile artifacts in a GitHub Actions workflow and you prefer using prebuilt actions over maintaining Node.js scripts or Fastlane lanes. This guide assumes your mobile builds already run in GitHub Actions and produce a build artifact.
## Before you begin
1. Make sure your GitHub Actions workflow produces a mobile build artifact (APK, AAB, or IPA).
2. Artifact naming conventions are defined for your environments. See [Artifact naming conventions](#artifact-naming-conventions) below.
Before mobile test runs can execute, QA Wolf must enable mobile triggers for your workspace. QA Wolf will handle this and may ask you for:
* Which environments you want to test.
* Whether PR testing is enabled.
* The artifact naming conventions you are using.
* The upload and trigger method you chose.
Until this step is complete, CI jobs can upload artifacts and send deployment notifications, but mobile test runs will not start automatically.
## How GitHub Actions works with QA Wolf
The GitHub Actions integration uses two QA Wolf-provided actions:
Upload a mobile build artifact to QA Wolf.
Notify QA Wolf of a deployment event to trigger a test run.
You can upload builds without triggering runs, which is useful during initial setup or validation.
You provide the [artifact basename](#artifact-naming-conventions) when uploading. QA Wolf applies the file extension automatically based on the uploaded file.
## Add QA Wolf actions to your workflow
Add the QA Wolf actions to an existing GitHub Actions workflow that builds your mobile app. The workflow must run after the build artifact has been created.
### Find the QAWOLF\_API\_KEY
Open the `Workspace name` dropdown in QA Wolf and click **Workspace Settings**.
Choose **Integrations**.
Generate your **QAWOLF\_API\_KEY** by clicking the icon to the right of **API Key** under **API Access**.
### Add the QAWOLF\_API\_KEY secret
Open your GitHub repository and go to **Settings**.
Select **Secrets and variables → Actions**.
Add a new repository secret named `QAWOLF_API_KEY` and paste your API key.
## Artifact naming conventions
Mobile build artifacts must follow consistent naming conventions so QA Wolf can correctly associate each build with the right environment and make failures easier to diagnose.
The artifact name is used to identify:
* Which environment the build belongs to.
* Whether the build is tied to a pull request.
* Which build was used for a given test run.
### Static environments
Static environments are long-lived environments such as staging or release environments.
**Format**
```text theme={null}
-
```
**Example**
```text theme={null}
app-staging
```
Use the same basename every time a build is generated for the same environment.
### PR (ephemeral) environments
PR environments are short-lived and tied to a specific pull request. These are only relevant if PR testing is enabled.
**Format**
```text theme={null}
---pr
```
**Example**
```text theme={null}
app-myorg-myrepo-pr123
```
Including the organization, repository, and pull request number ensures each build can be traced back to the correct change and environment.
QA Wolf applies the file extension (.apk, .aab, or .ipa) automatically based on the uploaded artifact. You only need to provide the basename.
## Upload a mobile build artifact
After your workflow produces a mobile build artifact, upload it to QA Wolf using the upload action.
```yaml theme={null}
- name: Upload mobile build to QA Wolf
id: upload-run-input
uses: qawolf/upload-run-inputs-executable-action@v1
with:
qawolf-api-key: ${{ secrets.QAWOLF_API_KEY }}
input-file-path: ./path/to/build.apk
executable-file-basename: app-staging
```
If this step completes successfully, the artifact is uploaded and available for test runs.
```yaml expandable highlight={1-25} theme={null}
name: Deploy and Notify QA Wolf
on: pull_request
jobs:
...
notify:
needs: deploy-preview-environment
name: Trigger QA Wolf PR testing
runs-on: ubuntu-latest
steps:
...
# Upload the run input file
- name: Upload Run Input
id: upload-run-inputs-executable
uses: qawolf/upload-run-inputs-executable-action@v1
with:
qawolf-api-key: "${{ secrets.QAWOLF_API_KEY }}"
input-file-path: "path/to/file.apk"
- name: Notify QA Wolf of deployment
uses: qawolf/notify-qawolf-on-deploy-action@v1
env:
...
# Use the output in the RUN_INPUT_PATH environmental variable
RUN_INPUT_PATH: "${{ steps.upload-run-inputs-executable.outputs.destination-file-path }}"
...
with: ...
```
## Trigger a test run
After uploading the artifact, notify QA Wolf that a new deployment is ready for testing.
```yaml theme={null}
- name: Notify QA Wolf of deployment
uses: qawolf/notify-qawolf-on-deploy-action@v1
with:
qawolf-api-key: ${{ secrets.QAWOLF_API_KEY }}
deployment-type: android_app
variables: >
{ "RUN_INPUT_PATH": "/home/wolf/run-inputs-executables/${{ steps.upload-run-input.outputs.destination-file-path }}" }
```
The deployment type and environment key must match the values configured by QA Wolf for your workspace.
If mobile triggers have not yet been enabled, this step will complete without starting a test run.
## Verify the integration
Run the GitHub Actions workflow.
Verify that the artifact upload step completes successfully.
Confirm that the deployment notification step runs without errors.
Once mobile triggers are enabled, check the **Runs** tab for the triggered test run.
## Troubleshooting and common issues
* **If uploads succeed but no runs start:** Mobile triggers may not yet be enabled. Contact QA Wolf to complete platform configuration.
* **If the artifact is not found during execution:** Verify that the artifact basename matches your naming conventions and that the destination file path from the upload step is used to trigger the run.
* **If you see authentication errors:** Verify that **QAWOLF\_API\_KEY** is configured correctly as a GitHub Actions secret.
* **If the workflow fails before the QA Wolf steps run:** Verify that the mobile build step completes successfully and produces the expected artifact.
# How to integrate a non-native bug tracker with webhooks
Source: https://docs.qawolf.com/qawolf/Integrate-with-webhooks-29c5b2a994fb818d9682eb32ff11feff
If we don't support your bug tracker through a native integration, you can still use webhooks to integrate.
We know that each implementation will vary. Please reach out and let us know if you need any support.
### Set up the integration
Open the `Workspace name` dropdown and click **Workspace settings**.
Navigate to **Integrations**.
Click **Enable** next to Webhook.
Click **Connect** in the modal.
Define a webhook title.
Define the webhook URL and enable "Notify when bug report is opened." This is the location of the webhook used by your bug tracker.
When an issue is created, your endpoint will receive `POST` requests with a JSON body (`Content-Type: application/json`). Here's the expected structure and an example payload.
#### Expected JSON Body Fields
The primary data is in the request body:
* `id` (`string`): Unique bug report ID.
* `title` (`string`): Bug report title.
* `description` (`string`): Detailed description (may contain newlines/links).
* `url` (`string`): Link to view the bug report in QA Wolf.
* `event` (`string`): `bug-report-opened`.
#### Example Payload
This example shows the full request structure your endpoint will receive:
```json JSON theme={null}
{
"id": "",
"title": "Bug Report Title",
"description": "🐞\n\nBug report description:\nYou can see it here.\n\nBug report:\nhttps://app.qawolf.com/test-client/bug-reports/...\n\nAffected workflows:\n\n\nIf you are aware of this bug you can set the priority to low which will prevent it from causing a run failure.",
"url": "https://app.qawolf.com/bug-reports/...",
"event": "bug-report-opened"
}
```
#### Authentication
If username and password are set, those will be used for basic HTTP authorization when performing the `POST` request against your webhook.
Enable the "Notify when bug report is closed" toggle:
Make your webhook respond to the `POST` request with a payload containing these fields:
* `externalId` (`string`): ID of the issue for the QA Wolf bug report in your issue tracker. It will be used to identify issues that will need to be updated in the future.
* `humanId` (`string`): Human-friendly issue ID that will be used on QA Wolf UI, Slack messages and other integrations.
* `url` (`string`): Link to the issue.
**When a bug report is closed**, your endpoint will receive `POST` requests with a JSON body (`Content-Type: application/json`). Here's the expected structure and an example payload.
#### Expected JSON Body Fields
The primary data is in the request body:
* `externalId` (`string`): The ID of the issue associated with the closed bug report.
* `event` (`string`): `bug-report-closed`.
# Integrating with Microsoft Teams
Source: https://docs.qawolf.com/qawolf/Integrating-with-microsoft-teams
Set up Microsoft Teams to receive QA Wolf notifications by configuring external access and shared channels.
## Completion checklist
In the [Teams Admin Center](https://admin.teams.microsoft.com/):
Teams policies > Join external shared channels to On
Users > External Access > Ensure QA Wolf is not blocked
In the [Microsoft Entra admin center](https://entra.microsoft.com/) (**as a Security Administrator**):
Add QA Wolf's tenant ID - `3d41cc11-5772-457c-aad2-16b3924b22d0`
Verify access settings do not restrict access to or from QA Wolf, or adjust accordingly
Share your Microsoft Entra tenant ID with QA Wolf
### Enable shared channels in Teams
Shared channels are **enabled by default** in Teams.
QA Wolf provides a shared channel for your Teams integration. To use it, your organization must have shared channels enabled.
In the [Microsoft Teams admin center](https://admin.teams.microsoft.com/), expand **Teams** in the left navigation bar and choose **Teams policies**.
Click the policy you want to confirm or change.
Under *Shared channels settings*, turn on **Join external shared channels** to allow collaboration with other orgs.
Click **Apply** to save your changes.
### Enable external access
You must enable external access so QA Wolf team members can join your **Teams meetings** and your team can see QA Wolf's presence status (online/offline/away) in shared channels.
In the [Teams admin center](https://admin.teams.microsoft.com/), expand **Users** in the left navigation bar and choose **External access**.
Choose the appropriate option for allowing **Teams and Skype for Business users in external organizations**.
**Do not** select **Block all external domains** — this setting prevents the integration from working.Allowing **qawolf.com** under **Allow only specific external domains** will ensure QA Wolf team members can participate while all other external domains remain blocked.
### Enable cross-tenant access in Microsoft Entra
To use a shared channel with QA Wolf, you must allow our Microsoft Entra tenant by adding QA Wolf as an allowed organization using our Microsoft Entra tenant ID.
As a **Security Administrator**, sign in to the [Microsoft Entra admin center](https://entra.microsoft.com/).
Choose **External Identities** from the left navigation bar, then choose **Cross-tenant access settings**.
On the **Organizational settings** tab, click **Add organization**.
In the **Add Organization** drawer on the right, type QA Wolf's tenant ID: `3d41cc11-5772-457c-aad2-16b3924b22d0` and click **Add** at the bottom.
If your default access settings restrict access, update your inbound and outbound B2B direct connect settings to allow QA Wolf.
Still under Organizational Settings, you should see QA Wolf listed under **organizations found**.
#### Check access
Click on the Configured link underneath Inbound access or Outbound access.
Click the B2B direct connect tab.
Select the Customize settings radio button.
On the **External users and groups** tab:
* For **Access status**, select *Allow access*
* For **Applies to**, select *All QA Wolf users and groups*
After updating these settings, verify that your default access configuration no longer blocks access to or from QA Wolf.
Shared channel collaboration will not work if the default settings continue to block external tenants.
We use shared channels so customers can access them without switching tenants, and B2B Direct Connect must be used to invite or join them. **B2B Collaboration** will not work (see [*MS doc*](https://learn.microsoft.com/en-us/microsoft-365/solutions/collaborate-teams-direct-connect?view=o365-worldwide) for details).
| B2B Collaboration | B2B Direct Connect |
| ------------------------------- | ----------------------------------------- |
| Applies to guest accounts | Enables direct shared channel invitations |
| Requires tenant switching | Seamless access from own tenant |
| Only supports standard channels | Fully supports shared channels |
#### Complete setup
We need your **Microsoft Entra tenant ID** and **domain name** so we can configure cross-tenant access on our side. Customers can locate their Tenant ID and domain on the [Microsoft Entra dashboard](https://entra.microsoft.com/#home).
# Reviewing run results
Source: https://docs.qawolf.com/qawolf/Interpret-run-results-2d55b2a994fb80199754e4b2d3ce0318
Navigate the Runs tab to review pass rates, failure counts, bug reports, and maintenance status for each run.
## Overview
For each run, the list shows:
* The name of the schedule that triggered the run
* — the number of fully passing attempts over total attempts
* — the number of flows still requiring investigation
* — the number of open bug reports linked to flows in the run
* — the number of open maintenance reports linked to flows in the run
* Total wall clock time of the run, including all attempts
## Identify environments that need attention
The icon next to each environment name indicates its current status.
| Status | Meaning |
| :----------------------------------------- | :-------------------------------------------------------------------------- |
| Ready | All flows passing |
| Needs investigation | One or more flows failed, or a medium or low priority bug has been reported |
| Failed | A high severity bug has been reported |
You can see environment status in two places:
* On the **Runs** tab, next to each environment name in the list on the left
* On the **Flows** tab, in the environment name dropdown
## Review a run
From the **Runs** tab, select the environment you want to investigate.
Runs are grouped into three categories:
* **Investigating** — flows in the run failed and have not been marked *Do Not Investigate*
* **Completed** — all flows have passed or been marked *Do Not Investigate*
* **Canceled** — the run was canceled mid-execution by a subsequent run matching the same branch and environment
Click the run you want to investigate.
## Sort and filter runs
The default view shows past runs sorted by date in descending order. Click the icon to filter by:
* **Created date** — *1 week ago*, *1 month ago*, or *1 year ago*
* **Trigger** — *Hourly* or *Daily*
# How to integrate with Jira
Source: https://docs.qawolf.com/qawolf/Jira-29c5b2a994fb81d18a87f23808479de0
Connect QA Wolf to Jira to automatically create, sync, and close issue tickets from QA Wolf bug reports.
## Overview
* **Automatic issue handling.** Creates Jira issues automatically when new bug reports are generated in QA Wolf, keeping Jira up to date without manual entry.
Context added in Jira is not written back to QA Wolf.
* **Synchronization.** Closing a bug report in QA Wolf will close the linked Jira ticket. QA Wolf also performs regular bug revalidations to ensure accuracy.
Closing a Jira ticket should automatically update QA Wolf. Contact us if that doesn't happen so we can help you troubleshoot.
* **Test creation requests.** Add a **QA Wolf** label to a Jira ticket to automatically create a coverage request in QA Wolf.
* **Custom configuration.** Specify the project, assignee, and issue status. Currently, each integration setup supports one project, one assignee, and one status type.
* **Custom fields.** Add custom Jira fields to bug reports if your Jira project uses them. Custom fields apply globally across all tickets created through QA Wolf, not per workflow.
**Full Service Customers:** Need help setting up your JIRA integration? The QA Wolf team can configure this integration on your behalf. To get started, invite us as a user in your JIRA workspace using the following email convention: qa+\[yourcompanyname]@qawolf.com (e.g. [qa+acme@qawolf.com](mailto:qa+acme@qawolf.com)).
## Set up the integration
Open the `Workspace name` dropdown and click **Workspace settings**.
Navigate to **Integrations**.
Click **Enable** next to Jira. The **Sync bug reports to Jira** modal appears.
Click **Connect**.
Select the Jira team you want to link to your QA Wolf team and click **Accept** to authorize the connection.
Choose your Jira **Project**, **Issue Type**, and **Assignee**, then enable:
* **Create bug issue report**
* **Close bug issue report**
This ensures bugs are created and closed automatically across both systems.
If there are any custom fields you need us to configure, such as the issue description, please reach out and let us know. We can configure those for you.
# How to integrate with Linear
Source: https://docs.qawolf.com/qawolf/Linear-29c5b2a994fb81ce85ded6d005d5e7ab
Connect QA Wolf to Linear to automatically create and sync bug report issues with your Linear team.
Open the `Workspace name` dropdown and click **Workspace settings**.
Navigate to **Integrations**.
Click **Enable** next to **Linear**. A modal opens.
Click **Connect**.
Enter the **Team name** exactly as in Linear.
Team name is case sensitive.
Enable the **Create bug report issue** toggle and optionally set **Label UUID** and **Assignee UUID** to set a default label and assignee for newly created issues.
# File, review & edit bug reports
Source: https://docs.qawolf.com/qawolf/Manage-bug-reports-2c75b2a994fb80308ddcc7cc60703c2f
Create, review, and resolve bug reports in QA Wolf to track confirmed product defects linked to failing flows.
## Overview
Bug reports track confirmed product defects, specifically unexpected application behavior rather than test or setup issues.
Bug reports are automatically associated with flows when opened from the Investigation view. Multiple flows can be linked to the same bug report when they expose the same issue. A bug report automatically closes once all associated flows are passing.
Once the test issues are fixed, you can attempt to clear the bug report by running all associated flows directly from the report. Any flows that pass are removed from the report and resume running according to their schedules. The bug report remains open until all associated flows have passing runs.
**Best practices**
* Use bug reports to track and resolve underlying product issues.
* Allow flows with known bugs to continue running to detect when the issue is fixed.
* Avoid quarantining flows for known bugs unless execution is causing operational issues.
## Get to know the bug report tab
You can find all bug reports for your workspace in the **Bugs** tab. This page lists both open and closed reports and allows you to create new ones or open existing ones for editing.
* Use the **Open** and **Closed** tabs to switch between active and resolved reports.
* Use the **Search** bar to filter by report name. Use the icon to filter by report **Priority** or **Creator is me**.
## File a bug report
In the Investigation view, select the failing flow.
**Under Diagnose**, click **Report a bug**.
You can also find **Diagnose** by hovering over the failing flow to show the icon, then clicking it to show the icon. **Diagnose** will appear below the Flow list.
A drawer appears, displaying a pre-populated form in QA Wolf's standard bug-report format.
Click the **Name** field to open a dropdown of existing bug reports. As you type, the list autocompletes to help you find a match.
a. If you need a new report, type its name and press **Enter**, or choose the top option in the dropdown.
After selecting a name, fill out the remaining form. The form contains the following fields:
a. **Description:** Provide a clear summary of the issue. This field includes a built-in template to help you capture the most important details—such as expected behavior, actual behavior, and steps to reproduce. You can also include a link to a cloud-hosted video (e.g., Loom) in the **Video** field to illustrate the problem.
b. **Priority:** Set the bug's urgency. **Urgent** and **High** represent blocking priorities and will affect the overall environment status.
c. **Environment (read-only):** Indicates the environment where you observed the bug.
d. **Affected flows (read-only):** Lists the flows impacted by this bug.
**Flows with open bugs are still included in scheduled runs.**
* If any bug associated with a flow has a blocking **Priority** (*Urgent* or *High*) and at least one flow in the run fails all three attempts, the run ends in a *Failed* status. The environment's status will appear with the icon on both the **Flows** and **Runs** tabs.
* If the flow has only non-blocking bugs, and at least one flow in the run fails all three attempts, the run ends as *Needs investigation*. The environment's status will appear with the icon on both the **Flows** and **Runs** tabs.
You can also file bug reports from the **Bugs** tab by clicking **Report bug** to open the form for filing a new bug. However, bugs you open from this method will not be associated with any flows. To associate this bug with flows, you must follow the **Diagnose** steps above.
## Review or edit an existing bug report
* From the **Bugs** tab, find the bug report you'd like to open and click on it.
* The only field you can change here is the **Priority** field. All other fields are read-only.
## Clear bug reports
A bug report should close automatically on the next scheduled run once you apply the fix. However, you can also manually run all flows associated with a given bug; if they pass, the bug report will close automatically.
Open the **Bugs** tab and click the report you'd like to close.
Click **Run all flows**. This opens the **Runs** tab in a separate browser tab and starts a manual run of all flows associated with the bug report. Any flows that pass will be removed from the bug report. If all flows pass, the bug report will close automatically.
## Update the diagnosis of a test failure
Sometimes, a run will still fail after an application fix is deployed. This can happen when the original issue masked a different problem, or when the fix changes behavior in a way that affects the test.
If this happens after a developer ships a fix:
Open the failed run under **Needs investigation** to enter the **Investigation view**.
Re-run the failing flow to confirm whether the failure still occurs.
Review the failure evidence:
* If the flow already has a bug filed and the failure points to a new application issue upstream of the existing one, click **Update diagnosis**, then select **Report a bug** to file a new bug report.
* If the flow already has a bug filed but the failure now indicates the test needs to be updated, click **Update diagnosis**, then select **Report as needing maintenance**.
This keeps diagnoses accurate as failures evolve, without tying the new failure too closely to the original issue.
# Managing maintenance reports
Source: https://docs.qawolf.com/qawolf/Manage-maintenance-reports-2d55b2a994fb80728909f48b1023f7fa
File maintenance reports to quarantine failing flows caused by test issues, and resume them after fixes are applied.
Maintenance reports are used when a failure is caused by incorrect, outdated, or fragile test logic rather than an issue in the application under test.
Maintenance reports cannot be created directly from the Maintenance tab. They are created from the Investigation view when reviewing a failing flow.
Minor test fixes should be made quickly when possible. File a maintenance report when a fix will not be completed before the next deployment or scheduled run, or when the change requires more involved updates to the test, such as refactoring.
Maintenance reports are linked to one or more flows. While a maintenance report is open, those flows are skipped in scheduled runs. When the flows pass in a manual run, the maintenance report automatically closes, and the flows are included in schedules again.
## Creating a maintenance report
Use a maintenance report for any failing flow that will take more than about 15 minutes to fix (a guideline, not a rule), and quarantine the flow.
From the **Runs** tab, select the environment from the **Environments** list.
In the center panel, select a run from **Investigating**.
In the Investigation view, select the failing flow.
In the center panel, click **Diagnose**.
Click **Report as needing maintenance** to open the drawer for creating a maintenance report.
Click the **Name** field to open a dropdown of existing maintenance reports. As you type, the list autocompletes to help you find a match. Selecting an existing report will add your flow to that report in the **Assign flows** field below.
a. If you need a new report, type its name and press **Enter**, or choose the top option in the dropdown.
After selecting a name, fill out the remaining form.
a. **Description:** Provide a short explanation of why the flow(s) need maintenance. Use this to outline the issue, what's outdated or broken, and any relevant context.
b. **Effort:** Estimate the effort required to update or repair the flow(s).
c. **Priority:** Set the urgency for maintenance. Flows associated with higher priority reports appear more prominently in the environment status.
d. **Reason for Maintenance:** Select the appropriate reason from the predefined list—these standard options help categorize and report on maintenance work consistently.
e. **Environment (read-only):** This displays the environment affected by the maintenance. The system automatically populates the field.
f. **Assigned Flows (read-only):** This displays the flows included in the maintenance report.
g. New reports automatically include only the flow where you initiated the report.
Submitting this change removes the flow from all runs.
## Viewing a maintenance report
* To view all maintenance reports, open the **Maintenance** tab.
* The tab includes two subtabs:
1. Filter by readiness status.
2. Search by report name.
## Closing a maintenance report
A maintenance report must be closed manually. When you fix a test with an open maintenance report:
Open the **Maintenance** tab, then click the maintenance report associated with the flow you fixed.
Click **Run all flows**. This opens the Runs tab in a separate browser tab and starts a manual run of all flows associated with the maintenance report. Any flows that pass will be removed from the maintenance report. If all flows pass, the maintenance report will close automatically.
# Create a coverage outline
Source: https://docs.qawolf.com/qawolf/Map-your-app-s-workflows-2a45b2a994fb80ca8a87c8fdc2a443fc
Use the outlining agent to build and maintain a coverage outline of your application's user journeys.
A coverage outline defines the scope required to test an application end-to-end. Without visibility into an application's surface area — key user paths, important states, and system boundaries — it's difficult to know whether coverage is complete or where it isn't.
The outlining agent builds that outline by exploring your application and generating the groups and flow stubs that represent its main areas and user journeys. The result is rendered as a coverage map, the visual view of what you cover. As features change or new ones are added, you can run the agent again to extend the outline.
## Start an outlining session
From the **Flows** tab, hover over the left sidebar to open the drawer, then click the icon next to **Create flow** and click **Outline flows**.
**Outline flows** is the supported way to start outlining. The other ways of creating a flow can be steered into outlining as well, but only this one starts the agent in outlining mode.
The agent chat opens with three starting prompts: **Outline a test plan**, **Identify key flows**, and **Start exploring**. Click one, or type your own instructions — for example, *"Outline a test plan for `https://yourapp.com`"*. You can specify a base URL, describe a specific journey, or ask the agent to focus on a particular feature area.
Provide real-time guidance as the agent works. If the agent encounters a login prompt or needs clarification, it will ask you directly in the chat. You can respond at any time to redirect or refine the exploration.
To cover more of an area the agent has already been through, type instructions into the chat or click **Find more flows** at the bottom of the chat panel.
## Outline from a file
Click in the chat input to attach a file, then use one of the following prompts depending on what you want.
Supported file types: CSV, JSON, Markdown, plain text, and YAML.
### Generate flows from a file
Parses the file and creates the group structure and flow stubs immediately, without exploring the app. Supports most file and image types.
Prompt: *"Upload the flows from this file."*
### Outline your app using a file as a guide
Uses the file as a reference while the agent actively explores your app.
Navigate to your app in the browser panel, or include the URL and login credentials in your message.
Prompt: *"Outline all the flows in this file."*
When the session ends, the agent produces a CSV with each test case's coverage status, the flow that covers it, and the reason for any rejections.
## View the coverage map
Select **All flows** in the left sidebar for the coverage map of every flow in the environment. The sidebar sits collapsed to a strip of icons — hover over it to expand it.
## Get better results
Outlining works best when you give the agent clear context upfront and steer it away from areas where it's likely to struggle.
### Write a strong opening prompt
The agent needs enough context to navigate your app and generate meaningful flows. A good opening prompt includes:
* The URL to explore
* Login credentials
* The application name (used to name the group the agent creates)
* Any specific task or scope
If every session needs the same background about your app, put it in a [SKILL.md](/qawolf/reusable-ai-knowledge) rather than repeating it in the prompt.
### Set a flow target
Outlining defaults to around 200 flows. If you want more coverage, specify a number explicitly — telling it to find "as many as possible" tends to stop at the default anyway.
```text theme={null}
Outline up to 500 flows for this application.
```
### Skip auth flows when they cause problems
If your app uses MFA or has a complex login flow, the agent can get stuck trying to test authentication before it can explore anything else. Tell it to bypass this:
```text theme={null}
Just log in first — don't test auth flows.
```
### Use manual browsing to supplement AI exploration
You don't have to rely entirely on the agent to find flows on its own. You can click through your app manually while outlining, and it will suggest flows based on what it observes. This is useful for:
* Apps where automated exploration struggles (complex forms, file uploads, role-based access)
* Specific flows you want to make sure are captured
* Verifying that the agent understood a flow correctly
To use this: stop the AI, click through the flow yourself, and look for the **Save as flows** button that appears.
### Hand off flows that need more control
Outlining is designed for breadth — capturing an app's testable surface quickly. For flows that need individual attention, use [AI flow creation](/qawolf/Automate-flows-using-AI-2d35b2a994fb8084b524cfc9aca7c358) to turn them into tests one at a time with more targeted prompts.
A common pattern: outline the app to get broad coverage, then use AI flow creation to build out the flows that need more precision.
# Integrate a mobile build with the QA Wolf SDK
Source: https://docs.qawolf.com/qawolf/Mobile-build-with-the-QA-Wolf-SDK-2db5b2a994fb80cdbd1ec31513f2cc15
Upload mobile build artifacts and trigger test runs from any Node.js-based CI system using the QA Wolf CI SDK.
Use this guide if your CI system supports Node.js but is not GitHub Actions or Fastlane.
Make sure you have:
* A CI pipeline that produces a mobile build artifact (APK, AAB, or IPA).
* Node.js 18 or later available in your CI environment.
* Admin access to your CI system's secret or environment variable storage.
* A QA Wolf API key.
Before mobile test runs can execute, QA Wolf must enable mobile triggers for your workspace. QA Wolf will handle this and may ask you for:
* Which environments you want to test.
* Whether PR testing is enabled.
* The artifact naming conventions you are using.
* The upload and trigger method you chose.
Until this step is complete, CI jobs can upload artifacts and send deployment notifications, but mobile test runs will not start automatically.
## Install the CI SDK
Install the SDK in your CI job:
```bash theme={null}
npm install @qawolf/ci-sdk
```
## Find the QAWOLF\_API\_KEY
Open the **Workspace name** dropdown in QA Wolf and click **Workspace Settings**.
Choose **Integrations**.
Generate your **QAWOLF\_API\_KEY** by clicking the icon to the right of **API Key** under **API Access**.
Make sure the job has access to the `QAWOLF_API_KEY` environment variable.
## Artifact naming conventions
Mobile build artifacts must follow consistent naming conventions so QA Wolf can correctly associate each build with the right environment and make failures easier to diagnose.
The artifact name is used to identify:
* Which environment the build belongs to.
* Whether the build is tied to a pull request.
* Which build was used for a given test run.
### Static environments
Static environments are long-lived environments such as staging or release environments.
**Format**
```text theme={null}
-
```
**Example**
```text theme={null}
app-staging
```
Use the same basename every time a build is generated for the same environment.
### PR (ephemeral) environments
PR environments are short-lived and tied to a specific pull request. These are only relevant if PR testing is enabled.
**Format**
```text theme={null}
---pr
```
**Example**
```text theme={null}
app-myorg-myrepo-pr123
```
Including the organization, repository, and pull request number ensures each build can be traced back to the correct change and environment.
QA Wolf applies the file extension (.apk, .aab, or .ipa) automatically based on the uploaded artifact. You only need to provide the basename.
## Upload a mobile build artifact
After your CI pipeline produces a mobile build artifact, upload it to QA Wolf using the SDK.
**Minimal example**
```js Javascript expandable theme={null}
import { makeQaWolfSdk } from "@qawolf/ci-sdk";
import fs from "fs/promises";
const sdk = makeQaWolfSdk({
apiKey: process.env.QAWOLF_API_KEY,
});
async function uploadBuild() {
const signedUrl = await sdk.generateSignedUrlForRunInputsExecutablesStorage({
destinationFilePath: "app-staging",
});
const fileBuffer = await fs.readFile("./path/to/build.apk");
await fetch(signedUrl.uploadUrl, {
method: "PUT",
body: fileBuffer,
headers: {
"Content-Type": "application/octet-stream",
},
});
return `/home/wolf/run-inputs-executables/${signedUrl.playgroundFileLocation}`;
}
const executablePath = await uploadBuild();
```
If this step completes successfully, the artifact is uploaded and available for test runs.
**Full implementation**
The following example includes both artifact upload and deploy notification in a single script.
```js Javascript expandable theme={null}
import { type DeployConfig, makeQaWolfSdk } from "@qawolf/ci-sdk";
import fs from "fs/promises";
import path from "path";
const { generateSignedUrlForRunInputsExecutablesStorage, attemptNotifyDeploy } =
makeQaWolfSdk({
apiKey: process.env.QAWOLF_API_KEY,
});
(async () => {
const playgroundFileLocation = await uploadRunArtifact("/FileLocation");
if (playgroundFileLocation) {
const deployConfig: DeployConfig = {
branch: undefined,
commitUrl: undefined,
deduplicationKey: undefined,
deploymentType: undefined,
deploymentUrl: undefined,
ephemeralEnvironment: undefined,
hostingService: undefined,
sha: undefined,
variables: {
RUN_INPUT_PATH: playgroundFileLocation,
},
};
const result = await attemptNotifyDeploy(deployConfig);
if (result.outcome !== "success") {
process.exit(1);
}
const runId = result.runId;
}
})();
async function uploadRunArtifact(filePath: string): Promise {
const fileName = path.basename(filePath);
const signedUrlResponse = await generateSignedUrlForRunInputsExecutablesStorage({
destinationFilePath: fileName,
});
if (
signedUrlResponse?.success &&
signedUrlResponse.playgroundFileLocation &&
signedUrlResponse.uploadUrl
) {
const fileBuffer = await fs.readFile(filePath);
const url = signedUrlResponse.uploadUrl;
try {
const response = await fetch(url, {
method: "PUT",
body: fileBuffer,
headers: {
"Content-Type": "application/octet-stream",
},
});
if (!response.ok) {
return "";
}
} catch (error) {
return "";
}
return signedUrlResponse.playgroundFileLocation;
}
return "";
}
```
## Trigger a test run
After uploading the artifact, notify QA Wolf that a new deployment is ready for testing.
```javascript theme={null}
await sdk.attemptNotifyDeploy({
deploymentType: "android_app",
variables: {
RUN_INPUT_PATH: executablePath,
},
});
```
The deployment type and environment key must match the values configured by QA Wolf for your workspace.
If mobile triggers have not yet been enabled, this step will complete without starting a test run.
## Verify the integration
Run the CI job.
Verify that the artifact upload completes successfully.
Confirm that the deployment notification step runs without errors.
Once mobile triggers are enabled, check the **Runs** tab for the test run that was triggered.
## Troubleshooting and common issues
* **If uploads succeed but no runs start:** Mobile triggers may not yet be enabled. Contact QA Wolf to complete platform configuration.
* **If the artifact is not found during execution:** Verify that the artifact basename matches your naming conventions, and the returned path is used when triggering the run.
* **If you see authentication errors:** Verify that **QAWOLF\_API\_KEY** is configured correctly in your CI environment.
* **If you encounter Node.js errors:** Ensure Node.js 18 or later is available in the CI job.
## Related
* [`@qawolf/ci-sdk`](/qawolf/libraries/ci-sdk/api-reference)
* [CI greenlight](/qawolf/v0-ci-greenlight)
# OpenVPN integration
Source: https://docs.qawolf.com/qawolf/OpenVPN-integration-29f5b2a994fb80e8b2e4cea94fa15bf2
Configure an OpenVPN connection so QA Wolf can access your application behind a private network.
Provide QA Wolf with your .ovpn client configuration file. Refer to your VPN provider's documentation for instructions on generating or downloading this file.
Configure your firewall to allow inbound VPN connections to your OpenVPN server from QA Wolf's static IP range:
* **Port:** UDP 1194 (default; configurable)
* **Source IPs:** `199.4.212.0/23`
# Ordering flows
Source: https://docs.qawolf.com/qawolf/Ordering-flows-29c5b2a994fb80789f3fea32feae8f92-copied-1
Use Run Rules to control the order that dependent flows run in.
Run Rules let you control the sequence flows run in. Each rule defines one **before** set and one **after** set of flows. You can select flows by name, group, or tag.
The order flows run in doesn't always matter, but when it does — for example, when you want quick sanity checks to complete before longer or more complex flows — Run Rules give you that control.
## Create a Run Rule
From the **Runs** tab, click **Rules** under **Scheduled**.
Click **Create Rule** to open a drawer.
Define which flows, groups, or tags should run before and after the others.
See [Use tags effectively](/qawolf/Use-tags-effectively-2c35b2a994fb809abbd7ea9402738bdb) if you want to use tags to select flows.
Toggle **Fail workflows if any priors fail** to automatically skip downstream flows when an earlier flow fails. This is enabled by default.
Use this when failures in an earlier set of flows — for example, sanity tests — make later flows likely to fail for the same reason. Skipping those later flows reduces noise, shortens run time, and keeps the failure list focused on the root cause.
Click **Create Rule** to save.
Only **active** flows execute in a run. Drafts are ignored even if they're part of a rule.
To run flows in a sequence like **A → B → C**, create two rules: run **A** before **B**, and run **B** before **C**.
The UI prevents most circular setups, but it's good practice to double-check your before and after sets.
## Manage Run Rules
### See all rules
Open the **Runs** tab and click **Rules** from the lower left.
### Edit a rule
Click the icon to the right of the rule, then click **Edit**. A drawer opens where you can edit your rule.
# Setting up PR testing
Source: https://docs.qawolf.com/qawolf/PR-testing-for-GitHub-Integrations-2db5b2a994fb80a0aa94f3297d1f60c3
Run QA Wolf tests against preview environments on pull requests and report results as GitHub status checks.
This guide is for QA Wolf full service customers. If you're unsure whether this applies to you, contact your QA Wolf team.
## Configure basic PR testing
Go to Workspace Settings -> Integrations to enable and authorize the GitHub integration.
Choose one of the following methods to tell QA Wolf when a new preview environment is ready to be tested.
**GitHub Deployments**
If your team uses a hosting platform that integrates with GitHub — like Vercel — your deployments are already being reported to GitHub via the [GitHub Deployments API](https://docs.github.com/en/rest/deployments/deployments). QA Wolf listens for those deployment records and automatically acts when one is marked as successful. No additional configuration is required.
**GitHub Action**
Configure the [Notify QA Wolf on Deploy](https://github.com/marketplace/actions/notify-qa-wolf-on-deploy) GitHub Action to tell us when a new preview environment is ready to be tested.
**SDK**
If GitHub Actions aren't an option, the `@qawolf/ci-sdk` npm package offers a flexible alternative. See the [npm package documentation](https://www.npmjs.com/package/@qawolf/ci-sdk) for setup details.
QA Wolf will configure a trigger that matches your preview deployments. Reach out to your QA Wolf team to get this set up.
***
## Advanced: Merge queue setup
This section covers an optional flow for teams using GitHub's merge queue. Some customers run QA Wolf in the merge queue as a required final gate; others avoid it when the latency or complexity outweighs the confidence gain.
### Add a default passing PR check
Add a GitHub Actions workflow that creates a passing **QA Wolf Test Results** check when a PR is opened or updated. This prevents pull requests from being blocked before the merge queue run happens.
```yaml Create .github/workflows/qawolf-pr-check.yml: expandable theme={null}
name: Create QA Wolf PR check
on:
pull_request:
types: [opened, reopened, synchronize]
jobs:
qawolf_check_creation:
runs-on: ubuntu-latest
permissions:
checks: write
pull-requests: read
steps:
- name: Create passing QA Wolf check
uses: actions/github-script@v6
with:
script: |
await github.rest.checks.create({
owner: context.repo.owner,
repo: context.repo.repo,
name: 'QA Wolf Test Results',
head_sha: context.payload.pull_request.head.sha,
status: 'completed',
conclusion: 'success',
output: {
title: 'QA Wolf Test Results',
summary: 'QA Wolf tests will run in the merge queue.'
}
});
```
### Trigger QA Wolf tests in the merge queue
Add a GitHub Actions workflow that notifies QA Wolf only during merge queue execution.
```yaml Create .github/workflows/qawolf-merge-queue.yml: expandable theme={null}
name: Test preview environment
on:
merge_group:
jobs:
test-preview-environment:
name: Trigger QA Wolf tests
runs-on: ubuntu-latest
needs:
- wait-for-preview-environment
steps:
- name: Notify QA Wolf of deployment
uses: qawolf/notify-qawolf-on-deploy-action@v1
with:
qawolf-api-key: "${{ secrets.QAWOLF_API_KEY }}"
deployment-url: "${{ env.PREVIEW_URL }}"
deployment-type: "provided-by-qawolf"
deduplication-key: "${{ github.ref }}"
```
### What this workflow assumes
* Your CI pipeline already creates a preview environment per pull request.
* The preview environment URL is available as `PREVIEW_URL` when this job runs.
* A prior job (such as `wait-for-preview-environment`) ensures the preview environment is fully deployed and reachable before QA Wolf is notified.
### Require QA Wolf tests before merge
In the left sidebar, do one of the following:
* Click **Rules** (if your repo uses GitHub's new rulesets), or
* Click **Branches** (for classic branch protection rules).
## Verify your PR testing setup
On the pull request page, confirm that a check named **QA Wolf Test Results** appears in the checks section and initially shows as passing.
When the PR enters the merge queue, GitHub will trigger the merge-queue workflow.
In the **QA Wolf app**, go to the **Runs** tab and verify that a new run starts for the preview environment associated with the pull request.
Back in **GitHub**, watch the **QA Wolf Test Results** check update from its placeholder state to the final pass or fail result. The merge completes only after this check finishes successfully.
# Setting up PR testing
Source: https://docs.qawolf.com/qawolf/PR-testing-for-GitLab-Integrations-2db5b2a994fb8075b13dd9d5c2e6be8c
Run QA Wolf tests against preview environments on merge requests and report results back to GitLab.
This guide is for QA Wolf full service customers. If you're unsure whether this applies to you, contact your QA Wolf team.
## Configure basic PR testing
Configure the GitLab integration in your team's settings page.
Choose one of the following methods to tell QA Wolf when a new preview environment is ready to be tested.
**GitLab CI job**
Add a `notify_qawolf` job to your `.gitlab-ci.yml` that runs only for merge request pipelines, after the preview environment is deployed and reachable.
```yaml expandable theme={null}
stages:
- deploy
- test
notify_qawolf:
stage: test
image: node:latest
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
needs:
- deploy_preview
script:
- npm install @qawolf/ci-sdk
- |
node -e "
const { makeQaWolfSdk } = require('@qawolf/ci-sdk');
const { attemptNotifyDeploy } = makeQaWolfSdk({ apiKey: process.env.QAWOLF_API_KEY });
attemptNotifyDeploy({
sha: process.env.CI_COMMIT_SHA,
branch: process.env.CI_COMMIT_REF_NAME,
deploymentType: 'provided-by-qawolf',
deploymentUrl: process.env.PREVIEW_URL,
hostingService: 'GitLab',
}).then(result => {
if (result.outcome !== 'success') process.exit(1);
});
"
```
**curl**
If Node isn't available in your pipeline, you can notify QA Wolf using a raw HTTP call instead.
```yaml expandable theme={null}
notify_qawolf:
stage: test
image: curlimages/curl:latest
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
needs:
- deploy_preview
script:
- |
curl -X POST "https://app.qawolf.com/api/webhooks/deploy_success" \
-H "Authorization: Bearer $QAWOLF_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"branch\": \"$CI_COMMIT_REF_NAME\",
\"deployment_type\": \"provided-by-qawolf\",
\"deployment_url\": \"$PREVIEW_URL\",
\"sha\": \"$CI_COMMIT_SHA\",
\"hosting_service\": \"GitLab\"
}"
```
### What this job assumes
* Your pipeline already creates a preview environment for each merge request.
* The preview environment URL is available as `PREVIEW_URL` when this job runs.
* A prior job (such as `deploy_preview`) deploys the preview environment and verifies it is reachable before notifying QA Wolf.
QA Wolf will configure a trigger that matches your preview deployments. Reach out to your QA Wolf team to get this set up.
***
## Advanced: Require QA Wolf tests before merge
In GitLab, merge requests are typically blocked unless pipelines pass. Complete this section if you want to enforce QA Wolf results as a required check.
If your project uses approval rules or protected branches, ensure the merge request pipeline is the one required to pass.
## Verify your PR testing setup
Confirm that your merge request triggers a pipeline that deploys a preview environment.
In GitLab, check the merge request's environment or the Review App link, and confirm that the preview URL is reachable.
In the **QA Wolf app**, go to the **Runs** tab and verify that a new run starts for the preview environment associated with the merge request.
In GitLab, confirm the pipeline reflects the final result and that the merge request cannot be merged unless the pipeline succeeds.
# Pass data between flows
Source: https://docs.qawolf.com/qawolf/Pass-data-between-flows-29d5b2a994fb801080bce743ab552931
Share authentication tokens, user IDs, and other values across flows in a coordinated run.
Flows that pass data between each other are called **Hopper Flows** in QA Wolf — the pattern for multi-user, multi-device, and multi-platform test scenarios.
## Examples
**Create a user, then log in as that user**
A common pattern: one flow creates a user and publishes credentials; a second flow logs in using those credentials.
```typescript theme={null}
// Flow 1: Create user (producer)
await page.goto("https://app.example.com/admin/users/new");
await page.fill('[name="email"]', "testuser@example.com");
await page.fill('[name="password"]', "hunter2");
await page.click('[type="submit"]');
setOutput("USER_EMAIL", "testuser@example.com", "USER_PASSWORD", "hunter2");
```
```typescript theme={null}
// Flow 2: Log in (consumer)
const email = inputs["USER_EMAIL"];
const password = inputs["USER_PASSWORD"];
await page.goto("https://app.example.com/login");
await page.fill('[name="email"]', email);
await page.fill('[name="password"]', password);
await page.click('[type="submit"]');
```
**Create a user, log in, then verify activity**
Extend the chain: a third flow consumes the session token published by the login flow to verify downstream activity.
```typescript theme={null}
// Flow 1: Create user (producer)
setOutput("USER_EMAIL", "testuser@example.com", "USER_PASSWORD", "hunter2");
```
```typescript theme={null}
// Flow 2: Log in (producer + consumer)
const email = inputs["USER_EMAIL"];
const password = inputs["USER_PASSWORD"];
// ... log in ...
const token = await page.evaluate(() => localStorage.getItem("auth_token"));
setOutput("AUTH_TOKEN", token);
```
```typescript theme={null}
// Flow 3: Verify activity (consumer)
const token = inputs["AUTH_TOKEN"];
await page.setExtraHTTPHeaders({ Authorization: `Bearer ${token}` });
await page.goto("https://app.example.com/activity");
// ... assert expected activity ...
```
## When to use
* Your app has multi-step workflows that span separate user sessions.
* Your test requires one user to create a resource another user acts on.
* Your scenario involves multiple devices or platforms in a single run.
* Your flow needs data that only exists after another flow has run.
* Your test validates coordination between concurrent users or roles.
## Notes
**Key naming** — Keys are uppercase by convention: `AUTH_TOKEN`, `USER_EMAIL`, `USER_PASSWORD`.
**Overwrites** — If the same flow calls `setOutput` multiple times, the last call wins for that key.
**Conflicts** — If two different flows publish the same key and a third flow depends on both, the run fails with `Workflow run failed due to conflicting dependency outputs`.
**Execution order** — A consumer will not run until its producer has published. Use [Run Rules](/qawolf/Ordering-flows-29c5b2a994fb80789f3fea32feae8f92-copied-1) to enforce producer-before-consumer ordering.
**Scheduling** — Producers and consumers must be included in the same scheduled run. Target all flows or use a shared tag. See [Scheduling flows](/qawolf/Scheduling-flows-29c5b2a994fb80789f3fea32feae8f92).
# QA Wolf's static IPv4
Source: https://docs.qawolf.com/qawolf/QA-Wolf-s-static-IPv4-29c5b2a994fb81bf8487d085abf9dc4d
QA Wolf's static IPv4 range for firewall allowlisting and IP-restricted access to your application.
Our range is: `199.4.212.0/23`
You can verify QA Wolf's ownership of this range through the [ARIN WHOIS registry](https://whois.arin.net/rest/net/NET-199-4-212-0-1).
## When this is required
You need this IP range if you restrict access by source IP, for example:
* Firewall allowlisting for public or internal services
* Client-based VPNs (such as OpenVPN) that restrict connections by IP
## When this is not required
You do not need this IP range for:
* Site-to-site IPsec VPNs
* Mesh VPNs (such as Tailscale or Twingate)
* Routing inside a VPN tunnel
# How to integrate with Qase.io
Source: https://docs.qawolf.com/qawolf/Qase-io-29c5b2a994fb812fb3bfcf8ee3f33a16
Sync QA Wolf automated test results to your Qase.io project, including pass/fail status and run metadata.
## What QA Wolf syncs to Qase
Integrate QA Wolf with [Qase.io](https://qase.io/) to sync automated test results with your Qase project.
For each QA Wolf run, the integration can:
* Create a test run in Qase.
* Sync automated test results (passed, failed, skipped, blocked, invalid).
* Include run metadata (title, run/bug link, environment).
* Map results to existing Qase test cases.
Results are synced after runs reach Completed status in QA Wolf.
## Limitations
* Cloud-hosted [Qase.io](https://qase.io/) only.
* Only one Qase project can be configured per QA Wolf workspace.
* Qase test cases must already exist.
* Custom fields are not supported unless specially configured.
## Configure Qase
Customers configure Qase. QA Wolf uses the provided credentials to sync results and enable the integration.
API tokens are generated at the account level in Qase. If you don't have permission to create one, ask a project or account admin.
### Set up in Qase
* Active Qase.io account.
* Existing Qase project to receive test runs.
* API access enabled (available on paid Qase plans).
* Qase test cases already created (tests are not created automatically).
### Credentials to share with QA Wolf
Once Qase is configured, provide QA Wolf with:
* Qase API token.
* Qase project code (for example, WEBAPP, API, MOBILE).
* *(Optional)* tags or environment values to associate with test runs.
## Verify the integration
After QA Wolf enables the integration:
Run your automated tests in QA Wolf.
After the run completes, a new test run appears in your Qase project.
Test results and run metadata are visible on the mapped test cases.
If runs do not appear as expected, or if test cases are not mapping correctly, contact QA Wolf for verification.
# Recommended reading for business users and manual testers
Source: https://docs.qawolf.com/qawolf/Recommended-reading-for-QA-engineers-and-developers-2cd5b2a994fb80768272e9f973188fe8
Recommended courses on HTML, JavaScript, CSS, and Playwright to help you get more out of QA Wolf.
Here are some courses that we recommend:
# How to request new coverage
Source: https://docs.qawolf.com/qawolf/Request-new-coverage-29c5b2a994fb81c79523f8ff2eefde47
Submit a coverage request to QA Wolf through the platform, Slack, Teams, Discord, or your issue tracker.
## Request new test coverage
* Open a [coverage request](/qawolf/Request-new-coverage-29c5b2a994fb81c79523f8ff2eefde47#what-happens-next) in the platform
* Send a message in Slack, Teams, or Discord
* Tag QA Wolf in Jira or Linear. Tag the ticket with QA Wolf and we'll pick it up from there.
## What to include
We can get started with a message as simple as "New onboarding flow in staging, ready for tests," but the more detail that you provide, the less follow-up we'll need and the faster we can get the tests live.
We have found that using this format yields the best results for our customers:
**Objective:** Describe the goal of the flow you want created.
*Example: Verify the user registration process to ensure a user can register and receive a confirmation email.*
**Preconditions:** List anything that must be set up before the test can run.
*Example: Access to a user with admin permissions.*
**Steps:** Outline the actions required to achieve the objective.
*Example:*
* *Navigate to the registration page.*
* *Fill out the registration form with valid data.*
* *Submit the form.*
* *Check for a confirmation message.*
* *Verify a confirmation email is sent with correct details.*
* *Confirm that the user is created in the database.*
**Test data:** Include any specific data inputs the test will need.
*Example: A unique email address and user details for registration.*
**Expected results:** Describe what should happen if everything works correctly.
*Example:*
* *The form submits successfully.*
* *A confirmation message appears.*
* *A confirmation email is received.*
* *The user record is correctly created in the database.*
**Postconditions:** Specify any clean-up work needed after the test.
*Example: Delete the test user from the database.*
**Priority and timeline:** Indicate the priority of the request (urgent, high, medium, low) and whether you have a deadline.
**Video recording (optional):** Short walkthrough videos (e.g., Loom) aren't required but are very helpful for the team.
## What happens next
As soon as we receive the coverage request we will begin building tests. If you submitted a request via chat (Slack, Teams, Discord) or an issue tracker, a Coverage Request item will also be created in the QA Wolf platform.
### Create a new request
From the **Requests tab**, click **Request coverage** in the top right of the page.
A drawer opens.
Add the details for the flows you're requesting, set a priority, and give the request a name.
The name will appear in the **Coverage Requests** list.
Keep the name action-oriented (e.g., "Verify OTP autofill on Android and iOS.")
Click **Submit request** to save.
### Edit a request
Coverage requests include a set of fields that control status, priority, ownership, and how the request connects to flows.
* **Name.** Edit this by clicking on the name and typing in the box. Press Enter to save the change.
* **Status.** Tracks the request's current state.
* **Priority.** Indicates the request's urgency.
* **Requested by.** Shows who opened the request and can be updated if needed.
* **Estimated completion date.** Provides a target date to help with planning.
* **Requirements.** This is the description of the requested coverage, which you can edit by clicking on the icon below the name.
* **Related flows.** Links the request to existing flows or creates a new flow. To connect the request to another flow, click the icon in this field to select from the list of all existing flows.
### Find open coverage requests
You can find all requests for your workspace in the **Requests** tab.
* Use the status tabs on the left bar to switch between requests in different statuses.
* Use the **Search** bar to filter by request name. Use the icon to filter by request **Priority** or **Creator is me**.
# How Runs work
Source: https://docs.qawolf.com/qawolf/Run-a-flow-or-suite-2a25b2a994fb8014b164d7ad2efd5ddel
Learn how QA Wolf executes runs, retries failed flows, and reports results as completed or needs investigation.
A **run** is an execution of one or more **flows**. Runs can be started manually or triggered automatically by a schedule. Each flow in a run is attempted up to three times in case a failure is the result of a temporary flake.
At the end of a run, it will be marked as one of:
* **Completed** — all flows in the run have passed or been marked *Do Not Investigate*
* **Needs Investigation** — one or more flows failed and have not been marked *Do Not Investigate*
A flow needing investigation should be reviewed quickly and identified as either a bug (filed in your issue tracker) or a broken test (updated accordingly).
* **Canceled** — the run was canceled mid-execution by a subsequent run matching the same branch and environment
## Where runs can be performed
There are several ways to execute flows in QA Wolf. What you can run, where feedback appears, and whether results are recorded differs by method.
| Method | How | Feedback | Appears in Run History |
| --------------------------- | ------------------------------ | ------------------------------- | ---------------------------------- |
| Individual lines | Select lines in editor + Run | Editor only | No |
| A test block | Select `test()` function + Run | Editor only | No |
| A full flow | Run button in the IDE | Editor only | No |
| All flows in environment | Flows list | Runs tab | No |
| Select flows in environment | Select flows in the flows list | Runs tab | No |
| Scheduled run | Automatic | Runs tab Run History | Yes |
Run History in the Flows tab only shows results from scheduled runs. See [Scheduling flows](/qawolf/Scheduling-flows-29c5b2a994fb80789f3fea32feae8f92) to set up a schedule.
## Start a run
A run can be initiated in three ways:
* Manually from the **Runs** tab
* On a schedule — see [Scheduling flows](/qawolf/Scheduling-flows-29c5b2a994fb80789f3fea32feae8f92)
* When a build is deployed — for example, from GitHub Actions or another CI/CD system
## Flow orchestration logic
Runs orchestrate the *execution order* of flows and their multiple attempts, while the flows themselves report the *results* of each attempt, including logs and video artifacts.
## Attempt retry logic
Flows are attempted up to three times to ensure that a failure was not due to an accidental or temporary environment issue. All flows must report a result before any of them are re-attempted. The concurrency of each wave decreases to further reduce the impact of environmental issues.
1. **First attempt** — each flow executes concurrently unless run rules specify an alternate order
2. **Second attempt** — failed flows execute in batches of five
3. **Final attempt** — remaining failing flows execute serially
Logs and video artifacts for each attempt are available immediately after that attempt.
# Scheduling flows
Source: https://docs.qawolf.com/qawolf/Scheduling-flows-29c5b2a994fb80789f3fea32feae8f92
Create scheduled and event-based runs, choose which flows to include, and set the execution order.
## Create a schedule
In the **Runs** tab, select the environment for your scheduled run from the list on the left.
Click **Schedule a run**.
A drawer opens where you can configure your schedule:
a. **Name** — give your schedule a descriptive name.
b. **Type** — set to **Scheduled** for a time-based run.
The other three options require setup from QA Wolf:
* *Deployment* — triggers a run from a VCS deployment hook
* *Pull Request Testing Button* — triggers a run from a custom button in your VCS
* *GitHub Deployment* — available for teams using GitHub-based deployments
c. **Flows to run** — run *All flows* or *Flows with selected tags*. See [Use tags effectively](/qawolf/Use-tags-effectively-2c35b2a994fb809abbd7ea9402738bdb) to learn how to tag your flows.
d. **Concurrency limit** — sets the maximum number of concurrent flows to control load on your system. This overrides the value set in **Workspace settings → Environment**.
e. **Frequency** — choose *Every day* or *Every hour*.
Once you've set up schedules for an environment, runs will appear in the Scheduled tab as soon as they are started.
## Manage schedules
### See all schedules
Open the **Runs** tab and select an environment. Its schedules are listed under **Scheduled**.
### Pause a schedule
Click the icon to the right of the schedule, then click **Pause**. Paused schedules appear in gray text with a status of **Paused**.
### Edit a schedule
Click the icon to the right of the schedule, then click **Edit**. A drawer opens where you can edit your schedule.
# Site-to-Site IPSec Tunnel
Source: https://docs.qawolf.com/qawolf/Site-to-Site-IPSec-Tunnel-29f5b2a994fb80d7a45dd62f9b8a272a
Set up an IPSec site-to-site VPN tunnel between your network and QA Wolf using pre-shared key authentication.
Configure your VPN gateway to support:
* **IPsec site-to-site tunnels**
* **Pre-shared key (PSK) authentication**
* **Static routing** (dynamic routing is not supported)
* **IKEv1 or IKEv2**
Configure your firewall and VPN gateway to allow IPsec traffic:
* **Ports:** UDP 500 and UDP 4500
Use the information provided by QA Wolf to complete your VPN configuration:
* **QA Wolf VPN gateway public IP address(es):** Configure these as the remote IPsec peer
* **Pre-shared key (PSK):** Set as the tunnel authentication secret
* **Assigned subnet:** Add static routes to allow traffic to and from QA Wolf
## Compatibility
* **Fully supported:** AWS Site-to-Site VPN, Google Cloud VPN, Azure VPN Gateway
* **Supported with configuration:** Cisco ASA, Palo Alto, FortiGate
* **Not supported:** Dynamic-routing-only VPNs (for example, BGP-only)
If your VPN solution is not listed as fully compatible, contact your QA Wolf engineering lead to confirm interoperability before setup.
# Tailscale Client-based VPN
Source: https://docs.qawolf.com/qawolf/Tailscale-Client-based-VPN-29f5b2a994fb8021a88af5e1c5b21313-copied-2
Connect QA Wolf to your private network through Tailscale by providing an access token or client configuration.
Provide QA Wolf with either an access token or client configuration, depending on your VPN.
Confirm that your VPN mesh supports Linux clients and outbound connections from a dedicated proxy.
Ensure your network allows VPN traffic on TCP 443.
# Prepare your environments for testing
Source: https://docs.qawolf.com/qawolf/Test-environments-2dc5b2a994fb800e8ce4fe36ee34764a
The guidance below focuses on keeping your environment predictable, scalable, and ready for concurrent automated testing.
## Why the stability of your CI environments is essential
QA Wolf automatically retries failed tests, but unstable environments and excessive flakiness can slow runs and delay releases. Most test failures caused by infrastructure issues are preventable with the right environment setup.
If you're unsure how to size your environment or choose the right concurrency level, contact your Customer Success Manager.
Based on your application and usage patterns, they can review your setup and recommend:
* Concurrency limits
* Infrastructure sizing
* Alerting thresholds
## Right-size and monitor your environment
Your test environment must handle bursts of concurrent users and background activity during test runs.
### Use scalable infrastructure
* For every **500 concurrent tests**, allocate at least **4–8 vCPUs and 8–16 GB of memory**.
* If you use a cloud provider (AWS, GCP, Azure), size instance groups or containers accordingly and enable auto-scaling.
### Monitor performance
Set up basic telemetry (CloudWatch, Datadog, New Relic, etc.) to track CPU, memory, and network usage.
* If utilization regularly exceeds 75%, increase capacity or temporarily reduce concurrency in QA Wolf.
### Isolate test traffic
Run QA Wolf tests in a **dedicated staging or test environment**.
Avoid sharing environments with:
* Demo traffic
* Manual QA
* Feature branch verification
## Configure concurrency safely
Each QA Wolf flow runs in its own container and behaves like a separate user. Your systems must support that level of concurrency.
### Match concurrency to capacity
If your environment slows down or times out under load, reduce concurrency in QA Wolf. Many teams start at **25% of total flows per batch** and increase as stability improves.
### Avoid data collisions
Prevent tests from interfering with each other:
* Use unique login credentials per run.
* Prefix generated data with a run ID or timestamp.
* Use separate tenants or domains if supported.
### Control deployments during test runs
Avoid deploying to test environments while QA Wolf runs are in progress. Many teams enforce this with a CI/CD "deployment hold" or environment flag.
#### Use ephemeral environments when possible
Preview or ephemeral environments (Vercel, Render, custom Terraform, etc.) provide a clean surface for each run and reduce cross-test interference.
## Prepare the app for automated testing
Automated tests must be able to run end-to-end without manual setup.
### Ensure dependencies are reliable
* Databases, authentication providers, email services, and queues must be available during runs.
* Temporary outages often appear as test failures.
### Align feature flags
* Keep feature flags in a consistent, test-ready state. If you use tools like LaunchDarkly or Optimizely, create a dedicated configuration for QA Wolf.
### Provision test users and data
* Maintain at least one QA Wolf account with sufficient permissions.
* Provide an API or scriptable way to create and clean up test users.
* This allows tests to start from a clean state and run concurrently.
### Configure email and messaging
* Allow emails from `@qawolfworkflows.com` and `@qawolf.email`.
* Support plus addressing (e.g., `user+test123@example.com`) or another delimiter.
* Aim for email and SMS delivery within **one minute** to avoid timeouts.
# How to integrate with TestRail
Source: https://docs.qawolf.com/qawolf/TestRail-29c5b2a994fb8158897afc10c5d66782
Sync QA Wolf automated test results to TestRail, including pass/fail status and run metadata for each test run.
## What QA Wolf syncs to TestRail
Integrate QA Wolf with [TestRail](https://www.testrail.com/) to centralize automated test results and reporting. Customers configure TestRail. QA Wolf uses the provided credentials to sync results and enable the integration.
For each QA Wolf run, the integration can:
* Create a new test run in TestRail.
* Sync automated test results (passed, failed, skipped).
* Include run metadata (name, run/bug link).
* Mark the test run as completed after results are synced.
Results are synced after runs reach Completed status in QA Wolf.
## Limitations
* Only one TestRail project can be configured per QA Wolf workspace.
* Test runs are created at the run level (no test plan–level updates).
* The *Skipped* test status must exist in TestRail.
## Configure TestRail for QA Wolf integration
Enabling TestRail API access requires an **Administrator** user. If you're not a TestRail admin, ask one to enable the API before generating your credentials.
### Set up in TestRail
* Active TestRail account.
* API access enabled.
* Target TestRail project to receive QA Wolf runs.
* Custom test status *Skipped* is configured in TestRail.
### Share with QA Wolf
* TestRail username or login email.
* TestRail API key or password (depending on your account configuration).
* Target TestRail project (project ID or URL).
## Verify the integration
After QA Wolf enables the integration:
Run your automated tests in QA Wolf.
After the run completes, a new test run appears in your TestRail project with synced results and metadata.
The TestRail run is automatically marked completed.
If runs do not appear as expected, or if you need assistance configuring TestRail, contact QA Wolf for verification.
# How to integrate with Testmo
Source: https://docs.qawolf.com/qawolf/Testmo-29c5b2a994fb810bb096c46ec45512fb
Sync QA Wolf automated test results to your Testmo project, including pass/fail status and run metadata.
For each QA Wolf run, the integration can:
* Create a new test run in Testmo.
* Sync automated test results (passed, failed, skipped, errored).
* Include run metadata (title, run link).
* Apply optional tags to all synced runs.
Results are synced after QA Wolf finishes processing a run.
### Limitations
* Up to two Testmo projects can be configured per QA Wolf workspace.
* The tags you provide during setup are applied to all synced runs.
Dynamic tags (for example, different tags for staging vs production or PR vs scheduled runs) require custom configuration by QA Wolf.
## Configure Testmo for QA Wolf integration
Enabling Testmo API access requires an **Administrator** user. If you're not a Testmo admin, ask one to enable the API before generating your credentials.
### Set up in Testmo
* Active Testmo account.
* API access enabled.
* Target Testmo project to receive QA Wolf runs.
* *(Optional)* Tags to apply to all synced runs.
### Share with QA Wolf
* Testmo login email.
* Testmo API key.
* Target Testmo project (project ID or URL).
* *(Optional)* Tags to apply to runs.
## Verify the integration
After QA Wolf enables the integration:
Run your automated tests in QA Wolf.
After QA Wolf finishes processing the run, a new test run appears in your Testmo project with synced results and metadata.
If runs do not appear as expected, or if you need assistance configuring Testmo, contact QA Wolf for verification.
# Twingate Client-based VPN
Source: https://docs.qawolf.com/qawolf/Twingate-Client-based-VPN-29f5b2a994fb8021a88af5e1c5b21313-copied-1
Connect QA Wolf to your private network through Twingate by providing an access token or client configuration.
Provide QA Wolf with either an access token or client configuration, depending on your VPN.
Confirm that your VPN mesh supports Linux clients and outbound connections from a dedicated proxy.
Ensure your network allows VPN traffic on TCP 443.
# Upload files
Source: https://docs.qawolf.com/qawolf/Uploading-manually
Upload files to QA Wolf and use them in your test flows.
QA Wolf accepts file uploads via a two-step signed URL process. See [v0/run-inputs-executables-signed-urls](/qawolf/run-inputs-executables-signed-urls).
Make sure you have:
* Your `QAWOLF_API_KEY` — available under **Workspace Name → Workspace Settings → Integrations → API Access**
## Use an uploaded file in a flow
### Mobile apps
Reference the uploaded file using the `RUN_INPUT_PATH` environment variable, which is set automatically when a file is uploaded via the API.
```javascript theme={null}
const APK_PATH = `/home/wolf/run-inputs-executables/${process.env.RUN_INPUT_PATH || "default-file.apk"}`;
const driver = await wdio.startAndroid({
"appium:app": APK_PATH,
});
```
### Web file upload
Use Playwright's `filechooser` event to intercept the file dialog and set the uploaded file programmatically.
```javascript theme={null}
const fileName = `${process.env.TEAM_STORAGE_DIR}/fileNameHere`;
page.once(
"filechooser",
(chooser) => void chooser.setFiles(fileName).catch(console.error)
);
await page.getByText("Upload file").click();
```
Use `page.once` rather than `page.on`. `page.once` registers a one-time event listener that automatically unregisters after the first use. Using `page.on` adds a persistent listener that may interfere with subsequent file uploads in the same flow.
### PDF viewing
Use the internal PDF viewer to open an uploaded PDF file in a flow.
```javascript theme={null}
const invoicePath = `${process.env.TEAM_STORAGE_DIR}/invoices/invoice-1.pdf`;
const pdfPage = await context.newPage();
await pdfPage.goto("http://pdf-viewer.psc.qaw.internal");
pdfPage.once(
"filechooser",
(chooser) => void chooser.setFiles(invoicePath).catch(console.error)
);
await pdfPage.waitForTimeout(4000);
await pdfPage.click("#openFile");
```
## Supported file types
Use a static filename based on the environment (e.g., `app-staging.ipa`) so tests always reference the latest build. See [Mobile build testing](/qawolf/Fastlane-2db5b2a994fb802186a3d88573899c78) for how to automate this as part of your build process.
ZIP files can be used to bundle multiple assets for a single upload. Ensure the contents follow any structure expected by the tests that will consume them.
CSV files are typically used as test data inputs. Use a consistent filename so flows always pick up the latest version.
PDF files can be uploaded for use in test flows that involve document handling or validation.
# Test a feature that sends or receives emails
Source: https://docs.qawolf.com/qawolf/Use-emails-in-tests-29c5b2a994fb81cf8a03de3eb25ed68e
Use mail.inbox() from @qawolf/emails to send, receive, and verify emails during your QA Wolf flows.
Any email address used in a flow must be on your team's [Email Allowlist](#the-email-allowlist).
## Examples
**Verify a signup confirmation or magic link**
Use a fresh inbox and fill it into your app's email field. Wait for the message after the triggering action.
```javascript theme={null}
const inbox = await mail.inbox({ new: true });
await page.getByLabel("Email").fill(inbox.emailAddress);
const after = new Date();
await page.getByRole("button", { name: "Send magic link" }).click();
const message = await inbox.waitForMessage({ after });
await expect(message.urls[0]).toContain("/activate");
```
**Extract a login code from email**
Use `after` immediately before the action that sends the code to avoid matching an older message from the same address.
```javascript theme={null}
const inbox = await mail.inbox({ new: true });
await page.getByLabel("Email").fill(inbox.emailAddress);
const after = new Date();
await page.getByRole("button", { name: "Send code" }).click();
const message = await inbox.waitForMessage({ after });
const match = message.text.match(/\b\d{6}\b/);
if (!match) throw new Error("Login code email did not contain a 6-digit code");
await page.getByLabel("Code").fill(match[0]);
await page.getByRole("button", { name: "Verify" }).click();
```
**Wait for a batch of emails**
Use `waitForMessages` when one action should trigger several emails, such as a team invite or multi-step onboarding sequence. Use `delay` if your app enqueues email work in the background.
```javascript theme={null}
const inbox = await mail.inbox({ new: true });
const after = new Date();
await page.getByRole("button", { name: "Invite team" }).click();
const messages = await inbox.waitForMessages({
after,
minCount: 3,
timeout: 120_000,
delay: 5_000,
});
const subjects = messages.map((m) => m.subject);
expect(subjects).toContain("Welcome");
expect(subjects).toContain("Your workspace is ready");
```
**Simulate an inbound email**
Use `sendMessage` when your app receives or reacts to incoming email — for example, a support reply or an automated trigger.
```javascript theme={null}
const inbox = await mail.inbox({ new: true });
await inbox.sendMessage({
to: ["support@example.com"],
subject: "Need help",
text: "The test user is asking for support.",
});
```
**Send with attachments**
Pass an `attachments` array to `sendMessage`. Use `contentId` when the HTML body references an inline file.
```javascript theme={null}
await inbox.sendMessage({
to: ["invoices@example.com"],
subject: "Invoice",
text: "Attached.",
attachments: [
{
fileName: "invoice.txt",
content: Buffer.from("invoice total: $10.00"),
type: "text/plain",
},
],
});
```
**Reply to an app email**
```javascript theme={null}
const original = await inbox.waitForMessage({});
await inbox.sendMessage({
to: [original.from],
subject: `Re: ${original.subject}`,
text: "Thanks, this worked.",
});
```
## When to use
* Your app sends a confirmation, magic link, or verification code by email.
* Your app sends a batch of emails when a user action occurs.
* Your app receives or reacts to inbound email.
* You need a fresh, isolated inbox for every test run.
* Your app processes email replies or attachments.
## The Email Allowlist
QA Wolf includes an **Email Allowlist** to ensure test emails are delivered only to approved addresses or domains. You must add any email address used in a flow to the allowlist before running the flow.
QA Wolf provides internal email domains — **qawolf.email** and **qawolfworkflows.com** — for email testing. These are recommended over public email services because QA Wolf controls the servers, which stabilizes tests that rely on email interactions.
The platform sets an automatic default address. You cannot delete the default.
**To add or change the default email address:**
Click the icon on the upper right. A drawer opens.
Click **Addresses**.
Enter the username and domain in the appropriate fields, then click **Add**.
a. You can create as many email addresses as you like in the allowlist.
b. To change the default, hover over the address you want to set as default, click the icon, and select **Make default**.
## Address options
**Use a fresh address for each run**
Pass `new: true` to generate a unique derived address. This is the right choice for most tests — it isolates each run so old emails from previous runs are never matched.
```javascript theme={null}
const inbox = await mail.inbox({ new: true });
// Example: my-team+a25sa5q@qawolf.email
```
If your app does not accept `+` addressing, use a custom delimiter:
```javascript theme={null}
const inbox = await mail.inbox({ new: true, delimiter: "-" });
```
**Use the workspace default**
Omit all options to use your team's default address. Use this only when a test intentionally shares a stable inbox.
```javascript theme={null}
const inbox = await mail.inbox();
```
**Use a specific allowlisted address**
```javascript theme={null}
const inbox = await mail.inbox({
address: "my-team+admin@qawolf.email",
});
```
## Full sample test
```javascript theme={null}
import { flow } from "@qawolf/flows/web";
import { mail } from "@qawolf/emails";
export default flow(
"Sign in with email code",
{ target: "Web - Chrome", launch: true },
async ({ page }) => {
const inbox = await mail.inbox({ new: true });
await page.goto("https://example.com/login");
await page.getByLabel("Email").fill(inbox.emailAddress);
const after = new Date();
await page.getByRole("button", { name: "Send code" }).click();
const message = await inbox.waitForMessage({ after });
const match = message.text.match(/\b\d{6}\b/);
if (!match) throw new Error("Login code email did not contain a 6-digit code");
await page.getByLabel("Code").fill(match[0]);
await page.getByRole("button", { name: "Verify" }).click();
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
},
);
```
# Use tags effectively
Source: https://docs.qawolf.com/qawolf/Use-tags-effectively-2c35b2a994fb809abbd7ea9402738bdb
Apply tags to flows for scheduling purposes, such as grouping by test type, priority, or expected run duration.
Tags group flows for scheduled runs and control execution order in run rules. They are primarily intended to express **run intent** rather than application structure.
Because QA Wolf supports full concurrency by default, organizing flows by feature area rarely makes sense. Instead, use tags to describe the characteristics of a run — common examples include test type (e.g., *sanity* or *smoke*) and expected duration (e.g., *long-running*).
Before applying tags, make sure the tags you need exist. See [Workspace settings](/qawolf/Workspace-settings-2d25b2a994fb807fbd9bece4eb1a4d5f#tags) to create and manage tags.
## Apply tags to a single flow
You can apply tags to a single flow from two places:
**In the IDE** — open the flow in the editor and click the **Details** tab in the right panel. Tags can be edited directly from there.
**In the flows list** — select a flow from the Flows tree. The **Details** panel on the right shows the flow's tags. Tags can be edited directly from there.
## Apply tags to multiple flows
In the **Flows** tab, hover over the checkbox next to a flow's status icon and click to select it. Repeat for each flow you want to tag.
Click the icon that appears at the bottom of the flow list.
Click the tags you want to apply.
## How tags are used
**Scheduling** — when creating a schedule, you can choose to run all flows or only flows with selected tags. See [Scheduling flows](/qawolf/Scheduling-flows-29c5b2a994fb80789f3fea32feae8f92) for more details.
**Ordering** — tags can be used in run rules to control execution order. See [Ordering flows](/qawolf/Ordering-flows-29c5b2a994fb80789f3fea32feae8f92-copied-1) for more details.
# Refine a selector with the Elements tab
Source: https://docs.qawolf.com/qawolf/Use-the-Elements-picker-2c35b2a994fb80a681baca4f6acf3179
Inspect DOM elements and build stable locators for automated tests using the Elements tab in QA Wolf.
The Elements tab is a testing-focused inspection tool built into the Flows tab. Unlike standard browser developer tools, it is optimized for identifying elements and generating stable locators rather than for debugging or modifying page structure. It is read-only and does not modify the page structure.
Use it while writing or debugging a flow to find and verify selectors before adding them to your code.
You must have an active browser session to use the **Elements** tab.
## Pick a locator
Click an element in the browser to inspect it. The Elements tab populates with the platform's recommended locator for that element.
To use a different locator, open the **Locator** dropdown. The platform's recommended locator appears at the top.
Use the **Action** dropdown to select an interaction or assertion. The locator populates in the field below.
Copy and paste the locator into your flow.
Use a stable `test ID` attribute where possible — one that stays the same even when the UI layout or styling changes. This prevents many common test failures caused by shifting locators.
# Welcome to QA Wolf
Source: https://docs.qawolf.com/qawolf/Welcome-to-QA-Wolf
Create, run, and maintain end-to-end tests automatically with QA Wolf.
QA Wolf helps teams plan, create, run, and maintain end-to-end tests automatically as part of the AI-driven software development lifecycle.
Using AI, QA Wolf maps your application, generates tests from natural-language prompts, and runs them in parallel on managed infrastructure. The tests are written as standard Playwright and Appium code, so your team keeps full ownership and can review, modify, and version them like any other code.
QA Wolf supports the full lifecycle of end-to-end testing—from identifying the user journeys that matter, to generating automation, to continuously validating releases as your product evolves.
***
## How QA Wolf works
Testing with QA Wolf happens in three steps.
### Outline
AI explores your application and identifies the user paths that can be tested. This creates a coverage outline of your product so teams can understand what to cover first, and where new features create gaps in coverage.
### Create
Describe a flow in plain language, and QA Wolf generates Playwright and Appium tests to validate the behavior of your application. The resulting tests are standard code that your team owns and can modify.
### Run
Tests run in parallel across QA Wolf infrastructure. Runs can be triggered on demand, scheduled, or executed automatically from CI pipelines.
***
## Why use QA Wolf
QA Wolf helps teams build reliable end-to-end coverage without managing testing infrastructure or maintaining fragile automation. Our deterministic tests make results trustworthy, so teams can move faster without second-guessing failures. And because we can handle the most complex test cases, teams can cover critical user journeys that other tools can't cover.
### AI-generated automation
Describe user journeys in natural language, and QA Wolf generates tests in Playwright and Appium.
### Customer-owned test code
Tests remain fully accessible to your team.
### Parallel execution by default
Run large suites quickly without managing browsers, devices, or test runners.
### Continuous validation
Tests run automatically across environments, so teams know when changes break critical user journeys.
### Clear coverage of critical paths
AI mapping helps teams understand what parts of the product are tested and where additional coverage may be needed.
***
## What you can test with QA Wolf
QA Wolf is designed to validate the workflows that matter most in modern applications—from simple journeys to complex multi-system interactions.
### Web applications
QA Wolf supports complex browser-based interactions and modern web application architectures.
* Canvas and WebGL interfaces.
* Drag-and-drop interactions.
* File uploads and downloads.
* PDF generation and validation.
* Visual regression and UI diffing.
* Multi-user workflows across multiple sessions.
* Multi-site flows that move across domains.
* Browser extensions and plugins.
* Electron and hybrid desktop applications.
### iOS applications
Test native iOS apps on **real iPhones and iPads**.
* Gestures, layouts, and device orientation.
* Push notifications and deep links.
* Apple Pay transactions.
* Apple ID authentication.
* Camera, photos, and video interactions.
* System permissions and OS dialogs.
* Sensors such as GPS, Bluetooth, and Wi-Fi.
* Cross-device journeys between web and mobile.
### Android applications
Test native Android apps in **isolated device environments designed for automation scale**.
* Google Pay transactions.
* Push notifications and deep links.
* Android permission prompts.
* Camera and sensor interactions.
* Device orientation and layout changes.
* Native authentication flows.
* Companion app workflows for connected systems.
***
## Who QA Wolf is for
* **Manual QA.** Map user journeys, generate end-to-end tests with AI, and validate behavior across releases without needing to write code.
* **Test Automators / SDET.** Build, extend, and maintain Playwright and Appium test suites with full access to the underlying code and infrastructure.
* **Developers.** Generate tests from prompts, modify the underlying code when needed, and run tests in CI to validate application behavior on every change.
* **Product and Design teams.** Map critical user flows and validate the experience before releases to ensure key journeys work as expected.
***
## Security and data protection
QA Wolf is designed to support teams building secure applications.
* **SOC 2 Type II compliant.** QA Wolf meets rigorous security and operational standards verified through independent audits.
* **HIPAA-ready infrastructure.** QA Wolf supports teams building and testing applications that must meet HIPAA requirements.
* **Secure infrastructure.** Tests run in isolated environments managed by QA Wolf.
* **Secure credential storage.** Sensitive credentials are stored securely and can be safely used in automated tests.
Learn more in the [Security](https://trust.qawolf.com) documentation.
***
## Explore QA Wolf
Connect your application and create your first tests.
Learn how workspaces, environments, flows, and runs are organized.
Let QA Wolf build and maintain your test suite for you.
# Configure workspace settings
Source: https://docs.qawolf.com/qawolf/Workspace-settings-2d25b2a994fb807fbd9bece4eb1a4d5f
Invite members, manage roles, create tags, and configure workspace-level settings in QA Wolf.
## Inviting organization members
Open the `Workspace name` dropdown and click **Workspace settings**.
Click **Organization** in the left navigation bar.
Click **Invite users**, type the **Email address** of the team member you want to grant access to, and specify their **Role**: *Member* or *Admin*.
Admins can set up SSO.
Click **Send Invite**. The invited user(s) will receive an email containing a login link to the site.
## Tags
Tags are used to group flows for scheduled runs and to control execution order in run rules. See [Use tags effectively](/qawolf/Use-tags-effectively-2c35b2a994fb809abbd7ea9402738bdb) for guidance on how to apply them.
### Access tags
Open the `Workspace name` dropdown and click **Workspace settings**.
Select **Tags** from the left menu.
### Create a tag
To create a new tag, click **Create Tag** in the lower left. A text box will appear. Enter the tag name you want to create, then click **Save**.
## Configuring your default runner
Node 20 with Playwright is the default runner.
Open the `Workspace name` dropdown and click **Workspace settings**.
Click **Flows** from the left navigation bar.
Under **Defaults**, use the **Runner to be used in new flows** dropdown to choose a new default runner.
# How to integrate with Xray
Source: https://docs.qawolf.com/qawolf/Xray-29c5b2a994fb8196a5eddfc6ce4a9e40
Sync QA Wolf automated test results to Xray Test Management for Jira, including test executions and metadata.
## Features
Integrate QA Wolf with [Xray Test Management for Jira](https://marketplace.atlassian.com/apps/1211769/xray-test-management-for-jira) to sync automated test results directly to Jira.
For each QA Wolf run, the integration can:
* Create a test execution in Xray.
* Sync automated test results (passed, failed, skipped).
* Link executions to existing Xray test cases.
* Include execution metadata (start/end time, environment).
* Optionally link executions to a test plan.
Results are synced after runs reach Completed status in QA Wolf.
## Limitations
* Cloud-hosted only (Jira Cloud + Xray Cloud).
* Only one Jira/Xray project can be configured per QA Wolf workspace.
* Xray test cases must already exist.
* Test executions are created at the execution level (no test plan management).
* Custom fields are not supported unless specially configured.
## Configure Xray / Jira
Customers configure Xray and Jira. QA Wolf uses the provided credentials to sync results and enable the integration.
Generating Xray API credentials requires administrator access in Jira/Xray. If you're not a Jira/Xray admin, ask one to create the credentials.
### Set up in Xray
* Jira Cloud instance with Xray installed.
* Xray-enabled Jira project to receive test executions.
* Xray Cloud API access enabled.
* Xray test cases already created (tests are not created automatically).
### Credentials to share with QA Wolf
Once Xray is configured, provide QA Wolf with:
* Xray Client ID.
* Xray Client Secret.
* Jira project key (for example, QA or MOBILE).
* *(Optional)* Default test plan to link executions.
## Verify the integration
After QA Wolf enables the integration:
Run your automated tests in QA Wolf.
After the run completes, a new Test Execution appears in your Jira project.
Test results and execution metadata are visible in Xray.
If executions do not appear as expected, or if test cases are not mapping correctly, contact QA Wolf for verification.
# How to integrate with Zephyr
Source: https://docs.qawolf.com/qawolf/Zephyr-29c5b2a994fb81ed97d9e214c8b177c6
Sync QA Wolf automated test results to Zephyr Test Management for Jira, including test cycles and metadata.
## What QA Wolf syncs to Zephyr
Integrate QA Wolf with [Zephyr Test Management for Jira](https://marketplace.atlassian.com/apps/1213259/zephyr-test-management-and-automation-for-jira) to sync automated test results directly to Jira.
For each QA Wolf run, the integration can:
* Create test executions in Zephyr.
* Associate executions with a test cycle.
* Sync automated test results (passed, failed, skipped).
* Include execution metadata (cycle name, environment).
Results are synced after runs reach Completed status in QA Wolf.
## Limitations
* Cloud-hosted only (Jira Cloud + Zephyr Squad or Scale).
* Only one Zephyr-enabled Jira project can be configured per QA Wolf workspace.
* Zephyr test cases must already exist.
* Custom fields are not supported unless specially configured.
## Configure in Jira / Zephyr
Customers configure Jira and Zephyr. QA Wolf uses the provided credentials to sync results and enable the integration.
Generating Jira API tokens requires administrator access to the Atlassian account. If you're not an admin, ask one to create the token.
### Jira/Zephyr setup
* Jira Cloud instance.
* Zephyr installed and licensed (Squad or Scale).
* Zephyr-enabled Jira project to receive test executions.
* API access enabled (via Atlassian REST API).
* Zephyr test cases already created (tests are not created automatically).
### Credentials to share with QA Wolf
Once Jira and Zephyr are configured, provide QA Wolf with:
* Jira user email.
* Jira API token.
* Jira base URL.
* Zephyr project key (for example, QA, MOBILE).
* *(Optional)* Default test cycle name or naming convention.
## Verify the integration
After QA Wolf enables the integration:
Run your automated tests in QA Wolf.
After the run completes, test executions appear in the configured Zephyr project.
Executions are grouped under the appropriate test cycle and reflect synced results.
If executions do not appear as expected, or if test cases are not mapping correctly, contact QA Wolf for verification.
# Measure accessibility for native mobile
Source: https://docs.qawolf.com/qawolf/a11y-native
Use accessibility IDs to verify that key screens are reachable and usable by screen readers on iOS and Android.
Use accessibility IDs to verify that key screens are reachable and usable by screen readers on iOS and Android. The `~` selector prefix is WebdriverIO shorthand for locating an element by its accessibility ID — this proves the element is correctly exposed to the accessibility layer and keeps your tests stable across visual refactors.
This recipe covers accessibility spot-checking for native mobile. For operationalized a11y monitoring — scheduled runs, trend tracking, aggregated reports, and stakeholder dashboards — talk to your QA Wolf team about full-service accessibility testing.
## Examples
**Verify a screen's key elements are reachable via accessibility semantics**
```typescript theme={null}
const email = await driver.$("~Email");
const password = await driver.$("~Password");
const loginBtn = await driver.$("~Log in");
await expect(email).toBeDisplayed();
await expect(password).toBeDisplayed();
await expect(loginBtn).toBeEnabled();
```
If you can't locate an element using `~`, that element is likely missing an accessibility label entirely — an accessibility bug worth raising with your dev team before writing the test.
**Assert that icon buttons have meaningful accessible names**
```typescript theme={null}
const menuIcon = await driver.$("~Main menu");
await expect(menuIcon).toBeDisplayed();
const label = await menuIcon.getAttribute("content-desc");
if (!label || label.trim().length < 3) {
throw new Error("A11y: menu icon accessible label is missing or too short");
}
```
## When to use
* Your app has key interactive elements that must be exposed to VoiceOver (iOS) or TalkBack (Android).
* Your team wants to catch regressions where a developer removed or renamed an accessibility label.
* Your team wants a lightweight proxy for accessibility hygiene without a full WCAG audit.
* Your app has icon-only buttons that must have meaningful accessible names.
## Platform reference
| Platform | Attribute | WebdriverIO selector |
| -------- | ------------------------------------------- | ------------------------- |
| Android | `content-desc` | `driver.$('~your-label')` |
| iOS | `accessibilityIdentifier` / accessible name | `driver.$('~your-id')` |
## Quick reference
| Goal | How to do it |
| -------------------------------------------------------- | ------------------------------------------------ |
| Prove an element is on the accessibility layer | Locate it with `driver.$('~accessibility-id')` |
| Assert a screen is navigable via accessibility semantics | Use `~` selectors throughout the full flow |
| Assert an icon button has a meaningful label | `getAttribute('content-desc')` and check length |
| Assert an error or toast is readable | `await expect($('~error-id')).toHaveText('...')` |
QA Wolf does not run a WCAG rules engine against native app views. Native apps don't have a DOM, so axe-core style scanning isn't available for native iOS or Android. For deeper audits — color contrast, touch target size, reading order — supplement these tests with manual testing using VoiceOver on iOS, TalkBack on Android, or the Xcode Accessibility Inspector and Android Studio Layout Inspector.
## Full sample test
```typescript theme={null}
import { flow } from "@qawolf/flows/android";
export default flow(
"Accessibility — login screen",
{ target: "Android - Pixel", launch: true },
async ({ driver, test }) => {
await test("verify login screen elements are accessible", async () => {
// Arrange
await driver.$("~Email").waitForDisplayed({ timeout: 10_000 });
// Act
const email = driver.$("~Email");
const password = driver.$("~Password");
const loginBtn = driver.$("~Log in");
await email.setValue(process.env.USER_EMAIL);
await password.setValue(process.env.USER_PASSWORD);
await loginBtn.click();
// Assert
await expect(driver.$("~Home")).toBeDisplayed();
const menuIcon = driver.$("~Main menu");
await expect(menuIcon).toBeDisplayed();
const label = await menuIcon.getAttribute("content-desc");
if (!label || label.trim().length < 3) {
throw new Error("A11y: menu icon accessible label is missing or too short");
}
});
},
);
```
# Measure web accessibility with axe-core
Source: https://docs.qawolf.com/qawolf/a11y-web
Inject axe-core into a Playwright page to catch WCAG violations and gate releases on accessibility quality.
Use axe-core or Lighthouse to assert that pages meet accessibility standards. Both tools are available in QA Wolf web flows — axe-core for precise violation gating, Lighthouse for scores and shareable reports.
This recipe covers accessibility spot-checking using axe-core and Lighthouse. For operationalized a11y monitoring — scheduled runs, trend tracking, aggregated reports, and stakeholder dashboards — talk to your QA Wolf team about full-service accessibility testing.
## Examples
**Gate a release on axe-core violations**
```typescript theme={null}
await page.addScriptTag({
url: "https://unpkg.com/axe-core@4.8.2/axe.min.js",
});
const violations = await page.evaluate(async () => {
const { violations } = await window.axe.run();
return violations;
});
const critical = violations.filter((x) => x.impact === "critical");
const serious = violations.filter((x) => x.impact === "serious");
expect(critical.length).toBe(0);
expect(serious.length).toBe(0);
```
**Generate a Lighthouse accessibility report**
```typescript theme={null}
const { lhr } = await playAudit({
page,
thresholds: { accessibility: 90 },
reports: {
formats: { html: true, json: true },
directory: `${process.env.TEAM_STORAGE_DIR}/lighthouse`,
name: `a11y-${Date.now()}`,
},
config: {
extends: "lighthouse:default",
settings: { onlyCategories: ["accessibility"] },
},
});
const a11yScore = Math.round(lhr.categories.accessibility.score * 100);
console.log(`Accessibility score: ${a11yScore}`);
```
Lighthouse scores accessibility against its own model, which does not map 1:1 to WCAG conformance levels. Use it as a directional score and audit artifact — not as a hard compliance gate.
## When to use
* Your app has pages that must meet WCAG standards and you need to catch regressions before they reach production.
* Your team needs a release gate that fails on critical or serious WCAG violations.
* Your team needs a shareable accessibility report for stakeholders or compliance purposes.
* Your team wants to establish a baseline score for a page and track it over time.
## Choosing the right approach
| | Axe-core | Lighthouse |
| ------------------- | ------------------------------------------- | ------------------------------------------- |
| **Best for** | CI checks, violation counts, release gating | Scores, formal reports, stakeholder sharing |
| **Output** | Violation list by severity | Accessibility score + full report |
| **Use as a gate** | Yes — throw on critical / serious | Not recommended |
| **Report artifact** | No (console / logs) | Yes — HTML, JSON, PDF |
These two approaches complement each other. Axe-core is precise and gateable; Lighthouse is broad and reportable. For the most complete picture, use both.
## Full sample test
```typescript theme={null}
import { flow, expect } from "@qawolf/flows/web";
export default flow(
"Accessibility",
{ target: "Web - Chrome", launch: true },
async ({ page, test }) => {
await test("assert no accessibility violations", async () => {
// Arrange
await page.goto("https://your-app.com");
// Act
await page.addScriptTag({
url: "https://unpkg.com/axe-core@4.8.2/axe.min.js",
});
const violations = await page.evaluate(async () => {
const { violations } = await window.axe.run();
return violations;
});
// Assert
const critical = violations.filter((x) => x.impact === "critical");
const serious = violations.filter((x) => x.impact === "serious");
const moderate = violations.filter((x) => x.impact === "moderate");
const minor = violations.filter((x) => x.impact === "minor");
expect(critical.length).toBe(0);
expect(serious.length).toBe(0);
expect(moderate.length).toBe(0);
expect(minor.length).toBe(0);
});
},
);
```
The full sample gates on all violation levels. Adjust the threshold to match your policy — some teams only gate on `critical` and `serious`, particularly when first introducing accessibility testing.
# Anatomy of a flow
Source: https://docs.qawolf.com/qawolf/anatomy-of-a-qa-wolf-test-mobile-edition
Learn the structure of every QA Wolf flow — imports, the flow wrapper, launch styles, callback parameters, and the AAA framework.
If the agent generated a flow for you and you want to understand what it produced, this page explains each part. If your team has written Playwright or Appium tests before, QA Wolf flows will look familiar. The selectors, interactions, and assertions work the same way. What's different is the wrapper around them.
Every flow is wrapped in `flow()` from the `@qawolf/flows` package — it tells the runner what to run, where to run it, and which runtime objects to inject into the callback.
## The import statement
Every flow file starts with an import. Which package you import from depends on what you're testing.
```typescript Web theme={null}
import { flow, expect } from "@qawolf/flows/web";
```
```typescript iOS theme={null}
import { flow, expect } from "@qawolf/flows/ios";
```
```typescript Android theme={null}
import { flow, expect } from "@qawolf/flows/android";
```
```typescript CLI theme={null}
import { flow } from "@qawolf/flows/cli";
```
| Package | Use when |
| ----------------------- | ------------------------------------------------------------------------------------------------------------ |
| `@qawolf/flows/web` | Testing a website, web app, or Electron desktop app |
| `@qawolf/flows/ios` | Testing a native iOS app |
| `@qawolf/flows/android` | Testing a native Android app |
| `@qawolf/flows/cli` | [Running setup, teardown, tasks that don't need browsers/devices](/qawolf/libraries/flows/api-reference/cli) |
For web flows, always import `expect` from `@qawolf/flows/web` — not from `@playwright/test`. Importing from `@playwright/test` will cause assertions to bypass QA Wolf's reporting.
For iOS, Android, and CLI flows, `expect` is wired to the QA Wolf runner automatically and is not connected to Playwright.
## The flow wrapper
`flow()` takes three arguments: a name, a configuration, and an async callback containing the test logic.
```typescript theme={null}
export default flow(
"Name shown in QA Wolf",
{ target: "Web - Chrome", launch: true },
async ({ page }) => {
// test steps go here
},
);
```
**Name** — what this flow is called in your results, bug reports, and QA Wolf dashboard. Make it descriptive enough that a failing flow name tells you where to look.
**Configuration** — specifies where the flow runs and how it starts. Pass a plain target string to name the browser, device, or environment, or pass an object with `target` and `launch` to configure startup simultaneously. The target string must exactly match one of the supported values, or your flow will fail to initialize — see [Target Literals](/qawolf/libraries/flows/api-reference/index#target-literals) for the current list.
**Callback** — the `async` function containing your test logic. What it receives depends on the launch style and platform (see below).
## Launch styles
| Style | When to use |
| ------------------- | ----------------------------------------------------------------------------- |
| `launch: true` | Most flows — default startup, no configuration needed |
| Options object | Startup options are known up front — e.g., persistent browser context |
| Explicit `launch()` | Startup depends on runtime logic — e.g., branching on an environment variable |
### Declarative launch — recommended
Pass `launch: true` to use the default startup, or pass an options object to customize it. Either way, QA Wolf starts the browser or app before your callback runs, and the callback receives the platform object ready to use.
If you've used Playwright directly, this is the equivalent of calling `await browser.launch()` — QA Wolf handles that setup for you, so your callback starts with a ready-to-use `page`.
```typescript Web theme={null}
export default flow(
"Sign in",
{ target: "Web - Chrome", launch: true },
async ({ page, test }) => {
await test("navigate to sign in", async () => {
await page.goto(process.env.BASE_URL);
});
},
);
```
```typescript iOS theme={null}
export default flow(
"Sign in",
{ target: "iOS - iPhone 15 (iOS 26)", launch: true },
async ({ driver, test }) => {
await test("launch app", async () => {
// driver is ready to use
});
},
);
```
```typescript Android theme={null}
export default flow(
"Sign in",
{ target: "Android - Pixel 9", launch: true },
async ({ driver, test }) => {
await test("launch app", async () => {
// driver is ready to use
});
},
);
```
Pass an options object instead of `true` when you need to customize startup, such as reusing a browser profile. See the [Web API Reference](/qawolf/libraries/flows/api-reference/web) for the full list of options.
```typescript theme={null}
export default flow(
"Sign in with saved profile",
{
target: "Web - Chrome",
launch: {
browserContext: "persistent",
userDataDir: "/tmp/qawolf-profile",
},
},
async ({ page, test }) => {
await test("navigate to sign in", async () => {
await page.goto(process.env.BASE_URL);
});
},
);
```
The callback receives a different object depending on the platform:
| Platform | Callback receives |
| -------------- | ----------------------------------------------------------- |
| Web browser | `page`, `context`, optional `browser` |
| Web (Electron) | `page` |
| iOS | `driver` — the Appium/WebdriverIO session for the device |
| Android | `driver` — the Appium/WebdriverIO session for the device |
| CLI | nothing additional — only `inputs`, `setOutput`, and `test` |
### Explicit launch — advanced flows only
For advanced cases where the startup depends on runtime logic, you can call `launch()` explicitly inside the callback. See the [Web API Reference](/qawolf/libraries/flows/api-reference/web) for details. For Electron desktop app testing, see [Testing Electron apps](/qawolf/electron).
## Callback parameters
Every flow callback receives three parameters regardless of platform:
**`inputs`** — values passed into the flow from outside. Use this to read data published by another flow in the same run. Keys are uppercase by convention, e.g. `inputs["EMAIL"]`.
**`setOutput(...)`** — publishes one or more key-value pairs that a later flow in the same run can read via its `inputs`. Keys are uppercase by convention. You can publish multiple values in a single call:
```typescript theme={null}
setOutput("USER_ID", userId, "EMAIL", email);
```
A consumer flow will not run until its producer has called `setOutput`. See [Passing data between flows](/qawolf/Pass-data-between-flows-29d5b2a994fb801080bce743ab552931) for how producers and consumers work together.
**`test(...)`** — wraps a named sub-step, grouping actions and assertions under a label that appears in your results. When a step fails, the label tells you exactly where in the flow the failure happened.
```typescript theme={null}
export default flow(
"Create account",
{ target: "Web - Chrome", launch: true },
async ({ page, inputs, setOutput, test }) => {
await test("fill registration form", async () => {
await page.goto(process.env.BASE_URL);
await page.getByLabel("Email").fill(inputs["EMAIL"]);
await page.getByRole("button", { name: "Sign up" }).click();
});
await test("confirm account created", async () => {
const userId = await page.getByTestId("user-id").textContent();
setOutput("USER_ID", userId);
});
},
);
```
Launch-enabled flows also receive the platform object (`page`, `context`, `browser`, or `driver`) as shown in the launch styles section above.
For the full callback context shape, see the API Reference for [Web](/qawolf/libraries/flows/api-reference/web#flow-callback-context), [Android](/qawolf/libraries/flows/api-reference/android#flow-callback-context), and [iOS](/qawolf/libraries/flows/api-reference/ios#flow-callback-context).
`launch()`, `device`, and `platform.target` are stubs in the package code, and they are replaced by the runner at execution time. They only work while a flow is running inside the QA Wolf runner — keep them inside the flow callback, not at the module level. \
\
Module-level code should be limited to imports, constants, and pure helper functions.
## Environment variables
Reference environment variables with `process.env.VAR_NAME`. They are typically used in the Arrange section before any interactions begin.
```typescript theme={null}
await page.goto(process.env.BASE_URL);
```
To set environment variables for your flows, see [Environment setup](/qawolf/Environment-settings-2cd5b2a994fb8014ba8bd7aa25f3ed0f#create-and-manage-environment-variables).
## The AAA framework
QA Wolf flows follow the Arrange, Act, Assert (AAA) format.
**Arrange** sets up state before the interaction.\
**Act** performs the interaction being tested.\
**Assert** verifies the expected outcome.
```typescript Web theme={null}
export default flow(
"Complete checkout",
{ target: "Web - Chrome", launch: true },
async ({ page, test }) => {
await test("arrange", async () => {
//--------------------------------
// Arrange:
//--------------------------------
await page.goto(process.env.BASE_URL);
});
await test("act", async () => {
//--------------------------------
// Act:
//--------------------------------
await page.fill("[data-testid='email']", "test@example.com");
await page.click("[data-testid='submit']");
});
await test("assert", async () => {
//--------------------------------
// Assert:
//--------------------------------
await expect(page).toHaveURL(`${process.env.BASE_URL}/confirmation`);
});
},
);
```
```typescript iOS theme={null}
export default flow(
"Complete checkout",
{ target: "iOS - iPhone 15 (iOS 26)", launch: true },
async ({ driver, test }) => {
await test("arrange", async () => {
//--------------------------------
// Arrange:
//--------------------------------
await driver.$(`-ios predicate string:name == 'Email' AND type == 'XCUIElementTypeTextField'`).waitForDisplayed();
});
await test("act", async () => {
//--------------------------------
// Act:
//--------------------------------
await driver.$(`-ios predicate string:name == 'Email' AND type == 'XCUIElementTypeTextField'`).setValue("test@example.com");
await driver.$(`-ios predicate string:name == 'Sign in' AND type == 'XCUIElementTypeButton'`).click();
});
await test("assert", async () => {
//--------------------------------
// Assert:
//--------------------------------
await driver.$(`-ios predicate string:name == 'Welcome' AND type == 'XCUIElementTypeStaticText'`).waitForDisplayed({ timeout: 5000 });
});
},
);
```
```typescript Android theme={null}
export default flow(
"Complete checkout",
{ target: "Android - Pixel 9", launch: true },
async ({ driver, test }) => {
await test("arrange", async () => {
//--------------------------------
// Arrange:
//--------------------------------
await driver.$(`~Email`).waitForDisplayed();
});
await test("act", async () => {
//--------------------------------
// Act:
//--------------------------------
await driver.$(`~Email`).setValue("test@example.com");
await driver.$(`~Sign in`).click();
});
await test("assert", async () => {
//--------------------------------
// Assert:
//--------------------------------
await driver.$(`~Welcome`).waitForDisplayed({ timeout: 5000 });
});
},
);
```
# Microphone injection (Android)
Source: https://docs.qawolf.com/qawolf/android-audio-injection
Inject audio into the Android emulator's microphone input to test recording and voice features.
The Android emulator routes audio played on the runner host through to the emulator's microphone input. Use `device.passAudioAsMicrophoneInput(...)` to play an audio file during a flow, and your app will receive it as microphone input.
**Start your app recording before you call this.** `passAudioAsMicrophoneInput` plays the file and resolves only once playback has finished, so awaiting it first plays the whole file into a microphone nobody is listening to, and the recording that follows captures silence. `delaySeconds` does not work around this either, since it is awaited inline as well.
Store your audio file in team storage and reference it via `process.env.TEAM_STORAGE_DIR`. See [Upload files](/qawolf/Uploading-manually) for instructions.
## Examples
**Inject audio into the microphone**
Start the recording in your app, then play the audio into it:
```typescript theme={null}
await driver.$(`//*[@text='Record']`).click();
await device.passAudioAsMicrophoneInput({
data: `${process.env.TEAM_STORAGE_DIR}/audio.mp3`,
durationSeconds: 10,
});
await driver.$(`//*[@text='Done']`).click();
```
**Start playback before your app begins listening**
If your app needs audio already flowing when it starts listening, hold the promise and await it once the app is ready:
```typescript theme={null}
const playback = device.passAudioAsMicrophoneInput({
data: `${process.env.TEAM_STORAGE_DIR}/audio.mp3`,
durationSeconds: 10,
});
await driver.$(`//*[@text='Record']`).click();
await playback;
```
**Pull a recording off the emulator**
```typescript theme={null}
await device.adb([
"pull",
"/sdcard/Recordings/My recording 1.m4a",
`${process.env.TEAM_STORAGE_DIR}/recorded.m4a`,
]);
```
## When to use
* Your app records audio or processes microphone input and you need to test that flow with known audio data.
* Your app has voice commands or speech recognition features.
* Your app validates or analyzes microphone input.
* Your test needs to run the same audio scenario repeatedly with consistent inputs.
## Options
`passAudioAsMicrophoneInput` accepts `delaySeconds` (wait before playback starts) and `durationSeconds` (cap playback length; omit to play the whole file). See the [Android Device Reference](/qawolf/libraries/flows/api-reference/android-device-reference) for the full signature.
`delaySeconds` delays playback, but the call still resolves only once playback has finished. Use it to line playback up with something your app does after it starts listening, not to start the recording after the call.
## Verifying the injected audio
Assert on something your app derived from the audio, such as the text a speech-recognition field transcribed. A file appearing on disk only proves your app recorded something, and a recording made in the wrong order contains silence while still producing a file.
`device.captureAudioOutput` cannot confirm that injection reached your app. The capture is taken on the runner host, where the injected audio is present whether or not the emulator ever passed it to the guest, so it returns your audio even when your app heard nothing. The same is true of any other host-side check. Verify from inside the app instead.
## Full sample test
```typescript theme={null}
import { device, expect, flow } from "@qawolf/flows/android";
const audioPath = `${process.env.TEAM_STORAGE_DIR}/audio.mp3`;
export default flow(
"Test voice input",
{ target: "Android - Pixel", launch: true },
async ({ driver, test }) => {
await test("app transcribes the injected speech", async () => {
// Arrange
await driver.$(`//*[@text='Add note']`).click();
// Act: start listening first, then play the audio into the microphone
await driver.$(`//*[@content-desc='Voice input']`).click();
await device.passAudioAsMicrophoneInput({
data: audioPath,
durationSeconds: 10,
});
await driver.$(`//*[@text='Done']`).click();
// Assert on what the app heard, not on whether a file was written
await expect(driver.$(`//*[@resource-id='note-body']`)).toHaveText(
"remember to buy oat milk",
);
});
},
);
```
# Android barcode and QR code scanning
Source: https://docs.qawolf.com/qawolf/android-barcode
Test barcode and QR code scanning features in Android apps using the emulator's virtual camera scene.
Use `device.setVirtualSceneImage()` and `device.playAutomation()` to place a barcode or QR code image in the emulator's virtual camera scene and animate the camera toward it — simulating a real scan without a physical device. See the [Android Device Reference](/qawolf/libraries/flows/api-reference/android-device-reference) for the full API.
The sequence is: store the barcode image in team storage → place it on the virtual scene (`table` or `wall`) → play the `Walk_to_image_room` macro to animate the camera toward it → wait for your app to detect and process the scan.
Store your barcode image in team storage and reference it via `process.env.TEAM_STORAGE_DIR`. See [Upload files](/qawolf/Uploading-manually) for instructions. The image must encode the exact value your test expects.
## Examples
**Scan a barcode on the virtual table**
```typescript theme={null}
await device.setVirtualSceneImage({
image: `${process.env.TEAM_STORAGE_DIR}/barcode.jpg`,
location: "table",
});
await device.playAutomation({
macro: "Walk_to_image_room",
});
```
**Scan a barcode on the virtual wall**
```typescript theme={null}
await device.setVirtualSceneImage({
image: `${process.env.TEAM_STORAGE_DIR}/barcode.jpg`,
location: "wall",
});
await device.playAutomation({
macro: "Walk_to_image_room",
});
```
## When to use
* Your app has a barcode or QR code scanner and you need to test it without physical hardware.
* Your app uses QR codes to initiate a session, link an account, or navigate to a URL.
* Your app reads barcodes as part of a checkout, inventory, or verification flow.
* Your test needs to assert on a specific scanned value.
## Full sample test
```typescript theme={null}
import { device, flow } from "@qawolf/flows/android";
const barcodeImagePath = `${process.env.TEAM_STORAGE_DIR}/barcode.jpg`;
export default flow(
"Scan barcode",
{ target: "Android - Pixel", launch: true },
async ({ driver, test }) => {
await test("scan barcode and verify value", async () => {
// Arrange
await driver.$(`//*[@text='Scan']`).click();
// Act
await device.setVirtualSceneImage({
image: barcodeImagePath,
location: "table",
});
await device.playAutomation({
macro: "Walk_to_image_room",
});
await driver
.$(`//*[@text='Barcode Captured']`)
.waitForDisplayed({ timeout: 10_000 });
// Assert
const value = await driver
.$(`//android.widget.TextView[@resource-id='com.example.app:id/scanned_value']`)
.getAttribute("text");
if (value !== "1234567890128") {
throw new Error(`Unexpected scan value: ${value}`);
}
});
},
);
```
# Mock device location (Android)
Source: https://docs.qawolf.com/qawolf/android-location-mocking
Set a custom GPS location on the Android emulator to test location-aware features in your app.
Use `device.setGeoLocation()` to override the emulator's GPS coordinates during a flow. This lets you test location-aware features — such as store finders, delivery zones, or region-specific content — without physically moving a device.
See the [Android Device Reference](/qawolf/libraries/flows/api-reference/android-device-reference) for the full `setGeoLocation` API.
## Examples
**Set a GPS location**
```typescript theme={null}
await device.setGeoLocation({
latitude: 40.78222,
longitude: -73.96528,
});
```
## When to use
* Your app shows location-aware content such as store finders, delivery zones, or regional pricing.
* Your app restricts features by geography and you need to test from a specific location.
* Your app uses GPS coordinates to personalize content and you need to assert on that behavior.
* Your test needs to simulate a device in a different city or country.
## Troubleshooting
Some apps don't immediately respond to a location change. Try one or more of the following.
**Grant location permissions automatically**
Pass `autoGrantPermissions: true` in your launch options.
```typescript theme={null}
const { driver } = await launch({
appPackage: "com.example.app",
autoGrantPermissions: true,
});
```
**Toggle location services**
```typescript theme={null}
await driver.toggleLocationServices();
await driver.toggleLocationServices();
```
**Reload the session**
```typescript theme={null}
await device.setGeoLocation({
latitude: 40.78222,
longitude: -73.96528,
});
await driver.reloadSession();
```
**Prime location tracking via Google Maps**
Some apps rely on the system location provider being active. Open Google Maps and trigger location tracking before your app reads the location, then reload the session.
```typescript theme={null}
await driver.activateApp("com.google.android.apps.maps");
const skipBtn = driver.$(`//android.widget.Button[@text='SKIP']`);
if (await skipBtn.isExisting()) {
await skipBtn.click();
}
const locationBtn = driver.$(
`//android.widget.FrameLayout[@resource-id='com.google.android.apps.maps:id/mylocation_button']`,
);
const contentDesc = await locationBtn.getAttribute("content-desc");
if (contentDesc.includes("Re-center")) {
await locationBtn.click();
}
const permissionBtn = driver.$(
`//android.widget.Button[@resource-id='com.android.permissioncontroller:id/permission_allow_foreground_only_button']`,
);
if (await permissionBtn.isExisting()) {
await permissionBtn.click();
}
await driver.reloadSession();
```
## Full sample test
```typescript theme={null}
import { device, flow, launch } from "@qawolf/flows/android";
export default flow(
"Test location feature",
{ target: "Android - Pixel", launch: true },
async ({ driver, test }) => {
await test("set location and verify location-based content", async () => {
// Arrange
await driver.$(`//*[@text='Find Stores']`).click();
// Act
await device.setGeoLocation({
latitude: 40.78222,
longitude: -73.96528,
});
// Assert
await driver
.$(`//*[@text='Stores near Central Park']`)
.waitForDisplayed({ timeout: 10_000 });
});
},
);
```
# Measure performance (Android)
Source: https://docs.qawolf.com/qawolf/android-performance
Measure frame rate and CPU usage on the Android emulator during automated flows.
Use `adb` commands via `device.adb(...)` to collect FPS and CPU metrics during a flow and assert on rendering performance and CPU load.
See the [Android `dumpsys` documentation](https://developer.android.com/tools/dumpsys) for more on SurfaceFlinger stats.
Performance measurements run on the Android emulator. FPS values reflect emulator rendering, not physical device GPU performance. CPU values are per-core percentages and can exceed 100% on multi-core emulators (typically 4 cores).
## Examples
**Assert average FPS**
```typescript theme={null}
await device.adb("shell dumpsys SurfaceFlinger --timestats -clear -enable");
// perform the rendering-heavy interaction you want to measure
const resTxt = await device.adb("shell dumpsys SurfaceFlinger --timestats -dump");
const match = resTxt.match(/averageFPS\s*=\s*(?\d+\.\d*)/);
const avgFps = Number(match.groups["avgFps"]);
expect(avgFps).toBeGreaterThan(40);
```
If the device is idle when you collect stats, the reported FPS may be near zero because there is no active rendering. Always start measuring immediately before a rendering-heavy action.
**Sample CPU usage**
```typescript theme={null}
const packageName = await driver.getCurrentPackage();
const processId = await device.adb(`shell pidof ${packageName}`);
const stdout = await device.adb(`shell top -n 1 -p ${processId}`);
const match = stdout.match(/\S+\s+\S+\s+\S+\s+\S+\s+(\S+)/);
const cpuUsage = parseFloat(match[1]);
expect(cpuUsage).toBeLessThan(50);
```
## When to use
* Your app has animations or rendering-heavy screens and you need to assert on frame rate.
* Your app performs background processing and you need to verify CPU usage stays within acceptable limits.
* Your flow includes a workflow that should complete without excessive CPU load.
* Your team has performance budgets you want to enforce in CI.
## Full sample test
```typescript theme={null}
import { device, flow } from "@qawolf/flows/android";
export default flow(
"Measure performance",
{ target: "Android - Pixel", launch: true },
async ({ driver, test }) => {
await test("assert FPS", async () => {
await device.adb("shell dumpsys SurfaceFlinger --timestats -clear -enable");
// perform the rendering-heavy interaction you want to measure
const resTxt = await device.adb("shell dumpsys SurfaceFlinger --timestats -dump");
const match = resTxt.match(/averageFPS\s*=\s*(?\d+\.\d*)/);
const avgFps = Number(match.groups["avgFps"]);
expect(avgFps).toBeGreaterThan(40);
});
await test("assert CPU usage", async () => {
const packageName = await driver.getCurrentPackage();
const processId = await device.adb(`shell pidof ${packageName}`);
const stdout = await device.adb(`shell top -n 1 -p ${processId}`);
const match = stdout.match(/\S+\s+\S+\s+\S+\s+\S+\s+(\S+)/);
const cpuUsage = parseFloat(match[1]);
expect(cpuUsage).toBeLessThan(50);
});
},
);
```
# Mock hardware sensors (Android)
Source: https://docs.qawolf.com/qawolf/android-sensor-mocking
Simulate accelerometer, gyroscope, light, and pressure sensor input on the Android emulator.
Use `device.adb(...)` to send `adb emu sensor set` commands during a flow. This lets you simulate hardware sensor input and test how your app responds.
```typescript theme={null}
import { device } from "@qawolf/flows/android";
```
See the [Android emulator sensor documentation](https://developer.android.com/studio/run/emulator-console#sensor-set) for available sensor names and value formats.
Sensor commands affect the emulator's sensor state directly. They work on emulators only — not physical devices.
## Examples
**Accelerometer**
```typescript theme={null}
await device.adb("emu sensor set acceleration 19:9.81:0");
```
**Gyroscope**
```typescript theme={null}
await device.adb("emu sensor set gyroscope 0.00:0.00:1.00");
```
**Light**
```typescript theme={null}
await device.adb("emu sensor set light 100");
```
**Pressure**
```typescript theme={null}
await device.adb("emu sensor set pressure 10");
```
## When to use
* Your app responds to device tilt or motion and you need to test that behavior.
* Your app adjusts UI based on ambient light levels.
* Your app reads barometric pressure for altitude or weather features.
* Your app uses gyroscope data for orientation or gesture detection.
# Speaker recording (iOS)
Source: https://docs.qawolf.com/qawolf/audio-capture
Record audio from the device speaker during a test to verify that your app plays the correct audio.
Use `device.startSpeakerRecording()` to capture audio output from the iOS device speaker while your app is playing audio. The recording is saved as a WAV file that can be downloaded and analyzed.
See the [iOS Device Reference](/qawolf/libraries/flows/api-reference/ios-device-reference) for the full API.
## Examples
**Record and download speaker audio**
```typescript theme={null}
const session = await device.startSpeakerRecording(driver);
// trigger audio playback in your app
const file = await device.stopSpeakerRecording(driver, session.id);
const buffer = await device.downloadSpeakerRecording(driver, file.filename);
const { writeFile } = await import("node:fs/promises");
await writeFile("/tmp/output.wav", buffer);
```
## When to use
* Your app plays audio and you need to verify the correct sound or track played.
* Your app uses text-to-speech and you need to validate the spoken output.
* Your app plays audio prompts and you need to confirm they play correctly after an update.
* Your test needs to capture audio output for comparison against a known reference.
## Analyzing the recording
To compare the recording against a reference file, see [Audio analysis (iOS)](/qawolf/i-os-audio-analysis).
`stopSpeakerRecording()` automatically calculates a Chromaprint fingerprint and includes it as `file.fingerprint`. You can use this directly for audio analysis without calling `calculateAudioFingerprint()` separately.
## Full sample test
```typescript theme={null}
import { device, flow } from "@qawolf/flows/ios";
export default flow(
"Record speaker audio",
{ target: "iOS - iPhone 15 (iOS 26)", launch: true },
async ({ driver, test }) => {
await test("record audio and verify file was captured", async () => {
// Arrange
await driver.$(`//XCUIElementTypeButton[@name='Play']`).waitForDisplayed({ timeout: 10_000 });
// Act
const session = await device.startSpeakerRecording(driver);
await driver.$(`//XCUIElementTypeButton[@name='Play']`).click();
await driver.pause(10_000);
const file = await device.stopSpeakerRecording(driver, session.id);
const buffer = await device.downloadSpeakerRecording(driver, file.filename);
// Assert
expect(buffer.length).toBeGreaterThan(0);
const { writeFile } = await import("node:fs/promises");
await writeFile("/tmp/output.wav", buffer);
});
},
);
```
# Track and review agent jobs
Source: https://docs.qawolf.com/qawolf/automate-jobs
Check the status of outlining and creation jobs, review the flows they produced, and retry failures.
**Automate jobs** lists every agent job in the environment, running and finished, for both outlining and creation. Use it to check on a job in progress, review what an agent produced, or work out why a job stopped.
## Open Automate jobs
From the **Flows** tab, click the icon in the left sidebar.
Each row is one job: its status, the flows it covers, the environment and job group it ran against, when it ran, and who started it. Jobs still running show their progress, and jobs that hit a failure show how many flows failed.
## Filter the list
Three filters narrow the list, and they combine:
* **Status** — one or more of Queued, In progress, Needs review, Merged, Partially merged, Canceled, and Error
* **Environment** — jobs from a single environment
* **Author** — jobs started by one person
Select **Sort** to order the list by newest or oldest first.
## Review a job
Click **Review** on the job you want to inspect. The button names the number of flows waiting on you.
Select a flow in the left panel.
Inspect the flow with the tabs above it:
a. **Outline** — what the flow covers
b. **Video** — a recording of the run
c. **Code** — the test code the agent wrote, whether it passed, and a control to run it again
d. **Details** — the run's metadata
The agent chat for the job stays open on the right, so you can ask about a decision it made or tell it to change something.
## Retry a failure
Open the flow's **Code** tab to see whether it passed and run it again. To retry everything that failed in the job at once, click **Retry failing** at the bottom of the flow list.
For prompting practices that cut down on failures in the first place, see [Get the most out of AI flow creation](/qawolf/getting-the-most-out-of-automate).
## Work with your published flows
Once published, your flows appear in **All flows**, the coverage map for the environment. From there, you can:
* Review run status across all flows — see [Reviewing run results](/qawolf/Interpret-run-results-2d55b2a994fb80199754e4b2d3ce0318)
* Run flows manually — see [How runs work](/qawolf/Run-a-flow-or-suite-2a25b2a994fb8014b164d7ad2efd5ddel)
* Apply tags and manage readiness — see [Use tags effectively](/qawolf/Use-tags-effectively-2c35b2a994fb809abbd7ea9402738bdb)
* Open a flow in the editor to write or edit code — see [Create flows in the IDE](/qawolf/Create-a-new-flow-2c15b2a994fb802193a5c05e0219eb59)
# iOS Barcode and QR code scanning
Source: https://docs.qawolf.com/qawolf/barcode-qrcode-scanning
Inject barcode and QR code data into any iOS app to test scanning workflows without a physical camera.
This feature works with any app, regardless of whether it uses Apple's built-in scanning or a third-party library like ZXing or Google ML Kit.
## Examples
**Inject a QR code:**
```js theme={null}
// Inject QR Code
import { device } from "@qawolf/flows/ios";
const { bundleId } = await driver.execute("mobile: activeAppInfo");
const cleanup = await device.injectBarcode(driver, bundleId, {
value: "https://qawolf.com/qr-code",
});
// ... assert that the app handled the scanned QR code ...
await cleanup();
```
**Inject a barcode:**
```js theme={null}
// Inject EAN-13 Barcode
import { device } from "@qawolf/flows/ios";
const { bundleId } = await driver.execute("mobile: activeAppInfo");
const cleanup = await device.injectBarcode(driver, bundleId, {
type: "org.gs1.EAN-13",
value: "1234567890123",
});
// ... assert that the app handled the scanned barcode ...
await cleanup();
```
## When to use
* Your app scans QR codes to trigger navigation, deep links, or actions.
* Your app scans barcodes for product lookups, ticketing, or payments.
* You need to test scanning flows without a physical code or camera setup.
* You need to run the same scanning scenario repeatedly with consistent results.
## Supported formats
**2D Codes:**
* QR Code (`org.iso.QRCode`)
* Aztec Code (`org.iso.Aztec`)
* PDF417 (`org.iso.PDF417`)
* Data Matrix (`org.iso.DataMatrix`)
* Micro QR Code (`org.iso.MicroQRCode`)
**1D Barcodes:**
* EAN-13/EAN-8 (`org.gs1.EAN`)
* UPC-A/UPC-E (`org.gs1.UPC`)
* Code 128 (`org.gs1.Code128`)
* Code 39 (`org.gs1.Code39`)
* Code 93 (`org.gs1.Code93`)
* ITF-14 (`org.gs1.ITF14`)
* Interleaved 2 of 5 (`org.gs1.Interleaved2of5`)
* Codabar (`org.gs1.Codabar`)
## Advanced: Multiple objects
Some apps expect the camera to detect multiple codes simultaneously. To inject multiple detections in a single call:
```js theme={null}
// Inject Multiple Barcodes (delivered to the delegate in one batch)
import { device } from "@qawolf/flows/ios";
const cleanup = await device.injectBarcode(driver, bundleId, [
{ type: "org.iso.QRCode", value: "https://example.com/qr-deep-link" },
{ type: "org.gs1.EAN", value: "1234567890123" },
{ type: "org.gs1.Code128", value: "PRODUCT-SKU-12345" },
]);
// ... assert that the app handled all scanned codes ...
await cleanup();
```
## Advanced: Custom bounds and corners
Some apps use the position of the detected code in the view. To include position data:
```js theme={null}
// Inject QR Code with Custom Bounds and Corners
import { device } from "@qawolf/flows/ios";
const cleanup = await device.injectBarcode(driver, bundleId, {
type: "org.iso.QRCode",
value: "https://example.com",
bounds: {
x: 0.3, // normalized coordinates (0.0 - 1.0)
y: 0.2,
width: 0.4,
height: 0.6,
},
corners: [
{ x: 0.3, y: 0.2 }, // top-left
{ x: 0.7, y: 0.2 }, // top-right
{ x: 0.7, y: 0.8 }, // bottom-right
{ x: 0.3, y: 0.8 }, // bottom-left
],
});
// ... assert that the app handled the scanned QR code at the expected position ...
await cleanup();
```
## Advanced: Raw binary data
To include raw binary data alongside the decoded value:
```js theme={null}
// Inject QR Code with Raw Binary Data (iOS 13+)
import { device } from "@qawolf/flows/ios";
const cleanup = await device.injectBarcode(driver, bundleId, {
type: "org.iso.QRCode",
value: "https://example.com",
rawValue: Buffer.from("custom binary data"), // also accepts a base64-encoded string
});
// ... assert the app received the raw payload ...
await cleanup();
```
## Default values
If not specified, the following defaults apply:
* **type:** `org.iso.QRCode`
* **bounds:** `{ x: 0.38, y: 0.27, width: 0.32, height: 0.57 }`
* **corners:** Calculated automatically from bounds
* **rawValue:** UTF-8 encoding of the value string
## Full sample test
```js theme={null}
import { flow, device, expect } from "@qawolf/flows/ios";
export default flow(
"iOS Media - AVMetadata Injection",
{
target: "iOS - iPhone 15 (iOS 26)",
launch: {
app: { env: "IPA_BUILD_LOCATION" },
respectSystemAlerts: true,
autoAcceptAlerts: true,
},
},
async ({ driver, test }) => {
await test("App install and open AVMetadata", async () => {
// Tap "Media"
await driver
.$(
`-ios predicate string:name == 'Media' AND type == 'XCUIElementTypeButton'`,
)
.waitForDisplayed();
await driver
.$(
`-ios predicate string:name == 'Media' AND type == 'XCUIElementTypeButton'`,
)
.click();
// Click media dropdown
await driver
.$(
`-ios predicate string:name == 'AVMetadata (Barcode/QRCode)' AND type == 'XCUIElementTypeStaticText'`,
)
.waitForDisplayed();
await driver
.$(
`-ios predicate string:name == 'AVMetadata (Barcode/QRCode)' AND type == 'XCUIElementTypeStaticText'`,
)
.click();
});
await test("QR Code Test", async () => {
const qrCodeLinkToInject = "https://qawolf.com/qr-code";
await device.injectBarcode(driver, process.env.BUNDLE_ID, {
value: qrCodeLinkToInject,
});
await driver.pause(3000);
//--------------------------------
// Assert:
//--------------------------------
expect(
await driver
.$(
`-ios predicate string:name == 'qrLastCodeLabel' AND type == 'XCUIElementTypeStaticText'`,
)
.getText(),
).toBe(`QR Code: ${qrCodeLinkToInject}`);
});
await test("Barcode Scanning Test", async () => {
const barcodeToInject = "1234567890123";
await device.injectBarcode(driver, process.env.BUNDLE_ID, {
type: "org.gs1.EAN-13",
value: barcodeToInject,
});
await driver.pause(3000);
//--------------------------------
// Assert:
//--------------------------------
expect(
await driver
.$(
`-ios predicate string:name == 'qrLastCodeLabel' AND type == 'XCUIElementTypeStaticText'`,
)
.getText(),
).toBe(`EAN-13: ${barcodeToInject}`);
});
},
);
```
# Test Chrome extensions
Source: https://docs.qawolf.com/qawolf/browser-extensions
Load and test Chrome extensions in QA Wolf flows using Playwright's persistent context and Chrome flags.
Use `launch()` with Chrome flags to load an extension into the browser before your flow runs. This lets you test extension popup UIs, content scripts, and extension behavior alongside your app.
Store your extension zip in team storage and reference it via `process.env.TEAM_STORAGE_DIR`. See [Upload files](/qawolf/Uploading-manually) for instructions. Replace `` with your extension's ID, which you can find at `chrome://extensions` with Developer mode enabled.
## Example
```typescript theme={null}
import { flow, launch } from "@qawolf/flows/web";
const extensionPath = `${process.env.TEAM_STORAGE_DIR}/my-extension.zip`;
const { context } = await launch({
browser: "chromium",
persistentContext: true,
args: [
`--disable-extensions-except=${extensionPath}`,
`--load-extension=${extensionPath}`,
],
permissions: ["clipboard-read", "clipboard-write"],
});
const extensionPage = await context.newPage();
await extensionPage.goto(`chrome-extension:///popup.html`);
```
## When to use
* Your app uses a Chrome extension and you need to test the full user flow with the extension installed.
* You need to test the extension's popup UI or options page directly.
* You need to verify that a content script modifies or interacts with a page correctly.
## Full sample test
```typescript theme={null}
import { flow, launch } from "@qawolf/flows/web";
const extensionPath = `${process.env.TEAM_STORAGE_DIR}/my-extension.zip`;
export default flow(
"Test Chrome extension popup",
"Web - Chrome",
async ({ test }) => {
await test("launch extension and verify popup renders", async () => {
// Arrange
const { context } = await launch({
browser: "chrome",
persistentContext: true,
acceptDownloads: true,
args: [
`--allow-file-access-from-files`,
`--disable-extensions-except=${extensionPath}`,
`--load-extension=${extensionPath}`,
`--no-sandbox`,
],
});
await expect
.poll(
() => context.pages(),
{ intervals: [1_000, 2_000, 10_000], timeout: 10_000 },
)
.toHaveLength(2);
const extensionPage = context.pages().at(-1);
// Act
await extensionPage.locator("#open-popup").click();
// Assert
await expect(extensionPage.locator("#popup-content")).toBeVisible();
});
},
);
```
# Camera injection
Source: https://docs.qawolf.com/qawolf/camera-and-audio-injection
Inject images into your app's camera input to test camera features without real hardware.
Camera injection is only available for apps that QA Wolf resigns during installation. It is not available for system apps or Safari.
## Example
**Inject an image into the camera feed:**
```javascript theme={null}
import { device } from "@qawolf/flows/ios";
const bundleId = process.env.BUNDLE_ID; // Bundle ID of app being tested
const storagePath = process.env.STORAGE_PATH; // QA Wolf remote storage
const imagePath = `${storagePath}/large.jpg`;
const cleanup = await device.injectCamera(driver, bundleId, {
data: imagePath,
type: "image", // optional — inferred from .jpg/.png/etc.
});
// ... run your assertions while the camera feed is mocked ...
await cleanup();
```
Once the config is pushed, the camera preview updates to the injected image and loops continuously.
## When to use
* Your app captures photos or displays a camera preview and you want to test that flow without a physical camera setup.
* You need to run the same scenario repeatedly with consistent inputs.
## Supported file types
**Images:** Any format supported by UIImage — JPG, PNG, HEIC, GIF, BMP, TIFF, WebP
## Full sample test
```js theme={null}
import { flow, device, expect } from "@qawolf/flows/ios";
export default flow(
"iOS Media - Camera Photo Injection",
{
target: "iOS - iPhone 15 (iOS 26)",
launch: {
app: { env: "IOS_APP_STAGING" },
respectSystemAlerts: true,
autoAcceptAlerts: true,
},
},
async ({ driver, test }) => {
await test("iOS Media - Camera Photo Injection", async () => {
//--------------------------------
// Arrange:
//--------------------------------
// Install and Launch Trot app
const imageName = "large.jpg";
const baseDir = process.env.BASE_IMAGE_DIR
const imagePath = `${baseDir}/${imageName}`;
// Tap "Media"
await driver
.$(
`-ios predicate string:name == 'Media' AND type == 'XCUIElementTypeButton'`,
)
.waitForDisplayed();
await driver
.$(
`-ios predicate string:name == 'Media' AND type == 'XCUIElementTypeButton'`,
)
.click();
// Tap Video Recording
await driver
.$(
`-ios predicate string:name == 'Video Recording' AND type == 'XCUIElementTypeButton'`,
)
.waitForDisplayed();
await driver
.$(
`-ios predicate string:name == 'Video Recording' AND type == 'XCUIElementTypeButton'`,
)
.click();
// Tap AVCaptureMovieFileOutput
await driver
.$(
`-ios predicate string:name == 'AVCaptureMovieFileOutput' AND type == 'XCUIElementTypeButton'`,
)
.waitForDisplayed();
await driver
.$(
`-ios predicate string:name == 'AVCaptureMovieFileOutput' AND type == 'XCUIElementTypeButton'`,
)
.click();
// Observe "Start Recording" button
await driver
.$(
`-ios predicate string:name == 'Start Recording' AND type == 'XCUIElementTypeStaticText'`,
)
.waitForDisplayed({ timeout: 10000 });
//--------------------------------
// Act:
//--------------------------------
// Inject image as the camera feed
await device.injectCamera(driver, process.env.APP_ID, {
data: `${imagePath}`,
type: "image",
});
await driver.pause(3000);
//--------------------------------
// Assert:
//--------------------------------
// Assert Screenshot of Image File Displaying in Live Preview
const previewElement = driver.$(
`-ios predicate string:name == 'livePreview' AND type == 'XCUIElementTypeOther'`,
);
await previewElement.waitForDisplayed({ timeout: 5000 });
await expect(driver)
.toHaveScreenshot(
previewElement,
"video_recording_image_preview",
{ maxMisMatchPercentage: 5 },
);
});
},
);
```
# How to integrate with CircleCI
Source: https://docs.qawolf.com/qawolf/circle-ci
Connect QA Wolf to CircleCI to automatically trigger test runs when you deploy code from your repository.
Deployment triggers are configured for your main environment per your QA Wolf contract. Contact your QA Wolf representative before setting this up — a QAE must configure a deployment trigger for your environment before deploy notifications can initiate test runs.
Make sure you have:
* Access to your QA Wolf workspace.
* Admin access to your CircleCI project.
* At least one QA Wolf environment already configured with a deployment trigger.
* A QA Wolf API key.
## Find the QAWOLF\_API\_KEY
Open the `Workspace name` dropdown in QA Wolf and click **Workspace Settings**.
Choose **Integrations** and then click the icon to the right of **API Key** under **API Access**.
### Add the QAWOLF\_API\_KEY secret
Open your CircleCI project and go to **Project Settings → Environment Variables**.
Click **Add Environment Variable**. Name it `QAWOLF_API_KEY`, paste your API key, and save.
## Add the notify script to your repository
Create a file at `.circleci/notifyQaWolf.mjs` in the repository that corresponds to the deployments QA Wolf will be testing.
```javascript theme={null}
import assert from "assert";
import { makeQaWolfSdk } from "https://esm.sh/@qawolf/ci-sdk@0.23.0";
const apiKey = process.env.QAWOLF_API_KEY;
assert(apiKey, "QAWOLF_API_KEY is required");
const sha = process.env.CIRCLE_SHA1;
assert(sha, "CIRCLE_SHA1 is required");
const branch = process.env.CIRCLE_BRANCH;
assert(branch, "CIRCLE_BRANCH is required");
const deployConfig = {
branch,
// Required only if the target trigger requires matching a deployment type
deploymentType: "staging", // e.g., "production", "staging", "qa"
// Optional: Include deployment URL to override URL environment variable for the run
deploymentUrl: undefined,
hostingService: "GitHub", // Set to where your repo is hosted, e.g. "GitHub" or "GitLab"
// Optional: Include pull request number for PR testing
// pullRequestNumber: 123,
// Recommended: Include repository information
repository: {
name: "your-repo-name",
owner: "your-org-name",
},
sha,
};
const { attemptNotifyDeploy } = makeQaWolfSdk({ apiKey });
const result = await attemptNotifyDeploy(deployConfig);
if (result.outcome !== "success") {
// Fail the job.
throw Error(`Failed to notify QAWolf: ${JSON.stringify(result)}`);
}
// result.runId can be output from the job to be used in a CI-greenlight job.
```
Replace `deploymentType` with the value your QA Wolf representative provides. Replace `name` and `owner` under `repository` with your actual repository details. Set `hostingService` to match where your code is hosted — `"GitHub"` or `"GitLab"` — not where your pipeline runs.
## Add the notify job to your CircleCI config
Add the `notify-qa-wolf` job to your `.circleci/config.yml` and place it in your workflow after your deploy step.
```yaml theme={null}
version: 2.1
orbs:
node: circleci/node@7.0.0
jobs:
# build: TODO
# deploy: TODO
notify-qa-wolf:
executor: node/default
steps:
- checkout
- run: node --version
- run: node --experimental-network-imports .circleci/notifyQaWolf.mjs
workflows:
build-deploy-test-workflow:
jobs:
# - build
# - deploy
- notify-qa-wolf
```
The `notify-qa-wolf` job must:
* Use the `node/default` executor.
* Check out your code.
* Run `node --experimental-network-imports .circleci/notifyQaWolf.mjs`.
Place it after your deploy job in the workflow so it runs only once your environment is healthy.
## Verify the integration
Push a new commit and wait for your CircleCI pipeline to complete.
Open QA Wolf and confirm a new run appears under the expected environment.
## Related
* [QA Wolf CI SDK](/qawolf/other-ci-node)
# Connect to a VPN (web)
Source: https://docs.qawolf.com/qawolf/connect-with-vpn-web
Start an OpenVPN tunnel from a web flow to reach internal services or staging environments.
This helper requires runner support. In QA Wolf-managed runs, the runner configures this automatically. If you're building a custom runner, see the [Client Reference](/qawolf/libraries/testkit/api-reference/client).
## Examples
**Start an OpenVPN tunnel**
```typescript theme={null}
import { startOpenVpn } from "@qawolf/testkit";
const pid = await startOpenVpn({
configPath: "/tmp/client.ovpn",
});
```
**Limit routing to specific hosts**
```typescript theme={null}
import { startOpenVpn } from "@qawolf/testkit";
const pid = await startOpenVpn({
configPath: "/tmp/client.ovpn",
routeHosts: ["internal.example.com", "api.staging.io"],
});
```
## When to use
* Your web app connects to internal or staging services that aren't publicly accessible.
* Your flow needs to reach an API behind a corporate firewall.
* You need to test against a staging environment that requires VPN access.
## Full example
```typescript theme={null}
import { flow } from "@qawolf/flows/web";
import { startOpenVpn } from "@qawolf/testkit";
export default flow(
"Test internal API",
{ target: "Web - Chrome", launch: true },
async ({ page, test }) => {
await test("connect to VPN", async () => {
await startOpenVpn({
configPath: "/tmp/client.ovpn",
routeHosts: ["internal.example.com"],
});
});
await test("navigate to internal service", async () => {
await page.goto(process.env["INTERNAL_URL"]!);
await expect(page).toHaveURL(/internal/);
});
},
);
```
# webhooks/deploy_success
Source: https://docs.qawolf.com/qawolf/deploy-success
Notify QA Wolf of a successful deployment to trigger a test run, including request format, headers, body fields, and example responses.
`POST https://app.qawolf.com/api/webhooks/deploy_success`
If your build server supports `node`, use [`@qawolf/ci-sdk`](/qawolf/libraries/ci-sdk/api-reference) instead of calling this endpoint directly. This gives you type safety and clearer errors and output.
## Request
```bash theme={null}
curl -X POST https://app.qawolf.com/api/webhooks/deploy_success \
-H "Authorization: Bearer $QAWOLF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"branch": "main",
"sha": "de12adda500f2bc5a29dbd89f4fb1b0e1a31de81",
"deployment_type": "staging",
"deployment_url": "https://staging.example.com",
"hosting_service": "GitHub",
"workspaceId": "ckz2y0o7j01914cgtra6lk4wn"
}'
```
## Request headers
| Header | Value |
| --------------- | ----------------------- |
| `Authorization` | `Bearer ` |
| `Content-Type` | `application/json` |
## Request body
All fields are optional.
### Commonly used
| Field | Type | Description |
| ----------------- | ------ | --------------------------------------------------------------------------------------------------- |
| `branch` | string | Git branch name. Used to display in the UI and match pull requests on any linked repo. |
| `sha` | string | Git commit SHA. Used to create GitHub commit checks and display a link to the commit in the run UI. |
| `deployment_type` | string | Required if the target trigger is configured to match a deployment type. |
| `deployment_url` | string | Overrides the environment URL. Available in tests as `process.env.URL`. |
### Advanced
| Field | Type | Description |
| ----------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `hosting_service` | string | `"GitHub"` or `"GitLab"`. Defaults to `"GitHub"`. |
| `commit_url` | string | Pass with `sha` if no hosting service repo is configured. Makes the commit ID a clickable link in the QA Wolf UI. |
| `variables` | object | Key/value pairs that override environment variables for every run triggered by this notification. |
| `deduplication_key` | string | Custom key controlling run cancellation behavior. By default, new runs cancel ongoing runs with the same branch/environment combination. |
| `ephemeral_environment` | boolean | Pass with `deployment_url` if the deployment is not associated with a code-hosting integration. |
| `workspaceId` | string | Workspace to create runs in. Ignored by a workspace API key, which already implies its workspace. With an organization or user API key, omitting it uses your organization's first-created workspace. See [which workspace a request acts on](/qawolf/rest-overview#which-workspace-a-request-acts-on). |
## Response
```json theme={null}
{
"results": [
{
"created_suite_id": "cl1f6i0in15676w115vt43vw2",
"outcome": "created",
"trigger_id": "ckzoog9wy01720xyrvl8ah7gu"
}
]
}
```
`results` is an array of matched triggers. Each entry carries an `outcome` of `created`, `skipped`, or `failed`, alongside `created_suite_id` if a run was created, `duplicate_suite_id` if a run for this deployment already exists, or `failure_code` and `failure_message` if the run could not be created. Branch on `failure_code`, such as `billing-prevented` or `environment-not-ready`, and display `failure_message`. Treat `failure_code` as an open set, since new codes are added over time.
A failed entry also carries `failure_reason`, which is deprecated. It holds an internal diagnostic whose values change without notice, so do not branch on it.
`warning` is present when the request needed a workspace and did not name one, in which case it reports the workspace QA Wolf used. It never changes the outcome. `@qawolf/ci-sdk` prints it for you.
```json theme={null}
{
"results": [
{
"created_suite_id": "cl1f6i0in15676w115vt43vw2",
"outcome": "created",
"trigger_id": "ckzoog9wy01720xyrvl8ah7gu"
}
],
"warning": "No workspaceId was sent, so this request used your first-created workspace: Acme (ckz2y0o7j01914cgtra6lk4wn). Send workspaceId to choose explicitly."
}
```
On the first delivery of a deployment, a matched trigger returns `created_suite_id` even when the deployment duplicates one QA Wolf is already handling — deduplication is settled after this response is sent, so the synchronous response does not report it. To find out whether your run was superseded by a duplicate, poll [CI greenlight](/qawolf/v0-ci-greenlight) and compare `relevantRunId` with `rootRunId` (see [Superseding logic](/qawolf/v0-ci-greenlight#superseding-logic)).
You see `duplicate_suite_id` only when retrying `deploy_success` for the same `sha` after deduplication has already settled. On the first delivery, a matched trigger is never reported as a duplicate.
## Response codes
| Code | Description |
| ----- | ------------------------------------------------------------------------- |
| `200` | Request accepted. Inspect the response body to confirm a run was created. |
| `401` | Missing or invalid API key. |
| `403` | Forbidden. Usually indicates a disabled team. |
| `404` | Run not found. |
| `405` | Method not allowed. Use `POST`. |
| `500` | Internal server error. Contact support if the issue persists. |
A 200 response does not guarantee a run was created. Inspect the response body to confirm.
## Related
* [`@qawolf/ci-sdk`](/qawolf/libraries/ci-sdk/api-reference)
* [CI greenlight](/qawolf/v0-ci-greenlight)
# Testing Electron apps
Source: https://docs.qawolf.com/qawolf/electron
Test a desktop app built with Electron from a QA Wolf flow.
Electron flows use `@qawolf/flows/web`. The callback receives the first window as `page`, giving you the full Playwright API for interacting with the app.
## Examples
**Launch and interact with an Electron app**
```typescript theme={null}
import { flow } from "@qawolf/flows/web";
export default flow(
"Sign in to desktop app",
{
target: "Electron",
launch: { executablePath: "/Applications/MyApp.app/Contents/MacOS/MyApp" },
},
async ({ page, test }) => {
await test("sign in", async () => {
await page.getByRole("button", { name: "Sign in" }).click();
});
},
);
```
**Choose the executable path at runtime**
Use explicit launch when the path depends on runtime logic — for example, switching between a stable and canary build.
```typescript theme={null}
import { flow, launch } from "@qawolf/flows/web";
export default flow("Sign in to desktop app", "Web - Chrome", async () => {
const useCanary = process.env.USE_CANARY_APP === "true";
const { firstWindowPage } = await launch({
kind: "electron",
executablePath: useCanary
? "/Applications/MyApp Canary.app/Contents/MacOS/MyApp"
: "/Applications/MyApp.app/Contents/MacOS/MyApp",
});
await firstWindowPage.getByRole("button", { name: "Sign in" }).click();
});
```
## When to use
* Your app is a desktop application built with Electron.
* You need to test app startup, window behavior, or native OS integrations.
* Your flow needs to interact with the first Electron window before any navigation.
* You want to switch between builds — stable vs canary — at runtime.
## Notes
**Declarative vs explicit launch**
For most Electron flows, use the declarative style with `target: "Electron"` and `launch.executablePath`. The callback receives the first window as `page` automatically.
Use explicit `launch({ kind: "electron", executablePath })` only when the executable path depends on runtime logic. In this case, the first window is returned as `firstWindowPage` rather than injected as `page`.
For the full Electron launch shape, see the [Web API Reference](/qawolf/libraries/flows/api-reference/web).
# webhooks/environment_terminated
Source: https://docs.qawolf.com/qawolf/environment_terminated
Stops all runs targeting the environment and requests flow promotion to the static base environment. The environment will show as "closed" in QA Wolf.
`POST https://app.qawolf.com/api/webhooks/environment_terminated`
If your build server supports `node`, use [`@qawolf/ci-sdk`](/qawolf/libraries/ci-sdk/api-reference) instead of calling this endpoint directly. This gives you type safety and clearer errors and output.
## Request
```bash theme={null}
curl -X POST https://app.qawolf.com/api/webhooks/environment_terminated \
-H "Authorization: Bearer $QAWOLF_API_KEY" \
-H "Content-Type: application/json" \
-d '{"deploymentUrl": "", "workspaceId": ""}'
```
## Request headers
| Header | Value |
| --------------- | ----------------------- |
| `Authorization` | `Bearer ` |
| `Content-Type` | `application/json` |
## Request body
Pass one of the following to identify the environment.
| Field | Type | Description |
| ------------------ | ------ | ---------------------------------------------------------------------------------------------------------- |
| `environmentId` | string | ID of the ephemeral environment to terminate. |
| `environmentAlias` | string | Alias of the ephemeral environment to terminate. |
| `deploymentUrl` | string | Preview URL used when the environment was created via `deploy_success` with `ephemeral_environment: true`. |
You may also pass `workspaceId`.
| Field | Type | Description |
| ------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `workspaceId` | string | Workspace that owns the environment. Only used alongside `environmentAlias` or `deploymentUrl`, since `environmentId` identifies its own workspace. Ignored by a workspace API key. With an organization or user API key, omitting it looks the alias up in your organization's first-created workspace. See [which workspace a request acts on](/qawolf/rest-overview#which-workspace-a-request-acts-on). |
## Response
```json theme={null}
{
"code": "terminated",
"environmentId": "cl1f6i0in15676w115vt43vw2"
}
```
`warning` is present when the request needed a workspace and did not name one, in which case it reports the workspace QA Wolf used. It never changes the outcome. `@qawolf/ci-sdk` prints it for you.
## Response codes
| Code | Description |
| ----- | ------------------------------------------------------------- |
| `200` | Environment terminated successfully. |
| `401` | Missing or invalid API key. |
| `403` | Forbidden. Usually indicates a disabled team. |
| `404` | Environment not found. |
| `409` | Conflict. Environment is already terminated. |
| `500` | Internal server error. Contact support if the issue persists. |
## Related
* [`@qawolf/ci-sdk`](/qawolf/libraries/ci-sdk/api-reference)
* [Notify deploy](/qawolf/v0-ci-greenlight)
# Get the most out of AI flow creation
Source: https://docs.qawolf.com/qawolf/getting-the-most-out-of-automate
Tips for writing better prompts and working more effectively with AI flow creation.
AI flow creation works best when you give the agent enough context to understand what you want and where to put it. It supports two ways of working:
No human in the loop. Front-load all the context into one prompt, hit run, and review the finished flow.
A copilot workflow. Prompt for one small change at a time, discovering the app together with the AI.
Both modes benefit from context you write down once. If you find yourself repeating the same background across sessions, put it in a [SKILL.md](/qawolf/reusable-ai-knowledge).
## Autonomous creation
In this mode the creation agent works unattended, so everything it needs to know has to be in the prompt before you hit run.
### Write a specific prompt
Vague prompts produce unreliable results. A prompt like "Create a test for the rate quote flow" gives the agent very little to work with. A stronger prompt includes:
* Where to start (URL and login credentials)
* What the user does, step by step
* What a successful outcome looks like
* Anything specific you want asserted
**Example:**
```handlebars wrap theme={null}
Create a test for the rate quote application. Go to {{your app URL}} and sign in with {{test user email}} and {{test user password}}. Navigate to the rate quote form, fill in the required fields, and submit. Assert that the confirmation message appears and that the new rate quote is visible in the list.
```
### Include assertions explicitly
If there's something specific you need verified, say so. The agent won't always infer what matters to you — telling it what to assert produces more useful tests.
### Link your app's documentation
If a flow depends on setup that is only explained in your documentation, include the link in the flow description or your prompt. The AI reads linked documentation before it starts building and uses it to complete configuration steps it cannot discover from the UI alone.
```handlebars wrap theme={null}
Create a test for connecting a Jira project. Follow the setup guide at {{your docs URL}} to configure the integration first.
```
### Structure the flow header around Arrange, Act, Assert
When the agent completes a flow on its own, the comment block at the top of the flow is its specification. Structuring that header around [Arrange, Act, Assert](/qawolf/anatomy-of-a-qa-wolf-test-mobile-edition#the-aaa-framework) tells the agent how far each verification should go.
For example, a flow that disables a feature could reload the page and check that the toggle is still off. It could also go further and sign in as a different user to verify the feature is no longer accessible. An AAA header states which one you want, so the agent doesn't have to guess:
```typescript theme={null}
/**
* Goal: Editor project transfers
*
* Test 1:
* Arrange: Log in as an enterprise owner and an enterprise editor. As the
* owner, enable editor project transfers in workspace settings under
* Privacy and Security.
* Act: As the editor, open the owner's project and go to project settings.
* Assert: The Move button is enabled.
*
* Test 2:
* Arrange: As the owner, disable editor project transfers.
* Act: As the editor, reload project settings.
* Assert: The Move button is disabled; check the tooltip content.
*/
```
### Use descriptive flow names
Before building a new flow, the AI searches your workspace for similar flows and reuses the parts that overlap instead of starting from scratch. Descriptive names help it find the right ones. If you want it to work from a specific example, [point it at a working reference](#point-the-agent-at-a-working-reference).
***
## Guided creation
In this mode you stay in the loop, hand-holding the creation agent to completion. Use it when you don't have the full context yourself and are discovering the app at the same time as the AI, so it makes sense to work the problem together.
### Start prompts with "Insert code to"
When adding code at a specific point in a test, beginning your prompt with "Insert code to" helps the agent understand it should add rather than replace.
```text theme={null}
Insert code to click the Submit button and wait for the confirmation message.
```
### Control where code is inserted
The agent inserts code relative to the current cursor position. If code ends up in the wrong place:
* Click on the line where you want the code inserted before prompting
* Or tell it explicitly: "Insert code on line 42 to..."
### Describe elements in plain language
You don't need to tell the agent which Playwright locator to use. It's better at finding the most relevant locator itself. If you prescribe a locator and it turns out not to work, the agent gets stuck between obeying your instruction and producing a working test, and the result suffers.
Say *"Click the Save button in the billing settings panel"*, not *"Click `page.locator('#btn-save-2')`"*.
### Hover before you prompt for elements
When targeting an element inside a container — for example, the first link within a specific panel — hover over the container for a second, then click the element. This gives the agent locators for both, and you can ask it to chain them:
```text theme={null}
Click the first link within the Recent Activity panel.
```
If you hover only on the element itself, the agent may use a less precise locator.
### Trigger actions before prompting
For interactions that produce a result (a download, a new page, a modal), trigger the action yourself first and let it complete. Then tell the agent what happened. This helps it understand the outcome, not only the selector.
**Example:**
1. Click the download button and wait for the download to start
2. Then prompt: "Click the download button"
### Use descriptive language for context
A few phrases that help the agent understand what it's working with:
* **"in the row"** — when an element is inside a table row
* **"file"** — for file-related actions like clicking download or handling file inputs
### Refactor whole-file changes
For changes that apply across the entire test — updating all comments, removing hardcoded values, renaming variables — use "refactor" to signal that the change is global:
```text theme={null}
Refactor to replace all hardcoded email addresses with the TEST_USER_EMAIL environment variable.
```
### Move code by selecting it first
If you need the agent to move a block of code, select all the lines you want moved before prompting. This removes ambiguity about what should be relocated.
### Fix errors directly
If a run produces an error, you can ask the agent to address it without explaining the cause:
```text theme={null}
Fix the error.
```
The agent will read the error output and attempt a fix.
### Point the agent at a working reference
If you have a working example that you want the agent to follow, give it the path to that file and tell it what you'd like it to copy.
```handlebars theme={null}
Use the new apiLogin method as exemplified in this file: {{path to file}}
```
This will prevent having to re-describe the change each time.
# Beacon injection (iOS)
Source: https://docs.qawolf.com/qawolf/i-beacon-injection
Simulate iBeacon proximity events in your iOS app without physical beacon hardware.
Use `device.injectBeacon()` to simulate the device detecting a nearby iBeacon, triggering `CLLocationManager` callbacks in your app as if real hardware were present. Pushing a `.region` file triggers a region-entry callback; also pushing a `.beacon` file triggers ranging callbacks.
See the [iOS Device Reference](/qawolf/libraries/flows/api-reference/ios-device-reference) for the full `injectBeacon` API.
## Examples
**Region entry and ranging**
```typescript theme={null}
const cleanup = await device.injectBeacon(driver, "com.example.app", {
uuid: "8613BEAD-5465-4515-8F9C-AEEA717484C9",
beacons: [{ major: 1, minor: 7 }],
});
// assert your app responded to the beacon event
await cleanup();
```
**Region entry only (no ranging)**
Omit the `beacons` array to trigger only a region-entry event.
```typescript theme={null}
const cleanup = await device.injectBeacon(driver, "com.example.app", {
uuid: "e62c96fd-014a-454f-9b41-32245d802bb3",
});
// assert your app responded to the region entry
await cleanup();
```
## When to use
* Your app uses iBeacon proximity for retail check-ins, loyalty triggers, or location-based promotions.
* Your app uses indoor navigation or asset tracking via Bluetooth beacons.
* Your app responds to beacon region entry or exit events.
* Your test needs to verify ranging callbacks with specific major/minor values.
## Location permissions
iBeacons require location permissions. Two important caveats:
**`autoAcceptAlerts` chooses "Ask Next Time" for location dialogs.** If the permission dialog appears, Appium will not choose "Always Allow" — and there is no way to change it after the fact.
**Background location access cannot be auto-accepted.** If your app requires "Change to Always Allow" for background beacon detection, you must handle the dialog manually using `respectSystemAlerts: true`:
```typescript theme={null}
const { driver: d } = await launch({
bundleId: "com.example.app",
respectSystemAlerts: true,
});
const allowBtn = d.$(`//XCUIElementTypeButton[@name='Allow']`);
if (await allowBtn.isExisting()) {
await allowBtn.click();
}
```
`respectSystemAlerts: true` may slow down Appium performance. Use it only when you need to interact with system dialogs manually.
## Full sample test
```typescript theme={null}
import { device, flow } from "@qawolf/flows/ios";
export default flow(
"Test beacon proximity",
{ target: "iOS - iPhone 15 (iOS 26)", launch: true },
async ({ driver, test }) => {
await test("inject beacon and verify app response", async () => {
// Arrange
const allowBtn = driver.$(`//XCUIElementTypeButton[@name='Allow']`);
if (await allowBtn.isExisting()) {
await allowBtn.click();
}
await driver.$(`//XCUIElementTypeStaticText[@name='Nearby']`).waitForDisplayed({ timeout: 10_000 });
// Act
const cleanup = await device.injectBeacon(driver, process.env.BUNDLE_ID, {
uuid: "8613BEAD-5465-4515-8F9C-AEEA717484C9",
beacons: [{ major: 1, minor: 7 }],
});
// Assert
await driver
.$(`//XCUIElementTypeStaticText[@name='Beacon Detected']`)
.waitForDisplayed({ timeout: 10_000 });
await cleanup();
});
},
);
```
# Audio analysis (iOS)
Source: https://docs.qawolf.com/qawolf/i-os-audio-analysis
Compare recorded audio against a reference file using Chromaprint fingerprinting to verify your app plays the correct audio.
Use `device.calculateAudioFingerprint()` to generate a Chromaprint fingerprint from a recorded audio file and compare it against a known reference. Chromaprint supports fuzzy matching — small differences in volume or encoding don't affect the result, making it resilient to minor variations in playback.
See the [iOS Device Reference](/qawolf/libraries/flows/api-reference/ios-device-reference) for the full API.
Store your reference audio file in team storage and reference it via `process.env.TEAM_STORAGE_DIR`. See [Upload files](/qawolf/Uploading-manually) for instructions. `stopSpeakerRecording()` automatically calculates a fingerprint — you only need to call `calculateAudioFingerprint()` separately for the reference file.
## Examples
**Compare recorded audio against a reference**
```typescript theme={null}
import { readFile } from "node:fs/promises";
const session = await device.startSpeakerRecording(driver);
// trigger audio playback in your app
const file = await device.stopSpeakerRecording(driver, session.id);
const buffer = await device.downloadSpeakerRecording(driver, file.filename);
const recordingFingerprint = file.fingerprint ??
(await device.calculateAudioFingerprint(driver, buffer)).fingerprint;
const referenceBuffer = await readFile(`${process.env.TEAM_STORAGE_DIR}/reference.wav`);
const { fingerprint: referenceFingerprint } =
await device.calculateAudioFingerprint(driver, referenceBuffer);
const result = findBestMatch(recordingFingerprint, referenceFingerprint);
expect(result.similarity).toBeGreaterThan(0.85);
```
## When to use
* Your app plays audio and you need to verify the correct track or sound effect played.
* Your app uses text-to-speech and you need to validate the output matches expected speech.
* Your app plays audio prompts and you need to confirm they haven't changed after an update.
* Your app has audio-dependent features and functional assertions aren't sufficient.
## Similarity thresholds
| Threshold | Use case |
| --------- | -------------------------------------------------------------------------- |
| `> 0.95` | Near-identical audio — same file, minimal encoding differences |
| `> 0.85` | Same content with minor volume or quality variations (recommended default) |
| `> 0.70` | Looser match — same song or speech with notable differences |
## Full sample test
```typescript theme={null}
import { device, flow } from "@qawolf/flows/ios";
import { readFile } from "node:fs/promises";
const REFERENCE_AUDIO_PATH = `${process.env.TEAM_STORAGE_DIR}/reference.wav`;
export default flow(
"Verify audio playback",
{ target: "iOS - iPhone 15 (iOS 26)", launch: true },
async ({ driver, test }) => {
await test("record audio and compare against reference", async () => {
// Arrange
await driver.$(`//XCUIElementTypeButton[@name='Play']`).waitForDisplayed({ timeout: 10_000 });
// Act
const session = await device.startSpeakerRecording(driver);
await driver.$(`//XCUIElementTypeButton[@name='Play']`).click();
await driver.pause(10_000);
const file = await device.stopSpeakerRecording(driver, session.id);
const buffer = await device.downloadSpeakerRecording(driver, file.filename);
// Assert
const recordingFingerprint = file.fingerprint ??
(await device.calculateAudioFingerprint(driver, buffer)).fingerprint;
const referenceBuffer = await readFile(REFERENCE_AUDIO_PATH);
const { fingerprint: referenceFingerprint } =
await device.calculateAudioFingerprint(driver, referenceBuffer);
const result = findBestMatch(recordingFingerprint, referenceFingerprint);
expect(result.similarity).toBeGreaterThan(0.85);
});
},
);
```
# Microphone injection (iOS)
Source: https://docs.qawolf.com/qawolf/ios-audio-injection
Inject audio into your app's microphone input to test audio features without real hardware.
Use `device.injectAudio()` to replace your app's microphone input with a pre-recorded audio file. This lets you test voice recording, speech recognition, and audio processing features under controlled, repeatable conditions.
See the [iOS Device Reference](/qawolf/libraries/flows/api-reference/ios-device-reference) for the full API.
Audio injection is only available for apps that QA Wolf resigns during installation. It is not available for system apps or Safari.
## Examples
**Inject audio into the microphone**
```typescript theme={null}
const cleanup = await device.injectAudio(driver, process.env.BUNDLE_ID, {
data: `${process.env.TEAM_STORAGE_DIR}/sample_audio_input.wav`,
});
// trigger the microphone feature in your app
await cleanup();
```
**Pull the recording off the device**
```typescript theme={null}
const recordingAsBase64 = await driver.execute("mobile: pullFile", {
remotePath: `@${process.env.BUNDLE_ID}:/path/to/recording.m4a`,
});
const buffer = Buffer.from(recordingAsBase64, "base64");
const { writeFile } = await import("node:fs/promises");
await writeFile(`${process.env.TEAM_STORAGE_DIR}/recording.m4a`, buffer);
```
## When to use
* Your app records audio or processes microphone input and you need to test that flow with known audio data.
* Your app has voice commands or speech recognition features that need consistent, repeatable input.
* Your app validates or transforms microphone input and you need to assert on the output.
* Your test needs to simulate a specific audio input that is difficult to produce in a test environment.
## Supported file types
WAV
## Full sample test
```typescript theme={null}
import { device, flow } from "@qawolf/flows/ios";
const audioPath = `${process.env.TEAM_STORAGE_DIR}/sample_audio_input.wav`;
export default flow(
"Test microphone input",
{
target: "iOS - iPhone 15 (iOS 26)",
launch: {
app: { env: "IPA_BUILD_LOCATION" },
respectSystemAlerts: true,
autoAcceptAlerts: true,
},
},
async ({ driver, test }) => {
await test("inject audio and verify recording was saved", async () => {
// Arrange
await driver.$(`//XCUIElementTypeButton[@name='Record Audio']`).click();
// Act
const cleanup = await device.injectAudio(driver, process.env.BUNDLE_ID, {
data: audioPath,
});
await driver.$(`//XCUIElementTypeButton[@name='Record']`).click();
await driver.pause(10_000);
await driver.$(`//XCUIElementTypeButton[@name='Done']`).click();
await cleanup();
// Assert
const recordingAsBase64 = await driver.execute("mobile: pullFile", {
remotePath: `@${process.env.BUNDLE_ID}:/path/to/recording.m4a`,
});
const buffer = Buffer.from(recordingAsBase64, "base64");
const { writeFile } = await import("node:fs/promises");
await writeFile(`${process.env.TEAM_STORAGE_DIR}/recording.m4a`, buffer);
expect(buffer.length).toBeGreaterThan(0);
});
},
);
```
# API Reference
Source: https://docs.qawolf.com/qawolf/libraries/ci-sdk/api-reference
Reference for the @qawolf/ci-sdk TypeScript package, including installation, initialization, and functions for triggering and polling test runs from CI.
`@qawolf/ci-sdk` provides a TypeScript SDK (CJS and ESM compatible) for interacting with the QA Wolf API from your CI pipeline.
## Installation
```bash theme={null}
npm install @qawolf/ci-sdk
```
Requires Node.js 18 or later.
```javascript theme={null}
import { fetch } from "undici";
const sdk = makeQaWolfSdk({ apiKey: process.env.QAWOLF_API_KEY }, { fetch });
```
## makeQaWolfSdk
The entry point for all SDK functions. Pass your `QAWOLF_API_KEY` to initialize.
```javascript theme={null}
import { makeQaWolfSdk } from "@qawolf/ci-sdk";
const {
attemptNotifyDeploy,
pollCiGreenlightStatus,
makePollCiGreenlightStatusIterator,
notifyTerminatedEphemeralEnvironment,
generateSignedUrlForRunInputsExecutablesStorage,
} = makeQaWolfSdk({
apiKey: process.env.QAWOLF_API_KEY,
});
```
SDK functions do not throw. They return a result object with an `outcome` field. Always inspect the outcome to determine whether your CI step should pass or fail.
## attemptNotifyDeploy
Notifies QA Wolf of a successful deployment, which starts a test run if one is configured. See [webhooks/deploy\_success](/qawolf/deploy-success) for the underlying endpoint.
```javascript theme={null}
import { type DeployConfig, makeQaWolfSdk } from "@qawolf/ci-sdk";
const { attemptNotifyDeploy } = makeQaWolfSdk({
apiKey: process.env.QAWOLF_API_KEY,
});
const deployConfig: DeployConfig = {
branch: undefined,
sha: undefined,
deploymentType: "staging",
deploymentUrl: undefined,
hostingService: undefined,
commitUrl: undefined,
deduplicationKey: undefined,
variables: undefined,
workspaceId: process.env.QAWOLF_WORKSPACE_ID,
};
const result = await attemptNotifyDeploy(deployConfig);
if (result.outcome !== "success") {
process.exit(1);
}
const runId = result.runId;
// Store runId as a CI job output to pass to pollCiGreenlightStatus.
```
### DeployConfig fields
| Field | Type | Description |
| ---------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `branch` | string | VCS branch name. |
| `sha` | string | VCS commit SHA. |
| `deploymentType` | string | Required if the target trigger matches on deployment type. |
| `deploymentUrl` | string | Overrides the environment URL. Available in tests as `process.env.URL`. |
| `hostingService` | string | `"GitHub"` or `"GitLab"`. Defaults to `"GitHub"`. |
| `commitUrl` | string | Pass with `sha` if no hosting service repo is configured. Makes the commit ID a clickable link in the QA Wolf UI. |
| `pullRequestNumber` | number | VCS PR number, for PR testing. |
| `mergeRequestNumber` | number | VCS MR number, for MR testing. |
| `repository.name` | string | Repository name. |
| `repository.owner` | string | Repository owner or organization (VCS). |
| `repository.namespace` | string | Repository namespace or group (VCS). |
| `ephemeralEnvironment` | boolean | Pass with `deploymentUrl` for ephemeral environments without a code-hosting integration. |
| `deduplicationKey` | string | Custom key controlling run cancellation behavior. |
| `variables` | object | Key/value pairs that override environment variables for triggered runs. |
| `workspaceId` | string | Workspace to create runs in. Ignored by a workspace API key, which already implies its workspace. Pass it with an organization or user API key. See [which workspace a request acts on](/qawolf/rest-overview#which-workspace-a-request-acts-on). |
### Result fields
| Field | Description |
| ------------- | ---------------------------------------------------------------------------------- |
| `outcome` | `"success"`, `"failed"`, or `"aborted"`. |
| `runId` | ID of the triggered run. Pass to `pollCiGreenlightStatus`. |
| `failReason` | Present when `outcome` is `"failed"`. Reason the notification failed. |
| `abortReason` | Present when `outcome` is `"aborted"`. Reason the notification was aborted. |
| `httpStatus` | HTTP status code if the notification failed or was aborted due to a network error. |
A run is only created if there is a matching trigger in your QA Wolf configuration. `outcome: "success"` means the notification was accepted, not that a run was created. Check `runId` to know whether a run was created.
## pollCiGreenlightStatus
Polls the [CI greenlight endpoint](/qawolf/v0-ci-greenlight) until the run completes and returns whether it is safe to release.
```javascript theme={null}
import { makeQaWolfSdk } from "@qawolf/ci-sdk";
const { pollCiGreenlightStatus } = makeQaWolfSdk({
apiKey: process.env.QAWOLF_API_KEY,
});
const { outcome } = await pollCiGreenlightStatus({
runId,
onRunStageChanged: (current, previous) => {
console.log(current, previous);
},
abortOnSuperseded: false,
});
if (outcome !== "success") {
process.exit(1);
}
```
### Options
| Field | Type | Description |
| ------------------- | -------- | -------------------------------------------------------------------------- |
| `runId` | string | The run ID returned by `attemptNotifyDeploy`. |
| `onRunStageChanged` | function | Optional callback fired when the run stage changes. |
| `abortOnSuperseded` | boolean | Defaults to `false`. When `true`, polling aborts if the run is superseded. |
| `pollTimeout` | number | Timeout in milliseconds. Defaults to two hours. |
### Result fields
| Field | Description |
| ------------- | ----------------------------------------------------------------------------------- |
| `outcome` | `"success"`, `"failed"`, or `"aborted"`. Only `"failed"` indicates bugs were found. |
| `abortReason` | Present when `outcome` is `"aborted"`. Reason polling stopped. |
| `httpStatus` | HTTP status code if polling was aborted due to a network error. |
### Advanced: makePollCiGreenlightStatusIterator
An async generator for fine-grained control over the polling lifecycle. Use this when you need custom early-exit logic — for example, proceeding after a time limit or when bug counts are within an acceptable threshold.
```javascript theme={null}
import { makeQaWolfSdk } from "@qawolf/ci-sdk";
const { makePollCiGreenlightStatusIterator } = makeQaWolfSdk({
apiKey: process.env.QAWOLF_API_KEY,
});
let underReviewStartTime: number | null = null;
const iterator = makePollCiGreenlightStatusIterator({ runId: "your-run-id" });
for await (const iteration of iterator) {
if (iteration.isAborted) {
console.error(`Poll aborted: ${iteration.abortReason}`);
process.exit(1);
}
const { status, stageChanged } = iteration;
switch (status.runStage) {
case "initializing":
break;
case "underReview":
if (stageChanged) underReviewStartTime = Date.now();
const timeInReview = underReviewStartTime ? Date.now() - underReviewStartTime : 0;
if (timeInReview > 10 * 60 * 1000) {
if (status.blockingBugsCount > 5) process.exit(1);
}
break;
case "completed":
if (!status.greenlight) process.exit(1);
return;
case "canceled":
process.exit(1);
default:
status.runStage satisfies never;
throw new Error(`Unexpected run stage: ${status.runStage}`);
}
}
```
Each iteration yields either a status update or an abort notification:
| Field | Present when | Description |
| ---------------- | ------------------ | ------------------------------------------------------------------- |
| `isAborted` | always | `false` for status updates, `true` for abort notifications. |
| `status` | `isAborted: false` | Current `CiGreenlightStatus` from the API. |
| `previousStatus` | `isAborted: false` | Status from the previous iteration. `undefined` on first iteration. |
| `stageChanged` | `isAborted: false` | `true` if the run stage changed from the previous iteration. |
| `elapsedMs` | always | Milliseconds elapsed since polling started. |
| `abortReason` | `isAborted: true` | Why polling was aborted. |
| `httpStatus` | `isAborted: true` | HTTP status code if applicable. |
Handle all run stages in your `switch` statement. The `default: status.runStage satisfies never` pattern provides compile-time safety — TypeScript will error if a new stage is added and your code doesn't handle it.
## notifyTerminatedEphemeralEnvironment
Notifies QA Wolf that an ephemeral environment has been terminated. Stops all runs targeting the environment and triggers flow promotion. See [webhooks/environment\_terminated](/qawolf/environment_terminated) for the underlying endpoint.
```javascript theme={null}
import {
type NotifyTerminatedEphemeralEnvironmentInput,
makeQaWolfSdk,
} from "@qawolf/ci-sdk";
const { notifyTerminatedEphemeralEnvironment } = makeQaWolfSdk({
apiKey: process.env.QAWOLF_API_KEY,
});
const terminateConfig: NotifyTerminatedEphemeralEnvironmentInput = {
deploymentUrl: "https://preview-123.example.com",
};
const result = await notifyTerminatedEphemeralEnvironment(terminateConfig);
if (result.outcome !== "success") {
process.exit(1);
}
const environmentId = result.environmentId;
```
### Input fields
Pass one of the following to identify the environment:
| Field | Type | Description |
| ------------------ | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `environmentId` | string | ID of the ephemeral environment. |
| `environmentAlias` | string | Alias of the ephemeral environment. |
| `deploymentUrl` | string | Preview URL used when the environment was created. This should be the same `deploymentUrl` you passed to `attemptNotifyDeploy` along with `ephemeralEnvironment`: true when originally notifying us. |
You may also pass `workspaceId`:
| Field | Type | Description |
| ------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `workspaceId` | string | Workspace that owns the environment. Only used alongside `environmentAlias` or `deploymentUrl`, since `environmentId` identifies its own workspace. Ignored by a workspace API key. Pass it with an organization or user API key. |
### Result fields
| Field | Description |
| --------------- | ---------------------------------------- |
| `outcome` | `"success"`, `"failed"`, or `"aborted"`. |
| `environmentId` | ID of the terminated environment. |
## generateSignedUrlForRunInputsExecutablesStorage
Generates a signed URL for uploading a run input executable (APK, AAB, IPA, ZIP, CSV, PDF) to QA Wolf. See [v0/run-inputs-executables-signed-urls](/qawolf/Uploading-manually) for the underlying endpoint.
```javascript theme={null}
import { makeQaWolfSdk } from "@qawolf/ci-sdk";
import fs from "fs/promises";
const { generateSignedUrlForRunInputsExecutablesStorage } = makeQaWolfSdk({
apiKey: process.env.QAWOLF_API_KEY,
});
const signedUrlResponse = await generateSignedUrlForRunInputsExecutablesStorage({
destinationFilePath: "app-staging",
});
if (
!signedUrlResponse?.success ||
!signedUrlResponse.uploadUrl ||
!signedUrlResponse.playgroundFileLocation
) {
throw new Error("No upload URL received from QA Wolf");
}
const fileBuffer = await fs.readFile("./path/to/build.apk");
await fetch(signedUrlResponse.uploadUrl, {
method: "PUT",
body: fileBuffer,
headers: { "Content-Type": "application/octet-stream" },
});
```
### Input fields
| Field | Type | Description |
| --------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------- |
| `destinationFilePath` | string | Filename and extension, optionally including directories. Reach out to your QA Wolf representative for the correct value. |
### Response fields
| Field | Description |
| ------------------------ | ------------------------------------------------------------------------------- |
| `success` | `true` if the signed URL was generated successfully. |
| `uploadUrl` | Pre-signed URL for uploading the file via `PUT`. |
| `playgroundFileLocation` | File path without team ID. Use as a `variables` value in `attemptNotifyDeploy`. |
## Versioning
This package follows SemVer. Notes:
* Use the `^` range operator — patch and minor updates will not introduce breaking changes.
* Major version bumps indicate a breaking API change. QA Wolf will give advance notice.
* Only top-level exports are covered by SemVer.
* New fields in API response types are not considered breaking changes.
# Troubleshooting
Source: https://docs.qawolf.com/qawolf/libraries/ci-sdk/troubleshooting
Common issues when integrating with @qawolf/ci-sdk.
Common issues when integrating `@qawolf/ci-sdk` into your CI pipeline.
## `outcome` is `"success"` but no run was created
Cause: The deployment notification was accepted but no trigger matched the request.
Check:
* `deploymentType` matches the value configured in your QA Wolf trigger
* `hostingService` matches where your code is hosted, not where your pipeline runs
* At least one trigger is configured for the target environment in QA Wolf
* Contact your QA Wolf representative to confirm trigger configuration
## `outcome` is `"failed"` or `"aborted"` on `attemptNotifyDeploy`
Cause: The notification request was rejected or timed out.
Check:
* `QAWOLF_API_KEY` is set correctly in your CI environment
* The key belongs to the correct QA Wolf workspace
* Your CI runner has outbound HTTPS access to `app.qawolf.com`
## `pollCiGreenlightStatus` times out
Cause: The run did not complete within the polling timeout (default: two hours).
Check:
* The run is visible in the QA Wolf UI and has not stalled
* The `pollTimeout` option is set appropriately for your expected run duration
* The `runId` passed to `pollCiGreenlightStatus` matches the run you intend to poll
## `pollCiGreenlightStatus` aborts with `"run-canceled"`
Cause: The run was canceled, typically because a newer run superseded it.
Check:
* Whether a newer deployment notification was sent before this run completed
* Whether `deduplicationKey` is configured correctly in `attemptNotifyDeploy`
* A canceled run cannot be recovered — submit a new deployment notification if needed
## Artifact upload succeeds but the run cannot find the file
Cause: The file path passed to `attemptNotifyDeploy` via `variables` does not match the uploaded location.
Check:
* Use `playgroundFileLocation` from the `generateSignedUrlForRunInputsExecutablesStorage` response as the variable value
* For mobile apps, prefix the path with `/home/wolf/run-inputs-executables/`
* The variable name (`RUN_INPUT_PATH`, `ANDROID_APP`, etc.) matches what your QA Wolf trigger expects
## `fetch is not defined` error
Cause: The SDK requires `fetch`, which is only available natively in Node.js 18 or later.
Check:
* Node.js 18 or later is available in your CI environment
```javascript theme={null}
import { fetch } from "undici";
const sdk = makeQaWolfSdk({ apiKey: process.env.QAWOLF_API_KEY }, { fetch });
```
## Mobile triggers are enabled but runs are not starting
Cause: Mobile triggers require additional platform configuration by QA Wolf before they activate.
Check:
* Contact your QA Wolf representative to confirm mobile triggers have been enabled for your workspace
* Artifact naming conventions match what was agreed with QA Wolf
* The artifact upload step is completing successfully before `attemptNotifyDeploy` is called
# Commands Reference
Source: https://docs.qawolf.com/qawolf/libraries/cli/api-reference/commands
Reference notes for every qawolf CLI command and flag.
The qawolf CLI exposes six top-level commands. Each section below documents the command's purpose, accepted arguments, and flags.
## `auth`
Manages authentication with the QA Wolf platform. Credentials are read from the `QAWOLF_API_KEY` environment variable first, then from the system keychain, then from a config file fallback.
### `auth login`
Prompts for an API key, validates it against the platform, and stores it locally. Requires an interactive terminal — in non-interactive contexts, set `QAWOLF_API_KEY` directly.
```bash theme={null}
qawolf auth login
```
### `auth logout`
Removes stored credentials. Credentials set via the `QAWOLF_API_KEY` environment variable cannot be removed by this command.
```bash theme={null}
qawolf auth logout
```
### `auth whoami`
Displays the authenticated team name, ID, optional slug, and the credential source.
```bash theme={null}
qawolf auth whoami
```
## `flows`
Manages and runs QA Wolf flows.
### `flows run [pattern]`
Runs flows matching `[pattern]`, or every flow when omitted. The default pattern matches `**/*.flow.{ts,js}` in the current working directory and in `.qawolf//` subdirectories.
```bash theme={null}
qawolf flows run
qawolf flows run "flows/checkout/**"
qawolf flows run checkout --env --headed
```
Options:
* `--retries ` — number of retries for each failing flow. Default: `0`.
* `--bail` — stop after the first failure. Default: `false`.
* `--workers ` — parallel worker count for web flows. Default: `1`. Android flows must run with `--workers 1`; the CLI errors out if Android flows are selected with a higher value.
* `--timeout ` — per-flow timeout in milliseconds. Default: `30000`.
* `--junit [path]` — write a JUnit XML report. With no value, writes to `/junit-report.xml`. Pass an explicit path to override.
* `--video ` — `on`, `off`, or `retain-on-failure`. Default: `off`.
* `--trace ` — Playwright trace mode. `on`, `off`, or `retain-on-failure`. Default: `off`.
* `--har ` — HAR capture mode. `on`, `off`, or `retain-on-failure`. Default: `off`.
* `--har-content ` — `omit` or `full`. `full` includes response bodies and uses more memory. Default: `omit`.
* `--output-dir ` — directory for artifacts. Default: `qawolf-output`.
* `--headed` — show the browser window instead of running headless. Default: `false`.
* `--env ` — environment ID. When set, missing flows are pulled before the run. See [where to find it](/qawolf/local-execution/pull-flows#pull-an-environment).
Exit codes: `0` when all flows pass, `1` when one or more flows fail, `2` for invalid arguments or an unrecognized flow target. See [Exit codes](/qawolf/libraries/cli/api-reference/index#exit-codes).
### `flows list [pattern]`
Lists flows matching `[pattern]`. By default, lists local flows; with `--remote`, lists flows from the QA Wolf platform.
```bash theme={null}
qawolf flows list
qawolf flows list "flows/checkout/**"
qawolf flows list --remote
```
Options:
* `--remote` — list flows from the QA Wolf platform instead of the local project. Default: `false`.
### `flows pull`
Downloads an environment's flows into the local `.qawolf//` cache.
```bash theme={null}
qawolf flows pull --env
qawolf flows pull --env --out ./snapshot
qawolf flows pull --env --yes
```
Options:
* `--env ` — required. The environment ID to pull from. See [where to find it](/qawolf/local-execution/pull-flows#pull-an-environment).
* `--out ` — destination directory. Defaults to `.qawolf//`.
* `--yes` — overwrite locally-modified files without prompting. Default: `false`.
## `install`
Installs every runtime dependency the project's flows need. With a `[pattern]` argument, only the dependencies for the matching flows are installed.
```bash theme={null}
qawolf install
qawolf install "flows/checkout/**"
```
### `install browsers [pattern]`
Installs the Playwright browsers used by the project's web flows.
```bash theme={null}
qawolf install browsers
qawolf install browsers "flows/web/**"
```
### `install android [pattern]`
Installs Android system images, AVDs, and the Appium driver used by the project's Android flows. Requires `ANDROID_HOME` or `ANDROID_SDK_ROOT`.
```bash theme={null}
qawolf install android
qawolf install android "flows/mobile/**"
```
## `init`
Scaffolds a QA Wolf project in the current directory. Creates `qawolf.config.ts`, an example flow at `src/flows/example.flow.ts`, and `.qawolf/.gitignore`. When no `package.json` exists, creates one with `"type": "module"`, the `@qawolf/flows` dependency, and a `test:e2e` script; when one already exists, only adds the `test:e2e` script (after a prompt).
```bash theme={null}
qawolf init
qawolf init --yes
```
Options:
* `--yes` — overwrite existing files without prompting. Default: `false`.
## `doctor`
Diagnoses problems running flows locally. Checks your environment (CLI and Node.js versions, API key, connectivity) and the dependencies your flows need, such as Playwright browsers and the Android SDK.
```bash theme={null}
qawolf doctor
qawolf doctor --all
```
Options:
* `--all` — run every platform check, including platforms the project does not use. Default: `false`.
## `run`
Triggers and manages runs on the QA Wolf platform. Unlike the local execution commands, `run` does not execute flows on your machine and requires authentication.
### `run create`
Creates a run on the QA Wolf platform.
```bash theme={null}
qawolf run create --environment-id
qawolf run create --environment-id --flow-ids ... --environment-variables KEY=VALUE
```
Options:
* `--environment-id ` — the ID of the environment to run. See [where to find it](/qawolf/local-execution/pull-flows#pull-an-environment).
* `--environment-variables ` — environment variables to set for the run.
* `--flow-ids ...` — limit the run to specific flow IDs.
* `--ignore-rules` — ignore the environment's run rules.
# Environment Variables
Source: https://docs.qawolf.com/qawolf/libraries/cli/api-reference/environment-variables
Environment variables the qawolf CLI reads for API access, Android and iOS SDK paths, and other runtime configuration.
The qawolf CLI reads the following environment variables.
## `QAWOLF_API_KEY`
API key for the QA Wolf platform. Used by `flows pull`, `flows list --remote`, and `flows run --env`. Required for any command that talks to the platform. Takes precedence over credentials stored via `qawolf auth login`.
## `QAWOLF_API_URL`
Base URL for the QA Wolf platform API. Defaults to `https://app.qawolf.com`. Trailing slashes are stripped.
## `ANDROID_HOME`
Path to the Android SDK root. Required by `qawolf install android` and by Android flow execution. `ANDROID_SDK_ROOT` is accepted as a fallback.
## `ANDROID_SDK_ROOT`
Fallback path to the Android SDK root, used when `ANDROID_HOME` is not set.
## CI Detection
The CLI treats the following variables as a signal that it is running in CI and switches the default output format to JSON:
* `CI`
* `GITHUB_ACTIONS`
* `GITLAB_CI`
* `CIRCLECI`
* `JENKINS_URL`
* `BUILDKITE`
## Agent Detection
The CLI treats the following variables as a signal that it is running inside an agent and switches the default output format to agent mode:
* `CLAUDE_CODE`
* `CURSOR_SESSION_ID`
# API Reference
Source: https://docs.qawolf.com/qawolf/libraries/cli/api-reference/index
Overview of qawolf CLI commands, flags, configuration, environment variables, and exit codes for running flows locally.
Use this section for stable command descriptions, flags, configuration, environment variables, and exit codes.
## Commands
* [`qawolf auth`](/qawolf/libraries/cli/api-reference/commands#auth) — manage authentication.
* [`qawolf flows`](/qawolf/libraries/cli/api-reference/commands#flows) — run, list, and pull flows.
* [`qawolf install`](/qawolf/libraries/cli/api-reference/commands#install) — install runtime dependencies for the project.
* [`qawolf init`](/qawolf/libraries/cli/api-reference/commands#init) — scaffold a project in the current directory.
* [`qawolf doctor`](/qawolf/libraries/cli/api-reference/commands#doctor) — diagnose problems running flows locally.
* [`qawolf run`](/qawolf/libraries/cli/api-reference/commands#run) — trigger and manage QA Wolf runs on the platform.
Example invocation:
```bash theme={null}
qawolf auth login
qawolf flows pull --env
qawolf flows run
```
Find the environment ID in the app URL — see [Pull flows](/qawolf/local-execution/pull-flows#pull-an-environment).
## Global Options
The following flags apply to every command:
* `--verbose` — emits debug logs to stderr.
* `--json` — formats output as JSON.
* `--agent` — formats output for agent consumption.
* `-V, --version` — prints the CLI version.
When a recognized CI environment is detected (`CI`, `GITHUB_ACTIONS`, `GITLAB_CI`, `CIRCLECI`, `JENKINS_URL`, `BUILDKITE`), output defaults to JSON. When a recognized agent environment is detected (`CLAUDE_CODE`, `CURSOR_SESSION_ID`), output defaults to agent mode.
With `--json` or `--agent`, the structured result is written to stdout and diagnostic text to stderr, so scripts can parse stdout directly.
## Exit Codes
Every qawolf CLI command exits with one of the codes below.
| Code | Name | Meaning |
| ---- | ------------- | ----------------------------------------------------------------------------------------------- |
| `0` | `success` | The command completed successfully. |
| `1` | `testFailure` | One or more flows failed. |
| `2` | `invalidArgs` | A Commander parse error, an unknown subcommand, or a bad flag value. |
| `3` | `auth` | `QAWOLF_API_KEY` is missing or invalid. |
| `4` | `network` | The QA Wolf API was unreachable, a download from storage failed, or a registry was unreachable. |
| `5` | `config` | `qawolf.config.ts` is invalid, or a file collision occurred during `qawolf init`. |
# CLI Troubleshooting
Source: https://docs.qawolf.com/qawolf/libraries/cli/troubleshooting
Solutions for common qawolf CLI issues including authentication errors, missing API keys, SDK path problems, and failed flow execution.
## `QAWOLF_API_KEY` Is Not Set
Cause: The CLI could not find an API key in the environment, the system keychain, or the local config file.
Check:
* the `QAWOLF_API_KEY` environment variable is set in the current shell
* credentials have been stored locally with `qawolf auth login`
* the value has not been overwritten by another shell profile
## QA Wolf API Rejected The Request (HTTP 401)
Cause: The API key is invalid or has been revoked.
Check:
* the key matches one issued for the current workspace
* the key was not truncated or copied with surrounding whitespace
* `qawolf auth whoami` returns the expected team
## QA Wolf API Rejected The Request (HTTP 403)
Cause: The API key is valid but does not have access to the requested environment.
Check:
* the key was issued for a workspace that contains this environment
* the environment ID matches one returned by the platform — see [where to find it](/qawolf/local-execution/pull-flows#pull-an-environment)
## Could Not Reach The QA Wolf API
Cause: The CLI could not connect to the platform at `QAWOLF_API_URL`.
Check:
* the network can reach `https://app.qawolf.com` (or the URL configured in `QAWOLF_API_URL`)
* `QAWOLF_API_URL` does not contain a typo or unexpected trailing path
* corporate proxy and VPN settings allow outbound HTTPS
## Flow Bundle Download Link Has Expired
Cause: The signed URL the CLI received from the platform has expired before the bundle was downloaded.
Check:
* re-run `qawolf flows pull --env ` to fetch a fresh link
* network conditions are not slowing the download past the link's expiration window
## Android SDK Not Found
Cause: `qawolf install android` could not locate the Android SDK at the path given by `ANDROID_HOME` or `ANDROID_SDK_ROOT`.
Check:
* `ANDROID_HOME` is exported in the current shell and points at the SDK root
* the SDK is installed via Android Studio or via the standalone `cmdline-tools` package
* `sdkmanager` exists at `$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager`
## iOS Targets Are Not Supported
Cause: The selected flow targets an iOS device. iOS execution is not yet supported by the CLI.
Check:
* the flow's `target` is set to a web or Android value
* iOS flows are filtered out of the run with a pattern argument
## Could Not Load `@qawolf/testkit`
Cause: The runner could not resolve `@qawolf/testkit` from the project directory.
Check:
* `@qawolf/testkit` is listed in `package.json` and installed
* the command is being run from the project root, or from a directory inside a pulled environment
## Android Flows Are Not Supported With `--workers > 1`
Cause: `--workers ` was set to a value greater than `1` and at least one Android flow was selected for the run.
Check:
* rerun the Android flows with `--workers 1`
* split the run into a web-only invocation with `--workers ` and an Android-only invocation with `--workers 1`
## Basic Target Flow Cannot Be Executed
Cause: The flow targets `"Basic"`, the Node-only platform type. The CLI does not execute Basic flows.
Check:
* the flow has been ported to a supported target, or
* the flow is excluded from the run with a pattern argument
# Client Reference
Source: https://docs.qawolf.com/qawolf/libraries/emails/api-reference/client
Reference notes for runtime-facing client creation and configuration.
Runtime-facing APIs include:
* `createEmailsClient(...)`
* `configureEmailsClient(...)`
* `getCurrentEmailsClient(...)`
* `resetEmailsClient(...)`
* `buildGetInboxFn(...)`
Example:
```ts theme={null}
import { configureEmailsClient, createEmailsClient } from "@qawolf/emails";
const client = await createEmailsClient({
apiKey: process.env["QAWOLF_API_KEY"]!,
pollForEmailsDefaultTimeoutMs: 30_000,
teamId: "team_123",
url: "https://app.qawolf.com/api",
waitForMessagesDefaultDelayMs: 1_000,
});
configureEmailsClient(client);
```
## `EmailsClientOptions`
`createEmailsClient(...)` accepts one of two transport configurations.
Use the QA Wolf API configuration for new code:
```ts theme={null}
const client = await createEmailsClient({
apiKey: process.env["QAWOLF_API_KEY"]!,
pollForEmailsDefaultTimeoutMs: 30_000,
teamId: "team_123",
url: "https://app.qawolf.com/api",
waitForMessagesDefaultDelayMs: 1_000,
});
```
The client also supports an internal runner configuration:
```ts theme={null}
const client = await createEmailsClient({
emailerUrl: process.env["EMAILER_URL"]!,
pollForEmailsDefaultTimeoutMs: 30_000,
teamId: "team_123",
waitForMessagesDefaultDelayMs: 1_000,
});
```
Do not pass both configurations at the same time. If you use `apiKey` and
`url`, do not pass `emailerUrl`.
```ts theme={null}
type EmailsClientOptions =
| {
apiKey: string;
emailerUrl?: never;
logger?: (severity: string, message: string) => void;
pollForEmailsDefaultTimeoutMs: number;
teamId?: string;
url: string;
waitForMessagesDefaultDelayMs: number;
}
| {
apiKey?: never;
emailerUrl: string;
logger?: (severity: string, message: string) => void;
pollForEmailsDefaultTimeoutMs: number;
teamId?: string;
url?: never;
waitForMessagesDefaultDelayMs: number;
};
```
## Role Of `createEmailsClient(...)`
This creates a service-backed client that can be used directly by local
harnesses, tooling, or other runtime environments.
Example:
```ts theme={null}
import { createEmailsClient } from "@qawolf/emails";
const client = await createEmailsClient({
apiKey: process.env["QAWOLF_API_KEY"]!,
logger: (severity, message) => {
console.log(severity, message);
},
pollForEmailsDefaultTimeoutMs: 30_000,
teamId: "team_123",
url: "https://app.qawolf.com/api",
waitForMessagesDefaultDelayMs: 1_000,
});
const inbox = await client.getInbox({ new: true });
```
## Compatibility Note
`buildGetInboxFn(...)` remains useful for compatibility with runtimes that still
inject `getInbox` directly, but new runtime code should prefer
`createEmailsClient(...)`.
Example:
```ts theme={null}
import { buildGetInboxFn } from "@qawolf/emails";
const getInbox = await buildGetInboxFn({
apiKey: process.env["QAWOLF_API_KEY"]!,
pollForEmailsDefaultTimeoutMs: 30_000,
teamId: "team_123",
url: "https://app.qawolf.com/api",
waitForMessagesDefaultDelayMs: 1_000,
});
const inbox = await getInbox({ new: true });
```
# API Reference
Source: https://docs.qawolf.com/qawolf/libraries/emails/api-reference/index
Overview of the @qawolf/emails API, including the mail and client modules used to create inboxes and wait for email messages in flows.
## Main Areas
* [Mail](/qawolf/libraries/emails/api-reference/mail)
* [Client](/qawolf/libraries/emails/api-reference/client)
Example imports:
```ts theme={null}
import { mail } from "@qawolf/emails";
import { configureEmailsClient, createEmailsClient } from "@qawolf/emails";
```
The mail reference covers:
* inbox creation options
* inbox handle methods
* send and wait option shapes
* parsed email fields
Example:
```ts theme={null}
const inbox = await mail.inbox({ new: true });
const message = await inbox.waitForMessage({});
```
# Mail Reference
Source: https://docs.qawolf.com/qawolf/libraries/emails/api-reference/mail
Reference for the @qawolf/emails mail API, covering inbox creation, sending messages, and waiting for parsed emails inside flows.
The top-level `mail` facade exposes:
* `mail.inbox(options?)`
`mail.inbox(...)` is a thin facade over the currently configured emails client. If no client has been configured, it throws a setup error that points callers to `configureEmailsClient(...)`.
Example:
```ts theme={null}
import { mail } from "@qawolf/emails";
const inbox = await mail.inbox({ new: true });
console.log(inbox.emailAddress);
```
## Inbox Handle
The returned inbox handle exposes:
* `emailAddress`
* `sendMessage(...)`
* `waitForMessage(...)`
* `waitForMessages(...)`
The current exported wait type is shared by both wait methods. The sections below describe which fields each method actually uses. Pass `{}` when you want default wait behavior.
Example:
```ts theme={null}
const inbox = await mail.inbox({ new: true });
await inbox.sendMessage({
subject: "Sign in link",
text: "Use the link in this email",
to: [inbox.emailAddress],
});
const latestMessage = await inbox.waitForMessage({});
```
## Current `GetInboxOptions`
```ts theme={null}
type GetInboxOptions = {
address?: string | undefined;
delimiter?: string | undefined;
new?: boolean | undefined;
};
```
Current validation and behavior:
* `new: true` derives a unique address based on the current default
* `delimiter` customizes the separator used for derived addresses
* `address` must come from the allowed workspace address set
* omitting `address` triggers a lookup of the workspace default address
If `teamId` is missing from the runtime client configuration, inbox creation fails.
Examples:
```ts theme={null}
const defaultInbox = await mail.inbox();
const namedInbox = await mail.inbox({ address: "flows@example.com" });
const derivedInbox = await mail.inbox({ new: true, delimiter: "-" });
```
## `sendMessage(...)`
Current input shape:
```ts theme={null}
type SendMessage = {
attachments?: {
content: Buffer;
contentId?: string;
disposition?: "attachment" | "inline";
fileName: string;
type?: string;
}[];
bcc?: string[];
cc?: string[];
html?: string;
replyTo?: string[];
replyToMessageId?: string;
subject: string;
text?: string;
to: string[];
};
```
Behavior:
* `to` is always an array of strings
* at least one of `html` or `text` is required
* attachments are base64-encoded before the request is sent to the email service
Return shape:
```ts theme={null}
type SendMessageResult = {
id: string;
};
```
Example:
```ts theme={null}
const result = await inbox.sendMessage({
attachments: [
{
content: Buffer.from("hello"),
fileName: "note.txt",
type: "text/plain",
},
],
html: "
Hello from QA Wolf
",
subject: "Welcome",
to: [inbox.emailAddress],
});
console.log(result.id);
```
## `waitForMessage(...)`
Current method options:
```ts theme={null}
type WaitForMessageOptions = {
after?: Date;
timeout?: number;
};
```
For `waitForMessage(...)`, the implementation uses:
* `after` when it is provided
* otherwise the time when `mail.inbox(...)` was called
* `timeout` when it is provided
It returns one `ParsedEmail` and throws if no message arrives in time.
Example:
```ts theme={null}
const message = await inbox.waitForMessage({
after: new Date(Date.now() - 60_000),
timeout: 15_000,
});
console.log(message.subject);
```
## `waitForMessages(...)`
Current method options:
```ts theme={null}
type WaitForMessagesOptions = {
after?: Date;
delay?: number;
minCount?: number;
timeout?: number;
};
```
Behavior:
* it uses `after` when provided, otherwise the time when `mail.inbox(...)` was called
* it waits for `delay` first
* it polls until at least `minCount` messages are found, or until timeout
* it returns the matching parsed emails
Example:
```ts theme={null}
const messages = await inbox.waitForMessages({
delay: 2_000,
minCount: 2,
timeout: 30_000,
});
console.log(messages.map((message) => message.subject));
```
## Parsed Email Shape
The parsed email shape is:
```ts theme={null}
type ParsedEmail = {
attachments?: {
content: Buffer;
fileName: string;
type?: string;
}[];
bcc?: string[];
cc?: string[];
from: string;
html: string;
id: string;
replyTo?: string[];
subject: string;
teamId: string;
text: string;
to: string[];
urls: string[];
};
```
`urls` is derived by parsing links from the HTML body.
Example:
```ts theme={null}
const message = await inbox.waitForMessage({});
console.log(message.from);
console.log(message.subject);
console.log(message.urls);
```
# Emails Troubleshooting
Source: https://docs.qawolf.com/qawolf/libraries/emails/troubleshooting
Troubleshoot common @qawolf/emails problems, including missing client configuration, stale messages, and inbox wait timeouts.
## `mail.inbox()` Throws Because No Client Is Configured
Cause: The runtime did not call `configureEmailsClient(...)` before the flow ran.
Check:
* the runner creates the client
* the runner configures it before flow execution
## `waitForMessage(...)` Returns An Older Message
Check:
* whether the flow should pass `after: new Date()`
* whether the same inbox is being reused across multiple steps or retries
## No Message Arrives Before Timeout
Check:
* the product under test actually sent the email
* the address being used is allowed for the workspace
* the timeout is long enough for the expected delivery path
## Old emails are matched by waitForMessage
Where possible, use `new: true` to generate a unique address for each run. This ensures `waitForMessage` only sees messages from the current run.
If you must reuse a stable address, pass `after: new Date()` immediately before the action that triggers the email:
```javascript theme={null}
const after = new Date();
await page.getByRole("button", { name: "Send code" }).click();
const message = await inbox.waitForMessage({ after });
```
If neither option is available and you are running a local harness, call `resetEmailsClient()` between test suites to clear the client state.
# Android Reference
Source: https://docs.qawolf.com/qawolf/libraries/flows/api-reference/android
Reference for @qawolf/flows/android, the entry point for defining Android flows with device controls, launch options, and assertions.
`@qawolf/flows/android` defines Android flows and advanced device controls.
## Primary Exports
* `flow(...)`
* `launch(...)`
* `device`
* `expect`
* `testContextDependencies`
It also exports Android-specific target, launch, device, callback context, and flow definition types.
Example:
```ts theme={null}
import { device, flow } from "@qawolf/flows/android";
export default flow(
"Open Android app",
{ target: "Android - Pixel", launch: true },
async ({ driver, test }) => {
await test("set location", async () => {
await driver.pause(1000);
await device.setGeoLocation({
latitude: 37.7749,
longitude: -122.4194,
});
});
},
);
```
## Flow Callback Context
All Android flow callbacks receive:
* `inputs`
* `setOutput(...)`
* `test(...)`
Launch-enabled Android flows also receive `driver`.
`test(...)` can be omitted for simple flows where grouping steps into named sub-steps doesn't add value. For most flows, wrapping steps in `test(...)` is recommended — the label appears in your results and makes failures easier to locate.
## `testContextDependencies`
`testContextDependencies` is exported for runner and tooling integration. Flow authors should usually use the public callback parameters above instead of depending on the raw runner dependency list.
## Target Model
The current target input model is:
```ts theme={null}
type AndroidFlowTargetInput =
| AndroidFlowTarget
| {
target: AndroidFlowTarget;
launch?: false | undefined;
}
| {
target: AndroidFlowTarget;
launch: true | LaunchOptions;
};
```
The implementation accepts either:
* a target directly for the common path
* `{ target, launch }` when startup behavior should be part of the flow
Example:
```ts theme={null}
import { flow } from "@qawolf/flows/android";
export const targetOnlyFlow = flow(
"Target-only path",
"Android - Pixel",
async () => {
// call launch() explicitly when startup should happen in the callback
},
);
export const launchedFlow = flow(
"Launch-enabled path",
{ target: "Android - Pixel", launch: true },
async ({ driver, test }) => {
await test("launch app", async () => {
await driver.pause(1000);
});
},
);
```
## `flow(...)`
Use `flow(...)` for Android authoring.
Behavior from the implementation:
* without launch, the callback receives `inputs`, `setOutput(...)`, and `test(...)`
* with `launch: true`, the flow calls `launch()` with default Android startup
* with `launch: `, the flow calls `launch(options)`
* when launch is enabled, the callback also receives `driver`
Example:
```ts theme={null}
import { flow } from "@qawolf/flows/android";
export default flow(
"Launch in flow definition",
{ target: "Android - Pixel", launch: true },
async ({ driver, test }) => {
await test("launch app", async () => {
await driver.pause(1000);
});
},
);
```
## `launch(...)`
`launch()` starts Android automation for the active flow and returns:
```ts theme={null}
type LaunchResult = {
driver: Awaited>;
};
```
This API is only available while a flow is running.
Example:
```ts theme={null}
import { flow, launch } from "@qawolf/flows/android";
export default flow("Launch explicitly", "Android - Pixel", async () => {
const { driver } = await launch();
await driver.pause(1000);
});
```
### Launch Shape
```ts theme={null}
type LaunchOptions = {
app?: {
path?: string;
env?: string;
url?: string;
};
appPackage?: string;
appActivity?: string;
appWaitActivity?: string;
autoGrantPermissions?: boolean;
noReset?: boolean;
waitForIdleTimeout?: number;
browserName?: string;
capabilities?: Record;
};
```
Example:
```ts theme={null}
import { flow, launch } from "@qawolf/flows/android";
export default flow("Launch apk", "Android - Pixel", async () => {
const { driver } = await launch({
app: { path: "apps/mobile/android/app.apk" },
appPackage: "com.example.android",
autoGrantPermissions: true,
});
await driver.pause(1000);
});
```
### Launch Defaults
The current implementation applies these defaults:
* when `app` is omitted, launch falls back to the runner-provided executable input path and then to installed-app startup through `appPackage`
* `autoGrantPermissions` defaults to `true`
* `noReset` defaults to `false`
Example:
```ts theme={null}
import { flow, launch } from "@qawolf/flows/android";
export default flow("Use Android defaults", "Android - Pixel", async () => {
const { driver } = await launch({
appPackage: "com.example.android",
});
await driver.pause(1000);
});
```
### App Resolution
When your CI pipeline uploads an Android build, QA Wolf sets `RUN_INPUT_PATH` to the uploaded file before the flow runs. Omit `app` in your launch call and QA Wolf will use that path automatically — you only need to provide the `appPackage`.
When `app` is provided, the current resolution order is:
1. `app.path`
2. `app.env`
3. `app.url`
When `app` is omitted, launch falls back to `RUN_INPUT_PATH`.
Relative paths are resolved against `RUN_INPUTS_EXECUTABLES_DIR` when that environment variable is present.
Explicit app source examples:
```ts theme={null}
await launch({ app: { path: "apps/mobile/android/app.apk" } });
await launch({ app: { env: "ANDROID_APP_PATH" } });
await launch({ app: { url: "https://example.com/app.apk" } });
```
`RUN_INPUT_PATH` fallback example:
```ts theme={null}
// The runner sets RUN_INPUT_PATH before the flow starts.
await launch({
appPackage: "com.example.android",
});
```
If `app` is present but does not resolve to a value, launch does not fall back to `RUN_INPUT_PATH`.
### Supported app formats
QA Wolf installs your app from either a local path or a URL, in either the `.apk` or `.aab` format.
| Source | Formats | What QA Wolf does |
| ---------- | -------------- | ------------------------------------- |
| local path | `.apk`, `.aab` | installs the build |
| URL | `.apk`, `.aab` | downloads the build, then installs it |
```ts theme={null}
// a build your CI uploaded, or a path in your repository
await launch({ app: { path: "apps/android/app.aab" } });
// a link QA Wolf can fetch, including a signed or expiring one
await launch({ app: { url: "https://builds.example.com/app.apk?token=abc" } });
```
QA Wolf writes the version of the installed build to the run log, so you can confirm which build a run tested against.
A URL is only fetched by QA Wolf when its path ends in `.apk` or `.aab`. A query string does not change that, so a signed or expiring download link works as long as the path keeps the extension. Anything else, such as a share page or a download endpoint that names the file elsewhere, is passed to the driver untouched.
You can also install an app yourself over adb, which skips everything above:
```ts theme={null}
// -r to replace an existing install, and a timeout large enough for the transfer
await device.adb(["install-multiple", "-r", ...splitPaths], { timeout: 600_000 });
await launch({ appPackage: "com.example.app", appActivity: ".MainActivity" });
```
Prefer handing us the `.apk` or `.aab`. Installing over adb means you own everything that comes with it: choosing the right split APKs for the device, reading the manifest for the launcher activity, and sizing the transfer. Passing `app` gets all of that handled for you.
### Advanced Appium capabilities
The named launch options above cover the most common settings. For anything else, use the `capabilities` escape hatch: its entries are spread into the underlying [webdriver.io `remote`](https://webdriver.io/docs/api) capabilities and passed straight to the Android driver.
`startAndroid` is a thin wrapper around webdriver.io's `remote`, so capabilities that QA Wolf does not set itself are forwarded as-is. See the Appium references for the full list of available keys:
* [Appium core capabilities](https://appium.io/docs/en/2.11/guides/caps/)
* [UiAutomator2 (Android) driver capabilities](https://github.com/appium/appium-uiautomator2-driver/blob/v3.7.8/README.md#capabilities)
```ts theme={null}
import { flow, launch } from "@qawolf/flows/android";
export default flow(
"Launch with custom capabilities",
"Android - Pixel",
async () => {
const { driver } = await launch({
// Named options cover the common knobs:
autoGrantPermissions: true,
waitForIdleTimeout: 10,
// Anything else is passed through verbatim:
capabilities: {
"appium:disableWindowAnimation": true,
"appium:uiautomator2ServerLaunchTimeout": 60000,
},
});
await driver.pause(1000);
},
);
```
Named options take precedence over `capabilities` for the same key. In particular, `autoGrantPermissions` and `noReset` are always applied (using their defaults when omitted), so configure those through the named options rather than `capabilities`.
### Migrating from `wdio.startAndroid`
Older flows obtained a `wdio` handle from the test context and called `startAndroid` directly. With the flow API, pass the same configuration through the flow's `launch` options instead — map the settings that have a named option, and route the rest through `capabilities`. The `driver` you receive is the same webdriver.io `Browser` handle, so there is no separate "raw" driver to obtain.
```ts theme={null}
// Before: raw wdio.startAndroid
const driver = await wdio.startAndroid({
"appium:app": process.env.ANDROID_PATH,
"appium:appPackage": "com.example.android",
"appium:autoGrantPermissions": true,
"appium:disableWindowAnimation": true,
"appium:waitForIdleTimeout": 10,
});
```
```ts theme={null}
// After: flow launch options
import { flow } from "@qawolf/flows/android";
export default flow(
"Open Android app",
{
target: "Android - Pixel",
launch: {
app: { env: "ANDROID_PATH" },
appPackage: "com.example.android",
autoGrantPermissions: true,
waitForIdleTimeout: 10,
capabilities: {
"appium:disableWindowAnimation": true,
},
},
},
async ({ driver }) => {
await driver.pause(1000);
},
);
```
`app: { env: "ANDROID_PATH" }` reads the path from the environment variable named `ANDROID_PATH`, replacing the raw `"appium:app": process.env.ANDROID_PATH` form.
## `device`
`device` is a runtime proxy over the Android emulator API. Use it for device-level operations — such as setting location, simulating sensors, or managing device state — that sit outside app UI interactions. Use `driver` for interacting with the app itself.
Example:
```ts theme={null}
import { device, flow } from "@qawolf/flows/android";
export default flow("Set device location", "Android - Pixel", async () => {
await device.setGeoLocation({
latitude: 37.7749,
longitude: -122.4194,
});
});
```
## `expect`
The exported `expect` is the assertion helper for Android flows, backed by [`expect-webdriverio`](https://webdriver.io/docs/api/expect-webdriverio). All WebdriverIO matchers are available — including auto-retrying element matchers like `toBeDisplayed`, `toExist`, `toHaveText`, and `toHaveAttribute`. See the [expect-webdriverio matchers reference](https://webdriver.io/docs/api/expect-webdriverio) for the full list.
Always import `expect` from `@qawolf/flows/android` rather than from `expect-webdriverio` directly, so that QA Wolf's defaults (timeout, `toHaveScreenshot`) stay in effect.
Example:
```ts theme={null}
import { expect, flow } from "@qawolf/flows/android";
export default flow(
"Verify welcome screen",
{ target: "Android - Pixel", launch: true },
async ({ driver, test }) => {
await test("welcome heading is visible", async () => {
const heading = driver.$("~welcome-heading");
await expect(heading).toBeDisplayed();
await expect(heading).toHaveText("Welcome");
});
},
);
```
For visual regression assertions with `expect(driver).toHaveScreenshot(...)`, see [Native mobile screenshots](/qawolf/visual-diffing-native-app).
# Android Device Reference
Source: https://docs.qawolf.com/qawolf/libraries/flows/api-reference/android-device-reference
Reference notes for the `device` object from `@qawolf/flows/android`.
`device` is a runtime proxy over the Android emulator API. Use it for emulator-level operations — such as setting location, placing images in the virtual camera scene, configuring network proxies, passing audio to the microphone, or running raw `adb` commands — that sit outside app UI interactions. Use `driver` for interacting with the app itself.
`device` is only available while a flow is running.
```typescript theme={null}
import { device, flow } from "@qawolf/flows/android";
```
## `setGeoLocation`
Sets the emulator's GPS location.
```typescript theme={null}
function setGeoLocation(options: GeoLocationOptions): Promise;
type GeoLocationOptions = {
latitude: number;
longitude: number;
altitude?: number;
satellites?: number;
velocity?: number;
};
```
Example:
```typescript theme={null}
import { device, flow } from "@qawolf/flows/android";
export default flow("Set device location", "Android - Pixel", async () => {
await device.setGeoLocation({
latitude: 37.7749,
longitude: -122.4194,
});
});
```
Use this to test location-aware features without physically moving a device. See [Mock device location](/qawolf/android-location-mocking) for a full walkthrough.
## `setVirtualSceneImage`
Places an image into the Android emulator's virtual camera scene. Use this to test features that require the camera to see a specific image — such as barcode or QR code scanning.
```typescript theme={null}
function setVirtualSceneImage(options: VirtualSceneImageOptions): Promise;
type VirtualSceneImageOptions = {
/** Path to the image file to place in the virtual scene. Defaults to the default virtual scene image when omitted. */
image?: string;
/** Where to place the image in the virtual scene. */
location: "table" | "wall";
};
```
Example:
```typescript theme={null}
import { device, flow } from "@qawolf/flows/android";
export default flow(
"Scan barcode",
{ target: "Android - Pixel", launch: true },
async ({ driver, test }) => {
await test("place barcode image", async () => {
await device.setVirtualSceneImage({
image: "/path/to/barcode.jpg",
location: "table",
});
});
},
);
```
Virtual scene is scoped to barcode/QR code scanning and augmented reality (AR) use cases. It is not a general photo or video injection mechanism. See [Android barcode and QR scanning](/qawolf/android-barcode) for a full walkthrough.
## `playAutomation`
Triggers an automation macro on the Android emulator. Use this in combination with `setVirtualSceneImage` to animate the virtual camera toward a placed image.
```typescript theme={null}
function playAutomation(options: PlayAutomationOptions): Promise;
type PlayAutomationOptions =
| {
/** Name of a built-in emulator macro. */
macro: "Reset_position" | "Track_horizontal_plane" | "Track_vertical_plane" | "Walk_to_image_room";
/** Optional path to override the macro file. */
overrideMacroPath?: string;
}
| {
/** Path to a custom macro file. */
file: string;
};
```
Example:
```typescript theme={null}
import { device, flow } from "@qawolf/flows/android";
export default flow(
"Scan barcode",
{ target: "Android - Pixel", launch: true },
async ({ driver, test }) => {
await test("place barcode and animate camera", async () => {
await device.setVirtualSceneImage({
image: "/path/to/barcode.jpg",
location: "table",
});
await device.playAutomation({
macro: "Walk_to_image_room",
});
});
},
);
```
Available built-in macros are determined by the Android emulator. `Walk_to_image_room` is the macro used to animate the virtual camera toward a placed image. See [Android Emulator camera support](https://developer.android.com/studio/run/emulator-use-camera#arcore) for more detail on virtual scene automation.
## `setProxy`
Configures a proxy for the Android emulator's network traffic.
```typescript theme={null}
function setProxy(options: ProxyOptions): Promise;
type ProxyOptions = {
/** Proxy URL. Include credentials in the URL if authentication is required (e.g. `http://username:password@proxy.example.com:8080`). */
url: string;
};
```
Example:
```typescript theme={null}
import { device, flow } from "@qawolf/flows/android";
export default flow("Set proxy", "Android - Pixel", async () => {
await device.setProxy({
url: "http://proxy.example.com:8080",
});
});
```
## `clearProxy`
Removes any proxy configuration from the Android emulator.
```typescript theme={null}
function clearProxy(): Promise;
```
Example:
```typescript theme={null}
import { device, flow } from "@qawolf/flows/android";
export default flow("Clear proxy", "Android - Pixel", async () => {
await device.clearProxy();
});
```
Call `clearProxy()` at the end of any flow that sets a proxy to avoid affecting subsequent flows.
## `passAudioAsMicrophoneInput`
Plays an audio file on the runner host so the emulator receives it as microphone input. Use this to test recording, voice, or speech-recognition features with known audio.
```typescript theme={null}
function passAudioAsMicrophoneInput(options: PassAudioAsMicrophoneInputOptions): Promise;
type PassAudioAsMicrophoneInputOptions = {
/** File path on the runner host, or URL, for the audio to play. */
data: string;
/** Delay in seconds before playback starts. Awaited inline, so it does not let the call return early. */
delaySeconds?: number;
/** Maximum playback duration in seconds. Omit to play the whole file. */
durationSeconds?: number;
};
```
Playback is synchronous: the promise resolves only once the audio has finished playing. Your app has to be recording **before** you call this, otherwise the file plays into a microphone nobody is listening to and the recording that follows captures silence. `delaySeconds` does not work around this, since it is awaited inline as well.
Example:
```typescript theme={null}
import { device, flow } from "@qawolf/flows/android";
export default flow(
"Inject microphone audio",
{ target: "Android - Pixel", launch: true },
async ({ driver, test }) => {
await test("play audio into the microphone", async () => {
// Start the app listening first
await driver.$(`//*[@text='Record']`).click();
await device.passAudioAsMicrophoneInput({
data: `${process.env.TEAM_STORAGE_DIR}/audio.mp3`,
durationSeconds: 10,
});
await driver.$(`//*[@text='Done']`).click();
});
},
);
```
If your app needs audio already flowing when it starts listening, hold the promise and await it once the app is ready:
```typescript theme={null}
const playback = device.passAudioAsMicrophoneInput({
data: `${process.env.TEAM_STORAGE_DIR}/audio.mp3`,
durationSeconds: 10,
});
await driver.$(`//*[@text='Record']`).click();
await playback;
```
Playback is bounded by the command timeout (1 minute by default). For audio longer than a minute, pass `durationSeconds`. See [Microphone injection](/qawolf/android-audio-injection) for a full walkthrough.
## `adb`
Runs an `adb` command against the emulator and returns its combined stdout/stderr. Use this as an escape hatch for emulator or device operations not covered by a dedicated `device` method — such as `dumpsys`, `emu sensor set`, or `pull`.
Pass the arguments either as a single string (split on whitespace; quote a single value that contains spaces) or as an array (preferred when building arguments programmatically). The command does not go through a host shell, so it does not expand variables or run pipes.
```typescript theme={null}
function adb(command: string | string[]): Promise;
```
Example:
```typescript theme={null}
import { device, flow } from "@qawolf/flows/android";
export default flow(
"Run adb commands",
{ target: "Android - Pixel", launch: true },
async ({ driver, test }) => {
await test("send a keyevent and pull a file", async () => {
await device.adb("shell input keyevent 26");
// quote a value that contains spaces, or pass an array to avoid quoting
await device.adb([
"pull",
"/sdcard/Recordings/My recording 1.m4a",
`${process.env.TEAM_STORAGE_DIR}/recorded.m4a`,
]);
});
},
);
```
Real-world example — jump the device clock to test time-gated features (disable auto time, move the clock, then restore):
```typescript theme={null}
import { device, flow } from "@qawolf/flows/android";
export default flow(
"Verify a time-gated feature can't be gamed",
{ target: "Android - Pixel", launch: true },
async ({ driver, test }) => {
await test("jump the device clock forward", async () => {
// Disable automatic time so the manual clock sticks
await device.adb("shell settings put global auto_time 0");
// Move the device clock forward 2 days
const targetMillis = Date.now() + 2 * 24 * 60 * 60 * 1000;
await device.adb(`shell cmd alarm set-time ${targetMillis}`);
// ... assert the time-gated feature (rewards/streaks) behaves correctly ...
// Restore automatic time
await device.adb("shell settings put global auto_time 1");
});
},
);
```
Prefer a dedicated `device` method when one exists. See [Measure performance](/qawolf/android-performance) and [Mock hardware sensors](/qawolf/android-sensor-mocking) for `adb` usage in context.
# CLI Reference
Source: https://docs.qawolf.com/qawolf/libraries/flows/api-reference/cli
Reference for @qawolf/flows/cli, the Node-based entry point for headless flows that handle setup, teardown, notifications, and file work.
`@qawolf/flows/cli` is the smallest platform entry point in `@qawolf/flows`. CLI flows run in Node and have no browser or mobile driver. Use them for work that needs to coordinate with other flows in a run but doesn't require UI interaction — data setup, teardown, sending notifications, or processing files.
Because CLI flows run in Node, you can use the Node standard library directly for file, process, and network work:
```ts theme={null}
import { flow } from "@qawolf/flows/cli";
import { readFile } from "node:fs/promises";
export default flow("Read build metadata", "Basic", async ({ test }) => {
await test("read metadata", async () => {
const metadata = await readFile("build-metadata.json", "utf8");
console.log(JSON.parse(metadata));
});
});
```
## Primary Exports
* `flow(...)`
* `expect`
* `testContextDependencies`
It also exports CLI-specific target, callback context, and flow definition types.
Example:
```ts theme={null}
import { flow } from "@qawolf/flows/cli";
export default flow(
"Prepare release metadata",
"Basic",
async ({ inputs, setOutput, test }) => {
await test("create release payload", async () => {
setOutput("RELEASE", {
generatedAt: new Date().toISOString(),
inputs,
status: "ready",
});
});
},
);
```
## Target Model
The current target type is:
```ts theme={null}
type CliFlowTarget = "Basic";
```
Accepted flow target input:
```ts theme={null}
type CliFlowTargetInput = CliFlowTarget | { target: CliFlowTarget };
```
Example:
```ts theme={null}
import { flow } from "@qawolf/flows/cli";
export const stringTargetFlow = flow("String target", "Basic", async () => {});
export const objectTargetFlow = flow(
"Object target",
{ target: "Basic" },
async () => {},
);
```
## Flow Callback Context
The callback receives the CLI flow context.
Public callback parameters:
* `inputs` — values published by an upstream flow in the same run
* `setOutput(...)` — publishes values for downstream flows to read
* `test(...)` — wraps named sub-steps that appear in your results
CLI flows do not receive `page`, `driver`, or any other launch object.
`test(...)` can be omitted for simple flows where grouping steps into named sub-steps doesn't add value. For most flows, wrapping steps in `test(...)` is recommended — the label appears in your results and makes failures easier to locate.
Example:
```ts theme={null}
import { flow } from "@qawolf/flows/cli";
export default flow(
"Prepare release metadata",
"Basic",
async ({ inputs, setOutput, test }) => {
await test("create release payload", async () => {
setOutput("RELEASE", {
generatedAt: new Date().toISOString(),
inputs,
status: "ready",
});
});
},
);
```
See [Passing data between flows](/qawolf/Pass-data-between-flows-29d5b2a994fb801080bce743ab552931) for how to use `inputs` and `setOutput` to coordinate with other flows in a run.
## `testContextDependencies`
`testContextDependencies` is exported for runner and tooling integration. Flow authors should usually use the public callback parameters above instead of depending on the raw runner dependency list.
## `expect`
The exported `expect` is a re-export of the [`expect`](https://www.npmjs.com/package/expect) package — the same assertion library Jest uses. Use Jest's [`expect` reference](https://jestjs.io/docs/expect) for the full matcher list (`toBe`, `toEqual`, `toMatchObject`, `toThrow`, etc.).
Always import `expect` from `@qawolf/flows/cli` rather than from `expect` or `jest` directly, so that future QA Wolf extensions stay in effect.
Example:
```ts theme={null}
import { expect, flow } from "@qawolf/flows/cli";
export default flow("Validate release payload", "Basic", async ({ inputs, test }) => {
await test("payload has expected shape", async () => {
expect(inputs).toMatchObject({
status: "ready",
generatedAt: expect.any(String),
});
});
});
```
# API Reference
Source: https://docs.qawolf.com/qawolf/libraries/flows/api-reference/index
API landing page for @qawolf/flows and its platform entry points.
Use this section for stable API descriptions, defaults, and runtime behaviors.
## Entry Points
* [Top-Level](/qawolf/libraries/flows/api-reference/top-level)
* [Web](/qawolf/libraries/flows/api-reference/web)
* [Android](/qawolf/libraries/flows/api-reference/android)
* [iOS](/qawolf/libraries/flows/api-reference/ios)
* [CLI](/qawolf/libraries/flows/api-reference/cli)
Example imports:
```ts theme={null}
import { platform } from "@qawolf/flows";
import { flow } from "@qawolf/flows/web";
import { flow } from "@qawolf/flows/android";
import { flow } from "@qawolf/flows/ios";
import { flow } from "@qawolf/flows/cli";
```
## Cross-Platform Surface
The top-level `@qawolf/flows` entry point exposes:
* `platform.target`
* target configuration helpers for tests and local execution
* shared target types
* shared error and scope types
Example:
```ts theme={null}
import { configureTarget, platform, resetTarget } from "@qawolf/flows";
import type { Target } from "@qawolf/flows";
declare const target: Target;
configureTarget({ target });
const activeTarget = platform.target;
resetTarget();
```
## Target Literals
The target string must exactly match one of the supported values or your flow will fail to initialize.
```ts theme={null}
type Target =
| "Basic"
| "Web - Chrome"
| "Web - Chrome (GPU)"
| "Web - Firefox"
| "Web - Firefox (GPU)"
| "Web - Safari"
| "Web - Safari (GPU)"
| "Android - Pixel"
| "Android - Pixel 2 (Android 14)"
| "Android - Pixel 9"
| "Android - Pixel 9 (Android 14)"
| "Android - Pixel 9 (Android 15)"
| "Android - Pixel 9 (Android 16)"
| "Android - Pixel Tablet (Android 14)"
| "Android - Tablet"
| "iOS - Allowlisted iPhone"
| "iOS - iPad"
| "iOS - iPad 11 (iOS 18)"
| "iOS - iPad 11 (iOS 26)"
| "iOS - iPhone 15 (iOS 17)"
| "iOS - iPhone 15 (iOS 18)"
| "iOS - iPhone 15 (iOS 26)"
| "iOS - iPhone 15 (iOS 26) (allowlisted)"
| "iOS - iPhone 15 (iOS 26) (private)"
| "iOS - iPhone 17 (iOS 26)"
| "iOS 26 - Any iPad"
| "iOS 26 - Any iPhone"
| "Latest iOS (iPad)"
| "Latest iOS (iPhone)";
```
See [Allowlisted devices](/qawolf/mobile/ios-allowlisted-devices) and [Private devices](/qawolf/mobile/ios-private-devices) for when to use these targets and how to set them up.
# iOS Reference
Source: https://docs.qawolf.com/qawolf/libraries/flows/api-reference/ios
Reference for @qawolf/flows/ios, the entry point for defining iOS flows with simulator and device controls, launch options, and assertions.
`@qawolf/flows/ios` defines iOS flows and advanced simulator or device control.
## Primary Exports
* `flow(...)`
* `launch(...)`
* `device`
* `expect`
* `testContextDependencies`
It also exports iOS-specific target, launch, device, callback context, and flow definition types.
Example:
```ts theme={null}
import { device, flow } from "@qawolf/flows/ios";
const profile = `
`;
export default flow(
"Open iOS app",
{ target: "iOS - iPhone 15 (iOS 26)", launch: true },
async ({ driver, test }) => {
await test("install profile", async () => {
await driver.pause(1000);
await device.installConfigurationProfile(driver, profile);
});
},
);
```
## Flow Callback Context
All iOS flow callbacks receive:
* `inputs`
* `setOutput(...)`
* `test(...)`
Launch-enabled iOS flows also receive `driver`.
`test(...)` can be omitted for simple flows where grouping steps into named sub-steps doesn't add value. For most flows, wrapping steps in `test(...)` is recommended — the label appears in your results and makes failures easier to locate.
## `testContextDependencies`
`testContextDependencies` is exported for runner and tooling integration. Flow authors should usually use the public callback parameters above instead of depending on the raw runner dependency list.
## Target Model
The current target input model is:
```ts theme={null}
type IosFlowTargetInput =
| IosFlowTarget
| {
target: IosFlowTarget;
launch?: false | undefined;
}
| {
target: IosFlowTarget;
launch: true | LaunchOptions;
};
```
The implementation accepts either:
* a target directly for the common path
* `{ target, launch }` when startup behavior should be part of the flow
Example:
```ts theme={null}
import { flow } from "@qawolf/flows/ios";
export const targetOnlyFlow = flow(
"Target-only path",
"iOS - iPhone 15 (iOS 26)",
async () => {
// call launch() explicitly when startup should happen in the callback
},
);
export const launchedFlow = flow(
"Launch-enabled path",
{ target: "iOS - iPhone 15 (iOS 26)", launch: true },
async ({ driver, test }) => {
await test("launch app", async () => {
await driver.pause(1000);
});
},
);
```
## `flow(...)`
Use `flow(...)` for iOS authoring.
Behavior from the implementation:
* without launch, the callback receives `inputs`, `setOutput(...)`, and `test(...)`
* with `launch: true`, the flow calls `launch()` with default iOS startup
* with `launch: `, the flow calls `launch(options)`
* when launch is enabled, the callback also receives `driver`
Example:
```ts theme={null}
import { flow } from "@qawolf/flows/ios";
export default flow(
"Launch in flow definition",
{ target: "iOS - iPhone 15 (iOS 26)", launch: true },
async ({ driver, test }) => {
await test("launch app", async () => {
await driver.pause(1000);
});
},
);
```
## `launch(...)`
`launch()` starts iOS automation for the active flow and returns:
```ts theme={null}
type LaunchResult = {
driver: Awaited>;
};
```
This API is only available while a flow is running.
Example:
```ts theme={null}
import { flow, launch } from "@qawolf/flows/ios";
export default flow(
"Launch explicitly",
"iOS - iPhone 15 (iOS 26)",
async () => {
const { driver } = await launch();
await driver.pause(1000);
},
);
```
### Launch Shape
```ts theme={null}
type LaunchOptions = {
app?: {
path?: string;
env?: string;
url?: string;
};
autoAcceptAlerts?: boolean;
autoDismissAlerts?: boolean;
browserName?: string;
bundleId?: string;
capabilities?: Record;
noReset?: boolean;
platformVersion?: string;
respectSystemAlerts?: boolean;
snapshotMaxDepth?: number;
udid?: string;
waitForIdleTimeout?: number;
webDriverAgentUrl?: string;
};
```
Example:
```ts theme={null}
import { flow, launch } from "@qawolf/flows/ios";
export default flow(
"Launch app bundle",
"iOS - iPhone 15 (iOS 26)",
async () => {
const { driver } = await launch({
app: { path: "ios/MyApp.app" },
bundleId: "com.example.ios",
snapshotMaxDepth: 500,
});
await driver.pause(1000);
},
);
```
### Launch Defaults
The current implementation applies these defaults:
* when `app` is omitted, launch falls back to the runner-provided executable input path and then to installed-app startup through `bundleId`
* `respectSystemAlerts` defaults to `true`
* `snapshotMaxDepth` defaults to `999`
* `noReset` defaults to `false`
Example:
```ts theme={null}
import { flow, launch } from "@qawolf/flows/ios";
export default flow(
"Use iOS defaults",
"iOS - iPhone 15 (iOS 26)",
async () => {
const { driver } = await launch({
bundleId: "com.example.ios",
});
await driver.pause(1000);
},
);
```
### App Resolution
When your CI pipeline uploads an iOS build, QA Wolf sets `RUN_INPUT_PATH` to the uploaded file before the flow runs. Omit `app` in your launch call and QA Wolf will use that path automatically — you only need to provide the `bundleId`.
When `app` is provided, the current resolution order is:
1. `app.path`
2. `app.env`
3. `app.url`
When `app` is omitted, launch falls back to `RUN_INPUT_PATH`.
Relative paths are resolved against `RUN_INPUTS_EXECUTABLES_DIR` when that environment variable is present.
Explicit app source examples:
```ts theme={null}
await launch({ app: { path: "ios/MyApp.app" } });
await launch({ app: { env: "IOS_APP_PATH" } });
await launch({ app: { url: "https://example.com/MyApp.zip" } });
```
`RUN_INPUT_PATH` fallback example:
```ts theme={null}
// The runner sets RUN_INPUT_PATH before the flow starts.
await launch({
bundleId: "com.example.ios",
});
```
If `app` is present but does not resolve to a value, launch does not fall back to `RUN_INPUT_PATH`.
### Advanced Appium capabilities
The named launch options above cover the most common settings. For anything else, use the `capabilities` escape hatch: its entries are spread into the underlying [webdriver.io `remote`](https://webdriver.io/docs/api) capabilities and passed straight to the iOS driver.
`startIos` is a thin wrapper around webdriver.io's `remote`, so capabilities that QA Wolf does not set itself are forwarded as-is. See the Appium references for the full list of available keys:
* [Appium core capabilities](https://appium.io/docs/en/2.11/guides/caps/)
* [XCUITest (iOS) driver capabilities](https://github.com/appium/appium-xcuitest-driver/blob/v8.3.1/docs/reference/capabilities.md)
```ts theme={null}
import { flow, launch } from "@qawolf/flows/ios";
export default flow(
"Launch with custom capabilities",
"iOS - iPhone 15 (iOS 26)",
async () => {
const { driver } = await launch({
// Named options cover the common knobs:
respectSystemAlerts: true,
waitForIdleTimeout: 10,
// Anything else is passed through verbatim:
capabilities: {
"appium:resetOnSessionStartOnly": true,
"appium:settings[useFirstMatch]": "true",
"appium:waitForQuiescence": "false",
},
});
await driver.pause(1000);
},
);
```
Named options take precedence over `capabilities` for the same key. In particular, `noReset`, `respectSystemAlerts`, and `snapshotMaxDepth` are always applied (using their defaults when omitted), so configure those through the named options rather than `capabilities`.
### Migrating from `wdio.startIos`
Older flows obtained a `wdio` handle from the test context and called `startIos` directly. With the flow API, pass the same configuration through the flow's `launch` options instead — map the settings that have a named option, and route the rest through `capabilities`. The `driver` you receive is the same webdriver.io `Browser` handle, so there is no separate "raw" driver to obtain.
```ts theme={null}
// Before: raw wdio.startIos
const driver = await wdio.startIos({
"appium:app": process.env.IOS_PATH,
"appium:settings[respectSystemAlerts]": true,
"appium:resetOnSessionStartOnly": true,
"appium:settings[useFirstMatch]": "true",
"appium:waitForQuiescence": "false",
"appium:waitForIdleTimeout": 10,
});
```
```ts theme={null}
// After: flow launch options
import { flow } from "@qawolf/flows/ios";
export default flow(
"Open iOS app",
{
target: "iOS - iPhone 15 (iOS 26)",
launch: {
app: { env: "IOS_PATH" },
respectSystemAlerts: true,
waitForIdleTimeout: 10,
capabilities: {
"appium:resetOnSessionStartOnly": true,
"appium:settings[useFirstMatch]": "true",
"appium:waitForQuiescence": "false",
},
},
},
async ({ driver }) => {
await driver.pause(1000);
},
);
```
`app: { env: "IOS_PATH" }` reads the path from the environment variable named `IOS_PATH`, replacing the raw `"appium:app": process.env.IOS_PATH` form.
## `device`
`device` is a runtime proxy over the iOS simulator or device API. Use it for device-level operations — such as installing configuration profiles, simulating sensors, or managing device state — that sit outside app UI interactions. Use `driver` for interacting with the app itself.
Example:
```ts theme={null}
import { device, flow, launch } from "@qawolf/flows/ios";
const profile = `
`;
export default flow("Install profile", "iOS - iPhone 15 (iOS 26)", async () => {
const { driver } = await launch();
await device.installConfigurationProfile(driver, profile);
});
```
## `expect`
The exported `expect` is the assertion helper for iOS flows, backed by [`expect-webdriverio`](https://webdriver.io/docs/api/expect-webdriverio). All WebdriverIO matchers are available — including auto-retrying element matchers like `toBeDisplayed`, `toExist`, `toHaveText`, and `toHaveAttribute`. See the [expect-webdriverio matchers reference](https://webdriver.io/docs/api/expect-webdriverio) for the full list.
Always import `expect` from `@qawolf/flows/ios` rather than from `expect-webdriverio` directly, so that QA Wolf's defaults (timeout, `toHaveScreenshot`) stay in effect.
Example:
```ts theme={null}
import { expect, flow } from "@qawolf/flows/ios";
export default flow(
"Verify welcome screen",
{ target: "iOS - iPhone 15 (iOS 26)", launch: true },
async ({ driver, test }) => {
await test("welcome heading is visible", async () => {
const heading = driver.$("~welcome-heading");
await expect(heading).toBeDisplayed();
await expect(heading).toHaveText("Welcome");
});
},
);
```
For visual regression assertions with `expect(driver).toHaveScreenshot(...)`, see [Native mobile screenshots](/qawolf/visual-diffing-native-app).
# iOS Device Reference
Source: https://docs.qawolf.com/qawolf/libraries/flows/api-reference/ios-device-reference
Reference notes for the `device` object from `@qawolf/flows/ios`.
`device` is a runtime proxy over the iOS simulator and device API. Use it for device-level operations — such as injecting camera or audio input, managing photos, recording audio, simulating network conditions, or installing configuration profiles — that sit outside app UI interactions. Use `driver` for interacting with the app itself.
`device` is only available while a flow is running.
```typescript theme={null}
import { device, flow } from "@qawolf/flows/ios";
```
## `injectCamera`
Replaces the app's camera input with a provided image or video. Affects photo capture, video data output, video recording, and preview layers.
```typescript theme={null}
function injectCamera(
driver: Browser,
bundleId: string,
source: CameraSource
): Promise<() => Promise>;
type CameraSource = {
/** File path, URL, or data URI for the media to inject. */
data: string;
/** Media type. Default: inferred from file extension. */
type?: "image" | "video";
/** Delay in seconds before injection starts. */
delaySeconds?: number;
};
```
Returns a cleanup function that removes the injection config and media file.
Cannot be used simultaneously with `injectAudio` — they share the same config file.
Example:
```typescript theme={null}
import { device, flow, launch } from "@qawolf/flows/ios";
export default flow(
"Test camera feature",
{ target: "iOS - iPhone 15 (iOS 26)", launch: true },
async ({ driver, test }) => {
await test("inject camera image", async () => {
const cleanup = await device.injectCamera(driver, "com.example.app", {
data: "/path/to/image.jpg",
type: "image",
});
// interact with your app's camera feature
await cleanup();
});
},
);
```
## `injectAudio`
Replaces the app's microphone input with a provided audio file. Affects `AVAudioRecorder` and `AVCaptureAudioDataOutput`.
```typescript theme={null}
function injectAudio(
driver: Browser,
bundleId: string,
source: AudioSource
): Promise<() => Promise>;
type AudioSource = {
/** File path, URL, or data URI for the audio to inject. */
data: string;
/** Delay in seconds before injection starts. */
delaySeconds?: number;
};
```
Returns a cleanup function that removes the injection config and audio file.
Cannot be used simultaneously with `injectCamera` — they share the same config file.
Example:
```typescript theme={null}
import { device, flow, launch } from "@qawolf/flows/ios";
export default flow(
"Test voice input",
{ target: "iOS - iPhone 15 (iOS 26)", launch: true },
async ({ driver, test }) => {
await test("inject audio", async () => {
const cleanup = await device.injectAudio(driver, "com.example.app", {
data: "/path/to/audio.mp3",
});
// interact with your app's microphone feature
await cleanup();
});
},
);
```
## `injectBarcode`
Injects a barcode or QR code detection into the app's `AVCaptureMetadataOutput`. The injected barcode is delivered to the app's metadata output delegate as if scanned by the camera. The config file is auto-consumed after delivery.
```typescript theme={null}
function injectBarcode(
driver: Browser,
bundleId: string,
barcodes: BarcodeConfig | BarcodeConfig[]
): Promise<() => Promise>;
type BarcodeConfig = {
/** AVMetadataObjectType constant. Default: "org.iso.QRCode". */
type?: string;
/** The barcode or QR code value to inject. */
value: string;
/** Normalized bounds (0.0–1.0) for the detected barcode position. */
bounds?: {
x: number;
y: number;
width: number;
height: number;
};
/** Corner points (0.0–1.0) for the detected barcode shape. */
corners?: { x: number; y: number }[];
/**
* Raw binary payload for `AVMetadataMachineReadableCodeObject.rawValue` (iOS 13+).
* Accepts a Buffer or base64-encoded string. Defaults to the UTF-8 encoding of `value`.
*/
rawValue?: Buffer | string;
};
```
Returns a cleanup function that removes the injection config.
Example:
```typescript theme={null}
import { device, flow, launch } from "@qawolf/flows/ios";
export default flow(
"Scan QR code",
{ target: "iOS - iPhone 15 (iOS 26)", launch: true },
async ({ driver, test }) => {
await test("inject QR code", async () => {
const cleanup = await device.injectBarcode(driver, "com.example.app", {
value: "https://example.com",
});
// your app's scanner receives the QR code
await cleanup();
});
},
);
```
## `injectBeacon`
Injects iBeacon detections into the app's `CLLocationManager`. Always triggers a region-entry callback. When `beacons` is provided, also triggers ranging callbacks.
```typescript theme={null}
function injectBeacon(
driver: Browser,
bundleId: string,
config: BeaconConfig
): Promise<() => Promise>;
type BeaconConfig = {
/** Beacon region UUID. */
uuid: string;
/**
* Beacons to simulate. When omitted or empty, only a region-entry callback is triggered.
*/
beacons?: {
major: number | string;
minor: number | string;
}[];
};
```
Returns a cleanup function that removes the injection config files.
Example:
```typescript theme={null}
import { device, flow, launch } from "@qawolf/flows/ios";
export default flow(
"Test beacon detection",
{ target: "iOS - iPhone 15 (iOS 26)", launch: true },
async ({ driver, test }) => {
await test("inject beacon", async () => {
const cleanup = await device.injectBeacon(driver, "com.example.app", {
uuid: "8613BEAD-5465-4515-8F9C-AEEA717484C9",
beacons: [{ major: 1, minor: 7 }],
});
// your app receives region-entry and ranging callbacks
await cleanup();
});
},
);
```
## `installConfigurationProfile`
Installs a configuration profile on the iOS simulator or device. Returns a cleanup function that removes the profile when called.
```typescript theme={null}
function installConfigurationProfile(
driver: Browser,
profileString: string
): Promise<() => Promise>;
```
Example:
```typescript theme={null}
import { device, flow, launch } from "@qawolf/flows/ios";
const profile = `
`;
export default flow("Install profile", "iOS - iPhone 15 (iOS 26)", async () => {
const { driver } = await launch();
const cleanup = await device.installConfigurationProfile(driver, profile);
// run your tests
await cleanup();
});
```
## `setWebViewDebugging`
Enables or disables WebView debugging (Safari Web Inspector) for all `WKWebView` instances in the app. When enabled, WKWebViews become inspectable via Safari DevTools. The default behavior is enabled.
```typescript theme={null}
function setWebViewDebugging(
driver: Browser,
bundleId: string,
enabled: boolean
): Promise<() => Promise>;
```
Returns a cleanup function that removes the config file, reverting to the default (enabled).
Example:
```typescript theme={null}
import { device, flow, launch } from "@qawolf/flows/ios";
export default flow(
"Disable WebView debugging",
{ target: "iOS - iPhone 15 (iOS 26)", launch: true },
async ({ driver, test }) => {
await test("disable debugging", async () => {
const cleanup = await device.setWebViewDebugging(driver, "com.example.app", false);
// run your tests
await cleanup();
});
},
);
```
## `savePhoto`
Saves an image file to the device's Photos library.
```typescript theme={null}
function savePhoto(
driver: Browser,
filePath: string
): Promise;
type SavePhotoResult = {
success: boolean;
message?: string;
};
```
Example:
```typescript theme={null}
import { device, flow, launch } from "@qawolf/flows/ios";
export default flow(
"Save photo to library",
{ target: "iOS - iPhone 15 (iOS 26)", launch: true },
async ({ driver, test }) => {
await test("save photo", async () => {
const result = await device.savePhoto(driver, "/path/to/image.jpg");
if (!result.success) throw new Error(result.message);
});
},
);
```
## `listPhotos`
Returns a list of all photo and video assets in the device's Photos library.
```typescript theme={null}
function listPhotos(driver: Browser): Promise;
type ListPhotosResult = {
success: boolean;
assets: PhotoAsset[];
totalCount: number;
};
type PhotoAsset = {
localIdentifier: string;
mediaType: string;
mediaSubtype: string;
creationDate: string;
modificationDate: string;
pixelWidth: number;
pixelHeight: number;
duration: number;
isFavorite: boolean;
isHidden: boolean;
};
```
## `deleteAllPhotos`
Deletes all photos and videos from the device's Photos library. Use this for test teardown to restore a clean state between runs.
```typescript theme={null}
function deleteAllPhotos(driver: Browser): Promise;
type DeletePhotosResult = {
success: boolean;
deletedCount: number;
message?: string;
};
```
Example:
```typescript theme={null}
import { device, flow, launch } from "@qawolf/flows/ios";
export default flow(
"Clean up photos",
{ target: "iOS - iPhone 15 (iOS 26)", launch: true },
async ({ driver, test }) => {
await test("delete all photos", async () => {
const result = await device.deleteAllPhotos(driver);
console.log(`Deleted ${result.deletedCount} photos`);
});
},
);
```
## `startSpeakerRecording`
Starts a recording session that captures audio output from the device speaker. Returns a session object with the ID needed to stop the recording.
```typescript theme={null}
function startSpeakerRecording(driver: Browser): Promise;
type SpeakerRecordingSession = {
id: string;
status: string;
};
```
## `stopSpeakerRecording`
Stops an active speaker recording session. Also calculates the audio fingerprint automatically. Returns the recorded file details.
```typescript theme={null}
function stopSpeakerRecording(
driver: Browser,
sessionId: string
): Promise;
type SpeakerRecordingFile = {
filename: string;
fingerprint?: number[];
duration?: number;
};
```
## `downloadSpeakerRecording`
Downloads the recorded audio file as a Buffer in WAV format.
```typescript theme={null}
function downloadSpeakerRecording(
driver: Browser,
filename: string
): Promise;
```
Example combining `startSpeakerRecording`, `stopSpeakerRecording`, and `downloadSpeakerRecording`:
```typescript theme={null}
import { device, flow, launch } from "@qawolf/flows/ios";
export default flow(
"Record and verify speaker audio",
{ target: "iOS - iPhone 15 (iOS 26)", launch: true },
async ({ driver, test }) => {
await test("record audio", async () => {
const session = await device.startSpeakerRecording(driver);
// trigger audio playback in your app
const file = await device.stopSpeakerRecording(driver, session.id);
const buffer = await device.downloadSpeakerRecording(driver, file.filename);
// assert against the buffer or fingerprint
});
},
);
```
## `calculateAudioFingerprint`
Calculates an audio fingerprint from a WAV audio buffer or file path. Use this to compare recorded audio against a known reference without byte-for-byte comparison.
```typescript theme={null}
function calculateAudioFingerprint(
driver: Browser,
audioData: Buffer | string
): Promise;
type AudioFingerprint = {
fingerprint: number[];
duration: number;
};
```
## `simulateNetworkCondition`
Simulates a degraded network condition on the device. Returns a cleanup function that restores normal network conditions when called.
```typescript theme={null}
function simulateNetworkCondition(
config: NetworkConditionConfig
): Promise<() => Promise>;
type NetworkConditionConfig = {
bandwidthKbps?: number;
latencyMs?: number;
jitterMs?: number;
packetLossPercent?: number;
};
```
Built-in presets:
| Preset | Bandwidth | Latency | Jitter | Packet loss |
| ------------------------------- | --------- | ------- | ------ | ----------- |
| `device.NETWORK_2G_EDGE` | 240 Kbps | 300ms | 100ms | 1.5% |
| `device.NETWORK_3G` | 1.8 Mbps | 100ms | 30ms | 0.5% |
| `device.NETWORK_4G_LTE` | 12 Mbps | 30ms | 10ms | 0.1% |
| `device.NETWORK_5G` | 100 Mbps | 10ms | 3ms | 0.01% |
| `device.NETWORK_SATELLITE` | 5 Mbps | 600ms | 50ms | 1% |
| `device.NETWORK_WIFI_CONGESTED` | 2 Mbps | 50ms | 40ms | 3% |
| `device.NETWORK_VERY_BAD` | 100 Kbps | 500ms | 200ms | 10% |
| `device.NETWORK_OFFLINE` | — | — | — | 100% |
Example:
```typescript theme={null}
import { device, flow, launch } from "@qawolf/flows/ios";
export default flow(
"Test on slow network",
{ target: "iOS - iPhone 15 (iOS 26)", launch: true },
async ({ driver, test }) => {
const cleanup = await device.simulateNetworkCondition(device.NETWORK_3G);
await test("load content on 3G", async () => {
// your test steps
});
await cleanup();
},
);
```
## `getNetworkCondition`
Returns the current network simulation state.
```typescript theme={null}
function getNetworkCondition(): Promise;
type NetworkConditionStatus = {
enabled: boolean;
config?: {
bandwidthKbps?: number;
latencyMs?: number;
jitterMs?: number;
packetLossPercent?: number;
};
interface?: string;
};
```
## `routeTraffic`
Routes network traffic for specific apps or domains through a tunnel. Returns a cleanup function that restores default routing when called.
```typescript theme={null}
function routeTraffic(config: RouteTrafficConfig): Promise<() => Promise>;
type RouteTrafficConfig = {
apps: string[];
domains?: string[];
tunnel:
| { type: "direct" }
| { type: "http-proxy"; host: string; port: number; username?: string; password?: string }
| { type: "wireguard"; configPath: string }
| { type: "openvpn"; configPath: string }
| { type: "relay"; host: string; port: number; username: string; password: string; dialer?: "tls" | "tcp"; secure?: boolean };
socksHost?: string;
socksPort?: number;
inspect?: boolean;
};
```
Example:
```typescript theme={null}
import { device, flow } from "@qawolf/flows/ios";
export default flow("Route traffic through proxy", "iOS - iPhone 15 (iOS 26)", async () => {
const cleanup = await device.routeTraffic({
apps: ["com.example.app"],
tunnel: {
type: "http-proxy",
host: "proxy.example.com",
port: 8080,
},
});
// run your tests
await cleanup();
});
```
## `getNetworkStatus`
Returns the current network routing status including VPN state, active tunnels, and routed apps.
```typescript theme={null}
function getNetworkStatus(): Promise;
type NetworkStatus = {
route: "direct" | "http-proxy" | "wireguard" | "openvpn" | "relay";
tunnels: Record;
routingTables?: { activeTable: string; tables: Record };
traffic?: {
routes: Record;
interfaces: Record;
};
vpnApp: { bundleId: string; installed: boolean; pid?: number };
routedApps: string[];
routedDomains: string[];
};
```
## `subscribeNetworkLogs`
Subscribes to real-time network log events streamed from the Appium plugin. Returns a subscription object with `on()` and `close()` methods.
```typescript theme={null}
function subscribeNetworkLogs(): NetworkLogSubscription;
interface NetworkLogSubscription {
on(handler: (entry: RecordedEntry) => void): void;
close(): void;
}
```
Requires `routeTraffic()` to have been called first with `inspect: true`.
Example:
```typescript theme={null}
import { device, flow } from "@qawolf/flows/ios";
export default flow("Inspect network traffic", "iOS - iPhone 15 (iOS 26)", async () => {
const cleanup = await device.routeTraffic({
apps: ["com.example.app"],
tunnel: { type: "direct" },
inspect: true,
});
const subscription = device.subscribeNetworkLogs();
subscription.on((entry) => {
if (entry.http) console.log(`${entry.http.method} ${entry.http.uri} → ${entry.http.statusCode}`);
if (entry.dns) console.log(`DNS ${entry.dns.name} → ${entry.dns.answer}`);
});
// trigger network activity in your app
subscription.close();
await cleanup();
});
```
The `RecordedEntry` object contains the following fields:
```typescript theme={null}
interface RecordedEntry {
service: string;
network: string;
remote?: string;
local?: string;
host?: string;
proto?: string;
http?: {
host: string;
method: string;
proto: string;
scheme: string;
uri: string;
statusCode: number;
request?: { contentLength: number; header: Record; body: string | null };
response?: { contentLength: number; header: Record; body: string | null };
};
websocket?: {
from: string;
fin: boolean; rsv1: boolean; rsv2: boolean; rsv3: boolean;
opcode: number; masked: boolean; maskKey: number; length: number; payload: string;
};
tls?: { serverName: string; cipherSuite: string; version: string; proto?: string };
dns?: { id: number; name: string; class: string; type: string; question: string; answer: string; cached: boolean };
sid: string;
time: string;
duration: number;
}
```
# Top-Level Reference
Source: https://docs.qawolf.com/qawolf/libraries/flows/api-reference/top-level
Reference notes for the top-level @qawolf/flows entry point.
The top-level `@qawolf/flows` entry point exposes cross-platform helpers and types.
## Primary Exports
* `platform.target`
* `configureTarget(...)`
* `getCurrentScope()`
* `resetTarget()`
* `FailWithoutRetryError`
It also re-exports:
* `type Target`
* `type TargetScope`
Example:
```ts theme={null}
import {
FailWithoutRetryError,
configureTarget,
getCurrentScope,
platform,
resetTarget,
} from "@qawolf/flows";
import type { Target } from "@qawolf/flows";
declare const target: Target;
configureTarget({ target });
const currentTarget = platform.target;
const currentScope = getCurrentScope();
if (!currentScope) {
throw new FailWithoutRetryError();
}
resetTarget();
```
## `platform.target`
`platform.target` is a getter on a frozen object. Every access reads the currently configured target.
```ts theme={null}
import { platform } from "@qawolf/flows";
const target = platform.target;
```
Example with setup:
```ts theme={null}
import { configureTarget, platform } from "@qawolf/flows";
import type { Target } from "@qawolf/flows";
declare const target: Target;
configureTarget({ target });
console.log(platform.target);
```
Current behavior from the implementation:
* it throws if no target has been configured
* it always reflects the latest value set through `configureTarget(...)`
## `configureTarget(...)`
Use this to set the active target in local execution or tests.
```ts theme={null}
import { configureTarget } from "@qawolf/flows";
import type { Target } from "@qawolf/flows";
declare const target: Target;
configureTarget({ target });
```
The input shape is:
```ts theme={null}
type TargetScope = {
target: Target;
};
```
## `getCurrentScope()`
Returns the current configured target scope, or `undefined` when no target has been configured yet.
Example:
```ts theme={null}
import { getCurrentScope } from "@qawolf/flows";
const scope = getCurrentScope();
if (scope) {
console.log(scope.target);
}
```
## `resetTarget()`
Clears the configured target scope.
Use this in tests to avoid leaking target state across cases.
Example:
```ts theme={null}
import { configureTarget, getCurrentScope, resetTarget } from "@qawolf/flows";
import type { Target } from "@qawolf/flows";
declare const target: Target;
configureTarget({ target });
resetTarget();
console.log(getCurrentScope()); // undefined
```
## `FailWithoutRetryError`
This is a dedicated error class whose message is `"failWithoutRetry"`.
Use it when flow execution should fail immediately rather than being treated as retryable by the surrounding runtime.
Example:
```ts theme={null}
import { FailWithoutRetryError } from "@qawolf/flows";
throw new FailWithoutRetryError();
```
# Web Reference
Source: https://docs.qawolf.com/qawolf/libraries/flows/api-reference/web
Reference for @qawolf/flows/web, the entry point for defining web flows with launch behavior, page assertions, and browser context helpers.
`@qawolf/flows/web` defines web flows and optional explicit launch behavior.
## Primary Exports
* `flow(...)`
* `launch(...)`
* `expect`
* `isAnonymous(...)`
* `isPersistent(...)`
* `isElectron(...)`
* `testContextDependencies`
It also exports web-specific target, launch, callback context, and flow definition types.
Example:
```ts theme={null}
import { expect, flow } from "@qawolf/flows/web";
export default flow(
"Open homepage",
{ target: "Web - Chrome", launch: true },
async ({ page, test }) => {
await test("open homepage", async () => {
await page.goto("https://example.com");
await expect(page).toBeDefined();
});
},
);
```
## Target Model
Web flows accept web targets only. Use the CLI entry point for `Basic`.
```ts theme={null}
type WebFlowTargetInput =
| WebFlowTarget
| {
target: WebFlowTarget;
launch?: false | undefined;
}
| {
target: WebBrowserFlowTarget;
launch: true | BrowserLaunchOptions;
}
| {
target: WebElectronFlowTarget;
launch: ElectronFlowLaunchOptions;
};
```
Examples:
```ts theme={null}
import { flow, isElectron, isPersistent, launch } from "@qawolf/flows/web";
export const explicitLaunchFlow = flow("Open later", "Web - Chrome", async () => {
const launchResult = await launch();
if (isElectron(launchResult)) throw new Error("Expected browser launch");
const page =
isPersistent(launchResult)
? launchResult.page
: await launchResult.context.newPage();
await page.goto("https://example.com");
});
export const launchedFlow = flow(
"Open immediately",
{ target: "Web - Chrome", launch: true },
async ({ page, test }) => {
await test("open page", async () => {
await page.goto("https://example.com");
});
},
);
```
## Launch Shapes
Browser launch accepts the generated Playwright browser and context launch options, excluding Playwright's raw `persistentContext` and `userDataDir` fields. QA Wolf adds `browserContext`, reintroduces `userDataDir` for persistent launches, and maps those options back to the underlying Playwright shape.
```ts theme={null}
type BrowserLaunchOptions = {
kind?: "browser";
browser?: "chrome" | "chromium" | "firefox" | "msedge" | "webkit";
browserContext?: "incognito" | "persistent";
userDataDir?: string;
// Generated Playwright browser and context options are also accepted,
// except for Playwright's raw persistentContext and userDataDir fields.
headless?: boolean;
permissions?: string[];
geolocation?: {
latitude: number;
longitude: number;
accuracy?: number;
};
polyfills?: {
intlListFormat?: boolean;
};
// ...plus other generated browser/context options.
};
type LaunchOptions =
| BrowserLaunchOptions
| {
kind: "electron";
executablePath: string;
};
type ElectronFlowLaunchOptions = {
executablePath: string;
};
type LaunchResult =
| AnonymousLaunchResult
| PersistentLaunchResult
| ElectronLaunchResult;
```
QA Wolf maps `browserContext: "persistent"` to Playwright's persistent context mode. Pass `userDataDir` with `browserContext: "persistent"` when a profile directory should be reused.
Browser example:
```ts theme={null}
import { flow, launch } from "@qawolf/flows/web";
export default flow(
"Open with persistent profile",
"Web - Chrome",
async () => {
const { page } = await launch({
browserContext: "persistent",
userDataDir: "/tmp/qawolf-profile",
});
await page.goto("https://example.com");
},
);
```
Electron declarative launch example:
```ts theme={null}
import { flow } from "@qawolf/flows/web";
export default flow(
"Open Electron app",
{
target: "Electron",
launch: {
executablePath: "/Applications/MyApp.app/Contents/MacOS/MyApp",
},
},
async ({ page, test }) => {
await test("open app", async () => {
await page.goto("about:blank");
});
},
);
```
Explicit `launch(...)` calls are not target-typed. Include `kind: "electron"` and use `firstWindowPage` from the result:
```ts theme={null}
import { flow, launch } from "@qawolf/flows/web";
export default flow("Open Electron app", "Web - Chrome", async () => {
const { firstWindowPage } = await launch({
kind: "electron",
executablePath: "/Applications/MyApp.app/Contents/MacOS/MyApp",
});
await firstWindowPage.getByRole("button", { name: "Sign in" }).click();
});
```
Static launch options infer a narrow return type when they determine the result shape:
* `launch({ browserContext: "persistent" })` returns `PersistentLaunchResult`
* `launch({ browserContext: "incognito" })` returns `AnonymousLaunchResult`
* `launch({ kind: "electron", executablePath })` returns `ElectronLaunchResult`
* `launch()` returns the full `LaunchResult` union because startup depends on the active flow target
Use `isAnonymous(...)`, `isPersistent(...)`, and `isElectron(...)` to narrow dynamic results before reading shape-specific fields.
## Flow Callback Context
All web flow callbacks receive:
* `inputs`
* `setOutput(...)`
* `test(...)`
Launch-enabled browser flows also receive:
* `page`
* `context`
* optional `browser`
Electron launch-enabled flows receive the first Electron window as `page`.
`test(...)` can be omitted for simple flows where grouping steps into named sub-steps doesn't add value. For most flows, wrapping steps in `test(...)` is recommended — the label appears in your results and makes failures easier to locate.
## `testContextDependencies`
`testContextDependencies` is exported for runner and tooling integration. Flow authors should usually use the public callback parameters above instead of depending on the raw runner dependency list.
## `expect`
The exported `expect` is the assertion helper for web flows, backed by [Playwright's `expect`](https://playwright.dev/docs/test-assertions). All Playwright matchers are available — including auto-retrying locator matchers like `toBeVisible`, `toHaveText`, `toHaveURL`, and `toHaveScreenshot`. See [Playwright's locator assertions reference](https://playwright.dev/docs/api/class-locatorassertions) for the full list.
Always import `expect` from `@qawolf/flows/web` rather than from `@playwright/test` directly, so that QA Wolf's defaults (custom timeout, visual diffing) stay in effect.
Example:
```ts theme={null}
import { expect, flow } from "@qawolf/flows/web";
export default flow(
"Verify homepage",
{ target: "Web - Chrome", launch: true },
async ({ page, test }) => {
await test("hero heading is visible", async () => {
await page.goto("https://example.com");
await expect(page.getByRole("heading", { name: "Example Domain" })).toBeVisible();
});
},
);
```
For visual regression assertions with `expect(page).toHaveScreenshot(...)`, see [Web & Mobile web screenshots](/qawolf/visual-diffing).
## Defaults
* default `kind` is browser launch
* default `browserContext` is `"incognito"`
* default `browser` comes from the flow target
* GPU launch behavior is derived from the flow target
* Electron declarative launch requires `target: "Electron"` and `executablePath`
Example:
```ts theme={null}
import { flow, isElectron, isPersistent, launch } from "@qawolf/flows/web";
export default flow(
"Use target browser defaults",
"Web - Firefox",
async () => {
const launchResult = await launch();
if (isElectron(launchResult))
throw new Error("Expected browser launch");
const page =
isPersistent(launchResult)
? launchResult.page
: await launchResult.context.newPage();
await page.goto("https://example.com");
},
);
```
# Flows Troubleshooting
Source: https://docs.qawolf.com/qawolf/libraries/flows/troubleshooting
Troubleshoot common issues when authoring or running @qawolf/flows, including target configuration errors and missing launch context.
## `platform.target` Throws
Cause: The active target was read before it was configured.
Check:
* the runner is setting the target before flow code executes
* tests and local tooling call `configureTarget(...)` explicitly
## Callback Is Missing `page` Or `driver`
Cause: The flow uses the no-`launch` path, so launched runtime objects are not injected into the callback automatically.
Check:
* whether the flow should use `{ target, launch: true }`
* whether the flow should call `launch()` explicitly inside the callback
## Runtime API Called Outside The Flow Callback
Cause: `launch()`, `device`, or `platform.target` was called at the module level, outside the flow callback.
Check:
* that all runtime API calls are inside the flow callback, not at the top of the file
* that module-level code is limited to imports, constants, and pure helper functions
# API Reference
Source: https://docs.qawolf.com/qawolf/libraries/pom/api-reference/index
API reference for @qawolf/pom: page object base classes, the page registry, typed construction, and page hooks.
`@qawolf/pom` provides Page Object Model (POM) infrastructure for
[Playwright](https://playwright.dev): base classes for page objects, a registry
that constructs page objects by name and collects their page hooks, and
automatic installation of popup and route-interception hooks.
## Requirements
* **Node.js** `>=22.22`
* **ES modules** — the package is ESM-only (`"type": "module"`). Import it from
ESM code and use explicit `.js` specifiers in relative imports.
* **Peer dependencies**
* `@qawolf/flows` — used by `EntryPointPageObject` to launch the browser.
* `playwright` — for the `Page` / `Locator` types your page objects use.
## Install
```sh theme={null}
npm install @qawolf/pom
```
## Defining a page object
Extend `BasePageObject`. It stores the Playwright `Page` (available as
`this.page`) and provides two ways to construct another page object: the static
`createFromPage(page)` factory on a class you imported, and `this.create(name)`
for a registered name. Keep selectors in a private `locators` getter.
```ts theme={null}
import { BasePageObject } from "@qawolf/pom";
import { DashboardPage } from "./dashboard-page.js";
export class LoginPage extends BasePageObject {
private get locators() {
return {
email: this.page.getByLabel("Email"),
password: this.page.getByLabel("Password"),
submit: this.page.getByRole("button", { name: "Sign in" }),
} as const;
}
async signIn(email: string, password: string) {
await this.locators.email.fill(email);
await this.locators.password.fill(password);
await this.locators.submit.click();
return DashboardPage.createFromPage(this.page);
}
}
```
`dashboard-page.js` may import `login-page.js` back, so two page objects that
navigate to each other can both expose the trip. What Node cannot load is a page
object that `extend`s a class in a file importing it back — see
[Troubleshooting](/qawolf/libraries/pom/troubleshooting).
## The page registry
The registry does two things: it constructs a page object from a name, and it is
the list `installPageHooks()` walks to collect popup handlers and route
interceptors. A direct import replaces the first job but not the second, so a
page object that declares `popupHandlers()` or `routeInterceptors()` needs a
registry entry however its instances get built.
Constructing by name also keeps a page object's module out of the calling file's
import graph until the first construction, which is worth having when a
workspace holds many page objects.
### Registering
Create one module that registers every page object, and import it for its side
effects **before** you construct any page object. Register lazily with a module
loader so a page object's module is only loaded when it is first used:
```ts theme={null}
// register-pages.ts
import { registerPage } from "@qawolf/pom";
registerPage("LoginPage", () => import("./pages/login-page.js"));
registerPage("DashboardPage", () => import("./pages/dashboard-page.js"));
```
The loader must resolve to a module that exports the class under the **same
name** it was registered with. Eager registration also works when you already
hold the class value:
```ts theme={null}
import { LoginPage } from "./pages/login-page.js";
registerPage("LoginPage", LoginPage);
```
### Constructing
`createPage` builds a registered page object for a given `Page`. Import the
registration module first so the registry is populated:
```ts theme={null}
import "./register-pages.js";
import { createPage } from "@qawolf/pom";
const loginPage = await createPage("LoginPage", page);
await loginPage.signIn("user@example.com", "hunter2");
```
Inside a page object, use the protected `this.create(...)` instead — it shares
the current `Page` and goes through the same registry. Import the sibling's type
for the return annotation:
```ts theme={null}
import { BasePageObject } from "@qawolf/pom";
import type { SettingsPage } from "./settings-page.js";
export class DashboardPage extends BasePageObject {
async openSettings(): Promise {
await this.page.getByRole("link", { name: "Settings" }).click();
return this.create("SettingsPage");
}
}
```
`create` / `createPage` are async because lazily registered modules load on
first use. Importing `SettingsPage` and calling
`SettingsPage.createFromPage(this.page)` is the synchronous alternative, and
needs no registry entry and no separate return-type annotation.
### Typing `this.create(...)`
By default `this.create("LoginPage")` returns `any`. Augment the
`RegisteredPages` interface — declared for exactly this purpose — to make the
name-to-type mapping known, and `create` becomes fully typed. This is
incremental: names you don't list keep the `any` fallback.
```ts theme={null}
import type { LoginPage } from "./pages/login-page.js";
import type { DashboardPage } from "./pages/dashboard-page.js";
declare module "@qawolf/pom" {
interface RegisteredPages {
LoginPage: LoginPage;
DashboardPage: DashboardPage;
}
}
```
Now `await this.create("LoginPage")` is typed as `LoginPage`.
## Entry points and page hooks
An **entry point** is the page object that owns browser startup. Extend
`EntryPointPageObject` and expose a `create()` that launches the browser
(`initializeBrowser`), builds the instance (`createFromPage`), and installs
page hooks:
```ts theme={null}
import { EntryPointPageObject } from "@qawolf/pom";
export class AppEntryPoint extends EntryPointPageObject {
static async create(): Promise {
const page = await AppEntryPoint.initializeBrowser();
const entryPoint = AppEntryPoint.createFromPage(page);
await entryPoint.installPageHooks();
return entryPoint;
}
async openLogin() {
await this.goto("/login");
return this.create("LoginPage");
}
}
```
A page object can own popups to auto-dismiss or routes to intercept by
overriding `popupHandlers()` / `routeInterceptors()` on its class:
```ts theme={null}
import { BasePageObject, type PopupHandlerDef } from "@qawolf/pom";
export class CookieBannerPage extends BasePageObject {
override popupHandlers(): PopupHandlerDef[] {
return [
{
name: "cookie-banner",
trigger: this.page.getByRole("button", { name: "Accept cookies" }),
dismiss: async () => {
await this.page
.getByRole("button", { name: "Accept cookies" })
.click();
},
},
];
}
}
```
`installPageHooks()` collects these across **every** registered page object, not
just the entry point — so register a page object that owns popups or routes even
when your code only ever constructs it from a direct import. Its hooks never
install otherwise. Overrides are detected by an own-property check on the
registered class's prototype, so declare `popupHandlers()` /
`routeInterceptors()` directly on the class you register — an override inherited
from a base class is not picked up.
When registering lazily, pass `{ providesPageHooks: false }` for page objects
you know declare no hooks, so hook installation can skip loading their modules:
```ts theme={null}
registerPage("StaticPage", () => import("./pages/static-page.js"), {
providesPageHooks: false,
});
```
## Exports
| Export | Description |
| ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
| `BasePageObject` | Base class for page objects. Holds `this.page`; provides `this.create(name)` and the static `createFromPage(page)` factory. |
| `SubPageObject` | Base class for a page object scoped to a parent region/component. |
| `EntryPointPageObject` | Base class for the entry point; owns browser launch (`initializeBrowser`), `goto`, and `installPageHooks`. |
| `registerPage(name, classOrLoader, options?)` | Register a page object by name, eagerly (class) or lazily (`() => import(...)`). |
| `createPage(name, page)` | Construct a registered page object for a `Page`. Returns a promise. |
| `PopupHandler` | Manages add/remove of popup handlers on a page. |
| `NetworkMonitor` | Observes network activity and surfaces `NetworkError`s. |
| `RegisteredPages` | Empty interface you augment to type `create(name)`. |
| `PopupHandlerDef`, `RouteInterceptorDef`, `PageSetupOptions` | Hook and setup types. |
| `callPlatformAPI` | Client for the QA Wolf platform API. |
| `assertPricesClose`, `moneyToNumber`, `numberToMoney` | Money/price test-data helpers. |
| `reportCleanupFailed`, `reportCleanupFailure` | Report failures from test cleanup steps. |
# POM Troubleshooting
Source: https://docs.qawolf.com/qawolf/libraries/pom/troubleshooting
Troubleshoot common issues when using @qawolf/pom, including unregistered pages, duplicate registrations, and stale page-hook flags.
## `Unknown page: ` When Calling `create`
Cause: The module that calls `registerPage` for that name has not been imported,
so the registry is empty for it.
Check:
* the workspace's `register-pages` module is side-effect imported before any
`create(name)` / `createPage(name, ...)` call
* the name passed to `create` exactly matches the name passed to `registerPage`
## `Page "" is already registered.`
Cause: `registerPage` was called twice for the same name. The registry throws on
a duplicate key rather than silently overwriting.
Check:
* the name is registered in exactly one place
* two page objects are not registered under the same name
## `Cannot access '' before initialization`
Cause: Two files import each other, and one of them uses the other's class while
that file is still loading — almost always a page object that `extend`s a class
declared in a file importing it back. Two page objects that import each other
are fine; extending across that pair is not, because the file that loads second
evaluates `extends` before the class exists.
Check:
* the base class lives in a file that imports neither of the two page objects
* nothing else in the cycle runs at module top level against an imported
binding — using it inside a method body is fine, since both files have
finished loading by then
## A Page Object's Popup Or Route Hook Never Runs
Cause: The page object is not registered, or page hooks are collected by an
own-property check on the **registered** class's prototype and the override is
inherited from a base class.
Check:
* the page object is passed to `registerPage`, even if your code only ever
constructs it from a direct import — `installPageHooks()` walks the registry
and cannot see an unregistered class
* `popupHandlers()` / `routeInterceptors()` is declared directly on the class
passed to `registerPage`, not only on a base class it extends
* the entry point calls `installPageHooks()` before navigating
## `providesPageHooks: false` Contradicts The Class
Cause: A page object registered with `{ providesPageHooks: false }` declares a
`popupHandlers()` / `routeInterceptors()` override. The registry throws so the
stale flag surfaces instead of silently skipping hook installation — at
`registerPage` time for an eagerly registered class, on first load for a lazy
one.
Check:
* drop `{ providesPageHooks: false }` from that registration so its hooks are
installed
## `this.create("X")` Is Typed As `any`
Cause: The name is not present in the `RegisteredPages` map.
Check:
* the workspace augments `RegisteredPages` (via `declare module "@qawolf/pom"`)
with an `X: X` entry
* the augmentation module is included in the TypeScript program
## Two Registries / Pages Invisible To Each Other
Cause: More than one copy of `@qawolf/pom` resolved at runtime. The registry is
module-level state, so each copy has its own — a page registered through one is
invisible to the other.
Check:
* a single installed copy of `@qawolf/pom` resolves at runtime (no duplicate in
a nested `node_modules`)
* workspace code does not mix `@qawolf/pom` with a locally vendored copy of the
POM library
# Client Reference
Source: https://docs.qawolf.com/qawolf/libraries/testkit/api-reference/client
Reference for @qawolf/testkit/client, the runner-facing entry point used to create and configure the testkit client for flow helpers.
`@qawolf/testkit/client` is the runner-facing entry point.
## Primary Export
* `createTestkitClient(...)`
Example:
```ts theme={null}
import { createTestkitClient } from "@qawolf/testkit/client";
const client = createTestkitClient({
mountCifsShare: async () => "/Volumes/shared",
startOpenVpn: async () => "vpn-started",
startWireGuard: async () => "wireguard-started",
});
```
## Input Ports
The client is built from environment-specific ports such as:
* `mountCifsShare`
* `saveSnapshot`
* `startOpenVpn`
* `startWireGuard`
If `saveSnapshot` is present, the created client exposes `saveBaselineScreenshot(...)`. Otherwise, that helper remains unavailable.
Example with screenshot support:
```ts theme={null}
import { createTestkitClient } from "@qawolf/testkit/client";
const client = createTestkitClient({
mountCifsShare: async ({ mountPoint }) => mountPoint,
saveSnapshot: async (name, bytes) => {
console.log(name, bytes.length);
},
startOpenVpn: async ({ configPath }) => configPath,
startWireGuard: async ({ configPath }) => configPath,
});
await client.saveBaselineScreenshot?.(
{
screenshot: async () => Buffer.from("image"),
},
"login-page",
);
```
# Core Reference
Source: https://docs.qawolf.com/qawolf/libraries/testkit/api-reference/core
Reference notes for the top-level @qawolf/testkit entry point.
The top-level package currently exposes:
* `otp.fromUri(...)`
* `configureTestkitClient(...)`
* `getCurrentTestkitClient(...)`
* `resetTestkitClient(...)`
* `mountCifsShare(...)`
* `startOpenVpn(...)`
* `saveBaselineScreenshot(...)`
* `saveEnvironmentVariable(...)`
* `reloadEnvironmentVariables(...)`
Example:
```ts theme={null}
import {
configureTestkitClient,
mountCifsShare,
otp,
reloadEnvironmentVariables,
saveEnvironmentVariable,
startOpenVpn,
} from "@qawolf/testkit";
import { createTestkitClient } from "@qawolf/testkit/client";
configureTestkitClient(
createTestkitClient({
mountCifsShare: async () => "/Volumes/shared",
startOpenVpn: async () => "vpn-started",
startWireGuard: async () => "wireguard-started",
}),
);
const code = otp.fromUri("otpauth://totp/QA%20Wolf?secret=JBSWY3DPEHPK3PXP");
await mountCifsShare({
mountPoint: "/Volumes/shared",
password: "secret",
share: "//server/share",
username: "wolf",
});
await startOpenVpn({ configPath: "/tmp/test.ovpn" });
await saveEnvironmentVariable("SESSION_TOKEN", "abc123");
await reloadEnvironmentVariables();
console.log(code);
```
## Environment Variables
Use `saveEnvironmentVariable(name, value)` to save a string value to the current QA Wolf environment and update `process.env[name]` for the running flow.
Use `reloadEnvironmentVariables()` to fetch the latest environment variables for the current QA Wolf environment and copy string values into `process.env`.
```ts theme={null}
import {
reloadEnvironmentVariables,
saveEnvironmentVariable,
} from "@qawolf/testkit";
await saveEnvironmentVariable("LOGIN_TOKEN", token);
await reloadEnvironmentVariables();
console.log(process.env["LOGIN_TOKEN"]);
```
These helpers are intended for QA Wolf runner environments. They require the runner-provided `QAWOLF_API_URL`, `QAWOLF_ENVIRONMENT_ID`, and `QAWOLF_TEAM_API_KEY` environment variables.
## Error Behavior
Client-dependent helpers throw when no client has been configured.
Environment variable helpers throw when the QA Wolf API configuration is missing or when the API request fails.
`saveBaselineScreenshot(...)` also throws when the configured client does not provide screenshot support.
Examples:
```ts theme={null}
import { mountCifsShare, resetTestkitClient } from "@qawolf/testkit";
resetTestkitClient();
await mountCifsShare({
mountPoint: "/Volumes/shared",
password: "secret",
share: "//server/share",
username: "wolf",
});
```
```ts theme={null}
import {
configureTestkitClient,
saveBaselineScreenshot,
} from "@qawolf/testkit";
import { createTestkitClient } from "@qawolf/testkit/client";
configureTestkitClient(
createTestkitClient({
mountCifsShare: async () => "/Volumes/shared",
startOpenVpn: async () => "vpn-started",
startWireGuard: async () => "wireguard-started",
}),
);
await saveBaselineScreenshot(
{
screenshot: async () => Buffer.from("image"),
},
"login-page",
);
```
# API Reference
Source: https://docs.qawolf.com/qawolf/libraries/testkit/api-reference/index
API landing page for @qawolf/testkit and related entry points.
## Entry Points
* [Core](/qawolf/libraries/testkit/api-reference/core)
* [Types](/qawolf/libraries/testkit/api-reference/types)
* [Web](/qawolf/libraries/testkit/api-reference/web)
* [Client](/qawolf/libraries/testkit/api-reference/client)
Example imports:
```ts theme={null}
import { mountCifsShare, otp, saveEnvironmentVariable } from "@qawolf/testkit";
import { createTestkitClient } from "@qawolf/testkit/client";
import { readQRCode } from "@qawolf/testkit/web";
```
# Types Reference
Source: https://docs.qawolf.com/qawolf/libraries/testkit/api-reference/types
Reference notes for the public types used by @qawolf/testkit.
These are the main public types exposed through `@qawolf/testkit` and `@qawolf/testkit/client`.
## `MountCifsShareOptions`
```ts theme={null}
type MountCifsShareOptions = {
mountPoint: string;
password: string;
share: string;
username: string;
};
```
Used by:
* `mountCifsShare(...)`
* `TestkitPorts["mountCifsShare"]`
Example:
```ts theme={null}
const options: MountCifsShareOptions = {
mountPoint: "/Volumes/shared",
password: "secret",
share: "//server/share",
username: "wolf",
};
```
## `StartOpenVpnOptions`
```ts theme={null}
type StartOpenVpnOptions = {
configPath: string;
routeHosts?: string[];
routeIps?: string[];
routeNoPull?: boolean;
};
```
Used by:
* `startOpenVpn(...)`
* `TestkitPorts["startOpenVpn"]`
Example:
```ts theme={null}
const options: StartOpenVpnOptions = {
configPath: "/tmp/test.ovpn",
routeHosts: ["internal.example.com"],
};
```
## `StartWireGuardOptions`
```ts theme={null}
type StartWireGuardOptions = {
configPath: string;
};
```
Used by:
* `TestkitPorts["startWireGuard"]`
The current code exposes this through the client ports, but there is no top-level `startWireGuard(...)` helper export today.
Example:
```ts theme={null}
const options: StartWireGuardOptions = {
configPath: "/tmp/test.conf",
};
```
## `Screenshotable`
```ts theme={null}
type Screenshotable = {
screenshot: (screenshotOptions?: unknown) => Promise;
};
```
Used by `saveBaselineScreenshot(...)`. Any page or locator-like object that supports this screenshot method shape can be used.
Example:
```ts theme={null}
const pageLike: Screenshotable = {
screenshot: async () => Buffer.from("image"),
};
```
## `SaveBaselineScreenshot`
```ts theme={null}
type SaveBaselineScreenshot = (
pageOrLocator: Screenshotable,
name: string,
screenshotOptions?: unknown,
) => Promise;
```
Example:
```ts theme={null}
await saveBaselineScreenshot(page, "login-page");
await saveBaselineScreenshot(locator, "login-form", {
animations: "disabled",
});
```
## `SaveSnapshot`
```ts theme={null}
type SaveSnapshot = (name: string, bytes: Buffer) => Promise;
```
This is the lower-level runner port used to implement `saveBaselineScreenshot(...)`.
Example:
```ts theme={null}
const saveSnapshot: SaveSnapshot = async (name, bytes) => {
console.log(name, bytes.length);
};
```
## `TestkitPorts`
```ts theme={null}
type TestkitPorts = {
mountCifsShare: (options: MountCifsShareOptions) => Promise;
saveSnapshot?: SaveSnapshot;
startOpenVpn: (options: StartOpenVpnOptions) => Promise;
startWireGuard: (options: StartWireGuardOptions) => Promise;
};
```
These are the environment-specific ports that runners pass to `createTestkitClient(...)`.
Example:
```ts theme={null}
const ports: TestkitPorts = {
mountCifsShare: async ({ mountPoint }) => mountPoint,
saveSnapshot: async (name, bytes) => {
console.log(name, bytes.length);
},
startOpenVpn: async ({ configPath }) => configPath,
startWireGuard: async ({ configPath }) => configPath,
};
```
## `TestkitClient`
`TestkitClient` is a union of:
* a client with baseline screenshot support
* a client without baseline screenshot support
If `saveSnapshot` is provided, the created client exposes `saveBaselineScreenshot(...)`. Otherwise that method is absent.
Example:
```ts theme={null}
import { createTestkitClient } from "@qawolf/testkit/client";
const client = createTestkitClient({
mountCifsShare: async ({ mountPoint }) => mountPoint,
startOpenVpn: async ({ configPath }) => configPath,
startWireGuard: async ({ configPath }) => configPath,
});
console.log(client.saveBaselineScreenshot); // undefined without saveSnapshot
```
# Web Reference
Source: https://docs.qawolf.com/qawolf/libraries/testkit/api-reference/web
Reference for @qawolf/testkit/web, including the readQRCode helper that decodes QR codes from a page locator during a flow.
`@qawolf/testkit/web` currently exposes `readQRCode(page, selector)`.
## Current Signature
```ts theme={null}
readQRCode(page: Page, selector: string): Promise
```
Example:
```ts theme={null}
import { readQRCode } from "@qawolf/testkit/web";
const code = await readQRCode(page, '[data-testid="login-qr"]');
```
## Behavior
The helper:
* screenshots the locator matched by `selector`
* parses the result as PNG
* decodes the QR code if one is present
* returns `undefined` when no QR code is found
Example:
```ts theme={null}
import { readQRCode } from "@qawolf/testkit/web";
await page.goto("https://example.com");
const qrValue = await readQRCode(page, '[data-testid="qr-code"]');
if (qrValue) {
console.log(qrValue);
}
```
# Testkit Troubleshooting
Source: https://docs.qawolf.com/qawolf/libraries/testkit/troubleshooting
Troubleshoot common @qawolf/testkit problems, including missing client configuration and baseline screenshot errors during flow runs.
## Helper Throws Because No Client Is Configured
Cause: The flow called a client-dependent helper before the runner configured the testkit client.
Check:
* the runner calls `configureTestkitClient(...)` before flow execution
* tests call `resetTestkitClient()` between cases
## `saveBaselineScreenshot()` Throws
Cause: The configured client was created without `saveSnapshot`.
Check:
* the runtime passed snapshot support into `createTestkitClient(...)`
## QR Code Helper Returns `undefined`
Check:
* the selected element actually contains a QR code
* the screenshot is stable and large enough to decode reliably
## Update a baseline screenshot programmatically
If the QA Wolf editor doesn't let you promote a new screenshot as the updated baseline, you can do it in flow code instead. Add `saveBaselineScreenshot` to the flow temporarily, run it once to save the new baseline, then remove it.
```typescript theme={null}
import { saveBaselineScreenshot } from "@qawolf/testkit";
await saveBaselineScreenshot(page, "revenue-chart");
```
The name must exactly match the name used in the corresponding `toHaveScreenshot` call. Remove this line once the baseline has been updated — it is not meant to run in normal test execution.
# Lint rules
Source: https://docs.qawolf.com/qawolf/lint-rules
Configure the lint rules QA Wolf runs on your flows and page objects with an .eslintrc.json file at the root of your repository.
QA Wolf lints every JavaScript and TypeScript file in your workspace as you edit
it, and again while an AI job writes code. Findings appear inline in the editor,
the same way a type error does.
A set of base rules always runs — the ones that catch real mistakes, such as an
unhandled promise, an unreachable branch, or a duplicate object key. On top of
those you can turn on QA Wolf's page object model rules, and set the severity
of any rule yourself.
## Configure rules
Add an `.eslintrc.json` file at the root of your repository. Without one, only
the base rules run.
```json .eslintrc.json theme={null}
{
"extends": ["@qawolf/eslint-plugin-pom"]
}
```
That turns on QA Wolf's [page object model rules](#page-object-model-rules),
each at the severity it ships with. A rule ships at `error` when the code it
reports breaks at runtime, and at `warn` when it marks a convention.
## Set a rule's severity
List a rule under `rules` with the severity you want: `"off"`, `"warn"` or
`"error"` — or the matching number `0`, `1` or `2`.
```json theme={null}
{
"extends": ["@qawolf/eslint-plugin-pom"],
"rules": {
"@qawolf/pom-lint/no-wait-for-timeout-in-poms": "error",
"@qawolf/pom-lint/assert-expect-pairing": "off",
"no-debugger": "error"
}
}
```
Rules you do not list keep their shipped severity.
A page object model rule only takes effect once the plugin is enabled. If you
set a severity on one without listing `@qawolf/eslint-plugin-pom` in `extends`
or `plugins`, the rule stays off and the editor says so.
## Turn on any other ESLint rule
You are not limited to the rules QA Wolf enables by default. Name any core
ESLint or [typescript-eslint](https://typescript-eslint.io/rules/) rule and it
runs.
```json theme={null}
{
"rules": {
"@typescript-eslint/no-deprecated": "warn",
"eqeqeq": "error"
}
}
```
`@typescript-eslint/no-deprecated` is a useful one to know about: it flags calls
to any API whose documentation marks it `@deprecated`, which includes the
Playwright methods that have been superseded — `page.type()`, for example.
Rules that need type information — `no-deprecated` among them — report where
the types they need are loaded. In the editor that includes the types of the
packages your code imports. While an AI job runs, imports from npm packages
are not resolved, so a rule of this kind reports less there.
## Page object model rules
These come from
[`@qawolf/eslint-plugin-pom`](https://github.com/qawolf/eslint-plugin-pom),
which is bundled into QA Wolf — you do not install anything.
Each rule id is prefixed with `@qawolf/pom-lint/` when you name it under
`rules`, which is also the prefix you see in the editor.
### How a rule finds its subject
A rule checks either a directory or a kind of file, and the **Where** column
below says which:
* **`src/pages/`** — the rule reads `.ts` files under your page-object
directory and ignores everything else.
* **flow** — the rule recognizes a flow from its code: a module that imports
`flow` from `@qawolf/flows` (any subpath) or default-exports a `flow(...)`
call. The `.flow.ts` filename is not the signal, so a flow kept elsewhere is
still checked.
* **page object** — the rule recognizes a class extending `BasePageObject`,
`SubPageObject` or `EntryPointPageObject`, wherever the file lives.
* **anywhere** — the rule checks every file.
### Rules
| Rule | Level | Where | Reports |
| --------------------------------------- | ----- | ------------------- | --------------------------------------------------------------------------------- |
| `no-raw-page-in-flows` | error | flow | `page.goto()`, `page.click()`, … in a flow |
| `no-selectors-in-flows` | error | flow | `locator()` / `getBy*()` / `frameLocator()` in a flow |
| `no-expect-in-flows` | warn | flow | `expect()` in a flow, rather than an `assert*()` page-object method |
| `no-fetch-axios-in-flows` | error | flow | `fetch()` or an `axios` import in a flow |
| `no-any-shared-state` | error | flow | a `let` in the flow callback typed `any`, or not typed at all |
| `flow-export-structure` | error | flow | a flow module without `export default flow(name, target, callback)` |
| `no-code-between-steps` | error | flow | a statement after the first `await test(...)` that is not itself one |
| `test-aaa-comments` | warn | flow | a step with no Arrange / Act / Assert comment |
| `aaa-banner-format` | warn | flow | an Arrange / Act / Assert marker that is not the three-line banner |
| `assert-expect-pairing` | warn | `src/pages/` | `expect()` in a page-object method not named `assert*` |
| `correct-base-class` | warn | `src/pages/` | a class that reads `this.page` but extends no page-object base |
| `entry-point-factory` | warn | `src/pages/` | an `EntryPointPageObject` subclass with no `static create()` |
| `no-direct-pom-construction` | warn | `src/pages/` | `new OtherPage(this.page)` instead of `this.create("OtherPage")` |
| `no-inline-locator-in-page-object` | warn | `src/pages/` | a locator built from `this.page` outside the `locators` getter |
| `no-legacy-selectors` | warn | `src/pages/` | XPath, a `css=` / `text=` / `id=` prefix, or a `>>` chain in a `locator()` string |
| `no-mutable-state-in-pom` | warn | `src/pages/` | an instance field that is not `readonly` |
| `no-public-constructor` | warn | `src/pages/` | a redeclared constructor that is not `protected` |
| `no-wait-for-timeout-in-poms` | warn | `src/pages/` | `waitForTimeout()` / `waitForSelector()` in a page object |
| `selector-getter-shape` | warn | `src/pages/` | a `locators` holder that is public, a field, a method, or missing `as const` |
| `typed-create-return` | warn | `src/pages/` | a method returning `this.create("Name")` with no return type naming `Name` |
| `web-first-assertions` | warn | `src/pages/` | `expect(await locator.isVisible()).toBe(true)` and its siblings |
| `require-locator-jsdoc` | warn | page object | an entry in the `locators` map with no `/** … */` above it |
| `require-env-pattern` | error | flow or page object | `process.env.X` instead of the workspace's `requireEnv()` |
| `require-value-import-for-created-page` | error | anywhere | `this.create("Name")` where `Name` is bound by a type-only import |
| `file-naming-convention` | warn | anywhere | a file under `src/` whose name is not kebab-case |
| `no-non-null-assertion` | error | anywhere | a postfix `!` |
| `no-parameter-properties` | error | anywhere | `constructor(private x: T)` |
The [plugin's README](https://github.com/qawolf/eslint-plugin-pom#rules)
explains each rule with examples of what it reports and what it expects
instead.
`warn` marks a convention rather than a defect. If your workspace is not ready
for one, set that rule to `"off"` in `.eslintrc.json` rather than disabling it
at each site.
## What QA Wolf reads
QA Wolf reads three keys from `.eslintrc.json`: `extends`, `plugins` and
`rules`. Anything else — `settings`, `parserOptions`, `overrides`, `env` — has
no effect, and the editor flags it so the file does not quietly lie about what
is running.
Two more limits worth knowing:
* **Options are not supported.** In `"no-redeclare": ["error", { … }]` the
severity applies and the options after it are dropped.
* **`@qawolf/eslint-plugin-pom` is the only plugin supported at this time.**
Naming any other plugin in `extends` or `plugins` has no effect, and the
editor tells you it was skipped. QA Wolf can support additional plugins —
ask, and we will look at adding the one you need.
Four spellings all enable the plugin, so use whichever reads best to you:
`@qawolf/eslint-plugin-pom`, `@qawolf/pom`, `@qawolf/pom-lint`, or
`plugin:@qawolf/pom/recommended`. They work in `extends` and in `plugins`
alike.
## When the file cannot be read
If `.eslintrc.json` is not valid JSON, is not a JSON object, or is longer than
131,072 characters, QA Wolf ignores the whole file and enables none of the page
object model rules. The base rules still run, and the editor explains why the
file was skipped.
# Authenticate the QA Wolf CLI
Source: https://docs.qawolf.com/qawolf/local-execution/authenticate
Install the qawolf CLI and connect it to your workspace so it can pull and run your flows.
Before the CLI can pull flows from your workspace or run flows that depend on environment variables, it needs an API key. You can log in interactively for local development, or pass the key through an environment variable for CI.
## Install the CLI
The CLI ships in two forms: an npm package and a precompiled standalone binary.
### Install with npm
Node.js 22.12 or later is required.
```bash theme={null}
npm install -g @qawolf/cli
```
### Install a precompiled binary
Download the binary for your platform from [GitHub Releases](https://github.com/qawolf/cli/releases). Builds are published for Linux (x64 and arm64), macOS (Apple Silicon and Intel), and Windows x64. The binary has no Node.js runtime dependency. Extract the archive, then move the binary into a directory on your `PATH`.
### Verify
```bash theme={null}
qawolf --version
```
## Log in for local development
Generate an API key in your QA Wolf workspace under **Workspace settings → API keys**.
Run the login command:
```bash theme={null}
qawolf auth login
```
The CLI prompts for your API key, validates it against the platform, and stores it in your system keychain. When a keychain is not available, the CLI falls back to a local config file in your user profile directory.
Confirm the connection:
```bash theme={null}
qawolf auth whoami
```
The output shows the team name, ID, and where the credential was loaded from.
## Authenticate in CI
CI environments cannot prompt for input. Set the `QAWOLF_API_KEY` environment variable instead:
```bash theme={null}
export QAWOLF_API_KEY=
qawolf flows pull --env
```
The CLI prefers `QAWOLF_API_KEY` over any credential stored by `qawolf auth login`, so the same machine can switch between accounts by setting or unsetting the variable.
## Log out
```bash theme={null}
qawolf auth logout
```
`qawolf auth logout` only removes credentials stored by `qawolf auth login`. Credentials set through `QAWOLF_API_KEY` cannot be removed by this command — unset the variable in your shell instead.
## Switch the API URL
By default the CLI talks to `https://app.qawolf.com`. Override this with `QAWOLF_API_URL` if your workspace uses a different host:
```bash theme={null}
export QAWOLF_API_URL=https://qawolf.example.com
```
See the [Environment variables reference](/qawolf/libraries/cli/api-reference/environment-variables) for the full list of variables the CLI reads.
# Diagnose problems running flows
Source: https://docs.qawolf.com/qawolf/local-execution/diagnose-problems
Use qawolf doctor to check your environment, find missing dependencies, and read the CLI's log file.
When a flow run fails for reasons that aren't in the flow itself — a missing browser, an Android SDK that isn't on the path, a Node.js version mismatch — `qawolf doctor` is the first thing to run.
## Run the diagnostics
From the project root, run:
```bash theme={null}
qawolf doctor
```
The CLI checks:
* the CLI version
* the Node.js version against the required minimum
* your API key and connectivity to QA Wolf and the npm registry
* the Playwright installation, if any flow targets a browser
* browser availability for each target the project uses
* the Android SDK, if any flow targets Android
* flow references to file assets that may not be available locally
Read the output. Each check reports a pass, a warning, or a failure, with a remediation hint when it fails.
## Check every platform
`qawolf doctor` only checks the platforms your project actually uses. Pass `--all` to run every check, including platforms not used by the current project:
```bash theme={null}
qawolf doctor --all
```
This is useful when copying the project to a new machine and you want a single command that surfaces every missing dependency.
## Read the log file
The CLI writes structured logs to the platform's log directory:
* macOS: `~/Library/Logs/qawolf/cli.log`
* Linux: `$XDG_STATE_HOME/qawolf/cli.log` or `~/.local/state/qawolf/cli.log`
Run with `--verbose` to mirror debug logs to stderr in real time:
```bash theme={null}
qawolf flows run --verbose
```
## Common issues
The [CLI Troubleshooting reference](/qawolf/libraries/cli/troubleshooting) covers the specific errors the CLI emits, including authentication failures, missing Android SDK, expired download links, and the `@qawolf/testkit` resolution error.
# Install runtime dependencies
Source: https://docs.qawolf.com/qawolf/local-execution/install-dependencies
Install the Playwright browsers, Android system images, and drivers your flows need to run.
[`qawolf flows run --env `](/qawolf/local-execution/run-flows-locally) installs npm dependencies and Playwright browsers automatically before its first run, so web-only users rarely invoke `qawolf install` directly. Android dependencies are not installed during a run; run `qawolf install android` before running Android flows. Also reach for `qawolf install` to install ahead of time (for example, to warm a CI cache between the checkout step and the run step) or when running flows from a [local-only project](/qawolf/local-execution/set-up-a-project).
## Install everything the project needs
From the project root, run:
```bash theme={null}
qawolf install
```
The CLI scans the project for flows, looks at each flow's `target`, and installs the dependencies required by every target it finds.
## Install only for specific flows
Pass a pattern to limit which flows are considered:
```bash theme={null}
qawolf install "checkout/**"
```
This is useful in CI when a job only runs a subset of the suite.
## Install browsers only
```bash theme={null}
qawolf install browsers
qawolf install browsers "flows/web/**"
```
Equivalent to running `playwright install` for the browsers your web flows target.
## Install Android dependencies only
Set `ANDROID_HOME` (or `ANDROID_SDK_ROOT`) to the path of your Android SDK before running this command. Install the SDK through Android Studio's **SDK Manager** or the standalone `cmdline-tools` package.
```bash theme={null}
qawolf install android
qawolf install android "flows/mobile/**"
```
The command installs the Appium uiautomator2 driver, downloads the Android system images for each target the matching flows use, and creates the corresponding AVDs.
## iOS support
iOS flows are not yet executable by the CLI. `qawolf install` skips them with a warning. iOS support requires macOS with Xcode when it ships.
# Pull your team's flows
Source: https://docs.qawolf.com/qawolf/local-execution/pull-flows
Download an environment's flows into a local cache without running them.
For most workflows, [`qawolf flows run --env `](/qawolf/local-execution/run-flows-locally) is enough — it pulls the environment's flows for you when they are not already cached locally. Reach for `qawolf flows pull` when you want to download the flow files without running them: to inspect them, commit them to a repository, or refresh a stale cache.
Authenticate the CLI first. See [Authenticate the QA Wolf CLI](/qawolf/local-execution/authenticate).
## Pull an environment
Find the environment's ID in the QA Wolf app. Open **Workspace settings → Environments** and select the environment. Copy the `id` query parameter from the page URL: `https://app.qawolf.com//settings/environments?id=`. Use that value, not the environment's display name.
Pull the flows:
```bash theme={null}
qawolf flows pull --env
```
The CLI writes flows to `.qawolf//` by default. To use a different destination, pass `--out`:
```bash theme={null}
qawolf flows pull --env --out ./snapshot
```
List the pulled flows:
```bash theme={null}
qawolf flows list
```
## What `pull` writes
Inside `.qawolf//`, the CLI stores:
* the flow source files
* a manifest tracking what was pulled and when
* an `assets/` directory with the environment's file assets from team storage, wired up so flows resolve them locally
* a `.env` file with the environment's variables, loaded automatically by `qawolf flows run`
## Refresh a pulled environment
Run `qawolf flows pull --env ` again. The CLI prompts before overwriting any file that has been modified locally. To skip the prompt and overwrite, pass `--yes`:
```bash theme={null}
qawolf flows pull --env --yes
```
## List flows on the platform
To see what is available on the platform without pulling, use `--remote`:
```bash theme={null}
qawolf flows list --remote
qawolf flows list "checkout/**" --remote
```
## What's not pulled
Mobile app binaries are not downloaded. Mobile flows reference the APK or IPA build through an environment variable, so make sure that path points to a build available on the machine before running. Flows that read paths from runner-only `QAWOLF_*_DIR` environment variables also cannot resolve those paths locally; `qawolf doctor` flags both cases.
# Run flows locally
Source: https://docs.qawolf.com/qawolf/local-execution/run-flows-locally
Run your team's flows on your own machine — the CLI pulls, installs, and runs in one command.
`qawolf flows run --env ` is the recommended path. It runs your team's flows from the local `.qawolf//` cache, pulling them first only if they are not already cached locally, then installs the npm dependencies and Playwright browsers they need and runs them. Android flows require installing the Android tooling first with `qawolf install android`. See [Install dependencies](/qawolf/local-execution/install-dependencies).
Authenticate the CLI first. See [Authenticate the QA Wolf CLI](/qawolf/local-execution/authenticate).
## Run an environment
Find the environment's ID in the QA Wolf app. Open **Workspace settings → Environments** and select the environment. Copy the `id` query parameter from the page URL: `https://app.qawolf.com//settings/environments?id=`. Use that value, not the environment's display name.
From any directory, run:
```bash theme={null}
qawolf flows run --env
```
The CLI:
1. checks the local `.qawolf//` cache, and pulls the environment's flows only if they are not already cached
2. loads the environment's `.env` file
3. installs the npm dependencies and Playwright browsers the flows need
4. runs every flow
To refresh the local cache against the platform, run `qawolf flows pull --env `. See [Pull your team's flows](/qawolf/local-execution/pull-flows).
## Run a subset
Pass a glob pattern to limit which flows run:
```bash theme={null}
qawolf flows run --env "checkout/**"
qawolf flows run --env "src/flows/login.flow.ts"
```
With `--env`, patterns are matched against the pulled cache under `.qawolf//`. Without `--env`, patterns are matched against both the current directory and the pulled cache.
## Watch the browser
Pass `--headed` to see the browser window during a web run:
```bash theme={null}
qawolf flows run --env --headed
```
`--headed` does not apply to Android flows.
## Capture artifacts on failure
By default, no video or trace is recorded. To keep artifacts only when a flow fails, set the mode to `retain-on-failure`:
```bash theme={null}
qawolf flows run --env --video retain-on-failure --trace retain-on-failure
```
Artifacts land in `qawolf-output/` (or the directory set by `--output-dir`).
To record HAR files of network traffic:
```bash theme={null}
qawolf flows run --env --har retain-on-failure
```
By default the HAR captures headers and timing only. To include response bodies, pass `--har-content full`. Response bodies use significantly more memory and disk.
## Retry failing flows
```bash theme={null}
qawolf flows run --env --retries 2
```
The CLI retries each failing flow up to the given number of times. A flow that passes on retry is counted as a pass for exit-code purposes.
## Stop after the first failure
```bash theme={null}
qawolf flows run --env --bail
```
`--bail` is useful when iterating on a single flow and you want to fail fast.
## Run web flows in parallel
```bash theme={null}
qawolf flows run --env --workers 4
```
`--workers` controls how many web flows run concurrently. Android flows must run with `--workers 1` — the CLI errors out if Android flows are selected with a higher value.
## Write a JUnit XML report
```bash theme={null}
qawolf flows run --env --junit
```
The CLI writes a JUnit XML report to `qawolf-output/junit-report.xml` (or under the directory set by `--output-dir`). Pass an explicit path to override the default:
```bash theme={null}
qawolf flows run --env --junit ./reports/results.xml
```
The XML report is written alongside the console output and is independent of `--json` and `--agent`.
## Run flows you authored locally
If you've scaffolded a [local-only project](/qawolf/local-execution/set-up-a-project) with `qawolf init`, run without `--env`:
```bash theme={null}
qawolf flows run
```
The CLI discovers flows matching `**/*.flow.{ts,js}` in the current directory and runs them against locally-installed runtime dependencies. See [Install dependencies](/qawolf/local-execution/install-dependencies) if you need to install browsers or Android tooling explicitly.
## Exit codes
`qawolf flows run` exits with `0` when every flow passes and `1` when one or more fail. See [Exit codes](/qawolf/libraries/cli/api-reference/index#exit-codes) for the full list.
## Current limitations
* Android flows must run with `--workers 1`. Parallel execution is supported for web flows only.
* iOS flows are not executed. The CLI skips them with a warning.
* Flows that target the legacy `"Basic"` platform pull successfully but cannot be executed by the CLI.
* Flows where `target` is a computed value rather than a string literal are skipped because the CLI cannot determine the platform ahead of time.
# Set up a local-only project
Source: https://docs.qawolf.com/qawolf/local-execution/set-up-a-project
Scaffold a project for hand-authoring flows that don't live on the QA Wolf platform.
`qawolf init` scaffolds a project for writing flow files by hand. Most users should [pull flows from QA Wolf](/qawolf/local-execution/pull-flows) instead — the platform is where flow creation, AI-powered test generation, and team collaboration live, and `qawolf flows pull` brings those flows into a local cache the CLI can run from. Reach for `qawolf init` only when you want to author flows locally without the platform.
## Scaffold a new project
Open a terminal in the directory you want to use as the project root:
```bash theme={null}
cd path/to/your/project
```
Run the init command:
```bash theme={null}
qawolf init
```
The CLI prompts before overwriting any existing files. To skip the prompts, pass `--yes`.
The command creates the following files:
* `qawolf.config.ts` — a project configuration file generated for future use; the CLI does not read it yet.
* `src/flows/example.flow.ts` — a minimal web flow you can edit or delete.
* `.qawolf/.gitignore` — ignores the contents of `.qawolf/`, such as flows pulled from the platform. Run artifacts in `qawolf-output/` are not covered.
If your directory has no `package.json`, the CLI creates one with `"type": "module"`, the `@qawolf/flows` dependency, and a `test:e2e` script that runs `qawolf flows run`. If a `package.json` already exists, the CLI only adds the `test:e2e` script.
## Write a flow
Flow files live anywhere in your project as long as they match the glob `**/*.flow.{ts,js}`. The example flow is a good starting point:
```typescript theme={null}
import { expect, flow } from "@qawolf/flows/web";
export default flow(
"Example",
{ launch: true, target: "Web - Chrome" },
async ({ page, test }) => {
await test("navigate to example.com", async () => {
await page.goto("https://example.com");
await expect(page).toHaveTitle(/Example/);
});
},
);
```
For the full flow authoring surface, see the [@qawolf/flows API reference](/qawolf/libraries/flows/api-reference/index).
## Verify the setup
```bash theme={null}
qawolf flows list
```
The command lists every flow the CLI can find in the project. If the example flow appears, the project is ready to run.
# Android devices
Source: https://docs.qawolf.com/qawolf/mobile/android-devices
QA Wolf runs your Android tests on ephemeral emulators that start from a clean state for every run.
QA Wolf runs your Android tests on ephemeral emulators. A fresh emulator is provisioned for each run and discarded when the run finishes, so every run starts from a clean, predictable state with no data or configuration carried over from a previous run.
Because emulators are created on demand, you do not reserve or configure devices yourself. Choose an Android target in your flow and QA Wolf allocates an emulator for it.
```typescript theme={null}
import { flow } from "@qawolf/flows/android";
export default flow(
"Open Android app",
{ target: "Android - Pixel 9 (Android 16)", launch: true },
async ({ driver, test }) => {
await test("app launches", async () => {
await driver.pause(1000);
});
},
);
```
See the [Android Reference](/qawolf/libraries/flows/api-reference/android) for the full list of targets and device controls.
# Allowlisted devices (iOS)
Source: https://docs.qawolf.com/qawolf/mobile/ios-allowlisted-devices
Test features that don't work on a resigned app — like push notifications and universal links — by signing your app for QA Wolf's devices.
QA Wolf installs unreleased iOS builds by resigning your app, which works for most tests. A few features cannot be tested on a resigned app, including:
* Push notifications
* Universal links (associated domains)
These features depend on capabilities that require a provisioning profile tied to your app's bundle ID. Because you own the bundle ID, only you can produce that profile — so for these tests you sign the app for QA Wolf's devices instead of letting QA Wolf resign it. Devices set up this way are called allowlisted devices.
Contact your QA Wolf representative to enable allowlisted devices for your team.
## Sign the app for QA Wolf's devices
QA Wolf shares a set of device UDIDs with you. Add these devices to the provisioning profile for your app's bundle ID.
Build and sign the app for those devices using that profile.
## Include QA Wolf instrumentation
When QA Wolf resigns an app, it injects an instrumentation library that lets your tests mock system behavior. When you sign the app yourself, include this library so mocking still works.
* **Fastlane users** — use the `inject_qawolf_instrumentation` action from the [QA Wolf Fastlane plugin](https://github.com/qawolf/fastlane-plugin-qawolf).
* **Non-Fastlane users** — use the injection [script in the plugin repository](https://github.com/qawolf/fastlane-plugin-qawolf/tree/main/scripts).
After injecting the instrumentation library, resign the IPA. Fastlane users can use the [Fastlane resign action](https://docs.fastlane.tools/actions/resign/); otherwise resign the IPA with whatever your pipeline already uses.
## Target allowlisted devices
Set `targetDevices` to `Allowlisted Device` in your workflow's target configuration so the run is scheduled on the devices you signed the app for. `deviceModel` and `iosVersion` depend on the devices QA Wolf allocated for your team.
```typescript theme={null}
{
platform: "ios",
schemaVersion: 1,
meta: {
deviceModel: "iPhone 15",
iosVersion: "26",
targetDevices: "Allowlisted Device",
},
}
```
## Disable resigning
Because you signed the app yourself, disable QA Wolf's resigning when the app launches by setting the `qawolf:disableResigning` capability:
```typescript theme={null}
import { flow } from "@qawolf/flows/ios";
export default flow(
"Test push notifications",
{
target: {
platform: "ios",
schemaVersion: 1,
meta: {
deviceModel: "iPhone 15",
iosVersion: "26",
targetDevices: "Allowlisted Device",
},
},
launch: {
app: { env: "IPA_BUILD_LOCATION" },
capabilities: {
"qawolf:disableResigning": true,
},
},
},
async ({ driver, test }) => {
await test("notification is received", async () => {
// your test steps
});
},
);
```
# iOS devices
Source: https://docs.qawolf.com/qawolf/mobile/ios-devices
QA Wolf runs your iOS tests on real devices, with allowlisted and private device options for cases the shared pool can't cover.
QA Wolf runs your iOS tests on real, physical devices. By default these devices are shared across customers, and QA Wolf cleans up data and configuration between runs so each run starts from a known state.
Choose an iOS target in your flow and QA Wolf allocates a device for it.
```typescript theme={null}
import { flow } from "@qawolf/flows/ios";
export default flow(
"Open iOS app",
{ target: "Latest iOS (iPhone)", launch: true },
async ({ driver, test }) => {
await test("app launches", async () => {
await driver.pause(1000);
});
},
);
```
To install unreleased builds on shared devices without a per-app provisioning profile, QA Wolf resigns your app during installation. Resigning covers most test cases, but two situations need a different setup:
* Some features cannot be tested on a resigned app — see [Allowlisted devices](/qawolf/mobile/ios-allowlisted-devices).
* Some tests need devices that are not shared with other customers — see [Private devices](/qawolf/mobile/ios-private-devices).
See the [iOS Reference](/qawolf/libraries/flows/api-reference/ios) for the full list of targets and device controls.
# Private devices (iOS)
Source: https://docs.qawolf.com/qawolf/mobile/ios-private-devices
Run iOS tests on devices dedicated to your team for cases that need full device control or persistence across runs.
By default, QA Wolf shares iOS devices across customers and cleans up data and configuration between runs. Some tests need full control over a device, or need data and configuration to persist between runs. For these cases, QA Wolf can dedicate devices to a single team. These are called private devices.
Example cases:
* MDM (Mobile Device Management) testing
* Tests that need data or configuration to persist across runs
Contact your QA Wolf representative to enable private devices for your team.
## Allowlisted vs private devices
| | Allowlisted devices | Private devices |
| ------------------ | ------------------------------------------ | ------------------------------------ |
| Device pool | Part of the shared device pool | Dedicated to your team |
| Use case | Features that don't work on a resigned app | Tests where a device can't be shared |
| State between runs | Cleaned up between every run | Can persist data and configuration |
Private devices can do anything [allowlisted devices](/qawolf/mobile/ios-allowlisted-devices) can. You can share the UDIDs of private devices and skip resigning when that is needed for your tests.
## Target private devices
Set `targetDevices` to `Team Dedicated Device` in your workflow's target configuration so the run is scheduled on your team's devices. `deviceModel` and `iosVersion` depend on the devices QA Wolf allocated for your team.
```typescript theme={null}
{
platform: "ios",
schemaVersion: 1,
meta: {
deviceModel: "iPhone 15",
iosVersion: "26",
targetDevices: "Team Dedicated Device",
},
}
```
# Use your network drive
Source: https://docs.qawolf.com/qawolf/mount-a-file-share
Mount your CIFS/SMB file share onto the runner so your flow can read and write files during a test run.
This helper requires runner support. In QA Wolf-managed runs, the runner configures this automatically. If you're building a custom runner, see the [Client Reference](/qawolf/libraries/testkit/api-reference/client).
## Examples
**Mount your file share**
```typescript theme={null}
import { mountCifsShare } from "@qawolf/testkit";
const mountPoint = await mountCifsShare({
share: "//server/share",
mountPoint: "/mnt/share",
username: process.env["SHARE_USERNAME"]!,
password: process.env["SHARE_PASSWORD"]!,
});
```
**Read a file from your file share**
```typescript theme={null}
import { mountCifsShare } from "@qawolf/testkit";
import { readFileSync } from "fs";
const mountPoint = await mountCifsShare({
share: "//server/share",
mountPoint: "/mnt/share",
username: process.env["SHARE_USERNAME"]!,
password: process.env["SHARE_PASSWORD"]!,
});
const data = readFileSync(`${mountPoint}/test-data.csv`, "utf-8");
```
## When to use
* Your app reads or writes files that live on your network drive.
* Your flow needs test fixtures or input data from your network drive that can't be committed to a repository.
* Your app writes output files to your network drive and your flow needs to verify them.
* Your flow needs to upload files to your app that are sourced from your network drive.
## Full example
```typescript theme={null}
import { flow } from "@qawolf/flows/web";
import { mountCifsShare } from "@qawolf/testkit";
export default flow(
"Upload file from your file share",
{ target: "Web - Chrome", launch: true },
async ({ page, test }) => {
await test("mount file share", async () => {
const mountPoint = await mountCifsShare({
share: "//server/share",
mountPoint: "/mnt/share",
username: process.env["SHARE_USERNAME"]!,
password: process.env["SHARE_PASSWORD"]!,
});
await page.goto(process.env["BASE_URL"]!);
await page.setInputFiles('[type="file"]', `${mountPoint}/upload.csv`);
});
await test("verify upload", async () => {
await expect(page.getByText("Upload complete")).toBeVisible();
});
},
);
```
# Connect to a VPN (iOS)
Source: https://docs.qawolf.com/qawolf/network-connectivity
Route iOS test device traffic through VPN tunnels to reach internal services, staging environments, or geo-restricted content.
By default, QA Wolf tests run on our cloud infrastructure, which means your app's network requests originate from our servers — not your corporate network. If your app needs to reach internal services (staging environments, internal APIs, services behind a firewall), you'll need to route your test device's traffic through a tunnel.
`device.routeTraffic()` sets up a per-app VPN on the test device that intercepts and redirects network traffic before your test runs. You can target specific apps by bundle ID, or limit routing to specific domains when testing with Safari.
Call `device.routeTraffic()` before starting your WebDriver session so the tunnel is active from the first network request.
## Examples
**Route a native app's traffic** (all traffic from the app is intercepted):
```js theme={null}
// the app property makes sure all traffic
// from app with bundle id `com.example.myapp` will be rerouted
import { device } from "@qawolf/flows/ios";
const cleanup = await device.routeTraffic({
apps: ["com.example.myapp"],
tunnel: ...,
});
// ... run tests ...
await cleanup();
```
**Route Safari traffic for specific domains only:**
```js theme={null}
// Only traffic to *.example.com and api.staging.io
// goes through the tunnel. All other Safari traffic goes direct.
import { device } from "@qawolf/flows/ios";
const cleanup = await device.routeTraffic({
apps: [],
domains: ["*.example.com", "api.staging.io"],
tunnel: ...,
});
await cleanup();
```
**Route multiple apps simultaneously:**
```js theme={null}
import { device } from "@qawolf/flows/ios";
const cleanup = await device.routeTraffic({
apps: ["com.example.myapp", "com.example.companion"],
tunnel: ...,
});
// Both apps' traffic is routed through the VPN
await cleanup();
```
## When to use
* Your app connects to internal or staging services that aren't publicly accessible.
* You need to test geo-restricted content by routing through a VPN endpoint in a specific region.
* You want to verify how your app behaves when traffic passes through a proxy (authentication, filtering, latency).
* You need to test Safari against specific internal domains without affecting other browsing.
## Tunnel types
Choose a tunnel type based on how your network is set up.
### WireGuard
Use if your team already has a WireGuard VPN or you need to simulate traffic from a specific geographic region.
```js theme={null}
import { device } from "@qawolf/flows/ios";
const cleanup = await device.routeTraffic({
apps: ["com.apple.mobilesafari"],
tunnel: {
type: "wireguard",
configPath: "/configs/us-west.conf"
},
});
// ... test geo-restricted content ...
await cleanup();
```
### OpenVPN
Use if your organization uses a standard VPN setup with `.ovpn` config files.
```js theme={null}
import { device } from "@qawolf/flows/ios";
const cleanup = await device.routeTraffic({
apps: ["com.example.app"],
tunnel: {
type: "openvpn",
configPath: "/configs/client.ovpn"
},
});
// ... test against staging servers behind VPN ...
await cleanup();
```
### HTTP Proxy
Use this if your organization provides an HTTP/HTTPS proxy endpoint for accessing internal services, or if you're using a third-party geolocation proxy service.
```js theme={null}
import { device } from "@qawolf/flows/ios";
const cleanup = await device.routeTraffic({
apps: ["com.apple.mobilesafari"],
tunnel: {
type: "http-proxy",
host: "proxy.corp.example.com",
port: 8080,
username: "testuser",
password: "testpass",
},
});
// ... run tests behind corporate proxy ...
await cleanup();
```
### GOST Relay (3rd-party VPN alternative)
Use this if your organization uses a third-party VPN that can't be configured as WireGuard or OpenVPN on our side. A QA Wolf team member will set up a relay VM that runs your VPN client and forwards traffic to your test device.
```js theme={null}
import { device } from "@qawolf/flows/ios";
const cleanup = await device.routeTraffic({
apps: ["com.example.app"],
tunnel: {
type: "relay",
host: "relay.corp.example.com",
port: 443,
username: "user",
password: "pass",
},
});
// ... test with traffic routed through remote relay ...
await cleanup();
```
## Full sample test (http-proxy)
```js theme={null}
import { flow, device } from "@qawolf/flows/ios";
export default flow(
"iOS - Residential Proxy With Safari",
{ target: "iOS - iPhone 15 (iOS 26)", launch: { browserName: "safari" } },
async ({ driver, test }) => {
await test("open google", async () => {
// Discover your IP address
await driver.url("https://ip.me");
await driver.pause(5000);
// Configure and start QA Wolf's tunnel client
const cleanup = await device.routeTraffic({
apps: [],
domains: ["ip.me", "qawolf.com"],
tunnel: {
type: "http-proxy",
username: process.env.PROXY_USER,
password: process.env.PROXY_PASSWORD,
host: process.env.PROXY_HOST,
port: 8006,
},
});
// Start the iOS Safari app
await driver.activateApp('com.apple.mobilesafari');
await driver.url("https://qawolf.com");
// Check your IP address again. It should have changed
await driver.url("https://ip.me");
});
},
);
```
# Network simulation (iOS)
Source: https://docs.qawolf.com/qawolf/network-simulation
Simulate degraded network conditions on iOS devices to test how your app handles slow connections, high latency, or no connectivity.
## Examples
**Using a predefined condition:**
```js theme={null}
import { device } from "@qawolf/flows/ios";
// Traffic must be routed through the gateway before applying
// network conditions — use direct mode if you don't need a VPN
const routeCleanup = await device.routeTraffic({
apps: ["com.example.app"],
tunnel: { type: "direct" },
});
const conditionCleanup = await device.simulateNetworkCondition(device.NETWORK_3G);
// ... verify the app handles slow network gracefully ...
await conditionCleanup();
await routeCleanup();
```
**Custom conditions:**
```js theme={null}
import { device } from "@qawolf/flows/ios";
const cleanup = await device.simulateNetworkCondition({
bandwidthKbps: 5000,
latencyMs: 600,
jitterMs: 50,
packetLossPercent: 1.0,
});
// ... verify timeouts, retries, offline banners ...
await cleanup();
```
**Offline mode (no connectivity):**
```js theme={null}
import { device } from "@qawolf/flows/ios";
// Simulates offline mode by dropping all packets.
// Note: this may not trigger every app's offline UI —
// some apps only show offline states when the connection drops abruptly
const cleanup = await device.simulateNetworkCondition(device.NETWORK_OFFLINE);
// ... verify offline error handling, cached content ...
await cleanup();
```
## When to use
* Testing how your app behaves when loading is slow — spinners, timeouts, retry logic.
* Verifying your app shows the right error messages when connectivity is poor or lost.
* Testing video or audio streaming features that adapt quality based on available bandwidth (adaptive bitrate streaming).
* Reproducing issues customers have reported on slow or unreliable connections.
* Making sure your app degrades gracefully for users on 2G or 3G networks.
## Notes
* Before calling `device.simulateNetworkCondition()`, you must call `device.routeTraffic()`; otherwise, you will get an error.
* Always call cleanup functions in reverse order: restore network conditions first, then stop routing.
* Bandwidth limiting applies to TCP traffic only.
* Latency, jitter, and packet loss apply to all traffic (TCP and UDP).
* `NETWORK_OFFLINE` simulates no connectivity by dropping all packets. Requests will time out rather than fail immediately — this may behave differently from a true offline state on some devices.
## Full sample test
```js theme={null}
import { flow, device } from "@qawolf/flows/ios";
export default flow(
"iOS Network Simulation with fast.com",
{ target: "iOS - iPhone 15 (iOS 26)", launch: { browserName: "safari" } },
async ({ driver, test }) => {
await test("Install app", async () => {
// Start VPN
const cleanupProxy = await device.routeTraffic({
apps: [],
tunnel: {
type: "direct",
}});
let cleanUpNetworkCondition = await device.simulateNetworkCondition(device.NETWORK_5G)
console.log(await device.getNetworkCondition())
await driver.url("https://fast.com");
// switch to webview
const contexts = await driver.getContexts();
const webViewContext = contexts.reverse().find(context => context.toLowerCase().startsWith("webview_"));
await driver.switchContext(webViewContext);
// wait for the test result for 5G
await driver.$(`//*[@id='your-speed-message']`).waitForDisplayed({timeout: 30_000});
// clean-up and set 2G
await cleanUpNetworkCondition();
cleanUpNetworkCondition = await device.simulateNetworkCondition(device.NETWORK_2G_EDGE)
console.log(await device.getNetworkCondition())
await driver.url("https://fast.com");
// wait for the test result for 2G
await driver.$(`//*[@id='your-speed-message']`).waitForDisplayed({timeout: 30_000});
});
},
);
```
# Non-deterministic AI assertions
Source: https://docs.qawolf.com/qawolf/non-deterministic-ai-testing
Validate AI-generated responses using a judge model and assert on a structured JSON verdict.
Add `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` to your environment variables before using these helpers. `askClaudeTextValidation` and `askChatGPTTextValidation` share the same signature and return shape, and can be swapped with no other changes to your test.
## Examples
**Assert that an AI response is valid and within the token budget**
```typescript theme={null}
const { validation, usage } = await askClaudeTextValidation(
"You are a strict but fair evaluator. Focus on accuracy and completeness.",
originalPrompt,
aiResponseText,
);
// Hard gate — judge model must deem the response valid
expect(
validation.isValidForPrompt,
`Response invalid.\nMissing: ${JSON.stringify(validation.issues.missingRequirements)}\n` +
`Incorrect: ${JSON.stringify(validation.issues.incorrectInformation)}\n` +
`Explanation: ${validation.explanation}`,
).toBe(true);
// Soft gate — tune threshold per client
expect(
validation.score,
`Score ${validation.score} below threshold.\nExplanation: ${validation.explanation}`,
).toBeGreaterThanOrEqual(0.8);
// No contradictions
expect(
validation.issues.incorrectInformation.length,
`Incorrect info flagged: ${JSON.stringify(validation.issues.incorrectInformation)}`,
).toBe(0);
// Token budget — tune per client and model
expect(usage.input_tokens).toBeLessThanOrEqual(2000);
expect(usage.output_tokens).toBeLessThanOrEqual(500);
```
**Use ChatGPT as the judge model instead**
```typescript theme={null}
// Drop-in replacement — identical signature and return shape
const { validation, usage } = await askChatGPTTextValidation(
"You are a strict but fair evaluator. Focus on accuracy and completeness.",
originalPrompt,
aiResponseText,
);
```
## When to use
* Your app surfaces AI-generated content (summaries, prep notes, chat responses) that must be checked for accuracy.
* Your app's AI feature must not introduce contradictions or hallucinations relative to source material.
* Your app has a quality bar for AI output that a simple string match cannot enforce.
* Your app sends AI prompts that could drift in cost and you need to assert token budgets.
* Your app uses different AI providers and you want a consistent validation interface across both.
## Helpers
### `askClaudeTextValidation`
Sends the original prompt and candidate response to Claude and returns a structured verdict.
Conforms to the [Anthropic Messages API](https://docs.anthropic.com/en/api/messages).
````typescript theme={null}
async function askClaudeTextValidation(
systemPrompt: string,
originalPrompt: string,
aiResponseText: string,
) {
const SCHEMA_INSTRUCTIONS = `
You MUST respond with ONLY a valid JSON object — no markdown, no explanation, no backticks.
{
"isValidForPrompt": boolean,
"score": number (0.00–1.00),
"issues": {
"missingRequirements": [string],
"incorrectInformation": [string],
"offTopicContent": [string],
"formattingProblems": [string],
"safetyOrPolicyConcerns": [string]
},
"explanation": string
}`;
const resp = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"x-api-key": process.env.ANTHROPIC_API_KEY,
"anthropic-version": "2023-06-01",
"content-type": "application/json",
},
body: JSON.stringify({
model: "claude-sonnet-4-6",
max_tokens: 1024,
system: systemPrompt + "\n\n" + SCHEMA_INSTRUCTIONS,
messages: [{
role: "user",
content:
"PROMPT:\n" + originalPrompt +
"\n\nCANDIDATE ANSWER:\n" + aiResponseText +
"\n\nReturn the JSON verdict.",
}],
}),
});
if (!resp.ok) throw new Error(`Anthropic error ${resp.status}: ${await resp.text()}`);
const data = await resp.json();
const rawText = data.content?.find((b: any) => b.type === "text")?.text ?? "";
const validation = JSON.parse(rawText.replace(/```json|```/g, "").trim());
const usage = data.usage; // { input_tokens, output_tokens }
return { validation, usage };
}
````
### `askChatGPTTextValidation`
Same signature and return shape as `askClaudeTextValidation`. Uses the OpenAI Responses API with structured JSON output.
Conforms to the [OpenAI Responses API](https://platform.openai.com/docs/api-reference/responses/create).
NPM needed: `openai@latest`
```typescript theme={null}
import OpenAI from "openai";
async function askChatGPTTextValidation(
systemPrompt: string,
originalPrompt: string,
aiResponseText: string,
) {
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const resp = await client.responses.create({
model: "gpt-4.1",
instructions: systemPrompt,
input: [{
role: "user",
content:
"PROMPT:\n" + originalPrompt +
"\n\nCANDIDATE ANSWER:\n" + aiResponseText +
"\n\nReturn the JSON verdict.",
}],
text: {
format: {
type: "json_schema",
name: "TextPromptValidation",
strict: true,
schema: {
type: "object",
additionalProperties: false,
required: ["isValidForPrompt", "score", "issues", "explanation"],
properties: {
isValidForPrompt: { type: "boolean" },
score: { type: "number", minimum: 0, maximum: 1, multipleOf: 0.01 },
issues: {
type: "object",
additionalProperties: false,
required: ["missingRequirements", "incorrectInformation", "offTopicContent", "formattingProblems", "safetyOrPolicyConcerns"],
properties: {
missingRequirements: { type: "array", items: { type: "string" } },
incorrectInformation: { type: "array", items: { type: "string" } },
offTopicContent: { type: "array", items: { type: "string" } },
formattingProblems: { type: "array", items: { type: "string" } },
safetyOrPolicyConcerns: { type: "array", items: { type: "string" } },
},
},
explanation: { type: "string" },
},
},
},
},
});
const validation = JSON.parse(resp.output_text);
const usage = {
input_tokens: resp.usage?.input_tokens ?? 0,
output_tokens: resp.usage?.output_tokens ?? 0,
};
return { validation, usage };
}
```
***
## Return shape
Both helpers return `{ validation, usage }` with the same structure.
| Field | Type | Description |
| ----------------------------- | ---------- | ----------------------------------------------------------- |
| `validation.isValidForPrompt` | `boolean` | Hard gate — did the response satisfy the prompt? |
| `validation.score` | `number` | Quality score 0–1. Assert `>= 0.8` as a starting threshold. |
| `validation.issues.*` | `string[]` | Arrays of flagged issues by category. |
| `validation.explanation` | `string` | Step-by-step reasoning from the judge model. |
| `usage.input_tokens` | `number` | Tokens consumed by the prompt. |
| `usage.output_tokens` | `number` | Tokens consumed by the response. |
***
## Full sample test
```typescript theme={null}
import { flow, expect } from "@qawolf/flows/web";
import { llms } from "./helpers/llm-helper.t";
export default flow(
"AI response is valid, scored, and within token budget",
{ target: "Web - Chrome", launch: true },
async ({ page }) => {
//--------------------------------
// Arrange
//--------------------------------
// The prompt sent to the AI feature under test
const originalPrompt =
"Summarize the key action items from the meeting transcript below " +
"in a bulleted list. Be concise and accurate.\n\n" +
"Transcript:\n" +
"Alice: We need to ship the new onboarding flow by Friday.\n" +
"Bob: I'll own the front-end changes.\n" +
"Alice: Great. Carol, can you handle QA?\n" +
"Carol: Yes, I'll have test cases ready by Thursday.\n" +
"Alice: Perfect. Also, let's schedule a retro for next Monday at 10am.";
const {
askChatGPTTextValidation,
askClaudeTextValidation
} = await llms();
//--------------------------------
// Act
//--------------------------------
await page.getByRole("link", { name: "AI Assistant" }).click();
await page.getByRole("textbox", { name: "Ask anything" }).fill(originalPrompt);
const [response] = await Promise.all([
page.waitForResponse(
(res) =>
res.url().includes("/api/chat") &&
res.request().method() === "POST",
{ timeout: 60_000 },
),
page.getByRole("button", { name: "Send" }).click(),
]);
expect(response.status()).toBe(200);
const aiResponseContainer = page.locator("[data-testid='ai-response']:last-of-type");
await expect(aiResponseContainer).toBeVisible({ timeout: 15_000 });
const aiResponseText = await aiResponseContainer.innerText();
//--------------------------------
// Assert
//--------------------------------
const { validation, usage } = await askClaudeTextValidation(
"You are a strict but fair evaluator of AI-generated meeting summaries. " +
"Focus on whether the response captures the correct action items, owners, and deadlines.",
originalPrompt,
aiResponseText,
);
// Structure
expect(typeof validation.isValidForPrompt).toBe("boolean");
expect(typeof validation.score).toBe("number");
expect(Array.isArray(validation.issues.missingRequirements)).toBe(true);
expect(Array.isArray(validation.issues.incorrectInformation)).toBe(true);
expect(Array.isArray(validation.issues.offTopicContent)).toBe(true);
expect(Array.isArray(validation.issues.formattingProblems)).toBe(true);
expect(Array.isArray(validation.issues.safetyOrPolicyConcerns)).toBe(true);
expect(typeof validation.explanation).toBe("string");
// Hard gate
expect(
validation.isValidForPrompt,
`Response invalid.\n` +
`Missing: ${JSON.stringify(validation.issues.missingRequirements)}\n` +
`Incorrect: ${JSON.stringify(validation.issues.incorrectInformation)}\n` +
`Off-topic: ${JSON.stringify(validation.issues.offTopicContent)}\n` +
`Explanation: ${validation.explanation}`,
).toBe(true);
// Soft gate
expect(
validation.score,
`Score ${validation.score} below threshold.\nExplanation: ${validation.explanation}`,
).toBeGreaterThanOrEqual(0.8);
// No contradictions
expect(
validation.issues.incorrectInformation.length,
`Incorrect info: ${JSON.stringify(validation.issues.incorrectInformation)}`,
).toBe(0);
expect(
validation.issues.offTopicContent.length,
`Off-topic content: ${JSON.stringify(validation.issues.offTopicContent)}`,
).toBe(0);
// Token budget
expect(
usage.input_tokens,
`Input tokens ${usage.input_tokens} exceeded budget of 2000`,
).toBeLessThanOrEqual(2000);
expect(
usage.output_tokens,
`Output tokens ${usage.output_tokens} exceeded budget of 500`,
).toBeLessThanOrEqual(500);
},
);
```
# How to integrate with our SDK
Source: https://docs.qawolf.com/qawolf/other-ci-node
Trigger QA Wolf test runs from any CI system that supports Node.js.
Use this guide if your CI system supports Node.js but does not have a dedicated QA Wolf integration page.
If your CI system does not support Node.js, use [Other CI (webhook)](/qawolf/other-ci-webhook) instead.
Make sure you have:
* A CI pipeline that runs after a successful deployment.
* Node.js 18 or later available in your CI environment.
* Admin access to your CI system's secret or environment variable storage.
* At least one QA Wolf environment already configured.
* A QA Wolf API key.
## Find the QAWOLF\_API\_KEY
Open the **Workspace name** dropdown in QA Wolf and click **Workspace Settings**.
Choose **Integrations** and click the icon to the right of **API Key** under **API Access**.
Store the key as a secret in your CI system named `QAWOLF_API_KEY`.
## Add the notify script to your repository
Create a file at `.ci/notifyQaWolf.mjs` in the repository that corresponds to the deployments QA Wolf will be testing.
```javascript theme={null}
import assert from "assert";
import { makeQaWolfSdk } from "@qawolf/ci-sdk";
const apiKey = process.env.QAWOLF_API_KEY;
assert(apiKey, "QAWOLF_API_KEY is required");
const sha = process.env.GIT_COMMIT_SHA;
assert(sha, "GIT_COMMIT_SHA is required");
const branch = process.env.GIT_BRANCH;
assert(branch, "GIT_BRANCH is required");
const { attemptNotifyDeploy } = makeQaWolfSdk({ apiKey });
const result = await attemptNotifyDeploy({
branch,
sha,
deploymentType: "staging",
hostingService: "GitHub", // or "GitLab"
repository: {
name: "your-repo-name",
owner: "your-org-name",
},
});
if (result.outcome !== "success") {
throw new Error(`Failed to notify QA Wolf: ${JSON.stringify(result)}`);
}
```
Replace `GIT_COMMIT_SHA` and `GIT_BRANCH` with the environment variable names your CI system provides for the current commit SHA and branch. Replace `deploymentType` with the value your QA Wolf representative provides. Set `hostingService` to where your code is hosted — `"GitHub"` or `"GitLab"` — not where your pipeline runs.
## Run the notify script in your pipeline
Add a step to your CI pipeline that runs after your deployment is healthy:
```bash theme={null}
npm install @qawolf/ci-sdk
node .ci/notifyQaWolf.mjs
```
## Verify the integration
Trigger your CI pipeline with a new deployment.
Confirm the notify step completes without errors.
Open QA Wolf and confirm a new run appears under the expected environment.
## Related
* [`@qawolf/ci-sdk`](/qawolf/libraries/ci-sdk/api-reference)
# How to integrate with webhooks
Source: https://docs.qawolf.com/qawolf/other-ci-webhook
Trigger QA Wolf test runs from any CI system using the deploy webhook directly.
Use this guide if your CI system cannot run Node.js. This includes environments such as ArgoCD, locked-down runners, and minimal containers.
If your CI system supports Node.js, use [Other CI (Node)](/qawolf/other-ci-node) instead.
Make sure you have:
* A CI pipeline that runs after a successful deployment.
* The ability to make HTTP requests from your CI environment (e.g. `curl`).
* Admin access to your CI system's secret or environment variable storage.
* At least one QA Wolf environment already configured.
* A QA Wolf API key.
## Find the QAWOLF\_API\_KEY
Open the **Workspace name** dropdown in QA Wolf and click **Workspace Settings**.
Choose **Integrations** and click the icon to the right of **API Key** under **API Access**.
Store the key as a secret in your CI system named `QAWOLF_API_KEY`.
## Add the deploy notification to your pipeline
Add a step to your CI pipeline that runs after your deployment is healthy:
```bash theme={null}
curl -X POST https://app.qawolf.com/api/webhooks/deploy_success \
-H "Authorization: Bearer $QAWOLF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"branch": "$GIT_BRANCH",
"sha": "$GIT_COMMIT_SHA",
"deployment_type": "staging",
"hosting_service": "GitHub"
}'
```
Replace `$GIT_BRANCH` and `$GIT_COMMIT_SHA` with the environment variable names your CI system provides for the current branch and commit SHA. Replace `deployment_type` with the value your QA Wolf representative provides. Set `hosting_service` to where your code is hosted — `"GitHub"` or `"GitLab"` — not where your pipeline runs.
A 200 response does not guarantee a run was created. Inspect the response body to confirm. See [webhooks/deploy\_success](/qawolf/deploy-success) for full response details.
## Verify the integration
Trigger your CI pipeline with a new deployment.
Confirm the notify step completes without errors.
Open QA Wolf and confirm a new run appears under the expected environment.
## Related
* [webhooks/deploy\_success](/qawolf/deploy-success)
* [REST API](/qawolf/rest-overview)
# Integrate a mobile build with QA Wolf's REST API
Source: https://docs.qawolf.com/qawolf/other-web-hook-for-mobile
Upload mobile build artifacts and trigger test runs from any CI system without Node.js.
Use this guide if your CI system cannot run Node.js. This includes environments such as ArgoCD, locked-down runners, and minimal containers.
Make sure you have:
* A CI pipeline that produces a mobile build artifact (APK, AAB, or IPA).
* The ability to make HTTP requests from your CI environment (e.g. `curl`).
* Admin access to your CI system's secret or environment variable storage.
* A QA Wolf API key.
Before mobile test runs can execute, QA Wolf must enable mobile triggers for your workspace. QA Wolf will handle this and may ask you for:
* Which environments you want to test.
* Whether PR testing is enabled.
* The artifact naming conventions you are using.
Until this step is complete, CI jobs can upload artifacts and send deployment notifications, but mobile test runs will not start automatically.
## Find the QAWOLF\_API\_KEY
Open the **Workspace name** dropdown in QA Wolf and click **Workspace Settings**.
Choose **Integrations**.
Generate your **QAWOLF\_API\_KEY** by clicking the icon to the right of **API Key** under **API Access**.
Store the key as a secret in your CI system named `QAWOLF_API_KEY`.
## Artifact naming conventions
Mobile build artifacts must follow consistent naming conventions so QA Wolf can correctly associate each build with the right environment.
### Static environments
**Format**
```text theme={null}
-
```
**Example**
```text theme={null}
app-staging
```
### PR (ephemeral) environments
**Format**
```text theme={null}
---pr
```
**Example**
```text theme={null}
app-myorg-myrepo-pr123
```
QA Wolf applies the file extension (.apk, .aab, or .ipa) automatically based on the uploaded artifact. You only need to provide the basename.
## Upload the build artifact
### Generate a signed URL
```bash theme={null}
curl "https://app.qawolf.com/api/v0/run-inputs-executables-signed-urls?file=app-staging" \
-H "Authorization: Bearer $QAWOLF_API_KEY"
```
On success, you'll receive:
```json theme={null}
{
"fileLocation": "$TEAM_ID/app-staging",
"playgroundFileLocation": "app-staging",
"signedUrl": "https://..."
}
```
Copy the `playgroundFileLocation` value — you'll use it in the notify step.
### Upload the file
```bash theme={null}
curl -X PUT \
--header "Content-Type: application/octet-stream" \
--data-binary @./path/to/build.apk \
"$SIGNED_URL"
```
## Trigger a test run
After uploading the artifact, notify QA Wolf that a new deployment is ready for testing. Pass the `playgroundFileLocation` from the upload step as `RUN_INPUT_PATH`:
```bash theme={null}
curl -X POST https://app.qawolf.com/api/webhooks/deploy_success \
-H "Authorization: Bearer $QAWOLF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"deployment_type": "staging",
"variables": {
"RUN_INPUT_PATH": ""
}
}'
```
Replace `deployment_type` with the value your QA Wolf representative provides. Replace `` with the value returned in the upload step.
A 200 response does not guarantee a run was created. Inspect the response body to confirm. See [webhooks/deploy\_success](/qawolf/deploy-success) for full response details.
If mobile triggers have not yet been enabled, this step will complete without starting a test run.
## Verify the integration
Run the CI job.
Verify that the artifact upload completes successfully.
Confirm that the deployment notification step runs without errors.
Once mobile triggers are enabled, check the **Runs** tab for the test run that was triggered.
## Troubleshooting and common issues
* **If uploads succeed but no runs start:** Mobile triggers may not yet be enabled. Contact QA Wolf to complete platform configuration.
* **If the artifact is not found during execution:** Verify that the artifact basename matches your naming conventions, and that `RUN_INPUT_PATH` is set to the `playgroundFileLocation` value from the upload step.
* **If you see authentication errors:** Verify that **QAWOLF\_API\_KEY** is configured correctly in your CI environment.
## Related
* [webhooks/deploy\_success](/qawolf/deploy-success)
* [v0/run-inputs-executables-signed-urls](/qawolf/run-inputs-executables-signed-urls)
* [Other CI (Node)](/qawolf/Mobile-build-with-the-QA-Wolf-SDK-2db5b2a994fb80cdbd1ec31513f2cc15)
# iOS photo library management
Source: https://docs.qawolf.com/qawolf/photo-library-management
Add, list, and delete photos in the device photo library to set up test state or verify your app saved photos.
## Examples
**Save, list, and delete photos:**
```javascript theme={null}
import { device } from "@qawolf/flows/ios";
// driver is provided by the flow callback when using:
// launch: { bundleId: "com.apple.mobileslideshow" }
// Save a photo to the library
const imagePath = "/home/wolf/files/large.jpg";
await device.savePhoto(driver, imagePath);
// Verify the photo was saved
const photosAfterSave = await device.listPhotos(driver);
expect(photosAfterSave.success).toBe(true);
expect(photosAfterSave.totalCount).toBe(1);
// Clean up
await device.deleteAllPhotos(driver);
const photosAfterDelete = await device.listPhotos(driver);
expect(photosAfterDelete.success).toBe(true);
expect(photosAfterDelete.totalCount).toBe(0);
```
## When to use
* Your app saves photos and you need to verify they appear in the library.
* Your app reads from the photo library and you need to set up known state before the test runs.
* You need to clean up photos between test runs to avoid state carrying over.
`deleteAllPhotos` permanently removes all photos from the device library. Use it only on test devices.
## Full sample test
```js theme={null}
import { flow, device, expect } from "@qawolf/flows/ios";
export default flow(
"iOS Media - Photo management in gallery",
{
target: "iOS - iPhone 15 (iOS 26)",
launch: { bundleId: "com.apple.mobileslideshow" },
},
async ({ driver, test }) => {
await test("Photo push and save and list and delete", async () => {
const baseDir = process.env.IMAGE_BASE_DIR;
const fileName = "large.jpg";
const imagePath = `${baseDir}/${fileName}`;
await device.savePhoto(driver, imagePath);
const photosAfterPush = await device.listPhotos(driver);
expect(photosAfterPush.success).toBe(true);
expect(photosAfterPush.totalCount).toBe(1);
await device.deleteAllPhotos(driver);
const photosAfterDelete = await device.listPhotos(driver);
expect(photosAfterDelete.success).toBe(true);
expect(photosAfterDelete.totalCount).toBe(0);
});
},
);
```
# Test QR code authentication
Source: https://docs.qawolf.com/qawolf/qrcode
Read a QR code from the page and use its value to complete authentication.
## Examples
**Read a QR code from the page**
```typescript theme={null}
import { readQRCode } from "@qawolf/testkit/web";
const qrValue = await readQRCode(page, "[data-testid='qr-code']");
if (!qrValue) throw new Error("QR code was not visible");
```
**Follow a QR code login link**
```typescript theme={null}
import { readQRCode } from "@qawolf/testkit/web";
const qrValue = await readQRCode(page, "[data-testid='qr-code']");
if (!qrValue) throw new Error("QR code was not visible");
await page.goto(qrValue);
```
## When to use
* Your app displays a QR code that a user scans to authenticate
* Your app uses QR codes to initiate a login session on a second device
* Your flow needs to extract a URL or token encoded in a QR code
* Your app generates a QR code as part of an MFA or SSO flow
## Full example
```typescript theme={null}
import { flow } from "@qawolf/flows/web";
import { readQRCode } from "@qawolf/testkit/web";
export default flow(
"Log in via QR code",
{ target: "Web - Chrome", launch: true },
async ({ page, test }) => {
await test("navigate to login", async () => {
await page.goto(process.env["BASE_URL"]!);
});
await test("read and follow QR code", async () => {
const qrValue = await readQRCode(page, "[data-testid='qr-code']");
if (!qrValue) throw new Error("QR code was not visible");
await page.goto(qrValue);
});
await test("verify authenticated", async () => {
await expect(page).toHaveURL(/dashboard/);
});
},
);
```
# Quick Start
Source: https://docs.qawolf.com/qawolf/quick-start
Connect your application, generate your first flow, and run your first tests with QA Wolf.
⏱ **Time to complete:** 10–15 minutes
## Before you begin
Before getting started, make sure the following are set up:
* **You have access to a QA Wolf workspace.** Create an account below or [Sign in](https://app.qawolf.com/sign-in) to your existing account.
* **QA Wolf can access the application you want to test.** If your application is behind a firewall or private network, a [**VPN or network allowlist** must be configured](/qawolf/QA-Wolf-s-static-IPv4-29c5b2a994fb81bf8487d085abf9dc4d) so QA Wolf infrastructure can reach it.
## Step 1: Outline your application
Give the outlining agent a target URL for your application (e.g., staging.wolfcorp.com) and tell it to outline all the workflows.
Read more: [Create a coverage outline →](/qawolf/Map-your-app-s-workflows-2a45b2a994fb80ca8a87c8fdc2a443fc)
## Step 2: Create a flow
Select a flow and ask the AI to write the test code.
Read more: [Create flows with AI →](/qawolf/Automate-flows-using-AI-2d35b2a994fb8084b524cfc9aca7c358)
## Step 3: Check in your new test code
Do a code review and check in your changes.
## Step 4: Run your test
Congratulations! You have a test suite.
## Next steps
Learn how workspaces, environments, flows, and runs are organized.
Let QA Wolf build and maintain your test suite for you.
Create flow files and folders, and write the test code by hand.
# Overview
Source: https://docs.qawolf.com/qawolf/rest-overview
The QA Wolf REST API provides direct HTTP access to core platform actions.
For most use cases, [`@qawolf/ci-sdk`](/qawolf/libraries/ci-sdk/api-reference) is recommended over direct endpoint access.
## Authentication
All endpoints require a `QAWOLF_API_KEY` passed as a Bearer token.
```bash theme={null}
Authorization: Bearer $QAWOLF_API_KEY
```
### Kinds of API key
| Kind | Acts as | Where to create it |
| ------------ | ------------------------------------------------------------------ | -------------------------------------------------- |
| Workspace | One workspace | **Workspace Settings → Integrations → API Access** |
| Organization | The whole organization, across every workspace in it | **Workspace Settings → API Keys** |
| User | The member who owns the key, across every workspace they can reach | **Workspace Settings → API Keys** |
To reach either settings page, open the **Workspace Name** dropdown in QA Wolf.
### Which workspace a request acts on
A workspace API key implies its workspace, so requests never name one. An organization or user API key does not, so a request that is scoped to a single workspace resolves it in this order:
1. The workspace named by a resource in the request, such as `environmentId`.
2. The workspace named by `workspaceId`.
3. Your organization's first-created workspace.
Creating another workspace never changes step 3, so a pipeline that relies on it keeps running. Even so, pass `workspaceId` when your organization has more than one workspace: it states which workspace you meant, and it keeps working if the first-created workspace is ever deleted or disabled. Copy the ID from **Workspace Settings → API Keys**.
## Base URL
```text theme={null}
https://app.qawolf.com
```
## Endpoints
| Endpoint | Method | Description |
| ------------------------------------------------------------------------------------------ | ------------- | ----------------------------------------------------------------- |
| [`/api/webhooks/deploy_success`](/qawolf/deploy-success) | `POST` | Notify QA Wolf of a successful deployment to trigger a run. |
| [`/api/webhooks/environment_terminated`](/qawolf/environment_terminated) | `POST` | Notify QA Wolf that an ephemeral environment has been terminated. |
| [`/api/v0/ci-greenlight/{root-run-id}`](/qawolf/v0-ci-greenlight) | `GET` | Poll for the outcome of a run to gate a pipeline. |
| [`/api/v0/run-inputs-executables-signed-urls`](/qawolf/run-inputs-executables-signed-urls) | `GET` + `PUT` | Generate a signed URL and upload an executable file. |
# Add skills the AI reuses across sessions
Source: https://docs.qawolf.com/qawolf/reusable-ai-knowledge
Package knowledge about your app in SKILL.md files that the AI loads in every session.
Prompts teach the AI about your app one session at a time. A skill makes that knowledge permanent. SKILL.md files are loaded in every autonomous creation, guided creation, and outlining session, so the AI starts with your context instead of asking you to repeat it.
## What a skill looks like
A skill is a Markdown file with two parts: frontmatter that names and describes it, and a body with the knowledge itself.
```markdown theme={null}
---
name: acme-conventions
description: Domain terms and test conventions for the Acme app
---
# Logging in
Always sign in through the SSO tile. The email and password form is
deprecated and fails for most test users.
# Domain terms
- A "quote" is a rate quote, not a chat message.
- "Members" and "subscribers" are the same object in the UI.
# Conventions
- Structure every flow header around Arrange, Act, Assert.
- Prefer unique generated values over hardcoded ones for new records.
```
## Where skills live
A skill can live anywhere in your workspace. The best practice is to give each SKILL.md its own folder:
```text theme={null}
your-workspace/
├── skills/
│ ├── acme-conventions/
│ │ └── SKILL.md
│ └── payments-testing/
│ └── SKILL.md
├── src/
│ ├── flows/
│ └── pages/
├── package.json
└── tsconfig.json
```
## What belongs in a skill
Good skill content is anything you would otherwise repeat across many prompts:
* Domain vocabulary and what each term maps to in the UI
* Environment quirks, such as which login method works and which test users are seeded
* Team conventions, such as [structuring flow headers around AAA](/qawolf/getting-the-most-out-of-automate#structure-the-flow-header-around-arrange-act-assert)
* Links to deeper documentation the AI should fetch when it becomes relevant
Keep each skill focused on one topic. Knowledge that matters to a single flow belongs in that flow's prompt or header, not in a skill.
## How each surface uses skills
**Autonomous creation.** The quality of an unattended run depends on the context you front-load. A skill carries the standing context, so your prompt only needs to describe the flow itself.
**Guided creation.** You stop re-explaining the same background at every step. The AI already knows your domain terms and conventions, so short prompts land correctly.
**Outlining.** The agent explores your app already knowing what the features are called and which areas matter, which produces more accurate flow stubs.
# Reuse state between runs
Source: https://docs.qawolf.com/qawolf/reuse-state-between-runs
Use environment variables to carry state from one run into the next, so flows can skip work they've already done.
## Examples
**Save a value for later runs**
```typescript theme={null}
import { saveEnvironmentVariable } from "@qawolf/testkit";
await saveEnvironmentVariable("SESSION_TOKEN", token);
```
**Read a value saved by a previous run**
```typescript theme={null}
import { reloadEnvironmentVariables } from "@qawolf/testkit";
await reloadEnvironmentVariables();
const sessionToken = process.env["SESSION_TOKEN"];
```
## When to use
* Your app requires a login or setup step that produces a value other flows need in future runs.
* A flow generates a token, ID, or credential that should persist across runs.
* You want to skip repeated setup work by reusing state from a previous run.
* Your test environment requires a value that changes at runtime and needs to be available to later runs.
## Full example
```typescript theme={null}
import { flow } from "@qawolf/flows/web";
import { reloadEnvironmentVariables, saveEnvironmentVariable } from "@qawolf/testkit";
export default flow(
"Login and save session token",
{ target: "Web - Chrome", launch: true },
async ({ page, test }) => {
await reloadEnvironmentVariables();
if (!process.env["SESSION_TOKEN"]) {
await test("log in and save session token", async () => {
await page.goto(process.env["BASE_URL"]!);
await page.fill('[name="email"]', process.env["EMAIL"]!);
await page.fill('[name="password"]', process.env["PASSWORD"]!);
await page.click('[type="submit"]');
const token = await page.evaluate(() =>
localStorage.getItem("session_token")
);
await saveEnvironmentVariable("SESSION_TOKEN", token!);
});
}
},
);
```
# v0/run-inputs-executables-signed-urls
Source: https://docs.qawolf.com/qawolf/run-inputs-executables-signed-urls
Generate a signed URL and upload an executable file to QA Wolf.
`GET https://app.qawolf.com/api/v0/run-inputs-executables-signed-urls`
Only the following file types are accepted: `.apk`, `.aab`, `.deb`, `.ipa`, `.zip`, `.csv`, `.pdf`
## Generate a signed URL
```bash theme={null}
curl "https://app.qawolf.com/api/v0/run-inputs-executables-signed-urls?file=$DESTINATION_FILE_PATH" \
-H "Authorization: Bearer $QAWOLF_API_KEY"
```
### Query parameters
| Parameter | Description |
| --------- | -------------------------------------------------------------------------------------- |
| `file` | Destination file path. At minimum the filename and extension. May include directories. |
### Response
```json theme={null}
{
"fileLocation": "$TEAM_ID/$DESTINATION_FILE_PATH",
"playgroundFileLocation": "$DESTINATION_FILE_PATH",
"signedUrl": "https://..."
}
```
| Field | Description |
| ------------------------ | ---------------------------------------------------------------------------------- |
| `fileLocation` | Full path including team ID. Use this to reference the file in run configurations. |
| `playgroundFileLocation` | Path without team ID. Use this to reference the file in the playground. |
| `signedUrl` | Pre-signed URL for uploading the file. Use in the next step. |
## Upload the file
`PUT {signedUrl}`
The signed URL is returned by the previous request. It is not a fixed `/api/` endpoint — it points directly to Google Cloud Storage.
```bash theme={null}
curl -X PUT \
--header "Content-Type: application/octet-stream" \
--data-binary @some_file.zip \
$SIGNED_URL
```
## Response codes
| Code | Description |
| ----- | ------------------------------------------------------------- |
| `200` | Success. |
| `401` | Missing or invalid API key. |
| `403` | Forbidden. Usually indicates a disabled team. |
| `500` | Internal server error. Contact support if the issue persists. |
## Related
* [Upload files](/qawolf/Uploading-manually)
* [`@qawolf/ci-sdk`](/qawolf/libraries/ci-sdk/api-reference)
* [Notify deploy](/qawolf/deploy-success)
# Share logic across flows
Source: https://docs.qawolf.com/qawolf/sharing-code-across-flows
Use helper functions and platform-specific entry points to keep flows linear and share logic across platforms.
## Helpers for platform differences
For platform-specific implementation differences that don't fit neatly into a Page Object — such as log capture, where iOS uses `"safariConsole"` and parses JSON while Android uses `"browser"` with logging preferences — write a helper function that handles the difference internally. The flow calls the helper and stays linear.
```typescript theme={null}
// /src/helpers/console-logs-ios.ts
export async function getConsoleLogs(driver: WebdriverIO.Browser) {
const raw = await driver.getLogs("safariConsole");
return raw.map((entry) => JSON.parse(entry.message));
}
//helpers/console-logs-android.ts
export async function getConsoleLogs(driver: WebdriverIO.Browser) {
const raw = await driver.getLogs("browser");
return raw.map((entry) => entry.message);
}
```
Module-level code should be limited to imports, constants, and pure helper functions. Runtime APIs like `launch()`, `expect`, `device`, and `platform.target` only work inside the flow callback — calling them at the module level will throw.
## Use platform-specific entry points
Rather than writing one flow that handles multiple platforms, write separate flows using the entry point for each platform. The `device` API surface is consistent across platforms, so the flows look similar even though they run on different infrastructure.
```typescript theme={null}
// /src/flows/set-location-android.ts
import { device, flow } from "@qawolf/flows/android";
export default flow(
"Set location",
{ target: "Android - Pixel 9", launch: true },
async ({ driver, test }) => {
await test("set device location", async () => {
await device.setGeoLocation({ latitude: 37.7749, longitude: -122.4194 });
});
},
);
```
```typescript theme={null}
// /src/flows/set-location-ios.ts
import { device, flow } from "@qawolf/flows/ios";
export default flow(
"Set location",
{ target: "iOS - iPhone 15 (iOS 26)", launch: true },
async ({ driver, test }) => {
await test("set device location", async () => {
await device.setGeoLocation({ latitude: 37.7749, longitude: -122.4194 });
});
},
);
```
## Use environment variables to parameterize flows
Environment variables let the same flow run against different environments without code changes. Reference them with `process.env.VAR_NAME`.
```typescript theme={null}
await page.goto(process.env.BASE_URL);
```
See [Environment setup](/qawolf/Environment-settings-2cd5b2a994fb8014ba8bd7aa25f3ed0f#create-and-manage-environment-variables) for how to configure environment variables in QA Wolf.
## Pass data between flows
Use `inputs` and `setOutput` to share data between flows in the same run — for example, passing a user ID created in one flow to another flow that needs to log in as that user. See [Passing data between flows](/qawolf/Pass-data-between-flows-29d5b2a994fb801080bce743ab552931) for the full model.
## `platform.target` for edge cases
For rare cases where a single flow genuinely needs to branch on the active platform at runtime, use `platform.target` from the top-level `@qawolf/flows` package.
```typescript theme={null}
import { platform } from "@qawolf/flows";
if (platform.target.startsWith("iOS - ")) {
// iOS-specific step
}
```
Use this sparingly. If you find yourself branching on `platform.target` frequently, consider splitting into platform-specific flows instead. See the [Top-Level Reference](/qawolf/libraries/flows/api-reference/top-level) for the full `platform` API.
# Solutions
Source: https://docs.qawolf.com/qawolf/solutions
Recipes and helpers for testing specific scenarios in your app.
Send, receive, and verify emails and SMS messages inside your test flows.
Use OTPs & QR codes to authenticate.
Test network conditions and connectivity.
Coordinate state and data across multiple users, sessions, and devices.
Inject audio and capture speaker output into mobile test flows.
Inject photos, camera feeds, barcodes, and video into mobile test flows.
Validate accessibility compliance for web and native apps.
Measure and assert on web performance metrics inside your flows.
Catch unintended visual regressions across web and mobile web.
Validate AI-generated responses and assert on quality, accuracy, and token usage.
Test Electron applications.
Mock geolocation and other mobile device sensors.
# Configure Single Sign-On (SSO)
Source: https://docs.qawolf.com/qawolf/sso
Set up SSO to authenticate with QA Wolf using your identity provider, such as Okta, OneLogin, or Entra ID.
QA Wolf supports SSO via SAML 2.0 and OpenID Connect, which covers most enterprise identity providers, including Okta, Azure AD, Google, OneLogin, JumpCloud, Auth0, ADFS, and Shibboleth.
## Set up SSO
The person configuring SSO must have the following:
### QA Wolf permissions
* **SSO Admin** role. Someone at your organization with the **Membership Manager** role will need to:
1. Go to **Workspace Settings → Organization**.
2. Grant the **SSO Admin** role to the user who is going to follow these instructions.
3. Sign out and back in after their role is updated.
### Identity provider permissions
The administrator must be able to:
* Verify your organization domain, such as by adding a TXT record.
* Create applications in the identity provider.
* Configure SAML or OIDC authentication settings.
* Assign users or groups to applications.
### Information needed for setup
Depending on your identity provider, you may need:
* Identity provider metadata URL
* Issuer URL
* X.509 signing certificate
* ACS URL
* Audience / Entity ID
* Redirect or callback URL
***
### Verify your email domain
You can only set up SSO for verified email domains. In other words, you need to prove you own the domain name.
As the **SSO Admin**:
1. Sign in to QA Wolf.
2. Navigate to **Workspace Settings → Organization**.
3. In the Domain Verification section, click **Add domain**.
Follow the instructions to add and verify as many domains as needed for your organization.
### Open the SSO settings page
As the **SSO Admin**:
1. Sign in to QA Wolf.
2. Navigate to **Workspace Settings → Organization**.
3. Click **Set up SSO**.
This page contains the configuration values required for your identity provider.
### Create an application in your identity provider
In your identity provider:
1. Create a **new application** for QA Wolf.
2. Select **SAML** or **OIDC** authentication.
3. Enter the configuration values provided in the QA Wolf SSO settings page.
Typical configuration fields include:
| Setting | Description |
| ----------------------- | ------------------------------------------------- |
| ACS URL | Endpoint that receives authentication responses |
| Audience / Entity ID | Identifier used to verify the QA Wolf application |
| Redirect / Callback URL | URL users return to after authentication |
***
### Enter identity provider details in QA Wolf
After creating the application in your identity provider, return to the QA Wolf SSO settings page and enter the details provided by your identity provider.
Typical configuration fields include:
| Setting | Description |
| ----------------- | ------------------------------------------------------------------ |
| Issuer URL | Unique identifier for your identity provider |
| Metadata URL | URL where QA Wolf retrieves your identity provider's configuration |
| X.509 Certificate | Certificate used to verify tokens from your identity provider |
| Login URL | Endpoint where QA Wolf sends authentication requests |
After saving, assign users or groups to the QA Wolf application in your identity provider.
***
### Test login
Before enabling access for your entire organization, test SSO with a single admin user.
Assign the admin user to the QA Wolf application in your identity provider.
Start a login attempt from the QA Wolf sign-in page.
Complete authentication in your identity provider.
Verify the user is redirected back to QA Wolf successfully.
Testing with one admin helps catch configuration issues before enabling access for additional users.
***
### Account matching
When a user signs in with SSO, QA Wolf matches the user to an existing account using their **email address**.
The email provided by your identity provider must **exactly match** the email associated with the user's QA Wolf account.
If the email addresses do not match, the user may not be able to access the workspace. Before enabling SSO broadly, confirm that user email addresses match between your identity provider and QA Wolf.
***
## Sign in with SSO
Users open the QA Wolf login page.
Based on the domain name of the email they enter, users automatically begin the SSO flow for their identity provider.
Users authenticate using their organization's identity provider.
After authentication, users are redirected back to QA Wolf.
Authentication is handled by the identity provider.
***
## Troubleshooting
### I do not see the SSO setup page
Confirm that:
* You have been granted the **SSO Admin** role by a user with the **Membership Manager** role.
* You signed out of QA Wolf and signed back in after your role was updated.
### A user cannot sign in
Check that:
* The user is assigned to the QA Wolf application.
* The user is signing in with the correct email address.
* The SSO configuration has been saved.
### Authentication fails
Verify that the following values match between QA Wolf and your identity provider:
* ACS URL
* Audience / entity ID
* Issuer
* Login URL
* Certificate
### Users are not redirected back to QA Wolf
Ensure the redirect or callback URL configured in your identity provider matches the value shown in the QA Wolf SSO settings page.
# Test MFA with an authenticator app
Source: https://docs.qawolf.com/qawolf/test-mfa
Generate a time-based OTP code in your flow to complete multi-factor authentication.
Store your authenticator app's OTP URI as a QA Wolf environment variable named `OTP_URI` before running this flow.
## Examples
**Complete MFA during login**
```typescript theme={null}
import { otp } from "@qawolf/testkit";
const code = otp.fromUri(process.env["OTP_URI"]!);
await page.getByLabel("Verification code").fill(code);
```
## When to use
* Your app requires a time-based one-time password (TOTP) to complete login
* Your app prompts for an MFA code after username and password are accepted
* Your flow needs to authenticate as a user who has MFA enabled
* Your test environment enforces MFA and cannot be bypassed for test accounts
## Full example
```typescript theme={null}
import { flow } from "@qawolf/flows/web";
import { otp } from "@qawolf/testkit";
export default flow(
"Log in with MFA",
{ target: "Web - Chrome", launch: true },
async ({ page, test }) => {
await test("log in", async () => {
await page.goto(process.env["BASE_URL"]!);
await page.fill('[name="email"]', process.env["EMAIL"]!);
await page.fill('[name="password"]', process.env["PASSWORD"]!);
await page.click('[type="submit"]');
});
await test("complete MFA", async () => {
const code = otp.fromUri(process.env["OTP_URI"]!);
await page.getByLabel("Verification code").fill(code);
await page.click('[type="submit"]');
});
},
);
```
# v0/ci-greenlight
Source: https://docs.qawolf.com/qawolf/v0-ci-greenlight
Returns the outcome of a run for use as a pipeline gate.
`GET https://app.qawolf.com/api/v0/ci-greenlight/{root-run-id}`
If your build server supports `node`, use [`@qawolf/ci-sdk`](/qawolf/libraries/ci-sdk/api-reference) instead of calling this endpoint directly.
## Request
```bash theme={null}
curl https://app.qawolf.com/api/v0/ci-greenlight/ \
-H "Authorization: Bearer $QAWOLF_API_KEY"
```
## Request headers
| Header | Value |
| --------------- | ----------------------- |
| `Authorization` | `Bearer ` |
## Query parameters
| Parameter | Values | Description |
| ----------------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `outcomeWhenBlockingBugsInOtherWorkflows` | `green` \| `red` | Defaults to `green`. When set to `red`, `greenlight` will be `false` if blocking bugs were found in workflows not executed in this run. Experimental. |
## Response
| Field | Description |
| ---------------------------------- | ------------------------------------------------------------------------------------ |
| `greenlight` | `true` when `runStage` is `"completed"` and zero blocking bugs were found. |
| `relevantRunId` | ID of the most recent superseding run, if one exists. Otherwise matches `rootRunId`. |
| `relevantRunUrl` | URL to the relevant run in QA Wolf. |
| `relevantRunWithBugsUrl` | URL to the relevant run filtered to workflows diagnosed as bugs. |
| `rootRunId` | ID of the run passed in the request. |
| `rootRunUrl` | URL to the root run in QA Wolf. |
| `runStage` | One of `"initializing"`, `"underReview"`, `"completed"`, `"canceled"`. |
| `workflowsDisabledAfterRunCount` | Number of workflows disabled after review. |
| `workflowsInRunCount` | Total number of workflows in this run. |
| `workflowsUnderInvestigationCount` | Number of failed workflows requiring investigation. |
### Conditional fields
Present when `runStage` is `"underReview"` or `"completed"`, except where noted.
| Field | Description |
| ---------------------------- | ----------------------------------------------------------------------------------------- |
| `blockingBugsCount` | Number of blocking bugs found after review. Present when `runStage` is `"completed"`. |
| `nonBlockingBugsCount` | Number of non-blocking bugs found after review. Present when `runStage` is `"completed"`. |
| `workflowsAutoRetryingCount` | Number of workflows still auto-retrying. Present when `runStage` is `"underReview"`. |
| `blockingBugUrls` | Array of URLs to blocking bugs. |
| `nonBlockingBugUrls` | Array of URLs to non-blocking bugs. |
| `reproducedBugs` | Array of detailed bug objects. |
## Interpreting `greenlight`
`greenlight` is `true` only when:
* `runStage` is `"completed"`, and
* Zero blocking bugs were found.
A bug is blocking if its priority is `"high"` or unset. Newly found bugs are unset by default and count as blocking. You can lower a bug's priority in the QA Wolf UI to make it non-blocking, then retry the greenlight poll.
## Run stages
| Stage | Description |
| -------------- | --------------------------------------------------------------------------- |
| `initializing` | Run is starting up. |
| `underReview` | Run is in progress and under review. |
| `completed` | Run has finished. |
| `canceled` | Run was canceled. Polling should stop — a canceled run cannot be recovered. |
## Superseding logic
If a newer run has superseded the root run (identified by matching `deduplication_key`), this endpoint returns the greenlight status for the most recent superseding run. When `relevantRunId` differs from `rootRunId`, you are seeing the superseding run's status.
When polling, use `relevantRunId` rather than `rootRunId` for faster queries.
## Response codes
| Code | Description |
| ----- | ------------------------------------------------------------- |
| `200` | Success. |
| `401` | Missing or invalid API key. |
| `403` | Forbidden. Usually indicates a disabled team. |
| `404` | Run not found. |
| `405` | Method not allowed. Use `GET`. |
| `410` | Gone. Returned for old or legacy runs. |
| `500` | Internal server error. Contact support if the issue persists. |
## Related
* [`@qawolf/ci-sdk`](/qawolf/libraries/ci-sdk/api-reference)
* [Notify deploy](/qawolf/deploy-success)
# Video injection
Source: https://docs.qawolf.com/qawolf/video-injection
Inject video into your app's camera input to test video features without real hardware.
Video injection is only available for apps that QA Wolf resigns during installation. It is not available for system apps or Safari.
## Example
**Inject a video into the camera feed:**
```javascript theme={null}
import { device } from "@qawolf/flows/ios";
const bundleId = process.env.BUNDLE_ID; // Bundle ID of app being tested
const storagePath = process.env.STORAGE_PATH; // QA Wolf remote storage
const videoPath = `${storagePath}/wolf.mp4`;
const cleanup = await device.injectCamera(driver, bundleId, {
data: videoPath,
type: "video", // optional — inferred from .mp4/.mov/.m4v/.avi
});
// ... run your assertions while the camera feed plays the video ...
await cleanup();
```
## When to use
* Your app records video and you need to verify the recording pipeline with deterministic content.
* You need to run the same scenario repeatedly with consistent inputs.
## Supported file types
**Video:** Any format supported by AVAsset — MP4, MOV, M4V with H.264/HEVC codecs
## Full sample test
```js theme={null}
import { flow, device, expect } from "@qawolf/flows/ios";
export default flow(
"iOS Media - Camera Video Injection",
{
target: "iOS - iPhone 15 (iOS 26)",
launch: {
app: { env: "IPA_APP_BUILD" },
respectSystemAlerts: true,
autoAcceptAlerts: true,
},
},
async ({ driver, test }) => {
await test("iOS Media - Camera Video Injections", async () => {
//--------------------------------
// Arrange:
//--------------------------------
// Install and Launch Trot app
// Tap "Media"
await driver
.$(
`-ios predicate string:name == 'Media' AND type == 'XCUIElementTypeButton'`,
)
.waitForDisplayed();
await driver
.$(
`-ios predicate string:name == 'Media' AND type == 'XCUIElementTypeButton'`,
)
.click();
// Tap Video Recording
await driver
.$(
`-ios predicate string:name == 'Video Recording' AND type == 'XCUIElementTypeButton'`,
)
.waitForDisplayed();
await driver
.$(
`-ios predicate string:name == 'Video Recording' AND type == 'XCUIElementTypeButton'`,
)
.click();
// Tap AVCaptureMovieFileOutput
await driver
.$(
`-ios predicate string:name == 'AVCaptureMovieFileOutput' AND type == 'XCUIElementTypeButton'`,
)
.waitForDisplayed();
await driver
.$(
`-ios predicate string:name == 'AVCaptureMovieFileOutput' AND type == 'XCUIElementTypeButton'`,
)
.click();
// Observe "Start Recording" button
await driver
.$(
`-ios predicate string:name == 'unifiedRecordButton' AND type == 'XCUIElementTypeButton'`,
)
.waitForDisplayed({ timeout: 10000 });
//--------------------------------
// Act:
//--------------------------------
// Inject video as the camera feed
const storagePath = process.env.STORAGE_PATH
await device.injectCamera(driver, "com.qawolf.trot", {
data: `${storagePath}/wolf.mp4`,
type: "video",
});
await driver.pause(3000);
//--------------------------------
// Assert:
//--------------------------------
// Assert Screenshot of Image File Displaying in Live Preview
const previewElement = driver.$(
`-ios predicate string:name == 'livePreview' AND type == 'XCUIElementTypeOther'`,
);
await previewElement.waitForDisplayed({ timeout: 5000 });
await expect(driver)
.toHaveScreenshot(
previewElement,
"video_recording_video_preview",
{ maxMisMatchPercentage: 5 },
);
// Click start recording
await driver
.$(
`-ios predicate string:name == 'unifiedRecordButton' AND type == 'XCUIElementTypeButton'`,
)
.click();
await driver.pause(3000);
// Click stop recording
await driver
.$(
`-ios predicate string:name == 'unifiedRecordButton' AND type == 'XCUIElementTypeButton'`,
)
.click();
// Observe a recording
await driver
.$(
`-ios predicate string:name BEGINSWITH 'AVCaptureMovieFileOutput_' AND type == 'XCUIElementTypeStaticText'`,
)
.click();
});
},
);
```
# Assert visual appearance
Source: https://docs.qawolf.com/qawolf/visual-diffing
Use toHaveScreenshot to catch layout regressions and verify UI elements that can't be targeted with selectors.
This page covers web only, including mobile web using Playwright emulators.
To perform a visual comparison, use `toHaveScreenshot`.
```js theme={null}
toHaveScreenshot(name, options)
```
* `name` — string used to identify and store the baseline image
* `options` — optional object controlling comparison behavior, see Key options below
## Examples
**Assert a canvas element:**
```js theme={null}
await expect(
page.locator(`[class*="chart-canvas"]`)
).toHaveScreenshot(
`revenue-chart`,
{
maxDiffPixelRatio: 0.01,
timeout: 60 * 1000,
}
);
```
**Assert a specific UI component:**
```js theme={null}
await expect(
page.locator(`[class*="nav-header"]`)
).toHaveScreenshot(
`nav-header`,
{
maxDiffPixelRatio: 0.01,
timeout: 60 * 1000,
}
);
```
**Assert a full page:**
```js theme={null}
await expect(page).toHaveScreenshot(
`checkout-page`,
{
fullPage: true,
maxDiffPixelRatio: 0.01,
timeout: 60 * 1000,
}
);
```
Omitting a name and `maxDiffPixelRatio` makes the baseline hard to identify and causes minor rendering differences to fail the test.
```js theme={null}
// Avoid
await expect(page.locator(`[class*="chart-canvas"]`)).toHaveScreenshot();
```
## When to use
* Your test needs to assert the content or appearance of a `canvas` element — canvas is not part of the DOM and cannot be targeted with locators.
* You want to catch layout or style regressions after a redesign or component change.
* No selector or functional assertion can verify the visual state you need.
* Your app has pixel-perfect design requirements, and you want automated enforcement.
Avoid `toHaveScreenshot` when the area contains timestamps, live data, animations, or other frequently-changing content — these cause false failures. Use a functional assertion instead.
## Key options
| Option | Description | Recommended |
| ------------------- | ------------------------------------------ | ----------- |
| `maxDiffPixelRatio` | Fraction of pixels allowed to differ (0–1) | `0.01` |
| `maxDiffPixels` | Absolute pixel count allowed to differ | Situational |
| `fullPage` | Capture the full scrollable page | `false` |
| `timeout` | Max milliseconds to wait for a match | `60000` |
## Notes
* On the first run, no baseline exists yet — the screenshot is saved automatically as the expected image. See the instructions below to review it before relying on the test.
* To inspect a visual diff result in the editor, collapse the test block — an icon will appear in the right gutter of that line. Click it to open the Image Diff panel, where you can view the **Diff**, **Expected**, **Actual**, and **Compare** tabs for each named snapshot.
* On subsequent runs, a new screenshot is compared pixel-by-pixel to the baseline. If the difference exceeds the threshold, the test fails, and a diff image is saved showing exactly which pixels changed.
* If a UI change is intentional, promote the new screenshot as the updated baseline in the QA Wolf editor. Future runs will compare against the new image.
## Full sample test
```js theme={null}
import { expect, flow } from "@qawolf/flows/web";
export default flow(
"Landing Page Visual Tests - Desktop",
{ target: "Web - Chrome", launch: true },
async ({ page, test }) => {
await test("Schedule Demo Visual Test", async () => {
//--------------------------------
// Arrange:
//--------------------------------
//! Opening the home page of QA Wolf
//!! navigate to the "https://www.qawolf.com/"
await page.goto("https://www.qawolf.com/");
//!! wait for loadstate to be "domcontentloaded"
await page.waitForLoadState("domcontentloaded");
//--------------------------------
// Assert:
//--------------------------------
//! Check that certain elements on the home page match the expected screenshots
//!! expect the screen to match "homepage-web"
await expect(page).toHaveScreenshot(
`homepage-web`,
{
fullPage: true,
maxDiffPixelRatio: 0.01,
timeout: 60 * 1000,
}
);
});
},
);
```
# Native mobile screenshots
Source: https://docs.qawolf.com/qawolf/visual-diffing-native-app
Use toHaveScreenshot to catch visual regressions in native iOS and Android apps.
Use `expect(driver).toHaveScreenshot()` to compare a screenshot of the current native app screen against a stored baseline. This works for both iOS and Android.
For web and mobile web visual diffing, see [Web & Mobile web screenshots](/qawolf/visual-diffing).
```typescript theme={null}
expect(driver).toHaveScreenshot(name, options)
```
* `name` — string used to identify and store the baseline image
* `options` — optional object controlling comparison behavior, see Key options below
## Examples
**iOS**
```typescript theme={null}
await expect(driver).toHaveScreenshot(
"home-screen",
{ maxMisMatchPercentage: 1 }
);
```
**Android**
```typescript theme={null}
await expect(driver).toHaveScreenshot(
"home-screen",
{ maxMisMatchPercentage: 1 }
);
```
**Assert a specific element**
Pass a WebdriverIO element instead of `driver` to scope the screenshot to a specific UI component:
```typescript theme={null}
await expect(
driver.$(`//android.widget.ImageView[@resource-id="com.example.app:id/hero_image"]`)
).toHaveScreenshot(
"hero-image",
{ maxMisMatchPercentage: 1 }
);
```
Omitting a name and `maxMisMatchPercentage` makes the baseline hard to identify and causes minor rendering differences to fail the test.
```typescript theme={null}
// Avoid
await expect(driver).toHaveScreenshot();
```
## When to use
* Your app has a rendering-heavy screen like a chart or map that can't be asserted with selectors.
* Your app has pixel-perfect design requirements and you want automated enforcement.
* Your app has gone through a redesign and you need to catch layout regressions.
* Your test needs to verify a visual state that no functional assertion can cover.
Avoid `toHaveScreenshot` when the screen contains timestamps, live data, animations, or other frequently-changing content — these cause false failures. Use a functional assertion instead.
## Key options
| Option | Description | Recommended |
| ----------------------- | ---------------------------------------------- | ----------- |
| `maxMisMatchPercentage` | Percentage of pixels allowed to differ (0–100) | `1` |
| `skipDiffUI` | Skip the visual diff UI in the editor | `false` |
`maxMisMatchPercentage` is a 0–100 percentage, unlike the web API's `maxDiffPixelRatio` which is a 0–1 ratio.
## Notes
* On the first run, no baseline exists yet — the screenshot is saved automatically as the expected image. Review it before relying on the test.
* To inspect a visual diff result in the editor, collapse the test block — an icon will appear in the right gutter of that line. Click it to open the Image Diff panel, where you can view the **Diff**, **Expected**, **Actual**, and **Compare** tabs for each named snapshot.
* On subsequent runs, a new screenshot is compared pixel-by-pixel to the baseline. If the difference exceeds the threshold, the test fails and a diff image is saved showing exactly which pixels changed.
* If a UI change is intentional, promote the new screenshot as the updated baseline in the QA Wolf editor. Future runs will compare against the new image.
* On iOS, baselines are named by device screen size (e.g. `home-screen-ios-390×844-expected.png`) — a baseline captured on an iPhone 15 will not match an iPhone 15 Plus.
* Baselines are stored in team storage under `_screenshots_` and are shared across all flows in the same team.
## Full sample test
```typescript theme={null}
import { expect, flow } from "@qawolf/flows/ios";
export default flow(
"Visual regression — home screen",
{ target: "iOS - iPhone 15 (iOS 26)", launch: true },
async ({ driver, test }) => {
await test("assert home screen appearance", async () => {
await expect(driver).toHaveScreenshot(
"home-screen",
{ maxMisMatchPercentage: 1 }
);
});
},
);
```
# Audio analysis (Web)
Source: https://docs.qawolf.com/qawolf/web-audio-analysis
Validate audio streams and files using ffprobe and ffmpeg to assert on codec, bitrate, duration, and silence.
Use `ffprobe` and `ffmpeg` via `execa` to inspect audio streams and downloaded files during a flow. Both tools are pre-installed on QA Wolf runners.
See the [ffprobe documentation](https://ffmpeg.org/ffprobe.html) and [ffmpeg documentation](https://ffmpeg.org/ffmpeg.html) for the full list of available flags.
## Examples
**Inspect a stream URL**
```typescript theme={null}
const { execa } = await import("execa");
const streamUrl = await page.locator("audio").getAttribute("src");
const { stdout } = await execa("ffprobe", [
"-v", "quiet",
"-print_format", "json",
"-show_format",
"-show_streams",
"-i", streamUrl,
]);
const metadata = JSON.parse(stdout);
const audioStream = metadata.streams.find((s) => s.codec_type === "audio");
expect(audioStream.codec_name).toBe("aac");
expect(parseInt(metadata.format.bit_rate) / 1000).toBeGreaterThan(100);
```
**Detect silence in a downloaded file**
```typescript theme={null}
const { execa } = await import("execa");
const { stderr } = await execa("ffmpeg", [
"-i", filePath,
"-af", "silencedetect=n=-50dB:d=1",
"-f", "null", "-",
]);
if (stderr.includes("silence_start")) {
throw new Error("Audio file contains silence.");
}
```
**Assert file metadata**
```typescript theme={null}
const { execa } = await import("execa");
const { stdout } = await execa("ffprobe", [
"-v", "error",
"-show_entries", "format=bit_rate,duration",
"-show_streams",
"-of", "json",
filePath,
]);
const metadata = JSON.parse(stdout);
const audioStream = metadata.streams.find((s) => s.codec_type === "audio");
expect(audioStream.codec_name).toBe("aac");
expect(parseInt(metadata.format.bit_rate) / 1000).toBeGreaterThan(100);
```
## When to use
* Your app streams audio and you need to assert on codec, bitrate, or sample rate.
* Your app generates or downloads audio files and you need to verify they aren't silent or corrupted.
* Your app plays back recordings and you need to confirm the file contains valid audio data.
* Your test needs to validate audio properties that can't be asserted through the UI.
## Full sample test
```typescript theme={null}
import { flow, launch } from "@qawolf/flows/web";
export default flow(
"Validate audio stream",
"Web - Chrome",
async ({ page, test }) => {
await test("navigate to player and assert stream metadata", async () => {
// Arrange
await page.goto("https://your-app.com/player");
await page.locator("audio").waitFor({ state: "attached" });
// Act
const { execa } = await import("execa");
const streamUrl = await page.locator("audio").getAttribute("src");
const { stdout } = await execa("ffprobe", [
"-v", "quiet",
"-print_format", "json",
"-show_format",
"-show_streams",
"-i", streamUrl,
]);
// Assert
const metadata = JSON.parse(stdout);
const audioStream = metadata.streams.find((s) => s.codec_type === "audio");
expect(audioStream).toBeTruthy();
expect(audioStream.codec_name).toBe("aac");
expect(parseInt(metadata.format.bit_rate) / 1000).toBeGreaterThan(100);
});
},
);
```
# Microphone injection (Web)
Source: https://docs.qawolf.com/qawolf/web-audio-injection
Replace the browser's microphone input with an audio file to test voice and speech features in web apps.
Use Playwright's fake media device flags to inject a pre-recorded audio file as the browser's microphone input. This lets you test voice-activated features, speech recognition, and audio processing in web apps under controlled, repeatable conditions.
WAV format is recommended. Store your audio file in team storage and reference it via `process.env.TEAM_STORAGE_DIR`. See [Upload files](/qawolf/Uploading-manually) for instructions.
Playwright initializes the fake audio device once per browser instance. You cannot swap the audio file mid-flow. If you need to test with different audio inputs, launch a new browser instance for each.
## Examples
**Inject audio into the browser microphone**
```typescript theme={null}
const { context } = await launch({
permissions: ["microphone"],
args: [
"--use-fake-device-for-media-stream",
"--use-fake-ui-for-media-stream",
`--use-file-for-fake-audio-capture=${process.env.TEAM_STORAGE_DIR}/voice-input.wav`,
],
});
const page = await context.newPage();
await page.goto("https://your-app.com");
// trigger the feature that reads from the microphone
```
## When to use
* Your app has speech recognition and you need to test that specific voice commands are interpreted correctly.
* Your app has voice-activated controls and you need to verify they produce the expected behavior.
* Your app processes or transforms microphone input and you need to assert on the output.
* Your app has voice-controlled accessibility features and you need to confirm they work as expected.
## Full sample test
```typescript theme={null}
import { flow, launch } from "@qawolf/flows/web";
export default flow(
"Test voice input",
"Web - Chrome",
async ({ test }) => {
const { context } = await launch({
permissions: ["microphone"],
args: [
"--use-fake-device-for-media-stream",
"--use-fake-ui-for-media-stream",
`--use-file-for-fake-audio-capture=${process.env.TEAM_STORAGE_DIR}/voice-input.wav`,
],
});
const page = await context.newPage();
await test("inject audio and verify voice command response", async () => {
// Arrange
await page.goto("https://your-app.com/voice");
await page.locator(`[data-testid='mic-button']`).waitFor({ state: "visible" });
// Act
await page.locator(`[data-testid='mic-button']`).click();
await page.waitForTimeout(5_000);
// Assert
await expect(page.locator(`[data-testid='transcript']`)).toContainText("hello");
});
},
);
```
# Measure web page performance
Source: https://docs.qawolf.com/qawolf/web-performance
Run a Lighthouse audit in a QA Wolf flow to spot-check Core Web Vitals and key performance metrics.
This recipe covers spot-checking web performance with Lighthouse. For operationalized performance monitoring — scheduled runs, trend tracking, aggregated reports, and alerting — talk to your QA Wolf team about full-service performance testing.
## Examples
**Run a Lighthouse audit and assert on the performance score:**
```javascript theme={null}
import { expect, flow } from "@qawolf/flows/web";
import { playAudit } from "playwright-lighthouse";
const PERF_SCORE_MIN = 80;
export default flow(
"Homepage performance",
{ target: "Web - Chrome", launch: { args: ["--remote-debugging-port=9222"] } },
async ({ page, test }) => {
await test("Lighthouse audit", async () => {
await page.goto(process.env.BASE_URL);
await page.waitForLoadState("networkidle").catch(() => {});
const { lhr } = await playAudit({
page,
port: 9222,
thresholds: { performance: PERF_SCORE_MIN },
config: {
extends: "lighthouse:default",
settings: { onlyCategories: ["performance"] },
},
});
const perfScore = Math.round(lhr.categories.performance.score * 100);
expect(perfScore).toBeGreaterThanOrEqual(PERF_SCORE_MIN);
});
},
);
```
**Assert on individual Core Web Vitals:**
```javascript theme={null}
const lcp = lhr.audits["largest-contentful-paint"].numericValue;
const fcp = lhr.audits["first-contentful-paint"].numericValue;
const tbt = lhr.audits["total-blocking-time"].numericValue;
const cls = lhr.audits["cumulative-layout-shift"].numericValue;
const tti = lhr.audits["interactive"].numericValue;
const ttfb = lhr.audits["server-response-time"].numericValue;
```
**Save the audit report to team storage:**
```javascript theme={null}
const { lhr } = await playAudit({
page,
thresholds: { performance: PERF_SCORE_MIN },
reports: {
formats: { json: true },
directory: `${process.env.TEAM_STORAGE_DIR}/lighthouse`,
name: `homepage-${Date.now()}`,
},
config: {
extends: "lighthouse:default",
settings: { onlyCategories: ["performance"] },
},
});
```
The double navigation is intentional. Lighthouse measures the second load so that cookie banner dismissal steps don't skew the metrics.
## When to use
* Your team wants a quick sanity check that a page meets minimum performance thresholds before or after a deploy.
* You want to catch regressions in Core Web Vitals — LCP, CLS, or FCP — introduced by new code.
* You need a lightweight performance gate in CI without a full monitoring setup.
* You want to verify that a specific page (checkout, landing page, dashboard) hasn't degraded after a significant change.
## Performance thresholds
For more on what each metric means and why it matters, see [Web performance metrics, explained](https://www.qawolf.com/blog/web-performance-metrics-explained).
## Notes
* The sample test below runs in Google Chrome only, using port `9222`, which is the Chrome DevTools Protocol port. Lighthouse connects to an already-running Chrome instance via CDP rather than launching its own.
* `playAudit` requires a Playwright `page` object with an active navigation. Call it after `waitForLoadState` to ensure the page has settled.
* Lighthouse runs in a simulated environment — results will vary slightly between runs. Avoid very tight thresholds (e.g. `≤ 100ms` for TBT) that will produce flaky results.
* Thresholds are declared as named constants at the top of the file so they are easy to find and adjust per environment.
* `onlyCategories: ["performance"]` scopes the audit to performance only. Lighthouse can also score accessibility and SEO.
## Full sample test
```js theme={null}
import { expect, flow } from "@qawolf/flows/web";
import { playAudit } from "playwright-lighthouse";
// Thresholds based on qawolf.com/blog/web-performance-metrics-explained
const PERF_SCORE_MIN = 80;
const LCP_MAX_MS = 2500;
const FCP_MAX_MS = 1800;
const TBT_MAX_MS = 200;
const CLS_MAX = 0.1;
const TTI_MAX_MS = 3000;
const TTFB_MAX_MS = 150;
const PAGE_LOAD_MAX_MS = 2000;
export default flow(
"Homepage",
{ target: "Web - Chrome", launch: { args: ["--remote-debugging-port=9222"] } },
async ({ page, test }) => {
await test("Performance test for homepage", async () => {
//--------------------------------
// Arrange: Launch browser and navigate to homepage
//--------------------------------
await page.goto(process.env.BASE_URL);
//--------------------------------
// Act: Run Lighthouse performance audit
//--------------------------------
await page.goto(process.env.BASE_URL);
await page.waitForLoadState("networkidle").catch(() => {});
const { lhr } = await playAudit({
page,
port: 9222,
thresholds: {
performance: PERF_SCORE_MIN,
},
reports: {
formats: { json: true },
directory: `${process.env.TEAM_STORAGE_DIR}/lighthouse`,
name: `homepage-${Date.now()}`,
},
config: {
extends: "lighthouse:default",
settings: {
onlyCategories: ["performance"],
},
},
});
//--------------------------------
// Assert: Verify all performance metrics are within budget
//--------------------------------
const perfScore = Math.round(lhr.categories.performance.score * 100);
const lcp = lhr.audits["largest-contentful-paint"].numericValue;
const fcp = lhr.audits["first-contentful-paint"].numericValue;
const tbt = lhr.audits["total-blocking-time"].numericValue;
const cls = lhr.audits["cumulative-layout-shift"].numericValue;
const tti = lhr.audits["interactive"].numericValue;
const ttfb = lhr.audits["server-response-time"].numericValue;
const pageLoadTime = lhr.audits["metrics"].details.items[0].observedLoad;
console.log(`Performance score: ${perfScore} (min ${PERF_SCORE_MIN})`);
console.log(`LCP: ${Math.round(lcp)}ms (max ${LCP_MAX_MS}ms)`);
console.log(`FCP: ${Math.round(fcp)}ms (max ${FCP_MAX_MS}ms)`);
console.log(`TBT: ${Math.round(tbt)}ms (max ${TBT_MAX_MS}ms)`);
console.log(`CLS: ${cls.toFixed(3)} (max ${CLS_MAX})`);
console.log(`TTI: ${Math.round(tti)}ms (max ${TTI_MAX_MS}ms)`);
console.log(`TTFB: ${Math.round(ttfb)}ms (max ${TTFB_MAX_MS}ms)`);
console.log(`Page load: ${Math.round(pageLoadTime)}ms (max ${PAGE_LOAD_MAX_MS}ms)`);
expect(perfScore).toBeGreaterThanOrEqual(PERF_SCORE_MIN);
expect(lcp).toBeLessThanOrEqual(LCP_MAX_MS);
expect(fcp).toBeLessThanOrEqual(FCP_MAX_MS);
expect(tbt).toBeLessThanOrEqual(TBT_MAX_MS);
expect(cls).toBeLessThanOrEqual(CLS_MAX);
expect(tti).toBeLessThanOrEqual(TTI_MAX_MS);
expect(ttfb).toBeLessThanOrEqual(TTFB_MAX_MS);
expect(pageLoadTime).toBeLessThanOrEqual(PAGE_LOAD_MAX_MS);
});
},
);
```
# Speaker recording (Web)
Source: https://docs.qawolf.com/qawolf/web-speaker-capture
Capture browser audio output during a test to verify that your app plays the correct audio.
Use Playwright's `getDisplayMedia` API with Chrome flags to record audio playing in a browser tab. The recording is saved as a `.webm` file that can be downloaded and analyzed.
Playwright initializes the fake audio device once per browser instance. You cannot swap audio sources mid-flow. If you need to record different audio content, launch a new browser instance for each.
## Examples
**Record audio from a browser tab**
```typescript theme={null}
const { rm } = await import("node:fs/promises");
await rm("/tmp/audioRecording.webm", { force: true });
page.on("download", async (download) => {
await download.saveAs(`/tmp/${download.suggestedFilename()}`);
});
await page.evaluate(async () => {
const stream = await navigator.mediaDevices.getDisplayMedia({
video: { displaySurface: "browser" },
audio: { displaySurface: "browser" },
});
const recorder = new MediaRecorder(stream);
window.__rec = recorder;
const chunks = [];
recorder.ondataavailable = (e) => chunks.push(e.data);
recorder.onstop = () => {
const blob = new Blob(chunks, { type: chunks[0]?.type || "video/webm" });
stream.getVideoTracks()[0]?.stop();
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = "audioRecording.webm";
document.body.appendChild(a);
a.click();
a.remove();
};
recorder.start();
});
// trigger audio playback in your app
await page.waitForTimeout(10_000);
await page.evaluate(() => window.__rec.stop());
await page.waitForTimeout(10_000);
```
**Download audio from a page element**
If your app exposes audio via a `