mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
feat: python error formatting (#54185)
Co-authored-by: Naomi <nhcarrigan@gmail.com>
This commit is contained in:
committed by
GitHub
parent
868a7c62bf
commit
3e03fc87e7
+51
-16
@@ -1,10 +1,26 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { expect, test, type Page } from '@playwright/test';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(
|
||||
'/learn/2022/responsive-web-design/learn-html-by-building-a-cat-photo-app/step-3'
|
||||
);
|
||||
});
|
||||
async function focusEditor({
|
||||
page,
|
||||
isMobile,
|
||||
browserName
|
||||
}: {
|
||||
page: Page;
|
||||
isMobile: boolean;
|
||||
browserName: string;
|
||||
}) {
|
||||
const monacoEditor = page.getByLabel('Editor content');
|
||||
|
||||
// The editor has an overlay div, which prevents the click event from bubbling up in iOS Safari.
|
||||
// This is a quirk in this browser-OS combination, and the workaround here is to use `.focus()`
|
||||
// in place of `.click()` to focus on the editor.
|
||||
// Ref: https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
|
||||
if (isMobile && browserName === 'webkit') {
|
||||
await monacoEditor.focus();
|
||||
} else {
|
||||
await monacoEditor.click();
|
||||
}
|
||||
}
|
||||
|
||||
test.describe('Editor Component', () => {
|
||||
test('should allow the user to insert text', async ({
|
||||
@@ -12,19 +28,38 @@ test.describe('Editor Component', () => {
|
||||
isMobile,
|
||||
browserName
|
||||
}) => {
|
||||
const monacoEditor = page.getByLabel('Editor content');
|
||||
await page.goto(
|
||||
'/learn/2022/responsive-web-design/learn-html-by-building-a-cat-photo-app/step-3'
|
||||
);
|
||||
|
||||
// The editor has an overlay div, which prevents the click event from bubbling up in iOS Safari.
|
||||
// This is a quirk in this browser-OS combination, and the workaround here is to use `.focus()`
|
||||
// in place of `.click()` to focus on the editor.
|
||||
// Ref: https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
|
||||
if (isMobile && browserName === 'webkit') {
|
||||
await monacoEditor.focus();
|
||||
} else {
|
||||
await monacoEditor.click();
|
||||
}
|
||||
await focusEditor({ page, isMobile, browserName });
|
||||
await page.keyboard.insertText('<h2>FreeCodeCamp</h2>');
|
||||
const text = page.getByText('<h2>FreeCodeCamp</h2>');
|
||||
await expect(text).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Python Terminal', () => {
|
||||
test('should display error message when the user enters invalid code', async ({
|
||||
page,
|
||||
isMobile,
|
||||
browserName
|
||||
}) => {
|
||||
await page.goto(
|
||||
'learn/scientific-computing-with-python/learn-string-manipulation-by-building-a-cipher/step-2'
|
||||
);
|
||||
|
||||
await focusEditor({ page, isMobile, browserName });
|
||||
// First clear the editor
|
||||
await page.keyboard.press('Control+a');
|
||||
await page.keyboard.press('Backspace');
|
||||
// Then enter invalid code
|
||||
await page.keyboard.insertText('def');
|
||||
const preview = page.getByTestId('preview-pane');
|
||||
|
||||
// While it's displayed on multiple lines, the string itself has no newlines, hence:
|
||||
const error = `>>> Traceback (most recent call last): File "main.py", line 1 def ^SyntaxError: invalid syntax`;
|
||||
// It shouldn't take this long, but the Python worker can be slow to respond.
|
||||
await expect(preview).toContainText(error, { timeout: 15000 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { loadPyodide, type PyodideInterface } from 'pyodide/pyodide.js';
|
||||
import pkg from 'pyodide/package.json';
|
||||
import type { PyProxy, PythonError } from 'pyodide/ffi';
|
||||
import * as helpers from '@freecodecamp/curriculum-helpers';
|
||||
|
||||
const ctx: Worker & typeof globalThis = self as unknown as Worker &
|
||||
typeof globalThis;
|
||||
@@ -53,6 +54,15 @@ async function setupPyodide() {
|
||||
// pyodide modifies self while loading.
|
||||
Object.freeze(self);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||
pyodide.FS.writeFile(
|
||||
'/home/pyodide/ast_helpers.py',
|
||||
helpers.python.astHelpers,
|
||||
{
|
||||
encoding: 'utf8'
|
||||
}
|
||||
);
|
||||
|
||||
ignoreRunMessages = true;
|
||||
postMessage({ type: 'stopped' });
|
||||
}
|
||||
@@ -134,10 +144,19 @@ function initRunPython() {
|
||||
else:
|
||||
return ""
|
||||
`);
|
||||
runPython(`
|
||||
def print_exception():
|
||||
from ast_helpers import format_exception
|
||||
formatted = format_exception(exception=sys.last_value, traceback=sys.last_traceback, filename="<exec>", new_filename="main.py")
|
||||
print(formatted)
|
||||
`);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
const printException = globals.get('print_exception') as PyProxy &
|
||||
(() => string);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
const getResetId = globals.get('__get_reset_id') as PyProxy & (() => string);
|
||||
return { runPython, getResetId, globals };
|
||||
return { runPython, getResetId, globals, printException };
|
||||
}
|
||||
|
||||
ctx.onmessage = (e: PythonRunEvent | ListenRequestEvent | CancelEvent) => {
|
||||
@@ -166,13 +185,16 @@ function handleRunRequest(data: PythonRunEvent['data']) {
|
||||
// TODO: use reset-terminal for clarity?
|
||||
postMessage({ type: 'reset' });
|
||||
|
||||
const { runPython, getResetId, globals } = initRunPython();
|
||||
const { runPython, getResetId, globals, printException } = initRunPython();
|
||||
// use pyodide.runPythonAsync if we want top-level await
|
||||
try {
|
||||
runPython(code);
|
||||
} catch (e) {
|
||||
const err = e as PythonError;
|
||||
console.error(e);
|
||||
// the formatted exception is printed to the terminal
|
||||
printException();
|
||||
// but the full error is logged to the console for debugging
|
||||
console.error(err);
|
||||
const resetId = getResetId();
|
||||
// TODO: if a user raises a KeyboardInterrupt with a custom message this
|
||||
// will be treated as a reset, the client will resend their code and this
|
||||
@@ -187,6 +209,7 @@ function handleRunRequest(data: PythonRunEvent['data']) {
|
||||
}
|
||||
} finally {
|
||||
getResetId.destroy();
|
||||
printException.destroy();
|
||||
globals.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user