ElementScrolled Event is not being fired with CdkScrollable and mat-side-nav - scroll

I have the following DOM Configuration
<div class="sticky-nav-wrapper">
<app-header (notificationClick)="notificationData=$event; sidenav.toggle()"></app-header>
</div>
<mat-sidenav-container>
<mat-sidenav-content>
<router-outlet></router-outlet>
</mat-sidenav-content>
<mat-sidenav #sidenav mode="over">
<app-notifications [notificationData]="notificationData" [opened]="sidenav.opened" (notificationItemClick)="sidenav.close()"></app-notifications>
</mat-sidenav>
</mat-sidenav-container>
In my app-notification component which is inside mat-sidenav, I want to listen to the scroll. Following is my template for app-notification
<div class="class-notification-container" cdkScrollable>
<div *ngIf="notifications && notifications.length > 0">
<div *ngFor="let item of notifications">
<div class="class-notification" [ngClass]="{'class-notification-new': item.localStatus === 'new','class-notification-old': item.localStatus === 'seen'}" (click)="onNotificationClick(item)">
<app-avatar [imageId]="item?.fromUser?.id"></app-avatar>
</div>
<mat-divider></mat-divider>
</div>
</div>
</div>
In my app-notification.ts:
#ViewChild(CdkScrollable) notificationContainer: CdkScrollable;
and
ngAfterViewInit(): void {
this.notificationContainer.elementScrolled()
.subscribe((scrollPosition) => {
this.ngZone.run(() => {
if (!this.endOfNotifications) {
this.pageNumber = this.pageNumber + 1;
this.getNotifications();
}
});
}, (error) => {});
}
However, this subscription is never invoked, no matter how much I scroll inside the mat-side-nav. The docs says that the CdkScrollable directive emits an observable on host elemnet scroll.
Returns observable that emits when a scroll event is fired on the host
element.
The other alternative is to listen to the scrollDispatcher. The issue however is scrolldispatcher is sending events on window scroll and not specifically on side-nav scroll.
I seem to be doing everything OK as per the doc. Can someone please suggest where the things are going wrong. I specifically want to listen to the scroll of my component within the side-nav i.e. app-notification component. Also I do not want to listen to the window scroll or to say the mat-side-nav-content scroll.

It may be you're listening on too high of a parent.
Have you tried listening on the div with the *ngFor ?
<div >
<div *ngIf="notifications && notifications.length > 0">
<div class="class-notification-container" cdkScrollable *ngFor="let item of notifications">
<div class="class-notification" [ngClass]="{'class-notification-new': item.localStatus === 'new','class-notification-old': item.localStatus === 'seen'}" (click)="onNotificationClick(item)">
<app-avatar [imageId]="item?.fromUser?.id"></app-avatar>
</div>
<mat-divider></mat-divider>
</div>
</div>
</div>

Related

Playwright: Click button does not work reliably (flaky)

