// Compile a small, explicit workflow. Natural-language planning stays with the host. const FIELD_OPS = new Set(['fill', 'select', 'check']); const target = value => typeof value === 'string' ? {name: value} : value; export function validateTarget(value) { const t = target(value); if (!t || typeof t !== 'object' || Array.isArray(t) || !(typeof t.name === 'string' && t.name.trim() || typeof t.selector === 'string' && t.selector.trim())) throw Error('A target needs an exact accessible name or selector'); for (const key of ['role', 'within']) if (t[key] !== undefined && typeof t[key] !== 'string') throw Error('Invalid target ' + key); return t; } export class Workflow { constructor(task) { if (!Array.isArray(task.steps) || !task.steps.length || task.steps.length > 100) throw Error('Expected 1–100 workflow steps'); this.steps = task.steps.map((step, index) => { const ops = Object.keys(step).filter(k => ['click', ...FIELD_OPS].includes(k)); if (ops.length !== 1 || Object.keys(step).some(k => ![ops[0], 'purpose', 'expect'].includes(k))) throw Error('Each step needs exactly one supported operation'); const op = ops[0], source = step[op]; if (op === 'click') { const t = validateTarget(source); return [{id: `step-${index}-click`, op, target: t, label: t.name ?? t.selector, purpose: step.purpose ?? (index === task.steps.length - 1 ? 'submit' : 'interaction'), expect: step.expect}]; } if (!source || typeof source !== 'object' || !Object.keys(source).length) throw Error('Field step needs a nonempty mapping or target/value list'); const entries = Array.isArray(source) ? source : Object.entries(source).map(([name, value]) => ({target: {name}, value})); return entries.map(({target: suppliedTarget, value}, i) => { const t = validateTarget(suppliedTarget); if (op === 'check' ? typeof value !== 'boolean' : !(typeof value === 'string' || typeof value === 'number' || op === 'select' && Array.isArray(value) && value.every(x => typeof x === 'string'))) throw Error('Invalid assigned field value'); return {id: `step-${index}-field-${i}`, op: op === 'check' ? value ? 'check' : 'uncheck' : op, target: t, label: t.name ?? t.selector, value, purpose: 'input', selectByLabel: op === 'select'}; }); }); this.index = 0; this.completed = []; } get finished() { return this.index >= this.steps.length; } controls() { return this.finished ? [] : this.steps[this.index]; } advance(observation) { if (this.finished || this.controls()[0].op === 'click') return false; const controls = observation.dom.controls; if (controls.length !== this.controls().length || !controls.every(c => c.count === 1 && c.current === 'match')) return false; this.completed.push({step: this.index + 1, operation: 'fields', fields: controls.map(c => ({name: c.label, matched: true}))}); this.index++; return true; } acted(action) { if (!this.finished && action.op === 'click' && this.controls()[0].op === 'click') { this.completed.push({step: this.index + 1, operation: 'click', name: action.label, executed: true}); this.index++; } } } export function workflowSuccess(expect) { if (!expect || typeof expect !== 'object') throw Error('An explicit final expectation is required'); if (typeof expect.visibleText === 'string' && expect.visibleText.trim()) return {kind: 'visibleText', value: expect.visibleText}; if (expect.target) return {kind: 'visible', target: validateTarget(expect.target)}; if (expect.kind) return expect; throw Error('Use expect.visibleText, expect.target or a documented condition'); }