mirror of
https://github.com/hanzoai/build.git
synced 2026-10-23 02:18:08 +00:00
A sentence typed before signing in is still there after: New keeps what was unsent for the life of the page
Signing in swaps a visitor's page for the rail and draws New again, so the draft and its files wait in the document the popup kept. The rail's sign-in tests read the name the rail now shows, and refused storage refuses the page's own kept choices while the session stays. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
+23
-15
@@ -70,7 +70,7 @@ const nothing = (page: Page) => serve(page, () => undefined)
|
||||
const stored = (page: Page, key: string) => page.evaluate((k) => window.localStorage.getItem(k), key)
|
||||
|
||||
/** The account has arrived: the rail names who is signed in. The exchange and userinfo take a moment. */
|
||||
const arrived = (page: Page, who = 'dave@acme.test') => expect(page.getByRole('button', { name: `Account: ${who}` })).toBeVisible({ timeout: 20_000 })
|
||||
const arrived = (page: Page, who = 'Dave · acme') => expect(page.getByRole('button', { name: `Account: ${who}` })).toBeVisible({ timeout: 20_000 })
|
||||
|
||||
test.describe('signing in', () => {
|
||||
test('a popup signs in and the page, with what was typed, stays', async ({ page, baseURL }) => {
|
||||
@@ -79,7 +79,7 @@ test.describe('signing in', () => {
|
||||
await page.goto('/')
|
||||
const ask = page.getByRole('textbox', { name: 'Describe a task or ask a question' })
|
||||
await ask.fill('Add a cart to the shop')
|
||||
const [popup] = await Promise.all([page.waitForEvent('popup'), page.getByRole('button', { name: 'Account: Sign in' }).click()])
|
||||
const [popup] = await Promise.all([page.waitForEvent('popup'), page.getByRole('banner').getByRole('button', { name: 'Log in' }).click()])
|
||||
await popup.waitForEvent('close')
|
||||
await arrived(page)
|
||||
await expect(ask).toHaveValue('Add a cart to the shop')
|
||||
@@ -103,7 +103,7 @@ test.describe('signing in', () => {
|
||||
const asked = await issuer(page.context())
|
||||
await nothing(page)
|
||||
await page.goto('/-/codebases')
|
||||
await page.getByRole('button', { name: 'Account: Sign in' }).click()
|
||||
await page.getByRole('banner').getByRole('button', { name: 'Log in' }).click()
|
||||
await arrived(page)
|
||||
expect(page.url()).toBe(new URL('/', baseURL).href)
|
||||
expect(asked.authorize).toHaveLength(1)
|
||||
@@ -118,7 +118,7 @@ test.describe('signing in', () => {
|
||||
await issuer(page.context(), { token: { status: 400, json: { error: 'invalid_grant' } } })
|
||||
await nothing(page)
|
||||
await page.goto('/')
|
||||
await page.getByRole('button', { name: 'Account: Sign in' }).click()
|
||||
await page.getByRole('banner').getByRole('button', { name: 'Log in' }).click()
|
||||
await expect(page.getByText('Token exchange failed (400): {"error":"invalid_grant"}')).toBeVisible({ timeout: 20_000 })
|
||||
await expect(page.getByText('Try signing in again.')).toBeVisible()
|
||||
expect(await stored(page, 'hanzo_iam_access_token')).toBeNull()
|
||||
@@ -172,7 +172,7 @@ test.describe('signing in', () => {
|
||||
})
|
||||
await nothing(page)
|
||||
await page.goto('/')
|
||||
await page.getByRole('button', { name: 'Account: Sign in' }).click()
|
||||
await page.getByRole('banner').getByRole('button', { name: 'Log in' }).click()
|
||||
// The trip to hanzo.id and back loads the page twice.
|
||||
await expect(page.getByText('Completing sign-in…')).toBeVisible({ timeout: 20_000 })
|
||||
await expect(page.getByText('One moment.')).toBeVisible()
|
||||
@@ -196,7 +196,7 @@ test.describe('who is signed in', () => {
|
||||
return r.fulfill({ status: 204 })
|
||||
})
|
||||
await page.goto('/')
|
||||
await page.getByRole('button', { name: 'Account: dave@acme.test' }).click()
|
||||
await page.getByRole('button', { name: 'Account: Dave · acme' }).click()
|
||||
await page.getByRole('menuitem', { name: 'Log out' }).click()
|
||||
await expect.poll(() => ended.length).toBe(1)
|
||||
expect(ended[0]!.searchParams.get('post_logout_redirect_uri')).toBe(new URL('/', baseURL).href)
|
||||
@@ -215,7 +215,7 @@ test.describe('who is signed in', () => {
|
||||
})
|
||||
await nothing(page)
|
||||
await page.goto('/')
|
||||
await expect(page.getByRole('button', { name: 'Account: Sign in' })).toBeVisible()
|
||||
await expect(page.getByRole('banner').getByRole('button', { name: 'Log in' })).toBeVisible()
|
||||
expect(await stored(page, 'hanzo:who')).toBeNull()
|
||||
expect(await stored(page, 'hanzo.build.slack')).toBeNull()
|
||||
expect(await stored(page, 'hanzo.build.new.acme')).toBeNull()
|
||||
@@ -245,7 +245,7 @@ test.describe('who is signed in', () => {
|
||||
|
||||
test('an account hanzo.id names nothing about is still signed in', async ({ page }) => {
|
||||
await account(page, {})
|
||||
await expect(page.getByRole('button', { name: 'Account:', exact: true })).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: `Account: ${ORG}`, exact: true })).toBeVisible()
|
||||
await expect(page.getByLabel('Full name')).toHaveValue('')
|
||||
})
|
||||
})
|
||||
@@ -443,7 +443,7 @@ test.describe('a host that draws its own rail', () => {
|
||||
await runs(page)
|
||||
await page.route('**/v1/pref', (r) => r.fulfill({ json: { prefs: { theme: 'light' }, updatedAt: 1 } }))
|
||||
await page.goto(`${HOST}?at=-/settings`)
|
||||
await expect(page.getByText('How Hanzo Build looks and listens', { exact: false })).toBeVisible()
|
||||
await expect(page.getByText('How Hanzo looks and listens', { exact: false })).toBeVisible()
|
||||
// No theme to choose on this host, and the saved one is not forced on it.
|
||||
await expect(page.getByRole('button', { name: /^Theme: / })).toHaveCount(0)
|
||||
await expect(page.locator('html')).toHaveClass(/\bt_dark\b/)
|
||||
@@ -791,14 +791,22 @@ test.describe('the person’s own settings', () => {
|
||||
test('a browser that refuses storage keeps a choice for this page only', async ({ page }) => {
|
||||
const errors: string[] = []
|
||||
page.on('pageerror', (e) => errors.push(e.message))
|
||||
// Storage refuses the page's own kept choices; the session IAM keeps is still there, so the rail is.
|
||||
await page.addInitScript(() => {
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
get() {
|
||||
throw new DOMException('The operation is insecure.', 'SecurityError')
|
||||
},
|
||||
})
|
||||
const refuse = (key: string) => {
|
||||
if (key.startsWith('hanzo.build.')) throw new DOMException('The operation is insecure.', 'SecurityError')
|
||||
}
|
||||
const { getItem, setItem } = Storage.prototype
|
||||
Storage.prototype.getItem = function (key: string) {
|
||||
refuse(key)
|
||||
return getItem.call(this, key)
|
||||
}
|
||||
Storage.prototype.setItem = function (key: string, value: string) {
|
||||
refuse(key)
|
||||
setItem.call(this, key, value)
|
||||
}
|
||||
})
|
||||
await serve(page, () => undefined)
|
||||
await signIn(page, () => undefined)
|
||||
await page.goto('/')
|
||||
const rail = page.getByRole('navigation', { name: 'Runs' }).first()
|
||||
await expect(rail).toHaveAttribute('data-collapsed', 'false')
|
||||
|
||||
@@ -22,7 +22,7 @@ const SCREENS: [string, string, RegExp][] = [
|
||||
['run', '/sess_0992f90537264a6b154ebff799f38e1c', /Sign in to follow this run/],
|
||||
]
|
||||
|
||||
const BAR = ['Features', 'Resources', 'Enterprise', 'Pricing', 'Log in', 'Sign up']
|
||||
const BAR = ['Download', 'Resources', 'Enterprise', 'Pricing', 'Log in', 'Sign up']
|
||||
|
||||
/** The IAM client this host signs in as: hanzo.app is the Hanzo App's. */
|
||||
const client = (base: string) => (new URL(base).hostname === 'hanzo.app' ? 'hanzo-app' : 'hanzo-build')
|
||||
@@ -58,7 +58,7 @@ test('the header leads to the product’s pages, and both doors open IAM beside
|
||||
const bar = page.getByRole('banner')
|
||||
const site = bar.getByRole('navigation', { name: 'Site' })
|
||||
for (const [label, href] of [
|
||||
['Features', 'https://hanzo.ai/app'],
|
||||
['Download', '/#features'],
|
||||
['Enterprise', 'https://hanzo.ai/enterprise'],
|
||||
['Pricing', 'https://hanzo.ai/pricing'],
|
||||
]) {
|
||||
@@ -132,7 +132,7 @@ test('a phone gets every screen whole, with the header folded into one menu', as
|
||||
}
|
||||
await page.getByRole('button', { name: 'Menu' }).click()
|
||||
const menu = page.getByRole('menu', { name: 'Menu' })
|
||||
for (const label of ['Features', 'Enterprise', 'Pricing', 'Docs', 'Sign up']) await expect(menu.getByText(label, { exact: true })).toBeVisible()
|
||||
for (const label of ['Download', 'Enterprise', 'Pricing', 'Docs', 'Sign up']) await expect(menu.getByText(label, { exact: true })).toBeVisible()
|
||||
})
|
||||
|
||||
test('a run asks a visitor to sign in, not for an API key', async ({ page }) => {
|
||||
|
||||
@@ -85,7 +85,8 @@ test('on a phone the places fold into one menu, which leaves and signs in', asyn
|
||||
await bar.getByRole('button', { name: 'Menu' }).click()
|
||||
const menu = page.getByRole('menu', { name: 'Menu' })
|
||||
await page.screenshot({ path: info.outputPath('visit-phone-menu.png') })
|
||||
expect(await leaves(page, () => menu.getByText('Features', { exact: true }).click())).toBe('https://hanzo.ai/app')
|
||||
await menu.getByText('Download', { exact: true }).click()
|
||||
await expect(page.getByRole('heading', { name: 'Hanzo Dev' })).toBeVisible()
|
||||
await page.keyboard.press('Escape')
|
||||
|
||||
await bar.getByRole('button', { name: 'Menu' }).click()
|
||||
|
||||
+13
-2
@@ -57,6 +57,14 @@ const FIRST: Kept = { repo: null, branch: '', place: '', mode: 'build', model: E
|
||||
|
||||
const COLUMN = 768
|
||||
|
||||
/**
|
||||
* What was typed here and not sent, for as long as this document lives. Signing
|
||||
* in swaps a visitor's page for the rail, which draws New again in another
|
||||
* place; the popup keeps the document so the sentence can come along, and this
|
||||
* is where it waits.
|
||||
*/
|
||||
let unsent: { draft: string; files: Attached[] } = { draft: '', files: [] }
|
||||
|
||||
export function Landing({ onStarted }: { onStarted: (session: string) => void }) {
|
||||
const host = useHost()
|
||||
const t = useTarget()
|
||||
@@ -67,8 +75,11 @@ export function Landing({ onStarted }: { onStarted: (session: string) => void })
|
||||
const kept: Kept = stored ?? { ...FIRST, ...prefs.code }
|
||||
const set = (patch: Partial<Kept>) => keep({ ...kept, ...patch })
|
||||
|
||||
const [draft, setDraft] = useState(kept.ask || '')
|
||||
const [files, setFiles] = useState<Attached[]>([])
|
||||
const [draft, setDraft] = useState(kept.ask || unsent.draft)
|
||||
const [files, setFiles] = useState<Attached[]>(unsent.files)
|
||||
useEffect(() => {
|
||||
unsent = { draft, files }
|
||||
}, [draft, files])
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [note, setNote] = useState('')
|
||||
const [adding, setAdding] = useState<Source | null>(null)
|
||||
|
||||
+3
-3
@@ -71,14 +71,14 @@ export function Pitch({ onStart }: { onStart: () => void }) {
|
||||
Powered by Zen 6 · routed by Enso
|
||||
</SizableText>
|
||||
<SizableText role="heading" aria-level={2} size="$9" color="$ink" style={{ textAlign: 'center' }}>
|
||||
Bring Hanzo to your computer.
|
||||
Hanzo Dev
|
||||
</SizableText>
|
||||
<SizableText size="$4" color="$soft" maxW={620} style={{ textAlign: 'center' }}>
|
||||
The agent that builds here also runs in your terminal, on any codebase, in any language. Hand it a task, or open it and talk.
|
||||
The agent. It runs in your terminal, on any codebase, in any language. This page is that same agent in the browser.
|
||||
</SizableText>
|
||||
<Line text={INSTALL} />
|
||||
<SizableText size="$2" color="$soft" fontFamily="$mono">
|
||||
then hanzo "fix the failing test", or hanzo code
|
||||
dev opens it. dev "fix the failing test" hands it a task.
|
||||
</SizableText>
|
||||
<a href={DOCS} style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||
<XStack items="center" height={34} px="$3" rounded="$3" borderWidth={1} borderColor="$borderColor" hoverStyle={{ bg: '$hover' }}>
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@
|
||||
* The page to a visitor: the product's header over New.
|
||||
*
|
||||
* Signed out there are no runs to list, so the rail gives way to a header —
|
||||
* Features (under New, pitch.tsx), Resources, Enterprise, Pricing — and the two
|
||||
* Download (Hanzo Dev, under New, pitch.tsx), Resources, Enterprise, Pricing — and the two
|
||||
* doors in, which are one door: IAM's page offers both. New stays the page, and
|
||||
* sending from it asks the visitor to sign in with the sentence kept
|
||||
* (landing.tsx). Below md the four places fold into one menu beside Log in.
|
||||
@@ -18,7 +18,7 @@ import { useHost } from './host.tsx'
|
||||
const SITE = 'https://hanzo.ai'
|
||||
|
||||
export const PLACES = [
|
||||
{ label: 'Features', href: '/#features' },
|
||||
{ label: 'Download', href: '/#features' },
|
||||
{ label: 'Enterprise', href: `${SITE}/enterprise` },
|
||||
{ label: 'Pricing', href: `${SITE}/pricing` },
|
||||
] as const
|
||||
|
||||
Reference in New Issue
Block a user