Playwright.pm 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. package Playwright;
  2. use strict;
  3. use warnings;
  4. use sigtrap qw/die normal-signals/;
  5. use File::Basename();
  6. use Cwd();
  7. use LWP::UserAgent();
  8. use Sub::Install();
  9. use Net::EmptyPort();
  10. use JSON::MaybeXS();
  11. use File::Slurper();
  12. use Carp qw{confess};
  13. use Playwright::Base();
  14. use Playwright::Util();
  15. #ABSTRACT: Perl client for Playwright
  16. no warnings 'experimental';
  17. use feature qw{signatures state};
  18. =head2 SYNOPSIS
  19. use JSON::PP;
  20. use Playwright;
  21. my $handle = Playwright->new();
  22. my $browser = $handle->launch( headless => JSON::PP::false, type => 'chrome' );
  23. my $page = $browser->newPage();
  24. my $res = $page->goto('http://google.com', { waitUntil => 'networkidle' });
  25. my $frameset = $page->mainFrame();
  26. my $kidframes = $frameset->childFrames();
  27. =head2 DESCRIPTION
  28. Perl interface to a lightweight node.js webserver that proxies commands runnable by Playwright.
  29. Currently understands commands you can send to all the playwright classes defined in api.json.
  30. See L<https://playwright.dev/#version=master&path=docs%2Fapi.md&q=>
  31. for what the classes do, and their usage.
  32. =head1 CONSTRUCTOR
  33. =head2 new(HASH) = (Playwright)
  34. Creates a new browser and returns a handle to interact with it.
  35. =head3 INPUT
  36. debug (BOOL) : Print extra messages from the Playwright server process
  37. =cut
  38. our ($spec, $server_bin, %mapper);
  39. BEGIN {
  40. my $path2here = File::Basename::dirname(Cwd::abs_path($INC{'Playwright.pm'}));
  41. my $specfile = "$path2here/../api.json";
  42. confess("Can't locate Playwright specification in '$specfile'!") unless -f $specfile;
  43. my $spec_raw = File::Slurper::read_text($specfile);
  44. my $decoder = JSON::MaybeXS->new();
  45. $spec = $decoder->decode($spec_raw);
  46. foreach my $class (keys(%$spec)) {
  47. $mapper{$class} = sub {
  48. my ($self, $res) = @_;
  49. my $class = "Playwright::$class";
  50. return $class->new( handle => $self, id => $res->{_guid}, type => $class );
  51. };
  52. #All of the Playwright::* Classes are made by this MAGIC
  53. Sub::Install::install_sub({
  54. code => sub ($classname,%options) {
  55. @class::ISA = qw{Playwright::Base};
  56. $options{type} = $class;
  57. return Playwright::Base::new($classname,%options);
  58. },
  59. as => 'new',
  60. into => "Playwright::$class",
  61. });
  62. }
  63. # Make sure it's possible to start the server
  64. $server_bin = "$path2here/../bin/playwright.js";
  65. confess("Can't locate Playwright server in '$server_bin'!") unless -f $specfile;
  66. }
  67. sub new ($class, %options) {
  68. #XXX yes, this is a race, so we need retries in _start_server
  69. my $port = Net::EmptyPort::empty_port();
  70. my $self = bless({
  71. ua => $options{ua} // LWP::UserAgent->new(),
  72. port => $port,
  73. debug => $options{debug},
  74. pid => _start_server( $port, $options{debug}),
  75. }, $class);
  76. return $self;
  77. }
  78. =head1 METHODS
  79. =head2 launch(HASH) = Playwright::Browser
  80. The Argument hash here is essentially those you'd see from browserType.launch(). See:
  81. L<https://playwright.dev/#version=v1.5.1&path=docs%2Fapi.md&q=browsertypelaunchoptions>
  82. There is an additional "special" argument, that of 'type', which is used to specify what type of browser to use, e.g. 'firefox'.
  83. =cut
  84. sub launch ($self, %args) {
  85. #TODO coerce types based on spec
  86. my $msg = Playwright::Util::request ('POST', 'session', $self->{port}, $self->{ua}, type => delete $args{type}, args => [\%args] );
  87. return $Playwright::mapper{$msg->{_type}}->($self,$msg) if (ref $msg eq 'HASH') && $msg->{_type} && exists $Playwright::mapper{$msg->{_type}};
  88. return $msg;
  89. }
  90. =head2 quit, DESTROY
  91. Terminate the browser session and wait for the Playwright server to terminate.
  92. Automatically called when the Playwright object goes out of scope.
  93. =cut
  94. sub quit ($self) {
  95. Playwright::Util::request ('GET', 'shutdown', $self->{port}, $self->{ua} );
  96. return waitpid($self->{pid},0);
  97. }
  98. sub DESTROY ($self) {
  99. $self->quit();
  100. }
  101. sub _start_server($port, $debug) {
  102. $debug = $debug ? '-d' : '';
  103. $ENV{DEBUG} = 'pw:api';
  104. my $pid = fork // confess("Could not fork");
  105. if ($pid) {
  106. print "Waiting for port to come up..." if $debug;
  107. Net::EmptyPort::wait_port($port,30) or confess("Server never came up after 30s!");
  108. print "done\n" if $debug;
  109. return $pid;
  110. }
  111. exec( $server_bin, "-p", $port, $debug);
  112. }
  113. 1;