Initialize Clash Web source and deployment documentation
This commit is contained in:
commit
8e0fa00a73
66 files changed
+8532
No files matched your search
@@ -0,0 +1,147 @@
|
||||
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<string, string>)[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([])
|
||||
})
|
||||
Reference in new issue
Block a user