From 1707bd240dbc89d146ba81a6cf62eff29d0576f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Fri, 17 Apr 2020 12:12:02 +0200 Subject: [PATCH] [IMP] add browser bindings to standard environment This could be done by each application, but it does cost only a few lines of code, and it helps standardizing the Owl ecosystem. For example, some library (such as o_spreadsheet) needs to mock side effects, and Odoo also needs to do that, so this prevents duplicated effort. closes #686 --- doc/reference/environment.md | 26 +++++++++++++++++++++++++- src/browser.ts | 23 +++++++++++++++++++++++ src/component/component.ts | 5 +++++ src/component/scheduler.ts | 8 ++++---- src/utils.ts | 8 +++++--- tests/helpers.ts | 4 +++- tests/utils.test.ts | 5 +++++ 7 files changed, 70 insertions(+), 9 deletions(-) create mode 100644 src/browser.ts diff --git a/doc/reference/environment.md b/doc/reference/environment.md index 6891c7ae..1c639a05 100644 --- a/doc/reference/environment.md +++ b/doc/reference/environment.md @@ -6,6 +6,7 @@ - [Setting an Environment](#setting-an-environment) - [Using a sub environment](#using-a-sub-environment) - [Content of an Environment](#content-of-an-environment) +- [Special keys](#special-keys) ## Overview @@ -93,7 +94,7 @@ Some good use cases for additional keys in the environment are: - some configuration keys, - session information, -- generic services (such as doing rpcs, or accessing local storage). +- generic services (such as doing rpcs). Doing it this way means that components are easily testable: we can simply create a test environment with mock services. @@ -125,3 +126,26 @@ async function start() { await app.mount(document.body); } ``` + +## Special Keys + +There are two special key/value added by Owl if not provided in the environment: +the `QWeb` instance and a `browser` object: + +- `qweb` will be set to an empty `QWeb` instance. This is absolutely necessary + for Owl to be able to render anything +- `browser`: this is an object that contains some common access points to the + browser methods with a side effect. This is particularly useful when one want + to test more advanced components, and be able to mock those methods. + +More specifically, the `browser` object contains the following methods and objects: + +- `setTimeout` +- `clearTimeout` +- `setInterval` +- `clearInterval` +- `requestAnimationFrame` +- `random` +- `Date` +- `fetch` +- `localStorage` diff --git a/src/browser.ts b/src/browser.ts new file mode 100644 index 00000000..46797042 --- /dev/null +++ b/src/browser.ts @@ -0,0 +1,23 @@ +export interface Browser { + setTimeout: Window["setTimeout"]; + clearTimeout: Window["clearTimeout"]; + setInterval: Window["setInterval"]; + clearInterval: Window["clearInterval"]; + requestAnimationFrame: Window["requestAnimationFrame"]; + random: Math["random"]; + Date: typeof Date; + fetch: Window["fetch"]; + localStorage: Window["localStorage"]; +} + +export const browser: Browser = { + setTimeout: window.setTimeout.bind(window), + clearTimeout: window.clearTimeout.bind(window), + setInterval: window.setInterval.bind(window), + clearInterval: window.clearInterval.bind(window), + requestAnimationFrame: window.requestAnimationFrame.bind(window), + random: Math.random, + Date: window.Date, + fetch: (window.fetch || (() => {})).bind(window), + localStorage: window.localStorage +}; diff --git a/src/component/component.ts b/src/component/component.ts index 2d495d85..0b3f4a36 100644 --- a/src/component/component.ts +++ b/src/component/component.ts @@ -7,6 +7,7 @@ import { Fiber } from "./fiber"; import "./props_validation"; import { Scheduler, scheduler } from "./scheduler"; import { activateSheet } from "./styles"; +import { Browser, browser } from "../browser"; /** * Owl Component System @@ -34,6 +35,7 @@ import { activateSheet } from "./styles"; */ export interface Env { qweb: QWeb; + browser: Browser; } export type MountPosition = "first-child" | "last-child" | "self"; @@ -157,6 +159,9 @@ export class Component { if (!this.env.qweb) { this.env.qweb = new QWeb(); } + if (!this.env.browser) { + this.env.browser = browser; + } this.env.qweb.on("update", this, () => { if (this.__owl__.isMounted) { this.render(true); diff --git a/src/component/scheduler.ts b/src/component/scheduler.ts index 32629faa..68147856 100644 --- a/src/component/scheduler.ts +++ b/src/component/scheduler.ts @@ -1,4 +1,5 @@ import { Fiber } from "./fiber"; +import { browser } from "../browser"; /** * Owl Scheduler Class @@ -19,9 +20,9 @@ interface Task { export class Scheduler { tasks: Task[] = []; isRunning: boolean = false; - requestAnimationFrame: typeof window.requestAnimationFrame; + requestAnimationFrame: Window["requestAnimationFrame"]; - constructor(requestAnimationFrame) { + constructor(requestAnimationFrame: Window["requestAnimationFrame"]) { this.requestAnimationFrame = requestAnimationFrame; } @@ -109,5 +110,4 @@ export class Scheduler { } } -const raf = window.requestAnimationFrame.bind(window); -export const scheduler = new Scheduler(raf); +export const scheduler = new Scheduler(browser.requestAnimationFrame); diff --git a/src/utils.ts b/src/utils.ts index 11249fea..31772cca 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -10,6 +10,8 @@ * - debounce */ +import { browser } from "./browser"; + export function whenReady(fn?: any) { return new Promise(function(resolve) { if (document.readyState !== "loading") { @@ -44,7 +46,7 @@ export function loadJS(url: string): Promise { } export async function loadFile(url: string): Promise { - const result = await fetch(url); + const result = await browser.fetch(url); if (!result.ok) { throw new Error("Error while fetching xml templates"); } @@ -83,8 +85,8 @@ export function debounce(func: Function, wait: number, immediate?: boolean): Fun } } const callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, wait); + browser.clearTimeout(timeout); + timeout = browser.setTimeout(later, wait); if (callNow) { func.apply(context, args); } diff --git a/tests/helpers.ts b/tests/helpers.ts index cd3483f2..772b3961 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -6,6 +6,7 @@ import { patch } from "../src/vdom"; import "../src/qweb/base_directives"; import "../src/qweb/extensions"; import "../src/component/directive"; +import { browser } from "../src/browser"; // modifies scheduler to make it faster to test components scheduler.requestAnimationFrame = function(callback: FrameRequestCallback) { @@ -75,7 +76,8 @@ export function makeDeferred(): Deferred { export function makeTestEnv(): Env { return { - qweb: new QWeb() + qweb: new QWeb(), + browser: browser }; } diff --git a/tests/utils.test.ts b/tests/utils.test.ts index 4542d9e7..a4e12c86 100644 --- a/tests/utils.test.ts +++ b/tests/utils.test.ts @@ -1,4 +1,5 @@ import { escape, debounce } from "../src/utils"; +import { browser } from "../src/browser"; describe("escape", () => { test("normal strings", () => { @@ -15,6 +16,10 @@ describe("escape", () => { describe("debounce", () => { test("works as expected", () => { jest.useFakeTimers(); + // need to reset them on browser because they were mocked in window, but not + // on browser object + browser.setTimeout = window.setTimeout.bind(window); + browser.clearTimeout = window.clearTimeout.bind(window); let n = 0; let f = debounce(() => n++, 100); expect(n).toBe(0);