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+ } )
0 commit comments