Skip to content

Commit 7c33c80

Browse files
committed
Fix webComponent.spec.ts to follow correct model pattern: use Astro ContainerAPI, use happy-dom
1 parent d73ced7 commit 7c33c80

2 files changed

Lines changed: 77 additions & 168 deletions

File tree

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

Lines changed: 77 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,102 @@
11
// @vitest-environment node
2-
import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'vitest'
2+
import { beforeEach, describe, expect, test } from 'vitest'
33
import { experimental_AstroContainer as AstroContainer } from 'astro/container'
4-
import { GlobalRegistrator } from '@happy-dom/global-registrator'
4+
import { Window } from 'happy-dom'
5+
import TestWebComponentAstro from '@components/Test/webComponent.astro'
56
import type { TestWebComponent as TestWebComponentInstance } from '@components/Test/webComponent'
6-
import { withLitRuntime } from '@test/unit/helpers/litRuntime'
77

8-
type TestWebComponentModule = typeof import('@components/Test/webComponent')
9-
type TestWebComponentAstroModule = typeof import('@components/Test/webComponent.astro')
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+
}
1050

11-
let TestWebComponentAstro: TestWebComponentAstroModule['default']
12-
let TestWebComponentCtor: TestWebComponentModule['TestWebComponent']
13-
let registerTestWebComponent: TestWebComponentModule['registerTestWebComponent']
14-
15-
beforeAll(async () => {
16-
GlobalRegistrator.register()
17-
const [astroComponent, componentModule] = await Promise.all([
18-
import('@components/Test/webComponent.astro'),
19-
import('@components/Test/webComponent'),
20-
])
21-
22-
TestWebComponentAstro = astroComponent.default
23-
TestWebComponentCtor = componentModule.TestWebComponent
24-
registerTestWebComponent = componentModule.registerTestWebComponent
25-
})
26-
27-
afterAll(async () => {
28-
await GlobalRegistrator.unregister()
29-
})
30-
31-
describe('Test web component rendering via Astro Container API', () => {
51+
describe('TestWebComponent class behavior', () => {
3252
let container: AstroContainer
3353

3454
beforeEach(async () => {
3555
container = await AstroContainer.create()
3656
})
3757

38-
test('container output references compiled client bundle instead of inlining code', async () => {
39-
const result = await container.renderToString(TestWebComponentAstro, {
40-
props: {
41-
heading: 'Test',
42-
},
43-
})
58+
const renderInHappyDom = async (
59+
assertion: (_context: { element: TestWebComponentInstance }) => Promise<void> | void,
60+
): Promise<void> => {
61+
await withHappyDomEnvironment(async ({ window }) => {
62+
const { TestWebComponent: TestWebComponentCtor, registerTestWebComponent } = await import(
63+
'@components/Test/webComponent'
64+
)
4465

45-
expect(result).toMatchInlineSnapshot(
46-
`"<test-web-component class=\"test-web-component\" data-testid=\"test-web-component\" data-astro-cid-7wyxbui3=\"true\"> <h2 data-astro-cid-7wyxbui3>Test</h2> </test-web-component> <script type=\"module\" src=\"/home/kevin/Repos/Webstack Builders/Corporate Website/astro.webstackbuilders.com/src/components/Test/webComponent.astro?astro&type=script&index=0&lang.ts\"></script>"`,
47-
)
66+
const renderedHtml: RenderResult = await container.renderToString(TestWebComponentAstro, {
67+
props: {
68+
heading: 'Integration test',
69+
},
70+
})
4871

49-
expect(result).toContain('<test-web-component')
50-
})
51-
})
72+
window.document.body.innerHTML = renderedHtml
73+
window.document.querySelectorAll('script').forEach((script) => script.remove())
5274

53-
describe('TestWebComponent class behavior', () => {
54-
const runtimeTagName = 'test-web-component-spec'
75+
registerTestWebComponent()
5576

56-
test('renders default message in light DOM', async () => {
57-
await withLitRuntime(async ({ register, mount }) => {
58-
await register(runtimeTagName, (tagName) => registerTestWebComponent(tagName))
59-
expect(customElements.get(runtimeTagName)).toBe(TestWebComponentCtor)
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+
}
6084

61-
const element = await mount<TestWebComponentInstance>(runtimeTagName)
85+
expect(window.customElements.get('test-web-component')).toBe(TestWebComponentCtor)
6286

87+
await element.updateComplete
88+
await assertion({ element })
89+
})
90+
}
91+
92+
test('renders default message in light DOM', async () => {
93+
await renderInHappyDom(async ({ element }) => {
6394
expect(element.querySelector('#message')?.textContent).toBe('Hello from Lit')
6495
})
6596
})
6697

6798
test('updates DOM when message changes', async () => {
68-
await withLitRuntime(async ({ register, mount }) => {
69-
await register(runtimeTagName, (tagName) => registerTestWebComponent(tagName))
70-
expect(customElements.get(runtimeTagName)).toBe(TestWebComponentCtor)
71-
72-
const element = await mount<TestWebComponentInstance>(runtimeTagName)
73-
99+
await renderInHappyDom(async ({ element }) => {
74100
element.message = 'Updated message'
75101
await element.updateComplete
76102

‎test/unit/helpers/litRuntime.ts‎

Lines changed: 0 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,117 +0,0 @@
1-
import { isBodyElement } from '@components/scripts/assertions/elements'
2-
import { BuildError } from '@lib/errors/BuildError'
3-
4-
type RegisterCallback = (_tagName: string) => void | Promise<void>
5-
6-
type PropertyDictionary = Record<string, unknown>
7-
8-
type AttributeDictionary = Record<string, string>
9-
10-
interface MountOptions {
11-
attributes?: AttributeDictionary
12-
properties?: PropertyDictionary
13-
}
14-
15-
interface LitRuntimeContext {
16-
register: (_tagName: string, _registerCallback: RegisterCallback) => Promise<void>
17-
mount: <TElement extends HTMLElement>(_tagName: string, _options?: MountOptions) => Promise<TElement>
18-
cleanup: () => void
19-
}
20-
21-
const ensureDocumentBody = (): HTMLBodyElement => {
22-
if (!document.documentElement) {
23-
const htmlElement = document.createElement('html')
24-
const headElement = document.createElement('head')
25-
const bodyElement = document.createElement('body')
26-
htmlElement.append(headElement, bodyElement)
27-
document.appendChild(htmlElement)
28-
return bodyElement
29-
}
30-
31-
if (!document.body) {
32-
const bodyElement = document.createElement('body')
33-
document.documentElement.appendChild(bodyElement)
34-
return bodyElement
35-
}
36-
37-
if (isBodyElement(document.body)) {
38-
return document.body
39-
}
40-
41-
throw BuildError.fileOperation('Unable to create document body for Lit runtime tests')
42-
}
43-
44-
const waitForUpdateComplete = async (element: HTMLElement): Promise<void> => {
45-
const maybeUpdate = (element as HTMLElement & { updateComplete?: Promise<unknown> }).updateComplete
46-
if (maybeUpdate) {
47-
await maybeUpdate
48-
return
49-
}
50-
51-
await Promise.resolve()
52-
}
53-
54-
const applyAttributes = (element: HTMLElement, attributes: AttributeDictionary = {}): void => {
55-
Object.entries(attributes).forEach(([name, value]) => {
56-
element.setAttribute(name, value)
57-
})
58-
}
59-
60-
const applyProperties = (element: HTMLElement, properties: PropertyDictionary = {}): void => {
61-
Object.entries(properties).forEach(([name, value]) => {
62-
;(element as unknown as PropertyDictionary)[name] = value
63-
})
64-
}
65-
66-
/**
67-
* Provides a scoped Lit runtime harness for hydrating custom elements in unit tests.
68-
*
69-
* @param callback Callback that receives Lit runtime helpers for registering and mounting elements.
70-
*/
71-
export const withLitRuntime = async <TReturn>(
72-
callback: (_context: LitRuntimeContext) => Promise<TReturn> | TReturn,
73-
): Promise<TReturn> => {
74-
const cleanup = () => {
75-
if (document.body) {
76-
document.body.replaceChildren()
77-
}
78-
}
79-
80-
const register = async (tagName: string, registerCallback: RegisterCallback) => {
81-
if (!customElements.get(tagName)) {
82-
await registerCallback(tagName)
83-
}
84-
}
85-
86-
const mount = async <TElement extends HTMLElement>(
87-
tagName: string,
88-
options?: MountOptions,
89-
): Promise<TElement> => {
90-
const body = ensureDocumentBody()
91-
const element = document.createElement(tagName) as TElement
92-
93-
if (options?.attributes) {
94-
applyAttributes(element, options.attributes)
95-
}
96-
97-
if (options?.properties) {
98-
applyProperties(element, options.properties)
99-
}
100-
101-
body.appendChild(element)
102-
await waitForUpdateComplete(element)
103-
return element
104-
}
105-
106-
cleanup()
107-
108-
try {
109-
return await callback({
110-
register,
111-
mount,
112-
cleanup,
113-
})
114-
} finally {
115-
cleanup()
116-
}
117-
}

0 commit comments

Comments
 (0)