Refactor FXIOS-15780 Reader mode Add custom readermode scheme#33846
Refactor FXIOS-15780 Reader mode Add custom readermode scheme#33846Alex-Bangu wants to merge 5 commits into
Conversation
💪 Quality guardian1 tests files modified. You're a champion of test coverage! 🚀 🧹 Tidy commitJust 4 file(s) touched. Thanks for keeping it clean and review-friendly! 💬 Description craftsmanGreat PR description! Reviewers salute you 🫡 🧑💻 New
|
| File | Line | Change |
|---|---|---|
firefox-ios/Client/Frontend/Reader/SchemeHandler/ReaderModeSchemeHandler.swift |
74 | Added |
✅ New file code coverage
All new files meet their thresholds**.
✅ Existing file code coverage
No modified file detected so code coverage gate wasn't ran.
Client.app: Coverage: 40.77
| File | Coverage | |
|---|---|---|
| PageRoute.swift | 94.74% | ✅ |
| ReaderModeSchemeHandler.swift | 76.92% | ✅ |
Generated by 🚫 Danger Swift against 8541d49
0ddee16 to
fef22c6
Compare
ih-codes
left a comment
There was a problem hiding this comment.
Great test cases, thanks @Alex-Bangu ! Most importantly, I appreciate you shrinking the scope of this PR. I was just starting to review as you updated and this was much more manageable for me to get through in a timely fashion. 😄
Nice work overall, I only had some tiny nitpicks but approve of the overall solution. I didn't run the app since this isn't hooked up yet, but I did run the new unit tests and confirmed they were passing for me too.
| import Shared | ||
| import WebEngine | ||
|
|
||
| /// Serves the reader-mode page document at `readermode://app/page?url=<encoded-article-url>`. |
There was a problem hiding this comment.
nitpick: In the plain English code documentation (here and other files), let's be consistent in describing reader mode as "reader mode", "readermode", or "ReaderMode" (the dash was specific to the legacy reader mode url scheme it seems?). I don't have strong feelings on which you prefer, but let's not use the dash and just be consistent across new documentation we add. 😄
| /// - `/app/page-exists` -> (future) | ||
| /// - `/app/styles/...` -> (future, static) | ||
| /// - `/app/fonts/...` -> (future, static) |
There was a problem hiding this comment.
Just curious, what are these other future reader mode routes for, exactly? 🤔
There was a problem hiding this comment.
Oops! Sorry about that, no other paths will be defined besides /app/page.
| /// This initial version validates the incoming URL parameters. Content rendering | ||
| /// (cache integration, readability extraction, error pages) will be added in | ||
| /// FXIOS-15783. |
There was a problem hiding this comment.
Thanks for splitting up the work! 👌
| init(profile: Profile) { | ||
| self.profile = profile | ||
| } | ||
| // Always erros our for now, will actually handle in next PR |
| /// Normalizes any thrown `Error` into a `TinyRouterError`. | ||
| private func mapError(_ error: Error) -> TinyRouterError { | ||
| if let tinyError = error as? TinyRouterError { | ||
| return tinyError | ||
| } | ||
| return .unknown(String(describing: error)) | ||
| } |
There was a problem hiding this comment.
I wonder if something like this would be better written as a (perhaps static?) method on the TinyRouterError itself.
@issammani, looks like you wrote that file initially, so what do you think?
There was a problem hiding this comment.
Yes that would neatly work in the router package itself as a helper.
| /// Validates incoming requests and forwards them to the router. | ||
| func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) { | ||
| let id = ObjectIdentifier(urlSchemeTask) | ||
| let requestTask = Task { @MainActor in |
There was a problem hiding this comment.
Just FYI, since ReaderModeSchemeHandler is, as you noted, already isolated to the main actor, this Task will inherit @MainActor so it is not necessary to write this explicitly.
| subject.webView(webView, start: task) | ||
| wait(for: [failExpectation], timeout: 1.0) | ||
|
|
||
| XCTAssertEqual(task.failedErrors.count, 1) |
There was a problem hiding this comment.
Let's check the other task properties, like on lines 38-41 here as well for completion's sake and extra validation.
Likewise for the other tests in this file.
| @@ -0,0 +1,107 @@ | |||
| // This Source Code Form is subject to the terms of the Mozilla Public | |||
There was a problem hiding this comment.
I noticed we don't have any tests covering correct usage, i.e. good scheme, good host, good URL. Can we add one?
There was a problem hiding this comment.
+1. @Alex-Bangu would be nice if you can also add a test for the default router rejecting files not registered as allowed per this comment
| // Path doesn't match the "page" prefix, so it falls through to the default route | ||
| // (`StaticFileRoute`), which attempts `Bundle.main.url(forResource:withExtension:)` | ||
| // for a resource that does not exist and throws `TinyRouterError.badURL`. |
There was a problem hiding this comment.
Outside the scope of this PR, I have a question for @issammani... Would it make sense for line 17 of StaticFileRoute.swift to return an error that implies the asset is missing from the bundle rather than a badURL? badURL seems a bit vague of an error to receive in this situation when the real issue is an unhandled path. 🤔 In an ideal world we would actually be getting some error for the task suggesting that the real problem is that the URL is an unhandled path, not "bad" as in possibly malformed.
There was a problem hiding this comment.
Yeah that makes sense. As I mentioned earlier in my comment we shouldn't use StaticFileRoute here. @Alex-Bangu feel free to create a new error in TinyRouterError
There was a problem hiding this comment.
Thanks @issammani, an even better suggestion then! 👌
Co-authored-by: Isabella <173110554+ih-codes@users.noreply.github.com>
issammani
left a comment
There was a problem hiding this comment.
Great work so far. I left a few comments we should address before merging 👍
| /// This initial version validates the incoming URL parameters. Content rendering | ||
| /// (cache integration, readability extraction, error pages) will be added in | ||
| /// FXIOS-15783. |
| self.router = TinyRouter() | ||
| .register("page", PageRoute(profile: profile)) | ||
| .setDefault(StaticFileRoute()) |
There was a problem hiding this comment.
From a security perspective, I don't think we should use StaticFileRoute() as is. For translations, the engine is not part of the content process. It's in the background and we have total control over the assets we inject. That's not the case here for reader mode. I would create a router that only allows loading explicit files from reader-mode/ like styles/Reader.css and so on othewise it fails even if the files exist in that path.
| /// Normalizes any thrown `Error` into a `TinyRouterError`. | ||
| private func mapError(_ error: Error) -> TinyRouterError { | ||
| if let tinyError = error as? TinyRouterError { | ||
| return tinyError | ||
| } | ||
| return .unknown(String(describing: error)) | ||
| } |
There was a problem hiding this comment.
Yes that would neatly work in the router package itself as a helper.
| @@ -0,0 +1,107 @@ | |||
| // This Source Code Form is subject to the terms of the Mozilla Public | |||
There was a problem hiding this comment.
+1. @Alex-Bangu would be nice if you can also add a test for the default router rejecting files not registered as allowed per this comment
| // Path doesn't match the "page" prefix, so it falls through to the default route | ||
| // (`StaticFileRoute`), which attempts `Bundle.main.url(forResource:withExtension:)` | ||
| // for a resource that does not exist and throws `TinyRouterError.badURL`. |
There was a problem hiding this comment.
Yeah that makes sense. As I mentioned earlier in my comment we shouldn't use StaticFileRoute here. @Alex-Bangu feel free to create a new error in TinyRouterError
Co-authored-by: Issam Mani <issamouu69@gmail.com>
📜 Tickets
Jira ticket
Github issue
💡 Description
Adds the readermode://app/... scheme handler scaffolding. After this PR the new types are present in the codebase but not yet visible to users. Reader mode still uses the legacy GCDWebServer/localhost path. The actual switch to the new scheme is enabled by a feature flag in FXIOS-15783.
Below is a full list of changes by file:
ReaderModeSchemeHandler.swift (new): The custom scheme handler that owns readermode://app/...; routes incoming requests and picks the right cache based on whether the tab is private.
PageRoute.swift (new): Handles readermode://app/page?url=... requests. Looks up the article in the cache, extracts it if missing, or shows a Load Original error page if extraction fails.
ReadabilityService.swift (modified): Added an async extract method on top of the existing operation so PageRoute can await a result instead of just side-effecting the cache.
ReaderModeCache.swift (modified): Made the protocol Sendable so it can be used as a stored property in Sendable types like PageRoute.
TabConfigurationProvider.swift (modified): Registers the new scheme handler on every tab's WKWebView config and passes the profile through instead of just prefs.
TabManagerImplementation.swift (modified): Updated to pass profile instead of profile.prefs into TabConfigurationProvider's new init.
BrowserViewController+WebViewDelegates.swift (modified): Added a branch in decidePolicyFor that allows readermode:// URLs through to WebKit instead of treating them as external-app links.
URLExtension.swift in BrowserKit/Common (modified): Broadened isReaderModeURL to recognize both the legacy localhost form and the new readermode:// form during the migration window.
ReaderMode.js (modified): Broadened the in-page URL regex to match both forms so the reader script keeps working after the cutover.
MockReaderModeCache.swift (new test): Tiny in-memory cache for tests; throws on miss to match the real caches.
PageRouteTests.swift (new test): Covers URL parsing, cache hit, cache miss with stubbed extractor, and the failure-fallback page.
ReaderModeSchemeHandlerTests.swift (new test): Covers scheme/host validation, nil URL handling, and unknown-path fallback through the default route.
URLExtensionTests.swift (modified test): Added two cases verifying isReaderModeURL accepts the new scheme form and rejects wrong-host variants.
🎥 Demos
No user-end demos to show for this pull request.
📝 Checklist