Skip to content

Commit 0710e73

Browse files
committed
Committing working model test case before refactor
1 parent 7c33c80 commit 0710e73

2 files changed

Lines changed: 150 additions & 74 deletions

File tree

‎src/components/Test/__tests__/webComponent.spec.ts‎

Lines changed: 37 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,14 @@
11
// @vitest-environment node
22
import { beforeEach, describe, expect, test } from 'vitest'
33
import { experimental_AstroContainer as AstroContainer } from 'astro/container'
4-
import { Window } from 'happy-dom'
54
import TestWebComponentAstro from '@components/Test/webComponent.astro'
65
import type { TestWebComponent as TestWebComponentInstance } from '@components/Test/webComponent'
6+
import {
7+
renderInHappyDom,
8+
type RenderResult,
9+
} from '@test/unit/helpers/litRuntime'
710

8-
type RenderResult = Awaited<ReturnType<AstroContainer['renderToString']>>
9-
10-
type HappyDomGlobalKey = 'window' | 'document' | 'customElements' | 'HTMLElement'
11-
type HappyDomGlobals = Partial<Record<HappyDomGlobalKey, unknown>>
12-
13-
const setGlobalValue = (key: HappyDomGlobalKey, value: unknown) => {
14-
if (value === undefined) {
15-
delete (globalThis as Record<string, unknown>)[key]
16-
return
17-
}
18-
19-
;(globalThis as Record<string, unknown>)[key] = value
20-
}
21-
22-
const withHappyDomEnvironment = async <TReturn>(
23-
callback: (_context: { window: Window }) => Promise<TReturn> | TReturn,
24-
): Promise<TReturn> => {
25-
const window = new Window()
26-
const globalObject = globalThis as Record<string, unknown>
27-
const previousGlobals: HappyDomGlobals = {
28-
window: globalObject['window'],
29-
document: globalObject['document'],
30-
customElements: globalObject['customElements'],
31-
HTMLElement: globalObject['HTMLElement'],
32-
}
33-
34-
setGlobalValue('window', window)
35-
setGlobalValue('document', window.document)
36-
setGlobalValue('customElements', window.customElements)
37-
setGlobalValue('HTMLElement', window.HTMLElement)
38-
39-
try {
40-
return await callback({ window })
41-
} finally {
42-
setGlobalValue('window', previousGlobals.window)
43-
setGlobalValue('document', previousGlobals.document)
44-
setGlobalValue('customElements', previousGlobals.customElements)
45-
setGlobalValue('HTMLElement', previousGlobals.HTMLElement)
46-
await window.happyDOM?.whenAsyncComplete?.()
47-
await window.happyDOM?.close?.()
48-
}
49-
}
11+
type TestComponentModule = typeof import('@components/Test/webComponent')
5012

