修复 M3 复核问题(第一批):族谱页双重空态、节点上限常量未钉住、窄屏网格溢出

- 族谱页空态收敛为一条:nodes 为空时只给整体空态(这部作品还没有可展示的族谱节点),有节点但无衍生时才给衍生空态,两者互斥不再同时渲染
- 新增两条组件用例覆盖该组合:空树只出现一条空态且无 .game-lineage-tree;只有母版时渲染母版节点 + 衍生空态且不出现整体空态
- 纯函数测试改用 GAME_DISTRIBUTION_LINEAGE_NODE_LIMIT 常量,并新增 node_limit_constant_is_pinned_and_governs_truncation:同时钉住常量值 200(合同值)与满量/超量时的 truncated 行为,改常量或改截断实现都必须过门
- LineageTreeNode 文档写明作者名空值语义:空串/纯空白归一为 None 与 M1 的 fork_lineage_visible_identity 三态刻意保留属有意为之,两端展示层按 falsy 判断、行为等价
- 族谱节点网格列下限改为 min(13rem, 100%):桌面列宽不变,极窄视口不再横向溢出
- 未动涉及 M2a session 的 spacetime-module / shared-contracts 注释与 lineage_entries 图装配测试,留待第二批
This commit is contained in:
2026-10-05 01:17:37 +08:00
parent 39669763fe
commit 1ce3adb8d0
7 changed files with 275 additions and 56 deletions
@@ -1022,6 +1022,67 @@ describe('GameLineagePage', () => {
).toBeTruthy();
});
it('没有任何可见节点时只渲染一条整体空态,不叠加衍生空态', async () => {
getGameLineageMock.mockResolvedValueOnce({
rootGameId: 'game-root',
root: null,
nodes: [],
truncated: false,
});
render(
<GameLineagePage
rootGameId="game-root"
onBack={vi.fn()}
onOpenDetail={vi.fn()}
/>,
);
expect(
await screen.findByText('这部作品还没有可展示的族谱节点。'),
).toBeTruthy();
// 双重空态回归:整体空态已覆盖「没有节点」,不能再叠一条衍生空态。
expect(
screen.queryByText('这部作品还没有衍生作品,欢迎从它开始共创。'),
).toBeNull();
expect(document.querySelectorAll('.game-lineage-tree')).toHaveLength(0);
});
it('只有母版时渲染母版节点并只给衍生空态,不给整体空态', async () => {
getGameLineageMock.mockResolvedValueOnce({
rootGameId: 'game-root',
root: createLineageNode({
gameId: 'game-root',
title: '母版',
generation: 0,
parentGameId: null,
}),
nodes: [
createLineageNode({
gameId: 'game-root',
title: '母版',
generation: 0,
parentGameId: null,
}),
],
truncated: false,
});
render(
<GameLineagePage
rootGameId="game-root"
onBack={vi.fn()}
onOpenDetail={vi.fn()}
/>,
);
expect(await screen.findByRole('button', { name: '母版' })).toBeTruthy();
expect(
screen.getByText('这部作品还没有衍生作品,欢迎从它开始共创。'),
).toBeTruthy();
expect(
screen.queryByText('这部作品还没有可展示的族谱节点。'),
).toBeNull();
});
it('超出节点上限时如实标注截断,不静默丢弃', async () => {
getGameLineageMock.mockResolvedValueOnce({
rootGameId: 'game-root',
@@ -211,47 +211,54 @@ export function GameLineagePage({
母版已不可用,下面按代际关系展示仍可阅读的衍生作品。
</p>
)}
{groups.length === 0 ? (
{/*
空态只有一条,避免「双重空态」:`nodes` 为空说明整棵树没有任何可见节点
(连母版都不可读),此时只给整体空态;只有「有节点但没有任何衍生」时才给衍生空态。
两者互斥,不会同时出现。
*/}
{nodes.length === 0 ? (
<PlatformEmptyState surface="subpanel" size="panel">
<span>这部作品还没有可展示的族谱节点。</span>
</PlatformEmptyState>
) : (
<div className="game-lineage-tree">
{groups.map(([generation, groupNodes]) => (
<section
key={generation}
className="game-lineage-generation"
aria-label={generationLabel(generation)}
>
<h3 className="game-lineage-generation__label">
{generationLabel(generation)}
<span className="game-lineage-generation__count">
{groupNodes.length}
</span>
</h3>
<div className="game-lineage-generation__nodes">
{groupNodes.map((node) => (
<LineageNodeCard
key={node.gameId}
node={node}
isAnchor={node.gameId === anchorId}
hasVisibleParent={
!node.parentGameId ||
visibleIds.has(node.parentGameId)
}
onOpenDetail={onOpenDetail}
/>
))}
</div>
</section>
))}
</div>
<>
<div className="game-lineage-tree">
{groups.map(([generation, groupNodes]) => (
<section
key={generation}
className="game-lineage-generation"
aria-label={generationLabel(generation)}
>
<h3 className="game-lineage-generation__label">
{generationLabel(generation)}
<span className="game-lineage-generation__count">
{groupNodes.length}
</span>
</h3>
<div className="game-lineage-generation__nodes">
{groupNodes.map((node) => (
<LineageNodeCard
key={node.gameId}
node={node}
isAnchor={node.gameId === anchorId}
hasVisibleParent={
!node.parentGameId ||
visibleIds.has(node.parentGameId)
}
onOpenDetail={onOpenDetail}
/>
))}
</div>
</section>
))}
</div>
{derivedCount === 0 ? (
<PlatformEmptyState surface="subpanel" size="panel">
<span>这部作品还没有衍生作品,欢迎从它开始共创。</span>
</PlatformEmptyState>
) : null}
</>
)}
{derivedCount === 0 ? (
<PlatformEmptyState surface="subpanel" size="panel">
<span>这部作品还没有衍生作品,欢迎从它开始共创。</span>
</PlatformEmptyState>
) : null}
</>
) : null}
@@ -1258,7 +1258,9 @@
}
.game-lineage-generation__nodes {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
/* `min(13rem, 100%)`:桌面端列宽不变,视口窄于 13rem 时下限跟着收缩,
避免固定下限把内容撑出容器产生横向溢出。 */
grid-template-columns: repeat(auto-fill, minmax(min(13rem, 100%), 1fr));
gap: 0.6rem;
}
.game-lineage-node {