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, 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`
1

2

`; } addOutsideDiv(fixture); await mount(Parent, fixture); expect(fixture.innerHTML).toBe('

2

1
'); }); test("basic use of portal, variation", async () => { class Parent extends Component { static template = xml`
1

2

`; target = "#outside"; } addOutsideDiv(fixture); await mount(Parent, fixture); expect(fixture.innerHTML).toBe('

2

1
'); }); test("basic use of portal on div", async () => { class Parent extends Component { static template = xml`
1

2

`; } addOutsideDiv(fixture); await mount(Parent, fixture); expect(fixture.innerHTML).toBe( '

2

1
' ); }); test("simple catchError with portal", async () => { class Boom extends Component { static template = xml`
1

`; } 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`
1

2

`; } addOutsideDiv(fixture); await mount(Parent, fixture, { dev: true }); expect(fixture.innerHTML).toBe('

2

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('

2

1'); parent.state.hasPortal = false; await nextTick(); expect(fixture.innerHTML).toBe('
1'); parent.state.hasPortal = true; await nextTick(); expect(fixture.innerHTML).toBe('

2

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

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('

2

1'); }); test("with target in template (before portal)", async () => { class Parent extends Component { static template = xml`
1

2

`; } await mount(Parent, fixture); expect(fixture.innerHTML).toBe( '

2

1
' ); }); test("with target in template (after portal)", async () => { class Parent extends Component { static template = xml`
1

2

`; } await mount(Parent, fixture); expect(fixture.innerHTML).toBe( '
1

2

' ); }); test("portal with target not in dom", async () => { class Parent extends Component { static template = xml`
2
`; } 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(`
`); 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`
1

2

`; } addOutsideDiv(fixture); await mount(Parent, fixture); expect(fixture.innerHTML).toBe('
1

2

'); }); 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((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` `; } 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('
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 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`
hasPortal

thePortal

`; 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( '

thePortal

hasPortal
' ); parent.state.hasPortal = false; await nextTick(); expect(fixture.innerHTML).toBe('
'); parent.state.hasPortal = true; await nextTick(); expect(fixture.innerHTML).toBe( '

thePortal

hasPortal
' ); }); test("conditional use of Portal with child and div", 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( '

thePortal

hasPortal
' ); parent.state.hasPortal = false; await nextTick(); expect(fixture.innerHTML).toBe('
'); parent.state.hasPortal = true; await nextTick(); expect(fixture.innerHTML).toBe( '

thePortal

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( '

thePortal

hasPortal
' ); parent.state.hasPortal = false; await nextTick(); expect(fixture.innerHTML).toBe('
'); parent.state.hasPortal = true; await nextTick(); expect(fixture.innerHTML).toBe( '

thePortal

hasPortal
' ); }); test("Add and remove portals with t-foreach inside 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('
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('
'); }); }); 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`
2
`; } 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`
2
`; } 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`
2
`; } 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`
2
`; } 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`); }); });