Files
hanzo.ai/e2e/unit/recent.spec.ts
zeekay 6c65e3e941 @hanzo/app 0.2.0: the app SDK, renamed from @hanzo/rooms
Every import, the catalog pin and the docs name @hanzo/app and
github.com/hanzoai/app. lib/rooms.ts is lib/app.ts and
components/rooms.tsx is components/app.tsx.
2026-10-22 19:07:20 -07:00

105 lines
5.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, expect } from '@playwright/test'
import { ago, arrange, chats, PLAIN, runs, viewOf, type View } from '@hanzo/app/recent'
/**
* RECENTS IS ONE MODE'S LIST, ARRANGED BY A VIEW.
*
* Chat's rows come from its conversations and Dev's from its runs, never both.
* A view narrows by status and last activity, sorts, and groups; a kept view the
* mode's menu does not offer reads as the plain one.
*/
const now = new Date('2026-09-29T18:00:00Z')
const at = (hours: number) => new Date(now.getTime() - hours * 3_600_000).toISOString()
const runRows = runs([
{ id: 'r1', title: 'Add login', status: 'running', project: 'shop', createdAt: at(50), updatedAt: at(1) },
{ id: 'r2', title: 'Fix cart', status: 'done', repo: 'hanzoai/shop', createdAt: at(2), updatedAt: at(2) },
{ id: 'r3', title: 'Bump deps', status: 'error', project: '', createdAt: at(400), updatedAt: at(300) },
])
const chatRows = chats([
{ id: 'c1', title: 'Aurora', updatedAt: at(0.5) },
{ id: 'c2', title: 'Beacon', updatedAt: at(30) },
])
const view = (v: Partial<View>): View => ({ ...PLAIN, ...v })
const titles = (g: { title: string; rows: { title: string }[] }[]) => g.map((x) => [x.title, x.rows.map((r) => r.title)])
test('rows keep their mode: a chat is a chat and a run is a run', () => {
expect(chatRows.every((r) => r.kind === 'chat')).toBe(true)
expect(runRows.every((r) => r.kind === 'dev')).toBe(true)
expect(runRows.map((r) => r.project)).toEqual(['shop', 'shop', ''])
})
test('plain is one list, newest first, with no days', () => {
expect(titles(arrange(chatRows, PLAIN, now))).toEqual([['', ['Aurora', 'Beacon']]])
})
test('grouped by date, newest first, under the reader’s own days', () => {
const g = arrange(chatRows, view({ group: 'date' }), now)
expect(g.map((x) => x.title)).toEqual(['Today', 'Yesterday'])
expect(g[0]!.rows.map((r) => r.title)).toEqual(['Aurora'])
})
test('narrowed by status, by last activity and by words', () => {
expect(titles(arrange(runRows, view({ status: 'done', group: 'none' }), now))).toEqual([['', ['Fix cart']]])
expect(titles(arrange(runRows, view({ since: 'week', group: 'none' }), now))).toEqual([['', ['Add login', 'Fix cart']]])
})
test('grouped by state and by project, sorted by creation or title', () => {
expect(titles(arrange(runRows, view({ group: 'state' }), now))).toEqual([
['Running', ['Add login']],
['Done', ['Fix cart']],
['Failed', ['Bump deps']],
])
expect(titles(arrange(runRows, view({ group: 'project', sort: 'created' }), now))).toEqual([
['shop', ['Fix cart', 'Add login']],
['No project', ['Bump deps']],
])
expect(titles(arrange(runRows, view({ group: 'none', sort: 'title' }), now))).toEqual([['', ['Add login', 'Bump deps', 'Fix cart']]])
})
test('a pinned conversation comes first, under Pinned, whatever the grouping', () => {
const pinned = chats([
{ id: 'c1', title: 'Aurora', updatedAt: at(0.5) },
{ id: 'c2', title: 'Beacon', updatedAt: at(30), pinned: true },
])
expect(titles(arrange(pinned, view({ group: 'date' }), now))).toEqual([
['Pinned', ['Beacon']],
['Today', ['Aurora']],
])
expect(titles(arrange(pinned, PLAIN, now))).toEqual([
['Pinned', ['Beacon']],
['', ['Aurora']],
])
})
test('an archived conversation reads under the Archived status alone', () => {
const away = chats([{ id: 'c3', title: 'Comet', updatedAt: at(2), archived: true }])
expect(titles(arrange(away, view({ status: 'archived', group: 'none' }), now))).toEqual([['', ['Comet']]])
expect(viewOf('chat', { status: 'archived', group: 'starred' })).toEqual({ ...PLAIN, status: 'archived' })
})
test('a kept view reads only what the mode’s menu offers', () => {
expect(viewOf('chat', { status: 'done', group: 'state', sort: 'created', since: 'day' })).toEqual({ ...PLAIN, since: 'day' })
expect(viewOf('dev', { status: 'done', group: 'state', sort: 'created' })).toEqual({ status: 'done', since: 'any', group: 'state', sort: 'created' })
expect(viewOf('dev', 'garbage')).toEqual(PLAIN)
expect(viewOf('dev', null)).toEqual(PLAIN)
})
test('grouped by date and sorted by title, the days still read in order', () => {
expect(arrange(chatRows, view({ group: 'date', sort: 'title' }), now).map((g) => g.title)).toEqual(['Today', 'Yesterday'])
const old = chats([{ id: 'c3', title: 'Aardvark', updatedAt: at(900) }, { id: 'c1', title: 'Zed', updatedAt: at(1) }])
expect(arrange(old, view({ group: 'date', sort: 'title' }), now).map((g) => g.title)).toEqual(['Today', 'Older'])
})
test('a row says how long ago it was last active, in the fewest letters', () => {
const minutes = (m: number) => new Date(now.getTime() - m * 60_000).toISOString()
expect(['', 'soon'].map((x) => ago(x, now))).toEqual(['', ''])
expect([0, 0.5, 2, 12, 59].map((m) => ago(minutes(m), now))).toEqual(['now', 'now', '2m', '12m', '59m'])
expect([60, 180, 23 * 60 + 59].map((m) => ago(minutes(m), now))).toEqual(['1h', '3h', '23h'])
expect([1, 6, 7, 29, 30, 364, 365, 800].map((d) => ago(minutes(d * 1440), now))).toEqual(['1d', '6d', '1w', '4w', '1mo', '12mo', '1y', '2y'])
expect(ago(new Date(now.getTime() + 60_000).toISOString(), now), 'a clock a little ahead reads as now').toBe('now')
})