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:
2026-06-05 11:46:40 +08:00
parent 236ac19789
commit 2c5205b06e
120 changed files with 8321 additions and 1865 deletions

View File

@ -0,0 +1,9 @@
{
"status": "failed",
"failedTests": [
"f5227d990c583bf1cb5e-1c04b5ceddc2ff622275",
"f5227d990c583bf1cb5e-960e0b45f6e36c90c9ae",
"f5227d990c583bf1cb5e-205bf022968074780a04",
"f5227d990c583bf1cb5e-ebfd66136ede563d8670"
]
}

View File

@ -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 |
```

View File

@ -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 |
```

View File

@ -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 |
```

View File

@ -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 |
```