Skip to content

Commit 01a79f6

Browse files
committed
Bump to v1.0.5, update README, strict type checks
This took a lot of little changes and additional `@type` comments here and there. But I think it actually did make the code a bit better in a couple of key places. Also updated the README to include directions on including `test-page-opener` directly in a test bundle. See mbland/tomcat-servlet-testing-example#83 and commit mbland/tomcat-servlet-testing-example@4c1bdb8. Changes include: - Added `settings.jsdoc.preferredTypes.Object = "object"` to .eslintrc to enable "Object.<..., ...>" syntax in a JSDoc `@typedef`. Got rid of some extra whitespaces in .eslintrc, too. - https://github.com/gajus/eslint-plugin-jsdoc/blob/b60cbb027b03b4f6d509933b0dca8681dbe47206/docs/rules/check-types.md#why-not-capital-case-everything - https://github.com/gajus/eslint-plugin-jsdoc/blob/b60cbb027b03b4f6d509933b0dca8681dbe47206/docs/settings.md#settings-to-configure-check-types-and-no-undefined-types - Updated jsconfig.json to specify: ```json "lib": [ "ES2022" ], "module": "node16", "target": "es2020", "strict": true, "exclude": [ "node_modules/**", "coverage*/**", "jsdoc/**" ] ``` The "lib", "modules", and "target" lines are to ensure compatibility with Node v18, and there's no more disabling `strictNullChecks` and `noImplicitAny` after `"strict": true`. Most of the changes in this commit are a result of removing those two options. - Added the jsdoc-plugin-intersection JSDoc plugin to prevent TypeScript intersection types from breaking `pnpm jsdoc`. I needed to use an intersection type in the `@typedef` for `CovWindow` to fix the `window` types when setting the Istanbul coverage map. Neither the JSDoc `@extends` tag or a union type (`|`) would do. - https://www.npmjs.com/package/jsdoc-plugin-intersection - https://stackoverflow.com/questions/36737921/how-to-extend-a-typedef-parameter-in-jsdoc - jsdoc/jsdoc#1285 - Defined `@typedef CovWindow` to eliminate warnings in the `BrowserPageOpener` code for creating and merging coverage maps. - Added a check for `window.open()` returning `null` in `BrowserPageOpener.open()`, along with a new test covering this case. - Defined `@typedef JSDOM` to eliminate warnings in `JsdomPageOpener`. - Restored globalThis.{document,window} instead of deleting them, and added a test to validate this. This also fixed a subtle bug whereby calling `reject(err)` previously allowed the rest of the function to continue. (Thanks for forcing me to look at this, TypeScript!) - Added @types/{chai,istanbul-lib-coverage,jsdom} to devDependencies and added a `pnpm typecheck` command. Now the whole project and its dependencies pass strict type checking. - Updated everything under test/ and test-modules/ to be strictly TypeScript compiliant. - Some "TestPageOpener > loads page with module successfully" assertions had to be extended with `|| {}`. This is because TypeScript doesn't recognize the `expect(...).not.toBeNull()` expression as a null check. - Added a new missing.html and "JsdomPageOpener > doesn't throw if missing app div" test case to cover new null check in test-modules/main.js. This did, however, throw off Istanbul coverage, but not V8 coverage. Running just the "doesn't throw" test case shows 0% coverage of main.js, even though the test clearly passes. My suspicion is that Istanbul can't associate the `./main.js?version=missing` import path from missing.html with the test-modules/main.js file. So now `pnpm test:ci:jsdom` will use the V8 provider, and `pnpm test:ci:browser`, which doesn't use missing.html, will continue to use Istanbul. Each task outputs its own separate .lcov file which then gets merged into Coveralls. - Updated `pnpm test:ci` to incorporate `pnpm jsdoc` and `pnpm typecheck`. - Other little style cleanups sprinkled here and there. --- Normally I'd prefer not to commit a change this large at once, and I'd likely ask someone else to break it up. Then again, each of these changes is so small and understandable, and they're thematically related to one another. Plus, the total change isn't that long. Hence, I've rationalized breaking my own rule in this instance.
1 parent f6bf538 commit 01a79f6

19 files changed

+270
-48
lines changed

