mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
d546244fc3
Currently when checking for duplicate keys, we insert the value of the key as is in a set then check for unicity against those. When the key is an object, we check for duplicates based on object identity, whereas the keys are used by owl as strings, and so using objects can cause duplicate key errors that do not throw correctly but crash in the owl internals. This commit fixes that by making the duplicate checking code serialize the key to string before insertion and when comparing against existing keys.
363 lines
10 KiB
TypeScript
363 lines
10 KiB
TypeScript
import { App, Component, mount, onMounted, useState, xml } from "../../src/index";
|
|
import {
|
|
makeTestFixture,
|
|
nextAppError,
|
|
nextTick,
|
|
snapshotEverything,
|
|
useLogLifecycle,
|
|
} from "../helpers";
|
|
|
|
snapshotEverything();
|
|
|
|
let originalconsoleWarn = console.warn;
|
|
let mockConsoleWarn: any;
|
|
|
|
let fixture: HTMLElement;
|
|
|
|
beforeEach(() => {
|
|
fixture = makeTestFixture();
|
|
mockConsoleWarn = jest.fn(() => {});
|
|
console.warn = mockConsoleWarn;
|
|
});
|
|
|
|
afterEach(() => {
|
|
console.warn = originalconsoleWarn;
|
|
});
|
|
|
|
describe("list of components", () => {
|
|
test("simple list", async () => {
|
|
class Child extends Component {
|
|
static template = xml`<span><t t-esc="props.value"/></span>`;
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<t t-foreach="state.elems" t-as="elem" t-key="elem.id">
|
|
<Child value="elem.value"/>
|
|
</t>`;
|
|
static components = { Child };
|
|
|
|
state = useState({
|
|
elems: [
|
|
{ id: 1, value: "a" },
|
|
{ id: 2, value: "b" },
|
|
],
|
|
});
|
|
}
|
|
|
|
const parent = await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe("<span>a</span><span>b</span>");
|
|
parent.state.elems.push({ id: 4, value: "d" });
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<span>a</span><span>b</span><span>d</span>");
|
|
|
|
parent.state.elems.pop();
|
|
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<span>a</span><span>b</span>");
|
|
});
|
|
|
|
test("components in a node in a t-foreach ", async () => {
|
|
class Child extends Component {
|
|
static template = xml`<div><t t-esc="props.item"/></div>`;
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<ul>
|
|
<t t-foreach="items" t-as="item" t-key="'li_'+item">
|
|
<li>
|
|
<Child item="item"/>
|
|
</li>
|
|
</t>
|
|
</ul>
|
|
</div>`;
|
|
static components = { Child };
|
|
|
|
setup() {
|
|
useLogLifecycle();
|
|
}
|
|
|
|
get items() {
|
|
return [1, 2];
|
|
}
|
|
}
|
|
|
|
await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe(
|
|
"<div><ul><li><div>1</div></li><li><div>2</div></li></ul></div>"
|
|
);
|
|
expect([
|
|
"Parent:setup",
|
|
"Parent:willStart",
|
|
"Parent:willRender",
|
|
"Child:setup",
|
|
"Child:willStart",
|
|
"Child:setup",
|
|
"Child:willStart",
|
|
"Parent:rendered",
|
|
"Child:willRender",
|
|
"Child:rendered",
|
|
"Child:willRender",
|
|
"Child:rendered",
|
|
"Child:mounted",
|
|
"Child:mounted",
|
|
"Parent:mounted",
|
|
]).toBeLogged();
|
|
});
|
|
|
|
test("reconciliation alg works for t-foreach in t-foreach", async () => {
|
|
class Child extends Component {
|
|
static template = xml`<div><t t-esc="props.blip"/></div>`;
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-foreach="state.s" t-as="section" t-key="section_index">
|
|
<t t-foreach="section.blips" t-as="blip" t-key="blip_index">
|
|
<Child blip="blip"/>
|
|
</t>
|
|
</t>
|
|
</div>`;
|
|
static components = { Child };
|
|
state = { s: [{ blips: ["a1", "a2"] }, { blips: ["b1"] }] };
|
|
}
|
|
|
|
await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe("<div><div>a1</div><div>a2</div><div>b1</div></div>");
|
|
});
|
|
|
|
test("reconciliation alg works for t-foreach in t-foreach, 2", async () => {
|
|
class Child extends Component {
|
|
static template = xml`<div><t t-esc="props.row + '_' + props.col"/></div>`;
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<p t-foreach="state.rows" t-as="row" t-key="row">
|
|
<p t-foreach="state.cols" t-as="col" t-key="col">
|
|
<Child row="row" col="col"/>
|
|
</p>
|
|
</p>
|
|
</div>`;
|
|
static components = { Child };
|
|
state = useState({ rows: [1, 2], cols: ["a", "b"] });
|
|
}
|
|
|
|
const parent = await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe(
|
|
"<div><p><p><div>1_a</div></p><p><div>1_b</div></p></p><p><p><div>2_a</div></p><p><div>2_b</div></p></p></div>"
|
|
);
|
|
parent.state.rows = [2, 1];
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe(
|
|
"<div><p><p><div>2_a</div></p><p><div>2_b</div></p></p><p><p><div>1_a</div></p><p><div>1_b</div></p></p></div>"
|
|
);
|
|
});
|
|
|
|
test("sub components rendered in a loop", async () => {
|
|
class Child extends Component {
|
|
static template = xml`<p><t t-esc="props.n"/></p>`;
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-foreach="state.numbers" t-as="number" t-key="number" >
|
|
<Child n="number"/>
|
|
</t>
|
|
</div>`;
|
|
static components = { Child };
|
|
|
|
state = useState({ numbers: [1, 2, 3] });
|
|
}
|
|
await mount(Parent, fixture);
|
|
|
|
expect(fixture.innerHTML).toBe(`<div><p>1</p><p>2</p><p>3</p></div>`);
|
|
});
|
|
|
|
test("sub components with some state rendered in a loop", async () => {
|
|
let n = 1;
|
|
|
|
class Child extends Component {
|
|
static template = xml`<p><t t-esc="state.n"/></p>`;
|
|
state: any;
|
|
setup() {
|
|
this.state = useState({ n });
|
|
n++;
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-foreach="state.numbers" t-as="number" t-key="number">
|
|
<Child/>
|
|
</t>
|
|
</div>`;
|
|
static components = { Child };
|
|
|
|
state = useState({
|
|
numbers: [1, 2, 3],
|
|
});
|
|
}
|
|
const parent = await mount(Parent, fixture);
|
|
|
|
parent.state.numbers = [1, 3];
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe(`<div><p>1</p><p>3</p></div>`);
|
|
});
|
|
|
|
test("list of sub components inside other nodes", async () => {
|
|
// this confuses the patching algorithm...
|
|
class SubComponent extends Component {
|
|
static template = xml`<span>asdf</span>`;
|
|
}
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<div t-foreach="state.blips" t-as="blip" t-key="blip.id">
|
|
<SubComponent />
|
|
</div>
|
|
</div>`;
|
|
static components = { SubComponent };
|
|
state = useState({
|
|
blips: [
|
|
{ a: "a", id: 1 },
|
|
{ b: "b", id: 2 },
|
|
{ c: "c", id: 4 },
|
|
],
|
|
});
|
|
}
|
|
const parent = await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe(
|
|
"<div><div><span>asdf</span></div><div><span>asdf</span></div><div><span>asdf</span></div></div>"
|
|
);
|
|
parent.state.blips.splice(0, 1);
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe(
|
|
"<div><div><span>asdf</span></div><div><span>asdf</span></div></div>"
|
|
);
|
|
});
|
|
|
|
test("t-foreach with t-component, and update", async () => {
|
|
class Child extends Component {
|
|
static template = xml`
|
|
<span>
|
|
<t t-esc="state.val"/>
|
|
<t t-esc="props.val"/>
|
|
</span>`;
|
|
state = useState({ val: "A" });
|
|
setup() {
|
|
onMounted(() => {
|
|
this.state.val = "B";
|
|
});
|
|
}
|
|
}
|
|
class Parent extends Component {
|
|
static components = { Child };
|
|
static template = xml`
|
|
<div>
|
|
<t t-foreach="Array(2)" t-as="n" t-key="n_index">
|
|
<Child val="n_index"/>
|
|
</t>
|
|
</div>`;
|
|
}
|
|
|
|
await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe("<div><span>A0</span><span>A1</span></div>");
|
|
|
|
await nextTick(); // wait for changes triggered in mounted to be applied
|
|
expect(fixture.innerHTML).toBe("<div><span>B0</span><span>B1</span></div>");
|
|
});
|
|
|
|
test("switch component position", async () => {
|
|
const childInstances = [];
|
|
class Child extends Component {
|
|
static template = xml`<div t-esc="props.key"></div>`;
|
|
setup() {
|
|
childInstances.push(this);
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static components = { Child };
|
|
static template = xml`<span>
|
|
<t t-foreach="clist" t-as="c" t-key="c">
|
|
<Child key="c"/>
|
|
</t>
|
|
</span>`;
|
|
|
|
clist = [1, 2];
|
|
}
|
|
|
|
const parent = await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe("<span><div>1</div><div>2</div></span>");
|
|
parent.clist = [2, 1];
|
|
parent.render();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<span><div>2</div><div>1</div></span>");
|
|
expect(childInstances.length).toBe(2);
|
|
});
|
|
|
|
test("crash on duplicate key in dev mode", async () => {
|
|
const consoleInfo = console.info;
|
|
console.info = jest.fn();
|
|
class Child extends Component {
|
|
static template = xml``;
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<t t-foreach="[1, 2]" t-as="item" t-key="'child'">
|
|
<Child/>
|
|
</t>
|
|
`;
|
|
static components = { Child };
|
|
}
|
|
|
|
const app = new App(Parent, { test: true });
|
|
const mountProm = expect(app.mount(fixture)).rejects.toThrow(
|
|
"Got duplicate key in t-foreach: child"
|
|
);
|
|
await expect(nextAppError(app)).resolves.toThrow("Got duplicate key in t-foreach: child");
|
|
await mountProm;
|
|
console.info = consoleInfo;
|
|
expect(mockConsoleWarn).toBeCalledTimes(1);
|
|
});
|
|
|
|
test("crash when using object as keys that serialize to the same string", async () => {
|
|
const consoleInfo = console.info;
|
|
console.info = jest.fn();
|
|
class Child extends Component {
|
|
static template = xml``;
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<t t-foreach="[{}, {}]" t-as="item" t-key="item">
|
|
<Child/>
|
|
</t>
|
|
`;
|
|
static components = { Child };
|
|
}
|
|
|
|
const app = new App(Parent, { test: true });
|
|
const mountProm = expect(app.mount(fixture)).rejects.toThrow(
|
|
"Got duplicate key in t-foreach: [object Object]"
|
|
);
|
|
await expect(nextAppError(app)).resolves.toThrow(
|
|
"Got duplicate key in t-foreach: [object Object]"
|
|
);
|
|
await mountProm;
|
|
console.info = consoleInfo;
|
|
expect(mockConsoleWarn).toBeCalledTimes(1);
|
|
});
|
|
});
|