mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
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.
213 lines
9.3 KiB
TypeScript
213 lines
9.3 KiB
TypeScript
import { test, expect } from '@playwright/test'
|
||
import { apex, app, away, CHAT, dev, GET, guest, place, room, ROOMS, settings, SIGNIN, talk, team, WEB, WORKSPACE } from '@hanzo/app/lib/host'
|
||
import { landing, safe } from '@hanzo/app/lib/destination'
|
||
import { roomHref } from '@hanzo/app/rooms'
|
||
import { SECTIONS } from '@hanzo/build/route'
|
||
import { MOVED } from '../../lib/publish'
|
||
|
||
/**
|
||
* ONE EXPORT, TWO PRODUCTS, SPLIT BY HOST.
|
||
*
|
||
* hanzo.ai is the app — `/` once signed in, /dev, and /chat; hanzo.team is the workspace and keeps
|
||
* every room. `lib/host.ts` says which host is which and where a room asked for
|
||
* on the apex belongs, and a sign-in with nowhere to return lands on the host's
|
||
* own front room. `e2e/gates/host.spec.ts` drives the same answers in a browser
|
||
* under the real hostnames; this holds the mapping itself.
|
||
*/
|
||
|
||
test.describe('the app has one address', () => {
|
||
const ID = 'sess_0123456789abcdef0123456789abcdef'
|
||
|
||
test('Dev is `/dev`, and the path under it is what it shows; bare, it is New', () => {
|
||
expect(app('')).toBe('/dev')
|
||
expect(app(ID)).toBe(`/dev/run/${ID}`)
|
||
expect(app('hanzo/circle')).toBe('/dev/projects/hanzo/circle')
|
||
expect(app('my-site')).toBe('/dev/projects/my-site')
|
||
})
|
||
|
||
test('every kind of app address survives the path, both ways, and carries no query', () => {
|
||
// New, a run, a project, a screen, a place the builder files under Settings, a Customize tab, the plans, a site.
|
||
for (const at of ['', ID, 'hanzo/circle', '-/projects', '-/settings/machines', '-/customize', '-/customize/connectors', '-/plans', 'my-site']) {
|
||
const url = new URL(app(at), 'https://hanzo.ai')
|
||
expect(url.search, at || 'New').toBe('')
|
||
expect(place(url.pathname), at || 'New').toEqual({ mode: 'dev', at })
|
||
}
|
||
expect(place('/')).toBeNull()
|
||
expect(place('/developers')).toBeNull()
|
||
})
|
||
|
||
test('Settings is one tree at /settings, and no builder section is written under /dev', () => {
|
||
expect(app('-/settings')).toBe('/settings')
|
||
expect(app('-/settings/usage')).toBe('/settings/organization/usage')
|
||
expect(app('-/settings/billing')).toBe('/settings/organization/billing')
|
||
expect(app('-/settings/members')).toBe('/settings/organization/members')
|
||
for (const s of SECTIONS) expect(app(`-/settings/${s}`), s).not.toMatch(/^\/dev\/settings/)
|
||
for (const p of ['/settings', '/settings/organization/usage']) expect(place(p), p).toBeNull()
|
||
})
|
||
|
||
test('the edge answers every address once written under /dev/settings at the page Settings names for it', () => {
|
||
const rows = Object.entries(MOVED).filter(([from]) => from === '/dev/settings' || from.startsWith('/dev/settings/'))
|
||
for (const s of SECTIONS.filter((s) => app(`-/settings/${s}`).startsWith('/settings'))) expect(MOVED[`/dev/settings/${s}`], s).toBeDefined()
|
||
for (const [from, to] of rows) expect(to, from).toBe(settings(from === '/dev/settings' ? null : from.split('/').pop()))
|
||
})
|
||
|
||
test('Chat is `/chat`, a new conversation, and `/chat/<id>` one of the reader’s', () => {
|
||
expect(CHAT).toBe('/chat')
|
||
expect(talk()).toBe('/chat')
|
||
expect(talk(null)).toBe('/chat')
|
||
const id = '1790205245190697166-000014'
|
||
expect(talk(id)).toBe(`/chat/${id}`)
|
||
expect(place(talk(id))).toEqual({ mode: 'chat', thread: id })
|
||
expect(place('/chat')).toEqual({ mode: 'chat', thread: null })
|
||
// An id is carried as one segment, never as more of the address.
|
||
expect(talk('a/../../dev')).toBe('/chat/a%2F..%2F..%2Fdev')
|
||
expect(place(talk('a/../../dev'))).toEqual({ mode: 'chat', thread: 'a/../../dev' })
|
||
// A chat shared by link is a page of its own, not a conversation.
|
||
expect(place('/chat/shared')).toBeNull()
|
||
})
|
||
|
||
test('it is /dev, which every Dev link opens', () => {
|
||
expect(dev()).toBe('/dev')
|
||
expect(dev()).toBe(dev())
|
||
expect(dev(null)).toBe(dev())
|
||
expect(dev('')).toBe(dev())
|
||
})
|
||
|
||
test('a run, a project or a site is carried as its path when it is exactly one', () => {
|
||
expect(dev(ID)).toBe(`${dev()}/run/${ID}`)
|
||
expect(dev('hanzo/circle')).toBe(`${dev()}/projects/hanzo/circle`)
|
||
expect(dev('my-site')).toBe(`${dev()}/projects/my-site`)
|
||
expect(dev('a'.repeat(40))).toBe(`${dev()}/projects/${'a'.repeat(40)}`)
|
||
})
|
||
|
||
test('anything else opens the builder, and never another address', () => {
|
||
for (const ref of [
|
||
'a'.repeat(41),
|
||
'My-Site',
|
||
'-lead',
|
||
'trail-',
|
||
'sess_zz',
|
||
`${ID}0`,
|
||
'../x',
|
||
'//evil.com',
|
||
'https://evil.com',
|
||
'%2F%2Fevil.com',
|
||
'x?y=1',
|
||
'x#y',
|
||
'@evil.com',
|
||
'javascript:alert(1)',
|
||
])
|
||
expect(dev(ref), ref).toBe(dev())
|
||
})
|
||
})
|
||
|
||
test.describe('which host is which', () => {
|
||
test('the apex is hanzo.ai and www.hanzo.ai, and nothing that merely contains the name', () => {
|
||
expect(apex('hanzo.ai')).toBe(true)
|
||
expect(apex('www.hanzo.ai')).toBe(true)
|
||
for (const other of [
|
||
'hanzo.team',
|
||
'www.hanzo.team',
|
||
'localhost',
|
||
'127.0.0.1',
|
||
'cloud.hanzo.ai',
|
||
'api.hanzo.ai',
|
||
'hanzo-ai.hanzo.app',
|
||
'hanzo.ai.example.com',
|
||
'nothanzo.ai',
|
||
'',
|
||
])
|
||
expect(apex(other), other).toBe(false)
|
||
})
|
||
|
||
test('the workspace host is hanzo.team and www.hanzo.team', () => {
|
||
expect(team('hanzo.team')).toBe(true)
|
||
expect(team('www.hanzo.team')).toBe(true)
|
||
for (const other of ['hanzo.ai', 'www.hanzo.ai', 'localhost', '127.0.0.1', 'hanzo.team.example.com', ''])
|
||
expect(team(other), other).toBe(false)
|
||
})
|
||
})
|
||
|
||
test.describe('where a sign-in can finish', () => {
|
||
test('hanzo.app, every <slug>.hanzo.app alias and hanzo.chat are guests, and hand sign-in to hanzo.ai', () => {
|
||
expect(SIGNIN).toBe('https://hanzo.ai')
|
||
for (const host of ['hanzo.app', 'cloud-hanzo-ai.hanzo.app', 'hanzo-bot.hanzo.app', 'hanzo-ai.hanzo.app', 'hanzo.chat'])
|
||
expect(guest(host), host).toBe(true)
|
||
for (const own of ['hanzo.ai', 'cloud.hanzo.ai', 'hanzo.bot', 'hanzo.team', 'localhost', 'hanzo.app.example.com', 'nothanzo.app', 'chat.hanzo.chat', 'hanzo.chat.example.com', ''])
|
||
expect(guest(own), own).toBe(false)
|
||
})
|
||
|
||
test('on a guest every address is the same address on hanzo.ai', () => {
|
||
expect(away('hanzo-bot.hanzo.app', '/chat?q=hi')).toBe('https://hanzo.ai/chat?q=hi')
|
||
expect(away('cloud-hanzo-ai.hanzo.app', '/board')).toBe('https://hanzo.ai/board')
|
||
expect(away('hanzo.app', '/chat')).toBe('https://hanzo.ai/chat')
|
||
})
|
||
|
||
test('hanzo.app is where the app is got, and the app opens at hanzo.ai/chat', () => {
|
||
expect(GET).toBe('https://hanzo.app')
|
||
expect(WEB).toBe('https://hanzo.ai/chat')
|
||
})
|
||
})
|
||
|
||
test.describe('where a room belongs', () => {
|
||
test('the workspace keeps nine rooms, and the apex keeps the app', () => {
|
||
expect([...ROOMS].sort()).toEqual(
|
||
['/board', '/bots', '/cal', '/contacts', '/drive', '/home', '/inbox', '/meet', '/work'].sort(),
|
||
)
|
||
for (const kept of ['/chat', '/dev', '/settings', '/settings/organization/members']) expect(room(kept), kept).toBe(false)
|
||
})
|
||
|
||
test('an address names a room by its path, whatever it carries', () => {
|
||
expect(room('/board')).toBe(true)
|
||
expect(room('/board/')).toBe(true)
|
||
expect(room('/board?view=list')).toBe(true)
|
||
expect(room('/home#top')).toBe(true)
|
||
// A path that begins like a room is a different page.
|
||
expect(room('/boards')).toBe(false)
|
||
expect(room('/')).toBe(false)
|
||
expect(room('/pricing')).toBe(false)
|
||
expect(room('https://hanzo.team/board')).toBe(false)
|
||
})
|
||
|
||
test('on the apex a room is the same path and query on hanzo.team', () => {
|
||
expect(WORKSPACE).toBe('https://hanzo.team')
|
||
expect(away('hanzo.ai', '/board')).toBe('https://hanzo.team/board')
|
||
expect(away('www.hanzo.ai', '/inbox?from=mail')).toBe('https://hanzo.team/inbox?from=mail')
|
||
expect(away('hanzo.ai', roomHref('team:acme/r1'))).toBe('https://hanzo.team/?room=team%3Aacme%2Fr1')
|
||
expect(away('hanzo.ai', '/home')).toBe('https://hanzo.team/')
|
||
expect(away('hanzo.ai', '/board?project=x')).toBe('https://hanzo.team/board?project=x')
|
||
expect(away('hanzo.ai', '/homework')).toBe(null)
|
||
expect(away('hanzo.ai', '/chat')).toBeNull()
|
||
expect(away('hanzo.ai', '/dev?run=1')).toBeNull()
|
||
expect(away('hanzo.ai', '/pricing')).toBeNull()
|
||
})
|
||
|
||
test('every other host keeps the room where it is', () => {
|
||
for (const host of ['hanzo.team', 'localhost', '127.0.0.1', 'cloud.hanzo.ai'])
|
||
expect(away(host, '/board'), host).toBeNull()
|
||
})
|
||
|
||
test('a room key opens on Home, carried the way Home reads it', () => {
|
||
const href = roomHref('team:acme/r1')
|
||
expect(href).toBe('/home?room=team%3Aacme%2Fr1')
|
||
expect(new URL(href, 'https://hanzo.team').searchParams.get('room')).toBe('team:acme/r1')
|
||
})
|
||
})
|
||
|
||
test.describe('where a sign-in lands with nowhere to return', () => {
|
||
test('the apex lands on the app at `/`, every other host on Home', () => {
|
||
expect(landing('hanzo.ai')).toBe('/')
|
||
expect(landing('www.hanzo.ai')).toBe('/')
|
||
expect(landing('hanzo.team')).toBe('/home')
|
||
expect(landing('localhost')).toBe('/home')
|
||
})
|
||
|
||
test('a refused destination falls to the landing it is given, and a good one is kept', () => {
|
||
expect(safe(null, '/home')).toBe('/home')
|
||
expect(safe('//evil.example', '/home')).toBe('/home')
|
||
expect(safe('/login', '/home')).toBe('/home')
|
||
expect(safe('/pricing', '/home')).toBe('/pricing')
|
||
expect(safe('/board?x=1', '/chat')).toBe('/board?x=1')
|
||
})
|
||
})
|