appium-session-recorder 0.0.2 → 0.0.3
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/dist/index.js +32422 -0
- package/dist/ui/assets/index-CUcJNRfB.css +1 -0
- package/dist/ui/assets/index-Cl_X3tPj.js +4 -0
- package/{src → dist}/ui/index.html +2 -1
- package/package.json +10 -3
- package/bun.lock +0 -731
- package/src/cli/arg-parser.ts +0 -311
- package/src/cli/commands/drive.ts +0 -147
- package/src/cli/commands/index.ts +0 -54
- package/src/cli/commands/proxy.ts +0 -41
- package/src/cli/commands/screen.ts +0 -73
- package/src/cli/commands/selectors.ts +0 -42
- package/src/cli/commands/session.ts +0 -64
- package/src/cli/commands/types.ts +0 -11
- package/src/cli/index.ts +0 -158
- package/src/cli/prompts.ts +0 -64
- package/src/cli/response.ts +0 -44
- package/src/core/appium/client.ts +0 -248
- package/src/core/index.ts +0 -5
- package/src/core/selectors/generate-candidates.ts +0 -155
- package/src/core/selectors/score-candidates.ts +0 -184
- package/src/core/types.ts +0 -79
- package/src/core/xml/parse-source.ts +0 -197
- package/src/index.ts +0 -7
- package/src/server/appium-client.ts +0 -24
- package/src/server/index.ts +0 -6
- package/src/server/interaction-recorder.ts +0 -74
- package/src/server/proxy-middleware.ts +0 -68
- package/src/server/routes.ts +0 -64
- package/src/server/server.ts +0 -43
- package/src/server/types.ts +0 -34
- package/src/ui/bun.lock +0 -311
- package/src/ui/package.json +0 -20
- package/src/ui/src/App.css +0 -12
- package/src/ui/src/App.tsx +0 -41
- package/src/ui/src/components/ActionCarousel.css +0 -128
- package/src/ui/src/components/ActionCarousel.tsx +0 -92
- package/src/ui/src/components/Inspector.css +0 -314
- package/src/ui/src/components/Inspector.tsx +0 -265
- package/src/ui/src/components/InteractionCard.css +0 -159
- package/src/ui/src/components/InteractionCard.tsx +0 -60
- package/src/ui/src/components/MainInspector.css +0 -304
- package/src/ui/src/components/MainInspector.tsx +0 -304
- package/src/ui/src/components/Stats.css +0 -27
- package/src/ui/src/components/Timeline.css +0 -31
- package/src/ui/src/components/Timeline.tsx +0 -37
- package/src/ui/src/hooks/useInteractions.ts +0 -73
- package/src/ui/src/index.tsx +0 -11
- package/src/ui/src/services/api.ts +0 -41
- package/src/ui/src/styles/tokens.css +0 -126
- package/src/ui/src/types.ts +0 -34
- package/src/ui/src/utils/__tests__/locators.test.ts +0 -304
- package/src/ui/src/utils/__tests__/xml-parser.test.ts +0 -326
- package/src/ui/src/utils/locators.ts +0 -14
- package/src/ui/src/utils/xml-parser.ts +0 -45
- package/src/ui/tsconfig.json +0 -34
- package/src/ui/tsconfig.node.json +0 -11
- package/src/ui/vite.config.ts +0 -22
- package/tests/cli/arg-parser.test.ts +0 -397
- package/tests/cli/drive-commands.test.ts +0 -151
- package/tests/cli/selectors-best.test.ts +0 -42
- package/tests/cli/session-commands.test.ts +0 -53
- package/tests/core/selector-candidates.test.ts +0 -83
- package/tests/core/selector-scoring.test.ts +0 -75
- package/tests/core/xml-parser.test.ts +0 -56
- package/tests/server/appium-client.test.ts +0 -229
- package/tests/server/interaction-recorder.test.ts +0 -377
- package/tests/server/proxy-middleware.test.ts +0 -343
- package/tests/server/routes.test.ts +0 -305
- package/tsconfig.json +0 -26
- package/vitest.config.ts +0 -16
- package/vitest.ui.config.ts +0 -15
- package/workflow.gif +0 -0
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { runSessionCreate, runSessionDelete } from '../../src/cli/commands/session';
|
|
3
|
-
|
|
4
|
-
function jsonResponse(body: unknown, status = 200): Response {
|
|
5
|
-
return new Response(JSON.stringify(body), {
|
|
6
|
-
status,
|
|
7
|
-
headers: { 'Content-Type': 'application/json' },
|
|
8
|
-
});
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
describe('session commands', () => {
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
vi.restoreAllMocks();
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it('creates session from inline capabilities', async () => {
|
|
17
|
-
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
|
|
18
|
-
jsonResponse({
|
|
19
|
-
sessionId: 'abc123',
|
|
20
|
-
value: { capabilities: { platformName: 'iOS' } },
|
|
21
|
-
}),
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
const result = await runSessionCreate([
|
|
25
|
-
'--appium-url', 'http://127.0.0.1:4723',
|
|
26
|
-
'--caps-json', '{"platformName":"iOS","appium:automationName":"XCUITest"}',
|
|
27
|
-
]);
|
|
28
|
-
|
|
29
|
-
expect(result.command).toBe('session.create');
|
|
30
|
-
expect((result.result as any).sessionId).toBe('abc123');
|
|
31
|
-
|
|
32
|
-
const [, options] = fetchMock.mock.calls[0];
|
|
33
|
-
const payload = JSON.parse(String(options?.body));
|
|
34
|
-
expect(payload.capabilities.alwaysMatch.platformName).toBe('iOS');
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('deletes existing session', async () => {
|
|
38
|
-
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(jsonResponse({ value: null }));
|
|
39
|
-
|
|
40
|
-
const result = await runSessionDelete([
|
|
41
|
-
'--appium-url', 'http://127.0.0.1:4723',
|
|
42
|
-
'--session-id', 'abc123',
|
|
43
|
-
]);
|
|
44
|
-
|
|
45
|
-
expect(result.command).toBe('session.delete');
|
|
46
|
-
expect((result.result as any).deleted).toBe(true);
|
|
47
|
-
|
|
48
|
-
expect(fetchMock).toHaveBeenCalledWith(
|
|
49
|
-
'http://127.0.0.1:4723/session/abc123',
|
|
50
|
-
expect.objectContaining({ method: 'DELETE' }),
|
|
51
|
-
);
|
|
52
|
-
});
|
|
53
|
-
});
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { generateSelectorCandidates } from '../../src/core/selectors/generate-candidates';
|
|
3
|
-
import type { ParsedElement } from '../../src/core/types';
|
|
4
|
-
|
|
5
|
-
function createElement(overrides: Partial<ParsedElement>): ParsedElement {
|
|
6
|
-
return {
|
|
7
|
-
elementRef: 'ios:/XCUIElementTypeButton[1]',
|
|
8
|
-
index: 0,
|
|
9
|
-
platform: 'ios',
|
|
10
|
-
type: 'XCUIElementTypeButton',
|
|
11
|
-
xpath: '/XCUIElementTypeApplication[1]/XCUIElementTypeButton[1]',
|
|
12
|
-
name: '',
|
|
13
|
-
label: '',
|
|
14
|
-
value: '',
|
|
15
|
-
text: '',
|
|
16
|
-
resourceId: '',
|
|
17
|
-
contentDesc: '',
|
|
18
|
-
enabled: true,
|
|
19
|
-
visible: true,
|
|
20
|
-
accessible: true,
|
|
21
|
-
clickable: true,
|
|
22
|
-
x: 0,
|
|
23
|
-
y: 0,
|
|
24
|
-
width: 10,
|
|
25
|
-
height: 10,
|
|
26
|
-
attributes: {},
|
|
27
|
-
...overrides,
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
describe('generateSelectorCandidates', () => {
|
|
32
|
-
it('generates iOS locator families', () => {
|
|
33
|
-
const element = createElement({
|
|
34
|
-
platform: 'ios',
|
|
35
|
-
name: 'loginBtn',
|
|
36
|
-
label: 'Log In',
|
|
37
|
-
type: 'XCUIElementTypeButton',
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
const candidates = generateSelectorCandidates(element);
|
|
41
|
-
const strategies = candidates.map(candidate => candidate.strategy);
|
|
42
|
-
|
|
43
|
-
expect(strategies).toContain('accessibility id');
|
|
44
|
-
expect(strategies).toContain('-ios predicate string');
|
|
45
|
-
expect(strategies).toContain('-ios class chain');
|
|
46
|
-
expect(strategies).toContain('xpath');
|
|
47
|
-
expect(strategies).toContain('class name');
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('generates Android-specific candidates', () => {
|
|
51
|
-
const element = createElement({
|
|
52
|
-
platform: 'android',
|
|
53
|
-
type: 'android.widget.Button',
|
|
54
|
-
text: 'Continue',
|
|
55
|
-
resourceId: 'com.example:id/continue',
|
|
56
|
-
contentDesc: 'continueButton',
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
const candidates = generateSelectorCandidates(element);
|
|
60
|
-
|
|
61
|
-
expect(candidates).toContainEqual({
|
|
62
|
-
strategy: 'id',
|
|
63
|
-
value: 'com.example:id/continue',
|
|
64
|
-
platform: 'android',
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
expect(candidates.some(candidate => candidate.strategy === '-android uiautomator')).toBe(true);
|
|
68
|
-
expect(candidates.some(candidate => candidate.strategy === 'accessibility id' && candidate.value === 'continueButton')).toBe(true);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('always includes xpath and class name fallback', () => {
|
|
72
|
-
const element = createElement({
|
|
73
|
-
platform: 'unknown',
|
|
74
|
-
type: 'GenericButton',
|
|
75
|
-
xpath: '/Root[1]/GenericButton[1]',
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
const candidates = generateSelectorCandidates(element);
|
|
79
|
-
|
|
80
|
-
expect(candidates).toContainEqual({ strategy: 'xpath', value: '/Root[1]/GenericButton[1]', platform: 'generic' });
|
|
81
|
-
expect(candidates).toContainEqual({ strategy: 'class name', value: 'GenericButton', platform: 'generic' });
|
|
82
|
-
});
|
|
83
|
-
});
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { rankSelectorCandidates } from '../../src/core/selectors/score-candidates';
|
|
3
|
-
import type { ParsedElement, SelectorCandidate } from '../../src/core/types';
|
|
4
|
-
|
|
5
|
-
function createElement(overrides: Partial<ParsedElement>): ParsedElement {
|
|
6
|
-
return {
|
|
7
|
-
elementRef: 'ios:/XCUIElementTypeButton[1]',
|
|
8
|
-
index: 0,
|
|
9
|
-
platform: 'ios',
|
|
10
|
-
type: 'XCUIElementTypeButton',
|
|
11
|
-
xpath: '/XCUIElementTypeApplication[1]/XCUIElementTypeButton[1]',
|
|
12
|
-
name: 'loginBtn',
|
|
13
|
-
label: 'Log In',
|
|
14
|
-
value: '',
|
|
15
|
-
text: '',
|
|
16
|
-
resourceId: '',
|
|
17
|
-
contentDesc: '',
|
|
18
|
-
enabled: true,
|
|
19
|
-
visible: true,
|
|
20
|
-
accessible: true,
|
|
21
|
-
clickable: true,
|
|
22
|
-
x: 0,
|
|
23
|
-
y: 0,
|
|
24
|
-
width: 10,
|
|
25
|
-
height: 10,
|
|
26
|
-
attributes: {},
|
|
27
|
-
...overrides,
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
describe('rankSelectorCandidates', () => {
|
|
32
|
-
it('prioritizes unique high-confidence selectors', () => {
|
|
33
|
-
const target = createElement({ name: 'loginBtn', xpath: '/Root[1]/Button[1]' });
|
|
34
|
-
const sibling = createElement({
|
|
35
|
-
elementRef: 'ios:/XCUIElementTypeButton[2]',
|
|
36
|
-
index: 1,
|
|
37
|
-
name: 'secondaryBtn',
|
|
38
|
-
xpath: '/Root[1]/Button[2]',
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
const candidates: SelectorCandidate[] = [
|
|
42
|
-
{ strategy: 'class name', value: 'XCUIElementTypeButton', platform: 'generic' },
|
|
43
|
-
{ strategy: 'accessibility id', value: 'loginBtn', platform: 'ios' },
|
|
44
|
-
];
|
|
45
|
-
|
|
46
|
-
const ranked = rankSelectorCandidates(target, [target, sibling], candidates);
|
|
47
|
-
|
|
48
|
-
expect(ranked[0].strategy).toBe('accessibility id');
|
|
49
|
-
expect(ranked[0].reasons).toContain('UNIQUE_MATCH');
|
|
50
|
-
expect(ranked[1].reasons).toContain('MULTIPLE_MATCHES');
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('applies dynamic token penalty', () => {
|
|
54
|
-
const target = createElement({ name: 'user_123456789' });
|
|
55
|
-
const candidates: SelectorCandidate[] = [
|
|
56
|
-
{ strategy: 'accessibility id', value: 'user_123456789', platform: 'ios' },
|
|
57
|
-
];
|
|
58
|
-
|
|
59
|
-
const ranked = rankSelectorCandidates(target, [target], candidates);
|
|
60
|
-
|
|
61
|
-
expect(ranked[0].reasons).toContain('DYNAMIC_TOKEN_PENALTY');
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('marks no-match selectors', () => {
|
|
65
|
-
const target = createElement({ xpath: '/Root[1]/Button[1]' });
|
|
66
|
-
const candidates: SelectorCandidate[] = [
|
|
67
|
-
{ strategy: 'xpath', value: '/Root[1]/Button[99]', platform: 'generic' },
|
|
68
|
-
];
|
|
69
|
-
|
|
70
|
-
const ranked = rankSelectorCandidates(target, [target], candidates);
|
|
71
|
-
|
|
72
|
-
expect(ranked[0].matchCount).toBe(0);
|
|
73
|
-
expect(ranked[0].reasons).toContain('NO_MATCH');
|
|
74
|
-
});
|
|
75
|
-
});
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { parseSource } from '../../src/core/xml/parse-source';
|
|
3
|
-
|
|
4
|
-
describe('parseSource', () => {
|
|
5
|
-
it('parses iOS source with normalized metadata', () => {
|
|
6
|
-
const xml = `
|
|
7
|
-
<AppiumAUT type="XCUIElementTypeApplication" name="MyApp" enabled="true" visible="true">
|
|
8
|
-
<XCUIElementTypeWindow type="XCUIElementTypeWindow" enabled="true" visible="true">
|
|
9
|
-
<XCUIElementTypeButton type="XCUIElementTypeButton" name="loginButton" label="Log In" enabled="true" visible="true" x="20" y="400" width="350" height="44" clickable="true" />
|
|
10
|
-
</XCUIElementTypeWindow>
|
|
11
|
-
</AppiumAUT>
|
|
12
|
-
`;
|
|
13
|
-
|
|
14
|
-
const parsed = parseSource(xml);
|
|
15
|
-
|
|
16
|
-
expect(parsed.platform).toBe('ios');
|
|
17
|
-
expect(parsed.elements.length).toBe(3);
|
|
18
|
-
|
|
19
|
-
const button = parsed.elements.find(element => element.type === 'XCUIElementTypeButton');
|
|
20
|
-
expect(button).toBeDefined();
|
|
21
|
-
expect(button?.name).toBe('loginButton');
|
|
22
|
-
expect(button?.label).toBe('Log In');
|
|
23
|
-
expect(button?.x).toBe(20);
|
|
24
|
-
expect(button?.width).toBe(350);
|
|
25
|
-
expect(button?.elementRef).toMatch(/^ios:/);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('parses Android bounds and strategy attributes', () => {
|
|
29
|
-
const xml = `
|
|
30
|
-
<hierarchy>
|
|
31
|
-
<android.widget.FrameLayout class="android.widget.FrameLayout" bounds="[0,0][1080,2400]">
|
|
32
|
-
<android.widget.Button class="android.widget.Button" text="Continue" content-desc="continueButton" resource-id="com.example:id/continue" bounds="[50,1900][1030,2050]" enabled="true" visible="true" clickable="true" />
|
|
33
|
-
</android.widget.FrameLayout>
|
|
34
|
-
</hierarchy>
|
|
35
|
-
`;
|
|
36
|
-
|
|
37
|
-
const parsed = parseSource(xml);
|
|
38
|
-
|
|
39
|
-
expect(parsed.platform).toBe('android');
|
|
40
|
-
|
|
41
|
-
const button = parsed.elements.find(element => element.type === 'android.widget.Button');
|
|
42
|
-
expect(button).toBeDefined();
|
|
43
|
-
expect(button?.resourceId).toBe('com.example:id/continue');
|
|
44
|
-
expect(button?.contentDesc).toBe('continueButton');
|
|
45
|
-
expect(button?.x).toBe(50);
|
|
46
|
-
expect(button?.y).toBe(1900);
|
|
47
|
-
expect(button?.width).toBe(980);
|
|
48
|
-
expect(button?.height).toBe(150);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('returns empty result for invalid XML', () => {
|
|
52
|
-
const parsed = parseSource('not xml');
|
|
53
|
-
expect(parsed.platform).toBe('unknown');
|
|
54
|
-
expect(parsed.elements).toEqual([]);
|
|
55
|
-
});
|
|
56
|
-
});
|
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
2
|
-
import { AppiumClient } from '../../src/server/appium-client';
|
|
3
|
-
|
|
4
|
-
describe('AppiumClient', () => {
|
|
5
|
-
let client: AppiumClient;
|
|
6
|
-
const mockAppiumUrl = 'http://localhost:4723';
|
|
7
|
-
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
client = new AppiumClient(mockAppiumUrl);
|
|
10
|
-
vi.stubGlobal('fetch', vi.fn());
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
afterEach(() => {
|
|
14
|
-
vi.restoreAllMocks();
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
describe('constructor', () => {
|
|
18
|
-
it('should create client with provided appium URL', () => {
|
|
19
|
-
const customClient = new AppiumClient('http://custom:8080');
|
|
20
|
-
expect(customClient).toBeInstanceOf(AppiumClient);
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
describe('fetchFromAppium', () => {
|
|
25
|
-
it('should fetch data from correct URL', async () => {
|
|
26
|
-
const mockResponse = { value: 'screenshot_data' };
|
|
27
|
-
vi.mocked(fetch).mockResolvedValue({
|
|
28
|
-
json: vi.fn().mockResolvedValue(mockResponse),
|
|
29
|
-
} as unknown as Response);
|
|
30
|
-
|
|
31
|
-
await client.fetchFromAppium('session123', 'screenshot');
|
|
32
|
-
|
|
33
|
-
expect(fetch).toHaveBeenCalledWith(
|
|
34
|
-
'http://localhost:4723/session/session123/screenshot'
|
|
35
|
-
);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('should return value from response', async () => {
|
|
39
|
-
const mockResponse = { value: 'screenshot_data' };
|
|
40
|
-
vi.mocked(fetch).mockResolvedValue({
|
|
41
|
-
json: vi.fn().mockResolvedValue(mockResponse),
|
|
42
|
-
} as unknown as Response);
|
|
43
|
-
|
|
44
|
-
const result = await client.fetchFromAppium('session123', 'screenshot');
|
|
45
|
-
|
|
46
|
-
expect(result).toBe('screenshot_data');
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('should return null if response has no value property', async () => {
|
|
50
|
-
const mockResponse = { data: 'something_else' };
|
|
51
|
-
vi.mocked(fetch).mockResolvedValue({
|
|
52
|
-
json: vi.fn().mockResolvedValue(mockResponse),
|
|
53
|
-
} as unknown as Response);
|
|
54
|
-
|
|
55
|
-
const result = await client.fetchFromAppium('session123', 'screenshot');
|
|
56
|
-
|
|
57
|
-
expect(result).toBeNull();
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('should return null if response is null', async () => {
|
|
61
|
-
vi.mocked(fetch).mockResolvedValue({
|
|
62
|
-
json: vi.fn().mockResolvedValue(null),
|
|
63
|
-
} as unknown as Response);
|
|
64
|
-
|
|
65
|
-
const result = await client.fetchFromAppium('session123', 'screenshot');
|
|
66
|
-
|
|
67
|
-
expect(result).toBeNull();
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('should return null on fetch error', async () => {
|
|
71
|
-
vi.mocked(fetch).mockRejectedValue(new Error('Network error'));
|
|
72
|
-
|
|
73
|
-
const result = await client.fetchFromAppium('session123', 'screenshot');
|
|
74
|
-
|
|
75
|
-
expect(result).toBeNull();
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('should return null on JSON parse error', async () => {
|
|
79
|
-
vi.mocked(fetch).mockResolvedValue({
|
|
80
|
-
json: vi.fn().mockRejectedValue(new Error('Invalid JSON')),
|
|
81
|
-
} as unknown as Response);
|
|
82
|
-
|
|
83
|
-
const result = await client.fetchFromAppium('session123', 'screenshot');
|
|
84
|
-
|
|
85
|
-
expect(result).toBeNull();
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('should handle different endpoints', async () => {
|
|
89
|
-
const mockResponse = { value: '<xml>source</xml>' };
|
|
90
|
-
vi.mocked(fetch).mockResolvedValue({
|
|
91
|
-
json: vi.fn().mockResolvedValue(mockResponse),
|
|
92
|
-
} as unknown as Response);
|
|
93
|
-
|
|
94
|
-
await client.fetchFromAppium('session456', 'source');
|
|
95
|
-
|
|
96
|
-
expect(fetch).toHaveBeenCalledWith(
|
|
97
|
-
'http://localhost:4723/session/session456/source'
|
|
98
|
-
);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('should handle value being an object', async () => {
|
|
102
|
-
const mockValue = { element: 'id123', attributes: { visible: true } };
|
|
103
|
-
const mockResponse = { value: mockValue };
|
|
104
|
-
vi.mocked(fetch).mockResolvedValue({
|
|
105
|
-
json: vi.fn().mockResolvedValue(mockResponse),
|
|
106
|
-
} as unknown as Response);
|
|
107
|
-
|
|
108
|
-
const result = await client.fetchFromAppium('session123', 'element');
|
|
109
|
-
|
|
110
|
-
expect(result).toEqual(mockValue);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('should handle value being an array', async () => {
|
|
114
|
-
const mockValue = [{ id: '1' }, { id: '2' }];
|
|
115
|
-
const mockResponse = { value: mockValue };
|
|
116
|
-
vi.mocked(fetch).mockResolvedValue({
|
|
117
|
-
json: vi.fn().mockResolvedValue(mockResponse),
|
|
118
|
-
} as unknown as Response);
|
|
119
|
-
|
|
120
|
-
const result = await client.fetchFromAppium('session123', 'elements');
|
|
121
|
-
|
|
122
|
-
expect(result).toEqual(mockValue);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('should handle value being empty string', async () => {
|
|
126
|
-
const mockResponse = { value: '' };
|
|
127
|
-
vi.mocked(fetch).mockResolvedValue({
|
|
128
|
-
json: vi.fn().mockResolvedValue(mockResponse),
|
|
129
|
-
} as unknown as Response);
|
|
130
|
-
|
|
131
|
-
const result = await client.fetchFromAppium('session123', 'endpoint');
|
|
132
|
-
|
|
133
|
-
expect(result).toBe('');
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('should handle value being false', async () => {
|
|
137
|
-
const mockResponse = { value: false };
|
|
138
|
-
vi.mocked(fetch).mockResolvedValue({
|
|
139
|
-
json: vi.fn().mockResolvedValue(mockResponse),
|
|
140
|
-
} as unknown as Response);
|
|
141
|
-
|
|
142
|
-
const result = await client.fetchFromAppium('session123', 'enabled');
|
|
143
|
-
|
|
144
|
-
expect(result).toBe(false);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('should handle value being zero', async () => {
|
|
148
|
-
const mockResponse = { value: 0 };
|
|
149
|
-
vi.mocked(fetch).mockResolvedValue({
|
|
150
|
-
json: vi.fn().mockResolvedValue(mockResponse),
|
|
151
|
-
} as unknown as Response);
|
|
152
|
-
|
|
153
|
-
const result = await client.fetchFromAppium('session123', 'count');
|
|
154
|
-
|
|
155
|
-
expect(result).toBe(0);
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
describe('captureState', () => {
|
|
160
|
-
it('should fetch both screenshot and source in parallel', async () => {
|
|
161
|
-
const screenshotResponse = { value: 'base64_screenshot' };
|
|
162
|
-
const sourceResponse = { value: '<xml>page source</xml>' };
|
|
163
|
-
|
|
164
|
-
vi.mocked(fetch)
|
|
165
|
-
.mockResolvedValueOnce({
|
|
166
|
-
json: vi.fn().mockResolvedValue(screenshotResponse),
|
|
167
|
-
} as unknown as Response)
|
|
168
|
-
.mockResolvedValueOnce({
|
|
169
|
-
json: vi.fn().mockResolvedValue(sourceResponse),
|
|
170
|
-
} as unknown as Response);
|
|
171
|
-
|
|
172
|
-
const result = await client.captureState('session123');
|
|
173
|
-
|
|
174
|
-
expect(fetch).toHaveBeenCalledTimes(2);
|
|
175
|
-
expect(result.screenshot).toBe('base64_screenshot');
|
|
176
|
-
expect(result.source).toBe('<xml>page source</xml>');
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('should return undefined screenshot if fetch fails', async () => {
|
|
180
|
-
vi.mocked(fetch)
|
|
181
|
-
.mockRejectedValueOnce(new Error('Network error'))
|
|
182
|
-
.mockResolvedValueOnce({
|
|
183
|
-
json: vi.fn().mockResolvedValue({ value: '<xml>source</xml>' }),
|
|
184
|
-
} as unknown as Response);
|
|
185
|
-
|
|
186
|
-
const result = await client.captureState('session123');
|
|
187
|
-
|
|
188
|
-
expect(result.screenshot).toBeNull();
|
|
189
|
-
expect(result.source).toBe('<xml>source</xml>');
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('should return undefined source if fetch fails', async () => {
|
|
193
|
-
vi.mocked(fetch)
|
|
194
|
-
.mockResolvedValueOnce({
|
|
195
|
-
json: vi.fn().mockResolvedValue({ value: 'base64_screenshot' }),
|
|
196
|
-
} as unknown as Response)
|
|
197
|
-
.mockRejectedValueOnce(new Error('Network error'));
|
|
198
|
-
|
|
199
|
-
const result = await client.captureState('session123');
|
|
200
|
-
|
|
201
|
-
expect(result.screenshot).toBe('base64_screenshot');
|
|
202
|
-
expect(result.source).toBeNull();
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it('should return both undefined if both fetches fail', async () => {
|
|
206
|
-
vi.mocked(fetch).mockRejectedValue(new Error('Network error'));
|
|
207
|
-
|
|
208
|
-
const result = await client.captureState('session123');
|
|
209
|
-
|
|
210
|
-
expect(result.screenshot).toBeNull();
|
|
211
|
-
expect(result.source).toBeNull();
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it('should call correct endpoints', async () => {
|
|
215
|
-
vi.mocked(fetch).mockResolvedValue({
|
|
216
|
-
json: vi.fn().mockResolvedValue({ value: 'data' }),
|
|
217
|
-
} as unknown as Response);
|
|
218
|
-
|
|
219
|
-
await client.captureState('session789');
|
|
220
|
-
|
|
221
|
-
expect(fetch).toHaveBeenCalledWith(
|
|
222
|
-
'http://localhost:4723/session/session789/screenshot'
|
|
223
|
-
);
|
|
224
|
-
expect(fetch).toHaveBeenCalledWith(
|
|
225
|
-
'http://localhost:4723/session/session789/source'
|
|
226
|
-
);
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
});
|