mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 871dad6a13 | |||
| b620502a0f | |||
| 89cb00cc83 | |||
| 56041bc133 | |||
| e788e361c7 | |||
| 9d378b0e7b | |||
| fd3c194525 |
@@ -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,
|
||||
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
|
||||
|
||||
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, ...
|
||||
|
||||
+77
-14
@@ -276,13 +276,39 @@ function inOwnerDocument(el) {
|
||||
const rootNode = el.getRootNode();
|
||||
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) {
|
||||
// Get the document and HTMLElement corresponding to the target to allow mounting in iframes
|
||||
const document = target && target.ownerDocument;
|
||||
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;
|
||||
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");
|
||||
}
|
||||
return;
|
||||
@@ -319,12 +345,40 @@ async function loadFile(url) {
|
||||
*/
|
||||
class Markup extends String {
|
||||
}
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
function markup(value) {
|
||||
return new Markup(value);
|
||||
function htmlEscape(str) {
|
||||
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);
|
||||
}
|
||||
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) {
|
||||
@@ -3796,7 +3850,16 @@ class CodeTarget {
|
||||
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*)$/;
|
||||
class CodeGenerator {
|
||||
constructor(ast, options) {
|
||||
@@ -4914,11 +4977,11 @@ function parseNode(node, ctx) {
|
||||
parseTPortal(node, ctx) ||
|
||||
parseTCall(node, ctx) ||
|
||||
parseTCallBlock(node) ||
|
||||
parseTTranslation(node, ctx) ||
|
||||
parseTTranslationContext(node, ctx) ||
|
||||
parseTEscNode(node, ctx) ||
|
||||
parseTOutNode(node, ctx) ||
|
||||
parseTKey(node, ctx) ||
|
||||
parseTTranslation(node, ctx) ||
|
||||
parseTTranslationContext(node, ctx) ||
|
||||
parseTSlot(node, ctx) ||
|
||||
parseComponent(node, ctx) ||
|
||||
parseDOMNode(node, ctx) ||
|
||||
@@ -5704,7 +5767,7 @@ function compile(template, options = {
|
||||
}
|
||||
|
||||
// do not modify manually. This file is generated by the release script.
|
||||
const version = "2.6.1";
|
||||
const version = "2.8.0";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Scheduler
|
||||
@@ -6172,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-03-05T08:37:58.580Z';
|
||||
__info__.hash = '2b5cea9';
|
||||
__info__.date = '2025-06-30T12:46:06.424Z';
|
||||
__info__.hash = 'b620502';
|
||||
__info__.url = 'https://github.com/odoo/owl';
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@odoo/owl",
|
||||
"version": "2.6.1",
|
||||
"version": "2.8.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@odoo/owl",
|
||||
"version": "2.6.1",
|
||||
"version": "2.8.0",
|
||||
"description": "Odoo Web Library (OWL)",
|
||||
"main": "dist/owl.cjs.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*)$/;
|
||||
|
||||
export class CodeGenerator {
|
||||
|
||||
@@ -253,11 +253,11 @@ function parseNode(node: Node, ctx: ParsingContext): AST | null {
|
||||
parseTPortal(node, ctx) ||
|
||||
parseTCall(node, ctx) ||
|
||||
parseTCallBlock(node, ctx) ||
|
||||
parseTTranslation(node, ctx) ||
|
||||
parseTTranslationContext(node, ctx) ||
|
||||
parseTEscNode(node, ctx) ||
|
||||
parseTOutNode(node, ctx) ||
|
||||
parseTKey(node, ctx) ||
|
||||
parseTTranslation(node, ctx) ||
|
||||
parseTTranslationContext(node, ctx) ||
|
||||
parseTSlot(node, ctx) ||
|
||||
parseComponent(node, ctx) ||
|
||||
parseDOMNode(node, ctx) ||
|
||||
|
||||
@@ -41,7 +41,7 @@ export { useComponent, useState } from "./component_node";
|
||||
export { status } from "./status";
|
||||
export { reactive, markRaw, toRaw } from "./reactivity";
|
||||
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 {
|
||||
onWillStart,
|
||||
onMounted,
|
||||
|
||||
+74
-4
@@ -35,13 +35,43 @@ export function inOwnerDocument(el?: HTMLElement) {
|
||||
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) {
|
||||
// Get the document and HTMLElement corresponding to the target to allow mounting in iframes
|
||||
const document = target && target.ownerDocument;
|
||||
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 (!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");
|
||||
}
|
||||
return;
|
||||
@@ -81,10 +111,50 @@ export async function loadFile(url: string): Promise<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.
|
||||
* 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) {
|
||||
return new Markup(value);
|
||||
export function markup(strings: TemplateStringsArray, ...placeholders: unknown[]): Markup;
|
||||
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.
|
||||
export const version = "2.6.1";
|
||||
export const version = "2.8.0";
|
||||
|
||||
@@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -29,6 +29,24 @@ describe("shadow_dom", () => {
|
||||
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 () => {
|
||||
let a = 1;
|
||||
class SomeComponent extends Component {
|
||||
@@ -64,4 +82,73 @@ describe("shadow_dom", () => {
|
||||
await mountedProm;
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
+92
-1
@@ -1,4 +1,4 @@
|
||||
import { batched, EventBus } from "../src/runtime/utils";
|
||||
import { batched, EventBus, htmlEscape, markup } from "../src/runtime/utils";
|
||||
import { nextMicroTick } from "./helpers";
|
||||
|
||||
describe("event bus behaviour", () => {
|
||||
@@ -71,3 +71,94 @@ describe("batched", () => {
|
||||
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