让状态不变量校验先判字段类型再取值

src/features/ui-editor/stateInvariants.ts 在取 path.trim 与 pixel_size.every 前先判 string / Array.isArray,残缺持久化文档此前会在 firstUiDesignInvariantMessage 里抛 TypeError,被保存流程吞成通用失败文案
tests/uiEditorState.test.ts 补三组残缺界面图都回落到 invalid-image 的用例
This commit is contained in:
2026-09-24 16:01:14 +08:00
parent e2e9243415
commit 9e49db419f
2 changed files with 20 additions and 0 deletions
@@ -14,8 +14,12 @@ export function validateUiDesignState(state: State): UiDesignInvariantIssue[] {
const issues: UiDesignInvariantIssue[] = [];
const nodeIds = new Set<string>();
for (const [id, image] of Object.entries(state.ui_design_images)) {
// 这个 seam 专门接住可能残缺的持久化 State,取字段前先判类型,别把 TypeError
// 漏给调用方,否则保存失败只会退化成通用文案。
if (
typeof image.path !== 'string' ||
image.path.trim() === '' ||
!Array.isArray(image.pixel_size) ||
!image.pixel_size.every((value) => Number.isFinite(value) && value > 0) ||
!Number.isFinite(image.pixels_per_unit) ||
image.pixels_per_unit <= 0
@@ -163,6 +163,22 @@ describe('useUiEditorState', () => {
]);
});
it('reports malformed images instead of throwing', () => {
const state = structuredClone(EMPTY_UI_EDITOR_STATE);
for (const malformed of [
{ pixel_size: [100, 80], pixels_per_unit: 1 },
{ path: 'assets/page.png', pixels_per_unit: 1 },
{ path: 'assets/page.png', pixel_size: [0, 80], pixels_per_unit: 1 },
]) {
state.ui_design_images = {
page: malformed as unknown as UIDesignImage,
};
expect(validateUiDesignState(state)).toEqual([
expect.objectContaining({ code: 'invalid-image' }),
]);
}
});
it('moves a subtree between UI trees without changing its transform', () => {
const moved = nodeWithSprite('moved');
const initial: State = {