mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
1ae9d514b9
With this commit, component only render child components if they have different props (shallow equality). Otherwise, we trust the reactivity system to make sure that all impacted components are updated
971 lines
27 KiB
TypeScript
971 lines
27 KiB
TypeScript
import {
|
|
App,
|
|
Component,
|
|
mount,
|
|
onError,
|
|
onMounted,
|
|
onPatched,
|
|
onWillPatch,
|
|
onWillUnmount,
|
|
useState,
|
|
} from "../../src";
|
|
import { xml } from "../../src/";
|
|
import { DEV_MSG } from "../../src/app/app";
|
|
import { elem, makeTestFixture, nextTick, snapshotEverything } from "../helpers";
|
|
|
|
let fixture: HTMLElement;
|
|
let originalconsoleWarn = console.warn;
|
|
let mockConsoleWarn: any;
|
|
const info = console.info;
|
|
|
|
function addOutsideDiv(fixture: HTMLElement): HTMLElement {
|
|
let outside = document.createElement("div");
|
|
outside.setAttribute("id", "outside");
|
|
fixture.appendChild(outside);
|
|
return outside;
|
|
}
|
|
|
|
snapshotEverything();
|
|
|
|
beforeAll(() => {
|
|
console.info = (message: any) => {
|
|
if (message === DEV_MSG()) {
|
|
return;
|
|
}
|
|
info(message);
|
|
};
|
|
});
|
|
|
|
afterAll(() => {
|
|
console.info = info;
|
|
});
|
|
|
|
beforeEach(() => {
|
|
fixture = makeTestFixture();
|
|
mockConsoleWarn = jest.fn(() => {});
|
|
console.warn = mockConsoleWarn;
|
|
});
|
|
|
|
afterEach(() => {
|
|
console.warn = originalconsoleWarn;
|
|
});
|
|
|
|
describe("Portal", () => {
|
|
test("basic use of portal", async () => {
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<span>1</span>
|
|
<t t-portal="'#outside'">
|
|
<p>2</p>
|
|
</t>
|
|
</div>`;
|
|
}
|
|
|
|
addOutsideDiv(fixture);
|
|
await mount(Parent, fixture);
|
|
|
|
expect(fixture.innerHTML).toBe('<div id="outside"><p>2</p></div><div><span>1</span></div>');
|
|
});
|
|
test("basic use of portal on div", async () => {
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<span>1</span>
|
|
<div t-portal="'#outside'">
|
|
<p>2</p>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
addOutsideDiv(fixture);
|
|
await mount(Parent, fixture);
|
|
|
|
expect(fixture.innerHTML).toBe(
|
|
'<div id="outside"><div><p>2</p></div></div><div><span>1</span></div>'
|
|
);
|
|
});
|
|
test("simple catchError with portal", async () => {
|
|
class Boom extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<span>1</span>
|
|
<t t-portal="'#outside'">
|
|
<p><t t-esc="a.b.c"/></p>
|
|
</t>
|
|
</div>`;
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-if="error">Error</t>
|
|
<t t-else="">
|
|
<Boom />
|
|
</t>
|
|
</div>`;
|
|
static components = { Boom };
|
|
|
|
error: any = false;
|
|
|
|
setup() {
|
|
onError((err) => {
|
|
this.error = err;
|
|
this.render();
|
|
});
|
|
}
|
|
}
|
|
addOutsideDiv(fixture);
|
|
|
|
await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe('<div id="outside"></div><div>Error</div>');
|
|
});
|
|
|
|
test("basic use of portal in dev mode", async () => {
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<span>1</span>
|
|
<t t-portal="'#outside'">
|
|
<p>2</p>
|
|
</t>
|
|
</div>`;
|
|
}
|
|
|
|
addOutsideDiv(fixture);
|
|
await mount(Parent, fixture, { dev: true });
|
|
|
|
expect(fixture.innerHTML).toBe('<div id="outside"><p>2</p></div><div><span>1</span></div>');
|
|
});
|
|
|
|
test("conditional use of Portal", async () => {
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<span>1</span>
|
|
<t t-portal="'#outside'" t-if="state.hasPortal">
|
|
<p>2</p>
|
|
</t>`;
|
|
|
|
state = useState({ hasPortal: false });
|
|
}
|
|
|
|
addOutsideDiv(fixture);
|
|
const parent = await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe('<div id="outside"></div><span>1</span>');
|
|
|
|
parent.state.hasPortal = true;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe('<div id="outside"><p>2</p></div><span>1</span>');
|
|
|
|
parent.state.hasPortal = false;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe('<div id="outside"></div><span>1</span>');
|
|
|
|
parent.state.hasPortal = true;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe('<div id="outside"><p>2</p></div><span>1</span>');
|
|
});
|
|
|
|
test("conditional use of Portal (with sub Component)", async () => {
|
|
class Child extends Component {
|
|
static template = xml`<p><t t-esc="props.val"/></p>`;
|
|
}
|
|
class Parent extends Component {
|
|
static components = { Child };
|
|
static template = xml`
|
|
<span>1</span>
|
|
<t t-portal="'#outside'" t-if="state.hasPortal">
|
|
<Child val="state.val"/>
|
|
</t>`;
|
|
state = useState({ hasPortal: false, val: 1 });
|
|
}
|
|
|
|
addOutsideDiv(fixture);
|
|
const parent = await mount(Parent, fixture);
|
|
|
|
expect(fixture.innerHTML).toBe('<div id="outside"></div><span>1</span>');
|
|
|
|
parent.state.hasPortal = true;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe('<div id="outside"><p>1</p></div><span>1</span>');
|
|
|
|
parent.state.hasPortal = false;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe('<div id="outside"></div><span>1</span>');
|
|
|
|
parent.state.val = 2;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe('<div id="outside"></div><span>1</span>');
|
|
|
|
parent.state.hasPortal = true;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe('<div id="outside"><p>2</p></div><span>1</span>');
|
|
});
|
|
|
|
test("with target in template (before portal)", async () => {
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<div id="local-target"></div>
|
|
<span>1</span>
|
|
<t t-portal="'#local-target'">
|
|
<p>2</p>
|
|
</t>
|
|
</div>`;
|
|
}
|
|
|
|
await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe(
|
|
'<div><div id="local-target"><p>2</p></div><span>1</span></div>'
|
|
);
|
|
});
|
|
|
|
test("with target in template (after portal)", async () => {
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<span>1</span>
|
|
<t t-portal="'#local-target'">
|
|
<p>2</p>
|
|
</t>
|
|
<div id="local-target"></div>
|
|
</div>`;
|
|
}
|
|
|
|
await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe(
|
|
'<div><span>1</span><div id="local-target"><p>2</p></div></div>'
|
|
);
|
|
});
|
|
|
|
test("portal with target not in dom", async () => {
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-portal="'#does-not-exist'">
|
|
<div>2</div>
|
|
</t>
|
|
</div>`;
|
|
}
|
|
|
|
let error: Error;
|
|
try {
|
|
await mount(Parent, fixture);
|
|
} catch (e) {
|
|
error = e as Error;
|
|
}
|
|
|
|
expect(error!).toBeDefined();
|
|
expect(error!.message).toBe("invalid portal target");
|
|
expect(fixture.innerHTML).toBe(`<div></div>`);
|
|
expect(mockConsoleWarn).toBeCalledTimes(1);
|
|
});
|
|
|
|
test("portal with child and props", async () => {
|
|
const steps: string[] = [];
|
|
const outside = addOutsideDiv(fixture);
|
|
|
|
class Child extends Component {
|
|
static template = xml`<span><t t-esc="props.val"/></span>`;
|
|
|
|
setup() {
|
|
onMounted(() => {
|
|
steps.push("mounted");
|
|
expect(outside.innerHTML).toBe("<span>1</span>");
|
|
});
|
|
onPatched(() => {
|
|
steps.push("patched");
|
|
expect(outside.innerHTML).toBe("<span>2</span>");
|
|
});
|
|
}
|
|
}
|
|
class Parent extends Component {
|
|
static components = { Child };
|
|
static template = xml`
|
|
<div>
|
|
<t t-portal="'#outside'">
|
|
<Child val="state.val"/>
|
|
</t>
|
|
</div>`;
|
|
state = useState({ val: 1 });
|
|
}
|
|
|
|
const parent = await mount(Parent, fixture);
|
|
expect(outside.innerHTML).toBe("<span>1</span>");
|
|
expect(fixture.innerHTML).toBe('<div id="outside"><span>1</span></div><div></div>');
|
|
|
|
parent.state.val = 2;
|
|
await nextTick();
|
|
expect(outside.innerHTML).toBe("<span>2</span>");
|
|
expect(fixture.innerHTML).toBe('<div id="outside"><span>2</span></div><div></div>');
|
|
expect(steps).toEqual(["mounted", "patched"]);
|
|
});
|
|
|
|
test("portal with only text as content", async () => {
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-portal="'#outside'">
|
|
<t t-esc="'only text'"/>
|
|
</t>
|
|
</div>`;
|
|
}
|
|
|
|
addOutsideDiv(fixture);
|
|
await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe('<div id="outside">only text</div><div></div>');
|
|
});
|
|
|
|
test("portal with no content", async () => {
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-portal="'#outside'">
|
|
<t t-if="false" t-esc="'ABC'"/>
|
|
</t>
|
|
</div>`;
|
|
}
|
|
|
|
addOutsideDiv(fixture);
|
|
await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe(`<div id="outside"></div><div></div>`);
|
|
});
|
|
|
|
test("portal with many children", async () => {
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-portal="'#outside'">
|
|
<div>1</div>
|
|
<p>2</p>
|
|
</t>
|
|
</div>`;
|
|
}
|
|
addOutsideDiv(fixture);
|
|
await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe('<div id="outside"><div>1</div><p>2</p></div><div></div>');
|
|
});
|
|
|
|
test("portal with dynamic body", async () => {
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-portal="'#outside'">
|
|
<span t-if="state.val" t-esc="state.val"/>
|
|
<div t-else=""/>
|
|
</t>
|
|
</div>`;
|
|
state = useState({ val: "ab" });
|
|
}
|
|
|
|
const outside = addOutsideDiv(fixture);
|
|
const parent = await mount(Parent, fixture);
|
|
|
|
expect(outside.innerHTML).toBe(`<span>ab</span>`);
|
|
|
|
parent.state.val = "";
|
|
await nextTick();
|
|
expect(outside.innerHTML).toBe(`<div></div>`);
|
|
});
|
|
|
|
test("portal could have dynamically no content", async () => {
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-portal="'#outside'">
|
|
<span t-if="state.val" t-esc="state.val"/>
|
|
</t>
|
|
</div>`;
|
|
state = useState({ val: "ab" });
|
|
}
|
|
const outside = addOutsideDiv(fixture);
|
|
const parent = await mount(Parent, fixture);
|
|
|
|
expect(outside.innerHTML).toBe(`<span>ab</span>`);
|
|
|
|
parent.state.val = "";
|
|
await nextTick();
|
|
expect(outside.innerHTML).toBe(``);
|
|
});
|
|
|
|
test("lifecycle hooks of portal sub component are properly called", async () => {
|
|
const steps: any[] = [];
|
|
|
|
class Child extends Component {
|
|
static template = xml`<span t-esc="props.val"/>`;
|
|
setup() {
|
|
onMounted(() => steps.push("child:mounted"));
|
|
onWillPatch(() => steps.push("child:willPatch"));
|
|
onPatched(() => steps.push("child:patched"));
|
|
onWillUnmount(() => steps.push("child:willUnmount"));
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static components = { Child };
|
|
static template = xml`
|
|
<div>
|
|
<t t-portal="'#outside'" t-if="state.hasChild">
|
|
<Child val="state.val"/>
|
|
</t>
|
|
</div>`;
|
|
state = useState({ hasChild: false, val: 1 });
|
|
setup() {
|
|
onMounted(() => steps.push("parent:mounted"));
|
|
onWillPatch(() => steps.push("parent:willPatch"));
|
|
onPatched(() => steps.push("parent:patched"));
|
|
onWillUnmount(() => steps.push("parent:willUnmount"));
|
|
}
|
|
}
|
|
|
|
addOutsideDiv(fixture);
|
|
const parent = await mount(Parent, fixture);
|
|
expect(steps).toEqual(["parent:mounted"]);
|
|
expect(fixture.innerHTML).toBe('<div id="outside"></div><div></div>');
|
|
|
|
parent.state.hasChild = true;
|
|
await nextTick();
|
|
expect(steps).toEqual([
|
|
"parent:mounted",
|
|
"parent:willPatch",
|
|
"child:mounted",
|
|
"parent:patched",
|
|
]);
|
|
expect(fixture.innerHTML).toBe('<div id="outside"><span>1</span></div><div></div>');
|
|
|
|
parent.state.val = 2;
|
|
await nextTick();
|
|
expect(steps).toEqual([
|
|
"parent:mounted",
|
|
"parent:willPatch",
|
|
"child:mounted",
|
|
"parent:patched",
|
|
"parent:willPatch",
|
|
"child:willPatch",
|
|
"child:patched",
|
|
"parent:patched",
|
|
]);
|
|
expect(fixture.innerHTML).toBe('<div id="outside"><span>2</span></div><div></div>');
|
|
|
|
parent.state.hasChild = false;
|
|
await nextTick();
|
|
expect(steps).toEqual([
|
|
"parent:mounted",
|
|
"parent:willPatch",
|
|
"child:mounted",
|
|
"parent:patched",
|
|
"parent:willPatch",
|
|
"child:willPatch",
|
|
"child:patched",
|
|
"parent:patched",
|
|
"parent:willPatch",
|
|
"child:willUnmount",
|
|
"parent:patched",
|
|
]);
|
|
expect(fixture.innerHTML).toBe('<div id="outside"></div><div></div>');
|
|
});
|
|
|
|
test("portal destroys on crash", async () => {
|
|
class Child extends Component {
|
|
static template = xml`<span t-esc="props.error and this.will.crash" />`;
|
|
state = {};
|
|
}
|
|
class Parent extends Component {
|
|
static components = { Child };
|
|
static template = xml`
|
|
<div>
|
|
<t t-portal="'#outside'" >
|
|
<Child error="state.error"/>
|
|
</t>
|
|
</div>`;
|
|
state = { error: false };
|
|
setup() {
|
|
onError((e) => (error = e));
|
|
}
|
|
}
|
|
addOutsideDiv(fixture);
|
|
const parent = await mount(Parent, fixture);
|
|
|
|
let error: Error;
|
|
parent.state.error = true;
|
|
parent.render();
|
|
await nextTick();
|
|
expect(error!).toBeDefined();
|
|
const regexp =
|
|
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
|
|
expect(error!.message).toMatch(regexp);
|
|
});
|
|
|
|
test("portal's parent's env is not polluted", async () => {
|
|
class Child extends Component {
|
|
static template = xml`
|
|
<button>child</button>`;
|
|
}
|
|
class Parent extends Component {
|
|
static components = { Child };
|
|
static template = xml`
|
|
<div>
|
|
<t t-portal="'#outside'">
|
|
<Child />
|
|
</t>
|
|
</div>`;
|
|
}
|
|
const env = {};
|
|
addOutsideDiv(fixture);
|
|
const parent = await mount(Parent, fixture, { env });
|
|
expect(parent.env).toStrictEqual({});
|
|
});
|
|
|
|
test("Portal composed with t-slot", async () => {
|
|
const steps: Array<string> = [];
|
|
let childInst: Component | null = null;
|
|
class Child2 extends Component {
|
|
static template = xml`<div t-on-custom="onCustom"><span id="childSpan">child2</span></div>`;
|
|
setup() {
|
|
childInst = this;
|
|
}
|
|
onCustom(ev: Event) {
|
|
this.props.customHandler(ev);
|
|
}
|
|
}
|
|
class Child extends Component {
|
|
static components = { Child2 };
|
|
static template = xml`
|
|
<t t-portal="'#outside'">
|
|
<t t-slot="default"/>
|
|
</t>`;
|
|
}
|
|
class Parent extends Component {
|
|
static components = { Child, Child2 };
|
|
static template = xml`
|
|
<div>
|
|
<Child>
|
|
<Child2 customHandler="_handled"/>
|
|
</Child>
|
|
</div>`;
|
|
|
|
_handled(ev: Event) {
|
|
steps.push(ev.type as string);
|
|
}
|
|
}
|
|
|
|
addOutsideDiv(fixture);
|
|
await mount(Parent, fixture);
|
|
|
|
elem(childInst!).dispatchEvent(new CustomEvent("custom"));
|
|
expect(steps).toEqual(["custom"]);
|
|
});
|
|
|
|
test("Add and remove portals", async () => {
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<t t-portal="'#outside'" t-foreach="portalIds" t-as="portalId" t-key="portalId">
|
|
Portal<t t-esc="portalId"/>
|
|
</t>`;
|
|
portalIds = useState([] as any);
|
|
}
|
|
|
|
addOutsideDiv(fixture);
|
|
const parent = await mount(Parent, fixture);
|
|
|
|
expect(fixture.innerHTML).toBe('<div id="outside"></div>');
|
|
|
|
parent.portalIds.push(1);
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe('<div id="outside"> Portal1</div>');
|
|
|
|
parent.portalIds.push(2);
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe('<div id="outside"> Portal1 Portal2</div>');
|
|
|
|
parent.portalIds.pop();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe('<div id="outside"> Portal1</div>');
|
|
|
|
parent.portalIds.pop();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe('<div id="outside"></div>');
|
|
});
|
|
|
|
test("Add and remove portals on div", async () => {
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div t-portal="'#outside'" t-foreach="portalIds" t-as="portalId" t-key="portalId">
|
|
Portal<t t-esc="portalId"/>
|
|
</div>`;
|
|
portalIds = useState([] as any);
|
|
}
|
|
|
|
addOutsideDiv(fixture);
|
|
const parent = await mount(Parent, fixture);
|
|
|
|
expect(fixture.innerHTML).toBe('<div id="outside"></div>');
|
|
|
|
parent.portalIds.push(1);
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe('<div id="outside"><div> Portal1</div></div>');
|
|
|
|
parent.portalIds.push(2);
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe(
|
|
'<div id="outside"><div> Portal1</div><div> Portal2</div></div>'
|
|
);
|
|
|
|
parent.portalIds.pop();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe('<div id="outside"><div> Portal1</div></div>');
|
|
|
|
parent.portalIds.pop();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe('<div id="outside"></div>');
|
|
});
|
|
|
|
test("Add and remove portals with t-foreach", async () => {
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<t t-foreach="portalIds" t-as="portalId" t-key="portalId">
|
|
<div>
|
|
<t t-esc="portalId"/>
|
|
<t t-portal="'#outside'">
|
|
Portal<t t-esc="portalId"/>
|
|
</t>
|
|
</div>
|
|
</t>`;
|
|
portalIds = useState([] as any);
|
|
}
|
|
|
|
addOutsideDiv(fixture);
|
|
const parent = await mount(Parent, fixture);
|
|
|
|
expect(fixture.innerHTML).toBe('<div id="outside"></div>');
|
|
|
|
parent.portalIds.push(1);
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe('<div id="outside"> Portal1</div><div>1</div>');
|
|
|
|
parent.portalIds.push(2);
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe(
|
|
'<div id="outside"> Portal1 Portal2</div><div>1</div><div>2</div>'
|
|
);
|
|
|
|
parent.portalIds.pop();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe('<div id="outside"> Portal1</div><div>1</div>');
|
|
|
|
parent.portalIds.pop();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe('<div id="outside"></div>');
|
|
});
|
|
|
|
test("Add and remove portals with t-foreach and destroy", async () => {
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<t t-foreach="portalIds" t-as="portalId" t-key="portalId">
|
|
<div>
|
|
<t t-esc="portalId"/>
|
|
<t t-portal="'#outside'">
|
|
Portal<t t-esc="portalId"/>
|
|
</t>
|
|
</div>
|
|
</t>`;
|
|
portalIds = useState([] as any);
|
|
}
|
|
|
|
addOutsideDiv(fixture);
|
|
const app = new App(Parent);
|
|
const parent = await app.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe('<div id="outside"></div>');
|
|
|
|
parent.portalIds.push(1);
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe('<div id="outside"> Portal1</div><div>1</div>');
|
|
|
|
parent.portalIds.push(2);
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe(
|
|
'<div id="outside"> Portal1 Portal2</div><div>1</div><div>2</div>'
|
|
);
|
|
|
|
app.destroy();
|
|
//This will test explicitly that we don't use an await nextTick(); after the destroy.
|
|
expect(fixture.innerHTML).toBe('<div id="outside"></div>');
|
|
});
|
|
|
|
test("conditional use of Portal with div", async () => {
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<t t-if="state.hasPortal">
|
|
<div>
|
|
<span>hasPortal</span>
|
|
<t t-portal="'#outside'">
|
|
<p>thePortal</p>
|
|
</t>
|
|
</div>
|
|
</t>`;
|
|
|
|
state = useState({ hasPortal: false });
|
|
}
|
|
|
|
addOutsideDiv(fixture);
|
|
const parent = await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe('<div id="outside"></div>');
|
|
|
|
parent.state.hasPortal = true;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe(
|
|
'<div id="outside"><p>thePortal</p></div><div><span>hasPortal</span></div>'
|
|
);
|
|
|
|
parent.state.hasPortal = false;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe('<div id="outside"></div>');
|
|
|
|
parent.state.hasPortal = true;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe(
|
|
'<div id="outside"><p>thePortal</p></div><div><span>hasPortal</span></div>'
|
|
);
|
|
});
|
|
|
|
test("conditional use of Portal with child and div", async () => {
|
|
class Child extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<span>hasPortal</span>
|
|
<t t-foreach="[1]" t-as="elem" t-key="elem">
|
|
<t t-portal="'#outside'">
|
|
<p>thePortal</p>
|
|
</t>
|
|
</t>
|
|
</div>`;
|
|
}
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<t t-if="state.hasPortal">
|
|
<Child />
|
|
</t>`;
|
|
|
|
static components = { Child };
|
|
|
|
state = useState({ hasPortal: false });
|
|
}
|
|
|
|
addOutsideDiv(fixture);
|
|
const parent = await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe('<div id="outside"></div>');
|
|
|
|
parent.state.hasPortal = true;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe(
|
|
'<div id="outside"><p>thePortal</p></div><div><span>hasPortal</span></div>'
|
|
);
|
|
|
|
parent.state.hasPortal = false;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe('<div id="outside"></div>');
|
|
|
|
parent.state.hasPortal = true;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe(
|
|
'<div id="outside"><p>thePortal</p></div><div><span>hasPortal</span></div>'
|
|
);
|
|
});
|
|
|
|
test("conditional use of Portal with child and div, variation", async () => {
|
|
class Child extends Component {
|
|
static template = xml`
|
|
<span>hasPortal</span>
|
|
<t t-foreach="[1]" t-as="elem" t-key="elem">
|
|
<t t-portal="'#outside'">
|
|
<p>thePortal</p>
|
|
</t>
|
|
</t>`;
|
|
}
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<t t-if="state.hasPortal">
|
|
<div>
|
|
<Child />
|
|
</div>
|
|
</t>`;
|
|
|
|
static components = { Child };
|
|
|
|
state = useState({ hasPortal: false });
|
|
}
|
|
|
|
addOutsideDiv(fixture);
|
|
const parent = await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe('<div id="outside"></div>');
|
|
|
|
parent.state.hasPortal = true;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe(
|
|
'<div id="outside"><p>thePortal</p></div><div><span>hasPortal</span></div>'
|
|
);
|
|
|
|
parent.state.hasPortal = false;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe('<div id="outside"></div>');
|
|
|
|
parent.state.hasPortal = true;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe(
|
|
'<div id="outside"><p>thePortal</p></div><div><span>hasPortal</span></div>'
|
|
);
|
|
});
|
|
test("Add and remove portals with t-foreach inside div", async () => {
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-foreach="portalIds" t-as="portalId" t-key="portalId">
|
|
<div>
|
|
<t t-esc="portalId"/>
|
|
<t t-portal="'#outside'">
|
|
Portal<t t-esc="portalId"/>
|
|
</t>
|
|
</div>
|
|
</t>
|
|
</div>`;
|
|
portalIds = useState([] as any);
|
|
}
|
|
|
|
addOutsideDiv(fixture);
|
|
const parent = await mount(Parent, fixture);
|
|
|
|
expect(fixture.innerHTML).toBe('<div id="outside"></div><div></div>');
|
|
|
|
parent.portalIds.push(1);
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe('<div id="outside"> Portal1</div><div><div>1</div></div>');
|
|
|
|
parent.portalIds.push(2);
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe(
|
|
'<div id="outside"> Portal1 Portal2</div><div><div>1</div><div>2</div></div>'
|
|
);
|
|
|
|
parent.portalIds.pop();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe('<div id="outside"> Portal1</div><div><div>1</div></div>');
|
|
|
|
parent.portalIds.pop();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe('<div id="outside"></div><div></div>');
|
|
});
|
|
});
|
|
|
|
describe("Portal: UI/UX", () => {
|
|
test("focus is kept across re-renders", async () => {
|
|
class Child extends Component {
|
|
static template = xml`
|
|
<input id="target-me" t-att-placeholder="props.val"/>`;
|
|
}
|
|
class Parent extends Component {
|
|
static components = { Child };
|
|
static template = xml`
|
|
<div>
|
|
<t t-portal="'#outside'">
|
|
<Child val="state.val"/>
|
|
</t>
|
|
</div>`;
|
|
state = useState({ val: "ab" });
|
|
}
|
|
addOutsideDiv(fixture);
|
|
const parent = await mount(Parent, fixture);
|
|
const input = document.querySelector("#target-me");
|
|
expect(input!.nodeName).toBe("INPUT");
|
|
expect((input as HTMLInputElement).placeholder).toBe("ab");
|
|
|
|
(input as HTMLInputElement).focus();
|
|
expect(document.activeElement === input).toBeTruthy();
|
|
|
|
parent.state.val = "bc";
|
|
await nextTick();
|
|
const inputReRendered = document.querySelector("#target-me");
|
|
expect(inputReRendered!.nodeName).toBe("INPUT");
|
|
expect((inputReRendered as HTMLInputElement).placeholder).toBe("bc");
|
|
expect(document.activeElement === inputReRendered).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe("Portal: Props validation", () => {
|
|
test("target is mandatory 1", async () => {
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-portal>
|
|
<div>2</div>
|
|
</t>
|
|
</div>`;
|
|
}
|
|
let error: Error;
|
|
try {
|
|
await mount(Parent, fixture, { dev: true });
|
|
} catch (e) {
|
|
error = e as Error;
|
|
}
|
|
expect(error!).toBeDefined();
|
|
expect(error!.message).toContain(`attribute without value.`);
|
|
});
|
|
|
|
test("target is mandatory 2", async () => {
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-portal="">
|
|
<div>2</div>
|
|
</t>
|
|
</div>`;
|
|
}
|
|
let error: Error;
|
|
try {
|
|
await mount(Parent, fixture, { dev: true });
|
|
} catch (e) {
|
|
error = e as Error;
|
|
}
|
|
expect(error!).toBeDefined();
|
|
expect(error!.message).toBe(`Unexpected token ','`);
|
|
});
|
|
|
|
test("target must be a valid selector", async () => {
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-portal="' '">
|
|
<div>2</div>
|
|
</t>
|
|
</div>`;
|
|
}
|
|
let error: Error;
|
|
try {
|
|
await mount(Parent, fixture, { dev: true });
|
|
} catch (e) {
|
|
error = e as Error;
|
|
}
|
|
expect(error!).toBeDefined();
|
|
expect(error!.message).toBe(`' ' is not a valid selector`);
|
|
});
|
|
|
|
test("target must be a valid selector 2", async () => {
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-portal="'aa'">
|
|
<div>2</div>
|
|
</t>
|
|
</div>`;
|
|
}
|
|
let error: Error;
|
|
try {
|
|
await mount(Parent, fixture, { dev: true });
|
|
} catch (e) {
|
|
error = e as Error;
|
|
}
|
|
expect(error!).toBeDefined();
|
|
expect(error!.message).toBe(`invalid portal target`);
|
|
});
|
|
});
|