DIF-015 — Interactive browser recorder for test creation.
Opens a Playwright browser pointed at the project's URL, streams a
live CDP screencast to the frontend via SSE (reusing the existing
emitRunEvent channel), and captures raw user interactions
(clicks, fills, key-presses, navigations) as Playwright actions.
On stop, the captured action list is transformed into a Playwright test body and returned so the caller (routes/tests.js) can persist a Draft test and run it through the rest of the generation pipeline (assertion enhancement, self-healing transform) just like any other AI-generated test.
Exports
startRecording— Launch browser + begin capture.stopRecording— Stop capture; return{ actions, playwrightCode }.getRecording— Inspect an in-flight recording (for abort / status).
Design notes
Capture is done entirely in the page context via a single injected
listener that posts events back to Node through page.exposeBinding.
This is the same approach Playwright's own codegen uses for JavaScript
action recording, minus the DevTools UI — we only need the raw event
stream.
- Source:
Members
(inner, constant) ACCEPTED_DEVICE_NAMES
DIF-015c Gap 5 — set of device names accepted by the recorder. Built
once at module load from the curated DEVICE_PRESETS exported by
config.js (the same list RunRegressionModal mirrors), plus the
empty-string sentinel for "Desktop (default)". Any other value is a
400 from the route layer — we deliberately do NOT accept arbitrary
playwright.devices keys at the recorder boundary because the curated
list is what the UI exposes and what executeTest.js exercises in CI.
- Source:
(inner, constant) INTERACTION_KINDS
Action kinds that represent a real user interaction (as opposed to a
drive-by hover or a passive goto). Used by the __sentriRecord
binding to strip a trailing hover action on the same selector when the
very next action is an interaction — see the block that consumes this
set for the full rationale.
Lives at module scope (matching TIMINGS above) rather than inside the
binding callback so we don't re-allocate a Set on every captured action
event during an active recording session.
- Source:
(inner, constant) RECORDER_SCRIPT
JS source injected into every page frame. It captures pointer/keyboard
events and relays them to Node via the __sentriRecord binding. We
de-duplicate by dispatch target + event type so that a single click
doesn't emit multiple entries when bubbling through the DOM.
selectorGenerator mirrors Playwright-style priority heuristics (DIF-015b):
prefer role-based selectors, then data-testid, then aria-label, then
a short CSS chain.
Disambiguation (DIF-015b follow-up): when the chosen CSS-fallback
selector matches more than one element on the page, append a Playwright
>> nth=N token so replay targets the same element the user clicked.
Without this, three identical button.btn-primary on a page would all
replay against the first match. Role/data-testid/label/text selectors are
NOT disambiguated by index — they're already semantic anchors and adding
nth=N to them would mask a real test smell (multiple identical labels
on the same page is a symptom worth surfacing, not silently fixing).
Shadow DOM and iframes still fall through to the host-document selector
(this PR's scope is naming + nth disambiguation only). Iframe support is
partially handled at the action layer via frameUrl capture — see the
__sentriRecord binding. Full shadow-root traversal is tracked as a
follow-up sub-item under DIF-015b in ROADMAP.md.
Built once at module load — the timing constants come from TIMINGS
(Node-side) and are baked into the script as numeric literals before
addInitScript ships it to the page. This keeps a single source of
truth across the Node boundary; previously the same values lived as
inline magic numbers inside the script.
- Source:
(inner, constant) STEALTH_SCRIPT
DIF-015c Gap 6 — opt-in stealth bootstrap script. Patches the five
fingerprint surfaces real-world if (navigator.webdriver) { block() }
detection scripts check, without pulling in the
puppeteer-extra-plugin-stealth dependency tree (which would add a
cat-and-mouse fingerprint patcher running on every page, plus a
security-review surface for anti-bot logic AGENT.md :123 warns
against bundling without explicit justification).
Applied via context.addInitScript(STEALTH_SCRIPT) ONLY when the
session was launched with stealth: true. Default-mode runs never
call addInitScript with this body so pre-Gap-6 behaviour is
bit-for-bit identical.
Coverage of common headless-detection probes:
navigator.webdriver— headless sets this totrue; we redefine the getter to returnundefined(matches what a real Chrome window returns).navigator.plugins— headless reports an emptyPluginArray; we synthesise a 3-entry array shaped like a real Chrome install (PDF Viewer + Chrome PDF Viewer + Chromium PDF Viewer).navigator.languages— headless reports[]; we force the equivalent of a US-English Chrome install. Operators can still override per-context via the existinglocaledevice option.window.chrome— real Chrome exposes achromeobject with at least aruntimestub; headless leaves it undefined. We synthesise the minimal shape that detection scripts probe.Permissions.prototype.queryfornotifications— real Chrome returns{ state: "prompt" }for an unset permission; headless returns{ state: "denied" }which is a strong tell. We patch the prototype to flip notifications back to"prompt"while leaving every other permission query untouched.
The script is intentionally narrow — it covers the ~90% of detection scripts that read these five surfaces, NOT the long tail of canvas-fingerprint / WebGL-renderer / battery-API patches the upstream plugin chains together. If a target site detects us despite this stealth profile, the right answer is to add a single targeted patch here rather than pull in the full upstream stack.
- Source:
(inner, constant) TIMINGS
Tunable timing constants for the recorder. Centralised so reviewers can see every magic number in one place and so operators can override the server-side ones via environment variables without grepping through the file.
The in-page timings (DBLCLICK_DEFER_MS, HOVER_DWELL_MS, FILL_DEBOUNCE_MS,
DBLCLICK_WINDOW_MS) are inlined into RECORDER_SCRIPT because that
string runs in the browser context where this module's bindings are not
in scope. Keep this block in sync if you change them inside the script
— a mismatch is silent and can produce confusing replay behaviour.
- Source:
(inner, constant) completedSessions :Map.<string, CompletedRecording>
Short-lived cache of recordings torn down by the MAX_RECORDING_MS safety-net
timeout. Entries live for COMPLETED_TTL_MS so a user who clicks
"Stop & Save" moments after the timeout fires can still recover their
captured actions instead of losing them to a 500 error. Scoped to the in-
process recorder so no external store is needed.
Type:
- Map.<string, CompletedRecording>
- Source:
(inner, constant) sessions :Map.<string, RecordingSession>
Type:
- Map.<string, RecordingSession>
- Source:
Methods
(static) actionsToPlaywrightCode(testName, startUrl, actions) → {string}
Convert a list of captured actions into a Playwright test body. The output
is wrapped in the repo-standard test(...) shape so the existing runner
(codeExecutor, codeParsing) treats it like any AI-generated test.
Parameters:
| Name | Type | Description |
|---|---|---|
testName |
string | |
startUrl |
string | |
actions |
Array.<RecordedAction> |
- Source:
Returns:
Playwright source code.
- Type
- string
(static) addAssertionAction(sessionId, action) → {RecordedAction}
Append a manual assertion action to an in-flight recording session. Mirrors Playwright recorder's explicit "Add assertion" flow.
Parameters:
| Name | Type | Description |
|---|---|---|
sessionId |
string | |
action |
RecordedAction |
- Source:
Returns:
- Type
- RecordedAction
(static) filterEmittableActions(actions) → {Array.<RecordedAction>}
Filter a list of captured actions down to the ones the code generator
would actually emit. Convenience wrapper around isEmittableAction.
Parameters:
| Name | Type | Description |
|---|---|---|
actions |
Array.<RecordedAction> |
- Source:
Returns:
- Type
- Array.<RecordedAction>
(static) forwardInput(sessionId, event) → {Promise.<void>}
Forward a user input event from the canvas overlay to the headless browser via CDP Input domain commands. This is the core mechanism that makes the recorder interactive — without it the canvas is read-only and the user can never produce actions in the headless browser.
Supported event types: mousePressed / mouseReleased / mouseMoved → Input.dispatchMouseEvent keyDown / keyUp / char → Input.dispatchKeyEvent scroll → Input.dispatchMouseEvent (wheel)
Parameters:
| Name | Type | Description | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
sessionId |
string | |||||||||||||||||||||||||||||||||||||||||||||||||||||
event |
Object |
Properties
|
- Source:
Throws:
-
When the session is not found or has no CDP session.
- Type
- Error
Returns:
- Type
- Promise.<void>
(static) getRecording(sessionId) → {RecordingSession|null}
Look up an in-flight recording session.
Parameters:
| Name | Type | Description |
|---|---|---|
sessionId |
string |
- Source:
Returns:
- Type
- RecordingSession | null
(static) isEmittableAction(a) → {boolean}
Predicate matching the required-field branches in
actionsToPlaywrightCode. Returns true iff the action carries
enough information for the code generator to emit a corresponding line.
Exported so route handlers building the persisted human-readable
steps[] array can filter with the same rules the code generator
applies — without this shared predicate the two would drift, causing
steps.length !== playwrightCode step counts on the Test Detail page
(and breaking step-based edit/regeneration that indexes by position).
If you add a new action kind to actionsToPlaywrightCode, add the
matching branch here too.
Parameters:
| Name | Type | Description |
|---|---|---|
a |
RecordedAction |
- Source:
Returns:
- Type
- boolean
(static) isNoisyTestId(value) → {boolean}
DIF-015b — quality-scores a data-testid value. Returns true when the
value looks machine-generated / random (numeric-only, el_ / comp- /
t- prefix + hex tail, or a long unseparated token).
This is only used by the hand-rolled fallback selectorGenerator that
runs when Playwright's InjectedScript source cannot be loaded (missing
playwright-core install, Playwright bumped to a version with a different
injected-bundle layout, etc.). The primary path delegates to Playwright's
own selector generator which has its own — more sophisticated — noise
scoring built in.
Exported for unit tests that exercise the fallback path directly; callers outside the fallback should not depend on this heuristic.
Parameters:
| Name | Type | Description |
|---|---|---|
value |
string | Raw |
- Source:
Returns:
true when the value looks noisy and should be demoted.
- Type
- boolean
(static) pauseRecording(sessionId) → {Object}
DIF-015c Gap 3 — pause action capture on an in-flight recording.
Flips session.paused = true; the browser stays open and the
screencast continues so the operator can navigate the SUT without
polluting actions[]. Three call sites honour the flag:
forwardInput— short-circuits CDP dispatch so user clicks / keystrokes from the canvas overlay never reach the page.- The
__sentriRecordexposeBinding callback instartRecording— drops in-page-captured DOM events (debounced fills flushing, framework re-renders firing change handlers, programmatic clicks from page JS) so in-flight work that started before pause does not silently leak in. - The popup + debounced main-page
framenavigatedhandlers — drop synthesisedgotoactions while paused.
Parameters:
| Name | Type | Description |
|---|---|---|
sessionId |
string |
- Source:
Throws:
-
when the session is unknown or not in
"recording"state. - Type
- Error
Returns:
- Type
- Object
(static) popLastRecordingAction(sessionId) → {Object}
DIF-015c Gap 3 — undo the most recent recorded action. Idempotent on
an empty session.actions[] (returns { removed: null, actionCount: 0 }
rather than 4xx) so the UI can fire the button without first checking
the step count — matches the spec's "idempotent on empty actions[]"
acceptance criterion in NEXT.md.
Parameters:
| Name | Type | Description |
|---|---|---|
sessionId |
string |
- Source:
Throws:
-
when the session is unknown or not in
"recording"state. - Type
- Error
Returns:
- Type
- Object
(static) probeAtPoint(sessionId, point) → {Promise.<({selector: string, label: string, rect: {x: number, y: number, width: number, height: number}}|null)>}
DIF-015c Gap 2 (point-and-click assert UX) — resolve the
{selector, label, rect} for an arbitrary viewport coordinate so the
frontend can highlight the hovered element and pre-fill the
"Add verification" form on click. Mirrors how Playwright codegen's
inspector probes the page under the cursor.
The probe runs entirely in the page context via page.evaluate,
calling the window.__sentriProbeAtPoint helper that the recorder
script attaches at init time. That helper reuses the SAME selector +
label heuristics the click/fill listeners use, so the picker's
suggestion is byte-aligned with what a real click would have captured.
The probe is cheap (~5 ms in practice) and idempotent — the operator can pump events at hover frequency (~30 fps) without polluting the session. We deliberately do NOT record the probe as an action; it's a read-only inspection.
Parameters:
| Name | Type | Description |
|---|---|---|
sessionId |
string | |
point |
Object | Viewport coordinates (already
scaled by the frontend from CSS pixels via |
- Source:
Throws:
-
when the session is unknown or not recording.
- Type
- Error
Returns:
The hovered element's selector + friendly label + bounding rect, or
null when no interactive ancestor was found (e.g. cursor over the
page background). The caller falls back to manual selector paste.
- Type
- Promise.<({selector: string, label: string, rect: {x: number, y: number, width: number, height: number}}|null)>
(static) recordedActionToStepText(a) → {string}
Render a captured action as a short, human-readable step sentence so the
recorder's persisted steps[] array aligns visually with the AI generate /
crawl pipeline output (outputSchema.js) and the manual-test creation path
— both of which produce English prose like "User clicks the Sign Up
button". The Test Detail page renders all three sources through the same
Steps panel, and previously the recorder was the only producer emitting
engineer-shaped strings ("Step 1: click → #login"), making recorded tests
stick out and look broken to manual reviewers.
Parameters:
| Name | Type | Description |
|---|---|---|
a |
RecordedAction |
- Source:
Returns:
A single step sentence suitable for the persisted steps[] array.
- Type
- string
(static) resumeRecording(sessionId) → {Object}
DIF-015c Gap 3 — resume action capture after a pause. Idempotent on a
session that was never paused (the flag was already falsy). See
pauseRecording for the list of guarded call sites.
Parameters:
| Name | Type | Description |
|---|---|---|
sessionId |
string |
- Source:
Throws:
-
when the session is unknown or not in
"recording"state. - Type
- Error
Returns:
- Type
- Object
(static) startRecording(args) → {Promise.<RecordingSession>}
Start a new interactive recording session. Opens a Playwright browser,
navigates to startUrl, installs the capture script, and begins a CDP
screencast on the given session ID (reused as the SSE run ID).
Parameters:
| Name | Type | Description | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
args |
Object |
Properties
|
- Source:
Returns:
- Type
- Promise.<RecordingSession>
(static) stopRecording(sessionId, optsopt) → {Promise.<{actions: Array.<RecordedAction>, playwrightCode: string, url: string}>}
Stop an in-flight recording session, tear down the Playwright browser,
and return the captured actions transformed into Playwright source. The
generated code is wrapped in the repo-standard test(...) shape so the
caller can persist it as a Draft test row and re-run it through the
normal runner.
Idempotent w.r.t. the in-memory map: the session is removed regardless
of whether teardown errors. The browser/context/page cleanup calls are
.catch(() => {})-shielded so a half-closed browser never leaks state.
Parameters:
| Name | Type | Attributes | Description | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
sessionId |
string | ||||||||||
opts |
Object |
<optional> |
Properties
|
- Source:
Throws:
-
When the session does not exist.
- Type
- Error
Returns:
- Type
- Promise.<{actions: Array.<RecordedAction>, playwrightCode: string, url: string}>
(static) switchDevice(sessionId, device) → {Promise.<{device: string, viewport: {width: number, height: number}, url: string}>}
DIF-015c Gap 5 — switch the device profile of an in-flight recording
session. Playwright applies device emulation (userAgent, viewport,
deviceScaleFactor, hasTouch, locale) at browser.newContext() time
only — there is no mid-context API for swapping descriptors. To honour
the operator's device choice mid-session we tear down the page+context
and rebuild them under the new descriptor against the same browser
process. Captured session.actions[] are preserved across the switch
(the operator's step history is not lost), but page state (cookies,
partially-filled forms, scroll position, in-flight requests) is — the
UI surfaces a confirmation prompt before calling this so operators
understand the trade-off.
Acceptance criteria (NEXT.md :53):
- Device dropdown shows the same options as
RunRegressionModal✓ (DEVICE_PRESETS shared viaconfig.js). - Switching mid-session resizes the canvas to match ✓ (response
viewportflows back to the frontend;LiveBrowserViewalready rescales pointer coordinates againstviewportW/viewportH). - Selectors regenerated at the new viewport's pixel scale ✓ (the
rebuilt context's
deviceScaleFactorflows from the descriptor; subsequent captures use the new viewport's coordinate space becauseselectorGeneratorruns against the rebuilt page).
Parameters:
| Name | Type | Description |
|---|---|---|
sessionId |
string | |
device |
string | One of |
- Source:
Throws:
-
when the session is unknown, not recording, the device is invalid, or the rebuild fails (in which case the session is left in a torn-down state and the caller should re-issue
stopRecording). - Type
- Error
Returns:
- Type
- Promise.<{device: string, viewport: {width: number, height: number}, url: string}>
(static) takeCompletedRecording(sessionId) → {CompletedRecording|null}
Look up and remove a recording that was auto-torn-down by the safety-net
timeout. Returns null if no such recording is cached (either never timed
out, or the TTL has expired). The entry is removed on read so callers get
at-most-once delivery of the captured actions.
Parameters:
| Name | Type | Description |
|---|---|---|
sessionId |
string |
- Source:
Returns:
- Type
- CompletedRecording | null
(inner) _looksLikeSecretValue(value) → {boolean}
SEC-007 — server-side defence-in-depth credential detector.
Module-scope sibling to the in-page isSensitiveField heuristic. The
page-side script is the primary redaction path (it sees the
<input type="password"> attribute and sets redacted: true before
the value crosses the binding boundary). This helper runs at the
Node-side binding callback and only matters when something slipped
past — a SUT that uses <input type="text"> for a password without
matching the name/id heuristic, or a future RECORDER_SCRIPT change
that introduces a regression.
Detection rules (entropy + known-credential shapes):
- JWT (3-segment base64url):
eyJ…\.…\.… - AWS access key id:
AKIA[0-9A-Z]{16} - Bearer token literal:
Bearer <token> - Stripe / GitHub / OpenAI / Slack key prefixes (industry-standard
gitleaks rules):
sk_(live|test)_…,ghp_…,gho_…,ghs_…,xox[abps]-… - High-entropy 32+ char base64ish blob (catches API keys / session tokens that don't match a known prefix)
- Credit card (Luhn-checked, 13–19 digits)
Returns false for the empty string, sentinel values
(__SENTRI_SECRET_<n>__), and obvious-non-secret short tokens (≤8
chars or all whitespace) so the false-positive rate stays low.
The detection is intentionally narrower than secretScanner.js
(which scans generated test code post-hoc and is allowed false
positives) — at this site a false positive would silently corrupt the
captured fill value, breaking replay. Only catch what we're sure of.
Parameters:
| Name | Type | Description |
|---|---|---|
value |
string | undefined | Raw value about to be persisted. |
- Source:
Returns:
true when the value matches a known credential pattern.
- Type
- boolean
(inner) _luhnValid(digits) → {boolean}
SEC-007 — Luhn check for credit card validation. Returns true when the digits-only string passes the standard mod-10 checksum.
Parameters:
| Name | Type | Description |
|---|---|---|
digits |
string | Digits-only string (caller must strip spaces/dashes). |
- Source:
Returns:
- Type
- boolean
(inner) escapeJsSingleQuote(str) → {string}
Escape a user-controlled string so it can be safely interpolated into a
JavaScript single-quoted string literal in generated source code. Handles
backslash (\), single quote ('), newline (\n), carriage return (\r),
line/paragraph separators (U+2028 / U+2029 — these break literals in most
engines), and other C0 control characters via \xHH escapes.
Order matters: backslash must be escaped first, otherwise subsequent replacements would double-escape their own inserted backslashes.
Parameters:
| Name | Type | Description |
|---|---|---|
str |
string |
- Source:
Returns:
- Type
- string
(inner) friendlyTarget(a) → {string}
Derive a human-readable target phrase from a recorded action — prefers the
captured label (aria-label / inner text / placeholder), then falls back
to extracting a friendly name from a Playwright role selector
(role=button[name="Sign in"] → the "Sign in" button), and finally to
an empty string so the caller can degrade to a target-less sentence.
Important: callers must NOT splice raw selectors into the persisted steps
— the AI generate / crawl pipeline ("User clicks the Sign Up button") and
the manual-test path both produce English prose, and recorded steps need
to render alongside them on the Test Detail page without leaking
role=…[name="…"] or CSS into the reviewer's view.
Parameters:
| Name | Type | Description |
|---|---|---|
a |
RecordedAction |
- Source:
Returns:
Either the "<label>" <noun>, "<label>", or "".
- Type
- string
(inner) friendlyTargetFromSelector(selector, nounopt) → {string}
Same as friendlyTarget but operates on a raw selector/label pair
pulled from a different action's drop-target fields (a.target is a
selector string only — there's no separate targetLabel). Used by the
drag step formatter so the rendered sentence reads as
User drags the 'Card 1' card onto the 'Done' column instead of dropping
the target half of the gesture entirely.
Parameters:
| Name | Type | Attributes | Description |
|---|---|---|---|
selector |
string | Raw selector to extract a friendly name from. |
|
noun |
string |
<optional> |
Element noun ( |
- Source:
Returns:
the '<name>' <noun>,'<name>', or"".
- Type
- string
(inner) resolveDeviceContext(deviceopt) → {Object}
DIF-015c Gap 5 — pure helper that resolves a device name to a
browser.newContext() options object plus the effective viewport.
Mirrors the device-descriptor merge already done by executeTest.js
(backend/src/runner/executeTest.js:208-232) so recorder runs feel
identical to test runs at the same device profile.
Empty / unknown device names fall back to the desktop defaults so a caller that never opts into device emulation behaves bit-for-bit identically to the pre-Gap-5 recorder.
Parameters:
| Name | Type | Attributes | Description |
|---|---|---|---|
device |
string |
<optional> |
One of |
- Source:
Returns:
- Type
- Object
(inner) shortUrl()
Trim a captured URL for display in the Steps panel. Strips the query string + fragment (which dominate noisy recorder URLs like Amazon search pages with 6 tracking params) and caps the rendered length so a single step doesn't push the panel sideways.
- Source:
Type Definitions
CompletedRecording
Type:
- Object
Properties:
| Name | Type | Description |
|---|---|---|
projectId |
string | |
actions |
Array.<RecordedAction> | |
playwrightCode |
string | |
url |
string | |
completedAt |
number | |
reason |
"auto_timeout" | "manual" |
- Source:
RecordedAction
Type:
- Object
Properties:
| Name | Type | Attributes | Description |
|---|---|---|---|
kind |
"goto" | "click" | "dblclick" | "rightClick" | "hover" | "fill" | "press" | "select" | "check" | "uncheck" | "upload" | "drag" | "assertVisible" | "assertText" | "assertValue" | "assertUrl" | "assertCount" | "assertHasClass" | ||
selector |
string |
<optional> |
Best-effort role/label/text/css selector. |
label |
string |
<optional> |
Human-readable label for the target
element (aria-label / inner text /
placeholder / |
value |
string |
<optional> |
For |
redacted |
boolean |
<optional> |
SEC-007: |
url |
string |
<optional> |
For |
key |
string |
<optional> |
For |
frameUrl |
string |
<optional> |
URL of iframe containing the action. |
pageAlias |
string |
<optional> |
"page" for main tab, "popupN" for popups. |
target |
string |
<optional> |
For drag/drop target selector. |
ts |
number | Epoch ms when the action was captured. |
- Source:
RecordingSession
Type:
- Object
Properties:
| Name | Type | Attributes | Description |
|---|---|---|---|
id |
string | ||
projectId |
string | ||
url |
string | Starting URL. |
|
status |
"recording" | "stopping" | "stopped" | ||
actions |
Array.<RecordedAction> | ||
startedAt |
number | ||
device |
string |
<optional> |
DIF-015c Gap 5: active device profile
(e.g. |
viewport |
Object |
<optional> |
Resolved viewport
(from device descriptor, falling
back to |
paused |
boolean |
<optional> |
DIF-015c Gap 3: pause flag. |
stealth |
boolean |
<optional> |
DIF-015c Gap 6: when true the
recorder context has |
browser |
Object |
<optional> |
Playwright Browser (internal). |
context |
Object |
<optional> |
Playwright BrowserContext (internal). |
page |
Object |
<optional> |
Playwright Page (internal). |
stopScreencast |
function |
<optional> |
Cleanup fn returned by startScreencast. |
cdpSession |
Object |
<optional> |
CDP session for input forwarding. |
frameNavTimer |
* |
<optional> |
DIF-015c Gap 5 follow-up: Node-side
setTimeout handle for the debounced
|
- Source: