Skip to content

Commit 56b488d

Browse files
committed
Add tests for explore and search fallback
1 parent 46ffb2b commit 56b488d

4 files changed

Lines changed: 202 additions & 51 deletions

File tree

tests/e2e/explore.spec.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { expect, test } from "@tests/support/fixtures.js";
2+
3+
const unreachableSeed = {
4+
hostname: "this.node.does.not.exist.xyz",
5+
port: 8081,
6+
scheme: "http",
7+
};
8+
9+
test("explore repos page lists all repos and hides sort without a search backend", async ({
10+
page,
11+
}) => {
12+
await page.goto("/explore/repos");
13+
14+
// Breadcrumb and subtitle. Without a search backend the listing is forced
15+
// to "All repos" (rid order).
16+
await expect(page.getByRole("link", { name: "Explore" })).toBeVisible();
17+
await expect(page.getByText("All repos")).toBeVisible();
18+
19+
// The repo grid renders the seeded repos.
20+
await expect(
21+
page.locator(".repo-card", { hasText: "source-browsing" }).first(),
22+
).toBeVisible();
23+
24+
// The sort control is only offered when the seed exposes a search backend.
25+
await expect(page.getByRole("button", { name: "Sort" })).toHaveCount(0);
26+
27+
// The breadcrumb navigates back to the explore landing page.
28+
await page.getByRole("link", { name: "Explore" }).click();
29+
await expect(page).toHaveURL("/");
30+
});
31+
32+
test("explore falls back to a healthy seed when the primary is unreachable", async ({
33+
page,
34+
}) => {
35+
// Make an unreachable seed the primary, then fail its requests instantly.
36+
await page.addInitScript(seed => {
37+
window.localStorage.setItem("selectedSeed", JSON.stringify(seed));
38+
}, unreachableSeed);
39+
await page.route(
40+
url => url.hostname === unreachableSeed.hostname,
41+
route => route.abort(),
42+
);
43+
44+
await page.goto("/", { waitUntil: "networkidle" });
45+
46+
// Failover lands on the reachable preferred seed and the page renders.
47+
await expect(page.getByText("source-browsing").first()).toBeVisible();
48+
await expect(
49+
page.getByRole("button", { name: "Seed selector" }),
50+
).toContainText("localhost");
51+
});
52+
53+
test("explore shows an error when no seed can be reached", async ({ page }) => {
54+
// Fail every request to the seed's httpd port so no candidate responds.
55+
await page.route(
56+
url => url.port === String(unreachableSeed.port),
57+
route => route.abort(),
58+
);
59+
60+
await page.goto("/", { waitUntil: "networkidle" });
61+
62+
await expect(page.getByText("Unable to reach any seed")).toBeVisible();
63+
});

tests/e2e/node.spec.ts

Lines changed: 27 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,10 @@ test("unreachable node shows error with seed selector", async ({ page }) => {
6969
page.getByText("Select a different node to continue."),
7070
).toBeVisible();
7171

72-
// Shows the seed selector dropdown toggle.
72+
// Shows the seed selector toggle.
7373
await expect(
74-
page.getByRole("button", { name: "Toggle seed selector dropdown" }),
74+
page.getByRole("button", { name: "Seed selector" }),
7575
).toBeVisible();
76-
77-
// Bookmark button is not shown in compact mode.
78-
await expect(
79-
page.getByRole("button", { name: "Add bookmark" }),
80-
).not.toBeVisible();
8176
});
8277

