Files
Genarrative/apps/ai-game-creator-shell/tests/resourceBookLayout.test.ts
T
suzmii 9788248d9b 「所有资源」页改走画本场景:与栏目页同一套卡片宿主与几何,并可拖动
- 新增 resourceBookShowsAllCategories:把"子画布里的 all"这一态判定收在 resourceBookModel 一处
- resourceBookCategoryCardPresentation:展开态下真实栏目返回 child(卡片仍由各栏目组承载,每张资源只有一个宿主)、all 自己返回 null(只出标题栏与全量计数)
- 新增纯函数 buildResourceBookAllLayout:按栏目顺序纵向排"分带",用各栏目可见资源的 extent 定带宽高;卡片世界坐标 = 栏目内局部坐标 + 带原点
- 新增 resourceBookAllTitlebarRects:分带 → 栏目标题栏矩形,让展开态复用总览态"按矩形钉标题栏"的同一条渲染路径(ResourceBookScene 不需要知道"展开态"这个概念)
- 新增 resourceBookAllBandLocalPoint:展开态世界坐标 → 栏目内局部坐标,这是落盘前唯一的换算(减带原点)
- buildResourceBookScenePlan:展开态下 all 只出标题栏(钉在视口 + 全量计数),真实栏目按带偏移铺满 child 卡;真实栏目这一支仍按 presentation === child 取卡,"能出卡的判定"只有一处
- buildResourceBookScenePlan:返回总览的淡出改为按"刚离开的那段子画布"判定(exitingCategory 传 all 时覆盖所有栏目),并用带偏移位置淡出,否则返回动画会跳位置
- buildResourceBookScenePlan:「所有资源」组不再接 child / exiting 卡,避免同一张卡在两组里各挂一份(DOM 查询与 FLIP 键撞车)
- index.tsx:删掉独立网格页组件、其 host 块与 resourceBookAllResourcesSections memo,展开态整体交给画本场景
- index.tsx:新增内存态 resourceBookAllViewport(不落盘、不进按真实栏目键的 resourceCanvasViewports),滚轮 / 缩放 / 适应 / 平移 / 框选都作用到它
- index.tsx:分带几何按"项目 + 排序 + 可见资源集"冻结(签名 + ref 短路;签名不含 state.token——返回总览时 token 也会变,含它会在淡出期间重算),拖动不重算带高,避免"拖上面一带让后面所有带整体位移"
- index.tsx:拖动放开——展开态下每张卡都能拖,section 取 resource.category(栏目页仍只拖当前分页栏目)
- index.tsx:提交前用 resourceBookAllBandLocalPoint 把世界坐标减回带原点,写回的仍是同一 mode 同一 sidecar 的同一 section(.agent/workbench/resource-layouts/{dependency,type}.json),没有第二份空间,不改契约与 schema
- index.tsx:renderResourceBookCard 去掉 options.draggable:展开态已可拖,这个"只读"开关没有调用方了
- 测试:resourceBookModel.test 钉住展开态 presentation 与"栏目页仍是原判据";resourceBookLayout.test 退役旧分组函数用例,新增分带几何 / 标题栏矩形 / 减带原点换算 / 展开态计划(含"每张资源只有一个宿主"与按带位置淡出)断言
- 测试:appSurface 两处「所有资源」判据从旧网格宿主改为画本场景(.game-resource-book-scene-card[data-resource-book-category] 与 .is-active[data-resource-book-category="all"]),并断言旧网格容器 / 宿主一个都不再存在
2026-09-11 23:33:08 +08:00

