blob: b1edfe8979f1bf11f3765ba525defc8d5284947c [file] [log] [blame]
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
Screentest compares images of rendered web pages.
It compares images obtained from two sources, one to test and one for the expected
result. The comparisons are driven by a script file in a format described below.
# Usage
screentest [flags] testURL wantURL file ...
The first two arguments are the URLs being tested and the URL of the desired result,
respectively. The remaining arguments are script file paths.
The URLs can be actual URLs for http:, https:, file: or gs: schemes, or they
can be slash-separated file paths (even on Windows).
The flags are:
-c
Number of test cases to run concurrently.
-d
URL of a Chrome websocket debugger. If omitted, screentest uses the
Chrome executable on the command path. It will look first for the
headless-shell binary, which is preferred.
-headers
HTTP(S) headers to send with each request, as a comma-separated list of name:value.
-retrypixels N
If the difference is no more than N pixels, take another screenshot. Repeat up to 3
times. N should be small. This is a last-resort method for handling small, inexplicable
output variations.
-run REGEXP
Run only tests matching regexp.
-o
URL or slash-separated path where output files for failing tests are written.
If omitted, files are written to a subdirectory of the user's cache directory.
At the start of each run, existing files are removed.
Each test file is given its own directory, so test names in two files can be identical,
but the directory name is the basename of the test file with the extension removed, so
files with identical basenames will overwrite each other.
-u
Instead of comparing screenshots, use the test screenshots to update the
want screenshots. This only makes sense if wantURL is a storage location
like a file path or GCS bucket.
-v
Variables provided to script templates as comma-separated KEY:VALUE pairs.
# Headless Chrome
Screentest needs a headless Chrome process to render web pages. Although it can use a full
Chrome browser, we have found the headless-shell build of Chrome to be more reliable.
Install headless-shell on your local machine with this command:
npx @puppeteer/browsers install chrome-headless-shell@VERSION
Put the binary on your path and screentest will find it. Omit the -d flag in this case.
You can also run headless-shell in docker. We use this command:
docker run --detach --rm --network host --shm-size 8G --name headless-shell chromedp/headless-shell:VERSION
Then pass "-d ws://localhost:9222" to screentest.
# Scripts
A script file contains one or more test cases described as a sequence of lines. The
file is first processed as Go template using the text/template package, with a map
of the variables given by the -v flag provided as `.`.
The script format is line-oriented.
Lines beginning with # characters are ignored as comments.
Each non-blank, non-comment line is a directive, listed below.
Each test case begins with the 'test' directive and ends with a blank line.
A test case describes actions to take on a page, along
with the dimensions of the screenshots to be compared. For example, here is
a trivial script:
test about
path /about
capture fullscreen
This script has a single test case. The first line names the test.
The second line sets the page to visit at each origin. The last line
captures full-page screenshots of the pages and generates a diff image if they
do not match.
# Directives
Use windowsize WIDTHxHEIGHT to set the default window size for all test cases
that follow.
windowsize 540x1080
Use block URL ... to set URL patterns to block. Wildcards ('*') are allowed.
block https://codecov.io/* https://travis-ci.com/*
The directives above apply to all test cases that follow.
The ones below must appear inside a test case and apply only to that case.
Use test NAME to create a name for the test case.
test about page
Use path PATH to set the page to visit at each origin.
path /about
Use status CODE to set an expected HTTP status code. The default is 200.
status 404
Use click SELECTOR to add a click an element on the page.
click button.submit
Use wait SELECTOR to wait for an element to appear.
wait [role="treeitem"][aria-expanded="true"]
Use eval JS to evaluate JavaScript snippets to hide elements or prepare the page in
some other way.
eval 'document.querySelector(".selector").remove();'
eval 'window.scrollTo({top: 0});'
Use sleep DURATION to pause the browser for the duration. This is a last resort
for deflaking; prefer to wait for an element.
sleep 50ms
Use capture [SIZE] [ARG] to create a test case with the properties
defined in the test case. If present, the first argument to capture must be one of
'fullscreen', 'viewport' or 'element'. The optional second argument provides
a viewport size. The defaults are 'viewport' with dimensions specified by the windowsize
directive.
capture fullscreen 540x1080
When taking an element screenshot provide a selector.
capture element header
Each capture directive creates a new test case for a single page.
windowsize 1536x960
test homepage
path /
capture viewport
capture viewport 540x1080
capture viewport 400x1000
test about page
path /about
capture viewport
capture viewport 540x1080
capture viewport 400x1000
*/
package main
import (
"context"
"flag"
"fmt"
"log"
"os"
)
var flags options
func init() {
flag.BoolVar(&flags.update, "update", false, "update want with test")
flag.StringVar(&flags.vars, "v", "", "variables provided to script templates as comma separated KEY:VALUE pairs")
flag.IntVar(&flags.maxConcurrency, "c", 1, "number of test cases to run concurrently")
flag.StringVar(&flags.debuggerURL, "d", "", "chrome debugger URL")
flag.StringVar(&flags.outputDirURL, "o", "", "path for output: file path or URL with 'file' or 'gs' scheme")
flag.StringVar(&flags.headers, "headers", "", "HTTP headers: comma-separated list of name:value")
flag.StringVar(&flags.filterRegexp, "run", "", "regexp to match test")
flag.IntVar(&flags.retryPixels, "retrypixels", 0, "repeat up to 3 times if diff is <= this value")
}
// options are the options for the program.
// See the top of this file and the flag.XXXVar calls above for documentation.
type options struct {
update bool
vars string
maxConcurrency int
debuggerURL string
filterRegexp string
outputDirURL string
headers string
retryPixels int
}
func main() {
log.SetFlags(0)
log.SetPrefix("screentest: ")
flag.Usage = func() {
fmt.Printf("usage: screentest [flags] testURL wantURL path ...\n")
fmt.Printf("\ttestURL is the URL or file path to be tested\n")
fmt.Printf("\twantURL is the URL or file path to compare it to\n")
fmt.Printf("\teach path is a script file to execute\n")
flag.PrintDefaults()
}
flag.Parse()
if flag.NArg() < 3 {
flag.Usage()
os.Exit(2)
}
if err := run(context.Background(), flag.Arg(0), flag.Arg(1), flag.Args()[2:], flags); err != nil {
log.Fatal(err)
}
}