Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -679,23 +679,29 @@ jobs:
# Spec §8: scroll-craft's own harness walks the homepage's pinned stage
# act and fails on dead scroll outside the declared hold and on cues that
# never peak. The Nx build lands in dist/apps/website with a rewritten
# next.config, which only `nx serve --configuration=production` can
# start, and the serve's own build dependency is the one build here:
# it runs with --skip-nx-cache (the earlier e2e builds wrote no cache
# entry either) so it emits dist rather than restoring it. The content
# next.config, which `nx serve --configuration=production` starts.
# Rebuild explicitly with the paired stage origin: serve reuses the
# prior output, whose public-copy build points at production. The content
# symlink, which mirrors playwright.config.ts's production mode, is
# added only after the server answers, so it lands on the dist the
# build has finished writing rather than on one it is about to replace.
- name: Stage scroll verification (scroll-craft harness)
env:
GROWTH_FORM_POLICY: growth_v1
SCROLLCRAFT_CHROME: /usr/bin/google-chrome
NEXT_PUBLIC_STAGE_DEMO_ORIGIN: http://localhost:4200
run: |
command -v ffmpeg > /dev/null || (sudo apt-get update && sudo apt-get install -y ffmpeg)
# Verify this commit's recording, not the older production demo.
(npx nx serve examples-chat-angular --configuration=production --port=4200 > /tmp/stage-demo.log 2>&1 &)
for i in $(seq 1 60); do curl -sf http://localhost:4200/stage > /dev/null && break; sleep 2; done
curl -sf http://localhost:4200/stage > /dev/null || { cat /tmp/stage-demo.log; exit 1; }
npx nx build website --configuration=production --skip-nx-cache
(npx nx serve website --configuration=production --port=4308 --skip-nx-cache > /tmp/next-start.log 2>&1 &)
for i in $(seq 1 60); do curl -sf http://127.0.0.1:4308/ > /dev/null && break; sleep 2; done
curl -sf http://127.0.0.1:4308/ > /dev/null || { cat /tmp/next-start.log; exit 1; }
ln -sfn ../../../apps/website/content dist/apps/website/content
STAGE_LIVE_FRAME=true BASE_URL=http://127.0.0.1:4308 npx playwright test apps/website/e2e/home-stage.spec.ts --config apps/website/playwright.config.ts
node apps/website/e2e/scroll-craft/verify-home.mjs --url http://127.0.0.1:4308 --out dist/stage-shots
- name: Upload stage contact sheets
if: always()
Expand Down
204 changes: 177 additions & 27 deletions apps/website/e2e/home-stage.spec.ts

Large diffs are not rendered by default.

