Skip to content

Commit 7545fb3

Browse files
authored
Enhance the Playwright test suite with comprehensive documentation, i… (#504)
* Enhance the Playwright test suite with comprehensive documentation, implementing proper page object naming conventions, and working tests cases Signed-off-by: Manav Gupta <[email protected]> * fix: resolve flake8 DAR203 error and pre-commit hook issues - Fix docstring return type mismatch in forward_request method - Update pre-commit config to exclude page objects from test naming - Add UTF-8 encoding pragma and fix file formatting These changes ensure code quality standards are met and resolve linting issues discovered during Playwright test development. * Update pytest.ini to include all test directories in testpaths Signed-off-by: Manav Gupta <[email protected]> * Updated pytest.ini to exclude playwright tests from make test and CI/CD Signed-off-by: Manav Gupta <[email protected]> * Resolve merge conflicts in .pre-commit-config.yaml and forward.py --------- Signed-off-by: Manav Gupta <[email protected]>
1 parent 32c5c10 commit 7545fb3

17 files changed

+459
-34
lines changed

Makefile

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2715,18 +2715,32 @@ playwright-install-all:
27152715
## --- UI Test Execution ------------------------------------------------------
27162716
test-ui: playwright-install
27172717
@echo "🎭 Running UI tests with visible browser..."
2718+
@echo "💡 Make sure the dev server is running: make dev"
27182719
@test -d "$(VENV_DIR)" || $(MAKE) venv
27192720
@mkdir -p $(PLAYWRIGHT_SCREENSHOTS) $(PLAYWRIGHT_REPORTS)
2721+
@if ! curl -s http://localhost:8000/health >/dev/null 2>&1; then \
2722+
echo "❌ Dev server not running on http://localhost:8000"; \
2723+
echo "💡 Start it with: make dev"; \
2724+
exit 1; \
2725+
fi
27202726
@/bin/bash -c "source $(VENV_DIR)/bin/activate && \
2721-
pytest $(PLAYWRIGHT_DIR)/ -v --headed --screenshot=only-on-failure \
2727+
export TEST_BASE_URL=http://localhost:8000 && \
2728+
python -m pytest tests/playwright/ -v --headed --screenshot=only-on-failure \
27222729
--browser chromium || { echo '❌ UI tests failed!'; exit 1; }"
27232730
@echo "✅ UI tests completed!"
27242731

27252732
test-ui-headless: playwright-install
27262733
@echo "🎭 Running UI tests in headless mode..."
2734+
@echo "💡 Make sure the dev server is running: make dev"
27272735
@test -d "$(VENV_DIR)" || $(MAKE) venv
27282736
@mkdir -p $(PLAYWRIGHT_SCREENSHOTS) $(PLAYWRIGHT_REPORTS)
2737+
@if ! curl -s http://localhost:8000/health >/dev/null 2>&1; then \
2738+
echo "❌ Dev server not running on http://localhost:8000"; \
2739+
echo "💡 Start it with: make dev"; \
2740+
exit 1; \
2741+
fi
27292742
@/bin/bash -c "source $(VENV_DIR)/bin/activate && \
2743+
export TEST_BASE_URL=http://localhost:8000 && \
27302744
pytest $(PLAYWRIGHT_DIR)/ -v --screenshot=only-on-failure \
27312745
--browser chromium || { echo '❌ UI tests failed!'; exit 1; }"
27322746
@echo "✅ UI tests completed!"

mcpgateway/templates/admin.html

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,11 @@ <h1 class="text-3xl font-bold text-gray-800 dark:text-gray-200">
4949
<div class="mb-8">
5050
<div class="border-b border-gray-200">
5151
<nav class="-mb-px flex space-x-8">
52-
<a href="#catalog" id="tab-catalog"
52+
<a href="#catalog" id="tab-catalog" data-testid="servers-tab"
5353
class="tab-link border-indigo-500 text-indigo-600 dark:text-indigo-500 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm">
5454
Virtual Servers Catalog
5555
</a>
56-
<a href="#tools" id="tab-tools"
56+
<a href="#tools" id="tab-tools" data-testid="tools-tab"
5757
class="tab-link border-transparent text-gray-500 dark:text-gray-300 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm">
5858
Global Tools
5959
</a>
@@ -65,7 +65,7 @@ <h1 class="text-3xl font-bold text-gray-800 dark:text-gray-200">
6565
class="tab-link border-transparent text-gray-500 dark:text-gray-300 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm">
6666
Global Prompts
6767
</a>
68-
<a href="#gateways" id="tab-gateways"
68+
<a href="#gateways" id="tab-gateways" data-testid="gateways-tab"
6969
class="tab-link border-transparent text-gray-500 dark:text-gray-300 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm">
7070
Gateways/MCP Servers
7171
</a>
@@ -95,14 +95,20 @@ <h1 class="text-3xl font-bold text-gray-800 dark:text-gray-200">
9595
<!-- Catalog Panel -->
9696
<div id="catalog-panel" class="tab-panel">
9797
<div class="flex justify-between items-center mb-4">
98-
<h2 class="text-2xl font-bold dark:text-gray-200">MCP Servers Catalog</h2>
99-
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">Virtual Servers let you combine Tools, Resources, and
100-
Prompts from the global Tools catalog into a reusable configuration.</p>
98+
<div>
99+
<h2 class="text-2xl font-bold dark:text-gray-200">MCP Servers Catalog</h2>
100+
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">Virtual Servers let you combine Tools, Resources, and
101+
Prompts from the global Tools catalog into a reusable configuration.</p>
102+
</div>
101103

102-
<div class="flex items-center">
103-
<input type="checkbox" id="show-inactive-servers" class="mr-2" onchange="toggleInactiveItems('servers')" />
104-
<label for="show-inactive-servers" class="text-sm font-medium text-gray-700 dark:text-gray-300">Show
105-
Inactive</label>
104+
<div class="flex items-center space-x-4">
105+
<input type="text" data-testid="search-input" placeholder="Search servers..."
106+
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-900 dark:placeholder-gray-300 dark:text-gray-300" />
107+
<div class="flex items-center">
108+
<input type="checkbox" id="show-inactive-servers" class="mr-2" onchange="toggleInactiveItems('servers')" />
109+
<label for="show-inactive-servers" class="text-sm font-medium text-gray-700 dark:text-gray-300">Show
110+
Inactive</label>
111+
</div>
106112
</div>
107113
</div>
108114
<div class="bg-white shadow rounded-lg p-6 mb-8 dark:bg-gray-800">
@@ -148,9 +154,9 @@ <h2 class="text-2xl font-bold dark:text-gray-200">MCP Servers Catalog</h2>
148154
</th>
149155
</tr>
150156
</thead>
151-
<tbody class="bg-white divide-y divide-gray-200 dark:bg-gray-900 dark:divide-gray-700">
157+
<tbody data-testid="server-list" class="bg-white divide-y divide-gray-200 dark:bg-gray-900 dark:divide-gray-700">
152158
{% for server in servers %}
153-
<tr>
159+
<tr data-testid="server-item">
154160
<td class="px-6 py-4 whitespace-nowrap">
155161
{% if server.icon %}
156162
<img src="{{ server.icon }}" alt="{{ server.name }} Icon" class="h-8 w-8" />
@@ -284,7 +290,7 @@ <h3 class="text-lg font-bold mb-4 dark:text-gray-200">Add New Server</h3>
284290
</div>
285291
</div>
286292
<div class="mt-6">
287-
<button type="submit"
293+
<button type="submit" data-testid="add-server-btn"
288294
x-tooltip="'💡Creates a new Virtual Server by combining Tools, Resources & Prompts from global catalogs.'"
289295
class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
290296
Add Server

playwright.config.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# -*- coding: utf-8 -*-
2+
import os
3+
from playwright.sync_api import Playwright
4+
5+
def pytest_configure(config):
6+
"""Configure Playwright for pytest runs."""
7+
os.environ.setdefault("PLAYWRIGHT_BROWSERS_PATH", os.path.expanduser("~/.cache/ms-playwright"))
8+
9+
def pytest_playwright_setup(playwright: Playwright):
10+
"""Setup Playwright browsers and configuration for pytest runs."""
11+
return {
12+
"base_url": os.getenv("TEST_BASE_URL", "http://localhost:8000"),
13+
"screenshot": "only-on-failure",
14+
"video": "retain-on-failure",
15+
"trace": "retain-on-failure",
16+
}

pytest.ini

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[pytest]
2+
addopts = --tb=short --strict-markers --disable-warnings --color=yes
3+
testpaths = tests tests/unit tests/integration
4+
5+
markers =
6+
smoke: marks tests as smoke tests
7+
slow: marks tests as slow
8+
auth: marks tests as authentication related
9+
htmx: marks tests as HTMX related
10+
api: marks tests as API related

tests/playwright/README.md

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,24 @@ This directory contains end-to-end UI tests for the MCP Context Forge admin inte
77
```
88
tests/playwright/
99
├── README.md # This file
10-
├── __init__.py # Package marker
1110
├── conftest.py # Pytest fixtures and configuration
1211
├── test_admin_ui.py # Admin panel UI tests
1312
├── test_api_endpoints.py # API integration tests via UI
14-
├── test_server_management.py # Server CRUD operations
13+
├── test_api_integration.py # API protocol tests
14+
├── test_auth.py # Authentication tests
15+
├── test_htmx_interactions.py # HTMX interaction tests
16+
├── test_realtime_features.py # Real-time feature tests
17+
├── entities/ # CRUD tests for admin entities
18+
│ ├── .gitkeep
19+
│ └── test_tools.py # Tools CRUD operations
20+
├── api/ # API protocol and REST endpoint tests
21+
│ └── .gitkeep
22+
├── fixtures/ # Shared fixtures, test data factories
23+
│ └── .gitkeep
1524
├── pages/ # Page Object Model
16-
│ ├── __init__.py
1725
│ ├── base_page.py # Base page class
1826
│ └── admin_page.py # Admin panel page objects
19-
├── screenshots/ # Test failure screenshots (auto-created)
27+
├── screenshots/ # Visual regression baseline images
2028
├── reports/ # Test reports (auto-created)
2129
└── videos/ # Test recordings (auto-created)
2230
```
@@ -248,6 +256,13 @@ Tests run automatically on GitHub Actions for:
248256
6. **Error Messages** - Include context in assertion messages
249257
7. **Cleanup** - Tests should clean up created resources
250258

259+
## 📂 Adding Tests
260+
261+
- Place CRUD tests in `entities/` directory
262+
- Place protocol/REST/error tests in `api/` directory
263+
- Add shared fixtures/page objects in `fixtures/` directory
264+
- Visual regression baselines go in `screenshots/` directory
265+
251266
## 🔍 Common Issues
252267

253268
### Server Not Running
@@ -299,3 +314,5 @@ page.click("button", timeout=10000) # 10 seconds for this action
299314
4. Include docstrings explaining test purpose
300315
5. Run `make test-ui` locally before submitting PR
301316
6. Ensure all smoke tests pass
317+
318+
See the main project README for more details.

tests/playwright/api/.gitkeep

Whitespace-only changes.

tests/playwright/conftest.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
# Third-Party
1313
from playwright.sync_api import APIRequestContext, Page, Playwright
1414
import pytest
15+
import re
16+
from playwright.sync_api import Page, Browser, BrowserContext
1517

1618
# Get configuration from environment
17-
BASE_URL = os.getenv("TEST_BASE_URL", "http://localhost:4444")
19+
BASE_URL = os.getenv("TEST_BASE_URL", "http://localhost:8000")
1820
API_TOKEN = os.getenv("MCP_AUTH_TOKEN", "test-token")
1921

2022
# Basic Auth credentials - these MUST be set in environment
@@ -58,6 +60,7 @@ def api_request_context(
5860
def page(browser) -> Generator[Page, None, None]:
5961
"""Create page with Basic Auth credentials."""
6062
context = browser.new_context(
63+
base_url=BASE_URL,
6164
http_credentials={
6265
"username": BASIC_AUTH_USER,
6366
"password": BASIC_AUTH_PASSWORD,
@@ -74,3 +77,34 @@ def page(browser) -> Generator[Page, None, None]:
7477
def authenticated_page(page: Page) -> Page:
7578
"""Alias for page fixture."""
7679
return page
80+
81+
@pytest.fixture
82+
def admin_page(page: Page):
83+
"""Provide a logged-in admin page for UI tests."""
84+
# Go directly to admin - HTTP Basic Auth is handled by the page fixture
85+
page.goto("/admin")
86+
# Verify we're on the admin page
87+
page.wait_for_url(re.compile(r".*admin"))
88+
return page
89+
90+
@pytest.fixture
91+
def test_tool_data():
92+
"""Provide test data for tool creation."""
93+
import uuid
94+
unique_id = uuid.uuid4()
95+
return {
96+
"name": f"test-api-tool-{unique_id}",
97+
"description": "Test API tool for automation",
98+
"url": "https://api.example.com/test",
99+
"integrationType": "REST",
100+
"requestType": "GET",
101+
"headers": '{"Authorization": "Bearer test-token"}',
102+
"input_schema": '{"type": "object", "properties": {"query": {"type": "string"}}}'
103+
}
104+
105+
@pytest.fixture(autouse=True)
106+
def setup_test_environment(page: Page):
107+
"""Set viewport and default timeout for consistent UI tests."""
108+
page.set_viewport_size({"width": 1280, "height": 720})
109+
page.set_default_timeout(30000)
110+
# Optionally, add request logging or interception here
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# -*- coding: utf-8 -*-
2+
import pytest
3+
from playwright.sync_api import Page, expect
4+
5+
class TestToolsCRUD:
6+
"""CRUD tests for Tools entity in MCP Gateway Admin UI.
7+
8+
Examples:
9+
pytest tests/playwright/entities/test_tools.py
10+
"""
11+
def test_create_new_tool(self, page: Page, test_tool_data, admin_page):
12+
"""Test creating a new tool with debug screenshots and waits."""
13+
# Go to the Global Tools tab (if not already there)
14+
page.click('[data-testid="tools-tab"]')
15+
16+
# Wait for the tools panel to be visible
17+
page.wait_for_selector('#tools-panel:not(.hidden)')
18+
19+
# Add a small delay to ensure the UI has time to update
20+
page.wait_for_timeout(500)
21+
22+
# Fill the always-visible form
23+
page.locator('#add-tool-form [name="name"]').fill(test_tool_data["name"])
24+
page.wait_for_timeout(300)
25+
page.locator('#add-tool-form [name="url"]').fill(test_tool_data["url"])
26+
page.wait_for_timeout(300)
27+
page.locator('#add-tool-form [name="description"]').fill(test_tool_data["description"])
28+
page.wait_for_timeout(300)
29+
page.locator('#add-tool-form [name="integrationType"]').select_option(test_tool_data["integrationType"])
30+
page.wait_for_timeout(300)
31+
32+
# Submit the form
33+
page.click('#add-tool-form button[type="submit"]')
34+
35+
# Assert the tool appears in the table
36+
expect(page.locator("#tools-panel table")).to_contain_text(test_tool_data["name"])
37+
38+
def test_delete_tool(self, page: Page, test_tool_data, admin_page):
39+
"""Test deleting a tool."""
40+
# Go to the Global Tools tab (if not already there)
41+
page.click('[data-testid="tools-tab"]')
42+
43+
# Wait for the tools panel to be visible
44+
page.wait_for_selector('#tools-panel:not(.hidden)')
45+
46+
# Create tool first
47+
page.locator('#add-tool-form [name="name"]').fill(test_tool_data["name"])
48+
page.wait_for_timeout(300)
49+
page.locator('#add-tool-form [name="url"]').fill(test_tool_data["url"])
50+
page.wait_for_timeout(300)
51+
page.locator('#add-tool-form [name="description"]').fill(test_tool_data["description"])
52+
page.wait_for_timeout(300)
53+
page.locator('#add-tool-form [name="integrationType"]').select_option(test_tool_data["integrationType"])
54+
page.wait_for_timeout(300)
55+
page.click('#add-tool-form button[type="submit"]')
56+
expect(page.locator("#tools-panel table")).to_contain_text(test_tool_data["name"])
57+
58+
# Delete tool
59+
tool_row = page.locator(f'#tools-panel tbody tr:has-text("{test_tool_data["name"]}")')
60+
61+
# Set up dialog handler before clicking delete
62+
page.on("dialog", lambda dialog: dialog.accept())
63+
64+
tool_row.locator('button:has-text("Delete")').click()
65+
66+
# Wait a moment for the deletion to process
67+
page.wait_for_timeout(1000)
68+
69+
# Assert the tool is no longer in the table
70+
expect(page.locator(f'#tools-panel tbody tr:has-text("{test_tool_data["name"]}")')).not_to_be_visible()

tests/playwright/fixtures/.gitkeep

Whitespace-only changes.

tests/playwright/pages/test_admin_page.py renamed to tests/playwright/pages/admin_page.py

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ class AdminPage(BasePage):
1212
"""Admin panel page object."""
1313

1414
# Selectors
15-
SERVERS_TAB = '[data-tab="servers"]'
16-
TOOLS_TAB = '[data-tab="tools"]'
17-
GATEWAYS_TAB = '[data-tab="gateways"]'
18-
ADD_SERVER_BTN = 'button:has-text("Add Server")'
19-
SERVER_LIST = ".server-list"
20-
SERVER_ITEM = ".server-item"
21-
SEARCH_INPUT = 'input[placeholder*="Search"]'
15+
SERVERS_TAB = '[data-testid="servers-tab"]'
16+
TOOLS_TAB = '[data-testid="tools-tab"]'
17+
GATEWAYS_TAB = '[data-testid="gateways-tab"]'
18+
ADD_SERVER_BTN = '[data-testid="add-server-btn"]'
19+
SERVER_LIST = '[data-testid="server-list"]'
20+
SERVER_ITEM = '[data-testid="server-item"]'
21+
SEARCH_INPUT = '[data-testid="search-input"]'
22+
SERVER_NAME_INPUT = 'input[name="name"]'
23+
SERVER_ICON_INPUT = 'input[name="icon"]'
2224

2325
def __init__(self, page: Page, base_url: str):
2426
super().__init__(page)
@@ -42,14 +44,11 @@ def click_gateways_tab(self) -> None:
4244
"""Click on gateways tab."""
4345
self.click_element(self.GATEWAYS_TAB)
4446

45-
def add_server(self, name: str, url: str) -> None:
47+
def add_server(self, name: str, icon_url: str) -> None:
4648
"""Add a new server."""
49+
self.fill_input(self.SERVER_NAME_INPUT, name)
50+
self.fill_input(self.SERVER_ICON_INPUT, icon_url)
4751
self.click_element(self.ADD_SERVER_BTN)
48-
# Wait for modal
49-
self.wait_for_element('input[name="name"]')
50-
self.fill_input('input[name="name"]', name)
51-
self.fill_input('input[name="url"]', url)
52-
self.click_element('button:has-text("Save")')
5352

5453
def search_servers(self, query: str) -> None:
5554
"""Search for servers."""

0 commit comments

Comments
 (0)