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 });