379 lines
14 KiB
Python
379 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""E2E tests for global connection management features.
|
|
|
|
Tests the MesApi client, Toast notifications, and page functionality
|
|
using Playwright.
|
|
|
|
Run with: pytest tests/e2e/ --headed (to see browser)
|
|
"""
|
|
|
|
import pytest
|
|
import re
|
|
from playwright.sync_api import Page, expect
|
|
|
|
|
|
@pytest.mark.e2e
|
|
class TestPortalPage:
|
|
"""E2E tests for the Portal page."""
|
|
|
|
def test_portal_loads_successfully(self, page: Page, app_server: str):
|
|
"""Portal page should load without errors."""
|
|
page.goto(app_server)
|
|
|
|
# Wait for page to load
|
|
expect(page.locator('h1')).to_contain_text('MES 報表入口')
|
|
|
|
def test_portal_has_all_tabs(self, page: Page, app_server: str):
|
|
"""Portal should have all navigation tabs."""
|
|
page.goto(app_server)
|
|
|
|
# Check released tabs exist
|
|
expect(page.locator('.tab:has-text("WIP 即時概況")')).to_be_visible()
|
|
expect(page.locator('.tab:has-text("設備即時概況")')).to_be_visible()
|
|
expect(page.locator('.tab:has-text("設備歷史績效")')).to_be_visible()
|
|
expect(page.locator('.tab:has-text("設備維修查詢")')).to_be_visible()
|
|
expect(page.locator('.tab:has-text("批次追蹤工具")')).to_be_visible()
|
|
|
|
def test_portal_tab_switching(self, page: Page, app_server: str):
|
|
"""Portal tabs should switch iframe content."""
|
|
page.goto(app_server)
|
|
|
|
# Click on a different tab
|
|
page.locator('.tab:has-text("設備即時概況")').click()
|
|
|
|
# Verify the tab is active
|
|
expect(page.locator('.tab:has-text("設備即時概況")')).to_have_class(re.compile(r'active'))
|
|
|
|
def test_portal_health_popup_clickable(self, page: Page, app_server: str):
|
|
"""Health status pill should toggle popup visibility on click."""
|
|
page.goto(app_server)
|
|
|
|
popup = page.locator('#healthPopup')
|
|
expect(popup).not_to_have_class(re.compile(r'show'))
|
|
|
|
page.locator('#healthStatus').click()
|
|
expect(popup).to_have_class(re.compile(r'show'))
|
|
|
|
|
|
@pytest.mark.e2e
|
|
class TestToastNotifications:
|
|
"""E2E tests for Toast notification system."""
|
|
|
|
def test_toast_container_exists(self, page: Page, app_server: str):
|
|
"""Toast container should be present in the DOM."""
|
|
page.goto(f"{app_server}/wip-overview")
|
|
|
|
# Toast container should exist in DOM (hidden when empty, which is expected)
|
|
page.wait_for_selector('#mes-toast-container', state='attached', timeout=5000)
|
|
|
|
def test_toast_info_display(self, page: Page, app_server: str):
|
|
"""Toast.info() should display info notification."""
|
|
page.goto(f"{app_server}/wip-overview")
|
|
|
|
# Execute Toast.info() in browser context
|
|
page.evaluate("Toast.info('Test info message')")
|
|
|
|
# Verify toast appears
|
|
toast = page.locator('.mes-toast-info')
|
|
expect(toast).to_be_visible()
|
|
expect(toast).to_contain_text('Test info message')
|
|
|
|
def test_toast_success_display(self, page: Page, app_server: str):
|
|
"""Toast.success() should display success notification."""
|
|
page.goto(f"{app_server}/wip-overview")
|
|
|
|
page.evaluate("Toast.success('Operation successful')")
|
|
|
|
toast = page.locator('.mes-toast-success')
|
|
expect(toast).to_be_visible()
|
|
expect(toast).to_contain_text('Operation successful')
|
|
|
|
def test_toast_error_display(self, page: Page, app_server: str):
|
|
"""Toast.error() should display error notification."""
|
|
page.goto(f"{app_server}/wip-overview")
|
|
|
|
page.evaluate("Toast.error('An error occurred')")
|
|
|
|
toast = page.locator('.mes-toast-error')
|
|
expect(toast).to_be_visible()
|
|
expect(toast).to_contain_text('An error occurred')
|
|
|
|
def test_toast_error_with_retry(self, page: Page, app_server: str):
|
|
"""Toast.error() with retry callback should show retry button."""
|
|
page.goto(f"{app_server}/wip-overview")
|
|
|
|
page.evaluate("Toast.error('Connection failed', { retry: () => console.log('retry clicked') })")
|
|
|
|
# Verify retry button exists
|
|
retry_btn = page.locator('.mes-toast-retry')
|
|
expect(retry_btn).to_be_visible()
|
|
expect(retry_btn).to_contain_text('重試')
|
|
|
|
def test_toast_loading_display(self, page: Page, app_server: str):
|
|
"""Toast.loading() should display loading notification."""
|
|
page.goto(f"{app_server}/wip-overview")
|
|
|
|
page.evaluate("Toast.loading('Loading data...')")
|
|
|
|
toast = page.locator('.mes-toast-loading')
|
|
expect(toast).to_be_visible()
|
|
|
|
def test_toast_dismiss(self, page: Page, app_server: str):
|
|
"""Toast.dismiss() should remove toast."""
|
|
page.goto(f"{app_server}/wip-overview")
|
|
|
|
# Create and dismiss a toast
|
|
toast_id = page.evaluate("Toast.info('Will be dismissed')")
|
|
page.evaluate(f"Toast.dismiss({toast_id})")
|
|
|
|
# Wait for animation
|
|
page.wait_for_timeout(500)
|
|
|
|
# Toast should be gone
|
|
expect(page.locator('.mes-toast-info')).not_to_be_visible()
|
|
|
|
def test_toast_max_limit(self, page: Page, app_server: str):
|
|
"""Toast system should enforce max 5 toasts."""
|
|
page.goto(f"{app_server}/wip-overview")
|
|
|
|
# Create 7 toasts
|
|
for i in range(7):
|
|
page.evaluate(f"Toast.info('Toast {i}')")
|
|
|
|
# Should only have 5 toasts visible
|
|
toasts = page.locator('.mes-toast')
|
|
expect(toasts).to_have_count(5)
|
|
|
|
|
|
@pytest.mark.e2e
|
|
class TestMesApiClient:
|
|
"""E2E tests for MesApi client."""
|
|
|
|
def test_mesapi_exists_on_page(self, page: Page, app_server: str):
|
|
"""MesApi should be available in window scope."""
|
|
page.goto(f"{app_server}/wip-overview")
|
|
|
|
has_mesapi = page.evaluate("typeof MesApi !== 'undefined'")
|
|
assert has_mesapi, "MesApi should be defined"
|
|
|
|
def test_mesapi_has_get_method(self, page: Page, app_server: str):
|
|
"""MesApi should have get() method."""
|
|
page.goto(f"{app_server}/wip-overview")
|
|
|
|
has_get = page.evaluate("typeof MesApi.get === 'function'")
|
|
assert has_get, "MesApi.get should be a function"
|
|
|
|
def test_mesapi_has_post_method(self, page: Page, app_server: str):
|
|
"""MesApi should have post() method."""
|
|
page.goto(f"{app_server}/wip-overview")
|
|
|
|
has_post = page.evaluate("typeof MesApi.post === 'function'")
|
|
assert has_post, "MesApi.post should be a function"
|
|
|
|
def test_mesapi_request_logging(self, page: Page, app_server: str):
|
|
"""MesApi should log requests to console."""
|
|
page.goto(f"{app_server}/wip-overview")
|
|
|
|
# Capture console messages
|
|
console_messages = []
|
|
page.on("console", lambda msg: console_messages.append(msg.text))
|
|
|
|
# Make a request (will fail but should log)
|
|
page.evaluate("""
|
|
(async () => {
|
|
try {
|
|
await MesApi.get('/api/test-endpoint');
|
|
} catch (e) {
|
|
// Expected to fail
|
|
}
|
|
})()
|
|
""")
|
|
|
|
page.wait_for_timeout(1000)
|
|
|
|
# Check for MesApi log pattern
|
|
mesapi_logs = [m for m in console_messages if '[MesApi]' in m]
|
|
assert len(mesapi_logs) > 0, "MesApi should log requests with [MesApi] prefix"
|
|
|
|
|
|
@pytest.mark.e2e
|
|
class TestWIPOverviewPage:
|
|
"""E2E tests for WIP Overview page."""
|
|
|
|
def test_wip_overview_loads(self, page: Page, app_server: str):
|
|
"""WIP Overview page should load."""
|
|
page.goto(f"{app_server}/wip-overview")
|
|
|
|
# Page should have the header
|
|
expect(page.locator('body')).to_be_visible()
|
|
|
|
def test_wip_overview_has_toast_system(self, page: Page, app_server: str):
|
|
"""WIP Overview should have Toast system loaded."""
|
|
page.goto(f"{app_server}/wip-overview")
|
|
|
|
has_toast = page.evaluate("typeof Toast !== 'undefined'")
|
|
assert has_toast, "Toast should be defined on WIP Overview page"
|
|
|
|
def test_wip_overview_has_mesapi(self, page: Page, app_server: str):
|
|
"""WIP Overview should have MesApi loaded."""
|
|
page.goto(f"{app_server}/wip-overview")
|
|
|
|
has_mesapi = page.evaluate("typeof MesApi !== 'undefined'")
|
|
assert has_mesapi, "MesApi should be defined on WIP Overview page"
|
|
|
|
|
|
@pytest.mark.e2e
|
|
class TestWIPDetailPage:
|
|
"""E2E tests for WIP Detail page."""
|
|
|
|
def test_wip_detail_loads(self, page: Page, app_server: str):
|
|
"""WIP Detail page should load."""
|
|
page.goto(f"{app_server}/wip-detail")
|
|
|
|
expect(page.locator('body')).to_be_visible()
|
|
|
|
def test_wip_detail_has_toast_system(self, page: Page, app_server: str):
|
|
"""WIP Detail should have Toast system loaded."""
|
|
page.goto(f"{app_server}/wip-detail")
|
|
|
|
has_toast = page.evaluate("typeof Toast !== 'undefined'")
|
|
assert has_toast, "Toast should be defined on WIP Detail page"
|
|
|
|
def test_wip_detail_has_mesapi(self, page: Page, app_server: str):
|
|
"""WIP Detail should have MesApi loaded."""
|
|
page.goto(f"{app_server}/wip-detail")
|
|
|
|
has_mesapi = page.evaluate("typeof MesApi !== 'undefined'")
|
|
assert has_mesapi, "MesApi should be defined on WIP Detail page"
|
|
|
|
|
|
@pytest.mark.e2e
|
|
class TestTablesPage:
|
|
"""E2E tests for Tables page."""
|
|
|
|
def test_tables_page_loads(self, page: Page, app_server: str):
|
|
"""Tables page should load."""
|
|
page.goto(f"{app_server}/tables")
|
|
header = page.locator('h1')
|
|
expect(header).to_be_visible()
|
|
text = header.inner_text()
|
|
assert (
|
|
'MES 數據表查詢工具' in text
|
|
or '頁面開發中' in text
|
|
)
|
|
|
|
def test_tables_has_toast_system(self, page: Page, app_server: str):
|
|
"""Tables page should have Toast system loaded."""
|
|
page.goto(f"{app_server}/tables")
|
|
|
|
has_toast = page.evaluate("typeof Toast !== 'undefined'")
|
|
assert has_toast, "Toast should be defined on Tables page"
|
|
|
|
def test_tables_has_mesapi(self, page: Page, app_server: str):
|
|
"""Tables page should have MesApi loaded."""
|
|
page.goto(f"{app_server}/tables")
|
|
|
|
has_mesapi = page.evaluate("typeof MesApi !== 'undefined'")
|
|
assert has_mesapi, "MesApi should be defined on Tables page"
|
|
|
|
|
|
@pytest.mark.e2e
|
|
class TestResourcePage:
|
|
"""E2E tests for Resource Status page."""
|
|
|
|
def test_resource_page_loads(self, page: Page, app_server: str):
|
|
"""Resource page should load."""
|
|
page.goto(f"{app_server}/resource")
|
|
|
|
expect(page.locator('body')).to_be_visible()
|
|
|
|
def test_resource_has_toast_system(self, page: Page, app_server: str):
|
|
"""Resource page should have Toast system loaded."""
|
|
page.goto(f"{app_server}/resource")
|
|
|
|
has_toast = page.evaluate("typeof Toast !== 'undefined'")
|
|
assert has_toast, "Toast should be defined on Resource page"
|
|
|
|
def test_resource_has_mesapi(self, page: Page, app_server: str):
|
|
"""Resource page should have MesApi loaded."""
|
|
page.goto(f"{app_server}/resource")
|
|
|
|
has_mesapi = page.evaluate("typeof MesApi !== 'undefined'")
|
|
assert has_mesapi, "MesApi should be defined on Resource page"
|
|
|
|
|
|
@pytest.mark.e2e
|
|
class TestExcelQueryPage:
|
|
"""E2E tests for Excel Query page."""
|
|
|
|
def test_excel_query_page_loads(self, page: Page, app_server: str):
|
|
"""Excel Query page should load."""
|
|
page.goto(f"{app_server}/excel-query")
|
|
|
|
expect(page.locator('body')).to_be_visible()
|
|
|
|
def test_excel_query_has_toast_system(self, page: Page, app_server: str):
|
|
"""Excel Query page should have Toast system loaded."""
|
|
page.goto(f"{app_server}/excel-query")
|
|
|
|
has_toast = page.evaluate("typeof Toast !== 'undefined'")
|
|
assert has_toast, "Toast should be defined on Excel Query page"
|
|
|
|
def test_excel_query_has_mesapi(self, page: Page, app_server: str):
|
|
"""Excel Query page should have MesApi loaded."""
|
|
page.goto(f"{app_server}/excel-query")
|
|
|
|
has_mesapi = page.evaluate("typeof MesApi !== 'undefined'")
|
|
assert has_mesapi, "MesApi should be defined on Excel Query page"
|
|
|
|
|
|
@pytest.mark.e2e
|
|
class TestConsoleLogVerification:
|
|
"""E2E tests for console log verification (Phase 4.2 tasks)."""
|
|
|
|
def test_request_has_request_id(self, page: Page, app_server: str):
|
|
"""API requests should log with req_xxx ID format."""
|
|
page.goto(f"{app_server}/wip-overview")
|
|
|
|
console_messages = []
|
|
page.on("console", lambda msg: console_messages.append(msg.text))
|
|
|
|
# Trigger an API request
|
|
page.evaluate("""
|
|
(async () => {
|
|
try {
|
|
await MesApi.get('/api/wip/overview/summary');
|
|
} catch (e) {}
|
|
})()
|
|
""")
|
|
|
|
page.wait_for_timeout(2000)
|
|
|
|
# Check for request ID pattern
|
|
req_id_pattern = re.compile(r'req_\d{4}')
|
|
has_req_id = any(req_id_pattern.search(m) for m in console_messages)
|
|
assert has_req_id, "Console should show request ID like req_0001"
|
|
|
|
def test_successful_request_shows_checkmark(self, page: Page, app_server: str):
|
|
"""Successful requests should show checkmark in console."""
|
|
page.goto(f"{app_server}/wip-overview")
|
|
|
|
console_messages = []
|
|
page.on("console", lambda msg: console_messages.append(msg.text))
|
|
|
|
# Make request to a working endpoint
|
|
page.evaluate("""
|
|
(async () => {
|
|
try {
|
|
await MesApi.get('/api/wip/overview/summary');
|
|
} catch (e) {}
|
|
})()
|
|
""")
|
|
|
|
page.wait_for_timeout(3000)
|
|
|
|
# Filter for MesApi logs
|
|
mesapi_logs = [m for m in console_messages if '[MesApi]' in m]
|
|
# The exact checkmark depends on implementation (✓ or similar)
|
|
assert len(mesapi_logs) > 0, "Should have MesApi console logs"
|