@toa.io/core 1.0.0-alpha.28 → 1.0.0-alpha.282

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 (98) hide show
  1. package/CHANGELOG.md +168 -0
  2. package/package.json +8 -10
  3. package/src/assignment.js +5 -8
  4. package/src/call.js +35 -9
  5. package/src/cascade.js +9 -11
  6. package/src/component.js +61 -21
  7. package/src/composition.js +3 -7
  8. package/src/connector.js +29 -22
  9. package/src/context.js +8 -6
  10. package/src/contract/contract.js +18 -0
  11. package/src/contract/index.js +2 -7
  12. package/src/contract/reply.js +36 -15
  13. package/src/contract/request.js +20 -10
  14. package/src/contract/schemas/index.js +9 -5
  15. package/src/contract/schemas/query.yaml +14 -1
  16. package/src/contract/schemas/source.yaml +23 -0
  17. package/src/discovery.js +15 -13
  18. package/src/effect.js +15 -0
  19. package/src/emission.js +2 -6
  20. package/src/entities/changeset.js +7 -14
  21. package/src/entities/entity.js +75 -29
  22. package/src/entities/factory.js +21 -14
  23. package/src/entities/index.js +1 -5
  24. package/src/entities/newid.js +9 -0
  25. package/src/entities/set.js +13 -4
  26. package/src/event.js +21 -7
  27. package/src/exceptions.js +53 -39
  28. package/src/exposition.js +5 -8
  29. package/src/guard.js +13 -0
  30. package/src/index.js +28 -47
  31. package/src/locator.js +2 -6
  32. package/src/observation.js +3 -15
  33. package/src/operation.js +40 -14
  34. package/src/outbox/index.js +1 -0
  35. package/src/outbox/outbox.js +296 -0
  36. package/src/query/criteria.js +3 -7
  37. package/src/query/options.js +5 -8
  38. package/src/query.js +36 -7
  39. package/src/receiver.js +76 -18
  40. package/src/reflection.js +2 -6
  41. package/src/remote.js +9 -12
  42. package/src/state.js +89 -50
  43. package/src/transition.js +13 -25
  44. package/src/transmission.js +15 -10
  45. package/src/unmanaged.js +7 -0
  46. package/test/call.fixtures.js +8 -12
  47. package/test/call.test.js +21 -9
  48. package/test/component.fixtures.js +5 -8
  49. package/test/component.test.js +22 -10
  50. package/test/connector.fixtures.js +10 -8
  51. package/test/connector.test.js +59 -38
  52. package/test/context.fixtures.js +8 -12
  53. package/test/context.test.js +18 -7
  54. package/test/contract/conditions.test.js +11 -9
  55. package/test/contract/contract.fixtures.js +12 -16
  56. package/test/contract/request.test.js +84 -34
  57. package/test/discovery.test.js +64 -0
  58. package/test/emission.fixtures.js +6 -10
  59. package/test/emission.test.js +29 -12
  60. package/test/entities/entity.fixtures.js +7 -11
  61. package/test/entities/entity.test.js +70 -49
  62. package/test/entities/factory.fixtures.js +22 -10
  63. package/test/entities/factory.test.js +29 -15
  64. package/test/entities/set.fixtures.js +6 -8
  65. package/test/entities/set.test.js +6 -5
  66. package/test/event.fixtures.js +9 -14
  67. package/test/event.test.js +57 -31
  68. package/test/locator.test.js +25 -24
  69. package/test/outbox.test.js +121 -0
  70. package/test/query.fixtures.js +1 -5
  71. package/test/query.test.js +42 -14
  72. package/test/receiver.fixtures.js +9 -12
  73. package/test/receiver.test.js +45 -22
  74. package/test/reflection.test.js +7 -6
  75. package/test/state.fixtures.js +25 -30
  76. package/test/state.test.js +72 -19
  77. package/test/transmission.fixtures.js +4 -6
  78. package/test/transmission.test.js +33 -15
  79. package/types/atomicity.d.ts +58 -0
  80. package/types/bindings.d.ts +8 -6
  81. package/types/bridges.ts +5 -2
  82. package/types/component.d.ts +7 -4
  83. package/types/context.d.ts +6 -3
  84. package/types/entity.d.ts +4 -4
  85. package/types/event.d.ts +1 -1
  86. package/types/extensions.d.ts +35 -10
  87. package/types/index.ts +16 -13
  88. package/types/message.d.ts +1 -0
  89. package/types/operations.d.ts +6 -0
  90. package/types/outbox.d.ts +53 -0
  91. package/types/query.d.ts +2 -0
  92. package/types/receiver.d.ts +2 -2
  93. package/types/reflection.d.ts +1 -1
  94. package/types/remote.d.ts +18 -0
  95. package/types/request.d.ts +16 -1
  96. package/types/state.d.ts +11 -8
  97. package/types/storages.d.ts +36 -11
  98. package/src/contract/conditions.js +0 -21
