Merge branch 'main' into agent

This commit is contained in:
Adrià Arrufat
2026-05-15 10:51:45 +02:00
9 changed files with 428 additions and 121 deletions

View File

@@ -258,6 +258,10 @@ fn installNewActivePage(self: *Session, frame_id: u32) !*Frame {
// the pointer on Frame is just returned as a convenience
pub fn createPage(self: *Session) !*Frame {
lp.assert(self._active == null, "Session.createPage - page not null", .{});
// Drain any pending Page deinits now, while we're at a known-safe point
self.processQueuedDestroyed();
if (comptime IS_DEBUG) {
log.debug(.browser, "create page", .{});
}

View File

@@ -89,7 +89,13 @@ fn parseSheet(self: *StyleManager, sheet: *CSSStyleSheet) !void {
const text = try style.asNode().getTextContentAlloc(self.arena);
var it = CssParser.parseStylesheet(text);
while (it.next()) |parsed_rule| {
try self.addRawRule(parsed_rule.selector, parsed_rule.block);
// StyleManager only filters on regular style rules (display,
// visibility, opacity). At-rules don't carry top-level
// declarations relevant here -- skip them.
switch (parsed_rule) {
.style => |s| try self.addRawRule(s.selector, s.block),
.at_rule => {},
}
}
}
}

View File

@@ -294,11 +294,36 @@ fn isBang(token: Tokenizer.Token) bool {
};
}
pub const Rule = struct {
pub const StyleRule = struct {
selector: []const u8,
block: []const u8,
};
/// An at-rule (`@keyframes`, `@media`, `@supports`, `@font-face`, etc.).
///
/// We don't apply at-rules to the page (the CSS engine doesn't process them
/// yet), but we do surface them so that JS-side reads via `cssRules` see
/// what was inserted. CSS-in-JS libraries (styled-components, emotion,
/// Stitches, Mantine) deduplicate their stylesheets by reading back
/// `cssRules` after `insertRule` -- if the rule is missing they fall back to
/// per-render `<style>` element injection, which leaks unboundedly. See
/// lightpanda-io/browser#2459.
pub const AtRule = struct {
/// At-keyword without the leading `@` (e.g., `"keyframes"`, `"media"`,
/// `"-webkit-keyframes"`). Borrowed from the input slice; copy if you
/// need to outlive the input.
keyword: []const u8,
/// Full at-rule source span starting at `@` and ending after the closing
/// brace (block at-rules) or semicolon (statement at-rules). Borrowed
/// from the input slice.
text: []const u8,
};
pub const Rule = union(enum) {
style: StyleRule,
at_rule: AtRule,
};
pub fn parseStylesheet(input: []const u8) RulesIterator {
return RulesIterator.init(input);
}
@@ -306,7 +331,6 @@ pub fn parseStylesheet(input: []const u8) RulesIterator {
pub const RulesIterator = struct {
input: []const u8,
stream: TokenStream,
has_skipped_at_rule: bool = false,
pub fn init(input: []const u8) RulesIterator {
return .{
@@ -352,18 +376,14 @@ pub const RulesIterator = struct {
var selector = self.input[selector_start.?..selector_end.?];
selector = std.mem.trim(u8, selector, &std.ascii.whitespace);
return .{
return .{ .style = .{
.selector = selector,
.block = self.input[block_start..block_end],
};
} };
}
if (peeked.token == .at_keyword) {
self.has_skipped_at_rule = true;
self.skipAtRule();
selector_start = null;
selector_end = null;
continue;
return .{ .at_rule = self.consumeAtRule() };
}
if (selector_start == null and (isWhitespaceOrComment(peeked.token) or isSemicolon(peeked.token))) {
@@ -395,29 +415,48 @@ pub const RulesIterator = struct {
}
}
fn skipAtRule(self: *RulesIterator) void {
_ = self.stream.next(); // consume @keyword
/// Consume a full at-rule (statement or block form) and return its
/// keyword (without `@`) and full source span. Mirrors `skipAtRule`'s
/// termination logic but records spans instead of discarding.
fn consumeAtRule(self: *RulesIterator) AtRule {
const at_span = self.stream.next() orelse unreachable; // caller peeked an at_keyword
const start = at_span.start;
const keyword = switch (at_span.token) {
.at_keyword => |name| name,
else => "",
};
var end = at_span.end;
var depth: usize = 0;
var saw_block = false;
while (true) {
const peeked = self.stream.peek() orelse return;
const peeked = self.stream.peek() orelse break;
if (!saw_block and isSemicolon(peeked.token) and depth == 0) {
_ = self.stream.next();
return;
const semi = self.stream.next() orelse break;
end = semi.end;
break;
}
const span = self.stream.next() orelse return;
if (isWhitespaceOrComment(span.token)) continue;
const span = self.stream.next() orelse break;
if (isWhitespaceOrComment(span.token)) {
end = span.end;
continue;
}
end = span.end;
if (span.token == .curly_bracket_block) {
depth += 1;
saw_block = true;
} else if (span.token == .close_curly_bracket) {
if (depth > 0) depth -= 1;
if (saw_block and depth == 0) return;
if (saw_block and depth == 0) break;
}
}
return .{
.keyword = keyword,
.text = self.input[start..end],
};
}
};
@@ -426,8 +465,8 @@ const testing = std.testing;
test "RulesIterator: single rule" {
var it = RulesIterator.init(".test { color: red; }");
const rule = it.next() orelse return error.MissingRule;
try testing.expectEqualStrings(".test", rule.selector);
try testing.expectEqualStrings(" color: red; ", rule.block);
try testing.expectEqualStrings(".test", rule.style.selector);
try testing.expectEqualStrings(" color: red; ", rule.style.block);
try testing.expectEqual(@as(?Rule, null), it.next());
}
@@ -435,31 +474,47 @@ test "RulesIterator: multiple rules" {
var it = RulesIterator.init("h1 { margin: 0; } p { padding: 10px; }");
var rule = it.next() orelse return error.MissingRule;
try testing.expectEqualStrings("h1", rule.selector);
try testing.expectEqualStrings(" margin: 0; ", rule.block);
try testing.expectEqualStrings("h1", rule.style.selector);
try testing.expectEqualStrings(" margin: 0; ", rule.style.block);
rule = it.next() orelse return error.MissingRule;
try testing.expectEqualStrings("p", rule.selector);
try testing.expectEqualStrings(" padding: 10px; ", rule.block);
try testing.expectEqualStrings("p", rule.style.selector);
try testing.expectEqualStrings(" padding: 10px; ", rule.style.block);
try testing.expectEqual(@as(?Rule, null), it.next());
}
test "RulesIterator: skips at-rules without block" {
test "RulesIterator: surfaces statement at-rules" {
var it = RulesIterator.init("@import url('style.css'); .test { color: red; }");
const at = it.next() orelse return error.MissingRule;
try testing.expectEqualStrings("import", at.at_rule.keyword);
try testing.expectEqualStrings("@import url('style.css');", at.at_rule.text);
const rule = it.next() orelse return error.MissingRule;
try testing.expectEqualStrings(".test", rule.selector);
try testing.expectEqualStrings(" color: red; ", rule.block);
try testing.expectEqualStrings(".test", rule.style.selector);
try testing.expectEqualStrings(" color: red; ", rule.style.block);
try testing.expectEqual(@as(?Rule, null), it.next());
}
test "RulesIterator: skips at-rules with block" {
test "RulesIterator: surfaces block at-rules" {
var it = RulesIterator.init("@media screen { .test { color: blue; } } .test2 { color: green; }");
const at = it.next() orelse return error.MissingRule;
try testing.expectEqualStrings("media", at.at_rule.keyword);
try testing.expectEqualStrings("@media screen { .test { color: blue; } }", at.at_rule.text);
const rule = it.next() orelse return error.MissingRule;
try testing.expectEqualStrings(".test2", rule.selector);
try testing.expectEqualStrings(" color: green; ", rule.block);
try testing.expectEqualStrings(".test2", rule.style.selector);
try testing.expectEqualStrings(" color: green; ", rule.style.block);
try testing.expectEqual(@as(?Rule, null), it.next());
}
test "RulesIterator: surfaces vendor-prefixed at-rules" {
var it = RulesIterator.init("@-webkit-keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }");
const at = it.next() orelse return error.MissingRule;
try testing.expectEqualStrings("-webkit-keyframes", at.at_rule.keyword);
try testing.expectEqual(@as(?Rule, null), it.next());
}
@@ -467,17 +522,17 @@ test "RulesIterator: comments and whitespace" {
var it = RulesIterator.init(" /* comment */ .test /* comment */ { /* comment */ color: red; } \n\t");
const rule = it.next() orelse return error.MissingRule;
try testing.expectEqualStrings(".test", rule.selector);
try testing.expectEqualStrings(" /* comment */ color: red; ", rule.block);
try testing.expectEqualStrings(".test", rule.style.selector);
try testing.expectEqualStrings(" /* comment */ color: red; ", rule.style.block);
try testing.expectEqual(@as(?Rule, null), it.next());
}
test "RulesIterator: top-level semicolons" {
var it = RulesIterator.init("*{}; ; p{}");
var rule = it.next() orelse return error.MissingRule;
try testing.expectEqualStrings("*", rule.selector);
try testing.expectEqualStrings("*", rule.style.selector);
rule = it.next() orelse return error.MissingRule;
try testing.expectEqualStrings("p", rule.selector);
try testing.expectEqualStrings("p", rule.style.selector);
try testing.expectEqual(@as(?Rule, null), it.next());
}

View File

@@ -555,3 +555,126 @@
testing.expectTrue(cssText.includes('}'));
}
</script>
<script id="CSSStyleSheet_insertRule_keyframes_surfaces">
{
// Regression test for #2459: at-rules must surface in cssRules so that
// CSS-in-JS libraries (styled-components, emotion, ...) don't fall back
// to per-render <style> element injection. Before the fix insertRule
// silently swallowed at-rules and returned a fake index, leaving
// cssRules.length at 0.
const sheet = new CSSStyleSheet();
const idx = sheet.insertRule('@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }');
testing.expectEqual(0, idx);
testing.expectEqual(1, sheet.cssRules.length);
const rule = sheet.cssRules[0];
testing.expectEqual(CSSRule.KEYFRAMES_RULE, rule.type);
testing.expectTrue(rule.cssText.includes('@keyframes'));
testing.expectTrue(rule.cssText.includes('spin'));
}
</script>
<script id="CSSStyleSheet_insertRule_media_surfaces">
{
const sheet = new CSSStyleSheet();
const idx = sheet.insertRule('@media screen and (min-width: 600px) { .test { color: red; } }');
testing.expectEqual(0, idx);
testing.expectEqual(1, sheet.cssRules.length);
testing.expectEqual(CSSRule.MEDIA_RULE, sheet.cssRules[0].type);
}
</script>
<script id="CSSStyleSheet_insertRule_atrule_mixed_with_style">
{
// CSS-in-JS dedup paths read back cssRules.length and per-index types
// after insertion; both must be correct when style and at-rules are
// interleaved.
const sheet = new CSSStyleSheet();
sheet.insertRule('.first { color: red; }', 0);
sheet.insertRule('@keyframes spin { from {} to {} }', 1);
sheet.insertRule('.last { color: blue; }', 2);
testing.expectEqual(3, sheet.cssRules.length);
testing.expectEqual(CSSRule.STYLE_RULE, sheet.cssRules[0].type);
testing.expectEqual(CSSRule.KEYFRAMES_RULE, sheet.cssRules[1].type);
testing.expectEqual(CSSRule.STYLE_RULE, sheet.cssRules[2].type);
}
</script>
<script id="CSSStyleSheet_insertRule_vendor_prefixed_keyframes">
{
const sheet = new CSSStyleSheet();
sheet.insertRule('@-webkit-keyframes spin { from {} to {} }');
testing.expectEqual(1, sheet.cssRules.length);
testing.expectEqual(CSSRule.KEYFRAMES_RULE, sheet.cssRules[0].type);
}
</script>
<script id="CSSStyleSheet_insertRule_supports_surfaces">
{
const sheet = new CSSStyleSheet();
sheet.insertRule('@supports (display: grid) { .grid { display: grid; } }');
testing.expectEqual(1, sheet.cssRules.length);
testing.expectEqual(CSSRule.SUPPORTS_RULE, sheet.cssRules[0].type);
}
</script>
<script id="CSSStyleSheet_insertRule_font_face_surfaces">
{
const sheet = new CSSStyleSheet();
sheet.insertRule("@font-face { font-family: 'Test'; src: url('test.woff2'); }");
testing.expectEqual(1, sheet.cssRules.length);
testing.expectEqual(CSSRule.FONT_FACE_RULE, sheet.cssRules[0].type);
}
</script>
<script id="CSSStyleSheet_replaceSync_preserves_atrules">
{
// replaceSync goes through the same parser path; at-rules in the
// replacement source should also surface in cssRules.
const sheet = new CSSStyleSheet();
sheet.replaceSync('.a { color: red; } @keyframes spin { from {} to {} } .b { color: blue; }');
testing.expectEqual(3, sheet.cssRules.length);
testing.expectEqual(CSSRule.STYLE_RULE, sheet.cssRules[0].type);
testing.expectEqual(CSSRule.KEYFRAMES_RULE, sheet.cssRules[1].type);
testing.expectEqual(CSSRule.STYLE_RULE, sheet.cssRules[2].type);
}
</script>
<script id="CSSStyleSheet_insertRule_atrule_repeated_no_growth">
{
// The CSS-in-JS dedup pattern: insert a hash-keyed at-rule, read back
// cssRules to check whether it's already there, skip if so. Before the
// fix the read-back was always empty, so the library re-inserted every
// render -- and (separately) created a fresh <style> element each time.
// After the fix the read-back finds the rule and the library can dedup.
const sheet = new CSSStyleSheet();
sheet.insertRule('@keyframes spin { from {} to {} }');
testing.expectEqual(1, sheet.cssRules.length);
// Library checks cssRules and finds the rule -- doesn't re-insert.
const exists = Array.from(sheet.cssRules).some(
r => r.type === CSSRule.KEYFRAMES_RULE && r.cssText.includes('spin')
);
testing.expectTrue(exists);
}
</script>
<script id="CSSStyleSheet_insertRule_unknown_at_rule_typed_as_zero">
{
const sheet = new CSSStyleSheet();
sheet.insertRule('@layer utilities { .x { color: red; } }');
testing.expectEqual(1, sheet.cssRules.length);
testing.expectEqual(0, sheet.cssRules[0].type);
testing.expectTrue(sheet.cssRules[0].cssText.includes('@layer'));
}
</script>

View File

@@ -23,9 +23,16 @@ pub const Type = union(enum) {
font_feature_values: void,
viewport: void,
region_style: void,
unknown: void,
};
_type: Type,
/// Original source text for at-rules (`@keyframes`, `@media`, ...). Empty
/// for `.style` because `CSSStyleRule.getCssText` constructs its own
/// serialization from selector + declarations. The bridge dispatches the
/// most-derived `cssText` accessor (CSSStyleRule's), so this field only
/// surfaces for opaque at-rule placeholders. See lightpanda-io/browser#2459.
_text: []const u8 = "",
pub fn as(self: *CSSRule, comptime T: type) *T {
return self.is(T).?;
@@ -44,12 +51,26 @@ pub fn init(rule_type: Type, frame: *Frame) !*CSSRule {
});
}
/// Construct an at-rule placeholder with stored source text. Used when an
/// `@keyframes` / `@media` / etc. lands via `insertRule` or `replaceSync`
/// so that JS-side reads via `cssRules` see the rule (matching length and
/// `cssText`) without the CSS engine actually applying it.
pub fn initAtRule(rule_type: Type, text: []const u8, frame: *Frame) !*CSSRule {
return frame._factory.create(CSSRule{
._type = rule_type,
._text = try frame.dupeString(text),
});
}
pub fn getType(self: *const CSSRule) u16 {
if (self._type == .unknown) {
return 0;
}
return @as(u16, @intFromEnum(std.meta.activeTag(self._type))) + 1;
}
pub fn getCssText(_: *const CSSRule, _: *Frame) []const u8 {
return "";
pub fn getCssText(self: *const CSSRule, _: *Frame) []const u8 {
return self._text;
}
pub fn getParentRule(_: *const CSSRule) ?*CSSRule {
@@ -70,22 +91,27 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const STYLE_RULE = 1;
pub const CHARSET_RULE = 2;
pub const IMPORT_RULE = 3;
pub const MEDIA_RULE = 4;
pub const FONT_FACE_RULE = 5;
pub const PAGE_RULE = 6;
pub const KEYFRAMES_RULE = 7;
pub const KEYFRAME_RULE = 8;
pub const MARGIN_RULE = 9;
pub const NAMESPACE_RULE = 10;
pub const COUNTER_STYLE_RULE = 11;
pub const SUPPORTS_RULE = 12;
pub const DOCUMENT_RULE = 13;
pub const FONT_FEATURE_VALUES_RULE = 14;
pub const VIEWPORT_RULE = 15;
pub const REGION_STYLE_RULE = 16;
// Spec rule-type constants. Wrapped with `bridge.property(.template)` so
// they're exposed on the JS-side `CSSRule` constructor (e.g.
// `CSSRule.KEYFRAMES_RULE === 7`). Without the wrapping these are plain
// Zig declarations that never reach JS, and code reading them gets
// `undefined`.
pub const STYLE_RULE = bridge.property(1, .{ .template = true });
pub const CHARSET_RULE = bridge.property(2, .{ .template = true });
pub const IMPORT_RULE = bridge.property(3, .{ .template = true });
pub const MEDIA_RULE = bridge.property(4, .{ .template = true });
pub const FONT_FACE_RULE = bridge.property(5, .{ .template = true });
pub const PAGE_RULE = bridge.property(6, .{ .template = true });
pub const KEYFRAMES_RULE = bridge.property(7, .{ .template = true });
pub const KEYFRAME_RULE = bridge.property(8, .{ .template = true });
pub const MARGIN_RULE = bridge.property(9, .{ .template = true });
pub const NAMESPACE_RULE = bridge.property(10, .{ .template = true });
pub const COUNTER_STYLE_RULE = bridge.property(11, .{ .template = true });
pub const SUPPORTS_RULE = bridge.property(12, .{ .template = true });
pub const DOCUMENT_RULE = bridge.property(13, .{ .template = true });
pub const FONT_FEATURE_VALUES_RULE = bridge.property(14, .{ .template = true });
pub const VIEWPORT_RULE = bridge.property(15, .{ .template = true });
pub const REGION_STYLE_RULE = bridge.property(16, .{ .template = true });
pub const @"type" = bridge.accessor(CSSRule.getType, null, .{});
pub const cssText = bridge.accessor(CSSRule.getCssText, null, .{});

View File

@@ -81,26 +81,26 @@ pub fn getOwnerRule(self: *const CSSStyleSheet) ?*CSSRule {
pub fn insertRule(self: *CSSStyleSheet, rule: []const u8, maybe_index: ?u32, frame: *Frame) !u32 {
const requested_index = maybe_index orelse 0;
var it = Parser.parseStylesheet(rule);
const parsed_rule = it.next() orelse {
if (it.has_skipped_at_rule) {
log.debug(.not_implemented, "CSSStyleSheet.insertRule", .{});
// Lightpanda currently skips at-rules (e.g., @keyframes, @media) in its
// CSS parser. To prevent JS apps (like Expo/Reanimated) from crashing
// during initialization, we simulate a successful insertion by returning
// the requested index.
return requested_index;
}
return error.SyntaxError;
};
const parsed_rule = it.next() orelse return error.SyntaxError;
if (it.next() != null) return error.SyntaxError;
const style_rule = try CSSStyleRule.init(frame);
try style_rule.setSelectorText(parsed_rule.selector, frame);
const inserted: *CSSRule = switch (parsed_rule) {
.style => |s| blk: {
const style_rule = try CSSStyleRule.init(frame);
try style_rule.setSelectorText(s.selector, frame);
const style_props = try style_rule.getStyle(frame);
const style = style_props.asCSSStyleDeclaration();
try style.setCssText(parsed_rule.block, frame);
const style_props = try style_rule.getStyle(frame);
const style = style_props.asCSSStyleDeclaration();
try style.setCssText(s.block, frame);
break :blk style_rule._proto;
},
// Opaque placeholder for at-rules. The CSS engine doesn't apply
// these (`@keyframes`, `@media`, ...) but JS-side reads must see
// them via `cssRules` so CSS-in-JS libraries don't fall back to
// per-render `<style>` injection. See #2459.
.at_rule => |a| try CSSRule.initAtRule(atRuleTypeFor(a.keyword), a.text, frame),
};
const rules = try self.getCssRules(frame);
@@ -113,7 +113,7 @@ pub fn insertRule(self: *CSSStyleSheet, rule: []const u8, maybe_index: ?u32, fra
if (index != requested_index) {
log.debug(.not_implemented, "insertRule clamped index", .{});
}
try rules.insert(index, style_rule._proto, frame);
try rules.insert(index, inserted, frame);
// Notify StyleManager that rules have changed
frame._style_manager.sheetModified();
@@ -121,6 +121,35 @@ pub fn insertRule(self: *CSSStyleSheet, rule: []const u8, maybe_index: ?u32, fra
return index;
}
/// Map an at-rule keyword (without `@`) to the matching `CSSRule.Type`
/// variant. Vendor prefixes are stripped before matching
/// (`-webkit-keyframes` -> `keyframes`). Unrecognized keywords fall back
/// to `.unknown`: which maps to 0 for rule.type.
fn atRuleTypeFor(keyword_with_prefix: []const u8) CSSRule.Type {
var keyword = keyword_with_prefix;
inline for (.{ "-webkit-", "-moz-", "-ms-", "-o-" }) |prefix| {
if (std.ascii.startsWithIgnoreCase(keyword, prefix)) {
keyword = keyword[prefix.len..];
break;
}
}
const eql = std.ascii.eqlIgnoreCase;
if (eql(keyword, "media")) return .media;
if (eql(keyword, "keyframes")) return .keyframes;
if (eql(keyword, "supports")) return .supports;
if (eql(keyword, "font-face")) return .font_face;
if (eql(keyword, "import")) return .import;
if (eql(keyword, "charset")) return .charset;
if (eql(keyword, "namespace")) return .namespace;
if (eql(keyword, "page")) return .frame;
if (eql(keyword, "counter-style")) return .counter_style;
if (eql(keyword, "font-feature-values")) return .font_feature_values;
if (eql(keyword, "viewport")) return .viewport;
if (eql(keyword, "document")) return .document;
return .unknown;
}
pub fn deleteRule(self: *CSSStyleSheet, index: u32, frame: *Frame) !void {
const rules = try self.getCssRules(frame);
try rules.remove(index);
@@ -141,14 +170,20 @@ pub fn replaceSync(self: *CSSStyleSheet, text: []const u8, frame: *Frame) CSSErr
var it = Parser.parseStylesheet(text);
var index: u32 = 0;
while (it.next()) |parsed_rule| {
const style_rule = try CSSStyleRule.init(frame);
try style_rule.setSelectorText(parsed_rule.selector, frame);
const inserted: *CSSRule = switch (parsed_rule) {
.style => |s| blk: {
const style_rule = try CSSStyleRule.init(frame);
try style_rule.setSelectorText(s.selector, frame);
const style_props = try style_rule.getStyle(frame);
const style = style_props.asCSSStyleDeclaration();
try style.setCssText(parsed_rule.block, frame);
const style_props = try style_rule.getStyle(frame);
const style = style_props.asCSSStyleDeclaration();
try style.setCssText(s.block, frame);
break :blk style_rule._proto;
},
.at_rule => |a| try CSSRule.initAtRule(atRuleTypeFor(a.keyword), a.text, frame),
};
try rules.insert(index, style_rule._proto, frame);
try rules.insert(index, inserted, frame);
index += 1;
}

View File

@@ -55,7 +55,7 @@ pub fn processMessage(cmd: *CDP.Command) !void {
switch (action) {
.enable => return enable(cmd),
.disable => return disable(cmd),
.setCacheDisabled => return cmd.sendResult(null, .{}),
.setCacheDisabled => return setCacheDisabled(cmd),
.setUserAgentOverride => return @import("emulation.zig").setUserAgentOverride(cmd),
.setExtraHTTPHeaders => return setExtraHTTPHeaders(cmd),
.deleteCookies => return deleteCookies(cmd),
@@ -82,6 +82,17 @@ fn disable(cmd: *CDP.Command) !void {
return cmd.sendResult(null, .{});
}
fn setCacheDisabled(cmd: *CDP.Command) !void {
const params = (try cmd.params(struct {
cacheDisabled: bool,
})) orelse return error.InvalidParams;
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const client = &bc.cdp.browser.http_client;
client.cache_layer.disabled = params.cacheDisabled;
return cmd.sendResult(null, .{});
}
fn setExtraHTTPHeaders(cmd: *CDP.Command) !void {
const params = (try cmd.params(struct {
headers: std.json.ArrayHashMap([]const u8),
@@ -734,3 +745,42 @@ test "cdp.Network: canClearBrowserCache" {
// Cache is disabled in standard tests for now.
try ctx.expectSentResult(.{ .result = false }, .{ .id = 1 });
}
test "cdp.Network: setCacheDisabled disables cache" {
var ctx = try testing.context();
defer ctx.deinit();
_ = try ctx.loadBrowserContext(.{ .id = "BID-CD1" });
try ctx.processMessage(.{
.id = 1,
.method = "Network.setCacheDisabled",
.params = .{ .cacheDisabled = true },
});
try ctx.expectSentResult(null, .{ .id = 1 });
const client = ctx.cdp().browser.http_client;
try testing.expectEqual(true, client.cache_layer.disabled);
}
test "cdp.Network: setCacheDisabled re-enables cache" {
var ctx = try testing.context();
defer ctx.deinit();
_ = try ctx.loadBrowserContext(.{ .id = "BID-CD2" });
try ctx.processMessage(.{
.id = 1,
.method = "Network.setCacheDisabled",
.params = .{ .cacheDisabled = true },
});
try ctx.expectSentResult(null, .{ .id = 1 });
try ctx.processMessage(.{
.id = 2,
.method = "Network.setCacheDisabled",
.params = .{ .cacheDisabled = false },
});
try ctx.expectSentResult(null, .{ .id = 2 });
const client = ctx.cdp().browser.http_client;
try testing.expectEqual(false, client.cache_layer.disabled);
}

View File

@@ -85,52 +85,55 @@ pub fn fetch(app: *App, browser: *Browser, url: [:0]const u8, opts: FetchOpts) !
// Stash scripts user want to inject.
session.inject_scripts = opts.inject_script.items;
const frame = try session.createPage();
{
const frame = try session.createPage();
// frame isn't safe to use after navigate, it can be swapped out
// // Comment this out to get a profile of the JS code in v8/profile.json.
// // You can open this in Chrome's profiler.
// // I've seen it generate invalid JSON, but I'm not sure why. It
// // happens rarely, and I manually fix the file.
// frame.js.startCpuProfiler();
// defer {
// if (frame.js.stopCpuProfiler()) |profile| {
// std.fs.cwd().writeFile(.{
// .sub_path = ".lp-cache/cpu_profile.json",
// .data = profile,
// }) catch |err| {
// log.err(.app, "profile write error", .{ .err = err });
// };
// } else |err| {
// log.err(.app, "profile error", .{ .err = err });
// }
// }
// // Comment this out to get a profile of the JS code in v8/profile.json.
// // You can open this in Chrome's profiler.
// // I've seen it generate invalid JSON, but I'm not sure why. It
// // happens rarely, and I manually fix the file.
// frame.js.startCpuProfiler();
// defer {
// if (frame.js.stopCpuProfiler()) |profile| {
// std.fs.cwd().writeFile(.{
// .sub_path = ".lp-cache/cpu_profile.json",
// .data = profile,
// }) catch |err| {
// log.err(.app, "profile write error", .{ .err = err });
// };
// } else |err| {
// log.err(.app, "profile error", .{ .err = err });
// }
// }
// // Comment this out to get a heap V8 heap profil
// frame.js.startHeapProfiler();
// defer {
// if (frame.js.stopHeapProfiler()) |profile| {
// std.fs.cwd().writeFile(.{
// .sub_path = ".lp-cache/allocating.heapprofile",
// .data = profile.@"0",
// }) catch |err| {
// log.err(.app, "allocating write error", .{ .err = err });
// };
// std.fs.cwd().writeFile(.{
// .sub_path = ".lp-cache/snapshot.heapsnapshot",
// .data = profile.@"1",
// }) catch |err| {
// log.err(.app, "heapsnapshot write error", .{ .err = err });
// };
// } else |err| {
// log.err(.app, "profile error", .{ .err = err });
// }
// }
// // Comment this out to get a heap V8 heap profil
// frame.js.startHeapProfiler();
// defer {
// if (frame.js.stopHeapProfiler()) |profile| {
// std.fs.cwd().writeFile(.{
// .sub_path = ".lp-cache/allocating.heapprofile",
// .data = profile.@"0",
// }) catch |err| {
// log.err(.app, "allocating write error", .{ .err = err });
// };
// std.fs.cwd().writeFile(.{
// .sub_path = ".lp-cache/snapshot.heapsnapshot",
// .data = profile.@"1",
// }) catch |err| {
// log.err(.app, "heapsnapshot write error", .{ .err = err });
// };
// } else |err| {
// log.err(.app, "profile error", .{ .err = err });
// }
// }
const encoded_url = try URL.ensureEncoded(frame.call_arena, url, "UTF-8");
_ = try frame.navigate(encoded_url, .{
.reason = .address_bar,
.kind = .{ .push = null },
});
const encoded_url = try URL.ensureEncoded(frame.call_arena, url, "UTF-8");
_ = try frame.navigate(encoded_url, .{
.reason = .address_bar,
.kind = .{ .push = null },
});
}
var runner = try session.runner(.{});
var timer = try std.time.Timer.start();
@@ -159,7 +162,11 @@ pub fn fetch(app: *App, browser: *Browser, url: [:0]const u8, opts: FetchOpts) !
}
const writer = opts.writer orelse return;
if (opts.dump_mode) |mode| {
if (opts.dump_mode) |mode| blk: {
const frame = session.currentFrame() orelse {
try writer.writeAll("Frame closed. Please open a bug report including the URL\n");
break :blk;
};
switch (mode) {
.html => try dump.root(frame.window._document, opts.dump, writer, frame),
.markdown => try markdown.dump(frame.window._document.asNode(), .{}, writer, frame),

View File

@@ -36,6 +36,7 @@ const log = lp.log;
const CacheLayer = @This();
next: Layer = undefined,
disabled: bool = false,
pub fn layer(self: *CacheLayer) Layer {
return .{
@@ -50,7 +51,7 @@ fn request(ptr: *anyopaque, transfer: *Transfer) anyerror!void {
const self: *CacheLayer = @ptrCast(@alignCast(ptr));
const req = &transfer.req;
if (req.params.method != .GET) {
if (self.disabled or req.params.method != .GET) {
return self.next.request(transfer);
}