src/lib/demo/flow-converter.ts

Total Symbols
10
Lines of Code
231
Avg Complexity
3.8
Symbol Types
2

File Relationships

graph LR convertRecordingToFlow["convertRecordingToFlow"] _resetIdCounter["_resetIdCounter"] convertSteps["convertSteps"] generateRandomSeed["generateRandomSeed"] generateFlowId["generateFlowId"] mapActionType["mapActionType"] nextStepId["nextStepId"] isValidFieldType["isValidFieldType"] mapAssertion["mapAssertion"] convertRecordingToFlow -->|calls| _resetIdCounter convertRecordingToFlow -->|calls| convertSteps convertRecordingToFlow -->|calls| generateRandomSeed convertRecordingToFlow -->|calls| generateFlowId convertSteps -->|calls| mapActionType convertSteps -->|calls| nextStepId convertSteps -->|calls| isValidFieldType convertSteps -->|calls| mapAssertion click convertRecordingToFlow "../symbols/757a5ffbc62eadcc.html" click _resetIdCounter "../symbols/66adb029029661b7.html" click convertSteps "../symbols/9420198414a645f6.html" click generateRandomSeed "../symbols/5413ae835026dc30.html" click generateFlowId "../symbols/1cb137866728c1e8.html" click mapActionType "../symbols/13e0c919e8b27254.html" click nextStepId "../symbols/60b2ffaea2f7c890.html" click isValidFieldType "../symbols/a95b73445027ba7e.html" click mapAssertion "../symbols/a4c2e86108e15f2e.html"

Architecture violations

View all

  • [warning] function-camel-case: '_resetIdCounter' does not match naming convention /^[a-z][a-zA-Z0-9]*$/
  • [warning] max-cyclomatic-complexity: 'convertSteps' has cyclomatic complexity 23 (max 10)

Symbols by Kind

function 9
interface 1

All Symbols

Name Kind Visibility Status Lines Signature
isValidFieldType function - 31-33 isValidFieldType(value: string | undefined): : value is FieldType
nextStepId function - 36-38 nextStepId(): : string
_resetIdCounter function exported- 41-43 _resetIdCounter(): : void
mapActionType function - 46-68 mapActionType( recordedType: RecordedStep["type"], ): : FlowActionType | null
mapAssertion function - 71-94 mapAssertion(step: RecordedStep): : FlowAssertion | undefined
ConvertOptions interface exported- 98-107 interface ConvertOptions
convertRecordingToFlow function exported- 115-141 convertRecordingToFlow( session: RecordingSession, options: ConvertOptions = {}, ): : FlowScript
convertSteps function exported- 147-220 convertSteps(recorded: RecordedStep[]): : FlowStep[]
generateFlowId function - 224-226 generateFlowId(): : string
generateRandomSeed function - 228-230 generateRandomSeed(): : string

Full Source

/**
 * Flow Converter — transforms a RecordingSession into a FlowScript.
 *
 * The converter maps e2e-export `RecordedStep[]` into the portable
 * `FlowScript` format. Each step's `fieldType` is used to create a
 * generator-backed `FlowValueSource` (via `mapValueToSource`) so that
 * replays produce fresh data.
 */

import type { FieldType } from "@/types";
import type {
  RecordedStep,
  RecordingSession,
} from "@/lib/e2e-export/e2e-export.types";
import type {
  FlowScript,
  FlowStep,
  FlowMetadata,
  FlowActionType,
  FlowAssertion,
  AssertOperator,
} from "./demo.types";
import { DEFAULT_REPLAY_CONFIG, FLOW_SCRIPT_VERSION } from "./demo.types";
import { mapValueToSource } from "./value-mapper";
import { FIELD_TYPES } from "@/types";

// ── Helpers ───────────────────────────────────────────────────────────────

const FIELD_TYPE_SET: ReadonlySet<string> = new Set(FIELD_TYPES);

function isValidFieldType(value: string | undefined): value is FieldType {
  return value != null && FIELD_TYPE_SET.has(value);
}

let idCounter = 0;
function nextStepId(): string {
  return `step_${++idCounter}`;
}

/** Reset internal counter (for testing) */
export function _resetIdCounter(): void {
  idCounter = 0;
}

