URL assertion failing in playwright while implementing Page Object model - playwright

I am trying to implement POM in playwright, I have created a class in which I am trying to click two buttons to navigate to a page and then apply URL assertion to check the expected URL. But the URL of the Page isn't changing and the assertion is getting failed. here is the class code:
exports.HomePage = class HomePage {
constructor(page) {
this.page = page
this.login_button = page.getByRole('button', { name: 'Login' })
this.client_login_button = page.getByRole(('link', { name: 'CLIENT LOGIN' }))
}
async gotoHomePage(){
await this.page.goto('https://plentifulapp.com/');
await this.login_button.click
await this.client_login_button.click
}
}
and here is the main test file.
test.only('ValidLogin', async ({ page }) => {
const Home = new HomePage(page);
await Home.gotoHomePage();
await expect(page).toHaveURL('https://app.plentifulapp.com/a/pantries')
Asserting applied in main test file in getting failed.

It appears the culprit is that the click methods aren’t actually being called, you’re just referencing the methods themselves. So the fix hopefully is just putting () after .click which should at least correct that and make sure the clicks are actually happening.

Related

can we use the toHaveScreenshot() and toMatchSnaphot() out side the test

Can we use the toHaveScreenshot() and toMatchSnaphot() outside the test without using config file only simple install NPM i playwright in package.json
I have already one snapshot I want to compare snapshot using toHaveScreenshot() method but I am confused we can use outside the test context?
const { chromium } =require( "playwright");
const example = async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
await page.goto("https://zversal.com/");
await page.toHaveScreenshot("zeversal.png", {
fullPage: false,
maxDiffPixelRatio: 0.24,
});
};
example();
Console reports error:
toHaveScreenshot() must be called during the test
I don't think this is possible. Afaik, toHaveScreenshot() is part of the #playwright/test package.
If I'm looking at the Page API docs there's no toHaveScreenshot() listed. I'd say it's only available in combination with Playwright Test and it's provided expect method.
await expect(page).toHaveScreenshot();
As similarly noted by stefan judis in his answer, toHaveScreenshot is an assertion matchers method provided by expect which comes from the Playwright Test library and is made to be used inside tests.
While that assertion can’t be used, you can still take screenshots using Playwright, and then compare them manually or with another tool.
I don't know if it's possible to use it outside of testing, but you can create a PlaywrightDevPage helper class to encapsulate common operations on the page.
Simple Usage =>
// models/PlaywrightDevPage.js
class PlaywrightDevPage {
/**
* #param {import('playwright').Page} page
*/
constructor(page) {
this.page = page;
this.getStartedLink = page.locator('a', { hasText: 'Get started' });
this.gettingStartedHeader = page.locator('h1', { hasText: 'Installation' });
this.pomLink = page.locator('li', { hasText: 'Playwright Test' }).locator('a', { hasText: 'Page Object Model' });
this.tocList = page.locator('article div.markdown ul > li > a');
}
async getStarted() {
await this.getStartedLink.first().click();
await expect(this.gettingStartedHeader).toBeVisible();
}
async pageObjectModel() {
await this.getStarted();
await this.pomLink.click();
}
}
module.exports = { PlaywrightDevPage };
More Info => PlaywrightDevPage

Playwright save storage state only for certain files

I currently have a test pack that has >50 files. 49 of the files will use the same authentication, so I have set up the below inside my playwright config file:
storageState: 'myauth.json',
This allows me to store the state and use it across all of the tests,
the issue becomes where I don't want to use this state in one of my tests. How would one go about this?
I'm aware I could pass the state into 49 of the files and ignore it for the one, but this seems like the wrong idea.
If you do not want to use any storageState in your 50th test, then you can use test.use({ storageState: undefined }); in that test:
import { test } from '#playwright/test';
import LoginPage from '../../page/LoginPage';
test.use({ storageState: undefined });
test('This test ignores the storageState and performs a fresh login', async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.login();
});
You can use fixtures and as default value set the one you use 49 times.
In that case when you are need other auth just overwrite it with keyword "use". It is described in playwright doc. https://playwright.dev/docs/test-fixtures
import { TodoPage } from './todo-page'; //In your case auth1 auth2
import { SettingsPage } from './settings-page';
// Declare the types of your fixtures.
type MyFixtures = {
todoPage: TodoPage;
settingsPage: SettingsPage;
};
// Extend base test by providing "todoPage" and "settingsPage".
// This new "test" can be used in multiple test files, and each of them will get the fixtures.
export const test = base.extend<MyFixtures>({
todoPage: async ({ page }, use) => {
// Set up the fixture.
const todoPage = new TodoPage(page);
await todoPage.goto();
await todoPage.addToDo('item1');
await todoPage.addToDo('item2');
// Use the fixture value in the test.
await use(todoPage);
// Clean up the fixture.
await todoPage.removeAll();
},
settingsPage: async ({ page }, use) => {
await use(new SettingsPage(page));
},
});
Default auth use as you you page in your tests.
And when you need to overwrite in your specific file spec.ts file just write:
test.use({ auth: 'reareAuth' });

Plaid Link Javascript

I am integrating with Plaid Link and Stripe into a wizard form flow (many pages with many fields on each page). All information that the user enters is stored in a global variable "this.applicationservice.application.applicant". The user hits a payment verification in the middle of this flow, where Plaid pops an IFrame after calling Plaid.create(plaidparameters).open(). When Plaid is initialized it wipes my browser memory and "this.applicationservice.application.applicant" is now undefined.
How can I avoid losing the browser memory when calling the Plaid Initialization?
this.http.post('https://localhost:8080/v1/validateach').subscribe(
response => {
let plaidparameters = {
token: response.linkToken,
onSuccess: function(public_token, metadata) {
// Memory is wiped
console.log(this.applicationservice.application)
}
};
// Works Fine
console.log(this.applicationservice.application)
Plaid.create(plaidparameters).open();
}
);
So the problem is that the scope of this inside the onSuccess callback doesn't extend outside the callback.
One of my colleagues who works a lot more on JavaScript suggested the following:
this.http.post('https://localhost:8080/v1/validateach').subscribe(
response => {
const onSuccess = (public_token, metadata) => {
console.log(this.applicationservice.application);
};
let plaidparameters = {
token: response.linkToken,
onSuccess,
};
// Works Fine
console.log(this.applicationservice.application)
Plaid.create(plaidparameters).open();
}
);
And also added: It might actually be better to define that callback function outside of the invocation of this.http.post so it doesn’t inherit its scope from the plaidparameters object.

How to handle browser history using History class

So I'm learning dart and web development in general. Right now I'm experimenting with the history API. I have:
import 'dart:html';
void main() {
ParagraphElement paragraph = querySelector('.parag');
ButtonElement buttonOne = querySelector('.testaja');
buttonOne.onClick.listen((_) {
window.history.pushState(null, 'test title', '/testdata/');
window.history.forward();
});
ButtonElement buttonTwo = querySelector('.testlagi');
buttonTwo.onClick.listen((_) {
window.history.back();
});
window.onPopState.listen((_) {
window.alert(window.location.pathname);
});
}
My conclusion is that onPopState only triggers when we click on browser's back or forward button, or using window.history.forward() or window.history.back(). So this is like, we render a template, then change its url using pushState, not update template based on url changes. Is this true or not?
Edit:
So maybe I'm not clear enough. Let's say I have something like this:
void main() {
InputElement input = querySelector('.input')
ButtonElement changeUrl = querySelector('.change-url');
changeUrl.onClick.listen((event) {
window.history.pushState(null, 'test tile', input.value);
});
Map urls = {
'/' : showRoot,
'/user/:id' : showUserProfile
};
window.onPopState.listen((_) {
var location = window.location.pathname;
urls[location]();
});
}
I can get input's value by clicking on changeUrl, and then by adding a listener to changeUrl, I can use pushState to update url on browser. What I'm expecting is, when I do pushState, the window.onPopState will triggered and invoke the callback when in reality it doesn't.
tldr, what I'm trying to achieve is:
listen on url changes -> get current url -> use current url to invoke a handler stored in a map. Using onHashChange also doesn't work when updating url using pushState prefixed by #.
edit
set the hash using
window.location.hash = input.value;
this triggers the PopState and HashChange event
as does a click on such a link
abc
original
I don't have time to take a close look what you'r trying to achive..
But I think you should add an event handler for 'window.onHashChange' this way ordinary links work too for navigation, not only buttons with onclick-handlers modifying browser history.