1. Initial post
I know that Playwright has auto-waiting.
I have a test where sometimes click works, sometimes does not. Below is the code.
import { test, expect } from '#playwright/test';
test('test', async ({ page }) => {
await page.goto('https://website.com');
await page.getByRole('link', { name: 'Log in' }).click();
await expect(page).toHaveURL('https://website.com/login/');
});
Error:
expect(received).toHaveURL(expected)
Expected string: "https://website.com/login/"
Received string: "https://website.com"
Here is a piece of HTML with a login button
<span data-v-154d6ddf="" class="absolute-top-right">
<div class="row justify-end q-gutter-x-sm q-pt-lg q-px-sm">
<div class="col-auto">
<a tabindex="0" type="button" href="/login/potential" role="link" class="q-btn q-btn-item non-selectable no-outline q-btn--flat q-btn--rectangle text-white q-btn--actionable q-focusable q-hoverable q-btn--no-uppercase q-btn--wrap">
<span class="q-focus-helper" tabindex="-1"></span>
<span class="q-btn__wrapper col row q-anchor--skip">
<span class="q-btn__content text-center col items-center q-anchor--skip justify-center row">
<span>Log in</span>
</span>
</span>
</a>
</div>
<div class="col-auto">
<a tabindex="0" type="button" href="/signup" role="link" class="q-btn q-btn-item non-selectable no-outline q-btn--standard q-btn--rectangle bg-primary text-white q-btn--actionable q-focusable q-hoverable q-btn--no-uppercase q-btn--wrap">
<span class="q-focus-helper"></span>
<span class="q-btn__wrapper col row q-anchor--skip">
<span class="q-btn__content text-center col items-center q-anchor--skip justify-center row">
<span>Sign up</span>
</span>
</span>
</a>
</div>
<div class="col-auto">
<label for="f_2334a082-8df9-41a7-9ca0-403e5ce7c237" class="q-field q-validation-component row no-wrap items-start q-select q-field--auto-height q-select--without-input q-select--without-chips q-field--outlined q-field--float q-field--labeled" style="min-width:120px;max-height:20px">
<div class="q-field__inner relative-position col self-stretch column justify-center">
<div tabindex="-1" class="q-field__control relative-position row no-wrap">
<div class="q-field__control-container col relative-position row no-wrap q-anchor--skip">
<div id="translate" class="q-field__native row items-center">
<span>English</span>
<div id="f_2334a082-8df9-41a7-9ca0-403e5ce7c237" tabindex="0" class="no-outline"></div>
</div>
<div class="q-field__label no-pointer-events absolute ellipsis">Translate</div>
</div>
<div class="q-field__append q-field__marginal row no-wrap items-center q-anchor--skip">
<i aria-hidden="true" role="presentation" class="q-select__dropdown-icon fal fa-caret-down q-icon notranslate"></i>
</div>
</div>
</div>
</label>
</div>
</div>
</span>
2. Update
I tried this variation (see below), and it seems like it fails less frequently, but still does not work 100%.
I also tried .click({force: true}). Fails on 3rd run.
I am new to test automation. Just started learning Playwright.
I will appreciate any comments/suggestions.
thank you.
import { test, expect } from '#playwright/test';
test('test', async ({ page }) => {
await page.goto('https://website.com');
const loginBtn = page.getByRole('link', { name: 'Log in' });
expect(await loginBtn.evaluate(node => node.isConnected)).toBe(true) ;
await page.getByRole('link', { name: 'Log in' }).click();
await expect(page).toHaveURL('https://website.com/login/');
});
3. Update
"You should wait after clicking the button before verifying the url." This ws suggestion from #jaky-ruby
I tried this code
test('test2', async ({ page }) => {
await page.goto('https://website.com/');
// Note that Promise.all prevents a race condition
// between clicking and expecting navigation result.
const [response] = await Promise.all([
page.getByRole('link', { name: 'Log in' }).click(),
page.waitForNavigation({ url: 'https://website.com/login' }),
]);
await expect(page).toHaveURL('https://website/login');
...
});
4. Update
I created similar test in Cypress.
Here is the code
describe("Login", () => {
beforeEach(() => {
cy.visit(baseUrl);
});
it.only("Login page should open", () => {
//does not help: cy.wait(3000);
cy.get('a[href*="login"]').click();
//does not help: cy.get(':nth-child(1) > .q-btn > .q-btn__wrapper > .q-btn__content > span').click();
//does not help: cy.get('#q-app > div > span > div > div:nth-child(1) > a > span.q-btn__wrapper.col.row.q-anchor--skip > span').click();
cy.url().should("eq", authUrl.replace("env", env) + "/login/potential");
});
});
As you can see i tried different variations of locators. The interesting part is that Cypress showed this error. Not sure this is the same problem that occurs in Playwright test.
CypressError
Timed out retrying after 4050ms: cy.click() failed because this element: ...is being covered by another element:
I tried to find that element in the DOM but it is not there.
5. Update
Another thing that I do not understand is that when I select "click" step in Cypress and use picker to select my button it shows me selector cy.get('[data-layer="Content"]'). Again, when I try to search it in the DOM it is not there.
6. Update
I checked each step in Playwright using Trace Viewer.
Interesting. The log shows that Playwright waited for element to be visible and stable. However, on the attached page both Login and Sign up buttons are not visible and still loading (turns out you can inspect it. It's not just a static screenshot). I think, possibly, Playwright's auto-wait is not good enough in this example. Possibly, element is not actually stable and still loading but Playwright performs click which does not work.
Now question, "can I do some additional wait to unsure that element has loaded and stable? "
Okay, looks like I found ~~solution~~ a way to make my test less flaky.
Test still fails sometimes. I think, additional 2 waits which I added just give more time for element to load, so in most cases, probably, that is enough for button to reach stable state. That's why clicking works. Note, on the screenshot below that 'networkidle' is 1.4s
Here is the final code
import { test, expect } from '#playwright/test';
test('test', async ({ page }) => {
await page.goto('https://website.com');
await page.waitForLoadState('networkidle'); // <- until there are no network connections for at least 500 ms.
await page.getByRole('link', { name: 'Log in' }).waitFor(); // <- wait for element
await page.getByRole('link', { name: 'Log in' }).click();
await expect(page).toHaveURL('https://website.com/login/');
});
Sharing here some interesting links I found along the way
Avoiding hard waits in Playwright and Puppeteer - DEV Community 👩‍💻👨‍💻
waitForLoadState waitUntil domcontentloaded doesn't wait · Issue #662 · microsoft/playwright · GitHub

Alpine JS - Creating a menu with active states

I am trying to create a sidebar menu with Alpine JS
I am not even sure if this is possible to do with Alpine JS.
#foreach($kanbans as $kanban)
<div x-data="activeKanban : ????">
<div #click="activeKanban = {{$kanban->id}}">
<div x-show="activeKanban !== {{$kanban->id}}">
// Design for collapsed kanban
</div>
<div>
<div x-show="activeKanban === {{$kanban->id}}">
// Design for active kanban
</div>
</div>
#endforeach
As I switch trough the pages, the $kanban->id changes, and I was wondering instead of manually setting activeKanban : 1 is there a way to pass this information to AlpineJS?
So that by default if I load the other page, the default menu that would be open would be based on the ID instead of them all being collapsed or just 1 that is specified being open?
If you're aiming for an accordion menu of sorts here's how you might achieve it with AlpineJs based on the code you shared:
// Set x-data on a div outside the loop and add your active kanban as a property
<div x-data="{
activeKanban: {{ $activeKanbanId ?? null }}
}">
#foreach($kanbans as $kanban)
<div #click="activeKanban = {{ $kanban->id }}">
<div x-show="activeKanban !== {{ $kanban->id }}">
// Collapsed view
</div>
<div x-show="activeKanban === {{ $kanban->id }}">
// Expanded view
</div>
</div>
#endforeach
</div>
Here each kanban menu item will have access to the activeKanban property in AlpineJs component instance and can set it reactively.
i.e. if activeKanban is set to a new id the current open item will close and the new one will open
Adding flexibility
What if you want to open and close them all independently though? There's more than one way to achieve this but in this case we can modify the code above to allow for it:
// Here we add an array of openItems and two helper functions:
// isOpen() - to check if the id is either the activeKanban or in the openItems array
// toggle() - to add/remove the item from the openItems array
<div x-data="{
activeKanban: {{ $activeKanbanId ?? null }},
openItems: [],
isOpen(id){
return this.activeKanban == id || openItems.includes(id)
},
toggle(id){
if(this.openItems.includes(id)){
this.openItems = this.openItems.filter(item => {
return item !== id
});
}else{
this.openItems.push(id)
}
}
}">
#foreach($kanbans as $kanban)
<div #click="toggle({{ $kanban->id }})">
<div x-show="!isOpen({{$kanban->id}})">
// Collapsed view
</div>
<div x-show="isOpen({{$kanban->id}})">
// Expanded view
</div>
</div>
#endforeach
</div>
This allows us to set an active item and also optionally open/close other menu items.