8378
test("seed selector on not-found page allows navigating to a working node", async ({
@@ -90,10 +85,9 @@ test("seed selector on not-found page allows navigating to a working node", asyn
9085
await expect(page.getByText("Could not connect to")).toBeVisible();
9186

9287
// Open the seed selector and navigate to the working local node.
93-
await page
94-
.getByRole("button", { name: "Toggle seed selector dropdown" })
95-
.click();
96-
await page.getByPlaceholder("seed.radicle.example").fill("localhost");
88+
await page.getByRole("button", { name: "Seed selector" }).click();
89+
await page.getByRole("button", { name: "Add new" }).click();
90+
await page.getByPlaceholder("seed.radicle.example").fill("localhost:8081");
9791
await page.getByPlaceholder("seed.radicle.example").press("Enter");
9892

9993
// Should navigate to the working node.
@@ -113,7 +107,7 @@ test("unreachable seed on repo page shows error with seed selector", async ({
113107

114108
// Seed selector is available to navigate away.
115109
await expect(
116-
page.getByRole("button", { name: "Toggle seed selector dropdown" }),
110+
page.getByRole("button", { name: "Seed selector" }),
117111
).toBeVisible();
118112
});
119113

@@ -135,53 +129,35 @@ test("edit seed bookmarks", async ({ page }) => {
135129

136130
await page.goto("/");
137131

138-
await page
139-
.getByRole("button", { name: "Toggle seed selector dropdown" })
140-
.click();
141-
await expect(page.getByPlaceholder("seed.radicle.example")).toHaveValue(
142-
"localhost",
143-
);
144-
await expect(
145-
page.getByRole("button", { name: "Default seeds can't be removed" }),
146-
).toBeVisible();
147-
await expect(page.locator(".dropdown > .dropdown-item")).toHaveCount(1);
132+
const seedSelector = page.getByRole("button", { name: "Seed selector" });
148133

149-
// The input box is focussed, has the text selected and ready to be overwritten.
134+
// Add a custom seed via the seed selector.
135+
await seedSelector.click();
136+
await page.getByRole("button", { name: "Add new" }).click();
150137
await page.getByPlaceholder("seed.radicle.example").fill("seed.example.tld");
151138
await page.getByPlaceholder("seed.radicle.example").press("Enter");
152139

153-
await expect(page).toHaveURL("/nodes/seed.example.tld");
140+
// On the explore page the URL is unchanged, but the active seed switches to
141+
// the new custom seed, which is added to the bookmarked custom seeds.
142+
await expect(seedSelector).toContainText("seed.example.tld");
143+
144+
await seedSelector.click();
154145
await expect(
155-
page.getByRole("button", { name: "Add bookmark" }),
146+
page.getByRole("button", { name: "Remove bookmark", exact: true }),
156147
).toBeVisible();
157148

158-
await page
159-
.getByRole("button", { name: "Toggle seed selector dropdown" })
160-
.click();
161-
162-
// After navigating to the seed it should not yet be added to the bookmarks.
163-
await expect(page.locator(".dropdown > .dropdown-item")).toHaveCount(1);
164-
165-
await page.getByRole("button", { name: "Add bookmark" }).click();
166-
await expect(page.locator(".dropdown > .dropdown-item")).toHaveCount(2);
167-
168-
// Test that new seed is persisted and opened when we go to the landing page.
169-
await page.getByRole("link", { name: "Home" }).click();
170-
await expect(page.getByText("seed.example.tld").first()).toBeVisible();
171-
172-
// Test removing a bookmark.
173-
await page
174-
.getByRole("button", { name: "Toggle seed selector dropdown" })
175-
.click();
176-
await page.getByRole("button", { name: "Remove bookmark" }).nth(1).click();
177-
await expect(page.locator(".dropdown > .dropdown-item")).toHaveCount(1);
149+
// The bookmark persists across reloads.
150+
await page.reload();
151+
await seedSelector.click();
152+
await expect(
153+
page.getByRole("button", { name: "Remove bookmark", exact: true }),
154+
).toBeVisible();
178155

179-
// Remove the bookmark from within the dropdown.
180-
await page.getByRole("button", { name: "Add bookmark" }).click();
181-
await expect(page.locator(".dropdown > .dropdown-item")).toHaveCount(2);
156+
// Removing the bookmark drops it from the custom seeds.
182157
await page
183-
.getByRole("button", { name: "seed.example.tld" })
184-
.getByRole("button", { name: "Remove bookmark" })
158+
.getByRole("button", { name: "Remove bookmark", exact: true })
185159
.click();
186-
await expect(page.locator(".dropdown > .dropdown-item")).toHaveCount(1);
160+
await expect(
161+
page.getByRole("button", { name: "Remove bookmark", exact: true }),
162+
).not.toBeVisible();
187163
});

tests/unit/localStore.test.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
2+
import { get } from "svelte/store";
3+
import { z } from "zod";
4+
5+
import storedWritable from "@app/lib/localStore";
6+
7+
const numberSchema = z.number();
8+
9+
describe("storedWritable", () => {
10+
beforeEach(() => {
11+
localStorage.clear();
12+
});
13+
14+
afterEach(() => {
15+
vi.restoreAllMocks();
16+
});
17+
18+
test("uses initial value when localStorage is empty", () => {
19+
const store = storedWritable("test-empty", numberSchema, 42);
20+
expect(get(store)).toBe(42);
21+
});
22+
23+
test("restores prior value from localStorage", () => {
24+
localStorage.setItem("test-restore", "7");
25+
const store = storedWritable("test-restore", numberSchema, 0);
26+
expect(get(store)).toBe(7);
27+
});
28+
29+
test("persists set() to localStorage", () => {
30+
const store = storedWritable("test-persist", numberSchema, 0);
31+
store.set(5);
32+
expect(localStorage.getItem("test-persist")).toBe("5");
33+
});
34+
35+
test("clear() resets the store and removes from localStorage", () => {
36+
const store = storedWritable("test-clear", numberSchema, 0);
37+
store.set(9);
38+
store.clear();
39+
expect(get(store)).toBe(0);
40+
expect(localStorage.getItem("test-clear")).toBeNull();
41+
});
42+
43+
test("falls back to initial value when stored data fails schema", () => {
44+
localStorage.setItem("test-bad-schema", '"not a number"');
45+
const store = storedWritable("test-bad-schema", numberSchema, 3);
46+
expect(get(store)).toBe(3);
47+
});
48+
49+
test("falls back to initial value when stored data is invalid JSON", () => {
50+
localStorage.setItem("test-bad-json", "{not json");
51+
const store = storedWritable("test-bad-json", numberSchema, 4);
52+
expect(get(store)).toBe(4);
53+
});
54+
55+
test("ignores localStorage.getItem errors and uses initial value", () => {
56+
vi.spyOn(Storage.prototype, "getItem").mockImplementation(() => {
57+
throw new Error("private mode");
58+
});
59+
const store = storedWritable("test-get-throws", numberSchema, 11);
60+
expect(get(store)).toBe(11);
61+
});
62+
63+
test("ignores localStorage.setItem errors but still updates in-memory store", () => {
64+
const store = storedWritable("test-set-throws", numberSchema, 0);
65+
vi.spyOn(Storage.prototype, "setItem").mockImplementation(() => {
66+
throw new Error("quota exceeded");
67+
});
68+
expect(() => store.set(99)).not.toThrow();
69+
expect(get(store)).toBe(99);
70+
});
71+
72+
test("ignores localStorage.removeItem errors but still resets in-memory store", () => {
73+
const store = storedWritable("test-remove-throws", numberSchema, 0);
74+
store.set(5);
75+
vi.spyOn(Storage.prototype, "removeItem").mockImplementation(() => {
76+
throw new Error("blocked");
77+
});
78+
expect(() => store.clear()).not.toThrow();
79+
expect(get(store)).toBe(0);
80+
});
81+
82+
test("update() persists to localStorage", () => {
83+
const store = storedWritable("test-update", numberSchema, 1);
84+
store.update(n => n + 10);
85+
expect(get(store)).toBe(11);
86+
expect(localStorage.getItem("test-update")).toBe("11");
87+
});
88+
89+
test("disableLocalStorage skips persistence", () => {
90+
const store = storedWritable("test-disabled", numberSchema, 0, true);
91+
store.set(7);
92+
expect(localStorage.getItem("test-disabled")).toBeNull();
93+
expect(get(store)).toBe(7);
94+
});
95+
});

tests/visual/desktop/landingPage.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,20 @@ test("response error", async ({ page }) => {
6767
mask: [page.locator(".command")],
6868
});
6969
});
70+
71+
test("unable to reach any seed", async ({ page }) => {
72+
await page.addInitScript(() => {
73+
localStorage.setItem(
74+
"configuredPreferredSeeds",
75+
JSON.stringify([{ hostname: "127.0.0.1", port: 8081, scheme: "http" }]),
76+
);
77+
});
78+
// Fail every seed request so no candidate responds, exercising the
79+
// failover dead-end on the explore page.
80+
await page.route(
81+
url => url.port === "8081",
82+
route => route.abort(),
83+
);
84+
await page.goto("/", { waitUntil: "networkidle" });
85+
await expect(page).toHaveScreenshot();
86+
});

0 commit comments

Comments
 (0)