diff --git a/.agents/skills/data-client-react-testing/references/Fixtures.md b/.agents/skills/data-client-react-testing/references/Fixtures.md index 1ca3e8c00c37..210c61804467 100644 --- a/.agents/skills/data-client-react-testing/references/Fixtures.md +++ b/.agents/skills/data-client-react-testing/references/Fixtures.md @@ -27,7 +27,7 @@ export interface SuccessFixture< E extends EndpointInterface = EndpointInterface, > { readonly endpoint: E; - readonly args: Parameters; + readonly args: Readonly>; readonly response: | ResolveType | ((...args: Parameters) => ResolveType); @@ -62,7 +62,7 @@ export interface ErrorFixture { ```ts export interface ErrorFixture { readonly endpoint: E; - readonly args: Parameters; + readonly args: Readonly>; readonly response: any; readonly error: true; /** Number of milliseconds to wait before resolving */ diff --git a/.agents/skills/data-client-react-testing/references/MockResolver.md b/.agents/skills/data-client-react-testing/references/MockResolver.md index 0afbf3bf78a7..bc214e98d30f 100644 --- a/.agents/skills/data-client-react-testing/references/MockResolver.md +++ b/.agents/skills/data-client-react-testing/references/MockResolver.md @@ -64,7 +64,7 @@ const results = [ // fixture { endpoint: ArticleResource.getList, - args: [{ maxResults: 10 }], + args: [{ maxResults: 10 }] as const, response: [ { id: 5, diff --git a/.agents/skills/data-client-react-testing/references/unit-testing-components.md b/.agents/skills/data-client-react-testing/references/unit-testing-components.md index 580361af24ca..3f757a67d895 100644 --- a/.agents/skills/data-client-react-testing/references/unit-testing-components.md +++ b/.agents/skills/data-client-react-testing/references/unit-testing-components.md @@ -28,7 +28,7 @@ export default { full: [ { endpoint: ArticleResource.getList, - args: [{ maxResults: 10 }], + args: [{ maxResults: 10 }] as const, response: [ { id: 5, @@ -46,7 +46,7 @@ export default { }, { endpoint: ArticleResource.update, - args: [{ id: 532 }], + args: [{ id: 532 }] as const, response({ id }, body) { return { id, @@ -58,14 +58,14 @@ export default { empty: [ { endpoint: ArticleResource.getList, - args: [{ maxResults: 10 }], + args: [{ maxResults: 10 }] as const, response: [], }, ], error: [ { endpoint: ArticleResource.getList, - args: [{ maxResults: 10 }], + args: [{ maxResults: 10 }] as const, response: { message: 'Bad request', status: 400, name: 'Not Found' }, error: true, }, diff --git a/.agents/skills/data-client-rest/references/Fixtures.md b/.agents/skills/data-client-rest/references/Fixtures.md index 0b72fffd8011..196d7b0068ad 100644 --- a/.agents/skills/data-client-rest/references/Fixtures.md +++ b/.agents/skills/data-client-rest/references/Fixtures.md @@ -27,7 +27,7 @@ export interface SuccessFixture< E extends EndpointInterface = EndpointInterface, > { readonly endpoint: E; - readonly args: Parameters; + readonly args: Readonly>; readonly response: | ResolveType | ((...args: Parameters) => ResolveType); @@ -62,7 +62,7 @@ export interface ErrorFixture { ```ts export interface ErrorFixture { readonly endpoint: E; - readonly args: Parameters; + readonly args: Readonly>; readonly response: any; readonly error: true; /** Number of milliseconds to wait before resolving */ diff --git a/.agents/skills/data-client-rest/references/Fixtures.vue.md b/.agents/skills/data-client-rest/references/Fixtures.vue.md index cefb6692aa34..dbbe20761348 100644 --- a/.agents/skills/data-client-rest/references/Fixtures.vue.md +++ b/.agents/skills/data-client-rest/references/Fixtures.vue.md @@ -27,7 +27,7 @@ export interface SuccessFixture< E extends EndpointInterface = EndpointInterface, > { readonly endpoint: E; - readonly args: Parameters; + readonly args: Readonly>; readonly response: | ResolveType | ((...args: Parameters) => ResolveType); @@ -62,7 +62,7 @@ export interface ErrorFixture { ```ts export interface ErrorFixture { readonly endpoint: E; - readonly args: Parameters; + readonly args: Readonly>; readonly response: any; readonly error: true; /** Number of milliseconds to wait before resolving */ diff --git a/.agents/skills/data-client-vue-testing/references/Fixtures.md b/.agents/skills/data-client-vue-testing/references/Fixtures.md index ed542d8b7502..a52c67a6ee41 100644 --- a/.agents/skills/data-client-vue-testing/references/Fixtures.md +++ b/.agents/skills/data-client-vue-testing/references/Fixtures.md @@ -27,7 +27,7 @@ export interface SuccessFixture< E extends EndpointInterface = EndpointInterface, > { readonly endpoint: E; - readonly args: Parameters; + readonly args: Readonly>; readonly response: | ResolveType | ((...args: Parameters) => ResolveType); @@ -62,7 +62,7 @@ export interface ErrorFixture { ```ts export interface ErrorFixture { readonly endpoint: E; - readonly args: Parameters; + readonly args: Readonly>; readonly response: any; readonly error: true; /** Number of milliseconds to wait before resolving */ diff --git a/.changeset/fixture-args-readonly.md b/.changeset/fixture-args-readonly.md new file mode 100644 index 000000000000..e95a5a622954 --- /dev/null +++ b/.changeset/fixture-args-readonly.md @@ -0,0 +1,25 @@ +--- +'@data-client/core': patch +'@data-client/test': patch +'@data-client/react': patch +'@data-client/vue': patch +--- + +Fixture `args` accept readonly tuples + +Fixtures written with `args: [...] as const` now type-check when passed to [MockResolver](https://dataclient.io/docs/api/MockResolver), `renderDataHook()`, Vue's `renderDataCompose()` or `mockInitialState()`. Before, TypeScript rejected them with "The type 'readonly [...]' is 'readonly' and cannot be assigned to the mutable type", so you had to drop `as const` or cast. + +```ts +const fixtures = [ + { + endpoint: TodoResource.getList, + args: [{ userId: 1 }] as const, + response: [{ id: 1, title: 'Write tests', userId: 1 }], + }, +]; + +// Before: type error on `fixtures`. After: works as written + + +; +``` diff --git a/docs/core/api/Fixtures.md b/docs/core/api/Fixtures.md index 3e8c0d986819..7d725a493485 100644 --- a/docs/core/api/Fixtures.md +++ b/docs/core/api/Fixtures.md @@ -35,7 +35,7 @@ export interface SuccessFixture< E extends EndpointInterface = EndpointInterface, > { readonly endpoint: E; - readonly args: Parameters; + readonly args: Readonly>; readonly response: | ResolveType | ((...args: Parameters) => ResolveType); @@ -74,7 +74,7 @@ export interface ErrorFixture { ```ts export interface ErrorFixture { readonly endpoint: E; - readonly args: Parameters; + readonly args: Readonly>; readonly response: any; readonly error: true; /** Number of milliseconds to wait before resolving */ diff --git a/docs/core/api/MockResolver.md b/docs/core/api/MockResolver.md index 170bfc2b22dc..887f88286cc5 100644 --- a/docs/core/api/MockResolver.md +++ b/docs/core/api/MockResolver.md @@ -70,7 +70,7 @@ const results = [ // fixture { endpoint: ArticleResource.getList, - args: [{ maxResults: 10 }], + args: [{ maxResults: 10 }] as const, response: [ { id: 5, diff --git a/docs/core/guides/unit-testing-components.md b/docs/core/guides/unit-testing-components.md index 9060eca55a75..af2676aadf14 100644 --- a/docs/core/guides/unit-testing-components.md +++ b/docs/core/guides/unit-testing-components.md @@ -31,7 +31,7 @@ export default { full: [ { endpoint: ArticleResource.getList, - args: [{ maxResults: 10 }], + args: [{ maxResults: 10 }] as const, response: [ { id: 5, @@ -49,7 +49,7 @@ export default { }, { endpoint: ArticleResource.update, - args: [{ id: 532 }], + args: [{ id: 532 }] as const, response({ id }, body) { return { id, @@ -61,14 +61,14 @@ export default { empty: [ { endpoint: ArticleResource.getList, - args: [{ maxResults: 10 }], + args: [{ maxResults: 10 }] as const, response: [], }, ], error: [ { endpoint: ArticleResource.getList, - args: [{ maxResults: 10 }], + args: [{ maxResults: 10 }] as const, response: { message: 'Bad request', status: 400, name: 'Not Found' }, error: true, }, diff --git a/packages/core/src/mock/fixtureTypes.ts b/packages/core/src/mock/fixtureTypes.ts index 40e6e2b7f292..7f408cc192e0 100644 --- a/packages/core/src/mock/fixtureTypes.ts +++ b/packages/core/src/mock/fixtureTypes.ts @@ -9,7 +9,7 @@ export interface SuccessFixtureEndpoint< E extends EndpointInterface & { update?: Updater } = EndpointInterface, > { readonly endpoint: E; - readonly args: Parameters; + readonly args: Readonly>; readonly response: ResolveType | ((...args: Parameters) => ResolveType); readonly error?: false; @@ -84,7 +84,7 @@ export interface ErrorFixtureEndpoint< E extends EndpointInterface & { update?: Updater } = EndpointInterface, > { readonly endpoint: E; - readonly args: Parameters; + readonly args: Readonly>; readonly response: any; readonly error: true; /** Number of miliseconds to wait before resolving */ diff --git a/packages/core/src/mock/mockState.ts b/packages/core/src/mock/mockState.ts index f7ce5d706b15..b7e995f6c815 100644 --- a/packages/core/src/mock/mockState.ts +++ b/packages/core/src/mock/mockState.ts @@ -32,7 +32,7 @@ export default function mockInitialState( function dispatchFixture( fixture: Fixture, - args: any[], + args: readonly any[], controller: Controller, fetchedAt?: number, ) { diff --git a/packages/test/src/fixtureTypes.ts b/packages/test/src/fixtureTypes.ts index a7bb6d91c075..ca96e571e557 100644 --- a/packages/test/src/fixtureTypes.ts +++ b/packages/test/src/fixtureTypes.ts @@ -9,7 +9,7 @@ export interface SuccessFixtureEndpoint< E extends EndpointInterface & { update?: Updater } = EndpointInterface, > { readonly endpoint: E; - readonly args: Parameters; + readonly args: Readonly>; readonly response: ResolveType | ((...args: Parameters) => ResolveType); readonly error?: false; @@ -84,7 +84,7 @@ export interface ErrorFixtureEndpoint< E extends EndpointInterface & { update?: Updater } = EndpointInterface, > { readonly endpoint: E; - readonly args: Parameters; + readonly args: Readonly>; readonly response: any; readonly error: true; /** Number of miliseconds to wait before resolving */ diff --git a/scripts/typeperf/README.md b/scripts/typeperf/README.md index f11b529d85f3..5a493a2ff659 100644 --- a/scripts/typeperf/README.md +++ b/scripts/typeperf/README.md @@ -42,6 +42,7 @@ When a count drops more than 10% below its budget, the check says so; re-record | setValues | `ctrl.set()` values on a Union, Collection and big Entity | | setUpdaters | `ctrl.set()` updaters spreading `prev` on a 30-member Union | | redux | `prepareStore()`'s store passed to react-redux's `Provider` and `ExternalDataProvider` | +| fixtures | 100 sets of `@data-client/test` fixtures (mutable and `as const` args) and interceptors passed to `MockResolver` and `mockInitialState()` | | setInvalidate | `ctrl.set()` single and batch Invalidate rows on a 30-member Union and big Entity | | patheq | path types against [patheq/orig.ts](./patheq/orig.ts) on fixed-seed fuzzed paths | diff --git a/scripts/typeperf/budget.json b/scripts/typeperf/budget.json index 82ff9d31fac0..bdc1e058b102 100644 --- a/scripts/typeperf/budget.json +++ b/scripts/typeperf/budget.json @@ -12,6 +12,7 @@ "setUpdaters": 66031, "redux": 12986, "setInvalidate": 7325, + "fixtures": 76837, "patheq": 934395 }, "typescript": "7.0.2" diff --git a/scripts/typeperf/gen.mjs b/scripts/typeperf/gen.mjs index 3a3913a9742c..85c3598b1ee9 100644 --- a/scripts/typeperf/gen.mjs +++ b/scripts/typeperf/gen.mjs @@ -267,7 +267,40 @@ declare function provide(props: ProviderProps { + let s = `import { Entity, resource } from '@data-client/rest'; +import { MockResolver, mockInitialState } from '@data-client/test'; +import type { Fixture, Interceptor } from '@data-client/test'; +`; + // a few resources, so the count tracks Fixture/Interceptor/MockResolver typing rather than resource() + const R = 4; + for (let r = 0; r < R; r++) + s += `export class E${r} extends Entity {\n id = '';\n${fields(20)}\n static key = 'E${r}';\n} +export const R${r} = resource({ path: '/e${r}/:id', schema: E${r}, searchParams: {} as { q?: string } | undefined }); +`; + for (let i = 0; i < 100 * N; i++) { + const r = `R${i % R}`; + s += `export const fixtures${i} = [ + { endpoint: ${r}.get, args: [{ id: '${i}' }] as const, response: { id: '${i}', f0: 'a' } }, + { endpoint: ${r}.getList, args: [{ q: 'x${i}' }], response: [{ id: '${i}' }] }, + { endpoint: ${r}.update, args: [{ id: '${i}' }, { f0: 'b' }] as const, response: { id: '${i}', f0: 'b' } }, + { endpoint: ${r}.delete, args: [{ id: '${i}' }] as const, response: { message: 'nope' }, error: true }, +]; +export const typed${i}: Fixture = { endpoint: ${r}.get, args: [{ id: '${i}' }] as const, response: { id: '${i}' } }; +export const interceptor${i}: Interceptor<{ n: number }> = { + endpoint: ${r}.get, + response({ id }) { return { id, f0: \`\${this.n++}\` }; }, + delay: () => ${i}, +}; +MockResolver({ fixtures: [...fixtures${i}, typed${i}, interceptor${i}], getInitialInterceptorData: () => ({ n: 0 }), children: null }); +mockInitialState([...fixtures${i}, typed${i}]); +`; + } + return s; +}; + +// 12. path types vs their frozen pre-#4173 implementation (a type error is a mismatch) scen.patheq = patheq; /** Writes scenarios// for the named fixtures (all when empty); returns the names */ diff --git a/website/src/components/Playground/editor-types/@data-client/core/mock.d.ts b/website/src/components/Playground/editor-types/@data-client/core/mock.d.ts index 2c366daa543c..2e1bd81d4e11 100644 --- a/website/src/components/Playground/editor-types/@data-client/core/mock.d.ts +++ b/website/src/components/Playground/editor-types/@data-client/core/mock.d.ts @@ -1193,7 +1193,7 @@ interface SuccessFixtureEndpoint { readonly endpoint: E; - readonly args: Parameters; + readonly args: Readonly>; readonly response: ResolveType | ((...args: Parameters) => ResolveType); readonly error?: false; /** Number of miliseconds to wait before resolving */ @@ -1252,7 +1252,7 @@ interface ErrorFixtureEndpoint { readonly endpoint: E; - readonly args: Parameters; + readonly args: Readonly>; readonly response: any; readonly error: true; /** Number of miliseconds to wait before resolving */ diff --git a/website/src/components/Playground/editor-types/@data-client/test.d.ts b/website/src/components/Playground/editor-types/@data-client/test.d.ts index bba4e6f3a4ae..878c1a916d16 100644 --- a/website/src/components/Playground/editor-types/@data-client/test.d.ts +++ b/website/src/components/Playground/editor-types/@data-client/test.d.ts @@ -11,7 +11,7 @@ interface SuccessFixtureEndpoint { readonly endpoint: E; - readonly args: Parameters; + readonly args: Readonly>; readonly response: ResolveType | ((...args: Parameters) => ResolveType); readonly error?: false; /** Number of miliseconds to wait before resolving */ @@ -70,7 +70,7 @@ interface ErrorFixtureEndpoint { readonly endpoint: E; - readonly args: Parameters; + readonly args: Readonly>; readonly response: any; readonly error: true; /** Number of miliseconds to wait before resolving */