Cypress with stripe: elements height not loaded - javascript

I'm using cypress since a week, and I succesfully did an integration with the stripe iframe: I've used this code:
in cypress/support/command.js
Cypress.Commands.add('iframeLoaded', { prevSubject: 'element' }, $iframe => {
const contentWindow = $iframe.prop('contentWindow')
return new Promise(resolve => {
if (contentWindow && contentWindow.document.readyState === 'complete') {
resolve(contentWindow)
} else {
$iframe.on('load', () => {
resolve(contentWindow)
})
}
})
})
Cypress.Commands.add('getInDocument', { prevSubject: 'document' }, (document, selector) =>
Cypress.$(selector, document),
)
In cypress/integration/staging-web/web.test.js
cy.get('iframe')
.eq(1)
.iframeLoaded()
.its('document')
.getInDocument('[name="cardnumber"]')
.then($iframe => {
if ($iframe.is(':visible')) {
$iframe.type('4242424242424242')
}
})
cy.get('iframe')
.eq(1)
.iframeLoaded()
.its('document')
.getInDocument('[name="exp-date"]')
.then($iframe => {
if ($iframe.is(':visible')) {
$iframe.type('1225')
}
})
cy.get('iframe')
.eq(2)
.iframeLoaded()
.its('document')
.getInDocument('[name="cvc"]')
.then($iframe => {
if ($iframe.is(':visible')) {
$iframe.type('123')
}
})
cy.get('.mt1 > .relative > .input').type('utente')
My problem is that during the page loading, cypress does not wait until stripe fields are fully loaded, and I get an error because happens this (sorry for not-english language, but it's a screenshot):
Those lines are:
cardnumber
expiration date , pin number
4th line is card owner
I've tried with .should('be.visibile') but it does nothing; plus I've tried with
cy.get('iframe')
.eq(1)
.iframeLoaded()
.its('document')
.getInDocument('[name="cardnumber"]')
.then($iframe => {
if ($iframe.is(':visible')) {
$iframe.type('4242424242424242')
}
})
but no way, it always gives me an error; in this latter case, it doesn't even give an error, it just goes on without filling the fields and after this it stops because it cant go on in the test.
If I add cy.wait(800) before the code in web.test.js it works fine, but I don't want to use wait, because it's basically wrong (what happens if it loads after 5 seconds?).
is there a way to check that those elements must have an height?
Remember that they are in an iframe (sadly).

If I add cy.wait(800) ... it works fine.
This is because you are not using Cypress commands with auto-retry inside getInDocument().
Cypress.$(selector) is jQuery, it just attempts to grab the element, not retry for async loading, and no test failure when not found.
Should use a proper command with retry, like
Cypress.Commands.add('getInDocument', { prevSubject: 'document' }, (document, selector) =>
cy.wrap(document).find(selector)
)
or you might need to work from body
Cypress.Commands.add('getInDocument', { prevSubject: 'document' }, (document, selector) =>
cy.wrap(document).its('body')
.find(selector)
.should('be.visible')
)
Without a test system I'm not sure exactly which one is correct syntax, but you get the idea.
Also, too many custom commands. You always follow .iframeLoaded() with .its('document'), so just wrap it all up in iframeLoaded custom command.
In fact, resolve(contentWindow.document.body) because it's a better point to chain .find(selector).
This is my test against the Stripe demo page,
Cypress.Commands.add('iframeLoaded', { prevSubject: 'element' }, $iframe => {
const contentWindow = $iframe.prop('contentWindow')
return new Promise(resolve => {
if (contentWindow && contentWindow.document.readyState === 'complete') {
resolve(contentWindow.document.body)
} else {
$iframe.on('load', () => {
resolve(contentWindow.document.body)
})
}
})
})
it('finds card number', () => {
cy.viewport(1000, 1000)
cy.visit('https://stripe-payments-demo.appspot.com/')
cy.get('iframe')
.should('have.length.gt', 1) // sometimes 2nd iframe is slow to load
.eq(1)
.iframeLoaded()
.find('input[name="cardnumber"]')
.should('be.visible')
})

#Nanker Phelge by the way I updated my function in this way, I posted it here because there was no space in comments:
UPDATE: I put waitForMillisecs inside the function -_- it HAVE TO stay outside! Correction made.
const ADD_MS = 200
const STACK_LIMIT = 15
let stackNumber = 0
let waitForMillisecs = 500
function checkAgainStripe() {
stackNumber++
if (stackNumber >= STACK_LIMIT) {
throw new Error('Error: too many tries on Stripe iframe. Limit reached is', STACK_LIMIT)
}
cy.get('.btn').scrollIntoView().should('be.visible').click()
cy.get('iframe')
.eq(1)
.then($elem => {
//console.log($elem[0].offsetHeight)
//console.log($elem[0])
cy.log('now wait for ' + waitForMillisecs + '; stack number is ' + stackNumber)
cy.wait(waitForMillisecs)
waitForMillisecs = waitForMillisecs + ADD_MS
if (!$elem[0] || !$elem[0].offsetHeight || $elem[0].offsetHeight < 19) {
console.log('entered in if')
cy.reload()
return checkAgainStripe()
}
})
}

Related

Event listener firing numerous time

I have the following code which is firing Event a lot of times and I want it to fire one time each time the modal appears, the gist of the code is to wait for Modal to be a part of DOM after which I run the function hence setTimeout to override the Modal classes. After which I have Event which is firing numerous times.
If I use clearInterval, my CSS isn't applied
setInterval(() => {
if (document.querySelector('[data-test-id="modal"]')) {
poller(['[data-test-id="modal"]'], () => {
let msgNode = document.querySelectorAll('[class*="AvailabilityMessagesstyled__"]');
[...msgNode].forEach((delivery) => {
setTimeout(() => {
if (delivery.children[0].innerText.indexOf("Available") !== -1 ||
delivery.children[0].innerText.indexOf("available") !== -1) {
delivery.children[0].classList.add("TP")
}
if (delivery.children[0].innerText.indexOf("Unavailable") !== -1) {
delivery.children[0].classList.add("TP__red")
}
}, 200)
// qty
let qtyCounter = Array.from(document.querySelectorAll('[data-test-id="qty-selector"]'));
console.log(qtyCounter);
[...qtyCounter].forEach((counter) => {
counter.addEventListener("click", function() {
console.log('clicked')
})
})
});
})
}
}, 2000)
I am not sure what I am doing wrong?

Puppeteer - Click a button only if exists

I'm trying to create a webscraper in Node, using Puppeteer.
My first challange (which I thought would be easy), it's pass by a pagination "Load more" button.
But, when I run the following code, Puppeteer click on all "Load more" and after click on a content, when I need to stop clicking.
Why this happens?
let loadMore = true;
while (loadMore) {
selector = 'ul.pager > li > a.button';
await page.waitForSelector(selector, { timeout: 600 }).then(() => {
page.click(selector);
}).catch(() => {
loadMore = false;
});
}
Thx all!
I would do it like this.
I think timeout doesn't make a difference here, if the await inside the if's condition is not enough then there could be other problem with the script.
let loadMore = true;
while (loadMore) {
const selector = 'ul.pager > li > a.button';
if ((await page.$(selector)) !== null) {
await page.click(selector);
} else {
loadMore = false;
}
}

ScrollIntoView() doesn't work - vanilla JS, why?

My idea is to have a simple search function, where people can type text in a search box and see if there is anything matched with the text in the html. If it does, the matched part(let's say it might be in later half of the whole HTML page that's not seenable on your current screen/viewport) will jump/scroll into view. If it doesn't, you get "no match found" message in the #feedback tag.
HTML looks like this:
<input id="search-text">
<p id="feedback"></p>
<p>title1</p>
<p>title2</p>
<p>title3</p>
JS is like this:
let searchText = ''
const titlesNodes = document.querySelectorAll('p')
const titles = Array.from(titlesNodes)
document.querySelector('#search-text').addEventListener('input', (e) => {
searchText = e.target.value
searchFunction()
})
const searchFunction = () => {
const filteredTitles = titles.filter((title) => title.innerText.toLowerCase().includes(searchText.toLowerCase()) )
const msg = document.querySelector('#feedback')
msg.innerHTML = ''
if (filteredTitles.length > 0){
filteredTitles[0].scrollIntoView()
} else {
msg.textContent = 'No match found'
}
}
So far I'm able to get the "no matched found" but when I type something that's only in the later half of the page, it doesn't jump to the view. I tested on both Chromium and Firefox and I use Linux.
What is wrong with my code? Thanks!
This is because by default, browser will set your <input> element into screen when you type in. So your own call is overridden by browser's default one.
Not sure what's the best way to avoid that though...
You may try to call scrollIntoView after a small timeout, so it occurs after the one of the input. If you use requestAnimationFrame timing function, it should occur just before the next paint, so you shouldn't notice the one of the input occurred.
let searchText = '';
const titlesNodes = document.querySelectorAll('p')
const titles = Array.from(titlesNodes)
document.querySelector('#search-text').addEventListener('input', (e) => {
searchText = e.target.value
searchFunction()
})
const searchFunction = () => {
const filteredTitles = titles.filter((title) => title.innerText.toLowerCase().includes(searchText.toLowerCase()))
const msg = document.querySelector('#feedback')
msg.innerHTML = ''
if (filteredTitles.length > 0) {
requestAnimationFrame(() => { // wait just a bit
filteredTitles[0].scrollIntoView()
});
} else {
msg.textContent = 'No match found'
}
}
#feedback { margin-bottom: 125vh }
p { margin-bottom: 50vh }
<input id="search-text">
<p id="feedback"></p>
<p>title1</p>
<p>title2</p>
<p>title3</p>
Try changing:
- filteredTitles[0].scrollIntoView()
+ filteredTitles[0].scrollIntoView({alignToTop: true})
document.querySelector('#search-text').addEventListener('input', (e) => {
searchText = e.target.value
- searchFunction()
+ setTimeout(searchFunction, 500)})
})

