btcp-browser-agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +230 -0
- package/LICENSE +21 -0
- package/README.md +309 -0
- package/SKILL.md +143 -0
- package/SNAPSHOT_IMPROVEMENTS.md +302 -0
- package/USAGE.md +146 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/docs/browser-cli-design.md +500 -0
- package/examples/chrome-extension/CHANGELOG.md +210 -0
- package/examples/chrome-extension/DEBUG.md +231 -0
- package/examples/chrome-extension/ERROR_FIXED.md +147 -0
- package/examples/chrome-extension/QUICK_TEST.md +189 -0
- package/examples/chrome-extension/README.md +149 -0
- package/examples/chrome-extension/SESSION_ONLY_MODE.md +305 -0
- package/examples/chrome-extension/TEST_WITH_YOUR_TABS.md +97 -0
- package/examples/chrome-extension/build.js +43 -0
- package/examples/chrome-extension/manifest.json +37 -0
- package/examples/chrome-extension/package-lock.json +1063 -0
- package/examples/chrome-extension/package.json +21 -0
- package/examples/chrome-extension/popup.html +195 -0
- package/examples/chrome-extension/src/background.ts +12 -0
- package/examples/chrome-extension/src/content.ts +7 -0
- package/examples/chrome-extension/src/popup.ts +303 -0
- package/examples/chrome-extension/src/scenario-google-github.ts +389 -0
- package/examples/chrome-extension/test-page.html +127 -0
- package/examples/chrome-extension/tests/README.md +206 -0
- package/examples/chrome-extension/tests/scenario-google-to-github-star.ts +380 -0
- package/examples/chrome-extension/tsconfig.json +14 -0
- package/examples/snapshots/README.md +207 -0
- package/examples/snapshots/amazon-com-detail.html +9528 -0
- package/examples/snapshots/amazon-com-detail.snapshot.txt +997 -0
- package/examples/snapshots/convert-snapshots.ts +97 -0
- package/examples/snapshots/edition-cnn-com.html +13292 -0
- package/examples/snapshots/edition-cnn-com.snapshot.txt +562 -0
- package/examples/snapshots/github-com-microsoft-vscode.html +2916 -0
- package/examples/snapshots/github-com-microsoft-vscode.snapshot.txt +455 -0
- package/examples/snapshots/google-search.html +20012 -0
- package/examples/snapshots/google-search.snapshot.txt +195 -0
- package/examples/snapshots/metadata.json +86 -0
- package/examples/snapshots/npr-org-templates.html +2031 -0
- package/examples/snapshots/npr-org-templates.snapshot.txt +224 -0
- package/examples/snapshots/stackoverflow-com.html +5216 -0
- package/examples/snapshots/stackoverflow-com.snapshot.txt +2404 -0
- package/examples/snapshots/test-all-mode.html +46 -0
- package/examples/snapshots/test-all-mode.snapshot.txt +5 -0
- package/examples/snapshots/validate.test.ts +296 -0
- package/package.json +65 -0
- package/packages/cli/package.json +42 -0
- package/packages/cli/src/__tests__/cli.test.ts +434 -0
- package/packages/cli/src/__tests__/errors.test.ts +226 -0
- package/packages/cli/src/__tests__/executor.test.ts +275 -0
- package/packages/cli/src/__tests__/formatter.test.ts +260 -0
- package/packages/cli/src/__tests__/parser.test.ts +288 -0
- package/packages/cli/src/__tests__/suggestions.test.ts +255 -0
- package/packages/cli/src/commands/back.ts +22 -0
- package/packages/cli/src/commands/check.ts +33 -0
- package/packages/cli/src/commands/clear.ts +33 -0
- package/packages/cli/src/commands/click.ts +32 -0
- package/packages/cli/src/commands/closetab.ts +31 -0
- package/packages/cli/src/commands/eval.ts +41 -0
- package/packages/cli/src/commands/fill.ts +30 -0
- package/packages/cli/src/commands/focus.ts +33 -0
- package/packages/cli/src/commands/forward.ts +22 -0
- package/packages/cli/src/commands/goto.ts +34 -0
- package/packages/cli/src/commands/help.ts +162 -0
- package/packages/cli/src/commands/hover.ts +34 -0
- package/packages/cli/src/commands/index.ts +129 -0
- package/packages/cli/src/commands/newtab.ts +35 -0
- package/packages/cli/src/commands/press.ts +40 -0
- package/packages/cli/src/commands/reload.ts +25 -0
- package/packages/cli/src/commands/screenshot.ts +27 -0
- package/packages/cli/src/commands/scroll.ts +64 -0
- package/packages/cli/src/commands/select.ts +35 -0
- package/packages/cli/src/commands/snapshot.ts +21 -0
- package/packages/cli/src/commands/tab.ts +32 -0
- package/packages/cli/src/commands/tabs.ts +26 -0
- package/packages/cli/src/commands/text.ts +27 -0
- package/packages/cli/src/commands/title.ts +17 -0
- package/packages/cli/src/commands/type.ts +38 -0
- package/packages/cli/src/commands/uncheck.ts +33 -0
- package/packages/cli/src/commands/url.ts +17 -0
- package/packages/cli/src/commands/wait.ts +54 -0
- package/packages/cli/src/errors.ts +164 -0
- package/packages/cli/src/executor.ts +68 -0
- package/packages/cli/src/formatter.ts +215 -0
- package/packages/cli/src/index.ts +257 -0
- package/packages/cli/src/parser.ts +195 -0
- package/packages/cli/src/suggestions.ts +207 -0
- package/packages/cli/src/terminal/Terminal.ts +365 -0
- package/packages/cli/src/terminal/index.ts +5 -0
- package/packages/cli/src/types.ts +155 -0
- package/packages/cli/tsconfig.json +20 -0
- package/packages/core/package.json +35 -0
- package/packages/core/src/actions.ts +1210 -0
- package/packages/core/src/errors.ts +296 -0
- package/packages/core/src/index.test.ts +638 -0
- package/packages/core/src/index.ts +220 -0
- package/packages/core/src/ref-map.ts +107 -0
- package/packages/core/src/snapshot.ts +873 -0
- package/packages/core/src/types.ts +536 -0
- package/packages/core/tsconfig.json +23 -0
- package/packages/extension/README.md +129 -0
- package/packages/extension/package.json +43 -0
- package/packages/extension/src/background.ts +888 -0
- package/packages/extension/src/content.ts +172 -0
- package/packages/extension/src/index.ts +579 -0
- package/packages/extension/src/session-manager.ts +385 -0
- package/packages/extension/src/session-types.ts +144 -0
- package/packages/extension/src/types.ts +162 -0
- package/packages/extension/tsconfig.json +28 -0
- package/src/index.ts +64 -0
- package/tsconfig.build.json +12 -0
- package/tsconfig.json +26 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import {
|
|
7
|
+
parseCommand,
|
|
8
|
+
getFlagString,
|
|
9
|
+
getFlagNumber,
|
|
10
|
+
getFlagBool,
|
|
11
|
+
} from '../parser.js';
|
|
12
|
+
import { ParseError } from '../errors.js';
|
|
13
|
+
|
|
14
|
+
describe('parseCommand', () => {
|
|
15
|
+
describe('basic parsing', () => {
|
|
16
|
+
it('parses a simple command', () => {
|
|
17
|
+
const result = parseCommand('goto');
|
|
18
|
+
expect(result.name).toBe('goto');
|
|
19
|
+
expect(result.args).toEqual([]);
|
|
20
|
+
expect(result.flags).toEqual({});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('parses command with one argument', () => {
|
|
24
|
+
const result = parseCommand('goto https://example.com');
|
|
25
|
+
expect(result.name).toBe('goto');
|
|
26
|
+
expect(result.args).toEqual(['https://example.com']);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('parses command with multiple arguments', () => {
|
|
30
|
+
const result = parseCommand('type @ref:1 hello world');
|
|
31
|
+
expect(result.name).toBe('type');
|
|
32
|
+
expect(result.args).toEqual(['@ref:1', 'hello', 'world']);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('converts command name to lowercase', () => {
|
|
36
|
+
const result = parseCommand('GOTO https://example.com');
|
|
37
|
+
expect(result.name).toBe('goto');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('preserves original input in raw field', () => {
|
|
41
|
+
const input = 'goto https://example.com';
|
|
42
|
+
const result = parseCommand(input);
|
|
43
|
+
expect(result.raw).toBe(input);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('trims whitespace', () => {
|
|
47
|
+
const result = parseCommand(' goto https://example.com ');
|
|
48
|
+
expect(result.name).toBe('goto');
|
|
49
|
+
expect(result.args).toEqual(['https://example.com']);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('quoted strings', () => {
|
|
54
|
+
it('parses double-quoted strings', () => {
|
|
55
|
+
const result = parseCommand('type @ref:1 "hello world"');
|
|
56
|
+
expect(result.args).toEqual(['@ref:1', 'hello world']);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('parses single-quoted strings', () => {
|
|
60
|
+
const result = parseCommand("type @ref:1 'hello world'");
|
|
61
|
+
expect(result.args).toEqual(['@ref:1', 'hello world']);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('handles empty quoted strings (parsed but filtered as empty token)', () => {
|
|
65
|
+
// Note: empty quoted strings result in empty tokens which are filtered
|
|
66
|
+
const result = parseCommand('fill @ref:1 ""');
|
|
67
|
+
expect(result.args).toEqual(['@ref:1']);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('preserves spaces in quoted strings', () => {
|
|
71
|
+
const result = parseCommand('type @ref:1 "hello world"');
|
|
72
|
+
expect(result.args).toEqual(['@ref:1', 'hello world']);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('handles quotes within different quotes', () => {
|
|
76
|
+
const result = parseCommand('type @ref:1 "it\'s working"');
|
|
77
|
+
expect(result.args).toEqual(['@ref:1', "it's working"]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('throws on unterminated double quote', () => {
|
|
81
|
+
expect(() => parseCommand('type @ref:1 "hello')).toThrow(ParseError);
|
|
82
|
+
expect(() => parseCommand('type @ref:1 "hello')).toThrow('Unterminated double quote');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('throws on unterminated single quote', () => {
|
|
86
|
+
expect(() => parseCommand("type @ref:1 'hello")).toThrow(ParseError);
|
|
87
|
+
expect(() => parseCommand("type @ref:1 'hello")).toThrow('Unterminated single quote');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('escape characters', () => {
|
|
92
|
+
it('handles escaped quotes', () => {
|
|
93
|
+
const result = parseCommand('type @ref:1 "hello \\"world\\""');
|
|
94
|
+
expect(result.args).toEqual(['@ref:1', 'hello "world"']);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('handles escaped backslash', () => {
|
|
98
|
+
const result = parseCommand('type @ref:1 "path\\\\file"');
|
|
99
|
+
expect(result.args).toEqual(['@ref:1', 'path\\file']);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('flags', () => {
|
|
104
|
+
it('parses long flag with value', () => {
|
|
105
|
+
const result = parseCommand('wait @ref:1 --state visible');
|
|
106
|
+
expect(result.flags).toEqual({ state: 'visible' });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('parses long flag with equals syntax', () => {
|
|
110
|
+
const result = parseCommand('wait @ref:1 --state=visible');
|
|
111
|
+
expect(result.flags).toEqual({ state: 'visible' });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('parses boolean long flag', () => {
|
|
115
|
+
const result = parseCommand('reload --hard');
|
|
116
|
+
expect(result.flags).toEqual({ hard: true });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('parses short flag with value', () => {
|
|
120
|
+
const result = parseCommand('screenshot -f png');
|
|
121
|
+
expect(result.flags).toEqual({ f: 'png' });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('parses boolean short flag', () => {
|
|
125
|
+
const result = parseCommand('reload -h');
|
|
126
|
+
expect(result.flags).toEqual({ h: true });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('parses multiple flags', () => {
|
|
130
|
+
const result = parseCommand('screenshot --format png --quality 80');
|
|
131
|
+
expect(result.flags).toEqual({ format: 'png', quality: '80' });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('separates args and flags correctly', () => {
|
|
135
|
+
const result = parseCommand('click @ref:1 --button right');
|
|
136
|
+
expect(result.args).toEqual(['@ref:1']);
|
|
137
|
+
expect(result.flags).toEqual({ button: 'right' });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('handles flag value with equals sign', () => {
|
|
141
|
+
const result = parseCommand('eval --script=a=b');
|
|
142
|
+
expect(result.flags).toEqual({ script: 'a=b' });
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('edge cases', () => {
|
|
147
|
+
it('throws on empty input', () => {
|
|
148
|
+
expect(() => parseCommand('')).toThrow(ParseError);
|
|
149
|
+
expect(() => parseCommand('')).toThrow('Empty command');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('throws on whitespace-only input', () => {
|
|
153
|
+
expect(() => parseCommand(' ')).toThrow(ParseError);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('handles tabs as whitespace', () => {
|
|
157
|
+
const result = parseCommand('goto\thttps://example.com');
|
|
158
|
+
expect(result.name).toBe('goto');
|
|
159
|
+
expect(result.args).toEqual(['https://example.com']);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('handles URLs with special characters', () => {
|
|
163
|
+
const result = parseCommand('goto https://example.com/path?query=value&foo=bar');
|
|
164
|
+
expect(result.args).toEqual(['https://example.com/path?query=value&foo=bar']);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('handles @ref selectors', () => {
|
|
168
|
+
const result = parseCommand('click @ref:123');
|
|
169
|
+
expect(result.args).toEqual(['@ref:123']);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('handles CSS selectors', () => {
|
|
173
|
+
const result = parseCommand('click #submit-button');
|
|
174
|
+
expect(result.args).toEqual(['#submit-button']);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('handles class selectors', () => {
|
|
178
|
+
const result = parseCommand('click .btn.primary');
|
|
179
|
+
expect(result.args).toEqual(['.btn.primary']);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('getFlagString', () => {
|
|
185
|
+
it('returns string value for string flag', () => {
|
|
186
|
+
const flags = { format: 'png' };
|
|
187
|
+
expect(getFlagString(flags, 'format')).toBe('png');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('returns undefined for missing flag', () => {
|
|
191
|
+
const flags = {};
|
|
192
|
+
expect(getFlagString(flags, 'format')).toBeUndefined();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('returns default for missing flag', () => {
|
|
196
|
+
const flags = {};
|
|
197
|
+
expect(getFlagString(flags, 'format', 'jpeg')).toBe('jpeg');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('returns default for boolean flag', () => {
|
|
201
|
+
const flags = { format: true };
|
|
202
|
+
expect(getFlagString(flags, 'format', 'jpeg')).toBe('jpeg');
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('getFlagNumber', () => {
|
|
207
|
+
it('returns number for numeric string flag', () => {
|
|
208
|
+
const flags = { quality: '80' };
|
|
209
|
+
expect(getFlagNumber(flags, 'quality')).toBe(80);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('returns undefined for missing flag', () => {
|
|
213
|
+
const flags = {};
|
|
214
|
+
expect(getFlagNumber(flags, 'quality')).toBeUndefined();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('returns default for missing flag', () => {
|
|
218
|
+
const flags = {};
|
|
219
|
+
expect(getFlagNumber(flags, 'quality', 100)).toBe(100);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('returns default for non-numeric string', () => {
|
|
223
|
+
const flags = { quality: 'high' };
|
|
224
|
+
expect(getFlagNumber(flags, 'quality', 100)).toBe(100);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('returns default for boolean flag', () => {
|
|
228
|
+
const flags = { quality: true };
|
|
229
|
+
expect(getFlagNumber(flags, 'quality', 100)).toBe(100);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('handles zero correctly', () => {
|
|
233
|
+
const flags = { offset: '0' };
|
|
234
|
+
expect(getFlagNumber(flags, 'offset')).toBe(0);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('handles negative numbers', () => {
|
|
238
|
+
const flags = { offset: '-10' };
|
|
239
|
+
expect(getFlagNumber(flags, 'offset')).toBe(-10);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('handles floating point', () => {
|
|
243
|
+
const flags = { scale: '1.5' };
|
|
244
|
+
expect(getFlagNumber(flags, 'scale')).toBe(1.5);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe('getFlagBool', () => {
|
|
249
|
+
it('returns true for boolean true flag', () => {
|
|
250
|
+
const flags = { hard: true };
|
|
251
|
+
expect(getFlagBool(flags, 'hard')).toBe(true);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('returns false for boolean false flag', () => {
|
|
255
|
+
const flags = { hard: false };
|
|
256
|
+
expect(getFlagBool(flags, 'hard')).toBe(false);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('returns false for missing flag by default', () => {
|
|
260
|
+
const flags = {};
|
|
261
|
+
expect(getFlagBool(flags, 'hard')).toBe(false);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('returns default for missing flag', () => {
|
|
265
|
+
const flags = {};
|
|
266
|
+
expect(getFlagBool(flags, 'hard', true)).toBe(true);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('returns true for string "true"', () => {
|
|
270
|
+
const flags = { hard: 'true' };
|
|
271
|
+
expect(getFlagBool(flags, 'hard')).toBe(true);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('returns true for string "1"', () => {
|
|
275
|
+
const flags = { hard: '1' };
|
|
276
|
+
expect(getFlagBool(flags, 'hard')).toBe(true);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('returns false for other strings', () => {
|
|
280
|
+
const flags = { hard: 'yes' };
|
|
281
|
+
expect(getFlagBool(flags, 'hard')).toBe(false);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('is case insensitive for "true"', () => {
|
|
285
|
+
const flags = { hard: 'TRUE' };
|
|
286
|
+
expect(getFlagBool(flags, 'hard')).toBe(true);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Suggestions tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import {
|
|
7
|
+
findSimilarCommands,
|
|
8
|
+
getContextualSuggestion,
|
|
9
|
+
commandCategories,
|
|
10
|
+
getCommandCategory,
|
|
11
|
+
getNextStepSuggestions,
|
|
12
|
+
} from '../suggestions.js';
|
|
13
|
+
|
|
14
|
+
describe('findSimilarCommands', () => {
|
|
15
|
+
describe('exact prefix matching', () => {
|
|
16
|
+
it('finds commands starting with input', () => {
|
|
17
|
+
const results = findSimilarCommands('go');
|
|
18
|
+
expect(results).toContain('goto');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('finds commands starting with input (tabs)', () => {
|
|
22
|
+
const results = findSimilarCommands('tab');
|
|
23
|
+
expect(results).toContain('tabs');
|
|
24
|
+
expect(results).toContain('tab');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('prioritizes prefix matches', () => {
|
|
28
|
+
const results = findSimilarCommands('scr');
|
|
29
|
+
expect(results[0]).toBe('scroll');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('typo correction', () => {
|
|
34
|
+
it('suggests click for clck', () => {
|
|
35
|
+
const results = findSimilarCommands('clck');
|
|
36
|
+
expect(results).toContain('click');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('suggests goto for gto', () => {
|
|
40
|
+
const results = findSimilarCommands('gto');
|
|
41
|
+
expect(results).toContain('goto');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('suggests snapshot for snapshto', () => {
|
|
45
|
+
const results = findSimilarCommands('snapshto');
|
|
46
|
+
expect(results).toContain('snapshot');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('suggests type for typ', () => {
|
|
50
|
+
const results = findSimilarCommands('typ');
|
|
51
|
+
expect(results).toContain('type');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('suggests fill for fil', () => {
|
|
55
|
+
const results = findSimilarCommands('fil');
|
|
56
|
+
expect(results).toContain('fill');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('suggests reload for relod', () => {
|
|
60
|
+
const results = findSimilarCommands('relod');
|
|
61
|
+
expect(results).toContain('reload');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('substring matching', () => {
|
|
66
|
+
it('finds commands containing input', () => {
|
|
67
|
+
const results = findSimilarCommands('shot');
|
|
68
|
+
expect(results).toContain('screenshot');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('finds check in commands', () => {
|
|
72
|
+
const results = findSimilarCommands('check');
|
|
73
|
+
expect(results).toContain('check');
|
|
74
|
+
expect(results).toContain('uncheck');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('limits', () => {
|
|
79
|
+
it('returns at most maxSuggestions results', () => {
|
|
80
|
+
const results = findSimilarCommands('t', 2);
|
|
81
|
+
expect(results.length).toBeLessThanOrEqual(2);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('returns empty for completely unrelated input', () => {
|
|
85
|
+
const results = findSimilarCommands('xyzabc123');
|
|
86
|
+
expect(results).toHaveLength(0);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('case insensitivity', () => {
|
|
91
|
+
it('matches regardless of case', () => {
|
|
92
|
+
const results = findSimilarCommands('GOTO');
|
|
93
|
+
expect(results).toContain('goto');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('matches mixed case', () => {
|
|
97
|
+
const results = findSimilarCommands('GoTo');
|
|
98
|
+
expect(results).toContain('goto');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('getContextualSuggestion', () => {
|
|
104
|
+
describe('element not found', () => {
|
|
105
|
+
it('provides suggestion for "not found" error', () => {
|
|
106
|
+
const suggestion = getContextualSuggestion('click', 'Element not found', ['@ref:5']);
|
|
107
|
+
expect(suggestion).toContain('snapshot');
|
|
108
|
+
expect(suggestion).toContain('@ref:5');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('provides suggestion for "no element" error', () => {
|
|
112
|
+
const suggestion = getContextualSuggestion('click', 'No element matches selector', ['#btn']);
|
|
113
|
+
expect(suggestion).toContain('snapshot');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('invalid selector', () => {
|
|
118
|
+
it('provides suggestion for selector errors', () => {
|
|
119
|
+
const suggestion = getContextualSuggestion('click', 'Invalid selector', ['>>>']);
|
|
120
|
+
expect(suggestion).toContain('@ref:5');
|
|
121
|
+
expect(suggestion).toContain('#id');
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('navigation errors', () => {
|
|
126
|
+
it('provides suggestion for navigation failures', () => {
|
|
127
|
+
const suggestion = getContextualSuggestion('goto', 'Failed to navigate', ['example.com']);
|
|
128
|
+
expect(suggestion).toContain('https://');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('provides suggestion for URL errors', () => {
|
|
132
|
+
// Note: 'invalid' pattern matches first, giving selector advice
|
|
133
|
+
// Use 'navigate' or 'url' pattern for navigation-specific advice
|
|
134
|
+
const suggestion = getContextualSuggestion('goto', 'Failed to navigate to URL', ['not-a-url']);
|
|
135
|
+
expect(suggestion).toContain('https://');
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('timeout errors', () => {
|
|
140
|
+
it('provides suggestion for timeout', () => {
|
|
141
|
+
const suggestion = getContextualSuggestion('wait', 'Timeout waiting for element', ['@ref:5']);
|
|
142
|
+
expect(suggestion).toContain('wait');
|
|
143
|
+
expect(suggestion).toContain('@ref:5');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('permission errors', () => {
|
|
148
|
+
it('provides suggestion for permission denied', () => {
|
|
149
|
+
const suggestion = getContextualSuggestion('click', 'Permission denied', ['#btn']);
|
|
150
|
+
expect(suggestion).toContain('Cross-origin');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('provides suggestion for blocked actions', () => {
|
|
154
|
+
const suggestion = getContextualSuggestion('type', 'Action blocked by security policy', ['#input']);
|
|
155
|
+
expect(suggestion).toContain('iframe');
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('no suggestion', () => {
|
|
160
|
+
it('returns null for unknown errors', () => {
|
|
161
|
+
const suggestion = getContextualSuggestion('click', 'Some random error', ['@ref:1']);
|
|
162
|
+
expect(suggestion).toBeNull();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('commandCategories', () => {
|
|
168
|
+
it('has navigation category', () => {
|
|
169
|
+
expect(commandCategories.navigation).toBeDefined();
|
|
170
|
+
expect(commandCategories.navigation.commands).toContain('goto');
|
|
171
|
+
expect(commandCategories.navigation.commands).toContain('back');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('has inspection category', () => {
|
|
175
|
+
expect(commandCategories.inspection).toBeDefined();
|
|
176
|
+
expect(commandCategories.inspection.commands).toContain('snapshot');
|
|
177
|
+
expect(commandCategories.inspection.commands).toContain('screenshot');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('has interaction category', () => {
|
|
181
|
+
expect(commandCategories.interaction).toBeDefined();
|
|
182
|
+
expect(commandCategories.interaction.commands).toContain('click');
|
|
183
|
+
expect(commandCategories.interaction.commands).toContain('type');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('has forms category', () => {
|
|
187
|
+
expect(commandCategories.forms).toBeDefined();
|
|
188
|
+
expect(commandCategories.forms.commands).toContain('check');
|
|
189
|
+
expect(commandCategories.forms.commands).toContain('select');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('has tabs category', () => {
|
|
193
|
+
expect(commandCategories.tabs).toBeDefined();
|
|
194
|
+
expect(commandCategories.tabs.commands).toContain('tabs');
|
|
195
|
+
expect(commandCategories.tabs.commands).toContain('newtab');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('has utility category', () => {
|
|
199
|
+
expect(commandCategories.utility).toBeDefined();
|
|
200
|
+
expect(commandCategories.utility.commands).toContain('wait');
|
|
201
|
+
expect(commandCategories.utility.commands).toContain('help');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('each category has name and description', () => {
|
|
205
|
+
for (const category of Object.values(commandCategories)) {
|
|
206
|
+
expect(category.name).toBeDefined();
|
|
207
|
+
expect(category.description).toBeDefined();
|
|
208
|
+
expect(category.commands.length).toBeGreaterThan(0);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe('getCommandCategory', () => {
|
|
214
|
+
it('returns correct category for navigation commands', () => {
|
|
215
|
+
expect(getCommandCategory('goto')).toBe('navigation');
|
|
216
|
+
expect(getCommandCategory('back')).toBe('navigation');
|
|
217
|
+
expect(getCommandCategory('reload')).toBe('navigation');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('returns correct category for interaction commands', () => {
|
|
221
|
+
expect(getCommandCategory('click')).toBe('interaction');
|
|
222
|
+
expect(getCommandCategory('type')).toBe('interaction');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('returns correct category for inspection commands', () => {
|
|
226
|
+
expect(getCommandCategory('snapshot')).toBe('inspection');
|
|
227
|
+
expect(getCommandCategory('screenshot')).toBe('inspection');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('returns null for unknown commands', () => {
|
|
231
|
+
expect(getCommandCategory('unknown')).toBeNull();
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe('getNextStepSuggestions', () => {
|
|
236
|
+
it('suggests snapshot after navigation', () => {
|
|
237
|
+
const suggestions = getNextStepSuggestions('goto');
|
|
238
|
+
expect(suggestions).toContain('snapshot # See page structure and get element refs');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('suggests actions after snapshot', () => {
|
|
242
|
+
const suggestions = getNextStepSuggestions('snapshot');
|
|
243
|
+
expect(suggestions.some((s) => s.includes('click'))).toBe(true);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('suggests snapshot after click', () => {
|
|
247
|
+
const suggestions = getNextStepSuggestions('click');
|
|
248
|
+
expect(suggestions.some((s) => s.includes('snapshot'))).toBe(true);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('returns empty for unknown commands', () => {
|
|
252
|
+
const suggestions = getNextStepSuggestions('unknown');
|
|
253
|
+
expect(suggestions).toHaveLength(0);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* back command - Go back in browser history
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { CommandHandler } from '../types.js';
|
|
6
|
+
|
|
7
|
+
export const backCommand: CommandHandler = {
|
|
8
|
+
name: 'back',
|
|
9
|
+
description: 'Go back in browser history',
|
|
10
|
+
usage: 'back',
|
|
11
|
+
examples: ['back'],
|
|
12
|
+
|
|
13
|
+
async execute(client) {
|
|
14
|
+
const response = await client.back();
|
|
15
|
+
|
|
16
|
+
if (response.success) {
|
|
17
|
+
return { success: true, message: 'Navigated back' };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return { success: false, error: response.error };
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check command - Check a checkbox
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { CommandHandler } from '../types.js';
|
|
6
|
+
import { InvalidArgumentsError } from '../errors.js';
|
|
7
|
+
|
|
8
|
+
export const checkCommand: CommandHandler = {
|
|
9
|
+
name: 'check',
|
|
10
|
+
description: 'Check a checkbox',
|
|
11
|
+
usage: 'check <selector>',
|
|
12
|
+
examples: ['check @ref:5', 'check #agree'],
|
|
13
|
+
|
|
14
|
+
async execute(client, args) {
|
|
15
|
+
if (args.length === 0) {
|
|
16
|
+
throw new InvalidArgumentsError('Selector required', 'check <selector>');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const selector = args[0];
|
|
20
|
+
|
|
21
|
+
const response = await client.execute({
|
|
22
|
+
id: `cmd_${Date.now()}`,
|
|
23
|
+
action: 'check',
|
|
24
|
+
selector,
|
|
25
|
+
} as any);
|
|
26
|
+
|
|
27
|
+
if (response.success) {
|
|
28
|
+
return { success: true, message: `Checked: ${selector}` };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return { success: false, error: response.error };
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* clear command - Clear an input field
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { CommandHandler } from '../types.js';
|
|
6
|
+
import { InvalidArgumentsError } from '../errors.js';
|
|
7
|
+
|
|
8
|
+
export const clearCommand: CommandHandler = {
|
|
9
|
+
name: 'clear',
|
|
10
|
+
description: 'Clear an input field',
|
|
11
|
+
usage: 'clear <selector>',
|
|
12
|
+
examples: ['clear @ref:1', 'clear #search'],
|
|
13
|
+
|
|
14
|
+
async execute(client, args) {
|
|
15
|
+
if (args.length === 0) {
|
|
16
|
+
throw new InvalidArgumentsError('Selector required', 'clear <selector>');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const selector = args[0];
|
|
20
|
+
|
|
21
|
+
const response = await client.execute({
|
|
22
|
+
id: `cmd_${Date.now()}`,
|
|
23
|
+
action: 'clear',
|
|
24
|
+
selector,
|
|
25
|
+
} as any);
|
|
26
|
+
|
|
27
|
+
if (response.success) {
|
|
28
|
+
return { success: true, message: `Cleared: ${selector}` };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return { success: false, error: response.error };
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* click command - Click an element
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { CommandHandler } from '../types.js';
|
|
6
|
+
import { InvalidArgumentsError } from '../errors.js';
|
|
7
|
+
import { getFlagString } from '../parser.js';
|
|
8
|
+
|
|
9
|
+
export const clickCommand: CommandHandler = {
|
|
10
|
+
name: 'click',
|
|
11
|
+
description: 'Click an element',
|
|
12
|
+
usage: 'click <selector> [--button left|right|middle]',
|
|
13
|
+
examples: ['click @ref:5', 'click #submit', 'click @ref:3 --button right'],
|
|
14
|
+
|
|
15
|
+
async execute(client, args, flags) {
|
|
16
|
+
if (args.length === 0) {
|
|
17
|
+
throw new InvalidArgumentsError('Selector required', 'click <selector>');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const selector = args[0];
|
|
21
|
+
const buttonFlag = getFlagString(flags, 'button');
|
|
22
|
+
const button = buttonFlag as 'left' | 'right' | 'middle' | undefined;
|
|
23
|
+
|
|
24
|
+
const response = await client.click(selector, { button });
|
|
25
|
+
|
|
26
|
+
if (response.success) {
|
|
27
|
+
return { success: true, message: `Clicked: ${selector}` };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return { success: false, error: response.error };
|
|
31
|
+
},
|
|
32
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* closetab command - Close a tab
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { CommandHandler } from '../types.js';
|
|
6
|
+
|
|
7
|
+
export const closetabCommand: CommandHandler = {
|
|
8
|
+
name: 'closetab',
|
|
9
|
+
description: 'Close a tab (current tab if no ID specified)',
|
|
10
|
+
usage: 'closetab [id]',
|
|
11
|
+
examples: ['closetab', 'closetab 123'],
|
|
12
|
+
|
|
13
|
+
async execute(client, args) {
|
|
14
|
+
const tabId = args[0] ? parseInt(args[0], 10) : undefined;
|
|
15
|
+
|
|
16
|
+
if (args[0] && isNaN(tabId!)) {
|
|
17
|
+
return { success: false, error: 'Invalid tab ID' };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const response = await client.tabClose(tabId);
|
|
21
|
+
|
|
22
|
+
if (response.success) {
|
|
23
|
+
return {
|
|
24
|
+
success: true,
|
|
25
|
+
message: tabId ? `Closed tab ${tabId}` : 'Closed current tab',
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return { success: false, error: response.error };
|
|
30
|
+
},
|
|
31
|
+
};
|