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,63 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { getUrl, unfocusAddressBar } from '../../../src/utilities/url.js';
|
|
4
|
+
import { createMockPlaywrightPage } from '../../helpers/mocks.js';
|
|
5
|
+
|
|
6
|
+
describe('url utilities', () => {
|
|
7
|
+
describe('getUrl', () => {
|
|
8
|
+
it('should return current page URL', () => {
|
|
9
|
+
const page = createMockPlaywrightPage({
|
|
10
|
+
url: 'https://example.com/page',
|
|
11
|
+
});
|
|
12
|
+
const url = getUrl({ page });
|
|
13
|
+
assert.strictEqual(url, 'https://example.com/page');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should return different URLs for different pages', () => {
|
|
17
|
+
const page1 = createMockPlaywrightPage({ url: 'https://example.com' });
|
|
18
|
+
const page2 = createMockPlaywrightPage({ url: 'https://other.com' });
|
|
19
|
+
|
|
20
|
+
assert.strictEqual(getUrl({ page: page1 }), 'https://example.com');
|
|
21
|
+
assert.strictEqual(getUrl({ page: page2 }), 'https://other.com');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('unfocusAddressBar', () => {
|
|
26
|
+
it('should throw when page is not provided', async () => {
|
|
27
|
+
await assert.rejects(() => unfocusAddressBar({}), /page is required/);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should call bringToFront on page', async () => {
|
|
31
|
+
let called = false;
|
|
32
|
+
const page = createMockPlaywrightPage();
|
|
33
|
+
page.bringToFront = async () => {
|
|
34
|
+
called = true;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
await unfocusAddressBar({ page });
|
|
38
|
+
assert.strictEqual(called, true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should not throw when bringToFront fails', async () => {
|
|
42
|
+
const page = createMockPlaywrightPage();
|
|
43
|
+
page.bringToFront = async () => {
|
|
44
|
+
throw new Error('Browser error');
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Should not throw
|
|
48
|
+
await unfocusAddressBar({ page });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should handle page without bringToFront method gracefully', async () => {
|
|
52
|
+
const page = createMockPlaywrightPage();
|
|
53
|
+
page.bringToFront = undefined;
|
|
54
|
+
|
|
55
|
+
// Should throw since bringToFront is not a function
|
|
56
|
+
try {
|
|
57
|
+
await unfocusAddressBar({ page });
|
|
58
|
+
} catch {
|
|
59
|
+
// Expected to fail when bringToFront is undefined
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { wait, evaluate, safeEvaluate } from '../../../src/utilities/wait.js';
|
|
4
|
+
import {
|
|
5
|
+
createMockPlaywrightPage,
|
|
6
|
+
createMockPuppeteerPage,
|
|
7
|
+
createMockLogger,
|
|
8
|
+
} from '../../helpers/mocks.js';
|
|
9
|
+
|
|
10
|
+
describe('wait utilities', () => {
|
|
11
|
+
describe('wait', () => {
|
|
12
|
+
it('should throw when ms is not provided', async () => {
|
|
13
|
+
const log = createMockLogger();
|
|
14
|
+
await assert.rejects(() => wait({ log }), /ms is required/);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should wait for specified time', async () => {
|
|
18
|
+
const log = createMockLogger();
|
|
19
|
+
const start = Date.now();
|
|
20
|
+
await wait({ log, ms: 50 });
|
|
21
|
+
const elapsed = Date.now() - start;
|
|
22
|
+
assert.ok(elapsed >= 45, `Expected at least 45ms, got ${elapsed}`);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should return completed status without abort signal', async () => {
|
|
26
|
+
const log = createMockLogger();
|
|
27
|
+
const result = await wait({ log, ms: 10 });
|
|
28
|
+
assert.strictEqual(result.completed, true);
|
|
29
|
+
assert.strictEqual(result.aborted, false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should accept reason for logging', async () => {
|
|
33
|
+
const log = createMockLogger({ collectLogs: true });
|
|
34
|
+
await wait({ log, ms: 10, reason: 'test wait' });
|
|
35
|
+
// Should not throw
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should handle abort signal that is already aborted', async () => {
|
|
39
|
+
const log = createMockLogger();
|
|
40
|
+
const controller = new AbortController();
|
|
41
|
+
controller.abort();
|
|
42
|
+
|
|
43
|
+
const result = await wait({
|
|
44
|
+
log,
|
|
45
|
+
ms: 1000,
|
|
46
|
+
abortSignal: controller.signal,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
assert.strictEqual(result.completed, false);
|
|
50
|
+
assert.strictEqual(result.aborted, true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should abort wait when signal is aborted', async () => {
|
|
54
|
+
const log = createMockLogger();
|
|
55
|
+
const controller = new AbortController();
|
|
56
|
+
|
|
57
|
+
const waitPromise = wait({
|
|
58
|
+
log,
|
|
59
|
+
ms: 10000,
|
|
60
|
+
abortSignal: controller.signal,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Abort after a short delay
|
|
64
|
+
setTimeout(() => controller.abort(), 10);
|
|
65
|
+
|
|
66
|
+
const result = await waitPromise;
|
|
67
|
+
assert.strictEqual(result.completed, false);
|
|
68
|
+
assert.strictEqual(result.aborted, true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should complete when not aborted', async () => {
|
|
72
|
+
const log = createMockLogger();
|
|
73
|
+
const controller = new AbortController();
|
|
74
|
+
|
|
75
|
+
const result = await wait({
|
|
76
|
+
log,
|
|
77
|
+
ms: 10,
|
|
78
|
+
abortSignal: controller.signal,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
assert.strictEqual(result.completed, true);
|
|
82
|
+
assert.strictEqual(result.aborted, false);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('evaluate', () => {
|
|
87
|
+
it('should throw when fn is not provided', async () => {
|
|
88
|
+
const page = createMockPlaywrightPage();
|
|
89
|
+
await assert.rejects(
|
|
90
|
+
() => evaluate({ page, engine: 'playwright' }),
|
|
91
|
+
/fn is required/
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should evaluate function on Playwright page', async () => {
|
|
96
|
+
const page = createMockPlaywrightPage({ evaluateResult: 'result' });
|
|
97
|
+
const result = await evaluate({
|
|
98
|
+
page,
|
|
99
|
+
engine: 'playwright',
|
|
100
|
+
fn: () => 'result',
|
|
101
|
+
});
|
|
102
|
+
assert.strictEqual(result, 'result');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should evaluate function on Puppeteer page', async () => {
|
|
106
|
+
const page = createMockPuppeteerPage({ evaluateResult: 'result' });
|
|
107
|
+
const result = await evaluate({
|
|
108
|
+
page,
|
|
109
|
+
engine: 'puppeteer',
|
|
110
|
+
fn: () => 'result',
|
|
111
|
+
});
|
|
112
|
+
assert.strictEqual(result, 'result');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should pass arguments to function', async () => {
|
|
116
|
+
const page = createMockPlaywrightPage();
|
|
117
|
+
// Our mock doesn't actually execute the function with args, so we'll test the interface
|
|
118
|
+
await evaluate({
|
|
119
|
+
page,
|
|
120
|
+
engine: 'playwright',
|
|
121
|
+
fn: (a, b) => a + b,
|
|
122
|
+
args: [1, 2],
|
|
123
|
+
});
|
|
124
|
+
// Should not throw
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should use provided adapter', async () => {
|
|
128
|
+
const page = createMockPlaywrightPage();
|
|
129
|
+
const customAdapter = {
|
|
130
|
+
evaluateOnPage: async (fn, args) => 'custom result',
|
|
131
|
+
};
|
|
132
|
+
const result = await evaluate({
|
|
133
|
+
page,
|
|
134
|
+
engine: 'playwright',
|
|
135
|
+
fn: () => 'test',
|
|
136
|
+
adapter: customAdapter,
|
|
137
|
+
});
|
|
138
|
+
assert.strictEqual(result, 'custom result');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('safeEvaluate', () => {
|
|
143
|
+
it('should return success result on successful evaluation', async () => {
|
|
144
|
+
const page = createMockPlaywrightPage({ evaluateResult: 'success' });
|
|
145
|
+
const result = await safeEvaluate({
|
|
146
|
+
page,
|
|
147
|
+
engine: 'playwright',
|
|
148
|
+
fn: () => 'success',
|
|
149
|
+
});
|
|
150
|
+
assert.strictEqual(result.success, true);
|
|
151
|
+
assert.strictEqual(result.value, 'success');
|
|
152
|
+
assert.strictEqual(result.navigationError, false);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should return default value on navigation error', async () => {
|
|
156
|
+
const page = createMockPlaywrightPage();
|
|
157
|
+
page.evaluate = async () => {
|
|
158
|
+
throw new Error('Execution context was destroyed');
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const result = await safeEvaluate({
|
|
162
|
+
page,
|
|
163
|
+
engine: 'playwright',
|
|
164
|
+
fn: () => 'test',
|
|
165
|
+
defaultValue: 'default',
|
|
166
|
+
silent: true,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
assert.strictEqual(result.success, false);
|
|
170
|
+
assert.strictEqual(result.value, 'default');
|
|
171
|
+
assert.strictEqual(result.navigationError, true);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should use null as default value when not specified', async () => {
|
|
175
|
+
const page = createMockPlaywrightPage();
|
|
176
|
+
page.evaluate = async () => {
|
|
177
|
+
throw new Error('Execution context was destroyed');
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const result = await safeEvaluate({
|
|
181
|
+
page,
|
|
182
|
+
engine: 'playwright',
|
|
183
|
+
fn: () => 'test',
|
|
184
|
+
silent: true,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
assert.strictEqual(result.value, null);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should rethrow non-navigation errors', async () => {
|
|
191
|
+
const page = createMockPlaywrightPage();
|
|
192
|
+
page.evaluate = async () => {
|
|
193
|
+
throw new Error('Regular error');
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
await assert.rejects(
|
|
197
|
+
() =>
|
|
198
|
+
safeEvaluate({
|
|
199
|
+
page,
|
|
200
|
+
engine: 'playwright',
|
|
201
|
+
fn: () => 'test',
|
|
202
|
+
}),
|
|
203
|
+
/Regular error/
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
});
|