Six files, and where WebKit lives
Sources/VoyagerWebKit/ is six files, and the browser's WebKit lives in them.
| File | Lines | What it is |
|---|---|---|
WebKitEngine.swift | 262 | The BrowserEngine: one data store per identity, the configuration every page starts from, the user agent, downloads |
WebKitPage.swift | 526 | "One WKWebView as an EnginePage" |
WebKitPanel.swift | 314 | Pippa's panel as a PanelSurface, with the gates on every message it sends up |
WebKitExtensionHost.swift | 705 | One WKWebExtensionController and a context per installed package (track 8) |
PageScripts.swift | 591 | The scripts that read a page for Pippa, in a content world of Voyager's own (track 7) |
PagePointer.swift | 171 | What the pointer is over, the context menu, and the Mac's web view subclass |
The engine's platform branches are here too: 20 #if os(macOS) lines across five of the six files, against none in the parent. One honest footnote. The Mac shell has a single WebKit import of its own, in SmokeRun.swift, the verification build's walk through its own screens: it casts a page's surface to WKWebView to run a script in it and to see which view holds the keyboard after a tab switch. Its own comment says it "runs only when the isolation gate permits", so it is an instrument, never part of browsing. The phone app imports WebKit nowhere.
One identity, one store
To the parent, an IdentityID is a UUID and nothing more. What an identity is to WebKit, its cookies and site storage, first appears in the child: store(for:) makes one WKWebsiteDataStore per identity, named by that UUID, and a store that lives only in memory for the private identity. Track 5 is about what that makes possible. Here, look at the boundary in the second function of the first block below. When the parent needs Firelink's hub cookie for one request, the child reads the identity's cookies itself and hands up only the header built from the names the parent asked for. The parent never holds a cookie store.
Translating WebKit's questions
The header of WebKitPage.swift states its whole job: "Everything WebKit asks its host to decide is turned into the contract's question and handed to the parent." Two examples:
- A new window. WebKit asks its UI delegate for a web view. The child makes a
WebKitPagefrom the configuration WebKit passed in, which carries the opener's data store, so the pop-up is in the opener's identity by construction. Then it asks the parent, with aNewPageRequest, whether to keep it. If the parent says no, the child closes it and hands WebKit nothing. - The microphone or the camera. WebKit asks with a security origin, a frame and a capture type. The child builds a
PermissionRequest, asks the parent, and maps the answer back, as the third block shows: grant to grant, deny to deny, ask to WebKit's own prompt.
The same file wakes a sleeping tab. Handed a PageState, it puts the bytes into the web view's interactionState only when the state's engine name is its own, which is the child's half of the rule lesson 3 tested on the parent's side.
The test for a silent failure
Most Swift mistakes are loud. This one is not. Under its Swift overlay WebKit is an Objective-C framework, and it decides whether to call a delegate method by asking the delegate whether it responds to that method's Objective-C selector. Write an async Swift method whose argument labels are a little off and it compiles, it is never called, and nothing says so. The page's request simply never arrives.
The trap is easy to fall into because WebKit's async methods have Swift names of their own. In Swift the media-capture decision is webView(_:decideMediaCapturePermissionsFor:initiatedBy:type:) async, while the selector WebKit looks for is webView:requestMediaCapturePermissionForOrigin:initiatedByFrame:type:decisionHandler:. The house sign says to grep the build log for "nearly matches" after adding any delegate method. The test in the second block turns that advice into a rule: fourteen selectors the page must answer, each checked with responds(to:) on a real WebKitPage, plus three more for the engine's download delegate. Trust zones live behind one of the fourteen. A typo there would quietly hand every microphone request back to WebKit's default, and the family's grant would never be consulted.
The real-WebKit suite
VoyagerWebKitTests runs 63 tests in 8 suites against WebKit inside the test process. Every suite is .serialized, with a time limit of one or two minutes. The page tests stay off the network: their pages are data: addresses, or HTML loaded under a made-up secure origin, https://voyager.test/. Their recorder denies every permission, so no system prompt appears and nothing is captured.
One gotcha the house sign records: a WebKit test that awaits something a hidden page never does hangs the whole run, in silence. getUserMedia, for one, is not answered until the page is in a visible window, and swift test prints nothing until it ends. The way out is to run one test at a time under an alarm, so the test that hangs names itself.