@ossy/platform 3.2.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 +3 -3
- package/src/test/flow-click.js +43 -0
- package/src/test/flow-click.spec.js +42 -0
- package/src/test/flow-press.js +25 -0
- package/src/test/flow-press.spec.js +25 -0
- package/src/test/flow-runner.js +52 -3
- package/src/test/flow-viewport.js +23 -0
- package/src/test/flow-viewport.spec.js +26 -0
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",
|
|
@@ -51,7 +51,7 @@
|
|
|
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.
|
|
54
|
+
"@ossy/sdk": "^3.3.0",
|
|
55
55
|
"@ossy/tokens": "^3.0.9",
|
|
56
56
|
"@ossy/users": "^3.2.0",
|
|
57
57
|
"@ossy/workspaces": "^3.2.0",
|
|
@@ -74,5 +74,5 @@
|
|
|
74
74
|
"src",
|
|
75
75
|
"Dockerfile"
|
|
76
76
|
],
|
|
77
|
-
"gitHead": "
|
|
77
|
+
"gitHead": "19e8bae4c1e9df79270218f28c0c9391f4f9871c"
|
|
78
78
|
}
|
|
@@ -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,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
|
@@ -19,12 +19,18 @@ import {
|
|
|
19
19
|
interpolateContextString,
|
|
20
20
|
resolveContextValue,
|
|
21
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'
|
|
22
25
|
|
|
23
26
|
export {
|
|
24
27
|
escapeCssAttrValue,
|
|
25
28
|
interpolateContextString,
|
|
26
29
|
resolveContextValue,
|
|
27
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'
|
|
28
34
|
|
|
29
35
|
const FLOW_PATTERN = /\.flow\.(mjs|cjs|js)$/
|
|
30
36
|
|
|
@@ -201,6 +207,31 @@ export async function runFlow (flow, options = {}) {
|
|
|
201
207
|
continue
|
|
202
208
|
}
|
|
203
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
|
+
|
|
204
235
|
if (step.form) {
|
|
205
236
|
if (!page) throw new Error('form step requires a Playwright page')
|
|
206
237
|
const formMeta = step.form
|
|
@@ -309,19 +340,37 @@ export async function runFlow (flow, options = {}) {
|
|
|
309
340
|
|
|
310
341
|
if (step.result) {
|
|
311
342
|
if (!page) throw new Error('result step requires a Playwright page')
|
|
312
|
-
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
|
+
}
|
|
313
359
|
if (action != null) {
|
|
314
360
|
const actionId = resolveActionId(action)
|
|
315
361
|
const service = resolveActionService(action, step.result.service)
|
|
316
|
-
await
|
|
362
|
+
await assertLocator(page.locator(actionSelector(actionId, service)).first())
|
|
317
363
|
}
|
|
318
364
|
if (selector != null) {
|
|
319
365
|
const resolvedSelector = interpolateContextString(selector, context, {
|
|
320
366
|
escape: escapeCssAttrValue,
|
|
321
367
|
})
|
|
322
|
-
await
|
|
368
|
+
await assertLocator(page.locator(resolvedSelector).first())
|
|
323
369
|
}
|
|
324
370
|
if (text != null) {
|
|
371
|
+
if (hidden) {
|
|
372
|
+
throw new Error('result.text cannot use hidden: true — assert via action or selector')
|
|
373
|
+
}
|
|
325
374
|
const pattern = text instanceof RegExp ? text : new RegExp(text, 'i')
|
|
326
375
|
await expectFn(page.getByText(pattern).first()).toBeVisible({ timeout })
|
|
327
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
|
+
})
|