1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
//! Per-page console / page-error retention backing
//! `page.consoleMessages()` / `page.pageErrors()` (Playwright parity).
//!
//! Buffers live on the backend page (like the frame cache) so every
//! `crate::Page` wrapper minted over the same backend page sees one
//! history. The `seed_frame_cache` listener task pushes entries as
//! events arrive; a main-frame navigation records a watermark so the
//! default `since-navigation` filter can slice without copying history.
use crate::console_message::ConsoleMessage;
use crate::web_error::WebError;
/// Retention cap per buffer, matching Playwright's `ensureArrayLimit`
/// guard against unbounded growth on chatty pages.
const MAX_ENTRIES: usize = 200;
/// Filter for [`crate::Page::console_messages`] / [`crate::Page::page_errors`].
/// Playwright: `{ filter?: 'all' | 'since-navigation' }`, defaulting to
/// `since-navigation`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ObservedFilter {
/// Everything retained since page creation (up to the cap).
All,
/// Only entries recorded after the last main-frame navigation.
#[default]
SinceNavigation,
}
impl ObservedFilter {
/// Parse the Playwright wire string. Unknown values fall back to the
/// default (`since-navigation`), mirroring the server's
/// `filter === 'all'` check.
#[must_use]
pub fn parse(s: Option<&str>) -> Self {
match s {
Some("all") => Self::All,
_ => Self::SinceNavigation,
}
}
}
struct Buffer<T> {
entries: Vec<T>,
nav_mark: usize,
}
impl<T> Default for Buffer<T> {
fn default() -> Self {
Self {
entries: Vec::new(),
nav_mark: 0,
}
}
}
impl<T: Clone> Buffer<T> {
fn push(&mut self, entry: T) {
self.entries.push(entry);
if self.entries.len() > MAX_ENTRIES {
let overflow = self.entries.len() - MAX_ENTRIES;
self.entries.drain(..overflow);
self.nav_mark = self.nav_mark.saturating_sub(overflow);
}
}
fn mark_navigation(&mut self) {
self.nav_mark = self.entries.len();
}
fn raise_nav_mark(&mut self, floor: usize) {
self.nav_mark = self.nav_mark.max(floor.min(self.entries.len()));
}
fn snapshot(&self, filter: ObservedFilter) -> Vec<T> {
match filter {
ObservedFilter::All => self.entries.clone(),
ObservedFilter::SinceNavigation => self.entries[self.nav_mark.min(self.entries.len())..].to_vec(),
}
}
fn clear(&mut self) {
self.entries.clear();
self.nav_mark = 0;
}
}
/// Console + page-error history for one backend page.
#[derive(Default)]
pub(crate) struct ObservedBuffers {
console: Buffer<ConsoleMessage>,
errors: Buffer<WebError>,
}
impl ObservedBuffers {
pub(crate) fn push_console(&mut self, msg: ConsoleMessage) {
self.console.push(msg);
}
pub(crate) fn push_error(&mut self, err: WebError) {
self.errors.push(err);
}
pub(crate) fn mark_navigation(&mut self) {
self.console.mark_navigation();
self.errors.mark_navigation();
}
/// Buffer lengths at a point in time — captured by the API navigation
/// path BEFORE issuing the backend nav, then passed to
/// [`Self::raise_nav_marks`] on success.
pub(crate) fn lens(&self) -> (usize, usize) {
(self.console.entries.len(), self.errors.entries.len())
}
/// Raise the since-navigation watermarks to at least the pre-nav
/// buffer lengths. The event-driven [`Self::mark_navigation`] (from
/// the page-event listener's `FrameNavigated`) is the primary path,
/// but broadcast delivery can lag or drop under load — an API-driven
/// navigation that returned successfully is PROOF a new document
/// committed, so entries recorded before the call must leave the
/// window. `max` semantics keep this idempotent with the event mark
/// and never evict entries pushed after the commit (e.g. the new
/// document's inline-script logs).
pub(crate) fn raise_nav_marks(&mut self, pre_nav: (usize, usize)) {
self.console.raise_nav_mark(pre_nav.0);
self.errors.raise_nav_mark(pre_nav.1);
}
pub(crate) fn console_messages(&self, filter: ObservedFilter) -> Vec<ConsoleMessage> {
self.console.snapshot(filter)
}
pub(crate) fn page_errors(&self, filter: ObservedFilter) -> Vec<WebError> {
self.errors.snapshot(filter)
}
pub(crate) fn clear_console(&mut self) {
self.console.clear();
}
pub(crate) fn clear_errors(&mut self) {
self.errors.clear();
}
}