Improve catching of errors when loading routes

- Handle ResponseParseErros, ResponseErrors and normal errors
  differently.
- Show them all in the same `LoadError` view
- Add a subtitle to give users some more information on what could have
  gone wrong.
- Add more properties to the copyable error message to make debugging
  easier.
This commit is contained in:
Sebastian Martinez
2024-02-15 13:26:11 -03:00
parent 8450f0cd9c
commit d216caec7d
24 changed files with 389 additions and 185 deletions
+3 -3
View File
@@ -24,7 +24,7 @@
import Session from "@app/views/session/Index.svelte";
import Source from "@app/views/projects/Source.svelte";
import LoadError from "@app/components/LoadError.svelte";
import Error from "@app/views/error/View.svelte";
import Loading from "@app/components/Loading.svelte";
const activeRouteStore = router.activeRouteStore;
@@ -87,8 +87,8 @@
<Patches {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "project.patch"}
<Patch {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "loadError"}
<LoadError {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "error"}
<Error {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "notFound"}
<NotFound {...$activeRouteStore.params} />
{:else}
+1 -1
View File
@@ -20,7 +20,7 @@
}
</style>
{#if $activeRouteStore.resource === "booting" || $activeRouteStore.resource === "home" || $activeRouteStore.resource === "session" || $activeRouteStore.resource === "loadError" || $activeRouteStore.resource === "notFound"}
{#if $activeRouteStore.resource === "booting" || $activeRouteStore.resource === "home" || $activeRouteStore.resource === "session" || $activeRouteStore.resource === "error" || $activeRouteStore.resource === "notFound"}
<!-- Don't render breadcrumbs for these routes. -->
{:else if $activeRouteStore.resource === "nodes"}
<div class="breadcrumbs">
+31 -9
View File
@@ -1,10 +1,20 @@
<script lang="ts">
import type {
ResponseError,
ResponseParseError,
} from "@httpd-client/lib/fetcher";
import type { ComponentProps } from "svelte";
import { config } from "@app/lib/config";
import Command from "./Command.svelte";
import ExternalLink from "./ExternalLink.svelte";
import Icon from "./Icon.svelte";
export let message: string;
export let error: any | undefined = undefined;
export let title: string;
export let description: string;
export let error: Error | ResponseParseError | ResponseError | undefined =
undefined;
export let icon: ComponentProps<Icon>["name"] = "alert";
</script>
<style>
@@ -19,6 +29,18 @@
border-radius: var(--border-radius-small);
gap: 1rem;
}
.label {
font-size: var(--font-size-small);
font-weight: var(--font-weight-regular);
max-width: 36rem;
}
.error :global(code) {
font-family: var(--font-family-monospace);
font-size: var(--font-size-small);
background-color: var(--color-fill-ghost);
border-radius: var(--border-radius-tiny);
padding: 0.125rem 0.25rem;
}
.help {
font-size: var(--font-size-small);
@@ -27,8 +49,12 @@
</style>
<div class="error">
<Icon name="alert" size="48" />
{message}
<Icon name={icon} size="48" />
<div class="txt-medium txt-bold">
{title}
</div>
<!-- This @html is secure since we don't allow user input -->
<div class="label">{@html description}</div>
{#if error}
<div class="help">
If you need help resolving this issue, copy the error message
@@ -40,11 +66,7 @@
</div>
<div style:max-width="25rem">
<Command
command={JSON.stringify({
message: error.message,
stack: error.stack,
...error,
})}
command={JSON.stringify(error, Object.getOwnPropertyNames(error))}
fullWidth
showPrompt={false} />
</div>
-58
View File
@@ -1,58 +0,0 @@
<script lang="ts">
import AppLayout from "@app/App/AppLayout.svelte";
import Command from "./Command.svelte";
import ExternalLink from "./ExternalLink.svelte";
import Icon from "./Icon.svelte";
export let title: string;
export let errorMessage: string;
export let stackTrace: string;
</script>
<style>
.wrapper {
gap: 1.5rem;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.container {
display: flex;
flex-direction: column;
text-align: center;
gap: 0.5rem;
}
.help {
font-size: var(--font-size-small);
}
</style>
<AppLayout>
<div class="wrapper">
<Icon name="desert" size="48" />
<div class="container">
<div class="txt-medium txt-bold">
{title}
</div>
<div class="help">
If you need help resolving this issue, copy the error message
<br class="global-hide-on-mobile" />
below and send it to us on
<ExternalLink href="https://radicle.zulipchat.com">
radicle.zulipchat.com
</ExternalLink>
</div>
</div>
<div style:max-width="25rem">
<Command
command={JSON.stringify({ errorMessage, stackTrace })}
fullWidth
showPrompt={false} />
</div>
</div>
</AppLayout>
+3 -3
View File
@@ -122,8 +122,8 @@ function setTitle(loadedRoute: LoadedRoute) {
if (loadedRoute.resource === "booting" || loadedRoute.resource === "home") {
title.push("Radicle");
} else if (loadedRoute.resource === "loadError") {
title.push("Load error");
} else if (loadedRoute.resource === "error") {
title.push("Error");
title.push("Radicle");
} else if (loadedRoute.resource === "notFound") {
title.push("Page not found");
@@ -259,7 +259,7 @@ export function routeToPath(route: Route): string {
} else if (
route.resource === "booting" ||
route.resource === "notFound" ||
route.resource === "loadError"
route.resource === "error"
) {
return "";
} else {
+10 -6
View File
@@ -1,4 +1,8 @@
import type { HomeRoute, HomeLoadedRoute } from "@app/views/home/router";
import type {
ResponseParseError,
ResponseError,
} from "@httpd-client/lib/fetcher";
import type {
ProjectLoadedRoute,
ProjectRoute,
@@ -29,19 +33,19 @@ interface SessionRoute {
};
}
export interface LoadErrorRoute {
resource: "loadError";
export interface ErrorRoute {
resource: "error";
params: {
title: string;
errorMessage: string;
stackTrace: string;
description: string;
error: Error | ResponseError | ResponseParseError;
};
}
export type Route =
| BootingRoute
| HomeRoute
| LoadErrorRoute
| ErrorRoute
| NotFoundRoute
| ProjectRoute
| NodesRoute
@@ -50,7 +54,7 @@ export type Route =
export type LoadedRoute =
| BootingRoute
| HomeLoadedRoute
| LoadErrorRoute
| ErrorRoute
| NotFoundRoute
| ProjectLoadedRoute
| NodesLoadedRoute
+5 -1
View File
@@ -1,4 +1,4 @@
import type { Comment } from "@httpd-client";
import type { BaseUrl, Comment } from "@httpd-client";
import md5 from "md5";
import bs58 from "bs58";
@@ -93,6 +93,10 @@ export function formatEditedCaption(lastEdit: Comment["edits"][0]) {
} edited ${formatTimestamp(lastEdit.timestamp / 1000)}`;
}
export function baseUrlToUrl(baseUrl: BaseUrl): URL {
return new URL(`${baseUrl.scheme}://${baseUrl.hostname}:${baseUrl.port}`);
}
// Generates a publicly shareable link.
export function formatPublicExplorer(
publicExplorer: string,
+38
View File
@@ -0,0 +1,38 @@
<script lang="ts">
import type { ComponentProps } from "svelte";
import AppLayout from "@app/App/AppLayout.svelte";
import ErrorMessage from "@app/components/ErrorMessage.svelte";
export let title: string;
export let description: string;
export let error: ComponentProps<ErrorMessage>["error"];
</script>
<style>
.wrapper {
padding: 4rem 0 2rem 0;
gap: 1.5rem;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 0.5rem;
}
</style>
<AppLayout>
<div class="wrapper">
<div class="container">
<ErrorMessage icon="desert" {title} {description} {error} />
</div>
</div>
</AppLayout>
+37 -33
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import type { ProjectWithListingData } from "@app/lib/projects";
import type { BaseUrl } from "@httpd-client";
import type { ProjectWithListingData } from "@app/lib/projects";
import type { ComponentProps } from "svelte";
import storedWritable from "@efstajas/svelte-stored-writable";
import { HttpdClient } from "@httpd-client";
@@ -9,15 +10,17 @@
import { api, httpdStore } from "@app/lib/httpd";
import { deduplicateStore } from "@app/lib/deduplicateStore";
import { baseUrlToUrl } from "@app/lib/utils";
import { getProjectsListingData } from "@app/lib/projects";
import { handleError } from "@app/views/home/error";
import { isDelegate } from "@app/lib/roles";
import { preferredSeeds } from "@app/lib/seeds";
import AppLayout from "@app/App/AppLayout.svelte";
import Button from "@app/components/Button.svelte";
import ConnectInstructions from "@app/components/ConnectInstructions.svelte";
import ProjectCard from "@app/components/ProjectCard.svelte";
import ErrorMessage from "@app/components/ErrorMessage.svelte";
import FilterButton from "./components/FilterButton.svelte";
import HomepageSection from "./components/HomepageSection.svelte";
import NewProjectButton from "./components/NewProjectButton.svelte";
@@ -36,8 +39,14 @@
"all",
);
let localProjects: ProjectWithListingData[] | "error" | undefined;
let preferredSeedProjects: ProjectWithListingData[] | "error" | undefined;
let localProjects:
| ProjectWithListingData[]
| ComponentProps<ErrorMessage>["error"]
| undefined;
let preferredSeedProjects:
| ProjectWithListingData[]
| ComponentProps<ErrorMessage>["error"]
| undefined;
async function fetchProjects(baseUrl: BaseUrl, show: "all" | "pinned") {
const api = new HttpdClient(baseUrl);
@@ -52,14 +61,10 @@
return await getProjectsListingData(projects);
}
function handleProjectLoadError(): "error" {
return "error";
}
async function loadLocalProjects() {
localProjects = undefined;
localProjects = await fetchProjects(api.baseUrl, "all").catch(
handleProjectLoadError,
error => error,
);
}
@@ -68,12 +73,12 @@
if (!$selectedSeed) return;
preferredSeedProjects = await fetchProjects($selectedSeed, "pinned").catch(
handleProjectLoadError,
error => error,
);
}
function isSeeding(projectId: string) {
if (localProjects === "error") return false;
if (localProjects instanceof Error) return false;
return localProjects?.some(p => p.project.id === projectId) ?? false;
}
@@ -82,7 +87,7 @@
$: $selectedSeed && void loadPreferredSeedProjects();
$: filteredLocalProjects =
$localProjectsFilter === "all" ||
localProjects === "error" ||
localProjects instanceof Error ||
localProjects === undefined
? localProjects
: localProjects.filter(p => isDelegate(nodeId, p.project.delegates));
@@ -114,9 +119,6 @@
font-size: var(--font-size-small);
font-weight: var(--font-weight-regular);
}
.empty-state .action {
margin-top: 0.5rem;
}
.project-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
@@ -140,11 +142,12 @@
<div class="global-hide-on-mobile">
<HomepageSection
loading={$httpdStore.state !== "stopped" && localProjects === undefined}
empty={localProjects === "error" ||
$httpdStore.state === "stopped" ||
!filteredLocalProjects?.length}
empty={$httpdStore.state === "stopped" ||
(filteredLocalProjects instanceof Array &&
!filteredLocalProjects.length) ||
localProjects instanceof Error}
title="Local projects"
subtitle="Projects you’re seeding with your local node">
subtitle="Projects you're seeding with your local node">
<svelte:fragment slot="actions">
<FilterButton disabled={!nodeId} bind:value={$localProjectsFilter} />
<NewProjectButton disabled={!nodeId} />
@@ -155,12 +158,12 @@
<div style="text-align: left; width: 100%;">
<ConnectInstructions />
</div>
{:else if localProjects === "error"}
<div class="heading">Error loading projects</div>
<div class="label">
There was an error loading projects from your local node.
</div>
<div class="action"><Button>Learn more</Button></div>
{:else if localProjects instanceof Error}
<ErrorMessage
{...handleError(
localProjects,
baseUrlToUrl(api.baseUrl).toString(),
)} />
{:else if !localProjects?.length}
<div class="heading">No local projects</div>
<div class="label">
@@ -175,7 +178,7 @@
</div>
</svelte:fragment>
<div class="project-grid">
{#if filteredLocalProjects && filteredLocalProjects !== "error"}
{#if filteredLocalProjects && !(filteredLocalProjects instanceof Error)}
{#each filteredLocalProjects as { project, baseUrl, activity, lastCommit }}
<ProjectCard
id={project.id}
@@ -197,7 +200,7 @@
<HomepageSection
loading={preferredSeedProjects === undefined}
empty={preferredSeedProjects === "error" ||
empty={preferredSeedProjects instanceof Error ||
preferredSeedProjects?.length === 0}
title="Explore"
subtitle="Pinned projects on your selected seed node">
@@ -212,11 +215,12 @@
</svelte:fragment>
<svelte:fragment slot="empty">
<div class="empty-state">
{#if preferredSeedProjects === "error"}
<div class="heading">Something went wrong</div>
<div class="label">
There was an error loading projects from your preferred seed node.
</div>
{#if preferredSeedProjects instanceof Error}
<ErrorMessage
{...handleError(
preferredSeedProjects,
baseUrlToUrl(api.baseUrl).toString(),
)} />
{:else}
<div class="heading">Nothing to see here</div>
<div class="label">
@@ -226,7 +230,7 @@
</div>
</svelte:fragment>
<div class="project-grid">
{#if preferredSeedProjects && preferredSeedProjects !== "error"}
{#if preferredSeedProjects && !(preferredSeedProjects instanceof Error)}
{#each preferredSeedProjects as { project, baseUrl, activity, lastCommit }}
<ProjectCard
id={project.id}
+41
View File
@@ -0,0 +1,41 @@
import { ResponseParseError, ResponseError } from "@httpd-client/lib/fetcher";
export function handleError(
error: Error | ResponseParseError | ResponseError,
url: string,
): {
error: Error | ResponseParseError | ResponseError;
title: string;
description: string;
} {
if (error instanceof ResponseParseError) {
return {
error,
title: "Could not parse the request",
description:
"The response received from the seed does not match the expected schema, this is usually due to a version mismatch between the seed and the web interface.",
};
} else if (error instanceof ResponseError) {
return {
error,
title: "Could not load the projects",
description: `You're trying to fetch projects from a node that is not reachable, make sure the address <a href="${url}">${url}</a> is correct and the right ports are exposed if its your node.`,
};
} else if (
error instanceof TypeError &&
error.message === "Failed to fetch"
) {
return {
error,
title: "Resource not found",
description: `You're trying to fetch a resource that is not available. Check that the ids is correct, and try to run <code>$ rad sync</code>.`,
};
} else {
return {
error,
title: "Could not load this view",
description:
"You stumbled on an unknown error, we aren't exactly sure what happened.",
};
}
}
+4 -4
View File
@@ -1,5 +1,7 @@
import type { LoadErrorRoute } from "@app/lib/router/definitions";
import type { ErrorRoute } from "@app/lib/router/definitions";
import * as seeds from "@app/lib/seeds";
export interface HomeRoute {
resource: "home";
}
@@ -9,9 +11,7 @@ export interface HomeLoadedRoute {
params: Record<string, never>;
}
export async function loadHomeRoute(): Promise<
HomeLoadedRoute | LoadErrorRoute
> {
export async function loadHomeRoute(): Promise<HomeLoadedRoute | ErrorRoute> {
seeds.initialize();
await seeds.waitForLoad();
+2 -1
View File
@@ -178,7 +178,8 @@
{#if error}
<ErrorMessage
message="Not able to load more projects from this node"
title="Not able to load more projects from this node"
description="You either loaded all remaining projects, or there was a network issue with this seed"
{error} />
{/if}
</div>
+46
View File
@@ -0,0 +1,46 @@
import type { ErrorRoute, NotFoundRoute } from "@app/lib/router/definitions";
import { ResponseParseError, ResponseError } from "@httpd-client/lib/fetcher";
export function handleError(
error: Error | ResponseParseError | ResponseError,
url: string,
): NotFoundRoute | ErrorRoute {
if (error instanceof ResponseParseError) {
return {
resource: "error",
params: {
error,
title: "Could not parse the request",
description:
"The response received from the seed does not match the expected schema, this is usually due to a version mismatch between the seed and the web interface.",
},
};
} else if (error instanceof ResponseError) {
return {
resource: "error",
params: {
error,
title: "Could not load this node",
description: `You're trying to access a node that is not reachable, make sure the address <a href="${url}">${url}</a> is correct and the right ports are exposed if its your node.`,
},
};
} else if (
error instanceof TypeError &&
error.message === "Failed to fetch"
) {
return {
resource: "notFound",
params: { title: "Node not found" },
};
} else {
return {
resource: "error",
params: {
error,
title: "Could not load this node",
description:
"You stumbled on an unknown error, we aren't exactly sure what happened.",
},
};
}
}
+5 -22
View File
@@ -1,13 +1,12 @@
import type { BaseUrl } from "@httpd-client";
import type {
LoadErrorRoute,
NotFoundRoute,
} from "@app/lib/router/definitions";
import type { ErrorRoute, NotFoundRoute } from "@app/lib/router/definitions";
import type { ProjectWithListingData } from "@app/lib/projects";
import { HttpdClient } from "@httpd-client";
import { config } from "@app/lib/config";
import { baseUrlToUrl } from "@app/lib/utils";
import { getProjectsListingData } from "@app/lib/projects";
import { handleError } from "@app/views/nodes/error";
export interface NodesRouteParams {
baseUrl: BaseUrl;
@@ -70,7 +69,7 @@ export function nodePath(baseUrl: BaseUrl) {
export async function loadNodeRoute(
params: NodesRouteParams,
): Promise<NodesLoadedRoute | NotFoundRoute | LoadErrorRoute> {
): Promise<NodesLoadedRoute | NotFoundRoute | ErrorRoute> {
const api = new HttpdClient(params.baseUrl);
try {
const projectPageIndex = 0;
@@ -91,22 +90,6 @@ export async function loadNodeRoute(
},
};
} catch (error: any) {
if (error.message === "Failed to fetch") {
return {
resource: "notFound",
params: {
title: "Node not found",
},
};
} else {
return {
resource: "loadError",
params: {
title: "Not able to load this node",
errorMessage: error.message,
stackTrace: error.stackTrace,
},
};
}
return handleError(error, baseUrlToUrl(api.baseUrl).toString());
}
}
+4 -1
View File
@@ -493,7 +493,10 @@
class="diff-error txt-monospace txt-small"
style:border-radius="var(--border-radius-small">
<ErrorMessage
message="Failed to load diff for this revision"
title="Failed to load diff for this revision"
description="Make sure you are able to connect to the seed <code>${utils
.baseUrlToUrl(api.baseUrl)
.toString()}</code>"
{error} />
</div>
{/if}
+9 -3
View File
@@ -8,9 +8,10 @@
} from "@httpd-client";
import type { Route } from "@app/lib/router";
import { HttpdClient } from "@httpd-client";
import { groupCommits } from "@app/lib/commit";
import { COMMITS_PER_PAGE } from "./router";
import { HttpdClient } from "@httpd-client";
import { baseUrlToUrl } from "@app/lib/utils";
import { groupCommits } from "@app/lib/commit";
import Button from "@app/components/Button.svelte";
import CommitTeaser from "./Commit/CommitTeaser.svelte";
@@ -157,7 +158,12 @@
{#if error}
<div class="message">
<ErrorMessage message="Couldn't load commits" {error} />
<ErrorMessage
title="Couldn't load commits"
description="Make sure you are able to connect to the seed <code>${baseUrlToUrl(
api.baseUrl,
).toString()}</code>"
{error} />
</div>
{/if}
</Layout>
+2 -1
View File
@@ -127,6 +127,7 @@
</div>
{:else}
<ErrorMessage
message="Couldn't access issue creation. Make sure you're authenticated." />
title="Not able to create a new issue"
description="Couldn't access issue creation. Make sure you're authenticated." />
{/if}
</Layout>
+7 -2
View File
@@ -5,7 +5,7 @@
import { ISSUES_PER_PAGE } from "./router";
import { closeFocused } from "@app/components/Popover.svelte";
import { httpdStore } from "@app/lib/httpd";
import { isLocal } from "@app/lib/utils";
import { baseUrlToUrl, isLocal } from "@app/lib/utils";
import capitalize from "lodash/capitalize";
import Button from "@app/components/Button.svelte";
@@ -174,7 +174,12 @@
</List>
{#if error}
<ErrorMessage message="Couldn't load issues" {error} />
<ErrorMessage
title="Couldn't load issues"
description="Please make sure you are able to connect to the seed <code>${baseUrlToUrl(
api.baseUrl,
).toString()}</code>"
{error} />
{/if}
{#if project.issues[state] === 0}
+7 -2
View File
@@ -6,7 +6,7 @@
import { PATCHES_PER_PAGE } from "./router";
import { httpdStore } from "@app/lib/httpd";
import { isLocal } from "@app/lib/utils";
import { baseUrlToUrl, isLocal } from "@app/lib/utils";
import Button from "@app/components/Button.svelte";
import DropdownList from "@app/components/DropdownList.svelte";
@@ -193,7 +193,12 @@
</List>
{#if error}
<ErrorMessage message="Couldn't load patches" {error} />
<ErrorMessage
title="Couldn't load patches"
description="Please make sure you are able to connect to the seed <code>${baseUrlToUrl(
api.baseUrl,
).toString()}</code>"
{error} />
{/if}
{#if project.patches[state] === 0}
+59
View File
@@ -0,0 +1,59 @@
import type { ErrorRoute, NotFoundRoute } from "@app/lib/router/definitions";
import type { ProjectRoute } from "@app/views/projects/router";
import { baseUrlToUrl } from "@app/lib/utils";
import { ResponseParseError, ResponseError } from "@httpd-client/lib/fetcher";
export function handleError(
error: Error | ResponseParseError | ResponseError,
route: ProjectRoute,
): NotFoundRoute | ErrorRoute {
const url = baseUrlToUrl(route.node);
if (error instanceof ResponseError && error.status === 404) {
let subject;
if (route.resource === "project.commit") {
subject = "Commit";
} else if (route.resource === "project.issue") {
subject = "Issue";
} else if (route.resource === "project.patch") {
subject = "Patch";
} else {
subject = "Project";
}
return {
resource: "notFound",
params: { title: `${subject} not found` },
};
} else if (error instanceof ResponseError) {
return {
resource: "error",
params: {
error,
title: "Could not load this project",
description: `Make sure you are able to connect to the seed <a href="${url}">${url}</a>.`,
},
};
} else if (error instanceof ResponseParseError) {
return {
resource: "error",
params: {
error,
title: "Could not parse the request",
description:
"The response received from the seed does not match the expected schema, this is usually due to a version mismatch between the seed and the web interface.",
},
};
} else {
return {
resource: "error",
params: {
error,
title: "Could not load this project",
description:
"You stumbled on an unknown error, we aren't exactly sure what happened.",
},
};
}
}
+5 -35
View File
@@ -1,7 +1,4 @@
import type {
LoadErrorRoute,
NotFoundRoute,
} from "@app/lib/router/definitions";
import type { ErrorRoute, NotFoundRoute } from "@app/lib/router/definitions";
import type {
BaseUrl,
Blob,
@@ -21,9 +18,10 @@ import type {
import * as Syntax from "@app/lib/syntax";
import * as httpd from "@app/lib/httpd";
import { config } from "@app/lib/config";
import { HttpdClient } from "@httpd-client";
import { ResponseError } from "@httpd-client/lib/fetcher";
import { config } from "@app/lib/config";
import { handleError } from "@app/views/projects/error";
import { nodePath } from "@app/views/nodes/router";
import { unreachable } from "@app/lib/utils";
@@ -271,7 +269,7 @@ async function isLocalNodeSeeding(route: ProjectRoute): Promise<boolean> {
export async function loadProjectRoute(
route: ProjectRoute,
): Promise<ProjectLoadedRoute | LoadErrorRoute | NotFoundRoute> {
): Promise<ProjectLoadedRoute | ErrorRoute | NotFoundRoute> {
const api = new HttpdClient(route.node);
const rawPath = (commit?: string) =>
`${route.node.scheme}://${route.node.hostname}:${route.node.port}/raw/${
@@ -341,35 +339,7 @@ export async function loadProjectRoute(
return unreachable(route);
}
} catch (error: any) {
if (error?.status === 404) {
let subject;
if (route.resource === "project.commit") {
subject = "Commit";
} else if (route.resource === "project.issue") {
subject = "Issue";
} else if (route.resource === "project.patch") {
subject = "Patch";
} else {
subject = "Project";
}
return {
resource: "notFound",
params: {
title: `${subject} not found`,
},
};
} else {
return {
resource: "loadError",
params: {
title: "Could not load this project",
errorMessage: error.message,
stackTrace: error.stack,
},
};
}
return handleError(error, route);
}
}
+23
View File
@@ -40,3 +40,26 @@ test("load error", async ({ page }) => {
await page.goto("/", { waitUntil: "networkidle" });
await expect(page).toHaveScreenshot();
});
test("response parse error", async ({ page }) => {
await page.addInitScript(appConfigWithFixture);
await page.route("*/**/v1/projects*", route => {
return route.fulfill({
json: [{ name: 1337 }],
});
});
await page.goto("/", { waitUntil: "networkidle" });
await expect(page).toHaveScreenshot();
});
test("response error", async ({ page }) => {
await page.addInitScript(appConfigWithFixture);
await page.route("*/**/v1/projects*", route => {
return route.fulfill({
status: 500,
body: "There is an error in the response",
});
});
await page.goto("/", { waitUntil: "networkidle" });
await expect(page).toHaveScreenshot();
});
+26
View File
@@ -37,3 +37,29 @@ test("node not found", async ({ page }) => {
});
await expect(page).toHaveScreenshot();
});
test("response parse error", async ({ page }) => {
await page.route("*/**/v1/projects*", route => {
return route.fulfill({
json: [{ name: 1337 }],
});
});
await page.goto("/nodes/radicle.local", {
waitUntil: "networkidle",
});
await expect(page).toHaveScreenshot();
});
test("response error", async ({ page }) => {
await page.route("*/**/v1/projects*", route => {
return route.fulfill({
status: 500,
});
});
await page.goto("/nodes/radicle.local", {
waitUntil: "networkidle",
});
await expect(page).toHaveScreenshot();
});
+21
View File
@@ -5,6 +5,7 @@ import {
sourceBrowsingUrl,
aliceRemote,
markdownUrl,
sourceBrowsingRid,
} from "@tests/support/fixtures.js";
test("source page", async ({ page }) => {
@@ -99,6 +100,26 @@ test("project not found", async ({ page }) => {
await expect(page).toHaveScreenshot();
});
test("response parse error", async ({ page }) => {
await page.route(`*/**/v1/projects/${sourceBrowsingRid}`, route => {
return route.fulfill({
json: [{ name: 1337 }],
});
});
await page.goto(sourceBrowsingUrl, { waitUntil: "networkidle" });
await expect(page).toHaveScreenshot();
});
test("response error", async ({ page }) => {
await page.route(`*/**/v1/projects/${sourceBrowsingRid}`, route => {
return route.fulfill({
status: 500,
});
});
await page.goto(sourceBrowsingUrl, { waitUntil: "networkidle" });
await expect(page).toHaveScreenshot();
});
test("readme not found", async ({ page }) => {
await page.goto(`${markdownUrl}/tree`, {
waitUntil: "networkidle",