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'