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(`
+
+ `);
+
+ await canPlay(window.video);
+ $ = parseDOM(serializeDOM());
+ expect($('#video')[0].getAttribute('poster').length > 25).toBe(true);
+ });
+
+ it('does not serialize videos with an existing poster', async () => {
+ withExample(`
+
+ `);
+
+ await canPlay(window.video);
+ $ = parseDOM(serializeDOM());
+ expect($('#video')[0].getAttribute('poster')).toBe('//:0');
+ });
+
+ it('does not apply blank poster images', () => {
+ withExample(`
+
+ `);
+
+ $ = parseDOM(serializeDOM());
+ expect($('#video')[0].getAttribute('poster')).toBe(null);
+ });
+});