File Coverage

blib/lib/Selenium/Specification.pm
Criterion Covered Total %
statement 47 205 22.9
branch 0 72 0.0
condition 0 64 0.0
subroutine 16 27 59.2
pod 2 2 100.0
total 65 370 17.5


line stmt bran cond sub pod time code
1             package Selenium::Specification;
2             $Selenium::Specification::VERSION = '2.01';
3             # ABSTRACT: Module for building a machine readable specification for Selenium
4              
5 3     3   254720 use strict;
  3         6  
  3         151  
6 3     3   20 use warnings;
  3         6  
  3         208  
7              
8 3     3   58 use v5.28;
  3         13  
9              
10 3     3   33 no warnings 'experimental';
  3         7  
  3         242  
11 3     3   22 use feature qw/signatures state unicode_strings/;
  3         6  
  3         688  
12              
13 3     3   25 use List::Util qw{uniq};
  3         5  
  3         619  
14 3     3   2390 use HTML::Parser();
  3         25778  
  3         155  
15 3     3   683 use JSON::MaybeXS();
  3         10979  
  3         78  
16 3     3   709 use File::HomeDir();
  3         7805  
  3         143  
17 3     3   1854 use File::Slurper();
  3         4258  
  3         84  
18 3     3   2198 use DateTime::Format::HTTP();
  3         2293924  
  3         148  
19 3     3   910 use HTTP::Tiny();
  3         58488  
  3         127  
20 3     3   23 use File::Path qw{make_path};
  3         8  
  3         226  
21 3     3   21 use File::Spec();
  3         9  
  3         78  
22 3     3   18 use Encode qw{decode};
  3         41  
  3         251  
23 3     3   774 use Unicode::Normalize qw{NFC};
  3         3881  
  3         11662  
