import { addTables, bootstrapToTable, cardToTable, classToStyle, formatTables, getCSSRules, listGroupToTable, normalizeColors, normalizeRem, } from "@mail/views/web/fields/html_mail_field/convert_inline"; import { afterEach, beforeEach, describe, expect, getFixture, test } from "@odoo/hoot"; import { getGridHtml, getRegularGridHtml, getRegularTableHtml, getTableHtml, getTdHtml, } from "./utils"; const TEST_WIDTH = 800; const TEST_HEIGHT = 600; let editable; function testConvertGrid({ before, after, title, stepFunction }) { editable.innerHTML = before; (stepFunction || bootstrapToTable)(editable); // Remove class that is added by `bootstrapToTable` for use in // further methods of `toInline`, and removed at the end of it. editable.querySelectorAll(".o_converted_col").forEach((node) => { node.classList.remove("o_converted_col"); if (!node.classList.length) { node.removeAttribute("class"); } }); expect(editable).toHaveInnerHTML(after, { message: title, type: "html" }); } describe("Convert Bootstrap grids to tables", () => { // Test bootstrapToTable, cardToTable and listGroupToTable beforeEach(() => { editable = document.createElement("div"); editable.style.setProperty("width", TEST_WIDTH + "px"); editable.style.setProperty("height", TEST_HEIGHT + "px"); getFixture().append(editable); }); test("convert a single-row regular grid", async () => { // 1x1 testConvertGrid({ before: getRegularGridHtml(1, 1), after: getRegularTableHtml(1, 1, 12, 100, TEST_WIDTH), title: "should have converted a 1x1 grid to an equivalent table", }); // 1x2 testConvertGrid({ before: getRegularGridHtml(1, 2), after: getRegularTableHtml(1, 2, 6, 50, TEST_WIDTH), title: "should have converted a 1x2 grid to an equivalent table", }); // 1x3 testConvertGrid({ before: getRegularGridHtml(1, 3), after: getRegularTableHtml(1, 3, 4, 33.33, TEST_WIDTH), title: "should have converted a 1x3 grid to an equivalent table", }); // 1x12 testConvertGrid({ before: getRegularGridHtml(1, 12), after: getRegularTableHtml(1, 12, 1, 8.33, TEST_WIDTH), title: "should have converted a 1x12 grid to an equivalent table", }); }); test("convert a single-row regular overflowing grid", async () => { // 1x13 testConvertGrid({ before: getRegularGridHtml(1, 13), after: getRegularTableHtml(1, 12, 1, 8.33, TEST_WIDTH).slice(0, -8) + `` + getTdHtml(1, "(0, 12)", TEST_WIDTH) + getTdHtml(11, "", TEST_WIDTH) + ``, title: "should have converted a 1x13 grid to an equivalent table (overflowing)", }); // 1x14 testConvertGrid({ before: getRegularGridHtml(1, 14), after: getRegularTableHtml(1, 12, 1, 8.33, TEST_WIDTH).slice(0, -8) + `` + getTdHtml(1, "(0, 12)", TEST_WIDTH) + getTdHtml(1, "(0, 13)", TEST_WIDTH) + getTdHtml(10, "", TEST_WIDTH) + ``, title: "should have converted a 1x14 grid to an equivalent table (overflowing)", }); // 1x25 testConvertGrid({ before: getRegularGridHtml(1, 25), after: getRegularTableHtml(1, 12, 1, 8.33, TEST_WIDTH).slice(0, -8) + getRegularTableHtml(1, 12, 1, 8.33, TEST_WIDTH) .replace(/\(0, (\d+)\)/g, (s, c) => `(0, ${+c + 12})`) .replace(/^/, "") .slice(0, -8) + `` + getTdHtml(1, "(0, 24)", TEST_WIDTH) + getTdHtml(11, "", TEST_WIDTH) + ``, title: "should have converted a 1x25 grid to an equivalent table (overflowing)", }); // 1x26 testConvertGrid({ before: getRegularGridHtml(1, 26), after: getRegularTableHtml(1, 12, 1, 8.33, TEST_WIDTH).slice(0, -8) + getRegularTableHtml(1, 12, 1, 8.33, TEST_WIDTH) .replace(/\(0, (\d+)\)/g, (s, c) => `(0, ${+c + 12})`) .replace(/^/, "") .slice(0, -8) + `` + getTdHtml(1, "(0, 24)", TEST_WIDTH) + getTdHtml(1, "(0, 25)", TEST_WIDTH) + getTdHtml(10, "", TEST_WIDTH) + ``, title: "should have converted a 1x26 grid to an equivalent table (overflowing)", }); }); test("convert a multi-row regular grid", async () => { // 2x1 testConvertGrid({ before: getRegularGridHtml(2, 1), after: getRegularTableHtml(2, 1, 12, 100, TEST_WIDTH), title: "should have converted a 2x1 grid to an equivalent table", }); // 2x[1,2] testConvertGrid({ before: getRegularGridHtml(2, [1, 2]), after: getRegularTableHtml(2, [1, 2], [12, 6], [100, 50], TEST_WIDTH), title: "should have converted a 2x[1,2] grid to an equivalent table", }); // 3x3 testConvertGrid({ before: getRegularGridHtml(3, 3), after: getRegularTableHtml(3, 3, 4, 33.33, TEST_WIDTH), title: "should have converted a 3x3 grid to an equivalent table", }); // 3x[3,2,1] testConvertGrid({ before: getRegularGridHtml(3, [3, 2, 1]), after: getRegularTableHtml(3, [3, 2, 1], [4, 6, 12], [33.33, 50, 100], TEST_WIDTH), title: "should have converted a 3x[3,2,1] grid to an equivalent table", }); }); test("convert a multi-row regular overflowing grid", async () => { // 2x[13,1] testConvertGrid({ before: getRegularGridHtml(2, [13, 1]), after: getRegularTableHtml(1, 12, 1, 8.33, TEST_WIDTH).slice(0, -8) + `` + getTdHtml(1, "(0, 12)", TEST_WIDTH) + getTdHtml(11, "", TEST_WIDTH) + // 13 overflowed the row by 1 -> fill up `` + `${getTdHtml(12, "(1, 0)", TEST_WIDTH)}`, // 1 col with no size == col-12 title: "should have converted a 2x[13,1] grid to an equivalent table (overflowing)", }); // 2x[1,13] testConvertGrid({ before: getRegularGridHtml(2, [1, 13]), after: getRegularTableHtml(2, [1, 12], [12, 1], [100, 8.33], TEST_WIDTH).slice(0, -8) + `` + getTdHtml(1, "(1, 12)", TEST_WIDTH) + getTdHtml(11, "", TEST_WIDTH) + // 13 overflowed the row by 1 -> fill up ``, title: "should have converted a 2x[1,13] grid to an equivalent table (overflowing)", }); // 3x[1,13,6] testConvertGrid({ before: getRegularGridHtml(3, [1, 13, 6]), after: getRegularTableHtml(2, [1, 12], [12, 1], [100, 8.33], TEST_WIDTH).slice(0, -8) + `` + getTdHtml(1, "(1, 12)", TEST_WIDTH) + getTdHtml(11, "", TEST_WIDTH) + // 13 overflowed the row by 1 -> fill up `` + getRegularTableHtml(1, 6, 2, 16.67, TEST_WIDTH) .replace(/\(0,/g, `(2,`) .replace(/^/, ""), title: "should have converted a 3x[1,13,6] grid to an equivalent table (overflowing)", }); // 3x[1,6,13] testConvertGrid({ before: getRegularGridHtml(3, [1, 6, 13]), after: getRegularTableHtml( 3, [1, 6, 12], [12, 2, 1], [100, 16.67, 8.33], TEST_WIDTH ).slice(0, -8) + `` + getTdHtml(1, "(2, 12)", TEST_WIDTH) + getTdHtml(11, "", TEST_WIDTH) + // 13 overflowed the row by 1 -> fill up ``, title: "should have converted a 3x[1,6,13] grid to an equivalent table (overflowing)", }); }); test("convert a single-row irregular grid", async () => { // 1x2 testConvertGrid({ before: getGridHtml([[8, 4]]), after: getTableHtml( [ [ [8, 66.67], [4, 33.33], ], ], TEST_WIDTH ), title: "should have converted a 1x2 irregular grid to an equivalent table", }); // 1x3 testConvertGrid({ before: getGridHtml([[2, 3, 7]]), after: getTableHtml( [ [ [2, 16.67], [3, 25], [7, 58.33], ], ], TEST_WIDTH ), title: "should have converted a 1x3 grid to an equivalent table", }); }); test("convert a single-row irregular overflowing grid", async () => { // 1x2 testConvertGrid({ before: getGridHtml([[8, 5]]), after: getTableHtml( [ [ [8, 66.67], [4, 33.33, ""], ], [ [5, 41.67, "(0, 1)"], [7, 58.33, ""], ], ], TEST_WIDTH ), title: "should have converted a 1x2 irregular overflowing grid to an equivalent table", }); // 1x3 testConvertGrid({ before: getGridHtml([[7, 6, 9]]), after: getTableHtml( [ [ [7, 58.33], [5, 41.67, ""], ], [ [6, 50, "(0, 1)"], [6, 50, ""], ], [ [9, 75, "(0, 2)"], [3, 25, ""], ], ], TEST_WIDTH ), title: "should have converted a 1x3 irregular overflowing grid to an equivalent table", }); }); test("convert a multi-row irregular grid", async () => { // 2x2 testConvertGrid({ before: getGridHtml([ [1, 11], [2, 10], ]), after: getTableHtml( [ [ [1, 8.33], [11, 91.67], ], [ [2, 16.67], [10, 83.33], ], ], TEST_WIDTH ), title: "should have converted a 2x2 irregular grid to an equivalent table", }); // 2x[2,3] testConvertGrid({ before: getGridHtml([ [3, 9], [4, 6, 2], ]), after: getTableHtml( [ [ [3, 25], [9, 75], ], [ [4, 33.33], [6, 50], [2, 16.67], ], ], TEST_WIDTH ), title: "should have converted a 2x[2,3] irregular grid to an equivalent table", }); }); test("convert a multi-row irregular overflowing grid", async () => { // 2x2 (both rows overflow) testConvertGrid({ before: getGridHtml([ [6, 8], [7, 9], ]), after: getTableHtml( [ [ [6, 50], [6, 50, ""], ], [ [8, 66.67, "(0, 1)"], [4, 33.33, ""], ], [ [7, 58.33, "(1, 0)"], [5, 41.67, ""], ], [ [9, 75, "(1, 1)"], [3, 25, ""], ], ], TEST_WIDTH ), title: "should have converted a 2x[1,13] irregular grid to an equivalent table (both rows overflowing)", }); // 2x[2,3] (first row overflows) testConvertGrid({ before: getGridHtml([ [5, 8], [4, 2, 6], ]), after: getTableHtml( [ [ [5, 41.67], [7, 58.33, ""], ], [ [8, 66.67, "(0, 1)"], [4, 33.33, ""], ], [ [4, 33.33, "(1, 0)"], [2, 16.67, "(1, 1)"], [6, 50, "(1, 2)"], ], ], TEST_WIDTH ), title: "should have converted a 2x[2,3] irregular grid to an equivalent table (first row overflowing)", }); // 2x[3,2] (second row overflows) testConvertGrid({ before: getGridHtml([ [4, 2, 6], [5, 8], ]), after: getTableHtml( [ [ [4, 33.33], [2, 16.67], [6, 50], ], [ [5, 41.67], [7, 58.33, ""], ], [ [8, 66.67, "(1, 1)"], [4, 33.33, ""], ], ], TEST_WIDTH ), title: "should have converted a 2x[3,2] irregular grid to an equivalent table (second row overflowing)", }); }); test("convert a card to a table", async () => { testConvertGrid({ title: "should have converted a card structure into a table", before: `
` + `
` + `HEADER` + `
` + `
` + `

TITLE

` + `BODY ` + `
` + `` + `
`, stepFunction: cardToTable, after: getRegularTableHtml(3, 1, 12, 100) .replace('role="presentation"', 'role="presentation" class="card"') .replace( /]*>\(0, 0\)<\/td>/, `` + `` + `` + `
HEADER
` ) .replace( /]*>\(1, 0\)<\/td>/, `` + `` + `` + `

TITLE

BODY
` ) .replace( /]*>\(2, 0\)<\/td>/, `` + `` + `` + `
` ), }); }); test("convert a list group to a table", async () => { testConvertGrid({ title: "should have converted a list group structure into a table", before: `
    ` + `
  • ` + `(0, 0)` + `
  • ` + `
  • ` + `(1, 0)` + `
  • ` + `
  • ` + `
  • ` + `(2, 0)` + `
  • ` + `
`, stepFunction: listGroupToTable, after: getRegularTableHtml(3, 1, 12, 100) .split('style="') .join('class="list-group-flush" style="') .replace(/]*>(\(0, 0\))<\/td>/, "$1") .replace(/]*>(\(1, 0\))<\/td>/, '$1') .replace( /]*>(\(2, 0\))<\/td>/, '$1' ), }); }); test("convert a grid with offsets to a table", async () => { testConvertGrid({ before: '
(0, 0)
', after: getTableHtml( [ [ [4, 33.33, ""], [6, 50, "(0, 0)"], [2, 16.67, ""], ], ], TEST_WIDTH ), title: "should have converted a column with an offset to two columns, then completed the column", }); testConvertGrid({ before: '
(0, 0)
(0, 1)
', after: getTableHtml( [ [ [4, 33.33, ""], [6, 50, "(0, 0)"], [1, 8.33, ""], [1, 8.33, ""], ], [ [6, 50, "(0, 1)"], [6, 50, ""], ], ], TEST_WIDTH ), title: "should have converted a column with an offset to two columns, then completed the column (overflowing)", }); }); }); describe("Normalize styles", () => { beforeEach(() => { editable = document.createElement("div"); }); // Test normalizeColors, normalizeRem and formatTables test("convert rgb color to hexadecimal", async () => { editable.innerHTML = `

Test

`; normalizeColors(editable); expect(editable).toHaveInnerHTML( `

Test

`, { message: "should have converted several rgb colors to hexadecimal" } ); }); test("convert rem sizes to px", async () => { const testDom = `

Test

`; editable.innerHTML = testDom; normalizeRem(editable); expect(editable).toHaveInnerHTML( `
` + `
` + `

Test

` + `
` + `
`, { message: "should have converted several rem sizes to px using the default rem size", } ); editable.innerHTML = testDom; normalizeRem(editable, 20); expect(editable).toHaveInnerHTML( `
` + `
` + `

Test

` + `
` + `
`, { message: "should have converted several rem sizes to px using a set rem size", } ); }); test("move padding from snippet containers to cells", async () => { const testTable = `
(0, 0, 0) (0, 1, 0) (0, 2, 0) (0, 3, 0) (0, 4, 0)
(0, 0, 1) (0, 1, 1) (0, 2, 1) (0, 3, 1)
(1, 0, 0) (1, 1, 0) (1, 2, 0) (1, 3, 0) (1, 4, 0)
`; const expectedTable = `` + `` + `` + `` + // TL `` + // T `` + // T `` + // T `` + // TR `` + `` + `` + `` + `` + `` + // BL `` + // B `` + // B `` + // B `` + // BR `` + `` + `
(0, 0, 0)(0, 1, 0)(0, 2, 0)(0, 3, 0)(0, 4, 0)
` + // LR `` + `` + `` + `` + // TBL `` + // TB `` + // TB `` + // TBR `` + `` + `
(0, 0, 1)(0, 1, 1)(0, 2, 1)(0, 3, 1)
` + `
(1, 0, 0)(1, 1, 0)(1, 2, 0)(1, 3, 0)(1, 4, 0)
`; // table.o_mail_snippet_general editable.innerHTML = testTable; formatTables(editable); expect(editable).toHaveInnerHTML(expectedTable, { message: "should have moved the padding from table.o_mail_snippet_general and table in it to their respective cells", }); }); test("add a tbody to any table that doesn't have one", async () => { editable.innerHTML = `
I don't have a body :'(
`; // unwrap tr (remove ) const tr = editable.querySelector("tr"); const body = tr.parentElement; body.parentElement.appendChild(tr); tr.parentElement.removeChild(body); formatTables(editable); expect(editable).toHaveInnerHTML( `
I don't have a body :'(
`, { message: "should have added a tbody to a table that didn't have one" } ); }); test("add number heights to parents of elements with percent heights", async () => { editable.innerHTML = `
yup
`; formatTables(editable); expect(editable).toHaveInnerHTML( `
yup
`, { message: "should have added a 0 height to the parent of a 100% height element" } ); editable.innerHTML = `
yup
`; formatTables(editable); expect(editable).toHaveInnerHTML( `
yup
`, { message: "should have added a 0 height to the parent of a 100% height element" } ); editable.innerHTML = `
yup
`; formatTables(editable); expect(editable).toHaveInnerHTML( `
yup
`, { message: "should have changed the height of the grandparent of a 100% height element", } ); }); test("express align-self with vertical-align on table cells", async () => { editable.innerHTML = `
yup
`; formatTables(editable); expect(editable).toHaveInnerHTML( `
yup
`, { message: "should have added a top vertical alignment" } ); editable.innerHTML = `
yup
`; formatTables(editable); expect(editable).toHaveInnerHTML( `
yup
`, { message: "should have added a middle vertical alignment" } ); editable.innerHTML = `
yup
`; formatTables(editable); expect(editable).toHaveInnerHTML( `
yup
`, { message: "should have added a bottom vertical alignment" } ); }); }); describe("Convert snippets and mailing bodies to tables", () => { // Test addTables beforeEach(() => { editable = document.createElement("div"); }); test("convert snippets to tables", async () => { editable.innerHTML = `
Snippet
`; addTables(editable); expect(editable).toHaveInnerHTML( getRegularTableHtml(1, 1, 12, 100) .split("style=") .join('class="o_mail_snippet_general" style=') .replace( /]*>\(0, 0\)/, "" + getRegularTableHtml(1, 1, 12, 100).replace( /]*>\(0, 0\)/, "
Snippet
" ) ), { message: "should have converted .o_mail_snippet_general to a special table structure with a table in it", } ); editable.innerHTML = `
Snippet
`; addTables(editable); expect(editable).toHaveInnerHTML( getRegularTableHtml(1, 1, 12, 100) .split("style=") .join('class="o_mail_snippet_general" style=') .replace( /]*>\(0, 0\)/, "
Snippet
" ), { message: "should have converted .o_mail_snippet_general to a special table structure, keeping the table in it", } ); }); test("convert mailing bodies to tables", async () => { editable.innerHTML = `
Mailing
`; addTables(editable); expect(editable).toHaveInnerHTML( getRegularTableHtml(1, 1, 12, 100) .split("style=") .join('class="o_layout" style=') .replace(" font-size: unset; line-height: inherit;", "") // o_layout keeps those default values .replace( /]*>\(0, 0\)/, "" + getRegularTableHtml(1, 1, 12, 100).replace( /]*>\(0, 0\)/, "
Mailing
" ) ), { message: "should have converted .o_layout to a special table structure with a table in it", } ); editable.innerHTML = `
Mailing
`; addTables(editable); expect(editable).toHaveInnerHTML( getRegularTableHtml(1, 1, 12, 100) .split("style=") .join('class="o_layout" style=') .replace(" font-size: unset; line-height: inherit;", "") // o_layout keeps those default values .replace( /]*>\(0, 0\)/, "
Mailing
" ), { message: "should have converted .o_layout to a special table structure, keeping the table in it", } ); }); }); describe("Convert classes to inline styles", () => { // Test classToStyle let styleEl, styleSheet; beforeEach(() => { editable = document.createElement("div"); styleEl = document.createElement("style"); styleEl.type = "text/css"; styleEl.title = "test-stylesheet"; document.head.appendChild(styleEl); styleSheet = [...document.styleSheets].find((sheet) => sheet.title === "test-stylesheet"); }); afterEach(() => { // @todo to adapt when hoot has a better way to remove it document.head.removeChild(styleEl); }); test("convert Bootstrap classes to inline styles", async () => { editable.innerHTML = `
Hello
`; getFixture().append(editable); // editable needs to be in the DOM to compute its dynamic styles. classToStyle(editable, getCSSRules(editable.ownerDocument)); // Some positional properties (eg., padding-right, margin-left) are not // concatenated (eg., as padding, margin) because they were defined with // variables (var) or calculated (calc). const containerStyle = `margin: 0px auto; box-sizing: border-box; max-width: 1320px; padding-left: 16px; padding-right: 16px; width: 100%;`; const rowStyle = `box-sizing: border-box; margin-left: -16px; margin-right: -16px; margin-top: 0px;`; const colStyle = `box-sizing: border-box; margin-top: 0px; padding-left: 16px; padding-right: 16px; max-width: 100%; width: 100%;`; expect(editable).toHaveInnerHTML( `
` + `
` + `
Hello
`, { message: "should have converted the classes of a simple Bootstrap grid to inline styles", } ); }); test("simplify border/margin/padding styles", async () => { // border-radius styleSheet.insertRule( ` .test-border-radius { border-top-right-radius: 10%; border-bottom-right-radius: 20%; border-bottom-left-radius: 30%; border-top-left-radius: 40%; } `, 0 ); editable.innerHTML = `
`; classToStyle(editable, getCSSRules(editable.ownerDocument)); expect(editable).toHaveInnerHTML( `
`, { message: "should have converted border-[position]-radius styles (from class) to border-radius", } ); styleSheet.deleteRule(0); // convert all positional styles to a style in the form `property: a b c d` styleSheet.insertRule( ` .test-border { border-top-style: dotted; border-right-style: dashed; border-left-style: solid; } `, 0 ); editable.innerHTML = `
`; classToStyle(editable, getCSSRules(editable.ownerDocument)); expect(editable).toHaveInnerHTML( `
`, { message: "should have converted border-[position]-style styles (from class) to border-style", } ); styleSheet.deleteRule(0); styleSheet.insertRule( ` .test-margin { margin-right: 20px; margin-bottom: 30px; margin-left: 40px; } `, 0 ); editable.innerHTML = `
`; classToStyle(editable, getCSSRules(editable.ownerDocument)); expect(editable).toHaveInnerHTML( `
`, { message: "should have converted margin-[position] styles (from class) to margin" } ); styleSheet.deleteRule(0); styleSheet.insertRule( ` .test-padding { padding-top: 10px; padding-bottom: 30px; padding-left: 40px; } `, 0 ); editable.innerHTML = `
`; classToStyle(editable, getCSSRules(editable.ownerDocument)); expect(editable).toHaveInnerHTML( `
`, { message: "should have converted padding-[position] styles (from class) to padding" } ); styleSheet.deleteRule(0); // convert all positional styles to a style in the form `property: a` styleSheet.insertRule( ` .test-border-uniform { border-top-style: dotted; border-right-style: dotted; border-bottom-style: dotted; border-left-style: dotted; } `, 0 ); editable.innerHTML = `
`; classToStyle(editable, getCSSRules(editable.ownerDocument)); expect(editable).toHaveInnerHTML( `
`, { message: "should have converted uniform border-[position]-style styles (from class) to border-style", } ); styleSheet.deleteRule(0); styleSheet.insertRule( ` .test-margin-uniform { margin-top: 10px; margin-right: 10px; margin-bottom: 10px; margin-left: 10px; } `, 0 ); editable.innerHTML = `
`; classToStyle(editable, getCSSRules(editable.ownerDocument)); expect(editable).toHaveInnerHTML( `
`, { message: "should have converted uniform margin-[position] styles (from class) to margin", } ); styleSheet.deleteRule(0); styleSheet.insertRule( ` .test-padding-uniform { padding-top: 10px; padding-right: 10px; padding-bottom: 10px; padding-left: 10px; } `, 0 ); editable.innerHTML = `
`; classToStyle(editable, getCSSRules(editable.ownerDocument)); expect(editable).toHaveInnerHTML( `
`, { message: "should have converted uniform padding-[position] styles (from class) to padding", } ); styleSheet.deleteRule(0); // do not convert positional styles that include an "inherit" value styleSheet.insertRule( ` .test-border-inherit { border-top-style: dotted; border-right-style: dashed; border-bottom-style: inherit; border-left-style: solid; } `, 0 ); editable.innerHTML = `
`; classToStyle(editable, getCSSRules(editable.ownerDocument)); expect(editable).toHaveInnerHTML( `
`, { message: "should not have converted border-[position]-style styles (from class) to border-style as they include an inherit", } ); styleSheet.deleteRule(0); styleSheet.insertRule( ` .test-margin-inherit { margin-top: 10px; margin-right: inherit; margin-bottom: 30px; margin-left: 40px; } `, 0 ); editable.innerHTML = `
`; classToStyle(editable, getCSSRules(editable.ownerDocument)); expect(editable).toHaveInnerHTML( `
`, { message: "should not have converted margin-[position] styles (from class) to margin as they include an inherit", } ); styleSheet.deleteRule(0); styleSheet.insertRule( ` .test-padding-inherit { padding-top: 10px; padding-right: 20px; padding-bottom: inherit; padding-left: 40px; } `, 0 ); editable.innerHTML = `
`; classToStyle(editable, getCSSRules(editable.ownerDocument)); expect(editable).toHaveInnerHTML( `
`, { message: "should have converted padding-[position] styles (from class) to padding as they include an inherit", } ); styleSheet.deleteRule(0); // do not convert positional styles that include an "initial" value // note: `border: initial` is automatically removed (tested in "remove // unsupported styles") styleSheet.insertRule( ` .test-margin-initial { margin-top: initial; margin-right: 20px; margin-bottom: 30px; margin-left: 40px; } `, 0 ); editable.innerHTML = `
`; classToStyle(editable, getCSSRules(editable.ownerDocument)); expect(editable).toHaveInnerHTML( `
`, { message: "should not have converted margin-[position] styles (from class) to margin as they include an initial", } ); styleSheet.deleteRule(0); styleSheet.insertRule( ` .test-padding-initial { padding-top: 10px; padding-right: 20px; padding-bottom: 30px; padding-left: initial; } `, 0 ); editable.innerHTML = `
`; classToStyle(editable, getCSSRules(editable.ownerDocument)); expect(editable).toHaveInnerHTML( `
`, { message: "should not have converted padding-[position] styles (from class) to padding as they include an initial", } ); styleSheet.deleteRule(0); // @todo to adapt when hoot has a better way to remove it }); test("remove unsupported styles", async () => { // text-decoration-[prop] styleSheet.insertRule( ` .test-decoration { text-decoration-line: underline; text-decoration-color: red; text-decoration-style: solid; text-decoration-thickness: 10px; } `, 0 ); editable.innerHTML = `
`; classToStyle(editable, getCSSRules(editable.ownerDocument)); expect(editable).toHaveInnerHTML( `
`, { message: "should have removed all text-decoration-[prop] styles (from class) and kept a simple text-decoration", } ); styleSheet.deleteRule(0); // border[\w-]*: initial styleSheet.insertRule( ` .test-border-initial { border-top-style: dotted; border-right-style: dashed; border-bottom-style: double; border-left-style: initial; } `, 0 ); editable.innerHTML = `
`; classToStyle(editable, getCSSRules(editable.ownerDocument)); expect(editable).toHaveInnerHTML( `
`, { message: "should have removed border initial" } ); styleSheet.deleteRule(0); // display: block styleSheet.insertRule( ` .test-block { display: block; } `, 0 ); editable.innerHTML = `
`; classToStyle(editable, getCSSRules(editable.ownerDocument)); expect(editable).toHaveInnerHTML( `
`, { message: "should have removed display block" } ); styleSheet.deleteRule(0); // !important styleSheet.insertRule( ` .test-unimportant-color { color: blue; } `, 0 ); editable.innerHTML = `
`; classToStyle(editable, getCSSRules(editable.ownerDocument)); expect(editable).toHaveInnerHTML( `
`, { message: "should have converted a simple color" } ); styleSheet.insertRule( ` .test-important-color { color: red !important; } `, 0 ); editable.innerHTML = `
`; classToStyle(editable, getCSSRules(editable.ownerDocument)); expect(editable).toHaveInnerHTML( `
`, { message: "should have converted an important color and removed the !important" } ); styleSheet.deleteRule(0); styleSheet.deleteRule(0); // animation styleSheet.insertRule( ` .test-animation { animation: example 5s linear 2s infinite alternate; } `, 0 ); editable.innerHTML = `
`; classToStyle(editable, getCSSRules(editable.ownerDocument)); expect(editable).toHaveInnerHTML( `
`, { message: "should have removed animation style" } ); styleSheet.deleteRule(0); styleSheet.insertRule( ` .test-animation-specific { animation-name: example; animation-duration: 5s; animation-timing-function: linear; animation-delay: 2s; animation-iteration-count: infinite; animation-direction: alternate; } `, 0 ); editable.innerHTML = `
`; classToStyle(editable, getCSSRules(editable.ownerDocument)); expect(editable).toHaveInnerHTML( `
`, { message: "should have removed all specific animation styles" } ); styleSheet.deleteRule(0); // flex styleSheet.insertRule( ` .test-flex { flex: 0 1 auto; flex-flow: column wrap; } `, 0 ); editable.innerHTML = `
`; classToStyle(editable, getCSSRules(editable.ownerDocument)); expect(editable).toHaveInnerHTML( `
`, { message: "should have removed all flex styles" } ); styleSheet.deleteRule(0); styleSheet.insertRule( ` .test-flex-specific { display: flex; flex-direction: row; flex-wrap: wrap; flex-basis: auto; flex-shrink: 3; flex-grow: 4; } `, 0 ); editable.innerHTML = `
`; classToStyle(editable, getCSSRules(editable.ownerDocument)); expect(editable).toHaveInnerHTML( `
`, { message: "should have removed all specific flex styles" } ); styleSheet.deleteRule(0); // @todo to adapt when hoot has a better way to remove it }); test("give .o_layout the styles of the body", async () => { const iframe = document.createElement("IFRAME"); getFixture().append(iframe); const iframeEditable = document.createElement("div"); iframe.contentDocument.body.append(iframeEditable); const styleEl = document.createElement("style"); styleEl.type = "text/css"; styleEl.title = "test-stylesheet"; iframe.contentDocument.head.appendChild(styleEl); const styleSheet = [...iframe.contentDocument.styleSheets].find( (sheet) => sheet.title === "test-stylesheet" ); styleSheet.insertRule( ` body { background-color: red; color: white; font-size: 50px; } `, 0 ); iframeEditable.innerHTML = `
`; classToStyle(iframeEditable, getCSSRules(iframeEditable.ownerDocument)); expect(iframeEditable).toHaveInnerHTML( `
`, { message: "should have given all styles of body to .o_layout" } ); }); test("convert classes to styles, preserving specificity", async () => { styleSheet.insertRule( ` div.test-color { color: green; } `, 0 ); styleSheet.insertRule( ` .test-color { color: red; } `, 1 ); styleSheet.insertRule( ` .test-color { color: blue; } `, 2 ); editable.innerHTML = ``; classToStyle(editable, getCSSRules(editable.ownerDocument)); expect(editable).toHaveInnerHTML( ``, { message: "should have prioritized the last defined style" } ); editable.innerHTML = `
`; classToStyle(editable, getCSSRules(editable.ownerDocument)); expect(editable).toHaveInnerHTML( `
`, { message: "should have prioritized the more specific style" } ); editable.innerHTML = `
`; classToStyle(editable, getCSSRules(editable.ownerDocument)); expect(editable).toHaveInnerHTML( `
`, { message: "should have prioritized the inline style" } ); styleSheet.insertRule( ` .test-color { color: black !important; } `, 0 ); editable.innerHTML = `
`; classToStyle(editable, getCSSRules(editable.ownerDocument)); expect(editable).toHaveInnerHTML( `
`, { message: "should have prioritized the important style" } ); // @todo to adapt when hoot has a better way to remove it }); });