/** Map recorded step type → FlowActionType (drop unsupported) */
function mapActionType(
  recordedType: RecordedStep["type"],
): FlowActionType | null {
  const map: Record<string, FlowActionType> = {
    navigate: "navigate",
    fill: "fill",
    click: "click",
    select: "select",
    check: "check",
    uncheck: "uncheck",
    clear: "clear",
    "press-key": "press-key",
    scroll: "scroll",
    assert: "assert",
    submit: "click",
    hover: "click",
    "wait-for-element": "wait",
    "wait-for-hidden": "wait",
    "wait-for-url": "wait",
    "wait-for-network-idle": "wait",
  };
  return map[recordedType] ?? null;
}

/** Map e2e-export assertion type → FlowAssertion */
function mapAssertion(step: RecordedStep): FlowAssertion | undefined {
  if (!step.assertion) return undefined;

  const operatorMap: Record<string, AssertOperator> = {
    "url-changed": "url-contains",
    "url-contains": "url-contains",
    "visible-text": "contains",
    "element-visible": "visible",
    "element-hidden": "hidden",
    "field-value": "equals",
    "field-error": "contains",
    redirect: "url-equals",
    "response-ok": "exists",
    "toast-message": "contains",
  };

  const operator = operatorMap[step.assertion.type];
  if (!operator) return undefined;

  return {
    operator,
    expected: step.assertion.expected,
  };
}

// ── Converter ─────────────────────────────────────────────────────────────

export interface ConvertOptions {
  /** Flow name (defaults to "Recorded Flow") */
  name?: string;
  /** Flow description */
  description?: string;
  /** Seed for deterministic PRNG (auto-generated if omitted) */
  seed?: string;
  /** Tags for categorisation */
  tags?: string[];
}

/**
 * Convert a `RecordingSession` into a `FlowScript`.
 *
 * Filters out steps that cannot be mapped and computes timing deltas
 * from the original timestamps.
 */
export function convertRecordingToFlow(
  session: RecordingSession,
  options: ConvertOptions = {},
): FlowScript {
  _resetIdCounter();

  const steps = convertSteps(session.steps);

  const now = Date.now();
  const metadata: FlowMetadata = {
    name: options.name ?? "Recorded Flow",
    description: options.description,
    baseUrl: session.startUrl,
    seed: options.seed ?? generateRandomSeed(),
    createdAt: now,
    updatedAt: now,
    version: FLOW_SCRIPT_VERSION,
    tags: options.tags,
  };

  return {
    id: generateFlowId(),
    metadata,
    replayConfig: { ...DEFAULT_REPLAY_CONFIG },
    steps,
  };
}

/**
 * Convert standalone `RecordedStep[]` into `FlowStep[]`.
 * Useful when you only have steps without a full session.
 */
export function convertSteps(recorded: RecordedStep[]): FlowStep[] {
  const result: FlowStep[] = [];
  let prevTimestamp: number | null = null;

  for (const rec of recorded) {
    const action = mapActionType(rec.type);
    if (!action) continue;

    const flowStep: FlowStep = {
      id: nextStepId(),
      action,
      label: rec.label,
    };

    // Selector
    if (rec.selector) {
      flowStep.selector = rec.selector;
    }
    if (rec.smartSelectors?.length) {
      flowStep.smartSelectors = rec.smartSelectors;
    }

    // Value source for fill steps
    if (action === "fill" && rec.value != null) {
      const fieldType = isValidFieldType(rec.fieldType) ? rec.fieldType : null;
      flowStep.valueSource = mapValueToSource(rec.value, fieldType);
    }

    // Navigation URL
    if (action === "navigate" && rec.url) {
      flowStep.url = rec.url;
    }

    // Select
    if (action === "select" && rec.value != null) {
      flowStep.selectText = rec.value;
    }

    // Key press
    if (action === "press-key" && rec.key) {
      flowStep.key = rec.key;
    }

    // Wait timeout
    if (action === "wait") {
      flowStep.waitTimeout = rec.waitTimeout ?? 10_000;
    }

    // Scroll
    if (action === "scroll" && rec.scrollPosition) {
      flowStep.scrollPosition = rec.scrollPosition;
    }

    // Assert
    if (action === "assert") {
      flowStep.assertion = mapAssertion(rec);
    }

    // Timing delta from previous step
    if (prevTimestamp !== null && rec.timestamp > 0) {
      const delta = rec.timestamp - prevTimestamp;
      if (delta > 0) {
        flowStep.delayBefore = delta;
      }
    }
    if (rec.timestamp > 0) {
      prevTimestamp = rec.timestamp;
    }

    result.push(flowStep);
  }

  return result;
}

// ── ID generation ─────────────────────────────────────────────────────────

function generateFlowId(): string {
  return `flow_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
}

function generateRandomSeed(): string {
  return Math.random().toString(36).slice(2, 10);
}