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,170 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import {
|
|
4
|
+
EngineAdapter,
|
|
5
|
+
PlaywrightAdapter,
|
|
6
|
+
PuppeteerAdapter,
|
|
7
|
+
createEngineAdapter,
|
|
8
|
+
} from '../../../src/core/engine-adapter.js';
|
|
9
|
+
import {
|
|
10
|
+
createMockPlaywrightPage,
|
|
11
|
+
createMockPuppeteerPage,
|
|
12
|
+
} from '../../helpers/mocks.js';
|
|
13
|
+
|
|
14
|
+
describe('engine-adapter', () => {
|
|
15
|
+
describe('EngineAdapter (base class)', () => {
|
|
16
|
+
it('should throw on abstract methods', () => {
|
|
17
|
+
const adapter = new EngineAdapter({});
|
|
18
|
+
|
|
19
|
+
assert.throws(() => adapter.getEngineName(), /must be implemented/);
|
|
20
|
+
assert.throws(() => adapter.createLocator('sel'), /must be implemented/);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('PlaywrightAdapter', () => {
|
|
25
|
+
let page;
|
|
26
|
+
let adapter;
|
|
27
|
+
|
|
28
|
+
it('should create adapter', () => {
|
|
29
|
+
page = createMockPlaywrightPage();
|
|
30
|
+
adapter = new PlaywrightAdapter(page);
|
|
31
|
+
assert.ok(adapter);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should return playwright as engine name', () => {
|
|
35
|
+
page = createMockPlaywrightPage();
|
|
36
|
+
adapter = new PlaywrightAdapter(page);
|
|
37
|
+
assert.strictEqual(adapter.getEngineName(), 'playwright');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should create locator from selector', () => {
|
|
41
|
+
page = createMockPlaywrightPage();
|
|
42
|
+
adapter = new PlaywrightAdapter(page);
|
|
43
|
+
const locator = adapter.createLocator('button');
|
|
44
|
+
assert.ok(locator);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should handle :nth-of-type selector', () => {
|
|
48
|
+
page = createMockPlaywrightPage();
|
|
49
|
+
adapter = new PlaywrightAdapter(page);
|
|
50
|
+
const locator = adapter.createLocator('button:nth-of-type(2)');
|
|
51
|
+
assert.ok(locator);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should query single element', async () => {
|
|
55
|
+
page = createMockPlaywrightPage({ elements: { button: { count: 1 } } });
|
|
56
|
+
adapter = new PlaywrightAdapter(page);
|
|
57
|
+
const element = await adapter.querySelector('button');
|
|
58
|
+
assert.ok(element);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should return null when element not found', async () => {
|
|
62
|
+
page = createMockPlaywrightPage({ elements: { button: { count: 0 } } });
|
|
63
|
+
adapter = new PlaywrightAdapter(page);
|
|
64
|
+
const element = await adapter.querySelector('button');
|
|
65
|
+
assert.strictEqual(element, null);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should query all elements', async () => {
|
|
69
|
+
page = createMockPlaywrightPage({ elements: { button: { count: 3 } } });
|
|
70
|
+
adapter = new PlaywrightAdapter(page);
|
|
71
|
+
const elements = await adapter.querySelectorAll('button');
|
|
72
|
+
assert.strictEqual(elements.length, 3);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should count elements', async () => {
|
|
76
|
+
page = createMockPlaywrightPage({ elements: { button: { count: 5 } } });
|
|
77
|
+
adapter = new PlaywrightAdapter(page);
|
|
78
|
+
const count = await adapter.count('button');
|
|
79
|
+
assert.strictEqual(count, 5);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('PuppeteerAdapter', () => {
|
|
84
|
+
let page;
|
|
85
|
+
let adapter;
|
|
86
|
+
|
|
87
|
+
it('should create adapter', () => {
|
|
88
|
+
page = createMockPuppeteerPage();
|
|
89
|
+
adapter = new PuppeteerAdapter(page);
|
|
90
|
+
assert.ok(adapter);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should return puppeteer as engine name', () => {
|
|
94
|
+
page = createMockPuppeteerPage();
|
|
95
|
+
adapter = new PuppeteerAdapter(page);
|
|
96
|
+
assert.strictEqual(adapter.getEngineName(), 'puppeteer');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should return selector string as locator', () => {
|
|
100
|
+
page = createMockPuppeteerPage();
|
|
101
|
+
adapter = new PuppeteerAdapter(page);
|
|
102
|
+
const locator = adapter.createLocator('button');
|
|
103
|
+
assert.strictEqual(locator, 'button');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should query single element', async () => {
|
|
107
|
+
page = createMockPuppeteerPage({ elements: { button: { count: 1 } } });
|
|
108
|
+
adapter = new PuppeteerAdapter(page);
|
|
109
|
+
const element = await adapter.querySelector('button');
|
|
110
|
+
assert.ok(element);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should return null when element not found', async () => {
|
|
114
|
+
page = createMockPuppeteerPage({ elements: { button: { count: 0 } } });
|
|
115
|
+
adapter = new PuppeteerAdapter(page);
|
|
116
|
+
const element = await adapter.querySelector('button');
|
|
117
|
+
assert.strictEqual(element, null);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should query all elements', async () => {
|
|
121
|
+
page = createMockPuppeteerPage({ elements: { button: { count: 3 } } });
|
|
122
|
+
adapter = new PuppeteerAdapter(page);
|
|
123
|
+
const elements = await adapter.querySelectorAll('button');
|
|
124
|
+
assert.strictEqual(elements.length, 3);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should count elements', async () => {
|
|
128
|
+
page = createMockPuppeteerPage({ elements: { button: { count: 5 } } });
|
|
129
|
+
adapter = new PuppeteerAdapter(page);
|
|
130
|
+
const count = await adapter.count('button');
|
|
131
|
+
assert.strictEqual(count, 5);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('createEngineAdapter', () => {
|
|
136
|
+
it('should create PlaywrightAdapter for playwright engine', () => {
|
|
137
|
+
const page = createMockPlaywrightPage();
|
|
138
|
+
const adapter = createEngineAdapter(page, 'playwright');
|
|
139
|
+
assert.ok(adapter instanceof PlaywrightAdapter);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should create PuppeteerAdapter for puppeteer engine', () => {
|
|
143
|
+
const page = createMockPuppeteerPage();
|
|
144
|
+
const adapter = createEngineAdapter(page, 'puppeteer');
|
|
145
|
+
assert.ok(adapter instanceof PuppeteerAdapter);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should throw for unknown engine', () => {
|
|
149
|
+
const page = createMockPlaywrightPage();
|
|
150
|
+
assert.throws(
|
|
151
|
+
() => createEngineAdapter(page, 'unknown'),
|
|
152
|
+
/Unsupported engine/
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should throw when page is not provided', () => {
|
|
157
|
+
assert.throws(
|
|
158
|
+
() => createEngineAdapter(null, 'playwright'),
|
|
159
|
+
/page is required/
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should throw when page is undefined', () => {
|
|
164
|
+
assert.throws(
|
|
165
|
+
() => createEngineAdapter(undefined, 'playwright'),
|
|
166
|
+
/page is required/
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { detectEngine } from '../../../src/core/engine-detection.js';
|
|
4
|
+
import {
|
|
5
|
+
createMockPlaywrightPage,
|
|
6
|
+
createMockPuppeteerPage,
|
|
7
|
+
} from '../../helpers/mocks.js';
|
|
8
|
+
|
|
9
|
+
describe('engine-detection', () => {
|
|
10
|
+
describe('detectEngine', () => {
|
|
11
|
+
it('should detect Playwright page', () => {
|
|
12
|
+
const mockPage = createMockPlaywrightPage();
|
|
13
|
+
// Add Playwright-specific methods
|
|
14
|
+
mockPage.locator = (selector) => ({});
|
|
15
|
+
mockPage.context = () => ({});
|
|
16
|
+
|
|
17
|
+
const engine = detectEngine(mockPage);
|
|
18
|
+
assert.strictEqual(engine, 'playwright');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should detect Puppeteer page', () => {
|
|
22
|
+
const mockPage = {
|
|
23
|
+
$eval: async () => {},
|
|
24
|
+
$$eval: async () => {},
|
|
25
|
+
$: async () => {},
|
|
26
|
+
$$: async () => {},
|
|
27
|
+
// Puppeteer does NOT have locator or context methods
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const engine = detectEngine(mockPage);
|
|
31
|
+
assert.strictEqual(engine, 'puppeteer');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should throw error for unknown engine', () => {
|
|
35
|
+
const mockPage = {
|
|
36
|
+
// No recognizable methods
|
|
37
|
+
someMethod: () => {},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
assert.throws(() => {
|
|
41
|
+
detectEngine(mockPage);
|
|
42
|
+
}, /Unknown browser automation engine/);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should detect Playwright when locator is function and context exists', () => {
|
|
46
|
+
const mockPage = {
|
|
47
|
+
$eval: async () => {},
|
|
48
|
+
$$eval: async () => {},
|
|
49
|
+
locator: (selector) => ({}),
|
|
50
|
+
context: () => ({}),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const engine = detectEngine(mockPage);
|
|
54
|
+
assert.strictEqual(engine, 'playwright');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should detect Puppeteer when $eval exists but no context', () => {
|
|
58
|
+
const mockPage = {
|
|
59
|
+
$eval: async () => {},
|
|
60
|
+
$$eval: async () => {},
|
|
61
|
+
// No locator as function, no context
|
|
62
|
+
locator: 'not-a-function',
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const engine = detectEngine(mockPage);
|
|
66
|
+
assert.strictEqual(engine, 'puppeteer');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should handle page with context as object', () => {
|
|
70
|
+
const mockPage = {
|
|
71
|
+
$eval: async () => {},
|
|
72
|
+
$$eval: async () => {},
|
|
73
|
+
locator: (selector) => ({}),
|
|
74
|
+
context: {}, // context as object, not function
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const engine = detectEngine(mockPage);
|
|
78
|
+
assert.strictEqual(engine, 'playwright');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { isVerboseEnabled, createLogger } from '../../../src/core/logger.js';
|
|
4
|
+
|
|
5
|
+
describe('logger', () => {
|
|
6
|
+
let originalEnv;
|
|
7
|
+
let originalArgv;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
originalEnv = process.env.VERBOSE;
|
|
11
|
+
originalArgv = [...process.argv];
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
if (originalEnv !== undefined) {
|
|
16
|
+
process.env.VERBOSE = originalEnv;
|
|
17
|
+
} else {
|
|
18
|
+
delete process.env.VERBOSE;
|
|
19
|
+
}
|
|
20
|
+
process.argv = originalArgv;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('isVerboseEnabled', () => {
|
|
24
|
+
it('should return false when VERBOSE env is not set', () => {
|
|
25
|
+
delete process.env.VERBOSE;
|
|
26
|
+
// Filter out --verbose from argv if present
|
|
27
|
+
process.argv = process.argv.filter((arg) => arg !== '--verbose');
|
|
28
|
+
assert.strictEqual(isVerboseEnabled(), false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should return true when VERBOSE env is set', () => {
|
|
32
|
+
process.env.VERBOSE = 'true';
|
|
33
|
+
assert.strictEqual(isVerboseEnabled(), true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should return true when VERBOSE env is set to any value', () => {
|
|
37
|
+
process.env.VERBOSE = '1';
|
|
38
|
+
assert.strictEqual(isVerboseEnabled(), true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should return true when --verbose flag is in argv', () => {
|
|
42
|
+
delete process.env.VERBOSE;
|
|
43
|
+
process.argv = ['node', 'script.js', '--verbose'];
|
|
44
|
+
assert.strictEqual(isVerboseEnabled(), true);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('createLogger', () => {
|
|
49
|
+
it('should create a logger instance', () => {
|
|
50
|
+
const log = createLogger();
|
|
51
|
+
assert.ok(log);
|
|
52
|
+
assert.ok(typeof log === 'function' || typeof log === 'object');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should create logger with verbose disabled by default', () => {
|
|
56
|
+
const log = createLogger();
|
|
57
|
+
assert.ok(log);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should create logger with verbose enabled', () => {
|
|
61
|
+
const log = createLogger({ verbose: true });
|
|
62
|
+
assert.ok(log);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should create logger with verbose disabled', () => {
|
|
66
|
+
const log = createLogger({ verbose: false });
|
|
67
|
+
assert.ok(log);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should handle empty options', () => {
|
|
71
|
+
const log = createLogger({});
|
|
72
|
+
assert.ok(log);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should have debug method', () => {
|
|
76
|
+
const log = createLogger({ verbose: true });
|
|
77
|
+
assert.ok(typeof log.debug === 'function');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import {
|
|
4
|
+
isNavigationError,
|
|
5
|
+
safeOperation,
|
|
6
|
+
makeNavigationSafe,
|
|
7
|
+
withNavigationSafety,
|
|
8
|
+
} from '../../../src/core/navigation-safety.js';
|
|
9
|
+
|
|
10
|
+
describe('navigation-safety', () => {
|
|
11
|
+
describe('isNavigationError', () => {
|
|
12
|
+
it('should return false for null', () => {
|
|
13
|
+
assert.strictEqual(isNavigationError(null), false);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should return false for undefined', () => {
|
|
17
|
+
assert.strictEqual(isNavigationError(undefined), false);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should return false for error without message', () => {
|
|
21
|
+
assert.strictEqual(isNavigationError({}), false);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should return false for regular error', () => {
|
|
25
|
+
const error = new Error('Some regular error');
|
|
26
|
+
assert.strictEqual(isNavigationError(error), false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should return true for "Execution context was destroyed" error', () => {
|
|
30
|
+
const error = new Error('Execution context was destroyed');
|
|
31
|
+
assert.strictEqual(isNavigationError(error), true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should return true for "detached Frame" error', () => {
|
|
35
|
+
const error = new Error('Element is a detached Frame');
|
|
36
|
+
assert.strictEqual(isNavigationError(error), true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should return true for "Target closed" error', () => {
|
|
40
|
+
const error = new Error('Target closed');
|
|
41
|
+
assert.strictEqual(isNavigationError(error), true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should return true for "Session closed" error', () => {
|
|
45
|
+
const error = new Error('Session closed');
|
|
46
|
+
assert.strictEqual(isNavigationError(error), true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should return true for "Protocol error" error', () => {
|
|
50
|
+
const error = new Error('Protocol error');
|
|
51
|
+
assert.strictEqual(isNavigationError(error), true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should return true for "frame was detached" error', () => {
|
|
55
|
+
const error = new Error('frame was detached');
|
|
56
|
+
assert.strictEqual(isNavigationError(error), true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should return true for "Page crashed" error', () => {
|
|
60
|
+
const error = new Error('Page crashed');
|
|
61
|
+
assert.strictEqual(isNavigationError(error), true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should return true for "context was destroyed" error', () => {
|
|
65
|
+
const error = new Error('context was destroyed');
|
|
66
|
+
assert.strictEqual(isNavigationError(error), true);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('safeOperation', () => {
|
|
71
|
+
it('should return success result on successful operation', async () => {
|
|
72
|
+
const result = await safeOperation(async () => 'success');
|
|
73
|
+
assert.strictEqual(result.success, true);
|
|
74
|
+
assert.strictEqual(result.value, 'success');
|
|
75
|
+
assert.strictEqual(result.navigationError, false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should return default value on navigation error', async () => {
|
|
79
|
+
const result = await safeOperation(
|
|
80
|
+
async () => {
|
|
81
|
+
throw new Error('Execution context was destroyed');
|
|
82
|
+
},
|
|
83
|
+
{ defaultValue: 'default', silent: true }
|
|
84
|
+
);
|
|
85
|
+
assert.strictEqual(result.success, false);
|
|
86
|
+
assert.strictEqual(result.value, 'default');
|
|
87
|
+
assert.strictEqual(result.navigationError, true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should rethrow non-navigation errors', async () => {
|
|
91
|
+
await assert.rejects(
|
|
92
|
+
async () =>
|
|
93
|
+
safeOperation(async () => {
|
|
94
|
+
throw new Error('Regular error');
|
|
95
|
+
}),
|
|
96
|
+
/Regular error/
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should use null as default value when not specified', async () => {
|
|
101
|
+
const result = await safeOperation(
|
|
102
|
+
async () => {
|
|
103
|
+
throw new Error('Execution context was destroyed');
|
|
104
|
+
},
|
|
105
|
+
{ silent: true }
|
|
106
|
+
);
|
|
107
|
+
assert.strictEqual(result.value, null);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should handle operation name for logging', async () => {
|
|
111
|
+
const result = await safeOperation(
|
|
112
|
+
async () => {
|
|
113
|
+
throw new Error('Execution context was destroyed');
|
|
114
|
+
},
|
|
115
|
+
{ operationName: 'test operation', silent: true }
|
|
116
|
+
);
|
|
117
|
+
assert.strictEqual(result.navigationError, true);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('makeNavigationSafe', () => {
|
|
122
|
+
it('should wrap async function', async () => {
|
|
123
|
+
const fn = async () => 'result';
|
|
124
|
+
const safeFn = makeNavigationSafe(fn);
|
|
125
|
+
const result = await safeFn();
|
|
126
|
+
assert.strictEqual(result, 'result');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should return default value on navigation error', async () => {
|
|
130
|
+
const fn = async () => {
|
|
131
|
+
throw new Error('Execution context was destroyed');
|
|
132
|
+
};
|
|
133
|
+
const safeFn = makeNavigationSafe(fn, 'default');
|
|
134
|
+
const result = await safeFn();
|
|
135
|
+
assert.strictEqual(result, 'default');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should pass arguments to wrapped function', async () => {
|
|
139
|
+
const fn = async (a, b) => a + b;
|
|
140
|
+
const safeFn = makeNavigationSafe(fn);
|
|
141
|
+
const result = await safeFn(1, 2);
|
|
142
|
+
assert.strictEqual(result, 3);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('withNavigationSafety', () => {
|
|
147
|
+
it('should return function result on success', async () => {
|
|
148
|
+
const fn = async () => 'success';
|
|
149
|
+
const safeFn = withNavigationSafety(fn);
|
|
150
|
+
const result = await safeFn();
|
|
151
|
+
assert.strictEqual(result, 'success');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should call onNavigationError callback on navigation error', async () => {
|
|
155
|
+
let called = false;
|
|
156
|
+
const fn = async () => {
|
|
157
|
+
throw new Error('Execution context was destroyed');
|
|
158
|
+
};
|
|
159
|
+
const safeFn = withNavigationSafety(fn, {
|
|
160
|
+
onNavigationError: () => {
|
|
161
|
+
called = true;
|
|
162
|
+
return 'handled';
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
const result = await safeFn();
|
|
166
|
+
assert.strictEqual(called, true);
|
|
167
|
+
assert.strictEqual(result, 'handled');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should return undefined when rethrow is false and no callback', async () => {
|
|
171
|
+
const fn = async () => {
|
|
172
|
+
throw new Error('Execution context was destroyed');
|
|
173
|
+
};
|
|
174
|
+
const safeFn = withNavigationSafety(fn, { rethrow: false });
|
|
175
|
+
const result = await safeFn();
|
|
176
|
+
assert.strictEqual(result, undefined);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should rethrow navigation error when rethrow is true and no callback', async () => {
|
|
180
|
+
const fn = async () => {
|
|
181
|
+
throw new Error('Execution context was destroyed');
|
|
182
|
+
};
|
|
183
|
+
const safeFn = withNavigationSafety(fn, { rethrow: true });
|
|
184
|
+
await assert.rejects(safeFn, /Execution context was destroyed/);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should rethrow non-navigation errors', async () => {
|
|
188
|
+
const fn = async () => {
|
|
189
|
+
throw new Error('Regular error');
|
|
190
|
+
};
|
|
191
|
+
const safeFn = withNavigationSafety(fn);
|
|
192
|
+
await assert.rejects(safeFn, /Regular error/);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should pass arguments through', async () => {
|
|
196
|
+
const fn = async (x) => x * 2;
|
|
197
|
+
const safeFn = withNavigationSafety(fn);
|
|
198
|
+
const result = await safeFn(5);
|
|
199
|
+
assert.strictEqual(result, 10);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
});
|