From 32d6083d764d65dc56585fdef733c79c03b5ef6b Mon Sep 17 00:00:00 2001 From: Matthew Oliveira Date: Thu, 18 Jan 2024 18:46:10 -0500 Subject: [PATCH] [Locale API]: Adjust fallback layer logic (#11404) * test(locale): restore sut consistently * test(locale): more accurately define falsy return for ipcinfoCookie.get * test(locale): fix base level test for getLangDisplay * test(locale): adjust assertions based on consistent mocks * test(locale): fix assumption on default html lang test * test(locale): update tests for new fallback logic * feat(locale): updated locale fallback logic * feat(locale): update JSDoc return * test(global): fix global test by mocking LocaleAPI * test(searchTypeahead): fix and improve test by mocking LocaleAPI * test(locale): add a test case for then the DDOAPI rejects * fix(locale): cover case when DDOAPI rejects --------- Co-authored-by: kennylam <909118+kennylam@users.noreply.github.com> --- .../services/src/services/Locale/Locale.js | 226 ++++++++----- .../services/Locale/__tests__/Locale.test.js | 316 ++++++++++-------- ...{response.json => countries_response.json} | 0 .../Locale/__tests__/data/ddo_response.json | 247 ++++++++++++++ .../__tests__/SearchTypeahead.test.js | 29 +- .../services/global/__tests__/global.test.js | 7 +- 6 files changed, 582 insertions(+), 243 deletions(-) rename packages/services/src/services/Locale/__tests__/data/{response.json => countries_response.json} (100%) create mode 100644 packages/services/src/services/Locale/__tests__/data/ddo_response.json diff --git a/packages/services/src/services/Locale/Locale.js b/packages/services/src/services/Locale/Locale.js index 094205c99ff..ed02ccadc53 100644 --- a/packages/services/src/services/Locale/Locale.js +++ b/packages/services/src/services/Locale/Locale.js @@ -1,5 +1,5 @@ /** - * Copyright IBM Corp. 2020, 2023 + * Copyright IBM Corp. 2020, 2024 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. @@ -11,7 +11,13 @@ import ipcinfoCookie from '../../internal/vendor/@carbon/ibmdotcom-utilities/uti import root from 'window-or-global'; /** - * @constant {string | string} Host for the Locale API call + * @typedef {object} Locale + * @property {string} cc The country code. + * @property {string} lc The language code. + */ + +/** + * @constant {string | string} Host for the Locale API call. * @private */ const _host = @@ -20,9 +26,9 @@ const _host = 'https://1.www.s81c.com'; /** - * Sets the default location if nothing is returned + * Sets the default location if nothing is returned. * - * @type {object} + * @type {Locale} * @private */ const _localeDefault = { @@ -31,7 +37,7 @@ const _localeDefault = { }; /** - * Default display name for lang combination + * Default display name for lang combination. * * @type {string} * @private @@ -39,7 +45,7 @@ const _localeDefault = { const _localeNameDefault = 'United States — English'; /** - * Locale API endpoint + * Locale API endpoint. * * @type {string} * @private @@ -47,7 +53,7 @@ const _localeNameDefault = 'United States — English'; const _endpoint = `${_host}/common/js/dynamicnav/www/countrylist/jsononly`; /** - * Configuration for axios + * Configuration for axios. * * @type {{headers: {'Content-Type': string}}} * @private @@ -59,7 +65,7 @@ const _axiosConfig = { }; /** - * Session Storage key for country list + * Session Storage key for country list. * * @type {string} * @private @@ -75,38 +81,95 @@ const _sessionListKey = 'cds-countrylist'; const _twoHours = 60 * 60 * 2000; /** - * Use the lang attr to determine a return locale object + * The cache for in-flight or resolved requests for the country list, keyed by + * the initiating locale. * * @type {object} * @private */ -const _getLocaleByLangAttr = () => { +const _requestsList = {}; + +/** + * Retrieves the default locale. + * + * @returns {Locale} The default locale. + */ +const _getLocaleDefault = () => _localeDefault; + +/** + * Use the lang attr to determine a return locale object, or "false" + * when it's not available so the consumer can decide what to do next. + * + * @type {(Locale | boolean)} + * @private + */ +function _getLocaleFromLangAttr() { if (root.document?.documentElement?.lang) { const lang = root.document.documentElement.lang.toLowerCase(); + const locale = {}; if (lang.indexOf('-') === -1) { - return { lc: lang }; + locale.lc = lang; } else { const codes = lang.split('-'); - return { cc: codes[1], lc: codes[0] }; + locale.cc = codes[1]; + locale.lc = codes[0]; } - } else { - return _localeDefault; + return locale; + } + return false; +} + +/** + * Gets the locale from the cookie and returns it if both 'cc' and 'lc' values + * are present. + * + * @async + * @returns {Promise} The cookie object if 'cc' and 'lc' values are present, otherwise false. + */ +const _getLocaleFromCookie = async () => { + const cookie = ipcinfoCookie.get(); + if (cookie && cookie.cc && cookie.lc) { + await LocaleAPI.getList(cookie); + return cookie; } + return false; }; /** - * The cache for in-flight or resolved requests for the country list, keyed by the initiating locale. + * Get the locale from the user's browser. * - * @type {object} - * @private + * @async + * @returns {Promise} The verified locale or false if not found. */ -const _requestsList = {}; +const _getLocaleFromBrowser = async () => { + try { + const cc = await DDOAPI.getLocation(); + + // Language preference from browser can return in either 'en-US' format or + // 'en' so will need to extract language only. + const lang = root.navigator.language; + const lc = lang.split('-')[0]; + + if (cc && lc) { + const list = await LocaleAPI.getList({ cc, lc }); + const verifiedCodes = LocaleAPI.verifyLocale(cc, lc, list); + + // Set the ipcInfo cookie. + ipcinfoCookie.set(verifiedCodes); + + return verifiedCodes; + } + } catch (e) { + // Intentionally throw away the exception in favor of returning false. + } + return false; +}; /** - * Return a locale object based on the DDO API, or "false" - * so the consumer can decide what to do next + * Return a locale object based on the DDO API, or "false" so the consumer can + * decide what to do next. * - * @type {(object | boolean)} + * @returns {(Locale | boolean)} Locale from the DDO, or "false" if not present. * @private */ function _getLocaleFromDDO() { @@ -150,8 +213,7 @@ function _getLocaleFromDDO() { } /** - * Locale API class with method of fetching user's locale for - * ibm.com + * Locale API class with method of fetching user's locale for ibm.com. */ class LocaleAPI { /** @@ -170,14 +232,18 @@ class LocaleAPI { } /** - * Gets the user's locale + * Gets the user's locale. + * + * Grab the locale from the available information on the page in the following + * order: * - * Grab the locale from the `lang` attribute from html, else - * check if ipcinfo cookie exists (ipcinfoCookie util) - * if not, retrieve the user's locale through DDO service + gets user's - * browser language preference then set the cookie + * 1. DDO + * 2. HTML lang attribute + * 3. ipcInfo cookie + * 4. Browser (navigator.language) + * 5. Default (us-EN) * - * @returns {object} object with lc and cc + * @returns {Promise} Locale object. * @example * import { LocaleAPI } from '@carbon/ibmdotcom-services'; * @@ -187,69 +253,46 @@ class LocaleAPI { * } */ static async getLocale() { - const cookie = ipcinfoCookie.get(); - const lang = await this.getLang(); - - if (lang) { - return lang; - } - // grab the locale from the cookie - else if (cookie && cookie.cc && cookie.lc) { - await this.getList(cookie); - return cookie; - } else { - const cc = await DDOAPI.getLocation(); - /** - * get language preference from browser - * can return in either 'en-US' format or 'en' so will need to extract language only - */ - const lang = root.navigator.language; - const lc = lang.split('-')[0]; - - if (cc && lc) { - const list = await this.getList({ cc, lc }); - const verifiedCodes = this.verifyLocale(cc, lc, list); - - // set the ipcInfo cookie - ipcinfoCookie.set(verifiedCodes); - - return verifiedCodes; + const localeGetters = [ + _getLocaleFromDDO, + _getLocaleFromLangAttr, + _getLocaleFromCookie, + _getLocaleFromBrowser, + ]; + for (const getter of localeGetters) { + const locale = await getter(); + if (locale) { + return locale; } } + return _getLocaleDefault(); } /** - * Checks for DDO object to return the correct cc and lc - * Otherwise gets those values from the lang attribute + * Gets the user's locale. * - * @returns {object} locale object + * @returns {Promise} Locale object. * @example * import { LocaleAPI } from '@carbon/ibmdotcom-services'; * * function async getLocale() { * const locale = await LocaleAPI.getLang(); * } + * + * @deprecated in favor of LocalAPI.getLocale. */ - static getLang() { - return new Promise((resolve) => { - const getLocaleFromDDO = _getLocaleFromDDO(); - - if (getLocaleFromDDO) { - resolve(getLocaleFromDDO); - } else { - resolve(_getLocaleByLangAttr()); - } - }); + static async getLang() { + return this.getLocale(); } /** - * This fetches the language display name based on language/locale combo + * This fetches the language display name based on locale. * - * @param {object} langCode lang code with cc and lc - * @returns {Promise} Display name of locale/language + * @param {(Locale | boolean)} locale (optional) If not given, uses LocaleAPI.getLocale logic. + * @returns {Promise} Display name of locale/language. */ - static async getLangDisplay(langCode) { - const lang = langCode ? langCode : await this.getLang(); + static async getLangDisplay(locale) { + const lang = locale ? locale : await this.getLocale(); const list = await this.getList(lang); // combines the countryList arrays let countries = []; @@ -281,12 +324,12 @@ class LocaleAPI { /** * Get the country list of all supported countries and their languages - * if it is not already stored in session storage + * if it is not already stored in session storage. * - * @param {object} params params object - * @param {string} params.cc country code - * @param {string} params.lc language code - * @returns {Promise} promise object + * @param {Locale} locale Locale object. + * @param {string} locale.cc Country code. + * @param {string} locale.lc Language code. + * @returns {Promise} Promise object. * @example * import { LocaleAPI } from '@carbon/ibmdotcom-services'; * @@ -302,12 +345,12 @@ class LocaleAPI { } /** - * Fetches the list data based on cc/lc combination + * Fetches the list data based on cc/lc combination. * - * @param {string} cc country code - * @param {string} lc language code - * @param {Function} resolve resolves the Promise - * @param {Function} reject rejects the promise + * @param {string} cc Country code. + * @param {string} lc Language code. + * @param {Function} resolve Resolves the Promise. + * @param {Function} reject Rejects the promise. */ static fetchList(cc, lc, resolve, reject) { const key = cc !== 'undefined' ? `${lc}-${cc}` : `${lc}`; @@ -345,13 +388,12 @@ class LocaleAPI { } /** - * Verify that the cc and lc combo is in the list of - * supported cc-lc combos + * Verify that the cc and lc combo is in the list of supported cc-lc combos. * - * @param {string} cc country code - * @param {string} lc language code - * @param {object} list country list - * @returns {object} object with lc and cc + * @param {string} cc Country code. + * @param {string} lc Language code. + * @param {object} list Country list. + * @returns {object} Object with lc and cc. * @example * import { LocaleAPI } from '@carbon/ibmdotcom-services'; * @@ -369,8 +411,8 @@ class LocaleAPI { list.regionList.forEach((region) => region.countryList.forEach((country) => { const code = country.locale[0][0].split('-'); - const countryCode = code[1]; const languageCode = code[0]; + const countryCode = code[1]; if (countryCode === cc && languageCode === lc) { locale = { cc, lc }; } @@ -389,7 +431,7 @@ class LocaleAPI { /** * Retrieves session cache and checks if cache needs to be refreshed * - * @param {string} key session storage key + * @param {string} key Session storage key. */ static getSessionCache(key) { const session = diff --git a/packages/services/src/services/Locale/__tests__/Locale.test.js b/packages/services/src/services/Locale/__tests__/Locale.test.js index ec4feef65a1..18102d5ea12 100644 --- a/packages/services/src/services/Locale/__tests__/Locale.test.js +++ b/packages/services/src/services/Locale/__tests__/Locale.test.js @@ -1,61 +1,78 @@ /** - * Copyright IBM Corp. 2020, 2023 + * Copyright IBM Corp. 2020, 2024 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ -import { ipcinfoCookie } from '../../../internal/vendor/@carbon/ibmdotcom-utilities'; -import digitalDataResponse from '../../DDO/__tests__/data/response.json'; +import ipcinfoCookie from '../../../internal/vendor/@carbon/ibmdotcom-utilities/utilities/ipcinfoCookie/ipcinfoCookie'; import LocaleAPI from '../Locale'; import { DDOAPI } from '../../DDO'; import mockAxios from 'axios'; -import oldSession from './data/timestamp_response.json'; -import response from './data/response.json'; import root from 'window-or-global'; +import countriesResponse from './data/countries_response.json'; +import digitalDataResponse from './data/ddo_response.json'; +import oldSession from './data/timestamp_response.json'; const mockDigitalDataResponse = digitalDataResponse; jest.mock( - '../../../internal/vendor/@carbon/ibmdotcom-utilities/utilities/ipcinfoCookie/ipcinfoCookie', - () => ({ - get: jest.fn(() => ({ cc: 'us', lc: 'en' })), - set: jest.fn(() => ({})), - }) + '../../../internal/vendor/@carbon/ibmdotcom-utilities/utilities/ipcinfoCookie/ipcinfoCookie' ); - -jest.mock('../../DDO', () => ({ - DDOAPI: { - getLocation: jest.fn(() => Promise.resolve('us')), - }, -})); +jest.mock('../../DDO'); describe('LocaleAPI', () => { const handles = []; beforeEach(function () { mockAxios.get.mockImplementation(async () => ({ - data: response, + data: countriesResponse, })); - root.digitalData = mockDigitalDataResponse; + // Setup all fallback levels to initially be set to Brazilian Portuguese. + // We intentionally avoid en-US to avoid matching the default fallback and + // producing false positives. - Object.defineProperty(window.document.documentElement, 'lang', { - value: '', + // Mock DDO includes: + // page.pageInfo.language === 'pt-BR' + // page.pageInfo.ibm.country === 'br' + root.digitalData = JSON.parse(JSON.stringify(mockDigitalDataResponse)); + + Object.defineProperty(root.document.documentElement, 'lang', { + value: 'pt', configurable: true, }); + ipcinfoCookie.get.mockImplementation(() => ({ cc: 'br', lc: 'pt' })); + + DDOAPI.getLocation.mockImplementation(() => Promise.resolve('br')); + + Object.defineProperty(root, 'navigator', { + value: { language: 'pt-BR' }, + writable: true, + }); + LocaleAPI.clearCache(); }); - it('should fetch the lang from the html attribute when there is no ddo defined', async () => { - root.digitalData.page = false; + afterEach(() => { + for (let handle = handles.pop(); handle; handle = handles.pop()) { + handle.release(); + } + // Restore any mocks back to a predictable state. + jest.restoreAllMocks(); + mockAxios.get.mockRestore(); + ipcinfoCookie.get.mockRestore(); + DDOAPI.getLocation.mockRestore(); + }); - Object.defineProperty(window.document.documentElement, 'lang', { - value: 'fr-ca', + it('should fallback to the locale from the html lang attribute (cc and lc) when there is no ddo defined', async () => { + root.digitalData.page = false; + Object.defineProperty(root.document.documentElement, 'lang', { + value: 'fr-CA', configurable: true, }); - const lang = await LocaleAPI.getLang(); + const lang = await LocaleAPI.getLocale(); expect(lang).toEqual({ cc: 'ca', @@ -63,20 +80,21 @@ describe('LocaleAPI', () => { }); }); - it('should default to value from the html lang attribute if cc and lc are not defined', async () => { - Object.defineProperty(window.document.documentElement, 'lang', { + it('should fallback to the lang from the html lang attribute (lc only) when there is no ddo defined', async () => { + root.digitalData.page = false; + Object.defineProperty(root.document.documentElement, 'lang', { value: 'it', configurable: true, }); - const lang = await LocaleAPI.getLang(); + const lang = await LocaleAPI.getLocale(); expect(lang).toEqual({ lc: 'it', }); }); - it('should return only lc when only lc is present in ddo language', async () => { + it('should fallback to only lc when only lc is present in ddo language', async () => { root.digitalData = { page: { pageInfo: { @@ -85,16 +103,57 @@ describe('LocaleAPI', () => { }, }; - const lang = await LocaleAPI.getLang(); + const lang = await LocaleAPI.getLocale(); expect(lang).toEqual({ lc: 'es', }); }); - it('should default when ddo and lang are undefined', async () => { + it('should fallback to the cookie locale when ddo and lang are undefined', async () => { root.digitalData = undefined; - const lang = await LocaleAPI.getLang(); + Object.defineProperty(root.document.documentElement, 'lang', { + value: '', + configurable: true, + }); + ipcinfoCookie.get.mockImplementationOnce(() => ({ cc: 'ca', lc: 'fr' })); + + const lang = await LocaleAPI.getLocale(); + + expect(lang).toEqual({ cc: 'ca', lc: 'fr' }); + }); + + it('should fallback to browser locale when ddo, lang, and cookie are undefined', async () => { + root.digitalData = undefined; + Object.defineProperty(root.document.documentElement, 'lang', { + value: '', + configurable: true, + }); + ipcinfoCookie.get.mockImplementationOnce(() => undefined); + DDOAPI.getLocation.mockImplementation(() => Promise.resolve('fr')); + Object.defineProperty(root, 'navigator', { + value: { language: 'fr-FR' }, + writable: true, + }); + + const lang = await LocaleAPI.getLocale(); + + expect(lang).toEqual({ cc: 'fr', lc: 'fr' }); + }); + + it('should fallback to default when ddo, lang, cookie, and browser are undefined', async () => { + root.digitalData = undefined; + Object.defineProperty(root.document.documentElement, 'lang', { + value: '', + configurable: true, + }); + ipcinfoCookie.get.mockImplementationOnce(() => undefined); + Object.defineProperty(root, 'navigator', { + value: { language: '' }, + writable: true, + }); + + const lang = await LocaleAPI.getLocale(); expect(lang).toEqual({ cc: 'us', @@ -102,9 +161,20 @@ describe('LocaleAPI', () => { }); }); - it('should default when no ddo.page', async () => { - root.digitalData.page = false; - const lang = await LocaleAPI.getLang(); + it('should fallback to default when ddo, lang, cookie, and browser are undefined and the DDOAPI timeouts', async () => { + root.digitalData = undefined; + Object.defineProperty(root.document.documentElement, 'lang', { + value: '', + configurable: true, + }); + ipcinfoCookie.get.mockImplementationOnce(() => undefined); + DDOAPI.getLocation.mockImplementation(() => Promise.reject(new Error())); + Object.defineProperty(root, 'navigator', { + value: { language: '' }, + writable: true, + }); + + const lang = await LocaleAPI.getLocale(); expect(lang).toEqual({ cc: 'us', @@ -112,21 +182,41 @@ describe('LocaleAPI', () => { }); }); - it('should default when no ddo.page.pageInfo', async () => { + it('should fallback to the locale from the html lang attribute when no ddo.page', async () => { + root.digitalData.page = false; + Object.defineProperty(root.document.documentElement, 'lang', { + value: 'fr-CA', + configurable: true, + }); + + const lang = await LocaleAPI.getLocale(); + + expect(lang).toEqual({ + cc: 'ca', + lc: 'fr', + }); + }); + + it('should fallback to the locale from the html lang attribute when no ddo.page.pageInfo', async () => { root.digitalData = { page: { pageInfo: false, }, }; - const lang = await LocaleAPI.getLang(); + Object.defineProperty(root.document.documentElement, 'lang', { + value: 'fr-CA', + configurable: true, + }); + + const lang = await LocaleAPI.getLocale(); expect(lang).toEqual({ - cc: 'us', - lc: 'en', + cc: 'ca', + lc: 'fr', }); }); - it('should default when no ddo.page.pageInfo.ibm', async () => { + it('should fallback to the locale from the html lang attribute when no ddo.page.pageInfo.ibm', async () => { root.digitalData = { page: { pageInfo: { @@ -134,15 +224,20 @@ describe('LocaleAPI', () => { }, }, }; - const lang = await LocaleAPI.getLang(); + Object.defineProperty(root.document.documentElement, 'lang', { + value: 'fr-CA', + configurable: true, + }); + + const lang = await LocaleAPI.getLocale(); expect(lang).toEqual({ - cc: 'us', - lc: 'en', + cc: 'ca', + lc: 'fr', }); }); - it('should default when no ddo.page.pageInfo.ibm.country', async () => { + it('should fallback to the locale from the html lang attribute when no ddo.page.pageInfo.ibm.country', async () => { root.digitalData = { page: { pageInfo: { @@ -152,15 +247,20 @@ describe('LocaleAPI', () => { }, }, }; - const lang = await LocaleAPI.getLang(); + Object.defineProperty(root.document.documentElement, 'lang', { + value: 'fr-CA', + configurable: true, + }); + + const lang = await LocaleAPI.getLocale(); expect(lang).toEqual({ - cc: 'us', - lc: 'en', + cc: 'ca', + lc: 'fr', }); }); - it('should default when no ddo.page.pageInfo.language', async () => { + it('should fallback to the locale from the html lang attribute when no ddo.page.pageInfo.language', async () => { root.digitalData = { page: { pageInfo: { @@ -171,15 +271,20 @@ describe('LocaleAPI', () => { }, }, }; - const lang = await LocaleAPI.getLang(); + Object.defineProperty(root.document.documentElement, 'lang', { + value: 'fr-CA', + configurable: true, + }); + + const lang = await LocaleAPI.getLocale(); expect(lang).toEqual({ - cc: 'us', - lc: 'en', + cc: 'ca', + lc: 'fr', }); }); - it('should get from DDO', async () => { + it('should get locale from DDO', async () => { root.digitalData = { page: { pageInfo: { @@ -190,7 +295,7 @@ describe('LocaleAPI', () => { }, }, }; - const lang = await LocaleAPI.getLang(); + const lang = await LocaleAPI.getLocale(); expect(lang).toEqual({ cc: 'de', @@ -209,7 +314,7 @@ describe('LocaleAPI', () => { }, }, }; - const lang = await LocaleAPI.getLang(); + const lang = await LocaleAPI.getLocale(); expect(lang).toEqual({ cc: 'de', @@ -228,7 +333,7 @@ describe('LocaleAPI', () => { }, }, }; - const lang = await LocaleAPI.getLang(); + const lang = await LocaleAPI.getLocale(); expect(lang).toEqual({ cc: 'uk', @@ -247,7 +352,7 @@ describe('LocaleAPI', () => { }, }, }; - const lang = await LocaleAPI.getLang(); + const lang = await LocaleAPI.getLocale(); expect(lang).toEqual({ cc: 'us', @@ -267,11 +372,10 @@ describe('LocaleAPI', () => { { headers: { 'Content-Type': 'application/json; charset=utf-8' } } ); - expect(countries).toEqual(response); + expect(countries).toEqual(countriesResponse); }); it('should get countries list from session cache', async () => { - mockAxios.get.mockClear(); const countries1 = await LocaleAPI.getList({ cc: 'testCC', lc: 'testLC', @@ -282,15 +386,14 @@ describe('LocaleAPI', () => { }); expect(mockAxios.get).toHaveBeenCalledTimes(1); - expect(countries1).toEqual(response); - expect(countries2).toEqual(response); + expect(countries1).toEqual(countriesResponse); + expect(countries2).toEqual(countriesResponse); }); - it('should get default countries list on inital reject', async () => { - mockAxios.get.mockClear(); + it('should get default countries list on initial reject', async () => { mockAxios.get .mockReturnValueOnce(Promise.reject()) - .mockReturnValueOnce(Promise.resolve({ data: response })); + .mockReturnValueOnce(Promise.resolve({ data: countriesResponse })); const countries = await LocaleAPI.getList({ cc: 'testCC', @@ -307,7 +410,7 @@ describe('LocaleAPI', () => { { headers: { 'Content-Type': 'application/json; charset=utf-8' } } ); - expect(countries).toEqual(response); + expect(countries).toEqual(countriesResponse); }); it('should reject countries list', async () => { @@ -408,13 +511,16 @@ describe('LocaleAPI', () => { expect(display).toEqual('testName — testDisplay'); }); - it('should get lang for display', async () => { + it('should use getLocale for display with no langCode argument', async () => { + jest.spyOn(LocaleAPI, 'getLocale'); + const display = await LocaleAPI.getLangDisplay(false); - expect(display).toEqual('testName — testDisplay'); + expect(LocaleAPI.getLocale).toHaveBeenCalledTimes(1); + expect(display).toEqual('Brazil — Portuguese'); }); - it('should get default lang display', async () => { + it('should get default lang display for missing locale', async () => { const display = await LocaleAPI.getLangDisplay({ cc: 'missingCC', lc: 'missingLC', @@ -423,74 +529,18 @@ describe('LocaleAPI', () => { expect(display).toEqual('United States — English'); }); - it('should get locale from getLang', async () => { + it('should get locale from getLocale', async () => { jest - .spyOn(LocaleAPI, 'getLang') + .spyOn(LocaleAPI, 'getLocale') .mockReturnValue(Promise.resolve('testLang')); - jest - .spyOn(LocaleAPI, 'getList') - .mockReturnValue(Promise.resolve('testList')); - jest.spyOn(LocaleAPI, 'verifyLocale').mockReturnValue('testVerified'); - const locale = await LocaleAPI.getLocale(); - - expect(locale).toEqual('testLang'); - }); - - it('should get locale from cookies', async () => { - jest.spyOn(LocaleAPI, 'getLang').mockReturnValue(Promise.resolve(false)); - - const locale = await LocaleAPI.getLocale(); - - expect(locale).toEqual({ cc: 'us', lc: 'en' }); - }); - - it('should get locale from DDO on missing cookie', async () => { - ipcinfoCookie.get.mockImplementation(() => false); - - await LocaleAPI.getLocale(); - - expect(DDOAPI.getLocation).toHaveBeenCalledTimes(1); - }); - - it('should get locale from DDO on missing cookie lc', async () => { - DDOAPI.getLocation.mockClear(); - ipcinfoCookie.get.mockImplementation(() => ({ cc: 'testCC' })); - - await LocaleAPI.getLocale(); - - expect(DDOAPI.getLocation).toHaveBeenCalledTimes(1); - }); - - it('should get locale from DDO on missing cookie cc', async () => { - DDOAPI.getLocation.mockClear(); - ipcinfoCookie.get.mockImplementation(() => ({ lc: 'testLC' })); - - await LocaleAPI.getLocale(); - expect(DDOAPI.getLocation).toHaveBeenCalledTimes(1); - }); - - it('should get locale from DDO', async () => { - ipcinfoCookie.set.mockClear(); - ipcinfoCookie.get.mockImplementation(() => false); - - const locale = await LocaleAPI.getLocale(); + const locale = await LocaleAPI.getLang(); - expect(locale).toEqual('testVerified'); - expect(ipcinfoCookie.set).toHaveBeenCalledTimes(1); - expect(ipcinfoCookie.set).toHaveBeenCalledWith('testVerified'); - }); - - it('should get undefined locale on no cc', async () => { - DDOAPI.getLocation.mockImplementation(() => Promise.resolve(false)); - - const locale = await LocaleAPI.getLocale(); - expect(locale).toBeUndefined(); + expect(LocaleAPI.getLocale).toHaveBeenCalledTimes(1); + expect(locale).toEqual('testLang'); }); it('should use the cache for the country list, keyed by locale', async () => { - mockAxios.get.mockClear(); - LocaleAPI.getList.mockRestore(); await LocaleAPI.getList({ cc: 'us', lc: 'en' }); await LocaleAPI.getList({ cc: 'us', lc: 'en' }); await LocaleAPI.getList({ cc: 'kr', lc: 'ko' }); @@ -519,12 +569,12 @@ describe('LocaleAPI', () => { }; })(); - Object.defineProperty(window, 'sessionStorage', { + Object.defineProperty(root, 'sessionStorage', { value: sessionStorageMock, }); const mockDate = 1546300800000; // Epoch time of January 1, 2019 midnight UTC - global.Date.now = jest.fn(() => mockDate); + root.Date.now = jest.fn(() => mockDate); // using very old cached session sessionStorageMock.setItem( @@ -544,10 +594,4 @@ describe('LocaleAPI', () => { // fresh cached data would lack this property expect(newSession).not.toHaveProperty('CACHE'); }); - - afterEach(() => { - for (let handle = handles.pop(); handle; handle = handles.pop()) { - handle.release(); - } - }); }); diff --git a/packages/services/src/services/Locale/__tests__/data/response.json b/packages/services/src/services/Locale/__tests__/data/countries_response.json similarity index 100% rename from packages/services/src/services/Locale/__tests__/data/response.json rename to packages/services/src/services/Locale/__tests__/data/countries_response.json diff --git a/packages/services/src/services/Locale/__tests__/data/ddo_response.json b/packages/services/src/services/Locale/__tests__/data/ddo_response.json new file mode 100644 index 00000000000..8d1eb066b22 --- /dev/null +++ b/packages/services/src/services/Locale/__tests__/data/ddo_response.json @@ -0,0 +1,247 @@ +{ + "page": { + "attributes": { + "pageidQueryStrings": ["q", "s", "tabType[0]", "tabType%5B0%5D"], + "agentMobileOS": "unknown" + }, + "category": { + "ibm": {}, + "primaryCategory": "null", + "iniPrimaryCategory": "null" + }, + "pageInfo": { + "analytics": { + "category": "Uncategorized" + }, + "convertro": { + "enabled": "false" + }, + "coremetrics": { + "visitorID": null, + "visitorId": null, + "clientID": "50200000|IBMTESTWWW", + "isEluminateLoaded": false, + "enabled": true + }, + "demandbase": { + "enabled": true + }, + "hotjar": { + "enabled": "false" + }, + "ibm": { + "siteID": "IBMTESTWWW", + "country": "br", + "industry": "", + "owner": "", + "subject": "", + "type": "", + "iniSiteID": "IBMTESTWWW" + }, + "lotame": { + "activeAudience": null, + "audience": "240097", + "pid": "446689ed8a1d9791b415f537e490d3af" + }, + "metrics": {}, + "medallia": { + "triggerFired": "true" + }, + "optimizely": { + "enabled": "false", + "projectID": "" + }, + "segment": { + "enabled": "false", + "key": "" + }, + "tealium": { + "collect": {} + }, + "trustarc": { + "enabled": "true" + }, + "urx": {}, + "destinationURL": "https://www.ibm.com/standards/carbon/react/", + "destinationDomain": "carbon-design-system.github.io", + "description": "A collection of IBM.com components implemented using React and Carbon Design System.", + "effectiveDate": "", + "expiryDate": "", + "keywords": "IBM, design, system, Carbon, design system, Bluemix, styleguide, style, guide, components, library, pattern, kit, component, cloud, React, React.js", + "language": "pt-BR", + "publishDate": "", + "publisher": "", + "rights": "", + "source": "", + "referrer": "", + "referrerDomain": "", + "referrerID": "", + "urlID": "www.ibm.com/standards/carbon/react", + "clickableURLID": "https://www.ibm.com/standards/carbon/react", + "pageID": "www.ibm.com/standards/carbon/react", + "dleID": "6804808e6d33a5e4e0eed41c0771f7e9d725a0a8f50799b3c9bec2e488e72c8e", + "dleURL": "https://tags.tiqcdn.com/dle/ibm/web/p_6804808e6d33a5e4e0eed41c0771f7e9d725a0a8f50799b3c9bec2e488e72c8e.js", + "contentId": "url-6804808e6d33a5e4e0eed41c0771f7e9d725a0a8f50799b3c9bec2e488e72c8e", + "canonical": "www.ibm.com/standards/carbon/react", + "onsiteSearchTerm": "", + "onsiteSearchResult": "", + "pageHeader": null, + "pageName": "Storybook", + "title": "Storybook", + "google": { + "gtag": { + "pageURL": "https://www.ibm.com/standards/carbon/react/", + "pageReferrerURL": "", + "pagePath": "www.ibm.com/standards/carbon/react", + "pageTitle": "", + "customDimension1": 1, + "customDimension2": "IBMTESTWWW", + "customDimension4": "", + "customDimension5": "5f3e204f-8dca-41f7-873b-3c608634cfdd", + "customDimension7": "", + "customDimension12": "www.ibm.com/standards/carbon/react", + "customDimension13": "https://www.ibm.com/standards/carbon/react/", + "customDimension16": "", + "customDimension17": "", + "customDimension3": "", + "customDimension6": "", + "customDimension8": "", + "customDimension9": "", + "customDimension10": "", + "customDimension11": "", + "customDimension14": "", + "customDimension15": "S:628|T:2,162,46,28,93,53,45,94|L:100|IP:f|IPC:us|DBC:us", + "customDimension18": "bf43c6e1-0681-49fd-8945-50a64c60fc92" + } + }, + "productTitle": "Unnamed Product" + }, + "session": { + "engagement": { + "isEngaged": false, + "isPageEngaged": false, + "scorePage": 0, + "scoreVisit": 0, + "events": 0, + "pages": 0, + "totalPages": 1 + }, + "pageloadEpoch": 1569589572001, + "pageviewId": "5f3e204f-8dca-41f7-873b-3c608634cfdd", + "uPageViewID": "5f3e204f-8dca-41f7-873b-3c608634cfdd", + "issuedInitialPageview": true, + "event": "profile" + }, + "isDataLayerReady": true + }, + "events": { + "processed": [], + "current": {} + }, + "content": { + "drivers": {}, + "nbo": {} + }, + "user": { + "gdpr": { + "state": "ny", + "country": "us", + "latitude": 40.75, + "longitude": -73.9967, + "isCountryMemberOfEU": false, + "isCountryRequiringExplicitConsent": false + }, + "location": { + "country": "US", + "stateProvince": "NJ", + "city": "NEWARK" + }, + "profile": { + "dw": {}, + "ex": {}, + "ng": { + "iuiHashed": null + }, + "topics": [] + }, + "segment": { + "30": true, + "31": true, + "11241": true, + "18192": true, + "isIBMer": 1 + }, + "userInfo": { + "registry_country_code": "us", + "browserLanguage": "en-US", + "annual_sales": 79590000000, + "audience_segment": "software & technology", + "audience": "enterprise business", + "b2b": true, + "b2c": true, + "city": "poughkeepsie", + "company_name": "ibm corporation", + "country_name": "united states", + "country": "us", + "demandbase_sid": 50918973, + "employee_count": 351000, + "employee_range": "enterprise", + "forbes_2000": false, + "fortune_1000": false, + "industry": "software & technology", + "information_level": "detailed", + "ip": "129.42.208.183", + "isp": false, + "latitude": 41.66, + "longitude": -73.93, + "marketing_alias": "ibm", + "phone": "845-433-1234", + "primary_naics": "334111", + "primary_sic": "3571", + "region_name": "new york", + "registry_city": "new york", + "registry_company_name": "advanced workstations division, ibm corporation", + "registry_country": "united states", + "registry_dma_code": 501, + "registry_latitude": 40.77, + "registry_longitude": -73.99, + "registry_state": "ny", + "registry_zip_code": "10019", + "revenue_range": "over $5b", + "state": "ny", + "street_address": "2455 south rd", + "sub_industry": "computer & mobile devices", + "traffic": "very high", + "web_site": "ibm.com", + "zip": "12601", + "ipLastOctet": "184" + } + }, + "util": { + "cp": { + "cmTPSet": "Y", + "_ga": "GA1.3.781956925.1569442431", + "_gid": "GA1.3.1110946260.1569442431", + "CMAVID": "none", + "notice_behavior": "implied|eu", + "pageviewContext": "5f3e204f-8dca-41f7-873b-3c608634cfdd" + }, + "meta": { + "viewport": "width=device-width,initial-scale=1", + "keywords": "IBM, design, system, Carbon, design system, Bluemix, styleguide, style, guide, components, library, pattern, kit, component, cloud, React, React.js", + "description": "A collection of IBM.com components implemented using React and Carbon Design System.", + "og:title": "Carbon for IBM.com React", + "og:site_name": "Carbon for IBM.com React", + "og:description": "A collection of IBM.com components implemented using React and Carbon Design System.", + "og:image": "https://media.github.ibm.com/user/525/files/59e3bfde-b990-11e7-87ef-072e89a87719", + "og:url": "https://www.ibm.com/standards/carbon/react", + "twitter:card": "summary_large_image", + "twitter:image:alt": "Carbon for IBM.com", + "twitter:site": "@_IBM", + "og:type": "website", + "og:locale": "en-US" + }, + "qp": {}, + "referrer": {} + } +} diff --git a/packages/services/src/services/SearchTypeahead/__tests__/SearchTypeahead.test.js b/packages/services/src/services/SearchTypeahead/__tests__/SearchTypeahead.test.js index b250d7cad9e..2bb8ee311e6 100644 --- a/packages/services/src/services/SearchTypeahead/__tests__/SearchTypeahead.test.js +++ b/packages/services/src/services/SearchTypeahead/__tests__/SearchTypeahead.test.js @@ -1,19 +1,17 @@ /** - * Copyright IBM Corp. 2020, 2022 + * Copyright IBM Corp. 2020, 2024 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ -import digitalDataResponse from '../../DDO/__tests__/data/response.json'; import mockAxios from 'axios'; import responseSuccess from './data/response.json'; -import root from 'window-or-global'; import SearchTypeaheadAPI from '../SearchTypeahead'; +import { LocaleAPI } from '../../Locale'; const _lc = 'en'; // TODO: bake in tests where lc changes const _cc = 'us'; // TODO: bake in tests where cc changes -const mockDigitalDataResponse = digitalDataResponse; describe('SearchTypeaheadAPI', () => { beforeEach(function () { @@ -23,10 +21,16 @@ describe('SearchTypeaheadAPI', () => { }) ); - root.digitalData = mockDigitalDataResponse; + // Restore any mocks back to a predictable state. + jest.restoreAllMocks(); }); it('should search for ibm.com results with just lc param', async () => { + jest.spyOn(LocaleAPI, 'getLang').mockReturnValue( + Promise.resolve({ + lc: 'en', + }) + ); const query = 'red hat'; const endpoint = `${process.env.SEARCH_TYPEAHEAD_API}/search/typeahead/${process.env.SEARCH_TYPEAHEAD_VERSION}`; const fetchUrl = `${endpoint}?lang=${_lc}&query=${encodeURIComponent( @@ -44,15 +48,12 @@ describe('SearchTypeaheadAPI', () => { }); it('should search for ibm.com results with both cc and lc param', async () => { - root.digitalData = { - page: { - pageInfo: { - ibm: { - country: 'us', - }, - }, - }, - }; + jest.spyOn(LocaleAPI, 'getLang').mockReturnValue( + Promise.resolve({ + lc: 'en', + cc: 'us', + }) + ); const query = 'red hat'; const endpoint = `${process.env.SEARCH_TYPEAHEAD_API}/search/typeahead/${process.env.SEARCH_TYPEAHEAD_VERSION}`; diff --git a/packages/services/src/services/global/__tests__/global.test.js b/packages/services/src/services/global/__tests__/global.test.js index 2f46eeca775..905add1de7d 100644 --- a/packages/services/src/services/global/__tests__/global.test.js +++ b/packages/services/src/services/global/__tests__/global.test.js @@ -1,5 +1,5 @@ /** - * Copyright IBM Corp. 2020 + * Copyright IBM Corp. 2020, 2024 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. @@ -14,6 +14,11 @@ jest.mock('../../DDO', () => ({ setVersion: jest.fn(async () => {}), }, })); +jest.mock('../../Locale', () => ({ + LocaleAPI: { + getLang: jest.fn(async () => ({ lc: 'en', cc: 'us' })), + }, +})); jest.mock('../../Analytics', () => ({ AnalyticsAPI: { initAll: jest.fn(),