diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts
index 47cdc6627..693fc8d27 100644
--- a/src/__tests__/config.test.ts
+++ b/src/__tests__/config.test.ts
@@ -36,6 +36,7 @@ test('resetToDefaults() resets internal config to defaults', () => {
switch: 'A',
scrollView: 'A',
modal: 'A',
+ image: 'A',
},
});
expect(getConfig().hostComponentNames).toEqual({
@@ -44,6 +45,7 @@ test('resetToDefaults() resets internal config to defaults', () => {
switch: 'A',
scrollView: 'A',
modal: 'A',
+ image: 'A',
});
resetToDefaults();
diff --git a/src/__tests__/host-component-names.test.tsx b/src/__tests__/host-component-names.test.tsx
index ca526742d..533c679ec 100644
--- a/src/__tests__/host-component-names.test.tsx
+++ b/src/__tests__/host-component-names.test.tsx
@@ -17,6 +17,7 @@ describe('getHostComponentNames', () => {
switch: 'banana',
scrollView: 'banana',
modal: 'banana',
+ image: 'banana',
},
});
@@ -26,6 +27,7 @@ describe('getHostComponentNames', () => {
switch: 'banana',
scrollView: 'banana',
modal: 'banana',
+ image: 'banana',
});
});
@@ -40,6 +42,7 @@ describe('getHostComponentNames', () => {
switch: 'RCTSwitch',
scrollView: 'RCTScrollView',
modal: 'Modal',
+ image: 'Image',
});
expect(getConfig().hostComponentNames).toBe(hostComponentNames);
});
@@ -70,6 +73,7 @@ describe('configureHostComponentNamesIfNeeded', () => {
switch: 'RCTSwitch',
scrollView: 'RCTScrollView',
modal: 'Modal',
+ image: 'Image',
});
});
@@ -81,6 +85,7 @@ describe('configureHostComponentNamesIfNeeded', () => {
switch: 'banana',
scrollView: 'banana',
modal: 'banana',
+ image: 'banana',
},
});
@@ -92,6 +97,7 @@ describe('configureHostComponentNamesIfNeeded', () => {
switch: 'banana',
scrollView: 'banana',
modal: 'banana',
+ image: 'banana',
});
});
diff --git a/src/config.ts b/src/config.ts
index fd867a895..75b7eb665 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -26,6 +26,7 @@ export type HostComponentNames = {
switch: string;
scrollView: string;
modal: string;
+ image: string;
};
export type InternalConfig = Config & {
diff --git a/src/helpers/format-default.ts b/src/helpers/format-default.ts
index 223140753..7ee42b5b1 100644
--- a/src/helpers/format-default.ts
+++ b/src/helpers/format-default.ts
@@ -9,6 +9,7 @@ const propsToDisplay = [
'accessibilityLabelledBy',
'accessibilityRole',
'accessibilityViewIsModal',
+ 'alt',
'aria-busy',
'aria-checked',
'aria-disabled',
diff --git a/src/helpers/host-component-names.tsx b/src/helpers/host-component-names.tsx
index cd4b52239..f806e392c 100644
--- a/src/helpers/host-component-names.tsx
+++ b/src/helpers/host-component-names.tsx
@@ -1,6 +1,6 @@
import * as React from 'react';
import { ReactTestInstance } from 'react-test-renderer';
-import { Modal, ScrollView, Switch, Text, TextInput, View } from 'react-native';
+import { Modal, ScrollView, Switch, Image, Text, TextInput, View } from 'react-native';
import { configureInternal, getConfig, HostComponentNames } from '../config';
import { renderWithAct } from '../render-act';
import { HostTestInstance } from './component-tree';
@@ -37,6 +37,7 @@ function detectHostComponentNames(): HostComponentNames {
+
,
);
@@ -46,6 +47,7 @@ function detectHostComponentNames(): HostComponentNames {
switch: getByTestId(renderer.root, 'switch').type as string,
scrollView: getByTestId(renderer.root, 'scrollView').type as string,
modal: getByTestId(renderer.root, 'modal').type as string,
+ image: getByTestId(renderer.root, 'image').type as string,
};
} catch (error) {
const errorMessage =
@@ -108,3 +110,11 @@ export function isHostScrollView(element?: ReactTestInstance | null): element is
export function isHostModal(element?: ReactTestInstance | null): element is HostTestInstance {
return element?.type === getHostComponentNames().modal;
}
+
+/**
+ * Checks if the given element is a host Image element.
+ * @param element The element to check.
+ */
+export function isHostImage(element?: ReactTestInstance | null): element is HostTestInstance {
+ return element?.type === getHostComponentNames().image;
+}
diff --git a/src/queries/__tests__/alt-text.test.tsx b/src/queries/__tests__/alt-text.test.tsx
new file mode 100644
index 000000000..d1baeb6b6
--- /dev/null
+++ b/src/queries/__tests__/alt-text.test.tsx
@@ -0,0 +1,150 @@
+import React from 'react';
+import { Image, View } from 'react-native';
+import { render, screen } from '../..';
+
+const Banana = () => (
+
+
+
+
+);
+
+test('it can locate an image by alt text', () => {
+ render(
+
+
+
+ ,
+ );
+
+ expect(screen.getByAltText('alt text')).toBeTruthy();
+ expect(screen.getByAltText('other text')).toBeTruthy();
+
+ expect(screen.getAllByAltText('alt text')).toHaveLength(1);
+ expect(screen.getAllByAltText('other text')).toHaveLength(1);
+});
+
+test('supports a regex matcher', () => {
+ render(
+
+
+
+ ,
+ );
+
+ expect(screen.getByAltText(/alt/)).toBeTruthy();
+ expect(screen.getAllByAltText(/alt/)).toHaveLength(1);
+ expect(screen.getAllByAltText(/text/)).toHaveLength(2);
+});
+
+test('getByAltText, queryByAltText', () => {
+ render();
+ const component = screen.getByAltText(/fresh banana/);
+
+ expect(() => screen.getByAltText('InExistent')).toThrow(
+ 'Unable to find an element with alt text: InExistent',
+ );
+
+ expect(screen.getByAltText(/fresh banana/)).toBe(component);
+ expect(screen.queryByAltText('InExistent')).toBeNull();
+
+ expect(() => screen.getByAltText(/banana/)).toThrow(
+ 'Found multiple elements with alt text: /banana/',
+ );
+ expect(() => screen.queryByAltText(/banana/)).toThrow(
+ 'Found multiple elements with alt text: /banana/',
+ );
+});
+
+test('getAllByAltText, queryAllByAltText', () => {
+ render();
+ const imageElements = screen.getAllByAltText(/banana/);
+
+ expect(imageElements.length).toBe(2);
+ expect(imageElements[0].props.alt).toBe('Image of a fresh banana');
+ expect(imageElements[1].props.alt).toBe('Image of a brown banana');
+
+ const queriedImageElements = screen.queryAllByAltText(/banana/);
+
+ expect(queriedImageElements.length).toBe(2);
+ expect(queriedImageElements[0]).toBe(imageElements[0]);
+ expect(queriedImageElements[1]).toBe(imageElements[1]);
+});
+
+test('findByAltText and findAllByAltText work asynchronously', async () => {
+ const options = { timeout: 10 }; // Short timeout so that this test runs quickly
+ render();
+ await expect(screen.findByAltText('alt text', {}, options)).rejects.toBeTruthy();
+ await expect(screen.findAllByAltText('alt text', {}, options)).rejects.toBeTruthy();
+
+ setTimeout(
+ () =>
+ screen.rerender(
+
+
+ ,
+ ),
+ 20,
+ );
+
+ await expect(screen.findByAltText('alt text')).resolves.toBeTruthy();
+ await expect(screen.findAllByAltText('alt text')).resolves.toHaveLength(1);
+}, 20000);
+
+test('byAltText queries support hidden option', () => {
+ render();
+
+ expect(screen.getByAltText('hidden', { includeHiddenElements: true })).toBeTruthy();
+
+ expect(screen.queryByAltText('hidden')).toBeFalsy();
+ expect(screen.queryByAltText('hidden', { includeHiddenElements: false })).toBeFalsy();
+ expect(() => screen.getByAltText('hidden', { includeHiddenElements: false }))
+ .toThrowErrorMatchingInlineSnapshot(`
+ "Unable to find an element with alt text: hidden
+
+ "
+ `);
+});
+
+test('error message renders the element tree, preserving only helpful props', async () => {
+ render();
+
+ expect(() => screen.getByAltText('FOO')).toThrowErrorMatchingInlineSnapshot(`
+ "Unable to find an element with alt text: FOO
+
+ "
+ `);
+
+ expect(() => screen.getAllByAltText('FOO')).toThrowErrorMatchingInlineSnapshot(`
+ "Unable to find an element with alt text: FOO
+
+ "
+ `);
+
+ await expect(screen.findByAltText('FOO')).rejects.toThrowErrorMatchingInlineSnapshot(`
+ "Unable to find an element with alt text: FOO
+
+ "
+ `);
+
+ await expect(screen.findAllByAltText('FOO')).rejects.toThrowErrorMatchingInlineSnapshot(`
+ "Unable to find an element with alt text: FOO
+
+ "
+ `);
+});
diff --git a/src/queries/alt-text.ts b/src/queries/alt-text.ts
new file mode 100644
index 000000000..654892771
--- /dev/null
+++ b/src/queries/alt-text.ts
@@ -0,0 +1,69 @@
+import type { ReactTestInstance } from 'react-test-renderer';
+import { findAll } from '../helpers/find-all';
+import { isHostImage } from '../helpers/host-component-names';
+import { matches, TextMatch, TextMatchOptions } from '../matches';
+import { makeQueries } from './make-queries';
+import type {
+ FindAllByQuery,
+ FindByQuery,
+ GetAllByQuery,
+ GetByQuery,
+ QueryAllByQuery,
+ QueryByQuery,
+} from './make-queries';
+import type { CommonQueryOptions } from './options';
+
+type ByAltTextOptions = CommonQueryOptions & TextMatchOptions;
+
+export function matchAltText(
+ element: ReactTestInstance,
+ expectedAltText: TextMatch,
+ options: TextMatchOptions = {},
+) {
+ const altText = element.props.alt;
+ if (altText == null) return false;
+
+ const { normalizer, exact } = options;
+
+ return matches(expectedAltText, altText, normalizer, exact);
+}
+
+const queryAllByAltText = (
+ instance: ReactTestInstance,
+): QueryAllByQuery =>
+ function queryAllByAltTextFn(text, options = {}) {
+ return findAll(instance, (node) => isHostImage(node) && matchAltText(node, text, options), {
+ ...options,
+ matchDeepestOnly: true,
+ });
+ };
+
+const getMultipleError = (text: TextMatch) =>
+ `Found multiple elements with alt text: ${String(text)}`;
+
+const getMissingError = (text: TextMatch) =>
+ `Unable to find an element with alt text: ${String(text)}`;
+
+const { getBy, getAllBy, queryBy, queryAllBy, findBy, findAllBy } = makeQueries(
+ queryAllByAltText,
+ getMissingError,
+ getMultipleError,
+);
+
+export type ByAltTextQueries = {
+ getByAltText: GetByQuery;
+ getAllByAltText: GetAllByQuery;
+ queryByAltText: QueryByQuery;
+ queryAllByAltText: QueryAllByQuery;
+ findByAltText: FindByQuery;
+ findAllByAltText: FindAllByQuery;
+};
+
+export const bindByAltTextQueries = (instance: ReactTestInstance): ByAltTextQueries => ({
+ getByAltText: getBy(instance),
+ getAllByAltText: getAllBy(instance),
+ queryByAltText: queryBy(instance),
+ queryAllByAltText: queryAllBy(instance),
+ findByAltText: findBy(instance),
+ findAllByAltText: findAllBy(instance),
+});
diff --git a/src/screen.ts b/src/screen.ts
index 1fcbd3e2a..81f03cf72 100644
--- a/src/screen.ts
+++ b/src/screen.ts
@@ -115,6 +115,12 @@ const defaultScreen: Screen = {
queryAllByText: notImplemented,
findByText: notImplemented,
findAllByText: notImplemented,
+ getByAltText: notImplemented,
+ getAllByAltText: notImplemented,
+ queryByAltText: notImplemented,
+ queryAllByAltText: notImplemented,
+ findByAltText: notImplemented,
+ findAllByAltText: notImplemented,
};
export let screen: Screen = defaultScreen;
diff --git a/src/within.ts b/src/within.ts
index 59db8bf1a..8c64482bf 100644
--- a/src/within.ts
+++ b/src/within.ts
@@ -8,6 +8,7 @@ import { bindByHintTextQueries } from './queries/hint-text';
import { bindByRoleQueries } from './queries/role';
import { bindByA11yStateQueries } from './queries/accessibility-state';
import { bindByA11yValueQueries } from './queries/accessibility-value';
+import { bindByAltTextQueries } from './queries/alt-text';
import { bindUnsafeByTypeQueries } from './queries/unsafe-type';
import { bindUnsafeByPropsQueries } from './queries/unsafe-props';
@@ -22,6 +23,7 @@ export function within(instance: ReactTestInstance) {
...bindByRoleQueries(instance),
...bindByA11yStateQueries(instance),
...bindByA11yValueQueries(instance),
+ ...bindByAltTextQueries(instance),
...bindUnsafeByTypeQueries(instance),
...bindUnsafeByPropsQueries(instance),
};