UNPKG

@skilltree/skills-client-js

Version:
789 lines (651 loc) 24.3 kB
/* * Copyright 2025 SkillTree * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import mock from 'xhr-mock'; import Postmate from 'postmate'; import MockAdapter from 'axios-mock-adapter'; import axios from 'axios'; import SkillsDisplayJS from '../../src/display/SkillsDisplayJS'; import ErrorPageUtils from '../../src/display/ErrorPageUtils'; import SkillsConfiguration from '../../src/config/SkillsConfiguration'; jest.mock('../../src/display/ErrorPageUtils.js'); jest.mock('postmate'); describe('SkillsDisplayJS', () => { let mockHttp; beforeEach(() => { mockHttp = new MockAdapter(axios); Postmate.mockClear(); ErrorPageUtils.removeAllChildren.mockClear(); ErrorPageUtils.buildErrorPage.mockClear(); // mockHttp.onGet(/\/public\/status/).reply(200); mock.setup(); mock.get(/.*\/public\/status/, (req, res) => res.status(200).body('{"status":"OK","clientLib":{"loggingEnabled":"false","loggingLevel":"DEBUG"}}')); }); afterEach(() => { mock.teardown(); mockHttp.reset(); }); it('is found', () => { expect(SkillsDisplayJS).toBeDefined(); }); it('destroys the childFrame', () => { const mockChildFrame = { destroy: jest.fn(), }; const client = new SkillsDisplayJS({ options: null, }); client._childFrame = mockChildFrame; client.destroy(); expect(client._childFrame.destroy).toHaveBeenCalled(); }); it('doesnt error if the childFrame is null on destroy', () => { const mockChildFrame = { destroy: jest.fn(), }; const client = new SkillsDisplayJS({ options: null, }); client._childFrame = null; expect(() => { client.destroy(); }).not.toThrow(); }); describe('attachTo', () => { let realDocument; beforeEach(() => { SkillsConfiguration.enabled = true realDocument = global.document; global.document.querySelector = jest.fn(); }); afterEach(() => { global.document = realDocument; }); it('checks the service status', (done) => { // mockHttp.reset(); mock.reset(); const mockServiceUrl = 'http://serviceUrlToNowhere'; const client = new SkillsDisplayJS({ options: { serviceUrl: mockServiceUrl, }, }); Postmate.mockImplementation(() => ({ then: jest.fn(), })); // mock.get(`${mockServiceUrl}/public/status`, () => Promise.reject(new Error())); mock.get(`${mockServiceUrl}/public/status`, { status: 503 }); const mockIframeContainer = { setAttribute: jest.fn(), appendChild: jest.fn(), style: {}, }; const mockErrorPage = `I am an error page ${Math.random()}`; ErrorPageUtils.buildErrorPage.mockImplementation(() => mockErrorPage); global.document.querySelector = () => mockIframeContainer; console.error = jest.fn(); client.attachTo(mockIframeContainer); setTimeout(() => { expect(console.error).toHaveBeenCalledWith('Please ensure the skilltree server is running. skilltree service URL: http://serviceUrlToNowhere'); expect(ErrorPageUtils.removeAllChildren).toHaveBeenCalledWith(mockIframeContainer); expect(mockIframeContainer.appendChild).toHaveBeenCalledWith(mockErrorPage); expect(mockIframeContainer.setAttribute).toHaveBeenCalledWith('style', 'border: 5px; height: 20rem; width: 100%'); done(); }); }); it('throws an error if element to attach to does not exist', () => { const client = new SkillsDisplayJS({ options: null }); global.document.querySelector = jest.fn(() => null); const mockSelector = `#${Math.random()}`; expect(() => { client.attachTo(mockSelector); }).toThrow(`Can't find element with selector='${mockSelector}'`); expect(() => { client.attachTo(null); }).toThrow(`Can't find element with selector='${null}'`); }); describe('Postmate event handling', () => { let skillsDisplay; let mockChildFrame; beforeEach(() => { const skillsDisplay = new SkillsDisplayJS({ options: null }); mockChildFrame = { eventHandlers: { }, call: jest.fn(), frame: { className: 'client-display-iframe-1' }, on(eventName, handler) { this.eventHandlers[eventName] = handler; }, emit(eventName, args) { this.eventHandlers[eventName].bind(skillsDisplay, args)(); }, }; Postmate.mockImplementation(() => Promise.resolve(mockChildFrame)); }); it('adjusts the iframe containers height on height-changed event', (done) => { const client = new SkillsDisplayJS({ options: null }); const mockIframeContainer = { setAttribute: jest.fn(), style: {}, }; global.document.querySelector = () => mockIframeContainer; client.attachTo(mockIframeContainer); expect(mockIframeContainer.height).toBe(0); expect(mockIframeContainer.style.height).toBe('0px'); // Let the promise resolve.. const newHeight = 1000000000; setTimeout(() => { mockChildFrame.emit('height-changed', newHeight); expect(mockIframeContainer.height).toBe(newHeight); expect(mockIframeContainer.style.height).toBe(`${newHeight}px`); done(); }); }); describe('do-scroll event', () => { it('do-scroll event is called', (done) => { const client = new SkillsDisplayJS({ options: {}, }); const mockIframeContainer = { setAttribute: jest.fn(), style: {}, getBoundingClientRect: jest.fn(() => ({ top: 200 })), }; global.document.querySelector = () => mockIframeContainer; global.scroll = jest.fn(); client.attachTo(mockIframeContainer); setTimeout(() => { mockChildFrame.emit('do-scroll', 500); expect(global.scroll).toHaveBeenCalledWith({ top: 700, behavior: 'smooth' }); done(); }); }); it('do-scroll event is called with scrollTopOffset', (done) => { const client = new SkillsDisplayJS({ options: { scrollTopOffset: 133, }, }); const mockIframeContainer = { setAttribute: jest.fn(), style: {}, getBoundingClientRect: jest.fn(() => ({ top: 200 })), }; global.document.querySelector = () => mockIframeContainer; global.scroll = jest.fn(); client.attachTo(mockIframeContainer); setTimeout(() => { mockChildFrame.emit('do-scroll', 500); expect(global.scroll).toHaveBeenCalledWith({ top: 567, behavior: 'smooth' }); done(); }); }); it('do-scroll event is called with iframe start above the viewable area', (done) => { const client = new SkillsDisplayJS({ options: { scrollTopOffset: 133, }, }); const mockIframeContainer = { setAttribute: jest.fn(), style: {}, // iframe starts outside of browsers viewable area getBoundingClientRect: jest.fn(() => ({ top: -123 })), }; global.document.querySelector = () => mockIframeContainer; global.scroll = jest.fn(); client.attachTo(mockIframeContainer); setTimeout(() => { mockChildFrame.emit('do-scroll', 500); expect(global.scroll).toHaveBeenCalledWith({ top: 367, behavior: 'smooth' }); done(); }); }); }); describe('route-changed event', () => { it('does not attempt to scroll if disableAutoScroll option is set', (done) => { const client = new SkillsDisplayJS({ options: { disableAutoScroll: true, }, }); const mockIframeContainer = { setAttribute: jest.fn(), style: {}, scrollIntoView: jest.fn(), }; global.document.querySelector = () => mockIframeContainer; client.attachTo(mockIframeContainer); setTimeout(() => { mockChildFrame.emit('route-changed', { path: 'blah', query: {} }); expect(mockIframeContainer.scrollIntoView).not.toHaveBeenCalled(); done(); }); }); it('does auto scrolls to the top of the iframe if autoScrollStrategy is not top-of-page', (done) => { const client = new SkillsDisplayJS({ options: null }); const mockIframeContainer = { setAttribute: jest.fn(), style: {}, scrollIntoView: jest.fn(), }; global.document.querySelector = () => mockIframeContainer; client.attachTo(mockIframeContainer); setTimeout(() => { mockChildFrame.emit('route-changed', { path: 'blah', query: {} }); expect(mockIframeContainer.scrollIntoView).toHaveBeenCalledWith({ behavior: 'smooth' }); done(); }); }); it('does auto scroll to configured offset', (done) => { const client = new SkillsDisplayJS({ options: { scrollTopOffset: 10, autoScrollStrategy: 'top-offset' } }); const mockIframeContainer = { setAttribute: jest.fn(), style: {}, scrollIntoView: jest.fn(), }; global.document.querySelector = () => mockIframeContainer; global.scroll = jest.fn(); client.attachTo(mockIframeContainer); const top = mockIframeContainer.offsetTop - 10; setTimeout(() => { mockChildFrame.emit('route-changed', { path: 'blah', query: {} }); expect(global.scroll).toHaveBeenCalledWith({ top, behavior: 'smooth' }); done(); }); }); it('if autoScrollStrategy is top-of-page it will scroll to the top of the html document', (done) => { const client = new SkillsDisplayJS({ options: { autoScrollStrategy: 'top-of-page' } }); const mockBody = { scrollIntoView: jest.fn(), }; const mockIframeContainer = { setAttribute: jest.fn(), style: {}, scrollIntoView: jest.fn(), }; global.document.querySelector = (selector) => { if (selector === 'body') { return mockBody; } return mockIframeContainer; }; client.attachTo(mockIframeContainer); setTimeout(() => { mockChildFrame.emit('route-changed', { path: 'blah', query: {} }); expect(mockBody.scrollIntoView).toHaveBeenCalledWith({ behavior: 'smooth' }); done(); }); }); it('handles null params', (done) => { const client = new SkillsDisplayJS({ options: { disableAutoScroll: true, }, }); const mockIframeContainer = { setAttribute: jest.fn(), style: {}, scrollIntoView: jest.fn(), }; global.document.querySelector = () => mockIframeContainer; client.attachTo(mockIframeContainer); setTimeout(() => { mockChildFrame.emit('route-changed'); expect(mockIframeContainer.scrollIntoView).not.toHaveBeenCalled(); done(); }); }); }); describe('needs-authentication event', () => { it('tells the child it is in pki mode', (done) => { const client = new SkillsDisplayJS({ options: { authenticator: 'pki', }, }); const mockIframeContainer = { setAttribute: jest.fn(), style: {}, }; global.document.querySelector = () => mockIframeContainer; client.attachTo(mockIframeContainer); setTimeout(() => { mockChildFrame.emit('needs-authentication'); expect(mockChildFrame.call).toHaveBeenCalledWith('updateAuthenticationToken', 'pki'); const client = new SkillsDisplayJS({ options: null, }); done(); }); }); it('gets a new authToken and sends it to the child', (done) => { const mockAuthenticator = 'http://urlToNowhere.com'; const client = new SkillsDisplayJS({ options: { authenticator: mockAuthenticator, }, }); const mockIframeContainer = { setAttribute: jest.fn(), style: {}, }; global.document.querySelector = () => mockIframeContainer; client.attachTo(mockIframeContainer); const mockAccessToken = `${Math.random()}`; mockHttp.onGet(mockAuthenticator).reply(200, { access_token: mockAccessToken, }); mock.get('http://urltonowhere.com/', (req, res) => res.status(200).body(`{"access_token":"${mockAccessToken}"}`)); setTimeout(() => { expect(client.authenticationPromise).not.toBeDefined(); mockChildFrame.emit('needs-authentication'); expect(client.authenticationPromise).toBeDefined(); setTimeout(() => { expect(mockChildFrame.call).toHaveBeenCalledWith('updateAuthenticationToken', mockAccessToken); setTimeout(() => { // finally block expect(client.authenticationPromise).toBe(null); done(); }); }); }); }); }); }); describe('newing up Postmate', () => { let mockServiceUrl; let mockAuthenticator; let mockProjectId; let mockTheme; let mockVersion; let mockOptions; beforeEach(() => { mockServiceUrl = `${Math.random()}`; mockAuthenticator = `${Math.random()}`; mockProjectId = `${Math.random()}`; mockTheme = { }; mockOptions = { authenticator: mockAuthenticator, isSummaryOnly: false, projectId: mockProjectId, serviceUrl: mockServiceUrl, }; mockVersion = `${Math.random()}`; Postmate.mockImplementation(() => ({ then: jest.fn(), })); }); it('properly configures Postmate isSummaryOnly === false and pki mode', () => { const mockUserId = `${Math.random()}`; const options = { isSummaryOnly: false, authenticator: 'pki', serviceUrl: mockServiceUrl, projectId: mockProjectId, }; const client = new SkillsDisplayJS({ options, userId: mockUserId, theme: mockTheme, version: mockVersion, }); const mockIframeContainer = { setAttribute: jest.fn(), style: {}, }; global.document.querySelector = () => mockIframeContainer; client.attachTo(mockIframeContainer); expect(Postmate).toHaveBeenCalledWith({ container: mockIframeContainer, url: `${mockServiceUrl}/static/clientPortal/index.html`, classListArray: [`client-display-iframe-${client.currentIframeId()}`], model: { serviceUrl: mockServiceUrl, projectId: mockProjectId, version: mockVersion, userId: mockUserId, theme: mockTheme, options, minHeight: '960px', isSummaryOnly: false, internalBackButton: false, }, }); }); it('properly configures Postmate isSummaryOnly === true and !pki mode', () => { const options = { isSummaryOnly: true, authenticator: mockAuthenticator, serviceUrl: mockServiceUrl, projectId: mockProjectId, }; const client = new SkillsDisplayJS({ options, theme: mockTheme, version: mockVersion, }); const mockIframeContainer = { setAttribute: jest.fn(), style: { }, }; global.document.querySelector = () => mockIframeContainer; client.attachTo(mockIframeContainer); expect(Postmate).toHaveBeenCalledWith({ container: mockIframeContainer, url: `${mockServiceUrl}/static/clientPortal/index.html`, classListArray: [`client-display-iframe-${client.currentIframeId()}`], model: { serviceUrl: mockServiceUrl, projectId: mockProjectId, version: mockVersion, userId: null, theme: mockTheme, options, minHeight: '600px', isSummaryOnly: true, internalBackButton: false, }, }); }); it('properly configures Postmate internalBackButton === false when set explicitly', () => { const options = { isSummaryOnly: true, authenticator: mockAuthenticator, serviceUrl: mockServiceUrl, projectId: mockProjectId, internalBackButton: false, }; const client = new SkillsDisplayJS({ options, theme: mockTheme, version: mockVersion, }); const mockIframeContainer = { setAttribute: jest.fn(), style: { }, }; global.document.querySelector = () => mockIframeContainer; client.attachTo(mockIframeContainer); expect(Postmate).toHaveBeenCalledWith({ container: mockIframeContainer, url: `${mockServiceUrl}/static/clientPortal/index.html`, classListArray: [`client-display-iframe-${client.currentIframeId()}`], model: { serviceUrl: mockServiceUrl, projectId: mockProjectId, version: mockVersion, userId: null, theme: mockTheme, options, minHeight: '600px', isSummaryOnly: true, internalBackButton: false, }, }); }); it('properly configures Postmate internalBackButton === true when set explicitly', () => { const options = { isSummaryOnly: true, authenticator: mockAuthenticator, serviceUrl: mockServiceUrl, projectId: mockProjectId, internalBackButton: true, }; const client = new SkillsDisplayJS({ options, theme: mockTheme, version: mockVersion, }); const mockIframeContainer = { setAttribute: jest.fn(), style: { }, }; global.document.querySelector = () => mockIframeContainer; client.attachTo(mockIframeContainer); expect(Postmate).toHaveBeenCalledWith({ container: mockIframeContainer, url: `${mockServiceUrl}/static/clientPortal/index.html`, classListArray: [`client-display-iframe-${client.currentIframeId()}`], model: { serviceUrl: mockServiceUrl, projectId: mockProjectId, version: mockVersion, userId: null, theme: mockTheme, options, minHeight: '600px', isSummaryOnly: true, internalBackButton: true, }, }); }); it('properly configures Postmate internalBackButton === false when NOT set explicitly', () => { const options = { isSummaryOnly: true, authenticator: mockAuthenticator, serviceUrl: mockServiceUrl, projectId: mockProjectId, }; const client = new SkillsDisplayJS({ options, theme: mockTheme, version: mockVersion, }); const mockIframeContainer = { setAttribute: jest.fn(), style: { }, }; global.document.querySelector = () => mockIframeContainer; client.attachTo(mockIframeContainer); expect(Postmate).toHaveBeenCalledWith({ container: mockIframeContainer, url: `${mockServiceUrl}/static/clientPortal/index.html`, classListArray: [`client-display-iframe-${client.currentIframeId()}`], model: { options, serviceUrl: mockServiceUrl, projectId: mockProjectId, version: mockVersion, userId: null, theme: mockTheme, minHeight: '600px', isSummaryOnly: true, internalBackButton: false, }, }); }); }); }); describe('construction', () => { describe('options', () => { it('accepts options in the constructor', () => { const mockOptions = { authenticator: 'option' }; const client = new SkillsDisplayJS({ options: mockOptions, }); expect(client.options).toEqual(mockOptions); }); it('supports null or undefined being passed', () => { let client = new SkillsDisplayJS({ options: null }); expect(client.options).toEqual({}); client = new SkillsDisplayJS({ options: undefined }); expect(client.options).toEqual({}); }); it('supports empty constructor', () => { const client = new SkillsDisplayJS(); expect(client.options).toEqual({}); }); it('accepts valid options', () => { const options = { authenticator: true, serviceUrl: true, projectId: true, disableAutoScroll: true, autoScrollStrategy: true, }; new SkillsDisplayJS({ options }); }); }); describe('theme', () => { it('accepts sets the theme object', () => { const mockTheme = { mock: 'theme' }; const client = new SkillsDisplayJS({ theme: mockTheme }); expect(client.theme).toBe(mockTheme); }); }); describe('version', () => { it('accepts a version', () => { const mockVersion = Math.random(); const client = new SkillsDisplayJS({ version: mockVersion }); expect(client.version).toBe(mockVersion); }); }); describe('userId', () => { it('accepts a version', () => { const userId = `${Math.random()}`; const client = new SkillsDisplayJS({ userId }); expect(client.userId).toBe(userId); }); }); }); describe('get configuration()', () => { it('users options passed during construction if they exist', () => { const mockServiceUrl = `${Math.random()}`; const mockAuthenticator = `${Math.random()}`; const mockProjectId = `${Math.random()}`; const options = { serviceUrl: mockServiceUrl, authenticator: mockAuthenticator, projectId: mockProjectId, }; const client = new SkillsDisplayJS({ options }); const { configuration } = client; expect(configuration.serviceUrl).toBe(mockServiceUrl); expect(configuration.authenticator).toBe(mockAuthenticator); expect(configuration.projectId).toBe(mockProjectId); }); }); it('setting a new versions informs the childFrame', () => { const mockClientFrame = { call: jest.fn(), }; const newVersion = 2; const client = new SkillsDisplayJS({ version: newVersion - 1 }); client._childFrame = mockClientFrame; client.version = newVersion; expect(mockClientFrame.call).toHaveBeenCalledWith('updateVersion', newVersion); }); });