Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add uiLang parameter #714

Merged
merged 8 commits into from
Sep 13, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 12 additions & 4 deletions packages/x-components/src/x-installer/api/api.types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { RequiredProperties } from '@empathyco/x-utils';
import { XBus } from '../../plugins/x-bus.types';
import { DocumentDirection } from '../../plugins/x-plugin.types';
import { XEvent, XEventPayload } from '../../wiring/events.types';
Expand Down Expand Up @@ -94,13 +95,13 @@ export interface SnippetConfig {
/** Customer instance. */
instance: string;
/** Backend services environment. */
env?: 'live' | 'staging';
env?: 'staging';
/** Execution scope (desktop, mobile, app, ...). */
scope: string;
/** Language to display. */
/** Language for the API request, and default value for {@link SnippetConfig.uiLang}. */
lang: string;
/** Language to send to backend services. */
searchLang?: string;
/** Language to use for the messages. Defaults to {@link SnippetConfig.lang}. */
uiLang?: string;
/** User GDPR consent. */
consent?: boolean;
/** Document direction. */
Expand All @@ -121,6 +122,13 @@ export interface SnippetConfig {
[extra: string]: unknown;
}

/**
* A normalised version of the snippet config.
*
* @public
*/
export type NormalisedSnippetConfig = RequiredProperties<SnippetConfig, 'uiLang'>;

/**
* Information to render a query preview with.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { XComponentsAdapterDummy } from '../../../__tests__/adapter.dummy';
import { AnyXModule } from '../../../x-modules/x-modules.types';
import { InitWrapper, InstallXOptions } from '../types';
import { XInstaller } from '../x-installer';
import { SnippetConfig } from '../../api/index';

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

const snippetConfig = {
const getMinimumSnippetConfig = (): SnippetConfig => ({
instance: 'test',
lang: 'test',
scope: 'test'
};
});

/**
* Creates a Vue component injecting the snippet config.
*
* @param snippetProperty
* @returns A Vue component with the injected snippet config.
*
* @internal
*/
function createSnippetConfigComponent(
snippetProperty: keyof SnippetConfig = 'instance'
): VueConstructor {
return Vue.extend({
inject: ['snippetConfig'],
render(h) {
return h('h1', [(this as any).snippetConfig[snippetProperty]]);
}
});
}

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

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

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

expect(window.InterfaceX).toBeDefined();
delete window.InterfaceX;
});

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

expect(window.InterfaceX).not.toBeDefined();
});

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

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

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

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

expect(app).toHaveProperty('testMethod');
expect(app).toHaveProperty('$router');
Expand All @@ -128,57 +150,57 @@ describe('testing `XInstaller` utility', () => {
};
},
app: component
}).init(snippetConfig);
}).init(getMinimumSnippetConfig());

expect(app).toHaveProperty('$router');
expect(app).toHaveProperty('bus');
expect(app).toHaveProperty('snippet', snippetConfig);
expect(app).toHaveProperty('snippet', { ...getMinimumSnippetConfig(), uiLang: 'test' });
});

it('initializes the app with the provided snippet config', async () => {
const vue = createLocalVue();
const { app } = await new XInstaller({
const { app, api } = await new XInstaller({
adapter,
vue,
app: injectSnippetConfigComponent()
}).init(snippetConfig);
app: createSnippetConfigComponent()
}).init(getMinimumSnippetConfig());

expect(app?.$el).toHaveTextContent(snippetConfig.instance);
expect(app?.$el).toHaveTextContent(getMinimumSnippetConfig().instance);

snippetConfig.instance = 'test-2';
api?.setSnippetConfig({ instance: 'test-2' });
await vue.nextTick();
expect(app?.$el).toHaveTextContent('test-2');
});

