182 lines
6.1 KiB
TypeScript
182 lines
6.1 KiB
TypeScript
import type { ImhotepId, StateKind, Environment, ExecutionContext } from 'imhotep-core'
|
|
import { getDefaultContext } from 'imhotep-core'
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Snapshot Identity
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** Stable identifier for a geometry snapshot. */
|
|
export type SnapshotId = ImhotepId
|
|
|
|
// Module-level monotonic counter ensures unique ids even when deterministic
|
|
// test contexts return identical clock/idGenerator values per call.
|
|
let _snapshotCounter = 0
|
|
|
|
/** Generates a unique snapshot id with an optional prefix.
|
|
* When no context is provided, uses the global default context.
|
|
* A module-level counter guarantees uniqueness across consecutive calls.
|
|
*/
|
|
export function createSnapshotId(prefix = 'snap', ctx?: ExecutionContext): SnapshotId {
|
|
const context = ctx ?? getDefaultContext()
|
|
const counter = ++_snapshotCounter
|
|
return `${prefix}_${context.clock()}_${context.idGenerator()}_${counter}`
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// State Source Classification
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Describes how a state was materialized.
|
|
*
|
|
* - interaction: Real Playwright interaction (hover, focus, mousedown).
|
|
* - css-pseudo: Synthetic emulation via forced CSS pseudo-class styles.
|
|
* - synthetic: Computed projection without browser interaction.
|
|
* - component-hook: Materialized via a component adapter hook.
|
|
*/
|
|
export type StateSource = 'interaction' | 'css-pseudo' | 'synthetic' | 'component-hook'
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Snapshot Metadata
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Metadata attached to every geometry snapshot so diagnostics can
|
|
* trace which state, source, and environment produced it.
|
|
*/
|
|
export interface SnapshotMetadata {
|
|
/** Unique id for this snapshot instance. */
|
|
snapshotId: SnapshotId
|
|
/** The UI state kind that was materialized (hover, focus, etc). */
|
|
stateKind: StateKind
|
|
/** How the state was produced (real interaction vs synthetic). */
|
|
stateSource: StateSource
|
|
/** Selector of the element that received the state, if any. */
|
|
selector?: string
|
|
/** Milliseconds since epoch when the snapshot was captured. */
|
|
timestamp: number
|
|
/** Environment case active when the snapshot was taken. */
|
|
env?: Environment
|
|
/** Duration of the transition sample in ms, if applicable. */
|
|
durationMs?: number
|
|
/** Approximation note for diagnostics when state is not fully determinate. */
|
|
approximationNote?: string
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Geometry Snapshot
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* A captured geometry world for a specific UI state.
|
|
*
|
|
* The `world` property holds the extractor output (geometry world schema).
|
|
* It is typed as `unknown` here because the exact schema is owned by the
|
|
* extractor package; imhotep-state materializes and tags snapshots without
|
|
* needing to inspect world internals.
|
|
*/
|
|
export interface GeometrySnapshot {
|
|
/** Snapshot identifier used by the solver and diagnostics. */
|
|
id: SnapshotId
|
|
/** State metadata for traceability and comparison. */
|
|
metadata: SnapshotMetadata
|
|
/** The materialized geometry world produced by the extractor. */
|
|
world: unknown
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Snapshot Comparison
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** Result of comparing two geometry snapshots. */
|
|
export interface SnapshotDiff {
|
|
/** True when both snapshots have identical world and metadata. */
|
|
identical: boolean
|
|
/** Human-readable list of detected differences. */
|
|
differences: string[]
|
|
}
|
|
|
|
/**
|
|
* Deep-compares two geometry snapshots for equality.
|
|
*
|
|
* Used by tests and diagnostics to detect unexpected changes between
|
|
* states (e.g. hover caused the element to shift).
|
|
*/
|
|
export function compareSnapshots(a: GeometrySnapshot, b: GeometrySnapshot): SnapshotDiff {
|
|
const differences: string[] = []
|
|
|
|
if (a.id !== b.id) {
|
|
differences.push(`id changed from "${a.id}" to "${b.id}"`)
|
|
}
|
|
|
|
if (a.metadata.stateKind !== b.metadata.stateKind) {
|
|
differences.push(
|
|
`stateKind changed from "${a.metadata.stateKind}" to "${b.metadata.stateKind}"`
|
|
)
|
|
}
|
|
|
|
if (a.metadata.stateSource !== b.metadata.stateSource) {
|
|
differences.push(
|
|
`stateSource changed from "${a.metadata.stateSource}" to "${b.metadata.stateSource}"`
|
|
)
|
|
}
|
|
|
|
if (a.metadata.selector !== b.metadata.selector) {
|
|
differences.push(
|
|
`selector changed from "${a.metadata.selector}" to "${b.metadata.selector}"`
|
|
)
|
|
}
|
|
|
|
// Compare world via JSON serialization for a stable deep equality check.
|
|
// This is sufficient for unit tests and diagnostic diffs.
|
|
const worldA = JSON.stringify(a.world)
|
|
const worldB = JSON.stringify(b.world)
|
|
if (worldA !== worldB) {
|
|
differences.push('world content differs')
|
|
}
|
|
|
|
return {
|
|
identical: differences.length === 0,
|
|
differences,
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Snapshot Registry
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* In-memory store for geometry snapshots during a test run.
|
|
*
|
|
* The materializer populates this store; the solver reads from it.
|
|
*/
|
|
export interface SnapshotStore {
|
|
/** Add a snapshot to the store. */
|
|
add(snapshot: GeometrySnapshot): void
|
|
/** Retrieve a snapshot by id. */
|
|
get(id: SnapshotId): GeometrySnapshot | undefined
|
|
/** List all snapshots in insertion order. */
|
|
list(): GeometrySnapshot[]
|
|
/** Remove all snapshots. */
|
|
clear(): void
|
|
}
|
|
|
|
/** Factory for a default Map-backed snapshot store. */
|
|
export function createSnapshotStore(): SnapshotStore {
|
|
const store = new Map<SnapshotId, GeometrySnapshot>()
|
|
return {
|
|
add(snapshot) {
|
|
store.set(snapshot.id, snapshot)
|
|
},
|
|
get(id) {
|
|
return store.get(id)
|
|
},
|
|
list() {
|
|
return Array.from(store.values())
|
|
},
|
|
clear() {
|
|
store.clear()
|
|
},
|
|
}
|
|
}
|