diff --git a/README.md b/README.md
index 846d446..4e8e540 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,19 @@ A sample application built solely to showcase axe DevTools Mobile implementation
------
+## Two ways to test
+
+This sample demonstrates both ways to run accessibility scans with axe DevTools Mobile. Each lives in its own folder under `axe-devtools-ios-sample-appUITests/` so you can find the pattern you need at a glance.
+
+| Approach | What it does | Where to look | Credentials |
+| --- | --- | --- | --- |
+| **Targeted scanning** | You choose exactly which screens to scan with `axe.run(onElement:)`, then report via an HTML report and/or upload to the dashboard. | [`TargetedScan/TargetedScanUITests.swift`](axe-devtools-ios-sample-appUITests/TargetedScan/TargetedScanUITests.swift) | `Login.swift` |
+| **AutoScan** | Zero-code: your test just drives the app (no imports, no scan calls). The framework automatically captures, de-duplicates, and scans each unique screen, then writes one HTML report. | [`AutoScan/AutoScanUITests.swift`](axe-devtools-ios-sample-appUITests/AutoScan/AutoScanUITests.swift) | [`axe_config.json`](axe-devtools-ios-sample-appUITests/axe_config.json) |
+
+Both approaches produce a self-contained **HTML report** locally. With targeted scanning you call `generateHtmlReportAndSummary()` yourself; with AutoScan it happens automatically when the test bundle finishes — controlled entirely by `axe_config.json`. AutoScan ships **disabled** (`"axeAutoScanMode": false`); set it to `true` and add your credentials in that file to try it.
+
+------
+
## Get Started:
> Prerequisites:
@@ -18,9 +31,9 @@ A sample application built solely to showcase axe DevTools Mobile implementation
This project uses Swift Package Manager to pull in the frameworks from [axe-devtools-ios](https://github.com/dequelabs/axe-devtools-ios/).
-1. Add your Deque API Key and your axe Developer Hub Project Id to `Login.swift`.
+1. Add your Deque API Key and your axe Developer Hub Project Id to `Login.swift` (for the targeted-scanning tests) and to `axe-devtools-ios-sample-appUITests/axe_config.json` (for the AutoScan tests).
-2. Once you have a device or simulator ready to go, open `SampleUITests.swift`, and tap on the diamond to run this suite. The simulator will launch, and the tests will run.
+2. Once you have a device or simulator ready to go, open either `TargetedScan/TargetedScanUITests.swift` or `AutoScan/AutoScanUITests.swift`, and tap on the diamond to run the suite. The simulator will launch, and the tests will run.
@@ -30,8 +43,8 @@ _Note: This project requires a device on iOS 16.0 or later. Additionally, Virtua
To run a test with this project, or to setup your own project, follow our online guide to get started with [XCUITesting on SauceLabs.](https://docs.deque.com/devtools-mobile/ios-example-sauce-labs-xcui)
-Assuming you have already downloaded the sample project to your computer:
-1. Add your Deque API Key and your axe Developer Hub Project Id to `Login.swift`.
+These steps run the **targeted-scanning** tests. Assuming you have already downloaded the sample project to your computer:
+1. Add your Deque API Key and your axe Developer Hub Project Id to `Login.swift`. (To exercise the AutoScan path instead, also enable it in `axe-devtools-ios-sample-appUITests/axe_config.json` as described above.)
2. Open a Terminal window, enter `cd `, then drag and drop the `axe-devtools-ios-sample-app` from Finder into the Terminal window. Hit return/enter.
To run on **Real Devices**, type `sh prepareForSauceRealDevice.sh` in the same window. Hit return/enter.
diff --git a/RegressionUITests/RegressionUITests.swift b/RegressionUITests/RegressionUITests.swift
index 0e155f5..3137d45 100644
--- a/RegressionUITests/RegressionUITests.swift
+++ b/RegressionUITests/RegressionUITests.swift
@@ -14,7 +14,7 @@ class RegressionUITests: XCTestCase {
var lastResult: AxeResult?
override func setUp() {
- axe = try? AxeDevTools.startSession(apiKey: Login.APIKey, projectId: Login.projectId)
+ axe = try? AxeDevTools.startScanSession(apiKey: Login.APIKey, projectId: Login.projectId)
app.launch()
sleep(2) // allow app to fully load, Github actions needed a moment.
diff --git a/axe-devtools-ios-sample-app.xcodeproj/project.pbxproj b/axe-devtools-ios-sample-app.xcodeproj/project.pbxproj
index 4c538fc..625fe86 100644
--- a/axe-devtools-ios-sample-app.xcodeproj/project.pbxproj
+++ b/axe-devtools-ios-sample-app.xcodeproj/project.pbxproj
@@ -7,11 +7,14 @@
objects = {
/* Begin PBXBuildFile section */
+ 30CF7AC9E4AF9F05A27959C6 /* TargetedScanUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5930CB6C34FD68A1DF754BBE /* TargetedScanUITests.swift */; };
+ 3399D4BF2D58349249B12104 /* axe_config.json in Resources */ = {isa = PBXBuildFile; fileRef = E91A725F8F38EF78E0303331 /* axe_config.json */; };
6A3B4C992A5D8EC20078BB09 /* RegressionUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A3B4C982A5D8EC20078BB09 /* RegressionUITests.swift */; };
6A3B4CA12A5D8F6B0078BB09 /* Login.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AEDD8512886FD4D0017DCF1 /* Login.swift */; };
6AEDD8522886FD4D0017DCF1 /* Login.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AEDD8512886FD4D0017DCF1 /* Login.swift */; };
6AEDD8542886FD500017DCF1 /* Login.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AEDD8512886FD4D0017DCF1 /* Login.swift */; };
- 6AF13275282E848C00A64994 /* SampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AF13274282E848C00A64994 /* SampleUITests.swift */; };
+ 805E4C1E5E82317B9E2A3B89 /* SampleAppNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B17D6D36DF84C738F14736B7 /* SampleAppNavigation.swift */; };
+ 845EEC6DCB3471149282DB01 /* AutoScanUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C23D938F193549E1C9DE7418 /* AutoScanUITests.swift */; };
9E38FE2F2E31209F000DAC24 /* NamedTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E38FE2E2E312097000DAC24 /* NamedTextField.swift */; };
9E38FE312E3121D0000DAC24 /* ContactUsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E38FE302E3121C8000DAC24 /* ContactUsTableViewCell.swift */; };
9E38FE332E312271000DAC24 /* Route.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E38FE322E31226E000DAC24 /* Route.swift */; };
@@ -125,15 +128,18 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
+ 5930CB6C34FD68A1DF754BBE /* TargetedScanUITests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetedScanUITests.swift; sourceTree = ""; };
6A3B4C962A5D8EC20078BB09 /* RegressionUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RegressionUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
6A3B4C982A5D8EC20078BB09 /* RegressionUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegressionUITests.swift; sourceTree = ""; };
6AEDD8512886FD4D0017DCF1 /* Login.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Login.swift; sourceTree = ""; };
- 6AF13274282E848C00A64994 /* SampleUITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleUITests.swift; sourceTree = ""; };
9E38FE2E2E312097000DAC24 /* NamedTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NamedTextField.swift; sourceTree = ""; };
9E38FE302E3121C8000DAC24 /* ContactUsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactUsTableViewCell.swift; sourceTree = ""; };
9E38FE322E31226E000DAC24 /* Route.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Route.swift; sourceTree = ""; };
9E38FE362E3169FA000DAC24 /* UIFont+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Extensions.swift"; sourceTree = ""; };
9E76EC592E3004BC0023917A /* ContactUsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactUsTableViewController.swift; sourceTree = ""; };
+ B17D6D36DF84C738F14736B7 /* SampleAppNavigation.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SampleAppNavigation.swift; sourceTree = ""; };
+ C23D938F193549E1C9DE7418 /* AutoScanUITests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AutoScanUITests.swift; sourceTree = ""; };
+ E91A725F8F38EF78E0303331 /* axe_config.json */ = {isa = PBXFileReference; includeInIndex = 1; path = axe_config.json; sourceTree = ""; };
F3799A7227E4D0C5009CFC2E /* ItemCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemCollectionView.swift; sourceTree = ""; };
F3799A7427E4D0DC009CFC2E /* ItemCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemCollectionViewCell.swift; sourceTree = ""; };
F3799A7627E4D7ED009CFC2E /* ItemCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemCellViewModel.swift; sourceTree = ""; };
@@ -239,6 +245,15 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
+ 21B84018A6A78D27990475E2 /* AutoScan */ = {
+ isa = PBXGroup;
+ children = (
+ C23D938F193549E1C9DE7418 /* AutoScanUITests.swift */,
+ );
+ name = AutoScan;
+ path = AutoScan;
+ sourceTree = "";
+ };
6A3B4C972A5D8EC20078BB09 /* RegressionUITests */ = {
isa = PBXGroup;
children = (
@@ -247,6 +262,15 @@
path = RegressionUITests;
sourceTree = "";
};
+ 86674B529DE75ED599ED20CF /* TargetedScan */ = {
+ isa = PBXGroup;
+ children = (
+ 5930CB6C34FD68A1DF754BBE /* TargetedScanUITests.swift */,
+ );
+ name = TargetedScan;
+ path = TargetedScan;
+ sourceTree = "";
+ };
9E38FE2D2E312081000DAC24 /* Views */ = {
isa = PBXGroup;
children = (
@@ -282,6 +306,15 @@
path = ContactUs;
sourceTree = "";
};
+ B19CEFD283E99B98B85E565F /* Shared */ = {
+ isa = PBXGroup;
+ children = (
+ B17D6D36DF84C738F14736B7 /* SampleAppNavigation.swift */,
+ );
+ name = Shared;
+ path = Shared;
+ sourceTree = "";
+ };
F3133A4628086B0A00389A4C /* Frameworks */ = {
isa = PBXGroup;
children = (
@@ -528,7 +561,10 @@
F3F31DAE27DFD32500D87988 /* axe-devtools-ios-sample-appUITests */ = {
isa = PBXGroup;
children = (
- 6AF13274282E848C00A64994 /* SampleUITests.swift */,
+ 86674B529DE75ED599ED20CF /* TargetedScan */,
+ 21B84018A6A78D27990475E2 /* AutoScan */,
+ B19CEFD283E99B98B85E565F /* Shared */,
+ E91A725F8F38EF78E0303331 /* axe_config.json */,
);
path = "axe-devtools-ios-sample-appUITests";
sourceTree = "";
@@ -608,8 +644,6 @@
dependencies = (
);
name = "axe-devtools-ios-sample-app";
- packageProductDependencies = (
- );
productName = "axe-devtools-ios-sample-app";
productReference = F3F31D8B27DFD32500D87988 /* axe-devtools-ios-sample-app.app */;
productType = "com.apple.product-type.application";
@@ -707,6 +741,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 3399D4BF2D58349249B12104 /* axe_config.json in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -806,7 +841,9 @@
buildActionMask = 2147483647;
files = (
6AEDD8542886FD500017DCF1 /* Login.swift in Sources */,
- 6AF13275282E848C00A64994 /* SampleUITests.swift in Sources */,
+ 30CF7AC9E4AF9F05A27959C6 /* TargetedScanUITests.swift in Sources */,
+ 845EEC6DCB3471149282DB01 /* AutoScanUITests.swift in Sources */,
+ 805E4C1E5E82317B9E2A3B89 /* SampleAppNavigation.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
diff --git a/axe-devtools-ios-sample-app.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/axe-devtools-ios-sample-app.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 0dfedce..3e91f8a 100644
--- a/axe-devtools-ios-sample-app.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/axe-devtools-ios-sample-app.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -7,7 +7,7 @@
"location" : "https://github.com/dequelabs/axe-devtools-ios",
"state" : {
"branch" : "main",
- "revision" : "474fcacb158ccc6e4ae1f175649e8f30d535e9d2"
+ "revision" : "162c924e0f91beb08cd047704380e78c09fd4bf2"
}
}
],
diff --git a/axe-devtools-ios-sample-app/Login.swift b/axe-devtools-ios-sample-app/Login.swift
index 9de1b9e..e9fb3b5 100644
--- a/axe-devtools-ios-sample-app/Login.swift
+++ b/axe-devtools-ios-sample-app/Login.swift
@@ -6,6 +6,14 @@
//
import Foundation
+
+/// Credentials for the **targeted scanning** examples (TargetedScanUITests).
+///
+/// - API key: https://axe.deque.com/settings
+/// - Project ID: https://axe.deque.com/axe-watcher/projects
+///
+/// Note: the **AutoScan** example reads its credentials from
+/// `axe-devtools-ios-sample-appUITests/axe_config.json` instead, not from here.
struct Login {
static let APIKey=""
static let projectId=""
diff --git a/axe-devtools-ios-sample-appUITests/AutoScan/AutoScanUITests.swift b/axe-devtools-ios-sample-appUITests/AutoScan/AutoScanUITests.swift
new file mode 100644
index 0000000..04d69c6
--- /dev/null
+++ b/axe-devtools-ios-sample-appUITests/AutoScan/AutoScanUITests.swift
@@ -0,0 +1,53 @@
+//
+// AutoScanUITests.swift
+// axe-devtools-ios-sample-appUITests
+//
+// AUTOSCAN — the zero-code path.
+//
+// Notice what's NOT here: no `import axeDevToolsXCUI`, no session setup, no
+// scan calls, no report calls. That's the whole point of AutoScan.
+//
+// As long as the axeDevToolsXCUI framework is linked to this test target and
+// `axe_config.json` has `"axeAutoScanMode": true`, the framework automatically:
+// 1. Starts AutoScan when the test bundle begins.
+// 2. Captures and de-duplicates each unique screen as you tap and swipe.
+// 3. Scans everything and writes an HTML report when the test bundle finishes.
+//
+// AutoScan ships DISABLED in this sample. To try it, open axe_config.json,
+// set `"axeAutoScanMode": true`, and fill in your axeMobileApiKey/axeProjectId.
+// Then just drive the app the way a user would — no other code required.
+//
+// Full guide: https://docs.deque.com/devtools-mobile/
+//
+
+import XCTest
+
+final class AutoScanUITests: XCTestCase {
+ private let app = XCUIApplication()
+
+ override func setUpWithError() throws {
+ continueAfterFailure = false
+ app.launch()
+ }
+
+ /// A plain user journey across the app, recorded with Xcode's UI test recorder
+ /// and then tidied up. There is no accessibility code here — AutoScan captures
+ /// and scans each screen automatically as these gestures run.
+ func testUserJourney() throws {
+ // Home — scroll through the featured content.
+ app.collectionViews.containing(.other, identifier: "Vertical scroll bar, 4 pages").firstMatch.swipeUp()
+ app.otherElements.matching(identifier: "Horizontal scroll bar, 1 page").element(boundBy: 0).swipeUp()
+
+ // Catalog — browse the categories.
+ app.images["Category"].firstMatch.tap()
+ app.scrollViews.firstMatch.swipeUp()
+
+ // Cart — add an item.
+ app.images["Bag"].firstMatch.tap()
+ app.buttons.matching(identifier: "+").element(boundBy: 1).tap()
+
+ // Profile — open and scroll to the help section.
+ app.images["Menu"].firstMatch.tap()
+ app.cells.containing(.image, identifier: "Info Square").firstMatch.swipeUp()
+ }
+}
diff --git a/axe-devtools-ios-sample-appUITests/SampleUITests.swift b/axe-devtools-ios-sample-appUITests/SampleUITests.swift
deleted file mode 100644
index 1570a1b..0000000
--- a/axe-devtools-ios-sample-appUITests/SampleUITests.swift
+++ /dev/null
@@ -1,122 +0,0 @@
-//
-// SampleUITests.swift
-// axe-devtools-ios-sample-appUITests
-//
-// Created by Kate Owens on 3/14/22.
-//
-
-import axe_devtools_ios_sample_app
-import axeDevToolsXCUI
-import XCTest
-
-/*
- Full Getting Started guide is available here: https://docs.deque.com/devtools-mobile/ios-setup
-
- Quick Start Guide
-
- To get started:
- Set up axeDevTools automation for UITests in the 2 easy steps outlined below.
- Don't forget to save this file when finished. File > Save or Command + S
-
- To run an accessibility test:
- Select a simulator or device in Xcode and press cmd+U to begin running tests.
- Review your test results from the Mobile Dashboard: https://axe-mobile.deque.com/
- */
-
-class SampleUITests: XCTestCase {
- var axe: AxeDevTools?
- var app = XCUIApplication()
-
- override func setUp() {
- // 1.
- // Enter your axe DevTools Mobile API key and axe Developer Hub project ID in the "" marks in the APIKey and projectId constants defined in the Login file
- // Get your API key here: https://axe.deque.com/settings
- // Get your axe Developer Hub Project Id here: https://axe.deque.com/axe-watcher/projects
- axe = try? AxeDevTools.startSession(apiKey: Login.APIKey, projectId: Login.projectId)
-
- // 1a (Optional). Experimental rules are rulesets that are still in testing and development. Results for the
- // experimental rules can be IGNORED with the ignoreExperimental method, and this way they will not run.
- axe?.configuration.ignoreExperimental()
-
- // 2.
- // Launch the sample app
- app.launch()
- sleep(1)
- }
-
- /*
- This test runs an accessibility scan on each tab in the sample application, then posts to the dashboard.
- This example contains a few different options for implementing -- feel free to play around with it!
- */
- func testAccessibilityAndPostResults() throws {
-
- // Run a scan on the first page of the sample app
- // For more information about running an accessibility scan visit:
- // https://docs.deque.com/devtools-mobile/xcui-setup#automated-testing
- guard let result = try axe?.run(onElement: app) else {
- XCTFail("\n\n🦮 axe DevTools didn't run - Did you add your API key in Login.swift?\n\n")
- return
- }
-
- // Post scan results to the mobile dashboard
- // For more information on this feature please visit:
- // https://docs.deque.com/devtools-mobile/ios-upload
- try axe?.postResult(result)
-
- let tabBar = XCUIApplication().tabBars["Tab Bar"]
-
- tabBar.buttons["Catalog"].tap()
-
- guard let result = try axe?.run(onElement: app) else {
- XCTFail("\n\n🦮 axe DevTools didn't run - Did you add your API key in Login.swift?\n\n")
- return
- }
-
- // Add a custom scan name when posting result. By default, the screen title is the scan name.
- // For more information on renaming a scan, check out:
- // https://docs.deque.com/devtools-mobile/ios-scan-name
- let newScanName = "Catalog Tab"
-
- try axe?.postResult(result, withScanName: newScanName)
-
- tabBar.buttons["Cart"].tap()
- guard let result = try axe?.run(onElement: app) else {
- XCTFail("\n\n🦮 axe DevTools didn't run - Did you add your API key in Login.swift?\n\n")
- return
- }
-
- // Add tags to your scan to easily share with your coworkers when it's pushed to the dashboard.
- // For more infromation on tagging a scan please see:
- // https://docs.deque.com/devtools-mobile/ios-tag-result
- let tags = ["Cart Tab, iPhone 15 Pro"]
-
- try axe?.postResult(result, withTags: tags)
-
- tabBar.buttons["Profile"].tap()
- guard let result = try axe?.run(onElement: app) else {
- XCTFail("\n\n🦮 axe DevTools didn't run - Did you add your API key in Login.swift?\n\n")
- return
- }
- try axe?.postResult(result, withScanName: "Profile Tab")
- }
-
- // This test runs an accessibility scan on the first page of the sample application and saves the scan results locally. By default, the results will be saved to a folder called 'AxeDevToolsResults' within your local User folder
- // For more information on saving results locally, please visit:
- // https://docs.deque.com/devtools-mobile/ios-save-result
- func testAccessibilityAndSaveResultsLocally() throws {
- var lastResult: AxeResult?
- guard let result = try axe?.run(onElement: app) else {
- XCTFail("\n\n🦮 axe DevTools didn't run - Did you add your API key in Login.swift?\n\n")
- return
- }
-
- // Save scan results locally
- _ = try axe?.saveResult(result)
-
- lastResult = result
- let critical = lastResult?.failures.filter { $0.impact == .CRITICAL }.count
-
- // Assert that there are no critical impact accessibility issues in scan results
- XCTAssertTrue(critical == 0, "Critical Accessibility Results were found." )
- }
-}
diff --git a/axe-devtools-ios-sample-appUITests/Shared/SampleAppNavigation.swift b/axe-devtools-ios-sample-appUITests/Shared/SampleAppNavigation.swift
new file mode 100644
index 0000000..a5afd39
--- /dev/null
+++ b/axe-devtools-ios-sample-appUITests/Shared/SampleAppNavigation.swift
@@ -0,0 +1,27 @@
+//
+// SampleAppNavigation.swift
+// axe-devtools-ios-sample-appUITests
+//
+// Small helpers that keep the example tests terse by centralizing
+// the sample app's tab-bar navigation. The four tabs (Home, Catalog,
+// Cart, Profile) are defined in MainTabBarViewController.
+//
+
+import XCTest
+
+extension XCUIApplication {
+ /// The sample app's main tab bar.
+ var sampleTabBar: XCUIElement {
+ tabBars["Tab Bar"]
+ }
+
+ /// The titles of every tab in the sample app, in display order.
+ static let sampleTabs = ["Home", "Catalog", "Cart", "Profile"]
+
+ /// Waits for the tab with the given title to appear, then taps it.
+ func goToTab(_ title: String) {
+ let button = sampleTabBar.buttons[title]
+ XCTAssertTrue(button.waitForExistence(timeout: 5), "Tab '\(title)' did not appear.")
+ button.tap()
+ }
+}
diff --git a/axe-devtools-ios-sample-appUITests/TargetedScan/TargetedScanUITests.swift b/axe-devtools-ios-sample-appUITests/TargetedScan/TargetedScanUITests.swift
new file mode 100644
index 0000000..60c85d4
--- /dev/null
+++ b/axe-devtools-ios-sample-appUITests/TargetedScan/TargetedScanUITests.swift
@@ -0,0 +1,82 @@
+//
+// TargetedScanUITests.swift
+// axe-devtools-ios-sample-appUITests
+//
+// TARGETED SCANNING — the on-demand path.
+//
+// You decide exactly which screens to scan and when, by calling
+// `axe.run(onElement:)`. This example walks the four tabs of the sample
+// app, then shows the two ways to report the results:
+// 1. Generate a self-contained HTML report + summary locally.
+// 2. Upload each scan to the axe Developer Hub dashboard.
+//
+// Full guide: https://docs.deque.com/devtools-mobile/
+//
+
+import axe_devtools_ios_sample_app
+import axeDevToolsXCUI
+import XCTest
+
+final class TargetedScanUITests: XCTestCase {
+ private var axe: AxeDevTools?
+ private let app = XCUIApplication()
+
+ override func setUpWithError() throws {
+ continueAfterFailure = false
+
+ // Start a scan session against the axe Developer Hub.
+ // Add your API key and project ID to Login.swift first:
+ // • API key: https://axe.deque.com/settings
+ // • Project ID: https://axe.deque.com/axe-watcher/projects
+ // `try?` so a missing/invalid key leaves `axe` nil and each test fails with the
+ // friendly guidance below, rather than aborting setUp with an opaque error.
+ axe = try? AxeDevTools.startScanSession(apiKey: Login.APIKey, projectId: Login.projectId)
+
+ // Experimental rules are still in development; ignore them so they don't run.
+ axe?.configuration.ignoreExperimental()
+
+ app.launch()
+ }
+
+ /// Scans every tab on demand, then produces ONE HTML report covering all of them.
+ ///
+ /// `run(onElement:)` accumulates each result on the `axe` instance, so a single
+ /// `generateHtmlReportAndSummary()` call at the end bundles every scan into one
+ /// self-contained HTML file plus a printed summary.
+ func testScanAllTabsAndGenerateHtmlReport() throws {
+ let axe = try XCTUnwrap(axe, "axe DevTools didn't start — did you add your API key to Login.swift?")
+
+ for tab in XCUIApplication.sampleTabs {
+ app.goToTab(tab)
+ _ = try axe.run(onElement: app)
+ }
+
+ // One report for the whole journey. Defaults to ~/AxeDevToolsMobileResults.
+ let report = try axe.generateHtmlReportAndSummary()
+ XCTAssertFalse(report.htmlReportPath.isEmpty, "Expected an HTML report to be written.")
+ print("📄 HTML report: \(report.htmlReportPath)")
+ }
+
+ /// Scans a couple of screens and uploads each result to the dashboard, showing the
+ /// common reporting options: a custom scan name and tags.
+ func testScanAndUploadToDashboard() throws {
+ let axe = try XCTUnwrap(axe, "axe DevTools didn't start — did you add your API key to Login.swift?")
+
+ app.goToTab("Home")
+ try axe.postResult(try axe.run(onElement: app), withScanName: "Home Tab")
+
+ app.goToTab("Catalog")
+ // Tags make a scan easy to find and share on the dashboard.
+ try axe.postResult(try axe.run(onElement: app), withTags: ["smoke", "catalog"], withScanName: "Catalog Tab")
+ }
+
+ /// Demonstrates gating a build on accessibility: fail if any CRITICAL issues are found.
+ func testHomeScreenHasNoCriticalIssues() throws {
+ let axe = try XCTUnwrap(axe, "axe DevTools didn't start — did you add your API key to Login.swift?")
+
+ app.goToTab("Home")
+ let result = try axe.run(onElement: app)
+ let criticalCount = result.failures.filter { $0.impact == .CRITICAL }.count
+ XCTAssertEqual(criticalCount, 0, "Found \(criticalCount) critical accessibility issue(s) on the Home screen.")
+ }
+}
diff --git a/axe-devtools-ios-sample-appUITests/axe_config.json b/axe-devtools-ios-sample-appUITests/axe_config.json
new file mode 100644
index 0000000..9e1fa3f
--- /dev/null
+++ b/axe-devtools-ios-sample-appUITests/axe_config.json
@@ -0,0 +1,8 @@
+{
+ "axeAutoScanMode": false,
+ "axeAppBundleId": "com.dequesystems.axe-devtools-ios-sample-app",
+ "axeMobileApiKey": "",
+ "axeProjectId": "",
+ "axeAccountUrl": "",
+ "axeUploadResults": true
+}