diff --git a/.changeset/real-rockets-divide.md b/.changeset/real-rockets-divide.md new file mode 100644 index 0000000000..2506a89f22 --- /dev/null +++ b/.changeset/real-rockets-divide.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Minor doc string changes diff --git a/.changeset/thin-chairs-ring.md b/.changeset/thin-chairs-ring.md new file mode 100644 index 0000000000..ebcb3afa93 --- /dev/null +++ b/.changeset/thin-chairs-ring.md @@ -0,0 +1,8 @@ +--- +'@backstage/test-utils': minor +'@backstage/frontend-test-utils': patch +--- + +Added a `mockApis` export, which will replace the `MockX` API implementation classes and their related types. This is analogous with the backend's `mockServices`. + +Deprecated `MockConfigApi`, please use `mockApis.config` instead. diff --git a/packages/backend-test-utils/report.api.md b/packages/backend-test-utils/report.api.md index 1235cd7cb3..d9c1c4c721 100644 --- a/packages/backend-test-utils/report.api.md +++ b/packages/backend-test-utils/report.api.md @@ -150,7 +150,7 @@ export function mockErrorHandler(): ErrorRequestHandler< Record >; -// @public (undocumented) +// @public export namespace mockServices { // (undocumented) export function auth(options?: { @@ -491,65 +491,64 @@ export class TestDatabases { // src/next/services/mockCredentials.d.ts:116:9 - (ae-undocumented) Missing documentation for "invalidToken". // src/next/services/mockCredentials.d.ts:117:9 - (ae-undocumented) Missing documentation for "invalidHeader". // src/next/services/mockServices.d.ts:5:1 - (ae-undocumented) Missing documentation for "ServiceMock". -// src/next/services/mockServices.d.ts:13:1 - (ae-undocumented) Missing documentation for "mockServices". -// src/next/services/mockServices.d.ts:14:5 - (ae-undocumented) Missing documentation for "rootConfig". -// src/next/services/mockServices.d.ts:15:5 - (ae-undocumented) Missing documentation for "rootConfig". -// src/next/services/mockServices.d.ts:16:9 - (ae-undocumented) Missing documentation for "Options". -// src/next/services/mockServices.d.ts:19:15 - (ae-undocumented) Missing documentation for "factory". -// src/next/services/mockServices.d.ts:20:15 - (ae-undocumented) Missing documentation for "mock". -// src/next/services/mockServices.d.ts:22:5 - (ae-undocumented) Missing documentation for "rootLogger". -// src/next/services/mockServices.d.ts:23:5 - (ae-undocumented) Missing documentation for "rootLogger". -// src/next/services/mockServices.d.ts:24:9 - (ae-undocumented) Missing documentation for "Options". -// src/next/services/mockServices.d.ts:27:15 - (ae-undocumented) Missing documentation for "factory". -// src/next/services/mockServices.d.ts:28:15 - (ae-undocumented) Missing documentation for "mock". -// src/next/services/mockServices.d.ts:30:5 - (ae-undocumented) Missing documentation for "auth". -// src/next/services/mockServices.d.ts:34:5 - (ae-undocumented) Missing documentation for "auth". -// src/next/services/mockServices.d.ts:35:15 - (ae-undocumented) Missing documentation for "factory". -// src/next/services/mockServices.d.ts:36:15 - (ae-undocumented) Missing documentation for "mock". -// src/next/services/mockServices.d.ts:38:5 - (ae-undocumented) Missing documentation for "discovery". -// src/next/services/mockServices.d.ts:39:5 - (ae-undocumented) Missing documentation for "discovery". -// src/next/services/mockServices.d.ts:40:15 - (ae-undocumented) Missing documentation for "factory". -// src/next/services/mockServices.d.ts:41:15 - (ae-undocumented) Missing documentation for "mock". -// src/next/services/mockServices.d.ts:61:5 - (ae-undocumented) Missing documentation for "httpAuth". -// src/next/services/mockServices.d.ts:72:15 - (ae-undocumented) Missing documentation for "mock". -// src/next/services/mockServices.d.ts:82:5 - (ae-undocumented) Missing documentation for "userInfo". -// src/next/services/mockServices.d.ts:90:15 - (ae-undocumented) Missing documentation for "mock". -// src/next/services/mockServices.d.ts:92:5 - (ae-undocumented) Missing documentation for "cache". -// src/next/services/mockServices.d.ts:93:15 - (ae-undocumented) Missing documentation for "factory". -// src/next/services/mockServices.d.ts:94:15 - (ae-undocumented) Missing documentation for "mock". -// src/next/services/mockServices.d.ts:96:5 - (ae-undocumented) Missing documentation for "database". -// src/next/services/mockServices.d.ts:97:15 - (ae-undocumented) Missing documentation for "factory". -// src/next/services/mockServices.d.ts:98:15 - (ae-undocumented) Missing documentation for "mock". -// src/next/services/mockServices.d.ts:100:5 - (ae-undocumented) Missing documentation for "rootHealth". -// src/next/services/mockServices.d.ts:101:15 - (ae-undocumented) Missing documentation for "factory". -// src/next/services/mockServices.d.ts:102:15 - (ae-undocumented) Missing documentation for "mock". -// src/next/services/mockServices.d.ts:104:5 - (ae-undocumented) Missing documentation for "httpRouter". -// src/next/services/mockServices.d.ts:105:15 - (ae-undocumented) Missing documentation for "factory". -// src/next/services/mockServices.d.ts:106:15 - (ae-undocumented) Missing documentation for "mock". -// src/next/services/mockServices.d.ts:108:5 - (ae-undocumented) Missing documentation for "rootHttpRouter". -// src/next/services/mockServices.d.ts:109:15 - (ae-undocumented) Missing documentation for "factory". -// src/next/services/mockServices.d.ts:110:15 - (ae-undocumented) Missing documentation for "mock". -// src/next/services/mockServices.d.ts:112:5 - (ae-undocumented) Missing documentation for "lifecycle". -// src/next/services/mockServices.d.ts:113:15 - (ae-undocumented) Missing documentation for "factory". -// src/next/services/mockServices.d.ts:114:15 - (ae-undocumented) Missing documentation for "mock". -// src/next/services/mockServices.d.ts:116:5 - (ae-undocumented) Missing documentation for "logger". -// src/next/services/mockServices.d.ts:117:15 - (ae-undocumented) Missing documentation for "factory". -// src/next/services/mockServices.d.ts:118:15 - (ae-undocumented) Missing documentation for "mock". -// src/next/services/mockServices.d.ts:120:5 - (ae-undocumented) Missing documentation for "permissions". -// src/next/services/mockServices.d.ts:121:15 - (ae-undocumented) Missing documentation for "factory". -// src/next/services/mockServices.d.ts:122:15 - (ae-undocumented) Missing documentation for "mock". -// src/next/services/mockServices.d.ts:124:5 - (ae-undocumented) Missing documentation for "rootLifecycle". -// src/next/services/mockServices.d.ts:125:15 - (ae-undocumented) Missing documentation for "factory". -// src/next/services/mockServices.d.ts:126:15 - (ae-undocumented) Missing documentation for "mock". -// src/next/services/mockServices.d.ts:128:5 - (ae-undocumented) Missing documentation for "scheduler". -// src/next/services/mockServices.d.ts:129:15 - (ae-undocumented) Missing documentation for "factory". -// src/next/services/mockServices.d.ts:130:15 - (ae-undocumented) Missing documentation for "mock". -// src/next/services/mockServices.d.ts:132:5 - (ae-undocumented) Missing documentation for "urlReader". -// src/next/services/mockServices.d.ts:133:15 - (ae-undocumented) Missing documentation for "factory". -// src/next/services/mockServices.d.ts:134:15 - (ae-undocumented) Missing documentation for "mock". -// src/next/services/mockServices.d.ts:136:5 - (ae-undocumented) Missing documentation for "events". -// src/next/services/mockServices.d.ts:137:15 - (ae-undocumented) Missing documentation for "factory". -// src/next/services/mockServices.d.ts:138:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:53:5 - (ae-undocumented) Missing documentation for "rootConfig". +// src/next/services/mockServices.d.ts:54:5 - (ae-undocumented) Missing documentation for "rootConfig". +// src/next/services/mockServices.d.ts:55:9 - (ae-undocumented) Missing documentation for "Options". +// src/next/services/mockServices.d.ts:58:15 - (ae-undocumented) Missing documentation for "factory". +// src/next/services/mockServices.d.ts:59:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:61:5 - (ae-undocumented) Missing documentation for "rootLogger". +// src/next/services/mockServices.d.ts:62:5 - (ae-undocumented) Missing documentation for "rootLogger". +// src/next/services/mockServices.d.ts:63:9 - (ae-undocumented) Missing documentation for "Options". +// src/next/services/mockServices.d.ts:66:15 - (ae-undocumented) Missing documentation for "factory". +// src/next/services/mockServices.d.ts:67:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:69:5 - (ae-undocumented) Missing documentation for "auth". +// src/next/services/mockServices.d.ts:73:5 - (ae-undocumented) Missing documentation for "auth". +// src/next/services/mockServices.d.ts:74:15 - (ae-undocumented) Missing documentation for "factory". +// src/next/services/mockServices.d.ts:75:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:77:5 - (ae-undocumented) Missing documentation for "discovery". +// src/next/services/mockServices.d.ts:78:5 - (ae-undocumented) Missing documentation for "discovery". +// src/next/services/mockServices.d.ts:79:15 - (ae-undocumented) Missing documentation for "factory". +// src/next/services/mockServices.d.ts:80:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:100:5 - (ae-undocumented) Missing documentation for "httpAuth". +// src/next/services/mockServices.d.ts:111:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:121:5 - (ae-undocumented) Missing documentation for "userInfo". +// src/next/services/mockServices.d.ts:129:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:131:5 - (ae-undocumented) Missing documentation for "cache". +// src/next/services/mockServices.d.ts:132:15 - (ae-undocumented) Missing documentation for "factory". +// src/next/services/mockServices.d.ts:133:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:135:5 - (ae-undocumented) Missing documentation for "database". +// src/next/services/mockServices.d.ts:136:15 - (ae-undocumented) Missing documentation for "factory". +// src/next/services/mockServices.d.ts:137:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:139:5 - (ae-undocumented) Missing documentation for "rootHealth". +// src/next/services/mockServices.d.ts:140:15 - (ae-undocumented) Missing documentation for "factory". +// src/next/services/mockServices.d.ts:141:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:143:5 - (ae-undocumented) Missing documentation for "httpRouter". +// src/next/services/mockServices.d.ts:144:15 - (ae-undocumented) Missing documentation for "factory". +// src/next/services/mockServices.d.ts:145:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:147:5 - (ae-undocumented) Missing documentation for "rootHttpRouter". +// src/next/services/mockServices.d.ts:148:15 - (ae-undocumented) Missing documentation for "factory". +// src/next/services/mockServices.d.ts:149:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:151:5 - (ae-undocumented) Missing documentation for "lifecycle". +// src/next/services/mockServices.d.ts:152:15 - (ae-undocumented) Missing documentation for "factory". +// src/next/services/mockServices.d.ts:153:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:155:5 - (ae-undocumented) Missing documentation for "logger". +// src/next/services/mockServices.d.ts:156:15 - (ae-undocumented) Missing documentation for "factory". +// src/next/services/mockServices.d.ts:157:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:159:5 - (ae-undocumented) Missing documentation for "permissions". +// src/next/services/mockServices.d.ts:160:15 - (ae-undocumented) Missing documentation for "factory". +// src/next/services/mockServices.d.ts:161:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:163:5 - (ae-undocumented) Missing documentation for "rootLifecycle". +// src/next/services/mockServices.d.ts:164:15 - (ae-undocumented) Missing documentation for "factory". +// src/next/services/mockServices.d.ts:165:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:167:5 - (ae-undocumented) Missing documentation for "scheduler". +// src/next/services/mockServices.d.ts:168:15 - (ae-undocumented) Missing documentation for "factory". +// src/next/services/mockServices.d.ts:169:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:171:5 - (ae-undocumented) Missing documentation for "urlReader". +// src/next/services/mockServices.d.ts:172:15 - (ae-undocumented) Missing documentation for "factory". +// src/next/services/mockServices.d.ts:173:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:175:5 - (ae-undocumented) Missing documentation for "events". +// src/next/services/mockServices.d.ts:176:15 - (ae-undocumented) Missing documentation for "factory". +// src/next/services/mockServices.d.ts:177:15 - (ae-undocumented) Missing documentation for "mock". // src/next/wiring/TestBackend.d.ts:5:1 - (ae-undocumented) Missing documentation for "TestBackendOptions". // src/next/wiring/TestBackend.d.ts:6:5 - (ae-undocumented) Missing documentation for "extensionPoints". // src/next/wiring/TestBackend.d.ts:14:5 - (ae-undocumented) Missing documentation for "features". diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts index 8b0f196399..789b75de9d 100644 --- a/packages/backend-test-utils/src/next/services/mockServices.ts +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -128,7 +128,46 @@ function simpleMock( } /** + * Mock implementations of the core services, to be used in tests. + * * @public + * @remarks + * + * There are some variations among the services depending on what needs tests + * might have, but overall there are three main usage patterns: + * + * 1. Creating an actual fake service instance, often with a simplified version + * of functionality, by calling the mock service itself as a function. + * + * ```ts + * // The function often accepts parameters that control its behavior + * const foo = mockServices.foo(); + * ``` + * + * 2. Creating a mock service, where all methods are replaced with jest mocks, by + * calling the service's `mock` function. + * + * ```ts + * // You can optionally supply a subset of its methods to implement + * const foo = mockServices.foo.mock({ + * someMethod: () => 'mocked result', + * }); + * // After exercising your test, you can make assertions on the mock: + * expect(foo.someMethod).toHaveBeenCalledTimes(2); + * expect(foo.otherMethod).toHaveBeenCalledWith(testData); + * ``` + * + * 3. Creating a service factory that behaves similarly to the mock as per above. + * + * ```ts + * await startTestBackend({ + * features: [ + * mockServices.foo.factory({ + * someMethod: () => 'mocked result', + * }) + * ], + * }); + * ``` */ export namespace mockServices { export function rootConfig(options?: rootConfig.Options): RootConfigService { diff --git a/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.test.ts index 1335808280..c1359ee6b2 100644 --- a/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.test.ts +++ b/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.test.ts @@ -17,7 +17,7 @@ import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi'; import { UrlPatternDiscovery } from '../../DiscoveryApi'; import BitbucketAuth from './BitbucketAuth'; -import { MockConfigApi } from '@backstage/test-utils'; +import { mockApis } from '@backstage/test-utils'; const getSession = jest.fn(); @@ -33,7 +33,7 @@ describe('BitbucketAuth', () => { jest.resetAllMocks(); }); - const configApi = new MockConfigApi({}); + const configApi = mockApis.config(); it.each([ ['team api write_repository', ['team', 'api', 'write_repository']], diff --git a/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/BitbucketServerAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/BitbucketServerAuth.test.ts index 5631a9a886..39f1d0dd68 100644 --- a/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/BitbucketServerAuth.test.ts +++ b/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/BitbucketServerAuth.test.ts @@ -17,7 +17,7 @@ import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi'; import { UrlPatternDiscovery } from '../../DiscoveryApi'; import BitbucketServerAuth from './BitbucketServerAuth'; -import { MockConfigApi } from '@backstage/test-utils'; +import { mockApis } from '@backstage/test-utils'; const getSession = jest.fn(); @@ -41,7 +41,7 @@ describe('BitbucketServerAuth', () => { ['PROJECT_ADMIN', 'REPO_READ', 'ACCOUNT_WRITE'], ], ])(`should normalize scopes correctly - %p`, (scope, scopes) => { - const configApi = new MockConfigApi({}); + const configApi = mockApis.config(); const bitbucketServerAuth = BitbucketServerAuth.create({ configApi: configApi, diff --git a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.test.ts index fc77754f6f..8a7bb9e671 100644 --- a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.test.ts +++ b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.test.ts @@ -17,7 +17,7 @@ import { UrlPatternDiscovery } from '../../DiscoveryApi'; import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi'; import GithubAuth from './GithubAuth'; -import { MockConfigApi } from '@backstage/test-utils'; +import { mockApis } from '@backstage/test-utils'; const getSession = jest.fn(); @@ -33,7 +33,7 @@ describe('GithubAuth', () => { jest.resetAllMocks(); }); - const configApi = new MockConfigApi({}); + const configApi = mockApis.config(); it('should forward access token request to session manager', async () => { const githubAuth = GithubAuth.create({ diff --git a/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.test.ts index 6dde0c0334..0818bbb253 100644 --- a/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.test.ts +++ b/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.test.ts @@ -17,7 +17,7 @@ import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi'; import { UrlPatternDiscovery } from '../../DiscoveryApi'; import GitlabAuth from './GitlabAuth'; -import { MockConfigApi } from '@backstage/test-utils'; +import { mockApis } from '@backstage/test-utils'; const getSession = jest.fn(); @@ -40,7 +40,7 @@ describe('GitlabAuth', () => { ], ['read_repository sudo', ['read_repository', 'sudo']], ])(`should normalize scopes correctly - %p`, (scope, scopes) => { - const configApi = new MockConfigApi({}); + const configApi = mockApis.config(); const gitlabAuth = GitlabAuth.create({ configApi: configApi, diff --git a/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.test.ts index 7897eef42e..a740b3089b 100644 --- a/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.test.ts +++ b/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.test.ts @@ -17,7 +17,7 @@ import GoogleAuth from './GoogleAuth'; import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi'; import { UrlPatternDiscovery } from '../../DiscoveryApi'; -import { MockConfigApi } from '@backstage/test-utils'; +import { mockApis } from '@backstage/test-utils'; const PREFIX = 'https://www.googleapis.com/auth/'; @@ -59,7 +59,7 @@ describe('GoogleAuth', () => { [`${PREFIX}profile`, [`${PREFIX}profile`]], [`${PREFIX}openid`, [`${PREFIX}openid`]], ])(`should normalize scopes correctly - %p`, (scope, scopes) => { - const configApi = new MockConfigApi({}); + const configApi = mockApis.config(); const googleAuth = GoogleAuth.create({ configApi: configApi, diff --git a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.test.ts b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.test.ts index 8ca818ebd8..a34589d746 100644 --- a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.test.ts +++ b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.test.ts @@ -17,7 +17,7 @@ import OAuth2 from './OAuth2'; import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi'; import { UrlPatternDiscovery } from '../../DiscoveryApi'; -import { MockConfigApi } from '@backstage/test-utils'; +import { mockApis } from '@backstage/test-utils'; const theFuture = new Date(Date.now() + 3600000); const thePast = new Date(Date.now() - 10); @@ -35,7 +35,7 @@ jest.mock('../../../../lib/AuthSessionManager', () => ({ }, })); -const configApi = new MockConfigApi({}); +const configApi = mockApis.config(); describe('OAuth2', () => { it('should get refreshed access token', async () => { diff --git a/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.test.ts index 5e7370a947..621172f8cc 100644 --- a/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.test.ts +++ b/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.test.ts @@ -17,7 +17,7 @@ import OktaAuth from './OktaAuth'; import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi'; import { UrlPatternDiscovery } from '../../DiscoveryApi'; -import { MockConfigApi } from '@backstage/test-utils'; +import { mockApis } from '@backstage/test-utils'; const PREFIX = 'okta.'; @@ -51,7 +51,7 @@ describe('OktaAuth', () => { [`${PREFIX}profile`, [`${PREFIX}profile`]], [`${PREFIX}openid`, [`${PREFIX}openid`]], ])(`should normalize scopes correctly - %p`, (scope, scopes) => { - const configApi = new MockConfigApi({}); + const configApi = mockApis.config(); const auth = OktaAuth.create({ configApi: configApi, oauthRequestApi: new MockOAuthApi(), diff --git a/packages/core-app-api/src/app/resolveRouteBindings.test.ts b/packages/core-app-api/src/app/resolveRouteBindings.test.ts index fdc1a9e456..e3dbbba0af 100644 --- a/packages/core-app-api/src/app/resolveRouteBindings.test.ts +++ b/packages/core-app-api/src/app/resolveRouteBindings.test.ts @@ -20,7 +20,7 @@ import { createRouteRef, } from '@backstage/core-plugin-api'; import { collectRouteIds, resolveRouteBindings } from './resolveRouteBindings'; -import { MockConfigApi } from '@backstage/test-utils'; +import { mockApis } from '@backstage/test-utils'; describe('resolveRouteBindings', () => { it('runs happy path', () => { @@ -30,7 +30,7 @@ describe('resolveRouteBindings', () => { ({ bind }) => { bind(external, { myRoute: ref }); }, - new MockConfigApi({}), + mockApis.config(), [], ); @@ -45,7 +45,7 @@ describe('resolveRouteBindings', () => { ({ bind }) => { bind(external, { someOtherRoute: ref } as any); }, - new MockConfigApi({}), + mockApis.config(), [], ), ).toThrow('Key someOtherRoute is not an existing external route'); @@ -56,8 +56,10 @@ describe('resolveRouteBindings', () => { const myTarget = createRouteRef({ id: 'test' }); const result = resolveRouteBindings( () => {}, - new MockConfigApi({ - app: { routes: { bindings: { 'test.mySource': 'test.myTarget' } } }, + mockApis.config({ + data: { + app: { routes: { bindings: { 'test.mySource': 'test.myTarget' } } }, + }, }), [ createPlugin({ @@ -84,8 +86,10 @@ describe('resolveRouteBindings', () => { ({ bind }) => { bind({ mySource }, { mySource: false }); }, - new MockConfigApi({ - app: { routes: { bindings: { 'test.mySource': 'myTarget' } } }, + mockApis.config({ + data: { + app: { routes: { bindings: { 'test.mySource': 'myTarget' } } }, + }, }), [ createPlugin({ @@ -106,8 +110,8 @@ describe('resolveRouteBindings', () => { ({ bind }) => { bind({ mySource }, { mySource: myTarget }); }, - new MockConfigApi({ - app: { routes: { bindings: { 'test.mySource': false } } }, + mockApis.config({ + data: { app: { routes: { bindings: { 'test.mySource': false } } } }, }), [ createPlugin({ @@ -128,7 +132,7 @@ describe('resolveRouteBindings', () => { expect(() => resolveRouteBindings( () => {}, - new MockConfigApi({ app: { routes: { bindings: 'derp' } } }), + mockApis.config({ data: { app: { routes: { bindings: 'derp' } } } }), [], ), ).toThrow( @@ -138,8 +142,8 @@ describe('resolveRouteBindings', () => { expect(() => resolveRouteBindings( () => {}, - new MockConfigApi({ - app: { routes: { bindings: { 'test.mySource': true } } }, + mockApis.config({ + data: { app: { routes: { bindings: { 'test.mySource': true } } } }, }), [], ), @@ -150,8 +154,10 @@ describe('resolveRouteBindings', () => { expect(() => resolveRouteBindings( () => {}, - new MockConfigApi({ - app: { routes: { bindings: { 'test.mySource': 'test.myTarget' } } }, + mockApis.config({ + data: { + app: { routes: { bindings: { 'test.mySource': 'test.myTarget' } } }, + }, }), [], ), @@ -162,8 +168,10 @@ describe('resolveRouteBindings', () => { expect(() => resolveRouteBindings( () => {}, - new MockConfigApi({ - app: { routes: { bindings: { 'test.mySource': 'test.myTarget' } } }, + mockApis.config({ + data: { + app: { routes: { bindings: { 'test.mySource': 'test.myTarget' } } }, + }, }), [ createPlugin({ @@ -198,17 +206,17 @@ describe('resolveRouteBindings', () => { }); // defaultTarget wins only if no bind or config matches - let result = resolveRouteBindings(() => {}, new MockConfigApi({}), [ - plugin, - ]); + let result = resolveRouteBindings(() => {}, mockApis.config(), [plugin]); expect(result.get(source)).toBe(target1); // config wins over defaultTarget result = resolveRouteBindings( () => {}, - new MockConfigApi({ - app: { routes: { bindings: { 'test.source': 'test.target2' } } }, + mockApis.config({ + data: { + app: { routes: { bindings: { 'test.source': 'test.target2' } } }, + }, }), [plugin], ); @@ -220,7 +228,7 @@ describe('resolveRouteBindings', () => { ({ bind }) => { bind(plugin.externalRoutes, { source: plugin.routes.target2 }); }, - new MockConfigApi({}), + mockApis.config(), [plugin], ); @@ -257,17 +265,15 @@ describe('collectRouteIds', () => { }); // resolves normally with no config - let result = resolveRouteBindings(() => {}, new MockConfigApi({}), [ - plugin, - ]); + let result = resolveRouteBindings(() => {}, mockApis.config(), [plugin]); expect(result.get(source)).toBe(target1); // can be disabled result = resolveRouteBindings( () => {}, - new MockConfigApi({ - app: { routes: { bindings: { 'test.source': false } } }, + mockApis.config({ + data: { app: { routes: { bindings: { 'test.source': false } } } }, }), [plugin], ); diff --git a/packages/core-components/src/components/SupportButton/SupportButton.test.tsx b/packages/core-components/src/components/SupportButton/SupportButton.test.tsx index 6b0e239aa5..9ed8fb8401 100644 --- a/packages/core-components/src/components/SupportButton/SupportButton.test.tsx +++ b/packages/core-components/src/components/SupportButton/SupportButton.test.tsx @@ -16,7 +16,7 @@ import { configApiRef } from '@backstage/core-plugin-api'; import { - MockConfigApi, + mockApis, renderInTestApp, TestApiProvider, } from '@backstage/test-utils'; @@ -24,17 +24,19 @@ import { act, fireEvent, screen } from '@testing-library/react'; import React from 'react'; import { SupportButton } from './SupportButton'; -const configApi = new MockConfigApi({ - app: { - support: { - url: 'https://github.com', - items: [ - { - title: 'Github', - icon: 'github', - links: [{ title: 'Github Issues', url: '/issues' }], - }, - ], +const configApi = mockApis.config({ + data: { + app: { + support: { + url: 'https://github.com', + items: [ + { + title: 'Github', + icon: 'github', + links: [{ title: 'Github Issues', url: '/issues' }], + }, + ], + }, }, }, }); diff --git a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts index 7ffe64d6d7..eac6e9d2aa 100644 --- a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts +++ b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts @@ -28,7 +28,7 @@ import { createFrontendPlugin, createRouteRef, } from '@backstage/frontend-plugin-api'; -import { MockConfigApi, TestApiRegistry } from '@backstage/test-utils'; +import { mockApis, TestApiRegistry } from '@backstage/test-utils'; import appPlugin from '@backstage/plugin-app'; import { readAppExtensionsConfig } from '../tree/readAppExtensionsConfig'; @@ -92,7 +92,7 @@ function routeInfoFromExtensions(extensions: ExtensionDefinition[]) { builtinExtensions: [ resolveExtensionDefinition(Root, { namespace: 'root' }), ], - parameters: readAppExtensionsConfig(new MockConfigApi({})), + parameters: readAppExtensionsConfig(mockApis.config()), forbidden: new Set(['root']), }), ); diff --git a/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx b/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx index 72594cefd1..6c5331a972 100644 --- a/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx +++ b/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx @@ -29,7 +29,7 @@ import { } from '@backstage/frontend-plugin-api'; import { screen, render } from '@testing-library/react'; import { createSpecializedApp } from './createSpecializedApp'; -import { MockConfigApi } from '@backstage/test-utils'; +import { mockApis } from '@backstage/test-utils'; import React from 'react'; import { configApiRef, @@ -98,7 +98,7 @@ describe('createSpecializedApp', () => { it('should forward config', () => { const app = createSpecializedApp({ - config: new MockConfigApi({ test: 'foo' }), + config: mockApis.config({ data: { test: 'foo' } }), features: [ createFrontendPlugin({ id: 'test', diff --git a/packages/frontend-defaults/src/createApp.test.tsx b/packages/frontend-defaults/src/createApp.test.tsx index 2890700653..06872a5691 100644 --- a/packages/frontend-defaults/src/createApp.test.tsx +++ b/packages/frontend-defaults/src/createApp.test.tsx @@ -26,7 +26,7 @@ import { } from '@backstage/frontend-plugin-api'; import { screen, waitFor } from '@testing-library/react'; import { CreateAppFeatureLoader, createApp } from './createApp'; -import { MockConfigApi, renderWithEffects } from '@backstage/test-utils'; +import { mockApis, renderWithEffects } from '@backstage/test-utils'; import React from 'react'; import { featureFlagsApiRef, useApi } from '@backstage/core-plugin-api'; import appPlugin from '@backstage/plugin-app'; @@ -35,12 +35,14 @@ describe('createApp', () => { it('should allow themes to be installed', async () => { const app = createApp({ configLoader: async () => ({ - config: new MockConfigApi({ - app: { - extensions: [ - { 'theme:app/light': false }, - { 'theme:app/dark': false }, - ], + config: mockApis.config({ + data: { + app: { + extensions: [ + { 'theme:app/light': false }, + { 'theme:app/dark': false }, + ], + }, }, }), }), @@ -72,7 +74,7 @@ describe('createApp', () => { it('should deduplicate features keeping the last received one', async () => { const duplicatedFeatureId = 'test'; const app = createApp({ - configLoader: async () => ({ config: new MockConfigApi({}) }), + configLoader: async () => ({ config: mockApis.config() }), features: [ createFrontendPlugin({ id: duplicatedFeatureId, @@ -135,7 +137,7 @@ describe('createApp', () => { const app = createApp({ configLoader: async () => ({ - config: new MockConfigApi({ key: 'config-value' }), + config: mockApis.config({ data: { key: 'config-value' } }), }), features: [appPlugin, loader], }); @@ -159,7 +161,7 @@ describe('createApp', () => { const app = createApp({ configLoader: async () => ({ - config: new MockConfigApi({}), + config: mockApis.config(), }), features: [loader], }); @@ -173,7 +175,7 @@ describe('createApp', () => { it('should register feature flags', async () => { const app = createApp({ - configLoader: async () => ({ config: new MockConfigApi({}) }), + configLoader: async () => ({ config: mockApis.config() }), features: [ appPlugin.withOverrides({ extensions: [ @@ -227,7 +229,7 @@ describe('createApp', () => { let appTreeApi: AppTreeApi | undefined = undefined; const app = createApp({ - configLoader: async () => ({ config: new MockConfigApi({}) }), + configLoader: async () => ({ config: mockApis.config() }), features: [ createFrontendPlugin({ id: 'my-plugin', diff --git a/packages/frontend-defaults/src/createPublicSignInApp.test.tsx b/packages/frontend-defaults/src/createPublicSignInApp.test.tsx index ac1adf9b0f..ce0f8c4b2a 100644 --- a/packages/frontend-defaults/src/createPublicSignInApp.test.tsx +++ b/packages/frontend-defaults/src/createPublicSignInApp.test.tsx @@ -22,7 +22,7 @@ import { import { render, screen, waitFor } from '@testing-library/react'; import React, { useEffect } from 'react'; import { createPublicSignInApp } from './createPublicSignInApp'; -import { MockConfigApi } from '@backstage/test-utils'; +import { mockApis } from '@backstage/test-utils'; describe('createPublicSignInApp', () => { beforeEach(() => { @@ -31,7 +31,7 @@ describe('createPublicSignInApp', () => { it('should render a sign-in page', async () => { const app = createPublicSignInApp({ - configLoader: async () => ({ config: new MockConfigApi({}) }), + configLoader: async () => ({ config: mockApis.config() }), features: [ createFrontendModule({ pluginId: 'app', @@ -59,7 +59,7 @@ describe('createPublicSignInApp', () => { .mockReturnValue(); const app = createPublicSignInApp({ - configLoader: async () => ({ config: new MockConfigApi({}) }), + configLoader: async () => ({ config: mockApis.config() }), features: [ createFrontendModule({ pluginId: 'app', diff --git a/packages/frontend-plugin-api/src/wiring/createFrontendPlugin.test.ts b/packages/frontend-plugin-api/src/wiring/createFrontendPlugin.test.ts index 65daad0586..678eff0b4c 100644 --- a/packages/frontend-plugin-api/src/wiring/createFrontendPlugin.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createFrontendPlugin.test.ts @@ -23,7 +23,7 @@ import { JsonObject } from '@backstage/types'; import { createExtension } from './createExtension'; import { createExtensionDataRef } from './createExtensionDataRef'; import { coreExtensionData } from './coreExtensionData'; -import { MockConfigApi, renderWithEffects } from '@backstage/test-utils'; +import { mockApis, renderWithEffects } from '@backstage/test-utils'; import { createExtensionInput } from './createExtensionInput'; const nameExtensionDataRef = createExtensionDataRef().with({ @@ -128,7 +128,7 @@ function createTestAppRoot({ }) { return createApp({ features: [...features], - configLoader: async () => ({ config: new MockConfigApi(config) }), + configLoader: async () => ({ config: mockApis.config({ data: config }) }), }).createRoot(); } diff --git a/packages/frontend-test-utils/package.json b/packages/frontend-test-utils/package.json index 49869b7df9..27bb592879 100644 --- a/packages/frontend-test-utils/package.json +++ b/packages/frontend-test-utils/package.json @@ -50,7 +50,6 @@ }, "peerDependencies": { "@testing-library/react": "^16.0.0", - "@types/jest": "*", "@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0", "react": "^16.13.1 || ^17.0.0 || ^18.0.0", "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0", diff --git a/packages/frontend-test-utils/report.api.md b/packages/frontend-test-utils/report.api.md index 3229da9898..dc37ec4fb4 100644 --- a/packages/frontend-test-utils/report.api.md +++ b/packages/frontend-test-utils/report.api.md @@ -3,13 +3,12 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -/// /// import { AnalyticsApi } from '@backstage/frontend-plugin-api'; import { AnalyticsEvent } from '@backstage/frontend-plugin-api'; import { AnyExtensionDataRef } from '@backstage/frontend-plugin-api'; -import { ApiFactory } from '@backstage/frontend-plugin-api'; +import { ApiMock } from '@backstage/test-utils'; import { AppNode } from '@backstage/frontend-plugin-api'; import { AppNodeInstance } from '@backstage/frontend-plugin-api'; import { ErrorWithContext } from '@backstage/test-utils'; @@ -18,6 +17,7 @@ import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; import { ExtensionDefinitionParameters } from '@backstage/frontend-plugin-api'; import { FrontendFeature } from '@backstage/frontend-app-api'; import { JsonObject } from '@backstage/types'; +import { mockApis } from '@backstage/test-utils'; import { MockConfigApi } from '@backstage/test-utils'; import { MockErrorApi } from '@backstage/test-utils'; import { MockErrorApiOptions } from '@backstage/test-utils'; @@ -34,14 +34,7 @@ import { TestApiProviderProps } from '@backstage/test-utils'; import { TestApiRegistry } from '@backstage/test-utils'; import { withLogCollector } from '@backstage/test-utils'; -// @public -export type ApiMock = { - factory: ApiFactory; -} & { - [Key in keyof TApi]: TApi[Key] extends (...args: infer Args) => infer Return - ? TApi[Key] & jest.MockInstance - : TApi[Key]; -}; +export { ApiMock }; // @public (undocumented) export function createExtensionTester( @@ -103,6 +96,8 @@ export class MockAnalyticsApi implements AnalyticsApi { getEvents(): AnalyticsEvent[]; } +export { mockApis }; + export { MockConfigApi }; export { MockErrorApi }; diff --git a/packages/frontend-test-utils/src/apis/index.ts b/packages/frontend-test-utils/src/apis/index.ts index 564c327aef..e5a0786cd0 100644 --- a/packages/frontend-test-utils/src/apis/index.ts +++ b/packages/frontend-test-utils/src/apis/index.ts @@ -24,7 +24,8 @@ export { MockPermissionApi, MockStorageApi, type MockStorageBucket, + mockApis, + type ApiMock, } from '@backstage/test-utils'; -export { type ApiMock } from './ApiMock'; export { MockAnalyticsApi } from './AnalyticsApi/MockAnalyticsApi'; diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 1c611fd584..47c61abae2 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -65,6 +65,7 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@testing-library/jest-dom": "^6.0.0", + "@types/jest": "*", "@types/react": "^18.0.0", "msw": "^1.0.0", "react": "^18.0.2", @@ -73,12 +74,16 @@ }, "peerDependencies": { "@testing-library/react": "^16.0.0", + "@types/jest": "*", "@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0", "react": "^16.13.1 || ^17.0.0 || ^18.0.0", "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0", "react-router-dom": "6.0.0-beta.0 || ^6.3.0" }, "peerDependenciesMeta": { + "@types/jest": { + "optional": true + }, "@types/react": { "optional": true } diff --git a/packages/test-utils/report.api.md b/packages/test-utils/report.api.md index 4a1f297c4e..a551d54c62 100644 --- a/packages/test-utils/report.api.md +++ b/packages/test-utils/report.api.md @@ -3,8 +3,11 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +/// + import { AnalyticsApi } from '@backstage/core-plugin-api'; import { AnalyticsEvent } from '@backstage/core-plugin-api'; +import { ApiFactory } from '@backstage/core-plugin-api'; import { ApiHolder } from '@backstage/core-plugin-api'; import { ApiRef } from '@backstage/core-plugin-api'; import { AppComponents } from '@backstage/core-plugin-api'; @@ -39,6 +42,15 @@ import { RouteRef } from '@backstage/core-plugin-api'; import { StorageApi } from '@backstage/core-plugin-api'; import { StorageValueSnapshot } from '@backstage/core-plugin-api'; +// @public +export type ApiMock = { + factory: ApiFactory; +} & { + [Key in keyof TApi]: TApi[Key] extends (...args: infer Args) => infer Return + ? TApi[Key] & jest.MockInstance + : TApi[Key]; +}; + // @public export type AsyncLogCollector = () => Promise; @@ -77,10 +89,27 @@ export class MockAnalyticsApi implements AnalyticsApi { getEvents(): AnalyticsEvent[]; } +// @public +export namespace mockApis { + export function config(options?: { data?: JsonObject }): jest.Mocked; + export namespace config { + const factory: ( + options?: + | { + data?: JsonObject | undefined; + } + | undefined, + ) => ApiFactory; + const mock: (partialImpl?: Partial | undefined) => ApiMock; + } +} + // @public @deprecated export function mockBreakpoint(options: { matches: boolean }): void; -// @public +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The reference is ambiguous because "config" has more than one declaration; you need to add a TSDoc member reference selector +// +// @public @deprecated export class MockConfigApi implements ConfigApi { constructor(data: JsonObject); get(key?: string): T; diff --git a/packages/frontend-test-utils/src/apis/ApiMock.ts b/packages/test-utils/src/testUtils/apis/ApiMock.ts similarity index 94% rename from packages/frontend-test-utils/src/apis/ApiMock.ts rename to packages/test-utils/src/testUtils/apis/ApiMock.ts index d11689f98a..fe66f35458 100644 --- a/packages/frontend-test-utils/src/apis/ApiMock.ts +++ b/packages/test-utils/src/testUtils/apis/ApiMock.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ApiFactory } from '@backstage/frontend-plugin-api'; +import { ApiFactory } from '@backstage/core-plugin-api'; /** * Represents a mocked version of an API, where you automatically have access to diff --git a/packages/test-utils/src/testUtils/apis/ConfigApi/MockConfigApi.ts b/packages/test-utils/src/testUtils/apis/ConfigApi/MockConfigApi.ts index 372c295c02..9d7462889c 100644 --- a/packages/test-utils/src/testUtils/apis/ConfigApi/MockConfigApi.ts +++ b/packages/test-utils/src/testUtils/apis/ConfigApi/MockConfigApi.ts @@ -23,6 +23,7 @@ import { ConfigApi } from '@backstage/core-plugin-api'; * that can be used to mock configuration using a plain object. * * @public + * @deprecated Use {@link mockApis.config} instead * @example * ```tsx * const mockConfig = new MockConfigApi({ diff --git a/packages/test-utils/src/testUtils/apis/index.ts b/packages/test-utils/src/testUtils/apis/index.ts index e122a4b432..94738a598e 100644 --- a/packages/test-utils/src/testUtils/apis/index.ts +++ b/packages/test-utils/src/testUtils/apis/index.ts @@ -20,3 +20,5 @@ export * from './ErrorApi'; export * from './FetchApi'; export * from './PermissionApi'; export * from './StorageApi'; +export { type ApiMock } from './ApiMock'; +export { mockApis } from './mockApis'; diff --git a/packages/test-utils/src/testUtils/apis/mockApis.test.tsx b/packages/test-utils/src/testUtils/apis/mockApis.test.tsx new file mode 100644 index 0000000000..b7a78867f8 --- /dev/null +++ b/packages/test-utils/src/testUtils/apis/mockApis.test.tsx @@ -0,0 +1,40 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { mockApis } from './mockApis'; + +describe('mockApis', () => { + describe('config', () => { + const data = { backend: { baseUrl: 'http://test.com' } }; + + it('can create an instance and make assertions on it', () => { + const empty = mockApis.config(); + const notEmpty = mockApis.config({ data }); + expect(empty.getOptional('backend.baseUrl')).toBeUndefined(); + expect(empty.getOptional).toHaveBeenCalledTimes(1); + expect(notEmpty.getOptional('backend.baseUrl')).toEqual( + 'http://test.com', + ); + expect(notEmpty.getOptional).toHaveBeenCalledTimes(1); + }); + + it('can create a mock and make assertions on it', async () => { + const mock = mockApis.config.mock({ getString: () => 'replaced' }); + expect(mock.getString('a')).toEqual('replaced'); + expect(mock.getString).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/packages/test-utils/src/testUtils/apis/mockApis.ts b/packages/test-utils/src/testUtils/apis/mockApis.ts new file mode 100644 index 0000000000..d084fae332 --- /dev/null +++ b/packages/test-utils/src/testUtils/apis/mockApis.ts @@ -0,0 +1,188 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ConfigReader } from '@backstage/config'; +import { + ApiFactory, + ApiRef, + configApiRef, + createApiFactory, +} from '@backstage/core-plugin-api'; +import { JsonObject } from '@backstage/types'; +import { ApiMock } from './ApiMock'; + +/** @internal */ +function simpleInstance( + _ref: ApiRef, + instance: TApi, + mockSkeleton: () => jest.Mocked, +): jest.Mocked { + const mock = mockSkeleton(); + const result = Object.create(instance) as any; + for (const [key, impl] of Object.entries(mock)) { + result[key] = (impl as any).mockImplementation((instance as any)[key]); + } + return result; +} + +/** @internal */ +function simpleFactory( + ref: ApiRef, + factory: (...args: TArgs) => TApi, +): (...args: TArgs) => ApiFactory { + return (...args) => + createApiFactory({ + api: ref, + deps: {}, + factory: () => factory(...args), + }); +} + +/** @internal */ +function simpleMock( + ref: ApiRef, + mockFactory: () => jest.Mocked, +): (partialImpl?: Partial) => ApiMock { + return partialImpl => { + const mock = mockFactory(); + if (partialImpl) { + for (const [key, impl] of Object.entries(partialImpl)) { + if (typeof impl === 'function') { + (mock as any)[key].mockImplementation(impl); + } else { + (mock as any)[key] = impl; + } + } + } + return Object.assign(mock, { + factory: createApiFactory({ + api: ref, + deps: {}, + factory: () => mock, + }), + }) as ApiMock; + }; +} + +/** + * Mock implementations of the core utility APIs, to be used in tests. + * + * @public + * @remarks + * + * There are some variations among the APIs depending on what needs tests + * might have, but overall there are three main usage patterns: + * + * 1: Creating an actual fake API instance, often with a simplified version + * of functionality, by calling the mock API itself as a function. + * + * ```ts + * // The function often accepts parameters that control its behavior + * const foo = mockApis.foo(); + * ``` + * + * 2: Creating a mock API, where all methods are replaced with jest mocks, by + * calling the API's `mock` function. + * + * ```ts + * // You can optionally supply a subset of its methods to implement + * const foo = mockApis.foo.mock({ + * someMethod: () => 'mocked result', + * }); + * // After exercising your test, you can make assertions on the mock: + * expect(foo.someMethod).toHaveBeenCalledTimes(2); + * expect(foo.otherMethod).toHaveBeenCalledWith(testData); + * ``` + * + * 3: Creating an API factory that behaves similarly to the mock as per above. + * + * ```ts + * const factory = mockApis.foo.factory({ + * someMethod: () => 'mocked result', + * }); + * ``` + */ +export namespace mockApis { + const configMockSkeleton = () => ({ + has: jest.fn(), + keys: jest.fn(), + get: jest.fn(), + getOptional: jest.fn(), + getConfig: jest.fn(), + getOptionalConfig: jest.fn(), + getConfigArray: jest.fn(), + getOptionalConfigArray: jest.fn(), + getNumber: jest.fn(), + getOptionalNumber: jest.fn(), + getBoolean: jest.fn(), + getOptionalBoolean: jest.fn(), + getString: jest.fn(), + getOptionalString: jest.fn(), + getStringArray: jest.fn(), + getOptionalStringArray: jest.fn(), + }); + /** + * Fake implementation of {@link @backstage/frontend-plugin-api#ConfigApi} + * with optional data supplied. + * + * @public + * @example + * + * ```tsx + * const config = mockApis.config({ + * data: { app: { baseUrl: 'https://example.com' } }, + * }); + * + * const rendered = await renderInTestApp( + * + * + * , + * ); + * ``` + */ + export function config(options?: { data?: JsonObject }) { + return simpleInstance( + configApiRef, + new ConfigReader(options?.data, 'mock-config'), + configMockSkeleton, + ); + } + /** + * Mock helpers for {@link @backstage/frontend-plugin-api#ConfigApi}. + * + * @see {@link @backstage/frontend-plugin-api#mockApis.config} + * @public + */ + export namespace config { + /** + * Creates a factory for a fake implementation of + * {@link @backstage/frontend-plugin-api#ConfigApi} with optional + * configuration data supplied. + * + * @public + */ + export const factory = simpleFactory(configApiRef, config); + /** + * Creates a mock implementation of + * {@link @backstage/frontend-plugin-api#ConfigApi}. All methods are + * replaced with jest mock functions, and you can optionally pass in a + * subset of methods with an explicit implementation. + * + * @public + */ + export const mock = simpleMock(configApiRef, configMockSkeleton); + } +} diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.test.tsx index 1ed8026a2b..f832c842a5 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.test.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.test.tsx @@ -16,7 +16,7 @@ import { Entity } from '@backstage/catalog-model'; import { configApiRef } from '@backstage/core-plugin-api'; -import { MockConfigApi, TestApiProvider } from '@backstage/test-utils'; +import { mockApis, TestApiProvider } from '@backstage/test-utils'; import { makeStyles } from '@material-ui/core/styles'; import { render, screen } from '@testing-library/react'; import { renderHook } from '@testing-library/react'; @@ -46,7 +46,7 @@ const entities: Entity[] = [ }, ]; -const mockConfigApi = new MockConfigApi({}); +const mockConfigApi = mockApis.config(); const apis = [[configApiRef, mockConfigApi]] as const; describe('', () => { @@ -122,10 +122,12 @@ describe('', () => { apis={[ [ configApiRef, - new MockConfigApi({ - catalog: { - import: { - entityFilename: 'anvil.yaml', + mockApis.config({ + data: { + catalog: { + import: { + entityFilename: 'anvil.yaml', + }, }, }, }), diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx index 6e81b27c55..acc2715fec 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx @@ -16,7 +16,7 @@ import { configApiRef, errorApiRef } from '@backstage/core-plugin-api'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { TestApiProvider, MockConfigApi } from '@backstage/test-utils'; +import { TestApiProvider, mockApis } from '@backstage/test-utils'; import TextField from '@material-ui/core/TextField'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; @@ -43,7 +43,7 @@ describe('', () => { post: jest.fn(), }; - const configApi = new MockConfigApi({}); + const configApi = mockApis.config(); const Wrapper = ({ children }: { children?: React.ReactNode }) => ( { describe('readFilterConfig', () => { it('returns filter data', async () => { - const mockConfig = new MockConfigApi({ - field: 'pathname', - operator: '==', - value: '/home', + const mockConfig = mockApis.config({ + data: { + field: 'pathname', + operator: '==', + value: '/home', + }, }); const res = readFilterConfig(mockConfig); expect(res).toEqual({ @@ -34,10 +36,12 @@ describe('config', () => { }); it('returns undefined for invalid filter', async () => { - const mockInvalidConfig = new MockConfigApi({ - myField: 'pathname', - operator: '==', - value: '3', + const mockInvalidConfig = mockApis.config({ + data: { + myField: 'pathname', + operator: '==', + value: '3', + }, }); const res = readFilterConfig(mockInvalidConfig); expect(res).toEqual(undefined); @@ -46,15 +50,19 @@ describe('config', () => { describe('createFilterByQueryParamFromConfig', () => { it('returns filter data', async () => { - const mockConfig1 = new MockConfigApi({ - field: 'id', - operator: '==', - value: '3', + const mockConfig1 = mockApis.config({ + data: { + field: 'id', + operator: '==', + value: '3', + }, }); - const mockConfig2 = new MockConfigApi({ - field: 'pathname', - operator: '==', - value: 'path', + const mockConfig2 = mockApis.config({ + data: { + field: 'pathname', + operator: '==', + value: 'path', + }, }); const res = createFilterByQueryParamFromConfig([ mockConfig1, @@ -67,15 +75,19 @@ describe('config', () => { }); it('returns only valid filters', async () => { - const mockValidConfig = new MockConfigApi({ - field: 'id', - operator: '==', - value: 3, + const mockValidConfig = mockApis.config({ + data: { + field: 'id', + operator: '==', + value: 3, + }, }); - const mockInvalidConfig = new MockConfigApi({ - myField: 'pathname', - operator: '==', - value: 'path', + const mockInvalidConfig = mockApis.config({ + data: { + myField: 'pathname', + operator: '==', + value: 'path', + }, }); const res = createFilterByQueryParamFromConfig([ mockValidConfig, diff --git a/plugins/home/src/homePageComponents/VisitedByType/Content.test.tsx b/plugins/home/src/homePageComponents/VisitedByType/Content.test.tsx index 055ebf998b..ec2d9eb068 100644 --- a/plugins/home/src/homePageComponents/VisitedByType/Content.test.tsx +++ b/plugins/home/src/homePageComponents/VisitedByType/Content.test.tsx @@ -19,7 +19,7 @@ import { Content } from './Content'; import { TestApiProvider, renderInTestApp, - MockConfigApi, + mockApis, } from '@backstage/test-utils'; import { visitsApiRef } from '../../api'; import { ContextProvider } from './Context'; @@ -138,16 +138,18 @@ describe('', () => { }); it('allows recent items to be filtered using config', async () => { - const configApiMock = new MockConfigApi({ - home: { - recentVisits: { - filterBy: [ - { - field: 'pathname', - operator: '==', - value: '/tech-radar', - }, - ], + const configApiMock = mockApis.config({ + data: { + home: { + recentVisits: { + filterBy: [ + { + field: 'pathname', + operator: '==', + value: '/tech-radar', + }, + ], + }, }, }, }); @@ -206,20 +208,22 @@ describe('', () => { }); it('allows recent items to have no filter if the filter config is not valid', async () => { - const configApiMock = new MockConfigApi({ - home: { - recentVisits: { - filterBy: [ - { - operator: '==', - value: '/tech-radar', - }, - { - field: 'pathname', - operator: '==', - value: '/explore', - }, - ], + const configApiMock = mockApis.config({ + data: { + home: { + recentVisits: { + filterBy: [ + { + operator: '==', + value: '/tech-radar', + }, + { + field: 'pathname', + operator: '==', + value: '/explore', + }, + ], + }, }, }, }); @@ -283,16 +287,18 @@ describe('', () => { }); it('allows top items to be filtered using config', async () => { - const configApiMock = new MockConfigApi({ - home: { - topVisits: { - filterBy: [ - { - field: 'pathname', - operator: '==', - value: '/explore', - }, - ], + const configApiMock = mockApis.config({ + data: { + home: { + topVisits: { + filterBy: [ + { + field: 'pathname', + operator: '==', + value: '/explore', + }, + ], + }, }, }, }); diff --git a/plugins/search-react/src/components/SearchFilter/SearchFilter.Autocomplete.test.tsx b/plugins/search-react/src/components/SearchFilter/SearchFilter.Autocomplete.test.tsx index abf5e828f6..bbc6454613 100644 --- a/plugins/search-react/src/components/SearchFilter/SearchFilter.Autocomplete.test.tsx +++ b/plugins/search-react/src/components/SearchFilter/SearchFilter.Autocomplete.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { MockConfigApi, TestApiProvider } from '@backstage/test-utils'; +import { mockApis, TestApiProvider } from '@backstage/test-utils'; import { screen, render, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; @@ -35,10 +35,12 @@ const SearchContextFilterSpy = ({ name }: { name: string }) => { }; describe('SearchFilter.Autocomplete', () => { - const configApiMock = new MockConfigApi({ - search: { - query: { - pageLimit: 100, + const configApiMock = mockApis.config({ + data: { + search: { + query: { + pageLimit: 100, + }, }, }, }); diff --git a/plugins/search-react/src/components/SearchFilter/SearchFilter.test.tsx b/plugins/search-react/src/components/SearchFilter/SearchFilter.test.tsx index c51a4832e3..e155a093d5 100644 --- a/plugins/search-react/src/components/SearchFilter/SearchFilter.test.tsx +++ b/plugins/search-react/src/components/SearchFilter/SearchFilter.test.tsx @@ -22,7 +22,7 @@ import { configApiRef } from '@backstage/core-plugin-api'; import { SearchContextProvider } from '../../context'; import { SearchFilter } from './SearchFilter'; -import { MockConfigApi, TestApiProvider } from '@backstage/test-utils'; +import { mockApis, TestApiProvider } from '@backstage/test-utils'; import { searchApiRef } from '../../api'; describe('SearchFilter', () => { @@ -37,10 +37,12 @@ describe('SearchFilter', () => { const values = ['value1', 'value2']; const filters = { unrelated: 'unrelated' }; - const configApiMock = new MockConfigApi({ - search: { - query: { - pagelimit: 10, + const configApiMock = mockApis.config({ + data: { + search: { + query: { + pagelimit: 10, + }, }, }, }); diff --git a/plugins/search-react/src/components/SearchFilter/hooks.test.tsx b/plugins/search-react/src/components/SearchFilter/hooks.test.tsx index 884512963f..28a6fbfd60 100644 --- a/plugins/search-react/src/components/SearchFilter/hooks.test.tsx +++ b/plugins/search-react/src/components/SearchFilter/hooks.test.tsx @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { ApiProvider } from '@backstage/core-app-api'; -import { MockConfigApi, TestApiRegistry } from '@backstage/test-utils'; +import { mockApis, TestApiRegistry } from '@backstage/test-utils'; import { act, renderHook, waitFor } from '@testing-library/react'; import { searchApiRef } from '../../api'; @@ -27,17 +28,19 @@ jest.useFakeTimers(); describe('SearchFilter.hooks', () => { describe('useDefaultFilterValue', () => { - const configApiMock = new MockConfigApi({ - search: { - query: { - pageLimit: 100, + const configApiMock = mockApis.config({ + data: { + search: { + query: { + pageLimit: 100, + }, }, }, }); const searchApiMock = { query: jest.fn().mockResolvedValue({ results: [] }), }; - const mockApis = TestApiRegistry.from( + const apis = TestApiRegistry.from( [searchApiRef, searchApiMock], [configApiRef, configApiMock], ); @@ -54,7 +57,7 @@ describe('SearchFilter.hooks', () => { filters: {}, }; return ( - + diff --git a/plugins/search-react/src/components/SearchPagination/SearchPagination.test.tsx b/plugins/search-react/src/components/SearchPagination/SearchPagination.test.tsx index 7c86e16e95..b0a64f0061 100644 --- a/plugins/search-react/src/components/SearchPagination/SearchPagination.test.tsx +++ b/plugins/search-react/src/components/SearchPagination/SearchPagination.test.tsx @@ -19,7 +19,7 @@ import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { - MockConfigApi, + mockApis, renderWithEffects, TestApiProvider, } from '@backstage/test-utils'; @@ -31,10 +31,12 @@ import { SearchPagination } from './SearchPagination'; import { configApiRef } from '@backstage/core-plugin-api'; describe('SearchPagination', () => { - const configApiMock = new MockConfigApi({ - search: { - query: { - pagelimit: 10, + const configApiMock = mockApis.config({ + data: { + search: { + query: { + pagelimit: 10, + }, }, }, }); diff --git a/plugins/search-react/src/context/SearchContext.test.tsx b/plugins/search-react/src/context/SearchContext.test.tsx index 58fde35667..2cb83e5ab6 100644 --- a/plugins/search-react/src/context/SearchContext.test.tsx +++ b/plugins/search-react/src/context/SearchContext.test.tsx @@ -22,7 +22,7 @@ import { act, renderHook, } from '@testing-library/react'; -import { MockConfigApi, TestApiProvider } from '@backstage/test-utils'; +import { mockApis, TestApiProvider } from '@backstage/test-utils'; import React from 'react'; import { SearchContextProvider, @@ -37,7 +37,7 @@ describe('SearchContext', () => { } satisfies typeof searchApiRef.T; const wrapper = ({ children, initialState, config = {} }: any) => { - const configApiMock = new MockConfigApi(config); + const configApiMock = mockApis.config({ data: config }); return ( { const { result } = renderHook(() => useSearch(), { wrapper: ({ children }) => { - const configApiMock = new MockConfigApi({}); + const configApiMock = mockApis.config(); return ( { const { result } = renderHook(() => useSearch(), { wrapper: ({ children }) => { - const configApiMock = new MockConfigApi({}); + const configApiMock = mockApis.config(); return ( ({ })); describe('SearchType.Accordion', () => { - const configApiMock = new MockConfigApi({ - search: { - query: { - pagelimit: 10, + const configApiMock = mockApis.config({ + data: { + search: { + query: { + pagelimit: 10, + }, }, }, }); diff --git a/plugins/search/src/components/SearchType/SearchType.Tabs.test.tsx b/plugins/search/src/components/SearchType/SearchType.Tabs.test.tsx index c0d8549253..d06106e1ae 100644 --- a/plugins/search/src/components/SearchType/SearchType.Tabs.test.tsx +++ b/plugins/search/src/components/SearchType/SearchType.Tabs.test.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { MockConfigApi, TestApiProvider } from '@backstage/test-utils'; +import { mockApis, TestApiProvider } from '@backstage/test-utils'; import { act, render } from '@testing-library/react'; import user from '@testing-library/user-event'; import { @@ -40,10 +40,12 @@ jest.mock('@backstage/plugin-search-react', () => ({ describe('SearchType.Tabs', () => { const searchApiMock = { query: jest.fn().mockResolvedValue({ results: [] }) }; - const configApiMock = new MockConfigApi({ - search: { - query: { - pageLimit: 100, + const configApiMock = mockApis.config({ + data: { + search: { + query: { + pageLimit: 100, + }, }, }, }); diff --git a/plugins/search/src/components/SearchType/SearchType.test.tsx b/plugins/search/src/components/SearchType/SearchType.test.tsx index 353b45c0fb..6a393798de 100644 --- a/plugins/search/src/components/SearchType/SearchType.test.tsx +++ b/plugins/search/src/components/SearchType/SearchType.test.tsx @@ -23,7 +23,7 @@ import { searchApiRef, } from '@backstage/plugin-search-react'; import { SearchType } from './SearchType'; -import { MockConfigApi, TestApiProvider } from '@backstage/test-utils'; +import { mockApis, TestApiProvider } from '@backstage/test-utils'; describe('SearchType', () => { const initialState = { @@ -36,10 +36,12 @@ describe('SearchType', () => { const values = ['value1', 'value2']; const typeValues = ['preselected']; - const configApiMock = new MockConfigApi({ - search: { - query: { - pagelimit: 10, + const configApiMock = mockApis.config({ + data: { + search: { + query: { + pagelimit: 10, + }, }, }, }); diff --git a/plugins/techdocs-react/src/context.test.tsx b/plugins/techdocs-react/src/context.test.tsx index a923b3c528..6c947b6858 100644 --- a/plugins/techdocs-react/src/context.test.tsx +++ b/plugins/techdocs-react/src/context.test.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { renderHook, act, waitFor } from '@testing-library/react'; @@ -21,7 +22,7 @@ import { ThemeProvider } from '@material-ui/core/styles'; import { lightTheme } from '@backstage/theme'; import { MockAnalyticsApi, - MockConfigApi, + mockApis, TestApiProvider, } from '@backstage/test-utils'; import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; @@ -84,7 +85,7 @@ const wrapper = ({ diff --git a/plugins/techdocs/src/client.test.ts b/plugins/techdocs/src/client.test.ts index 62d497d790..e2c68a7d73 100644 --- a/plugins/techdocs/src/client.test.ts +++ b/plugins/techdocs/src/client.test.ts @@ -18,7 +18,7 @@ import { UrlPatternDiscovery } from '@backstage/core-app-api'; import { IdentityApi } from '@backstage/core-plugin-api'; import { NotFoundError } from '@backstage/errors'; import { fetchEventSource } from '@microsoft/fetch-event-source'; -import { MockConfigApi, MockFetchApi } from '@backstage/test-utils'; +import { mockApis, MockFetchApi } from '@backstage/test-utils'; import { TechDocsStorageClient } from './client'; jest.mock('@microsoft/fetch-event-source'); @@ -34,7 +34,7 @@ const mockEntity = { describe('TechDocsStorageClient', () => { const mockBaseUrl = 'http://backstage:9191/api/techdocs'; - const configApi = new MockConfigApi({}); + const configApi = mockApis.config(); const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); const identityApi: jest.Mocked = { getCredentials: jest.fn(), diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx index 5e8cd7af27..5d08355ae4 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { scmIntegrationsApiRef } from '@backstage/integration-react'; import { entityRouteRef } from '@backstage/plugin-catalog-react'; import { - MockConfigApi, + mockApis, renderInTestApp, TestApiProvider, } from '@backstage/test-utils'; @@ -106,10 +107,8 @@ jest.mock('@backstage/core-components', () => ({ Page: jest.fn(), })); -const configApi = new MockConfigApi({ - app: { - baseUrl: 'http://localhost:3000', - }, +const configApi = mockApis.config({ + data: { app: { baseUrl: 'http://localhost:3000' } }, }); const Wrapper = ({ children }: { children: React.ReactNode }) => { diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/useNavigateUrl.test.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/useNavigateUrl.test.tsx index b7d4c55ed7..f2894e4d21 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/useNavigateUrl.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/useNavigateUrl.test.tsx @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { - MockConfigApi, + mockApis, renderInTestApp, TestApiProvider, } from '@backstage/test-utils'; @@ -55,16 +56,7 @@ describe('useNavigateUrl', () => { const baseUrl = 'http://localhost:3000'; await renderInTestApp( , @@ -75,16 +67,7 @@ describe('useNavigateUrl', () => { const baseUrl = 'http://localhost:3000/instance'; await renderInTestApp( , @@ -95,16 +78,7 @@ describe('useNavigateUrl', () => { const baseUrl = 'http://localhost:3000'; await renderInTestApp( , diff --git a/yarn.lock b/yarn.lock index f998b87e11..5c29a9dbec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4683,7 +4683,6 @@ __metadata: zod: ^3.22.4 peerDependencies: "@testing-library/react": ^16.0.0 - "@types/jest": "*" "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 react: ^16.13.1 || ^17.0.0 || ^18.0.0 react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 @@ -8564,6 +8563,7 @@ __metadata: "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 "@testing-library/jest-dom": ^6.0.0 + "@types/jest": "*" "@types/react": ^18.0.0 cross-fetch: ^4.0.0 i18next: ^22.4.15 @@ -8574,11 +8574,14 @@ __metadata: zen-observable: ^0.10.0 peerDependencies: "@testing-library/react": ^16.0.0 + "@types/jest": "*" "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 react: ^16.13.1 || ^17.0.0 || ^18.0.0 react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 react-router-dom: 6.0.0-beta.0 || ^6.3.0 peerDependenciesMeta: + "@types/jest": + optional: true "@types/react": optional: true languageName: unknown @@ -17912,7 +17915,7 @@ __metadata: languageName: node linkType: hard -"@types/jest@npm:^29.5.11": +"@types/jest@npm:*, @types/jest@npm:^29.5.11": version: 29.5.13 resolution: "@types/jest@npm:29.5.13" dependencies: