import type { DesignScreenInteraction, DesignScreenAssertion, DesignSerializable, StoryboardEnvironment } from './designImport.js'; import type { PageModel } from './store.js'; export { reachablePreviewTestIds } from './previewReachability.js'; /** * Screen-state axes — same-frame variations reproduced by a gesture or a boot * condition. * * A collapsed sidebar or a hovered row is not a separate screen: it is the * same screen after one gesture, reachable on every frame that has the * control. Declaring it as an axis keeps those variations out of the * catalog (where each would cost a captured frame) and puts them in the * right panel, where a designer flips between them. * * An option either extends the ordinary interaction recipe, declares a boot * condition, or supplies a host-owned desired-state value. Everything * downstream keys off that complete reproduction contract. */ export interface InteractiveStateOption { id: string; label: string; /** Optional reference to a declared target group for availability filtering. */ targetId?: string; /** Absent or empty marks the base state — the screen as captured. */ steps?: readonly DesignScreenInteraction[]; /** * Host-owned value rendered directly after mount. Selecting the option puts * it under the axis id in the frame's complete desired-state map. * * This is an alternative to both gesture replay and a boot environment: the * host adapter translates the value into preview-only application state. */ desiredState?: DesignSerializable; /** * Boot condition this option puts the frame under, for states no gesture can * reach: a failed request, an empty result, a stalled stream. * * Those are the states QA most wants to hold still, and until now they had no * declaration. A gesture cannot produce them, so the only options were a * dedicated capture (a frame per failure) or burying a mock flag inside one * case's recipe — so hosts did neither, and their error/empty/loading markers * sat in the product with nothing pointing at them. * * The environment is merged into the frame's own, which already belongs to the * recipe identity — so selecting the option changes the cache key and the * existing supply path (live replay, flow session, artifact lookup) reproduces * it. Nothing downstream needs to know this option kind exists. * * May be combined with `steps` when the state needs a gesture AFTER booting * under the condition. */ environment?: StoryboardEnvironment; /** * Recognizes this option in the CAPTURED tree, which is what makes the base * a property of the frame instead of the axis. * * One axis covers every frame carrying the control, but "the screen as * captured" differs between them: a result panel captured mid-recording sits * on its transcript tab, and the same panel captured after conversion sits on * its summary tab. With a single declared base the axis is a no-op on every * frame whose capture is the other option — selecting it only reproduces the * capture that is already showing. * * Declare it on EVERY option (each with its own steps) and the axis resolves * per frame: whichever option the capture already shows needs no steps, and * the others run theirs. */ capturedWhen?: { testId: string; attribute?: { name: string; value: string; }; }; } /** Every way a screen state can be reproduced; a declaration with any other kind is refused. */ export declare const SCREEN_STATE_AXIS_KINDS: readonly ["interaction", "condition"]; export type ScreenStateAxisKind = (typeof SCREEN_STATE_AXIS_KINDS)[number]; /** Explicit reversible edges on an already prepared document; no inverse gesture is inferred. */ export interface ScreenStateLiveTransition { from: string; to: string; steps: readonly DesignScreenInteraction[]; before: readonly DesignScreenAssertion[]; after: readonly DesignScreenAssertion[]; } export interface InteractiveStateDef { id: string; label: string; /** * Why this variation belongs on the frame instead of becoming another frame. * * `interaction` is a deterministic local state reached by a gesture on the * already booted screen. `condition` is an external state that must exist * before the screen boots, such as an empty response, denied permission, or * stalled request. The validator refuses mixed axes so a sample value cannot * quietly masquerade as a review state. */ kind: ScreenStateAxisKind; /** * Frames the axis applies to. A frame qualifies when its route matches * (prefix) and its imported tree carries the test id, so an axis declared * once covers every screen that actually has the control. */ requires?: { testId?: string; route?: string; attribute?: { name: string; value: string; }; }; /** Stable host-owned group identity; rendering semantics remain in the host adapter. */ target?: { id: string; label: string; }; /** * Options combine instead of replacing one another. * * Most axes are a choice between mutually exclusive states — a sidebar is * open or collapsed, never both. Some are not: a repeated row can carry a * list of status marks, and the screen worth looking at is often two of them * at once. Declared as an exclusive axis, such a screen cannot be reached at * all, and declaring every combination as its own option is a power set. * * Selecting toggles the option in the set; the base option (the one with no * steps, condition, or desired state) clears it. Steps run, conditions merge, * and desired values collect in declaration order, so the result is stable no * matter what order they were clicked in. * * Not compatible with `capturedWhen`: a per-frame base answers "which single * option is already on screen", which a combination has no answer to. */ multiple?: boolean; /** * Optional single-axis live transitions. Each edge must prove its starting * and resulting state. Unsupported combinations keep the ordinary fresh * capture path, and a failed edge permanently discards that live document. */ liveTransitions?: readonly ScreenStateLiveTransition[]; options: readonly InteractiveStateOption[]; } export interface InteractiveStateDiagnostic { severity: 'error' | 'warning'; interactionId: string; message: string; } /** Preferred public name: the collection contains interaction and condition axes. */ export type ScreenStateAxisDef = InteractiveStateDef; /** Preferred public name: an option on either kind of screen-state axis. */ export type ScreenStateOption = InteractiveStateOption; /** Preferred public name for validation output. */ export type ScreenStateAxisDiagnostic = InteractiveStateDiagnostic; /** * Whether selecting this option asks supply for a different screen. * * Three ways do that: direct desired state, a gesture on the booted screen, or * a boot condition. The base is the option that declares none of them and * reproduces the capture. Judging the base by "no steps" alone would read a * direct or environment-only option as a second base and refuse the declaration. */ export declare function optionChangesSupply(option: InteractiveStateOption): boolean; export declare function validateInteractiveStates(defs: readonly InteractiveStateDef[]): InteractiveStateDiagnostic[]; /** Validates both interaction and condition axes. */ export declare const validateScreenStateAxes: typeof validateInteractiveStates; /** Reads only the control-presence evidence needed by the view-only inspector. */ export declare function previewMarkupTestIds(markup: string | null | undefined, bodyAttributes?: string | null): readonly string[]; /** Exact target attributes from the frozen markup; detached parsing never runs scripts. */ export declare function previewMarkupStateWitnesses(markup: string | null | undefined): NonNullable; /** * Axes available on one frame. The test-id requirement is checked against * either the imported tree or the frozen preview's reachable test-id index. * View-only can therefore offer a real screen control without importing the * whole layer tree, while a frame without that control still hides the axis. */ export declare function interactiveStatesForPage(page: PageModel, defs: readonly InteractiveStateDef[]): InteractiveStateDef[]; /** Preferred public name for axes applicable to one frame. */ export declare const screenStateAxesForPage: typeof interactiveStatesForPage; /** * The option this frame's capture already shows, or undefined when the axis * declares a single base instead of witnessing each option. */ export declare function capturedInteractiveOption(def: InteractiveStateDef, page: PageModel): InteractiveStateOption | undefined; /** * The option that reproduces the capture — the frame's base. * * A witnessed axis resolves it against this frame's tree; otherwise it is the * one option declared without steps. */ export declare function baseInteractiveOption(def: InteractiveStateDef, page?: PageModel): InteractiveStateOption | undefined; /** One editor's declared screen state axes. */ export declare function createInteractiveStates(): { directInteractiveStateAxisForPage: (page: PageModel, defs?: readonly InteractiveStateDef[]) => InteractiveStateDef | undefined; setScreenStateAxes: (defs: readonly InteractiveStateDef[] | undefined) => void; setInteractiveStates: (defs: readonly InteractiveStateDef[] | undefined) => void; getInteractiveStates: () => readonly InteractiveStateDef[]; getScreenStateAxes: () => readonly InteractiveStateDef[]; }; export type InteractiveStates = ReturnType;