Public: anyone can read this; the crew can change it.

The pill joins the other side's origin to this page's path, so a `//` path stays on the app #6278

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/6269-the-pill-joins-the-other-side-s-origin-t → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/6269-the-pill-joins-the-other-side-s-origin-t was merged into main as 84558b4.

Review

Sign in to review this change.
2 files changed +110 −3
src/apps/toolbar.ts +19 −3
@@ -72,10 +72,11 @@ export const toolbarJs = `(function () {
7272 if (d.env === "production") line("Production");
7373 else if (d.pull) line("Preview of pull #" + d.pull);
7474 if (d.title) line(d.title);
75 var LS = "color:#8ab4ff;text-decoration:none;display:flex;align-items:center;min-height:44px";
7576 var link = document.createElement("a");
7677 link.textContent = "Home";
7778 link.rel = "noopener";
78 link.style.cssText = "color:#8ab4ff;text-decoration:none;display:flex;align-items:center;min-height:44px";
79 link.style.cssText = LS;
7980 try {
8081 var target = new URL(home);
8182 if (target.origin === new URL(me.src).origin) link.href = target.href;
@@ -247,10 +248,25 @@ export const toolbarJs = `(function () {
247248 ta.target = "_blank";
248249 ta.rel = "noopener";
249250 ta.href = t.href;
250 ta.style.cssText = "color:#8ab4ff;text-decoration:none;display:flex;align-items:center;min-height:44px";
251 ta.style.cssText = LS;
251252 crew.appendChild(ta);
252253 }
253254 }
255 // The other side: origin plus this path as text, so "//" cannot leave it.
256 var sd = c.side;
257 if (sd && typeof sd.label === "string" && typeof sd.url === "string") {
258 try {
259 var so = new URL(sd.url);
260 if (so.protocol === "https:" || so.protocol === location.protocol) {
261 var sa = document.createElement("a");

Fine as is. Consider adding aria-label (label + " (other side)") so screen-reader users know it leaves this origin.

262 sa.textContent = sd.label;
263 sa.rel = "noopener";
264 sa.href = so.origin + location.pathname + location.search;
265 sa.style.cssText = LS;
266 crew.appendChild(sa);
267 }
268 } catch (e) {}
269 }
254270 if (typeof c.who !== "string" || typeof c.url !== "string") return;
255271 var u = new URL(c.url);
256272 if (u.origin !== forge) return;
@@ -258,7 +274,7 @@ export const toolbarJs = `(function () {
258274 a.textContent = c.who + " is working on this";
259275 a.rel = "noopener";
260276 a.href = u.href;
261 a.style.cssText = "color:#8ab4ff;text-decoration:none;display:flex;align-items:center;min-height:44px";
277 a.style.cssText = LS;
262278 crew.appendChild(a);
263279 })
264280 .catch(function () {});
test/apps-pill-side.test.ts added +91
@@ -0,0 +1,91 @@
1 /**
2 * The pill's link to the other side (#6269): its origin joined to this page's
3 * path and query as text, so a `//` path stays on the other side; drawn only
4 * for an https `side.url` or one on the page's own scheme; nothing without a side.
5 */
6
7 import { describe, expect, test } from "bun:test";
8 import { toolbarJs } from "../src/apps/toolbar.ts";
9
10 type El = {
11 tag: string;
12 children: El[];
13 style: Record<string, string>;
14 dataset?: Record<string, string>;
15 // eslint-disable-next-line @typescript-eslint/no-explicit-any
16 [k: string]: any;
17 };
18
19 function el(tag: string): El {
20 const e: El = {
21 tag,
22 children: [],
23 style: {},
24 appendChild(c: El) { e.children.push(c); return c; },
25 setAttribute() {},
26 addEventListener() {},
27 attachShadow() { return e.appendChild(el("root")); },
28 };
29 return e;
30 }
31
32 function walk(e: El, out: El[] = []): El[] {
33 out.push(e);
34 for (const c of e.children) walk(c, out);
35 return out;
36 }
37
38 async function run(side: unknown, loc: { protocol: string; pathname: string; search: string }) {
39 const doc = {
40 ...el("document"),
41 currentScript: {
42 src: "https://forge.test/assets/toolbar.js",
43 dataset: { app: "lab", env: "production" },
44 removeAttribute() {},
45 },
46 createElement: el,
47 body: el("body"),
48 } as unknown as El;
49 const g = globalThis as Record<string, unknown>;
50 const saved = { document: g.document, location: g.location, fetch: g.fetch, window: g.window };
51 const win = { innerWidth: 800, innerHeight: 600, localStorage: { getItem: () => null, setItem() {} } };
52 g.window = win;
53 g.document = doc;
54 g.location = { ...loc, origin: "https://app.test", href: "https://app.test" + loc.pathname + loc.search };
55 g.fetch = async () => ({ ok: true, json: async () => ({ side }) });
56 try {
57 new Function("window", "document", "location", "fetch", toolbarJs)(win, doc, g.location, g.fetch);
58 await new Promise((r) => setTimeout(r, 5));
59 return walk(doc.body)
60 .filter((e) => e.tag === "a" && e.href && e.textContent === "Other")
61 .map((e) => e.href as string);
62 } finally {
63 Object.assign(g, saved);
64 }
65 }
66
67 const https = { protocol: "https:", pathname: "/a/b", search: "?x=1" };
68
69 describe("the pill's other side", () => {
70 test("joins the other origin to this path and query", async () => {
71 expect(await run({ label: "Other", url: "https://other.test/ignored?y=2" }, https)).toEqual([
72 "https://other.test/a/b?x=1",
73 ]);
74 });
75
76 test("a // path stays on the other side's origin", async () => {
77 const [href] = await run({ label: "Other", url: "https://other.test/" }, { ...https, pathname: "//evil.test/x", search: "" });
78 expect(new URL(href!).origin).toBe("https://other.test");
79 });
80
81 test("no side, no link", async () => {
82 expect(await run(undefined, https)).toEqual([]);
83 });
84
85 test("http is drawn only on an http page", async () => {
86 expect(await run({ label: "Other", url: "http://other.test" }, https)).toEqual([]);
87 expect(await run({ label: "Other", url: "http://other.test" }, { ...https, protocol: "http:" })).toEqual([
88 "http://other.test/a/b?x=1",
89 ]);
90 });
91 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs