TestCafe and grabbing an element/selector when no ID or CSS is present - testing

Hello I have a simple page of our application, the start page that has a disclaimer on it and one button. Because of the technology we are using the IDs are stripped out and obfuscated.
So the button's name is 'ok' as it appears in the page-source view below. Note no ID. Even if I put it in our code, when it is produced the ID is removed.
<div style="text-align:center;">
<input type="button" name="ok" value="Ok" width="40" onclick="swap()" />
</div>
I have the following in my TestCafe js file:
import {Selector } from 'testcafe';
import { ClientFunction } from 'testcafe';
fixture `My fixture`
.page `http://192.168.2.86/mpes/login.jsp`;
test('Disclaimer Page 1', async t => {
const okBtn = Selector('button').withAttribute('ok');
await t
.click(okBtn);
});
test('Disclaimer Page 2 -- of course tried this ', async t => {
await t
.click('ok');
});
.click('ok') -- does not work either
Tried numerous things even trying findElementByName .. but nothing works with Window vs Document and Selector vs element.
Any help would be greatly appreciated.
I have been all over looking at how to grab selectors on TestCafe docs. None seem to fit this scenario which is quite simple -- but highly problematic.
Thanks in advance.

If you want to select this element:
<input type="button" name="ok" value="Ok" width="40" onclick="swap()" />
you can use (one or more) attribute selectors like this:
input[type="button"][name="ok"]
If only one element on the page has the attribute name="ok", you can grab that single element in javascript, using document.querySelector(), like this:
const myButton = document.querySelector('input[type="button"][name="ok"]');
N.B. If multiple elements on the page have the attribute name="ok", you can grab all of them in javascript, using document.querySelectorALL().

Related

How to include Handlebars partial in a string? (add it to the innerHTML of a DOM Element)

