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',