@tantawowa/hosanna-tools 3.9.0 → 3.10.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 +100 -18
- package/dist/build-info.json +3 -3
- package/dist/cli.js +32 -140
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -8
- package/dist/index.js +2 -12
- package/dist/index.js.map +1 -1
- package/dist/run/run-controller.js +10 -1
- package/dist/run/run-controller.js.map +1 -1
- package/dist/support-tools/build-config-resolver.d.ts +1 -0
- package/dist/support-tools/build-config-resolver.js +30 -5
- package/dist/support-tools/build-config-resolver.js.map +1 -1
- package/dist/support-tools/command-debugger.d.ts +1 -0
- package/dist/support-tools/command-debugger.js +6 -0
- package/dist/support-tools/command-debugger.js.map +1 -1
- package/dist/support-tools/dev-runner.d.ts +1 -0
- package/dist/support-tools/dev-runner.js +3 -3
- package/dist/support-tools/dev-runner.js.map +1 -1
- package/dist/support-tools/mcp-server/{cli-test-recorder.d.ts → rasp-capture.d.ts} +7 -7
- package/dist/support-tools/mcp-server/{cli-test-recorder.js → rasp-capture.js} +58 -58
- package/dist/support-tools/mcp-server/rasp-capture.js.map +1 -0
- package/dist/support-tools/mcp-server/rasp-exporter.d.ts +12 -12
- package/dist/support-tools/mcp-server/rasp-exporter.js +23 -23
- package/dist/support-tools/mcp-server/rasp-exporter.js.map +1 -1
- package/dist/support-tools/mcp-server/rasp-input-capture.d.ts +60 -0
- package/dist/support-tools/mcp-server/rasp-input-capture.js +100 -0
- package/dist/support-tools/mcp-server/rasp-input-capture.js.map +1 -0
- package/dist/support-tools/mcp-server/roku-device-client.d.ts +9 -0
- package/dist/support-tools/mcp-server/roku-device-client.js +17 -2
- package/dist/support-tools/mcp-server/roku-device-client.js.map +1 -1
- package/dist/support-tools/mcp-server/roku-log-client.d.ts +2 -0
- package/dist/support-tools/mcp-server/roku-log-client.js +16 -2
- package/dist/support-tools/mcp-server/roku-log-client.js.map +1 -1
- package/dist/support-tools/mcp-server/tools.js +30 -679
- package/dist/support-tools/mcp-server/tools.js.map +1 -1
- package/dist/support-tools/mcp-server/types.d.ts +4 -1
- package/dist/support-tools/mcp-server/types.js +3 -1
- package/dist/support-tools/mcp-server/types.js.map +1 -1
- package/dist/testing/client.d.ts +215 -0
- package/dist/testing/client.js +1029 -0
- package/dist/testing/client.js.map +1 -0
- package/dist/testing/config.d.ts +5 -0
- package/dist/testing/config.js +55 -0
- package/dist/testing/config.js.map +1 -0
- package/dist/testing/global-setup.d.ts +10 -0
- package/dist/testing/global-setup.js +39 -0
- package/dist/testing/global-setup.js.map +1 -0
- package/dist/testing/index.d.ts +7 -0
- package/dist/testing/index.js +26 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/launcher.d.ts +23 -0
- package/dist/testing/launcher.js +675 -0
- package/dist/testing/launcher.js.map +1 -0
- package/dist/testing/target.d.ts +16 -0
- package/dist/testing/target.js +40 -0
- package/dist/testing/target.js.map +1 -0
- package/dist/testing/types.d.ts +266 -0
- package/dist/testing/types.js +3 -0
- package/dist/testing/types.js.map +1 -0
- package/dist/testing/vitest-fixture.d.mts +28 -0
- package/dist/testing/vitest-fixture.mjs +87 -0
- package/dist/testing/vitest-fixture.mjs.map +1 -0
- package/dist/testing/vitest-reporter.d.mts +35 -0
- package/dist/testing/vitest-reporter.mjs +176 -0
- package/dist/testing/vitest-reporter.mjs.map +1 -0
- package/dist/testing/vitest.d.ts +1 -0
- package/dist/testing/vitest.js +8 -0
- package/dist/testing/vitest.js.map +1 -0
- package/dist/vite.d.ts +7 -0
- package/dist/vite.js +58 -0
- package/dist/vite.js.map +1 -1
- package/package.json +28 -3
- package/dist/support-tools/mcp-server/cli-test-recorder.js.map +0 -1
- package/dist/support-tools/mcp-server/cli-test-runner.d.ts +0 -37
- package/dist/support-tools/mcp-server/cli-test-runner.js +0 -332
- package/dist/support-tools/mcp-server/cli-test-runner.js.map +0 -1
- package/dist/support-tools/mcp-server/test-reporter.d.ts +0 -19
- package/dist/support-tools/mcp-server/test-reporter.js +0 -248
- package/dist/support-tools/mcp-server/test-reporter.js.map +0 -1
- package/dist/support-tools/mcp-server/test-runner.d.ts +0 -222
- package/dist/support-tools/mcp-server/test-runner.js +0 -686
- package/dist/support-tools/mcp-server/test-runner.js.map +0 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Documentation
|
|
4
4
|
|
|
5
|
-
- **[MCP debugger
|
|
5
|
+
- **[MCP debugger](src/support-tools/mcp-server/README.md)** — `hst mcp:start`, debugger tools, commands by use case, examples, and RASP certification capture.
|
|
6
6
|
- **[docs/README.md](docs/README.md)** — index of docs in this repo (MCP + source-map note).
|
|
7
7
|
- **hosanna-ui** (separate repo): `docs/README.md` for agentic debugging and links back to this README.
|
|
8
8
|
|
|
@@ -94,7 +94,7 @@ Common build-script replacements:
|
|
|
94
94
|
| `npx hst secrets:check` | `await secrets.check({ cwd: process.cwd() })` |
|
|
95
95
|
| `npx hst env check` | `await env.check({ cwd: process.cwd() })` |
|
|
96
96
|
|
|
97
|
-
Long-running APIs such as `dev.run`, `debugger.start`, `mcp.start`,
|
|
97
|
+
Long-running APIs such as `dev.run`, `debugger.start`, `mcp.start`, and RASP-owned certification capture workflows keep the same operational behavior as the corresponding CLI commands: they start services, attach to debuggers, or wait for user/session activity. `mcp.start` and `dev.run` are wired for programmatic no-exit behavior through the top-level API.
|
|
98
98
|
|
|
99
99
|
# Getting started
|
|
100
100
|
|
|
@@ -386,9 +386,95 @@ silently break an install.
|
|
|
386
386
|
|
|
387
387
|
**Cursor and MCP:** Cursor does not magically attach to a terminal `hst mcp:start`. Add Hosanna as a Command MCP server (project **`.cursor/mcp.json`** or **Cursor Settings → MCP → Add Custom MCP**) so Cursor spawns **`hosanna-mcp`** over stdio. Start the command debugger/app explicitly with `hst run --platform web --target web`, `hst run --platform ios --target web`, `hst run --platform android-tv --target web`, or `hst run --platform roku --target device`.
|
|
388
388
|
|
|
389
|
-
###
|
|
390
|
-
|
|
391
|
-
-
|
|
389
|
+
### Deterministic UI testing
|
|
390
|
+
|
|
391
|
+
Applications own regular Vitest suites. Global setup comes from `@tantawowa/hosanna-tools/testing/vitest`; worker fixtures and matchers come from `@tantawowa/hosanna-tools/testing/vitest/fixture`; shared lifecycle, types, and helpers come from `@tantawowa/hosanna-tools/testing`. There is no HST test CLI or recording/replay format.
|
|
392
|
+
|
|
393
|
+
Add `createHosannaReporter(config)` from `@tantawowa/hosanna-tools/testing/vitest/reporter` to the application's Vitest reporters. Every run then writes a self-contained JSON and HTML report under `test-results/<testRunId>/`, updates `test-results/latest.json` and `test-results/latest.html`, and includes every Vitest result plus any named visual proofs captured by the tests.
|
|
394
|
+
|
|
395
|
+
Browser targets can also record the complete owned browser session as WebM:
|
|
396
|
+
|
|
397
|
+
```ts
|
|
398
|
+
defineHosannaTestConfig({
|
|
399
|
+
// ...
|
|
400
|
+
video: true,
|
|
401
|
+
// Or: video: { size: { width: 1280, height: 720 } },
|
|
402
|
+
});
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
The finalized recording is written to
|
|
406
|
+
`test-results/<testRunId>/<platform>-web/run.webm` and embedded in the HTML
|
|
407
|
+
report. Recording is opt-in in the shared tools and ignored for device and
|
|
408
|
+
simulator targets. It records browser video only, without audio. This is a
|
|
409
|
+
review artifact, not a replay format or executable test source.
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
import { defineConfig } from 'vitest/config';
|
|
413
|
+
import { createHosannaReporter } from '@tantawowa/hosanna-tools/testing/vitest/reporter';
|
|
414
|
+
import hosannaTestConfig from './integration/hosanna-test.config';
|
|
415
|
+
|
|
416
|
+
export default defineConfig({
|
|
417
|
+
reporters: ['default', createHosannaReporter(hosannaTestConfig)],
|
|
418
|
+
});
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
Use `hs.proof.capture(...)` at meaningful acceptance moments. A proof contains the target screenshot and the inspected screen/focus/control state used to understand it; hierarchy and scoped logs are optional. Proofs are evidence in the report, not an assertion by themselves:
|
|
422
|
+
|
|
423
|
+
```ts
|
|
424
|
+
const poster = hs.image.inCollectionCell({
|
|
425
|
+
collectionId: 'collectionView',
|
|
426
|
+
rowSettingsKey: 'rows.regular',
|
|
427
|
+
cellIndex: 0,
|
|
428
|
+
imageId: 'poster',
|
|
429
|
+
});
|
|
430
|
+
await poster.waitForLoaded({ requireBitmapSize: true });
|
|
431
|
+
await expect(poster).toBeLoadedImage({ requireBitmapSize: true });
|
|
432
|
+
|
|
433
|
+
await hs.proof.capture('programme-focused', {
|
|
434
|
+
controls: {
|
|
435
|
+
programmeCard: hs.control.byId('programmeCard'),
|
|
436
|
+
},
|
|
437
|
+
images: { poster },
|
|
438
|
+
includeLogs: true,
|
|
439
|
+
});
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
Image readiness requires a non-empty URI, `loadStatus: "ready"`, positive rendered dimensions, and positive decoded bitmap dimensions by default. Explicit `loadWidth`/`loadHeight` assertions remain available for controls that request a fixed decode size; zero is valid when the runtime decodes at the source size. `hs.image.byId(...)`, `hs.image.within(...)`, and semantic `hs.image.inCollectionCell(...)` locators work across browser, Roku, and later native drivers.
|
|
443
|
+
|
|
444
|
+
Screenshots and proofs wait for the command-plane hierarchy and focus to remain stable for 300ms by default, then allow a short platform render-settle window. Consecutive screenshots are also spaced automatically. Override this only for known platform behaviour through the test config's `screenshot` settings or per-capture options; failure diagnostics bypass the stable-frame wait so they can still capture broken states.
|
|
445
|
+
|
|
446
|
+
Custom test ports must be unique. Keep the Vite and browser CDP ports outside the debugger's dynamic app allocation range; the launcher validates this before starting any processes and reports the conflicting range.
|
|
447
|
+
|
|
448
|
+
Keep structural and state assertions as the default correctness checks. Use `hs.screenshot.match(...)` only for an explicit visual regression whose target-specific baseline and tolerance are intentionally maintained. Failure diagnostics and proof artifacts are separated below `test-results/<testRunId>/<platform>-<target>/`.
|
|
449
|
+
|
|
450
|
+
`buildConfig` supplies a non-secret overlay that is merged last for the owned test launch. It may be a target-aware factory, so browser emulation can enable deterministic mocks while a physical Roku keeps its real platform services. The launcher and resolver both reject test overlays for `prod`:
|
|
451
|
+
|
|
452
|
+
```ts
|
|
453
|
+
defineHosannaTestConfig({
|
|
454
|
+
// ...
|
|
455
|
+
environment: 'test',
|
|
456
|
+
buildConfig: ({ platform, target }) => ({
|
|
457
|
+
remoteDebug: { isEnabled: true },
|
|
458
|
+
...(target === 'web'
|
|
459
|
+
? { channelStore: { testMode: { enabled: true } } }
|
|
460
|
+
: {}),
|
|
461
|
+
}),
|
|
462
|
+
});
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
Use `createHosannaTestTarget(config)` with Vitest's standard conditional APIs. Selectors accept one platform or a set, and distinguish browser, simulator, and physical-device targets:
|
|
466
|
+
|
|
467
|
+
```ts
|
|
468
|
+
const target = createHosannaTestTarget(config);
|
|
469
|
+
|
|
470
|
+
test.runIf(target.matches({ platform: 'roku', target: 'device' }))('Roku only', async ({ hs }) => {});
|
|
471
|
+
test.runIf(target.matches({ platform: ['ios', 'android'], target: 'sim' }))('mobile simulators', async ({ hs }) => {});
|
|
472
|
+
test.runIf(target.matches({ target: 'web' }))('all browser flavours', async ({ hs }) => {});
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
Browser Channel Store emulation supports per-test `success`, `cancelled`, and `failed` outcomes when `channelStore.testMode.enabled` is present in the test overlay. Set it from a named precondition with `hs.mocks.channelStore.setPurchaseOutcome(...)`. This API refuses physical-device targets. Roku purchase tests use Roku Pay [billing testing](https://developer.roku.com/dev/docs/billing-testing), a linked [Roku test user](https://developer.roku.com/dev/docs/test-users), and real Channel Store/OS purchase screens; keep them opt-in and serial.
|
|
476
|
+
|
|
477
|
+
For Roku device suites, the launcher builds and deploys before the test launch, then relaunches through ECP with the exact test run ID and a debugger address reachable from the device. Configure the device IP and developer password in the application test config/environment. `HS_TEST_DEBUG_HOST` overrides automatic LAN address selection when needed. Physical Roku input, held keys, deep links, screenshots, and logs use their device transports; the committed Vitest source remains the same API used by browser tests.
|
|
392
478
|
|
|
393
479
|
## Usage Examples
|
|
394
480
|
|
|
@@ -445,16 +531,15 @@ hst env prepare-gitignore # Update .gitignore
|
|
|
445
531
|
hst debugger:start # Start debug proxy (required)
|
|
446
532
|
hst mcp:start # Start MCP server for Cursor/Claude
|
|
447
533
|
|
|
448
|
-
# UI
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
hst test:ui --json --report-dir ./reports # CI mode with reports
|
|
534
|
+
# Application-owned UI tests
|
|
535
|
+
npm run test:ui
|
|
536
|
+
npm run test:ui -- auth/login.test.ts
|
|
537
|
+
HS_TEST_PLATFORM=roku HS_TEST_TARGET=device npm run test:ui
|
|
453
538
|
```
|
|
454
539
|
|
|
455
|
-
## MCP Debugging & UI
|
|
540
|
+
## MCP Debugging & Deterministic UI Tests
|
|
456
541
|
|
|
457
|
-
Hosanna Tools includes
|
|
542
|
+
Hosanna Tools includes an MCP server for interactive AI-assisted debugging. Deterministic UI tests run in an application's Vitest process and use the separate `@tantawowa/hosanna-tools/testing` export; MCP is only an authoring aid.
|
|
458
543
|
|
|
459
544
|
### Cursor Settings → MCP (Hosanna debugger)
|
|
460
545
|
|
|
@@ -507,7 +592,7 @@ After saving, enable the server in the MCP list. **`hst mcp:start`** is still us
|
|
|
507
592
|
### Quick Start
|
|
508
593
|
|
|
509
594
|
```bash
|
|
510
|
-
# 1. Start the debug proxy
|
|
595
|
+
# 1. Start the debug proxy for an interactive MCP debugging session
|
|
511
596
|
hst debugger:start
|
|
512
597
|
|
|
513
598
|
# 2. Start your Hosanna app with remote debugging enabled
|
|
@@ -515,11 +600,8 @@ hst debugger:start
|
|
|
515
600
|
# 3. For AI debugging (Cursor, Claude Code):
|
|
516
601
|
# Add MCP server (see "Cursor Settings → MCP" above) or hst mcp:start in a terminal
|
|
517
602
|
|
|
518
|
-
# 4.
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
# 5. For UI test replay:
|
|
522
|
-
hst test:ui
|
|
603
|
+
# 4. Run committed deterministic tests from the application repository:
|
|
604
|
+
npm run test:ui
|
|
523
605
|
```
|
|
524
606
|
|
|
525
607
|
### See also
|
package/dist/build-info.json
CHANGED
package/dist/cli.js
CHANGED
|
@@ -300,7 +300,7 @@ catch (err) {
|
|
|
300
300
|
// Let yargs handle all commands including env
|
|
301
301
|
(0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
|
|
302
302
|
.version(`${buildVersion}\n git: ${buildGitHash}\n built: ${buildDate}`)
|
|
303
|
-
.usage('$0 [command] [options]', `Hosanna Tools CLI\nVersion: ${buildVersion}\nBuild Date: ${buildDate}\n\nCommand Groups:\n compiler:* Compiler lifecycle (install, status, list)\n config hosanna.json management (wizard, show, set)\n env Environment checks and repair (check, fix, prepare-gitignore)\n dev:* Local development execution\n debugger:* Command debugger control\n mcp:* Hosanna MCP server control\n build Cross-platform build orchestration\n run Cross-platform launch orchestration\n native:* Native platform bootstrap and validation\n target:* Run target discovery and inventory\n roku:* Roku deployment, commands & packaging (run, command, package, map-stack)\n device:* Device discovery and run workflows\n generate:* Code and asset generation\n sdk:* Hosanna SDK/framework installation\n build-config:* Build/runtime configuration resolution\n ci:* Continuous integration helpers\n framework:* Framework distribution helpers\n license:* Hosanna framework licensing\n secrets:* Portable .secrets files\n
|
|
303
|
+
.usage('$0 [command] [options]', `Hosanna Tools CLI\nVersion: ${buildVersion}\nBuild Date: ${buildDate}\n\nCommand Groups:\n compiler:* Compiler lifecycle (install, status, list)\n config hosanna.json management (wizard, show, set)\n env Environment checks and repair (check, fix, prepare-gitignore)\n dev:* Local development execution\n debugger:* Command debugger control\n mcp:* Hosanna MCP server control\n build Cross-platform build orchestration\n run Cross-platform launch orchestration\n native:* Native platform bootstrap and validation\n target:* Run target discovery and inventory\n roku:* Roku deployment, commands & packaging (run, command, package, map-stack)\n device:* Device discovery and run workflows\n generate:* Code and asset generation\n sdk:* Hosanna SDK/framework installation\n build-config:* Build/runtime configuration resolution\n ci:* Continuous integration helpers\n framework:* Framework distribution helpers\n license:* Hosanna framework licensing\n secrets:* Portable .secrets files\n rasp:* RASP cert scripts`)
|
|
304
304
|
.option('verbose', {
|
|
305
305
|
type: 'boolean',
|
|
306
306
|
default: false,
|
|
@@ -2056,121 +2056,13 @@ catch (err) {
|
|
|
2056
2056
|
appName: args.appName,
|
|
2057
2057
|
});
|
|
2058
2058
|
})
|
|
2059
|
-
.command('
|
|
2060
|
-
'Requires a running debug proxy (hst debugger:start) and a connected app.', yargs => yargs
|
|
2061
|
-
.positional('recordings', {
|
|
2062
|
-
describe: 'Recording IDs to replay (omit to run all)',
|
|
2063
|
-
type: 'string',
|
|
2064
|
-
array: true,
|
|
2065
|
-
})
|
|
2066
|
-
.option('suite', {
|
|
2067
|
-
type: 'string',
|
|
2068
|
-
describe: 'Path to a test-suite.json file (relative to recordings dir or absolute)',
|
|
2069
|
-
})
|
|
2070
|
-
.option('appId', {
|
|
2071
|
-
type: 'string',
|
|
2072
|
-
describe: 'Target app ID (defaults to the one stored in each recording)',
|
|
2073
|
-
})
|
|
2074
|
-
.option('managementPort', { type: 'number', default: 59150, describe: 'Debug proxy management port' })
|
|
2075
|
-
.option('managementHost', { type: 'string', default: 'localhost', describe: 'Debug proxy host' })
|
|
2076
|
-
.option('extensionPort', { type: 'number', default: 59153, describe: 'Debug proxy extension WebSocket port' })
|
|
2077
|
-
.option('stop-on-failure', { type: 'boolean', default: true, describe: 'Stop each recording on first step failure' })
|
|
2078
|
-
.option('step-delay-ms', { type: 'number', default: 500, describe: 'Delay after each action (ms)' })
|
|
2079
|
-
.option('focus-assert-retry-ms', {
|
|
2080
|
-
type: 'number',
|
|
2081
|
-
default: 300,
|
|
2082
|
-
describe: 'Delay (ms) between focus polls after a mismatch (0 = no polling)',
|
|
2083
|
-
})
|
|
2084
|
-
.option('focus-assert-max-retries', {
|
|
2085
|
-
type: 'number',
|
|
2086
|
-
default: 12,
|
|
2087
|
-
describe: 'Max extra GetFocusedView polls per step (skeleton/load); 0 = single read only',
|
|
2088
|
-
})
|
|
2089
|
-
.option('screenshot-settle-ms', { type: 'number', default: 400, describe: 'Extra delay before screenshot capture (ms)' })
|
|
2090
|
-
.option('compare-screenshots', { type: 'boolean', default: true, describe: 'Enable visual regression comparison' })
|
|
2091
|
-
.option('screenshot-threshold', { type: 'number', default: 0.15, describe: 'Pixel diff tolerance (0-1)' })
|
|
2092
|
-
.option('screenshot-fail-percent', { type: 'number', default: 2.0, describe: 'Max allowed diff % before failure' })
|
|
2093
|
-
.option('roku-ip', { type: 'string', describe: 'Roku device IP (env: HOSANNA_ROKU_DEVICE_IP)' })
|
|
2094
|
-
.option('roku-password', { type: 'string', describe: 'Roku dev password (env: HOSANNA_ROKU_DEV_PASSWORD)' })
|
|
2095
|
-
.option('json', { type: 'boolean', default: false, describe: 'Output results as JSON only' })
|
|
2096
|
-
.option('report-dir', { type: 'string', describe: 'Directory for HTML + JUnit XML reports' })
|
|
2097
|
-
.option('update-baselines', { type: 'boolean', default: false, describe: 'Re-capture baseline screenshots without asserting (replays actions only)' })
|
|
2098
|
-
.option('coverage', { type: 'boolean', default: false, describe: 'Analyze recordings and print navigation coverage (no running app needed)' }), async (args) => {
|
|
2099
|
-
if (args.verbose) {
|
|
2100
|
-
process.env.HOSANNA_VERBOSE = '1';
|
|
2101
|
-
}
|
|
2102
|
-
const { runTestUI } = await Promise.resolve().then(() => __importStar(require('./support-tools/mcp-server/cli-test-runner.js')));
|
|
2103
|
-
await runTestUI({
|
|
2104
|
-
recordings: args.recordings,
|
|
2105
|
-
suite: args.suite,
|
|
2106
|
-
appId: args.appId,
|
|
2107
|
-
managementPort: args.managementPort,
|
|
2108
|
-
managementHost: args.managementHost,
|
|
2109
|
-
extensionPort: args.extensionPort,
|
|
2110
|
-
stopOnFailure: args.stopOnFailure,
|
|
2111
|
-
stepDelayMs: args.stepDelayMs,
|
|
2112
|
-
focusAssertRetryMs: args.focusAssertRetryMs,
|
|
2113
|
-
focusAssertMaxRetries: args.focusAssertMaxRetries,
|
|
2114
|
-
screenshotSettleMs: args.screenshotSettleMs,
|
|
2115
|
-
compareScreenshots: args.compareScreenshots,
|
|
2116
|
-
screenshotThreshold: args.screenshotThreshold,
|
|
2117
|
-
screenshotFailPercent: args.screenshotFailPercent,
|
|
2118
|
-
rokuIp: args.rokuIp,
|
|
2119
|
-
rokuPassword: args.rokuPassword,
|
|
2120
|
-
json: args.json,
|
|
2121
|
-
verbose: args.verbose,
|
|
2122
|
-
reportDir: args.reportDir,
|
|
2123
|
-
updateBaselines: args.updateBaselines,
|
|
2124
|
-
coverage: args.coverage,
|
|
2125
|
-
});
|
|
2126
|
-
})
|
|
2127
|
-
.command('test:record', 'Interactively record a UI test by navigating the app. ' +
|
|
2128
|
-
'Key events from the running app are captured via the debug proxy.', yargs => yargs
|
|
2129
|
-
.option('name', {
|
|
2130
|
-
type: 'string',
|
|
2131
|
-
demandOption: true,
|
|
2132
|
-
describe: 'Name for this test recording',
|
|
2133
|
-
})
|
|
2134
|
-
.option('description', {
|
|
2135
|
-
type: 'string',
|
|
2136
|
-
describe: 'Optional description of what this test covers',
|
|
2137
|
-
})
|
|
2138
|
-
.option('appId', {
|
|
2139
|
-
type: 'string',
|
|
2140
|
-
describe: 'Target app ID (defaults to first connected app)',
|
|
2141
|
-
})
|
|
2142
|
-
.option('managementPort', { type: 'number', default: 59150, describe: 'Debug proxy management port' })
|
|
2143
|
-
.option('managementHost', { type: 'string', default: 'localhost', describe: 'Debug proxy host' })
|
|
2144
|
-
.option('extensionPort', { type: 'number', default: 59153, describe: 'Debug proxy extension WebSocket port' })
|
|
2145
|
-
.option('roku-ip', { type: 'string', describe: 'Roku device IP for screenshots (env: HOSANNA_ROKU_DEVICE_IP)' })
|
|
2146
|
-
.option('roku-password', { type: 'string', describe: 'Roku dev password for screenshots (env: HOSANNA_ROKU_DEV_PASSWORD)' })
|
|
2147
|
-
.option('screenshot-on-select', { type: 'boolean', default: true, describe: 'Auto-capture screenshot on Select key press' })
|
|
2148
|
-
.example('$0 test:record --name "Home screen navigation"', 'Record key presses while navigating')
|
|
2149
|
-
.example('$0 test:record --name "Details test" --roku-ip 192.168.1.10', 'Record with Roku screenshot support'), async (args) => {
|
|
2150
|
-
if (args.verbose) {
|
|
2151
|
-
process.env.HOSANNA_VERBOSE = '1';
|
|
2152
|
-
}
|
|
2153
|
-
const { runTestRecord } = await Promise.resolve().then(() => __importStar(require('./support-tools/mcp-server/cli-test-recorder.js')));
|
|
2154
|
-
await runTestRecord({
|
|
2155
|
-
name: args.name,
|
|
2156
|
-
description: args.description,
|
|
2157
|
-
appId: args.appId,
|
|
2158
|
-
managementPort: args.managementPort,
|
|
2159
|
-
managementHost: args.managementHost,
|
|
2160
|
-
extensionPort: args.extensionPort,
|
|
2161
|
-
rokuIp: args.rokuIp,
|
|
2162
|
-
rokuPassword: args.rokuPassword,
|
|
2163
|
-
screenshotOnSelect: args.screenshotOnSelect,
|
|
2164
|
-
verbose: args.verbose,
|
|
2165
|
-
});
|
|
2166
|
-
})
|
|
2167
|
-
.command('rasp:export <recordingId> <name>', 'Convert a saved test recording into a Roku Automation Scripting Platform (RASP) ' +
|
|
2059
|
+
.command('rasp:export <captureId> <name>', 'Convert a saved RASP input capture into a Roku Automation Scripting Platform (RASP) ' +
|
|
2168
2060
|
'YAML script suitable for Roku certification testing. Output goes to ' +
|
|
2169
2061
|
'rasp-scripts/<name>.rasp in the current project by default.', yargs => yargs
|
|
2170
|
-
.positional('
|
|
2062
|
+
.positional('captureId', {
|
|
2171
2063
|
type: 'string',
|
|
2172
2064
|
demandOption: true,
|
|
2173
|
-
describe: 'ID of the saved
|
|
2065
|
+
describe: 'ID of the saved capture (filename without .json), e.g. "signin_1736888000000"',
|
|
2174
2066
|
})
|
|
2175
2067
|
.positional('name', {
|
|
2176
2068
|
type: 'string',
|
|
@@ -2180,11 +2072,11 @@ catch (err) {
|
|
|
2180
2072
|
.option('out', {
|
|
2181
2073
|
type: 'string',
|
|
2182
2074
|
alias: 'o',
|
|
2183
|
-
describe: 'Override the output path. Default: rasp-scripts/<name>.rasp next to
|
|
2075
|
+
describe: 'Override the output path. Default: rasp-scripts/<name>.rasp next to the capture directory.',
|
|
2184
2076
|
})
|
|
2185
2077
|
.option('channel-name', {
|
|
2186
2078
|
type: 'string',
|
|
2187
|
-
describe: 'Channel name for the launch step (overrides
|
|
2079
|
+
describe: 'Channel name for the launch step (overrides capture.debugContext.channel.name).',
|
|
2188
2080
|
})
|
|
2189
2081
|
.option('channel-mode', {
|
|
2190
2082
|
type: 'string',
|
|
@@ -2242,7 +2134,7 @@ catch (err) {
|
|
|
2242
2134
|
describe: 'Master gate for credential auto-injection. When true (default), the exporter ' +
|
|
2243
2135
|
'watches for OK presses landing on a TextInputView classified as email or password ' +
|
|
2244
2136
|
'(via inputType / secureMode or field id) and injects the matching half of the RFI credential flow. ' +
|
|
2245
|
-
'Use --no-useWithRfi to disable injection entirely (
|
|
2137
|
+
'Use --no-useWithRfi to disable injection entirely (capture flows through verbatim).',
|
|
2246
2138
|
})
|
|
2247
2139
|
.option('login-text', {
|
|
2248
2140
|
type: 'string',
|
|
@@ -2277,8 +2169,8 @@ catch (err) {
|
|
|
2277
2169
|
}
|
|
2278
2170
|
const path = await Promise.resolve().then(() => __importStar(require('path')));
|
|
2279
2171
|
const fs = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
2280
|
-
const {
|
|
2281
|
-
const {
|
|
2172
|
+
const { getRaspCapturesDir } = await Promise.resolve().then(() => __importStar(require('./support-tools/mcp-server/rasp-input-capture.js')));
|
|
2173
|
+
const { exportRaspFromCapture, validateRaspYaml } = await Promise.resolve().then(() => __importStar(require('./support-tools/mcp-server/rasp-exporter.js')));
|
|
2282
2174
|
// Parses a minimal RASP YAML fragment containing only `- press: <key>` and
|
|
2283
2175
|
// `- text: <value>` ops (with `LOGIN` / `PASSWORD` reserved tokens). Comments
|
|
2284
2176
|
// (#…) and blank lines are allowed. Anything else throws.
|
|
@@ -2305,16 +2197,16 @@ catch (err) {
|
|
|
2305
2197
|
}
|
|
2306
2198
|
return ops;
|
|
2307
2199
|
};
|
|
2308
|
-
const
|
|
2200
|
+
const captureId = args.captureId;
|
|
2309
2201
|
const scriptName = args.name.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
2310
|
-
const
|
|
2311
|
-
const
|
|
2312
|
-
let
|
|
2202
|
+
const capturesDir = getRaspCapturesDir();
|
|
2203
|
+
const capturePath = path.join(capturesDir, `${captureId}.json`);
|
|
2204
|
+
let capture;
|
|
2313
2205
|
try {
|
|
2314
|
-
|
|
2206
|
+
capture = JSON.parse(fs.readFileSync(capturePath, 'utf-8'));
|
|
2315
2207
|
}
|
|
2316
2208
|
catch (_c) {
|
|
2317
|
-
console.error(`
|
|
2209
|
+
console.error(`RASP capture "${captureId}" not found in ${capturesDir}.`);
|
|
2318
2210
|
process.exit(1);
|
|
2319
2211
|
}
|
|
2320
2212
|
// Parse repeatable --text-field flags into a map.
|
|
@@ -2332,7 +2224,7 @@ catch (err) {
|
|
|
2332
2224
|
const credentialFlow = credentialFlowPath
|
|
2333
2225
|
? parseCredentialFlowFile(credentialFlowPath)
|
|
2334
2226
|
: undefined;
|
|
2335
|
-
const result =
|
|
2227
|
+
const result = exportRaspFromCapture(capture, {
|
|
2336
2228
|
channelName: args.channelName,
|
|
2337
2229
|
channelMode: args.channelMode,
|
|
2338
2230
|
contentId: args.contentId,
|
|
@@ -2357,7 +2249,7 @@ catch (err) {
|
|
|
2357
2249
|
process.stdout.write(result.yaml);
|
|
2358
2250
|
}
|
|
2359
2251
|
console.error(`✅ Wrote ${outPath}`);
|
|
2360
|
-
console.error(` ${result.stats.
|
|
2252
|
+
console.error(` ${result.stats.capturedSteps} captured steps → ${result.stats.raspOps} RASP ops` +
|
|
2361
2253
|
` (auto-pauses: ${result.stats.autoPausesInserted}, text injections: ${result.stats.textInjections}` +
|
|
2362
2254
|
`, keyboard steps dropped: ${result.stats.keyboardStepsDropped}, verify ops: ${result.stats.verifyOpsEmitted})`);
|
|
2363
2255
|
for (const d of result.rfiDetections) {
|
|
@@ -2380,7 +2272,7 @@ catch (err) {
|
|
|
2380
2272
|
}
|
|
2381
2273
|
})
|
|
2382
2274
|
.command('rasp:capture <name>', 'Record a UI session and export it directly to a RASP script in one step. ' +
|
|
2383
|
-
'Navigate the app, press Ctrl+C to stop — the
|
|
2275
|
+
'Navigate the app, press Ctrl+C to stop — the capture is saved and the RASP script is written automatically.', yargs => yargs
|
|
2384
2276
|
.positional('name', {
|
|
2385
2277
|
type: 'string',
|
|
2386
2278
|
demandOption: true,
|
|
@@ -2465,10 +2357,10 @@ catch (err) {
|
|
|
2465
2357
|
default: false,
|
|
2466
2358
|
describe: 'Print the YAML to stdout after writing.',
|
|
2467
2359
|
})
|
|
2468
|
-
.option('keep-
|
|
2360
|
+
.option('keep-capture', {
|
|
2469
2361
|
type: 'boolean',
|
|
2470
2362
|
default: false,
|
|
2471
|
-
describe: 'Keep the temporary
|
|
2363
|
+
describe: 'Keep the temporary RASP capture JSON/screenshots after the script is written.',
|
|
2472
2364
|
})
|
|
2473
2365
|
.example('$0 rasp:capture login --channel-name "Criterion"', 'Record and export to rasp-scripts/login.rasp')
|
|
2474
2366
|
.example('$0 rasp:capture login --channel-name "Criterion" --login-text "user@example.com" --password-text "MyPass1!"', 'Default credential flow with custom text')
|
|
@@ -2476,13 +2368,13 @@ catch (err) {
|
|
|
2476
2368
|
var _a, _b;
|
|
2477
2369
|
const path = await Promise.resolve().then(() => __importStar(require('path')));
|
|
2478
2370
|
const fs = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
2479
|
-
const {
|
|
2480
|
-
const {
|
|
2481
|
-
const {
|
|
2371
|
+
const { cleanupRaspCaptureArtifacts, runRaspCapture } = await Promise.resolve().then(() => __importStar(require('./support-tools/mcp-server/rasp-capture.js')));
|
|
2372
|
+
const { getRaspCapturesDir } = await Promise.resolve().then(() => __importStar(require('./support-tools/mcp-server/rasp-input-capture.js')));
|
|
2373
|
+
const { exportRaspFromCapture, validateRaspYaml } = await Promise.resolve().then(() => __importStar(require('./support-tools/mcp-server/rasp-exporter.js')));
|
|
2482
2374
|
const scriptName = args.name.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
2483
2375
|
const raspScriptsDir = path.join(process.cwd(), 'rasp-scripts');
|
|
2484
2376
|
const outPath = (_a = args.out) !== null && _a !== void 0 ? _a : path.join(raspScriptsDir, `${scriptName}.rasp`);
|
|
2485
|
-
const
|
|
2377
|
+
const capturesDir = getRaspCapturesDir();
|
|
2486
2378
|
const textFields = {};
|
|
2487
2379
|
const tfArgs = (_b = args['text-field']) !== null && _b !== void 0 ? _b : [];
|
|
2488
2380
|
for (const tf of tfArgs) {
|
|
@@ -2520,7 +2412,7 @@ catch (err) {
|
|
|
2520
2412
|
const credentialFlow = credentialFlowPath
|
|
2521
2413
|
? parseCredentialFlowFile(credentialFlowPath)
|
|
2522
2414
|
: undefined;
|
|
2523
|
-
await
|
|
2415
|
+
await runRaspCapture({
|
|
2524
2416
|
name: args.name,
|
|
2525
2417
|
appId: args.appId,
|
|
2526
2418
|
managementPort: args.managementPort,
|
|
@@ -2528,8 +2420,8 @@ catch (err) {
|
|
|
2528
2420
|
extensionPort: args.extensionPort,
|
|
2529
2421
|
rokuIp: args.rokuIp,
|
|
2530
2422
|
rokuPassword: args.rokuPassword,
|
|
2531
|
-
onSaved: async (
|
|
2532
|
-
const result =
|
|
2423
|
+
onSaved: async (captureId, capture) => {
|
|
2424
|
+
const result = exportRaspFromCapture(capture, {
|
|
2533
2425
|
channelName: args.channelName,
|
|
2534
2426
|
channelMode: args.channelMode,
|
|
2535
2427
|
contentId: args.contentId,
|
|
@@ -2548,7 +2440,7 @@ catch (err) {
|
|
|
2548
2440
|
if (args.print)
|
|
2549
2441
|
process.stdout.write(result.yaml);
|
|
2550
2442
|
console.log(`✅ RASP script written: ${outPath}`);
|
|
2551
|
-
console.log(` ${result.stats.
|
|
2443
|
+
console.log(` ${result.stats.capturedSteps} steps → ${result.stats.raspOps} RASP ops` +
|
|
2552
2444
|
` (pauses: ${result.stats.autoPausesInserted}, text injections: ${result.stats.textInjections})`);
|
|
2553
2445
|
for (const d of result.rfiDetections) {
|
|
2554
2446
|
const icon = d.type === 'login' ? '📧' : '🔑';
|
|
@@ -2567,15 +2459,15 @@ catch (err) {
|
|
|
2567
2459
|
console.error(` Script has validation errors — review before submitting to Roku cert.`);
|
|
2568
2460
|
process.exitCode = 1;
|
|
2569
2461
|
}
|
|
2570
|
-
if (!args.
|
|
2462
|
+
if (!args.keepCapture) {
|
|
2571
2463
|
try {
|
|
2572
|
-
const cleanup =
|
|
2464
|
+
const cleanup = cleanupRaspCaptureArtifacts(capturesDir, captureId);
|
|
2573
2465
|
if (cleanup.jsonDeleted || cleanup.artifactDirDeleted) {
|
|
2574
|
-
console.log(`🧹 Cleaned up temporary
|
|
2466
|
+
console.log(`🧹 Cleaned up temporary capture: ${captureId}`);
|
|
2575
2467
|
}
|
|
2576
2468
|
}
|
|
2577
2469
|
catch (err) {
|
|
2578
|
-
console.warn(`⚠ RASP script was written, but temporary
|
|
2470
|
+
console.warn(`⚠ RASP script was written, but temporary capture cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2579
2471
|
}
|
|
2580
2472
|
}
|
|
2581
2473
|
},
|