diff --git a/karma.config.js b/karma.config.js index 6981d737a..73eace899 100644 --- a/karma.config.js +++ b/karma.config.js @@ -19,7 +19,8 @@ module.exports = config => config.set({ // local package files { pattern: 'src/index.js', watched: false }, { pattern: 'test/helpers.js', watched: false }, - { pattern: 'test/**/*.test.js', watched: false } + { pattern: 'test/**/*.test.js', watched: false }, + { pattern: 'test/assets/**', watched: false, included: false } ], proxies: { diff --git a/packages/dom/README.md b/packages/dom/README.md index 561147cf3..8f49b72aa 100644 --- a/packages/dom/README.md +++ b/packages/dom/README.md @@ -11,6 +11,7 @@ Serializes a document's DOM into a DOM string suitable for re-rendering. - [Frame elements](#frame-elements) - [CSSOM rules](#cssom-rules) - [Canvas elements](#canvas-elements) + - [Video elements](#video-elements) - [Other elements](#other-elements) ## Usage @@ -64,6 +65,12 @@ with image elements. The image elements reference the serialized data URI and ha attributes as their respective canvas elements. The image elements also have a max-width of 100% to accomidate responsive layouts in situations where canvases may be expected to resize with JS. +### Video elements + +Videos without a `poster` attribute will have the current frame of the video +serialized into an image and set as the `poster` attribute automatically. This is +to ensure videos have a stable image to display when screenshots are captured. + ### Other elements _All other elements are not serialized._ The resulting cloned document is passed to any provided diff --git a/packages/dom/src/prepare-dom.js b/packages/dom/src/prepare-dom.js index edefaf19a..d12a56d23 100644 --- a/packages/dom/src/prepare-dom.js +++ b/packages/dom/src/prepare-dom.js @@ -5,7 +5,7 @@ function uid() { // Marks elements that are to be serialized later with a data attribute. export function prepareDOM(dom) { - for (let elem of dom.querySelectorAll('input, textarea, select, iframe, canvas')) { + for (let elem of dom.querySelectorAll('input, textarea, select, iframe, canvas, video')) { if (!elem.getAttribute('data-percy-element-id')) { elem.setAttribute('data-percy-element-id', uid()); } diff --git a/packages/dom/src/serialize-dom.js b/packages/dom/src/serialize-dom.js index 7466affec..fb43398b9 100644 --- a/packages/dom/src/serialize-dom.js +++ b/packages/dom/src/serialize-dom.js @@ -3,6 +3,7 @@ import serializeInputs from './serialize-inputs'; import serializeFrames from './serialize-frames'; import serializeCSSOM from './serialize-cssom'; import serializeCanvas from './serialize-canvas'; +import serializeVideos from './serialize-video'; // Returns a copy or new doctype for a document. function doctype(dom) { @@ -34,6 +35,7 @@ export function serializeDOM(options) { let clone = dom.cloneNode(true); serializeInputs(dom, clone); serializeFrames(dom, clone, { enableJavaScript }); + serializeVideos(dom, clone); if (!enableJavaScript) { serializeCSSOM(dom, clone); diff --git a/packages/dom/src/serialize-video.js b/packages/dom/src/serialize-video.js new file mode 100644 index 000000000..fa7532568 --- /dev/null +++ b/packages/dom/src/serialize-video.js @@ -0,0 +1,23 @@ +// Captures the current frame of videos and sets the poster image +export function serializeVideos(dom, clone) { + for (let video of dom.querySelectorAll('video')) { + // If the video already has a poster image, no work for us to do + if (video.getAttribute('poster')) continue; + + let videoId = video.getAttribute('data-percy-element-id'); + let cloneEl = clone.querySelector(`[data-percy-element-id="${videoId}"]`); + let canvas = document.createElement('canvas'); + let width = canvas.width = video.videoWidth; + let height = canvas.height = video.videoHeight; + + canvas.getContext('2d').drawImage(video, 0, 0, width, height); + + let dataUrl = canvas.toDataURL(); + // If the canvas produces a blank image, skip + if (!dataUrl || dataUrl === 'data:,') continue; + + cloneEl.setAttribute('poster', dataUrl); + } +} + +export default serializeVideos; diff --git a/packages/dom/test/assets/example.webm b/packages/dom/test/assets/example.webm new file mode 100644 index 000000000..ec801e8b9 Binary files /dev/null and b/packages/dom/test/assets/example.webm differ diff --git a/packages/dom/test/serialize-videos.test.js b/packages/dom/test/serialize-videos.test.js new file mode 100644 index 000000000..31cecbea6 --- /dev/null +++ b/packages/dom/test/serialize-videos.test.js @@ -0,0 +1,40 @@ +import { withExample, parseDOM } from './helpers'; +import serializeDOM from '@percy/dom'; + +let canPlay = $video => new Promise(resolve => { + if ($video.readyState > 2) resolve(); + else $video.addEventListener('canplay', resolve); +}); + +describe('serializeVideos', () => { + let $; + + it('serializes video elements', async () => { + withExample(` +