App in cypress redirects, outside does not - javascript

I am trying to write end to end tests for this application with Cypress: https://app.gotphoto.com/admin/auth/login
When I visit the above url from my browswer, a login form is showing, as expected.
When I visit the above url through Cypress:
cypress first navigates to https://app.gotphoto.com/admin/auth/login
immediately afterwards I am redirected to https://app.gotphoto.com/__/ and the login form is not showing
These are two screenshots from inside Cypress:
My question is: why is there a difference between how it runs in my browser and how it runs in Cypress / Cypress's browswer?
The browswer I am using is Chrome 89, both when running with and without Cypress.
The entirety of the test I am running is this:
describe('login screen', () => {
it('logs in', () => {
cy.visit('/admin/auth/login');
});
});
with a cypress.json:
{
"baseUrl": "https://app.gotphoto.com"
}
I created a repo with the above configuration so it's simple to reproduce.

The /__/ portion of https://app.gotphoto.com/__/ is called the clientRoute and is an internal configuration item in Cypress.
You can turn it off in your cypress.json configuration file
{
...
"clientRoute": "/"
}
This effectively keeps your original url and allows the page to load properly.
cy.visit('https://app.gotphoto.com/admin/auth/login')
cy.get('input#username', { timeout: 10000 }).type('admin') // long timeout
// wait for page to load
cy.get('input#password').type('password')
cy.intercept('POST', 'api.getphoto.io/v4/auth/login/user').as('user')
cy.contains('button', 'Submit').click()
cy.wait('#user').then(interception => {
// incorrect credentials
expect(interception.response.body.detail).to.eq('Login failed!')
})
I'm not sure of any bad side effects of changing clientRoute, will post more information if I find it.

That redirect to __/ sounds familiar to an issue I stumbled upon some time ago. I found this comment in one of Cypress' issues quite helpful.
So did you already try to use the configuration option experimentalSourceRewriting? In your cypress.json, it may look like this:
{
"baseUrl": "https://app.gotphoto.com"
"experimentalSourceRewriting": true
}
As it's labelled experimental, I'd recommend testing it carefully but maybe it helps a bit. I hope for the best! 🙏

why is there a difference between how it runs in my browser and how it runs in Cypress / Cypress's browser?
Your normal browser waits for the XHR requests to be completed and renders the final output created by whatever js magic you have written in there but cy.visit is not supposed to wait for those XHR / AJAX requests inside. It gets 200 in response and moves ahead. If you add a cypress command next to cy.visit, something like cy.get('h1'), you will notice that this command runs instantly after cy.visit, and after that, your XHR requests are resolved.
One work around here can be to use cy.intercept, for example (Cypress 6.8.0, Chrome 89):
describe("login screen", () => {
it("logs in", () => {
cy.intercept({
method: "GET",
url: "admin/version/master/index.html"
}).as("indexHTML"); // Similarly add other internal xhr requests
cy.visit("/admin/auth/login");
cy.wait("#indexHTML").then(interception => {
expect(interception.response.statusCode).to.be.eq(200);
});
});
});
Output:
It basically waits for your internal XHR requests to finish and allows you to play with the request and responses once they are resolved.
This issue will help you debug further: https://github.com/cypress-io/cypress/issues/4383
Also, this /__/ has no hand in rendering the blank page IMO.

An example of logging in. Ultimately this is a bit of a hacky solution as it fails on the very first try; however, it works on any subsequent attempt.
Add the following to your command.js
// -- Visit multiple domains in one test
Cypress.Commands.add('forceVisit', url => {
cy.window().then(win => {
return win.open(url, '_self');
});
});
login.spec.js
describe('login screen', () => {
it('logs in', {
retries: {
runMode: 1,
openMode: 1
}
}, () => {
cy.forceVisit('https://app.gotphoto.com/admin/auth/login');
cy.get('#username').should('exist');
});
});
Screenshot:

Related

How to make puppeteer do a javascript function

