diff --git a/docs/testing/test-harness.md b/docs/testing/test-harness.md index 365dc4d3..42f09837 100644 --- a/docs/testing/test-harness.md +++ b/docs/testing/test-harness.md @@ -46,6 +46,23 @@ The snapshot contains: Nested Components are included in the `tree` as Component snapshots as well. +## Finding Nodes by Inspector Data + +Use `findByData()` and `findAllByData()` to find nodes with matching `inspector-data`. + +```xml + + +``` + +```js +const title = fixture.findByData('testId', 'menu-title') +const items = fixture.findAllByData('role', 'menu-item') +``` + +`findByData(key, value)` returns the first matching node or `null`. +`findAllByData(key, value)` returns all matching nodes, or an empty array when nothing matches. + ## Updating Props and State Props can be updated with `setProps()`. diff --git a/src/component.js b/src/component.js index 71fbdd43..60648b20 100644 --- a/src/component.js +++ b/src/component.js @@ -38,6 +38,11 @@ import { plugins } from './plugin.js' // object to store global components let globalComponents +let devMode = false + +export const setDevMode = (enabled) => { + devMode = enabled === true +} const required = (name) => { throw new Error(`Parameter ${name} is required`) @@ -493,7 +498,7 @@ const Component = (name = required('name'), config = required('config')) => { // one time code generation (only if precompilation is turned off) if (config.code === undefined) { Log.debug(`Generating code for ${name} component`) - config.code = codegenerator.call(config, parser(config.template, name)) + config.code = codegenerator.call(config, parser(config.template, name), devMode) } // create an instance of the component, using base as the prototype (which contains Base) diff --git a/src/testing/index.d.ts b/src/testing/index.d.ts index 3f9f9a38..c392e2ad 100644 --- a/src/testing/index.d.ts +++ b/src/testing/index.d.ts @@ -43,6 +43,8 @@ export interface RenderComponentFixture { component: any root: any snapshot(): SnapshotNode | ComponentSnapshotNode + findByData(key: string, value: any): SnapshotNode | ComponentSnapshotNode | null + findAllByData(key: string, value: any): Array setProps(props: Record): void setState(state: Record): void focus(event?: KeyboardEvent): Promise diff --git a/src/testing/renderComponent.js b/src/testing/renderComponent.js index b0b585e5..5ffe39a7 100644 --- a/src/testing/renderComponent.js +++ b/src/testing/renderComponent.js @@ -17,12 +17,14 @@ import Settings from '../settings.js' import { renderer, stage } from '../launch.js' +import { setDevMode } from '../component.js' import symbols from '../lib/symbols.js' import { initLog } from '../lib/log.js' import { getRaw } from '../lib/reactivity/reactive.js' import Focus from '../focus/focus.js' import { initKeyMap, keyMap } from '../application.js' import { configurePlatform, platform } from '../platform.js' +import { isObjectString, parseToObject } from '../lib/utils.js' import nodePlatform from './nodePlatform.js' let nodeId = 0 @@ -32,6 +34,7 @@ const renderComponent = (Component, options = {}) => { const originalPlatform = platform Settings.set(options.settings || {}) + setDevMode(true) configurePlatform(() => nodePlatform()) initLog() initKeyMap() @@ -58,6 +61,27 @@ const renderComponent = (Component, options = {}) => { return componentSnapshot(component, holderMap) } + const findAllByData = (key, value) => { + const matches = [] + visitSnapshot(snapshot(), (node) => { + if (node.attributes && node.attributes.data && node.attributes.data[key] === value) { + matches.push(node) + } + }) + return matches + } + + const findByData = (key, value) => { + let match = null + visitSnapshot(snapshot(), (node) => { + if (node.attributes && node.attributes.data && node.attributes.data[key] === value) { + match = node + return true + } + }) + return match + } + const setProps = (props = {}) => { const keys = Object.keys(props) for (let i = 0; i < keys.length; i++) { @@ -107,6 +131,8 @@ const renderComponent = (Component, options = {}) => { component, root, snapshot, + findByData, + findAllByData, setProps, setState, focus, @@ -289,10 +315,21 @@ const createElement = ({ parent } = {}) => { } }, set(key, value) { + if (key === 'inspector-data') { + this.setInspectorMetadata(value) + return + } this.attributes[key] = value this.node[key] = value }, - setInspectorMetadata() {}, + setInspectorMetadata(data) { + if (isObjectString(data) === true) { + data = parseToObject(data) + } + if (data === null || typeof data !== 'object') return + this.attributes.data = { ...(this.attributes.data || {}), ...data } + this.node.data = { ...(this.node.data || {}), ...data } + }, destroy() { this.eol = true this.children.length = 0 @@ -314,6 +351,19 @@ const appendToParent = (element, parent) => { } } +const visitSnapshot = (node, visit) => { + if (node === undefined || node === null) return false + if (visit(node) === true) return true + if (node.type === 'Component') { + return visitSnapshot(node.tree, visit) + } + const children = node.children || [] + for (let i = 0; i < children.length; i++) { + if (visitSnapshot(children[i], visit) === true) return true + } + return false +} + const createKeyboardEvent = (key, init = {}) => { const keyCode = init.keyCode || keyCodeFromKey(key) return platform.createKeyboardEvent('keydown', { diff --git a/src/testing/renderComponent.test.js b/src/testing/renderComponent.test.js index 9a459df8..d5bfb0f1 100644 --- a/src/testing/renderComponent.test.js +++ b/src/testing/renderComponent.test.js @@ -191,6 +191,36 @@ test('renderComponent setState updates state and reactive attributes in snapshot assert.end() }) +test('renderComponent finds nodes by inspector data', (assert) => { + const Menu = Component('InspectableMenu', { + template: ` + + + + + + `, + }) + + const fixture = renderComponent(Menu) + + assert.equal( + fixture.findByData('testId', 'menu-title').attributes.content, + 'Menu', + 'findByData should return the first node with matching inspector data' + ) + assert.equal( + fixture.findAllByData('role', 'menu-item').length, + 2, + 'findAllByData should return all matching nodes' + ) + assert.equal(fixture.findByData('testId', 'missing'), null, 'findByData should return null') + assert.deepEqual(fixture.findAllByData('role', 'missing'), [], 'findAllByData should return []') + + fixture.destroy() + assert.end() +}) + test('renderComponent snapshots nested component attributes and props separately', (assert) => { const Card = Component('Card', { template: ` @@ -263,6 +293,9 @@ test('renderComponent snapshots nested component attributes and props separately attributes: { x: 100, y: 20, + data: { + 'blits-componentType': 'Card', + }, }, props: { title: 'Dune',