Streaming Standard Output of a console application to an ASP.NET MVC View

What I am looking to do is:
1) From an MVC View, Start a long running Process. In my case, this process is a seperate Console Application being executed. The Console Application runs for potentially 30 minutes and regurlarily Console.Write's its current actions.
2) Back on the MVC View, periodically poll the server to retrieve the latest Standard Out which I have redirected to a Stream (or anywhere I can get access to it for that matter). I'll append newly retieved standard output to a log textbox or something equivalent.
Sounds relativly easy. My client side programming is a bit rusty though and I'm having issues with the actual streaming. I would assume this is not an uncommon task. Anyone got a decent solution for it in ASP.NET MVC?
Biggest issue seems to be that I cant get the StandardOutput until the end of execution, but I was able to get it with an event handler. Of course, using the event handler seems to lose focus of my output.
This is what I was working with so far...
public ActionResult ProcessImport()
{
// Get the file path of your Application (exe)
var importApplicationFilePath = ConfigurationManager.AppSettings["ImportApplicationFilePath"];
var info = new ProcessStartInfo
{
FileName = importApplicationFilePath,
RedirectStandardError = true,
RedirectStandardInput = true,
RedirectStandardOutput = true,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
UseShellExecute = false
};
_process = Process.Start(info);
_process.BeginOutputReadLine();
_process.OutputDataReceived += new DataReceivedEventHandler(_process_OutputDataReceived);
_process.WaitForExit(1);
Session["pid"] = _process.Id;
return Json(new { success = true }, JsonRequestBehavior.AllowGet);
}
void _process_OutputDataReceived(object sender, DataReceivedEventArgs e)
{
_importStandardOutputBuilder.Insert(0, e.Data);
}
public ActionResult Update()
{
//var pid = (int)Session["pid"];
//_process = Process.GetProcessById(pid);
var newOutput = _importStandardOutputBuilder.ToString();
_importStandardOutputBuilder.Clear();
//return View("Index", new { Text = _process.StandardOutput.ReadToEnd() });
return Json(new { output = newOutput }, "text/html");
}
I haven't written the client code yet as I am just hitting the URL to test the Actions, but I'm also interested how you would approach polling for this text. If you could provide the actual code for this too, it would be great. I would assume you'd have a js loop running after kicking off the process that would use ajax calls to the server which returns JSON results... but again, its not my forte so would love to see how its done.
Thanks!
Right, so from the couple of suggestions I received and a lot of trial and error I have come up with a work in progress solution and thought I should share with you all. There are definitely potential issues with it at the moment, as it relies on static variables shared across the website, but for my requirement it does the job well. Here goes!
Let's start off with my view. We start off by binding the click event of my button with some jquery which does a post to /Upload/ProcessImport (Upload being my MVC Controller and ProcessImport being my MVC Action). Process Import kicks off my process which I will detail below. The js then waits a short time (using setTimeout) before calling the js function getMessages.
So getMessages gets called after the button is clicked and it does a post to /Upload/Update (my Update action). The Update action basically retrieves the status of the Process and returns it as well as the StandardOutput since last time Update was called. getMessages will then parse the JSON result and append the StandardOutput to a list in my view. I also try to scroll to the bottom of the list, but that doesn't work perfectly. Finally, getMessages checks whether the process has finished, and if it hasn't it will recursivly call itself every second until it has.
<script type="text/javascript">
function getMessages() {
$.post("/Upload/Update", null, function (data, s) {
if (data) {
var obj = jQuery.parseJSON(data);
$("#processOutputList").append('<li>' + obj.message + '</li>');
$('#processOutputList').animate({
scrollTop: $('#processOutputList').get(0).scrollHeight
}, 500);
}
// Recurivly call itself until process finishes
if (!obj.processExited) {
setTimeout(function () {
getMessages();
}, 1000)
}
});
}
$(document).ready(function () {
// bind importButton click to run import and then poll for messages
$('#importButton').bind('click', function () {
// Call ProcessImport
$.post("/Upload/ProcessImport", {}, function () { });
// TODO: disable inputs
// Run's getMessages after waiting the specified time
setTimeout(function () {
getMessages();
}, 500)
});
});
</script>
<h2>Upload</h2>
<p style="padding: 20px;">
Description of the upload process and any warnings or important information here.
</p>
<div style="padding: 20px;">
<div id="importButton" class="qq-upload-button">Process files</div>
<div id="processOutput">
<ul id="processOutputList"
style="list-style-type: none; margin: 20px 0px 10px 0px; max-height: 500px; min-height: 500px; overflow: auto;">
</ul>
</div>
</div>
The Controller. I chose not to go with an AsyncController, mainly because I found I didn't need to. My original issue was piping the StdOut of my Console application to the view. I found couldn't ReadToEnd of the standard out, so instead hooked the event handler ProcessOutputDataReceived up which gets fired when standard out data is recieved and then using a StringBuilder, append the output to previously received output. The issue with this approach was that the Controller gets reinstantiated every post and to overcome this I decided to make the Process and the StringBuilder static for the application. This allows me to then receive a call to the Update Action, grab the static StringBuilder and effectivly flush its contents back to my view. I also send back to the view a boolean indicating whether the process has exited or not, so that the view can stop polling when it knows this. Also, being static I tried to ensure that if an import in in progress, don't allow other's to begin.
public class UploadController : Controller
{
private static Process _process;
private static StringBuilder _importStandardOutputBuilder;
public UploadController()
{
if(_importStandardOutputBuilder == null)
_importStandardOutputBuilder = new StringBuilder();
}
public ActionResult Index()
{
ViewData["Title"] = "Upload";
return View("UploadView");
}
//[HttpPost]
public ActionResult ProcessImport()
{
// Validate that process is not running
if (_process != null && !_process.HasExited)
return Json(new { success = false, message = "An Import Process is already in progress. Only one Import can occur at any one time." }, "text/html");
// Get the file path of your Application (exe)
var importApplicationFilePath = ConfigurationManager.AppSettings["ImportApplicationFilePath"];
var info = new ProcessStartInfo
{
FileName = importApplicationFilePath,
RedirectStandardError = true,
RedirectStandardInput = true,
RedirectStandardOutput = true,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
UseShellExecute = false
};
_process = Process.Start(info);
_process.BeginOutputReadLine();
_process.OutputDataReceived += ProcessOutputDataReceived;
_process.WaitForExit(1);
return Json(new { success = true }, JsonRequestBehavior.AllowGet);
}
static void ProcessOutputDataReceived(object sender, DataReceivedEventArgs e)
{
_importStandardOutputBuilder.Append(String.Format("{0}{1}", e.Data, "</br>"));
}
public ActionResult Update()
{
var newOutput = _importStandardOutputBuilder.ToString();
_importStandardOutputBuilder.Clear();
return Json(new { message = newOutput, processExited = _process.HasExited }, "text/html");
}
}
Well, that's it so far. It works. It still needs work, so hopefully I'll update this solution when I perfect mine. What are your thoughts on the static approach (assuming the business rule is that only one import can occur at any one time)?
Look into long poll. Basically you can open an ajax request and then hold onto it inside the controller.
Sample of long poll
This is something that you will want to do Async or you will possibly have issues with thread starvation.
Consider writing a service that runs on a server somewhere and pipes its output to a file/db accessible by your web server. Then you can just load the generated data in your website and returning them to your caller.
Understand that tying up your web server's threads for extended periods of time can result in thread starvation and make it look like your website has crashed (even though it's acutally just busy waiting for your console app to run).

Resources