diff --git a/src/browser/js/Local.zig b/src/browser/js/Local.zig
index a45b35df..f913dafd 100644
--- a/src/browser/js/Local.zig
+++ b/src/browser/js/Local.zig
@@ -1212,6 +1212,12 @@ pub fn rejectPromise(self: *const Local, value: anytype) !js.Promise {
return resolver.promise();
}
+pub fn rejectErrorPromise(self: *const Local, value: js.PromiseResolver.RejectError) !js.Promise {
+ var resolver = js.PromiseResolver.init(self);
+ resolver.rejectError("Local.rejectPromise", value);
+ return resolver.promise();
+}
+
pub fn resolvePromise(self: *const Local, value: anytype) !js.Promise {
var resolver = js.PromiseResolver.init(self);
resolver.resolve("Local.resolvePromise", value);
diff --git a/src/browser/js/PromiseResolver.zig b/src/browser/js/PromiseResolver.zig
index 67f04311..6386569a 100644
--- a/src/browser/js/PromiseResolver.zig
+++ b/src/browser/js/PromiseResolver.zig
@@ -18,7 +18,9 @@
const js = @import("js.zig");
const v8 = js.v8;
+
const log = @import("../../log.zig");
+const DOMException = @import("../webapi/DOMException.zig");
const PromiseResolver = @This();
@@ -63,14 +65,19 @@ pub fn reject(self: PromiseResolver, comptime source: []const u8, value: anytype
};
}
-const RejectError = union(enum) {
+pub const RejectError = union(enum) {
generic: []const u8,
type_error: []const u8,
+ dom_exception: anyerror,
};
pub fn rejectError(self: PromiseResolver, comptime source: []const u8, err: RejectError) void {
const handle = switch (err) {
.type_error => |str| self.local.isolate.createTypeError(str),
.generic => |str| self.local.isolate.createError(str),
+ .dom_exception => |exception| {
+ self.reject(source, DOMException.fromError(exception));
+ return;
+ },
};
self._reject(js.Value{ .handle = handle, .local = self.local }) catch |reject_err| {
log.err(.bug, "rejectError", .{ .source = source, .err = reject_err, .persistent = false });
diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig
index 1b035837..dd9a81b4 100644
--- a/src/browser/js/bridge.zig
+++ b/src/browser/js/bridge.zig
@@ -726,7 +726,6 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/Console.zig"),
@import("../webapi/Crypto.zig"),
@import("../webapi/Permissions.zig"),
- @import("../webapi/Permissions.zig").PermissionStatus,
@import("../webapi/StorageManager.zig"),
@import("../webapi/CSS.zig"),
@import("../webapi/css/CSSRule.zig"),
diff --git a/src/browser/tests/window/navigator.html b/src/browser/tests/navigator/navigator.html
similarity index 56%
rename from src/browser/tests/window/navigator.html
rename to src/browser/tests/navigator/navigator.html
index df9f436a..b82c6e38 100644
--- a/src/browser/tests/window/navigator.html
+++ b/src/browser/tests/navigator/navigator.html
@@ -27,3 +27,44 @@
testing.expectEqual(false, navigator.javaEnabled());
testing.expectEqual(false, navigator.webdriver);
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/tests/navigator/permissions.html b/src/browser/tests/navigator/permissions.html
deleted file mode 100644
index befecf8b..00000000
--- a/src/browser/tests/navigator/permissions.html
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/src/browser/webapi/Navigator.zig b/src/browser/webapi/Navigator.zig
index 36d32f1e..83b883b1 100644
--- a/src/browser/webapi/Navigator.zig
+++ b/src/browser/webapi/Navigator.zig
@@ -27,8 +27,8 @@ const StorageManager = @import("StorageManager.zig");
const Navigator = @This();
_pad: bool = false,
_plugins: PluginArray = .{},
-_permissions: Permissions = Permissions.init,
-_storage: StorageManager = StorageManager.init,
+_permissions: Permissions = .{},
+_storage: StorageManager = .{},
pub const init: Navigator = .{};
@@ -70,7 +70,7 @@ pub fn getStorage(self: *Navigator) *StorageManager {
pub fn getBattery(_: *const Navigator, page: *Page) !js.Promise {
// Battery API is not supported in headless mode.
// Return a rejected Promise — callers must .catch() this.
- return js.Promise.reject(error.NotSupportedError, page);
+ return page.js.local.?.rejectErrorPromise(.{ .dom_exception = error.NotSupported });
}
pub fn registerProtocolHandler(_: *const Navigator, scheme: []const u8, url: [:0]const u8, page: *const Page) !void {
@@ -179,3 +179,8 @@ pub const JsApi = struct {
pub const permissions = bridge.accessor(Navigator.getPermissions, null, .{});
pub const storage = bridge.accessor(Navigator.getStorage, null, .{});
};
+
+const testing = @import("../../testing.zig");
+test "WebApi: Navigator" {
+ try testing.htmlRunner("navigator", .{});
+}
diff --git a/src/browser/webapi/Permissions.zig b/src/browser/webapi/Permissions.zig
index 5f5954b3..ee197d3f 100644
--- a/src/browser/webapi/Permissions.zig
+++ b/src/browser/webapi/Permissions.zig
@@ -1,28 +1,70 @@
-// src/browser/webapi/Permissions.zig
+// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
-// Minimal Permissions API stub.
-// https://www.w3.org/TR/permissions/
+// Francis Bouvier
+// Pierre Tachoire
//
-// Turnstile probes: navigator.permissions.query({ name: 'notifications' })
-// It expects a Promise resolving to { state: 'granted' | 'denied' | 'prompt' }
+// 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 Page = @import("../Page.zig");
+const Session = @import("../Session.zig");
+
+const Allocator = std.mem.Allocator;
+
+pub fn registerTypes() []const type {
+ return &.{ Permissions, PermissionStatus };
+}
const Permissions = @This();
// Padding to avoid zero-size struct pointer collisions
_pad: bool = false,
-pub const init: Permissions = .{};
-
const QueryDescriptor = struct {
name: []const u8,
};
+// We always report 'prompt' (the default safe value — neither granted nor denied).
+pub fn query(_: *const Permissions, qd: QueryDescriptor, page: *Page) !js.Promise {
+ const arena = try page.getArena(.{ .debug = "PermissionStatus" });
+ errdefer page.releaseArena(arena);
+
+ const status = try arena.create(PermissionStatus);
+ status.* = .{
+ ._arena = arena,
+ ._state = "prompt",
+ ._name = try arena.dupe(u8, qd.name),
+ };
+ return page.js.local.?.resolvePromise(status);
+}
const PermissionStatus = struct {
- state: []const u8,
+ _arena: Allocator,
+ _name: []const u8,
+ _state: []const u8,
+
+ pub fn deinit(self: *PermissionStatus, _: bool, session: *Session) void {
+ session.releaseArena(self._arena);
+ }
+
+ fn getName(self: *const PermissionStatus) []const u8 {
+ return self._name;
+ }
+
+ fn getState(self: *const PermissionStatus) []const u8 {
+ return self._state;
+ }
pub const JsApi = struct {
pub const bridge = js.Bridge(PermissionStatus);
@@ -30,23 +72,14 @@ const PermissionStatus = struct {
pub const name = "PermissionStatus";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
- pub const empty_with_no_proto = true;
+ pub const weak = true;
+ pub const finalizer = bridge.finalizer(PermissionStatus.deinit);
};
+ pub const name = bridge.accessor(getName, null, .{});
pub const state = bridge.accessor(getState, null, .{});
};
-
- fn getState(self: *const PermissionStatus) []const u8 {
- return self.state;
- }
};
-// query() returns a Promise.
-// We always report 'prompt' (the default safe value — neither granted nor denied).
-pub fn query(_: *const Permissions, _: QueryDescriptor, page: *Page) !js.Promise {
- const status = try page._factory.create(PermissionStatus{ .state = "prompt" });
- return js.Promise.resolve(status, page);
-}
-
pub const JsApi = struct {
pub const bridge = js.Bridge(Permissions);
diff --git a/src/browser/webapi/StorageManager.zig b/src/browser/webapi/StorageManager.zig
index 280614a0..e7b95cc4 100644
--- a/src/browser/webapi/StorageManager.zig
+++ b/src/browser/webapi/StorageManager.zig
@@ -1,18 +1,51 @@
-// src/browser/webapi/StorageManager.zig
-// Minimal stub for navigator.storage
-// https://storage.spec.whatwg.org/#storagemanager
+// 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 js = @import("../js/js.zig");
const Page = @import("../Page.zig");
+pub fn registerTypes() []const type {
+ return &.{ StorageManager, StorageEstimate };
+}
+
const StorageManager = @This();
+
_pad: bool = false,
-pub const init: StorageManager = .{};
+pub fn estimate(_: *const StorageManager, page: *Page) !js.Promise {
+ const est = try page._factory.create(StorageEstimate{
+ ._usage = 0,
+ ._quota = 1024 * 1024 * 1024, // 1 GiB
+ });
+ return page.js.local.?.resolvePromise(est);
+}
const StorageEstimate = struct {
- quota: u64,
- usage: u64,
+ _quota: u64,
+ _usage: u64,
+
+ fn getUsage(self: *const StorageEstimate) u64 {
+ return self._usage;
+ }
+
+ fn getQuota(self: *const StorageEstimate) u64 {
+ return self._quota;
+ }
pub const JsApi = struct {
pub const bridge = js.Bridge(StorageEstimate);
@@ -20,30 +53,12 @@ const StorageEstimate = struct {
pub const name = "StorageEstimate";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
- pub const empty_with_no_proto = true;
};
pub const quota = bridge.accessor(getQuota, null, .{});
pub const usage = bridge.accessor(getUsage, null, .{});
};
-
- fn getQuota(self: *const StorageEstimate) u64 {
- return self.quota;
- }
- fn getUsage(self: *const StorageEstimate) u64 {
- return self.usage;
- }
};
-// Returns a resolved Promise with plausible stub values.
-// quota = 1GB, usage = 0 (headless browser has no real storage)
-pub fn estimate(_: *const StorageManager, page: *Page) !js.Promise {
- const est = try page._factory.create(StorageEstimate{
- .quota = 1024 * 1024 * 1024, // 1 GiB
- .usage = 0,
- });
- return js.Promise.resolve(est, page);
-}
-
pub const JsApi = struct {
pub const bridge = js.Bridge(StorageManager);
pub const Meta = struct {