Skip to content

Commit 873b838

Browse files
authored
Merge pull request #460 from msabramo/playwright-test
Add a Playwright e2e test
2 parents 5db6426 + 86eaabe commit 873b838

File tree

10 files changed

+351
-2
lines changed

10 files changed

+351
-2
lines changed

.github/workflows/e2e_tests.yml

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
name: Playwright Tests
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
timeout-minutes: 5
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- name: Install dependencies
16+
run: |
17+
sudo apt-get update
18+
sudo apt-get install -y libwoff1
19+
20+
- uses: actions/checkout@v4
21+
22+
- uses: actions/setup-node@v4
23+
with:
24+
node-version: 18
25+
26+
# Cache Playwright browsers
27+
- name: Cache Playwright browsers
28+
id: cache-playwright
29+
uses: actions/cache@v4
30+
with:
31+
path: ~/.cache/ms-playwright # The default Playwright cache path
32+
key: ${{ runner.os }}-playwright-${{ hashFiles('package-lock.json') }} # Cache key based on OS and package-lock.json
33+
restore-keys: |
34+
${{ runner.os }}-playwright-
35+
36+
- name: Install dependencies
37+
run: npm ci
38+
39+
- name: Install Playwright dependencies
40+
run: npx playwright install-deps
41+
42+
- name: Install Playwright and browsers unless cached
43+
run: npx playwright install --with-deps
44+
if: steps.cache-playwright.outputs.cache-hit != 'true'
45+
46+
- name: Run Playwright tests
47+
run: npm run test:e2e
48+
49+
- name: Upload Playwright Report and Screenshots
50+
uses: actions/upload-artifact@v4
51+
if: always()
52+
with:
53+
name: playwright-report
54+
path: |
55+
client/playwright-report/
56+
client/test-results/
57+
client/results.json
58+
retention-days: 2
59+
60+
- name: Publish Playwright Test Summary
61+
uses: daun/playwright-report-summary@v3
62+
if: always()
63+
with:
64+
create-comment: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
65+
report-file: client/results.json
66+
comment-title: "🎭 Playwright E2E Test Results"
67+
job-summary: true
68+
icon-style: "emojis"
69+
custom-info: |
70+
**Test Environment:** Ubuntu Latest, Node.js 18
71+
**Browsers:** Chromium, Firefox
72+
73+
📊 [View Detailed HTML Report](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) (download artifacts)
74+
test-command: "npm run test:e2e"

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,7 @@ client/tsconfig.app.tsbuildinfo
99
client/tsconfig.node.tsbuildinfo
1010
cli/build
1111
test-output
12+
client/playwright-report/
13+
client/results.json
14+
client/test-results/
15+

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Thanks for your interest in contributing! This guide explains how to get involve
1313

1414
1. Create a new branch for your changes
1515
2. Make your changes following existing code style and conventions. You can run `npm run prettier-check` and `npm run prettier-fix` as applicable.
16-
3. Test changes locally by running `npm test`
16+
3. Test changes locally by running `npm test` and `npm run test:e2e`
1717
4. Update documentation as needed
1818
5. Use clear commit messages explaining your changes
1919
6. Verify all changes work as expected

