@vitronai/themis 0.1.0-beta.0 → 0.1.0-beta.2

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/src/test-utils.js CHANGED
@@ -5,6 +5,9 @@ function createTestUtils(options = {}) {
5
5
  const activeMocks = new Set();
6
6
  const activeSpies = new Set();
7
7
  const moduleLoader = options.moduleLoader;
8
+ const renderedContainers = new Set();
9
+ let timerState = null;
10
+ let fetchState = null;
8
11
 
9
12
  function fn(implementation) {
10
13
  const mockFn = createMockFunction(implementation);
@@ -78,8 +81,65 @@ function createTestUtils(options = {}) {
78
81
  spy.mockRestore();
79
82
  }
80
83
  }
84
+ if (fetchState) {
85
+ fetchState.restore();
86
+ }
87
+ if (timerState) {
88
+ timerState.useRealTimers();
89
+ }
81
90
  }
82
91
 
92
+ function render(input, renderOptions = {}) {
93
+ assertDomAvailable('render');
94
+ const container = renderOptions.container || document.createElement('div');
95
+ if (!renderOptions.container) {
96
+ document.body.appendChild(container);
97
+ }
98
+ renderedContainers.add(container);
99
+ container.innerHTML = '';
100
+ const node = toDomNode(input);
101
+ container.appendChild(node);
102
+
103
+ return {
104
+ container,
105
+ rerender(nextInput) {
106
+ container.innerHTML = '';
107
+ container.appendChild(toDomNode(nextInput));
108
+ },
109
+ unmount() {
110
+ cleanupContainer(container);
111
+ }
112
+ };
113
+ }
114
+
115
+ async function waitFor(assertion, options = {}) {
116
+ const timeout = Number(options.timeout || 250);
117
+ const interval = Number(options.interval || 10);
118
+ const startedAt = Date.now();
119
+ let lastError = null;
120
+
121
+ while (Date.now() - startedAt <= timeout) {
122
+ try {
123
+ return await assertion();
124
+ } catch (error) {
125
+ lastError = error;
126
+ }
127
+
128
+ await new Promise((resolve) => setTimeout(resolve, interval));
129
+ }
130
+
131
+ throw lastError || new Error(`waitFor timed out after ${timeout}ms`);
132
+ }
133
+
134
+ function cleanup() {
135
+ for (const container of [...renderedContainers]) {
136
+ cleanupContainer(container);
137
+ }
138
+ }
139
+
140
+ const screen = createScreenQueries();
141
+ const fireEvent = createFireEventApi();
142
+
83
143
  return {
84
144
  fn,
85
145
  spyOn,
@@ -87,8 +147,239 @@ function createTestUtils(options = {}) {
87
147
  unmock,
88
148
  clearAllMocks,
89
149
  resetAllMocks,
90
- restoreAllMocks
150
+ restoreAllMocks,
151
+ render,
152
+ waitFor,
153
+ screen,
154
+ fireEvent,
155
+ cleanup,
156
+ useFakeTimers() {
157
+ return getTimerState().useFakeTimers();
158
+ },
159
+ useRealTimers() {
160
+ return getTimerState().useRealTimers();
161
+ },
162
+ advanceTimersByTime(ms) {
163
+ return getTimerState().advanceTimersByTime(ms);
164
+ },
165
+ runAllTimers() {
166
+ return getTimerState().runAllTimers();
167
+ },
168
+ flushMicrotasks,
169
+ mockFetch(handlerOrResponse) {
170
+ return getFetchState().mockFetch(handlerOrResponse);
171
+ },
172
+ restoreFetch() {
173
+ return getFetchState().restore();
174
+ },
175
+ resetFetchMocks() {
176
+ return getFetchState().reset();
177
+ }
91
178
  };
179
+
180
+ function cleanupContainer(container) {
181
+ if (!container) {
182
+ return;
183
+ }
184
+ renderedContainers.delete(container);
185
+ container.innerHTML = '';
186
+ if (container.parentNode) {
187
+ container.parentNode.removeChild(container);
188
+ }
189
+ }
190
+
191
+ function createScreenQueries() {
192
+ return {
193
+ getByText(text) {
194
+ const match = queryAllNodes((node) => normalizeText(node.textContent) === normalizeText(text))[0];
195
+ if (!match) {
196
+ throw new Error(`Unable to find element with text: ${String(text)}`);
197
+ }
198
+ return match;
199
+ },
200
+ queryByText(text) {
201
+ return queryAllNodes((node) => normalizeText(node.textContent) === normalizeText(text))[0] || null;
202
+ },
203
+ getByRole(role, options = {}) {
204
+ const match = queryAllNodes((node) => {
205
+ if (resolveRole(node) !== role) {
206
+ return false;
207
+ }
208
+ if (options.name !== undefined) {
209
+ return normalizeText(resolveAccessibleName(node)) === normalizeText(options.name);
210
+ }
211
+ return true;
212
+ })[0];
213
+
214
+ if (!match) {
215
+ throw new Error(`Unable to find element with role: ${String(role)}`);
216
+ }
217
+ return match;
218
+ },
219
+ queryByRole(role, options = {}) {
220
+ return queryAllNodes((node) => {
221
+ if (resolveRole(node) !== role) {
222
+ return false;
223
+ }
224
+ if (options.name !== undefined) {
225
+ return normalizeText(resolveAccessibleName(node)) === normalizeText(options.name);
226
+ }
227
+ return true;
228
+ })[0] || null;
229
+ },
230
+ getByLabelText(labelText) {
231
+ const label = queryAllNodes((node) => node.tagName === 'LABEL' && normalizeText(node.textContent) === normalizeText(labelText))[0];
232
+ if (!label) {
233
+ throw new Error(`Unable to find label: ${String(labelText)}`);
234
+ }
235
+
236
+ const forId = label.getAttribute('for');
237
+ if (forId) {
238
+ const control = document.getElementById(forId);
239
+ if (control) {
240
+ return control;
241
+ }
242
+ }
243
+
244
+ const nestedControl = label.querySelector('input, textarea, select, button');
245
+ if (nestedControl) {
246
+ return nestedControl;
247
+ }
248
+
249
+ throw new Error(`Unable to resolve control for label: ${String(labelText)}`);
250
+ }
251
+ };
252
+ }
253
+
254
+ function createFireEventApi() {
255
+ return {
256
+ click(node) {
257
+ return dispatchDomEvent(node, new MouseEvent('click', { bubbles: true, cancelable: true }));
258
+ },
259
+ change(node, payload = {}) {
260
+ applyTargetPayload(node, payload);
261
+ return dispatchDomEvent(node, new Event('change', { bubbles: true, cancelable: true }));
262
+ },
263
+ input(node, payload = {}) {
264
+ applyTargetPayload(node, payload);
265
+ return dispatchDomEvent(node, new Event('input', { bubbles: true, cancelable: true }));
266
+ },
267
+ submit(node) {
268
+ return dispatchDomEvent(node, new Event('submit', { bubbles: true, cancelable: true }));
269
+ },
270
+ keyDown(node, payload = {}) {
271
+ return dispatchDomEvent(node, new KeyboardEvent('keydown', {
272
+ bubbles: true,
273
+ cancelable: true,
274
+ key: payload.key || ''
275
+ }));
276
+ }
277
+ };
278
+ }
279
+
280
+ function dispatchDomEvent(node, event) {
281
+ assertDomNode(node, 'fireEvent');
282
+ node.dispatchEvent(event);
283
+ return event;
284
+ }
285
+
286
+ function applyTargetPayload(node, payload) {
287
+ assertDomNode(node, 'fireEvent');
288
+ if (payload && Object.prototype.hasOwnProperty.call(payload, 'target') && payload.target && typeof payload.target === 'object') {
289
+ for (const [key, value] of Object.entries(payload.target)) {
290
+ node[key] = value;
291
+ }
292
+ }
293
+ }
294
+
295
+ function queryAllNodes(predicate) {
296
+ assertDomAvailable('screen');
297
+ const nodes = Array.from(document.body.querySelectorAll('*'));
298
+ return nodes.filter(predicate);
299
+ }
300
+
301
+ function toDomNode(value) {
302
+ if (value instanceof Node) {
303
+ return value;
304
+ }
305
+ if (value === null || value === undefined || value === false) {
306
+ return document.createTextNode('');
307
+ }
308
+ if (typeof value === 'string' || typeof value === 'number') {
309
+ return document.createTextNode(String(value));
310
+ }
311
+ if (Array.isArray(value)) {
312
+ const fragment = document.createDocumentFragment();
313
+ for (const item of value) {
314
+ fragment.appendChild(toDomNode(item));
315
+ }
316
+ return fragment;
317
+ }
318
+ if (isReactLikeElement(value)) {
319
+ return reactLikeElementToDom(value);
320
+ }
321
+
322
+ throw new Error(`render(...) does not support value: ${format(value)}`);
323
+ }
324
+
325
+ function reactLikeElementToDom(element) {
326
+ if (typeof element.type === 'function') {
327
+ return toDomNode(element.type(element.props || {}));
328
+ }
329
+
330
+ if (element.type === Symbol.for('react.fragment')) {
331
+ return toDomNode(flattenChildren(element.props && element.props.children));
332
+ }
333
+
334
+ const node = document.createElement(String(element.type));
335
+ const props = element.props || {};
336
+ for (const [key, value] of Object.entries(props)) {
337
+ if (key === 'children' || value === null || value === undefined) {
338
+ continue;
339
+ }
340
+ if (key === 'className') {
341
+ node.setAttribute('class', String(value));
342
+ continue;
343
+ }
344
+ if (key === 'htmlFor') {
345
+ node.setAttribute('for', String(value));
346
+ continue;
347
+ }
348
+ if (key.startsWith('on') && typeof value === 'function') {
349
+ node.addEventListener(key.slice(2).toLowerCase(), value);
350
+ continue;
351
+ }
352
+ if (key in node) {
353
+ try {
354
+ node[key] = value;
355
+ continue;
356
+ } catch (error) {
357
+ // Fall through to attribute set.
358
+ }
359
+ }
360
+ node.setAttribute(key, String(value));
361
+ }
362
+
363
+ for (const child of flattenChildren(props.children)) {
364
+ node.appendChild(toDomNode(child));
365
+ }
366
+
367
+ return node;
368
+ }
369
+
370
+ function getTimerState() {
371
+ if (!timerState) {
372
+ timerState = createTimerController();
373
+ }
374
+ return timerState;
375
+ }
376
+
377
+ function getFetchState() {
378
+ if (!fetchState) {
379
+ fetchState = createFetchController(activeMocks);
380
+ }
381
+ return fetchState;
382
+ }
92
383
  }
