browser-commander 0.26.3 → 0.27.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.
Files changed (54) hide show
  1. package/.jscpd-baseline.json +24 -23
  2. package/CHANGELOG.md +6 -0
  3. package/README.md +2 -0
  4. package/package.json +5 -3
  5. package/src/bindings.js +19 -0
  6. package/src/browser/attach/snapshot-launch.js +8 -6
  7. package/src/browser/attach/snapshot.js +39 -2
  8. package/src/browser/browser-cookie-database.js +71 -2
  9. package/src/browser/browser-cookie-session.js +183 -0
  10. package/src/browser/browser-cookies.js +29 -0
  11. package/src/browser/debugging-port.js +4 -2
  12. package/src/browser/default-browser.js +33 -9
  13. package/src/browser/launch-diagnostics.js +102 -0
  14. package/src/browser/launcher.js +28 -8
  15. package/src/browser/migration/sqlite-snapshot.js +5 -27
  16. package/src/browser/navigation.js +173 -98
  17. package/src/browser/real-browser.js +135 -68
  18. package/src/browser/session-cookies.js +78 -0
  19. package/src/browser/session-persistence.js +72 -0
  20. package/src/browser/storage-state.js +32 -6
  21. package/src/core/dialog-manager.js +7 -0
  22. package/src/core/managed-navigation.js +101 -0
  23. package/src/core/navigation-manager.js +62 -78
  24. package/src/core/navigation-operation.js +25 -0
  25. package/src/core/navigation-readiness.js +40 -16
  26. package/src/core/network-tracker.js +39 -92
  27. package/src/core/page-session.js +3 -2
  28. package/src/core/page-trigger-manager.js +15 -12
  29. package/src/core/readiness.js +67 -13
  30. package/src/core/subscriptions.js +20 -0
  31. package/src/core/webdriver-page.js +38 -0
  32. package/src/elements/locators.js +35 -5
  33. package/src/elements/reusable.js +92 -0
  34. package/src/elements/visibility.js +6 -11
  35. package/src/exports.js +14 -0
  36. package/src/factory.js +4 -4
  37. package/src/high-level/universal-logic.js +57 -21
  38. package/src/interactions/click.js +3 -0
  39. package/src/interactions/scroll.js +8 -2
  40. package/src/utilities/subprocess.js +15 -4
  41. package/tests/e2e/navigation-budget.e2e.test.js +132 -0
  42. package/tests/e2e/reusable-session.e2e.test.js +231 -0
  43. package/tests/unit/browser/attach/snapshot-launch.test.js +2 -2
  44. package/tests/unit/browser/attach/snapshot.test.js +33 -0
  45. package/tests/unit/browser/default-browser.test.js +18 -0
  46. package/tests/unit/browser/launch-diagnostics.test.js +52 -0
  47. package/tests/unit/browser/real-browser.test.js +1 -1
  48. package/tests/unit/browser/session-workflows.test.js +206 -0
  49. package/tests/unit/browser/storage-state.test.js +7 -2
  50. package/tests/unit/core/navigation-budget.test.js +120 -0
  51. package/tests/unit/core/navigation-manager.test.js +23 -1
  52. package/tests/unit/core/webdriver-page.test.js +33 -0
  53. package/tests/unit/elements/reusable-helpers.test.js +83 -0
  54. package/tests/unit/traces/links.test.js +2 -2
