Merge pull request #27026 from backstage/freben/mock-apis-config
Implement the beginnings of `mockApis`
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
```
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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 { ApiFactory } from '@backstage/core-plugin-api';
|
||||
|
||||
/**
|
||||
* Represents a mocked version of an API, where you automatically have access to
|
||||
* the mocked versions of all of its methods along with a factory that returns
|
||||
* that same mock.
|
||||
*
|
||||
* @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];
|
||||
};
|
||||
@@ -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 <div>');
|
||||
});
|
||||
|
||||
it('should support nesting', () => {
|
||||
const snapshot = snapshotWithMessages({
|
||||
foo: 'Foo $t(bar) $t(baz)',
|
||||
bar: 'Nested',
|
||||
baz: 'Baz {{ qux }}',
|
||||
});
|
||||
|
||||
expect(snapshot.t('foo', { qux: 'Deep' })).toBe('Foo Nested Baz Deep');
|
||||
});
|
||||
|
||||
it('should support formatting', () => {
|
||||
const snapshot = snapshotWithMessages({
|
||||
plain: '= {{ x }}',
|
||||
number: '= {{ x, number }}',
|
||||
numberFixed: '= {{ x, number(minimumFractionDigits: 2) }}',
|
||||
relativeTime: '= {{ x, relativeTime }}',
|
||||
relativeSeconds: '= {{ x, relativeTime(second) }}',
|
||||
relativeSecondsShort:
|
||||
'= {{ x, relativeTime(range: second; style: short) }}',
|
||||
list: '= {{ x, list }}',
|
||||
});
|
||||
|
||||
expect(snapshot.t('plain', { x: '5' })).toBe('= 5');
|
||||
expect(snapshot.t('number', { x: 5 })).toBe('= 5');
|
||||
expect(
|
||||
snapshot.t('number', {
|
||||
x: 5,
|
||||
formatParams: { x: { minimumFractionDigits: 1 } },
|
||||
}),
|
||||
).toBe('= 5.0');
|
||||
expect(snapshot.t('numberFixed', { x: 5 })).toBe('= 5.00');
|
||||
expect(
|
||||
snapshot.t('numberFixed', {
|
||||
x: 5,
|
||||
formatParams: { x: { minimumFractionDigits: 3 } },
|
||||
}),
|
||||
).toBe('= 5.000');
|
||||
expect(snapshot.t('relativeTime', { x: 3 })).toBe('= in 3 days');
|
||||
expect(snapshot.t('relativeTime', { x: -3 })).toBe('= 3 days ago');
|
||||
expect(
|
||||
snapshot.t('relativeTime', {
|
||||
x: 15,
|
||||
formatParams: { x: { range: 'weeks' } },
|
||||
}),
|
||||
).toBe('= in 15 weeks');
|
||||
expect(
|
||||
snapshot.t('relativeTime', {
|
||||
x: 15,
|
||||
formatParams: { x: { range: 'weeks', style: 'short' } },
|
||||
}),
|
||||
).toBe('= in 15 wk.');
|
||||
expect(snapshot.t('relativeSeconds', { x: 1 })).toBe('= in 1 second');
|
||||
expect(snapshot.t('relativeSeconds', { x: 2 })).toBe('= in 2 seconds');
|
||||
expect(snapshot.t('relativeSeconds', { x: -3 })).toBe(
|
||||
'= 3 seconds ago',
|
||||
);
|
||||
expect(snapshot.t('relativeSeconds', { x: 0 })).toBe('= in 0 seconds');
|
||||
expect(snapshot.t('relativeSecondsShort', { x: 1 })).toBe(
|
||||
'= in 1 sec.',
|
||||
);
|
||||
expect(snapshot.t('relativeSecondsShort', { x: 2 })).toBe(
|
||||
'= in 2 sec.',
|
||||
);
|
||||
expect(snapshot.t('relativeSecondsShort', { x: -3 })).toBe(
|
||||
'= 3 sec. ago',
|
||||
);
|
||||
expect(snapshot.t('relativeSecondsShort', { x: 0 })).toBe(
|
||||
'= in 0 sec.',
|
||||
);
|
||||
expect(snapshot.t('list', { x: ['a'] })).toBe('= a');
|
||||
expect(snapshot.t('list', { x: ['a', 'b'] })).toBe('= a and b');
|
||||
expect(snapshot.t('list', { x: ['a', 'b', 'c'] })).toBe(
|
||||
'= a, b, and c',
|
||||
);
|
||||
});
|
||||
|
||||
it('should support plurals', () => {
|
||||
const snapshot = snapshotWithMessages({
|
||||
derp_one: 'derp',
|
||||
derp_other: 'derps',
|
||||
derpWithCount_one: '{{ count }} derp',
|
||||
derpWithCount_other: '{{ count }} derps',
|
||||
});
|
||||
|
||||
expect(snapshot.t('derp', { count: 1 })).toBe('derp');
|
||||
expect(snapshot.t('derp', { count: 2 })).toBe('derps');
|
||||
expect(snapshot.t('derp', { count: 0 })).toBe('derps');
|
||||
expect(snapshot.t('derpWithCount', { count: 1 })).toBe('1 derp');
|
||||
expect(snapshot.t('derpWithCount', { count: 2 })).toBe('2 derps');
|
||||
expect(snapshot.t('derpWithCount', { count: 0 })).toBe('0 derps');
|
||||
});
|
||||
});
|
||||
|
||||
it('can create an instance', () => {
|
||||
const translation = mockApis.translation();
|
||||
const ref = createTranslationRef({
|
||||
id: 'test',
|
||||
messages: { a: 'b' },
|
||||
});
|
||||
const result = translation.getTranslation(ref);
|
||||
if (!result.ready) {
|
||||
throw new Error('not ready');
|
||||
}
|
||||
expect(result.t('a')).toEqual('b');
|
||||
});
|
||||
|
||||
it('can create a mock and make assertions on it', () => {
|
||||
const ref = createTranslationRef({
|
||||
id: 'test',
|
||||
messages: { a: 'b' },
|
||||
});
|
||||
|
||||
const empty = mockApis.translation.mock();
|
||||
expect(empty.getTranslation(ref)).toBeUndefined();
|
||||
|
||||
const notEmpty = mockApis.translation.mock({
|
||||
getTranslation: () =>
|
||||
({
|
||||
ready: true,
|
||||
t: () => 'b',
|
||||
} as any),
|
||||
});
|
||||
const result = notEmpty.getTranslation(ref);
|
||||
if (!result.ready) {
|
||||
throw new Error('not ready');
|
||||
}
|
||||
expect(result.t('a')).toEqual('b');
|
||||
expect(notEmpty.getTranslation).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user