93
384
 
94
385
  function createMockFunction(implementation) {
@@ -193,6 +484,321 @@ function format(value) {
193
484
  return util.inspect(value, { depth: 5, colors: false, maxArrayLength: 20 });
194
485
  }
195
486
 
487
+ function createTimerController() {
488
+ const original = {
489
+ setTimeout: globalThis.setTimeout,
490
+ clearTimeout: globalThis.clearTimeout,
491
+ setInterval: globalThis.setInterval,
492
+ clearInterval: globalThis.clearInterval
493
+ };
494
+ const state = {
495
+ enabled: false,
496
+ now: 0,
497
+ nextId: 1,
498
+ timers: new Map()
499
+ };
500
+
501
+ function useFakeTimers() {
502
+ if (state.enabled) {
503
+ return;
504
+ }
505
+
506
+ state.enabled = true;
507
+ state.now = 0;
508
+ state.nextId = 1;
509
+ state.timers.clear();
510
+ globalThis.setTimeout = function themisSetTimeout(callback, delay = 0, ...args) {
511
+ return registerTimer('timeout', callback, delay, args);
512
+ };
513
+ globalThis.clearTimeout = function themisClearTimeout(id) {
514
+ state.timers.delete(Number(id));
515
+ };
516
+ globalThis.setInterval = function themisSetInterval(callback, delay = 0, ...args) {
517
+ return registerTimer('interval', callback, delay, args);
518
+ };
519
+ globalThis.clearInterval = function themisClearInterval(id) {
520
+ state.timers.delete(Number(id));
521
+ };
522
+ }
523
+
524
+ function useRealTimers() {
525
+ if (!state.enabled) {
526
+ return;
527
+ }
528
+
529
+ state.enabled = false;
530
+ state.timers.clear();
531
+ globalThis.setTimeout = original.setTimeout;
532
+ globalThis.clearTimeout = original.clearTimeout;
533
+ globalThis.setInterval = original.setInterval;
534
+ globalThis.clearInterval = original.clearInterval;
535
+ }
536
+
537
+ function registerTimer(kind, callback, delay, args) {
538
+ const id = state.nextId++;
539
+ const numericDelay = Math.max(0, Number(delay) || 0);
540
+ state.timers.set(id, {
541
+ id,
542
+ kind,
543
+ callback,
544
+ args,
545
+ delay: numericDelay,
546
+ dueAt: state.now + numericDelay
547
+ });
548
+ return id;
549
+ }
550
+
551
+ function advanceTimersByTime(ms) {
552
+ ensureFakeTimers('advanceTimersByTime');
553
+ const targetTime = state.now + Math.max(0, Number(ms) || 0);
554
+
555
+ while (true) {
556
+ const nextTimer = findNextTimer(targetTime);
557
+ if (!nextTimer) {
558
+ break;
559
+ }
560
+
561
+ state.now = nextTimer.dueAt;
562
+ executeTimer(nextTimer);
563
+ }
564
+
565
+ state.now = targetTime;
566
+ }
567
+
568
+ function runAllTimers() {
569
+ ensureFakeTimers('runAllTimers');
570
+ let guard = 0;
571
+ while (state.timers.size > 0) {
572
+ const nextTimer = findNextTimer(Infinity);
573
+ if (!nextTimer) {
574
+ break;
575
+ }
576
+ state.now = nextTimer.dueAt;
577
+ executeTimer(nextTimer);
578
+ guard += 1;
579
+ if (guard > 1000) {
580
+ throw new Error('runAllTimers aborted after 1000 timer executions');
581
+ }
582
+ }
583
+ }
584
+
585
+ function findNextTimer(maxDueAt) {
586
+ let candidate = null;
587
+ for (const timer of state.timers.values()) {
588
+ if (timer.dueAt > maxDueAt) {
589
+ continue;
590
+ }
591
+ if (!candidate || timer.dueAt < candidate.dueAt || (timer.dueAt === candidate.dueAt && timer.id < candidate.id)) {
592
+ candidate = timer;
593
+ }
594
+ }
595
+ return candidate;
596
+ }
597
+
598
+ function executeTimer(timer) {
599
+ if (!state.timers.has(timer.id)) {
600
+ return;
601
+ }
602
+
603
+ if (timer.kind === 'timeout') {
604
+ state.timers.delete(timer.id);
605
+ }
606
+
607
+ timer.callback(...timer.args);
608
+
609
+ if (timer.kind === 'interval' && state.timers.has(timer.id)) {
610
+ timer.dueAt = state.now + timer.delay;
611
+ state.timers.set(timer.id, timer);
612
+ }
613
+ }
614
+
615
+ function ensureFakeTimers(apiName) {
616
+ if (!state.enabled) {
617
+ throw new Error(`${apiName}(...) requires useFakeTimers()`);
618
+ }
619
+ }
620
+
621
+ return {
622
+ useFakeTimers,
623
+ useRealTimers,
624
+ advanceTimersByTime,
625
+ runAllTimers
626
+ };
627
+ }
628
+
629
+ function createFetchController(activeMocks) {
630
+ const originalFetch = globalThis.fetch;
631
+ const originalResponse = typeof Response === 'function' ? Response : null;
632
+ const state = {
633
+ active: false,
634
+ mockFn: null
635
+ };
636
+
637
+ function mockFetch(handlerOrResponse) {
638
+ const mockFn = createMockFunction(async function themisFetch(...args) {
639
+ if (typeof handlerOrResponse === 'function') {
640
+ return normalizeFetchResponse(await handlerOrResponse(...args));
641
+ }
642
+ return normalizeFetchResponse(handlerOrResponse);
643
+ });
644
+ activeMocks.add(mockFn);
645
+ state.active = true;
646
+ state.mockFn = mockFn;
647
+ globalThis.fetch = mockFn;
648
+ return mockFn;
649
+ }
650
+
651
+ function reset() {
652
+ if (state.mockFn) {
653
+ state.mockFn.mockClear();
654
+ }
655
+ }
656
+
657
+ function restore() {
658
+ state.active = false;
659
+ state.mockFn = null;
660
+ if (originalFetch) {
661
+ globalThis.fetch = originalFetch;
662
+ } else {
663
+ delete globalThis.fetch;
664
+ }
665
+ }
666
+
667
+ function normalizeFetchResponse(value) {
668
+ if (originalResponse && value instanceof originalResponse) {
669
+ return Promise.resolve(value);
670
+ }
671
+
672
+ if (isPlainObject(value) && (Object.prototype.hasOwnProperty.call(value, 'status') || Object.prototype.hasOwnProperty.call(value, 'body') || Object.prototype.hasOwnProperty.call(value, 'json'))) {
673
+ const body = Object.prototype.hasOwnProperty.call(value, 'json')
674
+ ? JSON.stringify(value.json)
675
+ : Object.prototype.hasOwnProperty.call(value, 'body')
676
+ ? value.body
677
+ : '';
678
+ const headers = {
679
+ ...(isPlainObject(value.headers) ? value.headers : {}),
680
+ ...(Object.prototype.hasOwnProperty.call(value, 'json') ? { 'content-type': 'application/json' } : {})
681
+ };
682
+ return Promise.resolve(new Response(body, {
683
+ status: value.status || 200,
684
+ headers
685
+ }));
686
+ }
687
+
688
+ return Promise.resolve(value);
689
+ }
690
+
691
+ return {
692
+ mockFetch,
693
+ reset,
694
+ restore
695
+ };
696
+ }
697
+
698
+ async function flushMicrotasks() {
699
+ await Promise.resolve();
700
+ await Promise.resolve();
701
+ }
702
+
703
+ function assertDomAvailable(apiName) {
704
+ if (typeof document === 'undefined' || typeof Node === 'undefined') {
705
+ throw new Error(`${apiName}(...) requires the jsdom test environment`);
706
+ }
707
+ }
708
+
709
+ function assertDomNode(node, apiName) {
710
+ assertDomAvailable(apiName);
711
+ if (!(node instanceof Node)) {
712
+ throw new Error(`${apiName}(...) expects a DOM node`);
713
+ }
714
+ }
715
+
716
+ function isReactLikeElement(value) {
717
+ return Boolean(value && typeof value === 'object' && value.$$typeof === 'react.test.element');
718
+ }
719
+
720
+ function flattenChildren(children) {
721
+ if (children === null || children === undefined) {
722
+ return [];
723
+ }
724
+ if (Array.isArray(children)) {
725
+ return children.flatMap((child) => flattenChildren(child));
726
+ }
727
+ return [children];
728
+ }
729
+
730
+ function normalizeText(value) {
731
+ return String(value || '').replace(/\s+/g, ' ').trim();
732
+ }
733
+
734
+ function isPlainObject(value) {
735
+ if (!value || typeof value !== 'object') {
736
+ return false;
737
+ }
738
+ const prototype = Object.getPrototypeOf(value);
739
+ return prototype === Object.prototype || prototype === null;
740
+ }
741
+
742
+ function resolveRole(node) {
743
+ const explicitRole = node.getAttribute && node.getAttribute('role');
744
+ if (explicitRole) {
745
+ return explicitRole;
746
+ }
747
+
748
+ const tagName = node.tagName ? node.tagName.toLowerCase() : '';
749
+ if (tagName === 'button') {
750
+ return 'button';
751
+ }
752
+ if (tagName === 'a' && node.getAttribute('href')) {
753
+ return 'link';
754
+ }
755
+ if (tagName === 'input') {
756
+ const type = (node.getAttribute('type') || 'text').toLowerCase();
757
+ if (type === 'checkbox') {
758
+ return 'checkbox';
759
+ }
760
+ if (type === 'radio') {
761
+ return 'radio';
762
+ }
763
+ return 'textbox';
764
+ }
765
+ if (tagName === 'textarea') {
766
+ return 'textbox';
767
+ }
768
+ if (tagName === 'form') {
769
+ return 'form';
770
+ }
771
+ return null;
772
+ }
773
+
774
+ function resolveAccessibleName(node) {
775
+ if (!node || !node.ownerDocument) {
776
+ return '';
777
+ }
778
+
779
+ const ariaLabel = node.getAttribute && node.getAttribute('aria-label');
780
+ if (ariaLabel) {
781
+ return ariaLabel;
782
+ }
783
+
784
+ const id = node.getAttribute && node.getAttribute('id');
785
+ if (id) {
786
+ const label = node.ownerDocument.querySelector(`label[for="${id}"]`);
787
+ if (label) {
788
+ return normalizeText(label.textContent);
789
+ }
790
+ }
791
+
792
+ if (node.closest) {
793
+ const wrappingLabel = node.closest('label');
794
+ if (wrappingLabel) {
795
+ return normalizeText(wrappingLabel.textContent);
796
+ }
797
+ }
798
+
799
+ return normalizeText(node.textContent);
800
+ }
801
+
196
802
  module.exports = {
197
803
  createTestUtils,
198
804
  createMockFunction,
package/src/watch.js CHANGED
@@ -32,6 +32,8 @@ function hasWatchSignatureChanged(previousSignature, nextSignature) {
32
32
  async function runWatchMode(options) {
33
33
  const cwd = path.resolve(options.cwd || process.cwd());
34
34
  const cliArgs = stripWatchFlags(options.cliArgs || []);
35
+ const executeInProcess = typeof options.executeInProcess === 'function' ? options.executeInProcess : null;
36
+ const useInProcess = Boolean(options.inProcess && executeInProcess);
35
37
  const cliPath = path.join(__dirname, '..', 'bin', 'themis.js');
36
38
  const pollIntervalMs = Number(options.pollIntervalMs) > 0 ? Number(options.pollIntervalMs) : 400;
37
39
  let previousSignature = collectWatchSignature(cwd);
@@ -45,6 +47,13 @@ async function runWatchMode(options) {
45
47
  });
46
48
 
47
49
  const runOnce = () => new Promise((resolve, reject) => {
50
+ if (useInProcess) {
51
+ Promise.resolve()
52
+ .then(() => executeInProcess(cliArgs))
53
+ .then(resolve, reject);
54
+ return;
55
+ }
56
+
48
57
  activeChild = spawn(process.execPath, [cliPath, 'test', ...cliArgs], {
49
58
  cwd,
50
59
  stdio: 'inherit',
package/src/worker.js CHANGED
@@ -10,8 +10,7 @@ const { collectAndRun } = require('./runtime');
10
10
  cwd: workerData.cwd,
11
11
  environment: workerData.environment,
12
12
  setupFiles: workerData.setupFiles,
13
- tsconfigPath: workerData.tsconfigPath,
14
- updateSnapshots: workerData.updateSnapshots
13
+ tsconfigPath: workerData.tsconfigPath
15
14
  });
16
15
  parentPort.postMessage({ ok: true, result });
17
16
  } catch (error) {