Skip to content

Commit 8620377

Browse files
chrisbbreuerclaude
andcommitted
fix: switch to happy-dom for full DOM API support in tests
Replace very-happy-dom with happy-dom which provides complete Range, Selection, Node, and other DOM APIs required by the rich text editor test suite. Fix navigator property access for test environment. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8ba40ae commit 8620377

File tree

4 files changed

+45
-93
lines changed

4 files changed

+45
-93
lines changed

bun.lock

Lines changed: 12 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

happy-dom.ts

Lines changed: 29 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,95 +1,37 @@
1-
import { GlobalWindow } from 'very-happy-dom'
1+
import { GlobalWindow } from 'happy-dom'
22

33
const win = new GlobalWindow({ url: 'http://localhost' })
44

5-
// Register core browser globals needed by the test environment
6-
const globals: Record<string, unknown> = {
7-
window: win,
8-
document: win.document,
9-
navigator: win.navigator,
10-
location: win.location,
11-
localStorage: win.localStorage,
12-
sessionStorage: win.sessionStorage,
13-
CustomEvent: win.CustomEvent,
14-
HTMLElement: win.HTMLElement,
15-
MutationObserver: win.MutationObserver,
16-
IntersectionObserver: win.IntersectionObserver,
17-
ResizeObserver: win.ResizeObserver,
18-
XMLHttpRequest: win.XMLHttpRequest,
19-
WebSocket: win.WebSocket,
20-
File: win.File,
21-
FileReader: win.FileReader,
22-
FileList: win.FileList,
23-
URL: win.URL,
24-
URLSearchParams: win.URLSearchParams,
25-
Headers: win.Headers,
26-
Request: win.Request,
27-
Response: win.Response,
28-
FormData: win.FormData,
29-
performance: win.performance,
30-
requestAnimationFrame: win.requestAnimationFrame.bind(win),
31-
cancelAnimationFrame: win.cancelAnimationFrame.bind(win),
32-
setTimeout: win.setTimeout.bind(win),
33-
clearTimeout: win.clearTimeout.bind(win),
34-
setInterval: win.setInterval.bind(win),
35-
clearInterval: win.clearInterval.bind(win),
36-
fetch: win.fetch,
37-
}
38-
39-
for (const [key, value] of Object.entries(globals)) {
40-
if (value !== undefined) {
41-
;(globalThis as any)[key] = value
5+
// Register all browser globals from the window instance
6+
for (const key of Object.getOwnPropertyNames(win)) {
7+
if (key === 'constructor' || key === 'undefined' || key === 'NaN' || key === 'Infinity') continue
8+
try {
9+
;(globalThis as any)[key] = (win as any)[key]
10+
}
11+
catch {
12+
// Some properties may not be configurable
4213
}
4314
}
4415

45-
// Stub Range and Selection APIs needed by rich text editor tests
46-
class MockRange {
47-
startContainer: any = null
48-
startOffset = 0
49-
endContainer: any = null
50-
endOffset = 0
51-
collapsed = true
52-
commonAncestorContainer: any = null
53-
54-
setStart(node: any, offset: number): void { this.startContainer = node; this.startOffset = offset; this.commonAncestorContainer = node }
55-
setEnd(node: any, offset: number): void { this.endContainer = node; this.endOffset = offset }
56-
collapse(toStart?: boolean): void { this.collapsed = true }
57-
cloneRange(): MockRange { return Object.assign(new MockRange(), this) }
58-
selectNode(node: any): void { this.startContainer = node; this.endContainer = node }
59-
selectNodeContents(node: any): void { this.startContainer = node; this.endContainer = node }
60-
deleteContents(): void {}
61-
insertNode(_node: any): void {}
62-
cloneContents(): any { return (win.document as any).createDocumentFragment?.() ?? {} }
63-
toString(): string { return '' }
64-
}
65-
66-
class MockSelection {
67-
rangeCount = 0
68-
private ranges: MockRange[] = []
69-
70-
getRangeAt(index: number): MockRange { return this.ranges[index] || new MockRange() }
71-
addRange(range: MockRange): void { this.ranges.push(range); this.rangeCount = this.ranges.length }
72-
removeAllRanges(): void { this.ranges = []; this.rangeCount = 0 }
73-
collapse(node: any, offset?: number): void {}
74-
toString(): string { return '' }
75-
get anchorNode(): any { return this.ranges[0]?.startContainer ?? null }
76-
get anchorOffset(): number { return this.ranges[0]?.startOffset ?? 0 }
77-
get focusNode(): any { return this.ranges[0]?.endContainer ?? null }
78-
get focusOffset(): number { return this.ranges[0]?.endOffset ?? 0 }
79-
get isCollapsed(): boolean { return this.ranges[0]?.collapsed ?? true }
16+
// Register prototype-level getters/methods
17+
for (const key of Object.getOwnPropertyNames(Object.getPrototypeOf(win))) {
18+
if (key === 'constructor' || key in globalThis) continue
19+
try {
20+
const descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(win), key)
21+
if (descriptor?.get) {
22+
Object.defineProperty(globalThis, key, {
23+
get: () => (win as any)[key],
24+
configurable: true,
25+
})
26+
}
27+
else if (typeof (win as any)[key] === 'function') {
28+
;(globalThis as any)[key] = (win as any)[key].bind(win)
29+
}
30+
}
31+
catch {
32+
// Skip non-configurable properties
33+
}
8034
}
8135

82-
const mockSelection = new MockSelection()
83-
const doc = (globalThis as any).document
84-
if (doc && !doc.createRange) {
85-
doc.createRange = () => new MockRange()
86-
}
87-
if (!(globalThis as any).window.getSelection) {
88-
;(globalThis as any).window.getSelection = () => mockSelection
89-
}
90-
if (!(globalThis as any).getSelection) {
91-
;(globalThis as any).getSelection = () => mockSelection
92-
}
93-
if (doc && !doc.getSelection) {
94-
doc.getSelection = () => mockSelection
95-
}
36+
// Ensure window is set
37+
;(globalThis as any).window = win

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@
6464
"@stacksjs/bunpress": "^0.1.4",
6565
"@types/rangy": "^1.3.0",
6666
"better-dx": "^0.2.7",
67-
"rangy": "^1.3.2",
68-
"very-happy-dom": "^0.0.9"
67+
"happy-dom": "^20.8.4",
68+
"rangy": "^1.3.2"
6969
},
7070
"git-hooks": {
7171
"pre-commit": {

test/helpers/test-utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,11 @@ export function setupTestHelpers(): TestHelpers {
6060

6161
// Browser detection utilities
6262
export function isIE9(): boolean {
63-
return navigator.appName.includes('Internet Explorer') && navigator.appVersion.includes('MSIE 9')
63+
return (navigator.appName || '').includes('Internet Explorer') && (navigator.appVersion || '').includes('MSIE 9')
6464
}
6565

6666
export function isIE10(): boolean {
67-
return navigator.appName.includes('Internet Explorer') && navigator.appVersion.includes('MSIE 10')
67+
return (navigator.appName || '').includes('Internet Explorer') && (navigator.appVersion || '').includes('MSIE 10')
6868
}
6969

7070
export function isOldIE(): boolean {

0 commit comments

Comments
 (0)