peachsim 0.1.527
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/LICENSE +22 -0
- package/README.md +640 -0
- package/cli/app-config.ts +82 -0
- package/cli/app-fonts.ts +388 -0
- package/cli/app-project.ts +59 -0
- package/cli/app-splash.ts +171 -0
- package/cli/app-state-reset.ts +26 -0
- package/cli/auth.ts +184 -0
- package/cli/bin.ts +59 -0
- package/cli/bridge-diagnostics.ts +226 -0
- package/cli/bridge-flow-runner.ts +2806 -0
- package/cli/browser-backend.ts +44 -0
- package/cli/browser-evals.ts +96 -0
- package/cli/cli-update.ts +261 -0
- package/cli/cloud-client.ts +1530 -0
- package/cli/cloud-dispatch.ts +206 -0
- package/cli/cloud-session.ts +227 -0
- package/cli/command-contract.ts +221 -0
- package/cli/command-registry.ts +304 -0
- package/cli/commands/agent-wrapper.ts +1048 -0
- package/cli/commands/agent.ts +439 -0
- package/cli/commands/app-fonts.ts +99 -0
- package/cli/commands/assert.ts +542 -0
- package/cli/commands/auth.ts +71 -0
- package/cli/commands/box/bundle-plane.ts +71 -0
- package/cli/commands/box/checkout-plane.ts +256 -0
- package/cli/commands/box/plane-share.ts +208 -0
- package/cli/commands/box/rnx-command.ts +60 -0
- package/cli/commands/box/search-command.ts +157 -0
- package/cli/commands/box.ts +925 -0
- package/cli/commands/camera.ts +266 -0
- package/cli/commands/cleanup.ts +169 -0
- package/cli/commands/compat.ts +176 -0
- package/cli/commands/config.ts +34 -0
- package/cli/commands/control.ts +2154 -0
- package/cli/commands/cpu-profile.ts +941 -0
- package/cli/commands/daemon-mac-app.ts +165 -0
- package/cli/commands/daemon.ts +800 -0
- package/cli/commands/debug.ts +751 -0
- package/cli/commands/desktop.ts +39 -0
- package/cli/commands/detect.ts +201 -0
- package/cli/commands/detox.ts +481 -0
- package/cli/commands/device.ts +134 -0
- package/cli/commands/diagnose.ts +589 -0
- package/cli/commands/do-chain.ts +408 -0
- package/cli/commands/electron.ts +94 -0
- package/cli/commands/film.ts +379 -0
- package/cli/commands/flow.ts +1240 -0
- package/cli/commands/inspect/actions.ts +516 -0
- package/cli/commands/inspect/core.ts +2086 -0
- package/cli/commands/inspect/count.ts +17 -0
- package/cli/commands/inspect/describe.ts +119 -0
- package/cli/commands/inspect/env.ts +25 -0
- package/cli/commands/inspect/find.ts +169 -0
- package/cli/commands/inspect/get-layout.ts +53 -0
- package/cli/commands/inspect/keyboard.ts +54 -0
- package/cli/commands/inspect/list.ts +175 -0
- package/cli/commands/inspect/memory.ts +215 -0
- package/cli/commands/inspect/redaction.ts +39 -0
- package/cli/commands/inspect/resolve-target.ts +77 -0
- package/cli/commands/inspect/screens.ts +78 -0
- package/cli/commands/inspect/settle.ts +32 -0
- package/cli/commands/inspect/settling.ts +197 -0
- package/cli/commands/inspect/shared.ts +259 -0
- package/cli/commands/inspect/sleep.ts +15 -0
- package/cli/commands/inspect/tree.ts +23 -0
- package/cli/commands/inspect/url.ts +17 -0
- package/cli/commands/inspect/wait-event.ts +211 -0
- package/cli/commands/inspect/wait-idle.ts +25 -0
- package/cli/commands/inspect/wait-ready.ts +87 -0
- package/cli/commands/inspect/wait-selector.ts +59 -0
- package/cli/commands/inspect/wait.ts +32 -0
- package/cli/commands/inspect.ts +4738 -0
- package/cli/commands/install-cli.ts +275 -0
- package/cli/commands/install-desktop.ts +385 -0
- package/cli/commands/login.ts +357 -0
- package/cli/commands/logout.ts +24 -0
- package/cli/commands/maestro-generate.ts +375 -0
- package/cli/commands/maestro.ts +536 -0
- package/cli/commands/mode.ts +58 -0
- package/cli/commands/no-bridge-hint.ts +80 -0
- package/cli/commands/perf.ts +67 -0
- package/cli/commands/permissions.ts +203 -0
- package/cli/commands/platform.ts +410 -0
- package/cli/commands/preview.ts +103 -0
- package/cli/commands/profile.ts +106 -0
- package/cli/commands/react.ts +353 -0
- package/cli/commands/record.ts +1435 -0
- package/cli/commands/report-issue.ts +305 -0
- package/cli/commands/reset.ts +89 -0
- package/cli/commands/runtime.ts +348 -0
- package/cli/commands/screenshot-command.ts +106 -0
- package/cli/commands/screenshot-layers.ts +144 -0
- package/cli/commands/screenshot-mode.ts +38 -0
- package/cli/commands/screenshot.ts +492 -0
- package/cli/commands/screenshots-capture.ts +608 -0
- package/cli/commands/screenshots.ts +127 -0
- package/cli/commands/serve.ts +139 -0
- package/cli/commands/setup.ts +416 -0
- package/cli/commands/shell-boolean-mode.ts +82 -0
- package/cli/commands/skills.ts +468 -0
- package/cli/commands/slides.ts +357 -0
- package/cli/commands/state.ts +94 -0
- package/cli/commands/storage.ts +58 -0
- package/cli/commands/telemetry.ts +55 -0
- package/cli/commands/test.ts +535 -0
- package/cli/commands/three-mode.ts +771 -0
- package/cli/commands/timeline.ts +121 -0
- package/cli/commands/upgrade.ts +167 -0
- package/cli/commands/upload.ts +1203 -0
- package/cli/commands/version.ts +63 -0
- package/cli/commands/welcome.ts +17 -0
- package/cli/commands/what-happened.ts +328 -0
- package/cli/current-sim.ts +217 -0
- package/cli/desktop-companion.ts +315 -0
- package/cli/drivers/electron.ts +70 -0
- package/cli/drivers/index.ts +20 -0
- package/cli/drivers/playwright-provisioning.ts +184 -0
- package/cli/drivers/playwright-sim-host.ts +537 -0
- package/cli/drivers/playwright.ts +209 -0
- package/cli/drivers/registry.ts +65 -0
- package/cli/drivers/types.ts +102 -0
- package/cli/flow-export.ts +55 -0
- package/cli/flow-file.ts +142 -0
- package/cli/flow-live-status.ts +126 -0
- package/cli/flow-session.ts +140 -0
- package/cli/help-core.ts +143 -0
- package/cli/help.ts +44 -0
- package/cli/hidden-runtime-alias.ts +19 -0
- package/cli/hints.ts +214 -0
- package/cli/inspect-notice-state.ts +114 -0
- package/cli/internal-child.ts +176 -0
- package/cli/maestro-js.ts +323 -0
- package/cli/main.ts +704 -0
- package/cli/open-url.ts +8 -0
- package/cli/outbound-endpoints.ts +830 -0
- package/cli/parent-pid.ts +204 -0
- package/cli/parse-args.ts +189 -0
- package/cli/privacy.ts +53 -0
- package/cli/prompt.ts +56 -0
- package/cli/recording-access.ts +108 -0
- package/cli/registry.ts +2 -0
- package/cli/resolve-assets.ts +63 -0
- package/cli/rnx-in-process.ts +98 -0
- package/cli/run-registry.ts +226 -0
- package/cli/run-rnx-core.ts +148 -0
- package/cli/run-rnx.ts +137 -0
- package/cli/runtime-notes.ts +66 -0
- package/cli/runtime-summary.ts +25 -0
- package/cli/secure-file.ts +31 -0
- package/cli/self-invocation.ts +58 -0
- package/cli/send-to-box.ts +109 -0
- package/cli/setup-repository.ts +191 -0
- package/cli/shell-init.ts +98 -0
- package/cli/standalone.ts +4 -0
- package/cli/telemetry.ts +84 -0
- package/cli/ui.ts +68 -0
- package/cli/ws-bridge.ts +1020 -0
- package/detox/attempt-events.cjs +23 -0
- package/detox/colors.ts +54 -0
- package/detox/config-loader.ts +135 -0
- package/detox/element-types.ts +48 -0
- package/detox/expectations.ts +534 -0
- package/detox/gestures.ts +501 -0
- package/detox/index.ts +2694 -0
- package/detox/jest-preset.cjs +29 -0
- package/detox/jest-setup-after-env.cjs +22 -0
- package/detox/matchers.ts +30 -0
- package/detox/motion-change.cjs +109 -0
- package/detox/motion-change.d.cts +35 -0
- package/detox/navigation-lifecycle.ts +290 -0
- package/detox/navigation.ts +43 -0
- package/detox/proof-frame.cjs +114 -0
- package/detox/record-block-execution.cjs +78 -0
- package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
- package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
- package/detox/screenshots/color-test-rest-norngh.png +0 -0
- package/detox/screenshots/theme-blue-toggle.png +0 -0
- package/detox/screenshots/theme-blue.png +0 -0
- package/detox/screenshots/theme-red-toggle.png +0 -0
- package/detox/screenshots/theme-red.png +0 -0
- package/dist-lib/agent-daemon-client.cjs +502 -0
- package/dist-lib/agent-events.cjs +48 -0
- package/dist-lib/agent-identity.cjs +53 -0
- package/dist-lib/agent-sessions.cjs +759 -0
- package/dist-lib/attached-projects.cjs +526 -0
- package/dist-lib/auth/shared-session.cjs +208 -0
- package/dist-lib/backend-origin.cjs +88 -0
- package/dist-lib/beta.cjs +41 -0
- package/dist-lib/beta.mjs +13 -0
- package/dist-lib/bridge-constants.cjs +161 -0
- package/dist-lib/bridge-contract-input.cjs +451 -0
- package/dist-lib/bridge-contract-input.mjs +414 -0
- package/dist-lib/bridge-contract.cjs +130 -0
- package/dist-lib/bridge-contract.mjs +94 -0
- package/dist-lib/capture-contract.cjs +220 -0
- package/dist-lib/capture-contract.mjs +190 -0
- package/dist-lib/cli-constants.cjs +32 -0
- package/dist-lib/cloud-contract.cjs +194 -0
- package/dist-lib/cloud-contract.mjs +145 -0
- package/dist-lib/cloud.cjs +670 -0
- package/dist-lib/cloud.mjs +639 -0
- package/dist-lib/config.cjs +96 -0
- package/dist-lib/detox/index.cjs +3789 -0
- package/dist-lib/detox/jest-preset.cjs +29 -0
- package/dist-lib/dev-bundle-resolution.cjs +290 -0
- package/dist-lib/home-paths.cjs +638 -0
- package/dist-lib/host/bridge-host.cjs +10903 -0
- package/dist-lib/host/fetch-proxy-handler.cjs +421 -0
- package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
- package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
- package/dist-lib/host/replacement-module-handler.cjs +469 -0
- package/dist-lib/host/websocket-proxy.cjs +298 -0
- package/dist-lib/index.cjs +4268 -0
- package/dist-lib/jump-to-source-babel.cjs +93 -0
- package/dist-lib/jump-to-source-native.cjs +112 -0
- package/dist-lib/menu.cjs +646 -0
- package/dist-lib/menu.mjs +604 -0
- package/dist-lib/metro-fingerprint-registry.cjs +186 -0
- package/dist-lib/metro-fingerprint-registry.mjs +156 -0
- package/dist-lib/metro-production-bundle.cjs +329 -0
- package/dist-lib/metro-production-bundle.mjs +295 -0
- package/dist-lib/metro.cjs +904 -0
- package/dist-lib/profiles.cjs +246 -0
- package/dist-lib/public-brand.cjs +39 -0
- package/dist-lib/react-native-host-modules.cjs +51 -0
- package/dist-lib/react-native-host-modules.mjs +25 -0
- package/dist-lib/render-mode.cjs +54 -0
- package/dist-lib/scripts/dev-server-scanner.cjs +1232 -0
- package/dist-lib/sdk.cjs +2433 -0
- package/dist-lib/sdk.mjs +2336 -0
- package/dist-lib/skills.cjs +39132 -0
- package/dist-lib/swift-submit.cjs +92 -0
- package/dist-lib/swift-submit.mjs +65 -0
- package/dist-lib/swift.cjs +1646 -0
- package/dist-lib/vite.cjs +1591 -0
- package/package.json +290 -0
- package/scripts/dev-server-scanner.ts +1291 -0
- package/scripts/optional-demo-registry.ts +113 -0
- package/skills/contrast/SKILL.md +165 -0
- package/skills/rnx-debug/SKILL.md +433 -0
- package/skills/rnx-perf/SKILL.md +399 -0
- package/skills/rnx-setup/SKILL.md +172 -0
- package/skills/rnx-test/SKILL.md +321 -0
- package/skills/rnx-visual/SKILL.md +126 -0
- package/src/agent-daemon-client.ts +410 -0
- package/src/agent-events.ts +71 -0
- package/src/agent-identity.ts +23 -0
- package/src/agent-prompt.ts +71 -0
- package/src/agent-sessions.ts +580 -0
- package/src/app-url.ts +39 -0
- package/src/attached-projects.ts +539 -0
- package/src/auth/shared-session.ts +235 -0
- package/src/backend-origin.ts +70 -0
- package/src/beta.ts +19 -0
- package/src/bridge-constants.ts +175 -0
- package/src/bridge-contract-input.ts +502 -0
- package/src/bridge-contract.ts +558 -0
- package/src/browser-cache.ts +16 -0
- package/src/capture-contract.ts +587 -0
- package/src/cli-constants.ts +1 -0
- package/src/cli-version.ts +40 -0
- package/src/cloud-contract.ts +274 -0
- package/src/cloud.ts +762 -0
- package/src/codex-client.ts +234 -0
- package/src/config.ts +132 -0
- package/src/connect.ts +101 -0
- package/src/dev-bundle-resolution.ts +346 -0
- package/src/dev-server-open.ts +198 -0
- package/src/disk-cleanup.ts +540 -0
- package/src/flow-timeout-scale.ts +15 -0
- package/src/home-paths.ts +806 -0
- package/src/host/agent-host.ts +666 -0
- package/src/host/bridge-host.ts +2788 -0
- package/src/host/fetch-proxy-handler.ts +449 -0
- package/src/host/fetch-proxy-overrides.ts +51 -0
- package/src/host/open-url.ts +89 -0
- package/src/host/plane-host.ts +203 -0
- package/src/host/replacement-module-handler.ts +158 -0
- package/src/host/websocket-proxy.ts +315 -0
- package/src/index.ts +71 -0
- package/src/jump-to-source-babel.ts +135 -0
- package/src/jump-to-source-native.ts +102 -0
- package/src/menu.ts +777 -0
- package/src/metro-fingerprint-registry.ts +302 -0
- package/src/metro-plugin.ts +341 -0
- package/src/metro-production-bundle.ts +471 -0
- package/src/native-dev-bundle-url.ts +118 -0
- package/src/native-seam-manifest.ts +341 -0
- package/src/playwright-host-log.ts +16 -0
- package/src/plist.ts +27 -0
- package/src/poll-until.ts +123 -0
- package/src/profiles.ts +179 -0
- package/src/public-brand.ts +9 -0
- package/src/react-native-host-modules.ts +26 -0
- package/src/render-mode.ts +27 -0
- package/src/rolldown-plugin-swift.ts +240 -0
- package/src/runtime-assets.ts +233 -0
- package/src/runtime-delivery.ts +54 -0
- package/src/screenshots/compose.ts +422 -0
- package/src/screenshots/frame-compose.ts +514 -0
- package/src/screenshots/orchestrate.ts +256 -0
- package/src/screenshots/org-deck.ts +91 -0
- package/src/screenshots/registry.ts +58 -0
- package/src/screenshots/schema.ts +364 -0
- package/src/sdk.ts +15 -0
- package/src/settings-bundle.ts +418 -0
- package/src/shell-bridge-identity.ts +15 -0
- package/src/sim-client.ts +697 -0
- package/src/skills/builtin/a11y-review.ts +132 -0
- package/src/skills/builtin/compat-check.ts +71 -0
- package/src/skills/builtin/maestro-test.ts +121 -0
- package/src/skills/builtin/perf-profile.ts +90 -0
- package/src/skills/builtin/screenshot-all.ts +52 -0
- package/src/skills/builtin/visual-diff.ts +100 -0
- package/src/skills/registry.ts +107 -0
- package/src/skills/types.ts +41 -0
- package/src/swift-submit.ts +185 -0
- package/src/swift.ts +7 -0
- package/src/team-machine-prompt.ts +73 -0
- package/src/vite-plugin-one.ts +264 -0
- package/src/vite-plugin-swift.ts +620 -0
- package/src/vite-plugin.ts +1946 -0
- package/src/worklets-babel.ts +250 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// jest preset for running existing detox suites against sootsim.
|
|
2
|
+
//
|
|
3
|
+
// users drop `preset: 'rnxsim/detox'` into their jest config
|
|
4
|
+
// (or `rnx-detox.config.cjs`) and their `import { by, device, element,
|
|
5
|
+
// expect, waitFor } from 'detox'` calls resolve to the sootsim driver.
|
|
6
|
+
|
|
7
|
+
const path = require('path')
|
|
8
|
+
|
|
9
|
+
// resolve the published driver entry so the mapping works from any cwd.
|
|
10
|
+
// `require.resolve('sootsim/detox')` honours the subpath export in the
|
|
11
|
+
// sootsim package.json.
|
|
12
|
+
const DRIVER_ENTRY = require.resolve('rnxsim/detox')
|
|
13
|
+
const PACKAGE_ROOT = path.dirname(require.resolve('rnxsim/package.json'))
|
|
14
|
+
|
|
15
|
+
/** @type {import('@jest/types').Config.InitialOptions} */
|
|
16
|
+
module.exports = {
|
|
17
|
+
testTimeout: 180000,
|
|
18
|
+
maxWorkers: 1,
|
|
19
|
+
watchman: false,
|
|
20
|
+
verbose: true,
|
|
21
|
+
testEnvironment: 'node',
|
|
22
|
+
setupFilesAfterEnv: [path.join(PACKAGE_ROOT, 'detox', 'jest-setup-after-env.cjs')],
|
|
23
|
+
transform: {
|
|
24
|
+
'^.+\\.[jt]sx?$': ['@sucrase/jest-plugin', { jsxRuntime: 'automatic' }],
|
|
25
|
+
},
|
|
26
|
+
moduleNameMapper: {
|
|
27
|
+
'^detox$': DRIVER_ENTRY,
|
|
28
|
+
},
|
|
29
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// closes the driver's browser when a test file finishes.
|
|
2
|
+
//
|
|
3
|
+
// the driver launches chrome lazily on first use and tracks it with the shared
|
|
4
|
+
// reaper, but that reaper's safety net is a `process.on('exit')` hook and jest
|
|
5
|
+
// hands every test file a sandboxed COPY of `process`. a handler registered
|
|
6
|
+
// from inside a test therefore never fires on real termination, so under jest
|
|
7
|
+
// the reaper is a no-op and every suite that launched a browser strands its
|
|
8
|
+
// whole chrome tree. one orphan per test file adds up fast on a full run.
|
|
9
|
+
//
|
|
10
|
+
// `cleanup()` is the teardown that does run, and it both closes the browser and
|
|
11
|
+
// force-reaps its tree. registering it here means no suite has to remember an
|
|
12
|
+
// afterAll of its own.
|
|
13
|
+
|
|
14
|
+
// records which test bodies actually ran, so a downstream reader can tell a
|
|
15
|
+
// setup failure from a behavior failure. no-op unless the run asks for it.
|
|
16
|
+
require('./record-block-execution.cjs')
|
|
17
|
+
|
|
18
|
+
const { cleanup } = require('rnxsim/detox')
|
|
19
|
+
|
|
20
|
+
afterAll(async () => {
|
|
21
|
+
await cleanup()
|
|
22
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// detox-compatible matchers: by.id(), by.text(), by.label(), by.type()
|
|
2
|
+
// these produce matcher descriptors consumed by element() and expect()
|
|
3
|
+
|
|
4
|
+
export interface Matcher {
|
|
5
|
+
type: 'id' | 'text' | 'label' | 'type' | 'role'
|
|
6
|
+
value: string
|
|
7
|
+
index?: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const by = {
|
|
11
|
+
id(testID: string): Matcher {
|
|
12
|
+
return { type: 'id', value: testID }
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
text(text: string): Matcher {
|
|
16
|
+
return { type: 'text', value: text }
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
label(accessibilityLabel: string): Matcher {
|
|
20
|
+
return { type: 'label', value: accessibilityLabel }
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
type(nativeType: string): Matcher {
|
|
24
|
+
return { type: 'type', value: nativeType }
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
role(accessibilityRole: string): Matcher {
|
|
28
|
+
return { type: 'role', value: accessibilityRole }
|
|
29
|
+
},
|
|
30
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// native recordings and SootSim live frames must use this exact definition.
|
|
2
|
+
// retained static Android crops measured as high as 0.0331% after the channel
|
|
3
|
+
// filter. the smallest corrected component transition with visible motion was
|
|
4
|
+
// 0.1748%, so a 0.1% floor separates every measured static and moving frame.
|
|
5
|
+
const MOTION_CHANGE_DEFINITION = Object.freeze({
|
|
6
|
+
channelThreshold: 24,
|
|
7
|
+
minimumChangePercent: 0.1,
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
function measureMotionChange(input) {
|
|
11
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
12
|
+
throw new Error('motion change measurement requires an input object')
|
|
13
|
+
}
|
|
14
|
+
if (input.installGlobal === true) {
|
|
15
|
+
globalThis.__sootsimMeasureMotionChange = measureMotionChange
|
|
16
|
+
return null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const definition = input.definition
|
|
20
|
+
if (
|
|
21
|
+
!definition ||
|
|
22
|
+
typeof definition !== 'object' ||
|
|
23
|
+
!Number.isInteger(definition.channelThreshold) ||
|
|
24
|
+
definition.channelThreshold < 0 ||
|
|
25
|
+
definition.channelThreshold > 255 ||
|
|
26
|
+
!Number.isFinite(definition.minimumChangePercent) ||
|
|
27
|
+
definition.minimumChangePercent <= 0 ||
|
|
28
|
+
definition.minimumChangePercent >= 100
|
|
29
|
+
) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
'motion change definition needs a channel threshold from 0 to 255 and a minimum greater than 0% and less than 100%',
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const label =
|
|
36
|
+
typeof input.label === 'string' && input.label.length > 0
|
|
37
|
+
? input.label
|
|
38
|
+
: 'motion change'
|
|
39
|
+
let changePixels = input.changePixels
|
|
40
|
+
let totalPixels = input.totalPixels
|
|
41
|
+
if (input.baseline !== undefined || input.candidate !== undefined) {
|
|
42
|
+
const baseline = input.baseline
|
|
43
|
+
const candidate = input.candidate
|
|
44
|
+
const width = input.width
|
|
45
|
+
const height = input.height
|
|
46
|
+
if (
|
|
47
|
+
!baseline ||
|
|
48
|
+
!candidate ||
|
|
49
|
+
typeof baseline.length !== 'number' ||
|
|
50
|
+
typeof candidate.length !== 'number' ||
|
|
51
|
+
!Number.isInteger(width) ||
|
|
52
|
+
!Number.isInteger(height) ||
|
|
53
|
+
width <= 0 ||
|
|
54
|
+
height <= 0
|
|
55
|
+
) {
|
|
56
|
+
throw new Error(`${label} needs two RGBA pixel arrays and positive dimensions`)
|
|
57
|
+
}
|
|
58
|
+
totalPixels = width * height
|
|
59
|
+
const expectedBytes = totalPixels * 4
|
|
60
|
+
if (baseline.length !== expectedBytes || candidate.length !== expectedBytes) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`${label} pixel data has ${baseline.length}/${candidate.length} bytes; expected ${expectedBytes}`,
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
changePixels = 0
|
|
66
|
+
for (let offset = 0; offset < expectedBytes; offset += 4) {
|
|
67
|
+
if (
|
|
68
|
+
Math.abs(baseline[offset] - candidate[offset]) > definition.channelThreshold ||
|
|
69
|
+
Math.abs(baseline[offset + 1] - candidate[offset + 1]) >
|
|
70
|
+
definition.channelThreshold ||
|
|
71
|
+
Math.abs(baseline[offset + 2] - candidate[offset + 2]) >
|
|
72
|
+
definition.channelThreshold ||
|
|
73
|
+
Math.abs(baseline[offset + 3] - candidate[offset + 3]) >
|
|
74
|
+
definition.channelThreshold
|
|
75
|
+
) {
|
|
76
|
+
changePixels++
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!Number.isInteger(totalPixels) || totalPixels <= 0) {
|
|
82
|
+
throw new Error(`${label} has no positive measured region area`)
|
|
83
|
+
}
|
|
84
|
+
if (!Number.isInteger(changePixels) || changePixels < 0 || changePixels > totalPixels) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`${label} changed pixel count ${changePixels} is outside its ${totalPixels}px measured region`,
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
const changePercent = (changePixels / totalPixels) * 100
|
|
90
|
+
return {
|
|
91
|
+
changePercent,
|
|
92
|
+
changePixels,
|
|
93
|
+
passedMinimum: changePercent >= definition.minimumChangePercent,
|
|
94
|
+
totalPixels,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function measureProofMotionChange(input) {
|
|
99
|
+
return measureMotionChange({
|
|
100
|
+
...input,
|
|
101
|
+
definition: MOTION_CHANGE_DEFINITION,
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = {
|
|
106
|
+
MOTION_CHANGE_DEFINITION,
|
|
107
|
+
measureMotionChange,
|
|
108
|
+
measureProofMotionChange,
|
|
109
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type MotionChangeDefinition = {
|
|
2
|
+
channelThreshold: number
|
|
3
|
+
minimumChangePercent: number
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
type MotionChangePixels = ArrayLike<number>
|
|
7
|
+
|
|
8
|
+
export type MotionChangeInput = {
|
|
9
|
+
baseline?: MotionChangePixels
|
|
10
|
+
candidate?: MotionChangePixels
|
|
11
|
+
changePixels?: number
|
|
12
|
+
definition: MotionChangeDefinition
|
|
13
|
+
height?: number
|
|
14
|
+
installGlobal?: boolean
|
|
15
|
+
label?: string
|
|
16
|
+
totalPixels?: number
|
|
17
|
+
width?: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type MotionChangeMeasurement = {
|
|
21
|
+
changePercent: number
|
|
22
|
+
changePixels: number
|
|
23
|
+
passedMinimum: boolean
|
|
24
|
+
totalPixels: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const MOTION_CHANGE_DEFINITION: Readonly<MotionChangeDefinition>
|
|
28
|
+
|
|
29
|
+
export function measureMotionChange(
|
|
30
|
+
input: MotionChangeInput,
|
|
31
|
+
): MotionChangeMeasurement | null
|
|
32
|
+
|
|
33
|
+
export function measureProofMotionChange(
|
|
34
|
+
input: Omit<MotionChangeInput, 'definition' | 'installGlobal'>,
|
|
35
|
+
): MotionChangeMeasurement
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { writeAttemptEvent } from './attempt-events.cjs'
|
|
2
|
+
import type { Page, Request, Response } from 'playwright'
|
|
3
|
+
|
|
4
|
+
export type SootsimNavigationIdentity = {
|
|
5
|
+
browserId: number
|
|
6
|
+
contextId: number
|
|
7
|
+
operation: 'launchApp' | 'openURL'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type SootsimPageState = {
|
|
11
|
+
bodyText: string
|
|
12
|
+
bridges: {
|
|
13
|
+
sootsim: boolean
|
|
14
|
+
test: boolean
|
|
15
|
+
}
|
|
16
|
+
globals: string[]
|
|
17
|
+
readyState: string
|
|
18
|
+
title: string
|
|
19
|
+
url: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type ActiveNavigation = SootsimNavigationIdentity & {
|
|
23
|
+
navigationUrl: string
|
|
24
|
+
navigationId: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type NavigationMilestone =
|
|
28
|
+
| 'document-request'
|
|
29
|
+
| 'document-request-failed'
|
|
30
|
+
| 'document-request-finished'
|
|
31
|
+
| 'document-response'
|
|
32
|
+
| 'domcontentloaded'
|
|
33
|
+
| 'error'
|
|
34
|
+
| 'load'
|
|
35
|
+
| 'start'
|
|
36
|
+
|
|
37
|
+
type NavigationDiagnostic = {
|
|
38
|
+
browserId: number
|
|
39
|
+
capturedAt: string
|
|
40
|
+
contextId: number
|
|
41
|
+
errorText?: string
|
|
42
|
+
failurePhase?: 'bridge' | 'navigation'
|
|
43
|
+
headersReceived?: boolean
|
|
44
|
+
method?: string
|
|
45
|
+
milestone: NavigationMilestone
|
|
46
|
+
navigationUrl: string
|
|
47
|
+
navigationId: number
|
|
48
|
+
operation: SootsimNavigationIdentity['operation']
|
|
49
|
+
pageState?: SootsimPageState | { evaluateError: string }
|
|
50
|
+
status?: number
|
|
51
|
+
target: string
|
|
52
|
+
type: 'rnx-navigation'
|
|
53
|
+
url: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type PageDiagnostic =
|
|
57
|
+
| NavigationDiagnostic
|
|
58
|
+
| {
|
|
59
|
+
capturedAt: string
|
|
60
|
+
level: string
|
|
61
|
+
message: string
|
|
62
|
+
type: 'page-console'
|
|
63
|
+
}
|
|
64
|
+
| {
|
|
65
|
+
capturedAt: string
|
|
66
|
+
message: string
|
|
67
|
+
type: 'page-error'
|
|
68
|
+
}
|
|
69
|
+
| {
|
|
70
|
+
capturedAt: string
|
|
71
|
+
errorText: string
|
|
72
|
+
method: string
|
|
73
|
+
type: 'subresource-request-failed'
|
|
74
|
+
url: string
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const DIAGNOSTIC_LIMIT = 40
|
|
78
|
+
const pageDiagnostics = new WeakMap<Page, PageDiagnostic[]>()
|
|
79
|
+
const activeNavigations = new WeakMap<Page, ActiveNavigation>()
|
|
80
|
+
const lastNavigations = new WeakMap<Page, ActiveNavigation>()
|
|
81
|
+
let nextNavigationIdentity = 0
|
|
82
|
+
|
|
83
|
+
function recordPageDiagnostic(page: Page, diagnostic: PageDiagnostic, persist = false) {
|
|
84
|
+
const entries = pageDiagnostics.get(page)
|
|
85
|
+
if (!entries) return
|
|
86
|
+
entries.push(diagnostic)
|
|
87
|
+
if (entries.length > DIAGNOSTIC_LIMIT) entries.shift()
|
|
88
|
+
if (persist) writeAttemptEvent(diagnostic)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isTopLevelDocumentRequest(page: Page, request: Request) {
|
|
92
|
+
return (
|
|
93
|
+
request.isNavigationRequest() &&
|
|
94
|
+
request.resourceType() === 'document' &&
|
|
95
|
+
request.frame() === page.mainFrame()
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function recordNavigationMilestoneFor(
|
|
100
|
+
page: Page,
|
|
101
|
+
active: ActiveNavigation,
|
|
102
|
+
milestone: NavigationMilestone,
|
|
103
|
+
details: Partial<
|
|
104
|
+
Pick<
|
|
105
|
+
NavigationDiagnostic,
|
|
106
|
+
| 'errorText'
|
|
107
|
+
| 'failurePhase'
|
|
108
|
+
| 'headersReceived'
|
|
109
|
+
| 'method'
|
|
110
|
+
| 'pageState'
|
|
111
|
+
| 'status'
|
|
112
|
+
| 'url'
|
|
113
|
+
>
|
|
114
|
+
> = {},
|
|
115
|
+
) {
|
|
116
|
+
recordPageDiagnostic(
|
|
117
|
+
page,
|
|
118
|
+
{
|
|
119
|
+
...active,
|
|
120
|
+
...details,
|
|
121
|
+
capturedAt: new Date().toISOString(),
|
|
122
|
+
milestone,
|
|
123
|
+
target: process.env.CONFORMANCE_PROOF_TARGET || 'sootsim',
|
|
124
|
+
type: 'rnx-navigation',
|
|
125
|
+
url: details.url ?? active.navigationUrl,
|
|
126
|
+
},
|
|
127
|
+
true,
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function recordNavigationMilestone(
|
|
132
|
+
page: Page,
|
|
133
|
+
milestone: NavigationMilestone,
|
|
134
|
+
details: Parameters<typeof recordNavigationMilestoneFor>[3] = {},
|
|
135
|
+
) {
|
|
136
|
+
const active = activeNavigations.get(page)
|
|
137
|
+
if (active) recordNavigationMilestoneFor(page, active, milestone, details)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function attachPageDiagnostics(page: Page) {
|
|
141
|
+
if (pageDiagnostics.has(page)) return
|
|
142
|
+
pageDiagnostics.set(page, [])
|
|
143
|
+
page.on('console', (message) => {
|
|
144
|
+
recordPageDiagnostic(page, {
|
|
145
|
+
capturedAt: new Date().toISOString(),
|
|
146
|
+
level: message.type(),
|
|
147
|
+
message: message.text(),
|
|
148
|
+
type: 'page-console',
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
page.on('pageerror', (error) => {
|
|
152
|
+
recordPageDiagnostic(page, {
|
|
153
|
+
capturedAt: new Date().toISOString(),
|
|
154
|
+
message: error.message,
|
|
155
|
+
type: 'page-error',
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
page.on('request', (request) => {
|
|
159
|
+
if (!isTopLevelDocumentRequest(page, request)) return
|
|
160
|
+
recordNavigationMilestone(page, 'document-request', {
|
|
161
|
+
method: request.method(),
|
|
162
|
+
url: request.url(),
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
page.on('response', (response) => {
|
|
166
|
+
const request = response.request()
|
|
167
|
+
if (!isTopLevelDocumentRequest(page, request)) return
|
|
168
|
+
recordNavigationMilestone(page, 'document-response', {
|
|
169
|
+
headersReceived: true,
|
|
170
|
+
status: response.status(),
|
|
171
|
+
url: request.url(),
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
page.on('requestfinished', (request) => {
|
|
175
|
+
if (!isTopLevelDocumentRequest(page, request)) return
|
|
176
|
+
recordNavigationMilestone(page, 'document-request-finished', { url: request.url() })
|
|
177
|
+
})
|
|
178
|
+
page.on('requestfailed', (request) => {
|
|
179
|
+
const failure = request.failure()
|
|
180
|
+
const errorText = failure?.errorText ?? ''
|
|
181
|
+
if (isTopLevelDocumentRequest(page, request)) {
|
|
182
|
+
recordNavigationMilestone(page, 'document-request-failed', {
|
|
183
|
+
errorText,
|
|
184
|
+
url: request.url(),
|
|
185
|
+
})
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
recordPageDiagnostic(page, {
|
|
189
|
+
capturedAt: new Date().toISOString(),
|
|
190
|
+
errorText,
|
|
191
|
+
method: request.method(),
|
|
192
|
+
type: 'subresource-request-failed',
|
|
193
|
+
url: request.url(),
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
page.on('domcontentloaded', () => {
|
|
197
|
+
recordNavigationMilestone(page, 'domcontentloaded')
|
|
198
|
+
})
|
|
199
|
+
page.on('load', () => {
|
|
200
|
+
recordNavigationMilestone(page, 'load')
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function readSootsimPageState(
|
|
205
|
+
page: Page,
|
|
206
|
+
): Promise<SootsimPageState | { evaluateError: string }> {
|
|
207
|
+
try {
|
|
208
|
+
return await page.evaluate(() => ({
|
|
209
|
+
bodyText: document.body?.innerText?.slice(0, 500) ?? '',
|
|
210
|
+
bridges: {
|
|
211
|
+
sootsim: Boolean(window.SootSim),
|
|
212
|
+
test: Boolean(window.__sootsimTest),
|
|
213
|
+
},
|
|
214
|
+
globals: Object.keys(window)
|
|
215
|
+
.filter((key) => key.startsWith('__sootsim') || key === 'SootSim')
|
|
216
|
+
.sort(),
|
|
217
|
+
readyState: document.readyState,
|
|
218
|
+
title: document.title,
|
|
219
|
+
url: window.location.href,
|
|
220
|
+
}))
|
|
221
|
+
} catch (error) {
|
|
222
|
+
return {
|
|
223
|
+
evaluateError: error instanceof Error ? error.message : String(error),
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export async function describeSootsimBridgeFailure(
|
|
229
|
+
page: Page,
|
|
230
|
+
cause: unknown,
|
|
231
|
+
summary = 'timed out waiting for sootsim test bridge',
|
|
232
|
+
pageState?: SootsimPageState | { evaluateError: string },
|
|
233
|
+
) {
|
|
234
|
+
const recordBridgeFailure = pageState === undefined
|
|
235
|
+
const state = pageState ?? (await readSootsimPageState(page))
|
|
236
|
+
const navigation = lastNavigations.get(page)
|
|
237
|
+
if (recordBridgeFailure && navigation) {
|
|
238
|
+
recordNavigationMilestoneFor(page, navigation, 'error', {
|
|
239
|
+
errorText: cause instanceof Error ? cause.message : String(cause),
|
|
240
|
+
failurePhase: 'bridge',
|
|
241
|
+
pageState: state,
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
return new Error(
|
|
245
|
+
[
|
|
246
|
+
summary,
|
|
247
|
+
`cause: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
248
|
+
`page: ${JSON.stringify(state)}`,
|
|
249
|
+
`recent page diagnostics:\n${JSON.stringify(getSootsimNavigationDiagnostics(page))}`,
|
|
250
|
+
].join('\n'),
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export async function navigateSootsimPage(
|
|
255
|
+
page: Page,
|
|
256
|
+
url: string,
|
|
257
|
+
identity: SootsimNavigationIdentity,
|
|
258
|
+
): Promise<Response | null> {
|
|
259
|
+
attachPageDiagnostics(page)
|
|
260
|
+
const navigation = {
|
|
261
|
+
...identity,
|
|
262
|
+
navigationUrl: url,
|
|
263
|
+
navigationId: ++nextNavigationIdentity,
|
|
264
|
+
}
|
|
265
|
+
activeNavigations.set(page, navigation)
|
|
266
|
+
lastNavigations.set(page, navigation)
|
|
267
|
+
recordNavigationMilestone(page, 'start')
|
|
268
|
+
try {
|
|
269
|
+
return await page.goto(url, { waitUntil: 'load', timeout: 30000 })
|
|
270
|
+
} catch (error) {
|
|
271
|
+
const pageState = await readSootsimPageState(page)
|
|
272
|
+
recordNavigationMilestone(page, 'error', {
|
|
273
|
+
errorText: error instanceof Error ? error.message : String(error),
|
|
274
|
+
failurePhase: 'navigation',
|
|
275
|
+
pageState,
|
|
276
|
+
})
|
|
277
|
+
throw await describeSootsimBridgeFailure(
|
|
278
|
+
page,
|
|
279
|
+
error,
|
|
280
|
+
'sootsim navigation failed',
|
|
281
|
+
pageState,
|
|
282
|
+
)
|
|
283
|
+
} finally {
|
|
284
|
+
activeNavigations.delete(page)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function getSootsimNavigationDiagnostics(page: Page): readonly PageDiagnostic[] {
|
|
289
|
+
return [...(pageDiagnostics.get(page) ?? [])]
|
|
290
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// sootsim-compatible replacement for kitchen-sink's e2e/utils/navigation.ts
|
|
2
|
+
// instead of navigating through the UI, we load the test case via URL query param
|
|
3
|
+
|
|
4
|
+
import { by, device, element, waitFor } from './index'
|
|
5
|
+
|
|
6
|
+
const BASE_URL = process.env.RNX_URL || 'http://localhost:5173'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* navigate to a kitchen-sink test case in sootsim
|
|
10
|
+
* loads via URL query param instead of tapping through the UI
|
|
11
|
+
*/
|
|
12
|
+
export async function navigateToTestCase(
|
|
13
|
+
testCaseName: string,
|
|
14
|
+
waitForElementId?: string,
|
|
15
|
+
) {
|
|
16
|
+
const page = device.getPage()
|
|
17
|
+
|
|
18
|
+
// navigate to the test case directly via URL, preserving any params
|
|
19
|
+
// already on BASE_URL (device=, colorScheme=, etc.) — the conformance
|
|
20
|
+
// runner sets these via RNX_URL and dropping them silently rendered
|
|
21
|
+
// pro max at iphone 16 dimensions.
|
|
22
|
+
const target = new URL(BASE_URL)
|
|
23
|
+
target.searchParams.set('test', testCaseName)
|
|
24
|
+
if (target.pathname === '' || target.pathname === '/') target.pathname = '/'
|
|
25
|
+
const url = target.toString()
|
|
26
|
+
await page.goto(url, { waitUntil: 'load', timeout: 30000 })
|
|
27
|
+
|
|
28
|
+
// wait for sootsim engine to initialize
|
|
29
|
+
await page.waitForFunction(
|
|
30
|
+
'window.__sootsimTest && window.__sootsimRoot && window.__sootsimRoot.children.length > 0',
|
|
31
|
+
{ timeout: 30000 },
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
// let rendering settle
|
|
35
|
+
await page.waitForTimeout(500)
|
|
36
|
+
|
|
37
|
+
// wait for the target element if specified
|
|
38
|
+
if (waitForElementId) {
|
|
39
|
+
await waitFor(element(by.id(waitForElementId)))
|
|
40
|
+
.toExist()
|
|
41
|
+
.withTimeout(10000)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// proof captures reserve the top and bottom of a device frame for platform
|
|
2
|
+
// chrome. those pixels can be fully opaque while the app surface is blank or
|
|
3
|
+
// still fading in, so evidence density is measured only in the content band.
|
|
4
|
+
const PROOF_CONTENT_DEFINITION = Object.freeze({
|
|
5
|
+
channelThreshold: 24,
|
|
6
|
+
contentBandInsetFraction: 0.08,
|
|
7
|
+
minimumContentPercent: 0.005,
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
// conformance compares these captures against native ios baselines, so a frame
|
|
11
|
+
// caught mid-animation reads as a real pixel diff. three consecutive identical
|
|
12
|
+
// frames is the evidence that the screen actually stopped moving.
|
|
13
|
+
const REQUIRED_IDENTICAL_PROOF_FRAMES = 3
|
|
14
|
+
|
|
15
|
+
// a flow's `takeScreenshot` is a developer artifact, not pixel evidence. one
|
|
16
|
+
// settled capture is what it wants: re-capturing flashes the screen on every
|
|
17
|
+
// attempt, and a screen with a blinking caret never produces two identical
|
|
18
|
+
// frames at all, so the step fails on a page that is perfectly fine.
|
|
19
|
+
const REQUIRED_IDENTICAL_FLOW_FRAMES = 1
|
|
20
|
+
|
|
21
|
+
function framesIdentical(a, b) {
|
|
22
|
+
if (!a || !b || a.length !== b.length) return false
|
|
23
|
+
return a.equals(b)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function measureProofFrameContent(input) {
|
|
27
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
28
|
+
throw new Error('proof content measurement requires an input object')
|
|
29
|
+
}
|
|
30
|
+
const { data, width, height } = input
|
|
31
|
+
if (
|
|
32
|
+
!data ||
|
|
33
|
+
typeof data.length !== 'number' ||
|
|
34
|
+
!Number.isInteger(width) ||
|
|
35
|
+
!Number.isInteger(height) ||
|
|
36
|
+
width <= 0 ||
|
|
37
|
+
height <= 0 ||
|
|
38
|
+
data.length !== width * height * 4
|
|
39
|
+
) {
|
|
40
|
+
throw new Error('proof content measurement needs RGBA pixels and dimensions')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const yStart = Math.floor(height * PROOF_CONTENT_DEFINITION.contentBandInsetFraction)
|
|
44
|
+
const yEnd = Math.ceil(height * (1 - PROOF_CONTENT_DEFINITION.contentBandInsetFraction))
|
|
45
|
+
const bucketCounts = new Uint32Array(32 * 32 * 32)
|
|
46
|
+
let totalPixels = 0
|
|
47
|
+
for (let y = yStart; y < yEnd; y++) {
|
|
48
|
+
for (let x = 0; x < width; x++) {
|
|
49
|
+
const offset = (y * width + x) * 4
|
|
50
|
+
const bucket =
|
|
51
|
+
((data[offset] >> 3) << 10) |
|
|
52
|
+
((data[offset + 1] >> 3) << 5) |
|
|
53
|
+
(data[offset + 2] >> 3)
|
|
54
|
+
bucketCounts[bucket]++
|
|
55
|
+
totalPixels++
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let dominantBucket = 0
|
|
60
|
+
for (let index = 1; index < bucketCounts.length; index++) {
|
|
61
|
+
if (bucketCounts[index] > bucketCounts[dominantBucket]) dominantBucket = index
|
|
62
|
+
}
|
|
63
|
+
const dominant = {
|
|
64
|
+
red: (((dominantBucket >> 10) & 31) << 3) + 4,
|
|
65
|
+
green: (((dominantBucket >> 5) & 31) << 3) + 4,
|
|
66
|
+
blue: ((dominantBucket & 31) << 3) + 4,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let contentPixels = 0
|
|
70
|
+
for (let y = yStart; y < yEnd; y++) {
|
|
71
|
+
for (let x = 0; x < width; x++) {
|
|
72
|
+
const offset = (y * width + x) * 4
|
|
73
|
+
if (
|
|
74
|
+
Math.max(
|
|
75
|
+
Math.abs(data[offset] - dominant.red),
|
|
76
|
+
Math.abs(data[offset + 1] - dominant.green),
|
|
77
|
+
Math.abs(data[offset + 2] - dominant.blue),
|
|
78
|
+
) >= PROOF_CONTENT_DEFINITION.channelThreshold
|
|
79
|
+
) {
|
|
80
|
+
contentPixels++
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const contentPercent = (contentPixels / totalPixels) * 100
|
|
86
|
+
return {
|
|
87
|
+
contentPercent,
|
|
88
|
+
contentPixels,
|
|
89
|
+
dominant,
|
|
90
|
+
passedMinimum: contentPercent >= PROOF_CONTENT_DEFINITION.minimumContentPercent,
|
|
91
|
+
totalPixels,
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function settleDecision(previousPixels, currentFrame) {
|
|
96
|
+
if (!currentFrame) return { decision: 'continue', content: null }
|
|
97
|
+
const content = measureProofFrameContent(currentFrame)
|
|
98
|
+
if (!content.passedMinimum) {
|
|
99
|
+
return { decision: 'insufficient-content', content }
|
|
100
|
+
}
|
|
101
|
+
if (previousPixels && framesIdentical(previousPixels, currentFrame.data)) {
|
|
102
|
+
return { decision: 'identical', content }
|
|
103
|
+
}
|
|
104
|
+
return { decision: 'changed', content }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = {
|
|
108
|
+
PROOF_CONTENT_DEFINITION,
|
|
109
|
+
REQUIRED_IDENTICAL_PROOF_FRAMES,
|
|
110
|
+
REQUIRED_IDENTICAL_FLOW_FRAMES,
|
|
111
|
+
framesIdentical,
|
|
112
|
+
measureProofFrameContent,
|
|
113
|
+
settleDecision,
|
|
114
|
+
}
|