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.
- package/.changeset/README.md +8 -0
- package/.changeset/config.json +11 -0
- package/.github/workflows/release.yml +296 -0
- package/.husky/pre-commit +1 -0
- package/.jscpd.json +20 -0
- package/.prettierignore +7 -0
- package/.prettierrc +10 -0
- package/CHANGELOG.md +32 -0
- package/LICENSE +24 -0
- package/README.md +320 -0
- package/bunfig.toml +3 -0
- package/deno.json +7 -0
- package/eslint.config.js +125 -0
- package/examples/react-test-app/index.html +25 -0
- package/examples/react-test-app/package.json +19 -0
- package/examples/react-test-app/src/App.jsx +473 -0
- package/examples/react-test-app/src/main.jsx +10 -0
- package/examples/react-test-app/src/styles.css +323 -0
- package/examples/react-test-app/vite.config.js +9 -0
- package/package.json +89 -0
- package/scripts/changeset-version.mjs +38 -0
- package/scripts/create-github-release.mjs +93 -0
- package/scripts/create-manual-changeset.mjs +86 -0
- package/scripts/format-github-release.mjs +83 -0
- package/scripts/format-release-notes.mjs +216 -0
- package/scripts/instant-version-bump.mjs +121 -0
- package/scripts/merge-changesets.mjs +260 -0
- package/scripts/publish-to-npm.mjs +126 -0
- package/scripts/setup-npm.mjs +37 -0
- package/scripts/validate-changeset.mjs +262 -0
- package/scripts/version-and-commit.mjs +237 -0
- package/src/ARCHITECTURE.md +270 -0
- package/src/README.md +517 -0
- package/src/bindings.js +298 -0
- package/src/browser/launcher.js +93 -0
- package/src/browser/navigation.js +513 -0
- package/src/core/constants.js +24 -0
- package/src/core/engine-adapter.js +466 -0
- package/src/core/engine-detection.js +49 -0
- package/src/core/logger.js +21 -0
- package/src/core/navigation-manager.js +503 -0
- package/src/core/navigation-safety.js +160 -0
- package/src/core/network-tracker.js +373 -0
- package/src/core/page-session.js +299 -0
- package/src/core/page-trigger-manager.js +564 -0
- package/src/core/preferences.js +46 -0
- package/src/elements/content.js +197 -0
- package/src/elements/locators.js +243 -0
- package/src/elements/selectors.js +360 -0
- package/src/elements/visibility.js +166 -0
- package/src/exports.js +121 -0
- package/src/factory.js +192 -0
- package/src/high-level/universal-logic.js +206 -0
- package/src/index.js +17 -0
- package/src/interactions/click.js +684 -0
- package/src/interactions/fill.js +383 -0
- package/src/interactions/scroll.js +341 -0
- package/src/utilities/url.js +33 -0
- package/src/utilities/wait.js +135 -0
- package/tests/e2e/playwright.e2e.test.js +442 -0
- package/tests/e2e/puppeteer.e2e.test.js +408 -0
- package/tests/helpers/mocks.js +542 -0
- package/tests/unit/bindings.test.js +218 -0
- package/tests/unit/browser/navigation.test.js +345 -0
- package/tests/unit/core/constants.test.js +72 -0
- package/tests/unit/core/engine-adapter.test.js +170 -0
- package/tests/unit/core/engine-detection.test.js +81 -0
- package/tests/unit/core/logger.test.js +80 -0
- package/tests/unit/core/navigation-safety.test.js +202 -0
- package/tests/unit/core/network-tracker.test.js +198 -0
- package/tests/unit/core/page-trigger-manager.test.js +358 -0
- package/tests/unit/elements/content.test.js +318 -0
- package/tests/unit/elements/locators.test.js +236 -0
- package/tests/unit/elements/selectors.test.js +302 -0
- package/tests/unit/elements/visibility.test.js +234 -0
- package/tests/unit/factory.test.js +174 -0
- package/tests/unit/high-level/universal-logic.test.js +299 -0
- package/tests/unit/interactions/click.test.js +340 -0
- package/tests/unit/interactions/fill.test.js +378 -0
- package/tests/unit/interactions/scroll.test.js +330 -0
- package/tests/unit/utilities/url.test.js +63 -0
- 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
|
+
});
|