view release on metacpan or search on metacpan
t/20-nav.t
t/21-teardown.t
t/30-script.t
t/40-find.t
t/40-frame-find.t
t/41-element-read.t
t/41-frame-ipc.t
t/42-element-write.t
t/43-wait.t
t/44-wait-teardown.t
t/45-auth-popup-viewport.t
t/45-lifecycle.t
t/46-collectability.t
t/46-find-js.t
t/47-nav-and-click.t
t/47-unicode.t
t/48-doc-promises.t
t/48-element-epoch.t
t/49-hostile.t
t/50-quit-flush.t
t/50-screenshot.t
("set_cookie"/"cookies"/"clear_cookies"/"save_cookies"/"load_cookies
") -- and is the default for "wait_for"'s and "pdf"'s own "timeout"
option, and "wait_for_navigation"'s. On expiry the operation's
callback is resolved with "$err eq 'timeout'". Default 30.
"download" is the deliberate exception: a large file legitimately
takes longer than any per-operation timeout would allow, so a
download is bounded only by the server and by "quit". Use
"$dl->cancel" to end one early.
"popups => 'follow' | 'block'"
What to do with a navigation that asks for a new window. Documented
in full under "EVENTS", with the rest of the options that shape how
the browser reacts to the page.
"user_agent => $string"
Sets the initial User-Agent (equivalent to calling "set_user_agent"
right after construction).
"ephemeral => $bool"
Use an ephemeral (in-memory, non-persistent) network session when
Requires the optional Proxy::Impersonate toolchain (which builds
"curl-impersonate" via Alien::curlimpersonate); croaks if it is
unavailable. Mutually exclusive with an explicit "proxy". Out of
scope: WebSockets, HTTP/3. See "network_fingerprint" and
"proxy_port".
"on_error", "on_load", "on_navigate", "on_close", "on_console",
"on_dialog", "on_policy", "on_file_chooser", "on_download",
"on_authenticate", "on_request", "on_response"
Event callbacks -- see "EVENTS", which documents each one and what
it is handed. ("popups", above, is documented there too: it is what
happens when no "on_policy" is set.)
METHODS
Navigation
Load pages and read basic document state.
go
$b->go($uri, sub { my ($result, $err) = @_; ... });
Loads $uri. On success $result is true; on failure (or timeout) $err is
my $prev = $b->on_console;
$b->on_console(sub { $prev->(@_) if $prev; ...also mine... });
Croaks on a non-coderef. Enabling "on_console" after a page has loaded
takes effect from the next navigation: the console proxy is a user
script, and those are injected at document start.
EVENTS
Optional callbacks passed to "new", and the one option that shapes what
the browser does when it has none ("popups"):
"on_error => sub { my ($err) = @_ }"
Called for a navigation failure that has no "go"/"load_html"
callback waiting for it (e.g. a stray "load-failed" signal).
Ordinary navigation failures go to that call's own callback instead,
not here.
"on_load => sub { }"
Called with no arguments when a navigation started through this API
("go", "load_html", "back", "forward", or "reload") finishes
you own the loop. For a browser window whose closing should end the
program, that is the whole handler:
my $b = EV::WebKit->new(chrome => 1, on_close => sub { EV::break });
...
EV::run; # returns when the window is closed
Unlike "on_console"/"on_dialog"/"on_policy", "on_close" is delivered
on a clean EV tick, so calling "EV::break" directly from it is safe.
"popups => 'follow' | 'block'"
What to do with a navigation that asks for a new window -- a
"target="_blank"" link, or "window.open". WebKit allows such a
navigation and then asks for a window to put it in; a one-view
browser has none to give, so the click would otherwise do nothing
whatsoever: no navigation, no error, no event. The default 'follow'
takes it in this view instead. 'block' keeps it dropped.
The two arrive by different routes, which matters if you set
"on_policy". A "target="_blank"" link is a policy decision, so that
handler sees it first, with "type => 'new-window-action'", and can
refuse it outright with "$p->block".
"window.open" is not a window request WebKitGTK asks about, so no
"new-window-action" ever arrives for it -- there is nothing to
refuse at that stage. Under the default 'follow', though, the popup
is re-issued in this view as an ordinary navigation, and that does
reach "on_policy" as a "navigation-action" carrying the popup's own
url. So selective filtering is possible for both mechanisms; only
the decision "type" differs. (Under 'block' the popup is dropped
before any navigation, so "on_policy" sees nothing at all.)
One caveat if you are testing this: WebKit's own popup blocker drops
a "window.open" made from an inline script with no user gesture
behind it, and then nothing reaches "on_policy" either. Drive it
from a real click -- "$el->click" counts -- as a page would.
What "on_policy" does not decide is where an allowed one goes:
WebKit asks for a window afterwards either way, and this option is
what answers. So an "on_policy" that allows a "target="_blank"" link
still lands it in this view under 'follow', and still drops it under
'block'. If you want to route it yourself, "$p->block" and navigate
from a clean tick -- starting a navigation inside the handler runs
lib/EV/WebKit.pm view on Meta::CPAN
$r = ($r * 10 + $_) % 4294967296 for split //, $_[0];
return $r;
}
my %KNOWN_NEW = map { $_ => 1 } qw(
timeout window display
on_load on_error on_close on_navigate on_console on_dialog on_policy
on_file_chooser on_download on_request on_response on_authenticate
data_dir cache_dir ephemeral cookie_jar jar_format
proxy user_agent devtools title chrome
fingerprint network_fingerprint seed popups fonts
);
# fonts => a fontconfig file, or the directories to build one from. Returns
# undef, or { conf => $abs_file, bind => [ [$path, $read_only], ... ] }.
#
# The web process runs under bubblewrap and cannot see a path nobody mounted
# for it: setting FONTCONFIG_FILE alone changes nothing at all (measured --
# identical text metrics), because fontconfig inside the sandbox never opens
# the file. So every path the config names has to be handed to
# add_path_to_sandbox as well, which is why this returns the list rather than
lib/EV/WebKit.pm view on Meta::CPAN
my $err = $@;
eval { $dlg->dismiss }; # best-effort: give the page a definite answer
warn "EV::WebKit: on_dialog callback died: $err";
}
}
else { $dlg->dismiss }
return 1; # handled -- suppress WebKit's own blocking native dialog
});
$self->{on_policy} = $o{on_policy};
$self->{popups} = $o{popups} // 'follow';
Carp::croak("EV::WebKit: popups must be 'follow' or 'block'")
unless $self->{popups} eq 'follow' || $self->{popups} eq 'block';
$view->signal_connect('decide-policy' => sub {
my (undef, $decision, $type_nick) = @_; # type_nick: navigation-action/new-window-action/response
local $IN_DISPATCH = 1; # on_policy runs nested in WebKit's dispatch frame -- see quit
my $self = $wself or return 0; # $self gone: WebKit applies its own default (allow)
return 0 if $self->{_dead}; # torn down: let WebKit apply its own default
# A target=_blank navigation is ALLOWED by default and then silently
# dropped: WebKit goes on to ask for a window through 'create', which a
# one-view browser does not answer, so the click does nothing at all --
# no navigation, no error, no event. Follow it in this view instead, on
# a clean tick, since starting a navigation inside WebKit's own dispatch
# frame is the wedge $IN_DISPATCH exists for. popups => 'block' keeps
# the drop; an on_policy handler owns the decision itself and is left
# alone. window.open does NOT arrive here -- see the 'create' handler.
if (($type_nick // '') eq 'new-window-action'
&& $self->{popups} eq 'follow' && !$self->{on_policy}) {
my $nu = eval { $decision->get_navigation_action->get_request->get_uri };
if (defined $nu && length $nu) {
$decision->ignore;
weaken(my $ws = $self);
$self->_defer(sub { my ($u) = @_; my $b = $ws or return; $b->go($u) }, $nu);
return 1;
}
}
return 0 unless $self->{on_policy}; # not handled -- WebKit applies its own default (allow)
# WebKitNavigationPolicyDecision (navigation-action/new-window-action) only
lib/EV/WebKit.pm view on Meta::CPAN
return 1;
}
$info->allow unless $info->{done}; # default allow if handler didn't decide
return 1; # handled
});
# window.open() is not a policy decision in WebKitGTK: decide-policy never
# fires for it at all (measured), and WebKit asks for a window through
# 'create' instead. A one-view browser that answers nothing there leaves the
# call returning null with no navigation, no error and no event -- so
# popups => 'follow' has to be honoured here as well as in decide-policy,
# which only ever saw target=_blank. on_policy is deliberately NOT consulted:
# there is no decision object to give it, and inventing one would document a
# policy hook that cannot allow, ignore or download.
$view->signal_connect(create => sub {
my (undef, $nav) = @_;
local $IN_DISPATCH = 1;
my $self = $wself or return undef;
return undef if $self->{_dead} || $self->{popups} ne 'follow';
my $nu = eval { $nav->get_request->get_uri };
return undef unless defined $nu && length $nu;
weaken(my $ws = $self);
$self->_defer(sub { my ($u) = @_; my $b = $ws or return; $b->go($u) }, $nu);
return undef; # no second view: the navigation happens in this one
});
# HTTP (and proxy) authentication. WITHOUT this connected at all, a 401
# challenge is answered by nobody: WebKit waits, the navigation resolves
# 'timeout' after the full instance timeout, and status() is undef -- so the
lib/EV/WebKit.pm view on Meta::CPAN
everything routed through a frame, C<resize>, C<html>, C<screenshot>, C<pdf>,
and the cookie operations
(C<set_cookie>/C<cookies>/C<clear_cookies>/C<save_cookies>/C<load_cookies>) --
and is the default for C<wait_for>'s and C<pdf>'s own C<timeout> option, and C<wait_for_navigation>'s. On expiry the operation's
callback is resolved with C<$err eq 'timeout'>. Default C<30>.
C<download> is the deliberate exception: a large file legitimately takes longer
than any per-operation timeout would allow, so a download is bounded only by
the server and by C<quit>. Use C<< $dl->cancel >> to end one early.
=item C<< popups => 'follow' | 'block' >>
What to do with a navigation that asks for a new window. Documented in full
under L</EVENTS>, with the rest of the options that shape how the browser
reacts to the page.
=item C<< user_agent => $string >>
Sets the initial User-Agent (equivalent to calling C<set_user_agent> right
after construction).
lib/EV/WebKit.pm view on Meta::CPAN
nor a settable C<GTlsDatabase>), which is why the C<IGNORE> policy is used.
Requires the optional L<Proxy::Impersonate> toolchain (which builds
C<curl-impersonate> via L<Alien::curlimpersonate>); croaks if it is unavailable.
Mutually exclusive with an explicit C<proxy>. Out of scope: WebSockets, HTTP/3.
See L</network_fingerprint> and L</proxy_port>.
=item C<on_error>, C<on_load>, C<on_navigate>, C<on_close>, C<on_console>, C<on_dialog>, C<on_policy>, C<on_file_chooser>, C<on_download>, C<on_authenticate>, C<on_request>, C<on_response>
Event callbacks -- see L</"EVENTS">, which documents each one and what it is
handed. (C<popups>, above, is documented there too: it is what happens when no
C<on_policy> is set.)
=back
=head1 METHODS
=head2 Navigation
Load pages and read basic document state.
lib/EV/WebKit.pm view on Meta::CPAN
my $prev = $b->on_console;
$b->on_console(sub { $prev->(@_) if $prev; ...also mine... });
Croaks on a non-coderef. Enabling C<on_console> after a page has loaded takes
effect from the B<next> navigation: the console proxy is a user script, and
those are injected at document start.
=head1 EVENTS
Optional callbacks passed to C<new>, and the one option that shapes what the
browser does when it has none (C<popups>):
=over 4
=item C<< on_error => sub { my ($err) = @_ } >>
Called for a navigation failure that has no C<go>/C<load_html> callback
waiting for it (e.g. a stray C<load-failed> signal). Ordinary navigation
failures go to that call's own callback instead, not here.
=item C<< on_load => sub { } >>
lib/EV/WebKit.pm view on Meta::CPAN
own the loop. For a browser window whose closing should end the program, that
is the whole handler:
my $b = EV::WebKit->new(chrome => 1, on_close => sub { EV::break });
...
EV::run; # returns when the window is closed
Unlike C<on_console>/C<on_dialog>/C<on_policy>, C<on_close> is delivered on a
clean EV tick, so calling C<EV::break> directly from it is safe.
=item C<< popups => 'follow' | 'block' >>
What to do with a navigation that asks for a new window -- a C<target="_blank">
link, or C<window.open>. WebKit allows such a navigation and then asks for a
window to put it in; a one-view browser has none to give, so the click would
otherwise do nothing whatsoever: no navigation, no error, no event. The default
C<'follow'> takes it in this view instead. C<'block'> keeps it dropped.
The two arrive by different routes, which matters if you set C<on_policy>. A
C<target="_blank"> link is a policy decision, so that handler sees it first,
with C<< type => 'new-window-action' >>, and can refuse it outright with
C<< $p->block >>.
C<window.open> is B<not> a window request WebKitGTK asks about, so no
C<new-window-action> ever arrives for it -- there is nothing to refuse at that
stage. Under the default C<'follow'>, though, the popup is re-issued in this
view as an ordinary navigation, and that B<does> reach C<on_policy> as a
C<navigation-action> carrying the popup's own url. So selective filtering is
possible for both mechanisms; only the decision C<type> differs. (Under
C<'block'> the popup is dropped before any navigation, so C<on_policy> sees
nothing at all.)
One caveat if you are testing this: WebKit's own popup blocker drops a
C<window.open> made from an inline script with no user gesture behind it, and
then nothing reaches C<on_policy> either. Drive it from a real click --
C<< $el->click >> counts -- as a page would.
What C<on_policy> does not decide is B<where> an allowed one goes: WebKit asks
for a window afterwards either way, and this option is what answers. So an
C<on_policy> that allows a C<target="_blank"> link still lands it in this view
under C<'follow'>, and still drops it under C<'block'>. If you want to route it
yourself, C<< $p->block >> and navigate from a clean tick -- starting a
navigation inside the handler runs it in WebKit's own dispatch frame.
t/45-auth-popup-viewport.t view on Meta::CPAN
$b->go("http://127.0.0.1:$port/secret", sub { $err = $_[1]; $secs = EV::time - $t0; EV::break });
TWK::run_with_timeout(30);
ok(defined $err, "on_authenticate that $name: the navigation fails");
cmp_ok($secs // 99, '<', 5, "on_authenticate that $name: ...promptly, not at the instance timeout");
like($err // '', $want, "on_authenticate that $name: ...saying which route it took");
$b->quit;
}
# --- window.open ------------------------------------------------------------
# window.open is NOT a policy decision in WebKitGTK: decide-policy never fires
# for it, so the popups option had to be honoured on 'create' as well. Before
# that it did nothing at all -- no navigation, no error, no event.
for my $case ([follow => 'POPUP'], [block => undef]) {
my ($mode, $want) = @$case;
my $b = EV::WebKit->new(window => [400,300], timeout => 15, popups => $mode);
# window.open's return value is recorded in a page global, so the 'block'
# case can assert the call was actually MADE -- otherwise "the title is not
# POPUP" is equally true of a page where the click never happened.
$b->load_html('<html><body><button id=x onclick=\'window.opened = window.open('
. '"http://127.0.0.1:' . $port . '/pop","_blank") ? "won" : "null"\'>go</button></body></html>', sub {
$b->find('#x', sub {
$_[0]->click(sub { my $t; $t = EV::timer(2.5, 0, sub { undef $t; EV::break }) });
});
});
TWK::run_with_timeout(30);
if (defined $want) {
is($b->title, $want, "popups => $mode follows window.open in this view");
like($b->uri // '', qr{/pop$}, "...landing on the requested uri");
}
else {
my $opened;
$b->script('return String(window.opened);', sub { $opened = $_[0]; EV::break });
TWK::run_with_timeout(20);
is($opened, 'null', "popups => $mode: window.open was called and returned null");
isnt($b->title // '', 'POPUP', "popups => $mode drops window.open");
}
$b->quit;
}
# --- resize / zoom ----------------------------------------------------------
{
my $b = EV::WebKit->new(window => [800,600], timeout => 15);
my (%g, @steps);
my $nxt; $nxt = sub { my $s = shift @steps or return EV::break; $s->() };
@steps = (
t/45-auth-popup-viewport.t view on Meta::CPAN
like($@, qr/name => value pairs/, '...rather than defaulting persist silently');
ok(!eval { $auth->login('u', 'p', 'permanent'); 1 }, 'login croaks on a bare option value too');
like($@, qr/name => value pairs/, '...with the same message');
is(scalar(@w), 0, '...and neither warned from inside the module');
ok(!eval { $auth->login('u', 'p', persist => 'forever'); 1 }, 'a bad persist value still croaks');
like($@, qr/persist must be/, '...saying the allowed values');
}
# window.open is not a WINDOW request WebKitGTK asks about, so no
# new-window-action ever arrives for it -- but under the default
# popups => 'follow' the popup is re-issued in this view as an ordinary
# navigation, which on_policy DOES see and can refuse. The POD used to say it
# "never reaches on_policy at all", which talks a user out of selective popup
# filtering entirely.
#
# The open must come from a real user gesture: WebKit's popup blocker drops a
# window.open called from an inline script, and then nothing reaches policy at
# all (measured -- that is why this clicks a button rather than running script).
{
our @POL;
my $pb = EV::WebKit->new(window => [300,200], ephemeral => 1, timeout => 10,
on_policy => sub {
my ($p) = @_;
push @POL, ($p->{type} // '?') . '(' . ($p->{uri} // '?') . ')';
$p->allow;
});
t/45-auth-popup-viewport.t view on Meta::CPAN
ok($el, 'premise: the opener page loaded and its button was found');
SKIP: {
skip 'no button', 2 unless $el;
@POL = ();
$el->click(sub { EV::break });
TWK::run_with_timeout(20);
{ my $settle = EV::timer(2.0, 0, sub { EV::break }); EV::run }
my $seen = join ' ', @POL;
like($seen, qr{navigation-action\(pw://host/target\)},
'a window.open popup reaches on_policy as a navigation-action')
or diag("policy events: $seen");
unlike($seen, qr{new-window-action\(pw://host/target\)},
'...and never as a new-window-action, which is what "not a policy decision" meant');
}
$pb->quit;
}
done_testing;
t/82-navigate.t view on Meta::CPAN
is(scalar(@nav), 1, 'load_html fires on_navigate');
# 4) nothing after quit
@nav = ();
$b->quit;
for (1 .. 3) { my $t = EV::timer(0.05, 0, sub { EV::break }); EV::run }
is(scalar(@nav), 0, 'no on_navigate after quit');
# A target=_blank click asks WebKit for a NEW WINDOW. WebKit allows it and then
# emits 'create' to get one; a single-view browser answers nothing, so the click
# used to do nothing at all -- no navigation, no error, no event. popups
# defaults to 'follow', which takes it in this view instead.
{
my $b = EV::WebKit->new(window => [400,300], timeout => 8);
$b->mock_scheme('mock', sub {
my ($uri) = @_;
return ('<html><body><a id="x" href="mock://target" target="_blank">go</a></body></html>',
'text/html') if $uri =~ m{mock://start};
return ('<html><head><title>TARGET</title></head><body>arrived</body></html>', 'text/html');
});
my ($uri, $title);
t/82-navigate.t view on Meta::CPAN
undef $t; $uri = $b->uri; $title = $b->title; EV::break });
});
});
});
TWK::run_with_timeout(20);
is($uri, 'mock://target', 'a target=_blank click navigates this view');
is($title, 'TARGET', '...and the page really loaded');
$b->quit;
}
# popups => 'block' keeps the drop, for a caller that wants it.
{
my $b = EV::WebKit->new(window => [400,300], timeout => 8, popups => 'block');
$b->mock_scheme('mock', sub {
my ($uri) = @_;
return ('<html><body><a id="x" href="mock://target" target="_blank">go</a></body></html>',
'text/html') if $uri =~ m{mock://start};
return ('<html><head><title>TARGET</title></head><body>arrived</body></html>', 'text/html');
});
my $uri;
$b->go('mock://start', sub {
$b->find('#x', sub {
my ($el) = @_;
$el->click(sub {
my $t; $t = EV::timer(2.5, 0, sub { undef $t; $uri = $b->uri; EV::break });
});
});
});
TWK::run_with_timeout(20);
is($uri, 'mock://start', "popups => 'block' leaves the view where it was");
$b->quit;
}
# and the option is validated rather than silently ignored
{
my $bad = eval { EV::WebKit->new(window => [200,200], popups => 'maybe'); 1 };
ok(!$bad, 'an unknown popups value croaks');
like($@, qr/popups must be/, '...naming the option');
}
done_testing;