Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(engine-core): fix warnings for invalid keys #4097

Merged
merged 10 commits into from
Mar 26, 2024
16 changes: 10 additions & 6 deletions packages/@lwc/engine-core/src/framework/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
isTrue,
isUndefined,
StringReplace,
StringToLowerCase,
toString,
} from '@lwc/shared';

Expand All @@ -35,7 +36,9 @@ import { RenderMode, ShadowMode, SlotSet, VM } from './vm';
import { LightningElementConstructor } from './base-lightning-element';
import { markAsDynamicChildren } from './rendering';
import {
isVBaseElement,
isVScopedSlotFragment,
isVStatic,
Key,
VComment,
VCustomElement,
Expand All @@ -47,11 +50,9 @@ import {
VNodeType,
VScopedSlotFragment,
VStatic,
VText,
VStaticPart,
VStaticPartData,
isVBaseElement,
isVStatic,
VText,
} from './vnodes';
import { getComponentRegisteredName } from './component';

Expand Down Expand Up @@ -432,15 +433,18 @@ function i(
if (process.env.NODE_ENV !== 'production') {
const vnodes = isArray(vnode) ? vnode : [vnode];
forEach.call(vnodes, (childVnode: VNode | null) => {
if (!isNull(childVnode) && isObject(childVnode) && !isUndefined(childVnode.sel)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

childVnode.sel is just a sniff test for VElement or VCustomElement since those are the only types that can have a sel. I prefer to be explicit and just check for the childVnode.type instead.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we need the check on isObject since vnodes should always contain a vnode.

It doesn't break any karma tests when it's removed but I'm not sure if there's any edge cases I'm not thinking of.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're right. It's not necessary.

// Check that the child vnode is either an element or VStatic
if (!isNull(childVnode) && (isVBaseElement(childVnode) || isVStatic(childVnode))) {
const { key } = childVnode;
const tagName =
childVnode.sel ?? StringToLowerCase.call(childVnode.fragment.tagName);
if (isString(key) || isNumber(key)) {
if (keyMap[key] === 1 && isUndefined(iterationError)) {
iterationError = `Duplicated "key" attribute value for "<${childVnode.sel}>" in ${vmBeingRendered} for item number ${j}. A key with value "${childVnode.key}" appears more than once in the iteration. Key values must be unique numbers or strings.`;
iterationError = `Duplicated "key" attribute value for "<${tagName}>" in ${vmBeingRendered} for item number ${j}. A key with value "${childVnode.key}" appears more than once in the iteration. Key values must be unique numbers or strings.`;
}
keyMap[key] = 1;
} else if (isUndefined(iterationError)) {
iterationError = `Invalid "key" attribute value in "<${childVnode.sel}>" in ${vmBeingRendered} for item number ${j}. Set a unique "key" value on all iterated child elements.`;
iterationError = `Invalid "key" attribute value in "<${tagName}>" in ${vmBeingRendered} for item number ${j}. Set a unique "key" value on all iterated child elements.`;
}
}
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { createElement } from 'lwc';
import XTest from 'x/test';
import XTestStatic from 'x/testStatic';
import XTestCustomElement from 'x/testCustomElement';
import ArrayNullPrototype from 'x/arrayNullPrototype';

function testForEach(type, obj) {
Expand Down Expand Up @@ -64,7 +66,7 @@ it('should throw an error when the passing a non iterable', () => {

// TODO [#1283]: Improve this error message. The vm should not be exposed and the message is not helpful.
expect(() => document.body.appendChild(elm)).toThrowCallbackReactionError(
/Invalid template iteration for value `\[object (ProxyObject|Object)\]` in \[object:vm Test \(\d+\)\]. It must be an array-like object and not `null` nor `undefined`.|is not a function/
/Invalid template iteration for value `\[object (ProxyObject|Object)]` in \[object:vm Test \(\d+\)]\. It must be an array-like object and not `null` nor `undefined`\.|is not a function/
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary escapes in regexes, and some missing escapes (like for the .).

);
});

Expand All @@ -75,13 +77,47 @@ it('should render an array of objects with null prototype', () => {
expect(elm.shadowRoot.querySelector('span').textContent).toBe('text');
});

it('logs an error when passing an invalid key', () => {
const elm = createElement('x-test', { is: XTest });
elm.items = [{ key: null, value: 'one' }];
const scenarios = [
{
testName: 'dynamic text node',
Ctor: XTest,
tagName: 'x-test',
},
{
testName: 'static text node',
Ctor: XTestStatic,
tagName: 'x-test-static',
},
{
testName: 'custom element',
Ctor: XTestCustomElement,
tagName: 'x-test-custom-element',
},
];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Testing the three scenarios we care about: Elements, CustomElements, and VStatics.

scenarios.forEach(({ testName, Ctor, tagName }) => {
describe(testName, () => {
it('logs an error when passing an invalid key', () => {
const elm = createElement(tagName, { is: Ctor });
elm.items = [{ key: null, value: 'one' }];

// TODO [#1283]: Improve this error message. The vm should not be exposed and the message is not helpful.
expect(() => document.body.appendChild(elm)).toLogErrorDev([
/Invalid key value "null" in \[object:vm Test \(\d+\)\]. Key must be a string or number./,
/Invalid "key" attribute value in "<li>"/,
]);
// TODO [#1283]: Improve this error message. The vm should not be exposed and the message is not helpful.
expect(() => document.body.appendChild(elm)).toLogErrorDev([
/Invalid key value "null" in \[object:vm (TestStatic|TestCustomElement|Test) \(\d+\)]. Key must be a string or number\./,
/Invalid "key" attribute value in "<(li|x-custom)>"/,
]);
});

it('logs an error when passing a duplicate key', () => {
const elm = createElement(tagName, { is: Ctor });
elm.items = [
{ key: 'xyz', value: 'one' },
{ key: 'xyz', value: 'two' },
];

// TODO [#1283]: Improve this error message. The vm should not be exposed and the message is not helpful.
expect(() => document.body.appendChild(elm)).toLogErrorDev(
/Duplicated "key" attribute value for "<(li|x-custom)>" in \[object:vm (TestStatic|TestCustomElement|Test) \(\d+\)] for item number 1\. A key with value "\d:xyz" appears more than once in the iteration\. Key values must be unique numbers or strings\./
);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We previously didn't have any tests for this "Duplicated key" error message.

});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { LightningElement } from 'lwc';

export default class Custom extends LightningElement {}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<template>
<ul>
<template for:each={items} for:item="item">
<li key={item.key}>{item.value}</li>
<li key={item.key}>
<!-- putting an lwc:if in here so that the LI will always be VElement instead of VStatic -->
<template lwc:if={item.value}>{item.value}</template>
</li>
</template>
</ul>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<template>
<ul>
<template for:each={items} for:item="item">
<x-custom key={item.key}></x-custom>
</template>
</ul>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { LightningElement, api } from 'lwc';

export default class TestCustomElement extends LightningElement {
@api items = [];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<template>
<ul>
<template for:each={items} for:item="item">
<li key={item.key}></li>
</template>
</ul>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { LightningElement, api } from 'lwc';

export default class TestStatic extends LightningElement {
@api items = [];
}
Loading