633 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest';
import {
buildResourceBookAllLayout,
buildResourceBookScenePlan,
groupResourceBookStacks,
RESOURCE_BOOK_ALL_BAND_TITLE_HEIGHT,
RESOURCE_BOOK_OVERVIEW_STACK_LIMIT,
type ResourceBookAllBand,
resourceBookAllBandLocalPoint,
type ResourceBookAllLayout,
resourceBookAllTitlebarRects,
resourceBookOverviewCardLayout,
type ResourceBookOverviewRect,
resourceBookSceneCardKey,
selectResourceBookOverviewCards,
} from '../src/view/project-development/resourceBookLayout';
import {
RESOURCE_BOOK_ALL_TARGET,
type ResourceBookCategory,
type ResourceBookTarget,
} from '../src/view/project-development/resourceBookModel';
import type { ProjectResource } from '../src/view/project-development/resourceProjectionModel';
function resource(
id: string,
category: ProjectResource['category'] = 'unclassified',
): ProjectResource {
return {
id,
category,
subtype: 'image',
label: id,
path: `assets/${id}.png`,
mediaType: 'image/png',
sourceLabel: '测试',
taskTitle: null,
manifestAssetId: id,
producerTaskId: null,
externalResourceId: null,
referenceResourceIds: [],
dependencies: [],
dependencyDepth: 0,
};
}
const rect = { left: 100, top: 200, width: 280, height: 210 };
describe('resourceBookOverviewCardLayout', () => {
it('keeps the stack tilt bounded however many resources share one type', () => {
const rotations = [0, 1, 2, 3, 12, 59].map(
(stackIndex) =>
resourceBookOverviewCardLayout({ rect, stackIndex, stackColumn: 0 })
.rotation,
);
for (const rotation of rotations) {
expect(Math.abs(rotation)).toBeLessThanOrEqual(3);
}
expect(rotations[0]).toBe(0.5);
expect(rotations.at(-1)).toBe(rotations[2]);
});
it('stops offsetting deeper cards once the visible stack depth is reached', () => {
const positions = [0, 1, 2, 3, 59].map(
(stackIndex) =>
resourceBookOverviewCardLayout({ rect, stackIndex, stackColumn: 0 }).y,
);
expect(positions[0]).toBe(rect.top + 58);
expect(positions[1]).toBe(rect.top + 63);
expect(positions[2]).toBe(rect.top + 68);
expect(positions.at(-1)).toBe(positions[2]);
});
it('limits how many cards the overview renders per type stack', () => {
expect(RESOURCE_BOOK_OVERVIEW_STACK_LIMIT).toBe(3);
});
describe('selectResourceBookOverviewCards', () => {
const cards = ['a', 'b', 'c', 'd'].map(resource);
const positions = new Map([
['a', { x: 0, y: 0 }],
['b', { x: 1_000, y: 0 }],
['c', { x: 2_000, y: 0 }],
['d', { x: 3_000, y: 0 }],
]);
const cardSizes = new Map<string, { width: number; height: number }>();
it('keeps the first visible cards when the view center is unknown', () => {
expect(
selectResourceBookOverviewCards({
resources: cards,
visibleResourceIds: new Set(['a', 'b', 'c', 'd']),
positions,
cardSizes,
viewCenter: undefined,
}).map((item) => item.id),
).toEqual(['a', 'b', 'c']);
});
it('prefers the cards nearest the current view center', () => {
expect(
selectResourceBookOverviewCards({
resources: cards,
visibleResourceIds: new Set(['a', 'b', 'c', 'd']),
positions,
cardSizes,
viewCenter: { x: 3_000, y: 64 },
}).map((item) => item.id),
).toEqual(['d', 'c', 'b']);
});
it('never surfaces cards hidden by the current filter', () => {
expect(
selectResourceBookOverviewCards({
resources: cards,
visibleResourceIds: new Set(['a', 'c', 'd']),
positions,
cardSizes,
viewCenter: { x: 1_000, y: 64 },
}).map((item) => item.id),
).toEqual(['a', 'c', 'd']);
});
});
});
describe('groupResourceBookStacks', () => {
it('keeps resources of one type in a single stack', () => {
const stacks = groupResourceBookStacks([
resource('overview-art-0'),
resource('overview-art-1'),
resource('overview-art-2'),
resource('overview-art-3'),
]);
expect(stacks).toHaveLength(1);
expect(stacks[0]?.[1]).toHaveLength(4);
});
});
describe('buildResourceBookScenePlan', () => {
const resources = ['art-0', 'art-1', 'art-2', 'art-3'].map(resource);
const positions = new Map(
resources.map((item, index) => [item.id, { x: index * 200, y: 0 }]),
);
const cardSizes = new Map(
resources.map((item) => [item.id, { width: 180, height: 128 }]),
);
const base = {
visibleCategoryOrder: ['unclassified'] as const,
resourcesByCategory: new Map([['unclassified', resources]]),
overviewRects: new Map([['unclassified', rect]]),
positions,
cardSizes,
visibleResourceIds: new Set(resources.map((item) => item.id)),
categoryViewCenters: new Map([['unclassified', { x: 0, y: 64 }]]),
cardsReady: true,
};
it('renders the bounded overview stack on the steady main canvas', () => {
const plan = buildResourceBookScenePlan({
...base,
state: {
view: 'main',
category: null,
phase: 'idle',
token: 0,
},
});
expect(plan).toHaveLength(1);
expect(plan[0]!.cards).toHaveLength(RESOURCE_BOOK_OVERVIEW_STACK_LIMIT);
expect(plan[0]!.cards[0]!.presentation).toBe('overview');
expect(plan[0]!.cards[0]!.stackCount).toBe(
RESOURCE_BOOK_OVERVIEW_STACK_LIMIT,
);
expect(plan[0]!.cards[0]!.layout).toMatchObject({
x: rect.left + 18,
y: rect.top + 58,
width: 92,
height: 64,
});
expect(plan[0]!.titlebarActive).toBe(false);
expect(plan[0]!.titlebarOpacity).toBe(1);
expect(plan[0]!.resourceCount).toBe(4);
});
it('keeps other categories mounted for a fade-out while entering', () => {
const plan = buildResourceBookScenePlan({
...base,
visibleCategoryOrder: ['unclassified', 'scene'],
resourcesByCategory: new Map([
['unclassified', resources],
['scene', [resource('code-0')]],
]),
overviewRects: new Map([
['unclassified', rect],
['scene', { ...rect, left: 500 }],
]),
state: {
view: 'child',
category: 'unclassified',
phase: 'entering',
token: 1,
},
});
const artGroup = plan.find((group) => group.category === 'unclassified')!;
const codeGroup = plan.find((group) => group.category === 'scene')!;
expect(artGroup.cards.every((card) => card.presentation === 'child')).toBe(
true,
);
expect(artGroup.cards).toHaveLength(4);
expect(artGroup.titlebarActive).toBe(true);
// 其他栏目留在 DOM 里淡出,而不是直接卸载。
expect(
codeGroup.cards.every((card) => card.presentation === 'overview'),
).toBe(true);
expect(codeGroup.titlebarActive).toBe(false);
expect(codeGroup.titlebarOpacity).toBe(0);
});
it('marks cards that do not return to the stack as exiting', () => {
const plan = buildResourceBookScenePlan({
...base,
state: {
view: 'main',
category: null,
phase: 'returning-main',
token: 2,
},
exitingCategory: 'unclassified',
});
const cards = plan[0]!.cards;
expect(
cards.filter((card) => card.presentation === 'overview'),
).toHaveLength(RESOURCE_BOOK_OVERVIEW_STACK_LIMIT);
const exiting = cards.filter((card) => card.presentation === 'exiting');
expect(exiting).toHaveLength(1);
// 淡出节点按子画布布局渲染,位置由 FLIP 钉在 First。
expect(exiting[0]!.layout.x).toBe(positions.get('art-3')!.x);
});
it('keeps search-driven removals mounted without duplicating keys', () => {
const plan = buildResourceBookScenePlan({
...base,
state: { view: 'main', category: null, phase: 'idle', token: 0 },
exitingCards: [
{
key: resourceBookSceneCardKey('art-3'),
category: 'unclassified',
resource: resources[3]!,
presentation: 'exiting',
stackIndex: 0,
stackColumn: 0,
stackCount: 1,
layout: {
x: 0,
y: 0,
width: 180,
height: 128,
dragX: 0,
dragY: 0,
rotation: 0,
},
},
{
key: resourceBookSceneCardKey('art-0'),
category: 'unclassified',
resource: resources[0]!,
presentation: 'exiting',
stackIndex: 0,
stackColumn: 0,
stackCount: 1,
layout: {
x: 0,
y: 0,
width: 180,
height: 128,
dragX: 0,
dragY: 0,
rotation: 0,
},
},
],
});
const keys = plan[0]!.cards.map((card) => card.key);
expect(keys).toContain(resourceBookSceneCardKey('art-3'));
expect(
keys.filter((key) => key === resourceBookSceneCardKey('art-0')),
).toHaveLength(1);
});
/**
* 资源总览第 0 张「所有资源」卡走同一条渲染管线:标题栏的计数是全量资源数,
* 预览摞则是全量投影按稳定顺序的前 N 张,且只铺一列(跨类型的一摞)。
*
* 它是同一张资源卡的第二份宿主(各栏目摞里仍各有一份),所以这里既钉预览的来源与
* 上限,也钉各栏目摞没有被改动。
*/
it('puts the first overview-stack slice of the full projection on the all-resources tile', () => {
const documentResource = resource('design-0', 'document');
const allResources = [...resources, documentResource];
const allPositions = new Map([
...positions,
[documentResource.id, { x: 900, y: 0 }],
]);
const allCardSizes = new Map([
...cardSizes,
[documentResource.id, { width: 180, height: 128 }],
]);
const plan = buildResourceBookScenePlan({
...base,
visibleCategoryOrder: [
RESOURCE_BOOK_ALL_TARGET,
'unclassified',
'document',
],
resourcesByCategory: new Map<ResourceBookTarget, ProjectResource[]>([
[RESOURCE_BOOK_ALL_TARGET, allResources],
['unclassified', resources],
['document', [documentResource]],
]),
overviewRects: new Map<ResourceBookTarget, ResourceBookOverviewRect>([
[RESOURCE_BOOK_ALL_TARGET, { ...rect, left: 0, top: 0 }],
['unclassified', rect],
['document', { ...rect, left: 500 }],
]),
positions: allPositions,
cardSizes: allCardSizes,
visibleResourceIds: new Set(allResources.map((item) => item.id)),
categoryViewCenters: new Map([
['unclassified', { x: 0, y: 64 }],
['document', { x: 900, y: 64 }],
]),
state: { view: 'main', category: null, phase: 'idle', token: 0 },
});
const allGroup = plan.find(
(group) => group.category === RESOURCE_BOOK_ALL_TARGET,
)!;
expect(allGroup.resourceCount).toBe(allResources.length);
expect(allGroup.titlebarActive).toBe(false);
expect(allGroup.titlebarOpacity).toBe(1);
expect(allGroup.cards.map((card) => card.resource.id)).toEqual(
allResources
.slice(0, RESOURCE_BOOK_OVERVIEW_STACK_LIMIT)
.map((item) => item.id),
);
// 跨类型也只铺一列:不然 66 项会一口气铺出十几列。
expect(
Array.from(new Set(allGroup.cards.map((card) => card.stackColumn))),
).toEqual([0]);
expect(
allGroup.cards.every((card) => card.presentation === 'overview'),
).toBe(true);
// 各栏目自己的摞照旧:预览是叠加的一份,不改其它栏目卡。
expect(
plan
.find((group) => group.category === 'document')!
.cards.map((card) => card.resource.id),
).toEqual(['design-0']);
expect(
plan.find((group) => group.category === 'unclassified')!.cards.length,
).toBeGreaterThan(0);
});
it('keeps the all-resources tile preview empty for an empty projection', () => {
const plan = buildResourceBookScenePlan({
...base,
visibleCategoryOrder: [RESOURCE_BOOK_ALL_TARGET],
resourcesByCategory: new Map<ResourceBookTarget, ProjectResource[]>([
[RESOURCE_BOOK_ALL_TARGET, []],
]),
overviewRects: new Map<ResourceBookTarget, ResourceBookOverviewRect>([
[RESOURCE_BOOK_ALL_TARGET, rect],
]),
visibleResourceIds: new Set<string>(),
categoryViewCenters: new Map(),
state: { view: 'main', category: null, phase: 'idle', token: 0 },
});
expect(plan).toHaveLength(1);
expect(plan[0]!.resourceCount).toBe(0);
expect(plan[0]!.cards).toHaveLength(0);
});
/**
* 展开态(子画布里的「所有资源」)与栏目页共用同一份计划:
* 每个真实栏目铺在自己的分带上,`all` 只出钉住的标题栏与全量计数。
*/
it('spreads every real category on its own band and keeps the all group as a titlebar', () => {
const sceneResource = resource('code-0', 'scene');
const allResources = [...resources, sceneResource];
const allPositions = new Map([
...positions,
[sceneResource.id, { x: 40, y: 20 }],
]);
const allCardSizes = new Map([
...cardSizes,
[sceneResource.id, { width: 180, height: 128 }],
]);
const allLayout = buildResourceBookAllLayout({
categoryOrder: ['unclassified', 'scene'],
resourcesByCategory: new Map<ResourceBookCategory, ProjectResource[]>([
['unclassified', resources],
['scene', [sceneResource]],
]),
positions: allPositions,
cardSizes: allCardSizes,
});
const plan = buildResourceBookScenePlan({
...base,
visibleCategoryOrder: [RESOURCE_BOOK_ALL_TARGET, 'unclassified', 'scene'],
resourcesByCategory: new Map<ResourceBookTarget, ProjectResource[]>([
[RESOURCE_BOOK_ALL_TARGET, allResources],
['unclassified', resources],
['scene', [sceneResource]],
]),
overviewRects: resourceBookAllTitlebarRects(allLayout),
positions: allPositions,
cardSizes: allCardSizes,
visibleResourceIds: new Set(allResources.map((item) => item.id)),
state: {
view: 'child',
category: RESOURCE_BOOK_ALL_TARGET,
phase: 'idle',
token: 1,
},
allLayout,
});
const allGroup = plan.find(
(group) => group.category === RESOURCE_BOOK_ALL_TARGET,
)!;
// `all` 一张卡都不挂:卡片由各真实栏目承载,否则同一张卡会挂两份、FLIP 键互相顶掉。
expect(allGroup.cards).toEqual([]);
expect(allGroup.titlebarActive).toBe(true);
expect(allGroup.titlebarOpacity).toBe(1);
expect(allGroup.resourceCount).toBe(allResources.length);
const artGroup = plan.find((group) => group.category === 'unclassified')!;
const band = allLayout.bandByCategory.get('unclassified')!;
expect(artGroup.cards).toHaveLength(resources.length);
expect(artGroup.cards.every((card) => card.presentation === 'child')).toBe(
true,
);
// 展开态的栏目标题栏不钉在视口上,但要可见(它随分带一起被视口变换)。
expect(artGroup.titlebarActive).toBe(false);
expect(artGroup.titlebarOpacity).toBe(1);
// 画出来的是"栏目内局部坐标 + 带原点",拖动起点与它同系(提交时再减回带原点)。
const first = artGroup.cards[0]!;
expect(first.layout.x).toBe(positions.get('art-0')!.x + band.originX);
expect(first.layout.y).toBe(positions.get('art-0')!.y + band.originY);
expect(first.layout.dragX).toBe(first.layout.x);
expect(first.layout.dragY).toBe(first.layout.y);
// 各栏目带之间不共享原点:不同栏目的卡不会被画到同一个位置。
const sceneCard = plan
.find((group) => group.category === 'scene')!
.cards.at(0)!;
expect(sceneCard.layout.y).toBe(
allPositions.get(sceneResource.id)!.y +
allLayout.bandByCategory.get('scene')!.originY,
);
expect(sceneCard.layout.y).not.toBe(first.layout.y);
// 展开态里每张资源在整份计划里只有一个宿主(总览态不是这样:`all` 那一格另有预览摞)。
const keys = plan.flatMap((group) => group.cards.map((card) => card.key));
expect(keys).toHaveLength(new Set(keys).size);
expect(keys).toHaveLength(allResources.length);
});
it('fades the leaving all-resources cards from their band positions', () => {
const allLayout = buildResourceBookAllLayout({
categoryOrder: ['unclassified'],
resourcesByCategory: new Map<ResourceBookCategory, ProjectResource[]>([
['unclassified', resources],
]),
positions,
cardSizes,
});
const plan = buildResourceBookScenePlan({
...base,
visibleCategoryOrder: [RESOURCE_BOOK_ALL_TARGET, 'unclassified'],
resourcesByCategory: new Map<ResourceBookTarget, ProjectResource[]>([
[RESOURCE_BOOK_ALL_TARGET, resources],
['unclassified', resources],
]),
overviewRects: resourceBookAllTitlebarRects(allLayout),
state: {
view: 'main',
category: null,
phase: 'returning-main',
token: 2,
},
// 返回总览时要按"刚离开的那段子画布"淡出:展开态是分带位置,栏目页是局部坐标。
exitingCategory: RESOURCE_BOOK_ALL_TARGET,
allLayout,
});
const band = allLayout.bandByCategory.get('unclassified')!;
const artGroup = plan.find((group) => group.category === 'unclassified')!;
const exiting = artGroup.cards.filter(
(card) => card.presentation === 'exiting',
);
expect(exiting).toHaveLength(1);
expect(exiting[0]!.layout.x).toBe(positions.get('art-3')!.x + band.originX);
expect(exiting[0]!.layout.y).toBe(positions.get('art-3')!.y + band.originY);
// 「所有资源」组这一态只出预览摞:它不接 child / exiting 卡,否则同一张卡会在两组里
// 各挂一份(DOM 查询与 FLIP 键都会撞车)。
expect(
plan
.find((group) => group.category === RESOURCE_BOOK_ALL_TARGET)!
.cards.every((card) => card.presentation === 'overview'),
).toBe(true);
});
});
describe('buildResourceBookAllLayout', () => {
const layout = buildResourceBookAllLayout({
categoryOrder: ['ui-interaction', 'character', 'audio'],
resourcesByCategory: new Map<ResourceBookCategory, ProjectResource[]>([
['ui-interaction', [resource('ui-0', 'ui-interaction')]],
['character', []],
['audio', [resource('bgm-0', 'audio'), resource('bgm-1', 'audio')]],
]),
positions: new Map([
['ui-0', { x: 0, y: 0 }],
['bgm-0', { x: 0, y: 0 }],
['bgm-1', { x: 200, y: 0 }],
]),
cardSizes: new Map([
['ui-0', { width: 180, height: 128 }],
['bgm-0', { width: 180, height: 128 }],
['bgm-1', { width: 180, height: 128 }],
]),
});
it('keeps the overview category order and drops the categories with nothing to show', () => {
expect(layout.bands.map((band) => band.category)).toEqual([
'ui-interaction',
'audio',
]);
});
it('stacks the bands downward so no two categories share one origin', () => {
const [first, second] = layout.bands;
// 标题栏横带压在卡片区之上,两个带的首尾不重叠:否则拖上面一带的卡会压到下面一带的标题。
expect(first!.titlebarTop).toBeLessThan(first!.originY);
expect(second!.titlebarTop).toBeGreaterThan(first!.originY + first!.height);
expect(second!.originY).toBeGreaterThan(first!.originY);
});
it('covers every titlebar and card box in its bounds', () => {
const last = layout.bands.at(-1)!;
expect(layout.bounds.x).toBeLessThanOrEqual(0);
expect(layout.bounds.y).toBeLessThanOrEqual(0);
expect(layout.bounds.width).toBeGreaterThanOrEqual(last.width);
expect(layout.bounds.height).toBeGreaterThanOrEqual(
last.originY + last.height,
);
});
it('hands every band a titlebar rect so the scene reuses the overview titlebar path', () => {
const rects = resourceBookAllTitlebarRects(layout);
const audio = layout.bandByCategory.get('audio')!;
expect(rects.get('audio')).toEqual({
left: audio.originX,
top: audio.titlebarTop,
width: audio.width,
height: RESOURCE_BOOK_ALL_BAND_TITLE_HEIGHT,
});
expect(rects.has('character')).toBe(false);
});
});
describe('resourceBookAllBandLocalPoint', () => {
const band: ResourceBookAllBand = {
category: 'document',
titlebarTop: 0,
originX: 40,
originY: 100,
width: 620,
height: 200,
};
const layout: ResourceBookAllLayout = {
bands: [band],
bandByCategory: new Map([['document', band]]),
bounds: { x: 0, y: 0, width: 620, height: 300 },
};
/**
* 这是本方案唯一会落到布局 sidecar 上的换算:展开态画的是"局部坐标 + 带原点"
* 落盘只认栏目内局部坐标。换算方向写错 = 把卡写到别处,所以单独钉住。
*/
it('subtracts the band origin so the world point lands back on the column-local point', () => {
expect(
resourceBookAllBandLocalPoint({
layout,
category: 'document',
x: 140,
y: 260,
}),
).toEqual({ x: 100, y: 160 });
});
it('passes the point through for the column page and for a category without a band', () => {
// 栏目页没有分带(`layout` 传 null),减 0:与既有"局部坐标直接落盘"逐字等价。
expect(
resourceBookAllBandLocalPoint({
layout: null,
category: 'document',
x: 140,
y: 260,
}),
).toEqual({ x: 140, y: 260 });
expect(
resourceBookAllBandLocalPoint({
layout,
category: 'audio',
x: 7,
y: 9,
}),
).toEqual({ x: 7, y: 9 });
});
});