So I am trying to make puppeteer lunch a page and then put a token inside a local storage.
.setItem not working it is just crushing my chromium.
so there is a page called discord
and if you have a user token you can log in to the page with a script
So I found out that someone has made a script that you can past in the console
and then when the code says "token here" you past your token and then it all happens
let token = "your token";
function login(token) {
setInterval(() => {
document.body.appendChild(document.createElement `iframe`).contentWindow.localStorage.token = `"${token}"`
}, 50);
setTimeout(() => {
location.reload();
}, 2500);
}
login(token);
This is the code.
so my idea is to make puppeteer run this code and then just login to the page after refresh
there is any option to do it?
If there is no option I have though about another solution,
maybe make puppeteer type in the console the whole code.
If you want to execute the JavaScript code in the browser context with Puppeteer, you need an evaluate method. For reference: https://github.com/puppeteer/puppeteer/blob/v10.4.0/docs/api.md#pageevaluatepagefunction-args or Google for similar and more user-friendly examples - there are plenty on web and SO.
Basically, to execute any JS code in a browser context, you put your code inside an evaluate method and it should look like this:
await page.evaluate(() => new Promise((resolve) => {
// your browser JS code goes here
}
As for the cookies part, they should persist even after reload (haven't tested, but check this for reference: Cookies gone after reload Puppeteer => page.setCookie(...cookies)).
Also, maybe unrelated, but be careful that everything is alright legal-wise, because bots and bot-like behavior is frowned upon by many sites and in breach of their ToS.

How to waitFor when page refreshes in Puppeteer?

I have an app I'm working with that is behaving like this... You visit a url /refresh, and it loads the page with a loader/spinner/bar showing for like 5 seconds, then it refreshes the page after it's done. It does this so it can load the latest data that was computed during /refresh.
Right now I am just setting a timeout longer than the loader will most likely stay around, but this is brittle because a bad network connection could put it over the line.
How can I instead "watch" for when the refresh happens? What technique would you recommend. It seems to start to get hairy pretty fast.
Into the nitty gritty, when the loader is showing, when it finishes it is gone for like a half a second before the page reload. So I can't just wait til the loader is gone. It seems like I need to keep some sort of state variable around in the DOM like in localStorage, but can't pinpoint it. Would love some help.
well you could "watch" for the element that display the data using page.$(selector), or if no such element you could also wait for the specific request 's response:
const waitForResponse = (page, url) => {
return new Promise(resolve => {
page.on("response", function callback(response){
if (response.url() === url) {
resolve(response);
page.removeListener("response",callback)
}
})
})
};
const res = await waitForResponse(page,"url of the request you want to wait for");
Wait for Network request before continuing process

Cypress - How to wait for XHR request

I'm a beginner to Cypress. I'm sure it is a simple question and I already read the documentation of Cypress, but something still seems to wrong in my Cypress test. I want to wait for an xhr request to be finished, when I click on a different language of the page I want to test.
It works, when I use wait(5000), but I think, there is a better way to wait for the xhr request to be finished than fix wait 5 secs.
This is my code:
describe('test',() => {
it('should open homepage, page "history", click on English language, click on German language',() => {
cy.server();
cy.route('POST','/ajax.php').as('request');
cy.visit('http://localhost:1234/history');
cy.wait('#request');
cy.get('div[class="cursorPointer flagSelect flag-icon-gb"]').click({force:true});
cy.route('POST','/ajax.php').as('request');
cy.wait(['#request']);
//cy.wait(5000); // <- this works, but seems to be not the best way
cy.get('h2').should(($res) => {
expect($res).to.contain('History');
})
cy.get('.dataContainer').find('.container').should('have.length', 8);
});
});
The last check
cy.get('.dataContainer').find('.container').should('have.length', 8);
is not successful, because the xhr request is not yet finished.
The xhr request is being fired, when the click on the icon is done:
cy.get('div[class="cursorPointer flagSelect flag-icon-gb"]').click({force:true});
Here an image of the xhr request, if that helps to find the error:
Are you sure that this line is correct? Otherwise the cy.wait won't function as you want.
cy.route('POST','/ajax.php').as('request');
I expect something like
cy.route('GET','/endpoint').as('request');
You can lookup what route is it via developer tools (F12 in Chrome).
Go to network to monitor what kind of XHRs load when you open your page.
Find out request URL and Method - example with bing.com
Also:
I prefer to include the cy.server() and cy.route() command in the beforeEach.
Then you only need the cy.wait() in the test itself.
See https://docs.cypress.io/guides/references/best-practices.html#2-Run-shared-code-before-each-test for more information about that.
you should do like that:
describe('test',() => { //no here async mode
it('should open homepage, page "history", click on English language, click on German language', async () => { //but here
cy.server();
cy.route('POST','/ajax.php').as('request').as('requestToWait); // as-construction
const requestToWait = await cy.wait('#requestToWait');//here we are waiting and getting response object
// any other code
});

Chrome requests taking much longer than other browser requests

One of the users of my website noticed a severe delay in loading times for a infinite scroll function which makes a GET request to my API. While testing it, it only seems to happen in Chrome and Opera, the other browsers provide a near seamless experience.
So after some testing, I decided to do some time logging (sorry I can't post inline images on here due to too little reputation):
These logs are from the same calls, and as you can see chrome takes about 3000ms while firefox is around 90ms. IE/Edge have the same performance as Firefox.
To further the mystery, the network tab seems to report the ms correctly (around 100ms with some exceptions, but nowhere near 3000ms):
Now for the actual code I'm calling:
loadMore() {
console.time('start load');
//some extra stuff here
console.timeEnd('start load');
console.time('state api call');
this.$APIService.getCards(this.deckId, this.page, this.searchInput, this.lastId)
.then(res => {
console.timeEnd('actual call');
console.timeEnd('state api call');
//some more stuff here
})
}
the "this.$APIService.getCards" call is just my apihandler:
getCards (deckId, page, searchInput,lastId) {
console.time('actual call');
return Api().get('cards?deckId=' + deckId + '&lastId='+lastId + '&search='+ searchInput)
},
where "Api()" is my axios object:
axios.create({
withCredentials: true,
baseURL: baseURL
})
Another thing to note is that my server only receives the request after the delay, so that would correspond with the network tabs stating it only takes around 100ms.
So, does anyone have any idea of why this might be happening? As you can see I literally have the time logs start right before the request and end them right upon receiving a result. Why would there be a delay before executing the call only in Chrome?
Thanks in advance!

Re-using same instance again webdriverJS

I am really new to Selenium. I managed to open a website using the below nodejs code
var webdriver = require('selenium-webdriver');
var driver = new webdriver.Builder()
.forBrowser('chrome')
.build();
console.log(driver);
driver.get('https://web.whatsapp.com');
//perform all other operations here.
https://web.whatsapp.com is opened and I manually scan a QR code and log in. Now I have different javascript files to perform actions like delete, clear chat inside web.whatsapp.com etc...
Now If I get some error, I debug and when I run the script again using node test.js, it takes another 2 minutes to load page and do the steps I needed. I just wanted to reopen the already opened tab and continue my script instead new window opens.
Edit day 2 : Still searching for solution. I tried below code to save object and reuse it.. Is this the correct approach ? I get a JSON parse error though.
var o = new chrome.Options();
o.addArguments("user-data-dir=/Users/vishnu/Library/Application Support/Google/Chrome/Profile 2");
o.addArguments("disable-infobars");
o.addArguments("--no-first-run");
var driver = new webdriver.Builder().withCapabilities(webdriver.Capabilities.chrome()).setChromeOptions(o).build();
var savefile = fs.writeFile('data.json', JSON.stringify(util.inspect(driver)) , 'utf-8');
var parsedJSON = require('./data.json');
console.log(parsedJSON);
It took me some time and a couple of different approaches, but I managed to work up something I think solves your problem and allows to develop tests in a rather nice way.
Because it does not directly answer the question of how to re-use a browser session in Selenium (using their JavaScript API), I will first present my proposed solution and then briefly discuss the other approaches I tried. It may give someone else an idea and help them to solve this problem in a nicer/better way. Who knows. At least my attempts will be documented.
Proposed solution (tested and works)
Because I did not manage to actually reuse a browser session (see below), I figured I could try something else. The approach will be the following.
Idea
Have a main loop in one file (say init.js) and tests in a separate file (test.js).
The main loop opens a browser instance and keeps it open. It also exposes some sort of CLI that allows one to run tests (from test.js), inspect errors as they occur and to close the browser instance and stop the main loop.
The test in test.js exports a test function that is being executed by the main loop. It is passed a driver instance to work with. Any errors that occur here are being caught by the main loop.
Because the browser instance is opened only once, we have to do the manual process of authenticating with WhatsApp (scanning a QR code) only once. After that, running a test will reload web.whatsapp.com, but it will have remembered that we authenticated and thus immediately be able to run whatever tests we define in test.js.
In order to keep the main loop alive, it is vital that we catch each and every error that might occur in our tests. I unfortunately had to resort to uncaughtException for that.
Implementation
This is the implementation of the above idea I came up with. It is possible to make this much fancier if you would want to do so. I went for simplicity here (hope I managed).
init.js
This is the main loop from the above idea.
var webdriver = require('selenium-webdriver'),
by = webdriver.By,
until = webdriver.until,
driver = null,
prompt = '> ',
testPath = 'test.js',
lastError = null;
function initDriver() {
return new Promise((resolve, reject) => {
// already opened a browser? done
if (driver !== null) {
resolve();
return;
}
// open a new browser, let user scan QR code
driver = new webdriver.Builder().forBrowser('chrome').build();
driver.get('https://web.whatsapp.com');
process.stdout.write("Please scan the QR code within 30 seconds...\n");
driver.wait(until.elementLocated(by.className('chat')), 30000)
.then(() => resolve())
.catch((timeout) => {
process.stdout.write("\b\bTimed out waiting for code to" +
" be scanned.\n");
driver.quit();
reject();
});
});
}
function recordError(err) {
process.stderr.write(err.name + ': ' + err.message + "\n");
lastError = err;
// let user know that test failed
process.stdout.write("Test failed!\n");
// indicate we are ready to read the next command
process.stdout.write(prompt);
}
process.stdout.write(prompt);
process.stdin.setEncoding('utf8');
process.stdin.on('readable', () => {
var chunk = process.stdin.read();
if (chunk === null) {
// happens on initialization, ignore
return;
}
// do various different things for different commands
var line = chunk.trim(),
cmds = line.split(/\s+/);
switch (cmds[0]) {
case 'error':
// print last error, when applicable
if (lastError !== null) {
console.log(lastError);
}
// indicate we are ready to read the next command
process.stdout.write(prompt);
break;
case 'run':
// open a browser if we didn't yet, execute tests
initDriver().then(() => {
// carefully load test code, report SyntaxError when applicable
var file = (cmds.length === 1 ? testPath : cmds[1] + '.js');
try {
var test = require('./' + file);
} catch (err) {
recordError(err);
return;
} finally {
// force node to read the test code again when we
// require it in the future
delete require.cache[__dirname + '/' + file];
}
// carefully execute tests, report errors when applicable
test.execute(driver, by, until)
.then(() => {
// indicate we are ready to read the next command
process.stdout.write(prompt);
})
.catch(recordError);
}).catch(() => process.stdin.destroy());
break;
case 'quit':
// close browser if it was opened and stop this process
if (driver !== null) {
driver.quit();
}
process.stdin.destroy();
return;
}
});
// some errors somehow still escape all catches we have...
process.on('uncaughtException', recordError);
test.js
This is the test from the above idea. I wrote some things just to test the main loop and some WebDriver functionality. Pretty much anything is possible here. I have used promises to make test execution work nicely with the main loop.
var driver, by, until,
timeout = 5000;
function waitAndClickElement(selector, index = 0) {
driver.wait(until.elementLocated(by.css(selector)), timeout)
.then(() => {
driver.findElements(by.css(selector)).then((els) => {
var element = els[index];
driver.wait(until.elementIsVisible(element), timeout);
element.click();
});
});
}
exports.execute = function(d, b, u) {
// make globally accessible for ease of use
driver = d;
by = b;
until = u;
// actual test as a promise
return new Promise((resolve, reject) => {
// open site
driver.get('https://web.whatsapp.com');
// make sure it loads fine
driver.wait(until.elementLocated(by.className('chat')), timeout);
driver.wait(until.elementIsVisible(
driver.findElement(by.className('chat'))), timeout);
// open menu
waitAndClickElement('.icon.icon-menu');
// click profile link
waitAndClickElement('.menu-shortcut', 1);
// give profile time to animate
// this prevents an error from occurring when we try to click the close
// button while it is still being animated (workaround/hack!)
driver.sleep(500);
// close profile
waitAndClickElement('.btn-close-drawer');
driver.sleep(500); // same for hiding profile
// click some chat
waitAndClickElement('.chat', 3);
// let main script know we are done successfully
// we do so after all other webdriver promise have resolved by creating
// another webdriver promise and hooking into its resolve
driver.wait(until.elementLocated(by.className('chat')), timeout)
.then(() => resolve());
});
};
Example output
Here is some example output. The first invocation of run test will open up an instance of Chrome. Other invocations will use that same instance. When an error occurs, it can be inspected as shown. Executing quit will close the browser instance and quit the main loop.
$ node init.js
> run test
> run test
WebDriverError: unknown error: Element <div class="chat">...</div> is not clickable at point (163, 432). Other element would receive the click: <div dir="auto" contenteditable="false" class="input input-text">...</div>
(Session info: chrome=57.0.2987.133)
(Driver info: chromedriver=2.29.461571 (8a88bbe0775e2a23afda0ceaf2ef7ee74e822cc5),platform=Linux 4.9.0-2-amd64 x86_64)
Test failed!
> error
<prints complete stacktrace>
> run test
> quit
You can run tests in other files by simply calling them. Say you have a file test-foo.js, then execute run test-foo in the above prompt to run it. All tests will share the same Chrome instance.
Failed attempt #1: saving and restoring storage
When inspecting the page using my development tools, I noticed that it appears to use the localStorage. It is possible to export this as JSON and write it to a file. On a next invocation, this file can be read, parsed and written to the new browser instance storage before reloading the page.
Unfortunately, WhatsApp still required me to scan the QR code. I have tried to figure out what I missed (cookies, sessionStorage, ...), but did not manage. It is possible that WhatsApp registers the browser as being disconnected after some time has passed. Or that it uses other browser properties (session ID?) to recognize the browser. This is pure speculating from my side though.
Failed attempt #2: switching session/window
Every browser instance started via WebDriver has a session ID. This ID can be retrieved, so I figured it may be possible to start a session and then connect to it from the test cases, which would then be run from a separate file (you can see this is the predecessor of the final solution). Unfortunately, I have not been able to figure out a way to set the session ID. This may actually be a security concern, I am not sure. People more expert in the usage of WebDriver might be able to clarify here.
I did find out that it is possible to retrieve a list of window handles and switch between them. Unfortunately, windows are only shared within a single session and not across sessions.

Categories

Resources