mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 01:25:53 -04:00
Merge branch 'main' into agent
This commit is contained in:
31
build.zig
31
build.zig
@@ -286,23 +286,42 @@ fn linkSqlite(b: *Build, mod: *Build.Module, enable_csan: ?std.zig.SanitizeC, is
|
||||
lib.root_module.sanitize_thread = is_tsan;
|
||||
|
||||
const macros = [_]struct { []const u8, []const u8 }{
|
||||
.{ "SQLITE_DQS", "0" },
|
||||
.{ "SQLITE_DEFAULT_FILE_PERMISSIONS", "0600" },
|
||||
.{ "SQLITE_DEFAULT_MEMSTATUS", "0" },
|
||||
.{ "SQLITE_DEFAULT_WAL_SYNCHRONOUS", "1" },
|
||||
.{ "SQLITE_USE_ALLOCA", "1" },
|
||||
.{ "SQLITE_THREADSAFE", "1" },
|
||||
.{ "SQLITE_TEMP_STORE", "3" },
|
||||
.{ "SQLITE_DQS", "0" },
|
||||
.{ "SQLITE_ENABLE_API_ARMOR", "1" },
|
||||
.{ "SQLITE_ENABLE_UNLOCK_NOTIFY", "1" },
|
||||
.{ "SQLITE_DEFAULT_FILE_PERMISSIONS", "0600" },
|
||||
.{ "SQLITE_TEMP_STORE", "3" },
|
||||
.{ "SQLITE_THREADSAFE", "1" },
|
||||
.{ "SQLITE_UNTESTABLE", "1" },
|
||||
.{ "SQLITE_USE_ALLOCA", "1" },
|
||||
.{ "SQLITE_OMIT_AUTHORIZATION", "1" },
|
||||
.{ "SQLITE_OMIT_AUTOMATIC_INDEX", "1" },
|
||||
.{ "SQLITE_OMIT_AUTORESET", "1" },
|
||||
.{ "SQLITE_OMIT_AUTOVACUUM", "1" },
|
||||
.{ "SQLITE_OMIT_BETWEEN_OPTIMIZATION", "1" },
|
||||
.{ "SQLITE_OMIT_CASE_SENSITIVE_LIKE_PRAGMA", "1" },
|
||||
.{ "SQLITE_OMIT_COMPLETE", "1" },
|
||||
.{ "SQLITE_OMIT_DECLTYPE", "1" },
|
||||
.{ "SQLITE_OMIT_DEPRECATED", "1" },
|
||||
.{ "SQLITE_OMIT_DESERIALIZE", "1" },
|
||||
.{ "SQLITE_OMIT_GET_TABLE", "1" },
|
||||
.{ "SQLITE_OMIT_INCRBLOB", "1" },
|
||||
.{ "SQLITE_OMIT_JSON", "1" },
|
||||
.{ "SQLITE_OMIT_LIKE_OPTIMIZATION", "1" },
|
||||
.{ "SQLITE_OMIT_LOAD_EXTENSION", "1" },
|
||||
.{ "SQLITE_OMIT_PROGRESS_CALLBACK", "1" },
|
||||
.{ "SQLITE_OMIT_SHARED_CACHE", "1" },
|
||||
.{ "SQLITE_OMIT_TCL_VARIABLE", "1" },
|
||||
.{ "SQLITE_OMIT_TEMPDB", "1" },
|
||||
.{ "SQLITE_OMIT_TRACE", "1" },
|
||||
.{ "SQLITE_OMIT_UTF16", "1" },
|
||||
.{ "SQLITE_OMIT_XFER_OPT", "1" },
|
||||
};
|
||||
for (macros) |m| lib.root_module.addCMacro(m[0], m[1]);
|
||||
for (macros) |m| {
|
||||
lib.root_module.addCMacro(m[0], m[1]);
|
||||
}
|
||||
|
||||
mod.linkLibrary(lib);
|
||||
}
|
||||
|
||||
@@ -582,8 +582,8 @@ pub fn printUsageAndExit(self: *const Config, success: bool) void {
|
||||
\\ Defaults to no caching.
|
||||
\\
|
||||
\\--storage-engine
|
||||
\\ The storage engine to use. Choices are: sqlite.
|
||||
\\ Default to sqlite.
|
||||
\\ The storage engine to use. Choices are: none, sqlite.
|
||||
\\ Default to none.
|
||||
\\
|
||||
\\--storage-sqlite-path
|
||||
\\ Path to SQLite database file for persistent storage.
|
||||
|
||||
@@ -721,7 +721,10 @@ fn scheduleNavigationWithArena(originator: *Frame, arena: Allocator, request_url
|
||||
};
|
||||
|
||||
const session = target._session;
|
||||
if (!opts.force and URL.eqlDocument(target.url, resolved_url)) {
|
||||
// Short-circuit only true fragment-only navigations (same path/query, different
|
||||
// fragment). Identical URLs fall through and trigger a real reload.
|
||||
const is_fragment_navigation = !std.mem.eql(u8, target.url, resolved_url) and URL.eqlDocument(target.url, resolved_url);
|
||||
if (!opts.force and is_fragment_navigation) {
|
||||
target.url = try target.arena.dupeZ(u8, resolved_url);
|
||||
target.window._location = try Location.init(target.url, target);
|
||||
if (target.parent == null) {
|
||||
|
||||
@@ -1572,7 +1572,19 @@ pub const Transfer = struct {
|
||||
}
|
||||
|
||||
const base_url = try conn.getEffectiveUrl();
|
||||
break :blk try URL.resolve(arena, std.mem.span(base_url), location.value, .{});
|
||||
const resolved = try URL.resolve(arena, std.mem.span(base_url), location.value, .{});
|
||||
|
||||
// RFC 7231 §7.1.2: if the Location value has no fragment, the redirect
|
||||
// inherits the fragment from the URI used to generate the request.
|
||||
// URL.resolve follows RFC 3986 §5.3, which drops the base fragment when
|
||||
// the relative ref has none, so we re-attach it here.
|
||||
if (URL.getHash(resolved).len == 0) {
|
||||
const original_hash = URL.getHash(transfer.url);
|
||||
if (original_hash.len != 0) {
|
||||
break :blk try std.mem.joinZ(arena, "", &.{ resolved, original_hash });
|
||||
}
|
||||
}
|
||||
break :blk resolved;
|
||||
};
|
||||
|
||||
try transfer.updateURL(url);
|
||||
|
||||
@@ -91,6 +91,11 @@ private_symbols: PrivateSymbols,
|
||||
|
||||
microtask_queues_are_running: bool,
|
||||
|
||||
// Serializes V8 calls that race with TerminateExecution (which can fire from
|
||||
// the sighandler thread). Without this, a terminate landing between the
|
||||
// IsExecutionTerminating check and PerformCheckpoint trips a V8 debug assert.
|
||||
terminate_mutex: std.Thread.Mutex = .{},
|
||||
|
||||
pub const InitOpts = struct {
|
||||
with_inspector: bool = false,
|
||||
};
|
||||
@@ -360,6 +365,9 @@ pub fn destroyContext(self: *Env, context: *Context) void {
|
||||
|
||||
pub fn runMicrotasks(self: *Env) void {
|
||||
if (self.microtask_queues_are_running == false) {
|
||||
self.terminate_mutex.lock();
|
||||
defer self.terminate_mutex.unlock();
|
||||
|
||||
const v8_isolate = self.isolate.handle;
|
||||
|
||||
if (v8.v8__Isolate__IsExecutionTerminating(v8_isolate)) {
|
||||
@@ -489,14 +497,18 @@ pub fn dumpMemoryStats(self: *Env) void {
|
||||
, .{ stats.total_heap_size, stats.total_heap_size_executable, stats.total_physical_size, stats.total_available_size, stats.used_heap_size, stats.heap_size_limit, stats.malloced_memory, stats.external_memory, stats.peak_malloced_memory, stats.number_of_native_contexts, stats.number_of_detached_contexts, stats.total_global_handles_size, stats.used_global_handles_size, stats.does_zap_garbage });
|
||||
}
|
||||
|
||||
pub fn terminate(self: *const Env) void {
|
||||
pub fn terminate(self: *Env) void {
|
||||
self.terminate_mutex.lock();
|
||||
defer self.terminate_mutex.unlock();
|
||||
v8.v8__Isolate__TerminateExecution(self.isolate.handle);
|
||||
}
|
||||
|
||||
/// Clears a pending termination so V8 calls (e.g. those made during cleanup)
|
||||
/// don't keep tripping over the terminating-state asserts. Safe to call
|
||||
/// unconditionally; a no-op if termination wasn't pending.
|
||||
pub fn cancelTerminate(self: *const Env) void {
|
||||
pub fn cancelTerminate(self: *Env) void {
|
||||
self.terminate_mutex.lock();
|
||||
defer self.terminate_mutex.unlock();
|
||||
v8.v8__Isolate__CancelTerminateExecution(self.isolate.handle);
|
||||
}
|
||||
|
||||
|
||||
@@ -276,9 +276,11 @@ fn createSnapshotContext(
|
||||
const name = JsApi.Meta.name;
|
||||
const v8_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
||||
var maybe_result: v8.MaybeBool = undefined;
|
||||
var properties: v8.PropertyAttribute = v8.None;
|
||||
if (@hasDecl(JsApi.Meta, "enumerable") and JsApi.Meta.enumerable == false) {
|
||||
properties |= v8.DontEnum;
|
||||
// Web IDL: interface objects on the global are non-enumerable
|
||||
// by default. Opt back in via JsApi.Meta.enumerable = true.
|
||||
var properties: v8.PropertyAttribute = v8.DontEnum;
|
||||
if (@hasDecl(JsApi.Meta, "enumerable") and JsApi.Meta.enumerable == true) {
|
||||
properties = v8.None;
|
||||
}
|
||||
v8.v8__Object__DefineOwnProperty(global_obj, context, v8_class_name, func, properties, &maybe_result);
|
||||
}
|
||||
@@ -578,6 +580,8 @@ pub fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *const v8
|
||||
const name_str = if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi);
|
||||
const class_name = v8.v8__String__NewFromUtf8(isolate, name_str.ptr, v8.kNormal, @intCast(name_str.len));
|
||||
v8.v8__FunctionTemplate__SetClassName(template, class_name);
|
||||
// Web IDL: interface object's `prototype` property is non-writable/non-configurable.
|
||||
v8.v8__FunctionTemplate__ReadOnlyPrototype(template);
|
||||
return template;
|
||||
}
|
||||
|
||||
@@ -615,13 +619,21 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.F
|
||||
.callback = value.getter,
|
||||
.signature = getter_signature,
|
||||
}).?;
|
||||
const setter_callback = if (value.setter) |setter|
|
||||
v8.v8__FunctionTemplate__New__Config(isolate, &.{
|
||||
// WebIDL: getter function's .name should be "get X"
|
||||
const getter_name_str = "get " ++ name;
|
||||
const getter_name_v8 = v8.v8__String__NewFromUtf8(isolate, getter_name_str.ptr, v8.kNormal, @intCast(getter_name_str.len));
|
||||
v8.v8__FunctionTemplate__SetClassName(getter_callback, getter_name_v8);
|
||||
|
||||
const setter_callback = if (value.setter) |setter| blk: {
|
||||
const cb = v8.v8__FunctionTemplate__New__Config(isolate, &.{
|
||||
.callback = setter,
|
||||
.signature = getter_signature,
|
||||
}).?
|
||||
else
|
||||
null;
|
||||
}).?;
|
||||
const setter_name_str = "set " ++ name;
|
||||
const setter_name_v8 = v8.v8__String__NewFromUtf8(isolate, setter_name_str.ptr, v8.kNormal, @intCast(setter_name_str.len));
|
||||
v8.v8__FunctionTemplate__SetClassName(cb, setter_name_v8);
|
||||
break :blk cb;
|
||||
} else null;
|
||||
|
||||
var attribute: v8.PropertyAttribute = 0;
|
||||
if (value.setter == null) {
|
||||
@@ -655,6 +667,7 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.F
|
||||
.signature = func_signature,
|
||||
}).?;
|
||||
const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
||||
v8.v8__FunctionTemplate__SetClassName(function_template, js_name);
|
||||
if (value.static) {
|
||||
v8.v8__Template__Set(@ptrCast(template), js_name, @ptrCast(function_template), v8.None);
|
||||
} else {
|
||||
|
||||
@@ -25,5 +25,15 @@
|
||||
testing.expectEqual(true, dom_load);
|
||||
testing.expectEqual(true, attribute_load);
|
||||
});
|
||||
|
||||
// Web IDL requires attribute getters/setters to be named "get X" / "set X"
|
||||
// and methods to use their identifier. Sentinel for the binding-level naming
|
||||
// fix in Snapshot.zig — Script.src exercises both accessor paths.
|
||||
const desc = Object.getOwnPropertyDescriptor(HTMLScriptElement.prototype, 'src');
|
||||
testing.expectEqual('get src', desc.get.name);
|
||||
testing.expectEqual('set src', desc.set.name);
|
||||
|
||||
const append = Object.getOwnPropertyDescriptor(Element.prototype, 'append');
|
||||
testing.expectEqual('append', append.value.name);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -5,10 +5,13 @@
|
||||
{
|
||||
let event = new CompositionEvent("test", {});
|
||||
testing.expectEqual(true, event instanceof CompositionEvent);
|
||||
testing.expectEqual(true, event instanceof UIEvent);
|
||||
testing.expectEqual(true, event instanceof Event);
|
||||
|
||||
testing.expectEqual("test", event.type);
|
||||
testing.expectEqual("", event.data);
|
||||
testing.expectEqual(0, event.detail);
|
||||
testing.expectEqual(window, event.view);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -34,3 +37,36 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=createEvent>
|
||||
{
|
||||
let evt = document.createEvent('CompositionEvent');
|
||||
testing.expectEqual(true, evt instanceof CompositionEvent);
|
||||
testing.expectEqual(true, evt instanceof UIEvent);
|
||||
testing.expectEqual(true, evt instanceof Event);
|
||||
testing.expectEqual('', evt.data);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=initCompositionEvent>
|
||||
{
|
||||
let evt = document.createEvent('CompositionEvent');
|
||||
evt.initCompositionEvent('compositionstart', true, false, window, 'hello');
|
||||
testing.expectEqual('compositionstart', evt.type);
|
||||
testing.expectEqual(true, evt.bubbles);
|
||||
testing.expectEqual(false, evt.cancelable);
|
||||
testing.expectEqual(window, evt.view);
|
||||
testing.expectEqual('hello', evt.data);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=initCompositionEventOptional>
|
||||
{
|
||||
let evt = document.createEvent('CompositionEvent');
|
||||
evt.initCompositionEvent('compositionend');
|
||||
testing.expectEqual('compositionend', evt.type);
|
||||
testing.expectEqual(false, evt.bubbles);
|
||||
testing.expectEqual(false, evt.cancelable);
|
||||
testing.expectEqual('', evt.data);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -116,3 +116,54 @@
|
||||
testing.expectEqual(true, called);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=domKeyLocationConstants>
|
||||
{
|
||||
testing.expectEqual(0, KeyboardEvent.DOM_KEY_LOCATION_STANDARD);
|
||||
testing.expectEqual(1, KeyboardEvent.DOM_KEY_LOCATION_LEFT);
|
||||
testing.expectEqual(2, KeyboardEvent.DOM_KEY_LOCATION_RIGHT);
|
||||
testing.expectEqual(3, KeyboardEvent.DOM_KEY_LOCATION_NUMPAD);
|
||||
|
||||
let evt = new KeyboardEvent('keydown', {key: 'a'});
|
||||
testing.expectEqual(0, evt.DOM_KEY_LOCATION_STANDARD);
|
||||
testing.expectEqual(1, evt.DOM_KEY_LOCATION_LEFT);
|
||||
testing.expectEqual(2, evt.DOM_KEY_LOCATION_RIGHT);
|
||||
testing.expectEqual(3, evt.DOM_KEY_LOCATION_NUMPAD);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=charCodeKeyCode>
|
||||
{
|
||||
let evt = new KeyboardEvent('keypress', {key: 'a'});
|
||||
testing.expectEqual(0, evt.charCode);
|
||||
testing.expectEqual(0, evt.keyCode);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=initKeyboardEvent>
|
||||
{
|
||||
let evt = new KeyboardEvent('keydown', {key: 'a'});
|
||||
evt.initKeyboardEvent('keyup', true, true, window, 'q', 1, true, false, true, false);
|
||||
testing.expectEqual('keyup', evt.type);
|
||||
testing.expectEqual(true, evt.bubbles);
|
||||
testing.expectEqual(true, evt.cancelable);
|
||||
testing.expectEqual('q', evt.key);
|
||||
testing.expectEqual(1, evt.location);
|
||||
testing.expectEqual(true, evt.ctrlKey);
|
||||
testing.expectEqual(false, evt.altKey);
|
||||
testing.expectEqual(true, evt.shiftKey);
|
||||
testing.expectEqual(false, evt.metaKey);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=initKeyboardEventOptional>
|
||||
{
|
||||
let evt = new KeyboardEvent('keydown', {key: 'x'});
|
||||
evt.initKeyboardEvent('foo');
|
||||
testing.expectEqual('foo', evt.type);
|
||||
testing.expectEqual(false, evt.bubbles);
|
||||
testing.expectEqual(false, evt.cancelable);
|
||||
testing.expectEqual('', evt.key);
|
||||
testing.expectEqual(0, evt.location);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -52,3 +52,67 @@
|
||||
document.dispatchEvent(new MouseEvent('mousetest'));
|
||||
testing.expectEqual(false, mouseIsTrusted);
|
||||
</script>
|
||||
|
||||
<script id=which>
|
||||
{
|
||||
testing.expectEqual(1, new MouseEvent('click').which);
|
||||
testing.expectEqual(2, new MouseEvent('click', {button: 1}).which);
|
||||
testing.expectEqual(3, new MouseEvent('click', {button: 2}).which);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=layerXY>
|
||||
{
|
||||
let evt = new MouseEvent('click', {clientX: 11, clientY: 22});
|
||||
testing.expectEqual(11, evt.layerX);
|
||||
testing.expectEqual(22, evt.layerY);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=getModifierState>
|
||||
{
|
||||
let evt = new MouseEvent('click', {altKey: true, ctrlKey: true, shiftKey: true, metaKey: true});
|
||||
testing.expectEqual(true, evt.getModifierState('Alt'));
|
||||
testing.expectEqual(true, evt.getModifierState('AltGraph'));
|
||||
testing.expectEqual(true, evt.getModifierState('Control'));
|
||||
testing.expectEqual(true, evt.getModifierState('Shift'));
|
||||
testing.expectEqual(true, evt.getModifierState('Meta'));
|
||||
testing.expectEqual(true, evt.getModifierState('Accel'));
|
||||
testing.expectEqual(false, evt.getModifierState('NumLock'));
|
||||
|
||||
let none = new MouseEvent('click');
|
||||
testing.expectEqual(false, none.getModifierState('Shift'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=initMouseEvent>
|
||||
{
|
||||
let evt = document.createEvent('MouseEvent');
|
||||
evt.initMouseEvent('click', true, true, window, 1, 10, 20, 30, 40, true, false, true, false, 2, null);
|
||||
testing.expectEqual('click', evt.type);
|
||||
testing.expectEqual(true, evt.bubbles);
|
||||
testing.expectEqual(true, evt.cancelable);
|
||||
testing.expectEqual(1, evt.detail);
|
||||
testing.expectEqual(10, evt.screenX);
|
||||
testing.expectEqual(20, evt.screenY);
|
||||
testing.expectEqual(30, evt.clientX);
|
||||
testing.expectEqual(40, evt.clientY);
|
||||
testing.expectEqual(true, evt.ctrlKey);
|
||||
testing.expectEqual(false, evt.altKey);
|
||||
testing.expectEqual(true, evt.shiftKey);
|
||||
testing.expectEqual(false, evt.metaKey);
|
||||
testing.expectEqual(2, evt.button);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=initMouseEventOptional>
|
||||
{
|
||||
let evt = document.createEvent('MouseEvent');
|
||||
evt.initMouseEvent('mousedown');
|
||||
testing.expectEqual('mousedown', evt.type);
|
||||
testing.expectEqual(false, evt.bubbles);
|
||||
testing.expectEqual(false, evt.cancelable);
|
||||
testing.expectEqual(0, evt.clientX);
|
||||
testing.expectEqual(0, evt.button);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,17 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
<script id=createEvent>
|
||||
let evt = document.createEvent('TextEvent');
|
||||
testing.expectEqual(true, evt instanceof TextEvent);
|
||||
testing.expectEqual(true, evt instanceof UIEvent);
|
||||
testing.expectEqual('', evt.data);
|
||||
{
|
||||
let evt = document.createEvent('TextEvent');
|
||||
testing.expectEqual(true, evt instanceof TextEvent);
|
||||
testing.expectEqual(true, evt instanceof UIEvent);
|
||||
testing.expectEqual('', evt.data);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=initTextEvent>
|
||||
let textEvent = document.createEvent('TextEvent');
|
||||
textEvent.initTextEvent('textInput', true, false, window, 'test data');
|
||||
testing.expectEqual('textInput', textEvent.type);
|
||||
testing.expectEqual('test data', textEvent.data);
|
||||
testing.expectEqual(true, textEvent.bubbles);
|
||||
testing.expectEqual(false, textEvent.cancelable);
|
||||
{
|
||||
let textEvent = document.createEvent('TextEvent');
|
||||
textEvent.initTextEvent('textInput', true, false, window, 'test data');
|
||||
testing.expectEqual('textInput', textEvent.type);
|
||||
testing.expectEqual('test data', textEvent.data);
|
||||
testing.expectEqual(true, textEvent.bubbles);
|
||||
testing.expectEqual(false, textEvent.cancelable);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=initTextEventOptional>
|
||||
{
|
||||
let evt = document.createEvent('TextEvent');
|
||||
evt.initTextEvent('foo');
|
||||
testing.expectEqual('foo', evt.type);
|
||||
testing.expectEqual('', evt.data);
|
||||
testing.expectEqual(false, evt.bubbles);
|
||||
testing.expectEqual(false, evt.cancelable);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -52,3 +52,33 @@
|
||||
const evt7 = new UIEvent('custom', { bubbles: true });
|
||||
testing.expectEqual(0, evt7.detail);
|
||||
</script>
|
||||
|
||||
<script id=which>
|
||||
{
|
||||
const evt = new UIEvent('focus');
|
||||
testing.expectEqual(0, evt.which);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=initUIEvent>
|
||||
{
|
||||
const evt = document.createEvent('UIEvent');
|
||||
evt.initUIEvent('foo', true, true, window, 7);
|
||||
testing.expectEqual('foo', evt.type);
|
||||
testing.expectEqual(true, evt.bubbles);
|
||||
testing.expectEqual(true, evt.cancelable);
|
||||
testing.expectEqual(window, evt.view);
|
||||
testing.expectEqual(7, evt.detail);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=initUIEventOptional>
|
||||
{
|
||||
const evt = document.createEvent('UIEvent');
|
||||
evt.initUIEvent('bar');
|
||||
testing.expectEqual('bar', evt.type);
|
||||
testing.expectEqual(false, evt.bubbles);
|
||||
testing.expectEqual(false, evt.cancelable);
|
||||
testing.expectEqual(0, evt.detail);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -151,8 +151,20 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<iframe id=f7></iframe>
|
||||
<script id=onloadorder type=module>
|
||||
{
|
||||
const state = await testing.async();
|
||||
$('#f7').onload = state.resolve;
|
||||
$('#f7').src = "about:blank";
|
||||
await state.done(() => {
|
||||
testing.expectEqual(true, true);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=count>
|
||||
testing.onload(() => {
|
||||
testing.expectEqual(9, window.length);
|
||||
testing.expectEqual(10, window.length);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
</script>
|
||||
|
||||
<script id=location_hash>
|
||||
location.hash = "";
|
||||
testing.expectEqual("", location.hash);
|
||||
testing.expectEqual(testing.BASE_URL + 'window/location.html', location.href);
|
||||
|
||||
location.hash = "#abcdef";
|
||||
testing.expectEqual("#abcdef", location.hash);
|
||||
testing.expectEqual(testing.BASE_URL + 'window/location.html#abcdef', location.href);
|
||||
|
||||
location.hash = "";
|
||||
testing.expectEqual("", location.hash);
|
||||
testing.expectEqual(testing.BASE_URL + 'window/location.html', location.href);
|
||||
|
||||
location.hash = "xyzxyz";
|
||||
testing.expectEqual("#xyzxyz", location.hash);
|
||||
testing.expectEqual(testing.BASE_URL + 'window/location.html#xyzxyz', location.href);
|
||||
@@ -23,3 +23,11 @@
|
||||
testing.expectEqual("", location.hash);
|
||||
testing.expectEqual(testing.BASE_URL + 'window/location.html', location.href);
|
||||
</script>
|
||||
|
||||
<script id=location_pathname_setter>
|
||||
testing.expectEqual('function', typeof Object.getOwnPropertyDescriptor(Location.prototype, 'pathname').set);
|
||||
</script>
|
||||
|
||||
<script id=location_search_setter>
|
||||
testing.expectEqual('function', typeof Object.getOwnPropertyDescriptor(Location.prototype, 'search').set);
|
||||
</script>
|
||||
|
||||
@@ -396,6 +396,11 @@ pub fn createEvent(_: *const Document, event_type: []const u8, frame: *Frame) !*
|
||||
return (try TextEvent.init("", null, frame)).asEvent();
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, normalized, "compositionevent")) {
|
||||
const CompositionEvent = @import("event/CompositionEvent.zig");
|
||||
return (try CompositionEvent.init("", null, frame)).asEvent();
|
||||
}
|
||||
|
||||
return error.NotSupported;
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,6 @@ pub const Type = union(enum) {
|
||||
custom_event: *@import("event/CustomEvent.zig"),
|
||||
message_event: *@import("event/MessageEvent.zig"),
|
||||
progress_event: *@import("event/ProgressEvent.zig"),
|
||||
composition_event: *@import("event/CompositionEvent.zig"),
|
||||
navigation_current_entry_change_event: *@import("event/NavigationCurrentEntryChangeEvent.zig"),
|
||||
page_transition_event: *@import("event/PageTransitionEvent.zig"),
|
||||
pop_state_event: *@import("event/PopStateEvent.zig"),
|
||||
@@ -163,7 +162,6 @@ pub fn is(self: *Event, comptime T: type) ?*T {
|
||||
.custom_event => |e| return if (T == @import("event/CustomEvent.zig")) e else null,
|
||||
.message_event => |e| return if (T == @import("event/MessageEvent.zig")) e else null,
|
||||
.progress_event => |e| return if (T == @import("event/ProgressEvent.zig")) e else null,
|
||||
.composition_event => |e| return if (T == @import("event/CompositionEvent.zig")) e else null,
|
||||
.navigation_current_entry_change_event => |e| return if (T == @import("event/NavigationCurrentEntryChangeEvent.zig")) e else null,
|
||||
.page_transition_event => |e| return if (T == @import("event/PageTransitionEvent.zig")) e else null,
|
||||
.pop_state_event => |e| return if (T == @import("event/PopStateEvent.zig")) e else null,
|
||||
|
||||
@@ -20,6 +20,7 @@ const std = @import("std");
|
||||
const js = @import("../js/js.zig");
|
||||
|
||||
const URL = @import("URL.zig");
|
||||
const U = @import("../URL.zig");
|
||||
const Frame = @import("../Frame.zig");
|
||||
|
||||
const Location = @This();
|
||||
@@ -65,6 +66,22 @@ pub fn getHash(self: *const Location) []const u8 {
|
||||
return self._url.getHash();
|
||||
}
|
||||
|
||||
pub fn setPathname(_: *const Location, pathname: []const u8, frame: *Frame) !void {
|
||||
const new_url = try U.setPathname(frame.url, pathname, frame.call_arena);
|
||||
return frame.scheduleNavigation(new_url, .{
|
||||
.reason = .script,
|
||||
.kind = .{ .push = null },
|
||||
}, .{ .script = frame });
|
||||
}
|
||||
|
||||
pub fn setSearch(_: *const Location, search: []const u8, frame: *Frame) !void {
|
||||
const new_url = try U.setSearch(frame.url, search, frame.call_arena);
|
||||
return frame.scheduleNavigation(new_url, .{
|
||||
.reason = .script,
|
||||
.kind = .{ .push = null },
|
||||
}, .{ .script = frame });
|
||||
}
|
||||
|
||||
pub fn setHash(_: *const Location, hash: []const u8, frame: *Frame) !void {
|
||||
const normalized_hash = blk: {
|
||||
if (hash.len == 0) {
|
||||
@@ -117,9 +134,9 @@ pub const JsApi = struct {
|
||||
return self.assign(url, frame);
|
||||
}
|
||||
|
||||
pub const search = bridge.accessor(Location.getSearch, null, .{});
|
||||
pub const search = bridge.accessor(Location.getSearch, Location.setSearch, .{});
|
||||
pub const hash = bridge.accessor(Location.getHash, Location.setHash, .{});
|
||||
pub const pathname = bridge.accessor(Location.getPathname, null, .{});
|
||||
pub const pathname = bridge.accessor(Location.getPathname, Location.setPathname, .{});
|
||||
pub const hostname = bridge.accessor(Location.getHostname, null, .{});
|
||||
pub const host = bridge.accessor(Location.getHost, null, .{});
|
||||
pub const port = bridge.accessor(Location.getPort, null, .{});
|
||||
|
||||
@@ -204,7 +204,8 @@ pub const JsApi = struct {
|
||||
pub const getSupportedExtensions = bridge.function(WebGLRenderingContext.getSupportedExtensions, .{});
|
||||
};
|
||||
|
||||
const testing = @import("../../../testing.zig");
|
||||
test "WebApi: WebGLRenderingContext" {
|
||||
try testing.htmlRunner("canvas/webgl_rendering_context.html", .{});
|
||||
}
|
||||
// getContext('web-gl') currently returns null, so this cannot be tested
|
||||
// const testing = @import("../../../testing.zig");
|
||||
// test "WebApi: WebGLRenderingContext" {
|
||||
// try testing.htmlRunner("canvas/webgl_rendering_context.html", .{});
|
||||
// }
|
||||
|
||||
@@ -85,9 +85,17 @@ pub fn getContext(self: *Canvas, context_type: []const u8, frame: *Frame) !?Draw
|
||||
break :blk .{ .@"2d" = ctx };
|
||||
}
|
||||
|
||||
// We only stub a tiny slice of the WebGL API (getParameter,
|
||||
// getExtension, getSupportedExtensions). Real WebGL consumers like
|
||||
// Three.js immediately call createTexture/createBuffer/etc. and
|
||||
// throw `TypeError: e.createTexture is not a function`. Pretending
|
||||
// WebGL works until the first non-stubbed call is the worst of both
|
||||
// worlds: pages that have an error boundary above the WebGL widget
|
||||
// catch the throw, reset, re-render, and loop forever.
|
||||
// Spec-correct signal for "no WebGL" is null, so apps that check
|
||||
// (Three.js does) can degrade gracefully.
|
||||
if (std.mem.eql(u8, context_type, "webgl") or std.mem.eql(u8, context_type, "experimental-webgl")) {
|
||||
const ctx = try frame._factory.create(WebGLRenderingContext{});
|
||||
break :blk .{ .webgl = ctx };
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -22,12 +22,14 @@ const js = @import("../../js/js.zig");
|
||||
const Frame = @import("../../Frame.zig");
|
||||
|
||||
const Event = @import("../Event.zig");
|
||||
const UIEvent = @import("UIEvent.zig");
|
||||
const Window = @import("../Window.zig");
|
||||
|
||||
const String = lp.String;
|
||||
|
||||
const CompositionEvent = @This();
|
||||
|
||||
_proto: *Event,
|
||||
_proto: *UIEvent,
|
||||
_data: []const u8 = "",
|
||||
|
||||
const CompositionEventOptions = struct {
|
||||
@@ -42,7 +44,7 @@ pub fn init(typ: []const u8, opts_: ?Options, frame: *Frame) !*CompositionEvent
|
||||
const type_string = try String.init(arena, typ, .{});
|
||||
|
||||
const opts = opts_ orelse Options{};
|
||||
const event = try frame._factory.event(
|
||||
const event = try frame._factory.uiEvent(
|
||||
arena,
|
||||
type_string,
|
||||
CompositionEvent{
|
||||
@@ -56,13 +58,35 @@ pub fn init(typ: []const u8, opts_: ?Options, frame: *Frame) !*CompositionEvent
|
||||
}
|
||||
|
||||
pub fn asEvent(self: *CompositionEvent) *Event {
|
||||
return self._proto;
|
||||
return self._proto.asEvent();
|
||||
}
|
||||
|
||||
pub fn getData(self: *const CompositionEvent) []const u8 {
|
||||
return self._data;
|
||||
}
|
||||
|
||||
pub fn initCompositionEvent(
|
||||
self: *CompositionEvent,
|
||||
typ: []const u8,
|
||||
bubbles: ?bool,
|
||||
cancelable: ?bool,
|
||||
view: ?*Window,
|
||||
data: ?[]const u8,
|
||||
) !void {
|
||||
const ui = self._proto;
|
||||
const event = ui._proto;
|
||||
if (event._event_phase != .none) {
|
||||
return;
|
||||
}
|
||||
|
||||
const arena = event._arena;
|
||||
event._type_string = try String.init(arena, typ, .{});
|
||||
event._bubbles = bubbles orelse false;
|
||||
event._cancelable = cancelable orelse false;
|
||||
ui._view = view;
|
||||
self._data = if (data) |d| try arena.dupe(u8, d) else "";
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(CompositionEvent);
|
||||
|
||||
@@ -74,6 +98,7 @@ pub const JsApi = struct {
|
||||
|
||||
pub const constructor = bridge.constructor(CompositionEvent.init, .{});
|
||||
pub const data = bridge.accessor(CompositionEvent.getData, null, .{});
|
||||
pub const initCompositionEvent = bridge.function(CompositionEvent.initCompositionEvent, .{});
|
||||
};
|
||||
|
||||
const testing = @import("../../../testing.zig");
|
||||
|
||||
@@ -270,6 +270,49 @@ pub fn getShiftKey(self: *const KeyboardEvent) bool {
|
||||
return self._shift_key;
|
||||
}
|
||||
|
||||
// Deprecated: tracked as 0 since we don't synthesise legacy character codes.
|
||||
pub fn getCharCode(self: *const KeyboardEvent) u32 {
|
||||
_ = self;
|
||||
return 0;
|
||||
}
|
||||
|
||||
pub fn getKeyCode(self: *const KeyboardEvent) u32 {
|
||||
_ = self;
|
||||
return 0;
|
||||
}
|
||||
|
||||
pub fn initKeyboardEvent(
|
||||
self: *KeyboardEvent,
|
||||
typ: []const u8,
|
||||
bubbles: ?bool,
|
||||
cancelable: ?bool,
|
||||
view: ?*@import("../Window.zig"),
|
||||
key: ?[]const u8,
|
||||
location: ?u32,
|
||||
ctrl_key: ?bool,
|
||||
alt_key: ?bool,
|
||||
shift_key: ?bool,
|
||||
meta_key: ?bool,
|
||||
) !void {
|
||||
const ui = self._proto;
|
||||
const event = ui._proto;
|
||||
if (event._event_phase != .none) {
|
||||
return;
|
||||
}
|
||||
|
||||
const arena = event._arena;
|
||||
event._type_string = try String.init(arena, typ, .{});
|
||||
event._bubbles = bubbles orelse false;
|
||||
event._cancelable = cancelable orelse false;
|
||||
ui._view = view;
|
||||
self._key = try Key.fromString(arena, key orelse "");
|
||||
self._location = std.meta.intToEnum(Location, location orelse 0) catch return error.TypeError;
|
||||
self._ctrl_key = ctrl_key orelse false;
|
||||
self._alt_key = alt_key orelse false;
|
||||
self._shift_key = shift_key orelse false;
|
||||
self._meta_key = meta_key orelse false;
|
||||
}
|
||||
|
||||
pub fn getModifierState(self: *const KeyboardEvent, str: []const u8) !bool {
|
||||
const key = try Key.fromString(self._proto._proto._arena, str);
|
||||
|
||||
@@ -309,7 +352,15 @@ pub const JsApi = struct {
|
||||
pub const metaKey = bridge.accessor(KeyboardEvent.getMetaKey, null, .{});
|
||||
pub const repeat = bridge.accessor(KeyboardEvent.getRepeat, null, .{});
|
||||
pub const shiftKey = bridge.accessor(KeyboardEvent.getShiftKey, null, .{});
|
||||
pub const charCode = bridge.accessor(KeyboardEvent.getCharCode, null, .{});
|
||||
pub const keyCode = bridge.accessor(KeyboardEvent.getKeyCode, null, .{});
|
||||
pub const getModifierState = bridge.function(KeyboardEvent.getModifierState, .{});
|
||||
pub const initKeyboardEvent = bridge.function(KeyboardEvent.initKeyboardEvent, .{});
|
||||
|
||||
pub const DOM_KEY_LOCATION_STANDARD = bridge.property(@intFromEnum(Location.DOM_KEY_LOCATION_STANDARD), .{ .template = true });
|
||||
pub const DOM_KEY_LOCATION_LEFT = bridge.property(@intFromEnum(Location.DOM_KEY_LOCATION_LEFT), .{ .template = true });
|
||||
pub const DOM_KEY_LOCATION_RIGHT = bridge.property(@intFromEnum(Location.DOM_KEY_LOCATION_RIGHT), .{ .template = true });
|
||||
pub const DOM_KEY_LOCATION_NUMPAD = bridge.property(@intFromEnum(Location.DOM_KEY_LOCATION_NUMPAD), .{ .template = true });
|
||||
};
|
||||
|
||||
const testing = @import("../../../testing.zig");
|
||||
|
||||
@@ -193,6 +193,65 @@ pub fn getShiftKey(self: *const MouseEvent) bool {
|
||||
return self._shift_key;
|
||||
}
|
||||
|
||||
// Deprecated: tracks the same value as offsetX/clientX in the absence of layout.
|
||||
pub fn getLayerX(self: *const MouseEvent) f64 {
|
||||
return self._client_x;
|
||||
}
|
||||
|
||||
pub fn getLayerY(self: *const MouseEvent) f64 {
|
||||
return self._client_y;
|
||||
}
|
||||
|
||||
pub fn getModifierState(self: *const MouseEvent, key: []const u8) bool {
|
||||
if (std.mem.eql(u8, key, "Alt") or std.mem.eql(u8, key, "AltGraph")) return self._alt_key;
|
||||
if (std.mem.eql(u8, key, "Control")) return self._ctrl_key;
|
||||
if (std.mem.eql(u8, key, "Shift")) return self._shift_key;
|
||||
if (std.mem.eql(u8, key, "Meta")) return self._meta_key;
|
||||
if (std.mem.eql(u8, key, "Accel")) return self._ctrl_key or self._meta_key;
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn initMouseEvent(
|
||||
self: *MouseEvent,
|
||||
typ: []const u8,
|
||||
bubbles: ?bool,
|
||||
cancelable: ?bool,
|
||||
view: ?*@import("../Window.zig"),
|
||||
detail: ?i32,
|
||||
screen_x: ?i32,
|
||||
screen_y: ?i32,
|
||||
client_x: ?i32,
|
||||
client_y: ?i32,
|
||||
ctrl_key: ?bool,
|
||||
alt_key: ?bool,
|
||||
shift_key: ?bool,
|
||||
meta_key: ?bool,
|
||||
button: ?i16,
|
||||
related_target: ?*EventTarget,
|
||||
) !void {
|
||||
const ui = self._proto;
|
||||
const event = ui._proto;
|
||||
if (event._event_phase != .none) {
|
||||
return;
|
||||
}
|
||||
|
||||
event._type_string = try String.init(event._arena, typ, .{});
|
||||
event._bubbles = bubbles orelse false;
|
||||
event._cancelable = cancelable orelse false;
|
||||
ui._view = view;
|
||||
ui._detail = if (detail) |d| @intCast(@max(d, 0)) else 0;
|
||||
self._screen_x = @floatFromInt(screen_x orelse 0);
|
||||
self._screen_y = @floatFromInt(screen_y orelse 0);
|
||||
self._client_x = @floatFromInt(client_x orelse 0);
|
||||
self._client_y = @floatFromInt(client_y orelse 0);
|
||||
self._ctrl_key = ctrl_key orelse false;
|
||||
self._alt_key = alt_key orelse false;
|
||||
self._shift_key = shift_key orelse false;
|
||||
self._meta_key = meta_key orelse false;
|
||||
self._button = std.meta.intToEnum(MouseButton, button orelse 0) catch return error.TypeError;
|
||||
self._related_target = related_target;
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(MouseEvent);
|
||||
|
||||
@@ -218,8 +277,12 @@ pub const JsApi = struct {
|
||||
pub const screenX = bridge.accessor(getScreenX, null, .{});
|
||||
pub const screenY = bridge.accessor(getScreenY, null, .{});
|
||||
pub const shiftKey = bridge.accessor(getShiftKey, null, .{});
|
||||
pub const layerX = bridge.accessor(getLayerX, null, .{});
|
||||
pub const layerY = bridge.accessor(getLayerY, null, .{});
|
||||
pub const x = bridge.accessor(getClientX, null, .{});
|
||||
pub const y = bridge.accessor(getClientY, null, .{});
|
||||
pub const getModifierState = bridge.function(MouseEvent.getModifierState, .{});
|
||||
pub const initMouseEvent = bridge.function(MouseEvent.initMouseEvent, .{});
|
||||
};
|
||||
|
||||
const testing = @import("../../../testing.zig");
|
||||
|
||||
@@ -72,24 +72,23 @@ pub fn getData(self: *const TextEvent) []const u8 {
|
||||
pub fn initTextEvent(
|
||||
self: *TextEvent,
|
||||
typ: []const u8,
|
||||
bubbles: bool,
|
||||
cancelable: bool,
|
||||
bubbles: ?bool,
|
||||
cancelable: ?bool,
|
||||
view: ?*@import("../Window.zig"),
|
||||
data: []const u8,
|
||||
data: ?[]const u8,
|
||||
) !void {
|
||||
_ = view; // view parameter is ignored in modern implementations
|
||||
|
||||
const event = self._proto._proto;
|
||||
const ui = self._proto;
|
||||
const event = ui._proto;
|
||||
if (event._event_phase != .none) {
|
||||
// Only allow initialization if event hasn't been dispatched
|
||||
return;
|
||||
}
|
||||
|
||||
const arena = event._arena;
|
||||
event._type_string = try String.init(arena, typ, .{});
|
||||
event._bubbles = bubbles;
|
||||
event._cancelable = cancelable;
|
||||
self._data = try arena.dupe(u8, data);
|
||||
event._bubbles = bubbles orelse false;
|
||||
event._cancelable = cancelable orelse false;
|
||||
ui._view = view;
|
||||
self._data = if (data) |d| try arena.dupe(u8, d) else "";
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
|
||||
@@ -40,6 +40,7 @@ pub const Type = union(enum) {
|
||||
focus_event: *@import("FocusEvent.zig"),
|
||||
text_event: *@import("TextEvent.zig"),
|
||||
input_event: *@import("InputEvent.zig"),
|
||||
composition_event: *@import("CompositionEvent.zig"),
|
||||
};
|
||||
|
||||
pub const UIEventOptions = struct {
|
||||
@@ -88,6 +89,7 @@ pub fn is(self: *UIEvent, comptime T: type) ?*T {
|
||||
.focus_event => |e| return if (T == @import("FocusEvent.zig")) e else null,
|
||||
.text_event => |e| return if (T == @import("TextEvent.zig")) e else null,
|
||||
.input_event => |e| return if (T == @import("InputEvent.zig")) e else null,
|
||||
.composition_event => |e| return if (T == @import("CompositionEvent.zig")) e else null,
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -111,7 +113,34 @@ pub fn getView(self: *UIEvent, frame: *Frame) *Window {
|
||||
return self._view orelse frame.window;
|
||||
}
|
||||
|
||||
// deprecated `initUIEvent()` not implemented
|
||||
// Legacy: see https://w3c.github.io/uievents/#dom-uievent-which
|
||||
pub fn getWhich(self: *const UIEvent) u32 {
|
||||
return switch (self._type) {
|
||||
.mouse_event => |me| @as(u32, @intCast(me.getButton())) + 1,
|
||||
.keyboard_event => 0,
|
||||
else => 0,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn initUIEvent(
|
||||
self: *UIEvent,
|
||||
typ: []const u8,
|
||||
bubbles: ?bool,
|
||||
cancelable: ?bool,
|
||||
view: ?*Window,
|
||||
detail: ?i32,
|
||||
) !void {
|
||||
const event = self._proto;
|
||||
if (event._event_phase != .none) {
|
||||
return;
|
||||
}
|
||||
|
||||
event._type_string = try String.init(event._arena, typ, .{});
|
||||
event._bubbles = bubbles orelse false;
|
||||
event._cancelable = cancelable orelse false;
|
||||
self._view = view;
|
||||
self._detail = if (detail) |d| @intCast(@max(d, 0)) else 0;
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(UIEvent);
|
||||
@@ -125,6 +154,8 @@ pub const JsApi = struct {
|
||||
pub const constructor = bridge.constructor(UIEvent.init, .{});
|
||||
pub const detail = bridge.accessor(UIEvent.getDetail, null, .{});
|
||||
pub const view = bridge.accessor(UIEvent.getView, null, .{});
|
||||
pub const which = bridge.accessor(UIEvent.getWhich, null, .{});
|
||||
pub const initUIEvent = bridge.function(UIEvent.initUIEvent, .{});
|
||||
};
|
||||
|
||||
const testing = @import("../../../testing.zig");
|
||||
|
||||
@@ -1003,6 +1003,60 @@ test "cdp.frame: reload" {
|
||||
}
|
||||
}
|
||||
|
||||
test "cdp.frame: navigate inherits original fragment across redirect" {
|
||||
// RFC 7231 §7.1.2: when a 3xx Location header has no fragment, the redirect
|
||||
// inherits the fragment of the request URL.
|
||||
var ctx = try testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
var bc = try ctx.loadBrowserContext(.{ .id = "BID-9", .url = "hi.html", .target_id = "FID-000000000X".* });
|
||||
|
||||
{
|
||||
// Location: /redirect-target (no fragment) — must inherit #myfrag.
|
||||
try ctx.processMessage(.{
|
||||
.id = 40,
|
||||
.method = "Page.navigate",
|
||||
.params = .{ .url = "http://127.0.0.1:9582/redirect-no-fragment#myfrag" },
|
||||
});
|
||||
|
||||
var runner = try bc.session.runner(.{});
|
||||
try runner.wait(.{ .ms = 2000 });
|
||||
|
||||
const frame = bc.session.currentFrame() orelse unreachable;
|
||||
try testing.expectEqualSlices(u8, "http://127.0.0.1:9582/redirect-target#myfrag", frame.url);
|
||||
}
|
||||
|
||||
{
|
||||
// Location: /redirect-target#target_fragment — target's fragment wins.
|
||||
try ctx.processMessage(.{
|
||||
.id = 41,
|
||||
.method = "Page.navigate",
|
||||
.params = .{ .url = "http://127.0.0.1:9582/redirect-with-fragment#requested" },
|
||||
});
|
||||
|
||||
var runner = try bc.session.runner(.{});
|
||||
try runner.wait(.{ .ms = 2000 });
|
||||
|
||||
const frame = bc.session.currentFrame() orelse unreachable;
|
||||
try testing.expectEqualSlices(u8, "http://127.0.0.1:9582/redirect-target#target_fragment", frame.url);
|
||||
}
|
||||
|
||||
{
|
||||
// No fragment on either side — final URL has no fragment.
|
||||
try ctx.processMessage(.{
|
||||
.id = 42,
|
||||
.method = "Page.navigate",
|
||||
.params = .{ .url = "http://127.0.0.1:9582/redirect-no-fragment" },
|
||||
});
|
||||
|
||||
var runner = try bc.session.runner(.{});
|
||||
try runner.wait(.{ .ms = 2000 });
|
||||
|
||||
const frame = bc.session.currentFrame() orelse unreachable;
|
||||
try testing.expectEqualSlices(u8, "http://127.0.0.1:9582/redirect-target", frame.url);
|
||||
}
|
||||
}
|
||||
|
||||
test "cdp.frame: addScriptToEvaluateOnNewDocument" {
|
||||
var ctx = try testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
@@ -405,7 +405,7 @@ pub fn Builder(comptime commands: anytype) type {
|
||||
error.Overflow => log.fatal(.app, "range overflow", .{ .arg = kebab_cased, .value = str }),
|
||||
error.InvalidCharacter => log.fatal(.app, "invalid character", .{ .arg = kebab_cased, .value = str }),
|
||||
}
|
||||
continue :iter_args;
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
if (is_multiple) {
|
||||
@@ -481,6 +481,7 @@ pub fn Builder(comptime commands: anytype) type {
|
||||
|
||||
// Invalid option choice.
|
||||
log.fatal(.app, "invalid option choice", .{ .arg = kebab_cased, .value = trimmed });
|
||||
return error.InvalidArgument;
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -493,7 +494,7 @@ pub fn Builder(comptime commands: anytype) type {
|
||||
const str = args.next() orelse return error.MissingArgument;
|
||||
const v = std.meta.stringToEnum(E, str) orelse {
|
||||
log.fatal(.app, "invalid option choice", .{ .arg = kebab_cased, .value = str });
|
||||
continue :iter_args;
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
if (is_multiple) {
|
||||
|
||||
24
src/storage/Blackhole.zig
Normal file
24
src/storage/Blackhole.zig
Normal file
@@ -0,0 +1,24 @@
|
||||
// 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 Allocator = std.mem.Allocator;
|
||||
|
||||
const Blackhole = @This();
|
||||
|
||||
pub fn deinit(_: *Blackhole, _: Allocator) void {}
|
||||
@@ -19,6 +19,7 @@
|
||||
const std = @import("std");
|
||||
const log = @import("../log.zig");
|
||||
const Config = @import("../Config.zig");
|
||||
const Blackhole = @import("Blackhole.zig");
|
||||
const Sqlite = @import("sqlite/Sqlite.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
@@ -26,17 +27,19 @@ const Allocator = std.mem.Allocator;
|
||||
const Storage = @This();
|
||||
|
||||
pub const EngineType = enum {
|
||||
none,
|
||||
sqlite,
|
||||
};
|
||||
|
||||
const Engine = union(EngineType) {
|
||||
none: Blackhole,
|
||||
sqlite: Sqlite,
|
||||
};
|
||||
|
||||
engine: Engine,
|
||||
|
||||
pub fn init(allocator: Allocator, config: *const Config) !Storage {
|
||||
const engine_type = config.storageEngine() orelse .sqlite;
|
||||
const engine_type = config.storageEngine() orelse .none;
|
||||
const engine = initEngine(allocator, engine_type, config) catch |err| {
|
||||
log.fatal(.storage, "storage setup", .{ .engine = engine_type, .err = err });
|
||||
return err;
|
||||
@@ -49,6 +52,7 @@ pub fn init(allocator: Allocator, config: *const Config) !Storage {
|
||||
|
||||
fn initEngine(allocator: Allocator, engine_type: EngineType, config: *const Config) !Engine {
|
||||
switch (engine_type) {
|
||||
.none => return .{ .none = Blackhole{} },
|
||||
.sqlite => {
|
||||
const sqlite_path = config.storageSqlitePath();
|
||||
return .{ .sqlite = try Sqlite.init(allocator, sqlite_path) };
|
||||
|
||||
@@ -29,8 +29,9 @@ const Sqlite = @This();
|
||||
|
||||
pool: Pool,
|
||||
|
||||
pub fn init(allocator: Allocator, path: ?[:0]const u8) !Sqlite {
|
||||
var pool = try Pool.init(allocator, path orelse ":memory:");
|
||||
pub fn init(allocator: Allocator, path_: ?[:0]const u8) !Sqlite {
|
||||
const path = path_ orelse ":memory:";
|
||||
var pool = try Pool.init(allocator, path);
|
||||
errdefer pool.deinit(allocator);
|
||||
|
||||
{
|
||||
@@ -39,7 +40,9 @@ pub fn init(allocator: Allocator, path: ?[:0]const u8) !Sqlite {
|
||||
// pool to the return value (copy A) and then release the original
|
||||
const conn = try pool.acquire();
|
||||
defer pool.release(conn);
|
||||
try @import("migrations.zig").run(conn);
|
||||
|
||||
const version = try @import("migrations.zig").run(conn);
|
||||
log.info(.storage, "storage initialized", .{ .engine = "sqlite", .version = version, .path = path });
|
||||
}
|
||||
|
||||
return .{
|
||||
|
||||
@@ -22,9 +22,9 @@ const Sqlite = @import("Sqlite.zig");
|
||||
|
||||
const log = lp.log;
|
||||
|
||||
pub fn run(conn: Sqlite.Conn) !void {
|
||||
pub fn run(conn: Sqlite.Conn) !i64 {
|
||||
const version = try getVersion(conn);
|
||||
log.info(.storage, "migration version", .{ .engine = "sqlite", .version = version });
|
||||
return version;
|
||||
}
|
||||
|
||||
fn getVersion(conn: Sqlite.Conn) !i64 {
|
||||
|
||||
@@ -610,6 +610,32 @@ fn testHTTPHandler(req: *std.http.Server.Request) !void {
|
||||
});
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, path, "/redirect-no-fragment")) {
|
||||
return req.respond("", .{
|
||||
.status = .found,
|
||||
.extra_headers = &.{
|
||||
.{ .name = "Location", .value = "/redirect-target" },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, path, "/redirect-target")) {
|
||||
return req.respond("<!DOCTYPE html><title>landed</title>", .{
|
||||
.extra_headers = &.{
|
||||
.{ .name = "Content-Type", .value = "text/html; charset=utf-8" },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, path, "/redirect-with-fragment")) {
|
||||
return req.respond("", .{
|
||||
.status = .found,
|
||||
.extra_headers = &.{
|
||||
.{ .name = "Location", .value = "/redirect-target#target_fragment" },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, path, "/xhr/404")) {
|
||||
return req.respond("Not Found", .{
|
||||
.status = .not_found,
|
||||
|
||||
Reference in New Issue
Block a user