From a63f27466b371f3935ac90056835ccc5f9fe9776 Mon Sep 17 00:00:00 2001 From: Mike Bland Date: Sun, 7 Jan 2024 17:01:04 -0500 Subject: [PATCH] Bump to v1.0.4, add type files, fix type issues This adds Typescript to devDependencies and a `prepack` script to generate `types/*.d.ts{,map}` files for distribution. It also adds a jsonconfig.json files to ensure Visual Studio Code performs more thorough type checking. Enables strict type checking, but disables null and implicit "any" checks. Fixes several existing issues, noted below. Inspired by VS Code warnings from importing v1.0.3 into mbland/rollup-plugin-handlebars-precompiler. Based on guidance from: - https://code.visualstudio.com/docs/nodejs/working-with-javascript - https://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html - https://code.visualstudio.com/docs/languages/jsconfig - https://www.typescriptlang.org/docs/handbook/intro-to-js-ts.html - https://www.typescriptlang.org/docs/handbook/modules/introduction.html - https://www.typescriptlang.org/docs/handbook/declaration-files/dts-from-js.html Minor issues fixed: - The `@returns` type of `TestPageOpener.create()` was originally `TestPageOpener`, but is now `Promise`. - The `beforeAll()` handler originally returned `Promise`, and now returns nothing. - Added '.js' extension to all internal imports. - Added @ts-nocheck to vite and vitest config files. - Changed `@returns {Promise}` to `@returns {Promise}`. - Expanded the `resetGlobals()` event callback so the `delete` statements and `resolve()` call are on separate lines. - Added `eslint-env browser` and `@type {HTMLScriptElement}` to test/event-ordering-demo/main.js. - Added `@ts-expect-error` comment when overriding `globalThis.window` in JsdomPageOpener. More substantial fixes: - Assigned `const Event = globalThis.window.Event` and changed `window.Event` to just `Event` in JsdomPageOpener. Using the builtin Node.js Event was incompatible with the jsdom Window and Document implementations. - Changed the jsdom and jsdom.JSDOM parameters of the JsdomPageOpener constructor to type "object". I had tried using: ```js /** @typedef {typeof import('jsdom')} jsdom */ ``` Which worked great in VS Code, but broke the jsdoc CLI. I found a couple jsdoc CLI plugins which seemed like they would strip this comment, but couldn't get them to work. My JSDoc comments could stand to use some work to get the `jsdoc`-generated site to look better and make more sense. They already work great in VS Code, though, and at least don't break the CLI. --- .gitignore | 2 ++ README.md | 3 ++- ci/vitest.config.browser.js | 3 ++- ci/vitest.config.js | 3 ++- index.js | 11 +++++++---- jsconfig.json | 10 ++++++++++ jsdoc.json | 4 +++- lib/browser.js | 3 +++ lib/jsdom.js | 24 +++++++++++++++--------- package.json | 13 ++++++++++--- pnpm-lock.yaml | 3 +++ test/browser.test.js | 2 +- test/event-ordering-demo/main.js | 6 ++++-- test/jsdom.test.js | 2 +- test/main.test.js | 4 +++- vite.config.js | 1 + 16 files changed, 69 insertions(+), 25 deletions(-) create mode 100644 jsconfig.json diff --git a/.gitignore b/.gitignore index 4802aa1..e19716d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,6 @@ node_modules/ out/ pnpm-debug.log tmp/ +types/ *.log +*.tgz diff --git a/README.md b/README.md index f69537d..253cf77 100644 --- a/README.md +++ b/README.md @@ -30,13 +30,14 @@ import TestPageOpener from 'test-page-opener' describe('TestPageOpener', () => { let opener - beforeAll(async () => opener = await TestPageOpener.create('/basedir/')) + 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!') }) }) diff --git a/ci/vitest.config.browser.js b/ci/vitest.config.browser.js index 923c7b7..72ff124 100644 --- a/ci/vitest.config.browser.js +++ b/ci/vitest.config.browser.js @@ -1,5 +1,6 @@ +// @ts-nocheck import { defineConfig, mergeConfig } from 'vitest/config' -import baseConfig from './vitest.config' +import baseConfig from './vitest.config.js' export default mergeConfig(baseConfig, defineConfig({ test: { diff --git a/ci/vitest.config.js b/ci/vitest.config.js index 1c775e6..b7a186a 100644 --- a/ci/vitest.config.js +++ b/ci/vitest.config.js @@ -1,5 +1,6 @@ +// @ts-nocheck import { defineConfig, mergeConfig } from 'vitest/config' -import viteConfig from '../vite.config' +import viteConfig from '../vite.config.js' export default mergeConfig(viteConfig, defineConfig({ test: { diff --git a/index.js b/index.js index cc9c64f..880e530 100644 --- a/index.js +++ b/index.js @@ -21,13 +21,14 @@ import { OpenedPage } from './lib/types.js' * describe('TestPageOpener', () => { * let opener * - * beforeAll(async () => opener = await TestPageOpener.create('/basedir/')) + * 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!') * }) * }) @@ -38,6 +39,7 @@ export default class TestPageOpener { #basePath #impl + /** @type {OpenedPage[]} */ #opened /** @@ -69,12 +71,13 @@ export default class TestPageOpener { * * ```js * let opener - * beforeAll(async () => opener = await TestPageOpener.create('/basedir/')) + * beforeAll(async () => {opener = await TestPageOpener.create('/basedir/')}) * ``` * @param {string} basePath - base path of the application under test; must * start with '/' and end with '/' - * @returns {TestPageOpener} - a new TestPageOpener initialized to open pages - * in the current test environment, either via Jsdom or the browser + * @returns {Promise} - a new TestPageOpener initialized to + * open pages in the current test environment, either via jsdom or the + * browser */ static async create(basePath) { const impl = globalThis.window ? diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..5367b8a --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "checkJs": true, + "module": "nodenext", + "strict": true, + "strictNullChecks": false, + "noImplicitAny": false + }, + "exclude": ["node_modules"] +} diff --git a/jsdoc.json b/jsdoc.json index b48e5f8..0361ec2 100644 --- a/jsdoc.json +++ b/jsdoc.json @@ -1,5 +1,7 @@ { - "plugins": [ "plugins/markdown" ], + "plugins": [ + "plugins/markdown" + ], "recurseDepth": 10, "source": { "includePattern": ".+\\.js$", diff --git a/lib/browser.js b/lib/browser.js index 5943716..1dba54e 100644 --- a/lib/browser.js +++ b/lib/browser.js @@ -21,6 +21,9 @@ export default class BrowserPageOpener { #window #coverageKey + /** + * @param {Window} window - the global (browser) window object + */ constructor(window) { const covKey = getCoverageKey(window) diff --git a/lib/jsdom.js b/lib/jsdom.js index b503967..827e7e1 100644 --- a/lib/jsdom.js +++ b/lib/jsdom.js @@ -61,7 +61,7 @@ export default class JsdomPageOpener { /** * Creates a JsdomPageOpener from a dynamically imported jsdom module * @param {object} jsdom - dynamically imported jsdom module - * @param {jsdom.JSDOM} jsdom.JSDOM - the JSDOM class + * @param {object} jsdom.JSDOM - JSDOM class from the jsdom module */ constructor({ JSDOM }) { this.#JSDOM = JSDOM @@ -91,7 +91,7 @@ export default class JsdomPageOpener { * Dynamically imports ECMAScript modules. * @param {Window} window - the jsdom window object * @param {Document} document - the jsdom window.document object - * @returns {Promise} - resolves after importing all ECMAScript modules + * @returns {Promise} - resolves after importing all ECMAScript modules * @throws if importing any ECMAScript modules fails */ #importModules(window, document) { @@ -125,8 +125,11 @@ export default class JsdomPageOpener { // register closures over window and document, or specific document // elements. That would ensure they remain defined even after we remove // window and document from globalThis. + // + // @ts-expect-error globalThis.window = window globalThis.document = document + const Event = globalThis.window.Event try { await importModules(document) } catch (err) { reject(err) } @@ -137,7 +140,7 @@ export default class JsdomPageOpener { // DOMContentLoaded event listeners and have them fire here. // // We eventually fire the 'load' event again too for the same reason. - document.dispatchEvent(new window.Event( + document.dispatchEvent(new Event( 'DOMContentLoaded', {bubbles: true, cancelable: false} )) @@ -145,12 +148,14 @@ export default class JsdomPageOpener { // document variables. Because it's registered after any // DOMContentLoaded listeners have fired, it should execute after any // other 'load' listeners registered by any module code. - const resetGlobals = () => resolve( - delete globalThis.document, delete globalThis.window - ) + const resetGlobals = () => { + delete globalThis.document + delete globalThis.window + resolve() + } window.addEventListener('load', resetGlobals, {once: true}) window.dispatchEvent( - new window.Event('load', {bubbles: false, cancelable: false}) + new Event('load', {bubbles: false, cancelable: false}) ) } window.addEventListener('load', importModulesOnEvent, {once: true}) @@ -163,10 +168,11 @@ export default class JsdomPageOpener { * * Only works with the `src` attribute; it will not execute inline code. * @param {Document} doc - the jsdom window.document object - * @returns {Promise} - resolves after importing all ECMAScript modules in doc - * @throws if importing any ECMAScript modules fails + * @returns {Promise} - resolves after importing all modules in doc + * @throws if any module import fails */ function importModules(doc) { + /** @type {HTMLScriptElement[]} */ const modules = Array.from(doc.querySelectorAll('script[type="module"]')) return Promise.all(modules.filter(m => m.src).map(async m => { try { await import(m.src) } diff --git a/package.json b/package.json index b340bf7..8c13a52 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,21 @@ { "name": "test-page-opener", - "version": "1.0.3", + "version": "1.0.4", "description": "Enables an application's tests to open its own page URLs both in the browser and in Node.js using jsdom", "main": "index.js", + "types": "types/index.d.ts", "scripts": { "lint": "eslint --color --max-warnings 0 .", "test": "vitest", "test:ci": "eslint --color --max-warnings 0 . && vitest run -c ci/vitest.config.js && vitest run -c ci/vitest.config.browser.js", - "jsdoc": "jsdoc-cli-wrapper -c jsdoc.json ." + "jsdoc": "jsdoc-cli-wrapper -c jsdoc.json .", + "prepack": "npx -p typescript tsc ./index.js --allowJs --declaration --declarationMap --emitDeclarationOnly --outDir types" }, - "files": [ "index.js", "lib/*" ], + "files": [ + "index.js", + "lib/*", + "types/*" + ], "keywords": [ "testing", "jsdom", @@ -35,6 +41,7 @@ "eslint-plugin-vitest": "^0.3.20", "jsdoc-cli-wrapper": "^1.0.4", "jsdom": "^23.1.0", + "typescript": "^5.3.3", "vite": "^5.0.11", "vitest": "^1.1.3", "webdriverio": "^8.27.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 67053a5..b61597c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,6 +40,9 @@ devDependencies: jsdom: specifier: ^23.1.0 version: 23.1.0 + typescript: + specifier: ^5.3.3 + version: 5.3.3 vite: specifier: ^5.0.11 version: 5.0.11 diff --git a/test/browser.test.js b/test/browser.test.js index 68eed81..9ffc54b 100644 --- a/test/browser.test.js +++ b/test/browser.test.js @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { DEFAULT_COVERAGE_KEY, getCoverageKey } from '../lib/browser' +import { DEFAULT_COVERAGE_KEY, getCoverageKey } from '../lib/browser.js' import { describe, expect, test } from 'vitest' describe('getCoverageKey', () => { diff --git a/test/event-ordering-demo/main.js b/test/event-ordering-demo/main.js index 3c3bda6..de49011 100755 --- a/test/event-ordering-demo/main.js +++ b/test/event-ordering-demo/main.js @@ -1,4 +1,5 @@ #!/usr/bin/env node +/* eslint-env browser */ /* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -46,8 +47,9 @@ const { window } = await JSDOM.fromFile( pagePath, {resources: 'usable', runScripts: 'dangerously'} ) const document = window.document -const modulePath = document.querySelector('script[type="module"]').src -const importPromise = import(modulePath) +/** @type {HTMLScriptElement} */ +const moduleElem = document.querySelector('script[type="module"]') +const importPromise = import(moduleElem.src) print(`document.readyState === ${document.readyState}`) document.addEventListener('DOMContentLoaded', () => print('DOMContentLoaded')) diff --git a/test/jsdom.test.js b/test/jsdom.test.js index fa8fa9a..4e311f9 100644 --- a/test/jsdom.test.js +++ b/test/jsdom.test.js @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import JsdomPageOpener from '../lib/jsdom' +import JsdomPageOpener from '../lib/jsdom.js' import { beforeAll, describe, expect, test } from 'vitest' describe.skipIf(globalThis.window !== undefined)('JsdomPageOpener', () => { diff --git a/test/main.test.js b/test/main.test.js index 22b8f3b..702c421 100644 --- a/test/main.test.js +++ b/test/main.test.js @@ -10,7 +10,7 @@ import TestPageOpener from '../index.js' describe('TestPageOpener', () => { let opener - beforeAll(async () => opener = await TestPageOpener.create('/basedir/')) + beforeAll(async () => {opener = await TestPageOpener.create('/basedir/')}) afterEach(() => opener.closeAll()) test('loads page with module successfully', async () => { @@ -18,7 +18,9 @@ describe('TestPageOpener', () => { const appElem = document.querySelector('#app') const linkElem = document.querySelector('#app p a') + expect(appElem).not.toBeNull() expect(appElem.textContent).toContain('Hello, World!') + expect(linkElem).not.toBeNull() expect(linkElem.href).toContain('%22Hello,_World!%22') }) diff --git a/vite.config.js b/vite.config.js index 7ffe66f..4752e8b 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,3 +1,4 @@ +// @ts-nocheck import { defineConfig } from 'vite' import { configDefaults } from 'vitest/config' import fs from 'node:fs'