{
  "config": {
    "argv": [
      "/opt/hostedtoolcache/node/20.20.2/x64/bin/node",
      "/home/runner/work/dr-card-web/dr-card-web/node_modules/.bin/playwright",
      "test",
      "tests/e2e/test-gate.spec.ts"
    ],
    "configFile": "/home/runner/work/dr-card-web/dr-card-web/playwright.config.ts",
    "rootDir": "/home/runner/work/dr-card-web/dr-card-web/tests/e2e",
    "failOnFlakyTests": false,
    "forbidOnly": true,
    "fullyParallel": true,
    "globalSetup": null,
    "globalTeardown": null,
    "globalTimeout": 0,
    "grep": {},
    "grepInvert": null,
    "maxFailures": 0,
    "metadata": {
      "ci": {
        "commitHref": "https://github.com/sanchuang-dev/dr-card-web/commit/e55462b3b5b7d556ec7a32336e1373b1601f2744",
        "commitHash": "e55462b3b5b7d556ec7a32336e1373b1601f2744",
        "prHref": "https://github.com/sanchuang-dev/dr-card-web/pull/306",
        "prTitle": "release(test): promote homepage Mall search and prod debug-arrow fix (#304)",
        "prBaseHash": "e5895f0443c9f67acd51d00fc5a9a5b8fd93d50b",
        "buildHref": "https://github.com/sanchuang-dev/dr-card-web/actions/runs/42744119730"
      },
      "gitCommit": {
        "shortHash": "e55462b",
        "hash": "e55462b3b5b7d556ec7a32336e1373b1601f2744",
        "subject": "Merge ceadf7ef23926dd70b9064ff78ebf63b064adbe8 into e5895f0443c9f67acd51d00fc5a9a5b8fd93d50b",
        "body": "Merge ceadf7ef23926dd70b9064ff78ebf63b064adbe8 into e5895f0443c9f67acd51d00fc5a9a5b8fd93d50b\n",
        "author": {
          "name": "SINGING-AKIBA",
          "email": "980325684@qq.com",
          "time": 1794278288000
        },
        "committer": {
          "name": "GitHub",
          "email": "noreply@github.com",
          "time": 1794278288000
        },
        "branch": "HEAD"
      },
      "gitDiff": "diff --git a/src/app/router/mallSearch.ts b/src/app/router/mallSearch.ts\nnew file mode 100644\nindex 0000000..c243121\n--- /dev/null\n+++ b/src/app/router/mallSearch.ts\n@@ -0,0 +1,3 @@\n+/** Single approved first-party shop search destination for the Home → Mall tab flow. */\n+export const MALL_SEARCH_PATH = '/pages/goods/goods_search/index'\n+export const MALL_SEARCH_TAB_ROUTE = `/mall?path=${encodeURIComponent(MALL_SEARCH_PATH)}`\ndiff --git a/src/pages/Home.signRecords.test.tsx b/src/pages/Home.signRecords.test.tsx\nindex 10e88aa..6ee74ae 100644\n--- a/src/pages/Home.signRecords.test.tsx\n+++ b/src/pages/Home.signRecords.test.tsx\n@@ -2,7 +2,12 @@ import { afterEach, describe, expect, it, vi } from 'vitest'\n import { cleanup, fireEvent, render, screen } from '@testing-library/react'\n import type { ReactNode } from 'react'\n \n-const mocks = vi.hoisted(() => ({ useSignRecords: vi.fn(), navigate: vi.fn(), reloadSignStatus: vi.fn() }))\n+const mocks = vi.hoisted(() => ({\n+  useSignRecords: vi.fn(),\n+  navigate: vi.fn(),\n+  reloadSignStatus: vi.fn(),\n+  bridgeLabEnabled: true,\n+}))\n \n vi.mock('react-router-dom', async (importOriginal) => {\n   const original = await importOriginal<typeof import('react-router-dom')>()\n@@ -12,6 +17,16 @@ vi.mock('react-router-dom', async (importOriginal) => {\n     useSearchParams: () => [new URLSearchParams(), vi.fn()],\n   }\n })\n+vi.mock('../app/config/runtime', async (importOriginal) => {\n+  const original = await importOriginal<typeof import('../app/config/runtime')>()\n+  return {\n+    ...original,\n+    runtimePolicy: {\n+      ...original.runtimePolicy,\n+      get bridgeLabEnabled() { return mocks.bridgeLabEnabled },\n+    },\n+  }\n+})\n vi.mock('./checkin/useCheckinFeed', () => ({ useSignRecords: mocks.useSignRecords }))\n vi.mock('./home/useHomeFeed', () => ({\n   useHomeBanners: () => ({ state: 'success', data: [] }),\n@@ -33,12 +48,14 @@ vi.mock('../components/mobile/CheckinMakeupSuccessOverlay', () => ({ default: ()\n vi.mock('../components/mobile/DebugPanel', () => ({ default: () => null }))\n \n import Home from './Home'\n+import { MALL_SEARCH_TAB_ROUTE } from '../app/router/mallSearch'\n \n afterEach(() => {\n   cleanup()\n   mocks.useSignRecords.mockReset()\n   mocks.navigate.mockReset()\n   mocks.reloadSignStatus.mockReset()\n+  mocks.bridgeLabEnabled = true\n   vi.useRealTimers()\n })\n \n@@ -68,3 +85,30 @@ describe('Issue #220: cross-day refresh', () => {\n     expect(mocks.reloadSignStatus).toHaveBeenCalledTimes(1)\n   })\n })\n+\n+describe('Issue #304: home search and production debug entry', () => {\n+  it('switches the Mall tab when the entire search box is tapped', () => {\n+    mocks.useSignRecords.mockReturnValue({ remote: { state: 'loading' }, reload: vi.fn() })\n+    render(<Home />)\n+    const search = screen.getByRole('button', { name: '搜索商品' })\n+    expect(search.textContent).toContain('搜索你想要的商品')\n+    expect(search.querySelector('svg')).toBeTruthy()\n+    fireEvent.click(search)\n+    expect(mocks.navigate).toHaveBeenCalledWith(MALL_SEARCH_TAB_ROUTE)\n+  })\n+\n+  it('hides the temporary left-side Bridge Lab entry in prod without hiding search', () => {\n+    mocks.bridgeLabEnabled = false\n+    mocks.useSignRecords.mockReturnValue({ remote: { state: 'loading' }, reload: vi.fn() })\n+    render(<Home />)\n+    expect(screen.queryByRole('button', { name: '打开 Bridge Lab（临时调试入口）' })).toBeNull()\n+    expect(screen.getByRole('button', { name: '搜索商品' }).querySelector('svg')).toBeTruthy()\n+  })\n+\n+  it('preserves the debug shortcut in nonproduction builds', () => {\n+    mocks.useSignRecords.mockReturnValue({ remote: { state: 'loading' }, reload: vi.fn() })\n+    render(<Home />)\n+    fireEvent.click(screen.getByRole('button', { name: '打开 Bridge Lab（临时调试入口）' }))\n+    expect(mocks.navigate).toHaveBeenCalledWith('/__debug/bridge-lab?osType=Android')\n+  })\n+})\ndiff --git a/src/pages/Home.tsx b/src/pages/Home.tsx\nindex 7998359..a9e434f 100644\n--- a/src/pages/Home.tsx\n+++ b/src/pages/Home.tsx\n@@ -15,6 +15,8 @@ import { useSignRecords } from './checkin/useCheckinFeed'\n import { SIGN_RECORDS_RANGE_WEEK } from '../services/signrecords'\n import UserAvatar from '../components/mobile/UserAvatar'\n import { useUserIdentity } from './profile/useUserIdentity'\n+import { runtimePolicy } from '../app/config/runtime'\n+import { MALL_SEARCH_TAB_ROUTE } from '../app/router/mallSearch'\n \n const sectionIcons = {\n   cause: Heart,\n@@ -85,26 +87,26 @@ export default function Home() {\n   return (\n     <PageContainer className=\"pb-24 pt-4\" inset={false}>\n       <section className=\"mx-4 flex items-center gap-2\" aria-label=\"搜索与用户入口\">\n-        {/* 临时调试入口（需移除）：原「返回卡博士首页」按钮临时改跳 Bridge Lab，\n-         * 便于在 App WebView 内直接进入 /__debug/bridge-lab?osType=Android。\n-         * 调试结束后恢复 onClick={() => navigate('/legacy-home')} 与原 aria-label。 */}\n+        {/* 调试入口仅出现在支持 Bridge Lab 的构建，正式环境隐藏。 */}\n+        {runtimePolicy.bridgeLabEnabled && (\n+          <button\n+            type=\"button\"\n+            aria-label=\"打开 Bridge Lab（临时调试入口）\"\n+            onClick={() => navigate('/__debug/bridge-lab?osType=Android')}\n+            className=\"flex h-10 w-10 flex-none items-center justify-center rounded-full border border-border-subtle bg-surface text-text-primary active:bg-surface-secondary\"\n+          >\n+            <ChevronLeft className=\"h-5 w-5\" />\n+          </button>\n+        )}\n         <button\n           type=\"button\"\n-          aria-label=\"打开 Bridge Lab（临时调试入口）\"\n-          onClick={() => navigate('/__debug/bridge-lab?osType=Android')}\n-          className=\"flex h-10 w-10 flex-none items-center justify-center rounded-full border border-border-subtle bg-surface text-text-primary active:bg-surface-secondary\"\n+          aria-label=\"搜索商品\"\n+          onClick={() => navigate(MALL_SEARCH_TAB_ROUTE)}\n+          className=\"flex h-10 flex-1 items-center gap-2 rounded-full border border-border-subtle bg-surface px-3 text-left text-text-tertiary active:bg-surface-secondary\"\n         >\n-          <ChevronLeft className=\"h-5 w-5\" />\n+          <Search className=\"h-4 w-4 flex-none\" aria-hidden />\n+          <span className=\"min-w-0 flex-1 truncate text-sm\">搜索你想要的商品</span>\n         </button>\n-        <label className=\"flex h-10 flex-1 items-center gap-2 rounded-full border border-border-subtle bg-surface px-3 text-text-tertiary\">\n-          <Search className=\"h-4 w-4\" />\n-          <input\n-            type=\"search\"\n-            placeholder=\"搜索你想要的商品\"\n-            aria-label=\"搜索商品\"\n-            className=\"min-w-0 flex-1 bg-transparent text-sm text-text-primary outline-none placeholder:text-text-tertiary\"\n-          />\n-        </label>\n         {/* T046｜右上角头像由「跳商城」改为「跳专栏内会员中心」（B-048 路由 slug：/dearseed/membership） */}\n         <button\n           type=\"button\"\ndiff --git a/src/pages/MallWebView.test.tsx b/src/pages/MallWebView.test.tsx\nindex dca2c77..2af7bf7 100644\n--- a/src/pages/MallWebView.test.tsx\n+++ b/src/pages/MallWebView.test.tsx\n@@ -13,6 +13,7 @@ vi.mock('../services/sso/mallSso', async (importOriginal) => {\n \n import MallWebView from './MallWebView'\n import { MALL_SHOPS } from '../services/sso/mallSso'\n+import { MALL_SEARCH_TAB_ROUTE } from '../app/router/mallSearch'\n \n function setup(initialEntry = '/mall', target: 'kbs_shop' | 'kbs_shop2' = 'kbs_shop') {\n   const dispose = vi.fn()\n@@ -45,6 +46,21 @@ describe('MallWebView', () => {\n     await waitFor(() => expect(iframe.src).toBe(expected.toString()))\n   })\n \n+  it('opens first-party product search from the Home tab and keeps SSO on the same origin', async () => {\n+    // The Home search destination is fixed to kbs_shop, even when the Mall default uses shop2.\n+    const { iframe } = setup(MALL_SEARCH_TAB_ROUTE, 'kbs_shop2')\n+    await waitFor(() => expect(iframe.src).toBe(\n+      'https://kbs-shop.3cgroup.cn/pages/goods/goods_search/index',\n+    ))\n+    expect(mocks.createBridge).toHaveBeenCalledWith(iframe, 'kbs_shop', expect.any(Object))\n+  })\n+\n+  it('does not accept a search path with extra query parameters or path traversal', async () => {\n+    const { iframe } = setup('/mall?path=%2Fpages%2Fgoods%2Fgoods_search%2Findex%3Fnext%3Dhttps%3A%2F%2Fevil.example')\n+    await waitFor(() => expect(iframe.src).toBe(MALL_SHOPS.kbs_shop.url))\n+    expect(mocks.createBridge).toHaveBeenCalledWith(iframe, 'kbs_shop', expect.any(Object))\n+  })\n+\n   it('loads the allowlisted address path', async () => {\n     const { iframe } = setup('/mall?path=%2Fpages%2Fusers%2Fuser_address_list%2Findex')\n     const expected = new URL(MALL_SHOPS.kbs_shop.url)\ndiff --git a/src/pages/MallWebView.tsx b/src/pages/MallWebView.tsx\nindex da568fc..16f2dea 100644\n--- a/src/pages/MallWebView.tsx\n+++ b/src/pages/MallWebView.tsx\n@@ -1,6 +1,7 @@\n import { useCallback, useEffect, useState } from 'react'\n import { useLocation } from 'react-router-dom'\n import { runtimePolicy } from '../app/config/runtime'\n+import { MALL_SEARCH_PATH } from '../app/router/mallSearch'\n import { Button, LoadingIndicator } from '../components/ui'\n import {\n   MALL_SHOPS,\n@@ -33,12 +34,15 @@ const PROFILE_MALL_PATHS = new Set([\n  */\n export default function MallWebView({ target }: MallWebViewProps) {\n   const location = useLocation()\n-  const mallTarget = target ?? resolveMallTarget()\n-  const shop = MALL_SHOPS[mallTarget]\n-  // Only allow the two profile destinations requested by product. Preserve the configured\n-  // shop base path (e.g. /h5) and never accept arbitrary URLs from the query string.\n+  // A fixed, allowlisted search link always opens the first-party shop requested by product,\n+  // even if the default Mall tab is configured to use a different shop.\n   const requestedPath = new URLSearchParams(location.search).get('path')\n-  const mallPath = requestedPath && PROFILE_MALL_PATHS.has(requestedPath)\n+  const isProductSearch = requestedPath === MALL_SEARCH_PATH\n+  const mallTarget = isProductSearch ? 'kbs_shop' : (target ?? resolveMallTarget())\n+  const shop = MALL_SHOPS[mallTarget]\n+  // Allow only known routes; query-string URLs cannot select arbitrary origins or paths.\n+  const mallPath = requestedPath &&\n+    (isProductSearch || PROFILE_MALL_PATHS.has(requestedPath))\n     ? requestedPath\n     : null\n   const targetUrl = new URL(shop.url)",
      "actualWorkers": 2
    },
    "preserveOutput": "always",
    "projects": [
      {
        "outputDir": "/home/runner/work/dr-card-web/dr-card-web/test-results/artifacts",
        "repeatEach": 1,
        "retries": 1,
        "metadata": {
          "ci": {
            "commitHref": "https://github.com/sanchuang-dev/dr-card-web/commit/e55462b3b5b7d556ec7a32336e1373b1601f2744",
            "commitHash": "e55462b3b5b7d556ec7a32336e1373b1601f2744",
            "prHref": "https://github.com/sanchuang-dev/dr-card-web/pull/306",
            "prTitle": "release(test): promote homepage Mall search and prod debug-arrow fix (#304)",
            "prBaseHash": "e5895f0443c9f67acd51d00fc5a9a5b8fd93d50b",
            "buildHref": "https://github.com/sanchuang-dev/dr-card-web/actions/runs/42744119730"
          },
          "gitCommit": {
            "shortHash": "e55462b",
            "hash": "e55462b3b5b7d556ec7a32336e1373b1601f2744",
            "subject": "Merge ceadf7ef23926dd70b9064ff78ebf63b064adbe8 into e5895f0443c9f67acd51d00fc5a9a5b8fd93d50b",
            "body": "Merge ceadf7ef23926dd70b9064ff78ebf63b064adbe8 into e5895f0443c9f67acd51d00fc5a9a5b8fd93d50b\n",
            "author": {
              "name": "SINGING-AKIBA",
              "email": "980325684@qq.com",
              "time": 1794278288000
            },
            "committer": {
              "name": "GitHub",
              "email": "noreply@github.com",
              "time": 1794278288000
            },
            "branch": "HEAD"
          },
          "gitDiff": "diff --git a/src/app/router/mallSearch.ts b/src/app/router/mallSearch.ts\nnew file mode 100644\nindex 0000000..c243121\n--- /dev/null\n+++ b/src/app/router/mallSearch.ts\n@@ -0,0 +1,3 @@\n+/** Single approved first-party shop search destination for the Home → Mall tab flow. */\n+export const MALL_SEARCH_PATH = '/pages/goods/goods_search/index'\n+export const MALL_SEARCH_TAB_ROUTE = `/mall?path=${encodeURIComponent(MALL_SEARCH_PATH)}`\ndiff --git a/src/pages/Home.signRecords.test.tsx b/src/pages/Home.signRecords.test.tsx\nindex 10e88aa..6ee74ae 100644\n--- a/src/pages/Home.signRecords.test.tsx\n+++ b/src/pages/Home.signRecords.test.tsx\n@@ -2,7 +2,12 @@ import { afterEach, describe, expect, it, vi } from 'vitest'\n import { cleanup, fireEvent, render, screen } from '@testing-library/react'\n import type { ReactNode } from 'react'\n \n-const mocks = vi.hoisted(() => ({ useSignRecords: vi.fn(), navigate: vi.fn(), reloadSignStatus: vi.fn() }))\n+const mocks = vi.hoisted(() => ({\n+  useSignRecords: vi.fn(),\n+  navigate: vi.fn(),\n+  reloadSignStatus: vi.fn(),\n+  bridgeLabEnabled: true,\n+}))\n \n vi.mock('react-router-dom', async (importOriginal) => {\n   const original = await importOriginal<typeof import('react-router-dom')>()\n@@ -12,6 +17,16 @@ vi.mock('react-router-dom', async (importOriginal) => {\n     useSearchParams: () => [new URLSearchParams(), vi.fn()],\n   }\n })\n+vi.mock('../app/config/runtime', async (importOriginal) => {\n+  const original = await importOriginal<typeof import('../app/config/runtime')>()\n+  return {\n+    ...original,\n+    runtimePolicy: {\n+      ...original.runtimePolicy,\n+      get bridgeLabEnabled() { return mocks.bridgeLabEnabled },\n+    },\n+  }\n+})\n vi.mock('./checkin/useCheckinFeed', () => ({ useSignRecords: mocks.useSignRecords }))\n vi.mock('./home/useHomeFeed', () => ({\n   useHomeBanners: () => ({ state: 'success', data: [] }),\n@@ -33,12 +48,14 @@ vi.mock('../components/mobile/CheckinMakeupSuccessOverlay', () => ({ default: ()\n vi.mock('../components/mobile/DebugPanel', () => ({ default: () => null }))\n \n import Home from './Home'\n+import { MALL_SEARCH_TAB_ROUTE } from '../app/router/mallSearch'\n \n afterEach(() => {\n   cleanup()\n   mocks.useSignRecords.mockReset()\n   mocks.navigate.mockReset()\n   mocks.reloadSignStatus.mockReset()\n+  mocks.bridgeLabEnabled = true\n   vi.useRealTimers()\n })\n \n@@ -68,3 +85,30 @@ describe('Issue #220: cross-day refresh', () => {\n     expect(mocks.reloadSignStatus).toHaveBeenCalledTimes(1)\n   })\n })\n+\n+describe('Issue #304: home search and production debug entry', () => {\n+  it('switches the Mall tab when the entire search box is tapped', () => {\n+    mocks.useSignRecords.mockReturnValue({ remote: { state: 'loading' }, reload: vi.fn() })\n+    render(<Home />)\n+    const search = screen.getByRole('button', { name: '搜索商品' })\n+    expect(search.textContent).toContain('搜索你想要的商品')\n+    expect(search.querySelector('svg')).toBeTruthy()\n+    fireEvent.click(search)\n+    expect(mocks.navigate).toHaveBeenCalledWith(MALL_SEARCH_TAB_ROUTE)\n+  })\n+\n+  it('hides the temporary left-side Bridge Lab entry in prod without hiding search', () => {\n+    mocks.bridgeLabEnabled = false\n+    mocks.useSignRecords.mockReturnValue({ remote: { state: 'loading' }, reload: vi.fn() })\n+    render(<Home />)\n+    expect(screen.queryByRole('button', { name: '打开 Bridge Lab（临时调试入口）' })).toBeNull()\n+    expect(screen.getByRole('button', { name: '搜索商品' }).querySelector('svg')).toBeTruthy()\n+  })\n+\n+  it('preserves the debug shortcut in nonproduction builds', () => {\n+    mocks.useSignRecords.mockReturnValue({ remote: { state: 'loading' }, reload: vi.fn() })\n+    render(<Home />)\n+    fireEvent.click(screen.getByRole('button', { name: '打开 Bridge Lab（临时调试入口）' }))\n+    expect(mocks.navigate).toHaveBeenCalledWith('/__debug/bridge-lab?osType=Android')\n+  })\n+})\ndiff --git a/src/pages/Home.tsx b/src/pages/Home.tsx\nindex 7998359..a9e434f 100644\n--- a/src/pages/Home.tsx\n+++ b/src/pages/Home.tsx\n@@ -15,6 +15,8 @@ import { useSignRecords } from './checkin/useCheckinFeed'\n import { SIGN_RECORDS_RANGE_WEEK } from '../services/signrecords'\n import UserAvatar from '../components/mobile/UserAvatar'\n import { useUserIdentity } from './profile/useUserIdentity'\n+import { runtimePolicy } from '../app/config/runtime'\n+import { MALL_SEARCH_TAB_ROUTE } from '../app/router/mallSearch'\n \n const sectionIcons = {\n   cause: Heart,\n@@ -85,26 +87,26 @@ export default function Home() {\n   return (\n     <PageContainer className=\"pb-24 pt-4\" inset={false}>\n       <section className=\"mx-4 flex items-center gap-2\" aria-label=\"搜索与用户入口\">\n-        {/* 临时调试入口（需移除）：原「返回卡博士首页」按钮临时改跳 Bridge Lab，\n-         * 便于在 App WebView 内直接进入 /__debug/bridge-lab?osType=Android。\n-         * 调试结束后恢复 onClick={() => navigate('/legacy-home')} 与原 aria-label。 */}\n+        {/* 调试入口仅出现在支持 Bridge Lab 的构建，正式环境隐藏。 */}\n+        {runtimePolicy.bridgeLabEnabled && (\n+          <button\n+            type=\"button\"\n+            aria-label=\"打开 Bridge Lab（临时调试入口）\"\n+            onClick={() => navigate('/__debug/bridge-lab?osType=Android')}\n+            className=\"flex h-10 w-10 flex-none items-center justify-center rounded-full border border-border-subtle bg-surface text-text-primary active:bg-surface-secondary\"\n+          >\n+            <ChevronLeft className=\"h-5 w-5\" />\n+          </button>\n+        )}\n         <button\n           type=\"button\"\n-          aria-label=\"打开 Bridge Lab（临时调试入口）\"\n-          onClick={() => navigate('/__debug/bridge-lab?osType=Android')}\n-          className=\"flex h-10 w-10 flex-none items-center justify-center rounded-full border border-border-subtle bg-surface text-text-primary active:bg-surface-secondary\"\n+          aria-label=\"搜索商品\"\n+          onClick={() => navigate(MALL_SEARCH_TAB_ROUTE)}\n+          className=\"flex h-10 flex-1 items-center gap-2 rounded-full border border-border-subtle bg-surface px-3 text-left text-text-tertiary active:bg-surface-secondary\"\n         >\n-          <ChevronLeft className=\"h-5 w-5\" />\n+          <Search className=\"h-4 w-4 flex-none\" aria-hidden />\n+          <span className=\"min-w-0 flex-1 truncate text-sm\">搜索你想要的商品</span>\n         </button>\n-        <label className=\"flex h-10 flex-1 items-center gap-2 rounded-full border border-border-subtle bg-surface px-3 text-text-tertiary\">\n-          <Search className=\"h-4 w-4\" />\n-          <input\n-            type=\"search\"\n-            placeholder=\"搜索你想要的商品\"\n-            aria-label=\"搜索商品\"\n-            className=\"min-w-0 flex-1 bg-transparent text-sm text-text-primary outline-none placeholder:text-text-tertiary\"\n-          />\n-        </label>\n         {/* T046｜右上角头像由「跳商城」改为「跳专栏内会员中心」（B-048 路由 slug：/dearseed/membership） */}\n         <button\n           type=\"button\"\ndiff --git a/src/pages/MallWebView.test.tsx b/src/pages/MallWebView.test.tsx\nindex dca2c77..2af7bf7 100644\n--- a/src/pages/MallWebView.test.tsx\n+++ b/src/pages/MallWebView.test.tsx\n@@ -13,6 +13,7 @@ vi.mock('../services/sso/mallSso', async (importOriginal) => {\n \n import MallWebView from './MallWebView'\n import { MALL_SHOPS } from '../services/sso/mallSso'\n+import { MALL_SEARCH_TAB_ROUTE } from '../app/router/mallSearch'\n \n function setup(initialEntry = '/mall', target: 'kbs_shop' | 'kbs_shop2' = 'kbs_shop') {\n   const dispose = vi.fn()\n@@ -45,6 +46,21 @@ describe('MallWebView', () => {\n     await waitFor(() => expect(iframe.src).toBe(expected.toString()))\n   })\n \n+  it('opens first-party product search from the Home tab and keeps SSO on the same origin', async () => {\n+    // The Home search destination is fixed to kbs_shop, even when the Mall default uses shop2.\n+    const { iframe } = setup(MALL_SEARCH_TAB_ROUTE, 'kbs_shop2')\n+    await waitFor(() => expect(iframe.src).toBe(\n+      'https://kbs-shop.3cgroup.cn/pages/goods/goods_search/index',\n+    ))\n+    expect(mocks.createBridge).toHaveBeenCalledWith(iframe, 'kbs_shop', expect.any(Object))\n+  })\n+\n+  it('does not accept a search path with extra query parameters or path traversal', async () => {\n+    const { iframe } = setup('/mall?path=%2Fpages%2Fgoods%2Fgoods_search%2Findex%3Fnext%3Dhttps%3A%2F%2Fevil.example')\n+    await waitFor(() => expect(iframe.src).toBe(MALL_SHOPS.kbs_shop.url))\n+    expect(mocks.createBridge).toHaveBeenCalledWith(iframe, 'kbs_shop', expect.any(Object))\n+  })\n+\n   it('loads the allowlisted address path', async () => {\n     const { iframe } = setup('/mall?path=%2Fpages%2Fusers%2Fuser_address_list%2Findex')\n     const expected = new URL(MALL_SHOPS.kbs_shop.url)\ndiff --git a/src/pages/MallWebView.tsx b/src/pages/MallWebView.tsx\nindex da568fc..16f2dea 100644\n--- a/src/pages/MallWebView.tsx\n+++ b/src/pages/MallWebView.tsx\n@@ -1,6 +1,7 @@\n import { useCallback, useEffect, useState } from 'react'\n import { useLocation } from 'react-router-dom'\n import { runtimePolicy } from '../app/config/runtime'\n+import { MALL_SEARCH_PATH } from '../app/router/mallSearch'\n import { Button, LoadingIndicator } from '../components/ui'\n import {\n   MALL_SHOPS,\n@@ -33,12 +34,15 @@ const PROFILE_MALL_PATHS = new Set([\n  */\n export default function MallWebView({ target }: MallWebViewProps) {\n   const location = useLocation()\n-  const mallTarget = target ?? resolveMallTarget()\n-  const shop = MALL_SHOPS[mallTarget]\n-  // Only allow the two profile destinations requested by product. Preserve the configured\n-  // shop base path (e.g. /h5) and never accept arbitrary URLs from the query string.\n+  // A fixed, allowlisted search link always opens the first-party shop requested by product,\n+  // even if the default Mall tab is configured to use a different shop.\n   const requestedPath = new URLSearchParams(location.search).get('path')\n-  const mallPath = requestedPath && PROFILE_MALL_PATHS.has(requestedPath)\n+  const isProductSearch = requestedPath === MALL_SEARCH_PATH\n+  const mallTarget = isProductSearch ? 'kbs_shop' : (target ?? resolveMallTarget())\n+  const shop = MALL_SHOPS[mallTarget]\n+  // Allow only known routes; query-string URLs cannot select arbitrary origins or paths.\n+  const mallPath = requestedPath &&\n+    (isProductSearch || PROFILE_MALL_PATHS.has(requestedPath))\n     ? requestedPath\n     : null\n   const targetUrl = new URL(shop.url)",
          "actualWorkers": 2
        },
        "id": "chromium-mobile",
        "name": "chromium-mobile",
        "testDir": "/home/runner/work/dr-card-web/dr-card-web/tests/e2e",
        "testIgnore": [],
        "testMatch": [
          "**/*.@(spec|test).?(c|m)[jt]s?(x)"
        ],
        "timeout": 30000
      }
    ],
    "quiet": false,
    "reporter": [
      [
        "line",
        null
      ],
      [
        "html",
        {
          "outputFolder": "playwright-report",
          "open": "never"
        }
      ],
      [
        "json",
        {
          "outputFile": "test-results/playwright-results.json"
        }
      ]
    ],
    "reportSlowTests": {
      "max": 5,
      "threshold": 300000
    },
    "shard": null,
    "tags": [],
    "updateSnapshots": "missing",
    "updateSourceMethod": "patch",
    "version": "1.63.0",
    "workers": 2,
    "webServer": null
  },
  "suites": [
    {
      "title": "test-gate.spec.ts",
      "file": "test-gate.spec.ts",
      "column": 0,
      "line": 0,
      "specs": [
        {
          "title": "@test-gate build identity matches the production-like test policy",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 0,
                  "parallelIndex": 0,
                  "status": "passed",
                  "duration": 54,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-10T02:39:10.008Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-c3b6e0ce5d8c5d8734e5",
          "file": "test-gate.spec.ts",
          "line": 31,
          "column": 1
        },
        {
          "title": "@test-gate browser entry does not start the app router",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 1,
                  "parallelIndex": 1,
                  "status": "passed",
                  "duration": 1034,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-10T02:39:10.038Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-8ed37c8041402a2a6b34",
          "file": "test-gate.spec.ts",
          "line": 40,
          "column": 1
        },
        {
          "title": "@test-gate / shows the unsupported-host notice in a browser",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 0,
                  "parallelIndex": 0,
                  "status": "passed",
                  "duration": 1046,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-10T02:39:10.126Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-b030d57053896c93a550",
          "file": "test-gate.spec.ts",
          "line": 51,
          "column": 3
        },
        {
          "title": "@test-gate /profile shows the unsupported-host notice in a browser",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 1,
                  "parallelIndex": 1,
                  "status": "passed",
                  "duration": 906,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-10T02:39:12.576Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-8605535a2807e17fcccc",
          "file": "test-gate.spec.ts",
          "line": 51,
          "column": 3
        },
        {
          "title": "@test-gate /settings shows the unsupported-host notice in a browser",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 0,
                  "parallelIndex": 0,
                  "status": "passed",
                  "duration": 900,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-10T02:39:12.578Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-4a2fdc9884c1725de4f8",
          "file": "test-gate.spec.ts",
          "line": 51,
          "column": 3
        },
        {
          "title": "@test-gate gated browser entry issues no business API request",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 1,
                  "parallelIndex": 1,
                  "status": "passed",
                  "duration": 873,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-10T02:39:13.496Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-09f38a2b8ed735e192d8",
          "file": "test-gate.spec.ts",
          "line": 65,
          "column": 1
        },
        {
          "title": "@test-gate injected native host bypasses the notice and starts the app",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 0,
                  "parallelIndex": 0,
                  "status": "passed",
                  "duration": 929,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-10T02:39:13.498Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-8caf237c4d42513fff2f",
          "file": "test-gate.spec.ts",
          "line": 80,
          "column": 1
        }
      ]
    }
  ],
  "errors": [],
  "stats": {
    "startTime": "2026-11-10T02:39:09.127Z",
    "duration": 5346.089,
    "expected": 7,
    "skipped": 0,
    "unexpected": 0,
    "flaky": 0
  }
}