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,442 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E tests for browser-commander using Playwright 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:playwright
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, before, after } from 'node:test';
|
|
10
|
+
import assert from 'node:assert';
|
|
11
|
+
|
|
12
|
+
// Dynamic import for playwright since it may not be installed
|
|
13
|
+
let playwright;
|
|
14
|
+
let createCommander;
|
|
15
|
+
|
|
16
|
+
describe(
|
|
17
|
+
'E2E Tests - Playwright Engine',
|
|
18
|
+
{ skip: !process.env.RUN_E2E },
|
|
19
|
+
() => {
|
|
20
|
+
let browser;
|
|
21
|
+
let page;
|
|
22
|
+
let commander;
|
|
23
|
+
const BASE_URL = process.env.TEST_URL || 'http://localhost:3000';
|
|
24
|
+
|
|
25
|
+
before(async () => {
|
|
26
|
+
try {
|
|
27
|
+
playwright = await import('playwright');
|
|
28
|
+
const module = await import('../../src/index.js');
|
|
29
|
+
createCommander = module.createCommander;
|
|
30
|
+
|
|
31
|
+
browser = await playwright.chromium.launch({
|
|
32
|
+
headless: process.env.HEADLESS !== 'false',
|
|
33
|
+
});
|
|
34
|
+
const context = await browser.newContext();
|
|
35
|
+
page = await context.newPage();
|
|
36
|
+
commander = createCommander({ page, verbose: true });
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.log(
|
|
39
|
+
'Skipping E2E tests - playwright not available or test app not running'
|
|
40
|
+
);
|
|
41
|
+
console.log('Error:', error.message);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
after(async () => {
|
|
46
|
+
if (browser) {
|
|
47
|
+
await browser.close();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('Navigation', () => {
|
|
52
|
+
it('should navigate to test app', async () => {
|
|
53
|
+
if (!commander) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await commander.goto({ url: BASE_URL });
|
|
58
|
+
const url = commander.getUrl();
|
|
59
|
+
assert.ok(url.includes('localhost:3000') || url.includes(BASE_URL));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should get page title', async () => {
|
|
63
|
+
if (!commander) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const title = await commander.textContent({
|
|
68
|
+
selector: '[data-testid="page-title"]',
|
|
69
|
+
});
|
|
70
|
+
assert.strictEqual(title.trim(), 'React Test App');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('Form Interactions', () => {
|
|
75
|
+
it('should fill text input', async () => {
|
|
76
|
+
if (!commander) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await commander.fill({
|
|
81
|
+
selector: '[data-testid="input-name"]',
|
|
82
|
+
text: 'John Doe',
|
|
83
|
+
});
|
|
84
|
+
const value = await commander.inputValue({
|
|
85
|
+
selector: '[data-testid="input-name"]',
|
|
86
|
+
});
|
|
87
|
+
assert.strictEqual(value, 'John Doe');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should fill email input', async () => {
|
|
91
|
+
if (!commander) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
await commander.fill({
|
|
96
|
+
selector: '[data-testid="input-email"]',
|
|
97
|
+
text: 'john@example.com',
|
|
98
|
+
});
|
|
99
|
+
const value = await commander.inputValue({
|
|
100
|
+
selector: '[data-testid="input-email"]',
|
|
101
|
+
});
|
|
102
|
+
assert.strictEqual(value, 'john@example.com');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should fill textarea', async () => {
|
|
106
|
+
if (!commander) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const bioText = 'This is my bio. I am a test user for E2E testing.';
|
|
111
|
+
await commander.fill({
|
|
112
|
+
selector: '[data-testid="textarea-bio"]',
|
|
113
|
+
text: bioText,
|
|
114
|
+
});
|
|
115
|
+
const value = await commander.inputValue({
|
|
116
|
+
selector: '[data-testid="textarea-bio"]',
|
|
117
|
+
});
|
|
118
|
+
assert.strictEqual(value, bioText);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should select radio button', async () => {
|
|
122
|
+
if (!commander) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
await commander.click({ selector: '[data-testid="radio-male"]' });
|
|
127
|
+
const checked = await commander.getAttribute({
|
|
128
|
+
selector: '[data-testid="radio-male"]',
|
|
129
|
+
attribute: 'checked',
|
|
130
|
+
});
|
|
131
|
+
// Playwright returns empty string for checked attribute when true
|
|
132
|
+
assert.ok(checked !== null);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should check checkbox', async () => {
|
|
136
|
+
if (!commander) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
await commander.click({
|
|
141
|
+
selector: '[data-testid="checkbox-technology"]',
|
|
142
|
+
});
|
|
143
|
+
// Wait for state change
|
|
144
|
+
await commander.wait({ ms: 100 });
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should select from dropdown', async () => {
|
|
148
|
+
if (!commander) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Using native select
|
|
153
|
+
await commander.evaluate({
|
|
154
|
+
fn: () => {
|
|
155
|
+
const select = document.querySelector(
|
|
156
|
+
'[data-testid="select-country"]'
|
|
157
|
+
);
|
|
158
|
+
if (select) {
|
|
159
|
+
select.value = 'us';
|
|
160
|
+
select.dispatchEvent(new Event('change', { bubbles: true }));
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
const value = await commander.inputValue({
|
|
165
|
+
selector: '[data-testid="select-country"]',
|
|
166
|
+
});
|
|
167
|
+
assert.strictEqual(value, 'us');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should check terms checkbox and submit form', async () => {
|
|
171
|
+
if (!commander) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Check terms
|
|
176
|
+
await commander.click({ selector: '[data-testid="checkbox-terms"]' });
|
|
177
|
+
await commander.wait({ ms: 100 });
|
|
178
|
+
|
|
179
|
+
// Submit button should be enabled now
|
|
180
|
+
const isEnabled = await commander.isEnabled({
|
|
181
|
+
selector: '[data-testid="btn-submit"]',
|
|
182
|
+
});
|
|
183
|
+
assert.strictEqual(isEnabled, true);
|
|
184
|
+
|
|
185
|
+
// Submit form
|
|
186
|
+
await commander.click({ selector: '[data-testid="btn-submit"]' });
|
|
187
|
+
|
|
188
|
+
// Wait for result
|
|
189
|
+
await commander.wait({ ms: 600 });
|
|
190
|
+
|
|
191
|
+
// Check result appears
|
|
192
|
+
const isVisible = await commander.isVisible({
|
|
193
|
+
selector: '[data-testid="submit-result"]',
|
|
194
|
+
});
|
|
195
|
+
assert.strictEqual(isVisible, true);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('Interactive Elements', () => {
|
|
200
|
+
it('should increment counter', async () => {
|
|
201
|
+
if (!commander) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const initialValue = await commander.textContent({
|
|
206
|
+
selector: '[data-testid="counter-value"]',
|
|
207
|
+
});
|
|
208
|
+
await commander.click({ selector: '[data-testid="btn-increment"]' });
|
|
209
|
+
await commander.wait({ ms: 50 });
|
|
210
|
+
const newValue = await commander.textContent({
|
|
211
|
+
selector: '[data-testid="counter-value"]',
|
|
212
|
+
});
|
|
213
|
+
assert.strictEqual(parseInt(newValue), parseInt(initialValue) + 1);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should decrement counter', async () => {
|
|
217
|
+
if (!commander) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const initialValue = await commander.textContent({
|
|
222
|
+
selector: '[data-testid="counter-value"]',
|
|
223
|
+
});
|
|
224
|
+
await commander.click({ selector: '[data-testid="btn-decrement"]' });
|
|
225
|
+
await commander.wait({ ms: 50 });
|
|
226
|
+
const newValue = await commander.textContent({
|
|
227
|
+
selector: '[data-testid="counter-value"]',
|
|
228
|
+
});
|
|
229
|
+
assert.strictEqual(parseInt(newValue), parseInt(initialValue) - 1);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should toggle switch', async () => {
|
|
233
|
+
if (!commander) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const initialStatus = await commander.textContent({
|
|
238
|
+
selector: '[data-testid="toggle-status"]',
|
|
239
|
+
});
|
|
240
|
+
await commander.click({ selector: '[data-testid="toggle-switch"]' });
|
|
241
|
+
await commander.wait({ ms: 50 });
|
|
242
|
+
const newStatus = await commander.textContent({
|
|
243
|
+
selector: '[data-testid="toggle-status"]',
|
|
244
|
+
});
|
|
245
|
+
assert.notStrictEqual(initialStatus.trim(), newStatus.trim());
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should open and close dropdown', async () => {
|
|
249
|
+
if (!commander) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Open dropdown
|
|
254
|
+
await commander.click({ selector: '[data-testid="dropdown-trigger"]' });
|
|
255
|
+
await commander.wait({ ms: 100 });
|
|
256
|
+
|
|
257
|
+
// Check menu is visible
|
|
258
|
+
const menuVisible = await commander.isVisible({
|
|
259
|
+
selector: '[data-testid="dropdown-menu"]',
|
|
260
|
+
});
|
|
261
|
+
assert.strictEqual(menuVisible, true);
|
|
262
|
+
|
|
263
|
+
// Select option
|
|
264
|
+
await commander.click({
|
|
265
|
+
selector: '[data-testid="dropdown-option-option-a"]',
|
|
266
|
+
});
|
|
267
|
+
await commander.wait({ ms: 100 });
|
|
268
|
+
|
|
269
|
+
// Check selection
|
|
270
|
+
const selected = await commander.textContent({
|
|
271
|
+
selector: '[data-testid="dropdown-selected"]',
|
|
272
|
+
});
|
|
273
|
+
assert.ok(selected.includes('Option A'));
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('should open and close modal', async () => {
|
|
277
|
+
if (!commander) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Open modal
|
|
282
|
+
await commander.click({ selector: '[data-testid="btn-open-modal"]' });
|
|
283
|
+
await commander.wait({ ms: 100 });
|
|
284
|
+
|
|
285
|
+
// Check modal is visible
|
|
286
|
+
const modalVisible = await commander.isVisible({
|
|
287
|
+
selector: '[data-testid="modal"]',
|
|
288
|
+
});
|
|
289
|
+
assert.strictEqual(modalVisible, true);
|
|
290
|
+
|
|
291
|
+
// Fill modal input
|
|
292
|
+
await commander.fill({
|
|
293
|
+
selector: '[data-testid="modal-input"]',
|
|
294
|
+
text: 'Test input',
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Close modal
|
|
298
|
+
await commander.click({ selector: '[data-testid="modal-cancel"]' });
|
|
299
|
+
await commander.wait({ ms: 100 });
|
|
300
|
+
|
|
301
|
+
// Check modal is closed
|
|
302
|
+
const modalClosed = await commander.isVisible({
|
|
303
|
+
selector: '[data-testid="modal"]',
|
|
304
|
+
});
|
|
305
|
+
assert.strictEqual(modalClosed, false);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('should load dynamic content', async () => {
|
|
309
|
+
if (!commander) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Click load button
|
|
314
|
+
await commander.click({ selector: '[data-testid="btn-load-content"]' });
|
|
315
|
+
|
|
316
|
+
// Wait for content to load
|
|
317
|
+
await commander.wait({ ms: 500 });
|
|
318
|
+
|
|
319
|
+
// Check content is visible
|
|
320
|
+
const contentVisible = await commander.isVisible({
|
|
321
|
+
selector: '[data-testid="dynamic-content"]',
|
|
322
|
+
});
|
|
323
|
+
assert.strictEqual(contentVisible, true);
|
|
324
|
+
|
|
325
|
+
// Check items are loaded
|
|
326
|
+
const item0 = await commander.textContent({
|
|
327
|
+
selector: '[data-testid="dynamic-item-0"]',
|
|
328
|
+
});
|
|
329
|
+
assert.strictEqual(item0.trim(), 'Item 1');
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
describe('Scroll Operations', () => {
|
|
334
|
+
it('should scroll to target element', async () => {
|
|
335
|
+
if (!commander) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Scroll to target item (item 15)
|
|
340
|
+
await commander.scroll({ selector: '[data-testid="scroll-item-15"]' });
|
|
341
|
+
await commander.wait({ ms: 500 });
|
|
342
|
+
|
|
343
|
+
// Element should now be visible
|
|
344
|
+
const isVisible = await commander.isVisible({
|
|
345
|
+
selector: '[data-testid="scroll-target-button"]',
|
|
346
|
+
});
|
|
347
|
+
assert.strictEqual(isVisible, true);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('should click button after scrolling', async () => {
|
|
351
|
+
if (!commander) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Use click which auto-scrolls
|
|
356
|
+
let alertHandled = false;
|
|
357
|
+
page.on('dialog', async (dialog) => {
|
|
358
|
+
alertHandled = true;
|
|
359
|
+
await dialog.accept();
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
await commander.click({
|
|
363
|
+
selector: '[data-testid="scroll-target-button"]',
|
|
364
|
+
});
|
|
365
|
+
await commander.wait({ ms: 100 });
|
|
366
|
+
|
|
367
|
+
assert.strictEqual(alertHandled, true);
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
describe('Element State', () => {
|
|
372
|
+
it('should check element visibility', async () => {
|
|
373
|
+
if (!commander) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const visible = await commander.isVisible({
|
|
378
|
+
selector: '[data-testid="page-title"]',
|
|
379
|
+
});
|
|
380
|
+
assert.strictEqual(visible, true);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('should check non-existent element visibility', async () => {
|
|
384
|
+
if (!commander) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const visible = await commander.isVisible({
|
|
389
|
+
selector: '[data-testid="non-existent"]',
|
|
390
|
+
});
|
|
391
|
+
assert.strictEqual(visible, false);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('should count elements', async () => {
|
|
395
|
+
if (!commander) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const count = await commander.count({ selector: '.scroll-item' });
|
|
400
|
+
assert.strictEqual(count, 20);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('should get element attribute', async () => {
|
|
404
|
+
if (!commander) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const href = await commander.getAttribute({
|
|
409
|
+
selector: '[data-testid="link-page-1"]',
|
|
410
|
+
attribute: 'href',
|
|
411
|
+
});
|
|
412
|
+
assert.strictEqual(href, '/page-1');
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
describe('Query Selectors', () => {
|
|
417
|
+
it('should find element by text', async () => {
|
|
418
|
+
if (!commander) {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const selector = await commander.findByText({
|
|
423
|
+
text: 'React Test App',
|
|
424
|
+
selector: 'h1',
|
|
425
|
+
});
|
|
426
|
+
const element = await commander.querySelector({ selector });
|
|
427
|
+
assert.ok(element);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('should query all matching elements', async () => {
|
|
431
|
+
if (!commander) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const elements = await commander.querySelectorAll({
|
|
436
|
+
selector: '.section',
|
|
437
|
+
});
|
|
438
|
+
assert.ok(elements.length >= 4);
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
);
|