When I run a basic test case like this:
fdescribe('Browser back button', () => {
fit('should return to previous', async () => {
const originalHref = window.location.href;
window.history.pushState({}, 'Test', '/test');
window.history.back(); // This doesn't seem to be working.
expect(window.location.href).toEqual(originalHref);
});
I get a failure with the message "Expected 'http://localhost:9876/test' to equal 'http://localhost:9876/context.html'." So clearly the pushState() worked but the back() did not.
Karma runs tests in an inner iframe. When I paste the first three lines in order into the browser's JavaScript console, the navigation works as expected. So there must be something different between the behavior of the window and the behavior of the inner iframe in this case.
This is Karma 5.1 on Chrome.
Any thoughts?
The issue is that history.back() API is asynchronous
This method is asynchronous. Add a listener for the popstate event in order to determine when the navigation has completed.
So the navigation back is triggered but the test does not wait for it to complete and the check fails.
We need to convert the test into an asynchronous one by adding done param (it's one method I know, maybe there are other ways too). And use popstate event handler to wait for navigation and complete the test.
fdescribe('Browser back button', () => {
fit('should return to previous', (done) => {
const originalHref = window.location.href;
window.history.pushState({}, 'Test', '/test');
window.history.back(); // gets executed asynchonously
window.addEventListener('popstate', () => {
// history.back() is done now
expect(window.location.href).toEqual(originalHref);
done();
})
});
});
Vitalii's answer is the most generally applicable one since it relies only on browser built-ins. Here is a basic suite for Angular users, based on their code.
import { ComponentFixture, TestBed } from '#angular/core/testing';
import { MyComponent } from './my.component';
import { Location } from '#angular/common';
describe('MyComponent', () => {
let component: MyComponent;
let fixture: ComponentFixture<MyComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ MyComponent ],
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
fdescribe('Browser back button', () => {
let location: Location;
let currentUrl: string;
const previousUrl = '/test';
beforeEach(() => {
location = TestBed.inject(Location);
currentUrl = location.normalize(window.location.href);
location.go(previousUrl);
location.go(currentUrl);
});
afterEach(() => location.go(currentUrl));
fit('should return to previous', done => {
location.subscribe(e => {
expect(e.url).toEqual(previousUrl);
done();
});
location.back();
});
});
});
To customize the back button's behavior, rename 'should return to previous' to describe whatever you want the button to do instead, and update the test accordingly.
Related
In latest version of Cypress Cookie preserve is deprecated so I wish to us cy.session. However I can't get this to work across multiple tests as cy.session needs to be inside a test 'it', example of how my tests are currently set up.
beforeEach(() => {
Cypress.Cookies.defaults({
preserve: /somecookie|someOtherCookie|AnotherCookie/
})
it('Navigate to URL', () => {
performance.NavigateToUrl(URL);
});
it('Login - Username/Pass', () => {
performance.LoginUserPass();
});
it('Navigate Order Page', () => {
performance.Orderpage();
});
//And so on............
Any help and suggestions welcome as i don't really want to rewrite the test structure as i create a report on it current output/design.
for the session to be kept across all tests
With recent changes, Cypress are trying to "enforce" test isolation more, and you are going against their best practice by having dependency across the different it() blocks.
A better approach would be to structure according to context
context('before logging in', () => {
it('Can navigate to home page', () => {
...
})
it('Can log in', () => {
...
})
})
context('after logging in', () => {
beforeEach(() => {
cy.session('login', () => {
// do login via cy.request()
// preserve all browser login data such as cookies, localstorage
})
})
it('Can use order page', () => {
...
})
})
Test isolation flag
There is a note on the cy.session() page that indicates you can turn off test isolation, but the paragraphs are a bit vague IMO
The page is cleared before setup when testIsolation is enabled and is not cleared when testIsolation is disabled.
Cookies, local storage and session storage in all domains are always cleared before setup runs, regardless of the testIsolation configuration.
So, it's worth trying with setting testIsolation: false - but don't call cy.session() at all.
I think you could use the cy.session with the callback function in a before hook with both the visit and login methods:
const sessionId = 'Login with valid credentials'
before(() => {
cy.session(sessionId, () => {
performance.NavigateToUrl(URL)
performance.LoginUserPass()
})
})
Then restore the session and visit the page again in a beforeEach hook.
beforeEach(() => {
cy.session(sessionId)
performance.NavigateToUrl(URL)
})
it('Navigate Order Page', () => {
performance.Orderpage();
})
//And so on............
describe("Share Link", () => {
beforeEach(() => {
cy.generateLink().then(response => {
let url = response.meeting_shared_link.split("/");
cy.wrap(url[url.length - 1]).as("link");
});
describe("when I turn on link sharing", () => {
// This works. Changing it to a before hook breaks it.
beforeEach(() => {
cy.get("#link").then(link =>
cy.toggleLinkSharing({ link: link })
);
});
});
I am currently generating an alias - #link - in my first beforeEach hook, and then accessing it in the next beforeEach hook which is nested in a describe.
My problem is that I need the latter hook to be a before, rather than a beforeEach.
When I modify it to a before hook - it is unable to find the alias "link". Why?
I understand that aliases are cleared between each test, hence the need for the first beforeEach hook - but why isn't it available inside a before hook?
Edit:. I think, it may be because the latter defined before hook triggers before the first beforeEach - in which case, the alias didn't yet exist. If this is the case - it's not intuitive. The before hook should only fire after the beforeEach since it's nested in another describe.
You can change the outer beforeEach() to before(), and preserve the alias between tests with beforeEach(function() { cy.wrap(this.link).as('link') })
It works because this.link is not cleared between tests, even though the alias #link is cleared.
describe("Share Link", () => {
before(() => {
cy.wrap('my-url').as("link");
console.log('Outer before')
});
describe("when I turn on link sharing", () => {
beforeEach(function() { cy.wrap(this.link).as('link') }) // preserve the alias
before(() => {
cy.get("#link").then(link => {
console.log('Inner before', link)
})
})
it('1st test', () => {
cy.get('#link').then(link => console.log('1st test', link))
})
it('2nd test', () => {
cy.get('#link').then(link => console.log('2nd test', link))
})
})
})
Console output
Outer beforeEach
Inner before my-url
1st test my-url
2nd test my-url
You can use this.link and try out.
describe("Share Link", () => {
beforeEach(() => {
cy.generateLink().then(response => {
let url = response.meeting_shared_link.split("/");
cy.wrap(url[url.length - 1]).as("link");
});
describe("when I turn on link sharing", () => {
// This works. Changing it to a before hook breaks it.
beforeEach(() => {
cy.toggleLinkSharing(this.link)
});
});
Reference here:
https://docs.cypress.io/guides/core-concepts/variables-and-aliases#Sharing-Context
I'm having a little trouble confirming my function is called using Jest. I've got two mocked functions. One is just mocked to return a promise, the other is a simple spy that should be called in the then() block of the first.
Below, in the test, you will see two expectations. The first expectation passes. The second does not. i.e. expect(sendSpy).toHaveBeenCalled() passes but expect(sendCallbackSpy).toHaveBeenCalled() does not.
However, when I put the console.log statements in the file (as below), both execute (i.e. the 'before' and 'after'), and if I console log the window.props.onSend it confirms that the mocked function is present. So it looks like the function should be called.
Another thing of note is that my implementation requires me to pass in the callback in a props object from my window. Further, to mock things on the window in Jest, you just mock global as I'm doing below. I don't think that is relevant to the issue but worth pointing out nonetheless.
Could it be that the expectation is run before the actual function is called in the then() block?
export class MyButton extends Component {
handleClick = () => {
this.props.send(this.props.url).then(res => {
console.log('before', window.props.onSend)
window.props.onSend(res.data)
console.log('after')
})
}
render() {
return <button onClick={handleClick} />
}
}
//test
test('it calls the identity function when button is clicked', () => {
const sendSpy = jest.fn(() => { return Promise.resolve({ data: 'hello' }) })
const sendCallbackSpy = jest.fn()
global.props = { onSend: sendCallbackSpy }
wrapper = shallow(<MyButton send={sendSpy} } />)
const button = wrapper.find('button')
button.simulate('click')
expect(sendSpy).toHaveBeenCalled()
expect(sendCallbackSpy).toHaveBeenCalled()
})
You need to wait for the promise before testing the second spy:
test('it calls the identity function when button is clicked', async() => {
const request = Promise.resolve({ data: 'hello' })
const sendSpy = jest.fn(() => request)
const sendCallbackSpy = jest.fn()
global.props = { onSend: sendCallbackSpy }
wrapper = shallow(<MyButton send={sendSpy} } />)
const button = wrapper.find('button')
button.simulate('click')
expect(sendSpy).toHaveBeenCalled()
await request
expect(sendCallbackSpy).toHaveBeenCalled()
})
One of my components uses a setTimeout inside ngOnInit, e.g:
ngOnInit() {
setTimeout(() => {
// do some setup stuff using FormBuilder
}, 100);
}
In the unit tests for this component, I need to spy on one of the methods of one of the controls which are built programmatically using FormBuilder, so I am doing this in a beforeEach:
describe('testing something', () => {
beforeEach(() => {
spyOn(component.form.controls.myControl, 'enable');
});
it('does something', () => {
// test stuff
});
});
Before the timeout was added, the test was working perfectly. How can I make the beforeEach wait for the 100 ms timeout in the ngOnInit method?
I've tried adding async and fakeAsync to the outer describe, e.g. like this:
describe('testing something', <any>fakeAsync(() => {
...
}));
or
describe('testing something', async(() => {
...
}));
But in the first case, with fakeAsync I see a message in the test runner Error: Expected to be running in 'ProxyZone', but it was not found., and in the second case it just doesn't even run the test. I also tried wrapping the it methods in fakeAsync but that doesn't do anything to delay the beforeEach
I've tried wrapping the spyOn method inside the beforeEach in a setTimeout and this doesn't appear to have any effect, i.e. the test fails in the same way.
I've also tried using fakeAsync with the beforeEach, like this:
beforeEach(<any>fakeAsync(() => {
tick(100);
spyOn(component.modelForm.controls.myControl, 'enable');
}));
But this doesn't work either. It doesn't result in any errors, but the method I want to spy on doesn't exist yet, even after the tick.
How can I force the beforeEach to wait for the timeout in ngOnInit()? Is it even possible?
The following solution can be adapted for beforeEach if needed
before(() => {
jasmine.clock().install();
})
after(() => {
jasmine.clock().uninstall();
})
it('test case', () => {
spyOn(component.modelForm.controls.myControl, 'enable');
component.ngOnInit();
var timeout = 2000 // 2 seconds
jasmine.clock().tick(timeout);
expect(component.modelForm.controls.myControl.enable).toHaveBeenCalled()
})
I was able to fix this using Jasmine's done callback inside the beforeEach, like this:
beforeEach((done) => {
component.ngOnInit();
setTimeout(() => {
spyOn(component.modelForm.controls.myControl, 'enable');
done();
}, 100);
});
Seems so intuitive!
I am trying to mock a function call, and expect it to have called another function within it once.
myFunctions.test.js
import { resetModal } from '../myFunctions.js';
describe('resetModal', () => {
it('calls the clearSomethingInModal function', () => {
const clearSomethingInModal = jest.fn();
resetModal();
expect(clearSomethingInModal.mock.calls.length).toBe(1);
})
})
myFunctions.js
export resetModal() {
clearSomethingInModal()
}
However, Jest output says that it has not been called. How can I best do this?
Your approach does not work because you mock clearSomethingInModal only in the context of your test file, so clearSomethingInModal in the myFunctions.js is still the original. The main point is that you can't mock something that is directly created in myFunctions.js. The only thing that you can mock are:
Modules that you import to myFunctions.js, like import clearSomethingInModal from 'clearSomethingInModal';
Callbacks that you pass into your function when calling them from your test;
This makes sense if you think about myFunctions.js as a blackbox, where you can control what goes in, like imports or function arguments, and where you can test what comes out. But you can not test the stuff that happens inside the box.
Here are two example that reflect the 2 points in the list:
myFunctions.test.js
import { resetModal } from '../myFunctions.js';
import clearSomethingInModal from 'clearSomethingInModal';
jest.mock('clearSomethingInModal', () => jest.fn())
describe('resetModal', () => {
it('calls the clearSomethingInModal function', () => {
resetCreationModal();
expect(clearSomethingInModal.mock.calls.length).toBe(1);
})
})
myFunctions.js
import clearSomethingInModal from 'clearSomethingInModal';
export resetModal() {
clearSomethingInModal()
}
myFunctions.test.js
import { resetModal } from '../myFunctions.js';
describe('resetModal', () => {
it('calls the clearSomethingInModal function', () => {
const clearSomethingInModal = jest.fn();
resetCreationModal(clearSomethingInModal);
expect(clearSomethingInModal.mock.calls.length).toBe(1);
})
})
myFunctions.js
export resetModal(clearSomethingInModal) {
clearSomethingInModal()
}
Another way is to use done and mock or spy on the implementation of the last function and check if the previous function was called by then.
it('should call function2 after function1', (done) => {
expect.assertions(2)
function2.mockImplementationOnce(() => {
expect(function1).toHaveBeenCalled()
done()
})
act() // This is where you run function you are testing
})
The drawback of this solution is that the structure of the test is not
// arrange
// act
// assert
but rather
// arrange & assert
// act