Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions docs/testing/test-harness.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<Text content="Menu" inspector-data="{testId: 'menu-title'}" />
<Element inspector-data="{role: 'menu-item'}" />
```

```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()`.
Expand Down
7 changes: 6 additions & 1 deletion src/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions src/testing/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<SnapshotNode | ComponentSnapshotNode>
setProps(props: Record<string, any>): void
setState(state: Record<string, any>): void
focus(event?: KeyboardEvent): Promise<any>
Expand Down
52 changes: 51 additions & 1 deletion src/testing/renderComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -32,6 +34,7 @@ const renderComponent = (Component, options = {}) => {
const originalPlatform = platform

Settings.set(options.settings || {})
setDevMode(true)
configurePlatform(() => nodePlatform())
initLog()
initKeyMap()
Expand All @@ -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++) {
Expand Down Expand Up @@ -107,6 +131,8 @@ const renderComponent = (Component, options = {}) => {
component,
root,
snapshot,
findByData,
findAllByData,
setProps,
setState,
focus,
Expand Down Expand Up @@ -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
Expand All @@ -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', {
Expand Down
33 changes: 33 additions & 0 deletions src/testing/renderComponent.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: `
<Element inspector-data="{testId: 'menu', role: 'menu'}">
<Text content="Menu" inspector-data="{testId: 'menu-title'}" />
<Element inspector-data="{role: 'menu-item'}" />
<Element inspector-data="{role: 'menu-item'}" />
</Element>
`,
})

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: `
Expand Down Expand Up @@ -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',
Expand Down
Loading