Radish alpha
r
rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5
Radicle web interface
Radicle
Git
Improve test suite
Merged rudolfs opened 2 months ago
  • run the tests against a precompiled production bundle of the app, which is an order of magnitude faster, this improves test run times from 1m30s to 17s
  • bump heartwood and radicle-httpd versions against which we run the testsuite to latest 1.6.1 and 0.24.0 respecitvely
  • resurrect accidentally skipped repo and router tests
  • make some specs less flaky
  • simplify commit listing spec to not require an additional web server
  • improve the test output visually
  • make running tests against a locally compiled httpd easy

Visual spec failures expected, due to version numbers changing, also because we changed the fixtures to remove review messages which are broken in heartwood cli 1.6.1.

check check-visual check-unit-test check-http-client-unit-test check-radicle-httpd check-e2e check-build check-http 👉 Preview 👉 Workflow runs 👉 Branch on GitHub

22 files changed +1055 -885 bd3b660b 4eae95ab
modified .github/workflows/check-e2e.yml
@@ -5,9 +5,6 @@ on:

jobs:
  check-e2e:
-
    strategy:
-
      matrix:
-
        browser: [chromium]
    timeout-minutes: 30
    runs-on: ubuntu-latest
    steps:
@@ -33,7 +30,7 @@ jobs:
          ./scripts/install-binaries;

      - name: Run Playwright tests
-
        run: npm run test:e2e -- --project ${{ matrix.browser }}
+
        run: npm run test:e2e -- --project chromium

      - name: Upload artifacts
        uses: actions/upload-artifact@v4
modified CONTRIBUTING.md
@@ -13,6 +13,50 @@ simple guidelines.
* Follow the guidelines when proposing code changes (see below).
* Write properly formatted git commits (see below).

+
Running Tests
+
-------------
+

+
The test suite includes end-to-end (e2e) tests that run against the built
+
application.
+

+
**Basic usage:**
+

+
    npm run test:e2e
+

+
**Skipping setup:**
+

+
The test suite performs setup operations before running tests:
+
* Building the application bundle
+
* Creating test fixtures (repositories with test data)
+

+
On subsequent test runs, you can skip this setup to save time:
+

+
    SKIP_SETUP=true npm run test:e2e
+

+
Use this when:
+
* You haven't changed any source code since the last test run
+
* The test fixtures are already created
+
* You want to iterate quickly on test development
+

+
**Note:** If you've made code changes or are getting unexpected test failures,
+
remove the `SKIP_SETUP` flag to ensure tests run against the latest build.
+

+
**Common usage patterns:**
+

+
* First time run: `npm run test:e2e -- --project chromium`
+
* Subsequent runs (no code changes): `SKIP_SETUP=true npm run test:e2e -- --project chromium`
+
* Single test by line number: `SKIP_SETUP=true npm run test:e2e -- tests/e2e/repo/commits.spec.ts:90 --project chromium`
+

+
**Testing with local radicle-httpd build:**
+

+
If you're developing radicle-httpd and want to test changes locally:
+

+
    npm run test:e2e:local
+

+
This will:
+
1. Compile radicle-httpd from the `radicle-httpd/` directory
+
2. Run the full test suite against the locally compiled binary
+

Proposing changes
-----------------
When proposing changes via a patch:
modified config/test.json
@@ -5,7 +5,7 @@
  },
  "preferredSeeds": [
    {
-
      "hostname": "127.0.0.1",
+
      "hostname": "localhost",
      "port": 8081,
      "scheme": "http"
    }
modified package.json
@@ -12,6 +12,7 @@
    "format": "npx prettier '**/*.@(ts|js|svelte|json|css|html|yml)' --write",
    "test:unit": "TZ='UTC' vitest run",
    "test:e2e": "NODE_CONFIG_ENV='test' TZ='UTC' playwright test",
+
    "test:e2e:local": "scripts/compile-local-httpd && USE_LOCAL_HTTPD=true NODE_CONFIG_ENV='test' TZ='UTC' playwright test",
    "test:http-client:unit": "NODE_CONFIG_ENV='test' TZ='UTC' vitest run --config http-client/vite.config.ts --reporter verbose",
    "test:radicle-httpd": "cd radicle-httpd && cargo test --all-features",
    "deploy": "rimraf build && npm clean-install && npm run build && scripts/inject-plausible && npx wrangler deploy"
modified playwright.config.ts
@@ -4,12 +4,12 @@ import { devices } from "@playwright/test";
const config: PlaywrightTestConfig = {
  testDir: "./tests/e2e",
  outputDir: "./tests/artifacts",
-
  timeout: 30_000,
+
  timeout: 10_000,
  expect: {
-
    timeout: 8000,
+
    timeout: 5000,
  },
  fullyParallel: true,
-
  workers: process.env.CI ? 1 : undefined,
+
  workers: process.env.CI ? 2 : undefined,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  reporter: "list",
@@ -29,18 +29,6 @@ const config: PlaywrightTestConfig = {
      },
    },
    {
-
      name: "firefox",
-
      use: {
-
        ...devices["Desktop Firefox"],
-
      },
-
    },
-
    {
-
      name: "webkit",
-
      use: {
-
        ...devices["Desktop Safari"],
-
      },
-
    },
-
    {
      name: "visual-desktop",
      timeout: 60_000,
      expect: {
@@ -82,19 +70,11 @@ const config: PlaywrightTestConfig = {
    },
  ],

-
  webServer: [
-
    {
-
      command: "npm run start -- --strictPort --port 3001",
-
      port: 3001,
-
    },
-
    // Required by test tests/e2e/repo/commits.spec.ts "loading more commits, adds them to the commits list"
-
    {
-
      command: "npm run start -- --strictPort --port 3002",
-
      port: 3002,
-
      // eslint-disable-next-line @typescript-eslint/naming-convention
-
      env: { COMMITS_PER_PAGE: "4" },
-
    },
-
  ],
+
  webServer: {
+
    // Use preview server (pre-built app) for main server - much faster startup
+
    command: "npm run serve -- --strictPort --port 3001",
+
    port: 3001,
+
  },
};

export default config;
added scripts/compile-local-httpd
@@ -0,0 +1,29 @@
+
#!/usr/bin/env bash
+
set -e
+

+
echo "🔨 Compiling local radicle-httpd..."
+

+
# Ensure radicle-httpd directory exists
+
if [ ! -d "radicle-httpd" ]; then
+
  echo "❌ radicle-httpd directory not found"
+
  exit 1
+
fi
+

+
# Compile radicle-httpd (debug build for faster compilation)
+
cd radicle-httpd
+
cargo build
+
cd ..
+

+
# Verify binary was created
+
if [ ! -f "radicle-httpd/target/debug/radicle-httpd" ]; then
+
  echo "❌ Compilation failed - binary not found"
+
  exit 1
+
fi
+

+
# Create target directory
+
mkdir -p tests/tmp/bin/httpd/local
+

+
# Copy binary to test location
+
cp radicle-httpd/target/debug/radicle-httpd tests/tmp/bin/httpd/local/radicle-httpd
+

+
echo "✅ Local radicle-httpd compiled and ready at tests/tmp/bin/httpd/local/radicle-httpd"
modified src/App.svelte
@@ -66,7 +66,7 @@
<Hotkeys />

{#if $activeRouteStore.resource === "booting"}
-
  <div class="loading">
+
  <div class="loading" role="progressbar" aria-label="App loading">
    <Loading />
  </div>
{:else if $activeRouteStore.resource === "nodes"}
modified tests/e2e/node.spec.ts
@@ -62,7 +62,7 @@ test("edit seed bookmarks", async ({ page }) => {
          Location: route
            .request()
            .url()
-
            .replace("seed.example.tld", "127.0.0.1"),
+
            .replace("seed.example.tld", "localhost"),
        },
      }),
  );