client/e2e/global-teardown.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { rimraf } from "rimraf";
2+
3+
async function globalTeardown() {
4+
if (!process.env.CI) {
5+
console.log("Cleaning up test-results directory...");
6+
// Add a small delay to ensure all Playwright files are written
7+
await new Promise((resolve) => setTimeout(resolve, 100));
8+
await rimraf("./e2e/test-results");
9+
console.log("Test-results directory cleaned up.");
10+
}
11+
}
12+
13+
export default globalTeardown;
14+
15+
// Call the function when this script is run directly
16+
if (import.meta.url === `file://${process.argv[1]}`) {
17+
globalTeardown().catch(console.error);
18+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { test, expect } from "@playwright/test";
2+
3+
// Adjust the URL if your dev server runs on a different port
4+
const APP_URL = "http://localhost:6274/";
5+
6+
test.describe("Transport Type Dropdown", () => {
7+
test("should have options for STDIO, SSE, and Streamable HTTP", async ({
8+
page,
9+
}) => {
10+
await page.goto(APP_URL);
11+
12+
// Wait for the Transport Type dropdown to be visible
13+
const selectTrigger = page.getByLabel("Transport Type");
14+
await expect(selectTrigger).toBeVisible();
15+
16+
// Open the dropdown
17+
await selectTrigger.click();
18+
19+
// Check for the three options
20+
await expect(page.getByRole("option", { name: "STDIO" })).toBeVisible();
21+
await expect(page.getByRole("option", { name: "SSE" })).toBeVisible();
22+
await expect(
23+
page.getByRole("option", { name: "Streamable HTTP" }),
24+
).toBeVisible();
25+
});
26+
27+
test("should show Command and Arguments fields and hide URL field when Transport Type is STDIO", async ({
28+
page,
29+
}) => {
30+
await page.goto(APP_URL);
31+
32+
// Wait for the Transport Type dropdown to be visible
33+
const selectTrigger = page.getByLabel("Transport Type");
34+
await expect(selectTrigger).toBeVisible();
35+
36+
// Open the dropdown and select STDIO
37+
await selectTrigger.click();
38+
await page.getByRole("option", { name: "STDIO" }).click();
39+
40+
// Wait for the form to update
41+
await page.waitForTimeout(100);
42+
43+
// Check that Command and Arguments fields are visible
44+
await expect(page.locator("#command-input")).toBeVisible();
45+
await expect(page.locator("#arguments-input")).toBeVisible();
46+
47+
// Check that URL field is not visible
48+
await expect(page.locator("#sse-url-input")).not.toBeVisible();
49+
50+
// Also verify the labels are present
51+
await expect(page.getByText("Command")).toBeVisible();
52+
await expect(page.getByText("Arguments")).toBeVisible();
53+
await expect(page.getByText("URL")).not.toBeVisible();
54+
});
55+
56+
test("should show URL field and hide Command and Arguments fields when Transport Type is SSE", async ({
57+
page,
58+
}) => {
59+
await page.goto(APP_URL);
60+
61+
// Wait for the Transport Type dropdown to be visible
62+
const selectTrigger = page.getByLabel("Transport Type");
63+
await expect(selectTrigger).toBeVisible();
64+
65+
// Open the dropdown and select SSE
66+
await selectTrigger.click();
67+
await page.getByRole("option", { name: "SSE" }).click();
68+
69+
// Wait for the form to update
70+
await page.waitForTimeout(100);
71+
72+
// Check that URL field is visible
73+
await expect(page.locator("#sse-url-input")).toBeVisible();
74+
75+
// Check that Command and Arguments fields are not visible
76+
await expect(page.locator("#command-input")).not.toBeVisible();
77+
await expect(page.locator("#arguments-input")).not.toBeVisible();
78+
79+
// Also verify the labels are present/absent
80+
await expect(page.getByText("URL")).toBeVisible();
81+
await expect(page.getByText("Command")).not.toBeVisible();
82+
await expect(page.getByText("Arguments")).not.toBeVisible();
83+
});
84+
85+
test("should show URL field and hide Command and Arguments fields when Transport Type is Streamable HTTP", async ({
86+
page,
87+
}) => {
88+
await page.goto(APP_URL);
89+
90+
// Wait for the Transport Type dropdown to be visible
91+
const selectTrigger = page.getByLabel("Transport Type");
92+
await expect(selectTrigger).toBeVisible();
93+
94+
// Open the dropdown and select Streamable HTTP
95+
await selectTrigger.click();
96+
await page.getByRole("option", { name: "Streamable HTTP" }).click();
97+
98+
// Wait for the form to update
99+
await page.waitForTimeout(100);
100+
101+
// Check that URL field is visible
102+
await expect(page.locator("#sse-url-input")).toBeVisible();
103+
104+
// Check that Command and Arguments fields are not visible
105+
await expect(page.locator("#command-input")).not.toBeVisible();
106+
await expect(page.locator("#arguments-input")).not.toBeVisible();
107+
108+
// Also verify the labels are present/absent
109+
await expect(page.getByText("URL")).toBeVisible();
110+
await expect(page.getByText("Command")).not.toBeVisible();
111+
await expect(page.getByText("Arguments")).not.toBeVisible();
112+
});
113+
});

client/jest.config.cjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@ module.exports = {
2121
"/node_modules/",
2222
"/dist/",
2323
"/bin/",
24+
"/e2e/",
2425
"\\.config\\.(js|ts|cjs|mjs)$",
2526
],
2627
// Exclude the same patterns from coverage reports
2728
coveragePathIgnorePatterns: [
2829
"/node_modules/",
2930
"/dist/",
3031
"/bin/",
32+
"/e2e/",
3133
"\\.config\\.(js|ts|cjs|mjs)$",
3234
],
3335
};

