Select input by label text in Puppeteer/Playwright - puppeteer

Given the following markup:
<label for="name">Name</label>
<input type="text" id="name" />
I want to fill the input value in a Playwright test. However, I prefer to follow a paradigm similar to Testing Library, which is using selectors like a real user would (usually visible text content). Therefore, I would like my Playwright selector to use the label text.
When I attempt the following I get an error element is not an <input> because it selects the actual label element:
await page.fill('"Name"')
Is there another way to achieve this?
Note: I'm not keen on using the Playwright Testing Library package because it simply isn't mature enough. Also, my understanding is that Puppeteer and Playwright are virtually the same, hence why this question applies to both frameworks.

You could .click() the label to set focus on the input, then use page.keyboard.type to type into the input.
Here's a minimal example:
const puppeteer = require("puppeteer");
let browser;
(async () => {
browser = await puppeteer.launch({headless: true});
const [page] = await browser.pages();
await page.setContent(`
<label for="name">Name</label>
<input type="text" id="name" />
`);
await page.click("label[for='name']");
await page.keyboard.type("hello world");
console.log(await page.$eval("#name", e => e.value)); // => hello world
console.log(await page.evaluate(() => document.activeElement.value)); // same
})()
.catch(err => console.error(err))
.finally(() => browser?.close())
;

