src/lib/generators/adaptive.ts

Total Symbols
6
Lines of Code
169
Avg Complexity
6.3
Avg Coverage
96.3%

File Relationships

graph LR generateWithConstraints["generateWithConstraints"] adaptGeneratedValue["adaptGeneratedValue"] resolveMaxLength["resolveMaxLength"] buildValueCandidates["buildValueCandidates"] isValueValidForElement["isValueValidForElement"] generateWithConstraints -->|calls| adaptGeneratedValue adaptGeneratedValue -->|calls| resolveMaxLength adaptGeneratedValue -->|calls| buildValueCandidates adaptGeneratedValue -->|calls| isValueValidForElement click generateWithConstraints "../symbols/b5634e31dcca6c7f.html" click adaptGeneratedValue "../symbols/08c3ad8d1ea27c4d.html" click resolveMaxLength "../symbols/30b51c9b1c4a4dca.html" click buildValueCandidates "../symbols/91d3155f6d532883.html" click isValueValidForElement "../symbols/5972465051ee2fe8.html"

Architecture violations

View all

  • [warning] max-cyclomatic-complexity: 'buildValueCandidates' has cyclomatic complexity 12 (max 10)
  • [warning] max-cyclomatic-complexity: 'isValueValidForElement' has cyclomatic complexity 11 (max 10)

Symbols by Kind

function 5
interface 1

All Symbols

Name Kind Visibility Status Lines Signature
ValueConstraints interface exported- 10-19 interface ValueConstraints
generateWithConstraints function exported- 30-55 generateWithConstraints( generatorFn: () => string, constraints: ValueConstraints = {}, ): : string
adaptGeneratedValue function exported- 64-78 adaptGeneratedValue( value: string, constraints: ValueConstraints = {}, ): : string
buildValueCandidates function - 80-114 buildValueCandidates(value: string, maxLength: number): : string[]
resolveMaxLength function - 116-127 resolveMaxLength(constraints: ValueConstraints): : number
isValueValidForElement function - 129-168 isValueValidForElement( element: ValueConstraints["element"], value: string, ): : boolean

Full Source

/**
 * Shared generation adaptation helpers.
 * Keeps generated values aligned with native input constraints when available.
 */

/**
 * Constraints extracted from the target HTML element to guide value generation.
 * Used to ensure generated values satisfy native input validation rules.
 */
export interface ValueConstraints {
  element?:
    | HTMLInputElement
    | HTMLTextAreaElement
    | HTMLSelectElement
    | HTMLElement;
  maxLength?: number;
  requireValidity?: boolean;
  attempts?: number;
}

const DEFAULT_ATTEMPTS = 12;

/**
 * Repeatedly invokes a generator function until the result satisfies the given
 * constraints (e.g. `maxLength`, native element validity).
 * @param generatorFn - Zero-arg function that produces a random value
 * @param constraints - Optional element/length constraints to satisfy
 * @returns A value that passes validation, or empty string if `requireValidity` is set and no valid value is found
 */
export function generateWithConstraints(
  generatorFn: () => string,
  constraints: ValueConstraints = {},
): string {
  const attempts = constraints.attempts ?? DEFAULT_ATTEMPTS;
  let lastGenerated = "";

  for (let i = 0; i < attempts; i += 1) {
    const raw = generatorFn();
    lastGenerated = raw;
    const adapted = adaptGeneratedValue(raw, {
      ...constraints,
      requireValidity: true,
    });
    if (adapted) return adapted;
  }

  if (constraints.requireValidity) {
    return "";
  }

  return adaptGeneratedValue(lastGenerated, {
    ...constraints,
    requireValidity: false,
  });
}

/**
 * Adapts a generated string value to fit within element constraints.
 * Tries the original value, trimmed, collapsed-whitespace, and digits-only variants.
 * @param value - Raw generated value
 * @param constraints - Optional element/length constraints
 * @returns The first variant that satisfies constraints, or empty string
 */
export function adaptGeneratedValue(
  value: string,
  constraints: ValueConstraints = {},
): string {
  const maxLength = resolveMaxLength(constraints);
  const candidates = buildValueCandidates(value, maxLength);

  for (const candidate of candidates) {
    if (isValueValidForElement(constraints.element, candidate)) {
      return candidate;
    }
  }

  return constraints.requireValidity ? "" : (candidates[0] ?? "");
}

function buildValueCandidates(value: string, maxLength: number): string[] {
  const candidates: string[] = [];

  const pushCandidate = (candidate: string): void => {
    if (!candidate && candidate !== "") return;
    if (!candidates.includes(candidate)) {
      candidates.push(candidate);
    }
  };

  pushCandidate(value);

  const trimmed = value.trim();
  if (trimmed !== value) pushCandidate(trimmed);

  const collapsedWhitespace = value.replace(/\s+/g, " ").trim();
  if (collapsedWhitespace && collapsedWhitespace !== value) {
    pushCandidate(collapsedWhitespace);
  }

  const digitsOnly = value.replace(/\D+/g, "");
  if (digitsOnly && digitsOnly !== value) {
    pushCandidate(digitsOnly);
  }

  if (maxLength > 0) {
    for (const candidate of [...candidates]) {
      if (candidate.length > maxLength) {
        pushCandidate(candidate.slice(0, maxLength));
      }
    }
  }

  return candidates;
}

function resolveMaxLength(constraints: ValueConstraints): number {
  if (typeof constraints.maxLength === "number") {
    return constraints.maxLength;
  }

  const el = constraints.element;
  if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
    return el.maxLength;
  }

  return -1;
}

function isValueValidForElement(
  element: ValueConstraints["element"],
  value: string,
): boolean {
  if (!element) return true;

  // Custom component wrappers (div, span, etc.) are not native form elements.
  // Skip the validity probe and trust the adapter to accept any generated value.
  if (
    !(element instanceof HTMLInputElement) &&
    !(element instanceof HTMLTextAreaElement) &&
    !(element instanceof HTMLSelectElement)
  ) {
    return true;
  }

  if (element instanceof HTMLSelectElement) {
    return true;
  }

  if (
    element instanceof HTMLInputElement &&
    (element.type === "checkbox" ||
      element.type === "radio" ||
      element.type === "file")
  ) {
    return true;
  }

  const probe = element.cloneNode(false) as
    | HTMLInputElement
    | HTMLTextAreaElement;

  try {
    probe.value = value;
    return probe.checkValidity();
  } catch {
    return false;
  }
}