diff --git a/.github/workflows/wpt.yml b/.github/workflows/wpt.yml
index 8a3b8516..89d640ab 100644
--- a/.github/workflows/wpt.yml
+++ b/.github/workflows/wpt.yml
@@ -17,7 +17,7 @@ on:
jobs:
wpt-build-release:
- name: zig build release
+ name: zig build -Dwpt_extensions release
env:
ARCH: aarch64
diff --git a/build.zig b/build.zig
index 83f287e5..781ac02b 100644
--- a/build.zig
+++ b/build.zig
@@ -41,6 +41,7 @@ pub fn build(b: *Build) !void {
const prebuilt_v8_path = b.option([]const u8, "prebuilt_v8_path", "Path to prebuilt libc_v8.a");
const snapshot_path = b.option([]const u8, "snapshot_path", "Path to v8 snapshot");
+ const wpt_extensions = b.option(bool, "wpt_extensions", "Extend WebAPI with WPT driver behavior") orelse false;
const version = resolveVersion(b);
var stdout = std.fs.File.stdout().writer(&.{});
@@ -53,6 +54,7 @@ pub fn build(b: *Build) !void {
opts.addOption([]const u8, "version", version_string);
opts.addOption([]const u8, "version_encoded", version_encoded);
opts.addOption(?[]const u8, "snapshot_path", snapshot_path);
+ opts.addOption(bool, "wpt_extensions", wpt_extensions);
const enable_tsan = b.option(bool, "tsan", "Enable Thread Sanitizer") orelse false;
const enable_asan = b.option(bool, "asan", "Enable Address Sanitizer") orelse false;
diff --git a/src/browser/js/Caller.zig b/src/browser/js/Caller.zig
index 05419752..61ce53a2 100644
--- a/src/browser/js/Caller.zig
+++ b/src/browser/js/Caller.zig
@@ -548,6 +548,7 @@ pub const Function = struct {
pub const Opts = struct {
noop: bool = false,
static: bool = false,
+ wpt_only: bool = false,
deletable: bool = true,
dom_exception: bool = false,
as_typed_array: bool = false,
diff --git a/src/browser/js/Snapshot.zig b/src/browser/js/Snapshot.zig
index b63e26cb..b95b5c29 100644
--- a/src/browser/js/Snapshot.zig
+++ b/src/browser/js/Snapshot.zig
@@ -18,9 +18,11 @@
const std = @import("std");
const lp = @import("lightpanda");
+
const js = @import("js.zig");
const bridge = @import("bridge.zig");
const log = @import("../../log.zig");
+const WebDriver = @import("../webapi/WebDriver.zig");
const IS_DEBUG = @import("builtin").mode == .Debug;
@@ -31,7 +33,7 @@ const WorkerJsApis = bridge.WorkerJsApis;
const Snapshot = @This();
-const embedded_snapshot_blob = if (@import("build_config").snapshot_path) |path| @embedFile(path) else "";
+const embedded_snapshot_blob = if (lp.build_config.snapshot_path) |path| @embedFile(path) else "";
// When creating our Snapshot, we use local function templates for every Zig type.
// You cannot, from what I can tell, create persisted FunctionTemplates at
@@ -335,6 +337,8 @@ fn countExternalReferences() comptime_int {
// +1 for unknownWindowPropertyCallback used on Window's global template
count += 1;
+ const wpt_extensions_enabled = lp.build_config.wpt_extensions;
+
inline for (JsApis) |JsApi| {
if (@hasDecl(JsApi, "constructor")) {
count += 1;
@@ -349,11 +353,17 @@ fn countExternalReferences() comptime_int {
const value = @field(JsApi, d.name);
const T = @TypeOf(value);
if (T == bridge.Accessor) {
+ if (value.wpt_only and wpt_extensions_enabled == false) {
+ continue;
+ }
count += 1;
if (value.setter != null) {
count += 1;
}
} else if (T == bridge.Function) {
+ if (value.wpt_only and wpt_extensions_enabled == false) {
+ continue;
+ }
count += 1;
} else if (T == bridge.Iterator) {
count += 1;
@@ -394,6 +404,8 @@ fn collectExternalReferences() [countExternalReferences()]isize {
references[idx] = @bitCast(@intFromPtr(&bridge.unknownWindowPropertyCallback));
idx += 1;
+ const wpt_extensions_enabled = lp.build_config.wpt_extensions;
+
inline for (JsApis) |JsApi| {
if (@hasDecl(JsApi, "constructor")) {
references[idx] = @bitCast(@intFromPtr(JsApi.constructor.func));
@@ -410,6 +422,10 @@ fn collectExternalReferences() [countExternalReferences()]isize {
const value = @field(JsApi, d.name);
const T = @TypeOf(value);
if (T == bridge.Accessor) {
+ if (value.wpt_only and wpt_extensions_enabled == false) {
+ continue;
+ }
+
references[idx] = @bitCast(@intFromPtr(value.getter));
idx += 1;
if (value.setter) |setter| {
@@ -417,6 +433,9 @@ fn collectExternalReferences() [countExternalReferences()]isize {
idx += 1;
}
} else if (T == bridge.Function) {
+ if (value.wpt_only and wpt_extensions_enabled == false) {
+ continue;
+ }
references[idx] = @bitCast(@intFromPtr(value.func));
idx += 1;
} else if (T == bridge.Iterator) {
@@ -573,6 +592,8 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.F
const declarations = @typeInfo(JsApi).@"struct".decls;
var has_named_index_getter = false;
+ const wpt_extensions_enabled = lp.build_config.wpt_extensions;
+
inline for (declarations) |d| {
const name: [:0]const u8 = d.name;
const value = @field(JsApi, name);
@@ -580,6 +601,10 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.F
switch (definition) {
bridge.Accessor => {
+ if (value.wpt_only and wpt_extensions_enabled == false) {
+ continue;
+ }
+
const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
const getter_signature = if (value.static) null else signature;
const getter_callback = v8.v8__FunctionTemplate__New__Config(isolate, &.{
@@ -614,6 +639,10 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.F
}
},
bridge.Function => {
+ if (value.wpt_only and wpt_extensions_enabled == false) {
+ continue;
+ }
+
// For non-static functions, use the signature to validate the receiver
const func_signature = if (value.static) null else signature;
const function_template = v8.v8__FunctionTemplate__New__Config(isolate, &.{
diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig
index 22cbed30..3e5ab70c 100644
--- a/src/browser/js/bridge.zig
+++ b/src/browser/js/bridge.zig
@@ -17,6 +17,8 @@
// along with this program. If not, see .
const std = @import("std");
+const lp = @import("lightpanda");
+
const js = @import("js.zig");
const Page = @import("../Page.zig");
const Session = @import("../Session.zig");
@@ -134,6 +136,7 @@ pub const Function = struct {
static: bool,
arity: usize,
noop: bool = false,
+ wpt_only: bool = false,
cache: ?Caller.Function.Opts.Caching = null,
func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,
@@ -141,6 +144,7 @@ pub const Function = struct {
return .{
.cache = opts.cache,
.static = opts.static,
+ .wpt_only = opts.wpt_only,
.arity = getArity(@TypeOf(func)),
.func = if (opts.noop) noopFunction else struct {
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
@@ -172,6 +176,7 @@ pub const Function = struct {
pub const Accessor = struct {
static: bool = false,
deletable: bool = true,
+ wpt_only: bool = false,
cache: ?Caller.Function.Opts.Caching = null,
getter: ?*const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void = null,
setter: ?*const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void = null,
@@ -180,6 +185,7 @@ pub const Accessor = struct {
var accessor = Accessor{
.cache = opts.cache,
.static = opts.static,
+ .wpt_only = opts.wpt_only,
.deletable = opts.deletable,
};
@@ -928,4 +934,10 @@ pub const WorkerJsApis = flattenTypes(&.{
// Used by Env (class IDs, templates), JsApiLookup, and anywhere that needs
// to know about all possible types. Individual snapshots use their own
// subsets (PageJsApis, WorkerSnapshot.JsApis).
-pub const JsApis = PageJsApis ++ [_]type{@import("../webapi/WorkerGlobalScope.zig").JsApi};
+pub const JsApis = blk: {
+ const base = PageJsApis ++ [_]type{@import("../webapi/WorkerGlobalScope.zig").JsApi};
+ if (lp.build_config.wpt_extensions == false) {
+ break :blk base;
+ }
+ break :blk base ++ [_]type{@import("../webapi/WebDriver.zig").JsApi};
+};
diff --git a/src/browser/webapi/WebDriver.zig b/src/browser/webapi/WebDriver.zig
new file mode 100644
index 00000000..e432b5d8
--- /dev/null
+++ b/src/browser/webapi/WebDriver.zig
@@ -0,0 +1,43 @@
+// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+const std = @import("std");
+
+const js = @import("../js/js.zig");
+const Session = @import("../Session.zig");
+
+// This type is only included when the binary is built with the -Dwpt_extensions flag
+const WebDriver = @This();
+
+_pad: bool = false,
+
+pub fn deleteAllCookies(_: *const WebDriver, session: *Session) void {
+ session.cookie_jar.clearRetainingCapacity();
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(WebDriver);
+
+ pub const Meta = struct {
+ pub const name = "WebDriver";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ pub const empty_with_no_proto = true;
+ };
+ pub const deleteAllCookies = bridge.function(WebDriver.deleteAllCookies, .{});
+};
diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig
index 9e716e91..01a77495 100644
--- a/src/browser/webapi/Window.zig
+++ b/src/browser/webapi/Window.zig
@@ -571,6 +571,11 @@ pub fn scrollBy(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void {
return self.scrollTo(.{ .x = absx }, absy, page);
}
+// only exposed when the binary is built with the -Dwpt_extensions flag
+pub fn getWebDriver(_: *const Window) @import("WebDriver.zig") {
+ return .{};
+}
+
pub fn unhandledPromiseRejection(self: *Window, no_handler: bool, rejection: js.PromiseRejection, page: *Page) !void {
if (comptime IS_DEBUG) {
log.debug(.js, "unhandled rejection", .{
@@ -916,6 +921,8 @@ pub const JsApi = struct {
return null;
}
}.prompt, .{});
+
+ pub const webdriver = bridge.accessor(Window.getWebDriver, null, .{ .wpt_only = true });
};
const CrossOriginWindow = struct {