protractor proper DRY for spec - javascript

We have an app with a grid of rows, having Per-page SELECT and Pagination, and would like to do e2e test which does navigation and PP selection then checks the results displayed by comparing it with result from DB (pseudo-code):
it('should check navigation and pp', function() {
for(i=0;i<SELECT.options;i++) {
element(by.repeater('SELECTOR HERE(i)')).click();
browser.wait(function which checks URL contains a segment(i));
browser.wait(function which checks if a 'loading div is displayed');
for(j=0;j<PagesForPP(i);j++) {
runExpects('for pp=i'); //contains a couple expect(someElement.text).toContain(asynResult());
element(by.css('SELECTOR(j)').click();
browser.wait(function which checks URL contains a segment(j));
browser.wait(function which checks 'loading div is displayed');
}
}
});
Where function(i) is a call dependent on current Perpage and function(j)
My question is: how can we nest the two loops in a way that protractor understands (possibly executes synchronously) using protractor.promise.controlFlow() or a better way if available.
Currently, protractor ignore functions like browser.wait(function which checks URL contains a segment(i));
Which looks like so:
waitForPageChange: function (urlSegment) {
console.log('>> Waiting for URL to contain: ', urlSegment);
var currentUrl;
return browser.getCurrentUrl().then(function (url) {
currentUrl = url;
}).then(function () {
browser.wait(function () {
return browser.getCurrentUrl().then(function (url) {
if (urlSegment) {
return url.indexOf(urlSegment) >= 0;
}
return url !== currentUrl;
});
});
});
}
And the e2e test completes before the expects are valid (ex: we're in page 1 and we're already reaching the 2 and 3 page checks => all tests fail because they're checking incorrect values.

Your waitForPageChange approach should work, I think. I believe you're missing a "return" (from the browser.wait), so the final then in the function (which is the return value of the whole function) isn't the right promise. Try this:
waitForPageChange: function (urlSegment) {
console.log('>> Waiting for URL to contain: ', urlSegment);
var originalUrl;
return browser.getCurrentUrl().then(function (url) {
originalUrl = url; // XXX this is racy, the page may have changed already
}).then(function () {
return browser.wait(function () { // this line was missing a 'return'
return browser.getCurrentUrl().then(function (url) {
if (urlSegment) {
return url.indexOf(urlSegment) >= 0;
}
return url !== originalUrl;
});
});
});
}
Note that code that does return muble.then(...).then(function() {return x; }) ends up return the promise from the last then in the chain.
Also, beware of the default case where urlSegment is not provided. You can't be sure the "originalUrl" gets initialized quickly enough. The browser could've moved on before you load that. To be reliable you probably want a separate "waitForPageToLeave(x)" that waits for the URL to change away from the provided one, and you should expect the caller to lookup that "original" URL before they make any changes. (And that would be separate from a "waitForPageToGoTo(x)" function that wait for the URL to become the given one.)

Related

JavaScript promise with .then inside .then doesn't work

Current state:
I have the following function in a link builder which is using a configuration service which returns a promise with some enviroment parameters:
_createLink: function (label, args) {
return LinkBuilder.builder()
.withLabel(label)
.withUrl(ConfigService.getConfig()
.then((env) => env.baseUrl
+ TranslationService.instant('MY.URL', args)))
.buildLink();
},
A few explanations:
withUrl(...) does the following:
withUrl: function (param) {
if (typeof param === 'string') {
builder.value.linkUrl = param;
} else if (typeof param === 'function') {
builder.value.linkCalculationFunction = param;
} else if (typeof param === 'object' && typeof param.lazyBuildUrl === 'function') {
builder.value.linkCalculationFunction = param.lazyBuildUrl();
} else if (typeof param === 'object' && typeof param.then === 'function') {
builder.value.linkCalculationFunction = () => param;
} else {
throw new Error('invalid url param ' + param);
}
return builder;
},
withLabel(label) will set a label which will be displayed as the URL text
withLabel: function (label) {builder.value.labelKey = label; return builder;}
.buildLink() is just returning builder.value, with the URL and all other params:
buildLink: function () {return builder.value;}
TranslationService will find the 'MY.URL' entry in a JSON file with translations. The value will be something like 'http://www.myserver.com#name={{name}}&age={{age}}'. The name and age parameters from args will be inserted there.
Problem:
I need to encode those args using an external service which also returns a promise and then to append the return value to my link. That external service will return something like 'data': {'encodedId': '123-456-789'}, and my final link should be: http://www.myserver.com#encodedId=123-456-789. Here is what I did:
My original attempt:
In the JSON file with translations I removed the parameters from the entry, so now it's only:
'http://www.myserver.com#'
The _createLink function now looks like this:
_createLink: function (label, args) {
let content = {
'name': args.name,
'age': args.age
};
return EncodingService.saveContent(content)
.then((data) => {
return LinkBuilder.builder()
.withLabel(label)
.withUrl(ConfigService.getConfig()
.then((env) => env.baseUrl
+ TranslationService.instant('MY.URL')
+ 'encodedId=' + data.encodedId))
.buildLink();
},
When I place a breakpoint at the last then line, I see that I have the correct data.encodedId, the correct env.baseUrl and the TranslationService is also correctly returning the JSON entry. But for some reason, I don't see the link in my page anymore. I guess that I'm doing something wrong with chaining the .thens, but I'm not sure what.
Another attempt which still doesn't work:
Here is the third version, which still doesn't work:
_createLink: function (label, args) {
let content = {
'name': args.name,
'age': args.age
};
return $q.all([
EncodingService.saveContent(content),
ConfigService.getConfig()
])
.then((data) => {
let url = data[1].baseUrl
+ TranslationService.instant('MY.URL')
+ 'encodedId=' + data[0].encodedId;
return LinkBuilder.builder()
.withLabel(label)
.withUrl(url)
.buildLink();
},
Again, I placed the breakpoint at the last return statement and there I have the url formed correctly...
Usage of `_createLink`:
This function is used like this:
getLeadLink: function (label, access, address) {
return myService._createLink(label, someService._createArguments(access, address));
},
...and getLeadLink is used in a component like this:
this.$onInit = () => {
...
this.leadLink = OtherService.getLeadLink('LINEINFO.LEAD.LINK', someData.access, someData.address);
...
};
...and leadLink is then displayed in the HTML file.
Note: None of these things was modified, so the usage of _createLink and the displaying of the link is still the same as it was before.
The third attempt is going in the right direction.
There is however no way you can get the future URL now. And so any attempt that in the end expects a simple function call to return the URL cannot be right. Such a function (like _createLink) is deemed to return before the asynchronous parts (i.e. any then callbacks) are executed. In your third attempt you improved things so that at least _createLink will return a promise.
Now remains to await the resolution of that promise.
So, any wrapping function execution around _createLink, should take into account the asynchronous pattern:
getLeadLink: function (label, access, address) {
return myService._createLink(label, someService._createArguments(access, address));
},
This is fine, but realise that _createLink does not return the URL, but a promise. And so also getLeadLink will return a promise.
You write that you want to display the URL, so you would do something like this (I don't know how you will display, so this is simplified):
OtherService.getLeadLink('LINEINFO.LEAD.LINK', someData.access, someData.address).then(function(builder) {
var $link = $("<a>").attr("href", builder.value.linkUrl)
.text(builder.value.labelKey);
$(document).append($link);
});
The key message is: don't try to get the URL as the return value of a function. It does not matter how many wrappers you write around the core logic; it will remain an asynchronous task, so you need to get and display the URL asynchronously as well. You'll need to do it in a then callback, or use await syntax, which makes anything following it asynchronous code.

Get Header's OOXML

I am unable to get the OOXML of a Header. According to the documentation getHeader" method will return Body type. The Body has a method to get OOXML. But it looks like it is not returning the OOXML. Maybe I am missing something?
Here's my code:
Word.run(function (context) {
// Create a proxy sectionsCollection object.
var mySections = context.document.sections;
// Queue a commmand to load the sections.
context.load(mySections, 'body/style');
// Synchronize the document state by executing the queued commands,
// and return a promise to indicate task completion.
return context.sync().then(function () {
// header
var headerBody = mySections.items[0].getHeader("primary");
// header OOXML
//// NOT GETTING OOXML HERE
var headerOOXML = headerBody.getOoxml();
// Synchronize the document state by executing the queued commands,
// and return a promise to indicate task completion.
return context.sync().then(function () {
// modify header
var headerOOXMLValue = ModifyHeaderMethod(headerOOXML.value);
headerBody.clear();
headerBody.insertOoxml(headerOOXMLValue, 'Start');
// Synchronize the document state by executing the queued commands,
// and return a promise to indicate task completion.
return context.sync().then(function () {
callBackFunc({
isError: false
});
});
});
});
})
The "art" of Office.js is to minimize the number of "syncs" you do. I know that is kind of an unnecessary burden, but that's how it is.
With that in mind, In this case you only need ONE sync.
this code works (assuming that you have only one section in the doc).
btw you can try it in script lab with this yaml.
if this does not work, please indicate if this is Word for Windows (and what build) or Online, or Mac... thanks!
async function run() {
await Word.run(async (context) => {
let myOOXML = context.document.sections.getFirst()
.getHeader("primary").getOoxml();
await context.sync();
console.log(myOOXML.value);
});
}
You have a lot of extra code here but the gist of your problem is that headerOOXML won't be populated until you sync():
Word.run(function (context) {
var header = context.document.sections // Grabv
.getFirst() // Get the first section
.getHeader("primary"); // Get the header
var ooxml = header.getOoxml();
return context.sync().then(function () {
console.log(ooxml.value);
});
});

Authorization interceptor with Infinity-scroll in AngularJS

I'm using angular-http-auth for intercepting 401 response in order to display login dialogue and when the user is authorized, to retry failed request.
Since I'm using infinity-scroll I'm increasing an offset value, with every additional upload:
var upload = function () {
dataResource.query($scope.model).then(function (result) {
angular.forEach(result.items, function (value) {
$scope.items.push(value);
});
});
}
$scope.uploadMore = function () {
$scope.model.Offset = $scope.model.Offset + 10;
upload();
};
upload();
When my page loads up it immediately sends 2 request to server upload(), invoked from this directive, and uploadMore() by infinity-scroll.
However, after user has logged in, the page does not display the first 10 entries, instead it displays 11-20 items 2 times in a row.
When I tried to debug it, I noticed that when angular-http-auth retries requests it uses increased by 10 Offset value for both queries($scope.module argument).
Functions upload() and uploadMore() are running for 2 times before angular-http-auth, so I guess that is why interceptor uses updated argument for both queries.
Could somebody please help me with this problem?
So you can resolve this problem prevent execute request until previous will finish.
The faster way to do that is :
var pending = false;
var upload = function () {
if(!pending) {
pending = true;
dataResource.query($scope.model).then(function (result) {
pending = false;
angular.forEach(result.items, function (value) {
$scope.items.push(value);
});
});
}
}

how can I get the current url using protractor?

I am testing a website using protractor, and jasmine. I would like to know the current url in order to verify a test.
I have tried
function waitForUrlToChangeTo(urlRegex) {
var currentUrl;
return browser.getCurrentUrl().then(function storeCurrentUrl(url) {
currentUrl = url;
}
).then(function waitForUrlToChangeTo() {
return browser.wait(function waitForUrlToChangeTo() {
return browser.getCurrentUrl().then(function compareCurrentUrl(url) {
return urlRegex.test(url);
});
});
}
);
}
and I am using this function in this way
it('should log', function() {
//element(by.model('user.username')).sendKeys('asd');
//element(by.model('user.password')).sendKeys('asd');
element(by.linkText('Acceder')).click();
waitForUrlToChangeTo("http://localhost:9000/#/solicitudes");
});
If you want to just check the current URL, then use browser.getCurrentUrl():
expect(browser.getCurrentUrl()).toEqual("expectedUrl");
But, if you need to wait until URL matches a certain value, see the next part of the answer.
Here is a working code based on the example provided by the author of the Expected Conditions:
var urlChanged = function(url) {
return function () {
return browser.getCurrentUrl().then(function(actualUrl) {
return url != actualUrl;
});
};
};
Usage:
element(by.linkText('Acceder')).click();
browser.wait(urlChanged("http://localhost:9000/#/solicitudes"), 5000);
Alecxe your answer was very helpful.
But couldn't you just code:
expect(browser.getCurrentUrl()).toEqual('whateverbrowseryouarexpectingtobein');
And if this fails, then you know you're going to the wrong place.
Since Protractor 4.0.0 you can now use expected conditions to know when your url changed.
const EC = protractor.ExpectedConditions;
browser.wait(EC.urlContains('my-url'), 5000);
The asnwer I found comes from this one, I have no real credits for it. But just in case it help.
Protractor- Generic wait for URL to change

Casper JS waitForResource with a restful API

We are having a little problem with a functional test with casper.js.
We request the same resource twice, first with the GET and then with POST method.
Now when waiting for the second resource (POST) it matches the first resource and directly goes to the "then" function.
We would like to be able to check for the HTTP method in the "test" function, that way we can identify the resource properly. For now we use the status code (res.status), but that doesn't solve our problem fully, we really need the http method.
// create new email
this.click(xPath('//div[#id="tab-content"]//a[#class="button create"]'));
// GET
this.waitForResource('/some/resource',
function then() {
this.test.assertExists(xPath('//form[#id="email_edit_form"]'), 'Email edit form is there');
this.fill('form#email_edit_form', {
'email_entity[email]': 'test.bruce#im.com',
'email_entity[isMain]': 1
}, true);
// POST
this.waitForResource(
function test(res) {
return res.url.search('/some/resource') !== -1 && res.status === 201;
},
function then() {
this.test.assert(true, 'Email creation worked.');
},
function timeout() {
this.test.fail('Email creation did not work.');
}
);
},
function timeout() {
this.test.fail('Email adress creation form has not been loaded');
});
Or maybe there is a better way to test this scenario? Although since this is a functional test we need to keep all those steps in one test.
You can try to alter the form action url to add some query string, therefore generating a new resource appended to the stack. Could be done this way:
casper.thenEvaluate(function() {
var form = __utils__.findOne('#email_edit_form');
form.setAttribute('action', form.getAttribute('action') + '?plop');
});
That's a hack though, and functional testing should never be achieved that way. Let's hope more information will be added to the response objects in the future.
The res parameter that is passed to the test function has an ID. I created a helper that tests against this ID and blacklists it, so the same resource won't get accepted a second time.
var blackListedResourceIds = [],
testUniqueResource = function (resourceUrl, statusCode) {
return function (res) {
// check if resource was already loaded
var resourceFound = res.url.search(resourceUrl) !== -1;
// check statuscode
if (statusCode !== undefined) {
resourceFound = resourceFound && res.status === statusCode;
}
// check blacklisting
if (!resourceFound || blackListedResourceIds[res.id] !== undefined) {
return false;
} else {
blackListedResourceIds[res.id] = true;
return true;
}
};
};

Categories