-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add ESLint, Vitest, JSDOM+ESM workaround, JS test
Includes a workaround for JSDOM's current lack of <script type="module"> support, specifically when loading a HTML file: - jsdom/jsdom#2475 The importModules() helper method uses dynamic import() to apply JavaScript modules after JSDOM has parsed, but not executed, <script type="module"> elements. The comments for importModules() and loadFromFile(), both currently in main.test.js, contain further explanation. (I may eventually extract these into a helper module, post them to the aforementioned issue, write a blog post about them, etc.) --- This problem arose because Vite depends on browser support for JavaScript modules (a.k.a. ES modules): - https://vitejs.dev/guide/build.html#browser-compatibility - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules - https://nodejs.org/dist/latest-v20.x/docs/api/esm.html The Vitest "Hello, World!" test currently uses JSDOM to emulate a browser (I'll add Vitest experimental browser support shortly): - https://github.com/jsdom/jsdom However, it took me a few hours to figure out that JSDOM parses, but doesn't execute, `<script type="module">`, which is essential to how Vite works: - https://vitejs.dev/guide/#index-html-and-project-root My original solution was to also include a `<script nomodule>` element to load the same file, but that wasn't ideal. It also broke down when I split `initApp` into init.js to force main.js to declare an `import` statement. The importModules() solution is far more robust. Given the constraints with regard to event listeners mentioned in its function comment, it provides a seamless solution. As described by the loadFromFile() comment, when JSDOM supports <script type="module">, removal of importModules() should prove straightforward.
- Loading branch information
Showing
11 changed files
with
1,939 additions
and
48 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,3 +14,4 @@ htmlReport | |
strcalc/src/main/webapp | ||
|
||
node_modules | ||
coverage |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
**/vendor/*.js | ||
coverage/* | ||
tmp/ | ||
node_modules/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
{ "extends": "eslint:recommended", | ||
"env" : { | ||
"browser" : true, | ||
"node": true, | ||
"es2023" : true | ||
}, | ||
"parserOptions": { | ||
"ecmaVersion": "latest", | ||
"sourceType": "module" | ||
}, | ||
"plugins": [ "@stylistic/js", "vitest" ], | ||
"overrides": [ | ||
{ | ||
"files": ["**/*.test.js"], | ||
"plugins": ["vitest"], | ||
"extends": ["plugin:vitest/recommended"] | ||
} | ||
], | ||
"rules" : { | ||
"@stylistic/js/comma-dangle": [ | ||
"error", "never" | ||
], | ||
"@stylistic/js/indent": [ | ||
"error", 2, { "VariableDeclarator": 2 } | ||
], | ||
"@stylistic/js/keyword-spacing": [ | ||
"error" | ||
], | ||
"@stylistic/js/max-len": [ | ||
"error", 80, 2 | ||
], | ||
"@stylistic/js/quotes": [ | ||
"error", "single" | ||
], | ||
"@stylistic/js/semi": [ | ||
"error", "never" | ||
], | ||
"camelcase": [ | ||
"error", { "properties": "always" } | ||
], | ||
"no-console": [ | ||
"error", { "allow": [ "warn", "error" ]} | ||
] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
/* eslint-env browser */ | ||
|
||
export default function initApp(document) { | ||
document.querySelector('#app').innerHTML = ` | ||
<p class="placeholder">Hello, World!</p> | ||
` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
import './style.css' | ||
/* eslint-env browser */ | ||
|
||
document.querySelector('#app').innerHTML = ` | ||
<p class="placeholder">Hello, World!</p> | ||
` | ||
import initApp from './init.js' | ||
|
||
(() => initApp(document))() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
/* eslint-env browser, node, jest, vitest */ | ||
'use strict' | ||
import { describe, expect, test } from 'vitest' | ||
import { JSDOM } from 'jsdom' | ||
|
||
// Returns window and document objects from a JSDOM-parsed HTML file. | ||
// | ||
// It will execute <script type="module"> elements with a `src` attribute, | ||
// but not those with inline code. See the comment for importModules(). | ||
// | ||
// Based on hints from: | ||
// - https://oliverjam.es/articles/frontend-testing-node-jsdom | ||
let loadFromFile = async (filePath) => { | ||
let dom = await JSDOM.fromFile( | ||
filePath, { resources: 'usable', runScripts: 'dangerously' } | ||
) | ||
|
||
// Once importModules() goes away, wrap the return value in a Promise that | ||
// resolves via dom.window.addEventListener('load', ...). | ||
await importModules(dom) | ||
return { window: dom.window, document: dom.window.document } | ||
} | ||
|
||
// Imports <script type="module"> elements parsed, but not executed, by JSDOM. | ||
// | ||
// Only works with scripts with a `src` attribute; it will not execute inline | ||
// code. | ||
// | ||
// Remove this function once "jsdom/jsdom: <script type=module> support #2475" | ||
// has been resolved: | ||
// - https://github.com/jsdom/jsdom/issues/2475 | ||
// | ||
// Note on timing of script execution | ||
// ---------------------------------- | ||
// By the time the dynamic import() calls registered by importModules() begin | ||
// executing, the window's 'DOMContentLoaded' and 'load' events will have | ||
// already fired. Technically, the imported modules should execute similarly | ||
// to <script defer> and execute before 'DOMContentLoaded'. As a result, we | ||
// can't register handlers for these events in our module code. We can add these | ||
// handlers in inline <script>s, but those can't reference module code and | ||
// expect JSDOM tests to work at the moment. | ||
// | ||
// All that said, these should prove to be corner cases easily avoided by sound, | ||
// modular app architecture. | ||
let importModules = async (dom) => { | ||
let modules = Array.from( | ||
dom.window.document.querySelectorAll('script[type="module"]') | ||
) | ||
|
||
// The JSDOM docs advise against setting global properties, but we don't | ||
// have another option given the module may access window and/or document. | ||
global.window = dom.window | ||
global.document = dom.window.document | ||
await Promise.all(modules.map(s => import(s.src))) | ||
global.window = global.document = undefined | ||
} | ||
|
||
describe('String Calculator UI', () => { | ||
describe('initial state', () => { | ||
test('contains the "Hello, World!" placeholder', async () => { | ||
let { document } = await loadFromFile('./index.html') | ||
|
||
let e = document.querySelector('#app .placeholder') | ||
|
||
expect(e.textContent).toContain('Hello, World!') | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,26 @@ | ||
{ | ||
"name": "frontend", | ||
"name": "strcalc-frontend", | ||
"private": true, | ||
"version": "0.0.0", | ||
"type": "module", | ||
"scripts": { | ||
"dev": "vite", | ||
"build": "vite build", | ||
"preview": "vite preview" | ||
"build": "vite build --emptyOutDir", | ||
"preview": "vite preview", | ||
"lint": "eslint --color --max-warnings 0 .", | ||
"test": "vitest", | ||
"test:run": "vitest --run", | ||
"test:ui": "vitest --ui", | ||
"coverage": "vitest run --coverage" | ||
}, | ||
"devDependencies": { | ||
"vite": "^4.4.5" | ||
"@stylistic/eslint-plugin-js": "^1.0.1", | ||
"@vitest/coverage-v8": "^0.34.6", | ||
"@vitest/ui": "^0.34.6", | ||
"eslint": "^8.53.0", | ||
"eslint-plugin-vitest": "^0.3.9", | ||
"jsdom": "^22.1.0", | ||
"vite": "^4.4.5", | ||
"vitest": "^0.34.6" | ||
} | ||
} |
Oops, something went wrong.