{"id":13537024,"url":"https://github.com/soundcloud/Axt","last_synced_at":"2025-04-02T03:31:32.939Z","repository":{"id":54638936,"uuid":"510681563","full_name":"soundcloud/Axt","owner":"soundcloud","description":"SwiftUI view testing library","archived":false,"fork":false,"pushed_at":"2022-11-04T15:23:17.000Z","size":646,"stargazers_count":175,"open_issues_count":0,"forks_count":1,"subscribers_count":44,"default_branch":"master","last_synced_at":"2025-03-18T15:18:31.376Z","etag":null,"topics":["swift","swiftui","testing"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/soundcloud.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-07-05T10:05:17.000Z","updated_at":"2025-02-14T13:49:07.000Z","dependencies_parsed_at":"2022-08-13T22:30:43.955Z","dependency_job_id":null,"html_url":"https://github.com/soundcloud/Axt","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soundcloud%2FAxt","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soundcloud%2FAxt/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soundcloud%2FAxt/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soundcloud%2FAxt/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/soundcloud","download_url":"https://codeload.github.com/soundcloud/Axt/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246751251,"owners_count":20827857,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["swift","swiftui","testing"],"created_at":"2024-08-01T09:00:53.704Z","updated_at":"2025-04-02T03:31:32.248Z","avatar_url":"https://github.com/soundcloud.png","language":"Swift","funding_links":[],"categories":["Samples"],"sub_categories":[],"readme":"# 🪓 Axt \n\n![](https://user-images.githubusercontent.com/13484323/185608030-21c45ddc-f90b-42e9-a8ac-e855bb090aea.svg)\n![](https://user-images.githubusercontent.com/13484323/185608132-f90bd70e-4518-404d-9ba7-c24739f7c2b2.svg)\n![](https://user-images.githubusercontent.com/13484323/200010051-3270dd90-1edd-42ff-b94f-cb8ba3618a4e.svg)\n\nAxt is a testing library for SwiftUI.\n\nUnit tests using Axt can interact with SwiftUI views, which are running live in the simulator and are in a fully functional state.\n\n```swift\nstruct MyView: View {\n    @State var showMore = false\n\n    var body: some View {\n        VStack {\n            Toggle(\"Show more\", isOn: $showMore)\n                .testId(\"show_more_toggle\", type: .toggle)\n            if showMore {\n                Text(\"More\")\n                    .testId(\"more_text\", type: .text)\n            }\n        }\n    }\n}\n```\n\n```swift\n@MainActor\nclass MyViewTests: XCTestCase {\n    func testShowMore() async throws {\n        let test = await AxtTest.host(MyView())\n        let showMoreToggle = test.find(id: \"show_more_toggle\")\n\n        await showMoreToggle?.performAction()\n\n        XCTAssertEqual(showMoreToggle?.value as? Bool, true)\n        XCTAssertEqual(test.find(id: \"more_text\")?.label, \"More\")\n    }\n}\n```\n\n## Getting started\n\nFollow the steps below to add Axt to an existing project. Note that Axt should be used with unit test targets, and not with UI test targets.\n\n1. Add the Axt Swift package as a dependency to your Xcode project.\n2. Link both your app target and unit test target to the Axt library. If the project is built for release, it will only contain stubs for Axt and no inspection code.\n3. Make sure your unit test target has a host application. We need some app to host the views to test, but the views do not need to be part of this host application.\n\n## Documentation \n\n### Exposing views\n\nTo expose a view, you give it an identifier with the `testId` modifier.\n\nTake this list of toggles, and notice the `toggle_1`, `show_more` and `toggle_2` identifiers.\n\n```swift\nList {\n    Toggle(\"1\", isOn: $value1)\n        .testId(\"toggle_1\", type: .toggle)\n    Toggle(\"Show more\", isOn: $showMore)\n        .testId(\"show_more\", type: .toggle)\n    if showMore {\n        Toggle(\"2\", isOn: $value2)\n            .testId(\"toggle_2\", type: .toggle)\n    }\n}\n.testId(\"toggle_list\")\n```\n\nThis will be exposed to the tests as below.\n\n```\n→ app\n  → toggle_list\n    → toggle_1 label=\"1\" value=false action\n    → show_more label=\"Show more\" value=false action\n```\n\nThere are different ways to expose views to unit tests, depending on whether they are built-in or custom views. You can also attach Axt elements without explicit child views to a view.\n\n#### Native views\n\nTo enable Axt on native SwiftUI views, you need to tell Axt what kind of view it needs to look for. The following built-in views are supported.\n\n##### Button\n\n```swift\nButton(\"Tap me\") { tap() }\n    .testId(\"tap_button\", type: .button)\n```\n\n```\n→ tap_button label=\"Tap me\" action\n```\n\n##### Toggle\n\n```swift\nToggle(\"Toggle me\", isOn: $isOn)\n    .testId(\"is_on_toggle\", type: .toggle)\n```\n\n```\n→ is_on_toggle label=\"Toggle me\" value=true action\n```\n\n##### NavigationLink\n\n```swift\nNavigationLink(\"More\", destination: Destination())\n    .testId(\"more_link\", type: .navigationLink)\n```\n\n```\n→ more_link label=\"More\" action\n```\n\n##### TextField\n\n```swift\nTextField(\"Name\", text: $name)\n    .testId(\"name_field\", type: .textField)\n```\n\n```\n→ name_field label=\"Name\" value=\"\" action\n```\n\n#### Custom views\n\nFor custom views, you can specify values or functionality manually to expose them to views.\n\n```swift\nColor.blue.frame(width: 50, height: 50)\n    .testId(\"color_1\", value: \"blue\")\nColor.red.frame(width: 50, height: 50)\n    .testId(\"color_2\", value: \"red\")\n```\n\nThese can now be accessed from tests.\n\n```\n→ app\n  → color_1 value=blue\n  → color_2 value=red\n```\n\nYou can also add closures to perform from tests (using the `action` parameter) or a way to set a value (using the `setValue` parameter).\n\n#### Re-usable controls\n\nIt is common to want to specify values or functionality for re-usable controls, but allow clients to set the test identifier or override values or functionality. This would be the case for custom buttons or search bars. For this, use the `testData` modifier.\n\n```swift\nstruct MyButton: View {\n    let action: () -\u003e Void\n\n    var body: some View {\n        Button(\"Tap me!\") { action() }\n            .testData(action: action)\n    }\n}\n\nMyButton(action: action)\n    .testId(\"my_button\")\n```\n\nThere will only be a single element for this button exposed to the tests.\n\n```\n→ app\n  → my_button action\n```\n\nUsing the `testData` modifier only results in an element exposed to tests, if an identifier is provided somewhere higher up in the view hierarchy.\n\nDo not use the `testId(:type:)` modifiers for native views on custom controls. For custom controls, extracting data from views is not necessary.\n\n#### Inserting extra elements\n\nSometimes it can be useful to insert Axt elements that do not correspond to a SwiftUI view. This can be useful to expose buttons that are handled in UIKit, or to interact with gestures or other objects that are not views, or provide an easy way to interact with view state when testing a view modifier.\n\nFor example, here is how we can expose the contents of an alert.\n\n```swift\ncontent.alert(isPresented: $isPresented) {\n    Alert(\n        title: Text(message),\n        primaryButton: .default(Text(\"1\"), action: action1),\n        secondaryButton: .default(Text(\"2\"), action: action2))\n}\n.testId(insert: \"button_1\", when: isPresented, label: \"1\", action: action1)\n.testId(insert: \"button_2\", when: isPresented, label: \"2\", action: action2)\n```\n\nThe elements will be exposed as siblings.\n\n```\n→ app\n  → button_1 label=\"1\" action\n  → button_2 label=\"2\" action\n```\n\nAnd here we expose a drag gesture to be testable.\n\n```swift\n@State private var dragY: CGFloat = 0\n\nvar body: some View {\n    knob\n        .frame(width: 50, height: 50)\n        .offset(x: 0, y: dragY)\n        .gesture(gesture)\n        .testId(insert: \"drag\", value: dragY, setValue: { dragY = $0 as? CGFloat ?? 0 })\n}\n```\n\n```\n→ app\n  → drag value=0.0\n```\n\n#### Sheets\n\nPreferences that are set on the contents of a SwiftUI sheet are never transferred to the view presenting the sheet. You can still expose contents of a sheet, but this should be a last resort. Use the following code to add a new `AxtTest` to the `AxtTest.sheets` variable.\n\n```swift\nButton(\"...\") { isPresented = true }\n    .sheet(isPresented: $isPresented) {\n        MoreMenu()\n            .hostAxtSheet()\n    }\n```\n\n### Writing tests\n\nThe first step to writing an Axt test is to create an asynchronous test method, and to host an Axt test with the view.\n\n```swift\nfunc test_myView() async {\n  let test = await AxtTest.host(MyView())\n  // ...\n```\n\nIn addition to creating the test, this will also display `MyView` in the simulator or iPhone. It will be displayed with a red border around it, to indicite that it is presented by Axt and distinguish it from the rest of the app contents.\n\n#### Watch the hierarchy\n\nAs a first step, we can watch view updates in the console.\n\n```swift\nawait test.watchHierarchy()\n```\n\nRunning this test prints the current view hierarchy in the console. The view is also interactive. If you interact with the view, a new view hierarchy will be printed in the console any time it changes.\n\n#### Finding views\n\nThe `test` we created before is also an Axt element, namely the root element. If you have an element, you can use it to search for other elements.\n\nYou can use the `find(id: \"my_button\")` method to recursively search for an element with id `my_button`, or `findAll(id: \"my_button\")` to get an array of all the elements with this id.\n\n```swift\nlet myButton = try XCTUnwrap(test.find(id: \"my_button\"))\n```\n\nYou can also get the direct children of an element using the `children` method. To recursively get all elements underneath another element, use the `all` property instead.\n\n#### Assert on elements\n\nYou can check if an Axt element (still) exists (`exists`). It has an identifier given to it through the `testId` modifier (`id`), and optionally a label (`label`), value (`value`), way to perform an action (`performAction()`), and way to set the value (`setValue`).\n\nFor any Axt element, you can use `await element.watchHierarchy()` to see how the hierarchy changes while interacting with it in the simulator or on your iPhone.\n\n#### The lifetime of Axt elements\n\nAn Axt element points to a view that is exposed to Axt by the methods presented before, but it differs to a view in that it is a reference type. If a view is re-evaluated, an Axt element that points to that view will be updated, but the same object. The Axt element will track changes in the view. That means you can store an Axt element, make changes to the SwiftUI state, and then check the Axt element again.\n\n```swift\nlet test = await AxtTest.host(MyView())\nlet label = try XCTUnwrap(test.find(id: \"my_label\")\nlet toggle = try XCTUnwrap(test.find(id: \"my_toggle\"))\n\nXCTAssertEqual(label.value as? String, \"yes\")\n\nawait toggle.performAction()\n\nXCTAssertEqual(label.value as? String, \"no\")\n```\n\n#### Waiting for view updates\n\nIf you change the state of a variable in a SwiftUI view, for example by performing an action on a control or changing a value, SwiftUI will trigger a re-evaluation of your view. However, SwiftUI does not re-evaluate the view immediately. This is done for efficiency reasons. Therefore, you cannot make an assertion immediately after changing state.\n\nIf you expect an update to happen after an action immediately after the current run loop cycle, use `performAction()`. If you don't want to give SwiftUI the time to update the views, use `performActionWithoutYielding()` instead. You can then give SwiftUI the time to update the views by calling `AxtTest.yield()`.\n\n```swift\nlet test = await AxtTest.host(TogglesView())\nlet moreToggle = try XCTUnwrap(test.find(id: \"show_more\"))\n\nmoreToggle.performActionWithoutYielding()\nawait AxtTest.yield()\n\nXCTAssertNotNil(test.find(id: \"toggle_2\"))\n```\n\nIf you expect that it might take longer for the view hierarchy to update, for example because the changes are animated, you can use the `waitFor` functions on Axt elements. These functions are efficient, because they only check for changes when the view hierarchy was changed.\n\n```swift\nlet test = await AxtTest.host(TogglesView())\nlet moreToggle = try XCTUnwrap(test.find(id: \"show_more\"))\n\nawait moreToggle.performAction()\n\nXCTAssertNotNil(try await test.waitForElement(id: \"toggle_2\", timeout: 1))\n```\n\nThere is also `waitForCondition` to wait for any boolean condition, and `waitForUpdate` that returns as soon as anything in the view hierarchy is changed.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsoundcloud%2FAxt","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsoundcloud%2FAxt","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsoundcloud%2FAxt/lists"}