5113
describe('TestWebComponent class behavior', () => {
5214
let container: AstroContainer
@@ -55,48 +17,49 @@ describe('TestWebComponent class behavior', () => {
5517
container = await AstroContainer.create()
5618
})
5719

58-
const renderInHappyDom = async (
20+
const componentSelector = 'test-web-component'
21+
const renderArgs = {
22+
props: {
23+
heading: 'Integration test',
24+
},
25+
}
26+
27+
const executeRender = async (
5928
assertion: (_context: { element: TestWebComponentInstance }) => Promise<void> | void,
6029
): Promise<void> => {
61-
await withHappyDomEnvironment(async ({ window }) => {
62-
const { TestWebComponent: TestWebComponentCtor, registerTestWebComponent } = await import(
63-
'@components/Test/webComponent'
64-
)
65-
66-
const renderedHtml: RenderResult = await container.renderToString(TestWebComponentAstro, {
67-
props: {
68-
heading: 'Integration test',
69-
},
70-
})
71-
72-
window.document.body.innerHTML = renderedHtml
73-
window.document.querySelectorAll('script').forEach((script) => script.remove())
74-
75-
registerTestWebComponent()
76-
77-
const element = window.document.querySelector('test-web-component') as
78-
| TestWebComponentInstance
79-
| null
80-
81-
if (!element) {
82-
throw new Error('Unable to locate rendered <test-web-component> instance in document')
83-
}
84-
85-
expect(window.customElements.get('test-web-component')).toBe(TestWebComponentCtor)
86-
87-
await element.updateComplete
88-
await assertion({ element })
30+
await renderInHappyDom<TestWebComponentInstance, TestComponentModule>({
31+
container,
32+
component: TestWebComponentAstro,
33+
args: renderArgs,
34+
selector: componentSelector,
35+
hydrate: async () => {
36+
const module = await import('@components/Test/webComponent')
37+
module.registerTestWebComponent()
38+
return module
39+
},
40+
waitForReady: async (element) => {
41+
await element.updateComplete
42+
},
43+
assert: async ({ element, window, hydrateResult, renderResult }) => {
44+
expect(window.customElements.get(componentSelector)).toBe(
45+
hydrateResult.TestWebComponent,
46+
)
47+
const hydratedMarkup: RenderResult = renderResult
48+
expect(hydratedMarkup).toContain(`<${componentSelector}`)
49+
50+
await assertion({ element })
51+
},
8952
})
9053
}
9154

9255
test('renders default message in light DOM', async () => {
93-
await renderInHappyDom(async ({ element }) => {
56+
await executeRender(async ({ element }) => {
9457
expect(element.querySelector('#message')?.textContent).toBe('Hello from Lit')
9558
})
9659
})
9760

9861
test('updates DOM when message changes', async () => {
99-
await renderInHappyDom(async ({ element }) => {
62+
await executeRender(async ({ element }) => {
10063
element.message = 'Updated message'
10164
await element.updateComplete
10265

‎test/unit/helpers/litRuntime.ts‎

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { Window } from 'happy-dom'
2+
import type { experimental_AstroContainer as AstroContainer } from 'astro/container'
3+
import { BuildError } from '@lib/errors/BuildError'
4+
5+
type HappyDomGlobalKey = 'window' | 'document' | 'customElements' | 'HTMLElement'
6+
type HappyDomGlobals = Partial<Record<HappyDomGlobalKey, unknown>>
7+
8+
export type AstroComponentInstance = Parameters<AstroContainer['renderToString']>[0]
9+
export type AstroRenderArgs = Parameters<AstroContainer['renderToString']>[1]
10+
export type RenderResult = Awaited<ReturnType<AstroContainer['renderToString']>>
11+
12+
export interface HappyDomEnvironmentContext {
13+
window: Window
14+
}
15+
16+
export type HappyDomEnvironmentCallback<TReturn> = (
17+
_context: HappyDomEnvironmentContext,
18+
) => Promise<TReturn> | TReturn
19+
20+
export const setGlobalValue = (key: HappyDomGlobalKey, value: unknown): void => {
21+
if (value === undefined) {
22+
delete (globalThis as Record<string, unknown>)[key]
23+
return
24+
}
25+
26+
;(globalThis as Record<string, unknown>)[key] = value
27+
}
28+
29+
const captureGlobalSnapshot = (): HappyDomGlobals => {
30+
const globalObject = globalThis as Record<string, unknown>
31+
return {
32+
window: globalObject['window'],
33+
document: globalObject['document'],
34+
customElements: globalObject['customElements'],
35+
HTMLElement: globalObject['HTMLElement'],
36+
}
37+
}
38+
39+
const installHappyDomGlobals = (window: Window): void => {
40+
setGlobalValue('window', window)
41+
setGlobalValue('document', window.document)
42+
setGlobalValue('customElements', window.customElements)
43+
setGlobalValue('HTMLElement', window.HTMLElement)
44+
}
45+
46+
const restoreGlobalSnapshot = (snapshot: HappyDomGlobals): void => {
47+
setGlobalValue('window', snapshot.window)
48+
setGlobalValue('document', snapshot.document)
49+
setGlobalValue('customElements', snapshot.customElements)
50+
setGlobalValue('HTMLElement', snapshot.HTMLElement)
51+
}
52+
53+
/**
54+
* Provides a scoped Happy DOM environment for DOM-centric tests.
55+
*/
56+
export const withHappyDomEnvironment = async <TReturn>(
57+
callback: HappyDomEnvironmentCallback<TReturn>,
58+
): Promise<TReturn> => {
59+
const window = new Window()
60+
const snapshot = captureGlobalSnapshot()
61+
62+
installHappyDomGlobals(window)
63+
64+
try {
65+
return await callback({ window })
66+
} finally {
67+
restoreGlobalSnapshot(snapshot)
68+
await window.happyDOM?.whenAsyncComplete?.()
69+
await window.happyDOM?.close?.()
70+
}
71+
}
72+
73+
interface RenderInHappyDomOptions<TElement, THydrateResult> {
74+
container: AstroContainer
75+
component: AstroComponentInstance
76+
args?: AstroRenderArgs
77+
selector: string
78+
hydrate?: () => Promise<THydrateResult> | THydrateResult
79+
waitForReady?: (_element: TElement) => Promise<void> | void
80+
assert: (_context: {
81+
element: TElement
82+
window: Window
83+
hydrateResult: THydrateResult
84+
renderResult: RenderResult
85+
}) => Promise<void> | void
86+
}
87+
88+
const defaultWaitForReady = async () => {}
89+
90+
export const renderInHappyDom = async <TElement, THydrateResult = void>(
91+
options: RenderInHappyDomOptions<TElement, THydrateResult>,
92+
): Promise<void> => {
93+
const { container, component, args, selector, hydrate, waitForReady = defaultWaitForReady, assert } = options
94+
95+
await withHappyDomEnvironment(async ({ window }) => {
96+
const renderedHtml: RenderResult = await container.renderToString(component, args)
97+
98+
window.document.body.innerHTML = renderedHtml
99+
window.document.querySelectorAll('script').forEach((script) => script.remove())
100+
101+
const hydrateResult = (await hydrate?.()) as THydrateResult
102+
103+
const element = window.document.querySelector(selector) as TElement | null
104+
if (!element) {
105+
throw BuildError.fileOperation(
106+
`Unable to locate rendered element for selector "${selector}" during Happy DOM rendering`,
107+
)
108+
}
109+
110+
await waitForReady(element)
111+
await assert({ element, window, hydrateResult, renderResult: renderedHtml })
112+
})
113+
}

0 commit comments

Comments
 (0)