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,473 @@
1
+ import React, { useState } from 'react';
2
+
3
+ function App() {
4
+ // Form state
5
+ const [formData, setFormData] = useState({
6
+ name: '',
7
+ email: '',
8
+ password: '',
9
+ bio: '',
10
+ gender: '',
11
+ interests: [],
12
+ country: '',
13
+ newsletter: false,
14
+ terms: false,
15
+ });
16
+
17
+ const [submitResult, setSubmitResult] = useState(null);
18
+ const [counter, setCounter] = useState(0);
19
+ const [toggleEnabled, setToggleEnabled] = useState(false);
20
+ const [showModal, setShowModal] = useState(false);
21
+ const [dropdownOpen, setDropdownOpen] = useState(false);
22
+ const [selectedOption, setSelectedOption] = useState('');
23
+ const [dynamicContent, setDynamicContent] = useState(null);
24
+ const [isLoading, setIsLoading] = useState(false);
25
+
26
+ const handleInputChange = (e) => {
27
+ const { name, value, type, checked } = e.target;
28
+
29
+ if (type === 'checkbox' && name === 'interests') {
30
+ setFormData((prev) => ({
31
+ ...prev,
32
+ interests: checked
33
+ ? [...prev.interests, value]
34
+ : prev.interests.filter((i) => i !== value),
35
+ }));
36
+ } else if (type === 'checkbox') {
37
+ setFormData((prev) => ({ ...prev, [name]: checked }));
38
+ } else {
39
+ setFormData((prev) => ({ ...prev, [name]: value }));
40
+ }
41
+ };
42
+
43
+ const handleSubmit = (e) => {
44
+ e.preventDefault();
45
+ setIsLoading(true);
46
+
47
+ // Simulate API call
48
+ setTimeout(() => {
49
+ setSubmitResult({
50
+ success: true,
51
+ message: 'Form submitted successfully!',
52
+ data: formData,
53
+ });
54
+ setIsLoading(false);
55
+ }, 500);
56
+ };
57
+
58
+ const handleReset = () => {
59
+ setFormData({
60
+ name: '',
61
+ email: '',
62
+ password: '',
63
+ bio: '',
64
+ gender: '',
65
+ interests: [],
66
+ country: '',
67
+ newsletter: false,
68
+ terms: false,
69
+ });
70
+ setSubmitResult(null);
71
+ };
72
+
73
+ const loadDynamicContent = () => {
74
+ setIsLoading(true);
75
+ setTimeout(() => {
76
+ setDynamicContent({
77
+ title: 'Dynamic Content Loaded',
78
+ items: ['Item 1', 'Item 2', 'Item 3'],
79
+ });
80
+ setIsLoading(false);
81
+ }, 300);
82
+ };
83
+
84
+ return (
85
+ <div className="app">
86
+ <h1 data-testid="page-title">React Test App</h1>
87
+ <p>This app is designed for E2E testing with browser-commander</p>
88
+
89
+ {/* Basic Form Section */}
90
+ <section className="section" data-testid="form-section">
91
+ <h2>Contact Form</h2>
92
+ <form onSubmit={handleSubmit} data-testid="contact-form">
93
+ <div className="form-group">
94
+ <label htmlFor="name">Full Name</label>
95
+ <input
96
+ type="text"
97
+ id="name"
98
+ name="name"
99
+ value={formData.name}
100
+ onChange={handleInputChange}
101
+ placeholder="Enter your name"
102
+ data-testid="input-name"
103
+ />
104
+ </div>
105
+
106
+ <div className="form-group">
107
+ <label htmlFor="email">Email Address</label>
108
+ <input
109
+ type="email"
110
+ id="email"
111
+ name="email"
112
+ value={formData.email}
113
+ onChange={handleInputChange}
114
+ placeholder="Enter your email"
115
+ data-testid="input-email"
116
+ />
117
+ </div>
118
+
119
+ <div className="form-group">
120
+ <label htmlFor="password">Password</label>
121
+ <input
122
+ type="password"
123
+ id="password"
124
+ name="password"
125
+ value={formData.password}
126
+ onChange={handleInputChange}
127
+ placeholder="Enter password"
128
+ data-testid="input-password"
129
+ />
130
+ </div>
131
+
132
+ <div className="form-group">
133
+ <label htmlFor="bio">Bio / Description</label>
134
+ <textarea
135
+ id="bio"
136
+ name="bio"
137
+ value={formData.bio}
138
+ onChange={handleInputChange}
139
+ placeholder="Tell us about yourself..."
140
+ data-testid="textarea-bio"
141
+ />
142
+ </div>
143
+
144
+ <div className="form-group">
145
+ <label>Gender</label>
146
+ <div className="radio-group" data-testid="radio-gender">
147
+ {['male', 'female', 'other'].map((gender) => (
148
+ <div className="radio-item" key={gender}>
149
+ <input
150
+ type="radio"
151
+ id={`gender-${gender}`}
152
+ name="gender"
153
+ value={gender}
154
+ checked={formData.gender === gender}
155
+ onChange={handleInputChange}
156
+ data-testid={`radio-${gender}`}
157
+ />
158
+ <label htmlFor={`gender-${gender}`}>
159
+ {gender.charAt(0).toUpperCase() + gender.slice(1)}
160
+ </label>
161
+ </div>
162
+ ))}
163
+ </div>
164
+ </div>
165
+
166
+ <div className="form-group">
167
+ <label>Interests</label>
168
+ <div className="checkbox-group" data-testid="checkbox-interests">
169
+ {['technology', 'sports', 'music', 'travel', 'food'].map(
170
+ (interest) => (
171
+ <div className="checkbox-item" key={interest}>
172
+ <input
173
+ type="checkbox"
174
+ id={`interest-${interest}`}
175
+ name="interests"
176
+ value={interest}
177
+ checked={formData.interests.includes(interest)}
178
+ onChange={handleInputChange}
179
+ data-testid={`checkbox-${interest}`}
180
+ />
181
+ <label htmlFor={`interest-${interest}`}>
182
+ {interest.charAt(0).toUpperCase() + interest.slice(1)}
183
+ </label>
184
+ </div>
185
+ )
186
+ )}
187
+ </div>
188
+ </div>
189
+
190
+ <div className="form-group">
191
+ <label htmlFor="country">Country</label>
192
+ <select
193
+ id="country"
194
+ name="country"
195
+ value={formData.country}
196
+ onChange={handleInputChange}
197
+ data-testid="select-country"
198
+ >
199
+ <option value="">Select a country</option>
200
+ <option value="us">United States</option>
201
+ <option value="uk">United Kingdom</option>
202
+ <option value="ca">Canada</option>
203
+ <option value="au">Australia</option>
204
+ <option value="de">Germany</option>
205
+ </select>
206
+ </div>
207
+
208
+ <div className="form-group">
209
+ <div className="checkbox-item">
210
+ <input
211
+ type="checkbox"
212
+ id="newsletter"
213
+ name="newsletter"
214
+ checked={formData.newsletter}
215
+ onChange={handleInputChange}
216
+ data-testid="checkbox-newsletter"
217
+ />
218
+ <label htmlFor="newsletter">Subscribe to newsletter</label>
219
+ </div>
220
+ </div>
221
+
222
+ <div className="form-group">
223
+ <div className="checkbox-item">
224
+ <input
225
+ type="checkbox"
226
+ id="terms"
227
+ name="terms"
228
+ checked={formData.terms}
229
+ onChange={handleInputChange}
230
+ data-testid="checkbox-terms"
231
+ />
232
+ <label htmlFor="terms">I agree to the terms and conditions</label>
233
+ </div>
234
+ </div>
235
+
236
+ <div className="button-group">
237
+ <button
238
+ type="submit"
239
+ className="primary"
240
+ disabled={isLoading || !formData.terms}
241
+ data-testid="btn-submit"
242
+ >
243
+ {isLoading ? 'Submitting...' : 'Submit Form'}
244
+ </button>
245
+ <button
246
+ type="button"
247
+ className="secondary"
248
+ onClick={handleReset}
249
+ data-testid="btn-reset"
250
+ >
251
+ Reset
252
+ </button>
253
+ </div>
254
+ </form>
255
+
256
+ {submitResult && (
257
+ <div
258
+ className={`result ${submitResult.success ? 'success' : 'error'}`}
259
+ data-testid="submit-result"
260
+ >
261
+ <strong>{submitResult.message}</strong>
262
+ <pre>{JSON.stringify(submitResult.data, null, 2)}</pre>
263
+ </div>
264
+ )}
265
+ </section>
266
+
267
+ {/* Interactive Elements Section */}
268
+ <section className="section" data-testid="interactive-section">
269
+ <h2>Interactive Elements</h2>
270
+
271
+ {/* Counter */}
272
+ <div className="form-group">
273
+ <label>Counter</label>
274
+ <div className="counter">
275
+ <button
276
+ className="secondary"
277
+ onClick={() => setCounter((c) => c - 1)}
278
+ data-testid="btn-decrement"
279
+ >
280
+ -
281
+ </button>
282
+ <span className="counter-value" data-testid="counter-value">
283
+ {counter}
284
+ </span>
285
+ <button
286
+ className="secondary"
287
+ onClick={() => setCounter((c) => c + 1)}
288
+ data-testid="btn-increment"
289
+ >
290
+ +
291
+ </button>
292
+ </div>
293
+ </div>
294
+
295
+ {/* Toggle */}
296
+ <div className="form-group">
297
+ <label>Toggle Switch</label>
298
+ <div className="toggle-container">
299
+ <label className="toggle">
300
+ <input
301
+ type="checkbox"
302
+ checked={toggleEnabled}
303
+ onChange={(e) => setToggleEnabled(e.target.checked)}
304
+ data-testid="toggle-switch"
305
+ />
306
+ <span className="toggle-slider"></span>
307
+ </label>
308
+ <span data-testid="toggle-status">
309
+ {toggleEnabled ? 'Enabled' : 'Disabled'}
310
+ </span>
311
+ </div>
312
+ </div>
313
+
314
+ {/* Dropdown */}
315
+ <div className="form-group">
316
+ <label>Custom Dropdown</label>
317
+ <div className="dropdown">
318
+ <button
319
+ type="button"
320
+ className="secondary"
321
+ onClick={() => setDropdownOpen(!dropdownOpen)}
322
+ data-testid="dropdown-trigger"
323
+ >
324
+ {selectedOption || 'Select an option'} ▼
325
+ </button>
326
+ {dropdownOpen && (
327
+ <div className="dropdown-menu" data-testid="dropdown-menu">
328
+ {['Option A', 'Option B', 'Option C'].map((option) => (
329
+ <button
330
+ key={option}
331
+ onClick={() => {
332
+ setSelectedOption(option);
333
+ setDropdownOpen(false);
334
+ }}
335
+ data-testid={`dropdown-option-${option.toLowerCase().replace(' ', '-')}`}
336
+ >
337
+ {option}
338
+ </button>
339
+ ))}
340
+ </div>
341
+ )}
342
+ </div>
343
+ {selectedOption && (
344
+ <p data-testid="dropdown-selected">Selected: {selectedOption}</p>
345
+ )}
346
+ </div>
347
+
348
+ {/* Modal */}
349
+ <div className="form-group">
350
+ <label>Modal Dialog</label>
351
+ <button
352
+ className="primary"
353
+ onClick={() => setShowModal(true)}
354
+ data-testid="btn-open-modal"
355
+ >
356
+ Open Modal
357
+ </button>
358
+ </div>
359
+
360
+ {/* Dynamic Content */}
361
+ <div className="form-group">
362
+ <label>Dynamic Content Loading</label>
363
+ <button
364
+ className="primary"
365
+ onClick={loadDynamicContent}
366
+ disabled={isLoading}
367
+ data-testid="btn-load-content"
368
+ >
369
+ {isLoading ? 'Loading...' : 'Load Content'}
370
+ </button>
371
+ {dynamicContent && (
372
+ <div className="result animated" data-testid="dynamic-content">
373
+ <strong>{dynamicContent.title}</strong>
374
+ <ul>
375
+ {dynamicContent.items.map((item, i) => (
376
+ <li key={i} data-testid={`dynamic-item-${i}`}>
377
+ {item}
378
+ </li>
379
+ ))}
380
+ </ul>
381
+ </div>
382
+ )}
383
+ </div>
384
+ </section>
385
+
386
+ {/* Scroll Test Section */}
387
+ <section className="section" data-testid="scroll-section">
388
+ <h2>Scroll Test</h2>
389
+ <div className="scroll-test" data-testid="scroll-container">
390
+ {Array.from({ length: 20 }, (_, i) => (
391
+ <div
392
+ key={i}
393
+ className={`scroll-item ${i === 15 ? 'target' : ''}`}
394
+ data-testid={`scroll-item-${i}`}
395
+ >
396
+ {i === 15 ? (
397
+ <>
398
+ <strong>Target Element</strong>
399
+ <p>This is the element to scroll to</p>
400
+ <button
401
+ className="primary"
402
+ data-testid="scroll-target-button"
403
+ onClick={() => alert('Target clicked!')}
404
+ >
405
+ Click Me
406
+ </button>
407
+ </>
408
+ ) : (
409
+ <>Item {i + 1}</>
410
+ )}
411
+ </div>
412
+ ))}
413
+ </div>
414
+ </section>
415
+
416
+ {/* Navigation Section */}
417
+ <section className="section" data-testid="navigation-section">
418
+ <h2>Navigation Test</h2>
419
+ <div className="button-group">
420
+ <a href="/page-1" data-testid="link-page-1">
421
+ Go to Page 1
422
+ </a>
423
+ <a href="/page-2" data-testid="link-page-2">
424
+ Go to Page 2
425
+ </a>
426
+ <button
427
+ className="primary"
428
+ onClick={() => (window.location.href = '/success')}
429
+ data-testid="btn-navigate"
430
+ >
431
+ Navigate to Success
432
+ </button>
433
+ </div>
434
+ </section>
435
+
436
+ {/* Modal */}
437
+ {showModal && (
438
+ <div className="modal-overlay" data-testid="modal-overlay">
439
+ <div className="modal" data-testid="modal">
440
+ <h3>Modal Title</h3>
441
+ <p>This is modal content. You can close it or confirm.</p>
442
+ <input
443
+ type="text"
444
+ placeholder="Enter something..."
445
+ data-testid="modal-input"
446
+ />
447
+ <div className="modal-buttons">
448
+ <button
449
+ className="secondary"
450
+ onClick={() => setShowModal(false)}
451
+ data-testid="modal-cancel"
452
+ >
453
+ Cancel
454
+ </button>
455
+ <button
456
+ className="primary"
457
+ onClick={() => {
458
+ alert('Confirmed!');
459
+ setShowModal(false);
460
+ }}
461
+ data-testid="modal-confirm"
462
+ >
463
+ Confirm
464
+ </button>
465
+ </div>
466
+ </div>
467
+ </div>
468
+ )}
469
+ </div>
470
+ );
471
+ }
472
+
473
+ export default App;
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import App from './App';
4
+ import './styles.css';
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>
10
+ );