@pixi-ui-editor/runtime-pixi 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/controls.d.ts +19 -0
- package/dist/controls.d.ts.map +1 -1
- package/dist/controls.js +40 -0
- package/dist/controls.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/layoutGroups.js.map +1 -1
- package/dist/pageGroup.d.ts +17 -0
- package/dist/pageGroup.d.ts.map +1 -0
- package/dist/pageGroup.js +20 -0
- package/dist/pageGroup.js.map +1 -0
- package/dist/particles.d.ts +5 -2
- package/dist/particles.d.ts.map +1 -1
- package/dist/particles.js +57 -12
- package/dist/particles.js.map +1 -1
- package/dist/scene.d.ts.map +1 -1
- package/dist/scene.js +43 -2
- package/dist/scene.js.map +1 -1
- package/dist/views/NodeView.d.ts +2 -2
- package/dist/views/NodeView.d.ts.map +1 -1
- package/dist/views/NodeView.js.map +1 -1
- package/dist/views/PageGroupNodeView.d.ts +42 -0
- package/dist/views/PageGroupNodeView.d.ts.map +1 -0
- package/dist/views/PageGroupNodeView.js +72 -0
- package/dist/views/PageGroupNodeView.js.map +1 -0
- package/dist/views/PaginationNodeView.d.ts +59 -0
- package/dist/views/PaginationNodeView.d.ts.map +1 -0
- package/dist/views/PaginationNodeView.js +238 -0
- package/dist/views/PaginationNodeView.js.map +1 -0
- package/dist/views/ParticleEmitterNodeView.d.ts +13 -0
- package/dist/views/ParticleEmitterNodeView.d.ts.map +1 -1
- package/dist/views/ParticleEmitterNodeView.js +164 -32
- package/dist/views/ParticleEmitterNodeView.js.map +1 -1
- package/dist/views/SpineNodeView.d.ts +23 -2
- package/dist/views/SpineNodeView.d.ts.map +1 -1
- package/dist/views/SpineNodeView.js +131 -2
- package/dist/views/SpineNodeView.js.map +1 -1
- package/dist/views/basic.d.ts.map +1 -1
- package/dist/views/basic.js +3 -1
- package/dist/views/basic.js.map +1 -1
- package/dist/views/createNodeView.d.ts.map +1 -1
- package/dist/views/createNodeView.js +9 -0
- package/dist/views/createNodeView.js.map +1 -1
- package/package.json +5 -3
- package/dist/assets/spine.test.d.ts +0 -2
- package/dist/assets/spine.test.d.ts.map +0 -1
- package/dist/assets/spine.test.js +0 -42
- package/dist/assets/spine.test.js.map +0 -1
- package/dist/index.test.d.ts +0 -2
- package/dist/index.test.d.ts.map +0 -1
- package/dist/index.test.js +0 -485
- package/dist/index.test.js.map +0 -1
- package/dist/particles.test.d.ts +0 -2
- package/dist/particles.test.d.ts.map +0 -1
- package/dist/particles.test.js +0 -210
- package/dist/particles.test.js.map +0 -1
- package/dist/test.setup.d.ts +0 -2
- package/dist/test.setup.d.ts.map +0 -1
- package/dist/test.setup.js +0 -6
- package/dist/test.setup.js.map +0 -1
- package/dist/views/ParticleEmitterNodeView.test.d.ts +0 -2
- package/dist/views/ParticleEmitterNodeView.test.d.ts.map +0 -1
- package/dist/views/ParticleEmitterNodeView.test.js +0 -148
- package/dist/views/ParticleEmitterNodeView.test.js.map +0 -1
package/dist/index.test.js
DELETED
|
@@ -1,485 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
3
|
-
import { materializedPrefabNodeId, serializeProjectDocument } from "@pixi-ui-editor/schema";
|
|
4
|
-
import { Graphics, NineSliceSprite, Sprite, Texture } from "pixi.js";
|
|
5
|
-
import { TextureAtlas } from "@esotericsoftware/spine-pixi-v8";
|
|
6
|
-
import { assignAtlasPageTextures, buildPrefabView, buildSceneView, collectNodeAssetIds, fitSpineToTransform, getCanvasLogicalSize, getCanvasScale, MaskNodeView, parseProjectDocumentJson, ProjectDocumentJsonParseError, resolveAnchoredTransform, resolveProfileForViewport, resolveProfileTransform, resolveSpineSettings, resolveTextStyle } from "./index.js";
|
|
7
|
-
import { createNodeView } from "./views/createNodeView.js";
|
|
8
|
-
import { ContainerNodeView } from "./views/basic.js";
|
|
9
|
-
const sampleUrl = new URL("../../../examples/sample-project/project.json", import.meta.url);
|
|
10
|
-
const sampleJson = readFileSync(sampleUrl, "utf8");
|
|
11
|
-
const ids = {
|
|
12
|
-
scene: "10000000-0000-4000-8000-000000000002",
|
|
13
|
-
root: "10000000-0000-4000-8000-000000000003",
|
|
14
|
-
image: "10000000-0000-4000-8000-000000000051",
|
|
15
|
-
asset: "10000000-0000-4000-8000-000000000010",
|
|
16
|
-
text: "10000000-0000-4000-8000-000000000006",
|
|
17
|
-
};
|
|
18
|
-
const clone = (value) => structuredClone(value);
|
|
19
|
-
const buttonIds = { node: "10000000-0000-4000-8000-000000000008", pressedAsset: "10000000-0000-4000-8000-000000000009" };
|
|
20
|
-
/** Rendering contracts use a DOM-free mini scene; the repository sample itself also exercises Yoga groups. */
|
|
21
|
-
function renderDocument() {
|
|
22
|
-
const document = parseProjectDocumentJson(sampleJson);
|
|
23
|
-
const scene = document.scenes[0];
|
|
24
|
-
const root = scene.nodes.find((node) => node.id === ids.root);
|
|
25
|
-
const image = scene.nodes.find((node) => node.id === ids.image);
|
|
26
|
-
const text = scene.nodes.find((node) => node.id === ids.text);
|
|
27
|
-
image.parentId = root.id;
|
|
28
|
-
text.parentId = root.id;
|
|
29
|
-
root.children = [image.id, text.id];
|
|
30
|
-
scene.nodes = [root, image, text];
|
|
31
|
-
return document;
|
|
32
|
-
}
|
|
33
|
-
/** Sample scene plus a button whose pressed state has its own image and whose hover/disabled states have none. */
|
|
34
|
-
function documentWithButton() {
|
|
35
|
-
const document = renderDocument();
|
|
36
|
-
const scene = document.scenes[0];
|
|
37
|
-
const root = scene.nodes.find((node) => node.id === ids.root);
|
|
38
|
-
document.assets.push({ id: buttonIds.pressedAsset, name: "Pressed", type: "image", source: { uri: "assets/pressed.png", mediaType: "image/png" } });
|
|
39
|
-
scene.nodes.push({
|
|
40
|
-
id: buttonIds.node,
|
|
41
|
-
name: "Play",
|
|
42
|
-
type: "button",
|
|
43
|
-
parentId: ids.root,
|
|
44
|
-
children: [],
|
|
45
|
-
visible: true,
|
|
46
|
-
enabled: true,
|
|
47
|
-
states: { normalAssetId: ids.asset, pressedAssetId: buttonIds.pressedAsset },
|
|
48
|
-
transform: { x: 0, y: 0, width: 120, height: 40, scaleX: 1, scaleY: 1, rotation: 0 },
|
|
49
|
-
});
|
|
50
|
-
root.children.push(buttonIds.node);
|
|
51
|
-
return document;
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* По одному узлу каждого типа, который можно вытащить на сцену.
|
|
55
|
-
* `Record<UINode["type"], ...>` — намеренно: компилятор не даст добавить новый тип ноды,
|
|
56
|
-
* не дописав его сюда, а значит новый тип не сможет молча потерять selection и drag.
|
|
57
|
-
*/
|
|
58
|
-
const NODE_TYPE_FIXTURES = {
|
|
59
|
-
container: (base) => ({ ...base, type: "container" }),
|
|
60
|
-
mask: (base) => ({ ...base, type: "mask" }),
|
|
61
|
-
"horizontal-layout": (base) => ({ ...base, type: "horizontal-layout", layoutGroup: { base: { padding: { left: 0, right: 0, top: 0, bottom: 0 }, spacing: 0, childAlignment: "upper-left", reverseOrder: false, controlChildWidth: true, controlChildHeight: true, forceExpandWidth: false, forceExpandHeight: false } } }),
|
|
62
|
-
"vertical-layout": (base) => ({ ...base, type: "vertical-layout", layoutGroup: { base: { padding: { left: 0, right: 0, top: 0, bottom: 0 }, spacing: 0, childAlignment: "upper-left", reverseOrder: false, controlChildWidth: true, controlChildHeight: true, forceExpandWidth: false, forceExpandHeight: false } } }),
|
|
63
|
-
"grid-layout": (base) => ({ ...base, type: "grid-layout", layoutGroup: { base: { padding: { left: 0, right: 0, top: 0, bottom: 0 }, spacingX: 0, spacingY: 0, cellWidth: 20, cellHeight: 20, startCorner: "upper-left", startAxis: "horizontal", childAlignment: "upper-left", constraint: "flexible" } } }),
|
|
64
|
-
image: (base) => ({ ...base, type: "image", assetId: ids.asset }),
|
|
65
|
-
text: (base) => ({ ...base, type: "text", text: "Label" }),
|
|
66
|
-
"bitmap-text": (base) => ({ ...base, type: "bitmap-text", text: "0", assetId: ids.asset, fontSize: 24, fill: "#FFFFFF", align: "left", verticalAlign: "top", letterSpacing: 0, wordWrap: false }),
|
|
67
|
-
spine: (base) => ({ ...base, type: "spine", assetId: ids.asset }),
|
|
68
|
-
button: (base) => ({ ...base, type: "button", enabled: true, states: { normalAssetId: ids.asset } }),
|
|
69
|
-
"staged-button": (base) => ({ ...base, type: "staged-button", enabled: true, wrap: true, stages: [{ id: "40000000-0000-4000-8000-0000000000fe", value: "1x", states: { normalAssetId: ids.asset } }], defaultStageId: "40000000-0000-4000-8000-0000000000fe" }),
|
|
70
|
-
checkbox: (base) => ({ ...base, type: "checkbox", enabled: true, defaultChecked: false, states: { uncheckedAssetId: ids.asset, checkedAssetId: ids.asset } }),
|
|
71
|
-
"prefab-instance": (base) => ({ ...base, type: "prefab-instance", prefabId: "40000000-0000-4000-8000-00000000000f" }),
|
|
72
|
-
"scroll-view": (base) => ({ ...base, type: "scroll-view", scrollView: { direction: "vertical", padding: { left: 0, right: 0, top: 0, bottom: 0 }, itemSpacing: 0, cornerRadius: 0, easingEnabled: true } }),
|
|
73
|
-
input: (base) => ({ ...base, type: "input", placeholder: "Enter text", defaultValue: "", secure: false, align: "left", padding: { left: 0, right: 0, top: 0, bottom: 0 }, cleanOnFocus: false, clipText: true, textStyle: { fontFamily: "Arial", fontSize: 24, fontWeight: "normal", fontStyle: "normal", fill: "#FFFFFF", align: "left", verticalAlign: "top", wordWrap: false, breakWords: false, letterSpacing: 0 } }),
|
|
74
|
-
slider: (base) => ({ ...base, type: "slider", backgroundAssetId: ids.asset, fillAssetId: ids.asset, handleAssetId: ids.asset, min: 0, max: 100, step: 1, defaultValue: 50, fillPadding: { left: 0, right: 0, top: 0, bottom: 0 } }),
|
|
75
|
-
"progress-bar": (base) => ({ ...base, type: "progress-bar", backgroundAssetId: ids.asset, fillAssetId: ids.asset, defaultProgress: 50, fillPadding: { left: 0, right: 0, top: 0, bottom: 0 } }),
|
|
76
|
-
"particle-emitter": (base) => ({ ...base, type: "particle-emitter", effectId: "40000000-0000-4000-8000-00000000000f", autoplay: true, simulationSpace: "local" }),
|
|
77
|
-
};
|
|
78
|
-
/** FancyButton keeps exactly one state view visible, so the shown texture identifies the active state. */
|
|
79
|
-
function shownStateTexture(view) {
|
|
80
|
-
const shown = [];
|
|
81
|
-
const walk = (container) => {
|
|
82
|
-
if (container instanceof Sprite && container.visible)
|
|
83
|
-
shown.push(container);
|
|
84
|
-
container.children.forEach(walk);
|
|
85
|
-
};
|
|
86
|
-
walk(view);
|
|
87
|
-
return shown.length === 1 ? shown[0].texture : undefined;
|
|
88
|
-
}
|
|
89
|
-
describe("sample project loader smoke test", () => {
|
|
90
|
-
it("materializes independently addressable overridden and local prefab nodes", () => {
|
|
91
|
-
const document = renderDocument();
|
|
92
|
-
const scene = document.scenes[0];
|
|
93
|
-
const root = scene.nodes[0];
|
|
94
|
-
const definitionNode = structuredClone(scene.nodes.find((node) => node.type === "text"));
|
|
95
|
-
definitionNode.id = "50000000-0000-4000-8000-000000000001";
|
|
96
|
-
definitionNode.parentId = null;
|
|
97
|
-
definitionNode.children = [];
|
|
98
|
-
const prefabId = "50000000-0000-4000-8000-000000000002";
|
|
99
|
-
document.prefabs = [{ id: prefabId, name: "Label", rootNodeIds: [definitionNode.id], nodes: [definitionNode], exposedProperties: [] }];
|
|
100
|
-
const instanceId = "50000000-0000-4000-8000-000000000003";
|
|
101
|
-
const localId = "50000000-0000-4000-8000-000000000004";
|
|
102
|
-
scene.nodes.push({
|
|
103
|
-
id: instanceId, name: "Configured label", type: "prefab-instance", prefabId, parentId: root.id, children: [], visible: true,
|
|
104
|
-
transform: { x: 0, y: 0, width: 200, height: 80, scaleX: 1, scaleY: 1, rotation: 0 },
|
|
105
|
-
propertyOverrides: [{ nodeId: definitionNode.id, patch: { text: "Overridden" } }],
|
|
106
|
-
locallyAddedNodes: [{ id: localId, name: "Local", type: "text", text: "Extra", parentId: null, children: [], visible: true, transform: { x: 0, y: 30, width: 100, height: 20, scaleX: 1, scaleY: 1, rotation: 0 } }],
|
|
107
|
-
});
|
|
108
|
-
root.children.push(instanceId);
|
|
109
|
-
const sceneView = buildSceneView(document, scene.id, "desktop", { interaction: "runtime" });
|
|
110
|
-
expect(sceneView.nodeViews.has(materializedPrefabNodeId(instanceId, definitionNode.id))).toBe(true);
|
|
111
|
-
expect(sceneView.nodeViews.has(localId)).toBe(true);
|
|
112
|
-
const viewport = scene.layout.referenceViewports.desktop;
|
|
113
|
-
sceneView.root.resize({ width: viewport.width * 2, height: viewport.height * 3 });
|
|
114
|
-
expect(sceneView.root.scale.x).toBe(2);
|
|
115
|
-
expect(sceneView.root.scale.y).toBe(2);
|
|
116
|
-
expect(sceneView.root.position).toMatchObject({ x: 0, y: viewport.height / 2 });
|
|
117
|
-
expect(sceneView.root.content.mask).not.toBeNull();
|
|
118
|
-
const spawned = buildPrefabView(document, prefabId, "desktop", { interaction: "runtime" }, {
|
|
119
|
-
propertyOverrides: [{ nodeId: definitionNode.id, patch: { text: "Spawned" } }],
|
|
120
|
-
});
|
|
121
|
-
expect(spawned.nodeViews.size).toBeGreaterThan(1);
|
|
122
|
-
expect(spawned.root.content.mask).toBeUndefined();
|
|
123
|
-
});
|
|
124
|
-
it("matches atlas pages to texture files by name", () => {
|
|
125
|
-
const atlas = new TextureAtlas("second.png\nsize: 1,1\nformat: RGBA8888\nfilter: Linear,Linear\nrepeat: none\n\nfirst.png\nsize: 1,1\nformat: RGBA8888\nfilter: Linear,Linear\nrepeat: none\n");
|
|
126
|
-
assignAtlasPageTextures(atlas, new Map([["first.png", Texture.WHITE], ["second.png", Texture.WHITE]]));
|
|
127
|
-
expect(atlas.pages.every((page) => page.texture !== null)).toBe(true);
|
|
128
|
-
});
|
|
129
|
-
it("resolves base transforms, partial desktop overrides, and profile visibility", () => {
|
|
130
|
-
const document = renderDocument();
|
|
131
|
-
const node = document.scenes[0].nodes[1];
|
|
132
|
-
expect(resolveProfileTransform(node, "desktop")).toEqual({ transform: node.transform, visible: true });
|
|
133
|
-
const withDesktopOverride = clone(node);
|
|
134
|
-
withDesktopOverride.layoutOverrides = { desktop: { transform: { x: 12, scaleY: 2 } } };
|
|
135
|
-
expect(resolveProfileTransform(withDesktopOverride, "desktop")).toEqual({
|
|
136
|
-
transform: { ...node.transform, x: 12, scaleY: 2 },
|
|
137
|
-
visible: true,
|
|
138
|
-
});
|
|
139
|
-
withDesktopOverride.layoutOverrides.desktop.visible = false;
|
|
140
|
-
expect(resolveProfileTransform(withDesktopOverride, "desktop").visible).toBe(false);
|
|
141
|
-
});
|
|
142
|
-
it("resolves sparse text styles and includes profile-only fonts in asset collection", () => {
|
|
143
|
-
const document = renderDocument();
|
|
144
|
-
const node = document.scenes[0].nodes.find((candidate) => candidate.type === "text");
|
|
145
|
-
const base = resolveTextStyle(node, "desktop");
|
|
146
|
-
const mobileFontId = "60000000-0000-4000-8000-000000000001";
|
|
147
|
-
node.textStyleOverrides = { mobile: { fontSize: 42, align: "center", fontAssetId: mobileFontId } };
|
|
148
|
-
expect(resolveTextStyle(node, "desktop")).toEqual(base);
|
|
149
|
-
expect(resolveTextStyle(node, "mobile")).toMatchObject({ ...base, fontSize: 42, align: "center", fontAssetId: mobileFontId });
|
|
150
|
-
expect(collectNodeAssetIds(node)).toContain(mobileFontId);
|
|
151
|
-
});
|
|
152
|
-
it("resolves Spine settings per profile and supports explicitly clearing animation", () => {
|
|
153
|
-
const node = {
|
|
154
|
-
id: "60000000-0000-4000-8000-000000000002", name: "Hero", type: "spine", assetId: ids.asset,
|
|
155
|
-
animation: "idle", autoplay: true, loop: true, animationSpeed: 1, parentId: null, children: [], visible: true,
|
|
156
|
-
transform: { x: 0, y: 0, width: 100, height: 100, scaleX: 1, scaleY: 1, rotation: 0 },
|
|
157
|
-
spineOverrides: { mobile: { animation: null, autoplay: false, loop: false, animationSpeed: 0.5 } },
|
|
158
|
-
};
|
|
159
|
-
expect(resolveSpineSettings(node, "desktop")).toEqual({ animation: "idle", autoplay: true, loop: true, animationSpeed: 1 });
|
|
160
|
-
expect(resolveSpineSettings(node, "mobile")).toEqual({ animation: undefined, autoplay: false, loop: false, animationSpeed: 0.5 });
|
|
161
|
-
});
|
|
162
|
-
it("resolves normalized anchors against the parent rectangle", () => {
|
|
163
|
-
const transform = { x: -160, y: -50, width: 320, height: 100, scaleX: 1, scaleY: 1, rotation: 0, anchorMinX: 0.5, anchorMaxX: 0.5, anchorMinY: 1, anchorMaxY: 1 };
|
|
164
|
-
expect(resolveAnchoredTransform(transform, { width: 1920, height: 1080 })).toMatchObject({ x: 800, y: 1030, width: 320, height: 100 });
|
|
165
|
-
});
|
|
166
|
-
it("uses Unity's uniform logarithmic Canvas Scaler blend at match 0.5", () => {
|
|
167
|
-
const preferred = { width: 1920, height: 1080 };
|
|
168
|
-
expect(getCanvasScale({ width: 3840, height: 2160 }, preferred)).toBeCloseTo(2);
|
|
169
|
-
expect(getCanvasScale({ width: 1920, height: 2160 }, preferred)).toBeCloseTo(Math.sqrt(2));
|
|
170
|
-
expect(getCanvasScale({ width: 3840, height: 1080 }, preferred)).toBeCloseTo(Math.sqrt(2));
|
|
171
|
-
});
|
|
172
|
-
it("keeps point-anchor rect proportions in logical Canvas space", () => {
|
|
173
|
-
const preferred = { width: 1920, height: 1080 };
|
|
174
|
-
const screen = { width: 1920, height: 2160 };
|
|
175
|
-
const scale = getCanvasScale(screen, preferred);
|
|
176
|
-
const canvas = getCanvasLogicalSize(screen, preferred);
|
|
177
|
-
const transform = { x: 0, y: 0, width: 50, height: 50, scaleX: 1, scaleY: 1, rotation: 0, anchorMinX: 0.5, anchorMaxX: 0.5, anchorMinY: 0.5, anchorMaxY: 0.5, pivotX: 0.5, pivotY: 0.5 };
|
|
178
|
-
const resolved = resolveAnchoredTransform(transform, canvas);
|
|
179
|
-
expect(resolved).toMatchObject({ x: canvas.width / 2, y: canvas.height / 2, width: 50, height: 50 });
|
|
180
|
-
expect(resolved.width * scale).toBeCloseTo(50 * Math.sqrt(2));
|
|
181
|
-
expect(resolved.height * scale).toBeCloseTo(50 * Math.sqrt(2));
|
|
182
|
-
});
|
|
183
|
-
it("uses anchorMax and sizeDelta for Unity-style stretch", () => {
|
|
184
|
-
const parent = { width: 800, height: 600 };
|
|
185
|
-
const transform = { x: 0, y: 0, width: -30, height: -40, scaleX: 1, scaleY: 1, rotation: 0, anchorMinX: 0, anchorMaxX: 1, anchorMinY: 0, anchorMaxY: 1, pivotX: 0.5, pivotY: 0.5 };
|
|
186
|
-
expect(resolveAnchoredTransform(transform, parent)).toMatchObject({ x: 400, y: 300, width: 770, height: 560 });
|
|
187
|
-
});
|
|
188
|
-
it("resolves the viewport profile on both sides of the breakpoint and picks mobile exactly on it", () => {
|
|
189
|
-
const settings = { layoutProfileSelection: { mode: "aspect-ratio", mobileMaxAspectRatio: 1 } };
|
|
190
|
-
expect(resolveProfileForViewport(settings, 1920, 1080)).toBe("desktop");
|
|
191
|
-
expect(resolveProfileForViewport(settings, 390, 844)).toBe("mobile");
|
|
192
|
-
expect(resolveProfileForViewport(settings, 1000, 1000)).toBe("mobile");
|
|
193
|
-
});
|
|
194
|
-
it("fits Spine setup bounds to the node transform dimensions", () => {
|
|
195
|
-
const transform = { x: 0, y: 0, width: 200, height: 200, scaleX: 1, scaleY: 1, rotation: 0 };
|
|
196
|
-
expect(fitSpineToTransform({ x: -50, y: 20, width: 100, height: 400 }, transform)).toEqual({ scaleX: 2, scaleY: 0.5, x: 100, y: -10 });
|
|
197
|
-
expect(fitSpineToTransform({ x: 0, y: 0, width: 0, height: 400 }, transform)).toBeUndefined();
|
|
198
|
-
});
|
|
199
|
-
it("uses supplied textures for image nodes and otherwise preserves the placeholder", () => {
|
|
200
|
-
const document = renderDocument();
|
|
201
|
-
const textured = buildSceneView(document, ids.scene, "desktop", { interaction: "authoring", textures: new Map([[ids.asset, Texture.WHITE]]) });
|
|
202
|
-
const imageView = textured.nodeViews.get(ids.image);
|
|
203
|
-
const sprite = imageView.children[0];
|
|
204
|
-
expect(imageView).not.toBeInstanceOf(Sprite);
|
|
205
|
-
expect(sprite).toBeInstanceOf(Sprite);
|
|
206
|
-
expect(sprite?.width).toBe(240);
|
|
207
|
-
const placeholder = buildSceneView(document, ids.scene, "desktop", { interaction: "authoring" }).nodeViews.get(ids.image)?.children[0];
|
|
208
|
-
expect(placeholder).not.toBeInstanceOf(Sprite);
|
|
209
|
-
const anchoredDocument = clone(document);
|
|
210
|
-
const anchoredNode = anchoredDocument.scenes[0].nodes.find((node) => node.id === ids.image);
|
|
211
|
-
anchoredNode.transform = { ...anchoredNode.transform, anchorMinX: 1, anchorMaxX: 1, pivotX: 1, x: 0 };
|
|
212
|
-
const anchoredView = buildSceneView(anchoredDocument, ids.scene, "desktop", { interaction: "authoring", textures: new Map([[ids.asset, Texture.WHITE]]) }).nodeViews.get(ids.image);
|
|
213
|
-
const anchoredSprite = anchoredView.children[0];
|
|
214
|
-
const renderedRight = anchoredView.position.x + (anchoredSprite.width - anchoredView.pivot.x) * anchoredView.scale.x;
|
|
215
|
-
expect(renderedRight).toBe(anchoredDocument.scenes[0].layout.referenceViewports.desktop.width);
|
|
216
|
-
const mobileNode = anchoredDocument.scenes[0].nodes.find((node) => node.id === ids.image);
|
|
217
|
-
mobileNode.layoutOverrides = { mobile: { transform: { anchorMinX: 1, anchorMaxX: 1, pivotX: 1, x: 0 } } };
|
|
218
|
-
const mobileView = buildSceneView(anchoredDocument, ids.scene, "mobile", { interaction: "authoring", textures: new Map([[ids.asset, Texture.WHITE]]) }).nodeViews.get(ids.image);
|
|
219
|
-
const mobileSprite = mobileView.children[0];
|
|
220
|
-
const mobileRight = mobileView.position.x + (mobileSprite.width - mobileView.pivot.x) * mobileView.scale.x;
|
|
221
|
-
expect(mobileRight).toBe(anchoredDocument.scenes[0].layout.referenceViewports.mobile.width);
|
|
222
|
-
});
|
|
223
|
-
it("places a zero-offset centred pivot at the centre of its parent", () => {
|
|
224
|
-
const document = renderDocument();
|
|
225
|
-
const imageNode = document.scenes[0].nodes.find((node) => node.id === ids.image);
|
|
226
|
-
imageNode.transform = {
|
|
227
|
-
...imageNode.transform,
|
|
228
|
-
x: 0,
|
|
229
|
-
y: 0,
|
|
230
|
-
anchorMinX: 0.5,
|
|
231
|
-
anchorMaxX: 0.5,
|
|
232
|
-
anchorMinY: 0.5,
|
|
233
|
-
anchorMaxY: 0.5,
|
|
234
|
-
pivotX: 0.5,
|
|
235
|
-
pivotY: 0.5,
|
|
236
|
-
};
|
|
237
|
-
const imageView = buildSceneView(document, ids.scene, "desktop", { interaction: "authoring" }).nodeViews.get(ids.image);
|
|
238
|
-
const viewport = document.scenes[0].layout.referenceViewports.desktop;
|
|
239
|
-
expect(imageView.position).toMatchObject({ x: viewport.width / 2, y: viewport.height / 2 });
|
|
240
|
-
});
|
|
241
|
-
it("keeps node geometry logical and applies one uniform scale on the Canvas root", () => {
|
|
242
|
-
const document = renderDocument();
|
|
243
|
-
const scene = document.scenes[0];
|
|
244
|
-
scene.layout.referenceViewports.desktop.height *= 2;
|
|
245
|
-
const expectedScale = Math.sqrt(2);
|
|
246
|
-
const built = buildSceneView(document, ids.scene, "desktop", { interaction: "authoring", textures: new Map([[ids.asset, Texture.WHITE]]) });
|
|
247
|
-
const canvasRoot = built.root.children[0];
|
|
248
|
-
const imageView = built.nodeViews.get(ids.image);
|
|
249
|
-
const authored = scene.nodes.find((node) => node.id === ids.image).transform;
|
|
250
|
-
expect(canvasRoot.scale).toMatchObject({ x: expectedScale, y: expectedScale });
|
|
251
|
-
expect(imageView.layoutRectangle).toEqual({ width: authored.width, height: authored.height });
|
|
252
|
-
expect(imageView.layoutRectangle.width * canvasRoot.scale.x).toBeCloseTo(authored.width * expectedScale);
|
|
253
|
-
expect(imageView.layoutRectangle.height * canvasRoot.scale.y).toBeCloseTo(authored.height * expectedScale);
|
|
254
|
-
});
|
|
255
|
-
it("gives every node type the same grab rectangle, whatever it renders", () => {
|
|
256
|
-
const types = Object.keys(NODE_TYPE_FIXTURES);
|
|
257
|
-
const nodeId = (index) => `40000000-0000-4000-8000-00000000000${index}`;
|
|
258
|
-
const views = types.map((type, index) => {
|
|
259
|
-
const node = NODE_TYPE_FIXTURES[type]({
|
|
260
|
-
id: nodeId(index),
|
|
261
|
-
name: type,
|
|
262
|
-
parentId: ids.root,
|
|
263
|
-
children: [],
|
|
264
|
-
visible: true,
|
|
265
|
-
transform: { x: 10, y: 20, width: 120, height: 40, scaleX: 1, scaleY: 1, rotation: 0 },
|
|
266
|
-
});
|
|
267
|
-
// Input and ScrollBox create DOM controls; their NodeView-level grab contract is the same.
|
|
268
|
-
const view = node.type === "scroll-view" || node.type === "input" ? new ContainerNodeView() : createNodeView(node, "authoring");
|
|
269
|
-
view.update(node, "desktop", { width: 1920, height: 1080 });
|
|
270
|
-
return view;
|
|
271
|
-
});
|
|
272
|
-
// Ни текстур, ни Spine-данных: grab-зона не должна зависеть ни от контента ноды, ни от загруженных ассетов.
|
|
273
|
-
const grab = (index, x, y) => views[index]?.containsPoint({ x, y });
|
|
274
|
-
const grabbedInsideRect = types.map((type, index) => [type, grab(index, 60, 20) ?? false]);
|
|
275
|
-
const missedOutsideRect = types.map((type, index) => [type, grab(index, 200, 20) ?? true]);
|
|
276
|
-
expect(grabbedInsideRect).toEqual(types.map((type) => [type, true]));
|
|
277
|
-
expect(missedOutsideRect).toEqual(types.map((type) => [type, false]));
|
|
278
|
-
});
|
|
279
|
-
it("clips a mask subtree to the resolved rectangular layout area and resizes the mask in place", () => {
|
|
280
|
-
const mask = {
|
|
281
|
-
id: "40000000-0000-4000-8000-000000000099",
|
|
282
|
-
name: "Mask",
|
|
283
|
-
type: "mask",
|
|
284
|
-
parentId: null,
|
|
285
|
-
children: [],
|
|
286
|
-
visible: true,
|
|
287
|
-
transform: { x: 0, y: 0, width: 120, height: 40, scaleX: 1, scaleY: 1, rotation: 0 },
|
|
288
|
-
};
|
|
289
|
-
const view = createNodeView(mask, "runtime");
|
|
290
|
-
view.update(mask, "desktop");
|
|
291
|
-
expect(view).toBeInstanceOf(MaskNodeView);
|
|
292
|
-
expect(view.mask).toBeInstanceOf(Graphics);
|
|
293
|
-
expect(view.children).toContain(view.mask);
|
|
294
|
-
expect(view.mask.getLocalBounds()).toMatchObject({ x: 0, y: 0, width: 120, height: 40 });
|
|
295
|
-
mask.transform.width = 75;
|
|
296
|
-
mask.transform.height = 25;
|
|
297
|
-
view.update(mask, "desktop");
|
|
298
|
-
expect(view.mask.getLocalBounds()).toMatchObject({ x: 0, y: 0, width: 75, height: 25 });
|
|
299
|
-
});
|
|
300
|
-
it("keeps a child grabbable where it reaches past its parent's rectangle", () => {
|
|
301
|
-
const document = renderDocument();
|
|
302
|
-
const scene = document.scenes[0];
|
|
303
|
-
const root = scene.nodes.find((node) => node.id === ids.root);
|
|
304
|
-
const parentId = "50000000-0000-4000-8000-000000000001";
|
|
305
|
-
const childId = "50000000-0000-4000-8000-000000000002";
|
|
306
|
-
const transform = { scaleX: 1, scaleY: 1, rotation: 0 };
|
|
307
|
-
// Ребёнок по умолчанию смещён на 50/50 внутри родителя 100x100, поэтому наполовину торчит наружу.
|
|
308
|
-
scene.nodes.push({ id: parentId, name: "Parent", type: "container", parentId: ids.root, children: [childId], visible: true, transform: { ...transform, x: 0, y: 0, width: 100, height: 100 } });
|
|
309
|
-
scene.nodes.push({ id: childId, name: "Child", type: "container", parentId, children: [], visible: true, transform: { ...transform, x: 50, y: 50, width: 100, height: 100 } });
|
|
310
|
-
root.children.push(parentId);
|
|
311
|
-
const { nodeViews } = buildSceneView(document, ids.scene, "desktop", { interaction: "authoring" });
|
|
312
|
-
const child = nodeViews.get(childId);
|
|
313
|
-
// Точка (90, 90) в локальных координатах ребёнка лежит за границей родителя.
|
|
314
|
-
expect(child.containsPoint({ x: 90, y: 90 })).toBe(true);
|
|
315
|
-
// hitArea родителя обрезала бы здесь всё поддерево, поэтому её быть не должно.
|
|
316
|
-
expect(nodeViews.get(parentId).hitArea).toBeUndefined();
|
|
317
|
-
});
|
|
318
|
-
it("falls back to the normal image for states without an asset and switches the shown view on every state", () => {
|
|
319
|
-
const document = documentWithButton();
|
|
320
|
-
const textures = new Map([[ids.asset, Texture.WHITE], [buttonIds.pressedAsset, Texture.EMPTY]]);
|
|
321
|
-
const view = buildSceneView(document, ids.scene, "desktop", { interaction: "runtime", textures }).nodeViews.get(buttonIds.node);
|
|
322
|
-
expect(view.state).toBe("normal");
|
|
323
|
-
expect(shownStateTexture(view)).toBe(Texture.WHITE);
|
|
324
|
-
// hover и disabled не имеют своего asset и обязаны показывать normal; pressed — собственный.
|
|
325
|
-
const shown = ["hover", "pressed", "normal", "disabled"].map((state) => {
|
|
326
|
-
view.setState(state);
|
|
327
|
-
return [view.state, shownStateTexture(view)];
|
|
328
|
-
});
|
|
329
|
-
expect(shown).toEqual([
|
|
330
|
-
["hover", Texture.WHITE],
|
|
331
|
-
["pressed", Texture.EMPTY],
|
|
332
|
-
["normal", Texture.WHITE],
|
|
333
|
-
["disabled", Texture.WHITE],
|
|
334
|
-
]);
|
|
335
|
-
});
|
|
336
|
-
it("uses a NineSliceSprite for every button state once nineSlice is set, carrying the authored borders", () => {
|
|
337
|
-
const document = documentWithButton();
|
|
338
|
-
const buttonNode = document.scenes[0].nodes.find((node) => node.id === buttonIds.node);
|
|
339
|
-
buttonNode.nineSlice = { left: 6, top: 4, right: 6, bottom: 4 };
|
|
340
|
-
const textures = new Map([[ids.asset, Texture.WHITE], [buttonIds.pressedAsset, Texture.EMPTY]]);
|
|
341
|
-
const view = buildSceneView(document, ids.scene, "desktop", { interaction: "runtime", textures }).nodeViews.get(buttonIds.node);
|
|
342
|
-
const stateViews = [];
|
|
343
|
-
const walk = (container) => { if (container instanceof Sprite || container instanceof NineSliceSprite)
|
|
344
|
-
stateViews.push(container); container.children.forEach(walk); };
|
|
345
|
-
walk(view);
|
|
346
|
-
// normal/hover/disabled fall back to the normal asset; pressed has its own — all four are sliced.
|
|
347
|
-
expect(stateViews).toHaveLength(4);
|
|
348
|
-
expect(stateViews.every((child) => child instanceof NineSliceSprite)).toBe(true);
|
|
349
|
-
expect(stateViews.map((child) => [child.leftWidth, child.topHeight, child.rightWidth, child.bottomHeight])).toEqual([[6, 4, 6, 4], [6, 4, 6, 4], [6, 4, 6, 4], [6, 4, 6, 4]]);
|
|
350
|
-
});
|
|
351
|
-
it("renders a plain Sprite without nineSlice and a NineSliceSprite with it, carrying the authored borders", () => {
|
|
352
|
-
const document = renderDocument();
|
|
353
|
-
const textures = new Map([[ids.asset, Texture.WHITE]]);
|
|
354
|
-
const plain = buildSceneView(document, ids.scene, "desktop", { interaction: "runtime", textures }).nodeViews.get(ids.image);
|
|
355
|
-
expect(plain.children[0]).toBeInstanceOf(Sprite);
|
|
356
|
-
expect(plain.children[0]).not.toBeInstanceOf(NineSliceSprite);
|
|
357
|
-
const imageNode = document.scenes[0].nodes.find((node) => node.id === ids.image && node.type === "image");
|
|
358
|
-
imageNode.nineSlice = { left: 8, top: 6, right: 8, bottom: 6 };
|
|
359
|
-
const sliced = buildSceneView(document, ids.scene, "desktop", { interaction: "runtime", textures }).nodeViews.get(ids.image);
|
|
360
|
-
const sprite = sliced.children[0];
|
|
361
|
-
expect(sprite).toBeInstanceOf(NineSliceSprite);
|
|
362
|
-
expect(sprite).toMatchObject({ leftWidth: 8, topHeight: 6, rightWidth: 8, bottomHeight: 6 });
|
|
363
|
-
});
|
|
364
|
-
it("swaps a checkbox's shown texture on hover, independently for its checked and unchecked views", () => {
|
|
365
|
-
const document = renderDocument();
|
|
366
|
-
const scene = document.scenes[0];
|
|
367
|
-
const root = scene.nodes.find((node) => node.id === ids.root);
|
|
368
|
-
const checkboxId = "10000000-0000-4000-8000-000000000011";
|
|
369
|
-
const uncheckedHoverAsset = "10000000-0000-4000-8000-000000000012";
|
|
370
|
-
const checkedHoverAsset = "10000000-0000-4000-8000-000000000013";
|
|
371
|
-
document.assets.push({ id: uncheckedHoverAsset, name: "Unchecked hover", type: "image", source: { uri: "assets/unchecked-hover.png", mediaType: "image/png" } });
|
|
372
|
-
document.assets.push({ id: checkedHoverAsset, name: "Checked hover", type: "image", source: { uri: "assets/checked-hover.png", mediaType: "image/png" } });
|
|
373
|
-
scene.nodes.push({
|
|
374
|
-
id: checkboxId, name: "Agree", type: "checkbox", parentId: root.id, children: [], visible: true,
|
|
375
|
-
enabled: true, defaultChecked: false,
|
|
376
|
-
// Hover textures deliberately mirror the *other* base state, so a swap is unambiguous with only two Texture identities available offscreen.
|
|
377
|
-
states: { uncheckedAssetId: ids.asset, checkedAssetId: buttonIds.pressedAsset, uncheckedHoverAssetId: buttonIds.pressedAsset, checkedHoverAssetId: ids.asset },
|
|
378
|
-
transform: { x: 0, y: 0, width: 32, height: 32, scaleX: 1, scaleY: 1, rotation: 0 },
|
|
379
|
-
});
|
|
380
|
-
root.children.push(checkboxId);
|
|
381
|
-
const textures = new Map([[ids.asset, Texture.WHITE], [buttonIds.pressedAsset, Texture.EMPTY]]);
|
|
382
|
-
const view = buildSceneView(document, ids.scene, "desktop", { interaction: "runtime", textures }).nodeViews.get(checkboxId);
|
|
383
|
-
// Pixi's typed Container.emit requires a payload for known federated event names; the listener
|
|
384
|
-
// only reads the event name, so a loosely-typed emit is enough to exercise it here.
|
|
385
|
-
const innerView = view.children[0].innerView;
|
|
386
|
-
expect(shownStateTexture(view)).toBe(Texture.WHITE);
|
|
387
|
-
innerView.emit("pointerover");
|
|
388
|
-
expect(shownStateTexture(view)).toBe(Texture.EMPTY);
|
|
389
|
-
innerView.emit("pointerout");
|
|
390
|
-
expect(shownStateTexture(view)).toBe(Texture.WHITE);
|
|
391
|
-
view.checked = true;
|
|
392
|
-
expect(shownStateTexture(view)).toBe(Texture.EMPTY);
|
|
393
|
-
innerView.emit("pointerover");
|
|
394
|
-
expect(shownStateTexture(view)).toBe(Texture.WHITE);
|
|
395
|
-
});
|
|
396
|
-
it("stops dispatching pointer events to a disabled button without writing runtime state back to the document", () => {
|
|
397
|
-
const document = documentWithButton();
|
|
398
|
-
const view = buildSceneView(document, ids.scene, "desktop", { interaction: "runtime", textures: new Map([[ids.asset, Texture.WHITE]]) }).nodeViews.get(buttonIds.node);
|
|
399
|
-
const node = document.scenes[0].nodes.find((candidate) => candidate.id === buttonIds.node);
|
|
400
|
-
const button = view.children[0];
|
|
401
|
-
expect(view.enabled).toBe(true);
|
|
402
|
-
expect(button.eventMode).toBe("static");
|
|
403
|
-
view.enabled = false;
|
|
404
|
-
expect(view.enabled).toBe(false);
|
|
405
|
-
expect(view.state).toBe("disabled");
|
|
406
|
-
// Именно так @pixi/ui глушит press: EventSystem не доставляет события неинтерактивному view.
|
|
407
|
-
expect(button.eventMode).not.toBe("static");
|
|
408
|
-
// Enabled — только начальное presentation-состояние: runtime setter документ не трогает.
|
|
409
|
-
expect(node).toMatchObject({ type: "button", enabled: true });
|
|
410
|
-
});
|
|
411
|
-
it("builds an inert button on an authoring canvas while keeping the node itself grabbable", () => {
|
|
412
|
-
const document = documentWithButton();
|
|
413
|
-
const view = buildSceneView(document, ids.scene, "desktop", { interaction: "authoring", textures: new Map([[ids.asset, Texture.WHITE]]) }).nodeViews.get(buttonIds.node);
|
|
414
|
-
// FancyButton не реагирует на pointer во время authoring...
|
|
415
|
-
expect(view.children[0].eventMode).toBe("none");
|
|
416
|
-
// ...но выделение и drag идут через grab-прямоугольник самого NodeView и обязаны продолжать работать.
|
|
417
|
-
expect(view.containsPoint({ x: 60, y: 20 })).toBe(true);
|
|
418
|
-
});
|
|
419
|
-
it("applies identical layout coordinates to image, text, and Spine node containers", () => {
|
|
420
|
-
const document = renderDocument();
|
|
421
|
-
const scene = document.scenes[0];
|
|
422
|
-
const root = scene.nodes.find((node) => node.id === ids.root);
|
|
423
|
-
const image = scene.nodes.find((node) => node.id === ids.image);
|
|
424
|
-
const text = scene.nodes.find((node) => node.id === ids.text);
|
|
425
|
-
const spineId = "10000000-0000-4000-8000-000000000007";
|
|
426
|
-
const transform = { x: -240, y: -90, width: 320, height: 180, scaleX: 1.25, scaleY: 0.75, rotation: 0.2, anchorMinX: 1, anchorMaxX: 1, anchorMinY: 0.5, anchorMaxY: 0.5, pivotX: 0.75, pivotY: 0.5 };
|
|
427
|
-
image.transform = { ...transform };
|
|
428
|
-
text.transform = { ...transform };
|
|
429
|
-
const spine = { id: spineId, name: "Spine", type: "spine", assetId: ids.asset, parentId: ids.root, children: [], visible: true, transform: { ...transform } };
|
|
430
|
-
scene.nodes.push(spine);
|
|
431
|
-
root.children.push(spineId);
|
|
432
|
-
const views = buildSceneView(document, ids.scene, "desktop", { interaction: "authoring", textures: new Map([[ids.asset, Texture.WHITE]]) }).nodeViews;
|
|
433
|
-
const snapshot = (nodeId) => {
|
|
434
|
-
const view = views.get(nodeId);
|
|
435
|
-
return { position: { x: view.position.x, y: view.position.y }, pivot: { x: view.pivot.x, y: view.pivot.y }, scale: { x: view.scale.x, y: view.scale.y }, rotation: view.rotation };
|
|
436
|
-
};
|
|
437
|
-
expect(snapshot(ids.text)).toEqual(snapshot(ids.image));
|
|
438
|
-
expect(snapshot(spineId)).toEqual(snapshot(ids.image));
|
|
439
|
-
});
|
|
440
|
-
it("loads the repository fixture through migration and validation", () => {
|
|
441
|
-
const document = parseProjectDocumentJson(sampleJson);
|
|
442
|
-
const scene = document.scenes[0];
|
|
443
|
-
expect(scene.id).toBe(ids.scene);
|
|
444
|
-
expect(scene.rootNodeIds).toEqual([ids.root]);
|
|
445
|
-
expect(scene.nodes.find((node) => node.id === ids.image && node.type === "image")).toMatchObject({ assetId: ids.asset });
|
|
446
|
-
expect(scene.nodes.find((node) => node.id === ids.text)).toMatchObject({ bindingKey: "showcase.title" });
|
|
447
|
-
expect(scene.layout.referenceViewports).toHaveProperty("desktop");
|
|
448
|
-
expect(scene.layout.referenceViewports).toHaveProperty("mobile");
|
|
449
|
-
});
|
|
450
|
-
it("round-trips deterministically while retaining semantic child order", () => {
|
|
451
|
-
const document = parseProjectDocumentJson(sampleJson);
|
|
452
|
-
const serialized = serializeProjectDocument(document);
|
|
453
|
-
const roundTripped = parseProjectDocumentJson(serialized);
|
|
454
|
-
expect(roundTripped).toEqual(document);
|
|
455
|
-
expect(serializeProjectDocument(roundTripped)).toBe(serialized);
|
|
456
|
-
expect(roundTripped.scenes[0].nodes[0].children).toEqual([
|
|
457
|
-
ids.text,
|
|
458
|
-
"10000000-0000-4000-8000-000000000020",
|
|
459
|
-
"10000000-0000-4000-8000-000000000030",
|
|
460
|
-
"10000000-0000-4000-8000-000000000040",
|
|
461
|
-
"10000000-0000-4000-8000-000000000050",
|
|
462
|
-
"10000000-0000-4000-8000-000000000062",
|
|
463
|
-
"10000000-0000-4000-8000-000000000063",
|
|
464
|
-
]);
|
|
465
|
-
});
|
|
466
|
-
it("does not use display names as references", () => {
|
|
467
|
-
const document = parseProjectDocumentJson(sampleJson);
|
|
468
|
-
const renamed = clone(document);
|
|
469
|
-
const image = renamed.scenes[0].nodes.find((node) => node.id === ids.image);
|
|
470
|
-
image.name = "Renamed image";
|
|
471
|
-
expect(parseProjectDocumentJson(serializeProjectDocument(renamed)).scenes[0].nodes.find((node) => node.id === ids.image)).toMatchObject({ id: ids.image, assetId: ids.asset });
|
|
472
|
-
});
|
|
473
|
-
it("surfaces schema validation codes through the JSON loading boundary", () => {
|
|
474
|
-
const invalid = clone(parseProjectDocumentJson(sampleJson));
|
|
475
|
-
invalid.scenes[0].nodes.find((node) => node.id === ids.image).assetId = ids.text;
|
|
476
|
-
expect(() => parseProjectDocumentJson(JSON.stringify(invalid))).toThrow("MISSING_ASSET_REFERENCE");
|
|
477
|
-
});
|
|
478
|
-
it("distinguishes malformed JSON from schema errors", () => {
|
|
479
|
-
expect(() => parseProjectDocumentJson("{not json")).toThrow(ProjectDocumentJsonParseError);
|
|
480
|
-
const invalid = clone(parseProjectDocumentJson(sampleJson));
|
|
481
|
-
delete invalid.scenes[0].layout.referenceViewports.mobile;
|
|
482
|
-
expect(() => parseProjectDocumentJson(JSON.stringify(invalid))).toThrow(TypeError);
|
|
483
|
-
});
|
|
484
|
-
});
|
|
485
|
-
//# sourceMappingURL=index.test.js.map
|