How to filter through assertion on deep child div content but yield original element?

I have a DOM tree and would like to use cypress to do e2e testing.
<div class="this-is-an-array">
<div class="array-item-element">
...
</div>
<div class="array-item-element">
<div class="very">
<div class="deep">
<div class="child">
<div class="element">
Expected Content
</div>
</div>
</div>
</div>
<div class="another">
<div class="component">
<div class="dom">
<div class="tree">
<button>ButtonToClick</button>
</div>
</div>
</div>
</div>
</div>
<div class="array-item-element">
...
</div>
</div>
I would like to filter out expected item by asserting on its one child component and click on its another child button through possible code like this:
cy.get('.this-is-an-array') // get the root node of array
.find('.array-item-element') // looping over all items
.should(($el) => {
// the very deep child element should contain text of 'Expected Content'
})
.get('.another .component .dom .tree button')
.click()
How can I write the should clause to achieve this?
Without fully understanding what you're trying to do, I think what you need is cy.within():
cy.get(`.this-is-an-array`)
.find(`.array-item-element`).then( $elems => {
$elems.each((idx, elem) => {
cy.wrap(elem)
.should(elem => {
// assert on child element
})
// ensure all subsequent DOM commands are scoped within
// the wrapped DOM elem
.within(() => {
cy.get(`.another .component .dom .tree button`)
.click();
});
});
});

