dom-to-image-more
Version:
Generates an image from a DOM node using HTML5 canvas and SVG
916 lines (886 loc) • 205 kB
JavaScript
/* eslint-disable no-undef */
(function (global) {
'use strict';
const assert = global.chai.assert;
const domtoimage = global.domtoimage;
const Promise = global.Promise;
const Tesseract = global.Tesseract;
const BASE_URL = '/base/spec/resources/';
const validPlaceholder =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAMSURBVBhXY7h79y4ABTICmGnXPbMAAAAASUVORK5CYII=';
// When the suite is launched with UPDATE_CONTROLS=1 the karma config sets this
// flag; instead of asserting against the stored control images, the comparison
// helpers POST each freshly-rendered image back to the karma updater middleware,
// which overwrites the matching control-image file. Run it in the SAME
// environment that validates the suite in CI (font rasterization/DPR differ).
const UPDATE_CONTROLS = !!(
global.__karma__ &&
global.__karma__.config &&
global.__karma__.config.updateControls
);
// Tests that compare a render against a stored control image are OS-font-dependent
// (ClearType vs FreeType, etc.), so they can't run cross-OS in CI. They are declared
// with `itImage(...)` instead of `it(...)`; under LOGIC_ONLY the karma config sets
// `logicOnly` and they're skipped, leaving the OS-robust logic subset to run anywhere.
const LOGIC_ONLY = !!(
global.__karma__ &&
global.__karma__.config &&
global.__karma__.config.logicOnly
);
function itImage(title, fn) {
return (LOGIC_ONLY ? it.skip : it)(title, fn);
}
let currentControlPath = null;
function writeControlImage(dataUrl) {
if (!currentControlPath) {
return Promise.reject(
new Error('UPDATE_CONTROLS: no control-image path for this test')
);
}
return fetch('/__update_control__', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: currentControlPath, data: dataUrl }),
}).then(function (res) {
if (!res.ok) {
return res.text().then(function (text) {
throw new Error('control update failed: ' + text);
});
}
});
}
describe('domtoimage', function () {
afterEach(purgePage);
it('should load', function () {
assert.ok(domtoimage);
});
describe('rendering', function () {
describe('content', function () {
describe('svg', function () {
it('renders an out-of-subtree icon-sprite <use xlink:href> (#139)', function (done) {
// #139: an SVG icon used via `<use xlink:href="#id">` referencing a
// `<symbol>` defined OUTSIDE the rendered subtree (the Vue/webpack
// svg-sprite pattern) was missing from the export. Closed by #215's
// collect-and-inject (legacy xlink:href + currentColor preserved).
loadTestPage()
.then(function () {
const sprite =
'<svg style="display:none" xmlns="http://www.w3.org/2000/svg">' +
'<symbol id="icon-star" viewBox="0 0 10 10">' +
'<path id="starpath" d="M5 0 L6 4 L10 4 L7 6 L8 10 L5 7 L2 10 L3 6 L0 4 L4 4 Z" fill="currentColor"></path>' +
'</symbol></svg>';
document
.querySelector('#test-root')
.insertAdjacentHTML('afterbegin', sprite);
domNode().innerHTML =
'<div style="color:red"><svg class="icon" width="20" height="20" xmlns="http://www.w3.org/2000/svg">' +
'<use xlink:href="#icon-star"></use></svg></div>';
return renderToSvg(domNode());
})
.then(function (svg) {
const decoded = decodeURIComponent(svg);
assert.include(
decoded,
'id="icon-star"',
'referenced symbol must be injected'
);
assert.include(
decoded,
'id="starpath"',
"the symbol's contents must be injected"
);
})
.then(done)
.catch(done);
});
it('renders a non-root SVG <g> element (#205)', function (done) {
// #205: rendering a non-root SVG element (`<g>`, `<path>`, …) directly used
// to reject — the bare element was wrapped in an XHTML <foreignObject> where
// it can't render. It is now wrapped in a real <svg> framed by its getBBox,
// so it rasterizes. Render the <g> and sample a pixel inside its red rect.
loadTestPage()
.then(function () {
domNode().innerHTML =
'<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">' +
'<g id="grp" transform="translate(10,10)">' +
'<rect x="0" y="0" width="40" height="30" fill="rgb(255,0,0)"></rect>' +
'<circle cx="60" cy="20" r="15" fill="rgb(0,0,255)"></circle>' +
'</g></svg>';
return domtoimage.toPixelData(
document.getElementById('grp')
);
})
.then(function (pixels) {
assert.isAbove(
pixels.length,
0,
'should produce pixel data, not reject'
);
// bbox is 75x35; sample (5,5) which is inside the red rect.
const i = (5 * 75 + 5) * 4;
assert.isAbove(
pixels[i],
200,
'red channel at the rect should be high'
);
assert.isBelow(
pixels[i + 2],
80,
'blue channel at the rect should be low'
);
assert.isAbove(
pixels[i + 3],
200,
'pixel at the rect should be opaque'
);
})
.then(done)
.catch(done);
});
it('inlines a nested SVG <image> href (#121)', function (done) {
// An SVG <image> references its bitmap via href / xlink:href (not .src
// like an HTML <img>), so it must be fetched and embedded as a data URL
// too, or it survives as an unfetchable external URL in the output.
this.timeout(15000);
const url = '/base/spec/resources/images/image.png';
loadTestPage()
.then(function () {
domNode().innerHTML =
'<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg">' +
'<image id="img" href="' +
url +
'" width="40" height="40"></image>' +
'</svg>';
return renderToSvg(domNode());
})
.then(function (svg) {
const decoded = decodeURIComponent(svg);
const image = (decoded.match(/<image[^>]*>/) || [])[0];
assert.isString(image, '<image> should be in the output');
assert.include(
image,
'data:image',
'the SVG <image> href must be inlined as a data: URL'
);
assert.notInclude(
image,
'image.png',
'the external href must not survive in the output'
);
})
.then(done)
.catch(done);
});
it('inlines out-of-subtree SVG referenced by <use> (#215)', function (done) {
// #215: a <use> that references a <symbol>/element defined OUTSIDE the
// rendered subtree would render nothing, because the referenced node was
// never cloned. We now collect the target and inject it into the output
// SVG so the reference resolves in the standalone image.
loadTestPage()
.then(function () {
const sprite =
'<svg style="display:none" xmlns="http://www.w3.org/2000/svg">' +
'<symbol id="diamond" viewBox="0 0 10 10">' +
'<rect id="symrect" x="2" y="2" width="6" height="6" fill="red"></rect>' +
'</symbol></svg>';
// Sibling of #dom-node, NOT inside it — so it is never cloned.
document
.querySelector('#test-root')
.insertAdjacentHTML('afterbegin', sprite);
domNode().innerHTML =
'<svg width="20" height="20" xmlns="http://www.w3.org/2000/svg">' +
'<use href="#diamond" width="20" height="20"></use></svg>';
return renderToSvg(domNode());
})
.then(function (svg) {
const decoded = decodeURIComponent(svg);
// The referenced symbol (and its contents) must be present in
// the standalone output, otherwise <use href="#diamond"> is
// dangling and nothing rasterizes.
assert.include(
decoded,
'id="diamond"',
'referenced <symbol> should be injected into the output'
);
assert.include(
decoded,
'id="symrect"',
"referenced symbol's contents should be injected"
);
})
.then(done)
.catch(done);
});
it('does not duplicate <use> targets already in the subtree (#215)', function (done) {
// #215: when the referenced id already exists inside the rendered
// subtree, we must NOT inject a duplicate of it.
loadTestPage()
.then(function () {
domNode().innerHTML =
'<svg width="20" height="20" xmlns="http://www.w3.org/2000/svg">' +
'<defs><rect id="inside" x="0" y="0" width="6" height="6" fill="blue"></rect></defs>' +
'<use href="#inside" width="20" height="20"></use></svg>';
return renderToSvg(domNode());
})
.then(function (svg) {
const decoded = decodeURIComponent(svg);
const occurrences =
decoded.split('id="inside"').length - 1;
assert.equal(
occurrences,
1,
'in-subtree target must not be duplicated'
);
})
.then(done)
.catch(done);
});
it('should render nested svg with broken namespace', function (done) {
// svg/path declare the (wrong) XHTML namespace; the path must still
// survive serialization rather than being dropped or throwing.
loadTestPage('svg-ns/dom-node.html', 'svg-ns/style.css')
.then(function () {
return renderToSvg(domNode());
})
.then(function (svg) {
assert.include(
decodeURIComponent(svg),
'M10 10 H 90 V 90 H 10 L 10 10',
'the path data must survive the broken namespace'
);
})
.then(done)
.catch(done);
});
it('should render svg <rect> with width and height', function (done) {
loadTestPage('svg-rect/dom-node.html', 'svg-rect/style.css')
.then(function () {
return renderToSvg(domNode());
})
.then(function (svg) {
const rect = (decodeURIComponent(svg).match(
/<rect[^>]*\/?>/
) || [])[0];
assert.isString(rect, '<rect> should be in the output');
assert.match(
rect,
/width="100"/,
'rect width must be preserved'
);
assert.match(
rect,
/height="100"/,
'rect height must be preserved'
);
})
.then(done)
.catch(done);
});
it('should render an HTML <a> (not confused with SVG <a>)', function (done) {
// issue #90: an HTML anchor must round-trip with its attribute-selector
// styling applied, not be treated as an SVG <a> element.
loadTestPage('svg-styles/dom-node.html', 'svg-styles/style.css')
.then(function () {
return renderToSvg(domNode());
})
.then(function (svg) {
const decoded = decodeURIComponent(svg);
const anchor = (decoded.match(/<a [^>]*>/) || [])[0];
assert.isString(anchor, '<a> should be in the output');
assert.include(
anchor,
'href="#dom-node"',
'anchor href must round-trip'
);
assert.match(
anchor,
/color:\s*rgb\(0,\s*0,\s*0\)/,
'attribute-selector color must be applied'
);
})
.then(done)
.catch(done);
});
itImage(
'should render open shadow DOM roots with assigned nodes intact',
function (done) {
this.timeout(60000);
loadTestPage(
'shadow-dom/dom-node.html',
'shadow-dom/styles.css',
'shadow-dom/control-image'
)
.then(renderToPngAndCheck)
.then(done)
.catch(done);
}
);
it('should not get fooled by math elements', function (done) {
loadTestPage()
.then(function () {
domNode().innerHTML =
'<math id="m"><mrow><mi>x</mi><mo>+</mo><mn>1</mn></mrow></math>';
return renderToSvg(domNode());
})
.then(function (svg) {
const decoded = decodeURIComponent(svg);
// MathML must not break the default-style/ascent-stopper path
// (lowercase 'math'); the render succeeds and structure survives.
assert.include(
decoded,
'id="m"',
'the <math> element should render without error'
);
assert.include(
decoded,
'<mi',
'math children should be preserved'
);
})
.then(done)
.catch(done);
});
});
describe('images', function () {
it('inlines a CSS mask-image url (#195)', function (done) {
// #195: an SVG icon rendered on an element via a CSS mask
// (`mask: url(icon.svg); background: currentColor`) came out blank because
// the inliner only inlined `background`/`background-image`, leaving the mask
// url external — and the standalone output can't fetch external resources.
// The inliner now inlines mask urls too.
this.timeout(15000);
const url = '/base/spec/resources/images/image.png';
loadTestPage()
.then(function () {
domNode().innerHTML =
'<i id="maskicon" style="display:inline-block;width:20px;height:20px;' +
'background-color:red;-webkit-mask-image:url(' +
url +
');mask-image:url(' +
url +
')"></i>';
return renderToSvg(domNode());
})
.then(function (svg) {
const decoded = decodeURIComponent(svg);
const mask = (decoded.match(/<i id="maskicon"[^>]*>/) ||
[])[0];
assert.isString(
mask,
'masked icon should be in the output'
);
assert.include(
mask,
'data:image',
'the mask url must be inlined as a data: URL'
);
assert.notInclude(
mask,
'image.png',
'the external mask url must not survive (unfetchable in the output)'
);
})
.then(done)
.catch(done);
});
it('inlines a CSS mask shorthand url (#127)', function (done) {
// #127: the `-webkit-mask` / `mask` *shorthand* (not just the
// `-image` longhand) carries the url; it must be inlined too or the
// masked icon renders blank in the standalone output.
this.timeout(15000);
const url = '/base/spec/resources/images/image.png';
loadTestPage()
.then(function () {
domNode().innerHTML =
'<i id="shortmask" style="display:inline-block;width:20px;height:20px;' +
'background-color:red;-webkit-mask:url(' +
url +
') center / contain no-repeat;mask:url(' +
url +
') center / contain no-repeat"></i>';
return renderToSvg(domNode());
})
.then(function (svg) {
const decoded = decodeURIComponent(svg);
const mask = (decoded.match(/<i id="shortmask"[^>]*>/) ||
[])[0];
assert.isString(
mask,
'masked icon should be in the output'
);
assert.include(
mask,
'data:image',
'the shorthand mask url must be inlined as a data: URL'
);
assert.notInclude(
mask,
'image.png',
'the external mask url must not survive (unfetchable in the output)'
);
})
.then(done)
.catch(done);
});
it('inlines a ::before background-image url (#16)', function (done) {
// A background-image on a ::before/::after pseudo-element lives
// inside a <style> rule that the element-style image inliner
// never visits.
this.timeout(15000);
const url = '/base/spec/resources/images/image.png';
const style = document.createElement('style');
style.id = 'pseudo16';
style.textContent =
'#p16::before { content: ""; display: inline-block;' +
' width: 20px; height: 20px; background-image: url(' +
url +
'); }';
document.head.appendChild(style);
function cleanup() {
const el = document.getElementById('pseudo16');
if (el) {
el.remove();
}
}
loadTestPage()
.then(function () {
domNode().innerHTML = '<div id="p16">x</div>';
return renderToSvg(domNode());
})
.then(function (svg) {
const decoded = decodeURIComponent(svg);
assert.include(
decoded,
'data:image',
'the ::before background-image must be inlined as a data: URL'
);
assert.notInclude(
decoded,
'image.png',
'the external pseudo background url must not survive'
);
})
.then(cleanup)
.then(done)
.catch(function (e) {
cleanup();
done(e);
});
});
it('adjustPseudoElement returning false drops a ::after pseudo-element (#244)', function (done) {
const style = document.createElement('style');
style.id = 'pf244';
style.textContent =
'#pf244host::before { content: "KEEPBEFORE244"; }' +
' #pf244host::after { content: "DROPAFTER244"; }';
document.head.appendChild(style);
function cleanup() {
const el = document.getElementById('pf244');
if (el) {
el.remove();
}
}
loadTestPage()
.then(function () {
domNode().innerHTML = '<div id="pf244host">x</div>';
return renderToSvg(domNode(), {
adjustPseudoElement: function (node, pseudo) {
return node.id === 'pf244host' &&
pseudo === ':after'
? false
: undefined;
},
});
})
.then(function (svg) {
const decoded = decodeURIComponent(svg);
assert.include(
decoded,
'KEEPBEFORE244',
'the kept ::before content must survive'
);
assert.notInclude(
decoded,
'DROPAFTER244',
'the filtered-out ::after must not be recreated'
);
})
.then(cleanup)
.then(done)
.catch(function (e) {
cleanup();
done(e);
});
});
it('adjustPseudoElement receives the node, pseudo, and style; true keeps it (#244)', function (done) {
const style = document.createElement('style');
style.id = 'pf244b';
style.textContent =
'#pf244bhost::before { content: "BOTH244"; }' +
' #pf244bhost::after { content: "BOTH244"; }';
document.head.appendChild(style);
const seen = [];
function cleanup() {
const el = document.getElementById('pf244b');
if (el) {
el.remove();
}
}
loadTestPage()
.then(function () {
domNode().innerHTML = '<div id="pf244bhost">x</div>';
return renderToSvg(domNode(), {
adjustPseudoElement: function (node, pseudo, s) {
seen.push({
id: node && node.id,
pseudo: pseudo,
hasStyle:
typeof s.getPropertyValue === 'function',
});
return true;
},
});
})
.then(function (svg) {
const pseudos = seen
.filter(function (c) {
return c.id === 'pf244bhost';
})
.map(function (c) {
return c.pseudo;
});
assert.include(pseudos, ':before');
assert.include(pseudos, ':after');
assert.isTrue(
seen.every(function (c) {
return c.hasStyle;
}),
'style argument must be a CSSStyleDeclaration'
);
assert.include(
decodeURIComponent(svg),
'BOTH244',
'returning true keeps the pseudo-elements'
);
})
.then(cleanup)
.then(done)
.catch(function (e) {
cleanup();
done(e);
});
});
it('adjustPseudoElement returning overrides tweaks the pseudo (em-dash → hyphen) (#244)', function (done) {
const style = document.createElement('style');
style.id = 'pf244c';
style.textContent =
'#pf244chost::before { content: "EMDASH\\2014END244"; }';
document.head.appendChild(style);
function cleanup() {
const el = document.getElementById('pf244c');
if (el) {
el.remove();
}
}
loadTestPage()
.then(function () {
domNode().innerHTML = '<div id="pf244chost">x</div>';
return renderToSvg(domNode(), {
adjustPseudoElement: function (node, pseudo) {
// swap the em-dash glyph for a hyphen on the
// host's ::before
return node.id === 'pf244chost' &&
pseudo === ':before'
? { content: '"HYPHEN-END244"' }
: undefined;
},
});
})
.then(function (svg) {
assert.include(
decodeURIComponent(svg),
'HYPHEN-END244',
'the content override must be applied (and win)'
);
})
.then(cleanup)
.then(done)
.catch(function (e) {
cleanup();
done(e);
});
});
it('adjustPseudoElement returning an empty object keeps the pseudo unchanged (#244)', function (done) {
const style = document.createElement('style');
style.id = 'pf244d';
style.textContent =
'#pf244dhost::before { content: "EMPTYOBJ244"; }';
document.head.appendChild(style);
function cleanup() {
const el = document.getElementById('pf244d');
if (el) {
el.remove();
}
}
loadTestPage()
.then(function () {
domNode().innerHTML = '<div id="pf244dhost">x</div>';
return renderToSvg(domNode(), {
adjustPseudoElement: function () {
return {}; // adjust nothing — keep as-is
},
});
})
.then(function (svg) {
assert.include(
decodeURIComponent(svg),
'EMPTYOBJ244',
'an empty overrides object must keep the pseudo'
);
})
.then(cleanup)
.then(done)
.catch(function (e) {
cleanup();
done(e);
});
});
it('emits clean url() quotes for background-image (#191)', function (done) {
// #191: a CSS `url()` set via options.style (the browser normalizes it to
// double quotes) was serialized inside the double-quoted style attribute as
// `url("…")`. We now emit clean single-quoted `url('…')`.
loadTestPage()
.then(function () {
domNode().innerHTML = '<div id="bg">x</div>';
return domtoimage.toSvg(document.getElementById('bg'), {
style: {
'background-image':
"url('https://example.com/img.png')",
},
});
})
.then(function (svg) {
const bg = (svg.match(/<div id="bg"[^>]*>/) || [])[0];
assert.isString(
bg,
'the styled div should be in the output'
);
assert.notInclude(
bg,
'"',
'url() quotes must not be HTML-escaped'
);
assert.include(
bg,
"url('https://example.com/img.png')",
'background-image should carry a clean single-quoted url()'
);
})
.then(done)
.catch(done);
});
it('should render images', function (done) {
this.timeout(30000);
loadTestPage('images/dom-node.html', 'images/style.css')
.then(renderToPng)
.then(drawDataUrl)
.then(assertTextRendered(['PNG', 'JPG']))
.then(done)
.catch(done);
});
itImage('should render active image in srcset', function (done) {
this.timeout(30000);
loadTestPage(
'srcset/dom-node.html',
'srcset/style.css',
'srcset/control-image'
)
.then(renderToPng)
.then(check)
.then(done)
.catch(done);
});
itImage('should render background images', function (done) {
loadTestPage(
'css-bg/dom-node.html',
'css-bg/style.css',
'css-bg/control-image'
)
.then(renderToPngAndCheck)
.then(done)
.catch(done);
});
itImage('should render iframe of street view', function (done) {
this.timeout(60000);
loadTestPage(
'iframe/street-view.html',
'iframe/style.css',
'iframe/control-image'
)
.then(renderToPngAndCheck)
.then(done)
.catch(done);
});
it('should render content from <canvas>', function (done) {
loadTestPage('canvas/dom-node.html', 'canvas/style.css')
.then(function () {
const canvas = document.getElementById('content');
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#000000';
ctx.font = '100px monospace';
ctx.fillText('0', canvas.width / 2, canvas.height / 2);
})
.then(renderToPng)
.then(drawDataUrl)
.then(assertTextRendered(['0']))
.then(done)
.catch(done);
});
it('should handle zero-width <canvas>', function (done) {
loadTestPage('canvas/empty-data.html', 'canvas/empty-style.css')
.then(renderToSvg)
.then(function (dataUrl) {
const img = new Image();
document.getElementById('result').appendChild(img);
img.src = dataUrl;
})
.then(done)
.catch(done);
});
it('should fill the whole canvas to its edges (guards Firefox crop #160 / blank #146)', function (done) {
// A solid-color box must cover every corner of the output. Firefox
// could crop a <foreignObject> capture to a default intrinsic box
// (#160) or read a blank canvas before the image finished decoding
// (#146); both leave outer pixels transparent. The fixes draw into an
// explicit destination rectangle and await decode() before reading.
// Solid-color pixel sampling is OS-independent, so this guards both
// engines in CI (where the real Firefox crop/blank would surface).
loadTestPage()
.then(function () {
domNode().innerHTML =
'<div id="fill" style="width:60px;height:40px;background-color:red"></div>';
return domtoimage.toPixelData(
document.getElementById('fill')
);
})
.then(function (px) {
const width = 60;
function offset(x, y) {
return (y * width + x) * 4;
}
[
['center', offset(30, 20)],
['top-left', offset(2, 2)],
['top-right', offset(57, 2)],
['bottom-left', offset(2, 37)],
['bottom-right', offset(57, 37)],
].forEach(function (sample) {
const name = sample[0];
const i = sample[1];
assert.equal(px[i], 255, name + ' red channel');
assert.equal(px[i + 1], 0, name + ' green channel');
assert.equal(px[i + 2], 0, name + ' blue channel');
assert.isAbove(px[i + 3], 200, name + ' opaque');
});
})
.then(done)
.catch(done);
});
});
describe('fonts', function () {
it('captures an icon-font glyph from a ::before pseudo-element (#149)', function (done) {
// #149: an icon delivered as an `@font-face` glyph via a `::before`
// pseudo-element. The composition works — the web font is embedded and the
// pseudo-element's `content` glyph + font-family are captured. (The original
// report's failure was an external icon font that couldn't be embedded —
// the documented web-font/CORS limitation — not a distinct bug.)
this.timeout(15000);
const style = document.createElement('style');
style.id = 's149';
style.textContent =
"@font-face { font-family: 'Ico149'; src: url('/base/tests/fontawesome/webfonts/fa-solid-900.woff2') format('woff2'); }" +
'#icon::before { content: "\\2605"; font-family: "Ico149"; font-size: 30px; }';
document.head.appendChild(style);
function cleanup() {
const el = document.getElementById('s149');
if (el) {
el.remove();
}
}
loadTestPage()
.then(function () {
domNode().innerHTML = '<span id="icon"></span>';
return renderToSvg(domNode());
})
.then(function (svg) {
const decoded = decodeURIComponent(svg);
// The web font must be inlined...
assert.include(
decoded,
'base64',
'the @font-face glyph font must be embedded'
);
// ...and the pseudo-element must keep its glyph + font.
assert.include(
decoded,
'content: "★"',
'the ::before glyph content must be preserved'
);
assert.include(
decoded,
'Ico149',
'the icon font-family must reach the pseudo-element'
);
})
.then(cleanup)
.then(done)
.catch(function (e) {
cleanup();
done(e);
});
});
it('preserves an overridden font-size on headings (#227)', function (done) {
// #227 (part 2): an element whose UA font-size is relative to its parent
// (h1–h6 are N.Nem). When the page overrides it to coincide with both the
// context-free sandbox default AND the parent, the diff dropped it — but
// the standalone output resolves the UA relative rule against a different
// parent font-size, so it diverged. We now always emit font-size for such
// elements.
const style = document.createElement('style');
style.id = 'reset-227b';
// Parent 24px; h2 overridden to 1em (= 24px), which coincides with
// the UA-default h2 (1.5em of 16px = 24px) and the parent — the exact
// drop case. Without the fix the output h2 re-applies UA 1.5em → 36px.
style.textContent =
'#dom-node { font-size: 24px; } #hd { font-size: 1em; }';
document.head.appendChild(style);
function cleanup() {
const el = document.getElementById('reset-227b');
if (el) {
el.remove();
}
}
let liveFontSize = '';
loadTestPage()
.then(function () {
domNode().innerHTML = '<h2 id="hd">Heading</h2>';
liveFontSize = getComputedStyle(
document.getElementById('hd')
).getPropertyValue('font-size');
return renderToSvg(domNode());
})
.then(function (svg) {
const decoded = decodeURIComponent(svg);
const h2 = (decoded.match(/<h2 id="hd"[^>]*>/) || [])[0];
assert.isString(h2, 'h2 should be in the output');
assert.match(
h2,
new RegExp(
'font-size:\\s*' +
liveFontSize.replace('.', '\\.')
),
'h2 must pin its overridden font-size (' +
liveFontSize +
') so the UA 1.5em rule doe