-
Notifications
You must be signed in to change notification settings - Fork 39
tests: add Playwright E2E tests with screenshot golden testing (+ fix examples session handling) #115
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
tests: add Playwright E2E tests with screenshot golden testing (+ fix examples session handling) #115
Changes from 1 commit
70c1215
b790f3d
3469766
ca62dfa
585b4d2
6db453a
0623088
fd790a1
42665c8
0d05cf8
f62e4f4
95aa92f
9aa7727
20ef214
6cd4d1a
13dde97
4b73b5f
d3ff6da
e6e6e7a
da89aa9
6c44f8e
277a004
4a0c3bc
53012ae
23d0e2e
0b890bd
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 |
|---|---|---|
|
|
@@ -6,3 +6,8 @@ bun.lockb | |
| .vscode/ | ||
| docs/api/ | ||
| tmp/ | ||
| intermediate-findings/ | ||
|
|
||
| # Playwright | ||
| playwright-report/ | ||
| test-results/ | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| import { defineConfig, devices } from "@playwright/test"; | ||
|
|
||
| export default defineConfig({ | ||
| testDir: "./tests/e2e", | ||
| fullyParallel: false, // Run tests sequentially to share server | ||
| forbidOnly: !!process.env.CI, | ||
| retries: process.env.CI ? 2 : 0, | ||
| workers: 1, // Single worker since we share the server | ||
|
||
| reporter: "html", | ||
| use: { | ||
| baseURL: "http://localhost:8080", | ||
| trace: "on-first-retry", | ||
| screenshot: "only-on-failure", | ||
| }, | ||
| projects: [ | ||
| { | ||
| name: "chromium", | ||
| use: { | ||
| ...devices["Desktop Chrome"], | ||
| launchOptions: { | ||
| // Use system Chrome on macOS for stability, default chromium in CI | ||
| ...(process.platform === "darwin" ? { channel: "chrome" } : {}), | ||
| }, | ||
| }, | ||
| }, | ||
| ], | ||
| // Run examples server before tests | ||
| webServer: { | ||
| command: "npm run examples:start", | ||
| url: "http://localhost:8080", | ||
| reuseExistingServer: !process.env.CI, | ||
| timeout: 120000, | ||
| }, | ||
| // Snapshot configuration | ||
| expect: { | ||
| toHaveScreenshot: { | ||
| // Allow 2% pixel difference for dynamic content (timestamps, etc.) | ||
| maxDiffPixelRatio: 0.02, | ||
| // Animation stabilization | ||
| animations: "disabled", | ||
| }, | ||
| }, | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| import { test, expect } from "@playwright/test"; | ||
|
|
||
| // Server configurations | ||
| const SERVERS = [ | ||
| { key: "basic-react", index: 0, name: "Basic MCP App Server (React-based)" }, | ||
| { | ||
| key: "basic-vanillajs", | ||
| index: 1, | ||
| name: "Basic MCP App Server (Vanilla JS)", | ||
| }, | ||
| { key: "budget-allocator", index: 2, name: "Budget Allocator Server" }, | ||
| { key: "cohort-heatmap", index: 3, name: "Cohort Heatmap Server" }, | ||
| { | ||
| key: "customer-segmentation", | ||
| index: 4, | ||
| name: "Customer Segmentation Server", | ||
| }, | ||
| { key: "scenario-modeler", index: 5, name: "SaaS Scenario Modeler" }, | ||
| { key: "system-monitor", index: 6, name: "System Monitor Server" }, | ||
| { key: "threejs", index: 7, name: "Three.js Server" }, | ||
| ]; | ||
|
Comment on lines
24
to
42
Member
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. Possibly for a v2, what do you think about having each example define its own Also, possibly for a v2 or even v3, if we factor out test helpers for each example to use in its own
Collaborator
Author
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. At this scale I'd rather keep things simple and centralized and one-fits-all (e.g. define same SEED for all - or just get the cue from CI env that the seed needs to be predictible - and have some unified way to disable animations - as in the wiki stuff - etc) |
||
|
|
||
| // Increase timeout for iframe-heavy tests | ||
| test.setTimeout(90000); | ||
|
|
||
| test.describe("Host UI", () => { | ||
| test("initial state shows controls", async ({ page }) => { | ||
| await page.goto("/"); | ||
| await expect(page.locator("label:has-text('Server')")).toBeVisible(); | ||
| await expect(page.locator("label:has-text('Tool')")).toBeVisible(); | ||
| await expect(page.locator('button:has-text("Call Tool")')).toBeVisible(); | ||
| }); | ||
|
|
||
| test("screenshot of initial state", async ({ page }) => { | ||
| await page.goto("/"); | ||
| await page.waitForTimeout(1000); | ||
| await expect(page).toHaveScreenshot("host-initial.png"); | ||
| }); | ||
| }); | ||
|
|
||
| // Generate tests for each server | ||
| for (const server of SERVERS) { | ||
| test.describe(`${server.name}`, () => { | ||
| test(`loads app UI`, async ({ page }) => { | ||
| await page.goto("/"); | ||
|
|
||
| // Select server | ||
| const serverSelect = page.locator("select").first(); | ||
| await serverSelect.selectOption({ index: server.index }); | ||
|
|
||
| // Click Call Tool | ||
| await page.click('button:has-text("Call Tool")'); | ||
|
|
||
| // Wait for outer iframe | ||
| await page.waitForSelector("iframe", { timeout: 10000 }); | ||
|
|
||
| // Wait for content to load (generous timeout for nested iframes) | ||
| await page.waitForTimeout(5000); | ||
|
|
||
| // Verify iframe structure exists | ||
| const outerFrame = page.frameLocator("iframe").first(); | ||
| await expect(outerFrame.locator("iframe")).toBeVisible({ | ||
| timeout: 10000, | ||
| }); | ||
| }); | ||
|
|
||
| test(`screenshot matches golden`, async ({ page }) => { | ||
| await page.goto("/"); | ||
|
|
||
| // Select server | ||
| const serverSelect = page.locator("select").first(); | ||
| await serverSelect.selectOption({ index: server.index }); | ||
|
|
||
| // Click Call Tool | ||
| await page.click('button:has-text("Call Tool")'); | ||
|
|
||
| // Wait for app to fully load | ||
| await page.waitForSelector("iframe", { timeout: 10000 }); | ||
| await page.waitForTimeout(6000); // Extra time for nested iframe content | ||
|
|
||
| // Take screenshot | ||
| await expect(page).toHaveScreenshot(`${server.key}.png`, { | ||
| maxDiffPixelRatio: 0.1, // 10% tolerance for dynamic content | ||
| timeout: 10000, | ||
| }); | ||
| }); | ||
| }); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.