Skip to content

Commit 20b795f

Browse files
added tests for hooks lifecycle and edge cases
1 parent 41f00b8 commit 20b795f

File tree

4 files changed

+206
-7
lines changed

4 files changed

+206
-7
lines changed

src/components/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const App = (props: any) => {
1616
<button onClick={() => visible.update((prev) => !prev)}>
1717
Show/Hide
1818
</button>
19-
{() => (visible.value ? <TimerComponent /> : "Hidden")}
19+
{() => (visible.value ? <PerformanceTest /> : "Hidden")}
2020
</>
2121
);
2222
// return <StylesTest />;

src/components/Performance.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,20 @@ const PerformanceTest = () => {
1818

1919
// Simulating add item after 1 second
2020
setTimeout(() => {
21-
items.value.push(`Item ${items.value.length + 1}`);
21+
items.update((items) => items.push(`Item ${items.length + 1}`));
2222
console.timeEnd("Update 1");
2323
console.time("Update 2");
2424
// Simulating sort after another second
2525
setTimeout(() => {
26-
sortAsc.value = !sortAsc.value;
26+
sortAsc.update((prev) => !prev);
2727
console.timeEnd("Update 2");
2828
}, 1000);
2929
}, 1000);
3030
}, 1000);
3131
});
3232

3333
setTimeout(() => {
34-
items.value = [];
34+
items.update([]);
3535
// items.value[0] = "item modified";
3636
// setTimeout(() => {
3737
// items.value = Array.from({ length: 1000 }, (_, i) => `Item ${i}`);
@@ -40,7 +40,7 @@ const PerformanceTest = () => {
4040

4141
// Sorting the list based on the selected order
4242
const sortedItems = computed(() => {
43-
return items.value.sort((a, b) => {
43+
return [...items.value].sort((a, b) => {
4444
if (sortAsc.value) return a.localeCompare(b);
4545
return b.localeCompare(a);
4646
});

src/rendering/render.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,9 @@ function workLoop(deadline: IdleDeadline) {
7373
}
7474

7575
if (elements.length == 0) {
76-
processEffectQueue();
7776
commitRootFragment();
77+
78+
processEffectQueue();
7879
return;
7980
}
8081
requestIdleCallback(workLoop);

src/tests/rendering/functionalComponents.test.tsx

Lines changed: 199 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ import {
77
} from "../../rendering/functionalComponents";
88
import * as rendering from "../../rendering/render";
99

10-
import { computed, createEffect, createSignal } from "../../signals/signal";
10+
import {
11+
computed,
12+
createEffect,
13+
createRef,
14+
createSignal,
15+
} from "../../signals/signal";
1116

1217
vi.stubGlobal("requestIdleCallback", (cb) => {
1318
queueMicrotask(() => cb({ timeRemaining: () => 2 }));
@@ -98,3 +103,196 @@ describe("Functional Components life cycle", () => {
98103
expect(computedCount).toBe(2);
99104
});
100105
});
106+
107+
describe("hooks lifecycle", () => {
108+
it("create effect should run synchrounously when outside of a FC", async () => {
109+
let count = 0;
110+
createEffect(() => {
111+
count++;
112+
});
113+
await Promise.resolve();
114+
expect(count).toBe(1);
115+
});
116+
it("computed should run synchrounously when outside of a FC or inside and should run only once", async () => {
117+
let fn = vi.fn();
118+
119+
const signal = createSignal(1);
120+
const comp = computed(() => {
121+
fn();
122+
return signal.value * 2;
123+
});
124+
expect(fn).toHaveBeenCalledTimes(1);
125+
expect(comp.value).toBe(2);
126+
127+
// inside FC
128+
const FC = () => {
129+
const signal = createSignal(1);
130+
const comp = computed(() => {
131+
fn();
132+
return signal.value * 2;
133+
});
134+
return <div></div>;
135+
};
136+
137+
const fiber = (
138+
<div>
139+
<FC />
140+
</div>
141+
);
142+
143+
createFiber(fiber);
144+
commitFiber(fiber);
145+
146+
expect(fn).toHaveBeenCalledTimes(2);
147+
await Promise.resolve();
148+
expect(fn).toHaveBeenCalledTimes(2);
149+
});
150+
151+
it("create effect should run asynchronously when inside of a FC, in render and commitFiber both", async () => {
152+
let fn1 = vi.fn();
153+
let fn2 = vi.fn();
154+
// render
155+
const FC = () => {
156+
createEffect(() => {
157+
fn1();
158+
});
159+
fn2();
160+
return <div></div>;
161+
};
162+
163+
const container = document.createElement("div");
164+
165+
rendering.render(<FC />, container);
166+
await Promise.resolve();
167+
168+
expect(container.innerHTML).toBe("<div></div>");
169+
expect(fn1).toHaveBeenCalledAfter(fn2);
170+
171+
// commitFiber
172+
const fiber = (
173+
<div>
174+
<FC />
175+
</div>
176+
);
177+
178+
createFiber(fiber);
179+
commitFiber(fiber);
180+
181+
expect(container.innerHTML).toBe("<div></div>");
182+
await Promise.resolve();
183+
184+
expect(fn1).toHaveBeenCalledTimes(2);
185+
expect(fn2).toHaveBeenCalledTimes(2);
186+
expect(fn1).toHaveBeenCalledAfter(fn2);
187+
});
188+
189+
// this is just a test to make sure that this does not cause an infinite loop and it is an anti-pattern
190+
it("create effect should run twice not infinitely when inside a FC with self dependency", async () => {
191+
let fn1 = vi.fn();
192+
let fn2 = vi.fn();
193+
// render
194+
const FC = () => {
195+
const signal = createSignal<number>(1);
196+
createEffect(() => {
197+
signal.value;
198+
// if the signal is registered first then only it will be called twice
199+
signal.update((prev) => prev + 1);
200+
fn1();
201+
});
202+
fn2();
203+
return <div></div>;
204+
};
205+
206+
const fiber = (
207+
<div>
208+
<FC />
209+
</div>
210+
);
211+
212+
createFiber(fiber);
213+
commitFiber(fiber);
214+
215+
await Promise.resolve();
216+
217+
expect(fn1).toHaveBeenCalledTimes(1);
218+
await Promise.resolve();
219+
220+
// if the signal is registered first then only it will be called twice
221+
expect(fn1).toHaveBeenCalledTimes(2);
222+
});
223+
224+
it("createEffect with a ref should always have the ref defined", async () => {
225+
// render
226+
227+
const fn = vi.fn();
228+
const FC = () => {
229+
const ref = createRef();
230+
231+
createEffect(() => {
232+
fn(ref.current);
233+
});
234+
235+
return (
236+
<div>
237+
<p>Child1</p>
238+
<div>
239+
Child2
240+
<p>Subchild 2</p>
241+
<div>
242+
Subchild 3<p ref={ref}>Subchild 4</p>
243+
</div>
244+
</div>
245+
</div>
246+
);
247+
};
248+
249+
const fiber = (
250+
<div>
251+
<FC />
252+
</div>
253+
);
254+
255+
createFiber(fiber);
256+
commitFiber(fiber);
257+
258+
expect(fn).toHaveBeenCalledTimes(0);
259+
await Promise.resolve();
260+
261+
expect(fn).toHaveBeenCalledWith(expect.any(HTMLParagraphElement));
262+
});
263+
it("createEffect with a ref should always have the ref defined, with render also", async () => {
264+
// render
265+
266+
const fn = vi.fn();
267+
const FC = () => {
268+
const ref = createRef();
269+
270+
createEffect(() => {
271+
fn(ref.current);
272+
});
273+
274+
return (
275+
<div>
276+
<p>Child1</p>
277+
<div>
278+
Child2
279+
<p>Subchild 2</p>
280+
<div>
281+
Subchild 3<p ref={ref}>Subchild 4</p>
282+
</div>
283+
</div>
284+
</div>
285+
);
286+
};
287+
288+
rendering.render(<FC />, document.body);
289+
290+
expect(document.body.innerHTML).toBe("");
291+
expect(fn).toHaveBeenCalledTimes(0);
292+
await Promise.resolve();
293+
expect(document.body.innerHTML).toBe(
294+
"<div><p>Child1</p><div>Child2<p>Subchild 2</p><div>Subchild 3<p>Subchild 4</p></div></div></div>"
295+
);
296+
expect(fn).toHaveBeenCalledWith(expect.any(HTMLParagraphElement));
297+
});
298+
});

0 commit comments

Comments
 (0)