diff --git a/README.md b/README.md index 8c52c7df2..fbbaaf329 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@
- The open-source thread-plane for agents. + The open-source thread-plane for Angular agents.
@@ -33,7 +33,7 @@ --- -**Threadplane is the open-source thread-plane for agents.** Chat, durable +**Threadplane is the open-source thread-plane for Angular agents.** Chat, durable threads, persistence, human approvals, tool progress, subagents, and generative UI — built on Angular Signals and dependency injection, for Angular 20–22. Your backend stays where it is: Threadplane adapts a LangGraph or AG-UI agent into a runtime-neutral diff --git a/apps/website/e2e/home-hero.spec.ts b/apps/website/e2e/home-hero.spec.ts index 1a9b97d23..b2f1bdd8c 100644 --- a/apps/website/e2e/home-hero.spec.ts +++ b/apps/website/e2e/home-hero.spec.ts @@ -24,20 +24,60 @@ test.describe('homepage hero', () => { await page.goto('/'); const demo = page.locator('[data-hero-demo]'); await expect(demo.locator('img')).toHaveAttribute('src', '/screenshots/hero-walkthrough-poster.webp'); - // The frame mounts on a 25%-visibility threshold, and the three-line H1 - // leaves the stage short of that at the default viewport. Scroll it in, as - // the mobile sibling below already does, so this asserts the mount and not - // the fold position. + // The frame mounts on a 25%-visibility threshold and the hero sits above + // it, so scroll it in — this asserts the mount, not the fold position. await demo.scrollIntoViewIfNeeded(); await expect(demo.locator('iframe')).toHaveAttribute('src', 'https://demo.threadplane.ai/hero'); }); - test('mobile shows Play walkthrough instead of the frame', async ({ page }) => { + test('the frame mounts on a phone without scrolling', async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await page.goto('/'); + // No scrollIntoViewIfNeeded, deliberately. Shortening the H1 lifted the demo + // to y=469 of this 844px fold, leaving ~44% of it visible on load — the 25% + // IntersectionObserver threshold is met without any scroll, and asserting + // the mount from a standing start is what proves it. + // + // This test still does NOT defend the fold; the budget below does. The 25% + // threshold is a proxy that only bites once the demo is pushed a long way + // down, and it moves whenever the stage's aspect ratio changes, so it is + // not a stable thing to reason about a fold with. Keep the two separate: + // this asserts that a phone autoplays, the budget asserts where the demo + // sits. const demo = page.locator('[data-hero-demo]'); - await demo.scrollIntoViewIfNeeded(); - await expect(demo.getByRole('button', { name: 'Play walkthrough' })).toBeVisible(); - await expect(demo.locator('iframe')).toHaveCount(0); + await expect(demo.locator('iframe')).toHaveAttribute('src', 'https://demo.threadplane.ai/hero'); + }); + + test('the hero fits the fold at 375x812', async ({ page }) => { + await page.setViewportSize({ width: 375, height: 812 }); + await page.goto('/'); + // Absolute budgets, not a comparison against whatever currently renders: a + // relative guard passes vacuously once the thing it guards drifts, which is + // how the four-line H1 and the sliced poster both shipped unnoticed. + // + // Three lines at 36px/1.08 measure 116.63px. line-height is pinned as a + // RATIO of font-size, so this height is quantized to 38.88px steps — 3 + // lines is 116.64, 4 lines is 155.52, and nothing lands in between. A + // webfont falling back changes where the text WRAPS, not the line box + // height, so any budget inside that gap behaves identically and there is no + // metric-variance flake surface. 130 sits in the gap. + const heading = page.locator('.hero-heading'); + await expect(heading).toBeVisible(); + const headingHeight = (await heading.boundingBox())!.height; + expect(headingHeight).toBeLessThanOrEqual(130); + + // Measure [data-hero-demo], the same element the 501.42px figure came from. + // .hero-demo-stage sits ~48px lower inside the BrowserFrame chrome, so + // budgeting the stage at this number would be a far tighter guard. + // + // 530, not 560: one extra H1 line adds 38.88px and lands at 540.3, which a + // 560 budget would wave through — leaving the heading budget as the only + // thing catching a taller hero. 530 trips on a single extra line by itself, + // and 28px of slack still absorbs sub-line drift in the eyebrow, subhead or + // CTA row. Measured: 501.42. + const demo = page.locator('[data-hero-demo]'); + await expect(demo).toBeVisible(); + const demoTop = (await demo.boundingBox())!.y; + expect(demoTop).toBeLessThanOrEqual(530); }); }); diff --git a/apps/website/e2e/website.spec.ts b/apps/website/e2e/website.spec.ts index 5bd65110c..e85e8514f 100644 --- a/apps/website/e2e/website.spec.ts +++ b/apps/website/e2e/website.spec.ts @@ -26,10 +26,12 @@ async function expectNoHorizontalOverflow( test('landing page renders hero headline', async ({ page }) => { await page.goto('/'); await expect(page.locator('#hero-heading')).toBeVisible(); - await expect(page.locator('#hero-heading')).toHaveText('The open-source thread-plane for agents.'); - // The tagline stopped naming Angular on 2026-09-06, so the guard that the - // hero says which framework this is moved to the eyebrow directly above it. - await expect(page.locator('.hero-eyebrow')).toContainText('Angular'); + // This also carries the guard that the hero names the framework. That guard + // used to live on .hero-eyebrow, because the H1 was framework-neutral; the H1 + // names Angular itself now, so the eyebrow no longer repeats it. Asserting + // "Angular" separately here would be green by construction — the literal + // above already contains it — so the full-text pin is the whole check. + await expect(page.locator('#hero-heading')).toHaveText('The open-source thread-plane for Angular agents.'); }); test('the default social card renders as a PNG', async ({ request }) => { diff --git a/apps/website/public/assets/hero.svg b/apps/website/public/assets/hero.svg index a25cc8f95..d17246672 100644 --- a/apps/website/public/assets/hero.svg +++ b/apps/website/public/assets/hero.svg @@ -1,4 +1,4 @@ -