Skip to content

Commit 4223dac

Browse files
committed
test: warn when fallthrough fails on non-single-root
1 parent e654550 commit 4223dac

File tree

4 files changed

+135
-143
lines changed

4 files changed

+135
-143
lines changed

packages/runtime-core/src/componentRenderUtils.ts

Lines changed: 41 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -169,40 +169,7 @@ export function renderComponentRoot(
169169
}
170170
root = cloneVNode(root, fallthroughAttrs, false, true)
171171
} else if (__DEV__ && !accessedAttrs && root.type !== Comment) {
172-
const allAttrs = Object.keys(attrs)
173-
const eventAttrs: string[] = []
174-
const extraAttrs: string[] = []
175-
for (let i = 0, l = allAttrs.length; i < l; i++) {
176-
const key = allAttrs[i]
177-
if (isOn(key)) {
178-
// ignore v-model handlers when they fail to fallthrough
179-
if (!isModelListener(key)) {
180-
// remove `on`, lowercase first letter to reflect event casing
181-
// accurately
182-
eventAttrs.push(key[2].toLowerCase() + key.slice(3))
183-
}
184-
} else {
185-
extraAttrs.push(key)
186-
}
187-
}
188-
if (extraAttrs.length) {
189-
warn(
190-
`Extraneous non-props attributes (` +
191-
`${extraAttrs.join(', ')}) ` +
192-
`were passed to component but could not be automatically inherited ` +
193-
`because component renders fragment or text or teleport root nodes.`,
194-
)
195-
}
196-
if (eventAttrs.length) {
197-
warn(
198-
`Extraneous non-emits event listeners (` +
199-
`${eventAttrs.join(', ')}) ` +
200-
`were passed to component but could not be automatically inherited ` +
201-
`because component renders fragment or text root nodes. ` +
202-
`If the listener is intended to be a component custom event listener only, ` +
203-
`declare it using the "emits" option.`,
204-
)
205-
}
172+
warnExtraneousAttributes(attrs)
206173
}
207174
}
208175
}
@@ -302,6 +269,46 @@ const getChildRoot = (vnode: VNode): [VNode, SetRootFn] => {
302269
return [normalizeVNode(childRoot), setRoot]
303270
}
304271

