@ossy/platform 3.1.0 → 3.3.0
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/package.json +6 -6
- package/src/tasks/task-service.js +10 -1
- package/src/test/flow-click.js +43 -0
- package/src/test/flow-click.spec.js +42 -0
- package/src/test/flow-context.js +44 -0
- package/src/test/flow-context.spec.js +44 -0
- package/src/test/flow-press.js +25 -0
- package/src/test/flow-press.spec.js +25 -0
- package/src/test/flow-runner.js +66 -10
- package/src/test/flow-viewport.js +23 -0
- package/src/test/flow-viewport.spec.js +26 -0
- package/src/test/test.util.js +25 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/platform",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "Ossy application server runtime",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -45,16 +45,16 @@
|
|
|
45
45
|
"@aws-sdk/util-format-url": "^3.972.17",
|
|
46
46
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
47
47
|
"@ossy/config": "^3.0.9",
|
|
48
|
-
"@ossy/event-store": "^3.0
|
|
48
|
+
"@ossy/event-store": "^3.2.0",
|
|
49
49
|
"@ossy/locale": "^3.0.9",
|
|
50
50
|
"@ossy/manifest": "^3.0.9",
|
|
51
51
|
"@ossy/observability": "^3.0.9",
|
|
52
52
|
"@ossy/policies": "^3.0.9",
|
|
53
53
|
"@ossy/schema": "^3.0.9",
|
|
54
|
-
"@ossy/sdk": "^3.0
|
|
54
|
+
"@ossy/sdk": "^3.3.0",
|
|
55
55
|
"@ossy/tokens": "^3.0.9",
|
|
56
|
-
"@ossy/users": "^3.0
|
|
57
|
-
"@ossy/workspaces": "^3.
|
|
56
|
+
"@ossy/users": "^3.2.0",
|
|
57
|
+
"@ossy/workspaces": "^3.2.0",
|
|
58
58
|
"cookie-parser": "^1.4.7",
|
|
59
59
|
"dotenv": ">=16.0.0 <18.0.0",
|
|
60
60
|
"express": ">=5.0.0 <6.0.0",
|
|
@@ -74,5 +74,5 @@
|
|
|
74
74
|
"src",
|
|
75
75
|
"Dockerfile"
|
|
76
76
|
],
|
|
77
|
-
"gitHead": "
|
|
77
|
+
"gitHead": "19e8bae4c1e9df79270218f28c0c9391f4f9871c"
|
|
78
78
|
}
|
|
@@ -99,10 +99,19 @@ export class TaskService {
|
|
|
99
99
|
|
|
100
100
|
const audit = context.audit !== false && task.metadata.audit !== false
|
|
101
101
|
|
|
102
|
+
// GET/API handlers often omit integrations; default like dispatch/scheduler so
|
|
103
|
+
// tasks can always use integrations.get('email') (including local delivery).
|
|
104
|
+
const invokeContext = {
|
|
105
|
+
...context,
|
|
106
|
+
sdk: context.sdk ?? TaskService._sdk,
|
|
107
|
+
integrations: context.integrations ?? IntegrationService,
|
|
108
|
+
log: context.log ?? createLogger(id),
|
|
109
|
+
}
|
|
110
|
+
|
|
102
111
|
return TaskRunService.execute({
|
|
103
112
|
taskId: id,
|
|
104
113
|
handler: task.handler,
|
|
105
|
-
context,
|
|
114
|
+
context: invokeContext,
|
|
106
115
|
trigger: 'invoke',
|
|
107
116
|
triggeredBy: {
|
|
108
117
|
channel: context.req ? undefined : 'api',
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize a declarative flow `click` step into a Playwright locator click.
|
|
3
|
+
* Prefer `{ action }` for `data-action` controls; use `click` for structural hooks
|
|
4
|
+
* (e.g. overlay backdrop) that are not action POJOs.
|
|
5
|
+
*
|
|
6
|
+
* @param {unknown} click
|
|
7
|
+
* @returns {{ selector: string, position?: { x: number, y: number } }}
|
|
8
|
+
*/
|
|
9
|
+
export function resolveClickTarget (click) {
|
|
10
|
+
if (typeof click === 'string') {
|
|
11
|
+
const selector = click.trim()
|
|
12
|
+
if (!selector) {
|
|
13
|
+
throw new Error('click step requires a non-empty CSS selector')
|
|
14
|
+
}
|
|
15
|
+
return { selector }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (click != null && typeof click === 'object' && !Array.isArray(click)) {
|
|
19
|
+
const selector = typeof click.selector === 'string' ? click.selector.trim() : ''
|
|
20
|
+
if (!selector) {
|
|
21
|
+
throw new Error('click step requires a non-empty CSS selector')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const out = { selector }
|
|
25
|
+
if (click.position != null) {
|
|
26
|
+
if (
|
|
27
|
+
typeof click.position !== 'object'
|
|
28
|
+
|| Array.isArray(click.position)
|
|
29
|
+
|| !Number.isFinite(Number(click.position.x))
|
|
30
|
+
|| !Number.isFinite(Number(click.position.y))
|
|
31
|
+
) {
|
|
32
|
+
throw new Error('click.position requires { x, y } with finite numbers')
|
|
33
|
+
}
|
|
34
|
+
out.position = {
|
|
35
|
+
x: Number(click.position.x),
|
|
36
|
+
y: Number(click.position.y),
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return out
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
throw new Error('click step requires a non-empty CSS selector')
|
|
43
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it } from '@jest/globals'
|
|
2
|
+
import { resolveClickTarget } from './flow-click.js'
|
|
3
|
+
|
|
4
|
+
describe('resolveClickTarget', () => {
|
|
5
|
+
it('accepts a bare selector string', () => {
|
|
6
|
+
expect(resolveClickTarget('[data-overlay]')).toEqual({
|
|
7
|
+
selector: '[data-overlay]',
|
|
8
|
+
})
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('accepts { selector } and trims whitespace', () => {
|
|
12
|
+
expect(resolveClickTarget({ selector: ' [data-ossy-mobile-shell-nav-overlay] ' })).toEqual({
|
|
13
|
+
selector: '[data-ossy-mobile-shell-nav-overlay]',
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('accepts optional { position: { x, y } }', () => {
|
|
18
|
+
expect(resolveClickTarget({
|
|
19
|
+
selector: '[data-ossy-mobile-shell-nav-overlay]',
|
|
20
|
+
position: { x: 360, y: 200 },
|
|
21
|
+
})).toEqual({
|
|
22
|
+
selector: '[data-ossy-mobile-shell-nav-overlay]',
|
|
23
|
+
position: { x: 360, y: 200 },
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('rejects empty or invalid values', () => {
|
|
28
|
+
expect(() => resolveClickTarget('')).toThrow(/non-empty CSS selector/)
|
|
29
|
+
expect(() => resolveClickTarget(' ')).toThrow(/non-empty CSS selector/)
|
|
30
|
+
expect(() => resolveClickTarget(null)).toThrow(/non-empty CSS selector/)
|
|
31
|
+
expect(() => resolveClickTarget({ selector: '' })).toThrow(/non-empty CSS selector/)
|
|
32
|
+
expect(() => resolveClickTarget([])).toThrow(/non-empty CSS selector/)
|
|
33
|
+
expect(() => resolveClickTarget({
|
|
34
|
+
selector: '[data-overlay]',
|
|
35
|
+
position: { x: 'left' },
|
|
36
|
+
})).toThrow(/position requires/)
|
|
37
|
+
expect(() => resolveClickTarget({
|
|
38
|
+
selector: '[data-overlay]',
|
|
39
|
+
position: 10,
|
|
40
|
+
})).toThrow(/position requires/)
|
|
41
|
+
})
|
|
42
|
+
})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flow-run context helpers — `$email`-style placeholders for declarative steps.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Escape a value for use inside a double-quoted CSS attribute selector.
|
|
7
|
+
* @param {unknown} value
|
|
8
|
+
* @returns {string}
|
|
9
|
+
*/
|
|
10
|
+
export function escapeCssAttrValue (value) {
|
|
11
|
+
return String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolve a whole-string `$token` placeholder from the run context.
|
|
16
|
+
* Non-strings and strings that do not start with `$` are returned as-is.
|
|
17
|
+
*
|
|
18
|
+
* @param {unknown} value
|
|
19
|
+
* @param {{ fields?: Record<string, unknown>, [key: string]: unknown }} context
|
|
20
|
+
*/
|
|
21
|
+
export function resolveContextValue (value, context) {
|
|
22
|
+
if (typeof value !== 'string' || !value.startsWith('$')) return value
|
|
23
|
+
const key = value.slice(1)
|
|
24
|
+
return context[key] ?? context.fields?.[key]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Replace `$token` placeholders embedded in a string (e.g. selectors).
|
|
29
|
+
* Unknown tokens are left unchanged.
|
|
30
|
+
*
|
|
31
|
+
* @param {unknown} template
|
|
32
|
+
* @param {{ fields?: Record<string, unknown>, [key: string]: unknown }} context
|
|
33
|
+
* @param {{ escape?: (value: unknown) => string }} [options]
|
|
34
|
+
* @returns {unknown}
|
|
35
|
+
*/
|
|
36
|
+
export function interpolateContextString (template, context, options = {}) {
|
|
37
|
+
if (typeof template !== 'string' || !template.includes('$')) return template
|
|
38
|
+
const escape = typeof options.escape === 'function' ? options.escape : (value) => String(value)
|
|
39
|
+
return template.replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (match, key) => {
|
|
40
|
+
const value = context[key] ?? context.fields?.[key]
|
|
41
|
+
if (value == null) return match
|
|
42
|
+
return escape(value)
|
|
43
|
+
})
|
|
44
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it } from '@jest/globals'
|
|
2
|
+
import {
|
|
3
|
+
escapeCssAttrValue,
|
|
4
|
+
interpolateContextString,
|
|
5
|
+
resolveContextValue,
|
|
6
|
+
} from './flow-context.js'
|
|
7
|
+
|
|
8
|
+
describe('flow-context', () => {
|
|
9
|
+
const context = {
|
|
10
|
+
email: 'new@example.com',
|
|
11
|
+
fields: { firstName: 'Ada', email: 'field@example.com' },
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
it('resolves whole-string $placeholders from context then fields', () => {
|
|
15
|
+
expect(resolveContextValue('$email', context)).toBe('new@example.com')
|
|
16
|
+
expect(resolveContextValue('$firstName', context)).toBe('Ada')
|
|
17
|
+
expect(resolveContextValue('literal', context)).toBe('literal')
|
|
18
|
+
expect(resolveContextValue('$missing', context)).toBeUndefined()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('interpolates embedded $placeholders in selectors', () => {
|
|
22
|
+
expect(
|
|
23
|
+
interpolateContextString('[data-user-email="$email"]', context),
|
|
24
|
+
).toBe('[data-user-email="new@example.com"]')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('escapes quotes and backslashes when an escape fn is provided', () => {
|
|
28
|
+
const tricky = {
|
|
29
|
+
email: 'a"b\\c@example.com',
|
|
30
|
+
fields: {},
|
|
31
|
+
}
|
|
32
|
+
expect(
|
|
33
|
+
interpolateContextString('[data-user-email="$email"]', tricky, {
|
|
34
|
+
escape: escapeCssAttrValue,
|
|
35
|
+
}),
|
|
36
|
+
).toBe('[data-user-email="a\\"b\\\\c@example.com"]')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('leaves unknown embedded tokens unchanged', () => {
|
|
40
|
+
expect(
|
|
41
|
+
interpolateContextString('[data-x="$missing"]', context),
|
|
42
|
+
).toBe('[data-x="$missing"]')
|
|
43
|
+
})
|
|
44
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize a declarative flow `press` step into a Playwright keyboard key.
|
|
3
|
+
*
|
|
4
|
+
* @param {unknown} press
|
|
5
|
+
* @returns {string}
|
|
6
|
+
*/
|
|
7
|
+
export function resolvePressKey (press) {
|
|
8
|
+
if (typeof press === 'string') {
|
|
9
|
+
const key = press.trim()
|
|
10
|
+
if (!key) {
|
|
11
|
+
throw new Error('press step requires a non-empty key string (e.g. "Escape")')
|
|
12
|
+
}
|
|
13
|
+
return key
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (press != null && typeof press === 'object' && !Array.isArray(press)) {
|
|
17
|
+
const key = typeof press.key === 'string' ? press.key.trim() : ''
|
|
18
|
+
if (!key) {
|
|
19
|
+
throw new Error('press step requires a non-empty key string (e.g. "Escape")')
|
|
20
|
+
}
|
|
21
|
+
return key
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
throw new Error('press step requires a non-empty key string (e.g. "Escape")')
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, it } from '@jest/globals'
|
|
2
|
+
import { resolvePressKey } from './flow-press.js'
|
|
3
|
+
|
|
4
|
+
describe('resolvePressKey', () => {
|
|
5
|
+
it('accepts a bare key string', () => {
|
|
6
|
+
expect(resolvePressKey('Escape')).toBe('Escape')
|
|
7
|
+
expect(resolvePressKey('Enter')).toBe('Enter')
|
|
8
|
+
expect(resolvePressKey('Tab')).toBe('Tab')
|
|
9
|
+
expect(resolvePressKey('Shift+Tab')).toBe('Shift+Tab')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('accepts { key } and trims whitespace', () => {
|
|
13
|
+
expect(resolvePressKey({ key: ' Escape ' })).toBe('Escape')
|
|
14
|
+
expect(resolvePressKey({ key: ' Shift+Tab ' })).toBe('Shift+Tab')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('rejects empty or invalid values', () => {
|
|
18
|
+
expect(() => resolvePressKey('')).toThrow(/non-empty key/)
|
|
19
|
+
expect(() => resolvePressKey(' ')).toThrow(/non-empty key/)
|
|
20
|
+
expect(() => resolvePressKey(null)).toThrow(/non-empty key/)
|
|
21
|
+
expect(() => resolvePressKey({ key: '' })).toThrow(/non-empty key/)
|
|
22
|
+
expect(() => resolvePressKey({ key: 27 })).toThrow(/non-empty key/)
|
|
23
|
+
expect(() => resolvePressKey([])).toThrow(/non-empty key/)
|
|
24
|
+
})
|
|
25
|
+
})
|
package/src/test/flow-runner.js
CHANGED
|
@@ -14,6 +14,23 @@ import { faker } from '@faker-js/faker'
|
|
|
14
14
|
import { Router } from '@ossy/router'
|
|
15
15
|
import { Schema } from '@ossy/schema'
|
|
16
16
|
import { test, expect } from '@playwright/test'
|
|
17
|
+
import {
|
|
18
|
+
escapeCssAttrValue,
|
|
19
|
+
interpolateContextString,
|
|
20
|
+
resolveContextValue,
|
|
21
|
+
} from './flow-context.js'
|
|
22
|
+
import { resolveClickTarget } from './flow-click.js'
|
|
23
|
+
import { resolvePressKey } from './flow-press.js'
|
|
24
|
+
import { resolveViewportSize } from './flow-viewport.js'
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
escapeCssAttrValue,
|
|
28
|
+
interpolateContextString,
|
|
29
|
+
resolveContextValue,
|
|
30
|
+
} from './flow-context.js'
|
|
31
|
+
export { resolveClickTarget } from './flow-click.js'
|
|
32
|
+
export { resolvePressKey } from './flow-press.js'
|
|
33
|
+
export { resolveViewportSize } from './flow-viewport.js'
|
|
17
34
|
|
|
18
35
|
const FLOW_PATTERN = /\.flow\.(mjs|cjs|js)$/
|
|
19
36
|
|
|
@@ -110,13 +127,6 @@ function resolveActionService (action, fallback) {
|
|
|
110
127
|
return fallback
|
|
111
128
|
}
|
|
112
129
|
|
|
113
|
-
/** Resolve `$token`-style placeholders from the run context. */
|
|
114
|
-
function resolveContextValue (value, context) {
|
|
115
|
-
if (typeof value !== 'string' || !value.startsWith('$')) return value
|
|
116
|
-
const key = value.slice(1)
|
|
117
|
-
return context[key] ?? context.fields?.[key]
|
|
118
|
-
}
|
|
119
|
-
|
|
120
130
|
function resolveContextObject (obj, context) {
|
|
121
131
|
if (!obj || typeof obj !== 'object') return obj
|
|
122
132
|
const out = {}
|
|
@@ -197,6 +207,31 @@ export async function runFlow (flow, options = {}) {
|
|
|
197
207
|
continue
|
|
198
208
|
}
|
|
199
209
|
|
|
210
|
+
if (step.viewport != null) {
|
|
211
|
+
if (!page) throw new Error('viewport step requires a Playwright page')
|
|
212
|
+
await page.setViewportSize(resolveViewportSize(step.viewport))
|
|
213
|
+
continue
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (step.press != null) {
|
|
217
|
+
if (!page) throw new Error('press step requires a Playwright page')
|
|
218
|
+
await page.keyboard.press(resolvePressKey(step.press))
|
|
219
|
+
continue
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (step.click != null) {
|
|
223
|
+
if (!page) throw new Error('click step requires a Playwright page')
|
|
224
|
+
const { selector, position } = resolveClickTarget(step.click)
|
|
225
|
+
const resolvedSelector = interpolateContextString(selector, context, {
|
|
226
|
+
escape: escapeCssAttrValue,
|
|
227
|
+
})
|
|
228
|
+
const locator = page.locator(resolvedSelector).first()
|
|
229
|
+
const clickTimeout = step.timeout ?? 15000
|
|
230
|
+
await locator.waitFor({ state: 'visible', timeout: clickTimeout })
|
|
231
|
+
await locator.click(position ? { position } : undefined)
|
|
232
|
+
continue
|
|
233
|
+
}
|
|
234
|
+
|
|
200
235
|
if (step.form) {
|
|
201
236
|
if (!page) throw new Error('form step requires a Playwright page')
|
|
202
237
|
const formMeta = step.form
|
|
@@ -305,16 +340,37 @@ export async function runFlow (flow, options = {}) {
|
|
|
305
340
|
|
|
306
341
|
if (step.result) {
|
|
307
342
|
if (!page) throw new Error('result step requires a Playwright page')
|
|
308
|
-
const {
|
|
343
|
+
const {
|
|
344
|
+
text,
|
|
345
|
+
url,
|
|
346
|
+
page: pageRef,
|
|
347
|
+
action,
|
|
348
|
+
selector,
|
|
349
|
+
hidden = false,
|
|
350
|
+
timeout = 8000,
|
|
351
|
+
} = step.result
|
|
352
|
+
const assertLocator = async (locator) => {
|
|
353
|
+
if (hidden) {
|
|
354
|
+
await expectFn(locator).toBeHidden({ timeout })
|
|
355
|
+
return
|
|
356
|
+
}
|
|
357
|
+
await expectFn(locator).toBeVisible({ timeout })
|
|
358
|
+
}
|
|
309
359
|
if (action != null) {
|
|
310
360
|
const actionId = resolveActionId(action)
|
|
311
361
|
const service = resolveActionService(action, step.result.service)
|
|
312
|
-
await
|
|
362
|
+
await assertLocator(page.locator(actionSelector(actionId, service)).first())
|
|
313
363
|
}
|
|
314
364
|
if (selector != null) {
|
|
315
|
-
|
|
365
|
+
const resolvedSelector = interpolateContextString(selector, context, {
|
|
366
|
+
escape: escapeCssAttrValue,
|
|
367
|
+
})
|
|
368
|
+
await assertLocator(page.locator(resolvedSelector).first())
|
|
316
369
|
}
|
|
317
370
|
if (text != null) {
|
|
371
|
+
if (hidden) {
|
|
372
|
+
throw new Error('result.text cannot use hidden: true — assert via action or selector')
|
|
373
|
+
}
|
|
318
374
|
const pattern = text instanceof RegExp ? text : new RegExp(text, 'i')
|
|
319
375
|
await expectFn(page.getByText(pattern).first()).toBeVisible({ timeout })
|
|
320
376
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize a declarative flow `viewport` step into Playwright setViewportSize input.
|
|
3
|
+
*
|
|
4
|
+
* @param {unknown} viewport
|
|
5
|
+
* @returns {{ width: number, height: number }}
|
|
6
|
+
*/
|
|
7
|
+
export function resolveViewportSize (viewport) {
|
|
8
|
+
if (viewport == null || typeof viewport !== 'object' || Array.isArray(viewport)) {
|
|
9
|
+
throw new Error('viewport step requires { width, height } with positive numbers')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const width = Number(viewport.width)
|
|
13
|
+
const height = Number(viewport.height)
|
|
14
|
+
|
|
15
|
+
if (!Number.isFinite(width) || width <= 0 || !Number.isFinite(height) || height <= 0) {
|
|
16
|
+
throw new Error('viewport step requires { width, height } with positive numbers')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
width: Math.round(width),
|
|
21
|
+
height: Math.round(height),
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, it } from '@jest/globals'
|
|
2
|
+
import { resolveViewportSize } from './flow-viewport.js'
|
|
3
|
+
|
|
4
|
+
describe('resolveViewportSize', () => {
|
|
5
|
+
it('accepts positive width/height', () => {
|
|
6
|
+
expect(resolveViewportSize({ width: 390, height: 844 })).toEqual({
|
|
7
|
+
width: 390,
|
|
8
|
+
height: 844,
|
|
9
|
+
})
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('rounds fractional dimensions', () => {
|
|
13
|
+
expect(resolveViewportSize({ width: 389.6, height: 843.2 })).toEqual({
|
|
14
|
+
width: 390,
|
|
15
|
+
height: 843,
|
|
16
|
+
})
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('rejects missing or non-positive sizes', () => {
|
|
20
|
+
expect(() => resolveViewportSize(null)).toThrow(/width, height/)
|
|
21
|
+
expect(() => resolveViewportSize({ width: 390 })).toThrow(/width, height/)
|
|
22
|
+
expect(() => resolveViewportSize({ width: 0, height: 844 })).toThrow(/width, height/)
|
|
23
|
+
expect(() => resolveViewportSize({ width: -1, height: 844 })).toThrow(/width, height/)
|
|
24
|
+
expect(() => resolveViewportSize('mobile')).toThrow(/width, height/)
|
|
25
|
+
})
|
|
26
|
+
})
|
package/src/test/test.util.js
CHANGED
|
@@ -173,6 +173,30 @@ export class TestUtil {
|
|
|
173
173
|
return ev.payload.token
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
static async getLatestEmailChangeJwtForSubject(subjectId) {
|
|
177
|
+
const ev = await EventStore.Collection.findOne(
|
|
178
|
+
{
|
|
179
|
+
type: TOKEN_SCHEMA,
|
|
180
|
+
event: 'Created',
|
|
181
|
+
'payload.type': 'EmailChange',
|
|
182
|
+
'payload.subject': subjectId,
|
|
183
|
+
},
|
|
184
|
+
{ sort: { created: -1 } },
|
|
185
|
+
)
|
|
186
|
+
if (!ev?.payload?.token) {
|
|
187
|
+
return Promise.reject(new Error('No EmailChange token found for subject'))
|
|
188
|
+
}
|
|
189
|
+
return ev.payload.token
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
static countEmailChangeTokenEvents() {
|
|
193
|
+
return EventStore.Collection.countDocuments({
|
|
194
|
+
type: TOKEN_SCHEMA,
|
|
195
|
+
event: 'Created',
|
|
196
|
+
'payload.type': 'EmailChange',
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
176
200
|
static GetVerificationToken() {
|
|
177
201
|
const email = `${casual.email}`
|
|
178
202
|
|
|
@@ -192,7 +216,7 @@ export class TestUtil {
|
|
|
192
216
|
|
|
193
217
|
await TestUtil.AssertActionResponse({
|
|
194
218
|
actionId: '@ossy/authentication/actions/sign-up',
|
|
195
|
-
|
|
219
|
+
payload: { email, firstName: 'Test', lastName: 'User' },
|
|
196
220
|
expectedResponseStatus: 200,
|
|
197
221
|
expectedResponseBody: { ok: true },
|
|
198
222
|
})
|