Testpages is a website and automated test suite for verifying that ad-blocking extensions (primarily Adblock Plus) correctly apply filters. The site is served at abptestpages.org and each page demonstrates a specific filter type or snippet. Automated tests load these pages in a real browser with ABP installed, apply the relevant filters, and verify the result by comparing screenshots.
| Directory | Purpose |
|---|---|
pages/ |
Page content written as Jinja2 templates (.tmpl files). Subdirectories map to URL paths: pages/filters/blocking.tmpl → /en/filters/blocking. |
templates/ |
Page layout templates referenced by pages via template = <name>. Most test pages use testcase, which wraps content in the standard HTML shell. |
includes/ |
Partial HTML fragments (header, footer, head) included by templates via <? include name ?>. |
globals/ |
Python functions available inside .tmpl files as Jinja2 globals: get_id(), heading(), get_filters(), get_date(), site_url, strip_proto, domain. |
filters/ |
Python filters available in Jinja2 templates (e.g. strip_proto, domain). Separate from ad-blocking filters. |
static/ |
Static assets served as-is: static/lib/ (shared JS utilities), static/css/, static/images/, static/testfiles/ (per-test JS and HTML used by test pages). |
endpoints/ |
Small Node.js servers that handle requests requiring dynamic server-side logic (sitekey signing, WebSocket echo, etc.). Started alongside nginx during test runs. |
test/ |
All test infrastructure: Selenium test runner, Docker/nginx config, CI entrypoint script. |
test/extension-tests/ |
The Mocha test suite that drives a real browser via Selenium WebDriver. |
test/extension-tests/helper-extension/ |
Source for the MV2/MV3 helper extension used as a second loaded extension during tests. Built before the test run. |
testext/ |
The unpacked ABP extension used during test runs. Populated in one of two ways: (1) at runtime by node ./test/extension-tests/extension-download.js (default, downloads the latest ABP release), or (2) at Docker build time when the EXTENSION_FILE build arg is provided (docker build --build-arg EXTENSION_FILE=...). Not committed — populated at runtime. |
| File | Purpose |
|---|---|
pages/abp-testcase-subscription.txt.tmpl |
Dynamically generated filter subscription file. get_filters() walks all pages and extracts filters from .testcase-filters pre and .site-panel pre elements. |
| Directory | When created | Purpose |
|---|---|---|
/var/www/local.testpages.adblockplus.org/ |
Docker build | Static HTML output from CMS — what nginx actually serves. |
test/screenshots/ |
Test run (on failure) | Screenshots taken when a test fails, copied out via docker cp. |
helper-extension-build/ |
Before test run | Built output of the helper extension. |
node_modules/ |
npm install |
JS dependencies. |
Testpages uses eyeo's CMS to turn
.tmpl source files into static HTML.
-
Each
.tmplfile inpages/starts with a metadata block (key-value pairs before any HTML), followed by Jinja2 template content:title = My Test Page template = testcase description = Tests that X works. {% set case = "Basic usage" %} ... -
The CMS reads the metadata, selects the layout from
templates/(e.g.testcase.tmpl), injects the page body as{{ body }}, renders the full Jinja2 template with globals fromglobals/and filters fromfilters/, and outputs static HTML. -
During the Docker build,
cms.bin.generate_static_pageswrites all rendered HTML files to/var/www/$DOMAIN/. nginx then serves these files for the duration of the test run. -
The subscription file (
abp-testcase-subscription.txt) is also a CMS page. Its body calls{{ get_filters() }}, which walks every page's rendered HTML and extracts filter text from.testcase-filters preand.site-panel preelements. This means filters must be in static HTML — filters injected by JavaScript will not appear in the subscription file.
| Global | What it does |
|---|---|
get_id(title) |
Converts a title string to a kebab-case HTML id, e.g. "Basic usage" → "basic-usage". Used to give each test case a stable, unique id. |
heading(title) |
Returns an <h2> element with the id and aria-label attributes set using get_id(). |
site_url |
The full base URL of the site (e.g. https://local.testpages.adblockplus.org:5001). Set via the SITE_URL environment variable. |
site_url|domain |
The domain only (e.g. local.testpages.adblockplus.org). Used in filter rules. |
site_url|strip_proto |
URL without the protocol prefix. Used in filter rules that require ` |
get_filters() |
Used only by the subscription file template to aggregate all filters from all pages. |
A typical test page section looks like this:
{% set case = "Basic usage" %}
{% set id = get_id(case) %}
<section id="{{id}}-section" class="testcase-panel">
{{ heading(case) }}
<p>Description of what is being tested.</p>
<div id="{{id}}-area" class="testcase-area">
<div class="testcase-examplecontent" aria-label="{{id}}-example">Example Content</div>
<div class="testcase-waiting-content" aria-label="{{id}}-waiting">Waiting...</div>
</div>
<h3>Filters</h3>
<ul id="{{id}}-filters" class="testcase-filters">
<li><pre>{{ site_url|domain }}##.ad</pre></li>
</ul>
</section>The automated test runner compares a screenshot of the page with filters
applied against a screenshot of the page in its expected state (loaded with
?expected=1). Pages use a consistent visual convention:
- Elements with
data-expectedresult="fail"are styled red — they represent content that should be blocked or hidden. If they remain visible after filters are applied, the test fails. - Elements with
data-expectedresult="pass"are styled green — they represent content that should remain visible. .testcase-examplecontentalways shows the green "Example Content" placeholder..testcase-waiting-contentshows a waiting message while a delayed script runs.
For snippet tests, the JS testfile appends a red fail element after a short delay. If the snippet is working correctly, the append is blocked and no fail element appears.
JS and HTML files used by test pages live in static/testfiles/<page-name>/.
They are served directly by nginx. The removeWaitingContent(targetId) function
from static/lib/utils.js is available globally on all test pages and removes
.testcase-waiting-content elements once a test script has run.
- Create
pages/<category>/<name>.tmpl. Usetemplate = testcasefor standard test pages. - Add one or more test case sections following the anatomy above.
- If the page needs JS, create
static/testfiles/<name>/and put scripts there. Reference them with<script src="/testfiles/<name>/script.js"></script>. - Add a link to
pages/index.tmplin the appropriate section, in alphabetical order. - If the test should not run in certain browsers or manifest versions, add an
exclusion to
test/extension-tests/pages/utils.jsisExcluded().
There are two levels of exclusion:
Exclude from automated test runs only (page still renders and is accessible
on the website): add a condition to isExcluded() in
test/extension-tests/pages/utils.js. Examples of existing exclusions:
// Exclude from all browsers:
else if (/^filters\/inline-css/.test(page))
return true;
// Exclude from a specific browser:
else if (page == "exceptions/iframe")
excluded = ["firefox"];
// Exclude based on manifest version:
else if (manifestVersion == 3 && page == "filters/header")
return true;Force-include a page that is excluded (useful for local debugging):
TESTS_TO_INCLUDE=filters/my-page npm test -- -g "chrome latest"Exclude from the website entirely: remove or rename the .tmpl file and
remove its entry from pages/index.tmpl.
-
docker build -t testpages .— builds the Docker image. During this step:- nginx is installed and configured with a self-signed TLS certificate for
local.testpages.adblockplus.org. - The CMS is cloned at a pinned commit and its dependencies installed.
- npm dependencies are installed.
- The CMS renders all
.tmplpages to static HTML in/var/www/$DOMAIN/. - If
EXTENSION_FILEis provided, the extension is unpacked intotestext/.
- nginx is installed and configured with a self-signed TLS certificate for
-
docker run --shm-size=2g -it testpages— runs the container. Thetest/entrypoint.shscript:- Starts nginx (testpages server) and the endpoints server.
- Sets
MANIFEST_VERSIONbased onGREP(chromeonly, excludingchromium→ MV3; everything else includingchromium,edge,firefox→ MV2). - Downloads the ABP extension unless
SKIP_EXTENSION_DOWNLOAD=true. - Runs
npm testwith theGREPfilter.
On merge to master, the CI pipeline:
- Runs the
build_productionstage: callscms.bin.generate_static_pageswithSITE_URL=https://abptestpages.orgto generate static HTML. - Runs the
deploy_productionstage: copies the build output to the production server via a deploy script. - Runs the
notifystage: if any.tmplfiles changed, triggers a pipeline in the test automation project to re-run tests against the live site.
Each browser has its own parallel CI job (e.g. test:chrome:latest,
test:firefox:latest). All jobs use the same Docker image but pass a different
GREP value to select the browser.
Jobs that only run on scheduled pipelines (not on push): chrome:beta,
chrome:dev, firefox:beta, edge:beta.
test:edge:windows is a special job that runs on a dedicated Windows runner
(eyeo-windows) without Docker, setting up nginx, Python, and Node.js directly
via Chocolatey.
test/extension-tests/entrypoint.js is the Mocha test root. It:
- Fetches the index page of testpages and parses all test case links into a
pageTestsarray ([[section, [[url, title], ...]], ...]). - For each browser/version combination, creates a Selenium WebDriver, loads the
ABP extension, and waits for it to report
startedviatesting.getReadyState. - Reads
manifest.jsonfrom the extension directly to determine MV2 vs MV3. - Runs the test suite defined in
test/extension-tests/pages/index.js, which iteratespageTests, applies filters from each page's<pre>elements, and compares screenshots.
For each test page, the runner:
- Navigates to
<url>?expected=1and takes a screenshot (the expected state). - Navigates to
<url>, imports the page's filters into ABP, refreshes, and takes a screenshot. - Compares the two screenshots using
Jimp. If they differ beyond a threshold, the test fails and the screenshots are saved totest/screenshots/.
For specialized tests (defined in test/extension-tests/pages/specialized.js), screenshot
comparison is replaced with custom DOM assertions.
Get screenshots from a failed Docker run:
docker cp $(docker ps -aqf ancestor=testpages | head -n 1):/testpages.adblockplus.org/test/screenshots ./screenshotsGet nginx logs:
docker cp $(docker ps -aqf ancestor=testpages | head -n 1):/var/log/nginx/ ./nginx-logsRun a single test locally:
TEST_PAGES_URL=http://localhost:5001/en/ MANIFEST_VERSION=2 npm test -- -g "chrome latest.*Blocking"Force-include an excluded test:
TESTS_TO_INCLUDE=filters/my-page MANIFEST_VERSION=2 npm test -- -g "chrome latest"Throw on extension errors (turns debug.getLastError warnings into failures):
docker run --shm-size=2g -e THROW_LAST_ERROR="true" -it testpagesView the rendered page locally: run the CMS dev server (see README) and open
the page in a browser. Append ?expected=1 to see what the test runner expects
to see after filters are applied.
- CMS dependency: the site requires a pinned version of the eyeo CMS, cloned at build time. Changes to CMS behaviour can silently break page rendering.
get_filters()reads static HTML only: filters defined using Jinja2 page-local variables (e.g.{{id}}) may not resolve correctly in the subscription file, becauseget_filters()processes pages with the global context, not the page-local one. Filters should always use hardcoded strings or global variables (site_url).- Screenshot comparison is pixel-sensitive: font rendering differences
between browser versions or platforms can cause false failures. The
browser-snapshots/directory holds reference screenshots per browser. - No parallelism within a browser: Selenium WebDriver requires sequential script execution on a single driver instance. Tests within a browser run serially.
- Self-signed certificate: the test server uses a self-signed TLS
certificate. Node.js
fetchcalls from outside the browser context will reject it — usedriver.executeAsyncScriptwithfetchinside the browser instead. - Subscription test requires HTTPS: the
subscribes to a linktest will fail on a local CMSrunserver.pysetup because it serves over HTTP, not HTTPS. - Windows CI is manual setup: the
test:edge:windowsjob does not use Docker. It installs dependencies via Chocolatey on each run, making it slower and more fragile than the Linux jobs.