Running tests in Parallel based on a dynamically generated array from a beforeAll using Playwright - playwright

The scenario I'm trying to test is:
Get an array of elements from the page and read the url property of that element
Test 1 - that the array is not empty
For Each of these elements
Test 2->number of elements - Navigate to the url read above and test there are no errors on the page
So far I've got the code working, but only by including the for loop in the second test, which only registers the above as two tests and does not run the second set (ie: the for loop) in parallel.
When trying to make the second set of tests parallel (as I've done in another test that's based on a static array of pages) I end up with the below code, but the second set of tests is not even detected, ie: if I comment out Test 1, no tests found is reported.
Here's the code structure I'm trying to use. As I said above, it is based upon another test that's using a static array of elements that does seem to work as expected. The main difference with the other test is that everything is within the loop and there is no beforeAll. I'm assuming that's where the crux of the issue is.
import { test, expect, chromium } from '#playwright/test';
const pages = [];
test.describe('Pages Load', () => {
test.beforeAll(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto(url);
const allPages = await page
.locator('container id')
.evaluateAll(($elements) => {
return $elements.map((element) => {
const pageTitle = element.querySelector('element id');
const pageURL = element.getAttribute(
'attribute id'
);
return {
title: pageTitle,
url: pageURL,
};
});
});
allPages.forEach(($page) => {
pages.push({
title: $page.title,
url: $page.url,
});
});
});
test('Pages exist', () => {
expect(pages.length).toBeGreaterThan(0);
});
test.describe('No errors on pages', () => {
pages.forEach((page) => {
test.describe(`${page.title}`, () => {
test('No errors', async () => {
// Test goes here
});
});
});
});
});
Update:
I've had a bit more of a play with this and while I know why this code isn't working, I still have no idea how to resolve the issue.
Couple of things I've noticed (and these are just theories/assumptions based on what I've observed):
Firstly, the reason why the above isn't working is because the array isn't set up by the time it get's to the loop, as the beforeAll only appears to happen with each test, not the test.describe. While this is just a theory (can't see anything in the documentation proving or disproving this), it does explain why the second test is ignored, because the array.length is 0
Secondly, when attempting to extract the functionality into a separate function outside of the test, as suggested here, however, this too had no luck, as when it gets to setting up the browser with const browser = await chromium.launch(); in an external function, playwright seems to exit with no errors being thrown. My assumption is that the browser setup needs to be within a test context, though that is just a theory.
Any advice on how I can get the desired results would be greatly appreciated. Many thanks in advance.

Related

How can I assert that an element is NOT on the page in playwright?

I'm testing a website that includes a logo, and I want to make sure the logo does not appear on some pages.
How can I assert that an element does NOT exist? I checked the Playwright assertions documentation, but it only has examples of checking for things that do exist.
async assertNoLog(): Promise<boolean> {
await this.page.locator('div#page-id'); // now the page has loaded
// How do I check if logo is on page without throwing an error if it is missing
}
I'm not sure what to write here to assert that my element is not found anywhere on the page.
You can use .count() and assert that it returns 0.
expect(await page.locator('.notexists').count()).toEqual(0);
https://playwright.dev/docs/api/class-locator#locator-count
I wanted to know that an element wasn't on screen, but I also wanted to wait until it was gone, and this is the way to do that:
await expect(locator).toHaveCount(0);
Found here
You can play with the conditions you expect your element to have. For example, at Playwright's homepage you expect an element by the class .navbar__brand to be visible, but you also expect an element by the class .notexists NOT to be visible (in this case this element would not exist). Then you can do:
test('element does exist #pass', async ({ page }) => {
await page.goto('https://playwright.dev/');
const locator = await page.locator('.navbar__brand').isVisible();
expect(locator).toBeTruthy();
});
test('element does NOT exist #fail', async ({ page }) => {
await page.goto('https://playwright.dev/');
const locator = await page.locator('.notexists').isVisible();
expect(locator).toBeTruthy();
});
Doing this, of course, would return the same results:
test('element does exist #pass', async ({ page }) => {
await page.goto('https://playwright.dev/');
expect(await page.locator('.navbar__brand').isVisible()).toBe(true);
});
test('element does NOT exist #fail', async ({ page }) => {
await page.goto('https://playwright.dev/');
expect(await page.locator('.notexists').isVisible()).toBe(true);
});
As I say, the element's conditions are up to you. For example, if you want to assert an element with visibility:hidden is also not present in the DOM, because it simply shouldn't be, you can wrap the visibility and a .isHidden() conditions within a if/else, etc. And of course, feel free to play with booleans (toBe(true)/toBe(false), toBeTruthy()/toBeFalsy()).
These are not the most elegant solutions out there, but I hope they can help.

