import { test, expect } from '@playwright/test' test('real workspace: auth, subscription, selection, rules, mobile and backup', async ({ page }) => { const errors: string[] = [] page.on('pageerror', error => errors.push(error.message)) await page.goto('/') await expect(page.getByRole('heading', { name: '从这里,连接世界。' })).toBeVisible() await page.getByPlaceholder('从服务启动日志复制令牌').fill(process.env.CW_E2E_SETUP_TOKEN!) await page.getByPlaceholder('至少 12 字节,最多 72 字节').fill('e2e-test-password-long') await page.getByRole('button', { name: '创建工作空间' }).click() await expect(page.getByRole('heading', { name: '网络总览' })).toBeVisible() await expect(page.getByText('规则代理已启用', { exact: true })).toBeVisible() const auth = await (await page.request.get('/api/v1/auth/status')).json() const settings = await (await page.request.get('/api/v1/settings')).json() // The local proxy answers this documentation address; no Internet request. // A health target equal to the proxy endpoint is rejected as a routing loop. const settingsResponse = await page.request.put('/api/v1/settings', { headers: { 'X-CSRF-Token': auth.csrf }, data: { ...settings, testURL: 'http://198.51.100.42/health' } }) expect(settingsResponse.ok()).toBe(true) await page.getByRole('link', { name: '订阅管理', exact: true }).click() await page.getByRole('button', { name: '添加订阅', exact: true }).click() await page.getByPlaceholder('例如:我的主力订阅').fill('本地测试订阅') await page.getByPlaceholder('https://example.com/subscribe?token=…').fill(process.env.CW_E2E_SUBSCRIPTION_URL!) await page.getByRole('button', { name: '添加并更新' }).click() await expect(page.getByRole('heading', { name: '本地测试订阅', exact: true })).toBeVisible() await expect(page.locator('.subscription-node-count')).toContainText('3') await page.getByRole('link', { name: '代理节点', exact: false }).first().click() await expect(page.getByRole('heading', { name: '香港 · 测试节点' })).toBeVisible() await page.getByPlaceholder('搜索节点、协议或订阅').fill('香港') await expect(page.locator('.node-card')).toHaveCount(1) await page.getByRole('button', { name: '选择节点', exact: true }).click() await expect(page.getByRole('button', { name: '当前节点', exact: true })).toBeVisible() await page.getByPlaceholder('搜索节点、协议或订阅').clear() await page.getByRole('button', { name: '测试延迟', exact: true }).click() await expect(page.getByText('3 个最近测试可用', { exact: false })).toBeVisible() // Test progressive results without contacting third-party services in browser CI. let connectivityRequests: string[] = [] await page.route('**/api/v1/system/connectivity/test', async route => { const id = route.request().postDataJSON().siteId connectivityRequests.push(id) await new Promise(resolve => setTimeout(resolve, 80)) const status = ({ claude: 'challenge', netflix: 'restricted', grok: 'failed', github: 'http_error' } as Record)[id] || 'reachable' await route.fulfill({ json: { siteId: id, status, detail: status === 'reachable' ? 'HTTPS 主页响应正常' : '测试夹具响应', httpStatus: status === 'failed' ? 0 : status === 'reachable' ? 200 : status === 'http_error' ? 503 : 403, durationMS: 640, tlsVerified: status !== 'failed', checkedAt: new Date().toISOString() } }) }) await page.getByRole('link', { name: '连通性测试', exact: true }).click() await expect(page.locator('.connectivity-card')).toHaveCount(9) await expect(page.locator('.connectivity-grid')).toHaveCount(1) await expect(page.locator('.connectivity-card-heading h3')).toHaveText(['GitHub', 'Docker Hub', 'Apple', 'Google', 'Netflix', 'YouTube', 'ChatGPT', 'Claude', 'Grok']) await expect(page.locator('.connectivity-state.reachable')).toHaveCount(0) await page.getByRole('button', { name: '测试 Apple', exact: true }).click() await expect(page.locator('[data-site="apple"] .connectivity-state')).toContainText('可访问') expect(connectivityRequests).toEqual(['apple']) await page.getByRole('button', { name: '一键测试', exact: true }).click() await expect(page.getByText('本次完成 9 / 9', { exact: true })).toBeVisible() expect(connectivityRequests.length).toBe(10) expect(connectivityRequests.slice(1)).toEqual(['github', 'dockerhub', 'apple', 'google', 'netflix', 'youtube', 'chatgpt', 'claude', 'grok']) await expect(page.locator('.connectivity-state.reachable')).toHaveCount(5) await expect(page.locator('[data-site="claude"] .connectivity-state')).toContainText('需要验证') await expect(page.locator('[data-site="netflix"] .connectivity-state')).toContainText('访问受限') await expect(page.locator('[data-site="grok"] .connectivity-state')).toContainText('连接失败') await expect(page.locator('[data-site="github"]')).toContainText('HTTP 503') await page.setViewportSize({ width: 1366, height: 900 }) const lastResult = await page.locator('.connectivity-card').last().boundingBox() expect(lastResult).not.toBeNull() expect(lastResult!.y + lastResult!.height).toBeLessThanOrEqual(900) for (const pair of [['github', 'dockerhub'], ['netflix', 'youtube'], ['chatgpt', 'claude'], ['claude', 'grok']]) { const left = await page.locator(`[data-site="${pair[0]}"]`).boundingBox() const right = await page.locator(`[data-site="${pair[1]}"]`).boundingBox() expect(Math.abs(left!.y - right!.y)).toBeLessThan(1) } expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true) await page.setViewportSize({ width: 1440, height: 1080 }) await page.screenshot({ path: '../.cache/screenshots/connectivity-desktop.png', fullPage: true, animations: 'disabled', style: '.n-message-container { visibility: hidden; }' }) await page.getByRole('button', { name: '切换深色主题' }).click() await page.screenshot({ path: '../.cache/screenshots/connectivity-dark.png', fullPage: true, animations: 'disabled', style: '.n-message-container { visibility: hidden; }' }) await page.getByRole('button', { name: '切换浅色主题' }).click() await page.setViewportSize({ width: 390, height: 844 }) expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true) await page.screenshot({ path: '../.cache/screenshots/connectivity-mobile.png', fullPage: true, animations: 'disabled', style: '.n-message-container { visibility: hidden; }' }) await page.setViewportSize({ width: 1440, height: 1080 }) await page.unroute('**/api/v1/system/connectivity/test') connectivityRequests = [] await page.route('**/api/v1/system/connectivity/test', async route => { connectivityRequests.push(route.request().postDataJSON().siteId) await new Promise(resolve => setTimeout(resolve, 1000)) await route.fulfill({ json: { siteId: route.request().postDataJSON().siteId, status: 'reachable', detail: 'ok', httpStatus: 200, durationMS: 1000, tlsVerified: true, checkedAt: new Date().toISOString() } }).catch(() => {}) }) await page.getByRole('button', { name: '一键测试', exact: true }).click() await expect(page.locator('.connectivity-state.testing')).toHaveCount(3) await page.getByRole('button', { name: '取消测试', exact: true }).click() await expect(page.locator('.connectivity-state.cancelled')).toHaveCount(9) await expect(page.getByRole('button', { name: '一键测试', exact: true })).toBeEnabled() expect(connectivityRequests.length).toBe(3) await page.unroute('**/api/v1/system/connectivity/test') await page.getByRole('link', { name: '分流规则', exact: true }).click() await page.getByRole('button', { name: '添加规则' }).click() await page.getByPlaceholder('例如 example.com').fill('custom.example.com') await page.getByRole('button', { name: '保存并应用' }).click() await expect(page.locator('.data-table')).toContainText('custom.example.com') await page.getByRole('link', { name: '连接与日志', exact: true }).click() await page.getByText('内核日志', { exact: true }).click() await expect(page.locator('.log-console')).toBeVisible() await page.getByRole('link', { name: '系统设置', exact: true }).click() await expect(page.getByText('当前光猫 DHCP 无法关闭。', { exact: false })).toBeVisible() await page.route('**/api/v1/system/updates', route => route.fulfill({ json: { currentVersion: 'v1.19.32', latestVersion: 'v1.19.33', updateAvailable: true, checkedAt: new Date().toISOString(), publishedAt: new Date().toISOString(), releaseURL: 'https://github.com/MetaCubeX/mihomo/releases/tag/v1.19.33', } })) await page.getByRole('button', { name: '检查更新', exact: true }).click() await expect(page.getByText('发现新版本,可查看发布说明后手动升级。', { exact: true })).toBeVisible() await expect(page.getByRole('link', { name: '查看官方发布说明' })).toHaveAttribute('href', 'https://github.com/MetaCubeX/mihomo/releases/tag/v1.19.33') await page.unroute('**/api/v1/system/updates') await page.route('**/api/v1/system/updates', route => route.fulfill({ status: 502, json: { error: 'GitHub 更新检查暂时受限,请稍后重试' } })) await page.getByRole('button', { name: '检查更新', exact: true }).click() await expect(page.getByText('GitHub 更新检查暂时受限,请稍后重试', { exact: true })).toBeVisible() await page.getByText('如何升级软件与内核', { exact: true }).click() await expect(page.getByText('完整操作与恢复步骤见项目 docs/upgrading.md。', { exact: true })).toBeVisible() const downloadPromise = page.waitForEvent('download') await page.getByRole('button', { name: '导出备份' }).click() const download = await downloadPromise expect(download.suggestedFilename()).toMatch(/^clashweb-backup/) await page.getByRole('button', { name: '切换深色主题' }).click() await expect(page.locator('.application')).toHaveClass(/dark/) await page.getByRole('button', { name: '切换浅色主题' }).click() await page.getByRole('link', { name: '网络总览', exact: true }).click() await page.setViewportSize({ width: 1440, height: 1080 }) await expect(page.getByRole('img', { name: 'Clash Web 网络猫' })).toBeVisible() await page.screenshot({ path: '../.cache/screenshots/dashboard-desktop.png', fullPage: true, animations: 'disabled', style: '.n-message-container { visibility: hidden; }' }) await page.getByRole('button', { name: '切换深色主题' }).click() await page.screenshot({ path: '../.cache/screenshots/dashboard-dark.png', fullPage: true, animations: 'disabled', style: '.n-message-container { visibility: hidden; }' }) await page.getByRole('button', { name: '切换浅色主题' }).click() await page.setViewportSize({ width: 390, height: 844 }) await page.getByRole('button', { name: '打开导航' }).click() await page.getByRole('link', { name: '代理节点', exact: false }).first().click() await expect(page.getByRole('heading', { name: '代理节点', exact: true })).toBeVisible() await expect(page.locator('.node-card')).toHaveCount(3) expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true) await page.screenshot({ path: '../.cache/screenshots/nodes-mobile.png', fullPage: true, animations: 'disabled', style: '.n-message-container { visibility: hidden; }' }) await page.getByRole('button', { name: '打开导航' }).click() await page.getByRole('button', { name: '退出登录' }).click() await expect(page.getByRole('heading', { name: '欢迎回来。' })).toBeVisible() await page.getByPlaceholder('输入你的密码').fill('e2e-test-password-long') await page.getByRole('button', { name: '进入工作空间' }).click() await expect(page.getByRole('heading', { name: '代理节点', exact: true })).toBeVisible() await expect(page.getByRole('button', { name: '当前节点', exact: true })).toBeVisible() expect(errors).toEqual([]) })