Best practice for testing for data-testid in a nested component with React Testing Library? - react-testing-library

I'm trying to write a test to check if my app is rendering correctly. On the initial page Ive added a data-testid of "start". So my top level test checks that the initial component has been rendered.
import React from "react";
import { render } from "react-testing-library";
import App from "../App";
test("App - Check the choose form is rendered", () => {
const wrapper = render(<App />);
const start = wrapper.getByTestId("start");
// console.log(start)
// start.debug();
});
If I console.log(start) the I can see all the properties of the node. However if I try and debug() then it errors saying it's not a function.
My test above does seem to work. If I change the getByTestId from start to anything else then it does error. But I'm not using the expect function so am I violating best practices?

There are two parts to this question -
Why console.log(start) works and why not start.debug()?
getByTestId returns an HTMLElement. When you use console.log(start), the HTMLElement details are logged. But an HTMLElement does not have debug function. Instead, react-testing-library provides you with a debug function when you use render to render a component. So instead of using start.debug(), you should use wrapper.debug().
Because you don't have an expect function, is it a good practice to write such tests ?
I am not sure about what could be a great answer to this, but I will tell the way I use it. There are two variants for getting an element using data-testid - getByTestId and queryByTestId. The difference is that getByTestId throws error if an element with the test id is not found whereas queryByTestId returns null in such case. This means that getByTestId in itself is an assertion for presence of element. So having another expect which checks if the element was found or not will be redundant in case you are using getByTestId. I would rather use queryByTestId if I am to assert the presence/absence of an element. Example below -
test("App - Check the "Submit" button is rendered", () => {
const { queryByTestId } = render(<App />)
expect(queryByTestId('submit')).toBeTruthy()
});
I would use getByTestId in such tests where I know that the element is present and we have expects for the element's properties (not on the element's presence/absence). Example below -
test("App - Check the "Submit" button is disabled by default", () => {
const { getByTestId } = render(<App />)
expect(getByTestId('submit')).toHaveClass('disabled')
});
In the above test, if getByTestId is not able to find the submit button, it fails by throwing an error, and does not execute the toHaveClass. Here we don't need to test for presence/absence of the element, as this test is concerned only with the "disabled" state of the button.

Related

Load suggestions in Autocomplete component only on input change

