feat: 添加MinIO文件系统支持并优化外部连接器功能
- 添加MinIO用户文件系统配置选项(BEAVER_MINIO_ROOT_USER等) - 更新外部连接器配置结构,包括BASE_URL和认证令牌设置 - 改进connector provider支持更多类型(official, feishu_bot等) - 实现Mistral模型推理模式支持reasoning_effort参数 - 增强外部连接器策略配置和运行时配置管理 - 添加connector bridge事件验证和安全保护机制 - 优化任务路由逻辑,区分simple_chat和new_task场景 - 更新初始技能工具提示配置,分离authoring admin功能
This commit is contained in:
9
app-instance/frontend/test-results/.last-run.json
Normal file
9
app-instance/frontend/test-results/.last-run.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"status": "failed",
|
||||
"failedTests": [
|
||||
"f5227d990c583bf1cb5e-1c04b5ceddc2ff622275",
|
||||
"f5227d990c583bf1cb5e-960e0b45f6e36c90c9ae",
|
||||
"f5227d990c583bf1cb5e-205bf022968074780a04",
|
||||
"f5227d990c583bf1cb5e-ebfd66136ede563d8670"
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,281 @@
|
||||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: market-settings-qa.spec.js >> marketplace and settings QA >> marketplace search detail file install flow works
|
||||
- Location: ../../../../../../tmp/beaver-market-settings-qa/market-settings-qa.spec.js:188:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: expect(locator).toBeVisible() failed
|
||||
|
||||
Locator: getByText(/src\/very-long-ui-token/)
|
||||
Expected: visible
|
||||
Error: strict mode violation: getByText(/src\/very-long-ui-token/) resolved to 3 elements:
|
||||
1) <span class="min-w-0 break-all font-mono text-xs">src/very-long-ui-token-0123456789abcdefghijklmnop…</span> aka getByRole('button', { name: 'src/very-long-ui-token-' })
|
||||
2) <div class="break-all font-mono text-sm font-medium">src/very-long-ui-token-0123456789abcdefghijklmnop…</div> aka getByText('src/very-long-ui-token-').nth(1)
|
||||
3) <h1>src/very-long-ui-token-0123456789abcdefghijklmnop…</h1> aka getByRole('heading', { name: 'src/very-long-ui-token-' })
|
||||
|
||||
Call log:
|
||||
- Expect "toBeVisible" with timeout 8000ms
|
||||
- waiting for getByText(/src\/very-long-ui-token/)
|
||||
|
||||
```
|
||||
|
||||
# Page snapshot
|
||||
|
||||
```yaml
|
||||
- generic [ref=e1]:
|
||||
- generic [ref=e2]:
|
||||
- banner [ref=e3]:
|
||||
- generic [ref=e5]:
|
||||
- generic [ref=e6]:
|
||||
- button "Open navigation" [ref=e7] [cursor=pointer]:
|
||||
- img [ref=e8]
|
||||
- link "Beaver" [ref=e9] [cursor=pointer]:
|
||||
- /url: /
|
||||
- generic [ref=e10]: Beaver
|
||||
- generic [ref=e12]:
|
||||
- generic [ref=e13]:
|
||||
- img [ref=e14]
|
||||
- button "ZH" [ref=e18] [cursor=pointer]
|
||||
- button "EN" [ref=e19] [cursor=pointer]
|
||||
- button "Open account menu" [ref=e20] [cursor=pointer]:
|
||||
- generic [ref=e22]: U
|
||||
- main [ref=e23]:
|
||||
- generic [ref=e24]:
|
||||
- generic [ref=e26]:
|
||||
- generic [ref=e27]:
|
||||
- img [ref=e28]
|
||||
- textbox "Search skills..." [ref=e31]: qa
|
||||
- button "Search" [ref=e32] [cursor=pointer]
|
||||
- generic [ref=e33]:
|
||||
- button "Back to search" [ref=e34] [cursor=pointer]:
|
||||
- img [ref=e35]
|
||||
- text: Back to search
|
||||
- generic [ref=e37]:
|
||||
- generic [ref=e39]:
|
||||
- generic [ref=e40]:
|
||||
- generic [ref=e41]:
|
||||
- generic [ref=e42]: "@team-very-long-ui-token-0123456789abcdefghijklmnopqrstuvwxyz-0123456789"
|
||||
- generic [ref=e43]: "Downloads: 12345"
|
||||
- generic [ref=e44]: "Stars: 7"
|
||||
- generic [ref=e45]: v1.0.0-alpha-long-version-name-0123456789
|
||||
- heading "SkillHub very-long-ui-token-0123456789abcdefghijklmnopqrstuvwxyz-0123456789" [level=2] [ref=e46]
|
||||
- paragraph [ref=e47]: 这是用于市场页和配置页响应式检测的超长文本。这是用于市场页和配置页响应式检测的超长文本。这是用于市场页和配置页响应式检测的超长文本。这是用于市场页和配置页响应式检测的超长文本。这是用于市场页和配置页响应式检测的超长文本。这是用于市场页和配置页响应式检测的超长文本。这是用于市场页和配置页响应式检测的超长文本。这是用于市场页和配置页响应式检测的超长文本。这是用于市场页和配置页响应式检测的超长文本。这是用于市场页和配置页响应式检测的超长文本。 https://example.com/very-long-ui-token-0123456789abcdefghijklmnopqrstuvwxyz-0123456789
|
||||
- button "Install" [ref=e48] [cursor=pointer]:
|
||||
- img [ref=e49]
|
||||
- text: Install
|
||||
- generic [ref=e52]:
|
||||
- tablist [ref=e53]:
|
||||
- tab "Overview" [ref=e54] [cursor=pointer]
|
||||
- tab "Files" [selected] [ref=e55] [cursor=pointer]
|
||||
- tab "Versions" [ref=e56] [cursor=pointer]
|
||||
- tabpanel "Files" [ref=e57]:
|
||||
- generic [ref=e58]:
|
||||
- generic [ref=e60]:
|
||||
- button "SKILL.md 2.0 KB" [ref=e61] [cursor=pointer]:
|
||||
- generic [ref=e62]: SKILL.md
|
||||
- generic [ref=e63]: 2.0 KB
|
||||
- button "src/very-long-ui-token-0123456789abcdefghijklmnopqrstuvwxyz-0123456789/implementation.ts 4.0 KB" [active] [ref=e64] [cursor=pointer]:
|
||||
- generic [ref=e65]: src/very-long-ui-token-0123456789abcdefghijklmnopqrstuvwxyz-0123456789/implementation.ts
|
||||
- generic [ref=e66]: 4.0 KB
|
||||
- generic [ref=e68]:
|
||||
- generic [ref=e69]:
|
||||
- generic [ref=e70]: src/very-long-ui-token-0123456789abcdefghijklmnopqrstuvwxyz-0123456789/implementation.ts
|
||||
- generic [ref=e71]: "Size: 2.0 KB"
|
||||
- generic [ref=e72]:
|
||||
- heading "src/very-long-ui-token-0123456789abcdefghijklmnopqrstuvwxyz-0123456789/implementation.ts" [level=1] [ref=e73]
|
||||
- paragraph [ref=e74]: 这是用于市场页和配置页响应式检测的超长文本。这是用于市场页和配置页响应式检测的超长文本。这是用于市场页和配置页响应式检测的超长文本。这是用于市场页和配置页响应式检测的超长文本。这是用于市场页和配置页响应式检测的超长文本。这是用于市场页和配置页响应式检测的超长文本。这是用于市场页和配置页响应式检测的超长文本。这是用于市场页和配置页响应式检测的超长文本。这是用于市场页和配置页响应式检测的超长文本。这是用于市场页和配置页响应式检测的超长文本。
|
||||
- paragraph [ref=e75]:
|
||||
- code [ref=e76]: "`very-long-ui-token-0123456789abcdefghijklmnopqrstuvwxyz-0123456789very-long-ui-token-0123456789abcdefghijklmnopqrstuvwxyz-0123456789`"
|
||||
- alert [ref=e77]
|
||||
```
|
||||
|
||||
# Test source
|
||||
|
||||
```ts
|
||||
99 | });
|
||||
100 | await page.route('**/api/marketplaces/skills/search**', async (route) => {
|
||||
101 | callLog.push({ method: route.request().method(), url: route.request().url() });
|
||||
102 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [skillItem()], total: 1, page: 0, size: 12 }) });
|
||||
103 | });
|
||||
104 | await page.route('**/api/marketplaces/skills/*/*/versions/*/file**', async (route) => {
|
||||
105 | const url = new URL(route.request().url());
|
||||
106 | const filePath = url.searchParams.get('path') || 'SKILL.md';
|
||||
107 | callLog.push({ method: route.request().method(), url: route.request().url(), filePath });
|
||||
108 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ filePath, fileSize: 2000, contentType: 'text/markdown', isBinary: false, content: `# ${filePath}\n\n${LONG_TEXT}\n\n\`${LONG}${LONG}\`` }) });
|
||||
109 | });
|
||||
110 | await page.route('**/api/marketplaces/skills/*/*/versions/*', async (route) => {
|
||||
111 | callLog.push({ method: route.request().method(), url: route.request().url() });
|
||||
112 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ detail: { version: '1.0.0-alpha-long-version-name-0123456789', status: 'published', parsedMetadataJson: JSON.stringify({ body: `# Skill\n\n${LONG_TEXT}` }) }, files: [{ filePath: 'SKILL.md', fileSize: 2048, contentType: 'text/markdown' }, { filePath: `src/${LONG}/implementation.ts`, fileSize: 4096, contentType: 'text/plain' }] }) });
|
||||
113 | });
|
||||
114 | await page.route('**/api/marketplaces/skills/*/*/versions', async (route) => {
|
||||
115 | callLog.push({ method: route.request().method(), url: route.request().url() });
|
||||
116 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [{ version: '1.0.0-alpha-long-version-name-0123456789', status: 'published', createdAt: '2026-06-04T08:00:00Z', changeReason: LONG_TEXT }], total: 1, page: 0, size: 20 }) });
|
||||
117 | });
|
||||
118 | await page.route('**/api/marketplaces/skills/*/*/install', async (route) => {
|
||||
119 | callLog.push({ method: route.request().method(), url: route.request().url(), body: route.request().postDataJSON?.() });
|
||||
120 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ ok: true, skill_name: `skill-${LONG}`, version: '1.0.0-alpha-long-version-name-0123456789', source: 'skillhub', namespace: `team-${LONG}` }) });
|
||||
121 | });
|
||||
122 | await page.route('**/api/marketplaces/skills/*/*', async (route) => {
|
||||
123 | callLog.push({ method: route.request().method(), url: route.request().url() });
|
||||
124 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(skillItem()) });
|
||||
125 | });
|
||||
126 | await page.route('**/api/status', async (route) => {
|
||||
127 | callLog.push({ method: route.request().method(), url: route.request().url() });
|
||||
128 | if (statusError) {
|
||||
129 | await route.fulfill({ status: 500, contentType: 'application/json', body: JSON.stringify({ detail: 'QA status error' }) });
|
||||
130 | return;
|
||||
131 | }
|
||||
132 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(statusPayload()) });
|
||||
133 | });
|
||||
134 | await page.route('**/api/providers/*/config', async (route) => {
|
||||
135 | callLog.push({ method: route.request().method(), url: route.request().url(), body: route.request().postDataJSON?.() });
|
||||
136 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ ok: true }) });
|
||||
137 | });
|
||||
138 | await page.route('**/api/agent/config', async (route) => {
|
||||
139 | callLog.push({ method: route.request().method(), url: route.request().url(), body: route.request().postDataJSON?.() });
|
||||
140 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ ok: true }) });
|
||||
141 | });
|
||||
142 | await page.route('**/api/channels/*/config', async (route) => {
|
||||
143 | const body = route.request().postDataJSON?.();
|
||||
144 | callLog.push({ method: route.request().method(), url: route.request().url(), body });
|
||||
145 | if (route.request().method() === 'GET') {
|
||||
146 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ channel_id: `telegram-${LONG}`, enabled: true, kind: 'telegram', mode: 'polling', account_id: `bot-${LONG}`, display_name: `Telegram ${LONG}`, config: { requireMentionInGroups: true }, secrets: { botToken: '***' } }) });
|
||||
147 | } else {
|
||||
148 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ ok: true, channel_id: `telegram-${LONG}`, restart_required: true, channel: { channel_id: `telegram-${LONG}`, enabled: true, kind: 'telegram', mode: 'polling', account_id: `bot-${LONG}`, display_name: `Telegram ${LONG}`, config: body?.config || {}, secrets: {} } }) });
|
||||
149 | }
|
||||
150 | });
|
||||
151 | await page.route('**/api/channels/*/events**', async (route) => {
|
||||
152 | callLog.push({ method: route.request().method(), url: route.request().url() });
|
||||
153 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify([{ event_id: `event-${LONG}`, channel_id: `telegram-${LONG}`, kind: 'message', status: `ok-${LONG}`, error: null, created_at: '2026-06-04T08:00:00Z' }]) });
|
||||
154 | });
|
||||
155 | await page.route('**/api/channel-connectors', async (route) => {
|
||||
156 | callLog.push({ method: route.request().method(), url: route.request().url() });
|
||||
157 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify([{ kind: 'terminal', displayName: 'Terminal' }, { kind: 'telegram', displayName: 'Telegram' }, { kind: 'feishu', displayName: 'Feishu/Lark', authType: 'plugin' }]) });
|
||||
158 | });
|
||||
159 | await page.route('**/api/channel-connections', async (route) => {
|
||||
160 | callLog.push({ method: route.request().method(), url: route.request().url() });
|
||||
161 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify([{ connection_id: `conn-${LONG}`, channel_id: `telegram-${LONG}`, kind: 'telegram', mode: 'polling', display_name: 'Telegram', account_id: `bot-${LONG}`, status: 'connected', auth_type: 'token' }]) });
|
||||
162 | });
|
||||
163 | await page.route('**/api/runtime/restart', async (route) => {
|
||||
164 | callLog.push({ method: route.request().method(), url: route.request().url() });
|
||||
165 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ ok: true }) });
|
||||
166 | });
|
||||
167 | }
|
||||
168 |
|
||||
169 | async function collectMetrics(page) {
|
||||
170 | return await page.evaluate(() => {
|
||||
171 | const viewportWidth = window.innerWidth;
|
||||
172 | const bodyWidth = document.body.scrollWidth;
|
||||
173 | const docWidth = document.documentElement.scrollWidth;
|
||||
174 | const overflowElements = Array.from(document.querySelectorAll('body *')).map((el) => {
|
||||
175 | const rect = el.getBoundingClientRect();
|
||||
176 | return { tag: el.tagName, text: (el.textContent || el.getAttribute('aria-label') || '').trim().slice(0, 100), left: rect.left, right: rect.right, width: rect.width, className: String(el.className || '') };
|
||||
177 | }).filter((item) => item.width > 1 && (item.left < -1 || item.right > viewportWidth + 1)).slice(0, 30);
|
||||
178 | const smallTargets = Array.from(document.querySelectorAll('button, a, input:not([type=hidden]), textarea, [role="button"], [role="switch"], [role="combobox"], [role="tab"]')).map((el) => {
|
||||
179 | const rect = el.getBoundingClientRect();
|
||||
180 | const style = window.getComputedStyle(el);
|
||||
181 | return { tag: el.tagName, text: (el.textContent || el.getAttribute('aria-label') || el.getAttribute('title') || '').trim().slice(0, 100), width: Math.round(rect.width), height: Math.round(rect.height), visible: style.visibility !== 'hidden' && style.display !== 'none' && Number(style.opacity) > 0.01 && rect.width > 0 && rect.height > 0 };
|
||||
182 | }).filter((item) => item.visible && (item.width < 44 || item.height < 44)).slice(0, 30);
|
||||
183 | return { viewportWidth, bodyWidth, docWidth, horizontalOverflow: Math.max(bodyWidth, docWidth) > viewportWidth + 1, overflowElements, smallTargets };
|
||||
184 | });
|
||||
185 | }
|
||||
186 |
|
||||
187 | test.describe('marketplace and settings QA', () => {
|
||||
188 | test('marketplace search detail file install flow works', async ({ page }) => {
|
||||
189 | await page.setViewportSize({ width: 390, height: 844 });
|
||||
190 | await installRoutes(page);
|
||||
191 | await page.goto(`${APP}/marketplace`);
|
||||
192 | await expect(page.getByPlaceholder(/搜索技能|Search skills/)).toBeVisible();
|
||||
193 | await page.getByPlaceholder(/搜索技能|Search skills/).fill('qa');
|
||||
194 | await page.getByRole('button', { name: /搜索|Search/ }).click();
|
||||
195 | await page.getByText(/SkillHub very-long-ui-token/).click();
|
||||
196 | await expect(page.getByRole('heading', { name: /SkillHub very-long-ui-token/ })).toBeVisible();
|
||||
197 | await page.getByRole('tab', { name: /文件|Files/ }).click();
|
||||
198 | await page.getByRole('button', { name: /src\/very-long-ui-token/ }).click();
|
||||
> 199 | await expect(page.getByText(/src\/very-long-ui-token/)).toBeVisible();
|
||||
| ^ Error: expect(locator).toBeVisible() failed
|
||||
200 | await page.getByRole('button', { name: /安装|Install/ }).click();
|
||||
201 | await expect.poll(() => callLog.some((call) => call.url.includes('/install') && call.method === 'POST')).toBe(true);
|
||||
202 | await page.screenshot({ path: path.join(SHOT_DIR, 'market-detail-390x844.png'), fullPage: true });
|
||||
203 | });
|
||||
204 |
|
||||
205 | test('settings agent provider channel and restart flows work', async ({ page }) => {
|
||||
206 | await page.setViewportSize({ width: 390, height: 844 });
|
||||
207 | await installRoutes(page);
|
||||
208 | await page.goto(`${APP}/settings`);
|
||||
209 | await expect(page.getByRole('heading', { name: /配置|Settings/ })).toBeVisible();
|
||||
210 | await page.getByLabel(/温度|Temperature/).fill('0.3');
|
||||
211 | await page.getByRole('button', { name: /保存智能体配置|Save agent config/ }).click();
|
||||
212 | await expect.poll(() => callLog.some((call) => call.url.includes('/api/agent/config') && call.method !== 'GET')).toBe(true);
|
||||
213 | await page.getByRole('button', { name: /OpenAI/ }).click();
|
||||
214 | await expect(page.getByRole('dialog')).toBeVisible();
|
||||
215 | await page.getByLabel(/请求超时|Request timeout/).fill('60');
|
||||
216 | await page.getByRole('button', { name: /保存|Save/ }).click();
|
||||
217 | await expect.poll(() => callLog.some((call) => call.url.includes('/api/providers/openai/config'))).toBe(true);
|
||||
218 | await page.getByRole('button', { name: /Telegram/ }).click();
|
||||
219 | await expect(page.getByRole('dialog')).toBeVisible();
|
||||
220 | await page.getByLabel(/显示名|Display name/).fill(`Telegram ${LONG}`);
|
||||
221 | await page.getByRole('button', { name: /保存通道配置|Save channel config/ }).click();
|
||||
222 | await expect.poll(() => callLog.some((call) => call.url.includes('/api/channels/') && call.method !== 'GET')).toBe(true);
|
||||
223 | await page.screenshot({ path: path.join(SHOT_DIR, 'settings-channel-390x844.png'), fullPage: true });
|
||||
224 | await page.keyboard.press('Escape');
|
||||
225 | await page.getByRole('button', { name: /重启实例|Restart instance/ }).first().click();
|
||||
226 | await expect(page.getByRole('dialog')).toBeVisible();
|
||||
227 | await page.getByRole('button', { name: /^重启$|^Restart$/ }).click();
|
||||
228 | await expect.poll(() => callLog.some((call) => call.url.includes('/api/runtime/restart'))).toBe(true);
|
||||
229 | });
|
||||
230 |
|
||||
231 | test('settings error state is readable', async ({ page }) => {
|
||||
232 | await page.setViewportSize({ width: 390, height: 844 });
|
||||
233 | await installRoutes(page, { statusError: true });
|
||||
234 | await page.goto(`${APP}/settings`);
|
||||
235 | await expect(page.getByText(/QA status error|无法连接|Unable to connect/)).toBeVisible();
|
||||
236 | await page.screenshot({ path: path.join(SHOT_DIR, 'settings-error-390x844.png'), fullPage: true });
|
||||
237 | const metrics = await collectMetrics(page);
|
||||
238 | expect(metrics.horizontalOverflow, JSON.stringify(metrics.overflowElements, null, 2)).toBe(false);
|
||||
239 | });
|
||||
240 |
|
||||
241 | test('responsive layouts have no page overflow or visible small targets', async ({ browser }) => {
|
||||
242 | const results = [];
|
||||
243 | for (const viewport of [
|
||||
244 | { width: 320, height: 568 },
|
||||
245 | { width: 390, height: 844 },
|
||||
246 | { width: 844, height: 390 },
|
||||
247 | { width: 768, height: 1024 },
|
||||
248 | { width: 1365, height: 900 },
|
||||
249 | ]) {
|
||||
250 | const market = await browser.newPage({ viewport });
|
||||
251 | await installRoutes(market);
|
||||
252 | await market.goto(`${APP}/marketplace`);
|
||||
253 | await market.getByText(/SkillHub very-long-ui-token/).click();
|
||||
254 | await market.screenshot({ path: path.join(SHOT_DIR, `market-${viewport.width}x${viewport.height}.png`), fullPage: true });
|
||||
255 | const marketMetrics = await collectMetrics(market);
|
||||
256 | await market.close();
|
||||
257 |
|
||||
258 | const settings = await browser.newPage({ viewport });
|
||||
259 | await installRoutes(settings);
|
||||
260 | await settings.goto(`${APP}/settings`);
|
||||
261 | await settings.getByRole('button', { name: /Telegram/ }).click();
|
||||
262 | await settings.screenshot({ path: path.join(SHOT_DIR, `settings-${viewport.width}x${viewport.height}.png`), fullPage: true });
|
||||
263 | const settingsMetrics = await collectMetrics(settings);
|
||||
264 | await settings.close();
|
||||
265 |
|
||||
266 | results.push({ viewport, marketMetrics, settingsMetrics });
|
||||
267 | expect(marketMetrics.horizontalOverflow, JSON.stringify(marketMetrics.overflowElements, null, 2)).toBe(false);
|
||||
268 | expect(settingsMetrics.horizontalOverflow, JSON.stringify(settingsMetrics.overflowElements, null, 2)).toBe(false);
|
||||
269 | expect(marketMetrics.smallTargets, JSON.stringify(marketMetrics.smallTargets, null, 2)).toEqual([]);
|
||||
270 | expect(settingsMetrics.smallTargets, JSON.stringify(settingsMetrics.smallTargets, null, 2)).toEqual([]);
|
||||
271 | }
|
||||
272 | fs.writeFileSync(REPORT, JSON.stringify({ results }, null, 2));
|
||||
273 | });
|
||||
274 | });
|
||||
275 |
|
||||
```
|
||||
@ -0,0 +1,208 @@
|
||||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: market-settings-qa.spec.js >> marketplace and settings QA >> settings error state is readable
|
||||
- Location: ../../../../../../tmp/beaver-market-settings-qa/market-settings-qa.spec.js:231:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: expect(locator).toBeVisible() failed
|
||||
|
||||
Locator: getByText(/QA status error|无法连接|Unable to connect/)
|
||||
Expected: visible
|
||||
Error: strict mode violation: getByText(/QA status error|无法连接|Unable to connect/) resolved to 2 elements:
|
||||
1) <p class="font-medium">Unable to connect to the Boardware Agent Sandbox …</p> aka getByText('Unable to connect to the')
|
||||
2) <p class="text-sm text-muted-foreground mt-1">API error 500: QA status error</p> aka getByText('API error 500: QA status error')
|
||||
|
||||
Call log:
|
||||
- Expect "toBeVisible" with timeout 8000ms
|
||||
- waiting for getByText(/QA status error|无法连接|Unable to connect/)
|
||||
|
||||
```
|
||||
|
||||
# Page snapshot
|
||||
|
||||
```yaml
|
||||
- generic [active] [ref=e1]:
|
||||
- generic [ref=e2]:
|
||||
- banner [ref=e3]:
|
||||
- generic [ref=e5]:
|
||||
- generic [ref=e6]:
|
||||
- button "Open navigation" [ref=e7] [cursor=pointer]:
|
||||
- img [ref=e8]
|
||||
- link "Beaver" [ref=e9] [cursor=pointer]:
|
||||
- /url: /
|
||||
- generic [ref=e10]: Beaver
|
||||
- generic [ref=e12]:
|
||||
- generic [ref=e13]:
|
||||
- img [ref=e14]
|
||||
- button "ZH" [ref=e18] [cursor=pointer]
|
||||
- button "EN" [ref=e19] [cursor=pointer]
|
||||
- button "Open account menu" [ref=e20] [cursor=pointer]:
|
||||
- generic [ref=e22]: U
|
||||
- main [ref=e23]:
|
||||
- generic [ref=e26]:
|
||||
- generic [ref=e27]:
|
||||
- img [ref=e28]
|
||||
- generic [ref=e30]:
|
||||
- paragraph [ref=e31]: Unable to connect to the Boardware Agent Sandbox backend
|
||||
- paragraph [ref=e32]: "API error 500: QA status error"
|
||||
- paragraph [ref=e33]: Please confirm the backend service is running and reachable from this page.
|
||||
- button "Retry" [ref=e34] [cursor=pointer]:
|
||||
- img [ref=e35]
|
||||
- text: Retry
|
||||
- alert [ref=e40]
|
||||
```
|
||||
|
||||
# Test source
|
||||
|
||||
```ts
|
||||
135 | callLog.push({ method: route.request().method(), url: route.request().url(), body: route.request().postDataJSON?.() });
|
||||
136 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ ok: true }) });
|
||||
137 | });
|
||||
138 | await page.route('**/api/agent/config', async (route) => {
|
||||
139 | callLog.push({ method: route.request().method(), url: route.request().url(), body: route.request().postDataJSON?.() });
|
||||
140 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ ok: true }) });
|
||||
141 | });
|
||||
142 | await page.route('**/api/channels/*/config', async (route) => {
|
||||
143 | const body = route.request().postDataJSON?.();
|
||||
144 | callLog.push({ method: route.request().method(), url: route.request().url(), body });
|
||||
145 | if (route.request().method() === 'GET') {
|
||||
146 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ channel_id: `telegram-${LONG}`, enabled: true, kind: 'telegram', mode: 'polling', account_id: `bot-${LONG}`, display_name: `Telegram ${LONG}`, config: { requireMentionInGroups: true }, secrets: { botToken: '***' } }) });
|
||||
147 | } else {
|
||||
148 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ ok: true, channel_id: `telegram-${LONG}`, restart_required: true, channel: { channel_id: `telegram-${LONG}`, enabled: true, kind: 'telegram', mode: 'polling', account_id: `bot-${LONG}`, display_name: `Telegram ${LONG}`, config: body?.config || {}, secrets: {} } }) });
|
||||
149 | }
|
||||
150 | });
|
||||
151 | await page.route('**/api/channels/*/events**', async (route) => {
|
||||
152 | callLog.push({ method: route.request().method(), url: route.request().url() });
|
||||
153 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify([{ event_id: `event-${LONG}`, channel_id: `telegram-${LONG}`, kind: 'message', status: `ok-${LONG}`, error: null, created_at: '2026-06-04T08:00:00Z' }]) });
|
||||
154 | });
|
||||
155 | await page.route('**/api/channel-connectors', async (route) => {
|
||||
156 | callLog.push({ method: route.request().method(), url: route.request().url() });
|
||||
157 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify([{ kind: 'terminal', displayName: 'Terminal' }, { kind: 'telegram', displayName: 'Telegram' }, { kind: 'feishu', displayName: 'Feishu/Lark', authType: 'plugin' }]) });
|
||||
158 | });
|
||||
159 | await page.route('**/api/channel-connections', async (route) => {
|
||||
160 | callLog.push({ method: route.request().method(), url: route.request().url() });
|
||||
161 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify([{ connection_id: `conn-${LONG}`, channel_id: `telegram-${LONG}`, kind: 'telegram', mode: 'polling', display_name: 'Telegram', account_id: `bot-${LONG}`, status: 'connected', auth_type: 'token' }]) });
|
||||
162 | });
|
||||
163 | await page.route('**/api/runtime/restart', async (route) => {
|
||||
164 | callLog.push({ method: route.request().method(), url: route.request().url() });
|
||||
165 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ ok: true }) });
|
||||
166 | });
|
||||
167 | }
|
||||
168 |
|
||||
169 | async function collectMetrics(page) {
|
||||
170 | return await page.evaluate(() => {
|
||||
171 | const viewportWidth = window.innerWidth;
|
||||
172 | const bodyWidth = document.body.scrollWidth;
|
||||
173 | const docWidth = document.documentElement.scrollWidth;
|
||||
174 | const overflowElements = Array.from(document.querySelectorAll('body *')).map((el) => {
|
||||
175 | const rect = el.getBoundingClientRect();
|
||||
176 | return { tag: el.tagName, text: (el.textContent || el.getAttribute('aria-label') || '').trim().slice(0, 100), left: rect.left, right: rect.right, width: rect.width, className: String(el.className || '') };
|
||||
177 | }).filter((item) => item.width > 1 && (item.left < -1 || item.right > viewportWidth + 1)).slice(0, 30);
|
||||
178 | const smallTargets = Array.from(document.querySelectorAll('button, a, input:not([type=hidden]), textarea, [role="button"], [role="switch"], [role="combobox"], [role="tab"]')).map((el) => {
|
||||
179 | const rect = el.getBoundingClientRect();
|
||||
180 | const style = window.getComputedStyle(el);
|
||||
181 | return { tag: el.tagName, text: (el.textContent || el.getAttribute('aria-label') || el.getAttribute('title') || '').trim().slice(0, 100), width: Math.round(rect.width), height: Math.round(rect.height), visible: style.visibility !== 'hidden' && style.display !== 'none' && Number(style.opacity) > 0.01 && rect.width > 0 && rect.height > 0 };
|
||||
182 | }).filter((item) => item.visible && (item.width < 44 || item.height < 44)).slice(0, 30);
|
||||
183 | return { viewportWidth, bodyWidth, docWidth, horizontalOverflow: Math.max(bodyWidth, docWidth) > viewportWidth + 1, overflowElements, smallTargets };
|
||||
184 | });
|
||||
185 | }
|
||||
186 |
|
||||
187 | test.describe('marketplace and settings QA', () => {
|
||||
188 | test('marketplace search detail file install flow works', async ({ page }) => {
|
||||
189 | await page.setViewportSize({ width: 390, height: 844 });
|
||||
190 | await installRoutes(page);
|
||||
191 | await page.goto(`${APP}/marketplace`);
|
||||
192 | await expect(page.getByPlaceholder(/搜索技能|Search skills/)).toBeVisible();
|
||||
193 | await page.getByPlaceholder(/搜索技能|Search skills/).fill('qa');
|
||||
194 | await page.getByRole('button', { name: /搜索|Search/ }).click();
|
||||
195 | await page.getByText(/SkillHub very-long-ui-token/).click();
|
||||
196 | await expect(page.getByRole('heading', { name: /SkillHub very-long-ui-token/ })).toBeVisible();
|
||||
197 | await page.getByRole('tab', { name: /文件|Files/ }).click();
|
||||
198 | await page.getByRole('button', { name: /src\/very-long-ui-token/ }).click();
|
||||
199 | await expect(page.getByText(/src\/very-long-ui-token/)).toBeVisible();
|
||||
200 | await page.getByRole('button', { name: /安装|Install/ }).click();
|
||||
201 | await expect.poll(() => callLog.some((call) => call.url.includes('/install') && call.method === 'POST')).toBe(true);
|
||||
202 | await page.screenshot({ path: path.join(SHOT_DIR, 'market-detail-390x844.png'), fullPage: true });
|
||||
203 | });
|
||||
204 |
|
||||
205 | test('settings agent provider channel and restart flows work', async ({ page }) => {
|
||||
206 | await page.setViewportSize({ width: 390, height: 844 });
|
||||
207 | await installRoutes(page);
|
||||
208 | await page.goto(`${APP}/settings`);
|
||||
209 | await expect(page.getByRole('heading', { name: /配置|Settings/ })).toBeVisible();
|
||||
210 | await page.getByLabel(/温度|Temperature/).fill('0.3');
|
||||
211 | await page.getByRole('button', { name: /保存智能体配置|Save agent config/ }).click();
|
||||
212 | await expect.poll(() => callLog.some((call) => call.url.includes('/api/agent/config') && call.method !== 'GET')).toBe(true);
|
||||
213 | await page.getByRole('button', { name: /OpenAI/ }).click();
|
||||
214 | await expect(page.getByRole('dialog')).toBeVisible();
|
||||
215 | await page.getByLabel(/请求超时|Request timeout/).fill('60');
|
||||
216 | await page.getByRole('button', { name: /保存|Save/ }).click();
|
||||
217 | await expect.poll(() => callLog.some((call) => call.url.includes('/api/providers/openai/config'))).toBe(true);
|
||||
218 | await page.getByRole('button', { name: /Telegram/ }).click();
|
||||
219 | await expect(page.getByRole('dialog')).toBeVisible();
|
||||
220 | await page.getByLabel(/显示名|Display name/).fill(`Telegram ${LONG}`);
|
||||
221 | await page.getByRole('button', { name: /保存通道配置|Save channel config/ }).click();
|
||||
222 | await expect.poll(() => callLog.some((call) => call.url.includes('/api/channels/') && call.method !== 'GET')).toBe(true);
|
||||
223 | await page.screenshot({ path: path.join(SHOT_DIR, 'settings-channel-390x844.png'), fullPage: true });
|
||||
224 | await page.keyboard.press('Escape');
|
||||
225 | await page.getByRole('button', { name: /重启实例|Restart instance/ }).first().click();
|
||||
226 | await expect(page.getByRole('dialog')).toBeVisible();
|
||||
227 | await page.getByRole('button', { name: /^重启$|^Restart$/ }).click();
|
||||
228 | await expect.poll(() => callLog.some((call) => call.url.includes('/api/runtime/restart'))).toBe(true);
|
||||
229 | });
|
||||
230 |
|
||||
231 | test('settings error state is readable', async ({ page }) => {
|
||||
232 | await page.setViewportSize({ width: 390, height: 844 });
|
||||
233 | await installRoutes(page, { statusError: true });
|
||||
234 | await page.goto(`${APP}/settings`);
|
||||
> 235 | await expect(page.getByText(/QA status error|无法连接|Unable to connect/)).toBeVisible();
|
||||
| ^ Error: expect(locator).toBeVisible() failed
|
||||
236 | await page.screenshot({ path: path.join(SHOT_DIR, 'settings-error-390x844.png'), fullPage: true });
|
||||
237 | const metrics = await collectMetrics(page);
|
||||
238 | expect(metrics.horizontalOverflow, JSON.stringify(metrics.overflowElements, null, 2)).toBe(false);
|
||||
239 | });
|
||||
240 |
|
||||
241 | test('responsive layouts have no page overflow or visible small targets', async ({ browser }) => {
|
||||
242 | const results = [];
|
||||
243 | for (const viewport of [
|
||||
244 | { width: 320, height: 568 },
|
||||
245 | { width: 390, height: 844 },
|
||||
246 | { width: 844, height: 390 },
|
||||
247 | { width: 768, height: 1024 },
|
||||
248 | { width: 1365, height: 900 },
|
||||
249 | ]) {
|
||||
250 | const market = await browser.newPage({ viewport });
|
||||
251 | await installRoutes(market);
|
||||
252 | await market.goto(`${APP}/marketplace`);
|
||||
253 | await market.getByText(/SkillHub very-long-ui-token/).click();
|
||||
254 | await market.screenshot({ path: path.join(SHOT_DIR, `market-${viewport.width}x${viewport.height}.png`), fullPage: true });
|
||||
255 | const marketMetrics = await collectMetrics(market);
|
||||
256 | await market.close();
|
||||
257 |
|
||||
258 | const settings = await browser.newPage({ viewport });
|
||||
259 | await installRoutes(settings);
|
||||
260 | await settings.goto(`${APP}/settings`);
|
||||
261 | await settings.getByRole('button', { name: /Telegram/ }).click();
|
||||
262 | await settings.screenshot({ path: path.join(SHOT_DIR, `settings-${viewport.width}x${viewport.height}.png`), fullPage: true });
|
||||
263 | const settingsMetrics = await collectMetrics(settings);
|
||||
264 | await settings.close();
|
||||
265 |
|
||||
266 | results.push({ viewport, marketMetrics, settingsMetrics });
|
||||
267 | expect(marketMetrics.horizontalOverflow, JSON.stringify(marketMetrics.overflowElements, null, 2)).toBe(false);
|
||||
268 | expect(settingsMetrics.horizontalOverflow, JSON.stringify(settingsMetrics.overflowElements, null, 2)).toBe(false);
|
||||
269 | expect(marketMetrics.smallTargets, JSON.stringify(marketMetrics.smallTargets, null, 2)).toEqual([]);
|
||||
270 | expect(settingsMetrics.smallTargets, JSON.stringify(settingsMetrics.smallTargets, null, 2)).toEqual([]);
|
||||
271 | }
|
||||
272 | fs.writeFileSync(REPORT, JSON.stringify({ results }, null, 2));
|
||||
273 | });
|
||||
274 | });
|
||||
275 |
|
||||
```
|
||||
@ -0,0 +1,316 @@
|
||||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: market-settings-qa.spec.js >> marketplace and settings QA >> settings agent provider channel and restart flows work
|
||||
- Location: ../../../../../../tmp/beaver-market-settings-qa/market-settings-qa.spec.js:205:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: expect(received).toBe(expected) // Object.is equality
|
||||
|
||||
Expected: true
|
||||
Received: false
|
||||
|
||||
Call Log:
|
||||
- Timeout 8000ms exceeded while waiting on the predicate
|
||||
```
|
||||
|
||||
# Page snapshot
|
||||
|
||||
```yaml
|
||||
- generic [active] [ref=e1]:
|
||||
- generic [ref=e2]:
|
||||
- banner [ref=e3]:
|
||||
- generic [ref=e5]:
|
||||
- generic [ref=e6]:
|
||||
- button "Open navigation" [ref=e7] [cursor=pointer]:
|
||||
- img [ref=e8]
|
||||
- link "Beaver" [ref=e9] [cursor=pointer]:
|
||||
- /url: /
|
||||
- generic [ref=e10]: Beaver
|
||||
- generic [ref=e12]:
|
||||
- generic [ref=e13]:
|
||||
- img [ref=e14]
|
||||
- button "ZH" [ref=e18] [cursor=pointer]
|
||||
- button "EN" [ref=e19] [cursor=pointer]
|
||||
- button "Open account menu" [ref=e20] [cursor=pointer]:
|
||||
- generic [ref=e22]: U
|
||||
- main [ref=e23]:
|
||||
- generic [ref=e24]:
|
||||
- generic [ref=e25]:
|
||||
- generic [ref=e26]:
|
||||
- heading "Settings" [level=1] [ref=e27]
|
||||
- paragraph [ref=e28]: Manage models, tools, integrations, and instance runtime status. Tasks and notifications stay in their own pages.
|
||||
- button "Refresh" [ref=e29] [cursor=pointer]:
|
||||
- img [ref=e30]
|
||||
- text: Refresh
|
||||
- generic [ref=e35]:
|
||||
- heading "Instance runtime" [level=3] [ref=e37]:
|
||||
- img [ref=e38]
|
||||
- text: Instance runtime
|
||||
- generic [ref=e41]:
|
||||
- generic [ref=e42]:
|
||||
- generic [ref=e43]:
|
||||
- paragraph [ref=e44]: Runtime and debugging
|
||||
- paragraph [ref=e45]: Inspect per-chat runtime logs and current instance status.
|
||||
- generic [ref=e46]:
|
||||
- link "Runtime Logs" [ref=e47] [cursor=pointer]:
|
||||
- /url: /logs
|
||||
- img [ref=e48]
|
||||
- text: Runtime Logs
|
||||
- button "Restart instance" [ref=e51] [cursor=pointer]:
|
||||
- img [ref=e52]
|
||||
- text: Restart instance
|
||||
- generic [ref=e57]:
|
||||
- generic [ref=e58]:
|
||||
- generic [ref=e59]: Config file
|
||||
- code [ref=e61]: /home/ivan/very-long-ui-token-0123456789abcdefghijklmnopqrstuvwxyz-0123456789/config/beaver.yml
|
||||
- generic [ref=e62]:
|
||||
- generic [ref=e63]: Workspace
|
||||
- code [ref=e65]: /home/ivan/very-long-ui-token-0123456789abcdefghijklmnopqrstuvwxyz-0123456789/workspace
|
||||
- generic [ref=e66]:
|
||||
- heading "Agent configuration" [level=3] [ref=e68]:
|
||||
- img [ref=e69]
|
||||
- text: Agent configuration
|
||||
- generic [ref=e72]:
|
||||
- generic [ref=e73]:
|
||||
- generic [ref=e74]: Model
|
||||
- code [ref=e76]: qwen-very-long-ui-token-0123456789abcdefghijklmnopqrstuvwxyz-0123456789
|
||||
- generic [ref=e77]:
|
||||
- generic [ref=e78]:
|
||||
- generic [ref=e79]: Max tokens
|
||||
- textbox "Max tokens" [ref=e80]:
|
||||
- /placeholder: Model default
|
||||
- text: "4096"
|
||||
- generic [ref=e81]:
|
||||
- generic [ref=e82]: Temperature
|
||||
- textbox "Temperature" [ref=e83]: "0.2"
|
||||
- generic [ref=e84]:
|
||||
- generic [ref=e85]: Max tool iterations
|
||||
- textbox "Max tool iterations" [ref=e86]: "30"
|
||||
- button "Save agent config" [ref=e88] [cursor=pointer]
|
||||
- generic [ref=e89]:
|
||||
- heading "Providers" [level=3] [ref=e91]:
|
||||
- img [ref=e92]
|
||||
- text: Providers
|
||||
- generic [ref=e97]:
|
||||
- button "OpenAI very-long-ui-token-0123456789abcdefghijklmnopqrstuvwxyz-0123456789 Current default sk-********" [ref=e98] [cursor=pointer]:
|
||||
- generic [ref=e99]:
|
||||
- generic [ref=e100]:
|
||||
- img [ref=e101]
|
||||
- generic [ref=e104]: OpenAI very-long-ui-token-0123456789abcdefghijklmnopqrstuvwxyz-0123456789
|
||||
- generic [ref=e105]: Current default
|
||||
- generic [ref=e106]: sk-********
|
||||
- img [ref=e107]
|
||||
- button "DeepSeek Click to configure" [ref=e110] [cursor=pointer]:
|
||||
- generic [ref=e111]:
|
||||
- generic [ref=e112]:
|
||||
- img [ref=e113]
|
||||
- generic [ref=e117]: DeepSeek
|
||||
- generic [ref=e118]: Click to configure
|
||||
- img [ref=e119]
|
||||
- generic [ref=e122]:
|
||||
- heading "Channels" [level=3] [ref=e124]:
|
||||
- img [ref=e125]
|
||||
- text: Channels
|
||||
- generic [ref=e133]:
|
||||
- button "Weixin QR Connect" [ref=e134] [cursor=pointer]:
|
||||
- generic [ref=e135]:
|
||||
- generic [ref=e136]:
|
||||
- img [ref=e137]
|
||||
- generic [ref=e143]: Weixin
|
||||
- generic [ref=e144]: QR
|
||||
- generic [ref=e145]: Connect
|
||||
- button "Feishu/Lark plugin Connect" [ref=e146] [cursor=pointer]:
|
||||
- generic [ref=e147]:
|
||||
- generic [ref=e148]:
|
||||
- img [ref=e149]
|
||||
- generic [ref=e155]: Feishu/Lark
|
||||
- generic [ref=e156]: plugin
|
||||
- generic [ref=e157]: Connect
|
||||
- button "Terminal Local terminal connection Connection instructions Guide" [ref=e158] [cursor=pointer]:
|
||||
- generic [ref=e159]:
|
||||
- generic [ref=e160]:
|
||||
- img [ref=e161]
|
||||
- generic [ref=e167]: Terminal
|
||||
- generic [ref=e168]: Local terminal connection
|
||||
- generic [ref=e169]: Connection instructions
|
||||
- generic [ref=e170]: Guide
|
||||
- alert [ref=e171]
|
||||
```
|
||||
|
||||
# Test source
|
||||
|
||||
```ts
|
||||
112 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ detail: { version: '1.0.0-alpha-long-version-name-0123456789', status: 'published', parsedMetadataJson: JSON.stringify({ body: `# Skill\n\n${LONG_TEXT}` }) }, files: [{ filePath: 'SKILL.md', fileSize: 2048, contentType: 'text/markdown' }, { filePath: `src/${LONG}/implementation.ts`, fileSize: 4096, contentType: 'text/plain' }] }) });
|
||||
113 | });
|
||||
114 | await page.route('**/api/marketplaces/skills/*/*/versions', async (route) => {
|
||||
115 | callLog.push({ method: route.request().method(), url: route.request().url() });
|
||||
116 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ items: [{ version: '1.0.0-alpha-long-version-name-0123456789', status: 'published', createdAt: '2026-06-04T08:00:00Z', changeReason: LONG_TEXT }], total: 1, page: 0, size: 20 }) });
|
||||
117 | });
|
||||
118 | await page.route('**/api/marketplaces/skills/*/*/install', async (route) => {
|
||||
119 | callLog.push({ method: route.request().method(), url: route.request().url(), body: route.request().postDataJSON?.() });
|
||||
120 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ ok: true, skill_name: `skill-${LONG}`, version: '1.0.0-alpha-long-version-name-0123456789', source: 'skillhub', namespace: `team-${LONG}` }) });
|
||||
121 | });
|
||||
122 | await page.route('**/api/marketplaces/skills/*/*', async (route) => {
|
||||
123 | callLog.push({ method: route.request().method(), url: route.request().url() });
|
||||
124 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(skillItem()) });
|
||||
125 | });
|
||||
126 | await page.route('**/api/status', async (route) => {
|
||||
127 | callLog.push({ method: route.request().method(), url: route.request().url() });
|
||||
128 | if (statusError) {
|
||||
129 | await route.fulfill({ status: 500, contentType: 'application/json', body: JSON.stringify({ detail: 'QA status error' }) });
|
||||
130 | return;
|
||||
131 | }
|
||||
132 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(statusPayload()) });
|
||||
133 | });
|
||||
134 | await page.route('**/api/providers/*/config', async (route) => {
|
||||
135 | callLog.push({ method: route.request().method(), url: route.request().url(), body: route.request().postDataJSON?.() });
|
||||
136 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ ok: true }) });
|
||||
137 | });
|
||||
138 | await page.route('**/api/agent/config', async (route) => {
|
||||
139 | callLog.push({ method: route.request().method(), url: route.request().url(), body: route.request().postDataJSON?.() });
|
||||
140 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ ok: true }) });
|
||||
141 | });
|
||||
142 | await page.route('**/api/channels/*/config', async (route) => {
|
||||
143 | const body = route.request().postDataJSON?.();
|
||||
144 | callLog.push({ method: route.request().method(), url: route.request().url(), body });
|
||||
145 | if (route.request().method() === 'GET') {
|
||||
146 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ channel_id: `telegram-${LONG}`, enabled: true, kind: 'telegram', mode: 'polling', account_id: `bot-${LONG}`, display_name: `Telegram ${LONG}`, config: { requireMentionInGroups: true }, secrets: { botToken: '***' } }) });
|
||||
147 | } else {
|
||||
148 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ ok: true, channel_id: `telegram-${LONG}`, restart_required: true, channel: { channel_id: `telegram-${LONG}`, enabled: true, kind: 'telegram', mode: 'polling', account_id: `bot-${LONG}`, display_name: `Telegram ${LONG}`, config: body?.config || {}, secrets: {} } }) });
|
||||
149 | }
|
||||
150 | });
|
||||
151 | await page.route('**/api/channels/*/events**', async (route) => {
|
||||
152 | callLog.push({ method: route.request().method(), url: route.request().url() });
|
||||
153 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify([{ event_id: `event-${LONG}`, channel_id: `telegram-${LONG}`, kind: 'message', status: `ok-${LONG}`, error: null, created_at: '2026-06-04T08:00:00Z' }]) });
|
||||
154 | });
|
||||
155 | await page.route('**/api/channel-connectors', async (route) => {
|
||||
156 | callLog.push({ method: route.request().method(), url: route.request().url() });
|
||||
157 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify([{ kind: 'terminal', displayName: 'Terminal' }, { kind: 'telegram', displayName: 'Telegram' }, { kind: 'feishu', displayName: 'Feishu/Lark', authType: 'plugin' }]) });
|
||||
158 | });
|
||||
159 | await page.route('**/api/channel-connections', async (route) => {
|
||||
160 | callLog.push({ method: route.request().method(), url: route.request().url() });
|
||||
161 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify([{ connection_id: `conn-${LONG}`, channel_id: `telegram-${LONG}`, kind: 'telegram', mode: 'polling', display_name: 'Telegram', account_id: `bot-${LONG}`, status: 'connected', auth_type: 'token' }]) });
|
||||
162 | });
|
||||
163 | await page.route('**/api/runtime/restart', async (route) => {
|
||||
164 | callLog.push({ method: route.request().method(), url: route.request().url() });
|
||||
165 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ ok: true }) });
|
||||
166 | });
|
||||
167 | }
|
||||
168 |
|
||||
169 | async function collectMetrics(page) {
|
||||
170 | return await page.evaluate(() => {
|
||||
171 | const viewportWidth = window.innerWidth;
|
||||
172 | const bodyWidth = document.body.scrollWidth;
|
||||
173 | const docWidth = document.documentElement.scrollWidth;
|
||||
174 | const overflowElements = Array.from(document.querySelectorAll('body *')).map((el) => {
|
||||
175 | const rect = el.getBoundingClientRect();
|
||||
176 | return { tag: el.tagName, text: (el.textContent || el.getAttribute('aria-label') || '').trim().slice(0, 100), left: rect.left, right: rect.right, width: rect.width, className: String(el.className || '') };
|
||||
177 | }).filter((item) => item.width > 1 && (item.left < -1 || item.right > viewportWidth + 1)).slice(0, 30);
|
||||
178 | const smallTargets = Array.from(document.querySelectorAll('button, a, input:not([type=hidden]), textarea, [role="button"], [role="switch"], [role="combobox"], [role="tab"]')).map((el) => {
|
||||
179 | const rect = el.getBoundingClientRect();
|
||||
180 | const style = window.getComputedStyle(el);
|
||||
181 | return { tag: el.tagName, text: (el.textContent || el.getAttribute('aria-label') || el.getAttribute('title') || '').trim().slice(0, 100), width: Math.round(rect.width), height: Math.round(rect.height), visible: style.visibility !== 'hidden' && style.display !== 'none' && Number(style.opacity) > 0.01 && rect.width > 0 && rect.height > 0 };
|
||||
182 | }).filter((item) => item.visible && (item.width < 44 || item.height < 44)).slice(0, 30);
|
||||
183 | return { viewportWidth, bodyWidth, docWidth, horizontalOverflow: Math.max(bodyWidth, docWidth) > viewportWidth + 1, overflowElements, smallTargets };
|
||||
184 | });
|
||||
185 | }
|
||||
186 |
|
||||
187 | test.describe('marketplace and settings QA', () => {
|
||||
188 | test('marketplace search detail file install flow works', async ({ page }) => {
|
||||
189 | await page.setViewportSize({ width: 390, height: 844 });
|
||||
190 | await installRoutes(page);
|
||||
191 | await page.goto(`${APP}/marketplace`);
|
||||
192 | await expect(page.getByPlaceholder(/搜索技能|Search skills/)).toBeVisible();
|
||||
193 | await page.getByPlaceholder(/搜索技能|Search skills/).fill('qa');
|
||||
194 | await page.getByRole('button', { name: /搜索|Search/ }).click();
|
||||
195 | await page.getByText(/SkillHub very-long-ui-token/).click();
|
||||
196 | await expect(page.getByRole('heading', { name: /SkillHub very-long-ui-token/ })).toBeVisible();
|
||||
197 | await page.getByRole('tab', { name: /文件|Files/ }).click();
|
||||
198 | await page.getByRole('button', { name: /src\/very-long-ui-token/ }).click();
|
||||
199 | await expect(page.getByText(/src\/very-long-ui-token/)).toBeVisible();
|
||||
200 | await page.getByRole('button', { name: /安装|Install/ }).click();
|
||||
201 | await expect.poll(() => callLog.some((call) => call.url.includes('/install') && call.method === 'POST')).toBe(true);
|
||||
202 | await page.screenshot({ path: path.join(SHOT_DIR, 'market-detail-390x844.png'), fullPage: true });
|
||||
203 | });
|
||||
204 |
|
||||
205 | test('settings agent provider channel and restart flows work', async ({ page }) => {
|
||||
206 | await page.setViewportSize({ width: 390, height: 844 });
|
||||
207 | await installRoutes(page);
|
||||
208 | await page.goto(`${APP}/settings`);
|
||||
209 | await expect(page.getByRole('heading', { name: /配置|Settings/ })).toBeVisible();
|
||||
210 | await page.getByLabel(/温度|Temperature/).fill('0.3');
|
||||
211 | await page.getByRole('button', { name: /保存智能体配置|Save agent config/ }).click();
|
||||
> 212 | await expect.poll(() => callLog.some((call) => call.url.includes('/api/agent/config') && call.method !== 'GET')).toBe(true);
|
||||
| ^ Error: expect(received).toBe(expected) // Object.is equality
|
||||
213 | await page.getByRole('button', { name: /OpenAI/ }).click();
|
||||
214 | await expect(page.getByRole('dialog')).toBeVisible();
|
||||
215 | await page.getByLabel(/请求超时|Request timeout/).fill('60');
|
||||
216 | await page.getByRole('button', { name: /保存|Save/ }).click();
|
||||
217 | await expect.poll(() => callLog.some((call) => call.url.includes('/api/providers/openai/config'))).toBe(true);
|
||||
218 | await page.getByRole('button', { name: /Telegram/ }).click();
|
||||
219 | await expect(page.getByRole('dialog')).toBeVisible();
|
||||
220 | await page.getByLabel(/显示名|Display name/).fill(`Telegram ${LONG}`);
|
||||
221 | await page.getByRole('button', { name: /保存通道配置|Save channel config/ }).click();
|
||||
222 | await expect.poll(() => callLog.some((call) => call.url.includes('/api/channels/') && call.method !== 'GET')).toBe(true);
|
||||
223 | await page.screenshot({ path: path.join(SHOT_DIR, 'settings-channel-390x844.png'), fullPage: true });
|
||||
224 | await page.keyboard.press('Escape');
|
||||
225 | await page.getByRole('button', { name: /重启实例|Restart instance/ }).first().click();
|
||||
226 | await expect(page.getByRole('dialog')).toBeVisible();
|
||||
227 | await page.getByRole('button', { name: /^重启$|^Restart$/ }).click();
|
||||
228 | await expect.poll(() => callLog.some((call) => call.url.includes('/api/runtime/restart'))).toBe(true);
|
||||
229 | });
|
||||
230 |
|
||||
231 | test('settings error state is readable', async ({ page }) => {
|
||||
232 | await page.setViewportSize({ width: 390, height: 844 });
|
||||
233 | await installRoutes(page, { statusError: true });
|
||||
234 | await page.goto(`${APP}/settings`);
|
||||
235 | await expect(page.getByText(/QA status error|无法连接|Unable to connect/)).toBeVisible();
|
||||
236 | await page.screenshot({ path: path.join(SHOT_DIR, 'settings-error-390x844.png'), fullPage: true });
|
||||
237 | const metrics = await collectMetrics(page);
|
||||
238 | expect(metrics.horizontalOverflow, JSON.stringify(metrics.overflowElements, null, 2)).toBe(false);
|
||||
239 | });
|
||||
240 |
|
||||
241 | test('responsive layouts have no page overflow or visible small targets', async ({ browser }) => {
|
||||
242 | const results = [];
|
||||
243 | for (const viewport of [
|
||||
244 | { width: 320, height: 568 },
|
||||
245 | { width: 390, height: 844 },
|
||||
246 | { width: 844, height: 390 },
|
||||
247 | { width: 768, height: 1024 },
|
||||
248 | { width: 1365, height: 900 },
|
||||
249 | ]) {
|
||||
250 | const market = await browser.newPage({ viewport });
|
||||
251 | await installRoutes(market);
|
||||
252 | await market.goto(`${APP}/marketplace`);
|
||||
253 | await market.getByText(/SkillHub very-long-ui-token/).click();
|
||||
254 | await market.screenshot({ path: path.join(SHOT_DIR, `market-${viewport.width}x${viewport.height}.png`), fullPage: true });
|
||||
255 | const marketMetrics = await collectMetrics(market);
|
||||
256 | await market.close();
|
||||
257 |
|
||||
258 | const settings = await browser.newPage({ viewport });
|
||||
259 | await installRoutes(settings);
|
||||
260 | await settings.goto(`${APP}/settings`);
|
||||
261 | await settings.getByRole('button', { name: /Telegram/ }).click();
|
||||
262 | await settings.screenshot({ path: path.join(SHOT_DIR, `settings-${viewport.width}x${viewport.height}.png`), fullPage: true });
|
||||
263 | const settingsMetrics = await collectMetrics(settings);
|
||||
264 | await settings.close();
|
||||
265 |
|
||||
266 | results.push({ viewport, marketMetrics, settingsMetrics });
|
||||
267 | expect(marketMetrics.horizontalOverflow, JSON.stringify(marketMetrics.overflowElements, null, 2)).toBe(false);
|
||||
268 | expect(settingsMetrics.horizontalOverflow, JSON.stringify(settingsMetrics.overflowElements, null, 2)).toBe(false);
|
||||
269 | expect(marketMetrics.smallTargets, JSON.stringify(marketMetrics.smallTargets, null, 2)).toEqual([]);
|
||||
270 | expect(settingsMetrics.smallTargets, JSON.stringify(settingsMetrics.smallTargets, null, 2)).toEqual([]);
|
||||
271 | }
|
||||
272 | fs.writeFileSync(REPORT, JSON.stringify({ results }, null, 2));
|
||||
273 | });
|
||||
274 | });
|
||||
275 |
|
||||
```
|
||||
@ -0,0 +1,260 @@
|
||||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: market-settings-qa.spec.js >> marketplace and settings QA >> responsive layouts have no page overflow or visible small targets
|
||||
- Location: ../../../../../../tmp/beaver-market-settings-qa/market-settings-qa.spec.js:241:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
TimeoutError: locator.click: Timeout 8000ms exceeded.
|
||||
Call log:
|
||||
- waiting for getByRole('button', { name: /Telegram/ })
|
||||
|
||||
```
|
||||
|
||||
# Page snapshot
|
||||
|
||||
```yaml
|
||||
- generic [active] [ref=e1]:
|
||||
- generic [ref=e2]:
|
||||
- banner [ref=e3]:
|
||||
- generic [ref=e5]:
|
||||
- button "Open navigation" [ref=e7] [cursor=pointer]:
|
||||
- img [ref=e8]
|
||||
- generic [ref=e10]:
|
||||
- generic [ref=e11]:
|
||||
- img [ref=e12]
|
||||
- button "ZH" [ref=e16] [cursor=pointer]
|
||||
- button "EN" [ref=e17] [cursor=pointer]
|
||||
- button "Open account menu" [ref=e18] [cursor=pointer]:
|
||||
- generic [ref=e20]: U
|
||||
- main [ref=e21]:
|
||||
- generic [ref=e22]:
|
||||
- generic [ref=e23]:
|
||||
- generic [ref=e24]:
|
||||
- heading "Settings" [level=1] [ref=e25]
|
||||
- paragraph [ref=e26]: Manage models, tools, integrations, and instance runtime status. Tasks and notifications stay in their own pages.
|
||||
- button "Refresh" [ref=e27] [cursor=pointer]:
|
||||
- img [ref=e28]
|
||||
- text: Refresh
|
||||
- generic [ref=e33]:
|
||||
- heading "Instance runtime" [level=3] [ref=e35]:
|
||||
- img [ref=e36]
|
||||
- text: Instance runtime
|
||||
- generic [ref=e39]:
|
||||
- generic [ref=e40]:
|
||||
- generic [ref=e41]:
|
||||
- paragraph [ref=e42]: Runtime and debugging
|
||||
- paragraph [ref=e43]: Inspect per-chat runtime logs and current instance status.
|
||||
- generic [ref=e44]:
|
||||
- link "Runtime Logs" [ref=e45] [cursor=pointer]:
|
||||
- /url: /logs
|
||||
- img [ref=e46]
|
||||
- text: Runtime Logs
|
||||
- button "Restart instance" [ref=e49] [cursor=pointer]:
|
||||
- img [ref=e50]
|
||||
- text: Restart instance
|
||||
- generic [ref=e55]:
|
||||
- generic [ref=e56]:
|
||||
- generic [ref=e57]: Config file
|
||||
- code [ref=e59]: /home/ivan/very-long-ui-token-0123456789abcdefghijklmnopqrstuvwxyz-0123456789/config/beaver.yml
|
||||
- generic [ref=e60]:
|
||||
- generic [ref=e61]: Workspace
|
||||
- code [ref=e63]: /home/ivan/very-long-ui-token-0123456789abcdefghijklmnopqrstuvwxyz-0123456789/workspace
|
||||
- generic [ref=e64]:
|
||||
- heading "Agent configuration" [level=3] [ref=e66]:
|
||||
- img [ref=e67]
|
||||
- text: Agent configuration
|
||||
- generic [ref=e70]:
|
||||
- generic [ref=e71]:
|
||||
- generic [ref=e72]: Model
|
||||
- code [ref=e74]: qwen-very-long-ui-token-0123456789abcdefghijklmnopqrstuvwxyz-0123456789
|
||||
- generic [ref=e75]:
|
||||
- generic [ref=e76]:
|
||||
- generic [ref=e77]: Max tokens
|
||||
- textbox "Max tokens" [ref=e78]:
|
||||
- /placeholder: Model default
|
||||
- text: "4096"
|
||||
- generic [ref=e79]:
|
||||
- generic [ref=e80]: Temperature
|
||||
- textbox "Temperature" [ref=e81]: "0.2"
|
||||
- generic [ref=e82]:
|
||||
- generic [ref=e83]: Max tool iterations
|
||||
- textbox "Max tool iterations" [ref=e84]: "30"
|
||||
- button "Save agent config" [ref=e86] [cursor=pointer]
|
||||
- generic [ref=e87]:
|
||||
- heading "Providers" [level=3] [ref=e89]:
|
||||
- img [ref=e90]
|
||||
- text: Providers
|
||||
- generic [ref=e95]:
|
||||
- button "OpenAI very-long-ui-token-0123456789abcdefghijklmnopqrstuvwxyz-0123456789 Current default sk-********" [ref=e96] [cursor=pointer]:
|
||||
- generic [ref=e97]:
|
||||
- generic [ref=e98]:
|
||||
- img [ref=e99]
|
||||
- generic [ref=e102]: OpenAI very-long-ui-token-0123456789abcdefghijklmnopqrstuvwxyz-0123456789
|
||||
- generic [ref=e103]: Current default
|
||||
- generic [ref=e104]: sk-********
|
||||
- img [ref=e105]
|
||||
- button "DeepSeek Click to configure" [ref=e108] [cursor=pointer]:
|
||||
- generic [ref=e109]:
|
||||
- generic [ref=e110]:
|
||||
- img [ref=e111]
|
||||
- generic [ref=e115]: DeepSeek
|
||||
- generic [ref=e116]: Click to configure
|
||||
- img [ref=e117]
|
||||
- generic [ref=e120]:
|
||||
- heading "Channels" [level=3] [ref=e122]:
|
||||
- img [ref=e123]
|
||||
- text: Channels
|
||||
- generic [ref=e131]:
|
||||
- button "Weixin QR Connect" [ref=e132] [cursor=pointer]:
|
||||
- generic [ref=e133]:
|
||||
- generic [ref=e134]:
|
||||
- img [ref=e135]
|
||||
- generic [ref=e141]: Weixin
|
||||
- generic [ref=e142]: QR
|
||||
- generic [ref=e143]: Connect
|
||||
- button "Feishu/Lark plugin Connect" [ref=e144] [cursor=pointer]:
|
||||
- generic [ref=e145]:
|
||||
- generic [ref=e146]:
|
||||
- img [ref=e147]
|
||||
- generic [ref=e153]: Feishu/Lark
|
||||
- generic [ref=e154]: plugin
|
||||
- generic [ref=e155]: Connect
|
||||
- button "Terminal Local terminal connection Connection instructions Guide" [ref=e156] [cursor=pointer]:
|
||||
- generic [ref=e157]:
|
||||
- generic [ref=e158]:
|
||||
- img [ref=e159]
|
||||
- generic [ref=e165]: Terminal
|
||||
- generic [ref=e166]: Local terminal connection
|
||||
- generic [ref=e167]: Connection instructions
|
||||
- generic [ref=e168]: Guide
|
||||
- alert [ref=e169]
|
||||
```
|
||||
|
||||
# Test source
|
||||
|
||||
```ts
|
||||
161 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify([{ connection_id: `conn-${LONG}`, channel_id: `telegram-${LONG}`, kind: 'telegram', mode: 'polling', display_name: 'Telegram', account_id: `bot-${LONG}`, status: 'connected', auth_type: 'token' }]) });
|
||||
162 | });
|
||||
163 | await page.route('**/api/runtime/restart', async (route) => {
|
||||
164 | callLog.push({ method: route.request().method(), url: route.request().url() });
|
||||
165 | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ ok: true }) });
|
||||
166 | });
|
||||
167 | }
|
||||
168 |
|
||||
169 | async function collectMetrics(page) {
|
||||
170 | return await page.evaluate(() => {
|
||||
171 | const viewportWidth = window.innerWidth;
|
||||
172 | const bodyWidth = document.body.scrollWidth;
|
||||
173 | const docWidth = document.documentElement.scrollWidth;
|
||||
174 | const overflowElements = Array.from(document.querySelectorAll('body *')).map((el) => {
|
||||
175 | const rect = el.getBoundingClientRect();
|
||||
176 | return { tag: el.tagName, text: (el.textContent || el.getAttribute('aria-label') || '').trim().slice(0, 100), left: rect.left, right: rect.right, width: rect.width, className: String(el.className || '') };
|
||||
177 | }).filter((item) => item.width > 1 && (item.left < -1 || item.right > viewportWidth + 1)).slice(0, 30);
|
||||
178 | const smallTargets = Array.from(document.querySelectorAll('button, a, input:not([type=hidden]), textarea, [role="button"], [role="switch"], [role="combobox"], [role="tab"]')).map((el) => {
|
||||
179 | const rect = el.getBoundingClientRect();
|
||||
180 | const style = window.getComputedStyle(el);
|
||||
181 | return { tag: el.tagName, text: (el.textContent || el.getAttribute('aria-label') || el.getAttribute('title') || '').trim().slice(0, 100), width: Math.round(rect.width), height: Math.round(rect.height), visible: style.visibility !== 'hidden' && style.display !== 'none' && Number(style.opacity) > 0.01 && rect.width > 0 && rect.height > 0 };
|
||||
182 | }).filter((item) => item.visible && (item.width < 44 || item.height < 44)).slice(0, 30);
|
||||
183 | return { viewportWidth, bodyWidth, docWidth, horizontalOverflow: Math.max(bodyWidth, docWidth) > viewportWidth + 1, overflowElements, smallTargets };
|
||||
184 | });
|
||||
185 | }
|
||||
186 |
|
||||
187 | test.describe('marketplace and settings QA', () => {
|
||||
188 | test('marketplace search detail file install flow works', async ({ page }) => {
|
||||
189 | await page.setViewportSize({ width: 390, height: 844 });
|
||||
190 | await installRoutes(page);
|
||||
191 | await page.goto(`${APP}/marketplace`);
|
||||
192 | await expect(page.getByPlaceholder(/搜索技能|Search skills/)).toBeVisible();
|
||||
193 | await page.getByPlaceholder(/搜索技能|Search skills/).fill('qa');
|
||||
194 | await page.getByRole('button', { name: /搜索|Search/ }).click();
|
||||
195 | await page.getByText(/SkillHub very-long-ui-token/).click();
|
||||
196 | await expect(page.getByRole('heading', { name: /SkillHub very-long-ui-token/ })).toBeVisible();
|
||||
197 | await page.getByRole('tab', { name: /文件|Files/ }).click();
|
||||
198 | await page.getByRole('button', { name: /src\/very-long-ui-token/ }).click();
|
||||
199 | await expect(page.getByText(/src\/very-long-ui-token/)).toBeVisible();
|
||||
200 | await page.getByRole('button', { name: /安装|Install/ }).click();
|
||||
201 | await expect.poll(() => callLog.some((call) => call.url.includes('/install') && call.method === 'POST')).toBe(true);
|
||||
202 | await page.screenshot({ path: path.join(SHOT_DIR, 'market-detail-390x844.png'), fullPage: true });
|
||||
203 | });
|
||||
204 |
|
||||
205 | test('settings agent provider channel and restart flows work', async ({ page }) => {
|
||||
206 | await page.setViewportSize({ width: 390, height: 844 });
|
||||
207 | await installRoutes(page);
|
||||
208 | await page.goto(`${APP}/settings`);
|
||||
209 | await expect(page.getByRole('heading', { name: /配置|Settings/ })).toBeVisible();
|
||||
210 | await page.getByLabel(/温度|Temperature/).fill('0.3');
|
||||
211 | await page.getByRole('button', { name: /保存智能体配置|Save agent config/ }).click();
|
||||
212 | await expect.poll(() => callLog.some((call) => call.url.includes('/api/agent/config') && call.method !== 'GET')).toBe(true);
|
||||
213 | await page.getByRole('button', { name: /OpenAI/ }).click();
|
||||
214 | await expect(page.getByRole('dialog')).toBeVisible();
|
||||
215 | await page.getByLabel(/请求超时|Request timeout/).fill('60');
|
||||
216 | await page.getByRole('button', { name: /保存|Save/ }).click();
|
||||
217 | await expect.poll(() => callLog.some((call) => call.url.includes('/api/providers/openai/config'))).toBe(true);
|
||||
218 | await page.getByRole('button', { name: /Telegram/ }).click();
|
||||
219 | await expect(page.getByRole('dialog')).toBeVisible();
|
||||
220 | await page.getByLabel(/显示名|Display name/).fill(`Telegram ${LONG}`);
|
||||
221 | await page.getByRole('button', { name: /保存通道配置|Save channel config/ }).click();
|
||||
222 | await expect.poll(() => callLog.some((call) => call.url.includes('/api/channels/') && call.method !== 'GET')).toBe(true);
|
||||
223 | await page.screenshot({ path: path.join(SHOT_DIR, 'settings-channel-390x844.png'), fullPage: true });
|
||||
224 | await page.keyboard.press('Escape');
|
||||
225 | await page.getByRole('button', { name: /重启实例|Restart instance/ }).first().click();
|
||||
226 | await expect(page.getByRole('dialog')).toBeVisible();
|
||||
227 | await page.getByRole('button', { name: /^重启$|^Restart$/ }).click();
|
||||
228 | await expect.poll(() => callLog.some((call) => call.url.includes('/api/runtime/restart'))).toBe(true);
|
||||
229 | });
|
||||
230 |
|
||||
231 | test('settings error state is readable', async ({ page }) => {
|
||||
232 | await page.setViewportSize({ width: 390, height: 844 });
|
||||
233 | await installRoutes(page, { statusError: true });
|
||||
234 | await page.goto(`${APP}/settings`);
|
||||
235 | await expect(page.getByText(/QA status error|无法连接|Unable to connect/)).toBeVisible();
|
||||
236 | await page.screenshot({ path: path.join(SHOT_DIR, 'settings-error-390x844.png'), fullPage: true });
|
||||
237 | const metrics = await collectMetrics(page);
|
||||
238 | expect(metrics.horizontalOverflow, JSON.stringify(metrics.overflowElements, null, 2)).toBe(false);
|
||||
239 | });
|
||||
240 |
|
||||
241 | test('responsive layouts have no page overflow or visible small targets', async ({ browser }) => {
|
||||
242 | const results = [];
|
||||
243 | for (const viewport of [
|
||||
244 | { width: 320, height: 568 },
|
||||
245 | { width: 390, height: 844 },
|
||||
246 | { width: 844, height: 390 },
|
||||
247 | { width: 768, height: 1024 },
|
||||
248 | { width: 1365, height: 900 },
|
||||
249 | ]) {
|
||||
250 | const market = await browser.newPage({ viewport });
|
||||
251 | await installRoutes(market);
|
||||
252 | await market.goto(`${APP}/marketplace`);
|
||||
253 | await market.getByText(/SkillHub very-long-ui-token/).click();
|
||||
254 | await market.screenshot({ path: path.join(SHOT_DIR, `market-${viewport.width}x${viewport.height}.png`), fullPage: true });
|
||||
255 | const marketMetrics = await collectMetrics(market);
|
||||
256 | await market.close();
|
||||
257 |
|
||||
258 | const settings = await browser.newPage({ viewport });
|
||||
259 | await installRoutes(settings);
|
||||
260 | await settings.goto(`${APP}/settings`);
|
||||
> 261 | await settings.getByRole('button', { name: /Telegram/ }).click();
|
||||
| ^ TimeoutError: locator.click: Timeout 8000ms exceeded.
|
||||
262 | await settings.screenshot({ path: path.join(SHOT_DIR, `settings-${viewport.width}x${viewport.height}.png`), fullPage: true });
|
||||
263 | const settingsMetrics = await collectMetrics(settings);
|
||||
264 | await settings.close();
|
||||
265 |
|
||||
266 | results.push({ viewport, marketMetrics, settingsMetrics });
|
||||
267 | expect(marketMetrics.horizontalOverflow, JSON.stringify(marketMetrics.overflowElements, null, 2)).toBe(false);
|
||||
268 | expect(settingsMetrics.horizontalOverflow, JSON.stringify(settingsMetrics.overflowElements, null, 2)).toBe(false);
|
||||
269 | expect(marketMetrics.smallTargets, JSON.stringify(marketMetrics.smallTargets, null, 2)).toEqual([]);
|
||||
270 | expect(settingsMetrics.smallTargets, JSON.stringify(settingsMetrics.smallTargets, null, 2)).toEqual([]);
|
||||
271 | }
|
||||
272 | fs.writeFileSync(REPORT, JSON.stringify({ results }, null, 2));
|
||||
273 | });
|
||||
274 | });
|
||||
275 |
|
||||
```
|
||||
Reference in New Issue
Block a user