Filter
In principle, recheck creates a difference for every change that occurred and writes it into the report. Then it is up to the UI (review or recheck.cli) to hide ignored differences. This is pretty much in line with how Git works—the diff is there, it just doesn't show up when ignored.
The advantages of this approach are as follows:
- It is inspection / revision safe – every change is documented.
- Hidden changes (technical changes that are usually invisible to the user) are treated as any other change and are presented as such.
- You can change the rules of what to ignore and instantly see the impact.
What Are Filters?
Filters can be used for different purposes, most notably to reduce the noise mentioned above within a report.
They can accept different arguments, which all are accessible using the below syntax:
- Element: Filter applies to all attributes of an element and all of its child elements. This may look at specific attributes, children or parent element to determine if the filter applies. The element in question can be identified by a wide range of identifying attributes, which are specified by the extensions.
- Attribute: Filter applies to a single attribute, either globally or for a specific element and all of its child elements.
- Difference: Filter applies to a difference where the attribute and the value can be retrieved.
Multiple filters are additive. If one filter returns true
, the result is taken and not further evaluated. Similarly, all filters must return false
to ignore a particular difference.
Note
The filter as such does not define what happens with the output. It just matches a element or a difference. Please note the context in which you use a filter and what the output is used for.
Location
By default, filters are simple text files, so that they are reusable within different products and stages. The name of the file corresponds with the appropriate name used within recheck. There are several locations where the filter files can be placed, so that they may be referenced within the RecheckOptions
.
Plain Filters
- Project filters in
${PROJECT_ROOT}/.retest/filter
. - User directory in
${USER_HOME}/.retest/filter
. Must be created manually. - Provided filters from recheck. We ship some categorized filters.
Note
We are currently experimenting with sensible defaults and may change the provided filters without notice. If you feel that they filter too much or too less within their respective category, please let us know, so that we can change these.
Tip
You may overwrite filters by using the same name. They are searched top-down. That is, project filters overwrite user filters, which in turn overwrite the provided filters. This allows you to easily overwrite the provided filters so that you can add your customizations.
Ignore Filters
An ignore filter recheck.ignore
is a special kind of filter which (by default) is loaded automatically. It defines that true
ignores (i.e. hides) the specified difference, while false
continues to use it. In contrast to plain filters, multiple ignore filters do not overwrite each other, despite having the same name. Rather, they are additive and each location contributes to the final ignore filter used by recheck. The following locations are searched:
- Globally:
${USER_HOME}/.retest/
. Must be created manually. - For each Project:
${PROJECT_ROOT}/.retest/
. Will be created on first execution. - For each Golden Master individually (i.e. suite). Must be created manually.
After using recheck.cli or review to update the ignore filter, only the ignore file for the project will be updated to additionally contain the new ignored differences and all ignores from the global and suite ignore file.
Using the default setup, that would be:
${USER_HOME}
+-- .retest/
+-- recheck.ignore
${PROJECT_ROOT}
+-- .retest/
| +-- recheck.ignore
+-- src/test/resources/retest/recheck/
+-- ${SUITE_NAME}
+-- recheck.ignore
Usage
Filters must be specified by name and given to the RecheckOptions
as described here. That is, to make them usable within different products such that no information is lost. So, if you configure a filter within RecheckOptions
(see below), you may want to use the same filter within review too, such that differences during test and review remain the same.
RecheckOptions
RecheckOptions.builder()
.addIgnore( "my-custom-filter.filter" )
.build();
# my-custom-filter.filter
# Define your rules here:
matcher: id=foo
If you do not specify an ignore like above, Recheck
will load the default ignore files.
Ignore
recheck.ignore
This is a filter file which supports rules with the syntax below. They are normal text files and can be opened by any text editor.
This file will be updated through the UI (review or recheck.cli) if you ignore any elements or differences. We try our best keep your custom-defined rules and respect the existing formatting.
recheck.ignore.js
This is a filter file which supports dynamic rules in JavaScript, using Mozilla's Rhino engine. This allows users to specify ignore rules very flexibly by using the following method:
matches( element, diff );
Method description
Arguments:
element
(Element
): The element on which the difference occurred.diff
(AttributeDifference
): The difference, may be null.
Returns:
bool
: Whether the difference should be ignored.
Example:
An implementation can be found at recheck-web.
Syntax
You may define filters in a file with the .filter
extension that is located in one of the two locations above. Since filters are additive, evaluated top to bottom, each line represents a separate filter. Thus, a file may represent a group of filters that can be combined to one topic (e.g. color differences).
Warning
If a filter (i.e. line) does not represent a valid syntax or comment, an error will be logged and the line is ignored.
Comments
# This is a comment. It starts with a '#' and encompasses the full line
foo # Comment lines must start with a '#' and do not have leading whitespaces
# ^ You may define empty lines
Expressions
Each line represents a filter, which itself can be made up by one or more expressions. Those expressions are separated with a comma and a whitespace ,
.
When writing expressions, it is important to understand that filters are additive. Take the example below where each filter refers to a different element. Please refer to the individual expressions below to lookup their usage in detail.
# Matches all elements and their attributes within a form element
matcher: type=form
# Matches the text attribute for all elements within body
matcher: type=body, attribute=text
If a filter refers to the same element, care must be taken, so that each filter is evaluated.
# Matches all elements and their attributes within a body element
matcher: type=body
# Matches the text attribute for all elements within body
# Will not be evaluated, because the above filter already matches
matcher: type=body, attribute=text
Since filters are evaluated top to bottom, we could switch both lines around. But since the body will be matched completely, regardless of the order, the more specific attribute is unnecessary.
Thus it should be taken care, that each filter line references a unique scope, so that each line is evaluated and contributes to the filter as a whole.
Scope
Each expression applies to a specific scope. By default, a global scope is assumed (i.e. the report
or state
) to which the resulting filter is applied. By chaining multiple expression within a single line, the following expression acts only upon the scope of the previous expression, while the last expression ultimately decides which final scope the filter applies.
A filter executes its expressions lazily (left to right) and aborts as soon as the scope is empty. Consequently, it will only match, if the last scope analyzed is not empty. Thus, an expression is only evaluated, if the applying scope is not empty.
State
Consider the element structure as represented by the state: A collection of elements, where each element contains attributes which are assigned to values. For that the following scopes can be defined:
- State (i.e. collection of elements): This is the global scope.
- Element: Select an element, e.g. type
button
. - Attribute: Select an attribute, e.g.
background-color
. - Value: Select the value, e.g.
rgb(125, 125, 125)
.
In words: The above example selects all button
elements with the attribute background-color=rgb(125, 125, 125)
.
Report
Although the structure of a report is quite different, ultimately, the scope is fairly similar to the scope of a state.
- Report (i.e. collection of element differences): This is the global scope.
- Element: Select an element, e.g. type
button
. The element difference is not exposed directly. - Attribute Difference: Select an attribute, e.g.
background-color
, - Value: Depending on the filter, this will select either the expected or actual value, e.g.
rgb(125, 125, 125)
.
In words: The above example selects all button
elements with the attribute difference background-color: actual=rgb(125, 125, 125)
.
Tip
When chaining multiple expressions, scopes may be omitted. For example, omitting the element scope for the above example would result in a selection of all elements with the respective background color.
Examples
We currently support these individual expressions. Please refer to the more in detail descriptions below:
# Import filters
$import
# Match any element
$element
# Match any attribute
$attribute
# Match any value
$value
# Match inserted changes (for elements)
$inserted
# Match deleted changes (for elements)
$deleted
# Match some pixel fluctuations (for attributes)
$pixel-diff
# Match some pixel fluctuations (for attributes)
$color-diff
You may chain them in the following way for elements:
# Match an attribute for a specific element
$element, $attribute
# Match a specific value of an elements' attribute
$element, $attribute, $value
# Match some pixel fluctuations for an elements' attribute
$element, $attribute, $pixel-diff
# Match some color fluctuations for an elements' attribute
$element, $attribute, $color-diff
# Match a specific value for all attributes of an element
$element, $value
# Match some pixel fluctuations for all attributes of an element
$element, $pixel-diff
# Match the element only if it is inserted
$element, $inserted
# Match the element only if it is removed
$element, $deleted
# Exclude child elements or specific attributes
$element, $exclude
You may chain them in the following way for attributes:
# Match a specific value of an attribute for all elements
$attribute, $value
# Match some pixel fluctuation of an attribute for all elements
$attribute, $pixel-diff
# Match some color fluctuation of an attribute for all elements
$attribute, $color-diff
Tip
By combining element, attribute and value matching, you are able to match certain differences very specifically.
Matching Elements
Elements are identified by one special attribute $key
, so called identifying attributes.
# Match the element and its children where its attribute $key fully matches the $value.
matcher: $key=$value
$key |
$value (Example) |
Description |
---|---|---|
retestid |
div-b4f23 |
A unique, stable ID for an element, that is generated by recheck. This is the default mechanism. |
xpath |
html[1]/body[1]/div[1] |
Note that this does only supports absolute XPaths. |
id |
myId |
HTML id attribute (supplied by recheck-web) |
class |
my-class or my-class other-class |
HTML class attribute (supplied by recheck-web) |
type |
button |
HTML tag name (supplied by recheck-web)1 |
Matching inserted or deleted elements
If you are not interested in inserted or deleted elements, but still want to get notified if the attributes of the specified elements change, you can ignore those changes. This is useful for lists similar where you do not care if an element is inserted, but do care if the font or color changes.
# Globally ignore insertions or deletions
change=inserted
change=deleted
# Ignore only insertions deletions within the element
matcher: $key=$value, change=inserted
matcher: $key=$value, change=deleted
Matching Attributes
You can filter specific attributes that occur in differences by their respective name.
Note
An extension specifies which attributes (as well as identifying attributes) it creates for an element. Their name is displayed in the corresponding difference. Please refer to extensions' documentation for that information.
By Attribute
If you are searching for a specific attribute, you may define the attribute
expression. The value specified must match fully in order to let the filter return true
.
# Match the attribute outline only on the elements of type input
matcher: type=input, attribute=outline
# Match the attribute outline on all elements, thus removing this difference completely
attribute=outline
By Regular Expression
Similarly, you can also use Java's regex mechanism and ignore attributes by a given pattern.
# Match the attribute border-.*-color (e.g. border-bottom-color) on the elements of type input
matcher: type=input, attribute-regex=border-.*-color
# Match the attribute border-.*-color (e.g. border-bottom-color) on all elements
attribute-regex=border-.*-color
Matching Values
If you want to ignore a specific value while still being notified about any other changes, you can specify a value-regex
. This is helpful if you want to ensure that the value has a specific patter (e.g. date format) but do not care about the concrete value (because it is changing each day).
# Match all attributes that look like a date format (dd.MM.yyyy)
value-regex=\d\d.\d\d.\d\d\d\d
Matching Pixel Differences
Minor visual differences (e.g. between different browser types or browser versions) can make traditional, pixel-based approaches fail, which means more manual maintenance effort. In recheck, one can easily ignore pixel differences that are unimportant from a user's point of view:
pixel-diff=5px
pixel-diff=5.5px
This would ignore every pixel difference (position- or size change) up to 5 pixels. You can either specify an integer or a float.
Matching Color Differences
Similarly to the pixel differences, it is possible to ignore small color differences. This is most useful to ignore small differences for color animations.
color-diff=5%
Each color component (red, green, blue) is reviews in isolation. Changing only the red component from 255
to 127
would result in a color difference of 50%.
Importing Filters
To avoid having huge filter or ignore files and to avoid redundancy, you can create smaller, specialized filters which you can import.
The import is specified by the filter name (i.e. the file name), which will look at the global scope of all filters to find the most specific one. For example using the below example, will search all available locations for the file name and choose the most specific filter to load. This way, the importing filter will always pick up any changes made to the imported filter.
# Import other filters based on the name
import: content.filter
Warning
Be careful not to use cyclic imports where a.filter
imports b.filter
and vice versa (same goes for longer cyclic chains).
Secondly, only import filters from the same or a broader scope (e.g. filters within the user home should only import filters present in the user home or provided locations). If need be, you can always overwrite these filters within the project directory and the importing filter will use the project filter if available.
Excluding Filters
Excluding filters can be used to negate an expression, so that an expression returns the inverse, i.e. true
instead of false
and vice versa. If used inside a recheck.ignore
, exclusions allows to revert an ignore (e.g. for sub elements).
These expressions must be attached to a scope and can be used to revert this scope using an expression. The exact application is limited by which scope the exclusion is bound to. For example, attaching an exclusion to an element allows to exclude both sub-elements and individual attributes.
Tip
Excluding filters can only be applied to elements. We would love to hear possible use cases for other rules by creating an issue.
Exclusion Syntax
Excluding a filter takes a child expression. The child expression can accept the same expression as the scope to where the exclusion is attached. For example, attaching an exclusion to a matcher
allows to also specify a matcher
as a child expression.
exclude($child)
Here are some examples:
# Match all div elements (including all attributes), except of the element .card
matcher: type=div, exclude(matcher: class=card)
# Match all p elements (including all attributes), except of the attribute p
matcher: type=p, exclude(attribute=text)
Chaining exclusions
It is important to understand that filters are additive, where each line must represent a complete filter. The same applies for excluding filters. If a exclusion binds to the same scope (e.g. a form
element), they must be chained together.
# Match all elements within a form, except buttons
matcher: type=form, exclude(type=button)
# Match all elements within a form, except input elements
# Will not be evaluated, because the above filter already matches
matcher: type=form, exclude(type=input)
You can chain multiple exclusions together the same way you can chain standard expressions. Contrary to expression chaining, exclusions will always only bind to the first non-exclusion scope.
# All exclusions will bind to the matcher scope (i.e. all div elements)
matcher: type=div, exclude(attribute=text), exclude(attribute=color)
# Match all elements within a form, except buttons and inputs
matcher: type=form, exclude(type=button), exclude(type=input)
Warning
Once an exclusion has been defined, only further exclusions may be defined. Currently, an exclusion cannot be mixed with other expressions.
Importing exclusions
Because exclusion expressions can become quite complex, you can also use the import statement to reference another filter. This filter is the same to each line wrapped into an exclusion.
# exclude.filter
matcher: type=input, attribute=text
matcher: type=button
# recheck.ignore
matcher: type=form, exclude(import: exclude.filter)
# Will be resolved to the following:
matcher: type=form, exclude(matcher: type=input, attribute=text), exclude(matcher: type=button)
Warning
Care must be taken to ensure that the imported filter only specifies expressions that are valid as a child expression (i.e. within the same scope). Otherwise the expression is considered invalid and will never match.
Complex exclusion example
Excluding filters can both be chained and nested, if the scope allows for it. Take a look at the example below.
You can use exclusion expression chaining:
matcher: id=body, exclude(attribute=text), exclude(matcher: id=btn-subscribe), exclude(matcher: id=div, exclude(matcher: id=form))
Or use imports from your filter of choice:
# exclude.filter
attribute=text
matcher: id=btn-subscribe
matcher: id=div, exclude(matcher: id=form)
# global.filter
matcher: id=body, exclude(import: exclude.filter)
Elements to be matched (green) or ignored (red). Note that when using this example inside a recheck.ignore
the respective element is inverted.
+<body>
- <div id="div">
+ <form id="form">
+ <label for="email">
- Email
+ </label>
+ <input id="email" type="email">
+ </form>
- </div>
+ <div>
- <button id="btn-subscribe">
- Subscribe
- </button>
+ </div>
+</body>
-
While the HTML tag name is mapped to
type
and part of the identifying attributes, the actual HTMLtype
is put into the ordinary attributes that define an element's state. ↩