How to free up memory from Puppeteer in infinite scroll?

I browse an infinite scroll page using Puppeteer but this page is really really long. The problem is that the memory used by Puppeteer grows way too much and after a while, it crashes. I was wondering if there is a nice way to somehow free up memory during the scroll.
For example, would it be possible to pause every minute to remove the HTML that has been loaded so far and copy it to the hard disk? That way, after I'm done scrolling, I have all the HTML in a file and can easily work with it. Is it possible to do that? If yes, how? If no, what would be a viable solution?
I would wager that the approach you outline would work. The trick will be to remove nodes from only the list that is being added to. The implementation would maybe look something like this:
await page.addScriptTag({ url: "https://code.jquery.com/jquery-3.2.1.min.js" });
const scrapedData = [];
while (true) {
const newData = await page.evaluate(async () => {
const listElm = $(".some-list");
const tempData = listElm.toArray().map(elm => {
//Get data...
});
listElm
.children()
.slice(20)
.remove();
//TODO: Scroll and wait for new content...
return tempData;
});
scrapedData.push(...newData)
if(someCondition){
break;
}
}

Can I use whileElement(...).atIndex(..) to distinguish multiple scroll page id's

I was getting the following error: Multiple elements were matched
However it turned out that the scrolling page ID was multiple times matched. This means that I need something like this:
await waitFor(element(by.id("someID")).toBeVisible()whileElement(by.id("anotherID")).atIndex(1).scroll(50, 'down')
I tried this, but get the following error:
TypeError: global.waitFor(...).toBeVisible(...).whileElement(...).atIndex is not a function
So my question is, when there are two scroll elements with the same id, can I select one of them with the function atIndex?
Or is there another solution for this?
Thanks in advance
Same issue experienced here. I was able to solve it using a function that scrolls the list until it meets the condition.
something like that:
await tryTap();
...
const tryTap = async() => {
try { await element(by.id('someID')).tap(); }
catch (e) {
await element(by.type("anotherID")).atIndex(1).scroll(50, "down");
await tryTap();
}
}

Knockout mapping is not updating my model

I'm having trouble with a knockout model that is not binding on a subscribed update. I have a C# MVC page that delivers a model to the template which is parsed to Json and delivered raw as part of a ViewModel assignment for ko.applyBindings. I have a subscription to an observable that calls a method to perform an update of the viewModel's data. Irrelevant stuff pulled out and renamed for example usage:
var myViewModel = function (data) {
var self = this;
self.CurrentPage = ko.observable();
self.SomeComplexArray= ko.observableArray([]);
self.Pager().CurrentPage.subscribe(function (newPage) {
self.UpdateMyViewModel(newPage);
});
self.UpdateMyViewModel= function (newPage) {
var postData = { PageNumber: newPage };
$.post('/Article/GetMyModelSearchByPage', postData, function (data) {
ko.mapping.fromJS(data, {}, self);;
});
};
When I perform logging, I can see all of the data, and it all looks correct. The same method is used to produce both the initial model and the updated model. I've used this technique on other pages and it worked flawlessly each time. In this case however, I'm looking for it to bind/update SomeComplexArray, and that's just not happening. If I attempt to do it manually, I don't get a proper bind on the array I get blank. I'm wondering if there is something obvious that I'm doing wrong that I'm just flat out missing.
Edit: I don't know that ko.mapping can be pointed to as the culprit. Standard model changes are also not affecting the interface. Here is something that is not working in a bound sense. I have a p element with visible bound to the length of the array and a div element with a click bound to a function that pops items off of SomeComplexArray. I can see in the console log that it is performing its function (and subsequent clicks result in 'undefined' not having that function). However, the p element never displays. The initial array has only 2 items so a single click empties it:
<p data-bind="visible: SomeComplexArray().length === 0">nothing found</p>
<div data-bind="click: function() { UpdateArray(); }">try it manually</div>
-- in js model
self.UpdateArray = function () {
console.log(self.SomeComplexArray());
console.log(self.SomeComplexArray().pop());
console.log(self.SomeComplexArray());
console.log(self.SomeComplexArray().pop());
console.log(self.SomeComplexArray());
});
Edit 2: from the comment #Matt Burland, I've modified how the pop is called and the manual method now works to modify the elements dynamically. However, the ko.mapping is still not functioning as I would expect. In a test, I did a console.log of a specific row before calling ko.mapping and after. No change was made to the observableArray.
I created a test of your knockout situation in JSFiddle.
You have to call your array function without paranthesis. I tested this part:
self.UpdateArray = function () {
self.SomeComplexArray.pop();
};
It seems to be working on JSFiddle side.
I'm not really sure why, but it would seem that ko.mapping is having difficulty remapping the viewmodel at all. Since none of the fields are being mapped into self my assumption is that there is an exception occurring somewhere that ko.mapping is simply swallowing or it is not being reported for some other reason. Given that I could manually manipulate the array with a helpful tip from #MattBurland, I decided to backtrack a bit and update only the elements that needed to change directly on the data load. I ended up creating an Init function for my viewModel and using ko.mapping to populate the items directly there:
self.Init = function (jsonData) {
self.CurrentPage(0);
self.Items(ko.mapping.fromJS(jsonData.Items)());
self.TotalItems(jsonData.TotalItems);
// More stuff below here not relevant to question
}
The primary difference here is that the ko.mapping.fromJS result needed to be called as a function before the observableArray would recognize it as such. Given that this worked and that my controller would be providing an identical object back during the AJAX request, it was almost copy/past:
self.UpdateMyViewModel= function (newPage) {
var postData = { PageNumber: newPage };
$.post('/Article/GetMyModelSearchByPage', postData, function (data) {
self.Items(ko.mapping.fromJS(JSON.parse(data).Items)());
});
};
This is probably not ideal for most situations, but since there is not a large manipulation of the viewModel occurring during the update this provides a working solution. I would still like to know why ko.mapping would not remap the viewModel at the top level, but in retrospect it probably would have been a disaster anyway since there was "modified" data in the viewModel that the server would have had to replace. This solution is quick and simple enough.

UI not updating on 'pageinit', probably timing issue

I have the following .js file for my project. This is running in a regular browser, using jquery 1.9.x and jquerymobile 1.3.1. The init function below appears to be running when the page loads and the UI is not updated. Though... I can copy the function into the console and run it, and the UI updates as it is supposed to, so this in not a case of incorrect file paths, or incorrect ids for the UI elements, but I suspect timing. I am also NOT using cordova or phone gap in this instance.
So, my question is, why is the UI not updating when the $(document).bind('pageinit', ...) function is called? If I put a breakpoint in the init method, it is getting called when the page loads. Any suggestions on using a different event or approach?
var simulator = simulator || {};
(function (feedback, $, undefined) { 'use-strict';
feedback.init = function () {
$.get('feedback-config.xml', function (data) {
$('#feedback-to').val($(data).find('email').text());
$('#feedback-subject').val($(data).find('emailSubject').text());
$('#feedback-display').html($(data).find('message').text());
$('#feedback-form').attr('action', $(data).find('serverurl').text()).ajaxForm({success: function () {
alert("Thank you for your feedback!");
}, error: function () {
alert("We're having difficulties sending your feedback, sorry for the inconvenience.");
}});
});
};
}(simulator.feedback = simulator.feedback || {}, jQuery));
$(document).bind('pageinit', function () { 'use strict';
simulator.feedback.init;
});
Thanks in advance.
Found it, simple mistake... In the 'pageinit' function I am calling simulator.feedback.init; instead of simulator.feedback.init(); Not sure why JSLint didn't pick that up initially, but it pointed it out when I tried again later. Thanks.

Resources