mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
c16d8ed6de
We re-add the possibility to validate props when dev mode is active. No change in the API right Now. The dev mode is activated via the configure method of App class.
319 lines
8.7 KiB
TypeScript
319 lines
8.7 KiB
TypeScript
import {
|
|
App,
|
|
Component,
|
|
mount,
|
|
useRef,
|
|
useState,
|
|
useComponent,
|
|
onMounted,
|
|
onPatched,
|
|
onWillStart,
|
|
onWillUpdateProps,
|
|
onWillPatch,
|
|
xml,
|
|
onWillUnmount,
|
|
} from "../../src";
|
|
import { makeTestFixture, nextTick, snapshotEverything } from "../helpers";
|
|
|
|
let fixture: HTMLElement;
|
|
|
|
snapshotEverything();
|
|
beforeEach(() => {
|
|
fixture = makeTestFixture();
|
|
});
|
|
|
|
describe("hooks", () => {
|
|
test("useRef hook: basic use", async () => {
|
|
let counter: Counter;
|
|
class Counter extends Component {
|
|
static template = xml`<div><button t-ref="button"><t t-esc="value"/></button></div>`;
|
|
button = useRef("button");
|
|
value = 0;
|
|
setup() {
|
|
counter = this;
|
|
}
|
|
increment() {
|
|
this.value++;
|
|
(this.button.el as HTMLButtonElement).innerHTML = String(this.value);
|
|
}
|
|
}
|
|
const mounted = mount(Counter, fixture);
|
|
expect(counter!.button.el).toBe(null);
|
|
await mounted;
|
|
expect(fixture.innerHTML).toBe("<div><button>0</button></div>");
|
|
expect(counter!.button.el).not.toBe(null);
|
|
expect(counter!.button.el).toBe(fixture.querySelector("button"));
|
|
counter!.increment();
|
|
expect(fixture.innerHTML).toBe("<div><button>1</button></div>");
|
|
});
|
|
// TODO: rename like next test (ensures willPatch/patched calls are symmetrical)
|
|
test("two different call to willPatch/patched should work", async () => {
|
|
const steps: string[] = [];
|
|
function useMyHook(i: number) {
|
|
onPatched(() => {
|
|
steps.push("hook:patched" + i);
|
|
});
|
|
onWillPatch(() => {
|
|
steps.push("hook:willPatch" + i);
|
|
});
|
|
}
|
|
class Test extends Component {
|
|
static template = xml`<div>hey<t t-esc="state.value"/></div>`;
|
|
state = useState({ value: 1 });
|
|
setup() {
|
|
useMyHook(1);
|
|
useMyHook(2);
|
|
}
|
|
}
|
|
const component = await mount(Test, fixture);
|
|
expect(fixture.innerHTML).toBe("<div>hey1</div>");
|
|
component.state.value++;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div>hey2</div>");
|
|
|
|
expect(steps).toEqual(["hook:willPatch2", "hook:willPatch1", "hook:patched1", "hook:patched2"]);
|
|
});
|
|
|
|
test("mounted callbacks should be called in reverse order from willUnmount callbacks", async () => {
|
|
const steps: string[] = [];
|
|
function useMyHook(i: number) {
|
|
onMounted(() => {
|
|
steps.push("hook:mounted" + i);
|
|
});
|
|
onWillUnmount(() => {
|
|
steps.push("hook:willUnmount" + i);
|
|
});
|
|
}
|
|
class Test extends Component {
|
|
static template = xml`<div>hey<t t-esc="state.value"/></div>`;
|
|
state = useState({ value: 1 });
|
|
setup() {
|
|
useMyHook(1);
|
|
useMyHook(2);
|
|
}
|
|
}
|
|
const app = new App(Test);
|
|
await app.mount(fixture);
|
|
app.destroy();
|
|
expect(steps).toEqual([
|
|
"hook:mounted2",
|
|
"hook:mounted1",
|
|
"hook:willUnmount1",
|
|
"hook:willUnmount2",
|
|
]);
|
|
});
|
|
|
|
describe("autofocus hook", () => {
|
|
function useAutofocus(name: string) {
|
|
let ref = useRef(name);
|
|
let isInDom = false;
|
|
function updateFocus() {
|
|
if (!isInDom && ref.el) {
|
|
isInDom = true;
|
|
ref.el.focus();
|
|
} else if (isInDom && !ref.el) {
|
|
isInDom = false;
|
|
}
|
|
}
|
|
onPatched(updateFocus);
|
|
onMounted(updateFocus);
|
|
}
|
|
|
|
test("simple input", async () => {
|
|
class Test extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<input t-ref="input1"/>
|
|
<input t-ref="input2"/>
|
|
</div>`;
|
|
|
|
setup() {
|
|
useAutofocus("input2");
|
|
}
|
|
}
|
|
|
|
await mount(Test, fixture);
|
|
expect(fixture.innerHTML).toBe("<div><input><input></div>");
|
|
const input2 = fixture.querySelectorAll("input")[1];
|
|
expect(input2).toBe(document.activeElement);
|
|
});
|
|
|
|
test("input in a t-if", async () => {
|
|
class Test extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<input t-ref="input1"/>
|
|
<t t-if="state.flag"><input t-ref="input2"/></t>
|
|
</div>`;
|
|
|
|
state = useState({ flag: false });
|
|
setup() {
|
|
useAutofocus("input2");
|
|
}
|
|
}
|
|
|
|
const component = await mount(Test, fixture);
|
|
expect(fixture.innerHTML).toBe("<div><input></div>");
|
|
expect(document.activeElement).toBe(document.body);
|
|
|
|
component.state.flag = true;
|
|
await nextTick();
|
|
const input2 = fixture.querySelectorAll("input")[1];
|
|
expect(input2).toBe(document.activeElement);
|
|
});
|
|
});
|
|
|
|
test.skip("can use useEnv", async () => {
|
|
expect.assertions(2);
|
|
class Test extends Component {
|
|
static template = xml`<div><t t-esc="env.val"/></div>`;
|
|
setup() {
|
|
//expect(useEnv()).toBe(this.env);
|
|
}
|
|
}
|
|
await mount(Test, fixture);
|
|
});
|
|
|
|
test.skip("can use sub env", async () => {
|
|
class Test extends Component {
|
|
static template = xml`<div><t t-esc="env.val"/></div>`;
|
|
setup() {
|
|
//useSubEnv({ val: 3 });
|
|
}
|
|
}
|
|
const component = await mount(Test, fixture);
|
|
expect(fixture.innerHTML).toBe("<div>3</div>");
|
|
expect(component.env).not.toHaveProperty("val");
|
|
expect(component.env).toHaveProperty("val");
|
|
});
|
|
|
|
test("can use useComponent", async () => {
|
|
expect.assertions(2);
|
|
class Test extends Component {
|
|
static template = xml`<div></div>`;
|
|
setup() {
|
|
expect(useComponent()).toBe(this);
|
|
}
|
|
}
|
|
await mount(Test, fixture);
|
|
});
|
|
|
|
test.skip("parent and child env", async () => {
|
|
class Child extends Component {
|
|
static template = xml`<div><t t-esc="env.val"/></div>`;
|
|
super() {
|
|
//useSubEnv({ val: 5 });
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`<t t-esc="env.val"/><Child/>`;
|
|
static components = { Child };
|
|
setup() {
|
|
//useSubEnv({ val: 3 });
|
|
}
|
|
}
|
|
mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe("3<div>5</div>");
|
|
});
|
|
|
|
test("can use onWillStart, onWillUpdateProps", async () => {
|
|
const steps: string[] = [];
|
|
async function slow(): Promise<string> {
|
|
return new Promise((resolve) => {
|
|
setTimeout(() => {
|
|
resolve("slow");
|
|
}, 0);
|
|
});
|
|
}
|
|
function useMyHook() {
|
|
onWillStart(async () => {
|
|
steps.push(await slow());
|
|
steps.push("onWillStart");
|
|
});
|
|
onWillUpdateProps(async (nextProps) => {
|
|
expect(nextProps).toEqual({ value: 2 });
|
|
steps.push(await slow());
|
|
steps.push("onWillUpdateProps");
|
|
});
|
|
}
|
|
function use2ndHook() {
|
|
onWillStart(() => {
|
|
steps.push("on2ndStart");
|
|
});
|
|
onWillUpdateProps((nextProps) => {
|
|
expect(nextProps).toEqual({ value: 2 });
|
|
steps.push("on2ndUpdate");
|
|
});
|
|
}
|
|
class MyComponent extends Component {
|
|
static template = xml`<span><t t-esc="props.value"/></span>`;
|
|
setup() {
|
|
useMyHook();
|
|
use2ndHook();
|
|
}
|
|
}
|
|
class App extends Component {
|
|
static template = xml`<MyComponent value="state.value"/>`;
|
|
static components = { MyComponent };
|
|
state = useState({ value: 1 });
|
|
}
|
|
|
|
const app = await mount(App, fixture);
|
|
expect(fixture.innerHTML).toBe("<span>1</span>");
|
|
|
|
// NOTE: 'on2ndStart' appears first in the list even though
|
|
// the 'use2ndHook' is declared after 'useMyHook'. This is
|
|
// because Promise.all is used to call the callbacks specified
|
|
// in the hooks, which runs them simultaneously.
|
|
// Additionally, 'slow' should be listed before 'onWillStart'
|
|
// because call to `slow` is awaited.
|
|
expect(steps).toEqual(["on2ndStart", "slow", "onWillStart"]);
|
|
|
|
app.state.value = 2;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<span>2</span>");
|
|
expect(steps).toEqual([
|
|
"on2ndStart",
|
|
"slow",
|
|
"onWillStart",
|
|
"on2ndUpdate",
|
|
"slow",
|
|
"onWillUpdateProps",
|
|
]);
|
|
});
|
|
|
|
test.skip("useExternalListener", async () => {
|
|
let n = 0;
|
|
|
|
class MyComponent extends Component {
|
|
static template = xml`<span><t t-esc="props.value"/></span>`;
|
|
setup() {
|
|
//useExternalListener(window as any, "click", this.increment);
|
|
}
|
|
increment() {
|
|
n++;
|
|
}
|
|
}
|
|
class App extends Component {
|
|
static template = xml`<MyComponent t-if="state.flag"/>`;
|
|
static components = { MyComponent };
|
|
state = useState({ flag: false });
|
|
}
|
|
|
|
const app = await mount(App, fixture);
|
|
|
|
expect(n).toBe(0);
|
|
window.dispatchEvent(new Event("click"));
|
|
expect(n).toBe(0);
|
|
app.state.flag = true;
|
|
await nextTick();
|
|
window.dispatchEvent(new Event("click"));
|
|
expect(n).toBe(1);
|
|
app.state.flag = false;
|
|
await nextTick();
|
|
window.dispatchEvent(new Event("click"));
|
|
expect(n).toBe(1);
|
|
});
|
|
});
|