mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-31 09:29:42 +00:00
Add --wait-selector, --wait-script and --wait-script-file options to fetch
These new optional parameter run AFTER --wait-until, allowing the (imo) useful combination of `--wait-until load --wait-script "report.complete === true"`. However, if `--wait-until` IS NOT specified but `--wait-selector/script` IS, then there is no default wait and it'll just check the selector/script. If neither `--wait-selector` or `--wait-script/--wait-script-file` are specified then `--wait-until` continues to default to `done`. These waiters were added to the Runner, and the existing Action.waitForSelector now uses the runner's version. Selector querying has been split into distinct parse and query functions, so that we can parse once, and query on every tick. We could potentially optimize --wait-script to compile the script once and call it on each tick, but we'd have to detect page navigation to recompile the script in the new context. Something I'd rather optimize separately.
This commit is contained in:
@@ -249,7 +249,9 @@ pub const Fetch = struct {
|
||||
with_frames: bool = false,
|
||||
strip: dump.Opts.Strip = .{},
|
||||
wait_ms: u32 = 5000,
|
||||
wait_until: WaitUntil = .done,
|
||||
wait_until: ?WaitUntil = null,
|
||||
wait_script: ?[:0]const u8 = null,
|
||||
wait_selector: ?[:0]const u8 = null,
|
||||
};
|
||||
|
||||
pub const Common = struct {
|
||||
@@ -413,12 +415,24 @@ pub fn printUsageAndExit(self: *const Config, success: bool) void {
|
||||
\\
|
||||
\\--with-frames Includes the contents of iframes. Defaults to false.
|
||||
\\
|
||||
\\--wait-ms Wait time in milliseconds.
|
||||
\\--wait-ms Wait time in milliseconds. Supersedes all other --wait
|
||||
\\ parameters.
|
||||
\\ Defaults to 5000.
|
||||
\\
|
||||
\\--wait-until Wait until the specified event.
|
||||
\\ Supported events: load, domcontentloaded, networkidle, done.
|
||||
\\ Defaults to 'done'.
|
||||
\\--wait-until Wait until the specified event. Checked before the other
|
||||
\\ --wait- options. Supported events: load, domcontentloaded,
|
||||
\\ networkidle, done.
|
||||
\\ Defaults to 'done'. If --wait-selector, --wait-script or
|
||||
\\ --wait-script-file are specified, defaults to none.
|
||||
\\
|
||||
\\--wait-selector Wait for an element matching the CSS selector to appear.
|
||||
\\ Checked after --wait-until condition is met.
|
||||
\\
|
||||
\\--wait-script Wait for a JavaScript expression to return truthy.
|
||||
\\ Checked after --wait-until condition is met.
|
||||
\\
|
||||
\\--wait-script-file
|
||||
\\ Like --wait-script, but reads the script from a file.
|
||||
\\
|
||||
++ common_options ++
|
||||
\\
|
||||
@@ -685,7 +699,9 @@ fn parseFetchArgs(
|
||||
var common: Common = .{};
|
||||
var strip: dump.Opts.Strip = .{};
|
||||
var wait_ms: u32 = 5000;
|
||||
var wait_until: WaitUntil = .done;
|
||||
var wait_until: ?WaitUntil = null;
|
||||
var wait_script: ?[:0]const u8 = null;
|
||||
var wait_selector: ?[:0]const u8 = null;
|
||||
|
||||
while (args.next()) |opt| {
|
||||
if (std.mem.eql(u8, "--wait-ms", opt) or std.mem.eql(u8, "--wait_ms", opt)) {
|
||||
@@ -712,6 +728,36 @@ fn parseFetchArgs(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--wait-selector", opt) or std.mem.eql(u8, "--wait_selector", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = opt });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
wait_selector = try allocator.dupeZ(u8, str);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--wait-script", opt) or std.mem.eql(u8, "--wait_script", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = opt });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
wait_script = try allocator.dupeZ(u8, str);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--wait-script-file", opt) or std.mem.eql(u8, "--wait_script_file", opt)) {
|
||||
const path = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = opt });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
wait_script = std.fs.cwd().readFileAllocOptions(allocator, path, 1024 * 1024, null, .of(u8), 0) catch |err| {
|
||||
log.fatal(.app, "failed to read file", .{ .arg = opt, .path = path, .err = err });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--dump", opt)) {
|
||||
var peek_args = args.*;
|
||||
if (peek_args.next()) |next_arg| {
|
||||
@@ -802,6 +848,8 @@ fn parseFetchArgs(
|
||||
.with_frames = with_frames,
|
||||
.wait_ms = wait_ms,
|
||||
.wait_until = wait_until,
|
||||
.wait_selector = wait_selector,
|
||||
.wait_script = wait_script,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -515,7 +515,7 @@ test "SemanticTree backendDOMNodeId" {
|
||||
var registry: CDPNode.Registry = .init(testing.allocator);
|
||||
defer registry.deinit();
|
||||
|
||||
var page = try testing.pageTest("cdp/registry1.html");
|
||||
var page = try testing.pageTest("cdp/registry1.html", .{});
|
||||
defer testing.reset();
|
||||
defer page._session.removePage();
|
||||
|
||||
@@ -539,7 +539,7 @@ test "SemanticTree max_depth" {
|
||||
var registry: CDPNode.Registry = .init(testing.allocator);
|
||||
defer registry.deinit();
|
||||
|
||||
var page = try testing.pageTest("cdp/registry1.html");
|
||||
var page = try testing.pageTest("cdp/registry1.html", .{});
|
||||
defer testing.reset();
|
||||
defer page._session.removePage();
|
||||
|
||||
|
||||
@@ -22,10 +22,14 @@ const builtin = @import("builtin");
|
||||
|
||||
const log = @import("../log.zig");
|
||||
|
||||
const js = @import("js/js.zig");
|
||||
const Page = @import("Page.zig");
|
||||
const Session = @import("Session.zig");
|
||||
const HttpClient = @import("HttpClient.zig");
|
||||
|
||||
const Node = @import("webapi/Node.zig");
|
||||
const Selector = @import("webapi/selector/Selector.zig");
|
||||
|
||||
const IS_DEBUG = builtin.mode == .Debug;
|
||||
|
||||
const Runner = @This();
|
||||
@@ -236,3 +240,111 @@ fn _tick(self: *Runner, comptime is_cdp: bool, opts: TickOpts) !CDPTickResult {
|
||||
.raw_done => return .done,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn waitForSelector(self: *Runner, selector: [:0]const u8, timeout_ms: u32) !*Node.Element {
|
||||
const arena = try self.session.getArena(.{ .debug = "Runner.waitForSelector" });
|
||||
defer self.session.releaseArena(arena);
|
||||
|
||||
var timer = try std.time.Timer.start();
|
||||
const parsed_selector = try Selector.parseLeaky(arena, selector);
|
||||
|
||||
while (true) {
|
||||
// self.page can change between ticks
|
||||
const page = self.page;
|
||||
if (try parsed_selector.query(page.document.asNode(), page)) |el| {
|
||||
return el;
|
||||
}
|
||||
|
||||
const elapsed: u32 = @intCast(timer.read() / std.time.ns_per_ms);
|
||||
if (elapsed >= timeout_ms) {
|
||||
return error.Timeout;
|
||||
}
|
||||
switch (try self.tick(.{ .ms = timeout_ms - elapsed })) {
|
||||
.done => return error.Timeout,
|
||||
.ok => |recommended_sleep_ms| {
|
||||
if (recommended_sleep_ms > 0) {
|
||||
std.Thread.sleep(std.time.ns_per_ms * recommended_sleep_ms);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn waitForScript(runner: *Runner, script: [:0]const u8, timeout_ms: u32) !void {
|
||||
var timer = try std.time.Timer.start();
|
||||
|
||||
while (true) {
|
||||
const page = runner.page;
|
||||
|
||||
// Execute the script and check if it returns truthy
|
||||
var ls: js.Local.Scope = undefined;
|
||||
page.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
var try_catch: js.TryCatch = undefined;
|
||||
try_catch.init(&ls.local);
|
||||
defer try_catch.deinit();
|
||||
|
||||
const value = ls.local.exec(script, "wait_script") catch |err| {
|
||||
const caught = try_catch.caughtOrError(page.call_arena, err);
|
||||
log.err(.app, "wait script error", .{ .err = caught });
|
||||
return error.ScriptError;
|
||||
};
|
||||
|
||||
if (value.toBool()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elapsed: u32 = @intCast(timer.read() / std.time.ns_per_ms);
|
||||
if (elapsed >= timeout_ms) {
|
||||
return error.Timeout;
|
||||
}
|
||||
switch (try runner.tick(.{ .ms = timeout_ms - elapsed })) {
|
||||
.done => return error.Timeout,
|
||||
.ok => |recommended_sleep_ms| {
|
||||
if (recommended_sleep_ms > 0) {
|
||||
std.Thread.sleep(std.time.ns_per_ms * recommended_sleep_ms);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
test "Runner: no page" {
|
||||
try testing.expectError(error.NoPage, Runner.init(testing.test_session, .{}));
|
||||
}
|
||||
|
||||
test "Runner: waitForSelector timeout" {
|
||||
const page = try testing.pageTest("runner/runner1.html", .{});
|
||||
defer page._session.removePage();
|
||||
|
||||
var runner = try page._session.runner(.{});
|
||||
try testing.expectError(error.Timeout, runner.waitForSelector("#nope", 10));
|
||||
}
|
||||
|
||||
test "Runner: waitForSelector" {
|
||||
defer testing.reset();
|
||||
const page = try testing.pageTest("runner/runner1.html", .{});
|
||||
defer page._session.removePage();
|
||||
|
||||
var runner = try page._session.runner(.{});
|
||||
const el = try runner.waitForSelector("#sel1", 10);
|
||||
try testing.expectEqual("selector-1-content", try el.asNode().getTextContentAlloc(testing.arena_allocator));
|
||||
}
|
||||
|
||||
test "Runner: waitForScript timeout" {
|
||||
const page = try testing.pageTest("runner/runner1.html", .{});
|
||||
defer page._session.removePage();
|
||||
|
||||
var runner = try page._session.runner(.{});
|
||||
try testing.expectError(error.Timeout, runner.waitForScript("document.querySelector('#nope')", 10));
|
||||
}
|
||||
|
||||
test "Runner: waitForScript" {
|
||||
const page = try testing.pageTest("runner/runner1.html", .{});
|
||||
defer page._session.removePage();
|
||||
|
||||
var runner = try page._session.runner(.{});
|
||||
try runner.waitForScript("document.querySelector('#sel1')", 10);
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ fn addRawRule(self: *StyleManager, selector_text: []const u8, block_text: []cons
|
||||
|
||||
if (!props.isRelevant()) return;
|
||||
|
||||
const selectors = SelectorParser.parseList(self.arena, selector_text, self.page) catch return;
|
||||
const selectors = SelectorParser.parseList(self.arena, selector_text) catch return;
|
||||
for (selectors) |selector| {
|
||||
const rightmost = if (selector.segments.len > 0) selector.segments[selector.segments.len - 1].compound else selector.first;
|
||||
const bucket_key = getBucketKey(rightmost) orelse continue;
|
||||
@@ -484,7 +484,7 @@ fn addRule(self: *StyleManager, style_rule: *CSSStyleRule) !void {
|
||||
}
|
||||
|
||||
// Parse the selector list
|
||||
const selectors = SelectorParser.parseList(self.arena, selector_text, self.page) catch return;
|
||||
const selectors = SelectorParser.parseList(self.arena, selector_text) catch return;
|
||||
if (selectors.len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -110,28 +110,10 @@ pub fn waitForSelector(selector: [:0]const u8, timeout_ms: u32, session: *Sessio
|
||||
var runner = try session.runner(.{});
|
||||
try runner.wait(.{ .ms = timeout_ms, .until = .load });
|
||||
|
||||
while (true) {
|
||||
const page = runner.page;
|
||||
const element = Selector.querySelector(page.document.asNode(), selector, page) catch {
|
||||
return error.InvalidSelector;
|
||||
};
|
||||
const elapsed: u32 = @intCast(timer.read() / std.time.ns_per_ms);
|
||||
const remaining = timeout_ms -| elapsed;
|
||||
if (remaining == 0) return error.Timeout;
|
||||
|
||||
if (element) |el| {
|
||||
const el = try runner.waitForSelector(selector, timeout_ms);
|
||||
return el.asNode();
|
||||
}
|
||||
|
||||
const elapsed: u32 = @intCast(timer.read() / std.time.ns_per_ms);
|
||||
if (elapsed >= timeout_ms) {
|
||||
return error.Timeout;
|
||||
}
|
||||
switch (try runner.tick(.{ .ms = timeout_ms - elapsed })) {
|
||||
.done => return error.Timeout,
|
||||
.ok => |recommended_sleep_ms| {
|
||||
if (recommended_sleep_ms > 0) {
|
||||
// guanrateed to be <= 20ms
|
||||
std.Thread.sleep(std.time.ns_per_ms * recommended_sleep_ms);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
4
src/browser/tests/runner/runner1.html
Normal file
4
src/browser/tests/runner/runner1.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<meta charset="UTF-8">
|
||||
<div id=sel0>selector-0-content</div>
|
||||
<div id=sel1>selector-1-content</div>
|
||||
@@ -26,6 +26,8 @@ const GenericIterator = @import("../collections/iterator.zig").Entry;
|
||||
const Page = @import("../../Page.zig");
|
||||
const String = @import("../../../string.zig").String;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
|
||||
pub fn registerTypes() []const type {
|
||||
@@ -419,7 +421,7 @@ pub fn validateAttributeName(name: String) !void {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn normalizeNameForLookup(name: String, page: *Page) !String {
|
||||
fn normalizeNameForLookup(name: String, page: *Page) !String {
|
||||
if (!needsLowerCasing(name.str())) {
|
||||
return name;
|
||||
}
|
||||
@@ -431,6 +433,14 @@ pub fn normalizeNameForLookup(name: String, page: *Page) !String {
|
||||
return .wrap(normalized);
|
||||
}
|
||||
|
||||
pub fn normalizeNameForLookupAlloc(allocator: Allocator, name: String) !String {
|
||||
if (!needsLowerCasing(name.str())) {
|
||||
return name.dupe(allocator);
|
||||
}
|
||||
const normalized = try std.ascii.allocLowerString(allocator, name.str());
|
||||
return .wrap(normalized);
|
||||
}
|
||||
|
||||
fn needsLowerCasing(name: []const u8) bool {
|
||||
var remaining = name;
|
||||
if (comptime std.simd.suggestVectorLength(u8)) |vector_len| {
|
||||
|
||||
@@ -74,7 +74,7 @@ fn preprocessInput(arena: Allocator, input: []const u8) ![]const u8 {
|
||||
return result.items;
|
||||
}
|
||||
|
||||
pub fn parseList(arena: Allocator, input: []const u8, page: *Page) ParseError![]const Selector.Selector {
|
||||
pub fn parseList(arena: Allocator, input: []const u8) ParseError![]const Selector.Selector {
|
||||
// Preprocess input to normalize line endings
|
||||
const preprocessed = try preprocessInput(arena, input);
|
||||
|
||||
@@ -140,7 +140,7 @@ pub fn parseList(arena: Allocator, input: []const u8, page: *Page) ParseError![]
|
||||
const selector_input = std.mem.trimRight(u8, trimmed[0..comma_pos], &std.ascii.whitespace);
|
||||
|
||||
if (selector_input.len > 0) {
|
||||
const selector = try parse(arena, selector_input, page);
|
||||
const selector = try parse(arena, selector_input);
|
||||
try selectors.append(arena, selector);
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ pub fn parseList(arena: Allocator, input: []const u8, page: *Page) ParseError![]
|
||||
return selectors.items;
|
||||
}
|
||||
|
||||
pub fn parse(arena: Allocator, input: []const u8, page: *Page) ParseError!Selector.Selector {
|
||||
pub fn parse(arena: Allocator, input: []const u8) ParseError!Selector.Selector {
|
||||
var parser = Parser{ .input = input };
|
||||
var segments: std.ArrayList(Segment) = .empty;
|
||||
var current_compound: std.ArrayList(Part) = .empty;
|
||||
@@ -164,7 +164,7 @@ pub fn parse(arena: Allocator, input: []const u8, page: *Page) ParseError!Select
|
||||
while (parser.skipSpaces()) {
|
||||
if (parser.peek() == 0) break;
|
||||
|
||||
const part = try parser.parsePart(arena, page);
|
||||
const part = try parser.parsePart(arena);
|
||||
try current_compound.append(arena, part);
|
||||
|
||||
// Check what comes after this part
|
||||
@@ -238,7 +238,7 @@ pub fn parse(arena: Allocator, input: []const u8, page: *Page) ParseError!Select
|
||||
while (parser.skipSpaces()) {
|
||||
if (parser.peek() == 0) break;
|
||||
|
||||
const part = try parser.parsePart(arena, page);
|
||||
const part = try parser.parsePart(arena);
|
||||
try current_compound.append(arena, part);
|
||||
|
||||
// Check what comes after this part
|
||||
@@ -289,7 +289,7 @@ pub fn parse(arena: Allocator, input: []const u8, page: *Page) ParseError!Select
|
||||
};
|
||||
}
|
||||
|
||||
fn parsePart(self: *Parser, arena: Allocator, page: *Page) !Part {
|
||||
fn parsePart(self: *Parser, arena: Allocator) !Part {
|
||||
return switch (self.peek()) {
|
||||
'#' => .{ .id = try self.id(arena) },
|
||||
'.' => .{ .class = try self.class(arena) },
|
||||
@@ -297,16 +297,17 @@ fn parsePart(self: *Parser, arena: Allocator, page: *Page) !Part {
|
||||
self.input = self.input[1..];
|
||||
break :blk .universal;
|
||||
},
|
||||
'[' => .{ .attribute = try self.attribute(arena, page) },
|
||||
':' => .{ .pseudo_class = try self.pseudoClass(arena, page) },
|
||||
'[' => .{ .attribute = try self.attribute(arena) },
|
||||
':' => .{ .pseudo_class = try self.pseudoClass(arena) },
|
||||
'a'...'z', 'A'...'Z', '_', '\\', 0x80...0xFF => blk: {
|
||||
// Use parseIdentifier for full escape support
|
||||
const tag_name = try self.parseIdentifier(arena, error.InvalidTagSelector);
|
||||
if (tag_name.len > 256) {
|
||||
return error.InvalidTagSelector;
|
||||
}
|
||||
var buf: [256]u8 = undefined;
|
||||
// Try to match as a known tag enum for optimization
|
||||
const lower = std.ascii.lowerString(&page.buf, tag_name);
|
||||
const lower = std.ascii.lowerString(&buf, tag_name);
|
||||
if (Node.Element.Tag.parseForMatch(lower)) |known_tag| {
|
||||
break :blk .{ .tag = known_tag };
|
||||
}
|
||||
@@ -373,7 +374,7 @@ fn consumeUntilCommaOrParen(self: *Parser) []const u8 {
|
||||
return result;
|
||||
}
|
||||
|
||||
fn pseudoClass(self: *Parser, arena: Allocator, page: *Page) !Selector.PseudoClass {
|
||||
fn pseudoClass(self: *Parser, arena: Allocator) !Selector.PseudoClass {
|
||||
if (comptime IS_DEBUG) {
|
||||
// Should have been verified by caller
|
||||
std.debug.assert(self.peek() == ':');
|
||||
@@ -445,7 +446,7 @@ fn pseudoClass(self: *Parser, arena: Allocator, page: *Page) !Selector.PseudoCla
|
||||
if (self.peek() == 0) return error.InvalidPseudoClass;
|
||||
|
||||
// Parse a full selector (with potential combinators and compounds)
|
||||
const selector = try parse(arena, self.consumeUntilCommaOrParen(), page);
|
||||
const selector = try parse(arena, self.consumeUntilCommaOrParen());
|
||||
try selectors.append(arena, selector);
|
||||
|
||||
_ = self.skipSpaces();
|
||||
@@ -472,7 +473,7 @@ fn pseudoClass(self: *Parser, arena: Allocator, page: *Page) !Selector.PseudoCla
|
||||
if (self.peek() == ')') break;
|
||||
if (self.peek() == 0) return error.InvalidPseudoClass;
|
||||
|
||||
const selector = try parse(arena, self.consumeUntilCommaOrParen(), page);
|
||||
const selector = try parse(arena, self.consumeUntilCommaOrParen());
|
||||
try selectors.append(arena, selector);
|
||||
|
||||
_ = self.skipSpaces();
|
||||
@@ -499,7 +500,7 @@ fn pseudoClass(self: *Parser, arena: Allocator, page: *Page) !Selector.PseudoCla
|
||||
if (self.peek() == ')') break;
|
||||
if (self.peek() == 0) return error.InvalidPseudoClass;
|
||||
|
||||
const selector = try parse(arena, self.consumeUntilCommaOrParen(), page);
|
||||
const selector = try parse(arena, self.consumeUntilCommaOrParen());
|
||||
try selectors.append(arena, selector);
|
||||
|
||||
_ = self.skipSpaces();
|
||||
@@ -526,7 +527,7 @@ fn pseudoClass(self: *Parser, arena: Allocator, page: *Page) !Selector.PseudoCla
|
||||
if (self.peek() == ')') break;
|
||||
if (self.peek() == 0) return error.InvalidPseudoClass;
|
||||
|
||||
const selector = try parse(arena, self.consumeUntilCommaOrParen(), page);
|
||||
const selector = try parse(arena, self.consumeUntilCommaOrParen());
|
||||
try selectors.append(arena, selector);
|
||||
|
||||
_ = self.skipSpaces();
|
||||
@@ -898,7 +899,7 @@ fn tag(self: *Parser) ![]const u8 {
|
||||
return input[0..i];
|
||||
}
|
||||
|
||||
fn attribute(self: *Parser, arena: Allocator, page: *Page) !Selector.Attribute {
|
||||
fn attribute(self: *Parser, arena: Allocator) !Selector.Attribute {
|
||||
if (comptime IS_DEBUG) {
|
||||
// should have been verified by caller
|
||||
std.debug.assert(self.peek() == '[');
|
||||
@@ -910,8 +911,7 @@ fn attribute(self: *Parser, arena: Allocator, page: *Page) !Selector.Attribute {
|
||||
const attr_name = try self.attributeName();
|
||||
|
||||
// Normalize the name to lowercase for fast matching (consistent with Attribute.normalizeNameForLookup)
|
||||
const normalized = try Attribute.normalizeNameForLookup(.wrap(attr_name), page);
|
||||
const name = try normalized.dupe(arena);
|
||||
const name = try Attribute.normalizeNameForLookupAlloc(arena, .wrap(attr_name));
|
||||
var case_insensitive = false;
|
||||
_ = self.skipSpaces();
|
||||
|
||||
|
||||
@@ -20,41 +20,24 @@ const std = @import("std");
|
||||
|
||||
const String = @import("../../../string.zig").String;
|
||||
|
||||
const Parser = @import("Parser.zig");
|
||||
const Node = @import("../Node.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
|
||||
const Parser = @import("Parser.zig");
|
||||
pub const List = @import("List.zig");
|
||||
|
||||
pub fn querySelector(root: *Node, input: []const u8, page: *Page) !?*Node.Element {
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
pub fn parseLeaky(arena: Allocator, input: []const u8) !Parsed {
|
||||
if (input.len == 0) {
|
||||
return error.SyntaxError;
|
||||
}
|
||||
|
||||
const arena = page.call_arena;
|
||||
const selectors = try Parser.parseList(arena, input, page);
|
||||
|
||||
for (selectors) |selector| {
|
||||
// Fast path: single compound with only an ID selector
|
||||
if (selector.segments.len == 0 and selector.first.parts.len == 1) {
|
||||
const first = selector.first.parts[0];
|
||||
if (first == .id) {
|
||||
const el = page.getElementByIdFromNode(root, first.id) orelse continue;
|
||||
// Check if the element is within the root subtree
|
||||
const node = el.asNode();
|
||||
if (node != root and root.contains(node)) {
|
||||
return el;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
return .{ .selectors = try Parser.parseList(arena, input) };
|
||||
}
|
||||
|
||||
if (List.initOne(root, selector, page)) |node| {
|
||||
if (node.is(Node.Element)) |el| {
|
||||
return el;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
pub fn querySelector(root: *Node, input: []const u8, page: *Page) !?*Node.Element {
|
||||
const parsed = try parseLeaky(page.call_arena, input);
|
||||
return parsed.query(root, page);
|
||||
}
|
||||
|
||||
pub fn querySelectorAll(root: *Node, input: []const u8, page: *Page) !*List {
|
||||
@@ -67,7 +50,7 @@ pub fn querySelectorAll(root: *Node, input: []const u8, page: *Page) !*List {
|
||||
|
||||
var nodes: std.AutoArrayHashMapUnmanaged(*Node, void) = .empty;
|
||||
|
||||
const selectors = try Parser.parseList(arena, input, page);
|
||||
const selectors = try Parser.parseList(arena, input);
|
||||
for (selectors) |selector| {
|
||||
try List.collect(arena, root, selector, &nodes, page);
|
||||
}
|
||||
@@ -86,7 +69,7 @@ pub fn matches(el: *Node.Element, input: []const u8, page: *Page) !bool {
|
||||
}
|
||||
|
||||
const arena = page.call_arena;
|
||||
const selectors = try Parser.parseList(arena, input, page);
|
||||
const selectors = try Parser.parseList(arena, input);
|
||||
|
||||
for (selectors) |selector| {
|
||||
if (List.matches(el.asNode(), selector, el.asNode(), page)) {
|
||||
@@ -104,7 +87,7 @@ pub fn matchesWithScope(el: *Node.Element, input: []const u8, scope: *Node.Eleme
|
||||
}
|
||||
|
||||
const arena = page.call_arena;
|
||||
const selectors = try Parser.parseList(arena, input, page);
|
||||
const selectors = try Parser.parseList(arena, input);
|
||||
|
||||
for (selectors) |selector| {
|
||||
if (List.matches(el.asNode(), selector, scope.asNode(), page)) {
|
||||
@@ -292,3 +275,32 @@ pub const Selector = struct {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub const Parsed = struct {
|
||||
selectors: []const Selector,
|
||||
|
||||
pub fn query(self: Parsed, root: *Node, page: *Page) !?*Node.Element {
|
||||
for (self.selectors) |selector| {
|
||||
// Fast path: single compound with only an ID selector
|
||||
if (selector.segments.len == 0 and selector.first.parts.len == 1) {
|
||||
const first = selector.first.parts[0];
|
||||
if (first == .id) {
|
||||
const el = page.getElementByIdFromNode(root, first.id) orelse continue;
|
||||
// Check if the element is within the root subtree
|
||||
const node = el.asNode();
|
||||
if (node != root and root.contains(node)) {
|
||||
return el;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (List.initOne(root, selector, page)) |node| {
|
||||
if (node.is(Node.Element)) |el| {
|
||||
return el;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1179,7 +1179,7 @@ test "AXNode: writer" {
|
||||
var registry = Node.Registry.init(testing.allocator);
|
||||
defer registry.deinit();
|
||||
|
||||
var page = try testing.pageTest("cdp/dom3.html");
|
||||
var page = try testing.pageTest("cdp/dom3.html", .{});
|
||||
defer page._session.removePage();
|
||||
var doc = page.window._document;
|
||||
|
||||
|
||||
@@ -343,7 +343,7 @@ test "cdp Node: Registry register" {
|
||||
try testing.expectEqual(0, registry.lookup_by_id.count());
|
||||
try testing.expectEqual(0, registry.lookup_by_node.count());
|
||||
|
||||
var page = try testing.pageTest("cdp/registry1.html");
|
||||
var page = try testing.pageTest("cdp/registry1.html", .{});
|
||||
defer page._session.removePage();
|
||||
var doc = page.window._document;
|
||||
|
||||
@@ -400,7 +400,7 @@ test "cdp Node: search list" {
|
||||
}
|
||||
|
||||
{
|
||||
var page = try testing.pageTest("cdp/registry2.html");
|
||||
var page = try testing.pageTest("cdp/registry2.html", .{});
|
||||
defer page._session.removePage();
|
||||
var doc = page.window._document;
|
||||
|
||||
@@ -440,7 +440,7 @@ test "cdp Node: Writer" {
|
||||
var registry = Registry.init(testing.allocator);
|
||||
defer registry.deinit();
|
||||
|
||||
var page = try testing.pageTest("cdp/registry3.html");
|
||||
var page = try testing.pageTest("cdp/registry3.html", .{});
|
||||
defer page._session.removePage();
|
||||
var doc = page.window._document;
|
||||
|
||||
|
||||
@@ -48,7 +48,9 @@ const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
|
||||
pub const FetchOpts = struct {
|
||||
wait_ms: u32 = 5000,
|
||||
wait_until: Config.WaitUntil = .load,
|
||||
wait_until: ?Config.WaitUntil = null,
|
||||
wait_script: ?[:0]const u8 = null,
|
||||
wait_selector: ?[:0]const u8 = null,
|
||||
dump: dump.Opts,
|
||||
dump_mode: ?Config.DumpFormat = null,
|
||||
writer: ?*std.Io.Writer = null,
|
||||
@@ -111,7 +113,31 @@ pub fn fetch(app: *App, url: [:0]const u8, opts: FetchOpts) !void {
|
||||
.kind = .{ .push = null },
|
||||
});
|
||||
var runner = try session.runner(.{});
|
||||
try runner.wait(.{ .ms = opts.wait_ms, .until = opts.wait_until });
|
||||
|
||||
var timer = try std.time.Timer.start();
|
||||
|
||||
if (opts.wait_until) |wu| {
|
||||
try runner.wait(.{ .ms = opts.wait_ms, .until = wu });
|
||||
} else if (opts.wait_selector == null and opts.wait_script == null) {
|
||||
// We default to .done if both wait_selector and wait_script are null
|
||||
// This allows the caller to ONLY --wait-selector or ONLY --wait-script
|
||||
// or combine --wait-until WITH --wait-selector/script
|
||||
try runner.wait(.{ .ms = opts.wait_ms, .until = .done });
|
||||
}
|
||||
|
||||
if (opts.wait_selector) |selector| {
|
||||
const elapsed: u32 = @intCast(timer.read() / std.time.ns_per_ms);
|
||||
const remaining = opts.wait_ms -| elapsed;
|
||||
if (remaining == 0) return error.Timeout;
|
||||
_ = try runner.waitForSelector(selector, opts.wait_ms);
|
||||
}
|
||||
|
||||
if (opts.wait_script) |script| {
|
||||
const elapsed: u32 = @intCast(timer.read() / std.time.ns_per_ms);
|
||||
const remaining = opts.wait_ms -| elapsed;
|
||||
if (remaining == 0) return error.Timeout;
|
||||
try runner.waitForScript(script, opts.wait_ms);
|
||||
}
|
||||
|
||||
const writer = opts.writer orelse return;
|
||||
if (opts.dump_mode) |mode| {
|
||||
|
||||
@@ -123,6 +123,8 @@ fn run(allocator: Allocator, main_arena: Allocator) !void {
|
||||
var fetch_opts = lp.FetchOpts{
|
||||
.wait_ms = opts.wait_ms,
|
||||
.wait_until = opts.wait_until,
|
||||
.wait_script = opts.wait_script,
|
||||
.wait_selector = opts.wait_selector,
|
||||
.dump_mode = opts.dump_mode,
|
||||
.dump = .{
|
||||
.strip = opts.strip,
|
||||
|
||||
@@ -452,8 +452,10 @@ fn runWebApiTest(test_file: [:0]const u8) !void {
|
||||
}
|
||||
}
|
||||
|
||||
// Used by a few CDP tests - wouldn't be sad to see this go.
|
||||
pub fn pageTest(comptime test_file: []const u8) !*Page {
|
||||
const PageTestOpts = struct {
|
||||
wait_until_done: bool = true,
|
||||
};
|
||||
pub fn pageTest(comptime test_file: []const u8, opts: PageTestOpts) !*Page {
|
||||
const page = try test_session.createPage();
|
||||
errdefer test_session.removePage();
|
||||
|
||||
@@ -466,7 +468,9 @@ pub fn pageTest(comptime test_file: []const u8) !*Page {
|
||||
|
||||
try page.navigate(url, .{});
|
||||
var runner = try test_session.runner(.{});
|
||||
if (opts.wait_until_done) {
|
||||
try runner.wait(.{ .ms = 2000 });
|
||||
}
|
||||
return page;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user