453 lines
17 KiB
JavaScript
453 lines
17 KiB
JavaScript
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,
|
||
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(),
|
||
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: () => <div 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: {} });
|
||
});
|
||
|
||
afterEach(() => {
|
||
window.history.pushState(null, "", "/");
|
||
vi.useRealTimers();
|
||
vi.clearAllMocks();
|
||
});
|
||
|
||
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(<DatabiApp />);
|
||
|
||
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(<DatabiApp />);
|
||
|
||
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(<DatabiApp />);
|
||
|
||
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(<DatabiApp />);
|
||
|
||
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(<DatabiApp />);
|
||
|
||
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(<DatabiApp />);
|
||
|
||
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" },
|
||
{ app_code: "aslan", app_name: "Aslan", kind: "legacy" }
|
||
],
|
||
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(<DatabiApp />);
|
||
|
||
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).toHaveBeenCalledWith(expect.objectContaining({ statTz: "Asia/Shanghai" }));
|
||
expect(fetchSocialBiKpi).toHaveBeenCalledWith(expect.objectContaining({ statTz: "Asia/Shanghai" }));
|
||
|
||
// 经营概览:App 名、Hero 汇总充值(12,300 + 5,000 USD 分 = $173)、
|
||
// App 对比里 Lalu 的单 App 充值($123)、区域 Top5 里的中东大区。
|
||
expect(screen.getAllByText("Lalu").length).toBeGreaterThan(0);
|
||
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();
|
||
});
|
||
expect(within(viewTabs).getByRole("tab", { name: "运营中心" })).toHaveAttribute("aria-selected", "true");
|
||
expect(screen.getAllByText("Omar").length).toBeGreaterThan(0);
|
||
expect(screen.getAllByText("$900").length).toBeGreaterThan(0);
|
||
});
|
||
|
||
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(<DatabiApp />);
|
||
|
||
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();
|
||
}
|
||
});
|
||
}
|