24              
25             #TODO make a JSONWire JSON spec since it's not changing
26              
27             # URLs and the container ID
28             our %spec_urls = (
29             unstable => {
30             url => 'https://w3c.github.io/webdriver/',
31             section_id => 'endpoints',
32             },
33             draft => {
34             url => "https://www.w3.org/TR/webdriver2/",
35             section_id => 'endpoints',
36             },
37             stable => {
38             url => "https://www.w3.org/TR/webdriver1/",
39             section_id => 'list-of-endpoints',
40             },
41             );
42              
43             our $browser = HTTP::Tiny->new();
44             my %state;
45             my $parse = [];
46             our $method = {};
47              
48              
49 0     0 1   sub read ( $client_dir, $type = 'stable', $nofetch = 1, $hardcode = 1 ) {
  0            
  0            
  0            
  0            
  0            
50 0           my $buf;
51 0           state $static;
52 0 0         if ( !$hardcode ) {
53 0           my $dir = File::Spec->catdir( $client_dir, "specs" );
54 0           my $file = File::Spec->catfile( "$dir", "$type.json" );
55 0           fetch( once => $nofetch, dir => $dir );
56 0 0         die "could not write $file: $@" unless -f $file;
57 0           $buf = File::Slurper::read_binary($file);
58             }
59             else {
60 0 0         $static = readline(DATA) unless $static;
61 0           $buf = $static;
62             }
63 0           my $array = JSON::MaybeXS->new()->utf8()->decode($buf);
64 0           my %hash;
65 0           @hash{ map { $_->{name} } @$array } = @$array;
  0            
66 0           return \%hash;
67             }
68              
69              
70             #TODO needs to grab args and argtypes still
71 0     0 1   sub fetch (%options) {
  0            
  0            
72 0           my $dir = $options{dir};
73              
74 0           my $rc = 0;
75 0           foreach my $spec ( sort keys(%spec_urls) ) {
76 0 0         make_path($dir) unless -d $dir;
77 0           my $file = File::Spec->catfile( "$dir", "$spec.json" );
78 0 0         my $last_modified = -f $file ? ( stat($file) )[9] : undef;
79              
80 0 0 0       if ( $options{once} && $last_modified ) {
81 0 0         print STDERR "Skipping fetch, using cached result" if $options{verbose};
82 0           next;
83             }
84              
85 0 0         $last_modified = 0 if $options{force};
86              
87 0           my $spc = _build_spec( $last_modified, %{ $spec_urls{$spec} } );
  0            
88 0 0         if ( !$spc ) {
89 0 0         print STDERR "Could not retrieve $spec_urls{$spec}{url}, skipping" if $options{verbose};
90 0           $rc = 1;
91 0           next;
92             }
93              
94             # Second clause is for an edge case -- if the header is not set for some bizarre reason we should obey force still
95 0 0 0       if ( ref $spc ne 'ARRAY' && $last_modified ) {
96 0 0         print STDERR "Keeping cached result '$file', as page has not changed since last fetch.\n" if $options{verbose};
97 0           next;
98             }
99              
100 0           _write_spec( $spc, $file );
101 0 0         print "Wrote $file\n" if $options{verbose};
102             }
103 0           return $rc;
104             }
105              
106 0     0     sub _write_spec ( $spec, $file ) {
  0            
  0            
  0            
107 0           my $spec_json = JSON::MaybeXS->new()->utf8()->encode($spec);
108 0           return File::Slurper::write_binary( $file, $spec_json );
109             }
110              
111 0     0     sub _build_spec ( $last_modified, %spec ) {
  0            
  0            
  0            
112 0           my $page = $browser->get( $spec{url} );
113 0 0         return unless $page->{success};
114              
115 0 0 0       if ( $page->{headers}{'last-modified'} && $last_modified ) {
116 0           my $modified = DateTime::Format::HTTP->parse_datetime( $page->{headers}{'last-modified'} )->epoch();
117 0 0         return 'cache' if $modified < $last_modified;
118             }
119              
120 0           my $html = NFC( decode( 'UTF-8', $page->{content} ) );
121              
122 0           $parse = [];
123 0           %state = ( id => $spec{section_id} );
124 0           my $parser = HTML::Parser->new(
125             handlers => {
126             start => [ \&_handle_open, "tagname,attr" ],
127             end => [ \&_handle_close, "tagname" ],
128             text => [ \&_handle_text, "text" ],
129             }
130             );
131 0           $parser->parse($html);
132              
133             # Now that we have parsed the methods, let us go ahead and build the argspec based on the anchors for each endpoint.
134 0           foreach my $m (@$parse) {
135 0           $method = $m;
136 0           %state = ();
137 0           my $mparser = HTML::Parser->new(
138             handlers => {
139             start => [ \&_endpoint_open, "tagname,attr" ],
140             end => [ \&_endpoint_close, "tagname" ],
141             text => [ \&_endpoint_text, "text" ],
142             },
143             );
144 0           $mparser->parse($html);
145             }
146              
147 0           return _fixup( \%spec, $parse );
148             }
149              
150 0     0     sub _fixup ( $spec, $parse ) {
  0            
  0            
  0            
151             @$parse = map {
152 0           $_->{href} = "$spec->{url}$_->{href}";
  0            
153              
154             #XXX correct TYPO in the spec
155 0           $_->{uri} =~ s/{sessionid\)/{sessionid}/g;
156 0           @{ $_->{output_params} } = grep { $_ ne 'null' } uniq @{ $_->{output_params} };
  0            
  0            
  0            
157 0           $_
158             } @$parse;
159              
160 0           return $parse;
161             }
162              
163 0     0     sub _handle_open ( $tag, $attr ) {
  0            
  0            
  0            
164              
165 0 0 0       if ( $tag eq 'section' && ( $attr->{id} || '' ) eq $state{id} ) {
      0        
166 0           $state{active} = 1;
167 0           return;
168             }
169 0 0         if ( $tag eq 'tr' ) {
170 0           $state{method} = 1;
171 0           $state{headers} = [qw{method uri name}];
172 0           $state{data} = {};
173 0           return;
174             }
175 0 0         if ( $tag eq 'td' ) {
176 0           $state{heading} = shift @{ $state{headers} };
  0            
177 0           return;
178             }
179 0 0 0       if ( $tag eq 'a' && $state{heading} && $attr->{href} ) {
      0        
180 0           $state{data}{href} = $attr->{href};
181             }
182             }
183              
184 0     0     sub _handle_close ($tag) {
  0            
  0            
185 0 0         if ( $tag eq 'section' ) {
186 0           $state{active} = 0;
187 0           return;
188             }
189 0 0 0       if ( $tag eq 'tr' && $state{active} ) {
190 0 0         if ( $state{past_first} ) {
191 0           push( @$parse, $state{data} );
192             }
193              
194 0           $state{past_first} = 1;
195 0           $state{method} = 0;
196 0           return;
197             }
198             }
199              
200 0     0     sub _handle_text ($text) {
  0            
  0            
201 0 0 0       return unless $state{active} && $state{method} && $state{past_first} && $state{heading};
      0        
      0        
202 0           $text =~ s/\s//gm;
203 0 0         return unless $text;
204 0           $state{data}{ $state{heading} } .= $text;
205             }
206              
207             # Endpoint parsers
208              
209 0     0     sub _endpoint_open ( $tag, $attr ) {
  0            
  0            
  0            
210 0           my $id = $method->{href};
211 0           $id =~ s/^#//;
212              
213 0 0 0       if ( $attr->{id} && $attr->{id} eq $id ) {
214 0           $state{active} = 1;
215             }
216 0 0         if ( $tag eq 'ol' ) {
217 0           $state{in_tag} = 1;
218             }
219 0 0 0       if ( $tag eq 'dt' && $state{in_tag} && $state{last_tag} eq 'dl' ) {
      0        
220 0           $state{in_dt} = 1;
221             }
222 0 0 0       if ( $tag eq 'code' && $state{in_dt} && $state{in_tag} && $state{last_tag} eq 'dt' ) {
      0        
      0        
223 0           $state{in_code} = 1;
224             }
225              
226 0           $state{last_tag} = $tag;
227             }
228              
229 0     0     sub _endpoint_close ($tag) {
  0            
  0            
230 0 0         return unless $state{active};
231 0 0         if ( $tag eq 'section' ) {
232 0           $state{active} = 0;
233 0           $state{in_tag} = 0;
234             }
235 0 0         if ( $tag eq 'ol' ) {
236 0           $state{in_tag} = 0;
237             }
238 0 0         if ( $tag eq 'dt' ) {
239 0           $state{in_dt} = 0;
240             }
241 0 0         if ( $tag eq 'code' ) {
242 0           $state{in_code} = 0;
243             }
244             }
245              
246 0     0     sub _endpoint_text ($text) {
  0            
  0            
247 0 0 0       if ( $state{active} && $state{in_tag} && $state{in_code} && $state{in_dt} && $state{last_tag} eq 'code' ) {
      0        
      0        
      0        
248 0   0       $method->{output_params} //= [];
249 0           $text =~ s/\s//gm;
250 0 0         push( @{ $method->{output_params} }, $text ) if $text;
  0            
251             }
252             }
253              
254             1;
255              
256             =pod
257              
258             =encoding UTF-8
259              
260             =head1 NAME
261              
262             Selenium::Specification - Module for building a machine readable specification for Selenium
263              
264             =head1 VERSION
265              
266             version 2.01
267              
268             =head1 SUBROUTINES
269              
270             =head2 read($client_dir STRING, $type STRING, $nofetch BOOL, $hardcoe BOOL)
271              
272             Reads the copy of the provided spec type, and fetches it if a cached version is not available.
273              
274             If hardcode is passed we use the JSON in the DATA section below.
275              
276             =head2 fetch(%OPTIONS HASH)
277              
278             Builds a spec hash based upon the WC3 specification documents, and writes it to disk.
279              
280             =head1 SEE ALSO
281              
282             Please see those modules/websites for more information related to this module.
283              
284             =over 4
285              
286             =item *
287              
288             L<Selenium::Client|Selenium::Client>
289              
290             =back
291              
292             =head1 BUGS
293              
294             Please report any bugs or feature requests on the bugtracker website
295             L<https://github.com/troglodyne-internet-widgets/selenium-client-perl/issues>
296              
297             When submitting a bug or request, please include a test-file or a
298             patch to an existing test-file that illustrates the bug or desired
299             feature.
300              
301             =head1 AUTHORS
302              
303             Current Maintainers:
304              
305             =over 4
306              
307             =item *
308              
309             George S. Baugh <george@troglodyne.net>
310              
311             =back
312              
313             =head1 COPYRIGHT AND LICENSE
314              
315             Copyright (c) 2024 Troglodyne LLC
316              
317              
318             Permission is hereby granted, free of charge, to any person obtaining a copy
319             of this software and associated documentation files (the "Software"), to deal
320             in the Software without restriction, including without limitation the rights
321             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
322             copies of the Software, and to permit persons to whom the Software is
323             furnished to do so, subject to the following conditions:
324             The above copyright notice and this permission notice shall be included in all
325             copies or substantial portions of the Software.
326             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
327             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
328             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
329             AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
330             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
331             OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
332             SOFTWARE.
333              
334             =cut
335              
336             __DATA__
337             [{"output_params":["capabilities","sessionId"],"href":"https://www.w3.org/TR/webdriver1/#dfn-creating-a-new-session","uri":"/session","name":"NewSession","method":"POST"},{"href":"https://www.w3.org/TR/webdriver1/#dfn-delete-session","output_params":[],"uri":"/session/{sessionid}","method":"DELETE","name":"DeleteSession"},{"method":"GET","name":"Status","uri":"/status","href":"https://www.w3.org/TR/webdriver1/#dfn-status","output_params":["ready"]},{"method":"GET","name":"GetTimeouts","uri":"/session/{sessionid}/timeouts","output_params":["script"],"href":"https://www.w3.org/TR/webdriver1/#dfn-get-timeouts"},{"output_params":["script"],"href":"https://www.w3.org/TR/webdriver1/#dfn-timeouts","name":"SetTimeouts","method":"POST","uri":"/session/{sessionid}/timeouts"},{"method":"POST","name":"NavigateTo","uri":"/session/{sessionid}/url","href":"https://www.w3.org/TR/webdriver1/#dfn-navigate-to","output_params":["https://example.com"]},{"href":"https://www.w3.org/TR/webdriver1/#dfn-get-current-url","output_params":[],"method":"GET","name":"GetCurrentURL","uri":"/session/{sessionid}/url"},{"href":"https://www.w3.org/TR/webdriver1/#dfn-back","output_params":["window.history.back"],"uri":"/session/{sessionid}/back","method":"POST","name":"Back"},{"output_params":["pageHide"],"href":"https://www.w3.org/TR/webdriver1/#dfn-forward","name":"Forward","method":"POST","uri":"/session/{sessionid}/forward"},{"output_params":["file"],"href":"https://www.w3.org/TR/webdriver1/#dfn-refresh","method":"POST","name":"Refresh","uri":"/session/{sessionid}/refresh"},{"href":"https://www.w3.org/TR/webdriver1/#dfn-get-title","output_params":["document.title"],"uri":"/session/{sessionid}/title","name":"GetTitle","method":"GET"},{"method":"GET","name":"GetWindowHandle","uri":"/session/{sessionid}/window","href":"https://www.w3.org/TR/webdriver1/#dfn-get-window-handle","output_params":[]},{"output_params":[],"href":"https://www.w3.org/TR/webdriver1/#dfn-close-window","name":"CloseWindow","method":"DELETE","uri":"/session/{sessionid}/window"},{"name":"SwitchToWindow","method":"POST","uri":"/session/{sessionid}/window","output_params":["handle"],"href":"https://www.w3.org/TR/webdriver1/#dfn-switch-to-window"},{"uri":"/session/{sessionid}/window/handles","name":"GetWindowHandles","method":"GET","output_params":[],"href":"https://www.w3.org/TR/webdriver1/#dfn-get-window-handles"},{"name":"SwitchToFrame","method":"POST","uri":"/session/{sessionid}/frame","output_params":["id"],"href":"https://www.w3.org/TR/webdriver1/#dfn-switch-to-frame"},{"uri":"/session/{sessionid}/frame/parent","name":"SwitchToParentFrame","method":"POST","href":"https://www.w3.org/TR/webdriver1/#dfn-switch-to-parent-frame","output_params":[]},{"uri":"/session/{sessionid}/window/rect","method":"GET","name":"GetWindowRect","output_params":[],"href":"https://www.w3.org/TR/webdriver1/#dfn-get-window-rect"},{"href":"https://www.w3.org/TR/webdriver1/#dfn-set-window-rect","output_params":["width"],"name":"SetWindowRect","method":"POST","uri":"/session/{sessionid}/window/rect"},{"uri":"/session/{sessionid}/window/maximize","method":"POST","name":"MaximizeWindow","href":"https://www.w3.org/TR/webdriver1/#dfn-maximize-window","output_params":[]},{"output_params":[],"href":"https://www.w3.org/TR/webdriver1/#dfn-minimize-window","name":"MinimizeWindow","method":"POST","uri":"/session/{sessionid}/window/minimize"},{"method":"POST","name":"FullscreenWindow","uri":"/session/{sessionid}/window/fullscreen","href":"https://www.w3.org/TR/webdriver1/#dfn-fullscreen-window","output_params":[]},{"uri":"/session/{sessionid}/element/active","method":"GET","name":"GetActiveElement","href":"https://www.w3.org/TR/webdriver1/#dfn-get-active-element","output_params":[]},{"output_params":["#toremove"],"href":"https://www.w3.org/TR/webdriver1/#dfn-find-element","uri":"/session/{sessionid}/element","method":"POST","name":"FindElement"},{"href":"https://www.w3.org/TR/webdriver1/#dfn-find-elements","output_params":["using"],"name":"FindElements","method":"POST","uri":"/session/{sessionid}/elements"},{"uri":"/session/{sessionid}/element/{elementid}/element","name":"FindElementFromElement","method":"POST","href":"https://www.w3.org/TR/webdriver1/#dfn-find-element-from-element","output_params":["using"]},{"method":"POST","name":"FindElementsFromElement","uri":"/session/{sessionid}/element/{elementid}/elements","output_params":["using"],"href":"https://www.w3.org/TR/webdriver1/#dfn-find-elements-from-element"},{"href":"https://www.w3.org/TR/webdriver1/#dfn-is-element-selected","output_params":["input"],"uri":"/session/{sessionid}/element/{elementid}/selected","name":"IsElementSelected","method":"GET"},{"output_params":[],"href":"https://www.w3.org/TR/webdriver1/#dfn-get-element-attribute","method":"GET","name":"GetElementAttribute","uri":"/session/{sessionid}/element/{elementid}/attribute/{name}"},{"href":"https://www.w3.org/TR/webdriver1/#dfn-get-element-property","output_params":[],"uri":"/session/{sessionid}/element/{elementid}/property/{name}","name":"GetElementProperty","method":"GET"},{"name":"GetElementCSSValue","method":"GET","uri":"/session/{sessionid}/element/{elementid}/css/{propertyname}","href":"https://www.w3.org/TR/webdriver1/#dfn-get-element-css-value","output_params":["xml"]},{"name":"GetElementText","method":"GET","uri":"/session/{sessionid}/element/{elementid}/text","href":"https://www.w3.org/TR/webdriver1/#dfn-get-element-text","output_params":["a"]},{"uri":"/session/{sessionid}/element/{elementid}/name","name":"GetElementTagName","method":"GET","output_params":[],"href":"https://www.w3.org/TR/webdriver1/#dfn-get-element-tag-name"},{"method":"GET","name":"GetElementRect","uri":"/session/{sessionid}/element/{elementid}/rect","href":"https://www.w3.org/TR/webdriver1/#dfn-get-element-rect","output_params":["x"]},{"method":"GET","name":"IsElementEnabled","uri":"/session/{sessionid}/element/{elementid}/enabled","href":"https://www.w3.org/TR/webdriver1/#dfn-is-element-enabled","output_params":["xml"]},{"output_params":["input"],"href":"https://www.w3.org/TR/webdriver1/#dfn-element-click","uri":"/session/{sessionid}/element/{elementid}/click","method":"POST","name":"ElementClick"},{"uri":"/session/{sessionid}/element/{elementid}/clear","name":"ElementClear","method":"POST","output_params":["innerHTML"],"href":"https://www.w3.org/TR/webdriver1/#dfn-element-clear"},{"method":"POST","name":"ElementSendKeys","uri":"/session/{sessionid}/element/{elementid}/value","href":"https://www.w3.org/TR/webdriver1/#dfn-element-send-keys","output_params":["type"]},{"href":"https://www.w3.org/TR/webdriver1/#dfn-get-page-source","output_params":["true"],"uri":"/session/{sessionid}/source","method":"GET","name":"GetPageSource"},{"method":"POST","name":"ExecuteScript","uri":"/session/{sessionid}/execute/sync","href":"https://www.w3.org/TR/webdriver1/#dfn-execute-script","output_params":[]},{"uri":"/session/{sessionid}/execute/async","name":"ExecuteAsyncScript","method":"POST","output_params":["unset"],"href":"https://www.w3.org/TR/webdriver1/#dfn-execute-async-script"},{"method":"GET","name":"GetAllCookies","uri":"/session/{sessionid}/cookie","href":"https://www.w3.org/TR/webdriver1/#dfn-get-all-cookies","output_params":[]},{"uri":"/session/{sessionid}/cookie/{name}","name":"GetNamedCookie","method":"GET","output_params":[],"href":"https://www.w3.org/TR/webdriver1/#dfn-get-named-cookie"},{"href":"https://www.w3.org/TR/webdriver1/#dfn-adding-a-cookie","output_params":["cookie"],"method":"POST","name":"AddCookie","uri":"/session/{sessionid}/cookie"},{"uri":"/session/{sessionid}/cookie/{name}","name":"DeleteCookie","method":"DELETE","href":"https://www.w3.org/TR/webdriver1/#dfn-delete-cookie","output_params":[]},{"uri":"/session/{sessionid}/cookie","name":"DeleteAllCookies","method":"DELETE","output_params":[],"href":"https://www.w3.org/TR/webdriver1/#dfn-delete-all-cookies"},{"uri":"/session/{sessionid}/actions","name":"PerformActions","method":"POST","output_params":[],"href":"https://www.w3.org/TR/webdriver1/#dfn-perform-actions"},{"href":"https://www.w3.org/TR/webdriver1/#dfn-release-actions","output_params":[],"name":"ReleaseActions","method":"DELETE","uri":"/session/{sessionid}/actions"},{"href":"https://www.w3.org/TR/webdriver1/#dfn-dismiss-alert","output_params":[],"uri":"/session/{sessionid}/alert/dismiss","name":"DismissAlert","method":"POST"},{"output_params":[],"href":"https://www.w3.org/TR/webdriver1/#dfn-accept-alert","uri":"/session/{sessionid}/alert/accept","method":"POST","name":"AcceptAlert"},{"output_params":[],"href":"https://www.w3.org/TR/webdriver1/#dfn-get-alert-text","uri":"/session/{sessionid}/alert/text","name":"GetAlertText","method":"GET"},{"href":"https://www.w3.org/TR/webdriver1/#dfn-send-alert-text","output_params":["prompt"],"uri":"/session/{sessionid}/alert/text","method":"POST","name":"SendAlertText"},{"href":"https://www.w3.org/TR/webdriver1/#dfn-take-screenshot","output_params":["canvas"],"uri":"/session/{sessionid}/screenshot","name":"TakeScreenshot","method":"GET"},{"href":"https://www.w3.org/TR/webdriver1/#dfn-error-code","output_params":["error"],"uri":"/session/{sessionid}/element/{elementid}/screenshot","method":"GET","name":"TakeElementScreenshot"}]