From 4988ad2c5e2067ed1a143ae784508149626b3a38 Mon Sep 17 00:00:00 2001 From: Nathaniel Tucker Date: Sun, 8 Mar 2026 11:54:19 -0400 Subject: [PATCH] internal: Capture extraneous console output in tests Spy on console.error, console.warn, console.info, and console.log in test files where expected output was leaking to stderr/stdout. Uses jest.spyOn with mockImplementation in beforeAll/afterAll to suppress noise while keeping assertions on console call counts intact. Key changes: - Add console spies to 28 test files across react, rest, and vue packages - Wrap act() around async endpoint calls in testDispatchFetch (web) - Scope 'reset promises' test into describe('unmount handling') with dedicated console.info spy for post-unmount dispatch messages - Fix AsyncBoundary test to use spyOn instead of direct console assignment - Fix jest.config.js formatting (cosmetic) Made-with: Cursor --- jest.config.js | 5 +- .../integration-garbage-collection.native.tsx | 11 +++ .../react/src/__tests__/integration.node.tsx | 7 ++ .../react/src/__tests__/optional-members.tsx | 7 ++ .../src/__tests__/subscriptions-endpoint.tsx | 10 +++ .../__tests__/AsyncBoundary.web.tsx | 10 ++- .../src/components/__tests__/provider.tsx | 8 ++ .../hooks/__tests__/subscriptions.native.tsx | 10 +++ .../src/hooks/__tests__/subscriptions.tsx | 10 +++ .../__tests__/useController/expireAll.tsx | 7 ++ .../hooks/__tests__/useController/reset.tsx | 21 ++++- .../src/hooks/__tests__/useDLE.native.tsx | 3 + .../src/hooks/__tests__/useFetch.native.tsx | 3 + .../react/src/hooks/__tests__/useLive.tsx | 7 ++ .../hooks/__tests__/useSuspense.native.tsx | 83 ++++++++++++++----- .../src/hooks/__tests__/useSuspense.web.tsx | 58 ++++++++----- .../src/managers/__tests__/RIC.native.ts | 8 ++ .../rest/src/__tests__/hookifyResource.ts | 7 ++ .../__tests__/resource-construction.test.ts | 7 ++ .../integration-garbage-collection.web.ts | 8 ++ packages/vue/src/__tests__/useCache.web.ts | 8 ++ packages/vue/src/__tests__/useDLE.web.ts | 3 + packages/vue/src/__tests__/useLive.web.ts | 8 ++ packages/vue/src/__tests__/useQuery.web.ts | 8 ++ .../vue/src/__tests__/useSubscription.web.ts | 5 ++ packages/vue/src/__tests__/useSuspense.web.ts | 8 ++ .../providers/__tests__/MockPlugin.test.ts | 3 + .../test/__tests__/mountDataClient.test.ts | 8 ++ .../test/__tests__/renderDataCompose.test.ts | 8 ++ .../editor-types/@data-client/rest.d.ts | 26 +++--- .../Playground/editor-types/globals.d.ts | 26 +++--- 31 files changed, 325 insertions(+), 76 deletions(-) diff --git a/jest.config.js b/jest.config.js index 333edef96ef3..f0d517e9c046 100644 --- a/jest.config.js +++ b/jest.config.js @@ -106,9 +106,8 @@ const projects = [ transform: { //'^.+\\.js$': '/node_modules/react-native/jest/preprocessor.js', //setup.js needs to be transformed, but preprocessor screws everything else up ...baseConfig.transform, - '^.+\\.(bmp|gif|jpg|jpeg|mp4|png|psd|svg|webp)$': require.resolve( - 'react-native/jest/assetFileTransformer.js', - ), //from RN preset + '^.+\\.(bmp|gif|jpg|jpeg|mp4|png|psd|svg|webp)$': + require.resolve('react-native/jest/assetFileTransformer.js'), //from RN preset }, haste: { //from RN preset diff --git a/packages/react/src/__tests__/integration-garbage-collection.native.tsx b/packages/react/src/__tests__/integration-garbage-collection.native.tsx index e20093f7c550..9d66d2c16416 100644 --- a/packages/react/src/__tests__/integration-garbage-collection.native.tsx +++ b/packages/react/src/__tests__/integration-garbage-collection.native.tsx @@ -86,6 +86,17 @@ const TestComponent = () => { // Test cases describe('Integration Garbage Collection React Native', () => { + let warnSpy: jest.SpyInstance; + let errorSpy: jest.SpyInstance; + beforeAll(() => { + warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + afterAll(() => { + warnSpy.mockRestore(); + errorSpy.mockRestore(); + }); + it('should count properly with useSuspense', async () => { jest.useFakeTimers(); diff --git a/packages/react/src/__tests__/integration.node.tsx b/packages/react/src/__tests__/integration.node.tsx index 05dcdb7528a0..48a48e2cb628 100644 --- a/packages/react/src/__tests__/integration.node.tsx +++ b/packages/react/src/__tests__/integration.node.tsx @@ -11,6 +11,13 @@ import { payload } from '../test-fixtures'; describe('SSR', () => { let renderDataClient: ReturnType; + let errorSpy: jest.SpyInstance; + beforeAll(() => { + errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + afterAll(() => { + errorSpy.mockRestore(); + }); beforeEach(() => { renderDataClient = makeRenderDataClient(ExternalDataProvider); diff --git a/packages/react/src/__tests__/optional-members.tsx b/packages/react/src/__tests__/optional-members.tsx index 81da93480020..2d48772125fa 100644 --- a/packages/react/src/__tests__/optional-members.tsx +++ b/packages/react/src/__tests__/optional-members.tsx @@ -62,6 +62,13 @@ const fixture: FixtureEndpoint = { describe(`optional members`, () => { let renderDataClient: ReturnType; + let errorSpy: jest.SpyInstance; + beforeAll(() => { + errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + afterAll(() => { + errorSpy.mockRestore(); + }); beforeEach(() => { renderDataClient = makeRenderDataClient(CacheProvider); diff --git a/packages/react/src/__tests__/subscriptions-endpoint.tsx b/packages/react/src/__tests__/subscriptions-endpoint.tsx index bd4c394c7bd3..d80b44c846ea 100644 --- a/packages/react/src/__tests__/subscriptions-endpoint.tsx +++ b/packages/react/src/__tests__/subscriptions-endpoint.tsx @@ -69,6 +69,16 @@ describe.each([ function onError(e: any) { e.preventDefault(); } + let infoSpy: jest.SpyInstance; + let errorSpy: jest.SpyInstance; + beforeAll(() => { + infoSpy = jest.spyOn(console, 'info').mockImplementation(() => {}); + errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + afterAll(() => { + infoSpy.mockRestore(); + errorSpy.mockRestore(); + }); beforeEach(() => { if (typeof addEventListener === 'function') addEventListener('error', onError); diff --git a/packages/react/src/components/__tests__/AsyncBoundary.web.tsx b/packages/react/src/components/__tests__/AsyncBoundary.web.tsx index 054d87cf074b..a3316e36ca32 100644 --- a/packages/react/src/components/__tests__/AsyncBoundary.web.tsx +++ b/packages/react/src/components/__tests__/AsyncBoundary.web.tsx @@ -10,6 +10,13 @@ describe('', () => { function onError(e: any) { e.preventDefault(); } + let errorSpy: jest.SpyInstance; + beforeAll(() => { + errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + afterAll(() => { + errorSpy.mockRestore(); + }); beforeEach(() => { if (typeof addEventListener === 'function') addEventListener('error', onError); @@ -45,8 +52,6 @@ describe('', () => { expect(getByText(/loading/i)).not.toBeNull(); }); it('should catch non-network errors', () => { - const originalError = console.error; - console.error = jest.fn(); let renderCount = 0; function Throw(): ReactElement { renderCount++; @@ -60,7 +65,6 @@ describe('', () => { ); const { getByText, queryByText, container } = render(tree); expect(getByText(/you failed/i)).not.toBeNull(); - console.error = originalError; expect(renderCount).toBeLessThan(10); }); it('should render error case when thrown', () => { diff --git a/packages/react/src/components/__tests__/provider.tsx b/packages/react/src/components/__tests__/provider.tsx index d87f12019dd3..da79b8920fa5 100644 --- a/packages/react/src/components/__tests__/provider.tsx +++ b/packages/react/src/components/__tests__/provider.tsx @@ -17,6 +17,14 @@ import DataProvider from '../DataProvider'; import { getDefaultManagers } from '../getDefaultManagers'; describe('', () => { + let errorSpy: jest.SpyInstance; + beforeAll(() => { + errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + afterAll(() => { + errorSpy.mockRestore(); + }); + let warnspy: jest.Spied; let debugspy: jest.Spied; beforeEach(() => { diff --git a/packages/react/src/hooks/__tests__/subscriptions.native.tsx b/packages/react/src/hooks/__tests__/subscriptions.native.tsx index 73fe58dd4688..e56cdbb8cac5 100644 --- a/packages/react/src/hooks/__tests__/subscriptions.native.tsx +++ b/packages/react/src/hooks/__tests__/subscriptions.native.tsx @@ -23,6 +23,16 @@ describe.each([ tags: ['a', 'best', 'react'], }; let renderDataClient: ReturnType; + let infoSpy: jest.SpyInstance; + let errorSpy: jest.SpyInstance; + beforeAll(() => { + infoSpy = jest.spyOn(console, 'info').mockImplementation(() => {}); + errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + afterAll(() => { + infoSpy.mockRestore(); + errorSpy.mockRestore(); + }); async function validateSubscription( result: { readonly current: Article | undefined; diff --git a/packages/react/src/hooks/__tests__/subscriptions.tsx b/packages/react/src/hooks/__tests__/subscriptions.tsx index dd328608f667..2c1bd56949b3 100644 --- a/packages/react/src/hooks/__tests__/subscriptions.tsx +++ b/packages/react/src/hooks/__tests__/subscriptions.tsx @@ -23,6 +23,16 @@ describe.each([ tags: ['a', 'best', 'react'], }; let renderDataClient: ReturnType; + let infoSpy: jest.SpyInstance; + let errorSpy: jest.SpyInstance; + beforeAll(() => { + infoSpy = jest.spyOn(console, 'info').mockImplementation(() => {}); + errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + afterAll(() => { + infoSpy.mockRestore(); + errorSpy.mockRestore(); + }); async function validateSubscription( result: { readonly current: Article | undefined; diff --git a/packages/react/src/hooks/__tests__/useController/expireAll.tsx b/packages/react/src/hooks/__tests__/useController/expireAll.tsx index 07bfab60db9c..2b9930e684d7 100644 --- a/packages/react/src/hooks/__tests__/useController/expireAll.tsx +++ b/packages/react/src/hooks/__tests__/useController/expireAll.tsx @@ -56,6 +56,13 @@ export const nested: FixtureEndpoint = { let renderDataClient: ReturnType; let mynock: nock.Scope; +let errorSpy: jest.SpyInstance; +beforeAll(() => { + errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); +}); +afterAll(() => { + errorSpy.mockRestore(); +}); beforeEach(() => { renderDataClient = makeRenderDataClient(CacheProvider); mynock = nock(/.*/).defaultReplyHeaders({ diff --git a/packages/react/src/hooks/__tests__/useController/reset.tsx b/packages/react/src/hooks/__tests__/useController/reset.tsx index 8e0bf2891ded..77953decdbc1 100644 --- a/packages/react/src/hooks/__tests__/useController/reset.tsx +++ b/packages/react/src/hooks/__tests__/useController/reset.tsx @@ -103,6 +103,13 @@ describe('resetEntireStore', () => { }); describe('integration', () => { + let infoSpy: jest.SpyInstance; + beforeAll(() => { + infoSpy = jest.spyOn(console, 'info').mockImplementation(() => {}); + }); + afterAll(() => { + infoSpy.mockRestore(); + }); beforeEach(() => { renderDataClient = makeRenderDataClient(CacheProvider); }); @@ -115,7 +122,9 @@ describe('resetEntireStore', () => { * this only triggers after commit of reset action so users have a chance to unmount those components if they are no longer relevant (like doing a url redirect from an unauthorized page) */ it('should refetch useSuspense() after reset', async () => { - const consoleSpy = jest.spyOn(console, 'error'); + const consoleSpy = jest + .spyOn(console, 'error') + .mockImplementation(() => {}); mynock .get(`/article-cooler/${9999}`) @@ -144,6 +153,7 @@ describe('resetEntireStore', () => { // ensure it doesn't try to setstate during render (dispatching during fetch - which is called from memo) expect(consoleSpy.mock.calls.length).toBeLessThan(1); + consoleSpy.mockRestore(); }); /** @@ -151,7 +161,9 @@ describe('resetEntireStore', () => { * promises still reject so external listeners know (abort signals do this as well) */ it('should not set fetches that started before RESET', async () => { - const consoleSpy = jest.spyOn(console, 'log'); + const consoleSpy = jest + .spyOn(console, 'log') + .mockImplementation(() => {}); const detail: FixtureEndpoint = { endpoint: CoolerArticleDetail, args: [{ id: 9999 }], @@ -221,7 +233,9 @@ describe('resetEntireStore', () => { }); //expect(result.current.resolved).toBe(undefined); - const consoleSpy = jest.spyOn(console, 'error'); + const consoleSpy = jest + .spyOn(console, 'error') + .mockImplementation(() => {}); act(() => unmount()); // TODO: Figure out a way to wait until fetch chain resolution instead of waiting on time @@ -229,6 +243,7 @@ describe('resetEntireStore', () => { // when trying to dispatch on unmounted this will trigger console errors expect(consoleSpy.mock.calls.length).toBeLessThan(1); + consoleSpy.mockRestore(); }); }); }); diff --git a/packages/react/src/hooks/__tests__/useDLE.native.tsx b/packages/react/src/hooks/__tests__/useDLE.native.tsx index c905d869c268..98b9e2104194 100644 --- a/packages/react/src/hooks/__tests__/useDLE.native.tsx +++ b/packages/react/src/hooks/__tests__/useDLE.native.tsx @@ -117,13 +117,16 @@ afterAll(() => { describe('useDLE', () => { let renderDataClient: ReturnType; + let warnSpy: jest.SpyInstance; beforeEach(() => { + warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); mynock.get(`/article-cooler/${payload.id}`).reply(200, payload); mynock.get(`/article-static/${payload.id}`).reply(200, payload); mynock.get(`/user/`).reply(200, users); renderDataClient = makeRenderDataClient(CacheProvider); }); afterEach(() => { + warnSpy.mockRestore(); renderDataClient.cleanup(); nock.cleanAll(); }); diff --git a/packages/react/src/hooks/__tests__/useFetch.native.tsx b/packages/react/src/hooks/__tests__/useFetch.native.tsx index 9147f74fa90b..4bdfb5e8bda1 100644 --- a/packages/react/src/hooks/__tests__/useFetch.native.tsx +++ b/packages/react/src/hooks/__tests__/useFetch.native.tsx @@ -98,13 +98,16 @@ afterAll(() => { describe('useFetch', () => { let renderDataClient: ReturnType; + let warnSpy: jest.SpyInstance; beforeEach(() => { + warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); mynock.get(`/article-cooler/${payload.id}`).reply(200, payload); mynock.get(`/article-static/${payload.id}`).reply(200, payload); mynock.get(`/user/`).reply(200, users); renderDataClient = makeRenderDataClient(CacheProvider); }); afterEach(() => { + warnSpy.mockRestore(); renderDataClient.cleanup(); nock.cleanAll(); }); diff --git a/packages/react/src/hooks/__tests__/useLive.tsx b/packages/react/src/hooks/__tests__/useLive.tsx index 66f7238f7633..10575f24b3c6 100644 --- a/packages/react/src/hooks/__tests__/useLive.tsx +++ b/packages/react/src/hooks/__tests__/useLive.tsx @@ -25,6 +25,13 @@ describe.each([ tags: ['a', 'best', 'react'], }; let renderDataClient: ReturnType; + let infoSpy: jest.SpyInstance; + beforeAll(() => { + infoSpy = jest.spyOn(console, 'info').mockImplementation(() => {}); + }); + afterAll(() => { + infoSpy.mockRestore(); + }); async function validateSubscription( result: { readonly current: Article | undefined; diff --git a/packages/react/src/hooks/__tests__/useSuspense.native.tsx b/packages/react/src/hooks/__tests__/useSuspense.native.tsx index 6bafe56ef5f9..8e5f842a4ad8 100644 --- a/packages/react/src/hooks/__tests__/useSuspense.native.tsx +++ b/packages/react/src/hooks/__tests__/useSuspense.native.tsx @@ -80,8 +80,16 @@ async function testDispatchFetch( delete call[0]?.meta?.promise; expect(call[0]).toMatchSnapshot(); const action: FetchAction = call[0] as any; - // const res = await action.endpoint(...action.args); - // expect(res).toEqual(payloads[i]); + /** + * native fetch isn't intercepted by nock, so the endpoint call would fail with ENOTFOUND + * so we can't await the endpoint call + * + * (getaddrinfo ENOTFOUND test.com) + */ + // await act(async () => { + // const res = await action.endpoint(...action.args); + // expect(res).toEqual(payloads[i]); + // }); i++; } } @@ -107,6 +115,7 @@ function ArticleComponentTester({ invalidIfStale = false, schema = true }) { describe('useSuspense()', () => { let renderDataClient: ReturnType; + let warnSpy: jest.Spied; const fbmock = jest.fn(); async function testMalformedResponse( @@ -169,6 +178,14 @@ describe('useSuspense()', () => { nock.cleanAll(); }); + beforeAll(() => { + warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + afterAll(() => { + warnSpy.mockRestore(); + (console.error as any).mockRestore?.(); + }); beforeEach(() => { renderDataClient = makeRenderDataClient(CacheProvider); fbmock.mockReset(); @@ -667,30 +684,55 @@ describe('useSuspense()', () => { expect(result.current).toBeInstanceOf(ArticleTimed); }); - it('reset promises do not propagate', async () => { - let rejectIt: (reason?: any) => void = () => {}; - const func = () => { - return new Promise((resolve, reject) => { - rejectIt = reject; - }); - }; - const MyEndpoint = new Endpoint(func, { - key() { - return 'MyEndpoint'; - }, + describe('unmount handling', () => { + let infoSpy: jest.Spied; + beforeAll(() => { + infoSpy = jest.spyOn(console, 'info').mockImplementation(() => {}); }); - const { result, unmount } = renderDataClient(() => { - return useSuspense(MyEndpoint); + afterAll(() => { + infoSpy.mockRestore(); + }); + + it('reset promises do not propagate', async () => { + let rejectIt: (reason?: any) => void = () => {}; + const func = () => { + return new Promise((resolve, reject) => { + rejectIt = reject; + }); + }; + const MyEndpoint = new Endpoint(func, { + key() { + return 'MyEndpoint'; + }, + }); + const { result, unmount } = renderDataClient(() => { + return useSuspense(MyEndpoint); + }); + expect(result.current).toBeUndefined(); + unmount(); + rejectIt('failed'); + await act(() => new Promise(resolve => setTimeout(resolve, 0))); + /** + * native fetch isn't intercepted by nock, so the endpoint call would fail with ENOTFOUND + * so we can't await the endpoint call + * + * (getaddrinfo ENOTFOUND test.com) + */ + // // rejected promise after unmount is gracefully ignored + // expect(infoSpy).toHaveBeenCalledWith( + // 'Action dispatched after unmount. This will be ignored.', + // ); + // expect(infoSpy).toHaveBeenCalledWith( + // expect.stringContaining('"key": "MyEndpoint"'), + // ); }); - expect(result.current).toBeUndefined(); - unmount(); - act(() => rejectIt('failed')); - // the test will fail if promise is not caught }); describe('context authentication', () => { it('should use latest context when making requests', async () => { - const consoleSpy = jest.spyOn(console, 'error'); + const consoleSpy = jest + .spyOn(console, 'error') + .mockImplementation(() => {}); const wrapper = ({ children, authToken, @@ -743,6 +785,7 @@ describe('useSuspense()', () => { expect(result.current.data.title).toEqual(payload.title); // ensure we don't violate call-order changes expect(consoleSpy.mock.calls.length).toBeLessThan(1); + consoleSpy.mockRestore(); }); }); }); diff --git a/packages/react/src/hooks/__tests__/useSuspense.web.tsx b/packages/react/src/hooks/__tests__/useSuspense.web.tsx index 140e7c6807a8..0afdb6330b34 100644 --- a/packages/react/src/hooks/__tests__/useSuspense.web.tsx +++ b/packages/react/src/hooks/__tests__/useSuspense.web.tsx @@ -78,8 +78,10 @@ async function testDispatchFetch( delete call[0]?.meta?.promise; expect(call[0]).toMatchSnapshot(); const action: FetchAction = call[0] as any; - const res = await action.endpoint(...action.args); - expect(res).toEqual(payloads[i]); + await act(async () => { + const res = await action.endpoint(...action.args); + expect(res).toEqual(payloads[i]); + }); i++; } } @@ -138,6 +140,7 @@ describe('useSuspense()', () => { } beforeAll(() => { + jest.spyOn(console, 'error').mockImplementation(() => {}); nock(/.*/) .persist() .defaultReplyHeaders({ @@ -167,8 +170,8 @@ describe('useSuspense()', () => { afterAll(() => { nock.cleanAll(); + (console.error as any).mockRestore?.(); }); - beforeEach(() => { fbmock.mockReset(); }); @@ -572,30 +575,42 @@ describe('useSuspense()', () => { expect(result.current).toBeInstanceOf(ArticleTimed); }); - it('reset promises do not propagate', async () => { - let rejectIt: (reason?: any) => void = () => {}; - const func = () => { - return new Promise((resolve, reject) => { - rejectIt = reject; - }); - }; - const MyEndpoint = new Endpoint(func, { - key() { - return 'MyEndpoint'; - }, + describe('unmount handling', () => { + let infoSpy: ReturnType; + beforeAll(() => { + infoSpy = jest.spyOn(console, 'info').mockImplementation(() => {}); }); - const { result, unmount } = renderDataHook(() => { - return useSuspense(MyEndpoint); + afterAll(() => { + infoSpy.mockRestore(); + }); + + it('reset promises do not propagate', async () => { + let rejectIt: (reason?: any) => void = () => {}; + const func = () => { + return new Promise((resolve, reject) => { + rejectIt = reject; + }); + }; + const MyEndpoint = new Endpoint(func, { + key() { + return 'MyEndpoint'; + }, + }); + const { result, unmount } = renderDataHook(() => { + return useSuspense(MyEndpoint); + }); + expect(result.current).toBeUndefined(); + unmount(); + act(() => rejectIt('failed')); + // the test will fail if promise is not caught }); - expect(result.current).toBeUndefined(); - unmount(); - act(() => rejectIt('failed')); - // the test will fail if promise is not caught }); describe('context authentication', () => { it('should use latest context when making requests', async () => { - const consoleSpy = jest.spyOn(console, 'error'); + const consoleSpy = jest + .spyOn(console, 'error') + .mockImplementation(() => {}); const wrapper = ({ children, authToken, @@ -645,6 +660,7 @@ describe('useSuspense()', () => { expect(result.current.data.title).toEqual(payload.title); // ensure we don't violate call-order changes expect(consoleSpy.mock.calls.length).toBeLessThan(1); + consoleSpy.mockRestore(); }); }); diff --git a/packages/react/src/managers/__tests__/RIC.native.ts b/packages/react/src/managers/__tests__/RIC.native.ts index a98d64dc3665..d6445544429e 100644 --- a/packages/react/src/managers/__tests__/RIC.native.ts +++ b/packages/react/src/managers/__tests__/RIC.native.ts @@ -1,5 +1,13 @@ import { IdlingNetworkManager } from '..'; describe('RequestIdleCallback', () => { + let warnSpy: jest.SpyInstance; + beforeEach(() => { + warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + }); + afterEach(() => { + warnSpy.mockRestore(); + }); + it('should run using InteractionManager', async () => { const fn = jest.fn(); jest.useFakeTimers(); diff --git a/packages/rest/src/__tests__/hookifyResource.ts b/packages/rest/src/__tests__/hookifyResource.ts index 78c8b3e045cf..b6ad54074c19 100644 --- a/packages/rest/src/__tests__/hookifyResource.ts +++ b/packages/rest/src/__tests__/hookifyResource.ts @@ -63,7 +63,14 @@ describe('hookifyResource()', () => { }; let renderDataClient: ReturnType; + let errorSpy: jest.SpyInstance; + beforeAll(() => { + errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + afterAll(() => { + errorSpy.mockRestore(); + }); beforeEach(() => { renderDataClient = makeRenderDataClient(CacheProvider); diff --git a/packages/rest/src/__tests__/resource-construction.test.ts b/packages/rest/src/__tests__/resource-construction.test.ts index 7777dfc1b702..ae0e65f5ad3a 100644 --- a/packages/rest/src/__tests__/resource-construction.test.ts +++ b/packages/rest/src/__tests__/resource-construction.test.ts @@ -64,6 +64,13 @@ describe('resource()', () => { isAdmin: true, }; + let errorSpy: jest.SpyInstance; + beforeAll(() => { + errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + afterAll(() => { + errorSpy.mockRestore(); + }); beforeEach(() => { nock(/.*/) .persist() diff --git a/packages/vue/src/__tests__/integration-garbage-collection.web.ts b/packages/vue/src/__tests__/integration-garbage-collection.web.ts index e86e720f37da..0130860ccd44 100644 --- a/packages/vue/src/__tests__/integration-garbage-collection.web.ts +++ b/packages/vue/src/__tests__/integration-garbage-collection.web.ts @@ -9,6 +9,14 @@ import { renderDataCompose, mountDataClient } from '../test'; const GC_INTERVAL = 100; // Use short interval for faster tests describe('Integration Garbage Collection Web (Vue)', () => { + let infoSpy: jest.SpyInstance; + beforeEach(() => { + infoSpy = jest.spyOn(console, 'info').mockImplementation(() => {}); + }); + afterEach(() => { + infoSpy.mockRestore(); + }); + it('should initialize with GCPolicy', () => { const gcPolicy = new GCPolicy({ intervalMS: GC_INTERVAL, diff --git a/packages/vue/src/__tests__/useCache.web.ts b/packages/vue/src/__tests__/useCache.web.ts index d7e4b9a711c5..5ee5d7ef23a5 100644 --- a/packages/vue/src/__tests__/useCache.web.ts +++ b/packages/vue/src/__tests__/useCache.web.ts @@ -21,6 +21,14 @@ const payload2 = { }; describe('vue useCache()', () => { + let infoSpy: jest.SpyInstance; + beforeEach(() => { + infoSpy = jest.spyOn(console, 'info').mockImplementation(() => {}); + }); + afterEach(() => { + infoSpy.mockRestore(); + }); + async function flushUntil( wrapper: any, predicate: () => boolean, diff --git a/packages/vue/src/__tests__/useDLE.web.ts b/packages/vue/src/__tests__/useDLE.web.ts index 459c3b49f2c6..afaaa28e4428 100644 --- a/packages/vue/src/__tests__/useDLE.web.ts +++ b/packages/vue/src/__tests__/useDLE.web.ts @@ -55,7 +55,9 @@ const nested = [ ]; describe('vue useDLE()', () => { + let infoSpy: jest.SpyInstance; beforeAll(() => { + infoSpy = jest.spyOn(console, 'info').mockImplementation(() => {}); nock(/.*/) .persist() .defaultReplyHeaders({ @@ -87,6 +89,7 @@ describe('vue useDLE()', () => { }); afterAll(() => { + infoSpy.mockRestore(); nock.cleanAll(); }); diff --git a/packages/vue/src/__tests__/useLive.web.ts b/packages/vue/src/__tests__/useLive.web.ts index f0627f97966c..ed4a7c39ca9c 100644 --- a/packages/vue/src/__tests__/useLive.web.ts +++ b/packages/vue/src/__tests__/useLive.web.ts @@ -19,6 +19,14 @@ const payload = { let currentPollingPayload: typeof payload = { ...payload }; describe('vue useLive()', () => { + let infoSpy: jest.SpyInstance; + beforeEach(() => { + infoSpy = jest.spyOn(console, 'info').mockImplementation(() => {}); + }); + afterEach(() => { + infoSpy.mockRestore(); + }); + async function flushUntil( wrapper: any, predicate: () => boolean, diff --git a/packages/vue/src/__tests__/useQuery.web.ts b/packages/vue/src/__tests__/useQuery.web.ts index ed5e500aca09..ae0c33cb99db 100644 --- a/packages/vue/src/__tests__/useQuery.web.ts +++ b/packages/vue/src/__tests__/useQuery.web.ts @@ -48,6 +48,14 @@ const nested = [ ]; describe('vue useQuery()', () => { + let infoSpy: jest.SpyInstance; + beforeEach(() => { + infoSpy = jest.spyOn(console, 'info').mockImplementation(() => {}); + }); + afterEach(() => { + infoSpy.mockRestore(); + }); + it('should be undefined with empty state', async () => { const { result } = await renderDataCompose(() => { return useQuery(ArticleWithSlug, { id: payloadSlug.id }); diff --git a/packages/vue/src/__tests__/useSubscription.web.ts b/packages/vue/src/__tests__/useSubscription.web.ts index 48ff9d61915c..b9e08e08fdf4 100644 --- a/packages/vue/src/__tests__/useSubscription.web.ts +++ b/packages/vue/src/__tests__/useSubscription.web.ts @@ -28,7 +28,12 @@ describe('vue useSubscription()', () => { tags: ['a', 'best', 'react'], }; + let infoSpy: jest.SpyInstance; + beforeEach(() => { + infoSpy = jest.spyOn(console, 'info').mockImplementation(() => {}); + }); afterEach(() => { + infoSpy.mockRestore(); jest.useRealTimers(); }); diff --git a/packages/vue/src/__tests__/useSuspense.web.ts b/packages/vue/src/__tests__/useSuspense.web.ts index 6855bd01f655..acf62667deb5 100644 --- a/packages/vue/src/__tests__/useSuspense.web.ts +++ b/packages/vue/src/__tests__/useSuspense.web.ts @@ -25,6 +25,14 @@ const payload2 = { }; describe('vue useSuspense()', () => { + let infoSpy: jest.SpyInstance; + beforeEach(() => { + infoSpy = jest.spyOn(console, 'info').mockImplementation(() => {}); + }); + afterEach(() => { + infoSpy.mockRestore(); + }); + async function flushUntil( wrapper: any, predicate: () => boolean, diff --git a/packages/vue/src/providers/__tests__/MockPlugin.test.ts b/packages/vue/src/providers/__tests__/MockPlugin.test.ts index ca6e0903c340..67b8aa8072cd 100644 --- a/packages/vue/src/providers/__tests__/MockPlugin.test.ts +++ b/packages/vue/src/providers/__tests__/MockPlugin.test.ts @@ -24,11 +24,14 @@ import { MockPlugin } from '../../test/MockPlugin'; import { DataClientPlugin } from '../DataClientPlugin'; describe('MockPlugin', () => { + let warnSpy: jest.Spied; beforeEach(() => { jest.clearAllMocks(); + warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); }); afterEach(() => { + warnSpy.mockRestore(); jest.restoreAllMocks(); }); diff --git a/packages/vue/src/test/__tests__/mountDataClient.test.ts b/packages/vue/src/test/__tests__/mountDataClient.test.ts index cb16994e1b7b..f7beec36f5d3 100644 --- a/packages/vue/src/test/__tests__/mountDataClient.test.ts +++ b/packages/vue/src/test/__tests__/mountDataClient.test.ts @@ -4,6 +4,14 @@ import { CoolerArticleResource } from '../../../../../__tests__/new'; import { mountDataClient } from '../mountDataClient'; describe('mountDataClient', () => { + let infoSpy: jest.SpyInstance; + beforeEach(() => { + infoSpy = jest.spyOn(console, 'info').mockImplementation(() => {}); + }); + afterEach(() => { + infoSpy.mockRestore(); + }); + const payload = { id: 5, title: 'hi ho', diff --git a/packages/vue/src/test/__tests__/renderDataCompose.test.ts b/packages/vue/src/test/__tests__/renderDataCompose.test.ts index f5f55a8cc15a..28ba3a79bbda 100644 --- a/packages/vue/src/test/__tests__/renderDataCompose.test.ts +++ b/packages/vue/src/test/__tests__/renderDataCompose.test.ts @@ -4,6 +4,14 @@ import { CoolerArticleResource } from '../../../../../__tests__/new'; import { renderDataCompose } from '../renderDataCompose'; describe('renderDataCompose', () => { + let infoSpy: jest.SpyInstance; + beforeEach(() => { + infoSpy = jest.spyOn(console, 'info').mockImplementation(() => {}); + }); + afterEach(() => { + infoSpy.mockRestore(); + }); + const payload = { id: 5, title: 'hi ho', diff --git a/website/src/components/Playground/editor-types/@data-client/rest.d.ts b/website/src/components/Playground/editor-types/@data-client/rest.d.ts index ffadd1b60e0c..45cd235cfc04 100644 --- a/website/src/components/Playground/editor-types/@data-client/rest.d.ts +++ b/website/src/components/Playground/editor-types/@data-client/rest.d.ts @@ -1250,24 +1250,24 @@ type ExtractCollection = S extends ({ } ? ExtractObject : never; type CleanKey = S extends `"${infer K}"` ? K : S; -type OnlyOptional = S extends `${infer K}}` ? CleanKey : never; -type OnlyRequired = S extends `${string}}` ? never : CleanKey; +type KeyName = CleanKey; +type KeyVal = K extends `*${string}` ? string[] : string | number; /** Parameters for a given path */ type PathArgs = PathKeys extends never ? unknown : KeysToArgs>; /** Computes the union of keys for a path string */ -type PathKeys = string extends S ? string : S extends `${infer A}\\${':' | '*' | '}'}${infer B}` ? PathKeys | PathKeys : PathSplits; -type PathSplits = S extends (`${string}${':' | '*'}${infer K}${'/' | '\\' | '%' | '&' | '*' | '{' | ';' | ',' | '!' | '@'}${infer R}`) ? PathSplits<`${':' | '*'}${K}`> | PathSplits : S extends `${string}${':' | '*'}${infer K}${':' | '*'}${infer R}` ? PathSplits<`${':' | '*'}${K}`> | PathSplits<`${':' | '*'}${R}`> : S extends `${string}${':' | '*'}${infer K}` ? K : never; +type PathKeys = string extends S ? string : S extends `${infer A}\\${':' | '*' | '}'}${infer B}` ? PathKeys | PathKeys : Splits | Splits; +type Splits = S extends `${string}${M}${infer K}${M}${infer R}` ? Splits<`${M}${K}`, M> | Splits<`${M}${R}`, M> : S extends (`${string}${M}${infer K}${'/' | '\\' | '%' | '&' | '*' | ':' | '{' | ';' | ',' | '!' | '@'}${infer R}`) ? Splits<`${M}${K}`, M> | Splits : S extends `${string}${M}${infer K}` ? M extends '*' ? `*${K}` : K : never; type KeysToArgs = { - [K in Key as OnlyOptional]?: string | number; -} & (OnlyRequired extends never ? unknown : { - [K in Key as OnlyRequired]: string | number; + [K in Key as K extends `${string}}` ? KeyName : never]?: KeyVal; +} & (Exclude extends never ? unknown : { + [K in Key as K extends `${string}}` ? never : KeyName]: KeyVal; }); -type PathArgsAndSearch = OnlyRequired> extends never ? Record | undefined : { - [K in PathKeys as OnlyRequired]: string | number; -} & Record; -/** Removes the last :token */ -type ShortenPath = string extends S ? string : S extends `${infer B}:${infer R}` ? TrimColon<`${B}:${ShortenPath}`> : ''; -type TrimColon = string extends S ? string : S extends `${infer R}:` ? R : S; +type PathArgsAndSearch = Exclude, `${string}}`> extends never ? Record | undefined : { + [K in PathKeys as K extends `${string}}` ? never : KeyName]: KeyVal; +} & Record; +/** Removes the last :param or *wildcard token */ +type ShortenPath = string extends S ? string : S extends `${infer B}:${infer R}` ? TrimToken<`${B}:${ShortenPath}`> : S extends `${infer B}*${infer R}` ? TrimToken<`${B}*${ShortenPath}`> : ''; +type TrimToken = string extends S ? string : S extends `${infer R}:` ? R : S extends `${infer R}*` ? R : S; type ResourcePath = string; type OptionsToFunction = S extends ({ } ? ExtractObject : never; type CleanKey = S extends `"${infer K}"` ? K : S; -type OnlyOptional = S extends `${infer K}}` ? CleanKey : never; -type OnlyRequired = S extends `${string}}` ? never : CleanKey; +type KeyName = CleanKey; +type KeyVal = K extends `*${string}` ? string[] : string | number; /** Parameters for a given path */ type PathArgs = PathKeys extends never ? unknown : KeysToArgs>; /** Computes the union of keys for a path string */ -type PathKeys = string extends S ? string : S extends `${infer A}\\${':' | '*' | '}'}${infer B}` ? PathKeys | PathKeys : PathSplits; -type PathSplits = S extends (`${string}${':' | '*'}${infer K}${'/' | '\\' | '%' | '&' | '*' | '{' | ';' | ',' | '!' | '@'}${infer R}`) ? PathSplits<`${':' | '*'}${K}`> | PathSplits : S extends `${string}${':' | '*'}${infer K}${':' | '*'}${infer R}` ? PathSplits<`${':' | '*'}${K}`> | PathSplits<`${':' | '*'}${R}`> : S extends `${string}${':' | '*'}${infer K}` ? K : never; +type PathKeys = string extends S ? string : S extends `${infer A}\\${':' | '*' | '}'}${infer B}` ? PathKeys | PathKeys : Splits | Splits; +type Splits = S extends `${string}${M}${infer K}${M}${infer R}` ? Splits<`${M}${K}`, M> | Splits<`${M}${R}`, M> : S extends (`${string}${M}${infer K}${'/' | '\\' | '%' | '&' | '*' | ':' | '{' | ';' | ',' | '!' | '@'}${infer R}`) ? Splits<`${M}${K}`, M> | Splits : S extends `${string}${M}${infer K}` ? M extends '*' ? `*${K}` : K : never; type KeysToArgs = { - [K in Key as OnlyOptional]?: string | number; -} & (OnlyRequired extends never ? unknown : { - [K in Key as OnlyRequired]: string | number; + [K in Key as K extends `${string}}` ? KeyName : never]?: KeyVal; +} & (Exclude extends never ? unknown : { + [K in Key as K extends `${string}}` ? never : KeyName]: KeyVal; }); -type PathArgsAndSearch = OnlyRequired> extends never ? Record | undefined : { - [K in PathKeys as OnlyRequired]: string | number; -} & Record; -/** Removes the last :token */ -type ShortenPath = string extends S ? string : S extends `${infer B}:${infer R}` ? TrimColon<`${B}:${ShortenPath}`> : ''; -type TrimColon = string extends S ? string : S extends `${infer R}:` ? R : S; +type PathArgsAndSearch = Exclude, `${string}}`> extends never ? Record | undefined : { + [K in PathKeys as K extends `${string}}` ? never : KeyName]: KeyVal; +} & Record; +/** Removes the last :param or *wildcard token */ +type ShortenPath = string extends S ? string : S extends `${infer B}:${infer R}` ? TrimToken<`${B}:${ShortenPath}`> : S extends `${infer B}*${infer R}` ? TrimToken<`${B}*${ShortenPath}`> : ''; +type TrimToken = string extends S ? string : S extends `${infer R}:` ? R : S extends `${infer R}*` ? R : S; type ResourcePath = string; type OptionsToFunction