@@ -1,27 +1,32 @@
1
- 'use strict'
1
+ import * as schemas from '@toa.io/schemas'
2
+ import { describe, it, beforeEach, mock } from 'node:test'
3
+ import assert from 'node:assert/strict'
4
+ import { isDeepStrictEqual } from 'node:util'
2
5
 
3
- const clone = require('clone-deep')
4
- const { generate } = require('randomstring')
6
+ import clone from 'clone-deep'
7
+ import { generate } from 'randomstring'
5
8
 
6
- jest.mock('../../src/contract/conditions')
9
+ import { Request } from '../../src/contract/request.js'
10
+ import { Contract } from '../../src/contract/contract.js'
11
+ import * as fixtures from './contract.fixtures.js'
7
12
 
8
- const { Request } = require('../../src/contract/request')
9
- const { Conditions } = require('../../src/contract/conditions')
10
- const fixtures = require('./contract.fixtures')
13
+ // the base is real; what it was constructed with and told to fit is observable
14
+ const fit = mock.method(Contract.prototype, 'fit', () => undefined)
11
15
 
12
16
  let contract
13
17
 
14
18
  beforeEach(() => {
15
- jest.clearAllMocks()
19
+ resetCalls()
20
+ fit.mock.resetCalls()
16
21
 
17
- contract = new Request(fixtures.schema)
22
+ contract = new Request(fixtures.schema, {})
18
23
  })
19
24
 
20
25
  const dummy = { schema: { properties: {} } }
21
26
 
22
27
  it('should extend Conditions', () => {
23
- expect(contract).toBeInstanceOf(Conditions)
24
- expect(Conditions).toHaveBeenCalledWith(fixtures.schema)
28
+ assert.ok(contract instanceof Contract)
29
+ assert.strictEqual(contract.schema, fixtures.schema)
25
30
  })
26
31
 
