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,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
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
const assert = require('node:assert/strict')
|
|
2
|
+
|
|
3
|
+
function getPath(target, path) {
|
|
4
|
+
return path.split('.').reduce((current, segment) => current?.[segment], target)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function getHeader(actual, name) {
|
|
8
|
+
if (typeof actual?.header === 'function') {
|
|
9
|
+
return actual.header(name)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (typeof actual?.headers?.get === 'function') {
|
|
13
|
+
return actual.headers.get(name)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return actual?.headers?.[name]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resolveStructuredValue(actual) {
|
|
20
|
+
if (actual?.data !== undefined) {
|
|
21
|
+
return actual.data
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return actual
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function shouldUseFallback(actual) {
|
|
28
|
+
return Boolean(
|
|
29
|
+
actual &&
|
|
30
|
+
typeof actual === 'object' &&
|
|
31
|
+
!Array.isArray(actual) &&
|
|
32
|
+
actual.status === undefined &&
|
|
33
|
+
actual.data === undefined &&
|
|
34
|
+
typeof actual.header !== 'function' &&
|
|
35
|
+
typeof actual.headers?.get !== 'function'
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createExpect(actual, { fallback } = {}) {
|
|
40
|
+
if (fallback && shouldUseFallback(actual)) {
|
|
41
|
+
return fallback(actual)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
toBe(expected) {
|
|
46
|
+
assert.strictEqual(actual, expected)
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
toEqual(expected) {
|
|
50
|
+
assert.deepStrictEqual(actual, expected)
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
toContain(expected) {
|
|
54
|
+
if (typeof actual === 'string') {
|
|
55
|
+
assert.ok(actual.includes(expected))
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (Array.isArray(actual)) {
|
|
60
|
+
assert.ok(actual.includes(expected))
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
throw new TypeError('Sounding expect().toContain() only supports strings and arrays in v0.0.1.')
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
toMatch(expected) {
|
|
68
|
+
if (expected instanceof RegExp) {
|
|
69
|
+
assert.match(actual, expected)
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
assert.ok(String(actual).includes(String(expected)))
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
toBeTruthy() {
|
|
77
|
+
assert.ok(actual)
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
toBeFalsy() {
|
|
81
|
+
assert.ok(!actual)
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
toBeDefined() {
|
|
85
|
+
assert.notStrictEqual(actual, undefined)
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
toHaveStatus(expected) {
|
|
89
|
+
assert.strictEqual(actual?.status, expected)
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
toHaveHeader(name, expected) {
|
|
93
|
+
const header = getHeader(actual, name)
|
|
94
|
+
assert.notStrictEqual(header, null)
|
|
95
|
+
assert.notStrictEqual(header, undefined)
|
|
96
|
+
|
|
97
|
+
if (expected !== undefined) {
|
|
98
|
+
assert.strictEqual(header, expected)
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
toRedirectTo(expected) {
|
|
103
|
+
const location = getHeader(actual, 'location')
|
|
104
|
+
assert.strictEqual(location, expected)
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
toHaveJsonPath(path, expected) {
|
|
108
|
+
const value = getPath(resolveStructuredValue(actual), path)
|
|
109
|
+
assert.deepStrictEqual(value, expected)
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
toBeInertiaPage(component) {
|
|
113
|
+
const value = resolveStructuredValue(actual)
|
|
114
|
+
assert.strictEqual(value?.component, component)
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
toHaveProp(path, expected) {
|
|
118
|
+
const value = getPath(resolveStructuredValue(actual)?.props, path)
|
|
119
|
+
assert.deepStrictEqual(value, expected)
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
toMatchProp(path, expected) {
|
|
123
|
+
const value = getPath(resolveStructuredValue(actual)?.props, path)
|
|
124
|
+
|
|
125
|
+
if (expected instanceof RegExp) {
|
|
126
|
+
assert.match(String(value), expected)
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
assert.ok(String(value).includes(String(expected)))
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
toHaveSharedProp(path, expected) {
|
|
134
|
+
const value = getPath(resolveStructuredValue(actual)?.props, path)
|
|
135
|
+
assert.deepStrictEqual(value, expected)
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
toHaveValidationError(path, expected) {
|
|
139
|
+
const value = getPath(resolveStructuredValue(actual)?.props?.errors, path)
|
|
140
|
+
assert.notStrictEqual(value, undefined)
|
|
141
|
+
|
|
142
|
+
if (expected !== undefined) {
|
|
143
|
+
assert.deepStrictEqual(value, expected)
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
createExpect.withFallback = function withFallback(fallback) {
|
|
150
|
+
return function soundingExpect(actual) {
|
|
151
|
+
return createExpect(actual, { fallback })
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = { createExpect }
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
function resolveHelper(sails, identity) {
|
|
2
|
+
return identity
|
|
3
|
+
.split('.')
|
|
4
|
+
.reduce((current, segment) => current?.[segment], sails.helpers)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function createHelperRunner({ sails }) {
|
|
8
|
+
async function invoke(identity, inputs = {}) {
|
|
9
|
+
const helper = resolveHelper(sails, identity)
|
|
10
|
+
|
|
11
|
+
if (!helper) {
|
|
12
|
+
throw new Error(`Unknown Sounding helper: ${identity}`)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (typeof helper.with === 'function') {
|
|
16
|
+
return helper.with(inputs)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (typeof helper === 'function') {
|
|
20
|
+
return helper(inputs)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
throw new Error(`Sounding helper \`${identity}\` is not callable.`)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function buildProxy(path = []) {
|
|
27
|
+
const callable = async (...args) => {
|
|
28
|
+
if (path.length === 0) {
|
|
29
|
+
const [identity, inputs = {}] = args
|
|
30
|
+
return invoke(identity, inputs)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const [inputs = {}] = args
|
|
34
|
+
return invoke(path.join('.'), inputs)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return new Proxy(callable, {
|
|
38
|
+
get(_target, property) {
|
|
39
|
+
if (property === 'path') {
|
|
40
|
+
return path.join('.')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (typeof property !== 'string') {
|
|
44
|
+
return undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return buildProxy([...path, property])
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return buildProxy()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { createHelperRunner }
|