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.
@@ -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 }