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,218 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { createBoundFunctions } from '../../src/bindings.js';
|
|
4
|
+
import {
|
|
5
|
+
createMockPlaywrightPage,
|
|
6
|
+
createMockLogger,
|
|
7
|
+
createMockNavigationManager,
|
|
8
|
+
createMockNetworkTracker,
|
|
9
|
+
} from '../helpers/mocks.js';
|
|
10
|
+
|
|
11
|
+
describe('bindings', () => {
|
|
12
|
+
describe('createBoundFunctions', () => {
|
|
13
|
+
it('should create bindings with minimal options', () => {
|
|
14
|
+
const page = createMockPlaywrightPage();
|
|
15
|
+
const log = createMockLogger();
|
|
16
|
+
|
|
17
|
+
const bindings = createBoundFunctions({
|
|
18
|
+
page,
|
|
19
|
+
engine: 'playwright',
|
|
20
|
+
log,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
assert.ok(bindings);
|
|
24
|
+
assert.ok(typeof bindings === 'object');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should create bindings with all options', () => {
|
|
28
|
+
const page = createMockPlaywrightPage();
|
|
29
|
+
const log = createMockLogger();
|
|
30
|
+
const navigationManager = createMockNavigationManager();
|
|
31
|
+
const networkTracker = createMockNetworkTracker();
|
|
32
|
+
|
|
33
|
+
const bindings = createBoundFunctions({
|
|
34
|
+
page,
|
|
35
|
+
engine: 'playwright',
|
|
36
|
+
log,
|
|
37
|
+
verbose: true,
|
|
38
|
+
navigationManager,
|
|
39
|
+
networkTracker,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
assert.ok(bindings);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should have all expected bound functions', () => {
|
|
46
|
+
const page = createMockPlaywrightPage();
|
|
47
|
+
const log = createMockLogger();
|
|
48
|
+
|
|
49
|
+
const bindings = createBoundFunctions({
|
|
50
|
+
page,
|
|
51
|
+
engine: 'playwright',
|
|
52
|
+
log,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Verify it returns an object with functions
|
|
56
|
+
assert.ok(bindings);
|
|
57
|
+
assert.ok(typeof bindings === 'object');
|
|
58
|
+
|
|
59
|
+
// Navigation
|
|
60
|
+
assert.ok(
|
|
61
|
+
typeof bindings.goto === 'function',
|
|
62
|
+
'goto should be a function'
|
|
63
|
+
);
|
|
64
|
+
assert.ok(
|
|
65
|
+
typeof bindings.getUrl === 'function',
|
|
66
|
+
'getUrl should be a function'
|
|
67
|
+
);
|
|
68
|
+
assert.ok(
|
|
69
|
+
typeof bindings.waitForNavigation === 'function',
|
|
70
|
+
'waitForNavigation should be a function'
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Selectors
|
|
74
|
+
assert.ok(
|
|
75
|
+
typeof bindings.querySelector === 'function',
|
|
76
|
+
'querySelector should be a function'
|
|
77
|
+
);
|
|
78
|
+
assert.ok(
|
|
79
|
+
typeof bindings.querySelectorAll === 'function',
|
|
80
|
+
'querySelectorAll should be a function'
|
|
81
|
+
);
|
|
82
|
+
assert.ok(
|
|
83
|
+
typeof bindings.findByText === 'function',
|
|
84
|
+
'findByText should be a function'
|
|
85
|
+
);
|
|
86
|
+
assert.ok(
|
|
87
|
+
typeof bindings.waitForSelector === 'function',
|
|
88
|
+
'waitForSelector should be a function'
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// Interactions
|
|
92
|
+
assert.ok(
|
|
93
|
+
typeof bindings.clickButton === 'function',
|
|
94
|
+
'clickButton should be a function'
|
|
95
|
+
);
|
|
96
|
+
assert.ok(
|
|
97
|
+
typeof bindings.fillTextArea === 'function',
|
|
98
|
+
'fillTextArea should be a function'
|
|
99
|
+
);
|
|
100
|
+
assert.ok(
|
|
101
|
+
typeof bindings.scrollIntoViewIfNeeded === 'function',
|
|
102
|
+
'scrollIntoViewIfNeeded should be a function'
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// Element state
|
|
106
|
+
assert.ok(
|
|
107
|
+
typeof bindings.isVisible === 'function',
|
|
108
|
+
'isVisible should be a function'
|
|
109
|
+
);
|
|
110
|
+
assert.ok(
|
|
111
|
+
typeof bindings.isEnabled === 'function',
|
|
112
|
+
'isEnabled should be a function'
|
|
113
|
+
);
|
|
114
|
+
assert.ok(
|
|
115
|
+
typeof bindings.count === 'function',
|
|
116
|
+
'count should be a function'
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// Content
|
|
120
|
+
assert.ok(
|
|
121
|
+
typeof bindings.textContent === 'function',
|
|
122
|
+
'textContent should be a function'
|
|
123
|
+
);
|
|
124
|
+
assert.ok(
|
|
125
|
+
typeof bindings.inputValue === 'function',
|
|
126
|
+
'inputValue should be a function'
|
|
127
|
+
);
|
|
128
|
+
assert.ok(
|
|
129
|
+
typeof bindings.getAttribute === 'function',
|
|
130
|
+
'getAttribute should be a function'
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// Utilities
|
|
134
|
+
assert.ok(
|
|
135
|
+
typeof bindings.wait === 'function',
|
|
136
|
+
'wait should be a function'
|
|
137
|
+
);
|
|
138
|
+
assert.ok(
|
|
139
|
+
typeof bindings.evaluate === 'function',
|
|
140
|
+
'evaluate should be a function'
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should pre-bind page and engine to functions', async () => {
|
|
145
|
+
const page = createMockPlaywrightPage({ url: 'https://example.com' });
|
|
146
|
+
const log = createMockLogger();
|
|
147
|
+
|
|
148
|
+
const bindings = createBoundFunctions({
|
|
149
|
+
page,
|
|
150
|
+
engine: 'playwright',
|
|
151
|
+
log,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// getUrl should work without passing page
|
|
155
|
+
const url = bindings.getUrl();
|
|
156
|
+
assert.strictEqual(url, 'https://example.com');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should pre-bind log to functions', async () => {
|
|
160
|
+
const page = createMockPlaywrightPage();
|
|
161
|
+
const log = createMockLogger({ collectLogs: true });
|
|
162
|
+
|
|
163
|
+
const bindings = createBoundFunctions({
|
|
164
|
+
page,
|
|
165
|
+
engine: 'playwright',
|
|
166
|
+
log,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// wait should work without passing log
|
|
170
|
+
await bindings.wait({ ms: 1 });
|
|
171
|
+
// Should not throw
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should allow verbose option', () => {
|
|
175
|
+
const page = createMockPlaywrightPage();
|
|
176
|
+
const log = createMockLogger();
|
|
177
|
+
|
|
178
|
+
const bindings = createBoundFunctions({
|
|
179
|
+
page,
|
|
180
|
+
engine: 'playwright',
|
|
181
|
+
log,
|
|
182
|
+
verbose: true,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
assert.ok(bindings);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should integrate with navigationManager when provided', () => {
|
|
189
|
+
const page = createMockPlaywrightPage();
|
|
190
|
+
const log = createMockLogger();
|
|
191
|
+
const navigationManager = createMockNavigationManager();
|
|
192
|
+
|
|
193
|
+
const bindings = createBoundFunctions({
|
|
194
|
+
page,
|
|
195
|
+
engine: 'playwright',
|
|
196
|
+
log,
|
|
197
|
+
navigationManager,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
assert.ok(bindings);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should integrate with networkTracker when provided', () => {
|
|
204
|
+
const page = createMockPlaywrightPage();
|
|
205
|
+
const log = createMockLogger();
|
|
206
|
+
const networkTracker = createMockNetworkTracker();
|
|
207
|
+
|
|
208
|
+
const bindings = createBoundFunctions({
|
|
209
|
+
page,
|
|
210
|
+
engine: 'playwright',
|
|
211
|
+
log,
|
|
212
|
+
networkTracker,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
assert.ok(bindings);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
});
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import {
|
|
4
|
+
defaultNavigationVerification,
|
|
5
|
+
verifyNavigation,
|
|
6
|
+
waitForUrlStabilization,
|
|
7
|
+
goto,
|
|
8
|
+
waitForNavigation,
|
|
9
|
+
waitForPageReady,
|
|
10
|
+
waitAfterAction,
|
|
11
|
+
} from '../../../src/browser/navigation.js';
|
|
12
|
+
import {
|
|
13
|
+
createMockPlaywrightPage,
|
|
14
|
+
createMockLogger,
|
|
15
|
+
createMockNavigationManager,
|
|
16
|
+
createMockNetworkTracker,
|
|
17
|
+
} from '../../helpers/mocks.js';
|
|
18
|
+
|
|
19
|
+
describe('navigation', () => {
|
|
20
|
+
describe('defaultNavigationVerification', () => {
|
|
21
|
+
it('should verify exact URL match', async () => {
|
|
22
|
+
const page = createMockPlaywrightPage({
|
|
23
|
+
url: 'https://example.com/page',
|
|
24
|
+
});
|
|
25
|
+
const result = await defaultNavigationVerification({
|
|
26
|
+
page,
|
|
27
|
+
expectedUrl: 'https://example.com/page',
|
|
28
|
+
});
|
|
29
|
+
assert.strictEqual(result.verified, true);
|
|
30
|
+
assert.ok(result.reason.includes('exact'));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should verify URL pattern match', async () => {
|
|
34
|
+
const page = createMockPlaywrightPage({
|
|
35
|
+
url: 'https://example.com/page?foo=bar',
|
|
36
|
+
});
|
|
37
|
+
const result = await defaultNavigationVerification({
|
|
38
|
+
page,
|
|
39
|
+
expectedUrl: 'https://example.com/page',
|
|
40
|
+
});
|
|
41
|
+
assert.strictEqual(result.verified, true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should fail verification on URL mismatch', async () => {
|
|
45
|
+
const page = createMockPlaywrightPage({
|
|
46
|
+
url: 'https://example.com/other',
|
|
47
|
+
});
|
|
48
|
+
const result = await defaultNavigationVerification({
|
|
49
|
+
page,
|
|
50
|
+
expectedUrl: 'https://example.com/page',
|
|
51
|
+
});
|
|
52
|
+
assert.strictEqual(result.verified, false);
|
|
53
|
+
assert.ok(result.reason.includes('mismatch'));
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should verify URL changed from start', async () => {
|
|
57
|
+
const page = createMockPlaywrightPage({ url: 'https://example.com/new' });
|
|
58
|
+
const result = await defaultNavigationVerification({
|
|
59
|
+
page,
|
|
60
|
+
startUrl: 'https://example.com/old',
|
|
61
|
+
});
|
|
62
|
+
assert.strictEqual(result.verified, true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should verify navigation completed without expectations', async () => {
|
|
66
|
+
const page = createMockPlaywrightPage({ url: 'https://example.com' });
|
|
67
|
+
const result = await defaultNavigationVerification({
|
|
68
|
+
page,
|
|
69
|
+
});
|
|
70
|
+
assert.strictEqual(result.verified, true);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('verifyNavigation', () => {
|
|
75
|
+
it('should verify navigation with retry logic', async () => {
|
|
76
|
+
const page = createMockPlaywrightPage({
|
|
77
|
+
url: 'https://example.com/target',
|
|
78
|
+
});
|
|
79
|
+
const log = createMockLogger();
|
|
80
|
+
|
|
81
|
+
const result = await verifyNavigation({
|
|
82
|
+
page,
|
|
83
|
+
expectedUrl: 'https://example.com/target',
|
|
84
|
+
timeout: 100,
|
|
85
|
+
log,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
assert.strictEqual(result.verified, true);
|
|
89
|
+
assert.ok(result.attempts >= 1);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should fail after timeout', async () => {
|
|
93
|
+
const page = createMockPlaywrightPage({
|
|
94
|
+
url: 'https://example.com/wrong',
|
|
95
|
+
});
|
|
96
|
+
const log = createMockLogger();
|
|
97
|
+
|
|
98
|
+
const result = await verifyNavigation({
|
|
99
|
+
page,
|
|
100
|
+
expectedUrl: 'https://example.com/target',
|
|
101
|
+
timeout: 50,
|
|
102
|
+
retryInterval: 10,
|
|
103
|
+
log,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
assert.strictEqual(result.verified, false);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('waitForUrlStabilization', () => {
|
|
111
|
+
it('should delegate to navigationManager if available', async () => {
|
|
112
|
+
const page = createMockPlaywrightPage();
|
|
113
|
+
const log = createMockLogger();
|
|
114
|
+
const wait = async () => {};
|
|
115
|
+
const navigationManager = createMockNavigationManager();
|
|
116
|
+
let waitForPageReadyCalled = false;
|
|
117
|
+
navigationManager.waitForPageReady = async () => {
|
|
118
|
+
waitForPageReadyCalled = true;
|
|
119
|
+
return true;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const result = await waitForUrlStabilization({
|
|
123
|
+
page,
|
|
124
|
+
log,
|
|
125
|
+
wait,
|
|
126
|
+
navigationManager,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
assert.strictEqual(waitForPageReadyCalled, true);
|
|
130
|
+
assert.strictEqual(result, true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should use polling approach without navigationManager', async () => {
|
|
134
|
+
const page = createMockPlaywrightPage({ url: 'https://example.com' });
|
|
135
|
+
const log = createMockLogger();
|
|
136
|
+
const wait = async () => {};
|
|
137
|
+
|
|
138
|
+
const result = await waitForUrlStabilization({
|
|
139
|
+
page,
|
|
140
|
+
log,
|
|
141
|
+
wait,
|
|
142
|
+
stableChecks: 1,
|
|
143
|
+
checkInterval: 10,
|
|
144
|
+
timeout: 100,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
assert.strictEqual(result, true);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('goto', () => {
|
|
152
|
+
it('should throw when url is not provided', async () => {
|
|
153
|
+
const page = createMockPlaywrightPage();
|
|
154
|
+
|
|
155
|
+
await assert.rejects(() => goto({ page }), /url is required/);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should navigate using navigationManager', async () => {
|
|
159
|
+
const page = createMockPlaywrightPage({
|
|
160
|
+
url: 'https://example.com/target',
|
|
161
|
+
});
|
|
162
|
+
const log = createMockLogger();
|
|
163
|
+
const navigationManager = createMockNavigationManager();
|
|
164
|
+
let navigateCalled = false;
|
|
165
|
+
navigationManager.navigate = async ({ url }) => {
|
|
166
|
+
navigateCalled = true;
|
|
167
|
+
return true;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const result = await goto({
|
|
171
|
+
page,
|
|
172
|
+
navigationManager,
|
|
173
|
+
log,
|
|
174
|
+
url: 'https://example.com/target',
|
|
175
|
+
verify: false,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
assert.strictEqual(navigateCalled, true);
|
|
179
|
+
assert.strictEqual(result.navigated, true);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should navigate without navigationManager', async () => {
|
|
183
|
+
const page = createMockPlaywrightPage({
|
|
184
|
+
url: 'https://example.com/target',
|
|
185
|
+
});
|
|
186
|
+
const log = createMockLogger();
|
|
187
|
+
let gotoCalled = false;
|
|
188
|
+
page.goto = async () => {
|
|
189
|
+
gotoCalled = true;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const result = await goto({
|
|
193
|
+
page,
|
|
194
|
+
log,
|
|
195
|
+
url: 'https://example.com/target',
|
|
196
|
+
waitForStableUrlBefore: false,
|
|
197
|
+
waitForStableUrlAfter: false,
|
|
198
|
+
verify: false,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
assert.strictEqual(gotoCalled, true);
|
|
202
|
+
assert.strictEqual(result.navigated, true);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('waitForNavigation', () => {
|
|
207
|
+
it('should delegate to navigationManager if available', async () => {
|
|
208
|
+
const page = createMockPlaywrightPage();
|
|
209
|
+
const navigationManager = createMockNavigationManager();
|
|
210
|
+
let waitCalled = false;
|
|
211
|
+
navigationManager.waitForNavigation = async () => {
|
|
212
|
+
waitCalled = true;
|
|
213
|
+
return true;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const result = await waitForNavigation({
|
|
217
|
+
page,
|
|
218
|
+
navigationManager,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
assert.strictEqual(waitCalled, true);
|
|
222
|
+
assert.strictEqual(result, true);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should use page.waitForNavigation without navigationManager', async () => {
|
|
226
|
+
const page = createMockPlaywrightPage();
|
|
227
|
+
let waitCalled = false;
|
|
228
|
+
page.waitForNavigation = async () => {
|
|
229
|
+
waitCalled = true;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const result = await waitForNavigation({ page });
|
|
233
|
+
|
|
234
|
+
assert.strictEqual(waitCalled, true);
|
|
235
|
+
assert.strictEqual(result, true);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should handle navigation errors', async () => {
|
|
239
|
+
const page = createMockPlaywrightPage();
|
|
240
|
+
page.waitForNavigation = async () => {
|
|
241
|
+
throw new Error('Execution context was destroyed');
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const result = await waitForNavigation({ page });
|
|
245
|
+
|
|
246
|
+
assert.strictEqual(result, false);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe('waitForPageReady', () => {
|
|
251
|
+
it('should delegate to navigationManager if available', async () => {
|
|
252
|
+
const page = createMockPlaywrightPage();
|
|
253
|
+
const log = createMockLogger();
|
|
254
|
+
const wait = async () => {};
|
|
255
|
+
const navigationManager = createMockNavigationManager();
|
|
256
|
+
let waitCalled = false;
|
|
257
|
+
navigationManager.waitForPageReady = async () => {
|
|
258
|
+
waitCalled = true;
|
|
259
|
+
return true;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const result = await waitForPageReady({
|
|
263
|
+
page,
|
|
264
|
+
log,
|
|
265
|
+
wait,
|
|
266
|
+
navigationManager,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
assert.strictEqual(waitCalled, true);
|
|
270
|
+
assert.strictEqual(result, true);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('should use networkTracker if navigationManager not available', async () => {
|
|
274
|
+
const page = createMockPlaywrightPage();
|
|
275
|
+
const log = createMockLogger();
|
|
276
|
+
const wait = async () => {};
|
|
277
|
+
const networkTracker = createMockNetworkTracker();
|
|
278
|
+
let waitCalled = false;
|
|
279
|
+
networkTracker.waitForNetworkIdle = async () => {
|
|
280
|
+
waitCalled = true;
|
|
281
|
+
return true;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const result = await waitForPageReady({
|
|
285
|
+
page,
|
|
286
|
+
log,
|
|
287
|
+
wait,
|
|
288
|
+
networkTracker,
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
assert.strictEqual(waitCalled, true);
|
|
292
|
+
assert.strictEqual(result, true);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should use minimal fallback without both managers', async () => {
|
|
296
|
+
const page = createMockPlaywrightPage();
|
|
297
|
+
const log = createMockLogger();
|
|
298
|
+
const wait = async () => {};
|
|
299
|
+
|
|
300
|
+
const result = await waitForPageReady({
|
|
301
|
+
page,
|
|
302
|
+
log,
|
|
303
|
+
wait,
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
assert.strictEqual(result, true);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
describe('waitAfterAction', () => {
|
|
311
|
+
it('should detect URL change as navigation', async () => {
|
|
312
|
+
const page = createMockPlaywrightPage({ url: 'https://example.com/new' });
|
|
313
|
+
const log = createMockLogger();
|
|
314
|
+
const wait = async () => {};
|
|
315
|
+
|
|
316
|
+
const result = await waitAfterAction({
|
|
317
|
+
page,
|
|
318
|
+
log,
|
|
319
|
+
wait,
|
|
320
|
+
navigationCheckDelay: 0,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Page URL is stable, so no navigation detected
|
|
324
|
+
assert.ok(typeof result.navigated === 'boolean');
|
|
325
|
+
assert.ok(typeof result.ready === 'boolean');
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('should wait for network idle without navigation', async () => {
|
|
329
|
+
const page = createMockPlaywrightPage({ url: 'https://example.com' });
|
|
330
|
+
const log = createMockLogger();
|
|
331
|
+
const wait = async () => {};
|
|
332
|
+
const networkTracker = createMockNetworkTracker();
|
|
333
|
+
|
|
334
|
+
const result = await waitAfterAction({
|
|
335
|
+
page,
|
|
336
|
+
log,
|
|
337
|
+
wait,
|
|
338
|
+
networkTracker,
|
|
339
|
+
navigationCheckDelay: 0,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
assert.ok(typeof result.ready === 'boolean');
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { CHROME_ARGS, TIMING } from '../../../src/core/constants.js';
|
|
4
|
+
|
|
5
|
+
describe('constants', () => {
|
|
6
|
+
describe('CHROME_ARGS', () => {
|
|
7
|
+
it('should be an array', () => {
|
|
8
|
+
assert.ok(Array.isArray(CHROME_ARGS));
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should contain expected browser arguments', () => {
|
|
12
|
+
assert.ok(CHROME_ARGS.includes('--disable-infobars'));
|
|
13
|
+
assert.ok(CHROME_ARGS.includes('--no-first-run'));
|
|
14
|
+
assert.ok(CHROME_ARGS.includes('--no-default-browser-check'));
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should contain crash-related flags', () => {
|
|
18
|
+
assert.ok(CHROME_ARGS.includes('--disable-session-crashed-bubble'));
|
|
19
|
+
assert.ok(CHROME_ARGS.includes('--hide-crash-restore-bubble'));
|
|
20
|
+
assert.ok(CHROME_ARGS.includes('--disable-crash-restore'));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should have at least 5 arguments', () => {
|
|
24
|
+
assert.ok(CHROME_ARGS.length >= 5);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('TIMING', () => {
|
|
29
|
+
it('should be an object', () => {
|
|
30
|
+
assert.ok(typeof TIMING === 'object');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should have SCROLL_ANIMATION_WAIT property', () => {
|
|
34
|
+
assert.ok(typeof TIMING.SCROLL_ANIMATION_WAIT === 'number');
|
|
35
|
+
assert.ok(TIMING.SCROLL_ANIMATION_WAIT > 0);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should have DEFAULT_WAIT_AFTER_SCROLL property', () => {
|
|
39
|
+
assert.ok(typeof TIMING.DEFAULT_WAIT_AFTER_SCROLL === 'number');
|
|
40
|
+
assert.ok(TIMING.DEFAULT_WAIT_AFTER_SCROLL > 0);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should have VISIBILITY_CHECK_TIMEOUT property', () => {
|
|
44
|
+
assert.ok(typeof TIMING.VISIBILITY_CHECK_TIMEOUT === 'number');
|
|
45
|
+
assert.ok(TIMING.VISIBILITY_CHECK_TIMEOUT > 0);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should have DEFAULT_TIMEOUT property', () => {
|
|
49
|
+
assert.ok(typeof TIMING.DEFAULT_TIMEOUT === 'number');
|
|
50
|
+
assert.ok(TIMING.DEFAULT_TIMEOUT >= 1000);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should have VERIFICATION_TIMEOUT property', () => {
|
|
54
|
+
assert.ok(typeof TIMING.VERIFICATION_TIMEOUT === 'number');
|
|
55
|
+
assert.ok(TIMING.VERIFICATION_TIMEOUT > 0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should have VERIFICATION_RETRY_INTERVAL property', () => {
|
|
59
|
+
assert.ok(typeof TIMING.VERIFICATION_RETRY_INTERVAL === 'number');
|
|
60
|
+
assert.ok(TIMING.VERIFICATION_RETRY_INTERVAL > 0);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should have reasonable timeout values', () => {
|
|
64
|
+
// Verification retry should be shorter than verification timeout
|
|
65
|
+
assert.ok(
|
|
66
|
+
TIMING.VERIFICATION_RETRY_INTERVAL < TIMING.VERIFICATION_TIMEOUT
|
|
67
|
+
);
|
|
68
|
+
// Scroll animation wait should be relatively short
|
|
69
|
+
assert.ok(TIMING.SCROLL_ANIMATION_WAIT < 1000);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|