Skip to content

Commit f4108d4

Browse files
feat: add uiLang parameter (#714)
EX-6855
1 parent 5dac07f commit f4108d4

File tree

9 files changed

+194
-94
lines changed

9 files changed

+194
-94
lines changed

packages/x-components/src/x-installer/api/api.types.ts

+12-4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { RequiredProperties } from '@empathyco/x-utils';
12
import { XBus } from '../../plugins/x-bus.types';
23
import { DocumentDirection } from '../../plugins/x-plugin.types';
34
import { XEvent, XEventPayload } from '../../wiring/events.types';
@@ -94,13 +95,13 @@ export interface SnippetConfig {
9495
/** Customer instance. */
9596
instance: string;
9697
/** Backend services environment. */
97-
env?: 'live' | 'staging';
98+
env?: 'staging';
9899
/** Execution scope (desktop, mobile, app, ...). */
99100
scope: string;
100-
/** Language to display. */
101+
/** Language for the API request, and default value for {@link SnippetConfig.uiLang}. */
101102
lang: string;
102-
/** Language to send to backend services. */
103-
searchLang?: string;
103+
/** Language to use for the messages. Defaults to {@link SnippetConfig.lang}. */
104+
uiLang?: string;
104105
/** User GDPR consent. */
105106
consent?: boolean;
106107
/** Document direction. */
@@ -121,6 +122,13 @@ export interface SnippetConfig {
121122
[extra: string]: unknown;
122123
}
123124

125+
/**
126+
* A normalised version of the snippet config.
127+
*
128+
* @public
129+
*/
130+
export type NormalisedSnippetConfig = RequiredProperties<SnippetConfig, 'uiLang'>;
131+
124132
/**
125133
* Information to render a query preview with.
126134
*

packages/x-components/src/x-installer/x-installer/__tests__/x-installer.spec.ts

+92-42
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { XComponentsAdapterDummy } from '../../../__tests__/adapter.dummy';
88
import { AnyXModule } from '../../../x-modules/x-modules.types';
99
import { InitWrapper, InstallXOptions } from '../types';
1010
import { XInstaller } from '../x-installer';
11+
import { SnippetConfig } from '../../api/index';
1112

1213
describe('testing `XInstaller` utility', () => {
1314
const adapter = XComponentsAdapterDummy;
@@ -25,11 +26,30 @@ describe('testing `XInstaller` utility', () => {
2526
mounted: jest.fn()
2627
};
2728

28-
const snippetConfig = {
29+
const getMinimumSnippetConfig = (): SnippetConfig => ({
2930
instance: 'test',
3031
lang: 'test',
3132
scope: 'test'
32-
};
33+
});
34+
35+
/**
36+
* Creates a Vue component injecting the snippet config.
37+
*
38+
* @param snippetProperty
39+
* @returns A Vue component with the injected snippet config.
40+
*
41+
* @internal
42+
*/
43+
function createSnippetConfigComponent(
44+
snippetProperty: keyof SnippetConfig = 'instance'
45+
): VueConstructor {
46+
return Vue.extend({
47+
inject: ['snippetConfig'],
48+
render(h) {
49+
return h('h1', [(this as any).snippetConfig[snippetProperty]]);
50+
}
51+
});
52+
}
3353

3454
beforeEach(() => {
3555
delete window.initX;
@@ -45,7 +65,7 @@ describe('testing `XInstaller` utility', () => {
4565
__PRIVATE__xModules,
4666
initialXModules: [initialXModule],
4767
vue: createLocalVue()
48-
}).init(snippetConfig);
68+
}).init(getMinimumSnippetConfig());
4969
const params = xPluginMock.install.mock.calls[0][1];
5070

5171
expect(xPluginMock.install).toHaveBeenCalledTimes(1);
@@ -59,21 +79,23 @@ describe('testing `XInstaller` utility', () => {
5979

6080
it('creates the public API in global scope by default', () => {
6181
delete window.InterfaceX;
62-
new XInstaller({ adapter, plugin, vue: createLocalVue() }).init(snippetConfig);
82+
new XInstaller({ adapter, plugin, vue: createLocalVue() }).init(getMinimumSnippetConfig());
6383

6484
expect(window.InterfaceX).toBeDefined();
6585
delete window.InterfaceX;
6686
});
6787

6888
it('does not create the public API passing the api parameter to false', () => {
69-
new XInstaller({ adapter, plugin, api: false, vue: createLocalVue() }).init(snippetConfig);
89+
new XInstaller({ adapter, plugin, api: false, vue: createLocalVue() }).init(
90+
getMinimumSnippetConfig()
91+
);
7092

7193
expect(window.InterfaceX).not.toBeDefined();
7294
});
7395

7496
it('installs the XPlugin using the passed vue', () => {
7597
const localVue = createLocalVue();
76-
new XInstaller({ adapter, plugin, vue: localVue }).init(snippetConfig);
98+
new XInstaller({ adapter, plugin, vue: localVue }).init(getMinimumSnippetConfig());
7799
const vueParam = xPluginMock.install.mock.calls[0][0];
78100

79101
expect(xPluginMock.install).toHaveBeenCalledTimes(1);
@@ -82,7 +104,7 @@ describe('testing `XInstaller` utility', () => {
82104

83105
it('creates a Vue application using the component passed in the app option', async () => {
84106
await new XInstaller({ adapter, plugin, app: component, vue: createLocalVue() }).init(
85-
snippetConfig
107+
getMinimumSnippetConfig()
86108
);
87109

88110
// eslint-disable-next-line @typescript-eslint/unbound-method
@@ -102,7 +124,7 @@ describe('testing `XInstaller` utility', () => {
102124
vue,
103125
vueOptions,
104126
app: component
105-
}).init(snippetConfig);
127+
}).init(getMinimumSnippetConfig());
106128

107129
expect(app).toHaveProperty('testMethod');
108130
expect(app).toHaveProperty('$router');
@@ -128,57 +150,57 @@ describe('testing `XInstaller` utility', () => {
128150
};
129151
},
130152
app: component
131-
}).init(snippetConfig);
153+
}).init(getMinimumSnippetConfig());
132154

133155
expect(app).toHaveProperty('$router');
134156
expect(app).toHaveProperty('bus');
135-
expect(app).toHaveProperty('snippet', snippetConfig);
157+
expect(app).toHaveProperty('snippet', { ...getMinimumSnippetConfig(), uiLang: 'test' });
136158
});
137159

138160
it('initializes the app with the provided snippet config', async () => {
139161
const vue = createLocalVue();
140-
const { app } = await new XInstaller({
162+
const { app, api } = await new XInstaller({
141163
adapter,
142164
vue,
143-
app: injectSnippetConfigComponent()
144-
}).init(snippetConfig);
165+
app: createSnippetConfigComponent()
166+
}).init(getMinimumSnippetConfig());
145167

146-
expect(app?.$el).toHaveTextContent(snippetConfig.instance);
168+
expect(app?.$el).toHaveTextContent(getMinimumSnippetConfig().instance);
147169

148-
snippetConfig.instance = 'test-2';
170+
api?.setSnippetConfig({ instance: 'test-2' });
149171
await vue.nextTick();
150172
expect(app?.$el).toHaveTextContent('test-2');
151173
});
152174

153175
it('initializes the app when window.initX has the snippet config object', async () => {
154176
const vue = createLocalVue();
155-
window.initX = snippetConfig;
156-
const { app } = (await new XInstaller({
177+
window.initX = getMinimumSnippetConfig();
178+
const { app, api } = (await new XInstaller({
157179
adapter,
158180
vue,
159-
app: injectSnippetConfigComponent()
181+
app: createSnippetConfigComponent()
160182
}).init()) as InitWrapper;
161183

162-
expect(app?.$el).toHaveTextContent(snippetConfig.instance);
184+
expect(app?.$el).toHaveTextContent(getMinimumSnippetConfig().instance);
163185

164-
snippetConfig.instance = 'test-2';
186+
api?.setSnippetConfig({ instance: 'test-2' });
165187
await vue.nextTick();
166188
expect(app?.$el).toHaveTextContent('test-2');
167189
});
168190

169191
// eslint-disable-next-line max-len
170192
it('initializes the app when window.initX is a function retrieving the snippet config', async () => {
171193
const vue = createLocalVue();
172-
window.initX = () => snippetConfig;
173-
const { app } = (await new XInstaller({
194+
window.initX = () => getMinimumSnippetConfig();
195+
const { app, api } = (await new XInstaller({
174196
adapter,
175197
vue,
176-
app: injectSnippetConfigComponent()
198+
app: createSnippetConfigComponent()
177199
}).init()) as InitWrapper;
178200

179-
expect(app?.$el).toHaveTextContent(snippetConfig.instance);
201+
expect(app?.$el).toHaveTextContent(getMinimumSnippetConfig().instance);
180202

181-
snippetConfig.instance = 'test-2';
203+
api?.setSnippetConfig({ instance: 'test-2' });
182204
await vue.nextTick();
183205
expect(app?.$el).toHaveTextContent('test-2');
184206
});
@@ -187,22 +209,50 @@ describe('testing `XInstaller` utility', () => {
187209
it('does not initialize XComponents when no snippet config is passed and no window.initX is not defined', async () => {
188210
expect(await new XInstaller({ adapter, plugin, vue: createLocalVue() }).init()).toBeUndefined();
189211
});
190-
});
191212

192-
/**
193-
* Creates a Vue component injecting the snippet config.
194-
*
195-
* @returns A Vue component with the injected snippet config.
196-
*
197-
* @internal
198-
*/
199-
function injectSnippetConfigComponent(): VueConstructor {
200-
return Vue.extend({
201-
inject: ['snippetConfig'],
202-
render(h) {
203-
// Vue does not provide type safety for inject
204-
const instance = (this as any).snippetConfig.instance;
205-
return h('h1', [instance]);
206-
}
213+
describe('`lang` & `uiLang`', () => {
214+
it('provides a `uiLang` by default', async () => {
215+
const { app } = await new XInstaller({
216+
adapter,
217+
plugin,
218+
vue: createLocalVue(),
219+
app: createSnippetConfigComponent('uiLang')
220+
}).init({ ...getMinimumSnippetConfig(), lang: 'en' });
221+
222+
expect(app?.$el).toHaveTextContent('en');
223+
});
224+
225+
it('respects user `uiLang` value', async () => {
226+
const { app } = await new XInstaller({
227+
adapter,
228+
plugin,
229+
vue: createLocalVue(),
230+
app: createSnippetConfigComponent('uiLang')
231+
}).init({ ...getMinimumSnippetConfig(), lang: 'en', uiLang: 'it' });
232+
expect(app?.$el).toHaveTextContent('it');
233+
});
234+
235+
it('updates `uiLang` when `lang` is changed', async () => {
236+
const vue = createLocalVue();
237+
const { app, api } = await new XInstaller({
238+
adapter,
239+
plugin,
240+
vue,
241+
app: createSnippetConfigComponent('uiLang')
242+
}).init({ ...getMinimumSnippetConfig(), lang: 'en', uiLang: 'it' });
243+
expect(app?.$el).toHaveTextContent('it');
244+
245+
api!.setSnippetConfig({ lang: 'es' });
246+
await vue.nextTick();
247+
expect(app?.$el).toHaveTextContent('es');
248+
249+
api!.setSnippetConfig({ uiLang: 'en' });
250+
await vue.nextTick();
251+
expect(app?.$el).toHaveTextContent('en');
252+
253+
api!.setSnippetConfig({ lang: 'fr', uiLang: 'it' });
254+
await vue.nextTick();
255+
expect(app?.$el).toHaveTextContent('it');
256+
});
207257
});
208-
}
258+
});

packages/x-components/src/x-installer/x-installer/types.ts

+31-16
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ComponentOptions, PluginObject, VueConstructor } from 'vue';
22
import { XBus } from '../../plugins/x-bus.types';
33
import { XPluginOptions } from '../../plugins/x-plugin.types';
4-
import { SnippetConfig, XAPI } from '../api/api.types';
4+
import { NormalisedSnippetConfig, XAPI } from '../api/api.types';
55

66
/**
77
* Interface for the parameter of the constructor of {@link XInstaller} function. It is an extended
@@ -10,25 +10,37 @@ import { SnippetConfig, XAPI } from '../api/api.types';
1010
* @public
1111
*/
1212
export interface InstallXOptions<API extends XAPI = XAPI> extends XPluginOptions {
13-
/** The Vue component used as root of the application. If is not passed no Vue Application is
14-
* initialized, only plugin installed. */
13+
/**
14+
* The Vue component used as root of the application. If it is not passed, no Vue Application is
15+
* initialized, only plugin installed.
16+
*/
1517
app?: VueConstructor | ComponentOptions<Vue>;
16-
/** The API to expose globally. If is not passed the default {@link BaseXAPI} will be used. If
17-
* a `false` value is passed then the API is not created.*/
18+
/**
19+
* The API to expose globally. If is not passed the default {@link BaseXAPI} will be used. If
20+
* a `false` value is passed then the API is not created.
21+
*/
1822
api?: API | false;
19-
/** The {@link XBus} used in the {@link XPlugin}. If not passed an instance of {@link BaseXBus}
20-
* will be used.*/
23+
/**
24+
* The {@link XBus} used in the {@link XPlugin}. If not passed an instance of {@link BaseXBus}
25+
* will be used.
26+
*/
2127
bus?: XBus;
22-
/** An Element | string to indicate the HTML element that will contain the Vue
28+
/**
29+
* An Element | string to indicate the HTML element that will contain the Vue
2330
* application. If string selector is passed and the element doesn't exits, the
24-
* {@link XInstaller} will create it. */
31+
* {@link XInstaller} will create it.
32+
*/
2533
domElement?: Element | string;
26-
/** The XPlugin which will be installed. If not passed, an instance of {@link XPlugin} will be
27-
* installed.*/
34+
/**
35+
* The XPlugin which will be installed. If not passed, an instance of {@link XPlugin} will be
36+
* installed.
37+
*/
2838
plugin?: PluginObject<XPluginOptions>;
29-
/** The Vue instance used to install the plugin and to create the Application. If not
39+
/**
40+
* The Vue instance used to install the plugin and to create the Application. If not
3041
* passed the default Vue instance is used. This can be useful to use the `localVue`
31-
* in the unit tests.*/
42+
* in the unit tests.
43+
*/
3244
vue?: VueConstructor;
3345
/**
3446
* This object can contain any option to pass to Vue instance at the moment of creating the App
@@ -45,6 +57,7 @@ export interface InstallXOptions<API extends XAPI = XAPI> extends XPluginOptions
4557
* ```
4658
*/
4759
vueOptions?: VueConstructorPartialArgument;
60+
4861
/**
4962
* Adds the option to install more Vue plugins, giving access to the {@link SnippetConfig} and
5063
* the {@link XBus}.
@@ -68,9 +81,11 @@ export interface ExtraPluginsOptions {
6881
vue: VueConstructor;
6982
/** The events bus instance used to communicate different part of the x-components. */
7083
bus: XBus;
71-
/** Configuration coming from the client website with options like the lang, or the active
72-
* currency. */
73-
snippet: SnippetConfig;
84+
/**
85+
* Configuration coming from the client website with options like the lang, or the active
86+
* currency.
87+
*/
88+
snippet: NormalisedSnippetConfig;
7489
}
7590

7691
/**

0 commit comments

Comments
 (0)