Skip to content

Commit a0d92c1

Browse files
committed
test(e2e): expand coverage for strategies, actions, and loader lifecycle
1 parent 9c6fdb9 commit a0d92c1

File tree

3 files changed

+379
-17
lines changed

3 files changed

+379
-17
lines changed

e2e/app/main.ts

Lines changed: 219 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,105 @@ import { prop, render } from '@fictjs/runtime'
22
import { createSignal } from '@fictjs/runtime/advanced'
33
import React from 'react'
44

5-
import { installReactIslands, reactify, reactify$ } from '../../src/index'
5+
import {
6+
ReactIsland,
7+
installReactIslands,
8+
reactAction$,
9+
reactify,
10+
reactify$,
11+
} from '../../src/index'
12+
13+
interface E2EState {
14+
lifecycleMounts: number
15+
lifecycleUnmounts: number
16+
actionCalls: string[]
17+
}
18+
19+
const globalHost = globalThis as typeof globalThis & {
20+
__FICT_E2E__?: E2EState
21+
}
22+
23+
function ensureE2EState(): E2EState {
24+
if (!globalHost.__FICT_E2E__) {
25+
globalHost.__FICT_E2E__ = {
26+
lifecycleMounts: 0,
27+
lifecycleUnmounts: 0,
28+
actionCalls: [],
29+
}
30+
}
31+
32+
return globalHost.__FICT_E2E__
33+
}
34+
35+
const e2eState = ensureE2EState()
36+
e2eState.lifecycleMounts = 0
37+
e2eState.lifecycleUnmounts = 0
38+
e2eState.actionCalls = []
39+
40+
const remoteModuleUrl = new URL('./remote-widget.tsx', import.meta.url).href
41+
42+
let actionCallSignal: ((value?: number) => number) | null = null
43+
44+
export function recordE2EAction(payload: string) {
45+
const state = ensureE2EState()
46+
state.actionCalls.push(payload)
47+
48+
if (actionCallSignal) {
49+
const current = actionCallSignal()
50+
actionCallSignal(current + 1)
51+
}
52+
}
653

754
const RemoteIsland = reactify$<{ label: string; count: number }>({
8-
module: new URL('./remote-widget.tsx', import.meta.url).href,
55+
module: remoteModuleUrl,
956
export: 'RemoteWidget',
1057
ssr: false,
1158
client: 'load',
1259
})
1360

61+
const ActionIsland = reactify$<{ label: string; onAction: ReturnType<typeof reactAction$> }>({
62+
module: remoteModuleUrl,
63+
export: 'ActionWidget',
64+
ssr: false,
65+
client: 'load',
66+
})
67+
1468
const Eager = reactify(({ count }: { count: number }) => {
1569
return React.createElement('div', { 'data-testid': 'eager-value' }, `eager:${count}`)
1670
})
1771