@@ -0,0 +1,120 @@
1
+ // feature-parity: navigation.budget@native-typed
2
+ import { it } from 'node:test';
3
+ import assert from 'node:assert/strict';
4
+ import { createNavigationManager } from '../../../src/core/navigation-manager.js';
5
+ import { goto } from '../../../src/browser/navigation.js';
6
+ import { createNetworkTracker } from '../../../src/core/network-tracker.js';
7
+ import {
8
+ createMockPlaywrightPage,
9
+ createMockLogger,
10
+ } from '../../helpers/mocks.js';
11
+
12
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
13
+ const noWait = {
14
+ waitForStableUrlBefore: false,
15
+ waitForStableUrlAfter: false,
16
+ waitForNetworkIdle: false,
17
+ verify: false,
18
+ };
19
+
20
+ it('managed goto honors every no-wait option and keeps tracking', async () => {
21
+ const page = createMockPlaywrightPage();
22
+ let idleCalls = 0;
23
+ const manager = createNavigationManager({
24
+ page,
25
+ engine: 'playwright',
26
+ log: createMockLogger(),
27
+ networkTracker: {
28
+ reset() {},
29
+ waitForNetworkIdle: async () => {
30
+ idleCalls++;
31
+ await delay(80);
32
+ return true;
33
+ },
34
+ },
35
+ });
36
+ const started = performance.now();
37
+ const result = await goto({
38
+ page,
39
+ navigationManager: manager,
40
+ url: 'data:text/html,<h1>fixture</h1>',
41
+ timeout: 50,
42
+ ...noWait,
43
+ });
44
+ assert.equal(result.navigated, true);
45
+ assert.equal(idleCalls, 0);
46
+ assert.ok(performance.now() - started < 70);
47
+ assert.equal(manager.getSessionId(), 1);
48
+ });
49
+
50
+ it('engine navigation and stalled readiness share a single budget', async () => {
51
+ const page = createMockPlaywrightPage();
52
+ page.goto = async () => delay(30);
53
+ const manager = createNavigationManager({
54
+ page,
55
+ engine: 'playwright',
56
+ log: createMockLogger(),
57
+ });
58
+ const started = performance.now();
59
+ const result = await goto({
60
+ page,
61
+ navigationManager: manager,
62
+ url: 'data:text/html,fixture',
63
+ timeout: 60,
64
+ ...noWait,
65
+ checks: [{ name: 'stall', run: async () => new Promise(() => {}) }],
66
+ });
67
+ assert.equal(result.status, 'timed_out');
68
+ assert.ok(performance.now() - started < 100);
69
+ assert.equal(manager.isNavigating(), false);
70
+ });
71
+
72
+ it('caller abort promptly interrupts and unregisters its listener', async () => {
73
+ const page = createMockPlaywrightPage();
74
+ page.goto = async () => delay(180);
75
+ const manager = createNavigationManager({
76
+ page,
77
+ engine: 'playwright',
78
+ log: createMockLogger(),
79
+ });
80
+ const controller = new AbortController();
81
+ let listeners = 0;
82
+ const add = controller.signal.addEventListener.bind(controller.signal);
83
+ const remove = controller.signal.removeEventListener.bind(controller.signal);
84
+ controller.signal.addEventListener = (...args) => {
85
+ listeners++;
86
+ return add(...args);
87
+ };
88
+ controller.signal.removeEventListener = (...args) => {
89
+ listeners--;
90
+ return remove(...args);
91
+ };
92
+ const pending = goto({
93
+ page,
94
+ navigationManager: manager,
95
+ url: 'data:text/html,fixture',
96
+ timeout: 1000,
97
+ signal: controller.signal,
98
+ ...noWait,
99
+ });
100
+ setTimeout(() => controller.abort(), 20);
101
+ const result = await pending;
102
+ assert.equal(result.status, 'interrupted');
103
+ assert.equal(listeners, 0);
104
+ assert.equal(manager.isNavigating(), false);
105
+ });
106
+
107
+ it('an idle network wait respects a shorter timeout than its quiet period', async () => {
108
+ const page = createMockPlaywrightPage();
109
+ const tracker = createNetworkTracker({
110
+ page,
111
+ engine: 'playwright',
112
+ log: createMockLogger(),
113
+ });
114
+ const started = performance.now();
115
+ assert.equal(
116
+ await tracker.waitForNetworkIdle({ timeout: 30, idleTime: 100 }),
117
+ false
118
+ );
119
+ assert.ok(performance.now() - started < 80);
120
+ });
@@ -59,18 +59,40 @@ describe('navigation manager', () => {
59
59
  assert.strictEqual(loaded, true);
60
60
  assert.strictEqual(manager.getSessionId(), 1);
61
61
  assert.strictEqual(manager.isNavigating(), false);
62
+ const contentTimeout = calls.find(([name]) => name === 'setContent')[2]
63
+ .timeout;
64
+ assert.ok(contentTimeout > 0 && contentTimeout <= 5000);
62
65
  assert.deepStrictEqual(calls, [
63
66
  ['cleanup'],
64
67
  ['navigationStart'],
65
68
  [
66
69
  'setContent',
67
70
  '<h1>Hi</h1>',
68
- { waitUntil: 'networkidle', timeout: 5000 },
71
+ { waitUntil: 'networkidle', timeout: contentTimeout },
69
72
  ],
70
73
  ['pageReady'],
71
74
  ]);
72
75
  });
73
76
 