Binary file modified apps/website/public/screenshots/stage-approve-mobile.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/website/public/screenshots/stage-approve.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/website/public/screenshots/stage-persist-mobile.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/website/public/screenshots/stage-persist.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/website/public/screenshots/stage-render-mobile.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/website/public/screenshots/stage-render.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/website/public/screenshots/stage-stream-mobile.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/website/public/screenshots/stage-stream.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
72 changes: 39 additions & 33 deletions apps/website/src/components/landing/Stage.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {

/** Stands in for `STAGE_PROOF`: the page derives these from the recording. */
const PROOF: Record<StageBeat, string> = {
subagents: '2 specialists',
stream: '312 events · 1 tool call · 3 sources',
persist: 'reloaded · 10 checkpoints · forked at step 1',
approve: '1 interrupt pending · checkpoint 10 of 10',
Expand All @@ -31,7 +32,8 @@ vi.mock('../ui/BrowserFrame', () => ({
),
}));

function mockViewport(width: number, reducedMotion: boolean) {
function mockViewport(width: number, reducedMotion: boolean, height = 900) {
Object.defineProperty(window, 'innerHeight', { configurable: true, value: height });
Object.defineProperty(window, 'innerWidth', {
configurable: true,
value: width,
Expand Down Expand Up @@ -79,22 +81,29 @@ describe('Stage', () => {
mockViewport(390, false);
render(<Stage proof={PROOF} />);
await flush();
expect(screen.getAllByTestId('stage-still-beat')).toHaveLength(4);
expect(screen.getAllByTestId('stage-still-beat')).toHaveLength(5);
expect(document.querySelector('[data-stage-act]')).toBeNull();
// The real stills, carrying the same proof and the ledger ending.
expect(
document.querySelector(
'[data-testid="stage-still-beat"][data-beat="stream"] [data-stage-proof]'
)!.textContent
).toBe(PROOF.stream);
expect(document.querySelector('[data-stage-proof]')).toBeNull();
for (const still of screen.getAllByTestId('stage-still-beat')) {
expect(still.firstElementChild?.className).toBe('stage-still-text');
}
expect(document.querySelectorAll('.stage-check')).toHaveLength(5);
expect(screen.getByTestId('stage-stills-close')).toBeTruthy();
});

it('keeps stills on desktop windows too short for the pinned stage', async () => {
mockViewport(1280, false, 600);
render(<Stage proof={PROOF} />);
await flush();
expect(screen.getAllByTestId('stage-still-beat')).toHaveLength(5);
expect(document.querySelector('[data-stage-act]')).toBeNull();
});

it('keeps the stills under reduced motion on a wide viewport', async () => {
mockViewport(1440, true);
render(<Stage proof={PROOF} />);
await flush();
expect(screen.getAllByTestId('stage-still-beat')).toHaveLength(4);
expect(screen.getAllByTestId('stage-still-beat')).toHaveLength(5);
expect(document.querySelector('[data-stage-act]')).toBeNull();
});

Expand All @@ -118,37 +127,36 @@ describe('Stage', () => {
const actEl = document.querySelector('[data-stage-act]');
expect(actEl).not.toBeNull();
expect(actEl?.getAttribute('data-sc-act')).toBe('pin');
expect(actEl?.getAttribute('data-sc-span')).toBe('6');
expect(actEl?.getAttribute('data-sc-span')).toBe('7.3');
expect(actEl?.getAttribute('data-state')).toBe('mounting');
expect(actEl?.querySelector('[data-sc-stage]')).not.toBeNull();
// The rail: the segment bar, four beat blocks stacked in one cell, one
// hold line, and the closing ledger.
const act = actEl!;
expect(act.querySelectorAll('[data-stage-segment]')).toHaveLength(4);
expect(act.querySelectorAll('[data-stage-segment]')).toHaveLength(5);
expect(
[...act.querySelectorAll('[data-stage-segment]')].map(
(s) => s.textContent
)
).toEqual(['Tools', 'Persist', 'Approve', 'Render']);
expect(
act.querySelectorAll('[data-testid="stage-rail-beat"]')
).toHaveLength(4);
// One check per beat block, four in the ledger.
expect(act.querySelectorAll('[data-stage-check]')).toHaveLength(4 + 4);
).toEqual([
'Tools & citations',
'Subagents',
'Threads & branches',
'Interrupts & approval',
'Generated UI',
]);
expect(act.querySelectorAll('[data-stage-check]')).toHaveLength(5);
expect(act.querySelector('[data-stage-proof]')).toBeNull();
expect(act.querySelectorAll('.stage-ledger')).toHaveLength(0);
expect(
act.querySelector('[data-testid="stage-rail-hold"]')!.textContent
).toBe('Keep scrolling to approve.');
expect(
act.querySelector('[data-testid="stage-rail-close"]')
).not.toBeNull();
expect(
act.querySelector('[data-testid="stage-rail-close"]')!.textContent
).toContain('Feature complete for the final mile.');
).toContain('One workflow. Every step in your Angular app.');
expect(
act.querySelector(
'[data-testid="stage-rail-beat"][data-beat="stream"] [data-stage-proof]'
)!.textContent
).toBe(PROOF.stream);
screen.getByRole('heading', {
name: 'Everything your agent needs on screen.',
})
).toBeTruthy();
expect(screen.queryAllByTestId('stage-still-beat')).toHaveLength(0);
// The engine collects acts with root.querySelectorAll('[data-sc-act]'),
// which matches descendants only — so the mount root must contain the act
Expand All @@ -164,13 +172,11 @@ describe('Stage', () => {
expect(skip?.getAttribute('href')).toBe('#stage-end');
expect(document.getElementById('stage-end')).not.toBeNull();
const segments = actEl!.querySelectorAll('a.stage-seg');
expect(segments).toHaveLength(4);
expect(segments).toHaveLength(5);
segments.forEach((a) => expect(a.hasAttribute('tabindex')).toBe(false));
const cueLinks = actEl!.querySelectorAll(
'.stage-rail-beat a, .stage-rail-close a'
);
expect(cueLinks).toHaveLength(4 + 4 + 1);
cueLinks.forEach((a) => expect(a.getAttribute('tabindex')).toBe('-1'));
const docsLinks = actEl!.querySelectorAll('.stage-rail-beat .stage-doc');
expect(docsLinks).toHaveLength(5);
docsLinks.forEach((a) => expect(a.hasAttribute('tabindex')).toBe(false));
// Each segment's href resolves to its beat block, so the anchor works
// even when the click handler does not run.
for (const b of STAGE_BEATS) {
Expand Down
4 changes: 3 additions & 1 deletion apps/website/src/components/landing/Stage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import { StageAct } from './StageAct';
import type { StageBeat } from '../../lib/stage-beats';

export const STAGE_MIN_WIDTH = 1024;
/** The full checklist and install action need this much vertical space. */
export const STAGE_MIN_HEIGHT = 720;
type Mode = 'stills' | 'act';

function actAllowed(): boolean {
if (typeof window === 'undefined') return false;
if (window.innerWidth < STAGE_MIN_WIDTH) return false;
if (window.innerWidth < STAGE_MIN_WIDTH || window.innerHeight < STAGE_MIN_HEIGHT) return false;
return !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}

Expand Down
Loading
Loading