Skip to content

Commit 1e0662a

Browse files
authored
Merge pull request #237 from line/coverage/share-target-vc
Add unit tests for ShareTargetSearchResultTableViewController
2 parents b87879f + ab587df commit 1e0662a

File tree

4 files changed

+268
-7
lines changed

4 files changed

+268
-7
lines changed

LineSDK/LineSDK.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
2B70D3AC2E41C7E40017479B /* ShareTargetSearchResultTableViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B70D3AB2E41C7E40017479B /* ShareTargetSearchResultTableViewControllerTests.swift */; };
1011
2B414E622E40B9CB006C2276 /* OpenChatUserProfileViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B414E612E40B9CB006C2276 /* OpenChatUserProfileViewControllerTests.swift */; };
1112
2B7596862E404ECB00C1C6B6 /* ShareRootViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B7596852E404ECB00C1C6B6 /* ShareRootViewControllerTests.swift */; };
1213
2B8D25332E39A2540029FB34 /* LoginFlowFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B8D25322E39A2540029FB34 /* LoginFlowFactory.swift */; };
@@ -582,6 +583,7 @@
582583
/* End PBXContainerItemProxy section */
583584

584585
/* Begin PBXFileReference section */
586+
2B70D3AB2E41C7E40017479B /* ShareTargetSearchResultTableViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareTargetSearchResultTableViewControllerTests.swift; sourceTree = "<group>"; };
585587
2B414E612E40B9CB006C2276 /* OpenChatUserProfileViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenChatUserProfileViewControllerTests.swift; sourceTree = "<group>"; };
586588
2B7596852E404ECB00C1C6B6 /* ShareRootViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareRootViewControllerTests.swift; sourceTree = "<group>"; };
587589
2B8D25322E39A2540029FB34 /* LoginFlowFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginFlowFactory.swift; sourceTree = "<group>"; };
@@ -1651,6 +1653,7 @@
16511653
4BE4E0FA2251C9090071FC60 /* Sharing */ = {
16521654
isa = PBXGroup;
16531655
children = (
1656+
2B70D3AB2E41C7E40017479B /* ShareTargetSearchResultTableViewControllerTests.swift */,
16541657
2B7596852E404ECB00C1C6B6 /* ShareRootViewControllerTests.swift */,
16551658
DB0AFF602247FB2E002729AD /* PageTabViewTests.swift */,
16561659
4BEFF643226DCD960046DB66 /* ShareControllerTests.swift */,
@@ -2410,6 +2413,7 @@
24102413
4B8A965721100A5800760219 /* LoginConfigurationTests.swift in Sources */,
24112414
4BEB4928212BA8CF00BA946A /* FlexSeparatorComponentTests.swift in Sources */,
24122415
4B39D1DE211044B000A45510 /* PostTokenExchangeRequestTests.swift in Sources */,
2416+
2B70D3AC2E41C7E40017479B /* ShareTargetSearchResultTableViewControllerTests.swift in Sources */,
24132417
4BFD605E212A8BA6009E9838 /* FlexComponentMessageTests.swift in Sources */,
24142418
4B94D06721533D4D0049DE68 /* ECDSATests.swift in Sources */,
24152419
4BF45A892137B29300CCD28E /* RSATests.swift in Sources */,

LineSDK/LineSDK/LineSDKUI/SharingUI/Model/ColumnDataStore.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ extension LineSDKNotificationKey {
3737
@MainActor
3838
class ColumnDataStore<T> {
3939

40-
struct ColumnIndex: Equatable {
40+
internal struct ColumnIndex: Equatable {
4141
let column: Int
4242
let row: Int
4343
}

LineSDK/LineSDK/LineSDKUI/SharingUI/TargetSearch/ShareTargetSearchResultTableViewController.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ final class ShareTargetSearchResultTableViewController: UITableViewController, S
3838
}
3939
}
4040

41-
private typealias ColumnIndex = ColumnDataStore<ShareTarget>.ColumnIndex
42-
private let store: ColumnDataStore<ShareTarget>
41+
internal typealias ColumnIndex = ColumnDataStore<ShareTarget>.ColumnIndex
42+
internal let store: ColumnDataStore<ShareTarget>
4343

44-
private var selectingObserver: NotificationToken!
45-
private var deselectingObserver: NotificationToken!
44+
internal var selectingObserver: NotificationToken!
45+
internal var deselectingObserver: NotificationToken!
4646

4747
init(store: ColumnDataStore<ShareTarget>) {
4848
self.store = store
@@ -58,7 +58,7 @@ final class ShareTargetSearchResultTableViewController: UITableViewController, S
5858
setupTableView()
5959
}
6060

61-
private var filteredIndexes = [[ColumnIndex]](
61+
internal var filteredIndexes = [[ColumnIndex]](
6262
repeating: [], count: MessageShareTargetType.allCases.count)
6363
{
6464
didSet { tableView.reloadData() }
@@ -169,7 +169,7 @@ final class ShareTargetSearchResultTableViewController: UITableViewController, S
169169
return view
170170
}
171171

172-
private func actualSection(_ section: Int) -> Int {
172+
internal func actualSection(_ section: Int) -> Int {
173173
return sectionOrder[section].rawValue
174174
}
175175
}
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
//
2+
// ShareTargetSearchResultTableViewControllerTests.swift
3+
//
4+
// Copyright (c) 2016-present, LY Corporation. All rights reserved.
5+
//
6+
// You are hereby granted a non-exclusive, worldwide, royalty-free license to use,
7+
// copy and distribute this software in source code or binary form for use
8+
// in connection with the web services and APIs provided by LY Corporation.
9+
//
10+
// As with any software that integrates with the LY Corporation platform, your use of this software
11+
// is subject to the LINE Developers Agreement [http://terms2.line.me/LINE_Developers_Agreement].
12+
// This copyright notice shall be included in all copies or substantial portions of the software.
13+
//
14+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
15+
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17+
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18+
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20+
//
21+
22+
import XCTest
23+
@testable import LineSDK
24+
25+
@MainActor
26+
class ShareTargetSearchResultTableViewControllerTests: XCTestCase {
27+
28+
// MARK: - Properties
29+
30+
private var store: ColumnDataStore<ShareTarget>!
31+
private var controller: ShareTargetSearchResultTableViewController!
32+
private let testIndex = ColumnDataStore<ShareTarget>.ColumnIndex(column: MessageShareTargetType.friends.rawValue, row: 0)
33+
34+
// MARK: - Setup & Teardown
35+
36+
override func setUp() async throws {
37+
store = ColumnDataStore<ShareTarget>(columnCount: MessageShareTargetType.allCases.count)
38+
controller = ShareTargetSearchResultTableViewController(store: store)
39+
}
40+
41+
override func tearDown() async throws {
42+
controller?.stopObserving()
43+
controller = nil
44+
store = nil
45+
}
46+
47+
// MARK: - Core Functionality Tests
48+
49+
func testInitialState() {
50+
XCTAssertEqual(controller.searchText, "")
51+
XCTAssertTrue(controller.hasSearchResult)
52+
XCTAssertEqual(controller.sectionOrder, [.friends, .groups])
53+
XCTAssertEqual(controller.filteredIndexes.count, MessageShareTargetType.allCases.count)
54+
}
55+
56+
func testSearchFiltering() {
57+
// Setup test data
58+
store.append(data: TestData.friends, to: MessageShareTargetType.friends.rawValue)
59+
60+
// Test specific search
61+
controller.searchText = "Alice"
62+
XCTAssertEqual(controller.filteredIndexes[MessageShareTargetType.friends.rawValue].count, 1)
63+
XCTAssertEqual(controller.filteredIndexes[MessageShareTargetType.groups.rawValue].count, 0)
64+
XCTAssertTrue(controller.hasSearchResult)
65+
66+
// Test no results
67+
controller.searchText = "NonExistent"
68+
XCTAssertFalse(controller.hasSearchResult)
69+
70+
// Test empty search (show all)
71+
controller.searchText = "temp" // Trigger change
72+
controller.searchText = ""
73+
XCTAssertTrue(controller.hasSearchResult)
74+
}
75+
76+
func testObserverLifecycle() {
77+
// Test setup
78+
controller.startObserving()
79+
XCTAssertNotNil(controller.selectingObserver)
80+
XCTAssertNotNil(controller.deselectingObserver)
81+
82+
// Test cleanup
83+
controller.stopObserving()
84+
XCTAssertNil(controller.selectingObserver)
85+
XCTAssertNil(controller.deselectingObserver)
86+
XCTAssertEqual(controller.searchText, "")
87+
}
88+
89+
func testSectionMapping() {
90+
XCTAssertEqual(controller.actualSection(0), MessageShareTargetType.friends.rawValue)
91+
XCTAssertEqual(controller.actualSection(1), MessageShareTargetType.groups.rawValue)
92+
XCTAssertEqual(controller.numberOfSections(in: UITableView()), MessageShareTargetType.allCases.count)
93+
}
94+
95+
// MARK: - Observer Tests
96+
97+
func testObserverNotifications() {
98+
setupControllerForObserverTests()
99+
100+
// Test select notification
101+
sendNotification(.columnDataStoreDidSelect)
102+
103+
// Test deselect notification
104+
sendNotification(.columnDataStoreDidDeselect)
105+
106+
// Verify handleSelectingChange logic components
107+
let foundRow = controller.filteredIndexes[testIndex.column].firstIndex(of: testIndex)
108+
XCTAssertNotNil(foundRow)
109+
XCTAssertEqual(controller.actualSection(testIndex.column), MessageShareTargetType.friends.rawValue)
110+
XCTAssertEqual(store.data(at: testIndex).displayName, "Alice")
111+
}
112+
113+
// MARK: - TableView Tests
114+
115+
func testTableViewDataSource() {
116+
setupControllerForTableViewTests()
117+
118+
let tableView = controller.tableView!
119+
120+
// Test sections
121+
XCTAssertEqual(controller.numberOfSections(in: tableView), MessageShareTargetType.allCases.count)
122+
123+
// Test empty state
124+
testEmptyTableViewState(tableView)
125+
126+
// Test with data
127+
populateFilteredIndexes()
128+
testPopulatedTableViewState(tableView)
129+
130+
// Test filtered state
131+
testFilteredTableViewState(tableView)
132+
}
133+
134+
// MARK: - Helper Methods
135+
136+
private func setupControllerForObserverTests() {
137+
controller.loadViewIfNeeded()
138+
controller.viewDidLoad()
139+
store.append(data: TestData.friends, to: MessageShareTargetType.friends.rawValue)
140+
controller.filteredIndexes[MessageShareTargetType.friends.rawValue] = [testIndex]
141+
controller.tableView.register(ShareTargetSelectingTableCell.self, forCellReuseIdentifier: ShareTargetSelectingTableCell.reuseIdentifier)
142+
controller.startObserving()
143+
}
144+
145+
private func setupControllerForTableViewTests() {
146+
controller.loadViewIfNeeded()
147+
controller.viewDidLoad()
148+
store.append(data: TestData.friends, to: MessageShareTargetType.friends.rawValue)
149+
store.append(data: TestData.groups, to: MessageShareTargetType.groups.rawValue)
150+
}
151+
152+
private func sendNotification(_ name: Notification.Name) {
153+
NotificationCenter.default.post(
154+
name: name,
155+
object: store,
156+
userInfo: [
157+
LineSDKNotificationKey.selectingIndex: testIndex,
158+
LineSDKNotificationKey.positionInSelected: 0
159+
]
160+
)
161+
}
162+
163+
private func testEmptyTableViewState(_ tableView: UITableView) {
164+
XCTAssertEqual(controller.tableView(tableView, numberOfRowsInSection: 0), 0)
165+
XCTAssertEqual(controller.tableView(tableView, numberOfRowsInSection: 1), 0)
166+
XCTAssertEqual(controller.tableView(tableView, heightForHeaderInSection: 0), 0)
167+
XCTAssertEqual(controller.tableView(tableView, heightForHeaderInSection: 1), 0)
168+
XCTAssertNil(controller.tableView(tableView, viewForHeaderInSection: 0))
169+
XCTAssertNil(controller.tableView(tableView, viewForHeaderInSection: 1))
170+
}
171+
172+
private func populateFilteredIndexes() {
173+
controller.filteredIndexes[MessageShareTargetType.friends.rawValue] = TestData.friendsIndexes
174+
controller.filteredIndexes[MessageShareTargetType.groups.rawValue] = TestData.groupsIndexes
175+
}
176+
177+
private func testPopulatedTableViewState(_ tableView: UITableView) {
178+
// Test row counts
179+
XCTAssertEqual(controller.tableView(tableView, numberOfRowsInSection: 0), TestData.friends.count)
180+
XCTAssertEqual(controller.tableView(tableView, numberOfRowsInSection: 1), TestData.groups.count)
181+
182+
// Test header heights
183+
let expectedHeight = ShareTargetSelectingSectionHeaderView.Design.height
184+
XCTAssertEqual(controller.tableView(tableView, heightForHeaderInSection: 0), expectedHeight)
185+
XCTAssertEqual(controller.tableView(tableView, heightForHeaderInSection: 1), expectedHeight)
186+
187+
// Test header views
188+
let headerView0 = controller.tableView(tableView, viewForHeaderInSection: 0)
189+
let headerView1 = controller.tableView(tableView, viewForHeaderInSection: 1)
190+
XCTAssertTrue(headerView0 is ShareTargetSelectingSectionHeaderView)
191+
XCTAssertTrue(headerView1 is ShareTargetSelectingSectionHeaderView)
192+
193+
// Test cell creation
194+
let cell = controller.tableView(tableView, cellForRowAt: IndexPath(row: 0, section: 0))
195+
XCTAssertTrue(cell is ShareTargetSelectingTableCell)
196+
}
197+
198+
private func testFilteredTableViewState(_ tableView: UITableView) {
199+
// Simulate filtered state (only Alice)
200+
controller.filteredIndexes[MessageShareTargetType.friends.rawValue] = [testIndex]
201+
controller.filteredIndexes[MessageShareTargetType.groups.rawValue] = []
202+
203+
XCTAssertEqual(controller.tableView(tableView, numberOfRowsInSection: 0), 1)
204+
XCTAssertEqual(controller.tableView(tableView, numberOfRowsInSection: 1), 0)
205+
XCTAssertEqual(controller.tableView(tableView, heightForHeaderInSection: 0), ShareTargetSelectingSectionHeaderView.Design.height)
206+
XCTAssertEqual(controller.tableView(tableView, heightForHeaderInSection: 1), 0)
207+
XCTAssertNotNil(controller.tableView(tableView, viewForHeaderInSection: 0))
208+
XCTAssertNil(controller.tableView(tableView, viewForHeaderInSection: 1))
209+
}
210+
}
211+
212+
// MARK: - Test Data
213+
214+
private enum TestData {
215+
static let friends: [ShareTarget] = [
216+
DummyUser(userID: "friend1", displayName: "Alice", pictureURL: nil),
217+
DummyUser(userID: "friend2", displayName: "Bob", pictureURL: nil),
218+
DummyUser(userID: "friend3", displayName: "Charlie", pictureURL: nil)
219+
]
220+
221+
static let groups: [ShareTarget] = [
222+
DummyGroup(groupID: "group1", groupName: "Development Team", pictureURL: nil),
223+
DummyGroup(groupID: "group2", groupName: "Design Team", pictureURL: nil)
224+
]
225+
226+
static let friendsIndexes = [
227+
ColumnDataStore<ShareTarget>.ColumnIndex(column: MessageShareTargetType.friends.rawValue, row: 0),
228+
ColumnDataStore<ShareTarget>.ColumnIndex(column: MessageShareTargetType.friends.rawValue, row: 1),
229+
ColumnDataStore<ShareTarget>.ColumnIndex(column: MessageShareTargetType.friends.rawValue, row: 2)
230+
]
231+
232+
static let groupsIndexes = [
233+
ColumnDataStore<ShareTarget>.ColumnIndex(column: MessageShareTargetType.groups.rawValue, row: 0),
234+
ColumnDataStore<ShareTarget>.ColumnIndex(column: MessageShareTargetType.groups.rawValue, row: 1)
235+
]
236+
}
237+
238+
// MARK: - Mock Objects
239+
240+
private struct DummyUser: ShareTarget, Sendable {
241+
let userID: String
242+
let displayName: String
243+
let pictureURL: URL?
244+
245+
var targetID: String { return userID }
246+
var avatarURL: URL? { return pictureURL }
247+
}
248+
249+
private struct DummyGroup: ShareTarget, Sendable {
250+
let groupID: String
251+
let groupName: String
252+
let pictureURL: URL?
253+
254+
var targetID: String { return groupID }
255+
var displayName: String { return groupName }
256+
var avatarURL: URL? { return pictureURL }
257+
}

0 commit comments

Comments
 (0)