browser-commander 0.2.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.
Files changed (82) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +11 -0
  3. package/.github/workflows/release.yml +296 -0
  4. package/.husky/pre-commit +1 -0
  5. package/.jscpd.json +20 -0
  6. package/.prettierignore +7 -0
  7. package/.prettierrc +10 -0
  8. package/CHANGELOG.md +32 -0
  9. package/LICENSE +24 -0
  10. package/README.md +320 -0
  11. package/bunfig.toml +3 -0
  12. package/deno.json +7 -0
  13. package/eslint.config.js +125 -0
  14. package/examples/react-test-app/index.html +25 -0
  15. package/examples/react-test-app/package.json +19 -0
  16. package/examples/react-test-app/src/App.jsx +473 -0
  17. package/examples/react-test-app/src/main.jsx +10 -0
  18. package/examples/react-test-app/src/styles.css +323 -0
  19. package/examples/react-test-app/vite.config.js +9 -0
  20. package/package.json +89 -0
  21. package/scripts/changeset-version.mjs +38 -0
  22. package/scripts/create-github-release.mjs +93 -0
  23. package/scripts/create-manual-changeset.mjs +86 -0
  24. package/scripts/format-github-release.mjs +83 -0
  25. package/scripts/format-release-notes.mjs +216 -0
  26. package/scripts/instant-version-bump.mjs +121 -0
  27. package/scripts/merge-changesets.mjs +260 -0
  28. package/scripts/publish-to-npm.mjs +126 -0
  29. package/scripts/setup-npm.mjs +37 -0
  30. package/scripts/validate-changeset.mjs +262 -0
  31. package/scripts/version-and-commit.mjs +237 -0
  32. package/src/ARCHITECTURE.md +270 -0
  33. package/src/README.md +517 -0
  34. package/src/bindings.js +298 -0
  35. package/src/browser/launcher.js +93 -0
  36. package/src/browser/navigation.js +513 -0
  37. package/src/core/constants.js +24 -0
  38. package/src/core/engine-adapter.js +466 -0
  39. package/src/core/engine-detection.js +49 -0
  40. package/src/core/logger.js +21 -0
  41. package/src/core/navigation-manager.js +503 -0
  42. package/src/core/navigation-safety.js +160 -0
  43. package/src/core/network-tracker.js +373 -0
  44. package/src/core/page-session.js +299 -0
  45. package/src/core/page-trigger-manager.js +564 -0
  46. package/src/core/preferences.js +46 -0
  47. package/src/elements/content.js +197 -0
  48. package/src/elements/locators.js +243 -0
  49. package/src/elements/selectors.js +360 -0
  50. package/src/elements/visibility.js +166 -0
  51. package/src/exports.js +121 -0
  52. package/src/factory.js +192 -0
  53. package/src/high-level/universal-logic.js +206 -0
  54. package/src/index.js +17 -0
  55. package/src/interactions/click.js +684 -0
  56. package/src/interactions/fill.js +383 -0
  57. package/src/interactions/scroll.js +341 -0
  58. package/src/utilities/url.js +33 -0
  59. package/src/utilities/wait.js +135 -0
  60. package/tests/e2e/playwright.e2e.test.js +442 -0
  61. package/tests/e2e/puppeteer.e2e.test.js +408 -0
  62. package/tests/helpers/mocks.js +542 -0
  63. package/tests/unit/bindings.test.js +218 -0
  64. package/tests/unit/browser/navigation.test.js +345 -0
  65. package/tests/unit/core/constants.test.js +72 -0
  66. package/tests/unit/core/engine-adapter.test.js +170 -0
  67. package/tests/unit/core/engine-detection.test.js +81 -0
  68. package/tests/unit/core/logger.test.js +80 -0
  69. package/tests/unit/core/navigation-safety.test.js +202 -0
  70. package/tests/unit/core/network-tracker.test.js +198 -0
  71. package/tests/unit/core/page-trigger-manager.test.js +358 -0
  72. package/tests/unit/elements/content.test.js +318 -0
  73. package/tests/unit/elements/locators.test.js +236 -0
  74. package/tests/unit/elements/selectors.test.js +302 -0
  75. package/tests/unit/elements/visibility.test.js +234 -0
  76. package/tests/unit/factory.test.js +174 -0
  77. package/tests/unit/high-level/universal-logic.test.js +299 -0
  78. package/tests/unit/interactions/click.test.js +340 -0
  79. package/tests/unit/interactions/fill.test.js +378 -0
  80. package/tests/unit/interactions/scroll.test.js +330 -0
  81. package/tests/unit/utilities/url.test.js +63 -0
  82. package/tests/unit/utilities/wait.test.js +207 -0
