How to know when front-end finish rendering?
I mean, you have an Array of something on your backend and
a ngFor on your view.
How can I know when that's finish? I want to know because I need to scroll down when it's finish, and currently it scrolls before it renders.
Here's some code to demostrate the problem:
https://gist.github.com/mtnoronha/e78eba6b610b71f1e72424ed21722be1
Thank you
Create a new promise to handle when it has finished loading the messages so you need to scroll to bottom, in your case use it like this:
YOUR .TS FILE
appendRows(newData){
if(!newData || newData.length == 0)
return;
this.myNewPromise().then(res =>{
if(res){
this.content.scrollToBottom(0);
}
})
}
myNewPromise = (): Promise<boolean> =>{
return new Promise<boolean>(res){
if(this.messages){
let newArray = this.messages.concat(newData);
this.messages = newArray;
res(true);
}else{
this.messages = newData;
res(false);
}
}
}
So your scrollToBottomwill only fire when myNewPromise finishs. In your promise you resolve it and return a boolean, so if there's new message you call the scrollToBottom.
If you need to call it either way, just pass a true in both res or remove the if (res) on the return of myNewPromise.
Hope it helps.
Related
I'm new to Protractor.
I'm trying to select a button based on the button title. I want to make this into a function, and pass the button title in as a parameter.
This is the hard-coded version which works:
it('I click on a button based on the button title', async function() {
let button = element(by.css('button[title=example_button_title]'));
await button.click();
});
I created a global variable and a function to try and replace this, where 'buttonTitle' is the parameter I'm passing into the function:
Variable:
let dynamicButton = buttonTitle => { return element(by.css("'button[title=" + buttonTitle + "]'")) };
Function:
this.selectDynamicButton = async function(buttonTitle) {
await browser.waitForAngularEnabled(false);
await dynamicButton(buttonTitle).click();
};
When I try this I get the following error:
Failed: invalid selector: An invalid or illegal selector was specified
Apologies if there appear to be basic errors here, I am still learning. I appreciate any help that anyone can give me. Thanks.
You can add a custom locator using protractors addLocator functionality. (this is actually a very similar use case to the example listed in the link)
This would look like the following:
onPrepare: function () {
by.addLocator('buttonTitle', function (titleText, opt_parentElement) {
// This function will be serialized as a string and will execute in the
// browser. The first argument is the text for the button. The second
// argument is the parent element, if any.
const using = opt_parentElement || document;
const matchingButtons = using.querySelectorAll(`button[title="${titleText}"]`);
let result = undefined;
if (matchingButtons.length === 0) {
result = null;
} else if (matchingButtons.length === 1) {
result = matchingButtons[0];
} else {
result = matchingButtons;
}
return result;
});
}
This is called like
const firstMatchingButton = element(by.buttonTitle('example_button_title'));
const allMatchingButtons = element.all(by.buttonTitle('example_button_title'));
I had to edit this code before posting so let me know if this does not work. My work here is largely based off this previous answer
let dynamicButton = buttonTitle => { return element(by.css('button[title=${buttonTitle} ]')) };
Use template literals instead of string concatenation with +.
Protractor already has a built in locator which allows you to get a button using the text. I think you are looking at something like that. See the element(by.buttonText('text of button')) locator.
For more reference see here.
I have a Gjs app that will need to save files. I can open the file chooser dialog just fine from my menu, and I have added a "save" and "cancel" button, but I can't get the "save" button to trigger anything.
I know I'm supposed to pass it a response_id, but I'm not sure what that's supposed to look like nor what I'm supposed to do with it afterwards.
I read that part here:
https://www.roojs.com/seed/gir-1.2-gtk-3.0/gjs/Gtk.FileChooserDialog.html#expand
let actionSaveAs = new Gio.SimpleAction ({ name: 'saveAs' });
actionSaveAs.connect('activate', () => {
const saver = new Gtk.FileChooserDialog({title:'Select a destination'});
saver.set_action(Gtk.FileChooserAction.SAVE);
saver.add_button('save', 'GTK_RESPONSE_ACCEPT');
saver.add_button('cancel', 'GTK_RESPONSE_CANCEL');
const res = saver.run();
if (res) {
print(res);
const filename = saver.get_filename();
print(filename);
}
saver.destroy();
});
APP.add_action(actionSaveAs);
I can catch res and fire the associated little logging action when I close the dialog, but both the "save" and "cancel" buttons just close the dialog without doing or saying anything.
My question is, what are GTK_RESPONSE_ACCEPT and GTK_RESPONSE_CANCEL supposed to be (look like) in GJS and how do I use them?
In GJS enums like GTK_RESPONSE_* are numbers and effectively look like this:
// imagine this is the Gtk import
const Gtk = {
ResponseType: {
NONE: -1,
REJECT: -2,
ACCEPT: -3,
DELETE_EVENT: -4,
...
}
};
// access like so
let response_id = -3;
if (response_id === Gtk.ResponseType.ACCEPT) {
log(true);
}
There's a bit more information here about that.
let saver = new Gtk.FileChooserDialog({
title:'Select a destination',
// you had the enum usage correct here
action: Gtk.FileChooserAction.SAVE
});
// Really the response code doesn't matter much, since you're
// deciding what to do with it. You could pass number literals
// like 1, 2 or 3. Probably this was not working because you were
// passing a string as a response id.
saver.add_button('Cancel', Gtk.ResponseType.CANCEL);
saver.add_button('Save', Gtk.ResponseType.OK);
// run() is handy, but be aware that it will block the current (only)
// thread until it returns, so I usually prefer to connect to the
// GtkDialog::response signal and use GtkWidget.show()
saver.connect('response', (dialog, response_id) => {
if (response_id === Gtk.ResponseType.OK) {
// outputs "-5"
print(response_id);
// NOTE: we're using #dialog instead of 'saver' in the callback to
// avoid a possible cyclic reference which could prevent the dialog
// from being garbage collected.
let filename = dialog.get_filename();
// here's where you do your stuff with the filename. You might consider
// wrapping this whole thing in a re-usable Promise. Then you could call
// `resolve(filename)` or maybe `resolve(null)` if the response_id
// was not Gtk.ResponseType.OK. You could then `await` the result to get
// the same functionality as run() but allow other code to execute while
// you wait for the user.
print(filename);
// Also note, you actually have to do the writing yourself, such as
// with a GFile. GtkFileChooserDialog is really just for getting a
// file path from the user
let file = Gio.File.new_for_path(filename);
file.replace_contents_bytes_async(
// of course you actually need bytes to write, since GActions
// have no way to return a value, unless you're passing all the
// data through as a parameter, it might not be the best option
new GLib.Bytes('file contents to write to disk'),
null,
false,
Gio.FileCreateFlags.REPLACE_DESTINATION,
null,
// "shadowing" variable with the same name is another way
// to prevent cyclic references in callbacks.
(file, res) => {
try {
file.replace_contents_finish(res);
} catch (e) {
logError(e);
}
}
);
}
// destroy the dialog regardless of the response when we're done.
dialog.destroy();
});
// for bonus points, here's how you'd implement a simple preview widget ;)
saver.preview_widget = new Gtk.Image();
saver.preview_widget_active = false;
this.connect('update-preview', (dialog) => {
try {
// you'll have to import GdkPixbuf to use this
let pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
dialog.get_preview_filename(),
dialog.get_scale_factor() * 128,
-1
);
dialog.preview_widget.pixbuf = pixbuf;
dialog.preview_widget.visible = true;
dialog.preview_widget_active = true;
// if there's some kind of error or the file isn't an image
// we'll just hide the preview widget
} catch (e) {
dialog.preview_widget.visible = false;
dialog.preview_widget_active = false;
}
});
// this is how we'll show the dialog to the user
saver.show();
Can any one help me on how to select a drop down in protractor.
Page Object code
function selectDropdownbyNum(element, optionNum) {
if (optionNum) {
element.all(by.tagName('option')).then(function(options) {
browser.sleep('5000');
options[optionNum].click();
console.log('Desired value selected');
});
}
}
var pageName= function(){
this.selectTier = async function(){
var Tiers = element(by.xpath(/*element value*/));
console.log('select silver method');
browser.sleep(5000);
selectDropdownbyNum(Tiers,2);
console.log('value selected');
};
};
module.exports = new pageName();
And Spec is as follows
it('select Silver Tier',async function(){
browser.ignoreSynchronization = true;
console.log('Executing silver tier selection test case');
await pageName.selectTier()
});
I have tried the above code. I am able to print all the values of the drop down, but am unable to click.
Is their any mistake in the above code.I can print the 'Desired value selected'. But value was not selected
May this will help you for selecting option
element(by.cssContainingText('option','Option value')).click();
or
element(by.id('id')).sendKeys("Values from option");
this worked for me
Try:
var Tiers = element(by.xpath(dropDownValue));
Tiers.click();
selectDropdownbyNum(element, optionNum) {
if (optionNum) {
element.all(by.tagName('option')).then(function(options) {
options[optionNum].click();
});
}
}
selectDropdownbyNum(Tiers,4)
Note:
avoid using Xpath example use :
element(by.css('select[formcontrolname="any value according to situation"]'));
I haven't tested it, but I suppose it's because of the nested promise you are using inside the for loop. The nature of the promise is to be async, and the for loop is synchronous, which results in the loop complete whyle the very first promise items[i].getText().then get's resolved and that's why your click didn't succeed. If you don't need to know the option names, then just remove the nested promise items[i].getText() and just execute the click in the loop.
sorry for the confusing question. So currently i'm working with this block of code in Ionic, it looks like this:
home.ts :
ionViewDidLoad() {
this.myFunction().subscribe()( data => {
// Do some stuff here, then
// open another page/modal with the data received
this.openModal(anotherPage,data);
}
}
The problem, I believe, is that I have to do something to "unsubscribe" my current function since it's being called every second. I tried putting the whole code in the ionViewDidLoad, believing that when it jumps to another page the function will be suspended but it just doesn;t work.
openModal(zone) {
let modal = this.modalCtrl.create(ZonePage, { 'zone': zone });
modal.present();
}
Any idea to solve this situation?
one solution could be:
ionViewDidLoad() {
let sub = this.myFunction().subscribe( data => {
// Do some stuff here, then
// open another page/modal with the data received
sub.unsubscribe();
this.openModal(anotherPage,data);
}
}
another one (when you know how often it emits before it you trigger your modal):
ionViewDidLoad() {
this.myFunction().take(10).subscribe( data => {
// Do some stuff here, then
// open another page/modal with the data received
this.openModal(anotherPage,data);
}
}
I have a table which uses infinite scroll to load more results and append them, when the user reaches the bottom of the page.
At the moment I have the following code:
var currentPage = 0;
var tableContent = Rx.Observable.empty();
function getHTTPDataPageObservable(pageNumber) {
return Rx.Observable.fromPromise($http(...));
}
function init() {
reset();
}
function reset() {
currentPage = 0;
tableContent = Rx.Observable.empty();
appendNextPage();
}
function appendNextPage() {
if(currentPage == 0) {
tableContent = getHTTPDataPageObservable(++currentPage)
.map(function(page) { return page.content; });
} else {
tableContent = tableContent.combineLatest(
getHTTPDataPageObservable(++currentPage)
.map(function(page) { return page.content; }),
function(o1, o2) {
return o1.concat(o2);
}
)
}
}
There's one major problem:
Everytime appendNextPage is called, I get a completely new Observable which then triggers all prior HTTP calls again and again.
A minor problem is, that this code is ugly and it looks like it's too much for such a simple use case.
Questions:
How to solve this problem in a nice way?
Is is possible to combine those Observables in a different way, without triggering the whole stack again and again?
You didn't include it but I'll assume that you have some way of detecting when the user reaches the bottom of the page. An event that you can use to trigger new loads. For the sake of this answer I'll say that you have defined it somewhere as:
const nextPage = fromEvent(page, 'nextpage');
What you really want to be doing is trying to map this to a stream of one directional flow rather than sort of using the stream as a mutable object. Thus:
const pageStream = nextPage.pipe(
//Always trigger the first page to load
startWith(0),
//Load these pages asynchronously, but keep them in order
concatMap(
(_, pageNum) => from($http(...)).pipe(pluck('content'))
),
//One option of how to join the pages together
scan((pages, p) => ([...pages, p]), [])
)
;
If you need reset functionality I would suggest that you also consider wrapping that whole stream to trigger the reset.
resetPages.pipe(
// Used for the "first" reset when the page first loads
startWith(0),
//Anytime there is a reset, restart the internal stream.
switchMapTo(
nextPage.pipe(
startWith(0),
concatMap(
(_, pageNum) => from($http(...)).pipe(pluck('content'))
),
scan((pages, p) => ([...pages, p]), [])
)
).subscribe(x => /*Render page content*/);
As you can see, by refactoring to nest the logic into streams we can remove the global state that was floating around before
You can use Subject and separate the problem you are solving into 2 observables. One is for scrolling events , and the other is for retrieving data. For example:
let scrollingSubject = new Rx.Subject();
let dataSubject = new Rx.Subject();
//store the data that has been received back from server to check if a page has been
// received previously
let dataList = [];
scrollingSubject.subscribe(function(page) {
dataSubject.onNext({
pageNumber: page,
pageData: [page + 10] // the data from the server
});
});
dataSubject.subscribe(function(data) {
console.log('Received data for page ' + data.pageNumber);
dataList.push(data);
});
//scroll to page 1
scrollingSubject.onNext(1);
//scroll to page 2
scrollingSubject.onNext(2);
//scroll to page 3
scrollingSubject.onNext(3);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/4.1.0/rx.all.js"></script>