@openmrs/esm-implementer-tools-app 11.0.1-pre.6014 → 11.0.1-pre.6015
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -4
- package/dist/1558.css +1 -0
- package/dist/{8088.css.map → 1558.css.map} +1 -1
- package/dist/1558.js +1 -0
- package/dist/1558.js.map +1 -0
- package/dist/1944.js +1 -0
- package/dist/1944.js.map +1 -0
- package/dist/{6479.css.map → 2989.css.map} +1 -1
- package/dist/2989.js +1 -0
- package/dist/2989.js.map +1 -0
- package/dist/4540.js +1 -0
- package/dist/4540.js.map +1 -0
- package/dist/{4698.js → 4938.js} +1 -1
- package/dist/4938.js.map +1 -0
- package/dist/5117.js +1 -1
- package/dist/5117.js.map +1 -1
- package/dist/5822.js +1 -0
- package/dist/5822.js.map +1 -0
- package/dist/6977.js +15 -0
- package/dist/6977.js.map +1 -0
- package/dist/7218.js +1 -0
- package/dist/7218.js.map +1 -0
- package/dist/8978.js +1 -0
- package/dist/8978.js.map +1 -0
- package/dist/932.js +1 -1
- package/dist/932.js.map +1 -1
- package/dist/openmrs-esm-implementer-tools-app.js +2 -2
- package/dist/openmrs-esm-implementer-tools-app.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +3 -3
- package/src/implementer-tools.test.tsx +38 -2
- package/src/popup/popup.component.tsx +3 -0
- package/src/popup/popup.test.tsx +2 -1
- package/src/store.ts +30 -1
- package/src/translations/translation-catalog.ts +14 -4
- package/src/translations/translations.component.tsx +696 -0
- package/src/translations/translations.styles.scss +133 -0
- package/src/translations/translations.test.tsx +839 -0
- package/translations/en.json +34 -0
- package/dist/1492.js +0 -1
- package/dist/1492.js.map +0 -1
- package/dist/1695.js +0 -1
- package/dist/1695.js.map +0 -1
- package/dist/195.js +0 -15
- package/dist/195.js.map +0 -1
- package/dist/2536.js +0 -1
- package/dist/2536.js.map +0 -1
- package/dist/4698.js.map +0 -1
- package/dist/6479.js +0 -1
- package/dist/6479.js.map +0 -1
- package/dist/8088.css +0 -1
- package/dist/8088.js +0 -1
- package/dist/8088.js.map +0 -1
- package/dist/9270.js +0 -1
- package/dist/9270.js.map +0 -1
- /package/dist/{6479.css → 2989.css} +0 -0
|
@@ -0,0 +1,839 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import '@testing-library/jest-dom/vitest';
|
|
4
|
+
import { act, fireEvent, render, screen, within } from '@testing-library/react';
|
|
5
|
+
import userEvent from '@testing-library/user-event';
|
|
6
|
+
import { useSession } from '@openmrs/esm-framework';
|
|
7
|
+
import { implementerToolsConfigStore, temporaryConfigStore } from '@openmrs/esm-framework/src/internal';
|
|
8
|
+
import { implementerToolsStore } from '../store';
|
|
9
|
+
import { Translations } from './translations.component';
|
|
10
|
+
|
|
11
|
+
const mockUseSession = vi.mocked(useSession);
|
|
12
|
+
|
|
13
|
+
// Without an i18next instance, react-i18next's `t` returns the default value uninterpolated, which
|
|
14
|
+
// would leave labels such as "{{module}} (not loaded)" unmatchable.
|
|
15
|
+
const mockT = (key: string, defaultValue?: string, options?: Record<string, unknown>) =>
|
|
16
|
+
(defaultValue ?? key).replace(/{{\s*(\w+)\s*}}/g, (match, name) => String(options?.[name] ?? match));
|
|
17
|
+
|
|
18
|
+
vi.mock('react-i18next', async (importOriginal) => ({
|
|
19
|
+
...(await importOriginal<typeof import('react-i18next')>()),
|
|
20
|
+
useTranslation: () => ({ t: mockT }),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
// i18next hands load failures to the callback, never to the returned promise, so these mocks honour
|
|
24
|
+
// the callback contract the production code depends on.
|
|
25
|
+
const mockReloadResources = vi.fn((_lngs: unknown, _ns: unknown, done?: (error?: unknown) => void) => done?.());
|
|
26
|
+
const mockHasResourceBundle = vi.fn(() => true);
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
window.i18next = {
|
|
30
|
+
...window.i18next,
|
|
31
|
+
language: 'en',
|
|
32
|
+
languages: ['en'],
|
|
33
|
+
store: {
|
|
34
|
+
data: {
|
|
35
|
+
en: {
|
|
36
|
+
'@openmrs/esm-login-app': {
|
|
37
|
+
loginButton: 'Log in',
|
|
38
|
+
locationsHeader: 'Locations',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
reloadResources: mockReloadResources,
|
|
44
|
+
loadLanguages: vi.fn(),
|
|
45
|
+
hasResourceBundle: mockHasResourceBundle,
|
|
46
|
+
getResourceBundle: vi.fn(() => ({})),
|
|
47
|
+
removeResourceBundle: vi.fn(),
|
|
48
|
+
addResourceBundle: vi.fn(),
|
|
49
|
+
options: { ns: [] },
|
|
50
|
+
services: {
|
|
51
|
+
languageUtils: {
|
|
52
|
+
toResolveHierarchy: (lng: string) => (lng === 'en' ? ['en'] : [lng, 'en']),
|
|
53
|
+
},
|
|
54
|
+
pluralResolver: {
|
|
55
|
+
getSuffixes: (lng: string) =>
|
|
56
|
+
new Intl.PluralRules(lng).resolvedOptions().pluralCategories.map((category) => `_${category}`),
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
emit: vi.fn(),
|
|
60
|
+
} as unknown as typeof window.i18next;
|
|
61
|
+
|
|
62
|
+
window.installedModules = [['@openmrs/esm-login-app', {}]] as unknown as typeof window.installedModules;
|
|
63
|
+
|
|
64
|
+
mockUseSession.mockReturnValue({ authenticated: true, sessionId: '' });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
implementerToolsConfigStore.setState({ config: {} });
|
|
69
|
+
temporaryConfigStore.setState({ config: {} });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('Translations', () => {
|
|
73
|
+
it('lists the keys and values of the loaded namespaces', () => {
|
|
74
|
+
render(<Translations />);
|
|
75
|
+
|
|
76
|
+
expect(screen.getByText('loginButton')).toBeInTheDocument();
|
|
77
|
+
expect(screen.getByText('Log in')).toBeInTheDocument();
|
|
78
|
+
expect(screen.getByText('locationsHeader')).toBeInTheDocument();
|
|
79
|
+
expect(screen.getAllByText('@openmrs/esm-login-app').length).toBeGreaterThan(0);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('searches by key and by the translated text', async () => {
|
|
83
|
+
const user = userEvent.setup();
|
|
84
|
+
render(<Translations />);
|
|
85
|
+
|
|
86
|
+
const search = screen.getByRole('textbox', { name: /search translations/i });
|
|
87
|
+
|
|
88
|
+
await user.type(search, 'Locations');
|
|
89
|
+
|
|
90
|
+
expect(screen.getByText('locationsHeader')).toBeInTheDocument();
|
|
91
|
+
expect(screen.queryByText('loginButton')).not.toBeInTheDocument();
|
|
92
|
+
|
|
93
|
+
await user.clear(search);
|
|
94
|
+
await user.type(search, 'loginBut');
|
|
95
|
+
|
|
96
|
+
expect(screen.getByText('loginButton')).toBeInTheDocument();
|
|
97
|
+
expect(screen.queryByText('locationsHeader')).not.toBeInTheDocument();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('writes an edited value to the temporary config and reloads the namespace', async () => {
|
|
101
|
+
const user = userEvent.setup();
|
|
102
|
+
render(<Translations />);
|
|
103
|
+
|
|
104
|
+
const row = screen.getByRole('row', { name: /loginButton/ });
|
|
105
|
+
await user.click(within(row).getByRole('button', { name: /edit/i }));
|
|
106
|
+
|
|
107
|
+
const editor = within(row).getByRole('textbox');
|
|
108
|
+
await user.clear(editor);
|
|
109
|
+
await user.type(editor, 'Sign in');
|
|
110
|
+
await user.click(within(row).getByRole('button', { name: /save/i }));
|
|
111
|
+
|
|
112
|
+
expect(temporaryConfigStore.getState().config).toEqual({
|
|
113
|
+
'@openmrs/esm-login-app': {
|
|
114
|
+
'Translation overrides': {
|
|
115
|
+
en: { loginButton: 'Sign in' },
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
expect(mockReloadResources).toHaveBeenCalledWith(['en'], ['@openmrs/esm-login-app'], expect.any(Function));
|
|
120
|
+
expect(window.i18next.emit).toHaveBeenCalledWith('languageChanged', 'en');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('shows where an override came from, and only offers to reset local ones', () => {
|
|
124
|
+
implementerToolsConfigStore.setState({
|
|
125
|
+
config: {
|
|
126
|
+
'@openmrs/esm-login-app': {
|
|
127
|
+
'Translation overrides': {
|
|
128
|
+
en: {
|
|
129
|
+
loginButton: { _value: 'Sign in', _source: 'temporary config' },
|
|
130
|
+
locationsHeader: { _value: 'Facilities', _source: 'my-distro-config.json' },
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
render(<Translations />);
|
|
138
|
+
|
|
139
|
+
const overriddenRow = screen.getByRole('row', { name: /loginButton/ });
|
|
140
|
+
expect(within(overriddenRow).getByText('temporary config')).toBeInTheDocument();
|
|
141
|
+
expect(within(overriddenRow).getByRole('button', { name: /reset to default/i })).toBeInTheDocument();
|
|
142
|
+
|
|
143
|
+
const providedRow = screen.getByRole('row', { name: /locationsHeader/ });
|
|
144
|
+
expect(within(providedRow).getByText('my-distro-config.json')).toBeInTheDocument();
|
|
145
|
+
expect(within(providedRow).queryByRole('button', { name: /reset to default/i })).not.toBeInTheDocument();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('offers every locale the server permits, as language tags', async () => {
|
|
149
|
+
const user = userEvent.setup();
|
|
150
|
+
mockUseSession.mockReturnValue({
|
|
151
|
+
authenticated: true,
|
|
152
|
+
sessionId: '',
|
|
153
|
+
allowedLocales: ['en', 'km_KH', 'fr'],
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
render(<Translations />);
|
|
157
|
+
|
|
158
|
+
await user.click(screen.getByRole('combobox', { name: /language/i }));
|
|
159
|
+
|
|
160
|
+
// `km_KH` is Java's form; i18next and the config system both want `km-KH`.
|
|
161
|
+
expect(screen.getByRole('option', { name: 'km-KH' })).toBeInTheDocument();
|
|
162
|
+
expect(screen.getByRole('option', { name: 'fr' })).toBeInTheDocument();
|
|
163
|
+
expect(screen.getByRole('option', { name: 'en' })).toBeInTheDocument();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('fetches the resources for a locale that has not been loaded yet', async () => {
|
|
167
|
+
const user = userEvent.setup();
|
|
168
|
+
mockUseSession.mockReturnValue({ authenticated: true, sessionId: '', allowedLocales: ['en', 'fr'] });
|
|
169
|
+
|
|
170
|
+
render(<Translations />);
|
|
171
|
+
|
|
172
|
+
await user.click(screen.getByRole('combobox', { name: /language/i }));
|
|
173
|
+
await user.click(screen.getByRole('option', { name: 'fr' }));
|
|
174
|
+
|
|
175
|
+
// Only what the application has loaded, so not i18next's default `translation` namespace, which
|
|
176
|
+
// the app shell refuses.
|
|
177
|
+
expect(mockReloadResources).toHaveBeenCalledWith(['fr'], ['@openmrs/esm-login-app'], expect.any(Function));
|
|
178
|
+
expect(mockReloadResources).toHaveBeenCalledTimes(1);
|
|
179
|
+
expect(window.i18next.loadLanguages).not.toHaveBeenCalled();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('does not refetch a locale i18next already holds', async () => {
|
|
183
|
+
const user = userEvent.setup();
|
|
184
|
+
mockUseSession.mockReturnValue({ authenticated: true, sessionId: '', allowedLocales: ['en', 'fr'] });
|
|
185
|
+
window.i18next.store.data.fr = { '@openmrs/esm-login-app': { loginButton: 'Connexion' } };
|
|
186
|
+
|
|
187
|
+
render(<Translations />);
|
|
188
|
+
|
|
189
|
+
await user.click(screen.getByRole('combobox', { name: /language/i }));
|
|
190
|
+
await user.click(screen.getByRole('option', { name: 'fr' }));
|
|
191
|
+
|
|
192
|
+
expect(await screen.findByText('Connexion')).toBeInTheDocument();
|
|
193
|
+
expect(mockReloadResources).not.toHaveBeenCalled();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('says so when a module’s translations cannot be loaded', async () => {
|
|
197
|
+
const user = userEvent.setup();
|
|
198
|
+
// i18next reports a load failure only through the callback; the promise it returns resolves.
|
|
199
|
+
mockReloadResources.mockImplementationOnce((_lngs, _ns, done) => done?.(new Error('no importTranslation export')));
|
|
200
|
+
window.installedModules = [
|
|
201
|
+
['@openmrs/esm-login-app', {}],
|
|
202
|
+
['@openmrs/esm-unloadable-app', {}],
|
|
203
|
+
] as unknown as typeof window.installedModules;
|
|
204
|
+
|
|
205
|
+
render(<Translations />);
|
|
206
|
+
|
|
207
|
+
await user.click(screen.getByRole('combobox', { name: /namespace/i }));
|
|
208
|
+
await user.click(screen.getByRole('option', { name: /esm-unloadable-app/ }));
|
|
209
|
+
|
|
210
|
+
expect(await screen.findByText(/no importTranslation export/)).toBeInTheDocument();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('still says the load failed once its notification is dismissed', async () => {
|
|
214
|
+
const user = userEvent.setup();
|
|
215
|
+
mockReloadResources.mockImplementationOnce((_lngs, _ns, done) => done?.(new Error('no importTranslation export')));
|
|
216
|
+
window.installedModules = [
|
|
217
|
+
['@openmrs/esm-login-app', {}],
|
|
218
|
+
['@openmrs/esm-unloadable-app', {}],
|
|
219
|
+
] as unknown as typeof window.installedModules;
|
|
220
|
+
|
|
221
|
+
render(<Translations />);
|
|
222
|
+
|
|
223
|
+
await user.click(screen.getByRole('combobox', { name: /namespace/i }));
|
|
224
|
+
await user.click(screen.getByRole('option', { name: /esm-unloadable-app/ }));
|
|
225
|
+
await screen.findByText(/no importTranslation export/);
|
|
226
|
+
|
|
227
|
+
await user.click(screen.getByRole('button', { name: /close notification/i }));
|
|
228
|
+
|
|
229
|
+
expect(screen.queryByText(/no importTranslation export/)).not.toBeInTheDocument();
|
|
230
|
+
expect(screen.getByText(/could not be loaded/i)).toBeInTheDocument();
|
|
231
|
+
expect(screen.queryByText(/has no translations for this language/)).not.toBeInTheDocument();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('does not claim a module has no translations when its bundle never arrived', async () => {
|
|
235
|
+
const user = userEvent.setup();
|
|
236
|
+
// i18next permanently skips a namespace whose first load failed and reports later attempts as a
|
|
237
|
+
// success with nothing loaded; saying "no translations" then would be a false statement of fact.
|
|
238
|
+
mockHasResourceBundle.mockReturnValue(false);
|
|
239
|
+
window.installedModules = [
|
|
240
|
+
['@openmrs/esm-login-app', {}],
|
|
241
|
+
['@openmrs/esm-unloadable-app', {}],
|
|
242
|
+
] as unknown as typeof window.installedModules;
|
|
243
|
+
|
|
244
|
+
render(<Translations />);
|
|
245
|
+
|
|
246
|
+
await user.click(screen.getByRole('combobox', { name: /namespace/i }));
|
|
247
|
+
await user.click(screen.getByRole('option', { name: /esm-unloadable-app/ }));
|
|
248
|
+
|
|
249
|
+
expect(await screen.findByText(/holds no @openmrs\/esm-unloadable-app resources/)).toBeInTheDocument();
|
|
250
|
+
expect(screen.queryByText(/has no translations for this language/)).not.toBeInTheDocument();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('discards an edit in progress when the language changes', async () => {
|
|
254
|
+
const user = userEvent.setup();
|
|
255
|
+
mockUseSession.mockReturnValue({ authenticated: true, sessionId: '', allowedLocales: ['en', 'fr'] });
|
|
256
|
+
window.i18next.store.data.fr = { '@openmrs/esm-login-app': { loginButton: 'Connexion' } };
|
|
257
|
+
|
|
258
|
+
render(<Translations />);
|
|
259
|
+
|
|
260
|
+
const row = screen.getByRole('row', { name: /loginButton/ });
|
|
261
|
+
await user.click(within(row).getByRole('button', { name: /edit/i }));
|
|
262
|
+
await user.clear(within(row).getByRole('textbox'));
|
|
263
|
+
await user.type(within(row).getByRole('textbox'), 'English draft');
|
|
264
|
+
|
|
265
|
+
await user.click(screen.getByRole('combobox', { name: /language/i }));
|
|
266
|
+
await user.click(screen.getByRole('option', { name: 'fr' }));
|
|
267
|
+
|
|
268
|
+
// Saving here would have written the English draft as the French override.
|
|
269
|
+
const frenchRow = screen.getByRole('row', { name: /loginButton/ });
|
|
270
|
+
expect(within(frenchRow).queryByRole('textbox')).not.toBeInTheDocument();
|
|
271
|
+
expect(within(frenchRow).getByText('Connexion')).toBeInTheDocument();
|
|
272
|
+
expect(temporaryConfigStore.getState().config ?? {}).toEqual({});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('loads the selected namespace again for a newly selected language', async () => {
|
|
276
|
+
const user = userEvent.setup();
|
|
277
|
+
const data = window.i18next.store.data as Record<string, Record<string, Record<string, string>>>;
|
|
278
|
+
mockHasResourceBundle.mockImplementation(((lng: string, ns: string) => Boolean(data[lng]?.[ns])) as () => boolean);
|
|
279
|
+
mockReloadResources.mockImplementation(((lngs: Array<string>, nss: Array<string>, done) => {
|
|
280
|
+
lngs.forEach((lng) => nss.forEach((ns) => ((data[lng] ??= {})[ns] = { greeting: `${lng} greeting` })));
|
|
281
|
+
done?.();
|
|
282
|
+
}) as typeof mockReloadResources);
|
|
283
|
+
mockUseSession.mockReturnValue({ authenticated: true, sessionId: '', allowedLocales: ['en', 'fr'] });
|
|
284
|
+
window.installedModules = [
|
|
285
|
+
['@openmrs/esm-login-app', {}],
|
|
286
|
+
['@openmrs/esm-other-app', {}],
|
|
287
|
+
] as unknown as typeof window.installedModules;
|
|
288
|
+
|
|
289
|
+
render(<Translations />);
|
|
290
|
+
|
|
291
|
+
await user.click(screen.getByRole('combobox', { name: /namespace/i }));
|
|
292
|
+
await user.click(screen.getByRole('option', { name: /esm-other-app/ }));
|
|
293
|
+
expect(await screen.findByText('en greeting')).toBeInTheDocument();
|
|
294
|
+
|
|
295
|
+
await user.click(screen.getByRole('combobox', { name: /language/i }));
|
|
296
|
+
await user.click(screen.getByRole('option', { name: 'fr' }));
|
|
297
|
+
|
|
298
|
+
expect(mockReloadResources).toHaveBeenCalledWith(['fr'], ['@openmrs/esm-other-app'], expect.any(Function));
|
|
299
|
+
expect(await screen.findByText('fr greeting')).toBeInTheDocument();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('lists a key that only the fallback language has, with the fallback’s value', () => {
|
|
303
|
+
window.i18next.language = 'fr';
|
|
304
|
+
window.i18next.languages = ['fr', 'en'];
|
|
305
|
+
window.i18next.store.data = {
|
|
306
|
+
fr: { '@openmrs/esm-login-app': { loginButton: 'Connexion' } },
|
|
307
|
+
en: { '@openmrs/esm-login-app': { loginButton: 'Log in', locationsHeader: 'Locations' } },
|
|
308
|
+
} as unknown as typeof window.i18next.store.data;
|
|
309
|
+
|
|
310
|
+
render(<Translations />);
|
|
311
|
+
|
|
312
|
+
expect(screen.getByText('Connexion')).toBeInTheDocument();
|
|
313
|
+
// Present only in the fallback, but still something an implementer needs to override for `fr`.
|
|
314
|
+
expect(screen.getByText('locationsHeader')).toBeInTheDocument();
|
|
315
|
+
expect(screen.getByText('Locations')).toBeInTheDocument();
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('reads values from the selected language, not the one the application is running in', async () => {
|
|
319
|
+
// i18next's own chain is ordered for the running language, so browsing `en` while the app runs
|
|
320
|
+
// in `fr` must not read every value out of the French bundle.
|
|
321
|
+
window.i18next.language = 'fr';
|
|
322
|
+
window.i18next.languages = ['fr', 'en'];
|
|
323
|
+
window.i18next.store.data = {
|
|
324
|
+
fr: { '@openmrs/esm-login-app': { loginButton: 'Connexion' } },
|
|
325
|
+
en: { '@openmrs/esm-login-app': { loginButton: 'Log in' } },
|
|
326
|
+
} as unknown as typeof window.i18next.store.data;
|
|
327
|
+
|
|
328
|
+
render(<Translations />);
|
|
329
|
+
|
|
330
|
+
await userEvent.setup().click(screen.getByRole('combobox', { name: /language/i }));
|
|
331
|
+
await userEvent.setup().click(screen.getByRole('option', { name: 'en' }));
|
|
332
|
+
|
|
333
|
+
expect(screen.getByText('Log in')).toBeInTheDocument();
|
|
334
|
+
expect(screen.queryByText('Connexion')).not.toBeInTheDocument();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('points out the plural forms that an override of the base key would miss', () => {
|
|
338
|
+
window.i18next.store.data.en['@openmrs/esm-login-app'] = {
|
|
339
|
+
itemCount: '{{count}} item',
|
|
340
|
+
itemCount_one: '{{count}} item',
|
|
341
|
+
itemCount_other: '{{count}} items',
|
|
342
|
+
greeting: 'Hello',
|
|
343
|
+
greeting_male: 'Hello sir',
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
render(<Translations />);
|
|
347
|
+
|
|
348
|
+
const pluralRow = screen.getByRole('row', { name: /itemCount / });
|
|
349
|
+
expect(
|
|
350
|
+
within(pluralRow).getByText('Plural forms to override too: itemCount_one, itemCount_other'),
|
|
351
|
+
).toBeInTheDocument();
|
|
352
|
+
|
|
353
|
+
// Context suffixes are arbitrary application data, so they are deliberately not guessed at.
|
|
354
|
+
const contextRow = screen.getByRole('row', { name: /greeting Hello/ });
|
|
355
|
+
expect(within(contextRow).queryByText(/Plural forms to override too/)).not.toBeInTheDocument();
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('drops a load the inspector’s handoff superseded, including its failure', async () => {
|
|
359
|
+
const user = userEvent.setup();
|
|
360
|
+
let failSlowLoad: () => void = () => {};
|
|
361
|
+
mockReloadResources.mockImplementationOnce((_lngs, _ns, done) => {
|
|
362
|
+
failSlowLoad = () => done?.(new Error('slow load failed'));
|
|
363
|
+
});
|
|
364
|
+
window.installedModules = [
|
|
365
|
+
['@openmrs/esm-login-app', {}],
|
|
366
|
+
['@openmrs/esm-slow-app', {}],
|
|
367
|
+
] as unknown as typeof window.installedModules;
|
|
368
|
+
|
|
369
|
+
render(<Translations />);
|
|
370
|
+
|
|
371
|
+
await user.click(screen.getByRole('combobox', { name: /namespace/i }));
|
|
372
|
+
await user.click(screen.getByRole('option', { name: /esm-slow-app/ }));
|
|
373
|
+
|
|
374
|
+
act(() =>
|
|
375
|
+
implementerToolsStore.setState((state) => ({
|
|
376
|
+
...state,
|
|
377
|
+
selectedTranslation: { namespace: '@openmrs/esm-login-app', key: 'loginButton' },
|
|
378
|
+
})),
|
|
379
|
+
);
|
|
380
|
+
await act(async () => failSlowLoad());
|
|
381
|
+
// Let the rejected load's handlers run before looking.
|
|
382
|
+
await act(() => new Promise((resolve) => setTimeout(resolve, 0)));
|
|
383
|
+
|
|
384
|
+
expect(screen.getByText('loginButton')).toBeInTheDocument();
|
|
385
|
+
expect(screen.queryByText(/slow load failed/)).not.toBeInTheDocument();
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('narrows to a single key when the inspector hands one over', () => {
|
|
389
|
+
implementerToolsStore.setState((state) => ({
|
|
390
|
+
...state,
|
|
391
|
+
selectedTranslation: { namespace: '@openmrs/esm-login-app', key: 'locationsHeader' },
|
|
392
|
+
}));
|
|
393
|
+
|
|
394
|
+
render(<Translations />);
|
|
395
|
+
|
|
396
|
+
expect(screen.getByText('locationsHeader')).toBeInTheDocument();
|
|
397
|
+
expect(screen.queryByText('loginButton')).not.toBeInTheDocument();
|
|
398
|
+
expect(implementerToolsStore.getState().selectedTranslation).toBeNull();
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('shows only the handed-over key, in its own namespace, until the search is edited', async () => {
|
|
402
|
+
window.i18next.store.data.en = {
|
|
403
|
+
'@openmrs/esm-login-app': { save: 'Save', saveDraft: 'Save draft' },
|
|
404
|
+
'@openmrs/esm-other-app': { save: 'Save' },
|
|
405
|
+
};
|
|
406
|
+
window.installedModules = [
|
|
407
|
+
['@openmrs/esm-login-app', {}],
|
|
408
|
+
['@openmrs/esm-other-app', {}],
|
|
409
|
+
] as unknown as typeof window.installedModules;
|
|
410
|
+
implementerToolsStore.setState((state) => ({
|
|
411
|
+
...state,
|
|
412
|
+
selectedTranslation: { namespace: '@openmrs/esm-login-app', key: 'save' },
|
|
413
|
+
}));
|
|
414
|
+
|
|
415
|
+
render(<Translations />);
|
|
416
|
+
|
|
417
|
+
expect(screen.getAllByRole('row', { name: /save/i })).toHaveLength(1);
|
|
418
|
+
expect(screen.getByRole('row', { name: /esm-login-app save/ })).toBeInTheDocument();
|
|
419
|
+
|
|
420
|
+
const search = screen.getByRole('textbox', { name: /search translations/i });
|
|
421
|
+
await userEvent.setup().type(search, ' ');
|
|
422
|
+
await userEvent.setup().type(search, '{Backspace}');
|
|
423
|
+
|
|
424
|
+
expect(screen.getByText('saveDraft')).toBeInTheDocument();
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('loads the handed-over namespace when the browsed language does not have it', () => {
|
|
428
|
+
mockHasResourceBundle.mockReturnValue(false);
|
|
429
|
+
implementerToolsStore.setState((state) => ({
|
|
430
|
+
...state,
|
|
431
|
+
selectedTranslation: { namespace: '@openmrs/esm-login-app', key: 'loginButton' },
|
|
432
|
+
}));
|
|
433
|
+
|
|
434
|
+
render(<Translations />);
|
|
435
|
+
|
|
436
|
+
expect(mockReloadResources).toHaveBeenCalledWith(['en'], ['@openmrs/esm-login-app'], expect.any(Function));
|
|
437
|
+
mockHasResourceBundle.mockReturnValue(true);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('lists a handed-over key that no translation file has, with the text the inspector saw', async () => {
|
|
441
|
+
const user = userEvent.setup();
|
|
442
|
+
implementerToolsStore.setState((state) => ({
|
|
443
|
+
...state,
|
|
444
|
+
selectedTranslation: { namespace: '@openmrs/esm-login-app', key: 'missingKey', value: 'Default text' },
|
|
445
|
+
}));
|
|
446
|
+
|
|
447
|
+
render(<Translations />);
|
|
448
|
+
|
|
449
|
+
const row = screen.getByRole('row', { name: /missingKey/ });
|
|
450
|
+
expect(within(row).getByText('Default text')).toBeInTheDocument();
|
|
451
|
+
expect(within(row).getByText('Not in the translation file')).toBeInTheDocument();
|
|
452
|
+
expect(screen.queryByText('No translations match your search.')).not.toBeInTheDocument();
|
|
453
|
+
|
|
454
|
+
await user.click(within(row).getByRole('button', { name: /edit/i }));
|
|
455
|
+
await user.clear(within(row).getByRole('textbox'));
|
|
456
|
+
await user.type(within(row).getByRole('textbox'), 'Overridden{Enter}');
|
|
457
|
+
|
|
458
|
+
expect(temporaryConfigStore.getState().config).toEqual({
|
|
459
|
+
'@openmrs/esm-login-app': { 'Translation overrides': { en: { missingKey: 'Overridden' } } },
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('does not list a handed-over key that is missing once the search is edited', async () => {
|
|
464
|
+
implementerToolsStore.setState((state) => ({
|
|
465
|
+
...state,
|
|
466
|
+
selectedTranslation: { namespace: '@openmrs/esm-login-app', key: 'missingKey', value: 'Default text' },
|
|
467
|
+
}));
|
|
468
|
+
|
|
469
|
+
render(<Translations />);
|
|
470
|
+
await userEvent.setup().type(screen.getByRole('textbox', { name: /search translations/i }), 'x');
|
|
471
|
+
|
|
472
|
+
expect(screen.queryByText('missingKey')).not.toBeInTheDocument();
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it('labels a value from a fallback language’s override, without offering to reset it from here', () => {
|
|
476
|
+
window.i18next.language = 'fr';
|
|
477
|
+
window.i18next.store.data = {
|
|
478
|
+
en: { '@openmrs/esm-login-app': { loginButton: 'Sign in' } },
|
|
479
|
+
fr: { '@openmrs/esm-login-app': {} },
|
|
480
|
+
} as unknown as typeof window.i18next.store.data;
|
|
481
|
+
implementerToolsConfigStore.setState({
|
|
482
|
+
config: {
|
|
483
|
+
'@openmrs/esm-login-app': {
|
|
484
|
+
'Translation overrides': { en: { loginButton: { _value: 'Sign in', _source: 'temporary config' } } },
|
|
485
|
+
},
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
render(<Translations />);
|
|
490
|
+
|
|
491
|
+
const row = screen.getByRole('row', { name: /loginButton/ });
|
|
492
|
+
expect(within(row).getByText('temporary config')).toBeInTheDocument();
|
|
493
|
+
// Resetting from `fr` would remove an `fr` override, which there is none of.
|
|
494
|
+
expect(within(row).queryByRole('button', { name: /reset to default/i })).not.toBeInTheDocument();
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it('keeps a superseded namespace load’s failure off the screen', async () => {
|
|
498
|
+
const user = userEvent.setup();
|
|
499
|
+
let failFirstLoad: () => void = () => {};
|
|
500
|
+
mockReloadResources.mockImplementationOnce((_lngs, _ns, done) => {
|
|
501
|
+
failFirstLoad = () => done?.(new Error('first load failed'));
|
|
502
|
+
});
|
|
503
|
+
window.installedModules = [
|
|
504
|
+
['@openmrs/esm-login-app', {}],
|
|
505
|
+
['@openmrs/esm-first-app', {}],
|
|
506
|
+
['@openmrs/esm-second-app', {}],
|
|
507
|
+
] as unknown as typeof window.installedModules;
|
|
508
|
+
|
|
509
|
+
render(<Translations />);
|
|
510
|
+
|
|
511
|
+
await user.click(screen.getByRole('combobox', { name: /namespace/i }));
|
|
512
|
+
await user.click(screen.getByRole('option', { name: /esm-first-app/ }));
|
|
513
|
+
await user.click(screen.getByRole('combobox', { name: /namespace/i }));
|
|
514
|
+
await user.click(screen.getByRole('option', { name: /esm-second-app/ }));
|
|
515
|
+
|
|
516
|
+
await act(async () => failFirstLoad());
|
|
517
|
+
await act(() => new Promise((resolve) => setTimeout(resolve, 0)));
|
|
518
|
+
|
|
519
|
+
expect(screen.queryByText(/first load failed/)).not.toBeInTheDocument();
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it('shows the table as loading until every load a language change started has finished', async () => {
|
|
523
|
+
const user = userEvent.setup();
|
|
524
|
+
const pending: Array<() => void> = [];
|
|
525
|
+
const data = window.i18next.store.data as Record<string, Record<string, Record<string, string>>>;
|
|
526
|
+
mockHasResourceBundle.mockImplementation(((lng: string, ns: string) => Boolean(data[lng]?.[ns])) as () => boolean);
|
|
527
|
+
mockReloadResources.mockImplementation(((lngs: Array<string>, nss: Array<string>, done) => {
|
|
528
|
+
pending.push(() => {
|
|
529
|
+
lngs.forEach((lng) => nss.forEach((ns) => ((data[lng] ??= {})[ns] = {})));
|
|
530
|
+
done?.();
|
|
531
|
+
});
|
|
532
|
+
}) as typeof mockReloadResources);
|
|
533
|
+
mockUseSession.mockReturnValue({ authenticated: true, sessionId: '', allowedLocales: ['en', 'fr'] });
|
|
534
|
+
data.en['@openmrs/esm-empty-app'] = {};
|
|
535
|
+
window.installedModules = [['@openmrs/esm-empty-app', {}]] as unknown as typeof window.installedModules;
|
|
536
|
+
delete data.en['@openmrs/esm-login-app'];
|
|
537
|
+
|
|
538
|
+
render(<Translations />);
|
|
539
|
+
|
|
540
|
+
await user.click(screen.getByRole('combobox', { name: /namespace/i }));
|
|
541
|
+
await user.click(screen.getByRole('option', { name: '@openmrs/esm-empty-app' }));
|
|
542
|
+
await user.click(screen.getByRole('combobox', { name: /language/i }));
|
|
543
|
+
await user.click(screen.getByRole('option', { name: 'fr' }));
|
|
544
|
+
|
|
545
|
+
// One load for the language, one for the selected namespace.
|
|
546
|
+
expect(pending.length).toBeGreaterThan(1);
|
|
547
|
+
|
|
548
|
+
await act(async () => pending.shift()?.());
|
|
549
|
+
expect(screen.getByText(/loading translations/i)).toBeInTheDocument();
|
|
550
|
+
|
|
551
|
+
await act(async () => pending.splice(0).forEach((finish) => finish()));
|
|
552
|
+
expect(await screen.findByText(/has no translations for this language/)).toBeInTheDocument();
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it('pulls the page back into range when the list shrinks under it', async () => {
|
|
556
|
+
const user = userEvent.setup();
|
|
557
|
+
const keys = Array.from({ length: 26 }, (_, i) => `key${String(i).padStart(2, '0')}`);
|
|
558
|
+
const overridden = (count: number) => ({
|
|
559
|
+
config: {
|
|
560
|
+
'@openmrs/esm-login-app': {
|
|
561
|
+
'Translation overrides': {
|
|
562
|
+
en: Object.fromEntries(
|
|
563
|
+
keys.slice(0, count).map((key) => [key, { _value: `Override ${key}`, _source: 'temporary config' }]),
|
|
564
|
+
),
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
});
|
|
569
|
+
window.i18next.store.data.en['@openmrs/esm-login-app'] = Object.fromEntries(
|
|
570
|
+
keys.map((key) => [key, `Value ${key}`]),
|
|
571
|
+
);
|
|
572
|
+
implementerToolsConfigStore.setState(overridden(26));
|
|
573
|
+
|
|
574
|
+
render(<Translations />);
|
|
575
|
+
|
|
576
|
+
await user.click(screen.getByRole('switch', { name: /only overridden/i }));
|
|
577
|
+
await user.click(screen.getByRole('button', { name: /next page/i }));
|
|
578
|
+
expect(screen.getByText('key25')).toBeInTheDocument();
|
|
579
|
+
|
|
580
|
+
// Resetting the one override on the last page shrinks the list without anything resetting the page.
|
|
581
|
+
act(() => implementerToolsConfigStore.setState(overridden(25)));
|
|
582
|
+
|
|
583
|
+
expect(screen.getByText('key00')).toBeInTheDocument();
|
|
584
|
+
expect(screen.getAllByRole('row')).toHaveLength(26);
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it('keeps the editor open, holding the draft, when a save could not be applied', async () => {
|
|
588
|
+
const user = userEvent.setup();
|
|
589
|
+
render(<Translations />);
|
|
590
|
+
|
|
591
|
+
const row = screen.getByRole('row', { name: /loginButton/ });
|
|
592
|
+
await user.click(within(row).getByRole('button', { name: /edit/i }));
|
|
593
|
+
await user.clear(within(row).getByRole('textbox'));
|
|
594
|
+
await user.type(within(row).getByRole('textbox'), 'Sign in');
|
|
595
|
+
|
|
596
|
+
mockReloadResources.mockImplementationOnce((_lngs, _ns, done) => done?.(new Error('chunk 404')));
|
|
597
|
+
await user.click(within(row).getByRole('button', { name: /save/i }));
|
|
598
|
+
|
|
599
|
+
expect(await screen.findByText(/saved but could not be applied/)).toBeInTheDocument();
|
|
600
|
+
expect(within(row).getByRole('textbox')).toHaveValue('Sign in');
|
|
601
|
+
|
|
602
|
+
// The next save that does apply clears the failure.
|
|
603
|
+
await user.click(within(row).getByRole('button', { name: /save/i }));
|
|
604
|
+
expect(screen.queryByText(/saved but could not be applied/)).not.toBeInTheDocument();
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it('drops the reset key from the loaded bundle so the shipped string can come back', async () => {
|
|
608
|
+
const user = userEvent.setup();
|
|
609
|
+
const bundle: Record<string, string> = { loginButton: 'Sign in', locationsHeader: 'Locations' };
|
|
610
|
+
window.i18next.getResourceBundle = vi.fn(() => bundle);
|
|
611
|
+
implementerToolsConfigStore.setState({
|
|
612
|
+
config: {
|
|
613
|
+
'@openmrs/esm-login-app': {
|
|
614
|
+
'Translation overrides': { en: { loginButton: { _value: 'Sign in', _source: 'temporary config' } } },
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
render(<Translations />);
|
|
620
|
+
|
|
621
|
+
const row = screen.getByRole('row', { name: /loginButton/ });
|
|
622
|
+
await user.click(within(row).getByRole('button', { name: /reset to default/i }));
|
|
623
|
+
|
|
624
|
+
expect(bundle).toEqual({ locationsHeader: 'Locations' });
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
it('focuses the editor, saves on Enter, and hands focus back to the Edit button', async () => {
|
|
628
|
+
const user = userEvent.setup();
|
|
629
|
+
render(<Translations />);
|
|
630
|
+
|
|
631
|
+
const row = screen.getByRole('row', { name: /loginButton/ });
|
|
632
|
+
await user.click(within(row).getByRole('button', { name: /edit/i }));
|
|
633
|
+
|
|
634
|
+
const editor = within(row).getByRole('textbox');
|
|
635
|
+
expect(editor).toHaveFocus();
|
|
636
|
+
|
|
637
|
+
await user.clear(editor);
|
|
638
|
+
await user.keyboard('Sign in{Enter}');
|
|
639
|
+
|
|
640
|
+
expect(temporaryConfigStore.getState().config).toEqual({
|
|
641
|
+
'@openmrs/esm-login-app': { 'Translation overrides': { en: { loginButton: 'Sign in' } } },
|
|
642
|
+
});
|
|
643
|
+
expect(within(row).queryByRole('textbox')).not.toBeInTheDocument();
|
|
644
|
+
expect(within(row).getByRole('button', { name: /edit/i })).toHaveFocus();
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
it('cancels an edit on Escape without writing anything', async () => {
|
|
648
|
+
const user = userEvent.setup();
|
|
649
|
+
render(<Translations />);
|
|
650
|
+
|
|
651
|
+
const row = screen.getByRole('row', { name: /loginButton/ });
|
|
652
|
+
await user.click(within(row).getByRole('button', { name: /edit/i }));
|
|
653
|
+
await user.keyboard('changed{Escape}');
|
|
654
|
+
|
|
655
|
+
expect(within(row).queryByRole('textbox')).not.toBeInTheDocument();
|
|
656
|
+
expect(within(row).getByRole('button', { name: /edit/i })).toHaveFocus();
|
|
657
|
+
expect(temporaryConfigStore.getState().config).toEqual({});
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it('stops saying a value was copied after a moment', async () => {
|
|
661
|
+
vi.useFakeTimers();
|
|
662
|
+
const writeText = vi.fn(() => Promise.resolve());
|
|
663
|
+
Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } });
|
|
664
|
+
|
|
665
|
+
try {
|
|
666
|
+
render(<Translations />);
|
|
667
|
+
|
|
668
|
+
const row = screen.getByRole('row', { name: /loginButton/ });
|
|
669
|
+
fireEvent.click(within(row).getByRole('button', { name: /copy override json/i }));
|
|
670
|
+
// Awaiting the write inside `act` lets the copy's callback, and the effect that starts the reset
|
|
671
|
+
// timer, run before the clock is advanced.
|
|
672
|
+
await act(() => writeText.mock.results[0].value);
|
|
673
|
+
|
|
674
|
+
expect(within(row).getByRole('button', { name: /copied/i })).toBeInTheDocument();
|
|
675
|
+
|
|
676
|
+
act(() => vi.advanceTimersByTime(2000));
|
|
677
|
+
|
|
678
|
+
expect(within(row).getByRole('button', { name: /copy override json/i })).toBeInTheDocument();
|
|
679
|
+
} finally {
|
|
680
|
+
Reflect.deleteProperty(navigator, 'clipboard');
|
|
681
|
+
vi.useRealTimers();
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
it('closes the editor once a reset applies, so Save cannot write the old override back', async () => {
|
|
686
|
+
const user = userEvent.setup();
|
|
687
|
+
implementerToolsConfigStore.setState({
|
|
688
|
+
config: {
|
|
689
|
+
'@openmrs/esm-login-app': {
|
|
690
|
+
'Translation overrides': { en: { loginButton: { _value: 'Sign in', _source: 'temporary config' } } },
|
|
691
|
+
},
|
|
692
|
+
},
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
render(<Translations />);
|
|
696
|
+
|
|
697
|
+
const row = screen.getByRole('row', { name: /loginButton/ });
|
|
698
|
+
await user.click(within(row).getByRole('button', { name: /edit/i }));
|
|
699
|
+
await user.click(within(row).getByRole('button', { name: /reset to default/i }));
|
|
700
|
+
|
|
701
|
+
expect(within(row).queryByRole('textbox')).not.toBeInTheDocument();
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
it('fetches a language’s missing namespaces even when it already holds others', async () => {
|
|
705
|
+
const user = userEvent.setup();
|
|
706
|
+
const data = window.i18next.store.data as Record<string, Record<string, Record<string, string>>>;
|
|
707
|
+
mockHasResourceBundle.mockImplementation(((lng: string, ns: string) => Boolean(data[lng]?.[ns])) as () => boolean);
|
|
708
|
+
mockUseSession.mockReturnValue({ authenticated: true, sessionId: '', allowedLocales: ['en', 'fr'] });
|
|
709
|
+
// French was browsed before the application loaded `esm-other-app`.
|
|
710
|
+
data.fr = { '@openmrs/esm-login-app': { loginButton: 'Connexion' } };
|
|
711
|
+
data.en['@openmrs/esm-other-app'] = { greeting: 'Hello' };
|
|
712
|
+
window.installedModules = [
|
|
713
|
+
['@openmrs/esm-login-app', {}],
|
|
714
|
+
['@openmrs/esm-other-app', {}],
|
|
715
|
+
] as unknown as typeof window.installedModules;
|
|
716
|
+
|
|
717
|
+
render(<Translations />);
|
|
718
|
+
|
|
719
|
+
await user.click(screen.getByRole('combobox', { name: /language/i }));
|
|
720
|
+
await user.click(screen.getByRole('option', { name: 'fr' }));
|
|
721
|
+
|
|
722
|
+
expect(mockReloadResources).toHaveBeenCalledWith(['fr'], ['@openmrs/esm-other-app'], expect.any(Function));
|
|
723
|
+
expect(mockReloadResources).toHaveBeenCalledTimes(1);
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it('keeps a superseded language load’s failure off the screen', async () => {
|
|
727
|
+
const user = userEvent.setup();
|
|
728
|
+
let failFrenchLoad: () => void = () => {};
|
|
729
|
+
mockReloadResources.mockImplementationOnce((_lngs, _ns, done) => {
|
|
730
|
+
failFrenchLoad = () => done?.(new Error('french load failed'));
|
|
731
|
+
});
|
|
732
|
+
mockUseSession.mockReturnValue({ authenticated: true, sessionId: '', allowedLocales: ['en', 'fr'] });
|
|
733
|
+
|
|
734
|
+
render(<Translations />);
|
|
735
|
+
|
|
736
|
+
await user.click(screen.getByRole('combobox', { name: /language/i }));
|
|
737
|
+
await user.click(screen.getByRole('option', { name: 'fr' }));
|
|
738
|
+
await user.click(screen.getByRole('combobox', { name: /language/i }));
|
|
739
|
+
await user.click(screen.getByRole('option', { name: 'en' }));
|
|
740
|
+
|
|
741
|
+
await act(async () => failFrenchLoad());
|
|
742
|
+
await act(() => new Promise((resolve) => setTimeout(resolve, 0)));
|
|
743
|
+
|
|
744
|
+
expect(screen.queryByText(/french load failed/)).not.toBeInTheDocument();
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
describe('exporting changes', () => {
|
|
748
|
+
const downloads: Array<{ name: string; content: Promise<unknown> }> = [];
|
|
749
|
+
|
|
750
|
+
beforeEach(() => {
|
|
751
|
+
downloads.length = 0;
|
|
752
|
+
let blob: Blob;
|
|
753
|
+
vi.spyOn(URL, 'createObjectURL').mockImplementation((value) => {
|
|
754
|
+
blob = value as Blob;
|
|
755
|
+
return 'blob:export';
|
|
756
|
+
});
|
|
757
|
+
vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => undefined);
|
|
758
|
+
vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(function (this: HTMLAnchorElement) {
|
|
759
|
+
downloads.push({ name: this.download, content: blob.text().then(JSON.parse) });
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
window.i18next.language = 'fr';
|
|
763
|
+
window.i18next.languages = ['fr', 'en'];
|
|
764
|
+
window.i18next.store.data = {
|
|
765
|
+
fr: { '@openmrs/esm-login-app': { loginButton: 'Connexion', locationsHeader: 'Emplacements' } },
|
|
766
|
+
en: { '@openmrs/esm-login-app': { loginButton: 'Log in', locationsHeader: 'Locations' } },
|
|
767
|
+
} as unknown as typeof window.i18next.store.data;
|
|
768
|
+
temporaryConfigStore.setState({
|
|
769
|
+
config: {
|
|
770
|
+
'@openmrs/esm-login-app': {
|
|
771
|
+
'Translation overrides': { fr: { loginButton: 'Se connecter' }, en: { loginButton: 'Sign in' } },
|
|
772
|
+
},
|
|
773
|
+
},
|
|
774
|
+
});
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
afterEach(() => vi.restoreAllMocks());
|
|
778
|
+
|
|
779
|
+
const pickNamespace = async (user: ReturnType<typeof userEvent.setup>) => {
|
|
780
|
+
await user.click(screen.getByRole('combobox', { name: /namespace/i }));
|
|
781
|
+
await user.click(screen.getByRole('option', { name: '@openmrs/esm-login-app' }));
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
it('waits for a single namespace to be picked', () => {
|
|
785
|
+
render(<Translations />);
|
|
786
|
+
|
|
787
|
+
expect(screen.getByRole('button', { name: /download overrides/i })).toBeDisabled();
|
|
788
|
+
expect(screen.getByRole('button', { name: /download for transifex/i })).toBeDisabled();
|
|
789
|
+
expect(screen.getByText(/pick a namespace to export/i)).toBeInTheDocument();
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
it('downloads the namespace’s edits as a config file', async () => {
|
|
793
|
+
const user = userEvent.setup();
|
|
794
|
+
render(<Translations />);
|
|
795
|
+
await pickNamespace(user);
|
|
796
|
+
|
|
797
|
+
await user.click(screen.getByRole('button', { name: /download overrides/i }));
|
|
798
|
+
|
|
799
|
+
expect(downloads[0].name).toBe('openmrs-esm-login-app.fr.overrides.json');
|
|
800
|
+
// Only this language's edits, and nothing the user did not change.
|
|
801
|
+
await expect(downloads[0].content).resolves.toEqual({
|
|
802
|
+
'@openmrs/esm-login-app': { 'Translation overrides': { fr: { loginButton: 'Se connecter' } } },
|
|
803
|
+
});
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
it('downloads the namespace’s edits as a Transifex translation file', async () => {
|
|
807
|
+
const user = userEvent.setup();
|
|
808
|
+
render(<Translations />);
|
|
809
|
+
await pickNamespace(user);
|
|
810
|
+
|
|
811
|
+
await user.click(screen.getByRole('button', { name: /download for transifex/i }));
|
|
812
|
+
|
|
813
|
+
expect(downloads[0].name).toBe('fr.json');
|
|
814
|
+
await expect(downloads[0].content).resolves.toEqual({ loginButton: 'Se connecter' });
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
it('does not offer a Transifex file for the source language', async () => {
|
|
818
|
+
const user = userEvent.setup();
|
|
819
|
+
render(<Translations />);
|
|
820
|
+
await pickNamespace(user);
|
|
821
|
+
await user.click(screen.getByRole('combobox', { name: /language/i }));
|
|
822
|
+
await user.click(screen.getByRole('option', { name: 'en' }));
|
|
823
|
+
|
|
824
|
+
expect(screen.getByRole('button', { name: /download overrides/i })).toBeEnabled();
|
|
825
|
+
expect(screen.getByRole('button', { name: /download for transifex/i })).toBeDisabled();
|
|
826
|
+
expect(screen.getByText(/source language in Transifex/i)).toBeInTheDocument();
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
it('has nothing to export for a namespace without edits in this language', async () => {
|
|
830
|
+
temporaryConfigStore.setState({ config: {} });
|
|
831
|
+
const user = userEvent.setup();
|
|
832
|
+
render(<Translations />);
|
|
833
|
+
await pickNamespace(user);
|
|
834
|
+
|
|
835
|
+
expect(screen.getByRole('button', { name: /download overrides/i })).toBeDisabled();
|
|
836
|
+
expect(screen.getByText(/no changes have been made/i)).toBeInTheDocument();
|
|
837
|
+
});
|
|
838
|
+
});
|
|
839
|
+
});
|