diff --git a/common/core/src/vvd-configurer.ts b/common/core/src/vvd-configurer.ts deleted file mode 100644 index 01402be0dd..0000000000 --- a/common/core/src/vvd-configurer.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * This module exposes internal APIs to manage a Vivid overlay configuration state - * state machine revolves over: - * - uninitialised state for all - * - first init merge where undefined resolved to defaults - * - following merges applied over an existing state (undefined is not to touch) - * - * Static way to pre-configure Vivid is via `data-vvd-context` attribute on the HTML element. - * As of now, only a single keyword token value is supported: - * - none: suspend auto-init, to be used in custom initialization flavor - */ -import { SchemeOption } from '@vonage/vvd-scheme'; - -const VVD_CONTEXT_ATTRIBUTE = 'data-vvd-context', - NONE_INIT_VALUE = 'none', - VALID_CONFIGURATION_KEYS = ['autoInit', 'scheme']; - -export interface Configuration { - autoInit: boolean; - scheme?: SchemeOption; -} -export default Object.freeze({ - initialConfiguration: buildInitialConfiguration(), - validateConfiguration, -}); - -function buildInitialConfiguration(): Configuration { - const result: Configuration = { - autoInit: true, - }; - const vvdContextAttrValue = document.documentElement.getAttribute( - VVD_CONTEXT_ATTRIBUTE - ); - if (vvdContextAttrValue === NONE_INIT_VALUE) { - result.autoInit = false; - } - return result; -} - -function validateConfiguration(configuration: Partial) { - const extraParams = Object.keys(configuration).filter( - (k) => !VALID_CONFIGURATION_KEYS.includes(k) - ); - - if (extraParams.length) { - console.warn( - `unexpected configuration part/s '${extraParams}', only some of '${VALID_CONFIGURATION_KEYS}' expected` - ); - } -} diff --git a/common/core/src/vvd-core.ts b/common/core/src/vvd-core.ts index 0bcc384496..e0437833bd 100644 --- a/common/core/src/vvd-core.ts +++ b/common/core/src/vvd-core.ts @@ -1,26 +1,95 @@ -import configurer, { Configuration } from './vvd-configurer.js'; import fonts from '@vonage/vvd-fonts/vvd-fonts.js'; -import schemeService from '@vonage/vvd-scheme'; +import schemeService, { SchemeOption } from '@vonage/vvd-scheme'; -let coreAutoInitDone: Promise>; -if (configurer.initialConfiguration.autoInit) { - coreAutoInitDone = applyConfiguration(configurer.initialConfiguration); +const VVD_CONTEXT_ATTRIBUTE = 'data-vvd-context', + NONE_INIT_VALUE = 'none', + VALID_CONFIGURATION_KEYS = ['scheme']; + +export interface Configuration { + scheme?: SchemeOption; +} + +interface InitialConfiguration extends Configuration { + autoInit: boolean; +} + +let coreAutoInitDone: Promise>; +const initialConfiguration = _buildConfiguration(); +if (initialConfiguration.autoInit) { + coreAutoInitDone = _applyConfiguration(initialConfiguration); } else { - coreAutoInitDone = Promise.reject('auto-init unavailable when "none" used'); + coreAutoInitDone = Promise.reject( + `auto-init unavailable when '${NONE_INIT_VALUE}' used` + ); } export default Object.freeze({ - set: applyConfiguration, + set: safeApplyConfiguration, settled: coreAutoInitDone, }); -async function applyConfiguration(configuration: Partial) { - configurer.validateConfiguration(configuration); - return init(configuration); +async function safeApplyConfiguration( + configuration: Partial +): Promise> { + _validateConfiguration(configuration); + return _applyConfiguration(configuration); } -async function init( +async function _applyConfiguration( configuration: Partial -): Promise> { - return Promise.all([fonts.init(), schemeService.set(configuration.scheme)]); +): Promise> { + const allResults = await Promise.all([ + fonts.init(), + schemeService.set(configuration.scheme), + ]); + return Object.freeze({ + fonts: allResults[0], + scheme: allResults[1], + }); +} + +function _buildConfiguration(): InitialConfiguration { + const result: InitialConfiguration = { + autoInit: true, + }; + const vvdContextAttrValue = document.documentElement.getAttribute( + VVD_CONTEXT_ATTRIBUTE + ); + if (vvdContextAttrValue === NONE_INIT_VALUE) { + result.autoInit = false; + } else if (vvdContextAttrValue) { + const parsed = _parseVvdContextAttr(vvdContextAttrValue); + Object.assign(result, parsed); + } + return result; +} + +function _validateConfiguration(configuration: Partial) { + const extraParams = Object.keys(configuration).filter( + (k) => !VALID_CONFIGURATION_KEYS.includes(k) + ); + + if (extraParams.length) { + console.warn( + `unexpected configuration part/s '${extraParams}', only some of '${VALID_CONFIGURATION_KEYS}' expected` + ); + } +} + +function _parseVvdContextAttr(value: string): Record { + const tokens = value.trim().split(/\s+/); + return tokens.reduce((result, token) => { + if (/^theme:/.test(token)) { + if (result.scheme) { + console.error( + `theme vivid context defined multiple times, only the first (${result.scheme}) will be effective` + ); + } else { + result.scheme = token.replace(/^theme:/, ''); + } + } else { + console.warn(`unsupported token '${token}' in vivid context`); + } + return result; + }, {} as Record); } diff --git a/common/core/test/core-setup.test.html b/common/core/test/core-setup.test.html new file mode 100644 index 0000000000..6c32722565 --- /dev/null +++ b/common/core/test/core-setup.test.html @@ -0,0 +1,15 @@ + + + + + Vivid - injectable isolated document as testing playground + + + + + + \ No newline at end of file diff --git a/common/core/test/core.test.js b/common/core/test/core.test.js index 2223834f71..23c7c612c9 100644 --- a/common/core/test/core.test.js +++ b/common/core/test/core.test.js @@ -1,27 +1,170 @@ -import vvdCore from '../vvd-core.js'; +import { + randomAlpha, + getFrameLoadedInjected, +} from '../../../test/test-helpers.js'; +import { + assertBaseVarsMatch, + PRINCIPAL_VARIABLES_FILTER, +} from '../../../test/style-utils.js'; + +const CONTEXT_ATTR = 'data-vvd-context'; +const CORE_SETUP_HTML_TAG = 'coreSetupTest'; +const LIGHT = 'light'; +const DARK = 'dark'; +const NONE = 'none'; describe('vvd-core service', () => { - it('verify basic core API', async () => { - assert.isDefined(vvdCore, 'core service is defined'); - assert.isObject(vvdCore, 'core service is a defaultly exported object'); - assert.isFunction(vvdCore.set, 'core service has "set" API method'); - assert.isDefined( - vvdCore.settled, - 'core service has "settled" object (Promise)' - ); - assert.isFunction( - vvdCore.settled.then, - 'core service has "settled" object - ensure it is Promise' - ); + describe('basic APIs', () => { + it('is should init to default', async () => { + const r = randomAlpha(); + const vvdCore = (await import(`../vvd-core.js?t=${r}`)).default; + assert.isDefined(vvdCore, 'core service is defined'); + assert.isObject(vvdCore, 'core service is a defaultly exported object'); + assert.isFunction(vvdCore.set, 'core service has "set" API method'); + assert.isDefined( + vvdCore.settled, + 'core service has "settled" object (Promise)' + ); + assert.isFunction( + vvdCore.settled.then, + 'core service has "settled" object - ensure it is Promise' + ); + }); + + it('should init to none', async () => { + document.documentElement.setAttribute(CONTEXT_ATTR, 'none'); + const r = randomAlpha(); + const vvdCore = (await import(`../vvd-core.js?t=${r}`)).default; + document.documentElement.removeAttribute(CONTEXT_ATTR); + try { + await vvdCore.settled; + } catch (e) { + expect(e).exist; + expect(e.includes('auto-init unavailable')).true; + } + }); + + it('should init to dark', async () => { + document.documentElement.setAttribute(CONTEXT_ATTR, `theme:${DARK}`); + const r = randomAlpha(); + const vvdCore = (await import(`../vvd-core.js?t=${r}`)).default; + document.documentElement.removeAttribute(CONTEXT_ATTR); + const coreInitResult = await vvdCore.settled; + + assertInitResult(coreInitResult, DARK); + }); + + it('should perform set', async () => { + const r = randomAlpha(); + const vvdCore = (await import(`../vvd-core.js?t=${r}`)).default; + const coreInitResult = await vvdCore.set({ scheme: LIGHT }); + + assertInitResult(coreInitResult, LIGHT); + }); + + it('should not fail on abnormal calls', async () => { + document.documentElement.setAttribute( + CONTEXT_ATTR, + `illegal theme:${DARK} theme:${LIGHT}` + ); + const r = randomAlpha(); + const vvdCore = (await import(`../vvd-core.js?t=${r}`)).default; + document.documentElement.removeAttribute(CONTEXT_ATTR); + let coreInitResult = await vvdCore.settled; + + assertInitResult(coreInitResult, DARK); + + coreInitResult = await vvdCore.set({ + scheme: DARK, + illegal: { some: null }, + }); + assertInitResult(coreInitResult, DARK); + }); }); - it('should perform and auto-init to default when no data-vvd-context provided', async () => { - const vvdCoreDedicated = (await import('../vvd-core.js')).default; - assert.isDefined(vvdCoreDedicated.settled); - const readyResult = await vvdCoreDedicated.settled; - assert.isArray(readyResult); - readyResult.forEach((r) => { - assert.isObject(r); + describe('switch flows in encapsulated environment and assert variables set', () => { + it('should perform auto-init to default when no data-vvd-context provided', async () => { + await getFrameLoadedInjected(CORE_SETUP_HTML_TAG, async (iframe) => { + const iframeWindow = iframe.contentWindow; + await iframeWindow.executeSetup(); + const coreInitResult = await iframeWindow.vvdCore.settled; + + assertInitResult(coreInitResult, LIGHT); + assertBaseVarsMatch( + LIGHT, + PRINCIPAL_VARIABLES_FILTER, + iframe.contentDocument.body + ); + }); + }); + + it('should perform auto-init to a value in data-vvd-context, when provided', async () => { + const vvdContextTheme = DARK; + await getFrameLoadedInjected(CORE_SETUP_HTML_TAG, async (iframe) => { + iframe.contentDocument.documentElement.setAttribute( + CONTEXT_ATTR, + `theme:${vvdContextTheme}` + ); + + const iframeWindow = iframe.contentWindow; + await iframeWindow.executeSetup(); + const coreInitResult = await iframeWindow.vvdCore.settled; + + assertInitResult(coreInitResult, vvdContextTheme); + assertBaseVarsMatch( + vvdContextTheme, + PRINCIPAL_VARIABLES_FILTER, + iframe.contentDocument.body + ); + }); + }); + + it('should NOT perform auto-init when data-vvd-context is "none"', async () => { + const vvdContextNone = NONE; + await getFrameLoadedInjected(CORE_SETUP_HTML_TAG, async (iframe) => { + iframe.contentDocument.documentElement.setAttribute( + CONTEXT_ATTR, + vvdContextNone + ); + + const iframeWindow = iframe.contentWindow; + await iframeWindow.executeSetup(); + + try { + await iframeWindow.vvdCore.settled; + } catch (e) { + expect(e).exist; + expect(e.includes('auto-init unavailable')).true; + } + }); + }); + + it('should perform init to a first value in data-vvd-context, when many provided', async () => { + const vvdContextTheme = LIGHT; + await getFrameLoadedInjected(CORE_SETUP_HTML_TAG, async (iframe) => { + iframe.contentDocument.documentElement.setAttribute( + CONTEXT_ATTR, + `theme:${vvdContextTheme} theme:${DARK}` + ); + + const iframeWindow = iframe.contentWindow; + await iframeWindow.executeSetup(); + const coreInitResult = await iframeWindow.vvdCore.settled; + + assertInitResult(coreInitResult, vvdContextTheme); + assertBaseVarsMatch( + vvdContextTheme, + PRINCIPAL_VARIABLES_FILTER, + iframe.contentDocument.body + ); + }); }); }); }); + +function assertInitResult(tested, expectedScheme) { + expect(tested).exist; + expect(tested.scheme).exist; + expect(tested.scheme.option).equal(expectedScheme); + expect(tested.scheme.scheme).equal(expectedScheme); +} diff --git a/karma.conf.js b/karma.conf.js index 5d70c3b597..82b5d90255 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -9,27 +9,26 @@ module.exports = config => { { pattern: config.grep ? config.grep : '{common,components}/**/test/**/*.test.js', type: 'module' }, ], preprocessors: { - 'common/design-tokens/build/scss/schemes/**/*.scss': ['file-fixtures'] + 'common/design-tokens/build/scss/schemes/**/*.scss': ['file-fixtures'], + '{common,components}/**/*.js': ['coverage'] }, esm: { nodeResolve: true, }, frameworks: ['chai'], + reporters: ['karmaHTML'], browserDisconnectTimeout: 300000, browserNoActivityTimeout: 360000, singleRun: true, autoWatch: false, restartOnFileChange: true, captureTimeout: 420000, - coverageIstanbulReporter: { - thresholds: { - global: { - statements: 10, - lines: 10, - branches: 3, - functions: 10, - }, - }, + client: { + karmaHTML: { + source: [ + { tag: 'coreSetupTest', src: 'common/core/test/core-setup.test.html' } + ] + } } }); diff --git a/package.json b/package.json index 5d3f1ed317..bd06461e03 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "karma-coverage-istanbul-reporter": "^3.0.3", "karma-firefox-launcher": "^1.3.0", "karma-file-fixtures-preprocessor": "^3.0.1", + "karma-html": "^1.0.5", "karma-mocha": "^2.0.1", "karma-mocha-reporter": "^2.2.5", "karma-safarinative-launcher": "^1.1.0", diff --git a/test/test-helpers.js b/test/test-helpers.js index dab3e92ccf..91caf34269 100644 --- a/test/test-helpers.js +++ b/test/test-helpers.js @@ -147,6 +147,40 @@ export function isSafari() { !window.navigator.userAgent.toLowerCase().includes('chrome'); } +/** + * creates iFrame with the specified HTML (via karmaHTML framework) + * waits until the iFrame is loaded + * executes testCode on the iFrame's window object + * resolves as soon as all of those operations done + * + * @param {string} htmlTag + * @param {function} testCode logic to run on the contentWindow of the newly created iframe + * @returns created and initialised iFrame element + */ +export async function getFrameLoadedInjected(htmlTag, testCode) { + if (!htmlTag || typeof htmlTag !== 'string') { + throw new Error(`htmlTag MUST be a non-null nor-empty string, got '${htmlTag}'`); + } + if (!testCode && typeof testCode !== 'function') { + throw new Error(`test code MUST be a function`); + } + + const loader = karmaHTML[htmlTag]; + loader.reload(); + return new Promise((resolve, reject) => { + loader.onstatechange = ready => { + if (!ready) { return; } + const result = loader.iframe; + + // test logic + Promise + .resolve(testCode.call(result.contentWindow, result)) + .catch(reject) + .finally(() => resolve(result)); + }; + }); +} + class TestComponent extends HTMLElement { connectedCallback() { this.attachShadow({ mode: 'open' }); diff --git a/yarn.lock b/yarn.lock index 55c47d59d8..a9617e6b7a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10907,6 +10907,13 @@ karma-firefox-launcher@^1.3.0: dependencies: is-wsl "^2.1.0" +karma-html@^1.0.5: + version "1.0.5" + resolved "https://vonagecc.jfrog.io/vonagecc/api/npm/npm/karma-html/-/karma-html-1.0.5.tgz#08d47d8afb374f9e100efe5f38694d2efd56b46a" + integrity sha1-CNR9ivs3T54QDv5fOGlNLv1WtGo= + dependencies: + glob "^7.1.2" + karma-mocha-reporter@^2.0.0, karma-mocha-reporter@^2.2.5: version "2.2.5" resolved "https://vonagecc.jfrog.io/vonagecc/api/npm/npm/karma-mocha-reporter/-/karma-mocha-reporter-2.2.5.tgz#15120095e8ed819186e47a0b012f3cd741895560"