Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
3 changes: 2 additions & 1 deletion client/modules/IDE/hooks/useP5Version.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
p5DataAddonURL,
p5URLTemplate
} from '../../../../common/p5URLs';
import { preserveNoProtect } from '../utils/loopProtection';

export const majorVersion = (version) => version.split('.')[0];

Expand Down Expand Up @@ -46,7 +47,7 @@ export function P5VersionProvider(props) {
src = `${doctype}\n${src}`;
}

return src;
return preserveNoProtect(indexSrc, src);
};

const usedP5Versions = [...dom.documentElement.querySelectorAll('script')]
Expand Down
87 changes: 87 additions & 0 deletions client/modules/IDE/hooks/useP5Version.unit.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React from 'react';
import { reduxRender } from '../../../test-utils';
import { initialState as filesInitialState } from '../reducers/files';
import { p5PreloadAddonURL } from '../../../../common/p5URLs';
import { hasNoProtect } from '../utils/loopProtection';
import { P5VersionProvider, useP5Version } from './useP5Version';

const P5_SCRIPT = 'https://cdn.jsdelivr.net/npm/p5@2.3.2/lib/p5.js';

function indexHtml({ noprotect = false, sound = false } = {}) {
const soundTag = sound
? '\n <script src="https://cdn.jsdelivr.net/npm/p5.sound@0.4.1/dist/p5.sound.min.js"></script>'
: '';
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<script src="${P5_SCRIPT}"></script>${soundTag}
</head>
<body></body>
</html>`;
return noprotect ? `<!-- noprotect -->\n${html}` : html;
}

let capturedVersionInfo = null;

function VersionProbe() {
const { versionInfo } = useP5Version();
capturedVersionInfo = versionInfo;
return null;
}

function renderVersionInfo(indexContent) {
capturedVersionInfo = null;
reduxRender(
<P5VersionProvider>
<VersionProbe />
</P5VersionProvider>,
{
initialState: {
files: filesInitialState().map((file) => ({
...file,
...(file.fileType === 'file' &&
file.name === 'index.html' &&
file.filePath === '' && { content: indexContent })
}))
}
}
);
return capturedVersionInfo;
}

describe('useP5Version HTML rewrites', () => {
afterEach(() => {
capturedVersionInfo = null;
});

it('keeps <!-- noprotect --> when replaceVersion rewrites index.html', () => {
const versionInfo = renderVersionInfo(indexHtml({ noprotect: true }));
const next = versionInfo.replaceVersion('2.3.3');

expect(hasNoProtect(next)).toBe(true);
expect(next).toContain('p5@2.3.3/lib/p5.js');
expect(next.match(/<!--\s*noprotect\s*-->/g)).toHaveLength(1);
});

it('does not insert <!-- noprotect --> when the original had none', () => {
const versionInfo = renderVersionInfo(indexHtml());
const next = versionInfo.replaceVersion('2.3.3');

expect(hasNoProtect(next)).toBe(false);
expect(next).toContain('p5@2.3.3/lib/p5.js');
});

it('keeps <!-- noprotect --> when addon setters rewrite index.html', () => {
const versionInfo = renderVersionInfo(
indexHtml({ noprotect: true, sound: true })
);

const afterSoundOff = versionInfo.setP5Sound(false);
expect(hasNoProtect(afterSoundOff)).toBe(true);
expect(afterSoundOff).not.toContain('p5.sound');

const afterPreload = versionInfo.setP5PreloadAddon(true);
expect(hasNoProtect(afterPreload)).toBe(true);
expect(afterPreload).toContain(p5PreloadAddonURL);
});
});
8 changes: 8 additions & 0 deletions client/modules/IDE/utils/loopProtection.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,11 @@ export function removeNoProtect(src = '') {
export function toggleLoopProtection(src = '', enabled) {
return enabled ? removeNoProtect(src) : addNoProtect(src);
}

// Version/addon HTML rewrites serialize via documentElement.outerHTML, which
// drops comments outside <html> — including a leading <!-- noprotect -->.
export function preserveNoProtect(originalSrc = '', serializedSrc = '') {
return hasNoProtect(originalSrc)
? addNoProtect(serializedSrc)
: serializedSrc;
}
64 changes: 64 additions & 0 deletions client/modules/IDE/utils/loopProtection.unit.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {
addNoProtect,
hasNoProtect,
preserveNoProtect,
removeNoProtect,
toggleLoopProtection
} from './loopProtection';

describe('loopProtection helpers', () => {
const html = `<!DOCTYPE html>
<html>
<head></head>
<body></body>
</html>`;
const withNoProtect = `<!-- noprotect -->\n${html}`;

describe('hasNoProtect', () => {
it('is true when a leading noprotect comment exists', () => {
expect(hasNoProtect(withNoProtect)).toBe(true);
});

it('is false when the comment is absent', () => {
expect(hasNoProtect(html)).toBe(false);
});
});

describe('addNoProtect / removeNoProtect', () => {
it('prepends the comment once', () => {
expect(addNoProtect(html)).toBe(withNoProtect);
expect(addNoProtect(withNoProtect)).toBe(withNoProtect);
});

it('strips the comment', () => {
expect(removeNoProtect(withNoProtect)).toBe(html);
expect(removeNoProtect(html)).toBe(html);
});
});

describe('toggleLoopProtection', () => {
it('removes noprotect when protection is enabled', () => {
expect(toggleLoopProtection(withNoProtect, true)).toBe(html);
});

it('adds noprotect when protection is disabled', () => {
expect(toggleLoopProtection(html, false)).toBe(withNoProtect);
});
});

describe('preserveNoProtect', () => {
it('restores noprotect after a rewrite that dropped it', () => {
expect(preserveNoProtect(withNoProtect, html)).toBe(withNoProtect);
});

it('does not insert noprotect when the original had none', () => {
expect(preserveNoProtect(html, html)).toBe(html);
});

it('does not duplicate noprotect when the rewrite kept it', () => {
expect(preserveNoProtect(withNoProtect, withNoProtect)).toBe(
withNoProtect
);
});
});
});