Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Comparing changes

Choose two branches to see what's changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
base fork: norm/webkit2png-driver
base: 0c1c920d51
...
head fork: norm/webkit2png-driver
compare: b4405b4a8b
Checking mergeability… Don't worry, you can still create the pull request.
  • 13 commits
  • 10 files changed
  • 0 commit comments
  • 1 contributor
View
194 README.markdown
@@ -1,11 +1,195 @@
webkit2png-driver
=================
+Wrapper around `webkit2png` for taking and archiving screengrabs of websites,
+with some support for interaction such logging in/out and filling out forms.
-Something something screenshots.
+This can be used as a manual pre-flight checklist before a website deploy, a
+permanent archive of the evolution of a site's appearance, a regression tester
+(when combined with some sort of image analysis tool), or just for teh shiny.
+Installing
+----------
+1. If you are not using OS X ... er ... good luck with that.
+1. Install my fork of [webkit2png][webkit2png] in your PATH:
+ * `sudo cp webkit2png /usr/bin`
+ * `sudo chmod 755 /usr/bin/webkit2png`
+1. Install `cpanm` (makes installing perl modules easy):
+ * *homebrew*: `brew install cpanminus`
+ * *not homebrew*: `curl -L http://cpanmin.us | sudo perl - --self-upgrade`
+1. Install required perl modules:
+ * `sudo cpanm Config::Std IO::All http://bumph.cackhanded.net/Template-Jigsaw-v0.1.tar.gz`
+1. Put this script in your PATH:
+ * `sudo cp webkit2png-driver /usr/bin`
+ * `sudo chmod 755 /usr/bin/webkit2png-driver`
-Installation
-------------
+Using
+-----
+Make a directory to keep your configuration and resulting screengrabs in.
+The rest of the instructions assume that this is your current working
+directory (ie. `cd` into it before going any further).
- 1. If you have `cpanm` installed: `sudo cpanm something something`
- 2. If you do not have `cpanm` installed: `curl something something | perl args`
+You will need at least one INI file to describe the screengrabs you want
+taken. A couple of examples are included in this repository that you might
+like to copy to your directory and then modify. Although they can be named
+anything, the default is `grabs.ini`.
+
+ # copy a sample INI file
+ % cp .../webkit2png-driver/sample-twitter.ini grabs.ini
+
+
+### Take the screengrabs
+
+The default mode of `webkit2png-driver` is to take all of the screengrabs
+specified in your `grabs.ini` file.
+
+ # take all screengrabs
+ % webkit2png-driver
+
+ # use a different config file
+ % webkit2png-driver -c other.ini
+
+ # take only those screengrabs that match the argument(s);
+ # arguments can be full or partial names, and support
+ # wildcards as it is really a regexp match
+ % webkit2png-driver homepage/logged-out
+ % webkit2png-driver nojs noflash
+ % webkit2png-driver home.*out
+
+### Generate HTML indexes
+
+If you want to more easily explore the screengrabs you have taken (especially
+useful if you are archiving them), you can generate HTML indexes.
+
+First, you will need to copy the sample HTML templates from this repository
+to your screengrabs directory. You can customise them later (if you can
+stand to look at the embedded perl code).
+
+ % cp -R .../webkit2png-driver/sample_templates ./_templates
+
+To regenerate the indexes:
+
+ % webkit2png-driver -i
+
+To temporarily explore the screengrabs using the HTML indexes without
+transferring them to a web server first:
+
+ # indexes browseable at <http://localhost:8000/>
+ % python -m SimpleHTTServer
+
+### Permanently archive screengrabs
+
+If you want to keep more permanent copies of the screengrabs (eg. to be able
+to demonstrate later how the site has evolved over time), issue an archive
+command after you have taken the screengrabs.
+
+ % webkit2png-driver -a [name of this archive]
+
+Each archived copy needs a name (some examples include `alpha-release`,
+`pre-awesome`, `deploy-20111122`, `tag-revolver-ocelot`). Archiving copies
+the 'latest' screengrabs to files matching the name, and then regenerates
+the HTML indexes.
+
+Using the same name again will overwrite previous archived copies without
+any issued warnings.
+
+
+INI options
+-----------
+The configuration file is in the "INI" format. Options are on a line with
+the key and value separated by an equals sign (=). Sections (which are the
+individual URLs to be screengrabbed) are named in square brackets ([]).
+Comments are lines that start with a hash (#).
+
+The file should start with any default options.
+
+Also see `sample-twitter.ini` for an example.
+
+### Default options
+
+* **base**: base URL, eg. `http://twitter.com`
+* **title**: title to put into generated HTML indexes
+ (default: same as **base**)
+* **width**: width of full size screengrabs in pixels
+ (default: 1024)
+* **delay**: delay after page load before taking screengrabs in seconds
+* **loggedout**: enforce starting screengrabs in a logged out state
+ (see "Using logins/forms")
+* **make_clipped**: generate the clipped screengrabs
+ (default: 1; 0 to suppress these files)
+* **make_fullsize**: generate the full size screengrabs
+ (default: 1; 0 to suppress these files)
+* **make_thumb**: generate the thumbnail screengrabs
+ (default: 1; 0 to suppress these files)
+* **clipheight**: height of clipped screengrabs in pixels
+ (default: 300)
+* **clipwidth**: width of clipped screengrabs in pixels
+ (default: 250)
+
+### Screengrab options
+
+Individual grabs can override any of the default options as well as using
+these options:
+
+* **url**: the URL to grab; **base** is prepended unless it is an absolute URL
+* **login**: should be logged in as this account before taking the screengrab
+ (see "Using logins/forms")
+* **nojs**: take screengrab with JavaScript support turned off
+* **js**: JavaScript to execute before taking the screengrab
+* **timeout**: execute the JavaScript specified in **js** after
+ this many milliseconds (wraps it in a setTimeout call)
+* **chain_after**: section name of another screengrab that this one should
+ be performed immediately after (most useful for chaining the after-effects
+ of form submissions)
+* **click**: jQuery selector specifying element(s) to be "clicked" before
+ taking the screengrab
+* **fill_name**: jQuery selector specifying the input element(s) to fill out
+ with a random name (John [12 random letters] Doe)
+* **fill_email**: jQuery selector specifying the input element(s) to fill out
+ with a random email ([12 random letters]@mailinator.com)
+* **fill_pass**: jQuery selector specifying the input element(s) to fill out
+ with a random password ([12 random letters])
+* **submit**: jQuery selector specifying the element to "click" after
+ filling out the form to submit it
+
+Using logins/forms
+------------------
+`webkit2png-driver` has some limited support for taking screen grabs with
+different logins, and filling out forms. **Note: these all expect jQuery to be
+loaded in the webpage**.
+
+### Logging in
+
+Create a user with a section called `user#[username]`, and provide the
+following options:
+
+* **username**: the ID/username/email of the login
+* **password**: the password of the login
+
+Create a section called `form#login`, and provide the following options:
+
+* **url**
+* **username**: jQuery selector specifying the input element to fill out
+ with the user ID/username/email
+* **password**: jQuery selector specifying the input element to fill out
+ with the user password
+* **checkbox**: jQuery selector specifying the input element(s) to click
+ before submitting the login form (eg. "Agree to T&Cs" or "Remember me")
+* **submit**: jQuery selector specifying the element to "click" to after
+ filling out the username and password.
+
+Create a section called `form#logout`, and provide the following options:
+
+* **url**
+* **submit**: jQuery selector specifying the element to "click" to log the
+ user out
+
+Then any screengrab can have the option **login** set to `[username]`
+that matches a `user#[username]` section to be logged in as that user before
+taking the screengrab.
+
+No error detection is performed on the submission of logins/logouts/forms,
+so if they fail for some reason the screengrabs will not be in the correct
+state.
+
+
+[webkit2png]: https://github.com/norm/webkit2png
View
83 sample-twitter.ini
@@ -0,0 +1,83 @@
+# Sample INI file for webkit2png-driver.
+# https://github.com/norm/webkit2png-driver/blob/master/sample-twitter.ini
+#
+# Get a few sample pages from Twitter. In order to try this, you'll need to
+# fill in the username & password under 'user#sample' with that of
+# a real twitter account.
+
+base = https://twitter.com
+width = 1200
+loggedout = 1
+
+[user#sample]
+username = FILL_IN_USERNAME
+password = FILL_IN_PASSWORD
+
+[form#logout]
+url = /
+submit = a.signout-button
+
+[form#login]
+url = /
+username = form.signin .username input
+password = form.signin .password input
+checkbox = form.signin .subchck input
+submit = form.signin button.submit
+
+
+[dev]
+url = https://dev.twitter.com/
+
+[blog]
+url = http://blog.twitter.com/
+
+[status]
+url = http://status.twitter.com/
+
+[homepage/logged-out]
+url = /
+
+[single-tweet/proper-url]
+url = /cackhanded/status/34992005834612737
+
+[single-tweet/hashbang-with-delay]
+url = /#!/cackhanded/status/34992005834612737
+delay = 5
+
+[single-tweet/hashbang-no-delay]
+url = /#!/cackhanded/status/34992005834612737
+
+[single-tweet/hashbang-no-js]
+url = /#!/cackhanded/status/34992005834612737
+nojs = 1
+
+[single-tweet/proper-url-no-js]
+url = /cackhanded/status/34992005834612737
+nojs = 1
+
+[homepage/logged-out-nojs]
+url = /
+nojs = 1
+
+[homepage/logged-in]
+url = /
+login = sample
+delay = 5
+
+[homepage/logged-in-no-js]
+url = /
+login = sample
+nojs = 1
+
+[compose-tweet]
+url = /
+click = a#new-tweet
+delay = 5
+login = sample
+
+[logged-out-page/with-js]
+url = /?lang=en&logged_out=1#!/download
+
+[logged-out-page/no-js]
+url = /?lang=en&logged_out=1#!/download
+nojs = 1
View
13 sample_templates/_base/base_template.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Screen grabs</title>
+ [[include css]]
+</head>
+<body>
+ [[include h1]]
+ [[include nav]]
+ [[include grabs]]
+ [[include commits]]
+</body>
+</html>
View
10 sample_templates/_base/commits.html
@@ -0,0 +1,10 @@
+<h2 id='commits'>List of all commits</h2>
+<ul>
+ <li><a href='/'>latest</a></li>
+ {{
+ foreach my $commit ( sort keys %{ $grabs{'commit'} } ) {
+ next if $commit eq 'latest';
+ }}
+ <li><a href='${commit}/'>${commit}</a></li>
+ {{ } }}
+</ul>
View
60 sample_templates/_base/css.html
@@ -0,0 +1,60 @@
+<style type='text/css'>
+ html, body, p, ul, li, h1, h2 {
+ margin: 0;
+ padding: 0;
+ }
+ body {
+ background: #ccc;
+ margin: 0;
+ padding: 1em 3em 3em;
+ }
+ a {
+ color: #039;
+ }
+ h1 {
+ font-family: 'Helvetica Neue', sans-serif;
+ font-weight: 300;
+ font-size: 5em;
+ }
+ p#nav {
+ margin: 1em 0 2em;
+ }
+ p#nav em {
+ display: block;
+ font-style: normal;
+ font-size: 1.4em;
+ font-weight: bold;
+ margin-bottom: 0.5em;
+ }
+
+ #grabs {
+ padding: 2em 0 0;
+ zoom: 1;
+ }
+ #grabs:after {
+ content: '';
+ display: block;
+ clear: both;
+ }
+ #grabs li {
+ padding-top: 300px;
+ position: relative;
+ float: left;
+ width: 250px;
+ margin-right: 50px;
+ min-height: 100px;
+ list-style: none;
+ }
+ #grabs li h2 {
+ font-size: 1.5em;
+ padding: 0.5em 0;
+ }
+ #grabs a.grab {
+ position: absolute;
+ top: 0;
+ left: 0;
+ }
+ #grabs img {
+ -webkit-box-shadow: #000 0 0 10px;
+ }
+</style>
View
14 sample_templates/_base/grabs.html
@@ -0,0 +1,14 @@
+<ul id='grabs'>
+{{
+ foreach my $grab ( sort keys %{ $grabs{'commit'}{$commit} } ) {
+ my $text = $grab;
+ $text =~ s{/}{ }g;
+}}
+ <li>
+ <h2><a href='/${grab}/'>${text}</a></h2>
+ <a href='/${grab}/${commit}-full.png' class='grab'>
+ <img src='/${grab}/${commit}-clipped.png'}>
+ </a>
+ </li>
+{{ } }}
+</ul>
View
1  sample_templates/_base/h1.html
@@ -0,0 +1 @@
+<h1>${title}</h1>
View
8 sample_templates/_base/nav.html
@@ -0,0 +1,8 @@
+{{ if ( defined $commit ) { }}
+ <p id='nav'>
+ <em>Screen grabs for <code>${commit}</code>.</em>
+ The last snapshot taken was
+ <a href='/${latest_commit}/index.html'>${latest_commit}</a>.
+ <a href='#commits'>List of all commits</a>.
+ </p>
+{{ } }}
View
21 sample_templates/section/grabs.html
@@ -0,0 +1,21 @@
+<ul id='grabs'>
+{{
+ my $latest_first = sub {
+ return 1 if $b =~ m{^latest};
+ return -1 if $a =~ m{^latest};
+ return $a cmp $b;
+ };
+
+ foreach my $grab ( sort $latest_first keys %{ $grabs{'section'}{$section} } ) {
+ next unless $grab =~ s{-full.png$}{};
+ my $text = $grab;
+ $text =~ s{/}{ }g;
+}}
+ <li>
+ <h2><a href='/${grab}/'>${text}</a></h2>
+ <a href='/${section}/${grab}-full.png' class='grab'>
+ <img src='/${section}/${grab}-clipped.png'}>
+ </a>
+ </li>
+{{ } }}
+</ul>
View
332 webkit2png-driver
@@ -5,6 +5,9 @@ use Config::Std;
use File::Copy;
use File::Path;
use Getopt::Long;
+use IO::All -utf8;
+use Pod::Usage;
+use Template::Jigsaw;
my %base_config = (
delay => 0,
@@ -12,21 +15,33 @@ my %base_config = (
make_fullsize => 1,
make_thumb => 1,
clipwidth => 250,
+ clipheight => 300,
width => 1024,
);
my %option;
-GetOptions( \%option, 'archive|a=s' );
+GetOptions(
+ \%option,
+ 'help|h',
+ 'archive|a=s',
+ 'generate-indexes|i',
+ 'config|c=s',
+ );
+
+pod2usage() if $option{'help'};
-my $conf_file = shift // 'driver.ini';
+my $conf_file = $option{'config'} // 'grabs.ini';
my %config = get_configuration( $conf_file );
my $current_login = '';
-
+my $generator = get_template_generator();
+my $grabs_title = get_title();
my @pages;
-if ( $#ARGV > -1 ) {
- @pages = @ARGV;
-}
-else {
+
+$| = 1;
+
+
+
+if ( $option{'generate-indexes'} || $option{'archive'} ) {
foreach my $section ( keys %config ) {
# skip anything that is not a screen grab
next if $section =~ m{\#};
@@ -34,22 +49,48 @@ else {
push @pages, $section;
}
-}
-
-if ( $option{'archive'} ) {
- archive_files();
+
+ archive_files()
+ if $option{'archive'};
+ generate_indexes()
}
else {
+ # performing screengrabs is the default option
+ if ( $#ARGV > -1 ) {
+ foreach my $page ( @ARGV ) {
+ foreach my $section ( keys %config ) {
+ push( @pages, $section )
+ if $section =~ m{$page};
+ }
+ }
+ }
+ else {
+ foreach my $section ( keys %config ) {
+ # skip anything that is not a screen grab
+ next if $section =~ m{\#};
+ next if $section eq '';
+
+ push @pages, $section;
+ }
+ }
+
+ my $total = scalar @pages;
+ my @sections = get_sections_in_order( @pages );
+
logout()
if $config{''}{'loggedout'};
-
- foreach my $section ( sort by_name_and_state @pages ) {
- screengrab( $section );
+
+ foreach my $section ( @sections ) {
+ state $count = 0;
+ $count++;
+
+ screengrab( $section, $count, $total );
}
}
exit;
+
sub archive_files {
my $signature = $option{'archive'};
@@ -62,21 +103,119 @@ sub archive_files {
if -f $source;
}
}
+ write_latest_commit( $signature );
+}
+sub generate_indexes {
+ my $grabs = find_all_screengrabs();
+ my $latest = read_latest_commit();
+
+ generate_from_template(
+ 'index',
+ 'index.html',
+ {
+ commit => 'latest',
+ grabs => $grabs,
+ latest_commit => $latest,
+ title => $grabs_title,
+ }
+ );
+
+ foreach my $commit ( keys %{ $grabs->{'commit'} } ) {
+ next unless $commit;
+
+ mkpath $commit;
+ generate_from_template(
+ 'commit',
+ "${commit}/index.html",
+ {
+ commit => $commit,
+ grabs => $grabs,
+ latest_commit => $latest,
+ title => $grabs_title,
+ }
+ );
+ }
+
+ foreach my $section ( keys %{ $grabs->{'section'} } ) {
+ generate_from_template(
+ 'section',
+ "${section}/index.html",
+ {
+ grabs => $grabs,
+ latest_commit => $latest,
+ section => $section,
+ title => $grabs_title,
+ }
+ );
+ }
+}
+sub find_all_screengrabs {
+ my %grabs = (
+ 'section' => {},
+ 'commit' => {},
+ );
+
+ foreach my $section ( @pages ) {
+ $grabs{'section'}{$section} = {};
+
+ opendir my $dir, $section
+ or die "Cannot open directory $section: $!";
+
+ while ( my $file = readdir $dir ) {
+ if ( $file =~ m{^ (.+)-(?: full|clipped|thumb)\.png $}x ) {
+ $grabs{'section'}{$section}{$file} = 1;
+ $grabs{'commit'}{$1}{$section} = 1;
+ }
+ }
+ }
+
+ return \%grabs;
+}
+sub read_latest_commit {
+ my $commit < io('latest.txt');
+ chomp $commit;
+
+ return $commit;
+}
+sub write_latest_commit {
+ my $commit = shift;
+ $commit > io('latest.txt');
+}
+sub generate_from_template {
+ my $template = shift;
+ my $target = shift;
+ my $data = shift;
+
+ my( $output, $errors ) = $generator->render(
+ '/',
+ 'html',
+ { type => $template },
+ $data
+ );
+
+ say STDERR $errors
+ if $errors;
+
+ $output > io($target);
}
sub login {
my $login = shift;
my $section = "user#${login}";
+ print " -- logging in as $login\r";
+
my $username_field = $config{'form#login'}{'username'};
my $username = $config{$section}{'username'};
my $password_field = $config{'form#login'}{'password'};
my $password = $config{$section}{'password'};
my $submit_button = $config{'form#login'}{'submit'};
- my $javascript = <<END;
- \$("${username_field}").val("${username}");
- \$("${password_field}").val("${password}");
- \$("${submit_button}").trigger('click');
-END
+ my $checkbox_field = $config{'form#login'}{'checkbox'};
+
+ my $javascript = qq(jQuery("${username_field}").val("${username}");)
+ . qq(jQuery("${password_field}").val("${password}"););
+ $javascript .= qq(jQuery("${checkbox_field}").trigger('click');)
+ if defined $checkbox_field;
+ $javascript .= qq(jQuery("${submit_button}").trigger('click'););
discarded_screengrab(
get_full_url_for('form#login'),
@@ -88,9 +227,11 @@ END
sub logout {
my $logout_button = $config{'form#logout'}{'submit'};
my $javascript = <<END;
- \$("${logout_button}").trigger('click');
+ jQuery("${logout_button}").trigger('click');
END
-
+
+ print " -- logging out\r";
+
discarded_screengrab(
get_full_url_for('form#logout'),
$javascript,
@@ -100,7 +241,13 @@ END
}
sub screengrab {
my $section = shift;
+ my $count = shift;
+ my $total = shift;
+
+ my $url = get_full_url_for( $section );
+ return unless $url;
+ my @args = get_args_for( $section );
my $login = get_key($section, 'login') // '';
if ( $login ne $current_login ) {
if ( !$login ) {
@@ -111,10 +258,18 @@ sub screengrab {
}
}
- my $url = get_full_url_for( $section );
- my @args = get_args_for( $section );
+ state $places = length( sprintf "%s", $total );
+ printf
+ "[%0${places}d/%0${places}d] %-40s\n%s",
+ $count,
+ $total,
+ $section,
+ ( ' ' x ($places*2 + 4 ) );
system( 'webkit2png', @args, $url );
+
+ logout()
+ if get_key($section, 'logout');
}
sub discarded_screengrab {
my $url = shift;
@@ -122,6 +277,7 @@ sub discarded_screengrab {
system(
'webkit2png',
+ '-q',
'--js',
$javascript,
'--dir',
@@ -147,14 +303,16 @@ sub get_args_for {
my $section = shift;
my @args;
- my $width = get_key($section, 'width');
- my $clipwidth = get_key($section, 'clipwidth');
- my $scale = sprintf "%0.5f", $clipwidth / $width;
+ my $width = get_key($section, 'width');
+ my $clipwidth = get_key($section, 'clipwidth');
+ my $clipheight = get_key($section, 'clipheight');
+ my $scale = sprintf "%0.5f", $clipwidth / $width;
mkpath $section;
push @args,
'--width', $width,
'--clipwidth', $clipwidth,
+ '--clipheight', $clipheight,
'--scale', $scale,
'--dir', $section,
'--filename', 'latest',
@@ -173,6 +331,36 @@ sub get_args_for {
if get_key($section, 'noimages');
my $javascript = get_key($section, 'js');
+ my $click = get_key($section, 'click');
+ if ( !defined $javascript && defined $click ) {
+ $javascript = qq(jQuery('${click}').trigger('click'););
+ }
+
+ my $timeout = get_key($section, 'timeout');
+ if ( defined $javascript && defined $timeout ) {
+ $javascript = "setTimeout( function() { ${javascript}; }, $timeout );"
+ }
+
+ my $fill_name = get_key($section, 'fill_name');
+ if ( defined $fill_name ) {
+ my $name = generate_random_name();
+ $javascript .= qq(jQuery('${fill_name}').val("$name"););
+ }
+ my $fill_pass = get_key($section, 'fill_pass');
+ if ( defined $fill_name ) {
+ my $pass = generate_random_password();
+ $javascript .= qq(jQuery('${fill_pass}').val("$pass"););
+ }
+ my $fill_email = get_key($section, 'fill_email');
+ if ( defined $fill_email ) {
+ my $email = generate_random_email();
+ $javascript .= qq(jQuery('${fill_email}').val("$email"););
+ }
+ my $submit = get_key($section, 'submit');
+ if ( defined $submit ) {
+ $javascript .= qq(jQuery('${submit}').trigger('click'););
+ }
+
push( @args, '--js', $javascript )
if defined $javascript;
@@ -184,6 +372,9 @@ sub get_key {
return $config{$section}{$key} // $config{''}{$key};
}
+sub get_title {
+ return get_key('', 'title') // get_key('', 'base');
+}
sub get_configuration {
my $file = shift;
@@ -196,6 +387,33 @@ sub get_configuration {
return %config;
}
+sub get_sections_in_order {
+ my @pages = @_;
+
+ # sort into logged out and logged in, alphabetically
+ my %sections = map { $_ => 1 } @pages;
+ my @chained_sections;
+
+ # pull out anything chained
+ foreach my $page ( @pages ) {
+ my $chain_after = get_key($page, 'chain_after');
+
+ if ( defined $chain_after ) {
+ if ( defined $sections{$chain_after} ) {
+ delete $sections{$chain_after};
+ push @chained_sections, $chain_after;
+ }
+
+ delete $sections{$page};
+ push @chained_sections, $page;
+ }
+ }
+
+ return(
+ sort( by_name_and_state keys %sections ),
+ @chained_sections,
+ );
+}
sub by_name_and_state {
my $a_login = $config{$a}{'login'};
my $b_login = $config{$b}{'login'};
@@ -213,3 +431,65 @@ sub by_name_and_state {
return $a cmp $b;
}
+sub get_template_generator {
+ my $jigsaw = Template::Jigsaw->new();
+
+ $jigsaw->set_repository( '_templates' );
+ $jigsaw->scan_repository();
+ $jigsaw->set_dimensions({ _ => 'type' });
+
+ return $jigsaw;
+}
+sub generate_random_name {
+ return sprintf
+ 'John %s Doe',
+ ucfirst generate_random_string();
+}
+sub generate_random_password {
+ return generate_random_string();
+}
+sub generate_random_email {
+ return generate_random_string() . '@mailinator.com';
+}
+sub generate_random_string {
+ my $string = '';
+ state $first_letter = ord 'a';
+
+ foreach my $i ( 1 .. 12 ) {
+ my $letter = int rand 26;
+ $string .= chr( $letter + $first_letter );
+ }
+
+ return $string;
+}
+
+__END__
+
+=head1 NAME
+
+B<webkit2png-driver> - automating screengrabs
+
+=head1 SYNOPSIS
+
+B<webkit2png-driver> [-c config] [ [<section>|<regexp>] ... ]
+
+B<webkit2png-driver> [-c config] -i
+
+B<webkit2png-driver> [-c config] -a <archive-name>
+
+=head1 MORE INFORMATION
+
+Original documentation can be found in the F<README.markdown> file
+provided in the git repo
+L<https://github.com/norm/webkit2png-driver/blob/master/README.markdown>.
+
+=head1 AUTHOR
+
+Mark Norman Francis, L<norm@cackhanded.net>.
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright 2011 Mark Norman Francis.
+
+This library is free software; you can redistribute it and/or modify it
+under the same terms as Perl itself.

No commit comments for this range

Something went wrong with that request. Please try again.