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..104c78a96d --- /dev/null +++ b/.changeset/thin-chairs-ring.md @@ -0,0 +1,14 @@ +--- +'@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** several old helpers: + +- Deprecated `MockAnalyticsApi`, please use `mockApis.analytics` instead. +- Deprecated `MockConfigApi`, please use `mockApis.config` instead. +- Deprecated `MockPermissionApi`, please use `mockApis.permission` instead. +- Deprecated `MockStorageApi`, please use `mockApis.storage` instead. +- Deprecated `MockTranslationApi`, please use `mockApis.translation` instead. diff --git a/packages/app/src/components/catalog/EntityPage.test.tsx b/packages/app/src/components/catalog/EntityPage.test.tsx index 5d2b23b007..0943a82efb 100644 --- a/packages/app/src/components/catalog/EntityPage.test.tsx +++ b/packages/app/src/components/catalog/EntityPage.test.tsx @@ -22,7 +22,7 @@ import { } from '@backstage/plugin-catalog-react'; import { permissionApiRef } from '@backstage/plugin-permission-react'; import { - MockPermissionApi, + mockApis, renderInTestApp, TestApiProvider, } from '@backstage/test-utils'; @@ -46,7 +46,6 @@ describe('EntityPage Test', () => { }, }; - const mockPermissionApi = new MockPermissionApi(); const rootRouteRef = catalogPlugin.routes.catalogIndex; describe('cicdContent', () => { @@ -55,7 +54,7 @@ describe('EntityPage Test', () => { 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/FetchApi/IdentityAuthInjectorFetchMiddleware.test.ts b/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAuthInjectorFetchMiddleware.test.ts index 1e29f2548a..50f93444ed 100644 --- a/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAuthInjectorFetchMiddleware.test.ts +++ b/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAuthInjectorFetchMiddleware.test.ts @@ -15,8 +15,8 @@ */ import { ConfigReader } from '@backstage/config'; -import { IdentityApi } from '@backstage/core-plugin-api'; import { IdentityAuthInjectorFetchMiddleware } from './IdentityAuthInjectorFetchMiddleware'; +import { mockApis } from '@backstage/test-utils'; describe('IdentityAuthInjectorFetchMiddleware', () => { it('creates using defaults', async () => { @@ -58,10 +58,7 @@ describe('IdentityAuthInjectorFetchMiddleware', () => { }); it('injects the header only when a token is available', async () => { - const tokenFunction = jest.fn(); - const identityApi = { - getCredentials: tokenFunction, - } as unknown as IdentityApi; + const identityApi = mockApis.identity.mock(); const middleware = new IdentityAuthInjectorFetchMiddleware( identityApi, @@ -73,27 +70,25 @@ describe('IdentityAuthInjectorFetchMiddleware', () => { const outer = middleware.apply(inner); // No token available - tokenFunction.mockResolvedValueOnce({ token: undefined }); + identityApi.getCredentials.mockResolvedValueOnce({ token: undefined }); await outer(new Request('https://example.com')); expect([...inner.mock.calls[0][0].headers.entries()]).toEqual([]); // Supply a token, header gets added - tokenFunction.mockResolvedValueOnce({ token: 'token' }); + identityApi.getCredentials.mockResolvedValueOnce({ token: 'token' }); await outer(new Request('https://example.com')); expect([...inner.mock.calls[1][0].headers.entries()]).toEqual([ ['authorization', 'Bearer token'], ]); // Token no longer available - tokenFunction.mockResolvedValueOnce({ token: undefined }); + identityApi.getCredentials.mockResolvedValueOnce({ token: undefined }); await outer(new Request('https://example.com')); expect([...inner.mock.calls[2][0].headers.entries()]).toEqual([]); }); it('does not overwrite an existing header with the same name', async () => { - const identityApi = { - getCredentials: () => ({ token: 'token' }), - } as unknown as IdentityApi; + const identityApi = mockApis.identity({ token: 'token' }); const middleware = new IdentityAuthInjectorFetchMiddleware( identityApi, diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.test.ts b/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.test.ts index 3b4b7136bb..bd82d58736 100644 --- a/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.test.ts +++ b/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.test.ts @@ -14,15 +14,14 @@ * limitations under the License. */ -import { DiscoveryApi } from '@backstage/core-plugin-api'; +import { mockApis } from '@backstage/test-utils'; import { PluginProtocolResolverFetchMiddleware } from './PluginProtocolResolverFetchMiddleware'; describe('PluginProtocolResolverFetchMiddleware', () => { it.each([['https://passthrough.com/a']])( 'passes through regular URLs, %p', async url => { - const resolve = jest.fn(); - const discoveryApi = { getBaseUrl: resolve } as unknown as DiscoveryApi; + const discoveryApi = mockApis.discovery.mock(); const middleware = new PluginProtocolResolverFetchMiddleware( discoveryApi, ); @@ -31,7 +30,7 @@ describe('PluginProtocolResolverFetchMiddleware', () => { await outer(url); expect(inner.mock.calls[0][0]).toBe(url); - expect(resolve).not.toHaveBeenCalled(); + expect(discoveryApi.getBaseUrl).not.toHaveBeenCalled(); }, ); @@ -76,30 +75,29 @@ describe('PluginProtocolResolverFetchMiddleware', () => { ])( 'resolves backstage URLs, %p', async (original, host, resolved, result) => { - const resolve = jest.fn(); - const discoveryApi = { getBaseUrl: resolve } as unknown as DiscoveryApi; + const discoveryApi = mockApis.discovery.mock({ + getBaseUrl: async () => resolved, + }); const middleware = new PluginProtocolResolverFetchMiddleware( discoveryApi, ); const inner = jest.fn(); const outer = middleware.apply(inner); - resolve.mockResolvedValueOnce(resolved); await outer(original); expect(inner.mock.calls[0][0]).toBe(result); - expect(resolve).toHaveBeenLastCalledWith(host); + expect(discoveryApi.getBaseUrl).toHaveBeenLastCalledWith(host); }, ); it('properly supports transferring request bodies too', async () => { - const resolve = jest.fn(); - const discoveryApi = { getBaseUrl: resolve } as unknown as DiscoveryApi; + const discoveryApi = mockApis.discovery.mock({ + getBaseUrl: async () => 'https://elsewhere.com', + }); const middleware = new PluginProtocolResolverFetchMiddleware(discoveryApi); const inner = jest.fn(); const outer = middleware.apply(inner); - resolve.mockResolvedValue('https://elsewhere.com'); - await outer('plugin://a', { method: 'POST', body: '123', 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/AppManager.test.tsx b/packages/core-app-api/src/app/AppManager.test.tsx index 01277a09c7..d995edffdf 100644 --- a/packages/core-app-api/src/app/AppManager.test.tsx +++ b/packages/core-app-api/src/app/AppManager.test.tsx @@ -14,9 +14,9 @@ * limitations under the License. */ -import { LocalStorageFeatureFlags, NoOpAnalyticsApi } from '../apis'; +import { LocalStorageFeatureFlags } from '../apis'; import { - MockAnalyticsApi, + mockApis, renderWithEffects, withLogCollector, registerMswTestHooks, @@ -59,7 +59,7 @@ describe('Integration Test', () => { const noOpAnalyticsApi = createApiFactory( analyticsApiRef, - new NoOpAnalyticsApi(), + mockApis.analytics(), ); const noopErrorApi = createApiFactory(errorApiRef, { error$() { @@ -575,7 +575,7 @@ describe('Integration Test', () => { }); it('should track route changes via analytics api', async () => { - const mockAnalyticsApi = new MockAnalyticsApi(); + const mockAnalyticsApi = mockApis.analytics(); const apis = [createApiFactory(analyticsApiRef, mockAnalyticsApi)]; const app = new AppManager({ apis, @@ -608,26 +608,27 @@ describe('Integration Test', () => { ); // Capture initial and subsequent navigation events with expected context. - const capturedEvents = mockAnalyticsApi.getEvents(); - expect(capturedEvents[0]).toMatchObject({ + expect(mockAnalyticsApi.captureEvent).toHaveBeenCalledTimes(2); + expect(mockAnalyticsApi.captureEvent).toHaveBeenNthCalledWith(1, { action: 'navigate', subject: '/', + attributes: {}, context: { extension: 'App', pluginId: 'blob', routeRef: 'ref-1-2', }, }); - expect(capturedEvents[1]).toMatchObject({ + expect(mockAnalyticsApi.captureEvent).toHaveBeenNthCalledWith(2, { action: 'navigate', subject: '/foo', + attributes: {}, context: { extension: 'App', pluginId: 'plugin2', routeRef: 'ref-2', }, }); - expect(capturedEvents).toHaveLength(2); }); it('should throw some error when the route has duplicate params', async () => { @@ -877,9 +878,9 @@ describe('Integration Test', () => { }), }), }; - const discoveryApiMock = { - getBaseUrl: jest.fn().mockResolvedValue('http://localhost:7007/app'), - }; + const discoveryApiMock = mockApis.discovery.mock({ + getBaseUrl: async () => 'http://localhost:7007/app', + }); const app = new AppManager({ icons, diff --git a/packages/core-app-api/src/app/AppRouter.test.tsx b/packages/core-app-api/src/app/AppRouter.test.tsx index 9f3ab2ad89..b595f91794 100644 --- a/packages/core-app-api/src/app/AppRouter.test.tsx +++ b/packages/core-app-api/src/app/AppRouter.test.tsx @@ -18,7 +18,6 @@ import React from 'react'; import { AppComponents, configApiRef, - IdentityApi, identityApiRef, SignInPageProps, useApi, @@ -30,7 +29,7 @@ import { render, screen } from '@testing-library/react'; import { AppRouter } from './AppRouter'; import useAsync from 'react-use/esm/useAsync'; import { AppContextProvider } from './AppContext'; -import { TestApiProvider } from '@backstage/test-utils'; +import { mockApis, TestApiProvider } from '@backstage/test-utils'; import { ConfigReader } from '@backstage/config'; function UserRefDisplay() { @@ -78,13 +77,7 @@ describe('AppRouter', () => { const appIdentityProxy = new AppIdentityProxy(); const SignInPage = (props: SignInPageProps) => { - props.onSignInSuccess({ - getBackstageIdentity: async () => ({ - type: 'user', - userEntityRef: 'user:default/test', - ownershipEntityRefs: ['user:default/test'], - }), - } as IdentityApi); + props.onSignInSuccess(mockApis.identity()); return null; }; 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-app-api/src/routing/RouteTracker.test.tsx b/packages/core-app-api/src/routing/RouteTracker.test.tsx index 25d5cfbcfb..ecbd44315d 100644 --- a/packages/core-app-api/src/routing/RouteTracker.test.tsx +++ b/packages/core-app-api/src/routing/RouteTracker.test.tsx @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { TestApiProvider } from '@backstage/test-utils'; + +import { TestApiProvider, mockApis } from '@backstage/test-utils'; import React from 'react'; import { BackstageRouteObject } from './types'; import { fireEvent, render } from '@testing-library/react'; import { RouteTracker } from './RouteTracker'; import { Link, MemoryRouter, Route, Routes } from 'react-router-dom'; import { - AnalyticsApi, analyticsApiRef, createPlugin, createRouteRef, @@ -68,9 +68,7 @@ describe('RouteTracker', () => { }, ]; - const mockedAnalytics: jest.Mocked = { - captureEvent: jest.fn(), - }; + const mockedAnalytics = mockApis.analytics(); beforeEach(() => { jest.clearAllMocks(); diff --git a/packages/core-components/src/components/AutoLogout/Autologout.test.tsx b/packages/core-components/src/components/AutoLogout/Autologout.test.tsx index 164ad716d2..5a555cd215 100644 --- a/packages/core-components/src/components/AutoLogout/Autologout.test.tsx +++ b/packages/core-components/src/components/AutoLogout/Autologout.test.tsx @@ -13,23 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { createMocks } from 'react-idle-timer'; // eslint-disable-next-line no-restricted-imports import { MessageChannel } from 'worker_threads'; import { ApiProvider } from '@backstage/core-app-api'; import { identityApiRef } from '@backstage/core-plugin-api'; -import { TestApiRegistry, renderInTestApp } from '@backstage/test-utils'; +import { + TestApiRegistry, + renderInTestApp, + mockApis, +} from '@backstage/test-utils'; import React from 'react'; import { AutoLogout } from './AutoLogout'; import { cleanup } from '@testing-library/react'; -// Mock the signOut function of identityApiRef -const mockSignOut = jest.fn(); -const mockIdentityApi = { - signOut: mockSignOut, - getCredentials: jest.fn().mockReturnValue({ token: 'xxx' }), -}; +const mockIdentityApi = mockApis.identity({ token: 'xxx' }); const apis = TestApiRegistry.from([identityApiRef, mockIdentityApi]); describe('AutoLogout', () => { diff --git a/packages/core-components/src/components/DismissableBanner/DismissableBanner.test.tsx b/packages/core-components/src/components/DismissableBanner/DismissableBanner.test.tsx index 8d93186ccc..6f6da1231f 100644 --- a/packages/core-components/src/components/DismissableBanner/DismissableBanner.test.tsx +++ b/packages/core-components/src/components/DismissableBanner/DismissableBanner.test.tsx @@ -16,24 +16,18 @@ import React from 'react'; import { fireEvent } from '@testing-library/react'; -import { TestApiRegistry, renderInTestApp } from '@backstage/test-utils'; +import { + TestApiRegistry, + renderInTestApp, + mockApis, +} from '@backstage/test-utils'; import { DismissableBanner } from './DismissableBanner'; -import { ApiProvider, WebStorage } from '@backstage/core-app-api'; -import { storageApiRef, StorageApi } from '@backstage/core-plugin-api'; +import { ApiProvider } from '@backstage/core-app-api'; +import { storageApiRef } from '@backstage/core-plugin-api'; import { screen } from '@testing-library/react'; describe('', () => { - let apis: TestApiRegistry; - const mockErrorApi = { post: jest.fn(), error$: jest.fn() }; - const createWebStorage = (): StorageApi => { - return WebStorage.create({ - errorApi: mockErrorApi, - }); - }; - - beforeEach(() => { - apis = TestApiRegistry.from([storageApiRef, createWebStorage()]); - }); + const apis = TestApiRegistry.from([storageApiRef, mockApis.storage()]); it('renders the message and the popover', async () => { await renderInTestApp( diff --git a/packages/core-components/src/components/Link/Link.test.tsx b/packages/core-components/src/components/Link/Link.test.tsx index fe616299c3..0a86c06d43 100644 --- a/packages/core-components/src/components/Link/Link.test.tsx +++ b/packages/core-components/src/components/Link/Link.test.tsx @@ -17,7 +17,7 @@ import React, { ComponentType } from 'react'; import { fireEvent, waitFor, screen, renderHook } from '@testing-library/react'; import { - MockAnalyticsApi, + mockApis, TestApiProvider, renderInTestApp, } from '@backstage/test-utils'; @@ -71,7 +71,7 @@ describe('', () => { it('captures click using analytics api', async () => { const linkText = 'Navigate!'; - const analyticsApi = new MockAnalyticsApi(); + const analyticsApi = mockApis.analytics(); const customOnClick = jest.fn(); await renderInTestApp( @@ -86,13 +86,15 @@ describe('', () => { // Analytics event should have been fired. await waitFor(() => { - expect(analyticsApi.getEvents()[0]).toMatchObject({ - action: 'click', - subject: linkText, - attributes: { - to: '/test', - }, - }); + expect(analyticsApi.captureEvent).toHaveBeenCalledWith( + expect.objectContaining({ + action: 'click', + subject: linkText, + attributes: { + to: '/test', + }, + }), + ); // Custom onClick handler should have still been fired too. expect(customOnClick).toHaveBeenCalled(); @@ -101,7 +103,7 @@ describe('', () => { it('does not capture click when noTrack is set', async () => { const linkText = 'Navigate!'; - const analyticsApi = new MockAnalyticsApi(); + const analyticsApi = mockApis.analytics(); const customOnClick = jest.fn(); await renderInTestApp( @@ -120,7 +122,7 @@ describe('', () => { expect(customOnClick).toHaveBeenCalled(); // But there should be no analytics event. - expect(analyticsApi.getEvents()).toHaveLength(0); + expect(analyticsApi.captureEvent).not.toHaveBeenCalled(); }); }); 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/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.test.tsx b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.test.tsx index e4e65b613f..ad7e650d24 100644 --- a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.test.tsx +++ b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.test.tsx @@ -20,6 +20,7 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { TestApiProvider, + mockApis, registerMswTestHooks, wrapInTestApp, } from '@backstage/test-utils'; @@ -37,9 +38,7 @@ describe('ProxiedSignInPage', () => { apis={[ [ discoveryApiRef, - { - getBaseUrl: async () => 'http://example.com/api/auth', - }, + mockApis.discovery({ baseUrl: 'http://example.com' }), ], ]} > diff --git a/packages/core-components/src/layout/Sidebar/Items.test.tsx b/packages/core-components/src/layout/Sidebar/Items.test.tsx index 6c959bfd0d..ee0f0ea401 100644 --- a/packages/core-components/src/layout/Sidebar/Items.test.tsx +++ b/packages/core-components/src/layout/Sidebar/Items.test.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { - MockAnalyticsApi, + mockApis, TestApiProvider, renderInTestApp, } from '@backstage/test-utils'; @@ -36,9 +36,8 @@ const useStyles = makeStyles({ }, }); -let analyticsApiMock: MockAnalyticsApi; - const handleSidebarItemClick = jest.fn(); +const analyticsApiMock = mockApis.analytics(); async function renderSidebar() { const { result } = renderHook(() => useStyles()); @@ -79,7 +78,6 @@ async function renderSidebar() { describe('Items', () => { beforeEach(async () => { jest.clearAllMocks(); - analyticsApiMock = new MockAnalyticsApi(); await renderSidebar(); }); @@ -107,8 +105,7 @@ describe('Items', () => { await screen.findByRole('button', { name: /create/i }), ); expect(handleSidebarItemClick).toHaveBeenCalledTimes(1); - expect(analyticsApiMock.getEvents()).toHaveLength(1); - expect(analyticsApiMock.getEvents()[0]).toMatchObject({ + expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith({ action: 'click', subject: 'Create...', context: { routeRef: 'unknown', pluginId: 'root', extension: 'App' }, @@ -119,8 +116,7 @@ describe('Items', () => { it('should send link clicks to analytics', async () => { await userEvent.click(await screen.findByRole('link', { name: /docs/i })); expect(handleSidebarItemClick).toHaveBeenCalledTimes(1); - expect(analyticsApiMock.getEvents()).toHaveLength(1); - expect(analyticsApiMock.getEvents()[0]).toMatchObject({ + expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith({ action: 'click', subject: 'Docs', context: { routeRef: 'unknown', pluginId: 'root', extension: 'App' }, @@ -132,10 +128,10 @@ describe('Items', () => { await userEvent.click( await screen.findByRole('link', { name: /explore/i }), ); - expect(handleSidebarItemClick).toHaveBeenCalledTimes(1); - expect(analyticsApiMock.getEvents()).toHaveLength(0); + expect(analyticsApiMock.captureEvent).not.toHaveBeenCalled(); }); }); + describe('SidebarSearchField', () => { it('should be defaultPrevented when enter is pressed', async () => { const searchEvent = createEvent.keyDown( diff --git a/packages/frontend-app-api/src/routing/RouteTracker.test.tsx b/packages/frontend-app-api/src/routing/RouteTracker.test.tsx index aeec03992f..372afea4c3 100644 --- a/packages/frontend-app-api/src/routing/RouteTracker.test.tsx +++ b/packages/frontend-app-api/src/routing/RouteTracker.test.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { TestApiProvider } from '@backstage/test-utils'; import React, { useEffect } from 'react'; import { BackstageRouteObject } from './types'; 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..560abc602f 100644 --- a/packages/frontend-defaults/src/createPublicSignInApp.test.tsx +++ b/packages/frontend-defaults/src/createPublicSignInApp.test.tsx @@ -15,14 +15,13 @@ */ import { - IdentityApi, SignInPageBlueprint, createFrontendModule, } from '@backstage/frontend-plugin-api'; 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 +30,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 +58,7 @@ describe('createPublicSignInApp', () => { .mockReturnValue(); const app = createPublicSignInApp({ - configLoader: async () => ({ config: new MockConfigApi({}) }), + configLoader: async () => ({ config: mockApis.config() }), features: [ createFrontendModule({ pluginId: 'app', @@ -70,9 +69,9 @@ describe('createPublicSignInApp', () => { async () => ({ onSignInSuccess }) => { useEffect(() => { - onSignInSuccess({ - getCredentials: async () => ({ token: 'mock-token' }), - } as IdentityApi); + onSignInSuccess( + mockApis.identity({ token: 'mock-token' }), + ); }, [onSignInSuccess]); return
; }, diff --git a/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx b/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx index 3e260f0c9b..14aaebb778 100644 --- a/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx +++ b/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx @@ -17,7 +17,7 @@ import React, { useEffect } from 'react'; import { act, screen, waitFor } from '@testing-library/react'; import { - MockAnalyticsApi, + mockApis, TestApiProvider, withLogCollector, } from '@backstage/test-utils'; @@ -93,7 +93,7 @@ describe('ExtensionBoundary', () => { it('should wrap children with analytics context', async () => { const action = 'render'; const subject = 'analytics'; - const analyticsApiMock = new MockAnalyticsApi(); + const analyticsApiMock = mockApis.analytics(); const AnalyticsComponent = () => { const analytics = useAnalytics(); @@ -112,17 +112,15 @@ describe('ExtensionBoundary', () => { ); await waitFor(() => { - const event = analyticsApiMock - .getEvents() - .find(e => e.subject === subject); - - expect(event).toMatchObject({ - action, - subject, - context: { - extensionId: 'test', - }, - }); + expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith( + expect.objectContaining({ + action, + subject, + context: expect.objectContaining({ + extensionId: 'test', + }), + }), + ); }); }); @@ -136,7 +134,7 @@ describe('ExtensionBoundary', () => { }); return null; }; - const analyticsApiMock = new MockAnalyticsApi(); + const analyticsApiMock = mockApis.analytics(); await act(async () => { renderInTestApp( @@ -147,7 +145,7 @@ describe('ExtensionBoundary', () => { ); }); - expect(analyticsApiMock.getEvents()).toEqual([ + expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith( expect.objectContaining({ action: 'navigate', subject: '/', @@ -156,7 +154,9 @@ describe('ExtensionBoundary', () => { extensionId: 'test', }), }), + ); + expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith( expect.objectContaining({ action: 'dummy' }), - ]); + ); }); }); 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 fe93b05b57..6285a8b479 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 8a1ea4f083..5fa0a19adb 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-alpha.api.md b/packages/test-utils/report-alpha.api.md index ba098135fb..19a3786fc9 100644 --- a/packages/test-utils/report-alpha.api.md +++ b/packages/test-utils/report-alpha.api.md @@ -8,7 +8,7 @@ import { TranslationApi } from '@backstage/core-plugin-api/alpha'; import { TranslationRef } from '@backstage/core-plugin-api/alpha'; import { TranslationSnapshot } from '@backstage/core-plugin-api/alpha'; -// @alpha (undocumented) +// @alpha @deprecated (undocumented) export class MockTranslationApi implements TranslationApi { // (undocumented) static create(): MockTranslationApi; @@ -30,10 +30,10 @@ export class MockTranslationApi implements TranslationApi { // Warnings were encountered during analysis: // -// src/testUtils/apis/TranslationApi/MockTranslationApi.d.ts:4:1 - (ae-undocumented) Missing documentation for "MockTranslationApi". -// src/testUtils/apis/TranslationApi/MockTranslationApi.d.ts:6:5 - (ae-undocumented) Missing documentation for "create". -// src/testUtils/apis/TranslationApi/MockTranslationApi.d.ts:8:5 - (ae-undocumented) Missing documentation for "getTranslation". -// src/testUtils/apis/TranslationApi/MockTranslationApi.d.ts:11:5 - (ae-undocumented) Missing documentation for "translation$". +// src/testUtils/apis/TranslationApi/MockTranslationApi.d.ts:7:1 - (ae-undocumented) Missing documentation for "MockTranslationApi". +// src/testUtils/apis/TranslationApi/MockTranslationApi.d.ts:9:5 - (ae-undocumented) Missing documentation for "create". +// src/testUtils/apis/TranslationApi/MockTranslationApi.d.ts:11:5 - (ae-undocumented) Missing documentation for "getTranslation". +// src/testUtils/apis/TranslationApi/MockTranslationApi.d.ts:14:5 - (ae-undocumented) Missing documentation for "translation$". // (No @packageDocumentation comment for this package) ``` diff --git a/packages/test-utils/report.api.md b/packages/test-utils/report.api.md index 4a1f297c4e..11657ce534 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'; @@ -38,6 +41,16 @@ import { RenderResult } from '@testing-library/react'; import { RouteRef } from '@backstage/core-plugin-api'; import { StorageApi } from '@backstage/core-plugin-api'; import { StorageValueSnapshot } from '@backstage/core-plugin-api'; +import { TranslationApi } from '@backstage/core-plugin-api/alpha'; + +// @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; @@ -69,7 +82,7 @@ export type LogCollector = AsyncLogCollector | SyncLogCollector; // @public export type LogFuncs = 'log' | 'warn' | 'error'; -// @public +// @public @deprecated export class MockAnalyticsApi implements AnalyticsApi { // (undocumented) captureEvent(event: AnalyticsEvent): void; @@ -77,10 +90,128 @@ export class MockAnalyticsApi implements AnalyticsApi { getEvents(): AnalyticsEvent[]; } +// @public +export namespace mockApis { + export function analytics(): AnalyticsApi; + export namespace analytics { + const // (undocumented) + factory: () => ApiFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ApiMock; + } + export function config(options?: { data?: JsonObject }): ConfigApi; + export namespace config { + const factory: ( + options?: + | { + data?: JsonObject | undefined; + } + | undefined, + ) => ApiFactory; + const mock: (partialImpl?: Partial | undefined) => ApiMock; + } + export function discovery(options?: { baseUrl?: string }): DiscoveryApi; + export namespace discovery { + const // (undocumented) + factory: ( + options?: + | { + baseUrl?: string | undefined; + } + | undefined, + ) => ApiFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ApiMock; + } + export function identity(options?: { + userEntityRef?: string; + ownershipEntityRefs?: string[]; + token?: string; + email?: string; + displayName?: string; + picture?: string; + }): IdentityApi; + export namespace identity { + const // (undocumented) + factory: ( + options?: + | { + userEntityRef?: string | undefined; + ownershipEntityRefs?: string[] | undefined; + token?: string | undefined; + email?: string | undefined; + displayName?: string | undefined; + picture?: string | undefined; + } + | undefined, + ) => ApiFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ApiMock; + } + export function permission(options?: { + authorize?: + | AuthorizeResult.ALLOW + | AuthorizeResult.DENY + | (( + request: EvaluatePermissionRequest, + ) => AuthorizeResult.ALLOW | AuthorizeResult.DENY); + }): PermissionApi; + export namespace permission { + const // (undocumented) + factory: ( + options?: + | { + authorize?: + | AuthorizeResult.DENY + | AuthorizeResult.ALLOW + | (( + request: EvaluatePermissionRequest, + ) => AuthorizeResult.DENY | AuthorizeResult.ALLOW) + | undefined; + } + | undefined, + ) => ApiFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ApiMock; + } + export function storage(options?: { data?: JsonObject }): StorageApi; + export namespace storage { + const // (undocumented) + factory: ( + options?: + | { + data?: JsonObject | undefined; + } + | undefined, + ) => ApiFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ApiMock; + } + export function translation(): TranslationApi; + export namespace translation { + const // (undocumented) + factory: () => ApiFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ApiMock; + } +} + // @public @deprecated export function mockBreakpoint(options: { matches: boolean }): void; -// @public +// @public @deprecated export class MockConfigApi implements ConfigApi { constructor(data: JsonObject); get(key?: string): T; @@ -146,7 +277,7 @@ export interface MockFetchApiOptions { }; } -// @public +// @public @deprecated export class MockPermissionApi implements PermissionApi { constructor( requestHandler?: ( @@ -159,7 +290,7 @@ export class MockPermissionApi implements PermissionApi { ): Promise; } -// @public +// @public @deprecated export class MockStorageApi implements StorageApi { // (undocumented) static create(data?: MockStorageBucket): MockStorageApi; @@ -177,7 +308,7 @@ export class MockStorageApi implements StorageApi { snapshot(key: string): StorageValueSnapshot; } -// @public +// @public @deprecated export type MockStorageBucket = { [key: string]: any; }; @@ -276,17 +407,29 @@ export function wrapInTestApp( // Warnings were encountered during analysis: // // src/deprecated.d.ts:5:1 - (ae-undocumented) Missing documentation for "setupRequestMockHandlers". -// src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.d.ts:10:5 - (ae-undocumented) Missing documentation for "captureEvent". -// src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.d.ts:11:5 - (ae-undocumented) Missing documentation for "getEvents". +// src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.d.ts:11:5 - (ae-undocumented) Missing documentation for "captureEvent". +// src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.d.ts:12:5 - (ae-undocumented) Missing documentation for "getEvents". // src/testUtils/apis/ErrorApi/MockErrorApi.d.ts:28:5 - (ae-undocumented) Missing documentation for "post". // src/testUtils/apis/ErrorApi/MockErrorApi.d.ts:29:5 - (ae-undocumented) Missing documentation for "error$". // src/testUtils/apis/ErrorApi/MockErrorApi.d.ts:33:5 - (ae-undocumented) Missing documentation for "getErrors". // src/testUtils/apis/ErrorApi/MockErrorApi.d.ts:34:5 - (ae-undocumented) Missing documentation for "waitForError". -// src/testUtils/apis/PermissionApi/MockPermissionApi.d.ts:13:5 - (ae-undocumented) Missing documentation for "authorize". -// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:19:5 - (ae-undocumented) Missing documentation for "create". -// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:20:5 - (ae-undocumented) Missing documentation for "forBucket". -// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:21:5 - (ae-undocumented) Missing documentation for "snapshot". -// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:22:5 - (ae-undocumented) Missing documentation for "set". -// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:23:5 - (ae-undocumented) Missing documentation for "remove". -// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:24:5 - (ae-undocumented) Missing documentation for "observe$". +// src/testUtils/apis/PermissionApi/MockPermissionApi.d.ts:14:5 - (ae-undocumented) Missing documentation for "authorize". +// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:21:5 - (ae-undocumented) Missing documentation for "create". +// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:22:5 - (ae-undocumented) Missing documentation for "forBucket". +// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:23:5 - (ae-undocumented) Missing documentation for "snapshot". +// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:24:5 - (ae-undocumented) Missing documentation for "set". +// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:25:5 - (ae-undocumented) Missing documentation for "remove". +// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:26:5 - (ae-undocumented) Missing documentation for "observe$". +// src/testUtils/apis/mockApis.d.ts:58:15 - (ae-undocumented) Missing documentation for "factory". +// src/testUtils/apis/mockApis.d.ts:59:15 - (ae-undocumented) Missing documentation for "mock". +// src/testUtils/apis/mockApis.d.ts:125:15 - (ae-undocumented) Missing documentation for "factory". +// src/testUtils/apis/mockApis.d.ts:128:15 - (ae-undocumented) Missing documentation for "mock". +// src/testUtils/apis/mockApis.d.ts:150:15 - (ae-undocumented) Missing documentation for "factory". +// src/testUtils/apis/mockApis.d.ts:158:15 - (ae-undocumented) Missing documentation for "mock". +// src/testUtils/apis/mockApis.d.ts:177:15 - (ae-undocumented) Missing documentation for "factory". +// src/testUtils/apis/mockApis.d.ts:180:15 - (ae-undocumented) Missing documentation for "mock". +// src/testUtils/apis/mockApis.d.ts:197:15 - (ae-undocumented) Missing documentation for "factory". +// src/testUtils/apis/mockApis.d.ts:200:15 - (ae-undocumented) Missing documentation for "mock". +// src/testUtils/apis/mockApis.d.ts:215:15 - (ae-undocumented) Missing documentation for "factory". +// src/testUtils/apis/mockApis.d.ts:216:15 - (ae-undocumented) Missing documentation for "mock". ``` diff --git a/packages/test-utils/src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.ts b/packages/test-utils/src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.ts index 6da225df1c..788d2ca0d8 100644 --- a/packages/test-utils/src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.ts +++ b/packages/test-utils/src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.ts @@ -21,6 +21,7 @@ import { AnalyticsApi, AnalyticsEvent } from '@backstage/core-plugin-api'; * Use getEvents in tests to verify captured events. * * @public + * @deprecated Use {@link @backstage/test-utils#mockApis.(analytics:namespace)} instead */ export class MockAnalyticsApi implements AnalyticsApi { private events: AnalyticsEvent[] = []; 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..3c5329962a 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:namespace)} instead * @example * ```tsx * const mockConfig = new MockConfigApi({ diff --git a/packages/test-utils/src/testUtils/apis/PermissionApi/MockPermissionApi.ts b/packages/test-utils/src/testUtils/apis/PermissionApi/MockPermissionApi.ts index 163af5dd53..6bd3c5d4c5 100644 --- a/packages/test-utils/src/testUtils/apis/PermissionApi/MockPermissionApi.ts +++ b/packages/test-utils/src/testUtils/apis/PermissionApi/MockPermissionApi.ts @@ -26,6 +26,7 @@ import { * {@link @backstage/plugin-permission-react#PermissionApi}. Supply a * requestHandler function to override the mock result returned for a given * request. + * @deprecated Use {@link @backstage/test-utils#mockApis.(permission:namespace)} instead * @public */ export class MockPermissionApi implements PermissionApi { diff --git a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts index 63aa4235e1..cdae96d5e4 100644 --- a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts +++ b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { StorageApi } from '@backstage/core-plugin-api'; import { MockStorageApi } from './MockStorageApi'; diff --git a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts index 30d523aed2..2fb70834ef 100644 --- a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts +++ b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts @@ -20,12 +20,14 @@ import ObservableImpl from 'zen-observable'; /** * Type for map holding data in {@link MockStorageApi} + * @deprecated Use {@link @backstage/test-utils#mockApis.(storage:namespace)} instead * @public */ export type MockStorageBucket = { [key: string]: any }; /** * Mock implementation of the {@link core-plugin-api#StorageApi} to be used in tests + * @deprecated Use {@link @backstage/test-utils#mockApis.(storage:namespace)} instead * @public */ export class MockStorageApi implements StorageApi { @@ -44,7 +46,21 @@ export class MockStorageApi implements StorageApi { } static create(data?: MockStorageBucket) { - return new MockStorageApi('', new Map(), data); + // Translate a nested data object structure into a flat object with keys + // like `/a/b` with their corresponding leaf values + const keyValues: { [key: string]: any } = {}; + function put(value: { [key: string]: any }, namespace: string) { + for (const [key, val] of Object.entries(value)) { + if (typeof val === 'object' && val !== null) { + put(val, `${namespace}/${key}`); + } else { + const namespacedKey = `${namespace}/${key.replace(/^\//, '')}`; + keyValues[namespacedKey] = val; + } + } + } + put(data ?? {}, ''); + return new MockStorageApi('', new Map(), keyValues); } forBucket(name: string): StorageApi { diff --git a/packages/test-utils/src/testUtils/apis/TranslationApi/MockTranslationApi.ts b/packages/test-utils/src/testUtils/apis/TranslationApi/MockTranslationApi.ts index 608b634cc4..46c5bfd5d3 100644 --- a/packages/test-utils/src/testUtils/apis/TranslationApi/MockTranslationApi.ts +++ b/packages/test-utils/src/testUtils/apis/TranslationApi/MockTranslationApi.ts @@ -30,7 +30,10 @@ import { toInternalTranslationRef } from '../../../../../core-plugin-api/src/tra const DEFAULT_LANGUAGE = 'en'; -/** @alpha */ +/** + * @alpha + * @deprecated Use `mockApis` from `@backstage/test-utils` instead + */ export class MockTranslationApi implements TranslationApi { static create() { const i18n = createI18n({ 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..31cb03e117 --- /dev/null +++ b/packages/test-utils/src/testUtils/apis/mockApis.test.tsx @@ -0,0 +1,773 @@ +/* + * 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 { + AuthorizeResult, + createPermission, +} from '@backstage/plugin-permission-common'; +import { mockApis } from './mockApis'; +import { JsonValue } from '@backstage/types'; +import { StorageValueSnapshot } from '@backstage/core-plugin-api'; +import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; + +describe('mockApis', () => { + describe('analytics', () => { + it('can create an instance', () => { + const analytics = mockApis.analytics(); + expect( + analytics.captureEvent({ + action: 'a', + subject: 'b', + context: { pluginId: 'c', extension: 'd', routeRef: 'e' }, + }), + ).toBeUndefined(); + }); + + it('can create a mock and make assertions on it', async () => { + expect.assertions(3); + const analytics = mockApis.analytics.mock({ + captureEvent: event => { + expect(event).toEqual({ + action: 'a', + subject: 'b', + context: { pluginId: 'c', extension: 'd', routeRef: 'e' }, + }); + }, + }); + expect( + analytics.captureEvent({ + action: 'a', + subject: 'b', + context: { pluginId: 'c', extension: 'd', routeRef: 'e' }, + }), + ).toBeUndefined(); + expect(analytics.captureEvent).toHaveBeenCalledTimes(1); + }); + }); + + describe('config', () => { + const data = { backend: { baseUrl: 'http://test.com' } }; + + it('can create an instance', () => { + const empty = mockApis.config(); + expect(empty.getOptional('backend.baseUrl')).toBeUndefined(); + + const notEmpty = mockApis.config({ data }); + expect(notEmpty.getOptional('backend.baseUrl')).toEqual( + 'http://test.com', + ); + }); + + 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); + }); + }); + + describe('discovery', () => { + it('can create an instance', async () => { + const empty = mockApis.discovery(); + await expect(empty.getBaseUrl('catalog')).resolves.toBe( + 'http://example.com/api/catalog', + ); + + const notEmpty = mockApis.discovery({ baseUrl: 'https://other.net' }); + await expect(notEmpty.getBaseUrl('catalog')).resolves.toBe( + 'https://other.net/api/catalog', + ); + }); + + it('can create a mock and make assertions on it', async () => { + const empty = mockApis.discovery.mock(); + expect(empty.getBaseUrl('catalog')).toBeUndefined(); + expect(empty.getBaseUrl).toHaveBeenCalledTimes(1); + + const notEmpty = mockApis.discovery.mock({ + getBaseUrl: async () => 'replaced', + }); + await expect(notEmpty.getBaseUrl('catalog')).resolves.toBe('replaced'); + expect(notEmpty.getBaseUrl).toHaveBeenCalledTimes(1); + }); + }); + + describe('identity', () => { + it('can create an instance', async () => { + const empty = mockApis.identity(); + await expect(empty.getBackstageIdentity()).resolves.toEqual({ + type: 'user', + userEntityRef: 'user:default/test', + ownershipEntityRefs: ['user:default/test'], + }); + await expect(empty.getCredentials()).resolves.toEqual({}); + await expect(empty.getProfileInfo()).resolves.toEqual({}); + await expect(empty.signOut()).resolves.toBeUndefined(); + + const notEmpty = mockApis.identity({ + userEntityRef: 'a', + ownershipEntityRefs: ['b'], + token: 'c', + email: 'd', + displayName: 'e', + picture: 'f', + }); + await expect(notEmpty.getBackstageIdentity()).resolves.toEqual({ + type: 'user', + userEntityRef: 'a', + ownershipEntityRefs: ['b'], + }); + await expect(notEmpty.getCredentials()).resolves.toEqual({ token: 'c' }); + await expect(notEmpty.getProfileInfo()).resolves.toEqual({ + email: 'd', + displayName: 'e', + picture: 'f', + }); + await expect(notEmpty.signOut()).resolves.toBeUndefined(); + }); + + it('can create a mock and make assertions on it', async () => { + const empty = mockApis.identity.mock(); + expect(empty.getBackstageIdentity()).toBeUndefined(); + expect(empty.getCredentials()).toBeUndefined(); + expect(empty.getProfileInfo()).toBeUndefined(); + expect(empty.signOut()).toBeUndefined(); + expect(empty.getBackstageIdentity).toHaveBeenCalledTimes(1); + expect(empty.getCredentials).toHaveBeenCalledTimes(1); + expect(empty.getProfileInfo).toHaveBeenCalledTimes(1); + expect(empty.signOut).toHaveBeenCalledTimes(1); + + const notEmpty = mockApis.identity.mock({ + getBackstageIdentity: async () => ({ + type: 'user', + userEntityRef: 'a', + ownershipEntityRefs: ['b'], + }), + getCredentials: async () => ({ token: 'c' }), + getProfileInfo: async () => ({ + email: 'd', + displayName: 'e', + picture: 'f', + }), + signOut: async () => undefined, + }); + await expect(notEmpty.getBackstageIdentity()).resolves.toEqual({ + type: 'user', + userEntityRef: 'a', + ownershipEntityRefs: ['b'], + }); + await expect(notEmpty.getCredentials()).resolves.toEqual({ token: 'c' }); + await expect(notEmpty.getProfileInfo()).resolves.toEqual({ + email: 'd', + displayName: 'e', + picture: 'f', + }); + await expect(notEmpty.signOut()).resolves.toBeUndefined(); + expect(notEmpty.getBackstageIdentity).toHaveBeenCalledTimes(1); + expect(notEmpty.getCredentials).toHaveBeenCalledTimes(1); + expect(notEmpty.getProfileInfo).toHaveBeenCalledTimes(1); + expect(notEmpty.signOut).toHaveBeenCalledTimes(1); + }); + }); + + describe('permission', () => { + it('can create an instance', async () => { + // default allow + const permission1 = mockApis.permission(); + await expect( + permission1.authorize({ + permission: createPermission({ + name: 'permission.1', + attributes: {}, + }), + }), + ).resolves.toEqual({ result: AuthorizeResult.ALLOW }); + + // static value + const permission2 = mockApis.permission({ + authorize: AuthorizeResult.DENY, + }); + await expect( + permission2.authorize({ + permission: createPermission({ + name: 'permission.1', + attributes: {}, + }), + }), + ).resolves.toEqual({ result: AuthorizeResult.DENY }); + + // callback form + const permission3 = mockApis.permission({ + authorize: req => + req.permission.name === 'permission.1' + ? AuthorizeResult.ALLOW + : AuthorizeResult.DENY, + }); + await expect( + permission3.authorize({ + permission: createPermission({ + name: 'permission.1', + attributes: {}, + }), + }), + ).resolves.toEqual({ result: AuthorizeResult.ALLOW }); + await expect( + permission3.authorize({ + permission: createPermission({ + name: 'permission.2', + attributes: {}, + }), + }), + ).resolves.toEqual({ result: AuthorizeResult.DENY }); + }); + + it('can create a mock and make assertions on it', async () => { + const empty = mockApis.permission.mock(); + expect( + empty.authorize({ + permission: createPermission({ + name: 'permission.1', + attributes: {}, + }), + }), + ).toBeUndefined(); + expect(empty.authorize).toHaveBeenCalledTimes(1); + + const notEmpty = mockApis.permission.mock({ + authorize: async req => ({ + result: + req.permission.name === 'permission.1' + ? AuthorizeResult.ALLOW + : AuthorizeResult.DENY, + }), + }); + await expect( + notEmpty.authorize({ + permission: createPermission({ + name: 'permission.1', + attributes: {}, + }), + }), + ).resolves.toEqual({ result: AuthorizeResult.ALLOW }); + await expect( + notEmpty.authorize({ + permission: createPermission({ + name: 'permission.2', + attributes: {}, + }), + }), + ).resolves.toEqual({ result: AuthorizeResult.DENY }); + expect(notEmpty.authorize).toHaveBeenCalledTimes(2); + }); + }); + + describe('storage', () => { + describe('instance deep tests', () => { + it('should return undefined for values which are unset', async () => { + const storage = mockApis.storage(); + + expect(storage.snapshot('myfakekey').value).toBeUndefined(); + expect(storage.snapshot('myfakekey')).toEqual({ + key: 'myfakekey', + presence: 'absent', + value: undefined, + newValue: undefined, + }); + }); + + it('should allow the setting and snapshotting of the simple data structures', async () => { + const storage = mockApis.storage(); + + await storage.set('myfakekey', 'helloimastring'); + await storage.set('mysecondfakekey', 1234); + await storage.set('mythirdfakekey', true); + expect(storage.snapshot('myfakekey').value).toBe('helloimastring'); + expect(storage.snapshot('mysecondfakekey').value).toBe(1234); + expect(storage.snapshot('mythirdfakekey').value).toBe(true); + expect(storage.snapshot('myfakekey')).toEqual({ + key: 'myfakekey', + presence: 'present', + value: 'helloimastring', + }); + expect(storage.snapshot('mysecondfakekey')).toEqual({ + key: 'mysecondfakekey', + presence: 'present', + value: 1234, + }); + expect(storage.snapshot('mythirdfakekey')).toEqual({ + key: 'mythirdfakekey', + presence: 'present', + value: true, + }); + }); + + it('should allow setting of complex datastructures', async () => { + const storage = mockApis.storage(); + + const mockData = { + something: 'here', + is: [{ super: { complex: [{ but: 'something', why: true }] } }], + }; + + await storage.set('myfakekey', mockData); + + expect(storage.snapshot('myfakekey').value).toEqual(mockData); + expect(storage.snapshot('myfakekey')).toEqual({ + key: 'myfakekey', + presence: 'present', + value: mockData, + }); + }); + + it('should subscribe to key changes when setting a new value', async () => { + const storage = mockApis.storage(); + + const wrongKeyNextHandler = jest.fn(); + const selectedKeyNextHandler = jest.fn(); + const mockData = { hello: 'im a great new value' }; + + await new Promise(resolve => { + storage.observe$('correctKey').subscribe({ + next: (...args) => { + selectedKeyNextHandler(...args); + resolve(); + }, + }); + + storage.observe$('wrongKey').subscribe({ next: wrongKeyNextHandler }); + + storage.set('correctKey', mockData); + }); + + expect(wrongKeyNextHandler).not.toHaveBeenCalled(); + expect(selectedKeyNextHandler).toHaveBeenCalledTimes(1); + expect(selectedKeyNextHandler).toHaveBeenCalledWith({ + key: 'correctKey', + presence: 'present', + value: mockData, + }); + }); + + it('should subscribe to key changes when deleting a value', async () => { + const storage = mockApis.storage(); + + const wrongKeyNextHandler = jest.fn(); + const selectedKeyNextHandler = jest.fn(); + const mockData = { hello: 'im a great new value' }; + + storage.set('correctKey', mockData); + + await new Promise(resolve => { + storage.observe$('correctKey').subscribe({ + next: (...args) => { + selectedKeyNextHandler(...args); + resolve(); + }, + }); + + storage.observe$('wrongKey').subscribe({ next: wrongKeyNextHandler }); + + storage.remove('correctKey'); + }); + + expect(wrongKeyNextHandler).not.toHaveBeenCalled(); + expect(selectedKeyNextHandler).toHaveBeenCalledTimes(1); + expect(selectedKeyNextHandler).toHaveBeenCalledWith({ + key: 'correctKey', + presence: 'absent', + value: undefined, + newValue: undefined, + }); + }); + + it('should be able to create different buckets for different uses', async () => { + const rootStorage = mockApis.storage(); + + const firstStorage = rootStorage.forBucket('userSettings'); + const secondStorage = rootStorage.forBucket('profileSettings'); + const keyName = 'blobby'; + + await firstStorage.set(keyName, 'boop'); + await secondStorage.set(keyName, 'deerp'); + + expect(firstStorage.snapshot(keyName)).not.toBe( + secondStorage.snapshot(keyName), + ); + expect(firstStorage.snapshot(keyName).value).toBe('boop'); + expect(secondStorage.snapshot(keyName).value).toBe('deerp'); + expect(firstStorage.snapshot(keyName)).not.toEqual( + secondStorage.snapshot(keyName), + ); + expect(firstStorage.snapshot(keyName)).toEqual({ + key: keyName, + presence: 'present', + value: 'boop', + }); + expect(secondStorage.snapshot(keyName)).toEqual({ + key: keyName, + presence: 'present', + value: 'deerp', + }); + }); + + it('should not clash with other namespaces when creating buckets', async () => { + const rootStorage = mockApis.storage(); + + // when getting key test2 it will translate to /profile/something/deep/test2 + const firstStorage = rootStorage + .forBucket('profile') + .forBucket('something') + .forBucket('deep'); + // when getting key deep/test2 it will translate to /profile/something/deep/test2 + const secondStorage = rootStorage.forBucket('profile/something'); + + await firstStorage.set('test2', { error: true }); + + expect(secondStorage.snapshot('deep/test2').value).toBe(undefined); + expect(secondStorage.snapshot('deep/test2')).toMatchObject({ + presence: 'absent', + }); + }); + + it('should not reuse storage instances between different rootStorages', async () => { + const rootStorage1 = mockApis.storage(); + const rootStorage2 = mockApis.storage(); + + const firstStorage = rootStorage1.forBucket('something'); + const secondStorage = rootStorage2.forBucket('something'); + + await firstStorage.set('test2', true); + + expect(firstStorage.snapshot('test2').value).toBe(true); + expect(secondStorage.snapshot('test2').value).toBe(undefined); + expect(firstStorage.snapshot('test2')).toEqual({ + key: 'test2', + presence: 'present', + value: true, + }); + expect(secondStorage.snapshot('test2')).toEqual({ + key: 'test2', + presence: 'absent', + value: undefined, + }); + }); + + it('should freeze the snapshot value', async () => { + const storage = mockApis.storage(); + + const data = { foo: 'bar', baz: [{ foo: 'bar' }] }; + storage.set('foo', data); + + const snapshot = storage.snapshot('foo'); + expect(snapshot.value).not.toBe(data); + + if (snapshot.presence !== 'present') { + throw new Error('Invalid presence'); + } + + expect(() => { + snapshot.value.foo = 'buzz'; + }).toThrow(/Cannot assign to read only property/); + expect(() => { + snapshot.value.baz[0].foo = 'buzz'; + }).toThrow(/Cannot assign to read only property/); + expect(() => { + snapshot.value.baz.push({ foo: 'buzz' }); + }).toThrow(/Cannot add property 1, object is not extensible/); + }); + + it('should freeze observed values', async () => { + const storage = mockApis.storage(); + + const snapshotPromise = new Promise(resolve => { + storage.observe$('test').subscribe({ + next: resolve, + }); + }); + + storage.set('test', { + foo: { + bar: 'baz', + }, + }); + + const snapshot = await snapshotPromise; + expect(snapshot.presence).toBe('present'); + expect(() => { + snapshot.value!.foo.bar = 'qux'; + }).toThrow(/Cannot assign to read only property 'bar' of object/); + }); + + it('should JSON serialize stored values', async () => { + const storage = mockApis.storage(); + + storage.set('test', { + foo: { + toJSON() { + return { + bar: 'baz', + }; + }, + }, + }); + + expect(storage.snapshot('test')).toMatchObject({ + presence: 'present', + value: { + foo: { + bar: 'baz', + }, + }, + }); + }); + }); + + it('can create an instance', () => { + const empty = mockApis.storage(); + expect(empty.snapshot('a')).toEqual({ key: 'a', presence: 'absent' }); + + const notEmpty = mockApis.storage({ data: { a: 1, b: { c: 2 } } }); + expect(notEmpty.snapshot('a')).toEqual({ + key: 'a', + presence: 'present', + value: 1, + }); + expect(notEmpty.forBucket('b').snapshot('c')).toEqual({ + key: 'c', + presence: 'present', + value: 2, + }); + }); + + it('can create a mock and make assertions on it', () => { + const empty = mockApis.storage.mock(); + expect(empty.snapshot('a')).toBeUndefined(); + expect(empty.snapshot).toHaveBeenCalledTimes(1); + + const notEmpty = mockApis.storage.mock({ + snapshot(k: string): StorageValueSnapshot { + return { key: k, presence: 'present', value: 'v' as T }; + }, + }); + expect(notEmpty.snapshot('a')).toEqual({ + key: 'a', + presence: 'present', + value: 'v', + }); + expect(notEmpty.snapshot).toHaveBeenCalledTimes(1); + }); + }); + + describe('translation', () => { + describe('instance deep tests', () => { + function snapshotWithMessages< + const TMessages extends { [key in string]: string }, + >(messages: TMessages) { + const translationApi = mockApis.translation(); + const ref = createTranslationRef({ + id: 'test', + messages, + }); + const snapshot = translationApi.getTranslation(ref); + if (!snapshot.ready) { + throw new Error('Translation snapshot is not ready'); + } + return snapshot; + } + + it('should format plain messages', () => { + const snapshot = snapshotWithMessages({ + foo: 'Foo', + bar: 'Bar', + baz: 'Baz', + }); + + expect(snapshot.t('foo')).toBe('Foo'); + expect(snapshot.t('bar')).toBe('Bar'); + expect(snapshot.t('baz')).toBe('Baz'); + }); + + it('should support interpolation', () => { + const snapshot = snapshotWithMessages({ + shallow: 'Foo {{ bar }}', + multiple: 'Foo {{ bar }} {{ baz }}', + deep: 'Foo {{ bar.baz }}', + }); + + // @ts-expect-error + expect(snapshot.t('shallow')).toBe('Foo {{ bar }}'); + expect(snapshot.t('shallow', { bar: 'Bar' })).toBe('Foo Bar'); + + // @ts-expect-error + expect(snapshot.t('multiple')).toBe('Foo {{ bar }} {{ baz }}'); + // @ts-expect-error + expect(snapshot.t('multiple', { bar: 'Bar' })).toBe( + 'Foo Bar {{ baz }}', + ); + expect(snapshot.t('multiple', { bar: 'Bar', baz: 'Baz' })).toBe( + 'Foo Bar Baz', + ); + + // @ts-expect-error + expect(snapshot.t('deep')).toBe('Foo {{ bar.baz }}'); + expect(snapshot.t('deep', { bar: { baz: 'Baz' } })).toBe('Foo Baz'); + }); + + // Escaping isn't as useful in React, since we don't need to escape HTML in strings + it('should not escape by default', () => { + const snapshot = snapshotWithMessages({ + foo: 'Foo {{ foo }}', + }); + + expect(snapshot.t('foo', { foo: '
' })).toBe('Foo
'); + expect( + snapshot.t('foo', { + foo: '
', + interpolation: { escapeValue: true }, + }), + ).toBe('Foo <div>'); + }); + + it('should support nesting', () => { + const snapshot = snapshotWithMessages({ + foo: 'Foo $t(bar) $t(baz)', + bar: 'Nested', + baz: 'Baz {{ qux }}', + }); + + expect(snapshot.t('foo', { qux: 'Deep' })).toBe('Foo Nested Baz Deep'); + }); + + it('should support formatting', () => { + const snapshot = snapshotWithMessages({ + plain: '= {{ x }}', + number: '= {{ x, number }}', + numberFixed: '= {{ x, number(minimumFractionDigits: 2) }}', + relativeTime: '= {{ x, relativeTime }}', + relativeSeconds: '= {{ x, relativeTime(second) }}', + relativeSecondsShort: + '= {{ x, relativeTime(range: second; style: short) }}', + list: '= {{ x, list }}', + }); + + expect(snapshot.t('plain', { x: '5' })).toBe('= 5'); + expect(snapshot.t('number', { x: 5 })).toBe('= 5'); + expect( + snapshot.t('number', { + x: 5, + formatParams: { x: { minimumFractionDigits: 1 } }, + }), + ).toBe('= 5.0'); + expect(snapshot.t('numberFixed', { x: 5 })).toBe('= 5.00'); + expect( + snapshot.t('numberFixed', { + x: 5, + formatParams: { x: { minimumFractionDigits: 3 } }, + }), + ).toBe('= 5.000'); + expect(snapshot.t('relativeTime', { x: 3 })).toBe('= in 3 days'); + expect(snapshot.t('relativeTime', { x: -3 })).toBe('= 3 days ago'); + expect( + snapshot.t('relativeTime', { + x: 15, + formatParams: { x: { range: 'weeks' } }, + }), + ).toBe('= in 15 weeks'); + expect( + snapshot.t('relativeTime', { + x: 15, + formatParams: { x: { range: 'weeks', style: 'short' } }, + }), + ).toBe('= in 15 wk.'); + expect(snapshot.t('relativeSeconds', { x: 1 })).toBe('= in 1 second'); + expect(snapshot.t('relativeSeconds', { x: 2 })).toBe('= in 2 seconds'); + expect(snapshot.t('relativeSeconds', { x: -3 })).toBe( + '= 3 seconds ago', + ); + expect(snapshot.t('relativeSeconds', { x: 0 })).toBe('= in 0 seconds'); + expect(snapshot.t('relativeSecondsShort', { x: 1 })).toBe( + '= in 1 sec.', + ); + expect(snapshot.t('relativeSecondsShort', { x: 2 })).toBe( + '= in 2 sec.', + ); + expect(snapshot.t('relativeSecondsShort', { x: -3 })).toBe( + '= 3 sec. ago', + ); + expect(snapshot.t('relativeSecondsShort', { x: 0 })).toBe( + '= in 0 sec.', + ); + expect(snapshot.t('list', { x: ['a'] })).toBe('= a'); + expect(snapshot.t('list', { x: ['a', 'b'] })).toBe('= a and b'); + expect(snapshot.t('list', { x: ['a', 'b', 'c'] })).toBe( + '= a, b, and c', + ); + }); + + it('should support plurals', () => { + const snapshot = snapshotWithMessages({ + derp_one: 'derp', + derp_other: 'derps', + derpWithCount_one: '{{ count }} derp', + derpWithCount_other: '{{ count }} derps', + }); + + expect(snapshot.t('derp', { count: 1 })).toBe('derp'); + expect(snapshot.t('derp', { count: 2 })).toBe('derps'); + expect(snapshot.t('derp', { count: 0 })).toBe('derps'); + expect(snapshot.t('derpWithCount', { count: 1 })).toBe('1 derp'); + expect(snapshot.t('derpWithCount', { count: 2 })).toBe('2 derps'); + expect(snapshot.t('derpWithCount', { count: 0 })).toBe('0 derps'); + }); + }); + + it('can create an instance', () => { + const translation = mockApis.translation(); + const ref = createTranslationRef({ + id: 'test', + messages: { a: 'b' }, + }); + const result = translation.getTranslation(ref); + if (!result.ready) { + throw new Error('not ready'); + } + expect(result.t('a')).toEqual('b'); + }); + + it('can create a mock and make assertions on it', () => { + const ref = createTranslationRef({ + id: 'test', + messages: { a: 'b' }, + }); + + const empty = mockApis.translation.mock(); + expect(empty.getTranslation(ref)).toBeUndefined(); + + const notEmpty = mockApis.translation.mock({ + getTranslation: () => + ({ + ready: true, + t: () => 'b', + } as any), + }); + const result = notEmpty.getTranslation(ref); + if (!result.ready) { + throw new Error('not ready'); + } + expect(result.t('a')).toEqual('b'); + expect(notEmpty.getTranslation).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..00473679d2 --- /dev/null +++ b/packages/test-utils/src/testUtils/apis/mockApis.ts @@ -0,0 +1,381 @@ +/* + * 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 { + AnalyticsApi, + ApiFactory, + ApiRef, + ConfigApi, + DiscoveryApi, + IdentityApi, + StorageApi, + analyticsApiRef, + configApiRef, + createApiFactory, + discoveryApiRef, + identityApiRef, + storageApiRef, +} from '@backstage/core-plugin-api'; +import { + TranslationApi, + translationApiRef, +} from '@backstage/core-plugin-api/alpha'; +import { + AuthorizeResult, + EvaluatePermissionRequest, +} from '@backstage/plugin-permission-common'; +import { + PermissionApi, + permissionApiRef, +} from '@backstage/plugin-permission-react'; +import { JsonObject } from '@backstage/types'; +import { ApiMock } from './ApiMock'; +import { MockPermissionApi } from './PermissionApi'; +import { MockStorageApi } from './StorageApi'; +import { MockTranslationApi } from './TranslationApi'; + +/** @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 analyticsMockSkeleton = (): jest.Mocked => ({ + captureEvent: jest.fn(), + }); + /** + * Mock implementation of {@link @backstage/core-plugin-api#AnalyticsApi}. + * + * @public + */ + export function analytics(): AnalyticsApi { + return analyticsMockSkeleton(); + } + /** + * Mock implementations of {@link @backstage/core-plugin-api#AnalyticsApi}. + * + * @public + */ + export namespace analytics { + export const factory = simpleFactory(analyticsApiRef, analytics); + export const mock = simpleMock(analyticsApiRef, analyticsMockSkeleton); + } + + /** + * Fake implementation of {@link @backstage/core-plugin-api#ConfigApi} + * with optional data supplied. + * + * @public + * @example + * + * ```tsx + * const config = mockApis.config({ + * data: { app: { baseUrl: 'https://example.com' } }, + * }); + * + * await renderInTestApp( + * + * + * , + * ); + * ``` + */ + export function config(options?: { data?: JsonObject }): ConfigApi { + return new ConfigReader(options?.data, 'mock-config'); + } + /** + * Mock helpers for {@link @backstage/core-plugin-api#ConfigApi}. + * + * @see {@link @backstage/core-plugin-api#mockApis.config} + * @public + */ + export namespace config { + /** + * Creates a factory for a fake implementation of + * {@link @backstage/core-plugin-api#ConfigApi} with optional + * configuration data supplied. + * + * @public + */ + export const factory = simpleFactory(configApiRef, config); + /** + * Creates a mock implementation of + * {@link @backstage/core-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, () => ({ + 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/core-plugin-api#DiscoveryApi}. By + * default returns URLs on the form `http://example.com/api/`. + * + * @public + */ + export function discovery(options?: { baseUrl?: string }): DiscoveryApi { + const baseUrl = options?.baseUrl ?? 'http://example.com'; + return { + async getBaseUrl(pluginId: string) { + return `${baseUrl}/api/${pluginId}`; + }, + }; + } + /** + * Mock implementations of {@link @backstage/core-plugin-api#DiscoveryApi}. + * + * @public + */ + export namespace discovery { + export const factory = simpleFactory(discoveryApiRef, discovery); + export const mock = simpleMock(discoveryApiRef, () => ({ + getBaseUrl: jest.fn(), + })); + } + + /** + * Fake implementation of {@link @backstage/core-plugin-api#IdentityApi}. By + * default returns no token or profile info, and the user `user:default/test`. + * + * @public + */ + export function identity(options?: { + userEntityRef?: string; + ownershipEntityRefs?: string[]; + token?: string; + email?: string; + displayName?: string; + picture?: string; + }): IdentityApi { + const { + userEntityRef = 'user:default/test', + ownershipEntityRefs = ['user:default/test'], + token, + email, + displayName, + picture, + } = options ?? {}; + return { + async getBackstageIdentity() { + return { type: 'user', ownershipEntityRefs, userEntityRef }; + }, + async getCredentials() { + return { token }; + }, + async getProfileInfo() { + return { email, displayName, picture }; + }, + async signOut() {}, + }; + } + /** + * Mock implementations of {@link @backstage/core-plugin-api#IdentityApi}. + * + * @public + */ + export namespace identity { + export const factory = simpleFactory(identityApiRef, identity); + export const mock = simpleMock( + identityApiRef, + (): jest.Mocked => ({ + getBackstageIdentity: jest.fn(), + getCredentials: jest.fn(), + getProfileInfo: jest.fn(), + signOut: jest.fn(), + }), + ); + } + + /** + * Fake implementation of + * {@link @backstage/plugin-permission-react#PermissionApi}. By default allows + * all actions. + * + * @public + */ + export function permission(options?: { + authorize?: + | AuthorizeResult.ALLOW + | AuthorizeResult.DENY + | (( + request: EvaluatePermissionRequest, + ) => AuthorizeResult.ALLOW | AuthorizeResult.DENY); + }): PermissionApi { + const authorizeInput = options?.authorize; + let authorize: ( + request: EvaluatePermissionRequest, + ) => AuthorizeResult.ALLOW | AuthorizeResult.DENY; + if (authorizeInput === undefined) { + authorize = () => AuthorizeResult.ALLOW; + } else if (typeof authorizeInput === 'function') { + authorize = authorizeInput; + } else { + authorize = () => authorizeInput; + } + return new MockPermissionApi(authorize); + } + /** + * Mock implementation of + * {@link @backstage/plugin-permission-react#PermissionApi}. + * + * @public + */ + export namespace permission { + export const factory = simpleFactory(permissionApiRef, permission); + export const mock = simpleMock(permissionApiRef, () => ({ + authorize: jest.fn(), + })); + } + + /** + * Fake implementation of {@link @backstage/core-plugin-api#StorageApi}. + * Stores data temporarily in memory. + * + * @public + */ + export function storage(options?: { data?: JsonObject }): StorageApi { + return MockStorageApi.create(options?.data); + } + /** + * Mock implementations of {@link @backstage/core-plugin-api#StorageApi}. + * + * @public + */ + export namespace storage { + export const factory = simpleFactory(storageApiRef, storage); + export const mock = simpleMock(storageApiRef, () => ({ + forBucket: jest.fn(), + set: jest.fn(), + remove: jest.fn(), + observe$: jest.fn(), + snapshot: jest.fn(), + })); + } + + /** + * Fake implementation of {@link @backstage/core-plugin-api/alpha#TranslationApi}. + * By default returns the default translation. + * + * @public + */ + export function translation(): TranslationApi { + return MockTranslationApi.create(); + } + /** + * Mock implementations of {@link @backstage/core-plugin-api/alpha#TranslationApi}. + * + * @public + */ + export namespace translation { + export const factory = simpleFactory(translationApiRef, translation); + export const mock = simpleMock(translationApiRef, () => ({ + getTranslation: jest.fn(), + translation$: jest.fn(), + })); + } +} diff --git a/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx b/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx index b8d8b07436..34706e8136 100644 --- a/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ -import { ConfigReader } from '@backstage/core-app-api'; import { TableColumn, TableProps } from '@backstage/core-components'; import { configApiRef, storageApiRef } from '@backstage/core-plugin-api'; import { @@ -28,8 +27,7 @@ import { } from '@backstage/plugin-catalog-react'; import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; import { - MockPermissionApi, - MockStorageApi, + mockApis, TestApiProvider, renderInTestApp, } from '@backstage/test-utils'; @@ -72,17 +70,15 @@ describe('DefaultApiExplorerPage', () => { }), }); - const configApi = new ConfigReader({ - organization: { - name: 'My Company', - }, + const configApi = mockApis.config({ + data: { organization: { name: 'My Company' } }, }); const apiDocsConfig = { getApiDefinitionWidget: () => undefined, }; - const storageApi = MockStorageApi.create(); + const storageApi = mockApis.storage(); const renderWrapped = (children: React.ReactNode) => renderInTestApp( @@ -96,7 +92,7 @@ describe('DefaultApiExplorerPage', () => { new DefaultStarredEntitiesApi({ storageApi }), ], [apiDocsConfigRef, apiDocsConfig], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > {children} diff --git a/plugins/auth-react/src/components/CookieAuthRedirect/CookieAuthRedirect.test.tsx b/plugins/auth-react/src/components/CookieAuthRedirect/CookieAuthRedirect.test.tsx index 6c0141a9d1..18c6408407 100644 --- a/plugins/auth-react/src/components/CookieAuthRedirect/CookieAuthRedirect.test.tsx +++ b/plugins/auth-react/src/components/CookieAuthRedirect/CookieAuthRedirect.test.tsx @@ -16,12 +16,16 @@ import React from 'react'; import { screen, waitFor } from '@testing-library/react'; -import { TestApiProvider, renderInTestApp } from '@backstage/test-utils'; +import { + TestApiProvider, + renderInTestApp, + mockApis, +} from '@backstage/test-utils'; import { identityApiRef } from '@backstage/core-plugin-api'; import { CookieAuthRedirect } from './CookieAuthRedirect'; describe('CookieAuthRedirect', () => { - const identityApiMock = { getCredentials: jest.fn() }; + const identityApiMock = mockApis.identity.mock(); beforeEach(() => { jest.clearAllMocks(); diff --git a/plugins/auth-react/src/components/CookieAuthRefreshProvider/CookieAuthRefreshProvider.test.tsx b/plugins/auth-react/src/components/CookieAuthRefreshProvider/CookieAuthRefreshProvider.test.tsx index bba876e6be..8b07abb163 100644 --- a/plugins/auth-react/src/components/CookieAuthRefreshProvider/CookieAuthRefreshProvider.test.tsx +++ b/plugins/auth-react/src/components/CookieAuthRefreshProvider/CookieAuthRefreshProvider.test.tsx @@ -19,7 +19,7 @@ import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { CookieAuthRefreshProvider } from './CookieAuthRefreshProvider'; import { - MockStorageApi, + mockApis, TestApiProvider, renderInTestApp, } from '@backstage/test-utils'; @@ -30,12 +30,7 @@ import { } from '@backstage/core-plugin-api'; describe('CookieAuthRefreshProvider', () => { - const storageApiMock = MockStorageApi.create(); - const discoveryApiMock = { - getBaseUrl: jest - .fn() - .mockResolvedValue('http://localhost:7000/api/techdocs'), - }; + const discoveryApiMock = mockApis.discovery(); function getExpiresAtInFuture() { const tenMinutesInMilliseconds = 10 * 60 * 1000; @@ -51,7 +46,7 @@ describe('CookieAuthRefreshProvider', () => { @@ -76,7 +71,7 @@ describe('CookieAuthRefreshProvider', () => { @@ -107,7 +102,7 @@ describe('CookieAuthRefreshProvider', () => { @@ -119,7 +114,7 @@ describe('CookieAuthRefreshProvider', () => { await waitFor(() => expect(fetchApiMock.fetch).toHaveBeenCalledWith( - 'http://localhost:7000/api/techdocs/.backstage/auth/v1/cookie', + 'http://example.com/api/techdocs/.backstage/auth/v1/cookie', { credentials: 'include' }, ), ); @@ -153,7 +148,7 @@ describe('CookieAuthRefreshProvider', () => { diff --git a/plugins/auth-react/src/hooks/useCookieAuthRefresh/useCookieAuthRefresh.test.tsx b/plugins/auth-react/src/hooks/useCookieAuthRefresh/useCookieAuthRefresh.test.tsx index 422f392e93..6b59dbcb9e 100644 --- a/plugins/auth-react/src/hooks/useCookieAuthRefresh/useCookieAuthRefresh.test.tsx +++ b/plugins/auth-react/src/hooks/useCookieAuthRefresh/useCookieAuthRefresh.test.tsx @@ -17,15 +17,11 @@ import React from 'react'; import { renderHook, waitFor } from '@testing-library/react'; import { fetchApiRef, discoveryApiRef } from '@backstage/core-plugin-api'; -import { TestApiProvider } from '@backstage/test-utils'; +import { TestApiProvider, mockApis } from '@backstage/test-utils'; import { useCookieAuthRefresh } from './useCookieAuthRefresh'; describe('useCookieAuthRefresh', () => { - const discoveryApiMock = { - getBaseUrl: jest - .fn() - .mockResolvedValue('http://localhost:7000/api/techdocs'), - }; + const discoveryApiMock = mockApis.discovery(); const now = 1710316886171; const tenMinutesInMilliseconds = 10 * 60 * 1000; @@ -269,7 +265,7 @@ describe('useCookieAuthRefresh', () => { await waitFor(() => expect(fetchApiMock.fetch).toHaveBeenCalledWith( - 'http://localhost:7000/api/techdocs/.backstage/auth/v1/cookie', + 'http://example.com/api/techdocs/.backstage/auth/v1/cookie', { credentials: 'include' }, ), ); diff --git a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx index 916653f600..22eff03843 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx @@ -24,7 +24,7 @@ import { } from '@backstage/plugin-catalog-react'; import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; import { - MockAnalyticsApi, + mockApis, renderInTestApp, TestApiProvider, TestApiRegistry, @@ -212,9 +212,9 @@ describe('', () => { ], })); - const analyticsSpy = new MockAnalyticsApi(); + const analyticsApi = mockApis.analytics(); await renderInTestApp( - + {wrapper} , { @@ -228,12 +228,14 @@ describe('', () => { expect(await screen.findByText('b:d/c')).toBeInTheDocument(); await userEvent.click(await screen.findByText('b:d/c')); - expect(analyticsSpy.getEvents()[0]).toMatchObject({ - action: 'click', - subject: 'b:d/c', - attributes: { - to: '/entity/{kind}/{namespace}/{name}', - }, - }); + expect(analyticsApi.captureEvent).toHaveBeenCalledWith( + expect.objectContaining({ + action: 'click', + subject: 'b:d/c', + attributes: { + to: '/entity/{kind}/{namespace}/{name}', + }, + }), + ); }); }); diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx index 9a081747ad..054986e595 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx @@ -23,7 +23,7 @@ import { analyticsApiRef } from '@backstage/core-plugin-api'; import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react'; import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; import { - MockAnalyticsApi, + mockApis, renderInTestApp, TestApiProvider, } from '@backstage/test-utils'; @@ -227,9 +227,9 @@ describe.skip('', () => { }), ); - const analyticsSpy = new MockAnalyticsApi(); + const analyticsApi = mockApis.analytics(); await renderInTestApp( - + {wrapper} , { @@ -243,10 +243,12 @@ describe.skip('', () => { await userEvent.click(screen.getByText('b:d/e')); - expect(analyticsSpy.getEvents()[0]).toMatchObject({ - action: 'click', - subject: 'b:d/e', - }); + expect(analyticsApi.captureEvent).toHaveBeenCalledWith( + expect.objectContaining({ + action: 'click', + subject: 'b:d/e', + }), + ); }); test('should capture analytics event when navigating to entity', async () => { @@ -256,9 +258,9 @@ describe.skip('', () => { }), ); - const analyticsSpy = new MockAnalyticsApi(); + const analyticsApi = mockApis.analytics(); await renderInTestApp( - + {wrapper} , { @@ -274,12 +276,14 @@ describe.skip('', () => { await user.keyboard('{Shift>}'); await user.click(screen.getByText('b:d/e')); - expect(analyticsSpy.getEvents()[0]).toMatchObject({ - action: 'click', - subject: 'b:d/e', - attributes: { - to: '/entity/b/d/e', - }, - }); + expect(analyticsApi.captureEvent).toHaveBeenCalledWith( + expect.objectContaining({ + action: 'click', + subject: 'b:d/e', + attributes: { + to: '/entity/b/d/e', + }, + }), + ); }); }); 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 }) => ( ', () => { it('should add to favorites', async () => { await renderInTestApp( @@ -79,7 +77,7 @@ describe('', () => { await renderInTestApp( diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx index 888940f30b..3ef5c242a1 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx @@ -34,15 +34,13 @@ import { } from '@backstage/catalog-client'; import { catalogApiRef } from '../../api'; import { - MockStorageApi, TestApiRegistry, + mockApis, renderInTestApp, } from '@backstage/test-utils'; import { ApiProvider } from '@backstage/core-app-api'; import { - ConfigApi, configApiRef, - IdentityApi, identityApiRef, storageApiRef, } from '@backstage/core-plugin-api'; @@ -61,15 +59,20 @@ const mockUser: UserEntity = { }, }; -const mockConfigApi = { - getOptionalString: () => 'Test Company', -} as Partial; +const ownershipEntityRefs = ['user:default/testuser']; + +const mockConfigApi = mockApis.config({ + data: { organization: { name: 'Test Company' } }, +}); const mockCatalogApi = catalogApiMock.mock(); +jest.spyOn(mockCatalogApi, 'queryEntities'); -const mockIdentityApi = { - getBackstageIdentity: jest.fn(), -} as Partial>; +const mockIdentityApi = mockApis.identity({ + userEntityRef: ownershipEntityRefs[0], + ownershipEntityRefs, +}); +jest.spyOn(mockIdentityApi, 'getBackstageIdentity'); const mockStarredEntitiesApi = new MockStarredEntitiesApi(); @@ -77,11 +80,10 @@ const apis = TestApiRegistry.from( [configApiRef, mockConfigApi], [catalogApiRef, mockCatalogApi], [identityApiRef, mockIdentityApi], - [storageApiRef, MockStorageApi.create()], + [storageApiRef, mockApis.storage()], [starredEntitiesApiRef, mockStarredEntitiesApi], ); -const ownershipEntityRefs = ['user:default/testuser']; describe('', () => { const mockQueryEntitiesImplementation: CatalogApi['queryEntities'] = async request => { @@ -133,19 +135,13 @@ describe('', () => { beforeEach(() => { mockCatalogApi.getEntityByRef?.mockResolvedValue(mockUser); - mockIdentityApi.getBackstageIdentity?.mockResolvedValue({ - ownershipEntityRefs, - type: 'user', - userEntityRef: 'user:default/testuser', - }); - mockCatalogApi.queryEntities?.mockImplementation( mockQueryEntitiesImplementation, ); }); afterEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); it('renders filter groups', async () => { diff --git a/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.test.tsx b/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.test.tsx index 4c620060fe..8ea9f63472 100644 --- a/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.test.tsx @@ -23,11 +23,7 @@ import { useEntityList, } from '../../hooks'; import { catalogApiRef } from '../../api'; -import { - ApiRef, - IdentityApi, - identityApiRef, -} from '@backstage/core-plugin-api'; +import { ApiRef, identityApiRef } from '@backstage/core-plugin-api'; import { MemoryRouter } from 'react-router-dom'; import { useOwnedEntitiesCount } from './useOwnedEntitiesCount'; import { @@ -36,12 +32,14 @@ import { EntityUserFilter, } from '../../filters'; import { useMountEffect } from '@react-hookz/web'; +import { mockApis } from '@backstage/test-utils'; const mockCatalogApi = catalogApiMock.mock(); - -const mockGetBackstageIdentity: jest.MockedFn< - IdentityApi['getBackstageIdentity'] -> = jest.fn(); +const mockIdentityApi = mockApis.identity({ + ownershipEntityRefs: ['user:default/spiderman', 'user:group/a-group'], + userEntityRef: 'user:default/spiderman', +}); +jest.spyOn(mockIdentityApi, 'getBackstageIdentity'); jest.mock('@backstage/core-plugin-api', () => { const actual = jest.requireActual('@backstage/core-plugin-api'); @@ -50,13 +48,9 @@ jest.mock('@backstage/core-plugin-api', () => { useApi: (ref: ApiRef) => { if (ref === catalogApiRef) { return mockCatalogApi; + } else if (ref === identityApiRef) { + return mockIdentityApi; } - if (ref === identityApiRef) { - return { - getBackstageIdentity: mockGetBackstageIdentity, - }; - } - return actual.useApi(ref); }, }; @@ -65,12 +59,6 @@ jest.mock('@backstage/core-plugin-api', () => { describe('useOwnedEntitiesCount', () => { beforeEach(() => { jest.clearAllMocks(); - - mockGetBackstageIdentity.mockResolvedValue({ - ownershipEntityRefs: ['user:default/spiderman', 'user:group/a-group'], - userEntityRef: 'user:default/spiderman', - type: 'user', - }); }); it(`shouldn't invoke queryEntities when filters are loading`, async () => { @@ -84,7 +72,9 @@ describe('useOwnedEntitiesCount', () => { wrapper: createWrapperWithInitialFilters({}), }); - await waitFor(() => expect(mockGetBackstageIdentity).toHaveBeenCalled()); + await waitFor(() => + expect(mockIdentityApi.getBackstageIdentity).toHaveBeenCalled(), + ); await expect( waitFor(() => expect(mockCatalogApi.queryEntities).toHaveBeenCalled()), @@ -114,7 +104,9 @@ describe('useOwnedEntitiesCount', () => { }), }); - await waitFor(() => expect(mockGetBackstageIdentity).toHaveBeenCalled()); + await waitFor(() => + expect(mockIdentityApi.getBackstageIdentity).toHaveBeenCalled(), + ); await waitFor(() => expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ @@ -151,7 +143,9 @@ describe('useOwnedEntitiesCount', () => { }), }); - await waitFor(() => expect(mockGetBackstageIdentity).toHaveBeenCalled()); + await waitFor(() => + expect(mockIdentityApi.getBackstageIdentity).toHaveBeenCalled(), + ); await expect( waitFor(() => expect(mockCatalogApi.queryEntities).toHaveBeenCalled()), @@ -185,7 +179,9 @@ describe('useOwnedEntitiesCount', () => { }), }); - await waitFor(() => expect(mockGetBackstageIdentity).toHaveBeenCalled()); + await waitFor(() => + expect(mockIdentityApi.getBackstageIdentity).toHaveBeenCalled(), + ); await waitFor(() => expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ diff --git a/plugins/catalog-react/src/hooks/useEntity.test.tsx b/plugins/catalog-react/src/hooks/useEntity.test.tsx index 2923ed828d..022584bd4c 100644 --- a/plugins/catalog-react/src/hooks/useEntity.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntity.test.tsx @@ -25,7 +25,7 @@ import { import { Entity } from '@backstage/catalog-model'; import { analyticsApiRef, useAnalytics } from '@backstage/core-plugin-api'; import { - MockAnalyticsApi, + mockApis, TestApiRegistry, withLogCollector, } from '@backstage/test-utils'; @@ -57,7 +57,7 @@ describe('useEntity', () => { }); it('should provide entityRef analytics context', () => { - const analyticsSpy = new MockAnalyticsApi(); + const analyticsSpy = mockApis.analytics(); const apis = TestApiRegistry.from([analyticsApiRef, analyticsSpy]); const { result } = renderHook(() => useAnalytics(), { wrapper: ({ children }: React.PropsWithChildren<{}>) => ( @@ -69,9 +69,13 @@ describe('useEntity', () => { result.current.captureEvent('test', 'value'); - expect(analyticsSpy.getEvents()[0]).toMatchObject({ - context: { entityRef: 'mykind:default/my-entity' }, - }); + expect(analyticsSpy.captureEvent).toHaveBeenCalledWith( + expect.objectContaining({ + context: expect.objectContaining({ + entityRef: 'mykind:default/my-entity', + }), + }), + ); }); }); @@ -127,7 +131,7 @@ describe('useAsyncEntity', () => { }); it('should provide entityRef analytics context', () => { - const analyticsSpy = new MockAnalyticsApi(); + const analyticsSpy = mockApis.analytics.mock(); const apis = TestApiRegistry.from([analyticsApiRef, analyticsSpy]); const { result } = renderHook(() => useAnalytics(), { wrapper: ({ children }: React.PropsWithChildren<{}>) => ( @@ -144,13 +148,13 @@ describe('useAsyncEntity', () => { result.current.captureEvent('test', 'value'); - expect(analyticsSpy.getEvents()[0]).toMatchObject({ - context: { entityRef: 'mykind:default/my-entity' }, - }); + expect(analyticsSpy.captureEvent.mock.calls[0][0].context.entityRef).toBe( + 'mykind:default/my-entity', + ); }); it('should omit entityRef analytics context', () => { - const analyticsSpy = new MockAnalyticsApi(); + const analyticsSpy = mockApis.analytics.mock(); const apis = TestApiRegistry.from([analyticsApiRef, analyticsSpy]); const { result } = renderHook(() => useAnalytics(), { wrapper: ({ children }: PropsWithChildren<{}>) => ( @@ -162,6 +166,8 @@ describe('useAsyncEntity', () => { result.current.captureEvent('test', 'value'); - expect(analyticsSpy.getEvents()[0].context).not.toHaveProperty('entityRef'); + expect( + analyticsSpy.captureEvent.mock.calls[0][0].context, + ).not.toHaveProperty('entityRef'); }); }); diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx index 0bda2d19ec..2ed944d339 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx @@ -18,14 +18,12 @@ import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; import { Entity } from '@backstage/catalog-model'; import { alertApiRef, - ConfigApi, configApiRef, errorApiRef, - IdentityApi, identityApiRef, storageApiRef, } from '@backstage/core-plugin-api'; -import { MockStorageApi, TestApiProvider } from '@backstage/test-utils'; +import { TestApiProvider, mockApis } from '@backstage/test-utils'; import { act, renderHook, waitFor } from '@testing-library/react'; import qs from 'qs'; import React, { PropsWithChildren } from 'react'; @@ -41,7 +39,6 @@ import { import { EntityListProvider, useEntityList } from './useEntityListProvider'; import { useMountEffect } from '@react-hookz/web'; import { translationApiRef } from '@backstage/core-plugin-api/alpha'; -import { MockTranslationApi } from '@backstage/test-utils/alpha'; import { EntityListPagination } from '../types'; const entities: Entity[] = [ @@ -67,20 +64,12 @@ const entities: Entity[] = [ }, ]; -const mockConfigApi = { - getOptionalString: () => '', -} as Partial; - const ownershipEntityRefs = ['user:default/guest']; -const mockIdentityApi: Partial = { - getBackstageIdentity: async () => ({ - type: 'user', - userEntityRef: 'user:default/guest', - ownershipEntityRefs, - }), - getCredentials: async () => ({ token: undefined }), -}; +const mockIdentityApi = mockApis.identity({ + userEntityRef: 'user:default/guest', + ownershipEntityRefs, +}); const mockCatalogApi = catalogApiMock.mock({ getEntities: jest.fn().mockResolvedValue({ items: entities }), queryEntities: jest.fn().mockResolvedValue({ @@ -108,13 +97,13 @@ const createWrapper = diff --git a/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx b/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx index 5516a249ea..684580792d 100644 --- a/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx @@ -15,20 +15,17 @@ */ import { ComponentEntity, RELATION_OWNED_BY } from '@backstage/catalog-model'; -import { IdentityApi, identityApiRef } from '@backstage/core-plugin-api'; -import { TestApiProvider } from '@backstage/test-utils'; +import { identityApiRef } from '@backstage/core-plugin-api'; +import { TestApiProvider, mockApis } from '@backstage/test-utils'; import { renderHook, waitFor } from '@testing-library/react'; import React from 'react'; import { useEntityOwnership } from './useEntityOwnership'; describe('useEntityOwnership', () => { - type MockIdentityApi = jest.Mocked>; - - const mockIdentityApi: MockIdentityApi = { - getBackstageIdentity: jest.fn(), - }; - - const identityApi = mockIdentityApi as unknown as IdentityApi; + const identityApi = mockApis.identity({ + userEntityRef: 'user:default/user1', + ownershipEntityRefs: ['user:default/user1', 'group:default/group1'], + }); const Wrapper = (props: { children?: React.ReactNode }) => ( @@ -64,12 +61,6 @@ describe('useEntityOwnership', () => { describe('useEntityOwnership', () => { it('matches ownership via ownership entity refs', async () => { - mockIdentityApi.getBackstageIdentity.mockResolvedValue({ - type: 'user', - userEntityRef: 'user:default/user1', - ownershipEntityRefs: ['user:default/user1', 'group:default/group1'], - }); - const { result } = renderHook(() => useEntityOwnership(), { wrapper: Wrapper, }); diff --git a/plugins/catalog/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.test.ts b/plugins/catalog/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.test.ts index b696e44e0a..5e9e6657af 100644 --- a/plugins/catalog/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.test.ts +++ b/plugins/catalog/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { MockStorageApi } from '@backstage/test-utils'; +import { mockApis } from '@backstage/test-utils'; import { DefaultStarredEntitiesApi } from './DefaultStarredEntitiesApi'; import { performMigrationToTheNewBucket } from './migration'; @@ -44,7 +44,7 @@ describe('DefaultStarredEntitiesApi', () => { describe('constructor', () => { it('should call migration', () => { const api = new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }); expect(performMigrationToTheNewBucket).toHaveBeenCalledTimes(1); expect(api).toBeDefined(); @@ -54,7 +54,7 @@ describe('DefaultStarredEntitiesApi', () => { it('should notify and toggle starred entities', async () => { const entityRef = 'component:default/mock'; - const storageApi = MockStorageApi.create(); + const storageApi = mockApis.storage(); const storageBucket = storageApi.forBucket('starredEntities'); const api = new DefaultStarredEntitiesApi({ storageApi }); @@ -85,7 +85,7 @@ describe('DefaultStarredEntitiesApi', () => { it('should read starred entities from storage', async () => { const entityRef = 'component:default/mock'; - const storageApi = MockStorageApi.create(); + const storageApi = mockApis.storage(); const storageBucket = storageApi.forBucket('starredEntities'); storageBucket.set('entityRefs', [entityRef]); const api = new DefaultStarredEntitiesApi({ storageApi }); diff --git a/plugins/catalog/src/apis/StarredEntitiesApi/migration.test.ts b/plugins/catalog/src/apis/StarredEntitiesApi/migration.test.ts index c9083969ee..9f99c6e2dd 100644 --- a/plugins/catalog/src/apis/StarredEntitiesApi/migration.test.ts +++ b/plugins/catalog/src/apis/StarredEntitiesApi/migration.test.ts @@ -15,18 +15,18 @@ */ import { StorageApi } from '@backstage/core-plugin-api'; -import { MockStorageApi } from '@backstage/test-utils'; +import { mockApis } from '@backstage/test-utils'; import { performMigrationToTheNewBucket } from './migration'; describe('performMigrationToTheNewBucket', () => { let mockStorage: StorageApi; beforeEach(() => { - mockStorage = MockStorageApi.create(); + mockStorage = mockApis.storage(); }); afterEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); it('should migrate', async () => { diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx index b175593b68..87f35bbc3e 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx @@ -24,7 +24,11 @@ import { ScmIntegrationsApi, scmIntegrationsApiRef, } from '@backstage/integration-react'; -import { TestApiProvider, renderInTestApp } from '@backstage/test-utils'; +import { + TestApiProvider, + mockApis, + renderInTestApp, +} from '@backstage/test-utils'; import { createFromTemplateRouteRef, viewTechDocRouteRef } from '../../routes'; import { AboutCard } from './AboutCard'; @@ -37,10 +41,6 @@ import { permissionApiRef } from '@backstage/plugin-permission-react'; import { AuthorizeResult } from '@backstage/plugin-permission-common'; import { SWRConfig } from 'swr'; -const mockAuthorize = jest.fn(); - -const mockPermissionApi = { authorize: mockAuthorize }; - describe('', () => { const catalogApi = catalogApiMock.mock(); @@ -411,10 +411,6 @@ describe('', () => { }, }; - mockAuthorize.mockImplementation(async () => ({ - result: AuthorizeResult.ALLOW, - })); - await renderInTestApp( ', () => { ScmIntegrationsApi.fromConfig(new ConfigReader({})), ], [catalogApiRef, catalogApi], - [permissionApiRef, mockPermissionApi], + [permissionApiRef, mockApis.permission()], ]} > @@ -466,10 +462,6 @@ describe('', () => { }, }; - mockAuthorize.mockImplementation(async () => ({ - result: AuthorizeResult.DENY, - })); - await renderInTestApp( ', () => { ScmIntegrationsApi.fromConfig(new ConfigReader({})), ], [catalogApiRef, catalogApi], - [permissionApiRef, mockPermissionApi], + [ + permissionApiRef, + mockApis.permission({ authorize: AuthorizeResult.DENY }), + ], ]} > @@ -766,9 +761,6 @@ describe('', () => { namespace: 'default', }, }; - mockAuthorize.mockImplementation(async () => ({ - result: AuthorizeResult.ALLOW, - })); await renderInTestApp( ', () => { ), ], [catalogApiRef, catalogApi], - [permissionApiRef, mockPermissionApi], + [permissionApiRef, mockApis.permission()], ]} > @@ -819,9 +811,6 @@ describe('', () => { namespace: 'default', }, }; - mockAuthorize.mockImplementation(async () => ({ - result: AuthorizeResult.DENY, - })); await renderInTestApp( new Map() }}> ', () => { ), ], [catalogApiRef, catalogApi], - [permissionApiRef, mockPermissionApi], + [ + permissionApiRef, + mockApis.permission({ authorize: AuthorizeResult.DENY }), + ], ]} > diff --git a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx index f5a16e3af3..07cf98359e 100644 --- a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx @@ -17,12 +17,7 @@ import { QueryEntitiesInitialRequest } from '@backstage/catalog-client'; import { RELATION_OWNED_BY } from '@backstage/catalog-model'; import { TableColumn, TableProps } from '@backstage/core-components'; -import { - IdentityApi, - identityApiRef, - ProfileInfo, - storageApiRef, -} from '@backstage/core-plugin-api'; +import { identityApiRef, storageApiRef } from '@backstage/core-plugin-api'; import { catalogApiRef, entityRouteRef, @@ -31,9 +26,8 @@ import { } from '@backstage/plugin-catalog-react'; import { mockBreakpoint } from '@backstage/core-components/testUtils'; import { - MockPermissionApi, - MockStorageApi, TestApiProvider, + mockApis, renderInTestApp, } from '@backstage/test-utils'; import DashboardIcon from '@material-ui/icons/Dashboard'; @@ -54,7 +48,6 @@ describe('DefaultCatalogPage', () => { }); afterEach(() => { window.history.replaceState = origReplaceState; - jest.clearAllMocks(); }); @@ -166,19 +159,11 @@ describe('DefaultCatalogPage', () => { }), }); - const testProfile: Partial = { + const identityApi = mockApis.identity({ + userEntityRef: 'user:default/guest', + ownershipEntityRefs: ['user:default/guest', 'group:default/tools'], displayName: 'Display Name', - }; - const identityApi: Partial = { - getBackstageIdentity: async () => ({ - type: 'user', - userEntityRef: 'user:default/guest', - ownershipEntityRefs: ['user:default/guest', 'group:default/tools'], - }), - getCredentials: async () => ({ token: undefined }), - getProfileInfo: async () => testProfile, - }; - const storageApi = MockStorageApi.create(); + }); const renderWrapped = (children: React.ReactNode) => renderInTestApp( @@ -186,9 +171,9 @@ describe('DefaultCatalogPage', () => { apis={[ [catalogApiRef, catalogApi], [identityApiRef, identityApi], - [storageApiRef, storageApi], + [storageApiRef, mockApis.storage()], [starredEntitiesApiRef, new MockStarredEntitiesApi()], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > {children} diff --git a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.test.tsx b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.test.tsx index f7220cb1ea..4725f35c86 100644 --- a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.test.tsx +++ b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.test.tsx @@ -17,7 +17,7 @@ import { EntityProvider } from '@backstage/plugin-catalog-react'; import { permissionApiRef } from '@backstage/plugin-permission-react'; import { - MockPermissionApi, + mockApis, renderInTestApp, TestApiProvider, } from '@backstage/test-utils'; @@ -26,11 +26,9 @@ import { fireEvent, screen } from '@testing-library/react'; import * as React from 'react'; import { EntityContextMenu } from './EntityContextMenu'; -const mockPermissionApi = new MockPermissionApi(); - function render(children: React.ReactNode) { return renderInTestApp( - + + { }, } as Entity; - const mockApis = TestApiRegistry.from( + const apis = TestApiRegistry.from( [catalogApiRef, catalogApiMock()], [alertApiRef, {} as AlertApi], [starredEntitiesApiRef, new MockStarredEntitiesApi()], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ); it('renders simplest case', async () => { await renderInTestApp( - + @@ -93,7 +93,7 @@ describe('EntityLayout', () => { } as Entity; await renderInTestApp( - + @@ -117,7 +117,7 @@ describe('EntityLayout', () => { it('renders default error message when entity is not found', async () => { await renderInTestApp( - + @@ -142,7 +142,7 @@ describe('EntityLayout', () => { it('renders custom message when entity is not found', async () => { await renderInTestApp( - + Oppps.. Your entity was not found
} @@ -171,7 +171,7 @@ describe('EntityLayout', () => { it('navigates when user clicks different tab', async () => { await renderInTestApp( - + @@ -211,7 +211,7 @@ describe('EntityLayout', () => { const shouldNotRenderTab = (e: Entity) => e.metadata.name === 'some-entity'; await renderInTestApp( - + @@ -254,7 +254,7 @@ describe('EntityLayout', () => { relations: [{ type: 'ownedBy', targetRef: mockTargetRef }], }; await renderInTestApp( - + @@ -327,7 +327,7 @@ describe('EntityLayout - CleanUpAfterRemoval', () => { [catalogApiRef, catalogApi], [alertApiRef, alertApi], [starredEntitiesApiRef, new MockStarredEntitiesApi()], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > @@ -378,7 +378,7 @@ describe('EntityLayout - CleanUpAfterRemoval', () => { [catalogApiRef, catalogApi], [alertApiRef, alertApi], [starredEntitiesApiRef, new MockStarredEntitiesApi()], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > diff --git a/plugins/home/src/api/VisitsStorageApi.test.ts b/plugins/home/src/api/VisitsStorageApi.test.ts index 5e8bc2dbfc..55cf623149 100644 --- a/plugins/home/src/api/VisitsStorageApi.test.ts +++ b/plugins/home/src/api/VisitsStorageApi.test.ts @@ -14,9 +14,8 @@ * limitations under the License. */ -import { BackstageUserIdentity, IdentityApi } from '@backstage/core-plugin-api'; import { VisitsStorageApi } from './VisitsStorageApi'; -import { MockStorageApi } from '@backstage/test-utils'; +import { mockApis } from '@backstage/test-utils'; import { Visit, VisitsApi } from './VisitsApi'; describe('VisitsStorageApi.create', () => { @@ -26,13 +25,9 @@ describe('VisitsStorageApi.create', () => { () => Math.floor(Math.random() * 16).toString(16), // 0x0 to 0xf ) as `${string}-${string}-${string}-${string}-${string}`; - const mockIdentityApi: IdentityApi = { - signOut: jest.fn(), - getProfileInfo: jest.fn(), - getBackstageIdentity: async () => - ({ userEntityRef: 'user:default/guest' } as BackstageUserIdentity), - getCredentials: jest.fn(), - }; + const mockIdentityApi = mockApis.identity({ + userEntityRef: 'user:default/guest', + }); beforeEach(() => { window.crypto.randomUUID = mockRandomUUID; @@ -40,14 +35,14 @@ describe('VisitsStorageApi.create', () => { }); afterEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); jest.useRealTimers(); window.localStorage.clear(); }); it('instantiates', () => { const api = VisitsStorageApi.create({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), identityApi: mockIdentityApi, }); expect(api).toBeTruthy(); @@ -56,7 +51,7 @@ describe('VisitsStorageApi.create', () => { describe('.save()', () => { it('saves a visit', async () => { const api = VisitsStorageApi.create({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), identityApi: mockIdentityApi, }); const visit = { @@ -73,7 +68,7 @@ describe('VisitsStorageApi.create', () => { it('can control the number of stored entities', async () => { const api = VisitsStorageApi.create({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), identityApi: mockIdentityApi, limit: 2, }); @@ -107,7 +102,7 @@ describe('VisitsStorageApi.create', () => { it('correctly bumps the hits from a previous visit', async () => { const api = VisitsStorageApi.create({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), identityApi: mockIdentityApi, }); const visit = { @@ -149,7 +144,7 @@ describe('VisitsStorageApi.create', () => { beforeEach(() => { api = VisitsStorageApi.create({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), identityApi: mockIdentityApi, }); diff --git a/plugins/home/src/api/VisitsWebStorageApi.test.ts b/plugins/home/src/api/VisitsWebStorageApi.test.ts index 1cf1fa3129..8d8669e99e 100644 --- a/plugins/home/src/api/VisitsWebStorageApi.test.ts +++ b/plugins/home/src/api/VisitsWebStorageApi.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { BackstageUserIdentity, IdentityApi } from '@backstage/core-plugin-api'; +import { mockApis } from '@backstage/test-utils'; import { VisitsWebStorageApi } from './VisitsWebStorageApi'; describe('VisitsWebStorageApi.create()', () => { @@ -24,13 +24,9 @@ describe('VisitsWebStorageApi.create()', () => { () => Math.floor(Math.random() * 16).toString(16), // 0x0 to 0xf ) as `${string}-${string}-${string}-${string}-${string}`; - const mockIdentityApi: IdentityApi = { - signOut: jest.fn(), - getProfileInfo: jest.fn(), - getBackstageIdentity: async () => - ({ userEntityRef: 'user:default/guest' } as BackstageUserIdentity), - getCredentials: jest.fn(), - }; + const mockIdentityApi = mockApis.identity({ + userEntityRef: 'user:default/guest', + }); const mockErrorApi = { post: jest.fn(), error$: jest.fn() }; @@ -40,7 +36,7 @@ describe('VisitsWebStorageApi.create()', () => { afterEach(() => { window.localStorage.clear(); - jest.resetAllMocks(); + jest.clearAllMocks(); }); it('instantiates with only identitiyApi', () => { diff --git a/plugins/home/src/api/config.test.ts b/plugins/home/src/api/config.test.ts index c21d856038..8ff472fd6b 100644 --- a/plugins/home/src/api/config.test.ts +++ b/plugins/home/src/api/config.test.ts @@ -15,15 +15,17 @@ */ import { readFilterConfig, createFilterByQueryParamFromConfig } from './config'; -import { MockConfigApi } from '@backstage/test-utils'; +import { mockApis } from '@backstage/test-utils'; describe('config', () => { 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/kubernetes-react/src/components/PodExecTerminal/PodExecTerminal.test.tsx b/plugins/kubernetes-react/src/components/PodExecTerminal/PodExecTerminal.test.tsx index 4aa6112436..0e56c9599a 100644 --- a/plugins/kubernetes-react/src/components/PodExecTerminal/PodExecTerminal.test.tsx +++ b/plugins/kubernetes-react/src/components/PodExecTerminal/PodExecTerminal.test.tsx @@ -14,8 +14,9 @@ * limitations under the License. */ -import { DiscoveryApi, discoveryApiRef } from '@backstage/core-plugin-api'; +import { discoveryApiRef } from '@backstage/core-plugin-api'; import { + mockApis, renderInTestApp, TestApiProvider, textContentMatcher, @@ -37,9 +38,7 @@ describe('PodExecTerminal', () => { const podName = 'pod1'; const podNamespace = 'podNamespace'; - const mockDiscoveryApi: Partial = { - getBaseUrl: () => Promise.resolve('http://localhost'), - }; + const mockDiscoveryApi = mockApis.discovery(); it('Should render an XTerm web terminal', async () => { await renderInTestApp( @@ -62,7 +61,7 @@ describe('PodExecTerminal', () => { it('Should connect to WebSocket server & render response', async () => { const server = new WS( - 'ws://localhost/proxy/api/v1/namespaces/podNamespace/pods/pod1/exec?container=container2&stdin=true&stdout=true&stderr=true&tty=true&command=%2Fbin%2Fsh', + 'ws://example.com/api/kubernetes/proxy/api/v1/namespaces/podNamespace/pods/pod1/exec?container=container2&stdin=true&stdout=true&stderr=true&tty=true&command=%2Fbin%2Fsh', ); await renderInTestApp( diff --git a/plugins/kubernetes-react/src/components/Pods/PodDrawer/PodDrawer.test.tsx b/plugins/kubernetes-react/src/components/Pods/PodDrawer/PodDrawer.test.tsx index 066fa0f874..4718f8f29f 100644 --- a/plugins/kubernetes-react/src/components/Pods/PodDrawer/PodDrawer.test.tsx +++ b/plugins/kubernetes-react/src/components/Pods/PodDrawer/PodDrawer.test.tsx @@ -17,19 +17,21 @@ import React from 'react'; import { screen } from '@testing-library/react'; -import { TestApiProvider, renderInTestApp } from '@backstage/test-utils'; +import { + TestApiProvider, + mockApis, + renderInTestApp, +} from '@backstage/test-utils'; import '@testing-library/jest-dom'; import { PodDrawer } from './PodDrawer'; -import { DiscoveryApi, discoveryApiRef } from '@backstage/core-plugin-api'; +import { discoveryApiRef } from '@backstage/core-plugin-api'; jest.mock('../../../hooks/useIsPodExecTerminalSupported'); describe('PodDrawer', () => { it('Should show title and container names', async () => { - const mockDiscoveryApi: Partial = { - getBaseUrl: () => Promise.resolve('http://localhost'), - }; + const mockDiscoveryApi = mockApis.discovery(); await renderInTestApp( diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx index 6b8261e015..04de321c50 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx @@ -22,7 +22,11 @@ import { StarredEntitiesApi, starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { + mockApis, + renderInTestApp, + TestApiProvider, +} from '@backstage/test-utils'; import React from 'react'; import { MembersListCard } from './MembersListCard'; import { @@ -184,7 +188,7 @@ describe('MemberTab Test', () => { apis={[ [catalogApiRef, mockedCatalogApiSupportingGroups], [starredEntitiesApiRef, mockedStarredEntitiesApi], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -212,7 +216,7 @@ describe('MemberTab Test', () => { apis={[ [catalogApiRef, mockedCatalogApiSupportingGroups], [starredEntitiesApiRef, mockedStarredEntitiesApi], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -238,7 +242,7 @@ describe('MemberTab Test', () => { apis={[ [catalogApiRef, mockedCatalogApiSupportingGroups], [starredEntitiesApiRef, mockedStarredEntitiesApi], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -273,7 +277,7 @@ describe('MemberTab Test', () => { apis={[ [catalogApiRef, mockedCatalogApiSupportingGroups], [starredEntitiesApiRef, mockedStarredEntitiesApi], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -308,7 +312,7 @@ describe('MemberTab Test', () => { apis={[ [catalogApiRef, mockedCatalogApiSupportingGroups], [starredEntitiesApiRef, mockedStarredEntitiesApi], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -366,7 +370,7 @@ describe('MemberTab Test', () => { apis={[ [catalogApiRef, mockedCatalogApiSupportingGroups], [starredEntitiesApiRef, mockedStarredEntitiesApi], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -406,7 +410,7 @@ describe('MemberTab Test', () => { apis={[ [catalogApiRef, mockedCatalogApiSupportingGroups], [starredEntitiesApiRef, mockedStarredEntitiesApi], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > diff --git a/plugins/org/src/components/MyGroupsSidebarItem/MyGroupsSidebarItem.test.tsx b/plugins/org/src/components/MyGroupsSidebarItem/MyGroupsSidebarItem.test.tsx index 9eb9b3f70f..de46be98be 100644 --- a/plugins/org/src/components/MyGroupsSidebarItem/MyGroupsSidebarItem.test.tsx +++ b/plugins/org/src/components/MyGroupsSidebarItem/MyGroupsSidebarItem.test.tsx @@ -14,11 +14,15 @@ * limitations under the License. */ -import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { + renderInTestApp, + TestApiProvider, + mockApis, +} from '@backstage/test-utils'; import React from 'react'; import { MyGroupsSidebarItem } from './MyGroupsSidebarItem'; import GroupIcon from '@material-ui/icons/People'; -import { IdentityApi, identityApiRef } from '@backstage/core-plugin-api'; +import { identityApiRef } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react'; import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; @@ -26,13 +30,10 @@ import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; describe('MyGroupsSidebarItem Test', () => { describe('For guests or users with no groups', () => { it('MyGroupsSidebarItem should be empty', async () => { - const identityApi: Partial = { - getBackstageIdentity: async () => ({ - type: 'user', - userEntityRef: 'user:default/guest', - ownershipEntityRefs: ['user:default/guest'], - }), - }; + const identityApi = mockApis.identity({ + userEntityRef: 'user:default/guest', + ownershipEntityRefs: ['user:default/guest'], + }); const catalogApi = catalogApiMock(); const rendered = await renderInTestApp( { describe('For users that are members of a single group', () => { it('MyGroupsSidebarItem should display a single item that links to their group', async () => { - const identityApi: Partial = { - getBackstageIdentity: async () => ({ - type: 'user', - userEntityRef: 'user:default/nigel.manning', - ownershipEntityRefs: ['user:default/nigel.manning'], - }), - }; + const identityApi = mockApis.identity({ + userEntityRef: 'user:default/nigel.manning', + ownershipEntityRefs: ['user:default/nigel.manning'], + }); const catalogApi = catalogApiMock.mock({ getEntities: async () => ({ items: [ @@ -114,13 +112,10 @@ describe('MyGroupsSidebarItem Test', () => { describe('For users that are members of multiple groups', () => { it('MyGroupsSidebarItem should display a sub-menu with all their groups and a link to each group', async () => { - const identityApi: Partial = { - getBackstageIdentity: async () => ({ - type: 'user', - userEntityRef: 'user:default/nigel.manning', - ownershipEntityRefs: ['user:default/nigel.manning'], - }), - }; + const identityApi = mockApis.identity({ + userEntityRef: 'user:default/nigel.manning', + ownershipEntityRefs: ['user:default/nigel.manning'], + }); const catalogApi = catalogApiMock.mock({ getEntities: async () => ({ items: [ @@ -192,13 +187,10 @@ describe('MyGroupsSidebarItem Test', () => { describe('When an additional filter is not provided', () => { it('catalogApi.getEntities() should be called with the default filter', async () => { - const identityApi: Partial = { - getBackstageIdentity: async () => ({ - type: 'user', - userEntityRef: 'user:default/guest', - ownershipEntityRefs: ['user:default/guest'], - }), - }; + const identityApi = mockApis.identity({ + userEntityRef: 'user:default/guest', + ownershipEntityRefs: ['user:default/guest'], + }); const mockCatalogApi = catalogApiMock.mock(); await renderInTestApp( { describe('When an additional filter is provided', () => { it('catalogApi.getEntities() should be called with an additional filter item', async () => { - const identityApi: Partial = { - getBackstageIdentity: async () => ({ - type: 'user', - userEntityRef: 'user:default/guest', - ownershipEntityRefs: ['user:default/guest'], - }), - }; + const identityApi = mockApis.identity({ + userEntityRef: 'user:default/guest', + ownershipEntityRefs: ['user:default/guest'], + }); const mockCatalogApi = catalogApiMock.mock(); await renderInTestApp( { - const mockPermissionApi = { authorize: jest.fn() }; - it('Returns loading when permissionApi has not yet responded.', () => { - mockPermissionApi.authorize.mockReturnValueOnce(new Promise(() => {})); + const permissionApi = mockApis.permission.mock({ + authorize: async () => new Promise(() => {}), + }); - const { getByText } = renderComponent(mockPermissionApi); + const { getByText } = renderComponent(permissionApi); - expect(mockPermissionApi.authorize).toHaveBeenCalledWith({ permission }); + expect(permissionApi.authorize).toHaveBeenCalledWith({ permission }); expect(getByText('loading')).toBeTruthy(); }); it('Returns allowed when permissionApi allows authorization.', async () => { - mockPermissionApi.authorize.mockResolvedValueOnce({ - result: AuthorizeResult.ALLOW, + const permissionApi = mockApis.permission({ + authorize: AuthorizeResult.ALLOW, }); + jest.spyOn(permissionApi, 'authorize'); - const { findByText } = renderComponent(mockPermissionApi); + const { findByText } = renderComponent(permissionApi); - expect(mockPermissionApi.authorize).toHaveBeenCalledWith({ permission }); + expect(permissionApi.authorize).toHaveBeenCalledWith({ permission }); expect(await findByText('content')).toBeTruthy(); }); it('Returns not allowed when permissionApi denies authorization.', async () => { - mockPermissionApi.authorize.mockResolvedValueOnce({ - result: AuthorizeResult.DENY, + const permissionApi = mockApis.permission({ + authorize: AuthorizeResult.DENY, }); + jest.spyOn(permissionApi, 'authorize'); - const { findByText } = renderComponent(mockPermissionApi); + const { findByText } = renderComponent(permissionApi); - expect(mockPermissionApi.authorize).toHaveBeenCalledWith({ permission }); + expect(permissionApi.authorize).toHaveBeenCalledWith({ permission }); await expect(findByText('content')).rejects.toThrow(); }); }); diff --git a/plugins/scaffolder-react/src/next/components/TemplateCard/CardHeader.test.tsx b/plugins/scaffolder-react/src/next/components/TemplateCard/CardHeader.test.tsx index 58033b718c..9ae2a507dd 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateCard/CardHeader.test.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateCard/CardHeader.test.tsx @@ -13,13 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { fireEvent } from '@testing-library/react'; import { CardHeader } from './CardHeader'; import { ThemeProvider } from '@material-ui/core/styles'; import { lightTheme } from '@backstage/theme'; import { - MockStorageApi, + mockApis, renderInTestApp, TestApiProvider, } from '@backstage/test-utils'; @@ -44,7 +45,7 @@ describe('CardHeader', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], ]} @@ -75,7 +76,7 @@ describe('CardHeader', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], ]} @@ -135,7 +136,7 @@ describe('CardHeader', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], ]} @@ -164,7 +165,7 @@ describe('CardHeader', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], ]} diff --git a/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.test.tsx b/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.test.tsx index febdc7c99e..673f1e4a9e 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.test.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.test.tsx @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { DefaultStarredEntitiesApi } from '@backstage/plugin-catalog'; import { entityRouteRef, starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; import { - MockPermissionApi, - MockStorageApi, + mockApis, renderInTestApp, TestApiProvider, } from '@backstage/test-utils'; @@ -51,10 +51,10 @@ describe('TemplateCard', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > @@ -81,10 +81,10 @@ describe('TemplateCard', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > @@ -113,10 +113,10 @@ describe('TemplateCard', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > @@ -143,10 +143,10 @@ describe('TemplateCard', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > @@ -179,10 +179,10 @@ describe('TemplateCard', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > @@ -219,10 +219,10 @@ describe('TemplateCard', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > @@ -264,10 +264,10 @@ describe('TemplateCard', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > @@ -313,10 +313,10 @@ describe('TemplateCard', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > @@ -356,10 +356,10 @@ describe('TemplateCard', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > @@ -396,10 +396,10 @@ describe('TemplateCard', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > @@ -428,9 +428,6 @@ describe('TemplateCard', () => { }, }; const mockOnSelected = jest.fn(); - const mockAuthorize = jest - .fn() - .mockImplementation(async () => ({ result: AuthorizeResult.DENY })); // SWR used by the usePermission hook needs cache to be reset for each test const { queryByText } = await renderInTestApp( new Map() }}> @@ -439,10 +436,13 @@ describe('TemplateCard', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, new MockPermissionApi(mockAuthorize)], + [ + permissionApiRef, + mockApis.permission({ authorize: AuthorizeResult.DENY }), + ], ]} > diff --git a/plugins/scaffolder-react/src/next/components/Workflow/Workflow.test.tsx b/plugins/scaffolder-react/src/next/components/Workflow/Workflow.test.tsx index 49f5c1feb0..243b9b7e79 100644 --- a/plugins/scaffolder-react/src/next/components/Workflow/Workflow.test.tsx +++ b/plugins/scaffolder-react/src/next/components/Workflow/Workflow.test.tsx @@ -16,7 +16,7 @@ import { ApiProvider } from '@backstage/core-app-api'; import { - MockAnalyticsApi, + mockApis, renderInTestApp, TestApiRegistry, } from '@backstage/test-utils'; @@ -42,7 +42,7 @@ const scaffolderApiMock: jest.Mocked = { const catalogApi = catalogApiMock.mock(); -const analyticsMock = new MockAnalyticsApi(); +const analyticsMock = mockApis.analytics(); const apis = TestApiRegistry.from( [scaffolderApiRef, scaffolderApiMock], [catalogApiRef, catalogApi], diff --git a/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.test.tsx b/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.test.tsx index 6079ec2e1d..01274cf6a6 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.test.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.test.tsx @@ -22,9 +22,9 @@ import { import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; import { permissionApiRef } from '@backstage/plugin-permission-react'; import { - MockStorageApi, renderInTestApp, TestApiProvider, + mockApis, } from '@backstage/test-utils'; import React from 'react'; import { rootRouteRef } from '../../../routes'; @@ -52,10 +52,10 @@ describe('TemplateListPage', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -74,10 +74,10 @@ describe('TemplateListPage', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -97,10 +97,10 @@ describe('TemplateListPage', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -119,10 +119,10 @@ describe('TemplateListPage', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -142,10 +142,10 @@ describe('TemplateListPage', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -164,10 +164,10 @@ describe('TemplateListPage', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -185,10 +185,10 @@ describe('TemplateListPage', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > = { }; const catalogApi = catalogApiMock.mock(); +const analyticsApi = mockApis.analytics(); -const analyticsMock = new MockAnalyticsApi(); const apis = TestApiRegistry.from( [scaffolderApiRef, scaffolderApiMock], [catalogApiRef, catalogApi], - [analyticsApiRef, analyticsMock], + [analyticsApiRef, analyticsApi], [catalogApiRef, catalogApi], ); @@ -81,6 +81,7 @@ const entityRefResponse = { }, }, }; + describe('TemplateWizardPage', () => { it('captures expected analytics events', async () => { scaffolderApiMock.scaffold.mockResolvedValue({ taskId: 'xyz' }); @@ -130,20 +131,29 @@ describe('TemplateWizardPage', () => { }); // The "Next Step" button should have fired an event - expect(analyticsMock.getEvents()[0]).toMatchObject({ - action: 'click', - subject: 'Next Step (1)', - context: { entityRef: 'template:default/test' }, - }); + expect(analyticsApi.captureEvent).toHaveBeenCalledWith( + expect.objectContaining({ + action: 'click', + subject: 'Next Step (1)', + context: expect.objectContaining({ + entityRef: 'template:default/test', + }), + }), + ); // And the "Create" button should have fired an event - expect(analyticsMock.getEvents()[1]).toMatchObject({ - action: 'create', - subject: 'expected-name', - context: { entityRef: 'template:default/test' }, - value: 120, - }); + expect(analyticsApi.captureEvent).toHaveBeenCalledWith( + expect.objectContaining({ + action: 'create', + subject: 'expected-name', + context: expect.objectContaining({ + entityRef: 'template:default/test', + }), + value: 120, + }), + ); }); + describe('scaffolder page context menu', () => { it('should render if editUrl is set to url', async () => { catalogApi.getEntityByRef.mockResolvedValue({ @@ -175,6 +185,7 @@ describe('TemplateWizardPage', () => { ); expect(queryByTestId('menu-button')).toBeInTheDocument(); }); + it('should not render if editUrl is undefined', async () => { catalogApi.getEntityByRef.mockResolvedValue({ apiVersion: 'v1', diff --git a/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx b/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx index 96374662a4..0a78e41966 100644 --- a/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx +++ b/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx @@ -15,7 +15,11 @@ */ import { Entity } from '@backstage/catalog-model'; -import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { + renderInTestApp, + TestApiProvider, + mockApis, +} from '@backstage/test-utils'; import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react'; import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; import React from 'react'; @@ -31,12 +35,7 @@ import { rootRouteRef } from '../../routes'; describe('', () => { const catalogApi = catalogApiMock.mock(); - const identityApi = { - getBackstageIdentity: jest.fn(), - getProfileInfo: jest.fn(), - getCredentials: jest.fn(), - signOut: jest.fn(), - }; + const identityApi = mockApis.identity(); const scaffolderApiMock: jest.Mocked> = { scaffold: jest.fn(), diff --git a/plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.test.tsx b/plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.test.tsx index 35eb9ce0b7..bd5676ba31 100644 --- a/plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.test.tsx +++ b/plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.test.tsx @@ -15,7 +15,11 @@ */ import { Entity } from '@backstage/catalog-model'; -import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { + renderInTestApp, + TestApiProvider, + mockApis, +} from '@backstage/test-utils'; import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react'; import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; import React from 'react'; @@ -24,13 +28,7 @@ import { identityApiRef } from '@backstage/core-plugin-api'; describe('', () => { const catalogApi = catalogApiMock.mock(); - - const identityApi = { - getBackstageIdentity: jest.fn(), - getProfileInfo: jest.fn(), - getCredentials: jest.fn(), - signOut: jest.fn(), - }; + const identityApi = mockApis.identity(); it('should render the column with the user', async () => { const props = { diff --git a/plugins/scaffolder/src/components/OngoingTask/OngoingTask.test.tsx b/plugins/scaffolder/src/components/OngoingTask/OngoingTask.test.tsx index 70d2193470..d62ef6f95e 100644 --- a/plugins/scaffolder/src/components/OngoingTask/OngoingTask.test.tsx +++ b/plugins/scaffolder/src/components/OngoingTask/OngoingTask.test.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { renderInTestApp, TestApiProvider, - MockPermissionApi, + mockApis, } from '@backstage/test-utils'; import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; import { act, fireEvent, waitFor, within } from '@testing-library/react'; @@ -70,7 +70,7 @@ describe('OngoingTask', () => { @@ -146,10 +146,9 @@ describe('OngoingTask', () => { }); it('should have cancel and start over buttons be disabled without the proper permissions', async () => { - const mockAuthorize = jest - .fn() - .mockImplementation(async () => ({ result: AuthorizeResult.DENY })); - const permissionApi: PermissionApi = { authorize: mockAuthorize }; + const permissionApi = mockApis.permission({ + authorize: AuthorizeResult.DENY, + }); const rendered = await render(permissionApi); const { getByTestId } = rendered; diff --git a/plugins/scaffolder/src/components/fields/MyGroupsPicker/MyGroupsPicker.test.tsx b/plugins/scaffolder/src/components/fields/MyGroupsPicker/MyGroupsPicker.test.tsx index d4a37d0752..57ce2d3023 100644 --- a/plugins/scaffolder/src/components/fields/MyGroupsPicker/MyGroupsPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/MyGroupsPicker/MyGroupsPicker.test.tsx @@ -18,7 +18,11 @@ import React from 'react'; import { waitFor } from '@testing-library/react'; import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; import { MyGroupsPicker } from './MyGroupsPicker'; -import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { + renderInTestApp, + TestApiProvider, + mockApis, +} from '@backstage/test-utils'; import { catalogApiRef, entityPresentationApiRef, @@ -26,7 +30,6 @@ import { import { Entity } from '@backstage/catalog-model'; import { ErrorApi, - IdentityApi, errorApiRef, identityApiRef, } from '@backstage/core-plugin-api'; @@ -34,25 +37,9 @@ import userEvent from '@testing-library/user-event'; import { ScaffolderRJSFFieldProps as FieldProps } from '@backstage/plugin-scaffolder-react'; import { DefaultEntityPresentationApi } from '@backstage/plugin-catalog'; -// Create a mock IdentityApi -const mockIdentityApi: IdentityApi = { - getProfileInfo: () => - Promise.resolve({ - displayName: 'Bob', - email: 'bob@example.com', - picture: 'https://example.com/picture.jpg', - }), - getBackstageIdentity: () => - Promise.resolve({ - id: 'Bob', - idToken: 'token', - type: 'user', - userEntityRef: 'user:default/bob', - ownershipEntityRefs: ['group:default/group1', 'group:default/group2'], - }), - getCredentials: () => Promise.resolve({ token: 'token' }), - signOut: () => Promise.resolve(), -}; +const mockIdentityApi = mockApis.identity({ + userEntityRef: 'user:default/bob', +}); describe('', () => { let entities: Entity[]; @@ -96,7 +83,7 @@ describe('', () => { }); afterEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); it('should only return the groups a user is part of and not the groups a user is not part of', async () => { diff --git a/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx b/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx index 3f667a4ad6..0e4df4fb34 100644 --- a/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx +++ b/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx @@ -20,7 +20,7 @@ import userEvent from '@testing-library/user-event'; import { configApiRef } from '@backstage/core-plugin-api'; import { ConfigReader } from '@backstage/core-app-api'; import { - MockAnalyticsApi, + mockApis, TestApiProvider, renderInTestApp, } from '@backstage/test-utils'; @@ -273,7 +273,7 @@ describe('SearchBar', () => { }); it('Does not capture analytics event if not enabled in app', async () => { - const analyticsApiMock = new MockAnalyticsApi(); + const analyticsApiMock = mockApis.analytics(); await renderInTestApp( { await waitFor(() => expect(textbox).toHaveValue(value)); - expect(analyticsApiMock.getEvents()).toHaveLength(0); + expect(analyticsApiMock.captureEvent).not.toHaveBeenCalled(); }); it('Renders custom search icon', async () => { 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/components/SearchResultGroup/SearchResultGroup.test.tsx b/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.test.tsx index 66513783d4..fe1e279dd3 100644 --- a/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.test.tsx +++ b/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.test.tsx @@ -24,7 +24,7 @@ import DocsIcon from '@material-ui/icons/InsertDriveFile'; import { renderInTestApp, TestApiProvider, - MockAnalyticsApi, + mockApis, } from '@backstage/test-utils'; import { createPlugin, analyticsApiRef } from '@backstage/core-plugin-api'; @@ -40,7 +40,7 @@ import { const query = jest.fn().mockResolvedValue({ results: [] }); const searchApiMock = { query }; -const analyticsApiMock = new MockAnalyticsApi(); +const analyticsApiMock = mockApis.analytics(); describe('SearchResultGroup', () => { const results = [ diff --git a/plugins/search-react/src/components/SearchResultList/SearchResultList.test.tsx b/plugins/search-react/src/components/SearchResultList/SearchResultList.test.tsx index 146e5822a4..86ce237135 100644 --- a/plugins/search-react/src/components/SearchResultList/SearchResultList.test.tsx +++ b/plugins/search-react/src/components/SearchResultList/SearchResultList.test.tsx @@ -20,7 +20,7 @@ import { screen, waitFor } from '@testing-library/react'; import { TestApiProvider, renderInTestApp, - MockAnalyticsApi, + mockApis, } from '@backstage/test-utils'; import { analyticsApiRef, createPlugin } from '@backstage/core-plugin-api'; @@ -31,7 +31,7 @@ import { SearchResultList } from './SearchResultList'; const query = jest.fn().mockResolvedValue({ results: [] }); const searchApiMock = { query }; -const analyticsApiMock = new MockAnalyticsApi(); +const analyticsApiMock = mockApis.analytics(); describe('SearchResultList', () => { const results = [ diff --git a/plugins/search-react/src/context/SearchContext.test.tsx b/plugins/search-react/src/context/SearchContext.test.tsx index 58fde35667..f858715adb 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 ( { describe('analytics', () => { it('captures analytics events if enabled in app', async () => { - const analyticsApiMock = { - captureEvent: jest.fn(), - } satisfies typeof analyticsApiRef.T; + const analyticsApiMock = mockApis.analytics(); searchApiMock.query.mockResolvedValue({ results: [], @@ -433,7 +431,7 @@ describe('SearchContext', () => { const { result } = renderHook(() => useSearch(), { wrapper: ({ children }) => { - const configApiMock = new MockConfigApi({}); + const configApiMock = mockApis.config(); return ( { }); it('captures analytics events even if number of results does not exist', async () => { - const analyticsApiMock = { - captureEvent: jest.fn(), - } satisfies typeof analyticsApiRef.T; + const analyticsApiMock = mockApis.analytics(); searchApiMock.query.mockResolvedValue({ results: [], @@ -491,7 +487,7 @@ describe('SearchContext', () => { const { result } = renderHook(() => useSearch(), { wrapper: ({ children }) => { - const configApiMock = new MockConfigApi({}); + const configApiMock = mockApis.config(); return ( { screen.getByRole('link', { name: /Search Result 1/ }), ); - expect(analyticsApiMock.getEvents()[0]).toMatchObject({ + expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith({ action: 'discover', subject: 'Search Result 1', context: { routeRef: 'unknown', pluginId: 'root', extension: 'App' }, @@ -141,7 +141,7 @@ describe('extensions', () => { await userEvent.click(screen.getByRole('listitem')); - expect(analyticsApiMock.getEvents()[0]).toMatchObject({ + expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith({ action: 'discover', subject: 'Search Result 1', context: { routeRef: 'unknown', pluginId: 'root', extension: 'App' }, diff --git a/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx b/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx index 453b766791..d184daa429 100644 --- a/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx +++ b/plugins/search/src/components/SearchType/SearchType.Accordion.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, waitFor } from '@testing-library/react'; import user from '@testing-library/user-event'; import { @@ -41,10 +41,12 @@ jest.mock('@backstage/plugin-search-react', () => ({ })); 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/signals/src/api/SignalsClient.test.ts b/plugins/signals/src/api/SignalsClient.test.ts index 2e93cd69f1..b6944cfdc1 100644 --- a/plugins/signals/src/api/SignalsClient.test.ts +++ b/plugins/signals/src/api/SignalsClient.test.ts @@ -14,27 +14,21 @@ * limitations under the License. */ -import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api'; +import { mockApis } from '@backstage/test-utils'; import WS from 'jest-websocket-mock'; import { SignalClient } from './SignalClient'; describe('SignalsClient', () => { - const tokenFunction = jest.fn(); - const baseUrlFunction = jest.fn(); - const identity = { - getCredentials: tokenFunction, - } as unknown as IdentityApi; - const discoveryApi = { - getBaseUrl: baseUrlFunction, - } as unknown as DiscoveryApi; + const identity = mockApis.identity({ token: '12345' }); + const discoveryApi = mockApis.discovery({ baseUrl: 'http://localhost:1234' }); let server: WS; beforeEach(async () => { - jest.resetAllMocks(); - tokenFunction.mockResolvedValue({ token: '12345' }); - baseUrlFunction.mockResolvedValue('http://localhost:1234'); - server = new WS('ws://localhost:1234', { jsonProtocol: true }); + jest.clearAllMocks(); + server = new WS('ws://localhost:1234/api/signals', { + jsonProtocol: true, + }); }); afterEach(() => { diff --git a/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.test.tsx b/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.test.tsx index 85ffaa5782..2bc770cc31 100644 --- a/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.test.tsx +++ b/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.test.tsx @@ -19,7 +19,7 @@ import { screen, fireEvent, waitFor } from '@testing-library/react'; import { analyticsApiRef } from '@backstage/core-plugin-api'; import { - MockAnalyticsApi, + mockApis, TestApiProvider, renderInTestApp, } from '@backstage/test-utils'; @@ -55,11 +55,11 @@ const defaultGitlabProps = { }; describe('FeedbackLink', () => { - const apiSpy = new MockAnalyticsApi(); + const analytics = mockApis.analytics(); it('Should open new Github issue tab', async () => { await renderInTestApp( - + , ); @@ -77,7 +77,7 @@ describe('FeedbackLink', () => { it('Should open new Gitlab issue tab', async () => { await renderInTestApp( - + , ); @@ -95,7 +95,7 @@ describe('FeedbackLink', () => { it('Should track click events', async () => { await renderInTestApp( - + , ); @@ -103,10 +103,12 @@ describe('FeedbackLink', () => { fireEvent.click(screen.getByText(/Open new Github issue/)); await waitFor(() => { - expect(apiSpy.getEvents()[0]).toMatchObject({ - action: 'click', - subject: 'Open new Github issue', - }); + expect(analytics.captureEvent).toHaveBeenCalledWith( + expect.objectContaining({ + action: 'click', + subject: 'Open new Github issue', + }), + ); }); }); }); diff --git a/plugins/techdocs-react/src/context.test.tsx b/plugins/techdocs-react/src/context.test.tsx index a923b3c528..6605d9ceaf 100644 --- a/plugins/techdocs-react/src/context.test.tsx +++ b/plugins/techdocs-react/src/context.test.tsx @@ -13,17 +13,14 @@ * 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'; import { ThemeProvider } from '@material-ui/core/styles'; import { lightTheme } from '@backstage/theme'; -import { - MockAnalyticsApi, - MockConfigApi, - TestApiProvider, -} from '@backstage/test-utils'; +import { mockApis, TestApiProvider } from '@backstage/test-utils'; import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; import { analyticsApiRef, @@ -65,7 +62,7 @@ const techdocsApiMock = { getTechDocsMetadata: jest.fn().mockResolvedValue(mockTechDocsMetadata), }; -const analyticsApiMock = new MockAnalyticsApi(); +const analyticsApiMock = mockApis.analytics(); const wrapper = ({ entityRef = { @@ -84,7 +81,7 @@ const wrapper = ({ @@ -169,12 +166,12 @@ describe('useTechDocsReaderPage', () => { wrapper, }); await waitFor(() => { - expect(analyticsApiMock.getEvents()[0]).toMatchObject({ + expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith({ action: 'action', subject: 'subject', - context: { + context: expect.objectContaining({ entityRef: 'component:default/test', - }, + }), }); }); }); diff --git a/plugins/techdocs/src/client.test.ts b/plugins/techdocs/src/client.test.ts index 62d497d790..f66587a071 100644 --- a/plugins/techdocs/src/client.test.ts +++ b/plugins/techdocs/src/client.test.ts @@ -15,10 +15,9 @@ */ 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,16 +33,13 @@ 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(), - } as unknown as jest.Mocked; + const identityApi = mockApis.identity(); const fetchApi = new MockFetchApi({ injectIdentityAuth: { identityApi } }); beforeEach(() => { jest.resetAllMocks(); - identityApi.getCredentials.mockResolvedValue({ token: undefined }); }); it('should return correct base url based on defined storage', async () => { @@ -95,7 +91,6 @@ describe('TechDocsStorageClient', () => { await Promise.resolve(); onmessage?.({ id: '', event: 'finish', data: '{"updated": false}' }); }); - identityApi.getCredentials.mockResolvedValue({}); await storageApi.syncEntityDocs(mockEntity); expect(mockFetchEventSource).toHaveBeenCalledWith( @@ -126,7 +121,6 @@ describe('TechDocsStorageClient', () => { onmessage?.({ id: '', event: 'finish', data: '{"updated": false}' }); }); - identityApi.getCredentials.mockResolvedValue({}); await expect(storageApi.syncEntityDocs(mockEntity)).resolves.toEqual( 'cached', ); @@ -146,7 +140,6 @@ describe('TechDocsStorageClient', () => { onmessage?.({ id: '', event: 'finish', data: '{"updated": true}' }); }); - identityApi.getCredentials.mockResolvedValue({}); await expect(storageApi.syncEntityDocs(mockEntity)).resolves.toEqual( 'updated', ); @@ -169,7 +162,6 @@ describe('TechDocsStorageClient', () => { onmessage?.({ id: '', event: 'finish', data: '{"updated": false}' }); }); - identityApi.getCredentials.mockResolvedValue({}); const logHandler = jest.fn(); await expect( storageApi.syncEntityDocs(mockEntity, logHandler), @@ -192,7 +184,6 @@ describe('TechDocsStorageClient', () => { }); // we await later after we emitted the error - identityApi.getCredentials.mockResolvedValue({}); const promise = storageApi.syncEntityDocs(mockEntity).then(); await expect(promise).rejects.toThrow(NotFoundError); @@ -207,7 +198,6 @@ describe('TechDocsStorageClient', () => { }); // we await later after we emitted the error - identityApi.getCredentials.mockResolvedValue({}); const promise = storageApi.syncEntityDocs(mockEntity).then(); mockFetchEventSource.mockImplementation(async (_url, options) => { diff --git a/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx b/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx index 513a56265c..4c3be25325 100644 --- a/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx +++ b/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx @@ -14,22 +14,18 @@ * limitations under the License. */ -import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; -import { - ConfigApi, - configApiRef, - storageApiRef, -} from '@backstage/core-plugin-api'; +import { ApiProvider } from '@backstage/core-app-api'; +import { configApiRef, storageApiRef } from '@backstage/core-plugin-api'; import { + MockStarredEntitiesApi, catalogApiRef, starredEntitiesApiRef, - MockStarredEntitiesApi, } from '@backstage/plugin-catalog-react'; import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; import { - MockStorageApi, - renderInTestApp, TestApiRegistry, + mockApis, + renderInTestApp, } from '@backstage/test-utils'; import { screen } from '@testing-library/react'; import React from 'react'; @@ -50,18 +46,14 @@ const mockCatalogApi = catalogApiMock({ }); describe('TechDocs Home', () => { - const configApi: ConfigApi = new ConfigReader({ - organization: { - name: 'My Company', - }, + const configApi = mockApis.config({ + data: { organization: { name: 'My Company' } }, }); - const storageApi = MockStorageApi.create(); - const apiRegistry = TestApiRegistry.from( [catalogApiRef, mockCatalogApi], [configApiRef, configApi], - [storageApiRef, storageApi], + [storageApiRef, mockApis.storage()], [starredEntitiesApiRef, new MockStarredEntitiesApi()], ); diff --git a/plugins/techdocs/src/home/components/Grids/EntityListDocsGrid.test.tsx b/plugins/techdocs/src/home/components/Grids/EntityListDocsGrid.test.tsx index 947c81ae85..25471d0769 100644 --- a/plugins/techdocs/src/home/components/Grids/EntityListDocsGrid.test.tsx +++ b/plugins/techdocs/src/home/components/Grids/EntityListDocsGrid.test.tsx @@ -14,12 +14,8 @@ * limitations under the License. */ -import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; -import { - ConfigApi, - configApiRef, - storageApiRef, -} from '@backstage/core-plugin-api'; +import { ApiProvider } from '@backstage/core-app-api'; +import { configApiRef, storageApiRef } from '@backstage/core-plugin-api'; import { catalogApiRef, starredEntitiesApiRef, @@ -30,9 +26,9 @@ import { catalogApiMock, } from '@backstage/plugin-catalog-react/testUtils'; import { - MockStorageApi, renderInTestApp, TestApiRegistry, + mockApis, } from '@backstage/test-utils'; import { screen } from '@testing-library/react'; import React from 'react'; @@ -68,21 +64,17 @@ const mockCatalogApi = catalogApiMock({ entities }); describe('Entity List Docs Grid', () => { beforeEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); - const configApi: ConfigApi = new ConfigReader({ - organization: { - name: 'My Company', - }, + const configApi = mockApis.config({ + data: { organization: { name: 'My Company' } }, }); - const storageApi = MockStorageApi.create(); - const apiRegistry = TestApiRegistry.from( [catalogApiRef, mockCatalogApi], [configApiRef, configApi], - [storageApiRef, storageApi], + [storageApiRef, mockApis.storage()], [starredEntitiesApiRef, new MockStarredEntitiesApi()], ); diff --git a/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx b/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx index 59c388df69..84c7275228 100644 --- a/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx @@ -16,12 +16,11 @@ import { TechDocsNotFound } from './TechDocsNotFound'; import React from 'react'; -import { render, screen, waitFor } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; import { - MockAnalyticsApi, + mockApis, TestApiProvider, renderInTestApp, - wrapInTestApp, } from '@backstage/test-utils'; import { analyticsApiRef } from '@backstage/core-plugin-api'; @@ -58,18 +57,16 @@ describe('', () => { }); it('should trigger analytics event not-found', async () => { - const mockAnalyticsApi = new MockAnalyticsApi(); + const mockAnalyticsApi = mockApis.analytics(); - render( - wrapInTestApp( - - - , - ), + await renderInTestApp( + + + , ); await waitFor(() => { - expect(mockAnalyticsApi.getEvents()[0]).toMatchObject({ + expect(mockAnalyticsApi.captureEvent).toHaveBeenCalledWith({ action: 'not-found', subject: '/the/pathname?the=search#the-anchor', attributes: { @@ -77,6 +74,7 @@ describe('', () => { namespace: 'namespace', kind: 'kind', }, + context: expect.anything(), }); }); }); diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx index 5e8cd7af27..afb57e22ba 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'; @@ -80,12 +81,6 @@ const techdocsStorageApiMock: jest.Mocked = { syncEntityDocs: jest.fn(), }; -const discoveryApiMock = { - getBaseUrl: jest - .fn() - .mockResolvedValue('https://localhost:7000/api/techdocs'), -}; - const fetchApiMock = { fetch: jest.fn().mockResolvedValue({ ok: true, @@ -106,10 +101,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 }) => { @@ -117,7 +110,7 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => { { 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/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.test.ts b/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.test.ts index 6bb0035b83..e54ac36624 100644 --- a/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.test.ts +++ b/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.test.ts @@ -18,10 +18,13 @@ import { DiscoveryApi, ErrorApi, FetchApi, - IdentityApi, StorageApi, } from '@backstage/core-plugin-api'; -import { MockFetchApi, registerMswTestHooks } from '@backstage/test-utils'; +import { + MockFetchApi, + mockApis, + registerMswTestHooks, +} from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { UserSettingsStorage } from './UserSettingsStorage'; @@ -35,13 +38,8 @@ describe('Persistent Storage API', () => { const mockDiscoveryApi = { getBaseUrl: async () => mockBaseUrl, }; - const mockIdentityApi: Partial = { - getCredentials: async () => ({ token: 'a-token' }), - }; - const mockIdentityApiFallback: Partial = { - // This API recreates the guest mode, where the WebStorage is used as fallback - getCredentials: async () => ({}), - }; + const mockIdentityApi = mockApis.identity({ token: 'a-token' }); + const mockIdentityApiFallback = mockApis.identity(); const createPersistentStorage = ( args?: Partial<{ @@ -55,7 +53,7 @@ describe('Persistent Storage API', () => { errorApi: mockErrorApi, fetchApi: new MockFetchApi(), discoveryApi: mockDiscoveryApi, - identityApi: mockIdentityApi as IdentityApi, + identityApi: mockIdentityApi, ...args, }); }; @@ -72,13 +70,13 @@ describe('Persistent Storage API', () => { errorApi: mockErrorApi, fetchApi: new MockFetchApi(), discoveryApi: mockDiscoveryApi, - identityApi: mockIdentityApiFallback as IdentityApi, + identityApi: mockIdentityApiFallback, ...args, }); }; afterEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); it('should return undefined for values which are unset', async () => { diff --git a/plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.test.tsx b/plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.test.tsx index 14b7b7b48b..8a2e882aa8 100644 --- a/plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.test.tsx +++ b/plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.test.tsx @@ -20,16 +20,15 @@ import { DefaultSettingsPage } from './DefaultSettingsPage'; import { UserSettingsTab } from '../UserSettingsTab'; import { useOutlet } from 'react-router-dom'; import { SettingsLayout } from '../SettingsLayout'; -import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useOutlet: jest.fn().mockReturnValue(undefined), })); -const catalogApiMock: jest.Mocked = { - getEntityByRef: jest.fn(), -} as any; +const catalogApi = catalogApiMock(); describe('', () => { beforeEach(() => { @@ -38,7 +37,7 @@ describe('', () => { it('should render the settings page with 3 tabs', async () => { const { container } = await renderInTestApp( - + , ); @@ -54,7 +53,7 @@ describe('', () => { ); const { container } = await renderInTestApp( - + , ); @@ -71,7 +70,7 @@ describe('', () => { ); const { container } = await renderInTestApp( - + , ); diff --git a/plugins/user-settings/src/components/General/UserSettingsIdentityCard.test.tsx b/plugins/user-settings/src/components/General/UserSettingsIdentityCard.test.tsx index b1a811d7fa..6fe0ee01d7 100644 --- a/plugins/user-settings/src/components/General/UserSettingsIdentityCard.test.tsx +++ b/plugins/user-settings/src/components/General/UserSettingsIdentityCard.test.tsx @@ -14,32 +14,28 @@ * limitations under the License. */ -import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; +import { + renderInTestApp, + TestApiRegistry, + mockApis, +} from '@backstage/test-utils'; import { screen } from '@testing-library/react'; import React from 'react'; import { UserSettingsIdentityCard } from './UserSettingsIdentityCard'; import { ApiProvider } from '@backstage/core-app-api'; import { identityApiRef } from '@backstage/core-plugin-api'; import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react'; +import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; const apiRegistry = TestApiRegistry.from( [ identityApiRef, - { - getProfileInfo: jest.fn(async () => ({})), - getBackstageIdentity: jest.fn(async () => ({ - type: 'user' as const, - userEntityRef: 'foo:bar/foobar', - ownershipEntityRefs: ['user:default/test-ownership'], - })), - }, - ], - [ - catalogApiRef, - { - getEntityByRef: jest.fn(), - }, + mockApis.identity({ + userEntityRef: 'foo:bar/foobar', + ownershipEntityRefs: ['user:default/test-ownership'], + }), ], + [catalogApiRef, catalogApiMock.mock()], ); describe('', () => { diff --git a/plugins/user-settings/src/components/General/UserSettingsMenu.test.tsx b/plugins/user-settings/src/components/General/UserSettingsMenu.test.tsx index c3b0aae4a7..0af33fac00 100644 --- a/plugins/user-settings/src/components/General/UserSettingsMenu.test.tsx +++ b/plugins/user-settings/src/components/General/UserSettingsMenu.test.tsx @@ -18,6 +18,7 @@ import { MockErrorApi, TestApiProvider, renderInTestApp, + mockApis, } from '@backstage/test-utils'; import { errorApiRef, identityApiRef } from '@backstage/core-plugin-api'; import { fireEvent, waitFor, screen } from '@testing-library/react'; @@ -35,9 +36,9 @@ describe('', () => { }); it('handles errors that occur when signing out', async () => { - const failingIdentityApi = { - signOut: jest.fn().mockRejectedValue(new Error('Logout error')), - }; + const failingIdentityApi = mockApis.identity.mock({ + signOut: () => Promise.reject(new Error('Logout error')), + }); const mockErrorApi = new MockErrorApi({ collect: true }); await renderInTestApp( ({})), - getBackstageIdentity: jest.fn(async () => ({ - type: 'user' as const, - userEntityRef: 'foo:bar/foobar', - ownershipEntityRefs: ['user:default/test-ownership'], - })), - }, - ], + [identityApiRef, mockApis.identity()], [ catalogApiRef, - { - getEntityByRef: jest.fn(async () => { - return { + catalogApiMock({ + entities: [ + { apiVersion: 'backstage.io/v1beta1', kind: 'User', metadata: { - name: 'Guest', + name: 'test', annotations: {}, }, spec: { @@ -50,9 +45,9 @@ const apiRegistry = TestApiRegistry.from( picture: 'https://example.com/avatar.png', }, }, - }; - }), - }, + }, + ], + }), ], ); diff --git a/plugins/user-settings/src/components/SettingsPage/SettingsPage.test.tsx b/plugins/user-settings/src/components/SettingsPage/SettingsPage.test.tsx index acdb6c3f87..12431a44a2 100644 --- a/plugins/user-settings/src/components/SettingsPage/SettingsPage.test.tsx +++ b/plugins/user-settings/src/components/SettingsPage/SettingsPage.test.tsx @@ -22,20 +22,15 @@ import { useOutlet } from 'react-router-dom'; import { SettingsLayout } from '../SettingsLayout'; import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { - CatalogApi, - catalogApiRef, - entityRouteRef, -} from '@backstage/plugin-catalog-react'; +import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react'; +import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useOutlet: jest.fn().mockReturnValue(undefined), })); -const catalogApiMock: jest.Mocked = { - getEntityByRef: jest.fn(), -} as any; +const catalogApi = catalogApiMock(); describe('', () => { beforeEach(() => { @@ -44,7 +39,7 @@ describe('', () => { it('should render the default settings page with 3 tabs', async () => { const { container } = await renderInTestApp( - + , { @@ -64,7 +59,7 @@ describe('', () => { ); (useOutlet as jest.Mock).mockReturnValue(advancedTabRoute); const { container } = await renderInTestApp( - + , { @@ -85,7 +80,7 @@ describe('', () => { ); (useOutlet as jest.Mock).mockReturnValue(advancedTabRoute); const { container } = await renderInTestApp( - + , { @@ -115,7 +110,7 @@ describe('', () => { ); (useOutlet as jest.Mock).mockReturnValue(customLayout); const { container } = await renderInTestApp( - + , { diff --git a/yarn.lock b/yarn.lock index 7d4e6a0452..75503208bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4699,7 +4699,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 @@ -8580,6 +8579,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 @@ -8590,11 +8590,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 @@ -17993,7 +17996,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: