diff --git a/src/lib/router/definitions.ts b/src/lib/router/definitions.ts index 0c9f32b8f..baad2848d 100644 --- a/src/lib/router/definitions.ts +++ b/src/lib/router/definitions.ts @@ -1,3 +1,4 @@ +import type { BaseUrl } from "@http-client"; import type { ResponseError, ResponseParseError, @@ -18,7 +19,7 @@ interface BootingRoute { export interface NotFoundRoute { resource: "notFound"; - params: { title: string }; + params: { title: string; description?: string; baseUrl?: BaseUrl }; } export type ErrorParam = Error | ResponseParseError | ResponseError | undefined; diff --git a/src/views/NotFound.svelte b/src/views/NotFound.svelte index e9196c065..73560ecb9 100644 --- a/src/views/NotFound.svelte +++ b/src/views/NotFound.svelte @@ -1,8 +1,13 @@
-
{title}
+
+ {title} + {#if baseUrl} + + {/if} +
+ {#if description} +
+ {#each description.split("\n") as line} +
{line}
+ {/each} +
+ {/if}
diff --git a/src/views/nodes/ReposView.svelte b/src/views/nodes/ReposView.svelte index dc335dbf7..c28e9e2b5 100644 --- a/src/views/nodes/ReposView.svelte +++ b/src/views/nodes/ReposView.svelte @@ -2,7 +2,6 @@ import type { BaseUrl, NodeStats } from "@http-client"; import * as router from "@app/lib/router"; - import { baseUrlToString } from "@app/lib/utils"; import { fetchRepoInfos } from "@app/components/RepoCard"; import { handleError } from "@app/views/nodes/error"; @@ -196,6 +195,6 @@ {/if} {:catch error} - {router.push(handleError(error, baseUrlToString(baseUrl)))} + {router.push(handleError(error, baseUrl))} {/await} diff --git a/src/views/nodes/SeedSelector.svelte b/src/views/nodes/SeedSelector.svelte index a723e7905..d4666889f 100644 --- a/src/views/nodes/SeedSelector.svelte +++ b/src/views/nodes/SeedSelector.svelte @@ -23,6 +23,7 @@ import TextInput from "@app/components/TextInput.svelte"; export let baseUrl: BaseUrl; + export let compact: boolean = false; let expanded: boolean = false; let loading = false; @@ -108,14 +109,17 @@ } -
+
-
+
{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(