ckeditor5-symfony 1.0.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.
Files changed (174) hide show
  1. package/dist/ckeditor5-symfony-error.d.ts +7 -0
  2. package/dist/ckeditor5-symfony-error.d.ts.map +1 -0
  3. package/dist/elements/context/context.d.ts +18 -0
  4. package/dist/elements/context/context.d.ts.map +1 -0
  5. package/dist/elements/context/contexts-registry.d.ts +9 -0
  6. package/dist/elements/context/contexts-registry.d.ts.map +1 -0
  7. package/dist/elements/context/index.d.ts +4 -0
  8. package/dist/elements/context/index.d.ts.map +1 -0
  9. package/dist/elements/context/typings.d.ts +34 -0
  10. package/dist/elements/context/typings.d.ts.map +1 -0
  11. package/dist/elements/editable.d.ts +18 -0
  12. package/dist/elements/editable.d.ts.map +1 -0
  13. package/dist/elements/editor/custom-editor-plugins.d.ts +54 -0
  14. package/dist/elements/editor/custom-editor-plugins.d.ts.map +1 -0
  15. package/dist/elements/editor/editor.d.ts +23 -0
  16. package/dist/elements/editor/editor.d.ts.map +1 -0
  17. package/dist/elements/editor/editors-registry.d.ts +9 -0
  18. package/dist/elements/editor/editors-registry.d.ts.map +1 -0
  19. package/dist/elements/editor/index.d.ts +3 -0
  20. package/dist/elements/editor/index.d.ts.map +1 -0
  21. package/dist/elements/editor/plugins/index.d.ts +2 -0
  22. package/dist/elements/editor/plugins/index.d.ts.map +1 -0
  23. package/dist/elements/editor/plugins/sync-editor-with-input.d.ts +6 -0
  24. package/dist/elements/editor/plugins/sync-editor-with-input.d.ts.map +1 -0
  25. package/dist/elements/editor/typings.d.ts +99 -0
  26. package/dist/elements/editor/typings.d.ts.map +1 -0
  27. package/dist/elements/editor/utils/create-editor-in-context.d.ts +44 -0
  28. package/dist/elements/editor/utils/create-editor-in-context.d.ts.map +1 -0
  29. package/dist/elements/editor/utils/index.d.ts +12 -0
  30. package/dist/elements/editor/utils/index.d.ts.map +1 -0
  31. package/dist/elements/editor/utils/is-single-root-editor.d.ts +9 -0
  32. package/dist/elements/editor/utils/is-single-root-editor.d.ts.map +1 -0
  33. package/dist/elements/editor/utils/load-editor-constructor.d.ts +9 -0
  34. package/dist/elements/editor/utils/load-editor-constructor.d.ts.map +1 -0
  35. package/dist/elements/editor/utils/load-editor-plugins.d.ts +20 -0
  36. package/dist/elements/editor/utils/load-editor-plugins.d.ts.map +1 -0
  37. package/dist/elements/editor/utils/load-editor-translations.d.ts +14 -0
  38. package/dist/elements/editor/utils/load-editor-translations.d.ts.map +1 -0
  39. package/dist/elements/editor/utils/normalize-custom-translations.d.ts +11 -0
  40. package/dist/elements/editor/utils/normalize-custom-translations.d.ts.map +1 -0
  41. package/dist/elements/editor/utils/query-all-editor-ids.d.ts +5 -0
  42. package/dist/elements/editor/utils/query-all-editor-ids.d.ts.map +1 -0
  43. package/dist/elements/editor/utils/query-editor-editables.d.ts +25 -0
  44. package/dist/elements/editor/utils/query-editor-editables.d.ts.map +1 -0
  45. package/dist/elements/editor/utils/resolve-editor-config-elements-references.d.ts +9 -0
  46. package/dist/elements/editor/utils/resolve-editor-config-elements-references.d.ts.map +1 -0
  47. package/dist/elements/editor/utils/set-editor-editable-height.d.ts +9 -0
  48. package/dist/elements/editor/utils/set-editor-editable-height.d.ts.map +1 -0
  49. package/dist/elements/editor/utils/wrap-with-watchdog.d.ts +24 -0
  50. package/dist/elements/editor/utils/wrap-with-watchdog.d.ts.map +1 -0
  51. package/dist/elements/index.d.ts +6 -0
  52. package/dist/elements/index.d.ts.map +1 -0
  53. package/dist/elements/register-custom-elements.d.ts +5 -0
  54. package/dist/elements/register-custom-elements.d.ts.map +1 -0
  55. package/dist/elements/ui-part.d.ts +18 -0
  56. package/dist/elements/ui-part.d.ts.map +1 -0
  57. package/dist/index.cjs +5 -0
  58. package/dist/index.cjs.map +1 -0
  59. package/dist/index.d.ts +3 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.mjs +1089 -0
  62. package/dist/index.mjs.map +1 -0
  63. package/dist/shared/async-registry.d.ts +136 -0
  64. package/dist/shared/async-registry.d.ts.map +1 -0
  65. package/dist/shared/camel-case.d.ts +8 -0
  66. package/dist/shared/camel-case.d.ts.map +1 -0
  67. package/dist/shared/debounce.d.ts +2 -0
  68. package/dist/shared/debounce.d.ts.map +1 -0
  69. package/dist/shared/deep-camel-case-keys.d.ts +8 -0
  70. package/dist/shared/deep-camel-case-keys.d.ts.map +1 -0
  71. package/dist/shared/filter-object-values.d.ts +9 -0
  72. package/dist/shared/filter-object-values.d.ts.map +1 -0
  73. package/dist/shared/index.d.ts +15 -0
  74. package/dist/shared/index.d.ts.map +1 -0
  75. package/dist/shared/is-empty-object.d.ts +2 -0
  76. package/dist/shared/is-empty-object.d.ts.map +1 -0
  77. package/dist/shared/is-plain-object.d.ts +8 -0
  78. package/dist/shared/is-plain-object.d.ts.map +1 -0
  79. package/dist/shared/map-object-values.d.ts +11 -0
  80. package/dist/shared/map-object-values.d.ts.map +1 -0
  81. package/dist/shared/once.d.ts +2 -0
  82. package/dist/shared/once.d.ts.map +1 -0
  83. package/dist/shared/shallow-equal.d.ts +9 -0
  84. package/dist/shared/shallow-equal.d.ts.map +1 -0
  85. package/dist/shared/timeout.d.ts +8 -0
  86. package/dist/shared/timeout.d.ts.map +1 -0
  87. package/dist/shared/uid.d.ts +7 -0
  88. package/dist/shared/uid.d.ts.map +1 -0
  89. package/dist/shared/wait-for-dom-ready.d.ts +5 -0
  90. package/dist/shared/wait-for-dom-ready.d.ts.map +1 -0
  91. package/dist/shared/wait-for.d.ts +20 -0
  92. package/dist/shared/wait-for.d.ts.map +1 -0
  93. package/dist/types/can-be-promise.type.d.ts +2 -0
  94. package/dist/types/can-be-promise.type.d.ts.map +1 -0
  95. package/dist/types/index.d.ts +3 -0
  96. package/dist/types/index.d.ts.map +1 -0
  97. package/dist/types/required-by.type.d.ts +2 -0
  98. package/dist/types/required-by.type.d.ts.map +1 -0
  99. package/package.json +40 -0
  100. package/src/ckeditor5-symfony-error.ts +9 -0
  101. package/src/elements/context/context.test.ts +291 -0
  102. package/src/elements/context/context.ts +99 -0
  103. package/src/elements/context/contexts-registry.test.ts +10 -0
  104. package/src/elements/context/contexts-registry.ts +10 -0
  105. package/src/elements/context/index.ts +3 -0
  106. package/src/elements/context/typings.ts +39 -0
  107. package/src/elements/editable.test.ts +334 -0
  108. package/src/elements/editable.ts +114 -0
  109. package/src/elements/editor/custom-editor-plugins.test.ts +103 -0
  110. package/src/elements/editor/custom-editor-plugins.ts +86 -0
  111. package/src/elements/editor/editor.test.ts +438 -0
  112. package/src/elements/editor/editor.ts +279 -0
  113. package/src/elements/editor/editors-registry.test.ts +10 -0
  114. package/src/elements/editor/editors-registry.ts +10 -0
  115. package/src/elements/editor/index.ts +2 -0
  116. package/src/elements/editor/plugins/index.ts +1 -0
  117. package/src/elements/editor/plugins/sync-editor-with-input.ts +78 -0
  118. package/src/elements/editor/typings.ts +114 -0
  119. package/src/elements/editor/utils/create-editor-in-context.ts +90 -0
  120. package/src/elements/editor/utils/index.ts +11 -0
  121. package/src/elements/editor/utils/is-single-root-editor.test.ts +40 -0
  122. package/src/elements/editor/utils/is-single-root-editor.ts +11 -0
  123. package/src/elements/editor/utils/load-editor-constructor.test.ts +62 -0
  124. package/src/elements/editor/utils/load-editor-constructor.ts +29 -0
  125. package/src/elements/editor/utils/load-editor-plugins.test.ts +100 -0
  126. package/src/elements/editor/utils/load-editor-plugins.ts +73 -0
  127. package/src/elements/editor/utils/load-editor-translations.ts +233 -0
  128. package/src/elements/editor/utils/normalize-custom-translations.test.ts +152 -0
  129. package/src/elements/editor/utils/normalize-custom-translations.ts +18 -0
  130. package/src/elements/editor/utils/query-all-editor-ids.ts +9 -0
  131. package/src/elements/editor/utils/query-editor-editables.ts +101 -0
  132. package/src/elements/editor/utils/resolve-editor-config-elements-references.test.ts +93 -0
  133. package/src/elements/editor/utils/resolve-editor-config-elements-references.ts +36 -0
  134. package/src/elements/editor/utils/set-editor-editable-height.test.ts +131 -0
  135. package/src/elements/editor/utils/set-editor-editable-height.ts +15 -0
  136. package/src/elements/editor/utils/wrap-with-watchdog.test.ts +45 -0
  137. package/src/elements/editor/utils/wrap-with-watchdog.ts +51 -0
  138. package/src/elements/index.ts +14 -0
  139. package/src/elements/register-custom-elements.ts +24 -0
  140. package/src/elements/ui-part.test.ts +142 -0
  141. package/src/elements/ui-part.ts +80 -0
  142. package/src/index.ts +6 -0
  143. package/src/shared/async-registry.test.ts +737 -0
  144. package/src/shared/async-registry.ts +353 -0
  145. package/src/shared/camel-case.test.ts +35 -0
  146. package/src/shared/camel-case.ts +11 -0
  147. package/src/shared/debounce.test.ts +72 -0
  148. package/src/shared/debounce.ts +16 -0
  149. package/src/shared/deep-camel-case-keys.test.ts +34 -0
  150. package/src/shared/deep-camel-case-keys.ts +26 -0
  151. package/src/shared/filter-object-values.test.ts +25 -0
  152. package/src/shared/filter-object-values.ts +17 -0
  153. package/src/shared/index.ts +14 -0
  154. package/src/shared/is-empty-object.test.ts +78 -0
  155. package/src/shared/is-empty-object.ts +3 -0
  156. package/src/shared/is-plain-object.test.ts +38 -0
  157. package/src/shared/is-plain-object.ts +15 -0
  158. package/src/shared/map-object-values.test.ts +29 -0
  159. package/src/shared/map-object-values.ts +19 -0
  160. package/src/shared/once.test.ts +116 -0
  161. package/src/shared/once.ts +12 -0
  162. package/src/shared/shallow-equal.test.ts +51 -0
  163. package/src/shared/shallow-equal.ts +30 -0
  164. package/src/shared/timeout.test.ts +65 -0
  165. package/src/shared/timeout.ts +13 -0
  166. package/src/shared/uid.test.ts +25 -0
  167. package/src/shared/uid.ts +8 -0
  168. package/src/shared/wait-for-dom-ready.test.ts +87 -0
  169. package/src/shared/wait-for-dom-ready.ts +21 -0
  170. package/src/shared/wait-for.test.ts +24 -0
  171. package/src/shared/wait-for.ts +56 -0
  172. package/src/types/can-be-promise.type.ts +1 -0
  173. package/src/types/index.ts +2 -0
  174. package/src/types/required-by.type.ts +1 -0
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { isPlainObject } from './is-plain-object';
4
+
5
+ describe('isPlainObject', () => {
6
+ it('returns true for plain objects', () => {
7
+ expect(isPlainObject({})).toBe(true);
8
+ expect(isPlainObject({ a: 1, b: 2 })).toBe(true);
9
+ expect(isPlainObject(Object.create(null))).toBe(true);
10
+ });
11
+
12
+ it('returns false for arrays', () => {
13
+ expect(isPlainObject([])).toBe(false);
14
+ expect(isPlainObject([1, 2, 3])).toBe(false);
15
+ });
16
+
17
+ it('returns false for null', () => {
18
+ expect(isPlainObject(null)).toBe(false);
19
+ });
20
+
21
+ it('returns false for primitives', () => {
22
+ expect(isPlainObject(42)).toBe(false);
23
+ expect(isPlainObject('string')).toBe(false);
24
+ expect(isPlainObject(true)).toBe(false);
25
+ expect(isPlainObject(undefined)).toBe(false);
26
+ expect(isPlainObject(Symbol('sym'))).toBe(false);
27
+ });
28
+
29
+ it('returns false for class instances', () => {
30
+ class MyClass {}
31
+ expect(isPlainObject(new MyClass())).toBe(false);
32
+ });
33
+
34
+ it('returns false for functions', () => {
35
+ expect(isPlainObject(() => {})).toBe(false);
36
+ expect(isPlainObject(() => {})).toBe(false);
37
+ });
38
+ });
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Utility to check if a value is a plain object (not an array, not null, not a class instance).
3
+ *
4
+ * @param value The value to check.
5
+ * @returns True if the value is a plain object, false otherwise.
6
+ */
7
+ export function isPlainObject(value: unknown): value is Record<string, unknown> {
8
+ if (Object.prototype.toString.call(value) !== '[object Object]') {
9
+ return false;
10
+ }
11
+
12
+ const proto = Object.getPrototypeOf(value);
13
+
14
+ return proto === Object.prototype || proto === null;
15
+ }
@@ -0,0 +1,29 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { mapObjectValues } from './map-object-values';
4
+
5
+ describe('mapObjectValues', () => {
6
+ it('should map object values using the provided mapper function', () => {
7
+ const input = { a: 1, b: 2, c: 3 };
8
+ const mapper = (value: number) => value * 2;
9
+ const result = mapObjectValues(input, mapper);
10
+
11
+ expect(result).toEqual({ a: 2, b: 4, c: 6 });
12
+ });
13
+
14
+ it('should pass the key to the mapper function', () => {
15
+ const input = { x: 'foo', y: 'bar' };
16
+ const mapper = (value: string, key: string) => `${key}:${value}`;
17
+ const result = mapObjectValues(input, mapper);
18
+
19
+ expect(result).toEqual({ x: 'x:foo', y: 'y:bar' });
20
+ });
21
+
22
+ it('should return an empty object if the input is empty', () => {
23
+ const input: Record<string, number> = {};
24
+ const mapper = (value: number) => value * 2;
25
+ const result = mapObjectValues(input, mapper);
26
+
27
+ expect(result).toEqual({});
28
+ });
29
+ });
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Maps the values of an object using a provided mapper function.
3
+ *
4
+ * @param obj The object whose values will be mapped.
5
+ * @param mapper A function that takes a value and its key, and returns a new value.
6
+ * @template T The type of the original values in the object.
7
+ * @template U The type of the new values in the object.
8
+ * @returns A new object with the same keys as the original, but with values transformed by
9
+ */
10
+ export function mapObjectValues<T, U>(
11
+ obj: Record<string, T>,
12
+ mapper: (value: T, key: string) => U,
13
+ ): Record<string, U> {
14
+ const mappedEntries = Object
15
+ .entries(obj)
16
+ .map(([key, value]) => [key, mapper(value, key)] as const);
17
+
18
+ return Object.fromEntries(mappedEntries);
19
+ }
@@ -0,0 +1,116 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { once } from './once';
4
+
5
+ describe('once', () => {
6
+ it('should call the original function only once', () => {
7
+ const mockFn = vi.fn().mockReturnValue('result');
8
+ const onceFn = once(mockFn);
9
+
10
+ onceFn();
11
+ onceFn();
12
+ onceFn();
13
+
14
+ expect(mockFn).toHaveBeenCalledTimes(1);
15
+ });
16
+
17
+ it('should return the same result on subsequent calls', () => {
18
+ const mockFn = vi.fn().mockReturnValue('test-result');
19
+ const onceFn = once(mockFn);
20
+
21
+ const result1 = onceFn();
22
+ const result2 = onceFn();
23
+ const result3 = onceFn();
24
+
25
+ expect(result1).toBe('test-result');
26
+ expect(result2).toBe('test-result');
27
+ expect(result3).toBe('test-result');
28
+ });
29
+
30
+ it('should pass arguments to the original function', () => {
31
+ const mockFn = vi.fn();
32
+ const onceFn = once(mockFn);
33
+
34
+ onceFn('arg1', 'arg2', 'arg3');
35
+ onceFn('different', 'args');
36
+
37
+ expect(mockFn).toHaveBeenCalledTimes(1);
38
+ expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2', 'arg3');
39
+ });
40
+
41
+ it('should preserve the context (this) when called', () => {
42
+ const context = { value: 'test-context' };
43
+ const mockFn = vi.fn(function (this: typeof context) {
44
+ return this.value;
45
+ });
46
+ const onceFn = once(mockFn);
47
+
48
+ const result1 = onceFn.call(context);
49
+ const result2 = onceFn.call({ value: 'different-context' });
50
+
51
+ expect(result1).toBe('test-context');
52
+ expect(result2).toBe('test-context');
53
+ expect(mockFn).toHaveBeenCalledTimes(1);
54
+ });
55
+
56
+ it('should work with functions that return undefined', () => {
57
+ const mockFn = vi.fn().mockReturnValue(undefined);
58
+ const onceFn = once(mockFn);
59
+
60
+ const result1 = onceFn();
61
+ const result2 = onceFn();
62
+
63
+ expect(result1).toBeUndefined();
64
+ expect(result2).toBeUndefined();
65
+ expect(mockFn).toHaveBeenCalledTimes(1);
66
+ });
67
+
68
+ it('should work with async functions', async () => {
69
+ const mockFn = vi.fn().mockResolvedValue('async-result');
70
+ const onceFn = once(mockFn);
71
+
72
+ const result1 = await onceFn();
73
+ const result2 = await onceFn();
74
+
75
+ expect(result1).toBe('async-result');
76
+ expect(result2).toBe('async-result');
77
+ expect(mockFn).toHaveBeenCalledTimes(1);
78
+ });
79
+
80
+ it('should maintain function signature for type safety', () => {
81
+ const originalFn = (a: string, b: number): string => `${a}-${b}`;
82
+ const onceFn = once(originalFn);
83
+
84
+ const result = onceFn('test', 42);
85
+
86
+ expect(result).toBe('test-42');
87
+ expect(typeof result).toBe('string');
88
+ });
89
+
90
+ it('should work with functions that have no parameters', () => {
91
+ let counter = 0;
92
+ const mockFn = vi.fn(() => ++counter);
93
+ const onceFn = once(mockFn);
94
+
95
+ const result1 = onceFn();
96
+ const result2 = onceFn();
97
+
98
+ expect(result1).toBe(1);
99
+ expect(result2).toBe(1);
100
+ expect(mockFn).toHaveBeenCalledTimes(1);
101
+ });
102
+
103
+ it('should work with functions that return objects', () => {
104
+ const returnValue = { id: 1, name: 'test' };
105
+ const mockFn = vi.fn().mockReturnValue(returnValue);
106
+ const onceFn = once(mockFn);
107
+
108
+ const result1 = onceFn();
109
+ const result2 = onceFn();
110
+
111
+ expect(result1).toBe(returnValue);
112
+ expect(result2).toBe(returnValue);
113
+ expect(result1).toEqual({ id: 1, name: 'test' });
114
+ expect(mockFn).toHaveBeenCalledTimes(1);
115
+ });
116
+ });
@@ -0,0 +1,12 @@
1
+ export function once<T extends (...args: any[]) => any>(fn: T): T {
2
+ let called = false;
3
+ let result: ReturnType<T>;
4
+
5
+ return function (this: any, ...args: Parameters<T>): ReturnType<T> {
6
+ if (!called) {
7
+ called = true;
8
+ result = fn.apply(this, args);
9
+ }
10
+ return result;
11
+ } as T;
12
+ }
@@ -0,0 +1,51 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { shallowEqual } from './shallow-equal';
4
+
5
+ describe('shallowEqual', () => {
6
+ it('should return true for identical objects', () => {
7
+ const obj = { a: 1, b: 2 };
8
+ expect(shallowEqual(obj, obj)).toBe(true);
9
+ });
10
+
11
+ it('should return true for objects with same primitive values', () => {
12
+ const objA = { a: 1, b: 'test', c: true };
13
+ const objB = { a: 1, b: 'test', c: true };
14
+ expect(shallowEqual(objA, objB)).toBe(true);
15
+ });
16
+
17
+ it('should return false for objects with different values', () => {
18
+ const objA = { a: 1, b: 2 };
19
+ const objB = { a: 1, b: 3 };
20
+ expect(shallowEqual(objA, objB)).toBe(false);
21
+ });
22
+
23
+ it('should return false for objects with different keys', () => {
24
+ const objA = { a: 1, b: 2 };
25
+ const objB = { a: 1, c: 2 } as any;
26
+ expect(shallowEqual(objA, objB)).toBe(false);
27
+ });
28
+
29
+ it('should return false for objects with different number of keys', () => {
30
+ const objA = { a: 1, b: 2 };
31
+ const objB = { a: 1 };
32
+ expect(shallowEqual(objA, objB)).toBe(false);
33
+ });
34
+
35
+ it('should return true for empty objects', () => {
36
+ expect(shallowEqual({}, {})).toBe(true);
37
+ });
38
+
39
+ it('should return false for nested objects with same structure but different references', () => {
40
+ const objA = { a: { nested: 1 } };
41
+ const objB = { a: { nested: 1 } };
42
+ expect(shallowEqual(objA, objB)).toBe(false);
43
+ });
44
+
45
+ it('should return true for nested objects with same reference', () => {
46
+ const nested = { value: 1 };
47
+ const objA = { a: nested };
48
+ const objB = { a: nested };
49
+ expect(shallowEqual(objA, objB)).toBe(true);
50
+ });
51
+ });
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Performs a shallow comparison of two objects.
3
+ *
4
+ * @param objA - The first object to compare.
5
+ * @param objB - The second object to compare.
6
+ * @returns True if the objects are shallowly equal, false otherwise.
7
+ */
8
+ export function shallowEqual<T extends Record<string, unknown>>(
9
+ objA: T,
10
+ objB: T,
11
+ ): boolean {
12
+ if (objA === objB) {
13
+ return true;
14
+ }
15
+
16
+ const keysA = Object.keys(objA);
17
+ const keysB = Object.keys(objB);
18
+
19
+ if (keysA.length !== keysB.length) {
20
+ return false;
21
+ }
22
+
23
+ for (const key of keysA) {
24
+ if (objA[key] !== objB[key] || !Object.prototype.hasOwnProperty.call(objB, key)) {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ return true;
30
+ }
@@ -0,0 +1,65 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { timeout } from './timeout.js';
4
+
5
+ describe('timeout', () => {
6
+ beforeEach(() => {
7
+ vi.useFakeTimers();
8
+ });
9
+
10
+ afterEach(() => {
11
+ vi.restoreAllMocks();
12
+ });
13
+
14
+ it('should resolve after specified milliseconds', async () => {
15
+ const promise = timeout(1000);
16
+
17
+ vi.advanceTimersByTime(1000);
18
+
19
+ await expect(promise).resolves.toBeUndefined();
20
+ });
21
+
22
+ it('should not resolve before specified time', async () => {
23
+ let resolved = false;
24
+ const promise = timeout(1000).then(() => {
25
+ resolved = true;
26
+ });
27
+
28
+ vi.advanceTimersByTime(500);
29
+
30
+ expect(resolved).toBe(false);
31
+
32
+ vi.advanceTimersByTime(500);
33
+ await promise;
34
+
35
+ expect(resolved).toBe(true);
36
+ });
37
+
38
+ it('should resolve immediately when milliseconds is 0', async () => {
39
+ const promise = timeout(0);
40
+
41
+ vi.advanceTimersByTime(0);
42
+
43
+ await expect(promise).resolves.toBeUndefined();
44
+ });
45
+
46
+ it('should handle multiple concurrent timeouts', async () => {
47
+ const results: number[] = [];
48
+
49
+ const promise1 = timeout(100).then(() => results.push(1));
50
+ const promise2 = timeout(200).then(() => results.push(2));
51
+ const promise3 = timeout(150).then(() => results.push(3));
52
+
53
+ vi.advanceTimersByTime(100);
54
+ await promise1;
55
+ expect(results).toEqual([1]);
56
+
57
+ vi.advanceTimersByTime(50);
58
+ await promise3;
59
+ expect(results).toEqual([1, 3]);
60
+
61
+ vi.advanceTimersByTime(50);
62
+ await promise2;
63
+ expect(results).toEqual([1, 3, 2]);
64
+ });
65
+ });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Returns a promise that resolves after a specified number of milliseconds.
3
+ *
4
+ * @param ms The number of milliseconds to wait.
5
+ * @returns A promise that resolves after the specified time.
6
+ */
7
+ export function timeout(ms: number): Promise<void> {
8
+ return new Promise((resolve) => {
9
+ setTimeout(() => {
10
+ resolve();
11
+ }, ms);
12
+ });
13
+ }
@@ -0,0 +1,25 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { uid } from './uid';
4
+
5
+ describe('uid', () => {
6
+ it('should return a string', () => {
7
+ const id = uid();
8
+ expect(typeof id).toBe('string');
9
+ });
10
+
11
+ it('should return a non-empty string', () => {
12
+ const id = uid();
13
+ expect(id.length).toBeGreaterThan(0);
14
+ });
15
+
16
+ it('should return different values on subsequent calls', () => {
17
+ const ids = new Set(Array.from({ length: 1000 }, () => uid()));
18
+ expect(ids.size).toBe(1000);
19
+ });
20
+
21
+ it('should only contain alphanumeric characters', () => {
22
+ const id = uid();
23
+ expect(/^[a-z0-9]+$/i.test(id)).toBe(true);
24
+ });
25
+ });
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Generates a unique identifier string
3
+ *
4
+ * @returns Random string that can be used as unique identifier
5
+ */
6
+ export function uid() {
7
+ return Math.random().toString(36).substring(2);
8
+ }
@@ -0,0 +1,87 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { timeout } from './timeout.js';
4
+ import { waitForDOMReady } from './wait-for-dom-ready.js';
5
+
6
+ describe('waitForDOMReady', () => {
7
+ let originalReadyState: DocumentReadyState;
8
+
9
+ beforeEach(() => {
10
+ originalReadyState = document.readyState;
11
+ });
12
+
13
+ afterEach(() => {
14
+ Object.defineProperty(document, 'readyState', {
15
+ configurable: true,
16
+ get() { return originalReadyState; },
17
+ });
18
+
19
+ vi.restoreAllMocks();
20
+ });
21
+
22
+ it('should resolve immediately if readyState is "interactive"', async () => {
23
+ Object.defineProperty(document, 'readyState', {
24
+ configurable: true,
25
+ get() { return 'interactive'; },
26
+ });
27
+
28
+ const spy = vi.fn();
29
+ void waitForDOMReady().then(spy);
30
+
31
+ await timeout(0);
32
+ expect(spy).toHaveBeenCalled();
33
+ });
34
+
35
+ it('should resolve immediately if readyState is "complete"', async () => {
36
+ Object.defineProperty(document, 'readyState', {
37
+ configurable: true,
38
+ get() { return 'complete'; },
39
+ });
40
+
41
+ const spy = vi.fn();
42
+ void waitForDOMReady().then(spy);
43
+
44
+ await new Promise(resolve => setTimeout(resolve, 0));
45
+ expect(spy).toHaveBeenCalled();
46
+ });
47
+
48
+ it('should wait for DOMContentLoaded event if readyState is "loading"', async () => {
49
+ Object.defineProperty(document, 'readyState', {
50
+ configurable: true,
51
+ get() { return 'loading'; },
52
+ });
53
+
54
+ const addEventListenerSpy = vi.spyOn(document, 'addEventListener');
55
+ const spy = vi.fn();
56
+
57
+ void waitForDOMReady().then(spy);
58
+
59
+ expect(addEventListenerSpy).toHaveBeenCalledWith('DOMContentLoaded', expect.any(Function), { once: true });
60
+ expect(spy).not.toHaveBeenCalled();
61
+
62
+ // Simulate the event
63
+ const handler = addEventListenerSpy.mock.calls[0]?.[1] as EventListener;
64
+ handler({} as Event);
65
+
66
+ await new Promise(resolve => setTimeout(resolve, 0));
67
+ expect(spy).toHaveBeenCalled();
68
+ });
69
+
70
+ it('should warn and resolve immediately for unexpected readyState', async () => {
71
+ const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
72
+
73
+ Object.defineProperty(document, 'readyState', {
74
+ configurable: true,
75
+ get() { return 'unexpected'; },
76
+ });
77
+
78
+ const spy = vi.fn();
79
+ void waitForDOMReady().then(spy);
80
+
81
+ await timeout(0);
82
+ expect(consoleWarnSpy).toHaveBeenCalledWith('Unexpected document.readyState:', 'unexpected');
83
+ expect(spy).toHaveBeenCalled();
84
+
85
+ consoleWarnSpy.mockRestore();
86
+ });
87
+ });
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Returns a promise that resolves when the DOM is fully loaded and ready.
3
+ */
4
+ export function waitForDOMReady(): Promise<void> {
5
+ return new Promise((resolve) => {
6
+ switch (document.readyState) {
7
+ case 'loading':
8
+ document.addEventListener('DOMContentLoaded', () => resolve(), { once: true });
9
+ break;
10
+
11
+ case 'interactive':
12
+ case 'complete':
13
+ setTimeout(resolve, 0);
14
+ break;
15
+
16
+ default:
17
+ console.warn('Unexpected document.readyState:', document.readyState);
18
+ setTimeout(resolve, 0);
19
+ }
20
+ });
21
+ }
@@ -0,0 +1,24 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { waitFor } from './wait-for.js';
4
+
5
+ describe('waitFor', () => {
6
+ it('should resolve when the callback succeeds', async () => {
7
+ const result = await waitFor(() => Promise.resolve('success'));
8
+
9
+ expect(result).toBe('success');
10
+ });
11
+
12
+ it('should reject when the callback throws an error', async () => {
13
+ await expect(waitFor(() => Promise.reject(new Error('failed')))).rejects.toThrow('failed');
14
+ });
15
+
16
+ it('should reject when the timeout is reached', async () => {
17
+ await expect(
18
+ waitFor(() => new Promise(resolve => setTimeout(resolve, 2000)), {
19
+ timeOutAfter: 200,
20
+ retryAfter: 100,
21
+ }),
22
+ ).rejects.toThrow('Timeout');
23
+ });
24
+ });
@@ -0,0 +1,56 @@
1
+ import type { CanBePromise } from '../types';
2
+
3
+ /**
4
+ * Waits for the provided callback to succeed. The callback is executed multiple times until it succeeds or the timeout is reached.
5
+ * It's executed immediately and then with a delay defined by the `retry` option.
6
+ *
7
+ * @param callback The callback to execute.
8
+ * @param config Configuration for the function.
9
+ * @param config.timeOutAfter The maximum time to wait for the callback to succeed, in milliseconds. Default is 500ms.
10
+ * @param config.retryAfter The time to wait between retries, in milliseconds. Default is 100ms.
11
+ * @returns A promise that resolves when the callback succeeds.
12
+ */
13
+ export function waitFor<R>(
14
+ callback: () => CanBePromise<R>,
15
+ {
16
+ timeOutAfter = 500,
17
+ retryAfter = 100,
18
+ }: WaitForConfig = {},
19
+ ): Promise<R> {
20
+ return new Promise<R>((resolve, reject) => {
21
+ const startTime = Date.now();
22
+ let lastError: Error | null = null;
23
+
24
+ const timeoutTimerId = setTimeout(() => {
25
+ reject(lastError ?? new Error('Timeout'));
26
+ }, timeOutAfter);
27
+
28
+ const tick = async () => {
29
+ try {
30
+ const result = await callback();
31
+ clearTimeout(timeoutTimerId);
32
+ resolve(result);
33
+ }
34
+ catch (err: any) {
35
+ lastError = err;
36
+
37
+ if (Date.now() - startTime > timeOutAfter) {
38
+ reject(err);
39
+ }
40
+ else {
41
+ setTimeout(tick, retryAfter);
42
+ }
43
+ }
44
+ };
45
+
46
+ void tick();
47
+ });
48
+ }
49
+
50
+ /**
51
+ * Configuration for the `waitFor` function.
52
+ */
53
+ export type WaitForConfig = {
54
+ timeOutAfter?: number;
55
+ retryAfter?: number;
56
+ };
@@ -0,0 +1 @@
1
+ export type CanBePromise<T> = T | Promise<T>;
@@ -0,0 +1,2 @@
1
+ export * from './can-be-promise.type';
2
+ export * from './required-by.type';
@@ -0,0 +1 @@
1
+ export type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;