Skip to content

Commit 00cf6e3

Browse files
authored
Add SwiftUI pill bar XCUITests (#2262)
* Add basic XCUITests * Some cleanup * some UI tests * Remove unnecessary comments * Revert provisioning changes
1 parent 0dcfd55 commit 00cf6e3

4 files changed

Lines changed: 239 additions & 54 deletions

File tree

Demos/FluentUIDemo_iOS/FluentUI.Demo.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
6F453CA528AC536300ED91A4 /* ShadowTokensDemoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F453CA428AC536300ED91A4 /* ShadowTokensDemoController.swift */; };
7777
6F4A92EF2DD268BA00B4B136 /* PillButtonDemoController_SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F4A92EE2DD268A600B4B136 /* PillButtonDemoController_SwiftUI.swift */; };
7878
6FC23D8C2E5443D40052A889 /* PillButtonBarDemoController_SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FC23D8B2E5443D40052A889 /* PillButtonBarDemoController_SwiftUI.swift */; };
79+
6FC37A5F2EA9978C00351E52 /* PillButtonBarTest_SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FC37A5E2EA9977F00351E52 /* PillButtonBarTest_SwiftUI.swift */; };
7980
6FC8AD3B28DBAF280010C0F8 /* ReadmeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FC8AD3A28DBAF280010C0F8 /* ReadmeViewController.swift */; };
8081
6FEED93B28A6E5520099D178 /* AliasColorTokensDemoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FEED93A28A6E5520099D178 /* AliasColorTokensDemoController.swift */; };
8182
7D0931C124AAA3D30072458A /* SideTabBarDemoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D0931C024AAA3D30072458A /* SideTabBarDemoController.swift */; };
@@ -219,6 +220,7 @@
219220
6F453CA428AC536300ED91A4 /* ShadowTokensDemoController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowTokensDemoController.swift; sourceTree = "<group>"; };
220221
6F4A92EE2DD268A600B4B136 /* PillButtonDemoController_SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillButtonDemoController_SwiftUI.swift; sourceTree = "<group>"; };
221222
6FC23D8B2E5443D40052A889 /* PillButtonBarDemoController_SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillButtonBarDemoController_SwiftUI.swift; sourceTree = "<group>"; };
223+
6FC37A5E2EA9977F00351E52 /* PillButtonBarTest_SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillButtonBarTest_SwiftUI.swift; sourceTree = "<group>"; };
222224
6FC8AD3A28DBAF280010C0F8 /* ReadmeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadmeViewController.swift; sourceTree = "<group>"; };
223225
6FEED93A28A6E5520099D178 /* AliasColorTokensDemoController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AliasColorTokensDemoController.swift; sourceTree = "<group>"; };
224226
7D0931C024AAA3D30072458A /* SideTabBarDemoController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SideTabBarDemoController.swift; sourceTree = "<group>"; };
@@ -391,6 +393,7 @@
391393
3A83F8E62953B9D100EF6629 /* NavigationControllerTest.swift */,
392394
3A83F8E82953BA4500EF6629 /* NotificationViewTest.swift */,
393395
3A83F8EA2953BA5400EF6629 /* NotificationViewTest_SwiftUI.swift */,
396+
6FC37A5E2EA9977F00351E52 /* PillButtonBarTest_SwiftUI.swift */,
394397
3A83F8EC2953BA6B00EF6629 /* OtherCellsTest.swift */,
395398
3A83F8EE2953BA7C00EF6629 /* PeoplePickerTest.swift */,
396399
3A83F8F02953BA8900EF6629 /* PersonaButtonCarouselTest.swift */,
@@ -767,6 +770,7 @@
767770
files = (
768771
3A83F8E12953B99A00EF6629 /* IndeterminateProgressBarTest.swift in Sources */,
769772
3A83F8FB2953BAC500EF6629 /* SearchBarTest.swift in Sources */,
773+
6FC37A5F2EA9978C00351E52 /* PillButtonBarTest_SwiftUI.swift in Sources */,
770774
3A83F8EF2953BA7C00EF6629 /* PeoplePickerTest.swift in Sources */,
771775
EC0A83E4299ED3630010ED3E /* TextFieldTest.swift in Sources */,
772776
3A83F8F52953BAA200EF6629 /* PillButtonTest.swift in Sources */,

Demos/FluentUIDemo_iOS/FluentUI.Demo/Demos/PillButtonBarDemoController_SwiftUI.swift

Lines changed: 85 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -29,61 +29,52 @@ private struct PillButtonBarDemoView: View {
2929
ScrollView(.vertical) {
3030
VStack(spacing: 30) {
3131
VStack(spacing: 20) {
32-
Text("onBrand bar")
33-
.multilineTextAlignment(.center)
34-
.foregroundStyle(fluentTheme.swiftUIColor(.foreground1))
35-
PillButtonBarView(style: .onBrand,
36-
viewModels: indexSelectionViewModels,
37-
selected: $onBrandSelectedIndex,
38-
centerAlignIfContentFits: true,
39-
tokenOverrides: tokenOverrides)
40-
.disabled(disablePills)
41-
.background {
42-
fluentTheme.swiftUIColor(.brandBackground1)
32+
PillBarDemoSection(title: "onBrand bar") {
33+
PillButtonBarView(style: .onBrand,
34+
viewModels: indexSelectionViewModels,
35+
selected: $onBrandSelectedIndex,
36+
centerAlignIfContentFits: true,
37+
tokenOverrides: tokenOverrides)
38+
.disabled(disablePills)
39+
.background {
40+
fluentTheme.swiftUIColor(.brandBackground1)
41+
}
42+
}
43+
44+
PillBarDemoSection(title: "Primary bar") {
45+
PillButtonBarView(style: .primary,
46+
viewModels: titleSelectionViewModels,
47+
selected: $primarySelectedTitle,
48+
centerAlignIfContentFits: true,
49+
tokenOverrides: tokenOverrides)
50+
.disabled(disablePills)
51+
}
52+
53+
PillBarDemoSection(title: "Bar with deselection",
54+
subtitle: "This pill button bar supports having no selected pill button. If the currently selected pill button is tapped, it will be deselected.") {
55+
PillButtonBarView(style: .primary,
56+
viewModels: titleDeselectionViewModels,
57+
selected: $deselectionBarTitle,
58+
tokenOverrides: tokenOverrides)
59+
.disabled(disablePills)
60+
}
61+
62+
PillBarDemoSection(title: "Leading aligned") {
63+
PillButtonBarView(style: .primary,
64+
viewModels: titleSelectionLeadingViewModels,
65+
selected: $leadingAlignedBarSelectedTitle,
66+
tokenOverrides: tokenOverrides)
67+
.disabled(disablePills)
68+
}
69+
70+
PillBarDemoSection(title: "Center aligned") {
71+
PillButtonBarView(style: .primary,
72+
viewModels: titleSelectionCenterViewModels,
73+
selected: $centerAlignedBarSelectedTitle,
74+
centerAlignIfContentFits: true,
75+
tokenOverrides: tokenOverrides)
76+
.disabled(disablePills)
4377
}
44-
45-
Text("Primary bar")
46-
.foregroundStyle(fluentTheme.swiftUIColor(.foreground1))
47-
.multilineTextAlignment(.center)
48-
PillButtonBarView(style: .primary,
49-
viewModels: titleSelectionViewModels,
50-
selected: $primarySelectedTitle,
51-
centerAlignIfContentFits: true,
52-
tokenOverrides: tokenOverrides)
53-
.disabled(disablePills)
54-
55-
Text("Bar with deselection")
56-
.multilineTextAlignment(.center)
57-
.foregroundStyle(fluentTheme.swiftUIColor(.foreground1))
58-
Text("This pill button bar supports having no selected pill button. If the currently selected pill button is tapped, it will be deselected.")
59-
.foregroundStyle(fluentTheme.swiftUIColor(.foreground1))
60-
.multilineTextAlignment(.center)
61-
.padding(.horizontal)
62-
.frame(maxWidth: .infinity, alignment: .center)
63-
.fixedSize(horizontal: false, vertical: true)
64-
.font(.caption)
65-
PillButtonBarView(style: .primary,
66-
viewModels: titleDeselectionViewModels,
67-
selected: $deselectionBarTitle,
68-
tokenOverrides: tokenOverrides)
69-
.disabled(disablePills)
70-
71-
Text("Leading aligned")
72-
.foregroundStyle(fluentTheme.swiftUIColor(.foreground1))
73-
PillButtonBarView(style: .primary,
74-
viewModels: titleSelectionLeadingViewModels,
75-
selected: $leadingAlignedBarSelectedTitle,
76-
tokenOverrides: tokenOverrides)
77-
.disabled(disablePills)
78-
79-
Text("Center aligned")
80-
.foregroundStyle(fluentTheme.swiftUIColor(.foreground1))
81-
PillButtonBarView(style: .primary,
82-
viewModels: titleSelectionCenterViewModels,
83-
selected: $centerAlignedBarSelectedTitle,
84-
centerAlignIfContentFits: true,
85-
tokenOverrides: tokenOverrides)
86-
.disabled(disablePills)
8778
}
8879
.fluentTheme(theme)
8980
}
@@ -202,3 +193,43 @@ private struct PillButtonBarDemoView: View {
202193
static let leadingImage = Image(systemName: "circle.fill")
203194
}
204195
}
196+
197+
private struct PillBarDemoSection<PillButtonBar: View>: View {
198+
let title: String
199+
let subtitle: String?
200+
let titleColor: Color?
201+
let pillBar: () -> PillButtonBar
202+
203+
fileprivate init(title: String,
204+
subtitle: String? = nil,
205+
titleColor: Color? = nil,
206+
pillBar: @escaping () -> PillButtonBar) {
207+
self.title = title
208+
self.subtitle = subtitle
209+
self.titleColor = titleColor
210+
self.pillBar = pillBar
211+
}
212+
213+
var body: some View {
214+
VStack(spacing: 20) {
215+
Text(title)
216+
.multilineTextAlignment(.center)
217+
.foregroundStyle(fluentTheme.swiftUIColor(.foreground1))
218+
219+
if let subtitle {
220+
Text(subtitle)
221+
.foregroundStyle(fluentTheme.swiftUIColor(.foreground1))
222+
.multilineTextAlignment(.center)
223+
.padding(.horizontal)
224+
.frame(maxWidth: .infinity, alignment: .center)
225+
.fixedSize(horizontal: false, vertical: true)
226+
.font(.caption)
227+
}
228+
229+
pillBar()
230+
.accessibilityIdentifier(title)
231+
}
232+
}
233+
234+
@Environment(\.fluentTheme) private var fluentTheme: FluentTheme
235+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
//
2+
// Copyright (c) Microsoft Corporation. All rights reserved.
3+
// Licensed under the MIT License.
4+
//
5+
6+
import XCTest
7+
8+
class PillButtonBarTestSwiftUI: BaseTest {
9+
override var controlName: String { "PillButtonBar" }
10+
11+
override func setUpWithError() throws {
12+
try super.setUpWithError()
13+
app.buttons["Show"].firstMatch.tap()
14+
}
15+
16+
func testLaunch() throws {
17+
XCTAssert(app.navigationBars.element(matching: NSPredicate(format: "identifier CONTAINS %@", "Pill Button Bar (SwiftUI)")).exists)
18+
}
19+
20+
func testButtons() throws {
21+
let pillBarNames = [
22+
"onBrand bar",
23+
"Primary bar",
24+
"Bar with deselection",
25+
"Leading aligned",
26+
"Center aligned"
27+
]
28+
29+
for pillBarName in pillBarNames {
30+
try testButtonsInPillBar(pillBarName: pillBarName)
31+
}
32+
try enableAllToggles()
33+
}
34+
35+
private func testButtonsInPillBar(pillBarName: String) throws {
36+
print("\n========== Testing pill bar: \(pillBarName) ==========")
37+
38+
// Find the main vertical scroll view (element 0) and the pill bar scroll view (should have identifier)
39+
let mainScrollView = app.scrollViews.element(boundBy: 0)
40+
XCTAssertTrue(mainScrollView.waitForExistence(timeout: 2), "Main scroll view not found")
41+
42+
// The PillButtonBarView is implemented as a ScrollView, so query it as such
43+
let pillBar = app.scrollViews[pillBarName]
44+
45+
// Scroll down the main view to ensure the pill bar is visible and hittable
46+
var scrollAttempts = 0
47+
while (!pillBar.exists || !pillBar.isHittable) && scrollAttempts < 11 {
48+
mainScrollView.swipeUp()
49+
scrollAttempts += 1
50+
usleep(500000)
51+
}
52+
53+
XCTAssertTrue(pillBar.waitForExistence(timeout: 5), "Pill bar '\(pillBarName)' not found")
54+
XCTAssertTrue(pillBar.isHittable, "Pill bar '\(pillBarName)' is not hittable")
55+
56+
// Get all buttons within the pill bar scroll view
57+
let buttons = pillBar.buttons.allElementsBoundByIndex
58+
59+
print("Number of buttons in '\(pillBarName)' = \(buttons.count)")
60+
XCTAssertFalse(buttons.isEmpty, "No buttons found in pill bar '\(pillBarName)'")
61+
62+
// Scroll the pill bar to the beginning (right) to ensure the first button is visible
63+
// Some pill bars may have a non-first button selected, causing the first button to be off-screen
64+
if buttons.count > 2 {
65+
var resetAttempts = 0
66+
while resetAttempts < 5 {
67+
let firstFrame = buttons[0].frame
68+
let barFrame = pillBar.frame
69+
// Stop once the first button's left edge is within the pill bar's visible area
70+
if firstFrame.width > 0 && firstFrame.minX >= barFrame.minX - 5 {
71+
break
72+
}
73+
pillBar.swipeRight()
74+
usleep(200000)
75+
resetAttempts += 1
76+
}
77+
if resetAttempts > 0 {
78+
usleep(300000)
79+
}
80+
}
81+
82+
// Test each button
83+
for (index, button) in buttons.enumerated() {
84+
print("Testing button \(index + 1) of \(buttons.count): '\(button.label)'")
85+
XCTAssertTrue(button.waitForExistence(timeout: 2), "Button \(index + 1) does not exist")
86+
87+
// Only scroll if the button is NOT already hittable
88+
if !button.isHittable {
89+
print("Button \(index + 1) is not hittable, scrolling into view...")
90+
var attempts = 0
91+
let maxAttempts = 8
92+
93+
while !button.isHittable && attempts < maxAttempts {
94+
// Ensure pill bar is hittable
95+
if !pillBar.isHittable {
96+
print("Pill bar not hittable, scrolling main view...")
97+
mainScrollView.swipeUp()
98+
usleep(300000)
99+
} else if buttons.count > 2 {
100+
print("Swiping to reveal button \(index + 1) (attempt \(attempts + 1))...")
101+
pillBar.swipeLeft()
102+
usleep(300000)
103+
attempts += 1
104+
}
105+
}
106+
107+
XCTAssertTrue(button.isHittable, "Button \(index + 1) '\(button.label)' is not hittable after \(attempts) attempts")
108+
} else {
109+
print("Button \(index + 1) is already hittable, tapping directly")
110+
}
111+
112+
// Tap the button
113+
button.tap()
114+
115+
// Dismiss alert if it appears
116+
let alertButton = app.buttons["OK"].firstMatch
117+
if alertButton.waitForExistence(timeout: 1) {
118+
alertButton.tap()
119+
}
120+
}
121+
122+
print("========== Completed testing pill bar: \(pillBarName) ==========\n")
123+
}
124+
125+
private func enableAllToggles() throws {
126+
let customThemeToggle = app.switches["Toggle custom theme"].switches.firstMatch
127+
let tokenOverridesToggle = app.switches["Toggle token overrides"].switches.firstMatch
128+
let disablePillsToggle = app.switches["Disable pills"].switches.firstMatch
129+
130+
XCTAssertTrue(customThemeToggle.waitForExistence(timeout: 5), "Toggle custom theme not found")
131+
XCTAssertTrue(tokenOverridesToggle.waitForExistence(timeout: 5), "Toggle token overrides not found")
132+
XCTAssertTrue(disablePillsToggle.waitForExistence(timeout: 5), "Disable pills toggle not found")
133+
134+
if customThemeToggle.value as? String == "0" {
135+
customThemeToggle.tap()
136+
}
137+
if tokenOverridesToggle.value as? String == "0" {
138+
tokenOverridesToggle.tap()
139+
}
140+
if disablePillsToggle.value as? String == "0" {
141+
disablePillsToggle.tap()
142+
}
143+
144+
XCTAssertEqual(customThemeToggle.value as? String, "1", "Custom theme toggle should be on")
145+
XCTAssertEqual(tokenOverridesToggle.value as? String, "1", "Token overrides toggle should be on")
146+
XCTAssertEqual(disablePillsToggle.value as? String, "1", "Disable pills toggle should be on")
147+
}
148+
}

Sources/FluentUI_iOS/Components/PillButtonBar/SwiftUI/PillButtonBarView.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,10 @@ public struct PillButtonBarView<Selection: Hashable>: View {
162162
}
163163
.allowsHitTesting(!ignoreTap && isEnabled)
164164
.id(value)
165+
.accessibilityAddTraits(.isButton)
165166
.accessibilityAddTraits(isSelected ? .isSelected : [])
166167
.accessibilityLabel(viewModel.isUnread ? String(format: "Accessibility.TabBarItemView.UnreadFormat".localized, title) : title)
168+
.accessibilityIdentifier(title)
167169
.showsLargeContentViewer(text: title)
168170
}
169171

0 commit comments

Comments
 (0)