/ src / browser / target-errors.test.ts
target-errors.test.ts
  1  import { describe, expect, it } from 'vitest';
  2  import { TargetError } from './target-errors.js';
  3  
  4  describe('TargetError', () => {
  5    it('creates not_found error with code and hint', () => {
  6      const err = new TargetError({
  7        code: 'not_found',
  8        message: 'ref=99 not found in DOM',
  9        hint: 'Re-run `opencli browser state` to get a fresh snapshot.',
 10      });
 11  
 12      expect(err).toBeInstanceOf(Error);
 13      expect(err.name).toBe('TargetError');
 14      expect(err.code).toBe('not_found');
 15      expect(err.message).toBe('ref=99 not found in DOM');
 16      expect(err.hint).toContain('fresh snapshot');
 17      expect(err.candidates).toBeUndefined();
 18    });
 19  
 20    it('creates selector_ambiguous error with candidates + matches_n', () => {
 21      const err = new TargetError({
 22        code: 'selector_ambiguous',
 23        message: 'CSS selector ".btn" matched 3 elements',
 24        hint: 'Use a more specific selector, or pass --nth.',
 25        candidates: ['<button> "Login"', '<button> "Sign Up"', '<button> "Cancel"'],
 26        matches_n: 3,
 27      });
 28  
 29      expect(err.code).toBe('selector_ambiguous');
 30      expect(err.candidates).toHaveLength(3);
 31      expect(err.candidates![0]).toContain('Login');
 32      expect(err.matches_n).toBe(3);
 33    });
 34  
 35    it('creates invalid_selector error', () => {
 36      const err = new TargetError({
 37        code: 'invalid_selector',
 38        message: 'Invalid CSS selector: >>> (unexpected token)',
 39        hint: 'Check the selector syntax.',
 40      });
 41  
 42      expect(err.code).toBe('invalid_selector');
 43      expect(err.message).toContain('Invalid CSS selector');
 44    });
 45  
 46    it('creates selector_not_found error with matches_n=0', () => {
 47      const err = new TargetError({
 48        code: 'selector_not_found',
 49        message: 'CSS selector ".missing" matched 0 elements',
 50        hint: 'Check the page or use browser find.',
 51        matches_n: 0,
 52      });
 53  
 54      expect(err.code).toBe('selector_not_found');
 55      expect(err.matches_n).toBe(0);
 56    });
 57  
 58    it('creates selector_nth_out_of_range error', () => {
 59      const err = new TargetError({
 60        code: 'selector_nth_out_of_range',
 61        message: 'matched 3 elements, but --nth=5 is out of range',
 62        hint: 'Use --nth between 0 and 2.',
 63        matches_n: 3,
 64      });
 65  
 66      expect(err.code).toBe('selector_nth_out_of_range');
 67      expect(err.matches_n).toBe(3);
 68    });
 69  
 70    it('creates stale_ref error', () => {
 71      const err = new TargetError({
 72        code: 'stale_ref',
 73        message: 'ref=12 was <button>"Login" but now points to <div>"Header"',
 74        hint: 'Re-run `opencli browser state` to refresh.',
 75      });
 76  
 77      expect(err.code).toBe('stale_ref');
 78      expect(err.message).toContain('was <button>');
 79    });
 80  
 81    it('serializes to JSON for structured output', () => {
 82      const err = new TargetError({
 83        code: 'selector_ambiguous',
 84        message: 'matched 3',
 85        hint: 'be specific',
 86        candidates: ['a', 'b'],
 87        matches_n: 3,
 88      });
 89  
 90      const json = err.toJSON();
 91      expect(json).toEqual({
 92        code: 'selector_ambiguous',
 93        message: 'matched 3',
 94        hint: 'be specific',
 95        candidates: ['a', 'b'],
 96        matches_n: 3,
 97      });
 98    });
 99  
100    it('omits candidates from JSON when not present', () => {
101      const err = new TargetError({
102        code: 'not_found',
103        message: 'gone',
104        hint: 'refresh',
105      });
106  
107      const json = err.toJSON();
108      expect(json).not.toHaveProperty('candidates');
109    });
110  });