diff --git a/ilc/client.js b/ilc/client.js
index ff2bc385..6866758b 100644
--- a/ilc/client.js
+++ b/ilc/client.js
@@ -3,7 +3,7 @@ import * as singleSpa from 'single-spa';
import Router from './common/router/ClientRouter';
import setupErrorHandlers from './client/errorHandler/setupErrorHandlers';
import {fragmentErrorHandlerFactory, crashIlc} from './client/errorHandler/fragmentErrorHandlerFactory';
-import handlePageTransaction from './client/handlePageTransaction';
+import handlePageTransaction, {slotWillBe} from './client/handlePageTransaction';
import initSpaConfig from './client/initSpaConfig';
import setupPerformanceMonitoring from './client/performance';
import selectSlotsToRegister from './client/selectSlotsToRegister';
@@ -75,9 +75,9 @@ function isActiveFactory(appName, slotName) {
let isActive = checkActivity(router.getCurrentRoute());
const wasActive = checkActivity(router.getPrevRoute());
- let willBe;
- !wasActive && isActive && (willBe = 'rendered');
- wasActive && !isActive && (willBe = 'removed');
+ let willBe = slotWillBe.default;
+ !wasActive && isActive && (willBe = slotWillBe.rendered);
+ wasActive && !isActive && (willBe = slotWillBe.removed);
if (isActive && wasActive && reload === false) {
const oldProps = router.getPrevRouteProps(appName, slotName);
@@ -95,12 +95,12 @@ function isActiveFactory(appName, slotName) {
});
isActive = false;
- willBe = 'rerendered';
+ willBe = slotWillBe.rerendered;
}
}
if (window.ilcConfig && window.ilcConfig.tmplSpinner) {
- willBe && handlePageTransaction(slotName, willBe);
+ handlePageTransaction(slotName, willBe);
}
reload = false;
diff --git a/ilc/client/handlePageTransaction.js b/ilc/client/handlePageTransaction.js
index c4bfd393..4d34c5b8 100644
--- a/ilc/client/handlePageTransaction.js
+++ b/ilc/client/handlePageTransaction.js
@@ -78,19 +78,32 @@ const renderFakeSlot = slotName => {
hiddenSlots.push(targetNode);
};
-/**
- * @param {string} slotName
- * @param {string} willBe - possible values: rendered, removed, rerendered
- */
-export default function (slotName, willBe) {
- if (!slotName || !willBe) return;
+export const slotWillBe = {
+ rendered: 'rendered',
+ removed: 'removed',
+ rerendered: 'rerendered',
+ default: null,
+};
+
+export default function handlePageTransaction(slotName, willBe) {
+ if (!slotName) {
+ throw new Error('A slot name was not provided!');
+ }
- if (willBe === 'rendered') {
- addContentListener(slotName);
- } else if (willBe === 'removed') {
- renderFakeSlot(slotName);
- } else if (willBe === 'rerendered') {
- renderFakeSlot(slotName);
- addContentListener(slotName);
+ switch (willBe) {
+ case slotWillBe.rendered:
+ addContentListener(slotName);
+ break;
+ case slotWillBe.removed:
+ renderFakeSlot(slotName);
+ break;
+ case slotWillBe.rerendered:
+ renderFakeSlot(slotName);
+ addContentListener(slotName);
+ break;
+ case slotWillBe.default:
+ break;
+ default:
+ throw new Error(`The slot action '${willBe}' did not match any possible values!`);
}
}
diff --git a/ilc/client/handlePageTransaction.spec.js b/ilc/client/handlePageTransaction.spec.js
new file mode 100644
index 00000000..070c2631
--- /dev/null
+++ b/ilc/client/handlePageTransaction.spec.js
@@ -0,0 +1,264 @@
+import chai from 'chai';
+import sinon from 'sinon';
+import html from 'nanohtml';
+
+import handlePageTransaction, {
+ slotWillBe,
+} from './handlePageTransaction';
+
+describe('handle page transaction', () => {
+ const locationHash = 'i-am-location-hash';
+
+ const slots = {
+ id: 'slots',
+ appendSlots: () => document.body.appendChild(slots.ref),
+ removeSlots: () => document.body.removeChild(slots.ref),
+ resetRef: () => {
+ slots.ref = html`
+
+ `;
+ },
+ navbar: {
+ id: 'navbar',
+ getComputedStyle: () => window.getComputedStyle(slots.navbar.ref, null),
+ },
+ body: {
+ id: 'body',
+ getComputedStyle: () => window.getComputedStyle(slots.body.ref, null),
+ getAttributeName: () => document.body.getAttribute('name'),
+ },
+ };
+
+ const applications = {
+ navbar: {
+ id: 'navbar-application',
+ class: 'navbar-spa',
+ appendApplication: () => slots.navbar.ref.appendChild(applications.navbar.ref),
+ resetRef: () => {
+ applications.navbar.ref = html`
+
+ Hello! I am Navbar SPA
+
+ `;
+ },
+ },
+ body: {
+ id: 'body-application',
+ class: 'body-spa',
+ appendApplication: () => slots.body.ref.appendChild(applications.body.ref),
+ removeApplication: () => slots.body.ref.removeChild(applications.body.ref),
+ resetRef: () => {
+ applications.body.ref = html`
+
+ Hello! I am Body SPA
+
+ `;
+ },
+ },
+ };
+
+ const spinner = {
+ id: 'ilc-spinner',
+ class: 'ilc-spinner',
+ getRef: () => document.getElementById(spinner.id),
+ };
+
+ let clock;
+
+ beforeEach(() => {
+ window.location.hash = locationHash;
+ window.ilcConfig = {
+ tmplSpinner: `Hello! I am Spinner
`,
+ };
+
+ slots.resetRef();
+ applications.body.resetRef();
+ applications.navbar.resetRef();
+
+ slots.appendSlots();
+
+ slots.navbar.ref = document.getElementById(slots.navbar.id);
+ slots.body.ref = document.getElementById(slots.body.id);
+
+ clock = sinon.useFakeTimers();
+ });
+
+ afterEach(() => {
+ slots.removeSlots();
+ clock.restore();
+ });
+
+ it('should throw an error when a slot name is not provided', async () => {
+ chai.expect(() => handlePageTransaction()).to.throw(
+ 'A slot name was not provided!'
+ );
+ });
+
+ it('should throw an error when a slot action does not match any possible option to handle', async () => {
+ chai.expect(() => handlePageTransaction(slots.body.id, 'undefined')).to.throw(
+ `The slot action 'undefined' did not match any possible values!`
+ );
+ });
+
+ it('should do nothing when a slot action is default', async () => {
+ handlePageTransaction(slots.body.id, slotWillBe.default);
+
+ await clock.runAllAsync();
+
+ chai.expect(spinner.getRef()).to.be.null;
+ chai.expect(slots.ref.innerHTML).to.be.equal(
+ `` +
+ ``
+ );
+
+ applications.body.appendApplication();
+
+ await clock.runAllAsync();
+
+ chai.expect(spinner.getRef()).to.be.null;
+ chai.expect(slots.ref.innerHTML).to.be.equal(
+ `` +
+ `` +
+ `
` +
+ 'Hello! I am Body SPA' +
+ '
' +
+ '
'
+ );
+
+ applications.body.removeApplication();
+
+ await clock.runAllAsync();
+
+ chai.expect(spinner.getRef()).to.be.null;
+ chai.expect(slots.ref.innerHTML).to.be.equal(
+ `` +
+ ``
+ );
+ });
+
+ it('should listen to slot content changes when a slot is going to be rendered', async () => {
+ handlePageTransaction(slots.navbar.id, slotWillBe.rendered);
+
+ await clock.runAllAsync();
+
+ chai.expect(spinner.getRef()).to.be.not.null;
+ chai.expect(slots.navbar.getComputedStyle().display).to.be.equal('none');
+ chai.expect(slots.body.getAttributeName()).to.be.equal(locationHash);
+
+ handlePageTransaction(slots.body.id, slotWillBe.rendered);
+
+ await clock.runAllAsync();
+
+ chai.expect(spinner.getRef()).to.be.not.null;
+ chai.expect(document.getElementsByClassName(spinner.class).length).to.equal(1);
+ chai.expect(slots.navbar.getComputedStyle().display).to.be.equal('none');
+ chai.expect(slots.body.getComputedStyle().display).to.be.equal('none');
+ chai.expect(slots.body.getAttributeName()).to.be.equal(locationHash);
+
+ applications.navbar.appendApplication();
+
+ await clock.runAllAsync();
+
+ chai.expect(spinner.getRef()).to.be.not.null;
+ chai.expect(slots.navbar.getComputedStyle().display).to.be.equal('none');
+ chai.expect(slots.body.getComputedStyle().display).to.be.equal('none');
+ chai.expect(slots.body.getAttributeName()).to.be.equal(locationHash);
+
+ applications.body.appendApplication();
+
+ await clock.runAllAsync();
+
+ chai.expect(spinner.getRef()).to.be.null;
+ chai.expect(slots.navbar.getComputedStyle().display).to.be.equal('block');
+ chai.expect(slots.body.getComputedStyle().display).to.be.equal('block');
+ chai.expect(slots.body.getAttributeName()).to.be.null;
+ });
+
+ it('should render a fake slot when a slot is going to be removed', async () => {
+ applications.navbar.appendApplication();
+ applications.body.appendApplication();
+
+ handlePageTransaction(slots.body.id, slotWillBe.removed);
+
+ await clock.runAllAsync();
+
+ applications.body.removeApplication();
+
+ await clock.runAllAsync();
+
+ const bodyApplications = document.getElementsByClassName(applications.body.class);
+ chai.expect(bodyApplications.length).to.be.equal(1);
+
+ const [fakeBodyApplicationRef] = bodyApplications;
+ const fakeBodySlot = fakeBodyApplicationRef.parentNode;
+
+ chai.expect(fakeBodyApplicationRef.id).to.be.equal(applications.body.id);
+
+ chai.expect(window.getComputedStyle(fakeBodyApplicationRef, null).display).to.be.equal('block');
+ chai.expect(window.getComputedStyle(fakeBodySlot, null).display).to.be.equal('block');
+ chai.expect(slots.body.getComputedStyle().display).to.be.equal('none');
+ chai.expect(slots.navbar.getComputedStyle().display).to.be.equal('block');
+
+ chai.expect(fakeBodySlot.nodeName).to.be.equal(slots.body.ref.nodeName);
+ chai.expect(fakeBodySlot.id).to.be.equal('');
+ chai.expect(fakeBodySlot.className).to.be.equal('');
+
+ chai.expect(spinner.getRef()).to.be.null;
+ });
+
+ it('should render a fake slot and listen to slot content changes when a slot is going to be rerendered', async () => {
+ const newBodyApplication = {
+ id: 'new-body-application',
+ class: 'new-body-spa',
+ };
+
+ newBodyApplication.ref = html`
+
+ Hello! I am new Body SPA
+
+ `;
+
+ applications.navbar.appendApplication();
+ applications.body.appendApplication();
+
+ handlePageTransaction(slots.body.id, slotWillBe.rerendered);
+
+ await clock.runAllAsync();
+
+ applications.body.removeApplication();
+
+ await clock.runAllAsync();
+
+ const bodyApplications = document.getElementsByClassName(applications.body.class);
+ chai.expect(bodyApplications.length).to.be.equal(1);
+
+ const [fakeBodyApplicationRef] = bodyApplications;
+ const fakeBodySlot = fakeBodyApplicationRef.parentNode;
+
+ chai.expect(fakeBodyApplicationRef.id).to.be.equal(applications.body.id);
+
+ chai.expect(window.getComputedStyle(fakeBodyApplicationRef, null).display).to.be.equal('block');
+ chai.expect(slots.navbar.getComputedStyle().display).to.be.equal('block');
+ chai.expect(slots.body.getComputedStyle().display).to.be.equal('none');
+ chai.expect(window.getComputedStyle(fakeBodySlot, null).display).to.be.equal('block');
+
+ chai.expect(fakeBodySlot.nodeName).to.be.equal(slots.body.ref.nodeName);
+ chai.expect(fakeBodySlot.id).to.be.equal('');
+ chai.expect(fakeBodySlot.className).to.be.equal('');
+
+ chai.expect(spinner.getRef()).to.be.not.null;
+ chai.expect(slots.body.getAttributeName()).to.be.equal(locationHash);
+
+ slots.body.ref.appendChild(newBodyApplication.ref);
+
+ await clock.runAllAsync();
+
+ chai.expect(spinner.getRef()).to.be.null;
+ chai.expect(slots.navbar.getComputedStyle().display).to.be.equal('block');
+ chai.expect(slots.body.getComputedStyle().display).to.be.equal('block');
+ chai.expect(slots.body.getAttributeName()).to.be.null;
+ });
+});
diff --git a/ilc/package.json b/ilc/package.json
index 14aac21d..40573a75 100644
--- a/ilc/package.json
+++ b/ilc/package.json
@@ -8,7 +8,7 @@
"test:watch": "npm run test -- --watch",
"test:coverage": "cross-env NODE_ENV=test nyc mocha",
"test:client": "npm run build:systemjs && cross-env NODE_ENV=test karma start",
- "test:client:watch": "npm run test:client -- --no-single-run",
+ "test:client:watch": "npm run test:client -- --single-run=false --auto-watch=true",
"start": "node --max-http-header-size 30000 server/app.js",
"dev": "npm run build:polyfills && cross-env NODE_ENV=development nodemon --ignore './public/' --max-http-header-size 30000 server/app.js",
"build": "rimraf public && npm run build:systemjs && npm run build:client && npm run build:polyfills",
@@ -59,6 +59,7 @@
"karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "^4.0.2",
"mocha": "^7.1.1",
+ "nanohtml": "^1.9.1",
"nodemon": "^2.0.3",
"nyc": "^15.0.1",
"raw-loader": "^3.1.0",