@furystack/shades 12.2.0 → 12.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/esm/components/lazy-load.spec.js +3 -3
- package/esm/components/lazy-load.spec.js.map +1 -1
- package/esm/components/nested-router.spec.js +24 -21
- package/esm/components/nested-router.spec.js.map +1 -1
- package/esm/components/router.spec.js +10 -9
- package/esm/components/router.spec.js.map +1 -1
- package/esm/shade.d.ts +2 -2
- package/esm/shade.d.ts.map +1 -1
- package/esm/shade.js +18 -4
- package/esm/shade.js.map +1 -1
- package/esm/shade.spec.js +5 -5
- package/esm/shade.spec.js.map +1 -1
- package/esm/shades.integration.spec.js +3 -7
- package/esm/shades.integration.spec.js.map +1 -1
- package/esm/vnode.integration.spec.js +0 -2
- package/esm/vnode.integration.spec.js.map +1 -1
- package/esm/vnode.js +1 -1
- package/esm/vnode.js.map +1 -1
- package/esm/vnode.spec.js +4 -5
- package/esm/vnode.spec.js.map +1 -1
- package/package.json +1 -1
- package/src/components/lazy-load.spec.tsx +3 -3
- package/src/components/nested-router.spec.tsx +24 -21
- package/src/components/router.spec.tsx +10 -9
- package/src/shade.spec.tsx +5 -5
- package/src/shade.ts +19 -5
- package/src/shades.integration.spec.tsx +3 -7
- package/src/vnode.integration.spec.tsx +0 -2
- package/src/vnode.spec.ts +4 -5
- package/src/vnode.ts +1 -1
|
@@ -64,7 +64,7 @@ describe('Lazy Load', () => {
|
|
|
64
64
|
})
|
|
65
65
|
await flushUpdates()
|
|
66
66
|
expect(document.body.innerHTML).toBe('<div id="root"><lazy-load><div>Loading...</div></lazy-load></div>')
|
|
67
|
-
await
|
|
67
|
+
await flushUpdates()
|
|
68
68
|
expect(load).toBeCalledTimes(1)
|
|
69
69
|
expect(document.body.innerHTML).toBe('<div id="root"><lazy-load><button id="retry">:(</button></lazy-load></div>')
|
|
70
70
|
document.getElementById('retry')?.click()
|
|
@@ -102,12 +102,12 @@ describe('Lazy Load', () => {
|
|
|
102
102
|
})
|
|
103
103
|
await flushUpdates()
|
|
104
104
|
expect(document.body.innerHTML).toBe('<div id="root"><lazy-load><div>Loading...</div></lazy-load></div>')
|
|
105
|
-
await
|
|
105
|
+
await flushUpdates()
|
|
106
106
|
expect(load).toBeCalledTimes(1)
|
|
107
107
|
expect(document.body.innerHTML).toBe('<div id="root"><lazy-load><button id="retry">:(</button></lazy-load></div>')
|
|
108
108
|
document.getElementById('retry')?.click()
|
|
109
109
|
expect(load).toBeCalledTimes(2)
|
|
110
|
-
await
|
|
110
|
+
await flushUpdates()
|
|
111
111
|
expect(document.body.innerHTML).toBe('<div id="root"><lazy-load><div>success</div></lazy-load></div>')
|
|
112
112
|
})
|
|
113
113
|
})
|
|
@@ -3,6 +3,7 @@ import { sleepAsync, usingAsync } from '@furystack/utils'
|
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
4
4
|
import { initializeShadeRoot } from '../initialize.js'
|
|
5
5
|
import { createComponent } from '../shade-component.js'
|
|
6
|
+
import { flushUpdates } from '../shade.js'
|
|
6
7
|
import {
|
|
7
8
|
buildMatchChain,
|
|
8
9
|
findDivergenceIndex,
|
|
@@ -366,7 +367,7 @@ describe('NestedRouter lifecycle hooks', () => {
|
|
|
366
367
|
const clickOn = (name: string) => document.getElementById(name)?.click()
|
|
367
368
|
|
|
368
369
|
// --- Initial load at /parent/child-a ---
|
|
369
|
-
await
|
|
370
|
+
await flushUpdates()
|
|
370
371
|
expect(getContent()).toBe('child-a')
|
|
371
372
|
expect(onVisitParent).toBeCalledTimes(1)
|
|
372
373
|
expect(onVisitChildA).toBeCalledTimes(1)
|
|
@@ -375,7 +376,7 @@ describe('NestedRouter lifecycle hooks', () => {
|
|
|
375
376
|
// --- Click same route: no lifecycle hooks should fire ---
|
|
376
377
|
callOrder.length = 0
|
|
377
378
|
clickOn('child-a')
|
|
378
|
-
await
|
|
379
|
+
await flushUpdates()
|
|
379
380
|
expect(onVisitParent).toBeCalledTimes(1)
|
|
380
381
|
expect(onVisitChildA).toBeCalledTimes(1)
|
|
381
382
|
expect(callOrder).toEqual([])
|
|
@@ -383,7 +384,7 @@ describe('NestedRouter lifecycle hooks', () => {
|
|
|
383
384
|
// --- Switch child: only child lifecycle fires, parent stays ---
|
|
384
385
|
callOrder.length = 0
|
|
385
386
|
clickOn('child-b')
|
|
386
|
-
await
|
|
387
|
+
await flushUpdates()
|
|
387
388
|
expect(getContent()).toBe('child-b')
|
|
388
389
|
expect(onLeaveChildA).toBeCalledTimes(1)
|
|
389
390
|
expect(onVisitChildB).toBeCalledTimes(1)
|
|
@@ -394,7 +395,8 @@ describe('NestedRouter lifecycle hooks', () => {
|
|
|
394
395
|
// --- Navigate to a completely different branch ---
|
|
395
396
|
callOrder.length = 0
|
|
396
397
|
clickOn('other')
|
|
397
|
-
await
|
|
398
|
+
await flushUpdates()
|
|
399
|
+
await flushUpdates()
|
|
398
400
|
expect(getContent()).toBe('other')
|
|
399
401
|
expect(onLeaveChildB).toBeCalledTimes(1)
|
|
400
402
|
expect(onLeaveParent).toBeCalledTimes(1)
|
|
@@ -405,7 +407,7 @@ describe('NestedRouter lifecycle hooks', () => {
|
|
|
405
407
|
// --- Navigate to non-matching URL: onLeave for all active ---
|
|
406
408
|
callOrder.length = 0
|
|
407
409
|
clickOn('nowhere')
|
|
408
|
-
await
|
|
410
|
+
await flushUpdates()
|
|
409
411
|
expect(getContent()).toBe('not found')
|
|
410
412
|
expect(onLeaveOther).toBeCalledTimes(1)
|
|
411
413
|
expect(callOrder).toEqual(['leave-other'])
|
|
@@ -490,7 +492,7 @@ describe('NestedRouter latest-wins on rapid navigation', () => {
|
|
|
490
492
|
const clickOn = (name: string) => document.getElementById(name)?.click()
|
|
491
493
|
|
|
492
494
|
// --- Initial load at /route-a ---
|
|
493
|
-
await
|
|
495
|
+
await flushUpdates()
|
|
494
496
|
expect(getContent()).toBe('route-a')
|
|
495
497
|
expect(onVisitA).toHaveBeenCalledTimes(1)
|
|
496
498
|
|
|
@@ -500,7 +502,7 @@ describe('NestedRouter latest-wins on rapid navigation', () => {
|
|
|
500
502
|
// Don't await — immediately navigate again
|
|
501
503
|
clickOn('go-c')
|
|
502
504
|
|
|
503
|
-
// Wait long enough for both transitions to settle
|
|
505
|
+
// Wait long enough for both transitions to settle (onVisitB has 200ms delay)
|
|
504
506
|
await sleepAsync(500)
|
|
505
507
|
|
|
506
508
|
// The final destination should be route-c
|
|
@@ -582,7 +584,7 @@ describe('NestedRouter lifecycle element scope', () => {
|
|
|
582
584
|
const clickOn = (name: string) => document.getElementById(name)?.click()
|
|
583
585
|
|
|
584
586
|
// --- Initial load at /parent/child-a ---
|
|
585
|
-
await
|
|
587
|
+
await flushUpdates()
|
|
586
588
|
expect(visitElements).toHaveLength(2)
|
|
587
589
|
// Parent's onVisit element should be the full tree (parent wrapping child)
|
|
588
590
|
expect(visitElements[0].route).toBe('parent')
|
|
@@ -595,7 +597,7 @@ describe('NestedRouter lifecycle element scope', () => {
|
|
|
595
597
|
visitElements.length = 0
|
|
596
598
|
leaveElements.length = 0
|
|
597
599
|
clickOn('child-b')
|
|
598
|
-
await
|
|
600
|
+
await flushUpdates()
|
|
599
601
|
|
|
600
602
|
// onLeave should receive the child-a element, not the full wrapper
|
|
601
603
|
expect(leaveElements).toHaveLength(1)
|
|
@@ -653,19 +655,19 @@ describe('NestedRouter flat routes', () => {
|
|
|
653
655
|
const getContent = () => document.getElementById('content')?.innerHTML
|
|
654
656
|
const clickOn = (name: string) => document.getElementById(name)?.click()
|
|
655
657
|
|
|
656
|
-
await
|
|
658
|
+
await flushUpdates()
|
|
657
659
|
expect(getContent()).toBe('home-page')
|
|
658
660
|
|
|
659
661
|
clickOn('about')
|
|
660
|
-
await
|
|
662
|
+
await flushUpdates()
|
|
661
663
|
expect(getContent()).toBe('about-page')
|
|
662
664
|
|
|
663
665
|
clickOn('contact')
|
|
664
|
-
await
|
|
666
|
+
await flushUpdates()
|
|
665
667
|
expect(getContent()).toBe('contact-page')
|
|
666
668
|
|
|
667
669
|
clickOn('home')
|
|
668
|
-
await
|
|
670
|
+
await flushUpdates()
|
|
669
671
|
expect(getContent()).toBe('home-page')
|
|
670
672
|
})
|
|
671
673
|
})
|
|
@@ -712,7 +714,7 @@ describe('NestedRouter outlet composition', () => {
|
|
|
712
714
|
),
|
|
713
715
|
})
|
|
714
716
|
|
|
715
|
-
await
|
|
717
|
+
await flushUpdates()
|
|
716
718
|
|
|
717
719
|
// Parent layout should be rendered with child inside
|
|
718
720
|
expect(document.getElementById('header')?.innerHTML).toBe('Dashboard Header')
|
|
@@ -753,7 +755,7 @@ describe('NestedRouter outlet composition', () => {
|
|
|
753
755
|
),
|
|
754
756
|
})
|
|
755
757
|
|
|
756
|
-
await
|
|
758
|
+
await flushUpdates()
|
|
757
759
|
|
|
758
760
|
// Parent matched alone, outlet is undefined, so the fallback renders
|
|
759
761
|
expect(document.getElementById('child')?.innerHTML).toBe('dashboard-index')
|
|
@@ -814,7 +816,7 @@ describe('NestedRouter route param changes', () => {
|
|
|
814
816
|
const clickOn = (name: string) => document.getElementById(name)?.click()
|
|
815
817
|
|
|
816
818
|
// Initial load at /users/1
|
|
817
|
-
await
|
|
819
|
+
await flushUpdates()
|
|
818
820
|
expect(getContent()).toBe('user-1')
|
|
819
821
|
expect(onVisitUser).toHaveBeenCalledTimes(1)
|
|
820
822
|
expect(callOrder).toEqual(['visit-user'])
|
|
@@ -822,7 +824,7 @@ describe('NestedRouter route param changes', () => {
|
|
|
822
824
|
// Navigate to /users/2 — same route, different param → lifecycle should fire
|
|
823
825
|
callOrder.length = 0
|
|
824
826
|
clickOn('user-2')
|
|
825
|
-
await
|
|
827
|
+
await flushUpdates()
|
|
826
828
|
expect(getContent()).toBe('user-2')
|
|
827
829
|
expect(onLeaveUser).toHaveBeenCalledTimes(1)
|
|
828
830
|
expect(onVisitUser).toHaveBeenCalledTimes(2)
|
|
@@ -831,7 +833,7 @@ describe('NestedRouter route param changes', () => {
|
|
|
831
833
|
// Navigate to /users/3
|
|
832
834
|
callOrder.length = 0
|
|
833
835
|
clickOn('user-3')
|
|
834
|
-
await
|
|
836
|
+
await flushUpdates()
|
|
835
837
|
expect(getContent()).toBe('user-3')
|
|
836
838
|
expect(onLeaveUser).toHaveBeenCalledTimes(2)
|
|
837
839
|
expect(onVisitUser).toHaveBeenCalledTimes(3)
|
|
@@ -840,7 +842,7 @@ describe('NestedRouter route param changes', () => {
|
|
|
840
842
|
// Click same user — no lifecycle change
|
|
841
843
|
callOrder.length = 0
|
|
842
844
|
clickOn('user-3')
|
|
843
|
-
await
|
|
845
|
+
await flushUpdates()
|
|
844
846
|
expect(getContent()).toBe('user-3')
|
|
845
847
|
expect(onLeaveUser).toHaveBeenCalledTimes(2)
|
|
846
848
|
expect(onVisitUser).toHaveBeenCalledTimes(3)
|
|
@@ -897,7 +899,7 @@ describe('NestedRouter route param changes', () => {
|
|
|
897
899
|
|
|
898
900
|
const clickOn = (name: string) => document.getElementById(name)?.click()
|
|
899
901
|
|
|
900
|
-
await
|
|
902
|
+
await flushUpdates()
|
|
901
903
|
expect(document.getElementById('org')?.textContent).toContain('org-alpha')
|
|
902
904
|
expect(document.getElementById('child')?.innerHTML).toBe('dashboard')
|
|
903
905
|
expect(onVisitOrg).toHaveBeenCalledTimes(1)
|
|
@@ -906,7 +908,8 @@ describe('NestedRouter route param changes', () => {
|
|
|
906
908
|
// Change parent param: org/alpha → org/beta, child stays /dashboard
|
|
907
909
|
// Both parent and child should get leave/visit since parent diverges
|
|
908
910
|
clickOn('beta-dash')
|
|
909
|
-
await
|
|
911
|
+
await flushUpdates()
|
|
912
|
+
await flushUpdates()
|
|
910
913
|
expect(document.getElementById('org')?.textContent).toContain('org-beta')
|
|
911
914
|
expect(document.getElementById('child')?.innerHTML).toBe('dashboard')
|
|
912
915
|
expect(onLeaveOrg).toHaveBeenCalledTimes(1)
|
|
@@ -4,6 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
4
4
|
import { initializeShadeRoot } from '../initialize.js'
|
|
5
5
|
import { LocationService } from '../services/location-service.js'
|
|
6
6
|
import { createComponent } from '../shade-component.js'
|
|
7
|
+
import { flushUpdates } from '../shade.js'
|
|
7
8
|
import { RouteLink } from './route-link.js'
|
|
8
9
|
import { Router, type Route } from './router.js'
|
|
9
10
|
|
|
@@ -81,7 +82,7 @@ describe('Router', () => {
|
|
|
81
82
|
const clickOn = (name: string) => document.getElementById(name)?.click()
|
|
82
83
|
|
|
83
84
|
// --- Initial load at /route-a ---
|
|
84
|
-
await
|
|
85
|
+
await flushUpdates()
|
|
85
86
|
expect(getContent()).toBe('route-a')
|
|
86
87
|
expect(onVisitA).toHaveBeenCalledTimes(1)
|
|
87
88
|
|
|
@@ -90,7 +91,7 @@ describe('Router', () => {
|
|
|
90
91
|
clickOn('go-b')
|
|
91
92
|
clickOn('go-c')
|
|
92
93
|
|
|
93
|
-
// Wait long enough for both transitions to settle
|
|
94
|
+
// Wait long enough for both transitions to settle (onVisitB has 200ms delay)
|
|
94
95
|
await sleepAsync(500)
|
|
95
96
|
|
|
96
97
|
// The final destination should be route-c
|
|
@@ -169,7 +170,7 @@ describe('Router', () => {
|
|
|
169
170
|
|
|
170
171
|
const clickOn = (name: string) => document.getElementById(name)?.click()
|
|
171
172
|
|
|
172
|
-
await
|
|
173
|
+
await flushUpdates()
|
|
173
174
|
|
|
174
175
|
expect(getLocation()).toBe('/')
|
|
175
176
|
expect(getContent()).toBe('home')
|
|
@@ -177,37 +178,37 @@ describe('Router', () => {
|
|
|
177
178
|
expect(onVisit).not.toBeCalled()
|
|
178
179
|
|
|
179
180
|
clickOn('a')
|
|
180
|
-
await
|
|
181
|
+
await flushUpdates()
|
|
181
182
|
expect(getContent()).toBe('route-a')
|
|
182
183
|
expect(getLocation()).toBe('/route-a')
|
|
183
184
|
expect(onRouteChange).toBeCalledTimes(1)
|
|
184
185
|
expect(onVisit).toBeCalledTimes(1)
|
|
185
186
|
|
|
186
187
|
clickOn('a')
|
|
187
|
-
await
|
|
188
|
+
await flushUpdates()
|
|
188
189
|
expect(onVisit).toBeCalledTimes(1)
|
|
189
190
|
expect(onLeave).not.toBeCalled()
|
|
190
191
|
|
|
191
192
|
clickOn('b')
|
|
192
|
-
await
|
|
193
|
+
await flushUpdates()
|
|
193
194
|
expect(onLeave).toBeCalledTimes(1)
|
|
194
195
|
|
|
195
196
|
expect(getContent()).toBe('route-b')
|
|
196
197
|
expect(getLocation()).toBe('/route-b')
|
|
197
198
|
|
|
198
199
|
clickOn('b-with-id')
|
|
199
|
-
await
|
|
200
|
+
await flushUpdates()
|
|
200
201
|
expect(getContent()).toBe('route-b123')
|
|
201
202
|
expect(getLocation()).toBe('/route-b/123')
|
|
202
203
|
|
|
203
204
|
clickOn('c')
|
|
204
|
-
await
|
|
205
|
+
await flushUpdates()
|
|
205
206
|
expect(getContent()).toBe('route-c')
|
|
206
207
|
expect(getLocation()).toBe('/route-c')
|
|
207
208
|
|
|
208
209
|
expect(onLastLeave).not.toBeCalled()
|
|
209
210
|
clickOn('x')
|
|
210
|
-
await
|
|
211
|
+
await flushUpdates()
|
|
211
212
|
expect(getContent()).toBe('not found')
|
|
212
213
|
expect(getLocation()).toBe('/route-x')
|
|
213
214
|
expect(onLastLeave).toBeCalledTimes(1)
|
package/src/shade.spec.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import { sleepAsync, usingAsync } from '@furystack/utils'
|
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
4
4
|
import { initializeShadeRoot } from './initialize.js'
|
|
5
5
|
import { createComponent } from './shade-component.js'
|
|
6
|
-
import { Shade } from './shade.js'
|
|
6
|
+
import { flushUpdates, Shade } from './shade.js'
|
|
7
7
|
|
|
8
8
|
describe('Shade edge cases', () => {
|
|
9
9
|
beforeEach(() => {
|
|
@@ -86,7 +86,7 @@ describe('Shade edge cases', () => {
|
|
|
86
86
|
),
|
|
87
87
|
})
|
|
88
88
|
|
|
89
|
-
await
|
|
89
|
+
await flushUpdates()
|
|
90
90
|
|
|
91
91
|
// Parent should use root injector (inherited from parent)
|
|
92
92
|
expect(parentCapturedInjector).toBe(rootInjector)
|
|
@@ -129,7 +129,7 @@ describe('Shade edge cases', () => {
|
|
|
129
129
|
jsxElement: <ExampleComponent />,
|
|
130
130
|
})
|
|
131
131
|
|
|
132
|
-
await
|
|
132
|
+
await flushUpdates()
|
|
133
133
|
expect(document.getElementById('value')?.textContent).toBe('initial')
|
|
134
134
|
|
|
135
135
|
// Simulate cross-tab message via BroadcastChannel
|
|
@@ -173,7 +173,7 @@ describe('Shade edge cases', () => {
|
|
|
173
173
|
jsxElement: <ExampleComponent />,
|
|
174
174
|
})
|
|
175
175
|
|
|
176
|
-
await
|
|
176
|
+
await flushUpdates()
|
|
177
177
|
expect(document.getElementById('value')?.textContent).toBe('initial')
|
|
178
178
|
|
|
179
179
|
// Simulate cross-tab message with different key
|
|
@@ -218,7 +218,7 @@ describe('Shade edge cases', () => {
|
|
|
218
218
|
jsxElement: <ExampleComponent />,
|
|
219
219
|
})
|
|
220
220
|
|
|
221
|
-
await
|
|
221
|
+
await flushUpdates()
|
|
222
222
|
expect(document.getElementById('value')?.textContent).toBe('initial')
|
|
223
223
|
|
|
224
224
|
// Remove the component from DOM
|
package/src/shade.ts
CHANGED
|
@@ -111,7 +111,7 @@ export const Shade = <TProps, TElementBase extends HTMLElement = HTMLElement>(
|
|
|
111
111
|
private _refs = new Map<string, RefObject<Element>>()
|
|
112
112
|
|
|
113
113
|
public connectedCallback() {
|
|
114
|
-
this.
|
|
114
|
+
this._performUpdate()
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
public async disconnectedCallback() {
|
|
@@ -249,12 +249,23 @@ export const Shade = <TProps, TElementBase extends HTMLElement = HTMLElement>(
|
|
|
249
249
|
if (!this._updateScheduled) {
|
|
250
250
|
this._updateScheduled = true
|
|
251
251
|
queueMicrotask(() => {
|
|
252
|
+
if (!this._updateScheduled) return
|
|
252
253
|
this._updateScheduled = false
|
|
253
254
|
this._performUpdate()
|
|
254
255
|
})
|
|
255
256
|
}
|
|
256
257
|
}
|
|
257
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Performs a synchronous component update, canceling any pending async update.
|
|
261
|
+
* Used during parent-to-child reconciliation so the entire subtree settles
|
|
262
|
+
* in a single call frame rather than cascading across microtask ticks.
|
|
263
|
+
*/
|
|
264
|
+
public updateComponentSync() {
|
|
265
|
+
this._updateScheduled = false
|
|
266
|
+
this._performUpdate()
|
|
267
|
+
}
|
|
268
|
+
|
|
258
269
|
private _performUpdate() {
|
|
259
270
|
this._pendingHostProps = []
|
|
260
271
|
let renderResult: unknown
|
|
@@ -265,11 +276,14 @@ export const Shade = <TProps, TElementBase extends HTMLElement = HTMLElement>(
|
|
|
265
276
|
setRenderMode(false)
|
|
266
277
|
}
|
|
267
278
|
|
|
279
|
+
// Apply host props before patching children so that child components
|
|
280
|
+
// rendered synchronously can discover parent state (e.g. injector)
|
|
281
|
+
// via getInjectorFromParent().
|
|
282
|
+
this._applyHostProps()
|
|
283
|
+
|
|
268
284
|
const newVTree = toVChildArray(renderResult)
|
|
269
285
|
patchChildren(this, this._prevVTree || [], newVTree)
|
|
270
286
|
this._prevVTree = newVTree
|
|
271
|
-
|
|
272
|
-
this._applyHostProps()
|
|
273
287
|
}
|
|
274
288
|
|
|
275
289
|
/**
|
|
@@ -441,8 +455,8 @@ export const Shade = <TProps, TElementBase extends HTMLElement = HTMLElement>(
|
|
|
441
455
|
* Flushes any pending microtask-based component updates.
|
|
442
456
|
* Useful in tests to wait for batched renders to complete before asserting DOM state.
|
|
443
457
|
*
|
|
444
|
-
*
|
|
445
|
-
* `
|
|
458
|
+
* Child component updates during reconciliation are performed synchronously, so a single
|
|
459
|
+
* `await flushUpdates()` is sufficient to settle the entire component tree after a state change.
|
|
446
460
|
* @returns a promise that resolves after the current microtask queue has been processed
|
|
447
461
|
*/
|
|
448
462
|
export const flushUpdates = (): Promise<void> => new Promise<void>((resolve) => queueMicrotask(resolve))
|
|
@@ -323,11 +323,11 @@ describe('Shades integration tests', () => {
|
|
|
323
323
|
}
|
|
324
324
|
const expectCount = (count: number) => expect(document.body.innerHTML).toContain(`Count is ${count}`)
|
|
325
325
|
|
|
326
|
-
await
|
|
326
|
+
await flushUpdates()
|
|
327
327
|
|
|
328
328
|
expectCount(0)
|
|
329
329
|
|
|
330
|
-
await
|
|
330
|
+
await flushUpdates()
|
|
331
331
|
await plus()
|
|
332
332
|
expectCount(1)
|
|
333
333
|
expect(store.getItem('count')).toBe('1')
|
|
@@ -383,7 +383,7 @@ describe('Shades integration tests', () => {
|
|
|
383
383
|
|
|
384
384
|
expectCount(0)
|
|
385
385
|
|
|
386
|
-
await
|
|
386
|
+
await flushUpdates()
|
|
387
387
|
await plus()
|
|
388
388
|
expectCount(1)
|
|
389
389
|
expect(location.search).toBe(`?${serializeToQueryString({ count: 1 })}`)
|
|
@@ -520,7 +520,6 @@ describe('Shades integration tests', () => {
|
|
|
520
520
|
jsxElement: <ParentComponent />,
|
|
521
521
|
})
|
|
522
522
|
|
|
523
|
-
await flushUpdates()
|
|
524
523
|
await flushUpdates()
|
|
525
524
|
|
|
526
525
|
const innerInput = document.querySelector('[data-testid="inner-input"]') as HTMLInputElement
|
|
@@ -531,9 +530,6 @@ describe('Shades integration tests', () => {
|
|
|
531
530
|
const toggleBtn = document.getElementById('toggle-disabled') as HTMLButtonElement
|
|
532
531
|
toggleBtn.click()
|
|
533
532
|
|
|
534
|
-
// Wait for parent re-render + child re-render (two microtask levels)
|
|
535
|
-
await flushUpdates()
|
|
536
|
-
await flushUpdates()
|
|
537
533
|
await flushUpdates()
|
|
538
534
|
|
|
539
535
|
const updatedInput = document.querySelector('[data-testid="inner-input"]') as HTMLInputElement
|
|
@@ -637,7 +637,6 @@ describe('VNode reconciliation integration tests', () => {
|
|
|
637
637
|
jsxElement: <ParentComponent />,
|
|
638
638
|
})
|
|
639
639
|
await flushUpdates()
|
|
640
|
-
await flushUpdates()
|
|
641
640
|
|
|
642
641
|
expect(document.getElementById('child-value')?.textContent).toBe('0')
|
|
643
642
|
const childElement = document.querySelector('morph-child-component')
|
|
@@ -645,7 +644,6 @@ describe('VNode reconciliation integration tests', () => {
|
|
|
645
644
|
// Trigger parent re-render
|
|
646
645
|
document.getElementById('parent-increment')?.click()
|
|
647
646
|
await flushUpdates()
|
|
648
|
-
await flushUpdates()
|
|
649
647
|
|
|
650
648
|
// Child should be the same DOM element (not recreated)
|
|
651
649
|
expect(document.querySelector('morph-child-component')).toBe(childElement)
|
package/src/vnode.spec.ts
CHANGED
|
@@ -532,19 +532,18 @@ describe('vnode', () => {
|
|
|
532
532
|
})
|
|
533
533
|
|
|
534
534
|
describe('Shade component boundaries', () => {
|
|
535
|
-
it('should call
|
|
535
|
+
it('should call updateComponentSync on child Shade when props change', () => {
|
|
536
536
|
const parent = document.createElement('div')
|
|
537
537
|
|
|
538
538
|
const fakeShadeEl = document.createElement('my-shade') as unknown as JSX.Element
|
|
539
539
|
const updateFn = vi.fn()
|
|
540
|
-
;(fakeShadeEl as unknown as Record<string, unknown>).
|
|
540
|
+
;(fakeShadeEl as unknown as Record<string, unknown>).updateComponentSync = updateFn
|
|
541
541
|
;(fakeShadeEl as unknown as Record<string, unknown>).props = { count: 1 }
|
|
542
542
|
;(fakeShadeEl as unknown as Record<string, unknown>).shadeChildren = undefined
|
|
543
543
|
|
|
544
544
|
const factory = vi.fn(() => fakeShadeEl as unknown as JSX.Element)
|
|
545
545
|
|
|
546
546
|
const old: VChild[] = [{ _brand: 'vnode', type: factory, props: { count: 1 }, children: [], _el: fakeShadeEl }]
|
|
547
|
-
// Simulate initial mount by manually appending
|
|
548
547
|
parent.appendChild(fakeShadeEl)
|
|
549
548
|
|
|
550
549
|
const updated: VChild[] = [{ _brand: 'vnode', type: factory, props: { count: 2 }, children: [] }]
|
|
@@ -554,13 +553,13 @@ describe('vnode', () => {
|
|
|
554
553
|
expect(fakeShadeEl.props).toEqual({ count: 2 })
|
|
555
554
|
})
|
|
556
555
|
|
|
557
|
-
it('should NOT call
|
|
556
|
+
it('should NOT call updateComponentSync when props are unchanged', () => {
|
|
558
557
|
const parent = document.createElement('div')
|
|
559
558
|
|
|
560
559
|
const fakeShadeEl = document.createElement('my-shade-2') as unknown as JSX.Element
|
|
561
560
|
const updateFn = vi.fn()
|
|
562
561
|
const props = { count: 1 }
|
|
563
|
-
;(fakeShadeEl as unknown as Record<string, unknown>).
|
|
562
|
+
;(fakeShadeEl as unknown as Record<string, unknown>).updateComponentSync = updateFn
|
|
564
563
|
;(fakeShadeEl as unknown as Record<string, unknown>).props = props
|
|
565
564
|
;(fakeShadeEl as unknown as Record<string, unknown>).shadeChildren = undefined
|
|
566
565
|
|
package/src/vnode.ts
CHANGED
|
@@ -446,7 +446,7 @@ const patchChild = (_parentEl: Node, oldChild: VChild, newChild: VChild): void =
|
|
|
446
446
|
patchProps(el, oldChild.props, newChild.props)
|
|
447
447
|
}
|
|
448
448
|
el.shadeChildren = newChild.children as unknown as ChildrenList
|
|
449
|
-
el.
|
|
449
|
+
;(el as unknown as { updateComponentSync: () => void }).updateComponentSync()
|
|
450
450
|
}
|
|
451
451
|
return
|
|
452
452
|
}
|