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