New Upstream Release - libfirefox-marionette-perl

Ready changes

Summary

Merged new upstream version: 1.38 (was: 1.35).

Diff

diff --git a/Changes b/Changes
index bec6aac..e04c025 100644
--- a/Changes
+++ b/Changes
@@ -1,5 +1,18 @@
 Revision history for Firefox-Marionette
 
+1.38  Sun May 28 06:57 2023
+       Fixing ssh-auth-cmd-marionette for FreeBSD in GH#24.  Thanks to prozorecJP
+       Adding cache_keys, check_cache_key and clear_cache methods
+       Renaming download method to downloaded
+       Test coverage improvements and related fixes
+
+1.37  Sun Apr 30 19:12 2023
+       Another test suite fix
+
+1.36  Sun Apr 30 15:59 2023
+       Fixes to cope with Firefox 112 capabilities changes.  Thanks to toreau
+       Documentation and test suite fixes
+
 1.35  Sat Jan 21 21:04 2023
        Documentation and test suite fixes
 
diff --git a/MANIFEST b/MANIFEST
index 602e86a..eb618bf 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -1,6 +1,7 @@
 Changes
 lib/Firefox/Marionette.pm
 lib/Firefox/Marionette/Buttons.pm
+lib/Firefox/Marionette/Cache.pm
 lib/Firefox/Marionette/Capabilities.pm
 lib/Firefox/Marionette/Certificate.pm
 lib/Firefox/Marionette/Cookie.pm
@@ -54,11 +55,22 @@ t/data/last_pass_example.csv
 t/00.load.t
 t/01-marionette.t
 t/02-taint.t
+t/03-close.t
+t/03-closedir.t
+t/03-fork.t
+t/03-mkdir.t
+t/03-opendir.t
+t/03-read.t
+t/03-seek.t
+t/03-stat.t
+t/03-sysopen.t
 t/manifest.t
 t/pod-coverage.t
 t/pod.t
 t/addons/test.xpi
 t/addons/discogs-search/manifest.json
 t/addons/discogs-search/README.md
+t/stub.pl
+t/syscall_tests.pm
 META.yml                                 Module YAML meta-data (added by MakeMaker)
 META.json                                Module JSON meta-data (added by MakeMaker)
diff --git a/META.json b/META.json
index 3817625..4c01b42 100644
--- a/META.json
+++ b/META.json
@@ -86,6 +86,6 @@
          "web" : "https://github.com/david-dick/firefox-marionette"
       }
    },
-   "version" : "1.35",
+   "version" : "1.38",
    "x_serialization_backend" : "JSON::PP version 4.11"
 }
diff --git a/META.yml b/META.yml
index 9143c13..f6c2973 100644
--- a/META.yml
+++ b/META.yml
@@ -61,5 +61,5 @@ requires:
 resources:
   bugtracker: https://github.com/david-dick/firefox-marionette/issues
   repository: https://github.com/david-dick/firefox-marionette
-version: '1.35'
+version: '1.38'
 x_serialization_backend: 'CPAN::Meta::YAML version 0.018'
diff --git a/README b/README
index 7f23328..ed0e197 100644
--- a/README
+++ b/README
@@ -5,7 +5,7 @@ NAME
 
 VERSION
 
-    Version 1.35
+    Version 1.38
 
 SYNOPSIS
 
@@ -299,6 +299,21 @@ SUBROUTINES/METHODS
     
         $firefox->bye(sub { $firefox->find_name('metacpan_search-input') })->await(sub { $firefox->interactive() && $firefox->find_partial('Download') })->click();
 
+ cache_keys
+
+    returns the set of all cache keys from Firefox::Marionette::Cache.
+
+        use Firefox::Marionette();
+    
+        my $firefox = Firefox::Marionette->new();
+        foreach my $key_name ($firefox->cache_keys()) {
+          my $key_value = $firefox->check_cache_key($key_name);
+          if (Firefox::Marionette::Cache->$key_name() != $key_value) {
+            warn "This module this the value of $key_name is " . Firefox::Marionette::Cache->$key_name();
+            warn "Firefox thinks the value of   $key_name is $key_value";
+          }
+        }
+
  capabilities
 
     returns the capabilities of the current firefox binary. You can
@@ -342,6 +357,26 @@ SUBROUTINES/METHODS
 
     This method returns itself to aid in chaining methods.
 
+ check_cache_key
+
+    accepts a cache_key as a parameter.
+
+        use Firefox::Marionette();
+    
+        my $firefox = Firefox::Marionette->new();
+        foreach my $key_name ($firefox->cache_keys()) {
+          my $key_value = $firefox->check_cache_key($key_name);
+          if (Firefox::Marionette::Cache->$key_name() != $key_value) {
+            warn "This module this the value of $key_name is " . Firefox::Marionette::Cache->$key_name();
+            warn "Firefox thinks the value of   $key_name is $key_value";
+          }
+        }
+
+    This method returns the cache_key's actual value from firefox as a
+    number. This may differ from the current value of the key from
+    Firefox::Marionette::Cache as these values have changed as firefox has
+    evolved.
+
  child_error
 
     This method returns the $? (CHILD_ERROR) for the Firefox process, or
@@ -386,6 +421,23 @@ SUBROUTINES/METHODS
     accepts a element as the first parameter and clears any user supplied
     input
 
+ clear_cache
+
+    accepts a single flag parameter, which can be an ORed set of keys from
+    Firefox::Marionette::Cache and clears the appropriate sections of the
+    cache. If no flags parameter is supplied, the default is CLEAR_ALL.
+    Note that this method, unlike delete_cookies will actually delete all
+    cookies for all hosts, not just the current webpage.
+
+        use Firefox::Marionette();
+        use Firefox::Marionette::Cache qw(:all);
+    
+        my $firefox = Firefox::Marionette->new()->go('https://do.lots.of.evil/')->clear_cache(); # default clear all
+    
+        $firefox->go('https://cookies.r.us')->clear_cache(CLEAR_COOKIES());
+
+    This method returns itself to aid in chaining methods.
+
  clear_pref
 
     accepts a preference <http://kb.mozillazine.org/About:config> name and
@@ -534,8 +586,9 @@ SUBROUTINES/METHODS
 
  delete_cookies
 
-    here be cookie monsters! This method returns itself to aid in chaining
-    methods.
+    Here be cookie monsters! Note that this method will only delete cookies
+    for the current site. See clear_cache for an alternative. This method
+    returns itself to aid in chaining methods.
 
  delete_header
 
@@ -643,7 +696,7 @@ SUBROUTINES/METHODS
             }
         }
 
- download
+ downloaded
 
     accepts a filesystem path and returns a matching filehandle. This is
     trivial for locally running firefox, but sufficiently complex to
