Skip to content
C.W.K.
Lesson 03 of 05 · published

Testing a Browser Without a Browser

~15 min · testing, test-doubles, swift-testing, fakes

Level 0New Tab
0 XP0/44 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

A page that only takes notes

The parent's test target cannot make a web view, so it brings its own engine. Tests/VoyagerCoreTests/FakeEngine.swift is 242 lines, and its first comment says why it exists: "The parent runs against this instead of WebKit: every rule of tabs, sessions, identity and trust is a plain value here."

Three small classes fill the contract. FakePage conforms to EnginePage and writes things down: every address it is told to load goes into loads, the state it was restored from into restoredFrom, a close into closed. It also has knobs a test turns. finish(title:) pretends the page finished loading, busy is what it answers when the sleep policy asks, context is what Pippa will read. Its captureState() writes the page's address as bytes and tags them "fake". FakePanel keeps every message delivered to Pippa's panel. FakeEngine keeps every page and panel it made, and a cookie jar for each identity.

Then Bench wires a fake engine, a BrowserServices and one BrowserWindowModel together. A test starts with let bench = Bench() and has a working browser with no browser in it.

Reading a test as a specification

The second block below is one session test, and it reads like a sentence about the product. Open three tabs, put the second in front, take a snapshot. Restore that snapshot into a fresh bench. Then the claims: the tab ids come back in order; only the tab in front is awake ([true, false, true] for asleep); every title is kept, even for tabs that have no page; the front tab came back from its stored bytes and loaded nothing; the engine made exactly one page. Select the first tab, and the engine makes a second page, restored from that tab's own bytes.

In executable form, that says a window with forty tabs restores one page, not forty. Real WebKit would add nothing to this test, because the claim is about the parent's decision, and the fake simply writes down what it was asked to do.

How much of the suite needs the fake at all

The parent's target holds 242 tests in 48 suites across 21 test files. Only 11 of those files reach for the fake engine: tab commands, the window model, sessions, settings, privacy, passwords, Pippa's host, the catalog model and a few more. The other 10 need no engine of any kind: trust and departures, address resolution, history, bookmarks, suggestions, the Chromium import, the mouse's history step. Trust, for one, is tested by building a PermissionRequest by hand and asking the policy, which is where the previous lesson's plain values pay off.

What a fake cannot prove

A fake is the parent's description of an engine. It proves the parent behaves correctly toward an engine that behaves the way the fake does. Whether WebKit behaves that way is a different claim, and the fake is the one instrument that can never test it.

ClaimProved by
A restored window wakes only the tab in frontParent suite, fake engine
A stored state written by another engine is ignored and the address loadedParent suite, fake engine
WebKit's bytes really bring a scroll position backReal WebKit suite, aPlaceComesBackFromBytes
The child answers every selector WebKit looks for, so a misnamed delegate method cannot hideReal WebKit suite, the selector test (lesson 4), which asks responds(to:) and never waits for a call
WebKit really calls the capture method with a page's requestThe gate's browser probe, scripts/probes/run-gate-browser.sh --auto (W6), because a page's microphone request is answered only in a visible window
A page presents Safari's token in its user agentReal WebKit suite, itPresentsSafarisToken
The window, the tab strip and the keyboard work togetherThe verification build's walk through its own screens, scripts/smoke.sh

The table also tells you when to stop extending a fake. When a test needs the fake to imitate WebKit in some detail, that test belongs in the other target.

One rule, held on both sides

The third block is the parent's half of a rule from the contract: a PageState written by another engine is ignored, and the tab loads the address instead. The child holds the other half with a real-WebKit test, anotherEnginesBytesAreNotFedToWebKit, which makes a page from bytes tagged not-webkit and expects it to stay empty. The parent checks the tag before it hands the bytes over, and the child checks again before it believes them. Two tests in two targets, for one sentence of the contract.

Principle: Test a decision where it is made. A rule of the parent gets a test against a fake; a promise about the engine gets a test against the engine.

Code

