From 302aee0df9bf6ba1d85b0c82eedbe32c453d2428 Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Mon, 4 Mar 2024 00:10:50 +0100 Subject: [PATCH] feat(angular-query): lazy signal initializer test --- .../lazy-signal-initializer.test.ts | 136 ++++++++++++++++++ .../lazy-signal-initializer.ts | 23 +-- 2 files changed, 148 insertions(+), 11 deletions(-) create mode 100644 packages/angular-query-experimental/src/__tests__/util/lazy-signal-initializer/lazy-signal-initializer.test.ts diff --git a/packages/angular-query-experimental/src/__tests__/util/lazy-signal-initializer/lazy-signal-initializer.test.ts b/packages/angular-query-experimental/src/__tests__/util/lazy-signal-initializer/lazy-signal-initializer.test.ts new file mode 100644 index 0000000000..afdd71e3f4 --- /dev/null +++ b/packages/angular-query-experimental/src/__tests__/util/lazy-signal-initializer/lazy-signal-initializer.test.ts @@ -0,0 +1,136 @@ +import { describe, expect, test } from 'vitest' +import { + Component, + type Signal, + type WritableSignal, + effect, + input, + signal, +} from '@angular/core' +import { TestBed } from '@angular/core/testing' +import { lazySignalInitializer } from '../../../util/lazy-signal-initializer/lazy-signal-initializer' +import { setFixtureSignalInputs } from '../../test-utils' + +async function flushQueue() { + await new Promise(setImmediate) +} + +describe('lazySignalInitializer', () => { + test('should init lazily in next tick when not accessing manually', async () => { + const mockFn = vi.fn() + + TestBed.runInInjectionContext(() => { + lazySignalInitializer(() => { + mockFn() + return signal(true) + }) + }) + + expect(mockFn).not.toHaveBeenCalled() + + await new Promise(setImmediate) + + expect(mockFn).toHaveBeenCalled() + }) + + test('should init eagerly accessing manually', async () => { + const mockFn = vi.fn() + + TestBed.runInInjectionContext(() => { + const lazySignal = lazySignalInitializer(() => { + mockFn() + return signal(true) + }) + + lazySignal() + }) + + expect(mockFn).toHaveBeenCalled() + }) + + test('should init lazily and only once', async () => { + const initCallFn = vi.fn() + const registerEffectValue = vi.fn<[number]>() + + let value!: Signal + const outerSignal = signal(0) + let innerSignal!: WritableSignal + + TestBed.runInInjectionContext(() => { + value = lazySignalInitializer(() => { + initCallFn() + innerSignal = signal(0) + + void outerSignal() + + return innerSignal + }) + + effect(() => registerEffectValue(value())) + }) + + expect(initCallFn).toHaveBeenCalledTimes(0) + expect(outerSignal).toBeDefined() + expect(innerSignal).not.toBeDefined() + + await flushQueue() + + expect(outerSignal).toBeDefined() + expect(innerSignal).toBeDefined() + + expect(initCallFn).toHaveBeenCalledTimes(1) + + innerSignal.set(1) + await flushQueue() + outerSignal.set(2) + await flushQueue() + + expect(initCallFn).toHaveBeenCalledTimes(1) + expect(registerEffectValue).toHaveBeenCalledTimes(2) + }) + + test('should init lazily', async () => { + @Component({ + standalone: true, + template: `{{ subscribed }}`, + }) + class Test { + subscribed = false + + lazySignal = lazySignalInitializer(() => { + this.subscribed = true + return signal('value') + }) + } + + const fixture = TestBed.createComponent(Test) + const { debugElement } = fixture + fixture.detectChanges() + + expect(debugElement.nativeElement.textContent).toBe('false') + + await new Promise(setImmediate) + + fixture.detectChanges() + + expect(debugElement.nativeElement.textContent).toBe('true') + }) + + test('should support required signal input', () => { + @Component({ + standalone: true, + template: `{{ subscribed }}`, + }) + class Test { + readonly title = input.required() + subscribed = false + + lazySignal = lazySignalInitializer(() => { + return signal(this.title()) + }) + } + + const fixture = TestBed.createComponent(Test) + setFixtureSignalInputs(fixture, { title: 'newValue' }) + }) +}) diff --git a/packages/angular-query-experimental/src/util/lazy-signal-initializer/lazy-signal-initializer.ts b/packages/angular-query-experimental/src/util/lazy-signal-initializer/lazy-signal-initializer.ts index 52998030e8..eb33083aab 100644 --- a/packages/angular-query-experimental/src/util/lazy-signal-initializer/lazy-signal-initializer.ts +++ b/packages/angular-query-experimental/src/util/lazy-signal-initializer/lazy-signal-initializer.ts @@ -1,4 +1,10 @@ -import { Injector, type Signal, computed, inject } from '@angular/core' +import { + Injector, + type Signal, + computed, + inject, + untracked, +} from '@angular/core' type SignalInitializerFn = (injector: Injector) => Signal @@ -9,19 +15,14 @@ export function lazySignalInitializer( let source: Signal | null = null - const initializeObject = () => { + const unwrapSignal = () => { if (!source) { - source = initializerFn(injector) + source = untracked(() => initializerFn(injector)) } - return source + return source() } - queueMicrotask(() => initializeObject()) + queueMicrotask(() => unwrapSignal()) - return computed(() => { - if (!source) { - source = initializeObject() - } - return source() - }) + return computed(unwrapSignal) }