Skip to content

Commit 303e012

Browse files
committed
Add AttributedText for note content view with unit tests.
1 parent aa11d09 commit 303e012

File tree

8 files changed

+153
-4
lines changed

8 files changed

+153
-4
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import Foundation
2+
3+
extension NSAttributedString {
4+
/// Returns an `NSAttributedString` with attributes applied to the whole string.
5+
func addingAttributes(_ attributes: [NSAttributedString.Key: Any]) -> NSAttributedString {
6+
let attributedHTMLString = NSMutableAttributedString(attributedString: self)
7+
8+
let range = NSRange(location: 0, length: attributedHTMLString.length)
9+
attributedHTMLString.addAttributes(attributes, range: range)
10+
return attributedHTMLString
11+
}
12+
}

WooCommerce/Classes/ViewRelated/Inbox/InboxNoteRow.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ struct InboxNoteRow: View {
1818
.fixedSize(horizontal: false, vertical: true)
1919

2020
// Content.
21-
// TODO: 5954 - HTML content
21+
AttributedText(viewModel.attributedContent)
22+
.attributedTextLinkColor(Color(.accent))
2223

2324
// HStack with actions and dismiss action.
2425
HStack(spacing: Constants.spacingBetweenActions) {

WooCommerce/Classes/ViewRelated/Inbox/InboxNoteRowViewModel.swift

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,38 @@
11
import Yosemite
22

33
/// View model for `InboxNoteRow`.
4-
struct InboxNoteRowViewModel: Identifiable {
4+
struct InboxNoteRowViewModel: Identifiable, Equatable {
55
let id: Int64
66
let title: String
77

8+
/// HTML note content.
9+
let attributedContent: NSAttributedString
10+
811
let actions: [InboxNoteRowActionViewModel]
912

1013
init(note: InboxNote) {
14+
let attributedContent = note.content.htmlToAttributedString
15+
.addingAttributes([
16+
.font: UIFont.body,
17+
.foregroundColor: UIColor.secondaryLabel
18+
])
1119
let actions = note.actions.map { InboxNoteRowActionViewModel(action: $0) }
1220
self.init(id: note.id,
1321
title: note.title,
22+
attributedContent: attributedContent,
1423
actions: actions)
1524
}
1625

17-
init(id: Int64, title: String, actions: [InboxNoteRowActionViewModel]) {
26+
init(id: Int64, title: String, attributedContent: NSAttributedString, actions: [InboxNoteRowActionViewModel]) {
1827
self.id = id
1928
self.title = title
29+
self.attributedContent = attributedContent
2030
self.actions = actions
2131
}
2232
}
2333

2434
/// View model for an action in `InboxNoteRow`.
25-
struct InboxNoteRowActionViewModel: Identifiable {
35+
struct InboxNoteRowActionViewModel: Identifiable, Equatable {
2636
let id: Int64
2737
let title: String
2838
let url: URL?

WooCommerce/Classes/ViewRelated/Inbox/InboxViewModel.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ final class InboxViewModel: ObservableObject {
1717
let placeholderRowViewModels: [InboxNoteRowViewModel] = [Int64](0..<3).map {
1818
// The content does not matter because the text in placeholder rows is redacted.
1919
InboxNoteRowViewModel(id: $0, title: " ",
20+
attributedContent: .init(string: "\n\n\n"),
2021
actions: [.init(id: 0, title: "Placeholder", url: nil)])
2122
}
2223

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,9 @@
213213
02645D8227BA20A30065DC68 /* InboxViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02645D8127BA20A30065DC68 /* InboxViewModelTests.swift */; };
214214
02645D8527BA2DB40065DC68 /* InboxNoteRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02645D8327BA2DB30065DC68 /* InboxNoteRowViewModel.swift */; };
215215
02645D8627BA2DB40065DC68 /* InboxNoteRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02645D8427BA2DB40065DC68 /* InboxNoteRow.swift */; };
216+
02645D8827BA2E820065DC68 /* NSAttributedString+Attributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02645D8727BA2E820065DC68 /* NSAttributedString+Attributes.swift */; };
217+
02645D8A27BA2EDB0065DC68 /* NSAttributedString+AttributesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02645D8927BA2EDB0065DC68 /* NSAttributedString+AttributesTests.swift */; };
218+
02645D8C27BA342D0065DC68 /* InboxNoteRowViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02645D8B27BA342D0065DC68 /* InboxNoteRowViewModelTests.swift */; };
216219
02691780232600A6002AFC20 /* ProductsTabProductViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0269177F232600A6002AFC20 /* ProductsTabProductViewModelTests.swift */; };
217220
02691782232605B9002AFC20 /* PaginatedListViewControllerStateCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02691781232605B9002AFC20 /* PaginatedListViewControllerStateCoordinatorTests.swift */; };
218221
0269576A23726304001BA0BF /* KeyboardFrameObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0269576923726304001BA0BF /* KeyboardFrameObserver.swift */; };
@@ -1841,6 +1844,9 @@
18411844
02645D8127BA20A30065DC68 /* InboxViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboxViewModelTests.swift; sourceTree = "<group>"; };
18421845
02645D8327BA2DB30065DC68 /* InboxNoteRowViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InboxNoteRowViewModel.swift; sourceTree = "<group>"; };
18431846
02645D8427BA2DB40065DC68 /* InboxNoteRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InboxNoteRow.swift; sourceTree = "<group>"; };
1847+
02645D8727BA2E820065DC68 /* NSAttributedString+Attributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Attributes.swift"; sourceTree = "<group>"; };
1848+
02645D8927BA2EDB0065DC68 /* NSAttributedString+AttributesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+AttributesTests.swift"; sourceTree = "<group>"; };
1849+
02645D8B27BA342D0065DC68 /* InboxNoteRowViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboxNoteRowViewModelTests.swift; sourceTree = "<group>"; };
18441850
0269177F232600A6002AFC20 /* ProductsTabProductViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsTabProductViewModelTests.swift; sourceTree = "<group>"; };
18451851
02691781232605B9002AFC20 /* PaginatedListViewControllerStateCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginatedListViewControllerStateCoordinatorTests.swift; sourceTree = "<group>"; };
18461852
0269576923726304001BA0BF /* KeyboardFrameObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardFrameObserver.swift; sourceTree = "<group>"; };
@@ -3770,6 +3776,7 @@
37703776
isa = PBXGroup;
37713777
children = (
37723778
02645D8127BA20A30065DC68 /* InboxViewModelTests.swift */,
3779+
02645D8B27BA342D0065DC68 /* InboxNoteRowViewModelTests.swift */,
37733780
);
37743781
path = Inbox;
37753782
sourceTree = "<group>";
@@ -6041,6 +6048,7 @@
60416048
DE67D46826BAA82600EFE8DB /* Publisher+WithLatestFromTests.swift */,
60426049
DE7842EE26F079A60030C792 /* NumberFormatter+LocalizedTests.swift */,
60436050
02DE5CAA279F8754007CBEF3 /* Double+RoundingTests.swift */,
6051+
02645D8927BA2EDB0065DC68 /* NSAttributedString+AttributesTests.swift */,
60446052
);
60456053
path = Extensions;
60466054
sourceTree = "<group>";
@@ -6656,6 +6664,7 @@
66566664
DE7842F626F2E9340030C792 /* UIViewController+Connectivity.swift */,
66576665
DEC51B05276B3F3C009F3DF4 /* Int64+Helpers.swift */,
66586666
02DE5CA8279F857D007CBEF3 /* Double+Rounding.swift */,
6667+
02645D8727BA2E820065DC68 /* NSAttributedString+Attributes.swift */,
66596668
);
66606669
path = Extensions;
66616670
sourceTree = "<group>";
@@ -8222,6 +8231,7 @@
82228231
57896D6625362B0C000E8C4D /* TitleAndEditableValueTableViewCellViewModel.swift in Sources */,
82238232
26E1BECE251CD9F80096D0A1 /* RefundItemViewModel.swift in Sources */,
82248233
DE7B479027A153C20018742E /* CouponSearchUICommand.swift in Sources */,
8234+
02645D8827BA2E820065DC68 /* NSAttributedString+Attributes.swift in Sources */,
82258235
024DF3092372CA00006658FE /* EditorViewProperties.swift in Sources */,
82268236
45F8C43B2680BC3500F1A6EC /* ShippingLabelDiscountInfoViewController.swift in Sources */,
82278237
DE77889A26FD7EF0008DFF44 /* ShippingLabelPackageItem.swift in Sources */,
@@ -9320,6 +9330,7 @@
93209330
26B119C024D0C69500FED5C7 /* SurveyViewControllerTests.swift in Sources */,
93219331
D8AB131E225DC25F002BB5D1 /* MockOrders.swift in Sources */,
93229332
D8025496265530E0001B2CC1 /* CardPresentModalFoundReaderTests.swift in Sources */,
9333+
02645D8C27BA342D0065DC68 /* InboxNoteRowViewModelTests.swift in Sources */,
93239334
D85136D5231E40B500DD0539 /* ProductReviewTableViewCellTests.swift in Sources */,
93249335
45FBDF2B238BF8A300127F77 /* ProductImageCollectionViewCellTests.swift in Sources */,
93259336
D89C009A25B4EEA4000E4683 /* WrongAccountErrorViewModelTests.swift in Sources */,
@@ -9442,6 +9453,7 @@
94429453
D8736B5122EB69E300A14A29 /* OrderDetailsViewModelTests.swift in Sources */,
94439454
02C0CD2E23B5E3AE00F880B1 /* DefaultImageServiceTests.swift in Sources */,
94449455
02E4FD812306AA890049610C /* StatsTimeRangeBarViewModelTests.swift in Sources */,
9456+
02645D8A27BA2EDB0065DC68 /* NSAttributedString+AttributesTests.swift in Sources */,
94459457
6856D2A5C2076F5BF14F2C11 /* KeyboardStateProviderTests.swift in Sources */,
94469458
6856D806DE7DB61522D54044 /* NSMutableAttributedStringHelperTests.swift in Sources */,
94479459
023D69442588C6BD00F7DA72 /* ShippingLabelPaperSizeListSelectorCommandTests.swift in Sources */,
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import XCTest
2+
import UIKit
3+
@testable import WooCommerce
4+
5+
final class NSAttributedString_AttributesTests: XCTestCase {
6+
func test_adding_attributes_applies_to_the_whole_range() {
7+
// Given
8+
let originalString = NSAttributedString(string: "It’s seamless to <a href=\"https://docs.woocommerce.com\">enable Apple Pay with Stripe</a>")
9+
10+
// When
11+
let string = originalString.addingAttributes([.foregroundColor: UIColor.purple])
12+
13+
// Then
14+
var effectiveRange = NSRange()
15+
XCTAssertEqual(string.attributes(at: 0, effectiveRange: &effectiveRange) as? [NSAttributedString.Key: UIColor], [
16+
.foregroundColor: UIColor.purple
17+
])
18+
XCTAssertEqual(effectiveRange, .init(location: 0, length: originalString.length))
19+
}
20+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import XCTest
2+
import Yosemite
3+
@testable import WooCommerce
4+
5+
final class InboxNoteRowViewModelTests: XCTestCase {
6+
// MARK: - `InboxNoteRowViewModel`
7+
8+
func test_initializing_InboxNoteRowViewModel_with_note_has_expected_properties() throws {
9+
// Given
10+
let action = InboxAction.fake().copy(id: 606)
11+
let note = InboxNote.fake().copy(actions: [action])
12+
13+
// When
14+
let viewModel = InboxNoteRowViewModel(note: note)
15+
16+
// Then
17+
XCTAssertEqual(viewModel.id, note.id)
18+
XCTAssertEqual(viewModel.title, note.title)
19+
XCTAssertEqual(viewModel.attributedContent.string, note.content)
20+
21+
let actionViewModel = try XCTUnwrap(viewModel.actions.first)
22+
XCTAssertEqual(actionViewModel.id, action.id)
23+
}
24+
25+
// MARK: - `InboxNoteRowActionViewModel`
26+
27+
func test_initializing_InboxNoteRowActionViewModel_with_action_with_empty_URL_path_has_nil_URL() {
28+
// When
29+
let actionViewModel = InboxNoteRowActionViewModel(action: .init(id: 134, name: "wcpay_applepay_holiday2021",
30+
label: "Accept Apple Pay",
31+
status: "actioned",
32+
url: ""))
33+
34+
// Then
35+
XCTAssertEqual(actionViewModel, .init(id: 134, title: "Accept Apple Pay", url: nil))
36+
XCTAssertNil(actionViewModel.url)
37+
}
38+
39+
func test_initializing_InboxNoteRowActionViewModel_with_action_has_expected_properties() {
40+
// When
41+
let actionViewModel = InboxNoteRowActionViewModel(action: .init(id: 134, name: "wcpay_applepay_holiday2021",
42+
label: "Accept Apple Pay",
43+
status: "actioned",
44+
url: "https://woocommerce.com"))
45+
46+
// Then
47+
XCTAssertEqual(actionViewModel, .init(id: 134, title: "Accept Apple Pay", url: .init(string: "https://woocommerce.com")))
48+
XCTAssertEqual(actionViewModel.id, 134)
49+
XCTAssertEqual(actionViewModel.title, "Accept Apple Pay")
50+
XCTAssertEqual(actionViewModel.url?.absoluteString, "https://woocommerce.com")
51+
}
52+
}

WooCommerce/WooCommerceTests/ViewRelated/Inbox/InboxViewModelTests.swift

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ final class InboxViewModelTests: XCTestCase {
1212
subscriptions = []
1313
}
1414

15+
// MARK: - State transitions
16+
1517
func test_state_is_empty_without_any_actions() {
1618
// Given
1719
let stores = MockStoresManager(sessionManager: .testingInstance)
@@ -148,4 +150,43 @@ final class InboxViewModelTests: XCTestCase {
148150
XCTAssertEqual(invocationCountOfLoadInboxNotes, 2)
149151
XCTAssertEqual(syncPageNumber, 2)
150152
}
153+
154+
// MARK: - Row view models
155+
156+
func test_noteRowViewModels_match_loaded_notes() {
157+
// Given
158+
let stores = MockStoresManager(sessionManager: .testingInstance)
159+
let note = InboxNote.fake()
160+
stores.whenReceivingAction(ofType: InboxNotesAction.self) { action in
161+
guard case let .loadAllInboxNotes(_, _, _, _, _, _, completion) = action else {
162+
return
163+
}
164+
completion(.success([note]))
165+
}
166+
let viewModel = InboxViewModel(siteID: sampleSiteID, stores: stores)
167+
168+
// When
169+
viewModel.onLoadTrigger.send()
170+
171+
// Then
172+
XCTAssertEqual(viewModel.noteRowViewModels.first, .init(note: note))
173+
}
174+
175+
func test_noteRowViewModels_are_empty_when_loaded_notes_are_empty() {
176+
// Given
177+
let stores = MockStoresManager(sessionManager: .testingInstance)
178+
stores.whenReceivingAction(ofType: InboxNotesAction.self) { action in
179+
guard case let .loadAllInboxNotes(_, _, _, _, _, _, completion) = action else {
180+
return
181+
}
182+
completion(.success([]))
183+
}
184+
let viewModel = InboxViewModel(siteID: sampleSiteID, stores: stores)
185+
186+
// When
187+
viewModel.onLoadTrigger.send()
188+
189+
// Then
190+
XCTAssertEqual(viewModel.noteRowViewModels, [])
191+
}
151192
}

0 commit comments

Comments
 (0)