///|
/// Page methods
///|
/// Navigate to URL
/// See: https://playwright.dev/docs/api/class-page#page-goto
pub async fn Page::goto(
self : Page,
url : String,
timeout? : Int,
wait_until? : String,
referer? : String,
) -> Response? {
consume(referer)
let state = self.state.val
let result = state.browser.state.val.client.browsing_context_navigate(
state.context_id,
url,
wait=wait_until.unwrap_or("complete"),
)
match result {
Ok(nav) => {
state.url = nav.url
// Try to find the matching network response (collected by event handler)
let timeout_ms = timeout.unwrap_or(5000)
match self.wait_for_response(
url,
timeout=timeout_ms,
stage="completed",
) {
Some(resp) => Some(resp)
None =>
// Fallback: check response_completed_events directly
match self.take_response_completed_event(url, "starts_with", None) {
Some(resp) => Some(resp)
None =>
// Last resort: check response_events (started)
match self.take_response_event(url, "starts_with", None) {
Some(resp) => Some(resp)
None => Some(make_response(nav.url))
}
}
}
}
Err(_) => None
}
}
///|
/// Reload page
pub async fn Page::reload(
self : Page,
timeout? : Int,
wait_until? : String,
) -> Response? {
consume((timeout, wait_until))
let state = self.state.val
let _ = state.browser.state.val.client.browsing_context_reload(
state.context_id,
)
Some(make_response(state.url))
}
///|
/// Go back in history
pub async fn Page::go_back(
self : Page,
timeout? : Int,
wait_until? : String,
) -> Response? {
consume((timeout, wait_until))
let _ = self.evaluate("history.back()")
None
}
///|
/// Go forward in history
pub async fn Page::go_forward(
self : Page,
timeout? : Int,
wait_until? : String,
) -> Response? {
consume((timeout, wait_until))
let _ = self.evaluate("history.forward()")
None
}
///|
/// Get page URL
pub fn Page::url(self : Page) -> String {
self.state.val.url
}
///|
/// Get page title
pub async fn Page::title(self : Page) -> String {
match self.eval_string("document.title") {
Some(value) => value
None => ""
}
}
///|
/// Get page content (HTML)
pub async fn Page::content(self : Page) -> String {
match self.eval_string("document.documentElement.outerHTML") {
Some(value) => value
None => ""
}
}
///|
/// Set page content
pub async fn Page::set_content(
self : Page,
html : String,
timeout? : Int,
wait_until? : String,
) -> Unit {
consume((timeout, wait_until))
let expr = "(html) => { document.open(); document.write(html); document.close(); }"
let args : Array[Json] = [
Json::object({ "type": Json::string("string"), "value": Json::string(html) }),
]
let _ = self.call_function(expr, args)
}
///|
/// Get locator for CSS selector or text
pub fn Page::locator(self : Page, selector : String) -> Locator {
let state : LocatorState = {
page: self,
selector,
index: None,
has_text: None,
has_text_exact: false,
has_not_text: None,
label_text: None,
label_text_exact: false,
label_scope_selector: None,
}
{ state: { val: state } }
}
///|
/// Get locator by role
pub fn Page::get_by_role(
self : Page,
role : String,
name? : String,
exact? : Bool,
) -> Locator {
consume(exact)
let selector = "[role=" + js_string_literal(role) + "]"
let base = self.locator(selector)
match name {
Some(text) => base.get_by_text(text, exact?)
None => base
}
}
///|
/// Get locator by text
pub fn Page::get_by_text(self : Page, text : String, exact? : Bool) -> Locator {
let exact_flag = match exact {
Some(true) => true
_ => false
}
let state : LocatorState = {
page: self,
selector: "*",
index: None,
has_text: Some(text),
has_text_exact: exact_flag,
has_not_text: None,
label_text: None,
label_text_exact: false,
label_scope_selector: None,
}
{ state: { val: state } }
}
///|
/// Get locator by label
pub fn Page::get_by_label(self : Page, text : String, exact? : Bool) -> Locator {
let exact_flag = match exact {
Some(true) => true
_ => false
}
let state : LocatorState = {
page: self,
selector: "label",
index: None,
has_text: None,
has_text_exact: false,
has_not_text: None,
label_text: Some(text),
label_text_exact: exact_flag,
label_scope_selector: None,
}
{ state: { val: state } }
}
///|
/// Get locator by placeholder
pub fn Page::get_by_placeholder(
self : Page,
text : String,
exact? : Bool,
) -> Locator {
consume(exact)
let selector = "[placeholder=" + js_string_literal(text) + "]"
let state : LocatorState = {
page: self,
selector,
index: None,
has_text: None,
has_text_exact: false,
has_not_text: None,
label_text: None,
label_text_exact: false,
label_scope_selector: None,
}
{ state: { val: state } }
}
///|
/// Get locator by alt text
pub fn Page::get_by_alt_text(
self : Page,
text : String,
exact? : Bool,
) -> Locator {
consume(exact)
let selector = "[alt=" + js_string_literal(text) + "]"
let state : LocatorState = {
page: self,
selector,
index: None,
has_text: None,
has_text_exact: false,
has_not_text: None,
label_text: None,
label_text_exact: false,
label_scope_selector: None,
}
{ state: { val: state } }
}
///|
/// Get locator by title
pub fn Page::get_by_title(self : Page, text : String, exact? : Bool) -> Locator {
consume(exact)
let selector = "[title=" + js_string_literal(text) + "]"
let state : LocatorState = {
page: self,
selector,
index: None,
has_text: None,
has_text_exact: false,
has_not_text: None,
label_text: None,
label_text_exact: false,
label_scope_selector: None,
}
{ state: { val: state } }
}
///|
/// Get locator by test ID
pub fn Page::get_by_test_id(self : Page, testId : String) -> Locator {
let selector = "[data-testid=" + js_string_literal(testId) + "]"
let state : LocatorState = {
page: self,
selector,
index: None,
has_text: None,
has_text_exact: false,
has_not_text: None,
label_text: None,
label_text_exact: false,
label_scope_selector: None,
}
{ state: { val: state } }
}
///|
/// Subscribe network event
pub fn Page::on_network_event(
self : Page,
handler : (NetworkEvent) -> Unit,
) -> Unit {
self.state.val.network_listeners.push(handler)
}
///|
/// Subscribe request event
pub fn Page::on_request(self : Page, handler : (Request) -> Unit) -> Unit {
self.on_network_event(fn(evt : NetworkEvent) {
match evt {
NetworkEvent::Request(req) => handler(req)
_ => ()
}
})
}
///|
/// Subscribe response event
pub fn Page::on_response(self : Page, handler : (Response) -> Unit) -> Unit {
self.on_network_event(fn(evt : NetworkEvent) {
match evt {
NetworkEvent::Response(resp) => handler(resp)
_ => ()
}
})
}
///|
/// Subscribe response completed event
pub fn Page::on_response_completed(
self : Page,
handler : (Response) -> Unit,
) -> Unit {
self.on_network_event(fn(evt : NetworkEvent) {
match evt {
NetworkEvent::ResponseCompleted(resp) => handler(resp)
_ => ()
}
})
}
///|
/// Click on element
pub async fn Page::click(
self : Page,
selector : String,
button? : String,
click_count? : Int,
delay? : Int,
force? : Bool,
modifiers? : Array[String],
no_wait_after? : Bool,
position? : (Double, Double),
timeout? : Int,
trial? : Bool,
) -> Unit {
consume(
(
button, click_count, delay, force, modifiers, no_wait_after, position, timeout,
trial,
),
)
let locator = self.locator(selector)
locator.click()
}
///|
/// Double click on element
pub async fn Page::dblclick(
self : Page,
selector : String,
timeout? : Int,
force? : Bool,
) -> Unit {
consume((timeout, force))
let locator = self.locator(selector)
locator.dblclick()
}
///|
/// Fill input
pub async fn Page::fill(
self : Page,
selector : String,
value : String,
force? : Bool,
no_wait_after? : Bool,
timeout? : Int,
) -> Unit {
consume((force, no_wait_after, timeout))
let locator = self.locator(selector)
locator.fill(value)
}
///|
/// Type text (keystroke by keystroke)
pub async fn Page::type_(
self : Page,
selector : String,
text : String,
delay? : Int,
no_wait_after? : Bool,
timeout? : Int,
) -> Unit {
consume((delay, no_wait_after, timeout))
let locator = self.locator(selector)
locator.type_(text)
}
///|
/// Press key
pub async fn Page::press(
self : Page,
selector : String,
key : String,
delay? : Int,
no_wait_after? : Bool,
timeout? : Int,
) -> Unit {
consume((delay, no_wait_after, timeout))
let locator = self.locator(selector)
locator.press(key)
}
///|
/// Check checkbox
pub async fn Page::check(
self : Page,
selector : String,
force? : Bool,
no_wait_after? : Bool,
timeout? : Int,
) -> Unit {
consume((force, no_wait_after, timeout))
let locator = self.locator(selector)
locator.check()
}
///|
/// Uncheck checkbox
pub async fn Page::uncheck(
self : Page,
selector : String,
force? : Bool,
no_wait_after? : Bool,
timeout? : Int,
) -> Unit {
consume((force, no_wait_after, timeout))
let locator = self.locator(selector)
locator.uncheck()
}
///|
/// Select option(s) in dropdown
pub async fn Page::select_option(
self : Page,
selector : String,
values : Array[SelectOption],
force? : Bool,
no_wait_after? : Bool,
timeout? : Int,
) -> Array[String] {
consume((self, selector, values, force, no_wait_after, timeout))
async_noop()
[]
}
///|
/// Hover over element
pub async fn Page::hover(
self : Page,
selector : String,
force? : Bool,
modifiers? : Array[String],
position? : (Double, Double),
timeout? : Int,
trial? : Bool,
) -> Unit {
consume((force, modifiers, position, timeout, trial))
let locator = self.locator(selector)
locator.hover()
}
///|
/// Focus element
pub async fn Page::focus(
self : Page,
selector : String,
timeout? : Int,
) -> Unit {
consume(timeout)
let locator = self.locator(selector)
locator.focus()
}
///|
/// Wait for selector
pub async fn Page::wait_for_selector(
self : Page,
selector : String,
state? : String,
timeout? : Int,
strict? : Bool,
) -> ElementHandle? {
let strict_mode = match strict {
Some(true) => true
_ => false
}
let desired = match state {
Some("attached") => "attached"
Some("detached") => "detached"
Some("hidden") => "hidden"
Some("visible") => "visible"
_ => "visible"
}
let timeout_ms = match timeout {
Some(value) => value
None => 30_000
}
let start = @async.now()
let result = for attempt = 0 {
let nodes = self.locate_nodes(selector)
let found = nodes.length() > 0
if strict_mode && nodes.length() > 1 {
raise PlaywrightError::StrictModeViolation(
selector~,
count=nodes.length(),
)
}
match desired {
"attached" =>
if found {
let handle = JSHandle::from_page(self, nodes[0])
break handle.as_element()
}
"detached" => if not(found) { break None }
"hidden" =>
if not(found) {
break None
} else {
match self.is_node_visible(nodes[0]) {
Some(true) => ()
Some(false) => break None
None => ()
}
}
"visible" =>
if found {
match self.is_node_visible(nodes[0]) {
Some(true) => {
let handle = JSHandle::from_page(self, nodes[0])
break handle.as_element()
}
_ => ()
}
}
_ => ()
}
if timeout_ms <= 0 {
break None
}
let elapsed = @async.now() - start
if elapsed >= timeout_ms.to_int64() {
break None
}
let remaining = timeout_ms - elapsed.to_int()
let base = 10
let cap = 200
let step = if attempt > 6 { 6 } else { attempt }
let delay = base * (1 << step)
let wait = if delay > cap { cap } else { delay }
let wait_ms = if wait > remaining { remaining } else { wait }
sleep_ms(wait_ms)
continue attempt + 1
}
result
}
///|
/// Wait for load state
pub async fn Page::wait_for_load_state(
self : Page,
state? : String,
timeout? : Int,
) -> Unit {
let desired = match state {
Some("domcontentloaded") => "domcontentloaded"
Some("load") => "load"
Some("networkidle") => "load"
Some(value) => value
None => "load"
}
match self.eval_string("document.readyState") {
Some("complete") =>
if desired == "load" || desired == "networkidle" {
return
}
Some("interactive") => if desired == "domcontentloaded" { return }
_ => ()
}
match timeout {
Some(value) => {
let _ = self.wait_for_navigation(timeout=value, wait_until=desired)
}
None => {
let _ = self.wait_for_navigation(wait_until=desired)
}
}
}
///|
/// Wait for navigation
pub async fn Page::wait_for_navigation(
self : Page,
timeout? : Int,
url? : String,
wait_until? : String,
) -> Response? {
let wait_method = match wait_until {
Some("domcontentloaded") => "browsingContext.domContentLoaded"
Some("load") => "browsingContext.load"
Some("networkidle") => "browsingContext.load"
Some(value) => value
None => "browsingContext.load"
}
let timeout_ms = match timeout {
Some(value) => value
None => 30_000
}
let start = @async.now()
let result = for attempt = 0 {
match self.take_navigation_event(Some(wait_method), url) {
Some(evt) => {
self.state.val.url = evt.url
break Some(make_response(evt.url))
}
None => ()
}
match url {
Some(target) =>
if self.state.val.url == target {
break Some(make_response(target))
}
None => ()
}
if timeout_ms <= 0 {
break None
}
let elapsed = @async.now() - start
if elapsed >= timeout_ms.to_int64() {
break None
}
let remaining = timeout_ms - elapsed.to_int()
let base = 10
let cap = 200
let step = if attempt > 6 { 6 } else { attempt }
let delay = base * (1 << step)
let wait = if delay > cap { cap } else { delay }
let wait_ms = if wait > remaining { remaining } else { wait }
sleep_ms(wait_ms)
continue attempt + 1
}
result
}
///|
/// Wait for URL
pub async fn Page::wait_for_url(
self : Page,
url : String,
timeout? : Int,
wait_until? : String,
) -> Unit {
consume(wait_until)
if self.state.val.url == url {
return
}
let timeout_ms = match timeout {
Some(value) => value
None => 30_000
}
let start = @async.now()
let _ = for attempt = 0 {
match self.take_navigation_event(None, Some(url)) {
Some(evt) => {
self.state.val.url = evt.url
break ()
}
None => ()
}
if self.state.val.url == url {
break ()
}
if timeout_ms <= 0 {
break ()
}
let elapsed = @async.now() - start
if elapsed >= timeout_ms.to_int64() {
break ()
}
let remaining = timeout_ms - elapsed.to_int()
let base = 10
let cap = 200
let step = if attempt > 6 { 6 } else { attempt }
let delay = base * (1 << step)
let wait = if delay > cap { cap } else { delay }
let wait_ms = if wait > remaining { remaining } else { wait }
sleep_ms(wait_ms)
continue attempt + 1
}
}
///|
/// Wait for network request
pub async fn Page::wait_for_request(
self : Page,
url : String,
timeout? : Int,
match_mode? : String,
predicate? : (Request) -> Bool,
) -> Request? {
let match_mode = resolve_match_mode(url, match_mode)
let timeout_ms = match timeout {
Some(value) => value
None => 30_000
}
let start = @async.now()
let result = for attempt = 0 {
match self.take_request_event(url, match_mode, predicate) {
Some(req) => break Some(req)
None => ()
}
if timeout_ms <= 0 {
break None
}
let elapsed = @async.now() - start
if elapsed >= timeout_ms.to_int64() {
break None
}
let remaining = timeout_ms - elapsed.to_int()
let base = 10
let cap = 200
let step = if attempt > 6 { 6 } else { attempt }
let delay = base * (1 << step)
let wait = if delay > cap { cap } else { delay }
let wait_ms = if wait > remaining { remaining } else { wait }
sleep_ms(wait_ms)
continue attempt + 1
}
result
}
///|
/// Wait for network response
pub async fn Page::wait_for_response(
self : Page,
url : String,
timeout? : Int,
match_mode? : String,
stage? : String,
predicate? : (Response) -> Bool,
) -> Response? {
let match_mode = resolve_match_mode(url, match_mode)
let resolved_stage = resolve_response_stage(stage)
let timeout_ms = match timeout {
Some(value) => value
None => 30_000
}
let start = @async.now()
let result = for attempt = 0 {
let matched = if resolved_stage == "completed" {
self.take_response_completed_event(url, match_mode, predicate)
} else {
self.take_response_event(url, match_mode, predicate)
}
match matched {
Some(resp) => break Some(resp)
None => ()
}
if timeout_ms <= 0 {
break None
}
let elapsed = @async.now() - start
if elapsed >= timeout_ms.to_int64() {
break None
}
let remaining = timeout_ms - elapsed.to_int()
let base = 10
let cap = 200
let step = if attempt > 6 { 6 } else { attempt }
let delay = base * (1 << step)
let wait = if delay > cap { cap } else { delay }
let wait_ms = if wait > remaining { remaining } else { wait }
sleep_ms(wait_ms)
continue attempt + 1
}
result
}
///|
/// Wait for timeout
pub async fn Page::wait_for_timeout(self : Page, timeout : Int) -> Unit {
consume(self)
sleep_ms(timeout)
}
///|
/// Wait for function to return truthy value
pub async fn Page::wait_for_function(
self : Page,
page_function : String,
arg? : Json,
polling? : Int,
timeout? : Int,
) -> Json {
consume((polling, timeout))
match arg {
Some(value) => self.evaluate(page_function, arg=value)
None => self.evaluate(page_function)
}
}
///|
/// Evaluate JavaScript in page context.
/// If page_function looks like a function (starts with "function", "async", "(" or "=>"),
/// uses script.callFunction for proper invocation. Otherwise uses script.evaluate.
pub async fn Page::evaluate(
self : Page,
page_function : String,
arg? : Json,
) -> Json {
let state = self.state.val
let client = state.browser.state.val.client
let ctx = state.context_id
let result = if is_function_expression(page_function) {
// Use callFunction for arrow functions, function expressions, etc.
let args : Array[Json] = match arg {
Some(a) => [a]
None => []
}
client.script_call_function(page_function, ctx, arguments=args)
} else {
client.script_evaluate(page_function, ctx)
}
match result {
Ok(json) =>
match json_get_object(json, "result") {
Some(value) => remote_value_to_json(value)
None => Json::null()
}
Err(_) => Json::null()
}
}
///|
/// Evaluate JavaScript and return handle
pub async fn Page::evaluate_handle(
self : Page,
page_function : String,
arg? : Json,
) -> JSHandle {
consume(arg)
let state = self.state.val
let result = state.browser.state.val.client.script_evaluate(
page_function,
state.context_id,
)
match result {
Ok(json) =>
match json_get_object(json, "result") {
Some(value) => JSHandle::from_page(self, value)
None => JSHandle::from_page(self, Json::null())
}
Err(_) => JSHandle::from_page(self, Json::null())
}
}
///|
/// Take screenshot
pub async fn Page::screenshot(
self : Page,
path? : String,
type_? : String,
quality? : Int,
full_page? : Bool,
clip? : Clip,
omit_background? : Bool,
timeout? : Int,
scale? : String,
) -> ResponseBody {
consume(
(
self, path, type_, quality, full_page, clip, omit_background, timeout, scale,
),
)
async_noop()
ResponseBody::bytes(b"")
}
///|
/// Generate PDF (Chromium only)
pub async fn Page::pdf(
self : Page,
path? : String,
scale? : Double,
display_header_footer? : Bool,
header_template? : String,
footer_template? : String,
print_background? : Bool,
landscape? : Bool,
page_ranges? : String,
format? : String,
width? : String,
height? : String,
margin? : Margin,
prefer_css_page_size? : Bool,
) -> ResponseBody {
consume(
(
self, path, scale, display_header_footer, header_template, footer_template,
print_background, landscape, page_ranges, format, width, height, margin, prefer_css_page_size,
),
)
async_noop()
ResponseBody::bytes(b"")
}
///|
/// Get all console messages collected since page creation
pub fn Page::console_messages(self : Page) -> Array[ConsoleMessage] {
self.state.val.console_messages.copy()
}
///|
/// Get console error messages (level == "error")
pub fn Page::console_errors(self : Page) -> Array[ConsoleMessage] {
self.state.val.console_messages.filter(fn(m) { m.level == "error" })
}
///|
/// Clear collected console messages
pub fn Page::clear_console(self : Page) -> Unit {
self.state.val.console_messages.clear()
}
///|
/// Fill an input element by selector. Uses script.evaluate with single-quote escaping
/// to avoid double-quote issues in BiDi expression evaluation.
pub async fn Page::fill_input(
self : Page,
selector : String,
value : String,
) -> Unit {
// Use single quotes for selector to avoid escaping issues with double-quote attributes
let sel_escaped = selector.replace(old="'", new="\\'")
let val_escaped = value.replace(old="'", new="\\'").replace(old="\\", new="\\\\")
let expr = "(function(){var el=document.querySelector('" +
sel_escaped +
"');if(!el)return;el.focus();el.value='" +
val_escaped +
"';el.dispatchEvent(new InputEvent('input',{bubbles:true,data:'" +
val_escaped +
"',inputType:'insertText'}));el.dispatchEvent(new Event('change',{bubbles:true}))})()"
let state = self.state.val
let _ = state.browser.state.val.client.script_evaluate(expr, state.context_id)
}
///|
/// Ensure Luna island hydration is complete.
/// Calls __LUNA_SCAN__ if available, then waits for the specified timeout.
/// Use after goto() when testing hydrated islands.
pub async fn Page::ensure_hydration(
self : Page,
timeout? : Int = 5000,
) -> Unit {
self.wait_for_timeout(timeout)
let _ = self.evaluate("() => { if (window.__LUNA_SCAN__) window.__LUNA_SCAN__(); }")
self.wait_for_timeout(500)
}
///|
/// Wait until page content contains a specific string.
/// Polls content() until the string is found or timeout.
pub async fn Page::wait_for_content(
self : Page,
text : String,
timeout? : Int = 10000,
) -> Bool {
let start = @async.now()
for {
let content = self.content()
if content.contains(text) {
return true
}
let elapsed = @async.now() - start
if elapsed >= timeout.to_int64() {
return false
}
self.wait_for_timeout(200)
}
}
///|
/// Click an element inside a shadow DOM host.
/// Uses evaluate to pierce shadow roots.
pub async fn Page::shadow_click(
self : Page,
host_selector : String,
inner_selector : String,
) -> Unit {
let expr = "() => { const el = document.querySelector(" +
js_string_literal(host_selector) +
")?.shadowRoot?.querySelector(" +
js_string_literal(inner_selector) +
"); if (el) { el.dispatchEvent(new MouseEvent('click', {bubbles:true,composed:true})); el.click(); } }"
let _ = self.evaluate(expr)
}
///|
/// Get text content from an element inside a shadow DOM host.
pub async fn Page::shadow_text(
self : Page,
host_selector : String,
inner_selector : String,
) -> String {
let expr = "() => document.querySelector(" +
js_string_literal(host_selector) +
")?.shadowRoot?.querySelector(" +
js_string_literal(inner_selector) +
")?.textContent || ''"
match self.evaluate(expr) {
String(s) => s
_ => ""
}
}
///|
/// Fetch a URL from the page context and return status + body text.
pub async fn Page::fetch_text(
self : Page,
url : String,
method? : String = "GET",
body? : String,
headers? : Array[(String, String)],
) -> (Int, String) {
let headers_js = match headers {
Some(h) => {
let buf = StringBuilder::new()
buf.write_string("{")
for i, pair in h {
if i > 0 { buf.write_string(",") }
buf.write_string(js_string_literal(pair.0))
buf.write_string(":")
buf.write_string(js_string_literal(pair.1))
}
buf.write_string("}")
buf.to_string()
}
None => "undefined"
}
let body_js = match body {
Some(b) => js_string_literal(b)
None => "undefined"
}
// Use synchronous XMLHttpRequest (evaluate doesn't await Promises reliably)
let result = self.evaluate(
"() => { const xhr = new XMLHttpRequest(); xhr.open(" +
js_string_literal(method) + ", " + js_string_literal(url) +
", false); " + // false = synchronous
(match headers {
Some(h) => {
let buf = StringBuilder::new()
for pair in h {
buf.write_string("xhr.setRequestHeader(")
buf.write_string(js_string_literal(pair.0))
buf.write_string(",")
buf.write_string(js_string_literal(pair.1))
buf.write_string(");")
}
buf.to_string()
}
None => ""
}) +
"xhr.send(" + body_js + "); return { status: xhr.status, body: xhr.responseText }; }",
)
match result {
Object(m) => {
let status = match m.get("status") {
Some(Number(n, ..)) => n.to_int()
_ => 0
}
let text = match m.get("body") {
Some(String(s)) => s
_ => ""
}
(status, text)
}
_ => (0, "")
}
}
///|
/// Fetch a URL and parse response as JSON.
pub async fn Page::fetch_json(
self : Page,
url : String,
method? : String = "GET",
body? : String,
headers? : Array[(String, String)],
) -> (Int, Json) {
let (status, text) = self.fetch_text(url, method~, body?, headers?)
let json = try { @json.parse(text) } catch { _ => Json::null() }
(status, json)
}
///|
/// Close page
pub async fn Page::close(self : Page, run_before_unload? : Bool) -> Unit {
consume(run_before_unload)
let state = self.state.val
let _ = state.browser.state.val.client.browsing_context_close(
state.context_id,
)
let _ = state.context
}
///|
/// Close page with error callback (for use with defer)
/// Note: This initiates close but doesn't wait for completion
pub fn Page::close_with_error_callback(
self : Page,
on_error : (Error) -> Unit,
) -> Unit {
consume((self, on_error))
}
///|
/// Check if page is closed
pub fn Page::is_closed(self : Page) -> Bool {
consume(self)
false
}
///|
/// Bring page to front
pub async fn Page::bring_to_front(self : Page) -> Unit {
consume(self)
async_noop()
}
///|
/// Set viewport size
pub async fn Page::set_viewport_size(
self : Page,
width : Int,
height : Int,
) -> Unit {
consume((self, width, height))
async_noop()
}
///|
/// Get viewport size
pub fn Page::viewport_size(self : Page) -> (Int, Int)? {
consume(self)
None
}
///|
/// Set extra HTTP headers
pub async fn Page::set_extra_http_headers(
self : Page,
headers : Headers,
) -> Unit {
consume((self, headers))
async_noop()
}
///|
/// Add script tag
pub async fn Page::add_script_tag(
self : Page,
url? : String,
path? : String,
content? : String,
type_? : String,
) -> Json {
consume((self, url, path, content, type_))
async_noop()
Json::null()
}
///|
/// Add style tag
pub async fn Page::add_style_tag(
self : Page,
url? : String,
path? : String,
content? : String,
) -> Json {
consume((self, url, path, content))
async_noop()
Json::null()
}
///|
/// Expose function to page
pub async fn Page::expose_function(
self : Page,
name : String,
callback : Json,
) -> Unit {
consume((self, name, callback))
async_noop()
}
///|
/// Set default navigation timeout
pub fn Page::set_default_navigation_timeout(self : Page, timeout : Int) -> Unit {
consume((self, timeout))
}
///|
/// Set default timeout
pub fn Page::set_default_timeout(self : Page, timeout : Int) -> Unit {
consume((self, timeout))
}
///|
async fn Page::eval_string(self : Page, expression : String) -> String? {
let state = self.state.val
let result = state.browser.state.val.client.script_evaluate(
expression,
state.context_id,
)
match result {
Ok(json) =>
match json_get_object(json, "result") {
Some(value) => remote_value_to_string(value)
None => None
}
Err(_) => None
}
}
///|
async fn Page::call_function(
self : Page,
function_declaration : String,
args : Array[Json],
) -> Unit {
let state = self.state.val
let _ = state.browser.state.val.client.script_call_function(
function_declaration,
state.context_id,
arguments=args,
)
}
///|
async fn Page::locate_nodes(self : Page, selector : String) -> Array[Json] {
let state = self.state.val
let locator = @bidi.Locator::css(selector)
let result = state.browser.state.val.client.browsing_context_locate_nodes_typed(
state.context_id,
locator,
)
match result {
Ok(json) => json_get_array(json, "nodes").unwrap_or([])
Err(_) => []
}
}
///|
async fn Page::is_node_visible(self : Page, node : Json) -> Bool? {
let state = self.state.val
let args : Array[Json] = [node]
let result = state.browser.state.val.client.script_call_function(
"(node) => { if (!node || !(node instanceof Element)) return false; if (node.hidden) return false; let el = node; while (el) { if (el.hidden) return false; if (el.get_attribute && el.get_attribute('aria-hidden') === 'true') return false; const style = getComputedStyle(el); if (!style) return false; const opacity = parseFloat(style.opacity || '1'); if (opacity <= 0) return false; if (style.visibility === 'hidden' || style.visibility === 'collapse' || style.display === 'none' || style.contentVisibility === 'hidden') return false; if (el instanceof HTMLDetailsElement && !el.open) { const summary = el.querySelector('summary'); if (summary && !summary.contains(node)) return false; } el = el.parentElement; } if (node.getClientRects().length === 0) return false; const rect = node.getBoundingClientRect(); return rect.width > 0 && rect.height > 0; }",
state.context_id,
arguments=args,
)
match result {
Ok(json) =>
match json_get_object(json, "result") {
Some(value) =>
match remote_value_to_json(value) {
True => Some(true)
False => Some(false)
_ => None
}
None => None
}
Err(_) => None
}
}
///|
fn Page::take_navigation_event(
self : Page,
event_name : String?,
url : String?,
) -> NavigationEvent? {
let events = self.state.val.navigation_events
let mut index = 0
while index < events.length() {
let evt = events[index]
let event_ok = match event_name {
Some(value) => evt.event_name == value
None => true
}
let url_ok = match url {
Some(value) => evt.url == value
None => true
}
if event_ok && url_ok {
let _ = events.remove(index)
return Some(evt)
}
index = index + 1
}
None
}
///|
fn Page::take_request_event(
self : Page,
pattern : String,
match_mode : String,
predicate : ((Request) -> Bool)?,
) -> Request? {
let events = self.state.val.request_events
let mut index = 0
while index < events.length() {
let req = events[index]
let url_ok = url_matches(req.url(), pattern, match_mode)
let predicate_ok = match predicate {
Some(pred) => pred(req)
None => true
}
if url_ok && predicate_ok {
let _ = events.remove(index)
return Some(req)
}
index = index + 1
}
None
}
///|
fn Page::take_response_event(
self : Page,
pattern : String,
match_mode : String,
predicate : ((Response) -> Bool)?,
) -> Response? {
let events = self.state.val.response_events
let mut index = 0
while index < events.length() {
let resp = events[index]
let url_ok = url_matches(resp.url(), pattern, match_mode)
let predicate_ok = match predicate {
Some(pred) => pred(resp)
None => true
}
if url_ok && predicate_ok {
let _ = events.remove(index)
return Some(resp)
}
index = index + 1
}
None
}
///|
fn Page::take_response_completed_event(
self : Page,
pattern : String,
match_mode : String,
predicate : ((Response) -> Bool)?,
) -> Response? {
let events = self.state.val.response_completed_events
let mut index = 0
while index < events.length() {
let resp = events[index]
let url_ok = url_matches(resp.url(), pattern, match_mode)
let predicate_ok = match predicate {
Some(pred) => pred(resp)
None => true
}
if url_ok && predicate_ok {
let _ = events.remove(index)
return Some(resp)
}
index = index + 1
}
None
}
///|
fn resolve_match_mode(pattern : String, match_mode : String?) -> String {
match match_mode {
Some(mode) => {
let lower = mode.to_lower()
if lower == "contains" || lower == "include" || lower == "substring" {
"contains"
} else if lower == "startswith" || lower == "prefix" {
"starts_with"
} else if lower == "endswith" || lower == "suffix" {
"ends_with"
} else if lower == "glob" || lower == "wildcard" {
"glob"
} else if lower == "regex" || lower == "re" {
"regex"
} else {
"exact"
}
}
None =>
if pattern.length() >= 2 &&
pattern.has_prefix("/") &&
pattern.has_suffix("/") {
"regex"
} else if pattern.contains_char('*') || pattern.contains_char('?') {
"glob"
} else {
"exact"
}
}
}
///|
fn resolve_response_stage(stage : String?) -> String {
match stage {
Some(value) => {
let lower = value.to_lower()
if lower == "complete" || lower == "completed" {
"completed"
} else {
"started"
}
}
None => "started"
}
}
///|
fn url_matches(url : String, pattern : String, match_mode : String) -> Bool {
match match_mode {
"contains" => url.contains(pattern)
"starts_with" => url.has_prefix(pattern)
"ends_with" => url.has_suffix(pattern)
"glob" => glob_match(url, pattern)
"regex" => regex_match(url, normalize_regex_pattern(pattern))
_ => url == pattern
}
}
///|
fn normalize_regex_pattern(pattern : String) -> String {
if pattern.length() >= 2 && pattern.has_prefix("/") && pattern.has_suffix("/") {
let view = pattern.sub(start=1, end=pattern.length() - 1) catch {
_ => return pattern
}
view.to_string()
} else {
pattern
}
}
///|
fn glob_match(text : String, pattern : String) -> Bool {
let text_chars = text.to_array()
let pattern_chars = pattern.to_array()
let mut ti = 0
let mut pi = 0
let mut star = -1
let mut match_index = 0
let tlen = text_chars.length()
let plen = pattern_chars.length()
while ti < tlen {
if pi < plen &&
(pattern_chars[pi] == '?' || pattern_chars[pi] == text_chars[ti]) {
ti = ti + 1
pi = pi + 1
continue
}
if pi < plen && pattern_chars[pi] == '*' {
star = pi
match_index = ti
pi = pi + 1
continue
}
if star >= 0 {
pi = star + 1
match_index = match_index + 1
ti = match_index
continue
}
return false
}
while pi < plen && pattern_chars[pi] == '*' {
pi = pi + 1
}
pi == plen
}