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",