Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions config/scripts/happy-dom-offscreen-canvas.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/** @vitest-environment happy-dom */
import { Terminal } from '@xterm/xterm'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { installHappyDomOffscreenCanvasCompatibility } from './happy-dom-offscreen-canvas'

type TestOffscreenCanvas = new (
width: number,
height: number
) => {
getContext: (contextType: string, contextAttributes?: unknown) => unknown
}

const openTerminals: Terminal[] = []

describe('happy-dom OffscreenCanvas compatibility', () => {
afterEach(() => {
while (openTerminals.length > 0) {
openTerminals.pop()?.dispose()
}
vi.restoreAllMocks()
document.body.replaceChildren()
})

it('delegates adapter-less 2D contexts to the existing HTML canvas double', () => {
const offscreenCanvas = (globalThis as { OffscreenCanvas?: TestOffscreenCanvas })
.OffscreenCanvas
if (!offscreenCanvas) {
expect(installHappyDomOffscreenCanvasCompatibility()).toBe(false)
return
}

const context = { measureText: () => ({ width: 10 }) }
const getContext = vi
.spyOn(HTMLCanvasElement.prototype, 'getContext')
.mockReturnValue(context as unknown as CanvasRenderingContext2D)

expect(installHappyDomOffscreenCanvasCompatibility()).toBe(true)
expect(new offscreenCanvas(1, 1).getContext('2d')).toBe(context)
expect(getContext).toHaveBeenCalled()
})

it('does not turn unsupported non-2D contexts into test doubles', () => {
const offscreenCanvas = (globalThis as { OffscreenCanvas?: TestOffscreenCanvas })
.OffscreenCanvas
if (!offscreenCanvas) {
return
}

const getContext = vi.spyOn(HTMLCanvasElement.prototype, 'getContext')
expect(new offscreenCanvas(1, 1).getContext('webgl')).toBeNull()
expect(getContext).not.toHaveBeenCalled()
})

it('keeps xterm DOM rendering open with the existing HTML canvas double', () => {
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue({
measureText: () => ({ width: 10 })
} as unknown as CanvasRenderingContext2D)
const host = document.createElement('div')
document.body.append(host)
const terminal = new Terminal()
openTerminals.push(terminal)

terminal.open(host)

expect(terminal.element).not.toBeNull()
})
})
59 changes: 59 additions & 0 deletions config/scripts/happy-dom-offscreen-canvas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Why: happy-dom exposes OffscreenCanvas without a default canvas adapter. xterm prefers that API
// and then cannot open in tests that already provide the HTML canvas text-metric stand-in.

const OFFSCREEN_CANVAS_COMPATIBILITY_INSTALLED = Symbol.for(
'orca.happyDomOffscreenCanvasCompatibility'
)

type PatchableCanvas = {
getContext?: (contextType: string, contextAttributes?: unknown) => unknown
} & Record<symbol, unknown>

type CanvasConstructor = {
prototype: PatchableCanvas
new (width: number, height: number): PatchableCanvas
}

type HappyDomGlobals = {
OffscreenCanvas?: CanvasConstructor
HTMLCanvasElement?: { prototype: PatchableCanvas }
document?: Pick<Document, 'createElement'>
}

/** Make happy-dom's adapter-less OffscreenCanvas honor the existing HTML canvas test double. */
export function installHappyDomOffscreenCanvasCompatibility(): boolean {
const globals = globalThis as unknown as HappyDomGlobals
const offscreenCanvas = globals.OffscreenCanvas
const htmlCanvas = globals.HTMLCanvasElement
const document = globals.document
if (!offscreenCanvas || !htmlCanvas || !document) {
return false
}

const prototype = offscreenCanvas.prototype
if (prototype[OFFSCREEN_CANVAS_COMPATIBILITY_INSTALLED] === true) {
return true
}
const originalGetContext = prototype.getContext
if (!originalGetContext || !htmlCanvas.prototype.getContext) {
return false
}

prototype.getContext = function patchedGetContext(
this: PatchableCanvas,
contextType: string,
contextAttributes?: unknown
): unknown {
const context = originalGetContext.call(this, contextType, contextAttributes)
if (contextType !== '2d' || context) {
return context
}

const canvas = document.createElement('canvas') as PatchableCanvas
return htmlCanvas.prototype.getContext!.call(canvas, contextType, contextAttributes)
}
prototype[OFFSCREEN_CANVAS_COMPATIBILITY_INSTALLED] = true
return true
}

installHappyDomOffscreenCanvasCompatibility()
1 change: 1 addition & 0 deletions config/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export default defineConfig({
execArgv: ['--no-experimental-webstorage', '--expose-gc'],
// Why: happy-dom drops MutationObserver callbacks on GC; keep them alive like a browser does.
setupFiles: [
resolve('config/scripts/happy-dom-offscreen-canvas.ts'),
resolve('config/scripts/happy-dom-mutation-observer-retention.ts'),
resolve('config/scripts/vitest-host-ports-setup.ts')
],
Expand Down
Loading