diff --git a/client/src/client/frame-runner.js b/client/src/client/frame-runner.js index 83b2c9babf..9ca9e2dae7 100644 --- a/client/src/client/frame-runner.js +++ b/client/src/client/frame-runner.js @@ -3,18 +3,12 @@ import jQuery from 'jquery'; window.$ = jQuery; -const testId = 'fcc-test-frame'; -if (window.frameElement && window.frameElement.id === testId) { - document.addEventListener('DOMContentLoaded', initTestFrame); -} - -// For tests in CI. document.__initTestFrame = initTestFrame; -async function initTestFrame() { - const code = (document.__source || '').slice(0); - if (!document.__getUserInput) { - document.__getUserInput = () => code; +async function initTestFrame(e = {}) { + const code = (e.code || '').slice(0); + if (!e.getUserInput) { + e.getUserInput = () => code; } /* eslint-disable no-unused-vars */ @@ -43,7 +37,7 @@ async function initTestFrame() { /* eslint-enable no-unused-vars */ let Enzyme; - if (document.__loadEnzyme) { + if (e.loadEnzyme) { let Adapter16; /* eslint-disable no-inline-comments */ [{ default: Enzyme }, { default: Adapter16 }] = await Promise.all([ @@ -66,7 +60,7 @@ async function initTestFrame() { // eslint-disable-next-line no-eval const test = eval(testString); if (typeof test === 'function') { - await test(document.__getUserInput); + await test(e.getUserInput); } return { pass: true }; } catch (err) { @@ -81,7 +75,4 @@ async function initTestFrame() { }; } }; - - // notify that the window methods are ready to run - document.__frameReady(); } diff --git a/client/src/client/workers/sass-compile.js b/client/src/client/workers/sass-compile.js index 00a5ee7cd5..126a8618f0 100644 --- a/client/src/client/workers/sass-compile.js +++ b/client/src/client/workers/sass-compile.js @@ -1,5 +1,17 @@ -// eslint-disable-next-line no-undef -importScripts('/js/sass.sync.js'); +// work around for SASS error in Edge +// https://github.com/medialize/sass.js/issues/96#issuecomment-424386171 +if (!self.crypto) { + self.crypto = { + getRandomValues: function(array) { + for (var i = 0, l = array.length; i < l; i++) { + array[i] = Math.floor(Math.random() * 256); + } + return array; + } + }; +} + +self.importScripts('/js/sass.sync.js'); self.onmessage = e => { const data = e.data; @@ -11,3 +23,5 @@ self.onmessage = e => { } }); }; + +self.postMessage({ type: 'contentLoaded' }); diff --git a/client/src/client/workers/test-evaluator.js b/client/src/client/workers/test-evaluator.js index 2478c52658..a74104cded 100644 --- a/client/src/client/workers/test-evaluator.js +++ b/client/src/client/workers/test-evaluator.js @@ -48,3 +48,5 @@ self.onmessage = async e => { } } }; + +self.postMessage({ type: 'contentLoaded' }); diff --git a/client/src/templates/Challenges/components/CompletionModal.js b/client/src/templates/Challenges/components/CompletionModal.js index b7c7908b7d..1ed0121e62 100644 --- a/client/src/templates/Challenges/components/CompletionModal.js +++ b/client/src/templates/Challenges/components/CompletionModal.js @@ -60,6 +60,44 @@ const propTypes = { }; export class CompletionModal extends Component { + state = { + downloadURL: null + }; + + static getDerivedStateFromProps(props, state) { + const { files, isOpen } = props; + if (!isOpen) { + return null; + } + const { downloadURL } = state; + if (downloadURL) { + URL.revokeObjectURL(downloadURL); + } + let newURL = null; + if (Object.keys(files).length) { + const filesForDownload = Object.keys(files) + .map(key => files[key]) + .reduce( + (allFiles, { path, contents }) => ({ + ...allFiles, + [path]: contents + }), + {} + ); + const blob = new Blob([JSON.stringify(filesForDownload, null, 2)], { + type: 'text/json' + }); + newURL = URL.createObjectURL(blob); + } + return { downloadURL: newURL }; + } + + componentWillUnmount() { + if (this.state.downloadURL) { + URL.revokeObjectURL(this.state.downloadURL); + } + } + render() { const { close, @@ -67,22 +105,11 @@ export class CompletionModal extends Component { submitChallenge, handleKeypress, message, - files = {}, title } = this.props; if (isOpen) { ga.modalview('/completion-modal'); } - const showDownloadButton = Object.keys(files).length; - const filesForDownload = Object.keys(files) - .map(key => files[key]) - .reduce( - (allFiles, { path, contents }) => ({ - ...allFiles, - [path]: contents - }), - {} - ); const dashedName = dasherize(title); return ( - Submit and go to next challenge (Ctrl + Enter) + Submit and go to next challenge{' '} + (Ctrl + Enter) - {showDownloadButton ? ( + {this.state.downloadURL ? ( @@ -137,4 +163,7 @@ export class CompletionModal extends Component { CompletionModal.displayName = 'CompletionModal'; CompletionModal.propTypes = propTypes; -export default connect(mapStateToProps, mapDispatchToProps)(CompletionModal); +export default connect( + mapStateToProps, + mapDispatchToProps +)(CompletionModal); diff --git a/client/src/templates/Challenges/utils/frame.js b/client/src/templates/Challenges/utils/frame.js index c4fff6759b..ad118debb0 100644 --- a/client/src/templates/Challenges/utils/frame.js +++ b/client/src/templates/Challenges/utils/frame.js @@ -72,15 +72,24 @@ const buildProxyConsole = proxyLogger => ctx => { return ctx; }; -const writeTestDepsToDocument = frameReady => ctx => { - const { sources, loadEnzyme } = ctx; - // default for classic challenges - // should not be used for modern - ctx.document.__source = sources && 'index' in sources ? sources['index'] : ''; - // provide the file name and get the original source - ctx.document.__getUserInput = fileName => toString(sources[fileName]); - ctx.document.__frameReady = frameReady; - ctx.document.__loadEnzyme = loadEnzyme; +const initTestFrame = frameReady => ctx => { + const contentLoaded = new Promise(resolve => { + if (ctx.document.readyState === 'loading') { + ctx.document.addEventListener('DOMContentLoaded', resolve); + } else { + resolve(); + } + }); + contentLoaded.then(async() => { + const { sources, loadEnzyme } = ctx; + // default for classic challenges + // should not be used for modern + const code = sources && 'index' in sources ? sources['index'] : ''; + // provide the file name and get the original source + const getUserInput = fileName => toString(sources[fileName]); + await ctx.document.__initTestFrame({ code, getUserInput, loadEnzyme }); + frameReady(); + }); return ctx; }; @@ -107,7 +116,7 @@ export const createTestFramer = (document, frameReady, proxyConsole) => flow( createFrame(document, testId), mountFrame(document), - writeTestDepsToDocument(frameReady), + writeContentToFrame, buildProxyConsole(proxyConsole), - writeContentToFrame + initTestFrame(frameReady) ); diff --git a/client/src/templates/Challenges/utils/worker-executor.js b/client/src/templates/Challenges/utils/worker-executor.js index ed423c55ce..054a2754b0 100644 --- a/client/src/templates/Challenges/utils/worker-executor.js +++ b/client/src/templates/Challenges/utils/worker-executor.js @@ -10,9 +10,17 @@ class WorkerExecutor { this.getWorker = this.getWorker.bind(this); } - getWorker() { + async getWorker() { if (this.worker === null) { - this.worker = new Worker(`${this.location}${this.workerName}.js`); + this.worker = await new Promise((resolve, reject) => { + const worker = new Worker(`${this.location}${this.workerName}.js`); + worker.onmessage = e => { + if (e.data && e.data.type && e.data.type === 'contentLoaded') { + resolve(worker); + } + }; + worker.onerror = e => reject(e.message); + }); } return this.worker; @@ -25,8 +33,8 @@ class WorkerExecutor { } } - execute(data, timeout = 1000) { - const worker = this.getWorker(); + async execute(data, timeout = 1000) { + const worker = await this.getWorker(); return new Promise((resolve, reject) => { // Handle timeout const timeoutId = setTimeout(() => { diff --git a/curriculum/test/test-challenges.js b/curriculum/test/test-challenges.js index e900bf4a4e..8697635052 100644 --- a/curriculum/test/test-challenges.js +++ b/curriculum/test/test-challenges.js @@ -291,11 +291,9 @@ async function createTestRunnerForDOMChallenge( await context.setContent(build); await context.evaluate( async(sources, loadEnzyme) => { - document.__source = sources && 'index' in sources ? sources['index'] : ''; - document.__getUserInput = fileName => sources[fileName]; - document.__frameReady = () => {}; - document.__loadEnzyme = loadEnzyme; - await document.__initTestFrame(); + const code = sources && 'index' in sources ? sources['index'] : ''; + const getUserInput = fileName => sources[fileName]; + await document.__initTestFrame({ code, getUserInput, loadEnzyme }); }, sources, loadEnzyme