Is there a way to get the "string version" of a handlebars partial to include it in the innerHTML of an HTML element?
For instance, imagine I have a ToDo list, and I want to add a task everytime I click the button "Add Task", like this:
todo_list.hbs
<div id="todo-list">
</div>
<button onclick="addTask">Add Task</button>
And that I have a handlebars partial in the file "task.hbs":
task.hbs
<h1 class="task-title">The task is: {{title}}</h1>
<button id="delete-task">Delete task</button>
<script>
const button_delete_task = document.getElementById('delete-task');
button_delete_task.addEventListener('click', deleteTask);
function deleteTask () {
// delete task code here
}
</script>
My question is: How could I create a Task partial everytime the button "Add Task" is clicked? Something like this:
<div id="todo-list">
</div>
<button onclick="addTask">Add Task</button>
<script>
function addTask() {
const todo_list = document.getElementById('todo_list');
todo_list.innerHTML += {{> Task title="A new task"}};
// More code here...
}
</script>
I have also tried enclosing the partial with backticks (`{{> Task title="A new task"}}`), and quotes ("{{> Task title='A new task'}}") as well as read many posts on this subject, but all of them use handlebars.js, not express-handlebars.
I am using express.js for the backend, and therefore, express-handlebars as the view engine. In advance, thanks a lot for your help!
I managed to solve the issue!
It turns out that enclosing the partial with backticks works! The problem was that my partial had <script></script> tags.
Imagine my task.hbs looked like this:
<div>
<script></script>
</div>
then, the processed version of todo_list.hbs would look like this:
<div id="todo-list">
</div>
<button onclick="addTask">Add Task</button>
<script>
function addTask() {
const todo_list = document.getElementById('todo_list');
todo_list.innerHTML += `<div>
<script></script>
</div>`;
// More code here...
}
</script>
This would be valid in a normal HTML file, but it looks like handlebars process the closing script tag that is inside the string (</script>) as a normal tag, and with it, closes the <script> tag of todo_list.hbs.
The solution I found was to not use <script> tags into my partial (not a beautiful solution, but works for me!) and instead, declare the javascript code in another file, and import it into todo_list.hbs using <script> tags with the src parameter like this:
todo_list.hbs
<div id="todo-list">
</div>
<button onclick="addTask">Add Task</button>
<script>
function addTask() {
const todo_list = document.getElementById('todo_list');
todo_list.innerHTML += `{{> Task title="New task!"}}`;
// More code here...
}
</script>
<!-- JAVASCRIPT CODE REQUIRED BY TASK PARTIAL -->
<script src="/foo/bar/partials/Task.js"></script>
Where Task.js is the file containing the javascript of the Task.hbs partial:
Task.js
const button_delete_task = document.getElementById('delete-task');
button_delete_task.addEventListener('click', deleteTask);
function deleteTask () {
// delete task code here
}
And with this changes, Task.hbs would look like this:
Task.hbs
<h1 class="task-title">The task is: {{title}}</h1>
<button id="delete-task">Delete task</button>
You are very close to getting this to work.
As you have noted, your Handlebars is executing on the server-side. In the case of your partial, you are trying to have it render within a script block. In order for the result to be valid JavaScript, you would need have quotes around the output of the partial so that it will be a valid JavaScript string. Therefore:
todo_list.innerHTML += "{{>Task title='A new task'}}";
Which, when rendered, would result in:
todo_list.innerHTML += "<h1>The task is: A new task</h1>";
It should be noted that quotes in your partial could be problematic. For example, if the <h1> in your partial had a class <h1 class="task">, the resultant JavaScript would now be invalid because the quote after the = would be interpreted as the closing quote of the JavaScript string. Therefore, you would need to be sure to either escape the quotes in your partial or ensure they are different from those used to wrap your partial call (a single-quote ('), in this case.
todo_list.innerHTML += "<h1 class=\"task\">The task is: A new task</h1>";
Additionally, you have an inconsistency with the id of your <div>. The tag has id="todo-list" (with a dash); but your JavaScript has document.getElementById('todo_list') (with an underscore). Those will need to be consistent.
Update
As #Sharif Velásquez Alzate noted in comments, the quotes will not work when the partial contains line-breaks because JavaScript strings cannot span multiple lines (unless each line ends with a \ to signify that the text continues to the next line. However, a template literal, using back-ticks, will support text with line-breaks.
Therefore, a better solution is:
todo_list.innerHTML += `{{>Task title='A new task'}}`;

Slow performance with Testcafe and ids and custom radio buttons

I have a test where testcafe says "Waiting for element to appear" and I dont understand why.
I use a ID selector
My test is fairly simple and I made a slow version using a ID and a fast one using a non ID
Is this a bug in testcafe?
Using Testcafe v1.11.0 on Chrome 88.0.4324.182 / Windows 10
Test output says:
Book
√ slow (17s)
√ fast (3.69s)
Why is selecting using ID so much slower? Shouldn't it be faster?
This is my test:
import { Selector } from 'testcafe';
fixture('Book').page('https://book.dinnerbooking.com/dk/en-US/book/table/pax/458/2');
test('slow', async t => {
'use strict';
await t
.maximizeWindow()
.click('button.ui-spinner-up')
.click('button.black-button')
.click('#RestaurantAreaConfigAreaId6');
});
test('fast', async t => {
'use strict';
await t
.maximizeWindow()
.click('button.ui-spinner-up')
.click('button.black-button')
.click(new Selector('.restaurant-area-label').nth(1));
});
These two different selectors select two different elements. '#RestaurantAreaConfigAreaId6' selects the <input type="radio" id="RestaurantAreaConfigAreaId6" ... /> element and Selector('.restaurant-area-label').nth(1) selects the <label for="RestaurantAreaConfigAreaId6" class="restaurant-area-label font-large">Café</label> element.
The input element, in turn, is overlapped by another transparent element. In such cases, TestCafe waits until the target element becomes uncovered within the timeout. If the target element stays still, TestCafe clicks on the element above.

With TestCafe Selector, How to verify the text of the selected item in a <select>?

I'm using TestCafe 1.8.1 and have a slightly different case than the documentation at https://devexpress.github.io/testcafe/documentation/recipes/test-select-elements.html - my problem is that the example assumes the value of an <option> and its text content will be the same, and in my case, the value is a very unpredictable value.
I can select an item in the dropdown without trouble, using .withText(value) to filter the options, and .click(item) to select it. However, my app then refreshes the page, and ought to re-select the relevant item as it loads up. This is not working and I want to test for it.
So I might have options in the select like:
<select id="foo">
<option value="1234">100x100</option>
<option value="5432">200x100</option>
<option value="9999">100x200</option>
</select>
Obviously, if I test with .expect(citySelect.value).eql('London'); as in the docs it'll fail because the values are nothing like the text content e.g. having clicked '200x100' in the dropdown the value becomes "5432".
Do I need to use a ClientFunction to get the text of the selected item? I understand it's quite awkward passing data into a ClientFunction, would I need to pass the id of the select so the ClientFunction can getElementById to find the select and retrieve it's selected option's text content? It all sounds like the wrong way to be doing things.
Please check the following example that uses ClientFunction API to obtain an option value:
import { Selector, ClientFunction } from 'testcafe';
fixture `Fixture 1`
.page `https://kys0l.csb.app/`;
test('Test 1', async t => {
const selector = Selector('select');
const getValue = ClientFunction((index) => {
const select = selector();
return select.options[index].value;
}, { dependencies: { selector } });
await t
.expect(getValue(0)).eql('1234')
.expect(getValue(1)).eql('5432')
.expect(getValue(2)).eql('9999');
});
See also: Obtain Client-Side Info.
Try using
.expect(citySelect.innertext).eql('London');

Event handling after HTML injection with Vue.js

Vue is not registering event handler for HTML injected objects. How do I do this manually or what is a better way to work around my problem?
Specifically, I send a query to my server to find a token in text and return the context (surrounding text) of that token as it exists in unstructured natural language. The server also goes through the context and finds a list of those words that also happen to be in my token set.
When I render to my page I want all of these found tokens in the list to be clickable so that I can send the text of that token as a new search query. The big problem I am having is my issue does not conform to a template. The clickable text varies in number and positioning.
An example of what I am talking about is that my return may look like:
{
"context": "When in the Course of human events, it becomes necessary for one people to dissolve the political bands which have connected",
"chunks": ['human events', 'one people', 'political bands']
}
And the resulting output I am looking for is the sentence looks something like this in psuedocode:
When in the Course of <a #click='search("human events")'>human events</a>, it becomes necessary for <a #click='search("one people")'>one people</a> to dissolve the <a #click='search("political bands")'>political bands</a> which have connected
This is what I have tried so far though the click handler is not registered and the function never gets called:
<v-flex xs10 v-html="addlink(context.context, context.chunks)"></v-flex>
and in my methods section:
addlink: function(words, matchterms){
for(var index in matchterms){
var regquery = matchterms[index].replace(this.regEscape, '\\$&');
var query = matchterms[index];
var regEx = new RegExp(regquery, "ig");
words = words.replace(regEx, '<a href=\'#\' v-on:click.prevent=\'doSearch("'+ query +'")\'>' + query + '</a>');
}
return words;
}
As I said, this does not work and I know why. This is just showing that because of the nature of the problem is seems like regex is the correct solution but that gets me into a v-html injection situation. Is there something I can do in Vue to register the event handlers or can some one tell me a better way to load this data so I keep my links inline with the sentence and make them functional as well?
I've already posted one answer but I've just realised that there's a totally different approach that might work depending on your circumstances.
You could use event delegation. So rather than putting click listeners on each <a> you could put a single listener on the wrapper element. Within the listener you could then check whether the clicked element was an <a> (using event.target) and act accordingly.
Here's one way you could approach it:
<template>
<div>
<template v-for="segment in textSegments">
<a v-if="segment.link" href="#" #click.prevent="search(segment.text)">
{{ segment.text }}
</a>
<template v-else>
{{ segment.text }}
</template>
</template>
</div>
</template>
<script>
export default {
data () {
return {
"context": "When in the Course of human events, it becomes necessary for one people to dissolve the political bands which have connected",
"chunks": ['human events', 'one people', 'political bands']
}
},
computed: {
textSegments () {
const chunks = this.chunks
// This needs escaping correctly
const re = new RegExp('(' + chunks.join('|') + ')', 'gi')
// The filter removes empty strings
const segments = this.context.split(re).filter(text => text)
return segments.map(segment => {
return {
link: segment.match(re),
text: segment
}
})
}
},
methods: {
search (chunk) {
console.log(chunk)
}
}
}
</script>
I've parsed the context text into an array of segments that can then be handled cleanly using Vue's template syntax.
I've used a single RegExp and split, which will not discard matches if you wrap them in a capture group, (...).
Going back to your original example, v-html only supports native HTML, not Vue template syntax. So you can add events using onclick attributes but not #click or v-on:click. However, using onclick wouldn't provide easy access to your search method, which is scoped to your component.

Vue v-model input change mobile chrome not work

If i open https://v2.vuejs.org/v2/guide/forms.html#Text and edit text - no effect on typing text in mobile chrome. #keyup #input #keypress - v-model does not change when I'm typing
<input v-model="message" #keyup="log" placeholder="Edit">
<p>Edited: {{ message }}</p>
How can i fix it? I need get input value on typing (#keyup #input)
Update: After a lot of discussion, I've come to understand that this is a feature, not a bug. v-model is more complicated than you might at first think, and a mobile 'keyboard' is more complicated than a keyboard. This behaviour can surprise, but it's not wrong. Code your #input separately if you want something else.
Houston we might have a problem. Vue does not seem to be doing what it says on the tin. V-model is supposed to update on input, but if we decompose the v-model and code the #input explicitly, it works fine on mobile. (both inputs behave normally in chrome desktop)
For display on mobiles, the issue can be seen at...
https://jsbin.com/juzakis/1
See this github issue.
function doIt(){
var vm = new Vue({
el : '#vueRoot',
data : {message : '',message1 : ''}
})
}
doIt();
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.16/dist/vue.js"></script>
<div id='vueRoot'>
<h1>v-model</h1>
<div>
<input type='text'
v-model='message'
>
{{message}}
</div>
<h1>Decomposed</h1>
<div>
<input type='text'
:value='message1'
#input='evt=>message1=evt.target.value'
>
{{message1}}
</div>
</div>
I tried all solutions I could find on the internet, nothing worked for me. in the end i came up with this, finally works on android!
Trick is to use compositionupdate event:
<input type="text" ... v-model="myinputbox" #compositionupdate="compositionUpdate($event)">
......
......
methods: {
compositionUpdate: function(event)
{
this.myinputbox = event.data;
},
}
Ok, I dont know if there is another solution for this issue, but it can be solved with a simple directive:
Vue.directive('$model', {
bind: function (el, binding, vnode) {
el.oninput = () => (vnode.context[binding.expression] = el.value)
}
})
using it just like
<input v-$model="{toBind}">
There is an issue on the oficial repo, and they say this is the normal behavior (because the composition mode), but I still need the functionality
EDIT: A simpler solution for me was to just use #input.native. Also, the this event has (now?) a isComposing attribute which we can use to either take $event.data into account, or $event.target.value
In my case, the only scheme that worked was handling #keydown to save the value before the user action, and handling #keyup to process the event if the value had changed. NOTE: the disadvantage of this is that any non-keyboard input (like copy/paste with a mouse) will not work.
<md-input
v-else
:value="myValue"
ref="input"
#keydown="keyDownValue = $event.target.value"
#keyup="handleKeyUp($event)"
#blur="handleBlur()"
/>
With handleKeyUp in my case being:
handleKeyUp(evt){
if(evt.target.value !== this.keyDownValue){
this.$emit('edited', evt);
}
}
My use case was the following:
I requested a search endpoint in the backend to get suggestions as the user typed. Solutions like handling #compositionupdate lead to sending several several requests to the backend (I also needed #input for non-mobile devices). I reduced the number of requests sent by correctly handling #compositionStarted, but there was still cases where 2 requests were sent for just 1 character typed (when composition was left then, e.g. with space character, then re-entered, e.g. with backspace character).