cwkVoyager, Tests/VoyagerCoreTests/FakeEngine.swift: the fake page that records (verbatim, lines 1-36)·swift
// The parent runs against this instead of WebKit: every rule of tabs,
// sessions, identity and trust is a plain value here.

import Foundation
@testable import VoyagerCore

@MainActor
final class FakePage: EnginePage {
    let identity: IdentityID
    let surface = EngineSurface(view: NSObject())
    var status = PageStatus()
    weak var delegate: (any EnginePageDelegate)?
    var zoom: Double = 1
    var isMuted = false

    var loads: [URL] = []
    var restoredFrom: PageState?
    var closed = false

    init(identity: IdentityID) {
        self.identity = identity
    }

    func load(_ url: URL) {
        loads.append(url)
        status = PageStatus(url: url, isLoading: true, progress: 0.1)
        delegate?.pageStatusDidChange(self)
    }

    /// The page finished loading with this title.
    func finish(title: String) {
        status.title = title
        status.isLoading = false
        status.progress = 1
        delegate?.pageStatusDidChange(self)
    }
cwkVoyager, Tests/VoyagerCoreTests/SessionTests.swift: a restored window wakes only the tab in front (verbatim, lines 7-34)·swift
    @Test func aRestoredWindowWakesOnlyTheTabInFront() {
        let source = Bench()
        let a = source.window.openTab(url: url("https://a.example/"))
        source.page(of: a)?.finish(title: "A")
        let b = source.window.openTab(url: url("https://b.example/"))
        source.page(of: b)?.finish(title: "B")
        source.window.openTab(url: url("https://c.example/"))
        source.window.select(b.id)
        let stored = source.window.snapshot(frame: "{{10, 20}, {800, 600}}")
        #expect(stored.selected == 1)
        #expect(stored.frame == "{{10, 20}, {800, 600}}")

        let bench = Bench(identities: source.services.identities)
        bench.window.restore(stored)
        #expect(bench.window.tabs.map(\.id) == source.window.tabs.map(\.id))
        #expect(bench.window.tabs.map(\.isAsleep) == [true, false, true])
        #expect(bench.window.tabs.map(\.status.title) == ["A", "B", ""])
        // The tab in front came back from its stored place, not by loading.
        let front = bench.page(of: bench.window.tabs[1])
        #expect(front?.restoredFrom == stored.tabs[1].state)
        #expect(front?.loads.isEmpty == true)
        #expect(bench.engine.pages.count == 1)

        // A sleeping tab wakes when it is selected, where it was.
        bench.window.select(bench.window.tabs[0].id)
        #expect(bench.engine.pages.count == 2)
        #expect(bench.page(of: bench.window.tabs[0])?.restoredFrom == stored.tabs[0].state)
    }
cwkVoyager, Tests/VoyagerCoreTests/SessionTests.swift: another engine's state is ignored and the address loaded (verbatim, lines 50-61)·swift
    @Test func anotherEnginesStateIsIgnoredAndTheAddressLoaded() {
        let bench = Bench()
        let stored = SessionWindow(
            tabs: [SessionTab(
                id: UUID(), identity: bench.services.defaultIdentity, url: url("https://a.example/"),
                title: "A", state: PageState(engine: "some-other-engine", bytes: Data([1, 2, 3])))],
            selected: 0)
        bench.window.restore(stored)
        let page = bench.page(of: bench.window.tabs[0])
        #expect(page?.restoredFrom == nil)
        #expect(page?.loads == [url("https://a.example/")])
    }

External links

Exercise

Write a fake for the contract you drew in the previous lesson's exercise: a class that records every call in an array and has a property for every answer. Write three tests of your app's own rules against it, each one readable as a sentence about the product. Then name one claim the fake cannot prove, and write that test against the real framework in a separate test target.
Hint
Keep the fake simple: arrays for what it was asked, properties for what it answers. The moment you catch yourself copying a production rule into the fake, call the production function from the fake instead, the way Voyager's fake calls RequestCookies.header.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.