Skip to content

Commit 0341eb4

Browse files
authored
Card fields UI tests (#1820)
* fix(uicomponents): Add new CardFields UI Test File
1 parent 972ad96 commit 0341eb4

7 files changed

Lines changed: 207 additions & 0 deletions

File tree

Demo/Demo.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
/* Begin PBXBuildFile section */
1010
04DDF4622EE8AA1900FBDE49 /* PaymentButtonHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DDF4612EE8AA1200FBDE49 /* PaymentButtonHelper.swift */; };
1111
08222B07C311489F90D08D41 /* Pods_Demo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6D23244B5E9EE59BAB3F3003 /* Pods_Demo.framework */; };
12+
084C2C6E2FD85A960018BB01 /* CardFields_UITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 084C2C6D2FD85A960018BB01 /* CardFields_UITests.swift */; };
1213
086561662EF1D4EF00B16439 /* BraintreeUIComponents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 08A83F5A2EDF965E0076BD28 /* BraintreeUIComponents.framework */; };
1314
086561672EF1D4EF00B16439 /* BraintreeUIComponents.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 08A83F5A2EDF965E0076BD28 /* BraintreeUIComponents.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
1415
08A83F592EDE268E0076BD28 /* UIComponentsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A83F582EDE268E0076BD28 /* UIComponentsViewController.swift */; };
@@ -132,6 +133,7 @@
132133

133134
/* Begin PBXFileReference section */
134135
04DDF4612EE8AA1200FBDE49 /* PaymentButtonHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentButtonHelper.swift; sourceTree = "<group>"; };
136+
084C2C6D2FD85A960018BB01 /* CardFields_UITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardFields_UITests.swift; sourceTree = "<group>"; };
135137
08A83F582EDE268E0076BD28 /* UIComponentsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIComponentsViewController.swift; sourceTree = "<group>"; };
136138
08A83F5A2EDF965E0076BD28 /* BraintreeUIComponents.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = BraintreeUIComponents.framework; sourceTree = BUILT_PRODUCTS_DIR; };
137139
08A83F692EE9AB5A0076BD28 /* VenmoButton_UITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VenmoButton_UITests.swift; sourceTree = "<group>"; };
@@ -252,6 +254,14 @@
252254
/* End PBXFrameworksBuildPhase section */
253255