I fetch my autocomplete suggestions from my db, and I do that whenever the input changes, the problem is my function triggers every time my value changes as well (because that triggers an input change I guess), which is really inefficient and causes a lot of network requests on rerenders, how can I prevent that ?
My code looks something like this.
const handleValueChange = (event, newValue) =>{
setValue(newValue);
}
const handleInputChange = (event, newInput) =>{
// Fetching and setting suggestions here
...
setInput(newInput);
}
<Autocomplete
value={value}
onChange={handleValueChange}
inputValue={input}
onInputChange={handleInputChange}
....
/>
I would say look into using the useEffect hook in combination with state. Something along the line of
const [suggestions,setSuggestions] = useState();
useEffect(()=>{ //fetch and set suggestions },[]}
then set the options in the autocomplete to the state you just set. Making sure you conditional render the autocomplete to not display before fetching is done.
{suggestions? (<AutoComplete.../>):("")}
I don't know if you can use this line for line, but at least in the right direction.

Cypress UI - get an element inside double within block

I have the following scenario: I find some text to navigate to a section in my HTML. Then I find some other text to navigate to a subsection. Inside this subsection I have a button when clicked displays a modal dialog (which is placed outside both sections that I'm currently in. If I try to grab the modal dialog from within the sections it does not work. If I go outside the sections, it works.
cy.contains("Some text").parent().within(() => {
cy.contains("Some other text").parent().within(() => {
cy.find("Button that triggers a modal dialog").click();
//does not work
cy.getModalDialog().within(() => {
cy.contains("OK").click();
})
})
})
cy.contains("Some text").parent().within(() => {
cy.contains("Some other text").parent().within(() => {
cy.find("Button that triggers a modal dialog").click();
})
})
//works
cy.getModalDialog().within(() => {
cy.contains("OK").click();
})
Is there a better way how to grab this modal, without going outside the double within blocks?
Cypress provides an option withinSubject that can remove the effect of .within() for that particular command.
Element to search for children in. If null, search begins from root-level DOM element
cy.get("div#1").within(() => {
cy.get("span").within(() => {
// cy.get("div#2"); // ❌ fails
cy.get("div#2", { withinSubject: null }); // ✅ passes
})
})
Test page
<body>
<div id="1">
<span>one</span>
</div>
<div id="2">
<span>two</span>
</div>
</body>
Note this feature was broken in v12.0.2 and fixed again in v12.1.1
Although Cypress does allow things like cy.document() from which I presume you can go down from there, in general whenever I find a pattern where I "find a thing, then inside the thing, find another thing and do stuff", I'm better off never using .within at all. Instead I combine all the "find the thing" into one very large, very specific selector for a single .get
This does mean I need to repeat the long selector multiple times, and the .get is very heavy which seems backward, but it helps me avoid descending into callback heck.
In my current level of understanding, I do believe .within is a trap.

Set initial value to select within custom component in Angular 4

As you can see in this plunkr (https://plnkr.co/edit/3EDk5xxSLRolv2t9br84?p=preview) I have two selects: one in the main component behaving as usual, and one in a custom component, inheriting the ngModel settings.
The following code links the innerNgModel to the component ngModel.
ngAfterViewInit() {
//First set the valueAccessor of the outerNgModel
this.ngModel.valueAccessor = this.innerNgModel.valueAccessor;
//Set the innerNgModel to the outerNgModel
//This will copy all properties like validators, change-events etc.
this.innerNgModel = this.ngModel;
}
It works, since the name property is updated by both selects.
However when it first loads the second select has no selection.
I guess I'm missing something, a way to initialize the innerNgModel with the initial value.
This is a weird situation to do something like this, but I believe to get this working they need to implement another life-cycle hook. AfterModelSet or something like that :)
Anyways, you can solve this with a simple setTimeout and a setValue:
ngAfterViewInit() {
this.ngModel.valueAccessor = this.innerNgModel.valueAccessor;
this.innerNgModel = this.ngModel;
setTimeout(() => {
this.innerNgModel.control.setValue(this.ngModel.model);
})
}
plunkr

Protractor element handling

I have a question regarding how protractor handles the locating of elements.
I am using page-objects just like I did in Webdriver.
The big difference with Webdriver is that locating the element only happens when a function is called on that element.
When using page-objects, it is advised to instantiate your objects before your tests. But then I was wondering, if you instantiate your object and the page changes, what happens to the state of the elements?
I shall demonstrate with an example
it('Change service', function() {
servicePage.clickChangeService();
serviceForm.selectService(1);
serviceForm.save();
expect(servicePage.getService()).toMatch('\bNo service\b');
});
When debugging servicePage.getService() returns undefined.
Is this because serviceForm is another page and the state of servicePage has been changed?
This is my pageobject:
var servicePage = function() {
this.changeServiceLink = element(by.id('serviceLink'));
this.service = element(by.id('service'));
this.clickChangeService = function() {
this.changeServiceLink.click();
};
this.getService = function() {
return this.service.getAttribute('value');
};
};
module.exports = servicePage;
Thank you in advance.
Regards
Essentially, element() is an 'elementFinder' which doesn't do any work unless you call some action like getAttribute().
So you can think of element(by.id('service')) as a placeholder.
When you want to actually find the element and do some action, then you combine it like element(by.id('service')).getAttribute('value'), but this in itself isn't the value that you are looking for, it's a promise to get the value. You can read all about how to deal with promises elsewhere.
The other thing that protractor does specifically is to patch in a waitForAngular() when it applies an action so that it will wait for any outstanding http calls and timeouts before actually going out to find the element and apply the action. So when you call .getAttribute() it really looks like
return browser.waitForAngular().then(function() {
return element(by.id('service')).getAttribute('value');
});
So, in your example, if your angular pages aren't set up correctly or depending on the controls you are using, you might be trying to get the value before the page has settled with the new value in the element.
To debug your example you should be doing something like
it('Change service', function() {
servicePage.getService().then(function(originalService) {
console.log('originalService: ' + originalService);
});
servicePage.clickChangeService();
serviceForm.selectService(1);
serviceForm.save();
servicePage.getService().then(function(newService) {
console.log('newService: ' + newService);
});
expect(servicePage.getService()).toMatch('\bNo service\b');
});
The other thing that I'm seeing is that your pageObject appears to be a constructor when you could just use an object instead:
// name this file servicePage.js, and use as 'var servicePage = require('./servicePage.js');'
module.exports = {
changeServiceLink: element(by.id('serviceLink')),
service: element(by.id('service')),
clickChangeService: function() {
this.changeServiceLink.click();
},
getService: function() {
return this.service.getAttribute('value');
}
};
Otherwise you would have to do something like module.exports = new servicePage(); or instantiate it in your test file.
When you navigate another page, the web elements will be clear, that you selected. So you have to select again. You can select all elements that is in a page of HTML. You can click that you see. So the protactor + Selenium can decide what is displayed.
You have a mistake in your code, try this:
expect(servicePage.getService()).toMatch('\bNo service\b');

Is it fine to mutate attributes of React-controlled DOM elements directly?

I'd like to use headroom.js with React. Headroom.js docs say:
At it's most basic headroom.js simply adds and removes CSS classes from an element in response to a scroll event.
Would it be fine to use it directly with elements controlled by React? I know that React fails badly when the DOM structure is mutated, but modifying just attributes should be fine. Is this really so? Could you show me some place in official documentation saying that it's recommended or not?
Side note: I know about react-headroom, but I'd like to use the original headroom.js instead.
EDIT: I just tried it, and it seems to work. I still don't know if it will be a good idea on the long run.
If React tries to reconcile any of the attributes you change, things will break. Here's an example:
class Application extends React.Component {
constructor() {
super();
this.state = {
classes: ["blue", "bold"]
}
}
componentDidMount() {
setTimeout(() => {
console.log("modifying state");
this.setState({
classes: this.state.classes.concat(["big"])
});
}, 2000)
}
render() {
return (
<div id="test" className={this.state.classes.join(" ")}>Hello!</div>
)
}
}
ReactDOM.render(<Application />, document.getElementById("app"), () => {
setTimeout(() => {
console.log("Adding a class manually");
const el = document.getElementById("test");
if (el.classList)
el.classList.add("grayBg");
else
el.className += ' grayBg';
}, 1000)
});
And here's the demo: https://jsbin.com/fadubo/edit?js,output
We start off with a component that has the classes blue and bold based on its state. After a second, we add the grayBg class without using React. After another second, the component sets its state so that the component has the classes blue, bold, and big, and the grayBg class is lost.
Since the DOM reconciliation strategy is a black box, it's difficult to say, "Okay, my use case will work as long as React doesn't define any classes." For example, React might decide it's better to use innerHTML to apply a large list of changes rather than setting attributes individually.
In general, if you need to do manual DOM manipulation of a React component, the best strategy is to wrap the manual operation or plugin in its own component that it can 100% control. See this post on Wrapping DOM Libs for one such example.