diff --git a/.agents/skills/changeset/no-agent-impact.json b/.agents/skills/changeset/no-agent-impact.json index ad171c3393f5..40d9c27dc69a 100644 --- a/.agents/skills/changeset/no-agent-impact.json +++ b/.agents/skills/changeset/no-agent-impact.json @@ -1,3 +1,4 @@ { - "drop-typescript-3": "Agents don't install or keep TypeScript 3.x, so no realistic task hits the old behavior." + "drop-typescript-3": "Agents don't install or keep TypeScript 3.x, so no realistic task hits the old behavior.", + "test-mock-reexports": "Agent-written test code and imports are the same before and after; only install-time peer resolution changes, for projects pinned to @data-client/react 0.14 or older." } diff --git a/.changeset/test-mock-reexports.md b/.changeset/test-mock-reexports.md new file mode 100644 index 000000000000..c6769e441f5b --- /dev/null +++ b/.changeset/test-mock-reexports.md @@ -0,0 +1,16 @@ +--- +'@data-client/test': minor +--- + +BREAKING: `@data-client/test` requires `@data-client/react` 0.15 or later + +Support for `@data-client/react` 0.12 through 0.14 is dropped, since `@data-client/test` imports +`@data-client/react/mock`, which first shipped in 0.15. Upgrade `@data-client/react` along with `@data-client/test`. + +```diff title="package.json" +- "@data-client/react": "^0.14.0", ++ "@data-client/react": "^0.19.0", +``` + +`Fixture`, `Interceptor`, and the other fixture types from `@data-client/test` are now the same types that +`@data-client/react/mock` exports, so fixtures typed with either import work everywhere. diff --git a/packages/test/package.json b/packages/test/package.json index 69a3030f9f4e..f9ca432dd305 100644 --- a/packages/test/package.json +++ b/packages/test/package.json @@ -109,7 +109,7 @@ "@testing-library/react": "^16.0.0" }, "peerDependencies": { - "@data-client/react": "^0.12.15 || ^0.13.0 || ^0.14.0 || ^0.15.0 || ^0.16.0 || ^0.17.0 || ^0.18.0", + "@data-client/react": "^0.15.0 || ^0.16.0 || ^0.17.0 || ^0.18.0", "@testing-library/react-hooks": "^8.0.0", "@testing-library/react-native": "^13.0.0 || ^14.0.0", "@types/react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-0 || ^19.0.0", diff --git a/packages/test/src/MockController.ts b/packages/test/src/MockController.ts deleted file mode 100644 index 16403d138400..000000000000 --- a/packages/test/src/MockController.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { - actionTypes, - Controller, - DataClientDispatch, - GenericDispatch, -} from '@data-client/react'; - -import { collapseFixture } from './collapseFixture.js'; -import { createFixtureMap } from './createFixtureMap.js'; -import type { Fixture, Interceptor } from './fixtureTypes.js'; -import { MockProps } from './mockTypes.js'; - -export function MockController< - TBase extends new (...args: any[]) => Controller, - T, ->( - Base: TBase, - { - fixtures = [], - getInitialInterceptorData = () => ({}) as any, - }: MockProps, -): TBase { - const [fixtureMap, interceptors] = createFixtureMap(fixtures); - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return class MockedController< - D extends GenericDispatch = DataClientDispatch, - > extends (Base as typeof Controller) { - // legacy compatibility (re-declaration) - // TODO: drop when drop support for destructuring (0.14 and below) - declare protected _dispatch: D; - - fixtureMap: Map = fixtureMap; - interceptors: Interceptor[] = interceptors; - interceptorData: T = getInitialInterceptorData(); - - constructor(...args: any[]) { - super(...args); - - // legacy compatibility - // TODO: drop when drop support for destructuring (0.14 and below) - if (!this._dispatch) { - this._dispatch = (args[0] as any).dispatch; - } - } - - // legacy compatibility - we need this to work with 0.14 and below as they do not have this setter - // TODO: drop when drop support for destructuring (0.14 and below) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - set dispatch(dispatch: D) { - this._dispatch = dispatch; - } - - get dispatch(): D { - return ((action: Parameters[0]): Promise => { - // support legacy that has _TYPE suffix - if (action.type === (actionTypes.FETCH ?? actionTypes.FETCH_TYPE)) { - // eslint-disable-next-line prefer-const - let { key, args } = action; - let fixture: Fixture | Interceptor | undefined; - if (this.fixtureMap.has(key)) { - fixture = this.fixtureMap.get(key) as Fixture; - if (!args) args = fixture.args; - // exact matches take priority; now test ComputedFixture - } else { - for (const cfix of this.interceptors) { - if (cfix.endpoint.testKey(key)) { - fixture = cfix; - break; - } - } - } - // we have a match - if (fixture !== undefined) { - const replacedAction: typeof action = { - ...action, - }; - const delayMs = - typeof fixture.delay === 'function' ? - fixture.delay(...(args as any)) - : (fixture.delay ?? 0); - - if ('fetchResponse' in fixture) { - const { fetchResponse } = fixture; - fixture = { - endpoint: fixture.endpoint, - response(...args) { - const endpoint = (action.endpoint as any).extend({ - fetchResponse: (input: RequestInfo, init: RequestInit) => { - const ret = fetchResponse.call(this, input, init); - return Promise.resolve( - new Response(JSON.stringify(ret), { - status: 200, - headers: new Headers({ - 'Content-Type': 'application/json', - }), - }), - ); - }, - }); - return (endpoint as any)(...args); - }, - }; - } - const fetch = async () => { - if (!fixture) { - throw new Error('No fixture found'); - } - // delayCollapse determines when the fixture function is 'collapsed' (aka 'run') - // collapsed: https://en.wikipedia.org/wiki/Copenhagen_interpretation - if (fixture.delayCollapse) { - await new Promise(resolve => setTimeout(resolve, delayMs)); - } - const result = await collapseFixture( - fixture as any, - args as any, - this.interceptorData, - ); - if (!fixture.delayCollapse && delayMs) { - await new Promise(resolve => setTimeout(resolve, delayMs)); - } - if (result.error) { - throw result.response; - } - return result.response; - }; - if (typeof (replacedAction.endpoint as any).extend === 'function') { - replacedAction.endpoint = (replacedAction.endpoint as any).extend( - { - fetch, - }, - ); - } else { - // TODO: full testing of this - replacedAction.endpoint = fetch as any; - (replacedAction.endpoint as any).__proto__ = action.endpoint; - } - - // TODO: make super.dispatch (once we drop support for destructuring) - return this._dispatch(replacedAction); - } - } - // TODO: make super.dispatch (once we drop support for destructuring) - return this._dispatch(action); - }) as any; - } - }; -} diff --git a/packages/test/src/MockResolver.tsx b/packages/test/src/MockResolver.tsx index 404f47f7776e..6fc2bfb16f16 100644 --- a/packages/test/src/MockResolver.tsx +++ b/packages/test/src/MockResolver.tsx @@ -1,11 +1,10 @@ 'use client'; import { ControllerContext, useController } from '@data-client/react'; +import { MockController } from '@data-client/react/mock'; +import type { MockProps } from '@data-client/react/mock'; import { useMemo } from 'react'; import React from 'react'; -import { MockController } from './MockController.js'; -import { MockProps } from './mockTypes.js'; - export interface MockResolverProps extends MockProps { children: React.ReactNode; silenceMissing?: boolean; diff --git a/packages/test/src/browser.ts b/packages/test/src/browser.ts index 57096af8f902..d6efa4148d24 100644 --- a/packages/test/src/browser.ts +++ b/packages/test/src/browser.ts @@ -1,4 +1,3 @@ -import mockInitialState from './mockState.js'; export { default as MockResolver } from './MockResolver.js'; export type { FixtureEndpoint, @@ -8,6 +7,5 @@ export type { SuccessFixture, ErrorFixture, Interceptor, -} from './fixtureTypes.js'; - -export { mockInitialState }; +} from '@data-client/react/mock'; +export { mockInitialState } from '@data-client/react/mock'; diff --git a/packages/test/src/collapseFixture.ts b/packages/test/src/collapseFixture.ts deleted file mode 100644 index 42927528345e..000000000000 --- a/packages/test/src/collapseFixture.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { Fixture, ResponseInterceptor } from './fixtureTypes.js'; - -export async function collapseFixture( - fixture: Fixture | ResponseInterceptor, - args: any[], - interceptorData: any, -) { - let error = 'error' in fixture ? fixture.error : false; - let response = fixture.response; - if (typeof fixture.response === 'function') { - try { - response = await fixture.response.apply(interceptorData, args); - // dispatch goes through user-code that can sometimes fail. - // let's ensure we always handle errors - } catch (e: any) { - response = e; - error = true; - } - } - return { response, error }; -} diff --git a/packages/test/src/createFixtureMap.tsx b/packages/test/src/createFixtureMap.tsx deleted file mode 100644 index 3f10ccb7fd73..000000000000 --- a/packages/test/src/createFixtureMap.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import type { Fixture, Interceptor } from './fixtureTypes.js'; - -export function createFixtureMap(fixtures: (Fixture | Interceptor)[] = []) { - const map: Map = new Map(); - const computed: Interceptor[] = []; - for (const fixture of fixtures) { - if ('args' in fixture) { - if (typeof fixture.response !== 'function') { - const key = fixture.endpoint.key(...fixture.args); - map.set(key, fixture); - } else { - // this has to be a typo. probably needs to remove args - console.warn( - `Fixture found with function response, and explicit args. Interceptors should not specify args. -${fixture.endpoint.name}: ${JSON.stringify(fixture.args)} - -Treating as Interceptor`, - ); - computed.push(fixture as any); - } - } else { - computed.push(fixture); - } - } - return [map, computed] as const; -} diff --git a/packages/test/src/fixtureTypes.ts b/packages/test/src/fixtureTypes.ts deleted file mode 100644 index ca96e571e557..000000000000 --- a/packages/test/src/fixtureTypes.ts +++ /dev/null @@ -1,120 +0,0 @@ -import type { EndpointInterface, ResolveType } from '@data-client/react'; - -type Updater = ( - result: any, - ...args: any -) => Record any>; - -export interface SuccessFixtureEndpoint< - E extends EndpointInterface & { update?: Updater } = EndpointInterface, -> { - readonly endpoint: E; - readonly args: Readonly>; - readonly response: - ResolveType | ((...args: Parameters) => ResolveType); - readonly error?: false; - /** Number of miliseconds to wait before resolving */ - readonly delay?: number; - /** Waits to run `response()` after `delay` time */ - readonly delayCollapse?: boolean; -} - -export interface ResponseInterceptor< - T = any, - E extends EndpointInterface & { - update?: Updater; - testKey(key: string): boolean; - } = EndpointInterface & { testKey(key: string): boolean }, -> { - readonly endpoint: E; - response(this: T, ...args: Parameters): ResolveType; - /** Number of miliseconds (or function that returns) to wait before resolving */ - readonly delay?: number | ((...args: Parameters) => number); - /** Waits to run `response()` after `delay` time */ - readonly delayCollapse?: boolean; -} -export interface FetchInterceptor< - T = any, - E extends EndpointInterface & { - update?: Updater; - testKey(key: string): boolean; - fetchResponse(input: RequestInfo, init: RequestInit): Promise; - extend(options: any): any; - } = EndpointInterface & { - testKey(key: string): boolean; - fetchResponse(input: RequestInfo, init: RequestInit): Promise; - extend(options: any): any; - }, -> { - readonly endpoint: E; - fetchResponse(this: T, input: RequestInfo, init: RequestInit): ResolveType; - /** Number of miliseconds (or function that returns) to wait before resolving */ - readonly delay?: number | ((...args: Parameters) => number); - /** Waits to run `response()` after `delay` time */ - readonly delayCollapse?: boolean; -} -/** Interceptors match and compute dynamic responses based on args - * - * @see https://dataclient.io/docs/api/Fixtures#interceptor - */ -export type Interceptor< - T = any, - E extends EndpointInterface & { - update?: Updater; - testKey(key: string): boolean; - fetchResponse?(input: RequestInfo, init: RequestInit): Promise; - extend?(options: any): any; - } = EndpointInterface & { - testKey(key: string): boolean; - fetchResponse(input: RequestInfo, init: RequestInit): Promise; - extend(options: any): any; - }, -> = - | ResponseInterceptor - | (E extends ( - { - fetchResponse(input: RequestInfo, init: RequestInit): Promise; - extend(options: any): any; - } - ) ? - FetchInterceptor - : never); - -export interface ErrorFixtureEndpoint< - E extends EndpointInterface & { update?: Updater } = EndpointInterface, -> { - readonly endpoint: E; - readonly args: Readonly>; - readonly response: any; - readonly error: true; - /** Number of miliseconds to wait before resolving */ - readonly delay?: number; - /** Waits to run `response()` after `delay` time */ - readonly delayCollapse?: boolean; -} - -export type FixtureEndpoint< - E extends EndpointInterface & { update?: Updater } = EndpointInterface, -> = SuccessFixtureEndpoint | ErrorFixtureEndpoint; - -/** Represents a successful response - * - * @see https://dataclient.io/docs/api/Fixtures#successfixture - */ -export type SuccessFixture< - E extends EndpointInterface & { update?: Updater } = EndpointInterface, -> = SuccessFixtureEndpoint; -/** Represents a failed/errored response - * - * @see https://dataclient.io/docs/api/Fixtures#errorfixtures - */ -export type ErrorFixture< - E extends EndpointInterface & { update?: Updater } = EndpointInterface, -> = ErrorFixtureEndpoint; -/** Represents a static response - * - * @see https://dataclient.io/docs/api/Fixtures - */ -export type Fixture< - E extends EndpointInterface & { update?: Updater } = EndpointInterface, -> = FixtureEndpoint; diff --git a/packages/test/src/index.ts b/packages/test/src/index.ts index 9a35cec9be71..45088e2506b5 100644 --- a/packages/test/src/index.ts +++ b/packages/test/src/index.ts @@ -17,7 +17,8 @@ export type { SuccessFixture, ErrorFixture, Interceptor, -} from './fixtureTypes.js'; +} from '@data-client/react/mock'; +export { mockInitialState } from '@data-client/react/mock'; import { act as cjsAct, renderHook as cjsRenderHook, @@ -26,11 +27,8 @@ import type { ActType, RenderHook, } from './makeRenderDataClient/renderHookTypes.js'; -import mockInitialState from './mockState.js'; export type { RenderHookOptions } from './makeRenderDataClient/renderHookOptions.js'; // Annotated so our declarations don't import renderHook.cjs, which TypeScript < 4.5 can't resolve export const act: ActType = cjsAct; export const renderHook: RenderHook = cjsRenderHook; - -export { mockInitialState }; diff --git a/packages/test/src/makeRenderDataClient/index.tsx b/packages/test/src/makeRenderDataClient/index.tsx index 3c69f34bc7b0..9d820183dc8c 100644 --- a/packages/test/src/makeRenderDataClient/index.tsx +++ b/packages/test/src/makeRenderDataClient/index.tsx @@ -7,14 +7,13 @@ import { Controller, GCInterface, } from '@data-client/react'; +import { MockController, mockInitialState } from '@data-client/react/mock'; +import type { Interceptor, Fixture } from '@data-client/react/mock'; import React, { memo, Suspense, useLayoutEffect, useState } from 'react'; import { renderHook, act } from './renderHook.cjs'; import type { RenderHookOptions } from './renderHookOptions.js'; import type { RenderHookResult } from './renderHookTypes.js'; -import { Interceptor, Fixture } from '../fixtureTypes.js'; -import { MockController } from '../MockController.js'; -import mockInitialState from '../mockState.js'; const activeCleanups = new Set<() => void>(); diff --git a/packages/test/src/mockState.ts b/packages/test/src/mockState.ts deleted file mode 100644 index d779ba0ec303..000000000000 --- a/packages/test/src/mockState.ts +++ /dev/null @@ -1 +0,0 @@ -export { mockInitialState as default } from '@data-client/react/mock'; diff --git a/packages/test/src/mockTypes.ts b/packages/test/src/mockTypes.ts deleted file mode 100644 index 10a74d906d03..000000000000 --- a/packages/test/src/mockTypes.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { Fixture, Interceptor } from './fixtureTypes.js'; - -export interface MockProps { - readonly fixtures?: (Fixture | Interceptor)[]; - getInitialInterceptorData?: () => T; -} diff --git a/website/blog/2026-10-03-v0.19-batch-set.md b/website/blog/2026-10-03-v0.19-batch-set.md index 5141a78e06ac..836bd4a880ad 100644 --- a/website/blog/2026-10-03-v0.19-batch-set.md +++ b/website/blog/2026-10-03-v0.19-batch-set.md @@ -60,6 +60,7 @@ Coding agents also get a Claude Code plugin, Vue skills and llms.txt; see - [TypeScript 4.0 or later is required](/blog/2026/10/03/v0.19-batch-set#typescript-4) - the TypeScript 3.x declarations are removed ([#4151](https://github.com/reactive/data-client/pull/4151)) - [Managers must define `middleware`](/blog/2026/10/03/v0.19-batch-set#manager-middleware) - `getMiddleware()` is removed ([#4378](https://github.com/reactive/data-client/pull/4378), [#4382](https://github.com/reactive/data-client/pull/4382)) +- [@data-client/test requires @data-client/react 0.15 or later](/blog/2026/10/03/v0.19-batch-set#test-react-0.15) ([#4414](https://github.com/reactive/data-client/pull/4414)) **[Get ready for future releases:](/blog/2026/10/03/v0.19-batch-set#future-releases)** @@ -796,6 +797,15 @@ class LoggingManager implements Manager { [#4382](https://github.com/reactive/data-client/pull/4382) +### @data-client/test requires @data-client/react 0.15 {#test-react-0.15} + +Skip this section if you don't use [@data-client/test](https://www.npmjs.com/package/@data-client/test). + +`@data-client/test` now builds on `@data-client/react/mock`, which first shipped in `@data-client/react` 0.15, so +support for 0.12 through 0.14 is dropped. Upgrade `@data-client/react` to the same version as `@data-client/test`, as +the install command above does. Fixture types imported from `@data-client/test` and from `@data-client/react/mock` are +now the same types. [#4414](https://github.com/reactive/data-client/pull/4414) + ### Get ready for future releases {#future-releases} Nothing here is needed to upgrade to 0.19; your code keeps working as is. Each item will break in a future release, 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 0b6e3a4225f8..32046819c6b9 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 @@ -1,112 +1,11 @@ +import { MockProps, Fixture, Interceptor } from '@data-client/core/mock'; +export { ErrorFixture, ErrorFixtureEndpoint, Fixture, FixtureEndpoint, Interceptor, SuccessFixture, SuccessFixtureEndpoint, mockInitialState } from '@data-client/core/mock'; import React from 'react'; -import { EndpointInterface, ResolveType, Manager, State, GCInterface, Controller } from '@data-client/react'; +import { Manager, State, GCInterface, Controller } from '@data-client/react'; import * as _testing_library_react from '@testing-library/react'; import { Queries, RenderOptions, waitForOptions } from '@testing-library/react'; import { queries } from '@testing-library/dom'; import * as ReactDOMClient from 'react-dom/client'; -export { mockInitialState } from '@data-client/core/mock'; - -type Updater = (result: any, ...args: any) => Record any>; -interface SuccessFixtureEndpoint { - readonly endpoint: E; - readonly args: Readonly>; - readonly response: ResolveType | ((...args: Parameters) => ResolveType); - readonly error?: false; - /** Number of miliseconds to wait before resolving */ - readonly delay?: number; - /** Waits to run `response()` after `delay` time */ - readonly delayCollapse?: boolean; -} -interface ResponseInterceptor { - readonly endpoint: E; - response(this: T, ...args: Parameters): ResolveType; - /** Number of miliseconds (or function that returns) to wait before resolving */ - readonly delay?: number | ((...args: Parameters) => number); - /** Waits to run `response()` after `delay` time */ - readonly delayCollapse?: boolean; -} -interface FetchInterceptor; - extend(options: any): any; -} = EndpointInterface & { - testKey(key: string): boolean; - fetchResponse(input: RequestInfo, init: RequestInit): Promise; - extend(options: any): any; -}> { - readonly endpoint: E; - fetchResponse(this: T, input: RequestInfo, init: RequestInit): ResolveType; - /** Number of miliseconds (or function that returns) to wait before resolving */ - readonly delay?: number | ((...args: Parameters) => number); - /** Waits to run `response()` after `delay` time */ - readonly delayCollapse?: boolean; -} -/** Interceptors match and compute dynamic responses based on args - * - * @see https://dataclient.io/docs/api/Fixtures#interceptor - */ -type Interceptor; - extend?(options: any): any; -} = EndpointInterface & { - testKey(key: string): boolean; - fetchResponse(input: RequestInfo, init: RequestInit): Promise; - extend(options: any): any; -}> = ResponseInterceptor | (E extends ({ - fetchResponse(input: RequestInfo, init: RequestInit): Promise; - extend(options: any): any; -}) ? FetchInterceptor : never); -interface ErrorFixtureEndpoint { - readonly endpoint: E; - readonly args: Readonly>; - readonly response: any; - readonly error: true; - /** Number of miliseconds to wait before resolving */ - readonly delay?: number; - /** Waits to run `response()` after `delay` time */ - readonly delayCollapse?: boolean; -} -type FixtureEndpoint = SuccessFixtureEndpoint | ErrorFixtureEndpoint; -/** Represents a successful response - * - * @see https://dataclient.io/docs/api/Fixtures#successfixture - */ -type SuccessFixture = SuccessFixtureEndpoint; -/** Represents a failed/errored response - * - * @see https://dataclient.io/docs/api/Fixtures#errorfixtures - */ -type ErrorFixture = ErrorFixtureEndpoint; -/** Represents a static response - * - * @see https://dataclient.io/docs/api/Fixtures - */ -type Fixture = FixtureEndpoint; - -interface MockProps { - readonly fixtures?: (Fixture | Interceptor)[]; - getInitialInterceptorData?: () => T; -} interface MockResolverProps extends MockProps { children: React.ReactNode; @@ -201,4 +100,4 @@ declare const renderDataHook: RenderDataHook; declare const act: ActType; declare const renderHook: RenderHook; -export { type DataProviderProps, type ErrorFixture, type ErrorFixtureEndpoint, type Fixture, type FixtureEndpoint, type Interceptor, MockResolver, type RenderDataHook, type RenderDataHookResult, type RenderHookOptions, type SuccessFixture, type SuccessFixtureEndpoint, act, makeRenderDataHook as makeRenderDataClient, makeRenderDataHook, renderDataHook, renderHook }; +export { type DataProviderProps, MockResolver, type RenderDataHook, type RenderDataHookResult, type RenderHookOptions, act, makeRenderDataHook as makeRenderDataClient, makeRenderDataHook, renderDataHook, renderHook };