-
Notifications
You must be signed in to change notification settings - Fork 0
feat: живая витрина showcase + чистка нейрослопа #286
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
87c62a1
1e39487
68d939c
20f02bc
82a666b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,3 +20,4 @@ playwright-report | |
| blob-report | ||
| .playwright | ||
| browser/.artifacts | ||
| site/dist | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,288 @@ | ||||||||||||||||||||||
| import type { Page } from '@playwright/test'; | ||||||||||||||||||||||
| import { expect, test } from './fixtures/harness'; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const SHOWCASE = '/site/dist/index.html'; | ||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Description: Confirm that the Playwright setup builds site/dist before the browser suite runs.
set -uo pipefail
echo "== package.json scripts =="
fd -H -t f 'package.json' -d 2 --exec sh -c 'echo "--- {} ---"; jq -r ".scripts // {} | to_entries[] | \"\(.key): \(.value)\"" {}'
echo "== playwright config: webServer / globalSetup / baseURL =="
fd -H -t f 'playwright*.config.*' --exec sh -c 'echo "--- {} ---"; cat -n {}'
echo "== harness fixture =="
fd -H -t f 'harness.ts' browser --exec cat -n
echo "== CI workflows referencing site:build or playwright =="
rg -n 'site:build|site:preview|playwright|site/dist' .github --glob '*.y*ml' || echo "no matches in .github"
echo "== is site/dist ignored? =="
rg -n 'site/dist|^site/' .gitignore || echo "no site entries in .gitignore"Repository: Labpics-Team/lab-motion Length of output: 8992 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== root scripts =="
jq -r '.scripts // {} | to_entries[] | "\(.key): \(.value)"' package.json
echo "== browser workflow =="
cat -n .github/workflows/browser.yml | sed -n '1,110p'
echo "== site build references =="
rg -n 'site:build|site/dist|dist/index.html|vite|build' package.json site browser .github --glob '!site/dist/**' --glob '!node_modules/**' | head -200
echo "== showcase test and server implementation =="
cat -n browser/20-showcase.spec.ts
cat -n browser/fixtures/server.mjsRepository: Labpics-Team/lab-motion Length of output: 27364 Build the showcase before running Playwright. The workflow runs 🤖 Prompt for AI Agents |
||||||||||||||||||||||
| const EXAMPLE = `import { animate } from '@labpics/motion/animate'; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| await animate('.card', { x: 240, opacity: 1 }, { | ||||||||||||||||||||||
| spring: { mass: 1, stiffness: 170, damping: 26 }, | ||||||||||||||||||||||
| stagger: 40, | ||||||||||||||||||||||
| }).finished;`; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| function watchRuntimeFailures(page: Page): string[] { | ||||||||||||||||||||||
| const failures: string[] = []; | ||||||||||||||||||||||
| page.on('pageerror', (error) => failures.push(`pageerror: ${error.message}`)); | ||||||||||||||||||||||
| page.on('console', (message) => { | ||||||||||||||||||||||
| if (message.type() === 'error') failures.push(`console.error: ${message.text()}`); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| page.on('requestfailed', (request) => failures.push(`requestfailed: ${request.url()}`)); | ||||||||||||||||||||||
| page.on('response', (response) => { | ||||||||||||||||||||||
| if (response.status() >= 400) failures.push(`http ${response.status()}: ${response.url()}`); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| return failures; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| function translation(page: Page, selector: string): Promise<{ x: number; y: number }> { | ||||||||||||||||||||||
| return page.locator(selector).evaluate((element) => { | ||||||||||||||||||||||
| const matrix = new DOMMatrixReadOnly(getComputedStyle(element).transform); | ||||||||||||||||||||||
| return { x: matrix.m41, y: matrix.m42 }; | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| test('showcase renders every proof and settles without runtime failures', async ({ page }) => { | ||||||||||||||||||||||
| const failures = watchRuntimeFailures(page); | ||||||||||||||||||||||
| await page.goto(SHOWCASE); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| await expect(page).toHaveTitle(/Lab Motion/); | ||||||||||||||||||||||
|
Check failure on line 36 in browser/20-showcase.spec.ts
|
||||||||||||||||||||||
| await expect(page.getByRole('heading', { name: 'Motion that survives interruption.' })).toBeVisible(); | ||||||||||||||||||||||
| await expect(page.getByRole('img', { name: 'Live spring preview' })).toBeVisible(); | ||||||||||||||||||||||
| await expect(page.getByRole('img', { name: 'Spring animation preview' })).toBeVisible(); | ||||||||||||||||||||||
| await expect(page.getByRole('img', { name: 'Staggered dots animation preview' })).toBeVisible(); | ||||||||||||||||||||||
| await expect(page.getByRole('img', { name: 'Retargetable object animation preview' })).toBeVisible(); | ||||||||||||||||||||||
| await expect(page.locator('.stage-coordinate')).toHaveCount(0); | ||||||||||||||||||||||
| await expect(page.locator('[data-card="spring"] [data-state]')).toHaveText('complete'); | ||||||||||||||||||||||
| await expect(page.locator('[data-card="stagger"] [data-state]')).toHaveText('complete'); | ||||||||||||||||||||||
| await expect(page.getByRole('status')).toHaveCount(3); | ||||||||||||||||||||||
| await expect.poll(() => translation(page, '[data-preview="spring-object"]')).toEqual({ x: 112, y: 0 }); | ||||||||||||||||||||||
| expect(failures).toEqual([]); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| test('spring Replay starts a new observed trajectory and reaches its endpoint', async ({ page }) => { | ||||||||||||||||||||||
| const failures = watchRuntimeFailures(page); | ||||||||||||||||||||||
| await page.goto(SHOWCASE); | ||||||||||||||||||||||
| const state = page.locator('[data-card="spring"] [data-state]'); | ||||||||||||||||||||||
| await expect(state).toHaveText('complete'); | ||||||||||||||||||||||
|
Check failure on line 54 in browser/20-showcase.spec.ts
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const before = await translation(page, '[data-preview="spring-object"]'); | ||||||||||||||||||||||
| await page.locator('[data-action="replay-spring"]').click(); | ||||||||||||||||||||||
| await expect(state).toHaveText('running'); | ||||||||||||||||||||||
| await expect.poll(async () => (await translation(page, '[data-preview="spring-object"]')).x).not.toBe(before.x); | ||||||||||||||||||||||
| await expect(state).toHaveText('complete'); | ||||||||||||||||||||||
| await expect.poll(async () => (await translation(page, '[data-preview="spring-object"]')).x).toBeCloseTo(112, 1); | ||||||||||||||||||||||
|
Comment on lines
+57
to
+61
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win The transient Line 58 asserts the text Assert the reachable end state instead, or accept both intermediate values. 🧪 Proposed change for Line 58 await page.locator('[data-action="replay-spring"]').click();
- await expect(state).toHaveText('running');
+ await expect(state).toHaveText(/running|complete/);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
| expect(failures).toEqual([]); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| test('stagger Replay exposes delayed visual ordering before all items settle', async ({ page }) => { | ||||||||||||||||||||||
| const failures = watchRuntimeFailures(page); | ||||||||||||||||||||||
| await page.goto(SHOWCASE); | ||||||||||||||||||||||
| const state = page.locator('[data-card="stagger"] [data-state]'); | ||||||||||||||||||||||
| await expect(state).toHaveText('complete'); | ||||||||||||||||||||||
|
Check failure on line 69 in browser/20-showcase.spec.ts
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| await page.locator('[data-action="replay-stagger"]').click(); | ||||||||||||||||||||||
| await expect(state).toHaveText('running'); | ||||||||||||||||||||||
| await expect.poll(async () => page.locator('[data-stagger-item]').evaluateAll((items) => { | ||||||||||||||||||||||
| const opacity = items.map((item) => Number(getComputedStyle(item).opacity)); | ||||||||||||||||||||||
| return Math.max(...opacity) - Math.min(...opacity); | ||||||||||||||||||||||
| })).toBeGreaterThan(0.1); | ||||||||||||||||||||||
| await expect(state).toHaveText('complete'); | ||||||||||||||||||||||
| await expect.poll(async () => page.locator('[data-stagger-item]').evaluateAll((items) => | ||||||||||||||||||||||
| items.every((item) => Math.abs(Number(getComputedStyle(item).opacity) - 1) < 0.001), | ||||||||||||||||||||||
| )).toBe(true); | ||||||||||||||||||||||
| expect(failures).toEqual([]); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| test('retarget changes the observed destination and settles at the redirected target', async ({ page }) => { | ||||||||||||||||||||||
| const failures = watchRuntimeFailures(page); | ||||||||||||||||||||||
| await page.goto(SHOWCASE); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| await page.locator('[data-action="retarget"]').click(); | ||||||||||||||||||||||
| await expect(page.locator('[data-card="retarget"] [data-state]')).toHaveText('running'); | ||||||||||||||||||||||
| await expect.poll(() => page.locator('[data-retarget-copy]').textContent()).toMatch(/Redirecting|Retargeted/); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| await page.locator('[data-action="reset-retarget"]').click(); | ||||||||||||||||||||||
| await expect(page.locator('[data-card="retarget"] [data-state]')).toHaveText('ready'); | ||||||||||||||||||||||
| await expect(page.locator('[data-retarget-copy]')).toHaveText('Start a transition, then redirect it without a teleport.'); | ||||||||||||||||||||||
| await expect.poll(async () => (await translation(page, '[data-preview="retarget-object"]')).x).toBe(-112); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| await page.locator('[data-action="retarget"]').click(); | ||||||||||||||||||||||
| await expect(page.locator('[data-card="retarget"] [data-state]')).toHaveText('complete'); | ||||||||||||||||||||||
| await expect.poll(async () => (await translation(page, '[data-preview="retarget-object"]')).x).toBeCloseTo(-34, 1); | ||||||||||||||||||||||
| expect(failures).toEqual([]); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| test('Copy sends the exact public example and reports success', async ({ page }) => { | ||||||||||||||||||||||
| const failures = watchRuntimeFailures(page); | ||||||||||||||||||||||
| await page.addInitScript(() => { | ||||||||||||||||||||||
| Object.defineProperty(navigator, 'clipboard', { | ||||||||||||||||||||||
| configurable: true, | ||||||||||||||||||||||
| value: { | ||||||||||||||||||||||
| writeText(text: string) { | ||||||||||||||||||||||
| (window as typeof window & { __showcaseCopied?: string }).__showcaseCopied = text; | ||||||||||||||||||||||
| return Promise.resolve(); | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| await page.goto(SHOWCASE); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| await page.locator('[data-copy]').click(); | ||||||||||||||||||||||
| await expect(page.locator('[data-copy-status]')).toHaveText('Copied to clipboard.'); | ||||||||||||||||||||||
| expect(await page.evaluate(() => | ||||||||||||||||||||||
| (window as typeof window & { __showcaseCopied?: string }).__showcaseCopied, | ||||||||||||||||||||||
| )).toBe(EXAMPLE); | ||||||||||||||||||||||
| expect(failures).toEqual([]); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| test('system reduced motion resolves previews without native animations', async ({ page }) => { | ||||||||||||||||||||||
| const failures = watchRuntimeFailures(page); | ||||||||||||||||||||||
| await page.emulateMedia({ reducedMotion: 'reduce' }); | ||||||||||||||||||||||
| await page.goto(SHOWCASE); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| await expect(page.locator('[data-action="toggle-motion"]')).toHaveAttribute('aria-pressed', 'true'); | ||||||||||||||||||||||
| await expect(page.locator('[data-action="toggle-motion"]')).toBeDisabled(); | ||||||||||||||||||||||
| await expect(page.locator('[data-action="toggle-motion"]')).toHaveText('System setting: reduced motion'); | ||||||||||||||||||||||
| await expect(page.locator('[data-site-status]')).toHaveText('Your system reduced-motion preference is active; the preview follows it.'); | ||||||||||||||||||||||
| await expect(page.locator('[data-card="spring"] [data-state]')).toHaveText('reduced'); | ||||||||||||||||||||||
| await expect(page.locator('[data-card="stagger"] [data-state]')).toHaveText('reduced'); | ||||||||||||||||||||||
| await expect.poll(() => page.locator('[data-preview], [data-stagger-item]').evaluateAll((items) => | ||||||||||||||||||||||
| items.reduce((count, item) => count + item.getAnimations().length, 0), | ||||||||||||||||||||||
| )).toBe(0); | ||||||||||||||||||||||
| await expect.poll(async () => (await translation(page, '[data-preview="spring-object"]')).x).toBeCloseTo(112, 1); | ||||||||||||||||||||||
| expect(failures).toEqual([]); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| test('changing the system motion preference updates the effective UI state', async ({ page }) => { | ||||||||||||||||||||||
| const failures = watchRuntimeFailures(page); | ||||||||||||||||||||||
| await page.emulateMedia({ reducedMotion: 'no-preference' }); | ||||||||||||||||||||||
| await page.goto(SHOWCASE); | ||||||||||||||||||||||
| await expect(page.locator('[data-card="spring"] [data-state]')).toHaveText('complete'); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| await page.emulateMedia({ reducedMotion: 'reduce' }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| await expect(page.locator('[data-action="toggle-motion"]')).toHaveAttribute('aria-pressed', 'true'); | ||||||||||||||||||||||
| await expect(page.locator('[data-action="toggle-motion"]')).toBeDisabled(); | ||||||||||||||||||||||
| await expect(page.locator('[data-site-status]')).toHaveText('Your system reduced-motion preference is active; the preview follows it.'); | ||||||||||||||||||||||
| await expect(page.locator('[data-card="spring"] [data-state]')).toHaveText('reduced'); | ||||||||||||||||||||||
| await expect(page.locator('[data-card="stagger"] [data-state]')).toHaveText('reduced'); | ||||||||||||||||||||||
| expect(failures).toEqual([]); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| test('enabling reduced motion clears the pending autonomous hero replay', async ({ page }) => { | ||||||||||||||||||||||
| const failures = watchRuntimeFailures(page); | ||||||||||||||||||||||
| await page.addInitScript(() => { | ||||||||||||||||||||||
| const nativeSetTimeout = window.setTimeout.bind(window); | ||||||||||||||||||||||
| const nativeClearTimeout = window.clearTimeout.bind(window); | ||||||||||||||||||||||
| const timers = new Map<number, { cleared: boolean; delay: number; fired: boolean }>(); | ||||||||||||||||||||||
| let heldTimerId = -1; | ||||||||||||||||||||||
| (window as typeof window & { __showcaseTimers?: typeof timers }).__showcaseTimers = timers; | ||||||||||||||||||||||
| window.setTimeout = ((callback: TimerHandler, delay = 0, ...args: unknown[]) => { | ||||||||||||||||||||||
| if (delay === 700) { | ||||||||||||||||||||||
| const id = heldTimerId--; | ||||||||||||||||||||||
| timers.set(id, { cleared: false, delay, fired: false }); | ||||||||||||||||||||||
| return id; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| let id = 0; | ||||||||||||||||||||||
| const tracked = typeof callback === 'function' | ||||||||||||||||||||||
| ? (...callbackArgs: unknown[]) => { | ||||||||||||||||||||||
| const timer = timers.get(id); | ||||||||||||||||||||||
| if (timer) timer.fired = true; | ||||||||||||||||||||||
| return Reflect.apply(callback, window, callbackArgs); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| : callback; | ||||||||||||||||||||||
| id = nativeSetTimeout(tracked, delay, ...args) as unknown as number; | ||||||||||||||||||||||
| if (delay === 700) timers.set(id, { cleared: false, delay, fired: false }); | ||||||||||||||||||||||
| return id; | ||||||||||||||||||||||
| }) as typeof window.setTimeout; | ||||||||||||||||||||||
| window.clearTimeout = ((id = 0) => { | ||||||||||||||||||||||
| const timer = timers.get(id as unknown as number); | ||||||||||||||||||||||
| if (timer) { | ||||||||||||||||||||||
| timer.cleared = true; | ||||||||||||||||||||||
| return; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| nativeClearTimeout(id); | ||||||||||||||||||||||
| }) as typeof window.clearTimeout; | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| await page.goto(SHOWCASE); | ||||||||||||||||||||||
| await expect(page.locator('[data-card="spring"] [data-state]')).toHaveText('complete'); | ||||||||||||||||||||||
| await expect(page.locator('[data-card="stagger"] [data-state]')).toHaveText('complete'); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| await expect.poll(() => page.evaluate(() => { | ||||||||||||||||||||||
| const timers = (window as typeof window & { | ||||||||||||||||||||||
| __showcaseTimers?: Map<number, { cleared: boolean; delay: number; fired: boolean }>; | ||||||||||||||||||||||
| }).__showcaseTimers; | ||||||||||||||||||||||
| return [...(timers?.values() ?? [])].some((timer) => timer.delay === 700 && !timer.cleared && !timer.fired); | ||||||||||||||||||||||
| })).toBe(true); | ||||||||||||||||||||||
| const pendingHeroTimers = await page.evaluate(() => { | ||||||||||||||||||||||
| const timers = (window as typeof window & { | ||||||||||||||||||||||
| __showcaseTimers?: Map<number, { cleared: boolean; delay: number; fired: boolean }>; | ||||||||||||||||||||||
| }).__showcaseTimers; | ||||||||||||||||||||||
| return [...(timers?.entries() ?? [])] | ||||||||||||||||||||||
| .filter(([, timer]) => timer.delay === 700 && !timer.cleared && !timer.fired) | ||||||||||||||||||||||
| .map(([id]) => id); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| await page.locator('[data-action="toggle-motion"]').click(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| expect(await page.evaluate((ids) => { | ||||||||||||||||||||||
| const timers = (window as typeof window & { | ||||||||||||||||||||||
| __showcaseTimers?: Map<number, { cleared: boolean; delay: number; fired: boolean }>; | ||||||||||||||||||||||
| }).__showcaseTimers; | ||||||||||||||||||||||
| return ids.every((id) => timers?.get(id)?.cleared === true); | ||||||||||||||||||||||
| }, pendingHeroTimers)).toBe(true); | ||||||||||||||||||||||
| expect(failures).toEqual([]); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| test('small functional text meets the WCAG AA contrast floor', async ({ page }) => { | ||||||||||||||||||||||
| await page.goto(SHOWCASE); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const ratios = await page.locator('.card-kicker, .card-state, .code-card-bar').evaluateAll((elements) => { | ||||||||||||||||||||||
| function channel(value: number): number { | ||||||||||||||||||||||
| const normalized = value / 255; | ||||||||||||||||||||||
| return normalized <= 0.03928 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| function luminance(color: string): number { | ||||||||||||||||||||||
| const [r = 0, g = 0, b = 0] = color.match(/[\d.]+/g)?.map(Number) ?? []; | ||||||||||||||||||||||
| return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| return elements.map((element) => { | ||||||||||||||||||||||
| const style = getComputedStyle(element); | ||||||||||||||||||||||
| const foreground = luminance(style.color); | ||||||||||||||||||||||
| const background = luminance(getComputedStyle(element.closest('.proof-card, .code-card') ?? document.body).backgroundColor); | ||||||||||||||||||||||
| return (Math.max(foreground, background) + 0.05) / (Math.min(foreground, background) + 0.05); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| expect(Math.min(...ratios)).toBeGreaterThanOrEqual(4.5); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| test('keyboard toggle enforces reduced motion and the phone layout does not overflow', async ({ page }) => { | ||||||||||||||||||||||
| const failures = watchRuntimeFailures(page); | ||||||||||||||||||||||
| await page.setViewportSize({ width: 375, height: 800 }); | ||||||||||||||||||||||
| await page.emulateMedia({ reducedMotion: 'no-preference' }); | ||||||||||||||||||||||
| await page.goto(SHOWCASE); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const overflow = await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth); | ||||||||||||||||||||||
| expect(overflow).toBeLessThanOrEqual(0); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| let focusedAction: string | undefined; | ||||||||||||||||||||||
| for (let step = 0; step < 12 && focusedAction !== 'toggle-motion'; step++) { | ||||||||||||||||||||||
| await page.keyboard.press('Tab'); | ||||||||||||||||||||||
| focusedAction = await page.evaluate(() => (document.activeElement as HTMLElement | null)?.dataset.action); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| expect(focusedAction).toBe('toggle-motion'); | ||||||||||||||||||||||
| await page.keyboard.press('Enter'); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const toggle = page.locator('[data-action="toggle-motion"]'); | ||||||||||||||||||||||
| await expect(toggle).toHaveAttribute('aria-pressed', 'true'); | ||||||||||||||||||||||
| await expect(toggle).toHaveText('Use full motion'); | ||||||||||||||||||||||
| await expect(page.locator('[data-card="spring"] [data-state]')).toHaveText('reduced'); | ||||||||||||||||||||||
| await expect(page.locator('[data-card="stagger"] [data-state]')).toHaveText('reduced'); | ||||||||||||||||||||||
| expect(await page.locator('[data-preview], [data-stagger-item]').evaluateAll((items) => | ||||||||||||||||||||||
| items.reduce((count, item) => count + item.getAnimations().length, 0), | ||||||||||||||||||||||
| )).toBe(0); | ||||||||||||||||||||||
| expect(failures).toEqual([]); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| test('hero preview stops off-screen and resumes when it re-enters the viewport', async ({ page }) => { | ||||||||||||||||||||||
| const failures = watchRuntimeFailures(page); | ||||||||||||||||||||||
| await page.goto(SHOWCASE); | ||||||||||||||||||||||
| const orb = page.locator('[data-preview="hero-orb"]'); | ||||||||||||||||||||||
| await expect.poll(() => orb.evaluate((element) => element.getAnimations().length)).toBeGreaterThan(0); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| await page.evaluate(() => window.scrollTo(0, document.documentElement.scrollHeight)); | ||||||||||||||||||||||
| await expect.poll(() => orb.evaluate((element) => element.getAnimations().length)).toBe(0); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| await page.evaluate(() => window.scrollTo(0, 0)); | ||||||||||||||||||||||
| await expect.poll(() => orb.evaluate((element) => element.getAnimations().length)).toBeGreaterThan(0); | ||||||||||||||||||||||
| expect(failures).toEqual([]); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -507,7 +507,9 @@ | |
| "test:browser:all": "playwright test", | ||
| "mutation": "stryker run", | ||
| "typecheck:native": "tsgo --noEmit", | ||
| "check:native-gate": "node scripts/check-native-gate.mjs" | ||
| "check:native-gate": "node scripts/check-native-gate.mjs", | ||
| "site:build": "pnpm build && vite build --config site/vite.config.mjs site", | ||
| "site:preview": "vite preview --config site/vite.config.mjs site/dist" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- candidate files ---'
git ls-files | rg '(^|/)(package\.json|vite\.config\.mjs)$' || true
printf '%s\n' '--- package scripts ---'
python3 - <<'PY'
import json
from pathlib import Path
p = Path("package.json")
data = json.loads(p.read_text())
for key, value in data.get("scripts", {}).items():
if key.startswith("site"):
print(f"{key}: {value}")
PY
printf '%s\n' '--- Vite config candidates and relevant lines ---'
for f in $(git ls-files | rg '(^|/)site/vite\.config\.mjs$|(^|/)vite\.config\.mjs$'); do
echo "FILE: $f"
rg -n -C 8 'outDir|root|preview|build' "$f" || true
done
printf '%s\n' '--- package.json context ---'
sed -n '500,518p' package.jsonRepository: Labpics-Team/lab-motion Length of output: 1526 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- Vite dependency declarations ---'
rg -n -C 3 '"vite"|vite@' package.json pnpm-lock.yaml package-lock.json yarn.lock 2>/dev/null || true
printf '%s\n' '--- lockfiles ---'
git ls-files | rg '(^|/)(pnpm-lock\.yaml|package-lock\.json|yarn\.lock|bun\.lockb?)$' || true
printf '%s\n' '--- Vite-related repository references ---'
rg -n -C 3 'vite preview|vite build|build\.outDir|preview\.outDir|preview' README.md site package.json 2>/dev/null || trueRepository: Labpics-Team/lab-motion Length of output: 13969 🌐 Web query:
💡 Result: The vite preview CLI command does not automatically append dist to the root path [1][2]. Instead, it serves files from the directory specified by your build configuration's outDir option [1][3]. Here is how the interaction works: 1. Root Path: The positional argument [root] you provide to vite preview represents your project's root directory (where your vite.config.ts or index.html is expected to be located) [4][2]. It defaults to the current working directory [4][2]. 2. Build Output Directory: Vite determines which directory to serve by looking at the build.outDir setting in your configuration [5][6]. This defaults to dist [1][5]. When you run vite preview, the server points to /<build.outDir> [1][3]. 3. CLI Overrides: If you specify an --outDir flag in the CLI, it overrides the build.outDir setting and is used as the directory to serve, relative to the provided [root] [3][6]. In summary, vite preview does not blindly append dist; it resolves the path by combining the project root and the configured (or overridden) output directory [3]. If your build output is not in a folder named dist, or if you have customized your outDir in your configuration, vite preview will respect those settings and serve from the correct location automatically [5][7]. Citations:
🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import PurePosixPath
import urllib.request
base = "https://raw.githubusercontent.com/vitejs/vite/v8.2.0/packages/vite/src"
files = {
"node/cli.ts": ("preview", "build.outDir"),
"node/config.ts": ("outDir", "resolve"),
}
for rel, needles in files.items():
url = f"{base}/{rel}"
print(f"--- {url} ---")
text = urllib.request.urlopen(url, timeout=15).read().decode()
lines = text.splitlines()
hits = [i for i, line in enumerate(lines) if any(n in line for n in needles)]
shown = set()
for i in hits:
for j in range(max(0, i - 5), min(len(lines), i + 8)):
shown.add(j)
for j in sorted(shown):
print(f"{j+1}: {lines[j]}")
print("--- path calculation ---")
for root in ("site/dist", "site"):
print(f"{root} + dist -> {PurePosixPath(root) / 'dist'}")
PYRepository: Labpics-Team/lab-motion Length of output: 2539 Fix the preview root. Vite uses the default 🤖 Prompt for AI AgentsSource: MCP tools |
||
| }, | ||
| "peerDependencies": { | ||
| "@angular/core": ">=16.0.0", | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win
Avoid building the package twice.
package.jsonLine 511 already runspnpm buildinsidesite:build. The documented sequence runs that build once on Line 72 and again on Line 73. Remove the standalonepnpm build, or split the script so each command performs one build.🤖 Prompt for AI Agents