diff --git a/packages/mobilewright-core/src/query-engine.test.ts b/packages/mobilewright-core/src/query-engine.test.ts index b28d0f4..c10ae0e 100644 --- a/packages/mobilewright-core/src/query-engine.test.ts +++ b/packages/mobilewright-core/src/query-engine.test.ts @@ -327,6 +327,45 @@ test.describe('React Native Android role mapping', () => { }); }); +test.describe('fully-qualified native types from real device dumps', () => { + // mobilecli reports the platform's raw native type: Android keeps the full + // package path (android.widget.EditText), and iOS may keep the XCUIElementType + // prefix. getByRole must resolve both to the same cross-platform role so a single + // test works on both platforms. + const androidScreen: ViewNode[] = [ + node({ type: 'android.widget.EditText', label: 'text_field', text: 'Text Field' }), + node({ type: 'android.widget.EditText', label: 'password_field', text: 'Password' }), + node({ type: 'android.widget.EditText', label: 'multiline_text', text: 'Multiline text' }), + node({ type: 'android.widget.Switch', label: 'toggle' }), + node({ type: 'android.widget.Button', text: 'RESET COUNTER' }), + node({ type: 'android.widget.ImageButton', label: 'Navigate up' }), + ]; + + test('fully-qualified android EditTexts match the textfield role', () => { + const results = queryAll(androidScreen, { kind: 'role', value: 'textfield' }); + expect(results).toHaveLength(3); + }); + + test('fully-qualified android Switch matches the switch role', () => { + const results = queryAll(androidScreen, { kind: 'role', value: 'switch' }); + expect(results).toHaveLength(1); + }); + + test('fully-qualified android Button and ImageButton match the button role', () => { + const results = queryAll(androidScreen, { kind: 'role', value: 'button' }); + expect(results).toHaveLength(2); + }); + + test('XCUIElementType-prefixed iOS TextField matches the textfield role', () => { + const iosScreen: ViewNode[] = [ + node({ type: 'XCUIElementTypeTextField', label: 'Email' }), + node({ type: 'XCUIElementTypeSecureTextField', label: 'Password' }), + ]; + const results = queryAll(iosScreen, { kind: 'role', value: 'textfield' }); + expect(results).toHaveLength(2); + }); +}); + test.describe('placeholder strategy', () => { const tree: ViewNode[] = [ node({ type: 'TextField', placeholder: 'Enter email' }), diff --git a/packages/mobilewright-core/src/query-engine.ts b/packages/mobilewright-core/src/query-engine.ts index ae0da50..4bbd293 100644 --- a/packages/mobilewright-core/src/query-engine.ts +++ b/packages/mobilewright-core/src/query-engine.ts @@ -194,8 +194,23 @@ const ROLE_TYPE_MAP: Record = { header: ['navigationbar', 'toolbar', 'header'], }; +/** + * Reduce a raw native type to the bare name ROLE_TYPE_MAP is keyed on, so a single + * getByRole works across platforms. mobilecli reports types verbatim: Android keeps + * the full package path ("android.widget.EditText") and iOS may keep the + * "XCUIElementType" prefix ("XCUIElementTypeTextField"). getByType still matches the + * raw type — only role resolution normalizes. + */ +function bareTypeName(type: string): string { + const lower = type.toLowerCase(); + const afterPackage = lower.includes('.') ? lower.slice(lower.lastIndexOf('.') + 1) : lower; + return afterPackage.startsWith('xcuielementtype') + ? afterPackage.slice('xcuielementtype'.length) + : afterPackage; +} + function matchesRole(node: ViewNode, role: string): boolean { - const normalizedType = node.type.toLowerCase(); + const normalizedType = bareTypeName(node.type); const roleTypes = ROLE_TYPE_MAP[role.toLowerCase()]; // React Native's ReactViewGroup is used for everything — only treat it as a