@@ -73,7 +73,7 @@ test("edit seed bookmarks", async ({ page }) => {
    .getByRole("button", { name: "Toggle seed selector dropdown" })
    .click();
  await expect(page.getByPlaceholder("seed.radicle.example")).toHaveValue(
-
    "127.0.0.1",
+
    "localhost",
  );
  await expect(
    page.getByRole("button", { name: "Default seeds can't be removed" }),
added tests/e2e/repo.spec.ts
@@ -0,0 +1,562 @@
+
import {
+
  aliceMainCommitCount,
+
  aliceMainCommitMessage,
+
  aliceMainHead,
+
  bobMainCommitCount,
+
  cobUrl,
+
  expect,
+
  markdownUrl,
+
  shortAliceHead,
+
  shortBobHead,
+
  sourceBrowsingRid,
+
  sourceBrowsingUrl,
+
  test,
+
} from "@tests/support/fixtures.js";
+
import { changeBranch, createRepo } from "@tests/support/repo";
+
import { expectUrlPersistsReload } from "@tests/support/router";
+

+
test("navigate to repo", async ({ page }) => {
+
  await page.goto(sourceBrowsingUrl);
+

+
  // Header.
+
  {
+
    const name = page.getByRole("link", { name: "source-browsing" }).nth(1);
+
    const id = page.getByText(sourceBrowsingRid);
+
    const description = page.getByText(
+
      "Git repository for source browsing tests",
+
    );
+

+
    await expect(name).toBeVisible();
+
    await expect(id).toBeVisible();
+
    await expect(description).toBeVisible();
+
  }
+

+
  // Repo menu shows default selected branch and commit and contributor counts.
+
  {
+
    await expect(page.getByTitle("Change branch")).toBeVisible();
+
    await expect(
+
      page
+
        .getByRole("button", {
+
          name: `${shortAliceHead} ${aliceMainCommitMessage}`,
+
        })
+
        .first(),
+
    ).toBeVisible();
+
    await expect(
+
      page.getByRole("link", {
+
        name: `Commits ${aliceMainCommitCount}`,
+
      }),
+
    ).toBeVisible();
+
  }
+

+
  // Navigate to the repo README.md by default.
+
  await expect(page.locator(".filename")).toContainText("README.md");
+

+
  // Show a commit teaser.
+
  await expect(page.getByText("dd068e9 Add README.md")).toBeVisible();
+

+
  // Show rendered README.md contents.
+
  await expect(page.getByText("Git test repository")).toBeVisible();
+
});
+

+
test("repo description", async ({ page, peer }) => {
+
  const { rid } = await createRepo(peer, {
+
    name: "heartwood",
+
    description: "Radicle Heartwood Protocol & Stack",
+
  });
+
  await page.goto(peer.ridUrl(rid));
+
  await page.waitForLoadState("networkidle");
+
  await expect(
+
    page.getByText("Radicle Heartwood Protocol & Stack"),
+
  ).toBeVisible();
+
});
+

+
test("show source tree at specific revision", async ({ page }) => {
+
  await page.goto(sourceBrowsingUrl);
+
  await page
+
    .getByRole("link", { name: `Commits ${aliceMainCommitCount}` })
+
    .click();
+

+
  await page
+
    .locator(".teaser", { hasText: "335dd6d" })
+
    .getByRole("button", {
+
      name: "Browse repo at this commit",
+
    })
+
    .click();
+

+
  await expect(page.getByTitle("Current HEAD")).toContainText("335dd6d");
+
  await expect(page.locator(".source-tree")).toHaveText("bin src");
+
  await expect(
+
    page.getByRole("link", {
+
      name: "Commits 2",
+
    }),
+
  ).toBeVisible();
+
});
+

+
test("source file highlighting", async ({ page }) => {
+
  await page.goto(sourceBrowsingUrl);
+

+
  await page.getByText("src").click();
+
  await page.getByText("true.c").click();
+

+
  await expect(page.getByText("return")).toHaveCSS(
+
    "color",
+
    "rgb(255, 123, 114)",
+
  );
+
});
+

+
test("navigate line numbers", async ({ page }) => {
+
  await page.goto(`${markdownUrl}/tree/main/cheatsheet.md`);
+
  await page.getByRole("button", { name: "Code" }).click();
+

+
  await page.getByRole("link", { name: "5", exact: true }).click();
+
  await expect(page.locator("#L5")).toHaveClass("line highlight");
+
  await expect(page).toHaveURL(`${markdownUrl}/tree/main/cheatsheet.md#L5`);
+

+
  await expectUrlPersistsReload(page);
+
  await expect(page.locator("#L5")).toHaveClass("line highlight");
+

+
  await page.getByRole("link", { name: "30", exact: true }).click();
+
  await expect(page.locator("#L5")).not.toHaveClass("line highlight");
+
  await expect(page.locator("#L30")).toHaveClass("line highlight");
+
  await expect(page).toHaveURL(`${markdownUrl}/tree/main/cheatsheet.md#L30`);
+

+
  // Check that we go back to the Markdown view when navigating to a different
+
  // file.
+
  await page.getByRole("link", { name: "footnotes.md" }).click();
+
  await expect(page.getByRole("button", { name: "Preview" })).toHaveClass(
+
    /selected/,
+
  );
+
});
+

+
test("navigate deep file hierarchies", async ({ page }) => {
+
  await page.goto(sourceBrowsingUrl);
+

+
  const sourceTree = page.locator(".source-tree");
+

+
  await sourceTree.getByText("deep").click();
+
  await sourceTree.getByText("directory").click();
+
  await sourceTree.getByText("hierarchy").click();
+
  await sourceTree.getByText("is").click();
+
  await sourceTree.getByText("entirely").click();
+
  await sourceTree.getByText("possible").click();
+
  await sourceTree.getByText("in").nth(1).click();
+
  await sourceTree.getByRole("button", { name: "git" }).click();
+
  await sourceTree.getByText("repositories").click();
+
  await sourceTree.getByText(".gitkeep").click();
+
  await expect(
+
    page.getByText("0801ace Add a deeply nested directory tree"),
+
  ).toBeVisible();
+

+
  // After a page reload the tree browser is still expanded and we're still
+
  // showing the .gitkeep file.
+
  {
+
    await page.reload();
+

+
    const sourceTree = page.locator(".source-tree");
+

+
    await expect(sourceTree.getByText("deep")).toBeVisible();
+
    await expect(sourceTree.getByText("directory")).toBeVisible();
+
    await expect(sourceTree.getByText("hierarchy")).toBeVisible();
+
    await expect(sourceTree.getByText("is")).toBeVisible();
+
    await expect(sourceTree.getByText("entirely")).toBeVisible();
+
    await expect(sourceTree.getByText("possible")).toBeVisible();
+
    await expect(sourceTree.getByText("in").nth(1)).toBeVisible();
+
    await expect(sourceTree.getByText("git").nth(1)).toBeVisible();
+
    await expect(sourceTree.getByText("repositories")).toBeVisible();
+
    await expect(sourceTree.getByText(".gitkeep")).toBeVisible();
+

+
    await expect(
+
      page.getByText("0801ace Add a deeply nested directory tree"),
+
    ).toBeVisible();
+
  }
+
});
+

+
test("submodules", async ({ page }) => {
+
  await page.goto(sourceBrowsingUrl);
+
  await expect(page.getByText("rips @ 329dee9")).toBeVisible();
+
});
+

+
test("files with special characters in the filename", async ({ page }) => {
+
  await page.goto(sourceBrowsingUrl);
+

+
  const sourceTree = page.locator(".source-tree");
+
  await sourceTree.getByText("special").click();
+

+
  await sourceTree.getByText("+plus+").click();
+
  await expect(page.getByRole("banner")).toContainText("+plus");
+

+
  await sourceTree.getByText("-dash-").click();
+
  await expect(page.getByRole("banner")).toContainText("-dash-");
+

+
  await sourceTree.getByText(":colon:").click();
+
  await expect(page.getByRole("banner")).toContainText(":colon:");
+

+
  await sourceTree.getByText(";semicolon;").click();
+
  await expect(page.getByRole("banner")).toContainText(";semicolon;");
+

+
  await sourceTree.getByText("@at@").click();
+
  await expect(page.getByRole("banner")).toContainText("@at@");
+

+
  await sourceTree.getByText("_underscore_").click();
+
  await expect(page.getByRole("banner")).toContainText("_underscore_");
+

+
  // TODO: fix these errors in `radicle-httpd` for the following edge cases.
+
  //
+
  // await sourceTree.getByText("back\\slash").click();
+
  // await expect(page.locator(".filename")).toContainText("back\\slash");
+
  // await sourceTree.getByText("qs?param1=value?param2=value2#hash").click();
+
  // await expect(page.locator(".filename")).toContainText(
+
  //   "qs?param1=value?param2=value2#hash",
+
  // );
+

+
  await sourceTree.getByText("spaces are okay").click();
+
  await expect(page.getByRole("banner")).toContainText("spaces are okay");
+

+
  await sourceTree.getByText("~tilde~").click();
+
  await expect(page.getByRole("banner")).toContainText("~tilde~");
+

+
  await sourceTree.getByText("👹👹👹").click();
+
  await expect(page.getByRole("banner")).toContainText("👹👹👹");
+
});
+

+
test("binary files", async ({ page }) => {
+
  await page.goto(sourceBrowsingUrl);
+

+
  await page.getByText("bin").click();
+
  await page.getByText("true").click();
+

+
  await expect(page.getByText("Binary file")).toBeVisible();
+
});
+

+
test("empty files", async ({ page }) => {
+
  await page.goto(sourceBrowsingUrl);
+

+
  await page.getByText("special").click();
+
  await page.getByText("_underscore_").click();
+

+
  await expect(page.getByText("Empty file")).toBeVisible();
+
});
+

+
test("hidden files", async ({ page }) => {
+
  await page.goto(sourceBrowsingUrl);
+

+
  await page.getByText(".hidden").click();
+

+
  await expect(page.getByText("I'm a hidden file.")).toBeVisible();
+
});
+

+
test("markdown files", async ({ page }) => {
+
  await page.goto(`${markdownUrl}/tree/main/cheatsheet.md`);
+

+
  await expect(
+
    page.getByText("This is intended as a quick reference and showcase."),
+
  ).toBeVisible();
+

+
  // Switch between raw and rendered modes.
+
  {
+
    await expect(page.getByRole("button", { name: "Preview" })).toHaveClass(
+
      /selected/,
+
    );
+
    await expect(page.getByRole("button", { name: "Code" })).toHaveClass(
+
      /not-selected/,
+
    );
+
    await page.getByRole("button", { name: "Code" }).click();
+
    await expect(page.getByRole("button", { name: "Preview" })).toHaveClass(
+
      /not-selected/,
+
    );
+
    await expect(page.getByRole("button", { name: "Code" })).toHaveClass(
+
      /selected/,
+
    );
+
    await expect(page.getByText("##### Table of Contents")).toBeVisible();
+
    await page.getByRole("button", { name: "Preview" }).click();
+
  }
+

+
  // Internal links go to anchor.
+
  {
+
    await page.getByRole("link", { name: "YouTube Videos" }).click();
+
    await expect(page).toHaveURL(
+
      `${markdownUrl}/tree/main/cheatsheet.md#videos`,
+
    );
+
  }
+
});
+

+
test("clone modal", async ({ page }) => {
+
  await page.goto(sourceBrowsingUrl);
+

+
  await page.getByRole("button", { name: "Clone" }).click();
+
  await expect(page.getByText(`rad clone ${sourceBrowsingRid}`)).toBeVisible();
+
  await page.getByRole("button", { name: "Git" }).click();
+
  await expect(
+
    page.getByText(
+
      `http://localhost/${sourceBrowsingRid.replace("rad:", "")}.git`,
+
    ),
+
  ).toBeVisible();
+
});
+

+
test("peer and branch switching", async ({ page }) => {
+
  await page.goto(sourceBrowsingUrl);
+

+
  // Alice's peer.
+
  {
+
    await changeBranch("alice", `main ${shortAliceHead}`, page);
+
    await expect(page.getByTitle("Change branch")).toHaveText(/alice/);
+

+
    // Default `main` branch.
+
    {
+
      await expect(page.getByTitle("Change branch")).toHaveText(/main/);
+
      await expect(
+
        page
+
          .getByRole("button", {
+
            name: `${shortAliceHead} ${aliceMainCommitMessage}`,
+
          })
+
          .first(),
+
      ).toBeVisible();
+
      await expect(
+
        page.getByRole("link", {
+
          name: `Commits ${aliceMainCommitCount}`,
+
        }),
+
      ).toBeVisible();
+
    }
+

+
    // Feature branch with a slash in the name.
+
    {
+
      await changeBranch("alice", "feature/branch", page);
+
      await page.getByTitle("Change branch").click();
+
      await page.getByText("feature/branch").click();
+

+
      await expect(
+
        page.getByRole("button", { name: "feature/branch" }),
+
      ).toBeVisible();
+
      await expect(
+
        page.getByRole("button", { name: "1aded56 Add subconscious file" }),
+
      ).toBeVisible();
+
      await expect(
+
        page.getByRole("link", {
+
          name: "Commits 9",
+
        }),
+
      ).toBeVisible();
+
    }
+

+
    // Branch without a history or files in it.
+
    {
+
      await changeBranch("alice", "orphaned-branch", page);
+

+
      await expect(
+
        page.getByRole("button", { name: "orphaned-branch" }),
+
      ).toBeVisible();
+
      await expect(
+
        page.getByRole("button", { name: "af3641c Add empty orphaned" }),
+
      ).toBeVisible();
+
      await expect(
+
        page.getByRole("link", {
+
          name: "Commits 1",
+
        }),
+
      ).toBeVisible();
+

+
      await expect(page.getByText("No files at this revision")).toBeVisible();
+
    }
+
  }
+

+
  // Reset the source browser by clicking the repo title.
+
  {
+
    await page.getByRole("link", { name: "source-browsing" }).nth(1).click();
+

+
    await expect(page.getByTitle("Change branch")).not.toContainText("alice");
+
    await expect(page.getByTitle("Change branch")).not.toContainText("bob");
+

+
    await expect(page.getByTitle("Change branch")).toBeVisible();
+
    await expect(
+
      page
+
        .getByRole("button", {
+
          name: `${shortAliceHead} ${aliceMainCommitMessage}`,
+
        })
+
        .first(),
+
    ).toBeVisible();
+
    await expect(page.getByText("Git test repository")).toBeVisible();
+
  }
+

+
  // Bob's peer.
+
  {
+
    await changeBranch("bob", `main ${shortBobHead}`, page);
+
    await expect(
+
      page.getByRole("button", { name: "avatar bob / main" }),
+
    ).toBeVisible();
+

+
    // Default `main` branch.
+
    {
+
      await expect(page.getByRole("button", { name: "main" })).toBeVisible();
+
      await expect(
+
        page
+
          .getByRole("button", { name: `${shortBobHead} Update readme` })
+
          .first(),
+
      ).toBeVisible();
+
      await expect(
+
        page.getByRole("link", {
+
          name: `Commits ${bobMainCommitCount}`,
+
        }),
+
      ).toBeVisible();
+
      await expect(
+
        page
+
          .getByRole("button", { name: `${shortBobHead} Update readme` })
+
          .first(),
+
      ).toBeVisible();
+
    }
+
  }
+
});
+

+
test("only one modal can be open at a time", async ({ page }) => {
+
  await page.goto(sourceBrowsingUrl);
+

+
  await changeBranch("alice", `main ${shortAliceHead}`, page);
+

+
  await page.getByText("Clone").click();
+
  await expect(page.getByText("Code font")).not.toBeVisible();
+
  await expect(page.getByText("Use the Radicle CLI")).toBeVisible();
+
  await expect(page.getByText("bob")).not.toBeVisible();
+

+
  await page.getByRole("button", { name: "Settings" }).click();
+
  await expect(page.getByText("Code font")).toBeVisible();
+
  await expect(page.getByText("Use the Radicle CLI")).not.toBeVisible();
+
  await expect(page.getByText("bob")).not.toBeVisible();
+

+
  await page.getByTitle("Change branch").click();
+
  await expect(page.getByText("Code font")).not.toBeVisible();
+
  await expect(page.getByText("Use the Radicle CLI")).not.toBeVisible();
+
  await expect(page.getByText("bob")).toBeVisible();
+
});
+

+
test.describe("browser error handling", () => {
+
  test("error appears when folder can't be loaded", async ({ page }) => {
+
    await page.route(
+
      ({ pathname }) =>
+
        pathname.startsWith(
+
          `/api/v1/repos/${sourceBrowsingRid}/tree/${aliceMainHead}/src`,
+
        ),
+
      route => route.fulfill({ status: 500 }),
+
    );
+

+
    await page.goto(sourceBrowsingUrl);
+

+
    const sourceTree = page.locator(".source-tree");
+
    await sourceTree.getByText("src").click();
+

+
    await expect(page.getByText("No README found.")).toBeVisible();
+
  });
+
  test("error appears when file can't be loaded", async ({ page }) => {
+
    await page.route(
+
      ({ pathname }) =>
+
        pathname ===
+
        `/api/v1/repos/${sourceBrowsingRid}/blob/${aliceMainHead}/.hidden`,
+
      route => route.fulfill({ status: 500 }),
+
    );
+

+
    await page.goto(sourceBrowsingUrl);
+
    await page.getByText(".hidden").click();
+

+
    await expect(page.getByText("File not found")).toBeVisible();
+
  });
+
  test("error appears when README can't be loaded", async ({ page }) => {
+
    await page.route(
+
      ({ pathname }) =>
+
        pathname ===
+
        `/api/v1/repos/${sourceBrowsingRid}/readme/${aliceMainHead}`,
+
      route => route.fulfill({ status: 500 }),
+
    );
+

+
    await page.goto(sourceBrowsingUrl);
+
    await expect(page.getByText("No README found.")).toBeVisible();
+
  });
+
  test("error appears when navigating to missing file", async ({ page }) => {
+
    await page.route(
+
      ({ pathname }) =>
+
        pathname ===
+
        `/api/v1/repos/${sourceBrowsingRid}/blob/${aliceMainHead}/.hidden`,
+
      route => route.fulfill({ status: 500 }),
+
    );
+

+
    await page.goto(`${sourceBrowsingUrl}/tree/master/.hidden`);
+

+
    await expect(page.getByText("File not found")).toBeVisible();
+
  });
+
});
+

+
test("external markdown link", async ({ context, page }) => {
+
  await context.route("https://example.com/**", route => {
+
    return route.fulfill({ body: "hello", contentType: "text/plain" });
+
  });
+
  await page.goto(`${markdownUrl}/tree/main/footnotes.md`);
+
  const pagePromise = context.waitForEvent("page");
+
  await page.getByRole("link", { name: "https://example.com" }).click();
+
  const newPage = await pagePromise;
+
  await expect(newPage).toHaveURL("https://example.com");
+
});
+

+
test("absolute markdown link", async ({ page }) => {
+
  await page.goto(markdownUrl);
+
  await page.getByRole("link", { name: "Nested Linked File" }).click();
+
  await expect(page).toHaveURL(
+
    `${markdownUrl}/tree/relative-files/linked-file.md`,
+
  );
+
  await page.goBack();
+
  await expect(page).toHaveURL(markdownUrl);
+
  await page.getByRole("link", { name: "Link Files" }).click();
+
  await page.getByRole("link", { name: "Absolute Link" }).click();
+
  await expect(page).toHaveURL(
+
    `${markdownUrl}/tree/relative-files/linked-file.md`,
+
  );
+
  await page.getByRole("link", { name: "nested file", exact: true }).click();
+
  await expect(page).toHaveURL(
+
    `${markdownUrl}/tree/relative-files/nested-file.md`,
+
  );
+
  await page.goBack();
+
  await page.getByRole("link", { name: "nested file with" }).click();
+
  await expect(page).toHaveURL(
+
    `${markdownUrl}/tree/relative-files/nested-file.md`,
+
  );
+
  await page.goBack();
+
  await page.getByRole("link", { name: "Back to link-files with" }).click();
+
  await expect(page).toHaveURL(`${markdownUrl}/tree/link-files.md`);
+
});
+

+
test("internal file markdown link", async ({ page }) => {
+
  await page.goto(`${markdownUrl}/tree/main/link-files.md`);
+
  await page.getByRole("link", { name: "Markdown Cheatsheet" }).click();
+
  await expect(page).toHaveURL(`${markdownUrl}/tree/main/cheatsheet.md`);
+
  await expect(page.getByText("cheatsheet.md").nth(2)).toBeVisible();
+

+
  await page.goto(markdownUrl);
+
  await page.getByRole("link", { name: "Link Files" }).click();
+
  await page.getByRole("button", { name: "Files", exact: true }).click();
+
  await page.getByRole("link", { name: "Link Files" }).click();
+
  await expect(page).toHaveURL(`${markdownUrl}/tree/link-files.md`);
+
  await expect(page.getByText("link-files.md").nth(2)).toBeVisible();
+

+
  await page.goto(`${markdownUrl}/tree/main/link-files.md`);
+
  await page.getByRole("link", { name: "black square" }).click();
+
  await expect(page).toHaveURL(
+
    `${markdownUrl}/tree/main/assets/black-square.png`,
+
  );
+
  await expect(page.getByText("assets/black-square.png").nth(1)).toBeVisible();
+
  await expect(
+
    page.getByRole("link", { name: "black-square.png" }),
+
  ).toBeVisible();
+
});
+

+
test("diff selection de-select", async ({ page }) => {
+
  await page.goto(`${cobUrl}/patches`);
+
  await page
+
    .getByRole("link", { name: "Taking another stab at the README" })
+
    .click();
+
  await page.getByRole("link", { name: "Changes" }).click();
+
  await page
+
    .getByRole("row", { name: "+ # Cobs Repo" })
+
    .locator("div")
+
    .first()
+
    .click();
+
  await expect(page).toHaveURL(new RegExp("tab=changes#README.md:H0L1$"));
+
  // Click outside.
+
  await page
+
    .getByText("1 file modified with 5 insertions and 1 deletion")
+
    .click();
+
  await expect(page).toHaveURL(new RegExp("tab=changes$"));
+
});
deleted tests/e2e/repo.ts
@@ -1,561 +0,0 @@
-
import {
-
  aliceMainCommitCount,
-
  aliceMainCommitMessage,
-
  aliceMainHead,
-
  bobMainCommitCount,
-
  cobUrl,
-
  expect,
-
  markdownUrl,
-
  shortAliceHead,
-
  shortBobHead,
-
  sourceBrowsingRid,
-
  sourceBrowsingUrl,
-
  test,
-
} from "@tests/support/fixtures.js";
-
import { changeBranch, createRepo } from "@tests/support/repo";
-
import { expectUrlPersistsReload } from "@tests/support/router";
-

-
test("navigate to repo", async ({ page }) => {
-
  await page.goto(sourceBrowsingUrl);
-

-
  // Header.
-
  {
-
    const name = page.getByRole("link", { name: "source-browsing" }).nth(1);
-
    const id = page.getByText(sourceBrowsingRid);
-
    const description = page.getByText(
-
      "Git repository for source browsing tests",
-
    );
-

-
    await expect(name).toBeVisible();
-
    await expect(id).toBeVisible();
-
    await expect(description).toBeVisible();
-
  }
-

-
  // Repo menu shows default selected branch and commit and contributor counts.
-
  {
-
    await expect(page.getByTitle("Change branch")).toBeVisible();
-
    await expect(
-
      page
-
        .getByRole("button", {
-
          name: `${shortAliceHead} ${aliceMainCommitMessage}`,
-
        })
-
        .first(),
-
    ).toBeVisible();
-
    await expect(
-
      page.getByRole("link", {
-
        name: `Commits ${aliceMainCommitCount}`,
-
      }),
-
    ).toBeVisible();
-
  }
-

-
  // Navigate to the repo README.md by default.
-
  await expect(page.locator(".filename")).toContainText("README.md");
-

-
  // Show a commit teaser.
-
  await expect(page.getByText("dd068e9 Add README.md")).toBeVisible();
-

-
  // Show rendered README.md contents.
-
  await expect(page.getByText("Git test repository")).toBeVisible();
-
});
-

-
test("repo description", async ({ page, peer }) => {
-
  const { rid } = await createRepo(peer, {
-
    name: "heartwood",
-
    description: "Radicle Heartwood Protocol & Stack",
-
  });
-
  await page.goto(peer.ridUrl(rid));
-
  await expect(
-
    page.getByText("Radicle Heartwood Protocol & Stack"),
-
  ).toBeVisible();
-
});
-

-
test("show source tree at specific revision", async ({ page }) => {
-
  await page.goto(sourceBrowsingUrl);
-
  await page
-
    .getByRole("link", { name: `Commits ${aliceMainCommitCount}` })
-
    .click();
-

-
  await page
-
    .locator(".teaser", { hasText: "335dd6d" })
-
    .getByRole("button", {
-
      name: "Browse repo at this commit",
-
    })
-
    .click();
-

-
  await expect(page.getByTitle("Current HEAD")).toContainText("335dd6d");
-
  await expect(page.locator(".source-tree")).toHaveText("bin src");
-
  await expect(
-
    page.getByRole("link", {
-
      name: "Commits 2",
-
    }),
-
  ).toBeVisible();
-
});
-

-
test("source file highlighting", async ({ page }) => {
-
  await page.goto(sourceBrowsingUrl);
-

-
  await page.getByText("src").click();
-
  await page.getByText("true.c").click();
-

-
  await expect(page.getByText("return")).toHaveCSS(
-
    "color",
-
    "rgb(255, 123, 114)",
-
  );
-
});
-

-
test("navigate line numbers", async ({ page }) => {
-
  await page.goto(`${markdownUrl}/tree/main/cheatsheet.md`);
-
  await page.getByRole("button", { name: "Code" }).click();
-

-
  await page.getByRole("link", { name: "5", exact: true }).click();
-
  await expect(page.locator("#L5")).toHaveClass("line highlight");
-
  await expect(page).toHaveURL(`${markdownUrl}/tree/main/cheatsheet.md#L5`);
-

-
  await expectUrlPersistsReload(page);
-
  await expect(page.locator("#L5")).toHaveClass("line highlight");
-

-
  await page.getByRole("link", { name: "30", exact: true }).click();
-
  await expect(page.locator("#L5")).not.toHaveClass("line highlight");
-
  await expect(page.locator("#L30")).toHaveClass("line highlight");
-
  await expect(page).toHaveURL(`${markdownUrl}/tree/main/cheatsheet.md#L30`);
-

-
  // Check that we go back to the Markdown view when navigating to a different
-
  // file.
-
  await page.getByRole("link", { name: "footnotes.md" }).click();
-
  await expect(page.getByRole("button", { name: "Preview" })).toHaveClass(
-
    /selected/,
-
  );
-
});
-

-
test("navigate deep file hierarchies", async ({ page }) => {
-
  await page.goto(sourceBrowsingUrl);
-

-
  const sourceTree = page.locator(".source-tree");
-

-
  await sourceTree.getByText("deep").click();
-
  await sourceTree.getByText("directory").click();
-
  await sourceTree.getByText("hierarchy").click();
-
  await sourceTree.getByText("is").click();
-
  await sourceTree.getByText("entirely").click();
-
  await sourceTree.getByText("possible").click();
-
  await sourceTree.getByText("in").nth(1).click();
-
  await sourceTree.getByRole("button", { name: "git" }).click();
-
  await sourceTree.getByText("repositories").click();
-
  await sourceTree.getByText(".gitkeep").click();
-
  await expect(
-
    page.getByText("0801ace Add a deeply nested directory tree"),
-
  ).toBeVisible();
-

-
  // After a page reload the tree browser is still expanded and we're still
-
  // showing the .gitkeep file.
-
  {
-
    await page.reload();
-

-
    const sourceTree = page.locator(".source-tree");
-

-
    await expect(sourceTree.getByText("deep")).toBeVisible();
-
    await expect(sourceTree.getByText("directory")).toBeVisible();
-
    await expect(sourceTree.getByText("hierarchy")).toBeVisible();
-
    await expect(sourceTree.getByText("is")).toBeVisible();
-
    await expect(sourceTree.getByText("entirely")).toBeVisible();
-
    await expect(sourceTree.getByText("possible")).toBeVisible();
-
    await expect(sourceTree.getByText("in").nth(1)).toBeVisible();
-
    await expect(sourceTree.getByText("git").nth(1)).toBeVisible();
-
    await expect(sourceTree.getByText("repositories")).toBeVisible();
-
    await expect(sourceTree.getByText(".gitkeep")).toBeVisible();
-

-
    await expect(
-
      page.getByText("0801ace Add a deeply nested directory tree"),
-
    ).toBeVisible();
-
  }
-
});
-

-
test("submodules", async ({ page }) => {
-
  await page.goto(sourceBrowsingUrl);
-
  await expect(page.getByText("rips @ 329dee9")).toBeVisible();
-
});
-

-
test("files with special characters in the filename", async ({ page }) => {
-
  await page.goto(sourceBrowsingUrl);
-

-
  const sourceTree = page.locator(".source-tree");
-
  await sourceTree.getByText("special").click();
-

-
  await sourceTree.getByText("+plus+").click();
-
  await expect(page.getByRole("banner")).toContainText("+plus");
-

-
  await sourceTree.getByText("-dash-").click();
-
  await expect(page.getByRole("banner")).toContainText("-dash-");
-

-
  await sourceTree.getByText(":colon:").click();
-
  await expect(page.getByRole("banner")).toContainText(":colon:");
-

-
  await sourceTree.getByText(";semicolon;").click();
-
  await expect(page.getByRole("banner")).toContainText(";semicolon;");
-

-
  await sourceTree.getByText("@at@").click();
-
  await expect(page.getByRole("banner")).toContainText("@at@");
-

-
  await sourceTree.getByText("_underscore_").click();
-
  await expect(page.getByRole("banner")).toContainText("_underscore_");
-

-
  // TODO: fix these errors in `radicle-httpd` for the following edge cases.
-
  //
-
  // await sourceTree.getByText("back\\slash").click();
-
  // await expect(page.locator(".filename")).toContainText("back\\slash");
-
  // await sourceTree.getByText("qs?param1=value?param2=value2#hash").click();
-
  // await expect(page.locator(".filename")).toContainText(
-
  //   "qs?param1=value?param2=value2#hash",
-
  // );
-

-
  await sourceTree.getByText("spaces are okay").click();
-
  await expect(page.getByRole("banner")).toContainText("spaces are okay");
-

-
  await sourceTree.getByText("~tilde~").click();
-
  await expect(page.getByRole("banner")).toContainText("~tilde~");
-

-
  await sourceTree.getByText("👹👹👹").click();
-
  await expect(page.getByRole("banner")).toContainText("👹👹👹");
-
});
-

-
test("binary files", async ({ page }) => {
-
  await page.goto(sourceBrowsingUrl);
-

-
  await page.getByText("bin").click();
-
  await page.getByText("true").click();
-

-
  await expect(page.getByText("Binary file")).toBeVisible();
-
});
-

-
test("empty files", async ({ page }) => {
-
  await page.goto(sourceBrowsingUrl);
-

-
  await page.getByText("special").click();
-
  await page.getByText("_underscore_").click();
-

-
  await expect(page.getByText("Empty file")).toBeVisible();
-
});
-

-
test("hidden files", async ({ page }) => {
-
  await page.goto(sourceBrowsingUrl);
-

-
  await page.getByText(".hidden").click();
-

-
  await expect(page.getByText("I'm a hidden file.")).toBeVisible();
-
});
-

-
test("markdown files", async ({ page }) => {
-
  await page.goto(`${markdownUrl}/tree/main/cheatsheet.md`);
-

-
  await expect(
-
    page.getByText("This is intended as a quick reference and showcase."),
-
  ).toBeVisible();
-

-
  // Switch between raw and rendered modes.
-
  {
-
    await expect(page.getByRole("button", { name: "Preview" })).toHaveClass(
-
      /selected/,
-
    );
-
    await expect(page.getByRole("button", { name: "Code" })).toHaveClass(
-
      /not-selected/,
-
    );
-
    await page.getByRole("button", { name: "Code" }).click();
-
    await expect(page.getByRole("button", { name: "Preview" })).toHaveClass(
-
      /not-selected/,
-
    );
-
    await expect(page.getByRole("button", { name: "Code" })).toHaveClass(
-
      /selected/,
-
    );
-
    await expect(page.getByText("##### Table of Contents")).toBeVisible();
-
    await page.getByRole("button", { name: "Preview" }).click();
-
  }
-

-
  // Internal links go to anchor.
-
  {
-
    await page.getByRole("link", { name: "YouTube Videos" }).click();
-
    await expect(page).toHaveURL(
-
      `${markdownUrl}/tree/main/cheatsheet.md#videos`,
-
    );
-
  }
-
});
-

-
test("clone modal", async ({ page }) => {
-
  await page.goto(sourceBrowsingUrl);
-

-
  await page.getByRole("button", { name: "Clone" }).click();
-
  await expect(page.getByText(`rad clone ${sourceBrowsingRid}`)).toBeVisible();
-
  await page.getByRole("button", { name: "Git" }).click();
-
  await expect(
-
    page.getByText(
-
      `http://127.0.0.1/${sourceBrowsingRid.replace("rad:", "")}.git`,
-
    ),
-
  ).toBeVisible();
-
});
-

-
test("peer and branch switching", async ({ page }) => {
-
  await page.goto(sourceBrowsingUrl);
-

-
  // Alice's peer.
-
  {
-
    await changeBranch("alice", `main ${shortAliceHead}`, page);
-
    await expect(page.getByTitle("Change branch")).toHaveText(/alice/);
-

-
    // Default `main` branch.
-
    {
-
      await expect(page.getByTitle("Change branch")).toHaveText(/main/);
-
      await expect(
-
        page
-
          .getByRole("button", {
-
            name: `${shortAliceHead} ${aliceMainCommitMessage}`,
-
          })
-
          .first(),
-
      ).toBeVisible();
-
      await expect(
-
        page.getByRole("link", {
-
          name: `Commits ${aliceMainCommitCount}`,
-
        }),
-
      ).toBeVisible();
-
    }
-

-
    // Feature branch with a slash in the name.
-
    {
-
      await changeBranch("alice", "feature/branch", page);
-
      await page.getByTitle("Change branch").click();
-
      await page.getByText("feature/branch").click();
-

-
      await expect(
-
        page.getByRole("button", { name: "feature/branch" }),
-
      ).toBeVisible();
-
      await expect(
-
        page.getByRole("button", { name: "1aded56 Add subconscious file" }),
-
      ).toBeVisible();
-
      await expect(
-
        page.getByRole("link", {
-
          name: "Commits 9",
-
        }),
-
      ).toBeVisible();
-
    }
-

-
    // Branch without a history or files in it.
-
    {
-
      await changeBranch("alice", "orphaned-branch", page);
-

-
      await expect(
-
        page.getByRole("button", { name: "orphaned-branch" }),
-
      ).toBeVisible();
-
      await expect(
-
        page.getByRole("button", { name: "af3641c Add empty orphaned" }),
-
      ).toBeVisible();
-
      await expect(
-
        page.getByRole("link", {
-
          name: "Commits 1",
-
        }),
-
      ).toBeVisible();
-

-
      await expect(page.getByText("No files at this revision")).toBeVisible();
-
    }
-
  }
-

-
  // Reset the source browser by clicking the repo title.
-
  {
-
    await page.getByRole("link", { name: "source-browsing" }).nth(1).click();
-

-
    await expect(page.getByTitle("Change branch")).not.toContainText("alice");
-
    await expect(page.getByTitle("Change branch")).not.toContainText("bob");
-

-
    await expect(page.getByTitle("Change branch")).toBeVisible();
-
    await expect(
-
      page
-
        .getByRole("button", {
-
          name: `${shortAliceHead} ${aliceMainCommitMessage}`,
-
        })
-
        .first(),
-
    ).toBeVisible();
-
    await expect(page.getByText("Git test repository")).toBeVisible();
-
  }
-

-
  // Bob's peer.
-
  {
-
    await changeBranch("bob", `main ${shortBobHead}`, page);
-
    await expect(
-
      page.getByRole("button", { name: "avatar bob / main" }),
-
    ).toBeVisible();
-

-
    // Default `main` branch.
-
    {
-
      await expect(page.getByRole("button", { name: "main" })).toBeVisible();
-
      await expect(
-
        page
-
          .getByRole("button", { name: `${shortBobHead} Update readme` })
-
          .first(),
-
      ).toBeVisible();
-
      await expect(
-
        page.getByRole("link", {
-
          name: `Commits ${bobMainCommitCount}`,
-
        }),
-
      ).toBeVisible();
-
      await expect(
-
        page
-
          .getByRole("button", { name: `${shortBobHead} Update readme` })
-
          .first(),
-
      ).toBeVisible();
-
    }
-
  }
-
});
-

-
test("only one modal can be open at a time", async ({ page }) => {
-
  await page.goto(sourceBrowsingUrl);
-

-
  await changeBranch("alice", `main ${shortAliceHead}`, page);
-

-
  await page.getByText("Clone").click();
-
  await expect(page.getByText("Code font")).not.toBeVisible();
-
  await expect(page.getByText("Use the Radicle CLI")).toBeVisible();
-
  await expect(page.getByText("bob")).not.toBeVisible();
-

-
  await page.getByRole("button", { name: "Settings" }).click();
-
  await expect(page.getByText("Code font")).toBeVisible();
-
  await expect(page.getByText("Use the Radicle CLI")).not.toBeVisible();
-
  await expect(page.getByText("bob")).not.toBeVisible();
-

-
  await page.getByTitle("Change branch").click();
-
  await expect(page.getByText("Code font")).not.toBeVisible();
-
  await expect(page.getByText("Use the Radicle CLI")).not.toBeVisible();
-
  await expect(page.getByText("bob")).toBeVisible();
-
});
-

-
test.describe("browser error handling", () => {
-
  test("error appears when folder can't be loaded", async ({ page }) => {
-
    await page.route(
-
      ({ pathname }) =>
-
        pathname.startsWith(
-
          `/api/v1/repos/${sourceBrowsingRid}/tree/${aliceMainHead}/src`,
-
        ),
-
      route => route.fulfill({ status: 500 }),
-
    );
-

-
    await page.goto(sourceBrowsingUrl);
-

-
    const sourceTree = page.locator(".source-tree");
-
    await sourceTree.getByText("src").click();
-

-
    await expect(page.getByText("No README found.")).toBeVisible();
-
  });
-
  test("error appears when file can't be loaded", async ({ page }) => {
-
    await page.route(
-
      ({ pathname }) =>
-
        pathname ===
-
        `/api/v1/repos/${sourceBrowsingRid}/blob/${aliceMainHead}/.hidden`,
-
      route => route.fulfill({ status: 500 }),
-
    );
-

-
    await page.goto(sourceBrowsingUrl);
-
    await page.getByText(".hidden").click();
-

-
    await expect(page.getByText("File not found")).toBeVisible();
-
  });
-
  test("error appears when README can't be loaded", async ({ page }) => {
-
    await page.route(
-
      ({ pathname }) =>
-
        pathname ===
-
        `/api/v1/repos/${sourceBrowsingRid}/readme/${aliceMainHead}`,
-
      route => route.fulfill({ status: 500 }),
-
    );
-

-
    await page.goto(sourceBrowsingUrl);
-
    await expect(page.getByText("No README found.")).toBeVisible();
-
  });
-
  test("error appears when navigating to missing file", async ({ page }) => {
-
    await page.route(
-
      ({ pathname }) =>
-
        pathname ===
-
        `/api/v1/repos/${sourceBrowsingRid}/blob/${aliceMainHead}/.hidden`,
-
      route => route.fulfill({ status: 500 }),
-
    );
-

-
    await page.goto(`${sourceBrowsingUrl}/tree/master/.hidden`);
-

-
    await expect(page.getByText("File not found")).toBeVisible();
-
  });
-
});
-

-
test("external markdown link", async ({ context, page }) => {
-
  await context.route("https://example.com/**", route => {
-
    return route.fulfill({ body: "hello", contentType: "text/plain" });
-
  });
-
  await page.goto(`${markdownUrl}/tree/main/footnotes.md`);
-
  const pagePromise = context.waitForEvent("page");
-
  await page.getByRole("link", { name: "https://example.com" }).click();
-
  const newPage = await pagePromise;
-
  await expect(newPage).toHaveURL("https://example.com");
-
});
-

-
test("absolute markdown link", async ({ page }) => {
-
  await page.goto(markdownUrl);
-
  await page.getByRole("link", { name: "Nested Linked File" }).click();
-
  await expect(page).toHaveURL(
-
    `${markdownUrl}/tree/relative-files/linked-file.md`,
-
  );
-
  await page.goBack();
-
  await expect(page).toHaveURL(markdownUrl);
-
  await page.getByRole("link", { name: "Link Files" }).click();
-
  await page.getByRole("link", { name: "Absolute Link" }).click();
-
  await expect(page).toHaveURL(
-
    `${markdownUrl}/tree/relative-files/linked-file.md`,
-
  );
-
  await page.getByRole("link", { name: "nested file", exact: true }).click();
-
  await expect(page).toHaveURL(
-
    `${markdownUrl}/tree/relative-files/nested-file.md`,
-
  );
-
  await page.goBack();
-
  await page.getByRole("link", { name: "nested file with" }).click();
-
  await expect(page).toHaveURL(
-
    `${markdownUrl}/tree/relative-files/nested-file.md`,
-
  );
-
  await page.goBack();
-
  await page.getByRole("link", { name: "Back to link-files with" }).click();
-
  await expect(page).toHaveURL(`${markdownUrl}/tree/link-files.md`);
-
});
-

-
test("internal file markdown link", async ({ page }) => {
-
  await page.goto(`${markdownUrl}/tree/main/link-files.md`);
-
  await page.getByRole("link", { name: "Markdown Cheatsheet" }).click();
-
  await expect(page).toHaveURL(`${markdownUrl}/tree/main/cheatsheet.md`);
-
  await expect(page.getByText("cheatsheet.md").nth(2)).toBeVisible();
-

-
  await page.goto(markdownUrl);
-
  await page.getByRole("link", { name: "Link Files" }).click();
-
  await page.getByRole("button", { name: "Files", exact: true }).click();
-
  await page.getByRole("link", { name: "Link Files" }).click();
-
  await expect(page).toHaveURL(`${markdownUrl}/tree/link-files.md`);
-
  await expect(page.getByText("link-files.md").nth(2)).toBeVisible();
-

-
  await page.goto(`${markdownUrl}/tree/main/link-files.md`);
-
  await page.getByRole("link", { name: "black square" }).click();
-
  await expect(page).toHaveURL(
-
    `${markdownUrl}/tree/main/assets/black-square.png`,
-
  );
-
  await expect(page.getByText("assets/black-square.png").nth(1)).toBeVisible();
-
  await expect(
-
    page.getByRole("link", { name: "black-square.png" }),
-
  ).toBeVisible();
-
});
-

-
test("diff selection de-select", async ({ page }) => {
-
  await page.goto(`${cobUrl}/patches`);
-
  await page
-
    .getByRole("link", { name: "Taking another stab at the README" })
-
    .click();
-
  await page.getByRole("link", { name: "Changes" }).click();
-
  await page
-
    .getByRole("row", { name: "+ # Cobs Repo" })
-
    .locator("div")
-
    .first()
-
    .click();
-
  await expect(page).toHaveURL(new RegExp("tab=changes#README.md:H0L1$"));
-
  // Click outside.
-
  await page
-
    .getByText("1 file modified with 5 insertions and 1 deletion")
-
    .click();
-
  await expect(page).toHaveURL(new RegExp("tab=changes$"));
-
});
modified tests/e2e/repo/commits.spec.ts
@@ -2,6 +2,7 @@ import {
  aliceMainCommitCount,
  aliceMainCommitMessage,
  bobMainCommitCount,
+
  commitsUrl,
  expect,
  gitOptions,
  shortAliceHead,
@@ -89,12 +90,12 @@ test("peer and branch switching", async ({ page }) => {
test("loading more commits, adds them to the commits list", async ({
  page,
}) => {
-
  await page.goto(`http://localhost:3002${sourceBrowsingUrl}`);
+
  await page.goto(commitsUrl);
  await page.getByRole("button", { name: "Commits" }).click();
-
  await expect(page.locator("div > div > .teaser")).toHaveCount(4);
+
  await expect(page.locator("div > div > .teaser")).toHaveCount(30);

  await page.getByRole("button", { name: "More" }).click();
-
  await expect(page.locator("div > div > .teaser")).toHaveCount(8);
+
  await expect(page.locator("div > div > .teaser")).toHaveCount(31);
});

test("commit messages with double colon not converted into single colon", async ({
@@ -131,6 +132,7 @@ test("commit messages with double colon not converted into single colon", async

test("expand commit message", async ({ page }) => {
  await page.goto(sourceBrowsingUrl);
+
  await page.waitForLoadState("networkidle");
  await page
    .getByRole("link", { name: `Commits ${aliceMainCommitCount}` })
    .click();
modified tests/e2e/repo/issues.spec.ts
@@ -3,6 +3,7 @@ import { createRepo } from "@tests/support/repo";

test("navigate issue listing", async ({ page }) => {
  await page.goto(cobUrl);
+
  await page.waitForLoadState("networkidle");
  await page.getByRole("link", { name: "Issues 1" }).click();
  await expect(page).toHaveURL(`${cobUrl}/issues`);

modified tests/e2e/repo/patch.spec.ts
@@ -7,6 +7,7 @@ test("navigate patch details", async ({ page }) => {
  await page.getByRole("link", { name: "Add subtitle to README" }).click();
  await expect(page).toHaveURL(/commits\/[a-f0-9]{40}$/);
  await page.goBack();
+
  await page.waitForLoadState("networkidle");
  await page.getByRole("link", { name: "Changes" }).click();
  await expect(page).toHaveURL(/patches\/[a-f0-9]{40}\?tab=changes$/);
});
@@ -77,6 +78,7 @@ test("navigate through revision diffs", async ({ page }) => {
      /patches\/[a-f0-9]{40}\?diff=38c225e2a0b47ba59def211f4e4825c31d9463ec\.\.9e4feab1b2123dfa5f22bd0e4656060ec9296638$/,
    );
    await page.goBack();
+
    await page.waitForLoadState("networkidle");
    await secondRevision
      .getByRole("button", { name: "toggle-context-menu" })
      .first()
@@ -94,6 +96,7 @@ test("navigate through revision diffs", async ({ page }) => {
      /patches\/[a-f0-9]{40}\?diff=88b7fd90389c1a629f91ed7bf838d4b947426622\.\.9e4feab1b2123dfa5f22bd0e4656060ec9296638$/,
    );
    await page.goBack();
+
    await page.waitForLoadState("networkidle");
  }
  // First revision and DiffStatBadge shortcut.
  {
@@ -141,6 +144,7 @@ test("commit listing ordering keeping stable on browser navigation", async ({
  await expectCorrectCommitListing();
  await page.getByRole("link", { name: "Rewrite subtitle to README" }).click();
  await page.goBack();
+
  await page.waitForLoadState("networkidle");
  await page
    .getByRole("heading", { name: "Taking another stab at the README" })
    .waitFor();
added tests/e2e/router.spec.ts
@@ -0,0 +1,189 @@
+
import {
+
  aliceMainCommitCount,
+
  aliceMainHead,
+
  aliceRemote,
+
  expect,
+
  sourceBrowsingUrl,
+
  test,
+
} from "@tests/support/fixtures.js";
+
import { createRepo } from "@tests/support/repo";
+
import {
+
  expectBackAndForwardNavigationWorks,
+
  expectUrlPersistsReload,
+
} from "@tests/support/router.js";
+

+
test("navigate between landing and repo page", async ({ page }) => {
+
  await page.goto("/");
+
  await expect(page).toHaveURL("/");
+

+
  await page.getByText("source-browsing").click();
+
  await expect(page).toHaveURL(sourceBrowsingUrl);
+

+
  await expectBackAndForwardNavigationWorks("/", page);
+
  await expectUrlPersistsReload(page);
+
});
+

+
test("navigation between node and repo pages", async ({ page }) => {
+
  await page.goto("/nodes/localhost");
+

+
  const repo = page
+
    .locator(".repo-card", { hasText: "source-browsing" })
+
    .nth(0);
+
  await repo.click();
+
  await expect(page).toHaveURL(sourceBrowsingUrl);
+

+
  await expectBackAndForwardNavigationWorks("/nodes/localhost", page);
+
  await expectUrlPersistsReload(page);
+

+
  await page.getByRole("link", { name: "Radicle logo localhost" }).click();
+
  await expect(page).toHaveURL("/nodes/localhost");
+
});
+

+
test.describe("repo page navigation", () => {
+
  test("navigation between commit history and single commit", async ({
+
    page,
+
  }) => {
+
    const repoHistoryURL = `${sourceBrowsingUrl}/history/${aliceMainHead}`;
+
    await page.goto(repoHistoryURL);
+

+
    await page
+
      .getByRole("link", {
+
        name: "Verify that crate::DoubleColon::should_work()",
+
        exact: true,
+
      })
+
      .click();
+
    await expect(page).toHaveURL(
+
      `${sourceBrowsingUrl}/commits/${aliceMainHead}`,
+
    );
+

+
    await expectBackAndForwardNavigationWorks(repoHistoryURL, page);
+
    await expectUrlPersistsReload(page);
+
  });
+

+
  test("navigate between tree and commit history", async ({ page }) => {
+
    const repoTreeURL = `${sourceBrowsingUrl}/tree/${aliceMainHead}`;
+

+
    await page.goto(repoTreeURL);
+
    await page
+
      .getByRole("progressbar", { name: "Page loading" })
+
      .waitFor({ state: "hidden" });
+
    await expect(page).toHaveURL(repoTreeURL);
+

+
    await page
+
      .getByRole("link", { name: `Commits ${aliceMainCommitCount}` })
+
      .click();
+

+
    await expect(page).toHaveURL(
+
      `${sourceBrowsingUrl}/history/${aliceMainHead}`,
+
    );
+

+
    await expectBackAndForwardNavigationWorks(repoTreeURL, page);
+
    await expectUrlPersistsReload(page);
+
  });
+

+
  test("navigate between tree and commit history while a file is selected", async ({
+
    page,
+
  }) => {
+
    const repoTreeURL = `${sourceBrowsingUrl}`;
+

+
    await page.goto(repoTreeURL);
+
    await page
+
      .getByRole("progressbar", { name: "Page loading" })
+
      .waitFor({ state: "hidden" });
+
    await expect(page).toHaveURL(repoTreeURL);
+

+
    await page.getByText(".hidden").click();
+
    await expect(page).toHaveURL(`${repoTreeURL}/tree/.hidden`);
+

+
    await page
+
      .getByRole("link", { name: `Commits ${aliceMainCommitCount}` })
+
      .click();
+
    await expect(page).toHaveURL(`${sourceBrowsingUrl}/history`);
+
  });
+

+
  test("navigate repo paths", async ({ page }) => {
+
    const repoTreeURL = `${sourceBrowsingUrl}/tree/${aliceMainHead}`;
+

+
    await page.goto(repoTreeURL);
+
    await expect(page).toHaveURL(repoTreeURL);
+

+
    await page.getByText(".hidden").click();
+
    await expect(page).toHaveURL(`${repoTreeURL}/.hidden`);
+

+
    await page.getByText("bin").click();
+
    await page.getByText("true").click();
+
    await expect(page).toHaveURL(`${repoTreeURL}/bin/true`);
+

+
    await expectBackAndForwardNavigationWorks(`${repoTreeURL}/.hidden`, page);
+
    await expectUrlPersistsReload(page);
+
  });
+

+
  test("page title", async ({ page }) => {
+
    await page.goto(sourceBrowsingUrl, {
+
      waitUntil: "networkidle",
+
    });
+
    const title = await page.title();
+
    expect(title).toBe(
+
      "source-browsing · Git repository for source browsing tests",
+
    );
+
  });
+

+
  test("page title on repo with empty description", async ({ page, peer }) => {
+
    const { rid } = await createRepo(peer, {
+
      name: "RepoWithNoDescription",
+
    });
+
    await page.goto(peer.ridUrl(rid), {
+
      waitUntil: "networkidle",
+
    });
+
    const title = await page.title();
+
    expect(title).toBe("RepoWithNoDescription");
+
  });
+

+
  test("navigate repo paths with an explicitly selected peer", async ({
+
    page,
+
  }) => {
+
    // If a branch isn't explicitly specified, the code assumes the repo
+
    // default branch is selected. We omit showing the default branch in the URL.
+

+
    const repoTreeURL = `${sourceBrowsingUrl}/remotes/${aliceRemote.substring(
+
      8,
+
    )}`;
+

+
    await page.goto(repoTreeURL);
+
    await expect(page).toHaveURL(repoTreeURL);
+

+
    await page.getByText(".hidden").click();
+
    await expect(page).toHaveURL(`${repoTreeURL}/tree/.hidden`);
+

+
    await page.getByText("bin").click();
+
    await page.getByText("true").click();
+
    await expect(page).toHaveURL(`${repoTreeURL}/tree/bin/true`);
+

+
    await expectBackAndForwardNavigationWorks(
+
      `${repoTreeURL}/tree/.hidden`,
+
      page,
+
    );
+
    await expectUrlPersistsReload(page);
+
  });
+

+
  test("navigate repo paths with an explicitly selected peer and branch", async ({
+
    page,
+
  }) => {
+
    const repoTreeURL = `${sourceBrowsingUrl}/remotes/${aliceRemote.substring(
+
      8,
+
    )}/tree/main`;
+

+
    await page.goto(repoTreeURL);
+
    await expect(page).toHaveURL(repoTreeURL);
+

+
    await page.getByText(".hidden").click();
+
    await expect(page).toHaveURL(`${repoTreeURL}/.hidden`);
+

+
    await page.getByText("bin").click();
+
    await page.getByText("true").click();
+
    await expect(page).toHaveURL(`${repoTreeURL}/bin/true`);
+

+
    await expectBackAndForwardNavigationWorks(`${repoTreeURL}/.hidden`, page);
+
    await expectUrlPersistsReload(page);
+
  });
+
});
deleted tests/e2e/router.ts
@@ -1,184 +0,0 @@
-
import {
-
  aliceMainCommitCount,
-
  aliceMainHead,
-
  aliceRemote,
-
  expect,
-
  sourceBrowsingUrl,
-
  test,
-
} from "@tests/support/fixtures.js";
-
import { createRepo } from "@tests/support/repo";
-
import {
-
  expectBackAndForwardNavigationWorks,
-
  expectUrlPersistsReload,
-
} from "@tests/support/router.js";
-

-
test("navigate between landing and repo page", async ({ page }) => {
-
  await page.goto("/");
-
  await expect(page).toHaveURL("/");
-

-
  await page.getByText("source-browsing").click();
-
  await expect(page).toHaveURL(sourceBrowsingUrl);
-

-
  await expectBackAndForwardNavigationWorks("/", page);
-
  await expectUrlPersistsReload(page);
-
});
-

-
test("navigation between node and repo pages", async ({ page }) => {
-
  await page.goto("/nodes/localhost");
-

-
  const repo = page
-
    .locator(".repo-card", { hasText: "source-browsing" })
-
    .nth(0);
-
  await repo.click();
-
  await expect(page).toHaveURL(sourceBrowsingUrl);
-

-
  await expectBackAndForwardNavigationWorks("/nodes/localhost", page);
-
  await expectUrlPersistsReload(page);
-

-
  await page.getByRole("link", { name: "Local Node" }).click();
-
  await expect(page).toHaveURL("/nodes/127.0.0.1");
-
});
-

-
test.describe("repo page navigation", () => {
-
  test("navigation between commit history and single commit", async ({
-
    page,
-
  }) => {
-
    const repoHistoryURL = `${sourceBrowsingUrl}/history/${aliceMainHead}`;
-
    await page.goto(repoHistoryURL);
-

-
    await page.getByText("Add README.md").click();
-
    await expect(page).toHaveURL(
-
      `${sourceBrowsingUrl}/commits/${aliceMainHead}`,
-
    );
-

-
    await expectBackAndForwardNavigationWorks(repoHistoryURL, page);
-
    await expectUrlPersistsReload(page);
-
  });
-

-
  test("navigate between tree and commit history", async ({ page }) => {
-
    const repoTreeURL = `${sourceBrowsingUrl}/tree/${aliceMainHead}`;
-

-
    await page.goto(repoTreeURL);
-
    await page
-
      .getByRole("progressbar", { name: "Page loading" })
-
      .waitFor({ state: "hidden" });
-
    await expect(page).toHaveURL(repoTreeURL);
-

-
    await page
-
      .getByRole("link", { name: `Commits ${aliceMainCommitCount}` })
-
      .click();
-

-
    await expect(page).toHaveURL(
-
      `${sourceBrowsingUrl}/history/${aliceMainHead}`,
-
    );
-

-
    await expectBackAndForwardNavigationWorks(repoTreeURL, page);
-
    await expectUrlPersistsReload(page);
-
  });
-

-
  test("navigate between tree and commit history while a file is selected", async ({
-
    page,
-
  }) => {
-
    const repoTreeURL = `${sourceBrowsingUrl}`;
-

-
    await page.goto(repoTreeURL);
-
    await page
-
      .getByRole("progressbar", { name: "Page loading" })
-
      .waitFor({ state: "hidden" });
-
    await expect(page).toHaveURL(repoTreeURL);
-

-
    await page.getByText(".hidden").click();
-
    await expect(page).toHaveURL(`${repoTreeURL}/tree/.hidden`);
-

-
    await page
-
      .getByRole("link", { name: `Commits ${aliceMainCommitCount}` })
-
      .click();
-
    await expect(page).toHaveURL(`${sourceBrowsingUrl}/history`);
-
  });
-

-
  test("navigate repo paths", async ({ page }) => {
-
    const repoTreeURL = `${sourceBrowsingUrl}/tree/${aliceMainHead}`;
-

-
    await page.goto(repoTreeURL);
-
    await expect(page).toHaveURL(repoTreeURL);
-

-
    await page.getByText(".hidden").click();
-
    await expect(page).toHaveURL(`${repoTreeURL}/.hidden`);
-

-
    await page.getByText("bin").click();
-
    await page.getByText("true").click();
-
    await expect(page).toHaveURL(`${repoTreeURL}/bin/true`);
-

-
    await expectBackAndForwardNavigationWorks(`${repoTreeURL}/.hidden`, page);
-
    await expectUrlPersistsReload(page);
-
  });
-

-
  test("page title", async ({ page }) => {
-
    await page.goto(sourceBrowsingUrl, {
-
      waitUntil: "networkidle",
-
    });
-
    const title = await page.title();
-
    expect(title).toBe(
-
      "source-browsing · Git repository for source browsing tests",
-
    );
-
  });
-

-
  test("page title on repo with empty description", async ({ page, peer }) => {
-
    const { rid } = await createRepo(peer, {
-
      name: "RepoWithNoDescription",
-
    });
-
    await page.goto(peer.ridUrl(rid), {
-
      waitUntil: "networkidle",
-
    });
-
    const title = await page.title();
-
    expect(title).toBe("RepoWithNoDescription");
-
  });
-

-
  test("navigate repo paths with an explicitly selected peer", async ({
-
    page,
-
  }) => {
-
    // If a branch isn't explicitly specified, the code assumes the repo
-
    // default branch is selected. We omit showing the default branch in the URL.
-

-
    const repoTreeURL = `${sourceBrowsingUrl}/remotes/${aliceRemote.substring(
-
      8,
-
    )}`;
-

-
    await page.goto(repoTreeURL);
-
    await expect(page).toHaveURL(repoTreeURL);
-

-
    await page.getByText(".hidden").click();
-
    await expect(page).toHaveURL(`${repoTreeURL}/tree/.hidden`);
-

-
    await page.getByText("bin").click();
-
    await page.getByText("true").click();
-
    await expect(page).toHaveURL(`${repoTreeURL}/tree/bin/true`);
-

-
    await expectBackAndForwardNavigationWorks(
-
      `${repoTreeURL}/tree/.hidden`,
-
      page,
-
    );
-
    await expectUrlPersistsReload(page);
-
  });
-

-
  test("navigate repo paths with an explicitly selected peer and branch", async ({
-
    page,
-
  }) => {
-
    const repoTreeURL = `${sourceBrowsingUrl}/remotes/${aliceRemote.substring(
-
      8,
-
    )}/tree/main`;
-

-
    await page.goto(repoTreeURL);
-
    await expect(page).toHaveURL(repoTreeURL);
-

-
    await page.getByText(".hidden").click();
-
    await expect(page).toHaveURL(`${repoTreeURL}/.hidden`);
-

-
    await page.getByText("bin").click();
-
    await page.getByText("true").click();
-
    await expect(page).toHaveURL(`${repoTreeURL}/bin/true`);
-

-
    await expectBackAndForwardNavigationWorks(`${repoTreeURL}/.hidden`, page);
-
    await expectUrlPersistsReload(page);
-
  });
-
});
modified tests/support/cobs/issue.ts
@@ -15,7 +15,7 @@ export async function create(
    title,
    "--description",
    description,
-
    ...labels.map(label => ["--label", label]).flat(),
+
    ...labels.map(label => ["--labels", label]).flat(),
  ];
  const { stdout } = await peer.rad(issueOptions, options);
  const match = stdout.match(/Issue {3}([a-zA-Z0-9]*)/);
modified tests/support/fixtures.ts
@@ -7,9 +7,9 @@ import * as Fs from "node:fs/promises";
import * as Path from "node:path";
import { test as base, expect } from "@playwright/test";
import { execa } from "execa";
+
import chalk from "chalk";

import * as issue from "@tests/support/cobs/issue.js";
-
import * as logLabel from "@tests/support/logPrefix.js";
import * as patch from "@tests/support/cobs/patch.js";
import { createOptions, supportDir, tmpDir } from "@tests/support/support.js";
import { createPeerManager } from "@tests/support/peerManager.js";
@@ -37,7 +37,7 @@ export const test = base.extend<{
        globalThis.__PLAYWRIGHT__ = true;
      });

-
      const browserLabel = logLabel.logPrefix("browser");
+
      const browserLabel = " ".repeat(23) + "→ " + chalk.blue("browser") + ": ";
      page.on("console", msg => {
        // Ignore common console logs that we don't care about.
        if (
@@ -62,7 +62,8 @@ export const test = base.extend<{
        });
      }

-
      const playwrightLabel = logLabel.logPrefix("playwright");
+
      const playwrightLabel =
+
        " ".repeat(23) + "→ " + chalk.yellowBright("playwright") + ": ";

      function isLocalhost(url: URL) {
        return url.hostname === "localhost" || url.hostname === "127.0.0.1";
@@ -143,7 +144,7 @@ export const test = base.extend<{
function log(text: string, label: string, outputLog: Stream.Writable) {
  const output = text
    .split("\n")
-
    .map(line => `${label}${line}`)
+
    .map(line => `${label}${chalk.dim(line)}`)
    .join("\n");

  outputLog.write(`${output}\n`);
@@ -443,7 +444,7 @@ export async function createCobsFixture(
    createOptions(repoFolder, 5),
  );
  await peer.rad(
-
    ["patch", "review", patchOne, "-m", "LGTM", "--accept"],
+
    ["patch", "review", patchOne, "--accept"],
    createOptions(repoFolder, 6),
  );
  await patch.merge(
@@ -462,14 +463,7 @@ export async function createCobsFixture(
    { cwd: repoFolder },
  );
  await peer.rad(
-
    [
-
      "patch",
-
      "review",
-
      patchTwo,
-
      "-m",
-
      "Not the README we are looking for",
-
      "--reject",
-
    ],
+
    ["patch", "review", patchTwo, "--reject"],
    createOptions(repoFolder, 1),
  );

@@ -494,7 +488,7 @@ export async function createCobsFixture(
    createOptions(repoFolder, 1),
  );
  await eve.rad(
-
    ["patch", "review", patchThree, "-m", "This looks better"],
+
    ["patch", "review", patchThree, "--accept"],
    createOptions(repoFolder, 2),
  );
  await Fs.appendFile(
@@ -516,14 +510,7 @@ export async function createCobsFixture(
    createOptions(repoFolder, 3),
  );
  await peer.rad(
-
    [
-
      "patch",
-
      "review",
-
      patchThree,
-
      "-m",
-
      "No this doesn't look better",
-
      "--reject",
-
    ],
+
    ["patch", "review", patchThree, "--reject"],
    createOptions(repoFolder, 2),
  );

@@ -536,13 +523,7 @@ export async function createCobsFixture(
    { cwd: repoFolder },
  );
  await peer.rad(
-
    [
-
      "patch",
-
      "review",
-
      patchFour,
-
      "-m",
-
      "No review due to patch being archived.",
-
    ],
+
    ["patch", "review", patchFour, "--accept"],
    createOptions(repoFolder, 1),
  );
  await peer.rad(["patch", "archive", patchFour], createOptions(repoFolder, 2));
@@ -594,6 +575,53 @@ export async function createMarkdownFixture(peer: RadiclePeer) {
  );
}

+
export async function createCommitsFixture(peer: RadiclePeer) {
+
  await Fs.mkdir(Path.join(tmpDir, "repos", "commits"));
+
  const { repoFolder } = await createRepo(peer, { name: "commits" });
+

+
  // Create 30 more commits for a total of 31 using git fast-import for speed
+
  const baseDate = 1671125284; // Same as Alice's date
+
  const authorName = gitOptions["alice"].GIT_AUTHOR_NAME;
+
  const authorEmail = gitOptions["alice"].GIT_AUTHOR_EMAIL;
+

+
  // Get the initial commit hash to use as parent
+
  const { stdout: initialCommit } = await peer.git(["rev-parse", "HEAD"], {
+
    cwd: repoFolder,
+
  });
+

+
  // Build fast-import data stream
+
  let fastImportData = "";
+
  for (let i = 1; i <= 30; i++) {
+
    const commitDate = baseDate + i;
+
    const message = `Commit ${i}`;
+
    const parent = i === 1 ? initialCommit.trim() : `:${i - 1}`;
+
    fastImportData += `commit refs/heads/main
+
mark :${i}
+
author ${authorName} <${authorEmail}> ${commitDate} +0000
+
committer ${authorName} <${authorEmail}> ${commitDate} +0000
+
data ${message.length}
+
${message}
+
from ${parent}
+

+
`;
+
  }
+

+
  // Write to temp file and pipe it to git fast-import
+
  const fastImportFile = Path.join(tmpDir, "repos", "commits-fast-import");
+
  await Fs.writeFile(fastImportFile, fastImportData);
+

+
  await peer.spawn(
+
    "bash",
+
    ["-c", `git fast-import --force < "${fastImportFile}"`],
+
    {
+
      cwd: repoFolder,
+
    },
+
  );
+

+
  await Fs.unlink(fastImportFile);
+
  await peer.git(["push", "rad"], { cwd: repoFolder });
+
}
+

export const aliceMainHead = "7babd25a74eb3752ec24672b5edf0e7ecb4daf24";
export const aliceMainCommitMessage =
  "Verify that crate::DoubleColon::should_work()";
@@ -609,9 +637,11 @@ export const shortBobHead = formatCommit(bobHead);
export const sourceBrowsingRid = "rad:z4BwwjPCFNVP27FwVbDFgwVwkjcir";
export const cobRid = "rad:z3fpY7nttPPa6MBnAv2DccHzQJnqe";
export const markdownRid = "rad:z2tchH2Ti4LxRKdssPQYs6VHE5rsg";
+
export const commitsRid = "rad:z2ysBeUDZnHejYvaToxYopZiUA3oy";
export const sourceBrowsingUrl = `/nodes/localhost/${sourceBrowsingRid}`;
export const cobUrl = `/nodes/localhost/${cobRid}`;
export const markdownUrl = `/nodes/localhost/${markdownRid}`;
+
export const commitsUrl = `/nodes/localhost/${commitsRid}`;
export const shortNodeRemote = "z6MktU…1xB22S";
export const gitOptions = {
  alice: {
modified tests/support/globalSetup.ts
@@ -6,10 +6,12 @@ import {
  radicleHttpdRelease,
  removeWorkspace,
  tmpDir,
+
  useLocalHttpd,
} from "@tests/support/support.js";
import {
  defaultConfig,
  createCobsFixture,
+
  createCommitsFixture,
  createMarkdownFixture,
  createSourceBrowsingFixture,
  gitOptions,
@@ -22,8 +24,10 @@ const heartwoodBinaryPath = Path.join(
  "bin",
  "heartwood",
  heartwoodRelease,
-
);
-
const httpdBinaryPath = Path.join(tmpDir, "bin", "httpd", radicleHttpdRelease);
+
).trim();
+
const httpdBinaryPath = useLocalHttpd
+
  ? Path.join(tmpDir, "bin", "httpd", "local").trim()
+
  : Path.join(tmpDir, "bin", "httpd", radicleHttpdRelease).trim();

process.env.PATH = [
  heartwoodBinaryPath,
@@ -36,81 +40,150 @@ export default async function globalSetup(): Promise<() => void> {
    await assertBinariesInstalled("rad", heartwoodRelease, heartwoodBinaryPath);
    await assertBinariesInstalled(
      "radicle-httpd",
-
      radicleHttpdRelease,
+
      useLocalHttpd ? "pre-release" : radicleHttpdRelease,
      httpdBinaryPath,
    );
  } catch (error) {
    console.error(error);
    console.log("");
-
    console.log("To download the required test binaries, run:");
-
    console.log(" 👉 ./scripts/install-binaries");
+
    if (useLocalHttpd) {
+
      console.log("To compile local radicle-httpd binary, run:");
+
      console.log(" 👉 ./scripts/compile-local-httpd");
+
    } else {
+
      console.log("To download the required test binaries, run:");
+
      console.log(" 👉 ./scripts/install-binaries");
+
    }
    console.log("");
    process.exit(1);
  }

-
  if (!process.env.SKIP_FIXTURE_CREATION) {
-
    console.log(
-
      "Recreating static fixtures. Set SKIP_FIXTURE_CREATION to skip this",
-
    );
-
    await removeWorkspace();
+
  // Evaluated once at startup; captured by async setup operations.
+
  // Set SKIP_SETUP=true to skip both build and fixture creation on subsequent runs.
+
  // See CONTRIBUTING.md for details.
+
  const shouldSetup = !process.env.SKIP_SETUP;
+

+
  if (shouldSetup) {
+
    console.log("⚡ Starting parallel setup...");
+
  } else {
+
    console.log("⏭️ Skipping setup (SKIP_SETUP is set)");
  }

-
  const peerManager = await createPeerManager({
-
    dataDir: Path.resolve(tmpDir, "peers"),
-
    outputLog: Fs.createWriteStream(
-
      Path.resolve(tmpDir, "globalPeerManager.log"),
-
    )
-
      // Workaround for fixing MaxListenersExceededWarning.
-
      // Since every prefixOutput stream adds stream listeners that don't autoClose.
-
      // TODO: We still seem to have some descriptors left open when running vitest, which we should handle.
-
      .setMaxListeners(16),
-
  });
-

-
  const palm = await peerManager.createPeer({
-
    name: "palm",
-
    gitOptions: gitOptions["alice"],
-
  });
-

-
  if (!process.env.SKIP_FIXTURE_CREATION) {
-
    await palm.startNode({
-
      web: {
-
        pinned: {
-
          repositories: ["rad:z4BwwjPCFNVP27FwVbDFgwVwkjcir"],
-
        },
-
        description: `:seedling: Radicle is an open source, peer-to-peer code collaboration stack built on Git.
+
  // Run build and fixture setup in parallel
+
  const buildPromise = (async () => {
+
    if (shouldSetup) {
+
      console.log("  🔨  Starting build...");
+
      const { execa: exec } = await import("execa");
+
      try {
+
        await exec("npm", ["run", "build"]);
+
        console.log("  🔨  Build complete");
+
      } catch (error) {
+
        console.log("  🔨  Build failed!");
+
        if (error && typeof error === "object" && "stdout" in error) {
+
          console.log(error.stdout);
+
        }
+
        if (error && typeof error === "object" && "stderr" in error) {
+
          console.log(error.stderr);
+
        }
+
        throw error;
+
      }
+
    }
+
  })();

-
:construction: [radicle.xyz](https://radicle.xyz)`,
-
      },
-
      node: {
-
        ...defaultConfig.node,
-
        seedingPolicy: { default: "allow", scope: "all" },
-
        alias: "palm",
-
      },
+
  const fixturesPromise = (async () => {
+
    if (shouldSetup) {
+
      console.log("  🗂️  Starting fixture creation...");
+
      await removeWorkspace();
+
    }
+

+
    const peerManager = await createPeerManager({
+
      dataDir: Path.resolve(tmpDir, "peers"),
+
      outputLog: Fs.createWriteStream(
+
        Path.resolve(tmpDir, "globalPeerManager.log"),
+
      )
+
        // Workaround for fixing MaxListenersExceededWarning.
+
        // Since every prefixOutput stream adds stream listeners that don't autoClose.
+
        // TODO: We still seem to have some descriptors left open when running vitest, which we should handle.
+
        .setMaxListeners(16),
+
    });
+

+
    const palm = await peerManager.createPeer({
+
      name: "palm",
+
      gitOptions: gitOptions["alice"],
    });
-
    await palm.startHttpd(config.nodes.defaultHttpdPort);
-

-
    try {
-
      console.log("Creating source-browsing fixture");
-
      await createSourceBrowsingFixture(peerManager, palm);
-
      console.log("Creating markdown fixture");
-
      await createMarkdownFixture(palm);
-
      console.log("Creating cobs fixture");
-
      await createCobsFixture(peerManager, palm);
-
      console.log("All fixtures created");
-
    } catch (error) {
-
      console.log("");
-
      console.log("Not able to create the required fixtures.");
-
      console.log("Make sure you are not using binaries compiled for release.");
-
      console.log("");
-
      console.log(error);
-
      console.log("");
-
      process.exit(1);
+

+
    if (shouldSetup) {
+
      await palm.startNode({
+
        web: {
+
          pinned: {
+
            repositories: ["rad:z4BwwjPCFNVP27FwVbDFgwVwkjcir"],
+
          },
+
          description: `:seedling: Radicle is an open source, peer-to-peer code collaboration stack built on Git.
+

+
:construction: [radicle.xyz](https://radicle.xyz)`,
+
        },
+
        node: {
+
          ...defaultConfig.node,
+
          seedingPolicy: { default: "allow", scope: "all" },
+
          alias: "palm",
+
        },
+
      });
+
      await palm.startHttpd(config.nodes.defaultHttpdPort);
+

+
      try {
+
        console.log("      Creating source-browsing fixture");
+
        await createSourceBrowsingFixture(peerManager, palm);
+
        console.log("      Creating markdown fixture");
+
        await createMarkdownFixture(palm);
+
        console.log("      Creating cobs fixture");
+
        await createCobsFixture(peerManager, palm);
+
        console.log("      Creating commits fixture");
+
        await createCommitsFixture(palm);
+
        console.log("  🗂️  All fixtures created");
+
      } catch (error) {
+
        console.log("");
+
        console.log("  🗂️  Not able to create the required fixtures.");
+
        console.log(
+
          "      Make sure you are not using binaries compiled for release.",
+
        );
+
        console.log("");
+
        console.log(error);
+
        console.log("");
+
        process.exit(1);
+
      }
+
      await palm.stopNode();
+
    } else {
+
      await palm.startHttpd(config.nodes.defaultHttpdPort);
    }
-
    await palm.stopNode();
-
  } else {
-
    await palm.startHttpd(config.nodes.defaultHttpdPort);
+

+
    return peerManager;
+
  })();
+

+
  // Wait for both build and fixtures to complete
+
  const [, peerManager] = await Promise.all([buildPromise, fixturesPromise]);
+

+
  if (shouldSetup) {
+
    console.log("🚀 Setup complete, ready to run tests");
  }

+
  // Print binary versions
+
  const { execa: exec } = await import("execa");
+
  const { stdout: radVersion } = await exec("rad", ["--version"]);
+
  const { stdout: gitRemoteRadVersion } = await exec("git-remote-rad", [
+
    "--version",
+
  ]);
+
  const { stdout: httpdVersion } = await exec("radicle-httpd", ["--version"]);
+
  // radicle-httpd outputs logging lines, extract just the version line (last line)
+
  const httpdVersionClean =
+
    httpdVersion.trim().split("\n").pop() || httpdVersion;
+
  console.log("");
+
  console.log("Binary versions:");
+
  console.log(`  rad: ${radVersion.trim()}`);
+
  console.log(`  git-remote-rad: ${gitRemoteRadVersion.trim()}`);
+
  console.log(
+
    `  radicle-httpd: ${httpdVersionClean}${useLocalHttpd ? " (local)" : ""}`,
+
  );
+
  console.log("");
+

  return async () => {
    await peerManager.shutdown();
  };
modified tests/support/heartwood-release
@@ -1 +1 @@
-
1.4.0

\ No newline at end of file
+
1.6.1
modified tests/support/radicle-httpd-release
@@ -1 +1 @@
-
0.20.0

\ No newline at end of file
+
0.24.0
modified tests/support/router.ts
@@ -5,6 +5,9 @@ import { expect } from "@tests/support/fixtures.js";
export const expectUrlPersistsReload = async (page: Page) => {
  const url = page.url();
  await page.reload();
+
  await page
+
    .getByRole("progressbar", { name: "App loading" })
+
    .waitFor({ state: "hidden" });
  await expect(page).toHaveURL(url);
};

modified tests/support/support.ts
@@ -25,15 +25,15 @@ export const tmpDir = Path.resolve(supportDir, "..", "./tmp");
export const fixturesDir = Path.resolve(supportDir, "..", "./fixtures");
const workspacePaths = [Path.join(tmpDir, "peers"), Path.join(tmpDir, "repos")];

-
export const heartwoodRelease = await Fs.readFile(
-
  `${supportDir}/heartwood-release`,
-
  "utf8",
-
);
+
export const heartwoodRelease = (
+
  await Fs.readFile(`${supportDir}/heartwood-release`, "utf8")
+
).trim();

-
export const radicleHttpdRelease = await Fs.readFile(
-
  `${supportDir}/radicle-httpd-release`,
-
  "utf8",
-
);
+
export const radicleHttpdRelease = (
+
  await Fs.readFile(`${supportDir}/radicle-httpd-release`, "utf8")
+
).trim();
+

+
export const useLocalHttpd = process.env.USE_LOCAL_HTTPD === "true";

// Assert that binaries are installed and are the correct version.
export async function assertBinariesInstalled(