File size: 3,710 Bytes
d786697
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// 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');
}