mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-21 20:24:42 +00:00
Fix Navigator Additions
Follow up to https://github.com/lightpanda-io/browser/pull/1884 Fixes build, uses arena/finalizer for PermissionStatus. Fixes tests. A few other small cleanups.
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -726,7 +726,6 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/Console.zig"),
|
@import("../webapi/Console.zig"),
|
||||||
@import("../webapi/Crypto.zig"),
|
@import("../webapi/Crypto.zig"),
|
||||||
@import("../webapi/Permissions.zig"),
|
@import("../webapi/Permissions.zig"),
|
||||||
@import("../webapi/Permissions.zig").PermissionStatus,
|
|
||||||
@import("../webapi/StorageManager.zig"),
|
@import("../webapi/StorageManager.zig"),
|
||||||
@import("../webapi/CSS.zig"),
|
@import("../webapi/CSS.zig"),
|
||||||
@import("../webapi/css/CSSRule.zig"),
|
@import("../webapi/css/CSSRule.zig"),
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html><body>
|
|
||||||
<script id=permissions_query_returns_promise>
|
|
||||||
const p = navigator.permissions.query({ name: 'notifications' });
|
|
||||||
console.assert(p instanceof Promise, 'permissions.query must return a Promise');
|
|
||||||
p.then(status => {
|
|
||||||
console.assert(typeof status.state === 'string', 'state must be a string');
|
|
||||||
console.assert(
|
|
||||||
['granted','denied','prompt'].includes(status.state),
|
|
||||||
'state must be valid'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script id=storage_estimate_returns_promise>
|
|
||||||
const p = navigator.storage.estimate();
|
|
||||||
console.assert(p instanceof Promise, 'storage.estimate must return a Promise');
|
|
||||||
p.then(e => {
|
|
||||||
console.assert(typeof e.quota === 'number', 'quota must be a number');
|
|
||||||
console.assert(typeof e.usage === 'number', 'usage must be a number');
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script id=device_memory_is_number>
|
|
||||||
console.assert(
|
|
||||||
typeof navigator.deviceMemory === 'number',
|
|
||||||
'deviceMemory must be a number, got: ' + typeof navigator.deviceMemory
|
|
||||||
);
|
|
||||||
console.assert(navigator.deviceMemory > 0, 'deviceMemory must be positive');
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script id=get_battery_returns_rejected_promise>
|
|
||||||
const p = navigator.getBattery();
|
|
||||||
console.assert(p instanceof Promise, 'getBattery must return a Promise');
|
|
||||||
p.catch(e => console.log('getBattery correctly rejected:', e));
|
|
||||||
</script>
|
|
||||||
</body></html>
|
|
||||||
@@ -27,8 +27,8 @@ const StorageManager = @import("StorageManager.zig");
|
|||||||
const Navigator = @This();
|
const Navigator = @This();
|
||||||
_pad: bool = false,
|
_pad: bool = false,
|
||||||
_plugins: PluginArray = .{},
|
_plugins: PluginArray = .{},
|
||||||
_permissions: Permissions = Permissions.init,
|
_permissions: Permissions = .{},
|
||||||
_storage: StorageManager = StorageManager.init,
|
_storage: StorageManager = .{},
|
||||||
|
|
||||||
pub const init: Navigator = .{};
|
pub const init: Navigator = .{};
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ pub fn getStorage(self: *Navigator) *StorageManager {
|
|||||||
pub fn getBattery(_: *const Navigator, page: *Page) !js.Promise {
|
pub fn getBattery(_: *const Navigator, page: *Page) !js.Promise {
|
||||||
// Battery API is not supported in headless mode.
|
// Battery API is not supported in headless mode.
|
||||||
// Return a rejected Promise — callers must .catch() this.
|
// 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 {
|
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 permissions = bridge.accessor(Navigator.getPermissions, null, .{});
|
||||||
pub const storage = bridge.accessor(Navigator.getStorage, null, .{});
|
pub const storage = bridge.accessor(Navigator.getStorage, null, .{});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const testing = @import("../../testing.zig");
|
||||||
|
test "WebApi: Navigator" {
|
||||||
|
try testing.htmlRunner("navigator", .{});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,28 +1,70 @@
|
|||||||
// src/browser/webapi/Permissions.zig
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
//
|
//
|
||||||
// Minimal Permissions API stub.
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// https://www.w3.org/TR/permissions/
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
//
|
//
|
||||||
// Turnstile probes: navigator.permissions.query({ name: 'notifications' })
|
// This program is free software: you can redistribute it and/or modify
|
||||||
// It expects a Promise resolving to { state: 'granted' | 'denied' | 'prompt' }
|
// 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 std = @import("std");
|
||||||
const js = @import("../js/js.zig");
|
const js = @import("../js/js.zig");
|
||||||
const Page = @import("../Page.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();
|
const Permissions = @This();
|
||||||
|
|
||||||
// Padding to avoid zero-size struct pointer collisions
|
// Padding to avoid zero-size struct pointer collisions
|
||||||
_pad: bool = false,
|
_pad: bool = false,
|
||||||
|
|
||||||
pub const init: Permissions = .{};
|
|
||||||
|
|
||||||
const QueryDescriptor = struct {
|
const QueryDescriptor = struct {
|
||||||
name: []const u8,
|
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 {
|
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 JsApi = struct {
|
||||||
pub const bridge = js.Bridge(PermissionStatus);
|
pub const bridge = js.Bridge(PermissionStatus);
|
||||||
@@ -30,23 +72,14 @@ const PermissionStatus = struct {
|
|||||||
pub const name = "PermissionStatus";
|
pub const name = "PermissionStatus";
|
||||||
pub const prototype_chain = bridge.prototypeChain();
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
pub var class_id: bridge.ClassId = undefined;
|
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, .{});
|
pub const state = bridge.accessor(getState, null, .{});
|
||||||
};
|
};
|
||||||
|
|
||||||
fn getState(self: *const PermissionStatus) []const u8 {
|
|
||||||
return self.state;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// query() returns a Promise<PermissionStatus>.
|
|
||||||
// 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 JsApi = struct {
|
||||||
pub const bridge = js.Bridge(Permissions);
|
pub const bridge = js.Bridge(Permissions);
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,51 @@
|
|||||||
// src/browser/webapi/StorageManager.zig
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
// Minimal stub for navigator.storage
|
//
|
||||||
// https://storage.spec.whatwg.org/#storagemanager
|
// 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 js = @import("../js/js.zig");
|
||||||
const Page = @import("../Page.zig");
|
const Page = @import("../Page.zig");
|
||||||
|
|
||||||
|
pub fn registerTypes() []const type {
|
||||||
|
return &.{ StorageManager, StorageEstimate };
|
||||||
|
}
|
||||||
|
|
||||||
const StorageManager = @This();
|
const StorageManager = @This();
|
||||||
|
|
||||||
_pad: bool = false,
|
_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 {
|
const StorageEstimate = struct {
|
||||||
quota: u64,
|
_quota: u64,
|
||||||
usage: 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 JsApi = struct {
|
||||||
pub const bridge = js.Bridge(StorageEstimate);
|
pub const bridge = js.Bridge(StorageEstimate);
|
||||||
@@ -20,30 +53,12 @@ const StorageEstimate = struct {
|
|||||||
pub const name = "StorageEstimate";
|
pub const name = "StorageEstimate";
|
||||||
pub const prototype_chain = bridge.prototypeChain();
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
pub var class_id: bridge.ClassId = undefined;
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
pub const empty_with_no_proto = true;
|
|
||||||
};
|
};
|
||||||
pub const quota = bridge.accessor(getQuota, null, .{});
|
pub const quota = bridge.accessor(getQuota, null, .{});
|
||||||
pub const usage = bridge.accessor(getUsage, 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<StorageEstimate> 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 JsApi = struct {
|
||||||
pub const bridge = js.Bridge(StorageManager);
|
pub const bridge = js.Bridge(StorageManager);
|
||||||
pub const Meta = struct {
|
pub const Meta = struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user