Files
freeCodeCamp/tools/client-plugins/browser-scripts/test-evaluator.ts
2025-04-03 09:02:05 -07:00

169 lines
5.1 KiB
TypeScript

import chai from 'chai';
import { toString as __toString } from 'lodash-es';
import * as curriculumHelpers from '@freecodecamp/curriculum-helpers';
import { format as __format } from './utils/format';
const ctx: Worker & typeof globalThis = self as unknown as Worker &
typeof globalThis;
const __utils = (() => {
const MAX_LOGS_SIZE = 64 * 1024;
const TRUNCATE_AT = 500_000;
let logs: string[] = [];
function flushLogs() {
if (logs.length) {
let data = logs.join('\n');
if (data.length > TRUNCATE_AT) {
data = `${data.substring(0, TRUNCATE_AT)} Logs truncated. See browser console for more`;
}
ctx.postMessage({
type: 'LOG',
data: data
});
logs = [];
}
}
function pushLogs(logs: string[], args: string[]) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
logs.push(args.map(arg => __format(arg)).join(' '));
if (logs.join('\n').length > MAX_LOGS_SIZE) {
flushLogs();
}
}
const oldLog = ctx.console.log.bind(ctx.console);
function proxyLog(...args: string[]) {
pushLogs(logs, args);
return oldLog(...args);
}
const oldInfo = ctx.console.info.bind(ctx.console);
function proxyInfo(...args: string[]) {
pushLogs(logs, args);
return oldInfo(...args);
}
const oldWarn = ctx.console.warn.bind(ctx.console);
function proxyWarn(...args: string[]) {
pushLogs(logs, args);
return oldWarn(...args);
}
const oldError = ctx.console.error.bind(ctx.console);
function proxyError(...args: string[]) {
pushLogs(logs, args);
return oldError(...args);
}
function log(...msgs: Error[]) {
if (msgs && msgs[0] && !(msgs[0] instanceof chai.AssertionError)) {
// discards the stack trace via toString as it only useful to debug the
// site, not a specific challenge.
console.log(...msgs.map(msg => msg.toString()));
}
}
const toggleProxyLogger = (on: unknown) => {
ctx.console.log = on ? proxyLog : oldLog;
ctx.console.info = on ? proxyInfo : oldInfo;
ctx.console.warn = on ? proxyWarn : oldWarn;
ctx.console.error = on ? proxyError : oldError;
};
return {
log,
toggleProxyLogger,
flushLogs
};
})();
// We can't simply import these because of how webpack names them when building
// the bundle. Since both assert and __helpers have to exist in the global
// scope, we have to declare them.
const assert = chai.assert;
const __helpers = curriculumHelpers;
// We freeze to prevent learners from getting the tester into a weird
// state by modifying these objects.
Object.freeze(self);
Object.freeze(__utils);
Object.freeze(assert);
Object.freeze(__helpers);
interface TestEvaluatorEvent extends MessageEvent {
data: {
code: {
contents: string;
editableContents: string;
};
firstTest: unknown;
testString: string;
build: string;
};
}
/* Run the test if there is one. If not just evaluate the user code */
ctx.onmessage = async (e: TestEvaluatorEvent) => {
/* eslint-disable @typescript-eslint/no-unused-vars */
const code = e.data?.code?.contents || '';
const editableContents = e.data?.code?.editableContents || '';
// Build errors should be reported, but only once:
__utils.toggleProxyLogger(e.data.firstTest);
/* eslint-enable @typescript-eslint/no-unused-vars */
try {
// This can be reassigned by the eval inside the try block, so it should be declared as a let
// eslint-disable-next-line prefer-const
let __userCodeWasExecuted = false;
try {
// Logging is proxyed after the build to catch console.log messages
// generated during testing.
await eval(`${e.data.build}
__utils.flushLogs();
__userCodeWasExecuted = true;
__utils.toggleProxyLogger(true);
(async () => {${e.data.testString}})()`);
} catch (err) {
if (__userCodeWasExecuted) {
// rethrow error, since test failed.
throw err;
}
// log build errors unless they're related to import/export/require (there
// are challenges that use them and they should not trigger warnings)
if (
(err as Error).name !== 'ReferenceError' ||
((err as Error).message !== 'require is not defined' &&
(err as Error).message !== 'exports is not defined')
) {
__utils.log(err as Error);
}
// the tests may not require working code, so they are evaluated even if
// the user code does not get executed.
eval(e.data.testString);
}
__utils.flushLogs();
ctx.postMessage({ pass: true });
} catch (err) {
// Errors from testing go to the browser console only.
__utils.toggleProxyLogger(false);
// Report execution errors in case user code has errors that are only
// uncovered during testing.
__utils.log(err as Error);
// Now that all logs have been created we can flush them.
__utils.flushLogs();
ctx.postMessage({
err: {
message: (err as Error).message,
stack: (err as Error).stack,
expected: (err as { expected?: string }).expected,
actual: (err as { actual?: string }).actual
}
});
}
};
ctx.postMessage({ type: 'contentLoaded' });