{
  "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/fa62e281efb6290a537c2c7b12fb530d28cc8ca7",
        "commitHash": "fa62e281efb6290a537c2c7b12fb530d28cc8ca7",
        "prHref": "https://github.com/sanchuang-dev/dr-card-web/pull/276",
        "prTitle": "release(test): #275 客服直达页无 H5 历史时关闭 WebView",
        "prBaseHash": "93b40eb8cd9c57551b2e4922e8c481f74b3f3cc0",
        "buildHref": "https://github.com/sanchuang-dev/dr-card-web/actions/runs/41557905585"
      },
      "gitCommit": {
        "shortHash": "fa62e28",
        "hash": "fa62e281efb6290a537c2c7b12fb530d28cc8ca7",
        "subject": "Merge 832177ea785a59ae183f8608c63784c7298a30c3 into 93b40eb8cd9c57551b2e4922e8c481f74b3f3cc0",
        "body": "Merge 832177ea785a59ae183f8608c63784c7298a30c3 into 93b40eb8cd9c57551b2e4922e8c481f74b3f3cc0\n",
        "author": {
          "name": "Tomz Dang",
          "email": "dangjingtao@163.com",
          "time": 1793668198000
        },
        "committer": {
          "name": "GitHub",
          "email": "noreply@github.com",
          "time": 1793668198000
        },
        "branch": "HEAD"
      },
      "gitDiff": "diff --git a/docs/engineering/native-oss-redirect-entry.md b/docs/engineering/native-oss-redirect-entry.md\nindex 929a9ff..11f2aad 100644\n--- a/docs/engineering/native-oss-redirect-entry.md\n+++ b/docs/engineering/native-oss-redirect-entry.md\n@@ -17,3 +17,10 @@\n - `test/prod` 的 Native WebView 宿主门禁在 Router 启动之前执行，不会被参数绕过。\n - Router 跳转后地址为 Browser History 真实业务路径，页面刷新、进程恢复重新请求深层路径时，仍要求 CDN/网关按环境配置 SPA rewrite（见 #80/#171）；原生每次新开 WebView 应使用静态入口 URL。\n - 入口跳转只保证 H5 路由可达，不代表人工客服的真实 Socket/API 已完成业务验收。\n+\n+## 返回语义\n+\n+- 从一个 H5 页面进入另一个 H5 页面时，React Router 管理的 `history.state.idx` 大于零，标题栏返回执行 `navigate(-1)`，回到上一条 H5 路由。\n+- 从 Native 静态入口直接通过 `index.html?redirect=...` 进入目标页时，初始 loader 使用 replace，不产生上一条 H5 路由（`idx === 0`）；标题栏返回调用正式 `closeWebView()` capability，关闭当前 WebView 容器。\n+- 不使用 `window.history.length` 判断 H5 历史，因为它统计浏览器文档历史，不等同 React Router 管理的路由历史。\n+- `closeWebView` 的 H5 adapter / fail-closed 行为由 H030 定义。Native App 只有实际注入对应平台方法后才能关闭；方法缺失或 Bridge 不可用时，H5 明确提示，不静默无响应。\ndiff --git a/src/app/router/history.test.ts b/src/app/router/history.test.ts\nnew file mode 100644\nindex 0000000..cd90df7\n--- /dev/null\n+++ b/src/app/router/history.test.ts\n@@ -0,0 +1,17 @@\n+import { describe, expect, it } from 'vitest'\n+import { hasPreviousRouterEntry } from './history'\n+\n+describe('React Router history boundary', () => {\n+  it.each([\n+    ['missing state', null, false],\n+    ['missing index', {}, false],\n+    ['initial document entry', { idx: 0 }, false],\n+    ['previous H5 route exists', { idx: 1 }, true],\n+    ['multiple H5 routes exist', { idx: 3 }, true],\n+    ['negative index', { idx: -1 }, false],\n+    ['fractional index', { idx: 1.5 }, false],\n+    ['string index', { idx: '1' }, false],\n+  ])('classifies %s', (_label, state, expected) => {\n+    expect(hasPreviousRouterEntry(state)).toBe(expected)\n+  })\n+})\ndiff --git a/src/app/router/history.ts b/src/app/router/history.ts\nnew file mode 100644\nindex 0000000..59799ba\n--- /dev/null\n+++ b/src/app/router/history.ts\n@@ -0,0 +1,10 @@\n+/**\n+ * React Router 6 BrowserHistory stores its managed entry index in history.state.idx.\n+ * This is intentionally not window.history.length: that counts document entries and cannot\n+ * answer whether the current H5 document has a previous SPA route.\n+ */\n+export function hasPreviousRouterEntry(historyState: unknown): boolean {\n+  if (typeof historyState !== 'object' || historyState === null) return false\n+  const index = (historyState as { idx?: unknown }).idx\n+  return typeof index === 'number' && Number.isSafeInteger(index) && index > 0\n+}\ndiff --git a/src/layouts/MobileLayout.tsx b/src/layouts/MobileLayout.tsx\nindex 676bb06..0b048ff 100644\n--- a/src/layouts/MobileLayout.tsx\n+++ b/src/layouts/MobileLayout.tsx\n@@ -19,7 +19,8 @@ import { protectedFixtureRedirect, useOverlay } from '../app/fixtures/useFixture\n import H5ScrollRestoration from '../components/mobile/H5ScrollRestoration'\n import MallWebView from '../pages/MallWebView'\n import Toast from '../components/ui/Toast'\n-import { closeWebView } from '../services/nativeBridge'\n+import { closeWebView, NativeBridgeError } from '../services/nativeBridge'\n+import { hasPreviousRouterEntry } from '../app/router/history'\n import { AUTH_EXPIRED_EVENT, AUTH_FAILURE_EVENT, setAuthFlowEnabled } from '../services/auth/session'\n \n type H5RouteTransitionKind = 'none' | 'tab' | 'forward' | 'back'\n@@ -121,6 +122,7 @@ export default function MobileLayout() {\n   const { unreadCount, countLoaded } = useNotifications()\n   const { open: openOverlay } = useOverlay()\n   const [authExpired, setAuthExpired] = useState(false)\n+  const [backFeedback, setBackFeedback] = useState<string | null>(null)\n   const authExpiredHandledRef = useRef(false)\n   const showLegacyNav = isLegacyTabPath(location.pathname)\n   const showNav = showLegacyNav || isFormalH5TabPath(location.pathname)\n@@ -153,6 +155,12 @@ export default function MobileLayout() {\n     }\n   }, [activeFormalH5, navigate])\n \n+  useEffect(() => {\n+    if (!backFeedback) return\n+    const timeoutId = window.setTimeout(() => setBackFeedback(null), 2600)\n+    return () => window.clearTimeout(timeoutId)\n+  }, [backFeedback])\n+\n   const fixtureRedirect = activeFormalH5 ? protectedFixtureRedirect(location) : null\n   const titleBarMode = route?.titleBar ?? 'back'\n   const fallbackTitle = location.pathname === '/tokens' ? '品牌 Token 展示' : '页面不存在'\n@@ -196,6 +204,30 @@ export default function MobileLayout() {\n     navigate(target)\n   }\n \n+  const handleActiveFormalH5Back = () => {\n+    // React Router's BrowserHistory `idx` counts H5-managed entries. The initial OSS loader\n+    // replace leaves it at zero; only a real previous H5 route should receive navigate(-1).\n+    if (hasPreviousRouterEntry(window.history.state)) {\n+      navigate(-1)\n+      return\n+    }\n+\n+    void closeWebView().catch((error: unknown) => {\n+      console.error('[h5-shell-back]', error)\n+      if (error instanceof NativeBridgeError) {\n+        if (error.code === 'bridge-disabled') {\n+          setBackFeedback('当前环境未启用原生关闭能力')\n+        } else if (error.code === 'bridge-unsupported' || error.code === 'capability-unsupported') {\n+          setBackFeedback('当前 App 版本暂不支持关闭页面')\n+        } else {\n+          setBackFeedback('关闭页面失败，请稍后重试')\n+        }\n+        return\n+      }\n+      setBackFeedback('关闭页面失败，请稍后重试')\n+    })\n+  }\n+\n   const openMarkAllRead = () => {\n     openOverlay('clear')\n   }\n@@ -247,6 +279,11 @@ export default function MobileLayout() {\n           <Toast message=\"用户信息过期\" />\n         </div>\n       )}\n+      {backFeedback && (\n+        <div className=\"pointer-events-none fixed inset-x-0 bottom-24 z-[101] flex justify-center px-6\">\n+          <Toast message={backFeedback} status=\"danger\" />\n+        </div>\n+      )}\n       <div className=\"app-background flex h-screen supports-[height:100dvh]:h-dvh flex-col overflow-hidden pt-[env(safe-area-inset-top)] text-text-primary\">\n         {/* T013R7：min-w-0 防止 TitleBar 第三列 action 被撑大撑出页面右侧 */}\n         <div className=\"min-w-0 shrink-0\">\n@@ -254,7 +291,13 @@ export default function MobileLayout() {\n             <TitleBar\n               title={title}\n               leadingAction={leadingAction}\n-              onBack={route?.backTo ? () => navigateShell(route.backTo as string) : undefined}\n+              onBack={\n+                route?.backTo\n+                  ? () => navigateShell(route.backTo as string)\n+                  : activeFormalH5\n+                    ? handleActiveFormalH5Back\n+                    : undefined\n+              }\n               action={titleAction}\n               actionWide={isNotificationsPage}\n               fullWidth={activeFormalH5}\ndiff --git a/tests/e2e/formal-h5.spec.ts b/tests/e2e/formal-h5.spec.ts\nindex 101a784..a17ef8a 100644\n--- a/tests/e2e/formal-h5.spec.ts\n+++ b/tests/e2e/formal-h5.spec.ts\n@@ -470,6 +470,37 @@ test('@formal-h5 OSS index redirect enters the target without mounting Home', as\n   expect(await page.evaluate(() => history.length)).toBe(\n     await page.evaluate(() => (window as Window & { __entryHistoryLength?: number }).__entryHistoryLength),\n   )\n+  expect(await page.evaluate(() => (history.state as { idx?: number } | null)?.idx)).toBe(0)\n+})\n+\n+\n+test('@formal-h5 direct OSS redirect back closes the host when no H5 route history exists', async ({ page }) => {\n+  await page.goto('/index.html?redirect=%2Fservice%2Fchat%2Fhuman', { waitUntil: 'domcontentloaded' })\n+  await expect(page).toHaveURL(/\\/service\\/chat\\/human$/)\n+  expect(await page.evaluate(() => (history.state as { idx?: number } | null)?.idx)).toBe(0)\n+\n+  await page.getByRole('button', { name: '返回' }).click()\n+\n+  // Preview deliberately has no Native bridge. The close branch must fail visibly rather than\n+  // navigating to Home or silently trying to pop a nonexistent H5 route.\n+  await expect(page.getByRole('alert')).toHaveText('当前环境未启用原生关闭能力')\n+  await expect(page).toHaveURL(/\\/service\\/chat\\/human$/)\n+  await expect(page.locator('[data-dearseed-avatar]')).toHaveCount(0)\n+})\n+\n+test('@formal-h5 H5 back uses the previous route before requesting host close', async ({ page }) => {\n+  await page.goto('/profile', { waitUntil: 'domcontentloaded' })\n+  await expectHealthyFormalRoute(page)\n+  await page.getByRole('button', { name: '通知' }).click()\n+  await expect(page).toHaveURL(/\\/notifications$/)\n+  await expectHealthyFormalRoute(page)\n+  expect(await page.evaluate(() => (history.state as { idx?: number } | null)?.idx ?? 0)).toBeGreaterThan(0)\n+\n+  await page.getByRole('button', { name: '返回' }).click()\n+\n+  await expect(page).toHaveURL(/\\/profile$/)\n+  await expectHealthyFormalRoute(page)\n+  await expect(page.getByRole('alert')).toHaveCount(0)\n })\n \n test('@formal-h5 explicit OSS index without redirect remains on its entry document', async ({ page }) => {",
      "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/fa62e281efb6290a537c2c7b12fb530d28cc8ca7",
            "commitHash": "fa62e281efb6290a537c2c7b12fb530d28cc8ca7",
            "prHref": "https://github.com/sanchuang-dev/dr-card-web/pull/276",
            "prTitle": "release(test): #275 客服直达页无 H5 历史时关闭 WebView",
            "prBaseHash": "93b40eb8cd9c57551b2e4922e8c481f74b3f3cc0",
            "buildHref": "https://github.com/sanchuang-dev/dr-card-web/actions/runs/41557905585"
          },
          "gitCommit": {
            "shortHash": "fa62e28",
            "hash": "fa62e281efb6290a537c2c7b12fb530d28cc8ca7",
            "subject": "Merge 832177ea785a59ae183f8608c63784c7298a30c3 into 93b40eb8cd9c57551b2e4922e8c481f74b3f3cc0",
            "body": "Merge 832177ea785a59ae183f8608c63784c7298a30c3 into 93b40eb8cd9c57551b2e4922e8c481f74b3f3cc0\n",
            "author": {
              "name": "Tomz Dang",
              "email": "dangjingtao@163.com",
              "time": 1793668198000
            },
            "committer": {
              "name": "GitHub",
              "email": "noreply@github.com",
              "time": 1793668198000
            },
            "branch": "HEAD"
          },
          "gitDiff": "diff --git a/docs/engineering/native-oss-redirect-entry.md b/docs/engineering/native-oss-redirect-entry.md\nindex 929a9ff..11f2aad 100644\n--- a/docs/engineering/native-oss-redirect-entry.md\n+++ b/docs/engineering/native-oss-redirect-entry.md\n@@ -17,3 +17,10 @@\n - `test/prod` 的 Native WebView 宿主门禁在 Router 启动之前执行，不会被参数绕过。\n - Router 跳转后地址为 Browser History 真实业务路径，页面刷新、进程恢复重新请求深层路径时，仍要求 CDN/网关按环境配置 SPA rewrite（见 #80/#171）；原生每次新开 WebView 应使用静态入口 URL。\n - 入口跳转只保证 H5 路由可达，不代表人工客服的真实 Socket/API 已完成业务验收。\n+\n+## 返回语义\n+\n+- 从一个 H5 页面进入另一个 H5 页面时，React Router 管理的 `history.state.idx` 大于零，标题栏返回执行 `navigate(-1)`，回到上一条 H5 路由。\n+- 从 Native 静态入口直接通过 `index.html?redirect=...` 进入目标页时，初始 loader 使用 replace，不产生上一条 H5 路由（`idx === 0`）；标题栏返回调用正式 `closeWebView()` capability，关闭当前 WebView 容器。\n+- 不使用 `window.history.length` 判断 H5 历史，因为它统计浏览器文档历史，不等同 React Router 管理的路由历史。\n+- `closeWebView` 的 H5 adapter / fail-closed 行为由 H030 定义。Native App 只有实际注入对应平台方法后才能关闭；方法缺失或 Bridge 不可用时，H5 明确提示，不静默无响应。\ndiff --git a/src/app/router/history.test.ts b/src/app/router/history.test.ts\nnew file mode 100644\nindex 0000000..cd90df7\n--- /dev/null\n+++ b/src/app/router/history.test.ts\n@@ -0,0 +1,17 @@\n+import { describe, expect, it } from 'vitest'\n+import { hasPreviousRouterEntry } from './history'\n+\n+describe('React Router history boundary', () => {\n+  it.each([\n+    ['missing state', null, false],\n+    ['missing index', {}, false],\n+    ['initial document entry', { idx: 0 }, false],\n+    ['previous H5 route exists', { idx: 1 }, true],\n+    ['multiple H5 routes exist', { idx: 3 }, true],\n+    ['negative index', { idx: -1 }, false],\n+    ['fractional index', { idx: 1.5 }, false],\n+    ['string index', { idx: '1' }, false],\n+  ])('classifies %s', (_label, state, expected) => {\n+    expect(hasPreviousRouterEntry(state)).toBe(expected)\n+  })\n+})\ndiff --git a/src/app/router/history.ts b/src/app/router/history.ts\nnew file mode 100644\nindex 0000000..59799ba\n--- /dev/null\n+++ b/src/app/router/history.ts\n@@ -0,0 +1,10 @@\n+/**\n+ * React Router 6 BrowserHistory stores its managed entry index in history.state.idx.\n+ * This is intentionally not window.history.length: that counts document entries and cannot\n+ * answer whether the current H5 document has a previous SPA route.\n+ */\n+export function hasPreviousRouterEntry(historyState: unknown): boolean {\n+  if (typeof historyState !== 'object' || historyState === null) return false\n+  const index = (historyState as { idx?: unknown }).idx\n+  return typeof index === 'number' && Number.isSafeInteger(index) && index > 0\n+}\ndiff --git a/src/layouts/MobileLayout.tsx b/src/layouts/MobileLayout.tsx\nindex 676bb06..0b048ff 100644\n--- a/src/layouts/MobileLayout.tsx\n+++ b/src/layouts/MobileLayout.tsx\n@@ -19,7 +19,8 @@ import { protectedFixtureRedirect, useOverlay } from '../app/fixtures/useFixture\n import H5ScrollRestoration from '../components/mobile/H5ScrollRestoration'\n import MallWebView from '../pages/MallWebView'\n import Toast from '../components/ui/Toast'\n-import { closeWebView } from '../services/nativeBridge'\n+import { closeWebView, NativeBridgeError } from '../services/nativeBridge'\n+import { hasPreviousRouterEntry } from '../app/router/history'\n import { AUTH_EXPIRED_EVENT, AUTH_FAILURE_EVENT, setAuthFlowEnabled } from '../services/auth/session'\n \n type H5RouteTransitionKind = 'none' | 'tab' | 'forward' | 'back'\n@@ -121,6 +122,7 @@ export default function MobileLayout() {\n   const { unreadCount, countLoaded } = useNotifications()\n   const { open: openOverlay } = useOverlay()\n   const [authExpired, setAuthExpired] = useState(false)\n+  const [backFeedback, setBackFeedback] = useState<string | null>(null)\n   const authExpiredHandledRef = useRef(false)\n   const showLegacyNav = isLegacyTabPath(location.pathname)\n   const showNav = showLegacyNav || isFormalH5TabPath(location.pathname)\n@@ -153,6 +155,12 @@ export default function MobileLayout() {\n     }\n   }, [activeFormalH5, navigate])\n \n+  useEffect(() => {\n+    if (!backFeedback) return\n+    const timeoutId = window.setTimeout(() => setBackFeedback(null), 2600)\n+    return () => window.clearTimeout(timeoutId)\n+  }, [backFeedback])\n+\n   const fixtureRedirect = activeFormalH5 ? protectedFixtureRedirect(location) : null\n   const titleBarMode = route?.titleBar ?? 'back'\n   const fallbackTitle = location.pathname === '/tokens' ? '品牌 Token 展示' : '页面不存在'\n@@ -196,6 +204,30 @@ export default function MobileLayout() {\n     navigate(target)\n   }\n \n+  const handleActiveFormalH5Back = () => {\n+    // React Router's BrowserHistory `idx` counts H5-managed entries. The initial OSS loader\n+    // replace leaves it at zero; only a real previous H5 route should receive navigate(-1).\n+    if (hasPreviousRouterEntry(window.history.state)) {\n+      navigate(-1)\n+      return\n+    }\n+\n+    void closeWebView().catch((error: unknown) => {\n+      console.error('[h5-shell-back]', error)\n+      if (error instanceof NativeBridgeError) {\n+        if (error.code === 'bridge-disabled') {\n+          setBackFeedback('当前环境未启用原生关闭能力')\n+        } else if (error.code === 'bridge-unsupported' || error.code === 'capability-unsupported') {\n+          setBackFeedback('当前 App 版本暂不支持关闭页面')\n+        } else {\n+          setBackFeedback('关闭页面失败，请稍后重试')\n+        }\n+        return\n+      }\n+      setBackFeedback('关闭页面失败，请稍后重试')\n+    })\n+  }\n+\n   const openMarkAllRead = () => {\n     openOverlay('clear')\n   }\n@@ -247,6 +279,11 @@ export default function MobileLayout() {\n           <Toast message=\"用户信息过期\" />\n         </div>\n       )}\n+      {backFeedback && (\n+        <div className=\"pointer-events-none fixed inset-x-0 bottom-24 z-[101] flex justify-center px-6\">\n+          <Toast message={backFeedback} status=\"danger\" />\n+        </div>\n+      )}\n       <div className=\"app-background flex h-screen supports-[height:100dvh]:h-dvh flex-col overflow-hidden pt-[env(safe-area-inset-top)] text-text-primary\">\n         {/* T013R7：min-w-0 防止 TitleBar 第三列 action 被撑大撑出页面右侧 */}\n         <div className=\"min-w-0 shrink-0\">\n@@ -254,7 +291,13 @@ export default function MobileLayout() {\n             <TitleBar\n               title={title}\n               leadingAction={leadingAction}\n-              onBack={route?.backTo ? () => navigateShell(route.backTo as string) : undefined}\n+              onBack={\n+                route?.backTo\n+                  ? () => navigateShell(route.backTo as string)\n+                  : activeFormalH5\n+                    ? handleActiveFormalH5Back\n+                    : undefined\n+              }\n               action={titleAction}\n               actionWide={isNotificationsPage}\n               fullWidth={activeFormalH5}\ndiff --git a/tests/e2e/formal-h5.spec.ts b/tests/e2e/formal-h5.spec.ts\nindex 101a784..a17ef8a 100644\n--- a/tests/e2e/formal-h5.spec.ts\n+++ b/tests/e2e/formal-h5.spec.ts\n@@ -470,6 +470,37 @@ test('@formal-h5 OSS index redirect enters the target without mounting Home', as\n   expect(await page.evaluate(() => history.length)).toBe(\n     await page.evaluate(() => (window as Window & { __entryHistoryLength?: number }).__entryHistoryLength),\n   )\n+  expect(await page.evaluate(() => (history.state as { idx?: number } | null)?.idx)).toBe(0)\n+})\n+\n+\n+test('@formal-h5 direct OSS redirect back closes the host when no H5 route history exists', async ({ page }) => {\n+  await page.goto('/index.html?redirect=%2Fservice%2Fchat%2Fhuman', { waitUntil: 'domcontentloaded' })\n+  await expect(page).toHaveURL(/\\/service\\/chat\\/human$/)\n+  expect(await page.evaluate(() => (history.state as { idx?: number } | null)?.idx)).toBe(0)\n+\n+  await page.getByRole('button', { name: '返回' }).click()\n+\n+  // Preview deliberately has no Native bridge. The close branch must fail visibly rather than\n+  // navigating to Home or silently trying to pop a nonexistent H5 route.\n+  await expect(page.getByRole('alert')).toHaveText('当前环境未启用原生关闭能力')\n+  await expect(page).toHaveURL(/\\/service\\/chat\\/human$/)\n+  await expect(page.locator('[data-dearseed-avatar]')).toHaveCount(0)\n+})\n+\n+test('@formal-h5 H5 back uses the previous route before requesting host close', async ({ page }) => {\n+  await page.goto('/profile', { waitUntil: 'domcontentloaded' })\n+  await expectHealthyFormalRoute(page)\n+  await page.getByRole('button', { name: '通知' }).click()\n+  await expect(page).toHaveURL(/\\/notifications$/)\n+  await expectHealthyFormalRoute(page)\n+  expect(await page.evaluate(() => (history.state as { idx?: number } | null)?.idx ?? 0)).toBeGreaterThan(0)\n+\n+  await page.getByRole('button', { name: '返回' }).click()\n+\n+  await expect(page).toHaveURL(/\\/profile$/)\n+  await expectHealthyFormalRoute(page)\n+  await expect(page.getByRole('alert')).toHaveCount(0)\n })\n \n test('@formal-h5 explicit OSS index without redirect remains on its entry document', async ({ page }) => {",
          "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": 59,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-03T01:11:19.315Z",
                  "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": 1000,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-03T01:11:19.319Z",
                  "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": 1023,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-03T01:11:19.413Z",
                  "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": 933,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-03T01:11:22.753Z",
                  "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": 924,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-03T01:11:22.774Z",
                  "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": 930,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-03T01:11:23.698Z",
                  "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": 988,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-03T01:11:23.712Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-8caf237c4d42513fff2f",
          "file": "test-gate.spec.ts",
          "line": 80,
          "column": 1
        }
      ]
    }
  ],
  "errors": [],
  "stats": {
    "startTime": "2026-11-03T01:11:18.426Z",
    "duration": 6324.239,
    "expected": 7,
    "skipped": 0,
    "unexpected": 0,
    "flaky": 0
  }
}