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

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.

Related

Awaiting code until filter is finished - Nextjs/Javascript

I am looking on how to make my code after my filter function await the results of my filter function to complete before running. However I am not sure how to do this.
My filter function takes in another function (useLocalCompare) which causes the execution of my filter function to be a little longer than normal, which then leads to my next piece of code (that depends on the results of my filter function) executing before my filter function is complete.....which leads to undefined.
Is there anything similar to a callback I can use to force my subsequent piece of code to wait till the filter is finished?
Relevant code is written below.
if (flatarrayofvalues !== null && genre !== null) {
const filtteredarray = await flatarrayofvalues.filter(
(placeholder) => {
if (useLocalCompare(genre, placeholder.name) == true) {
console.log("HURAY!!!!", placeholder.id, placeholder.name);
placeholder.name == placeholder.name;
}
}
);
console.log("MY FILTERED ARRAY IS", filtteredarray);
console.log("The ID FOR MY MY FILERED ARRAY IS two ID", filtteredarray[0]?.id);
return filtteredarray[0].id;
}
}
}
For those curious, useLocalCompare basically checks to see if the genre parameter pulled down from the URL is the same as a name parameter from the array I am filtering. Reason I have this is due to people having different case sensitivity when putting in URLS. EX: it will pull down "HORrOR" and match it to the object name in the array I am filtering called "horror". I then extract the ID from that object.
you have to return the conditional from filter as it is "explicit return"
const filtteredarray = await flatarrayofvalues.filter(
(placeholder) => {
if (useLocalCompare(genre, placeholder.name) == true) {
console.log("HURAY!!!!", placeholder.id, placeholder.name);
return placeholder.name == placeholder.name; // here
// why not just return true ?? instead of above line
}return false
}
);
Also I'm not sure this makes sense
placeholder.name == placeholder.name; you mean just return true; ?

Understanding JavaScript for TFS widget

