Skip to content

Commit

Permalink
feat(angular-query): lazy signal initializer test
Browse files Browse the repository at this point in the history
  • Loading branch information
riccardoperra committed Mar 3, 2024
1 parent bbeb63b commit 302aee0
Show file tree
Hide file tree
Showing 2 changed files with 148 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -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<number>
const outerSignal = signal(0)
let innerSignal!: WritableSignal<number>

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<string>()
subscribed = false

lazySignal = lazySignalInitializer(() => {
return signal(this.title())
})
}

const fixture = TestBed.createComponent(Test)
setFixtureSignalInputs(fixture, { title: 'newValue' })
})
})
Original file line number Diff line number Diff line change
@@ -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<T> = (injector: Injector) => Signal<T>

Expand All @@ -9,19 +15,14 @@ export function lazySignalInitializer<T>(

let source: Signal<T> | 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)
}

0 comments on commit 302aee0

Please sign in to comment.