diff --git a/src/jest/server/pageContributions/getAndMerge.test.ts b/src/jest/server/pageContributions/getAndMerge.test.ts
new file mode 100644
index 000000000..4889e5615
--- /dev/null
+++ b/src/jest/server/pageContributions/getAndMerge.test.ts
@@ -0,0 +1,72 @@
+import {describe, expect, jest, test as it} from '@jest/globals';
+
+jest.mock('/lib/enonic/react4xp/asset/executor/getExecutorUrl', () => ({
+ getExecutorUrl: () => '/_/service/com.example.myproject/react4xp/executor.js'
+}));
+
+jest.mock('/lib/enonic/react4xp/pageContributions/buildPageContributions', () => ({
+ buildPageContributions: ({entries, suppressJS}: {entries: string[], suppressJS: boolean}) => ({
+ headEnd: suppressJS ? [] : entries.map(entry => `\n`)
+ })
+}));
+
+import {getAndMerge} from '/lib/enonic/react4xp/pageContributions/getAndMerge';
+
+const EXECUTOR = '\n';
+
+describe('getAndMerge', () => {
+ it('adds the executor last when JS is not suppressed', () => {
+ const result = getAndMerge({
+ entryNames: 'Header',
+ incomingPgContrib: {},
+ newPgContrib: {bodyEnd: ['']},
+ suppressJS: false
+ });
+ expect(result.bodyEnd).toEqual(['', EXECUTOR]);
+ });
+
+ it('does not add the executor when JS is suppressed and no previous rendering needed it', () => {
+ const result = getAndMerge({
+ entryNames: 'Footer',
+ incomingPgContrib: {},
+ newPgContrib: {},
+ suppressJS: true
+ });
+ expect(result.bodyEnd).toEqual([]);
+ });
+
+ it('keeps the executor last when a previous rendering needed it and the current one suppresses JS', () => {
+ const header = getAndMerge({
+ entryNames: 'Header',
+ incomingPgContrib: {},
+ newPgContrib: {},
+ suppressJS: false
+ });
+ expect(header.bodyEnd).toEqual([EXECUTOR]);
+
+ const footer = getAndMerge({
+ entryNames: 'Footer',
+ incomingPgContrib: header,
+ newPgContrib: {bodyEnd: ['']},
+ suppressJS: true
+ });
+ expect(footer.headEnd).toEqual(['\n']);
+ expect(footer.bodyEnd).toEqual(['', EXECUTOR]);
+ });
+
+ it('does not duplicate the executor when chaining two renderings that both need it', () => {
+ const header = getAndMerge({
+ entryNames: 'Header',
+ incomingPgContrib: {},
+ newPgContrib: {},
+ suppressJS: false
+ });
+ const footer = getAndMerge({
+ entryNames: 'Footer',
+ incomingPgContrib: header,
+ newPgContrib: {},
+ suppressJS: false
+ });
+ expect(footer.bodyEnd).toEqual([EXECUTOR]);
+ });
+});
diff --git a/src/main/resources/lib/enonic/react4xp/pageContributions/getAndMerge.ts b/src/main/resources/lib/enonic/react4xp/pageContributions/getAndMerge.ts
index 41bee8c64..fefa10517 100644
--- a/src/main/resources/lib/enonic/react4xp/pageContributions/getAndMerge.ts
+++ b/src/main/resources/lib/enonic/react4xp/pageContributions/getAndMerge.ts
@@ -60,6 +60,8 @@ export function getAndMerge({
const executorEntry = `\n`;
+ const keepExecutor = !suppressJS || (incomingPgContrib.bodyEnd || []).indexOf(executorEntry) !== -1;
+
// Keeps track of already-added entries across headBegin, headEnd, bodyBegin and bodyEnd
const controlSet = [
executorEntry // Skip until manually added, must be last...
@@ -97,6 +99,6 @@ export function getAndMerge({
newPgContrib.bodyEnd
],
controlSet
- ).concat(suppressJS ? [] : executorEntry) // Manually added last :)
+ ).concat(keepExecutor ? executorEntry : []) // Manually added last :)
};
}