sounding 0.0.0 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -4
- package/RESEARCH.md +743 -231
- package/index.js +74 -3
- package/lib/create-app-manager.js +329 -0
- package/lib/create-auth-helpers.js +279 -0
- package/lib/create-browser-manager.js +132 -0
- package/lib/create-expect.js +155 -0
- package/lib/create-helper-runner.js +55 -0
- package/lib/create-mail-capture.js +391 -0
- package/lib/create-mailbox.js +28 -0
- package/lib/create-request-client.js +552 -0
- package/lib/create-runtime.js +170 -0
- package/lib/create-test-api.js +228 -0
- package/lib/create-visit-client.js +114 -0
- package/lib/create-world-engine.js +300 -0
- package/lib/create-world-loader.js +128 -0
- package/lib/default-config.js +76 -0
- package/lib/define-world.js +37 -0
- package/lib/merge-config.js +25 -0
- package/lib/normalize-config.js +54 -0
- package/lib/resolve-auth-config.js +93 -0
- package/lib/resolve-datastore.js +97 -0
- package/package.json +17 -1
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
const nodeTest = require('node:test')
|
|
2
|
+
const { createAppManager } = require('./create-app-manager')
|
|
3
|
+
const { createExpect } = require('./create-expect')
|
|
4
|
+
|
|
5
|
+
let defaultAppManager = null
|
|
6
|
+
let defaultCleanupRegistered = false
|
|
7
|
+
let trialQueue = Promise.resolve()
|
|
8
|
+
|
|
9
|
+
function getDefaultAppManager() {
|
|
10
|
+
defaultAppManager ||= createAppManager()
|
|
11
|
+
return defaultAppManager
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function ensureDefaultAppManagerCleanup() {
|
|
15
|
+
if (defaultCleanupRegistered || typeof nodeTest.after !== 'function') {
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
nodeTest.after(async () => {
|
|
20
|
+
if (defaultAppManager) {
|
|
21
|
+
await defaultAppManager.lower()
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
defaultCleanupRegistered = true
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeTestArgs(title, optionsOrHandler, maybeHandler) {
|
|
29
|
+
if (typeof optionsOrHandler === 'function') {
|
|
30
|
+
return {
|
|
31
|
+
title,
|
|
32
|
+
options: {},
|
|
33
|
+
handler: optionsOrHandler,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
title,
|
|
39
|
+
options: optionsOrHandler || {},
|
|
40
|
+
handler: maybeHandler,
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function resolveRuntimeFromGlobals(options = {}) {
|
|
45
|
+
const runtime = globalThis.sounding || globalThis.sails?.sounding || globalThis.sails?.hooks?.sounding
|
|
46
|
+
const requiresHttp = Boolean(options.http || options.browser)
|
|
47
|
+
const httpServer = globalThis.sails?.hooks?.http?.server
|
|
48
|
+
const hasHttpServer = Boolean(
|
|
49
|
+
httpServer &&
|
|
50
|
+
(httpServer.listening ||
|
|
51
|
+
(typeof httpServer.address === 'function' && httpServer.address()))
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if (runtime && (!requiresHttp || hasHttpServer)) {
|
|
55
|
+
return {
|
|
56
|
+
sounding: runtime,
|
|
57
|
+
teardown: async () => runtime.lower(),
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const appManager = getDefaultAppManager()
|
|
62
|
+
ensureDefaultAppManagerCleanup()
|
|
63
|
+
const sounding = await appManager.runtime({ http: requiresHttp })
|
|
64
|
+
return {
|
|
65
|
+
sounding,
|
|
66
|
+
teardown: async () => sounding.lower(),
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function bindRequestMethod(request, method) {
|
|
71
|
+
return typeof request?.[method] === 'function' ? request[method].bind(request) : undefined
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function splitTestOptions(options = {}) {
|
|
75
|
+
const {
|
|
76
|
+
transport,
|
|
77
|
+
browser,
|
|
78
|
+
...nodeOptions
|
|
79
|
+
} = options
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
nodeOptions: {
|
|
83
|
+
concurrency: false,
|
|
84
|
+
...nodeOptions,
|
|
85
|
+
},
|
|
86
|
+
trialOptions: {
|
|
87
|
+
transport,
|
|
88
|
+
browser,
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function runInTrialQueue(handler) {
|
|
94
|
+
const previous = trialQueue
|
|
95
|
+
let release = () => {}
|
|
96
|
+
|
|
97
|
+
trialQueue = new Promise((resolve) => {
|
|
98
|
+
release = resolve
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
await previous
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
return await handler()
|
|
105
|
+
} finally {
|
|
106
|
+
release()
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function runTrial({ runtime, mode, nodeContext, handler, options = {} }) {
|
|
111
|
+
const activeRuntime = typeof runtime === 'function' ? await runtime() : runtime
|
|
112
|
+
const resolved = activeRuntime
|
|
113
|
+
? {
|
|
114
|
+
sounding: activeRuntime,
|
|
115
|
+
teardown: async () => activeRuntime.lower(),
|
|
116
|
+
}
|
|
117
|
+
: await resolveRuntimeFromGlobals({
|
|
118
|
+
http: options.transport === 'http',
|
|
119
|
+
browser: Boolean(options.browser),
|
|
120
|
+
})
|
|
121
|
+
const sounding = resolved.sounding
|
|
122
|
+
const booted = await sounding.boot({ mode })
|
|
123
|
+
const sails = booted.sails || {}
|
|
124
|
+
|
|
125
|
+
sails.sounding ||= sounding
|
|
126
|
+
sails.hooks ||= {}
|
|
127
|
+
sails.hooks.sounding ||= sounding
|
|
128
|
+
sails.helpers ||= sounding.helpers
|
|
129
|
+
|
|
130
|
+
const request = options.transport ? sounding.request.using(options.transport) : sounding.request
|
|
131
|
+
const visit = options.transport ? sounding.visit.using(options.transport) : sounding.visit
|
|
132
|
+
|
|
133
|
+
let browserSession = null
|
|
134
|
+
if (options.browser) {
|
|
135
|
+
browserSession = await sounding.browser.open(options.browser === true ? {} : options.browser)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const expect = browserSession?.expect
|
|
139
|
+
? createExpect.withFallback(browserSession.expect)
|
|
140
|
+
: createExpect
|
|
141
|
+
|
|
142
|
+
const context = {
|
|
143
|
+
...nodeContext,
|
|
144
|
+
t: nodeContext,
|
|
145
|
+
expect,
|
|
146
|
+
sails,
|
|
147
|
+
request,
|
|
148
|
+
visit,
|
|
149
|
+
auth: sounding.auth,
|
|
150
|
+
login: sounding.auth?.login,
|
|
151
|
+
world: sounding.world,
|
|
152
|
+
mailbox: sounding.mailbox,
|
|
153
|
+
browser: browserSession?.browser,
|
|
154
|
+
browserContext: browserSession?.context,
|
|
155
|
+
page: browserSession?.page,
|
|
156
|
+
get: bindRequestMethod(request, 'get'),
|
|
157
|
+
head: bindRequestMethod(request, 'head'),
|
|
158
|
+
post: bindRequestMethod(request, 'post'),
|
|
159
|
+
put: bindRequestMethod(request, 'put'),
|
|
160
|
+
patch: bindRequestMethod(request, 'patch'),
|
|
161
|
+
del: bindRequestMethod(request, 'delete'),
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
return await handler(context)
|
|
166
|
+
} finally {
|
|
167
|
+
await resolved.teardown()
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function createTrialMethod(baseTest, runtime, mode) {
|
|
172
|
+
return function registerTrial(title, optionsOrHandler, maybeHandler) {
|
|
173
|
+
const { options, handler } = normalizeTestArgs(title, optionsOrHandler, maybeHandler)
|
|
174
|
+
const { nodeOptions, trialOptions } = splitTestOptions(options)
|
|
175
|
+
|
|
176
|
+
return baseTest(title, nodeOptions, async (nodeContext) => {
|
|
177
|
+
return runInTrialQueue(async () => {
|
|
178
|
+
return runTrial({
|
|
179
|
+
runtime,
|
|
180
|
+
mode,
|
|
181
|
+
nodeContext,
|
|
182
|
+
handler,
|
|
183
|
+
options: trialOptions,
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function createTestApi({ baseTest = nodeTest, runtime } = {}) {
|
|
191
|
+
function soundingTest(title, optionsOrHandler, maybeHandler) {
|
|
192
|
+
const { options, handler } = normalizeTestArgs(title, optionsOrHandler, maybeHandler)
|
|
193
|
+
const { nodeOptions, trialOptions } = splitTestOptions(options)
|
|
194
|
+
|
|
195
|
+
return baseTest(title, nodeOptions, async (nodeContext) => {
|
|
196
|
+
return runInTrialQueue(async () => {
|
|
197
|
+
return runTrial({
|
|
198
|
+
runtime,
|
|
199
|
+
mode: 'trial',
|
|
200
|
+
nodeContext,
|
|
201
|
+
handler,
|
|
202
|
+
options: trialOptions,
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
soundingTest.skip = (...args) => baseTest.skip(...args)
|
|
209
|
+
soundingTest.todo = (...args) => baseTest.todo(...args)
|
|
210
|
+
if (typeof baseTest.only === 'function') {
|
|
211
|
+
soundingTest.only = (...args) => baseTest.only(...args)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Temporary compatibility aliases while the public docs move fully to `test()`.
|
|
215
|
+
soundingTest.helper = createTrialMethod(baseTest, runtime, 'helper')
|
|
216
|
+
soundingTest.endpoint = createTrialMethod(baseTest, runtime, 'endpoint')
|
|
217
|
+
|
|
218
|
+
return soundingTest
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = {
|
|
222
|
+
createTestApi,
|
|
223
|
+
normalizeTestArgs,
|
|
224
|
+
resolveRuntimeFromGlobals,
|
|
225
|
+
runInTrialQueue,
|
|
226
|
+
runTrial,
|
|
227
|
+
splitTestOptions,
|
|
228
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const DEFAULT_HEADERS = {
|
|
2
|
+
'x-inertia': 'true',
|
|
3
|
+
'x-requested-with': 'XMLHttpRequest',
|
|
4
|
+
accept: 'text/html, application/xhtml+xml',
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function joinHeaderValue(value) {
|
|
8
|
+
return Array.isArray(value) ? value.join(',') : value
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function buildVisitHeaders(options = {}) {
|
|
12
|
+
const headers = {
|
|
13
|
+
...(options.headers || {}),
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (options.version) {
|
|
17
|
+
headers['x-inertia-version'] = options.version
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (options.errorBag) {
|
|
21
|
+
headers['x-inertia-error-bag'] = options.errorBag
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (options.only?.length) {
|
|
25
|
+
if (!options.component) {
|
|
26
|
+
throw new Error('Sounding visit() requires `component` when using `only`.')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
headers['x-inertia-partial-component'] = options.component
|
|
30
|
+
headers['x-inertia-partial-data'] = joinHeaderValue(options.only)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (options.except?.length) {
|
|
34
|
+
if (!options.component) {
|
|
35
|
+
throw new Error('Sounding visit() requires `component` when using `except`.')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
headers['x-inertia-partial-component'] = options.component
|
|
39
|
+
headers['x-inertia-partial-except'] = joinHeaderValue(options.except)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (options.reset?.length) {
|
|
43
|
+
headers['x-inertia-reset'] = joinHeaderValue(options.reset)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return headers
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildRequestOptions(options = {}) {
|
|
50
|
+
const {
|
|
51
|
+
component,
|
|
52
|
+
only,
|
|
53
|
+
except,
|
|
54
|
+
reset,
|
|
55
|
+
errorBag,
|
|
56
|
+
version,
|
|
57
|
+
...requestOptions
|
|
58
|
+
} = options
|
|
59
|
+
|
|
60
|
+
const visitHeaders = buildVisitHeaders({
|
|
61
|
+
component,
|
|
62
|
+
only,
|
|
63
|
+
except,
|
|
64
|
+
reset,
|
|
65
|
+
errorBag,
|
|
66
|
+
version,
|
|
67
|
+
headers: requestOptions.headers,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const output = {
|
|
71
|
+
...requestOptions,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (Object.keys(visitHeaders).length > 0) {
|
|
75
|
+
output.headers = visitHeaders
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return output
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function createVisitClient({ request }) {
|
|
82
|
+
const client = request.withHeaders(DEFAULT_HEADERS)
|
|
83
|
+
|
|
84
|
+
function visit(target, options = {}) {
|
|
85
|
+
return client.get(target, buildRequestOptions(options))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
visit.get = (target, options = {}) => client.get(target, buildRequestOptions(options))
|
|
89
|
+
visit.head = (target, options = {}) => client.head(target, buildRequestOptions(options))
|
|
90
|
+
visit.post = (target, payload, options = {}) => client.post(target, payload, buildRequestOptions(options))
|
|
91
|
+
visit.put = (target, payload, options = {}) => client.put(target, payload, buildRequestOptions(options))
|
|
92
|
+
visit.patch = (target, payload, options = {}) =>
|
|
93
|
+
client.patch(target, payload, buildRequestOptions(options))
|
|
94
|
+
visit.delete = (target, payload, options = {}) =>
|
|
95
|
+
client.delete(target, payload, buildRequestOptions(options))
|
|
96
|
+
visit.del = visit.delete
|
|
97
|
+
visit.using = (transport) => createVisitClient({ request: request.using(transport) })
|
|
98
|
+
|
|
99
|
+
Object.defineProperty(visit, 'transport', {
|
|
100
|
+
enumerable: true,
|
|
101
|
+
get() {
|
|
102
|
+
return client.transport
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
return visit
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
createVisitClient,
|
|
111
|
+
DEFAULT_HEADERS,
|
|
112
|
+
buildVisitHeaders,
|
|
113
|
+
buildRequestOptions,
|
|
114
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
const {
|
|
2
|
+
isFactoryDefinition,
|
|
3
|
+
isScenarioDefinition,
|
|
4
|
+
} = require('./define-world')
|
|
5
|
+
|
|
6
|
+
function mergeValue(base, patch) {
|
|
7
|
+
if (typeof patch === 'function') {
|
|
8
|
+
return patch(base)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
...base,
|
|
13
|
+
...(patch || {}),
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function createFakeHelpers({ sequence }) {
|
|
18
|
+
return {
|
|
19
|
+
person: {
|
|
20
|
+
fullName() {
|
|
21
|
+
return `Test User ${sequence('fake-person')}`
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
internet: {
|
|
25
|
+
email() {
|
|
26
|
+
return `user${sequence('fake-email')}@example.com`
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
lorem: {
|
|
30
|
+
words(count = 3) {
|
|
31
|
+
return Array.from({ length: count }, () => `word-${sequence('fake-word')}`).join(' ')
|
|
32
|
+
},
|
|
33
|
+
sentence(count = 6) {
|
|
34
|
+
return `${Array.from({ length: count }, () => `word-${sequence('fake-sentence')}`).join(' ')}.`
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function createThenableBuilder(executor, initialOverrides = {}) {
|
|
41
|
+
const state = {
|
|
42
|
+
overrides: initialOverrides,
|
|
43
|
+
traits: [],
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function run() {
|
|
47
|
+
return executor(state.overrides, {
|
|
48
|
+
traits: [...state.traits],
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const builder = {
|
|
53
|
+
trait(name) {
|
|
54
|
+
state.traits.push(name)
|
|
55
|
+
return builder
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
traits(names = []) {
|
|
59
|
+
state.traits.push(...names)
|
|
60
|
+
return builder
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
with(overrides = {}) {
|
|
64
|
+
state.overrides = overrides
|
|
65
|
+
return builder
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
value() {
|
|
69
|
+
return run()
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
then(onFulfilled, onRejected) {
|
|
73
|
+
return Promise.resolve(run()).then(onFulfilled, onRejected)
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
catch(onRejected) {
|
|
77
|
+
return Promise.resolve(run()).catch(onRejected)
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
finally(onFinally) {
|
|
81
|
+
return Promise.resolve(run()).finally(onFinally)
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return builder
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function createWorldEngine({ sails }) {
|
|
89
|
+
const factories = new Map()
|
|
90
|
+
const scenarios = new Map()
|
|
91
|
+
const sequences = new Map()
|
|
92
|
+
let currentWorld = null
|
|
93
|
+
let currentSeed = null
|
|
94
|
+
|
|
95
|
+
function sequence(nameOrBuilder = 'default', maybeBuilder) {
|
|
96
|
+
const name = typeof nameOrBuilder === 'function' ? 'default' : nameOrBuilder
|
|
97
|
+
const builder = typeof nameOrBuilder === 'function' ? nameOrBuilder : maybeBuilder
|
|
98
|
+
const next = (sequences.get(name) || 0) + 1
|
|
99
|
+
sequences.set(name, next)
|
|
100
|
+
return typeof builder === 'function' ? builder(next) : next
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function resolveFactory(name) {
|
|
104
|
+
const entry = factories.get(name)
|
|
105
|
+
|
|
106
|
+
if (!entry) {
|
|
107
|
+
throw new Error(`Unknown Sounding factory: ${name}`)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return entry
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function buildOne(name, overrides = {}, options = {}) {
|
|
114
|
+
const entry = resolveFactory(name)
|
|
115
|
+
const helpers = {
|
|
116
|
+
fake: createFakeHelpers({ sequence }),
|
|
117
|
+
sequence,
|
|
118
|
+
seed: currentSeed,
|
|
119
|
+
sails,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let value =
|
|
123
|
+
typeof entry.definition === 'function'
|
|
124
|
+
? entry.definition(helpers)
|
|
125
|
+
: { ...entry.definition }
|
|
126
|
+
|
|
127
|
+
for (const traitName of options.traits || []) {
|
|
128
|
+
if (!entry.traits.has(traitName)) {
|
|
129
|
+
throw new Error(`Unknown Sounding trait \`${traitName}\` for factory \`${name}\``)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
value = mergeValue(value, entry.traits.get(traitName))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
...value,
|
|
137
|
+
...overrides,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function createOne(name, overrides = {}, options = {}) {
|
|
142
|
+
const value = buildOne(name, overrides, options)
|
|
143
|
+
const model = sails?.models?.[name]
|
|
144
|
+
|
|
145
|
+
if (model?.create) {
|
|
146
|
+
const query = model.create(value)
|
|
147
|
+
return typeof query.fetch === 'function' ? query.fetch() : query
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return value
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function registerFactoryDefinition(entry) {
|
|
154
|
+
const nextEntry = {
|
|
155
|
+
definition: entry.definition,
|
|
156
|
+
traits: new Map(entry.traits || []),
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
factories.set(entry.name, nextEntry)
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
trait(traitName, patch) {
|
|
163
|
+
nextEntry.traits.set(traitName, patch)
|
|
164
|
+
return this
|
|
165
|
+
},
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function registerScenarioDefinition(entry) {
|
|
170
|
+
scenarios.set(entry.name, entry.definition)
|
|
171
|
+
return entry
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function defineFactory(nameOrEntry, definition) {
|
|
175
|
+
if (isFactoryDefinition(nameOrEntry)) {
|
|
176
|
+
return registerFactoryDefinition(nameOrEntry)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const entry = {
|
|
180
|
+
name: nameOrEntry,
|
|
181
|
+
definition,
|
|
182
|
+
traits: [],
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return registerFactoryDefinition(entry)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function defineScenario(nameOrEntry, definition) {
|
|
189
|
+
if (isScenarioDefinition(nameOrEntry)) {
|
|
190
|
+
return registerScenarioDefinition(nameOrEntry)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return registerScenarioDefinition({
|
|
194
|
+
name: nameOrEntry,
|
|
195
|
+
definition,
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function use(name, context = {}) {
|
|
200
|
+
const definition = scenarios.get(name)
|
|
201
|
+
|
|
202
|
+
if (!definition) {
|
|
203
|
+
throw new Error(`Unknown Sounding scenario: ${name}`)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
currentWorld = await definition({
|
|
207
|
+
build(name, overrides = {}) {
|
|
208
|
+
return createThenableBuilder(
|
|
209
|
+
(nextOverrides, options) => buildOne(name, nextOverrides, options),
|
|
210
|
+
overrides
|
|
211
|
+
)
|
|
212
|
+
},
|
|
213
|
+
create(name, overrides = {}) {
|
|
214
|
+
return createThenableBuilder(
|
|
215
|
+
(nextOverrides, options) => createOne(name, nextOverrides, options),
|
|
216
|
+
overrides
|
|
217
|
+
)
|
|
218
|
+
},
|
|
219
|
+
defineFactory,
|
|
220
|
+
defineScenario,
|
|
221
|
+
sails,
|
|
222
|
+
sequence,
|
|
223
|
+
seed: currentSeed,
|
|
224
|
+
context,
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
return currentWorld
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
build(name, overrides = {}, options = {}) {
|
|
232
|
+
return buildOne(name, overrides, options)
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
async buildMany(name, count, overrides = {}, options = {}) {
|
|
236
|
+
return Array.from({ length: count }, () => buildOne(name, overrides, options))
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
create(name, overrides = {}, options = {}) {
|
|
240
|
+
return createOne(name, overrides, options)
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
async createMany(name, count, overrides = {}, options = {}) {
|
|
244
|
+
const records = []
|
|
245
|
+
for (let index = 0; index < count; index += 1) {
|
|
246
|
+
records.push(await createOne(name, overrides, options))
|
|
247
|
+
}
|
|
248
|
+
return records
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
defineFactory,
|
|
252
|
+
defineScenario,
|
|
253
|
+
|
|
254
|
+
register(definition) {
|
|
255
|
+
if (isFactoryDefinition(definition)) {
|
|
256
|
+
return defineFactory(definition)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (isScenarioDefinition(definition)) {
|
|
260
|
+
return defineScenario(definition)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
throw new Error('Sounding could not register an unknown world definition.')
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
get current() {
|
|
267
|
+
return currentWorld
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
get factories() {
|
|
271
|
+
return Array.from(factories.keys())
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
get scenarios() {
|
|
275
|
+
return Array.from(scenarios.keys())
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
reset(options = {}) {
|
|
279
|
+
const { preserveSequences = false } = options
|
|
280
|
+
|
|
281
|
+
factories.clear()
|
|
282
|
+
scenarios.clear()
|
|
283
|
+
if (!preserveSequences) {
|
|
284
|
+
sequences.clear()
|
|
285
|
+
}
|
|
286
|
+
currentWorld = null
|
|
287
|
+
currentSeed = null
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
seed(value) {
|
|
291
|
+
currentSeed = value
|
|
292
|
+
return currentSeed
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
sequence,
|
|
296
|
+
use,
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
module.exports = { createWorldEngine }
|