Skip to content

[WIP] Angular injectQueries fixes #8690

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/angular-query-experimental/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"@angular/platform-browser": "^19.1.0-next.0",
"@angular/platform-browser-dynamic": "^19.1.0-next.0",
"@microsoft/api-extractor": "^7.48.1",
"@testing-library/angular": "^17.3.6",
"eslint-plugin-jsdoc": "^50.5.0",
"npm-run-all": "^4.1.5",
"tsup": "8.0.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { describe, expectTypeOf, it } from 'vitest'
import { skipToken } from '..'
import { injectQueries } from '../inject-queries'
import { queryOptions } from '../query-options'
import type { CreateQueryOptions, CreateQueryResult, OmitKeyof } from '..'
import type { Signal } from '@angular/core'

describe('InjectQueries config object overload', () => {
it('TData should always be defined when initialData is provided as an object', () => {
const query1 = {
queryKey: ['key1'],
queryFn: () => {
return {
wow: true,
}
},
initialData: {
wow: false,
},
}

const query2 = {
queryKey: ['key2'],
queryFn: () => 'Query Data',
initialData: 'initial data',
}

const query3 = {
queryKey: ['key2'],
queryFn: () => 'Query Data',
}

const queryResults = injectQueries(() => ({
queries: [query1, query2, query3],
}))

const query1Data = queryResults()[0].data()
const query2Data = queryResults()[1].data()
const query3Data = queryResults()[2].data()

expectTypeOf(query1Data).toEqualTypeOf<{ wow: boolean }>()
expectTypeOf(query2Data).toEqualTypeOf<string>()
expectTypeOf(query3Data).toEqualTypeOf<string | undefined>()
})

it('TData should be defined when passed through queryOptions', () => {
const options = queryOptions({
queryKey: ['key'],
queryFn: () => {
return {
wow: true,
}
},
initialData: {
wow: true,
},
})
const queryResults = injectQueries(() => ({ queries: [options] }))

const data = queryResults()[0].data()

expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>()
})

it('should be possible to define a different TData than TQueryFnData using select with queryOptions spread into injectQuery', () => {
const query1 = queryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve(1),
select: (data) => data > 1,
})

const query2 = {
queryKey: ['key'],
queryFn: () => Promise.resolve(1),
select: (data: number) => data > 1,
}

const queryResults = injectQueries(() => ({ queries: [query1, query2] }))
const query1Data = queryResults()[0].data()
const query2Data = queryResults()[1].data()

expectTypeOf(query1Data).toEqualTypeOf<boolean | undefined>()
expectTypeOf(query2Data).toEqualTypeOf<boolean | undefined>()
})

it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => {
const queryResults = injectQueries(() => ({
queries: [
{
queryKey: ['key'],
queryFn: () => {
return {
wow: true,
}
},
initialData: () => undefined as { wow: boolean } | undefined,
},
],
}))

const data = queryResults()[0].data()

expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>()
})

describe('custom injectable', () => {
it('should allow custom hooks using UseQueryOptions', () => {
type Data = string

const injectCustomQueries = (
options?: OmitKeyof<CreateQueryOptions<Data>, 'queryKey' | 'queryFn'>,
) => {
return injectQueries(() => ({
queries: [
{
...options,
queryKey: ['todos-key'],
queryFn: () => Promise.resolve('data'),
},
],
}))
}

const queryResults = injectCustomQueries()
const data = queryResults()[0].data()

expectTypeOf(data).toEqualTypeOf<Data | undefined>()
})
})

it('TData should have correct type when conditional skipToken is passed', () => {
const queryResults = injectQueries(() => ({
queries: [
{
queryKey: ['withSkipToken'],
queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5),
},
],
}))

const firstResult = queryResults()[0]

expectTypeOf(firstResult).toEqualTypeOf<CreateQueryResult<number, Error>>()
expectTypeOf(firstResult.data()).toEqualTypeOf<number | undefined>()
})

it('should return correct data for dynamic queries with mixed result types', () => {
const Queries1 = {
get: () =>
queryOptions({
queryKey: ['key1'],
queryFn: () => Promise.resolve(1),
}),
}
const Queries2 = {
get: () =>
queryOptions({
queryKey: ['key2'],
queryFn: () => Promise.resolve(true),
}),
}

const queries1List = [1, 2, 3].map(() => ({ ...Queries1.get() }))
const result = injectQueries(() => ({
queries: [...queries1List, { ...Queries2.get() }],
}))

expectTypeOf(result).branded.toEqualTypeOf<
Signal<
[
...Array<CreateQueryResult<number, Error>>,
CreateQueryResult<boolean, Error>,
]
>
>()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { afterEach, describe, expect, it } from 'vitest'
import { render, waitFor } from '@testing-library/angular'
import {
Component,
effect,
provideExperimentalZonelessChangeDetection,
} from '@angular/core'
import { TestBed } from '@angular/core/testing'
import { QueryClient, injectQueries, provideTanStackQuery } from '..'
import { evaluateSignals, queryKey } from './test-utils'

let queryClient: QueryClient

beforeEach(() => {
queryClient = new QueryClient()
// vi.useFakeTimers()
TestBed.configureTestingModule({
providers: [
provideExperimentalZonelessChangeDetection(),
provideTanStackQuery(queryClient),
],
})
})

afterEach(() => {
// vi.useRealTimers()
})

describe('useQueries', () => {
it('should return the correct states', async () => {
const key1 = queryKey()
const key2 = queryKey()
const results: Array<Array<Record<string, any>>> = []

@Component({
template: `
<div>
<div>
data1: {{ toString(result()[0].data() ?? 'null') }}, data2:
{{ toString(result()[1].data() ?? 'null') }}
</div>
</div>
`,
})
class Page {
toString(val: any) {
return String(val)
}
result = injectQueries(() => ({
queries: [
{
queryKey: key1,
queryFn: async () => {
await new Promise((r) => setTimeout(r, 10))
return 1
},
},
{
queryKey: key2,
queryFn: async () => {
await new Promise((r) => setTimeout(r, 100))
return 2
},
},
],
}))

_pushResults = effect(() => {
results.push(this.result().map(evaluateSignals))
})
}

const rendered = await render(Page)

await waitFor(() => rendered.getByText('data1: 1, data2: 2'))

expect(results.length).toBe(3)
expect(results[0]).toMatchObject([{ data: undefined }, { data: undefined }])
expect(results[1]).toMatchObject([{ data: 1 }, { data: undefined }])
expect(results[2]).toMatchObject([{ data: 1 }, { data: 2 }])
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export function errorMutator(_parameter?: unknown): Promise<Error> {
}

// Evaluate all signals on an object and return the result
function evaluateSignals<T extends Record<string, any>>(
export function evaluateSignals<T extends Record<string, any>>(
obj: T,
): { [K in keyof T]: ReturnType<T[K]> } {
const result: Partial<{ [K in keyof T]: ReturnType<T[K]> }> = {}
Expand Down
Loading