import { describe, it, expect } from 'vitest';
import * as fs from 'fs/promises';
import * as path from 'path';
// Tests will import actual parser once implemented
// import { parseAndroidHierarchy, parseIOSHierarchy } from '../../../src/utils/xml-parser.js';
describe('XML Hierarchy Parser', () => {
describe('parseAndroidHierarchy', () => {
it('should parse Android UI hierarchy XML', async () => {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<hierarchy rotation="0">
<node index="0" text="Welcome" resource-id="com.example.app:id/title"
class="android.widget.TextView" package="com.example.app"
content-desc="" checkable="false" checked="false" clickable="false"
enabled="true" focusable="true" focused="false" scrollable="false"
long-clickable="false" password="false" selected="false"
visible-to-user="true" bounds="[100,200][980,300]" />
<node index="1" text="Login" resource-id="com.example.app:id/btn_login"
class="android.widget.Button" package="com.example.app"
content-desc="Login button" checkable="false" checked="false"
clickable="true" enabled="true" focusable="true" focused="false"
scrollable="false" long-clickable="false" password="false"
selected="false" visible-to-user="true" bounds="[200,400][880,500]" />
</hierarchy>`;
// When implemented:
// const elements = await parseAndroidHierarchy(xml);
// expect(elements).toHaveLength(2);
// expect(elements[0].text).toBe('Welcome');
// expect(elements[0].type).toBe('text');
// expect(elements[1].text).toBe('Login');
// expect(elements[1].type).toBe('button');
// expect(elements[1].clickable).toBe(true);
// Placeholder test structure
const expectedOutput = {
elements: [
{ text: 'Welcome', type: 'text' },
{ text: 'Login', type: 'button' },
],
};
expect(expectedOutput.elements).toHaveLength(2);
});
it('should handle nested elements', async () => {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<hierarchy rotation="0">
<node index="0" class="android.widget.FrameLayout" bounds="[0,0][1080,2340]">
<node index="0" class="android.widget.LinearLayout" bounds="[0,0][1080,2340]">
<node index="0" text="Nested" class="android.widget.TextView"
bounds="[100,100][200,150]" visible-to-user="true" enabled="true" />
</node>
</node>
</hierarchy>`;
// When implemented:
// const elements = await parseAndroidHierarchy(xml, { flatten: true });
// expect(elements.some(e => e.text === 'Nested')).toBe(true);
expect(true).toBe(true);
});
it('should filter invisible elements', async () => {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<hierarchy rotation="0">
<node index="0" text="Visible" class="android.widget.Button"
bounds="[100,100][200,150]" visible-to-user="true" enabled="true" clickable="true" />
<node index="1" text="Hidden" class="android.widget.Button"
bounds="[0,0][0,0]" visible-to-user="false" enabled="true" clickable="true" />
</hierarchy>`;
// When implemented:
// const elements = await parseAndroidHierarchy(xml, { includeInvisible: false });
// expect(elements.filter(e => e.visible)).toHaveLength(1);
expect(true).toBe(true);
});
it('should map element types correctly', async () => {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<hierarchy rotation="0">
<node class="android.widget.Button" bounds="[0,0][100,50]" visible-to-user="true" enabled="true" />
<node class="android.widget.EditText" bounds="[0,50][100,100]" visible-to-user="true" enabled="true" />
<node class="android.widget.ImageView" bounds="[0,100][100,150]" visible-to-user="true" enabled="true" />
<node class="androidx.recyclerview.widget.RecyclerView" bounds="[0,150][100,500]" visible-to-user="true" enabled="true" />
</hierarchy>`;
// When implemented:
// const elements = await parseAndroidHierarchy(xml);
// expect(elements[0].type).toBe('button');
// expect(elements[1].type).toBe('input');
// expect(elements[2].type).toBe('image');
// expect(elements[3].type).toBe('list');
const expectedTypes = ['button', 'input', 'image', 'list'];
expect(expectedTypes).toHaveLength(4);
});
it('should use mock response file', async () => {
// Read actual mock file
const mockPath = path.join(
process.cwd(),
'tests/mocks/adb-responses/ui-hierarchy.xml'
);
try {
const mockXml = await fs.readFile(mockPath, 'utf-8');
// When implemented:
// const elements = await parseAndroidHierarchy(mockXml);
// expect(elements.length).toBeGreaterThan(0);
expect(mockXml).toContain('<hierarchy');
} catch {
// Mock file may not exist yet
expect(true).toBe(true);
}
});
});
describe('parseIOSHierarchy', () => {
it('should parse iOS accessibility hierarchy', async () => {
// iOS hierarchy is typically in a different format
// This tests the expected structure
const iosHierarchy = {
elements: [
{
type: 'XCUIElementTypeButton',
label: 'Login',
identifier: 'loginButton',
frame: { x: 100, y: 200, width: 200, height: 50 },
},
],
};
// When implemented:
// const elements = parseIOSHierarchy(JSON.stringify(iosHierarchy));
// expect(elements[0].type).toBe('button');
expect(iosHierarchy.elements).toHaveLength(1);
});
});
});
describe('Element Type Mapping', () => {
const androidToUnifiedMap: Record<string, string> = {
'android.widget.Button': 'button',
'android.widget.TextView': 'text',
'android.widget.EditText': 'input',
'android.widget.ImageView': 'image',
'androidx.recyclerview.widget.RecyclerView': 'list',
'android.widget.ListView': 'list',
'android.widget.ScrollView': 'scroll',
'android.widget.Switch': 'switch',
'android.widget.CheckBox': 'checkbox',
'android.widget.LinearLayout': 'container',
};
it('should have mappings for common Android views', () => {
expect(Object.keys(androidToUnifiedMap).length).toBeGreaterThan(5);
expect(androidToUnifiedMap['android.widget.Button']).toBe('button');
});
const iosToUnifiedMap: Record<string, string> = {
XCUIElementTypeButton: 'button',
XCUIElementTypeStaticText: 'text',
XCUIElementTypeTextField: 'input',
XCUIElementTypeImage: 'image',
XCUIElementTypeTable: 'list',
XCUIElementTypeScrollView: 'scroll',
XCUIElementTypeSwitch: 'switch',
};
it('should have mappings for common iOS elements', () => {
expect(Object.keys(iosToUnifiedMap).length).toBeGreaterThan(5);
expect(iosToUnifiedMap['XCUIElementTypeButton']).toBe('button');
});
});