Angular UI Bootstrap - I can open and close a modal just once

I have a weird problem with a modal. It should be a absolutely normal modal that I can open and close many times, but I can open just one and also just close one time! http://plnkr.co/ksTy0HdifAJDhDf4jcNr
My index.html file looks this:
<body ng-controller="MainCtrl">
<div ng-include src="'widget.html'" ng-controller="WidgetCtrl"></div>
<!-- other widgets and content -->
</body>
As you can see I have devided my application in different parts (called widgets), that I am including per ng-include in my index html file. Every widget has it's own controller.
The widget.html looks like this:
<div modal="theModal">
<div class="modal-header"><h3>The Modal</h3></div>
<div class="modal-body">
Body intentionally left blank
</div>
<div class="modal-footer">
<button class="btn" type="button" ng-click="CloseModal()">ok</button>
</div>
</div>
<!-- more modals and other stuff -->
<button ng-click="OpenModal()">open modal</button>
And now the widget controller (which is a child controller of the main controller)
app.controller('WidgetCtrl', function ($scope) {
$scope.OpenModal = function() { $scope.theModal = true; }
$scope.CloseModal = function() { $scope.theModal = false;}
});
All stuff for opening and closing the modal is part of the sub controller (WidgetCtrl) and therefore shouldn't conflict with anything from the parent controller.
$scope.theModal is in the beginning undefined, so the modal is not shown. With a click on the button $scope.theModal is defined and set to true; this is triggered by Angular UI and the modal is shown. On a click of ok, the now existing $scope.theModal is set to false and the modal disappears. Everything is perfect but .. it's not working again!
You just have to include close="CloseModal()" in the first div of your widget
<div modal="theModal" close="CloseModal()">
<div class="modal-header"><h3>The Modal</h3></div>
<div class="modal-body">
Body intentionally left blank
</div>
<div class="modal-footer">
<button class="btn" type="button" ng-click="CloseModal()">ok</button>
</div>
</div>
Here is a working plunker

ajax script onclick alert to popup message box

I found this voting script online and was wondering instead of a onclick alert(You already voted) can i change it to a popup message that i can style with css... i have only submitted a part of the code if you need to see more let me know. thanks in advance
function addVotData(elm_id, vote, nvotes, renot) {
// exists elm_id stored in ivotings
if(ivotings[elm_id]) {
// sets to add "onclick" for vote up (plus), if renot is 0
var clik_up = (renot == 0) ? ' onclick="addVote(this, 1)"' : ' onclick="<a type="button" class="btn" style="width:100%;" href="#test_modal" data-toggle="modal">alert</a>"';
// if vot_plus, add code with <img> 'votplus', else, if vot_updown1/2, add code with <img> 'votup', 'votdown'
if(ivotings[elm_id].className == 'vot_plus') { // simple vote
ivotings[elm_id].innerHTML = '<h6>'+ vote+ '</h6><span><img src="'+votingfiles+'arrow.png" alt="1" title="vote"'+ clik_up+ '/></span>';
};
}
}
You can use Bootstrap modal pop up instead of alert.
This is well explained example
If you need more help let me know
Update
html for model pop up:
<div class="modal fade" id="test_modal"> <div class="modal-header">
<a class="close" data-dismiss="modal">×</a> <h3>Modal Header</h3> </div>
<div class="modal-body"> <p>Test Alert</p> </div> <div class="modal-footer">
Close </div> </div>
Html for Button
<input type="Button" Text="ShowModal" Id="MyButton"/>
javaScript:
$( "#MyButton" ).click(function() {
$('#modalName').modal('show');
});

Resources