mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9edd31f755 | |||
| c2728c9daf | |||
| 871dad6a13 | |||
| b620502a0f | |||
| 89cb00cc83 | |||
| 56041bc133 | |||
| e788e361c7 | |||
| 9d378b0e7b | |||
| fd3c194525 | |||
| ac9ccb81ca | |||
| 2b5cea944b | |||
| cf8039f643 |
@@ -320,6 +320,28 @@ class ComponentB extends owl.Component {
|
|||||||
|
|
||||||
Note: the props validation code is done by using the [validate utility function](utils.md#validate).
|
Note: the props validation code is done by using the [validate utility function](utils.md#validate).
|
||||||
|
|
||||||
|
### `slots` prop
|
||||||
|
|
||||||
|
If a component that uses [slots](slots.md) also lists or validates its props, then
|
||||||
|
you will have to explicitely allow the `slots` prop (with an `Object` type), or
|
||||||
|
allow extra props using the `*` notation mentioned above. This is because slots
|
||||||
|
are provided to a component [as props](slots.md#slots-and-props).
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class MyComponent extends Component {
|
||||||
|
static props = [someProp, slots?];
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyComponentWithValidation extends Component {
|
||||||
|
static props = {
|
||||||
|
someProp: {type: Number, optional: true},
|
||||||
|
slots : {type: Object, optional: true},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Good Practices
|
## Good Practices
|
||||||
|
|
||||||
A `props` object is a collection of values that come from the parent. As such,
|
A `props` object is a collection of values that come from the parent. As such,
|
||||||
|
|||||||
@@ -193,6 +193,15 @@ The first `t-out` will act as a `t-esc` directive, which means that the content
|
|||||||
of `value1` will be escaped. However, since `value2` has been tagged as a markup,
|
of `value1` will be escaped. However, since `value2` has been tagged as a markup,
|
||||||
this will be injected as html.
|
this will be injected as html.
|
||||||
|
|
||||||
|
`markup` can also be used as a tag function, allowing the interpolated values to
|
||||||
|
be safely escaped:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const maliciousInput = "<script>alert('💥💥')</script>";
|
||||||
|
// <b><script>alert('💥💥')</script></b>
|
||||||
|
const value = markup`<b>${maliciousInput}</b>`;
|
||||||
|
```
|
||||||
|
|
||||||
### Setting Variables
|
### Setting Variables
|
||||||
|
|
||||||
QWeb allows creating variables from within the template, to memoize a computation (to use it multiple times), give a piece of data a clearer name, ...
|
QWeb allows creating variables from within the template, to memoize a computation (to use it multiple times), give a piece of data a clearer name, ...
|
||||||
|
|||||||
+82
-27
@@ -276,13 +276,39 @@ function inOwnerDocument(el) {
|
|||||||
const rootNode = el.getRootNode();
|
const rootNode = el.getRootNode();
|
||||||
return rootNode instanceof ShadowRoot && el.ownerDocument.contains(rootNode.host);
|
return rootNode instanceof ShadowRoot && el.ownerDocument.contains(rootNode.host);
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Determine whether the given element is contained in a specific root documnet:
|
||||||
|
* either directly or with a shadow root in between or in an iframe.
|
||||||
|
*/
|
||||||
|
function isAttachedToDocument(element, documentElement) {
|
||||||
|
let current = element;
|
||||||
|
const shadowRoot = documentElement.defaultView.ShadowRoot;
|
||||||
|
while (current) {
|
||||||
|
if (current === documentElement) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (current.parentNode) {
|
||||||
|
current = current.parentNode;
|
||||||
|
}
|
||||||
|
else if (current instanceof shadowRoot && current.host) {
|
||||||
|
current = current.host;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
function validateTarget(target) {
|
function validateTarget(target) {
|
||||||
// Get the document and HTMLElement corresponding to the target to allow mounting in iframes
|
// Get the document and HTMLElement corresponding to the target to allow mounting in iframes
|
||||||
const document = target && target.ownerDocument;
|
const document = target && target.ownerDocument;
|
||||||
if (document) {
|
if (document) {
|
||||||
|
if (!document.defaultView) {
|
||||||
|
throw new OwlError("Cannot mount a component: the target document is not attached to a window (defaultView is missing)");
|
||||||
|
}
|
||||||
const HTMLElement = document.defaultView.HTMLElement;
|
const HTMLElement = document.defaultView.HTMLElement;
|
||||||
if (target instanceof HTMLElement || target instanceof ShadowRoot) {
|
if (target instanceof HTMLElement || target instanceof ShadowRoot) {
|
||||||
if (!document.body.contains(target instanceof HTMLElement ? target : target.host)) {
|
if (!isAttachedToDocument(target, document)) {
|
||||||
throw new OwlError("Cannot mount a component on a detached dom node");
|
throw new OwlError("Cannot mount a component on a detached dom node");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -319,12 +345,40 @@ async function loadFile(url) {
|
|||||||
*/
|
*/
|
||||||
class Markup extends String {
|
class Markup extends String {
|
||||||
}
|
}
|
||||||
/*
|
function htmlEscape(str) {
|
||||||
* Marks a value as safe, that is, a value that can be injected as HTML directly.
|
if (str instanceof Markup) {
|
||||||
* It should be used to wrap the value passed to a t-out directive to allow a raw rendering.
|
return str;
|
||||||
*/
|
}
|
||||||
function markup(value) {
|
if (str === undefined) {
|
||||||
return new Markup(value);
|
return markup("");
|
||||||
|
}
|
||||||
|
if (typeof str === "number") {
|
||||||
|
return markup(String(str));
|
||||||
|
}
|
||||||
|
[
|
||||||
|
["&", "&"],
|
||||||
|
["<", "<"],
|
||||||
|
[">", ">"],
|
||||||
|
["'", "'"],
|
||||||
|
['"', """],
|
||||||
|
["`", "`"],
|
||||||
|
].forEach((pairs) => {
|
||||||
|
str = String(str).replace(new RegExp(pairs[0], "g"), pairs[1]);
|
||||||
|
});
|
||||||
|
return markup(str);
|
||||||
|
}
|
||||||
|
function markup(valueOrStrings, ...placeholders) {
|
||||||
|
if (!Array.isArray(valueOrStrings)) {
|
||||||
|
return new Markup(valueOrStrings);
|
||||||
|
}
|
||||||
|
const strings = valueOrStrings;
|
||||||
|
let acc = "";
|
||||||
|
let i = 0;
|
||||||
|
for (; i < placeholders.length; ++i) {
|
||||||
|
acc += strings[i] + htmlEscape(placeholders[i]);
|
||||||
|
}
|
||||||
|
acc += strings[i];
|
||||||
|
return new Markup(acc);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEventHandler(rawEvent) {
|
function createEventHandler(rawEvent) {
|
||||||
@@ -3796,7 +3850,16 @@ class CodeTarget {
|
|||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
|
const TRANSLATABLE_ATTRS = [
|
||||||
|
"alt",
|
||||||
|
"aria-label",
|
||||||
|
"aria-placeholder",
|
||||||
|
"aria-roledescription",
|
||||||
|
"aria-valuetext",
|
||||||
|
"label",
|
||||||
|
"placeholder",
|
||||||
|
"title",
|
||||||
|
];
|
||||||
const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
|
const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
|
||||||
class CodeGenerator {
|
class CodeGenerator {
|
||||||
constructor(ast, options) {
|
constructor(ast, options) {
|
||||||
@@ -4801,16 +4864,15 @@ class CodeGenerator {
|
|||||||
isMultiple = isMultiple || this.slotNames.has(ast.name);
|
isMultiple = isMultiple || this.slotNames.has(ast.name);
|
||||||
this.slotNames.add(ast.name);
|
this.slotNames.add(ast.name);
|
||||||
}
|
}
|
||||||
const dynProps = ast.attrs ? ast.attrs["t-props"] : null;
|
const attrs = { ...ast.attrs };
|
||||||
if (ast.attrs) {
|
const dynProps = attrs["t-props"];
|
||||||
delete ast.attrs["t-props"];
|
delete attrs["t-props"];
|
||||||
}
|
|
||||||
let key = this.target.loopLevel ? `key${this.target.loopLevel}` : "key";
|
let key = this.target.loopLevel ? `key${this.target.loopLevel}` : "key";
|
||||||
if (isMultiple) {
|
if (isMultiple) {
|
||||||
key = this.generateComponentKey(key);
|
key = this.generateComponentKey(key);
|
||||||
}
|
}
|
||||||
const props = ast.attrs
|
const props = ast.attrs
|
||||||
? this.formatPropObject(ast.attrs, ast.attrsTranslationCtx, ctx.translationCtx)
|
? this.formatPropObject(attrs, ast.attrsTranslationCtx, ctx.translationCtx)
|
||||||
: [];
|
: [];
|
||||||
const scope = this.getPropString(props, dynProps);
|
const scope = this.getPropString(props, dynProps);
|
||||||
if (ast.defaultContent) {
|
if (ast.defaultContent) {
|
||||||
@@ -4915,11 +4977,11 @@ function parseNode(node, ctx) {
|
|||||||
parseTPortal(node, ctx) ||
|
parseTPortal(node, ctx) ||
|
||||||
parseTCall(node, ctx) ||
|
parseTCall(node, ctx) ||
|
||||||
parseTCallBlock(node) ||
|
parseTCallBlock(node) ||
|
||||||
|
parseTTranslation(node, ctx) ||
|
||||||
|
parseTTranslationContext(node, ctx) ||
|
||||||
parseTEscNode(node, ctx) ||
|
parseTEscNode(node, ctx) ||
|
||||||
parseTOutNode(node, ctx) ||
|
parseTOutNode(node, ctx) ||
|
||||||
parseTKey(node, ctx) ||
|
parseTKey(node, ctx) ||
|
||||||
parseTTranslation(node, ctx) ||
|
|
||||||
parseTTranslationContext(node, ctx) ||
|
|
||||||
parseTSlot(node, ctx) ||
|
parseTSlot(node, ctx) ||
|
||||||
parseComponent(node, ctx) ||
|
parseComponent(node, ctx) ||
|
||||||
parseDOMNode(node, ctx) ||
|
parseDOMNode(node, ctx) ||
|
||||||
@@ -5705,7 +5767,7 @@ function compile(template, options = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// do not modify manually. This file is generated by the release script.
|
// do not modify manually. This file is generated by the release script.
|
||||||
const version = "2.6.0";
|
const version = "2.8.0";
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Scheduler
|
// Scheduler
|
||||||
@@ -5800,13 +5862,6 @@ class Scheduler {
|
|||||||
Scheduler.requestAnimationFrame = window.requestAnimationFrame.bind(window);
|
Scheduler.requestAnimationFrame = window.requestAnimationFrame.bind(window);
|
||||||
|
|
||||||
let hasBeenLogged = false;
|
let hasBeenLogged = false;
|
||||||
const DEV_MSG = () => {
|
|
||||||
const hash = window.owl ? window.owl.__info__.hash : "master";
|
|
||||||
return `Owl is running in 'dev' mode.
|
|
||||||
|
|
||||||
This is not suitable for production use.
|
|
||||||
See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration for more information.`;
|
|
||||||
};
|
|
||||||
const apps = new Set();
|
const apps = new Set();
|
||||||
window.__OWL_DEVTOOLS__ || (window.__OWL_DEVTOOLS__ = { apps, Fiber, RootFiber, toRaw, reactive });
|
window.__OWL_DEVTOOLS__ || (window.__OWL_DEVTOOLS__ = { apps, Fiber, RootFiber, toRaw, reactive });
|
||||||
class App extends TemplateSet {
|
class App extends TemplateSet {
|
||||||
@@ -5823,7 +5878,7 @@ class App extends TemplateSet {
|
|||||||
}
|
}
|
||||||
this.warnIfNoStaticProps = config.warnIfNoStaticProps || false;
|
this.warnIfNoStaticProps = config.warnIfNoStaticProps || false;
|
||||||
if (this.dev && !config.test && !hasBeenLogged) {
|
if (this.dev && !config.test && !hasBeenLogged) {
|
||||||
console.info(DEV_MSG());
|
console.info(`Owl is running in 'dev' mode.`);
|
||||||
hasBeenLogged = true;
|
hasBeenLogged = true;
|
||||||
}
|
}
|
||||||
const env = config.env || {};
|
const env = config.env || {};
|
||||||
@@ -6180,9 +6235,9 @@ TemplateSet.prototype._compileTemplate = function _compileTemplate(name, templat
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export { App, Component, EventBus, OwlError, __info__, batched, blockDom, loadFile, markRaw, markup, mount, onError, onMounted, onPatched, onRendered, onWillDestroy, onWillPatch, onWillRender, onWillStart, onWillUnmount, onWillUpdateProps, reactive, status, toRaw, useChildSubEnv, useComponent, useEffect, useEnv, useExternalListener, useRef, useState, useSubEnv, validate, validateType, whenReady, xml };
|
export { App, Component, EventBus, OwlError, __info__, batched, blockDom, htmlEscape, loadFile, markRaw, markup, mount, onError, onMounted, onPatched, onRendered, onWillDestroy, onWillPatch, onWillRender, onWillStart, onWillUnmount, onWillUpdateProps, reactive, status, toRaw, useChildSubEnv, useComponent, useEffect, useEnv, useExternalListener, useRef, useState, useSubEnv, validate, validateType, whenReady, xml };
|
||||||
|
|
||||||
|
|
||||||
__info__.date = '2025-01-15T10:40:24.184Z';
|
__info__.date = '2025-06-30T12:46:06.424Z';
|
||||||
__info__.hash = 'a9be149';
|
__info__.hash = 'b620502';
|
||||||
__info__.url = 'https://github.com/odoo/owl';
|
__info__.url = 'https://github.com/odoo/owl';
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@odoo/owl",
|
"name": "@odoo/owl",
|
||||||
"version": "2.6.0",
|
"version": "2.8.0",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@odoo/owl",
|
"name": "@odoo/owl",
|
||||||
"version": "2.6.0",
|
"version": "2.8.0",
|
||||||
"description": "Odoo Web Library (OWL)",
|
"description": "Odoo Web Library (OWL)",
|
||||||
"main": "dist/owl.cjs.js",
|
"main": "dist/owl.cjs.js",
|
||||||
"module": "dist/owl.es.js",
|
"module": "dist/owl.es.js",
|
||||||
|
|||||||
@@ -254,7 +254,16 @@ class CodeTarget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
|
const TRANSLATABLE_ATTRS = [
|
||||||
|
"alt",
|
||||||
|
"aria-label",
|
||||||
|
"aria-placeholder",
|
||||||
|
"aria-roledescription",
|
||||||
|
"aria-valuetext",
|
||||||
|
"label",
|
||||||
|
"placeholder",
|
||||||
|
"title",
|
||||||
|
];
|
||||||
const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
|
const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
|
||||||
|
|
||||||
export class CodeGenerator {
|
export class CodeGenerator {
|
||||||
@@ -1350,17 +1359,16 @@ export class CodeGenerator {
|
|||||||
isMultiple = isMultiple || this.slotNames.has(ast.name);
|
isMultiple = isMultiple || this.slotNames.has(ast.name);
|
||||||
this.slotNames.add(ast.name);
|
this.slotNames.add(ast.name);
|
||||||
}
|
}
|
||||||
const dynProps = ast.attrs ? ast.attrs["t-props"] : null;
|
const attrs = { ...ast.attrs };
|
||||||
if (ast.attrs) {
|
const dynProps = attrs["t-props"];
|
||||||
delete ast.attrs["t-props"];
|
delete attrs["t-props"];
|
||||||
}
|
|
||||||
let key = this.target.loopLevel ? `key${this.target.loopLevel}` : "key";
|
let key = this.target.loopLevel ? `key${this.target.loopLevel}` : "key";
|
||||||
if (isMultiple) {
|
if (isMultiple) {
|
||||||
key = this.generateComponentKey(key);
|
key = this.generateComponentKey(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = ast.attrs
|
const props = ast.attrs
|
||||||
? this.formatPropObject(ast.attrs, ast.attrsTranslationCtx, ctx.translationCtx)
|
? this.formatPropObject(attrs, ast.attrsTranslationCtx, ctx.translationCtx)
|
||||||
: [];
|
: [];
|
||||||
const scope = this.getPropString(props, dynProps);
|
const scope = this.getPropString(props, dynProps);
|
||||||
if (ast.defaultContent) {
|
if (ast.defaultContent) {
|
||||||
|
|||||||
@@ -253,11 +253,11 @@ function parseNode(node: Node, ctx: ParsingContext): AST | null {
|
|||||||
parseTPortal(node, ctx) ||
|
parseTPortal(node, ctx) ||
|
||||||
parseTCall(node, ctx) ||
|
parseTCall(node, ctx) ||
|
||||||
parseTCallBlock(node, ctx) ||
|
parseTCallBlock(node, ctx) ||
|
||||||
|
parseTTranslation(node, ctx) ||
|
||||||
|
parseTTranslationContext(node, ctx) ||
|
||||||
parseTEscNode(node, ctx) ||
|
parseTEscNode(node, ctx) ||
|
||||||
parseTOutNode(node, ctx) ||
|
parseTOutNode(node, ctx) ||
|
||||||
parseTKey(node, ctx) ||
|
parseTKey(node, ctx) ||
|
||||||
parseTTranslation(node, ctx) ||
|
|
||||||
parseTTranslationContext(node, ctx) ||
|
|
||||||
parseTSlot(node, ctx) ||
|
parseTSlot(node, ctx) ||
|
||||||
parseComponent(node, ctx) ||
|
parseComponent(node, ctx) ||
|
||||||
parseDOMNode(node, ctx) ||
|
parseDOMNode(node, ctx) ||
|
||||||
|
|||||||
+1
-10
@@ -29,15 +29,6 @@ export interface AppConfig<P, E> extends TemplateSetConfig, RootConfig<P, E> {
|
|||||||
|
|
||||||
let hasBeenLogged = false;
|
let hasBeenLogged = false;
|
||||||
|
|
||||||
export const DEV_MSG = () => {
|
|
||||||
const hash = (window as any).owl ? (window as any).owl.__info__.hash : "master";
|
|
||||||
|
|
||||||
return `Owl is running in 'dev' mode.
|
|
||||||
|
|
||||||
This is not suitable for production use.
|
|
||||||
See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration for more information.`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const apps = new Set<App>();
|
const apps = new Set<App>();
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -88,7 +79,7 @@ export class App<
|
|||||||
}
|
}
|
||||||
this.warnIfNoStaticProps = config.warnIfNoStaticProps || false;
|
this.warnIfNoStaticProps = config.warnIfNoStaticProps || false;
|
||||||
if (this.dev && !config.test && !hasBeenLogged) {
|
if (this.dev && !config.test && !hasBeenLogged) {
|
||||||
console.info(DEV_MSG());
|
console.info(`Owl is running in 'dev' mode.`);
|
||||||
hasBeenLogged = true;
|
hasBeenLogged = true;
|
||||||
}
|
}
|
||||||
const env = config.env || {};
|
const env = config.env || {};
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export { useComponent, useState } from "./component_node";
|
|||||||
export { status } from "./status";
|
export { status } from "./status";
|
||||||
export { reactive, markRaw, toRaw } from "./reactivity";
|
export { reactive, markRaw, toRaw } from "./reactivity";
|
||||||
export { useEffect, useEnv, useExternalListener, useRef, useChildSubEnv, useSubEnv } from "./hooks";
|
export { useEffect, useEnv, useExternalListener, useRef, useChildSubEnv, useSubEnv } from "./hooks";
|
||||||
export { batched, EventBus, whenReady, loadFile, markup } from "./utils";
|
export { batched, EventBus, htmlEscape, whenReady, loadFile, markup } from "./utils";
|
||||||
export {
|
export {
|
||||||
onWillStart,
|
onWillStart,
|
||||||
onMounted,
|
onMounted,
|
||||||
|
|||||||
+117
-4
@@ -1,4 +1,5 @@
|
|||||||
import { OwlError } from "../common/owl_error";
|
import { OwlError } from "../common/owl_error";
|
||||||
|
import { ComponentNode, getCurrent } from "./component_node";
|
||||||
export type Callback = () => void;
|
export type Callback = () => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,13 +36,43 @@ export function inOwnerDocument(el?: HTMLElement) {
|
|||||||
return rootNode instanceof ShadowRoot && el.ownerDocument.contains(rootNode.host);
|
return rootNode instanceof ShadowRoot && el.ownerDocument.contains(rootNode.host);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the given element is contained in a specific root documnet:
|
||||||
|
* either directly or with a shadow root in between or in an iframe.
|
||||||
|
*/
|
||||||
|
function isAttachedToDocument(
|
||||||
|
element: HTMLElement | ShadowRoot,
|
||||||
|
documentElement: Document
|
||||||
|
): boolean {
|
||||||
|
let current: Node = element;
|
||||||
|
const shadowRoot = documentElement.defaultView!.ShadowRoot;
|
||||||
|
while (current) {
|
||||||
|
if (current === documentElement) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (current.parentNode) {
|
||||||
|
current = current.parentNode;
|
||||||
|
} else if (current instanceof shadowRoot && current.host) {
|
||||||
|
current = current.host;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export function validateTarget(target: HTMLElement | ShadowRoot) {
|
export function validateTarget(target: HTMLElement | ShadowRoot) {
|
||||||
// Get the document and HTMLElement corresponding to the target to allow mounting in iframes
|
// Get the document and HTMLElement corresponding to the target to allow mounting in iframes
|
||||||
const document = target && target.ownerDocument;
|
const document = target && target.ownerDocument;
|
||||||
if (document) {
|
if (document) {
|
||||||
const HTMLElement = document.defaultView!.HTMLElement;
|
if (!document.defaultView) {
|
||||||
|
throw new OwlError(
|
||||||
|
"Cannot mount a component: the target document is not attached to a window (defaultView is missing)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const HTMLElement = document.defaultView.HTMLElement;
|
||||||
if (target instanceof HTMLElement || target instanceof ShadowRoot) {
|
if (target instanceof HTMLElement || target instanceof ShadowRoot) {
|
||||||
if (!document.body.contains(target instanceof HTMLElement ? target : target.host)) {
|
if (!isAttachedToDocument(target, document)) {
|
||||||
throw new OwlError("Cannot mount a component on a detached dom node");
|
throw new OwlError("Cannot mount a component on a detached dom node");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -51,10 +82,52 @@ export function validateTarget(target: HTMLElement | ShadowRoot) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class EventBus extends EventTarget {
|
export class EventBus extends EventTarget {
|
||||||
|
constructor(events?: string[]) {
|
||||||
|
if (events) {
|
||||||
|
let node: ComponentNode | null = null;
|
||||||
|
try {
|
||||||
|
node = getCurrent();
|
||||||
|
} catch {}
|
||||||
|
if (node?.app?.dev) {
|
||||||
|
return new DebugEventBus(events);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super();
|
||||||
|
}
|
||||||
trigger(name: string, payload?: any) {
|
trigger(name: string, payload?: any) {
|
||||||
this.dispatchEvent(new CustomEvent(name, { detail: payload }));
|
this.dispatchEvent(new CustomEvent(name, { detail: payload }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
class DebugEventBus extends EventBus {
|
||||||
|
private events: Set<string>;
|
||||||
|
constructor(events: string[]) {
|
||||||
|
super();
|
||||||
|
this.events = new Set(events);
|
||||||
|
}
|
||||||
|
addEventListener(
|
||||||
|
type: string,
|
||||||
|
listener: EventListenerOrEventListenerObject | null,
|
||||||
|
options?: boolean | AddEventListenerOptions
|
||||||
|
): void {
|
||||||
|
if (!this.events.has(type)) {
|
||||||
|
throw new OwlError(`EventBus: subscribing to unknown event '${type}'`);
|
||||||
|
}
|
||||||
|
super.addEventListener(type, listener, options);
|
||||||
|
}
|
||||||
|
trigger(name: string, payload?: any) {
|
||||||
|
if (!this.events.has(name)) {
|
||||||
|
throw new OwlError(`EventBus: triggering unknown event '${name}'`);
|
||||||
|
}
|
||||||
|
super.trigger(name, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchEvent(event: Event): boolean {
|
||||||
|
if (!this.events.has(event.type)) {
|
||||||
|
throw new OwlError(`EventBus: dispatching unknown event '${event.type}'`);
|
||||||
|
}
|
||||||
|
return super.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function whenReady(fn?: any): Promise<void> {
|
export function whenReady(fn?: any): Promise<void> {
|
||||||
return new Promise(function (resolve) {
|
return new Promise(function (resolve) {
|
||||||
@@ -81,10 +154,50 @@ export async function loadFile(url: string): Promise<string> {
|
|||||||
*/
|
*/
|
||||||
export class Markup extends String {}
|
export class Markup extends String {}
|
||||||
|
|
||||||
|
export function htmlEscape(str: any): Markup {
|
||||||
|
if (str instanceof Markup) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
if (str === undefined) {
|
||||||
|
return markup("");
|
||||||
|
}
|
||||||
|
if (typeof str === "number") {
|
||||||
|
return markup(String(str));
|
||||||
|
}
|
||||||
|
[
|
||||||
|
["&", "&"],
|
||||||
|
["<", "<"],
|
||||||
|
[">", ">"],
|
||||||
|
["'", "'"],
|
||||||
|
['"', """],
|
||||||
|
["`", "`"],
|
||||||
|
].forEach((pairs) => {
|
||||||
|
str = String(str).replace(new RegExp(pairs[0], "g"), pairs[1]);
|
||||||
|
});
|
||||||
|
return markup(str);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Marks a value as safe, that is, a value that can be injected as HTML directly.
|
* Marks a value as safe, that is, a value that can be injected as HTML directly.
|
||||||
* It should be used to wrap the value passed to a t-out directive to allow a raw rendering.
|
* It should be used to wrap the value passed to a t-out directive to allow a raw rendering.
|
||||||
|
*
|
||||||
|
* If called as a tag function, the interpolated strings are escaped.
|
||||||
*/
|
*/
|
||||||
export function markup(value: any) {
|
export function markup(strings: TemplateStringsArray, ...placeholders: unknown[]): Markup;
|
||||||
return new Markup(value);
|
export function markup(value: string): Markup;
|
||||||
|
export function markup(
|
||||||
|
valueOrStrings: string | TemplateStringsArray,
|
||||||
|
...placeholders: unknown[]
|
||||||
|
): Markup {
|
||||||
|
if (!Array.isArray(valueOrStrings)) {
|
||||||
|
return new Markup(valueOrStrings);
|
||||||
|
}
|
||||||
|
const strings = valueOrStrings;
|
||||||
|
let acc = "";
|
||||||
|
let i = 0;
|
||||||
|
for (; i < placeholders.length; ++i) {
|
||||||
|
acc += strings[i] + htmlEscape(placeholders[i]);
|
||||||
|
}
|
||||||
|
acc += strings[i];
|
||||||
|
return new Markup(acc);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
// do not modify manually. This file is generated by the release script.
|
// do not modify manually. This file is generated by the release script.
|
||||||
export const version = "2.6.0";
|
export const version = "2.8.0";
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`t-slot compile t-props correctly multiple time 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
let { callSlot } = helpers;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return callSlot(ctx, node, key, 'default', false, Object.assign({}, {a:1}));
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
@@ -1971,6 +1971,54 @@ describe("qweb parser", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('t-translation="off": interaction with t-esc', async () => {
|
||||||
|
expect(parse(`<span t-esc="a" t-translation="off"/>`)).toEqual({
|
||||||
|
type: ASTType.TTranslation,
|
||||||
|
content: {
|
||||||
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
defaultValue: "",
|
||||||
|
expr: "a",
|
||||||
|
type: ASTType.TEsc,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
dynamicTag: null,
|
||||||
|
model: null,
|
||||||
|
ns: null,
|
||||||
|
on: null,
|
||||||
|
ref: null,
|
||||||
|
tag: "span",
|
||||||
|
type: ASTType.DomNode,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('t-translation="off": interaction with t-out', async () => {
|
||||||
|
expect(parse(`<span t-out="a" t-translation="off"/>`)).toEqual({
|
||||||
|
type: ASTType.TTranslation,
|
||||||
|
content: {
|
||||||
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
body: null,
|
||||||
|
expr: "a",
|
||||||
|
type: ASTType.TOut,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
dynamicTag: null,
|
||||||
|
model: null,
|
||||||
|
ns: null,
|
||||||
|
on: null,
|
||||||
|
ref: null,
|
||||||
|
tag: "span",
|
||||||
|
type: ASTType.DomNode,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// t-translation-context
|
// t-translation-context
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -2008,6 +2056,56 @@ describe("qweb parser", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("t-translation-context: interaction with t-esc", async () => {
|
||||||
|
expect(parse(`<span t-esc="a" t-translation-context="fr"/>`)).toEqual({
|
||||||
|
type: ASTType.TTranslationContext,
|
||||||
|
content: {
|
||||||
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
defaultValue: "",
|
||||||
|
expr: "a",
|
||||||
|
type: ASTType.TEsc,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
dynamicTag: null,
|
||||||
|
model: null,
|
||||||
|
ns: null,
|
||||||
|
on: null,
|
||||||
|
ref: null,
|
||||||
|
tag: "span",
|
||||||
|
type: ASTType.DomNode,
|
||||||
|
},
|
||||||
|
translationCtx: "fr",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("t-translation-context: interaction with t-out", async () => {
|
||||||
|
expect(parse(`<span t-out="a" t-translation-context="fr"/>`)).toEqual({
|
||||||
|
type: ASTType.TTranslationContext,
|
||||||
|
content: {
|
||||||
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
body: null,
|
||||||
|
expr: "a",
|
||||||
|
type: ASTType.TOut,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
dynamicTag: null,
|
||||||
|
model: null,
|
||||||
|
ns: null,
|
||||||
|
on: null,
|
||||||
|
ref: null,
|
||||||
|
tag: "span",
|
||||||
|
type: ASTType.DomNode,
|
||||||
|
},
|
||||||
|
translationCtx: "fr",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// t-translation-context-attr
|
// t-translation-context-attr
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { parseXML } from "../../src/common/utils";
|
||||||
|
import { compile } from "../../src/compiler";
|
||||||
|
|
||||||
|
describe("t-slot", () => {
|
||||||
|
test("compile t-props correctly multiple time", () => {
|
||||||
|
const template = `<t t-slot="default" t-props="{ a: 1 }"/>`;
|
||||||
|
const parsedTemplate = parseXML(template).firstChild as Element;
|
||||||
|
|
||||||
|
const fn1 = compile(parsedTemplate);
|
||||||
|
expect(fn1.toString()).toMatchSnapshot();
|
||||||
|
|
||||||
|
const fn2 = compile(parsedTemplate);
|
||||||
|
expect(fn2.toString()).toBe(fn1.toString());
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { makeTestFixture, nextAppError, nextTick, snapshotEverything } from "../helpers";
|
import { makeTestFixture, nextAppError, nextTick, snapshotEverything } from "../helpers";
|
||||||
import { Component, onError, xml, mount, OwlError, useState } from "../../src";
|
import { Component, onError, xml, mount, OwlError, useState } from "../../src";
|
||||||
import { App, DEV_MSG } from "../../src/runtime/app";
|
import { App } from "../../src/runtime/app";
|
||||||
import { validateProps } from "../../src/runtime/template_helpers";
|
import { validateProps } from "../../src/runtime/template_helpers";
|
||||||
import { Schema } from "../../src/runtime/validation";
|
import { Schema } from "../../src/runtime/validation";
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ let mockConsoleWarn: any;
|
|||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
console.info = (message: any) => {
|
console.info = (message: any) => {
|
||||||
if (message === DEV_MSG()) {
|
if (message === `Owl is running in 'dev' mode.`) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
info(message);
|
info(message);
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from "../../src";
|
} from "../../src";
|
||||||
import { xml } from "../../src/";
|
import { xml } from "../../src/";
|
||||||
import { DEV_MSG } from "../../src/runtime/app";
|
|
||||||
import { elem, makeTestFixture, nextAppError, nextTick, snapshotEverything } from "../helpers";
|
import { elem, makeTestFixture, nextAppError, nextTick, snapshotEverything } from "../helpers";
|
||||||
|
|
||||||
let fixture: HTMLElement;
|
let fixture: HTMLElement;
|
||||||
@@ -30,7 +29,7 @@ snapshotEverything();
|
|||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
console.info = (message: any) => {
|
console.info = (message: any) => {
|
||||||
if (message === DEV_MSG()) {
|
if (message === `Owl is running in 'dev' mode.`) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
info(message);
|
info(message);
|
||||||
|
|||||||
@@ -27,6 +27,58 @@ exports[`shadow_dom can mount app 1`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`shadow_dom can mount app in closed shadow dom 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div class=\\"my-div\\"/>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`shadow_dom can mount app inside a separate HTML document 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div class=\\"my-div\\"/>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`shadow_dom can mount app inside a shadow child element 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div class=\\"my-div\\"/>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`shadow_dom can mount app inside an element in a shadow root inside an iframe 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div class=\\"my-div\\"/>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`shadow_dom useRef hook 1`] = `
|
exports[`shadow_dom useRef hook 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -29,6 +29,24 @@ describe("shadow_dom", () => {
|
|||||||
expect(status(comp)).toBe("destroyed");
|
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 () => {
|
test("can bind event handler", async () => {
|
||||||
let a = 1;
|
let a = 1;
|
||||||
class SomeComponent extends Component {
|
class SomeComponent extends Component {
|
||||||
@@ -64,4 +82,73 @@ describe("shadow_dom", () => {
|
|||||||
await mountedProm;
|
await mountedProm;
|
||||||
expect(comp!.div.el).toBe(shadow.querySelector(".my-div"));
|
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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+155
-2
@@ -1,5 +1,7 @@
|
|||||||
import { batched, EventBus } from "../src/runtime/utils";
|
import { batched, EventBus, htmlEscape, markup } from "../src/runtime/utils";
|
||||||
import { nextMicroTick } from "./helpers";
|
import { makeTestFixture, nextMicroTick } from "./helpers";
|
||||||
|
import { getCurrent } from "../src/runtime/component_node";
|
||||||
|
import { Component, mount, xml } from "../src";
|
||||||
|
|
||||||
describe("event bus behaviour", () => {
|
describe("event bus behaviour", () => {
|
||||||
test("can subscribe and be notified", () => {
|
test("can subscribe and be notified", () => {
|
||||||
@@ -33,6 +35,66 @@ describe("event bus behaviour", () => {
|
|||||||
bus.addEventListener("event", (ev: any) => expect(ev.detail).toBe("hello world"));
|
bus.addEventListener("event", (ev: any) => expect(ev.detail).toBe("hello world"));
|
||||||
bus.trigger("event", "hello world");
|
bus.trigger("event", "hello world");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("events are not validated if the bus is created outside of dev mode", async () => {
|
||||||
|
let bus_empty: EventBus | null = null;
|
||||||
|
class Root extends Component {
|
||||||
|
static template = xml`<div/>`;
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
getCurrent(); // checks that we're in a component context
|
||||||
|
|
||||||
|
bus_empty = new EventBus([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await mount(Root, makeTestFixture());
|
||||||
|
|
||||||
|
bus_empty!.addEventListener("a", () => {});
|
||||||
|
bus_empty!.trigger("a");
|
||||||
|
bus_empty!.dispatchEvent(new CustomEvent("a"));
|
||||||
|
});
|
||||||
|
test("events are validated if the bus is created in dev mode & events are provided", async () => {
|
||||||
|
let bus: EventBus | null = null;
|
||||||
|
let bus_empty: EventBus | null = null;
|
||||||
|
let bbus_no_validation: EventBus | null = null;
|
||||||
|
class Root extends Component {
|
||||||
|
static template = xml`<div/>`;
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
getCurrent(); // checks that we're in a component context
|
||||||
|
|
||||||
|
bus = new EventBus(["a", "b"]);
|
||||||
|
bus_empty = new EventBus([]);
|
||||||
|
bbus_no_validation = new EventBus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await mount(Root, makeTestFixture(), { test: true });
|
||||||
|
|
||||||
|
bbus_no_validation!.addEventListener("c", () => {});
|
||||||
|
bbus_no_validation!.trigger("c");
|
||||||
|
bbus_no_validation!.dispatchEvent(new CustomEvent("c"));
|
||||||
|
|
||||||
|
bus!.addEventListener("a", () => {});
|
||||||
|
bus!.trigger("a");
|
||||||
|
bus!.dispatchEvent(new CustomEvent("a"));
|
||||||
|
|
||||||
|
expect(() => bus!.addEventListener("c", () => {})).toThrow(
|
||||||
|
"EventBus: subscribing to unknown event 'c'"
|
||||||
|
);
|
||||||
|
expect(() => bus!.trigger("c")).toThrow("EventBus: triggering unknown event 'c'");
|
||||||
|
expect(() => bus!.dispatchEvent(new CustomEvent("c"))).toThrow(
|
||||||
|
"EventBus: dispatching unknown event 'c'"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(() => bus_empty!.addEventListener("a", () => {})).toThrow(
|
||||||
|
"EventBus: subscribing to unknown event 'a'"
|
||||||
|
);
|
||||||
|
expect(() => bus_empty!.trigger("a")).toThrow("EventBus: triggering unknown event 'a'");
|
||||||
|
expect(() => bus_empty!.dispatchEvent(new CustomEvent("a"))).toThrow(
|
||||||
|
"EventBus: dispatching unknown event 'a'"
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("batched", () => {
|
describe("batched", () => {
|
||||||
@@ -71,3 +133,94 @@ describe("batched", () => {
|
|||||||
expect(n).toBe(2);
|
expect(n).toBe(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const Markup = markup("").constructor;
|
||||||
|
describe("markup", () => {
|
||||||
|
test("string is flagged as safe", () => {
|
||||||
|
const html = markup("<blink>Hello</blink>");
|
||||||
|
expect(html).toBeInstanceOf(Markup);
|
||||||
|
});
|
||||||
|
describe("htmlEscape", () => {
|
||||||
|
test("htmlEscape escapes text", () => {
|
||||||
|
const res = htmlEscape("<p>test</p>");
|
||||||
|
expect(res.toString()).toBe("<p>test</p>");
|
||||||
|
expect(res).toBeInstanceOf(Markup);
|
||||||
|
});
|
||||||
|
test("htmlEscape keeps html markup", () => {
|
||||||
|
const res = htmlEscape(markup("<p>test</p>"));
|
||||||
|
expect(res.toString()).toBe("<p>test</p>");
|
||||||
|
expect(res).toBeInstanceOf(Markup);
|
||||||
|
});
|
||||||
|
test("htmlEscape produces empty string on undefined", () => {
|
||||||
|
const res = htmlEscape(undefined);
|
||||||
|
expect(res.toString()).toBe("");
|
||||||
|
expect(res).toBeInstanceOf(Markup);
|
||||||
|
});
|
||||||
|
test("htmlEscape produces string from number", () => {
|
||||||
|
const res = htmlEscape(10);
|
||||||
|
expect(res.toString()).toBe("10");
|
||||||
|
expect(res).toBeInstanceOf(Markup);
|
||||||
|
});
|
||||||
|
test("htmlEscape produces string from boolean", () => {
|
||||||
|
const res = htmlEscape(false);
|
||||||
|
expect(res.toString()).toBe("false");
|
||||||
|
expect(res).toBeInstanceOf(Markup);
|
||||||
|
});
|
||||||
|
test("htmlEscape correctly escapes various links", () => {
|
||||||
|
expect(htmlEscape("<a>this is a link</a>").toString()).toBe(
|
||||||
|
"<a>this is a link</a>"
|
||||||
|
);
|
||||||
|
expect(htmlEscape(`<a href="https://www.odoo.com">odoo<a>`).toString()).toBe(
|
||||||
|
`<a href="https://www.odoo.com">odoo<a>`
|
||||||
|
);
|
||||||
|
expect(htmlEscape(`<a href='https://www.odoo.com'>odoo<a>`).toString()).toBe(
|
||||||
|
`<a href='https://www.odoo.com'>odoo<a>`
|
||||||
|
);
|
||||||
|
expect(htmlEscape("<a href='https://www.odoo.com'>Odoo`s website<a>").toString()).toBe(
|
||||||
|
`<a href='https://www.odoo.com'>Odoo`s website<a>`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
test("htmlEscape doesn't escape already escaped content", () => {
|
||||||
|
const res = htmlEscape("<p>test</p>");
|
||||||
|
expect(res.toString()).toBe("<p>test</p>");
|
||||||
|
expect(res).toBeInstanceOf(Markup);
|
||||||
|
const res2 = htmlEscape(res);
|
||||||
|
expect(res2.toString()).toBe("<p>test</p>");
|
||||||
|
expect(res2).toBeInstanceOf(Markup);
|
||||||
|
expect(res2).toBe(res);
|
||||||
|
});
|
||||||
|
test("htmlEscape returns markup even for only-safe text", () => {
|
||||||
|
const res = htmlEscape("safe");
|
||||||
|
expect(res.toString()).toBe("safe");
|
||||||
|
expect(res).toBeInstanceOf(Markup);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("tag function", () => {
|
||||||
|
test("interpolated values are escaped", () => {
|
||||||
|
const maliciousInput = "<script>alert('💥💥')</script>";
|
||||||
|
const html = markup`<b>${maliciousInput}</b>`;
|
||||||
|
expect(html.toString()).toBe("<b><script>alert('💥💥')</script></b>");
|
||||||
|
expect(html).toBeInstanceOf(Markup);
|
||||||
|
});
|
||||||
|
test("interpolated markups aren't escaped", () => {
|
||||||
|
const shouldBeEscaped = "<script>alert('should be escaped')</script>";
|
||||||
|
const shouldnt = markup("<b>this is safe</b>");
|
||||||
|
const html = markup`<div>${shouldBeEscaped} ${shouldnt}</div>`;
|
||||||
|
expect(html.toString()).toBe(
|
||||||
|
"<div><script>alert('should be escaped')</script> <b>this is safe</b></div>"
|
||||||
|
);
|
||||||
|
expect(html).toBeInstanceOf(Markup);
|
||||||
|
});
|
||||||
|
test("quotes in interpolated values are escaped", () => {
|
||||||
|
const imgUrl = `lol" onerror="alert('xss')`;
|
||||||
|
const html = markup`<img src="${imgUrl}">`;
|
||||||
|
expect(html.toString()).toBe(`<img src="lol" onerror="alert('xss')">`);
|
||||||
|
});
|
||||||
|
test("already escaped content is not escaped again", () => {
|
||||||
|
const res = htmlEscape("<p>test</p>");
|
||||||
|
expect(res.toString()).toBe("<p>test</p>");
|
||||||
|
const html = markup`${res}`;
|
||||||
|
expect(html.toString()).toBe("<p>test</p>");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user