Merge pull request #1911 from lightpanda-io/fix/turnstile-300030-missing-navigator-apis

Fix/turnstile 300030 missing navigator apis
This commit is contained in:
Karl Seguin
2026-03-19 20:26:27 +08:00
committed by GitHub
7 changed files with 252 additions and 1 deletions

View File

@@ -1212,6 +1212,12 @@ pub fn rejectPromise(self: *const Local, value: anytype) !js.Promise {
return resolver.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 { pub fn resolvePromise(self: *const Local, value: anytype) !js.Promise {
var resolver = js.PromiseResolver.init(self); var resolver = js.PromiseResolver.init(self);
resolver.resolve("Local.resolvePromise", value); resolver.resolve("Local.resolvePromise", value);

View File

@@ -18,7 +18,9 @@
const js = @import("js.zig"); const js = @import("js.zig");
const v8 = js.v8; const v8 = js.v8;
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const DOMException = @import("../webapi/DOMException.zig");
const PromiseResolver = @This(); 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, generic: []const u8,
type_error: []const u8, type_error: []const u8,
dom_exception: anyerror,
}; };
pub fn rejectError(self: PromiseResolver, comptime source: []const u8, err: RejectError) void { pub fn rejectError(self: PromiseResolver, comptime source: []const u8, err: RejectError) void {
const handle = switch (err) { const handle = switch (err) {
.type_error => |str| self.local.isolate.createTypeError(str), .type_error => |str| self.local.isolate.createTypeError(str),
.generic => |str| self.local.isolate.createError(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| { self._reject(js.Value{ .handle = handle, .local = self.local }) catch |reject_err| {
log.err(.bug, "rejectError", .{ .source = source, .err = reject_err, .persistent = false }); log.err(.bug, "rejectError", .{ .source = source, .err = reject_err, .persistent = false });

View File

@@ -725,6 +725,8 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/collections.zig"), @import("../webapi/collections.zig"),
@import("../webapi/Console.zig"), @import("../webapi/Console.zig"),
@import("../webapi/Crypto.zig"), @import("../webapi/Crypto.zig"),
@import("../webapi/Permissions.zig"),
@import("../webapi/StorageManager.zig"),
@import("../webapi/CSS.zig"), @import("../webapi/CSS.zig"),
@import("../webapi/css/CSSRule.zig"), @import("../webapi/css/CSSRule.zig"),
@import("../webapi/css/CSSRuleList.zig"), @import("../webapi/css/CSSRuleList.zig"),

View File

@@ -27,3 +27,44 @@
testing.expectEqual(false, navigator.javaEnabled()); testing.expectEqual(false, navigator.javaEnabled());
testing.expectEqual(false, navigator.webdriver); testing.expectEqual(false, navigator.webdriver);
</script> </script>
<script id=permission_query>
testing.async(async (restore) => {
const p = navigator.permissions.query({ name: 'notifications' });
testing.expectTrue(p instanceof Promise);
const status = await p;
restore();
testing.expectEqual('prompt', status.state);
testing.expectEqual('notifications', status.name);
});
</script>
<script id=storage_estimate>
testing.async(async (restore) => {
const p = navigator.storage.estimate();
testing.expectTrue(p instanceof Promise);
const estimate = await p;
restore();
testing.expectEqual(0, estimate.usage);
testing.expectEqual(1024 * 1024 * 1024, estimate.quota);
});
</script>
<script id=deviceMemory>
testing.expectEqual(8, navigator.deviceMemory);
</script>
<script id=getBattery>
testing.async(async (restore) => {
const p = navigator.getBattery();
try {
await p;
testing.fail('getBattery should reject');
} catch (err) {
restore();
testing.expectEqual('NotSupportedError', err.name);
}
});
</script>

View File

@@ -18,13 +18,21 @@
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const log = @import("../../log.zig");
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
const PluginArray = @import("PluginArray.zig"); const PluginArray = @import("PluginArray.zig");
const Permissions = @import("Permissions.zig");
const StorageManager = @import("StorageManager.zig");
const Navigator = @This(); const Navigator = @This();
_pad: bool = false, _pad: bool = false,
_plugins: PluginArray = .{}, _plugins: PluginArray = .{},
_permissions: Permissions = .{},
_storage: StorageManager = .{},
pub const init: Navigator = .{}; pub const init: Navigator = .{};
@@ -55,6 +63,19 @@ pub fn getPlugins(self: *Navigator) *PluginArray {
return &self._plugins; return &self._plugins;
} }
pub fn getPermissions(self: *Navigator) *Permissions {
return &self._permissions;
}
pub fn getStorage(self: *Navigator) *StorageManager {
return &self._storage;
}
pub fn getBattery(_: *const Navigator, page: *Page) !js.Promise {
log.info(.not_implemented, "navigator.getBattery", .{});
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 { pub fn registerProtocolHandler(_: *const Navigator, scheme: []const u8, url: [:0]const u8, page: *const Page) !void {
try validateProtocolHandlerScheme(scheme); try validateProtocolHandlerScheme(scheme);
try validateProtocolHandlerURL(url, page); try validateProtocolHandlerURL(url, page);
@@ -144,6 +165,7 @@ pub const JsApi = struct {
pub const onLine = bridge.property(true, .{ .template = false }); pub const onLine = bridge.property(true, .{ .template = false });
pub const cookieEnabled = bridge.property(true, .{ .template = false }); pub const cookieEnabled = bridge.property(true, .{ .template = false });
pub const hardwareConcurrency = bridge.property(4, .{ .template = false }); pub const hardwareConcurrency = bridge.property(4, .{ .template = false });
pub const deviceMemory = bridge.property(@as(f64, 8.0), .{ .template = false });
pub const maxTouchPoints = bridge.property(0, .{ .template = false }); pub const maxTouchPoints = bridge.property(0, .{ .template = false });
pub const vendor = bridge.property("", .{ .template = false }); pub const vendor = bridge.property("", .{ .template = false });
pub const product = bridge.property("Gecko", .{ .template = false }); pub const product = bridge.property("Gecko", .{ .template = false });
@@ -156,4 +178,12 @@ pub const JsApi = struct {
// Methods // Methods
pub const javaEnabled = bridge.function(Navigator.javaEnabled, .{}); pub const javaEnabled = bridge.function(Navigator.javaEnabled, .{});
pub const getBattery = bridge.function(Navigator.getBattery, .{});
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", .{});
}

View File

@@ -0,0 +1,94 @@
// Copyright (C) 2023-2025 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 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,
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 {
_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);
pub const Meta = struct {
pub const name = "PermissionStatus";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
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, .{});
};
};
pub const JsApi = struct {
pub const bridge = js.Bridge(Permissions);
pub const Meta = struct {
pub const name = "Permissions";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const empty_with_no_proto = true;
};
pub const query = bridge.function(Permissions.query, .{ .dom_exception = true });
};

View File

@@ -0,0 +1,71 @@
// 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 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 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,
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);
pub const Meta = struct {
pub const name = "StorageEstimate";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const quota = bridge.accessor(getQuota, null, .{});
pub const usage = bridge.accessor(getUsage, null, .{});
};
};
pub const JsApi = struct {
pub const bridge = js.Bridge(StorageManager);
pub const Meta = struct {
pub const name = "StorageManager";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const empty_with_no_proto = true;
};
pub const estimate = bridge.function(StorageManager.estimate, .{});
};