Reputation: 473823
The Story:
We've developed a custom jasmine matcher that does 2 main things:
Implementation:
toHaveTooltip: function() {
return {
compare: function(elm, expectedTooltip) {
var tooltipPage = requirePO("tooltip");
browser.actions().mouseMove(elm).perform();
browser.wait(EC.visibilityOf(tooltipPage.tooltip), 5000, "Tooltip is still not visible.");
return {
pass: tooltipPage.tooltip.getText().then(function(actualTooltip) {
return jasmine.matchersUtil.equals(actualTooltip, expectedTooltip);
}),
message: "Element does not have the tooltip '" + expectedTooltip + "'."
};
}
};
},
where tooltipPage
is a Page Object defined separately:
var Tooltip = function () {
this.tooltip = element(by.css(".tooltip"));
};
module.exports = new Tooltip();
The usage is quite convenient for us and really helps to follow the DRY principle keeping our test code base clean and readable:
expect(page.fromDateInput).toHaveTooltip("After");
The Problem and the Question:
Now, what I'm trying to do is to have the matcher handle 2 use cases separately:
browser.wait()
rejected promise)How can I improve the matcher to be able to handle these two problems separately and report different errors?
What I've tried:
toHaveTooltip: function() {
return {
compare: function(elm, expectedTooltip) {
var tooltipPage = requirePO("tooltip");
browser.actions().mouseMove(elm).perform();
return browser.wait(EC.visibilityOf(tooltipPage.tooltip), 5000, "Tooltip is still not visible.").then(function () {
return {
pass: tooltipPage.tooltip.getText().then(function(actualTooltip) {
return jasmine.matchersUtil.equals(actualTooltip, expectedTooltip);
}),
message: "Element does not have the tooltip '" + expectedTooltip + "'."
};
}, function () {
return {
pass: false,
message: "No tooltip shown on mouse over the element"
}
});
}
};
},
Here I've tried to resolve browser.wait()
explicitly and handle the "success" and "error" cases separately. This resulted into a Jasmine Spec timeout and a huge "red" text on the console:
Expected ({ ptor_: ({ setFileDetector: Function, ...
5 minutes scrolling here
... InnerHtml: Function, getId: Function, getRawId: Function }) to have tooltip 'After'.
I'm afraid I cannot return a promise from the "compare" function.
Upvotes: 12
Views: 1513
Reputation: 3181
For some reason, I couldn't get this to work in Karma/Angular 2+. I ended up calling Jasmine's global fail
method within the promise itself:
const _global: any = (typeof window === 'undefined' ? global : window);
const customMatchers: jasmine.CustomMatcherFactories = {
toPassA11y: () => {
return {
compare: (el: any): any => {
const axe = require('axe-core');
const result: any = {
message: '',
pass: true
};
axe.run((error: Error, results: any) => {
if (error) throw error;
if (results.violations.length > 0) {
_global.fail('Expected element to pass accessibility checks.');
}
});
return result;
}
};
}
};
_global.beforeEach(() => {
jasmine.addMatchers(customMatchers);
});
And in the spec:
describe('Home component', () => {
it('should check accessibility', async(() => {
expect(document).toPassA11y();
}));
});
Upvotes: 1
Reputation: 473823
Based on the @Girish Sortur's perfect answer, here is the complete code of the matcher that is now working perfectly handling both missing tooltip and a different tooltip text cases separately:
toHaveTooltip: function() {
return {
compare: function(elm, expectedTooltip) {
var tooltipPage = requirePO("tooltip"),
result = {};
// mouse over the element
browser.actions().mouseMove(elm).perform();
// wait for tooltip to appear and handle errors
result.pass = browser.wait(EC.visibilityOf(tooltipPage.tooltip), 5000).then(function () {
return tooltipPage.tooltip.getText().then(function(actualTooltip) {
result.message = "Expected tooltip: '" + expectedTooltip + "'. Actual tooltip: '" + actualTooltip + "'.";
return jasmine.matchersUtil.equals(actualTooltip, expectedTooltip);
})
}, function () {
result.message = "No tooltip shown on mouse over the element";
return false;
});
return result;
}
};
},
Upvotes: 0
Reputation: 6962
As per jasminewd2 (An adapter for Jasmine-to-WebDriverJS. Used by Protractor) code -
An expectation resolves any promises given for actual and expected values, as well as the
pass
property of theresult
object.
So if at all there is an async function or a promise that needs to be resolved in a custom matcher/expectation then it needs to be wrapped to the result.pass
value, so that protractor waits for the promise to be resolved.
In the question, a jasmine spec timeout
error is encountered because protractor couldn't understand that there is a promise that needs to be resolved before performing that particular operation. In order to resolve it, either pass the async function in the expect statement directly or pass it to the pass
value of the result
object. Here's the code for it -
toHaveTooltip: function() {
return {
compare: function(elm, expectedTooltip) {
var tooltipPage = requirePO("tooltip");
browser.actions().mouseMove(elm).perform();
return {
pass: browser.wait(EC.visibilityOf(tooltipPage.tooltip), 5000, "Tooltip is still not visible.").then(function () {
tooltipPage.tooltip.getText().then(function(actualTooltip) {
return jasmine.matchersUtil.equals(actualTooltip, expectedTooltip);
}),
}, function () {
return false;
}),
message: "Error Occured"
}
}
};
},
However, the problem with the above code is that a custom error message cannot be crafted. To resolve it, the best method I could find was to return the result
object explicitly, so that an error message can be assigned to it as required. Here's an example -
var result = {};
result.pass = browser.wait(EC.visibilityOf(tooltipPage.tooltip), 5000, "Tooltip is still not visible.").then(function () {
tooltipPage.tooltip.getText().then(function(actualTooltip) {
result.message = "Element does not have the tooltip '" + expectedTooltip + "'.";
return jasmine.matchersUtil.equals(actualTooltip, expectedTooltip);
}),
}, function () {
result.message = "No tooltip shown on mouse over the element";
return false;
});
return result;
Note: If there is no message
property in the result
object, then protractor will try to create a generic error message itself and it will contain the promise object (A lengthy message starting with - { ptor_: ... }
) as shown in the question.
Hope it helps.
Upvotes: 4
Reputation: 817
Well, i remember reading somewhere that jasmine 2 does not support the type of matcher you are trying to do (with async function inside), and returning promises.. i will try to find the source and update here. Also you shouldn't do the mouse actions inside of the matcher, that's not the point of matchers.
So basically what im saying and suggesting is the following: If you want a clean code, export the following into a function and call it.
var checkToolTipVisibility (elm, expectedTooltip) {
browser.actions().mouseMove(elm).perform();
browser.wait(EC.visibilityOf(tooltipPage.tooltip), 5000, "Tooltip is still not visible.");//optional then here if you want to fail with a timeout or something...
expect(tooltipPage.tooltip.getText()).toEqual(expectedTooltip);
}
checkToolTipVisibility(page.fromDateInput, "After");//usage
I think it's a very clean and simple solution, that doesn't require any custom matchers, and it is the jasmine way of doing things (not async functions in matchers), that's the way i use in my code, except those functions sit in a utils.js file which i require when needed .
Hope i helped, and i will continue looking for the source of my first statement!
Upvotes: 2