mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
9d378b0e7b
Previously, `validateTarget` only checked if the target element or its host (if it was a ShadowRoot) was directly contained in the document body. This failed in cases where the target element was nested inside a shadow DOM, which itself was attached to the document. This commit introduces a new helper `isAttachedToDocument` that traverses through parent nodes and shadow roots to ensure that the target is ultimately attached to the given document. Additionally, it now throws a clear error if `document.defaultView` is missing, indicating that the target document is detached or invalid. This ensures proper validation of mount targets, including complex scenarios with shadow roots and iframes.
155 lines
4.9 KiB
TypeScript
155 lines
4.9 KiB
TypeScript
import { App, Component, useRef, xml } from "../../src";
|
|
import { status } from "../../src/runtime/status";
|
|
import { makeTestFixture, snapshotEverything } from "../helpers";
|
|
|
|
let fixture: HTMLElement;
|
|
|
|
snapshotEverything();
|
|
|
|
beforeEach(() => {
|
|
fixture = makeTestFixture();
|
|
});
|
|
|
|
describe("shadow_dom", () => {
|
|
test("can mount app", async () => {
|
|
class SomeComponent extends Component {
|
|
static template = xml`<div class="my-div"/>`;
|
|
}
|
|
|
|
const container = document.createElement("div");
|
|
fixture.appendChild(container);
|
|
const shadow = container.attachShadow({ mode: "open" });
|
|
const app = new App(SomeComponent);
|
|
const comp = await app.mount(shadow);
|
|
const div = shadow.querySelector(".my-div");
|
|
expect(div).not.toBe(null);
|
|
expect(shadow.contains(div)).toBe(true);
|
|
app.destroy();
|
|
expect(shadow.contains(div)).toBe(false);
|
|
expect(status(comp)).toBe("destroyed");
|
|
});
|
|
|
|
test("can mount app in closed shadow dom", async () => {
|
|
class SomeComponent extends Component {
|
|
static template = xml`<div class="my-div"/>`;
|
|
}
|
|
|
|
const container = document.createElement("div");
|
|
fixture.appendChild(container);
|
|
const shadow = container.attachShadow({ mode: "closed" });
|
|
const app = new App(SomeComponent);
|
|
const comp = await app.mount(shadow);
|
|
const div = shadow.querySelector(".my-div");
|
|
expect(div).not.toBe(null);
|
|
expect(shadow.contains(div)).toBe(true);
|
|
app.destroy();
|
|
expect(shadow.contains(div)).toBe(false);
|
|
expect(status(comp)).toBe("destroyed");
|
|
});
|
|
|
|
test("can bind event handler", async () => {
|
|
let a = 1;
|
|
class SomeComponent extends Component {
|
|
static template = xml`<button t-on-click="add">Click</button>`;
|
|
|
|
add() {
|
|
a = 3;
|
|
}
|
|
}
|
|
const container = document.createElement("div");
|
|
fixture.appendChild(container);
|
|
const shadow = container.attachShadow({ mode: "open" });
|
|
await new App(SomeComponent).mount(shadow);
|
|
expect(a).toBe(1);
|
|
shadow.querySelector("button")!.click();
|
|
expect(a).toBe(3);
|
|
});
|
|
|
|
test("useRef hook", async () => {
|
|
let comp: SomeComponent;
|
|
class SomeComponent extends Component {
|
|
static template = xml`<div t-ref="refName" class="my-div"/>`;
|
|
div = useRef("refName");
|
|
setup() {
|
|
comp = this;
|
|
}
|
|
}
|
|
const container = document.createElement("div");
|
|
fixture.appendChild(container);
|
|
const shadow = container.attachShadow({ mode: "open" });
|
|
const mountedProm = new App(SomeComponent).mount(shadow);
|
|
expect(comp!.div.el).toBe(null);
|
|
await mountedProm;
|
|
expect(comp!.div.el).toBe(shadow.querySelector(".my-div"));
|
|
});
|
|
|
|
test("can mount app inside a shadow child element", async () => {
|
|
class SomeComponent extends Component {
|
|
static template = xml`<div class="my-div"/>`;
|
|
}
|
|
const shadow = fixture.attachShadow({ mode: "open" });
|
|
const shadowDiv = document.createElement("div");
|
|
shadow.append(shadowDiv);
|
|
const app = new App(SomeComponent);
|
|
const comp = await app.mount(shadowDiv);
|
|
const div = shadow.querySelector(".my-div");
|
|
expect(div).not.toBe(null);
|
|
expect(shadow.contains(div)).toBe(true);
|
|
app.destroy();
|
|
expect(shadow.contains(div)).toBe(false);
|
|
expect(status(comp)).toBe("destroyed");
|
|
});
|
|
|
|
test("can mount app inside a separate HTML document", async () => {
|
|
class SomeComponent extends Component {
|
|
static template = xml`<div class="my-div"/>`;
|
|
}
|
|
|
|
const separateDoc = document.implementation.createHTMLDocument();
|
|
const container = separateDoc.createElement("div");
|
|
separateDoc.body.appendChild(container);
|
|
|
|
const app = new App(SomeComponent);
|
|
let error: Error;
|
|
try {
|
|
await app.mount(container);
|
|
} catch (e) {
|
|
error = e as Error;
|
|
}
|
|
expect(error!).toBeDefined();
|
|
expect(error!.message).toBe(
|
|
"Cannot mount a component: the target document is not attached to a window (defaultView is missing)"
|
|
);
|
|
});
|
|
|
|
test("can mount app inside an element in a shadow root inside an iframe", async () => {
|
|
class SomeComponent extends Component {
|
|
static template = xml`<div class="my-div"/>`;
|
|
}
|
|
|
|
const iframe = document.createElement("iframe");
|
|
fixture.appendChild(iframe);
|
|
|
|
const iframeDoc = iframe.contentDocument!;
|
|
const container = iframeDoc.createElement("div");
|
|
iframeDoc.body.appendChild(container);
|
|
|
|
const shadow = container.attachShadow({ mode: "open" });
|
|
|
|
const shadowTarget = iframeDoc.createElement("div");
|
|
shadow.appendChild(shadowTarget);
|
|
|
|
const app = new App(SomeComponent);
|
|
const comp = await app.mount(shadowTarget);
|
|
|
|
const div = shadow.querySelector(".my-div");
|
|
expect(div).not.toBe(null);
|
|
expect(shadow.contains(div)).toBe(true);
|
|
expect(iframeDoc.body.contains(container)).toBe(true);
|
|
|
|
app.destroy();
|
|
expect(shadow.contains(div)).toBe(false);
|
|
expect(status(comp)).toBe("destroyed");
|
|
});
|
|
});
|