core-plugin-api: further error handling improvements + fixes and tests for useTranslationRef
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -15,7 +15,11 @@
|
||||
*/
|
||||
|
||||
import React, { ReactNode } from 'react';
|
||||
import { TestApiProvider, withLogCollector } from '@backstage/test-utils';
|
||||
import {
|
||||
MockErrorApi,
|
||||
TestApiProvider,
|
||||
withLogCollector,
|
||||
} from '@backstage/test-utils';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { createTranslationRef } from './TranslationRef';
|
||||
import { useTranslationRef } from './useTranslationRef';
|
||||
@@ -28,6 +32,7 @@ import {
|
||||
TranslationApi,
|
||||
translationApiRef,
|
||||
} from '../alpha';
|
||||
import { ErrorApi, errorApiRef } from '../apis';
|
||||
|
||||
const plainRef = createTranslationRef({
|
||||
id: 'plain',
|
||||
@@ -37,10 +42,16 @@ const plainRef = createTranslationRef({
|
||||
},
|
||||
});
|
||||
|
||||
function makeWrapper(translationApi: TranslationApi) {
|
||||
function makeWrapper(
|
||||
translationApi: TranslationApi,
|
||||
errorApi: ErrorApi = { error$: jest.fn(), post: jest.fn() },
|
||||
) {
|
||||
return ({ children }: { children: ReactNode }) => (
|
||||
<TestApiProvider
|
||||
apis={[[translationApiRef, translationApi]]}
|
||||
apis={[
|
||||
[translationApiRef, translationApi],
|
||||
[errorApiRef, errorApi],
|
||||
]}
|
||||
children={children}
|
||||
/>
|
||||
);
|
||||
@@ -177,6 +188,7 @@ describe('useTranslationRef', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const errorApi = new MockErrorApi({ collect: true });
|
||||
const languageApi = AppLanguageSelector.create();
|
||||
const translationApi = I18nextTranslationApi.create({
|
||||
languageApi,
|
||||
@@ -193,21 +205,83 @@ describe('useTranslationRef', () => {
|
||||
});
|
||||
|
||||
const rendered1 = renderHook(() => useTranslationRef(ref), {
|
||||
wrapper: makeWrapper(translationApi),
|
||||
wrapper: makeWrapper(translationApi, errorApi),
|
||||
});
|
||||
const rendered2 = renderHook(() => useTranslationRef(ref), {
|
||||
wrapper: makeWrapper(translationApi),
|
||||
wrapper: makeWrapper(translationApi, errorApi),
|
||||
});
|
||||
|
||||
const { error } = await withLogCollector(['error'], async () => {
|
||||
await rendered2.waitForNextUpdate();
|
||||
});
|
||||
|
||||
expect(error).toEqual([
|
||||
"Failed to load translation resource 'test'; caused by Error: NOPE",
|
||||
const msg =
|
||||
"Failed to load translation resource 'test'; caused by Error: NOPE";
|
||||
expect(error).toEqual([msg]);
|
||||
expect(errorApi.getErrors()).toEqual([
|
||||
{
|
||||
error: new Error(msg),
|
||||
},
|
||||
]);
|
||||
|
||||
expect(rendered1.result.current.t('key')).toBe('default');
|
||||
expect(rendered2.result.current.t('key')).toBe('default');
|
||||
});
|
||||
|
||||
it('should log once and then ignore loading errors after initial load', async () => {
|
||||
const ref = createTranslationRef({
|
||||
id: 'test',
|
||||
messages: {
|
||||
key: 'default',
|
||||
},
|
||||
});
|
||||
|
||||
const errorApi = new MockErrorApi({ collect: true });
|
||||
const languageApi = AppLanguageSelector.create({
|
||||
availableLanguages: ['en', 'de'],
|
||||
});
|
||||
const translationApi = I18nextTranslationApi.create({
|
||||
languageApi,
|
||||
resources: [
|
||||
createTranslationResource({
|
||||
ref,
|
||||
translations: {
|
||||
de: async () => {
|
||||
throw new Error('NOPE');
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useTranslationRef(ref), {
|
||||
wrapper: makeWrapper(translationApi, errorApi),
|
||||
});
|
||||
|
||||
expect(result.current.t('key')).toBe('default');
|
||||
languageApi.setLanguage('de');
|
||||
|
||||
const { error } = await withLogCollector(['error'], async () => {
|
||||
const rendered1 = renderHook(() => useTranslationRef(ref), {
|
||||
wrapper: makeWrapper(translationApi, errorApi),
|
||||
});
|
||||
const rendered2 = renderHook(() => useTranslationRef(ref), {
|
||||
wrapper: makeWrapper(translationApi, errorApi),
|
||||
});
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve)); // Wait a long tick
|
||||
|
||||
expect(rendered1.result.current.t('key')).toBe('default');
|
||||
expect(rendered2.result.current.t('key')).toBe('default');
|
||||
});
|
||||
|
||||
const msg =
|
||||
"Failed to load translation resource 'test'; caused by Error: NOPE";
|
||||
expect(error).toEqual([msg]);
|
||||
expect(errorApi.getErrors()).toEqual([
|
||||
{
|
||||
error: new Error(msg),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,9 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useApi } from '../apis';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { errorApiRef, useApi } from '../apis';
|
||||
import {
|
||||
translationApiRef,
|
||||
TranslationFunction,
|
||||
@@ -33,14 +32,28 @@ export const useTranslationRef = <
|
||||
>(
|
||||
translationRef: TranslationRef<string, TMessages>,
|
||||
): { t: TranslationFunction<TMessages> } => {
|
||||
const errorApi = useApi(errorApiRef);
|
||||
const translationApi = useApi(translationApiRef);
|
||||
|
||||
const [error, setError] = useState<Error>();
|
||||
const [snapshot, setSnapshot] = useState<TranslationSnapshot<TMessages>>(() =>
|
||||
translationApi.getTranslation(translationRef),
|
||||
);
|
||||
const [observable] = useState(() =>
|
||||
translationApi.translation$(translationRef),
|
||||
const observable = useMemo(
|
||||
() => translationApi.translation$(translationRef),
|
||||
[translationApi, translationRef],
|
||||
);
|
||||
|
||||
const onError = useCallback(
|
||||
(error: Error) => {
|
||||
if (!loggedRefs.has(translationRef)) {
|
||||
const errMsg = `Failed to load translation resource '${translationRef.id}'; caused by ${error}`;
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(errMsg);
|
||||
errorApi.post(new Error(errMsg));
|
||||
loggedRefs.add(translationRef);
|
||||
}
|
||||
},
|
||||
[errorApi, translationRef],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -50,17 +63,15 @@ export const useTranslationRef = <
|
||||
setSnapshot(next);
|
||||
}
|
||||
},
|
||||
error: setError,
|
||||
error(error) {
|
||||
onError(error);
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [observable]);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
}, [observable, onError]);
|
||||
|
||||
if (!snapshot.ready) {
|
||||
throw new Promise<void>(resolve => {
|
||||
@@ -71,14 +82,9 @@ export const useTranslationRef = <
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
error(err) {
|
||||
if (!loggedRefs.has(translationRef)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`Failed to load translation resource '${translationRef.id}'; caused by ${err}`,
|
||||
);
|
||||
loggedRefs.add(translationRef);
|
||||
}
|
||||
error(error) {
|
||||
subscription.unsubscribe();
|
||||
onError(error);
|
||||
resolve();
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user