@@ -0,0 +1,408 @@
1
+ /**
2
+ * E2E tests for browser-commander using Puppeteer engine
3
+ *
4
+ * Prerequisites:
5
+ * 1. Start the React test app: cd examples/react-test-app && npm install && npm run dev
6
+ * 2. Run tests: npm run test:e2e:puppeteer
7
+ */
8
+
9
+ import { describe, it, before, after } from 'node:test';
10
+ import assert from 'node:assert';
11
+
12
+ // Dynamic import for puppeteer since it may not be installed
13
+ let puppeteer;
14
+ let createCommander;
15
+
16
+ describe('E2E Tests - Puppeteer Engine', { skip: !process.env.RUN_E2E }, () => {
17
+ let browser;
18
+ let page;
19
+ let commander;
20
+ const BASE_URL = process.env.TEST_URL || 'http://localhost:3000';
21
+
22
+ before(async () => {
23
+ try {
24
+ puppeteer = await import('puppeteer');
25
+ const module = await import('../../src/index.js');
26
+ createCommander = module.createCommander;
27
+
28
+ browser = await puppeteer.default.launch({
29
+ headless: process.env.HEADLESS !== 'false' ? 'new' : false,
30
+ });
31
+ page = await browser.newPage();
32
+ commander = createCommander({ page, verbose: true });
33
+ } catch (error) {
34
+ console.log(
35
+ 'Skipping E2E tests - puppeteer not available or test app not running'
36
+ );
37
+ console.log('Error:', error.message);
38
+ }
39
+ });
40
+
41
+ after(async () => {
42
+ if (browser) {
43
+ await browser.close();
44
+ }
45
+ });
46
+
47
+ describe('Navigation', () => {
48
+ it('should navigate to test app', async () => {
49
+ if (!commander) {
50
+ return;
51
+ }
52
+
53
+ await commander.goto({ url: BASE_URL });
54
+ const url = commander.getUrl();
55
+ assert.ok(url.includes('localhost:3000') || url.includes(BASE_URL));
56
+ });
57
+
58
+ it('should get page title', async () => {
59
+ if (!commander) {
60
+ return;
61
+ }
62
+
63
+ const title = await commander.textContent({
64
+ selector: '[data-testid="page-title"]',
65
+ });
66
+ assert.strictEqual(title.trim(), 'React Test App');
67
+ });
68
+ });
69
+
70
+ describe('Form Interactions', () => {
71
+ it('should fill text input', async () => {
72
+ if (!commander) {
73
+ return;
74
+ }
75
+
76
+ await commander.fill({
77
+ selector: '[data-testid="input-name"]',
78
+ text: 'Jane Doe',
79
+ });
80
+ const value = await commander.inputValue({
81
+ selector: '[data-testid="input-name"]',
82
+ });
83
+ assert.strictEqual(value, 'Jane Doe');
84
+ });
85
+
86
+ it('should fill email input', async () => {
87
+ if (!commander) {
88
+ return;
89
+ }
90
+
91
+ await commander.fill({
92
+ selector: '[data-testid="input-email"]',
93
+ text: 'jane@example.com',
94
+ });
95
+ const value = await commander.inputValue({
96
+ selector: '[data-testid="input-email"]',
97
+ });
98
+ assert.strictEqual(value, 'jane@example.com');
99
+ });
100
+
101
+ it('should fill textarea', async () => {
102
+ if (!commander) {
103
+ return;
104
+ }
105
+
106
+ const bioText = 'This is my bio from Puppeteer test.';
107
+ await commander.fill({
108
+ selector: '[data-testid="textarea-bio"]',
109
+ text: bioText,
110
+ });
111
+ const value = await commander.inputValue({
112
+ selector: '[data-testid="textarea-bio"]',
113
+ });
114
+ assert.strictEqual(value, bioText);
115
+ });
116
+
117
+ it('should select radio button', async () => {
118
+ if (!commander) {
119
+ return;
120
+ }
121
+
122
+ await commander.click({ selector: '[data-testid="radio-female"]' });
123
+ await commander.wait({ ms: 100 });
124
+ });
125
+
126
+ it('should check multiple checkboxes', async () => {
127
+ if (!commander) {
128
+ return;
129
+ }
130
+
131
+ await commander.click({ selector: '[data-testid="checkbox-sports"]' });
132
+ await commander.click({ selector: '[data-testid="checkbox-music"]' });
133
+ await commander.wait({ ms: 100 });
134
+ });
135
+
136
+ it('should select from dropdown', async () => {
137
+ if (!commander) {
138
+ return;
139
+ }
140
+
141
+ // Using evaluate to interact with select
142
+ await commander.evaluate({
143
+ fn: () => {
144
+ const select = document.querySelector(
145
+ '[data-testid="select-country"]'
146
+ );
147
+ if (select) {
148
+ select.value = 'uk';
149
+ select.dispatchEvent(new Event('change', { bubbles: true }));
150
+ }
151
+ },
152
+ });
153
+ const value = await commander.inputValue({
154
+ selector: '[data-testid="select-country"]',
155
+ });
156
+ assert.strictEqual(value, 'uk');
157
+ });
158
+
159
+ it('should submit form after accepting terms', async () => {
160
+ if (!commander) {
161
+ return;
162
+ }
163
+
164
+ // Check terms
165
+ await commander.click({ selector: '[data-testid="checkbox-terms"]' });
166
+ await commander.wait({ ms: 100 });
167
+
168
+ // Submit form
169
+ await commander.click({ selector: '[data-testid="btn-submit"]' });
170
+
171
+ // Wait for result
172
+ await commander.wait({ ms: 600 });
173
+
174
+ // Check result
175
+ const isVisible = await commander.isVisible({
176
+ selector: '[data-testid="submit-result"]',
177
+ });
178
+ assert.strictEqual(isVisible, true);
179
+ });
180
+
181
+ it('should reset form', async () => {
182
+ if (!commander) {
183
+ return;
184
+ }
185
+
186
+ await commander.click({ selector: '[data-testid="btn-reset"]' });
187
+ await commander.wait({ ms: 100 });
188
+
189
+ const nameValue = await commander.inputValue({
190
+ selector: '[data-testid="input-name"]',
191
+ });
192
+ assert.strictEqual(nameValue, '');
193
+ });
194
+ });
195
+
196
+ describe('Interactive Elements', () => {
197
+ it('should work with counter', async () => {
198
+ if (!commander) {
199
+ return;
200
+ }
201
+
202
+ // Increment multiple times
203
+ await commander.click({ selector: '[data-testid="btn-increment"]' });
204
+ await commander.click({ selector: '[data-testid="btn-increment"]' });
205
+ await commander.click({ selector: '[data-testid="btn-increment"]' });
206
+ await commander.wait({ ms: 50 });
207
+
208
+ const value = await commander.textContent({
209
+ selector: '[data-testid="counter-value"]',
210
+ });
211
+ assert.ok(parseInt(value) >= 3);
212
+ });
213
+
214
+ it('should toggle switch state', async () => {
215
+ if (!commander) {
216
+ return;
217
+ }
218
+
219
+ // Get initial state
220
+ const initialStatus = await commander.textContent({
221
+ selector: '[data-testid="toggle-status"]',
222
+ });
223
+
224
+ // Toggle
225
+ await commander.click({ selector: '[data-testid="toggle-switch"]' });
226
+ await commander.wait({ ms: 50 });
227
+
228
+ // Verify changed
229
+ const newStatus = await commander.textContent({
230
+ selector: '[data-testid="toggle-status"]',
231
+ });
232
+ assert.notStrictEqual(initialStatus.trim(), newStatus.trim());
233
+
234
+ // Toggle back
235
+ await commander.click({ selector: '[data-testid="toggle-switch"]' });
236
+ await commander.wait({ ms: 50 });
237
+
238
+ // Verify back to original
239
+ const finalStatus = await commander.textContent({
240
+ selector: '[data-testid="toggle-status"]',
241
+ });
242
+ assert.strictEqual(initialStatus.trim(), finalStatus.trim());
243
+ });
244
+
245
+ it('should interact with custom dropdown', async () => {
246
+ if (!commander) {
247
+ return;
248
+ }
249
+
250
+ // Open
251
+ await commander.click({ selector: '[data-testid="dropdown-trigger"]' });
252
+ await commander.wait({ ms: 100 });
253
+
254
+ // Select Option B
255
+ await commander.click({
256
+ selector: '[data-testid="dropdown-option-option-b"]',
257
+ });
258
+ await commander.wait({ ms: 100 });
259
+
260
+ // Verify
261
+ const selected = await commander.textContent({
262
+ selector: '[data-testid="dropdown-selected"]',
263
+ });
264
+ assert.ok(selected.includes('Option B'));
265
+ });
266
+
267
+ it('should handle modal interactions', async () => {
268
+ if (!commander) {
269
+ return;
270
+ }
271
+
272
+ // Open modal
273
+ await commander.click({ selector: '[data-testid="btn-open-modal"]' });
274
+ await commander.wait({ ms: 150 });
275
+
276
+ // Modal should be visible
277
+ const modalVisible = await commander.isVisible({
278
+ selector: '[data-testid="modal"]',
279
+ });
280
+ assert.strictEqual(modalVisible, true);
281
+
282
+ // Type in modal input
283
+ await commander.fill({
284
+ selector: '[data-testid="modal-input"]',
285
+ text: 'Puppeteer modal test',
286
+ });
287
+
288
+ // Cancel
289
+ await commander.click({ selector: '[data-testid="modal-cancel"]' });
290
+ await commander.wait({ ms: 100 });
291
+
292
+ // Modal should be closed
293
+ const modalClosed = await commander.isVisible({
294
+ selector: '[data-testid="modal"]',
295
+ });
296
+ assert.strictEqual(modalClosed, false);
297
+ });
298
+ });
299
+
300
+ describe('Element State & Content', () => {
301
+ it('should check visibility of multiple elements', async () => {
302
+ if (!commander) {
303
+ return;
304
+ }
305
+
306
+ const sections = [
307
+ 'form-section',
308
+ 'interactive-section',
309
+ 'scroll-section',
310
+ 'navigation-section',
311
+ ];
312
+
313
+ for (const section of sections) {
314
+ const visible = await commander.isVisible({
315
+ selector: `[data-testid="${section}"]`,
316
+ });
317
+ assert.strictEqual(
318
+ visible,
319
+ true,
320
+ `Section ${section} should be visible`
321
+ );
322
+ }
323
+ });
324
+
325
+ it('should count scroll items', async () => {
326
+ if (!commander) {
327
+ return;
328
+ }
329
+
330
+ const count = await commander.count({ selector: '.scroll-item' });
331
+ assert.strictEqual(count, 20);
332
+ });
333
+
334
+ it('should get multiple attributes', async () => {
335
+ if (!commander) {
336
+ return;
337
+ }
338
+
339
+ const page1Href = await commander.getAttribute({
340
+ selector: '[data-testid="link-page-1"]',
341
+ attribute: 'href',
342
+ });
343
+ const page2Href = await commander.getAttribute({
344
+ selector: '[data-testid="link-page-2"]',
345
+ attribute: 'href',
346
+ });
347
+
348
+ assert.strictEqual(page1Href, '/page-1');
349
+ assert.strictEqual(page2Href, '/page-2');
350
+ });
351
+ });
352
+
353
+ describe('Scroll Operations', () => {
354
+ it('should scroll to element in container', async () => {
355
+ if (!commander) {
356
+ return;
357
+ }
358
+
359
+ // Scroll to specific item
360
+ await commander.scroll({ selector: '[data-testid="scroll-item-15"]' });
361
+ await commander.wait({ ms: 500 });
362
+
363
+ // Target should be visible
364
+ const visible = await commander.isVisible({
365
+ selector: '[data-testid="scroll-target-button"]',
366
+ });
367
+ assert.strictEqual(visible, true);
368
+ });
369
+ });
370
+
371
+ describe('Evaluate', () => {
372
+ it('should execute JavaScript in page context', async () => {
373
+ if (!commander) {
374
+ return;
375
+ }
376
+
377
+ const result = await commander.evaluate({
378
+ fn: () => document.title,
379
+ });
380
+ assert.ok(result.includes('React Test App'));
381
+ });
382
+
383
+ it('should pass arguments to evaluated function', async () => {
384
+ if (!commander) {
385
+ return;
386
+ }
387
+
388
+ const result = await commander.evaluate({
389
+ fn: (a, b) => a + b,
390
+ args: [2, 3],
391
+ });
392
+ assert.strictEqual(result, 5);
393
+ });
394
+ });
395
+
396
+ describe('Wait Operations', () => {
397
+ it('should wait for specified time', async () => {
398
+ if (!commander) {
399
+ return;
400
+ }
401
+
402
+ const start = Date.now();
403
+ await commander.wait({ ms: 100 });
404
+ const elapsed = Date.now() - start;
405
+ assert.ok(elapsed >= 90, `Expected at least 90ms, got ${elapsed}`);
406
+ });
407
+ });
408
+ });