Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add ability to disable animations in assertView #800

Merged
merged 1 commit into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,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 @@ -1020,7 +1021,8 @@ Default options used when calling [assertView](https://github.com/gemini-testing
```javascript
ignoreElements: [],
captureElementFromTop: true,
allowViewportOverflow: false
allowViewportOverflow: false,
disableAnimation: false
```

#### 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
57 changes: 57 additions & 0 deletions src/browser/existing-browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,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 @@ -281,6 +293,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 @@ -226,6 +226,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: false,
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
Loading