sounding 0.0.0 → 0.0.1
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 +31 -4
- package/RESEARCH.md +743 -231
- package/index.js +58 -3
- package/lib/create-app-manager.js +329 -0
- package/lib/create-auth-helpers.js +159 -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 +549 -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 +60 -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-datastore.js +97 -0
- package/package.json +17 -1
package/index.js
CHANGED
|
@@ -1,4 +1,59 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
const { createRuntime } = require('./lib/create-runtime')
|
|
2
|
+
const { createAppManager } = require('./lib/create-app-manager')
|
|
3
|
+
const { createMailbox } = require('./lib/create-mailbox')
|
|
4
|
+
const { createMailCapture } = require('./lib/create-mail-capture')
|
|
5
|
+
const { createWorldEngine } = require('./lib/create-world-engine')
|
|
6
|
+
const { loadWorldFiles } = require('./lib/create-world-loader')
|
|
7
|
+
const { defineFactory, defineScenario } = require('./lib/define-world')
|
|
8
|
+
const { createHelperRunner } = require('./lib/create-helper-runner')
|
|
9
|
+
const { createRequestClient } = require('./lib/create-request-client')
|
|
10
|
+
const { createVisitClient } = require('./lib/create-visit-client')
|
|
11
|
+
const { createBrowserManager } = require('./lib/create-browser-manager')
|
|
12
|
+
const { createAuthHelpers } = require('./lib/create-auth-helpers')
|
|
13
|
+
const { createExpect } = require('./lib/create-expect')
|
|
14
|
+
const { createTestApi } = require('./lib/create-test-api')
|
|
15
|
+
const { getDefaultConfig } = require('./lib/default-config')
|
|
16
|
+
|
|
17
|
+
function soundingHook(sails) {
|
|
18
|
+
const runtime = createRuntime(sails)
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
defaults: {
|
|
22
|
+
sounding: getDefaultConfig(),
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
configure() {
|
|
26
|
+
sails.hooks ||= {}
|
|
27
|
+
sails.sounding = runtime
|
|
28
|
+
sails.hooks.sounding = this
|
|
29
|
+
runtime.configure()
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
initialize(done) {
|
|
33
|
+
sails.sounding = runtime
|
|
34
|
+
Object.assign(this, runtime)
|
|
35
|
+
sails.hooks.sounding = this
|
|
36
|
+
return done()
|
|
37
|
+
},
|
|
38
|
+
}
|
|
4
39
|
}
|
|
40
|
+
|
|
41
|
+
module.exports = soundingHook
|
|
42
|
+
module.exports.test = createTestApi()
|
|
43
|
+
module.exports.expect = createExpect
|
|
44
|
+
module.exports.defineFactory = defineFactory
|
|
45
|
+
module.exports.defineScenario = defineScenario
|
|
46
|
+
module.exports.createRuntime = createRuntime
|
|
47
|
+
module.exports.createAppManager = createAppManager
|
|
48
|
+
module.exports.createMailbox = createMailbox
|
|
49
|
+
module.exports.createWorldEngine = createWorldEngine
|
|
50
|
+
module.exports.loadWorldFiles = loadWorldFiles
|
|
51
|
+
module.exports.createHelperRunner = createHelperRunner
|
|
52
|
+
module.exports.createRequestClient = createRequestClient
|
|
53
|
+
module.exports.createVisitClient = createVisitClient
|
|
54
|
+
module.exports.createBrowserManager = createBrowserManager
|
|
55
|
+
module.exports.createAuthHelpers = createAuthHelpers
|
|
56
|
+
module.exports.createExpect = createExpect
|
|
57
|
+
module.exports.createTestApi = createTestApi
|
|
58
|
+
module.exports.getDefaultConfig = getDefaultConfig
|
|
59
|
+
module.exports.createMailCapture = createMailCapture
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
const fs = require('node:fs')
|
|
2
|
+
const path = require('node:path')
|
|
3
|
+
|
|
4
|
+
const { getDefaultConfig } = require('./default-config')
|
|
5
|
+
const { mergeConfig } = require('./merge-config')
|
|
6
|
+
const { normalizeUserConfig } = require('./normalize-config')
|
|
7
|
+
const { buildManagedSqlitePath, resolveManagedRoot } = require('./resolve-datastore')
|
|
8
|
+
|
|
9
|
+
function resolveModuleFromApp(appPath, moduleId) {
|
|
10
|
+
return require(require.resolve(moduleId, { paths: [appPath, process.cwd(), __dirname] }))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function loadAppSoundingConfig(appPath) {
|
|
14
|
+
const configPath = path.join(appPath, 'config', 'sounding.js')
|
|
15
|
+
|
|
16
|
+
if (!fs.existsSync(configPath)) {
|
|
17
|
+
return getDefaultConfig()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
delete require.cache[require.resolve(configPath)]
|
|
21
|
+
const loaded = require(configPath)
|
|
22
|
+
return mergeConfig(getDefaultConfig(), normalizeUserConfig(loaded?.sounding || {}))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function buildManagedDatastoreOverrides(config, appPath) {
|
|
26
|
+
if (config.datastore?.mode !== 'managed') {
|
|
27
|
+
return {}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const identity = config.datastore.identity || 'default'
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
datastores: {
|
|
34
|
+
[identity]: {
|
|
35
|
+
adapter: config.datastore.adapter || 'sails-sqlite',
|
|
36
|
+
url: buildManagedSqlitePath({
|
|
37
|
+
root: resolveManagedRoot({
|
|
38
|
+
datastore: config.datastore,
|
|
39
|
+
appPath,
|
|
40
|
+
}),
|
|
41
|
+
identity,
|
|
42
|
+
isolation: config.datastore.isolation || 'worker',
|
|
43
|
+
}),
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
models: {
|
|
47
|
+
migrate: 'drop',
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function createOutputFilter(config = {}) {
|
|
53
|
+
if (config.app?.quiet === false) {
|
|
54
|
+
return {
|
|
55
|
+
install() {},
|
|
56
|
+
uninstall() {},
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const noisyPatterns = [
|
|
61
|
+
/start\s+build started/i,
|
|
62
|
+
/ready\s+built in/i,
|
|
63
|
+
/\[DEP0044\].*util\.isArray/i,
|
|
64
|
+
/node --trace-deprecation/i,
|
|
65
|
+
/^\s*info:\s/m,
|
|
66
|
+
/success:\s*true.*no new issues to notify/i,
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
let installed = false
|
|
70
|
+
let originalStdoutWrite = null
|
|
71
|
+
let originalStderrWrite = null
|
|
72
|
+
|
|
73
|
+
function shouldSuppress(chunk) {
|
|
74
|
+
const value = String(chunk || '')
|
|
75
|
+
const normalizedValue = value.replace(/\u001b\[[0-9;]*m/g, '')
|
|
76
|
+
return noisyPatterns.some((pattern) => pattern.test(normalizedValue))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function wrapWrite(write) {
|
|
80
|
+
return function soundingWrite(chunk, encoding, callback) {
|
|
81
|
+
if (shouldSuppress(chunk)) {
|
|
82
|
+
if (typeof encoding === 'function') {
|
|
83
|
+
encoding()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (typeof callback === 'function') {
|
|
87
|
+
callback()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return true
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return write.call(this, chunk, encoding, callback)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
install() {
|
|
99
|
+
if (installed) {
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
originalStdoutWrite = process.stdout.write
|
|
104
|
+
originalStderrWrite = process.stderr.write
|
|
105
|
+
process.stdout.write = wrapWrite(originalStdoutWrite)
|
|
106
|
+
process.stderr.write = wrapWrite(originalStderrWrite)
|
|
107
|
+
installed = true
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
uninstall() {
|
|
111
|
+
if (!installed) {
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
process.stdout.write = originalStdoutWrite
|
|
116
|
+
process.stderr.write = originalStderrWrite
|
|
117
|
+
originalStdoutWrite = null
|
|
118
|
+
originalStderrWrite = null
|
|
119
|
+
installed = false
|
|
120
|
+
},
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function resolveManagedDatastoreFile(config, appPath) {
|
|
125
|
+
if (config.datastore?.mode !== 'managed') {
|
|
126
|
+
return null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const identity = config.datastore.identity || 'default'
|
|
130
|
+
|
|
131
|
+
return buildManagedSqlitePath({
|
|
132
|
+
root: resolveManagedRoot({
|
|
133
|
+
datastore: config.datastore,
|
|
134
|
+
appPath,
|
|
135
|
+
}),
|
|
136
|
+
identity,
|
|
137
|
+
isolation: config.datastore.isolation || 'worker',
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function createAppManager({
|
|
142
|
+
appPath = process.cwd(),
|
|
143
|
+
environment = 'test',
|
|
144
|
+
liftOptions = {},
|
|
145
|
+
SailsConstructor,
|
|
146
|
+
} = {}) {
|
|
147
|
+
let loadedApp = null
|
|
148
|
+
let liftedApp = null
|
|
149
|
+
let loadPromise = null
|
|
150
|
+
let liftPromise = null
|
|
151
|
+
const managedArtifacts = new Set()
|
|
152
|
+
let outputFilter = null
|
|
153
|
+
|
|
154
|
+
function resolveAppPath() {
|
|
155
|
+
return path.resolve(appPath)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function resolveConfig() {
|
|
159
|
+
return loadAppSoundingConfig(resolveAppPath())
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function resolveSailsConstructor() {
|
|
163
|
+
if (SailsConstructor) {
|
|
164
|
+
return SailsConstructor
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return resolveModuleFromApp(resolveAppPath(), 'sails').constructor
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function buildOptions(mode) {
|
|
171
|
+
const config = resolveConfig()
|
|
172
|
+
outputFilter ||= createOutputFilter(config)
|
|
173
|
+
const managedFile = resolveManagedDatastoreFile(config, resolveAppPath())
|
|
174
|
+
|
|
175
|
+
if (managedFile) {
|
|
176
|
+
managedArtifacts.add(managedFile)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const appConfig = config.app || {}
|
|
180
|
+
const baseOptions = {
|
|
181
|
+
appPath: resolveAppPath(),
|
|
182
|
+
environment: appConfig.environment || environment,
|
|
183
|
+
...buildManagedDatastoreOverrides(config, resolveAppPath()),
|
|
184
|
+
}
|
|
185
|
+
const modeOptions =
|
|
186
|
+
mode === 'load'
|
|
187
|
+
? mergeConfig(
|
|
188
|
+
{
|
|
189
|
+
hooks: {
|
|
190
|
+
shipwright: false,
|
|
191
|
+
content: false,
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
appConfig.loadOptions || {}
|
|
195
|
+
)
|
|
196
|
+
: mergeConfig(appConfig.liftOptions || {}, liftOptions)
|
|
197
|
+
const nextOptions = mergeConfig(baseOptions, modeOptions)
|
|
198
|
+
|
|
199
|
+
if (mode === 'load' && nextOptions.datastores?.content) {
|
|
200
|
+
delete nextOptions.datastores.content
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return nextOptions
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function cleanupManagedArtifacts() {
|
|
207
|
+
for (const filePath of managedArtifacts) {
|
|
208
|
+
const companionPaths = [
|
|
209
|
+
filePath,
|
|
210
|
+
`${filePath}-journal`,
|
|
211
|
+
`${filePath}-wal`,
|
|
212
|
+
`${filePath}-shm`,
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
for (const artifactPath of companionPaths) {
|
|
216
|
+
await fs.promises.rm(artifactPath, { force: true }).catch(() => {})
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function load() {
|
|
222
|
+
if (loadedApp) {
|
|
223
|
+
return loadedApp
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (loadPromise) {
|
|
227
|
+
return loadPromise
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const Sails = resolveSailsConstructor()
|
|
231
|
+
const sailsApp = new Sails()
|
|
232
|
+
const nextLoadOptions = buildOptions('load')
|
|
233
|
+
outputFilter?.install()
|
|
234
|
+
|
|
235
|
+
loadPromise = new Promise((resolve, reject) => {
|
|
236
|
+
sailsApp.load(nextLoadOptions, (error, loadedSails) => {
|
|
237
|
+
if (error) {
|
|
238
|
+
loadPromise = null
|
|
239
|
+
cleanupManagedArtifacts().catch(() => {})
|
|
240
|
+
outputFilter?.uninstall()
|
|
241
|
+
reject(error)
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
loadedApp = loadedSails
|
|
246
|
+
globalThis.sails = loadedSails
|
|
247
|
+
globalThis.sounding = loadedSails.sounding
|
|
248
|
+
resolve(loadedSails)
|
|
249
|
+
})
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
return loadPromise
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function lift() {
|
|
256
|
+
if (liftedApp) {
|
|
257
|
+
return liftedApp
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (liftPromise) {
|
|
261
|
+
return liftPromise
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const Sails = resolveSailsConstructor()
|
|
265
|
+
const sailsApp = new Sails()
|
|
266
|
+
const nextLiftOptions = buildOptions('lift')
|
|
267
|
+
outputFilter?.install()
|
|
268
|
+
|
|
269
|
+
liftPromise = new Promise((resolve, reject) => {
|
|
270
|
+
sailsApp.lift(nextLiftOptions, (error, liftedSails) => {
|
|
271
|
+
if (error) {
|
|
272
|
+
liftPromise = null
|
|
273
|
+
cleanupManagedArtifacts().catch(() => {})
|
|
274
|
+
outputFilter?.uninstall()
|
|
275
|
+
reject(error)
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
liftedApp = liftedSails
|
|
280
|
+
globalThis.sails = liftedSails
|
|
281
|
+
globalThis.sounding = liftedSails.sounding
|
|
282
|
+
resolve(liftedSails)
|
|
283
|
+
})
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
return liftPromise
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function runtime(options = {}) {
|
|
290
|
+
const app = options.http ? await lift() : await load()
|
|
291
|
+
return app.sounding || app.hooks?.sounding
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function lower() {
|
|
295
|
+
const apps = [loadedApp, liftedApp].filter(Boolean)
|
|
296
|
+
|
|
297
|
+
loadedApp = null
|
|
298
|
+
liftedApp = null
|
|
299
|
+
loadPromise = null
|
|
300
|
+
liftPromise = null
|
|
301
|
+
|
|
302
|
+
await Promise.all(
|
|
303
|
+
apps.map(
|
|
304
|
+
(app) =>
|
|
305
|
+
new Promise((resolve) => {
|
|
306
|
+
app.lower(() => resolve())
|
|
307
|
+
})
|
|
308
|
+
)
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
delete globalThis.sails
|
|
312
|
+
delete globalThis.sounding
|
|
313
|
+
outputFilter?.uninstall()
|
|
314
|
+
await cleanupManagedArtifacts()
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
load,
|
|
319
|
+
lift,
|
|
320
|
+
runtime,
|
|
321
|
+
lower,
|
|
322
|
+
resolveConfig,
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
module.exports = {
|
|
327
|
+
createAppManager,
|
|
328
|
+
loadAppSoundingConfig,
|
|
329
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
function normalizeEmail(value) {
|
|
2
|
+
return String(value || '')
|
|
3
|
+
.trim()
|
|
4
|
+
.toLowerCase()
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function looksLikeEmail(value) {
|
|
8
|
+
return typeof value === 'string' && value.includes('@')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function createAuthHelpers({ sails, world, mailbox, request }) {
|
|
12
|
+
function getUserModel() {
|
|
13
|
+
return sails?.models?.user || sails?.models?.User
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function createUserFromEmail(email, fullName) {
|
|
17
|
+
const User = getUserModel()
|
|
18
|
+
|
|
19
|
+
if (!sails?.helpers?.user?.signupWithTeam) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
'Sounding auth helpers could not find `sails.helpers.user.signupWithTeam`.'
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const signupResult = await sails.helpers.user.signupWithTeam.with({
|
|
26
|
+
fullName: fullName || normalizeEmail(email).split('@')[0],
|
|
27
|
+
email: normalizeEmail(email),
|
|
28
|
+
tosAcceptedByIp: '127.0.0.1',
|
|
29
|
+
emailStatus: 'verified',
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
if (User?.updateOne) {
|
|
33
|
+
await User.updateOne({ id: signupResult.user.id }).set({
|
|
34
|
+
emailStatus: 'verified',
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (User?.findOne) {
|
|
39
|
+
return User.findOne({ id: signupResult.user.id })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return signupResult.user
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function resolveUser(actorOrEmail, options = {}) {
|
|
46
|
+
const User = getUserModel()
|
|
47
|
+
|
|
48
|
+
if (!actorOrEmail) {
|
|
49
|
+
throw new Error('Sounding auth helpers require an actor, user, or email address.')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let candidate = actorOrEmail
|
|
53
|
+
|
|
54
|
+
if (typeof candidate === 'string' && world.current?.users?.[candidate]) {
|
|
55
|
+
candidate = world.current.users[candidate]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (candidate?.id && User?.findOne) {
|
|
59
|
+
return User.findOne({ id: candidate.id }) || candidate
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const email = candidate?.email || (looksLikeEmail(candidate) ? candidate : null)
|
|
63
|
+
|
|
64
|
+
if (!email) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Sounding auth helpers could not resolve an email address for actor \`${candidate}\`.`
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const normalizedEmail = normalizeEmail(email)
|
|
71
|
+
let user = User?.findOne ? await User.findOne({ email: normalizedEmail }) : null
|
|
72
|
+
|
|
73
|
+
if (!user && options.createIfMissing !== false) {
|
|
74
|
+
user = await createUserFromEmail(normalizedEmail, candidate?.fullName || options.fullName)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!user) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Sounding auth helpers could not find a user for ${normalizedEmail}.`
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return user
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function issueMagicLink(actorOrEmail, options = {}) {
|
|
87
|
+
const User = getUserModel()
|
|
88
|
+
|
|
89
|
+
if (!User?.updateOne) {
|
|
90
|
+
throw new Error('Sounding auth helpers require a User model with updateOne().')
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const user = await resolveUser(actorOrEmail, {
|
|
94
|
+
createIfMissing: true,
|
|
95
|
+
...options,
|
|
96
|
+
})
|
|
97
|
+
const token = await sails.helpers.magicLink.generateToken()
|
|
98
|
+
const hashedToken = await sails.helpers.magicLink.hashToken(token)
|
|
99
|
+
|
|
100
|
+
await User.updateOne({ id: user.id }).set({
|
|
101
|
+
emailStatus: 'verified',
|
|
102
|
+
magicLinkToken: hashedToken,
|
|
103
|
+
magicLinkTokenExpiresAt: Date.now() + 15 * 60 * 1000,
|
|
104
|
+
magicLinkTokenUsedAt: null,
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
const refreshedUser = User.findOne ? await User.findOne({ id: user.id }) : user
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
user: refreshedUser,
|
|
111
|
+
email: refreshedUser.email,
|
|
112
|
+
token,
|
|
113
|
+
url: `/magic-link/${token}`,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function requestMagicLink(actorOrEmail, options = {}) {
|
|
118
|
+
const user = await resolveUser(actorOrEmail, {
|
|
119
|
+
createIfMissing: true,
|
|
120
|
+
...options,
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
const response = await request.post(
|
|
124
|
+
'/magic-link',
|
|
125
|
+
{
|
|
126
|
+
email: user.email,
|
|
127
|
+
fullName: options.fullName || user.fullName,
|
|
128
|
+
redirectUrl: options.redirectUrl || '/login',
|
|
129
|
+
},
|
|
130
|
+
options.requestOptions || {}
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
response,
|
|
135
|
+
email: user.email,
|
|
136
|
+
message: mailbox.latest(),
|
|
137
|
+
url: mailbox.latest()?.ctaUrl,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const login = {
|
|
142
|
+
async as(actorOrEmail, page, options = {}) {
|
|
143
|
+
const magicLink = await issueMagicLink(actorOrEmail, options)
|
|
144
|
+
await page.goto(magicLink.url)
|
|
145
|
+
return magicLink
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
resolveUser,
|
|
151
|
+
issueMagicLink,
|
|
152
|
+
requestMagicLink,
|
|
153
|
+
login,
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = {
|
|
158
|
+
createAuthHelpers,
|
|
159
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const { resolveBaseUrl } = require('./create-request-client')
|
|
2
|
+
|
|
3
|
+
function resolveModuleFromApp(appPath, moduleId) {
|
|
4
|
+
return require(require.resolve(moduleId, { paths: [appPath, process.cwd(), __dirname] }))
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function defaultLoadPlaywright(appPath) {
|
|
8
|
+
return resolveModuleFromApp(appPath, 'playwright')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function defaultLoadPlaywrightTest(appPath) {
|
|
12
|
+
return resolveModuleFromApp(appPath, '@playwright/test')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function resolveProjectOptions(projectName, devices = {}) {
|
|
16
|
+
if (projectName === 'mobile') {
|
|
17
|
+
return (
|
|
18
|
+
devices['iPhone 13'] || {
|
|
19
|
+
viewport: {
|
|
20
|
+
width: 390,
|
|
21
|
+
height: 844,
|
|
22
|
+
},
|
|
23
|
+
isMobile: true,
|
|
24
|
+
hasTouch: true,
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function createBrowserManager({
|
|
33
|
+
sails,
|
|
34
|
+
getConfig,
|
|
35
|
+
appPathResolver = () => sails?.config?.appPath || process.cwd(),
|
|
36
|
+
loadPlaywright = defaultLoadPlaywright,
|
|
37
|
+
loadPlaywrightTest = defaultLoadPlaywrightTest,
|
|
38
|
+
} = {}) {
|
|
39
|
+
let session = null
|
|
40
|
+
|
|
41
|
+
async function open(options = {}) {
|
|
42
|
+
if (session) {
|
|
43
|
+
return session
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const config = typeof getConfig === 'function' ? getConfig() : sails?.config?.sounding || {}
|
|
47
|
+
|
|
48
|
+
if (config.browser?.enabled === false) {
|
|
49
|
+
throw new Error('Sounding browser support is disabled in `config/sounding.js`.')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const appPath = appPathResolver()
|
|
53
|
+
const playwright = await loadPlaywright(appPath)
|
|
54
|
+
const playwrightTest = await Promise.resolve()
|
|
55
|
+
.then(() => loadPlaywrightTest(appPath))
|
|
56
|
+
.catch(() => null)
|
|
57
|
+
|
|
58
|
+
const browserTypeName = options.type || config.browser?.type || 'chromium'
|
|
59
|
+
const browserType = playwright?.[browserTypeName]
|
|
60
|
+
|
|
61
|
+
if (!browserType?.launch) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Sounding could not find a Playwright browser type named \`${browserTypeName}\`.`
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const projectName =
|
|
68
|
+
options.project ||
|
|
69
|
+
config.browser?.defaultProject ||
|
|
70
|
+
config.browser?.projects?.[0] ||
|
|
71
|
+
'desktop'
|
|
72
|
+
|
|
73
|
+
const browser = await browserType.launch({
|
|
74
|
+
headless: true,
|
|
75
|
+
...(config.browser?.launchOptions || {}),
|
|
76
|
+
...(options.launchOptions || {}),
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const context = await browser.newContext({
|
|
80
|
+
baseURL: resolveBaseUrl({ sails, getConfig }),
|
|
81
|
+
...resolveProjectOptions(projectName, playwright.devices || {}),
|
|
82
|
+
...(options.contextOptions || {}),
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const page = await context.newPage()
|
|
86
|
+
|
|
87
|
+
session = {
|
|
88
|
+
playwright,
|
|
89
|
+
browser,
|
|
90
|
+
context,
|
|
91
|
+
page,
|
|
92
|
+
expect: playwrightTest?.expect,
|
|
93
|
+
project: projectName,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return session
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function close() {
|
|
100
|
+
if (!session) {
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
await session.context?.close?.()
|
|
105
|
+
await session.browser?.close?.()
|
|
106
|
+
session = null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
open,
|
|
111
|
+
close,
|
|
112
|
+
get active() {
|
|
113
|
+
return Boolean(session?.page)
|
|
114
|
+
},
|
|
115
|
+
get page() {
|
|
116
|
+
return session?.page
|
|
117
|
+
},
|
|
118
|
+
get context() {
|
|
119
|
+
return session?.context
|
|
120
|
+
},
|
|
121
|
+
get expect() {
|
|
122
|
+
return session?.expect
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = {
|
|
128
|
+
createBrowserManager,
|
|
129
|
+
defaultLoadPlaywright,
|
|
130
|
+
defaultLoadPlaywrightTest,
|
|
131
|
+
resolveProjectOptions,
|
|
132
|
+
}
|