When an input field and a label are correctly connected.
For example like this (using the for attribute:
<label for="name">Name</label>
<input type="text" id="name" name="name" />
Then it's really easy to fill the input field using the label.
It works like this for example:
await page.fill('label:has-text("name")', 'Peter');
The feature to fill an input via a selected label was added to Playwright in this pull request: https://github.com/microsoft/playwright/issues/3466

To expand on #ggorlen answer, and to make it even more similar to Testing Library, you can use the :has-text() pseudo-class to select the label by its accessible name:
await page.click('label:has-text("Name")');
await page.keyboard.type('hello world');

Playwright has an exact locator for this page.getByLabel
Allows locating input elements by the text of the associated label. For example, this method will find the input by label text Password in the following DOM:
<label for="password-input">Password:</label>
<input id="password-input">
#paramtext — Text to locate the element for.

Related

How to execute HTML code after user submit

I want the user to load Movie.swf size of their choice. I have two input boxes, one for width and one for the height. How can I execute the object with the size the user submitted?
I can't figure out how to make this work.
//Let the user set the size for Movie.swf
<label for="fname">Width</label><br>
<input type="text" id="objW"><br><br>
<label for="fname">Height</label><br>
<input type="text" id="objH"><br><br>
//Movie.swf will load with a new size from the input box
<input type="submit" value="Execute"><br>
//Execute this line when submit
<object width="" height="" data="/Movie.swf"></object>
You need to add javascript code for this part unless you are already using a framework. Also you should wrap the inputs inside a form tag and add an on submit eventlistener.
In current situation, following code should work:
const objW = document.getElementById('objW');
const objH = document.getElementById('objH');
const submitInput = document.querySelector('input[type=submit]');
const body = document.getElementsByTagName('body')[0];
submitInput.addEventListener('click', e => {
e.preventDefault();
/*if you want to display the object element after clicking input submit and replace all input fields*/
body.innerHTML = `<object width="${objW.value}" height="${objH.value}" data="/Movie.swf"></object>`;
/*or you can use following code if you always have object element in your html*/
const object = document.getElementsByTagName('object')[0];
object.setAttribute("height", objH.value);
object.setAttribute("width", objW.value);
});

HTML input must contain a specific string

I have a input field, which will record the response of the end-user. Say the label of it is Linkedin URL. The user who has to enter the linkedin URL could enter some other input(facebook.com/satyaram2k14), which is not invalid. So how can one check, if the url entered contains string 'linkedin.com' along with other text. Final url should be 'linkedin.com/satyaram2k14'. How can I check for this pattern at front-end.
There are several approaches you can take:
Rely on the HTML5 form validation via required and pattern attributes
Validate the input value via JavaScript on form validation
Optionally provide a visual hint on the (in)valid state of the field content
HTML5 validation works in any modern browser and integrates into the browser UI (including localisation of error messages). But it's not as flexible and the helpfulness of the error message given when the pattern does not match varies from browser to browser.
In any case, you should always validate user input on the server side as well, because any client validation can be circumvented.
Here are some code examples of all three approaches. They all use the RegEx pattern ^https?://(www\.)?linkedin\.com, so they'll allow http or https protocol and urls with or without "www.".
var form = document.getElementById('form');
var field = document.getElementById('url');
var fieldstatus = document.getElementById('fieldstatus');
var regExPattern = new RegExp('^https?://(www\.)?linkedin\.com', 'i');
// validation on form submit
form.addEventListener('submit', function(ev) {
if (!regExPattern.test(field.value)) {
ev.preventDefault();
alert('The input value does not match the pattern!');
}
});
// validation on input
field.addEventListener('input', function() {
if (!regExPattern.test(field.value)) {
fieldstatus.innerHTML = 'invalid';
} else {
fieldstatus.innerHTML = 'valid';
}
});
<h1>HTML5 validation pattern</h1>
<form>
<input type="text" pattern="^https?://(www\.)?linkedin\.com" required /><br>
<input type="submit" />
</form>
<h1>Custom JavaScript validation</h1>
<form id="form">
<input type="text" id="url" /> <span id="fieldstatus">invalid</span><br>
<input type="submit" />
</form>
One quick way of doing this is by using the pattern attribute of input, like so:
<input type='text' pattern='^linkedin\.com/'>
^linkedin\.com/ being a regular expression that matches all strings that start with linkedin.com/.
Using this attribute, the browser by itself will only accept such strings.

How do I reset a form including removing all validation errors?

I have an Angular form. The fields are validated using the ng-pattern attribute. I also have a reset button. I'm using the Ui.Utils Event Binder to handle the reset event like so:
<form name="searchForm" id="searchForm" ui-event="{reset: 'reset(searchForm)'}" ng-submit="search()">
<div>
<label>
Area Code
<input type="tel" name="areaCode" ng-model="areaCode" ng-pattern="/^([0-9]{3})?$/">
</label>
<div ng-messages="searchForm.areaCode.$error">
<div class="error" ng-message="pattern">The area code must be three digits</div>
</div>
</div>
<div>
<label>
Phone Number
<input type="tel" name="phoneNumber" ng-model="phoneNumber" ng-pattern="/^([0-9]{7})?$/">
</label>
<div ng-messages="searchForm.phoneNumber.$error">
<div class="error" ng-message="pattern">The phone number must be seven digits</div>
</div>
</div>
<br>
<div>
<button type="reset">Reset</button>
<button type="submit" ng-disabled="searchForm.$invalid">Search</button>
</div>
</form>
As you can see, when the form is reset it calls the reset method on the $scope. Here's what the entire controller looks like:
angular.module('app').controller('mainController', function($scope) {
$scope.resetCount = 0;
$scope.reset = function(form) {
form.$setPristine();
form.$setUntouched();
$scope.resetCount++;
};
$scope.search = function() {
alert('Searching');
};
});
I'm calling form.$setPristine() and form.$setUntouched, following the advice from another question here on Stack Overflow. The only reason I added the counter was to prove that the code is being called (which it is).
The problem is that even after reseting the form, the validation messages don't go away. You can see the full code on Plunker. Here's a screenshot showing that the errors don't go away:
I started with the comment from #Brett and built upon it. I actually have multiple forms and each form has many fields (more than just the two shown). So I wanted a general solution.
I noticed that the Angular form object has a property for each control (input, select, textarea, etc) as well as some other Angular properties. Each of the Angular properties, though, begins with a dollar sign ($). So I ended up doing this (including the comment for the benefit of other programmers):
$scope.reset = function(form) {
// Each control (input, select, textarea, etc) gets added as a property of the form.
// The form has other built-in properties as well. However it's easy to filter those out,
// because the Angular team has chosen to prefix each one with a dollar sign.
// So, we just avoid those properties that begin with a dollar sign.
let controlNames = Object.keys(form).filter(key => key.indexOf('$') !== 0);
// Set each control back to undefined. This is the only way to clear validation messages.
// Calling `form.$setPristine()` won't do it (even though you wish it would).
for (let name of controlNames) {
let control = form[name];
control.$setViewValue(undefined);
}
form.$setPristine();
form.$setUntouched();
};
$scope.search = {areaCode: xxxx, phoneNumber: yyyy}
Structure all models in your form in one place like above, so you can clear it like this:
$scope.search = angular.copy({});
After that you can just call this for reset the validation:
$scope.search_form.$setPristine();
$scope.search_form.$setUntouched();
$scope.search_form.$rollbackViewValue();
There doesn't seem to be an easy way to reset the $errors in angular. The best way would probably be to reload the current page to start with a new form. Alternatively you have to remove all $error manually with this script:
form.$setPristine(true);
form.$setUntouched(true);
// iterate over all from properties
angular.forEach(form, function(ctrl, name) {
// ignore angular fields and functions
if (name.indexOf('$') != 0) {
// iterate over all $errors for each field
angular.forEach(ctrl.$error, function(value, name) {
// reset validity
ctrl.$setValidity(name, null);
});
}
});
$scope.resetCount++;
You can add a validation flag and show or hide errors according to its value with ng-if or ng-show in your HTML. The form has a $valid flag you can send to your controller.
ng-if will remove or recreate the element to the DOM, while ng-show will add it but won't show it (depending on the flag value).
EDIT: As pointed by Michael, if form is disabled, the way I pointed won't work because the form is never submitted. Updated the code accordingly.
HTML
<form name="searchForm" id="searchForm" ui-event="{reset: 'reset(searchForm)'}" ng-submit="search()">
<div>
<label>
Area Code
<input type="tel" name="areaCode" ng-model="areaCode" ng-pattern="/^([0-9]{3})?$/">
</label>
<div ng-messages="searchForm.areaCode.$error">
<div class="error" ng-message="pattern" ng-if="searchForm.areaCode.$dirty">The area code must be three digits</div>
</div>
</div>
<div>
<label>
Phone Number
<input type="tel" name="phoneNumber" ng-model="phoneNumber" ng-pattern="/^([0-9]{7})?$/">
</label>
<div ng-messages="searchForm.phoneNumber.$error">
<div class="error" ng-message="pattern" ng-if="searchForm.phoneNumber.$dirty">The phone number must be seven digits</div>
</div>
</div>
<br>
<div>
<button type="reset">Reset</button>
<button type="submit" ng-disabled="searchForm.$invalid">Search</button>
</div>
</form>
JS
$scope.search = function() {
alert('Searching');
};
$scope.reset = function(form) {
form.$setPristine();
form.$setUntouched();
$scope.resetCount++;
};
Codepen with working solution: http://codepen.io/anon/pen/zGPZoB
It looks like I got to do the right behavior at reset. Unfortunately, using the standard reset failed. I also do not include the library ui-event. So my code is a little different from yours, but it does what you need.
<form name="searchForm" id="searchForm" ng-submit="search()">
pristine = {{searchForm.$pristine}} valid ={{searchForm.$valid}}
<div>
<label>
Area Code
<input type="tel" required name="areaCode" ng-model="obj.areaCode" ng-pattern="/^([0-9]{3})?$/" ng-model-options="{ allowInvalid: true }">
</label>
<div ng-messages="searchForm.areaCode.$error">
<div class="error" ng-message="pattern">The area code must be three digits</div>
<div class="error" ng-message="required">The area code is required</div>
</div>
</div>
<div>
<label>
Phone Number
<input type="tel" required name="phoneNumber" ng-model="obj.phoneNumber" ng-pattern="/^([0-9]{7})?$/" ng-model-options="{ allowInvalid: true }">
</label>
<div ng-messages="searchForm.phoneNumber.$error">
<div class="error" ng-message="pattern">The phone number must be seven digits</div>
<div class="error" ng-message="required">The phone number is required</div>
</div>
</div>
<br>
<div>
<button ng-click="reset(searchForm)" type="reset">Reset</button>
<button type="submit" ng-disabled="searchForm.$invalid">Search</button>
</div>
</form>
And JS:
$scope.resetCount = 0;
$scope.obj = {};
$scope.reset = function(form_) {
$scope.resetCount++;
$scope.obj = {};
form_.$setPristine();
form_.$setUntouched();
console.log($scope.resetCount);
};
$scope.search = function() {
alert('Searching');
};
Live example on jsfiddle.
Note the directive ng-model-options="{allowinvalid: true}". Use it necessarily, or until the entry field will not be valid, the model value is not recorded. Therefore, the reset will not operate.
P.S. Put value (areaCode, phoneNumber) on the object simplifies purification.
Following worked for me
let form = this.$scope.myForm;
let controlNames = Object.keys(form).filter(key => key.indexOf('$') !== 0);
for (let name of controlNames) {
let control = form [name];
control.$error = {};
}
In Short: to get rid of ng-messages errors you need to clear out the $error object for each form item.
further to #battmanz 's answer, but without using any ES6 syntax to support older browsers.
$scope.resetForm = function (form) {
try {
var controlNames = Object.keys(form).filter(function (key) { return key.indexOf('$') !== 0 });
console.log(controlNames);
for (var x = 0; x < controlNames.length; x++) {
form[controlNames[x]].$setViewValue(undefined);
}
form.$setPristine();
form.$setUntouched();
} catch (e) {
console.log('Error in Reset');
console.log(e);
}
};
I had the same problem and tried to do battmanz solution (accepted answer).
I'm pretty sure his answer is really good, but however for me it wasn't working.
I am using ng-model to bind data, and angular material library for the inputs and ng-message directives for error message , so maybe what I will say will be useful only for people using the same configuration.
I took a lot of look at the formController object in javascript, in fact there is a lot of $ angular function as battmanz noted, and there is in addition, your fields names, which are object with some functions in its fields.
So what is clearing your form ?
Usually I see a form as a json object, and all the fields are binded to a key of this json object.
//lets call here this json vm.form
vm.form = {};
//you should have something as ng-model = "vm.form.name" in your view
So at first to clear the form I just did callback of submiting form :
vm.form = {};
And as explained in this question, ng-messages won't disappear with that, that's really bad.
When I used battmanz solution as he wrote it, the messages didn't appear anymore, but the fields were not empty anymore after submiting, even if I wrote
vm.form = {};
And I found out it was normal, because using his solution actually remove the model binding from the form, because it sets all the fields to undefined.
So the text was still in the view because somehow there wan't any binding anymore and it decided to stay in the HTML.
So what did I do ?
Actually I just clear the field (setting the binding to {}), and used just
form.$setPristine();
form.$setUntouched();
Actually it seems logical, since the binding is still here, the values in the form are now empty, and angular ng-messages directive is triggering only if the form is not untouched, so I think it's normal after all.
Final (very simple) code is that :
function reset(form) {
form.$setPristine();
form.$setUntouched();
};
A big problem I encountered with that :
Only once, the callback seems to have fucked up somewhere, and somehow the fields weren't empty (it was like I didn't click on the submit button).
When I clicked again, the date sent was empty. That even more weird because my submit button is supposed to be disabled when a required field is not filled with the good pattern, and empty is certainly not a good one.
I don't know if my way of doing is the best or even correct, if you have any critic/suggestion or any though about the problem I encountered, please let me know, I always love to step up in angularJS.
Hope this will help someone and sorry for the bad english.
You can pass your loginForm object into the function ng-click="userCtrl.login(loginForm)
and in the function call
this.login = function (loginForm){
loginForm.$setPristine();
loginForm.$setUntouched();
}
So none of the answers were completely working for me. Esp, clearing the view value, so I combined all the answers clearing view value, clearing errors and clearing the selection with j query(provided the fields are input and name same as model name)
var modelNames = Object.keys($scope.form).filter(key => key.indexOf('$') !== 0);
modelNames.forEach(function(name){
var model = $scope.form[name];
model.$setViewValue(undefined);
jq('input[name='+name+']').val('');
angular.forEach(model.$error, function(value, name) {
// reset validity
model.$setValidity(name, null);
});
});
$scope.form.$setPristine();
$scope.form.$setUntouched();

