implement translation too

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2024-10-09 14:34:58 +02:00
parent 7d06a43916
commit d081861694
10 changed files with 301 additions and 62 deletions
+3
View File
@@ -5,7 +5,10 @@
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.
+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)
```
+31 -23
View File
@@ -41,6 +41,7 @@ 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> = {
@@ -81,8 +82,6 @@ export type LogCollector = AsyncLogCollector | SyncLogCollector;
// @public
export type LogFuncs = 'log' | 'warn' | 'error';
// Warning: (ae-unresolved-link) The @link reference could not be resolved: The reference is ambiguous because "analytics" has more than one declaration; you need to add a TSDoc member reference selector
//
// @public @deprecated
export class MockAnalyticsApi implements AnalyticsApi {
// (undocumented)
@@ -193,6 +192,17 @@ export namespace mockApis {
partialImpl?: Partial<StorageApi> | undefined,
) => ApiMock<StorageApi>;
}
// (undocumented)
export function translation(): jest.Mocked<TranslationApi>;
// (undocumented)
export namespace translation {
const // (undocumented)
factory: () => ApiFactory<TranslationApi, TranslationApi, {}>;
const // (undocumented)
mock: (
partialImpl?: Partial<TranslationApi> | undefined,
) => ApiMock<TranslationApi>;
}
}
// @public @deprecated
@@ -264,8 +274,6 @@ export interface MockFetchApiOptions {
};
}
// Warning: (ae-unresolved-link) The @link reference could not be resolved: No member was found with name "permissions"
//
// @public @deprecated
export class MockPermissionApi implements PermissionApi {
constructor(
@@ -279,8 +287,6 @@ export class MockPermissionApi implements PermissionApi {
): Promise<EvaluatePermissionResponse>;
}
// Warning: (ae-unresolved-link) The @link reference could not be resolved: The reference is ambiguous because "storage" has more than one declaration; you need to add a TSDoc member reference selector
//
// @public @deprecated
export class MockStorageApi implements StorageApi {
// (undocumented)
@@ -299,8 +305,6 @@ export class MockStorageApi implements StorageApi {
snapshot<T extends JsonValue>(key: string): StorageValueSnapshot<T>;
}
// Warning: (ae-unresolved-link) The @link reference could not be resolved: The reference is ambiguous because "storage" has more than one declaration; you need to add a TSDoc member reference selector
//
// @public @deprecated
export type MockStorageBucket = {
[key: string]: any;
@@ -413,20 +417,24 @@ export function wrapInTestApp(
// 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:46:5 - (ae-undocumented) Missing documentation for "analytics".
// src/testUtils/apis/mockApis.d.ts:47:5 - (ae-undocumented) Missing documentation for "analytics".
// src/testUtils/apis/mockApis.d.ts:48:15 - (ae-undocumented) Missing documentation for "factory".
// src/testUtils/apis/mockApis.d.ts:49:15 - (ae-undocumented) Missing documentation for "mock".
// src/testUtils/apis/mockApis.d.ts:100:5 - (ae-undocumented) Missing documentation for "identity".
// src/testUtils/apis/mockApis.d.ts:108:5 - (ae-undocumented) Missing documentation for "identity".
// src/testUtils/apis/mockApis.d.ts:109:15 - (ae-undocumented) Missing documentation for "factory".
// src/testUtils/apis/mockApis.d.ts:117:15 - (ae-undocumented) Missing documentation for "mock".
// src/testUtils/apis/mockApis.d.ts:119:5 - (ae-undocumented) Missing documentation for "permission".
// src/testUtils/apis/mockApis.d.ts:122:5 - (ae-undocumented) Missing documentation for "permission".
// src/testUtils/apis/mockApis.d.ts:123:15 - (ae-undocumented) Missing documentation for "factory".
// src/testUtils/apis/mockApis.d.ts:126:15 - (ae-undocumented) Missing documentation for "mock".
// src/testUtils/apis/mockApis.d.ts:128:5 - (ae-undocumented) Missing documentation for "storage".
// src/testUtils/apis/mockApis.d.ts:131:5 - (ae-undocumented) Missing documentation for "storage".
// src/testUtils/apis/mockApis.d.ts:132:15 - (ae-undocumented) Missing documentation for "factory".
// src/testUtils/apis/mockApis.d.ts:135:15 - (ae-undocumented) Missing documentation for "mock".
// src/testUtils/apis/mockApis.d.ts:48:5 - (ae-undocumented) Missing documentation for "analytics".
// src/testUtils/apis/mockApis.d.ts:49:15 - (ae-undocumented) Missing documentation for "factory".
// src/testUtils/apis/mockApis.d.ts:50:15 - (ae-undocumented) Missing documentation for "mock".
// src/testUtils/apis/mockApis.d.ts:101:5 - (ae-undocumented) Missing documentation for "identity".
// src/testUtils/apis/mockApis.d.ts:109:5 - (ae-undocumented) Missing documentation for "identity".
// src/testUtils/apis/mockApis.d.ts:110:15 - (ae-undocumented) Missing documentation for "factory".
// src/testUtils/apis/mockApis.d.ts:118:15 - (ae-undocumented) Missing documentation for "mock".
// src/testUtils/apis/mockApis.d.ts:120:5 - (ae-undocumented) Missing documentation for "permission".
// src/testUtils/apis/mockApis.d.ts:123:5 - (ae-undocumented) Missing documentation for "permission".
// src/testUtils/apis/mockApis.d.ts:124:15 - (ae-undocumented) Missing documentation for "factory".
// src/testUtils/apis/mockApis.d.ts:127:15 - (ae-undocumented) Missing documentation for "mock".
// src/testUtils/apis/mockApis.d.ts:129:5 - (ae-undocumented) Missing documentation for "storage".
// src/testUtils/apis/mockApis.d.ts:132:5 - (ae-undocumented) Missing documentation for "storage".
// src/testUtils/apis/mockApis.d.ts:133:15 - (ae-undocumented) Missing documentation for "factory".
// src/testUtils/apis/mockApis.d.ts:136:15 - (ae-undocumented) Missing documentation for "mock".
// src/testUtils/apis/mockApis.d.ts:138:5 - (ae-undocumented) Missing documentation for "translation".
// src/testUtils/apis/mockApis.d.ts:139:5 - (ae-undocumented) Missing documentation for "translation".
// src/testUtils/apis/mockApis.d.ts:140:15 - (ae-undocumented) Missing documentation for "factory".
// src/testUtils/apis/mockApis.d.ts:141:15 - (ae-undocumented) Missing documentation for "mock".
```
@@ -21,7 +21,7 @@ import { AnalyticsApi, AnalyticsEvent } from '@backstage/core-plugin-api';
* Use getEvents in tests to verify captured events.
*
* @public
* @deprecated Use {@link mockApis.analytics} instead
* @deprecated Use {@link @backstage/test-utils#mockApis.(analytics:namespace)} instead
*/
export class MockAnalyticsApi implements AnalyticsApi {
private events: AnalyticsEvent[] = [];
@@ -26,7 +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 mockApis.permissions} instead
* @deprecated Use {@link @backstage/test-utils#mockApis.(permission:namespace)} instead
* @public
*/
export class MockPermissionApi implements PermissionApi {
@@ -20,14 +20,14 @@ import ObservableImpl from 'zen-observable';
/**
* Type for map holding data in {@link MockStorageApi}
* @deprecated Use {@link mockApis.storage} instead
* @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 mockApis.storage} instead
* @deprecated Use {@link @backstage/test-utils#mockApis.(storage:namespace)} instead
* @public
*/
export class MockStorageApi implements 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({
@@ -19,6 +19,9 @@ import {
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', () => {
@@ -537,6 +540,224 @@ describe('mockApis', () => {
expect(notEmpty.forBucket).toHaveBeenCalledTimes(1);
});
it('can create a mock and make assertions on it', () => {});
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 and make assertions on it', () => {
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');
expect(translation.getTranslation).toHaveBeenCalledTimes(1);
});
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);
});
});
});
@@ -40,6 +40,11 @@ import { JsonObject } from '@backstage/types';
import { ApiMock } from './ApiMock';
import { MockPermissionApi } from './PermissionApi';
import { MockStorageApi } from './StorageApi';
import {
TranslationApi,
translationApiRef,
} from '@backstage/core-plugin-api/alpha';
import { MockTranslationApi } from './TranslationApi';
/** @internal */
function simpleFactory<TApi, TArgs extends unknown[]>(
@@ -208,7 +213,7 @@ export namespace mockApis {
email?: string;
displayName?: string;
picture?: string;
}) {
}): IdentityApi {
const {
userEntityRef = 'user:default/test',
ownershipEntityRefs = ['user:default/test'],
@@ -217,22 +222,18 @@ export namespace mockApis {
displayName,
picture,
} = options ?? {};
return simpleInstance(
identityApiRef,
{
async getBackstageIdentity() {
return { type: 'user', ownershipEntityRefs, userEntityRef };
},
async getCredentials() {
return { token };
},
async getProfileInfo() {
return { email, displayName, picture };
},
async signOut() {},
return {
async getBackstageIdentity() {
return { type: 'user', ownershipEntityRefs, userEntityRef };
},
identityMockSkeleton,
);
async getCredentials() {
return { token };
},
async getProfileInfo() {
return { email, displayName, picture };
},
async signOut() {},
};
}
export namespace identity {
export const factory = simpleFactory(identityApiRef, identity);
@@ -261,11 +262,7 @@ export namespace mockApis {
} else {
authorize = () => authorizeInput;
}
return simpleInstance(
permissionApiRef,
new MockPermissionApi(authorize),
permissionMockSkeleton,
);
return new MockPermissionApi(authorize);
}
export namespace permission {
export const factory = simpleFactory(permissionApiRef, permission);
@@ -280,14 +277,22 @@ export namespace mockApis {
snapshot: jest.fn(),
});
export function storage(options?: { data?: JsonObject }) {
return simpleInstance(
storageApiRef,
MockStorageApi.create(options?.data),
storageMockSkeleton,
);
return MockStorageApi.create(options?.data);
}
export namespace storage {
export const factory = simpleFactory(storageApiRef, storage);
export const mock = simpleMock(storageApiRef, storageMockSkeleton);
}
const translationMockSkeleton = (): jest.Mocked<TranslationApi> => ({
getTranslation: jest.fn(),
translation$: jest.fn(),
});
export function translation() {
return MockTranslationApi.create();
}
export namespace translation {
export const factory = simpleFactory(translationApiRef, translation);
export const mock = simpleMock(translationApiRef, translationMockSkeleton);
}
}
@@ -39,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[] = [
@@ -104,7 +103,7 @@ const createWrapper =
[storageApiRef, mockApis.storage()],
[starredEntitiesApiRef, new MockStarredEntitiesApi()],
[alertApiRef, { post: jest.fn() }],
[translationApiRef, MockTranslationApi.create()],
[translationApiRef, mockApis.translation()],
[errorApiRef, { error$: jest.fn(), post: jest.fn() }],
]}
>