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