mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
/enterprise's "What the same tokens cost" offered only the price list's featured models, a hand-kept list (hanzoai/pricing featuredModelIds) that had fallen behind: eleven older models, and none of a lab's newer ones however long the price list had listed them. The model picker now offers every paid, priced model of the labs, at list price (a :batch row is the same model discounted, so it is left out). The featured ones lead under "Featured", dearest first, and the page still opens on the first of them; every other follows under its lab, A to Z, so a new model is offered the day the price list lists it. The featured set itself is refreshed in hanzoai/pricing. The calculator gate holds it: the featured model under Featured and chosen, and a model the price list does not feature still offered under its lab.
180 lines
10 KiB
TypeScript
180 lines
10 KiB
TypeScript
import { expect, test, type Page } from '@playwright/test'
|
||
import { readFileSync } from 'node:fs'
|
||
import { join } from 'node:path'
|
||
import { serveExport } from './export'
|
||
import { block, serve } from './api'
|
||
import { api } from './revision'
|
||
import { infrastructureLogos } from '../../lib/constants/partner-logos'
|
||
import { FIELD, PRICED } from './field'
|
||
import { toPricing } from '../../lib/api/read'
|
||
import { derive, stat } from '../../lib/enso'
|
||
|
||
/**
|
||
* /ENTERPRISE ASKS FOR ONE THING: A CALL WITH SALES.
|
||
*
|
||
* The page's one loud control is "Talk to sales", at the top and at the end,
|
||
* and both open /contact-sales; the closing call the marketing layout mounts
|
||
* elsewhere is quiet here. Where Enterprise runs is one picture of four stops.
|
||
* The proof is the research runs, each opening its board on /benchmarks, and
|
||
* the data room's studies; a run the API does not hold leaves its figure out
|
||
* while the studies stay. No certification is named. Measured on the shipped
|
||
* bytes at a phone, a laptop and a wide screen.
|
||
*/
|
||
|
||
let site: Awaited<ReturnType<typeof serveExport>>
|
||
test.beforeAll(async () => {
|
||
site = await serveExport()
|
||
})
|
||
test.afterAll(async () => {
|
||
await site.close()
|
||
})
|
||
|
||
/** The page is no wider than the window, and the window is the width asked for. */
|
||
async function fits(page: Page, width: number) {
|
||
const [scroll, client] = await page.evaluate(() => [document.documentElement.scrollWidth, document.documentElement.clientWidth])
|
||
expect(client, 'the viewport applied').toBe(width)
|
||
expect(scroll, 'no sideways scroll').toBeLessThanOrEqual(client)
|
||
}
|
||
|
||
const ENSO = stat(derive(FIELD, toPricing(PRICED)!)!)!
|
||
|
||
for (const [width, height] of [
|
||
[390, 844],
|
||
[1440, 900],
|
||
[1920, 1080],
|
||
] as const) {
|
||
test(`at ${width}: one call to sales, four places to run, and proof from the records`, async ({ page }) => {
|
||
await page.setViewportSize({ width, height })
|
||
await serve(page, { '/v1/pricing': PRICED })
|
||
await page.route(api, (r) => r.fulfill({ json: { data: FIELD, total: FIELD.length } }))
|
||
await page.goto(`${site.url}/enterprise`, { waitUntil: 'domcontentloaded' })
|
||
|
||
await expect(page.getByRole('heading', { level: 1 })).toHaveText(/Give every team AI\.\s*Run it where you choose\./)
|
||
const calls = page.locator('#main').getByRole('link', { name: 'Talk to sales' })
|
||
await expect(calls).toHaveCount(2)
|
||
for (const a of await calls.all()) await expect(a).toHaveAttribute('href', '/contact-sales')
|
||
// The only filled controls on the page are those two.
|
||
await expect(page.locator('#main [data-fill]')).toHaveCount(2)
|
||
await expect(page.getByText('Build what’s next')).toHaveCount(0)
|
||
// The first call is above the fold.
|
||
expect((await calls.first().boundingBox())!.y + 56, 'Talk to sales on the first screen').toBeLessThan(height)
|
||
|
||
await expect(page.locator('[data-slot="deploy"] li')).toHaveCount(4)
|
||
for (const stop of ['Hanzo Cloud', 'Your cloud', 'Your servers', 'Air-gapped']) await expect(page.locator('[data-slot="deploy"]')).toContainText(stop)
|
||
|
||
const runs = page.locator('[data-slot="proof-runs"] a')
|
||
await expect(runs.first()).toBeVisible({ timeout: 20_000 })
|
||
const enso = page.locator('[data-slot="proof-runs"] a[href="/benchmarks#enso"]')
|
||
await expect(enso).toContainText(ENSO.figure)
|
||
for (const a of await runs.all()) await expect(a).toHaveAttribute('href', /^\/benchmarks#[a-z-]+$/)
|
||
const studies = page.locator('[data-slot="proof-studies"] a')
|
||
await expect(studies).toHaveCount(6)
|
||
for (const a of await studies.all()) await expect(a).toHaveAttribute('href', /^\/customers(\/|#)[a-z-]+$/)
|
||
|
||
await expect(page.getByText('How is Enterprise priced?')).toBeVisible()
|
||
expect(await page.locator('#main').innerText(), 'no certification is named').not.toMatch(/SOC ?2|ISO ?27001|HIPAA|FedRAMP/i)
|
||
await fits(page, width)
|
||
})
|
||
}
|
||
|
||
test('the savings calculator prices the same tokens at list, on Hanzo Cloud and on your own GPUs, from the served rates', async ({ page }) => {
|
||
// The served answers (e2e/gates/api.ts), and the sums written here by hand from them:
|
||
// Claude Opus 4.6 at $6.1 / $29, 500M in and 50M out: 3,050 + 1,450 = $4,500 at list
|
||
// Enso Flash at $3 / $9: 1,500 + 450 = 1,950, plus 50 members of Team at $27 = 1,350: $3,300, 27% less
|
||
// 1x H100 at $4.44 an hour, 730 hours: $3,241, 28% less; even with list at 396.1 million tokens, 151 a second
|
||
// the same model as /v1/models bills it, $6.4 / $30.6: 3,200 + 1,530 = $4,730
|
||
await page.setViewportSize({ width: 1440, height: 900 })
|
||
await serve(page)
|
||
await page.goto(`${site.url}/enterprise#savings`, { waitUntil: 'domcontentloaded' })
|
||
const calc = page.locator('[data-slot="savings"]')
|
||
await expect(calc).toBeVisible({ timeout: 20_000 })
|
||
// Each field stands below the one before it: the model's choice never lies over the tokens.
|
||
const [model, input] = await Promise.all([calc.locator('#model-0').boundingBox(), calc.locator('#in-0').boundingBox()])
|
||
expect(model!.y + model!.height, 'the model above the tokens').toBeLessThanOrEqual(input!.y - 8)
|
||
// Every model the price list prices is offered: the featured ones first, and the
|
||
// page opens on the dearest of them; every other under its lab.
|
||
const picker = calc.locator('#model-0')
|
||
await expect(picker.locator('optgroup[label="Featured"] option')).toHaveText(['Claude Opus 4.6'])
|
||
await expect(picker.locator('optgroup[label="OpenAI"] option'), 'a model the price list does not feature is still offered').toHaveText(['GPT-5'])
|
||
await expect(picker).toHaveValue('anthropic/claude-opus-4.6')
|
||
await calc.locator('#sku').selectOption('enso-flash')
|
||
const list = calc.locator('[data-figure="list"]')
|
||
const cloud = calc.locator('[data-figure="cloud"]')
|
||
const own = calc.locator('[data-figure="gpus"]')
|
||
await expect(list).toContainText('$4,500')
|
||
await expect(list).toContainText('Claude Opus 4.6 at $6.1 in and $29 out per million tokens')
|
||
await expect(cloud).toContainText('$3,300')
|
||
await expect(cloud.locator('[data-saving]')).toHaveText('27% less than list')
|
||
await expect(cloud).toContainText('50 members on Team at $27 a member a month: $1,350')
|
||
await expect(cloud).toContainText('at the rates our model list states: $4,730 a month')
|
||
await expect(own).toContainText('$3,241')
|
||
await expect(own.locator('[data-saving]')).toHaveText('28% less than list')
|
||
await expect(own).toContainText('1 × 1x H100 on all month (730 hours) at $4.44 an hour')
|
||
await expect(own).toContainText('at 396.1 million tokens a month at your mix, about 151 tokens a second')
|
||
await expect(own).toContainText('not in this figure')
|
||
// The reader's own GPU hour moves only the GPU figure.
|
||
await calc.locator('#hour').fill('2')
|
||
await expect(own).toContainText('$1,460')
|
||
await expect(list).toContainText('$4,500')
|
||
// A spend at a share of output buys the tokens that cost exactly that at list.
|
||
await calc.getByRole('radio', { name: 'Spend' }).click()
|
||
await calc.locator('#spend-0').fill('5000')
|
||
await calc.locator('#share-0').fill('10')
|
||
await expect(list).toContainText('$5,000')
|
||
})
|
||
|
||
test('with the price list down the calculator says so and states no figure', async ({ page }) => {
|
||
await block(page)
|
||
await page.goto(`${site.url}/enterprise#savings`, { waitUntil: 'domcontentloaded' })
|
||
await expect(page.locator('#savings [data-api="unreachable"]')).toHaveCount(1, { timeout: 20_000 })
|
||
await expect(page.locator('[data-slot="savings"]')).toHaveCount(0)
|
||
})
|
||
|
||
test('partners are the programmes the plan list sells, and no company is called a partner', async ({ page }) => {
|
||
await serve(page)
|
||
await page.goto(`${site.url}/enterprise#partners`, { waitUntil: 'domcontentloaded' })
|
||
const ways = page.locator('[data-slot="partners"]')
|
||
await expect(ways.getByRole('link', { name: /affiliate programme/ })).toHaveAttribute('href', '/affiliate')
|
||
// Every cloud and silicon vendor Hanzo deploys on, Nebius among them since 7b573631.
|
||
await expect(page.locator('[data-slot="runs-on"] img')).toHaveCount(infrastructureLogos.length)
|
||
for (const l of infrastructureLogos) await expect(page.locator(`[data-slot="runs-on"] img[alt="${l.name}"]`)).toHaveCount(1)
|
||
await expect(page.locator('#partners')).toContainText('not partners or customers')
|
||
})
|
||
|
||
test('a run the API does not hold leaves its figure out, and the studies stay', async ({ page }) => {
|
||
await serve(page)
|
||
await page.route(api, (r) => r.abort('connectionrefused'))
|
||
await page.goto(`${site.url}/enterprise`, { waitUntil: 'domcontentloaded' })
|
||
await expect(page.locator('[data-slot="proof-studies"] a')).toHaveCount(6)
|
||
await expect(page.locator('#proof [data-api="loading"]')).toHaveCount(0, { timeout: 20_000 })
|
||
await expect(page.locator('[data-slot="proof-runs"]')).toHaveCount(0)
|
||
})
|
||
|
||
test('/contact sends sales to /contact-sales and Enterprise to /enterprise', async ({ page }) => {
|
||
await page.goto(`${site.url}/contact`, { waitUntil: 'domcontentloaded' })
|
||
const sales = page.locator('[data-slot="contact-sales"]')
|
||
await expect(sales.getByRole('link', { name: 'Talk to sales' })).toHaveAttribute('href', '/contact-sales')
|
||
await expect(sales.getByRole('link', { name: 'See Enterprise' })).toHaveAttribute('href', '/enterprise')
|
||
await expect(page.locator('[data-slot="contact-teams"]').getByRole('link', { name: 'Get support' })).toHaveAttribute('href', '/support')
|
||
await fits(page, page.viewportSize()!.width)
|
||
})
|
||
|
||
test('the share card claims nothing a page may not, and the sales page shares it', () => {
|
||
// A share card is a picture, so no text gate reads it: its words are read
|
||
// where they are written. The /enterprise card once said SOC 2 Type II,
|
||
// HIPAA, ISO 27001 and a 99.99% uptime SLA, none of them held.
|
||
const cards = readFileSync('scripts/generate-og.mjs', 'utf8')
|
||
expect(cards, 'no certification on any card').not.toMatch(/SOC ?2|HIPAA|ISO ?27001|FedRAMP|PCI[- ]DSS/i)
|
||
expect(cards, 'no availability figure, which is contractual').not.toMatch(/\d+(\.\d+)?\s?%\s*(<[^>]+>\s*)*(<span[^>]*>)?\s*Uptime/i)
|
||
for (const page of ['enterprise.html', 'contact-sales.html']) {
|
||
const html = readFileSync(join('out', page), 'utf8')
|
||
expect(html, `${page} shares the Enterprise card`).toMatch(/<meta property="og:image" content="[^"]*\/og\/enterprise\.png(\?[^"]*)?"/)
|
||
}
|
||
})
|
||
|
||
test('the sales contact a machine reads is the one the sales page shows', () => {
|
||
const html = readFileSync(join('out', 'index.html'), 'utf8')
|
||
expect(html).not.toMatch(/(sales|solutions)@hanzo\.ai/)
|
||
expect(html).toContain('"contactType":"sales","email":"[email protected]","url":"https://hanzo.ai/contact-sales"')
|
||
})
|