Cypress's visible matcher treats an element as visible based on a variety of factors, however it doesn't take the viewport into account, so an element that is scrolled off-screen is still treated as visible.
I need to test that a link to an on-page anchor is functioning correctly. Once the link is clicked, the page scrolls to the element with the id as defined in the href of the link (example/#some-id).
How can verify that the element is within the viewport?
I've cobbled together the following commands which appear to work so far, but amazed there isn't on out-of-box solution:
Cypress.Commands.add('topIsWithinViewport', { prevSubject: true }, subject => {
const windowInnerWidth = Cypress.config(`viewportWidth`);
const bounding = subject[0].getBoundingClientRect();
const rightBoundOfWindow = windowInnerWidth;
expect(bounding.top).to.be.at.least(0);
expect(bounding.left).to.be.at.least(0);
expect(bounding.right).to.be.lessThan(rightBoundOfWindow);
return subject;
})
Cypress.Commands.add('isWithinViewport', { prevSubject: true }, subject => {
const windowInnerWidth = Cypress.config(`viewportWidth`);
const windowInnerHeight = Cypress.config(`viewportHeight`);
const bounding = subject[0].getBoundingClientRect();
const rightBoundOfWindow = windowInnerWidth;
const bottomBoundOfWindow = windowInnerHeight;
expect(bounding.top).to.be.at.least(0);
expect(bounding.left).to.be.at.least(0);
expect(bounding.right).to.be.lessThan(rightBoundOfWindow);
expect(bounding.bottom).to.be.lessThan(bottomBoundOfWindow);
return subject;
})
I did a little refactoring on Undistracted's approach if anyone is interested:
Cypress.Commands.add('isWithinViewport', { prevSubject: true }, (subject) => {
const rect = subject[0].getBoundingClientRect();
expect(rect.top).to.be.within(0, window.innerHeight);
expect(rect.right).to.be.within(0, window.innerWidth);
expect(rect.bottom).to.be.within(0, window.innerHeight);
expect(rect.left).to.be.within(0, window.innerWidth);
return subject;
});
Cypress.Commands.add('isOutsideViewport', { prevSubject: true }, (subject) => {
const rect = subject[0].getBoundingClientRect();
expect(rect.top).not.to.be.within(0, window.innerHeight);
expect(rect.right).not.to.be.within(0, window.innerWidth);
expect(rect.bottom).not.to.be.within(0, window.innerHeight);
expect(rect.left).not.to.be.within(0, window.innerWidth);
return subject;
});
Uses window.innerWidth and window.innerHeight in case you have used cy.viewport before calling. Also uses .within to facilitate the outside addition.
Related
I am trying to create a function that starts out with a report button and, if you click it, the page will redirect you to where you can type some words into the text box and send or you have a cancel button. If you type in a comment and press the send button then whatever you typed will show up. However, if you click the cancel button it will return you to a page that just has the report button.
While I am certain my code so far is incorrect, my main question is why doesn't anything happen after I click the report button? I based my code off this post.
Here is my code:
let reportButton;
let cancelButton;
let reporting;
let button;
//let input;
const scenes = {
report: () => {
cancelButton?.remove();
// initialize idle state
reporting = false;
reportButton = createButton("Report");
reportButton.position(5, 5);
reportButton.mousePressed = () => {
scenes.reportComment();
};
draw = () => {
clear();
};
},
reportComment: () => {
reportButton?.remove();
reporting = true;
cancelButton = createButton("Cancel");
cancelButton.position(5, 5);
cancelButton.mousePressed = () => {
scenes.report();
};
draw = () => {
clear();
input = createInput();
input.position(20, 65);
button = createButton('Submit');
button.position(input.x + input.width, 65);
button.mousePressed(sendReport);
question = createElement('h2', 'Report');
question.position(20, 5);
textAlign(CENTER);
textSize(50);
};
},
};
function sendReport() {
const customQ = input.value();
question.html(customQ);
input.value('');
}
function setup() {
createCanvas(500, 100);
textSize(20);
}
function draw() {
scenes.report();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.5.0/p5.js"></script>
Your code (overwriting p5's mousePressed method with a new, unrelated function):
reportButton.mousePressed = () => {
scenes.reportComment();
};
Correct code (calling p5's mousePressed method, passing in a callback that represents the handler to trigger on press):
reportButton.mousePressed(() => {
scenes.reportComment();
});
It's a subtle difference, but a close look at the example in the createButton docs shows the way.
Manipulating p5.js objects usually involves method calls rather than setting object properties with =, in case this helps you "smell" the pattern easier in the future. You may have been misled by the look of draw = () => {} and mousePressed = () => {}, but these overwrite window variables, replacing function draw() and function mousePressed() to change the main loop that p5 runs and the global mouse press handler, respectively. That's a different case.
A good debugging strategy is to eliminate all unneeded code until all that's left is the following minimal reproduction:
function setup() {
const b = createButton("Report");
b.position(10, 10);
b.mousePressed = () => console.log("worked!");
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.5.0/p5.js"></script>
After removing all distractions having to do with scenes, it's much easier to figure out that mousePressed is being used incorrectly.
Your other handler, cancelButton.mousePressed, should also be adjusted.
After you fix your handlers, you'll probably notice that you can't type into your input. The reason is that it's being created many times per second because it's in the draw() loop:
draw = () => {
// repeat this code many times a second
clear();
input = createInput();
input.position(20, 65);
// ...
Instead, put your element-creation code outside draw() so it's executed one time. For each scene, that top-level code is basically setup() for that scene. draw() is generally for animation, moving objects around the screen, and occasionally creating and destroying objects as the animation requires. Your current app doesn't need any draw()s at the current time, so I'd remove them to avoid confusion.
clear() clears the canvas, not DOM elements, so it's also not necessary in your app yet. Ditto for textAlign(CENTER); and textSize(50);.
Putting it all together:
const scenes = {
report: () => {
const reportButton = createButton("Report");
reportButton.position(5, 5);
reportButton.mousePressed(() => {
reportButton.remove();
scenes.reportComment();
});
},
reportComment: () => {
const cleanup = () => {
input.remove();
cancelButton.remove();
submitButton.remove();
header.remove();
};
const transitionToReport = () => {
cleanup();
scenes.report();
};
const transitionToSendReport = () => {
const value = input.value();
cleanup();
scenes.sendReport(value);
};
const cancelButton = createButton("Cancel");
cancelButton.position(5, 5);
cancelButton.mousePressed(transitionToReport);
const input = createInput();
input.position(20, 65);
const submitButton = createButton("Submit");
submitButton.position(input.x + input.width, 65);
submitButton.mousePressed(transitionToSendReport);
const header = createElement("h2", "Report");
header.position(20, 5);
},
sendReport: customQ => {
const header = createElement("h2", `report: '${customQ}'`);
header.position(20, 5);
},
};
function setup() {
scenes.report();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.5.0/p5.js"></script>
Or keeping it to two scenes:
const scenes = {
report: () => {
const reportButton = createButton("Report");
reportButton.position(5, 5);
reportButton.mousePressed(() => {
reportButton.remove();
scenes.reportComment();
});
},
reportComment: () => {
const transitionToReport = () => {
input.remove();
cancelButton.remove();
submitButton.remove();
header.remove();
scenes.report();
};
const cancelButton = createButton("Cancel");
cancelButton.position(5, 5);
cancelButton.mousePressed(transitionToReport);
const input = createInput();
input.position(20, 65);
const submitButton = createButton("Submit");
submitButton.position(input.x + input.width, 65);
const sendReport = () =>
header.elt.textContent = input.value();
submitButton.mousePressed(sendReport);
const header = createElement("h2", "Report");
header.position(20, 5);
},
};
function setup() {
scenes.report();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.5.0/p5.js"></script>
Note that I'm locally-scoping all variables to each scene. Each scene is responsible for cleaning up its own elements when transitioning to another scene. This helps manage complexity as the app grows.
UX isn't great since the user should be able to press Enter to transition to the next scene, and there should probably be some validation that the input has some content in it, but that's out of scope for this answer.
I set up a grid 16x16 which a white background now i want the divs (grid items) to change color every time i press and hold my mouse and move over them, and to stop changing color when i stop the mousedown event, like in paint. in my script:
let gridContainer = document.getElementById('grid-container');
let gridArray = [];
for(let i = 0; i < 256; i++){
let div = document.createElement('div');
div.setAttribute('class', 'grid-item');
gridContainer.append(div);
gridArray[i] = div;
}
gridContainer.addEventListener("mousedown", draw)
gridContainer.addEventListener("mouseup",stopDraw)
function draw(){
gridArray.forEach(item => item.addEventListener("mouseover", () => {
(item.classList.add('hover'))
}));
}
function stopDraw(){
gridArray.forEach(item => item.removeEventListener("mouseover", () => {
(item.classList.add('hover'))
}));
}
The hover class is used to change the background color to blue.
I tried multiple other approaches but i always end up at the same place, the draw function not working unless i click which runs the function then it doesnt stop, it keeps running even after i leave the mouse.
Im using vanilla JS, I am still learning the basics.
Here is my code for better understanding my problem: https://codepen.io/ahmedmk11/pen/VwrmyGW
EDIT: I just realized that i didnt explain my problem in a clear way, so the draw function works properly, but my problem is that it doesnt stop working, i need in to only work when im pressing and holding only, like a pen.
Using event listeners is a good approach, to keep with this I have changed your code to use an object. Using an object allows the state of the mouse to be temporarily stored. Using the "canDraw()" method we can read from the object to determine if the mouse event is still occurring. It is still occurring if the key "mousedown" is still present.
The events now just add or remove the key from "mouseEvent" object.
let gridContainer = document.getElementById('grid-container');
let gridArray = [];
let mouseEvent = {};
for(let i = 0; i < 256; i++){
let div = document.createElement('div');
div.setAttribute('class', 'grid-item');
div.addEventListener("mouseover", draw);
gridContainer.append(div);
gridArray[i] = div;
}
gridContainer.addEventListener("mousedown", () => mouseEvent.mouseDown = true)
gridContainer.addEventListener("mouseup", () => delete mouseEvent.mouseDown)
function canDraw() {
return mouseEvent.mouseDown;
}
function draw(e){
if (canDraw()) {
e.fromElement.classList.add('hover');
}
}
Looks like you say that the div's should be paint when the mouse button is pressed, but you never really say that the divs should not be painted when the mouse button is not pressed, this happen in this specifc part:
function draw(){
gridArray.forEach(item => item.addEventListener("mouseover", () => {
(item.classList.add('hover'))
}));
}
function stopDraw(){
// right here!
gridArray.forEach(item => item.removeEventListener("mouseover", () => {
(item.classList.add('hover'))
}));
}
You could change your code to make something like "hey, when my mouse is up and over the divs, I don't want to insert the class that paint them", like this:
function draw(){
gridArray.forEach(item => item.addEventListener("mouseover", () => {
item.classList.add('hover');
}));
}
function stopDraw(){
// now we have a event that removes the class
gridArray.forEach(item => item.addEventListener("mouseover", () => {
item.classList.remove('hover');
}));
}
I also did some refactor (changed let by const when necessary, corrected the layout and moved the use of the functions draw and stopDraw after his declarations), I tried make less changes as I could, the final solution is:
const gridContainer = document.getElementById('grid-container');
let gridArray = [];
for(let i = 0; i < 256; i += 1){
const div = document.createElement('div');
div.setAttribute('class', 'grid-item');
gridContainer.append(div);
gridArray.push(div);
}
function draw(){
gridArray.forEach(item => item.addEventListener("mouseover", () => {
item.classList.add('hover');
}));
}
function stopDraw(){
gridArray.forEach(item => item.addEventListener("mouseover", () => {
item.classList.remove('hover');
}));
}
gridContainer.addEventListener("mousedown", draw);
gridContainer.addEventListener("mouseup", stopDraw);
I'm upgrading a lazy load scroll mechanism with an intersection observer. My scroll mechanism looks like this:
function setScrollListener(callBack) {
document.addEventListener('scroll', throttle(callBack, 1000));
}
const state = {};
function checkInViewport() {
const offset = 400;
state.unrendered = state.unrendered.filter(el => {
const visible = isInViewport(el, offset)
if (visible) {
display(el);
}
return !visible;
});
}
setScrollListener(checkInViewport)
Without going too much into the weeds, isInViewport uses the offset provided and the getClientBoundRect of the element provided to determine if the element is near the viewport accounting for the offset. Essentially, is this element 400px from the viewport? The method display simply displays the element.
Now using an intersection observer I have this:
const observeInViewport = (el, io) => {
const { offset = 400 } = this.props;
if (isInViewport(el, offset)) {
display(el);
io.unobserve(el);
}
}
const observationOpts = {
root: null,
rootMargin: '0px',
threshold: 0.1,
};
const lazyRef = document.getElementsByClassName("lazy");
const io = new IntersectionObserver(([entry]) => {
observeInViewport(entry.target, io);
}, observationOpts);
[].slice.call(lazyRef).forEach(el => {
io.observe(el);
});
I'm using the same isInViewport and display methods using the offset, but the offset won't trigger until the observer does. The observer is of course trigger by its threshold. I can't provide a negative threshold which would be ideal. I tried using the documentElement as my root option and then providing a rootMargin of ${(el.offsetTop - offset)}px 0px 0px 0px. That is a string template, but the formatting here was messed up with the tick marks.
Can I achieve the same offset logic as I did with my scroll listener, but with an intersection observable? Let me know if you have any questions.
given IntersectionObserver like this:
const observeVisibility = intersectionMargin => {
const observer = new IntersectionObserver(
nodes => {
if (nodes[0].isIntersecting) {
/* is really in viewport? */
this.observer.disconnect();
}
},
{ rootMargin: intersectionMargin }
);
observer.observe(...);
};
How to check whether the node itself is actually in viewport or it's just the intersectionMargin that caused observer to be called?
The IntersectionObserver will fire immediately on load. After that, your callback passed to IntersectionObserver will be called when isIntersecting changes or when the intersectionRatio crosses one of your configured thresholds.
As you can see, the callback gets the list of entries and then it is up to you to do what you want.
//assuming 'threshold' is defined in scope
nodes => {
nodes.forEach(entry => {
const { isIntersecting, intersectionRatio } = entry;
if (Array.isArray(threshold)) {
threshold = threshold[threshold.length - 1];
}
if (isIntersecting || intersectionRatio >= threshold) {
this.observer.disconnect();
}
}
}
I wish to check that a DOM element is visible with Puppeteer and pure JavaScript (not jQuery), how can I do this? By visible I mean that the element is displayed through CSS, and not hidden (f.ex. by display: none).
For example, I can determine whether my element #menu is not hidden via CSS rule display: none, in the following way:
const isNotHidden = await page.$eval('#menu', (elem) => {
return elem.style.display !== 'none'
})
How can I determine in general though if the element is hidden or not, and not just through display: none?
I found that Puppeteer has an API method for this purpose: Page.waitForSelector, via its visible option. I wasn't aware of the latter option, but it lets you wait until an element is visible.
await page.waitForSelector('#element', {
visible: true,
})
Conversely you can wait for an element to be hidden, via the hidden option.
I think this is the idiomatic answer, with regards to the Puppeteer API. Thanks to Colin Cline though as I think his answer is probably useful as a general JavaScript solution.
One is by checking its display style value.
Second is by checking its height, for exp if the element is a child of an element which is display: none, the offsetHeight will be 0 and thus you know the element is not visible despite its display value. opacity: 0 is not considered as hidden element so we will not checking it.
const isNotHidden = await page.$eval('#menu', (elem) => {
return window.getComputedStyle(elem).getPropertyValue('display') !== 'none' && elem.offsetHeight
});
You can check elem.offsetWidth as well and is not bad before any calculation, check if element exist or not.
Use boundingBox()
This method returns the bounding box of the element (relative to the main frame), or null if the element is not visible.
API: https://github.com/puppeteer/puppeteer/blob/master/docs/api.md#elementhandleboundingbox
The current accepted answer involves waiting for an element to appear and become visible.
If we are not interested in waiting on the element, and we would simply like to test the visibility of the element, we can use a combination of getComputedStyle() and getBoundingClientRect() to test whether or not the element is visible.
We can first check that the visibility is not set to hidden.
Then we can validate that the bounding box is visible by checking that the bottom, top, height, and width attributes are not set to 0 (this will filter out elements that have display set to none as well).
const element_is_visible = await page.evaluate(() => {
const element = document.querySelector('#example');
const style = getComputedStyle(element);
const rect = element.getBoundingClientRect();
return style.visibility !== 'hidden' && !!(rect.bottom || rect.top || rect.height || rect.width);
});
Maybe you can using elementHandle.boundingBox() (thank to #huypham idea)
It will return a Promise that show a bounding box of the element (relative to the main frame), or null if the element is not visible.
The snippet example:
const loadMoreButton = await getDataPage.$(
'button.ao-tour-reviews__load-more-cta.js-ao-tour-reviews__load-more-cta'
);
const buttonVisible = await loadMoreButton.boundingBox();
if (buttonVisible) {
await loadMoreButton.click().catch((e) => {
console.log('💥💥💥: ' + e)
});
}
The answer #aknuds1 gave is perfect but you may make it even more convenient for yourself by creating a helper such as this one. This resolves to true if the element is visible and to false otherwise.
function isElementVisible(page, selector, timeout = 150) {
return new Promise((resolve) => {
page.waitForSelector(selector, {visible: true, timeout}).then(() => {
resolve(true);
}).catch(() => {
resolve(false);
});
});
}
Usage
in pure JS with Puppeteer
let isVisible = await isElementVisible(page, selector)
isVisible = await isElementVisible(page, selector, 300)
and if you happen to use Jest or another framework
expect(await isElementVisible(page, selector)).toBeTrue();
in the case of Jest (and most other frameworks), you can go even further and create a custom matcher to extend the existing ones. (https://jestjs.io/docs/expect#expectextendmatchers)
expect.extend({
async toHaveVisible(page, selector, timeout = 150) {
let isVisible = await isElementVisible(page, selector, timeout);
if (isVisible) {
return {
message: () => `expected ${selector} not to be visible`,
pass: true
};
} else {
return {
message: () => `expected ${selector} to be visible`,
pass: false
};
}
}
});
await expect(page).toHaveVisible(selector);
await expect(page).not.toHaveVisible(anotherSelector);
await expect(page).not.toHaveVisible(yetAnotherSelector, 300);
based on playwright's logic for checking if the element is visible - https://github.com/microsoft/playwright/blob/master/src/server/injected/injectedScript.ts#L120-L129
function isVisible(element: Element): boolean {
// Note: this logic should be similar to waitForDisplayedAtStablePosition() to avoid surprises.
if (!element.ownerDocument || !element.ownerDocument.defaultView)
return true;
const style = element.ownerDocument.defaultView.getComputedStyle(element);
if (!style || style.visibility === 'hidden')
return false;
const rect = element.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
}
Apparently here's how jQuery does it:
visible = await page.evaluate((e) => e.offsetWidth > 0 && e.offsetHeight > 0, element)
If you just want to know if an element is visible or not then you can use this function. You should make sure that the page is ready before calling this function. You can do that by using waitForSelector on other elements you expect to be visible.
async function isVisible(page, selector) {
return await page.evaluate((selector) => {
var e = document.querySelector(selector);
if (e) {
var style = window.getComputedStyle(e);
return style && style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
}
else {
return false;
}
}, selector);
}
// Example usage:
page.waitForSelector('#otherPeerElement');
var myElementIsVisible = await isVisible(page, '#visibleOrNot');
if (myElementIsVisible) {
// Interact with #visibleOrNot
}
this code definitely help you.
It basically means the element is already available on the page but is not visible yet or in CSS, the display property is set as none or visibility is hidden. Now, while writing our tests, we assume that as soon as the element is available, do an action on it like clicking or typing. But as this element is not yet visible, Puppeteer fails to perform that action.
async function isLocatorReady(element, page) {
const isVisibleHandle = await page.evaluateHandle((e) =>
{
const style = window.getComputedStyle(e);
return (style && style.display !== 'none' &&
style.visibility !== 'hidden' && style.opacity !== '0');
}, element);
var visible = await isVisibleHandle.jsonValue();
const box = await element.boxModel();
if (visible && box) {
return true;
}
return false;
}
const firstName= await page.$('[name=firstName]')
expect(firstName!=null).equal(true)
I would use #aknuds1 's approach, but you can also do the following.
expect((await page.$('#element')) !== null).toEqual(true)
If you are fetching a resource asynchronously, be aware that the above expectation may not pass, since it won't wait for the changes to reflect on the UI. That's why this approach may not be preferred in this scenario.