diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000..7bd5cb3f26 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,31 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "test core", + "request": "launch", + "runtimeArgs": [ + "run-script", + "test" + ], + "cwd": "${workspaceFolder}/core", + "runtimeExecutable": "npm", + "skipFiles": [ + "/**" + ], + "type": "node" + },{ + "name": "test core -watch", + "request": "launch", + "runtimeArgs": [ + "run-script", + "test:watch" + ], + "cwd": "${workspaceFolder}/core", + "runtimeExecutable": "npm", + "skipFiles": [ + "/**" + ], + "type": "node" + }] +} \ No newline at end of file diff --git a/client/src/luigi-client.js b/client/src/luigi-client.js index 6c03567995..c0041a34b7 100644 --- a/client/src/luigi-client.js +++ b/client/src/luigi-client.js @@ -84,10 +84,8 @@ class LuigiClient { /** * @private */ - storageManager(){ + storageManager() { return storageManager; } - - } export default LuigiClient = new LuigiClient(); diff --git a/client/src/luigi-element.js b/client/src/luigi-element.js index 76683411ac..5e772e202e 100644 --- a/client/src/luigi-element.js +++ b/client/src/luigi-element.js @@ -2,45 +2,141 @@ * Base class for Luigi web component micro frontends. */ export class LuigiElement extends HTMLElement { - constructor() { - super(); - const template = document.createElement('template'); - template.innerHTML = this.render ? this.render() : ''; - this._shadowRoot = this.attachShadow({ mode: 'open', delegatesFocus: false }); - this._shadowRoot.appendChild(template.content.cloneNode(true)); - } + constructor() { + super(); + this._shadowRoot = this.attachShadow({ + mode: 'closed', + delegatesFocus: false + }); + this.__initialized = false; + } - /** - * Query selector operating on shadow root. - * - * @see ParentNode.querySelector - */ - querySelector(selector) { - return this._shadowRoot.querySelector(selector); - } + /** + * Invoked by luigi core if present, internal, don't override. + * @private + */ + __postProcess(ctx, luigiClient, module_location_path) { + this.luigiClient = luigiClient; + this.context = ctx; + const template = document.createElement('template'); + template.innerHTML = this.render(ctx); + const attCnt = () => { + this._shadowRoot.appendChild(template.content.cloneNode(true)); + Reflect.ownKeys(Reflect.getPrototypeOf(this)).forEach(el => { + if (el.startsWith('$_')) { + this._shadowRoot[el] = this[el].bind(this); + } + }); + const elementsWithIds = this._shadowRoot.querySelectorAll('[id]'); + if (elementsWithIds) { + elementsWithIds.forEach(el => { + this['$' + el.getAttribute('id')] = el; + }); + } + this.afterInit(ctx); + this.__initialized = true; + }; + if ( + this.luigiConfig && + this.luigiConfig.styleSources && + this.luigiConfig.styleSources.length > 0 + ) { + let nr_styles = this.luigiConfig.styleSources.length; + const loadStylesSync = this.luigiConfig.loadStylesSync; + const afterLoadOrError = () => { + nr_styles--; + if (nr_styles < 1) { + attCnt(); + } + }; - /** - * Handles changes on the context property. - * - * @private - */ - set context(ctx) { - if(this.onContextUpdate) { - this.onContextUpdate(ctx); + this.luigiConfig.styleSources.forEach((element, index) => { + const link = document.createElement('link'); + link.setAttribute('rel', 'stylesheet'); + link.setAttribute('href', module_location_path + element); + if (loadStylesSync) { + link.addEventListener('load', afterLoadOrError); + link.addEventListener('error', afterLoadOrError); } - this.attributeChangedCallback(); + this._shadowRoot.appendChild(link); + }); + if (!loadStylesSync) { + attCnt(); + } + } else { + attCnt(); } + } - /** - * Handles changes on attributes. - * - * @private - */ - attributeChangedCallback(name, oldVal, newVal) { - if (this.update) { - this.update(); - } + /** + * Override to execute logic after initialization of the web component, i.e. + * after internal rendering and all context data set. + * + * @param {*} ctx The context object passed by luigi core + */ + afterInit(ctx) { + return; + } + + /** + * Override to return the html template string defining the web component view. + * + * @param {*} ctx The context object passed by luigi core + */ + render(ctx) { + return ''; + } + + /** + * Override to execute logic after an attribute of this web component has changed. + */ + update() { + return; + } + + /** + * Override to execute logic when a new context object is set. + * + * @param {*} ctx The new context object passed by luigi core + */ + onContextUpdate(ctx) { + return; + } + + /** + * Query selector operating on shadow root. + * + * @see ParentNode.querySelector + */ + querySelector(selector) { + return this._shadowRoot.querySelector(selector); + } + + /** + * Handles changes on the context property. + * + * @private + */ + set context(ctx) { + this.__lui_ctx = ctx; + if (this.__initialized) { + this.onContextUpdate(ctx); + this.attributeChangedCallback(); } + } + + get context() { + return this.__lui_ctx; + } + + /** + * Handles changes on attributes. + * + * @private + */ + attributeChangedCallback(name, oldVal, newVal) { + this.update(); + } } /** @@ -50,6 +146,17 @@ export class LuigiElement extends HTMLElement { * @param {String} literal The literal to process. * @returns {String} Returns the processed literal. */ -export function html(literal) { - return literal; +export function html(literal, ...keys) { + let html = ''; + literal.forEach((el, index) => { + html += el; + if ( + index < keys.length && + keys[index] !== undefined && + keys[index] !== null + ) { + html += keys[index]; + } + }); + return html.replace(/\$\_/gi, 'this.getRootNode().$_'); } diff --git a/core/package-lock.json b/core/package-lock.json index 2a2316e375..606ae1427c 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -3285,6 +3285,12 @@ "estraverse": "^4.1.1" } }, + "esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "dev": true + }, "esprima": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", diff --git a/core/package.json b/core/package.json index a59a74a8ce..94cf5d1403 100644 --- a/core/package.json +++ b/core/package.json @@ -21,6 +21,7 @@ "copy-webpack-plugin": "^5.1.1", "core-js": "^3.2.1", "css-loader": "^3.0.0", + "esm": "^3.2.25", "file-loader": "^2.0.0", "fs-extra": "9.0.0", "fundamental-styles": "^0.11.0", @@ -61,7 +62,7 @@ "bundle-develop": "npm run bundle-develop-evergreen", "bundle-develop-evergreen": "npm run bundle-evergreen -- -d --watch", "bundle-develop-ie11": "MINIFY=false webpack --display-error-details --config webpack-ie11.config.js --debug --devtool cheap-source-map --output-pathinfo --watch", - "test": "babel-node ./node_modules/nyc/bin/nyc.js mocha -- --recursive test", + "test": "babel-node ./node_modules/nyc/bin/nyc.js mocha -- --require esm --recursive test", "test:watch": "npm run test -- --watch", "bundlesize": "npm run bundle && bundlesize", "bundlesizeOnly": "bundlesize", @@ -81,7 +82,7 @@ }, { "path": "./public-ie11/luigi-ie11.js", - "maxSize": "590 kB", + "maxSize": "600 kB", "compression": "none" }, { diff --git a/core/src/App.html b/core/src/App.html index 93fb73e379..151ee8c14b 100644 --- a/core/src/App.html +++ b/core/src/App.html @@ -9,6 +9,7 @@ nodepath="{mfModal.nodepath}" on:close="{closeModal}" on:iframeCreated="{modalIframeCreated}" + on:wcCreated="{modalWCCreated}" > {/if} {#if mfDrawer.displayed && mfDrawer.settings.isDrawer} @@ -45,6 +46,7 @@ nodepath="{mfSplitView.nodepath}" on:iframeCreated="{splitViewIframeCreated}" on:statusChanged="{splitViewStatusChanged}" + on:wcCreated="{splitViewWCCreated}" disableBackdrop="{disableBackdrop}" > {/if} @@ -160,6 +162,8 @@ /// MFs let modalIframe; let modalIframeData; + let modalWC; + let modalWCData; let modal; let activeDrawer = false; let disableBackdrop; @@ -168,6 +172,8 @@ let drawer; let splitViewIframe; let splitViewIframeData; + let splitViewWC; + let splitViewWCData; let splitView; let context; let nodeParams; @@ -348,6 +354,7 @@ mfSplitView, splitViewValues, splitViewIframe, + splitViewWC, showLoadingIndicator, tabNav, isNavigateBack, @@ -390,6 +397,8 @@ splitViewValues = obj.splitViewValues; } else if (prop === 'splitViewIframe') { splitViewIframe = obj.splitViewIframe; + } else if (prop == 'splitViewWC') { + splitViewWC = obj.splitViewWC; } else if (prop === 'showLoadingIndicator') { showLoadingIndicator = obj.showLoadingIndicator; } else if (prop === 'tabNav') { @@ -706,6 +715,12 @@ } }; + const splitViewWCCreated = event => { + splitViewWC = event.detail.splitViewWC; + splitViewWCData = event.detail.splitViewWCData; + $: mfSplitView.collapsed = event.detail.collapsed; + }; + /// RESIZING let hideNav; @@ -906,11 +921,18 @@ modalIframeData = event.detail.modalIframeData; }; + const modalWCCreated = event => { + modalWC = event.detail.modalWC; + modalWCData = event.detail.modalWCData; + }; + const closeModal = event => { if (modalIframe) { getUnsavedChangesModalPromise(modalIframe.contentWindow).then(() => { resetMicrofrontendModalData(); }); + } else if (modalWC) { + resetMicrofrontendModalData(); } }; @@ -1305,7 +1327,13 @@ if ('storage' === e.data.msg) { let getValidMessageSource = IframeHelpers.getValidMessageSource(e); let microfrontendId = getValidMessageSource.luigi.id; - StorageHelper.process(microfrontendId, e.origin, e.data.data.id, e.data.data.operation, e.data.data.params); + StorageHelper.process( + microfrontendId, + e.origin, + e.data.data.id, + e.data.data.operation, + e.data.data.params + ); } }); diff --git a/core/src/Modal.html b/core/src/Modal.html index 382e080cd3..7dd7bce0b9 100644 --- a/core/src/Modal.html +++ b/core/src/Modal.html @@ -1,9 +1,12 @@ - -
+ +
+ data-testid="modal-mf" + > {#if isModal || (isDrawer && settings.header)}
@@ -74,6 +77,7 @@

{settings.title}

} from './utilities/helpers'; import { LuigiConfig } from './core-api'; import { KEYCODE_ESC } from './utilities/keycode.js'; + import { WebComponentService } from './services/web-components'; export let settings; export let isDataPrepared = false; @@ -82,6 +86,7 @@

{settings.title}

let pathData; let nodeParams; let iframeCreated = false; + let wcCreated = false; let showLoadingIndicator = true; let isDrawer = false; let isModal = true; @@ -120,40 +125,62 @@

{settings.title}

}; const getNode = async path => { - if (iframeCreated) { + if (iframeCreated || wcCreated) { return; } if (isDataPrepared) { - const iframe = await createIframeModal(nodeObject.viewUrl, { - context: pathData.context, - pathParams: pathData.pathParams, - nodeParams - }); - dispatch('iframeCreated', { - modalIframe: iframe, - modalIframeData: { ...pathData, nodeParams } - }); - iframeCreated = true; + if (nodeObject.webcomponent) { + //"Workaround" because we need a webcomponent client api to hide/show the loadingIndicator + showLoadingIndicator = false; + await setModalSize(); + WebComponentService.renderWebComponent( + nodeObject.viewUrl, + document.querySelector('.iframeModalCtn'), + pathData.context, + nodeObject + ); + dispatch('wcCreated', { + modalWC: document.querySelector('.iframeModalCtn'), + modalWCData: { ...pathData, nodeParams } + }); + wcCreated = true; + } else { + const iframe = await createIframeModal(nodeObject.viewUrl, { + context: pathData.context, + pathParams: pathData.pathParams, + nodeParams + }); + dispatch('iframeCreated', { + modalIframe: iframe, + modalIframeData: { ...pathData, nodeParams } + }); + iframeCreated = true; + } } else { await prepareNodeData(path); } }; + + const setModalSize = async () => { + const elem = document.getElementsByClassName('lui-modal-mf'); + let modalSize = '80%'; + if (settings.size) { + if (settings.size === 'l') { + modalSize = '80%'; + } else if (settings.size === 'm') { + modalSize = '60%'; + } else if (settings.size === 's') { + modalSize = '40%'; + } + } + elem[0].setAttribute('style', `width:${modalSize};height:${modalSize}`); + }; + const createIframeModal = async (viewUrl, componentData) => { if (isDrawer) { await setDrawerSize(); } else { - const elemModal = document.getElementsByClassName('lui-modal-mf'); - let modalSize = '80%'; - if (settings.size) { - if (settings.size === 'l') { - modalSize = '80%'; - } else if (settings.size === 'm') { - modalSize = '60%'; - } else if (settings.size === 's') { - modalSize = '40%'; - } - } - elemModal[0].setAttribute('style', `width:${modalSize};height:${modalSize}`); + await setModalSize(); } if (viewUrl) { viewUrl = RoutingHelpers.substituteViewUrl(viewUrl, componentData); @@ -221,15 +248,20 @@

{settings.title}

} if ('luigi.close-modal' === e.data.msg) { - dispatch('close', { 'type': 'modal' }); + dispatch('close', { type: 'modal' }); } }; - const backdropStateChanged = (event) => { - if (event && event.detail && event.detail.backdropActive && event.detail.drawer) { + const backdropStateChanged = event => { + if ( + event && + event.detail && + event.detail.backdropActive && + event.detail.drawer + ) { //renderBackdrop = false; } - } + }; onMount(() => { EventListenerHelpers.addEventListener('message', onMessage); @@ -250,7 +282,7 @@

{settings.title}

\ No newline at end of file + diff --git a/core/src/SplitView.html b/core/src/SplitView.html index 9c0b498303..347236eb67 100644 --- a/core/src/SplitView.html +++ b/core/src/SplitView.html @@ -60,6 +60,8 @@

{splitViewSettings.title}

let messageHandler; let splitViewIframe; let splitViewIframeData; + let splitViewWC; + let splitViewWCData; export let nodepath; export let collapsed; export let splitViewSettings = {}; @@ -83,7 +85,9 @@

{splitViewSettings.title}

nodeParams, currentNode, splitViewIframe, - splitViewIframeData + splitViewIframeData, + splitViewWC, + splitViewWCData }; }, set: obj => { @@ -107,6 +111,10 @@

{splitViewSettings.title}

splitViewIframe = obj.splitViewIframe; } else if (prop === 'splitViewIframeData') { splitViewIframeData = obj.splitViewIframeData; + } else if (prop === 'splitViewWC') { + splitViewWC = obj.splitViewWC; + } else if (prop === 'splitViewWCData') { + splitViewWCData = obj.splitViewWCData; } }); } diff --git a/core/src/services/routing.js b/core/src/services/routing.js index 1c51ccc5e9..0f620666d6 100644 --- a/core/src/services/routing.js +++ b/core/src/services/routing.js @@ -208,7 +208,7 @@ class RoutingClass { ); const viewUrl = nodeObject.viewUrl || ''; - if (!viewUrl) { + if (!viewUrl && !nodeObject.compound) { const defaultChildNode = await RoutingHelpers.getDefaultChildNode( pathData, async (node, ctx) => { @@ -352,17 +352,16 @@ class RoutingClass { Navigation.onNodeChange(previousNode, currentNode); } } - if (nodeObject.webcomponent) { + if (nodeObject.compound && GenericHelpers.requestExperimentalFeature('webcomponents', true)) { if (iContainer) { iContainer.classList.add('lui-webComponent'); } - this.navigateWebComponent( - config, - component, - iframeElement, - nodeObject, - iContainer - ); + this.navigateWebComponentCompound(config, component, iframeElement, nodeObject, iContainer); + } else if (nodeObject.webcomponent && GenericHelpers.requestExperimentalFeature('webcomponents', true)) { + if (iContainer) { + iContainer.classList.add('lui-webComponent'); + } + this.navigateWebComponent(config, component, iframeElement, nodeObject, iContainer); } else { if (iContainer) { iContainer.classList.remove('lui-webComponent'); @@ -527,9 +526,23 @@ class RoutingClass { WebComponentService.renderWebComponent( componentData.viewUrl, wc_container, - componentData.context + componentData.context, + navNode ); } + + + + navigateWebComponentCompound(config, component, node, navNode, iframeContainer) { + const componentData = component.get(); + const wc_container = document.querySelector('.wcContainer'); + + while (wc_container.lastChild) { + wc_container.lastChild.remove(); + } + + WebComponentService.renderWebComponentCompound(navNode, wc_container, componentData.context); + } } export const Routing = new RoutingClass(); diff --git a/core/src/services/split-view.js b/core/src/services/split-view.js index ebc25d1910..c6e31e3161 100644 --- a/core/src/services/split-view.js +++ b/core/src/services/split-view.js @@ -5,6 +5,7 @@ import { IframeHelpers, RoutingHelpers } from '../utilities/helpers'; +import { WebComponentService } from './web-components'; class SplitViewSvcClass { constructor() { @@ -88,25 +89,43 @@ class SplitViewSvcClass { createAndSetView(component) { const { nodeParams, lastNode, pathData } = component.get(); - const iframe = this.setIframe( - lastNode.viewUrl, - { - context: pathData.context, - pathParams: pathData.pathParams, - nodeParams - }, - component - ); + if (lastNode.webcomponent) { + WebComponentService.renderWebComponent( + lastNode.viewUrl, + document.querySelector('.iframeSplitViewCnt'), + pathData.context, + lastNode + ); + const wcInfo = { + splitViewWC: document.querySelector('.iframeSplitViewCnt'), + splitViewWCData: { ...pathData, nodeParams } + }; + component.set(wcInfo); + component.dispatch('wcCreated', { + ...wcInfo, + ...{ collapsed: false } + }); + } else { + const iframe = this.setIframe( + lastNode.viewUrl, + { + context: pathData.context, + pathParams: pathData.pathParams, + nodeParams + }, + component + ); - const iframeInfo = { - splitViewIframe: iframe, - splitViewIframeData: { ...pathData, nodeParams } - }; - component.set(iframeInfo); - component.dispatch('iframeCreated', { - ...iframeInfo, - ...{ collapsed: false } - }); + const iframeInfo = { + splitViewIframe: iframe, + splitViewIframeData: { ...pathData, nodeParams } + }; + component.set(iframeInfo); + component.dispatch('iframeCreated', { + ...iframeInfo, + ...{ collapsed: false } + }); + } this.fixIOSscroll(); } @@ -208,9 +227,13 @@ class SplitViewSvcClass { } close(comp) { - if (comp.get().splitViewIframe) { + if (comp.get().splitViewIframe || comp.get().splitViewWC) { comp - .getUnsavedChangesModalPromise(comp.get().splitViewIframe.contentWindow) + .getUnsavedChangesModalPromise( + comp.get().splitViewWC + ? comp.get().splitViewWC + : comp.get().splitViewIframe.contentWindow + ) .then(() => { if (comp.get().mfSplitView) { comp.get().mfSplitView.displayed = false; @@ -250,9 +273,13 @@ class SplitViewSvcClass { } collapse(comp) { - if (comp.get().splitViewIframe) { + if (comp.get().splitViewIframe || comp.get().splitViewWC) { comp - .getUnsavedChangesModalPromise(comp.get().splitViewIframe.contentWindow) + .getUnsavedChangesModalPromise( + comp.get().splitViewWC + ? comp.get().splitViewWC + : comp.get().splitViewIframe.contentWindow + ) .then(() => { this.sendMessageToClients('internal', { exists: true, diff --git a/core/src/services/web-components.js b/core/src/services/web-components.js index 4c39248368..a98aaadca4 100644 --- a/core/src/services/web-components.js +++ b/core/src/services/web-components.js @@ -1,20 +1,48 @@ +import { + DefaultCompoundRenderer, + resolveRenderer, + registerEventListeners +} from '../utilities/helpers/web-component-helpers'; +import { LuigiConfig } from '../core-api'; + /** Methods for dealing with web components based micro frontend handling */ class WebComponentSvcClass { constructor() {} dynamicImport(viewUrl) { /** __luigi_dyn_import() is replaced by import() after webpack is done, - * because webpack can't let his hands off imports ;) */ + * because webpack can't let his hands off imports ;) */ return __luigi_dyn_import(viewUrl); } - /** Creates a web component with tagname wc_id and adds it to wcItemContainer, if attached to wc_container*/ - attachWC(wc_id, wcItemContainer, wc_container, ctx) { - if(wc_container && wc_container.contains(wcItemContainer)) { + /** Creates a web component with tagname wc_id and adds it to wcItemContainer, + * if attached to wc_container + */ + attachWC(wc_id, wcItemPlaceholder, wc_container, ctx, viewUrl, nodeId) { + if (wc_container && wc_container.contains(wcItemPlaceholder)) { const wc = document.createElement(wc_id); - wc.context = ctx; - wc.luigi = window.Luigi; - wcItemContainer.appendChild(wc); + if (nodeId) { + wc.setAttribute('nodeId', nodeId); + } + + const clientAPI = { + linkManager: window.Luigi.navigation, + uxManager: window.Luigi.ux, + publishEvent: ev => { + if (wc_container.eventBus) { + wc_container.eventBus.onPublishEvent(ev, nodeId, wc_id); + } + } + }; + + if (wc.__postProcess) { + const url = new URL('./', viewUrl); + wc.__postProcess(ctx, clientAPI, url.origin + url.pathname); + } else { + wc.context = ctx; + wc.LuigiClient = clientAPI; + } + wc_container.replaceChild(wc, wcItemPlaceholder); } } @@ -24,50 +52,271 @@ class WebComponentSvcClass { */ generateWCId(viewUrl) { let charRep = ''; - for(let i = 0; i < viewUrl.length; i++) { + for (let i = 0; i < viewUrl.length; i++) { charRep += viewUrl.charCodeAt(i).toString(16); } return 'luigi-wc-' + charRep; } /** Does a module import from viewUrl and defines a new web component - * with the default export of the module. - * returns a promise that gets resolved after successfull import */ + * with the default export of the module or the first export extending HTMLElement if no default is + * specified. + * @returns a promise that gets resolved after successfull import */ registerWCFromUrl(viewUrl, wc_id) { return new Promise((resolve, reject) => { - this.dynamicImport(viewUrl).then(module => { - try { - window.customElements.define(wc_id, module.default); - resolve(); - } catch(e) { - reject(e); - } - }).catch(err => reject(err)); + if (this.checkWCUrl(viewUrl)) { + this.dynamicImport(viewUrl) + .then(module => { + try { + if (!window.customElements.get(wc_id)) { + let cmpClazz = module.default; + if (!HTMLElement.isPrototypeOf(cmpClazz)) { + let props = Object.keys(module); + for (let i = 0; i < props.length; i++) { + cmpClazz = module[props[i]]; + if (HTMLElement.isPrototypeOf(cmpClazz)) { + break; + } + } + } + window.customElements.define(wc_id, cmpClazz); + } + resolve(); + } catch (e) { + reject(e); + } + }) + .catch(err => reject(err)); + } else { + console.warn(`View URL '${viewUrl}' not allowed to be included`); + reject(`View URL '${viewUrl}' not allowed`); + } }); } + /** + * Handles the import of self registered web component bundles, i.e. the web component + * is added to the customElements registry by the bundle code rather than by luigi. + * + * @param {*} node the corresponding navigation node + * @param {*} viewUrl the source of the wc bundle + * @param {*} onload callback function executed after script attached and loaded + */ + includeSelfRegisteredWCFromUrl(node, viewUrl, onload) { + if (this.checkWCUrl(viewUrl)) { + /** Append reg function to luigi object if not present */ + if (!window.Luigi._registerWebcomponent) { + window.Luigi._registerWebcomponent = (srcString, el) => { + window.customElements.define(this.generateWCId(srcString), el); + }; + } + + let scriptTag = document.createElement('script'); + scriptTag.setAttribute('src', viewUrl); + if (node.webcomponent.type === 'module') { + scriptTag.setAttribute('type', 'module'); + } + scriptTag.setAttribute('defer', true); + scriptTag.addEventListener('load', () => { + onload(); + }); + document.body.appendChild(scriptTag); + } else { + console.warn(`View URL '${viewUrl}' not allowed to be included`); + } + } + + /** + * Checks if a url is allowed to be included, based on 'navigation.validWebcomponentUrls' in luigi config. + * Returns true, if allowed. + * + * @param {*} url the url string to check + */ + checkWCUrl(url) { + if (url.indexOf('://') > 0 || url.trim().indexOf('//') === 0) { + const ur = new URL(url); + if (ur.host === window.location.host) { + return true; // same host is okay + } + + const valids = LuigiConfig.getConfigValue( + 'navigation.validWebcomponentUrls' + ); + if (valids && valids.length > 0) { + for (let el of valids) { + try { + if (new RegExp(el).test(url)) { + return true; + } + } catch (e) { + console.error(e); + } + } + } + return false; + } + // relative URL is okay + return true; + } + /** Adds a web component defined by viewUrl to the wc_container and sets the node context. * If the web component is not defined yet, it gets imported. */ - renderWebComponent(viewUrl, wc_container, context) { - const wc_id = this.generateWCId(viewUrl); - const wcItemCnt = document.createElement('div'); - wc_container.appendChild(wcItemCnt); + renderWebComponent(viewUrl, wc_container, context, node, nodeId) { + const wc_id = + node.webcomponent && node.webcomponent.tagName + ? node.webcomponent.tagName + : this.generateWCId(viewUrl); + const wcItemPlaceholder = document.createElement('div'); + wc_container.appendChild(wcItemPlaceholder); if (window.customElements.get(wc_id)) { - this.attachWC(wc_id, wcItemCnt, wc_container, context); + this.attachWC( + wc_id, + wcItemPlaceholder, + wc_container, + context, + viewUrl, + nodeId + ); } else { /** Custom import function, if defined */ - if(window.luigiWCFn) { - window.luigiWCFn(viewUrl, wc_id, wcItemCnt, () => { - this.attachWC(wc_id, wcItemCnt, wc_container, context); + if (window.luigiWCFn) { + window.luigiWCFn(viewUrl, wc_id, wcItemPlaceholder, () => { + this.attachWC( + wc_id, + wcItemPlaceholder, + wc_container, + context, + viewUrl, + nodeId + ); + }); + } else if (node.webcomponent && node.webcomponent.selfRegistered) { + this.includeSelfRegisteredWCFromUrl(node, viewUrl, () => { + this.attachWC( + wc_id, + wcItemPlaceholder, + wc_container, + context, + viewUrl, + nodeId + ); }); } else { this.registerWCFromUrl(viewUrl, wc_id).then(() => { - this.attachWC(wc_id, wcItemCnt, wc_container, context); + this.attachWC( + wc_id, + wcItemPlaceholder, + wc_container, + context, + viewUrl, + nodeId + ); + }); + } + } + } + + /** + * Creates a compound container according to the given renderer. + * Returns a promise that gets resolved with the created container DOM element. + * + * @param {DefaultCompoundRenderer} renderer + */ + createCompoundContainerAsync(renderer) { + return new Promise(resolve => { + if (renderer.viewUrl) { + const wc_id = this.generateWCId(renderer.viewUrl); + this.registerWCFromUrl(renderer.viewUrl, wc_id).then(() => { + resolve(document.createElement(wc_id)); }); + } else { + resolve(renderer.createCompoundContainer()); } + }); + } + + /** + * Responsible for rendering web component compounds based on a renderer or a nesting + * micro frontend. + * + * @param {*} navNode the navigation node defining the compound + * @param {*} wc_container the web component container dom element + * @param {*} context the luigi node context + */ + renderWebComponentCompound(navNode, wc_container, context) { + let renderer; + + if (navNode.webcomponent && navNode.viewUrl) { + renderer = new DefaultCompoundRenderer(); + renderer.viewUrl = navNode.viewUrl; + renderer.createCompoundItemContainer = layoutConfig => { + var cnt = document.createElement('div'); + if (layoutConfig && layoutConfig.slot) { + cnt.setAttribute('slot', layoutConfig.slot); + } + return cnt; + }; + } else if (navNode.compound.renderer) { + renderer = resolveRenderer(navNode.compound.renderer); } + + renderer = renderer || new DefaultCompoundRenderer(); + + return new Promise(resolve => { + this.createCompoundContainerAsync(renderer).then(compoundCnt => { + const ebListeners = {}; + compoundCnt.eventBus = { + listeners: ebListeners, + onPublishEvent: (event, srcNodeId, wcId) => { + const listeners = ebListeners[srcNodeId + '.' + event.type] || []; + listeners.push(...(ebListeners['*.' + event.type] || [])); + + listeners.forEach(listenerInfo => { + const target = + listenerInfo.wcElement || + compoundCnt.querySelector( + '[nodeId=' + listenerInfo.wcElementId + ']' + ); + if (target) { + target.dispatchEvent( + new CustomEvent(listenerInfo.action, { + detail: listenerInfo.converter + ? listenerInfo.converter(event.detail) + : event.detail + }) + ); + } else { + console.debug('Could not find event target', listenerInfo); + } + }); + } + }; + navNode.compound.children.forEach((wc, index) => { + const ctx = { ...context, ...wc.context }; + const compoundItemCnt = renderer.createCompoundItemContainer( + wc.layoutConfig + ); + compoundItemCnt.eventBus = compoundCnt.eventBus; + renderer.attachCompoundItem(compoundCnt, compoundItemCnt); + + const nodeId = wc.id || 'gen_' + index; + this.renderWebComponent(wc.viewUrl, compoundItemCnt, ctx, wc, nodeId); + registerEventListeners(ebListeners, wc, nodeId); + }); + wc_container.appendChild(compoundCnt); + + // listener for nesting wc + registerEventListeners( + ebListeners, + navNode.compound, + undefined, + compoundCnt + ); + resolve(compoundCnt); + }); + }); } } diff --git a/core/src/utilities/helpers/generic-helpers.js b/core/src/utilities/helpers/generic-helpers.js index 49d412d50b..b860ebfcb4 100644 --- a/core/src/utilities/helpers/generic-helpers.js +++ b/core/src/utilities/helpers/generic-helpers.js @@ -1,5 +1,6 @@ // Standalone or partly-standalone methods that are used widely through the whole app and are synchronous. import { LuigiElements } from '../../core-api'; +import { LuigiConfig } from '../../core-api'; class GenericHelpersClass { /** @@ -319,6 +320,22 @@ class GenericHelpersClass { } return 0; } + + /** + * Checks, if an experimental feature is enabled under settings.experminental + * + * @param {*} expFeatureName the feature name to check for + * @param {*} showWarn if true, prints a warning on js console that feature is not enabled + * + * @returns true, if feature enabled, false otherwise. + */ + requestExperimentalFeature(expFeatureName, showWarn) { + var val = LuigiConfig.getConfigValue('settings.experimental.' + expFeatureName); + if(showWarn && !val) { + console.warn("Experimental feature not enabled: ", expFeatureName); + } + return val; + } } export const GenericHelpers = new GenericHelpersClass(); diff --git a/core/src/utilities/helpers/iframe-helpers.js b/core/src/utilities/helpers/iframe-helpers.js index 3f7fab6847..224bc01ad3 100644 --- a/core/src/utilities/helpers/iframe-helpers.js +++ b/core/src/utilities/helpers/iframe-helpers.js @@ -61,9 +61,9 @@ class IframeHelpersClass { new RegExp( GenericHelpers.escapeRegExp( (parenthesis ? '{' : '') + - prefix + - entry[0] + - (parenthesis ? '}' : '') + prefix + + entry[0] + + (parenthesis ? '}' : '') ), 'g' ), diff --git a/core/src/utilities/helpers/index.js b/core/src/utilities/helpers/index.js index d46d124bb5..11c599fb3a 100644 --- a/core/src/utilities/helpers/index.js +++ b/core/src/utilities/helpers/index.js @@ -7,4 +7,4 @@ export * from './navigation-helpers'; export * from './routing-helpers'; export * from './state-helpers'; export * from './event-listener-helpers'; -export * from './storage-helper' +export * from './storage-helper'; diff --git a/core/src/utilities/helpers/navigation-helpers.js b/core/src/utilities/helpers/navigation-helpers.js index b773ea5ef7..dd33406455 100644 --- a/core/src/utilities/helpers/navigation-helpers.js +++ b/core/src/utilities/helpers/navigation-helpers.js @@ -261,22 +261,24 @@ class NavigationHelpersClass { storeExpandedState(key, value, replace = false) { let expandedList = this.loadExpandedCategories(); - + //get conxtext for siblings - let context = key.split(':')[0] + let context = key.split(':')[0]; if (value) { if (replace) { // Filter out other categories - expandedList = expandedList.filter(f => f.indexOf(context + ':') === -1) - } - + expandedList = expandedList.filter( + f => f.indexOf(context + ':') === -1 + ); + } + if (expandedList.indexOf(key) < 0) { expandedList.push(key); } } else { let index = expandedList.indexOf(key); if (index >= 0) { - expandedList.splice(index, 1); + expandedList.splice(index, 1); } } localStorage.setItem(this.EXP_CAT_KEY, JSON.stringify(expandedList)); diff --git a/core/src/utilities/helpers/storage-helper.js b/core/src/utilities/helpers/storage-helper.js index c2b77d2bef..7e4d0f2e28 100644 --- a/core/src/utilities/helpers/storage-helper.js +++ b/core/src/utilities/helpers/storage-helper.js @@ -2,52 +2,56 @@ import { IframeHelpers } from './iframe-helpers'; class StorageHelperClass { constructor() { - this.init=false; - this.storage=undefined; - this.browseSupported=undefined; + this.init = false; + this.storage = undefined; + this.browseSupported = undefined; } - checkInit(){ - if (this.init){ + checkInit() { + if (this.init) { return; } - this.storage = window.localStorage + this.storage = window.localStorage; this.browseSupported = this.supportLocalStorage(); - this.init=true; + this.init = true; } - supportLocalStorage(){ + supportLocalStorage() { try { return 'localStorage' in window && window['localStorage'] !== null; - } catch(e) { + } catch (e) { return false; } } - checkStorageBrowserSupport(){ - if (!this.browseSupported){ - throw "Browser does not support local storage" + checkStorageBrowserSupport() { + if (!this.browseSupported) { + throw 'Browser does not support local storage'; } } - process(microfrontendId, hostname, id, operation, params){ - try{ - this.checkInit(); - this.checkStorageBrowserSupport(); - const operationFunction = this[operation]; - if (typeof operationFunction !== 'function'){ - throw operation + " is not a supported operation for the storage"; - } - const result = operationFunction.bind(this, this.cleanHostname(hostname), params)(); - this.sendBackOperation(microfrontendId, id, 'OK', result); - }catch(error){ - console.log(error); - this.sendBackOperation(microfrontendId, id, 'ERROR', error); + process(microfrontendId, hostname, id, operation, params) { + try { + this.checkInit(); + this.checkStorageBrowserSupport(); + const operationFunction = this[operation]; + if (typeof operationFunction !== 'function') { + throw operation + ' is not a supported operation for the storage'; } + const result = operationFunction.bind( + this, + this.cleanHostname(hostname), + params + )(); + this.sendBackOperation(microfrontendId, id, 'OK', result); + } catch (error) { + console.log(error); + this.sendBackOperation(microfrontendId, id, 'ERROR', error); + } } - cleanHostname(hostname){ - return hostname.replace('http://','').replace("https://",''); + cleanHostname(hostname) { + return hostname.replace('http://', '').replace('https://', ''); } setItem(hostname, params) { @@ -57,69 +61,69 @@ class StorageHelperClass { this.storage.setItem(key, value); } - getItem(hostname, params){ + getItem(hostname, params) { this.checkKey(params); const key = this.buildKey(hostname, params.key); const item = this.storage.getItem(key); - if (item){ + if (item) { return this.parseJsonIfPossible(item); - }else{ + } else { return undefined; } } - buildKey(hostname, subKey){ - return this.buildPrefix(hostname) + subKey.trim(); + buildKey(hostname, subKey) { + return this.buildPrefix(hostname) + subKey.trim(); } - buildPrefix(hostname){ - return "Luigi#"+ hostname + "#"; + buildPrefix(hostname) { + return 'Luigi#' + hostname + '#'; } - removeItem(hostname, params){ + removeItem(hostname, params) { this.checkKey(params); const key = this.buildKey(hostname, params.key); const item = this.storage.getItem(key); - if (item){ + if (item) { this.storage.removeItem(key); return item; - }else{ + } else { return undefined; } } clear(hostname, params) { - const keyPrefix = this.buildPrefix(hostname); + const keyPrefix = this.buildPrefix(hostname); Object.keys(this.storage) .filter(key => key.startsWith(keyPrefix)) .forEach(key => this.storage.removeItem(key)); } - has(hostname, params){ + has(hostname, params) { this.checkKey(params); const key = this.buildKey(hostname, params.key); const item = this.storage.getItem(key); - if (item){ + if (item) { return true; - }else{ + } else { return false; } } - getAllKeys(hostname, params){ - const keyPrefix = this.buildPrefix(hostname); + getAllKeys(hostname, params) { + const keyPrefix = this.buildPrefix(hostname); return Object.keys(this.storage) .filter(key => key.startsWith(keyPrefix)) - .map(key=> key.substring(keyPrefix.length)); + .map(key => key.substring(keyPrefix.length)); } - checkKey(params){ - if (!params.key || params.key.trim().length === 0){ - throw "Missing key, we cannot execute storage operation"; + checkKey(params) { + if (!params.key || params.key.trim().length === 0) { + throw 'Missing key, we cannot execute storage operation'; } } - parseJsonIfPossible(text){ + parseJsonIfPossible(text) { try { return JSON.parse(text); } catch (e) { @@ -127,21 +131,21 @@ class StorageHelperClass { } } - stringifyValue(value){ - if (!value){ - throw "Value is empty"; + stringifyValue(value) { + if (!value) { + throw 'Value is empty'; } - if (typeof value === 'string' || value instanceof String){ + if (typeof value === 'string' || value instanceof String) { return value; } try { return JSON.stringify(value); } catch (error) { - throw "Value cannot be stringify, error: "+error; + throw 'Value cannot be stringify, error: ' + error; } } - sendBackOperation(microfrontendId, id, status, result){ + sendBackOperation(microfrontendId, id, status, result) { let message = { msg: 'storage', data: { @@ -149,11 +153,13 @@ class StorageHelperClass { status, result } - } + }; IframeHelpers.getMicrofrontendsInDom() .filter(microfrontendObj => microfrontendObj.id === microfrontendId) .map(microfrontendObj => microfrontendObj.container) - .map(mfContainer => IframeHelpers.sendMessageToIframe(mfContainer,message )); + .map(mfContainer => + IframeHelpers.sendMessageToIframe(mfContainer, message) + ); } } diff --git a/core/src/utilities/helpers/web-component-helpers.js b/core/src/utilities/helpers/web-component-helpers.js new file mode 100644 index 0000000000..863cde176e --- /dev/null +++ b/core/src/utilities/helpers/web-component-helpers.js @@ -0,0 +1,176 @@ +/** + * Default compound renderer. + */ +export class DefaultCompoundRenderer { + constructor(rendererObj) { + if(rendererObj) { + this.rendererObject = rendererObj; + this.config = rendererObj.config || {}; + } else { + this.config = {}; + } + } + + createCompoundContainer() { + return document.createElement('div'); + } + + createCompoundItemContainer() { + return document.createElement('div'); + } + + attachCompoundItem(compoundCnt, compoundItemCnt) { + compoundCnt.appendChild(compoundItemCnt); + } +} + +/** + * Compound Renderer for custom rendering as defined in luigi config. + */ +export class CustomCompoundRenderer extends DefaultCompoundRenderer { + constructor(rendererObj) { + super(rendererObj || { use: {} }); + if(rendererObj && rendererObj.use && rendererObj.use.extends) { + this.superRenderer = resolveRenderer({ + use: rendererObj.use.extends, + config: rendererObj.config + }); + } + } + + createCompoundContainer() { + if(this.rendererObject.use.createCompoundContainer) { + return this.rendererObject.use.createCompoundContainer(this.config, this.superRenderer); + } else if (this.superRenderer) { + return this.superRenderer.createCompoundContainer(); + } + return super.createCompoundContainer(); + } + + createCompoundItemContainer(layoutConfig) { + if(this.rendererObject.use.createCompoundItemContainer) { + return this.rendererObject.use.createCompoundItemContainer(layoutConfig, this.config, this.superRenderer); + } else if (this.superRenderer) { + return this.superRenderer.createCompoundItemContainer(layoutConfig); + } + return super.createCompoundItemContainer(layoutConfig); + } + + attachCompoundItem(compoundCnt, compoundItemCnt) { + if(this.rendererObject.use.attachCompoundItem) { + this.rendererObject.use.attachCompoundItem(compoundCnt, compoundItemCnt, this.superRenderer); + } else if (this.superRenderer) { + this.superRenderer.attachCompoundItem(compoundCnt, compoundItemCnt); + } else { + super.attachCompoundItem(compoundCnt, compoundItemCnt); + } + } +} + +/** + * Compound Renderer for a css grid compound view. + */ +export class GridCompoundRenderer extends DefaultCompoundRenderer { + createCompoundContainer() { + const containerClass = '__lui_compound_' + new Date().getTime(); + const compoundCnt = document.createElement('div'); + compoundCnt.classList.add(containerClass); + let mediaQueries = ''; + + if(this.config.layouts) { + this.config.layouts.forEach(el => { + if(el.minWidth || el.maxWidth) { + let mq = '@media only screen '; + if(el.minWidth != null) { + mq += `and (min-width: ${el.minWidth}px) ` + } + if(el.maxWidth != null) { + mq += `and (max-width: ${el.maxWidth}px) ` + } + + mq += `{ + .${containerClass} { + grid-template-columns: ${el.columns || 'auto'}; + grid-template-rows: ${el.rows || 'auto'}; + grid-gap: ${el.gap || '0'}; + } + } + `; + mediaQueries += mq; + } + }); + } + + compoundCnt.innerHTML = /*html*/` + + `; + return compoundCnt; + } + + createCompoundItemContainer(layoutConfig) { + const config = layoutConfig || {}; + const compoundItemCnt = document.createElement('div'); + compoundItemCnt.setAttribute('style', `grid-row: ${config.row || 'auto'}; grid-column: ${config.column || 'auto'}`); + return compoundItemCnt; + } +} + +/** + * Returns the compound renderer class for a given config. + * If no specific one is found, {DefaultCompoundRenderer} is returned. + * + * @param {*} rendererConfig the renderer config object defined in luigi config + */ +export const resolveRenderer = (rendererConfig) => { + const rendererDef = rendererConfig.use; + if(!rendererDef) { + return new DefaultCompoundRenderer(rendererConfig); + } + else if(rendererDef === 'grid') { + return new GridCompoundRenderer(rendererConfig); + } else if(rendererDef.createCompoundContainer + || rendererDef.createCompoundItemContainer + || rendererDef.attachCompoundItem) { + return new CustomCompoundRenderer(rendererConfig); + } + return new DefaultCompoundRenderer(rendererConfig); +}; + + +/** + * Registers event listeners defined at the navNode. + * + * @param {*} eventbusListeners a map of event listener arrays with event id as key + * @param {*} navNode the web component node configuration object + * @param {*} nodeId the web component node id + * @param {*} wcElement the web component element - optional + */ +export const registerEventListeners = (eventbusListeners, navNode, nodeId, wcElement) => { + if(navNode.eventListeners) { + navNode.eventListeners.forEach(el => { + const evID = el.source + '.' + el.name; + const listenerList = eventbusListeners[evID]; + const listenerInfo = { + wcElementId: nodeId, + wcElement: wcElement, + action: el.action, + converter: el.dataConverter + }; + + if(listenerList) { + listenerList.push(listenerInfo); + } else { + eventbusListeners[evID] = [listenerInfo]; + } + }); + } +} diff --git a/core/test/services/web-components.spec.js b/core/test/services/web-components.spec.js index 8259c7ee46..025c23d3a9 100644 --- a/core/test/services/web-components.spec.js +++ b/core/test/services/web-components.spec.js @@ -4,10 +4,15 @@ const expect = chai.expect; const assert = chai.assert; import { WebComponentService } from '../../src/services/web-components'; +import { LuigiConfig } from '../../src/core-api'; +import { DefaultCompoundRenderer } from '../../src/utilities/helpers/web-component-helpers'; +import { LuigiElement } from '../../../client/src/luigi-element'; +import { fail } from 'sinon/lib/sinon/mock-expectation'; describe('WebComponentService', function() { describe('generate web component id', function() { - const someRandomString = 'dsfgljhbakjdfngb,mdcn vkjrzwero78to4 wfoasb f,asndbf'; + const someRandomString = + 'dsfgljhbakjdfngb,mdcn vkjrzwero78to4 wfoasb f,asndbf'; it('check determinism', () => { let wcId = WebComponentService.generateWCId(someRandomString); @@ -17,109 +22,184 @@ describe('WebComponentService', function() { it('check uniqueness', () => { let wcId = WebComponentService.generateWCId(someRandomString); - let wcId2 = WebComponentService.generateWCId('someOtherRandomString_9843utieuhfgiasdf'); + let wcId2 = WebComponentService.generateWCId( + 'someOtherRandomString_9843utieuhfgiasdf' + ); expect(wcId).to.not.equal(wcId2); }); }); describe('attach web component', function() { - const container = document.createElement('div'); - const itemContainer = document.createElement('div'); - const ctx = { someValue: true}; + const sb = sinon.createSandbox(); + let container; + let itemPlaceholder; + const ctx = { someValue: true }; + + before(() => { + window.Luigi = { + navigation: 'mock1', + ux: 'mock2' + }; + }); - before(()=>{ - window.Luigi = { mario: 'luigi', luigi: window.luigi }; + afterEach(() => { + sb.restore(); }); - after(()=>{ - window.Luigi = window.Luigi.luigi; + beforeEach(() => { + container = document.createElement('div'); + itemPlaceholder = document.createElement('div'); }); it('check dom injection abort if container not attached', () => { - WebComponentService.attachWC('div', itemContainer, container, ctx); + WebComponentService.attachWC('div', itemPlaceholder, container, ctx); - expect(itemContainer.children.length).to.equal(0); + expect(container.children.length).to.equal(0); }); it('check dom injection', () => { - container.appendChild(itemContainer); - WebComponentService.attachWC('div', itemContainer, container, ctx); + container.appendChild(itemPlaceholder); + WebComponentService.attachWC('div', itemPlaceholder, container, ctx); - const expectedCmp = itemContainer.children[0]; + const expectedCmp = container.children[0]; expect(expectedCmp.context).to.equal(ctx); - expect(expectedCmp.luigi).to.equal(window.Luigi); + expect(expectedCmp.LuigiClient.linkManager).to.equal( + window.Luigi.navigation + ); + expect(expectedCmp.LuigiClient.uxManager).to.equal(window.Luigi.ux); + expect(expectedCmp.LuigiClient.publishEvent).to.be.a('function'); + }); + + it('check post-processing', () => { + const wc_id = 'my-wc'; + var MyLuigiElement = class extends LuigiElement { + render(ctx) { + return '
'; + } + }; + + var myEl = Object.create(MyLuigiElement.prototype, {}); + sb.stub(myEl, '__postProcess').callsFake(() => {}); + sb.stub(document, 'createElement') + .callThrough() + .withArgs('my-wc') + .callsFake(() => { + return myEl; + }); + sb.stub(container, 'replaceChild').callsFake(() => {}); + + container.appendChild(itemPlaceholder); + WebComponentService.attachWC( + wc_id, + itemPlaceholder, + container, + ctx, + 'http://localhost:8080/' + ); + + assert(myEl.__postProcess.calledOnce, '__postProcess should be called'); }); }); describe('register web component from url', function() { const sb = sinon.createSandbox(); - afterEach(()=>{ + afterEach(() => { sb.restore(); }); - it('check resolve', (done) => { + it('check resolve', done => { let definedId; - sb.stub(WebComponentService, 'dynamicImport').returns(new Promise((resolve, reject) => { - resolve({ default: {} }); - })); - window.customElements = { define: (id, clazz)=>{ - definedId = id; - }}; - - WebComponentService.registerWCFromUrl('url', 'id').then(()=>{ + sb.stub(WebComponentService, 'dynamicImport').returns( + new Promise((resolve, reject) => { + resolve({ default: {} }); + }) + ); + window.customElements = { + define: (id, clazz) => { + definedId = id; + }, + get: id => { + return undefined; + } + }; + + WebComponentService.registerWCFromUrl('url', 'id').then(() => { expect(definedId).to.equal('id'); done(); }); }); - it('check reject', (done) => { + it('check reject', done => { let definedId; - sb.stub(WebComponentService, 'dynamicImport').returns(new Promise((resolve, reject) => { - reject({ default: {} }); - })); - window.customElements = { define: (id, clazz)=>{ - definedId = id; - }}; - - WebComponentService.registerWCFromUrl('url', 'id').then(()=>{ - assert(false, "should not be here"); - done(); - }).catch(err=>{ - expect(definedId).to.be.undefined; - done(); - }); + sb.stub(WebComponentService, 'dynamicImport').returns( + new Promise((resolve, reject) => { + reject({ default: {} }); + }) + ); + window.customElements = { + define: (id, clazz) => { + definedId = id; + } + }; + + WebComponentService.registerWCFromUrl('url', 'id') + .then(() => { + assert(false, 'should not be here'); + done(); + }) + .catch(err => { + expect(definedId).to.be.undefined; + done(); + }); + }); + + it('check reject due to not-allowed url', done => { + WebComponentService.registerWCFromUrl( + 'http://luigi-project.io/mfe.js', + 'id' + ) + .then(() => { + assert(false, 'should not be here'); + done(); + }) + .catch(err => { + done(); + }); }); }); describe('render web component', function() { const container = document.createElement('div'); - const ctx = { someValue: true}; + const ctx = { someValue: true }; const viewUrl = 'someurl'; const sb = sinon.createSandbox(); + const node = {}; - before(()=>{ + before(() => { window.Luigi = { mario: 'luigi', luigi: window.luigi }; }); - after(()=>{ + after(() => { window.Luigi = window.Luigi.luigi; }); - beforeEach(()=>{ - sb.stub(WebComponentService, 'dynamicImport').returns(new Promise((resolve, reject) => { - resolve({ default: {} }); - })); + beforeEach(() => { + sb.stub(WebComponentService, 'dynamicImport').returns( + new Promise((resolve, reject) => { + resolve({ default: {} }); + }) + ); }); - afterEach(()=>{ + afterEach(() => { sb.restore(); + delete window.luigiWCFn; }); - - it('check attachment of already existing wc', (done) => { + it('check attachment of already existing wc', done => { window.customElements = { - define: (id, clazz)=>{ + define: (id, clazz) => { definedId = id; }, get: () => { @@ -127,24 +207,26 @@ describe('WebComponentService', function() { } }; - sb.stub(WebComponentService, 'registerWCFromUrl').callsFake(()=>{ - assert(false, "should not be here"); + sb.stub(WebComponentService, 'registerWCFromUrl').callsFake(() => { + assert(false, 'should not be here'); }); - sb.stub(WebComponentService, 'attachWC').callsFake((id, iCnt, cnt, context)=>{ - expect(cnt).to.equal(container); - expect(context).to.equal(ctx); - done(); - }); + sb.stub(WebComponentService, 'attachWC').callsFake( + (id, iCnt, cnt, context) => { + expect(cnt).to.equal(container); + expect(context).to.equal(ctx); + done(); + } + ); - WebComponentService.renderWebComponent(viewUrl, container, ctx); + WebComponentService.renderWebComponent(viewUrl, container, ctx, node); }); - it('check invocation of custom function', (done) => { + it('check invocation of custom function', done => { let definedId; window.customElements = { - define: (id, clazz)=>{ + define: (id, clazz) => { definedId = id; }, get: () => { @@ -152,28 +234,30 @@ describe('WebComponentService', function() { } }; - sb.stub(WebComponentService, 'registerWCFromUrl').callsFake(()=>{ - assert(false, "should not be here"); + sb.stub(WebComponentService, 'registerWCFromUrl').callsFake(() => { + assert(false, 'should not be here'); }); - sb.stub(WebComponentService, 'attachWC').callsFake((id, iCnt, cnt, context)=>{ - expect(cnt).to.equal(container); - expect(context).to.equal(ctx); - done(); - }); + sb.stub(WebComponentService, 'attachWC').callsFake( + (id, iCnt, cnt, context) => { + expect(cnt).to.equal(container); + expect(context).to.equal(ctx); + done(); + } + ); window.luigiWCFn = (viewUrl, wc_id, wc_container, cb) => { cb(); - } + }; - WebComponentService.renderWebComponent(viewUrl, container, ctx); + WebComponentService.renderWebComponent(viewUrl, container, ctx, node); }); - it('check creation and attachment of new wc', (done) => { + it('check creation and attachment of new wc', done => { let definedId; window.customElements = { - define: (id, clazz)=>{ + define: (id, clazz) => { definedId = id; }, get: () => { @@ -181,20 +265,302 @@ describe('WebComponentService', function() { } }; - sb.stub(WebComponentService, 'registerWCFromUrl').callsFake(()=>{ + sb.stub(WebComponentService, 'registerWCFromUrl').callsFake(() => { return new Promise((resolve, reject) => { resolve(); - }) + }); }); - sb.stub(WebComponentService, 'attachWC').callsFake((id, iCnt, cnt, context)=>{ - expect(cnt).to.equal(container); - expect(context).to.equal(ctx); - done(); + sb.stub(WebComponentService, 'attachWC').callsFake( + (id, iCnt, cnt, context) => { + expect(cnt).to.equal(container); + expect(context).to.equal(ctx); + done(); + } + ); + + WebComponentService.renderWebComponent(viewUrl, container, ctx, node); + }); + }); + + describe('check valid wc url', function() { + const sb = sinon.createSandbox(); + + afterEach(() => { + sb.restore(); + }); + + it('check permission for relative and absolute urls from same domain', () => { + let relative1 = WebComponentService.checkWCUrl('/folder/sth.js'); + expect(relative1).to.be.true; + let relative2 = WebComponentService.checkWCUrl('folder/sth.js'); + expect(relative2).to.be.true; + let relative3 = WebComponentService.checkWCUrl('./folder/sth.js'); + expect(relative3).to.be.true; + + let absolute = WebComponentService.checkWCUrl( + window.location.href + '/folder/sth.js' + ); + expect(absolute).to.be.true; + }); + + it('check permission and denial for urls based on config', () => { + sb.stub(LuigiConfig, 'getConfigValue').returns([ + 'https://fiddle.luigi-project.io/.?', + 'https://docs.luigi-project.io/.?' + ]); + + let valid1 = WebComponentService.checkWCUrl( + 'https://fiddle.luigi-project.io/folder/sth.js' + ); + expect(valid1).to.be.true; + let valid2 = WebComponentService.checkWCUrl( + 'https://docs.luigi-project.io/folder/sth.js' + ); + expect(valid2).to.be.true; + + let invalid1 = WebComponentService.checkWCUrl( + 'http://fiddle.luigi-project.io/folder/sth.js' + ); + expect(invalid1).to.be.false; + let invalid2 = WebComponentService.checkWCUrl( + 'https://slack.luigi-project.io/folder/sth.js' + ); + expect(invalid2).to.be.false; + }); + }); + + describe('check includeSelfRegisteredWCFromUrl', function() { + const sb = sinon.createSandbox(); + const node = { + webcomponent: { + selfRegistered: true + } + }; + + before(() => { + window.Luigi = { mario: 'luigi', luigi: window.luigi }; + }); + + after(() => { + window.Luigi = window.Luigi.luigi; + }); + + afterEach(() => { + sb.restore(); + }); + + it('check if script tag is added', () => { + let element; + sb.stub(document.body, 'appendChild').callsFake(el => { + element = el; }); - WebComponentService.renderWebComponent(viewUrl, container, ctx); + WebComponentService.includeSelfRegisteredWCFromUrl( + node, + '/mfe.js', + () => {} + ); + expect(element.getAttribute('src')).to.equal('/mfe.js'); + }); + + it('check if script tag is not added for untrusted url', () => { + sb.spy(document.body, 'appendChild'); + WebComponentService.includeSelfRegisteredWCFromUrl( + node, + 'https://luigi-project.io/mfe.js', + () => {} + ); + assert(document.body.appendChild.notCalled); }); }); -}); + describe('check createCompoundContainerAsync', function() { + const sb = sinon.createSandbox(); + + afterEach(() => { + sb.restore(); + }); + + it('check compound container created', done => { + let renderer = new DefaultCompoundRenderer(); + sb.spy(renderer); + WebComponentService.createCompoundContainerAsync(renderer).then( + () => { + assert( + renderer.createCompoundContainer.calledOnce, + 'createCompoundContainer called once' + ); + done(); + }, + e => { + assert(false, 'should not be here'); + done(); + } + ); + }); + + it('check nesting mfe created', done => { + let renderer = new DefaultCompoundRenderer(); + renderer.viewUrl = 'mfe.js'; + sb.stub(WebComponentService, 'registerWCFromUrl').resolves(); + sb.spy(renderer); + WebComponentService.createCompoundContainerAsync(renderer).then( + () => { + assert( + renderer.createCompoundContainer.notCalled, + 'createCompoundContainer should not be called' + ); + assert( + WebComponentService.registerWCFromUrl.calledOnce, + 'registerWCFromUrl called once' + ); + done(); + }, + e => { + assert(false, 'should not be here'); + done(); + } + ); + }); + }); + + describe('check renderWebComponentCompound', function() { + const sb = sinon.createSandbox(); + + const context = { key: 'value', mario: 'luigi' }; + + const eventEmitter = 'emitterId'; + const eventName = 'emitterId'; + + const navNode = { + compound: { + eventListeners: [ + { + source: '*', + name: eventName, + action: 'update', + dataConverter: data => { + return 'new text: ' + data; + } + } + ], + children: [ + { + viewUrl: 'mfe1.js', + context: { + title: 'My Awesome Grid' + }, + layoutConfig: { + row: '1', + column: '1 / -1' + }, + eventListeners: [ + { + source: eventEmitter, + name: eventName, + action: 'update', + dataConverter: data => { + return 'new text: ' + data; + } + } + ] + }, + { + id: eventEmitter, + viewUrl: 'mfe2.js', + context: { + title: 'Some input', + instant: true + } + } + ] + } + }; + + before(() => { + window.Luigi = { mario: 'luigi', luigi: window.luigi }; + }); + + after(() => { + window.Luigi = window.Luigi.luigi; + }); + + afterEach(() => { + sb.restore(); + }); + + it('render flat compound', done => { + const wc_container = document.createElement('div'); + + sb.spy(WebComponentService, 'renderWebComponent'); + sb.stub(WebComponentService, 'registerWCFromUrl').resolves(); + + WebComponentService.renderWebComponentCompound( + navNode, + wc_container, + context + ) + .then(compoundCnt => { + expect(wc_container.children.length).to.equal(1); + + // eventbus test + let evBus = compoundCnt.eventBus; + const listeners = evBus.listeners[eventEmitter + '.' + eventName]; + expect(listeners.length).to.equal(1); + const target = compoundCnt.querySelector( + '[nodeId=' + listeners[0].wcElementId + ']' + ); + sb.spy(target, 'dispatchEvent'); + evBus.onPublishEvent(new CustomEvent(eventName), eventEmitter); + assert(target.dispatchEvent.calledOnce); + + // Check if renderWebComponent is called for each child + assert(WebComponentService.renderWebComponent.calledTwice); + + done(); + }) + .catch(() => { + fail(); + done(); + }); + }); + + it('render nested compound', done => { + const wc_container = document.createElement('div'); + const compoundCnt = document.createElement('div'); + const node = JSON.parse(JSON.stringify(navNode)); + node.viewUrl = 'mfe.js'; + node.webcomponent = true; + window.customElements = { + get: () => { + return false; + } + }; + + sb.stub(WebComponentService, 'registerWCFromUrl').resolves(); + + WebComponentService.renderWebComponentCompound( + node, + wc_container, + context + ).then( + compoundCnt => { + expect(WebComponentService.registerWCFromUrl.callCount).to.equal(3); + + // eventbus test + let evBus = compoundCnt.eventBus; + sb.spy(compoundCnt, 'dispatchEvent'); + evBus.onPublishEvent(new CustomEvent(eventName), eventEmitter); + assert(compoundCnt.dispatchEvent.calledOnce); + + done(); + }, + () => { + assert(false, 'should not be here'); + done(); + } + ); + }); + }); +}); diff --git a/core/test/utilities/helpers/iframe-helpers.spec.js b/core/test/utilities/helpers/iframe-helpers.spec.js index e4de017b61..b80068a5f9 100644 --- a/core/test/utilities/helpers/iframe-helpers.spec.js +++ b/core/test/utilities/helpers/iframe-helpers.spec.js @@ -68,7 +68,7 @@ describe('Iframe-helpers', () => { }); it('createIframe with interceptor', () => { - const icf = () => { }; + const icf = () => {}; const interceptor = sinon.spy(icf); sinon .stub(LuigiConfig, 'getConfigValue') @@ -133,21 +133,30 @@ describe('Iframe-helpers', () => { let domain = 'https://luigi.url.com/bla/bli'; let a1 = document.createElement('a'); let a2 = document.createElement('a'); - sb.stub(document, 'createElement').callThrough().withArgs('a').callsFake(() => { - if (a1.stubReturned) { - return a2; - } else { - a1.stubReturned = true; - return a1; - } - }); + sb.stub(document, 'createElement') + .callThrough() + .withArgs('a') + .callsFake(() => { + if (a1.stubReturned) { + return a2; + } else { + a1.stubReturned = true; + return a1; + } + }); // Mimic IE11 behaviour // no origin sb.stub(a1, 'origin').value(undefined); sb.stub(a2, 'origin').value(undefined); // add port to https urls - sb.stub(a1, 'host').get(() => { return a1.protocol === 'https:' ? a1.hostname + ':443' : a1.hostname }); - sb.stub(a2, 'host').get(() => { return a2.protocol === 'https:' ? a2.hostname + ':443' + a2.port : a2.hostname }); + sb.stub(a1, 'host').get(() => { + return a1.protocol === 'https:' ? a1.hostname + ':443' : a1.hostname; + }); + sb.stub(a2, 'host').get(() => { + return a2.protocol === 'https:' + ? a2.hostname + ':443' + a2.port + : a2.hostname; + }); assert.isTrue(IframeHelpers.urlMatchesTheDomain(href, domain)); expect(a1.host).to.equal('luigi.url.com:443'); expect(a2.host).to.equal('luigi.url.com:443'); diff --git a/core/test/utilities/helpers/storage-helpers.spec.js b/core/test/utilities/helpers/storage-helpers.spec.js index e47aaf1a45..5026550204 100644 --- a/core/test/utilities/helpers/storage-helpers.spec.js +++ b/core/test/utilities/helpers/storage-helpers.spec.js @@ -2,42 +2,64 @@ const chai = require('chai'); const assert = chai.assert; const sinon = require('sinon'); import { IframeHelpers, StorageHelper } from '../../../src/utilities/helpers'; -import 'mock-local-storage' - +import 'mock-local-storage'; describe('Storage-helpers', () => { describe('process', () => { - let microfrontendId='mockMicroId'; - let hostname= 'luigi.core.test'; + let microfrontendId = 'mockMicroId'; + let hostname = 'luigi.core.test'; let key = 'key_'; - let value = "value_"; + let value = 'value_'; let id = 'messageId_'; let sendBackOperationSpy; let sendMessageToIframeSpy; - const buildLuigiKey = () =>{ - return "Luigi#"+hostname + "#"+key; - } + const buildLuigiKey = () => { + return 'Luigi#' + hostname + '#' + key; + }; const assertSendMessage = (status, result) => { assert(sendBackOperationSpy.calledOnce); - let args = sendBackOperationSpy.getCalls()[0].args + let args = sendBackOperationSpy.getCalls()[0].args; assert(sendBackOperationSpy.calledOnce); - assert.equal(args[0], microfrontendId, "sendBackOperation argument microfrontendId is different from expected"); - assert.equal(args[1], id, "sendBackOperation argument id is different from expected"); - assert.equal(args[2], status, "sendBackOperation argument status is different from expected"); - if (!result){ - assert.isTrue(!args[3], "sendBackOperation argument result shuld be undefined for this operation"); - return ; + assert.equal( + args[0], + microfrontendId, + 'sendBackOperation argument microfrontendId is different from expected' + ); + assert.equal( + args[1], + id, + 'sendBackOperation argument id is different from expected' + ); + assert.equal( + args[2], + status, + 'sendBackOperation argument status is different from expected' + ); + if (!result) { + assert.isTrue( + !args[3], + 'sendBackOperation argument result shuld be undefined for this operation' + ); + return; } - if (Array.isArray(result)){ - assert.deepEqual(args[3], result, "sendBackOperation argument result is different from expected"); + if (Array.isArray(result)) { + assert.deepEqual( + args[3], + result, + 'sendBackOperation argument result is different from expected' + ); return; } - assert.equal(args[3], result, "sendBackOperation argument result is different from expected"); - } + assert.equal( + args[3], + result, + 'sendBackOperation argument result is different from expected' + ); + }; const assertsendMessageToIframe = () => { assert(sendMessageToIframeSpy.calledOnce); - } + }; beforeEach(() => { key = 'key_' + Math.random(); @@ -45,7 +67,9 @@ describe('Storage-helpers', () => { id = 'messageId_' + Math.random(); sendBackOperationSpy = sinon.spy(StorageHelper, 'sendBackOperation'); sendMessageToIframeSpy = sinon.spy(IframeHelpers, 'sendMessageToIframe'); - sinon.stub(IframeHelpers, "getMicrofrontendsInDom").callsFake(() => [{ id: microfrontendId, container: {} }]); + sinon + .stub(IframeHelpers, 'getMicrofrontendsInDom') + .callsFake(() => [{ id: microfrontendId, container: {} }]); window.localStorage.clear(); }); @@ -54,76 +78,88 @@ describe('Storage-helpers', () => { }); it('setItem', () => { - StorageHelper.process(microfrontendId, hostname, id, 'setItem', { key, value }) + StorageHelper.process(microfrontendId, hostname, id, 'setItem', { + key, + value + }); let luigiKey = buildLuigiKey(); - assert.equal(window.localStorage.getItem(luigiKey), value, "Luigi value is different for setItem"); - assertSendMessage("OK", undefined); + assert.equal( + window.localStorage.getItem(luigiKey), + value, + 'Luigi value is different for setItem' + ); + assertSendMessage('OK', undefined); assertsendMessageToIframe(); }); it('getItem', () => { let luigiKey = buildLuigiKey(); window.localStorage.setItem(luigiKey, value); - StorageHelper.process(microfrontendId, hostname, id, 'getItem', { key }) - assertSendMessage("OK", value); + StorageHelper.process(microfrontendId, hostname, id, 'getItem', { key }); + assertSendMessage('OK', value); assertsendMessageToIframe(); }); it('getItem no value', () => { - StorageHelper.process(microfrontendId, hostname, id, 'getItem', { key }) - assertSendMessage("OK", undefined); + StorageHelper.process(microfrontendId, hostname, id, 'getItem', { key }); + assertSendMessage('OK', undefined); assertsendMessageToIframe(); }); it('has', () => { let luigiKey = buildLuigiKey(); window.localStorage.setItem(luigiKey, value); - StorageHelper.process(microfrontendId, hostname, id, 'has', { key }) - assertSendMessage("OK", true); + StorageHelper.process(microfrontendId, hostname, id, 'has', { key }); + assertSendMessage('OK', true); assertsendMessageToIframe(); }); it('has no value', () => { - StorageHelper.process(microfrontendId, hostname, id, 'has', { key }) - assertSendMessage("OK", false); + StorageHelper.process(microfrontendId, hostname, id, 'has', { key }); + assertSendMessage('OK', false); assertsendMessageToIframe(); }); - it('clear', () => { let luigiKey = buildLuigiKey(); window.localStorage.setItem(luigiKey, value); - StorageHelper.process(microfrontendId, hostname, id, 'clear', { }) - assert.isTrue(!window.localStorage.getItem(luigiKey), "After clear, item should not be present"); - assertSendMessage("OK", undefined); + StorageHelper.process(microfrontendId, hostname, id, 'clear', {}); + assert.isTrue( + !window.localStorage.getItem(luigiKey), + 'After clear, item should not be present' + ); + assertSendMessage('OK', undefined); assertsendMessageToIframe(); }); it('removeItem', () => { let luigiKey = buildLuigiKey(); window.localStorage.setItem(luigiKey, value); - StorageHelper.process(microfrontendId, hostname, id, 'removeItem', { key }) - assert.isTrue(!window.localStorage.getItem(luigiKey), "After removed, item should not be present"); - assertSendMessage("OK", value); + StorageHelper.process(microfrontendId, hostname, id, 'removeItem', { + key + }); + assert.isTrue( + !window.localStorage.getItem(luigiKey), + 'After removed, item should not be present' + ); + assertSendMessage('OK', value); assertsendMessageToIframe(); }); it('removeItem no value', () => { - StorageHelper.process(microfrontendId, hostname, id, 'removeItem', { key }) - assertSendMessage("OK", undefined); + StorageHelper.process(microfrontendId, hostname, id, 'removeItem', { + key + }); + assertSendMessage('OK', undefined); assertsendMessageToIframe(); }); it('getAllKeys', () => { let luigiKey = buildLuigiKey(); window.localStorage.setItem(luigiKey, value); - StorageHelper.process(microfrontendId, hostname, id, 'getAllKeys', { }) - assertSendMessage("OK", [key]); + StorageHelper.process(microfrontendId, hostname, id, 'getAllKeys', {}); + assertSendMessage('OK', [key]); assertsendMessageToIframe(); }); - }); - }); - - diff --git a/core/test/utilities/helpers/web-component-helpers.spec.js b/core/test/utilities/helpers/web-component-helpers.spec.js new file mode 100644 index 0000000000..667a111388 --- /dev/null +++ b/core/test/utilities/helpers/web-component-helpers.spec.js @@ -0,0 +1,272 @@ +const chai = require('chai'); +const sinon = require('sinon'); +const expect = chai.expect; +const assert = chai.assert; + +import { + DefaultCompoundRenderer, + CustomCompoundRenderer, + GridCompoundRenderer, + resolveRenderer, + registerEventListeners + } from '../../../src/utilities/helpers/web-component-helpers'; + +describe('WebComponentHelpers', function() { + describe('check DefaultCompoundRenderer', function() { + it('check default constructor and methods', () => { + const dcr = new DefaultCompoundRenderer(); + + const compoundContainer = dcr.createCompoundContainer(); + expect(compoundContainer.tagName).to.equal('DIV'); + + const compoundItemContainer = dcr.createCompoundItemContainer(); + expect(compoundItemContainer.tagName).to.equal('DIV'); + + dcr.attachCompoundItem(compoundContainer, compoundItemContainer); + expect(compoundContainer.firstChild).to.equal(compoundItemContainer); + }); + + it('check constructor with renderer object', () => { + const rendererObject = { + config: { + key: 'value' + } + }; + const dcr = new DefaultCompoundRenderer(rendererObject); + expect(dcr.config.key).to.equal('value'); + }); + }); + + describe('check CustomCompoundRenderer', function() { + const sb = sinon.createSandbox(); + + afterEach(() => { + sb.restore(); + }) + + it('check default constructor and methods', () => { + const ccr = new CustomCompoundRenderer(); + + const compoundContainer = ccr.createCompoundContainer(); + expect(compoundContainer.tagName).to.equal('DIV'); + + const compoundItemContainer = ccr.createCompoundItemContainer(); + expect(compoundItemContainer.tagName).to.equal('DIV'); + + ccr.attachCompoundItem(compoundContainer, compoundItemContainer); + expect(compoundContainer.firstChild).to.equal(compoundItemContainer); + }); + + it('check constructor with custom renderer object', () => { + const rendererObject = { + config: { + key: 'value', + tag: 'span' + }, + use: { + createCompoundContainer: (config, superRenderer)=>{ + expect(superRenderer).to.be.undefined; + return document.createElement(config.tag); + }, + createCompoundItemContainer: (layoutConfig, config, superRenderer) => { + expect(superRenderer).to.be.undefined; + return document.createElement(config.tag); + }, + attachCompoundItem: (compoundCnt, compoundItemCnt, superRenderer) => { + expect(superRenderer).to.be.undefined; + const wrapper = document.createElement('div'); + compoundCnt.appendChild(wrapper); + wrapper.appendChild(compoundItemCnt); + } + } + }; + const ccr = new CustomCompoundRenderer(rendererObject); + expect(ccr.config.key).to.equal('value'); + expect(ccr.rendererObject).to.equal(rendererObject); + + const compoundContainer = ccr.createCompoundContainer(); + expect(compoundContainer.tagName).to.equal('SPAN'); + + const compoundItemContainer = ccr.createCompoundItemContainer(); + expect(compoundItemContainer.tagName).to.equal('SPAN'); + + ccr.attachCompoundItem(compoundContainer, compoundItemContainer); + expect(compoundContainer.firstChild.tagName).to.equal('DIV'); + expect(compoundContainer.firstChild.firstChild).to.equal(compoundItemContainer); + }); + + it('check extending existing renderer', () => { + const rendererObject = { + config: { + key: 'value', + tag: 'span' + }, + use: { + extends: 'sth' + } + }; + + const ccr = new CustomCompoundRenderer(rendererObject); + const superRenderer = ccr.superRenderer; + expect(superRenderer).to.not.be.undefined; + sb.spy(superRenderer); + + const compoundContainer = ccr.createCompoundContainer(); + assert(superRenderer.createCompoundContainer.calledOnce, 'superrenderer should be called'); + + const compoundItemContainer = ccr.createCompoundItemContainer(); + assert(superRenderer.createCompoundContainer.calledOnce, 'superrenderer should be called'); + + ccr.attachCompoundItem(compoundContainer, compoundItemContainer); + assert(superRenderer.attachCompoundItem.calledOnce, 'superrenderer should be called'); + }); + }); + + describe('check GridCompoundRenderer', function() { + it('check default constructor and methods', () => { + const gcr = new GridCompoundRenderer(); + + const compoundContainer = gcr.createCompoundContainer(); + const cnt = compoundContainer.innerHTML.trim(); + assert(cnt.indexOf(' 1, 'should contain display grid'); + assert(cnt.indexOf('grid-template-columns: auto') > 1, 'should contain default grid-template-columns'); + assert(cnt.indexOf('grid-template-rows: auto;') > 1, 'should contain default grid-template-rows'); + assert(cnt.indexOf('grid-gap: 0;') > 1, 'should contain default grid-gap'); + assert(cnt.indexOf('min-height: auto;') > 1, 'should contain default min-height'); + + const compoundItemContainer = gcr.createCompoundItemContainer(); + assert(compoundItemContainer.getAttribute('style').indexOf('grid-row:') >= 0, + 'style attribute should contain grid-row'); + assert(compoundItemContainer.getAttribute('style').indexOf('grid-column:') >= 0, + 'style attribute should contain grid-column'); + + const layoutConfig = { + row: 'myrowconfigvalue', + column: 'mycolumnconfigvalue' + }; + const compoundItemContainer2 = gcr.createCompoundItemContainer(layoutConfig); + assert(compoundItemContainer2.getAttribute('style').indexOf('myrowconfigvalue') >= 0, + 'style attribute should contain grid-row'); + assert(compoundItemContainer2.getAttribute('style').indexOf('mycolumnconfigvalue') >= 0, + 'style attribute should contain grid-column'); + + gcr.attachCompoundItem(compoundContainer, compoundItemContainer); + expect(compoundContainer.children[1]).to.equal(compoundItemContainer); + }); + + it('check layout config', () => { + const rendererObject = { + config: { + columns: '10', + rows: '10', + gap: '20', + minHeight: '10vh', + layouts: [{ + minWidth: 0, + maxWidth: 50, + columns: 4, + rows: 4, + gap: 10 + },{ + minWidth: 51, + maxWidth: 100, + columns: 40, + rows: 40, + gap: 100 + }] + } + }; + + const gcr = new GridCompoundRenderer(rendererObject); + const compoundContainer = gcr.createCompoundContainer(); + const cnt = compoundContainer.innerHTML.trim(); + assert(cnt.indexOf('display: grid;') > 1, 'should contain display grid'); + assert(cnt.indexOf('grid-template-columns: 10') > 1, 'should contain configured grid-template-columns'); + assert(cnt.indexOf('grid-template-rows: 10;') > 1, 'should contain configured grid-template-rows'); + assert(cnt.indexOf('grid-gap: 20;') > 1, 'should contain configured grid-gap'); + assert(cnt.indexOf('min-height: 10vh;') > 1, 'should contain configured min-height'); + + const mqIndex = cnt.indexOf('@media only screen and (min-width: 0px) and (max-width: 50px)'); + assert( mqIndex > 1, 'should contain proper media query'); + assert(cnt.indexOf('grid-template-columns: 4') > mqIndex, 'should contain configured mq grid-template-columns'); + assert(cnt.indexOf('grid-template-rows: 4;') > mqIndex, 'should contain configured mq grid-template-rows'); + assert(cnt.indexOf('grid-gap: 10;') > mqIndex, 'should contain configured mq grid-gap'); + + const mqIndex2 = cnt.indexOf('@media only screen and (min-width: 51px) and (max-width: 100px)'); + assert( mqIndex2 > mqIndex, 'should contain proper media query'); + assert(cnt.indexOf('grid-template-columns: 40') > mqIndex2, 'should contain configured mq grid-template-columns'); + assert(cnt.indexOf('grid-template-rows: 40;') > mqIndex2, 'should contain configured mq grid-template-rows'); + assert(cnt.indexOf('grid-gap: 100;') > mqIndex2, 'should contain configured mq grid-gap'); + }); + }); + + + describe('check resolveRenderer function', function() { + it('check gridRenderer resolution', () => { + const rendererInstance = resolveRenderer({ use: 'grid' }); + expect(typeof rendererInstance === typeof new GridCompoundRenderer()); + }); + + it('check customRenderer resolution', () => { + let rendererInstance = resolveRenderer({ + use: { createCompoundContainer: () => {} } + }); + expect(typeof rendererInstance === typeof new CustomCompoundRenderer()); + + rendererInstance = resolveRenderer({ + use: { createCompoundItemContainer: () => {} } + }); + expect(typeof rendererInstance === typeof new CustomCompoundRenderer()); + + rendererInstance = resolveRenderer({ + use: { attachCompoundItem: () => {} } + }); + expect(typeof rendererInstance === typeof new CustomCompoundRenderer()); + }); + + it('check fallback to default', () => { + let rendererInstance = resolveRenderer({}); + expect(typeof rendererInstance === typeof new DefaultCompoundRenderer()); + + rendererInstance = resolveRenderer({ use: 'unknownRenderer'}); + expect(typeof rendererInstance === typeof new DefaultCompoundRenderer()); + }); + }); + + describe('check registerEventListeners', function() { + it('check resolve', () => { + const navNode = { + eventListeners: [{ + source: 'evSrc', + name: 'someEvent', + action: 'handler', + dataConverter: (data) => { return data + 'tada'; } + },{ + source: '*', + name: 'update', + action: 'doSth' + }] + }; + const eventbusListeners = { + '*.update': ['listenerMock'], + 'src.someEvent' : ['listenerMock'] + }; + const nodeId = 'somerandomid'; + const wcElement = { elementmock : 1 }; + + registerEventListeners(eventbusListeners, navNode, nodeId, wcElement); + + expect(Object.keys(eventbusListeners).length).to.equal(3); + expect(eventbusListeners['evSrc.someEvent'].length).to.equal(1); + expect(eventbusListeners['*.update'].length).to.equal(2); + + const listenerInfo = eventbusListeners['evSrc.someEvent'][0]; + expect(listenerInfo.wcElementId).to.equal(nodeId); + expect(listenerInfo.wcElement).to.equal(wcElement); + expect(listenerInfo.action).to.equal('handler'); + expect(listenerInfo.converter('data')).to.equal('datatada'); + }); + }); +}); + diff --git a/test/e2e-test-application/e2e/tests/1-angular/luigi-client-link-manager-features.spec.js b/test/e2e-test-application/e2e/tests/1-angular/luigi-client-link-manager-features.spec.js index ac2cab3bcc..6606cac9cf 100644 --- a/test/e2e-test-application/e2e/tests/1-angular/luigi-client-link-manager-features.spec.js +++ b/test/e2e-test-application/e2e/tests/1-angular/luigi-client-link-manager-features.spec.js @@ -3,166 +3,187 @@ describe('Luigi client linkManager', () => { cy.visitLoggedIn('/'); }); - it('linkManager features', { - retries: { - runMode: 3, - openMode: 3 - } - }, () => { - cy.getIframeBody().then($iframeBody => { - cy.goToLinkManagerMethods($iframeBody); - - //navigate using absolute path - cy.wrap($iframeBody) - .contains('absolute: to overview') - .click(); - cy.expectPathToBe('/overview'); - - cy.goToLinkManagerMethods($iframeBody); + it( + 'linkManager features', + { + retries: { + runMode: 3, + openMode: 3 + } + }, + () => { + cy.getIframeBody().then($iframeBody => { + cy.goToLinkManagerMethods($iframeBody); - //navigate using relative path - cy.wrap($iframeBody) - .contains('relative: to stakeholders') - .click(); - cy.expectPathToBe('/projects/pr2/users/groups/stakeholders'); + //navigate using absolute path + cy.wrap($iframeBody) + .contains('absolute: to overview') + .click(); + cy.expectPathToBe('/overview'); - cy.goToOverviewPage(); - cy.goToLinkManagerMethods($iframeBody); + cy.goToLinkManagerMethods($iframeBody); - //navigate using closest context - cy.wrap($iframeBody) - .contains('closest parent: to stakeholders') - .click(); - cy.expectPathToBe('/projects/pr2/users/groups/stakeholders'); + //navigate using relative path + cy.wrap($iframeBody) + .contains('relative: to stakeholders') + .click(); + cy.expectPathToBe('/projects/pr2/users/groups/stakeholders'); - cy.goToOverviewPage(); - cy.goToLinkManagerMethods($iframeBody); + cy.goToOverviewPage(); + cy.goToLinkManagerMethods($iframeBody); - //navigate using context - cy.wrap($iframeBody) - .contains('parent by name: project to settings') - .click(); - cy.expectPathToBe('/projects/pr2/settings'); + //navigate using closest context + cy.wrap($iframeBody) + .contains('closest parent: to stakeholders') + .click(); + cy.expectPathToBe('/projects/pr2/users/groups/stakeholders'); - cy.wrap($iframeBody).should('contain', 'Settings'); - cy.wrap($iframeBody) - .contains('Click here') - .click(); - cy.expectPathToBe('/projects/pr2'); + cy.goToOverviewPage(); + cy.goToLinkManagerMethods($iframeBody); - //navigate to sibling through parent - cy.wrap($iframeBody) - .contains('from parent: to sibling') - .click(); - cy.expectPathToBe('/projects/pr1'); + //navigate using context + cy.wrap($iframeBody) + .contains('parent by name: project to settings') + .click(); + cy.expectPathToBe('/projects/pr2/settings'); - cy.goToOverviewPage(); - cy.goToLinkManagerMethods($iframeBody); + cy.wrap($iframeBody).should('contain', 'Settings'); + cy.wrap($iframeBody) + .contains('Click here') + .click(); + cy.expectPathToBe('/projects/pr2'); - //navigate with params - cy.wrap($iframeBody) - .contains('project to settings with params (foo=bar)') - .click(); - cy.wrap($iframeBody).should('contain', 'Called with params:'); - cy.wrap($iframeBody).should('contain', '"foo": "bar"'); + //navigate to sibling through parent + cy.wrap($iframeBody) + .contains('from parent: to sibling') + .click(); + cy.expectPathToBe('/projects/pr1'); - cy.expectSearchToBe('?~foo=bar'); + cy.goToOverviewPage(); + cy.goToLinkManagerMethods($iframeBody); - cy.wrap($iframeBody) - .contains('Click here') - .click(); - cy.expectPathToBe('/projects/pr2'); + //navigate with params + cy.wrap($iframeBody) + .contains('project to settings with params (foo=bar)') + .click(); + cy.wrap($iframeBody).should('contain', 'Called with params:'); + cy.wrap($iframeBody).should('contain', '"foo": "bar"'); - //don't navigate - cy.wrap($iframeBody) - .contains('parent by name: with nonexisting context') - .click(); - cy.expectPathToBe('/projects/pr2'); + cy.expectSearchToBe('?~foo=bar'); - //navigate with intent - cy.wrap($iframeBody) - .contains('navigate to settings with intent with parameters') - .click(); - cy.expectPathToBe('/projects/pr2/settings'); - cy.expectSearchToBe('?~param1=abc&~param2=bcd'); + cy.wrap($iframeBody) + .contains('Click here') + .click(); + cy.expectPathToBe('/projects/pr2'); - cy.goToOverviewPage(); - cy.goToLinkManagerMethods($iframeBody); + //don't navigate + cy.wrap($iframeBody) + .contains('parent by name: with nonexisting context') + .click(); + cy.expectPathToBe('/projects/pr2'); - cy.goToLinkManagerMethods($iframeBody); - //navigate with preserve view functionality - cy.wrap($iframeBody) - .contains('with preserved view: project to global settings and back') - .click(); - cy.expectPathToBe('/settings'); + //open webcomponent in splitview + cy.wrap($iframeBody) + .contains('Open webcomponent in splitView') + .click(); + cy.get('.iframeSplitViewCnt>').then(container => { + const root = container.children().prevObject[0].shadowRoot; + const wcContent = root.querySelector('p').innerText; + expect(wcContent).to.equal('Hello WebComponent!'); + root.querySelector('button').click(); + cy.get('[data-testid=luigi-alert]').should( + 'have.class', + 'fd-message-strip--information' + ); + cy.get('[data-testid=luigi-alert]').should( + 'contain', + 'Hello from uxManager in Web Component' + ); + }); + //navigate with intent + cy.wrap($iframeBody) + .contains('navigate to settings with intent with parameters') + .click(); + cy.expectPathToBe('/projects/pr2/settings'); + cy.expectSearchToBe('?~param1=abc&~param2=bcd'); - //wait for the alert coming from an inactive iFrame to be shown and second iFrame to be loaded - cy.wait(500); - cy.get('.fd-message-strip').should( - 'contain', - 'Information alert sent from an inactive iFrame' - ); + cy.goToOverviewPage(); + cy.goToLinkManagerMethods($iframeBody); - cy.getIframeBody().then($iframeBody => { + //navigate with preserve view functionality cy.wrap($iframeBody) - .find('input') - .clear() - .type('tets'); - cy.wrap($iframeBody) - .find('button') + .contains('with preserved view: project to global settings and back') .click(); - cy.expectPathToBe('/projects/pr2'); - }); + cy.expectPathToBe('/settings'); - cy.visitLoggedIn('/'); + //wait for the alert coming from an inactive iFrame to be shown and second iFrame to be loaded + cy.wait(500); + cy.get('.fd-message-strip').should( + 'contain', + 'Information alert sent from an inactive iFrame' + ); - cy.getIframeBody().then($iframeBody => { - // check if path exists - cy.goToLinkManagerMethods($iframeBody); - [ - // non-existent relative path - { path: 'projects/pr2/', successExpected: false }, - // non-existent absolute path - { path: '/developers', successExpected: false }, - // existent absolute path with '/' at the end - { path: '/projects/pr2/', successExpected: true }, - // existent absolute path without '/' at the end - { path: '/projects/pr2', successExpected: true }, - // existent path with two dynamic pathSegments - { - path: '/projects/pr1/users/groups/avengers/settings/dynamic-two', - successExpected: true - }, - // existent relative path - { path: 'developers', successExpected: true } - ].map(data => { - const msgExpected = data.successExpected - ? `Path ${data.path} exists` - : `Path ${data.path} does not exist`; - const checkPathSelector = '.link-manager .check-path'; + cy.getIframeBody().then($iframeBody => { cy.wrap($iframeBody) - .find(checkPathSelector + ' input') + .find('input') .clear() - .type(data.path); + .type('tets'); cy.wrap($iframeBody) - .find(checkPathSelector + ' button') + .find('button') .click(); - cy.wrap($iframeBody) - .find(checkPathSelector + ' .check-path-result') - .contains(msgExpected); + cy.expectPathToBe('/projects/pr2'); }); - // go back - cy.goToOverviewPage(); - cy.goToLinkManagerMethods($iframeBody); - cy.expectPathToBe('/projects/pr2'); - cy.wrap($iframeBody) - .contains('go back: single iframe') - .click(); - cy.expectPathToBe('/overview'); + cy.visitLoggedIn('/'); + + cy.getIframeBody().then($iframeBody => { + // check if path exists + cy.goToLinkManagerMethods($iframeBody); + [ + // non-existent relative path + { path: 'projects/pr2/', successExpected: false }, + // non-existent absolute path + { path: '/developers', successExpected: false }, + // existent absolute path with '/' at the end + { path: '/projects/pr2/', successExpected: true }, + // existent absolute path without '/' at the end + { path: '/projects/pr2', successExpected: true }, + // existent path with two dynamic pathSegments + { + path: '/projects/pr1/users/groups/avengers/settings/dynamic-two', + successExpected: true + }, + // existent relative path + { path: 'developers', successExpected: true } + ].map(data => { + const msgExpected = data.successExpected + ? `Path ${data.path} exists` + : `Path ${data.path} does not exist`; + const checkPathSelector = '.link-manager .check-path'; + cy.wrap($iframeBody) + .find(checkPathSelector + ' input') + .clear() + .type(data.path); + cy.wrap($iframeBody) + .find(checkPathSelector + ' button') + .click(); + cy.wrap($iframeBody) + .find(checkPathSelector + ' .check-path-result') + .contains(msgExpected); + }); + + // go back + cy.goToOverviewPage(); + cy.goToLinkManagerMethods($iframeBody); + cy.expectPathToBe('/projects/pr2'); + cy.wrap($iframeBody) + .contains('go back: single iframe') + .click(); + cy.expectPathToBe('/overview'); + }); }); - }); - }); + } + ); describe('linkManager wrong paths navigation', () => { let $iframeBody; @@ -303,7 +324,9 @@ describe('Luigi client linkManager', () => { cy.get('.drawer').should('exist'); cy.expectPathToBe('/projects/pr2'); cy.get('.drawer .fd-dialog__close').should('not.exist'); - cy.wrap($iframeBody).contains('go back: single iframe, standard history back').click(); + cy.wrap($iframeBody) + .contains('go back: single iframe, standard history back') + .click(); cy.get('.drawer').should('not.exist'); }); }); diff --git a/test/e2e-test-application/e2e/tests/1-angular/luigi-client-ux-manager-features.spec.js b/test/e2e-test-application/e2e/tests/1-angular/luigi-client-ux-manager-features.spec.js index b19d05b26e..295e3cea82 100644 --- a/test/e2e-test-application/e2e/tests/1-angular/luigi-client-ux-manager-features.spec.js +++ b/test/e2e-test-application/e2e/tests/1-angular/luigi-client-ux-manager-features.spec.js @@ -81,36 +81,42 @@ describe('Luigi Client ux manager features', () => { .contains('Luigi confirmation modal has been dismissed'); }); - it('loading indicator', { - retries: { - runMode: 3, - openMode: 3 - } - }, () => { - cy.get('[data-testid="misc"]').click(); - - cy.get('[data-testid="ext_externalpage"]') - .contains('External Page') - .click(); - - cy.get('[data-testid=luigi-loading-spinner]').should('exist'); - - cy.wait(250); // give it some time to hide - - cy.get('[data-testid=luigi-loading-spinner]').should('not.be.visible'); - - // show loading indicator - cy.getIframeBody().then($iframeBody => { - cy.wrap($iframeBody) - .contains('Show loading indicator') + it( + 'loading indicator', + { + retries: { + runMode: 3, + openMode: 3 + } + }, + () => { + cy.get('[data-testid="misc"]').click(); + + cy.get('[data-testid="ext_externalpage"]') + .contains('External Page') .click(); cy.get('[data-testid=luigi-loading-spinner]').should('exist'); + cy.wait(250); // give it some time to hide - // wait for programmatic hide of loading indicator + cy.get('[data-testid=luigi-loading-spinner]').should('not.be.visible'); - }); - }); + + // show loading indicator + cy.getIframeBody().then($iframeBody => { + cy.wrap($iframeBody) + .contains('Show loading indicator') + .click(); + + cy.get('[data-testid=luigi-loading-spinner]').should('exist'); + cy.wait(250); // give it some time to hide + // wait for programmatic hide of loading indicator + cy.get('[data-testid=luigi-loading-spinner]').should( + 'not.be.visible' + ); + }); + } + ); describe('Unsaved changes', () => { it("shouldn't proceed when 'No' was pressed in modal", () => { diff --git a/test/e2e-test-application/package-lock.json b/test/e2e-test-application/package-lock.json index 1d1cd81dbf..45cf55de9f 100644 --- a/test/e2e-test-application/package-lock.json +++ b/test/e2e-test-application/package-lock.json @@ -6482,28 +6482,28 @@ "dependencies": { "abbrev": { "version": "1.1.1", - "resolved": false, + "resolved": "", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true, "optional": true }, "ansi-regex": { "version": "2.1.1", - "resolved": false, + "resolved": "", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "dev": true, "optional": true }, "aproba": { "version": "1.2.0", - "resolved": false, + "resolved": "", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "dev": true, "optional": true }, "are-we-there-yet": { "version": "1.1.5", - "resolved": false, + "resolved": "", "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "dev": true, "optional": true, @@ -6514,14 +6514,14 @@ }, "balanced-match": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true, "optional": true }, "brace-expansion": { "version": "1.1.11", - "resolved": false, + "resolved": "", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "optional": true, @@ -6532,42 +6532,42 @@ }, "chownr": { "version": "1.1.1", - "resolved": false, + "resolved": "", "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", "dev": true, "optional": true }, "code-point-at": { "version": "1.1.0", - "resolved": false, + "resolved": "", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "dev": true, "optional": true }, "concat-map": { "version": "0.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true, "optional": true }, "console-control-strings": { "version": "1.1.0", - "resolved": false, + "resolved": "", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "dev": true, "optional": true }, "core-util-is": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true, "optional": true }, "debug": { "version": "4.1.1", - "resolved": false, + "resolved": "", "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "optional": true, @@ -6577,28 +6577,28 @@ }, "deep-extend": { "version": "0.6.0", - "resolved": false, + "resolved": "", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true, "optional": true }, "delegates": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "dev": true, "optional": true }, "detect-libc": { "version": "1.0.3", - "resolved": false, + "resolved": "", "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "dev": true, "optional": true }, "fs-minipass": { "version": "1.2.5", - "resolved": false, + "resolved": "", "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", "dev": true, "optional": true, @@ -6608,14 +6608,14 @@ }, "fs.realpath": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true, "optional": true }, "gauge": { "version": "2.7.4", - "resolved": false, + "resolved": "", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "dev": true, "optional": true, @@ -6632,7 +6632,7 @@ }, "glob": { "version": "7.1.3", - "resolved": false, + "resolved": "", "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "dev": true, "optional": true, @@ -6647,14 +6647,14 @@ }, "has-unicode": { "version": "2.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "dev": true, "optional": true }, "iconv-lite": { "version": "0.4.24", - "resolved": false, + "resolved": "", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, "optional": true, @@ -6664,7 +6664,7 @@ }, "ignore-walk": { "version": "3.0.1", - "resolved": false, + "resolved": "", "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", "dev": true, "optional": true, @@ -6674,7 +6674,7 @@ }, "inflight": { "version": "1.0.6", - "resolved": false, + "resolved": "", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dev": true, "optional": true, @@ -6685,21 +6685,21 @@ }, "inherits": { "version": "2.0.3", - "resolved": false, + "resolved": "", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "dev": true, "optional": true }, "ini": { "version": "1.3.5", - "resolved": false, + "resolved": "", "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "dev": true, "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, "optional": true, @@ -6709,14 +6709,14 @@ }, "isarray": { "version": "1.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "dev": true, "optional": true }, "minimatch": { "version": "3.0.4", - "resolved": false, + "resolved": "", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, "optional": true, @@ -6726,14 +6726,14 @@ }, "minimist": { "version": "0.0.8", - "resolved": false, + "resolved": "", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "dev": true, "optional": true }, "minipass": { "version": "2.3.5", - "resolved": false, + "resolved": "", "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", "dev": true, "optional": true, @@ -6744,7 +6744,7 @@ }, "minizlib": { "version": "1.2.1", - "resolved": false, + "resolved": "", "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", "dev": true, "optional": true, @@ -6754,7 +6754,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": false, + "resolved": "", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, "optional": true, @@ -6764,14 +6764,14 @@ }, "ms": { "version": "2.1.1", - "resolved": false, + "resolved": "", "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true, "optional": true }, "needle": { "version": "2.3.0", - "resolved": false, + "resolved": "", "integrity": "sha512-QBZu7aAFR0522EyaXZM0FZ9GLpq6lvQ3uq8gteiDUp7wKdy0lSd2hPlgFwVuW1CBkfEs9PfDQsQzZghLs/psdg==", "dev": true, "optional": true, @@ -6783,7 +6783,7 @@ }, "node-pre-gyp": { "version": "0.12.0", - "resolved": false, + "resolved": "", "integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==", "dev": true, "optional": true, @@ -6802,7 +6802,7 @@ }, "nopt": { "version": "4.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "dev": true, "optional": true, @@ -6813,14 +6813,14 @@ }, "npm-bundled": { "version": "1.0.6", - "resolved": false, + "resolved": "", "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==", "dev": true, "optional": true }, "npm-packlist": { "version": "1.4.1", - "resolved": false, + "resolved": "", "integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==", "dev": true, "optional": true, @@ -6831,7 +6831,7 @@ }, "npmlog": { "version": "4.1.2", - "resolved": false, + "resolved": "", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "dev": true, "optional": true, @@ -6844,21 +6844,21 @@ }, "number-is-nan": { "version": "1.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "dev": true, "optional": true }, "object-assign": { "version": "4.1.1", - "resolved": false, + "resolved": "", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "dev": true, "optional": true }, "once": { "version": "1.4.0", - "resolved": false, + "resolved": "", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, "optional": true, @@ -6868,21 +6868,21 @@ }, "os-homedir": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "dev": true, "optional": true }, "os-tmpdir": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true, "optional": true }, "osenv": { "version": "0.1.5", - "resolved": false, + "resolved": "", "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "dev": true, "optional": true, @@ -6893,21 +6893,21 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true, "optional": true }, "process-nextick-args": { "version": "2.0.0", - "resolved": false, + "resolved": "", "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "dev": true, "optional": true }, "rc": { "version": "1.2.8", - "resolved": false, + "resolved": "", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "dev": true, "optional": true, @@ -6920,7 +6920,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": false, + "resolved": "", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true, "optional": true @@ -6929,7 +6929,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": false, + "resolved": "", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "optional": true, @@ -6945,7 +6945,7 @@ }, "rimraf": { "version": "2.6.3", - "resolved": false, + "resolved": "", "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "dev": true, "optional": true, @@ -6955,49 +6955,49 @@ }, "safe-buffer": { "version": "5.1.2", - "resolved": false, + "resolved": "", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true, "optional": true }, "safer-buffer": { "version": "2.1.2", - "resolved": false, + "resolved": "", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true, "optional": true }, "sax": { "version": "1.2.4", - "resolved": false, + "resolved": "", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "dev": true, "optional": true }, "semver": { "version": "5.7.0", - "resolved": false, + "resolved": "", "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", "dev": true, "optional": true }, "set-blocking": { "version": "2.0.0", - "resolved": false, + "resolved": "", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true, "optional": true }, "signal-exit": { "version": "3.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true, "optional": true }, "string-width": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, "optional": true, @@ -7009,7 +7009,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": false, + "resolved": "", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "optional": true, @@ -7019,7 +7019,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "optional": true, @@ -7029,14 +7029,14 @@ }, "strip-json-comments": { "version": "2.0.1", - "resolved": false, + "resolved": "", "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "dev": true, "optional": true }, "tar": { "version": "4.4.8", - "resolved": false, + "resolved": "", "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", "dev": true, "optional": true, @@ -7052,14 +7052,14 @@ }, "util-deprecate": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true, "optional": true }, "wide-align": { "version": "1.1.3", - "resolved": false, + "resolved": "", "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "dev": true, "optional": true, @@ -7069,14 +7069,14 @@ }, "wrappy": { "version": "1.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true, "optional": true }, "yallist": { "version": "3.0.3", - "resolved": false, + "resolved": "", "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", "dev": true, "optional": true diff --git a/test/e2e-test-application/src/app/project/project.component.html b/test/e2e-test-application/src/app/project/project.component.html index d943ab19f6..9f6d21fac3 100644 --- a/test/e2e-test-application/src/app/project/project.component.html +++ b/test/e2e-test-application/src/app/project/project.component.html @@ -10,12 +10,18 @@

Backdrop

-

- +

@@ -26,17 +32,28 @@

Confirmation modal

-

- +

-

+ }}" + *ngIf="confirmationModalResult" + > Luigi confirmation modal has been {{ confirmationModalResult }}

@@ -53,44 +70,78 @@

Alert

-
- +
- +
-
-

- +

-

+

Luigi alert has been dismissed

@@ -108,24 +159,39 @@

Localization

Current locale is: '{{ currentLocale }}'

- +

- +
-

- +

@@ -143,11 +209,20 @@

-