Skip to content

Commit b224f4d

Browse files
authored
Add navigator UI tests (#683)
1 parent c1fd5b5 commit b224f4d

29 files changed

+874
-19
lines changed

.github/workflows/checks.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,23 @@ jobs:
4040
set -eo pipefail
4141
xcodebuild test-without-building -scheme "$scheme" -destination "platform=$platform,name=$device" | if command -v xcpretty &> /dev/null; then xcpretty; else cat; fi
4242
43+
navigator-ui-tests:
44+
name: Navigator UI Tests
45+
runs-on: macos-14
46+
if: ${{ !github.event.pull_request.draft }}
47+
steps:
48+
- name: Checkout
49+
uses: actions/checkout@v3
50+
- name: Install dependencies
51+
run: |
52+
brew update
53+
brew install xcodegen
54+
- name: Test
55+
run: |
56+
set -eo pipefail
57+
make navigator-ui-tests-project
58+
xcodebuild test -project Tests/NavigatorTests/UITests/NavigatorUITests.xcodeproj -scheme NavigatorTestHost -destination "platform=$platform,name=$device" | if command -v xcpretty &> /dev/null; then xcpretty; else cat; fi
59+
4360
lint:
4461
name: Lint
4562
runs-on: macos-14

Makefile

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ carthage-project:
1515
rm -rf $(SCRIPTS_PATH)/node_modules/
1616
xcodegen -s Support/Carthage/project.yml --use-cache --cache-path Support/Carthage/.xcodegen
1717

18+
.PHONY: navigator-ui-tests-project
19+
navigator-ui-tests-project:
20+
xcodegen -s Tests/NavigatorTests/UITests/project.yml
21+
1822
.PHONY: scripts
1923
scripts:
2024
@which corepack >/dev/null 2>&1 || (echo "ERROR: corepack is required, please install it first\nhttps://pnpm.io/installation#using-corepack"; exit 1)
@@ -32,11 +36,6 @@ update-scripts:
3236
@which corepack >/dev/null 2>&1 || (echo "ERROR: corepack is required, please install it first\nhttps://pnpm.io/installation#using-corepack"; exit 1)
3337
pnpm install --dir "$(SCRIPTS_PATH)"
3438

35-
.PHONY: test
36-
test:
37-
# To limit to a particular test suite: -only-testing:ReadiumSharedTests
38-
xcodebuild test -scheme "Readium-Package" -destination "platform=iOS Simulator,name=iPhone 15" | xcbeautify -q
39-
4039
.PHONY: lint-format
4140
lint-format:
4241
swift run --package-path BuildTools swiftformat --lint .

Package.swift

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,10 @@ let package = Package(
5353
),
5454
.testTarget(
5555
name: "ReadiumSharedTests",
56-
dependencies: ["ReadiumShared"],
56+
dependencies: [
57+
"ReadiumShared",
58+
"TestPublications",
59+
],
5760
path: "Tests/SharedTests",
5861
resources: [
5962
.copy("Fixtures"),
@@ -101,7 +104,10 @@ let package = Package(
101104
.testTarget(
102105
name: "ReadiumNavigatorTests",
103106
dependencies: ["ReadiumNavigator"],
104-
path: "Tests/NavigatorTests"
107+
path: "Tests/NavigatorTests",
108+
exclude: [
109+
"UITests",
110+
]
105111
),
106112

107113
.target(
@@ -140,7 +146,7 @@ let package = Package(
140146
// dependencies: ["ReadiumLCP"],
141147
// path: "Tests/LCPTests",
142148
// resources: [
143-
// .copy("Fixtures"),
149+
// .copy("../Fixtures"),
144150
// ]
145151
// ),
146152

@@ -171,5 +177,14 @@ let package = Package(
171177
dependencies: ["ReadiumInternal"],
172178
path: "Tests/InternalTests"
173179
),
180+
181+
// Shared test publications used across multiple test targets.
182+
.target(
183+
name: "TestPublications",
184+
path: "Tests/Publications",
185+
resources: [
186+
.copy("Publications"),
187+
]
188+
),
174189
]
175190
)

Sources/Navigator/EPUB/EPUBReflowableSpreadView.swift

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@ final class EPUBReflowableSpreadView: EPUBSpreadView {
3333
)
3434
}
3535

36+
override func clear() {
37+
super.clear()
38+
39+
// Clean up go to continuations.
40+
for continuation in goToContinuations {
41+
continuation.resume()
42+
}
43+
goToContinuations.removeAll()
44+
}
45+
3646
override func setupWebView() {
3747
super.setupWebView()
3848

@@ -193,7 +203,6 @@ final class EPUBReflowableSpreadView: EPUBSpreadView {
193203
// Location to scroll to in the resource once the page is loaded.
194204
private var pendingLocation: PageLocation = .start
195205

196-
@MainActor
197206
override func go(to location: PageLocation) async {
198207
guard isSpreadLoaded else {
199208
// Delays moving to the location until the document is loaded.
@@ -215,22 +224,19 @@ final class EPUBReflowableSpreadView: EPUBSpreadView {
215224
didCompleteGoTo()
216225
}
217226

218-
@MainActor
219227
private func waitGoToCompletion() async {
220228
await withCheckedContinuation { continuation in
221229
goToContinuations.append(continuation)
222230
}
223231
}
224232

225-
@MainActor
226233
private func didCompleteGoTo() {
227234
for cont in goToContinuations {
228235
cont.resume()
229236
}
230237
goToContinuations.removeAll()
231238
}
232239

233-
@MainActor
234240
private var goToContinuations: [CheckedContinuation<Void, Never>] = []
235241

236242
@discardableResult

Sources/Navigator/EPUB/EPUBSpreadView.swift

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,13 @@ class EPUBSpreadView: UIView, Loggable, PageView {
9595

9696
deinit {
9797
NotificationCenter.default.removeObserver(self)
98+
clear()
99+
}
100+
101+
/// Called when the spread view is removed from the view hierarchy, to
102+
/// clear pending operations and retain cycles.
103+
func clear() {
104+
// Disable JS messages to break WKUserContentController reference.
98105
disableJSMessages()
99106
}
100107

@@ -126,14 +133,18 @@ class EPUBSpreadView: UIView, Loggable, PageView {
126133
webView.scrollView
127134
}
128135

136+
override func willMove(toSuperview newSuperview: UIView?) {
137+
super.willMove(toSuperview: newSuperview)
138+
139+
if newSuperview == nil {
140+
clear()
141+
}
142+
}
143+
129144
override func didMoveToSuperview() {
130145
super.didMoveToSuperview()
131146

132-
if superview == nil {
133-
disableJSMessages()
134-
// Fixing an iOS 9 bug by explicitly clearing scrollView.delegate before deinitialization
135-
scrollView.delegate = nil
136-
} else {
147+
if superview != nil {
137148
enableJSMessages()
138149
scrollView.delegate = self
139150
}

Sources/Navigator/Toolkit/PaginationView.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,18 @@ final class PaginationView: UIView, Loggable {
150150
scrollView.contentOffset.x = xOffsetForIndex(currentIndex)
151151
}
152152

153+
override func willMove(toSuperview newSuperview: UIView?) {
154+
super.willMove(toSuperview: newSuperview)
155+
156+
if newSuperview == nil {
157+
// Remove all spread views to break retain cycles
158+
for (_, view) in loadedViews {
159+
view.removeFromSuperview()
160+
}
161+
loadedViews.removeAll()
162+
}
163+
}
164+
153165
override func didMoveToWindow() {
154166
super.didMoveToWindow()
155167

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.xcodeproj
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//
2+
// Copyright 2025 Readium Foundation. All rights reserved.
3+
// Use of this source code is governed by the BSD-style license
4+
// available in the top-level LICENSE file of the project.
5+
//
6+
7+
import Foundation
8+
import SwiftUI
9+
10+
enum AccessibilityID: String {
11+
case open
12+
case close
13+
case allMemoryDeallocated
14+
case isNavigatorReady
15+
}
16+
17+
extension View {
18+
func accessibilityIdentifier(_ id: AccessibilityID) -> ModifiedContent<Self, AccessibilityAttachmentModifier> {
19+
accessibilityIdentifier(id.rawValue)
20+
}
21+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
//
2+
// Copyright 2025 Readium Foundation. All rights reserved.
3+
// Use of this source code is governed by the BSD-style license
4+
// available in the top-level LICENSE file of the project.
5+
//
6+
7+
import ReadiumAdapterGCDWebServer
8+
import ReadiumNavigator
9+
import ReadiumShared
10+
import ReadiumStreamer
11+
import UIKit
12+
13+
/// Shared Readium infrastructure for testing.
14+
@MainActor class Container {
15+
static let shared = Container()
16+
17+
let memoryTracker = MemoryTracker()
18+
let httpClient: HTTPClient
19+
let httpServer: HTTPServer
20+
let assetRetriever: AssetRetriever
21+
let publicationOpener: PublicationOpener
22+
23+
init() {
24+
httpClient = DefaultHTTPClient()
25+
assetRetriever = AssetRetriever(httpClient: httpClient)
26+
httpServer = GCDHTTPServer(assetRetriever: assetRetriever)
27+
28+
publicationOpener = PublicationOpener(
29+
parser: DefaultPublicationParser(
30+
httpClient: httpClient,
31+
assetRetriever: assetRetriever,
32+
pdfFactory: DefaultPDFDocumentFactory()
33+
),
34+
contentProtections: []
35+
)
36+
}
37+
38+
func publication(at url: FileURL) async throws -> Publication {
39+
let asset = try await assetRetriever.retrieve(url: url).get()
40+
let publication = try await publicationOpener.open(
41+
asset: asset,
42+
allowUserInteraction: false,
43+
sender: nil
44+
).get()
45+
46+
memoryTracker.track(publication)
47+
return publication
48+
}
49+
50+
func navigator(for publication: Publication) throws -> VisualNavigator & UIViewController {
51+
if publication.conforms(to: .epub) {
52+
return try epubNavigator(for: publication)
53+
} else if publication.conforms(to: .pdf) {
54+
return try pdfNavigator(for: publication)
55+
} else {
56+
fatalError("Publication not supported")
57+
}
58+
}
59+
60+
func epubNavigator(for publication: Publication) throws -> EPUBNavigatorViewController {
61+
let navigator = try EPUBNavigatorViewController(
62+
publication: publication,
63+
initialLocation: nil,
64+
config: EPUBNavigatorViewController.Configuration(),
65+
httpServer: httpServer
66+
)
67+
memoryTracker.track(navigator)
68+
return navigator
69+
}
70+
71+
func pdfNavigator(for publication: Publication) throws -> PDFNavigatorViewController {
72+
let navigator = try PDFNavigatorViewController(
73+
publication: publication,
74+
initialLocation: nil,
75+
httpServer: httpServer
76+
)
77+
memoryTracker.track(navigator)
78+
return navigator
79+
}
80+
}

0 commit comments

Comments
 (0)