I've been trying to modify the sample dashboard widget at this location
https://learn.microsoft.com/en-us/vsts/extend/develop/add-dashboard-widget?view=vsts#part-2-hello-world-with-vsts-rest-api
However, reluctantly have to admit I simply can't understand the structure required to extend it
Near the end, it uses "load: function" and returns the outputs of a REST API call, which I can consume however I want
However, I need to make more than one different REST call, and I simply cannot figure out how to get that info usable in my function
I modified the code so it starts like this:
VSS.require(["TFS/Dashboards/WidgetHelpers", "TFS/Work/RestClient","VSS/Service", "TFS/WorkItemTracking/RestClient" ],
I then created a handle for the other call I want to make like this:
var queryClient = VSS_Service.getCollectionClient(TFS_Wit_QueryAPI.WorkItemTrackingHttpClient);
var queryResults = queryClient.getQuery(projectId, "Shared Queries/My Bugs");
However, I cannot consume the contents of queryResults - I know it's working up to a point as if I put in an invalid URL it will error as it knows it can't access anything there. If the URL is correct, no matter what I've tried - even stringify just to see what comes back - I get 'undefined' or something similar (it's definitely a valid JavaScript object)
The key seems to be right at the end when you have "load: function" except that only allows one thing to be returned? The reason I know this is if I change the function that it returns to be the one I've written rather than the one from the sample, it works fine - but the problem remains the same in that I can only process the results of one API call.
You can call more than one APIs, the code in that article is just the simple sample.
For Widget extension, you just need to return the status (e.g. Success()) in load function, so you can return status at the end of the function. For example:
var getQueryInfo = function (widgetSettings) {
// Get a WIT client to make REST calls to VSTS
return TFS_Wit_WebApi.getClient().getQuery(projectId, "Shared Queries/Feedback")
.then(function (query) {
// Create a list with query details
var $list = $('<ul>');
$list.append($('<li>').text("Query ID: " + query.id));
$list.append($('<li>').text("Query Name: " + query.name));
$list.append($('<li>').text("Created By: " + (query.createdBy ? query.createdBy.displayName: "<unknown>") ));
// Append the list to the query-info-container
var $container = $('#query-info-container');
$container.empty();
$container.append($list);
// Use the widget helper and return success as Widget Status
return true;
}, function (error) {
// Use the widget helper and return failure as Widget Status
console.log(error);
return false;
});
}
var getAnOhterQueryInfo = function (widgetSettings) {
// Get a WIT client to make REST calls to VSTS
return TFS_Wit_WebApi.getClient().getQuery(projectId, "Shared Queries/Bug")
.then(function (query) {
// Create a list with query details
var $list = $('<ul>');
$list.append($('<li>').text("Query ID: " + query.id));
$list.append($('<li>').text("Query Name: " + query.name));
$list.append($('<li>').text("Created By: " + (query.createdBy ? query.createdBy.displayName: "<unknown>") ));
// Append the list to the query-info-container
var $container = $('#query-info-container');
$container.empty();
$container.append($list);
// Use the widget helper and return success as Widget Status
return true;
}, function (error) {
// Use the widget helper and return failure as Widget Status
console.log(error);
return false;
});
}
return {
load: function (widgetSettings) {
// Set your title
var $title = $('h2.title');
$title.text('Hello World');
var r1= getQueryInfo(widgetSettings);
var r2=getAnOhterQueryInfo(widgetSettings);
if(r1==true && r2==true){
return WidgetHelpers.WidgetStatusHelper.Success();
}else{
return WidgetHelpers.WidgetStatusHelper.Failure("failed, check error in console");
}
}

Repeating/Resetting an observable

I am using rxjs to create a "channel nummber" selector for a remote control on a smart tv. The idea being that as you are entering the numbers you would see them on the screen and after you have finished entering the numbers, the user would would actually be taken to that channel.
I use two observables to achieve this:
A "progress" stream that listens to all number input and emits the concatenated number string out as the numbers are inputed via the scan operator.
A "completed" stream that, after n milliseconds of no number having being entered, would emit the final numeric string as completed. EG: 1-2-3 -> "123".
Here is the code that I use to try and solve this:
channelNumber:
module.exports = function (numberKeys, source, scheduler) {
return function (completedDelay) {
var toNumericString = function (name) {
return numberKeys.indexOf(name).toString();
},
concat = function (current, numeric) {
return current.length === 3 ? current : current + numeric;
},
live = createPress(source, scheduler)(numberKeys)
.map(toNumericString)
.scan(concat, '')
.distinctUntilChanged(),
completed = live.flatMapLatest(function (value) {
return Rx.Observable.timer(completedDelay, scheduler).map(value);
}),
progress = live.takeUntil(completed).repeat();
return {
progress: progress,
completed: completed
};
};
};
createPress:
module.exports = function (source, scheduler) {
return function (keyName, throttle) {
return source
.filter(H.isKeyDownOf(keyName))
.map(H.toKeyName);
};
};
createSource:
module.exports = function (provider) {
var createStream = function (event) {
var filter = function (e) {
return provider.hasCode(e.keyCode);
},
map = function (e) {
return {
type: event,
name: provider.getName(e.keyCode),
code: e.keyCode
};
};
return Rx.Observable.fromEvent(document, event)
.filter(filter)
.map(map);
};
return Rx.Observable.merge(createStream('keyup'), createStream('keydown'));
};
Interestingly the above code, under test conditions (mocking source and scheduler using Rx.TestScheduler) works as expected. But in production, when the scheduler is not passed at all and source is the result of createPress (above), the progress stream only ever emits until complete, and then never again. It's as if the repeat is completely ignored or redundant. I have no idea why.
Am I missing something here?
You can use Window. In this case, I would suggest WindowWithTime. You can also do more interesting things like use Window(windowBoundaries) and then pass the source with Debounce as boundary.
source
.windowWithTime(1500)
.flatMap(ob => ob.reduce((acc, cur) => acc + cur, ""))
Also, since our windows are closed observables, we can use Reduce to accumulate the values from the window and concat our number.
Now, this variant will close after 1,5 second. Rather, we would want to wait x seconds after the last keypress. Naïve we could do source.window(source.debounce(1000)) but now we subscribe to our source twice, that's something we want to avoid for two reasons. First we do not know is subscribing has any side effects, second we do not know the order subscriptions will receive events. That last thing isn't a problem since we use debounce that already adds a delay after the last keypress, but still something to consider.
The solution is to publish our source. In order to keep the publish inside the sequence, we wrap it into observable.create.
Rx.Observable.create(observer => {
var ob = source.publish();
return new Rx.CompositeDisposable(
ob.window(ob.debounce(1000))
.subscribe(observer),
ob.connect());
}).flatMap(ob => ob.reduce((acc, cur) => acc + cur, ""))
Edit: Or use publish like this:
source.publish(ob => ob.window(ob.debounce(1000)))
.flatMap(ob => ob.reduce((acc, cur) => acc + cur, ""))

protractor proper DRY for spec

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.)

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