import { OwlError } from "../../src/runtime/error_handling";
import {
App,
Component,
mount,
onError,
onMounted,
onPatched,
onWillPatch,
onWillUnmount,
useState,
} from "../../src";
import { xml } from "../../src/";
import { DEV_MSG } from "../../src/runtime/app";
import { elem, makeTestFixture, nextAppError, 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`
`;
}
addOutsideDiv(fixture);
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe('1
');
});
test("basic use of portal, variation", async () => {
class Parent extends Component {
static template = xml`
`;
target = "#outside";
}
addOutsideDiv(fixture);
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe('1
');
});
test("basic use of portal on div", async () => {
class Parent extends Component {
static template = xml`
`;
}
addOutsideDiv(fixture);
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe(
'1
'
);
});
test("simple catchError with portal", async () => {
class Boom extends Component {
static template = xml`
`;
}
class Parent extends Component {
static template = xml`
Error
`;
static components = { Boom };
error: any = false;
setup() {
onError((err) => {
this.error = err;
this.render();
});
}
}
addOutsideDiv(fixture);
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe('
Error
');
});
test("basic use of portal in dev mode", async () => {
class Parent extends Component {
static template = xml`
`;
}
addOutsideDiv(fixture);
await mount(Parent, fixture, { dev: true });
expect(fixture.innerHTML).toBe('1
');
});
test("conditional use of Portal", async () => {
class Parent extends Component {
static template = xml`
1
2
`;
state = useState({ hasPortal: false });
}
addOutsideDiv(fixture);
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe('
1 ');
parent.state.hasPortal = true;
await nextTick();
expect(fixture.innerHTML).toBe('1 ');
parent.state.hasPortal = false;
await nextTick();
expect(fixture.innerHTML).toBe('
1 ');
parent.state.hasPortal = true;
await nextTick();
expect(fixture.innerHTML).toBe('1 ');
});
test("conditional use of Portal (with sub Component)", async () => {
class Child extends Component {
static template = xml`
`;
}
class Parent extends Component {
static components = { Child };
static template = xml`
1
`;
state = useState({ hasPortal: false, val: 1 });
}
addOutsideDiv(fixture);
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe('
1 ');
parent.state.hasPortal = true;
await nextTick();
expect(fixture.innerHTML).toBe('1 ');
parent.state.hasPortal = false;
await nextTick();
expect(fixture.innerHTML).toBe('
1 ');
parent.state.val = 2;
await nextTick();
expect(fixture.innerHTML).toBe('
1 ');
parent.state.hasPortal = true;
await nextTick();
expect(fixture.innerHTML).toBe('1 ');
});
test("with target in template (before portal)", async () => {
class Parent extends Component {
static template = xml`
`;
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe(
''
);
});
test("with target in template (after portal)", async () => {
class Parent extends Component {
static template = xml`
`;
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe(
''
);
});
test("portal with target not in dom", async () => {
class Parent extends Component {
static template = xml`
`;
}
let error: Error;
const app = new App(Parent);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("invalid portal target");
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe("invalid portal target");
expect(fixture.innerHTML).toBe(``);
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` `;
setup() {
onMounted(() => {
steps.push("mounted");
expect(outside.innerHTML).toBe("1 ");
});
onPatched(() => {
steps.push("patched");
expect(outside.innerHTML).toBe("2 ");
});
}
}
class Parent extends Component {
static components = { Child };
static template = xml`
`;
state = useState({ val: 1 });
}
const parent = await mount(Parent, fixture);
expect(outside.innerHTML).toBe("1 ");
expect(fixture.innerHTML).toBe('1
');
parent.state.val = 2;
await nextTick();
expect(outside.innerHTML).toBe("2 ");
expect(fixture.innerHTML).toBe('2
');
expect(steps).toEqual(["mounted", "patched"]);
});
test("portal with only text as content", async () => {
class Parent extends Component {
static template = xml`
`;
}
addOutsideDiv(fixture);
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe('only text
');
});
test("portal with no content", async () => {
class Parent extends Component {
static template = xml`
`;
}
addOutsideDiv(fixture);
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe(`
`);
});
test("portal with many children", async () => {
class Parent extends Component {
static template = xml`
`;
}
addOutsideDiv(fixture);
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe('
');
});
test("portal with dynamic body", async () => {
class Parent extends Component {
static template = xml`
`;
state = useState({ val: "ab" });
}
const outside = addOutsideDiv(fixture);
const parent = await mount(Parent, fixture);
expect(outside.innerHTML).toBe(`ab `);
parent.state.val = "";
await nextTick();
expect(outside.innerHTML).toBe(`
`);
});
test("portal could have dynamically no content", async () => {
class Parent extends Component {
static template = xml`
`;
state = useState({ val: "ab" });
}
const outside = addOutsideDiv(fixture);
const parent = await mount(Parent, fixture);
expect(outside.innerHTML).toBe(`ab `);
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` `;
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`
`;
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('
');
parent.state.hasChild = true;
await nextTick();
expect(steps).toEqual([
"parent:mounted",
"parent:willPatch",
"child:mounted",
"parent:patched",
]);
expect(fixture.innerHTML).toBe('1
');
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('2
');
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('
');
});
test("portal destroys on crash", async () => {
class Child extends Component {
static template = xml` `;
state = {};
}
class Parent extends Component {
static components = { Child };
static template = xml`
`;
state = { error: false };
setup() {
onError(({ cause }) => (error = cause));
}
}
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`
child `;
}
class Parent extends Component {
static components = { Child };
static template = xml`
`;
}
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 = [];
let childInst: Component | null = null;
class Child2 extends Component {
static template = xml`child2
`;
setup() {
childInst = this;
}
onCustom(ev: Event) {
this.props.customHandler(ev);
}
}
class Child extends Component {
static components = { Child2 };
static template = xml`
`;
}
class Parent extends Component {
static components = { Child, Child2 };
static template = xml`
`;
_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`
Portal
`;
portalIds = useState([] as any);
}
addOutsideDiv(fixture);
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe('
');
parent.portalIds.push(1);
await nextTick();
expect(fixture.innerHTML).toBe(' Portal1
');
parent.portalIds.push(2);
await nextTick();
expect(fixture.innerHTML).toBe(' Portal1 Portal2
');
parent.portalIds.pop();
await nextTick();
expect(fixture.innerHTML).toBe(' Portal1
');
parent.portalIds.pop();
await nextTick();
expect(fixture.innerHTML).toBe('
');
});
test("Add and remove portals on div", async () => {
class Parent extends Component {
static template = xml`
Portal
`;
portalIds = useState([] as any);
}
addOutsideDiv(fixture);
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe('
');
parent.portalIds.push(1);
await nextTick();
expect(fixture.innerHTML).toBe('');
parent.portalIds.push(2);
await nextTick();
expect(fixture.innerHTML).toBe(
''
);
parent.portalIds.pop();
await nextTick();
expect(fixture.innerHTML).toBe('');
parent.portalIds.pop();
await nextTick();
expect(fixture.innerHTML).toBe('
');
});
test("Add and remove portals with t-foreach", async () => {
class Parent extends Component {
static template = xml`
Portal
`;
portalIds = useState([] as any);
}
addOutsideDiv(fixture);
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe('
');
parent.portalIds.push(1);
await nextTick();
expect(fixture.innerHTML).toBe(' Portal1
1
');
parent.portalIds.push(2);
await nextTick();
expect(fixture.innerHTML).toBe(
' Portal1 Portal2
1
2
'
);
parent.portalIds.pop();
await nextTick();
expect(fixture.innerHTML).toBe(' Portal1
1
');
parent.portalIds.pop();
await nextTick();
expect(fixture.innerHTML).toBe('
');
});
test("Add and remove portals with t-foreach and destroy", async () => {
class Parent extends Component {
static template = xml`
Portal
`;
portalIds = useState([] as any);
}
addOutsideDiv(fixture);
const app = new App(Parent);
const parent = await app.mount(fixture);
expect(fixture.innerHTML).toBe('
');
parent.portalIds.push(1);
await nextTick();
expect(fixture.innerHTML).toBe(' Portal1
1
');
parent.portalIds.push(2);
await nextTick();
expect(fixture.innerHTML).toBe(
' Portal1 Portal2
1
2
'
);
app.destroy();
//This will test explicitly that we don't use an await nextTick(); after the destroy.
expect(fixture.innerHTML).toBe('
');
});
test("conditional use of Portal with div", async () => {
class Parent extends Component {
static template = xml`
`;
state = useState({ hasPortal: false });
}
addOutsideDiv(fixture);
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe('
');
parent.state.hasPortal = true;
await nextTick();
expect(fixture.innerHTML).toBe(
'hasPortal
'
);
parent.state.hasPortal = false;
await nextTick();
expect(fixture.innerHTML).toBe('
');
parent.state.hasPortal = true;
await nextTick();
expect(fixture.innerHTML).toBe(
'hasPortal
'
);
});
test("conditional use of Portal with child and div", async () => {
class Child extends Component {
static template = xml`
`;
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
state = useState({ hasPortal: false });
}
addOutsideDiv(fixture);
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe('
');
parent.state.hasPortal = true;
await nextTick();
expect(fixture.innerHTML).toBe(
'hasPortal
'
);
parent.state.hasPortal = false;
await nextTick();
expect(fixture.innerHTML).toBe('
');
parent.state.hasPortal = true;
await nextTick();
expect(fixture.innerHTML).toBe(
'hasPortal
'
);
});
test("conditional use of Portal with child and div, variation", async () => {
class Child extends Component {
static template = xml`
hasPortal
thePortal
`;
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
state = useState({ hasPortal: false });
}
addOutsideDiv(fixture);
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe('
');
parent.state.hasPortal = true;
await nextTick();
expect(fixture.innerHTML).toBe(
'hasPortal
'
);
parent.state.hasPortal = false;
await nextTick();
expect(fixture.innerHTML).toBe('
');
parent.state.hasPortal = true;
await nextTick();
expect(fixture.innerHTML).toBe(
'hasPortal
'
);
});
test("Add and remove portals with t-foreach inside div", async () => {
class Parent extends Component {
static template = xml`
`;
portalIds = useState([] as any);
}
addOutsideDiv(fixture);
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe('
');
parent.portalIds.push(1);
await nextTick();
expect(fixture.innerHTML).toBe(' Portal1
');
parent.portalIds.push(2);
await nextTick();
expect(fixture.innerHTML).toBe(
' Portal1 Portal2
'
);
parent.portalIds.pop();
await nextTick();
expect(fixture.innerHTML).toBe(' Portal1
');
parent.portalIds.pop();
await nextTick();
expect(fixture.innerHTML).toBe('
');
});
test("Child and Portal", async () => {
class Child extends Component {
static template = xml`
child
portal `;
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe(
'child portal
'
);
});
test("portal and Child", async () => {
class Child extends Component {
static template = xml`
child
portal `;
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe(
'portal
child '
);
});
});
describe("Portal: UI/UX", () => {
test("focus is kept across re-renders", async () => {
class Child extends Component {
static template = xml`
`;
}
class Parent extends Component {
static components = { Child };
static template = xml`
`;
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`
`;
}
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`
`;
}
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`
`;
}
let error: OwlError;
const app = new App(Parent);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle");
await mountProm;
expect(error!).toBeDefined();
expect(error!.cause).toBeDefined();
expect(error!.cause.message).toBe(`' ' is not a valid selector`);
});
test("target must be a valid selector 2", async () => {
class Parent extends Component {
static template = xml`
`;
}
let error: Error;
const app = new App(Parent);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("invalid portal target");
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe(`invalid portal target`);
});
});