-
+
{baseUrl.hostname}
@@ -199,27 +203,29 @@
- {
- if (some($bookmarkedSeeds, baseUrl)) {
- removeBookmark(baseUrl);
- } else {
- addBookmark(baseUrl);
- }
- }}>
- {#if some($bookmarkedSeeds, baseUrl) || some(config.preferredSeeds, baseUrl)}
-
- {:else}
-
- {/if}
-
+ {#if !compact}
+ {
+ if (some($bookmarkedSeeds, baseUrl)) {
+ removeBookmark(baseUrl);
+ } else {
+ addBookmark(baseUrl);
+ }
+ }}>
+ {#if some($bookmarkedSeeds, baseUrl) || some(config.preferredSeeds, baseUrl)}
+
+ {:else}
+
+ {/if}
+
+ {/if}
diff --git a/src/views/nodes/error.ts b/src/views/nodes/error.ts
index 0eddc7ecb..22aaefb1b 100644
--- a/src/views/nodes/error.ts
+++ b/src/views/nodes/error.ts
@@ -1,11 +1,15 @@
+import type { BaseUrl } from "@http-client";
import type { ErrorRoute, NotFoundRoute } from "@app/lib/router/definitions";
import { ResponseParseError, ResponseError } from "@http-client/lib/fetcher";
+import { baseUrlToString } from "@app/lib/utils";
export function handleError(
error: Error | ResponseParseError | ResponseError,
- url: string,
+ baseUrl: BaseUrl,
): NotFoundRoute | ErrorRoute {
+ const url = baseUrlToString(baseUrl);
+
if (error instanceof ResponseParseError) {
return {
resource: "error",
@@ -30,7 +34,12 @@ export function handleError(
) {
return {
resource: "notFound",
- params: { title: "Node not found" },
+ params: {
+ title: "Could not connect to",
+ description:
+ "The node may be offline or the address may be incorrect.\nSelect a different node to continue.",
+ baseUrl,
+ },
};
} else {
return {
diff --git a/src/views/nodes/router.ts b/src/views/nodes/router.ts
index 74cb74d0f..4c4b94b64 100644
--- a/src/views/nodes/router.ts
+++ b/src/views/nodes/router.ts
@@ -4,7 +4,6 @@ import type { ErrorRoute, NotFoundRoute } from "@app/lib/router/definitions";
import config from "@app/lib/config";
import { HttpdClient } from "@http-client";
import { ResponseError, ResponseParseError } from "@http-client/lib/fetcher";
-import { baseUrlToString } from "@app/lib/utils";
import { handleError } from "@app/views/nodes/error";
import { unreachableError } from "@app/views/repos/error";
import { determineSeed } from "./SeedSelector";
@@ -70,7 +69,7 @@ export async function loadNodeRoute(
error instanceof ResponseError ||
error instanceof ResponseParseError
) {
- return handleError(error, baseUrlToString(api.baseUrl));
+ return handleError(error, baseUrl);
} else {
return unreachableError();
}
diff --git a/src/views/repos/error.ts b/src/views/repos/error.ts
index f14239848..2db786755 100644
--- a/src/views/repos/error.ts
+++ b/src/views/repos/error.ts
@@ -44,6 +44,19 @@ export function handleError(
description: error.description,
},
};
+ } else if (
+ error instanceof TypeError &&
+ error.message === "Failed to fetch"
+ ) {
+ return {
+ resource: "notFound",
+ params: {
+ title: "Could not connect to",
+ description:
+ "The node may be offline or the address may be incorrect.\nSelect a different node to continue.",
+ baseUrl: route.node,
+ },
+ };
} else {
return {
resource: "error",
diff --git a/src/views/users/UserReposView.svelte b/src/views/users/UserReposView.svelte
index 7f006ec0c..7b4808b73 100644
--- a/src/views/users/UserReposView.svelte
+++ b/src/views/users/UserReposView.svelte
@@ -77,5 +77,5 @@
{/if}
{:catch error}
- {router.push(handleError(error, utils.baseUrlToString(baseUrl)))}
+ {router.push(handleError(error, baseUrl))}
{/await}
diff --git a/src/views/users/router.ts b/src/views/users/router.ts
index f9fdfda66..87e75a8cb 100644
--- a/src/views/users/router.ts
+++ b/src/views/users/router.ts
@@ -67,7 +67,7 @@ export async function loadUserRoute({
error instanceof ResponseError ||
error instanceof ResponseParseError
) {
- return handleError(error, utils.baseUrlToString(api.baseUrl));
+ return handleError(error, api.baseUrl);
} else {
return unreachableError();
}
diff --git a/tests/build/smoke.spec.ts b/tests/build/smoke.spec.ts
index b1216c5a8..89231e542 100644
--- a/tests/build/smoke.spec.ts
+++ b/tests/build/smoke.spec.ts
@@ -5,5 +5,5 @@ test("exceptions in production build", async ({ page }) => {
// Wait for scripts to finish executing, there might be exceptions that
// happen after the page has been painted.
await page.waitForTimeout(2000);
- await expect(page.getByText("Node not found")).toBeVisible();
+ await expect(page.getByText("Could not connect to")).toBeVisible();
});
diff --git a/tests/e2e/node.spec.ts b/tests/e2e/node.spec.ts
index 1bbe4dbcd..e238fada4 100644
--- a/tests/e2e/node.spec.ts
+++ b/tests/e2e/node.spec.ts
@@ -2,6 +2,7 @@ import {
defaultConfig,
expect,
shortNodeRemote,
+ sourceBrowsingRid,
test,
} from "@tests/support/fixtures.js";
@@ -51,6 +52,71 @@ test("show pinned repositories", async ({ page }) => {
).toBeVisible();
});
+test("unreachable node shows error with seed selector", async ({ page }) => {
+ await page.goto("/nodes/this.node.does.not.exist.xyz", {
+ waitUntil: "networkidle",
+ });
+
+ // Shows the error title with the unreachable hostname.
+ await expect(page.getByText("Could not connect to")).toBeVisible();
+ await expect(page.getByText("this.node.does.not.exist.xyz")).toBeVisible();
+
+ // Shows the description.
+ await expect(
+ page.getByText("The node may be offline or the address may be incorrect."),
+ ).toBeVisible();
+ await expect(
+ page.getByText("Select a different node to continue."),
+ ).toBeVisible();
+
+ // Shows the seed selector dropdown toggle.
+ await expect(
+ page.getByRole("button", { name: "Toggle seed selector dropdown" }),
+ ).toBeVisible();
+
+ // Bookmark button is not shown in compact mode.
+ await expect(
+ page.getByRole("button", { name: "Add bookmark" }),
+ ).not.toBeVisible();
+});
+
+test("seed selector on not-found page allows navigating to a working node", async ({
+ page,
+}) => {
+ await page.goto("/nodes/this.node.does.not.exist.xyz", {
+ waitUntil: "networkidle",
+ });
+
+ await expect(page.getByText("Could not connect to")).toBeVisible();
+
+ // Open the seed selector and navigate to the working local node.
+ await page
+ .getByRole("button", { name: "Toggle seed selector dropdown" })
+ .click();
+ await page.getByPlaceholder("seed.radicle.example").fill("localhost");
+ await page.getByPlaceholder("seed.radicle.example").press("Enter");
+
+ // Should navigate to the working node.
+ await expect(page).toHaveURL("/nodes/localhost");
+ await expect(page.getByText("source-browsing")).toBeVisible();
+});
+
+test("unreachable seed on repo page shows error with seed selector", async ({
+ page,
+}) => {
+ await page.goto(`/nodes/this.node.does.not.exist.xyz/${sourceBrowsingRid}`, {
+ waitUntil: "networkidle",
+ });
+
+ await expect(page.getByText("Could not connect to")).toBeVisible();
+ await expect(page.getByText("this.node.does.not.exist.xyz")).toBeVisible();
+
+ // Seed selector is available to navigate away.
+ await expect(
+ page.getByRole("button", { name: "Toggle seed selector dropdown" }),
+ ).toBeVisible();
+});
+
test("edit seed bookmarks", async ({ page }) => {
// Proxy requests to seed.example.tld to the local test api.
await page.route(