I have a filter bar written with React which has basically 3 dropdowns in it. My goal is to test whether clicking one of the dropdowns and selecting an item changes the redux state.
At the start of my app, I make an API request to pull all items which will be filtered by filter bar.
And to prevent any user apply a filter before all items loaded, I made all dropdowns disabled
Communication between dropdowns and listed items are provided by redux.
The problem is since I cannot fake redux, I try to grab the dropdowns and make them enabled by changing their properties, but changing properties doesn't make dropdowns clickable at all.
How can I achieve the behavior I want?
I've already tried to mock API request. But because the API call is fired from different component, mocking it has no use
it("selecting a filter updates redux store", async () => {
const { getByText, getByLabelText } = setup(FilterNav);
const countrySelect = getByText("Country Filter");
// Dropdown disabled
// <a aria-haspopup="true" aria-expanded="false" href="#"
class="dropdown-toggle nav-link disabled" role="button"
tabindex="-1" aria-disabled="true">Country Filter</a>
countrySelect.removeAttribute("aria-disabled");
countrySelect.classList.remove("disabled");
// Enabled
// <a aria-haspopup="true" aria-expanded="false" href="#"
class="dropdown-toggle nav-link" role="button">Company</a>
fireEvent.click(countrySelect);
// countrySelect is disabled at the beginning
// updating state some how doesn't update the disabled attribute
// so click becomes useless
const targetCountry = await waitForElement(() =>
getByLabelText("Example Country")
);
});
Start by breaking up the components in a better way. First, separate the data fetching (api call) from the list of dropdowns. Then, have the dropdown list component accept the data that is needed. This will allow you to easily mock the data when creating a unit test.
Once the first dropdown has its data and is enabled, this can be tested by:
expect(dropdown1.prop('disabled')).toBeTruthy()
expect(dropdown2.prop('disabled')).toBeFalsy()
expect(dropdown3.prop('disabled')).toBeFalsy()
I don't think you need to update the disabled attribute. However, if this is the case you should be able to use something like .setProp()
If you're not using jest/enzyme just determine an equivalent for prop() and setProp()
Related
I am writing a React application that I'm testing with Cypress. In my integration tests I've written a cypress command that takes an array of strings, in order to select items in a multi-value select box.
The code I'm using looks roughly like this:
Cypress.Commands.add("command", (options) => {
options.forEach((option) => {
cy.get("div[data-g-portal-id='1']") // this is the container for the select box dropdown
.find("div[role='menubar']")
.contains(option)
.click({ force: true });
});
}
I've tried various iterations of this, including things like cy.wrap and .each. Whatever I do, when the array contains more than one item, it clicks one item, the item is marked as selected successfully, then it clicks the other item, marks it as selected, but the first item loses its selection state. Its as if the internal state of the component never really got the initial change.
I've confirmed this is not a bug in my application; when testing it manually, the multi-select works fine. But Cypress just doesn't want to know. Any help would be much appreciated.
The v2.grommet.io/select uses a "portal" to display the dropdown options.
The portal is just a div that's appended to body when the dropdown opened and removed again when an option is clicked.
Problem is, there isn't a property to connect the dropdown and portal containing the options. There may be other portals present e.g the Layer component uses a portal.
The options portal we need to target will always be the last portal on the page (since we just opened the dropdown). In the custom command, applying the .last() command will select the options portal.
Cypress.Commands.add("multiselect", (options) => {
options.forEach((option) => {
cy.get(`[data-g-portal-id]`)
.last() // just opened the dropdown so option are in the last portal
.find("div[role='menubar']")
.contains(option)
.click();
});
});
cy.get('input[name="my-select"]')
.click(); // open dropdown
.multiselect(['First', 'Third']);
cy.get('input[name="my-select"]')
.click(); // close dropdown
cy.get('input[name="my-select"]')
.should('have.value', 'multiple')
The test ends with dropdown displaying the text "multiple".
Configuration of the Select,
<Select
multiple // allow multiple selected options
closeOnChange={false} // do not close the dropdown between selections
name="my-select"
options={myOptions}
...
/>
I have many dropdown lists which I created and work properly .When I click on its list title element (here on City ) it opens or shows its dropdown menu below .
Below is an illustration to let you know how all my dropdown list are made .
<div class="dropdown-container">
<div class="title">City </div>
<ul class="dropdown-menu" >
<li class="glist_item">Calgary</li>
<li class="glist_item">Miisssauga</li>
<li class="glist_item">Winnipeg</li>
<li class="glist_item">Vancouver</li>
<li class="glist_item">Surrey</li>
</ul>
</div>
My problem is how to save the state (opened or closed) of each dropdown list after the page reloads so that those who were opened or closed keep their respectives state before reload.
NB: I tried localStorage or sessionStorage before but these cannot store complex data or data with many records since the number of list is undefined .
So I need a persistent data storage capability as well as an ability to store data like array does .
CAN YOU HELP ME ?
You can do it in this way. Just create an array includes information of lists states.
let states = {
list1: "opened",
list2: "opened",
list3: "closed",
list4: "opened",
list5: "opened",
}
Here, I think you can change listx to list id.
You can save this to localStorage by converting it to string.
Then, you can get that string from localStorage and parse it to js object and do what you are going to do.
Simplified problem
I have a store. For a product to be included in the store there needs to be a shelf for it. So, to add a new product to the store the workflow is:
Add a shelf
Add product to that shelf
(The workflow can not be changed)
Realization
The shelf is realized by a row in a table, which in turn is controlled by an Angular.js controller (each shelf is an object in an array). To add an product the user selects "create product" in a drop-down menu that is present on each row. This will show an bootstrap modal where I have from a controller added a tab for each product that is possible to add (since each product needs configuration :) ) , then when the user presses a "create" button in the modal a JavaScript method is called interfacing a REST interface to add the product (the UI is updated by a Socket.io event send from the server when the product has been added successfully.
Problem
The JavaScript method (CreateProduct) needs to now what row (R) was affected as well as what tab (T) was selected so that the "onclick" method for the button is CreateProduct(R, T);
My current solution is pretty ugly imho, I have two global variables for R and T, then I use jQuery to capture show event and tab event from the modal, the link in the dropdown has a field "data-row-id" that is identifying the row
HTML (Jade) snippet from dropdown menu:
a(data-toggle="modal", href="#createProduct", data-row-id="{{row.RowID}}") Create Product
JavaScript:
var R = null;
$('#productModal').on('show.bs.modal', function(e) {
R = $(e.relatedTarget).data('row-id');
});
var T = null;
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
T = e.target.text;
});
I hope there is a better solution to this, I probably am just thinking a bit upsidedown due to inexperience with Angular.js , perhaps there is a way to pass these through the model? Perhaps add these to the modal controller, but then, how to pass the row? My dream would be something like this on the button code
button(type="button", class="btn btn-default", data-dismiss="modal", ng-click="storeCtrl.CreateProduct({{modalCtrl.shelf, modalCtrl.Product)") Create Product
I found a better way (at least I don't need to use jQuery) using ModalService
I created a CreateProductModalController having a variable selectedProduct, this is set on a ng-click event in the tab
ul(class="nav nav-pills", id="tabContent")
li(ng-repeat="prod in products", ng-class="{active: $index == 0}", ng-click="activateTab(prod.name)")
a(href="#{{prod.name}}", data-toggle="tab") {{prod.name}}
The ModalService is called with the rowID that was clicked.
The only problem I have now is that all must be in $scope, I want it to be more encapsulated
I am working with an app that has an ng-repeat that populates a navigation sidebar with a list of items from a Mongo DB. The ng-repeat also populates a series of option buttons for each item. A couple of these option buttons share a dynamic id for each iteration in the ng-repeat. What should be happening here is when I click on one of these buttons, it would change the button 'text' and display some additional options under the menu item and toggle back when clicked again.
Here is my code for these buttons:
<span>
<button ng-hide="highlightItem()" ng-click="showTopic()" ng-attr-id="{{ 'category' + subject._id }}" class="add-button"><i class="fa fa-chevron-down"></i></button>
<button ng-click="hideTopic()" ng-show="highlightItem()" ng-attr-id="{{ 'category' + subject._id }}" class="add-button"><i class="fa fa-chevron-up"></i></button>
</span>
The issue that I am having is that I cannot seem to figure out how to access that dynamic id in my controller. I have code in place that will change the button between the ng-show and ng-hide, but it does it for all iterations of ng-repeat.
This is currently how I am attempting to access the dynamic id. I am not getting any errors, but when I try to use this in my function it doesn't work.
$scope.subjectList = subjects.get({});
var topicButton = document.getElementById('topic' + $scope.subjectList._id);
I have also tried
var topicButton = document.getElementById('topic' + $scope.subject._id);
What is the best way to access the dynamic id in Angular/Javascript? I do not want to use jQuery with this if at all possible.
First and foremost, never manipulate the DOM within an angular controller! It is bad practice. Also, it is bad practice to evaluate methods in ngShow/ngHide.
If I understand you correctly, you're trying to get the subject_id for some reason when the button is clicked. Why can't you just pass back either the id or the entire subject to your method? Then your html would look something like this:
<span>
<button ngClick="toggleTopic(subject)" class="add-button">
<i class="fa" ng-class="{'fa-caret-down': subject.hidden, 'fa-caret-up': !subject.hidden}"></i>
</button>
</span>
Then in your controller you could write something like this:
$scope.toggleTopic = function(subject) {
subject.hidden = !subject.hidden;
};
Using the hidden attribute of your subjects, you can now show or hide elements of your dropdown with ngShow/ngHide like so:
<p ng-bind="subject.descripton" ng-hide="subject.hidden"></p>
This way, you don't have to search the DOM for elements at all.
I am trying to enhance my page with a jquery right mouse menu, but am having trouble building the correct structures to populate it easily.
Currently my page contains (among other things) a list of items for the user to review. (an html table) Based on the users role, and the current state and context of the row, the user may take one of various actions on each row of data. (approve, reject, refer it to someone else, ect.) My ASP.Net page handles this by setting the visibility of an imagebutton within the row to true, if the option is available. I can control the Cssclass of each button, and am setting the class of for example the "approve" button to “approvebtn”.
Now I want to enhance my site with a right menu.
I am extending my site with Cory S.N. LaViska’s jQuery Context Menu Plugin -
http://abeautifulsite.net/notebook/80
This plugin allows the default right mouse behavior for any elelement to be overridden with a user controlled context menu. The menu is inserted into your page as an unordered list and becomes visible when it is needed.
<ul id="rightMenu" class="contextMenu">
<li class="details">Details </li>
<li class="addnote">AddNote </li>
<li class="listnote">ShowNotes </li>
<li class="approve">Approve </li>
<li class="reject">Reject </li>
<li class="release">Release </li>
<li class="takeover">Takeover </li>
</ul>
Your app gets a callback when something on the right menu is clicked, and you can interrogate the action (the bogus href element) to see which item it was.
I really like this menu because it is simple to use and is completely CSS styled.
However, I need to do something that this plugin does not nativly seem to support. I need to change which items are available on the menu from row to row. Basically if an Imagebutton (for say approve) is avaiable in the row, then its corrisponding menu item should exist as well.
I was able to gain access to the menu just before it is displayed by altering the plugin slightly, to call my function right before the menu is displayed.
This works, but the logic I had to write seems so brute force, that there must be a better way….
In my callback:
function jimsbuggeredfunction(menu,el)
"el" is the element that was right clicked on (usually a table cell), and "menu" is the menu that this right click is bound to. (so I should be using that name and not hardcoding to #rightMenu')
So, the “if” line finds out if the table row containing the element that was “right clicked” contains a specific button (by its class name) if it does the menu item is enabled, otherwise it is disabled. This process continues for every menu item that I want to be flexable row-to-row.
function jimsbuggeredfunction(menu,el) {
if($(el).parents("tr:eq(0)").find('.approvebtn').length > 0)
$('#rightMenu').enableContextMenuItems('#approve');
else
$('#rightMenu').disableContextMenuItems('#approve');
if($(el).parents("tr:eq(0)").find('.rejectbtn').length > 0)
$('#rightMenu').enableContextMenuItems('#reject');
else
$('#rightMenu').disableContextMenuItems('#reject');
if($(el).parents("tr:eq(0)").find('.releasebtn').length > 0)
$('#rightMenu').enableContextMenuItems('#release');
else
$('#rightMenu').disableContextMenuItems('#release');
if($(el).parents("tr:eq(0)").find('.takeoverbtn').length > 0)
$('#rightMenu').enableContextMenuItems('#takeover');
else
$('#rightMenu').disableContextMenuItems('#takeover');
if($(el).parents("tr:eq(0)").find('.revertbtn').length > 0)
$('#rightMenu').enableContextMenuItems('#revert');
else
$('#rightMenu').disableContextMenuItems('#revert');
if($(el).parents("tr:eq(0)").find('.removebtn').length > 0)
$('#rightMenu').enableContextMenuItems('#remove');
else
$('#rightMenu').disableContextMenuItems('#remove');
if($(el).parents("tr:eq(0)").find('.addnotebtn').length > 0)
$('#rightMenu').enableContextMenuItems('#addnote');
else
$('#rightMenu').disableContextMenuItems('#addnote');
if($(el).parents("tr:eq(0)").find('.listnotebtn').length > 0)
$('#rightMenu').enableContextMenuItems('#listnote');
else
$('#rightMenu').disableContextMenuItems('#listnote');
};
There must be a better way to set this up, so that it also just ignores menu items that I want to display all of the time) but it is escaping me at the moment. Is there a better way to accomplish this?
Thanks,
Jim
I would find some way to create a mapping between the two IDs and some more systematic way of finding the relevant button. For example, if the button always belongs inside a certain cell that has a class, let's say "buttonclass", then something like this should work:
var mapping = {
takeoverbtn: '#takeover',
listnotebtn: '#listnote'
// ...
};
function jimsbuggeredfunction(menu,el) {
var buttontype = $(el).parents("tr:eq(0)").find('.buttonclass').children().attr("class");
$('#rightMenu').disableContextMenuItems(mapping[buttontype]);
}
My jQuery is a little rusty, there's probably a cleaner way of retrieving the buttontype, but that general idea ought to work.