{
  "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/69a0b3e6eb49ceda771f227a9f0ee6d5bca524f3",
        "commitHash": "69a0b3e6eb49ceda771f227a9f0ee6d5bca524f3",
        "prHref": "https://github.com/sanchuang-dev/dr-card-web/pull/267",
        "prTitle": "feat(profile): load order and address management in mall iframe",
        "prBaseHash": "8a426fb13a88b7ec3771d3fdb62ceaae54b442fe",
        "buildHref": "https://github.com/sanchuang-dev/dr-card-web/actions/runs/40073530155"
      },
      "gitCommit": {
        "shortHash": "69a0b3e",
        "hash": "69a0b3e6eb49ceda771f227a9f0ee6d5bca524f3",
        "subject": "Merge 96cb655210c525e6e569ee5976aa64c1e04c42ec into 8a426fb13a88b7ec3771d3fdb62ceaae54b442fe",
        "body": "Merge 96cb655210c525e6e569ee5976aa64c1e04c42ec into 8a426fb13a88b7ec3771d3fdb62ceaae54b442fe\n",
        "author": {
          "name": "SINGING-AKIBA",
          "email": "980325684@qq.com",
          "time": 1792826228000
        },
        "committer": {
          "name": "GitHub",
          "email": "noreply@github.com",
          "time": 1792826228000
        },
        "branch": "HEAD"
      },
      "gitDiff": "diff --git a/src/pages/MallWebView.test.tsx b/src/pages/MallWebView.test.tsx\nindex a54cc21..dca2c77 100644\n--- a/src/pages/MallWebView.test.tsx\n+++ b/src/pages/MallWebView.test.tsx\n@@ -1,4 +1,5 @@\n import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'\n+import { MemoryRouter } from 'react-router-dom'\n import { describe, expect, it, vi } from 'vitest'\n \n const mocks = vi.hoisted(() => ({\n@@ -13,68 +14,71 @@ vi.mock('../services/sso/mallSso', async (importOriginal) => {\n import MallWebView from './MallWebView'\n import { MALL_SHOPS } from '../services/sso/mallSso'\n \n-function setup() {\n+function setup(initialEntry = '/mall', target: 'kbs_shop' | 'kbs_shop2' = 'kbs_shop') {\n   const dispose = vi.fn()\n   mocks.createBridge.mockReturnValue({ dispose })\n-  const view = render(<MallWebView target=\"kbs_shop\" />)\n+  const view = render(\n+    <MemoryRouter initialEntries={[initialEntry]}>\n+      <MallWebView target={target} />\n+    </MemoryRouter>,\n+  )\n   const iframe = screen.getByTitle('卡博士商城') as HTMLIFrameElement\n   return { view, iframe, dispose }\n }\n \n describe('MallWebView', () => {\n-  it('loads the configured shop /h5 entry after attaching the SSO bridge', async () => {\n+  it('loads the configured shop entry after attaching the SSO bridge', async () => {\n     const { iframe } = setup()\n-\n     await waitFor(() => expect(iframe.src).toBe(MALL_SHOPS.kbs_shop.url))\n-    expect(mocks.createBridge).toHaveBeenCalledWith(\n-      iframe,\n-      'kbs_shop',\n-      expect.objectContaining({\n-        onSsoError: expect.any(Function),\n-        onSsoResult: expect.any(Function),\n-      }),\n-    )\n+    expect(mocks.createBridge).toHaveBeenCalledWith(iframe, 'kbs_shop', expect.objectContaining({\n+      onSsoError: expect.any(Function),\n+      onSsoResult: expect.any(Function),\n+    }))\n     expect(screen.getByText('商城加载中…')).toBeTruthy()\n     expect(screen.getByText('商城加载中…').closest('[data-mall-target]')).toBeTruthy()\n   })\n \n+  it('loads the allowlisted order path while preserving the shop base path', async () => {\n+    const { iframe } = setup('/mall?path=%2Fpages%2Fgoods%2Forder_list%2Findex', 'kbs_shop2')\n+    const expected = new URL(MALL_SHOPS.kbs_shop2.url)\n+    expected.pathname = `${expected.pathname.replace(/\\/$/, '')}/pages/goods/order_list/index`\n+    await waitFor(() => expect(iframe.src).toBe(expected.toString()))\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)\n+    expected.pathname = `${expected.pathname.replace(/\\/$/, '')}/pages/users/user_address_list/index`\n+    await waitFor(() => expect(iframe.src).toBe(expected.toString()))\n+  })\n+\n+  it('ignores unapproved paths and external URLs', async () => {\n+    const { iframe } = setup('/mall?path=https%3A%2F%2Fevil.example%2Fphishing')\n+    await waitFor(() => expect(iframe.src).toBe(MALL_SHOPS.kbs_shop.url))\n+  })\n+\n   it('stops showing the loading state once the shop reports load', async () => {\n     const { iframe } = setup()\n     await waitFor(() => expect(iframe.src).toBe(MALL_SHOPS.kbs_shop.url))\n-\n     fireEvent.load(iframe)\n-\n     await waitFor(() => expect(screen.queryByText('商城加载中…')).toBeNull())\n   })\n \n   it('shows a retry state when the mall reports an SSO failure', async () => {\n     const { iframe } = setup()\n     await waitFor(() => expect(iframe.src).toBe(MALL_SHOPS.kbs_shop.url))\n-\n     const options = mocks.createBridge.mock.calls[0][2] as {\n       onSsoError: (error: { code: string; message: string }) => void\n     }\n-    act(() => {\n-      options.onSsoError({ code: 'SSO_TICKET_EXPIRED', message: '登录凭证已失效' })\n-    })\n-\n+    act(() => options.onSsoError({ code: 'SSO_TICKET_EXPIRED', message: '登录凭证已失效' }))\n     expect(screen.getByText('商城登录未完成，请重试')).toBeTruthy()\n-\n     fireEvent.click(screen.getByRole('button', { name: '重试' }))\n-\n     await waitFor(() => expect(mocks.createBridge).toHaveBeenCalledTimes(2))\n   })\n \n-  it('uses the second shop origin and /h5 entry when selected', async () => {\n-    mocks.createBridge.mockReturnValue({ dispose: vi.fn() })\n-    render(<MallWebView target=\"kbs_shop2\" />)\n-\n-    const iframe = screen.getByTitle('卡博士商城') as HTMLIFrameElement\n+  it('uses the second shop origin and configured base path when selected', async () => {\n+    const { iframe } = setup('/mall', 'kbs_shop2')\n     await waitFor(() => expect(iframe.src).toBe(MALL_SHOPS.kbs_shop2.url))\n-    expect(mocks.createBridge).toHaveBeenCalledWith(\n-      iframe,\n-      'kbs_shop2',\n-      expect.any(Object),\n-    )\n+    expect(mocks.createBridge).toHaveBeenCalledWith(iframe, 'kbs_shop2', expect.any(Object))\n   })\n })\ndiff --git a/src/pages/MallWebView.tsx b/src/pages/MallWebView.tsx\nindex f819683..da568fc 100644\n--- a/src/pages/MallWebView.tsx\n+++ b/src/pages/MallWebView.tsx\n@@ -1,4 +1,5 @@\n import { useCallback, useEffect, useState } from 'react'\n+import { useLocation } from 'react-router-dom'\n import { runtimePolicy } from '../app/config/runtime'\n import { Button, LoadingIndicator } from '../components/ui'\n import {\n@@ -14,6 +15,11 @@ export interface MallWebViewProps {\n   target?: MallTarget\n }\n \n+const PROFILE_MALL_PATHS = new Set([\n+  '/pages/goods/order_list/index',\n+  '/pages/users/user_address_list/index',\n+])\n+\n /**\n  * 商城 H5 承载页\n  * -------------------------------------------------------------\n@@ -26,8 +32,23 @@ export interface MallWebViewProps {\n  * - 商城换域名/路径时只改 `MALL_SHOPS`，本页不写死地址。\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+  const requestedPath = new URLSearchParams(location.search).get('path')\n+  const mallPath = requestedPath && PROFILE_MALL_PATHS.has(requestedPath)\n+    ? requestedPath\n+    : null\n+  const targetUrl = new URL(shop.url)\n+  if (mallPath) {\n+    const basePath = targetUrl.pathname.replace(/\\/$/, '')\n+    targetUrl.pathname = `${basePath}${mallPath}`\n+  }\n+  targetUrl.search = ''\n+  targetUrl.hash = ''\n+  const mallUrl = targetUrl.toString()\n \n   const [iframe, setIframe] = useState<HTMLIFrameElement | null>(null)\n   const [loaded, setLoaded] = useState(false)\n@@ -59,10 +80,10 @@ export default function MallWebView({ target }: MallWebViewProps) {\n \n     // 监听器就绪后再开始导航，避免 B 的首个 SSO.REQUEST 早于监听器。\n     // StrictMode 下 effect 会执行两次，已导航到目标地址时不要重复加载商城。\n-    if (iframe.src !== shop.url) iframe.src = shop.url\n+    if (iframe.src !== mallUrl) iframe.src = mallUrl\n \n     return () => bridge.dispose()\n-  }, [iframe, mallTarget, shop.url])\n+  }, [iframe, mallTarget, mallUrl])\n \n   useEffect(() => {\n     if (!iframe || loaded || problem) return\n@@ -110,7 +131,7 @@ export default function MallWebView({ target }: MallWebViewProps) {\n         className=\"h-full w-full flex-1 border-0\"\n         onLoad={(event) => {\n           // 初始 about:blank 也会触发 load，只认商城自己的地址。\n-          if (event.currentTarget.src === shop.url) setLoaded(true)\n+          if (event.currentTarget.src === mallUrl) setLoaded(true)\n         }}\n       />\n     </div>\ndiff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx\nindex 1f603ab..b9b1624 100644\n--- a/src/pages/Profile.tsx\n+++ b/src/pages/Profile.tsx\n@@ -34,8 +34,8 @@ type Tile = {\n const tiles: Tile[] = [\n   { icon: Ticket, name: '卡券兑换', from: '#FFF8E6', deep: '#F4DFA9', color: '#B5793B', to: '/redeem' },\n   { icon: Crown, name: '会员权益', from: '#FFF4CF', deep: '#E8C361', color: '#8A5A10', to: '/membership' },\n-  { icon: ClipboardList, name: '订单管理', from: '#FFF3EB', deep: '#FFD3C0', color: '#D63D10', to: '/orders' },\n-  { icon: MapPin, name: '地址管理', from: '#FFF8E6', deep: '#F3DFA9', color: '#9A6110', to: '/address' },\n+  { icon: ClipboardList, name: '订单管理', from: '#FFF3EB', deep: '#FFD3C0', color: '#D63D10', to: '/mall?path=%2Fpages%2Fgoods%2Forder_list%2Findex' },\n+  { icon: MapPin, name: '地址管理', from: '#FFF8E6', deep: '#F3DFA9', color: '#9A6110', to: '/mall?path=%2Fpages%2Fusers%2Fuser_address_list%2Findex' },\n   { icon: UserRoundPlus, name: '绑定搭子', from: '#EFFCFE', deep: '#CDEFF5', color: '#0E9FB3', to: '/buddy' },\n   { icon: Gift, name: '品牌福利官', from: '#EEFAF3', deep: '#CDEAD9', color: '#147A4C', to: '/service/welfare-officer' },\n   { icon: Headphones, name: '客服中心', from: '#F6F8FB', deep: '#E1E6ED', color: '#535D72', to: '/service/chat' },",
      "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/69a0b3e6eb49ceda771f227a9f0ee6d5bca524f3",
            "commitHash": "69a0b3e6eb49ceda771f227a9f0ee6d5bca524f3",
            "prHref": "https://github.com/sanchuang-dev/dr-card-web/pull/267",
            "prTitle": "feat(profile): load order and address management in mall iframe",
            "prBaseHash": "8a426fb13a88b7ec3771d3fdb62ceaae54b442fe",
            "buildHref": "https://github.com/sanchuang-dev/dr-card-web/actions/runs/40073530155"
          },
          "gitCommit": {
            "shortHash": "69a0b3e",
            "hash": "69a0b3e6eb49ceda771f227a9f0ee6d5bca524f3",
            "subject": "Merge 96cb655210c525e6e569ee5976aa64c1e04c42ec into 8a426fb13a88b7ec3771d3fdb62ceaae54b442fe",
            "body": "Merge 96cb655210c525e6e569ee5976aa64c1e04c42ec into 8a426fb13a88b7ec3771d3fdb62ceaae54b442fe\n",
            "author": {
              "name": "SINGING-AKIBA",
              "email": "980325684@qq.com",
              "time": 1792826228000
            },
            "committer": {
              "name": "GitHub",
              "email": "noreply@github.com",
              "time": 1792826228000
            },
            "branch": "HEAD"
          },
          "gitDiff": "diff --git a/src/pages/MallWebView.test.tsx b/src/pages/MallWebView.test.tsx\nindex a54cc21..dca2c77 100644\n--- a/src/pages/MallWebView.test.tsx\n+++ b/src/pages/MallWebView.test.tsx\n@@ -1,4 +1,5 @@\n import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'\n+import { MemoryRouter } from 'react-router-dom'\n import { describe, expect, it, vi } from 'vitest'\n \n const mocks = vi.hoisted(() => ({\n@@ -13,68 +14,71 @@ vi.mock('../services/sso/mallSso', async (importOriginal) => {\n import MallWebView from './MallWebView'\n import { MALL_SHOPS } from '../services/sso/mallSso'\n \n-function setup() {\n+function setup(initialEntry = '/mall', target: 'kbs_shop' | 'kbs_shop2' = 'kbs_shop') {\n   const dispose = vi.fn()\n   mocks.createBridge.mockReturnValue({ dispose })\n-  const view = render(<MallWebView target=\"kbs_shop\" />)\n+  const view = render(\n+    <MemoryRouter initialEntries={[initialEntry]}>\n+      <MallWebView target={target} />\n+    </MemoryRouter>,\n+  )\n   const iframe = screen.getByTitle('卡博士商城') as HTMLIFrameElement\n   return { view, iframe, dispose }\n }\n \n describe('MallWebView', () => {\n-  it('loads the configured shop /h5 entry after attaching the SSO bridge', async () => {\n+  it('loads the configured shop entry after attaching the SSO bridge', async () => {\n     const { iframe } = setup()\n-\n     await waitFor(() => expect(iframe.src).toBe(MALL_SHOPS.kbs_shop.url))\n-    expect(mocks.createBridge).toHaveBeenCalledWith(\n-      iframe,\n-      'kbs_shop',\n-      expect.objectContaining({\n-        onSsoError: expect.any(Function),\n-        onSsoResult: expect.any(Function),\n-      }),\n-    )\n+    expect(mocks.createBridge).toHaveBeenCalledWith(iframe, 'kbs_shop', expect.objectContaining({\n+      onSsoError: expect.any(Function),\n+      onSsoResult: expect.any(Function),\n+    }))\n     expect(screen.getByText('商城加载中…')).toBeTruthy()\n     expect(screen.getByText('商城加载中…').closest('[data-mall-target]')).toBeTruthy()\n   })\n \n+  it('loads the allowlisted order path while preserving the shop base path', async () => {\n+    const { iframe } = setup('/mall?path=%2Fpages%2Fgoods%2Forder_list%2Findex', 'kbs_shop2')\n+    const expected = new URL(MALL_SHOPS.kbs_shop2.url)\n+    expected.pathname = `${expected.pathname.replace(/\\/$/, '')}/pages/goods/order_list/index`\n+    await waitFor(() => expect(iframe.src).toBe(expected.toString()))\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)\n+    expected.pathname = `${expected.pathname.replace(/\\/$/, '')}/pages/users/user_address_list/index`\n+    await waitFor(() => expect(iframe.src).toBe(expected.toString()))\n+  })\n+\n+  it('ignores unapproved paths and external URLs', async () => {\n+    const { iframe } = setup('/mall?path=https%3A%2F%2Fevil.example%2Fphishing')\n+    await waitFor(() => expect(iframe.src).toBe(MALL_SHOPS.kbs_shop.url))\n+  })\n+\n   it('stops showing the loading state once the shop reports load', async () => {\n     const { iframe } = setup()\n     await waitFor(() => expect(iframe.src).toBe(MALL_SHOPS.kbs_shop.url))\n-\n     fireEvent.load(iframe)\n-\n     await waitFor(() => expect(screen.queryByText('商城加载中…')).toBeNull())\n   })\n \n   it('shows a retry state when the mall reports an SSO failure', async () => {\n     const { iframe } = setup()\n     await waitFor(() => expect(iframe.src).toBe(MALL_SHOPS.kbs_shop.url))\n-\n     const options = mocks.createBridge.mock.calls[0][2] as {\n       onSsoError: (error: { code: string; message: string }) => void\n     }\n-    act(() => {\n-      options.onSsoError({ code: 'SSO_TICKET_EXPIRED', message: '登录凭证已失效' })\n-    })\n-\n+    act(() => options.onSsoError({ code: 'SSO_TICKET_EXPIRED', message: '登录凭证已失效' }))\n     expect(screen.getByText('商城登录未完成，请重试')).toBeTruthy()\n-\n     fireEvent.click(screen.getByRole('button', { name: '重试' }))\n-\n     await waitFor(() => expect(mocks.createBridge).toHaveBeenCalledTimes(2))\n   })\n \n-  it('uses the second shop origin and /h5 entry when selected', async () => {\n-    mocks.createBridge.mockReturnValue({ dispose: vi.fn() })\n-    render(<MallWebView target=\"kbs_shop2\" />)\n-\n-    const iframe = screen.getByTitle('卡博士商城') as HTMLIFrameElement\n+  it('uses the second shop origin and configured base path when selected', async () => {\n+    const { iframe } = setup('/mall', 'kbs_shop2')\n     await waitFor(() => expect(iframe.src).toBe(MALL_SHOPS.kbs_shop2.url))\n-    expect(mocks.createBridge).toHaveBeenCalledWith(\n-      iframe,\n-      'kbs_shop2',\n-      expect.any(Object),\n-    )\n+    expect(mocks.createBridge).toHaveBeenCalledWith(iframe, 'kbs_shop2', expect.any(Object))\n   })\n })\ndiff --git a/src/pages/MallWebView.tsx b/src/pages/MallWebView.tsx\nindex f819683..da568fc 100644\n--- a/src/pages/MallWebView.tsx\n+++ b/src/pages/MallWebView.tsx\n@@ -1,4 +1,5 @@\n import { useCallback, useEffect, useState } from 'react'\n+import { useLocation } from 'react-router-dom'\n import { runtimePolicy } from '../app/config/runtime'\n import { Button, LoadingIndicator } from '../components/ui'\n import {\n@@ -14,6 +15,11 @@ export interface MallWebViewProps {\n   target?: MallTarget\n }\n \n+const PROFILE_MALL_PATHS = new Set([\n+  '/pages/goods/order_list/index',\n+  '/pages/users/user_address_list/index',\n+])\n+\n /**\n  * 商城 H5 承载页\n  * -------------------------------------------------------------\n@@ -26,8 +32,23 @@ export interface MallWebViewProps {\n  * - 商城换域名/路径时只改 `MALL_SHOPS`，本页不写死地址。\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+  const requestedPath = new URLSearchParams(location.search).get('path')\n+  const mallPath = requestedPath && PROFILE_MALL_PATHS.has(requestedPath)\n+    ? requestedPath\n+    : null\n+  const targetUrl = new URL(shop.url)\n+  if (mallPath) {\n+    const basePath = targetUrl.pathname.replace(/\\/$/, '')\n+    targetUrl.pathname = `${basePath}${mallPath}`\n+  }\n+  targetUrl.search = ''\n+  targetUrl.hash = ''\n+  const mallUrl = targetUrl.toString()\n \n   const [iframe, setIframe] = useState<HTMLIFrameElement | null>(null)\n   const [loaded, setLoaded] = useState(false)\n@@ -59,10 +80,10 @@ export default function MallWebView({ target }: MallWebViewProps) {\n \n     // 监听器就绪后再开始导航，避免 B 的首个 SSO.REQUEST 早于监听器。\n     // StrictMode 下 effect 会执行两次，已导航到目标地址时不要重复加载商城。\n-    if (iframe.src !== shop.url) iframe.src = shop.url\n+    if (iframe.src !== mallUrl) iframe.src = mallUrl\n \n     return () => bridge.dispose()\n-  }, [iframe, mallTarget, shop.url])\n+  }, [iframe, mallTarget, mallUrl])\n \n   useEffect(() => {\n     if (!iframe || loaded || problem) return\n@@ -110,7 +131,7 @@ export default function MallWebView({ target }: MallWebViewProps) {\n         className=\"h-full w-full flex-1 border-0\"\n         onLoad={(event) => {\n           // 初始 about:blank 也会触发 load，只认商城自己的地址。\n-          if (event.currentTarget.src === shop.url) setLoaded(true)\n+          if (event.currentTarget.src === mallUrl) setLoaded(true)\n         }}\n       />\n     </div>\ndiff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx\nindex 1f603ab..b9b1624 100644\n--- a/src/pages/Profile.tsx\n+++ b/src/pages/Profile.tsx\n@@ -34,8 +34,8 @@ type Tile = {\n const tiles: Tile[] = [\n   { icon: Ticket, name: '卡券兑换', from: '#FFF8E6', deep: '#F4DFA9', color: '#B5793B', to: '/redeem' },\n   { icon: Crown, name: '会员权益', from: '#FFF4CF', deep: '#E8C361', color: '#8A5A10', to: '/membership' },\n-  { icon: ClipboardList, name: '订单管理', from: '#FFF3EB', deep: '#FFD3C0', color: '#D63D10', to: '/orders' },\n-  { icon: MapPin, name: '地址管理', from: '#FFF8E6', deep: '#F3DFA9', color: '#9A6110', to: '/address' },\n+  { icon: ClipboardList, name: '订单管理', from: '#FFF3EB', deep: '#FFD3C0', color: '#D63D10', to: '/mall?path=%2Fpages%2Fgoods%2Forder_list%2Findex' },\n+  { icon: MapPin, name: '地址管理', from: '#FFF8E6', deep: '#F3DFA9', color: '#9A6110', to: '/mall?path=%2Fpages%2Fusers%2Fuser_address_list%2Findex' },\n   { icon: UserRoundPlus, name: '绑定搭子', from: '#EFFCFE', deep: '#CDEFF5', color: '#0E9FB3', to: '/buddy' },\n   { icon: Gift, name: '品牌福利官', from: '#EEFAF3', deep: '#CDEAD9', color: '#147A4C', to: '/service/welfare-officer' },\n   { icon: Headphones, name: '客服中心', from: '#F6F8FB', deep: '#E1E6ED', color: '#535D72', to: '/service/chat' },",
          "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": 63,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-10-24T07:18:19.540Z",
                  "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": 1307,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-10-24T07:18:19.499Z",
                  "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": 1318,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-10-24T07:18:19.683Z",
                  "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": 1074,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-10-24T07:18:21.211Z",
                  "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": 1079,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-10-24T07:18:21.212Z",
                  "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": 1054,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-10-24T07:18:22.302Z",
                  "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": 1177,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-10-24T07:18:22.319Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-8caf237c4d42513fff2f",
          "file": "test-gate.spec.ts",
          "line": 80,
          "column": 1
        }
      ]
    }
  ],
  "errors": [],
  "stats": {
    "startTime": "2026-10-24T07:18:18.537Z",
    "duration": 5021.610000000001,
    "expected": 7,
    "skipped": 0,
    "unexpected": 0,
    "flaky": 0
  }
}