Odoo18-Base/addons/web/static/tests/core/reactive.test.js
2025-01-06 10:57:38 +07:00

196 lines
6.6 KiB
JavaScript

import { describe, expect, test } from "@odoo/hoot";
import { EventBus, reactive } from "@odoo/owl";
import { Reactive, effect, withComputedProperties } from "@web/core/utils/reactive";
describe.current.tags("headless");
describe("class", () => {
test("callback registered without Reactive class constructor will not notify", async () => {
// This test exists to showcase why we need the Reactive class
const bus = new EventBus();
class MyReactiveClass {
constructor() {
this.counter = 0;
bus.addEventListener("change", () => this.counter++);
}
}
const obj = reactive(new MyReactiveClass(), () => {
expect.step(`counter: ${obj.counter}`);
});
obj.counter; // initial subscription to counter
obj.counter++;
expect.verifySteps(["counter: 1"]);
bus.trigger("change");
expect(obj.counter).toBe(2);
expect.verifySteps([
// The mutation in the event handler was missed by the reactivity, this is because
// the `this` in the event handler is captured during construction and is not reactive
]);
});
test("callback registered in Reactive class constructor will notify", async () => {
const bus = new EventBus();
class MyReactiveClass extends Reactive {
constructor() {
super();
this.counter = 0;
bus.addEventListener("change", () => this.counter++);
}
}
const obj = reactive(new MyReactiveClass(), () => {
expect.step(`counter: ${obj.counter}`);
});
obj.counter; // initial subscription to counter
obj.counter++;
expect.verifySteps(["counter: 1"]);
bus.trigger("change");
expect(obj.counter).toBe(2);
expect.verifySteps(["counter: 2"]);
});
});
describe("effect", () => {
test("effect runs once immediately", async () => {
const state = reactive({ counter: 0 });
expect.verifySteps([]);
effect(
(state) => {
expect.step(`counter: ${state.counter}`);
},
[state]
);
expect.verifySteps(["counter: 0"]);
});
test("effect runs when reactive deps change", async () => {
const state = reactive({ counter: 0 });
expect.verifySteps([]);
effect(
(state) => {
expect.step(`counter: ${state.counter}`);
},
[state]
);
// effect runs immediately
expect.verifySteps(["counter: 0"]);
state.counter++;
// first mutation runs the effect
expect.verifySteps(["counter: 1"]);
state.counter++;
// subsequent mutations run the effect
expect.verifySteps(["counter: 2"]);
});
test("Original reactive callback is not subscribed to keys observed by effect", async () => {
let reactiveCallCount = 0;
const state = reactive(
{
counter: 0,
},
() => reactiveCallCount++
);
expect.verifySteps([]);
expect(reactiveCallCount).toBe(0);
effect(
(state) => {
expect.step(`counter: ${state.counter}`);
},
[state]
);
expect.verifySteps(["counter: 0"]);
expect(reactiveCallCount).toBe(0, {
message: "did not call the original reactive's callback",
});
state.counter = 1;
expect.verifySteps(["counter: 1"]);
expect(reactiveCallCount).toBe(0, {
message: "did not call the original reactive's callback",
});
state.counter; // subscribe the original reactive
state.counter = 2;
expect.verifySteps(["counter: 2"]);
expect(reactiveCallCount).toBe(1, {
message: "the original callback was called because it is subscribed independently",
});
});
test("mutating keys not observed by the effect doesn't cause it to run", async () => {
const state = reactive({ counter: 0, unobserved: 0 });
effect(
(state) => {
expect.step(`counter: ${state.counter}`);
},
[state]
);
expect.verifySteps(["counter: 0"]);
state.counter = 1;
expect.verifySteps(["counter: 1"]);
state.unobserved = 1;
expect.verifySteps([]);
});
});
describe("withComputedProperties", () => {
test("computed properties are set immediately", async () => {
const source = reactive({ counter: 1 });
const derived = withComputedProperties(reactive({}), [source], {
doubleCounter(source) {
return source.counter * 2;
},
});
expect(derived.doubleCounter).toBe(2);
});
test("computed properties are recomputed when dependencies change", async () => {
const source = reactive({ counter: 1 });
const derived = withComputedProperties(reactive({}), [source], {
doubleCounter(source) {
return source.counter * 2;
},
});
expect(derived.doubleCounter).toBe(2);
source.counter++;
expect(derived.doubleCounter).toBe(4);
});
test("can observe computed properties", async () => {
const source = reactive({ counter: 1 });
const derived = withComputedProperties(reactive({}), [source], {
doubleCounter(source) {
return source.counter * 2;
},
});
const observed = reactive(derived, () => {
expect.step(`doubleCounter: ${observed.doubleCounter}`);
});
observed.doubleCounter; // subscribe to doubleCounter
expect.verifySteps([]);
source.counter++;
expect.verifySteps(["doubleCounter: 4"]);
});
test("computed properties can use nested objects", async () => {
const source = reactive({ subObj: { counter: 1 } });
const derived = withComputedProperties(reactive({}), [source], {
doubleCounter(source) {
return source.subObj.counter * 2;
},
});
const observed = reactive(derived, () => {
expect.step(`doubleCounter: ${observed.doubleCounter}`);
});
observed.doubleCounter; // subscribe to doubleCounter
expect(derived.doubleCounter).toBe(2);
expect.verifySteps([]);
source.subObj.counter++;
expect(derived.doubleCounter).toBe(4);
// reactive gets notified even for computed properties dervied from nested objects
expect.verifySteps(["doubleCounter: 4"]);
});
});