it('initializes the app when window.initX has the snippet config object', async () => {
const vue = createLocalVue();
window.initX = snippetConfig;
const { app } = (await new XInstaller({
window.initX = getMinimumSnippetConfig();
const { app, api } = (await new XInstaller({
adapter,
vue,
app: injectSnippetConfigComponent()
app: createSnippetConfigComponent()
}).init()) as InitWrapper;

expect(app?.$el).toHaveTextContent(snippetConfig.instance);
expect(app?.$el).toHaveTextContent(getMinimumSnippetConfig().instance);

snippetConfig.instance = 'test-2';
api?.setSnippetConfig({ instance: 'test-2' });
await vue.nextTick();
expect(app?.$el).toHaveTextContent('test-2');
});

// eslint-disable-next-line max-len
it('initializes the app when window.initX is a function retrieving the snippet config', async () => {
const vue = createLocalVue();
window.initX = () => snippetConfig;
const { app } = (await new XInstaller({
window.initX = () => getMinimumSnippetConfig();
const { app, api } = (await new XInstaller({
adapter,
vue,
app: injectSnippetConfigComponent()
app: createSnippetConfigComponent()
}).init()) as InitWrapper;

expect(app?.$el).toHaveTextContent(snippetConfig.instance);
expect(app?.$el).toHaveTextContent(getMinimumSnippetConfig().instance);

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

/**
* Creates a Vue component injecting the snippet config.
*
* @returns A Vue component with the injected snippet config.
*
* @internal
*/
function injectSnippetConfigComponent(): VueConstructor {
return Vue.extend({
inject: ['snippetConfig'],
render(h) {
// Vue does not provide type safety for inject
const instance = (this as any).snippetConfig.instance;
return h('h1', [instance]);
}
describe('`lang` & `uiLang`', () => {
it('provides a `uiLang` by default', async () => {
const { app } = await new XInstaller({
adapter,
plugin,
vue: createLocalVue(),
app: createSnippetConfigComponent('uiLang')
}).init({ ...getMinimumSnippetConfig(), lang: 'en' });

expect(app?.$el).toHaveTextContent('en');
});

it('respects user `uiLang` value', async () => {
const { app } = await new XInstaller({
adapter,
plugin,
vue: createLocalVue(),
app: createSnippetConfigComponent('uiLang')
}).init({ ...getMinimumSnippetConfig(), lang: 'en', uiLang: 'it' });
expect(app?.$el).toHaveTextContent('it');
});

it('updates `uiLang` when `lang` is changed', async () => {
const vue = createLocalVue();
const { app, api } = await new XInstaller({
adapter,
plugin,
vue,
app: createSnippetConfigComponent('uiLang')
}).init({ ...getMinimumSnippetConfig(), lang: 'en', uiLang: 'it' });
expect(app?.$el).toHaveTextContent('it');

api!.setSnippetConfig({ lang: 'es' });
await vue.nextTick();
expect(app?.$el).toHaveTextContent('es');

api!.setSnippetConfig({ uiLang: 'en' });
await vue.nextTick();
expect(app?.$el).toHaveTextContent('en');

api!.setSnippetConfig({ lang: 'fr', uiLang: 'it' });
await vue.nextTick();
expect(app?.$el).toHaveTextContent('it');
});
});
}
});
37 changes: 27 additions & 10 deletions packages/x-components/src/x-installer/x-installer/x-installer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { BaseXBus } from '../../plugins/x-bus';
import { XBus } from '../../plugins/x-bus.types';
import { XPlugin } from '../../plugins/x-plugin';
import { XPluginOptions } from '../../plugins/x-plugin.types';
import { SnippetConfig, XAPI } from '../api/api.types';
import { NormalisedSnippetConfig, SnippetConfig, XAPI } from '../api/api.types';
import { BaseXAPI } from '../api/base-api';
import { InitWrapper, InstallXOptions, VueConstructorPartialArgument } from './types';

Expand Down Expand Up @@ -52,7 +52,7 @@ declare global {
* ```
*
* 2.3 When the script of the project build is loaded it searches for a global `initX`
* variable that the customer must have in their web site. This variable can be a
* variable that the customer must have in their website. This variable can be a
* function that returns the {@link SnippetConfig} or an object that contains the
* {@link SnippetConfig} itself:
*
Expand All @@ -63,7 +63,7 @@ declare global {
* env,
* scope,
* lang,
* searchLang,
* uiLang,
* currency,
* consent,
* documentDirection
Expand All @@ -77,7 +77,7 @@ declare global {
* env,
* scope,
* lang,
* searchLang,
* uiLang,
* currency,
* consent,
* documentDirection
Expand Down Expand Up @@ -276,14 +276,12 @@ export class XInstaller {
): Vue | undefined {
if (this.options.app !== undefined) {
const vue = this.getVue();
this.snippetConfig = vue.observable(snippetConfig);
this.snippetConfig = vue.observable(this.normaliseSnippetConfig(snippetConfig));
return new vue({
...extraPlugins,
...this.options.vueOptions,
provide() {
return {
snippetConfig
};
provide: {
snippetConfig: this.snippetConfig
javieri-empathy marked this conversation as resolved.
Show resolved Hide resolved
},
store: this.options.store,
el: this.getMountingTarget(this.options.domElement),
Expand All @@ -292,6 +290,25 @@ export class XInstaller {
}
}

protected normaliseSnippetConfig(snippetConfig: SnippetConfig): NormalisedSnippetConfig;
protected normaliseSnippetConfig(snippetConfig: Partial<SnippetConfig>): Partial<SnippetConfig>;
/**
* Transforms the snippet configuration.
* - If `lang` is provided and `uiLang` is not, it sets `uiLang=lang`.
*
* @param snippetConfig - The snippet config to normalise.
* @returns The normalised version of the given snippet config.
* @internal
*/
protected normaliseSnippetConfig(
snippetConfig: SnippetConfig | Partial<SnippetConfig>
): NormalisedSnippetConfig | Partial<SnippetConfig> {
if (snippetConfig.lang) {
snippetConfig.uiLang ??= snippetConfig.lang;
luismmdev marked this conversation as resolved.
Show resolved Hide resolved
}
Comment on lines +300 to +302
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The watcher approach, while ideal to have in one place the side effects of mutating the snippet config brought some issues, as you lost the information of which properties where modified, so instead I opted to do a more imperative approach in the only method that had all the information we needed to make a decision.

return snippetConfig;
}

/**
* It returns the HTML element to mount the Vue Application. If the `domElement` parameter in the
* {@link InstallXOptions} is an Element or a string, then it is used. If it is
Expand Down Expand Up @@ -326,7 +343,7 @@ export class XInstaller {
* @internal
*/
protected updateSnippetConfig(snippetConfig: Partial<SnippetConfig>): void {
forEach(snippetConfig, (name, value) => {
forEach(this.normaliseSnippetConfig(snippetConfig), (name, value) => {
this.getVue().set(this.snippetConfig, name, value);
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ describe('testing snippet config extra params component', () => {
});

// eslint-disable-next-line max-len
it('not emits the ExtraParamsProvided event when any no extra params in the snippet config changes', async () => {
it('does not emit ExtraParamsProvided when any no extra params in the snippet config changes', async () => {
const { wrapper, setSnippetConfig } = renderSnippetConfigExtraParams();
const extraParamsProvidedCallback = jest.fn();

Expand All @@ -114,7 +114,7 @@ describe('testing snippet config extra params component', () => {
})
);

await setSnippetConfig({ searchLang: 'es' });
await setSnippetConfig({ uiLang: 'es' });

expect(extraParamsProvidedCallback).toHaveBeenCalledTimes(1);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
default: (): Array<keyof SnippetConfig> => [
'callbacks',
'productId',
'searchLang',
'uiLang',
'consent',
'documentDirection',
'currency',
Expand Down