diff --git a/client/src/templates/Challenges/utils/frame.js b/client/src/templates/Challenges/utils/frame.js
deleted file mode 100644
index 5da71c9c669..00000000000
--- a/client/src/templates/Challenges/utils/frame.js
+++ /dev/null
@@ -1,222 +0,0 @@
-import { toString, flow } from 'lodash-es';
-import { format } from '../../../utils/format';
-
-// we use two different frames to make them all essentially pure functions
-// main iframe is responsible rendering the preview and is where we proxy the
-export const mainPreviewId = 'fcc-main-frame';
-// the test frame is responsible for running the assert tests
-const testId = 'fcc-test-frame';
-// the project preview frame demos the finished project
-export const projectPreviewId = 'fcc-project-preview-frame';
-
-const DOCUMENT_NOT_FOUND_ERROR = 'document not found';
-
-// base tag here will force relative links
-// within iframe to point to '' instead of
-// append to the current challenge url
-// this also allows in-page anchors to work properly
-// rather than load another instance of the learn
-
-// window.onerror is added here to report any errors thrown during the building
-// of the frame. React dom errors already appear in the console, so onerror
-// does not need to pass them on to the default error handler.
-const createHeader = (id = mainPreviewId) => `
-
-
-`;
-
-export const runTestInTestFrame = async function (document, test, timeout) {
- const { contentDocument: frame } = document.getElementById(testId);
- return await Promise.race([
- new Promise((_, reject) => setTimeout(() => reject('timeout'), timeout)),
- frame.__runTest(test)
- ]);
-};
-
-const createFrame = (document, id, title) => ctx => {
- const frame = document.createElement('iframe');
- frame.id = id;
- if (typeof title === 'string') {
- frame.title = title;
- }
- return {
- ...ctx,
- element: frame
- };
-};
-
-const hiddenFrameClassName = 'hide-test-frame';
-const mountFrame =
- (document, id) =>
- ({ element, ...rest }) => {
- const oldFrame = document.getElementById(element.id);
- if (oldFrame) {
- element.className = oldFrame.className || hiddenFrameClassName;
- oldFrame.parentNode.replaceChild(element, oldFrame);
- // only test frames can be added (and hidden) here, other frames must be
- // added by react
- } else if (id === testId) {
- element.className = hiddenFrameClassName;
- document.body.appendChild(element);
- }
- return {
- ...rest,
- element,
- document: element.contentDocument,
- window: element.contentWindow
- };
- };
-
-const buildProxyConsole = proxyLogger => ctx => {
- // window does not exist if the preview is hidden, so we have to check.
- if (proxyLogger && ctx?.window) {
- const oldLog = ctx.window.console.log.bind(ctx.window.console);
- ctx.window.console.log = function proxyConsole(...args) {
- proxyLogger(args.map(arg => format(arg)).join(' '));
- return oldLog(...args);
- };
- }
- return ctx;
-};
-
-const initTestFrame = frameReady => ctx => {
- waitForFrame(ctx)
- .then(async () => {
- const { sources, loadEnzyme } = ctx;
- // provide the file name and get the original source
- const getUserInput = fileName => toString(sources[fileName]);
- await ctx.document.__initTestFrame({
- code: sources,
- getUserInput,
- loadEnzyme
- });
- frameReady();
- })
- .catch(handleDocumentNotFound);
- return ctx;
-};
-
-const initMainFrame = (_, proxyLogger) => ctx => {
- waitForFrame(ctx)
- .then(() => {
- // Overwriting the onerror added by createHeader to catch any errors thrown
- // after the frame is ready. It has to be overwritten, as proxyLogger cannot
- // be added as part of createHeader.
- ctx.window.onerror = function (msg) {
- var string = msg.toLowerCase();
- if (string.includes('script error')) {
- msg = 'Error, open your browser console to learn more.';
- }
- if (proxyLogger) {
- proxyLogger(msg);
- }
- // let the error propagate so it appears in the browser console, otherwise
- // an error from a cross origin script just appears as 'Script error.'
- return false;
- };
- })
- .catch(handleDocumentNotFound);
- return ctx;
-};
-
-function handleDocumentNotFound(err) {
- if (err !== DOCUMENT_NOT_FOUND_ERROR) {
- console.log(err);
- }
-}
-
-const initPreviewFrame = () => ctx => ctx;
-
-const waitForFrame = ctx => {
- return new Promise((resolve, reject) => {
- if (!ctx.document) {
- reject(DOCUMENT_NOT_FOUND_ERROR);
- } else if (ctx.document.readyState === 'loading') {
- ctx.document.addEventListener('DOMContentLoaded', resolve);
- } else {
- resolve();
- }
- });
-};
-
-function writeToFrame(content, frame) {
- // it's possible, if the preview is rapidly opened and closed, for the frame
- // to be null at this point.
- if (frame) {
- frame.open();
- frame.write(content);
- frame.close();
- }
-}
-
-const writeContentToFrame = ctx => {
- writeToFrame(createHeader(ctx.element.id) + ctx.build, ctx.document);
- return ctx;
-};
-
-export const createMainPreviewFramer = (document, proxyLogger) =>
- createFramer(
- document,
- mainPreviewId,
- initMainFrame,
- proxyLogger,
- undefined,
- 'preview'
- );
-
-export const createProjectPreviewFramer = (document, frameTitle) =>
- createFramer(
- document,
- projectPreviewId,
- initPreviewFrame,
- undefined,
- undefined,
- frameTitle
- );
-
-export const createTestFramer = (document, proxyLogger, frameReady) =>
- createFramer(document, testId, initTestFrame, proxyLogger, frameReady);
-
-const createFramer = (
- document,
- id,
- init,
- proxyLogger,
- frameReady,
- frameTitle
-) =>
- flow(
- createFrame(document, id, frameTitle),
- mountFrame(document, id),
- buildProxyConsole(proxyLogger),
- writeContentToFrame,
- init(frameReady, proxyLogger)
- );
diff --git a/client/src/templates/Challenges/utils/frame.ts b/client/src/templates/Challenges/utils/frame.ts
new file mode 100644
index 00000000000..b9c405768c8
--- /dev/null
+++ b/client/src/templates/Challenges/utils/frame.ts
@@ -0,0 +1,279 @@
+import { toString, flow } from 'lodash-es';
+import { format } from '../../../utils/format';
+
+const utilsFormat: (x: T) => string = format;
+
+declare global {
+ interface Window {
+ console: {
+ log: () => void;
+ };
+ }
+}
+
+interface Context {
+ window: Window;
+ document: Document;
+ element: HTMLIFrameElement;
+ build: string;
+ sources: {
+ contents?: string;
+ editableContents?: string;
+ original?: { [id: string]: string };
+ };
+ loadEnzyme?: () => void;
+}
+
+type ProxyLogger = (msg: string) => void;
+
+type InitFrame = (
+ arg1?: () => unknown,
+ arg2?: ProxyLogger
+) => (ctx: Context) => Context;
+
+// we use two different frames to make them all essentially pure functions
+// main iframe is responsible rendering the preview and is where we proxy the
+export const mainPreviewId = 'fcc-main-frame';
+// the test frame is responsible for running the assert tests
+const testId = 'fcc-test-frame';
+// the project preview frame demos the finished project
+export const projectPreviewId = 'fcc-project-preview-frame';
+
+const DOCUMENT_NOT_FOUND_ERROR = 'document not found';
+
+// base tag here will force relative links
+// within iframe to point to '' instead of
+// append to the current challenge url
+// this also allows in-page anchors to work properly
+// rather than load another instance of the learn
+
+// window.onerror is added here to report any errors thrown during the building
+// of the frame. React dom errors already appear in the console, so onerror
+// does not need to pass them on to the default error handler.
+const createHeader = (id = mainPreviewId) => `
+
+
+`;
+
+export const runTestInTestFrame = async function (
+ document: Document,
+ test: string,
+ timeout: number
+) {
+ const { contentDocument: frame } = document.getElementById(
+ testId
+ ) as HTMLIFrameElement;
+ if (frame !== null) {
+ return await Promise.race([
+ new Promise<
+ { pass: boolean } | { err: { message: string; stack?: string } }
+ >((_, reject) => setTimeout(() => reject('timeout'), timeout)),
+ frame.__runTest(test)
+ ]);
+ }
+};
+
+const createFrame =
+ (document: Document, id: string, title?: string) => (ctx: Context) => {
+ const frame = document.createElement('iframe');
+ frame.id = id;
+ if (typeof title === 'string') {
+ frame.title = title;
+ }
+ return {
+ ...ctx,
+ element: frame
+ };
+ };
+
+const hiddenFrameClassName = 'hide-test-frame';
+const mountFrame = (document: Document, id: string) => (ctx: Context) => {
+ const { element }: { element: HTMLIFrameElement } = ctx;
+ const oldFrame = document.getElementById(element.id) as HTMLIFrameElement;
+ if (oldFrame) {
+ element.className = oldFrame.className || hiddenFrameClassName;
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ oldFrame.parentNode!.replaceChild(element, oldFrame);
+ // only test frames can be added (and hidden) here, other frames must be
+ // added by react
+ } else if (id === testId) {
+ element.className = hiddenFrameClassName;
+ document.body.appendChild(element);
+ }
+ return {
+ ...ctx,
+ element,
+ document: element.contentDocument,
+ window: element.contentWindow
+ };
+};
+
+const buildProxyConsole = (proxyLogger?: ProxyLogger) => (ctx: Context) => {
+ // window does not exist if the preview is hidden, so we have to check.
+ if (proxyLogger && ctx?.window) {
+ const oldLog = ctx.window.console.log.bind(ctx.window.console);
+ ctx.window.console.log = function proxyConsole(...args: string[]) {
+ proxyLogger(args.map((arg: string) => utilsFormat(arg)).join(' '));
+ return oldLog(...(args as []));
+ };
+ }
+ return ctx;
+};
+
+const initTestFrame = (frameReady?: () => void) => (ctx: Context) => {
+ waitForFrame(ctx)
+ .then(async () => {
+ const { sources, loadEnzyme } = ctx;
+ // provide the file name and get the original source
+ const getUserInput = (fileName: string) =>
+ toString(sources[fileName as keyof typeof sources]);
+ await ctx.document.__initTestFrame({
+ code: sources,
+ getUserInput,
+ loadEnzyme
+ });
+ if (frameReady) {
+ frameReady();
+ }
+ })
+ .catch(handleDocumentNotFound);
+ return ctx;
+};
+
+const initMainFrame =
+ (_: unknown, proxyLogger?: ProxyLogger) => (ctx: Context) => {
+ waitForFrame(ctx)
+ .then(() => {
+ // Overwriting the onerror added by createHeader to catch any errors thrown
+ // after the frame is ready. It has to be overwritten, as proxyLogger cannot
+ // be added as part of createHeader.
+
+ ctx.window.onerror = function (msg) {
+ if (typeof msg === 'string') {
+ const string = msg.toLowerCase();
+ if (string.includes('script error')) {
+ msg = 'Error, open your browser console to learn more.';
+ }
+ if (proxyLogger) {
+ proxyLogger(msg);
+ }
+ }
+ // let the error propagate so it appears in the browser console, otherwise
+ // an error from a cross origin script just appears as 'Script error.'
+ return false;
+ };
+ })
+ .catch(handleDocumentNotFound);
+ return ctx;
+ };
+
+function handleDocumentNotFound(err: string) {
+ if (err !== DOCUMENT_NOT_FOUND_ERROR) {
+ console.log(err);
+ }
+}
+
+const initPreviewFrame = () => (ctx: Context) => ctx;
+
+const waitForFrame = (ctx: Context) => {
+ return new Promise((resolve, reject) => {
+ if (!ctx.document) {
+ reject(DOCUMENT_NOT_FOUND_ERROR);
+ } else if (ctx.document.readyState === 'loading') {
+ ctx.document.addEventListener('DOMContentLoaded', resolve);
+ } else {
+ resolve(null);
+ }
+ });
+};
+
+function writeToFrame(content: string, frame: Document | null) {
+ // it's possible, if the preview is rapidly opened and closed, for the frame
+ // to be null at this point.
+ if (frame) {
+ frame.open();
+ frame.write(content);
+ frame.close();
+ }
+}
+
+const writeContentToFrame = (ctx: Context) => {
+ writeToFrame(createHeader(ctx.element.id) + ctx.build, ctx.document);
+ return ctx;
+};
+
+export const createMainPreviewFramer = (
+ document: Document,
+ proxyLogger: ProxyLogger
+) =>
+ createFramer(
+ document,
+ mainPreviewId,
+ initMainFrame,
+ proxyLogger,
+ undefined,
+ 'preview'
+ );
+
+export const createProjectPreviewFramer = (
+ document: Document,
+ frameTitle: string
+) =>
+ createFramer(
+ document,
+ projectPreviewId,
+ initPreviewFrame,
+ undefined,
+ undefined,
+ frameTitle
+ );
+
+export const createTestFramer = (
+ document: Document,
+ proxyLogger: ProxyLogger,
+ frameReady: () => void
+) => createFramer(document, testId, initTestFrame, proxyLogger, frameReady);
+
+const createFramer = (
+ document: Document,
+ id: string,
+ init: InitFrame,
+ proxyLogger?: ProxyLogger,
+ frameReady?: () => void,
+ frameTitle?: string
+) =>
+ flow(
+ createFrame(document, id, frameTitle),
+ mountFrame(document, id),
+ buildProxyConsole(proxyLogger),
+ writeContentToFrame,
+ init(frameReady, proxyLogger)
+ );
diff --git a/curriculum/package.json b/curriculum/package.json
index 7dad6bb3c8c..9341d50a6ea 100644
--- a/curriculum/package.json
+++ b/curriculum/package.json
@@ -25,7 +25,7 @@
"delete-step": "cross-env CALLING_DIR=$INIT_CWD ts-node --project ../tsconfig.json ../tools/challenge-helper-scripts/delete-step",
"lint": "ts-node --project ../tsconfig.json lint-localized",
"update-step-titles": "cross-env CALLING_DIR=$INIT_CWD ts-node --project ../tsconfig.json ../tools/challenge-helper-scripts/update-step-titles",
- "test": "mocha --delay --exit --reporter progress --bail",
+ "test": "ts-node ../node_modules/mocha/bin/mocha --delay --exit --reporter progress --bail",
"test:full-output": "cross-env FULL_OUTPUT=true mocha --delay --reporter progress"
},
"devDependencies": {