mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 17:46:32 -04:00
Add WPT extensions
Some WPT tests need to interact with the browser in a way that isn't possible with web apis. Browsers need to expose a way for tests to do this and then use the testdriver-vendor.js to hook into these special WPT actions. This commit sets up the infrastructure for supporting this and includes the delete_all_cookies functionality needed by various cookie tests (e.g. /cookies/attributes/attributes-ctl.sub.html). A new compilation flag, `-Dwpt_extensions`, can be specified. When specified a `window.webdriver` accessor is defined and a `WebDriver` type is exposed. Note that, while I only implemented delete_all_cookies for now, I've seen other tests fail because of missing vendor-specific implementation.
This commit is contained in:
2
.github/workflows/wpt.yml
vendored
2
.github/workflows/wpt.yml
vendored
@@ -17,7 +17,7 @@ on:
|
||||
|
||||
jobs:
|
||||
wpt-build-release:
|
||||
name: zig build release
|
||||
name: zig build -Dwpt_extensions release
|
||||
|
||||
env:
|
||||
ARCH: aarch64
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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, &.{
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
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};
|
||||
};
|
||||
|
||||
43
src/browser/webapi/WebDriver.zig
Normal file
43
src/browser/webapi/WebDriver.zig
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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, .{});
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user