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

The Asked tab reads "Asked 3" while three asks have not landed #6779

Merged opened this

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

Merged

scout/forge:scout/6745-the-asked-tab-reads-asked-3-while-three → main 1 commit fork

  • Merged. scout/6745-the-asked-tab-reads-asked-3-while-three was merged into main as 67e5c5c.

Review

Sign in to review this change.
5 files changed +104 −9
src/web/pages/apps.ts +2 −2
@@ -64,7 +64,7 @@ import { html, when, type Html } from "../html.ts";
6464 import { flashTo, page, redirect } from "../layout.ts";
6565 import { writableAppsOf } from "../studio/asks.ts";
6666 import { costLine } from "../studio/cost_line.ts";
67 import { homeTabs } from "../studio/home_tabs.ts";
67 import { homeTabs, unshippedAsks } from "../studio/home_tabs.ts";
6868 import { shelfRows } from "../studio/shelf.ts";
6969 import {
7070 badge,
@@ -214,7 +214,7 @@ export async function appsList(ctx: Ctx): Promise<Response> {
214214 costLine(ctx.app.db, writableAppsOf(ctx, ctx.auth.user).repos),
215215 );
216216 const rendering = performance.now();
217 const body = html`${when(ctx.auth.user, () => homeTabs("apps"))}
217 const body = html`${when(ctx.auth.user, (user) => homeTabs("apps", unshippedAsks(ctx.app.db, user)))}
218218 ${pageHead("Apps", { actions: create })}
219219 ${cost}
220220 ${filters}
src/web/studio/asked_page.ts +1 −1
@@ -204,7 +204,7 @@ export function askedPage(ctx: Ctx): Response {
204204 ? "Nothing asked yet"
205205 : `${asked} asked · ${shipped} shipped${flagged > 0 ? ` · ${flagged} with nothing built` : ""}`;
206206
207 const body = html`${homeTabs("asked")}
207 const body = html`${homeTabs("asked", unshipped)}
208208 <h1 id="asked-sentence">What you asked for</h1>
209209 <p class="muted">${sentence}</p>
210210 ${thoughts}
src/web/studio/home.ts +3 −2
@@ -61,7 +61,7 @@ import { archivedAppCount } from "../pages/apps.ts";
6161 import { pinnedPanels } from "./pinned.ts";
6262 import { isNavigation, lastVisit, noteVisit } from "../../core/home_visit.ts";
6363 import { sinceYouWere } from "./since.ts";
64 import { homeTabs } from "./home_tabs.ts";
64 import { homeTabs, unshippedAsks } from "./home_tabs.ts";
6565 import { crewKeyNote } from "./crew.ts";
6666 import { everydayCard } from "../pages/everyday.ts";
6767 import { nextStepsCard, nextStepsOf } from "../pages/next-steps.ts";
@@ -280,6 +280,7 @@ export async function studioHome(
280280 isNavigation(ctx.request) ? noteVisit(ctx.app.db, user.id, visitedAt) : lastVisit(ctx.app.db, user.id),
281281 );
282282 const since = timePhase(ctx.request, "since", () => sinceYouWere(ctx.app.db, user, before, visitedAt));
283 const asking = timePhase(ctx.request, "asked", () => unshippedAsks(ctx.app.db, user));
283284 const ask = firstSteps ?? everydayCard(ctx, user);
284285 const figures = user.isAdmin && steps === undefined;
285286 const ceilingUsd = forgeCeiling(ctx.app.config);
@@ -301,7 +302,7 @@ export async function studioHome(
301302 </p>`,
302303 )}
303304 ${since}
304 ${homeTabs("feed")}
305 ${homeTabs("feed", asking)}
305306 <div class="split" data-rail="wide">
306307 <div class="stack">
307308 ${composer(ctx, user)}
src/web/studio/home_tabs.ts +13 −4
@@ -4,21 +4,30 @@
44 * logo away (#599, #851). Drawn with §5's tabs.
55 */
66
7 import type { Database } from "bun:sqlite";
8 import { askedCounts, askedRepos } from "../../core/asked.ts";
9 import type { User } from "../../core/users.ts";
710 import { html, type Html } from "../html.ts";
811 import { tabs } from "../ui.ts";
912
1013 export type HomeTab = "feed" | "apps" | "asked";
1114
12 /** §5's `tabs()` in a `.mb` block: link tabs, so `aria-current`. */
13 export function homeTabs(current: HomeTab): Html {
14 const item = (id: HomeTab, href: string, label: string) => ({
15 /** The asks not yet shipped across the viewer's repositories: `/asked`'s own number. */
16 export function unshippedAsks(db: Database, viewer: User): number {
17 return askedCounts(db, askedRepos(db, viewer, null)).unshipped;
18 }
19
20 /** §5's `tabs()` in a `.mb` block: link tabs, so `aria-current`; Asked wears the unshipped count. */
21 export function homeTabs(current: HomeTab, unshipped = 0): Html {
22 const item = (id: HomeTab, href: string, label: string, count?: number) => ({
1523 label,
24 ...(count === undefined ? {} : { count }),
1625 href,
1726 current: current === id,
1827 });
1928 return html`<div class="mb">
2029 ${tabs(
21 [item("feed", "/", "Feed"), item("apps", "/apps", "Your apps"), item("asked", "/asked", "Asked")],
30 [item("feed", "/", "Feed"), item("apps", "/apps", "Your apps"), item("asked", "/asked", "Asked", unshipped)],
2231 "Home",
2332 )}
2433 </div>`;
test/asked-tab-count.test.ts added +85
@@ -0,0 +1,85 @@
1 /** The Asked tab wears the number of unshipped asks, on `/`, `/apps` and `/asked`; none reads plain `Asked` (#6745). */
2 import { afterAll, beforeAll, expect, test } from "bun:test";
3 import { mkdtempSync, rmSync } from "node:fs";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { createRepo } from "../src/actions/repos.ts";
7 import { createApp, type App } from "../src/app.ts";
8 import { createIssue } from "../src/core/issues.ts";
9 import { createUser } from "../src/core/users.ts";
10 import { serve, type Servers } from "../src/http/server.ts";
11 import { homeTabs } from "../src/web/studio/home_tabs.ts";
12 import { proveAll } from "./helpers/verified.ts";
13 import { quietLogs } from "./helpers/globals.ts";
14
15 quietLogs();
16
17 let root = "";
18 let app: App;
19 let servers: Servers;
20 let cookie = "";
21 let repoId = "";
22 let userId = "";
23
24 beforeAll(async () => {
25 root = mkdtempSync(join(tmpdir(), "forge-asked-tab-"));
26 app = createApp({ dataDir: root, port: 0, forgeRepo: "alice/forge" });
27 const u = await createUser(app.db, {
28 origin: "seed",
29 trusted: true,
30 username: "alice",
31 email: "[email protected]",
32 password: "a-good-passphrase",
33 adminIfFirst: true,
34 });
35 if (!u.ok) throw new Error(u.error);
36 proveAll(app);
37 const made = await createRepo(app, { id: u.value.id, username: "alice" } as never, { name: "r" });
38 if (!made.ok) throw new Error(made.error);
39 repoId = made.value.id;
40 userId = u.value.id;
41 servers = await serve(app);
42 const signin = await fetch(`${servers.url}/signin`, {
43 method: "POST",
44 headers: { origin: servers.url },
45 body: new URLSearchParams({ username: "alice", password: "a-good-passphrase" }),
46 redirect: "manual",
47 });
48 cookie = (signin.headers.get("set-cookie") ?? "").split(";")[0]!;
49 });
50 afterAll(() => {
51 servers.stop();
52 app.close();
53 rmSync(root, { recursive: true, force: true });
54 });
55
56 /** The Asked tab's inner text, tags stripped. */
57 async function askedTab(path: string): Promise<string> {
58 const res = await fetch(`${servers.url}${path}`, { headers: { cookie } });
59 const page = await res.text();
60 const nav = page.match(/<nav class="tabs" aria-label="Home"[\s\S]*?<\/nav>/)?.[0] ?? "";
61 const m = nav.match(/<a[^>]*href="\/asked"[^>]*>([\s\S]*?)<\/a\s*>/);
62 if (!m) throw new Error(`no Asked tab on ${path}`);
63 return m[1]!.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
64 }
65
66 test("homeTabs reads plain Asked at 0 or when not given, and the count otherwise", () => {
67 expect(String(homeTabs("feed"))).not.toContain("pill");
68 expect(String(homeTabs("feed", 0))).not.toContain("pill");
69 expect(String(homeTabs("feed", 3))).toContain("pill");
70 });
71
72 test("with no asks the Asked tab is plain on all three pages", async () => {
73 for (const path of ["/?all=1", "/apps", "/asked"]) expect(await askedTab(path)).toBe("Asked");
74 });
75
76 test("three unshipped asks read Asked 3 on `/`, `/apps` and `/asked`", async () => {
77 for (let i = 0; i < 3; i++) {
78 const ask = createIssue(app.db, { repoId, authorId: userId, title: `Ask ${i}` });
79 if (!ask.ok) throw new Error(ask.error);
80 }
81 for (const path of ["/?all=1", "/apps", "/asked"]) expect(await askedTab(path)).toBe("Asked 3");
82 const res = await fetch(`${servers.url}/?all=1`, { headers: { cookie } });
83 await res.arrayBuffer();
84 expect(res.headers.get("server-timing") ?? "").toContain("asked;");
85 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs