@ng-org/alien-deepsignals 0.1.2-alpha.3 → 0.1.2-alpha.4
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/deepSignal.d.ts.map +1 -1
- package/dist/deepSignal.js +168 -16
- package/dist/hooks/svelte/useDeepSignal.svelte.d.ts +3 -7
- package/dist/hooks/svelte/useDeepSignal.svelte.d.ts.map +1 -1
- package/dist/hooks/svelte/useDeepSignal.svelte.js +34 -18
- package/dist/hooks/vue/useDeepSignal.d.ts +4 -3
- package/dist/hooks/vue/useDeepSignal.d.ts.map +1 -1
- package/dist/hooks/vue/useDeepSignal.js +52 -24
- package/dist/test/frontend/astro-app/src/components/PerfSuiteClient.d.ts +4 -0
- package/dist/test/frontend/astro-app/src/components/PerfSuiteClient.d.ts.map +1 -0
- package/dist/test/frontend/astro-app/src/components/PerfSuiteClient.js +225 -0
- package/dist/test/frontend/astro-app/src/components/ReactPanel.d.ts +4 -0
- package/dist/test/frontend/astro-app/src/components/ReactPanel.d.ts.map +1 -0
- package/dist/test/frontend/astro-app/src/components/ReactPanel.js +227 -0
- package/dist/test/frontend/astro-app/src/components/perf/react/ReactPerfDeep.d.ts +4 -0
- package/dist/test/frontend/astro-app/src/components/perf/react/ReactPerfDeep.d.ts.map +1 -0
- package/dist/test/frontend/astro-app/src/components/perf/react/ReactPerfDeep.js +150 -0
- package/dist/test/frontend/astro-app/src/components/perf/react/ReactPerfNative.d.ts +4 -0
- package/dist/test/frontend/astro-app/src/components/perf/react/ReactPerfNative.d.ts.map +1 -0
- package/dist/test/frontend/astro-app/src/components/perf/react/ReactPerfNative.js +184 -0
- package/dist/test/frontend/playwright/crossFrameworkHooks.spec.d.ts +2 -0
- package/dist/test/frontend/playwright/crossFrameworkHooks.spec.d.ts.map +1 -0
- package/dist/test/frontend/playwright/crossFrameworkHooks.spec.js +171 -0
- package/dist/test/frontend/playwright/perfSuite.spec.d.ts +2 -0
- package/dist/test/frontend/playwright/perfSuite.spec.d.ts.map +1 -0
- package/dist/test/frontend/playwright/perfSuite.spec.js +128 -0
- package/dist/test/frontend/utils/mockData.d.ts +53 -0
- package/dist/test/frontend/utils/mockData.d.ts.map +1 -0
- package/dist/test/frontend/utils/mockData.js +78 -0
- package/dist/test/frontend/utils/paths.d.ts +4 -0
- package/dist/test/frontend/utils/paths.d.ts.map +1 -0
- package/dist/test/frontend/utils/paths.js +28 -0
- package/dist/test/frontend/utils/perfScenarios.d.ts +15 -0
- package/dist/test/frontend/utils/perfScenarios.d.ts.map +1 -0
- package/dist/test/frontend/utils/perfScenarios.js +287 -0
- package/dist/test/frontend/utils/renderMetrics.d.ts +13 -0
- package/dist/test/frontend/utils/renderMetrics.d.ts.map +1 -0
- package/dist/test/frontend/utils/renderMetrics.js +45 -0
- package/dist/test/frontend/utils/state.d.ts +57 -0
- package/dist/test/frontend/utils/state.d.ts.map +1 -0
- package/dist/test/frontend/utils/state.js +79 -0
- package/dist/test/lib/core.test.d.ts +2 -0
- package/dist/test/lib/core.test.d.ts.map +1 -0
- package/dist/test/lib/core.test.js +53 -0
- package/dist/test/lib/deepSignalOptions.test.d.ts +2 -0
- package/dist/test/lib/deepSignalOptions.test.d.ts.map +1 -0
- package/dist/test/lib/deepSignalOptions.test.js +230 -0
- package/dist/test/lib/index.test.d.ts +2 -0
- package/dist/test/lib/index.test.d.ts.map +1 -0
- package/dist/test/lib/index.test.js +807 -0
- package/dist/test/lib/misc.test.d.ts +2 -0
- package/dist/test/lib/misc.test.d.ts.map +1 -0
- package/dist/test/lib/misc.test.js +140 -0
- package/dist/test/lib/watch.test.d.ts +2 -0
- package/dist/test/lib/watch.test.d.ts.map +1 -0
- package/dist/test/lib/watch.test.js +1280 -0
- package/package.json +36 -19
- package/src/index.ts +5 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const react_1 = __importStar(require("react"));
|
|
37
|
+
const mockData_1 = require("../../../../../utils/mockData");
|
|
38
|
+
const renderMetrics_1 = require("../../../../../utils/renderMetrics");
|
|
39
|
+
const perfScenarios_1 = require("../../../../../utils/perfScenarios");
|
|
40
|
+
const ObjectRow = ({ entry, updateEntries }) => {
|
|
41
|
+
const rowRenderCount = (0, react_1.useRef)(0);
|
|
42
|
+
rowRenderCount.current += 1;
|
|
43
|
+
(0, renderMetrics_1.recordObjectRender)("react", entry["@id"], rowRenderCount.current);
|
|
44
|
+
return (<div className="object-row" data-entry-id={entry["@id"]} data-render-count={rowRenderCount.current}>
|
|
45
|
+
<span className="object-id">{entry["@id"]}</span>
|
|
46
|
+
<input type="text" data-role="label" value={entry.label} onChange={(event) => updateEntries((draft) => {
|
|
47
|
+
const target = draft.find((item) => item["@id"] === entry["@id"]);
|
|
48
|
+
if (target)
|
|
49
|
+
target.label = event.target.value;
|
|
50
|
+
})}/>
|
|
51
|
+
<input type="number" data-role="count-input" value={entry.count} onChange={(event) => updateEntries((draft) => {
|
|
52
|
+
const target = draft.find((item) => item["@id"] === entry["@id"]);
|
|
53
|
+
if (target)
|
|
54
|
+
target.count = Number(event.target.value || 0);
|
|
55
|
+
})}/>
|
|
56
|
+
<span data-role="count">{entry.count}</span>
|
|
57
|
+
<button type="button" data-action="increment" onClick={() => updateEntries((draft) => {
|
|
58
|
+
const target = draft.find((item) => item["@id"] === entry["@id"]);
|
|
59
|
+
if (target)
|
|
60
|
+
target.count += 1;
|
|
61
|
+
})}>
|
|
62
|
+
+1
|
|
63
|
+
</button>
|
|
64
|
+
</div>);
|
|
65
|
+
};
|
|
66
|
+
const cloneInitialEntries = () => (0, mockData_1.cloneDefaultObjectSet)();
|
|
67
|
+
const ReactPerfNative = () => {
|
|
68
|
+
const [objectSet, setObjectSet] = (0, react_1.useState)(() => new Set(cloneInitialEntries()));
|
|
69
|
+
const renderCount = (0, react_1.useRef)(0);
|
|
70
|
+
const counterRef = (0, react_1.useRef)(0);
|
|
71
|
+
const [busy, setBusy] = (0, react_1.useState)(false);
|
|
72
|
+
renderCount.current += 1;
|
|
73
|
+
(0, react_1.useEffect)(() => {
|
|
74
|
+
(0, renderMetrics_1.recordRender)("react", renderCount.current);
|
|
75
|
+
});
|
|
76
|
+
const entries = (0, react_1.useMemo)(() => Array.from(objectSet.values()), [objectSet]);
|
|
77
|
+
const updateEntries = (0, react_1.useCallback)((mutate) => {
|
|
78
|
+
setObjectSet((prev) => {
|
|
79
|
+
const draft = Array.from(prev.values()).map((entry) => ({
|
|
80
|
+
...entry,
|
|
81
|
+
}));
|
|
82
|
+
mutate(draft);
|
|
83
|
+
return new Set(draft);
|
|
84
|
+
});
|
|
85
|
+
}, []);
|
|
86
|
+
(0, react_1.useEffect)(() => {
|
|
87
|
+
const dispose = (0, perfScenarios_1.registerScenarioAdapter)("react", "native", {
|
|
88
|
+
reset: () => {
|
|
89
|
+
setObjectSet(new Set(cloneInitialEntries()));
|
|
90
|
+
},
|
|
91
|
+
mutateExisting: (iterations) => {
|
|
92
|
+
updateEntries((draft) => {
|
|
93
|
+
if (!draft.length)
|
|
94
|
+
return;
|
|
95
|
+
for (let cycle = 0; cycle < iterations; cycle += 1) {
|
|
96
|
+
draft.forEach((entry, index) => {
|
|
97
|
+
entry.label = `POJO ${entry["@id"]} #${cycle}-${index}`;
|
|
98
|
+
entry.count += 2;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
bulkMutate: (iterations) => {
|
|
104
|
+
updateEntries((draft) => {
|
|
105
|
+
if (!draft.length)
|
|
106
|
+
return;
|
|
107
|
+
for (let i = 0; i < iterations; i += 1) {
|
|
108
|
+
for (const entry of draft) {
|
|
109
|
+
entry.count += 2;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
batchAddRemove: (iterations) => {
|
|
115
|
+
const additions = [];
|
|
116
|
+
for (let i = 0; i < iterations; i += 1) {
|
|
117
|
+
counterRef.current += 1;
|
|
118
|
+
const id = `react-native-${counterRef.current}`;
|
|
119
|
+
additions.push({
|
|
120
|
+
"@id": id,
|
|
121
|
+
label: `Native ${id}`,
|
|
122
|
+
count: i,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
updateEntries((draft) => {
|
|
126
|
+
draft.push(...additions);
|
|
127
|
+
});
|
|
128
|
+
updateEntries((draft) => {
|
|
129
|
+
const start = Math.max(draft.length - additions.length, 0);
|
|
130
|
+
draft.splice(start, additions.length);
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
return () => dispose();
|
|
135
|
+
}, [updateEntries]);
|
|
136
|
+
const handleAddEntry = () => {
|
|
137
|
+
counterRef.current += 1;
|
|
138
|
+
const id = `react-native-${counterRef.current}`;
|
|
139
|
+
updateEntries((draft) => {
|
|
140
|
+
draft.push({ "@id": id, label: `Native ${id}`, count: 0 });
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
const handleRemoveEntry = () => {
|
|
144
|
+
updateEntries((draft) => {
|
|
145
|
+
draft.pop();
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
const handleRunScenario = async () => {
|
|
149
|
+
try {
|
|
150
|
+
setBusy(true);
|
|
151
|
+
await (0, perfScenarios_1.runScenarioImmediately)("react", "native");
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
setBusy(false);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
return (<section className="perf-panel react" data-field="objectSet">
|
|
158
|
+
<h2 className="title">react (native state)</h2>
|
|
159
|
+
<div className="render-meta" data-render-count={renderCount.current}>
|
|
160
|
+
Render #{renderCount.current}
|
|
161
|
+
</div>
|
|
162
|
+
<div className="field" data-field="objectSet">
|
|
163
|
+
<legend>objectSet entries</legend>
|
|
164
|
+
<div className="set-controls">
|
|
165
|
+
<span data-role="set-size">Size: {objectSet.size}</span>
|
|
166
|
+
<div>
|
|
167
|
+
<button type="button" onClick={handleAddEntry}>
|
|
168
|
+
Add entry
|
|
169
|
+
</button>
|
|
170
|
+
<button type="button" onClick={handleRemoveEntry}>
|
|
171
|
+
Remove entry
|
|
172
|
+
</button>
|
|
173
|
+
<button type="button" data-action="run-scenario" onClick={handleRunScenario} disabled={busy}>
|
|
174
|
+
{busy ? "Running…" : "Run perf scenario"}
|
|
175
|
+
</button>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
<div className="object-set">
|
|
179
|
+
{entries.map((entry) => (<ObjectRow key={entry["@id"]} entry={entry} updateEntries={updateEntries}/>))}
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
</section>);
|
|
183
|
+
};
|
|
184
|
+
exports.default = ReactPerfNative;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crossFrameworkHooks.spec.d.ts","sourceRoot":"","sources":["../../../../src/test/frontend/playwright/crossFrameworkHooks.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const test_1 = require("@playwright/test");
|
|
4
|
+
const mockData_1 = require("../utils/mockData");
|
|
5
|
+
const frameworks = ["react", "vue", "svelte"];
|
|
6
|
+
const alphaEntry = Array.from(mockData_1.mockTestObject.objectSet).find((entry) => entry["@id"] === "urn:object:alpha");
|
|
7
|
+
if (!alphaEntry) {
|
|
8
|
+
throw new Error("mock data must include an objectSet entry with @id urn:object:alpha");
|
|
9
|
+
}
|
|
10
|
+
const fieldLocator = (page, framework, key) => page.locator(`.${framework} [data-field='${key}']`);
|
|
11
|
+
const createTextPlan = (key, nextValue) => ({
|
|
12
|
+
key,
|
|
13
|
+
mutate: async (page, framework) => {
|
|
14
|
+
const field = fieldLocator(page, framework, key);
|
|
15
|
+
const input = field.locator("input[data-role='editor']");
|
|
16
|
+
await input.fill(nextValue);
|
|
17
|
+
await input.blur();
|
|
18
|
+
},
|
|
19
|
+
assert: async (page, framework) => {
|
|
20
|
+
const field = fieldLocator(page, framework, key);
|
|
21
|
+
await (0, test_1.expect)(field.locator("[data-role='value']")).toHaveText(nextValue);
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
const createNumberPlan = (key, nextValue) => ({
|
|
25
|
+
key,
|
|
26
|
+
mutate: async (page, framework) => {
|
|
27
|
+
const field = fieldLocator(page, framework, key);
|
|
28
|
+
const input = field.locator("input[data-role='editor']");
|
|
29
|
+
await input.fill(String(nextValue));
|
|
30
|
+
await input.blur();
|
|
31
|
+
},
|
|
32
|
+
assert: async (page, framework) => {
|
|
33
|
+
const field = fieldLocator(page, framework, key);
|
|
34
|
+
await (0, test_1.expect)(field.locator("[data-role='value']")).toHaveText(String(nextValue));
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
const createBooleanPlan = (key, nextValue) => ({
|
|
38
|
+
key,
|
|
39
|
+
mutate: async (page, framework) => {
|
|
40
|
+
const field = fieldLocator(page, framework, key);
|
|
41
|
+
const input = field.locator("input[data-role='editor']");
|
|
42
|
+
await input.setChecked(nextValue);
|
|
43
|
+
},
|
|
44
|
+
assert: async (page, framework) => {
|
|
45
|
+
const field = fieldLocator(page, framework, key);
|
|
46
|
+
await (0, test_1.expect)(field.locator("[data-role='value']")).toHaveText(String(nextValue));
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
const createArrayPlan = (key, initialLength, additions) => ({
|
|
50
|
+
key,
|
|
51
|
+
mutate: async (page, framework) => {
|
|
52
|
+
const field = fieldLocator(page, framework, key);
|
|
53
|
+
const addButton = field.locator("button[data-action='push']");
|
|
54
|
+
for (let i = 0; i < additions; i++) {
|
|
55
|
+
await addButton.click();
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
assert: async (page, framework) => {
|
|
59
|
+
const expectedLength = initialLength + additions;
|
|
60
|
+
const field = fieldLocator(page, framework, key);
|
|
61
|
+
await (0, test_1.expect)(field.locator("[data-role='array-length']")).toHaveText(`Length: ${expectedLength}`);
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
const createSetPlan = (key, initialSize, additions) => ({
|
|
65
|
+
key,
|
|
66
|
+
mutate: async (page, framework) => {
|
|
67
|
+
const field = fieldLocator(page, framework, key);
|
|
68
|
+
const addButton = field.locator("button[data-action='add']");
|
|
69
|
+
for (let i = 0; i < additions; i++) {
|
|
70
|
+
await addButton.click();
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
assert: async (page, framework) => {
|
|
74
|
+
const expectedSize = initialSize + additions;
|
|
75
|
+
const field = fieldLocator(page, framework, key);
|
|
76
|
+
await (0, test_1.expect)(field.locator("[data-role='set-size']")).toHaveText(`Size: ${expectedSize}`);
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
const createObjectSetPlan = (entryId, nextLabel, expectedCount) => ({
|
|
80
|
+
key: "objectSet",
|
|
81
|
+
mutate: async (page, framework) => {
|
|
82
|
+
const entry = fieldLocator(page, framework, "objectSet").locator(`[data-entry-id='${entryId}']`);
|
|
83
|
+
const labelInput = entry.locator("input[data-role='label']");
|
|
84
|
+
await labelInput.fill(nextLabel);
|
|
85
|
+
await labelInput.blur();
|
|
86
|
+
await entry.locator("button[data-action='increment']").click();
|
|
87
|
+
},
|
|
88
|
+
assert: async (page, framework) => {
|
|
89
|
+
const entry = fieldLocator(page, framework, "objectSet").locator(`[data-entry-id='${entryId}']`);
|
|
90
|
+
await (0, test_1.expect)(entry.locator("input[data-role='label']")).toHaveValue(nextLabel);
|
|
91
|
+
await (0, test_1.expect)(entry.locator("[data-role='count']")).toHaveText(String(expectedCount));
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
const fieldPlans = [
|
|
95
|
+
createTextPlan("type", "TestObjectUpdated"),
|
|
96
|
+
createTextPlan("stringValue", "string_changed"),
|
|
97
|
+
createNumberPlan("numValue", 1337),
|
|
98
|
+
createBooleanPlan("boolValue", false),
|
|
99
|
+
createTextPlan("objectValue.nestedString", "nested_changed"),
|
|
100
|
+
createNumberPlan("objectValue.nestedNum", 77),
|
|
101
|
+
createArrayPlan("arrayValue", mockData_1.mockTestObject.arrayValue.length, 2),
|
|
102
|
+
createArrayPlan("objectValue.nestedArray", mockData_1.mockTestObject.objectValue.nestedArray.length, 1),
|
|
103
|
+
createSetPlan("setValue", mockData_1.mockTestObject.setValue.size, 1),
|
|
104
|
+
createObjectSetPlan(alphaEntry["@id"], "Alpha updated", alphaEntry.count + 1),
|
|
105
|
+
];
|
|
106
|
+
const getRenderCounts = (page) => page.evaluate(() => ({ ...(window.renderCounts ?? {}) }));
|
|
107
|
+
const waitForFrameworkReady = async (page, framework) => {
|
|
108
|
+
try {
|
|
109
|
+
await page.waitForSelector(`.${framework} .title`, {
|
|
110
|
+
state: "visible",
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
throw new Error(`${framework} panel did not render within 10 seconds: ${error}`);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
test_1.test.beforeEach(async ({ page }) => {
|
|
118
|
+
await page.goto("/");
|
|
119
|
+
await waitForFrameworkReady(page, "react");
|
|
120
|
+
await waitForFrameworkReady(page, "vue");
|
|
121
|
+
await waitForFrameworkReady(page, "svelte");
|
|
122
|
+
await page.waitForFunction("window.testHarness?.ready === true");
|
|
123
|
+
await page.evaluate(() => window.testHarness?.resetState());
|
|
124
|
+
});
|
|
125
|
+
(0, test_1.test)("components load", async ({ page }) => {
|
|
126
|
+
for (const framework of frameworks) {
|
|
127
|
+
await (0, test_1.expect)(page.locator(`.${framework} .title`)).toHaveText(framework);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
test_1.test.describe("cross framework propagation", () => {
|
|
131
|
+
for (const source of frameworks) {
|
|
132
|
+
for (const target of frameworks) {
|
|
133
|
+
if (source === target)
|
|
134
|
+
continue;
|
|
135
|
+
(0, test_1.test)(`${source} edits propagate to ${target}`, async ({ page }) => {
|
|
136
|
+
await test_1.test.step(`mutate in ${source}`, async () => {
|
|
137
|
+
for (const plan of fieldPlans) {
|
|
138
|
+
await plan.mutate(page, source);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
await page.waitForTimeout(100);
|
|
142
|
+
await test_1.test.step(`assert in ${target}`, async () => {
|
|
143
|
+
for (const plan of fieldPlans) {
|
|
144
|
+
await plan.assert(page, target);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
await test_1.test.step(`validate mutated source ${source}`, async () => {
|
|
148
|
+
for (const plan of fieldPlans) {
|
|
149
|
+
await plan.assert(page, source);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
(0, test_1.test)("hidden mutations do not trigger renders", async ({ page }) => {
|
|
157
|
+
const before = await getRenderCounts(page);
|
|
158
|
+
await page.evaluate(() => {
|
|
159
|
+
if (window.sharedState) {
|
|
160
|
+
window.sharedState.hiddenValue = Math.random();
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
await page.waitForTimeout(50);
|
|
164
|
+
const after = await getRenderCounts(page);
|
|
165
|
+
for (const framework of frameworks) {
|
|
166
|
+
const previous = before[framework] ?? 0;
|
|
167
|
+
const current = after[framework] ?? 0;
|
|
168
|
+
(0, test_1.expect)(current).toBeGreaterThanOrEqual(previous);
|
|
169
|
+
(0, test_1.expect)(current - previous).toBeLessThanOrEqual(2);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"perfSuite.spec.d.ts","sourceRoot":"","sources":["../../../../src/test/frontend/playwright/perfSuite.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const test_1 = require("@playwright/test");
|
|
4
|
+
const frameworks = ["react", "vue", "svelte"];
|
|
5
|
+
const scenarios = frameworks.flatMap((framework) => [
|
|
6
|
+
{ framework, variant: "deep" },
|
|
7
|
+
{ framework, variant: "native" },
|
|
8
|
+
]);
|
|
9
|
+
const waitForFrameworkReady = async (page, framework) => {
|
|
10
|
+
await page.waitForSelector(`.${framework} .title`, {
|
|
11
|
+
state: "visible",
|
|
12
|
+
});
|
|
13
|
+
await page.waitForFunction((name) => (window.renderEventCounts?.[name] ?? 0) > 0, framework, { timeout: 5_000 });
|
|
14
|
+
};
|
|
15
|
+
const waitForPerfSuiteReady = async (page, framework, variant) => {
|
|
16
|
+
await page.waitForFunction(({ framework, variant }) => Boolean(window.perfSuite?.runners?.[framework]?.[variant]), { framework, variant }, { timeout: 15_000 });
|
|
17
|
+
};
|
|
18
|
+
const navigateToScenario = async (page, framework, variant) => {
|
|
19
|
+
await page.goto(`/perf?framework=${framework}&variant=${variant}`);
|
|
20
|
+
await waitForFrameworkReady(page, framework);
|
|
21
|
+
await waitForPerfSuiteReady(page, framework, variant);
|
|
22
|
+
};
|
|
23
|
+
const runScenario = async (page, framework, variant, counts) => {
|
|
24
|
+
return page.evaluate(({ framework, variant, counts }) => window.perfSuite?.runScenario?.(framework, variant, counts), { framework, variant, counts });
|
|
25
|
+
};
|
|
26
|
+
const formatDuration = (value) => typeof value === "number" ? `${value.toFixed(1)}ms` : "n/a";
|
|
27
|
+
const logScenarioOverview = (framework, variant, result) => {
|
|
28
|
+
if (!result) {
|
|
29
|
+
// eslint-disable-next-line no-console
|
|
30
|
+
console.log(`[perfSuite] ${framework}/${variant} produced no result`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const mutate = result.blocks?.mutateExisting;
|
|
34
|
+
const renderCount = mutate?.renderCounts?.[framework] ?? 0;
|
|
35
|
+
const touchedIds = Object.keys(mutate?.objectRenderCounts?.[framework] ?? {});
|
|
36
|
+
const sample = touchedIds.slice(0, 5);
|
|
37
|
+
const sampleDisplay = sample.length
|
|
38
|
+
? ` [${sample.join(", ")}${touchedIds.length > sample.length ? ", ..." : ""}]`
|
|
39
|
+
: "";
|
|
40
|
+
// eslint-disable-next-line no-console
|
|
41
|
+
console.log(`[perfSuite] ${framework}/${variant} total=${formatDuration(result.totalDuration)} mutate=${formatDuration(mutate?.duration)} renders=${renderCount} touched=${touchedIds.length}${sampleDisplay}`);
|
|
42
|
+
};
|
|
43
|
+
test_1.test.describe("perfSuite object-set scenarios", () => {
|
|
44
|
+
const baseCounts = {
|
|
45
|
+
primitives: 96,
|
|
46
|
+
nested: 64,
|
|
47
|
+
arrays: 40,
|
|
48
|
+
sets: 40,
|
|
49
|
+
objectSet: 48,
|
|
50
|
+
};
|
|
51
|
+
for (const { framework, variant } of scenarios) {
|
|
52
|
+
(0, test_1.test)(`${framework}/${variant} runner reports block metrics`, async ({ page, }) => {
|
|
53
|
+
await navigateToScenario(page, framework, variant);
|
|
54
|
+
if (framework === "react" && variant === "deep") {
|
|
55
|
+
const debugInfo = await page.evaluate(() => ({
|
|
56
|
+
identity: window.__reactSharedIdentity,
|
|
57
|
+
globalMatch: window.__reactSharedGlobal,
|
|
58
|
+
watchHits: window.__reactWatchHits,
|
|
59
|
+
adapterMatch: window.__adapterSharedIdentity,
|
|
60
|
+
version: window.sharedStateVersion,
|
|
61
|
+
}));
|
|
62
|
+
// eslint-disable-next-line no-console
|
|
63
|
+
console.log(`[debug] react/deep identity=${debugInfo.identity} global=${debugInfo.globalMatch} adapter=${debugInfo.adapterMatch} watchHits=${debugInfo.watchHits} version=${debugInfo.version}`);
|
|
64
|
+
}
|
|
65
|
+
const result = await runScenario(page, framework, variant, baseCounts);
|
|
66
|
+
(0, test_1.expect)(result).toBeTruthy();
|
|
67
|
+
const mutateBlock = result?.blocks?.mutateExisting;
|
|
68
|
+
logScenarioOverview(framework, variant, result);
|
|
69
|
+
(0, test_1.expect)(mutateBlock?.duration ?? 0).toBeGreaterThan(0);
|
|
70
|
+
const alphaRenders = mutateBlock?.objectRenderCounts?.[framework]?.["urn:object:alpha"] ?? 0;
|
|
71
|
+
(0, test_1.expect)(alphaRenders).toBeGreaterThan(0);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
(0, test_1.test)("objectSet counts are configurable (react/deep)", async ({ page }) => {
|
|
75
|
+
const framework = "react";
|
|
76
|
+
const variant = "deep";
|
|
77
|
+
await navigateToScenario(page, framework, variant);
|
|
78
|
+
const light = await runScenario(page, framework, variant, {
|
|
79
|
+
objectSet: 1,
|
|
80
|
+
});
|
|
81
|
+
const heavy = await runScenario(page, framework, variant, {
|
|
82
|
+
objectSet: 8,
|
|
83
|
+
});
|
|
84
|
+
(0, test_1.expect)(light).toBeTruthy();
|
|
85
|
+
(0, test_1.expect)(heavy).toBeTruthy();
|
|
86
|
+
const lightAlpha = light?.blocks?.mutateExisting?.objectRenderCounts?.react?.["urn:object:alpha"] ?? 0;
|
|
87
|
+
const heavyAlpha = heavy?.blocks?.mutateExisting?.objectRenderCounts?.react?.["urn:object:alpha"] ?? 0;
|
|
88
|
+
(0, test_1.expect)(heavyAlpha).toBeGreaterThanOrEqual(lightAlpha);
|
|
89
|
+
});
|
|
90
|
+
for (const framework of frameworks) {
|
|
91
|
+
(0, test_1.test)(`${framework}/deep only re-renders touched object`, async ({ page, }) => {
|
|
92
|
+
const variant = "deep";
|
|
93
|
+
await navigateToScenario(page, framework, variant);
|
|
94
|
+
const targetId = "urn:object:alpha";
|
|
95
|
+
const peerId = "urn:object:beta";
|
|
96
|
+
const targetRow = page.locator(`[data-entry-id="${targetId}"]`);
|
|
97
|
+
const peerRow = page.locator(`[data-entry-id="${peerId}"]`);
|
|
98
|
+
await Promise.all([targetRow.waitFor(), peerRow.waitFor()]);
|
|
99
|
+
const readRenderCount = async (locator) => Number((await locator.getAttribute("data-render-count")) ?? 0);
|
|
100
|
+
const targetBefore = await readRenderCount(targetRow);
|
|
101
|
+
const peerBefore = await readRenderCount(peerRow);
|
|
102
|
+
await page.evaluate((id) => {
|
|
103
|
+
const state = window.sharedState;
|
|
104
|
+
const entry = Array.from(state.objectSet.values()).find((item) => item["@id"] === id);
|
|
105
|
+
if (!entry)
|
|
106
|
+
throw new Error("Entry not found");
|
|
107
|
+
entry.count += 1;
|
|
108
|
+
}, targetId);
|
|
109
|
+
await page.waitForFunction(({ selector, before }) => {
|
|
110
|
+
const element = document.querySelector(selector);
|
|
111
|
+
if (!element)
|
|
112
|
+
return false;
|
|
113
|
+
const count = Number(element.getAttribute("data-render-count") ?? "0");
|
|
114
|
+
return count > before;
|
|
115
|
+
}, {
|
|
116
|
+
selector: `[data-entry-id="${targetId}"]`,
|
|
117
|
+
before: targetBefore,
|
|
118
|
+
});
|
|
119
|
+
const targetAfter = await readRenderCount(targetRow);
|
|
120
|
+
const peerAfter = await readRenderCount(peerRow);
|
|
121
|
+
(0, test_1.expect)(targetAfter).toBeGreaterThan(targetBefore);
|
|
122
|
+
if (framework === "react") {
|
|
123
|
+
test_1.test.fail(true, "React reconciles the entire list because the single deepSignal subscription sits at the component root, so any Set mutation invalidates every row.");
|
|
124
|
+
}
|
|
125
|
+
(0, test_1.expect)(peerAfter).toBe(peerBefore);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export type PerfCounters = {
|
|
2
|
+
react: number;
|
|
3
|
+
svelte: number;
|
|
4
|
+
vue: number;
|
|
5
|
+
};
|
|
6
|
+
export interface TestState {
|
|
7
|
+
type: string;
|
|
8
|
+
stringValue: string;
|
|
9
|
+
numValue: number;
|
|
10
|
+
boolValue: boolean;
|
|
11
|
+
nullValue: null;
|
|
12
|
+
hiddenValue: number;
|
|
13
|
+
arrayValue: number[];
|
|
14
|
+
objectValue: {
|
|
15
|
+
nestedString: string;
|
|
16
|
+
nestedNum: number;
|
|
17
|
+
nestedArray: number[];
|
|
18
|
+
};
|
|
19
|
+
setValue: Set<string>;
|
|
20
|
+
objectSet: Set<TaggedObject>;
|
|
21
|
+
count: number;
|
|
22
|
+
perfCounters: PerfCounters;
|
|
23
|
+
}
|
|
24
|
+
export interface TaggedObject {
|
|
25
|
+
"@id": string;
|
|
26
|
+
label: string;
|
|
27
|
+
count: number;
|
|
28
|
+
}
|
|
29
|
+
export declare const mockTestObject: Readonly<{
|
|
30
|
+
type: string;
|
|
31
|
+
stringValue: string;
|
|
32
|
+
numValue: number;
|
|
33
|
+
boolValue: true;
|
|
34
|
+
nullValue: null;
|
|
35
|
+
hiddenValue: number;
|
|
36
|
+
arrayValue: number[];
|
|
37
|
+
objectValue: {
|
|
38
|
+
nestedString: string;
|
|
39
|
+
nestedNum: number;
|
|
40
|
+
nestedArray: number[];
|
|
41
|
+
};
|
|
42
|
+
setValue: Set<string>;
|
|
43
|
+
objectSet: Set<TaggedObject>;
|
|
44
|
+
count: number;
|
|
45
|
+
perfCounters: {
|
|
46
|
+
react: number;
|
|
47
|
+
svelte: number;
|
|
48
|
+
vue: number;
|
|
49
|
+
};
|
|
50
|
+
}>;
|
|
51
|
+
export declare function cloneDefaultObjectSet(): TaggedObject[];
|
|
52
|
+
export declare function buildInitialState(): TestState;
|
|
53
|
+
//# sourceMappingURL=mockData.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mockData.d.ts","sourceRoot":"","sources":["../../../../src/test/frontend/utils/mockData.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,WAAW,SAAS;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,IAAI,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,WAAW,EAAE;QACT,YAAY,EAAE,MAAM,CAAC;QACrB,SAAS,EAAE,MAAM,CAAC;QAClB,WAAW,EAAE,MAAM,EAAE,CAAC;KACzB,CAAC;IACF,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACtB,SAAS,EAAE,GAAG,CAAC,YAAY,CAAC,CAAC;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,YAAY,CAAC;CAC9B;AAED,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACjB;AA4BD,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;EA0BzB,CAAC;AAEH,wBAAgB,qBAAqB,IAAI,YAAY,EAAE,CAEtD;AAED,wBAAgB,iBAAiB,IAAI,SAAS,CAyB7C"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mockTestObject = void 0;
|
|
4
|
+
exports.cloneDefaultObjectSet = cloneDefaultObjectSet;
|
|
5
|
+
exports.buildInitialState = buildInitialState;
|
|
6
|
+
const buildDefaultObjectSetEntries = () => {
|
|
7
|
+
const baseEntries = [
|
|
8
|
+
{
|
|
9
|
+
"@id": "urn:object:alpha",
|
|
10
|
+
label: "Alpha",
|
|
11
|
+
count: 1,
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"@id": "urn:object:beta",
|
|
15
|
+
label: "Beta",
|
|
16
|
+
count: 3,
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
const extraEntries = Array.from({ length: 298 }, (_, index) => {
|
|
20
|
+
const idNumber = (index + 1).toString().padStart(3, "0");
|
|
21
|
+
return {
|
|
22
|
+
"@id": `urn:object:item-${idNumber}`,
|
|
23
|
+
label: `Item ${idNumber}`,
|
|
24
|
+
count: 5 + index,
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
return [...baseEntries, ...extraEntries];
|
|
28
|
+
};
|
|
29
|
+
const defaultObjectSetEntries = buildDefaultObjectSetEntries();
|
|
30
|
+
exports.mockTestObject = Object.freeze({
|
|
31
|
+
type: "TestObject",
|
|
32
|
+
stringValue: "string",
|
|
33
|
+
numValue: 42,
|
|
34
|
+
boolValue: true,
|
|
35
|
+
nullValue: null,
|
|
36
|
+
hiddenValue: 0,
|
|
37
|
+
arrayValue: [1, 2, 3],
|
|
38
|
+
objectValue: {
|
|
39
|
+
nestedString: "nested",
|
|
40
|
+
nestedNum: 7,
|
|
41
|
+
nestedArray: [10, 12],
|
|
42
|
+
},
|
|
43
|
+
setValue: new Set(["v1", "v2", "v3"]),
|
|
44
|
+
objectSet: new Set(defaultObjectSetEntries.map((entry) => ({ ...entry }))),
|
|
45
|
+
count: 0,
|
|
46
|
+
perfCounters: {
|
|
47
|
+
react: 0,
|
|
48
|
+
svelte: 0,
|
|
49
|
+
vue: 0,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
function cloneDefaultObjectSet() {
|
|
53
|
+
return defaultObjectSetEntries.map((entry) => ({ ...entry }));
|
|
54
|
+
}
|
|
55
|
+
function buildInitialState() {
|
|
56
|
+
return {
|
|
57
|
+
type: exports.mockTestObject.type,
|
|
58
|
+
stringValue: exports.mockTestObject.stringValue,
|
|
59
|
+
numValue: exports.mockTestObject.numValue,
|
|
60
|
+
boolValue: exports.mockTestObject.boolValue,
|
|
61
|
+
nullValue: exports.mockTestObject.nullValue,
|
|
62
|
+
hiddenValue: exports.mockTestObject.hiddenValue,
|
|
63
|
+
arrayValue: [...exports.mockTestObject.arrayValue],
|
|
64
|
+
objectValue: {
|
|
65
|
+
nestedString: exports.mockTestObject.objectValue.nestedString,
|
|
66
|
+
nestedNum: exports.mockTestObject.objectValue.nestedNum,
|
|
67
|
+
nestedArray: [...exports.mockTestObject.objectValue.nestedArray],
|
|
68
|
+
},
|
|
69
|
+
setValue: new Set(Array.from(exports.mockTestObject.setValue)),
|
|
70
|
+
objectSet: new Set(Array.from(exports.mockTestObject.objectSet).map((entry) => ({ ...entry }))),
|
|
71
|
+
count: exports.mockTestObject.count,
|
|
72
|
+
perfCounters: {
|
|
73
|
+
react: 0,
|
|
74
|
+
svelte: 0,
|
|
75
|
+
vue: 0,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../../../src/test/frontend/utils/paths.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,CAAC;AAK1C,wBAAgB,SAAS,CAAC,CAAC,GAAG,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAM3E;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,OAAO,CAWxE"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getByPath = getByPath;
|
|
4
|
+
exports.setByPath = setByPath;
|
|
5
|
+
const toSegment = (segment) => segment.match(/^\d+$/) ? Number(segment) : segment;
|
|
6
|
+
function getByPath(source, path) {
|
|
7
|
+
const segments = path.split(".").filter(Boolean).map(toSegment);
|
|
8
|
+
return segments.reduce((acc, key) => {
|
|
9
|
+
if (acc == null)
|
|
10
|
+
return undefined;
|
|
11
|
+
return acc[key];
|
|
12
|
+
}, source);
|
|
13
|
+
}
|
|
14
|
+
function setByPath(source, path, value) {
|
|
15
|
+
const segments = path.split(".").filter(Boolean).map(toSegment);
|
|
16
|
+
if (!segments.length)
|
|
17
|
+
return false;
|
|
18
|
+
const lastKey = segments.pop();
|
|
19
|
+
const parent = segments.reduce((acc, key) => {
|
|
20
|
+
if (acc == null)
|
|
21
|
+
return undefined;
|
|
22
|
+
return acc[key];
|
|
23
|
+
}, source);
|
|
24
|
+
if (parent == null)
|
|
25
|
+
return false;
|
|
26
|
+
parent[lastKey] = value;
|
|
27
|
+
return true;
|
|
28
|
+
}
|