Set time out event on a m() generated element

I have a m("p.help") element which is removed with a click event.
I also want the element to be removed automatically after a few seconds if not clicked. I need to set a time out on it. Setting time out does not work.
function help() {
var text = `This is a service template. Use Service section to set the schedule`;
var node = m("p.help", {
onclick() {
this.parentNode.removeChild(this);
},
}, text);
setTimeout(() => {
if (node.parentNode) {
node.parentNode.removeChild(node);
console.log("removed");
m.redraw();
}
}, 5000);
return node;
}
The click event works fine but the time out does not work. It is not even triggered judging by the console.log()
What am I doing wrong?
EDIT
Thanks ciscoheat for the tip.
I had to put the timer in the controller for this to work.
So this one works fine:
function controller(init) {
this.display = {
help: true
};
setTimeout(() => {
this.display.help = false;
m.redraw();
}, 5000);
}
function view(vm) {
return m(".container", [
(() => {
var text = "Some text";
if (vm.display.help) {
return m("p.memo", {
onclick() {
this.parentNode.removeChild(this);
}
}, text);
}
})(),
]);
}
To use Mithril correctly, you should avoid DOM manipulation, leaving that to Mithril's fast diff algorithm.
Use a state variable instead, related to displaying the help paragraph that will be changed automatically after 5 seconds.
Here's a jsbin showing what I mean: http://jsbin.com/kixece/edit?html,js,output