How can I turn this line of HTML into an HTML helper?

<input type="text" name="search" id="search" style="border-style:ridge;" />
Thats the line of Html I'd like to have it as a html helper. I tried:
#Html.TextBox(" ", "",new { id="search", name="search", style="border-style:ridge;"})
but it won't post back for that text box when i press enter. It works fine for the input tag.
That would just be a regular box:
#Html.TextBox("search", null, new { style = "border-style: ridge;" })
Or assuming your model has a search property, it would be:
#Html.TextBoxFor(x => x.search, new { style = "border-style: ridge;" })
Both of these produce the same HTML. Unless there's any funny stuff going on, both the id and name of the textbox will be search.

Ember set input value to JSON

I'd like to set the value attribute of an input the some JSON:
Ember.TextField.extend({
valueBinding: Ember.Binding.transform(function(val){
return JSON.stringify(val);
})
});
If val="test" (a string) The element in the DOM is rendered like this:
<input id="ember881" class="ember-view ember-text-field" type="text" value="" test""="">
Is there a way to return some "SafeEscaped" version?
Regards
To answer your question, yes. The following code should do the trick, though I'm sure there is a prettier solution somewhere:
return new Handlebars.SafeString(Handlebars.Utils.escapeExpression(JSON.stringify(val)).toString()).toString();
However (though I'm not sure what you're trying to achieve with that code), I'd rewrite the view to read:
App.test = Ember.TextField.extend({
valueBinding: 'test',
val: {"cat":"meow"},
test: function(){
return new Handlebars.SafeString(Handlebars.Utils.escapeExpression(JSON.stringify(this.val)).toString()).toString();
}.property('val')
});
This should output:
<input type="text" value="{"test":"meow"}" class="ember-view ember-text-field" id="ember239">
Note that I'm assuming you're using Handlebars.