Merge pull request #27026 from backstage/freben/mock-apis-config

Implement the beginnings of `mockApis`
This commit is contained in:
Fredrik Adelöw
2024-10-11 11:08:54 +02:00
committed by GitHub
114 changed files with 2273 additions and 1031 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-test-utils': patch
---
Minor doc string changes
+14
View File
@@ -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.
@@ -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', () => {
<TestApiProvider
apis={[
[starredEntitiesApiRef, new MockStarredEntitiesApi()],
[permissionApiRef, mockPermissionApi],
[permissionApiRef, mockApis.permission()],
]}
>
<EntityProvider entity={entity}>
+59 -60
View File
@@ -150,7 +150,7 @@ export function mockErrorHandler(): ErrorRequestHandler<
Record<string, any>
>;
// @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".
@@ -128,7 +128,46 @@ function simpleMock<TService>(
}
/**
* 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 {
@@ -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,
@@ -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',
@@ -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']],
@@ -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,
@@ -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({
@@ -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,
@@ -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,
@@ -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 () => {
@@ -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(),
@@ -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,
@@ -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;
};
@@ -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],
);
@@ -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<AnalyticsApi> = {
captureEvent: jest.fn(),
};
const mockedAnalytics = mockApis.analytics();
beforeEach(() => {
jest.clearAllMocks();
@@ -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', () => {
@@ -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('<DismissableBanner />', () => {
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(
@@ -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('<Link />', () => {
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('<Link />', () => {
// 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('<Link />', () => {
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('<Link />', () => {
expect(customOnClick).toHaveBeenCalled();
// But there should be no analytics event.
expect(analyticsApi.getEvents()).toHaveLength(0);
expect(analyticsApi.captureEvent).not.toHaveBeenCalled();
});
});
@@ -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' }],
},
],
},
},
},
});
@@ -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' }),
],
]}
>
@@ -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(
@@ -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';
@@ -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']),
}),
);
@@ -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',
@@ -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',
@@ -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 <div />;
},
@@ -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' }),
]);
);
});
});
@@ -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<string>().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();
}
@@ -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",
+5 -10
View File
@@ -3,13 +3,12 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
/// <reference types="jest" />
/// <reference types="react" />
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<TApi> = {
factory: ApiFactory<TApi, TApi, {}>;
} & {
[Key in keyof TApi]: TApi[Key] extends (...args: infer Args) => infer Return
? TApi[Key] & jest.MockInstance<Return, Args>
: TApi[Key];
};
export { ApiMock };
// @public (undocumented)
export function createExtensionTester<T extends ExtensionDefinitionParameters>(
@@ -103,6 +96,8 @@ export class MockAnalyticsApi implements AnalyticsApi {
getEvents(): AnalyticsEvent[];
}
export { mockApis };
export { MockConfigApi };
export { MockErrorApi };
@@ -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';
+5
View File
@@ -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
}
+5 -5
View File
@@ -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)
```
+157 -14
View File
@@ -3,8 +3,11 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
/// <reference types="jest" />
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<TApi> = {
factory: ApiFactory<TApi, TApi, {}>;
} & {
[Key in keyof TApi]: TApi[Key] extends (...args: infer Args) => infer Return
? TApi[Key] & jest.MockInstance<Return, Args>
: TApi[Key];
};
// @public
export type AsyncLogCollector = () => Promise<void>;
@@ -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<AnalyticsApi, AnalyticsApi, {}>;
const // (undocumented)
mock: (
partialImpl?: Partial<AnalyticsApi> | undefined,
) => ApiMock<AnalyticsApi>;
}
export function config(options?: { data?: JsonObject }): ConfigApi;
export namespace config {
const factory: (
options?:
| {
data?: JsonObject | undefined;
}
| undefined,
) => ApiFactory<Config, Config, {}>;
const mock: (partialImpl?: Partial<Config> | undefined) => ApiMock<Config>;
}
export function discovery(options?: { baseUrl?: string }): DiscoveryApi;
export namespace discovery {
const // (undocumented)
factory: (
options?:
| {
baseUrl?: string | undefined;
}
| undefined,
) => ApiFactory<DiscoveryApi, DiscoveryApi, {}>;
const // (undocumented)
mock: (
partialImpl?: Partial<DiscoveryApi> | undefined,
) => ApiMock<DiscoveryApi>;
}
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<IdentityApi, IdentityApi, {}>;
const // (undocumented)
mock: (
partialImpl?: Partial<IdentityApi> | undefined,
) => ApiMock<IdentityApi>;
}
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<PermissionApi, PermissionApi, {}>;
const // (undocumented)
mock: (
partialImpl?: Partial<PermissionApi> | undefined,
) => ApiMock<PermissionApi>;
}
export function storage(options?: { data?: JsonObject }): StorageApi;
export namespace storage {
const // (undocumented)
factory: (
options?:
| {
data?: JsonObject | undefined;
}
| undefined,
) => ApiFactory<StorageApi, StorageApi, {}>;
const // (undocumented)
mock: (
partialImpl?: Partial<StorageApi> | undefined,
) => ApiMock<StorageApi>;
}
export function translation(): TranslationApi;
export namespace translation {
const // (undocumented)
factory: () => ApiFactory<TranslationApi, TranslationApi, {}>;
const // (undocumented)
mock: (
partialImpl?: Partial<TranslationApi> | undefined,
) => ApiMock<TranslationApi>;
}
}
// @public @deprecated
export function mockBreakpoint(options: { matches: boolean }): void;
// @public
// @public @deprecated
export class MockConfigApi implements ConfigApi {
constructor(data: JsonObject);
get<T = JsonValue>(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<EvaluatePermissionResponse>;
}
// @public
// @public @deprecated
export class MockStorageApi implements StorageApi {
// (undocumented)
static create(data?: MockStorageBucket): MockStorageApi;
@@ -177,7 +308,7 @@ export class MockStorageApi implements StorageApi {
snapshot<T extends JsonValue>(key: string): StorageValueSnapshot<T>;
}
// @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".
```
@@ -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[] = [];
@@ -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
@@ -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({
@@ -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 {
@@ -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';
@@ -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 {
@@ -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({
@@ -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';
@@ -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<void>(resolve => {
storage.observe$<typeof mockData>('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<void>(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<typeof data>('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<any>(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<any>('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<T extends JsonValue>(k: string): StorageValueSnapshot<T> {
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: '<div>' })).toBe('Foo <div>');
expect(
snapshot.t('foo', {
foo: '<div>',
interpolation: { escapeValue: true },
}),
).toBe('Foo &lt;div&gt;');
});
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);
});
});
});
@@ -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<TApi, TArgs extends unknown[]>(
ref: ApiRef<TApi>,
factory: (...args: TArgs) => TApi,
): (...args: TArgs) => ApiFactory<TApi, TApi, {}> {
return (...args) =>
createApiFactory({
api: ref,
deps: {},
factory: () => factory(...args),
});
}
/** @internal */
function simpleMock<TApi>(
ref: ApiRef<TApi>,
mockFactory: () => jest.Mocked<TApi>,
): (partialImpl?: Partial<TApi>) => ApiMock<TApi> {
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<TApi>;
};
}
/**
* 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<AnalyticsApi> => ({
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(
* <TestApiProvider apis={[[configApiRef, config]]}>
* <MyTestedComponent />
* </TestApiProvider>,
* );
* ```
*/
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/<pluginIs>`.
*
* @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<IdentityApi> => ({
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(),
}));
}
}
@@ -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}
@@ -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();
@@ -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', () => {
<TestApiProvider
apis={[
[fetchApiRef, fetchApiMock],
[storageApiRef, storageApiMock],
[storageApiRef, mockApis.storage()],
[discoveryApiRef, discoveryApiMock],
]}
>
@@ -76,7 +71,7 @@ describe('CookieAuthRefreshProvider', () => {
<TestApiProvider
apis={[
[fetchApiRef, fetchApiMock],
[storageApiRef, storageApiMock],
[storageApiRef, mockApis.storage()],
[discoveryApiRef, discoveryApiMock],
]}
>
@@ -107,7 +102,7 @@ describe('CookieAuthRefreshProvider', () => {
<TestApiProvider
apis={[
[fetchApiRef, fetchApiMock],
[storageApiRef, storageApiMock],
[storageApiRef, mockApis.storage()],
[discoveryApiRef, discoveryApiMock],
]}
>
@@ -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', () => {
<TestApiProvider
apis={[
[fetchApiRef, fetchApiMock],
[storageApiRef, storageApiMock],
[storageApiRef, mockApis.storage()],
[discoveryApiRef, discoveryApiMock],
]}
>
@@ -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' },
),
);
@@ -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('<CatalogGraphCard/>', () => {
],
}));
const analyticsSpy = new MockAnalyticsApi();
const analyticsApi = mockApis.analytics();
await renderInTestApp(
<TestApiProvider apis={[[analyticsApiRef, analyticsSpy]]}>
<TestApiProvider apis={[[analyticsApiRef, analyticsApi]]}>
{wrapper}
</TestApiProvider>,
{
@@ -228,12 +228,14 @@ describe('<CatalogGraphCard/>', () => {
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}',
},
}),
);
});
});
@@ -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('<CatalogGraphPage/>', () => {
}),
);
const analyticsSpy = new MockAnalyticsApi();
const analyticsApi = mockApis.analytics();
await renderInTestApp(
<TestApiProvider apis={[[analyticsApiRef, analyticsSpy]]}>
<TestApiProvider apis={[[analyticsApiRef, analyticsApi]]}>
{wrapper}
</TestApiProvider>,
{
@@ -243,10 +243,12 @@ describe.skip('<CatalogGraphPage/>', () => {
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('<CatalogGraphPage/>', () => {
}),
);
const analyticsSpy = new MockAnalyticsApi();
const analyticsApi = mockApis.analytics();
await renderInTestApp(
<TestApiProvider apis={[[analyticsApiRef, analyticsSpy]]}>
<TestApiProvider apis={[[analyticsApiRef, analyticsApi]]}>
{wrapper}
</TestApiProvider>,
{
@@ -274,12 +276,14 @@ describe.skip('<CatalogGraphPage/>', () => {
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',
},
}),
);
});
});
@@ -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('<PreviewCatalogInfoComponent />', () => {
@@ -122,10 +122,12 @@ describe('<PreviewCatalogInfoComponent />', () => {
apis={[
[
configApiRef,
new MockConfigApi({
catalog: {
import: {
entityFilename: 'anvil.yaml',
mockApis.config({
data: {
catalog: {
import: {
entityFilename: 'anvil.yaml',
},
},
},
}),
@@ -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('<StepPrepareCreatePullRequest />', () => {
post: jest.fn(),
};
const configApi = new MockConfigApi({});
const configApi = mockApis.config();
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
<TestApiProvider
@@ -21,7 +21,7 @@ import { MockStarredEntitiesApi, starredEntitiesApiRef } from '../../apis';
import { FavoriteEntity } from './FavoriteEntity';
import { ComponentEntity } from '@backstage/catalog-model';
import {
MockStorageApi,
mockApis,
renderInTestApp,
TestApiProvider,
} from '@backstage/test-utils';
@@ -41,14 +41,12 @@ const entity: ComponentEntity = {
},
};
const mockStorage = MockStorageApi.create();
describe('<FavoriteEntity/>', () => {
it('should add to favorites', async () => {
await renderInTestApp(
<TestApiProvider
apis={[
[storageApiRef, mockStorage],
[storageApiRef, mockApis.storage()],
[starredEntitiesApiRef, new MockStarredEntitiesApi()],
]}
>
@@ -79,7 +77,7 @@ describe('<FavoriteEntity/>', () => {
await renderInTestApp(
<TestApiProvider
apis={[
[storageApiRef, mockStorage],
[storageApiRef, mockApis.storage()],
[starredEntitiesApiRef, starredEntities],
]}
>
@@ -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<ConfigApi>;
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<jest.Mocked<IdentityApi>>;
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('<UserListPicker />', () => {
const mockQueryEntitiesImplementation: CatalogApi['queryEntities'] =
async request => {
@@ -133,19 +135,13 @@ describe('<UserListPicker />', () => {
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 () => {
@@ -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<any>) => {
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({
@@ -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');
});
});
@@ -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<ConfigApi>;
const ownershipEntityRefs = ['user:default/guest'];
const mockIdentityApi: Partial<IdentityApi> = {
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 =
<MemoryRouter initialEntries={[options.location ?? '']}>
<TestApiProvider
apis={[
[configApiRef, mockConfigApi],
[configApiRef, mockApis.config()],
[catalogApiRef, mockCatalogApi],
[identityApiRef, mockIdentityApi],
[storageApiRef, MockStorageApi.create()],
[storageApiRef, mockApis.storage()],
[starredEntitiesApiRef, new MockStarredEntitiesApi()],
[alertApiRef, { post: jest.fn() }],
[translationApiRef, MockTranslationApi.create()],
[translationApiRef, mockApis.translation()],
[errorApiRef, { error$: jest.fn(), post: jest.fn() }],
]}
>
@@ -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<Pick<IdentityApi, 'getBackstageIdentity'>>;
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 }) => (
<TestApiProvider apis={[[identityApiRef, identityApi]]}>
@@ -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,
});
@@ -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 });
@@ -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 () => {
@@ -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('<AboutCard />', () => {
const catalogApi = catalogApiMock.mock();
@@ -411,10 +411,6 @@ describe('<AboutCard />', () => {
},
};
mockAuthorize.mockImplementation(async () => ({
result: AuthorizeResult.ALLOW,
}));
await renderInTestApp(
<TestApiProvider
apis={[
@@ -423,7 +419,7 @@ describe('<AboutCard />', () => {
ScmIntegrationsApi.fromConfig(new ConfigReader({})),
],
[catalogApiRef, catalogApi],
[permissionApiRef, mockPermissionApi],
[permissionApiRef, mockApis.permission()],
]}
>
<EntityProvider entity={entity}>
@@ -466,10 +462,6 @@ describe('<AboutCard />', () => {
},
};
mockAuthorize.mockImplementation(async () => ({
result: AuthorizeResult.DENY,
}));
await renderInTestApp(
<TestApiProvider
apis={[
@@ -478,7 +470,10 @@ describe('<AboutCard />', () => {
ScmIntegrationsApi.fromConfig(new ConfigReader({})),
],
[catalogApiRef, catalogApi],
[permissionApiRef, mockPermissionApi],
[
permissionApiRef,
mockApis.permission({ authorize: AuthorizeResult.DENY }),
],
]}
>
<EntityProvider entity={entity}>
@@ -766,9 +761,6 @@ describe('<AboutCard />', () => {
namespace: 'default',
},
};
mockAuthorize.mockImplementation(async () => ({
result: AuthorizeResult.ALLOW,
}));
await renderInTestApp(
<TestApiProvider
apis={[
@@ -788,7 +780,7 @@ describe('<AboutCard />', () => {
),
],
[catalogApiRef, catalogApi],
[permissionApiRef, mockPermissionApi],
[permissionApiRef, mockApis.permission()],
]}
>
<EntityProvider entity={entity}>
@@ -819,9 +811,6 @@ describe('<AboutCard />', () => {
namespace: 'default',
},
};
mockAuthorize.mockImplementation(async () => ({
result: AuthorizeResult.DENY,
}));
await renderInTestApp(
<SWRConfig value={{ provider: () => new Map() }}>
<TestApiProvider
@@ -842,7 +831,10 @@ describe('<AboutCard />', () => {
),
],
[catalogApiRef, catalogApi],
[permissionApiRef, mockPermissionApi],
[
permissionApiRef,
mockApis.permission({ authorize: AuthorizeResult.DENY }),
],
]}
>
<EntityProvider entity={entity}>
@@ -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<ProfileInfo> = {
const identityApi = mockApis.identity({
userEntityRef: 'user:default/guest',
ownershipEntityRefs: ['user:default/guest', 'group:default/tools'],
displayName: 'Display Name',
};
const identityApi: Partial<IdentityApi> = {
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}
@@ -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(
<TestApiProvider apis={[[permissionApiRef, mockPermissionApi]]}>
<TestApiProvider apis={[[permissionApiRef, mockApis.permission()]]}>
<EntityProvider
entity={{ apiVersion: 'a', kind: 'b', metadata: { name: 'c' } }}
children={children}
@@ -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';
@@ -25,11 +25,9 @@ import { fireEvent, screen } from '@testing-library/react';
import * as React from 'react';
import { UnregisterEntity } from './UnregisterEntity';
const mockPermissionApi = new MockPermissionApi();
function render(children: React.ReactNode) {
return renderInTestApp(
<TestApiProvider apis={[[permissionApiRef, mockPermissionApi]]}>
<TestApiProvider apis={[[permissionApiRef, mockApis.permission()]]}>
<EntityProvider
entity={{ apiVersion: 'a', kind: 'b', metadata: { name: 'c' } }}
children={children}
@@ -32,7 +32,7 @@ import {
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
import { permissionApiRef } from '@backstage/plugin-permission-react';
import {
MockPermissionApi,
mockApis,
renderInTestApp,
TestApiProvider,
TestApiRegistry,
@@ -51,16 +51,16 @@ describe('EntityLayout', () => {
},
} 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(
<ApiProvider apis={mockApis}>
<ApiProvider apis={apis}>
<EntityProvider entity={mockEntity}>
<EntityLayout>
<EntityLayout.Route path="/" title="tabbed-test-title">
@@ -93,7 +93,7 @@ describe('EntityLayout', () => {
} as Entity;
await renderInTestApp(
<ApiProvider apis={mockApis}>
<ApiProvider apis={apis}>
<EntityProvider entity={mockEntityWithTitle}>
<EntityLayout>
<EntityLayout.Route path="/" title="tabbed-test-title">
@@ -117,7 +117,7 @@ describe('EntityLayout', () => {
it('renders default error message when entity is not found', async () => {
await renderInTestApp(
<ApiProvider apis={mockApis}>
<ApiProvider apis={apis}>
<AsyncEntityProvider loading={false}>
<EntityLayout>
<EntityLayout.Route path="/" title="tabbed-test-title">
@@ -142,7 +142,7 @@ describe('EntityLayout', () => {
it('renders custom message when entity is not found', async () => {
await renderInTestApp(
<ApiProvider apis={mockApis}>
<ApiProvider apis={apis}>
<AsyncEntityProvider loading={false}>
<EntityLayout
NotFoundComponent={<div>Oppps.. Your entity was not found</div>}
@@ -171,7 +171,7 @@ describe('EntityLayout', () => {
it('navigates when user clicks different tab', async () => {
await renderInTestApp(
<ApiProvider apis={mockApis}>
<ApiProvider apis={apis}>
<EntityProvider entity={mockEntity}>
<EntityLayout>
<EntityLayout.Route path="/" title="tabbed-test-title">
@@ -211,7 +211,7 @@ describe('EntityLayout', () => {
const shouldNotRenderTab = (e: Entity) => e.metadata.name === 'some-entity';
await renderInTestApp(
<ApiProvider apis={mockApis}>
<ApiProvider apis={apis}>
<EntityProvider entity={mockEntity}>
<EntityLayout>
<EntityLayout.Route path="/" title="tabbed-test-title">
@@ -254,7 +254,7 @@ describe('EntityLayout', () => {
relations: [{ type: 'ownedBy', targetRef: mockTargetRef }],
};
await renderInTestApp(
<ApiProvider apis={mockApis}>
<ApiProvider apis={apis}>
<EntityProvider entity={ownerEntity}>
<EntityLayout>
<EntityLayout.Route path="/" title="tabbed-test-title">
@@ -327,7 +327,7 @@ describe('EntityLayout - CleanUpAfterRemoval', () => {
[catalogApiRef, catalogApi],
[alertApiRef, alertApi],
[starredEntitiesApiRef, new MockStarredEntitiesApi()],
[permissionApiRef, new MockPermissionApi()],
[permissionApiRef, mockApis.permission()],
]}
>
<EntityProvider entity={entity}>
@@ -378,7 +378,7 @@ describe('EntityLayout - CleanUpAfterRemoval', () => {
[catalogApiRef, catalogApi],
[alertApiRef, alertApi],
[starredEntitiesApiRef, new MockStarredEntitiesApi()],
[permissionApiRef, new MockPermissionApi()],
[permissionApiRef, mockApis.permission()],
]}
>
<EntityProvider entity={entity}>
+10 -15
View File
@@ -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,
});
@@ -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', () => {
+37 -25
View File
@@ -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,
@@ -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('<Content kind="recent"/>', () => {
});
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('<Content kind="recent"/>', () => {
});
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('<Content kind="top"/>', () => {
});
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',
},
],
},
},
},
});
@@ -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<DiscoveryApi> = {
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(
@@ -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<DiscoveryApi> = {
getBaseUrl: () => Promise.resolve('http://localhost'),
};
const mockDiscoveryApi = mockApis.discovery();
await renderInTestApp(
<TestApiProvider apis={[[discoveryApiRef, mockDiscoveryApi]]}>
@@ -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()],
]}
>
<EntityProvider entity={groupA}>
@@ -212,7 +216,7 @@ describe('MemberTab Test', () => {
apis={[
[catalogApiRef, mockedCatalogApiSupportingGroups],
[starredEntitiesApiRef, mockedStarredEntitiesApi],
[permissionApiRef, {}],
[permissionApiRef, mockApis.permission()],
]}
>
<EntityProvider entity={groupA}>
@@ -238,7 +242,7 @@ describe('MemberTab Test', () => {
apis={[
[catalogApiRef, mockedCatalogApiSupportingGroups],
[starredEntitiesApiRef, mockedStarredEntitiesApi],
[permissionApiRef, {}],
[permissionApiRef, mockApis.permission()],
]}
>
<EntityProvider entity={groupA}>
@@ -273,7 +277,7 @@ describe('MemberTab Test', () => {
apis={[
[catalogApiRef, mockedCatalogApiSupportingGroups],
[starredEntitiesApiRef, mockedStarredEntitiesApi],
[permissionApiRef, {}],
[permissionApiRef, mockApis.permission()],
]}
>
<EntityProvider entity={groupA}>
@@ -308,7 +312,7 @@ describe('MemberTab Test', () => {
apis={[
[catalogApiRef, mockedCatalogApiSupportingGroups],
[starredEntitiesApiRef, mockedStarredEntitiesApi],
[permissionApiRef, {}],
[permissionApiRef, mockApis.permission()],
]}
>
<EntityProvider entity={groupA}>
@@ -366,7 +370,7 @@ describe('MemberTab Test', () => {
apis={[
[catalogApiRef, mockedCatalogApiSupportingGroups],
[starredEntitiesApiRef, mockedStarredEntitiesApi],
[permissionApiRef, {}],
[permissionApiRef, mockApis.permission()],
]}
>
<EntityProvider entity={groupA}>
@@ -406,7 +410,7 @@ describe('MemberTab Test', () => {
apis={[
[catalogApiRef, mockedCatalogApiSupportingGroups],
[starredEntitiesApiRef, mockedStarredEntitiesApi],
[permissionApiRef, {}],
[permissionApiRef, mockApis.permission()],
]}
>
<EntityProvider entity={groupA}>
@@ -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<IdentityApi> = {
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(
<TestApiProvider
@@ -59,13 +60,10 @@ describe('MyGroupsSidebarItem Test', () => {
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<IdentityApi> = {
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<IdentityApi> = {
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<IdentityApi> = {
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(
<TestApiProvider
@@ -233,13 +225,10 @@ describe('MyGroupsSidebarItem Test', () => {
describe('When an additional filter is provided', () => {
it('catalogApi.getEntities() should be called with an additional filter item', async () => {
const identityApi: Partial<IdentityApi> = {
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(
<TestApiProvider
@@ -21,7 +21,7 @@ import {
AuthorizeResult,
createPermission,
} from '@backstage/plugin-permission-common';
import { TestApiProvider } from '@backstage/test-utils';
import { TestApiProvider, mockApis } from '@backstage/test-utils';
import { PermissionApi, permissionApiRef } from '../apis';
import { SWRConfig } from 'swr';
@@ -52,36 +52,38 @@ function renderComponent(mockApi: PermissionApi) {
}
describe('usePermission', () => {
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();
});
});
@@ -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(),
}),
],
]}
@@ -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()],
]}
>
<TemplateCard template={mockTemplate} />
@@ -81,10 +81,10 @@ describe('TemplateCard', () => {
[
starredEntitiesApiRef,
new DefaultStarredEntitiesApi({
storageApi: MockStorageApi.create(),
storageApi: mockApis.storage(),
}),
],
[permissionApiRef, new MockPermissionApi()],
[permissionApiRef, mockApis.permission()],
]}
>
<TemplateCard template={mockTemplate} />
@@ -113,10 +113,10 @@ describe('TemplateCard', () => {
[
starredEntitiesApiRef,
new DefaultStarredEntitiesApi({
storageApi: MockStorageApi.create(),
storageApi: mockApis.storage(),
}),
],
[permissionApiRef, new MockPermissionApi()],
[permissionApiRef, mockApis.permission()],
]}
>
<TemplateCard template={mockTemplate} />
@@ -143,10 +143,10 @@ describe('TemplateCard', () => {
[
starredEntitiesApiRef,
new DefaultStarredEntitiesApi({
storageApi: MockStorageApi.create(),
storageApi: mockApis.storage(),
}),
],
[permissionApiRef, new MockPermissionApi()],
[permissionApiRef, mockApis.permission()],
]}
>
<TemplateCard template={mockTemplate} />
@@ -179,10 +179,10 @@ describe('TemplateCard', () => {
[
starredEntitiesApiRef,
new DefaultStarredEntitiesApi({
storageApi: MockStorageApi.create(),
storageApi: mockApis.storage(),
}),
],
[permissionApiRef, new MockPermissionApi()],
[permissionApiRef, mockApis.permission()],
]}
>
<TemplateCard template={mockTemplate} />
@@ -219,10 +219,10 @@ describe('TemplateCard', () => {
[
starredEntitiesApiRef,
new DefaultStarredEntitiesApi({
storageApi: MockStorageApi.create(),
storageApi: mockApis.storage(),
}),
],
[permissionApiRef, new MockPermissionApi()],
[permissionApiRef, mockApis.permission()],
]}
>
<TemplateCard template={mockTemplate} />
@@ -264,10 +264,10 @@ describe('TemplateCard', () => {
[
starredEntitiesApiRef,
new DefaultStarredEntitiesApi({
storageApi: MockStorageApi.create(),
storageApi: mockApis.storage(),
}),
],
[permissionApiRef, new MockPermissionApi()],
[permissionApiRef, mockApis.permission()],
]}
>
<TemplateCard template={mockTemplate} additionalLinks={[]} />
@@ -313,10 +313,10 @@ describe('TemplateCard', () => {
[
starredEntitiesApiRef,
new DefaultStarredEntitiesApi({
storageApi: MockStorageApi.create(),
storageApi: mockApis.storage(),
}),
],
[permissionApiRef, new MockPermissionApi()],
[permissionApiRef, mockApis.permission()],
]}
>
<TemplateCard template={mockTemplate} additionalLinks={[]} />
@@ -356,10 +356,10 @@ describe('TemplateCard', () => {
[
starredEntitiesApiRef,
new DefaultStarredEntitiesApi({
storageApi: MockStorageApi.create(),
storageApi: mockApis.storage(),
}),
],
[permissionApiRef, new MockPermissionApi()],
[permissionApiRef, mockApis.permission()],
]}
>
<TemplateCard template={mockTemplate} />
@@ -396,10 +396,10 @@ describe('TemplateCard', () => {
[
starredEntitiesApiRef,
new DefaultStarredEntitiesApi({
storageApi: MockStorageApi.create(),
storageApi: mockApis.storage(),
}),
],
[permissionApiRef, new MockPermissionApi()],
[permissionApiRef, mockApis.permission()],
]}
>
<TemplateCard template={mockTemplate} onSelected={mockOnSelected} />
@@ -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(
<SWRConfig value={{ provider: () => 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 }),
],
]}
>
<TemplateCard template={mockTemplate} onSelected={mockOnSelected} />
@@ -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<ScaffolderApi> = {
const catalogApi = catalogApiMock.mock();
const analyticsMock = new MockAnalyticsApi();
const analyticsMock = mockApis.analytics();
const apis = TestApiRegistry.from(
[scaffolderApiRef, scaffolderApiMock],
[catalogApiRef, catalogApi],
@@ -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()],
]}
>
<TemplateListPage />
@@ -74,10 +74,10 @@ describe('TemplateListPage', () => {
[
starredEntitiesApiRef,
new DefaultStarredEntitiesApi({
storageApi: MockStorageApi.create(),
storageApi: mockApis.storage(),
}),
],
[permissionApiRef, {}],
[permissionApiRef, mockApis.permission()],
]}
>
<TemplateListPage />
@@ -97,10 +97,10 @@ describe('TemplateListPage', () => {
[
starredEntitiesApiRef,
new DefaultStarredEntitiesApi({
storageApi: MockStorageApi.create(),
storageApi: mockApis.storage(),
}),
],
[permissionApiRef, {}],
[permissionApiRef, mockApis.permission()],
]}
>
<TemplateListPage />
@@ -119,10 +119,10 @@ describe('TemplateListPage', () => {
[
starredEntitiesApiRef,
new DefaultStarredEntitiesApi({
storageApi: MockStorageApi.create(),
storageApi: mockApis.storage(),
}),
],
[permissionApiRef, {}],
[permissionApiRef, mockApis.permission()],
]}
>
<TemplateListPage />
@@ -142,10 +142,10 @@ describe('TemplateListPage', () => {
[
starredEntitiesApiRef,
new DefaultStarredEntitiesApi({
storageApi: MockStorageApi.create(),
storageApi: mockApis.storage(),
}),
],
[permissionApiRef, {}],
[permissionApiRef, mockApis.permission()],
]}
>
<TemplateListPage />
@@ -164,10 +164,10 @@ describe('TemplateListPage', () => {
[
starredEntitiesApiRef,
new DefaultStarredEntitiesApi({
storageApi: MockStorageApi.create(),
storageApi: mockApis.storage(),
}),
],
[permissionApiRef, {}],
[permissionApiRef, mockApis.permission()],
]}
>
<TemplateListPage />
@@ -185,10 +185,10 @@ describe('TemplateListPage', () => {
[
starredEntitiesApiRef,
new DefaultStarredEntitiesApi({
storageApi: MockStorageApi.create(),
storageApi: mockApis.storage(),
}),
],
[permissionApiRef, {}],
[permissionApiRef, mockApis.permission()],
]}
>
<TemplateListPage
@@ -17,7 +17,7 @@
import { ApiProvider } from '@backstage/core-app-api';
import { analyticsApiRef } from '@backstage/core-plugin-api';
import {
MockAnalyticsApi,
mockApis,
renderInTestApp,
TestApiRegistry,
} from '@backstage/test-utils';
@@ -56,12 +56,12 @@ const scaffolderApiMock: jest.Mocked<ScaffolderApi> = {
};
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',
@@ -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('<ListTasksPage />', () => {
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<Required<ScaffolderApi>> = {
scaffold: jest.fn(),
@@ -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('<OwnerEntityColumn />', () => {
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 = {
@@ -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', () => {
<TestApiProvider
apis={[
[scaffolderApiRef, mockScaffolderApi],
[permissionApiRef, permissionApi || new MockPermissionApi()],
[permissionApiRef, permissionApi || mockApis.permission()],
]}
>
<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;
@@ -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('<MyGroupsPicker />', () => {
let entities: Entity[];
@@ -96,7 +83,7 @@ describe('<MyGroupsPicker />', () => {
});
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 () => {
@@ -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(
<TestApiProvider
@@ -296,7 +296,7 @@ describe('SearchBar', () => {
await waitFor(() => expect(textbox).toHaveValue(value));
expect(analyticsApiMock.getEvents()).toHaveLength(0);
expect(analyticsApiMock.captureEvent).not.toHaveBeenCalled();
});
it('Renders custom search icon', async () => {
@@ -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,
},
},
},
});
@@ -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,
},
},
},
});
@@ -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 (
<ApiProvider apis={mockApis}>
<ApiProvider apis={apis}>
<SearchContextProvider
initialState={{ ...emptySearchContext, ...overrides }}
>
@@ -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,
},
},
},
});
@@ -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 = [
@@ -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 = [
@@ -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 (
<TestApiProvider
apis={[
@@ -422,9 +422,7 @@ describe('SearchContext', () => {
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 (
<TestApiProvider
apis={[
@@ -481,9 +479,7 @@ describe('SearchContext', () => {
});
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 (
<TestApiProvider
apis={[
+4 -4
View File
@@ -23,7 +23,7 @@ import ListItemText from '@material-ui/core/ListItemText';
import {
renderInTestApp,
TestApiProvider,
MockAnalyticsApi,
mockApis,
} from '@backstage/test-utils';
import {
createPlugin,
@@ -38,7 +38,7 @@ import {
SearchResultListItemExtensionOptions,
} from './extensions';
const analyticsApiMock = new MockAnalyticsApi();
const analyticsApiMock = mockApis.analytics();
const results = [
{
@@ -118,7 +118,7 @@ describe('extensions', () => {
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' },
@@ -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,
},
},
},
});
@@ -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,
},
},
},
});
@@ -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,
},
},
},
});
+7 -13
View File
@@ -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(() => {
@@ -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(
<TestApiProvider apis={[[analyticsApiRef, apiSpy]]}>
<TestApiProvider apis={[[analyticsApiRef, analytics]]}>
<IssueLink {...defaultGithubProps} />
</TestApiProvider>,
);
@@ -77,7 +77,7 @@ describe('FeedbackLink', () => {
it('Should open new Gitlab issue tab', async () => {
await renderInTestApp(
<TestApiProvider apis={[[analyticsApiRef, apiSpy]]}>
<TestApiProvider apis={[[analyticsApiRef, analytics]]}>
<IssueLink {...defaultGitlabProps} />
</TestApiProvider>,
);
@@ -95,7 +95,7 @@ describe('FeedbackLink', () => {
it('Should track click events', async () => {
await renderInTestApp(
<TestApiProvider apis={[[analyticsApiRef, apiSpy]]}>
<TestApiProvider apis={[[analyticsApiRef, analytics]]}>
<IssueLink {...defaultGithubProps} />
</TestApiProvider>,
);
@@ -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',
}),
);
});
});
});

Some files were not shown because too many files have changed in this diff Show More