mirror of
https://github.com/pyscript/pyscript.git
synced 2026-03-17 22:00:14 -04:00
Add error codes to our custom errors (#959)
This commit is contained in:
28
docs/reference/exceptions.md
Normal file
28
docs/reference/exceptions.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Exceptions and error codes
|
||||
|
||||
When creating pages with PyScript, you may encounter exceptions. Each handled exception will contain a specific code which will give you more information about it.
|
||||
This reference guide contains the error codes you might find and a description of each of them.
|
||||
|
||||
## User Errors
|
||||
|
||||
| Error code | Description | Recommendation |
|
||||
|------------|--------------------------------|--------------------|
|
||||
| PY1000 | Invalid configuration supplied | Confirm that your `py-config` tag is using a valid `TOML` or `JSON` syntax and is using the correct configuration type. |
|
||||
| PY9000 | Top level await is deprecated | Create a coroutine with your code and schedule it with `asyncio.ensure_future` or similar |
|
||||
|
||||
|
||||
|
||||
## Fetch Errors
|
||||
|
||||
These error codes are related to any exception raised when trying to fetch a resource. If, while trying to fetch a resource, we encounter a status code that is not 200, the error code will contain the HTTP status code and the `PY0` prefix. For example, if we encounter a 404 error, the error code will be `P02404`.
|
||||
|
||||
|
||||
| Error Code | Description |
|
||||
|------------|--------------------------------------------------------------|
|
||||
| PY0001 | Generic fetch error, failed to fetch page from the server |
|
||||
| PY0002 | Name supplied when trying to fetch resource is invalid |
|
||||
| PY0401 | You are not authorized to access this resource. |
|
||||
| PY0403 | You are not allowed to access this resource. |
|
||||
| PY0404 | The page you are trying to fetch does not exist. |
|
||||
| PY0500 | The server encountered an internal error. |
|
||||
| PY0503 | The server is currently unavailable. |
|
||||
@@ -36,6 +36,15 @@ caption: API
|
||||
API/*
|
||||
```
|
||||
|
||||
```{toctree}
|
||||
---
|
||||
maxdepth: 2
|
||||
glob:
|
||||
caption: Exceptions
|
||||
---
|
||||
exceptions
|
||||
```
|
||||
|
||||
```{toctree}
|
||||
---
|
||||
maxdepth: 1
|
||||
|
||||
@@ -2,7 +2,8 @@ import { htmlDecode, ensureUniqueId } from '../utils';
|
||||
import type { Runtime } from '../runtime';
|
||||
import { getLogger } from '../logger';
|
||||
import { pyExec } from '../pyexec';
|
||||
import { FetchError, _createAlertBanner } from '../exceptions';
|
||||
import { _createAlertBanner } from '../exceptions';
|
||||
import { robustFetch } from '../fetch';
|
||||
|
||||
const logger = getLogger('py-script');
|
||||
|
||||
@@ -18,17 +19,14 @@ export function make_PyScript(runtime: Runtime) {
|
||||
async getPySrc(): Promise<string> {
|
||||
if (this.hasAttribute('src')) {
|
||||
const url = this.getAttribute('src');
|
||||
const response = await fetch(url);
|
||||
if (response.status !== 200) {
|
||||
const errorMessage = (
|
||||
`Failed to fetch '${url}' - Reason: ` +
|
||||
`${response.status} ${response.statusText}`
|
||||
);
|
||||
_createAlertBanner(errorMessage);
|
||||
try {
|
||||
const response = await robustFetch(url);
|
||||
return await response.text();
|
||||
} catch(e) {
|
||||
_createAlertBanner(e.message);
|
||||
this.innerHTML = '';
|
||||
throw new FetchError(errorMessage);
|
||||
throw e
|
||||
}
|
||||
return await response.text();
|
||||
} else {
|
||||
return htmlDecode(this.innerHTML);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Runtime } from '../runtime';
|
||||
import type { PyProxy } from 'pyodide';
|
||||
import { getLogger } from '../logger';
|
||||
import { robustFetch } from '../fetch';
|
||||
|
||||
const logger = getLogger('py-register-widget');
|
||||
|
||||
@@ -92,7 +93,7 @@ export function make_PyWidget(runtime: Runtime) {
|
||||
}
|
||||
|
||||
async getSourceFromFile(s: string): Promise<string> {
|
||||
const response = await fetch(s);
|
||||
const response = await robustFetch(s);
|
||||
return await response.text();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,20 +2,49 @@ const CLOSEBUTTON = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'
|
||||
|
||||
type MessageType = "text" | "html";
|
||||
|
||||
/*
|
||||
These error codes are used to identify the type of error that occurred.
|
||||
The convention is:
|
||||
* PY0 - errors that occur when fetching
|
||||
* PY1 - errors that occur in config
|
||||
* PY9 - Deprecation errors
|
||||
*/
|
||||
|
||||
export enum ErrorCode {
|
||||
GENERIC = "PY0000", // Use this only for development then change to a more specific error code
|
||||
FETCH_ERROR = "PY0001",
|
||||
FETCH_NAME_ERROR = "PY0002",
|
||||
// Currently these are created depending on error code received from fetching
|
||||
FETCH_UNAUTHORIZED_ERROR = "PY0401",
|
||||
FETCH_FORBIDDEN_ERROR = "PY0403",
|
||||
FETCH_NOT_FOUND_ERROR = "PY0404",
|
||||
FETCH_SERVER_ERROR = "PY0500",
|
||||
FETCH_UNAVAILABLE_ERROR = "PY0503",
|
||||
BAD_CONFIG = "PY1000",
|
||||
TOP_LEVEL_AWAIT = "PY9000"
|
||||
}
|
||||
|
||||
export class UserError extends Error {
|
||||
messageType: MessageType;
|
||||
errorCode: ErrorCode;
|
||||
|
||||
constructor(message: string, t: MessageType = "text") {
|
||||
constructor(errorCode: ErrorCode, message: string, t: MessageType = "text") {
|
||||
super(message);
|
||||
this.errorCode = errorCode;
|
||||
this.name = "UserError";
|
||||
this.messageType = t;
|
||||
this.message = `(${errorCode}): ${message}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class FetchError extends Error {
|
||||
constructor(message: string) {
|
||||
errorCode: ErrorCode;
|
||||
constructor(errorCode: ErrorCode, message: string) {
|
||||
super(message)
|
||||
this.name = "FetchError"
|
||||
this.name = "FetchError";
|
||||
this.errorCode = errorCode;
|
||||
this.message = `(${errorCode}): ${message}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
47
pyscriptjs/src/fetch.ts
Normal file
47
pyscriptjs/src/fetch.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { FetchError, ErrorCode } from "./exceptions";
|
||||
|
||||
|
||||
/*
|
||||
This is a fetch wrapper that handles any non 200 response and throws a FetchError
|
||||
with the right ErrorCode.
|
||||
TODO: Should we only throw on 4xx and 5xx responses?
|
||||
*/
|
||||
export async function robustFetch(url: string, options?: RequestInit): Promise<Response> {
|
||||
const response = await fetch(url, options);
|
||||
if (response.status !== 200) {
|
||||
const errorMsg = `Fetching from URL ${url} failed with error ${response.status} (${response.statusText}).`;
|
||||
switch(response.status) {
|
||||
case 404:
|
||||
throw new FetchError(
|
||||
ErrorCode.FETCH_NOT_FOUND_ERROR,
|
||||
errorMsg
|
||||
);
|
||||
case 401:
|
||||
throw new FetchError(
|
||||
ErrorCode.FETCH_UNAUTHORIZED_ERROR,
|
||||
errorMsg
|
||||
);
|
||||
case 403:
|
||||
throw new FetchError(
|
||||
ErrorCode.FETCH_FORBIDDEN_ERROR,
|
||||
errorMsg
|
||||
);
|
||||
case 500:
|
||||
throw new FetchError(
|
||||
ErrorCode.FETCH_SERVER_ERROR,
|
||||
errorMsg
|
||||
);
|
||||
case 503:
|
||||
throw new FetchError(
|
||||
ErrorCode.FETCH_UNAVAILABLE_ERROR,
|
||||
errorMsg
|
||||
);
|
||||
default:
|
||||
throw new FetchError(
|
||||
ErrorCode.FETCH_ERROR,
|
||||
errorMsg
|
||||
);
|
||||
}
|
||||
}
|
||||
return response
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import { getLogger } from './logger';
|
||||
import { handleFetchError, showWarning, globalExport } from './utils';
|
||||
import { calculatePaths } from './plugins/fetch';
|
||||
import { createCustomElements } from './components/elements';
|
||||
import { UserError, _createAlertBanner } from "./exceptions"
|
||||
import { UserError, ErrorCode, _createAlertBanner } from "./exceptions"
|
||||
import { type Stdio, StdioMultiplexer, DEFAULT_STDIO } from './stdio';
|
||||
import { PyTerminalPlugin } from './plugins/pyterminal';
|
||||
import { SplashscreenPlugin } from './plugins/splashscreen';
|
||||
@@ -133,7 +133,7 @@ export class PyScriptApp {
|
||||
loadRuntime() {
|
||||
logger.info('Initializing runtime');
|
||||
if (this.config.runtimes.length == 0) {
|
||||
throw new UserError('Fatal error: config.runtimes is empty');
|
||||
throw new UserError(ErrorCode.BAD_CONFIG, 'Fatal error: config.runtimes is empty');
|
||||
}
|
||||
|
||||
if (this.config.runtimes.length > 1) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { joinPaths } from '../utils';
|
||||
import { FetchConfig } from "../pyconfig";
|
||||
import { UserError } from '../exceptions';
|
||||
import { UserError, ErrorCode } from '../exceptions';
|
||||
|
||||
export function calculatePaths(fetch_cfg: FetchConfig[]) {
|
||||
const fetchPaths: string[] = [];
|
||||
@@ -14,7 +14,10 @@ export function calculatePaths(fetch_cfg: FetchConfig[]) {
|
||||
{
|
||||
if (to_file !== undefined)
|
||||
{
|
||||
throw new UserError(`Cannot use 'to_file' and 'files' parameters together!`);
|
||||
throw new UserError(
|
||||
ErrorCode.BAD_CONFIG,
|
||||
`Cannot use 'to_file' and 'files' parameters together!`
|
||||
);
|
||||
}
|
||||
for (const each_f of files)
|
||||
{
|
||||
@@ -29,7 +32,10 @@ export function calculatePaths(fetch_cfg: FetchConfig[]) {
|
||||
fetchPaths.push(from);
|
||||
const filename = to_file || from.split('/').pop();
|
||||
if (filename === '') {
|
||||
throw new UserError(`Couldn't determine the filename from the path ${from}, supply ${to_file} parameter!`);
|
||||
throw new UserError(
|
||||
ErrorCode.BAD_CONFIG,
|
||||
`Couldn't determine the filename from the path ${from}, please supply 'to_file' parameter.`
|
||||
);
|
||||
}
|
||||
else {
|
||||
paths.push(joinPaths([to_folder, filename]));
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { PyScriptApp } from '../main';
|
||||
import type { AppConfig } from '../pyconfig';
|
||||
import type { Runtime } from '../runtime';
|
||||
import { Plugin } from '../plugin';
|
||||
import { UserError } from "../exceptions"
|
||||
import { UserError, ErrorCode } from "../exceptions"
|
||||
import { getLogger } from '../logger';
|
||||
import { type Stdio } from '../stdio';
|
||||
|
||||
@@ -24,8 +24,11 @@ export class PyTerminalPlugin extends Plugin {
|
||||
t !== false &&
|
||||
t !== "auto") {
|
||||
const got = JSON.stringify(t);
|
||||
throw new UserError('Invalid value for config.terminal: the only accepted' +
|
||||
`values are true, false and "auto", got "${got}".`);
|
||||
throw new UserError(
|
||||
ErrorCode.BAD_CONFIG,
|
||||
'Invalid value for config.terminal: the only accepted' +
|
||||
`values are true, false and "auto", got "${got}".`
|
||||
);
|
||||
}
|
||||
if (t === undefined) {
|
||||
config.terminal = "auto"; // default value
|
||||
|
||||
@@ -2,7 +2,7 @@ import toml from '../src/toml';
|
||||
import { getLogger } from './logger';
|
||||
import { version } from './runtime';
|
||||
import { getAttribute, readTextFromPath, htmlDecode } from './utils';
|
||||
import { UserError } from "./exceptions"
|
||||
import { UserError, ErrorCode } from "./exceptions"
|
||||
|
||||
const logger = getLogger('py-config');
|
||||
|
||||
@@ -145,22 +145,34 @@ function parseConfig(configText: string, configType = 'toml') {
|
||||
try {
|
||||
// TOML parser is soft and can parse even JSON strings, this additional check prevents it.
|
||||
if (configText.trim()[0] === '{') {
|
||||
throw new UserError(`The config supplied: ${configText} is an invalid TOML and cannot be parsed`);
|
||||
throw new UserError(
|
||||
ErrorCode.BAD_CONFIG,
|
||||
`The config supplied: ${configText} is an invalid TOML and cannot be parsed`
|
||||
);
|
||||
}
|
||||
config = toml.parse(configText);
|
||||
} catch (err) {
|
||||
const errMessage: string = err.toString();
|
||||
throw new UserError(`The config supplied: ${configText} is an invalid TOML and cannot be parsed: ${errMessage}`);
|
||||
throw new UserError(
|
||||
ErrorCode.BAD_CONFIG,
|
||||
`The config supplied: ${configText} is an invalid TOML and cannot be parsed: ${errMessage}`
|
||||
);
|
||||
}
|
||||
} else if (configType === 'json') {
|
||||
try {
|
||||
config = JSON.parse(configText);
|
||||
} catch (err) {
|
||||
const errMessage: string = err.toString();
|
||||
throw new UserError(`The config supplied: ${configText} is an invalid JSON and cannot be parsed: ${errMessage}`);
|
||||
throw new UserError(
|
||||
ErrorCode.BAD_CONFIG,
|
||||
`The config supplied: ${configText} is an invalid JSON and cannot be parsed: ${errMessage}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new UserError(`The type of config supplied '${configType}' is not supported, supported values are ["toml", "json"]`);
|
||||
throw new UserError(
|
||||
ErrorCode.BAD_CONFIG,
|
||||
`The type of config supplied '${configType}' is not supported, supported values are ["toml", "json"]`
|
||||
);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getLogger } from './logger';
|
||||
import { ensureUniqueId, ltrim } from './utils';
|
||||
import { UserError } from './exceptions';
|
||||
import { ensureUniqueId } from './utils';
|
||||
import { UserError, ErrorCode } from './exceptions';
|
||||
import type { Runtime } from './runtime';
|
||||
|
||||
const logger = getLogger('pyexec');
|
||||
@@ -16,11 +16,12 @@ export function pyExec(runtime: Runtime, pysrc: string, outElem: HTMLElement) {
|
||||
try {
|
||||
if (usesTopLevelAwait(pysrc)){
|
||||
throw new UserError(
|
||||
'The use of top-level "await", "async for", and ' +
|
||||
'"async with" is deprecated.' +
|
||||
'\nPlease write a coroutine containing ' +
|
||||
'your code and schedule it using asyncio.ensure_future() or similar.' +
|
||||
'\nSee https://docs.pyscript.net/latest/guides/asyncio.html for more information.'
|
||||
ErrorCode.TOP_LEVEL_AWAIT,
|
||||
'The use of top-level "await", "async for", and ' +
|
||||
'"async with" is deprecated.' +
|
||||
'\nPlease write a coroutine containing ' +
|
||||
'your code and schedule it using asyncio.ensure_future() or similar.' +
|
||||
'\nSee https://docs.pyscript.net/latest/guides/asyncio.html for more information.',
|
||||
)
|
||||
}
|
||||
return runtime.run(pysrc);
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Runtime } from './runtime';
|
||||
import { Runtime, version } from './runtime';
|
||||
import { getLogger } from './logger';
|
||||
import { FetchError } from './exceptions'
|
||||
import type { loadPyodide as loadPyodideDeclaration, PyodideInterface, PyProxy } from 'pyodide';
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
import pyscript from './python/pyscript.py';
|
||||
import { version } from './runtime';
|
||||
import { robustFetch } from './fetch';
|
||||
import type { AppConfig } from './pyconfig';
|
||||
import type { Stdio } from './stdio';
|
||||
|
||||
@@ -112,10 +111,7 @@ export class PyodideRuntime extends Runtime {
|
||||
this.interpreter.FS.mkdir(eachPath);
|
||||
}
|
||||
}
|
||||
const response = await fetch(fetch_path);
|
||||
if (response.status !== 200) {
|
||||
throw new FetchError(`Unable to fetch ${fetch_path}, reason: ${response.status} - ${response.statusText}`);
|
||||
}
|
||||
const response = await robustFetch(fetch_path);
|
||||
const buffer = await response.arrayBuffer();
|
||||
const data = new Uint8Array(buffer);
|
||||
pathArr.push(filename);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { _createAlertBanner, UserError } from "./exceptions"
|
||||
import { _createAlertBanner, UserError, FetchError, ErrorCode } from "./exceptions"
|
||||
|
||||
export function addClasses(element: HTMLElement, classes: string[]) {
|
||||
for (const entry of classes) {
|
||||
@@ -50,21 +50,21 @@ export function handleFetchError(e: Error, singleFile: string) {
|
||||
// fragile. We need a better solution.
|
||||
let errorContent: string;
|
||||
if (e.message.includes('Failed to fetch')) {
|
||||
errorContent = `<p>PyScript: Access to local files
|
||||
errorContent = `PyScript: Access to local files
|
||||
(using "Paths:" in <py-config>)
|
||||
is not available when directly opening a HTML file;
|
||||
you must use a webserver to serve the additional files.
|
||||
See <a style="text-decoration: underline;" href="https://github.com/pyscript/pyscript/issues/257#issuecomment-1119595062">this reference</a>
|
||||
on starting a simple webserver with Python.</p>`;
|
||||
on starting a simple webserver with Python.`;
|
||||
} else if (e.message.includes('404')) {
|
||||
errorContent =
|
||||
`<p>PyScript: Loading from file <u>` +
|
||||
`PyScript: Loading from file <u>` +
|
||||
singleFile +
|
||||
`</u> failed with error 404 (File not Found). Are your filename and path are correct?</p>`;
|
||||
`</u> failed with error 404 (File not Found). Are your filename and path are correct?`;
|
||||
} else {
|
||||
errorContent = `<p>PyScript encountered an error while loading from file: ${e.message} </p>`;
|
||||
errorContent = `PyScript encountered an error while loading from file: ${e.message}`;
|
||||
}
|
||||
throw new UserError(errorContent, "html");
|
||||
throw new UserError(ErrorCode.FETCH_ERROR, errorContent, "html");
|
||||
}
|
||||
|
||||
export function readTextFromPath(path: string) {
|
||||
|
||||
@@ -145,7 +145,13 @@ class TestBasic(PyScriptTest):
|
||||
self.check_js_errors()
|
||||
|
||||
error_msg = str(exc.value)
|
||||
assert "Failed to fetch" in error_msg
|
||||
if self.is_fake_server:
|
||||
assert "Failed to fetch" in error_msg
|
||||
else:
|
||||
assert (
|
||||
"Fetching from URL foo.py failed with error 404 (File not found)"
|
||||
in error_msg
|
||||
)
|
||||
|
||||
pyscript_tag = self.page.locator("py-script")
|
||||
assert pyscript_tag.inner_html() == ""
|
||||
|
||||
@@ -118,7 +118,7 @@ class TestConfig(PyScriptTest):
|
||||
banner = self.page.wait_for_selector(".py-error")
|
||||
assert "SyntaxError: Unexpected end of JSON input" in self.console.error.text
|
||||
expected = (
|
||||
"The config supplied: [[ is an invalid JSON and cannot be "
|
||||
"(PY1000): The config supplied: [[ is an invalid JSON and cannot be "
|
||||
"parsed: SyntaxError: Unexpected end of JSON input"
|
||||
)
|
||||
assert banner.inner_text() == expected
|
||||
@@ -137,7 +137,7 @@ class TestConfig(PyScriptTest):
|
||||
banner = self.page.wait_for_selector(".py-error")
|
||||
assert "SyntaxError: Expected DoubleQuote" in self.console.error.text
|
||||
expected = (
|
||||
"The config supplied: [[ is an invalid TOML and cannot be parsed: "
|
||||
"(PY1000): The config supplied: [[ is an invalid TOML and cannot be parsed: "
|
||||
"SyntaxError: Expected DoubleQuote, Whitespace, or [a-z], [A-Z], "
|
||||
'[0-9], "-", "_" but "\\n" found.'
|
||||
)
|
||||
@@ -178,7 +178,7 @@ class TestConfig(PyScriptTest):
|
||||
"""
|
||||
self.pyscript_run(snippet, wait_for_pyscript=False)
|
||||
div = self.page.wait_for_selector(".py-error")
|
||||
assert div.text_content() == "Fatal error: config.runtimes is empty"
|
||||
assert div.text_content() == "(PY1000): Fatal error: config.runtimes is empty"
|
||||
|
||||
def test_multiple_runtimes(self):
|
||||
snippet = """
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { expect, it, jest } from "@jest/globals"
|
||||
import { _createAlertBanner, UserError } from "../../src/exceptions"
|
||||
import { expect, it, jest, describe, afterEach } from "@jest/globals"
|
||||
import { _createAlertBanner, UserError, FetchError, ErrorCode } from "../../src/exceptions"
|
||||
|
||||
describe("Test _createAlertBanner", () => {
|
||||
|
||||
afterEach(() => {
|
||||
// Ensure we always have a clean body
|
||||
document.body.innerHTML = `<div>Hello World</div>`;
|
||||
@@ -108,3 +107,21 @@ describe("Test _createAlertBanner", () => {
|
||||
expect(banner[0].textContent).toBe("Test message");
|
||||
})
|
||||
})
|
||||
|
||||
describe("Test Exceptions", () => {
|
||||
it('UserError contains errorCode and shows in message', async() => {
|
||||
const errorCode = ErrorCode.BAD_CONFIG;
|
||||
const message = 'Test error';
|
||||
const userError = new UserError(ErrorCode.BAD_CONFIG, message);
|
||||
expect(userError.errorCode).toBe(errorCode);
|
||||
expect(userError.message).toBe(`(${errorCode}): ${message}`);
|
||||
})
|
||||
|
||||
it('FetchError contains errorCode and shows in message', async() => {
|
||||
const errorCode = ErrorCode.FETCH_NOT_FOUND_ERROR;
|
||||
const message = 'Test error';
|
||||
const fetchError = new FetchError(errorCode, message);
|
||||
expect(fetchError.errorCode).toBe(errorCode);
|
||||
expect(fetchError.message).toBe(`(${errorCode}): ${message}`);
|
||||
})
|
||||
})
|
||||
|
||||
87
pyscriptjs/tests/unit/fetch.test.ts
Normal file
87
pyscriptjs/tests/unit/fetch.test.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { describe, it, expect, jest } from '@jest/globals'
|
||||
import { FetchError, ErrorCode } from "../../src/exceptions"
|
||||
import { robustFetch } from "../../src/fetch"
|
||||
import { Response } from 'node-fetch';
|
||||
|
||||
describe("robustFetch", () => {
|
||||
|
||||
it("should return a response object", async () => {
|
||||
global.fetch = jest.fn(() => (Promise.resolve(new Response(status="200", "Hello World"))));
|
||||
|
||||
const response = await robustFetch("https://pyscript.net");
|
||||
expect(response).toBeInstanceOf(Response);
|
||||
expect(response.status).toBe(200);
|
||||
})
|
||||
|
||||
it('receiving a 404 when fetching should throw FetchError with the right errorCode', async () => {
|
||||
global.fetch = jest.fn(() => (Promise.resolve(new Response("Not Found", {status: 404}))));
|
||||
|
||||
const url = "https://pyscript.net/non-existent-page"
|
||||
const expectedError = new FetchError(
|
||||
ErrorCode.FETCH_NOT_FOUND_ERROR,
|
||||
`Fetching from URL ${url} failed with error 404 (Not Found).`
|
||||
)
|
||||
|
||||
expect(() => robustFetch(url)).rejects.toThrow(expectedError);
|
||||
})
|
||||
|
||||
it('receiving a 404 when fetching should throw FetchError with the right errorCode', async () => {
|
||||
global.fetch = jest.fn(() => (Promise.resolve(new Response("Not Found", {status: 404}))));
|
||||
|
||||
const url = "https://pyscript.net/non-existent-page"
|
||||
const expectedError = new FetchError(
|
||||
ErrorCode.FETCH_NOT_FOUND_ERROR,
|
||||
`Fetching from URL ${url} failed with error 404 (Not Found).`
|
||||
)
|
||||
|
||||
expect(() => robustFetch(url)).rejects.toThrow(expectedError);
|
||||
})
|
||||
|
||||
it('receiving a 401 when fetching should throw FetchError with the right errorCode', async () => {
|
||||
global.fetch = jest.fn(() => (Promise.resolve(new Response("", {status: 401}))));
|
||||
|
||||
const url = "https://pyscript.net/protected-page"
|
||||
const expectedError = new FetchError(
|
||||
ErrorCode.FETCH_UNAUTHORIZED_ERROR,
|
||||
`Fetching from URL ${url} failed with error 401 (Unauthorized).`
|
||||
)
|
||||
|
||||
expect(() => robustFetch(url)).rejects.toThrow(expectedError);
|
||||
})
|
||||
|
||||
it('receiving a 403 when fetching should throw FetchError with the right errorCode', async () => {
|
||||
global.fetch = jest.fn(() => (Promise.resolve(new Response("", {status: 403}))));
|
||||
|
||||
const url = "https://pyscript.net/secret-page"
|
||||
const expectedError = new FetchError(
|
||||
ErrorCode.FETCH_FORBIDDEN_ERROR,
|
||||
`Fetching from URL ${url} failed with error 403 (Forbidden).`
|
||||
)
|
||||
|
||||
expect(() => robustFetch(url)).rejects.toThrow(expectedError);
|
||||
})
|
||||
|
||||
it('receiving a 500 when fetching should throw FetchError with the right errorCode', async () => {
|
||||
global.fetch = jest.fn(() => (Promise.resolve(new Response("Not Found", {status: 500}))));
|
||||
|
||||
const url = "https://pyscript.net/protected-page"
|
||||
const expectedError = new FetchError(
|
||||
ErrorCode.FETCH_SERVER_ERROR,
|
||||
`Fetching from URL ${url} failed with error 500 (Internal Server Error).`
|
||||
)
|
||||
|
||||
expect(() => robustFetch(url)).rejects.toThrow(expectedError);
|
||||
})
|
||||
|
||||
it('receiving a 503 when fetching should throw FetchError with the right errorCode', async () => {
|
||||
global.fetch = jest.fn(() => (Promise.resolve(new Response("Not Found", {status: 503}))));
|
||||
|
||||
const url = "https://pyscript.net/protected-page"
|
||||
const expectedError = new FetchError(
|
||||
ErrorCode.FETCH_UNAVAILABLE_ERROR,
|
||||
`Fetching from URL ${url} failed with error 503 (Service Unavailable).`
|
||||
)
|
||||
|
||||
expect(() => robustFetch(url)).rejects.toThrow(expectedError);
|
||||
})
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { jest } from "@jest/globals"
|
||||
import { UserError } from "../../src/exceptions"
|
||||
import { describe, it, beforeEach, expect } from "@jest/globals"
|
||||
import { UserError, ErrorCode } from "../../src/exceptions"
|
||||
import { PyScriptApp } from "../../src/main"
|
||||
|
||||
describe("Test withUserErrorHandler", () => {
|
||||
@@ -24,38 +24,38 @@ describe("Test withUserErrorHandler", () => {
|
||||
|
||||
it("userError doesn't stop execution", () => {
|
||||
function myRealMain() {
|
||||
throw new UserError("Computer says no");
|
||||
throw new UserError(ErrorCode.GENERIC, "Computer says no");
|
||||
}
|
||||
|
||||
const app = new MyApp(myRealMain);
|
||||
app.main();
|
||||
const banners = document.getElementsByClassName("alert-banner");
|
||||
expect(banners.length).toBe(1);
|
||||
expect(banners[0].innerHTML).toBe("Computer says no");
|
||||
expect(banners[0].innerHTML).toBe("(PY0000): Computer says no");
|
||||
});
|
||||
|
||||
it("userError escapes by default", () => {
|
||||
function myRealMain() {
|
||||
throw new UserError("hello <br>");
|
||||
throw new UserError(ErrorCode.GENERIC, "hello <br>");
|
||||
}
|
||||
|
||||
const app = new MyApp(myRealMain);
|
||||
app.main();
|
||||
const banners = document.getElementsByClassName("alert-banner");
|
||||
expect(banners.length).toBe(1);
|
||||
expect(banners[0].innerHTML).toBe("hello <br>");
|
||||
expect(banners[0].innerHTML).toBe("(PY0000): hello <br>");
|
||||
});
|
||||
|
||||
it("userError messageType=html don't escape", () => {
|
||||
function myRealMain() {
|
||||
throw new UserError("hello <br>", "html");
|
||||
throw new UserError(ErrorCode.GENERIC, "hello <br>", "html");
|
||||
}
|
||||
|
||||
const app = new MyApp(myRealMain);
|
||||
app.main();
|
||||
const banners = document.getElementsByClassName("alert-banner");
|
||||
expect(banners.length).toBe(1);
|
||||
expect(banners[0].innerHTML).toBe("hello <br>");
|
||||
expect(banners[0].innerHTML).toBe("(PY0000): hello <br>");
|
||||
});
|
||||
|
||||
it("any other exception should stop execution and raise", () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { jest } from '@jest/globals';
|
||||
import { jest, describe, it, expect, } from '@jest/globals';
|
||||
import { loadConfigFromElement, defaultConfig } from '../../src/pyconfig';
|
||||
import { version } from '../../src/runtime';
|
||||
import { UserError } from '../../src/exceptions'
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { expect } from "@jest/globals"
|
||||
import { ensureUniqueId, joinPaths } from "../../src/utils"
|
||||
|
||||
import { beforeEach, expect, describe, it } from "@jest/globals"
|
||||
import { ensureUniqueId, joinPaths} from "../../src/utils"
|
||||
|
||||
describe("Utils", () => {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user