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
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const fs = require('node:fs')
|
|
2
|
+
const path = require('node:path')
|
|
3
|
+
const { pathToFileURL } = require('node:url')
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
defineFactory,
|
|
7
|
+
defineScenario,
|
|
8
|
+
isFactoryDefinition,
|
|
9
|
+
isScenarioDefinition,
|
|
10
|
+
} = require('./define-world')
|
|
11
|
+
|
|
12
|
+
const WORLD_EXTENSIONS = new Set(['.js', '.cjs', '.mjs'])
|
|
13
|
+
|
|
14
|
+
function listDefinitionFiles(directory) {
|
|
15
|
+
const output = []
|
|
16
|
+
|
|
17
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
18
|
+
const nextPath = path.join(directory, entry.name)
|
|
19
|
+
|
|
20
|
+
if (entry.isDirectory()) {
|
|
21
|
+
output.push(...listDefinitionFiles(nextPath))
|
|
22
|
+
continue
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (WORLD_EXTENSIONS.has(path.extname(entry.name))) {
|
|
26
|
+
output.push(nextPath)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return output.sort()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function loadModule(filePath) {
|
|
34
|
+
try {
|
|
35
|
+
delete require.cache[require.resolve(filePath)]
|
|
36
|
+
return require(filePath)
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (error.code !== 'ERR_REQUIRE_ESM') {
|
|
39
|
+
throw error
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return import(`${pathToFileURL(filePath).href}?t=${Date.now()}`)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function registerExport(value, api, source) {
|
|
47
|
+
const entry = value?.default ?? value
|
|
48
|
+
|
|
49
|
+
if (entry === undefined || entry === null) {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (Array.isArray(entry)) {
|
|
54
|
+
for (const item of entry) {
|
|
55
|
+
await registerExport(item, api, source)
|
|
56
|
+
}
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (isFactoryDefinition(entry) || isScenarioDefinition(entry)) {
|
|
61
|
+
api.world.register(entry)
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (typeof entry === 'function') {
|
|
66
|
+
const returned = await entry(api)
|
|
67
|
+
|
|
68
|
+
if (returned !== undefined) {
|
|
69
|
+
await registerExport(returned, api, source)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (typeof entry === 'object' && (entry.factories || entry.scenarios)) {
|
|
76
|
+
for (const factory of entry.factories || []) {
|
|
77
|
+
await registerExport(factory, api, source)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const scenario of entry.scenarios || []) {
|
|
81
|
+
await registerExport(scenario, api, source)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Sounding could not understand the world definition exported from ${source}.`
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function loadWorldFiles({ world, appPath, config, sails }) {
|
|
93
|
+
const directories = [config.world?.factories, config.world?.scenarios]
|
|
94
|
+
.filter(Boolean)
|
|
95
|
+
.map((relativePath) => path.resolve(appPath, relativePath))
|
|
96
|
+
|
|
97
|
+
const loadedFiles = []
|
|
98
|
+
const api = {
|
|
99
|
+
sails,
|
|
100
|
+
world,
|
|
101
|
+
defineFactory,
|
|
102
|
+
defineScenario,
|
|
103
|
+
factory: defineFactory,
|
|
104
|
+
scenario: defineScenario,
|
|
105
|
+
registerFactory: world.defineFactory.bind(world),
|
|
106
|
+
registerScenario: world.defineScenario.bind(world),
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for (const directory of directories) {
|
|
110
|
+
if (!fs.existsSync(directory)) {
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const filePath of listDefinitionFiles(directory)) {
|
|
115
|
+
const loaded = await loadModule(filePath)
|
|
116
|
+
await registerExport(loaded, api, filePath)
|
|
117
|
+
loadedFiles.push(filePath)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return loadedFiles
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = {
|
|
125
|
+
loadWorldFiles,
|
|
126
|
+
listDefinitionFiles,
|
|
127
|
+
loadModule,
|
|
128
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const DEFAULT_CONFIG = Object.freeze({
|
|
2
|
+
app: {
|
|
3
|
+
path: '.',
|
|
4
|
+
environment: 'test',
|
|
5
|
+
quiet: true,
|
|
6
|
+
liftOptions: {},
|
|
7
|
+
},
|
|
8
|
+
world: {
|
|
9
|
+
factories: 'tests/factories',
|
|
10
|
+
scenarios: 'tests/scenarios',
|
|
11
|
+
},
|
|
12
|
+
datastore: {
|
|
13
|
+
mode: 'managed',
|
|
14
|
+
identity: 'default',
|
|
15
|
+
adapter: 'sails-sqlite',
|
|
16
|
+
root: '.tmp/db',
|
|
17
|
+
isolation: 'worker',
|
|
18
|
+
},
|
|
19
|
+
browser: {
|
|
20
|
+
enabled: true,
|
|
21
|
+
type: 'chromium',
|
|
22
|
+
projects: ['desktop'],
|
|
23
|
+
defaultProject: 'desktop',
|
|
24
|
+
launchOptions: {
|
|
25
|
+
headless: true,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
mail: {
|
|
29
|
+
capture: true,
|
|
30
|
+
},
|
|
31
|
+
request: {
|
|
32
|
+
transport: 'virtual',
|
|
33
|
+
},
|
|
34
|
+
auth: {
|
|
35
|
+
defaultActor: 'guest',
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
function cloneValue(value) {
|
|
40
|
+
if (Array.isArray(value)) {
|
|
41
|
+
return value.map(cloneValue)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (value && typeof value === 'object') {
|
|
45
|
+
return Object.fromEntries(
|
|
46
|
+
Object.entries(value).map(([key, nested]) => [key, cloneValue(nested)])
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return value
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getDefaultConfig() {
|
|
54
|
+
return cloneValue(DEFAULT_CONFIG)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = {
|
|
58
|
+
DEFAULT_CONFIG,
|
|
59
|
+
getDefaultConfig,
|
|
60
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
function defineFactory(name, definition) {
|
|
2
|
+
const entry = {
|
|
3
|
+
__soundingType: 'factory',
|
|
4
|
+
name,
|
|
5
|
+
definition,
|
|
6
|
+
traits: [],
|
|
7
|
+
trait(traitName, patch) {
|
|
8
|
+
entry.traits.push([traitName, patch])
|
|
9
|
+
return entry
|
|
10
|
+
},
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return entry
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function defineScenario(name, definition) {
|
|
17
|
+
return {
|
|
18
|
+
__soundingType: 'scenario',
|
|
19
|
+
name,
|
|
20
|
+
definition,
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isFactoryDefinition(value) {
|
|
25
|
+
return value?.__soundingType === 'factory'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isScenarioDefinition(value) {
|
|
29
|
+
return value?.__soundingType === 'scenario'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = {
|
|
33
|
+
defineFactory,
|
|
34
|
+
defineScenario,
|
|
35
|
+
isFactoryDefinition,
|
|
36
|
+
isScenarioDefinition,
|
|
37
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
function isPlainObject(value) {
|
|
2
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function mergeConfig(base, override) {
|
|
6
|
+
const output = { ...base }
|
|
7
|
+
|
|
8
|
+
for (const [key, value] of Object.entries(override || {})) {
|
|
9
|
+
if (Array.isArray(value)) {
|
|
10
|
+
output[key] = [...value]
|
|
11
|
+
continue
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (isPlainObject(value) && isPlainObject(base[key])) {
|
|
15
|
+
output[key] = mergeConfig(base[key], value)
|
|
16
|
+
continue
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
output[key] = value
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return output
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = { mergeConfig }
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
function isPlainObject(value) {
|
|
2
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function normalizeDatastore(datastore) {
|
|
6
|
+
if (typeof datastore === 'string') {
|
|
7
|
+
return {
|
|
8
|
+
mode: datastore,
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (!isPlainObject(datastore)) {
|
|
13
|
+
return datastore
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const normalized = { ...datastore }
|
|
17
|
+
const legacyManaged = isPlainObject(normalized.managed) ? normalized.managed : {}
|
|
18
|
+
|
|
19
|
+
if (normalized.adapter == null && legacyManaged.adapter != null) {
|
|
20
|
+
normalized.adapter = legacyManaged.adapter
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (normalized.root == null) {
|
|
24
|
+
normalized.root = legacyManaged.root ?? legacyManaged.directory
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (normalized.isolation == null && legacyManaged.isolation != null) {
|
|
28
|
+
normalized.isolation = legacyManaged.isolation
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
delete normalized.managed
|
|
32
|
+
delete normalized.directory
|
|
33
|
+
|
|
34
|
+
return normalized
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeUserConfig(config = {}) {
|
|
38
|
+
if (!isPlainObject(config)) {
|
|
39
|
+
return {}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const normalized = { ...config }
|
|
43
|
+
|
|
44
|
+
if ('datastore' in normalized) {
|
|
45
|
+
normalized.datastore = normalizeDatastore(normalized.datastore)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return normalized
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = {
|
|
52
|
+
normalizeDatastore,
|
|
53
|
+
normalizeUserConfig,
|
|
54
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const path = require('node:path')
|
|
2
|
+
|
|
3
|
+
function getWorkerToken(env = process.env) {
|
|
4
|
+
return (
|
|
5
|
+
env.SOUNDING_WORKER_INDEX ||
|
|
6
|
+
env.PLAYWRIGHT_WORKER_INDEX ||
|
|
7
|
+
env.TEST_WORKER_INDEX ||
|
|
8
|
+
String(process.pid)
|
|
9
|
+
)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function buildManagedSqlitePath({
|
|
13
|
+
root = path.join(process.cwd(), '.tmp', 'db'),
|
|
14
|
+
identity = 'default',
|
|
15
|
+
isolation = 'worker',
|
|
16
|
+
env = process.env,
|
|
17
|
+
}) {
|
|
18
|
+
const workerToken = isolation === 'run' ? 'run' : `worker-${getWorkerToken(env)}`
|
|
19
|
+
return path.join(root, identity, `${workerToken}.db`)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function resolveManagedRoot({ sails, datastore = {}, appPath } = {}) {
|
|
23
|
+
const configuredRoot = datastore.root || path.join('.tmp', 'db')
|
|
24
|
+
|
|
25
|
+
if (path.isAbsolute(configuredRoot)) {
|
|
26
|
+
return configuredRoot
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return path.resolve(appPath || sails?.config?.appPath || process.cwd(), configuredRoot)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function resolveDatastore({ sails, soundingConfig }) {
|
|
33
|
+
const datastoreConfig = soundingConfig.datastore || {}
|
|
34
|
+
const mode = datastoreConfig.mode || 'managed'
|
|
35
|
+
const identity = datastoreConfig.identity || 'default'
|
|
36
|
+
const datastores = (sails.config.datastores ||= {})
|
|
37
|
+
|
|
38
|
+
if (mode === 'inherit' || mode === 'external') {
|
|
39
|
+
const configuredDatastore = datastores[identity]
|
|
40
|
+
|
|
41
|
+
if (!configuredDatastore) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Sounding could not find datastore \`${identity}\` in sails.config.datastores for mode \`${mode}\`.`
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
mode,
|
|
49
|
+
identity,
|
|
50
|
+
config: { ...configuredDatastore },
|
|
51
|
+
managed: false,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (mode === 'managed') {
|
|
56
|
+
const adapter = datastoreConfig.adapter || 'sails-sqlite'
|
|
57
|
+
|
|
58
|
+
if (adapter !== 'sails-sqlite') {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Sounding only supports managed adapter \`sails-sqlite\` in v0.0.1. Received \`${adapter}\`.`
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const filePath = buildManagedSqlitePath({
|
|
65
|
+
root: resolveManagedRoot({
|
|
66
|
+
sails,
|
|
67
|
+
datastore: datastoreConfig,
|
|
68
|
+
}),
|
|
69
|
+
identity,
|
|
70
|
+
isolation: datastoreConfig.isolation || 'worker',
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const nextDatastore = {
|
|
74
|
+
...(datastores[identity] || {}),
|
|
75
|
+
adapter,
|
|
76
|
+
url: filePath,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
datastores[identity] = nextDatastore
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
mode,
|
|
83
|
+
identity,
|
|
84
|
+
config: { ...nextDatastore },
|
|
85
|
+
managed: true,
|
|
86
|
+
filePath,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
throw new Error(`Unknown Sounding datastore mode: ${mode}`)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = {
|
|
94
|
+
buildManagedSqlitePath,
|
|
95
|
+
resolveManagedRoot,
|
|
96
|
+
resolveDatastore,
|
|
97
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sounding",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.1",
|
|
4
4
|
"description": "Testing framework for Sails applications and The Boring JavaScript Stack.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"license": "MIT",
|
|
@@ -13,10 +13,26 @@
|
|
|
13
13
|
],
|
|
14
14
|
"files": [
|
|
15
15
|
"index.js",
|
|
16
|
+
"lib",
|
|
16
17
|
"README.md",
|
|
17
18
|
"RESEARCH.md",
|
|
18
19
|
"LICENSE"
|
|
19
20
|
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "node --test"
|
|
23
|
+
},
|
|
24
|
+
"sails": {
|
|
25
|
+
"isHook": true,
|
|
26
|
+
"hookName": "sounding"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/sailscastshq/sounding.git"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/sailscastshq/sounding#readme",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/sailscastshq/sounding/issues"
|
|
35
|
+
},
|
|
20
36
|
"publishConfig": {
|
|
21
37
|
"access": "public"
|
|
22
38
|
}
|