appium-session-recorder 0.0.2 → 0.0.4

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 (73) hide show
  1. package/dist/index.js +33319 -0
  2. package/dist/ui/assets/index-CnJwu_Mc.js +8 -0
  3. package/dist/ui/assets/index-VIFL67d5.css +1 -0
  4. package/{src → dist}/ui/index.html +2 -1
  5. package/package.json +20 -13
  6. package/bun.lock +0 -731
  7. package/src/cli/arg-parser.ts +0 -311
  8. package/src/cli/commands/drive.ts +0 -147
  9. package/src/cli/commands/index.ts +0 -54
  10. package/src/cli/commands/proxy.ts +0 -41
  11. package/src/cli/commands/screen.ts +0 -73
  12. package/src/cli/commands/selectors.ts +0 -42
  13. package/src/cli/commands/session.ts +0 -64
  14. package/src/cli/commands/types.ts +0 -11
  15. package/src/cli/index.ts +0 -158
  16. package/src/cli/prompts.ts +0 -64
  17. package/src/cli/response.ts +0 -44
  18. package/src/core/appium/client.ts +0 -248
  19. package/src/core/index.ts +0 -5
  20. package/src/core/selectors/generate-candidates.ts +0 -155
  21. package/src/core/selectors/score-candidates.ts +0 -184
  22. package/src/core/types.ts +0 -79
  23. package/src/core/xml/parse-source.ts +0 -197
  24. package/src/index.ts +0 -7
  25. package/src/server/appium-client.ts +0 -24
  26. package/src/server/index.ts +0 -6
  27. package/src/server/interaction-recorder.ts +0 -74
  28. package/src/server/proxy-middleware.ts +0 -68
  29. package/src/server/routes.ts +0 -64
  30. package/src/server/server.ts +0 -43
  31. package/src/server/types.ts +0 -34
  32. package/src/ui/bun.lock +0 -311
  33. package/src/ui/package.json +0 -20
  34. package/src/ui/src/App.css +0 -12
  35. package/src/ui/src/App.tsx +0 -41
  36. package/src/ui/src/components/ActionCarousel.css +0 -128
  37. package/src/ui/src/components/ActionCarousel.tsx +0 -92
  38. package/src/ui/src/components/Inspector.css +0 -314
  39. package/src/ui/src/components/Inspector.tsx +0 -265
  40. package/src/ui/src/components/InteractionCard.css +0 -159
  41. package/src/ui/src/components/InteractionCard.tsx +0 -60
  42. package/src/ui/src/components/MainInspector.css +0 -304
  43. package/src/ui/src/components/MainInspector.tsx +0 -304
  44. package/src/ui/src/components/Stats.css +0 -27
  45. package/src/ui/src/components/Timeline.css +0 -31
  46. package/src/ui/src/components/Timeline.tsx +0 -37
  47. package/src/ui/src/hooks/useInteractions.ts +0 -73
  48. package/src/ui/src/index.tsx +0 -11
  49. package/src/ui/src/services/api.ts +0 -41
  50. package/src/ui/src/styles/tokens.css +0 -126
  51. package/src/ui/src/types.ts +0 -34
  52. package/src/ui/src/utils/__tests__/locators.test.ts +0 -304
  53. package/src/ui/src/utils/__tests__/xml-parser.test.ts +0 -326
  54. package/src/ui/src/utils/locators.ts +0 -14
  55. package/src/ui/src/utils/xml-parser.ts +0 -45
  56. package/src/ui/tsconfig.json +0 -34
  57. package/src/ui/tsconfig.node.json +0 -11
  58. package/src/ui/vite.config.ts +0 -22
  59. package/tests/cli/arg-parser.test.ts +0 -397
  60. package/tests/cli/drive-commands.test.ts +0 -151
  61. package/tests/cli/selectors-best.test.ts +0 -42
  62. package/tests/cli/session-commands.test.ts +0 -53
  63. package/tests/core/selector-candidates.test.ts +0 -83
  64. package/tests/core/selector-scoring.test.ts +0 -75
  65. package/tests/core/xml-parser.test.ts +0 -56
  66. package/tests/server/appium-client.test.ts +0 -229
  67. package/tests/server/interaction-recorder.test.ts +0 -377
  68. package/tests/server/proxy-middleware.test.ts +0 -343
  69. package/tests/server/routes.test.ts +0 -305
  70. package/tsconfig.json +0 -26
  71. package/vitest.config.ts +0 -16
  72. package/vitest.ui.config.ts +0 -15
  73. package/workflow.gif +0 -0