272+
/**
273+
* Dev only
274+
*/
275+
export function warnExtraneousAttributes(attrs: Record<string, any>): void {
276+
const allAttrs = Object.keys(attrs)
277+
const eventAttrs: string[] = []
278+
const extraAttrs: string[] = []
279+
for (let i = 0, l = allAttrs.length; i < l; i++) {
280+
const key = allAttrs[i]
281+
if (isOn(key)) {
282+
// ignore v-model handlers when they fail to fallthrough
283+
if (!isModelListener(key)) {
284+
// remove `on`, lowercase first letter to reflect event casing
285+
// accurately
286+
eventAttrs.push(key[2].toLowerCase() + key.slice(3))
287+
}
288+
} else {
289+
extraAttrs.push(key)
290+
}
291+
}
292+
if (extraAttrs.length) {
293+
warn(
294+
`Extraneous non-props attributes (` +
295+
`${extraAttrs.join(', ')}) ` +
296+
`were passed to component but could not be automatically inherited ` +
297+
`because component renders fragment or text or teleport root nodes.`,
298+
)
299+
}
300+
if (eventAttrs.length) {
301+
warn(
302+
`Extraneous non-emits event listeners (` +
303+
`${eventAttrs.join(', ')}) ` +
304+
`were passed to component but could not be automatically inherited ` +
305+
`because component renders fragment or text root nodes. ` +
306+
`If the listener is intended to be a component custom event listener only, ` +
307+
`declare it using the "emits" option.`,
308+
)
309+
}
310+
}
311+
305312
export function filterSingleRoot(
306313
children: VNodeArrayChildren,
307314
recurse = true,

packages/runtime-core/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,3 +674,8 @@ export {
674674
* @internal
675675
*/
676676
export type { GenericComponent } from './component'
677+
678+
/**
679+
* @internal
680+
*/
681+
export { warnExtraneousAttributes } from './componentRenderUtils'

packages/runtime-vapor/__tests__/componentAttrs.spec.ts

Lines changed: 86 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { type Ref, nextTick, onUpdated, ref } from '@vue/runtime-dom'
22
import {
3+
VaporTeleport,
34
createComponent,
45
createDynamicComponent,
56
createIf,
@@ -256,130 +257,106 @@ describe('attribute fallthrough', () => {
256257
expect(node.hasAttribute('foo')).toBe(false)
257258
})
258259

259-
// it('should not fallthrough with inheritAttrs: false', () => {
260-
// const Parent = {
261-
// render() {
262-
// return h(Child, { foo: 1, class: 'parent' })
263-
// },
264-
// }
265-
266-
// const Child = defineVaporComponent({
267-
// props: ['foo'],
268-
// inheritAttrs: false,
269-
// render() {
270-
// return h('div', this.foo)
271-
// },
272-
// })
273-
274-
// const root = document.createElement('div')
275-
// document.body.appendChild(root)
276-
// render(h(Parent), root)
277-
278-
// // should not contain class
279-
// expect(root.innerHTML).toMatch(`<div>1</div>`)
280-
// })
281-
282-
// // #3741
283-
// it('should not fallthrough with inheritAttrs: false from mixins', () => {
284-
// const Parent = {
285-
// render() {
286-
// return h(Child, { foo: 1, class: 'parent' })
287-
// },
288-
// }
289-
290-
// const mixin = {
291-
// inheritAttrs: false,
292-
// }
260+
it('should not fallthrough with inheritAttrs: false', () => {
261+
const Parent = defineVaporComponent({
262+
setup() {
263+
return createComponent(Child, { foo: () => 1, class: () => 'parent' })
264+
},
265+
})
293266

294-
// const Child = defineVaporComponent({
295-
// mixins: [mixin],
296-
// props: ['foo'],
297-
// render() {
298-
// return h('div', this.foo)
299-
// },
300-
// })
267+
const Child = defineVaporComponent({
268+
props: ['foo'],
269+
inheritAttrs: false,
270+
setup(props) {
271+
const n0 = template('<div></div>', true)() as Element
272+
renderEffect(() => setElementText(n0, props.foo))
273+
return n0
274+
},
275+
})
301276

302-
// const root = document.createElement('div')
303-
// document.body.appendChild(root)
304-
// render(h(Parent), root)
277+
const { html } = define(Parent).render()
305278

306-
// // should not contain class
307-
// expect(root.innerHTML).toMatch(`<div>1</div>`)
308-
// })
279+
// should not contain class
280+
expect(html()).toMatch(`<div>1</div>`)
281+
})
309282

310-
// it('explicit spreading with inheritAttrs: false', () => {
311-
// const Parent = {
312-
// render() {
313-
// return h(Child, { foo: 1, class: 'parent' })
314-
// },
315-
// }
283+
it('explicit spreading with inheritAttrs: false', () => {
284+
const Parent = defineVaporComponent({
285+
setup() {
286+
return createComponent(Child, { foo: () => 1, class: () => 'parent' })
287+
},
288+
})
316289

317-
// const Child = defineVaporComponent({
318-
// props: ['foo'],
319-
// inheritAttrs: false,
320-
// render() {
321-
// return h(
322-
// 'div',
323-
// mergeProps(
324-
// {
325-
// class: 'child',
326-
// },
327-
// this.$attrs,
328-
// ),
329-
// this.foo,
330-
// )
331-
// },
332-
// })
290+
const Child = defineVaporComponent({
291+
props: ['foo'],
292+
inheritAttrs: false,
293+
setup(props, { attrs }) {
294+
const n0 = template('<div>', true)() as Element
295+
renderEffect(() => {
296+
setElementText(n0, props.foo)
297+
setDynamicProps(n0, [{ class: 'child' }, attrs])
298+
})
299+
return n0
300+
},
301+
})
333302

334-
// const root = document.createElement('div')
335-
// document.body.appendChild(root)
336-
// render(h(Parent), root)
303+
const { html } = define(Parent).render()
337304

338-
// // should merge parent/child classes
339-
// expect(root.innerHTML).toMatch(`<div class="child parent">1</div>`)
340-
// })
305+
// should merge parent/child classes
306+
expect(html()).toMatch(`<div class="child parent">1</div>`)
307+
})
341308

342-
// it('should warn when fallthrough fails on non-single-root', () => {
343-
// const Parent = {
344-
// render() {
345-
// return h(Child, { foo: 1, class: 'parent', onBar: () => {} })
346-
// },
347-
// }
309+
it('should warn when fallthrough fails on non-single-root', () => {
310+
const Parent = {
311+
setup() {
312+
return createComponent(Child, {
313+
foo: () => 1,
314+
class: () => 'parent',
315+
onBar: () => () => {},
316+
})
317+
},
318+
}
348319

349-
// const Child = defineVaporComponent({
350-
// props: ['foo'],
351-
// render() {
352-
// return [h('div'), h('div')]
353-
// },
354-
// })
320+
const Child = defineVaporComponent({
321+
props: ['foo'],
322+
render() {
323+
return [template('<div></div>')(), template('<div></div>')()]
324+
},
325+
})
355326

356-
// const root = document.createElement('div')
357-
// document.body.appendChild(root)
358-
// render(h(Parent), root)
327+
define(Parent).render()
359328

360-
// expect(`Extraneous non-props attributes (class)`).toHaveBeenWarned()
361-
// expect(`Extraneous non-emits event listeners`).toHaveBeenWarned()
362-
// })
329+
expect(`Extraneous non-props attributes (class)`).toHaveBeenWarned()
330+
expect(`Extraneous non-emits event listeners`).toHaveBeenWarned()
331+
})
363332

364-
// it('should warn when fallthrough fails on teleport root node', () => {
365-
// const Parent = {
366-
// render() {
367-
// return h(Child, { class: 'parent' })
368-
// },
369-
// }
370-
// const root = document.createElement('div')
333+
it.todo('should warn when fallthrough fails on teleport root node', () => {
334+
const Parent = {
335+
render() {
336+
return createComponent(Child, { class: () => 'parent' })
337+
},
338+
}
371339

372-
// const Child = defineVaporComponent({
373-
// render() {
374-
// return h(Teleport, { to: root }, h('div'))
375-
// },
376-
// })
340+
const root = document.createElement('div')
341+
const Child = defineVaporComponent({
342+
render() {
343+
// return h(Teleport, { to: root }, h('div'))
344+
return createComponent(
345+
VaporTeleport,
346+
{ to: () => root },
347+
{
348+
default: () => template('<div></div>')(),
349+
},
350+
)
351+
},
352+
})
377353

378-
// document.body.appendChild(root)
379-
// render(h(Parent), root)
354+
document.body.appendChild(root)
355+
// render(h(Parent), root)
356+
define(Parent).render()
380357

381-
// expect(`Extraneous non-props attributes (class)`).toHaveBeenWarned()
382-
// })
358+
expect(`Extraneous non-props attributes (class)`).toHaveBeenWarned()
359+
})
383360

384361
// it('should dedupe same listeners when $attrs is used during render', () => {
385362
// const click = vi.fn()

packages/runtime-vapor/src/component.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
startMeasure,
2828
unregisterHMR,
2929
warn,
30+
warnExtraneousAttributes,
3031
} from '@vue/runtime-dom'
3132
import {
3233
type Block,
@@ -425,6 +426,8 @@ export function applyFallthroughProps(
425426
isApplyingFallthroughProps = true
426427
setDynamicProps(el, [attrs])
427428
isApplyingFallthroughProps = false
429+
} else if (__DEV__) {
430+
warnExtraneousAttributes(attrs)
428431
}
429432
}
430433

0 commit comments

Comments
 (0)