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,378 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import {
|
|
4
|
+
defaultFillVerification,
|
|
5
|
+
verifyFill,
|
|
6
|
+
checkIfElementEmpty,
|
|
7
|
+
performFill,
|
|
8
|
+
fillTextArea,
|
|
9
|
+
} from '../../../src/interactions/fill.js';
|
|
10
|
+
import {
|
|
11
|
+
createMockPlaywrightPage,
|
|
12
|
+
createMockLogger,
|
|
13
|
+
} from '../../helpers/mocks.js';
|
|
14
|
+
|
|
15
|
+
describe('fill', () => {
|
|
16
|
+
describe('defaultFillVerification', () => {
|
|
17
|
+
it('should verify exact match', async () => {
|
|
18
|
+
const page = createMockPlaywrightPage();
|
|
19
|
+
const mockLocator = {
|
|
20
|
+
inputValue: async () => 'test value',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const result = await defaultFillVerification({
|
|
24
|
+
page,
|
|
25
|
+
engine: 'playwright',
|
|
26
|
+
locatorOrElement: mockLocator,
|
|
27
|
+
expectedText: 'test value',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
assert.strictEqual(result.verified, true);
|
|
31
|
+
assert.strictEqual(result.actualValue, 'test value');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should verify partial match', async () => {
|
|
35
|
+
const page = createMockPlaywrightPage();
|
|
36
|
+
const mockLocator = {
|
|
37
|
+
inputValue: async () => 'test value with extra',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const result = await defaultFillVerification({
|
|
41
|
+
page,
|
|
42
|
+
engine: 'playwright',
|
|
43
|
+
locatorOrElement: mockLocator,
|
|
44
|
+
expectedText: 'test value',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
assert.strictEqual(result.verified, true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should fail verification on mismatch', async () => {
|
|
51
|
+
const page = createMockPlaywrightPage();
|
|
52
|
+
const mockLocator = {
|
|
53
|
+
inputValue: async () => 'different value',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const result = await defaultFillVerification({
|
|
57
|
+
page,
|
|
58
|
+
engine: 'playwright',
|
|
59
|
+
locatorOrElement: mockLocator,
|
|
60
|
+
expectedText: 'expected value',
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
assert.strictEqual(result.verified, false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should handle navigation errors', async () => {
|
|
67
|
+
const page = createMockPlaywrightPage();
|
|
68
|
+
// Override the locator method to return a locator that throws navigation error
|
|
69
|
+
page.locator = () => ({
|
|
70
|
+
inputValue: async () => {
|
|
71
|
+
throw new Error('Execution context was destroyed');
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const result = await defaultFillVerification({
|
|
76
|
+
page,
|
|
77
|
+
engine: 'playwright',
|
|
78
|
+
locatorOrElement: page.locator(),
|
|
79
|
+
expectedText: 'test',
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Note: getInputValue catches navigation errors and returns '',
|
|
83
|
+
// so defaultFillVerification sees '' as actualValue and verification fails
|
|
84
|
+
// The navigation error is handled at the getInputValue level
|
|
85
|
+
assert.strictEqual(result.verified, false);
|
|
86
|
+
assert.strictEqual(result.actualValue, '');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('verifyFill', () => {
|
|
91
|
+
it('should verify fill with retry logic', async () => {
|
|
92
|
+
const page = createMockPlaywrightPage();
|
|
93
|
+
const log = createMockLogger();
|
|
94
|
+
const mockLocator = {};
|
|
95
|
+
|
|
96
|
+
const result = await verifyFill({
|
|
97
|
+
page,
|
|
98
|
+
engine: 'playwright',
|
|
99
|
+
locatorOrElement: mockLocator,
|
|
100
|
+
expectedText: 'test',
|
|
101
|
+
verifyFn: async () => ({ verified: true, actualValue: 'test' }),
|
|
102
|
+
timeout: 100,
|
|
103
|
+
log,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
assert.strictEqual(result.verified, true);
|
|
107
|
+
assert.ok(result.attempts >= 1);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should fail after timeout', async () => {
|
|
111
|
+
const page = createMockPlaywrightPage();
|
|
112
|
+
const log = createMockLogger();
|
|
113
|
+
|
|
114
|
+
const result = await verifyFill({
|
|
115
|
+
page,
|
|
116
|
+
engine: 'playwright',
|
|
117
|
+
locatorOrElement: {},
|
|
118
|
+
expectedText: 'expected',
|
|
119
|
+
verifyFn: async () => ({ verified: false, actualValue: 'different' }),
|
|
120
|
+
timeout: 50,
|
|
121
|
+
retryInterval: 10,
|
|
122
|
+
log,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
assert.strictEqual(result.verified, false);
|
|
126
|
+
assert.ok(result.attempts >= 1);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('checkIfElementEmpty', () => {
|
|
131
|
+
it('should throw when locatorOrElement is not provided', async () => {
|
|
132
|
+
const page = createMockPlaywrightPage();
|
|
133
|
+
|
|
134
|
+
await assert.rejects(
|
|
135
|
+
() => checkIfElementEmpty({ page, engine: 'playwright' }),
|
|
136
|
+
/locatorOrElement is required/
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should throw when page is not provided and no adapter', async () => {
|
|
141
|
+
await assert.rejects(
|
|
142
|
+
() =>
|
|
143
|
+
checkIfElementEmpty({ engine: 'playwright', locatorOrElement: {} }),
|
|
144
|
+
/page is required/
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should return true for empty element', async () => {
|
|
149
|
+
const page = createMockPlaywrightPage();
|
|
150
|
+
const adapter = {
|
|
151
|
+
getInputValue: async () => '',
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const result = await checkIfElementEmpty({
|
|
155
|
+
page,
|
|
156
|
+
engine: 'playwright',
|
|
157
|
+
locatorOrElement: {},
|
|
158
|
+
adapter,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
assert.strictEqual(result, true);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should return true for whitespace-only element', async () => {
|
|
165
|
+
const page = createMockPlaywrightPage();
|
|
166
|
+
const adapter = {
|
|
167
|
+
getInputValue: async () => ' ',
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const result = await checkIfElementEmpty({
|
|
171
|
+
page,
|
|
172
|
+
engine: 'playwright',
|
|
173
|
+
locatorOrElement: {},
|
|
174
|
+
adapter,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
assert.strictEqual(result, true);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should return false for element with content', async () => {
|
|
181
|
+
const page = createMockPlaywrightPage();
|
|
182
|
+
const adapter = {
|
|
183
|
+
getInputValue: async () => 'some content',
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const result = await checkIfElementEmpty({
|
|
187
|
+
page,
|
|
188
|
+
engine: 'playwright',
|
|
189
|
+
locatorOrElement: {},
|
|
190
|
+
adapter,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
assert.strictEqual(result, false);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should handle navigation errors', async () => {
|
|
197
|
+
const page = createMockPlaywrightPage();
|
|
198
|
+
const adapter = {
|
|
199
|
+
getInputValue: async () => {
|
|
200
|
+
throw new Error('Execution context was destroyed');
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const result = await checkIfElementEmpty({
|
|
205
|
+
page,
|
|
206
|
+
engine: 'playwright',
|
|
207
|
+
locatorOrElement: {},
|
|
208
|
+
adapter,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
assert.strictEqual(result, true);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('performFill', () => {
|
|
216
|
+
it('should throw when text is not provided', async () => {
|
|
217
|
+
const page = createMockPlaywrightPage();
|
|
218
|
+
const log = createMockLogger();
|
|
219
|
+
|
|
220
|
+
await assert.rejects(
|
|
221
|
+
() =>
|
|
222
|
+
performFill({
|
|
223
|
+
page,
|
|
224
|
+
engine: 'playwright',
|
|
225
|
+
locatorOrElement: {},
|
|
226
|
+
log,
|
|
227
|
+
}),
|
|
228
|
+
/text is required/
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should throw when locatorOrElement is not provided', async () => {
|
|
233
|
+
const page = createMockPlaywrightPage();
|
|
234
|
+
const log = createMockLogger();
|
|
235
|
+
|
|
236
|
+
await assert.rejects(
|
|
237
|
+
() =>
|
|
238
|
+
performFill({
|
|
239
|
+
page,
|
|
240
|
+
engine: 'playwright',
|
|
241
|
+
text: 'test',
|
|
242
|
+
log,
|
|
243
|
+
}),
|
|
244
|
+
/locatorOrElement is required/
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should fill with typing simulation', async () => {
|
|
249
|
+
const page = createMockPlaywrightPage();
|
|
250
|
+
const log = createMockLogger();
|
|
251
|
+
let typeCalled = false;
|
|
252
|
+
const adapter = {
|
|
253
|
+
type: async (el, text) => {
|
|
254
|
+
typeCalled = true;
|
|
255
|
+
},
|
|
256
|
+
getInputValue: async () => 'test text',
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const result = await performFill({
|
|
260
|
+
page,
|
|
261
|
+
engine: 'playwright',
|
|
262
|
+
locatorOrElement: {},
|
|
263
|
+
text: 'test text',
|
|
264
|
+
simulateTyping: true,
|
|
265
|
+
verify: false,
|
|
266
|
+
log,
|
|
267
|
+
adapter,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
assert.strictEqual(result.filled, true);
|
|
271
|
+
assert.strictEqual(typeCalled, true);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('should fill without typing simulation', async () => {
|
|
275
|
+
const page = createMockPlaywrightPage();
|
|
276
|
+
const log = createMockLogger();
|
|
277
|
+
let fillCalled = false;
|
|
278
|
+
const adapter = {
|
|
279
|
+
fill: async (el, text) => {
|
|
280
|
+
fillCalled = true;
|
|
281
|
+
},
|
|
282
|
+
getInputValue: async () => 'test text',
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const result = await performFill({
|
|
286
|
+
page,
|
|
287
|
+
engine: 'playwright',
|
|
288
|
+
locatorOrElement: {},
|
|
289
|
+
text: 'test text',
|
|
290
|
+
simulateTyping: false,
|
|
291
|
+
verify: false,
|
|
292
|
+
log,
|
|
293
|
+
adapter,
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
assert.strictEqual(result.filled, true);
|
|
297
|
+
assert.strictEqual(fillCalled, true);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('should handle navigation errors', async () => {
|
|
301
|
+
const page = createMockPlaywrightPage();
|
|
302
|
+
const log = createMockLogger();
|
|
303
|
+
const adapter = {
|
|
304
|
+
type: async () => {
|
|
305
|
+
throw new Error('Execution context was destroyed');
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const result = await performFill({
|
|
310
|
+
page,
|
|
311
|
+
engine: 'playwright',
|
|
312
|
+
locatorOrElement: {},
|
|
313
|
+
text: 'test',
|
|
314
|
+
simulateTyping: true,
|
|
315
|
+
verify: false,
|
|
316
|
+
log,
|
|
317
|
+
adapter,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
assert.strictEqual(result.filled, false);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
describe('fillTextArea', () => {
|
|
325
|
+
it('should throw when selector is not provided', async () => {
|
|
326
|
+
const page = createMockPlaywrightPage();
|
|
327
|
+
const log = createMockLogger();
|
|
328
|
+
const wait = async () => {};
|
|
329
|
+
|
|
330
|
+
await assert.rejects(
|
|
331
|
+
() =>
|
|
332
|
+
fillTextArea({
|
|
333
|
+
page,
|
|
334
|
+
engine: 'playwright',
|
|
335
|
+
log,
|
|
336
|
+
wait,
|
|
337
|
+
text: 'test',
|
|
338
|
+
}),
|
|
339
|
+
/selector and text are required/
|
|
340
|
+
);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('should throw when text is not provided', async () => {
|
|
344
|
+
const page = createMockPlaywrightPage();
|
|
345
|
+
const log = createMockLogger();
|
|
346
|
+
const wait = async () => {};
|
|
347
|
+
|
|
348
|
+
await assert.rejects(
|
|
349
|
+
() =>
|
|
350
|
+
fillTextArea({
|
|
351
|
+
page,
|
|
352
|
+
engine: 'playwright',
|
|
353
|
+
log,
|
|
354
|
+
wait,
|
|
355
|
+
selector: 'textarea',
|
|
356
|
+
}),
|
|
357
|
+
/selector and text are required/
|
|
358
|
+
);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('should throw when page is not provided', async () => {
|
|
362
|
+
const log = createMockLogger();
|
|
363
|
+
const wait = async () => {};
|
|
364
|
+
|
|
365
|
+
await assert.rejects(
|
|
366
|
+
() =>
|
|
367
|
+
fillTextArea({
|
|
368
|
+
engine: 'playwright',
|
|
369
|
+
log,
|
|
370
|
+
wait,
|
|
371
|
+
selector: 'textarea',
|
|
372
|
+
text: 'test',
|
|
373
|
+
}),
|
|
374
|
+
/page is required/
|
|
375
|
+
);
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
});
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import {
|
|
4
|
+
defaultScrollVerification,
|
|
5
|
+
verifyScroll,
|
|
6
|
+
scrollIntoView,
|
|
7
|
+
needsScrolling,
|
|
8
|
+
scrollIntoViewIfNeeded,
|
|
9
|
+
} from '../../../src/interactions/scroll.js';
|
|
10
|
+
import {
|
|
11
|
+
createMockPlaywrightPage,
|
|
12
|
+
createMockPuppeteerPage,
|
|
13
|
+
createMockLogger,
|
|
14
|
+
} from '../../helpers/mocks.js';
|
|
15
|
+
|
|
16
|
+
describe('scroll', () => {
|
|
17
|
+
describe('defaultScrollVerification', () => {
|
|
18
|
+
it('should verify element is in viewport for Playwright', async () => {
|
|
19
|
+
const page = createMockPlaywrightPage();
|
|
20
|
+
const mockLocator = {
|
|
21
|
+
evaluate: async (fn, margin) => true, // Element is in viewport
|
|
22
|
+
};
|
|
23
|
+
const result = await defaultScrollVerification({
|
|
24
|
+
page,
|
|
25
|
+
engine: 'playwright',
|
|
26
|
+
locatorOrElement: mockLocator,
|
|
27
|
+
});
|
|
28
|
+
assert.strictEqual(result.verified, true);
|
|
29
|
+
assert.strictEqual(result.inViewport, true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should return false when element not in viewport', async () => {
|
|
33
|
+
const page = createMockPlaywrightPage();
|
|
34
|
+
const mockLocator = {
|
|
35
|
+
evaluate: async (fn, margin) => false, // Element not in viewport
|
|
36
|
+
};
|
|
37
|
+
const result = await defaultScrollVerification({
|
|
38
|
+
page,
|
|
39
|
+
engine: 'playwright',
|
|
40
|
+
locatorOrElement: mockLocator,
|
|
41
|
+
});
|
|
42
|
+
assert.strictEqual(result.verified, false);
|
|
43
|
+
assert.strictEqual(result.inViewport, false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should handle navigation errors', async () => {
|
|
47
|
+
const page = createMockPlaywrightPage();
|
|
48
|
+
const mockLocator = {
|
|
49
|
+
evaluate: async () => {
|
|
50
|
+
throw new Error('Execution context was destroyed');
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
const result = await defaultScrollVerification({
|
|
54
|
+
page,
|
|
55
|
+
engine: 'playwright',
|
|
56
|
+
locatorOrElement: mockLocator,
|
|
57
|
+
});
|
|
58
|
+
assert.strictEqual(result.verified, false);
|
|
59
|
+
assert.strictEqual(result.navigationError, true);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('verifyScroll', () => {
|
|
64
|
+
it('should verify scroll with retry logic', async () => {
|
|
65
|
+
const page = createMockPlaywrightPage();
|
|
66
|
+
const log = createMockLogger();
|
|
67
|
+
const mockLocator = {
|
|
68
|
+
evaluate: async () => true,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const result = await verifyScroll({
|
|
72
|
+
page,
|
|
73
|
+
engine: 'playwright',
|
|
74
|
+
locatorOrElement: mockLocator,
|
|
75
|
+
log,
|
|
76
|
+
timeout: 100,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
assert.strictEqual(result.verified, true);
|
|
80
|
+
assert.ok(result.attempts >= 1);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should fail after timeout if element never in viewport', async () => {
|
|
84
|
+
const page = createMockPlaywrightPage();
|
|
85
|
+
const log = createMockLogger();
|
|
86
|
+
const mockLocator = {
|
|
87
|
+
evaluate: async () => false,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const result = await verifyScroll({
|
|
91
|
+
page,
|
|
92
|
+
engine: 'playwright',
|
|
93
|
+
locatorOrElement: mockLocator,
|
|
94
|
+
log,
|
|
95
|
+
timeout: 50,
|
|
96
|
+
retryInterval: 10,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
assert.strictEqual(result.verified, false);
|
|
100
|
+
assert.ok(result.attempts >= 1);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('scrollIntoView', () => {
|
|
105
|
+
it('should throw when locatorOrElement is not provided', async () => {
|
|
106
|
+
const page = createMockPlaywrightPage();
|
|
107
|
+
await assert.rejects(
|
|
108
|
+
() => scrollIntoView({ page, engine: 'playwright' }),
|
|
109
|
+
/locatorOrElement is required/
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should scroll element into view for Playwright', async () => {
|
|
114
|
+
const page = createMockPlaywrightPage();
|
|
115
|
+
const log = createMockLogger();
|
|
116
|
+
let scrollCalled = false;
|
|
117
|
+
const mockLocator = {
|
|
118
|
+
evaluate: async (fn, behavior) => {
|
|
119
|
+
scrollCalled = true;
|
|
120
|
+
return true;
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const result = await scrollIntoView({
|
|
125
|
+
page,
|
|
126
|
+
engine: 'playwright',
|
|
127
|
+
locatorOrElement: mockLocator,
|
|
128
|
+
log,
|
|
129
|
+
verify: false,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
assert.strictEqual(result.scrolled, true);
|
|
133
|
+
assert.strictEqual(scrollCalled, true);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should use smooth behavior by default', async () => {
|
|
137
|
+
const page = createMockPlaywrightPage();
|
|
138
|
+
const log = createMockLogger();
|
|
139
|
+
let receivedBehavior = null;
|
|
140
|
+
const mockLocator = {
|
|
141
|
+
evaluate: async (fn, behavior) => {
|
|
142
|
+
receivedBehavior = behavior;
|
|
143
|
+
return true;
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
await scrollIntoView({
|
|
148
|
+
page,
|
|
149
|
+
engine: 'playwright',
|
|
150
|
+
locatorOrElement: mockLocator,
|
|
151
|
+
log,
|
|
152
|
+
verify: false,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
assert.strictEqual(receivedBehavior, 'smooth');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should use instant behavior when specified', async () => {
|
|
159
|
+
const page = createMockPlaywrightPage();
|
|
160
|
+
const log = createMockLogger();
|
|
161
|
+
let receivedBehavior = null;
|
|
162
|
+
const mockLocator = {
|
|
163
|
+
evaluate: async (fn, behavior) => {
|
|
164
|
+
receivedBehavior = behavior;
|
|
165
|
+
return true;
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
await scrollIntoView({
|
|
170
|
+
page,
|
|
171
|
+
engine: 'playwright',
|
|
172
|
+
locatorOrElement: mockLocator,
|
|
173
|
+
behavior: 'instant',
|
|
174
|
+
log,
|
|
175
|
+
verify: false,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
assert.strictEqual(receivedBehavior, 'instant');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should handle navigation errors', async () => {
|
|
182
|
+
const page = createMockPlaywrightPage();
|
|
183
|
+
const log = createMockLogger();
|
|
184
|
+
const mockLocator = {
|
|
185
|
+
evaluate: async () => {
|
|
186
|
+
throw new Error('Execution context was destroyed');
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const result = await scrollIntoView({
|
|
191
|
+
page,
|
|
192
|
+
engine: 'playwright',
|
|
193
|
+
locatorOrElement: mockLocator,
|
|
194
|
+
log,
|
|
195
|
+
verify: false,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
assert.strictEqual(result.scrolled, false);
|
|
199
|
+
assert.strictEqual(result.verified, false);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('needsScrolling', () => {
|
|
204
|
+
it('should throw when locatorOrElement is not provided', async () => {
|
|
205
|
+
const page = createMockPlaywrightPage();
|
|
206
|
+
await assert.rejects(
|
|
207
|
+
() => needsScrolling({ page, engine: 'playwright' }),
|
|
208
|
+
/locatorOrElement is required/
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should return true when element needs scrolling', async () => {
|
|
213
|
+
const page = createMockPlaywrightPage();
|
|
214
|
+
const mockLocator = {
|
|
215
|
+
evaluate: async () => true, // needs scrolling
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const result = await needsScrolling({
|
|
219
|
+
page,
|
|
220
|
+
engine: 'playwright',
|
|
221
|
+
locatorOrElement: mockLocator,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
assert.strictEqual(result, true);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should return false when element is in view', async () => {
|
|
228
|
+
const page = createMockPlaywrightPage();
|
|
229
|
+
const mockLocator = {
|
|
230
|
+
evaluate: async () => false, // doesn't need scrolling
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const result = await needsScrolling({
|
|
234
|
+
page,
|
|
235
|
+
engine: 'playwright',
|
|
236
|
+
locatorOrElement: mockLocator,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
assert.strictEqual(result, false);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should handle navigation errors', async () => {
|
|
243
|
+
const page = createMockPlaywrightPage();
|
|
244
|
+
const mockLocator = {
|
|
245
|
+
evaluate: async () => {
|
|
246
|
+
throw new Error('Execution context was destroyed');
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const result = await needsScrolling({
|
|
251
|
+
page,
|
|
252
|
+
engine: 'playwright',
|
|
253
|
+
locatorOrElement: mockLocator,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
assert.strictEqual(result, false);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe('scrollIntoViewIfNeeded', () => {
|
|
261
|
+
it('should throw when locatorOrElement is not provided', async () => {
|
|
262
|
+
const page = createMockPlaywrightPage();
|
|
263
|
+
const log = createMockLogger();
|
|
264
|
+
const wait = async () => {};
|
|
265
|
+
|
|
266
|
+
await assert.rejects(
|
|
267
|
+
() => scrollIntoViewIfNeeded({ page, engine: 'playwright', log, wait }),
|
|
268
|
+
/locatorOrElement is required/
|
|
269
|
+
);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('should skip scrolling when element is in view', async () => {
|
|
273
|
+
const page = createMockPlaywrightPage();
|
|
274
|
+
const log = createMockLogger();
|
|
275
|
+
const wait = async () => {};
|
|
276
|
+
let scrollCalled = false;
|
|
277
|
+
const mockLocator = {
|
|
278
|
+
evaluate: async (fn, arg) => {
|
|
279
|
+
// First call checks if scrolling needed, second is for scroll
|
|
280
|
+
if (!scrollCalled) {
|
|
281
|
+
return false; // doesn't need scrolling
|
|
282
|
+
}
|
|
283
|
+
scrollCalled = true;
|
|
284
|
+
return true;
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const result = await scrollIntoViewIfNeeded({
|
|
289
|
+
page,
|
|
290
|
+
engine: 'playwright',
|
|
291
|
+
log,
|
|
292
|
+
wait,
|
|
293
|
+
locatorOrElement: mockLocator,
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
assert.strictEqual(result.scrolled, false);
|
|
297
|
+
assert.strictEqual(result.skipped, true);
|
|
298
|
+
assert.strictEqual(result.verified, true);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('should scroll when element needs scrolling', async () => {
|
|
302
|
+
const page = createMockPlaywrightPage();
|
|
303
|
+
const log = createMockLogger();
|
|
304
|
+
const wait = async () => {};
|
|
305
|
+
let callCount = 0;
|
|
306
|
+
const mockLocator = {
|
|
307
|
+
evaluate: async (fn, arg) => {
|
|
308
|
+
callCount++;
|
|
309
|
+
if (callCount === 1) {
|
|
310
|
+
return true; // needs scrolling
|
|
311
|
+
}
|
|
312
|
+
// Subsequent calls are for scroll and verification
|
|
313
|
+
return true;
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const result = await scrollIntoViewIfNeeded({
|
|
318
|
+
page,
|
|
319
|
+
engine: 'playwright',
|
|
320
|
+
log,
|
|
321
|
+
wait,
|
|
322
|
+
locatorOrElement: mockLocator,
|
|
323
|
+
verify: false,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
assert.strictEqual(result.scrolled, true);
|
|
327
|
+
assert.strictEqual(result.skipped, false);
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
});
|