.eslintrc

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
2-
"env" : {
2+
"env": {
33
"node": true,
4-
"es2023" : true
4+
"es2023": true
55
},
66
"parserOptions": {
77
"ecmaVersion": "latest",
@@ -25,7 +25,7 @@
2525
]
2626
}
2727
],
28-
"rules" : {
28+
"rules": {
2929
"@stylistic/js/comma-dangle": [
3030
"error", "never"
3131
],
@@ -50,5 +50,12 @@
5050
"no-console": [
5151
"error", { "allow": [ "warn", "error" ]}
5252
]
53+
},
54+
"settings": {
55+
"jsdoc": {
56+
"preferredTypes": {
57+
"Object": "object"
58+
}
59+
}
5360
}
5461
}

README.md

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,39 @@ describe('TestPageOpener', () => {
4343
})
4444
```
4545

46+
### Using with a bundler (e.g., with Rollup, Vite, and Vitest)
47+
48+
If your project uses any bundler plugins that perform source transforms, you
49+
_may_ need to configure your project to include `test-page-loader` in the test
50+
bundle. Specifically, if it transforms files without a `.js` extension into importable JavaScript, `test-page-opener` may fail with an error resembling:
51+
52+
```text
53+
Caused by: TypeError: Unknown file extension ".hbs" for
54+
/.../mbland/tomcat-servlet-testing-example/strcalc/src/main/frontend/components/calculator.hbs
55+
————————————————————————————————————————————————————————
56+
Serialized Error: { code: 'ERR_UNKNOWN_FILE_EXTENSION' }
57+
————————————————————————————————————————————————————————
58+
```
59+
60+
For example, using [Vite][] and [Vitest][], which use [Rollup][] under the hood,
61+
you will need to add this `server:` setting to the `test` config object:
62+
63+
```js
64+
test: {
65+
server: {
66+
deps: {
67+
// Without this, jsdom tests will fail to import '.hbs' files
68+
// transformed by rollup-plugin-handlebars-precompiler.
69+
inline: ['test-page-opener']
70+
}
71+
}
72+
}
73+
```
74+
75+
For a concrete example with more details, see:
76+
77+
- <https://github.com/mbland/tomcat-servlet-testing-example/pull/83>
78+
4679
### Reporting code coverage
4780

4881
`TestPageOpener` makes it possible to collect code coverage from opened browser
@@ -229,11 +262,12 @@ level explanation.
229262
[coveralls-tpo]: https://coveralls.io/github/mbland/test-page-opener?branch=main
230263
[npm-tpo]: https://www.npmjs.com/package/test-page-opener
231264
[pnpm]: https://pnpm.io/
265+
[Vite]: https://vitejs.dev/
266+
[Vitest]: https://vitest.dev/
267+
[Rollup]: https://rollupjs.org/
232268
[DOMContentLoaded]: https://developer.mozilla.org/docs/Web/API/Document/DOMContentLoaded_event
233269
[window.load]: https://developer.mozilla.org/docs/Web/API/Window/load_event
234270
[DOM]: https://developer.mozilla.org/docs/Web/API/Document_Object_Model
235-
[Vite]: https://vitejs.dev/
236-
[Vitest]: https://vitest.dev/
237271
[ECMAScript Modules]: https://nodejs.org/docs/latest-v18.x/api/esm.html
238272
[ESM resolution and loading algorithm]: https://nodejs.org/docs/latest-v18.x/api/esm.html#resolution-and-loading-algorithm
239273
[jsdom-2475]: https://github.com/jsdom/jsdom/issues/2475

ci/vitest.config.browser.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export default mergeConfig(baseConfig, defineConfig({
66
test: {
77
outputFile: 'TESTS-TestSuites-browser.xml',
88
coverage: {
9+
provider: 'istanbul',
910
reportsDirectory: 'coverage-browser'
1011
},
1112
browser: {

ci/vitest.config.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ export default mergeConfig(viteConfig, defineConfig({
88
reporters: [ 'junit', 'default' ],
99
coverage: {
1010
enabled: true,
11-
provider: 'istanbul',
1211
reporter: [ 'text', 'lcovonly' ],
1312
reportsDirectory: 'coverage-jsdom'
1413
}

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ export default class TestPageOpener {
102102
* @param {string} pagePath - path to the HTML file relative to the basePath
103103
* specified during `TestPageOpener.create()`
104104
* @returns {Promise<OpenedPage>} - object representing the opened page
105+
* @throws {Error} if pagePath is malformed or opening page failed
105106
*/
106107
async open(pagePath) {
107108
if (pagePath.startsWith('/')) {

jsconfig.json

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
{
22
"compilerOptions": {
33
"checkJs": true,
4-
"module": "nodenext",
5-
"strict": true,
6-
"strictNullChecks": false,
7-
"noImplicitAny": false
4+
"lib": [
5+
"ES2022"
6+
],
7+
"module": "node16",
8+
"target": "es2020",
9+
"strict": true
810
},
9-
"exclude": ["node_modules"]
11+
"exclude": [
12+
"node_modules/**",
13+
"coverage*/**",
14+
"jsdoc/**"
15+
]
1016
}

jsdoc.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
{
22
"plugins": [
3-
"plugins/markdown"
3+
"plugins/markdown",
4+
"node_modules/jsdoc-plugin-intersection"
45
],
56
"recurseDepth": 10,
67
"source": {
78
"includePattern": ".+\\.js$",
8-
"exclude": ["node_modules"]
9+
"exclude": ["node_modules", "test"]
910
},
1011
"opts": {
1112
"destination": "jsdoc",

lib/browser.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
import libCoverage from 'istanbul-lib-coverage'
99
import { OpenedPage } from './types.js'
1010

11+
/**
12+
* Type for accessing the Istanbul coverage object in Window
13+
* @typedef {Window & Object.<string, libCoverage.CoverageMap>} CovWindow
14+
*/
15+
1116
/**
1217
* Returns the window and document from a browser-opened HTML file.
1318
*
@@ -20,6 +25,7 @@ import { OpenedPage } from './types.js'
2025
export default class BrowserPageOpener {
2126
#window
2227
#coverageKey
28+
#coverageMap
2329

2430
/**
2531
* @param {Window} window - the global (browser) window object
@@ -34,19 +40,26 @@ export default class BrowserPageOpener {
3440
// coverage. There's no harm in this, and it avoids a coverage gap for a
3541
// condition that, by definition, would never execute when collecting
3642
// coverage. We could use a directive to ignore that gap, but why bother.
37-
window[covKey] = libCoverage.createCoverageMap(window[covKey])
43+
const covWindow = /** @type {CovWindow} */ (window)
44+
this.#coverageMap = libCoverage.createCoverageMap(covWindow[covKey])
45+
covWindow[covKey] = this.#coverageMap
3846
}
3947

4048
/**
4149
* Opens another page within a web browser.
4250
* @param {string} basePath - base path of the application under test
4351
* @param {string} pagePath - path to the HTML file relative to basePath
4452
* @returns {Promise<OpenedPage>} - object representing the opened page
53+
* @throws {Error} if opening page failed
4554
*/
4655
async open(basePath, pagePath) {
47-
const w = this.#window.open(`${basePath}${pagePath}`)
56+
const fullPath = `${basePath}${pagePath}`
57+
const w = this.#window.open(fullPath)
58+
if (w === null) throw new Error(`failed to open: ${fullPath}`)
59+
4860
const close = () => {
49-
this.#window[this.#coverageKey].merge(w[this.#coverageKey])
61+
const testWindow = /** @type {CovWindow} */ (w)
62+
this.#coverageMap.merge(testWindow[this.#coverageKey])
5063
w.close()
5164
}
5265

lib/jsdom.js

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77

88
import { OpenedPage } from './types.js'
99

10+
/**
11+
* @typedef {object} JSDOM - simulated jsdom.JSDOM
12+
* @property {Function} fromFile - simulated JSDOM.fromFile
13+
*/
14+
1015
/**
1116
* Returns window and document objects from a jsdom-parsed HTML file.
1217
*
@@ -61,7 +66,7 @@ export default class JsdomPageOpener {
6166
/**
6267
* Creates a JsdomPageOpener from a dynamically imported jsdom module
6368
* @param {object} jsdom - dynamically imported jsdom module
64-
* @param {object} jsdom.JSDOM - JSDOM class from the jsdom module
69+
* @param {JSDOM} jsdom.JSDOM - JSDOM class from the jsdom module
6570
*/
6671
constructor({ JSDOM }) {
6772
this.#JSDOM = JSDOM
@@ -72,6 +77,7 @@ export default class JsdomPageOpener {
7277
* @param {string} _ - ignored
7378
* @param {string} pagePath - path to the HTML file to load
7479
* @returns {Promise<OpenedPage>} - object representing the opened page
80+
* @throws {Error} if opening page failed
7581
*/
7682
async open(_, pagePath) {
7783
const { window } = await this.#JSDOM.fromFile(
@@ -125,35 +131,40 @@ export default class JsdomPageOpener {
125131
// register closures over window and document, or specific document
126132
// elements. That would ensure they remain defined even after we remove
127133
// window and document from globalThis.
128-
//
134+
const { window: origWindow, document: origDocument } = globalThis
135+
136+
/** @param {Function} done - called after restoring original globals */
137+
const resetGlobals = done => {
138+
globalThis.document = origDocument
139+
globalThis.window = origWindow
140+
done()
141+
}
142+
129143
// @ts-expect-error
130144
globalThis.window = window
131145
globalThis.document = document
132-
const Event = globalThis.window.Event
133146

134147
try { await importModules(document) }
135-
catch (err) { reject(err) }
148+
catch (err) { return resetGlobals(() => {reject(err)}) }
136149

137150
// Manually firing DOMContentLoaded again after loading modules
138151
// approximates the requirement that modules execute before
139152
// DOMContentLoaded. This means that the modules can register
140153
// DOMContentLoaded event listeners and have them fire here.
141154
//
142155
// We eventually fire the 'load' event again too for the same reason.
143-
document.dispatchEvent(new Event(
144-
'DOMContentLoaded', {bubbles: true, cancelable: false}
145-
))
156+
const Event = globalThis.window.Event
157+
document.dispatchEvent(
158+
new Event('DOMContentLoaded', {bubbles: true, cancelable: false})
159+
)
146160

147161
// Register a 'load' listener that deletes the global window and
148162
// document variables. Because it's registered after any
149163
// DOMContentLoaded listeners have fired, it should execute after any
150164
// other 'load' listeners registered by any module code.
151-
const resetGlobals = () => {
152-
delete globalThis.document
153-
delete globalThis.window
154-
resolve()
155-
}
156-
window.addEventListener('load', resetGlobals, {once: true})
165+
window.addEventListener(
166+
'load', () => {resetGlobals(resolve)}, {once: true}
167+
)
157168
window.dispatchEvent(
158169
new Event('load', {bubbles: false, cancelable: false})
159170
)

package.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
{
22
"name": "test-page-opener",
3-
"version": "1.0.4",
3+
"version": "1.0.5",
44
"description": "Enables an application's tests to open its own page URLs both in the browser and in Node.js using jsdom",
55
"main": "index.js",
66
"types": "types/index.d.ts",
77
"scripts": {
88
"lint": "eslint --color --max-warnings 0 .",
99
"test": "vitest",
10-
"test:ci": "eslint --color --max-warnings 0 . && vitest run -c ci/vitest.config.js && vitest run -c ci/vitest.config.browser.js",
10+
"test:ci": "pnpm lint && pnpm typecheck && pnpm jsdoc && pnpm test:ci:jsdom && pnpm test:ci:browser",
11+
"test:ci:jsdom": "vitest run -c ci/vitest.config.js",
12+
"test:ci:browser": "vitest run -c ci/vitest.config.browser.js",
1113
"jsdoc": "jsdoc-cli-wrapper -c jsdoc.json .",
14+
"typecheck": "npx -p typescript tsc -p jsconfig.json --noEmit --pretty",
1215
"prepack": "npx -p typescript tsc ./index.js --allowJs --declaration --declarationMap --emitDeclarationOnly --outDir types"
1316
},
1417
"files": [
@@ -32,6 +35,9 @@
3235
"bugs": "https://github.com/mbland/test-page-opener/issues",
3336
"devDependencies": {
3437
"@stylistic/eslint-plugin-js": "^1.5.3",
38+
"@types/chai": "^4.3.11",
39+
"@types/istanbul-lib-coverage": "^2.0.6",
40+
"@types/jsdom": "^21.1.6",
3541
"@vitest/browser": "^1.1.3",
3642
"@vitest/coverage-istanbul": "^1.1.3",
3743
"@vitest/coverage-v8": "^1.1.3",
@@ -40,6 +46,7 @@
4046
"eslint-plugin-jsdoc": "^46.10.1",
4147
"eslint-plugin-vitest": "^0.3.20",
4248
"jsdoc-cli-wrapper": "^1.0.4",
49+
"jsdoc-plugin-intersection": "^1.0.4",
4350
"jsdom": "^23.1.0",
4451
"typescript": "^5.3.3",
4552
"vite": "^5.0.11",

pnpm-lock.yaml

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)