Public: anyone can read this; the crew can change it.
A search that matches nothing anywhere says so once and offers Explore #8625
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/8484-a-search-that-matches-nothing-anywhere-s → main 1 commit fork
2 files
changed +58
−13
src/web/pages/search.ts +29 −8
| @@ -296,7 +296,18 @@ function section( | ||
| 296 | 296 | >`} |
| 297 | 297 | </h2> |
| 298 | 298 | ${rows.length === 0 |
| 299 | ? empty({ icon: emptyIcon, title: emptyText }) | |
| 299 | ? empty({ | |
| 300 | icon: emptyIcon, | |
| 301 | title: emptyText, | |
| 302 | ...(a.kind === "all" | |
| 303 | ? {} | |
| 304 | : { | |
| 305 | action: btn("Search everything", { | |
| 306 | href: searchHref(a, { kind: "all" }), | |
| 307 | size: "sm", | |
| 308 | }), | |
| 309 | }), | |
| 310 | }) | |
| 300 | 311 | : listCard(rows)} |
| 301 | 312 | ${after} |
| 302 | 313 | ${when( |
| @@ -483,6 +494,9 @@ export async function searchPage(ctx: Ctx): Promise<Response> { | ||
| 483 | 494 | </p>`, |
| 484 | 495 | ); |
| 485 | 496 | |
| 497 | const nothing = | |
| 498 | a.kind === "all" && | |
| 499 | found.repos.length + found.issues.length + found.code.length === 0; | |
| 486 | 500 | const body = html`${pageHead(html`Results for “${a.query}”`)} ${form} |
| 487 | 501 | <div class="bar mb">${chips}</div> |
| 488 | 502 | ${liveToggle} |
| @@ -503,13 +517,20 @@ export async function searchPage(ctx: Ctx): Promise<Response> { | ||
| 503 | 517 | shortly. |
| 504 | 518 | </p>`, |
| 505 | 519 | )} |
| 506 | ${WORDS | |
| 507 | .filter( | |
| 508 | ([kind]) => kind !== "all" && (a.kind === "all" || a.kind === kind), | |
| 509 | ) | |
| 510 | .map((w) => | |
| 511 | section(a, w, ...rows[w[0]](), w[0] === "code" ? moreCode : null), | |
| 512 | )}`; | |
| 520 | ${nothing | |
| 521 | ? empty({ | |
| 522 | icon: "search", | |
| 523 | title: `Nothing matches “${a.query}”`, | |
| 524 | note: "Search reads app and repository names and descriptions, issues, pull requests and the code you can see.", | |
| 525 | action: btn("Explore", { href: "/explore", size: "sm" }), | |
| 526 | }) | |
| 527 | : WORDS | |
| 528 | .filter( | |
| 529 | ([kind]) => kind !== "all" && (a.kind === "all" || a.kind === kind), | |
| 530 | ) | |
| 531 | .map((w) => | |
| 532 | section(a, w, ...rows[w[0]](), w[0] === "code" ? moreCode : null), | |
| 533 | )}`; | |
| 513 | 534 | const response = page({ ctx, title: `Search · ${a.query}`, body }); |
| 514 | 535 | notePhase(ctx.request, "render", performance.now() - rendering); |
| 515 | 536 | return response; |
test/search-more-code.test.ts +29 −5
| @@ -85,11 +85,35 @@ test("a roomy budget shows every row and offers nothing more", async () => { | ||
| 85 | 85 | expect(html).not.toContain("data-more-code"); |
| 86 | 86 | }); |
| 87 | 87 | |
| 88 | test("a search with no hits draws the empty state, not .no-hits", async () => { | |
| 88 | test("a search with no hits in any kind says so once and offers Explore", async () => { | |
| 89 | 89 | const html = await page(roomy, "q=qqqxnothingmatchesqqq"); |
| 90 | 90 | expect(html).not.toContain("no-hits"); |
| 91 | expect(html).toContain("No app or repository matches"); | |
| 92 | expect(html).toContain("No issue or pull request matches"); | |
| 93 | expect(html).toContain("No line of code matches"); | |
| 94 | expect(html).toContain("empty-title"); | |
| 91 | expect(html).toContain("Nothing matches “qqqxnothingmatchesqqq”"); | |
| 92 | expect(html).toContain("code you can see"); | |
| 93 | expect(html).toContain('href="/explore"'); | |
| 94 | expect(html).not.toContain("No app or repository matches"); | |
| 95 | expect(html).not.toContain('id="repos"'); | |
| 96 | expect(html).not.toContain('id="code"'); | |
| 97 | }); | |
| 98 | ||
| 99 | test("the empty state escapes the query", async () => { | |
| 100 | const html = await page(roomy, "q=" + encodeURIComponent("<b>qqzz</b>")); | |
| 101 | expect(html).toContain("Nothing matches “<b>qqzz</b>”"); | |
| 102 | expect(html).not.toContain("<b>qqzz</b>"); | |
| 103 | }); | |
| 104 | ||
| 105 | test("a narrowed kind with no match keeps its empty state and offers everything", async () => { | |
| 106 | const html = await page(roomy, "q=qqqxnothingmatchesqqq&kind=code"); | |
| 107 | expect(html).toContain("No line of code matches"); | |
| 108 | expect(html).not.toContain("Nothing matches"); | |
| 109 | expect(html).toContain('href="/search?q=qqqxnothingmatchesqqq"'); | |
| 110 | expect(html).toContain("Search everything"); | |
| 111 | }); | |
| 112 | ||
| 113 | test("one result in any kind keeps the sections", async () => { | |
| 114 | const html = await page(roomy, "q=zebrafish"); | |
| 115 | expect(html).not.toContain("Nothing matches"); | |
| 116 | expect(html).toContain('id="repos"'); | |
| 117 | expect(html).toContain("No app or repository matches"); | |
| 118 | expect(html).toContain('id="code"'); | |
| 95 | 119 | }); |
new above
offline