From 4c1bdb8985bfdc336eb49407bb5fe7b8dad082e6 Mon Sep 17 00:00:00 2001 From: Mike Bland Date: Fri, 5 Jan 2024 17:54:36 -0500 Subject: [PATCH] Replace test/page-loader.js with test-page-opener MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Required adding test-page-opener to the test.server.deps.inline config variable. Also updated all internal import paths to end with the '.js' extension. These changes were a consequence of the fact that the previous test/page-loader.js file was automatically bundled via Rollup, and the "external" test-page-opener isn't by default. Without bundling, test-page-opener can't resolve Handlebars modules transformed via rollup-plugin-handlebars-precompiler. --- On first adding test-page-opener to the project, running main.test.js in the browser passed. When running it in Node, all of the internal imports that didn't end with '.js' failed. This followed on the heels of mbland/test-page-opener#20, which I created when test-page-opener's own internal imports failed when used in this project. (I also filed mbland/test-page-opener#21 when its internal istanbul-lib-coverage import broke in this project, too.) After updating all the local imports on .js files, the Handlebars imports (ending in '.hbs') continued to fail with ERR_UNKNOWN_FILE_EXTENSION: ```sh % pnpm test -- run main.test.js RUN v1.1.3 /.../tomcat-servlet-testing-example/strcalc/src/main/frontend ❯ main.test.js (1) ❯ String Calculator UI on initial page load (1) × contains the "Hello, World!" placeholder —— Failed Tests 1 —— FAIL main.test.js > String Calculator UI on initial page load > contains the "Hello, World!" placeholder Error: opening index.html ❯ JsdomPageOpener.open node_modules/.pnpm/test-page-opener@1.0.3/node_modules/test-page-opener/lib/jsdom.js:85:13 ❯ TestPageOpener.open node_modules/.pnpm/test-page-opener@1.0.3/node_modules/test-page-opener/index.js:109:18 ❯ main.test.js:18:26 16| 17| test('contains the "Hello, World!" placeholder', async () => { 18| const { document } = await opener.open('index.html') | ^ 19| const appElem = document.querySelector('#app') 20| Caused by: Error: importing file:///.../tomcat-servlet-testing-example/strcalc/src/main/frontend/main.js ❯ node_modules/.pnpm/test-page-opener@1.0.3/node_modules/test-page-opener/lib/jsdom.js:173:25 ❯ importModulesOnEvent node_modules/.pnpm/test-page-opener@1.0.3/node_modules/test-page-opener/lib/jsdom.js:131:15 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' } ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ``` (I'm rather relieved to have implemented mbland/test-page-opener#14 just today, which is why there are such nice "Caused by:" messages above.) When I temporarily replaced the npm dependency with my local working copy via `pnpm link ../../../../../test-page-opener`, the test would pass again. It occurred to me that the Node runs of the test weren't including test-page-opener in the Rollup bundle containing the transformed .hbs files. Or, it would include it when it was linked to the local copy, but not include it when installed normally. I eventually figured out that the Vitest config setting server.deps.inline would cause the "external" test-page-opener to get bundled, enabling it to resolve the '.hbs' imports. - --- strcalc/src/main/frontend/components/app.js | 4 +- .../main/frontend/components/calculators.js | 2 +- strcalc/src/main/frontend/main.js | 2 +- strcalc/src/main/frontend/main.test.js | 14 +- strcalc/src/main/frontend/package.json | 1 + strcalc/src/main/frontend/pnpm-lock.yaml | 10 + strcalc/src/main/frontend/test/page-loader.js | 346 ------------------ strcalc/src/main/frontend/vite.config.js | 7 + 8 files changed, 30 insertions(+), 356 deletions(-) delete mode 100644 strcalc/src/main/frontend/test/page-loader.js diff --git a/strcalc/src/main/frontend/components/app.js b/strcalc/src/main/frontend/components/app.js index d955841..2bac7a3 100644 --- a/strcalc/src/main/frontend/components/app.js +++ b/strcalc/src/main/frontend/components/app.js @@ -10,8 +10,8 @@ * @module init */ -import Introduction from './introduction' -import Calculator from './calculator' +import Introduction from './introduction.js' +import Calculator from './calculator.js' export default class App { /** diff --git a/strcalc/src/main/frontend/components/calculators.js b/strcalc/src/main/frontend/components/calculators.js index 772ce7d..6ab3691 100644 --- a/strcalc/src/main/frontend/components/calculators.js +++ b/strcalc/src/main/frontend/components/calculators.js @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { postFormData } from './request' +import { postFormData } from './request.js' export const DEFAULT_ENDPOINT = './add' diff --git a/strcalc/src/main/frontend/main.js b/strcalc/src/main/frontend/main.js index d8abf40..39d9e5d 100644 --- a/strcalc/src/main/frontend/main.js +++ b/strcalc/src/main/frontend/main.js @@ -16,7 +16,7 @@ * @module main */ import App from './components/app.js' -import calculators from './components/calculators' +import calculators from './components/calculators.js' /** * Calls the app initializer with production parameters. diff --git a/strcalc/src/main/frontend/main.test.js b/strcalc/src/main/frontend/main.test.js index 7585a0c..f831d1e 100644 --- a/strcalc/src/main/frontend/main.test.js +++ b/strcalc/src/main/frontend/main.test.js @@ -4,16 +4,18 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { describe, afterEach, expect, test } from 'vitest' -import { PageLoader } from './test/page-loader.js' -import StringCalculatorPage from './test/page' +import { afterEach, beforeAll, describe, expect, test } from 'vitest' +import StringCalculatorPage from './test/page.js' +import TestPageOpener from 'test-page-opener' describe('String Calculator UI on initial page load', () => { - const loader = new PageLoader('/strcalc/') - afterEach(() => loader.closeAll()) + let opener + + beforeAll(async () => opener = await TestPageOpener.create('/strcalc/')) + afterEach(() => opener.closeAll()) test('contains the "Hello, World!" placeholder', async () => { - const { document } = await loader.load('index.html') + const { document } = await opener.open('index.html') const appElem = document.querySelector('#app') const e = new StringCalculatorPage(appElem, document).title() diff --git a/strcalc/src/main/frontend/package.json b/strcalc/src/main/frontend/package.json index b1ace10..e4ec739 100644 --- a/strcalc/src/main/frontend/package.json +++ b/strcalc/src/main/frontend/package.json @@ -44,6 +44,7 @@ "handlebars": "^4.7.8", "jsdoc-cli-wrapper": "^1.0.4", "jsdom": "^23.1.0", + "test-page-opener": "^1.0.3", "vite": "^5.0.11", "vitest": "^1.1.3", "webdriverio": "^8.27.0" diff --git a/strcalc/src/main/frontend/pnpm-lock.yaml b/strcalc/src/main/frontend/pnpm-lock.yaml index d8aaf82..e6321e1 100644 --- a/strcalc/src/main/frontend/pnpm-lock.yaml +++ b/strcalc/src/main/frontend/pnpm-lock.yaml @@ -41,6 +41,9 @@ devDependencies: jsdom: specifier: ^23.1.0 version: 23.1.0 + test-page-opener: + specifier: ^1.0.3 + version: 1.0.3 vite: specifier: ^5.0.11 version: 5.0.11 @@ -3546,6 +3549,13 @@ packages: minimatch: 3.1.2 dev: true + /test-page-opener@1.0.3: + resolution: {integrity: sha512-vHDT/nd39MhUS5ScCAZtdDKsbHN+e7954lIBeKYk/Ei7bBE6dwROPJs9b4tPHjmWjOcL6TT+6Pivhss2Oz7Jzg==} + engines: {node: '>= 18.0.0'} + dependencies: + istanbul-lib-coverage: 3.2.2 + dev: true + /text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true diff --git a/strcalc/src/main/frontend/test/page-loader.js b/strcalc/src/main/frontend/test/page-loader.js deleted file mode 100644 index 7b5d9f3..0000000 --- a/strcalc/src/main/frontend/test/page-loader.js +++ /dev/null @@ -1,346 +0,0 @@ -/* eslint-env browser, node */ -/* - * 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 - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -/** - * Exports test helper utilities for this project. - * @module test-helpers - */ - -/** - * Enables tests to load page URLs both in the browser and in Node using JSDom. - */ -export class PageLoader { - static #impl - - #basePath - #loaded - - constructor(basePath) { - if (!basePath.startsWith('/') || !basePath.endsWith('/')) { - const msg = 'basePath should start with \'/\' and ' + - 'end with \'/\', got: ' - throw new Error(`${msg}"${basePath}"`) - } - this.#basePath = basePath - this.#loaded = [] - } - - async load(pagePath) { - if (pagePath.startsWith('/')) { - const msg = 'page path should not start with \'/\', got: ' - throw new Error(`${msg}"${pagePath}"`) - } - - const impl = await PageLoader.getImpl() - const page = await impl.load(this.#basePath, pagePath) - - this.#loaded.push(page) - return page - } - - closeAll() { - this.#loaded.forEach(p => p.close()) - this.#loaded = [] - } - - static async getImpl() { - if (this.#impl) { - return this.#impl - } - - if (globalThis.window) { - return this.#impl = new BrowserPageLoader(globalThis.window) - } - - const {JSDOM} = await import('jsdom') - return this.#impl = new JsdomPageLoader(JSDOM, importModulesDynamically) - } -} - -class BrowserPageLoader { - #window - - constructor(window) { - this.#window = window - } - - // Loads a page and returns {window, document, close() } using the browser. - async load(basePath, pagePath) { - const w = this.#window.open(`${basePath}${pagePath}`) - return new Promise(resolve => { - const listener = () => { - this.#setCoverageStore(w) - resolve({window: w, document: w.document, close() {w.close()}}) - } - w.addEventListener('load', listener, {once: true}) - }) - } - - // This is an egregious, brittle hack that's very specific to Vitest's - // Istanbul coverage provider. It also only collects coverage from the last - // page loaded; it loses coverage information for all other pages. - // - // But as long as a test function calls BrowserPageLoader.load() only once, it - // should work pretty well. - #setCoverageStore(openedWindow) { - const COVERAGE_STORE_KEY = '__VITEST_COVERAGE__' - - if (COVERAGE_STORE_KEY in openedWindow) { - this.#window[COVERAGE_STORE_KEY] = openedWindow[COVERAGE_STORE_KEY] - } - } -} - -// Returns window and document objects from a JSDOM-parsed HTML file. -// -// It will execute