@@ -664,7 +717,7 @@ SUBROUTINES/METHODS
     
         foreach my $path ($firefox->downloads()) {
     
-            my $handle = $firefox->download($path);
+            my $handle = $firefox->downloaded($path);
     
             # do something with downloaded file handle
     
@@ -2979,11 +3032,8 @@ ACKNOWLEDGEMENTS
       * Marionette Protocol
       <https://firefox-source-docs.mozilla.org/testing/marionette/marionette/index.html>
 
-      * Marionette Documentation
-      <https://firefox-source-docs.mozilla.org/testing/marionette/marionette/index.html>
-
       * Marionette driver.js
-      <https://hg.mozilla.org/mozilla-central/file/tip/remote/marionette/driver.js>
+      <https://hg.mozilla.org/mozilla-central/file/tip/remote/marionette/driver.sys.mjs>
 
       * about:config <http://kb.mozillazine.org/About:config_entries>
 
diff --git a/README.md b/README.md
index 3be5ad6..924fca9 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@ Firefox::Marionette - Automate the Firefox browser with the Marionette protocol
 
 # VERSION
 
-Version 1.35
+Version 1.38
 
 # SYNOPSIS
 
@@ -224,6 +224,21 @@ accepts a subroutine reference as a parameter and then executes the subroutine.
 
     $firefox->bye(sub { $firefox->find_name('metacpan_search-input') })->await(sub { $firefox->interactive() && $firefox->find_partial('Download') })->click();
 
+## cache\_keys
+
+returns the set of all cache keys from [Firefox::Marionette::Cache](https://metacpan.org/pod/Firefox::Marionette::Cache).
+
+    use Firefox::Marionette();
+
+    my $firefox = Firefox::Marionette->new();
+    foreach my $key_name ($firefox->cache_keys()) {
+      my $key_value = $firefox->check_cache_key($key_name);
+      if (Firefox::Marionette::Cache->$key_name() != $key_value) {
+        warn "This module this the value of $key_name is " . Firefox::Marionette::Cache->$key_name();
+        warn "Firefox thinks the value of   $key_name is $key_value";
+      }
+    }
+
 ## capabilities
 
 returns the [capabilities](https://metacpan.org/pod/Firefox::Marionette::Capabilities) of the current firefox binary.  You can retrieve [timeouts](https://metacpan.org/pod/Firefox::Marionette::Timeouts) or a [proxy](https://metacpan.org/pod/Firefox::Marionette::Proxy) with this method.
@@ -263,6 +278,23 @@ returns a list of all known [certificates in the Firefox database](https://metac
 
 This method returns [itself](https://metacpan.org/pod/Firefox::Marionette) to aid in chaining methods.
 
+## check\_cache\_key
+
+accepts a [cache\_key](https://metacpan.org/pod/Firefox::Marionette::Cache) as a parameter.
+
+    use Firefox::Marionette();
+
+    my $firefox = Firefox::Marionette->new();
+    foreach my $key_name ($firefox->cache_keys()) {
+      my $key_value = $firefox->check_cache_key($key_name);
+      if (Firefox::Marionette::Cache->$key_name() != $key_value) {
+        warn "This module this the value of $key_name is " . Firefox::Marionette::Cache->$key_name();
+        warn "Firefox thinks the value of   $key_name is $key_value";
+      }
+    }
+
+This method returns the [cache\_key](https://metacpan.org/pod/Firefox::Marionette::Cache)'s actual value from firefox as a number.  This may differ from the current value of the key from [Firefox::Marionette::Cache](https://metacpan.org/pod/Firefox::Marionette::Cache) as these values have changed as firefox has evolved.
+
 ## child\_error
 
 This method returns the $? (CHILD\_ERROR) for the Firefox process, or undefined if the process has not yet exited.
@@ -292,6 +324,19 @@ returns identifiers for each open chrome window for tests interested in managing
 
 accepts a [element](https://metacpan.org/pod/Firefox::Marionette::Element) as the first parameter and clears any user supplied input
 
+## clear\_cache
+
+accepts a single flag parameter, which can be an ORed set of keys from [Firefox::Marionette::Cache](https://metacpan.org/pod/Firefox::Marionette::Cache) and clears the appropriate sections of the cache.  If no flags parameter is supplied, the default is [CLEAR\_ALL](https://metacpan.org/pod/Firefox::Marionette::Cache#CLEAR_ALL).  Note that this method, unlike [delete\_cookies](#delete_cookies) will actually delete all cookies for all hosts, not just the current webpage.
+
+    use Firefox::Marionette();
+    use Firefox::Marionette::Cache qw(:all);
+
+    my $firefox = Firefox::Marionette->new()->go('https://do.lots.of.evil/')->clear_cache(); # default clear all
+
+    $firefox->go('https://cookies.r.us')->clear_cache(CLEAR_COOKIES());
+
+This method returns [itself](https://metacpan.org/pod/Firefox::Marionette) to aid in chaining methods.
+
 ## clear\_pref
 
 accepts a [preference](http://kb.mozillazine.org/About:config) name and restores it to the original value.  See the [get\_pref](https://metacpan.org/pod/Firefox::Marionette#get_pref) and [set\_pref](https://metacpan.org/pod/Firefox::Marionette#set_pref) methods to get a preference value and to set to it to a particular value.  This method returns [itself](https://metacpan.org/pod/Firefox::Marionette) to aid in chaining methods.
@@ -419,7 +464,7 @@ deletes a single cookie by name.  Accepts a scalar containing the cookie name as
 
 ## delete\_cookies
 
-here be cookie monsters! This method returns [itself](https://metacpan.org/pod/Firefox::Marionette) to aid in chaining methods.
+Here be cookie monsters! Note that this method will only delete cookies for the current site.  See [clear\_cache](#clear_cache) for an alternative.  This method returns [itself](https://metacpan.org/pod/Firefox::Marionette) to aid in chaining methods. 
 
 ## delete\_header
 
@@ -508,7 +553,7 @@ accepts an optional regex to filter against the [usage for the display](https://
         }
     }
 
-## download
+## downloaded
 
 accepts a filesystem path and returns a matching filehandle.  This is trivial for locally running firefox, but sufficiently complex to justify the method for a remote firefox running over ssh.
 
@@ -527,7 +572,7 @@ accepts a filesystem path and returns a matching filehandle.  This is trivial fo
 
     foreach my $path ($firefox->downloads()) {
 
-        my $handle = $firefox->download($path);
+        my $handle = $firefox->downloaded($path);
 
         # do something with downloaded file handle
 
@@ -2150,8 +2195,7 @@ Thanks to [Mike Kaply](https://mike.kaply.com/about/) for his [post](https://mik
 Thanks also to the authors of the documentation in the following sources;
 
 - [Marionette Protocol](https://firefox-source-docs.mozilla.org/testing/marionette/marionette/index.html)
-- [Marionette Documentation](https://firefox-source-docs.mozilla.org/testing/marionette/marionette/index.html)
-- [Marionette driver.js](https://hg.mozilla.org/mozilla-central/file/tip/remote/marionette/driver.js)
+- [Marionette driver.js](https://hg.mozilla.org/mozilla-central/file/tip/remote/marionette/driver.sys.mjs)
 - [about:config](http://kb.mozillazine.org/About:config_entries)
 - [nsIPrefService interface](https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPrefService)
 
diff --git a/ca-bundle-for-firefox b/ca-bundle-for-firefox
index 053f989..c918c37 100755
--- a/ca-bundle-for-firefox
+++ b/ca-bundle-for-firefox
@@ -8,7 +8,7 @@ use Firefox::Marionette();
 use FileHandle();
 use Encode();
 
-our $VERSION = '1.35';
+our $VERSION = '1.38';
 
 my %options;
 
@@ -115,7 +115,7 @@ ca-bundle-for-firefox - generate the ca-bundle.crt for the current firefox insta
 
 =head1 VERSION
 
-Version 1.35
+Version 1.38
 
 =head1 USAGE
 
diff --git a/check-firefox-certificate-authorities b/check-firefox-certificate-authorities
index 6e44b5f..dcd553b 100755
--- a/check-firefox-certificate-authorities
+++ b/check-firefox-certificate-authorities
@@ -7,7 +7,7 @@ use English qw( -no_match_vars );
 use Encode();
 use Firefox::Marionette();
 
-our $VERSION = '1.35';
+our $VERSION = '1.38';
 
 sub _NUMBER_OF_SPACES_FOR_CODE_QUOTING_IN_MARKDOWN { return 4 }
 
@@ -101,7 +101,7 @@ check-firefox-certificate-authorities - check the CA certificates in firefox for
 
 =head1 VERSION
 
-Version 1.35
+Version 1.38
 
 =head1 USAGE
 
diff --git a/debian/changelog b/debian/changelog
index 5572db8..91e6734 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+libfirefox-marionette-perl (1.38-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Tue, 13 Jun 2023 19:14:53 -0000
+
 libfirefox-marionette-perl (1.35-1) unstable; urgency=medium
 
   * Import upstream version 1.35.
diff --git a/debian/patches/no-network.patch b/debian/patches/no-network.patch
index 8c05e35..8a7330b 100644
--- a/debian/patches/no-network.patch
+++ b/debian/patches/no-network.patch
@@ -4,9 +4,11 @@ Forwarded: not-needed
 Author: gregor herrmann <gregoa@debian.org>
 Last-Update: 2022-12-16
 
---- a/t/01-marionette.t
-+++ b/t/01-marionette.t
-@@ -879,7 +879,7 @@
+Index: libfirefox-marionette-perl.git/t/01-marionette.t
+===================================================================
+--- libfirefox-marionette-perl.git.orig/t/01-marionette.t
++++ libfirefox-marionette-perl.git/t/01-marionette.t
+@@ -885,7 +885,7 @@ SKIP: {
  	ok(not($firefox->script('window.open("about:blank", "_blank");')), "Opening new window to about:blank via 'window.open' script");
  	ok($firefox->close_current_window_handle(), "Closed new tab/window");
  	SKIP: {
@@ -15,7 +17,7 @@ Last-Update: 2022-12-16
  			skip("Deleting and re-creating sessions can hang firefox for old versions", 1);
  		}
  		ok($firefox->delete_session()->new_session(), "\$firefox->delete_session()->new_session() has cleared the old session and created a new session");
-@@ -2084,7 +2084,7 @@
+@@ -2162,7 +2162,7 @@ SKIP: {
  	if (!$skip_message) {
  		$at_least_one_success = 1;
  	}
diff --git a/firefox-passwords b/firefox-passwords
index f7bd7c6..711784b 100755
--- a/firefox-passwords
+++ b/firefox-passwords
@@ -14,7 +14,7 @@ use XML::Parser();
 use Term::ReadKey();
 use charnames ':full';
 
-our $VERSION = '1.35';
+our $VERSION = '1.38';
 
 sub _NUMBER_OF_BYTES_FOR_ZIP_MAGIC_NUMBER { return 4 }
 
@@ -313,7 +313,7 @@ firefox-passwords - import and export passwords from firefox
 
 =head1 VERSION
 
-Version 1.35
+Version 1.38
 
 =head1 USAGE
 
diff --git a/lib/Firefox/Marionette.pm b/lib/Firefox/Marionette.pm
index 6480709..31ebeee 100644
--- a/lib/Firefox/Marionette.pm
+++ b/lib/Firefox/Marionette.pm
@@ -4,6 +4,7 @@ use warnings;
 use strict;
 use Firefox::Marionette::Response();
 use Firefox::Marionette::Element();
+use Firefox::Marionette::Cache();
 use Firefox::Marionette::Cookie();
 use Firefox::Marionette::Display();
 use Firefox::Marionette::Window::Rect();
@@ -63,7 +64,7 @@ our @EXPORT_OK =
   qw(BY_XPATH BY_ID BY_NAME BY_TAG BY_CLASS BY_SELECTOR BY_LINK BY_PARTIAL);
 our %EXPORT_TAGS = ( all => \@EXPORT_OK );
 
-our $VERSION = '1.35';
+our $VERSION = '1.38';
 
 sub _ANYPROCESS                     { return -1 }
 sub _COMMAND                        { return 0 }
@@ -256,6 +257,77 @@ _JS_
     return $self;
 }
 
+sub _clear_data_service_interface_preamble {
+    my ($self) = @_;
+    return <<'_JS_';    # toolkit/components/cleardata/nsIClearDataService.idl
+let clearDataService = Components.classes["@mozilla.org/clear-data-service;1"].getService(Components.interfaces.nsIClearDataService);
+_JS_
+}
+
+sub cache_keys {
+    my ($self) = @_;
+    my @names;
+    foreach my $name (@Firefox::Marionette::Cache::EXPORT_OK) {
+        if ( defined $self->check_cache_key($name) ) {
+            push @names, $name;
+        }
+    }
+    return @names;
+}
+
+sub check_cache_key {
+    my ( $self, $name ) = @_;
+    my $class = ref $self;
+    defined $name
+      or Firefox::Marionette::Exception->throw(
+        "$class->check_cache_value() must be passed an argument.");
+    $name =~ /^[[:upper:]_]+$/smx
+      or Firefox::Marionette::Exception->throw(
+"$class->check_cache_key() must be passed an argument consisting of uppercase characters and underscores."
+      );
+    my $script = <<"_JS_";
+if (typeof clearDataService.$name === undefined) {
+  return;
+} else {
+  return clearDataService.$name;
+}
+_JS_
+    my $old    = $self->_context('chrome');
+    my $result = $self->script(
+        $self->_compress_script(
+            $self->_clear_data_service_interface_preamble() . $script
+        )
+    );
+    $self->_context($old);
+    return $result;
+}
+
+sub clear_cache {
+    my ( $self, $flags ) = @_;
+    $flags = defined $flags ? $flags : Firefox::Marionette::Cache::CLEAR_ALL();
+    my $script = <<'_JS_';
+let argument_flags = arguments[0];
+let clearCache = function(flags) {
+  return new Promise((resolve) => {
+    clearDataService.deleteData(flags, function() { resolve(); });
+  })};
+let result = (async function() {
+  let awaitResult = await clearCache(argument_flags);
+  return awaitResult;
+})();
+return arguments[0];
+_JS_
+    my $old    = $self->_context('chrome');
+    my $result = $self->script(
+        $self->_compress_script(
+            $self->_clear_data_service_interface_preamble() . $script
+        ),
+        args => [$flags]
+    );
+    $self->_context($old);
+    return $self;
+}
+
 sub clear_pref {
     my ( $self, $name ) = @_;
     my $script = <<'_JS_';
@@ -287,6 +359,14 @@ sub mime_types {
 }
 
 sub download {
+    my ( $self, $path ) = @_;
+    Carp::carp( '**** DEPRECATED - The download(' . q[$]
+          . 'path) method HAS BEEN REPLACED BY downloaded(' . q[$]
+          . 'path) ****' );
+    return $self->downloaded($path);
+}
+
+sub downloaded {
     my ( $self, $path ) = @_;
     my $handle;
     if ( my $ssh = $self->_ssh() ) {
@@ -339,7 +419,7 @@ sub _directory_listing {
     else {
         my $handle = DirHandle->new($directory);
         if ($handle) {
-            while ( my $entry = $handle->read() ) {
+            while ( defined( my $entry = $handle->read() ) ) {
                 next if ( $entry eq File::Spec->updir() );
                 next if ( $entry eq File::Spec->curdir() );
                 if ($short) {
@@ -349,7 +429,7 @@ sub _directory_listing {
                     push @entries, File::Spec->catfile( $directory, $entry );
                 }
             }
-            $handle->close()
+            closedir $handle
               or Firefox::Marionette::Exception->throw(
                 "Failed to close directory '$directory':$EXTENDED_OS_ERROR");
         }
@@ -448,7 +528,7 @@ sub _get_max_scp_file_index {
             }
         }
     }
-    $directory_handle->close()
+    closedir $directory_handle
       or Firefox::Marionette::Exception->throw(
         "Failed to close directory '$directory_path':$EXTENDED_OS_ERROR");
     return $maximum_index;
@@ -536,7 +616,7 @@ sub _setup_ssh_with_reconnect {
             }
         }
     }
-    $temp_handle->close()
+    closedir $temp_handle
       or Firefox::Marionette::Exception->throw(
         "Failed to close directory '$temp_directory':$EXTENDED_OS_ERROR");
     if ( $self->_ssh() ) {
@@ -880,29 +960,34 @@ sub _get_local_reconnect_pid {
             }
             $self->{_initial_version} = $local_proxy->{firefox}->{version};
             $self->{_root_directory}  = $possible_root_directory;
-            if ( $self->{profile_name} ) {
-                $self->{_profile_directory} =
-                  Firefox::Marionette::Profile->directory(
-                    $self->{profile_name} );
-                $self->{profile_path} =
-                  File::Spec->catfile( $self->{_profile_directory},
-                    'prefs.js' );
-            }
-            else {
-                $self->{_profile_directory} =
-                  File::Spec->catfile( $self->{_root_directory}, 'profile' );
-                $self->{_download_directory} =
-                  File::Spec->catfile( $self->{_root_directory}, 'downloads' );
-                $self->{profile_path} =
-                  File::Spec->catfile( $self->{_profile_directory},
-                    'prefs.js' );
-            }
+            $self->_setup_profile();
         }
     }
-    $temp_handle->close();
+    closedir $temp_handle
+      or Firefox::Marionette::Exception->throw(
+        "Failed to close directory '$temp_directory':$EXTENDED_OS_ERROR");
     return $alive_pid;
 }
 
+sub _setup_profile {
+    my ($self) = @_;
+    if ( $self->{profile_name} ) {
+        $self->{_profile_directory} =
+          Firefox::Marionette::Profile->directory( $self->{profile_name} );
+        $self->{profile_path} =
+          File::Spec->catfile( $self->{_profile_directory}, 'prefs.js' );
+    }
+    else {
+        $self->{_profile_directory} =
+          File::Spec->catfile( $self->{_root_directory}, 'profile' );
+        $self->{_download_directory} =
+          File::Spec->catfile( $self->{_root_directory}, 'downloads' );
+        $self->{profile_path} =
+          File::Spec->catfile( $self->{_profile_directory}, 'prefs.js' );
+    }
+    return;
+}
+
 sub _reconnect {
     my ( $self, %parameters ) = @_;
     if ( $parameters{profile_name} ) {
@@ -1138,18 +1223,18 @@ sub _import_profile_paths {
                     $read_handle->read( my $buffer, _LOCAL_READ_BUFFER_SIZE() )
                   )
                 {
-                    $write_handle->print($buffer)
+                    print {$write_handle} $buffer
                       or Firefox::Marionette::Exception->throw(
                         "Failed to write to '$write_path':$EXTENDED_OS_ERROR");
                 }
                 defined $result
                   or Firefox::Marionette::Exception->throw(
                     "Failed to read from '$path':$EXTENDED_OS_ERROR");
-                $write_handle->close()
+                close $write_handle
                   or Firefox::Marionette::Exception->throw(
                     "Failed to close '$write_path':$EXTENDED_OS_ERROR");
             }
-            $read_handle->close()
+            close $read_handle
               or Firefox::Marionette::Exception->throw(
                 "Failed to close '$path':$EXTENDED_OS_ERROR");
         }
@@ -1799,7 +1884,7 @@ sub _get_update_status {
         }
         else {
             Firefox::Marionette::Exception->throw(
-"Failed to open $updates_status_path for reading:$EXTENDED_OS_ERROR"
+"Failed to open '$updates_status_path' for reading:$EXTENDED_OS_ERROR"
             );
         }
     }
@@ -2727,14 +2812,12 @@ sub _post_launch_checks_and_setup {
           or Firefox::Marionette::Exception->throw(
             "Failed to open '$path' for writing:$EXTENDED_OS_ERROR");
         binmode $handle;
-        $handle->print(
-            MIME::Base64::decode_base64(
-                Firefox::Marionette::Extension::HarExportTrigger->as_string()
-            )
-          )
+        print {$handle}
+          MIME::Base64::decode_base64(
+            Firefox::Marionette::Extension::HarExportTrigger->as_string() )
           or Firefox::Marionette::Exception->throw(
             "Failed to write to '$path':$EXTENDED_OS_ERROR");
-        $handle->close()
+        close $handle
           or Firefox::Marionette::Exception->throw(
             "Failed to close '$path':$EXTENDED_OS_ERROR");
         $self->install( $path, 0 );
@@ -2810,7 +2893,7 @@ sub _clean_local_extension_directory {
                 $entry );
             unlink $path or $cleaned = 0;
         }
-        $handle->close()
+        closedir $handle
           or Firefox::Marionette::Exception->throw(
 "Failed to close directory '$self->{_local_extension_directory}':$EXTENDED_OS_ERROR"
           );
@@ -3101,10 +3184,10 @@ sub _profile_arguments {
               )
               or Firefox::Marionette::Exception->throw(
                 "Failed to open '$path' for writing:$EXTENDED_OS_ERROR");
-            $handle->print($mime_types_content)
+            print {$handle} $mime_types_content
               or Firefox::Marionette::Exception->throw(
                 "Failed to write to '$path':$EXTENDED_OS_ERROR");
-            $handle->close
+            close $handle
               or Firefox::Marionette::Exception->throw(
                 "Failed to close '$path':$EXTENDED_OS_ERROR");
         }
@@ -3466,7 +3549,7 @@ sub _read_and_close_handle {
     defined $result
       or Firefox::Marionette::Exception->throw(
         "Failed to read from '$path':$EXTENDED_OS_ERROR");
-    $handle->close()
+    close $handle
       or Firefox::Marionette::Exception->throw(
         "Failed to close '$path':$EXTENDED_OS_ERROR");
     return $content;
@@ -3708,7 +3791,7 @@ sub _active_update_version {
             $active_update_handle =
               FileHandle->new( $active_update_path, Fcntl::O_RDONLY() )
               or Firefox::Marionette::Exception->throw(
-"Failed to open $active_update_path for reading:$EXTENDED_OS_ERROR"
+"Failed to open '$active_update_path' for reading:$EXTENDED_OS_ERROR"
               );
         }
         if ($active_update_handle) {
@@ -4245,7 +4328,11 @@ sub _launch {
 
 sub _launch_win32 {
     my ( $self, @arguments ) = @_;
-    my $binary  = $self->_binary();
+    my $binary = $self->_binary();
+    if ( $binary =~ /[.]pl$/smx ) {
+        unshift @arguments, $binary;
+        $binary = $EXECUTABLE_NAME;
+    }
     my $process = $self->_start_win32_process( $binary, @arguments );
     $self->{_win32_firefox_process} = $process;
     return $process->GetProcessID();
@@ -4257,9 +4344,11 @@ sub _xvfb_binary {
 
 sub _dev_fd_works {
     my ($self) = @_;
-    my $test_handle =
-      File::Temp::tempfile( File::Spec->tmpdir(),
-        'firefox_marionette_dev_fd_test_XXXXXXXXXXX' )
+    my $test_handle = File::Temp::tempfile(
+        File::Spec->catfile(
+            File::Spec->tmpdir(), 'firefox_marionette_dev_fd_test_XXXXXXXXXXX'
+        )
+      )
       or Firefox::Marionette::Exception->throw(
         "Failed to open temporary file for writing:$EXTENDED_OS_ERROR");
     my @stats = stat '/dev/fd/' . fileno $test_handle;
@@ -4337,14 +4426,16 @@ sub _launch_xauth {
       )
       or Firefox::Marionette::Exception->throw(
         "Failed to open '$ENV{XAUTHORITY}' for writing:$EXTENDED_OS_ERROR");
-    $auth_handle->close()
+    close $auth_handle
       or Firefox::Marionette::Exception->throw(
         "Failed to close '$ENV{XAUTHORITY}':$EXTENDED_OS_ERROR");
     my $mcookie = unpack 'H*',
       Crypt::URandom::urandom( _NUMBER_OF_MCOOKIE_BYTES() );
-    my $source_handle =
-      File::Temp::tempfile( File::Spec->tmpdir(),
-        'firefox_marionette_xauth_source_XXXXXXXXXXX' )
+    my $source_handle = File::Temp::tempfile(
+        File::Spec->catfile(
+            File::Spec->tmpdir(), 'firefox_marionette_xauth_source_XXXXXXXXXXX'
+        )
+      )
       or Firefox::Marionette::Exception->throw(
         "Failed to open temporary file for writing:$EXTENDED_OS_ERROR");
     fcntl $source_handle, Fcntl::F_SETFD(), 0
@@ -4352,7 +4443,9 @@ sub _launch_xauth {
 "Failed to clear the close-on-exec flag on a temporary file:$EXTENDED_OS_ERROR"
       );
     my $xauth_proto = q[.];
-    $source_handle->print("add :$display_number $xauth_proto $mcookie\n");
+    print {$source_handle} "add :$display_number $xauth_proto $mcookie\n"
+      or Firefox::Marionette::Exception->throw(
+        "Failed to write to temporary file:$EXTENDED_OS_ERROR");
     seek $source_handle, 0, Fcntl::SEEK_SET()
       or Firefox::Marionette::Exception->throw(
         "Failed to seek to start of temporary file:$EXTENDED_OS_ERROR");
@@ -5604,9 +5697,14 @@ sub _network_connection_and_initial_read_from_marionette {
         if ($number_of_bytes) {
             $connected = 1;
         }
-        else {
+        elsif ( defined $number_of_bytes ) {
             sleep 1;
         }
+        else {
+            Firefox::Marionette::Exception->throw(
+"Failed to read from connection to $host on port $port:$EXTENDED_OS_ERROR"
+            );
+        }
     }
     elsif ( $EXTENDED_OS_ERROR == POSIX::ECONNREFUSED() ) {
         sleep 1;
@@ -5830,7 +5928,7 @@ sub _write_local_proxy {
       FileHandle->new( $local_proxy_path,
         Fcntl::O_CREAT() | Fcntl::O_EXCL() | Fcntl::O_WRONLY() )
       or Firefox::Marionette::Exception->throw(
-        "Failed to open $local_proxy_path for writing:$EXTENDED_OS_ERROR");
+        "Failed to open '$local_proxy_path' for writing:$EXTENDED_OS_ERROR");
     my $local_proxy = {};
     if ( defined $local_proxy->{version} ) {
         foreach my $key (qw(major minor patch)) {
@@ -5859,10 +5957,10 @@ sub _write_local_proxy {
         $local_proxy->{firefox}->{binary}  = $self->_binary();
         $local_proxy->{firefox}->{version} = $self->{_initial_version};
     }
-    $local_proxy_handle->print( JSON::encode_json($local_proxy) )
+    print {$local_proxy_handle} JSON::encode_json($local_proxy)
       or Firefox::Marionette::Exception->throw(
         "Failed to write to $local_proxy_path:$EXTENDED_OS_ERROR");
-    $local_proxy_handle->close()
+    close $local_proxy_handle
       or Firefox::Marionette::Exception->throw(
         "Failed to close '$local_proxy_path':$EXTENDED_OS_ERROR");
     return;
@@ -6143,11 +6241,11 @@ sub _copy_content_to_profile_directory {
           FileHandle->new( $path,
             Fcntl::O_CREAT() | Fcntl::O_EXCL() | Fcntl::O_WRONLY() )
           or Firefox::Marionette::Exception->throw(
-            "Failed to open $path for writing:$EXTENDED_OS_ERROR");
-        $handle->print($content)
+            "Failed to open '$path' for writing:$EXTENDED_OS_ERROR");
+        print {$handle} $content
           or Firefox::Marionette::Exception->throw(
             "Failed to write to $path:$EXTENDED_OS_ERROR");
-        $handle->close()
+        close $handle
           or Firefox::Marionette::Exception->throw(
             "Failed to close '$path':$EXTENDED_OS_ERROR");
         if ( $OSNAME eq 'cygwin' ) {
@@ -6264,7 +6362,7 @@ sub _get_local_command_output {
       or Firefox::Marionette::Exception->throw( "Failed to read from $binary "
           . ( join q[ ], @arguments )
           . ":$EXTENDED_OS_ERROR" );
-         $handle->close()
+    close $handle
       or $parameters->{ignore_exit_status}
       or $parameters->{return_exit_status}
       or Firefox::Marionette::Exception->throw( q[Command ']
@@ -6506,7 +6604,7 @@ sub _put_file_via_scp {
     while ( $result =
         $original_handle->read( my $buffer, _LOCAL_READ_BUFFER_SIZE() ) )
     {
-        $temp_handle->print($buffer)
+        print {$temp_handle} $buffer
           or Firefox::Marionette::Exception->throw(
             "Failed to write to '$local_path':$EXTENDED_OS_ERROR");
     }
@@ -6610,10 +6708,10 @@ sub _search_file_via_ssh {
       )
       or Firefox::Marionette::Exception->throw(
         "Failed to open temporary file for writing:$EXTENDED_OS_ERROR");
-    $handle->print($output)
+    print {$handle} $output
       or Firefox::Marionette::Exception->throw(
         "Failed to write to temporary file:$EXTENDED_OS_ERROR");
-    $handle->seek( 0, Fcntl::SEEK_SET() )
+    seek $handle, 0, Fcntl::SEEK_SET()
       or Firefox::Marionette::Exception->throw(
         "Failed to seek to start of temporary file:$EXTENDED_OS_ERROR");
     return $handle;
@@ -6642,7 +6740,7 @@ sub _get_marionette_port {
                     $port = $1;
                 }
             }
-            $profile_handle->close()
+            close $profile_handle
               or Firefox::Marionette::Exception->throw(
                 "Failed to close '$self->{profile_path}':$EXTENDED_OS_ERROR");
         }
@@ -7670,8 +7768,16 @@ sub capabilities {
     );
     my $response = $self->_get_response($message_id);
     if ( $self->marionette_protocol() == _MARIONETTE_PROTOCOL_VERSION_3() ) {
-        return $self->_create_capabilities(
-            $response->result()->{capabilities} );
+        if (   ( $response->result()->{value} )
+            && ( $response->result()->{value}->{capabilities} ) )
+        {
+            return $self->_create_capabilities(
+                $response->result()->{value}->{capabilities} );
+        }
+        else {
+            return $self->_create_capabilities(
+                $response->result()->{capabilities} );
+        }
     }
     else {
         return $self->_create_capabilities( $response->result()->{value} );
@@ -8221,10 +8327,10 @@ sub pdf {
             "Failed to open temporary file for writing:$EXTENDED_OS_ERROR");
         binmode $handle;
         my $content = $self->_response_result_value($response);
-        $handle->print( MIME::Base64::decode_base64($content) )
+        print {$handle} MIME::Base64::decode_base64($content)
           or Firefox::Marionette::Exception->throw(
             "Failed to write to temporary file:$EXTENDED_OS_ERROR");
-        $handle->seek( 0, Fcntl::SEEK_SET() )
+        seek $handle, 0, Fcntl::SEEK_SET()
           or Firefox::Marionette::Exception->throw(
             "Failed to seek to start of temporary file:$EXTENDED_OS_ERROR");
         return $handle;
@@ -8314,10 +8420,10 @@ sub selfie {
         binmode $handle;
         my $content = $self->_response_result_value($response);
         $content =~ s/^data:image\/png;base64,//smx;
-        $handle->print( MIME::Base64::decode_base64($content) )
+        print {$handle} MIME::Base64::decode_base64($content)
           or Firefox::Marionette::Exception->throw(
             "Failed to write to temporary file:$EXTENDED_OS_ERROR");
-        $handle->seek( 0, Fcntl::SEEK_SET() )
+        seek $handle, 0, Fcntl::SEEK_SET()
           or Firefox::Marionette::Exception->throw(
             "Failed to seek to start of temporary file:$EXTENDED_OS_ERROR");
         return $handle;
@@ -8987,10 +9093,14 @@ sub quit {
         $self->_terminate_xvfb();
     }
     elsif ( $self->_socket() ) {
-        if ( $self->_session_id() ) {
-            $self->_quit_over_marionette($flags);
-            delete $self->{session_id};
-        }
+        eval {
+            if ( $self->_session_id() ) {
+                $self->_quit_over_marionette($flags);
+                delete $self->{session_id};
+            }
+        } or do {
+            warn "Caught an exception while quitting:$EVAL_ERROR\n";
+        };
         $self->_terminate_process();
     }
     else {
@@ -9035,9 +9145,12 @@ sub _quit_over_marionette {
         $self->_wait_for_firefox_to_exit();
     }
     else {
-        close $socket
-          or Firefox::Marionette::Exception->throw(
-            "Failed to close socket to firefox:$EXTENDED_OS_ERROR");
+        if ( !close $socket ) {
+            my $error = $EXTENDED_OS_ERROR;
+            $self->_terminate_xvfb();
+            Firefox::Marionette::Exception->throw(
+                "Failed to close socket to firefox:$error");
+        }
         $socket = undef;
         $self->_wait_for_firefox_to_exit();
     }
@@ -9994,7 +10107,7 @@ sub install {
           File::Spec->splitpath("$xpi_path");
         my $handle = FileHandle->new( $xpi_path, Fcntl::O_RDONLY() )
           or Firefox::Marionette::Exception->throw(
-            "Failed to open $xpi_path for reading:$EXTENDED_OS_ERROR");
+            "Failed to open '$xpi_path' for reading:$EXTENDED_OS_ERROR");
         binmode $handle;
         my $addons_directory = $self->{_addons_directory};
         $actual_path = $self->_remote_catfile( $addons_directory, $name );
@@ -10305,7 +10418,7 @@ Firefox::Marionette - Automate the Firefox browser with the Marionette protocol
 
 =head1 VERSION
 
-Version 1.35
+Version 1.38
 
 =head1 SYNOPSIS
 
@@ -10541,6 +10654,21 @@ accepts a subroutine reference as a parameter and then executes the subroutine.
 
     $firefox->bye(sub { $firefox->find_name('metacpan_search-input') })->await(sub { $firefox->interactive() && $firefox->find_partial('Download') })->click();
 
+=head2 cache_keys
+
+returns the set of all cache keys from L<Firefox::Marionette::Cache|Firefox::Marionette::Cache>.
+
+    use Firefox::Marionette();
+
+    my $firefox = Firefox::Marionette->new();
+    foreach my $key_name ($firefox->cache_keys()) {
+      my $key_value = $firefox->check_cache_key($key_name);
+      if (Firefox::Marionette::Cache->$key_name() != $key_value) {
+        warn "This module this the value of $key_name is " . Firefox::Marionette::Cache->$key_name();
+        warn "Firefox thinks the value of   $key_name is $key_value";
+      }
+    }
+
 =head2 capabilities
 
 returns the L<capabilities|Firefox::Marionette::Capabilities> of the current firefox binary.  You can retrieve L<timeouts|Firefox::Marionette::Timeouts> or a L<proxy|Firefox::Marionette::Proxy> with this method.
@@ -10580,6 +10708,23 @@ returns a list of all known L<certificates in the Firefox database|Firefox::Mari
 
 This method returns L<itself|Firefox::Marionette> to aid in chaining methods.
 
+=head2 check_cache_key
+
+accepts a L<cache_key|Firefox::Marionette::Cache> as a parameter.
+
+    use Firefox::Marionette();
+
+    my $firefox = Firefox::Marionette->new();
+    foreach my $key_name ($firefox->cache_keys()) {
+      my $key_value = $firefox->check_cache_key($key_name);
+      if (Firefox::Marionette::Cache->$key_name() != $key_value) {
+        warn "This module this the value of $key_name is " . Firefox::Marionette::Cache->$key_name();
+        warn "Firefox thinks the value of   $key_name is $key_value";
+      }
+    }
+
+This method returns the L<cache_key|Firefox::Marionette::Cache>'s actual value from firefox as a number.  This may differ from the current value of the key from L<Firefox::Marionette::Cache|Firefox::Marionette::Cache> as these values have changed as firefox has evolved.
+
 =head2 child_error
 
 This method returns the $? (CHILD_ERROR) for the Firefox process, or undefined if the process has not yet exited.
@@ -10609,6 +10754,19 @@ returns identifiers for each open chrome window for tests interested in managing
 
 accepts a L<element|Firefox::Marionette::Element> as the first parameter and clears any user supplied input
 
+=head2 clear_cache
+
+accepts a single flag parameter, which can be an ORed set of keys from L<Firefox::Marionette::Cache|Firefox::Marionette::Cache> and clears the appropriate sections of the cache.  If no flags parameter is supplied, the default is L<CLEAR_ALL|Firefox::Marionette::Cache#CLEAR_ALL>.  Note that this method, unlike L<delete_cookies|/delete_cookies> will actually delete all cookies for all hosts, not just the current webpage.
+
+    use Firefox::Marionette();
+    use Firefox::Marionette::Cache qw(:all);
+
+    my $firefox = Firefox::Marionette->new()->go('https://do.lots.of.evil/')->clear_cache(); # default clear all
+
+    $firefox->go('https://cookies.r.us')->clear_cache(CLEAR_COOKIES());
+
+This method returns L<itself|Firefox::Marionette> to aid in chaining methods.
+
 =head2 clear_pref
 
 accepts a L<preference|http://kb.mozillazine.org/About:config> name and restores it to the original value.  See the L<get_pref|Firefox::Marionette#get_pref> and L<set_pref|Firefox::Marionette#set_pref> methods to get a preference value and to set to it to a particular value.  This method returns L<itself|Firefox::Marionette> to aid in chaining methods.
@@ -10736,7 +10894,7 @@ deletes a single cookie by name.  Accepts a scalar containing the cookie name as
 
 =head2 delete_cookies
 
-here be cookie monsters! This method returns L<itself|Firefox::Marionette> to aid in chaining methods.
+Here be cookie monsters! Note that this method will only delete cookies for the current site.  See L<clear_cache|/clear_cache> for an alternative.  This method returns L<itself|Firefox::Marionette> to aid in chaining methods. 
 
 =head2 delete_header
 
@@ -10825,7 +10983,7 @@ accepts an optional regex to filter against the L<usage for the display|Firefox:
         }
     }
 
-=head2 download
+=head2 downloaded
 
 accepts a filesystem path and returns a matching filehandle.  This is trivial for locally running firefox, but sufficiently complex to justify the method for a remote firefox running over ssh.
 
@@ -10844,7 +11002,7 @@ accepts a filesystem path and returns a matching filehandle.  This is trivial fo
 
     foreach my $path ($firefox->downloads()) {
 
-        my $handle = $firefox->download($path);
+        my $handle = $firefox->downloaded($path);
 
         # do something with downloaded file handle
 
@@ -12646,9 +12804,7 @@ Thanks also to the authors of the documentation in the following sources;
 
 =item * L<Marionette Protocol|https://firefox-source-docs.mozilla.org/testing/marionette/marionette/index.html>
 
-=item * L<Marionette Documentation|https://firefox-source-docs.mozilla.org/testing/marionette/marionette/index.html>
-
-=item * L<Marionette driver.js|https://hg.mozilla.org/mozilla-central/file/tip/remote/marionette/driver.js>
+=item * L<Marionette driver.js|https://hg.mozilla.org/mozilla-central/file/tip/remote/marionette/driver.sys.mjs>
 
 =item * L<about:config|http://kb.mozillazine.org/About:config_entries>
 
diff --git a/lib/Firefox/Marionette/Buttons.pm b/lib/Firefox/Marionette/Buttons.pm
index e806eaf..dfc9806 100644
--- a/lib/Firefox/Marionette/Buttons.pm
+++ b/lib/Firefox/Marionette/Buttons.pm
@@ -12,7 +12,7 @@ our @EXPORT_OK = qw(
 
 our %EXPORT_TAGS = ( 'all' => \@EXPORT_OK, );
 
-our $VERSION = '1.35';
+our $VERSION = '1.38';
 
 sub LEFT_BUTTON   { return 0 }
 sub MIDDLE_BUTTON { return 1 }
@@ -26,7 +26,7 @@ Firefox::Marionette::Buttons - Human readable mouse buttons for the Marionette p
 
 =head1 VERSION
 
-Version 1.35
+Version 1.38
 
 =head1 SYNOPSIS
 
diff --git a/lib/Firefox/Marionette/Cache.pm b/lib/Firefox/Marionette/Cache.pm
new file mode 100644
index 0000000..8aa25f1
--- /dev/null
+++ b/lib/Firefox/Marionette/Cache.pm
@@ -0,0 +1,285 @@
+package Firefox::Marionette::Cache;
+
+use strict;
+use warnings;
+use Exporter();
+*import = \&Exporter::import;
+our @EXPORT_OK = qw(
+  CLEAR_COOKIES
+  CLEAR_NETWORK_CACHE
+  CLEAR_IMAGE_CACHE
+  CLEAR_DOWNLOADS
+  CLEAR_PASSWORDS
+  CLEAR_MEDIA_DEVICES
+  CLEAR_DOM_QUOTA
+  CLEAR_PREDICTOR_NETWORK_DATA
+  CLEAR_DOM_PUSH_NOTIFICATIONS
+  CLEAR_HISTORY
+  CLEAR_SESSION_HISTORY
+  CLEAR_AUTH_TOKENS
+  CLEAR_AUTH_CACHE
+  CLEAR_PERMISSIONS
+  CLEAR_CONTENT_PREFERENCES
+  CLEAR_HSTS
+  CLEAR_EME
+  CLEAR_REPORTS
+  CLEAR_STORAGE_ACCESS
+  CLEAR_CERT_EXCEPTIONS
+  CLEAR_CONTENT_BLOCKING_RECORDS
+  CLEAR_CSS_CACHE
+  CLEAR_PREFLIGHT_CACHE
+  CLEAR_CLIENT_AUTH_REMEMBER_SERVICE
+  CLEAR_CREDENTIAL_MANAGER_STATE
+  CLEAR_ALL
+  CLEAR_ALL_CACHES
+  CLEAR_DOM_STORAGES
+  CLEAR_FORGET_ABOUT_SITE
+);
+
+our %EXPORT_TAGS = ( 'all' => \@EXPORT_OK, );
+
+our $VERSION = '1.38';
+
+sub CLEAR_COOKIES                      { return 1 }
+sub CLEAR_NETWORK_CACHE                { return 2 }
+sub CLEAR_IMAGE_CACHE                  { return 4 }
+sub CLEAR_DOWNLOADS                    { return 16 }
+sub CLEAR_PASSWORDS                    { return 32 }
+sub CLEAR_MEDIA_DEVICES                { return 64 }
+sub CLEAR_DOM_QUOTA                    { return 128 }
+sub CLEAR_PREDICTOR_NETWORK_DATA       { return 256 }
+sub CLEAR_DOM_PUSH_NOTIFICATIONS       { return 512 }
+sub CLEAR_HISTORY                      { return 1024 }
+sub CLEAR_SESSION_HISTORY              { return 2048 }
+sub CLEAR_AUTH_TOKENS                  { return 4096 }
+sub CLEAR_AUTH_CACHE                   { return 8192 }
+sub CLEAR_PERMISSIONS                  { return 16_384 }
+sub CLEAR_CONTENT_PREFERENCES          { return 32_768 }
+sub CLEAR_HSTS                         { return 65_536 }
+sub CLEAR_EME                          { return 131_072 }
+sub CLEAR_REPORTS                      { return 262_144 }
+sub CLEAR_STORAGE_ACCESS               { return 524_288 }
+sub CLEAR_CERT_EXCEPTIONS              { return 1_048_576 }
+sub CLEAR_CONTENT_BLOCKING_RECORDS     { return 2_097_152 }
+sub CLEAR_CSS_CACHE                    { return 4_194_304 }
+sub CLEAR_PREFLIGHT_CACHE              { return 8_388_608 }
+sub CLEAR_CLIENT_AUTH_REMEMBER_SERVICE { return 16_777_216 }
+sub CLEAR_CREDENTIAL_MANAGER_STATE     { return 16_777_216 }
+sub CLEAR_ALL                          { return 0xFFFFFFFF }
+
+sub CLEAR_ALL_CACHES {
+    return CLEAR_NETWORK_CACHE() | CLEAR_IMAGE_CACHE() | CLEAR_CSS_CACHE() |
+      CLEAR_PREFLIGHT_CACHE() | CLEAR_HSTS();
+}
+
+sub CLEAR_DOM_STORAGES {
+    return CLEAR_DOM_QUOTA() | CLEAR_DOM_PUSH_NOTIFICATIONS() | CLEAR_REPORTS();
+}
+
+sub CLEAR_FORGET_ABOUT_SITE {
+    return CLEAR_HISTORY() | CLEAR_SESSION_HISTORY() | CLEAR_ALL_CACHES() |
+      CLEAR_COOKIES() | CLEAR_EME() | CLEAR_DOWNLOADS() | CLEAR_PERMISSIONS() |
+      CLEAR_DOM_STORAGES() | CLEAR_CONTENT_PREFERENCES() |
+      CLEAR_PREDICTOR_NETWORK_DATA() | CLEAR_DOM_PUSH_NOTIFICATIONS() |
+      CLEAR_CLIENT_AUTH_REMEMBER_SERVICE() | CLEAR_REPORTS() |
+      CLEAR_CERT_EXCEPTIONS() | CLEAR_CREDENTIAL_MANAGER_STATE();
+}
+
+1;    # Magic true value required at end of module
+__END__
+=head1 NAME
+
+Firefox::Marionette::Cache - Constants to describe actions on the cache
+
+=head1 VERSION
+
+Version 1.38
+
+=head1 SYNOPSIS
+
+    use Firefox::Marionette();
+    use Firefox::Marionette::Cache qw(:all);
+
+    my $firefox = Firefox::Marionette->new();
+
+    $firefox->go('https://google.com'); # where is a good site to produce a lot of cookies?
+
+    $firefox->go('about:blank');
+
+    $firefox->clear_cache(CLEAR_COOKIES());
+
+=head1 DESCRIPTION
+
+This module handles the implementation of the Firefox cache constants.  This is sourced from L<toolkit/components/cleardata/nsIClearDataService.idl|https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/cleardata/nsIClearDataService.idl>
+
+=head1 SUBROUTINES/METHODS
+
+=head2 CLEAR_COOKIES
+
+returns the value of CLEAR_COOKIES, which is 1 << 0 = 1
+
+=head2 CLEAR_NETWORK_CACHE
+
+returns the value of CLEAR_NETWORK_CACHE, which is 1 << 1 = 2
+
+=head2 CLEAR_IMAGE_CACHE
+
+returns the value of CLEAR_IMAGE_CACHE, which is 1 << 2 = 4
+
+=head2 CLEAR_DOWNLOADS
+
+returns the value of CLEAR_DOWNLOADS, which is 1 << 4 = 16
+
+=head2 CLEAR_PASSWORDS
+
+returns the value of CLEAR_PASSWORDS, which is 1 << 5 = 32
+
+=head2 CLEAR_MEDIA_DEVICES
+
+returns the value of CLEAR_MEDIA_DEVICES, which is 1 << 6 = 64
+
+=head2 CLEAR_DOM_QUOTA
+
+returns the value of CLEAR_DOM_QUOTA, which is 1 << 7 = 128 (LocalStorage, IndexedDB, ServiceWorkers, DOM Cache and so on.)
+
+=head2 CLEAR_PREDICTOR_NETWORK_DATA
+
+returns the value of CLEAR_PREDICTOR_NETWORK_DATA, which is 1 << 8 = 256 
+
+=head2 CLEAR_DOM_PUSH_NOTIFICATIONS
+
+returns the value of CLEAR_DOM_PUSH_NOTIFICATIONS, which is 1 << 9 = 512
+
+=head2 CLEAR_HISTORY
+
+returns the value of CLEAR_HISTORY, which is 1 << 10 = 1024 (Places history)
+
+=head2 CLEAR_SESSION_HISTORY
+
+returns the value of CLEAR_SESSION_HISTORY, which is 1 << 11 = 2048
+
+=head2 CLEAR_AUTH_TOKENS
+
+returns the value of CLEAR_AUTH_TOKENS, which is 1 << 12 = 4096
+
+=head2 CLEAR_AUTH_CACHE
+
+returns the value of CLEAR_AUTH_CACHE, which is 1 << 13 = 8192 (Login cache)
+
+=head2 CLEAR_PERMISSIONS
+
+returns the value of CLEAR_PERMISSIONS, which is 1 << 14 = 16384
+
+=head2 CLEAR_CONTENT_PREFERENCES
+
+returns the value of CLEAR_CONTENT_PREFERENCES, which is 1 << 15 = 32768
+
+=head2 CLEAR_HSTS
+
+returns the value of CLEAR_HSTS, which is 1 << 16 = 65536 (HTTP Strict Transport Security data)
+
+=head2 CLEAR_EME
+
+returns the value of CLEAR_EME, which is 1 << 17 = 131072 (Media plugin data)
+
+=head2 CLEAR_REPORTS
+
+returns the value of CLEAR_REPORTS, which is 1 << 18 = 262144 (Reporting API reports)
+
+=head2 CLEAR_STORAGE_ACCESS
+
+returns the value of CLEAR_STORAGE_ACCESS, which is 1 << 19 = 524288 (StorageAccessAPI flag, which indicates user interaction)
+
+=head2 CLEAR_CERT_EXCEPTIONS
+
+returns the value of CLEAR_CERT_EXCEPTIONS, which is 1 << 20 = 1048576
+
+=head2 CLEAR_CONTENT_BLOCKING_RECORDS
+
+returns the value of CLEAR_CONTENT_BLOCKING_RECORDS, which is 1 << 21 = 2097152 (content blocking database)
+
+=head2 CLEAR_CSS_CACHE
+
+returns the value of CLEAR_CSS_CACHE, which is 1 << 22 = 4194304 (in-memory CSS cache)
+
+=head2 CLEAR_PREFLIGHT_CACHE
+
+returns the value of CLEAR_PREFLIGHT_CACHE, which is 1 << 23 = 8388608 (CORS preflight cache)
+
+=head2 CLEAR_CLIENT_AUTH_REMEMBER_SERVICE
+
+returns the value of CLEAR_CLIENT_AUTH_REMEMBER_SERVICE, which is 1 << 24 = 16777216 (clients authentification certificate)
+
+=head2 CLEAR_CREDENTIAL_MANAGER_STATE
+
+returns the value of CLEAR_CREDENTIAL_MANAGER_STATE, which is 1 << 24 = 16777216 (FedCM)
+
+=head2 CLEAR_ALL
+
+returns the value of CLEAR_ALL, which is 4294967295 (0xFFFFFFFF)
+
+=head2 CLEAR_ALL_CACHES
+
+returns the value of CLEAR_ALL_CACHES, which is 12648454 (L<CLEAR_NETWORK_CACHE|/CLEAR_NETWORK_CACHE> | L<CLEAR_IMAGE_CACHE|/CLEAR_IMAGE_CACHE> | L<CLEAR_CSS_CACHE|/CLEAR_CSS_CACHE> | L<CLEAR_PREFLIGHT_CACHE|/CLEAR_PREFLIGHT_CACHE> | L<CLEAR_HSTS|/CLEAR_HSTS>)
+
+=head2 CLEAR_DOM_STORAGES
+
+returns the value of CLEAR_DOM_STORAGES, which is 262784 (L<CLEAR_DOM_QUOTA|/CLEAR_DOM_QUOTA> | L<CLEAR_DOM_PUSH_NOTIFICATIONS|/CLEAR_DOM_PUSH_NOTIFICATIONS> | L<CLEAR_REPORTS|/CLEAR_REPORTS>)
+
+=head2 CLEAR_FORGET_ABOUT_SITE
+
+returns the value of CLEAR_FORGET_ABOUT_SITE, which is 30920599 (L<CLEAR_HISTORY|/CLEAR_HISTORY> | L<CLEAR_SESSION_HISTORY|/CLEAR_SESSION_HISTORY> | L<CLEAR_ALL_CACHES|/CLEAR_ALL_CACHES> | L<CLEAR_COOKIES|/CLEAR_COOKIES> | L<CLEAR_EME|/CLEAR_EME> | L<CLEAR_DOWNLOADS|/CLEAR_DOWNLOADS> | L<CLEAR_PERMISSIONS|/CLEAR_PERMISSIONS> | L<CLEAR_DOM_STORAGES|/CLEAR_DOM_STORAGES> | L<CLEAR_CONTENT_PREFERENCES|/CLEAR_CONTENT_PREFERENCES> | L<CLEAR_PREDICTOR_NETWORK_DATA|/CLEAR_PREDICTOR_NETWORK_DATA> | L<CLEAR_DOM_PUSH_NOTIFICATIONS|/CLEAR_DOM_PUSH_NOTIFICATIONS> | L<CLEAR_CLIENT_AUTH_REMEMBER_SERVICE|/CLEAR_CLIENT_AUTH_REMEMBER_SERVICE> | L<CLEAR_REPORTS|/CLEAR_REPORTS> | L<CLEAR_CERT_EXCEPTIONS|/CLEAR_CERT_EXCEPTIONS> | L<CLEAR_CREDENTIAL_MANAGER_STATE|/CLEAR_CREDENTIAL_MANAGER_STATE>)
+
+=head1 DIAGNOSTICS
+
+None.
+
+=head1 CONFIGURATION AND ENVIRONMENT
+
+Firefox::Marionette::Cache requires no configuration files or environment variables.
+
+=head1 DEPENDENCIES
+
+None.
+
+=head1 INCOMPATIBILITIES
+
+None reported.
+
+=head1 BUGS AND LIMITATIONS
+
+To report a bug, or view the current list of bugs, please visit L<https://github.com/david-dick/firefox-marionette/issues>
+
+=head1 AUTHOR
+
+David Dick  C<< <ddick@cpan.org> >>
+
+=head1 LICENSE AND COPYRIGHT
+
+Copyright (c) 2023, David Dick C<< <ddick@cpan.org> >>. All rights reserved.
+
+This module is free software; you can redistribute it and/or
+modify it under the same terms as Perl itself. See L<perlartistic/perlartistic>.
+
+=head1 DISCLAIMER OF WARRANTY
+
+BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
+EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
+ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
+YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
+NECESSARY SERVICING, REPAIR, OR CORRECTION.
+
+IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
+LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
+OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
+THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
diff --git a/lib/Firefox/Marionette/Capabilities.pm b/lib/Firefox/Marionette/Capabilities.pm
index c7434fe..c89ca63 100755
--- a/lib/Firefox/Marionette/Capabilities.pm
+++ b/lib/Firefox/Marionette/Capabilities.pm
@@ -3,7 +3,7 @@ package Firefox::Marionette::Capabilities;
 use strict;
 use warnings;
 
-our $VERSION = '1.35';
+our $VERSION = '1.38';
 
 sub new {
     my ( $class, %parameters ) = @_;
@@ -126,7 +126,7 @@ Firefox::Marionette::Capabilities - Represents Firefox Capabilities retrieved us
 
 =head1 VERSION
 
-Version 1.35
+Version 1.38
 
 =head1 SYNOPSIS
 
diff --git a/lib/Firefox/Marionette/Certificate.pm b/lib/Firefox/Marionette/Certificate.pm
index 5cf93c5..4fb6623 100644
--- a/lib/Firefox/Marionette/Certificate.pm
+++ b/lib/Firefox/Marionette/Certificate.pm
@@ -3,7 +3,7 @@ package Firefox::Marionette::Certificate;
 use strict;
 use warnings;
 
-our $VERSION = '1.35';
+our $VERSION = '1.38';
 
 sub _NUMBER_OF_MICROSECOND_DIGITS { return -6 }
 
@@ -177,7 +177,7 @@ Firefox::Marionette::Certificate - Represents a x509 Certificate from Firefox
 
 =head1 VERSION
 
-Version 1.35
+Version 1.38
 
 =head1 SYNOPSIS
 
diff --git a/lib/Firefox/Marionette/Cookie.pm b/lib/Firefox/Marionette/Cookie.pm
index 079c806..b464eb7 100755
--- a/lib/Firefox/Marionette/Cookie.pm
+++ b/lib/Firefox/Marionette/Cookie.pm
@@ -3,7 +3,7 @@ package Firefox::Marionette::Cookie;
 use strict;
 use warnings;
 
-our $VERSION = '1.35';
+our $VERSION = '1.38';
 
 sub new {
     my ( $class, %parameters ) = @_;
@@ -73,7 +73,7 @@ Firefox::Marionette::Cookie - Represents a Firefox cookie retrieved using the Ma
 
 =head1 VERSION
 
-Version 1.35
+Version 1.38
 
 =head1 SYNOPSIS
 
diff --git a/lib/Firefox/Marionette/Display.pm b/lib/Firefox/Marionette/Display.pm
index 98596c0..766f6ef 100644
--- a/lib/Firefox/Marionette/Display.pm
+++ b/lib/Firefox/Marionette/Display.pm
@@ -3,7 +3,7 @@ package Firefox::Marionette::Display;
 use strict;
 use warnings;
 
-our $VERSION = '1.35';
+our $VERSION = '1.38';
 
 sub new {
     my ( $class, %parameters ) = @_;
@@ -60,7 +60,7 @@ Firefox::Marionette::Display - Represents a display from the displays method
 
 =head1 VERSION
 
-Version 1.35
+Version 1.38
 
 =head1 SYNOPSIS
 
diff --git a/lib/Firefox/Marionette/Element.pm b/lib/Firefox/Marionette/Element.pm
index 49aa637..3adf324 100755
--- a/lib/Firefox/Marionette/Element.pm
+++ b/lib/Firefox/Marionette/Element.pm
@@ -3,7 +3,7 @@ package Firefox::Marionette::Element;
 use strict;
 use warnings;
 
-our $VERSION = '1.35';
+our $VERSION = '1.38';
 
 sub IDENTIFIER { return 'element-6066-11e4-a52e-4f735466cecf' }
 
@@ -350,7 +350,7 @@ Firefox::Marionette::Element - Represents a Firefox element retrieved using the
 
 =head1 VERSION
 
-Version 1.35
+Version 1.38
 
 =head1 SYNOPSIS
 
diff --git a/lib/Firefox/Marionette/Element/Rect.pm b/lib/Firefox/Marionette/Element/Rect.pm
index 77043f1..eb0515a 100755
--- a/lib/Firefox/Marionette/Element/Rect.pm
+++ b/lib/Firefox/Marionette/Element/Rect.pm
@@ -3,7 +3,7 @@ package Firefox::Marionette::Element::Rect;
 use strict;
 use warnings;
 
-our $VERSION = '1.35';
+our $VERSION = '1.38';
 
 sub new {
     my ( $class, %parameters ) = @_;
@@ -40,7 +40,7 @@ Firefox::Marionette::Element::Rect - Represents the box around an Element
 
 =head1 VERSION
 
-Version 1.35
+Version 1.38
 
 =head1 SYNOPSIS
 
diff --git a/lib/Firefox/Marionette/Exception.pm b/lib/Firefox/Marionette/Exception.pm
index 756afd7..7dcb40f 100755
--- a/lib/Firefox/Marionette/Exception.pm
+++ b/lib/Firefox/Marionette/Exception.pm
@@ -5,7 +5,7 @@ use warnings;
 use Carp();
 use overload '""' => 'string';
 
-our $VERSION = '1.35';
+our $VERSION = '1.38';
 
 sub throw {
     my ( $class, $string ) = @_;
@@ -39,7 +39,7 @@ Firefox::Marionette::Exception - Represents an base exception class for exceptio
 
 =head1 VERSION
 
-Version 1.35
+Version 1.38
 
 =head1 SYNOPSIS
 
diff --git a/lib/Firefox/Marionette/Exception/InsecureCertificate.pm b/lib/Firefox/Marionette/Exception/InsecureCertificate.pm
index 0c498d6..40680d6 100644
--- a/lib/Firefox/Marionette/Exception/InsecureCertificate.pm
+++ b/lib/Firefox/Marionette/Exception/InsecureCertificate.pm
@@ -4,7 +4,7 @@ use strict;
 use warnings;
 use base qw(Firefox::Marionette::Exception::Response);
 
-our $VERSION = '1.35';
+our $VERSION = '1.38';
 
 sub throw {
     my ( $class, $response, $parameters ) = @_;
@@ -25,7 +25,7 @@ Firefox::Marionette::Exception::InsecureCertificate - Represents a 'insecure cer
 
 =head1 VERSION
 
-Version 1.35
+Version 1.38
 
 =head1 SYNOPSIS
 
diff --git a/lib/Firefox/Marionette/Exception/NoSuchAlert.pm b/lib/Firefox/Marionette/Exception/NoSuchAlert.pm
index 52b2c4d..d6b35af 100755
--- a/lib/Firefox/Marionette/Exception/NoSuchAlert.pm
+++ b/lib/Firefox/Marionette/Exception/NoSuchAlert.pm
@@ -4,7 +4,7 @@ use strict;
 use warnings;
 use base qw(Firefox::Marionette::Exception::Response);
 
-our $VERSION = '1.35';
+our $VERSION = '1.38';
 
 sub throw {
     my ( $class, $response, $parameters ) = @_;
@@ -25,7 +25,7 @@ Firefox::Marionette::Exception::NoSuchAlert - Represents a 'no such alert' excep
 
 =head1 VERSION
 
-Version 1.35
+Version 1.38
 
 =head1 SYNOPSIS
 
diff --git a/lib/Firefox/Marionette/Exception/NotFound.pm b/lib/Firefox/Marionette/Exception/NotFound.pm
index 30f3a63..f20d362 100755
--- a/lib/Firefox/Marionette/Exception/NotFound.pm
+++ b/lib/Firefox/Marionette/Exception/NotFound.pm
@@ -4,7 +4,7 @@ use strict;
 use warnings;
 use base qw(Firefox::Marionette::Exception::Response);
 
-our $VERSION = '1.35';
+our $VERSION = '1.38';
 
 sub throw {
     my ( $class, $response, $parameters ) = @_;
@@ -29,7 +29,7 @@ Firefox::Marionette::Exception::NotFound - Represents a 'no such element' except
 
 =head1 VERSION
 
-Version 1.35
+Version 1.38
 
 =head1 SYNOPSIS
 
diff --git a/lib/Firefox/Marionette/Exception/Response.pm b/lib/Firefox/Marionette/Exception/Response.pm
index 8a86623..0d8124a 100755
--- a/lib/Firefox/Marionette/Exception/Response.pm
+++ b/lib/Firefox/Marionette/Exception/Response.pm
@@ -4,7 +4,7 @@ use strict;
 use warnings;
 use base qw(Firefox::Marionette::Exception);
 
-our $VERSION = '1.35';
+our $VERSION = '1.38';
 
 sub throw {
     my ( $class, $response ) = @_;
@@ -45,7 +45,7 @@ Firefox::Marionette::Exception::Response - Represents an exception thrown by Fir
 
 =head1 VERSION
 
-Version 1.35
+Version 1.38
 
 =head1 SYNOPSIS
 
diff --git a/lib/Firefox/Marionette/Exception/StaleElement.pm b/lib/Firefox/Marionette/Exception/StaleElement.pm
index 7102c35..5bf8f68 100755
--- a/lib/Firefox/Marionette/Exception/StaleElement.pm
+++ b/lib/Firefox/Marionette/Exception/StaleElement.pm
@@ -4,7 +4,7 @@ use strict;
 use warnings;
 use base qw(Firefox::Marionette::Exception::Response);
 
-our $VERSION = '1.35';
+our $VERSION = '1.38';
 
 sub throw {
     my ( $class, $response, $parameters ) = @_;
@@ -35,7 +35,7 @@ Firefox::Marionette::Exception::StaleElement - Represents a 'stale element refer
 
 =head1 VERSION
 
-Version 1.35
+Version 1.38
 
 =head1 SYNOPSIS
 
diff --git a/lib/Firefox/Marionette/Extension/HarExportTrigger.pm b/lib/Firefox/Marionette/Extension/HarExportTrigger.pm
index b567f7f..17db825 100644
--- a/lib/Firefox/Marionette/Extension/HarExportTrigger.pm
+++ b/lib/Firefox/Marionette/Extension/HarExportTrigger.pm
@@ -3,7 +3,7 @@ package Firefox::Marionette::Extension::HarExportTrigger;
 use strict;
 use warnings;
 
-our $VERSION = '1.35';
+our $VERSION = '1.38';
 
 sub as_string {
     return <<'_BASE64_';
@@ -573,7 +573,7 @@ Firefox::Marionette::Extension::HarExportTrigger - Contains the HAR Export Trigg
 
 =head1 VERSION
 
-Version 1.35
+Version 1.38
 
 =head1 SYNOPSIS
 
diff --git a/lib/Firefox/Marionette/Image.pm b/lib/Firefox/Marionette/Image.pm
index abe8c0f..3f09200 100644
--- a/lib/Firefox/Marionette/Image.pm
+++ b/lib/Firefox/Marionette/Image.pm
@@ -6,7 +6,7 @@ use URI::URL();
 
 use base qw(Firefox::Marionette::Element);
 
-our $VERSION = '1.35';
+our $VERSION = '1.38';
 
 sub new {
     my ( $class, $element ) = @_;
@@ -89,7 +89,7 @@ Firefox::Marionette::Image - Represents an image from the images method
 
 =head1 VERSION
 
-Version 1.35
+Version 1.38
 
 =head1 SYNOPSIS
 
diff --git a/lib/Firefox/Marionette/Keys.pm b/lib/Firefox/Marionette/Keys.pm
index 0d7cb5e..0e1b454 100644
--- a/lib/Firefox/Marionette/Keys.pm
+++ b/lib/Firefox/Marionette/Keys.pm
@@ -53,7 +53,7 @@ our @EXPORT_OK = qw(
 
 our %EXPORT_TAGS = ( 'all' => \@EXPORT_OK, );
 
-our $VERSION = '1.35';
+our $VERSION = '1.38';
 
 sub CANCEL          { return chr hex '0xE001' }
 sub HELP            { return chr hex '0xE002' }
@@ -108,7 +108,7 @@ Firefox::Marionette::Keys - Human readable special keys for the Marionette proto
 
 =head1 VERSION
 
-Version 1.35
+Version 1.38
 
 =head1 SYNOPSIS
 
diff --git a/lib/Firefox/Marionette/Link.pm b/lib/Firefox/Marionette/Link.pm
index 6927cf2..7c66d69 100644
--- a/lib/Firefox/Marionette/Link.pm
+++ b/lib/Firefox/Marionette/Link.pm
@@ -6,7 +6,7 @@ use URI::URL();
 
 use base qw(Firefox::Marionette::Element);
 
-our $VERSION = '1.35';
+our $VERSION = '1.38';
 
 sub new {
     my ( $class, $element ) = @_;
@@ -78,7 +78,7 @@ Firefox::Marionette::Link - Represents a link from the links method
 
 =head1 VERSION
 
-Version 1.35
+Version 1.38
 
 =head1 SYNOPSIS
 
diff --git a/lib/Firefox/Marionette/Login.pm b/lib/Firefox/Marionette/Login.pm
index 7264c83..a362676 100644
--- a/lib/Firefox/Marionette/Login.pm
+++ b/lib/Firefox/Marionette/Login.pm
@@ -3,7 +3,7 @@ package Firefox::Marionette::Login;
 use strict;
 use warnings;
 
-our $VERSION = '1.35';
+our $VERSION = '1.38';
 
 sub _NUMBER_OF_MILLISECONDS_IN_A_SECOND { return 1000 }
 
@@ -131,7 +131,7 @@ Firefox::Marionette::Login - Represents a login from the Firefox Password Manage
 
 =head1 VERSION
 
-Version 1.35
+Version 1.38
 
 =head1 SYNOPSIS
 
diff --git a/lib/Firefox/Marionette/Profile.pm b/lib/Firefox/Marionette/Profile.pm
index 275a031..be519bb 100755
--- a/lib/Firefox/Marionette/Profile.pm
+++ b/lib/Firefox/Marionette/Profile.pm
@@ -13,7 +13,7 @@ BEGIN {
         require Win32;
     }
 }
-our $VERSION = '1.35';
+our $VERSION = '1.38';
 
 sub ANY_PORT            { return 0 }
 sub _GETPWUID_DIR_INDEX { return 7 }
@@ -521,7 +521,7 @@ Firefox::Marionette::Profile - Represents a prefs.js Firefox Profile
 
 =head1 VERSION
 
-Version 1.35
+Version 1.38
 
 =head1 SYNOPSIS
 
diff --git a/lib/Firefox/Marionette/Proxy.pm b/lib/Firefox/Marionette/Proxy.pm
index 8e99957..ac210ef 100755
--- a/lib/Firefox/Marionette/Proxy.pm
+++ b/lib/Firefox/Marionette/Proxy.pm
@@ -3,7 +3,7 @@ package Firefox::Marionette::Proxy;
 use strict;
 use warnings;
 
-our $VERSION = '1.35';
+our $VERSION = '1.38';
 
 sub new {
     my ( $class, %parameters ) = @_;
@@ -82,7 +82,7 @@ Firefox::Marionette::Proxy - Represents a Proxy used by Firefox Capabilities usi
 
 =head1 VERSION
 
-Version 1.35
+Version 1.38
 
 =head1 SYNOPSIS
 
diff --git a/lib/Firefox/Marionette/Response.pm b/lib/Firefox/Marionette/Response.pm
index 00850e1..378d3de 100755
--- a/lib/Firefox/Marionette/Response.pm
+++ b/lib/Firefox/Marionette/Response.pm
@@ -8,7 +8,7 @@ use Firefox::Marionette::Exception::StaleElement();
 use Firefox::Marionette::Exception::InsecureCertificate();
 use Firefox::Marionette::Exception::Response();
 
-our $VERSION = '1.35';
+our $VERSION = '1.38';
 
 sub _TYPE_INDEX            { return 0 }
 sub _MESSAGE_ID_INDEX      { return 1 }
@@ -152,7 +152,7 @@ Firefox::Marionette::Response - Represents a Marionette protocol response
 
 =head1 VERSION
 
-Version 1.35
+Version 1.38
 
 =head1 SYNOPSIS
 
diff --git a/lib/Firefox/Marionette/ShadowRoot.pm b/lib/Firefox/Marionette/ShadowRoot.pm
index 7827fb0..a01707a 100644
--- a/lib/Firefox/Marionette/ShadowRoot.pm
+++ b/lib/Firefox/Marionette/ShadowRoot.pm
@@ -3,7 +3,7 @@ package Firefox::Marionette::ShadowRoot;
 use strict;
 use warnings;
 
-our $VERSION = '1.35';
+our $VERSION = '1.38';
 
 sub IDENTIFIER { return 'shadow-6066-11e4-a52e-4f735466cecf' }
 
@@ -36,7 +36,7 @@ Firefox::Marionette::ShadowRoot - Represents a Firefox shadow root retrieved usi
 
 =head1 VERSION
 
-Version 1.35
+Version 1.38
 
 =head1 SYNOPSIS
 
diff --git a/lib/Firefox/Marionette/Timeouts.pm b/lib/Firefox/Marionette/Timeouts.pm
index 92f7746..3d3bb32 100755
--- a/lib/Firefox/Marionette/Timeouts.pm
+++ b/lib/Firefox/Marionette/Timeouts.pm
@@ -3,7 +3,7 @@ package Firefox::Marionette::Timeouts;
 use strict;
 use warnings;
 
-our $VERSION = '1.35';
+our $VERSION = '1.38';
 
 sub new {
     my ( $class, %parameters ) = @_;
@@ -35,7 +35,7 @@ Firefox::Marionette::Timeouts - Represents the timeouts for page loading, search
 
 =head1 VERSION
 
-Version 1.35
+Version 1.38
 
 =head1 SYNOPSIS
 
diff --git a/lib/Firefox/Marionette/UpdateStatus.pm b/lib/Firefox/Marionette/UpdateStatus.pm
index 7dbbd5c..d5289dc 100644
--- a/lib/Firefox/Marionette/UpdateStatus.pm
+++ b/lib/Firefox/Marionette/UpdateStatus.pm
@@ -4,7 +4,7 @@ use strict;
 use warnings;
 use URI();
 
-our $VERSION = '1.35';
+our $VERSION = '1.38';
 
 sub _NUMBER_OF_MILLISECONDS_IN_A_SECOND { return 1000 }
 
@@ -155,7 +155,7 @@ Firefox::Marionette::UpdateStatus - Represents the resulting status of an Firefo
 
 =head1 VERSION
 
-Version 1.35
+Version 1.38
 
 =head1 SYNOPSIS
 
diff --git a/lib/Firefox/Marionette/Window/Rect.pm b/lib/Firefox/Marionette/Window/Rect.pm
index 1934220..88500b7 100755
--- a/lib/Firefox/Marionette/Window/Rect.pm
+++ b/lib/Firefox/Marionette/Window/Rect.pm
@@ -3,7 +3,7 @@ package Firefox::Marionette::Window::Rect;
 use strict;
 use warnings;
 
-our $VERSION = '1.35';
+our $VERSION = '1.38';
 
 sub new {
     my ( $class, %parameters ) = @_;
@@ -45,7 +45,7 @@ Firefox::Marionette::Window::Rect - Represents the browser window's shape and si
 
 =head1 VERSION
 
-Version 1.35
+Version 1.38
 
 =head1 SYNOPSIS
 
diff --git a/lib/Waterfox/Marionette.pm b/lib/Waterfox/Marionette.pm
index 6838470..d3dea0f 100644
--- a/lib/Waterfox/Marionette.pm
+++ b/lib/Waterfox/Marionette.pm
@@ -11,7 +11,7 @@ our @EXPORT_OK =
   qw(BY_XPATH BY_ID BY_NAME BY_TAG BY_CLASS BY_SELECTOR BY_LINK BY_PARTIAL);
 our %EXPORT_TAGS = ( all => \@EXPORT_OK );
 
-our $VERSION = '1.35';
+our $VERSION = '1.38';
 
 sub default_binary_name {
     return 'waterfox';
@@ -54,7 +54,7 @@ Waterfox::Marionette - Automate the Waterfox browser with the Marionette protoco
 
 =head1 VERSION
 
-Version 1.35
+Version 1.38
 
 =head1 SYNOPSIS
 
diff --git a/lib/Waterfox/Marionette/Profile.pm b/lib/Waterfox/Marionette/Profile.pm
index 6123eb1..591fe0c 100644
--- a/lib/Waterfox/Marionette/Profile.pm
+++ b/lib/Waterfox/Marionette/Profile.pm
@@ -11,7 +11,7 @@ BEGIN {
         require Win32;
     }
 }
-our $VERSION = '1.35';
+our $VERSION = '1.38';
 
 sub profile_ini_directory {
     my ($class) = @_;
@@ -101,7 +101,7 @@ Waterfox::Marionette::Profile - Represents a prefs.js Waterfox Profile
 
 =head1 VERSION
 
-Version 1.35
+Version 1.38
 
 =head1 SYNOPSIS
 
diff --git a/mozilla-head-check b/mozilla-head-check
index a8e132c..8a33aa7 100755
--- a/mozilla-head-check
+++ b/mozilla-head-check
@@ -10,7 +10,7 @@ use English qw( -no_match_vars );
 use Carp();
 use Sys::Syslog();
 
-our $VERSION = '1.35';
+our $VERSION = '1.38';
 
 MAIN: {
     my $facility = 'LOG_LOCAL0';
diff --git a/ssh-auth-cmd-marionette b/ssh-auth-cmd-marionette
index f6f6744..fd61b02 100755
--- a/ssh-auth-cmd-marionette
+++ b/ssh-auth-cmd-marionette
@@ -26,7 +26,7 @@ local $ENV{PATH} = '/usr/bin:/bin:/usr/sbin:/sbin'
     : q[]
   );
 
-our $VERSION = '1.35';
+our $VERSION = '1.38';
 
 my $binary  = 'firefox';
 my $ident   = 'ssh-auth-cmd-marionette';
@@ -344,7 +344,7 @@ sub _filesystem_regexes {
                 push @allowed_binary_paths, $os_allowed_binary_paths{$OSNAME};
             }
             my %os_allowed_binary_directories = (
-                freebsd   => '/usr/local/lib',
+                freebsd   => '/usr/local/lib/firefox',
                 dragonfly => '/usr/local/lib/firefox',
                 netbsd    => '/usr/pkg/bin',
                 openbsd   => '/usr/local/lib/firefox',
@@ -401,7 +401,7 @@ ssh-auth-cmd-marionette - ssh ~/.ssh/authorized_keys command for Firefox::Marion
 
 =head1 VERSION
 
-Version 1.35
+Version 1.38
 
 =head1 USAGE
 
diff --git a/t/01-marionette.t b/t/01-marionette.t
index a9cc92c..3192a48 100755
--- a/t/01-marionette.t
+++ b/t/01-marionette.t
@@ -797,6 +797,12 @@ SKIP: {
 			diag("Unable to determine the number of updates available");
 			ok(1, "Unable to determine the number of updates available");
 		}
+		$update = Firefox::Marionette::UpdateStatus->new(elevation_failure => 0, unsupported => undef, is_complete_update => 1, install_date => undef);
+		ok(ref $update eq 'Firefox::Marionette::UpdateStatus', "Firefox::Marionette::UpdateStatus->new() produces a Firefox::Marionette::UpdateStatus object");
+		ok($update->elevation_failure() == 0, "\$update->elevation_failure() == 0 when parameter is 0");
+		ok(!defined $update->unsupported(), "\$update->unsupported() is not defined when parameter is not defined");
+		ok($update->is_complete_update() == 1, "\$update->is_complete_update() == 1 when parameter is 1");
+		ok(!defined $update->install_date(), "\$update->install_date() is not defined when parameter is not defined");
 	}
 	if ($ENV{FIREFOX_HOST}) {
 		ok(-d $firefox->ssh_local_directory(), "Firefox::Marionette->ssh_local_directory() returns the existing ssh local directory:" . $firefox->ssh_local_directory());
@@ -1460,6 +1466,59 @@ SKIP: {
 			ok($hash->{value} == 2, "Value returned from script is the numeric 2 in a hash");
 		}
 	}
+	if (($tls_tests_ok) && ($ENV{RELEASE_TESTING})) {
+		if ($major_version < 63) {
+			diag("Not attempting to do cache operations for Firefox $major_version");
+		} else {
+			ok($firefox->go('https://github.com'), "\$firefox->go('https://github.com') succeeded");
+			my $old_session_cookie = github_session_cookie($firefox);
+			ok($old_session_cookie, "Found github session cookie");
+			ok($firefox->go('about:blank'), "\$firefox->go('about:blank') succeeded");
+			my $cookie_count = 0;
+			foreach my $cookie ($firefox->cookies()) {
+				$cookie_count += 1;
+				diag("Should not have found cookie " . $cookie->name() . " for about:blank");
+			}
+			ok($cookie_count == 0, "There are no availabe cookies for about:blank");
+			ok(ref $firefox->clear_cache() eq $class, "\$firefox->clear_cache() produces a $class object");
+			ok($firefox->go('https://github.com'), "\$firefox->go('https://github.com') succeeded");
+			my $new_session_cookie = github_session_cookie($firefox);
+			ok(defined $new_session_cookie, "The session cookie was found after clearing cache");
+			ok($old_session_cookie ne $new_session_cookie, "Different session cookie found after clearing everything in the cache");
+			$old_session_cookie = $new_session_cookie;
+			ok($firefox->go('about:blank'), "\$firefox->go('about:blank') succeeded");
+			ok(ref $firefox->clear_cache(Firefox::Marionette::Cache::CLEAR_COOKIES()) eq $class, "\$firefox->clear_cache(Firefox::Marionette::Cache::CLEAR_COOKIES()) produces a $class object");
+			ok($firefox->go('https://github.com'), "\$firefox->go('https://github.com') succeeded");
+			$new_session_cookie = github_session_cookie($firefox);
+			ok(defined $new_session_cookie, "The session cookie was found after clearing cache");
+			ok($old_session_cookie ne $new_session_cookie, "Different session cookie found after clearing cookie cache");
+			$old_session_cookie = $new_session_cookie;
+			ok($firefox->go('about:blank'), "\$firefox->go('about:blank') succeeded");
+			ok(ref $firefox->clear_cache(Firefox::Marionette::Cache::CLEAR_NETWORK_CACHE()) eq $class, "\$firefox->clear_cache(Firefox::Marionette::Cache::CLEAR_NETWORK_CACHE()) produces a $class object");
+			ok($firefox->go('https://github.com'), "\$firefox->go('https://github.com') succeeded");
+			$new_session_cookie = github_session_cookie($firefox);
+			ok(defined $new_session_cookie, "The session cookie was found after clearing cache");
+			ok($old_session_cookie eq $new_session_cookie, "The same session cookie found after clearing network cache");
+		}
+	}
+	Firefox::Marionette::Cache->import(qw(:all));
+	my $clear_data_service_is_ok = 1;
+	eval { $firefox->check_cache_key('CLEAR_COOKIES'); } or do { $clear_data_service_is_ok = 0; chomp $@; diag("Unable to check cache values:$@"); };
+	if ($clear_data_service_is_ok) {
+		foreach my $name ($firefox->cache_keys()) {
+			no strict;
+			TODO: {
+				local $TODO = ($major_version < 113 && $name !~ /(^CLEAR_COOKIES|CLEAR_NETWORK_CACHE|CLEAR_IMAGE_CACHE)$/smx) ? "Older firefox can have different values for Firefox::Marionette::Cache constants" : q[];
+				ok($firefox->check_cache_key($name) eq &$name(), "\$firefox->check_cache_key($name) eq Firefox::Marionette::Cache::${name} which is " . &$name());
+			}
+			use strict;
+		}
+	}
+	eval { $firefox->check_cache_key(); };
+	ok(ref $@ eq 'Firefox::Marionette::Exception', "\$firefox->check_cache_key() throws an exception");
+	eval { $firefox->check_cache_key("123!#"); };
+	ok(ref $@ eq 'Firefox::Marionette::Exception', "\$firefox->check_cache_key(\"123!#\") throws an exception");
+	ok($firefox->content(), "\$firefox->content() is called in case of previous exceptions getting the context out of sync");
 	my $capabilities = $firefox->capabilities();
 	ok((ref $capabilities) eq 'Firefox::Marionette::Capabilities', "\$firefox->capabilities() returns a Firefox::Marionette::Capabilities object");
 	if (!grep /^accept_insecure_certs$/, $capabilities->enumerate()) {
@@ -1521,9 +1580,16 @@ SKIP: {
 		($llx, $lly, $urx, $ury) = $page->mediabox();
 		$urx = int $urx; # for darwin
 		$ury = int $ury; # for darwin
-		ok(((centimetres_to_points(12) == $urx) || (centimetres_to_points(12) == $urx - 1)) &&
-			 ((centimetres_to_points(7) == $ury) || (centimetres_to_points(7) == $ury - 1)),
-				"Correct page height of " . centimetres_to_points(7) . " (was actually $ury) and width " . centimetres_to_points(12) . " (was actually $urx)");
+		if ((centimetres_to_points(12) == $urx) || (centimetres_to_points(12) == $urx - 1)) {
+			ok(((centimetres_to_points(12) == $urx) || (centimetres_to_points(12) == $urx - 1)) &&
+				 ((centimetres_to_points(7) == $ury) || (centimetres_to_points(7) == $ury - 1)),
+					"Correct page height of " . centimetres_to_points(7) . " (was actually $ury) and width " . centimetres_to_points(12) . " (was actually $urx)");
+		} else {
+			# at least like this since firefox 112
+			ok(((centimetres_to_points(12) == $ury) || (centimetres_to_points(12) == $ury - 1)) &&
+				 ((centimetres_to_points(7) == $urx) || (centimetres_to_points(7) == $urx - 1)),
+					"Correct page width of " . centimetres_to_points(7) . " (was actually $urx) and height " . centimetres_to_points(12) . " (was actually $ury)");
+		}
 		foreach my $paper_size ($firefox->paper_sizes()) {
 			$raw_pdf = $firefox->pdf(raw => 1, size => $paper_size, page_ranges => [], print_background => 1, shrink_to_fit => 1);
 			$pdf = PDF::API2->open_scalar($raw_pdf);
@@ -1574,6 +1640,18 @@ SKIP: {
 	}
 }
 
+sub github_session_cookie {
+	my ($firefox) = @_;
+	my $session_name = '_gh_sess';
+	my $session_value;
+	foreach my $cookie ($firefox->cookies()) {
+		if ($cookie->name() eq $session_name) {
+			$session_value = $cookie->value();
+		}
+	}
+	return $session_value;
+}
+
 sub centimetres_to_points {
 	my ($centimetres) = @_;
 	my $inches = $centimetres / 2.54;
@@ -2367,7 +2445,7 @@ SKIP: {
 		}
 		ok($count, "Link from metacpan.org has $count attributes");
 		my @scroll_arguments = test_scroll_arguments($number_of_links++);
-		ok($firefox->scroll($link, @scroll_arguments), "Firefox scrolled to the link with arguments of:" . join q[, ], stringify_scroll_arguments(@scroll_arguments));
+		ok($link->scroll(@scroll_arguments), "Firefox scrolled to the link with arguments of:" . join q[, ], stringify_scroll_arguments(@scroll_arguments));
 	}
 	my @images = $firefox->images();
 	foreach my $image (@images) {
@@ -3026,8 +3104,10 @@ SKIP: {
 			}
 		}
 		ok($count == 1, "Downloaded 1 files:$count");
-		my $handle = $firefox->download($download_path);
-		ok($handle->isa('GLOB'), "Obtained GLOB from \$firefox->download(\$path)");
+		my $deprecated_handle = $firefox->download($download_path);
+		ok($deprecated_handle->isa('GLOB'), "Obtained GLOB from \$firefox->downloaded('$download_path')");
+		my $handle = $firefox->downloaded($download_path);
+		ok($handle->isa('GLOB'), "Obtained GLOB from \$firefox->downloaded('$download_path')");
 		my $gz = Compress::Zlib::gzopen($handle, 'rb') or die "Failed to open gzip stream";
 		my $bytes_read = 0;
 		while($gz->gzread(my $buffer, 4096)) {
@@ -4119,7 +4199,10 @@ SKIP: {
 				if ($major_version >= 59) {
 					ok($firefox->scroll($element, { block => 'center' }), "Scroll until the username field is in the center of the screen");
 					$percentage = $firefox->percentage_visible($element);
-					ok($percentage == 100, "Percentage visible is 100% for the username field:$percentage");
+					TODO: {
+						local $TODO = $firefox->capabilities()->platform_name() eq 'mac' ? "mac sometimes doesn't have the correct 100% value, more like 95%" : q[];
+						ok($percentage == 100, "Percentage visible is 100% for the username field:$percentage");
+					}
 				}
 			} else {
 				diag("Skipping checks that require resize to work");
diff --git a/t/03-close.t b/t/03-close.t
new file mode 100644
index 0000000..bb77291
--- /dev/null
+++ b/t/03-close.t
@@ -0,0 +1,22 @@
+#! /usr/bin/perl -w
+
+use strict;
+use JSON();
+use IPC::Open3();
+use Archive::Zip();
+use XML::Parser();
+use lib qw(t/);
+use syscall_tests (qw(close));
+
+*CORE::GLOBAL::close = sub { if (syscall_tests::allow()) { CORE::close $_[0]; } else { $! = POSIX::EIO(); return } };
+
+require Firefox::Marionette;
+
+syscall_tests::run(POSIX::EIO());
+syscall_tests::visible(POSIX::EIO());
+
+no warnings;
+*CORE::GLOBAL::close = sub { return CORE::close $_[0]; };
+use warnings;
+
+syscall_tests::finalise();
diff --git a/t/03-closedir.t b/t/03-closedir.t
new file mode 100644
index 0000000..e8caa7c
--- /dev/null
+++ b/t/03-closedir.t
@@ -0,0 +1,20 @@
+#! /usr/bin/perl -w
+
+use strict;
+use Archive::Zip();
+use XML::Parser();
+use lib qw(t/);
+use syscall_tests (qw(closedir));
+
+*CORE::GLOBAL::closedir = sub { if (syscall_tests::allow()) { CORE::closedir $_[0]; } else { $! = POSIX::EBADF(); return } };
+
+require Firefox::Marionette;
+
+syscall_tests::run(POSIX::EBADF());
+syscall_tests::visible(POSIX::EBADF());
+
+no warnings;
+*CORE::GLOBAL::closedir = sub { return CORE::closedir $_[0]; };
+use warnings;
+
+syscall_tests::finalise();
diff --git a/t/03-fork.t b/t/03-fork.t
new file mode 100644
index 0000000..7b6ad25
--- /dev/null
+++ b/t/03-fork.t
@@ -0,0 +1,22 @@
+#! /usr/bin/perl -w
+
+use strict;
+use lib qw(t/);
+use syscall_tests (qw(fork));
+
+*CORE::GLOBAL::fork = sub { if (syscall_tests::allow()) { CORE::fork; } else { $! = POSIX::ENOMEM(); return } };
+
+require Firefox::Marionette;
+
+syscall_tests::run(POSIX::ENOMEM());
+
+TODO: {
+	local $syscall_tests::TODO = $^O eq 'MSWin32' ? "There are no fork calls in $^O": q[];
+	syscall_tests::visible(POSIX::ENOENT());
+}
+
+no warnings;
+*CORE::GLOBAL::fork = sub { return CORE::fork; };
+use warnings;
+
+syscall_tests::finalise();
diff --git a/t/03-mkdir.t b/t/03-mkdir.t
new file mode 100644
index 0000000..409740b
--- /dev/null
+++ b/t/03-mkdir.t
@@ -0,0 +1,18 @@
+#! /usr/bin/perl -w
+
+use strict;
+use lib qw(t/);
+use syscall_tests (qw(mkdir));
+
+*CORE::GLOBAL::mkdir = sub { if (syscall_tests::allow()) { CORE::mkdir $_[0]; } else { $! = POSIX::EACCES(); return } };
+
+require Firefox::Marionette;
+
+syscall_tests::run(POSIX::EACCES());
+syscall_tests::visible(POSIX::EACCES());
+
+no warnings;
+*CORE::GLOBAL::mkdir = sub { return CORE::mkdir $_[0]; };
+use warnings;
+
+syscall_tests::finalise();
diff --git a/t/03-opendir.t b/t/03-opendir.t
new file mode 100644
index 0000000..80f0a67
--- /dev/null
+++ b/t/03-opendir.t
@@ -0,0 +1,20 @@
+#! /usr/bin/perl -w
+
+use strict;
+use Archive::Zip();
+use XML::Parser();
+use lib qw(t/);
+use syscall_tests (qw(opendir));
+
+*CORE::GLOBAL::opendir = sub { if (syscall_tests::allow()) { CORE::opendir $_[0], $_[1]; } else { $! = POSIX::EACCES(); return } };
+
+require Firefox::Marionette;
+
+syscall_tests::run(POSIX::EACCES());
+syscall_tests::visible(POSIX::EACCES());
+
+no warnings;
+*CORE::GLOBAL::opendir = sub { return CORE::opendir $_[0], $_[1]; };
+use warnings;
+
+syscall_tests::finalise();
diff --git a/t/03-read.t b/t/03-read.t
new file mode 100644
index 0000000..b0edba4
--- /dev/null
+++ b/t/03-read.t
@@ -0,0 +1,26 @@
+#! /usr/bin/perl -w
+
+use strict;
+use JSON();
+BEGIN {
+	if (($^O eq 'cygwin') || ($^O eq 'darwin') || ($^O eq 'MSWin32')) {
+	} else {
+		require Crypt::URandom;
+		require FileHandle;
+	}
+}
+use lib qw(t/);
+use syscall_tests (qw(read));
+
+*CORE::GLOBAL::read = sub { if (syscall_tests::allow()) { CORE::read $_[0], $_[1], $_[2]; } else { $! = POSIX::EACCES(); return } };
+
+require Firefox::Marionette;
+
+syscall_tests::run(POSIX::EACCES());
+syscall_tests::visible(POSIX::EACCES());
+
+no warnings;
+*CORE::GLOBAL::read = sub { return CORE::read $_[0], $_[1], $_[2]; };
+use warnings;
+
+syscall_tests::finalise();
diff --git a/t/03-seek.t b/t/03-seek.t
new file mode 100644
index 0000000..362c65b
--- /dev/null
+++ b/t/03-seek.t
@@ -0,0 +1,18 @@
+#! /usr/bin/perl -w
+
+use strict;
+use lib qw(t/);
+use syscall_tests (qw(seek));
+
+*CORE::GLOBAL::seek = sub { if (syscall_tests::allow()) { return CORE::seek $_[0], $_[1], $_[2]; } else { $! = POSIX::ESPIPE(); return } };
+
+require Firefox::Marionette;
+
+syscall_tests::run(POSIX::ESPIPE());
+syscall_tests::visible(POSIX::ESPIPE());
+
+no warnings;
+*CORE::GLOBAL::seek = sub { return CORE::seek $_[0], $_[1], $_[2]; };
+use warnings;
+
+syscall_tests::finalise();
diff --git a/t/03-stat.t b/t/03-stat.t
new file mode 100644
index 0000000..065d0f3
--- /dev/null
+++ b/t/03-stat.t
@@ -0,0 +1,23 @@
+#! /usr/bin/perl -w
+
+use strict;
+use Archive::Zip();
+use lib qw(t/);
+use syscall_tests (qw(stat));
+
+*CORE::GLOBAL::stat = sub { if (syscall_tests::allow()) { CORE::stat $_[0]; } else { $! = POSIX::ENOENT(); return } };
+
+require Firefox::Marionette;
+
+syscall_tests::run(POSIX::ENOENT());
+
+TODO: {
+	local $syscall_tests::TODO = (($^O eq 'darwin') or ($^O eq 'MSWin32') or ($^O eq 'cygwin')) ? "There are no stat calls when $^O firefox starts": q[];
+	syscall_tests::visible(POSIX::ENOENT());
+}
+
+no warnings;
+*CORE::GLOBAL::stat = sub { return CORE::stat $_[0]; };
+use warnings;
+
+syscall_tests::finalise();
diff --git a/t/03-sysopen.t b/t/03-sysopen.t
new file mode 100644
index 0000000..6035e98
--- /dev/null
+++ b/t/03-sysopen.t
@@ -0,0 +1,18 @@
+#! /usr/bin/perl -w
+
+use strict;
+use lib qw(t/);
+use syscall_tests (qw(sysopen));
+
+*CORE::GLOBAL::sysopen = sub { my $handle = CORE::sysopen $_[0], $_[1], $_[2]; if (($handle) && (syscall_tests::allow())) { return $handle } else { $! = POSIX::EACCES(); return } };
+
+require Firefox::Marionette;
+
+syscall_tests::run(POSIX::EACCES());
+syscall_tests::visible(POSIX::EACCES());
+
+no warnings;
+*CORE::GLOBAL::sysopen = sub { return CORE::sysopen $_[0], $_[1], $_[2]; };
+use warnings;
+
+syscall_tests::finalise();
diff --git a/t/author/bulk_test.pl b/t/author/bulk_test.pl
index 3d15493..fc569a9 100755
--- a/t/author/bulk_test.pl
+++ b/t/author/bulk_test.pl
@@ -40,6 +40,19 @@ $ENV{DEVEL_COVER_DB_FORMAT} = $devel_cover_db_format;
 system { 'cover' } 'cover', '-delete' and die "Failed to 'cover' for " . ($ENV{FIREFOX_BINARY} || 'firefox');
 MAIN: {
 	my $cwd = Cwd::cwd();
+	my $test_directory = File::Spec->catdir($cwd, 't');
+	my $test_directory_handle = DirHandle->new($test_directory);
+	my @syscall_entries;
+	if ($test_directory_handle) {
+		while(my $entry = $test_directory_handle->read()) {
+			next if ($entry eq File::Spec->updir());
+			next if ($entry eq File::Spec->curdir());
+			next if ($entry !~ /03\-/smx);
+			push @syscall_entries, File::Spec->catfile($test_directory, $entry);
+		}
+	} else {
+		die "Failed to open test directory:$!";
+	}
 	my @servers;
         my $csv = Text::CSV_XS->new ({ binary => 1, auto_diag => 1 });
 	my $servers_path = $cwd . '/servers.csv';
@@ -73,10 +86,10 @@ MAIN: {
 			$background_pids->{$pid} = $server;
 		} elsif (defined $pid) {
 			eval {
-				my $win32_local_alarm = 600;
+				my $win32_local_alarm = 900;
 				my $cygwin_local_alarm = 2700;
 				my $cygwin_remote_alarm = 7200;
-				my $physical_local_alarm = 600;
+				my $physical_local_alarm = 900;
 				$ENV{FIREFOX_ALARM} = $win32_remote_alarm;
 				$ENV{FIREFOX_NO_RECONNECT} = 1;
 				if ((lc $server->{type}) eq 'virsh') {
@@ -509,6 +522,9 @@ MAIN: {
 		}
 		_check_for_background_processes($background_pids, @servers);
 	}
+	foreach my $syscall_entry (@syscall_entries) {
+		_multiple_attempts_execute($^X, [ ($devel_cover_inc ? $devel_cover_inc : ()), '-Ilib', $syscall_entry ], {});
+	}
 	while (_check_for_background_processes($background_pids, @servers)) {
 		sleep 10;
 	}
@@ -516,13 +532,14 @@ MAIN: {
 		if ((lc $server->{type}) eq 'virsh') {
 			if (_virsh_node_running($server)) {
 				_virsh_shutdown($server);
+				_sleep_until_shutdown($server);
 			}
 		}
 	}
 	chdir $cwd or die "Failed to chdir to '$cwd':$EXTENDED_OS_ERROR";
 	if (-d "$cwd/$cover_db_name") {
 		$ENV{DEVEL_COVER_DB_FORMAT} = $devel_cover_db_format;
-		system { 'cover' } 'cover', '-ignore', $test_marionette_file and die "Failed to 'cover'";
+		system { 'cover' } 'cover', '-ignore_re', '^t/*' and die "Failed to 'cover'";
 	} else {
 		warn "No coverage generated\n";
 	}
@@ -644,6 +661,7 @@ sub _check_for_background_processes {
 		foreach my $server (@{$servers}) {
 			if ((lc $server->{type}) eq 'virsh') {
 				_virsh_shutdown($server);
+				_sleep_until_shutdown($server);
 			}
 		}
 	}
diff --git a/t/pod-coverage.t b/t/pod-coverage.t
index 6da2c37..3e2f32e 100755
--- a/t/pod-coverage.t
+++ b/t/pod-coverage.t
@@ -8,6 +8,7 @@ all_pod_coverage_ok({ trustme => [
 		 qr/^(find_elements?|page_source|send_keys)$/,
 		 qr/^(active_frame|switch_to_shadow_root)$/,
 		 qr/^(chrome_window_handle|chrome_window_handles|current_chrome_window_handle)$/,
+		 qr/^(download)$/,
 		 qr/^(ftp)$/,
 		 qr/^(xvfb)$/,
 		 qr/^(TO_JSON)$/,
diff --git a/t/stub.pl b/t/stub.pl
new file mode 100755
index 0000000..bbdf238
--- /dev/null
+++ b/t/stub.pl
@@ -0,0 +1,80 @@
+#! /usr/bin/perl -w
+
+use strict;
+use warnings;
+use Getopt::Long();
+use File::Spec();
+use FileHandle();
+use Fcntl();
+use Socket();
+use JSON();
+
+MAIN: {
+	my %options;
+	Getopt::Long::GetOptions(\%options, 'version', 'marionette', 'headless', 'profile:s', 'no-remote', 'new-instance', 'devtools', 'safe-mode');
+	my $browser_version = "112.0.2";
+	if ($options{version}) {
+		print "Mozilla Firefox $browser_version\n";
+		exit 0;
+	}
+	socket my $server, Socket::PF_INET(), Socket::SOCK_STREAM(), 0 or die "Failed to create a socket:$!";
+	bind $server, Socket::sockaddr_in( 0, Socket::INADDR_LOOPBACK() ) or die "Failed to bind socket:$!";
+	listen $server, Socket::SOMAXCONN() or die "Failed to listen:$!";
+	my $port = ( Socket::sockaddr_in( getsockname $server ) )[0];
+	my $prefs_path = File::Spec->catfile($options{profile}, 'prefs.js');
+	my $old_prefs_handle = FileHandle->new($prefs_path, Fcntl::O_RDONLY()) or die "Failed to open $prefs_path for reading:$!";
+	my $new_prefs_path = File::Spec->catfile($options{profile}, 'prefs.new');
+	my $new_prefs_handle = FileHandle->new($new_prefs_path, Fcntl::O_CREAT() | Fcntl::O_EXCL() | Fcntl::O_WRONLY(), Fcntl::S_IRUSR() | Fcntl::S_IWUSR()) or die "Failed to open $new_prefs_path for writing:$!";
+	while(my $line = <$old_prefs_handle>) {
+		if ($line =~ /^user_pref\("marionette.port",[ ]0\);/smx) {
+			print {$new_prefs_handle} qq[user_pref("marionette.port", $port);\n] or die "Failed to write to $new_prefs_path:$!";	
+		} else {
+			print {$new_prefs_handle} $line or die "Failed to write to $new_prefs_path:$!";	
+		}
+	}
+	close $new_prefs_handle or die "Failed to close $new_prefs_path:$!";
+	close $old_prefs_handle or die "Failed to close $prefs_path:$!";
+	rename $new_prefs_path, $prefs_path or die "Failed to rename $new_prefs_path to $prefs_path:$!";
+	my $paddr = accept(my $client, $server);
+	syswrite $client, qq[50:{"applicationType":"gecko","marionetteProtocol":3}] or die "Failed to write to socket:$!";
+	my $request = _get_request($client);
+	my $platform = $^O;
+	my $headless = $options{headless} ? 'true' : 'false';
+	my $response_type = 1;
+	my $profile_path = $options{profile};
+	$profile_path =~ s/\\/\\\\/smxg;
+	my $capabilities = qq([1,1,null,{"sessionId":"5a5f9a08-0faa-4794-aa85-ee85980ce422","capabilities":{"browserName":"firefox","browserVersion":"$browser_version","platformName":"$platform","acceptInsecureCerts":false,"pageLoadStrategy":"normal","setWindowRect":true,"timeouts":{"implicit":0,"pageLoad":300000,"script":30000},"strictFileInteractability":false,"unhandledPromptBehavior":"dismiss and notify","moz:accessibilityChecks":false,"moz:buildID":"20230427144338","moz:headless":$headless,"moz:platformVersion":"6.2.14-200.fc37.x86_64","moz:processID":$$,"moz:profile":"$profile_path","moz:shutdownTimeout":60000,"moz:useNonSpecCompliantPointerOrigin":false,"moz:webdriverClick":true,"moz:windowless":false,"proxy":{}}}]);
+	my $capability_length = length $capabilities;
+	syswrite $client, $capability_length . q[:] . $capabilities or die "Failed to write to socket:$!";
+	while(1) {
+		$request = _get_request($client);
+		my $message_id = $request->[1];
+		if ($request->[2] eq 'Marionette:Quit') {
+			syswrite $client, qq(60:[$response_type,$message_id,null,{"cause":"shutdown","forced":false,"in_app":true}]) or die "Failed to write to socket:$!";
+			last;
+		} elsif ($request->[2] eq 'Addon:Install') {
+			syswrite $client, qq(79:[$response_type,$message_id,null,{"value":"6eea9fdc37a5d8fbcbbecd57ee7272669e828a31\@temporary-addon"}]) or die "Failed to write to socket:$!";
+		} elsif ($request->[2] eq 'WebDriver:Print') {
+			syswrite $client, qq(1475:[$response_type,$message_id,null,{"value":"JVBERi0xLjUKJbXtrvsKNCAwIG9iago8PCAvTGVuZ3RoIDUgMCBSCiAgIC9GaWx0ZXIgL0ZsYXRlRGVjb2RlCj4+CnN0cmVhbQp4nDNUMABCXUMgYW5ppJCcy1XIFahQyGVkoWdsaqQApUxNTfWMDQwVzI0hdFGqQrhCHpehAggWpSvoJxoopBcT1pTGFcgFACsfF2cKZW5kc3RyZWFtCmVuZG9iago1IDAgb2JqCiAgIDc1CmVuZG9iagozIDAgb2JqCjw8CiAgIC9FeHRHU3RhdGUgPDwKICAgICAgL2EwIDw8IC9DQSAxIC9jYSAxID4+CiAgID4+Cj4+CmVuZG9iago2IDAgb2JqCjw8IC9UeXBlIC9PYmpTdG0KICAgL0xlbmd0aCA3IDAgUgogICAvTiAxCiAgIC9GaXJzdCA0CiAgIC9GaWx0ZXIgL0ZsYXRlRGVjb2RlCj4+CnN0cmVhbQp4nD3NMQvCMBQE4L2/4hbnJlEUIXRoC8VBkOgmDiU+pEsSkkbsvzeJ1PG+d7wTYJWUqG+LI9SX8UXYgFdADp7MDA4GVeBMz2ls7Qf3RAx7LnA4CjzKsbNmTvWA3b8/eBsdpMwh599G0ZWuSf1ogstbeln5hNlHWlOXWj29J01qaDM2TfmvKNjoNQVsy2biL4KVMvQKZW5kc3RyZWFtCmVuZG9iago3IDAgb2JqCiAgIDE0NwplbmRvYmoKOCAwIG9iago8PCAvVHlwZSAvT2JqU3RtCiAgIC9MZW5ndGggMTEgMCBSCiAgIC9OIDMKICAgL0ZpcnN0IDE2CiAgIC9GaWx0ZXIgL0ZsYXRlRGVjb2RlCj4+CnN0cmVhbQp4nE2PTQvCMBBE7/kVc7NFaHZrxQ+kF8WLCCLexEOosQZKt6QR1F+vRgSvs/OWNwxSM4xJMYGnrBYL6MOjs9A7U9teAdAbd+5xRA7CHqcYLeXWBrAqy0jsvJxvlfVIKuO8gDOeZAWSawhdP9c6prU33dVVfSa+TtPvG29NkDe2ladrGoO18/Yi97+rk3ZlgkWymueUj2jMIybiohgyDYjSn8JXemmCaaSOeBwA/lh/Si9o4j6UCmVuZHN0cmVhbQplbmRvYmoKMTEgMCBvYmoKICAgMTgxCmVuZG9iagoxMiAwIG9iago8PCAvVHlwZSAvWFJlZgogICAvTGVuZ3RoIDU3CiAgIC9GaWx0ZXIgL0ZsYXRlRGVjb2RlCiAgIC9TaXplIDEzCiAgIC9XIFsxIDIgMl0KICAgL1Jvb3QgMTAgMCBSCiAgIC9JbmZvIDkgMCBSCj4+CnN0cmVhbQp4nBXKQQ0AIAwEwW1LCLyQgB1cIA8TeIPrZ7K5HPCe08CpYNxkJEdYEd6TmZemEG6xtMWGD8f2BIAKZW5kc3RyZWFtCmVuZG9iagpzdGFydHhyZWYKODYzCiUlRU9GCg=="}]) or die "Failed to write to socket:$!";
+		} elsif ($request->[2] eq 'WebDriver:TakeScreenshot') {
+			syswrite $client, qq(423:[$response_type,$message_id,null,{"value":"iVBORw0KGgoAAAANSUhEUgAABVYAAAAICAYAAAAShaQyAAAA8UlEQVR4Xu3YsQ0AIAwEMbL/0ICYgOud+isr1c2+txwBAgQIECBAgAABAgQIECBAgAABAgQIfAuMsPptZUiAAAECBAgQIECAAAECBAgQIECAAIEnIKx6BAIECBAgQIAAAQIECBAgQIAAAQIECEQBYTWCmRMgQIAAAQIECBAgQIAAAQIECBAgQEBY9QMECBAgQIAAAQIECBAgQIAAAQIECBCIAsJqBDMnQIAAAQIECBAgQIAAAQIECBAgQICAsOoHCBAgQIAAAQIECBAgQIAAAQIECBAgEAWE1QhmToAAAQIECBAgQIAAAQIECBAgQIAAgQMdMh/pgqHYUwAAAABJRU5ErkJggg=="}]) or die "Failed to write to socket:$!";
+		} else {
+			die "Unsupported method in stub firefox";
+		}
+	}
+	close $client or die "Failed to close socket:$!";
+	exit 0;
+}
+
+sub _get_request {
+	my ($client) = @_;
+	my $length_buffer = q[];
+	sysread $client, my $buffer, 1 or die "Failed to read from socket:$!";
+	while($buffer ne q[:]) {
+		$length_buffer .= $buffer;
+		sysread $client, $buffer, 1 or die "Failed to read from socket:$!";
+	}
+	sysread $client, $buffer, $length_buffer or die "Failed to read from socket:$!";
+	my $request = JSON->new()->utf8()->decode($buffer);
+	return $request;
+}
diff --git a/t/syscall_tests.pm b/t/syscall_tests.pm
new file mode 100644
index 0000000..8a436c1
--- /dev/null
+++ b/t/syscall_tests.pm
@@ -0,0 +1,119 @@
+package syscall_tests;
+
+use strict;
+use warnings;
+use Test::More;
+use File::Spec();
+use Fcntl();
+use File::Path();
+use Cwd();
+BEGIN {
+	if ($^O eq 'MSWin32') {
+		require Win32;
+		require Win32::Process;
+		require Win32API::Registry;
+	}
+}
+
+my $base_directory;
+my $syscall_count = 0;
+my $syscall_error_at_count = 0;
+my $function;
+my %parameters;
+
+my @CHARS = (qw/ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
+                 a b c d e f g h i j k l m n o p q r s t u v w x y z
+                 0 1 2 3 4 5 6 7 8 9 _
+               /);
+
+sub import {
+	my $class = shift @_;
+	$function = shift;
+
+	unless ($ENV{RELEASE_TESTING}) {
+		plan( skip_all => "Author tests not required for installation" );
+	}
+
+	$base_directory = File::Spec->catdir(File::Spec->tmpdir(), 'firefox_marionette_test_suite_syscall_' . 'X' x 11);
+	my $end = ( $] >= 5.006 ? "\\z" : "\\Z" );
+	$base_directory =~ s/X(?=X*$end)/$CHARS[ int( rand( @CHARS ) ) ]/gesmx;
+	mkdir $base_directory, Fcntl::S_IRWXU() or die "Failed to create temporary directory:$!";
+	$ENV{TMPDIR} = $base_directory;
+
+	return;
+}
+
+sub allow {
+	my ($package, $file, $line) = caller;
+	if ((defined $syscall_error_at_count) && ($syscall_count == $syscall_error_at_count)) {
+		$syscall_count += 1;
+		return 0;
+	} else {
+		$syscall_count += 1;
+		return 1;
+	}
+}
+
+sub run {
+	my ($class, $expected_error_as_posix) = @_;
+	my $cwd = Cwd::cwd();
+	%parameters = ( binary => File::Spec->catfile($cwd, 't', 'stub.pl'), har => 1 );
+	my $success = 0;
+	while(!$success) {
+		$syscall_count = 0;
+		eval {
+			my $firefox = Firefox::Marionette->new(%parameters);
+			$firefox->pdf();
+			$firefox->selfie();
+			my $final = $syscall_error_at_count;
+			$syscall_error_at_count = undef;
+			ok($syscall_count >= 0 && $firefox->quit() == 0, "Firefox exited okay after $final successful $function calls");
+			$success = 1;
+		} or do {
+			chomp $@;
+			my $actual_error_message = $@;
+			my $expected_error_message = quotemeta POSIX::strerror($expected_error_as_posix);
+			ok($actual_error_message =~ /(?:$expected_error_message|[ ]exited[ ]with[ ]a[ ][1])/smx, "Firefox failed with $function count set to $syscall_error_at_count:" . $actual_error_message);
+			$syscall_error_at_count += 1;
+		};
+	}
+	my $firefox = Firefox::Marionette->new(%parameters);
+	ok($firefox->quit() == 0, "Firefox exited okay when $function is reset");
+}
+
+sub visible {
+	my ($class, $expected_error_as_posix) = @_;
+	$syscall_error_at_count = 0;
+	delete $ENV{DISPLAY};
+	$parameters{visible} = 1;
+	my $success = 0;
+	while(!$success) {
+		$syscall_count = 0;
+		eval {
+			my $firefox = Firefox::Marionette->new(%parameters);
+			$firefox->pdf();
+			$firefox->selfie();
+			my $final = $syscall_error_at_count;
+			$syscall_error_at_count = undef;
+			ok($syscall_count > 0 && $firefox->quit() == 0, "Firefox (visible => 1) exited okay after $final successful $function calls");
+			$success = 1;
+		} or do {
+			chomp $@;
+			my $actual_error_message = $@;
+			my $expected_error_message = quotemeta POSIX::strerror($expected_error_as_posix);
+			ok($actual_error_message =~ /(?:$expected_error_message|[ ]exited[ ]with[ ]a[ ][1])/smx, "Firefox (visible => 1) failed with $function count set to $syscall_error_at_count:" . $actual_error_message);
+			$syscall_error_at_count += 1;
+		};
+	}
+}
+
+sub finalise {
+	my ($class) = @_;
+	my $firefox = Firefox::Marionette->new(%parameters);
+	ok($firefox->quit() == 0, "Firefox (visible => 1) exited okay when $function is reset");
+	File::Path::rmtree( $base_directory, 0, 0 );
+
+	done_testing();
+}
+
+1;

More details

Full run details

Historical runs