diff --git a/src/index.ts b/src/index.ts index aa20066..e6d0288 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,3 @@ export * from './queries'; export * from './event'; +export * from './query-helpers'; diff --git a/src/queries/text.spec.ts b/src/queries/text.spec.ts index 5eea3b2..506f79d 100644 --- a/src/queries/text.spec.ts +++ b/src/queries/text.spec.ts @@ -4,7 +4,6 @@ import { findByText, getAllByText, getByText, - getMultipleElementsFoundError, queryAllByText, queryByText, } from './text'; @@ -16,6 +15,7 @@ import { } from '@babylonjs/gui'; import { BabylonContainer } from './utils'; import { getElementError } from '@testing-library/dom'; +import { getMultipleElementsFoundError } from '../query-helpers'; describe('text query', () => { let scene: Scene, @@ -232,7 +232,7 @@ describe('text query', () => { expect(() => singleText(container, 'Hello World!')).toThrow( new Error( - `Found multiple elements matching Hello World!\n\n(If this is intentional, then use the \`*AllBy*\` variant of the query (like \`queryAllByText\`, \`getAllByText\`, or \`findAllByText\`)). Container: ${container}` + `Found multiple elements with the text: Hello World!\n\n(If this is intentional, then use the \`*AllBy*\` variant of the query (like \`queryAllByText\`, \`getAllByText\`, or \`findAllByText\`)). Container: ${container}` ) ); } @@ -276,7 +276,7 @@ describe('text query', () => { ).rejects.toEqual( getElementError( getMultipleElementsFoundError( - 'Found multiple elements matching Hello World!', + 'Found multiple elements with the text: Hello World!', container ).message, document.firstElementChild as HTMLElement diff --git a/src/queries/text.ts b/src/queries/text.ts index cfd867d..ea34f7a 100644 --- a/src/queries/text.ts +++ b/src/queries/text.ts @@ -2,17 +2,13 @@ import { Control, TextBlock } from '@babylonjs/gui'; import { BabylonContainer, findAllMatchingDescendants } from './utils'; import { buildQueries } from '../query-helpers'; -export function getMultipleElementsFoundError( - message: string, - container: BabylonContainer -) { - return new Error( - `${message}\n\n(If this is intentional, then use the \`*AllBy*\` variant of the query (like \`queryAllByText\`, \`getAllByText\`, or \`findAllByText\`)). Container: ${container}` - ); -} +const getMultipleError = (_c: BabylonContainer, text: string) => + `Found multiple elements with the text: ${text}`; const getMissingError = (message: string, container: BabylonContainer) => { - return new Error(`${message}. Container: ${container}`); + return new Error( + `Failed to find an element matching: ${message}. Container: ${container}` + ); }; const queryAllByText = ( @@ -23,13 +19,7 @@ const queryAllByText = ( return control instanceof TextBlock && control.text === text; }; - const baseArray: Control[] = []; - - if (container instanceof Control && matcher(container)) { - baseArray.push(container); - } - - return [...baseArray, ...findAllMatchingDescendants(container, matcher)]; + return findAllMatchingDescendants(container, matcher); }; const { @@ -38,11 +28,7 @@ const { getBy: getByText, findAllBy: findAllByText, findBy: findByText, -} = buildQueries( - queryAllByText, - getMultipleElementsFoundError, - getMissingError -); +} = buildQueries(queryAllByText, getMultipleError, getMissingError); export { queryAllByText, diff --git a/src/queries/utils.ts b/src/queries/utils.ts index 8f13abf..6d1b76d 100644 --- a/src/queries/utils.ts +++ b/src/queries/utils.ts @@ -7,7 +7,15 @@ const findAllMatchingInControl = ( container: Control, matcher: (control: Control) => boolean ): Control[] => { - return container.getDescendants().filter((control) => matcher(control)); + const controls = []; + if (matcher(container)) { + controls.push(container); + } + + return [ + ...controls, + ...container.getDescendants().filter((control) => matcher(control)), + ]; }; const findAllMatchingInTexture = ( container: AdvancedDynamicTexture, diff --git a/src/query-helpers.spec.ts b/src/query-helpers.spec.ts new file mode 100644 index 0000000..2f27334 --- /dev/null +++ b/src/query-helpers.spec.ts @@ -0,0 +1,115 @@ +import { Engine, Mesh, MeshBuilder, NullEngine, Scene } from '@babylonjs/core'; + +import { + AdvancedDynamicTexture, + Control, + Grid, + TextBlock, +} from '@babylonjs/gui'; +import { BabylonContainer } from './queries/utils'; +import { + getMultipleElementsFoundError, + queryAllByAttribute, + queryByAttribute, +} from './query-helpers'; + +describe('query-helpers', () => { + let scene: Scene, + engine: Engine, + texture: AdvancedDynamicTexture, + containerControl: Grid, + expectedControl: Control, + uiPlane: Mesh; + + beforeAll(() => { + engine = new NullEngine(); + }); + + beforeEach(() => { + scene = new Scene(engine); + + uiPlane = MeshBuilder.CreatePlane('container'); + texture = AdvancedDynamicTexture.CreateForMesh(uiPlane); + + containerControl = new Grid('container'); + containerControl.addColumnDefinition(1); + containerControl.addColumnDefinition(1); + texture.addControl(containerControl); + + expectedControl = new TextBlock('text', 'Hello World!'); + containerControl.addControl(expectedControl, 0, 0); + }); + + afterEach(() => { + texture.dispose(); + uiPlane.material?.dispose(); + uiPlane.dispose(); + scene.dispose(); + }); + + afterAll(() => { + engine.dispose(); + }); + + const scenarios: [string, () => BabylonContainer][] = [ + ['a scene', () => scene], + ['a texture', () => texture], + ['a control', () => containerControl], + ]; + + describe.each(scenarios)( + 'query*ByAttribute within %s', + (_label, getContainer) => { + it('should queryAllByAttribute', () => { + const container = getContainer(); + + const controls = queryAllByAttribute( + 'width', + container, + '100%' + ); + + expect(controls).toHaveLength(2); + expect(controls).toEqual([containerControl, expectedControl]); + }); + + it('should error when queryByAttribute finds multiple elements', () => { + const container = getContainer(); + + expect(() => { + queryByAttribute('width', container, '100%'); + }).toThrow( + getMultipleElementsFoundError( + 'Found multiple elements by [width=100%]', + container + ) + ); + }); + } + ); + + describe('query*ByAttribute within itself', () => { + it('should queryAllByAttribute', () => { + const controls = queryAllByAttribute( + 'width', + expectedControl, + '100%' + ); + + expect(controls).toHaveLength(1); + expect(controls).toEqual([expectedControl]); + }); + + it('should return a single element', () => { + const control = queryByAttribute('width', expectedControl, '100%'); + + expect(control).toEqual(expectedControl); + }); + + it('should return null if no element is found', () => { + const control = queryByAttribute('width', expectedControl, '99%%'); + + expect(control).toEqual(null); + }); + }); +}); diff --git a/src/query-helpers.ts b/src/query-helpers.ts index 135941c..0cb521f 100644 --- a/src/query-helpers.ts +++ b/src/query-helpers.ts @@ -1,11 +1,53 @@ import { waitFor, waitForOptions } from '@testing-library/dom'; +import { BabylonContainer, findAllMatchingDescendants } from './queries/utils'; + +export function getMultipleElementsFoundError( + message: string, + container: ContainerType +) { + return new Error( + `${message}\n\n(If this is intentional, then use the \`*AllBy*\` variant of the query (like \`queryAllByText\`, \`getAllByText\`, or \`findAllByText\`)). Container: ${container}` + ); +} + +export function queryAllByAttribute( + attribute: string, + container: BabylonContainer, + value: AttributeType +) { + return findAllMatchingDescendants( + container, + (control) => control[attribute] === value + ); +} + +export function queryByAttribute( + attribute: string, + container: BabylonContainer, + value: AttributeType +) { + const controls = queryAllByAttribute(attribute, container, value); + + if (controls.length === 0) { + return null; + } + + if (controls.length > 1) { + throw getMultipleElementsFoundError( + `Found multiple elements by [${attribute}=${value}]`, + container + ); + } + + return controls[0]; +} export function buildQueries( queryAllBy: ( container: ContainerType, matcher: MatcherType ) => ResultType[], - getMultipleError: (message: string, container: ContainerType) => Error, + getMultipleError: (container: ContainerType, message: string) => string, getMissingError: (text: string, container: ContainerType) => Error ) { const queryBy = (container: ContainerType, matcher: MatcherType) => { @@ -16,8 +58,8 @@ export function buildQueries( } if (result.length > 1) { - throw getMultipleError( - `Found multiple elements matching ${matcher}`, + throw getMultipleElementsFoundError( + getMultipleError(container, `${matcher}`), container ); } @@ -28,10 +70,7 @@ export function buildQueries( const getAllBy = (container: ContainerType, matcher: MatcherType) => { const result = queryAllBy(container, matcher); if (result.length === 0) { - throw getMissingError( - `Failed to find an element matching: ${matcher}`, - container - ); + throw getMissingError(`${matcher}`, container); } return result; }; @@ -39,8 +78,8 @@ export function buildQueries( const getBy = (container: ContainerType, matcher: MatcherType) => { const result = getAllBy(container, matcher); if (result.length > 1) { - throw getMultipleError( - `Found multiple elements matching ${matcher}`, + throw getMultipleElementsFoundError( + getMultipleError(container, `${matcher}`), container ); }