@decohere/core 0.0.1-dev.125 → 0.0.1-dev.5
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/package.json +40 -15
- package/src/caching/cache-manager.ts +102 -0
- package/src/caching/cache.decorator.ts +56 -0
- package/src/caching/cache.ts +11 -0
- package/{es6/src/caching/index.js → src/caching/index.ts} +1 -1
- package/src/caching/invalidate-cache.decorator.ts +81 -0
- package/src/caching/memoize.decorator.ts +34 -0
- package/src/caching/node-cache-memoize.adapter.ts +47 -0
- package/src/cli/action.ts +77 -0
- package/src/cli/command.ts +173 -0
- package/src/cli/index.ts +30 -0
- package/src/cli/module.type.ts +48 -0
- package/src/cli/options.ts +81 -0
- package/src/collections/find-duplicates.ts +16 -0
- package/src/collections/merge-arrays.ts +9 -0
- package/src/collections/sum.ts +71 -0
- package/src/collections/unique-objects.ts +23 -0
- package/src/composition/index.ts +48 -0
- package/src/configuration/configuration.types.ts +13 -0
- package/src/diagnostics/index.ts +1 -0
- package/src/diagnostics/trace.ts +97 -0
- package/{es6/src/extensibility/decorator-factory-base.js → src/extensibility/decorator-factory-base.ts} +17 -9
- package/src/extensibility/decorator-factory.ts +44 -0
- package/src/extensibility/extension.ts +34 -0
- package/src/extensibility/mixwith.ts +42 -0
- package/src/extensibility/pipeline.ts +60 -0
- package/{types/src/extensibility/step.d.ts → src/extensibility/step.ts} +6 -4
- package/src/interop/docker/docker.ts +57 -0
- package/src/interop/errors/interop.error.ts +1 -0
- package/src/interop/git/git.ts +98 -0
- package/src/interop/git/git.types.ts +17 -0
- package/src/interop/index.ts +22 -0
- package/src/interop/node/index.ts +1 -0
- package/src/interop/node/node.ts +38 -0
- package/src/interop/npm/npm.ts +168 -0
- package/src/interop/npm/npm.types.ts +28 -0
- package/src/interop/pnpm/pnpm.ts +22 -0
- package/src/interop/pnpm/pnpm.types.ts +3 -0
- package/src/interop/shell/shell.argv.ts +31 -0
- package/src/interop/shell/shell.ts +100 -0
- package/src/interop/shell/shell.types.ts +9 -0
- package/src/interop/types/detect-package-manager.ts +45 -0
- package/src/interop/types/package-manager.ts +7 -0
- package/src/interop/yarn/yarn.ts +28 -0
- package/src/interop/yarn/yarn.types.ts +3 -0
- package/src/io/copy-file.ts +13 -0
- package/src/io/delete-file.ts +4 -0
- package/src/io/directory-exists.ts +9 -0
- package/src/io/file-exists.ts +10 -0
- package/src/io/find-directories.ts +13 -0
- package/src/io/find-file.ts +37 -0
- package/{es6/src/io/io.js → src/io/io.ts} +3 -0
- package/src/io/read-file.ts +25 -0
- package/src/io/remove-dir.ts +11 -0
- package/src/io/types/directory.type.ts +4 -0
- package/src/io/types/file.ts +34 -0
- package/src/io/url-to-path.ts +17 -0
- package/src/io/watch.ts +290 -0
- package/src/io/write-file.ts +21 -0
- package/src/objects/deep-merge.ts +131 -0
- package/{es6/src/reflection/index.js → src/reflection/index.ts} +1 -0
- package/src/reflection/types/reflection.types.ts +28 -0
- package/src/reflection/utilities.ts +92 -0
- package/src/semver/index.ts +1 -0
- package/src/semver/semver.ts +96 -0
- package/src/system/camel-case.ts +3 -0
- package/src/system/debounce.ts +13 -0
- package/src/system/decorator.ts +41 -0
- package/src/system/environment.ts +6 -0
- package/{types/src/system/function.types.d.ts → src/system/function.types.ts} +10 -2
- package/src/system/retry.decorator.ts +43 -0
- package/src/system/sleep.ts +6 -0
- package/src/system/type.utils.ts +5 -0
- package/src/testing/flush-promises.ts +7 -0
- package/src/testing/glob-with-memfs.ts +25 -0
- package/src/testing/index.ts +5 -0
- package/src/testing/memfs.ts +28 -0
- package/src/testing/mock-logger.ts +42 -0
- package/src/testing/spy-on-constructor.ts +33 -0
- package/tests/caching/cache-manager.spec.ts +147 -0
- package/tests/caching/cache-test-class.ts +91 -0
- package/tests/caching/cache.decorator.spec.ts +259 -0
- package/tests/caching/invalidate-cache-test-class.ts +52 -0
- package/tests/caching/invalidate-cache.decorator.spec.ts +120 -0
- package/tests/caching/memoize-test-class.ts +99 -0
- package/tests/caching/memoize.decorator.spec.ts +203 -0
- package/tests/cli/command.model.spec.ts +91 -0
- package/tests/collections/merge-arrays.spec.ts +50 -0
- package/tests/collections/sum.spec.ts +86 -0
- package/tests/collections/unique-objects.spec.ts +233 -0
- package/tests/extensibility/extension.spec.ts +29 -0
- package/tests/extensibility/pipeline.spec.ts +379 -0
- package/tests/interop/detect-package-manager.spec.ts +70 -0
- package/tests/interop/npm.spec.ts +129 -0
- package/tests/interop/pnpm.spec.ts +30 -0
- package/tests/interop/shell.spec.ts +47 -0
- package/tests/interop/yarn.spec.ts +18 -0
- package/tests/io/find-file.spec.ts +188 -0
- package/tests/io/watch.real-fs.spec.ts +76 -0
- package/tests/io/watch.spec.ts +291 -0
- package/tests/mock-prototype.ts +12 -0
- package/tests/semver/semver.spec.ts +51 -0
- package/tests/system/decorator.spec.ts +250 -0
- package/tests/system/retry.decorator.spec.ts +57 -0
- package/LICENSE +0 -15
- package/commonjs/src/caching/cache-manager.js +0 -98
- package/commonjs/src/caching/cache.decorator.js +0 -69
- package/commonjs/src/caching/cache.js +0 -6
- package/commonjs/src/caching/index.js +0 -23
- package/commonjs/src/caching/invalidate-cache.decorator.js +0 -92
- package/commonjs/src/caching/map-cache.js +0 -39
- package/commonjs/src/caching/memoize.decorator.js +0 -36
- package/commonjs/src/collections/collections.js +0 -21
- package/commonjs/src/collections/find-duplicates.js +0 -35
- package/commonjs/src/collections/index.js +0 -52
- package/commonjs/src/collections/merge-arrays.js +0 -34
- package/commonjs/src/collections/sum.js +0 -63
- package/commonjs/src/collections/unique-objects.js +0 -39
- package/commonjs/src/composition/index.js +0 -4
- package/commonjs/src/configuration/configuration.types.js +0 -11
- package/commonjs/src/configuration/index.js +0 -18
- package/commonjs/src/extensibility/decorator-factory-base.js +0 -22
- package/commonjs/src/extensibility/decorator-factory.js +0 -31
- package/commonjs/src/extensibility/extension.js +0 -37
- package/commonjs/src/extensibility/index.js +0 -23
- package/commonjs/src/extensibility/mixwith.js +0 -36
- package/commonjs/src/extensibility/pipeline.js +0 -56
- package/commonjs/src/extensibility/step.js +0 -18
- package/commonjs/src/io/copy-file.js +0 -25
- package/commonjs/src/io/delete-file.js +0 -21
- package/commonjs/src/io/directory-exists.js +0 -24
- package/commonjs/src/io/file-exists.js +0 -19
- package/commonjs/src/io/index.js +0 -66
- package/commonjs/src/io/io.js +0 -25
- package/commonjs/src/io/read-file.js +0 -39
- package/commonjs/src/io/remove-dir.js +0 -22
- package/commonjs/src/io/types/directory.type.js +0 -4
- package/commonjs/src/io/types/file.js +0 -28
- package/commonjs/src/io/types/index.js +0 -19
- package/commonjs/src/io/url-to-path.js +0 -33
- package/commonjs/src/io/write-file.js +0 -30
- package/commonjs/src/objects/deep-merge.js +0 -67
- package/commonjs/src/objects/index.js +0 -11
- package/commonjs/src/reflection/index.js +0 -20
- package/commonjs/src/reflection/types/index.js +0 -18
- package/commonjs/src/reflection/types/reflection.types.js +0 -26
- package/commonjs/src/reflection/utilities.js +0 -118
- package/commonjs/src/system/camel-case.js +0 -13
- package/commonjs/src/system/debounce.js +0 -19
- package/commonjs/src/system/decorator.js +0 -46
- package/commonjs/src/system/environment.js +0 -23
- package/commonjs/src/system/function.types.js +0 -18
- package/commonjs/src/system/index.js +0 -24
- package/commonjs/src/system/retry.decorator.js +0 -40
- package/commonjs/src/system/sleep.js +0 -13
- package/commonjs/src/system/type.utils.js +0 -4
- package/es6/src/caching/cache-manager.js +0 -82
- package/es6/src/caching/cache.decorator.js +0 -51
- package/es6/src/caching/cache.js +0 -3
- package/es6/src/caching/invalidate-cache.decorator.js +0 -71
- package/es6/src/caching/map-cache.js +0 -35
- package/es6/src/caching/memoize.decorator.js +0 -27
- package/es6/src/collections/find-duplicates.js +0 -17
- package/es6/src/collections/merge-arrays.js +0 -16
- package/es6/src/collections/sum.js +0 -45
- package/es6/src/collections/unique-objects.js +0 -21
- package/es6/src/composition/index.js +0 -1
- package/es6/src/configuration/configuration.types.js +0 -1
- package/es6/src/extensibility/decorator-factory.js +0 -21
- package/es6/src/extensibility/extension.js +0 -27
- package/es6/src/extensibility/mixwith.js +0 -26
- package/es6/src/extensibility/pipeline.js +0 -46
- package/es6/src/extensibility/step.js +0 -16
- package/es6/src/io/copy-file.js +0 -10
- package/es6/src/io/delete-file.js +0 -6
- package/es6/src/io/directory-exists.js +0 -9
- package/es6/src/io/file-exists.js +0 -9
- package/es6/src/io/read-file.js +0 -13
- package/es6/src/io/remove-dir.js +0 -7
- package/es6/src/io/types/directory.type.js +0 -1
- package/es6/src/io/types/file.js +0 -18
- package/es6/src/io/url-to-path.js +0 -15
- package/es6/src/io/write-file.js +0 -15
- package/es6/src/objects/deep-merge.js +0 -57
- package/es6/src/reflection/types/reflection.types.js +0 -8
- package/es6/src/reflection/utilities.js +0 -74
- package/es6/src/system/camel-case.js +0 -3
- package/es6/src/system/debounce.js +0 -9
- package/es6/src/system/decorator.js +0 -30
- package/es6/src/system/environment.js +0 -5
- package/es6/src/system/function.types.js +0 -26
- package/es6/src/system/retry.decorator.js +0 -30
- package/es6/src/system/sleep.js +0 -3
- package/es6/src/system/type.utils.js +0 -1
- package/types/src/caching/cache-manager.d.ts +0 -59
- package/types/src/caching/cache.d.ts +0 -11
- package/types/src/caching/cache.decorator.d.ts +0 -18
- package/types/src/caching/index.d.ts +0 -6
- package/types/src/caching/invalidate-cache.decorator.d.ts +0 -17
- package/types/src/caching/map-cache.d.ts +0 -23
- package/types/src/caching/memoize.decorator.d.ts +0 -16
- package/types/src/collections/collections.d.ts +0 -4
- package/types/src/collections/find-duplicates.d.ts +0 -2
- package/types/src/collections/index.d.ts +0 -1
- package/types/src/collections/merge-arrays.d.ts +0 -2
- package/types/src/collections/sum.d.ts +0 -4
- package/types/src/collections/unique-objects.d.ts +0 -2
- package/types/src/composition/index.d.ts +0 -37
- package/types/src/configuration/configuration.types.d.ts +0 -10
- package/types/src/configuration/index.d.ts +0 -1
- package/types/src/extensibility/decorator-factory-base.d.ts +0 -39
- package/types/src/extensibility/decorator-factory.d.ts +0 -19
- package/types/src/extensibility/extension.d.ts +0 -9
- package/types/src/extensibility/index.d.ts +0 -6
- package/types/src/extensibility/mixwith.d.ts +0 -6
- package/types/src/extensibility/pipeline.d.ts +0 -12
- package/types/src/io/copy-file.d.ts +0 -4
- package/types/src/io/delete-file.d.ts +0 -1
- package/types/src/io/directory-exists.d.ts +0 -1
- package/types/src/io/file-exists.d.ts +0 -1
- package/types/src/io/index.d.ts +0 -2
- package/types/src/io/io.d.ts +0 -8
- package/types/src/io/read-file.d.ts +0 -4
- package/types/src/io/remove-dir.d.ts +0 -5
- package/types/src/io/types/directory.type.d.ts +0 -4
- package/types/src/io/types/file.d.ts +0 -20
- package/types/src/io/types/index.d.ts +0 -2
- package/types/src/io/url-to-path.d.ts +0 -2
- package/types/src/io/write-file.d.ts +0 -5
- package/types/src/objects/deep-merge.d.ts +0 -41
- package/types/src/objects/index.d.ts +0 -1
- package/types/src/reflection/index.d.ts +0 -3
- package/types/src/reflection/types/index.d.ts +0 -1
- package/types/src/reflection/types/reflection.types.d.ts +0 -21
- package/types/src/reflection/utilities.d.ts +0 -27
- package/types/src/system/camel-case.d.ts +0 -1
- package/types/src/system/debounce.d.ts +0 -1
- package/types/src/system/decorator.d.ts +0 -15
- package/types/src/system/environment.d.ts +0 -6
- package/types/src/system/index.d.ts +0 -7
- package/types/src/system/retry.decorator.d.ts +0 -6
- package/types/src/system/sleep.d.ts +0 -1
- package/types/src/system/type.utils.d.ts +0 -1
- /package/{es6/src/collections/collections.js → src/collections/collections.ts} +0 -0
- /package/{es6/src/collections/index.js → src/collections/index.ts} +0 -0
- /package/{es6/src/configuration/index.js → src/configuration/index.ts} +0 -0
- /package/{es6/src/extensibility/index.js → src/extensibility/index.ts} +0 -0
- /package/{es6/src/io/index.js → src/io/index.ts} +0 -0
- /package/{es6/src/io/types/index.js → src/io/types/index.ts} +0 -0
- /package/{es6/src/objects/index.js → src/objects/index.ts} +0 -0
- /package/{es6/src/reflection/types/index.js → src/reflection/types/index.ts} +0 -0
- /package/{es6/src/system/index.js → src/system/index.ts} +0 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { pathToFileUrl } from "../../src/io/url-to-path.js";
|
|
4
|
+
import { MEMOIZE_CACHE } from "./memoize-test-class.js";
|
|
5
|
+
|
|
6
|
+
// Test class import path constant
|
|
7
|
+
const MEMOIZE_TEST_CLASS_PATH = "./memoize-test-class.js";
|
|
8
|
+
|
|
9
|
+
// Function-based import URL for easier refactoring
|
|
10
|
+
const importUrl = () => getImportUrl(MEMOIZE_TEST_CLASS_PATH);
|
|
11
|
+
|
|
12
|
+
// Helper function to create a versioned import URL for cache busting
|
|
13
|
+
function getImportUrl(filename: string): string {
|
|
14
|
+
const modulePath = path.resolve(__dirname, filename);
|
|
15
|
+
const moduleUrl = pathToFileUrl(modulePath);
|
|
16
|
+
return `${moduleUrl}?version=${Date.now()}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("memoize decorator", () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
vi.resetAllMocks();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
MEMOIZE_CACHE.flushAll();
|
|
26
|
+
delete process.env.FORGE_DISABLE_CACHE;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("FORGE_DISABLE_CACHE environment variable behavior", () => {
|
|
30
|
+
it("should memoize normally by default", async () => {
|
|
31
|
+
const { MemoizeTestClass } = await import(importUrl());
|
|
32
|
+
const instance = new MemoizeTestClass();
|
|
33
|
+
|
|
34
|
+
const result1 = instance.syncMethod(5);
|
|
35
|
+
const result2 = instance.syncMethod(5);
|
|
36
|
+
|
|
37
|
+
expect(result1).toBe(10);
|
|
38
|
+
expect(result2).toBe(10);
|
|
39
|
+
expect(instance.callCount).toBe(1); // Should be memoized
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should not memoize when FORGE_DISABLE_CACHE=true", async () => {
|
|
43
|
+
process.env.FORGE_DISABLE_CACHE = "true";
|
|
44
|
+
|
|
45
|
+
const { MemoizeTestClass } = await import(importUrl());
|
|
46
|
+
const instance = new MemoizeTestClass();
|
|
47
|
+
|
|
48
|
+
const result1 = instance.syncMethod(5);
|
|
49
|
+
const result2 = instance.syncMethod(5);
|
|
50
|
+
|
|
51
|
+
expect(result1).toBe(10);
|
|
52
|
+
expect(result2).toBe(10);
|
|
53
|
+
expect(instance.callCount).toBe(2); // Should not be memoized
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("memoization functionality", () => {
|
|
58
|
+
it("should memoize synchronous method results", async () => {
|
|
59
|
+
const { MemoizeTestClass } = await import(importUrl());
|
|
60
|
+
const instance = new MemoizeTestClass();
|
|
61
|
+
|
|
62
|
+
const result1 = instance.syncMethod(42);
|
|
63
|
+
const result2 = instance.syncMethod(42);
|
|
64
|
+
|
|
65
|
+
expect(result1).toBe(84);
|
|
66
|
+
expect(result2).toBe(84);
|
|
67
|
+
expect(instance.callCount).toBe(1);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should memoize asynchronous method results", async () => {
|
|
71
|
+
const { MemoizeTestClass } = await import(importUrl());
|
|
72
|
+
const instance = new MemoizeTestClass();
|
|
73
|
+
|
|
74
|
+
const result1 = await instance.asyncMethod(10);
|
|
75
|
+
const result2 = await instance.asyncMethod(10);
|
|
76
|
+
|
|
77
|
+
expect(result1).toBe(30);
|
|
78
|
+
expect(result2).toBe(30);
|
|
79
|
+
expect(instance.callCount).toBe(1);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should handle multiple arguments", async () => {
|
|
83
|
+
const { MemoizeTestClass } = await import(importUrl());
|
|
84
|
+
const instance = new MemoizeTestClass();
|
|
85
|
+
|
|
86
|
+
const result1 = instance.multiArgMethod(5, 3);
|
|
87
|
+
const result2 = instance.multiArgMethod(5, 3);
|
|
88
|
+
const result3 = instance.multiArgMethod(5, 4);
|
|
89
|
+
|
|
90
|
+
expect(result1).toBe(8);
|
|
91
|
+
expect(result2).toBe(8);
|
|
92
|
+
expect(result3).toBe(9);
|
|
93
|
+
expect(instance.callCount).toBe(2); // Two different argument sets
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should handle undefined return values", async () => {
|
|
97
|
+
const { MemoizeTestClass } = await import(importUrl());
|
|
98
|
+
const instance = new MemoizeTestClass();
|
|
99
|
+
|
|
100
|
+
const result1 = instance.undefinedMethod();
|
|
101
|
+
const result2 = instance.undefinedMethod();
|
|
102
|
+
|
|
103
|
+
expect(result1).toBeUndefined();
|
|
104
|
+
expect(result2).toBeUndefined();
|
|
105
|
+
expect(instance.callCount).toBe(1);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should handle falsy return values", async () => {
|
|
109
|
+
const { MemoizeTestClass } = await import(importUrl());
|
|
110
|
+
const instance = new MemoizeTestClass();
|
|
111
|
+
|
|
112
|
+
const result1 = instance.falsyMethod();
|
|
113
|
+
const result2 = instance.falsyMethod();
|
|
114
|
+
|
|
115
|
+
expect(result1).toBe(0);
|
|
116
|
+
expect(result2).toBe(0);
|
|
117
|
+
expect(instance.callCount).toBe(1);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("should support custom cache keys", async () => {
|
|
121
|
+
const { MemoizeTestClass } = await import(importUrl());
|
|
122
|
+
const instance = new MemoizeTestClass();
|
|
123
|
+
|
|
124
|
+
const result1 = instance.namedCacheMethod(7);
|
|
125
|
+
const result2 = instance.namedCacheMethod(7);
|
|
126
|
+
|
|
127
|
+
expect(result1).toBe(28);
|
|
128
|
+
expect(result2).toBe(28);
|
|
129
|
+
expect(instance.callCount).toBe(1);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("should work with default cache", async () => {
|
|
133
|
+
const { MemoizeTestClass } = await import(importUrl());
|
|
134
|
+
const instance = new MemoizeTestClass();
|
|
135
|
+
|
|
136
|
+
const result1 = instance.defaultCacheMethod(6);
|
|
137
|
+
const result2 = instance.defaultCacheMethod(6);
|
|
138
|
+
|
|
139
|
+
expect(result1).toBe(30);
|
|
140
|
+
expect(result2).toBe(30);
|
|
141
|
+
expect(instance.callCount).toBe(1);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("object reference behavior", () => {
|
|
146
|
+
it("should call method only once for same arguments", async () => {
|
|
147
|
+
const { MemoizeTestClass } = await import(importUrl());
|
|
148
|
+
const instance = new MemoizeTestClass();
|
|
149
|
+
|
|
150
|
+
instance.objectMethod(5);
|
|
151
|
+
instance.objectMethod(5);
|
|
152
|
+
|
|
153
|
+
expect(instance.callCount).toBe(1);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("should return same object reference (not a clone)", async () => {
|
|
157
|
+
const { MemoizeTestClass } = await import(importUrl());
|
|
158
|
+
const instance = new MemoizeTestClass();
|
|
159
|
+
|
|
160
|
+
const obj1 = instance.objectMethod(5);
|
|
161
|
+
const obj2 = instance.objectMethod(5);
|
|
162
|
+
|
|
163
|
+
expect(obj1).toBe(obj2);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("should preserve mutations across all references", async () => {
|
|
167
|
+
const { MemoizeTestClass } = await import(importUrl());
|
|
168
|
+
const instance = new MemoizeTestClass();
|
|
169
|
+
|
|
170
|
+
const obj1 = instance.objectMethod(5);
|
|
171
|
+
obj1.value = 999;
|
|
172
|
+
|
|
173
|
+
const obj2 = instance.objectMethod(5);
|
|
174
|
+
|
|
175
|
+
expect(obj2.value).toBe(999);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("should maintain same reference after mutation", async () => {
|
|
179
|
+
const { MemoizeTestClass } = await import(importUrl());
|
|
180
|
+
const instance = new MemoizeTestClass();
|
|
181
|
+
|
|
182
|
+
const obj1 = instance.objectMethod(5);
|
|
183
|
+
obj1.value = 999;
|
|
184
|
+
|
|
185
|
+
const obj2 = instance.objectMethod(5);
|
|
186
|
+
|
|
187
|
+
expect(obj2).toBe(obj1);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("should not call method again after retrieving mutated object", async () => {
|
|
191
|
+
const { MemoizeTestClass } = await import(importUrl());
|
|
192
|
+
const instance = new MemoizeTestClass();
|
|
193
|
+
|
|
194
|
+
instance.objectMethod(5);
|
|
195
|
+
const callCountAfterFirst = instance.callCount;
|
|
196
|
+
|
|
197
|
+
instance.objectMethod(5);
|
|
198
|
+
const callCountAfterSecond = instance.callCount;
|
|
199
|
+
|
|
200
|
+
expect(callCountAfterSecond).toBe(callCountAfterFirst);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { OptionValues } from "commander";
|
|
2
|
+
import { expect, it, vi } from "vitest";
|
|
3
|
+
import type { Action } from "../../src/cli/index.js";
|
|
4
|
+
import { ActionResponse, DecohereCommand } from "../../src/cli/index.js";
|
|
5
|
+
|
|
6
|
+
type TestActions = {
|
|
7
|
+
a: Action;
|
|
8
|
+
b: Action;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
class AAction implements Action {
|
|
12
|
+
public constructor(
|
|
13
|
+
private readonly response: ActionResponse = ActionResponse.Success("a"),
|
|
14
|
+
) {}
|
|
15
|
+
|
|
16
|
+
public async run(): Promise<ActionResponse> {
|
|
17
|
+
return this.response;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class BAction implements Action {
|
|
22
|
+
public constructor(
|
|
23
|
+
private readonly response: ActionResponse = ActionResponse.Success("b"),
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
public async run(): Promise<ActionResponse> {
|
|
27
|
+
return this.response;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class TestCommand extends DecohereCommand<TestActions> {
|
|
32
|
+
public override get actions() {
|
|
33
|
+
return [AAction, BAction] as const;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public constructor() {
|
|
37
|
+
super("test");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
it("does not throw when an action is skipped", async () => {
|
|
42
|
+
const command = new TestCommand();
|
|
43
|
+
const actions: TestActions = {
|
|
44
|
+
a: new AAction(ActionResponse.Skipped("a")),
|
|
45
|
+
b: new BAction(ActionResponse.Success("b")),
|
|
46
|
+
};
|
|
47
|
+
const runA = vi.spyOn(actions.a, "run");
|
|
48
|
+
const runB = vi.spyOn(actions.b, "run");
|
|
49
|
+
|
|
50
|
+
const response = await command.run(actions, {} as OptionValues);
|
|
51
|
+
expect(runA).toHaveBeenCalledTimes(1);
|
|
52
|
+
expect(runB).toHaveBeenCalledTimes(1);
|
|
53
|
+
expect(response.a?.skipped).toBe(true);
|
|
54
|
+
expect(response.b?.succeeded).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("throws when an action returns a non-skipped error", async () => {
|
|
58
|
+
const command = new TestCommand();
|
|
59
|
+
const actions: TestActions = {
|
|
60
|
+
a: new AAction(ActionResponse.Error("a", "a failed")),
|
|
61
|
+
b: new BAction(ActionResponse.Skipped("b")),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
await expect(command.run(actions, {} as OptionValues)).rejects.toThrow(
|
|
65
|
+
"test command failed",
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("throws when mapped action constructor is missing", async () => {
|
|
70
|
+
class MissingBCommand extends DecohereCommand<TestActions> {
|
|
71
|
+
public override get actions() {
|
|
72
|
+
return [AAction, BAction] as const;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public constructor() {
|
|
76
|
+
super("test");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const command = new MissingBCommand();
|
|
81
|
+
const actions: TestActions = {
|
|
82
|
+
a: new AAction(),
|
|
83
|
+
b: {
|
|
84
|
+
run: async () => ActionResponse.Success("b"),
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
await expect(command.run(actions, {} as OptionValues)).rejects.toThrow(
|
|
89
|
+
"missing action instance for BAction",
|
|
90
|
+
);
|
|
91
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { collections } from "../../src/collections/index.js";
|
|
3
|
+
|
|
4
|
+
describe("merge-arrays functions", () => {
|
|
5
|
+
describe("mergeBy", () => {
|
|
6
|
+
it("should merge arrays and prefer second array values for duplicate keys", () => {
|
|
7
|
+
const first = [
|
|
8
|
+
{ id: "build", value: "default-build" },
|
|
9
|
+
{ id: "lint", value: "default-lint" },
|
|
10
|
+
];
|
|
11
|
+
const second = [
|
|
12
|
+
{ id: "build", value: "custom-build" },
|
|
13
|
+
{ id: "test", value: "custom-test" },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const result = collections.mergeBy(first, second, (item) => item.id);
|
|
17
|
+
|
|
18
|
+
expect(result).toEqual([
|
|
19
|
+
{ id: "build", value: "custom-build" },
|
|
20
|
+
{ id: "lint", value: "default-lint" },
|
|
21
|
+
{ id: "test", value: "custom-test" },
|
|
22
|
+
]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should return an empty array when both arrays are empty", () => {
|
|
26
|
+
expect(collections.mergeBy([], [], () => "")).toEqual([]);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("mergeByKey", () => {
|
|
31
|
+
it("should merge arrays by key and prefer second array values", () => {
|
|
32
|
+
const first = [
|
|
33
|
+
{ name: "build", order: 1 },
|
|
34
|
+
{ name: "lint", order: 2 },
|
|
35
|
+
];
|
|
36
|
+
const second = [
|
|
37
|
+
{ name: "build", order: 100 },
|
|
38
|
+
{ name: "test", order: 3 },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const result = collections.mergeByKey(first, second, "name");
|
|
42
|
+
|
|
43
|
+
expect(result).toEqual([
|
|
44
|
+
{ name: "build", order: 100 },
|
|
45
|
+
{ name: "lint", order: 2 },
|
|
46
|
+
{ name: "test", order: 3 },
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { collections } from "../../src/collections/index.js";
|
|
3
|
+
|
|
4
|
+
describe("sum functions", () => {
|
|
5
|
+
describe("sumByKey", () => {
|
|
6
|
+
it("should sum numeric values from object array using key selector", () => {
|
|
7
|
+
const items = [{ value: 10 }, { value: 20 }, { value: 30 }];
|
|
8
|
+
const result = collections.sumByKey(items, "value");
|
|
9
|
+
expect(result).toBe(60);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("should handle empty array", () => {
|
|
13
|
+
const items: { value: number }[] = [];
|
|
14
|
+
const result = collections.sumByKey(items, "value");
|
|
15
|
+
expect(result).toBe(0);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should handle null/undefined items", () => {
|
|
19
|
+
const items: Array<{ value: number } | null | undefined> = [
|
|
20
|
+
{ value: 10 },
|
|
21
|
+
null,
|
|
22
|
+
{ value: 20 },
|
|
23
|
+
];
|
|
24
|
+
const result = collections.sumByKey(items, "value");
|
|
25
|
+
expect(result).toBe(30);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should treat non-numeric values as 0", () => {
|
|
29
|
+
const items: Array<{ value: number | string }> = [
|
|
30
|
+
{ value: 10 },
|
|
31
|
+
{ value: "string" },
|
|
32
|
+
{ value: 20 },
|
|
33
|
+
];
|
|
34
|
+
const result = collections.sumByKey(items, "value");
|
|
35
|
+
expect(result).toBe(30);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should work with different property names", () => {
|
|
39
|
+
const items = [{ price: 5.5 }, { price: 10.25 }, { price: 3.75 }];
|
|
40
|
+
const result = collections.sumByKey(items, "price");
|
|
41
|
+
expect(result).toBe(19.5);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("sumBy", () => {
|
|
46
|
+
it("should sum using custom selector function", () => {
|
|
47
|
+
const items = [
|
|
48
|
+
{ value: 10, multiplier: 2 },
|
|
49
|
+
{ value: 20, multiplier: 3 },
|
|
50
|
+
{ value: 30, multiplier: 1 },
|
|
51
|
+
];
|
|
52
|
+
const result = collections.sumBy(items, (item) => item.value * item.multiplier);
|
|
53
|
+
expect(result).toBe(110); // 10*2 + 20*3 + 30*1
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("should handle empty array with function selector", () => {
|
|
57
|
+
const items: { value: number }[] = [];
|
|
58
|
+
const result = collections.sumBy(items, (item) => item.value);
|
|
59
|
+
expect(result).toBe(0);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("should work with complex calculations", () => {
|
|
63
|
+
const items = [
|
|
64
|
+
{ quantity: 5, price: 10 },
|
|
65
|
+
{ quantity: 3, price: 20 },
|
|
66
|
+
{ quantity: 2, price: 15 },
|
|
67
|
+
];
|
|
68
|
+
const result = collections.sumBy(items, (item) => item.quantity * item.price);
|
|
69
|
+
expect(result).toBe(140); // 5*10 + 3*20 + 2*15
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should handle negative values", () => {
|
|
73
|
+
const items = [{ value: 10 }, { value: -5 }, { value: 20 }];
|
|
74
|
+
const result = collections.sumBy(items, (item) => item.value);
|
|
75
|
+
expect(result).toBe(25);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should treat non-numeric function results as 0", () => {
|
|
79
|
+
const items = [{ value: 10 }, { value: 20 }, { value: 30 }];
|
|
80
|
+
const result = collections.sumBy(items, (item) =>
|
|
81
|
+
item.value > 15 ? item.value : ("invalid" as unknown as number),
|
|
82
|
+
);
|
|
83
|
+
expect(result).toBe(50); // Only 20 and 30 are summed
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { collections } from "../../src/collections/index.js";
|
|
3
|
+
|
|
4
|
+
describe("unique-objects functions", () => {
|
|
5
|
+
describe("uniqueObjectsByKey", () => {
|
|
6
|
+
it("should remove duplicate objects based on key property", () => {
|
|
7
|
+
const items = [
|
|
8
|
+
{ id: 1, name: "Alice" },
|
|
9
|
+
{ id: 2, name: "Bob" },
|
|
10
|
+
{ id: 1, name: "Alice Duplicate" },
|
|
11
|
+
];
|
|
12
|
+
const result = collections.uniqueObjectsByKey(items, "id");
|
|
13
|
+
expect(result).toHaveLength(2);
|
|
14
|
+
expect(result[0]).toEqual({ id: 1, name: "Alice Duplicate" });
|
|
15
|
+
expect(result[1]).toEqual({ id: 2, name: "Bob" });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should keep last occurrence when duplicates exist", () => {
|
|
19
|
+
const items = [
|
|
20
|
+
{ id: 1, value: "first" },
|
|
21
|
+
{ id: 1, value: "second" },
|
|
22
|
+
{ id: 1, value: "third" },
|
|
23
|
+
];
|
|
24
|
+
const result = collections.uniqueObjectsByKey(items, "id");
|
|
25
|
+
expect(result).toHaveLength(1);
|
|
26
|
+
expect(result[0].value).toBe("third");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should handle empty array", () => {
|
|
30
|
+
const items: { id: number }[] = [];
|
|
31
|
+
const result = collections.uniqueObjectsByKey(items, "id");
|
|
32
|
+
expect(result).toEqual([]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should handle array with single item", () => {
|
|
36
|
+
const items = [{ id: 1, name: "Alice" }];
|
|
37
|
+
const result = collections.uniqueObjectsByKey(items, "id");
|
|
38
|
+
expect(result).toHaveLength(1);
|
|
39
|
+
expect(result[0]).toEqual({ id: 1, name: "Alice" });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should handle all unique items", () => {
|
|
43
|
+
const items = [
|
|
44
|
+
{ id: 1, name: "Alice" },
|
|
45
|
+
{ id: 2, name: "Bob" },
|
|
46
|
+
{ id: 3, name: "Charlie" },
|
|
47
|
+
];
|
|
48
|
+
const result = collections.uniqueObjectsByKey(items, "id");
|
|
49
|
+
expect(result).toHaveLength(3);
|
|
50
|
+
expect(result).toEqual(items);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should work with different property types", () => {
|
|
54
|
+
const items = [
|
|
55
|
+
{ code: "A", value: 1 },
|
|
56
|
+
{ code: "B", value: 2 },
|
|
57
|
+
{ code: "A", value: 3 },
|
|
58
|
+
];
|
|
59
|
+
const result = collections.uniqueObjectsByKey(items, "code");
|
|
60
|
+
expect(result).toHaveLength(2);
|
|
61
|
+
expect(result[0].code).toBe("A");
|
|
62
|
+
expect(result[1].code).toBe("B");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should handle null items gracefully", () => {
|
|
66
|
+
const items: Array<{ id: number; name: string } | null> = [
|
|
67
|
+
{ id: 1, name: "Alice" },
|
|
68
|
+
null,
|
|
69
|
+
{ id: 2, name: "Bob" },
|
|
70
|
+
];
|
|
71
|
+
const result = collections.uniqueObjectsByKey(items, "id");
|
|
72
|
+
expect(result).toHaveLength(3);
|
|
73
|
+
expect(result[0]).toEqual({ id: 1, name: "Alice" });
|
|
74
|
+
expect(result[1]).toBe(null);
|
|
75
|
+
expect(result[2]).toEqual({ id: 2, name: "Bob" });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should group multiple null items together", () => {
|
|
79
|
+
const items: Array<{ id: number; name: string } | null> = [
|
|
80
|
+
{ id: 1, name: "Alice" },
|
|
81
|
+
null,
|
|
82
|
+
{ id: 2, name: "Bob" },
|
|
83
|
+
null,
|
|
84
|
+
null,
|
|
85
|
+
];
|
|
86
|
+
const result = collections.uniqueObjectsByKey(items, "id");
|
|
87
|
+
expect(result).toHaveLength(3);
|
|
88
|
+
expect(result.filter((item) => item === null)).toHaveLength(1);
|
|
89
|
+
expect(result[0]).toEqual({ id: 1, name: "Alice" });
|
|
90
|
+
expect(result[1]).toBe(null);
|
|
91
|
+
expect(result[2]).toEqual({ id: 2, name: "Bob" });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should handle undefined items gracefully", () => {
|
|
95
|
+
const items: Array<{ id: number; name: string } | undefined> = [
|
|
96
|
+
{ id: 1, name: "Alice" },
|
|
97
|
+
undefined,
|
|
98
|
+
{ id: 2, name: "Bob" },
|
|
99
|
+
];
|
|
100
|
+
const result = collections.uniqueObjectsByKey(items, "id");
|
|
101
|
+
expect(result).toHaveLength(3);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should handle numeric keys", () => {
|
|
105
|
+
const items = [
|
|
106
|
+
{ count: 5, label: "first" },
|
|
107
|
+
{ count: 10, label: "second" },
|
|
108
|
+
{ count: 5, label: "third" },
|
|
109
|
+
];
|
|
110
|
+
const result = collections.uniqueObjectsByKey(items, "count");
|
|
111
|
+
expect(result).toHaveLength(2);
|
|
112
|
+
expect(result[0].count).toBe(5);
|
|
113
|
+
expect(result[1].count).toBe(10);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("should handle boolean keys", () => {
|
|
117
|
+
const items = [
|
|
118
|
+
{ active: true, id: 1 },
|
|
119
|
+
{ active: false, id: 2 },
|
|
120
|
+
{ active: true, id: 3 },
|
|
121
|
+
];
|
|
122
|
+
const result = collections.uniqueObjectsByKey(items, "active");
|
|
123
|
+
expect(result).toHaveLength(2);
|
|
124
|
+
expect(result[0].active).toBe(true);
|
|
125
|
+
expect(result[1].active).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("uniqueObjectsBy", () => {
|
|
130
|
+
it("should remove duplicates using custom selector function", () => {
|
|
131
|
+
const items = [
|
|
132
|
+
{ firstName: "Alice", lastName: "Smith" },
|
|
133
|
+
{ firstName: "Bob", lastName: "Jones" },
|
|
134
|
+
{ firstName: "Alice", lastName: "Brown" },
|
|
135
|
+
];
|
|
136
|
+
const result = collections.uniqueObjectsBy(items, (obj) => obj.firstName);
|
|
137
|
+
expect(result).toHaveLength(2);
|
|
138
|
+
expect(result[0].firstName).toBe("Alice");
|
|
139
|
+
expect(result[1].firstName).toBe("Bob");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("should handle complex key generation", () => {
|
|
143
|
+
const items = [
|
|
144
|
+
{ firstName: "Alice", lastName: "Smith" },
|
|
145
|
+
{ firstName: "Bob", lastName: "Jones" },
|
|
146
|
+
{ firstName: "Alice", lastName: "Smith" },
|
|
147
|
+
];
|
|
148
|
+
const result = collections.uniqueObjectsBy(
|
|
149
|
+
items,
|
|
150
|
+
(obj) => `${obj.firstName}-${obj.lastName}`,
|
|
151
|
+
);
|
|
152
|
+
expect(result).toHaveLength(2);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("should keep last occurrence when duplicates exist", () => {
|
|
156
|
+
const items = [
|
|
157
|
+
{ id: 1, category: "A", value: "first" },
|
|
158
|
+
{ id: 2, category: "A", value: "second" },
|
|
159
|
+
{ id: 3, category: "B", value: "third" },
|
|
160
|
+
];
|
|
161
|
+
const result = collections.uniqueObjectsBy(items, (obj) => obj.category);
|
|
162
|
+
expect(result).toHaveLength(2);
|
|
163
|
+
expect(result[0].value).toBe("second");
|
|
164
|
+
expect(result[1].value).toBe("third");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("should handle empty array", () => {
|
|
168
|
+
const items: { id: number }[] = [];
|
|
169
|
+
const result = collections.uniqueObjectsBy(items, (obj) => String(obj.id));
|
|
170
|
+
expect(result).toEqual([]);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("should handle array with single item", () => {
|
|
174
|
+
const items = [{ id: 1, name: "Alice" }];
|
|
175
|
+
const result = collections.uniqueObjectsBy(items, (obj) => String(obj.id));
|
|
176
|
+
expect(result).toHaveLength(1);
|
|
177
|
+
expect(result[0]).toEqual({ id: 1, name: "Alice" });
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("should handle all unique items", () => {
|
|
181
|
+
const items = [
|
|
182
|
+
{ id: 1, name: "Alice" },
|
|
183
|
+
{ id: 2, name: "Bob" },
|
|
184
|
+
{ id: 3, name: "Charlie" },
|
|
185
|
+
];
|
|
186
|
+
const result = collections.uniqueObjectsBy(items, (obj) => String(obj.id));
|
|
187
|
+
expect(result).toHaveLength(3);
|
|
188
|
+
expect(result).toEqual(items);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("should work with transformed values", () => {
|
|
192
|
+
const items = [
|
|
193
|
+
{ email: "Alice@Example.com" },
|
|
194
|
+
{ email: "bob@example.com" },
|
|
195
|
+
{ email: "alice@example.com" },
|
|
196
|
+
];
|
|
197
|
+
const result = collections.uniqueObjectsBy(items, (obj) => obj.email.toLowerCase());
|
|
198
|
+
expect(result).toHaveLength(2);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("should handle complex object transformations", () => {
|
|
202
|
+
const items = [
|
|
203
|
+
{ x: 1, y: 2 },
|
|
204
|
+
{ x: 3, y: 4 },
|
|
205
|
+
{ x: 1, y: 2 },
|
|
206
|
+
];
|
|
207
|
+
const result = collections.uniqueObjectsBy(items, (obj) => `${obj.x},${obj.y}`);
|
|
208
|
+
expect(result).toHaveLength(2);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("should handle numeric return from selector", () => {
|
|
212
|
+
const items = [
|
|
213
|
+
{ id: 1, value: 100 },
|
|
214
|
+
{ id: 2, value: 200 },
|
|
215
|
+
{ id: 3, value: 100 },
|
|
216
|
+
];
|
|
217
|
+
const result = collections.uniqueObjectsBy(items, (obj) => String(obj.value));
|
|
218
|
+
expect(result).toHaveLength(2);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("should deduplicate based on computed values", () => {
|
|
222
|
+
const items = [
|
|
223
|
+
{ price: 10, quantity: 2 },
|
|
224
|
+
{ price: 5, quantity: 4 },
|
|
225
|
+
{ price: 20, quantity: 1 },
|
|
226
|
+
];
|
|
227
|
+
const result = collections.uniqueObjectsBy(items, (obj) =>
|
|
228
|
+
String(obj.price * obj.quantity),
|
|
229
|
+
);
|
|
230
|
+
expect(result).toHaveLength(1); // All have total of 20
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { extension } from "../../src/extensibility/extension.js";
|
|
3
|
+
|
|
4
|
+
class Host {
|
|
5
|
+
public static instance = new Host();
|
|
6
|
+
public n = 0;
|
|
7
|
+
|
|
8
|
+
public bump(amount: number): number {
|
|
9
|
+
this.n += amount;
|
|
10
|
+
return this.n;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class HostExtensions {
|
|
15
|
+
@extension(Host, { static: true })
|
|
16
|
+
public static grow(host: Host, amount: number): number {
|
|
17
|
+
return host.bump(amount);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
void HostExtensions;
|
|
22
|
+
|
|
23
|
+
describe("extension({ static: true })", () => {
|
|
24
|
+
it("installs a constructor method that runs on the singleton", () => {
|
|
25
|
+
Host.instance.n = 0;
|
|
26
|
+
expect((Host as unknown as { grow: (amount: number) => number }).grow(2)).toBe(2);
|
|
27
|
+
expect(Host.instance.n).toBe(2);
|
|
28
|
+
});
|
|
29
|
+
});
|