Web Inspector Testing¶
Web Inspector tests are layout tests that exercise the browser developer tools built into WebKit. They live under LayoutTests/inspector/ and run via run-webkit-tests. Each test opens a Web Inspector frontend connected to the test page, runs JavaScript in the frontend context, and produces text output compared against an -expected.txt baseline.
The architecture has two sides:
- The test page -- the HTML file loaded by the test runner. It defines a
test()function and includes one of two harness scripts. - The inspector frontend -- a separate JavaScript context where
WI.*is available. Yourtest()function is serialized and evaluated here.
The test() function does not run in the page. It runs inside the inspector. This is the single most important thing to understand.
Your First Test¶
Create LayoutTests/inspector/unit-tests/my-first-test.html:
<!DOCTYPE html>
<html>
<head>
<script src="../../http/tests/inspector/resources/inspector-test.js"></script>
<script>
function test()
{
let suite = InspectorTest.createSyncSuite("MyFirstTest");
suite.addTestCase({
name: "AddingNumbers",
test() {
InspectorTest.expectEqual(1 + 1, 2, "1 + 1 should equal 2.");
return true;
}
});
suite.runTestCasesAndFinish();
}
</script>
</head>
<body onload="runTest()">
<p>Basic arithmetic assertions.</p>
</body>
</html>
The matching my-first-test-expected.txt:
Basic arithmetic assertions.
== Running test suite: MyFirstTest
-- Running test case: AddingNumbers
PASS: 1 + 1 should equal 2.
Run it:
Tools/Scripts/run-webkit-tests inspector/unit-tests/my-first-test.html
Every inspector test follows this pattern: include a harness script, define test() with suites and test cases, and trigger via <body onload="runTest()">.
Two Harnesses¶
| Harness | Script | Global | Use when |
|---|---|---|---|
| Frontend | inspector-test.js |
InspectorTest |
You need WI.* managers, model objects, or the event system |
| Protocol | protocol-test.js |
ProtocolTest |
You need raw protocol messages via InspectorProtocol |
Frontend tests load the full inspector frontend. Protocol tests load a minimal stub (TestStub.js) with only InspectorProtocol and ProtocolTest -- no WI.* namespace.
Async Tests with Protocol Agents¶
Most real tests are asynchronous:
<script src="../../http/tests/inspector/resources/inspector-test.js"></script>
<script>
function test()
{
let suite = InspectorTest.createAsyncSuite("Runtime.BasicEvaluate");
suite.addTestCase({
name: "EvaluateExpression",
async test() {
let response = await WI.mainTarget.RuntimeAgent.evaluate("2 + 3");
InspectorTest.expectEqual(response.result.value, 5, "2 + 3 should equal 5.");
}
});
suite.runTestCasesAndFinish();
}
</script>
Key differences from sync tests: use createAsyncSuite, mark test() as async, and protocol agents (RuntimeAgent, DOMAgent, etc.) return promises. Access agents through the target -- WI.mainTarget.RuntimeAgent -- rather than bare globals; using the agent directly without a target is deprecated.
Testing with Page Content¶
Functions defined outside test() run on the test page. Use evaluateInPage() to bridge from the inspector context:
// Page-side (in <script>, outside test()):
function updateElement() {
document.getElementById("target").setAttribute("data-value", "updated");
}
// Inspector-side (inside test()):
suite.addTestCase({
name: "DOM.AttributeChanged",
async test() {
await Promise.all([
node.awaitEvent(WI.DOMNode.Event.AttributeModified),
InspectorTest.evaluateInPage("updateElement()")
]);
InspectorTest.pass("Attribute modification event received.");
}
});
The Promise.all pattern -- listening for an event while triggering the action -- is the standard way to avoid race conditions. Set up the listener before triggering the action.
Protocol Tests¶
Protocol tests use InspectorProtocol for raw message sending:
// awaitCommand -- returns a Promise resolving with the result
let {root} = await InspectorProtocol.awaitCommand({
method: "DOM.getDocument", params: {}
});
// awaitEvent -- one-shot event listener returning a Promise
let event = await InspectorProtocol.awaitEvent({event: "Debugger.scriptParsed"});
// addEventListener / removeEventListener for persistent listeners
InspectorProtocol.addEventListener("Network.requestWillBeSent", handler);
// Unconditional pass (useful for event-driven tests)
ProtocolTest.pass("Event received successfully.");
Test Suites and Test Cases¶
AsyncTestSuite (most common)¶
Created via InspectorTest.createAsyncSuite("Name") or ProtocolTest.createAsyncSuite("Name"). Test cases run sequentially. Default timeout: 10 seconds (override with timeout property; -1 disables).
SyncTestSuite¶
Created via InspectorTest.createSyncSuite("Name"). Test functions must be non-async and return true for success.
Test Case Shape¶
{
name: "SuiteName.TestCaseName", // Required
description: "What this tests.", // Optional (does not appear in output)
test() { ... }, // Required (sync or async)
setup() { ... }, // Optional, runs before test
teardown() { ... }, // Optional, runs after test
timeout: 10000, // Optional (ms), -1 to disable
}
If setup throws, both test and teardown are skipped. A failing test case does not abort the suite -- remaining cases continue.
Always end with suite.runTestCasesAndFinish().
Assertion API¶
Available on both InspectorTest and ProtocolTest. Each logs PASS: or FAIL: with diagnostics.
Truthiness and Existence¶
| Method | Passes when |
|---|---|
expectThat(actual, msg) |
!!actual is true |
expectFalse(actual, msg) |
!actual is true |
expectNull(actual, msg) |
actual === null |
expectNotNull(actual, msg) |
actual !== null |
expectEmpty(actual, msg) |
Array/Set/Map/object has no entries |
expectNotEmpty(actual, msg) |
Has entries |
Equality and Comparison¶
| Method | Passes when |
|---|---|
expectEqual(actual, expected, msg) |
actual === expected |
expectNotEqual(actual, expected, msg) |
actual !== expected |
expectShallowEqual(actual, expected, msg) |
Object.shallowEqual(actual, expected) |
expectEqualWithAccuracy(actual, expected, accuracy, msg) |
\|actual - expected\| <= accuracy |
expectLessThan(actual, expected, msg) |
actual < expected |
expectGreaterThan(actual, expected, msg) |
actual > expected |
expectException(work) |
work() throws or rejects |
Manual and Logging¶
| Method | Description |
|---|---|
pass(msg) / fail(msg) |
Unconditional PASS/FAIL |
assert(condition, msg) |
Logs ASSERT: msg only on failure |
log(msg) |
Write to test output |
json(obj) |
Pretty-printed JSON to test output |
Helper Scripts and TestPage.registerInitializer¶
Shared utilities in resources/ directories use TestPage.registerInitializer to inject code into the inspector frontend:
// resources/my-helpers.js
TestPage.registerInitializer(() => {
window.myHelper = function() {
InspectorTest.log("Helper called!");
};
});
Include before the test script. The initializer is stringified and evaluated in the frontend before test() runs.
Site Isolation Test Patterns¶
Tests under http/tests/site-isolation/inspector/ verify inspector behavior with cross-process iframes. Key patterns:
- Cross-origin iframes use
localhostvs127.0.0.1(both served by the test HTTP server). Note that127.0.0.1is the default origin for tests underhttp/tests/. - Await new targets via
WI.targetManager.awaitEvent(WI.TargetManager.Event.TargetAdded) - Use
StableIdMapfor deterministic output (target/frame IDs change between runs)
Writing Deterministic Output¶
| Problem | Solution |
|---|---|
| Object/target IDs change | StableIdMap assigns sequential integers |
| File paths differ | TestHarness.sanitizeURL() strips to filename |
| Stack traces | suppressStackTraces = true |
| Nondeterministic ordering | Collect into a set, check membership |
Debugging Tests¶
InspectorBackend.dumpInspectorProtocolMessages = true; // protocol traffic to stderr
InspectorBackend.filterMultiplexingBackendInspectorProtocolMessages = false; // include SI routing
InspectorTest.forceDebugLogging = true; // log() to stderr
InspectorTest.dumpActivityToSystemConsole = true; // lifecycle to stderr
Or use the shortcut: InspectorTest.debug() / ProtocolTest.debug().
Verify event names before use: grep -r "WI.ClassName.Event.Name" Source/WebInspectorUI/
Running Tests¶
Tools/Scripts/run-webkit-tests inspector/ # all
Tools/Scripts/run-webkit-tests inspector/dom/attributeModified.html # single
Tools/Scripts/run-webkit-tests http/tests/site-isolation/inspector/ # SI tests
Tools/Scripts/run-webkit-tests -v inspector/unit-tests/ # verbose
Tools/Scripts/run-webkit-tests --reset-results inspector/my-test.html # rebaseline
Common Patterns¶
Parameterized test cases -- factory function:
function addTestCase({name, expression, expected}) {
suite.addTestCase({
name,
async test() {
let response = await WI.mainTarget.RuntimeAgent.evaluate(expression);
InspectorTest.expectEqual(response.result.value, expected, `${expression} = ${expected}.`);
}
});
}
WebKitTestRunner options via HTML comment: <!DOCTYPE html> <!-- webkit-test-runner [ jscOptions=--useShadowRealm=1 ] -->
Dispatching events from page to frontend:
// Page-side:
TestPage.dispatchEventToFrontend("TestPage-myEvent", {data: 123});
// Inspector-side:
let event = await InspectorTest.awaitEvent("TestPage-myEvent");