test(codemirror): fire selectionchange after the selection moves, as browsers do

@codemirror/view 6.43.14 makes DOMObserver.flush() call view.update([])
whenever it saw a selection change. happy-dom dispatches selectionchange
synchronously from inside Selection.collapse(), which CodeMirror calls
while writing its selection during an update, so all ten
hosted_completion specs threw "Calls to EditorView.update are not
allowed while an update is in progress".

The package had no test setup, so it did not get the deferral added to
apps/client/src/test/setup.ts. Add the same setup here, kept out of the
library build and the coverage count.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UQQd11a32NYctkABZQY3dM
This commit is contained in:
Claude
2026-10-11 10:49:03 +00:00
parent 9add68c15b
commit fcd6d86d12
3 changed files with 28 additions and 2 deletions
+24
View File
@@ -0,0 +1,24 @@
deferSelectionChange();
/**
* Fires `selectionchange` after the code that moved the selection, as browsers do.
*
* happy-dom dispatches it from inside `Selection.collapse()`. CodeMirror moves the DOM selection
* during `EditorView.update()`, and its listener updates the view again, which throws
* "Calls to EditorView.update are not allowed while an update is in progress".
*/
function deferSelectionChange() {
const dispatch = document.dispatchEvent.bind(document);
// happy-dom's `dispatchEvent()` calls itself again for each event phase.
const deferred = new WeakSet<Event>();
document.dispatchEvent = (event: Event) => {
if (event.type !== "selectionchange" || deferred.has(event)) {
return dispatch(event);
}
deferred.add(event);
queueMicrotask(() => dispatch(event));
return true;
};
}
+2 -1
View File
@@ -36,6 +36,7 @@
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx"
"src/**/*.spec.jsx",
"src/test/**"
]
}
+2 -1
View File
@@ -31,6 +31,7 @@ export default defineConfig(() => ({
watch: false,
globals: true,
environment: 'happy-dom',
setupFiles: ['./src/test/setup.ts'],
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: [
'default',
@@ -45,7 +46,7 @@ export default defineConfig(() => ({
},
provider: 'v8',
include: ['src/**/*.{ts,tsx}'],
exclude: ['**/*.{test,spec}.{ts,mts,cts,tsx,js,jsx}', '**/*.d.ts'],
exclude: ['**/*.{test,spec}.{ts,mts,cts,tsx,js,jsx}', '**/*.d.ts', 'src/test/**'],
// Codecov resolves an lcov `SF:` path by matching it against the repo's file list, so the
// package-relative paths istanbul emits by default (`src/index.ts`, relative to cwd) are
// ambiguous in this monorepo and get attributed to whichever package wins the match. Emit