event for file upload cancel in javascript

We can use file.onchange if we gonna set an event callback for file reading using javascript, but how to set event for when user cancel the upload (close the browse panel)?
There is no API for the file input modal. Besides, if the user closes the browser your code won't be running anymore, will it?
Of course there is the window.onunload method which allows you to detect the example you give.
Per the comments, the best thing I can come up with that would be helpful is that if nothing is selected, file.value.length will be 0.
That’s a great solution:
const createUpload = () => {
let lock = false
return new Promise((resolve, reject) => {
const el = document.createElement('input');
el.id = +new Date();
el.style.display = 'none';
el.setAttribute('type', 'file');
document.body.appendChild(el)
el.addEventListener('change', () => {
lock = true;
const file = el.files[0];
resolve(file)
document.body.removeChild(document.getElementById(el.id));
}, { once: true })
window.addEventListener('focus', () => { // file blur
setTimeout(() => {
if (!lock && document.getElementById(el.id)) {
reject(new Error('onblur'))
document.body.removeChild(document.getElementById(el.id))
}
}, 300)
}, { once: true })
el.click() // open file select box
})
}
Ussage:
// $label.classList.add('loading');
createUpload()
.then(function (file) {
// Sent file
// $label.classList.remove('loading');
})
.catch(function (err) {
// Your Cancel Event
// $label.classList.remove('loading');
});
It is very simple with jQuery :
$("#fileInputId").change(function () {
//implement your code here
});

Categories

Resources