254256
/* Begin PBXGroup section */
257+
084C2C6C2FD85A740018BB01 /* CardFields UI Tests */ = {
258+
isa = PBXGroup;
259+
children = (
260+
084C2C6D2FD85A960018BB01 /* CardFields_UITests.swift */,
261+
);
262+
path = "CardFields UI Tests";
263+
sourceTree = "<group>";
264+
};
255265
08A83F682EE8D0F10076BD28 /* Payment Button UI Tests */ = {
256266
isa = PBXGroup;
257267
children = (
@@ -421,6 +431,7 @@
421431
A9B5ABDF24EB2A2200A4E1C8 /* UI Tests */ = {
422432
isa = PBXGroup;
423433
children = (
434+
084C2C6C2FD85A740018BB01 /* CardFields UI Tests */,
424435
08A83F682EE8D0F10076BD28 /* Payment Button UI Tests */,
425436
42456E3B25474B620018374E /* Helpers */,
426437
A9B5ABE224EB2A2200A4E1C8 /* Info.plist */,
@@ -732,6 +743,7 @@
732743
isa = PBXSourcesBuildPhase;
733744
buildActionMask = 2147483647;
734745
files = (
746+
084C2C6E2FD85A960018BB01 /* CardFields_UITests.swift in Sources */,
735747
A9C4E07F24EC2A0C002F6FF2 /* ThreeDSecure_UITests_Extensions.swift in Sources */,
736748
42456E3F25474B620018374E /* DateGenerator.swift in Sources */,
737749
08A83F6A2EE9AB5A0076BD28 /* VenmoButton_UITests.swift in Sources */,
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import XCTest
2+
3+
final class CardFields_UITests: XCTestCase {
4+
5+
var app = XCUIApplication()
6+
7+
override func setUp() {
8+
super.setUp()
9+
continueAfterFailure = false
10+
app.launchArguments.append("-EnvironmentSandbox")
11+
app.launchArguments.append("-ClientToken")
12+
app.launchArguments.append("-Integration:UIComponentsViewController")
13+
app.launchEnvironment["UITEST_DISABLE_ANIMATIONS"] = "YES"
14+
app.launch()
15+
}
16+
17+
// MARK: - Pay Button State
18+
19+
func testCardFields_payButtonInitiallyDisabled_enabledAfterValidInput() {
20+
let payButton = app.buttons["Pay"]
21+
XCTAssertTrue(payButton.waitForExistence(timeout: 5), "Pay button should be visible")
22+
XCTAssertFalse(payButton.isEnabled, "Pay button should be disabled when card fields are empty")
23+
24+
let cardNumberField = app.textFields["Card Number"]
25+
waitForElementToBeHittable(cardNumberField)
26+
cardNumberField.tap()
27+
cardNumberField.typeText("4111111111111111")
28+
29+
let expirationField = app.textFields["Expiration Date"]
30+
expirationField.tap()
31+
expirationField.typeText("1245")
32+
33+
let cvvField = app.textFields["CVV"]
34+
cvvField.tap()
35+
cvvField.typeText("123")
36+
37+
XCTAssertTrue(payButton.isEnabled, "Pay button should be enabled after valid card details are entered")
38+
}
39+
40+
// MARK: - Validation Errors
41+
42+
func testCardFields_showsError_whenInvalidDataIsEntered() {
43+
let cardNumberField = app.textFields["Card Number"]
44+
let expirationField = app.textFields["Expiration Date"]
45+
let cvvField = app.textFields["CVV"]
46+
47+
// Enter incomplete card number (10 of 16 digits), then advance focus
48+
waitForElementToBeHittable(cardNumberField)
49+
cardNumberField.tap()
50+
cardNumberField.typeText("4111111111")
51+
52+
expirationField.tap()
53+
XCTAssertTrue(
54+
app.staticTexts["Card number is invalid"].waitForExistence(timeout: 2),
55+
"Error should appear below card number field after leaving with incomplete number"
56+
)
57+
58+
// Advance to CVV without entering an expiration date
59+
cvvField.tap()
60+
XCTAssertTrue(
61+
app.staticTexts["Expiration date is required"].waitForExistence(timeout: 2),
62+
"Error should appear below expiration field after leaving it empty"
63+
)
64+
65+
// Enter only 2 CVV digits, then advance focus
66+
cvvField.typeText("12")
67+
expirationField.tap()
68+
XCTAssertTrue(
69+
app.staticTexts["CVV is invalid"].waitForExistence(timeout: 2),
70+
"Error should appear below CVV field after leaving with incomplete CVV"
71+
)
72+
73+
// Correct all fields and verify Pay button becomes enabled
74+
cardNumberField.tap(withNumberOfTaps: 3, numberOfTouches: 1)
75+
cardNumberField.typeText("4111111111111111")
76+
77+
expirationField.typeText("1245")
78+
79+
cvvField.tap(withNumberOfTaps: 3, numberOfTouches: 1)
80+
cvvField.typeText("123")
81+
82+
waitForElementToBeHittable(app.buttons["Pay"])
83+
84+
XCTAssertTrue(
85+
app.buttons["Pay"].isEnabled,
86+
"Pay button should be enabled after correcting all fields"
87+
)
88+
}
89+
90+
// MARK: - Card Brand Detection
91+
92+
func testCardFields_displaysVisaBrand_whenCardStartsWith4() {
93+
let cardNumberField = app.textFields["Card Number"]
94+
waitForElementToBeHittable(cardNumberField)
95+
cardNumberField.tap()
96+
cardNumberField.typeText("4")
97+
XCTAssertTrue(app.images["Visa"].waitForExistence(timeout: 2))
98+
}
99+
100+
func testCardFields_displaysMastercardBrand_whenCardStartsWith51() {
101+
let cardNumberField = app.textFields["Card Number"]
102+
waitForElementToBeHittable(cardNumberField)
103+
cardNumberField.tap()
104+
cardNumberField.typeText("51")
105+
XCTAssertTrue(app.images["Mastercard"].waitForExistence(timeout: 2))
106+
}
107+
108+
func testCardFields_displaysAmexBrand_whenCardStartsWith34() {
109+
let cardNumberField = app.textFields["Card Number"]
110+
waitForElementToBeHittable(cardNumberField)
111+
cardNumberField.tap()
112+
cardNumberField.typeText("34")
113+
XCTAssertTrue(app.images["American Express"].waitForExistence(timeout: 2))
114+
}
115+
116+
func testCardFields_displaysDiscoverBrand_whenCardStartsWith6011() {
117+
let cardNumberField = app.textFields["Card Number"]
118+
waitForElementToBeHittable(cardNumberField)
119+
cardNumberField.tap()
120+
cardNumberField.typeText("6011")
121+
XCTAssertTrue(app.images["Discover"].waitForExistence(timeout: 2))
122+
}
123+
124+
func testCardFields_displaysUnionPayBrand_whenCardStartsWith620() {
125+
let cardNumberField = app.textFields["Card Number"]
126+
waitForElementToBeHittable(cardNumberField)
127+
cardNumberField.tap()
128+
cardNumberField.typeText("620")
129+
XCTAssertTrue(app.images["UnionPay"].waitForExistence(timeout: 2))
130+
}
131+
132+
// MARK: - CVV Length Changes With Brand
133+
134+
func testCardFields_showsCVVError_whenSwitchingFromAmexToVisa() {
135+
let cardNumberField = app.textFields["Card Number"]
136+
let expirationField = app.textFields["Expiration Date"]
137+
let cvvField = app.textFields["CVV"]
138+
139+
waitForElementToBeHittable(cardNumberField)
140+
cardNumberField.tap()
141+
cardNumberField.typeText("378282246310005")
142+
143+
expirationField.typeText("1245")
144+
145+
cvvField.tap()
146+
cvvField.typeText("1234")
147+
148+
cardNumberField.tap(withNumberOfTaps: 3, numberOfTouches: 1)
149+
cardNumberField.typeKey(.delete, modifierFlags: [])
150+
waitForElementToBeHittable(cardNumberField)
151+
cardNumberField.typeText("4111111111111111")
152+
153+
cvvField.tap()
154+
expirationField.tap()
155+
156+
XCTAssertTrue(
157+
app.staticTexts["CVV is invalid"].waitForExistence(timeout: 2),
158+
"CVV error should appear when a 4-digit Amex CVV is used after switching to a Visa card"
159+
)
160+
161+
cvvField.tap(withNumberOfTaps: 3, numberOfTouches: 1)
162+
cvvField.typeText("123")
163+
164+
waitForElementToBeHittable(app.buttons["Pay"])
165+
166+
XCTAssertTrue(
167+
app.buttons["Pay"].isEnabled,
168+
"Pay button should be enabled after correcting CVV to 3 digits for Visa"
169+
)
170+
}
171+
}

Sources/BraintreeUIComponents/CardFields/CVVField/CVVFieldView.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ struct CVVFieldView: View {
3838
.foregroundColor(.clear)
3939
.tint(Color(.label))
4040
.font(.system(size: 16))
41+
.accessibilityLabel("CVV")
42+
.accessibilityHint("3 or 4-digit security code")
4143
.onChange(of: textFieldText) { _, newValue in
4244
let digits = String(newValue.filter { $0.isNumber }.prefix(viewModel.maxLength))
4345
if digits != textFieldText {

Sources/BraintreeUIComponents/CardFields/CardNumberField/CardBrandView.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ struct CardBrandView: View {
1313
brand.image
1414
.resizable()
1515
.scaledToFit()
16+
.accessibilityLabel(brand.displayName)
1617
}
1718
}
1819
}

Sources/BraintreeUIComponents/CardFields/CardNumberField/CardNumberFieldView.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ struct CardNumberFieldView: View {
2525
.focused($isFocused)
2626
.font(.system(size: 16))
2727
.foregroundColor(Color(.label))
28+
.accessibilityLabel("Card Number")
2829
.onChange(of: textFieldText) { _, newValue in
2930
let digits = String(newValue.filter { $0.isNumber }.prefix(viewModel.maxLength))
3031
let formatted = viewModel.formatted(digits: digits)

Sources/BraintreeUIComponents/CardFields/ExpirationDateField/ExpirationDateFieldView.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ struct ExpirationDateFieldView: View {
2727
.focused($isFocused)
2828
.font(.body)
2929
.foregroundColor(Color(.label))
30+
.accessibilityLabel("Expiration Date")
31+
.accessibilityHint("Enter in MM/YY format")
3032
.onChange(of: textFieldText) { _, newValue in
3133
var digits = String(newValue.filter { $0.isNumber }.prefix(viewModel.maxLength))
3234
if digits.count == 1, let digit = digits.first?.wholeNumberValue, digit >= 2 {

Sources/BraintreeUIComponents/CardFields/Shared/CardBrand.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,24 @@ enum CardBrand: CaseIterable {
103103
self == .amex ? 4 : 3
104104
}
105105

106+
// MARK: - Display
107+
108+
var displayName: String {
109+
switch self {
110+
case .visa: return "Visa"
111+
case .mastercard: return "Mastercard"
112+
case .amex: return "American Express"
113+
case .discover: return "Discover"
114+
case .jcb: return "JCB"
115+
case .dinersClub: return "Diners Club"
116+
case .unionPay: return "UnionPay"
117+
case .maestro: return "Maestro"
118+
case .hiper: return "Hiper"
119+
case .hipercard: return "Hipercard"
120+
case .unknown: return "Credit card"
121+
}
122+
}
123+
106124
// MARK: - Image
107125

108126
var image: Image {

0 commit comments

Comments
 (0)