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
4 changes: 2 additions & 2 deletions .github/workflows/daily-multi-device-docs-tester.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 8 additions & 5 deletions .github/workflows/daily-multi-device-docs-tester.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,14 @@ tools:
cli-proxy: true
github:
mode: gh-proxy
timeout: 120 # Playwright navigation on Astro dev server can take >60s; increase to 120s
timeout: 120 # Multi-device runs include docs build + preview startup
playwright:
mode: cli
bash:
- "npm install*"
- "npm run build*"
- "npm run dev*"
- "npm run preview*"
- "npx astro*"
- "npx playwright*"
- "playwright-cli*" # CLI-mode playwright commands
Expand Down Expand Up @@ -99,7 +101,8 @@ pre-agent-steps:
LOG_FILE="/tmp/gh-aw/agent/docs-server-$EXPR_GITHUB_RUN_ID.log"
PID_FILE="/tmp/gh-aw/agent/docs-server-$EXPR_GITHUB_RUN_ID.pid"
cd "$EXPR_GITHUB_WORKSPACE/docs"
nohup npm run dev -- --host 0.0.0.0 --port 4321 > "$LOG_FILE" 2>&1 &
npm run build
nohup npm run preview -- --host 0.0.0.0 --port 4321 > "$LOG_FILE" 2>&1 &
PID=$!
echo $PID > "$PID_FILE"
echo "Server PID: $PID"
Expand Down Expand Up @@ -158,11 +161,11 @@ This workflow has `strict: true` — it will fail if no safe output is produced.

## Your Mission

Start the documentation development server and perform comprehensive multi-device testing. Test layout responsiveness, accessibility, interactive elements, and visual rendering across all device types. Use a single Playwright browser instance for efficiency.
Start the documentation preview server and perform comprehensive multi-device testing. Test layout responsiveness, accessibility, interactive elements, and visual rendering across all device types. Use a single Playwright browser instance for efficiency.

## Step 1: Verify Server Availability

The workflow pre-agent steps already installed docs dependencies and started the Astro dev server.
The workflow pre-agent steps already installed docs dependencies, built the docs site, and started the Astro preview server.
Quickly verify it is reachable before testing:

