diff --git a/bench/Dockerfile b/bench/Dockerfile new file mode 100644 index 0000000..faf6ee3 --- /dev/null +++ b/bench/Dockerfile @@ -0,0 +1,16 @@ +FROM node:20-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + chromium \ + fonts-liberation \ + && rm -rf /var/lib/apt/lists/* + +ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true +ENV CHROMIUM_PATH=/usr/bin/chromium + +WORKDIR /bench +COPY bench-runner.mjs package.json ./ + +RUN npm install + +ENTRYPOINT ["node", "bench-runner.mjs"] diff --git a/bench/README.md b/bench/README.md new file mode 100644 index 0000000..492b71b --- /dev/null +++ b/bench/README.md @@ -0,0 +1,66 @@ +# Style synchronization benchmark + +Measures morph time for elements with inline styles to compare +`setAttribute("style")` (master) vs `style.cssText` (fix). + +## Quick start (Docker) + +```bash +# From the morphdom root directory: +npm run build +docker build -t morphdom-bench ./bench +docker run --rm \ + -v "$(pwd)/dist:/morphdom/dist:ro" \ + -v "$(pwd)/bench/style-benchmark.html:/bench/style-benchmark.html:ro" \ + morphdom-bench +``` + +To compare branches: + +```bash +# 1. Benchmark the fix branch +git checkout fix/csp-safe-style-sync +npm run build +docker run --rm \ + -v "$(pwd)/dist:/morphdom/dist:ro" \ + -v "$(pwd)/bench/style-benchmark.html:/bench/style-benchmark.html:ro" \ + morphdom-bench + +# 2. Benchmark master +git checkout master +npm run build +docker run --rm \ + -v "$(pwd)/dist:/morphdom/dist:ro" \ + -v "$(pwd)/bench/style-benchmark.html:/bench/style-benchmark.html:ro" \ + morphdom-bench +``` + +## Browser (manual) + +Open `bench/style-benchmark.html` in any browser. Click "Run extended" for +more precise results (50 iterations with outlier trimming). + +## Configuration + +The Docker runner accepts environment variables: + +- `ITERATIONS` — number of iterations per scenario (default: 30) + +```bash +docker run --rm -e ITERATIONS=50 \ + -v "$(pwd)/dist:/morphdom/dist:ro" \ + -v "$(pwd)/bench/style-benchmark.html:/bench/style-benchmark.html:ro" \ + morphdom-bench +``` + +## Scenarios + +| Scenario | Purpose | +| --------------------------------- | --------------------------------------- | +| 1000 els, 5 props, 100% changed | Moderate worst case | +| 1000 els, 20 props, 100% changed | Heavy worst case | +| 1000 els, 5/20 props, 10% changed | Realistic incremental update | +| 1000 els, 20 props, 0% changed | Fast-path (no mutation) | +| 5000 els, 5 props, 100% changed | Scale test | +| 100 els, 20 props + CSS vars | CSS custom properties | +| 1000 els, no style (baseline) | Regression check for non-style elements | diff --git a/bench/RESULTS.md b/bench/RESULTS.md new file mode 100644 index 0000000..4cf77fd --- /dev/null +++ b/bench/RESULTS.md @@ -0,0 +1,66 @@ +# morphdom style benchmark results + +Environment: Docker node:20-slim + Chromium, headless, 30 iterations (trimmed mean) + +## master (setAttribute) + +| Scenario | Mean (ms) | Median (ms) | StdDev | Min | Max | +| -------------------------------- | --------- | ----------- | ------ | ----- | ----- | +| 1000 els, 5 props, 100% changed | 3.77 | 3.70 | 0.31 | 3.30 | 4.40 | +| 1000 els, 20 props, 100% changed | 5.46 | 5.50 | 0.26 | 5.10 | 6.10 | +| 1000 els, 5 props, 10% changed | 1.79 | 1.70 | 0.22 | 1.50 | 2.30 | +| 1000 els, 20 props, 10% changed | 2.09 | 2.00 | 0.38 | 1.70 | 3.00 | +| 1000 els, 20 props, 0% changed | 1.61 | 1.50 | 0.27 | 1.40 | 2.30 | +| 5000 els, 5 props, 100% changed | 16.80 | 16.20 | 1.82 | 14.70 | 20.90 | +| 100 els, 20 props + CSS vars | 0.67 | 0.70 | 0.06 | 0.60 | 0.80 | +| 1000 els, no style (baseline) | 1.20 | 1.20 | 0.06 | 1.10 | 1.30 | + +## fix v1 — syncStyle property-by-property (REJECTED) + +| Scenario | Mean (ms) | Median (ms) | StdDev | Min | Max | +| -------------------------------- | --------- | ----------- | ------ | ----- | ----- | +| 1000 els, 5 props, 100% changed | 15.26 | 15.30 | 0.86 | 13.80 | 16.70 | +| 1000 els, 20 props, 100% changed | 61.18 | 61.40 | 1.76 | 57.90 | 63.90 | +| 1000 els, 5 props, 10% changed | 5.66 | 5.50 | 0.33 | 5.40 | 6.30 | +| 1000 els, 20 props, 10% changed | 19.23 | 19.00 | 0.73 | 18.30 | 20.60 | +| 1000 els, 20 props, 0% changed | 15.00 | 15.00 | 0.50 | 14.10 | 16.00 | +| 5000 els, 5 props, 100% changed | 75.53 | 75.40 | 2.93 | 71.50 | 81.60 | +| 100 els, 20 props + CSS vars | 6.93 | 7.00 | 0.25 | 6.50 | 7.50 | +| 1000 els, no style (baseline) | 1.27 | 1.20 | 0.13 | 1.10 | 1.70 | + +4-11x slower than master. Rejected due to property-by-property loop overhead. + +## fix v3 — getAttribute + style.cssText (CURRENT) + +| Scenario | Mean (ms) | Median (ms) | StdDev | Min | Max | +| -------------------------------- | --------- | ----------- | ------ | ----- | ----- | +| 1000 els, 5 props, 100% changed | 5.51 | 5.40 | 0.64 | 4.40 | 7.30 | +| 1000 els, 20 props, 100% changed | 13.73 | 13.70 | 0.73 | 12.70 | 15.90 | +| 1000 els, 5 props, 10% changed | 1.90 | 1.90 | 0.18 | 1.70 | 2.50 | +| 1000 els, 20 props, 10% changed | 2.92 | 2.80 | 0.37 | 2.50 | 3.80 | +| 1000 els, 20 props, 0% changed | 1.57 | 1.50 | 0.26 | 1.40 | 2.30 | +| 5000 els, 5 props, 100% changed | 24.35 | 24.40 | 1.70 | 22.00 | 27.60 | +| 100 els, 20 props + CSS vars | 1.32 | 1.30 | 0.04 | 1.30 | 1.40 | +| 1000 els, no style (baseline) | 1.26 | 1.20 | 0.10 | 1.10 | 1.50 | + +## Final comparison (v3 vs master) + +| Scenario | master | v3 | Factor | +| -------------------------------- | ------ | ----- | --------------- | +| 1000 els, 5 props, 100% changed | 3.77 | 5.51 | 1.46x slower | +| 1000 els, 20 props, 100% changed | 5.46 | 13.73 | 2.51x slower | +| 1000 els, 5 props, 10% changed | 1.79 | 1.90 | 1.06x (neutral) | +| 1000 els, 20 props, 10% changed | 2.09 | 2.92 | 1.40x slower | +| 1000 els, 20 props, 0% changed | 1.61 | 1.57 | 0.97x (neutral) | +| 5000 els, 5 props, 100% changed | 16.80 | 24.35 | 1.45x slower | +| 100 els, 20 props + CSS vars | 0.67 | 1.32 | 1.97x slower | +| 1000 els, no style (baseline) | 1.20 | 1.26 | 1.05x (neutral) | + +## Analysis + +- **0% changed (most common in LiveView re-renders): identical to master** (0.97x) +- **10% changed (typical incremental update): ~1.06-1.40x** — negligible +- **100% changed (worst case, all 1000+ elements change all styles): 1.5-2.5x** — the minimum cost of CSP-safe CSSOM setter vs setAttribute +- **No style (baseline): identical** — zero overhead for elements without style +- The remaining overhead in 100% changed is intrinsic to `style.cssText = value` (CSSOM parsing + layout invalidation) vs `setAttribute("style")` (string-only) +- v3 approach: compare via `getAttribute("style")` (cheap string read), write via `style.cssText` (CSP-safe CSSOM setter) diff --git a/bench/bench-runner.mjs b/bench/bench-runner.mjs new file mode 100644 index 0000000..7728dd2 --- /dev/null +++ b/bench/bench-runner.mjs @@ -0,0 +1,144 @@ +import puppeteer from 'puppeteer-core'; +import { readFileSync } from 'fs'; +import path from 'path'; + +const CHROMIUM = process.env.CHROMIUM_PATH || '/usr/bin/chromium'; +const ITERATIONS = parseInt(process.env.ITERATIONS || '30', 10); +const MORPHDOM_PATH = '/morphdom/dist/morphdom-umd.js'; + +const benchHtml = readFileSync('/bench/style-benchmark.html', 'utf8'); + +// Inject morphdom from mounted volume instead of relative path +const patchedHtml = benchHtml.replace( + '', + '' +); + +const browser = await puppeteer.launch({ + executablePath: CHROMIUM, + args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'], + headless: true, +}); + +const page = await browser.newPage(); +await page.setContent(patchedHtml, { waitUntil: 'domcontentloaded' }); + +// Run benchmark inside page context +const results = await page.evaluate((iterations) => { + /* globals morphdom, performance */ + + var STYLE_PROPS = [ + 'color', 'background-color', 'padding', 'margin', 'font-size', + 'border', 'border-radius', 'opacity', 'display', 'width', + 'height', 'max-width', 'min-height', 'line-height', 'letter-spacing', + 'text-align', 'overflow', 'position', 'top', 'left' + ]; + var VALUES_A = [ + 'red', '#f0f0f0', '10px', '5px', '14px', + '1px solid #ccc', '4px', '1', 'block', '100px', + '50px', '200px', '20px', '1.5', '0.5px', + 'left', 'hidden', 'relative', '0px', '0px' + ]; + var VALUES_B = [ + 'blue', '#333333', '20px', '15px', '18px', + '2px solid #999', '8px', '0.8', 'flex', '150px', + '80px', '300px', '40px', '1.8', '1px', + 'center', 'auto', 'absolute', '10px', '20px' + ]; + + function styleStr(values, count, cssVarSuffix) { + var parts = []; + for (var i = 0; i < count && i < STYLE_PROPS.length; i++) { + parts.push(STYLE_PROPS[i] + ':' + values[i]); + } + return parts.join(';') + (cssVarSuffix || ''); + } + + function buildTree(elCount, propCount, variant, changeRatio, cssVars) { + var wrap = document.createElement('div'); + wrap.id = 'bench-root'; + var sA = propCount > 0 ? styleStr(VALUES_A, propCount, cssVars ? ';--value:10;--size:4rem' : '') : null; + var sB = propCount > 0 ? styleStr(VALUES_B, propCount, cssVars ? ';--value:75;--size:6rem' : '') : null; + for (var i = 0; i < elCount; i++) { + var el = document.createElement('div'); + el.id = 'item-' + i; + el.textContent = 'item ' + i; + var useB = variant === 'b' && (changeRatio >= 1.0 || (i / elCount) < changeRatio); + var s = useB ? sB : sA; + if (s) el.setAttribute('style', s); + wrap.appendChild(el); + } + return wrap; + } + + function measure(sc) { + var times = []; + var container = document.createElement('div'); + container.style.display = 'none'; + document.body.appendChild(container); + + for (var iter = 0; iter < iterations; iter++) { + var from = buildTree(sc.elCount, sc.propCount, 'a', sc.changeRatio, sc.cssVars); + container.textContent = ''; + container.appendChild(from); + var to = buildTree(sc.elCount, sc.propCount, 'b', sc.changeRatio, sc.cssVars); + from.offsetHeight; // force layout + var t0 = performance.now(); + morphdom(from, to); + var t1 = performance.now(); + times.push(t1 - t0); + } + document.body.removeChild(container); + + times.sort(function(a, b) { return a - b; }); + var trim = Math.floor(times.length * 0.1); + var t = times.slice(trim, times.length - trim); + if (t.length === 0) t = times; + var sum = 0; + for (var i = 0; i < t.length; i++) sum += t[i]; + var mean = sum / t.length; + var vari = 0; + for (var i = 0; i < t.length; i++) vari += (t[i] - mean) * (t[i] - mean); + return { + mean: mean, + median: t[Math.floor(t.length / 2)], + stddev: Math.sqrt(vari / t.length), + min: t[0], + max: t[t.length - 1] + }; + } + + var scenarios = [ + { name: '1000 els, 5 props, 100% changed', elCount: 1000, propCount: 5, changeRatio: 1.0 }, + { name: '1000 els, 20 props, 100% changed', elCount: 1000, propCount: 20, changeRatio: 1.0 }, + { name: '1000 els, 5 props, 10% changed', elCount: 1000, propCount: 5, changeRatio: 0.1 }, + { name: '1000 els, 20 props, 10% changed', elCount: 1000, propCount: 20, changeRatio: 0.1 }, + { name: '1000 els, 20 props, 0% changed', elCount: 1000, propCount: 20, changeRatio: 0.0 }, + { name: '5000 els, 5 props, 100% changed', elCount: 5000, propCount: 5, changeRatio: 1.0 }, + { name: '100 els, 20 props + CSS vars', elCount: 100, propCount: 20, changeRatio: 1.0, cssVars: true }, + { name: '1000 els, no style (baseline)', elCount: 1000, propCount: 0, changeRatio: 0 } + ]; + + var out = []; + for (var s = 0; s < scenarios.length; s++) { + var r = measure(scenarios[s]); + out.push({ name: scenarios[s].name, mean: r.mean, median: r.median, stddev: r.stddev, min: r.min, max: r.max }); + } + return out; +}, ITERATIONS); + +await browser.close(); + +// Print results as markdown table +console.log('| Scenario | Mean (ms) | Median (ms) | StdDev | Min | Max |'); +console.log('|----------|-----------|-------------|--------|-----|-----|'); +for (const r of results) { + console.log( + '| ' + r.name + + ' | ' + r.mean.toFixed(2) + + ' | ' + r.median.toFixed(2) + + ' | ' + r.stddev.toFixed(2) + + ' | ' + r.min.toFixed(2) + + ' | ' + r.max.toFixed(2) + ' |' + ); +} diff --git a/bench/package.json b/bench/package.json new file mode 100644 index 0000000..b11c110 --- /dev/null +++ b/bench/package.json @@ -0,0 +1,8 @@ +{ + "name": "morphdom-bench", + "private": true, + "type": "module", + "dependencies": { + "puppeteer-core": "^24.0.0" + } +} diff --git a/bench/style-benchmark.html b/bench/style-benchmark.html new file mode 100644 index 0000000..cf9b1bc --- /dev/null +++ b/bench/style-benchmark.html @@ -0,0 +1,241 @@ + + + + +morphdom style synchronization benchmark + + + + +

morphdom — style synchronization benchmark

+

+ Measures morph time for elements with inline styles.
+ Run on master (setAttribute) and on fix/csp-safe-style-sync (syncStyle) to compare. +

+ + + + + +
+
+ + + + + + diff --git a/src/morphAttrs.js b/src/morphAttrs.js index b07d709..985e05f 100644 --- a/src/morphAttrs.js +++ b/src/morphAttrs.js @@ -1,63 +1,80 @@ var DOCUMENT_FRAGMENT_NODE = 11; export default function morphAttrs(fromNode, toNode) { - var toNodeAttrs = toNode.attributes; - var attr; - var attrName; - var attrNamespaceURI; - var attrValue; - var fromValue; - - // document-fragments dont have attributes so lets not do anything - if (toNode.nodeType === DOCUMENT_FRAGMENT_NODE || fromNode.nodeType === DOCUMENT_FRAGMENT_NODE) { - return; - } + var toNodeAttrs = toNode.attributes; + var attr; + var attrName; + var attrNamespaceURI; + var attrValue; + var fromValue; + + // document-fragments dont have attributes so lets not do anything + if ( + toNode.nodeType === DOCUMENT_FRAGMENT_NODE || + fromNode.nodeType === DOCUMENT_FRAGMENT_NODE + ) { + return; + } + + // update attributes on original DOM element + for (var i = toNodeAttrs.length - 1; i >= 0; i--) { + attr = toNodeAttrs[i]; + attrName = attr.name; + attrNamespaceURI = attr.namespaceURI; + attrValue = attr.value; - // update attributes on original DOM element - for (var i = toNodeAttrs.length - 1; i >= 0; i--) { - attr = toNodeAttrs[i]; - attrName = attr.name; - attrNamespaceURI = attr.namespaceURI; - attrValue = attr.value; - - if (attrNamespaceURI) { - attrName = attr.localName || attrName; - fromValue = fromNode.getAttributeNS(attrNamespaceURI, attrName); - - if (fromValue !== attrValue) { - if (attr.prefix === 'xmlns'){ - attrName = attr.name; // It's not allowed to set an attribute with the XMLNS namespace without specifying the `xmlns` prefix - } - fromNode.setAttributeNS(attrNamespaceURI, attrName, attrValue); - } - } else { - fromValue = fromNode.getAttribute(attrName); - - if (fromValue !== attrValue) { - fromNode.setAttribute(attrName, attrValue); - } + if (attrNamespaceURI) { + attrName = attr.localName || attrName; + fromValue = fromNode.getAttributeNS(attrNamespaceURI, attrName); + + if (fromValue !== attrValue) { + if (attr.prefix === "xmlns") { + attrName = attr.name; // It's not allowed to set an attribute with the XMLNS namespace without specifying the `xmlns` prefix } - } + fromNode.setAttributeNS(attrNamespaceURI, attrName, attrValue); + } + } else { + if (attrName === "style") { + // Use style.cssText instead of setAttribute("style") to comply + // with Content Security Policy (CSP) style-src directives. + // setAttribute("style") is blocked by CSP without 'unsafe-inline', + // but the CSSOM API (style.cssText) is never blocked. + // We compare via getAttribute (cheap string read) and write via + // style.cssText (CSP-safe CSSOM setter). + fromValue = fromNode.getAttribute(attrName); + + if (fromValue !== attrValue) { + fromNode.style.cssText = attrValue; + } + } else { + fromValue = fromNode.getAttribute(attrName); - // Remove any extra attributes found on the original DOM element that - // weren't found on the target element. - var fromNodeAttrs = fromNode.attributes; - - for (var d = fromNodeAttrs.length - 1; d >= 0; d--) { - attr = fromNodeAttrs[d]; - attrName = attr.name; - attrNamespaceURI = attr.namespaceURI; - - if (attrNamespaceURI) { - attrName = attr.localName || attrName; - - if (!toNode.hasAttributeNS(attrNamespaceURI, attrName)) { - fromNode.removeAttributeNS(attrNamespaceURI, attrName); - } - } else { - if (!toNode.hasAttribute(attrName)) { - fromNode.removeAttribute(attrName); - } + if (fromValue !== attrValue) { + fromNode.setAttribute(attrName, attrValue); } + } + } + } + + // Remove any extra attributes found on the original DOM element that + // weren't found on the target element. + var fromNodeAttrs = fromNode.attributes; + + for (var d = fromNodeAttrs.length - 1; d >= 0; d--) { + attr = fromNodeAttrs[d]; + attrName = attr.name; + attrNamespaceURI = attr.namespaceURI; + + if (attrNamespaceURI) { + attrName = attr.localName || attrName; + + if (!toNode.hasAttributeNS(attrNamespaceURI, attrName)) { + fromNode.removeAttributeNS(attrNamespaceURI, attrName); + } + } else { + if (!toNode.hasAttribute(attrName)) { + fromNode.removeAttribute(attrName); + } } + } } diff --git a/test/browser/test.js b/test/browser/test.js index 1569886..13765ce 100644 --- a/test/browser/test.js +++ b/test/browser/test.js @@ -1692,4 +1692,161 @@ describe('morphdom' , function() { expect(div1).to.equal(div1_2); }); + + // ========================================================================= + // CSP-safe style synchronization + // ========================================================================= + + describe('CSP-safe style handling', function() { + it('should sync styles without calling setAttribute("style")', function() { + var el1 = document.createElement('div'); + el1.style.color = 'red'; + el1.style.padding = '10px'; + + var el2 = document.createElement('div'); + el2.style.color = 'blue'; + el2.style.margin = '5px'; + + // Spy on setAttribute to verify it is NOT called with "style" + var origSetAttribute = el1.setAttribute; + var styleSetAttrCalled = false; + el1.setAttribute = function(name, value) { + if (name === 'style') { + styleSetAttrCalled = true; + } + return origSetAttribute.call(this, name, value); + }; + + morphdom(el1, el2); + + expect(styleSetAttrCalled).to.equal(false); + expect(el1.style.color).to.equal('blue'); + expect(el1.style.margin).to.equal('5px'); + expect(el1.style.padding).to.equal(''); + }); + + it('should add style to an element that had none', function() { + var el1 = document.createElement('div'); + + var el2 = document.createElement('div'); + el2.style.color = 'red'; + el2.style.fontWeight = 'bold'; + + morphdom(el1, el2); + + expect(el1.style.color).to.equal('red'); + expect(el1.style.fontWeight).to.equal('bold'); + }); + + it('should remove style attribute entirely', function() { + var el1 = document.createElement('div'); + el1.style.color = 'red'; + el1.style.padding = '10px'; + + var el2 = document.createElement('div'); + + morphdom(el1, el2); + + expect(el1.hasAttribute('style')).to.equal(false); + }); + + it('should handle !important priority', function() { + var el1 = document.createElement('div'); + el1.style.setProperty('color', 'red', ''); + + var el2 = document.createElement('div'); + el2.style.setProperty('color', 'blue', 'important'); + + morphdom(el1, el2); + + expect(el1.style.getPropertyValue('color')).to.equal('blue'); + expect(el1.style.getPropertyPriority('color')).to.equal('important'); + }); + + it('should handle CSS custom properties (variables)', function() { + var el1 = document.createElement('div'); + el1.style.setProperty('--value', '0'); + el1.style.setProperty('--size', '4rem'); + + var el2 = document.createElement('div'); + el2.style.setProperty('--value', '75'); + el2.style.setProperty('--size', '6rem'); + + morphdom(el1, el2); + + expect(el1.style.getPropertyValue('--value').trim()).to.equal('75'); + expect(el1.style.getPropertyValue('--size').trim()).to.equal('6rem'); + }); + + it('should be idempotent — second morph with same style is a no-op', function() { + var el1 = document.createElement('div'); + el1.style.color = 'red'; + el1.style.padding = '10px'; + + var el2 = document.createElement('div'); + el2.style.color = 'blue'; + + morphdom(el1, el2); + + expect(el1.style.color).to.equal('blue'); + expect(el1.style.padding).to.equal(''); + + // Morph again with same target — cssText setter should not be called + // because getAttribute("style") returns the same value + var el3 = document.createElement('div'); + el3.style.color = 'blue'; + + var cssTextSet = false; + var origDescriptor = Object.getOwnPropertyDescriptor(CSSStyleDeclaration.prototype, 'cssText'); + Object.defineProperty(el1.style, 'cssText', { + get: function() { return origDescriptor.get.call(this); }, + set: function(v) { cssTextSet = true; return origDescriptor.set.call(this, v); }, + configurable: true + }); + + morphdom(el1, el3); + + expect(cssTextSet).to.equal(false); + expect(el1.style.color).to.equal('blue'); + }); + + it('should handle style morph from HTML string', function() { + var el1 = document.createElement('div'); + el1.id = 'test'; + el1.style.color = 'red'; + + morphdom(el1, '
'); + + expect(el1.style.color).to.equal('blue'); + expect(el1.style.fontSize).to.equal('14px'); + expect(el1.style.getPropertyValue('color')).to.equal('blue'); + }); + + it('should handle shorthand properties', function() { + var el1 = document.createElement('div'); + el1.style.margin = '10px'; + + var el2 = document.createElement('div'); + el2.style.margin = '20px'; + + morphdom(el1, el2); + + expect(el1.style.margin).to.equal('20px'); + }); + + it('should handle mixed style and other attribute changes', function() { + var el1 = document.createElement('div'); + el1.className = 'foo'; + el1.style.color = 'red'; + + var el2 = document.createElement('div'); + el2.className = 'bar'; + el2.style.color = 'blue'; + + morphdom(el1, el2); + + expect(el1.className).to.equal('bar'); + expect(el1.style.color).to.equal('blue'); + }); + }); }); diff --git a/test/fixtures/autotest/style-add/from.html b/test/fixtures/autotest/style-add/from.html new file mode 100644 index 0000000..85be96f --- /dev/null +++ b/test/fixtures/autotest/style-add/from.html @@ -0,0 +1 @@ +
Hello
\ No newline at end of file diff --git a/test/fixtures/autotest/style-add/to.html b/test/fixtures/autotest/style-add/to.html new file mode 100644 index 0000000..3f65977 --- /dev/null +++ b/test/fixtures/autotest/style-add/to.html @@ -0,0 +1 @@ +
Hello
\ No newline at end of file diff --git a/test/fixtures/autotest/style-change/from.html b/test/fixtures/autotest/style-change/from.html new file mode 100644 index 0000000..dd149b5 --- /dev/null +++ b/test/fixtures/autotest/style-change/from.html @@ -0,0 +1 @@ +
Hello
\ No newline at end of file diff --git a/test/fixtures/autotest/style-change/to.html b/test/fixtures/autotest/style-change/to.html new file mode 100644 index 0000000..f18469e --- /dev/null +++ b/test/fixtures/autotest/style-change/to.html @@ -0,0 +1 @@ +
Hello
\ No newline at end of file diff --git a/test/fixtures/autotest/style-css-variable/from.html b/test/fixtures/autotest/style-css-variable/from.html new file mode 100644 index 0000000..7c70bf9 --- /dev/null +++ b/test/fixtures/autotest/style-css-variable/from.html @@ -0,0 +1 @@ +
Counter
\ No newline at end of file diff --git a/test/fixtures/autotest/style-css-variable/to.html b/test/fixtures/autotest/style-css-variable/to.html new file mode 100644 index 0000000..d987f77 --- /dev/null +++ b/test/fixtures/autotest/style-css-variable/to.html @@ -0,0 +1 @@ +
Counter
\ No newline at end of file diff --git a/test/fixtures/autotest/style-remove/from.html b/test/fixtures/autotest/style-remove/from.html new file mode 100644 index 0000000..dd149b5 --- /dev/null +++ b/test/fixtures/autotest/style-remove/from.html @@ -0,0 +1 @@ +
Hello
\ No newline at end of file diff --git a/test/fixtures/autotest/style-remove/to.html b/test/fixtures/autotest/style-remove/to.html new file mode 100644 index 0000000..85be96f --- /dev/null +++ b/test/fixtures/autotest/style-remove/to.html @@ -0,0 +1 @@ +
Hello
\ No newline at end of file