client/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
"lint": "eslint .",
2121
"preview": "vite preview --port 6274",
2222
"test": "jest --config jest.config.cjs",
23-
"test:watch": "jest --config jest.config.cjs --watch"
23+
"test:watch": "jest --config jest.config.cjs --watch",
24+
"test:e2e": "playwright test e2e && npm run cleanup:e2e",
25+
"cleanup:e2e": "node e2e/global-teardown.js"
2426
},
2527
"dependencies": {
2628
"@modelcontextprotocol/sdk": "^1.13.0",

client/playwright.config.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { defineConfig, devices } from "@playwright/test";
2+
3+
/**
4+
* @see https://playwright.dev/docs/test-configuration
5+
*/
6+
export default defineConfig({
7+
/* Run your local dev server before starting the tests */
8+
webServer: {
9+
cwd: "..",
10+
command: "npm run dev",
11+
url: "http://localhost:6274",
12+
reuseExistingServer: !process.env.CI,
13+
},
14+
15+
testDir: "./e2e",
16+
outputDir: "./e2e/test-results",
17+
/* Run tests in files in parallel */
18+
fullyParallel: true,
19+
/* Fail the build on CI if you accidentally left test.only in the source code. */
20+
forbidOnly: !!process.env.CI,
21+
/* Retry on CI only */
22+
retries: process.env.CI ? 2 : 0,
23+
/* Opt out of parallel tests on CI. */
24+
workers: process.env.CI ? 1 : undefined,
25+
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
26+
reporter: process.env.CI
27+
? [
28+
["html", { outputFolder: "playwright-report" }],
29+
["json", { outputFile: "results.json" }],
30+
["line"],
31+
]
32+
: [["line"]],
33+
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
34+
use: {
35+
/* Base URL to use in actions like `await page.goto('/')`. */
36+
baseURL: "http://localhost:6274",
37+
38+
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
39+
trace: "on-first-retry",
40+
41+
/* Take screenshots on failure */
42+
screenshot: "only-on-failure",
43+
44+
/* Record video on failure */
45+
video: "retain-on-failure",
46+
},
47+
48+
/* Configure projects for major browsers */
49+
projects: [
50+
{
51+
name: "chromium",
52+
use: { ...devices["Desktop Chrome"] },
53+
},
54+
55+
{
56+
name: "firefox",
57+
use: { ...devices["Desktop Firefox"] },
58+
},
59+
60+
// Skip WebKit on macOS due to compatibility issues
61+
...(process.platform !== "darwin"
62+
? [
63+
{
64+
name: "webkit",
65+
use: { ...devices["Desktop Safari"] },
66+
},
67+
]
68+
: []),
69+
],
70+
});

0 commit comments

Comments
 (0)