77
+ it('bounds stalled content replacement and clears navigation state', async () => {
78
+ const page = createMockPlaywrightPage();
79
+ page.setContent = async () =>
80
+ new Promise((resolve) => setTimeout(resolve, 150));
81
+ const manager = createNavigationManager({
82
+ page,
83
+ engine: 'playwright',
84
+ log: createMockLogger(),
85
+ });
86
+ const start = performance.now();
87
+ assert.equal(
88
+ await manager.setContent({ html: '<h1>Bounded</h1>', timeout: 40 }),
89
+ false
90
+ );
91
+ assert.ok(performance.now() - start < 120);
92
+ assert.equal(manager.isNavigating(), false);
93
+ await new Promise((resolve) => setTimeout(resolve, 160));
94
+ });
95
+
74
96
  it('should require html while accepting an empty string', async () => {
75
97
  const page = createMockPlaywrightPage();
76
98
  const log = createMockLogger();
@@ -256,6 +256,39 @@ describe('WebDriverPage', () => {
256
256
  );
257
257
  });
258
258
 
259
+ it('supplies BiDi cookie domains from explicit scope, URL or current page', async () => {
260
+ const { page, bidi } = bidiPage({
261
+ driverOptions: { url: 'https://current.example.test/path' },
262
+ });
263
+ await page.syncUrl();
264
+ await page.setCookie(
265
+ { name: 'current', value: 'artificial' },
266
+ {
267
+ name: 'url',
268
+ value: 'artificial',
269
+ url: 'https://url.example.test/path',
270
+ },
271
+ { name: 'domain', value: 'artificial', domain: '.explicit.example.test' }
272
+ );
273
+ const cookies = bidi.sent
274
+ .filter(({ method }) => method === 'storage.setCookie')
275
+ .map(({ params }) => params.cookie);
276
+ assert.deepEqual(
277
+ cookies.map(({ domain }) => domain),
278
+ ['current.example.test', 'url.example.test', '.explicit.example.test']
279
+ );
280
+ assert.ok(cookies.every((cookie) => !('url' in cookie)));
281
+ assert.ok(cookies.every((cookie) => !('expiry' in cookie)));
282
+ assert.equal(cookies[1].secure, true);
283
+ await page.setCookie({
284
+ name: 'http',
285
+ value: 'artificial',
286
+ url: 'http://url.example.test',
287
+ secure: false,
288
+ });
289
+ assert.equal(bidi.sent.at(-1).params.cookie.secure, false);
290
+ });
291
+
259
292
  it('adds and removes preload scripts over BiDi', async () => {
260
293
  const { page, bidi } = bidiPage({
261
294
  responses: { 'script.addPreloadScript': { script: 'preload-1' } },
@@ -0,0 +1,83 @@
1
+ import { it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import * as api from '../../../src/index.js';
4
+ import {
5
+ createMockPlaywrightPage,
6
+ createMockLogger,
7
+ } from '../../helpers/mocks.js';
8
+
9
+ it('finds ordered fallback selectors and supports alternate toggle texts', async () => {
10
+ const commander = api.makeBrowserCommander({
11
+ page: createMockPlaywrightPage({
12
+ elements: {
13
+ '#absent': { count: 0 },
14
+ '#present': { count: 1, visible: true },
15
+ },
16
+ }),
17
+ engine: 'playwright',
18
+ log: createMockLogger(),
19
+ enableNavigationManager: false,
20
+ });
21
+ assert.equal(
22
+ await commander.findFirst({
23
+ selectors: ['#absent', '#present'],
24
+ visible: true,
25
+ }),
26
+ '#present'
27
+ );
28
+ const found = await api.findToggleButton({
29
+ texts: ['one', 'two'],
30
+ elementTypes: ['button'],
31
+ count: async ({ selector }) => (selector === 'two' ? 1 : 0),
32
+ findByText: async ({ text }) => text,
33
+ });
34
+ assert.equal(found, 'two');
35
+ commander.destroy();
36
+ });
37
+
38
+ it('navigation subscriptions return idempotent unregister functions', () => {
39
+ const commander = api.makeBrowserCommander({
40
+ page: createMockPlaywrightPage(),
41
+ engine: 'playwright',
42
+ });
43
+ for (const event of [
44
+ 'onUrlChange',
45
+ 'onNavigationStart',
46
+ 'onNavigationComplete',
47
+ 'onPageReady',
48
+ ]) {
49
+ const unregister = commander[event](() => {});
50
+ assert.equal(typeof unregister, 'function');
51
+ unregister();
52
+ unregister();
53
+ }
54
+ commander.destroy();
55
+ });
56
+
57
+ it('exposes reusable text, checkbox, flag and cookie helpers', () => {
58
+ for (const name of [
59
+ 'hasText',
60
+ 'isChecked',
61
+ 'check',
62
+ 'readFlag',
63
+ 'uninstallClickListener',
64
+ 'setCookies',
65
+ 'clearCookies',
66
+ ]) {
67
+ assert.equal(typeof api[name], 'function', name);
68
+ }
69
+ });
70
+
71
+ it('unsubscribe retains a second registration of the same callback', async () => {
72
+ const { subscribeCallbacks } =
73
+ await import('../../../src/core/subscriptions.js');
74
+ const callbacks = [],
75
+ callback = () => {};
76
+ const first = subscribeCallbacks(callbacks, callback);
77
+ const second = subscribeCallbacks(callbacks, callback);
78
+ first();
79
+ first();
80
+ assert.deepEqual(callbacks, [callback]);
81
+ second();
82
+ assert.deepEqual(callbacks, []);
83
+ });
@@ -544,8 +544,8 @@ describe('trace links export (issue #94)', () => {
544
544
  // The notation's formatter and parser decide whether an export can be read
545
545
  // back, so the range this package depends on and the version the tests
546
546
  // proved are the same range.
547
- assert.strictEqual(manifest.dependencies['links-notation'], '^0.22.0');
548
- assert.match(installed.version, /^0\.22\./);
547
+ assert.strictEqual(manifest.dependencies['links-notation'], '^0.23.0');
548
+ assert.match(installed.version, /^0\.23\./);
549
549
  });
550
550
 
551
551
  it('should export a bundle named by path as readily as an open one', async () => {