Disclaimer; I'm a bit new to react-testing-library (been using an admittedly older version of Enzyme) and the Apollo Query/MockedProvider components (been using the client via a JS service object), so this may be a stupid question...
I have a component that receives a list of countries for which I am trying to write tests. What I would like to do is something like:
import React from 'react';
import { MockedProvider } from '#apollo/react-testing';
import { render, act } from '#testing-library/react';
import wait from 'waait';
import Countries, { countryQuery } from './Countries';
import { isTerminating } from 'apollo-link/lib/linkUtils';
const mockCountryName = 'sample country';
const mocks = [
{
request: {
query: countryQuery,
vairables: {},
},
result: {
data: {
countries: [{ name: mockCountryName }],
},
},
},
];
describe('when working with the countries component', () => {
describe('and the component is loading', () => {
let component;
beforeAll(async (done) => {
await act(async () => {
component = render(
<MockedProvider mocks={[]}>
<Countries />
</MockedProvider>
);
});
done();
});
it('should have a title', () => {
expect(component.getByText('Countries Component')).not.toBeUndefined();
});
it('should have a loading status', () => {
expect(component.getByText('Loading...')).not.toBeUndefined();
});
});
});
When this runs, the second test (about loading status) fails as it looks like the component is only a body tag at that point. I tried changing beforeAll to beforeEach, but that just produced a component that had an Error indicator. I put some console.log statements in my component, and this is what they are showing me:
console.log src/components/Countries.js:45
Loading is: true
console.log src/components/Countries.js:46
Error is: undefined
console.log src/components/Countries.js:45
Loading is: false
console.log src/components/Countries.js:46
Error is: Error: Network error: No more mocked responses for the query: {
countries {
name
phone
__typename
}
}
, variables: {}
I'm wondering if it does not like the empty array passed in as the mocks property for the MockedProvider. But every example I've seen does it that way, so...
As an experiment, I added a second set of test to the spec file to see if it was just a weird timing issue with the component that was causing the issue. Here's the second test:
describe('and the component has data', () => {
let component;
beforeAll(async (done) => {
await act(async () => {
component = render(
<MockedProvider mocks={mocks} addTypename={false}>
<Countries />
</MockedProvider>
);
await wait(0);
});
done();
});
it('should have a title', () => {
expect(component.getByText('Countries Component')).not.toBeUndefined();
});
it('should have a loading status', () => {
expect(component.getByText(mockCountryName)).not.toBeUndefined();
});
});
This has the same problem; the first test works (if I reorder the test, the one that first always works) but the second one fails, and the component seems to be an empty body tag.
Is there a way to make this type of test structure work? I don't like the idea of having to put everything into a single test, let alone the setup code for the component.
Thanks!
I'm not sure if it's the best approach, but I think I found a workaround.
First, the empty array/loading issue was not an issue; I traced everything back to testing-library resetting/re-rendering the component between tests.
Here's what I did:
describe('and the component is loading', () => {
let component, pageTitle, loadingMessage;
beforeAll(async (done) => {
await act(async () => {
component = render(
<MockedProvider mocks={[]}>
<Countries />
</MockedProvider>
);
pageTitle = component.getByText(mockPageTitle);
loadingMessage = component.getByText(mockLoadingMessage);
});
done();
});
it('should have a title', () => {
expect(pageTitle).not.toBeUndefined();
});
it('should have a loading status', () => {
expect(loadingMessage).not.toBeUndefined();
});
});
Instead of trying to call component.getTextBy in each test, I moved them into the beforeAll, and assigned the output to variables. Each test uses the variables for their tests. I also wrote a test for my Routes component, and I was still able to call fireEvent.click() on the components.
I would be very interested in any feedback from anyone who has a more experience with testing-library on this. It seems better than what I had, but I want to make sure it's really the best approach.
Thanks.
Related
I'm trying to use fixtures to hold data for different tests, specifically user credentials. This is an example of the code. I'm getting 'Cannot read properties of undefined (reading 'data')'. I tried to google search , I found Cypress fixtures - Cannot read properties of undefined (reading 'data')
I used closure variable technique as reccomended in that post , yet I got reference error of unable to reference data.Please help me.I know cypress.config can be used but I want to keep that for global configs
Json(credentials.json):
{
"username":"*****",
"password":"*****"
}
Code:
import { LoginPage } from "./pageobject/login_page"
describe('Test Scenario', () => {
before(function () {
cy
.fixture('credentials').then(function (data) {
this.data = data
})
})
it('Simple login', () => {
cy.visit(Cypress.env('url'))
var loginpage = new LoginPage()
loginpage.EnterUsername(this.data.username)
loginpage.clickonSubmit()
loginpage.EnterPassword(this.data.password)
loginpage.clickonSubmit()
Cypress
.on('uncaught:exception', (err, runnable) => {
return false;
});
cy.
wait(10000)
cy.
get('span[id="user"]').should('have.text', this.data.username , 'User Login Unsuccessfully')
});
});
There's a few things need adjusting
use function () {} syntax in the it() block
use beforeEach() and alias to load the fixture, because data on this can be cleared (especially after login)
move uncaught:exception catcher to the top of the block
don't cy.wait(), instead add timeout to next command
.should() only has two parameters in this case, so use .and() to test the 2nd text
import { LoginPage } from './pageobject/login_page';
describe('Test Scenario', () => {
beforeEach(function () {
cy.fixture('credentials').as('data')
})
it('Simple login', function() {
Cypress.on('uncaught:exception', (err, runnable) => {
return false;
});
cy.visit(Cypress.env('url'));
var loginpage = new LoginPage();
loginpage.EnterUsername(this.data.username);
loginpage.clickonSubmit();
loginpage.EnterPassword(this.data.password);
loginpage.clickonSubmit();
cy.get('span[id="user"]', {timout:10_000})
.should('have.text', this.data.username)
.and('have.text', 'User Login Unsuccessfully')
})
})
I suspect it's because you are using an arrow function instead of a regular function, you cannot access the this object with an arrow function.
Cypress docs
If you store and access the fixture data using this test context
object, make sure to use function () { ... } callbacks. Otherwise the
test engine will NOT have this pointing at the test context.
change it to this:
it('Simple login', function() {
...
});
I am learning Cypress along with JavaScript. I am running into a problem that I am not certain how to search it into documentation. The site I started testing has the typical wait issues so I encountered a very good solution here.
Now my test is looking in this way
/// <reference types="Cypress" />
let appHasStarted
function spyOnAddEventListener (win) {
// win = window object in our application
const addListener = win.EventTarget.prototype.addEventListener
win.EventTarget.prototype.addEventListener = function (name) {
if (name === 'change') {
// web app added an event listener to the input box -
// that means the web application has started
appHasStarted = true
// restore the original event listener
win.EventTarget.prototype.addEventListener = addListener
}
return addListener.apply(this, arguments)
}
}
function waitForAppStart() {
// keeps rechecking "appHasStarted" variable
return new Cypress.Promise((resolve, reject) => {
const isReady = () => {
if (appHasStarted) {
return resolve()
}
setTimeout(isReady, 0)
}
isReady()
})
}
describe('Main test suite', () => {
beforeEach(() => {
cy.visit('http://mercadolibre.com.ar',{
onBeforeLoad: spyOnAddEventListener
}).then({ timeout: 10000 }, waitForAppStart)
})
it('search first scanner', () => {
cy.contains('nav-search-input').type("scanner bluetooth para auto")
})
})
The problem with this is, I should replicate spyOnAddEventListener, waitForAppStart and variable appHasStarted at the beginning of every source file but I want to avoid this. How could properly extend this functions as a part of the internal source project without replicating in every test source? I have tried to make a simple source JavaScript file at the root of the project but when I import it, Cypress clients give an unrelated plug error like this one:
It looks like you've added the code to /cypress/plugins/index.js, but that is for task extensions (code that requires NodeJS access).
The two functions can be added to a file, ideally in the /cypress/support folder
wait-for-app-utils.js
let appHasStarted
function spyOnAddEventListener (win) {
...
}
function waitForAppStart() {
...
}
module.exports = {
spyOnAddEventListener,
waitForAppStart
}
test
import {spyOnAddEventListener, waitForAppStart} from '../support/wait-for-app-utils.js'
describe('Main test suite', () => {
beforeEach(() => {
cy.visit('http://mercadolibre.com.ar', {
onBeforeLoad: spyOnAddEventListener
}).then({ timeout: 10000 }, waitForAppStart)
})
Another approach is to wrap it all up (including the visit) into a custom command. Now there's no need to export and import, the command will be available globally.
/cypress/support/commands.js
let appHasStarted
function spyOnAddEventListener (win) {
...
}
function waitForAppStart() {
...
}
Cypress.Commands.add('visitAndWait', (url) =>
cy.visit(url, { onBeforeLoad: spyOnAddEventListener })
.then({ timeout: 10000 }, waitForAppStart)
)
test
describe('Main test suite', () => {
beforeEach(() => {
cy.visitAndWait('http://mercadolibre.com.ar')
})
I'm trying to do a pretty simple intercept in Cypress using a Vue's application. My component has a setup method using render function as such:
setup() {
useInfiniteLoading({ runner: ... })
}
Then on my tests I do the following:
describe("List todo resource", () => {
it("Checks it loads more todos when scrolling to the bottom", function () {
cy.intercept('/todo').as('getTodos');
cy.visit("/todos");
cy.wait("#getTodos").then(({response}) => {
console.log(response);
})
})
})
When running the test I see that the intercept is not stubbing the response.
As you can see from the image the request makes a request to my actual server running locally and the response is stubed. The weird part is that in a previous test I have:
it("Checks the todo list gets updated when clicking on to resolve it (from true to false)", function () {
cy.visit("/todos");
const resolved = false;
const shouldHaveClass = resolved
? "mdi-checkbox-marked-outline"
: "mdi-checkbox-blank-outline";
cy.intercept("GET", "todo", {
fixture: "resources/todo/list.todo.json",
}).as("getTodos");
cy.intercept("PUT", "todo", {
body: { data: { ...this.updateTodoFixture.data, resolved } },
}).as("updateTodo");
cy.get(".todo-list-item__resolve")
.first()
.each((btn) => {
btn.click();
});
cy.get(".todo-list-item__resolve")
.first()
.should("satisfy", ($el) => {
const classList = Array.from($el[0].classList);
return classList.includes(shouldHaveClass);
});
});
And the response is stubbed using intercept as you can see from the previous screenshot. Is it possible that the previous test is affecting the next test? I have tried taking a look into "Intercept too soon" but no luck on trying to apply the fix described in the page.
Any idea on what could be causing the stub not to happen?
I have a VueJS component text-editor that I call as such in my tests:
import Vue from 'vue';
import TextEditor from '../src/TextEditor.vue';
import expect, {spyOn} from 'expect';
describe("<text-editor>", (done) => {
beforeEach(() => {
this.vm = new Vue({
data: { hi: { text: "hi" } },
template: '<div><text-editor ref="texteditor" v-model="hi" #hide="hidecb"></text-editor></div>',
components: { 'text-editor': TextEditor },
methods: {
hidecb: function() {
console.log('hide');
}
}
}).$mount();
});
it("should call the #hide callback", (done) => {
let spy = spyOn(this.vm, 'hidecb');
// This call will trigger an this.$emit('hide') in the component
this.vm.$refs.texteditor.save();
Vue.nextTick(() => {
expect(spy).toHaveBeenCalled();
done();
});
});
});
Weird thing is, method hidecb does get called and I see "hide" on the console. But the spy will fail with the following error:
Error: spy was not called (undefined:29)
I checked and the callback is indeed called before nextTick. I should also point out that I am using Webpack, expectjs and karma.
Would someone be able to point out why the spy doesn't work?
I use default Aurelia's sceleton-esnext updated to last version .
I added this line to App (
Example from doc. Customizing the Navigation Pipeline)
config.addPipelineStep('authorize', AuthorizeStep);
After this I catch error running 'gulp test'
Chrome 52.0.2743 (Linux 0.0.0) the App module contains a router property FAILED
TypeError: config.addPipelineStep is not a function
Test
it('contains a router property', () => {
expect(sut.router).toBeDefined();
});
Test is going well without line.
I just came across this issue. To fix, you just need to add an empty method into your RouterStub:
class RouterStub {
configure(handler) {
handler(this);
}
map(routes) {
this.routes = routes;
}
addPipelineStep(stepName, stepClass) {
}
}
then in your test:
describe('the App module', () => {
var app;
var mockedRouter;
beforeEach(() => {
mockedRouter = new RouterStub();
app = new App();
app.configureRouter(mockedRouter, mockedRouter);
});
it('contains a router property', () => {
expect(app.router).toBeDefined();
});
});
If you're trying to test the pipeline step you'll need to mock the router itself and test the actual logic, but if you just want your tests to run (i.e. be defined, check the router title etc), this will work.