Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DO NOT MERGE]: Added script loader #708

Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/initialize.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ import {
import {
configure as configureAnalytics, SegmentAnalyticsService, identifyAnonymousUser, identifyAuthenticatedUser,
} from './analytics';
import { GoogleAnalyticsLoader } from './scripts';
import { GoogleAnalyticsLoader, ScriptInserter } from './scripts';
import {
getAuthenticatedHttpClient,
configure as configureAuth,
Expand Down Expand Up @@ -290,7 +290,7 @@ export async function initialize({
analyticsService = SegmentAnalyticsService,
authService = AxiosJwtAuthService,
authMiddleware = [],
externalScripts = [GoogleAnalyticsLoader],
externalScripts = [GoogleAnalyticsLoader, ScriptInserter],
requireAuthenticatedUser: requireUser = false,
hydrateAuthenticatedUser: hydrateUser = false,
messages,
Expand Down
97 changes: 97 additions & 0 deletions src/scripts/ScriptInserter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* Class representing a Script Inserter.
*/
class ScriptInserter {
/**
* Create a Script Inserter.
* @param {Array<Object>} scripts - An array of script objects to insert.
* @param {string} [scripts[].head] - The script to insert into the head section.
* @param {string} [scripts[].body.top] - The script to insert at the top of the body section.
* @param {string} [scripts[].body.bottom] - The script to insert at the bottom of the body section.
*/
constructor({ config }) {
this.scripts = config.EXTERNAL_SCRIPTS || [];
}

/**
* Inserts the scripts into their respective locations (head, body start, body end).
*/
loadScript() {
if (!this.scripts.length) {
return;
}

this.scripts.forEach((script) => {
if (script.head) {
this.insertToHead(script.head);
}
if (script.body?.top) {
this.insertToBodyTop(script.body.top);
}
if (script.body?.bottom) {
this.insertToBodyBottom(script.body.bottom);
}
});
}

/**
* Inserts content into the head section.
* @param {string} content - The content to insert into the head section.
*/
insertToHead(content) {
this.createAndAppendScript(content, document.head);
}

/**
* Inserts content at the start of the body section.
* @param {string} content - The content to insert at the top of the body section.
*/
insertToBodyTop(content) {
this.createAndAppendScript(content, document.body, true);
}

/**
* Inserts content at the end of the body section.
* @param {string} content - The content to insert at the bottom of the body section.
*/
insertToBodyBottom(content) {
this.createAndAppendScript(content, document.body);
}

/**
* Creates a script element and appends it to the specified location.
* @param {string} content - The content of the script.
* @param {Element} parent - The parent element to insert the script into (head or body).
* @param {boolean} atStart - Whether to insert the script at the start of the parent element.
*/
createAndAppendScript(content, parent, atStart = false) {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = content;
const scriptElement = tempDiv.querySelector('script');

if (scriptElement && scriptElement.src) {
// If the script has a src attribute, create a new script element with the same src
const newScriptElement = document.createElement('script');
newScriptElement.src = scriptElement.src;
newScriptElement.async = true;

if (atStart && parent.firstChild) {
parent.insertBefore(newScriptElement, parent.firstChild);
} else {
parent.appendChild(newScriptElement);
}
} else {
// If the script does not have a src attribute, insert its inner content as inline script
const newScriptElement = document.createElement('script');
newScriptElement.text = scriptElement ? scriptElement.innerHTML : content;

if (atStart && parent.firstChild) {
parent.insertBefore(newScriptElement, parent.firstChild);
} else {
parent.appendChild(newScriptElement);
}
}
}
}

export default ScriptInserter;
94 changes: 94 additions & 0 deletions src/scripts/ScriptInserter.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import ScriptInserter from './ScriptInserter';

describe('ScriptInserter', () => {
let data;

beforeEach(() => {
document.head.innerHTML = '';
document.body.innerHTML = '';
});

function loadScripts(scriptData) {
const scriptInserter = new ScriptInserter(scriptData);
scriptInserter.loadScript();
}

describe('with multiple scripts', () => {
beforeEach(() => {
data = {
config: {
EXTERNAL_SCRIPTS: [
{
head: '<script>console.log("First head script");</script>',
body: {
top: '<script>console.log("First body top script");</script>',
bottom: '<script>console.log("First body bottom script");</script>',
},
},
{
head: '<script src="https://example.com/second-script.js"></script>',
},
{
body: {
top: '<script>console.log("Third body top script");</script>',
},
},
],
},
};
loadScripts(data);
});

it('should insert all head scripts', () => {
const headScripts = document.head.querySelectorAll('script');
expect(headScripts.length).toBe(2);

const inlineHeadScript = Array.from(headScripts)
.find(script => script.src === '' && script.innerHTML.includes('console.log("First head script")'));
const srcHeadScript = document.head
.querySelector('script[src="https://example.com/second-script.js"]');

expect(inlineHeadScript).not.toBeNull();
expect(srcHeadScript).not.toBeNull();
expect(srcHeadScript.async).toBe(true);
});

it('should insert all body top scripts in correct order', () => {
const bodyTopScripts = document.body.querySelectorAll('script');
expect(bodyTopScripts.length).toBe(3); // Top scripts + Bottom script

const firstTopScript = Array.from(bodyTopScripts)
.find(script => script.innerHTML.includes('console.log("First body top script")'));
const thirdTopScript = Array.from(bodyTopScripts)
.find(script => script.innerHTML.includes('console.log("Third body top script")'));

expect(firstTopScript).not.toBeNull();
expect(thirdTopScript).not.toBeNull();
});

it('should insert all body bottom scripts', () => {
const bodyBottomScripts = Array.from(document.body.querySelectorAll('script'))
.filter(script => script.innerHTML.includes('First body bottom script'));
expect(bodyBottomScripts.length).toBe(1);

const firstBottomScript = bodyBottomScripts[0];
expect(firstBottomScript.innerHTML).toBe('console.log("First body bottom script");');
});
});

describe('with no external scripts', () => {
beforeEach(() => {
data = {
config: {
EXTERNAL_SCRIPTS: [],
},
};
loadScripts(data);
});

it('should not insert any scripts', () => {
expect(document.head.querySelectorAll('script').length).toBe(0);
expect(document.body.querySelectorAll('script').length).toBe(0);
});
});
});
2 changes: 1 addition & 1 deletion src/scripts/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
/* eslint-disable import/prefer-default-export */
export { default as GoogleAnalyticsLoader } from './GoogleAnalyticsLoader';
export { default as ScriptInserter } from './ScriptInserter';
Loading