72+
function StrategyView(props: { testId: string; label: string }) {
73+
return React.createElement('div', { 'data-testid': props.testId }, props.label)
74+
}
75+
76+
function ReactIslandLabel(props: { label: string }) {
77+
return React.createElement('div', { 'data-testid': 'react-island-value' }, props.label)
78+
}
79+
80+
const HoverStrategyIsland = reactify(StrategyView, {
81+
client: 'hover',
82+
tagName: 'button',
83+
})
84+
85+
const EventStrategyIsland = reactify(StrategyView, {
86+
client: 'event',
87+
event: 'dblclick',
88+
tagName: 'button',
89+
})
90+
91+
const VisibleStrategyIsland = reactify(StrategyView, {
92+
client: 'visible',
93+
visibleRootMargin: '0px',
94+
})
95+
1896
function App() {
1997
const eagerCount = createSignal(0)
2098
const qrlCount = createSignal(0)
99+
const islandLabel = createSignal('alpha')
100+
const signalMountGate = createSignal(false)
101+
const actionCount = createSignal(0)
102+
103+
actionCallSignal = actionCount
21104

22105
return {
23106
type: 'div',
@@ -52,6 +135,57 @@ function App() {
52135
count: prop(() => qrlCount()),
53136
},
54137
},
138+
{
139+
type: 'button',
140+
props: {
141+
id: 'react-island-swap',
142+
onClick: () => islandLabel(islandLabel() === 'alpha' ? 'beta' : 'alpha'),
143+
children: 'swap react island',
144+
},
145+
},
146+
{
147+
type: ReactIsland,
148+
props: {
149+
component: ReactIslandLabel,
150+
props: () => ({
151+
label: islandLabel(),
152+
}),
153+
},
154+
},
155+
{
156+
type: 'button',
157+
props: {
158+
id: 'signal-mount',
159+
onClick: () => signalMountGate(true),
160+
children: 'mount signal strategy',
161+
},
162+
},
163+
{
164+
type: ReactIsland,
165+
props: {
166+
component: StrategyView,
167+
client: 'signal',
168+
signal: () => signalMountGate(),
169+
props: {
170+
testId: 'signal-strategy-value',
171+
label: 'signal-mounted',
172+
},
173+
},
174+
},
175+
{
176+
type: ActionIsland,
177+
props: {
178+
label: 'main',
179+
onAction: reactAction$(import.meta.url, 'recordE2EAction'),
180+
},
181+
},
182+
{
183+
type: 'div',
184+
props: {
185+
'data-testid': 'action-call-count',
186+
children: prop(() => String(actionCount())),
187+
},
188+
},
55189
],
56190
},
57191
}
@@ -60,34 +194,104 @@ function App() {
60194
const app = document.getElementById('app') as HTMLElement
61195
render(() => ({ type: App, props: {} }), app)
62196

63-
const loaderHost = document.getElementById('loader-island') as HTMLElement
64-
const loaderQrl = `${new URL('./remote-widget.tsx', import.meta.url).href}#LoaderWidget`
65-
let loaderCount = 1
197+
const hoverRoot = document.createElement('div')
198+
hoverRoot.id = 'hover-root'
199+
document.body.appendChild(hoverRoot)
200+
render(
201+
() => ({
202+
type: HoverStrategyIsland,
203+
props: {
204+
testId: 'hover-strategy-value',
205+
label: 'hover-mounted',
206+
},
207+
}),
208+
hoverRoot,
209+
)
210+
211+
const eventRoot = document.createElement('div')
212+
eventRoot.id = 'event-root'
213+
document.body.appendChild(eventRoot)
214+
render(
215+
() => ({
216+
type: EventStrategyIsland,
217+
props: {
218+
testId: 'event-strategy-value',
219+
label: 'event-mounted',
220+
},
221+
}),
222+
eventRoot,
223+
)
224+
225+
const visibleRoot = document.createElement('div')
226+
visibleRoot.id = 'visible-root'
227+
visibleRoot.style.marginTop = '2200px'
228+
visibleRoot.style.minHeight = '20px'
229+
document.body.appendChild(visibleRoot)
230+
render(
231+
() => ({
232+
type: VisibleStrategyIsland,
233+
props: {
234+
testId: 'visible-strategy-value',
235+
label: 'visible-mounted',
236+
},
237+
}),
238+
visibleRoot,
239+
)
66240

67241
const encode = (value: Record<string, unknown>) => encodeURIComponent(JSON.stringify(value))
68242

243+
function appendControlButton(id: string, text: string, onClick: () => void) {
244+
const button = document.createElement('button')
245+
button.id = id
246+
button.textContent = text
247+
button.addEventListener('click', onClick)
248+
document.body.appendChild(button)
249+
}
250+
251+
const loaderHost = document.getElementById('loader-island') as HTMLElement
252+
const loaderQrl = `${remoteModuleUrl}#LoaderWidget`
253+
const loaderAltQrl = `${remoteModuleUrl}#LoaderWidgetAlt`
254+
const loaderLifecycleQrl = `${remoteModuleUrl}#LifecycleWidget`
255+
let loaderCount = 1
256+
let dynamicLoaderHost: HTMLElement | null = null
257+
69258
loaderHost.setAttribute('data-fict-react', loaderQrl)
70259
loaderHost.setAttribute('data-fict-react-client', 'load')
71260
loaderHost.setAttribute('data-fict-react-ssr', '0')
72261
loaderHost.setAttribute('data-fict-react-props', encode({ label: 'loader', count: loaderCount }))
73262

74-
const button = document.createElement('button')
75-
button.id = 'loader-inc'
76-
button.textContent = 'inc loader'
77-
button.addEventListener('click', () => {
263+
appendControlButton('loader-inc', 'inc loader', () => {
78264
loaderCount += 1
79265
loaderHost.setAttribute('data-fict-react-props', encode({ label: 'loader', count: loaderCount }))
80266
})
81-
document.body.appendChild(button)
82267

83-
const immutableMutationButton = document.createElement('button')
84-
immutableMutationButton.id = 'loader-mutate-immutable'
85-
immutableMutationButton.textContent = 'mutate loader immutable attrs'
86-
immutableMutationButton.addEventListener('click', () => {
268+
appendControlButton('loader-switch-qrl', 'switch loader qrl', () => {
269+
loaderHost.setAttribute('data-fict-react', loaderAltQrl)
270+
loaderHost.setAttribute('data-fict-react-props', encode({ label: 'loader', count: loaderCount }))
271+
})
272+
273+
appendControlButton('loader-add-dynamic', 'add dynamic loader host', () => {
274+
if (dynamicLoaderHost?.isConnected) return
275+
276+
dynamicLoaderHost = document.createElement('div')
277+
dynamicLoaderHost.id = 'loader-dynamic-host'
278+
dynamicLoaderHost.setAttribute('data-fict-react', loaderLifecycleQrl)
279+
dynamicLoaderHost.setAttribute('data-fict-react-client', 'load')
280+
dynamicLoaderHost.setAttribute('data-fict-react-ssr', '0')
281+
dynamicLoaderHost.setAttribute('data-fict-react-props', encode({ label: 'dynamic' }))
282+
document.body.appendChild(dynamicLoaderHost)
283+
})
284+
285+
appendControlButton('loader-remove-dynamic', 'remove dynamic loader host', () => {
286+
if (!dynamicLoaderHost) return
287+
dynamicLoaderHost.remove()
288+
dynamicLoaderHost = null
289+
})
290+
291+
appendControlButton('loader-mutate-immutable', 'mutate loader immutable attrs', () => {
87292
loaderHost.setAttribute('data-fict-react-client', 'idle')
88293
loaderHost.setAttribute('data-fict-react-ssr', '1')
89294
loaderHost.setAttribute('data-fict-react-prefix', 'mutated-prefix')
90295
})
91-
document.body.appendChild(immutableMutationButton)
92296

93297
installReactIslands()

e2e/app/remote-widget.tsx

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,28 @@
1-
import React from 'react'
1+
import React, { useEffect } from 'react'
2+
3+
interface E2EState {
4+
lifecycleMounts: number
5+
lifecycleUnmounts: number
6+
actionCalls: string[]
7+
}
8+
9+
declare global {
10+
interface Window {
11+
__FICT_E2E__?: E2EState
12+
}
13+
}
14+
15+
function ensureE2EState(): E2EState {
16+
if (!window.__FICT_E2E__) {
17+
window.__FICT_E2E__ = {
18+
lifecycleMounts: 0,
19+
lifecycleUnmounts: 0,
20+
actionCalls: [],
21+
}
22+
}
23+
24+
return window.__FICT_E2E__
25+
}
226

327
export function RemoteWidget(props: { label: string; count: number }) {
428
return React.createElement(
@@ -15,3 +39,42 @@ export function LoaderWidget(props: { label: string; count: number }) {
1539
`${props.label}:${props.count}`,
1640
)
1741
}
42+
43+
export function LoaderWidgetAlt(props: { label: string; count: number }) {
44+
return React.createElement(
45+
'div',
46+
{ 'data-testid': 'loader-value' },
47+
`ALT-${props.label}:${props.count}`,
48+
)
49+
}
50+
51+
export function StrategyWidget(props: { testId: string; label: string }) {
52+
return React.createElement('div', { 'data-testid': props.testId }, props.label)
53+
}
54+
55+
export function ActionWidget(props: { label: string; onAction?: (payload: string) => void }) {
56+
return React.createElement(
57+
'button',
58+
{
59+
id: 'action-widget-trigger',
60+
'data-testid': 'action-widget-trigger',
61+
onClick: () => {
62+
props.onAction?.(`action:${props.label}`)
63+
},
64+
},
65+
`trigger:${props.label}`,
66+
)
67+
}
68+
69+
export function LifecycleWidget(props: { label: string }) {
70+
useEffect(() => {
71+
const state = ensureE2EState()
72+
state.lifecycleMounts += 1
73+
74+
return () => {
75+
state.lifecycleUnmounts += 1
76+
}
77+
}, [])
78+
79+
return React.createElement('div', { 'data-testid': 'loader-lifecycle-value' }, props.label)
80+
}

0 commit comments

Comments
 (0)