test-page-opener
v1.0.6
Published
Enables an application's tests to open its own page URLs both in the browser and in Node.js using jsdom
Downloads
12
Readme
Test Page Opener
Enables an application's tests to open its own page URLs both in the browser and in Node.js using jsdom. Provides limited, though still very useful support for opening pages that load external JavaScript modules when using jsdom.
Source: https://github.com/mbland/test-page-opener
Installation
Add this package to your project's devDependencies
, e.g., using pnpm:
pnpm add -D test-page-opener
Usage
import { afterEach, beforeAll, describe, expect, test } from 'vitest'
import TestPageOpener from 'test-page-opener'
describe('TestPageOpener', () => {
let opener
beforeAll(async () => {opener = await TestPageOpener.create('/basedir/')})
afterEach(() => opener.closeAll())
test('loads page with module successfully', async () => {
const { document } = await opener.open('path/to/index.html')
const appElem = document.querySelector('#app')
expect(appElem).not.toBeNull()
expect(appElem.textContent).toContain('Hello, World!')
})
})
Using with a bundler (e.g., with Rollup, Vite, and Vitest)
If your project uses any bundler plugins that perform source transforms, you
may need to configure your project to include test-page-loader
in the test
bundle. Specifically, if it transforms files without a .js
extension into importable JavaScript, test-page-opener
may fail with an error resembling:
Caused by: TypeError: Unknown file extension ".hbs" for
/.../mbland/tomcat-servlet-testing-example/strcalc/src/main/frontend/components/calculator.hbs
————————————————————————————————————————————————————————
Serialized Error: { code: 'ERR_UNKNOWN_FILE_EXTENSION' }
————————————————————————————————————————————————————————
For example, using Vite and Vitest, which use Rollup under the hood,
you will need to add this server:
setting to the test
config object:
test: {
server: {
deps: {
// Without this, jsdom tests will fail to import '.hbs' files
// transformed by rollup-plugin-handlebars-precompiler.
inline: ['test-page-opener']
}
}
}
For a concrete example with more details, see:
Reporting code coverage
TestPageOpener
makes it possible to collect code coverage from opened browser
windows and to merge it with coverage from jsdom test runs.
For example, this project is configured to generate coverage-jsdom/lcov.info
and coverage-browser/lcov.info
, the results of which are merged via the
Coveralls GitHub Action. See:
See Code coverage collection from opened pages, scripts, and modules below for further details.
Features and limitations
Limited JavaScript/ECMAScript/ES6 Module (a.k.a. ESM) support for jsdom
jsdom doesn't natively support JavaScript modules at all, even though Node.js has supported ECMAScript modules since v18. The problem is that the current Node.js ESM API leaves implementation of the nontrivial ESM resolution and loading algorithm up to the user. See: jsdom/jsdom: <script type=module> support #2475.
TestPageOpener
provides limited support for loading external modules specified
by the src
attribute of <script type="module"> tags.
It achieves this by passing the src
path to dynamic import().
Inline module scripts and <script type="importmap"> aren't supported.
Timing of <script type="module">
execution
Technically, imported modules should execute similarly to <script defer> and execute before the DOMContentLoaded event.
However, TestPageOpener
registers a load
event handler that collects src
paths and waits for the dynamic import()
of each path to resolve. It then
fires the DOMContentLoaded
and load
events again, enabling modules that
register listeners for those events to behave as expected.
Even more detail
DOMContentLoaded
and load
events from JSDOM.fromFile()
always fire before
dynamic module imports finish resolving. In some cases, DOMContentLoaded
fires
even before JSDOM.fromFile()
resolves.
- If, immediately after
JSDOM.fromFile()
resolves, document.readyState isloading
,DOMContentLoaded
has yet to fire. If it'sinteractive
,DOMContentLoaded
has already fired, andload
is about to fire.
The test/event-ordering-demo/main.js demo script from this package shows this behavior in action. See that file's comments for details.
Code coverage collection from opened pages, scripts, and modules
When TestPageOpener
closes an opened page in the browser, it will collect
Istanbul code coverage information from the page's Window before closing
it. Otherwise any code coverage information generated by code running in the
other browser window would be lost. (The jsdom implementation doesn't need to do
this.)
Only supports Istanbul code coverage in the browser
Vitest allows you to collect coverage via Istanbul, v8 code
coverage, or your own custom provider when running tests in Node.js. However,
TestPageOpener
only supports Istanbul when running tests in the browser.
Technically, it's not strictly required that you use Istanbul for running tests under Node.js and jsdom if you also run them in the browser. You may choose to do so anyway for consistency's sake, in your continuous integration system if nowhere else.
Development
Uses pnpm and Vitest for building and testing.
Uses GitHub Actions for continuous integration.
Developed using Vim, Visual Studio Code, and IntelliJ IDEA interchangeably, depending on my inclination from moment to moment.
Motivation
Validating initial page state using different DOM implementations
The TestPageOpener
class enables smaller tests to validate the initial state
of an application's pages after the DOMContentLoaded and window.load
events. They can access the DOM directly, using jsdom or any browser
implementation interchangeably. Running the same tests using Node.js, then in
different browsers, then becomes very easy using frontend development and
testing frameworks like Vite together with Vitest.
Accelerating development while building confidence
Using jsdom as a test double in Node.js makes detecting and fixing problems much faster than testing solely using the browser. Then running the same tests unchanged in the browser increases confidence in both the code under test and in the tests themselves.
Most of your code will behave the same way running under jsdom it will under any browser implementation most of the time. This is why jsdom is such an effective test double, because it helps validate most behaviors and catch most programming errors very quickly. However, if the tests do reveal behavioral discrepancies between jsdom and any browsers, developing robust, portable solutions also becomes much faster than using larger tests.
TestPageOpener
extends these benefits to page loading, which would otherwise
be beyond the reach of smaller tests. Not all of your smaller tests need or
should use it—most of your page logic should be testable independently
from its own Window context. But you can use TestPageOpener
to write
smaller, faster tests for some behavior that would normally involve writing
larger, slower tests.
Testing independently from a backend server and tests in other languages
Using TestPageOpener
with frameworks like Vite and Vitest can help
validate some page loading details without building and serving the backend.
This is especially convenient if the backend and/or your larger tests that use
Selenium WebDriver or another browser-based framework are in another
language. You can iterate quickly on JavaScript, in JavaScript, without other
languages, tools, or processes involved until you're reasonably confident that
everything is in order.
Reducing investment in writing and running larger test suites
TestPageOpener
avoids having to validate all page loading logic by only
using frameworks like Selenium WebDriver that interact with pages by
launching a separate browser. It can validate loading behaviors that are beyond
the scope of unit tests for individual page components while using the same unit
testing framework.
Writing all page validation tests using TestPageOpener
may not be feasible,
and isn't necessarily desirable. However, it enables rapid iteration on details
that can be validated this way. Combined with a suite of small, fast tests for
individual components, it can allow for fast smoke testing before running larger
test suites. In turn, this reduces the need to write as many larger tests, to
run them as frequently, or to spend as much time debugging failures.
Improving coverage, efficiency, and productivity overall
Designing for testability, and using TestPageOpener
to write smaller tests as
appropriate, can improve test coverage while relying less on larger tests to
validate everything. Failures can be caught (and fixes validated) by tests of
the appropriate size and scope, minimizing the time to diagnose, repair, and
recover from them. This improves the speed, stability, and coverage of the
entire test suite, making every individual test more valuable and more of a
boost to productivity. The larger and more complex the system—and the team
developing it—the greater the overall benefit.
For more thoughts on this approach to automated testing, see The Test Pyramid and the Chain Reaction.
Background
I developed this while writing tests for the frontend component of
mbland/tomcat-servlet-testing-example, found under
strcalc/src/main/frontend
. I started developing the Java backend first, then
wrote an initial Selenium WebDriver test in Java against a placeholder
frontend page. When I started to focus on developing the frontend, I wanted to
see if I could write tests that could run both in Node.js and any browser.
TestPageOpener
is the result of that experiment. At first I thought I might
use it to make all of my frontend tests portable from jsdom to any browser.
Eventually I realized I only really needed it to validate page loading. The
Vitest browser mode (using the @vitest/browser plugin) enables all other
tests written using the jsdom environment to run as expected in the browser.
Copyright
© 2023 Mike Bland <[email protected]> (https://mike-bland.com/)
Open Source License
This software is made available as Open Source software under the Mozilla Public License 2.0. For the text of the license, see the LICENSE.txt file. See the MPL 2.0 FAQ for a higher level explanation.