mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
basic project infrastructure
This commit is contained in:
@@ -3,3 +3,16 @@
|
||||
This is a POC, not at all production ready code!!!
|
||||
|
||||
|
||||
## Tests
|
||||
|
||||
npm run test
|
||||
npm run test:watch
|
||||
|
||||
## Build code bundles
|
||||
|
||||
npm run bundle:core
|
||||
npm run bundle:core:watch
|
||||
|
||||
## Demo page
|
||||
|
||||
npm run demo
|
||||
+434
@@ -0,0 +1,434 @@
|
||||
/**
|
||||
* @license almond 0.3.3 Copyright jQuery Foundation and other contributors.
|
||||
* Released under MIT license, http://github.com/requirejs/almond/LICENSE
|
||||
*/
|
||||
//Going sloppy to avoid 'use strict' string cost, but strict practices should
|
||||
//be followed.
|
||||
/*global setTimeout: false */
|
||||
|
||||
var requirejs, require, define;
|
||||
(function (undef) {
|
||||
var main, req, makeMap, handlers,
|
||||
defined = {},
|
||||
waiting = {},
|
||||
config = {},
|
||||
defining = {},
|
||||
hasOwn = Object.prototype.hasOwnProperty,
|
||||
aps = [].slice,
|
||||
jsSuffixRegExp = /\.js$/;
|
||||
|
||||
function hasProp(obj, prop) {
|
||||
return hasOwn.call(obj, prop);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a relative module name, like ./something, normalize it to
|
||||
* a real name that can be mapped to a path.
|
||||
* @param {String} name the relative name
|
||||
* @param {String} baseName a real name that the name arg is relative
|
||||
* to.
|
||||
* @returns {String} normalized name
|
||||
*/
|
||||
function normalize(name, baseName) {
|
||||
var nameParts, nameSegment, mapValue, foundMap, lastIndex,
|
||||
foundI, foundStarMap, starI, i, j, part, normalizedBaseParts,
|
||||
baseParts = baseName && baseName.split("/"),
|
||||
map = config.map,
|
||||
starMap = (map && map['*']) || {};
|
||||
|
||||
//Adjust any relative paths.
|
||||
if (name) {
|
||||
name = name.split('/');
|
||||
lastIndex = name.length - 1;
|
||||
|
||||
// If wanting node ID compatibility, strip .js from end
|
||||
// of IDs. Have to do this here, and not in nameToUrl
|
||||
// because node allows either .js or non .js to map
|
||||
// to same file.
|
||||
if (config.nodeIdCompat && jsSuffixRegExp.test(name[lastIndex])) {
|
||||
name[lastIndex] = name[lastIndex].replace(jsSuffixRegExp, '');
|
||||
}
|
||||
|
||||
// Starts with a '.' so need the baseName
|
||||
if (name[0].charAt(0) === '.' && baseParts) {
|
||||
//Convert baseName to array, and lop off the last part,
|
||||
//so that . matches that 'directory' and not name of the baseName's
|
||||
//module. For instance, baseName of 'one/two/three', maps to
|
||||
//'one/two/three.js', but we want the directory, 'one/two' for
|
||||
//this normalization.
|
||||
normalizedBaseParts = baseParts.slice(0, baseParts.length - 1);
|
||||
name = normalizedBaseParts.concat(name);
|
||||
}
|
||||
|
||||
//start trimDots
|
||||
for (i = 0; i < name.length; i++) {
|
||||
part = name[i];
|
||||
if (part === '.') {
|
||||
name.splice(i, 1);
|
||||
i -= 1;
|
||||
} else if (part === '..') {
|
||||
// If at the start, or previous value is still ..,
|
||||
// keep them so that when converted to a path it may
|
||||
// still work when converted to a path, even though
|
||||
// as an ID it is less than ideal. In larger point
|
||||
// releases, may be better to just kick out an error.
|
||||
if (i === 0 || (i === 1 && name[2] === '..') || name[i - 1] === '..') {
|
||||
continue;
|
||||
} else if (i > 0) {
|
||||
name.splice(i - 1, 2);
|
||||
i -= 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
//end trimDots
|
||||
|
||||
name = name.join('/');
|
||||
}
|
||||
|
||||
//Apply map config if available.
|
||||
if ((baseParts || starMap) && map) {
|
||||
nameParts = name.split('/');
|
||||
|
||||
for (i = nameParts.length; i > 0; i -= 1) {
|
||||
nameSegment = nameParts.slice(0, i).join("/");
|
||||
|
||||
if (baseParts) {
|
||||
//Find the longest baseName segment match in the config.
|
||||
//So, do joins on the biggest to smallest lengths of baseParts.
|
||||
for (j = baseParts.length; j > 0; j -= 1) {
|
||||
mapValue = map[baseParts.slice(0, j).join('/')];
|
||||
|
||||
//baseName segment has config, find if it has one for
|
||||
//this name.
|
||||
if (mapValue) {
|
||||
mapValue = mapValue[nameSegment];
|
||||
if (mapValue) {
|
||||
//Match, update name to the new value.
|
||||
foundMap = mapValue;
|
||||
foundI = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (foundMap) {
|
||||
break;
|
||||
}
|
||||
|
||||
//Check for a star map match, but just hold on to it,
|
||||
//if there is a shorter segment match later in a matching
|
||||
//config, then favor over this star map.
|
||||
if (!foundStarMap && starMap && starMap[nameSegment]) {
|
||||
foundStarMap = starMap[nameSegment];
|
||||
starI = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundMap && foundStarMap) {
|
||||
foundMap = foundStarMap;
|
||||
foundI = starI;
|
||||
}
|
||||
|
||||
if (foundMap) {
|
||||
nameParts.splice(0, foundI, foundMap);
|
||||
name = nameParts.join('/');
|
||||
}
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
function makeRequire(relName, forceSync) {
|
||||
return function () {
|
||||
//A version of a require function that passes a moduleName
|
||||
//value for items that may need to
|
||||
//look up paths relative to the moduleName
|
||||
var args = aps.call(arguments, 0);
|
||||
|
||||
//If first arg is not require('string'), and there is only
|
||||
//one arg, it is the array form without a callback. Insert
|
||||
//a null so that the following concat is correct.
|
||||
if (typeof args[0] !== 'string' && args.length === 1) {
|
||||
args.push(null);
|
||||
}
|
||||
return req.apply(undef, args.concat([relName, forceSync]));
|
||||
};
|
||||
}
|
||||
|
||||
function makeNormalize(relName) {
|
||||
return function (name) {
|
||||
return normalize(name, relName);
|
||||
};
|
||||
}
|
||||
|
||||
function makeLoad(depName) {
|
||||
return function (value) {
|
||||
defined[depName] = value;
|
||||
};
|
||||
}
|
||||
|
||||
function callDep(name) {
|
||||
if (hasProp(waiting, name)) {
|
||||
var args = waiting[name];
|
||||
delete waiting[name];
|
||||
defining[name] = true;
|
||||
main.apply(undef, args);
|
||||
}
|
||||
|
||||
if (!hasProp(defined, name) && !hasProp(defining, name)) {
|
||||
throw new Error('No ' + name);
|
||||
}
|
||||
return defined[name];
|
||||
}
|
||||
|
||||
//Turns a plugin!resource to [plugin, resource]
|
||||
//with the plugin being undefined if the name
|
||||
//did not have a plugin prefix.
|
||||
function splitPrefix(name) {
|
||||
var prefix,
|
||||
index = name ? name.indexOf('!') : -1;
|
||||
if (index > -1) {
|
||||
prefix = name.substring(0, index);
|
||||
name = name.substring(index + 1, name.length);
|
||||
}
|
||||
return [prefix, name];
|
||||
}
|
||||
|
||||
//Creates a parts array for a relName where first part is plugin ID,
|
||||
//second part is resource ID. Assumes relName has already been normalized.
|
||||
function makeRelParts(relName) {
|
||||
return relName ? splitPrefix(relName) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a name map, normalizing the name, and using a plugin
|
||||
* for normalization if necessary. Grabs a ref to plugin
|
||||
* too, as an optimization.
|
||||
*/
|
||||
makeMap = function (name, relParts) {
|
||||
var plugin,
|
||||
parts = splitPrefix(name),
|
||||
prefix = parts[0],
|
||||
relResourceName = relParts[1];
|
||||
|
||||
name = parts[1];
|
||||
|
||||
if (prefix) {
|
||||
prefix = normalize(prefix, relResourceName);
|
||||
plugin = callDep(prefix);
|
||||
}
|
||||
|
||||
//Normalize according
|
||||
if (prefix) {
|
||||
if (plugin && plugin.normalize) {
|
||||
name = plugin.normalize(name, makeNormalize(relResourceName));
|
||||
} else {
|
||||
name = normalize(name, relResourceName);
|
||||
}
|
||||
} else {
|
||||
name = normalize(name, relResourceName);
|
||||
parts = splitPrefix(name);
|
||||
prefix = parts[0];
|
||||
name = parts[1];
|
||||
if (prefix) {
|
||||
plugin = callDep(prefix);
|
||||
}
|
||||
}
|
||||
|
||||
//Using ridiculous property names for space reasons
|
||||
return {
|
||||
f: prefix ? prefix + '!' + name : name, //fullName
|
||||
n: name,
|
||||
pr: prefix,
|
||||
p: plugin
|
||||
};
|
||||
};
|
||||
|
||||
function makeConfig(name) {
|
||||
return function () {
|
||||
return (config && config.config && config.config[name]) || {};
|
||||
};
|
||||
}
|
||||
|
||||
handlers = {
|
||||
require: function (name) {
|
||||
return makeRequire(name);
|
||||
},
|
||||
exports: function (name) {
|
||||
var e = defined[name];
|
||||
if (typeof e !== 'undefined') {
|
||||
return e;
|
||||
} else {
|
||||
return (defined[name] = {});
|
||||
}
|
||||
},
|
||||
module: function (name) {
|
||||
return {
|
||||
id: name,
|
||||
uri: '',
|
||||
exports: defined[name],
|
||||
config: makeConfig(name)
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
main = function (name, deps, callback, relName) {
|
||||
var cjsModule, depName, ret, map, i, relParts,
|
||||
args = [],
|
||||
callbackType = typeof callback,
|
||||
usingExports;
|
||||
|
||||
//Use name if no relName
|
||||
relName = relName || name;
|
||||
relParts = makeRelParts(relName);
|
||||
|
||||
//Call the callback to define the module, if necessary.
|
||||
if (callbackType === 'undefined' || callbackType === 'function') {
|
||||
//Pull out the defined dependencies and pass the ordered
|
||||
//values to the callback.
|
||||
//Default to [require, exports, module] if no deps
|
||||
deps = !deps.length && callback.length ? ['require', 'exports', 'module'] : deps;
|
||||
for (i = 0; i < deps.length; i += 1) {
|
||||
map = makeMap(deps[i], relParts);
|
||||
depName = map.f;
|
||||
|
||||
//Fast path CommonJS standard dependencies.
|
||||
if (depName === "require") {
|
||||
args[i] = handlers.require(name);
|
||||
} else if (depName === "exports") {
|
||||
//CommonJS module spec 1.1
|
||||
args[i] = handlers.exports(name);
|
||||
usingExports = true;
|
||||
} else if (depName === "module") {
|
||||
//CommonJS module spec 1.1
|
||||
cjsModule = args[i] = handlers.module(name);
|
||||
} else if (hasProp(defined, depName) ||
|
||||
hasProp(waiting, depName) ||
|
||||
hasProp(defining, depName)) {
|
||||
args[i] = callDep(depName);
|
||||
} else if (map.p) {
|
||||
map.p.load(map.n, makeRequire(relName, true), makeLoad(depName), {});
|
||||
args[i] = defined[depName];
|
||||
} else {
|
||||
throw new Error(name + ' missing ' + depName);
|
||||
}
|
||||
}
|
||||
|
||||
ret = callback ? callback.apply(defined[name], args) : undefined;
|
||||
|
||||
if (name) {
|
||||
//If setting exports via "module" is in play,
|
||||
//favor that over return value and exports. After that,
|
||||
//favor a non-undefined return value over exports use.
|
||||
if (cjsModule && cjsModule.exports !== undef &&
|
||||
cjsModule.exports !== defined[name]) {
|
||||
defined[name] = cjsModule.exports;
|
||||
} else if (ret !== undef || !usingExports) {
|
||||
//Use the return value from the function.
|
||||
defined[name] = ret;
|
||||
}
|
||||
}
|
||||
} else if (name) {
|
||||
//May just be an object definition for the module. Only
|
||||
//worry about defining if have a module name.
|
||||
defined[name] = callback;
|
||||
}
|
||||
};
|
||||
|
||||
requirejs = require = req = function (deps, callback, relName, forceSync, alt) {
|
||||
if (typeof deps === "string") {
|
||||
if (handlers[deps]) {
|
||||
//callback in this case is really relName
|
||||
return handlers[deps](callback);
|
||||
}
|
||||
//Just return the module wanted. In this scenario, the
|
||||
//deps arg is the module name, and second arg (if passed)
|
||||
//is just the relName.
|
||||
//Normalize module name, if it contains . or ..
|
||||
return callDep(makeMap(deps, makeRelParts(callback)).f);
|
||||
} else if (!deps.splice) {
|
||||
//deps is a config object, not an array.
|
||||
config = deps;
|
||||
if (config.deps) {
|
||||
req(config.deps, config.callback);
|
||||
}
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (callback.splice) {
|
||||
//callback is an array, which means it is a dependency list.
|
||||
//Adjust args if there are dependencies
|
||||
deps = callback;
|
||||
callback = relName;
|
||||
relName = null;
|
||||
} else {
|
||||
deps = undef;
|
||||
}
|
||||
}
|
||||
|
||||
//Support require(['a'])
|
||||
callback = callback || function () {};
|
||||
|
||||
//If relName is a function, it is an errback handler,
|
||||
//so remove it.
|
||||
if (typeof relName === 'function') {
|
||||
relName = forceSync;
|
||||
forceSync = alt;
|
||||
}
|
||||
|
||||
//Simulate async callback;
|
||||
if (forceSync) {
|
||||
main(undef, deps, callback, relName);
|
||||
} else {
|
||||
//Using a non-zero value because of concern for what old browsers
|
||||
//do, and latest browsers "upgrade" to 4 if lower value is used:
|
||||
//http://www.whatwg.org/specs/web-apps/current-work/multipage/timers.html#dom-windowtimers-settimeout:
|
||||
//If want a value immediately, use require('id') instead -- something
|
||||
//that works in almond on the global level, but not guaranteed and
|
||||
//unlikely to work in other AMD implementations.
|
||||
setTimeout(function () {
|
||||
main(undef, deps, callback, relName);
|
||||
}, 4);
|
||||
}
|
||||
|
||||
return req;
|
||||
};
|
||||
|
||||
/**
|
||||
* Just drops the config on the floor, but returns req in case
|
||||
* the config return value is used.
|
||||
*/
|
||||
req.config = function (cfg) {
|
||||
return req(cfg);
|
||||
};
|
||||
|
||||
/**
|
||||
* Expose module registry for debugging and tooling
|
||||
*/
|
||||
requirejs._defined = defined;
|
||||
|
||||
define = function (name, deps, callback) {
|
||||
if (typeof name !== 'string') {
|
||||
throw new Error('See almond README: incorrect module build, no module name');
|
||||
}
|
||||
|
||||
//This module may not have dependencies
|
||||
if (!deps.splice) {
|
||||
//deps is not an array, so probably means
|
||||
//an object literal or factory function for
|
||||
//the value. Adjust args.
|
||||
callback = deps;
|
||||
deps = [];
|
||||
}
|
||||
|
||||
if (!hasProp(defined, name) && !hasProp(waiting, name)) {
|
||||
waiting[name] = [name, deps, callback];
|
||||
}
|
||||
};
|
||||
|
||||
define.amd = {
|
||||
jQuery: true
|
||||
};
|
||||
}());
|
||||
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Odoo Web Core Demo</title>
|
||||
<script src="almond.js"></script>
|
||||
<script src="main.js"></script>
|
||||
<script>
|
||||
require('main');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,26 @@
|
||||
import Widget from "../../src/core/widget";
|
||||
|
||||
const template = `
|
||||
<div t-debug="1">
|
||||
<button t-on-click="increment(-1)">-</button>
|
||||
<span>Value: <t t-esc="state.counter"/></span>
|
||||
<button t-on-click="increment(1)">+</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export default class Counter extends Widget {
|
||||
name = "counter";
|
||||
template = template;
|
||||
state = {
|
||||
counter: 0
|
||||
};
|
||||
|
||||
constructor(parent: Widget, initialState?: number) {
|
||||
super(parent);
|
||||
this.state.counter = initialState || 0;
|
||||
}
|
||||
|
||||
increment(delta: number) {
|
||||
this.updateState({ counter: this.state.counter + delta });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Env } from "../../src/core/widget";
|
||||
import QWeb from "../../src/core/qweb";
|
||||
|
||||
const qweb = new QWeb();
|
||||
|
||||
const env: Env = {
|
||||
qweb: qweb,
|
||||
services: {}
|
||||
};
|
||||
|
||||
export default env;
|
||||
@@ -0,0 +1,12 @@
|
||||
///<amd-module name="main" />
|
||||
|
||||
// import RootWidget from "./RootWidget";
|
||||
import Counter from "./Counter";
|
||||
import env from "./env";
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async function() {
|
||||
const rootWidget = new Counter(null);
|
||||
rootWidget.setEnvironment(env);
|
||||
const mainDiv = document.getElementById("app")!;
|
||||
await rootWidget.mount(mainDiv);
|
||||
});
|
||||
+9
-3
@@ -4,10 +4,12 @@
|
||||
"description": "Core Utils for Odoo Web Client",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "tsc -m amd --lib es2017,dom --target esnext --outfile dist/bundle.js",
|
||||
"build:watch": "tsc --watch -m amd --lib es2017,dom --target esnext --outfile dist/bundle.js",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch"
|
||||
"test:watch": "jest --watch",
|
||||
"bundle:core": "tsc -m es6 --lib es2017,dom --target esnext --outdir dist src/bundles/core.ts && rollup dist/bundles/core.js --file dist/bundle-core.js --format iife",
|
||||
"bundle:core:watch": "nodemon -w src -e ts -x 'npm run bundle:core'",
|
||||
"demo": "cp -r demo/ dist/demo && tsc -m amd --lib es2017,dom --target esnext --outfile dist/demo/main.js demo/src/main.ts",
|
||||
"postdemo": "open dist/demo/index.html"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -22,6 +24,10 @@
|
||||
"devDependencies": {
|
||||
"@types/jest": "^23.3.12",
|
||||
"jest": "^23.6.0",
|
||||
"nodemon": "^1.18.9",
|
||||
"rollup": "^1.1.0",
|
||||
"rollup-plugin-typescript2": "^0.19.0",
|
||||
"source-map-support": "^0.5.10",
|
||||
"ts-jest": "^23.10.5",
|
||||
"typescript": "^3.2.2"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import QWeb from "../core/qweb";
|
||||
import * as utils from "../core/utils";
|
||||
import Widget from "../core/widget";
|
||||
|
||||
|
||||
(<any>window).core = {
|
||||
QWeb,
|
||||
utils,
|
||||
Widget
|
||||
}
|
||||
@@ -75,9 +75,9 @@ export class Context {
|
||||
}
|
||||
}
|
||||
addLine(line: string) {
|
||||
const prefix = new Array(this.indentLevel).join('\t');
|
||||
const prefix = new Array(this.indentLevel).join("\t");
|
||||
const lastChar = line[line.length - 1];
|
||||
const suffix = lastChar !=='}' && lastChar !== '{' ? ';' : '';
|
||||
const suffix = lastChar !== "}" && lastChar !== "{" ? ";" : "";
|
||||
this.code.push(prefix + line + suffix);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import QWeb from "./qweb";
|
||||
|
||||
export interface Env {
|
||||
qweb: QWeb;
|
||||
services: { [key: string]: any };
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export default class Widget {
|
||||
name: string = "widget";
|
||||
template: string = "<div></div>";
|
||||
|
||||
parent: Widget | null;
|
||||
children: Widget[] = [];
|
||||
env: Env | null = null;
|
||||
el: ChildNode | null = null;
|
||||
state: Object = {};
|
||||
refs: { [key: string]: Widget } = {};
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Lifecycle
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
constructor(parent: Widget | null, props?: any) {
|
||||
this.parent = parent;
|
||||
if (parent) {
|
||||
parent.children.push(this);
|
||||
this.setEnvironment(parent.env);
|
||||
}
|
||||
}
|
||||
|
||||
async willStart() {}
|
||||
|
||||
mounted() {}
|
||||
|
||||
willUnmount() {}
|
||||
|
||||
destroyed() {}
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
async mount(target: HTMLElement) {
|
||||
await this.willStart();
|
||||
await this.render();
|
||||
target.appendChild(this.el!);
|
||||
}
|
||||
|
||||
destroy() {}
|
||||
|
||||
setEnvironment(env: Env | null) {
|
||||
this.env = env ? Object.create(env) : null;
|
||||
if (this.env) {
|
||||
this.env.qweb.addTemplate(this.name, this.template);
|
||||
delete this.template;
|
||||
}
|
||||
}
|
||||
|
||||
async updateState(newState: Object) {
|
||||
Object.assign(this.state, newState);
|
||||
await this.render();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
async render() {
|
||||
const fragment = await this.env!.qweb.render(this.name, this);
|
||||
this._setElement(fragment.firstChild!);
|
||||
}
|
||||
|
||||
private _setElement(el: ChildNode) {
|
||||
if (this.el) {
|
||||
this.el.replaceWith(el);
|
||||
}
|
||||
this.el = el;
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import QWeb from "../src/qweb";
|
||||
import { EvalContext } from "../src/qweb";
|
||||
import QWeb from "../src/core/qweb";
|
||||
import { EvalContext } from "../src/core/qweb";
|
||||
|
||||
|
||||
function renderToDOM(t: string, context: EvalContext = {}): DocumentFragment {
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { escape, htmlTrim } from "../src/utils";
|
||||
import { escape, htmlTrim } from "../src/core/utils";
|
||||
|
||||
describe("escape", () => {
|
||||
test("normal strings", () => {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import Widget from "../src/core/widget";
|
||||
import QWeb from "../src/core/qweb";
|
||||
|
||||
function makeWidget(W: typeof Widget): Widget {
|
||||
const env = {
|
||||
qweb: new QWeb(),
|
||||
services: {}
|
||||
};
|
||||
const w = new W(null);
|
||||
w.setEnvironment(env);
|
||||
return w;
|
||||
}
|
||||
|
||||
async function click(el: HTMLElement) {
|
||||
el.click();
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const template = `
|
||||
<div><t t-esc="state.counter"/><button t-on-click="inc">Inc</button></div>
|
||||
`;
|
||||
|
||||
export default class Counter extends Widget {
|
||||
name = "counter";
|
||||
template = template;
|
||||
state = {
|
||||
counter: 0
|
||||
};
|
||||
|
||||
inc() {
|
||||
this.updateState({ counter: this.state.counter + 1 });
|
||||
}
|
||||
}
|
||||
|
||||
describe("basic widget properties", () => {
|
||||
test("has no el after creation", async () => {
|
||||
const widget = makeWidget(Widget);
|
||||
expect(widget.el).toBe(null);
|
||||
});
|
||||
|
||||
test("can be mounted", async () => {
|
||||
const widget = makeWidget(Widget);
|
||||
const target = document.createElement("div");
|
||||
await widget.mount(target);
|
||||
expect(target.innerHTML).toBe("<div></div>");
|
||||
});
|
||||
|
||||
test("can be clicked on and updated", async () => {
|
||||
const counter = makeWidget(Counter);
|
||||
const target = document.createElement("div");
|
||||
await counter.mount(target);
|
||||
expect(target.innerHTML).toBe("<div>0<button>Inc</button></div>");
|
||||
await click((<HTMLElement>counter.el).getElementsByTagName("button")[0]);
|
||||
expect(target.innerHTML).toBe("<div>1<button>Inc</button></div>");
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -17,5 +17,5 @@
|
||||
"strictPropertyInitialization": true,
|
||||
"strictNullChecks": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "tests/**/*.ts", "examples/**/*.ts"]
|
||||
"include": ["src/**/*.ts", "tests/**/*.ts", "demo/**/*.ts"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user