```bash
Expand Down Expand Up @@ -190,7 +193,7 @@ Playwright is pre-installed as `@playwright/cli`. Use `playwright-cli <command>`

**⚠️ CRITICAL: Navigation Timeout Prevention**

The Astro development server uses Vite, which loads many JavaScript modules per page. Using the default `waitUntil: 'load'` will cause 60s timeouts because the browser waits for all modules to finish. **Use `waitUntil: 'domcontentloaded'`** for navigation:
Use `waitUntil: 'domcontentloaded'` for navigation to keep checks fast and consistent:

```bash
playwright-cli browser_run_code --code "async (page) => {
Expand Down
6 changes: 3 additions & 3 deletions docs/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ export default defineConfig({
// Ensure the command always runs relative to the docs folder, regardless
// of the caller's current working directory.
cwd: configDir,
// Keep startup lighter than a full prebuild, but ensure the homepage slide
// preview asset exists before the dev server starts.
command: 'npm run build:slides && npm run dev --silent -- --host 127.0.0.1 --port 4321',
// Search is only available in production builds, so run tests against a

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnosing-bugs] The timeout for webServer is still 180 * 1000 ms but the startup command now includes a full npm run build before preview. If the build takes longer than ~3 min on a cold CI runner, Playwright will kill the server before tests start and produce confusing timeout errors.

💡 Suggestion

Consider raising the timeout to accommodate the build step, or separating it:

timeout: 300 * 1000, // 5 min: build (≈2 min) + preview startup

Alternatively, document the expected build time in a comment so future editors know the limit isn't arbitrary.

@copilot please address this.

// built preview server.
command: 'npm run build && npm run preview -- --host 127.0.0.1 --port 4321',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

webServer timeout of 180 s will be breached by a full production build — tests will never start.

💡 Explanation and fix

The old command was npm run build:slides && npm run dev — a fast partial build. The new command is npm run build && npm run preview. Looking at package.json, npm run build triggers its prebuild hook, which runs generate-agent-factory, generate-model-tables, and build:slides before the full Astro SSG pass. On a cold CI runner without Node module cache, this pipeline routinely exceeds 3 minutes. The current timeout: 180 * 1000 (exactly 3 minutes) will fire before the build finishes, terminating the test run with a misleading Playwright startup error.

Increase the timeout:

timeout: 360 * 1000,  // 6 min — covers full Astro prebuild + preview startup

If build time is a concern, a PLAYWRIGHT_REUSE_SERVER=1 convention (or a custom env var checked via reuseExistingServer) lets local developers reuse an already-built site across test runs.

url: 'http://localhost:4321/gh-aw/',
reuseExistingServer: !process.env.CI,
timeout: 180 * 1000,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The webServer.timeout is still 180 s, but the startup command changed from npm run build:slides && npm run dev to a full npm run build (which runs codegen + asset compilation) followed by npm run preview. A full Astro build is considerably heavier and may exceed 180 s in CI, causing spurious test failures.

Consider increasing the timeout or adding a step in CI to cache the build output:

timeout: 300 * 1000, // full production build can take 3–5 min in CI

@copilot please address this.

Expand Down
35 changes: 35 additions & 0 deletions docs/tests/search-dialog.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { test, expect } from '@playwright/test';

const VIEWPORTS = [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The PR title mentions "responsive" coverage, but only desktop (1366px) and tablet (834px) are tested — no mobile viewport. The original false-critical failure was a desktop issue; a mobile breakpoint would round out the regression suite.

💡 Suggested addition
const VIEWPORTS = [
  { name: 'desktop', width: 1366, height: 768 },
  { name: 'tablet', width: 834, height: 1194 },
  { name: 'mobile', width: 390, height: 844 },
];

On narrow viewports, Starlight typically hides or replaces the desktop search button with a mobile-specific trigger — worth asserting the modal is still reachable.

@copilot please address this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing mobile viewport coverage directly contradicts the PR's claim of 'responsive search-modal coverage'.

💡 Explanation and fix

VIEWPORTS covers only desktop (1366×768) and tablet (834×1194). On Starlight-based sites, mobile viewports (≤640 px) often hide the top-bar search button and expose it via a different control (e.g., inside a hamburger menu or a bottom-sheet overlay). Any mobile-specific regression in the search UI goes completely undetected by this test suite.

Add at least one mobile breakpoint:

const VIEWPORTS = [
  { name: 'mobile',  width: 390,  height: 844  },
  { name: 'tablet',  width: 834,  height: 1194 },
  { name: 'desktop', width: 1366, height: 768  },
];

On mobile, the search button may need a different locator (e.g., a menu must be opened first). Test that path explicitly rather than assuming desktop and tablet selectors will work.

{ name: 'desktop', width: 1366, height: 768 },
{ name: 'tablet', width: 834, height: 1194 },
];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spec loops over only desktop (1366×768) and tablet (834×1194) viewports, but the original bug report was a multi-device regression. A mobile viewport (e.g., 390×844 — iPhone 14) would complete the coverage story.

const VIEWPORTS = [
  { name: 'desktop', width: 1366, height: 768 },
  { name: 'tablet',  width: 834,  height: 1194 },
  { name: 'mobile',  width: 390,  height: 844 },
];

Without a mobile entry, a breakpoint-specific regression on small screens would go undetected by this suite.

@copilot please address this.


for (const viewport of VIEWPORTS) {
test(`search dialog is usable and dismissible on ${viewport.name}`, async ({ page }) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The for loop generates a single test case that packs open → dismiss → reopen → CTA navigation into one scenario. If the reopen assertion fails, the CTA check is silently skipped. Splitting these into separate test blocks would give clearer signal on which behaviour regressed.

💡 Suggested structure
test(`search dialog opens with visible input on ${viewport.name}`, async ({ page }) => { /* open only */ });
test(`search dialog dismisses on Escape on ${viewport.name}`, async ({ page }) => { /* open + Escape */ });
test(`search dialog can be reopened on ${viewport.name}`, async ({ page }) => { /* open, close, reopen */ });
test(`CTA is actionable after closing dialog on ${viewport.name}`, async ({ page }) => { /* open, close, click CTA */ });

Each test name reads as a spec statement and the failure message points directly to the broken behaviour.

@copilot please address this.

await page.setViewportSize({ width: viewport.width, height: viewport.height });
await page.goto('/gh-aw/');
await page.waitForLoadState('networkidle');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flakiness risk: waitForLoadState('networkidle') will cause sporadic CI timeouts against a production preview build.

💡 Explanation and fix

A full Astro production build registers deferred hydration callbacks, analytics pings, and background fetches that keep open network requests well past Playwright's 500 ms idle window. On a cold CI runner this reliably causes networkidle to time out. The workflow's own agent guidance already tells agents to use waitUntil: 'domcontentloaded' for exactly this reason.

Replace:

await page.waitForLoadState('networkidle');

With:

await page.waitForLoadState('domcontentloaded');

For a static Astro site, domcontentloaded is sufficient — all critical HTML is rendered synchronously.


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnosing-bugs] waitForLoadState('networkidle') is prone to flakiness and could reintroduce intermittent failures — exactly the problem this PR fixes.

💡 Suggestion

Drop the explicit networkidle wait and rely on Playwright's auto-waiting:

await page.goto('/gh-aw/');
// Auto-wait: Playwright retries until the element is visible
await expect(page.getByRole('button', { name: 'Search' })).toBeVisible();

The workflow guidance already recommends domcontentloaded over heavier load states for the same reason (avoiding timeout-induced false failures).

@copilot please address this.

await page.getByRole('button', { name: 'Search' }).click();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using waitForLoadState('networkidle') can be flaky in CI — the workflow guidance in this same PR recommends domcontentloaded for consistent checks.

Consider replacing with:

await page.goto('/gh-aw/', { waitUntil: 'domcontentloaded' });
// remove the separate waitForLoadState call

networkidle waits for 500 ms of zero network activity, which can time out if Astro's prefetch or service-worker traffic is ongoing.

@copilot please address this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getByRole('button', { name: 'Search' }) may throw a strict-mode violation if multiple elements match.

💡 Explanation and fix

Starlight search implementations commonly render multiple elements whose accessible name includes the word "Search" — for example, the visible icon button in the top bar, a keyboard-shortcut hint button (⌘K), and potentially an aria-label on the search container itself. Playwright's getByRole in strict mode (the default) throws if more than one element matches, so on real Starlight pages this selector is fragile.

Use exact: true to match only the primary button and reduce the chance of multiple matches:

await page.getByRole('button', { name: 'Search', exact: true }).click();

Or, if there are still multiple matches, scope the query to the header:

await page.locator('header').getByRole('button', { name: 'Search', exact: true }).click();


const openDialog = page.locator('dialog[open]');
await expect(openDialog).toBeVisible();
await expect(openDialog.getByRole('search').getByRole('textbox', { name: 'Search' })).toBeVisible();

await page.keyboard.press('Escape');
await expect(openDialog).toHaveCount(0);

await page.getByRole('button', { name: 'Search' }).click();
const reopenedDialog = page.locator('dialog[open]');
await expect(reopenedDialog).toBeVisible();
await expect(
reopenedDialog.getByRole('search').getByRole('textbox', { name: 'Search' })
).toBeVisible();
await page.keyboard.press('Escape');
await expect(reopenedDialog).toHaveCount(0);

await page.getByRole('link', { name: 'Quick Start with CLI' }).click();
await expect(page).toHaveURL(/\/gh-aw\/setup\/quick-start\//);
});
}
Loading