Skip to content

Commit 8883541

Browse files
authored
Make ValidationWrapper view more precise (#2)
* Make ValidationWrapper more precise * Fix flaky test * Fix test xcwaiter validation expectations fulfilling
1 parent afa6fad commit 8883541

7 files changed

Lines changed: 214 additions & 47 deletions

File tree

Sources/CombineValidate/Publishers/Publishers+Validators.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public func NotEmptyValidator(
88
) -> ValidationPublisher {
99
publisher
1010
.dropFirst()
11-
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
11+
.debounce(for: .seconds(0.25), scheduler: RunLoop.main)
1212
.map(\.isEmpty)
1313
.map { !$0 ? .success(.none) : .failure(reason: message, tableName: tableName) }
1414
.eraseToAnyPublisher()
@@ -22,7 +22,7 @@ public func RegexValidator<Pattern>(
2222
) -> ValidationPublisher where Pattern: RegexProtocol {
2323
publisher
2424
.dropFirst()
25-
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
25+
.debounce(for: .seconds(0.25), scheduler: RunLoop.main)
2626
.map { pattern.test($0) }
2727
.map { $0 ? .success(.none) : .failure(reason: message, tableName: tableName) }
2828
.eraseToAnyPublisher()
@@ -36,7 +36,7 @@ public func OneOfRegexValidator<Pattern>(
3636
) -> RichValidationPublisher<Pattern> where Pattern: RegexProtocol {
3737
publisher
3838
.dropFirst()
39-
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
39+
.debounce(for: .seconds(0.25), scheduler: RunLoop.main)
4040
.map { value in
4141
for pattern in patterns {
4242
if pattern.test(value) {
@@ -56,7 +56,7 @@ public func MultiRegexValidator<Pattern>(
5656
) -> ValidationPublisher where Pattern: RegexProtocol {
5757
publisher
5858
.dropFirst()
59-
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
59+
.debounce(for: .seconds(0.25), scheduler: RunLoop.main)
6060
.map { (value: String) -> [Validated<Void>] in
6161
var validationResults: [Validated<Void>] = .init(repeating: .untouched, count: patterns.count)
6262

Sources/CombineValidate/Views/ValidationWrapper.swift

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public struct ValidationWrapper<ValidatedView: View, ValidationPayload>: View {
3535
HStack {
3636
content
3737
.animation(nil)
38-
.padding(validated.isSuccess ? .vertical : .top, 4)
38+
.padding(validated.isSuccess || validated.isUntouched ? .vertical : .top, 4)
3939

4040
Spacer()
4141
icon
@@ -78,3 +78,37 @@ public struct ValidationWrapper<ValidatedView: View, ValidationPayload>: View {
7878
}
7979
}
8080
}
81+
82+
struct ValidationWrapper_Previews: PreviewProvider {
83+
class ViewModel: ObservableObject {
84+
@Published var email = ""
85+
86+
public lazy var emailValidator: ValidationPublisher = {
87+
$email.validateWithRegex(
88+
regex: RegularPattern.email,
89+
error: "Not email",
90+
tableName: nil
91+
)
92+
}()
93+
}
94+
95+
struct Content: View {
96+
@StateObject private var viewModel = ViewModel()
97+
98+
var body: some View {
99+
NavigationView {
100+
List {
101+
TextField("Email", text: $viewModel.email)
102+
.validate(for: viewModel.emailValidator)
103+
}
104+
}
105+
}
106+
}
107+
108+
static var previews: some View {
109+
Group {
110+
Content()
111+
}
112+
}
113+
}
114+

Sources/CombineValidate/Views/ValidationWrapperConfiguration.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ extension ValidationWrapper {
2929
self.successImage = successImage
3030
self.failureImage = failureImage
3131

32-
self.hintFont = hintFont ?? .callout
32+
self.hintFont = hintFont ?? .caption
3333

3434
self.iconTransition = iconTransition ?? .asymmetric(
3535
insertion: .move(edge: .trailing),

Tests/CombineValidateTests/MultiRegexValidatorTests.swift

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,38 @@ class MultiRegexValidatorTests: XCTestCase {
1414
errors: ["Should be one number at least", "Should be one special symbol at least", "Should be one capital letter at least"]
1515
)
1616
}()
17-
18-
private var subscription = Set<AnyCancellable>()
19-
20-
init() {
21-
specialTextValidator
22-
.assign(to: \.validationResult, on: self)
23-
.store(in: &subscription)
24-
}
2517
}
2618

2719
let viewModel = ViewModel()
20+
var cancellables: Set<AnyCancellable>!
21+
22+
override func setUp() {
23+
super.setUp()
24+
cancellables = .init()
25+
}
2826

2927
func testIgnoreFirstValue() {
28+
29+
viewModel.specialTextValidator
30+
.sink(receiveValue: { _ in })
31+
.store(in: &cancellables)
32+
3033
XCTAssertEqual(viewModel.validationResult, .untouched)
3134
}
3235

3336
func testFullyInvalidValue() {
37+
let expectation = XCTestExpectation(description: "Empty input expectation")
38+
39+
viewModel.specialTextValidator
40+
.sink(receiveValue: { [weak self] result in
41+
self?.viewModel.validationResult = result
42+
expectation.fulfill()
43+
})
44+
.store(in: &cancellables)
45+
3446
viewModel.specialText = ""
3547

36-
_ = XCTWaiter.wait(for: [XCTestExpectation()], timeout: 0.75)
48+
wait(for: [expectation], timeout: 1)
3749

3850
XCTAssertEqual(
3951
viewModel.validationResult,
@@ -42,17 +54,35 @@ class MultiRegexValidatorTests: XCTestCase {
4254
}
4355

4456
func testValueWithOneNumber() {
57+
let expectation = XCTestExpectation(description: "1 input expectation")
58+
59+
viewModel.specialTextValidator
60+
.sink(receiveValue: { [weak self] result in
61+
self?.viewModel.validationResult = result
62+
expectation.fulfill()
63+
})
64+
.store(in: &cancellables)
65+
4566
viewModel.specialText = "1"
4667

47-
_ = XCTWaiter.wait(for: [XCTestExpectation()], timeout: 0.75)
68+
wait(for: [expectation], timeout: 1)
4869

4970
XCTAssertEqual(viewModel.validationResult, .failure(reason: "Should be one special symbol at least", tableName: nil))
5071
}
5172

5273
func testValueWithOneNumberAndSpecialSymbol() {
74+
let expectation = XCTestExpectation(description: "1% input expectation")
75+
76+
viewModel.specialTextValidator
77+
.sink(receiveValue: { [weak self] result in
78+
self?.viewModel.validationResult = result
79+
expectation.fulfill()
80+
})
81+
.store(in: &cancellables)
82+
5383
viewModel.specialText = "1%"
5484

55-
_ = XCTWaiter.wait(for: [XCTestExpectation()], timeout: 0.75)
85+
wait(for: [expectation], timeout: 1)
5686

5787
XCTAssertEqual(
5888
viewModel.validationResult,
@@ -61,9 +91,18 @@ class MultiRegexValidatorTests: XCTestCase {
6191
}
6292

6393
func testValueWithOneNumberAndSpecialSymbolAndCapitalLetter() {
94+
let expectation = XCTestExpectation(description: "1%A input expectation")
95+
96+
viewModel.specialTextValidator
97+
.sink(receiveValue: { [weak self] result in
98+
self?.viewModel.validationResult = result
99+
expectation.fulfill()
100+
})
101+
.store(in: &cancellables)
102+
64103
viewModel.specialText = "1%A"
65104

66-
_ = XCTWaiter.wait(for: [XCTestExpectation()], timeout: 0.75)
105+
wait(for: [expectation], timeout: 1)
67106

68107
XCTAssertEqual(viewModel.validationResult, .success(.none))
69108
}

Tests/CombineValidateTests/NotEmpyValidatorTests.swift

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,31 +13,56 @@ final class NotEmptyValidatorTests: XCTestCase {
1313
public lazy var nameNotEmptyValidator: ValidationPublisher = {
1414
$name.validateNonEmpty(error: "Should not empty")
1515
}()
16-
17-
init() {
18-
subscription = nameNotEmptyValidator
19-
.assign(to: \.validationResult, on: self)
20-
}
16+
}
17+
18+
override func setUp() {
19+
super.setUp()
20+
cancellables = .init()
2121
}
2222

2323
let viewModel = ViewModel()
2424

25+
var cancellables: Set<AnyCancellable>!
26+
2527
func testIgnoreFirstValue() {
28+
viewModel.nameNotEmptyValidator
29+
.sink(receiveValue: { _ in })
30+
.store(in: &cancellables)
31+
2632
XCTAssertEqual(viewModel.validationResult, .untouched)
2733
}
2834

2935
func testInputEmptyValue() {
36+
let expectation = XCTestExpectation(description: "Empty input expectation")
37+
38+
viewModel.nameNotEmptyValidator
39+
.sink(receiveValue: { [weak self] value in
40+
self?.viewModel.validationResult = value
41+
expectation.fulfill()
42+
})
43+
.store(in: &cancellables)
44+
45+
3046
viewModel.name = ""
3147

32-
_ = XCTWaiter.wait(for: [XCTestExpectation()], timeout: 0.75)
48+
wait(for: [expectation], timeout: 1)
3349

3450
XCTAssertEqual(viewModel.validationResult, .failure(reason: "Should not empty", tableName: nil))
3551
}
3652

3753
func testInputNonEmptyValue() {
54+
let expectation = XCTestExpectation(description: "User input expectation")
55+
56+
viewModel.nameNotEmptyValidator
57+
.sink(receiveValue: { [weak self] value in
58+
self?.viewModel.validationResult = value
59+
expectation.fulfill()
60+
})
61+
.store(in: &cancellables)
62+
3863
viewModel.name = "alex"
3964

40-
_ = XCTWaiter.wait(for: [XCTestExpectation()], timeout: 0.75)
65+
wait(for: [expectation], timeout: 1)
4166

4267
XCTAssertEqual(viewModel.validationResult, .success(.none))
4368
}

Tests/CombineValidateTests/OneOfRegexValidatorTests.swift

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,58 +20,105 @@ final class OneOfRegexValidatorTests: XCTestCase {
2020
error: "Type one of social profile link (insta, facebook, linkedIn)"
2121
)
2222
}()
23-
24-
private var subscription = Set<AnyCancellable>()
25-
26-
init() {
27-
socialProfileValidator
28-
.assign(to: \.validationResult, on: self)
29-
.store(in: &subscription)
30-
}
3123
}
3224

3325
let viewModel = ViewModel()
26+
var cancellables: Set<AnyCancellable>!
27+
28+
override func setUp() {
29+
super.setUp()
30+
cancellables = .init()
31+
}
3432

3533
func testIgnoreFirstValue() {
34+
viewModel.socialProfileValidator
35+
.sink(receiveValue: { _ in })
36+
.store(in: &cancellables)
37+
3638
XCTAssertEqual(viewModel.validationResult, .untouched)
3739
}
3840

3941
func testExpectedInstagramInput() {
42+
let expectation = XCTestExpectation(description: "Instagram input expectation")
43+
44+
viewModel.socialProfileValidator
45+
.sink(receiveValue: { [weak self] result in
46+
self?.viewModel.validationResult = result
47+
expectation.fulfill()
48+
})
49+
.store(in: &cancellables)
50+
4051
viewModel.socialProfileUrl = "instagram.com/userprofile"
4152

42-
_ = XCTWaiter.wait(for: [XCTestExpectation()], timeout: 0.75)
53+
wait(for: [expectation], timeout: 1)
4354

4455
XCTAssertEqual(viewModel.validationResult, .success(.instagram))
4556
}
4657

4758
func testExpectedFacebookInput() {
48-
viewModel.socialProfileUrl = "facebook.com/userprofile"
59+
let expectation = XCTestExpectation(description: "Facebook input expectation")
4960

50-
_ = XCTWaiter.wait(for: [XCTestExpectation()], timeout: 0.75)
61+
viewModel.socialProfileValidator
62+
.sink(receiveValue: { [weak self] result in
63+
self?.viewModel.validationResult = result
64+
expectation.fulfill()
65+
})
66+
.store(in: &cancellables)
67+
68+
viewModel.socialProfileUrl = "facebook.com/userprofile"
69+
70+
wait(for: [expectation], timeout: 1)
5171

5272
XCTAssertEqual(viewModel.validationResult, .success(.facebook))
5373
}
5474

5575
func testExpectedLinkedInInput() {
76+
let expectation = XCTestExpectation(description: "Linkedin input expectation")
77+
78+
viewModel.socialProfileValidator
79+
.sink(receiveValue: { [weak self] result in
80+
self?.viewModel.validationResult = result
81+
expectation.fulfill()
82+
})
83+
.store(in: &cancellables)
84+
5685
viewModel.socialProfileUrl = "linkedin.com/in/userprofile"
5786

58-
_ = XCTWaiter.wait(for: [XCTestExpectation()], timeout: 0.75)
87+
wait(for: [expectation], timeout: 1)
5988

6089
XCTAssertEqual(viewModel.validationResult, .success(.linkedIn))
6190
}
6291

6392
func testUnexpectedValue() {
93+
let expectation = XCTestExpectation(description: "Youtube input expectation")
94+
95+
viewModel.socialProfileValidator
96+
.sink(receiveValue: { [weak self] result in
97+
self?.viewModel.validationResult = result
98+
expectation.fulfill()
99+
})
100+
.store(in: &cancellables)
101+
64102
viewModel.socialProfileUrl = "http://youtube.com/userprofile"
65103

66-
_ = XCTWaiter.wait(for: [XCTestExpectation()], timeout: 0.75)
104+
wait(for: [expectation], timeout: 1)
67105

68106
XCTAssertEqual(viewModel.validationResult, .failure(reason: "Type one of social profile link (insta, facebook, linkedIn)", tableName: nil))
69107
}
70108

71109
func testEmptyValue() {
110+
let expectation = XCTestExpectation(description: "Invalid input expectation")
111+
112+
viewModel.socialProfileValidator
113+
.sink(receiveValue: { [weak self] result in
114+
self?.viewModel.validationResult = result
115+
expectation.fulfill()
116+
})
117+
.store(in: &cancellables)
118+
72119
viewModel.socialProfileUrl = ""
73120

74-
_ = XCTWaiter.wait(for: [XCTestExpectation()], timeout: 0.75)
121+
wait(for: [expectation], timeout: 1)
75122

76123
XCTAssertEqual(viewModel.validationResult, .failure(reason: "Type one of social profile link (insta, facebook, linkedIn)", tableName: nil))
77124
}

0 commit comments

Comments
 (0)