diff --git a/src/components/game-distribution/GameDistributionPages.test.tsx b/src/components/game-distribution/GameDistributionPages.test.tsx
index e9a9552a2..a155c98e8 100644
--- a/src/components/game-distribution/GameDistributionPages.test.tsx
+++ b/src/components/game-distribution/GameDistributionPages.test.tsx
@@ -399,9 +399,7 @@ describe('GameDetailPage', () => {
expect(screen.getByText('由 原作者 制作')).toBeTruthy();
expect(screen.getByText('已被改编 3 次')).toBeTruthy();
// 已公开 + 授权开放 → 「Fork 这个作品」入口出现(M1 时它还不存在,这里钉住新契约)。
- expect(
- screen.getByRole('button', { name: 'Fork 这个作品' }),
- ).toBeTruthy();
+ expect(screen.getByRole('button', { name: 'Fork 这个作品' })).toBeTruthy();
});
it('旧数据缺少授权与血缘字段时只按禁止共创渲染', async () => {
@@ -436,9 +434,7 @@ describe('GameDetailPage', () => {
);
expect(await screen.findByText('允许全开放共创')).toBeTruthy();
- expect(
- screen.getByText('第 2 代作品 · 原作品已不可用'),
- ).toBeTruthy();
+ expect(screen.getByText('第 2 代作品 · 原作品已不可用')).toBeTruthy();
expect(screen.queryByText(/改编自/u)).toBeNull();
// 血缘作者行(卡内 `.game-fork-lineage__author`)不该出现;hero 里的
// 「由 测试作者 制作」是作品自身署名,不在断言范围内。
@@ -1134,7 +1130,7 @@ describe('GameLineagePage', () => {
// 父作品不在可见集合里:只标注「原作品已不可用」,不回显父作品内容。
expect(screen.getByText('原作品已不可用')).toBeTruthy();
// 任一公开节点都能进入对应详情页。
- fireEvent.click(screen.getByRole('button', { name: '母版' }));
+ fireEvent.click(screen.getByRole('button', { name: /《母版》/u }));
expect(onOpenDetail).toHaveBeenCalledWith('game-root');
});
@@ -1222,13 +1218,13 @@ describe('GameLineagePage', () => {
/>,
);
- expect(await screen.findByRole('button', { name: '母版' })).toBeTruthy();
+ expect(
+ await screen.findByRole('button', { name: /《母版》/u }),
+ ).toBeTruthy();
expect(
screen.getByText('这部作品还没有衍生作品,欢迎从它开始共创。'),
).toBeTruthy();
- expect(
- screen.queryByText('这部作品还没有可展示的族谱节点。'),
- ).toBeNull();
+ expect(screen.queryByText('这部作品还没有可展示的族谱节点。')).toBeNull();
});
it('超出节点上限时如实标注截断,不静默丢弃', async () => {
@@ -1240,7 +1236,11 @@ describe('GameLineagePage', () => {
generation: 0,
}),
nodes: [
- createLineageNode({ gameId: 'game-root', title: '母版', generation: 0 }),
+ createLineageNode({
+ gameId: 'game-root',
+ title: '母版',
+ generation: 0,
+ }),
createLineageNode({ gameId: 'game-2', title: '第二部', generation: 1 }),
],
truncated: true,
@@ -1336,11 +1336,7 @@ describe('GameLineagePage', () => {
it('缺少根作品编号时不发请求并提示编号无效', async () => {
render(
- ,
+ ,
);
expect(await screen.findByText('游戏编号无效')).toBeTruthy();
@@ -1435,9 +1431,7 @@ describe('GameDetailPage Fork 入口', () => {
expect(copyTextToClipboardMock).toHaveBeenCalledWith(
expect.stringContaining('/games/detail?id=game-1'),
);
- expect(
- screen.getByRole('button', { name: '作品链接已复制' }),
- ).toBeTruthy();
+ expect(screen.getByRole('button', { name: '作品链接已复制' })).toBeTruthy();
vi.useRealTimers();
});
@@ -1628,7 +1622,9 @@ describe('原版与二创对比入口', () => {
expect(await screen.findByText('叶节点')).toBeTruthy();
// 两个节点各有一个可见父引用?只有叶节点的父(game-root)在树里。
- const compareButtons = screen.getAllByRole('button', { name: '与原版对比' });
+ const compareButtons = screen.getAllByRole('button', {
+ name: '与原版对比',
+ });
expect(compareButtons).toHaveLength(1);
fireEvent.click(compareButtons[0]!);
expect(onOpenCompare).toHaveBeenCalledWith('game-root', 'game-leaf');
@@ -1695,7 +1691,9 @@ describe('GameComparePage', () => {
expect(await screen.findByText('作品不存在或未公开')).toBeTruthy();
expect(screen.getByText('二创作品')).toBeTruthy();
// 降级那一侧不渲染 iframe,另一侧照常。
- expect(document.querySelectorAll('iframe.game-player-frame')).toHaveLength(1);
+ expect(document.querySelectorAll('iframe.game-player-frame')).toHaveLength(
+ 1,
+ );
});
});
@@ -1866,9 +1864,9 @@ describe('GameThemePage', () => {
expect(getGameLineageMock).toHaveBeenCalledWith('game-root-1');
expect(getGameLineageMock).toHaveBeenCalledWith('game-root-2');
// 内嵌模式不渲染族谱页外壳(页头/返回按钮),但保留「全屏查看」入口。
- expect(screen.getAllByRole('button', { name: '全屏查看族谱' })).toHaveLength(
- 2,
- );
+ expect(
+ screen.getAllByRole('button', { name: '全屏查看族谱' }),
+ ).toHaveLength(2);
fireEvent.click(
screen.getAllByRole('button', { name: '全屏查看族谱' })[0]!,
);
@@ -1980,6 +1978,202 @@ describe('GameDetailPage 共创主题入口', () => {
});
});
+describe('GameLineagePage 可视化与交互', () => {
+ const treeWithSibling = {
+ rootGameId: 'game-root',
+ root: createLineageNode({
+ gameId: 'game-root',
+ title: '母版',
+ generation: 0,
+ parentGameId: null,
+ }),
+ nodes: [
+ createLineageNode({
+ gameId: 'game-root',
+ title: '母版',
+ generation: 0,
+ parentGameId: null,
+ }),
+ createLineageNode({
+ gameId: 'game-mid',
+ title: '中间代',
+ generation: 1,
+ parentGameId: 'game-root',
+ }),
+ createLineageNode({
+ gameId: 'game-leaf',
+ title: '叶节点',
+ generation: 2,
+ parentGameId: 'game-mid',
+ }),
+ createLineageNode({
+ gameId: 'game-other',
+ title: '旁支',
+ generation: 1,
+ parentGameId: 'game-root',
+ }),
+ ],
+ truncated: false,
+ };
+
+ it('节点是真实 DOM 卡片:点击进详情、Enter 等价、当前作品有标记且父在上子在下', async () => {
+ getGameLineageMock.mockResolvedValueOnce(treeWithSibling);
+ const onOpenDetail = vi.fn();
+ render(
+ ,
+ );
+
+ const leafCard = await screen.findByRole('button', { name: /《叶节点》/u });
+ const rootCard = screen.getByRole('button', { name: /《母版》/u });
+ // 当前作品标记
+ expect(leafCard.getAttribute('aria-current')).toBe('true');
+ expect(rootCard.getAttribute('aria-current')).toBeNull();
+ expect(within(leafCard).getByText('当前作品')).toBeTruthy();
+ // 分层:子节点在父节点下方
+ expect(parseFloat(leafCard.style.top)).toBeGreaterThan(
+ parseFloat(rootCard.style.top),
+ );
+ // 贝塞尔连线
+ const paths = document.querySelectorAll('path.game-lineage-edge');
+ expect(paths.length).toBeGreaterThan(0);
+ expect(paths[0]!.getAttribute('d')).toContain(' C ');
+ // 键盘 Enter 与点击等价
+ fireEvent.keyDown(leafCard, { key: 'Enter' });
+ expect(onOpenDetail).toHaveBeenCalledWith('game-leaf');
+ fireEvent.click(rootCard);
+ expect(onOpenDetail).toHaveBeenCalledWith('game-root');
+ });
+
+ it('hover 一个节点高亮祖先路径与子树,其余节点降透明;移开后恢复', async () => {
+ getGameLineageMock.mockResolvedValueOnce(treeWithSibling);
+ render(
+ ,
+ );
+
+ const midCard = await screen.findByRole('button', { name: /《中间代》/u });
+ const rootCard = screen.getByRole('button', { name: /《母版》/u });
+ const leafCard = screen.getByRole('button', { name: /《叶节点》/u });
+ const otherCard = screen.getByRole('button', { name: /《旁支》/u });
+
+ fireEvent.mouseEnter(midCard);
+ // 祖先(母版)+ 自身 + 子树(叶节点)都不降透明,旁支降透明。
+ expect(rootCard.className).not.toContain('game-lineage-node--dimmed');
+ expect(midCard.className).not.toContain('game-lineage-node--dimmed');
+ expect(leafCard.className).not.toContain('game-lineage-node--dimmed');
+ expect(otherCard.className).toContain('game-lineage-node--dimmed');
+ // 连线同样有活跃/降透明区分。
+ expect(
+ document.querySelectorAll('path.game-lineage-edge--active').length,
+ ).toBeGreaterThan(0);
+ expect(
+ document.querySelectorAll('path.game-lineage-edge--dimmed').length,
+ ).toBeGreaterThan(0);
+
+ fireEvent.mouseLeave(midCard);
+ expect(otherCard.className).not.toContain('game-lineage-node--dimmed');
+ expect(
+ document.querySelectorAll('path.game-lineage-edge--dimmed').length,
+ ).toBe(0);
+ });
+
+ it('缩放按钮改变百分比,「适应窗口」重新适配', async () => {
+ getGameLineageMock.mockResolvedValueOnce(treeWithSibling);
+ render(
+ ,
+ );
+
+ await screen.findByRole('button', { name: /《母版》/u });
+ const zoomLabel = () => screen.getByRole('status').textContent ?? '';
+ const before = Number.parseInt(zoomLabel(), 10);
+ fireEvent.click(screen.getByRole('button', { name: '放大' }));
+ expect(Number.parseInt(zoomLabel(), 10)).toBeGreaterThan(before);
+ fireEvent.click(screen.getByRole('button', { name: '缩小' }));
+ expect(Number.parseInt(zoomLabel(), 10)).toBe(before);
+ fireEvent.click(screen.getByRole('button', { name: '适应窗口' }));
+ expect(zoomLabel()).toMatch(/%$/u);
+ });
+
+ it('节点的 Fork 次要动作不触发导航;未失焦时给出降级提示与复制入口', async () => {
+ getGameLineageMock.mockResolvedValueOnce(treeWithSibling);
+ const onOpenDetail = vi.fn();
+ render(
+ ,
+ );
+
+ const leafCard = await screen.findByRole('button', { name: /《叶节点》/u });
+ const forkButton = within(leafCard).getByRole('button', { name: 'Fork' });
+ vi.useFakeTimers();
+ fireEvent.click(forkButton);
+ // 次要动作直通唤起,且不抢主点击。
+ expect(openForkProtocolMock).toHaveBeenCalledWith('game-leaf');
+ expect(onOpenDetail).not.toHaveBeenCalled();
+
+ await act(async () => {
+ vi.advanceTimersByTime(FORK_LAUNCH_FALLBACK_DELAY_MS + 10);
+ });
+ expect(screen.getByText(/没有检测到陶泥儿客户端/u)).toBeTruthy();
+ fireEvent.click(
+ screen.getByRole('button', { name: /复制 game-leaf 去 Fork/u }),
+ );
+ await act(async () => {});
+ expect(copyTextToClipboardMock).toHaveBeenCalledWith('game-leaf');
+ vi.useRealTimers();
+ });
+
+ it('不可用父节点:可聚焦、不可点进详情,且不出现被删作品的标题与作者名', async () => {
+ getGameLineageMock.mockResolvedValueOnce({
+ rootGameId: 'game-root',
+ root: null,
+ nodes: [
+ createLineageNode({
+ gameId: 'game-leaf',
+ title: '叶节点',
+ generation: 1,
+ parentGameId: 'game-deleted',
+ }),
+ ],
+ truncated: false,
+ });
+ const onOpenDetail = vi.fn();
+ render(
+ ,
+ );
+
+ const stubTitle = await screen.findByText('原作品已不可用');
+ const stubCard = stubTitle.closest('.game-lineage-node') as HTMLElement;
+ expect(stubCard.getAttribute('tabindex')).toBe('0');
+ expect(stubCard.getAttribute('role')).toBeNull();
+ expect(stubCard.getAttribute('aria-disabled')).toBe('true');
+ // 被删作品只以「不可用」形式出现:没有标题、没有作者名、也没有 Fork/对比动作。
+ expect(document.querySelector('.game-lineage-node--stub')).toBeTruthy();
+ expect(screen.queryByText(/作者 game-deleted/u)).toBeNull();
+ expect(within(stubCard).queryByRole('button')).toBeNull();
+ fireEvent.click(stubCard);
+ expect(onOpenDetail).not.toHaveBeenCalled();
+ });
+});
+
describe('GameDetailPage 收藏按钮', () => {
it('可判定为已收藏时显示「已收藏」,点击取消收藏', async () => {
getGameMock.mockResolvedValueOnce(createGame({ collected: true }));
diff --git a/src/components/game-distribution/GameLineagePage.tsx b/src/components/game-distribution/GameLineagePage.tsx
index edee00007..9333af468 100644
--- a/src/components/game-distribution/GameLineagePage.tsx
+++ b/src/components/game-distribution/GameLineagePage.tsx
@@ -1,23 +1,42 @@
import './gameDistribution.css';
-import { ArrowLeft, RefreshCcw } from 'lucide-react';
-import { useCallback, useEffect, useMemo, useState } from 'react';
+import { ArrowLeft, Expand, Minus, Plus, RefreshCcw } from 'lucide-react';
+import {
+ type PointerEvent as ReactPointerEvent,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+ type WheelEvent as ReactWheelEvent,
+} from 'react';
import {
- type GameDistributionLineageNode,
type GameDistributionLineageResponse,
getGameLineage,
} from '../../services/gameDistributionClient';
+import { CopyFeedbackButton } from '../common/CopyFeedbackButton';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformEmptyState } from '../common/PlatformEmptyState';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
+import { useCopyFeedback } from '../common/useCopyFeedback';
+import {
+ FORK_LAUNCH_FALLBACK_DELAY_MS,
+ openForkProtocol,
+} from './gameForkProtocol';
+import {
+ buildLineageTreeLayout,
+ collectLineageHighlight,
+ generationAxisLabel,
+ type LineageLayoutNode,
+} from './lineageTreeLayout';
type GameLineagePageProps = {
/** 路由 `?id=` 里的根作品 ID;为空时给出明确的无效编号提示,不发请求。 */
rootGameId: string | null;
/**
- * 用户是从作品详情页的哪个节点进来的,用来高亮「当前作品」。
- * 缺失时退回高亮根节点(直接打开族谱链接的场景)。
+ * 用户是从作品详情页的哪个节点进来的,用来标记「当前作品」。
+ * 缺失时退回标记根节点(直接打开族谱链接的场景)。
*/
highlightGameId?: string | null;
onBack: () => void;
@@ -25,101 +44,151 @@ type GameLineagePageProps = {
/** 打开「原版与二创对比」;参数是原版(父作品)与二创(该节点)。 */
onOpenCompare?: (leftGameId: string, rightGameId: string) => void;
/**
- * 内嵌模式:隐藏页面外壳(返回按钮 / 页头 / 脚注),只渲染树本身。
+ * 内嵌模式:隐藏页面外壳(返回按钮 / 页头 / 脚注),只渲染可视化树本身。
* 供主题页逐根渲染多棵树复用同一份树查询与渲染(不新建第二套树逻辑)。
*/
embedded?: boolean;
};
+const MIN_SCALE = 0.35;
+const MAX_SCALE = 1.8;
+const FIT_PADDING = 24;
+
+type ViewportState = { scale: number; offsetX: number; offsetY: number };
+
/**
- * 按代际给节点分组;节点在服务端已按「代际升序、同代创建时间升序」排好,
- * 这里只做分组,不重新排序。
+ * 一个树节点:真实 HTML 卡片(可聚焦、可点击、有可访问名称),不是 SVG 矩形。
+ *
+ * 交互分工:主点击(含键盘 Enter/Space)进作品详情;次要动作 Fork 放在卡片内部的独立
+ * `
) : null}
{lineage.root ? null : (
-
+
母版已不可用,下面按代际关系展示仍可阅读的衍生作品。
)}
- {/*
- 空态只有一条,避免「双重空态」:`nodes` 为空说明整棵树没有任何可见节点
- (连母版都不可读),此时只给整体空态;只有「有节点但没有任何衍生」时才给衍生空态。
- 两者互斥,不会同时出现。
- */}
{nodes.length === 0 ? (
这部作品还没有可展示的族谱节点。
) : (
<>
-
- {groups.map(([generation, groupNodes]) => (
-
+
+
+
+ {layout.axis.map((entry) => (
+
+
+ {entry.label}
-
-
- {groupNodes.map((node) => (
-
- ))}
-
-
- ))}
+
+ {entry.count}
+
+
+ ))}
+ {layout.nodes.map((layoutNode) => (
+
setActiveGameId('')}
+ onOpenFork={launchForkFromNode}
+ />
+ ))}
+
{derivedCount === 0 ? (
@@ -295,7 +631,8 @@ export function GameLineagePage({
{embedded ? null : (
- 族谱只展示已公开的作品;已删除或未公开的来源作品会标注为「原作品已不可用」。
+ 拖拽平移、滚轮或按钮缩放;悬停或键盘聚焦一个节点会高亮它的祖先路径。已删除或未公开的
+ 来源作品只显示「原作品已不可用」。
)}
diff --git a/src/components/game-distribution/gameDistribution.css b/src/components/game-distribution/gameDistribution.css
index 85ad651db..16fe67916 100644
--- a/src/components/game-distribution/gameDistribution.css
+++ b/src/components/game-distribution/gameDistribution.css
@@ -723,6 +723,175 @@
color: var(--platform-accent-strong);
font-weight: 700;
}
+
+/*
+ * 族谱树可视化:一个可平移/缩放的画布 + 内容层(CSS transform)。
+ * 连线在 SVG 里(坐标就是内容层坐标,随内容层一起缩放),节点是绝对定位的真实 HTML 卡片
+ * —— 因为节点要真的可点击 / 可聚焦 / 可读屏,不能画成 SVG 矩形。
+ */
+.game-lineage-viewport-controls {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.5rem;
+}
+.game-lineage-toggle {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.35rem;
+ color: var(--platform-text-soft);
+ font-size: 0.78rem;
+}
+.game-lineage-fork-notice {
+ color: var(--platform-text-soft);
+ font-size: 0.78rem;
+ overflow-wrap: anywhere;
+}
+.game-lineage-fork-copy {
+ border: 1px solid var(--platform-surface-border);
+ border-radius: 999px;
+ padding: 0.2rem 0.7rem;
+ background: transparent;
+ color: var(--platform-text-base);
+ font: inherit;
+ font-size: 0.74rem;
+ font-weight: 600;
+ cursor: pointer;
+}
+.game-lineage-canvas {
+ position: relative;
+ height: clamp(22rem, 62vh, 46rem);
+ max-width: 100%;
+ overflow: hidden;
+ border: 1px solid var(--platform-surface-border);
+ border-radius: 0.85rem;
+ background: radial-gradient(
+ circle at 1px 1px,
+ var(--platform-surface-border) 1px,
+ transparent 0
+ )
+ 0 0 / 22px 22px;
+ cursor: grab;
+ /* 拖拽平移不要触发浏览器自身的滚动/缩放。 */
+ touch-action: none;
+}
+.game-lineage-canvas:active {
+ cursor: grabbing;
+}
+.game-lineage-canvas__content {
+ position: absolute;
+ top: 0;
+ left: 0;
+ transform-origin: 0 0;
+ transition: transform 180ms ease-out;
+ will-change: transform;
+}
+.game-lineage-canvas__edges {
+ position: absolute;
+ top: 0;
+ left: 0;
+ overflow: visible;
+ pointer-events: none;
+}
+.game-lineage-edge {
+ fill: none;
+ stroke: var(--platform-surface-border);
+ stroke-width: 2;
+ transition:
+ stroke 160ms ease,
+ opacity 160ms ease;
+}
+.game-lineage-edge--active {
+ stroke: var(--platform-accent-strong);
+ stroke-width: 2.5;
+}
+.game-lineage-edge--dimmed {
+ opacity: 0.18;
+}
+.game-lineage-axis {
+ position: absolute;
+ left: 0;
+ display: flex;
+ align-items: center;
+ gap: 0.3rem;
+ color: var(--platform-text-soft);
+ font-size: 0.74rem;
+ font-weight: 700;
+ white-space: nowrap;
+}
+.game-lineage-axis__count {
+ border-radius: 999px;
+ padding: 0.05rem 0.45rem;
+ background: var(--platform-surface-border);
+ color: var(--platform-text-soft);
+ font-size: 0.68rem;
+ font-weight: 600;
+}
+.game-lineage-node {
+ position: absolute;
+ box-sizing: border-box;
+ cursor: pointer;
+ transition:
+ opacity 160ms ease,
+ box-shadow 160ms ease;
+}
+.game-lineage-node--dimmed {
+ opacity: 0.34;
+}
+.game-lineage-node--stub {
+ border-style: dashed;
+ cursor: default;
+}
+.game-lineage-node:focus-visible {
+ outline: 2px solid var(--platform-accent-strong);
+ outline-offset: 2px;
+}
+.game-lineage-node__title {
+ color: var(--platform-text-strong);
+ font-size: 0.9rem;
+ font-weight: 700;
+ overflow-wrap: anywhere;
+}
+.game-lineage-node__actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.3rem;
+}
+.game-lineage-node__fork,
+.game-lineage-node__compare {
+ border: 1px solid var(--platform-surface-border);
+ border-radius: 999px;
+ padding: 0.14rem 0.6rem;
+ background: transparent;
+ color: var(--platform-text-base);
+ font: inherit;
+ font-size: 0.72rem;
+ font-weight: 600;
+ cursor: pointer;
+}
+.game-lineage-node__fork:hover,
+.game-lineage-node__compare:hover {
+ color: var(--platform-text-strong);
+}
+.game-lineage-zoom {
+ color: var(--platform-text-soft);
+ font-size: 0.76rem;
+ font-variant-numeric: tabular-nums;
+}
+/* 尊重「减少动效」偏好:只保留状态变化,不做过渡动画。 */
+@media (prefers-reduced-motion: reduce) {
+ .game-lineage-canvas__content,
+ .game-lineage-node,
+ .game-lineage-edge {
+ transition: none;
+ }
+}
+/* 窄屏:节点尺寸适合触摸,画布可横向平移(内容层平移而不是页面溢出)。 */
+@media (max-width: 720px) {
+ .game-lineage-canvas {
+ height: clamp(20rem, 56vh, 34rem);
+ }
+}
.game-loading-panel {
display: grid;
min-height: 18rem;
@@ -1507,40 +1676,6 @@
font-size: 0.78rem;
cursor: pointer;
}
-.game-lineage-tree {
- display: flex;
- flex-direction: column;
- gap: 1rem;
-}
-.game-lineage-generation {
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
-}
-.game-lineage-generation__label {
- display: inline-flex;
- align-items: center;
- gap: 0.4rem;
- margin: 0;
- color: var(--platform-text-strong);
- font-size: 0.9rem;
- font-weight: 700;
-}
-.game-lineage-generation__count {
- border-radius: 999px;
- padding: 0.05rem 0.5rem;
- background: var(--platform-surface-border);
- color: var(--platform-text-soft);
- font-size: 0.72rem;
- font-weight: 600;
-}
-.game-lineage-generation__nodes {
- display: grid;
- /* `min(13rem, 100%)`:桌面端列宽不变,视口窄于 13rem 时下限跟着收缩,
- 避免固定下限把内容撑出容器产生横向溢出。 */
- grid-template-columns: repeat(auto-fill, minmax(min(13rem, 100%), 1fr));
- gap: 0.6rem;
-}
.game-lineage-node {
display: flex;
flex-direction: column;
diff --git a/src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx b/src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx
index c11cca556..7f594d87c 100644
--- a/src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx
+++ b/src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx
@@ -1383,7 +1383,7 @@ describe('创作族谱', () => {
expect(gameDistributionMock.getGameLineage).toHaveBeenCalledWith(
'game-root',
);
- fireEvent.click(screen.getByRole('button', { name: '母版' }));
+ fireEvent.click(screen.getByRole('button', { name: /《母版》/u }));
expect(setSelectionStage).toHaveBeenCalledWith('game-detail', {
path: '/games/detail?id=game-root',
});
@@ -1598,7 +1598,9 @@ describe('共创主题', () => {
/>,
);
- expect(await screen.findByRole('heading', { name: '精选共创' })).toBeTruthy();
+ expect(
+ await screen.findByRole('heading', { name: '精选共创' }),
+ ).toBeTruthy();
expect(screen.getByText('根作品')).toBeTruthy();
expect(gameDistributionMock.getTheme).toHaveBeenCalledWith('theme-1');
// 树复用既有族谱查询:按根请求一次。