My use case is that I have a UI which prevents operation B unless operation A happenned in the previous calendar month (accounting lifecycle madness - does not matter).
I am writing a test that verifies operation B is working correctly, but I cannot perform operation B is working unless I have operation A that occured last month. My protractor setup runs on top of real APIs, and mocking data is not an option that I will consider (granted if I chose to mock API responses this would be easier).
The APIs do not allow me to manipulate the created/update dates of operation A, so my options to test this scenario are to manipulate the underlying DB or to push the browser into the next month while testing. Assuming I want to take the browser approach, how can I get this working with protractor?
Once I have a mechanism to manipulate browser time, then the solution I chose was to start a scenario, perform operation A, advance the browser time to one month in the future, then perform operation B.
This involved using protractors browser.executeScript
to run JS in the browser context, and override the JS Date object using the timeshift-js module.
Here is the code
This code is using:
- angular: 1.5.10
- protractor: 5.1.2
- cucumber-js: 1.3.3
- timeshift-js: 1.0.1
I can write scenarios like this:
Feature: Travelling in time
Scenario: Scenario that manipulates time
When I view some page
And I check what time it is
And I do operation A
And I advance the browser by 1 days
And I check what time it is
Then I can do operation B
Scenario: Scenario Double Check that the next scenario gets the correct time
When I view the list of maintenance requests
And I check what time it is
And here is the step implementation. There is nothing here cucumber specific so it should be easy to adapt to a jasmine or mocha based describe/it framework:
const timeShiftLibraryString = fs.readFileSync(`path/to/node_modules/timeshift-js/timeshift.js`, 'utf-8')
module.exports = function () {
this.When(/I advance the browser by (-?[0-9]+) (day|month)s?$/, function (offset, unit) {
const now = moment()
const future = moment().add(offset, unit)
const amountOfMillisecondsToAdvanceBrowser = (future.unix() - now.unix()) * 1000
const tolerance = 15 * 1000
const advanceTime = function (futureOffsetInMilliseconds, timeShiftLibraryString) {
// these two lines are only necessary because I dont want Timeshift in my production code, only during test
const timeshiftLibrary = new Function(timeShiftLibraryString)
timeshiftLibrary.call(window)
Date = window.TimeShift.Date
window.TimeShift.setTime(Date.now() + futureOffsetInMilliseconds)
return Date.now()
}
return browser.executeScript(advanceTime, amountOfMillisecondsToAdvanceBrowser, timeShiftLibraryString).then((browserTime) => {
const expectedTime = Date.now() + amountOfMillisecondsToAdvanceBrowser - tolerance
this.logger.debug(`Time manipulation complete: browserTime = ${moment(browserTime)}`)
if (browserTime >= expectedTime) {
return Promise.resolve(browserTime)
}
return Promise.reject(new Error(`advanceTime did not work: reported browserTime: ${browserTime}. Expected Time: ${expectedTime}`))
})
})
this.When(/I check what time it is/, function () {
const whatTimeIsItInTheBrowser = function () {
return Date.now()
}
return browser.executeScript(whatTimeIsItInTheBrowser).then((browserTime) => {
console.log(`Browser Time = ${moment(browserTime)}`)
})
})
}
Considerations:
- the tricky part is serialising the timeshift-js library. I did not want to package it with my app, so that meant I had to inject it on demand during test
- you will not see the browser logs unless you explicitly go and get them using https://github.com/angular/protractor/blob/master/docs/faq.md#how-can-i-get-hold-of-the-browsers-console
- you cannot manipulate the browser time until you have done at least one page get, the browser needs to have loaded some HTML and JS before you can manipulate Date in the browser JS context