Trouble to find element depending from other element in testcafe page object model - testing

Having problems implementing the locator lookup method depending on its parent in POM
Example of DOM (roughly):
<div class="lessons">
<div [data-test="lesson"]>
<div class="lesson__info">
<div ...>
<h2 [data-test="lessonTitle"]>FirstLesson</h2>
<div class"lesson__data">
<div [data-test="lessonDataButton"]>
<div class"lesson__controls">
<div [data-test="lessonStartButton"]>
<div [data-test="lesson"]>
<div class="lesson__info">
<div ...>
<h2 [data-test="lessonTitle"]>SecondLesson</h2>
<div class"lesson__data">
<div [data-test="lessonDataButton"]>
<div class"lesson__controls">
<div [data-test="lessonStartButton"]>
Example of my POM:
import { Selector, t } from 'testcafe'
class Page {
constructor() {
this.lesson = Selector('[data-test="lesson"]')
this.lessonDataBtn = Selector('[data-test="lessonDataButton"]')
this.lessonStartBtn = Selector('[data-test="lessonStartButton"]')
this.lessonTitle = Selector('[data-test="lessonTitle"]')
}
async getLessonButton(title, lessonButton) {
const titleLocator = this.lessonTitle.withText(title);
const currentLesson = this.lesson.filter((node) => {
return node.contains(titleLocator())
}, { titleLocator });
const buttonSelector = currentLesson.find((node) => {
return node === lessonButton();
}, { lessonButton });
return buttonSelector;
}
In my test I'm trying to click "lessonDataButton" in specific lesson filtered by its "title":
await t.click(await schedule.getLessonButton(testData.lesson.data.title, page.lessonDataBtn))
It works correctly only for first occurrence of "lessonDataBtn" on page, but if I try to find the same button in second lesson - it will be an error:
The specified selector does not match any element in the DOM tree.
> | Selector('[data-test="lesson"]')
| .filter([function])
| .find([function])

I created an example using the code samples you provided and got a different error:
1. The specified selector does not match any element in the DOM tree.
| Selector('[data-test="lesson"]')
| .filter([function])
> | .find([function])
But I believe the case is the same: the lessonButton() call in the filter function of the find method of the currentLesson selector will always return the first node of the set. A straightforward solution is to search for the button directly with the css selector: const buttonSelector = currentLesson.find('[data-test="lessonDataButton"]');. You also can get rid of filter functions completely:
getLessonButton (title) {
return this.lessonTitle.withText(title)
.parent('[data-test="lesson"]')
.find('[data-test="lessonDataButton"]');
}

Related

Vue class components dynamically add component depending on answer from backend

So from the backend I get a array of objects that look kind of like this
ItemsToAdd
{
Page: MemberPage
Feature: Search
Text: "Something to explain said feature"
}
So i match these values to enums in the frontend and then on for example the memberpage i do this check
private get itemsForPageFeatures(): ItemsToAdd[] {
return this.items.filter(
(f) =>
f.page== Pages.MemberPage &&
f.feature != null
);
}
What we get from the backend will change a lot over time and is only the same for weeks at most. So I would like to avoid to have to add the components in the template as it will become dead code fast and will become a huge thing to have to just go around and delete dead code. So preferably i would like to add it using a function and then for example for the search feature i would have a ref on the parent like
<SearchBox :ref="Features.Search" />
and in code just add elements where the ItemsToAdd objects Feature property match the ref
is this possible in Vue? things like appendChild and so on doesn't work in Vue but that is the closest thing i can think of to kind of what I want. This function would basically just loop through the itemsForPageFeatures and add the features belonging to the page it is run on.
For another example how the template looks
<template>
<div class="container-fluid mt-3">
<div
class="d-flex flex-row justify-content-between flex-wrap align-items-center"
>
<div class="d-align-self-end">
<SearchBox :ref="Features.Search" />
</div>
</div>
<MessagesFilter
:ref="Features.MessagesFilter"
/>
<DataChart
:ref="Features.DataChart"
/>
So say we got an answer from backend where it contains an object that has a feature property DataChart and another one with Search so now i would want components to be added under the DataChart component and the SearchBox component but not the messagesFilter one as we didnt get that from the backend. But then next week we change in backend so we no longer want to display the Search feature component under searchbox. so we only get the object with DataChart so then it should only render the DataChart one. So the solution would have to work without having to make changes to the frontend everytime we change what we want to display as the backend will only be database configs that dont require releases.
Closest i can come up with is this function that does not work for Vue as appendChild doesnt work there but to help with kind of what i imagine. So the component to be generated is known and will always be the same type of component. It is where it is to be placed that is the dynamic part.
private showTextBoxes() {
this.itemsForPageFeatures.forEach((element) => {
let el = this.$createElement(NewMinorFeatureTextBox, {
props: {
item: element,
},
});
var ref = `${element.feature}`
this.$refs.ref.appendChild(el);
});
}
You can use dynamic components for it. use it like this:
<component v-for="item in itemsForPageFeatures" :is="getComponent(item.Feature)" :key="item.Feature"/>
also inside your script:
export default {
data() {
return {
items: [
{
Page: "MemberPage",
Feature: "Search",
Text: "Something to explain said feature"
}
]
};
},
computed: {
itemsForPageFeatures() {
return this.items.filter(
f =>
f.Page === "MemberPage" &&
f.Feature != null
);
}
},
methods: {
getComponent(feature) {
switch (feature) {
case "Search":
return "search-box";
default:
return "";
}
}
}
};

Not able to set and access a dynamic variable in a JSRender loop

I have a loop where I only want to display the header when a project name changes.
So I would like to set a variable previousProject and compare it in an IF statement.
I have tried as follows, setting it:
{{* window.previousProject=:project}}
And as follows, reading it:
{{* if window.previousProject==:project}}
But I can't get it to work.
Update:
What I am trying to accomplish is this:
HEADER 1 (This only prints on the first occurrence of HEADER 1)
ITEM 1.1
ITEM 1.2
ITEM 1.3
...
HEADER 2 (This only prints on the first occurrence of HEADER 2)
ITEM 2.1
ITEM 2.2
ITEM 2.3
...
So HEADER is in fact data.project.
The first time, when header is undefined I need to print an opening div + header
Every time a new header is detected I need to print a closing div and an opening div + header
The last time (last row in the iteration) I need to print a closing div
I'm not sure if I understand correctly your intended behavior (for example is project a variable data.project on the data you are passing to render(data),
or is it a JavaScript var defined globally outside the template. But at any rate you have some syntax errors above, including :project.
Here is a working example that you can try out, which may help you.
<script id="myTemplate" type="text/x-jsrender">
{{* window.previousProject=data.project;}}
{{* if (window.previousProject==data.project) { }}
A {{*:data.project}} {{:project}}
{{* } else { }}
B {{*:window.previousProject}}
{{* } }}
{{* window.previousProject="Other project"; }}
{{* if (window.previousProject==data.project) { }}
A {{*:data.project}} {{:project}}
{{* } else { }}
B {{*:window.previousProject}}
{{* } }}
</script>
<div id="result"></div>
<script>
var data = {project: "My project"};
$.views.settings.allowCode(true);
var html = $("#myTemplate").render(data);
$("#result").html(html);
</script>
UPDATE
That said, I'm not sure you actually need to use allowCode(true). Here are a couple of alternatives, based on your additional explanation of the scenario:
If you have this data
data = {projects: [
{project: "Header1", item: "Item 1.1"},
{project: "Header1", item: "Item 1.2"},
{project: "Header1", item: "Item 1.3"},
{project: "Header2", item: "Item 2.1"},
{project: "Header2", item: "Item 2.2"},
{project: "Header2", item: "Item 2.3"},
{project: "Header3", item: "Item 3.1"},
{project: "Header3", item: "Item 3.2"},
{project: "Header3", item: "Item 3.3"}
]};
you can use your approach of having state which is mutated during template rendering, but without exposing full javascript with allowCode(true). Instead pass in a ~previousProject() helper with a get/set pattern:
var _prevProject = ""; //initial state
var html = $("#myTemplate").render(data, {
prevProject: function(val) {
if (val===undefined) {
return _prevProject;
} else {
_prevProject=val;
return "";
}
}
});
with the following template:
<script id="myTemplate" type="text/x-jsrender">
{{for projects}}
{{if !~prevProject()}}
<div>{{:project}} {{:item}}
{{else ~prevProject()===project}}
{{:item}}
{{else}}
</div><div>{{:project}} {{:item}}
{{/if}}
{{:~prevProject(project)}}
{{/for}}
</div>
</script>
But in fact you don't need to set state dynamically at all, but instead can access the array items directly to test for project, as in the following template. (No helper function needed for this):
<script id="myTemplate" type="text/x-jsrender">
{{for projects ~projects=projects}}
{{if #getIndex()===0}}
<div>{{:project}} {{:item}}
{{else}}
{{if ~projects[#getIndex()-1].project===project}}
{{:item}}
{{else}}
</div><div>{{:project}} {{:item}}
{{/if}}
{{/if}}
{{/for}}
</div>
</script>
It's better to use the JsRender standard tags, when possible, rather than setting allowCode to true and inserting javascript code into the template...
ADDED:
The above solutions work fine, but the template is not very easy to maintain or understand, and it doesn't follow or reveal the natural hierarchical structure of the output. So yet another alternative is to use the filter property: {{for filter=...}}, as in the following:
Helpers
var html = $("#myTemplate").render(data, {
header: function(item, index, items) {
if (index===0 || item.project!==items[index-1].project) {
_prevProject=item.project;
return true;
}
},
items: function(item, index, items) {
return item.project===_prevProject;
}
});
Template:
<script id="myTemplate" type="text/x-jsrender">
{{for projects filter=~header ~projects=projects}}
<div>
{{:project}}
{{for ~projects filter=~items}}
{{:item}}
{{/for}}
</div>
{{/for}}
</script>

In Cypress how to found count a selection with same ID and get the length?

I have a such HTML code.
<div id ='pages'>
<div id='wrapper'>1 </div>
<div id='wrapper'>2 </div>
</div>
I am want to find elements count with id wrapper.
I using Cypress. I'm starting to learn Cypress.
If I try:
cy.get('div#wrapper').should('have.length', 2)
I get AssertionError:
CypressError: Timed out retrying: expected 1 to equal 2
As jonrsharpe pointed out, it's invalid HTML to have multiple elements with identical id attribute.
That being said, DOM is quite smart and can recover and work even with invalid HTML. Duplicate-id elements shouldn't cause much trouble.
If you e.g. try doing document.querySelectorAll('#wrapper') it should return list of 2 elements (in your case).
Problem is, Cypress is using jQuery to query the DOM instead of using native DOM methods and I guess jQuery isn't as smart (or it's more pedantic).
That being said, I can't reproduce that error when running:
// succeeds
cy.get('div#wrapper').should('have.length', 2)
Only when querying #wrapper directly (without the preceding div):
// fails
cy.get('#wrapper').should('have.length', 2)
I reckon this is because jQuery uses a heuristic of exiting early when a selector string (#wrapper) contains only a single id (and that's why div#wrapper returns both elements).
Also, your solution in comments (cy.get('#pages') .find('div#wrapper') .should(($div) => { expect($div).to.have.length(2) })), while working, isn't ideal because it won't retry. Let me demonstrate:
In the following code, the 2nd #wrapper will appear in the DOM only after 1 sec.
describe( 'test', () => {
beforeEach(() => {
cy.document().then( doc => {
doc.body.innerHTML = `
<div id='pages'>
<div id='wrapper'>1</div>
</div>
`;
setTimeout(() => {
doc.body.innerHTML = `
<div id='pages'>
<div id='wrapper'>1</div>
<div id='wrapper'>2</div>
</div>
`;
}, 1000 );
});
});
// will fail
it('solution A', () => {
cy.get('#pages') // <- won't be retried
.find('div#wrapper') // <- only this command will be retried
.should( $div => expect($div).to.have.length(2) );
});
// will pass
it('solution B', () => {
cy.get('#pages #wrapper') // <- will be retried and succeed in 1sec
.should( $div => {
expect($div).to.have.length(2);
});
});
// will pass
it('solution C', () => {
cy.get('#pages')
.should($pages => {
// using native DOM querying
expect($pages[0].querySelectorAll('#wrapper').length).to.eq(2);
});
});
});
Thus, you should go with solution similar to B or C.

how to do pagination in vuejs

hi i want to do pagination in my view page.can anyone tell me how to do that in vuejs..
Here is my view page:
<div class="container">
<div class="row">
<el-row :gutter="12">
<el-col>
<p>View Candidates</p>
</el-col>
</el-row>
<el-row :gutter="12">
<template v-for="c in candidates">
<el-col :span="6">
<Candidate :c="c" :key="c.id"></Candidate>
</el-col>
</template>
</el-row>
</div>
here is my js page:
const app = new Vue({
el: '#app',
data() {
return {
candidates: window.data.candidates,
}
},
components: { Candidate }
});
i am working on laravel5.4 and vuejs 2
Please can anyone help me..how to do this..
For real pagination you will need to ensure that your endpoints (from your post I'd say something like /candidates) will return json AND that it will return a pagniated object ofcourse.
In Laravel you'd do it like
public function index() {
return Candidates::paginate(10);
}
EDIT: for more information regarding laravel pagination you can take a look at their examples and docs: https://laravel.com/docs/5.4/pagination
A full example is rather hard to give but here a really short one
routes/web.php
Route::get('candidates', 'CandidateController#index');
app/http/controller/CandidateController
public function index() {
$candidates = App\Candidate::paginate(10);
return $candidates;
}
For a more detailed version of the laravel part you should provide your Controller, Migration, Routing setup.
In Vue I'd suggest you load all your data from within Vue and not with blade. Even though you could keep it as it is - it would be more "unified".
data: function() {
return { paginator: null }
},
created: function() {
// load initial first 10 entries
axios.get('/candidates').then(function(response) {
this.paginator = response.data;
}.bind(this));
}
Ok so now you have the initial load as you had it before. You can loop through pagniator.data which is your actual list now. Small example:
<ul v-if="paginator"><!-- important !not loaded on initial render-->
<li v-for="paginator.data as candidate">{{ candidate.name }}</li>
</ul>
Now to the load more. Let's say you want a button for that. The paginator has a pro called next_page_url to give you the next http endpoint. If it's null - now data is left to load.
<button v-if="paginator && paginator.next_page_url" #click.prevent="loadMore">Load more</button>
Button is setup - now the load more
methods: {
loadMore: function() {
// load next 10 elements
axios.get(this.paginator.next_page_url).then(function(response) {
// you have two options now. Either replace the paginator fully - then you will actually "page" your results.
// only 10 will be visible at any time
this.paginator = response.data;
}.bind(this));
}
}
There you go this is an actual pagination. If you want to loadMore to add 10 elements to your current list it is a little bit more tricky because you don't want to replace the paginator.data with the new loaded stuff. You want to concat it.
...
axios.get(this.paginator.next_page_url).then(function(response) {
response.data.data = this.paginator.data.concat(response.data.data);
this.paginator = response.data;
}.bind(this));

Sending multiple parameters to WinJS.Binding.converter() function

Is there a way to send more than one parameter to a WinJS.Binding.converter() function? Consider the following data and output:
{ contactName: "Tara Miller", mainNumber: "555-405-6190", alternateNumber: "555-209-1927" },
{ contactName: "Bryan Bond", alternateNumber: "555-574-4270" },
{ contactName: "Jenna Siever", mainNumber: "555-843-8823", alternateNumber: "555-799-5424" },
Here is the HTML. The MyData.chooseBestNumber converter function is used to display either a person's main phone number or the words "no main number" if they don't have a main number:
<div id="listViewTemplate" data-win-control="WinJS.Binding.Template">
<div class="contactCard">
<div data-win-bind="innerText: contactName"></div>
<div data-win-bind="innerText: mainNumber MyData.chooseBestNumber"></div>
</div>
</div>
Here is the JS defining the converter function:
WinJS.Namespace.define("MyData", {
chooseBestNumber: WinJS.Binding.converter(function (mainNumber) {
if (mainNumber) return mainNumber;
else return "no main number";
}),
});
Below is what I'd ultimately like to be able to do...passing more than one parameter into the converter function so that I can return either the main number (if it is defined), the alternate number (as a fallback), or a message (if all else fails):
WinJS.Namespace.define("MyData", {
chooseBestNumber: WinJS.Binding.converter(function (mainNumber, alternateNumber) {
if (mainNumber) return mainNumber;
else if (alternateNumber) return alternateNumber;
else return "no phone numbers";
}),
});
Is it possible to send more than one parameter to a WinJS.Binding.converter() function?
You can actually bind your phone number div to the this keyword which will effectively bind the innerText property of that div to the entire model object. That way in your converter, you'd have access to the whole model.
So your updated code would look like this:
HTML
<div id="listViewTemplate" data-win-control="WinJS.Binding.Template">
<div class="contactCard">
<div data-win-bind="innerText: contactName"></div>
<div data-win-bind="innerText: this MyData.chooseBestNumber"></div>
</div>
</div>
JavaScript Converter
WinJS.Namespace.define("MyData", {
chooseBestNumber: WinJS.Binding.converter(function (model) {
if (model && model.mainNumber) return mainNumber;
else if (model && model.alternateNumber) return alternateNumber;
else return "no main number";
}),
});