Skip to content

Commit 7e0bcf8

Browse files
committed
Add navigator UI tests
1 parent c1fd5b5 commit 7e0bcf8

23 files changed

+794
-7
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: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,10 @@ let package = Package(
101101
.testTarget(
102102
name: "ReadiumNavigatorTests",
103103
dependencies: ["ReadiumNavigator"],
104-
path: "Tests/NavigatorTests"
104+
path: "Tests/NavigatorTests",
105+
exclude: [
106+
"UITests",
107+
]
105108
),
106109

107110
.target(
@@ -140,7 +143,7 @@ let package = Package(
140143
// dependencies: ["ReadiumLCP"],
141144
// path: "Tests/LCPTests",
142145
// resources: [
143-
// .copy("Fixtures"),
146+
// .copy("../Fixtures"),
144147
// ]
145148
// ),
146149

File renamed without changes.
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+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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 ReadiumShared
8+
import SwiftUI
9+
10+
/// Provides a simple UI for opening publication fixtures and display memory
11+
/// status for UI test verification.
12+
struct FixtureList: View {
13+
@ObservedObject private var memoryTracker: MemoryTracker
14+
@StateObject private var viewModel = FixtureListViewModel()
15+
16+
init() {
17+
memoryTracker = Container.shared.memoryTracker
18+
}
19+
20+
var body: some View {
21+
List {
22+
Section {
23+
fixture(.childrensLiteratureEPUB)
24+
fixture(.daisyPDF)
25+
}
26+
27+
Section {
28+
Toggle(isOn: $memoryTracker.allDeallocated) {
29+
Text("All memory is deallocated")
30+
}
31+
.accessibilityIdentifier(.allMemoryDeallocated)
32+
}
33+
.disabled(true)
34+
}
35+
.fullScreenCover(item: $viewModel.readerViewModel) { viewModel in
36+
ReaderView(viewModel: viewModel)
37+
}
38+
}
39+
40+
private func fixture(_ fixture: PublicationFixture) -> some View {
41+
ListRow(action: { viewModel.open(fixture) }) {
42+
VStack(alignment: .leading) {
43+
Text(fixture.filename)
44+
.font(.headline)
45+
46+
Text(fixture.description)
47+
.font(.caption)
48+
}
49+
50+
Spacer()
51+
52+
Image(systemName: "chevron.right")
53+
}
54+
.accessibilityIdentifier(fixture.accessibilityIdentifier)
55+
}
56+
}
57+
58+
@MainActor
59+
class FixtureListViewModel: ObservableObject {
60+
@Published var readerViewModel: ReaderViewModel?
61+
62+
private var openTask: Task<Void, Never>?
63+
64+
func open(_ fixture: PublicationFixture) {
65+
openTask?.cancel()
66+
openTask = Task { try! await open(fixture) }
67+
}
68+
69+
private func open(_ fixture: PublicationFixture) async throws {
70+
let components = fixture.filename.split(separator: ".", maxSplits: 1)
71+
.map { String($0) }
72+
73+
guard
74+
components.count == 2,
75+
let epubURL = Bundle.main.url(
76+
forResource: components[0],
77+
withExtension: components[1],
78+
subdirectory: "Fixtures"
79+
)
80+
else {
81+
throw FixtureError.notFound(fixture)
82+
}
83+
84+
let fileURL = FileURL(url: epubURL)!
85+
86+
let container = Container.shared
87+
let publication = try await container.publication(at: fileURL)
88+
let navigator = try container.navigator(for: publication)
89+
90+
readerViewModel = ReaderViewModel(navigator: navigator)
91+
}
92+
}
93+
94+
enum FixtureError: LocalizedError {
95+
case notFound(PublicationFixture)
96+
97+
var errorDescription: String? {
98+
switch self {
99+
case .notFound:
100+
return "Test EPUB fixture not found in bundle"
101+
}
102+
}
103+
}

0 commit comments

Comments
 (0)