Open rad: links in the app

This commit is contained in:
Rūdolfs Ošiņš
2026-10-05 17:37:03 +02:00
parent 91ab693b4b
commit 2f283f1047
13 changed files with 902 additions and 15 deletions
+120
View File
@@ -0,0 +1,120 @@
import type { Config } from "@bindings/config/Config";
import { describe, expect, test, vi } from "vitest";
import {
createLinkQueue,
deepLinkTarget,
parseDeepLink,
} from "@app/lib/deepLink";
const repo = "z3gqcJUoA1n9HaHKufZs5FCSGazv5";
const rid = `rad:${repo}`;
const oid = "5e4683023476778201ab25c87e1343fbfbe67c89";
const nid = "z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi";
const config = {
publicExplorer: "https://radicle.network/nodes/$host/$rid$path",
preferredSeeds: [],
} as unknown as Config;
const explorer = `https://radicle.network/nodes/rosa.radicle.network/${rid}`;
function target(link: string, local = true) {
return deepLinkTarget(parseDeepLink(link)!, local, config);
}
describe("parseDeepLink", () => {
test.each([rid, `rad:///${repo}`, ` ${rid}\n`])("accepts %j", link => {
expect(parseDeepLink(link)).toEqual({ type: "uri", uri: { repo } });
});
test.each([
`did:key:${nid}`,
"https://radicle.network",
`web+${rid}`,
"rad:bogus",
])("rejects %j", link => {
expect(parseDeepLink(link)).toBeUndefined();
});
});
describe("deepLinkTarget", () => {
test.each([
[rid, { resource: "repo.home", rid }],
[
`${rid}/cob/xyz.radicle.patch/${oid}`,
{
resource: "repo.patch",
rid,
patch: oid,
status: undefined,
reviewId: undefined,
},
],
[`${rid}/commit/${oid}`, { resource: "repo.commit", rid, commit: oid }],
[
`${rid}/commit/main?path=src/lib.rs#L10`,
{ resource: "repo.home", rid, revision: "main", path: "src/lib.rs" },
],
])("opens %s in-app when the repo is here", (link, route) => {
expect(target(link)).toEqual({ type: "route", route });
});
test("opens the explorer when the repo is not here", () => {
expect(target(`${rid}/cob/xyz.radicle.issue/${oid}`, false)).toEqual({
type: "external",
url: `${explorer}/issues/${oid}`,
});
});
test("opens the explorer for a page the app lacks", () => {
expect(target(`${rid}/cob/dev.radicle.artifact/${oid}`)).toEqual({
type: "external",
url: `${explorer}/releases/${oid}`,
});
});
test("goes nowhere when neither has a page", () => {
expect(target(`${rid}/tree/${oid}`)).toBeUndefined();
});
});
describe("createLinkQueue", () => {
function setup(fail?: string) {
const opened: string[] = [];
let running = 0;
let overlapped = false;
const queue = createLinkQueue(async link => {
running++;
overlapped ||= running > 1;
await new Promise(resolve => setTimeout(resolve, 1));
running--;
if (link === fail) throw new Error(link);
opened.push(link);
});
return { queue, opened, overlapped: () => overlapped };
}
const settle = () => new Promise(resolve => setTimeout(resolve, 20));
test("holds links until the app is ready", async () => {
const { queue, opened } = setup();
queue.receive(["a"]);
await settle();
expect(opened).toEqual([]);
queue.ready();
queue.receive(["b"]);
await settle();
expect(opened).toEqual(["a", "b"]);
});
test("opens links one at a time, past a failing one", async () => {
vi.spyOn(console, "error").mockImplementation(() => undefined);
const { queue, opened, overlapped } = setup("b");
queue.ready();
queue.receive(["a", "b"]);
queue.receive(["c"]);
await settle();
expect(opened).toEqual(["a", "c"]);
expect(overlapped()).toBe(false);
});
});