import { act, render, screen, within } from "@testing-library/react"; import { afterEach, beforeEach, expect, test, vi } from "vitest"; import { DatabiApp } from "./DatabiApp.jsx"; import { fetchFilterOptions, fetchSelfGameStatisticsOverview, fetchSocialBiFunnel, fetchSocialBiKpi, fetchSocialBiMaster, fetchSocialBiOverview, fetchSocialBiRequirements, fetchStatisticsOverview } from "./api.js"; vi.mock("./api.js", () => ({ fetchFilterOptions: vi.fn(), fetchSocialBiFilterOptions: vi.fn(), fetchSocialBiFunnel: vi.fn(), fetchSocialBiKpi: vi.fn(), fetchSocialBiMaster: vi.fn(), fetchSocialBiOverview: vi.fn(), fetchSocialBiRequirements: vi.fn(), fetchSelfGameStatisticsOverview: vi.fn(), fetchStatisticsOverview: vi.fn(), getCurrentAppCode: vi.fn(() => "lalu"), getDefaultAppOptions: vi.fn(() => [ { code: "", label: "全部", value: "" }, { code: "lalu", label: "Lalu", value: "lalu" }, { code: "aslan", label: "Aslan", value: "aslan" }, { code: "yumi", label: "Yumi", value: "yumi" } ]), setCurrentAppCode: vi.fn((value) => String(value || "lalu").toLowerCase()) })); vi.mock("./charts/EChart.jsx", () => ({ EChart: ({ option }) => (
({ data, name })))} data-testid="databi-chart" /> ) })); beforeEach(() => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-06-06T08:00:00Z")); fetchFilterOptions.mockResolvedValue({ appOptions: [ { code: "", label: "全部", value: "" }, { code: "lalu", label: "Lalu", value: "lalu" }, { code: "aslan", label: "Aslan", value: "aslan" }, { code: "yumi", label: "Yumi", value: "yumi" } ], countryOptions: [{ countryCode: "", id: 0, label: "全部国家", regionIds: [], searchText: "全部国家 all", value: 0 }], regionOptions: [{ countryCodes: [], id: "all", label: "全部区域", value: "all" }] }); fetchSelfGameStatisticsOverview.mockResolvedValue({}); fetchSocialBiMaster.mockResolvedValue({ access: { all: true, scopes: [] }, apps: [], operators: [], permissions: {}, regions: [] }); fetchSocialBiOverview.mockResolvedValue({ access: { all: true, scopes: [] }, apps: [] }); fetchSocialBiFunnel.mockResolvedValue({ access: { all: true, scopes: [] }, apps: [] }); fetchSocialBiKpi.mockResolvedValue({ items: [], period_month: "2026-06", summary: {} }); fetchSocialBiRequirements.mockResolvedValue({ columns: [], daily_rows: [], section: "new_users", total: null }); }); afterEach(() => { window.history.pushState(null, "", "/"); vi.useRealTimers(); vi.clearAllMocks(); vi.unstubAllGlobals(); }); test("keeps current data visible during scheduled refresh", async () => { const overview = { active_users: 10, paid_users: 2, recharge_usd_minor: 100, updated_at_ms: 1 }; fetchStatisticsOverview .mockResolvedValueOnce(overview) .mockResolvedValueOnce(overview) .mockImplementationOnce(() => new Promise(() => {})); render(); await flushEffects(); expect(screen.getAllByText("$1").length).toBeGreaterThan(0); expect(screen.getByLabelText("数据报表")).toBeTruthy(); expect(document.querySelector(".report-metric-card.is-loading")).toBeNull(); await act(async () => { vi.advanceTimersByTime(60_000); }); expect(fetchStatisticsOverview).toHaveBeenCalledTimes(3); expect(screen.getAllByText("$1").length).toBeGreaterThan(0); expect(document.querySelector(".report-metric-card.is-loading")).toBeNull(); }); test("queries China natural day statistics when display timezone is Beijing", async () => { fetchStatisticsOverview.mockResolvedValue({ active_users: 10, paid_users: 2, recharge_usd_minor: 100, updated_at_ms: 1 }); render(); await flushEffects(); expect(fetchStatisticsOverview).toHaveBeenCalledWith(expect.objectContaining({ endMs: Date.UTC(2026, 5, 6, 15, 59, 59), seriesEndMs: Date.UTC(2026, 5, 6, 15, 59, 59), seriesStartMs: Date.UTC(2026, 4, 30, 16, 0, 0), startMs: Date.UTC(2026, 5, 5, 16, 0, 0), statTz: "Asia/Shanghai" })); }); test("renders legacy app metric columns in report view", async () => { fetchStatisticsOverview.mockResolvedValue({ coin_seller_stock_coin: 400_000, coin_seller_transfer_coin: 160_000, salary_transfer_coin: 88_000, country_breakdown: [ { coin_seller_stock_coin: 400_000, coin_seller_transfer_coin: 160_000, country: "巴西", country_id: 86, salary_transfer_coin: 88_000 } ], updated_at_ms: 1 }); render(); await flushEffects(); expect(screen.getByRole("columnheader", { name: "币商充值金币" })).toBeTruthy(); expect(screen.getByRole("columnheader", { name: "币商出货金币" })).toBeTruthy(); expect(screen.getByRole("columnheader", { name: "工资兑换金币" })).toBeTruthy(); expect(screen.getAllByText("400,000").length).toBeGreaterThan(0); expect(screen.getAllByText("160,000").length).toBeGreaterThan(0); expect(screen.getAllByText("88,000").length).toBeGreaterThan(0); }); test("switches report sidebar sections and stops data report polling on empty sections", async () => { fetchStatisticsOverview.mockResolvedValue({ active_users: 10, paid_users: 2, recharge_usd_minor: 100, updated_at_ms: 1 }); render(); await flushEffects(); const reportTypeTabs = screen.getByRole("tablist", { name: "报表类型" }); expect(within(reportTypeTabs).getByRole("tab", { name: "数据报表" })).toHaveAttribute("aria-selected", "true"); expect(screen.getByRole("region", { name: "数据报表" })).toBeTruthy(); await act(async () => { within(reportTypeTabs).getByRole("tab", { name: "财务报表" }).click(); }); expect(within(reportTypeTabs).getByRole("tab", { name: "财务报表" })).toHaveAttribute("aria-selected", "true"); expect(screen.getByRole("region", { name: "财务报表" })).toBeTruthy(); expect(screen.queryByRole("region", { name: "数据报表" })).toBeNull(); const requestCountAfterFinanceSwitch = fetchStatisticsOverview.mock.calls.length; await act(async () => { vi.advanceTimersByTime(60_000); }); expect(fetchStatisticsOverview).toHaveBeenCalledTimes(requestCountAfterFinanceSwitch); await act(async () => { within(reportTypeTabs).getByRole("tab", { name: "运营报表" }).click(); }); expect(within(reportTypeTabs).getByRole("tab", { name: "运营报表" })).toHaveAttribute("aria-selected", "true"); expect(screen.getByRole("region", { name: "运营报表" })).toBeTruthy(); await act(async () => { within(reportTypeTabs).getByRole("tab", { name: "数据报表" }).click(); }); await flushEffects(); expect(screen.getByRole("region", { name: "数据报表" })).toBeTruthy(); expect(fetchStatisticsOverview.mock.calls.length).toBeGreaterThan(requestCountAfterFinanceSwitch); }); test("loads self game statistics from the big screen switch", async () => { fetchStatisticsOverview.mockResolvedValue({ active_users: 10, paid_users: 2, recharge_usd_minor: 100, updated_at_ms: 1 }); fetchSelfGameStatisticsOverview.mockResolvedValue({ events: [{ event_name: "page_open", event_count: 8, user_count: 5 }], match_totals: { created_matches: 3, matched_matches: 2, settled_matches: 1 }, retention: { cohort_users: 5, day1_rate: 0.2, day7_rate: 0.1 } }); render(); await flushEffects(); await act(async () => { screen.getByRole("button", { name: "自研游戏" }).click(); }); await flushEffects(); expect(fetchSelfGameStatisticsOverview).toHaveBeenCalledWith(expect.objectContaining({ appCode: "lalu", gameId: "all", statTz: "Asia/Shanghai" })); expect(screen.getByText("Dice / Rock H5 聚合统计")).toBeTruthy(); expect(screen.getByText("创建局")).toBeTruthy(); }); test("renders real-room robot gift cards in a separate row", async () => { fetchStatisticsOverview.mockResolvedValue({ real_room_robot_avg_room_gift_coin: 300, real_room_robot_gift_room_count: 2, real_room_robot_lucky_gift_coin: 200, real_room_robot_normal_gift_coin: 100, real_room_robot_super_gift_coin: 300, updated_at_ms: 1 }); render(); await flushEffects(); await act(async () => { screen.getByRole("tab", { name: "大屏 BI" }).click(); }); const businessSection = screen.getByLabelText("业务指标"); const robotGiftSection = screen.getByLabelText("真人房机器人送礼指标"); expect(within(businessSection).queryByText("真人房机器人普通礼物")).toBeNull(); expect(within(robotGiftSection).getByText("真人房机器人普通礼物")).toBeTruthy(); expect(within(robotGiftSection).getByText("真人房机器人幸运礼物")).toBeTruthy(); expect(within(robotGiftSection).getByText("真人房机器人超级幸运礼物")).toBeTruthy(); expect(within(robotGiftSection).getByText("真人房机器人房均礼物")).toBeTruthy(); }); test("routes databi social page to Social BI and loads real overview rows", async () => { window.history.pushState(null, "", "/databi/social/"); const laluTotal = { active_users: 20, arpu_usd_minor: 410, arppu_usd_minor: 615, avg_mic_online_ms: 120_000, consumed_coin: 900_000, consume_output_delta: 450_000, consume_output_ratio: 0.5, d1_retention_rate: 0.25, game_profit_rate: 0.17, game_turnover: 300_000, gift_coin_spent: 123_456, google_recharge_usd_minor: 700, mifapay_recharge_usd_minor: 800, new_users: 3, output_coin: 450_000, paid_conversion_rate: 0.2, paid_users: 4, recharge_usd_minor: 12_300, recharge_users: 4 }; const aslanRegionRow = { active_users: 10, new_users: 1, paid_users: 2, recharge_usd_minor: 5000, recharge_users: 2, region_code: "MENA", region_id: 11, region_name: "中东大区" }; fetchSocialBiMaster.mockResolvedValue({ access: { all: true, scopes: [] }, apps: [ { app_code: "lalu", app_name: "Lalu", kind: "hyapp", logo_url: "https://media.example.com/lalu.png" }, { app_code: "aslan", app_name: "Aslan", kind: "legacy", logo_url: "https://media.example.com/aslan.png" } ], operators: [ { account: "omar", name: "Omar", scopes: [{ app_code: "aslan", region_id: 11 }], team: "中东运营部", team_id: 2, user_id: 9 } ], permissions: {}, regions: [{ app_code: "aslan", countries: ["SA"], region_code: "MENA", region_id: 11, region_name: "中东大区" }] }); fetchSocialBiOverview.mockResolvedValue({ access: { all: true, scopes: [] }, apps: [ { app_code: "lalu", app_name: "Lalu", country_breakdown: [], daily_country_breakdown: [], daily_region_breakdown: [], daily_series: [], kind: "hyapp", region_breakdown: [], total: laluTotal }, { app_code: "aslan", app_name: "Aslan", country_breakdown: [], daily_country_breakdown: [], daily_region_breakdown: [], daily_series: [], kind: "legacy", region_breakdown: [aslanRegionRow], total: { active_users: 10, new_users: 1, paid_users: 2, recharge_usd_minor: 5000, recharge_users: 2 } } ] }); fetchSocialBiKpi.mockResolvedValue({ items: [ { app_code: "aslan", app_name: "Aslan", month_recharge_usd_minor: 90_000, operator_account: "omar", operator_name: "Omar", operator_user_id: 9, range_new_users: 1, range_recharge_usd_minor: 5000, region_id: 11, region_name: "中东大区", team: "中东运营部", team_id: 2 } ], period_month: "2026-06", summary: { month_recharge_usd_minor: 90_000, operator_count: 1, range_recharge_usd_minor: 5000 } }); render(); await flushEffects(); // v2 Shell:主视图容器 + 侧栏视图 tab,默认选中“经营概览”。 expect(screen.getByLabelText("Social BI 数据中心")).toBeTruthy(); const viewTabs = screen.getByRole("tablist", { name: "数据视图" }); ["经营概览", "地区洞察", "运营中心", "留存质量", "数据明细", "埋点漏斗"].forEach((name) => { expect(within(viewTabs).getByRole("tab", { name })).toBeTruthy(); }); expect(within(viewTabs).getByRole("tab", { name: "经营概览" })).toHaveAttribute("aria-selected", "true"); expect(fetchSocialBiMaster).toHaveBeenCalledTimes(1); expect(fetchSocialBiOverview).toHaveBeenCalledWith(expect.objectContaining({ statTz: "Asia/Shanghai" })); expect(fetchSocialBiFunnel).not.toHaveBeenCalled(); expect(fetchSocialBiKpi).not.toHaveBeenCalled(); // 经营概览:App 名、Hero 汇总充值(12,300 + 5,000 USD 分 = $173)、 // App 对比里 Lalu 的单 App 充值($123)、区域 Top5 里的中东大区。 expect(screen.getAllByText("Lalu").length).toBeGreaterThan(0); expect(document.querySelector('img[src="https://media.example.com/lalu.png"]')).toBeTruthy(); expect(document.querySelector('img[src="https://media.example.com/aslan.png"]')).toBeTruthy(); expect(screen.getAllByText("充值 ($)").length).toBeGreaterThan(0); expect(screen.getByText("付费用户")).toBeTruthy(); expect(screen.getAllByText("$173").length).toBeGreaterThan(0); expect(screen.getAllByText("$123").length).toBeGreaterThan(0); expect(screen.getAllByText("中东大区").length).toBeGreaterThan(0); // 侧栏切换:地区洞察 —— 区域下钻表出现,Aslan 中东大区行充值 $50。 await act(async () => { within(viewTabs).getByRole("tab", { name: "地区洞察" }).click(); }); expect(within(viewTabs).getByRole("tab", { name: "地区洞察" })).toHaveAttribute("aria-selected", "true"); expect(screen.getByText("区域下钻")).toBeTruthy(); expect(screen.getAllByText("中东大区").length).toBeGreaterThan(0); expect(screen.getAllByText("$50").length).toBeGreaterThan(0); // 侧栏切换:运营中心 —— 运营人员 Omar 上榜,只展示负责范围内的充值数据。 await act(async () => { within(viewTabs).getByRole("tab", { name: "运营中心" }).click(); }); await flushEffects(); expect(within(viewTabs).getByRole("tab", { name: "运营中心" })).toHaveAttribute("aria-selected", "true"); expect(fetchSocialBiKpi).toHaveBeenCalledTimes(1); expect(fetchSocialBiKpi).toHaveBeenCalledWith(expect.objectContaining({ statTz: "Asia/Shanghai" })); expect(screen.getAllByText("Omar").length).toBeGreaterThan(0); expect(screen.getAllByText("$900").length).toBeGreaterThan(0); }); test("filters operation KPI by person and shows totals with App and region amounts", async () => { window.history.pushState(null, "", "/databi/social/?view=team&operator=9"); fetchSocialBiMaster.mockResolvedValue({ access: { all: true, scopes: [] }, apps: [ { app_code: "aslan", app_name: "Aslan", kind: "legacy" }, { app_code: "yumi", app_name: "Yumi", kind: "legacy" } ], operators: [ { account: "omar", name: "Omar", scopes: [{ app_code: "aslan", region_id: 11 }, { app_code: "yumi", region_id: 20 }], team: "中东运营部", team_id: 2, user_id: 9 } ], permissions: {}, regions: [ { app_code: "aslan", region_id: 11, region_name: "中东大区" }, { app_code: "yumi", region_id: 20, region_name: "南亚大区" } ] }); fetchSocialBiOverview.mockResolvedValue({ access: { all: true, scopes: [] }, apps: [] }); fetchSocialBiKpi.mockResolvedValue({ items: [ { app_code: "aslan", app_name: "Aslan", month_recharge_usd_minor: 90_000, operator_account: "omar", operator_name: "Omar", operator_user_id: 9, range_recharge_usd_minor: 5000, region_id: 11, region_name: "中东大区", team: "中东运营部" }, { app_code: "yumi", app_name: "Yumi", month_recharge_usd_minor: 50_000, operator_account: "omar", operator_name: "Omar", operator_user_id: 9, range_recharge_usd_minor: 3000, region_id: 20, region_name: "南亚大区", team: "中东运营部" } ], operator_app_rows: [ { app_code: "aslan", app_name: "Aslan", month_recharge_usd_minor: 90_000, operator_account: "omar", operator_name: "Omar", operator_user_id: 9, range_recharge_usd_minor: 5000, region_count: 1, region_names: "中东大区", team: "中东运营部" }, { app_code: "yumi", app_name: "Yumi", month_recharge_usd_minor: 50_000, operator_account: "omar", operator_name: "Omar", operator_user_id: 9, range_recharge_usd_minor: 3000, region_count: 1, region_names: "南亚大区", team: "中东运营部" } ], period_month: "2026-06", summary: { month_recharge_usd_minor: 140_000, operator_count: 1, range_recharge_usd_minor: 8000 } }); render(); await flushEffects(); expect(fetchSocialBiKpi).toHaveBeenCalledWith(expect.objectContaining({ operatorUserId: 9 })); expect(screen.getByRole("combobox", { name: "运营人员筛选" })).toHaveValue("Omar"); expect(window.location.search).toContain("operator=9"); const summary = screen.getByLabelText("充值金额汇总"); expect(within(summary).getByText("Omar")).toBeTruthy(); expect(within(summary).getByText("$80")).toBeTruthy(); expect(within(summary).getByText("$1,400")).toBeTruthy(); expect(screen.getByRole("cell", { name: "中东大区" })).toBeTruthy(); expect(screen.getByRole("cell", { name: "南亚大区" })).toBeTruthy(); await act(async () => { screen.getByRole("radio", { name: "按人员×App" }).click(); }); expect(screen.getByRole("cell", { name: "中东大区" })).toBeTruthy(); expect(screen.getByRole("cell", { name: "南亚大区" })).toBeTruthy(); expect(screen.getAllByText("$50").length).toBeGreaterThan(0); expect(screen.getAllByText("$30").length).toBeGreaterThan(0); }); test("defaults retention quality to canonical active cohort and can switch to new-user cohort", async () => { window.history.pushState(null, "", "/databi/social/?view=retention&apps=lalu"); fetchSocialBiMaster.mockResolvedValue({ access: { all: true, scopes: [] }, apps: [{ app_code: "lalu", app_name: "Lalu", kind: "hyapp" }], operators: [], permissions: {}, regions: [] }); fetchSocialBiOverview.mockResolvedValue({ access: { all: true, scopes: [] }, apps: [ { app_code: "lalu", app_name: "Lalu", country_breakdown: [], daily_region_breakdown: [], daily_series: [ { active_users: 20, d1_retention_base_users: 4, d1_retention_rate: 0.25, d1_retention_users: 1, d30_retention_base_users: 2, d30_retention_rate: 0.5, d30_retention_users: 1, d7_retention_base_users: 3, d7_retention_rate: 0.3333, d7_retention_users: 1, new_users: 4, stat_day: "2026-06-05" } ], kind: "hyapp", region_breakdown: [], total: { active_users: 20, d1_retention_base_users: 4, d1_retention_rate: 0.25, d1_retention_users: 1, d30_retention_base_users: 2, d30_retention_rate: 0.5, d30_retention_users: 1, d7_retention_base_users: 3, d7_retention_rate: 0.3333, d7_retention_users: 1, new_users: 4 } } ] }); fetchSocialBiRequirements.mockResolvedValue({ access: { all: true, scopes: [] }, apps: [ { app_code: "lalu", app_name: "Lalu", kind: "hyapp", sections: [ { daily_series: [ { metrics: { active_d1_active_base_users: 12, active_d1_active_rate: 0.5, active_d1_active_users: 6, active_d30_active_base_users: 3, active_d30_active_rate: 0.3333, active_d30_active_users: 1, active_d7_active_base_users: 6, active_d7_active_rate: 0.5, active_d7_active_users: 3 }, stat_day: "2026-06-04" }, { metrics: { active_d1_active_base_users: 8, active_d1_active_rate: 0.5, active_d1_active_users: 4, active_d30_active_base_users: 2, active_d30_active_rate: 0, active_d30_active_users: 0, active_d7_active_base_users: 4, active_d7_active_rate: 0.25, active_d7_active_users: 1 }, stat_day: "2026-06-05" }, { metrics: { active_d1_active_base_users: null, active_d1_active_rate: null, active_d1_active_users: null, active_d30_active_base_users: null, active_d30_active_rate: null, active_d30_active_users: null, active_d7_active_base_users: null, active_d7_active_rate: null, active_d7_active_users: null }, stat_day: "2026-06-06" } ], key: "retention", total: { metrics: { active_d1_active_base_users: 20, active_d1_active_rate: 0.5, active_d1_active_users: 10, active_d30_active_base_users: 5, active_d30_active_rate: 0.2, active_d30_active_users: 1, active_d7_active_base_users: 10, active_d7_active_rate: 0.4, active_d7_active_users: 4 } } } ] } ] }); render(); await flushEffects(); expect(fetchSocialBiRequirements).toHaveBeenCalledWith(expect.objectContaining({ newUserType: "all", payerType: "all", section: "retention", userRole: "all" })); // Overview 的 access 结果落地不能改变 requirements callback 身份,否则同一轮 D30 查询会重复执行。 expect(fetchSocialBiRequirements).toHaveBeenCalledTimes(1); const modeSwitch = screen.getByRole("radiogroup", { name: "留存口径" }); expect(within(modeSwitch).getByRole("radio", { name: "整体次留" })).toHaveAttribute("aria-checked", "true"); expect(screen.getByText("活跃基数与次日留存")).toBeTruthy(); expect(screen.getByText("整体用户留存趋势")).toBeTruthy(); expect(screen.getByRole("columnheader", { name: "活跃用户基数" })).toBeTruthy(); expect(screen.getByRole("columnheader", { name: "次留人数" })).toBeTruthy(); expect(screen.getAllByText("50.0%").length).toBeGreaterThan(0); expect(screen.getAllByText(/活跃基数 20 人/).length).toBeGreaterThan(0); expect(screen.getAllByText(/已接入 Social App 汇总/).length).toBeGreaterThan(0); const overallCharts = screen.getAllByTestId("databi-chart"); const comboSeries = JSON.parse(overallCharts[0].dataset.series); const trendSeries = JSON.parse(overallCharts[1].dataset.series); expect(comboSeries.find((series) => series.name === "整体次日留存").data).toEqual([50, 50, null]); expect(trendSeries.find((series) => series.name === "整体次日留存").data).toEqual([50, 50, null]); await act(async () => { within(modeSwitch).getByRole("radio", { name: "新增次留" }).click(); }); expect(within(modeSwitch).getByRole("radio", { name: "新增次留" })).toHaveAttribute("aria-checked", "true"); expect(screen.getByText("新增与次日留存")).toBeTruthy(); expect(screen.getByText("新增用户留存趋势")).toBeTruthy(); expect(screen.getByRole("columnheader", { name: "新增" })).toBeTruthy(); expect(screen.getAllByText("25.0%").length).toBeGreaterThan(0); expect(screen.getAllByText(/注册基数 4 人/).length).toBeGreaterThan(0); }); test("marks overall retention as a partial subset when one app fails", async () => { window.history.pushState(null, "", "/databi/social/?view=retention"); fetchSocialBiMaster.mockResolvedValue({ access: { all: true, scopes: [] }, apps: [ { app_code: "lalu", app_name: "Lalu", kind: "hyapp" }, { app_code: "huwaa", app_name: "Huwaa", kind: "hyapp" } ], operators: [], permissions: {}, regions: [] }); fetchSocialBiOverview.mockResolvedValue({ access: { all: true, scopes: [] }, apps: [] }); fetchSocialBiRequirements.mockResolvedValue({ access: { all: true, scopes: [] }, apps: [ { app_code: "lalu", app_name: "Lalu", kind: "hyapp", sections: [{ daily_series: [{ active_d1_active_base_users: 10, active_d1_active_rate: 0.5, active_d1_active_users: 5, stat_day: "2026-06-05" }], key: "retention", total: { active_d1_active_base_users: 10, active_d1_active_rate: 0.5, active_d1_active_users: 5 } }] }, { app_code: "huwaa", app_name: "Huwaa", error: "statistics timeout", kind: "hyapp" } ] }); render(); await flushEffects(); expect(screen.getByRole("alert")).toHaveTextContent("部分 App 整体留存加载失败"); expect(screen.getByRole("alert")).toHaveTextContent("Huwaa: statistics timeout"); expect(screen.getAllByText(/成功返回的 Social App 子集/).length).toBeGreaterThan(0); expect(screen.queryByText(/已接入 Social App 汇总/)).toBeNull(); }); test("renders social BI data requirements table with section filters and CSV export", async () => { window.history.pushState(null, "", "/databi/social/?view=table®ions=lalu:9,lalu:11"); fetchSocialBiMaster.mockResolvedValue({ access: { all: true, scopes: [] }, apps: [{ app_code: "lalu", app_name: "Lalu", kind: "hyapp" }], operators: [], permissions: {}, regions: [] }); fetchSocialBiOverview.mockResolvedValue({ access: { all: true, scopes: [] }, apps: [{ app_code: "lalu", app_name: "Lalu", daily_series: [], kind: "hyapp", total: {} }] }); fetchSocialBiRequirements.mockImplementation(async ({ section }) => { if (section === "revenue") { return { access: { all: true, scopes: [] }, apps: [{ app_code: "lalu", app_name: "Lalu", kind: "hyapp", sections: [{ columns: [ { key: "recharge_users", label: "充值用户", type: "count" }, { key: "recharge_usd_minor", label: "充值金额", type: "money_minor" } ], daily_series: [{ metrics: { recharge_users: 2, recharge_usd_minor: 12300 }, stat_day: "2026-06-05" }], key: section, total: { label: "汇总", metrics: { recharge_users: 2, recharge_usd_minor: 12300 }, stat_day: "total" } }] }] }; } return { access: { all: true, scopes: [] }, apps: [{ app_code: "lalu", app_name: "Lalu", kind: "hyapp", sections: [{ columns: [ { key: "app_download_users", label: "下载APP人数", type: "count" }, { key: "registration_success_rate", label: "注册成功率", type: "ratio" } ], daily_series: [{ metrics: { app_download_users: 12, registration_success_rate: 0.6666 }, stat_day: "2026-06-05" }], key: section, total: { label: "汇总", metrics: { app_download_users: 12, registration_success_rate: 0.6666 }, stat_day: "total" } }] }] }; }); render(); await flushEffects(); expect(fetchSocialBiOverview).toHaveBeenCalledWith( expect.objectContaining({ appCodes: ["lalu"], regionIds: [9, 11], statTz: "Asia/Shanghai" }) ); const viewTabs = screen.getByRole("tablist", { name: "数据视图" }); expect(within(viewTabs).getByRole("tab", { name: "数据明细" })).toHaveAttribute("aria-selected", "true"); await act(async () => { screen.getByRole("radio", { name: "数据需求" }).click(); }); await flushEffects(); expect(fetchSocialBiRequirements).toHaveBeenCalledWith(expect.objectContaining({ payerType: "all", regionIds: [9, 11], section: "revenue", userRole: "all" })); const requirementTabs = screen.getByRole("tablist", { name: "数据需求分组" }); expect(within(requirementTabs).getAllByRole("tab").map((tab) => tab.textContent)).toEqual([ "充值", "新增", "日活", "礼物", "游戏", "留存" ]); expect(screen.getByRole("tab", { name: "充值" })).toHaveAttribute("aria-selected", "true"); expect(screen.getByRole("radiogroup", { name: "用户身份" })).toBeTruthy(); expect(screen.getByRole("radiogroup", { name: "付费身份" })).toBeTruthy(); expect(screen.getByText("充值用户")).toBeTruthy(); expect(screen.getByText("充值金额")).toBeTruthy(); expect(screen.getAllByText("$123").length).toBeGreaterThan(0); await act(async () => { screen.getByRole("tab", { name: "新增" }).click(); }); await flushEffects(); expect(fetchSocialBiRequirements).toHaveBeenLastCalledWith(expect.objectContaining({ section: "new_users" })); expect(screen.getByRole("tab", { name: "新增" })).toHaveAttribute("aria-selected", "true"); expect(screen.getByText("下载APP人数")).toBeTruthy(); expect(screen.getByText("注册成功率")).toBeTruthy(); expect(screen.getAllByText("12").length).toBeGreaterThan(0); expect(screen.getAllByText("66.66%").length).toBeGreaterThan(0); expect(screen.queryByRole("radiogroup", { name: "用户身份" })).toBeNull(); expect(screen.queryByRole("radiogroup", { name: "付费身份" })).toBeNull(); await act(async () => { screen.getByRole("tab", { name: "充值" }).click(); }); await flushEffects(); expect(screen.getByRole("radiogroup", { name: "用户身份" })).toBeTruthy(); expect(screen.getByRole("radiogroup", { name: "付费身份" })).toBeTruthy(); await act(async () => { screen.getByRole("radio", { name: "主播" }).click(); }); await flushEffects(); await act(async () => { screen.getByRole("radio", { name: "已付费" }).click(); }); await flushEffects(); expect(fetchSocialBiRequirements).toHaveBeenLastCalledWith(expect.objectContaining({ payerType: "paid", section: "revenue", userRole: "host" })); expect(screen.getByText("充值用户")).toBeTruthy(); expect(screen.getByText("充值金额")).toBeTruthy(); expect(screen.getAllByText("$123").length).toBeGreaterThan(0); const urlMock = { createObjectURL: vi.fn(() => "blob:social-bi-requirements"), revokeObjectURL: vi.fn() }; const anchorClick = vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(() => {}); vi.stubGlobal("URL", urlMock); await act(async () => { screen.getByRole("button", { name: "导出 CSV" }).click(); }); expect(urlMock.createObjectURL).toHaveBeenCalledTimes(1); expect(anchorClick).toHaveBeenCalledTimes(1); anchorClick.mockRestore(); }); test("groups social BI wide-table daily rows by app instead of interleaving by date", async () => { window.history.pushState(null, "", "/databi/social/?view=table"); fetchSocialBiMaster.mockResolvedValue({ access: { all: true, scopes: [] }, apps: [ // 带 logo:AppIdentity 渲染图片而不是字母头像,单元格 textContent 恰好等于 App 名。 { app_code: "lalu", app_name: "Lalu", kind: "hyapp", logo_url: "https://media.example.com/lalu.png" }, { app_code: "aslan", app_name: "Aslan", kind: "legacy", logo_url: "https://media.example.com/aslan.png" } ], operators: [], permissions: {}, regions: [] }); const dailyRow = (statDay, rechargeUsdMinor) => ({ recharge_usd_minor: rechargeUsdMinor, recharge_users: 1, stat_day: statDay }); fetchSocialBiOverview.mockResolvedValue({ access: { all: true, scopes: [] }, apps: [ { app_code: "lalu", app_name: "Lalu", country_breakdown: [], daily_region_breakdown: [], daily_series: [dailyRow("2026-06-04", 100), dailyRow("2026-06-05", 200)], kind: "hyapp", region_breakdown: [], total: { recharge_usd_minor: 300, recharge_users: 2 } }, { // Aslan 各日充值都高于 Lalu:旧的“按日倒序 + 日内充值倒序”排序会把两个 App 一天一行穿插, // 新排序必须仍按 master 顺序把 Lalu 块排在前面。 app_code: "aslan", app_name: "Aslan", country_breakdown: [], daily_region_breakdown: [], daily_series: [dailyRow("2026-06-04", 900), dailyRow("2026-06-05", 800)], kind: "legacy", region_breakdown: [], total: { recharge_usd_minor: 1700, recharge_users: 2 } } ] }); render(); await flushEffects(); const table = document.querySelector(".sbi-dt-table"); expect(table).toBeTruthy(); const headerCells = [...table.querySelectorAll("thead tr:first-child th")].map((cell) => cell.textContent.trim()); expect(headerCells.slice(0, 2)).toEqual(["App", "日期"]); // 行序:汇总/平均值之后,每个 App 的日行连成块(块内日期倒序),不按日期把 App 穿插。 const bodyRows = [...table.querySelectorAll("tbody tr")]; const dimCells = bodyRows.map((row) => { const cells = row.querySelectorAll("td"); return [cells[0].textContent.trim(), cells[1].textContent.trim()]; }); expect(dimCells).toEqual([ ["全部", "汇总"], ["平均值", "--"], ["Lalu", "2026-06-05"], ["Lalu", "2026-06-04"], ["Aslan", "2026-06-05"], ["Aslan", "2026-06-04"] ]); // 分块分隔线只标在后一个块的首行。 expect(bodyRows[4].classList.contains("sbi-dt-group-start")).toBe(true); expect(bodyRows[3].classList.contains("sbi-dt-group-start")).toBe(false); }); test("renders social BI funnel view and limits funnel apps to supported apps", async () => { window.history.pushState(null, "", "/databi/social/?view=funnel"); fetchSocialBiMaster.mockResolvedValue({ access: { all: true, scopes: [] }, apps: [ { app_code: "lalu", app_name: "Lalu", kind: "hyapp" }, { app_code: "huwaa", app_name: "Huwaa", kind: "hyapp" }, { app_code: "fami", app_name: "Fami", kind: "hyapp" }, { app_code: "aslan", app_name: "Aslan", kind: "legacy" }, { app_code: "yumi", app_name: "Yumi", kind: "legacy" } ], operators: [], permissions: {}, regions: [] }); fetchSocialBiFunnel.mockResolvedValue({ access: { all: true, scopes: [] }, apps: [ { app_code: "lalu", app_name: "Lalu", cohorts: [ { base_users: 1, d1_retention_rate: 1, d1_retention_users: 1, dimension: "country", label: "US", steps: [ { event_name: "login_success", user_count: 1 }, { event_name: "profile_complete", user_count: 1 }, { event_name: "room_join_success", user_count: 1 }, { event_name: "stay_3m", user_count: 1 }, { event_name: "send_message", user_count: 1 } ], value: "US" } ], kind: "hyapp", steps: [ { event_name: "login_start", event_count: 2, label: "登录开始", user_count: 2 }, { event_name: "login_success", event_count: 1, label: "登录成功", user_count: 1 }, { event_name: "room_join_success", event_count: 1, label: "进房成功", user_count: 1 }, { event_name: "room_join_fail", event_count: 1, is_failure: true, label: "进房失败", user_count: 1 }, { event_name: "stay_3m", event_count: 1, label: "停留 3 分钟", user_count: 1 }, { event_name: "send_message", event_count: 1, label: "发消息", user_count: 1 } ], totals: { d1_retention_base_users: 1, d1_retention_rate: 1, d1_retention_users: 1, login_start_users: 2, login_success_users: 1, room_join_fail_users: 1, room_join_success_users: 1 } } ] }); render(); await flushEffects(); expect(screen.getByRole("tab", { name: "埋点漏斗" })).toHaveAttribute("aria-selected", "true"); expect(screen.getByRole("button", { name: "全部漏斗 App" })).toBeTruthy(); expect(screen.getByRole("button", { name: /Lalu/ })).toBeTruthy(); expect(screen.getByRole("button", { name: /Huwaa/ })).toBeTruthy(); expect(screen.getByRole("button", { name: /Fami/ })).toBeTruthy(); expect(screen.queryByRole("button", { name: /Aslan/ })).toBeNull(); expect(screen.queryByRole("button", { name: /Yumi/ })).toBeNull(); expect(fetchSocialBiFunnel).toHaveBeenCalledWith(expect.objectContaining({ appCodes: ["lalu", "huwaa", "fami"] })); expect(screen.getByText("主路径转化")).toBeTruthy(); expect(screen.getByText("事件明细")).toBeTruthy(); expect(screen.getByText("D1 Cohort")).toBeTruthy(); expect(screen.getByText("login_start")).toBeTruthy(); expect(screen.getByText("room_join_fail")).toBeTruthy(); expect(screen.getByText("US")).toBeTruthy(); expect(screen.getAllByText("100.0%").length).toBeGreaterThan(0); expect(screen.getByTestId("databi-chart")).toBeTruthy(); }); async function flushEffects() { await act(async () => { for (let index = 0; index < 10; index += 1) { await Promise.resolve(); } }); }