@@ -1,155 +0,0 @@
1
- import type { ParsedElement, Platform, SelectorCandidate } from '../types';
2
-
3
- function pushUnique(candidates: SelectorCandidate[], candidate: SelectorCandidate): void {
4
- if (!candidate.value) return;
5
- if (candidates.some(existing => existing.strategy === candidate.strategy && existing.value === candidate.value)) {
6
- return;
7
- }
8
- candidates.push(candidate);
9
- }
10
-
11
- function escapeDoubleQuoted(value: string): string {
12
- return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
13
- }
14
-
15
- export function generateSelectorCandidates(element: ParsedElement): SelectorCandidate[] {
16
- const candidates: SelectorCandidate[] = [];
17
- const platform: Platform = element.platform;
18
-
19
- const primaryAccessibility = element.name || element.label || element.contentDesc;
20
- if (primaryAccessibility) {
21
- pushUnique(candidates, {
22
- strategy: 'accessibility id',
23
- value: primaryAccessibility,
24
- platform,
25
- });
26
- }
27
-
28
- if (platform === 'ios') {
29
- if (element.label && element.label !== primaryAccessibility) {
30
- pushUnique(candidates, {
31
- strategy: 'accessibility id',
32
- value: element.label,
33
- platform,
34
- });
35
- }
36
-
37
- if (element.name) {
38
- pushUnique(candidates, {
39
- strategy: '-ios predicate string',
40
- value: `name == "${escapeDoubleQuoted(element.name)}"`,
41
- platform,
42
- });
43
- pushUnique(candidates, {
44
- strategy: '-ios class chain',
45
- value: `**/${element.type}[\`name == "${escapeDoubleQuoted(element.name)}"\`]`,
46
- platform,
47
- });
48
- }
49
-
50
- if (element.label) {
51
- pushUnique(candidates, {
52
- strategy: '-ios predicate string',
53
- value: `label == "${escapeDoubleQuoted(element.label)}"`,
54
- platform,
55
- });
56
- }
57
- }
58
-
59
- if (platform === 'android') {
60
- if (element.resourceId) {
61
- pushUnique(candidates, {
62
- strategy: 'id',
63
- value: element.resourceId,
64
- platform,
65
- });
66
- pushUnique(candidates, {
67
- strategy: '-android uiautomator',
68
- value: `new UiSelector().resourceId("${escapeDoubleQuoted(element.resourceId)}")`,
69
- platform,
70
- });
71
- }
72
-
73
- if (element.contentDesc) {
74
- pushUnique(candidates, {
75
- strategy: 'accessibility id',
76
- value: element.contentDesc,
77
- platform,
78
- });
79
- pushUnique(candidates, {
80
- strategy: '-android uiautomator',
81
- value: `new UiSelector().description("${escapeDoubleQuoted(element.contentDesc)}")`,
82
- platform,
83
- });
84
- }
85
-
86
- if (element.text) {
87
- pushUnique(candidates, {
88
- strategy: '-android uiautomator',
89
- value: `new UiSelector().text("${escapeDoubleQuoted(element.text)}")`,
90
- platform,
91
- });
92
- }
93
- }
94
-
95
- if (element.type && (element.name || element.label || element.text)) {
96
- const attrName = element.platform === 'android' ? 'text' : 'name';
97
- const attrValue = element.platform === 'android' ? element.text || element.label : element.name || element.label;
98
- if (attrValue) {
99
- pushUnique(candidates, {
100
- strategy: 'xpath',
101
- value: `//*[@type="${escapeDoubleQuoted(element.type)}" and @${attrName}="${escapeDoubleQuoted(attrValue)}"]`,
102
- platform: 'generic',
103
- });
104
- }
105
- }
106
-
107
- pushUnique(candidates, { strategy: 'xpath', value: element.xpath, platform: 'generic' });
108
- pushUnique(candidates, { strategy: 'class name', value: element.type, platform: 'generic' });
109
-
110
- return candidates;
111
- }
112
-
113
- export type LegacyLocatorInput = {
114
- type: string;
115
- name: string;
116
- label: string;
117
- xpath: string;
118
- };
119
-
120
- export function generateLegacyLocators(element: LegacyLocatorInput): SelectorCandidate[] {
121
- const locators: SelectorCandidate[] = [];
122
-
123
- if (element.name) {
124
- locators.push({ strategy: 'accessibility id', value: element.name, platform: 'ios' });
125
- }
126
- if (element.label && element.label !== element.name) {
127
- locators.push({ strategy: 'accessibility id', value: element.label, platform: 'ios' });
128
- }
129
- locators.push({ strategy: 'xpath', value: element.xpath, platform: 'generic' });
130
- locators.push({ strategy: 'class name', value: element.type, platform: 'generic' });
131
-
132
- if (element.name) {
133
- locators.push({
134
- strategy: '-ios predicate string',
135
- value: `name == "${escapeDoubleQuoted(element.name)}"`,
136
- platform: 'ios',
137
- });
138
- }
139
- if (element.label) {
140
- locators.push({
141
- strategy: '-ios predicate string',
142
- value: `label == "${escapeDoubleQuoted(element.label)}"`,
143
- platform: 'ios',
144
- });
145
- }
146
- if (element.name) {
147
- locators.push({
148
- strategy: '-ios class chain',
149
- value: `**/${element.type}[\`name == "${escapeDoubleQuoted(element.name)}"\`]`,
150
- platform: 'ios',
151
- });
152
- }
153
-
154
- return locators;
155
- }
@@ -1,184 +0,0 @@
1
- import type { ParsedElement, RankedSelector, SelectorCandidate, SelectorReason, SelectorStrategy } from '../types';
2
-
3
- const baseScoreByStrategy: Record<SelectorStrategy, number> = {
4
- id: 100,
5
- 'accessibility id': 95,
6
- '-ios predicate string': 90,
7
- '-ios class chain': 85,
8
- '-android uiautomator': 85,
9
- xpath: 70,
10
- 'class name': 55,
11
- };
12
-
13
- function isDynamicValue(value: string): boolean {
14
- if (/(?:^|[^a-zA-Z])\d{5,}(?:$|[^a-zA-Z])/.test(value)) return true;
15
- if (/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}/.test(value)) {
16
- return true;
17
- }
18
- if (/autogen|generated|tmp|temp|anonymous/i.test(value)) return true;
19
- return false;
20
- }
21
-
22
- function xpathFragilityPenalty(xpath: string): number {
23
- if (!xpath.startsWith('/')) return 0;
24
-
25
- const depth = xpath.split('/').filter(Boolean).length;
26
- const indexCount = (xpath.match(/\[\d+\]/g) || []).length;
27
-
28
- let penalty = 0;
29
- if (depth > 5) penalty += 8;
30
- if (indexCount > 3) penalty += 8;
31
-
32
- return penalty;
33
- }
34
-
35
- function valueEquals(value: string, expected: string): boolean {
36
- return value === expected;
37
- }
38
-
39
- function matchIosPredicate(element: ParsedElement, predicate: string): boolean {
40
- const equalsMatch = predicate.match(/(name|label|type)\s*==\s*['"](.+?)['"]/i);
41
- if (equalsMatch) {
42
- const [, field, expected] = equalsMatch;
43
- if (field === 'name') return valueEquals(element.name, expected);
44
- if (field === 'label') return valueEquals(element.label, expected);
45
- return valueEquals(element.type, expected);
46
- }
47
-
48
- const containsMatch = predicate.match(/(name|label|type)\s*CONTAINS\s*['"](.+?)['"]/i);
49
- if (containsMatch) {
50
- const [, field, expected] = containsMatch;
51
- if (field === 'name') return element.name.includes(expected);
52
- if (field === 'label') return element.label.includes(expected);
53
- return element.type.includes(expected);
54
- }
55
-
56
- return false;
57
- }
58
-
59
- function matchIosClassChain(element: ParsedElement, classChain: string): boolean {
60
- const classChainMatch = classChain.match(/\*\*\/(\w+)(?:\[`(.+?)`\])?/);
61
- if (!classChainMatch) return false;
62
-
63
- const targetType = classChainMatch[1];
64
- const predicate = classChainMatch[2];
65
- if (element.type !== targetType) return false;
66
-
67
- if (!predicate) return true;
68
- return matchIosPredicate(element, predicate);
69
- }
70
-
71
- function matchAndroidUiAutomator(element: ParsedElement, value: string): boolean {
72
- const matches = [...value.matchAll(/\.(\w+)\("(.+?)"\)/g)];
73
- if (matches.length === 0) return false;
74
-
75
- return matches.every(([, method, expected]) => {
76
- if (method === 'resourceId') return element.resourceId === expected;
77
- if (method === 'description') return element.contentDesc === expected;
78
- if (method === 'text') return element.text === expected || element.label === expected;
79
- if (method === 'className') return element.type === expected;
80
- return false;
81
- });
82
- }
83
-
84
- function matchSimpleXPath(element: ParsedElement, xpath: string): boolean {
85
- if (xpath === element.xpath) return true;
86
-
87
- const attrMatches = [...xpath.matchAll(/@(\w+)="([^"]+)"/g)];
88
- if (attrMatches.length === 0) return false;
89
-
90
- return attrMatches.every(([, attribute, expected]) => {
91
- if (attribute === 'type') return element.type === expected;
92
- if (attribute === 'name') return element.name === expected;
93
- if (attribute === 'label') return element.label === expected;
94
- if (attribute === 'text') return element.text === expected;
95
- if (attribute === 'resource-id') return element.resourceId === expected;
96
- if (attribute === 'content-desc') return element.contentDesc === expected;
97
- return element.attributes[attribute] === expected;
98
- });
99
- }
100
-
101
- function candidateMatchesElement(candidate: SelectorCandidate, element: ParsedElement): boolean {
102
- switch (candidate.strategy) {
103
- case 'id':
104
- return element.resourceId === candidate.value;
105
- case 'accessibility id':
106
- return (
107
- element.name === candidate.value ||
108
- element.label === candidate.value ||
109
- element.contentDesc === candidate.value
110
- );
111
- case 'class name':
112
- return element.type === candidate.value;
113
- case 'xpath':
114
- return matchSimpleXPath(element, candidate.value);
115
- case '-ios predicate string':
116
- return matchIosPredicate(element, candidate.value);
117
- case '-ios class chain':
118
- return matchIosClassChain(element, candidate.value);
119
- case '-android uiautomator':
120
- return matchAndroidUiAutomator(element, candidate.value);
121
- default:
122
- return false;
123
- }
124
- }
125
-
126
- export function rankSelectorCandidates(
127
- target: ParsedElement,
128
- allElements: ParsedElement[],
129
- candidates: SelectorCandidate[],
130
- ): RankedSelector[] {
131
- const ranked = candidates.map((candidate) => {
132
- const reasons: SelectorReason[] = ['BASE_STRATEGY_PRIORITY'];
133
- let score = baseScoreByStrategy[candidate.strategy] ?? 50;
134
-
135
- const matchCount = allElements.filter(element => candidateMatchesElement(candidate, element)).length;
136
-
137
- if (matchCount === 1) {
138
- score += 25;
139
- reasons.push('UNIQUE_MATCH');
140
- } else if (matchCount > 1) {
141
- score -= 10;
142
- reasons.push('MULTIPLE_MATCHES');
143
- } else {
144
- score -= 40;
145
- reasons.push('NO_MATCH');
146
- }
147
-
148
- if (matchCount > 0) {
149
- score += 5;
150
- reasons.push('VALID_MATCH');
151
- }
152
-
153
- if (target.enabled && target.visible && (target.clickable || target.accessible)) {
154
- score += 8;
155
- reasons.push('ACTIONABLE_ELEMENT_BONUS');
156
- }
157
-
158
- if (isDynamicValue(candidate.value)) {
159
- score -= 18;
160
- reasons.push('DYNAMIC_TOKEN_PENALTY');
161
- }
162
-
163
- if (candidate.strategy === 'xpath') {
164
- const penalty = xpathFragilityPenalty(candidate.value);
165
- if (penalty > 0) {
166
- score -= penalty;
167
- reasons.push('FRAGILE_XPATH_PENALTY');
168
- }
169
- }
170
-
171
- return {
172
- ...candidate,
173
- score: Math.max(0, Math.min(100, Math.round(score))),
174
- matchCount,
175
- reasons,
176
- };
177
- });
178
-
179
- return ranked.sort((a, b) => {
180
- if (b.score !== a.score) return b.score - a.score;
181
- if (a.matchCount !== b.matchCount) return a.matchCount - b.matchCount;
182
- return a.value.localeCompare(b.value);
183
- });
184
- }
package/src/core/types.ts DELETED
@@ -1,79 +0,0 @@
1
- export type Platform = 'ios' | 'android' | 'unknown';
2
-
3
- export type SelectorStrategy =
4
- | 'accessibility id'
5
- | 'id'
6
- | 'xpath'
7
- | 'class name'
8
- | '-ios predicate string'
9
- | '-ios class chain'
10
- | '-android uiautomator';
11
-
12
- export type SelectorReason =
13
- | 'BASE_STRATEGY_PRIORITY'
14
- | 'UNIQUE_MATCH'
15
- | 'MULTIPLE_MATCHES'
16
- | 'NO_MATCH'
17
- | 'VALID_MATCH'
18
- | 'ACTIONABLE_ELEMENT_BONUS'
19
- | 'DYNAMIC_TOKEN_PENALTY'
20
- | 'FRAGILE_XPATH_PENALTY';
21
-
22
- export type ParsedElement = {
23
- elementRef: string;
24
- index: number;
25
- platform: Platform;
26
- type: string;
27
- xpath: string;
28
- name: string;
29
- label: string;
30
- value: string;
31
- text: string;
32
- resourceId: string;
33
- contentDesc: string;
34
- enabled: boolean;
35
- visible: boolean;
36
- accessible: boolean;
37
- clickable: boolean;
38
- x: number;
39
- y: number;
40
- width: number;
41
- height: number;
42
- attributes: Record<string, string>;
43
- };
44
-
45
- export type SelectorCandidate = {
46
- strategy: SelectorStrategy;
47
- value: string;
48
- platform: Platform | 'generic';
49
- };
50
-
51
- export type RankedSelector = SelectorCandidate & {
52
- score: number;
53
- matchCount: number;
54
- reasons: SelectorReason[];
55
- };
56
-
57
- export type ParsedSource = {
58
- platform: Platform;
59
- elements: ParsedElement[];
60
- };
61
-
62
- export type CommandError = {
63
- code: string;
64
- message: string;
65
- details?: unknown;
66
- };
67
-
68
- export type CommandResponse<T = unknown> = {
69
- ok: boolean;
70
- command: string;
71
- timestamp: string;
72
- result?: T;
73
- error?: CommandError;
74
- };
75
-
76
- export type Point = {
77
- x: number;
78
- y: number;
79
- };
@@ -1,197 +0,0 @@
1
- import { XMLParser } from 'fast-xml-parser';
2
- import type { ParsedElement, ParsedSource, Platform } from '../types';
3
-
4
- type XmlNode = Record<string, unknown>;
5
-
6
- const parser = new XMLParser({
7
- ignoreAttributes: false,
8
- attributeNamePrefix: '',
9
- parseTagValue: false,
10
- parseAttributeValue: false,
11
- trimValues: false,
12
- textNodeName: '#text',
13
- preserveOrder: false,
14
- });
15
-
16
- function asString(value: unknown): string {
17
- if (value === undefined || value === null) return '';
18
- return String(value);
19
- }
20
-
21
- function asBoolean(value: unknown): boolean {
22
- if (typeof value === 'boolean') return value;
23
- return asString(value).toLowerCase() === 'true' || asString(value) === '1';
24
- }
25
-
26
- function asNumber(value: unknown): number {
27
- const n = Number(asString(value));
28
- return Number.isFinite(n) ? n : 0;
29
- }
30
-
31
- function parseAndroidBounds(bounds: string): { x: number; y: number; width: number; height: number } {
32
- const match = bounds.match(/\[(\-?\d+),(\-?\d+)\]\[(\-?\d+),(\-?\d+)\]/);
33
- if (!match) {
34
- return { x: 0, y: 0, width: 0, height: 0 };
35
- }
36
-
37
- const x1 = Number(match[1]);
38
- const y1 = Number(match[2]);
39
- const x2 = Number(match[3]);
40
- const y2 = Number(match[4]);
41
-
42
- return {
43
- x: x1,
44
- y: y1,
45
- width: Math.max(0, x2 - x1),
46
- height: Math.max(0, y2 - y1),
47
- };
48
- }
49
-
50
- function detectPlatform(type: string, attrs: Record<string, string>): Platform {
51
- if (type.startsWith('XCUIElementType') || attrs.type?.startsWith('XCUIElementType')) {
52
- return 'ios';
53
- }
54
-
55
- if (
56
- type.startsWith('android.') ||
57
- attrs.class?.startsWith('android.') ||
58
- attrs['resource-id'] ||
59
- attrs['content-desc']
60
- ) {
61
- return 'android';
62
- }
63
-
64
- return 'unknown';
65
- }
66
-
67
- function extractAttributes(node: XmlNode): Record<string, string> {
68
- const attrs: Record<string, string> = {};
69
-
70
- for (const [key, value] of Object.entries(node)) {
71
- if (value === null || value === undefined || key === '#text') continue;
72
- if (Array.isArray(value)) continue;
73
- if (typeof value === 'object') continue;
74
- attrs[key] = String(value);
75
- }
76
-
77
- return attrs;
78
- }
79
-
80
- function extractChildren(node: XmlNode): Array<{ tag: string; child: XmlNode }> {
81
- const children: Array<{ tag: string; child: XmlNode }> = [];
82
-
83
- for (const [key, value] of Object.entries(node)) {
84
- if (key === '#text' || value === null || value === undefined) continue;
85
-
86
- if (Array.isArray(value)) {
87
- for (const item of value) {
88
- if (item && typeof item === 'object') {
89
- children.push({ tag: key, child: item as XmlNode });
90
- }
91
- }
92
- continue;
93
- }
94
-
95
- if (typeof value === 'object') {
96
- children.push({ tag: key, child: value as XmlNode });
97
- }
98
- }
99
-
100
- return children;
101
- }
102
-
103
- function makeElementRef(platform: Platform, xpath: string): string {
104
- return `${platform}:${xpath}`;
105
- }
106
-
107
- export function parseSource(xmlString: string): ParsedSource {
108
- if (!xmlString || !xmlString.trim()) {
109
- return { platform: 'unknown', elements: [] };
110
- }
111
-
112
- let parsed: Record<string, unknown>;
113
- try {
114
- parsed = parser.parse(xmlString) as Record<string, unknown>;
115
- } catch {
116
- return { platform: 'unknown', elements: [] };
117
- }
118
-
119
- const rootEntry = Object.entries(parsed).find(([, value]) => value && typeof value === 'object');
120
- if (!rootEntry) {
121
- return { platform: 'unknown', elements: [] };
122
- }
123
-
124
- const [rootTag, rootNode] = rootEntry;
125
- const elements: ParsedElement[] = [];
126
- let nextIndex = 0;
127
-
128
- function visit(tag: string, node: XmlNode, parentXpath: string, siblingIndex: number): void {
129
- const attrs = extractAttributes(node);
130
- const type = attrs.type || attrs.class || tag;
131
- const xpath = `${parentXpath}/${type}[${siblingIndex}]`;
132
-
133
- const platform = detectPlatform(type, attrs);
134
- const bounds = attrs.bounds ? parseAndroidBounds(attrs.bounds) : undefined;
135
-
136
- const x = attrs.x !== undefined ? asNumber(attrs.x) : (bounds?.x ?? 0);
137
- const y = attrs.y !== undefined ? asNumber(attrs.y) : (bounds?.y ?? 0);
138
- const width = attrs.width !== undefined ? asNumber(attrs.width) : (bounds?.width ?? 0);
139
- const height = attrs.height !== undefined ? asNumber(attrs.height) : (bounds?.height ?? 0);
140
-
141
- const name = attrs.name || attrs['content-desc'] || attrs.resourceId || '';
142
- const label = attrs.label || attrs.text || attrs['content-desc'] || '';
143
- const value = attrs.value || '';
144
- const text = attrs.text || value || '';
145
- const resourceId = attrs['resource-id'] || attrs.resourceId || attrs.id || '';
146
- const contentDesc = attrs['content-desc'] || attrs.contentDesc || '';
147
-
148
- elements.push({
149
- elementRef: makeElementRef(platform, xpath),
150
- index: nextIndex++,
151
- platform,
152
- type,
153
- xpath,
154
- name,
155
- label,
156
- value,
157
- text,
158
- resourceId,
159
- contentDesc,
160
- enabled: asBoolean(attrs.enabled),
161
- visible: asBoolean(attrs.visible) || asBoolean(attrs.displayed),
162
- accessible: asBoolean(attrs.accessible),
163
- clickable: asBoolean(attrs.clickable),
164
- x,
165
- y,
166
- width,
167
- height,
168
- attributes: attrs,
169
- });
170
-
171
- const childCounters: Record<string, number> = {};
172
- const children = extractChildren(node);
173
- for (const { tag: childTag, child } of children) {
174
- const childType = asString((child as XmlNode).type || (child as XmlNode).class || childTag);
175
- childCounters[childType] = (childCounters[childType] || 0) + 1;
176
- visit(childTag, child, xpath, childCounters[childType]);
177
- }
178
- }
179
-
180
- visit(rootTag, rootNode as XmlNode, '', 1);
181
-
182
- const platform =
183
- elements.find(element => element.platform === 'ios')?.platform ||
184
- elements.find(element => element.platform === 'android')?.platform ||
185
- 'unknown';
186
-
187
- const withResolvedPlatform = elements.map(element => ({
188
- ...element,
189
- platform: element.platform === 'unknown' ? platform : element.platform,
190
- elementRef: makeElementRef(element.platform === 'unknown' ? platform : element.platform, element.xpath),
191
- }));
192
-
193
- return {
194
- platform,
195
- elements: withResolvedPlatform,
196
- };
197
- }
package/src/index.ts DELETED
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env bun
2
- import { runCLI } from './cli/index';
3
-
4
- runCLI().catch((error) => {
5
- console.error('Fatal error:', error);
6
- process.exit(1);
7
- });
@@ -1,24 +0,0 @@
1
- export class AppiumClient {
2
- constructor(private appiumUrl: string) { }
3
-
4
- async fetchFromAppium(sessionId: string, endpoint: string): Promise<any> {
5
- try {
6
- const response = await fetch(`${this.appiumUrl}/session/${sessionId}/${endpoint}`);
7
- const data = await response.json();
8
- if (data && typeof data === 'object' && 'value' in data) {
9
- return data.value;
10
- }
11
- return null;
12
- } catch (e) {
13
- return null;
14
- }
15
- }
16
-
17
- async captureState(sessionId: string): Promise<{ screenshot?: string; source?: string }> {
18
- const [screenshot, source] = await Promise.all([
19
- this.fetchFromAppium(sessionId, 'screenshot'),
20
- this.fetchFromAppium(sessionId, 'source'),
21
- ]);
22
- return { screenshot, source };
23
- }
24
- }
@@ -1,6 +0,0 @@
1
- export * from './types';
2
- export * from './appium-client';
3
- export * from './interaction-recorder';
4
- export * from './proxy-middleware';
5
- export * from './routes';
6
- export * from './server';