{
  "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/0933dd4e11dcaba14cdaa661ec4981440ad028a3",
        "commitHash": "0933dd4e11dcaba14cdaa661ec4981440ad028a3",
        "prHref": "https://github.com/sanchuang-dev/dr-card-web/pull/268",
        "prTitle": "feat: show backend new-user bubble points popup",
        "prBaseHash": "7256662658203b130b9623e13368d6e4eac7eecd",
        "buildHref": "https://github.com/sanchuang-dev/dr-card-web/actions/runs/40269982758"
      },
      "gitCommit": {
        "shortHash": "0933dd4",
        "hash": "0933dd4e11dcaba14cdaa661ec4981440ad028a3",
        "subject": "Merge 004d232a1dfc5ba842adeefbdba7170c8a044df2 into 7256662658203b130b9623e13368d6e4eac7eecd",
        "body": "Merge 004d232a1dfc5ba842adeefbdba7170c8a044df2 into 7256662658203b130b9623e13368d6e4eac7eecd\n",
        "author": {
          "name": "SINGING-AKIBA",
          "email": "980325684@qq.com",
          "time": 1792989414000
        },
        "committer": {
          "name": "GitHub",
          "email": "noreply@github.com",
          "time": 1792989414000
        },
        "branch": "HEAD"
      },
      "gitDiff": "diff --git a/src/App.tsx b/src/App.tsx\nindex 7f72dab..277ef97 100644\n--- a/src/App.tsx\n+++ b/src/App.tsx\n@@ -1,6 +1,66 @@\n+import { useEffect, useRef, useState } from 'react'\n import { RouterProvider } from 'react-router-dom'\n import { router } from './app/router'\n+import NewcomerDialog from './components/mobile/NewcomerDialog'\n+import { getAuthSession, type NewUserPopup } from './services/auth/session'\n+import { STORAGE_KEYS, storage } from './storage'\n+\n+function getUserPopupSeenId(userInfo: unknown): string | undefined {\n+  if (!userInfo || typeof userInfo !== 'object') return undefined\n+  const user = userInfo as Record<string, unknown>\n+  const id = user.id ?? user.user_id ?? user.uid\n+  return (typeof id === 'string' || typeof id === 'number') && String(id).trim()\n+    ? String(id)\n+    : undefined\n+}\n \n export default function App() {\n-  return <RouterProvider router={router} />\n+  const [newUserPopup, setNewUserPopup] = useState<NewUserPopup | null>(null)\n+  const pendingSeenUserId = useRef<string | undefined>(undefined)\n+\n+  useEffect(() => {\n+    let popupHandledThisDocument = false\n+    const handleSessionChanged = () => {\n+      const session = getAuthSession()\n+      if (session?.is_new_user !== true || !session.popup) {\n+        setNewUserPopup(null)\n+        pendingSeenUserId.current = undefined\n+        return\n+      }\n+\n+      const seenUserId = getUserPopupSeenId(session.userInfo)\n+      if (seenUserId && storage.read(STORAGE_KEYS.newUserPopupSeen)?.[seenUserId]) {\n+        setNewUserPopup(null)\n+        return\n+      }\n+      if (popupHandledThisDocument) return\n+      popupHandledThisDocument = true\n+      pendingSeenUserId.current = seenUserId\n+      setNewUserPopup(session.popup)\n+    }\n+\n+    window.addEventListener('dr-card-ui:auth-session-changed', handleSessionChanged)\n+    // Handle a login that completed just before this listener was mounted.\n+    handleSessionChanged()\n+    return () => window.removeEventListener('dr-card-ui:auth-session-changed', handleSessionChanged)\n+  }, [])\n+\n+  const dismissPopup = () => {\n+    const seenUserId = pendingSeenUserId.current\n+    if (seenUserId) {\n+      const seenUsers = storage.read(STORAGE_KEYS.newUserPopupSeen) ?? {}\n+      storage.write(STORAGE_KEYS.newUserPopupSeen, { ...seenUsers, [seenUserId]: true })\n+    }\n+    pendingSeenUserId.current = undefined\n+    setNewUserPopup(null)\n+  }\n+\n+  return (\n+    <>\n+      <RouterProvider router={router} />\n+      <NewcomerDialog open={newUserPopup !== null} title={newUserPopup?.title ?? ''}\n+        description={newUserPopup?.description ?? ''} points={newUserPopup?.points ?? ''}\n+        onComplete={dismissPopup} onDismiss={dismissPopup} />\n+    </>\n+  )\n }\ndiff --git a/src/components/mobile/NewcomerDialog.tsx b/src/components/mobile/NewcomerDialog.tsx\nindex aa25358..7a3b06a 100644\n--- a/src/components/mobile/NewcomerDialog.tsx\n+++ b/src/components/mobile/NewcomerDialog.tsx\n@@ -1,60 +1,30 @@\n import PromptOverlay from './PromptOverlay'\n-import { NEWCOMER_FIXTURE } from '../../app/fixtures'\n-import promptBottle from '../../assets/brand/dialog/dialog-prompt-bottle.webp'\n import { Button } from '../ui'\n \n export interface NewcomerDialogProps {\n   open: boolean\n-  /** 主操作「去完善信息」 */\n+  title: string\n+  description: string\n+  points: number | string\n   onComplete: () => void\n-  /** 摹客 click：点击卡片本体继续查看 APP 下载引导（#13） */\n-  onBody: () => void\n-  /** 点遮罩关闭 */\n   onDismiss: () => void\n }\n \n-/**\n- * 新人弹窗（#12）\n- * -------------------------------------------------------------\n- * 保留摹客「新人弹窗」的标题、左图右文和点击卡片进入 APP 引导的动线，\n- * 视觉改为 Com Design Token：暖色 surface、reward 强调、primary 主按钮、\n- * overlay 圆角/阴影及标准 48px 触控目标。\n- */\n-export default function NewcomerDialog({ open, onComplete, onBody, onDismiss }: NewcomerDialogProps) {\n+/** Displays the new-user bubble-points reward returned by the login API. */\n+export default function NewcomerDialog({ open, title, description, points, onComplete, onDismiss }: NewcomerDialogProps) {\n   return (\n-    <PromptOverlay open={open} label=\"新人弹窗\" onDismiss={onDismiss}>\n-      <button\n-        type=\"button\"\n-        onClick={onBody}\n-        className=\"group block w-full bg-[var(--color-surface-overlay-warm)] px-5 pb-5 pt-6 text-left outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-border-focused\"\n-      >\n-        <span className=\"mx-auto block w-fit rounded-pill bg-reward-subtle px-3 py-1 text-[11px] font-semibold tracking-[0.12em] text-reward-text\">\n-          DEAR SEED\n-        </span>\n-        <h2 className=\"mt-3 text-center text-[22px] font-bold leading-7 text-text-primary\">\n-          {NEWCOMER_FIXTURE.title}\n-        </h2>\n-        <div className=\"mx-auto mt-3 h-px w-12 bg-reward\" aria-hidden />\n-\n-        <div className=\"mt-4 flex items-center gap-4\">\n-          <span className=\"flex h-[116px] w-[92px] flex-none items-center justify-center rounded-container border border-border-subtle bg-surface shadow-floating\">\n-            <img\n-              src={promptBottle}\n-              alt=\"\"\n-              aria-hidden\n-              className=\"h-[106px] w-[78px] object-contain transition-transform group-active:scale-[.98]\"\n-            />\n-          </span>\n-          <p className=\"flex-1 text-left text-sm leading-6 text-text-secondary\">\n-            {NEWCOMER_FIXTURE.desc}\n-          </p>\n+    <PromptOverlay open={open} label=\"新人泡泡值赠送提示\" onDismiss={onDismiss}>\n+      <div className=\"bg-[var(--color-surface-overlay-warm)] px-5 pb-5 pt-6 text-center\">\n+        <span className=\"mx-auto inline-flex rounded-pill bg-reward-subtle px-3 py-1 text-[11px] font-semibold tracking-[0.12em] text-reward-text\">新人福利</span>\n+        <h2 className=\"mt-3 text-xl font-bold leading-7 text-text-primary\">{title}</h2>\n+        <p className=\"mt-3 text-sm leading-6 text-text-secondary\">{description}</p>\n+        <div className=\"mx-auto mt-5 flex min-h-24 w-full flex-col items-center justify-center rounded-container border border-reward/30 bg-reward-subtle px-4 py-3\">\n+          <span className=\"text-xs font-medium text-reward-text\">赠送泡泡值</span>\n+          <span className=\"mt-1 text-3xl font-extrabold tabular-nums text-reward-text\">{points}</span>\n         </div>\n-      </button>\n-\n+      </div>\n       <div className=\"bg-surface px-5 pb-5 pt-4\">\n-        <Button size=\"large\" className=\"w-full rounded-pill\" onClick={onComplete}>\n-          {NEWCOMER_FIXTURE.cta}\n-        </Button>\n+        <Button size=\"large\" className=\"w-full rounded-pill\" onClick={onComplete}>我知道了</Button>\n       </div>\n     </PromptOverlay>\n   )\ndiff --git a/src/pages/DearseedColumn.tsx b/src/pages/DearseedColumn.tsx\nindex a4cdfd0..f5cc644 100644\n--- a/src/pages/DearseedColumn.tsx\n+++ b/src/pages/DearseedColumn.tsx\n@@ -1,15 +1,11 @@\n-import { useEffect, useState } from 'react'\n+import { useState } from 'react'\n import { useNavigate } from 'react-router-dom'\n import { ArrowUpRight, ChevronRight, Crown, Droplets, ShoppingBag, Sparkles, X } from 'lucide-react'\n import AppPromptDialog from '../components/mobile/AppPromptDialog'\n import DebugPanel from '../components/mobile/DebugPanel'\n import FixtureOverlay from '../components/mobile/FixtureOverlay'\n-import NewcomerDialog from '../components/mobile/NewcomerDialog'\n import PageContainer from '../components/mobile/PageContainer'\n import PromptOverlay from '../components/mobile/PromptOverlay'\n-import NewcomerCouponDialog from '../components/mobile/NewcomerCouponDialog'\n-import IdentityPickerSheet, { type PickerIdentity } from '../components/coupon/IdentityPickerSheet'\n-import NewcomerGiftSheet from '../components/coupon/NewcomerGiftSheet'\n import { Button, ProgressIndicator } from '../components/ui'\n import { findRouteByPathname } from '../app/router/routes'\n import { openNativeAppStore } from '../app/adapters/appOpen'\n@@ -21,11 +17,7 @@ import {\n   CHECKIN_REMINDER,\n   DEARSEED_BANNER_TEXT,\n   DEARSEED_PICKS,\n-  GIFT_FOR_NEW_USERS,\n   MEMBER_PROFILE,\n-  NEWCOMER_COUPON_SUCCESS,\n-  NEWCOMER_FIXTURE,\n-  NEWCOMER_COUPON_VARIANTS,\n } from '../app/fixtures'\n import columnBanner from '../assets/brand/home/home-banner-carousel.webp'\n import UserAvatar from '../components/mobile/UserAvatar'\n@@ -59,39 +51,9 @@ export default function DearseedColumn() {\n   const { overlay, open, close } = useOverlay()\n   const [downloadHint, setDownloadHint] = useState<string | undefined>(undefined)\n   const [downloadPending, setDownloadPending] = useState(false)\n-  /* T043｜专栏入口三级弹窗状态。\n-   * 流程：进入 /dearseed 页面 → pickerOpen 弹身份选择 → 选择身份\n-   *   - 'new'（诗得丽新增用户）→ couponOpen 弹 NewcomerCouponDialog（洗发水体验券）\n-   *   - 'existing'（卡博士存量用户）→ giftOpen 弹 NewcomerGiftSheet（新人礼包演示态）\n-   * Demo 演示用，关闭选择器不需要后端身份识别（B-043）。\n-   * 触发时机：useEffect 挂载时弹一次；本会话内不重复弹（sessionStorage 标记）。\n-   * URL `?picker=off` 抑制本次触发（演示态可关闭）。\n-   */\n-  const [pickerOpen, setPickerOpen] = useState(false)\n-  const [couponOpen, setCouponOpen] = useState(false)\n-  const [couponSuccessOpen, setCouponSuccessOpen] = useState(false)\n-  const [giftOpen, setGiftOpen] = useState(false)\n-  /* 关爱机用户弹窗用 NEWCOMER_COUPON_VARIANTS，本期 mock 固定 coupon-1（1 张洗发水体验券） */\n-  const dearseedCoupons = NEWCOMER_COUPON_VARIANTS['coupon-1']\n-\n-  /* T043R2｜进入专栏页面自动弹身份选择器。\n-   * - 用户 2026-09-10 现场反馈：从卡博士 APP 首页「诗得丽品牌专栏」卡片跳转进专栏后必须弹出\n-   * - 取消原 T043 实现的 sessionStorage 会话级抑制：每次 mount 都弹\n-   * - URL 带 `?picker=off` 时跳过（演示态可关闭、自动化脚本可抑制）\n-   * - 已带其他 overlay 的入口（如 ?overlay=reminder）也跳过，避免覆盖其他演示态\n-   * - 路由切换时 pickerOpen 状态自然清空，组件重新 mount 会再次触发（符合\"每次跳转进入都弹\"语义）\n-   */\n-  useEffect(() => {\n-    if (typeof window === 'undefined') return\n-    const params = new URLSearchParams(window.location.search)\n-    if (params.get('picker') === 'off') return\n-    if (params.has('overlay')) return\n-    setPickerOpen(true)\n-  }, [])\n-\n   const campaign = CAMPAIGN_FIXTURE\n   const claimed = state?.key === 'claimed'\n-  const ownedOverlay = overlay === 'reminder' || overlay === 'newcomer' || overlay === 'app-guide'\n+  const ownedOverlay = overlay === 'reminder' || overlay === 'app-guide'\n \n   const closeAppGuide = () => {\n     setDownloadHint(undefined)\n@@ -117,48 +79,12 @@ export default function DearseedColumn() {\n     }\n   }\n \n-  /* T043｜身份选择回调：关闭选择器，按身份打开对应二级弹窗 */\n-  const handleIdentityPick = (identity: PickerIdentity) => {\n-    setPickerOpen(false)\n-    if (identity === 'new') {\n-      setCouponOpen(true)\n-    } else {\n-      setGiftOpen(true)\n-    }\n-  }\n-\n-  /* T043｜体验券（新增用户分支）确认 → 出领取成功反馈 */\n-  const handleCouponConfirm = () => {\n-    setCouponOpen(false)\n-    setCouponSuccessOpen(true)\n-  }\n-\n-  /* T043｜新人礼包（存量用户分支）确认 → 跳 GIFT_FOR_NEW_USERS.actionTo */\n-  const handleGiftConfirm = () => {\n-    setGiftOpen(false)\n-    navigate(GIFT_FOR_NEW_USERS.actionTo)\n-  }\n-\n-  /* T043｜任意弹窗关闭：清空对应状态 */\n-  const handlePickerDismiss = () => setPickerOpen(false)\n-  const handleCouponDismiss = () => {\n-    setCouponOpen(false)\n-    setCouponSuccessOpen(false)\n-  }\n-  const handleGiftDismiss = () => setGiftOpen(false)\n-\n-  /* T043｜领取成功反馈点「查看体验券」 → 跳 NEWCOMER_COUPON_SUCCESS.actionTo */\n-  const handleCouponSuccessAction = () => {\n-    setCouponSuccessOpen(false)\n-    navigate(NEWCOMER_COUPON_SUCCESS.actionTo)\n-  }\n-\n   return (\n     <PageContainer inset={false} className=\"overflow-hidden pb-[calc(2rem+env(safe-area-inset-bottom))]\">\n       <button\n         type=\"button\"\n         aria-label=\"诗得丽产品与活动推荐\"\n-        onClick={() => open('newcomer')}\n+        onClick={() => open('app-guide')}\n         className=\"relative block w-full overflow-hidden text-left\"\n       >\n         <img src={columnBanner} alt=\"诗得丽产品与活动推荐\" className=\"aspect-[375/210] w-full object-cover\" />\n@@ -328,7 +254,6 @@ export default function DearseedColumn() {\n         <Button size=\"large\" className=\"mt-5 w-full rounded-full\" onClick={() => navigate('/checkin?state=success')}>{CHECKIN_REMINDER.action}</Button>\n       </PromptOverlay>\n \n-      <NewcomerDialog open={overlay === 'newcomer'} onComplete={() => navigate(NEWCOMER_FIXTURE.ctaTo)} onBody={() => open(NEWCOMER_FIXTURE.bodyToOverlay)} onDismiss={close} />\n       <AppPromptDialog\n         open={overlay === 'app-guide'}\n         variant=\"guide\"\n@@ -339,27 +264,6 @@ export default function DearseedColumn() {\n         downloadPending={downloadPending}\n       />\n \n-      {/* T043｜专栏入口三级弹窗流程：\n-       *  1. IdentityPickerSheet（身份选择，先弹）\n-       *  2a. NewcomerCouponDialog（选「诗得丽新增用户」→ 洗发水体验券，沿用 Home 弹窗）\n-       *  2b. NewcomerGiftSheet（选「卡博士存量用户」→ 新人礼包演示态，新增）\n-       *  3. NewcomerCouponDialog 自带的领取成功反馈（仅 2a 路径会触发）\n-       */}\n-      <IdentityPickerSheet\n-        open={pickerOpen}\n-        onPick={handleIdentityPick}\n-        onDismiss={handlePickerDismiss}\n-      />\n-      <NewcomerCouponDialog\n-        open={couponOpen}\n-        successOpen={couponSuccessOpen}\n-        coupons={dearseedCoupons}\n-        onConfirm={handleCouponConfirm}\n-        onDismiss={handleCouponDismiss}\n-        onSuccessAction={handleCouponSuccessAction}\n-      />\n-      <NewcomerGiftSheet open={giftOpen} onConfirm={handleGiftConfirm} onDismiss={handleGiftDismiss} />\n-\n       <DebugPanel route={route} />\n     </PageContainer>\n   )\ndiff --git a/src/services/auth/session.test.ts b/src/services/auth/session.test.ts\nindex aafb7f6..7add2db 100644\n--- a/src/services/auth/session.test.ts\n+++ b/src/services/auth/session.test.ts\n@@ -11,23 +11,29 @@ describe('login response contract', () => {\n   })\n \n   it('unwraps a successful code/data response from the backend', () => {\n-    expect(\n-      parseLoginResponse({\n-        code: 0,\n-        data: { userInfo: { id: 'user-1' }, accessToken: 'token-1', is_new_user: false },\n-      }),\n-    ).toEqual({\n-      userInfo: { id: 'user-1' },\n-      accessToken: 'token-1',\n+    expect(parseLoginResponse({\n+      code: 0,\n+      data: { userInfo: { id: 'user-1' }, accessToken: 'token-1', is_new_user: false },\n+    })).toEqual({ userInfo: { id: 'user-1' }, accessToken: 'token-1', is_new_user: false })\n+  })\n+\n+  it('preserves new-user popup content from the backend envelope', () => {\n+    expect(parseLoginResponse({ code: 0, data: {\n+      userInfo: { id: 'user-2' }, accessToken: 'token-2', is_new_user: true,\n+      popup: { title: '注册成功', description: '已获得泡泡值奖励', points: 120 },\n+    } })).toEqual({ userInfo: { id: 'user-2' }, accessToken: 'token-2', is_new_user: true,\n+      popup: { title: '注册成功', description: '已获得泡泡值奖励', points: 120 } })\n+  })\n+\n+  it('ignores malformed popup content and preserves ordinary login', () => {\n+    expect(parseLoginResponse({ userInfo: {}, accessToken: 'token-3', is_new_user: true,\n+      popup: { title: 'Hi', description: '', points: 5 } })).toEqual({\n+      userInfo: {}, accessToken: 'token-3', is_new_user: true,\n     })\n   })\n \n   it('does not treat a non-success envelope as a valid session', () => {\n-    expect(() =>\n-      parseLoginResponse({\n-        code: 500,\n-        data: { message: 'backend failure' },\n-      }),\n-    ).toThrowError(expect.objectContaining({ code: 'AUTH_LOGIN_RESPONSE_INVALID' }))\n+    expect(() => parseLoginResponse({ code: 500, data: { message: 'backend failure' } }))\n+      .toThrowError(expect.objectContaining({ code: 'AUTH_LOGIN_RESPONSE_INVALID' }))\n   })\n })\ndiff --git a/src/services/auth/session.ts b/src/services/auth/session.ts\nindex eacd033..5e2f9c2 100644\n--- a/src/services/auth/session.ts\n+++ b/src/services/auth/session.ts\n@@ -15,16 +15,21 @@ const AUTH_FAILURE_EVENT = 'dr-card-ui:auth-failure'\n const AUTH_EXPIRED_EVENT = 'dr-card-ui:auth-expired'\n const LOGIN_PATH = '/api/oauth/login'\n \n-export interface AuthSession {\n-  userInfo: unknown\n-  accessToken: string\n+export interface NewUserPopup {\n+  title: string\n+  description: string\n+  points: number | string\n }\n \n-interface LoginResponse {\n+export interface AuthSession {\n   userInfo: unknown\n   accessToken: string\n+  is_new_user?: boolean\n+  popup?: NewUserPopup\n }\n \n+interface LoginResponse extends AuthSession {}\n+\n interface LoginEnvelope {\n   code?: unknown\n   data?: unknown\n@@ -46,7 +51,8 @@ let signatureCredentials: ApiSignatureCredentials | undefined\n function readSession(): AuthSession | undefined {\n   const value = storage.read(STORAGE_KEYS.authSession)\n   if (value && value.accessToken.trim()) {\n-    return { userInfo: volatileSession?.userInfo ?? null, accessToken: value.accessToken }\n+    if (volatileSession?.accessToken === value.accessToken) return volatileSession\n+    return { userInfo: null, accessToken: value.accessToken }\n   }\n   return volatileSession\n }\n@@ -93,7 +99,20 @@ export function parseLoginResponse(result: unknown): LoginResponse {\n     })\n   }\n \n-  return { userInfo: payload.userInfo, accessToken: payload.accessToken }\n+  const popupValue = payload.popup\n+  const popup = popupValue && typeof popupValue === 'object' ? popupValue as Partial<NewUserPopup> : undefined\n+  const validPopup = popup && typeof popup.title === 'string' && popup.title.trim()\n+    && typeof popup.description === 'string' && popup.description.trim()\n+    && (typeof popup.points === 'number' || typeof popup.points === 'string')\n+    ? { title: popup.title, description: popup.description, points: popup.points }\n+    : undefined\n+\n+  return {\n+    userInfo: payload.userInfo,\n+    accessToken: payload.accessToken,\n+    ...(typeof payload.is_new_user === 'boolean' ? { is_new_user: payload.is_new_user } : {}),\n+    ...(validPopup ? { popup: validPopup } : {}),\n+  }\n }\n \n async function loginFromNative(): Promise<AuthSession> {\ndiff --git a/src/storage/keys.ts b/src/storage/keys.ts\nindex 7086fe8..efe4ea4 100644\n--- a/src/storage/keys.ts\n+++ b/src/storage/keys.ts\n@@ -40,6 +40,11 @@ export const STORAGE_KEYS = {\n   ),\n   // User-confirmed UI preference: only retain a per-account six-dot mask flag\n   // after successful profile update. Never persist the PIN or its digest.\n+  newUserPopupSeen: defineStorageKey(\n+    'new-user-popup-seen',\n+    'local',\n+    z.record(z.string(), z.literal(true)),\n+  ),\n   profilePinMasks: defineStorageKey(\n     'profile-pin-masks',\n     'local',",
      "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/0933dd4e11dcaba14cdaa661ec4981440ad028a3",
            "commitHash": "0933dd4e11dcaba14cdaa661ec4981440ad028a3",
            "prHref": "https://github.com/sanchuang-dev/dr-card-web/pull/268",
            "prTitle": "feat: show backend new-user bubble points popup",
            "prBaseHash": "7256662658203b130b9623e13368d6e4eac7eecd",
            "buildHref": "https://github.com/sanchuang-dev/dr-card-web/actions/runs/40269982758"
          },
          "gitCommit": {
            "shortHash": "0933dd4",
            "hash": "0933dd4e11dcaba14cdaa661ec4981440ad028a3",
            "subject": "Merge 004d232a1dfc5ba842adeefbdba7170c8a044df2 into 7256662658203b130b9623e13368d6e4eac7eecd",
            "body": "Merge 004d232a1dfc5ba842adeefbdba7170c8a044df2 into 7256662658203b130b9623e13368d6e4eac7eecd\n",
            "author": {
              "name": "SINGING-AKIBA",
              "email": "980325684@qq.com",
              "time": 1792989414000
            },
            "committer": {
              "name": "GitHub",
              "email": "noreply@github.com",
              "time": 1792989414000
            },
            "branch": "HEAD"
          },
          "gitDiff": "diff --git a/src/App.tsx b/src/App.tsx\nindex 7f72dab..277ef97 100644\n--- a/src/App.tsx\n+++ b/src/App.tsx\n@@ -1,6 +1,66 @@\n+import { useEffect, useRef, useState } from 'react'\n import { RouterProvider } from 'react-router-dom'\n import { router } from './app/router'\n+import NewcomerDialog from './components/mobile/NewcomerDialog'\n+import { getAuthSession, type NewUserPopup } from './services/auth/session'\n+import { STORAGE_KEYS, storage } from './storage'\n+\n+function getUserPopupSeenId(userInfo: unknown): string | undefined {\n+  if (!userInfo || typeof userInfo !== 'object') return undefined\n+  const user = userInfo as Record<string, unknown>\n+  const id = user.id ?? user.user_id ?? user.uid\n+  return (typeof id === 'string' || typeof id === 'number') && String(id).trim()\n+    ? String(id)\n+    : undefined\n+}\n \n export default function App() {\n-  return <RouterProvider router={router} />\n+  const [newUserPopup, setNewUserPopup] = useState<NewUserPopup | null>(null)\n+  const pendingSeenUserId = useRef<string | undefined>(undefined)\n+\n+  useEffect(() => {\n+    let popupHandledThisDocument = false\n+    const handleSessionChanged = () => {\n+      const session = getAuthSession()\n+      if (session?.is_new_user !== true || !session.popup) {\n+        setNewUserPopup(null)\n+        pendingSeenUserId.current = undefined\n+        return\n+      }\n+\n+      const seenUserId = getUserPopupSeenId(session.userInfo)\n+      if (seenUserId && storage.read(STORAGE_KEYS.newUserPopupSeen)?.[seenUserId]) {\n+        setNewUserPopup(null)\n+        return\n+      }\n+      if (popupHandledThisDocument) return\n+      popupHandledThisDocument = true\n+      pendingSeenUserId.current = seenUserId\n+      setNewUserPopup(session.popup)\n+    }\n+\n+    window.addEventListener('dr-card-ui:auth-session-changed', handleSessionChanged)\n+    // Handle a login that completed just before this listener was mounted.\n+    handleSessionChanged()\n+    return () => window.removeEventListener('dr-card-ui:auth-session-changed', handleSessionChanged)\n+  }, [])\n+\n+  const dismissPopup = () => {\n+    const seenUserId = pendingSeenUserId.current\n+    if (seenUserId) {\n+      const seenUsers = storage.read(STORAGE_KEYS.newUserPopupSeen) ?? {}\n+      storage.write(STORAGE_KEYS.newUserPopupSeen, { ...seenUsers, [seenUserId]: true })\n+    }\n+    pendingSeenUserId.current = undefined\n+    setNewUserPopup(null)\n+  }\n+\n+  return (\n+    <>\n+      <RouterProvider router={router} />\n+      <NewcomerDialog open={newUserPopup !== null} title={newUserPopup?.title ?? ''}\n+        description={newUserPopup?.description ?? ''} points={newUserPopup?.points ?? ''}\n+        onComplete={dismissPopup} onDismiss={dismissPopup} />\n+    </>\n+  )\n }\ndiff --git a/src/components/mobile/NewcomerDialog.tsx b/src/components/mobile/NewcomerDialog.tsx\nindex aa25358..7a3b06a 100644\n--- a/src/components/mobile/NewcomerDialog.tsx\n+++ b/src/components/mobile/NewcomerDialog.tsx\n@@ -1,60 +1,30 @@\n import PromptOverlay from './PromptOverlay'\n-import { NEWCOMER_FIXTURE } from '../../app/fixtures'\n-import promptBottle from '../../assets/brand/dialog/dialog-prompt-bottle.webp'\n import { Button } from '../ui'\n \n export interface NewcomerDialogProps {\n   open: boolean\n-  /** 主操作「去完善信息」 */\n+  title: string\n+  description: string\n+  points: number | string\n   onComplete: () => void\n-  /** 摹客 click：点击卡片本体继续查看 APP 下载引导（#13） */\n-  onBody: () => void\n-  /** 点遮罩关闭 */\n   onDismiss: () => void\n }\n \n-/**\n- * 新人弹窗（#12）\n- * -------------------------------------------------------------\n- * 保留摹客「新人弹窗」的标题、左图右文和点击卡片进入 APP 引导的动线，\n- * 视觉改为 Com Design Token：暖色 surface、reward 强调、primary 主按钮、\n- * overlay 圆角/阴影及标准 48px 触控目标。\n- */\n-export default function NewcomerDialog({ open, onComplete, onBody, onDismiss }: NewcomerDialogProps) {\n+/** Displays the new-user bubble-points reward returned by the login API. */\n+export default function NewcomerDialog({ open, title, description, points, onComplete, onDismiss }: NewcomerDialogProps) {\n   return (\n-    <PromptOverlay open={open} label=\"新人弹窗\" onDismiss={onDismiss}>\n-      <button\n-        type=\"button\"\n-        onClick={onBody}\n-        className=\"group block w-full bg-[var(--color-surface-overlay-warm)] px-5 pb-5 pt-6 text-left outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-border-focused\"\n-      >\n-        <span className=\"mx-auto block w-fit rounded-pill bg-reward-subtle px-3 py-1 text-[11px] font-semibold tracking-[0.12em] text-reward-text\">\n-          DEAR SEED\n-        </span>\n-        <h2 className=\"mt-3 text-center text-[22px] font-bold leading-7 text-text-primary\">\n-          {NEWCOMER_FIXTURE.title}\n-        </h2>\n-        <div className=\"mx-auto mt-3 h-px w-12 bg-reward\" aria-hidden />\n-\n-        <div className=\"mt-4 flex items-center gap-4\">\n-          <span className=\"flex h-[116px] w-[92px] flex-none items-center justify-center rounded-container border border-border-subtle bg-surface shadow-floating\">\n-            <img\n-              src={promptBottle}\n-              alt=\"\"\n-              aria-hidden\n-              className=\"h-[106px] w-[78px] object-contain transition-transform group-active:scale-[.98]\"\n-            />\n-          </span>\n-          <p className=\"flex-1 text-left text-sm leading-6 text-text-secondary\">\n-            {NEWCOMER_FIXTURE.desc}\n-          </p>\n+    <PromptOverlay open={open} label=\"新人泡泡值赠送提示\" onDismiss={onDismiss}>\n+      <div className=\"bg-[var(--color-surface-overlay-warm)] px-5 pb-5 pt-6 text-center\">\n+        <span className=\"mx-auto inline-flex rounded-pill bg-reward-subtle px-3 py-1 text-[11px] font-semibold tracking-[0.12em] text-reward-text\">新人福利</span>\n+        <h2 className=\"mt-3 text-xl font-bold leading-7 text-text-primary\">{title}</h2>\n+        <p className=\"mt-3 text-sm leading-6 text-text-secondary\">{description}</p>\n+        <div className=\"mx-auto mt-5 flex min-h-24 w-full flex-col items-center justify-center rounded-container border border-reward/30 bg-reward-subtle px-4 py-3\">\n+          <span className=\"text-xs font-medium text-reward-text\">赠送泡泡值</span>\n+          <span className=\"mt-1 text-3xl font-extrabold tabular-nums text-reward-text\">{points}</span>\n         </div>\n-      </button>\n-\n+      </div>\n       <div className=\"bg-surface px-5 pb-5 pt-4\">\n-        <Button size=\"large\" className=\"w-full rounded-pill\" onClick={onComplete}>\n-          {NEWCOMER_FIXTURE.cta}\n-        </Button>\n+        <Button size=\"large\" className=\"w-full rounded-pill\" onClick={onComplete}>我知道了</Button>\n       </div>\n     </PromptOverlay>\n   )\ndiff --git a/src/pages/DearseedColumn.tsx b/src/pages/DearseedColumn.tsx\nindex a4cdfd0..f5cc644 100644\n--- a/src/pages/DearseedColumn.tsx\n+++ b/src/pages/DearseedColumn.tsx\n@@ -1,15 +1,11 @@\n-import { useEffect, useState } from 'react'\n+import { useState } from 'react'\n import { useNavigate } from 'react-router-dom'\n import { ArrowUpRight, ChevronRight, Crown, Droplets, ShoppingBag, Sparkles, X } from 'lucide-react'\n import AppPromptDialog from '../components/mobile/AppPromptDialog'\n import DebugPanel from '../components/mobile/DebugPanel'\n import FixtureOverlay from '../components/mobile/FixtureOverlay'\n-import NewcomerDialog from '../components/mobile/NewcomerDialog'\n import PageContainer from '../components/mobile/PageContainer'\n import PromptOverlay from '../components/mobile/PromptOverlay'\n-import NewcomerCouponDialog from '../components/mobile/NewcomerCouponDialog'\n-import IdentityPickerSheet, { type PickerIdentity } from '../components/coupon/IdentityPickerSheet'\n-import NewcomerGiftSheet from '../components/coupon/NewcomerGiftSheet'\n import { Button, ProgressIndicator } from '../components/ui'\n import { findRouteByPathname } from '../app/router/routes'\n import { openNativeAppStore } from '../app/adapters/appOpen'\n@@ -21,11 +17,7 @@ import {\n   CHECKIN_REMINDER,\n   DEARSEED_BANNER_TEXT,\n   DEARSEED_PICKS,\n-  GIFT_FOR_NEW_USERS,\n   MEMBER_PROFILE,\n-  NEWCOMER_COUPON_SUCCESS,\n-  NEWCOMER_FIXTURE,\n-  NEWCOMER_COUPON_VARIANTS,\n } from '../app/fixtures'\n import columnBanner from '../assets/brand/home/home-banner-carousel.webp'\n import UserAvatar from '../components/mobile/UserAvatar'\n@@ -59,39 +51,9 @@ export default function DearseedColumn() {\n   const { overlay, open, close } = useOverlay()\n   const [downloadHint, setDownloadHint] = useState<string | undefined>(undefined)\n   const [downloadPending, setDownloadPending] = useState(false)\n-  /* T043｜专栏入口三级弹窗状态。\n-   * 流程：进入 /dearseed 页面 → pickerOpen 弹身份选择 → 选择身份\n-   *   - 'new'（诗得丽新增用户）→ couponOpen 弹 NewcomerCouponDialog（洗发水体验券）\n-   *   - 'existing'（卡博士存量用户）→ giftOpen 弹 NewcomerGiftSheet（新人礼包演示态）\n-   * Demo 演示用，关闭选择器不需要后端身份识别（B-043）。\n-   * 触发时机：useEffect 挂载时弹一次；本会话内不重复弹（sessionStorage 标记）。\n-   * URL `?picker=off` 抑制本次触发（演示态可关闭）。\n-   */\n-  const [pickerOpen, setPickerOpen] = useState(false)\n-  const [couponOpen, setCouponOpen] = useState(false)\n-  const [couponSuccessOpen, setCouponSuccessOpen] = useState(false)\n-  const [giftOpen, setGiftOpen] = useState(false)\n-  /* 关爱机用户弹窗用 NEWCOMER_COUPON_VARIANTS，本期 mock 固定 coupon-1（1 张洗发水体验券） */\n-  const dearseedCoupons = NEWCOMER_COUPON_VARIANTS['coupon-1']\n-\n-  /* T043R2｜进入专栏页面自动弹身份选择器。\n-   * - 用户 2026-09-10 现场反馈：从卡博士 APP 首页「诗得丽品牌专栏」卡片跳转进专栏后必须弹出\n-   * - 取消原 T043 实现的 sessionStorage 会话级抑制：每次 mount 都弹\n-   * - URL 带 `?picker=off` 时跳过（演示态可关闭、自动化脚本可抑制）\n-   * - 已带其他 overlay 的入口（如 ?overlay=reminder）也跳过，避免覆盖其他演示态\n-   * - 路由切换时 pickerOpen 状态自然清空，组件重新 mount 会再次触发（符合\"每次跳转进入都弹\"语义）\n-   */\n-  useEffect(() => {\n-    if (typeof window === 'undefined') return\n-    const params = new URLSearchParams(window.location.search)\n-    if (params.get('picker') === 'off') return\n-    if (params.has('overlay')) return\n-    setPickerOpen(true)\n-  }, [])\n-\n   const campaign = CAMPAIGN_FIXTURE\n   const claimed = state?.key === 'claimed'\n-  const ownedOverlay = overlay === 'reminder' || overlay === 'newcomer' || overlay === 'app-guide'\n+  const ownedOverlay = overlay === 'reminder' || overlay === 'app-guide'\n \n   const closeAppGuide = () => {\n     setDownloadHint(undefined)\n@@ -117,48 +79,12 @@ export default function DearseedColumn() {\n     }\n   }\n \n-  /* T043｜身份选择回调：关闭选择器，按身份打开对应二级弹窗 */\n-  const handleIdentityPick = (identity: PickerIdentity) => {\n-    setPickerOpen(false)\n-    if (identity === 'new') {\n-      setCouponOpen(true)\n-    } else {\n-      setGiftOpen(true)\n-    }\n-  }\n-\n-  /* T043｜体验券（新增用户分支）确认 → 出领取成功反馈 */\n-  const handleCouponConfirm = () => {\n-    setCouponOpen(false)\n-    setCouponSuccessOpen(true)\n-  }\n-\n-  /* T043｜新人礼包（存量用户分支）确认 → 跳 GIFT_FOR_NEW_USERS.actionTo */\n-  const handleGiftConfirm = () => {\n-    setGiftOpen(false)\n-    navigate(GIFT_FOR_NEW_USERS.actionTo)\n-  }\n-\n-  /* T043｜任意弹窗关闭：清空对应状态 */\n-  const handlePickerDismiss = () => setPickerOpen(false)\n-  const handleCouponDismiss = () => {\n-    setCouponOpen(false)\n-    setCouponSuccessOpen(false)\n-  }\n-  const handleGiftDismiss = () => setGiftOpen(false)\n-\n-  /* T043｜领取成功反馈点「查看体验券」 → 跳 NEWCOMER_COUPON_SUCCESS.actionTo */\n-  const handleCouponSuccessAction = () => {\n-    setCouponSuccessOpen(false)\n-    navigate(NEWCOMER_COUPON_SUCCESS.actionTo)\n-  }\n-\n   return (\n     <PageContainer inset={false} className=\"overflow-hidden pb-[calc(2rem+env(safe-area-inset-bottom))]\">\n       <button\n         type=\"button\"\n         aria-label=\"诗得丽产品与活动推荐\"\n-        onClick={() => open('newcomer')}\n+        onClick={() => open('app-guide')}\n         className=\"relative block w-full overflow-hidden text-left\"\n       >\n         <img src={columnBanner} alt=\"诗得丽产品与活动推荐\" className=\"aspect-[375/210] w-full object-cover\" />\n@@ -328,7 +254,6 @@ export default function DearseedColumn() {\n         <Button size=\"large\" className=\"mt-5 w-full rounded-full\" onClick={() => navigate('/checkin?state=success')}>{CHECKIN_REMINDER.action}</Button>\n       </PromptOverlay>\n \n-      <NewcomerDialog open={overlay === 'newcomer'} onComplete={() => navigate(NEWCOMER_FIXTURE.ctaTo)} onBody={() => open(NEWCOMER_FIXTURE.bodyToOverlay)} onDismiss={close} />\n       <AppPromptDialog\n         open={overlay === 'app-guide'}\n         variant=\"guide\"\n@@ -339,27 +264,6 @@ export default function DearseedColumn() {\n         downloadPending={downloadPending}\n       />\n \n-      {/* T043｜专栏入口三级弹窗流程：\n-       *  1. IdentityPickerSheet（身份选择，先弹）\n-       *  2a. NewcomerCouponDialog（选「诗得丽新增用户」→ 洗发水体验券，沿用 Home 弹窗）\n-       *  2b. NewcomerGiftSheet（选「卡博士存量用户」→ 新人礼包演示态，新增）\n-       *  3. NewcomerCouponDialog 自带的领取成功反馈（仅 2a 路径会触发）\n-       */}\n-      <IdentityPickerSheet\n-        open={pickerOpen}\n-        onPick={handleIdentityPick}\n-        onDismiss={handlePickerDismiss}\n-      />\n-      <NewcomerCouponDialog\n-        open={couponOpen}\n-        successOpen={couponSuccessOpen}\n-        coupons={dearseedCoupons}\n-        onConfirm={handleCouponConfirm}\n-        onDismiss={handleCouponDismiss}\n-        onSuccessAction={handleCouponSuccessAction}\n-      />\n-      <NewcomerGiftSheet open={giftOpen} onConfirm={handleGiftConfirm} onDismiss={handleGiftDismiss} />\n-\n       <DebugPanel route={route} />\n     </PageContainer>\n   )\ndiff --git a/src/services/auth/session.test.ts b/src/services/auth/session.test.ts\nindex aafb7f6..7add2db 100644\n--- a/src/services/auth/session.test.ts\n+++ b/src/services/auth/session.test.ts\n@@ -11,23 +11,29 @@ describe('login response contract', () => {\n   })\n \n   it('unwraps a successful code/data response from the backend', () => {\n-    expect(\n-      parseLoginResponse({\n-        code: 0,\n-        data: { userInfo: { id: 'user-1' }, accessToken: 'token-1', is_new_user: false },\n-      }),\n-    ).toEqual({\n-      userInfo: { id: 'user-1' },\n-      accessToken: 'token-1',\n+    expect(parseLoginResponse({\n+      code: 0,\n+      data: { userInfo: { id: 'user-1' }, accessToken: 'token-1', is_new_user: false },\n+    })).toEqual({ userInfo: { id: 'user-1' }, accessToken: 'token-1', is_new_user: false })\n+  })\n+\n+  it('preserves new-user popup content from the backend envelope', () => {\n+    expect(parseLoginResponse({ code: 0, data: {\n+      userInfo: { id: 'user-2' }, accessToken: 'token-2', is_new_user: true,\n+      popup: { title: '注册成功', description: '已获得泡泡值奖励', points: 120 },\n+    } })).toEqual({ userInfo: { id: 'user-2' }, accessToken: 'token-2', is_new_user: true,\n+      popup: { title: '注册成功', description: '已获得泡泡值奖励', points: 120 } })\n+  })\n+\n+  it('ignores malformed popup content and preserves ordinary login', () => {\n+    expect(parseLoginResponse({ userInfo: {}, accessToken: 'token-3', is_new_user: true,\n+      popup: { title: 'Hi', description: '', points: 5 } })).toEqual({\n+      userInfo: {}, accessToken: 'token-3', is_new_user: true,\n     })\n   })\n \n   it('does not treat a non-success envelope as a valid session', () => {\n-    expect(() =>\n-      parseLoginResponse({\n-        code: 500,\n-        data: { message: 'backend failure' },\n-      }),\n-    ).toThrowError(expect.objectContaining({ code: 'AUTH_LOGIN_RESPONSE_INVALID' }))\n+    expect(() => parseLoginResponse({ code: 500, data: { message: 'backend failure' } }))\n+      .toThrowError(expect.objectContaining({ code: 'AUTH_LOGIN_RESPONSE_INVALID' }))\n   })\n })\ndiff --git a/src/services/auth/session.ts b/src/services/auth/session.ts\nindex eacd033..5e2f9c2 100644\n--- a/src/services/auth/session.ts\n+++ b/src/services/auth/session.ts\n@@ -15,16 +15,21 @@ const AUTH_FAILURE_EVENT = 'dr-card-ui:auth-failure'\n const AUTH_EXPIRED_EVENT = 'dr-card-ui:auth-expired'\n const LOGIN_PATH = '/api/oauth/login'\n \n-export interface AuthSession {\n-  userInfo: unknown\n-  accessToken: string\n+export interface NewUserPopup {\n+  title: string\n+  description: string\n+  points: number | string\n }\n \n-interface LoginResponse {\n+export interface AuthSession {\n   userInfo: unknown\n   accessToken: string\n+  is_new_user?: boolean\n+  popup?: NewUserPopup\n }\n \n+interface LoginResponse extends AuthSession {}\n+\n interface LoginEnvelope {\n   code?: unknown\n   data?: unknown\n@@ -46,7 +51,8 @@ let signatureCredentials: ApiSignatureCredentials | undefined\n function readSession(): AuthSession | undefined {\n   const value = storage.read(STORAGE_KEYS.authSession)\n   if (value && value.accessToken.trim()) {\n-    return { userInfo: volatileSession?.userInfo ?? null, accessToken: value.accessToken }\n+    if (volatileSession?.accessToken === value.accessToken) return volatileSession\n+    return { userInfo: null, accessToken: value.accessToken }\n   }\n   return volatileSession\n }\n@@ -93,7 +99,20 @@ export function parseLoginResponse(result: unknown): LoginResponse {\n     })\n   }\n \n-  return { userInfo: payload.userInfo, accessToken: payload.accessToken }\n+  const popupValue = payload.popup\n+  const popup = popupValue && typeof popupValue === 'object' ? popupValue as Partial<NewUserPopup> : undefined\n+  const validPopup = popup && typeof popup.title === 'string' && popup.title.trim()\n+    && typeof popup.description === 'string' && popup.description.trim()\n+    && (typeof popup.points === 'number' || typeof popup.points === 'string')\n+    ? { title: popup.title, description: popup.description, points: popup.points }\n+    : undefined\n+\n+  return {\n+    userInfo: payload.userInfo,\n+    accessToken: payload.accessToken,\n+    ...(typeof payload.is_new_user === 'boolean' ? { is_new_user: payload.is_new_user } : {}),\n+    ...(validPopup ? { popup: validPopup } : {}),\n+  }\n }\n \n async function loginFromNative(): Promise<AuthSession> {\ndiff --git a/src/storage/keys.ts b/src/storage/keys.ts\nindex 7086fe8..efe4ea4 100644\n--- a/src/storage/keys.ts\n+++ b/src/storage/keys.ts\n@@ -40,6 +40,11 @@ export const STORAGE_KEYS = {\n   ),\n   // User-confirmed UI preference: only retain a per-account six-dot mask flag\n   // after successful profile update. Never persist the PIN or its digest.\n+  newUserPopupSeen: defineStorageKey(\n+    'new-user-popup-seen',\n+    'local',\n+    z.record(z.string(), z.literal(true)),\n+  ),\n   profilePinMasks: defineStorageKey(\n     'profile-pin-masks',\n     'local',",
          "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": 56,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-10-26T04:38:15.681Z",
                  "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": 1221,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-10-26T04:38:15.692Z",
                  "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": 1212,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-10-26T04:38:15.811Z",
                  "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": 0,
                  "parallelIndex": 0,
                  "status": "passed",
                  "duration": 1068,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-10-26T04:38:17.254Z",
                  "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": 1,
                  "parallelIndex": 1,
                  "status": "passed",
                  "duration": 1111,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-10-26T04:38:17.295Z",
                  "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": 0,
                  "parallelIndex": 0,
                  "status": "passed",
                  "duration": 1025,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-10-26T04:38:18.340Z",
                  "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": 1,
                  "parallelIndex": 1,
                  "status": "passed",
                  "duration": 1078,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-10-26T04:38:18.429Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-8caf237c4d42513fff2f",
          "file": "test-gate.spec.ts",
          "line": 80,
          "column": 1
        }
      ]
    }
  ],
  "errors": [],
  "stats": {
    "startTime": "2026-10-26T04:38:14.730Z",
    "duration": 4848.784000000001,
    "expected": 7,
    "skipped": 0,
    "unexpected": 0,
    "flaky": 0
  }
}