A worked example of end-to-end browser testing with TestCafe: page objects that stay readable as suites grow, React selectors that query component state instead of guessing at markup, and accessibility checks that run alongside functional ones. The architecture here will help you build reliable, maintainable tests over time.
Needs Node 22 or newer and a local Chrome. TestCafe drives browsers you have
installed rather than bundling its own. If you would rather not install Chrome,
./build.sh runs everything in a container instead.
npm ci
npm run test:localThat suite runs entirely against a React app in this repo. It needs no network
and cannot be broken by someone else's redesign, so it is the one to start
with. npm test adds the third-party suite on top.
The TestCafe examples here began as a tutorial project inside Instructure for teams new to TestCafe, or new to maintainable test architecture, and was hosted at test_advisory_board, an Apache-2.0 repository at Instructure. I wrote that tutorial code in May 2019; the repository's other examples (Pact for Ruby and Java) were written by colleagues and are not included here. The original commits are preserved in this repository's history. (See NOTICE.)
The 2019 commits are authored as mswailes-inst, my Instructure work account.
Everything since is a 2026 modernization: TestCafe 1.x to 3.x, a rewritten test target, GitHub Actions in place of the old internal CI, and the cleanups described below.
| Suite | Target | Demonstrates |
|---|---|---|
reactSelectors/ |
fixtures/app - a minimal React app in this repo |
React selectors, prop-based state queries, and an accessibility scan of the open menu state |
theInternet/ |
the-internet, a third-party fixture site | Dynamic content, auto-waiting assertions, accessibility with a documented baseline |
lib/config.js environment resolution shared by both suites
lib/fixturePort.mjs the fixture app's port, shared by the server and the suite
fixtures/app/ the React app the local suite runs against
fixtures/serve.mjs dependency-free static server, started by TestCafe
*/page/ page objects: selectors and behavior
*/tests/ the tests themselves
npm test # both suites, headless Chrome
npm run test:local # all owned tests, no network needed
npm run test:react # local browser suite only
npm run test:unit # fast configuration and fixture-server checks
npm run test:the-internet # third-party site only
npm run test:live # React suite in live mode, visible browser
npm run lintAny browser TestCafe supports works:
npx testcafe firefox theInternet/tests/*.js. Invoking the React suite
directly also needs --app 'node fixtures/serve.mjs', or it has no fixture app
to talk to. npm run test:react and ./build.sh react already pass it.
To point a suite somewhere else:
# Any URL, overriding everything below.
TEST_BASE_URL=https://staging.example.com npm run test:the-internet
# The fixture app's port, if something on your machine already wants 7357.
FIXTURE_PORT=7358 npm run test:reactTEST_ENV selects one of the named environments a suite declares in its
config.js. Only demo ships, so adding a staging target is a one-line change
local to that suite, and an unknown name fails with the list of valid ones
rather than being silently treated as a URL.
./build.sh runs the suites in a Linux container with browsers already
installed. That gets you closer to CI than a run on your own machine, and it is
the way to run without installing Chrome. On macOS, it also allows you to run
without granting TestCafe's browser tools screen-recording permission.
./build.sh # both suites
./build.sh react # local fixture app only
./build.sh the-internet # third-party site only
BROWSER=firefox ./build.sh # any browser the image providesnpm run test:liveKeeps a browser open and re-runs the React suite as you work. TestCafe serves
the fixture app itself via --app, so there is nothing to start first.
One thing to know: TestCafe watches the files your tests require : the tests,
the page objects, and anything under lib/. It cannot watch fixtures/app/,
because the browser loads that over HTTP and Node never requires it. So:
-
Editing a test, a page object, or
lib/re-runs on its own. -
Editing the app needs the bundler running in a second terminal:
npm run build:fixture -- --watch=forever
plus a
Ctrl+Rin the live-mode terminal to re-run tests.
No sleeps. TestCafe's assertions retry until they pass or time out, so waiting is expressed as an assertion about the thing you are waiting for:
await t.click(this.checkboxButton);
await t.expect(this.checkbox.exists).notOk('Expected checkbox to be removed');Precondition, action, postcondition, with a message that explains the failure
when it comes. There is not a single t.wait() in the suite.
Page objects assert their own readiness. Each one is built through an async static factory that will not hand you an object until the page is usable:
const menu = await Menu.create(); // throws if the menu never appearedThat moves "is the page ready?" out of every individual test.
React selectors query the component, not the markup. The navigation drawer decides whether it needs to click by reading the component's own state:
if (!await this.hamburger.getReact(({props}) => props.expanded)) {
await t.click(this.hamburger);
}Restyling the drawer cannot break this. Renaming its expanded prop can, and
should, because that is a real change to the component's contract.
The local suite owns its target. fixtures/app is a small React app served
from this repo. The original version of these tests ran against a public
documentation site, and by 2026 every selector in them had rotted. Owning the
fixture makes the suite deterministic. Its accessibility test scans the open
menu state with an empty violation baseline, because any violation it finds is
one we can actually fix.
Both suites run axe-core against the full WCAG 2.1 Level AA rule set, plus
best-practice and section508. Axe's WCAG tags are additive per version
rather than inclusive, so covering 2.1 AA means naming all four of wcag2a,
wcag2aa, wcag21a, and wcag21aa.
For the tested open-menu state in fixtures/app, the accepted-violation list
is empty and must stay that way.
For the third-party site, it is not empty, because those violations are not ours to fix. Rather than disabling accessibility checking, the suite keeps an explicit, justified baseline. Anything outside it still fails:
const ACCEPTED_VIOLATIONS = {
// "Form elements must have labels" - the checkbox and text input on
// /dynamic_controls ship with no associated <label>. A genuine WCAG 2 A
// failure in the fixture site, verified still present.
label: {enabled: false},
// "All page content should be contained by landmarks" - the site predates
// landmark regions. Best-practice tag rather than a WCAG failure.
region: {enabled: false},
// "Elements must meet minimum color contrast ratio thresholds" - the two
// swap buttons on /dynamic_controls fail WCAG 1.4.3. A genuine Level AA
// failure, and one the old Level A rule set never looked for; it surfaced
// the moment this suite moved to the full 2.1 AA tags.
'color-contrast': {enabled: false},
};Delete an entry to watch axe correctly report the violation. Each one was re-verified as still present during the 2026 update.
This began as educational software, where accessibility is a legal obligation rather than a nice-to-have. Public schools and state universities fall under ADA Title II, private institutions under Title III, and anything taking federal funds under Section 504 of the Rehabilitation Act.
Title II is the one with a codified technical standard. DOJ's 2024 rule adopts WCAG 2.1 Level AA, due 26 April 2027 for entities serving 50,000 or more people and 26 April 2028 for smaller ones, dates an interim final rule extended in April 2026. Title III and Section 504 impose accessibility obligations without DOJ adopting a single web standard, which is why WCAG 2.1 AA is the working benchmark rather than a literal citation.
That is also why the accessibility checks here are not decoration. Both suites
run the full 2.1 AA rule set, and the fixtures/app open-menu scan permits no
violations.
It is an automated check, not a conformance claim. Axe covers roughly a third of WCAG success criteria; focus order, alt-text quality, and screen-reader semantics still need manual and assistive-technology testing. What this repo shows is the automatable portion wired into CI, so a regression fails the build instead of reaching a student.
CI tests the local suite on Node 22 and 24, with Chrome on both and Firefox on
Node 24. Its stable CI result covers lint and all owned tests. The third-party
job starts only after those jobs finish and is continue-on-error, so someone
else's outage does not turn this badge red; the result is still reported, and
a weekly scheduled run surfaces drift even when nothing here changes.