inbox-search.test.ts
Language TypeScript5.7 KB140 lines
/*** Live Gmail search.** The 30-day window is several times what the board holds, with the busiest* mailbox alone in the thousands. Holding all of it is megabytes re-parsed on* every click, and nobody finds an email by scrolling thousands of rows. So the* board keeps the recent slice for browsing and lets the search box reach* everything.** That makes the search the thing standing between "we might be missing an* important email" and not. What is tested here is the part that decides what a* typed query is allowed to reach, and the part that refuses to imply a result* set is complete when it is not.*/import { describe, expect, it } from 'vitest'import { SEARCH_QUERY_CAP, buildSearchQuery } from '../../../config/inbox.js'import { INBOX_MAILBOXES } from '../../../config/inbox.js'import { SEARCH_RESULT_CAP, searchResultsPage, type SearchHit } from '../src/inbox-view.js'import type { InboxMessage } from '@meridian/gmail-client'const msg = (over: Partial<InboxMessage> = {}): InboxMessage => ({id: 'm1',threadId: 't1',mailbox: 'hello@meridiandisplay.example',sender: 'client@example.com',senderName: 'A Client',subject: 'Banner proof',snippet: 'preview',date: '2026-09-04T18:00:00.000Z',labels: ['INBOX'],unread: false,...over,})const hit = (over: Partial<SearchHit> = {}): SearchHit => ({mailbox: 'hello@meridiandisplay.example',ok: true,error: null,messages: [msg()],truncated: false,...over,})describe('buildSearchQuery', () => {it('drops the 30-day window, which is the whole point', () => {// The footer has always said "older mail is in Gmail, not gone". This is// the board finally reaching it.expect(buildSearchQuery('proof')).not.toContain('newer_than')})it('forces in:inbox, so a search cannot widen what the board shows', () => {// Archived mail, Sent, Spam and Trash have never been on this board, and a// search box is not where that should quietly change.expect(buildSearchQuery('proof')).toBe('in:inbox proof')})it('removes a typed in: or label:, rather than combining with it', () => {// `in:inbox in:anywhere` is Gmail's syntax for widening, and a reader// typing it would not know they had. Stripped, not merged.for (const q of ['in:anywhere proof', 'proof in:trash', 'label:spam proof', 'proof -in:inbox']) {const built = buildSearchQuery(q)expect(built.startsWith('in:inbox '), q).toBe(true)expect(built.slice('in:inbox '.length), q).not.toMatch(/\b(in|label):/)}})it('leaves every other Gmail operator alone, because they are the feature', () => {// These are the searcher's own words in Gmail's own language, not a second// query language invented here.const q = buildSearchQuery('from:jordan@expohall.example has:attachment "feather flag"')expect(q).toContain('from:jordan@expohall.example')expect(q).toContain('has:attachment')expect(q).toContain('"feather flag"')})it('caps the length rather than passing an unbounded string to Gmail', () => {expect(buildSearchQuery('x'.repeat(5_000)).length).toBeLessThanOrEqual(SEARCH_QUERY_CAP + 'in:inbox '.length,)})it('an empty query still scopes to the inbox rather than matching everything', () => {expect(buildSearchQuery(' ')).toBe('in:inbox')})})describe('the results page', () => {it('names a mailbox that could not be searched, and says matches are missing', () => {// Six results and a named failure, never a short list that looks complete.const html = searchResultsPage('proof', [hit(),hit({ mailbox: 'accounts@meridiandisplay.example', ok: false, error: 'invalid_grant', messages: [] }),], INBOX_MAILBOXES, null)expect(html).toContain('invalid_grant')expect(html).toContain('could not be searched')expect(html).toContain('not absent')})it('says when more matched than it is showing', () => {// Rule 5: capped and labelled, never capped and silent.const html = searchResultsPage('proof', [hit({ truncated: true })], INBOX_MAILBOXES, null)expect(html).toContain(String(SEARCH_RESULT_CAP))expect(html).toContain('More matched than are shown')})it('distinguishes an empty RESULT from an unloaded one', () => {// The distinction the whole feature turns on: Gmail was asked and said no,// which is not the same as the board not holding it.const html = searchResultsPage('nothing', [hit({ messages: [] })], INBOX_MAILBOXES, null)expect(html).toContain('empty result rather than an unloaded one')})it('states the corpus, both halves of it', () => {const html = searchResultsPage('proof', [hit()], INBOX_MAILBOXES, null)expect(html).toContain('past the 30 days')expect(html).toContain('Sent, Spam and Trash are not searched')})it('carries no read, filter or replied controls', () => {// A row here that silently lacked a replied chip would read as "not// replied" rather than "not known"; the search reaches past the board's// own record of what it holds.const html = searchResultsPage('proof', [hit()], INBOX_MAILBOXES, null)expect(html).not.toContain('action="/inbox/read"')expect(html).not.toContain('msg-replied')expect(html).toContain('not known')})it('escapes the query, which is user input rendered back', () => {const html = searchResultsPage('<img src=x onerror=alert(1)>', [hit()], INBOX_MAILBOXES, null)expect(html).not.toContain('<img src=x')expect(html).toContain('<img')})it('says which mailbox it searched when scoped to one', () => {const html = searchResultsPage('proof', [hit()], INBOX_MAILBOXES, 'hello@meridiandisplay.example')expect(html).toContain('Sam Whitaker')})})