Skip to content

Commit 3ca9e94

Browse files
author
dasathyakuma
committed
fix tests
1 parent fa3e607 commit 3ca9e94

File tree

7 files changed

+1010
-17
lines changed

7 files changed

+1010
-17
lines changed

packages/marko-virtual/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@
6060
"@tanstack/virtual-core": "workspace:*"
6161
},
6262
"devDependencies": {
63+
"@marko/testing-library": "^1.1.0",
64+
"@marko/vite": "^3.0.0",
65+
"@testing-library/dom": "^10.0.0",
6366
"marko": ">=6.0.0"
6467
},
6568
"peerDependencies": {
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Test fixture for <virtualizer> tag.
2+
// Not a route file — no <!DOCTYPE html> needed.
3+
// No <if=mounted> guard needed since template.mount() runs client-side directly.
4+
5+
export interface Input {
6+
count?: number
7+
itemHeight?: number
8+
horizontal?: boolean
9+
lanes?: number
10+
}
11+
12+
<div/scrollEl style="height: 400px; width: 400px; overflow: auto">
13+
<virtualizer|{ virtualItems, totalSize }|
14+
count=input.count ?? 100
15+
estimateSize=() => input.itemHeight ?? 50
16+
horizontal=input.horizontal ?? false
17+
lanes=input.lanes ?? 1
18+
getScrollElement=scrollEl
19+
>
20+
<div
21+
data-testid="virtual-wrapper"
22+
style=`${input.horizontal ? "width" : "height"}: ${totalSize}px; position: relative`
23+
>
24+
<for|item| of=virtualItems>
25+
<div
26+
data-testid="virtual-item"
27+
data-index=item.index
28+
style=`position: absolute; ${input.horizontal ? "width" : "height"}: ${item.size}px; transform: translate${input.horizontal ? "X" : "Y"}(${item.start}px)`
29+
>
30+
Row ${item.index}
31+
</div>
32+
</for>
33+
</div>
34+
</virtualizer>
35+
</div>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Test fixture for <window-virtualizer> tag.
2+
3+
export interface Input {
4+
count?: number
5+
itemHeight?: number
6+
}
7+
8+
<window-virtualizer|{ virtualItems, totalSize }|
9+
count=input.count ?? 100
10+
estimateSize=() => input.itemHeight ?? 50
11+
>
12+
<div
13+
data-testid="virtual-wrapper"
14+
style=`height: ${totalSize}px; position: relative`
15+
>
16+
<for|item| of=virtualItems>
17+
<div
18+
data-testid="virtual-item"
19+
data-index=item.index
20+
style=`position: absolute; height: ${item.size}px; transform: translateY(${item.start}px)`
21+
>
22+
Row ${item.index}
23+
</div>
24+
</for>
25+
</div>
26+
</window-virtualizer>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { vi } from "vitest"
2+
3+
// ---------------------------------------------------------------------------
4+
// requestAnimationFrame — not available in jsdom.
5+
// Marko 6's scheduler uses rAF. Without this, signal-driven DOM updates
6+
// never flush and items never appear after onMount.
7+
// ---------------------------------------------------------------------------
8+
if (!global.requestAnimationFrame) {
9+
global.requestAnimationFrame = (cb: FrameRequestCallback) =>
10+
setTimeout(cb, 16) as unknown as number
11+
global.cancelAnimationFrame = (id: number) => clearTimeout(id)
12+
}
13+
14+
// ---------------------------------------------------------------------------
15+
// ResizeObserver — not available in jsdom.
16+
// observeElementRect sets one up, but also fires handler() synchronously
17+
// first (giving us the initial rect). The mock prevents errors on .observe().
18+
// ---------------------------------------------------------------------------
19+
global.ResizeObserver = vi.fn().mockImplementation(() => ({
20+
observe: vi.fn(),
21+
unobserve: vi.fn(),
22+
disconnect: vi.fn(),
23+
}))
24+
25+
// ---------------------------------------------------------------------------
26+
// offsetHeight / offsetWidth — always 0 in jsdom (no CSS layout engine).
27+
// observeElementRect calls getRect(element) = { offsetWidth, offsetHeight }.
28+
// Without this, the virtualizer sees a 0-height viewport and renders no items.
29+
// 400px ÷ 50px default estimateSize = 8 visible + 5 overscan each side ≈ 18.
30+
// ---------------------------------------------------------------------------
31+
Object.defineProperty(HTMLElement.prototype, "offsetHeight", {
32+
configurable: true,
33+
get: () => 400,
34+
})
35+
Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
36+
configurable: true,
37+
get: () => 400,
38+
})
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/**
2+
* Integration tests for <virtualizer> and <window-virtualizer> Marko tags.
3+
*
4+
* @marko/testing-library v1.1.2 has a broken isV6 detection:
5+
* const isV6 = !(template as any).renderSync
6+
* Marko 6 browser-compiled templates include renderSync from the runtime,
7+
* causing the library to fall into the Marko 3/4 code path and call
8+
* template.render(input, callback) — which doesn't exist on browser templates.
9+
*
10+
* We bypass @marko/testing-library entirely and call template.mount() directly,
11+
* which is the Marko 6 DOM API. @marko/vite compiles .marko files for the
12+
* browser in vitest/jsdom, so mount() is always available.
13+
*
14+
* ENVIRONMENT:
15+
* - jsdom with rAF, ResizeObserver, offsetHeight mocked (see tests/setup.ts)
16+
* - @marko/vite compiles .marko fixtures → browser templates with mount()
17+
* - waitFor() retries until Marko's RAF-based reactive flush completes
18+
*
19+
* WHY THESE TESTS EXIST:
20+
* The TS unit tests in index.test.ts never compile or mount a .marko file.
21+
* The <return> vs <${input.content}> architecture bug was invisible to them.
22+
* These tests close that gap by mounting real fixture components and asserting
23+
* that virtual items appear in the DOM.
24+
*/
25+
26+
import { afterEach, describe, expect, test } from "vitest"
27+
import { waitFor } from "@testing-library/dom"
28+
import VirtualizerFixture from "./fixtures/virtualizer-fixture.marko"
29+
import WindowVirtualizerFixture from "./fixtures/window-virtualizer-fixture.marko"
30+
31+
// Cast to any — @marko/vite compiles .marko files as ES modules whose default
32+
// export is the template object with mount(input, container): Instance.
33+
const Virtualizer = VirtualizerFixture as any
34+
const WindowVirtualizer = WindowVirtualizerFixture as any
35+
36+
// ---------------------------------------------------------------------------
37+
// Test helpers
38+
// ---------------------------------------------------------------------------
39+
40+
function mountFixture(Template: any, input: Record<string, unknown> = {}) {
41+
const container = document.createElement("div")
42+
document.body.appendChild(container)
43+
Template.mount(input, container)
44+
return container
45+
}
46+
47+
afterEach(() => {
48+
// Reset DOM between tests
49+
document.body.innerHTML = ""
50+
})
51+
52+
// ---------------------------------------------------------------------------
53+
// <virtualizer> — row virtualisation
54+
// ---------------------------------------------------------------------------
55+
56+
describe("<virtualizer> rows", () => {
57+
test("renders body content — catches <return> vs <${input.content}> bug", () => {
58+
// With <return=...>: body never renders → virtual-wrapper missing
59+
// With <${input.content}>: body IS rendered → virtual-wrapper present
60+
// This test definitively catches the architecture bug.
61+
const el = mountFixture(Virtualizer, { count: 100 })
62+
expect(el.querySelector("[data-testid='virtual-wrapper']")).toBeTruthy()
63+
})
64+
65+
test("populates virtualItems after mount", async () => {
66+
// onMount fires → _willUpdate → observeElementRect (sync, mocked 400px)
67+
// → notify() → items signal → Marko RAF flush → DOM updated
68+
const el = mountFixture(Virtualizer, { count: 100 })
69+
await waitFor(() =>
70+
expect(el.querySelectorAll("[data-testid='virtual-item']").length).toBeGreaterThan(0)
71+
)
72+
})
73+
74+
test("items have sequential data-index starting from 0", async () => {
75+
const el = mountFixture(Virtualizer, { count: 100 })
76+
await waitFor(() => {
77+
const items = el.querySelectorAll("[data-testid='virtual-item']")
78+
expect(items[0].getAttribute("data-index")).toBe("0")
79+
expect(items[1].getAttribute("data-index")).toBe("1")
80+
})
81+
})
82+
83+
test("totalSize equals count × estimateSize in px", async () => {
84+
const el = mountFixture(Virtualizer, { count: 100, itemHeight: 50 })
85+
await waitFor(() =>
86+
expect(
87+
(el.querySelector("[data-testid='virtual-wrapper']") as HTMLElement)?.style.height
88+
).toBe("5000px")
89+
)
90+
})
91+
92+
test("renders exactly count items when count < viewport capacity", async () => {
93+
// 3 items × 50px = 150px < 400px mocked viewport → all 3 rendered
94+
const el = mountFixture(Virtualizer, { count: 3 })
95+
await waitFor(() =>
96+
expect(el.querySelectorAll("[data-testid='virtual-item']")).toHaveLength(3)
97+
)
98+
})
99+
100+
test("renders no items when count is 0", () => {
101+
const el = mountFixture(Virtualizer, { count: 0 })
102+
// Wrapper exists (body content rendered) but no items
103+
expect(el.querySelector("[data-testid='virtual-wrapper']")).toBeTruthy()
104+
expect(el.querySelectorAll("[data-testid='virtual-item']")).toHaveLength(0)
105+
})
106+
107+
test("totalSize is 0 when count is 0", () => {
108+
const el = mountFixture(Virtualizer, { count: 0 })
109+
const wrapper = el.querySelector("[data-testid='virtual-wrapper']") as HTMLElement
110+
expect(wrapper?.style.height).toBe("0px")
111+
})
112+
})
113+
114+
// ---------------------------------------------------------------------------
115+
// <virtualizer> — column virtualisation
116+
// ---------------------------------------------------------------------------
117+
118+
describe("<virtualizer> columns", () => {
119+
test("horizontal mode renders items with translateX positioning", async () => {
120+
const el = mountFixture(Virtualizer, { count: 100, horizontal: true })
121+
await waitFor(() => {
122+
const items = el.querySelectorAll("[data-testid='virtual-item']")
123+
expect(items.length).toBeGreaterThan(0)
124+
expect((items[0] as HTMLElement).style.transform).toContain("translateX(0px)")
125+
})
126+
})
127+
})
128+
129+
// ---------------------------------------------------------------------------
130+
// <virtualizer> — masonry lanes
131+
// ---------------------------------------------------------------------------
132+
133+
describe("<virtualizer> lanes", () => {
134+
test("multi-lane layout renders items", async () => {
135+
const el = mountFixture(Virtualizer, { count: 20, lanes: 3 })
136+
await waitFor(() =>
137+
expect(el.querySelectorAll("[data-testid='virtual-item']").length).toBeGreaterThan(0)
138+
)
139+
})
140+
})
141+
142+
// ---------------------------------------------------------------------------
143+
// <window-virtualizer>
144+
// ---------------------------------------------------------------------------
145+
146+
describe("<window-virtualizer>", () => {
147+
test("renders body content", () => {
148+
const el = mountFixture(WindowVirtualizer, { count: 100 })
149+
expect(el.querySelector("[data-testid='virtual-wrapper']")).toBeTruthy()
150+
})
151+
152+
test("populates virtualItems after mount", async () => {
153+
const el = mountFixture(WindowVirtualizer, { count: 100 })
154+
await waitFor(() =>
155+
expect(el.querySelectorAll("[data-testid='virtual-item']").length).toBeGreaterThan(0)
156+
)
157+
})
158+
159+
test("items have data-index starting from 0", async () => {
160+
const el = mountFixture(WindowVirtualizer, { count: 100 })
161+
await waitFor(() =>
162+
expect(
163+
el.querySelectorAll("[data-testid='virtual-item']")[0].getAttribute("data-index")
164+
).toBe("0")
165+
)
166+
})
167+
})
Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,25 @@
1-
import { defineConfig, mergeConfig } from 'vitest/config'
2-
import { tanstackViteConfig } from '@tanstack/vite-config'
3-
import packageJson from './package.json'
1+
import { defineConfig, mergeConfig } from "vitest/config"
2+
import { tanstackViteConfig } from "@tanstack/vite-config"
3+
import marko from "@marko/vite"
4+
import packageJson from "./package.json"
45

56
const config = defineConfig({
7+
// @marko/vite compiles .marko files for the browser in the vitest/jsdom
8+
// environment, producing templates with mount() for DOM rendering.
9+
plugins: [marko()],
610
test: {
711
name: packageJson.name,
8-
dir: './tests',
12+
dir: "./tests",
913
watch: false,
10-
environment: 'jsdom',
14+
environment: "jsdom",
15+
setupFiles: ["./tests/setup.ts"],
1116
},
1217
})
1318

1419
export default mergeConfig(
1520
config,
1621
tanstackViteConfig({
17-
entry: './src/index.ts',
18-
srcDir: './src',
22+
entry: "./src/index.ts",
23+
srcDir: "./src",
1924
}),
20-
)
25+
)

0 commit comments

Comments
 (0)