@lytjs/plugin-testing 6.4.0

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/index.js ADDED
@@ -0,0 +1,487 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/index.ts
9
+ import { definePlugin } from "@lytjs/core";
10
+ function createMockFn(options = {}) {
11
+ let returnValue = void 0;
12
+ let implementation = options.implementation;
13
+ const calls = [];
14
+ const mockFn = ((...args) => {
15
+ calls.push(args);
16
+ if (implementation) {
17
+ return implementation(...args);
18
+ }
19
+ return returnValue;
20
+ });
21
+ Object.defineProperties(mockFn, {
22
+ callCount: {
23
+ get: () => calls.length
24
+ },
25
+ calls: {
26
+ get: () => [...calls]
27
+ },
28
+ lastCall: {
29
+ get: () => calls[calls.length - 1]
30
+ },
31
+ originalFn: {
32
+ get: () => options.preserveOriginal ? void 0 : void 0
33
+ }
34
+ });
35
+ mockFn.mockReturnValue = (value) => {
36
+ returnValue = value;
37
+ };
38
+ mockFn.mockImplementation = (fn) => {
39
+ implementation = fn;
40
+ };
41
+ mockFn.mockReset = () => {
42
+ calls.length = 0;
43
+ returnValue = void 0;
44
+ implementation = void 0;
45
+ };
46
+ mockFn.mockClear = () => {
47
+ calls.length = 0;
48
+ };
49
+ return mockFn;
50
+ }
51
+ function createDOMTestHelpers() {
52
+ const getElement = (selectorOrElement) => {
53
+ if (typeof selectorOrElement === "string") {
54
+ const el = document.querySelector(selectorOrElement);
55
+ if (!el) {
56
+ throw new Error(`Element not found: ${selectorOrElement}`);
57
+ }
58
+ return el;
59
+ }
60
+ return selectorOrElement;
61
+ };
62
+ return {
63
+ waitForElement: async (selector, timeout = 5e3) => {
64
+ const startTime = Date.now();
65
+ while (Date.now() - startTime < timeout) {
66
+ const el = document.querySelector(selector);
67
+ if (el) {
68
+ return el;
69
+ }
70
+ await new Promise((resolve) => setTimeout(resolve, 10));
71
+ }
72
+ throw new Error(`Element not found within timeout: ${selector}`);
73
+ },
74
+ waitForElementToDisappear: async (selector, timeout = 5e3) => {
75
+ const startTime = Date.now();
76
+ while (Date.now() - startTime < timeout) {
77
+ const el = document.querySelector(selector);
78
+ if (!el) {
79
+ return;
80
+ }
81
+ await new Promise((resolve) => setTimeout(resolve, 10));
82
+ }
83
+ throw new Error(`Element did not disappear within timeout: ${selector}`);
84
+ },
85
+ waitForText: async (text, timeout = 5e3) => {
86
+ const startTime = Date.now();
87
+ while (Date.now() - startTime < timeout) {
88
+ const el = Array.from(document.body.querySelectorAll("*")).find(
89
+ (el2) => el2.textContent?.includes(text)
90
+ );
91
+ if (el) {
92
+ return el;
93
+ }
94
+ await new Promise((resolve) => setTimeout(resolve, 10));
95
+ }
96
+ throw new Error(`Text not found within timeout: ${text}`);
97
+ },
98
+ fillForm: (data) => {
99
+ for (const [name, value] of Object.entries(data)) {
100
+ const el = document.querySelector(`[name="${name}"]`);
101
+ if (el) {
102
+ if (typeof value === "boolean" && "checked" in el) {
103
+ el.checked = value;
104
+ el.dispatchEvent(new Event("change", { bubbles: true }));
105
+ } else if ("value" in el) {
106
+ el.value = String(value);
107
+ el.dispatchEvent(new Event("input", { bubbles: true }));
108
+ el.dispatchEvent(new Event("change", { bubbles: true }));
109
+ }
110
+ }
111
+ }
112
+ },
113
+ click: (selectorOrElement) => {
114
+ const el = getElement(selectorOrElement);
115
+ el.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
116
+ },
117
+ exists: (selector) => {
118
+ return document.querySelector(selector) !== null;
119
+ },
120
+ isVisible: (selectorOrElement) => {
121
+ const el = getElement(selectorOrElement);
122
+ const style = window.getComputedStyle(el);
123
+ return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0";
124
+ },
125
+ isDisabled: (selectorOrElement) => {
126
+ const el = getElement(selectorOrElement);
127
+ return "disabled" in el && el.disabled;
128
+ },
129
+ text: (selectorOrElement) => {
130
+ const el = getElement(selectorOrElement);
131
+ return el.textContent || "";
132
+ },
133
+ attribute: (selectorOrElement, name) => {
134
+ const el = getElement(selectorOrElement);
135
+ return el.getAttribute(name);
136
+ },
137
+ classes: (selectorOrElement) => {
138
+ const el = getElement(selectorOrElement);
139
+ return Array.from(el.classList);
140
+ },
141
+ hasClass: (selectorOrElement, className) => {
142
+ const el = getElement(selectorOrElement);
143
+ return el.classList.contains(className);
144
+ }
145
+ };
146
+ }
147
+ function createSignalTestHelpers() {
148
+ return {
149
+ trackUpdates: (signal) => {
150
+ const history = [signal.value];
151
+ let updateCount = 0;
152
+ let currentValue = signal.value;
153
+ const originalDescriptor = Object.getOwnPropertyDescriptor(signal, "value");
154
+ Object.defineProperty(signal, "value", {
155
+ get() {
156
+ return currentValue;
157
+ },
158
+ set(newValue) {
159
+ history.push(newValue);
160
+ updateCount++;
161
+ currentValue = newValue;
162
+ },
163
+ enumerable: originalDescriptor?.enumerable ?? true,
164
+ configurable: true
165
+ });
166
+ return {
167
+ get value() {
168
+ return currentValue;
169
+ },
170
+ set value(newValue) {
171
+ history.push(newValue);
172
+ updateCount++;
173
+ currentValue = newValue;
174
+ },
175
+ get updateCount() {
176
+ return updateCount;
177
+ },
178
+ get history() {
179
+ return [...history];
180
+ }
181
+ };
182
+ },
183
+ waitForUpdate: async (signal, timeout = 5e3) => {
184
+ const initialValue = signal.value;
185
+ const startTime = Date.now();
186
+ while (Date.now() - startTime < timeout) {
187
+ if (signal.value !== initialValue) {
188
+ return;
189
+ }
190
+ await new Promise((resolve) => setTimeout(resolve, 10));
191
+ }
192
+ throw new Error("Signal did not update within timeout");
193
+ }
194
+ };
195
+ }
196
+ function createFuzzTestHelpers() {
197
+ const randomString = (options = {}) => {
198
+ const maxLength = options.maxLength ?? 100;
199
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}|;:,.<>?";
200
+ const length = Math.floor(Math.random() * maxLength) + 1;
201
+ let result = "";
202
+ for (let i = 0; i < length; i++) {
203
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
204
+ }
205
+ return result;
206
+ };
207
+ const randomNumber = (options = {}) => {
208
+ const min = options.min ?? -1e3;
209
+ const max = options.max ?? 1e3;
210
+ return Math.random() * (max - min) + min;
211
+ };
212
+ const randomBoolean = () => {
213
+ return Math.random() < 0.5;
214
+ };
215
+ const randomArray = (generator, options = {}) => {
216
+ const maxLength = options.maxLength ?? 20;
217
+ const length = Math.floor(Math.random() * maxLength) + 1;
218
+ const result = [];
219
+ for (let i = 0; i < length; i++) {
220
+ result.push(generator());
221
+ }
222
+ return result;
223
+ };
224
+ const randomObject = (options = {}) => {
225
+ const maxLength = options.maxLength ?? 10;
226
+ const length = Math.floor(Math.random() * maxLength) + 1;
227
+ const result = {};
228
+ for (let i = 0; i < length; i++) {
229
+ const key = `key_${i}`;
230
+ const type = Math.random();
231
+ if (type < 0.3) {
232
+ result[key] = randomString({ maxLength: 20 });
233
+ } else if (type < 0.6) {
234
+ result[key] = randomNumber({ min: -100, max: 100 });
235
+ } else if (type < 0.8) {
236
+ result[key] = randomBoolean();
237
+ } else {
238
+ result[key] = null;
239
+ }
240
+ }
241
+ return result;
242
+ };
243
+ const randomDate = (options = {}) => {
244
+ const now = Date.now();
245
+ const offset = Math.floor(Math.random() * 1e10) - 5e9;
246
+ return new Date(now + offset);
247
+ };
248
+ const fuzz = async (generator, testFn, iterations = 100) => {
249
+ const failedCases = [];
250
+ let passedCases = 0;
251
+ for (let i = 0; i < iterations; i++) {
252
+ const input = generator();
253
+ try {
254
+ await testFn(input);
255
+ passedCases++;
256
+ } catch (error) {
257
+ failedCases.push({
258
+ input,
259
+ error: error instanceof Error ? error : new Error(String(error))
260
+ });
261
+ }
262
+ }
263
+ return {
264
+ totalCases: iterations,
265
+ passedCases,
266
+ failedCases,
267
+ success: failedCases.length === 0
268
+ };
269
+ };
270
+ return {
271
+ randomString,
272
+ randomNumber,
273
+ randomBoolean,
274
+ randomArray,
275
+ randomObject,
276
+ randomDate,
277
+ fuzz
278
+ };
279
+ }
280
+ function createPerformanceTestHelpers() {
281
+ const benchmarks = /* @__PURE__ */ new Map();
282
+ const benchmark = async (name, fn, options = {}) => {
283
+ const iterations = options.iterations ?? 1e3;
284
+ const warmupIterations = options.warmupIterations ?? 100;
285
+ for (let i = 0; i < warmupIterations; i++) {
286
+ await fn();
287
+ }
288
+ const times = [];
289
+ const start = performance.now();
290
+ for (let i = 0; i < iterations; i++) {
291
+ const iterStart = performance.now();
292
+ await fn();
293
+ times.push(performance.now() - iterStart);
294
+ }
295
+ const totalTime = performance.now() - start;
296
+ const averageTime = totalTime / iterations;
297
+ const minTime = Math.min(...times);
298
+ const maxTime = Math.max(...times);
299
+ const opsPerSecond = 1e3 / averageTime;
300
+ const result = {
301
+ name,
302
+ totalTime,
303
+ averageTime,
304
+ minTime,
305
+ maxTime,
306
+ iterations,
307
+ opsPerSecond
308
+ };
309
+ benchmarks.set(name, result);
310
+ if (options.verbose) {
311
+ console.log(`Benchmark "${name}":`);
312
+ console.log(` Average: ${averageTime.toFixed(4)}ms`);
313
+ console.log(` Min: ${minTime.toFixed(4)}ms`);
314
+ console.log(` Max: ${maxTime.toFixed(4)}ms`);
315
+ console.log(` Ops/s: ${opsPerSecond.toFixed(2)}`);
316
+ }
317
+ return result;
318
+ };
319
+ const compare = (baseline, current) => {
320
+ const percentChange = (current.averageTime - baseline.averageTime) / baseline.averageTime * 100;
321
+ return {
322
+ percentChange,
323
+ isFaster: percentChange < 0,
324
+ isSlower: percentChange > 0
325
+ };
326
+ };
327
+ const regressionTest = async (name, fn, baseline, options = {}) => {
328
+ const threshold = options.threshold ?? 10;
329
+ const current = await benchmark(name, fn);
330
+ const { percentChange, isSlower } = compare(baseline, current);
331
+ let message = "";
332
+ if (isSlower && percentChange > threshold) {
333
+ message = `Performance regression detected: ${percentChange.toFixed(2)}% slower (threshold: ${threshold}%)`;
334
+ } else if (isSlower) {
335
+ message = `Performance slightly slower: ${percentChange.toFixed(2)}% (within threshold)`;
336
+ } else if (percentChange < 0) {
337
+ message = `Performance improved: ${Math.abs(percentChange).toFixed(2)}% faster`;
338
+ } else {
339
+ message = "Performance unchanged";
340
+ }
341
+ return {
342
+ passed: !isSlower || percentChange <= threshold,
343
+ baseline,
344
+ current,
345
+ regressionPercent: percentChange,
346
+ message
347
+ };
348
+ };
349
+ const saveBaseline = (result, path = "./benchmarks") => {
350
+ try {
351
+ if (typeof window === "undefined") {
352
+ const fs = __require("fs");
353
+ const fspath = __require("path");
354
+ if (!fs.existsSync(path)) {
355
+ fs.mkdirSync(path, { recursive: true });
356
+ }
357
+ const filePath = fspath.join(path, `${result.name}.json`);
358
+ fs.writeFileSync(filePath, JSON.stringify(result, null, 2));
359
+ }
360
+ } catch (e) {
361
+ console.warn("Could not save baseline:", e);
362
+ }
363
+ };
364
+ const loadBaseline = (name, path = "./benchmarks") => {
365
+ try {
366
+ if (typeof window === "undefined") {
367
+ const fs = __require("fs");
368
+ const fspath = __require("path");
369
+ const filePath = fspath.join(path, `${name}.json`);
370
+ if (fs.existsSync(filePath)) {
371
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
372
+ }
373
+ }
374
+ } catch (e) {
375
+ console.warn("Could not load baseline:", e);
376
+ }
377
+ return null;
378
+ };
379
+ return {
380
+ benchmark,
381
+ compare,
382
+ regressionTest,
383
+ saveBaseline,
384
+ loadBaseline
385
+ };
386
+ }
387
+ function createTestingContext(options = {}) {
388
+ const mocks = /* @__PURE__ */ new Map();
389
+ const mockFns = [];
390
+ const domHelpers = createDOMTestHelpers();
391
+ const signalHelpers = createSignalTestHelpers();
392
+ const fuzzHelpers = createFuzzTestHelpers();
393
+ const performanceHelpers = createPerformanceTestHelpers();
394
+ return {
395
+ mount: (component, options2 = {}) => {
396
+ const container = typeof options2.container === "string" ? document.querySelector(options2.container) : options2.container || document.createElement("div");
397
+ if (!container) {
398
+ throw new Error("Invalid container");
399
+ }
400
+ if (options2.attach !== false && container.parentNode === null) {
401
+ document.body.appendChild(container);
402
+ }
403
+ let instance = {};
404
+ const element = container;
405
+ return {
406
+ instance,
407
+ element,
408
+ unmount: () => {
409
+ while (container.firstChild) {
410
+ container.firstChild.remove();
411
+ }
412
+ if (options2.attach !== false && container.parentNode === document.body) {
413
+ document.body.removeChild(container);
414
+ }
415
+ },
416
+ rerender: (newProps) => {
417
+ },
418
+ find: (selector) => element.querySelector(selector),
419
+ findAll: (selector) => Array.from(element.querySelectorAll(selector)),
420
+ trigger: (eventName, payload) => {
421
+ element.dispatchEvent(new CustomEvent(eventName, { detail: payload }));
422
+ }
423
+ };
424
+ },
425
+ mockFn: (mockOptions) => {
426
+ const mockFn = createMockFn(mockOptions);
427
+ mockFns.push(mockFn);
428
+ return mockFn;
429
+ },
430
+ mockModule: (moduleName, factory) => {
431
+ mocks.set(moduleName, factory());
432
+ },
433
+ clearAllMocks: () => {
434
+ mockFns.forEach((mockFn) => mockFn.mockClear());
435
+ mocks.clear();
436
+ },
437
+ signal: signalHelpers,
438
+ dom: domHelpers,
439
+ fuzz: fuzzHelpers,
440
+ performance: performanceHelpers,
441
+ wait: async (ms) => {
442
+ await new Promise((resolve) => setTimeout(resolve, ms));
443
+ },
444
+ waitFor: async (condition, timeout = 5e3) => {
445
+ const startTime = Date.now();
446
+ while (Date.now() - startTime < timeout) {
447
+ const result = await condition();
448
+ if (result) {
449
+ return;
450
+ }
451
+ await new Promise((resolve) => setTimeout(resolve, 10));
452
+ }
453
+ throw new Error("Condition not met within timeout");
454
+ },
455
+ nextTick: async () => {
456
+ await new Promise((resolve) => setTimeout(resolve, 0));
457
+ }
458
+ };
459
+ }
460
+ var pluginTesting = definePlugin({
461
+ name: "testing",
462
+ version: "6.0.0",
463
+ description: "LytJS official testing plugin with testing utilities and helpers, including fuzz testing and performance regression testing",
464
+ author: "LytJS Team",
465
+ keywords: ["lytjs", "testing", "test-utilities", "fuzz-testing", "benchmark"],
466
+ schema: {
467
+ type: "object",
468
+ object: {
469
+ properties: {
470
+ defaultTimeout: { type: "number", default: 5e3 },
471
+ autoCleanup: { type: "boolean", default: true },
472
+ environment: { type: "string", default: "jsdom" }
473
+ }
474
+ }
475
+ },
476
+ install(app, options) {
477
+ const testingContext = createTestingContext(options);
478
+ app.config.globalProperties.$testing = testingContext;
479
+ app.provide("lyt-testing", testingContext);
480
+ }
481
+ });
482
+ var index_default = pluginTesting;
483
+ export {
484
+ createMockFn,
485
+ createTestingContext,
486
+ index_default as default
487
+ };
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@lytjs/plugin-testing",
3
+ "version": "6.4.0",
4
+ "description": "LytJS official testing plugin with testing utilities and helpers",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.mjs",
13
+ "require": "./dist/index.cjs"
14
+ },
15
+ "./package.json": "./package.json"
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "sideEffects": false,
21
+ "scripts": {
22
+ "build": "tsup",
23
+ "dev": "tsup --watch",
24
+ "test": "vitest run",
25
+ "test:watch": "vitest",
26
+ "test:coverage": "vitest run --coverage",
27
+ "type-check": "tsc --noEmit",
28
+ "lint": "eslint \"src/**/*.ts\"",
29
+ "clean": "rm -rf dist"
30
+ },
31
+ "dependencies": {
32
+ "@lytjs/core": "^6.4.0",
33
+ "@lytjs/reactivity": "^6.4.0"
34
+ },
35
+ "devDependencies": {
36
+ "tsup": "^8.0.0",
37
+ "typescript": "^5.4.0",
38
+ "vitest": "^3.0.0"
39
+ },
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://gitee.com/lytjs/lytjs.git",
44
+ "directory": "packages/plugins/packages/plugin-testing"
45
+ },
46
+ "keywords": [
47
+ "lytjs",
48
+ "testing",
49
+ "test-utilities"
50
+ ]
51
+ }