Skip to content

Commit

Permalink
Merge pull request #794 from gemini-testing/HERMIONE-1129
Browse files Browse the repository at this point in the history
feat: add ability to disable animations in assertView
  • Loading branch information
KuznetsovRoman authored Oct 19, 2023
2 parents 3fc5bc9 + fc4dbe7 commit bb4a8d6
Show file tree
Hide file tree
Showing 10 changed files with 267 additions and 15 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,7 @@ Parameters:
- compositeImage (optional) `Boolean` - overrides config [browsers](#browsers).[compositeImage](#compositeImage) value
- screenshotDelay (optional) `Number` - overrides config [browsers](#browsers).[screenshotDelay](#screenshotDelay) value
- selectorToScroll (optional) `String` - DOM-node selector which should be scroll when the captured element does not completely fit on the screen. Useful when you capture the modal (popup). In this case a duplicate of the modal appears on the screenshot. That happens because we scroll the page using `window` selector, which scroll only the background of the modal, and the modal itself remains in place. Works only when `compositeImage` is `true`.
- disableAnimation (optional): `Boolean` - ability to disable animations and transitions while capturing a screenshot.

All options inside `assertView` command override the same options in the [browsers](#browsers).[assertViewOpts](#assertViewOpts).

Expand Down Expand Up @@ -1031,7 +1032,8 @@ Default options used when calling [assertView](https://github.com/gemini-testing
```javascript
ignoreElements: [],
captureElementFromTop: true,
allowViewportOverflow: false
allowViewportOverflow: false,
disableAnimation: true
```

#### screenshotsDir
Expand Down
59 changes: 59 additions & 0 deletions src/browser/client-scripts/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,27 @@ exports.prepareScreenshot = function prepareScreenshot(areas, opts) {
}
};

exports.disableFrameAnimations = function disableFrameAnimations() {
try {
return disableFrameAnimationsUnsafe();
} catch (e) {
return {
error: "JS",
message: e.stack || e.message
};
}
};

exports.cleanupFrameAnimations = function cleanupFrameAnimations() {
if (window.__cleanupAnimation) {
window.__cleanupAnimation();
}
};

function prepareScreenshotUnsafe(areas, opts) {
var allowViewportOverflow = opts.allowViewportOverflow;
var captureElementFromTop = opts.captureElementFromTop;
var disableAnimation = opts.disableAnimation;
var scrollElem = window;

if (opts.selectorToScroll) {
Expand Down Expand Up @@ -102,6 +120,10 @@ function prepareScreenshotUnsafe(areas, opts) {
};
}

if (disableAnimation) {
disableFrameAnimationsUnsafe();
}

return {
captureArea: rect.scale(pixelRatio).serialize(),
ignoreAreas: findIgnoreAreas(opts.ignoreSelectors, {
Expand All @@ -125,6 +147,43 @@ function prepareScreenshotUnsafe(areas, opts) {
};
}

function disableFrameAnimationsUnsafe() {
var everyElementSelector = "*:not(#hermione-q.hermione-w.hermione-e.hermione-r.hermione-t.hermione-y)";
var everythingSelector = ["", "::before", "::after"]
.map(function (pseudo) {
return everyElementSelector + pseudo;
})
.join(", ");

var styleElements = [];

util.forEachRoot(function (root) {
var styleElement = document.createElement("style");
styleElement.innerHTML =
everythingSelector +
[
"{",
" animation-delay: 0ms !important;",
" animation-duration: 0ms !important;",
" animation-timing-function: step-start !important;",
" transition-timing-function: step-start !important;",
" scroll-behavior: auto !important;",
"}"
].join("\n");

root.appendChild(styleElement);
styleElements.push(styleElement);
});

window.__cleanupAnimation = function () {
for (var i = 0; i < styleElements.length; i++) {
styleElements[i].remove();
}

delete window.__cleanupAnimation;
};
}

exports.resetZoom = function () {
var meta = lib.queryFirst('meta[name="viewport"]');
if (!meta) {
Expand Down
16 changes: 16 additions & 0 deletions src/browser/client-scripts/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,19 @@ exports.isSafariMobile = function () {
exports.isInteger = function (num) {
return num % 1 === 0;
};

exports.forEachRoot = function (cb) {
function traverseRoots(root) {
cb(root);

var treeWalker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);

for (var node = treeWalker.currentNode; node !== null; node = treeWalker.nextNode()) {
if (node instanceof Element && node.shadowRoot) {
traverseRoots(node.shadowRoot);
}
}
}

traverseRoots(document.documentElement);
};
16 changes: 14 additions & 2 deletions src/browser/commands/assert-view/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,15 @@ const InvalidPngError = require("./errors/invalid-png-error");
module.exports = browser => {
const screenShooter = ScreenShooter.create(browser);
const { publicAPI: session, config } = browser;
const { assertViewOpts, compareOpts, compositeImage, screenshotDelay, tolerance, antialiasingTolerance } = config;
const {
assertViewOpts,
compareOpts,
compositeImage,
screenshotDelay,
tolerance,
antialiasingTolerance,
disableAnimation,
} = config;

const { handleNoRefImage, handleImageDiff } = getCaptureProcessors();

Expand All @@ -27,6 +35,7 @@ module.exports = browser => {
screenshotDelay,
tolerance,
antialiasingTolerance,
disableAnimation,
});

const { hermioneCtx } = session.executionContext;
Expand All @@ -44,6 +53,7 @@ module.exports = browser => {
allowViewportOverflow: opts.allowViewportOverflow,
captureElementFromTop: opts.captureElementFromTop,
selectorToScroll: opts.selectorToScroll,
disableAnimation: opts.disableAnimation,
});

const { tempOpts } = RuntimeConfig.getInstance();
Expand All @@ -55,7 +65,9 @@ module.exports = browser => {
"screenshotDelay",
"selectorToScroll",
]);
const currImgInst = await screenShooter.capture(page, screenshoterOpts);
const currImgInst = await screenShooter
.capture(page, screenshoterOpts)
.finally(() => browser.cleanupScreenshot(opts));
const currSize = await currImgInst.getSize();
const currImg = { path: temp.path(Object.assign(tempOpts, { suffix: ".png" })), size: currSize };

Expand Down
10 changes: 10 additions & 0 deletions src/browser/commands/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ export interface AssertViewOpts extends Partial<AssertViewOptsConfig> {
* @defaultValue `undefined`
*/
selectorToScroll?: string;
/**
* Ability to disable animations and transitions while making a screenshot
*
* @remarks
* Usefull when you capture screenshot of a page, having animations and transitions.
* Iframe animations are only disabled when using webdriver protocol.
*
* @defaultValue `true`
*/
disableAnimation?: boolean;
}

export type AssertViewCommand = (state: string, selectors: string | string[], opts?: AssertViewOpts) => Promise<void>;
Expand Down
57 changes: 57 additions & 0 deletions src/browser/existing-browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,21 @@ module.exports = class ExistingBrowser extends Browser {
`Prepare screenshot failed with error type '${result.error}' and error message: ${result.message}`,
);
}

// https://github.com/webdriverio/webdriverio/issues/11396
if (this._config.automationProtocol === "webdriver" && opts.disableAnimation) {
await this._disableIframeAnimations();
}

return result;
}

async cleanupScreenshot(opts = {}) {
if (opts.disableAnimation) {
await this._cleanupPageAnimations();
}
}

open(url) {
return this._session.url(url);
}
Expand Down Expand Up @@ -285,6 +297,51 @@ module.exports = class ExistingBrowser extends Browser {
.then(clientBridge => (this._clientBridge = clientBridge));
}

async _runInEachIframe(cb) {
const iframes = await this._session.findElements("css selector", "iframe");

try {
for (const iframe of iframes) {
await this._session.switchToFrame(iframe);
await cb();
}
} finally {
await this._session.switchToParentFrame();
}
}

async _disableFrameAnimations() {
const result = await this._clientBridge.call("disableFrameAnimations");

if (result && result.error) {
throw new Error(
`Disable animations failed with error type '${result.error}' and error message: ${result.message}`,
);
}

return result;
}

async _disableIframeAnimations() {
await this._runInEachIframe(() => this._disableFrameAnimations());
}

async _cleanupFrameAnimations() {
return this._clientBridge.call("cleanupFrameAnimations");
}

async _cleanupIframeAnimations() {
await this._runInEachIframe(() => this._cleanupFrameAnimations());
}

async _cleanupPageAnimations() {
await this._cleanupFrameAnimations();

if (this._config.automationProtocol === "webdriver") {
await this._cleanupIframeAnimations();
}
}

_stubCommands() {
for (let commandName of this._session.commandList) {
if (commandName === "deleteSession") {
Expand Down
2 changes: 2 additions & 0 deletions src/config/browser-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ function buildBrowserOptions(defaultFactory, extra) {
validate: value => utils.assertNonNegativeNumber(value, "antialiasingTolerance"),
}),

disableAnimation: options.boolean("disableAnimation"),

compareOpts: options.optionalObject("compareOpts"),

buildDiffOpts: options.optionalObject("buildDiffOpts"),
Expand Down
1 change: 1 addition & 0 deletions src/config/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ module.exports = {
diffColor: "#ff00ff",
tolerance: 2.3,
antialiasingTolerance: 4,
disableAnimation: true,
compareOpts: {
shouldCluster: false,
clustersSize: 10,
Expand Down
103 changes: 95 additions & 8 deletions test/src/browser/existing-browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ describe("ExistingBrowser", () => {
return browser.init(sessionData, calibrator);
};

const stubClientBridge_ = () => {
const bridge = { call: sandbox.stub().resolves({}) };

clientBridge.build.resolves(bridge);

return bridge;
};

beforeEach(() => {
session = mkSessionStub_();
sandbox.stub(webdriverio, "attach").resolves(session);
Expand Down Expand Up @@ -648,14 +656,6 @@ describe("ExistingBrowser", () => {
});

describe("prepareScreenshot", () => {
const stubClientBridge_ = () => {
const bridge = { call: sandbox.stub().resolves({}) };

clientBridge.build.resolves(bridge);

return bridge;
};

it("should prepare screenshot", async () => {
const clientBridge = stubClientBridge_();
clientBridge.call.withArgs("prepareScreenshot").resolves({ foo: "bar" });
Expand Down Expand Up @@ -721,6 +721,93 @@ describe("ExistingBrowser", () => {
"Prepare screenshot failed with error type 'JS' and error message: stub error",
);
});

it("should disable animations if 'disableAnimation: true' and 'automationProtocol: webdriver'", async () => {
const clientBridge = stubClientBridge_();
const browser = await initBrowser_(mkBrowser_({ automationProtocol: "webdriver" }));
const [wdElement] = await browser.publicAPI.findElements("css selector", ".some-selector");

await browser.prepareScreenshot(".selector", { disableAnimation: true });

assert.calledWith(clientBridge.call, "prepareScreenshot", [
".selector",
sinon.match({ disableAnimation: true }),
]);
assert.calledOnceWith(browser.publicAPI.switchToFrame, wdElement);
assert.calledWith(clientBridge.call, "disableFrameAnimations");
});

it("should not disable iframe animations if 'disableAnimation: true' and 'automationProtocol: devtools'", async () => {
const clientBridge = stubClientBridge_();
const browser = await initBrowser_(mkBrowser_({ automationProtocol: "devtools" }));

await browser.prepareScreenshot(".selector", { disableAnimation: true });

assert.calledWith(clientBridge.call, "prepareScreenshot", [
".selector",
sinon.match({ disableAnimation: true }),
]);
assert.notCalled(browser.publicAPI.switchToFrame);
assert.neverCalledWith(clientBridge.call, "disableFrameAnimations");
});

it("should not disable animations if 'disableAnimation: false'", async () => {
const clientBridge = stubClientBridge_();
const browser = await initBrowser_(mkBrowser_({ automationProtocol: "webdriver" }));
const [wdElement] = await browser.publicAPI.findElements("css selector", ".some-selector");

await browser.prepareScreenshot(".selector", { disableAnimation: false });

assert.neverCalledWith(clientBridge.call, "prepareScreenshot", [
".selector",
sinon.match({ disableAnimation: true }),
]);
assert.neverCalledWith(browser.publicAPI.switchToFrame, wdElement);
assert.neverCalledWith(clientBridge.call, "disableFrameAnimations");
});
});

describe("cleanupScreenshot", () => {
it("should cleanup parent frame if 'disableAnimation: true'", async () => {
const clientBridge = stubClientBridge_();
const browser = await initBrowser_(mkBrowser_({ automationProtocol: "webdriver" }));

await browser.cleanupScreenshot({ disableAnimation: true });

assert.calledWith(clientBridge.call, "cleanupFrameAnimations");
});

it("should not cleanup frames if 'disableAnimation: false'", async () => {
const clientBridge = stubClientBridge_();
const browser = await initBrowser_(mkBrowser_({ automationProtocol: "webdriver" }));

await browser.cleanupScreenshot({ disableAnimation: false });

assert.neverCalledWith(clientBridge.call, "cleanupFrameAnimations");
});

it("should cleanup animations in iframe if 'automationProtocol: webdriver'", async () => {
const clientBridge = stubClientBridge_();
const browser = await initBrowser_(mkBrowser_({ automationProtocol: "webdriver" }));
const [wdElement] = await browser.publicAPI.findElements("css selector", ".some-selector");

await browser.cleanupScreenshot({ disableAnimation: true });

assert.calledOnceWith(browser.publicAPI.switchToFrame, wdElement);
assert.calledWith(clientBridge.call, "cleanupFrameAnimations");
assert.callOrder(browser.publicAPI.switchToFrame, clientBridge.call);
});

it("should not cleanup animations in iframe if 'automationProtocol: devtools'", async () => {
const clientBridge = stubClientBridge_();
const browser = await initBrowser_(mkBrowser_({ automationProtocol: "devtools" }));
const [wdElement] = await browser.publicAPI.findElements("css selector", ".some-selector");

await browser.cleanupScreenshot({ disableAnimation: true });

assert.notCalled(browser.publicAPI.switchToFrame);
assert.neverCalledWith(clientBridge.call, "cleanupFrameAnimations", wdElement);
});
});

describe("open", () => {
Expand Down
Loading

0 comments on commit bb4a8d6

Please sign in to comment.