27
32
  it('should fit request', () => {
@@ -29,7 +34,8 @@ it('should fit request', () => {
29
34
 
30
35
  contract.fit(request)
31
36
 
32
- expect(Conditions.mock.instances[0].fit).toHaveBeenCalledWith(request)
37
+ assert.ok(fit.mock.calls.some((call) =>
38
+ call.this === contract && isDeepStrictEqual(call.arguments[0], request)))
33
39
  })
34
40
 
35
41
  describe('schema', () => {
@@ -40,73 +46,117 @@ describe('schema', () => {
40
46
  })
41
47
 
42
48
  it('should provide schema', () => {
43
- expect(Request.schema({}, dummy)).toBeDefined()
49
+ assert.notStrictEqual(Request.schema({}, dummy), undefined)
44
50
  })
45
51
 
46
52
  it('should add required input if defined', () => {
47
53
  const input = { type: 'number' }
48
54
 
49
- expect(Request.schema({ input }, dummy).properties.input).toStrictEqual(input)
55
+ assert.deepStrictEqual(Request.schema({ input }, dummy).properties.input, input)
50
56
  })
51
57
 
52
58
  it('should set input as null if undefined', async () => {
53
- expect(Request.schema({}, dummy).properties.input).toStrictEqual({ type: 'null' })
59
+ assert.deepStrictEqual(Request.schema({}, dummy).properties.input, { type: 'null' })
54
60
  })
55
61
 
56
62
  it('should contain query if declaration.query is not defined', () => {
57
- expect(Request.schema({}, dummy).properties.query).toBeDefined()
63
+ assert.notStrictEqual(Request.schema({}, dummy).properties.query, undefined)
58
64
  })
59
65
 
60
66
  it('should not contain query if declaration.query is false', () => {
61
67
  schema.properties.query = { type: 'null' }
62
- expect(Request.schema({ query: false }, dummy)).toStrictEqual(schema)
68
+ assert.partialDeepStrictEqual(Request.schema({ query: false }, dummy), schema)
63
69
  })
64
70
 
65
71
  it('should require query if declaration.query is true', () => {
66
72
  schema.required = ['query']
67
- expect(Request.schema({ query: true }, dummy).required).toStrictEqual(expect.arrayContaining(['query']))
73
+ assert.ok(['query'].every((item) => Request.schema({ query: true }, dummy).required.some((candidate) => isDeepStrictEqual(candidate, item))))
68
74
  })
69
75
 
70
76
  it('should forbid projection for non observations', () => {
71
- expect(Request.schema({ type: 'transition' }, dummy).properties.query.properties.projection)
72
- .toBe(undefined)
77
+ assert.strictEqual(Request.schema({ type: 'transition' }, dummy).properties.query.properties.projection, undefined)
73
78
 
74
- expect(Request.schema({ type: 'assignment' }, dummy).properties.query.properties.projection)
75
- .toBe(undefined)
79
+ assert.strictEqual(Request.schema({ type: 'assignment' }, dummy).properties.query.properties.projection, undefined)
76
80
 
77
- expect(Request.schema({ type: 'observation' }, dummy).properties.query.properties.projection)
78
- .toBeDefined()
81
+ assert.notStrictEqual(Request.schema({ type: 'observation' }, dummy).properties.query.properties.projection, undefined)
79
82
  })
80
83
 
81
84
  it('should forbid version for observations', () => {
82
- expect(Request.schema({ type: 'transition' }, dummy).properties.query.properties.version)
83
- .toBeDefined()
85
+ assert.notStrictEqual(Request.schema({ type: 'transition' }, dummy).properties.query.properties.version, undefined)
84
86
 
85
- expect(Request.schema({ type: 'observation' }, dummy).properties.query.properties.version)
86
- .toBe(undefined)
87
+ assert.strictEqual(Request.schema({ type: 'observation' }, dummy).properties.query.properties.version, undefined)
87
88
  })
88
89
 
89
90
  it('should allow omit, limit only for set observations', () => {
90
91
  const schema = Request.schema({ type: 'transition' }, dummy)
91
92
  const transition = schema.properties.query.properties
92
93
 
93
- expect(transition.omit).toBeUndefined()
94
- expect(transition.limit).toBeUndefined()
94
+ assert.strictEqual(transition.omit, undefined)
95
+ assert.strictEqual(transition.limit, undefined)
95
96
 
96
97
  const object = Request.schema({
97
98
  type: 'observation',
98
99
  scope: 'object'
99
100
  }, dummy).properties.query.properties
100
101
 
101
- expect(object.omit).toBeUndefined()
102
- expect(object.limit).toBeUndefined()
102
+ assert.strictEqual(object.omit, undefined)
103
+ assert.strictEqual(object.limit, undefined)
103
104
 
104
105
  const objects = Request.schema({
105
106
  type: 'observation',
106
107
  scope: 'objects'
107
108
  }, dummy).properties.query.properties
108
109
 
109
- expect(objects.omit).toBeDefined()
110
- expect(objects.limit).toBeDefined()
110
+ assert.notStrictEqual(objects.omit, undefined)
111
+ assert.notStrictEqual(objects.limit, undefined)
111
112
  })
112
113
  })
114
+
115
+ describe('source', () => {
116
+
117
+
118
+ const compile = (definition, entity) =>
119
+ schemas.schema(Request.schema(definition, entity), { removeAdditional: true })
120
+
121
+ it('should declare source', () => {
122
+ const schema = Request.schema({}, dummy)
123
+
124
+ assert.notStrictEqual(schema.properties.source, undefined)
125
+ assert.deepStrictEqual(schema.properties.source.additionalProperties, false)
126
+ })
127
+
128
+ it('should pass known source variants', () => {
129
+ const schema = compile({}, undefined)
130
+
131
+ for (const source of [
132
+ { namespace: 'a', component: 'b', operation: 'c' },
133
+ { namespace: 'a', component: 'b', event: 'c' },
134
+ { service: 'exposition' }
135
+ ]) {
136
+ const request = { input: null, query: null, source }
137
+
138
+ assert.deepStrictEqual(schema.fit(request), null)
139
+ assert.deepStrictEqual(request.source, source)
140
+ }
141
+ })
142
+
143
+ // `source` crosses the wire and keys the introspection map, so whatever
144
+ // a peer adds to it must not survive
145
+ it('should strip unknown source properties', () => {
146
+ const schema = compile({}, undefined)
147
+ const request = { input: null, query: null, source: { service: 'exposition', evil: 'x' } }
148
+
149
+ assert.deepStrictEqual(schema.fit(request), null)
150
+ assert.deepStrictEqual(request.source, { service: 'exposition' })
151
+ })
152
+ })
153
+
154
+ function resetCalls (target = [assert, clone, fixtures, dummy], seen = new Set()) {
155
+ if (target === null || typeof target !== 'object' || seen.has(target)) return
156
+
157
+ seen.add(target)
158
+
159
+ for (const value of Object.values(target))
160
+ if (typeof value === 'function' && value.mock !== undefined) value.mock.resetCalls()
161
+ else resetCalls(value, seen)
162
+ }
@@ -0,0 +1,64 @@
1
+ import { it, beforeEach, afterEach, mock } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { isDeepStrictEqual } from 'node:util'
4
+
5
+ import { console } from 'openspan'
6
+ import { Connector } from '../src/connector.js'
7
+ import { Discovery } from '../src/discovery.js'
8
+
9
+ class Lookup extends Connector {
10
+ invoke = mock.fn(async () => ({ operations: {} }))
11
+ }
12
+
13
+ /** @type {Lookup} */
14
+ let lookup
15
+
16
+ /** @type {Discovery} */
17
+ let discovery
18
+
19
+ /** @type {import('node:test').Mock<any>} */
20
+ let warn
21
+
22
+ const locator = { id: 'dummies.one' }
23
+
24
+ beforeEach(async () => {
25
+ mock.timers.enable()
26
+
27
+ warn = mock.method(console, 'warn', () => undefined)
28
+ lookup = new Lookup()
29
+ discovery = new Discovery(async () => lookup)
30
+
31
+ await discovery.connect()
32
+ })
33
+
34
+ afterEach(() => {
35
+ warn.mock.restore()
36
+ mock.timers.reset()
37
+ })
38
+
39
+ it('should not warn if a lookup is answered', async () => {
40
+ await discovery.lookup(locator)
41
+
42
+ mock.timers.tick(60_000)
43
+
44
+ assert.strictEqual(warn.mock.callCount(), 0)
45
+ })
46
+
47
+ it('should keep warning while a lookup is unanswered', async () => {
48
+ lookup.invoke.mock.mockImplementation(() => new Promise(() => undefined))
49
+
50
+ void discovery.lookup(locator)
51
+
52
+ // the interval is set after the lookup is resolved, so it has to exist
53
+ // before time is advanced
54
+ await new Promise((resolve) => process.nextTick(resolve))
55
+
56
+ // node:test moves the clock before running the callbacks it releases, so the
57
+ // elapsed time each warning reports is only right when time advances by steps
58
+ mock.timers.tick(5_000)
59
+ mock.timers.tick(5_000)
60
+
61
+ assert.strictEqual(warn.mock.callCount(), 2)
62
+
63
+ assert.ok(((call) => call.arguments.length === 2 && isDeepStrictEqual(call.arguments[0], 'Waiting for lookup response') && isDeepStrictEqual(call.arguments[1], { component: locator.id, waiting: 10 }))(warn.mock.calls.at(-1) ?? { arguments: [] }))
64
+ })
@@ -1,17 +1,13 @@
1
- 'use strict'
1
+ import { mock } from 'node:test'
2
2
 
3
- const { generate } = require('randomstring')
3
+ import { generate } from 'randomstring'
4
4
 
5
5
  // noinspection JSCheckFunctionSignatures
6
- const events = [0, 1, 2].map((index) => ({
7
- emit: jest.fn(async (state) => ({ ...state, event: index }))
6
+ export const events = [0, 1, 2].map((index) => ({
7
+ emit: mock.fn(async (state) => ({ ...state, event: index }))
8
8
  }))
9
9
 
10
- const event = {
10
+ export const event = {
11
11
  origin: { [generate()]: generate() },
12
- state: { [generate()]: generate() },
13
- changeset: { [generate()]: generate() }
12
+ state: { [generate()]: generate() }
14
13
  }
15
-
16
- exports.events = events
17
- exports.event = event
@@ -1,17 +1,25 @@
1
- 'use strict'
1
+ import { it, beforeEach, mock } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { isDeepStrictEqual } from 'node:util'
2
4
 
3
- jest.mock('../src/connector')
5
+ import clone from 'clone-deep'
4
6
 
5
- const clone = require('clone-deep')
7
+ import { Connector } from '../src/connector.js'
6
8
 
7
- const { Connector } = require('../src/connector')
8
- const { Emission } = require('../src/emission')
9
- const fixtures = require('./emission.fixtures')
9
+ // the fixtures are not connectors, so a dependency is recorded rather than linked
10
+ const depends = mock.method(Connector.prototype, 'depends', () => undefined)
11
+
12
+ const dependencies = (instance) => depends.mock.calls
13
+ .filter((call) => call.this === instance)
14
+ .map((call) => call.arguments[0])
15
+ import { Emission } from '../src/emission.js'
16
+ import * as fixtures from './emission.fixtures.js'
10
17
 
11
18
  let emission, event
12
19
 
13
20
  beforeEach(async () => {
14
- jest.clearAllMocks()
21
+ resetCalls()
22
+ depends.mock.resetCalls()
15
23
 
16
24
  emission = new Emission(fixtures.events)
17
25
  event = clone(fixtures.event)
@@ -20,16 +28,25 @@ beforeEach(async () => {
20
28
  })
21
29
 
22
30
  it('should depend on events', () => {
23
- expect(emission).toBeInstanceOf(Connector)
24
- expect(Connector.mock.instances[0].depends).toHaveBeenCalledWith(fixtures.events)
31
+ assert.ok(emission instanceof Connector)
32
+ assert.ok(dependencies(emission).some((one) => isDeepStrictEqual(one, fixtures.events)))
25
33
  })
26
34
 
27
35
  it('should emit events', async () => {
28
- expect.assertions(fixtures.events.length)
29
-
36
+
30
37
  await emission.emit(event)
31
38
 
32
39
  for (const evt of fixtures.events) {
33
- expect(evt.emit).toHaveBeenCalledWith(event)
40
+ assert.ok(evt.emit.mock.calls.some((call) => call.arguments.length === 1 && isDeepStrictEqual(call.arguments[0], event)))
34
41
  }
35
42
  })
43
+
44
+ function resetCalls (target = [assert, clone, depends, dependencies, fixtures], seen = new Set()) {
45
+ if (target === null || typeof target !== 'object' || seen.has(target)) return
46
+
47
+ seen.add(target)
48
+
49
+ for (const value of Object.values(target))
50
+ if (typeof value === 'function' && value.mock !== undefined) value.mock.resetCalls()
51
+ else resetCalls(value, seen)
52
+ }
@@ -1,16 +1,16 @@
1
- 'use strict'
1
+ import { mock } from 'node:test'
2
2
 
3
- const { generate } = require('randomstring')
3
+ import { generate } from 'randomstring'
4
4
 
5
5
  // noinspection JSCheckFunctionSignatures
6
- const schema = {
7
- fit: jest.fn((object) =>
6
+ export const schema = {
7
+ fit: mock.fn((object) =>
8
8
  (object.fail ? { [generate()]: generate() } : null)),
9
9
 
10
- defaults: jest.fn(() => ({ [generate()]: generate() }))
10
+ defaults: mock.fn(() => ({ [generate()]: generate() }))
11
11
  }
12
12
 
13
- const state = () => ({
13
+ export const state = () => ({
14
14
  id: generate(),
15
15
  foo: generate(),
16
16
  _created: generate(),
@@ -19,11 +19,7 @@ const state = () => ({
19
19
  _version: 0
20
20
  })
21
21
 
22
- const failed = () => ({
22
+ export const failed = () => ({
23
23
  ...state(),
24
24
  fail: true
25
25
  })
26
-
27
- exports.schema = schema
28
- exports.state = state
29
- exports.failed = failed
@@ -1,46 +1,78 @@
1
- 'use strict'
1
+ import { describe, it, beforeEach, mock } from 'node:test'
2
+ import assert from 'node:assert/strict'
2
3
 
3
- const { Entity } = require('../../src/entities/entity')
4
- const fixtures = require('./entity.fixtures')
4
+ import { Entity } from '../../src/entities/entity.js'
5
+ import * as fixtures from './entity.fixtures.js'
5
6
 
6
7
  beforeEach(() => {
7
- jest.clearAllMocks()
8
+ resetCalls()
8
9
  })
9
10
 
10
- describe('new', () => {
11
- it('should throw on schema error', () => {
12
- const entity = new Entity(fixtures.schema)
11
+ describe('argument', () => {
12
+ it('should set state', () => {
13
+ const state = fixtures.state()
14
+ const entity = new Entity(fixtures.schema, state)
13
15
 
14
- expect(() => entity.set(fixtures.failed())).toThrow()
16
+ assert.deepStrictEqual(entity.get(), state)
15
17
  })
16
18
 
17
- it('should provide state', () => {
18
- const entity = new Entity(fixtures.schema)
19
- const state = fixtures.state()
19
+ it('should snapshot the record it may commit', () => {
20
+ const record = fixtures.state()
21
+ const entity = new Entity(fixtures.schema, record)
20
22
 
21
- entity.set(state)
23
+ assert.notStrictEqual(entity.get(), record)
24
+ assert.strictEqual(entity.event().origin, record)
25
+ })
26
+ })
27
+
28
+ describe('read-only', () => {
29
+ it('should take the record as it came', () => {
30
+ const record = fixtures.state()
31
+ const entity = new Entity(fixtures.schema, record, undefined, false)
22
32
 
23
- expect(entity.get()).toEqual(state)
33
+ // no pre-image to diff against, hence no copy of it
34
+ assert.strictEqual(entity.get(), record)
35
+ })
36
+
37
+ it('should still report a tombstone', () => {
38
+ const record = { ...fixtures.state(), _deleted: Date.now() }
39
+ const entity = new Entity(fixtures.schema, record, undefined, false)
40
+
41
+ assert.strictEqual(entity.deleted, true)
42
+ })
43
+
44
+ it('should refuse to be modified', () => {
45
+ const entity = new Entity(fixtures.schema, fixtures.state(), undefined, false)
46
+
47
+ assert.throws(() => entity.set(entity.get()), (error) => /read-only/.test(error.message))
24
48
  })
25
49
  })
26
50
 
27
- describe('argument', () => {
28
- it('should provide initial state if no argument passed', () => {
29
- const entity = new Entity(fixtures.schema)
30
- const defaults = fixtures.schema.defaults.mock.results[0].value
31
- const expected = {
32
- ...defaults,
33
- _version: 0
34
- }
35
-
36
- expect(entity.get()).toStrictEqual(expect.objectContaining(expected))
51
+ describe('tombstone', () => {
52
+ it('should lift tombstone when transition leaves _deleted untouched', () => {
53
+ const origin = fixtures.state()
54
+ const entity = new Entity(fixtures.schema, origin)
55
+ const state = entity.get()
56
+
57
+ state.foo = 'revived'
58
+ entity.set(state)
59
+
60
+ assert.strictEqual(entity.get()._deleted, null)
61
+ assert.strictEqual(entity.deleted, false)
62
+ assert.strictEqual(entity.event().state._deleted, null)
37
63
  })
38
64
 
39
- it('should set state', () => {
40
- const state = fixtures.state()
41
- const entity = new Entity(fixtures.schema, state)
65
+ it('should keep tombstone written by transition', () => {
66
+ const origin = { ...fixtures.state(), _deleted: null }
67
+ const entity = new Entity(fixtures.schema, origin)
68
+ const state = entity.get()
69
+ const timestamp = Date.now()
70
+
71
+ state._deleted = timestamp
72
+ entity.set(state)
42
73
 
43
- expect(entity.get()).toEqual(state)
74
+ assert.strictEqual(entity.get()._deleted, timestamp)
75
+ assert.strictEqual(entity.deleted, true)
44
76
  })
45
77
  })
46
78
 
@@ -54,29 +86,18 @@ it('should provide event', () => {
54
86
 
55
87
  const event = entity.event()
56
88
 
57
- expect(event).toEqual(expect.objectContaining({
58
- state,
59
- origin,
60
- changeset: expect.objectContaining({
61
- foo: 'new value',
62
- _version: 1
63
- })
64
- }))
89
+ assert.partialDeepStrictEqual(event, { state, origin })
90
+ assert.strictEqual(event.state.foo, 'new value')
91
+ assert.strictEqual(event.state._version, 1)
92
+ assert.notStrictEqual(event.origin.foo, 'new value')
65
93
  })
66
94
 
67
- it('should define `id` as readonly', async () => {
68
- const origin = fixtures.state()
69
- const entity = new Entity(fixtures.schema, origin)
70
- const state = entity.get()
71
-
72
- expect(() => (state.id = 1)).toThrow('assign to read only property')
73
- })
95
+ function resetCalls (target = [assert, fixtures], seen = new Set()) {
96
+ if (target === null || typeof target !== 'object' || seen.has(target)) return
74
97
 
75
- it('should seal id', async () => {
76
- const origin = fixtures.state()
77
- const entity = new Entity(fixtures.schema, origin)
78
- const state = entity.get()
79
- const redefine = () => Object.defineProperty(state, 'id', { writable: true })
98
+ seen.add(target)
80
99
 
81
- expect(redefine).toThrow('redefine property')
82
- })
100
+ for (const value of Object.values(target))
101
+ if (typeof value === 'function' && value.mock !== undefined) value.mock.resetCalls()
102
+ else resetCalls(value, seen)
103
+ }
@@ -1,18 +1,30 @@
1
- 'use strict'
1
+ import { mock } from 'node:test'
2
2
 
3
- const randomstring = require('randomstring')
3
+ import randomstring from 'randomstring'
4
4
 
5
5
  const schema = { [randomstring.generate()]: randomstring.generate() }
6
- const storage = { id: jest.fn(() => randomstring.generate()) }
6
+ const storage = { id: mock.fn(() => randomstring.generate()) }
7
7
  const entity = { [randomstring.generate()]: randomstring.generate() }
8
8
  const set = Array.from(Array(5))
9
9
  .map((_, index) => ({ id: index, [randomstring.generate()]: randomstring.generate() }))
10
10
 
11
- const Entity = jest.fn().mockImplementation(function () { this.id = randomstring.generate() })
12
- const EntitySet = jest.fn().mockImplementation(function () {})
11
+ // node:test records a mock's calls but not the instances it constructed
12
+ const entities = []
13
13
 
14
- exports.schema = schema
15
- exports.storage = storage
16
- exports.entity = entity
17
- exports.set = set
18
- exports.mock = { Entity, EntitySet }
14
+ const Entity = mock.fn(function () {
15
+ this.id = randomstring.generate()
16
+ entities.push(this)
17
+ })
18
+
19
+ const EntitySet = mock.fn(function () {})
20
+
21
+
22
+
23
+
24
+
25
+
26
+
27
+ // named `mock` for its consumers, which is what node:test calls its own tracker
28
+ const mocks = { Entity, EntitySet }
29
+
30
+ export { schema, storage, entity, set, entities, mocks as mock }
@@ -1,19 +1,23 @@
1
- 'use strict'
1
+ import { it, beforeEach, mock as mocking } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { isDeepStrictEqual } from 'node:util'
2
4
 
3
- const { generate } = require('randomstring')
5
+ import { generate } from 'randomstring'
4
6
 
5
- const fixtures = require('./factory.fixtures')
7
+ import * as fixtures from './factory.fixtures.js'
6
8
  const mock = fixtures.mock
7
9
 
8
- jest.mock('../../src/entities/entity', () => ({ Entity: mock.Entity }))
9
- jest.mock('../../src/entities/set', () => ({ EntitySet: mock.EntitySet }))
10
+ mocking.module('../../src/entities/entity', { namedExports: ({ Entity: mock.Entity }) })
11
+ mocking.module('../../src/entities/set', { namedExports: ({ EntitySet: mock.EntitySet }) })
12
+
13
+ const { Factory } = await import('../../src/entities/factory.js')
10
14
 
11
- const { Factory } = require('../../src/entities/factory')
12
15
 
13
16
  let factory
14
17
 
15
18
  beforeEach(async () => {
16
- jest.clearAllMocks()
19
+ resetCalls()
20
+ fixtures.entities.length = 0
17
21
 
18
22
  factory = new Factory(fixtures.schema, () => fixtures.storage.id())
19
23
  })
@@ -22,27 +26,37 @@ it('should create initial', () => {
22
26
  const id = generate()
23
27
  const initial = factory.init(id)
24
28
 
25
- expect(initial).toBeInstanceOf(mock.Entity)
26
- expect(initial.constructor).toHaveBeenCalledWith(fixtures.schema, id)
29
+ assert.ok(initial instanceof mock.Entity)
30
+ assert.ok(mock.Entity.mock.calls.some((call) => call.arguments.length === 3 && isDeepStrictEqual(call.arguments[0], fixtures.schema) && isDeepStrictEqual(call.arguments[1], id) && typeof call.arguments[2] === 'function'))
27
31
  })
28
32
 
29
33
  it('should create instance', () => {
30
34
  const object = factory.object(fixtures.entity)
31
35
 
32
- expect(object).toBeInstanceOf(mock.Entity)
33
- expect(object.constructor).toHaveBeenCalledWith(fixtures.schema, fixtures.entity)
36
+ assert.ok(object instanceof mock.Entity)
37
+ assert.ok(mock.Entity.mock.calls.some((call) => call.arguments.length === 4 && isDeepStrictEqual(call.arguments[0], fixtures.schema) && isDeepStrictEqual(call.arguments[1], fixtures.entity) && typeof call.arguments[2] === 'function' && isDeepStrictEqual(call.arguments[3], true)))
34
38
  })
35
39
 
36
40
  it('should create set', () => {
37
41
  const objects = factory.objects(fixtures.set)
38
42
 
39
- expect(objects).toBeInstanceOf(mock.EntitySet)
43
+ assert.ok(objects instanceof mock.EntitySet)
40
44
 
41
45
  const instances = fixtures.set.map((entity, index) => {
42
- expect(mock.Entity).toHaveBeenNthCalledWith(index + 1, fixtures.schema, entity)
46
+ assert.ok(((call) => call.arguments.length === 4 && isDeepStrictEqual(call.arguments[0], fixtures.schema) && isDeepStrictEqual(call.arguments[1], entity) && typeof call.arguments[2] === 'function' && isDeepStrictEqual(call.arguments[3], true))(mock.Entity.mock.calls[index + 1 - 1] ?? { arguments: [] }))
43
47
 
44
- return mock.Entity.mock.instances[index]
48
+ return fixtures.entities[index]
45
49
  })
46
50
 
47
- expect(objects.constructor).toHaveBeenCalledWith(instances)
51
+ assert.ok(mock.EntitySet.mock.calls.some((call) => call.arguments.length === 1 && isDeepStrictEqual(call.arguments[0], instances)))
48
52
  })
53
+
54
+ function resetCalls (target = [assert, fixtures, mock], seen = new Set()) {
55
+ if (target === null || typeof target !== 'object' || seen.has(target)) return
56
+
57
+ seen.add(target)
58
+
59
+ for (const value of Object.values(target))
60
+ if (typeof value === 'function' && value.mock !== undefined) value.mock.resetCalls()
61
+ else resetCalls(value, seen)
62
+ }