Playwright: visited link colour - css

I have just started experimenting with Playwright and have fallen at the first hurdle. I want to be able to test that visited links have the correct style applied. However, the browsers used by Playwright do not appear to be applying the visited link style (a:visited). Here's an example test:
import { test, expect } from '#playwright/test';
test('link colours', async ({ page }) => {
await page.goto('https://www.nngroup.com/articles/change-the-color-of-visited-links/');
const byline = page.locator(
'article .authors-and-topics .byline-wrapper a:text-is("Jakob Nielsen")'
).first();
const bylineURL = await byline.getAttribute('href');
// Initial colour.
expect(await byline.evaluate(
e => window.getComputedStyle(e).getPropertyValue("color")
)).toBe('rgb(1, 118, 152)');
await byline.hover();
// Underline on hover.
expect(await byline.evaluate(
e => window.getComputedStyle(e).getPropertyValue("text-decoration-line")
)).toBe('underline');
if (bylineURL !== null) {
await page.goto('https://www.nngroup.com' + bylineURL);
await page.goBack();
// Visited colour.
expect(await byline.evaluate(
e => window.getComputedStyle(e).getPropertyValue("color")
)).toBe('rgb(128, 65, 128)');
}
});
The initial and hover style tests both pass. However, the final test fails:
Expected: "rgb(128, 65, 128)"
Received: "rgb(1, 118, 152)"
When I debug the tests in a headed browser, I see that the a:visited style is not applied after the page is visited. Nor is the style applied if I manually visit the page within the test browser instance. It appears as though the browsers are not maintaining the page history, even though page.goBack() works correctly.

Related

Select CSS Parent `selector` in iOS Content Blocking

I am setting up some content blockers (https://developer.apple.com/documentation/safariservices/creating_a_content_blocker)
The HTML I am testing on looks something like this:
<div class="<random>">
<div>
<div>
<div>
<a class="bad" />
Now, I am wondering if I can have a CSS selector that selects on .bad but then removes the entire .random div block.
I have tried things like:
{
"action": {
"type": "css-display-none",
"selector": "div > div > div > div >a[href*='speedtest.net']"
},
"trigger": {
"url-filter": "^https?:\/+([^\/:]+\\.)?google.*[:\/]"
}
}
and div:has(a).
Nothing seems to work.
Anyone know if its possible? I can't even find anywhere that says what apple supports for this (CSS4?)
Update:: has is now supported in Safari and Safari iOS. source: caniuse
There is currently no parent selector in css even though it is one of the most requested CSS feature.
The simple reason for the lack of implementation yet is performance issues.
https://snook.ca/archives/html_and_css/css-parent-selectors
The closest thing we have to a parent selector in CSS for now is :focus-within which will match an element if the element or any of its descendants is focused. However it is of no use in your case.
The proposed implementation of a parent selector as you mentioned is :has and is part of the level 4 of CSS selectors.
https://www.w3.org/TR/selectors-4/
Although it isn't implemented yet in any browser, it is now part of the technical preview for Safari so we might get it some day.
However it is not yet part of the technical preview for iOS and that might still take a lot of time.
https://caniuse.com/css-has
Even though there is no CSS parent selector, what you are describing can be easily achieved through Javascript in your Safari extension.
However you will need to give permissions to your Safari extension to inject scripts in the web page.
The users will have to accept the permissions. If they refuse you won't be able to access the page DOM through javascript.
You can do that by going in the manifest.json of your Xcode project : AppName => Shared (Extension) => Ressources => manifest
In content_scripts you will need to add
"content_scripts": [{
"js": [ "content.js" ],
"matches": [ "<all_urls>" ]
}]
You can also select the sites you want your extension to have access to with regular expressions like you did previously.
https://developer.apple.com/documentation/safariservices/safari_web_extensions/managing_safari_web_extension_permissions
Then in the content.js file you can add your javascript code to edit the web pages as you please.
Here is a function you could use to remove the parent of the bad elements:
const removeBadElementParent = () =>{
const badElements = document.querySelectorAll('.bad-class-name')
if(badElements.length === 0) return;
badElements.forEach(element =>{
if(!element instanceof HTMLElement) return;
const parent = element.parentElement;
if(!parent instanceof HTMLElement) return;
parent.style.display = 'none'
})
}
You don't want to use it immediately but only when the DOM has loaded so you will need to call it like this:
if( document.readyState !== 'loading' ) {
removeBadElementParent();
} else {
document.addEventListener("DOMContentLoaded", function(event) {
removeBadElementParent();
});
}
However most website make changes to the DOM and might add the bad elements after the DOMContentLoaded event has fired. So you will need to add a mutation observer to check when the DOM is changed:
const onDOMMutation = (callback) =>{
MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
const observer = new MutationObserver(function(mutations, observer) {
callback();
});
observer.observe(document, {
subtree: true,
attributes: true
});
}
And you can call it like this instead of calling directly the removeBadElementParent function:
if( document.readyState !== 'loading' ) {
onDOMMutation(removeBadElementParent);
} else {
document.addEventListener("DOMContentLoaded", function(event) {
onDOMMutation(removeBadElementParent);
});
}
https://developer.apple.com/documentation/safariservices/safari_app_extensions/injecting_a_script_into_a_webpage

Keeping react-player full-screen mode between videos

I am building an online course website.
When the user watches a lesson in full-screen mode, I want to remember that, so as to use full-screen mode when I mount react-player with the next lesson. I hoped there would be an onFullscreenMode callback, but the documentation does not list anything of the kind. How can I achieve this?
Edit 1: Based on the reply of #onkarruikar, I tried using screenfull. First, I was surprised that it was not installed although real-player was supposed to use it to enter full-screen mode. After installing the package and importing it, I get the compilation error:
.../node_modules/screenfull/index.js 11:44
Module parse failed: Unexpected token (11:44)
File was processed with these loaders:
.../node_modules/babel-loader/lib/index.js
You may need an additional loader to handle the result of these loaders.
|
| for (const methodList of methodMap) {
> const exitFullscreenMethod = methodList?.[1];
|
| if (exitFullscreenMethod in document) {
Edit 2: I also don't get it why the demo uses a custom button for switching to full-screen mode, whereas I see a button () on the player itself:
The player doesn't have fullscreen inbuilt. It uses screenfull to go full-screen. As per their demo https://cookpete.com/react-player/ full-screen is handled externally by the component users.
You can use following screenfull features directly on your website:
screenfull.isFullscreen //<-- is the browser in fullscreen
screenfull.isEnabled //<-- is the facility available to use
screenfull.request(element);
screenfull.toggle(element);
etc.
Or you can use standard web apis like:
if(document.fullscreenElement) { //<-- is the browser in fullscreen
...
}
document.fullscreenEnabled //<-- is the facility available to use
Document.fullscreenElement / ShadowRoot.fullscreenElement
The fullscreenElement property tells you the Element that's currently being displayed in full-screen mode on the DOM (or shadow DOM). If this is null, the document (or shadow DOM) is not in full-screen mode.
ref
These apis should work even if you go fullscreen using controls inside player.
Here is a demo website using react: https://gizcx.csb.app/
Corresponding codesandbox code
Also, if you are not playing videos one by one then you can pass full course playlist to the player at once:
<ReactPlayer
url={[
'https://www.youtube.com/watch?v=oUFJJNQGwhk',
'https://www.youtube.com/watch?v=jNgP6d9HraI'
]}
/>
For the benefit of others, this is how it is achieved:
import { findDOMNode } from 'react-dom'
import { toast } from 'react-toastify';
const PlayerComponent = () => {
const [fullscreenMode, setFullscreenMode] = useState(false)
let player = null;
const ref = (p) => {player = p;}
const onStart = () => {
if (fullscreenMode)
findDOMNode(player).requestFullscreen().catch(
(err) =>
{toast.error("Could not activate full-screen mode :(")}
);
}
const onEnded = () => {
setFullscreenMode(document.fullscreenElement !== null);
}
return (
<ReactPlayer
ref={ref}
url="whatever url"
onStart={onStart}
onEnded={onEnded} />);
);
}

How to conditionally render a component in Next.js without caching CSS styles?

I'm pretty new with Next.js and do not fully understand the cache functioning.
Given the following simplified example:
An index page that renders components Test1 or Test2, depending whether the current minute is even or odd:
import { Test2 } from '#src/components/test2'
import React from 'react'
const conditionallyChooseComponent = () => {
const d = new Date()
if (d.getMinutes() % 2 === 0) return <Test1 />
else return <Test2 />
}
export default function Home() {
return <div>{conditionallyChooseComponent()}</div>
}
And having the following components. Test1:
export const Test1 = () => {
const d = new Date()
return (
<div className={`${utilStyles.redContainer}`}>
<h1>It's {d.toISOString()} and I'm Test1 component. My background should be red</h1>
</div>
)
}
And Test2:
export const Test2 = () => {
const d = new Date()
return (
<div className={`${utilStyles.blueContainer}`}>
<h1>It's {d.toISOString()} and I'm Test2 component. My background should be blue</h1>
</div>
)
}
And this CSS:
.redContainer {
background-color: red;
}
.blueContainer {
background-color: blue;
}
The background color is being cached when the code is executed by building and serving from the transpiled code. When running with yarn dev it is working just fine.
Here is the unexpected result:
Screenshot with Test1 component being rendered with blue background
PS: I made this work with the workaround of using the getInitialProps to prevent Next.js from caching anything in that page but, for my real use case that option is not valid because I need the render condition to be calculated in the client side since it will depend on the local date of the browser.
Next will automatically cache all static pages that doesn't depends on external data, maybe you can implement useEffect to update your date variable or use a simple state, so it should work the way you expect
https://nextjs.org/docs/basic-features/pages#static-generation-without-data
To make that works, you will need to add some client-side code (via useEffect), so the React component updates every minute (or so). Funny enough, this is not as simple as it sounds, and even Dan Abramov has published a long post explaining why things such as setInterval may not work intuitively with React (specifically, with React Hooks).
Assuming you use the custom hook Dan explains in the article above, this should work:
export default function Home() {
const [date, setDate] = useState(new Date());
useInterval(() => {
// this will update the component's date every second
setDate(new Date());
}, 1000);
return <div>{date.getMinutes() % 2 === 0 ? <p>Test 1</p> : <p>Test 2</p>}</div>;
}
Observe that your code example only executes the conditionallyChooseComponent once, just when Next is trying to server-side rendering your page.

How to handle unsupported CSS properties in Next.js or use #supports directives in Chakra-UI

I am working with a React component that uses different opacity values according to whether there is support for the CSS backdrop-filter directive:
background={(() => {
const opacity = isBackdropFilterSupported() ? 0.75 : 0.98
return (
`linear-gradient(
180deg, rgba(76, 63, 143, ${opacity}) 62.76%,
rgba(184, 169, 255, ${opacity}) 100%
)`
)
})()}
The issue is that the site is generated server-side using Next.js. CSS.supports('backdrop-filter', 'blur(1px)') returns false on the server, so the value is always false regardless of the client properties.
One solution would be to use CSS like:
.drawer {
--opacity: 0.75;
background: linear-gradient(
180deg, rgba(76, 63, 143, var(--opacity)) 62.76%,
rgba(184, 169, 255, var(--opacity)) 100%
);
}
#supports not (backdrop-filter: blur(1px)) {
.drawer { --opacity: 0.98; }
}
This should be interpreted by the client and avoid the server-side rendering issue, but I've found no indication as to how to integrate such a style into Chakra-UI which this is build on.
I didn't mention it in my original post, but I was getting an error like: Prop id did not match. Server: "toggle--gxfg3t7xwo" Client: "toggle--ki0j10p2l".
It turns out this means that the DOM generated by the browser doesn't match the DOM generated by Next.js. When this happens Next.js gives up on trying to rehydrate the document which is why I was getting the server-rendered values.
The solution was to use a hook to determine when the component was mounted (which only happens on the client). That hook looks like:
export const useMounted = () => {
// https://www.joshwcomeau.com/react/the-perils-of-rehydration/
const [hasMounted, setHasMounted] = React.useState(false);
React.useEffect(() => {
setHasMounted(true);
}, []);
return hasMounted;
};
The opacity determination then became:
const hasMounted = useMounted()
⋮
const opacity = hasMounted && isBackdropFilterSupported() ? 0.75 : 0.98

How to get a react component's size (height/width) before render?

I have a react component that needs to know its dimensions ahead of time, before it renders itself.
When I'd make a widget in jquery I could just $('#container').width() and get the width of the container ahead of time when I build my component.
<div id='container'></div>
these container's dimensions are defined in CSS, along with a bunch of other containers on the page. who defines the height and width and placement of the components in React? I'm used to CSS doing that and being able to access that. But in React it seems I can only access that information after the component has rendered.
The example below uses react hook useEffect.
Working example here
import React, { useRef, useLayoutEffect, useState } from "react";
const ComponentWithDimensions = props => {
const targetRef = useRef();
const [dimensions, setDimensions] = useState({ width:0, height: 0 });
useLayoutEffect(() => {
if (targetRef.current) {
setDimensions({
width: targetRef.current.offsetWidth,
height: targetRef.current.offsetHeight
});
}
}, []);
return (
<div ref={targetRef}>
<p>{dimensions.width}</p>
<p>{dimensions.height}</p>
</div>
);
};
export default ComponentWithDimensions;
Some Caveats
useEffect will not be able to detect it's own influence to width and height
For example if you change the state hook without specifying initial values (eg const [dimensions, setDimensions] = useState({});), the height would read as zero when rendered because
no explicit height was set on the component via css
only content drawn before useEffect can be used to measure width and height
The only component contents are p tags with the height and width variables, when empty will give the component a height of zero
useEffect will not fire again after setting the new state variables.
This is probably not an issue in most use cases, but I thought I would include it because it has implications for window resizing.
Window Resizing
I also think there are some unexplored implications in the original question. I ran into the issue of window resizing for dynamically drawn components such as charts.
I'm including this answer even though it wasn't specified because
It's fair to assume that if the dimensions are needed by the application, they will probably be needed on window resize.
Only changes to state or props will cause a redraw, so a window resize listener is also needed to monitor changes to the dimensions
There's a performance hit if you redraw the component on every window resize event with more complex components. I found
introducing setTimeout and clearInterval helped. My component
included a chart, so my CPU spiked and the browser started to crawl.
The solution below fixed this for me.
code below, working example here
import React, { useRef, useLayoutEffect, useState } from 'react';
const ComponentWithDimensions = (props) => {
const targetRef = useRef();
const [dimensions, setDimensions] = useState({});
// holds the timer for setTimeout and clearInterval
let movement_timer = null;
// the number of ms the window size must stay the same size before the
// dimension state variable is reset
const RESET_TIMEOUT = 100;
const test_dimensions = () => {
// For some reason targetRef.current.getBoundingClientRect was not available
// I found this worked for me, but unfortunately I can't find the
// documentation to explain this experience
if (targetRef.current) {
setDimensions({
width: targetRef.current.offsetWidth,
height: targetRef.current.offsetHeight
});
}
}
// This sets the dimensions on the first render
useLayoutEffect(() => {
test_dimensions();
}, []);
// every time the window is resized, the timer is cleared and set again
// the net effect is the component will only reset after the window size
// is at rest for the duration set in RESET_TIMEOUT. This prevents rapid
// redrawing of the component for more complex components such as charts
window.addEventListener('resize', ()=>{
clearInterval(movement_timer);
movement_timer = setTimeout(test_dimensions, RESET_TIMEOUT);
});
return (
<div ref={ targetRef }>
<p>{ dimensions.width }</p>
<p>{ dimensions.height }</p>
</div>
);
}
export default ComponentWithDimensions;
re: window resizing timeout - In my case I'm drawing a dashboard with charts downstream from these values and I found 100ms on RESET_TIMEOUT seemed to strike a good balance for me between CPU usage and responsiveness. I have no objective data on what's ideal, so I made this a variable.
As it was already mentioned, you can't get any element's dimensions until it is rendered to DOM. What you can do in React is to render only a container element, then get it's size in componentDidMount, and then render rest of the content.
I made a working example.
Please note that using setState in componentDidMount is an anti-pattern but in this case is fine, as it is exactly what are we trying to achieve.
Cheers!
Code:
import React, { Component } from 'react';
export default class Example extends Component {
state = {
dimensions: null,
};
componentDidMount() {
this.setState({
dimensions: {
width: this.container.offsetWidth,
height: this.container.offsetHeight,
},
});
}
renderContent() {
const { dimensions } = this.state;
return (
<div>
width: {dimensions.width}
<br />
height: {dimensions.height}
</div>
);
}
render() {
const { dimensions } = this.state;
return (
<div className="Hello" ref={el => (this.container = el)}>
{dimensions && this.renderContent()}
</div>
);
}
}
You cannot. Not reliably, anyway. This is a limitation of browser behavior in general, not React.
When you call $('#container').width(), you are querying the width of an element that has rendered in the DOM. Even in jQuery you can't get around this.
If you absolutely need an element's width before it renders, you will need to estimate it. If you need to measure before being visible you can do so while applying visibility: hidden, or render it somewhere discretely on the page then moving it after measurement.
There's an unexpected "gotcha" with #shane's approach for handling window resizing: The functional component adds a new event listener on every re-render, and never removes an event listener, so the number of event listeners grows exponentially with each resize. You can see that by logging each call to window.addEventListener:
window.addEventListener("resize", () => {
console.log(`Resize: ${dimensions.width} x ${dimensions.height}`);
clearInterval(movement_timer);
movement_timer = setTimeout(test_dimensions, RESET_TIMEOUT);
});
This could be fixed by using an event cleanup pattern. Here's some code that's a blend of #shane's code and this tutorial, with the resizing logic in a custom hook:
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useState, useEffect, useLayoutEffect, useRef } from "react";
// Usage
function App() {
const targetRef = useRef();
const size = useDimensions(targetRef);
return (
<div ref={targetRef}>
<p>{size.width}</p>
<p>{size.height}</p>
</div>
);
}
// Hook
function useDimensions(targetRef) {
const getDimensions = () => {
return {
width: targetRef.current ? targetRef.current.offsetWidth : 0,
height: targetRef.current ? targetRef.current.offsetHeight : 0
};
};
const [dimensions, setDimensions] = useState(getDimensions);
const handleResize = () => {
setDimensions(getDimensions());
};
useEffect(() => {
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
useLayoutEffect(() => {
handleResize();
}, []);
return dimensions;
}
export default App;
There's a working example here.
This code doesn't use a timer, for simplicity, but that approach is further discussed in the linked tutorial.
As stated, it is a limitation of the browsers - they render in one go and "in one thread" (from JS perspective) between your script that manipulates the DOM, and between event handlers execution. To get the dimensions after manipulating / loading the DOM, you need to yield (leave your function) and let the browser render, and react to some event that rendering is done.
But try this trick:
You could try to set CSS display: hidden; position: absolute; and restrict it to some invisible bounding box to get the desired width. Then yield, and when the rendering is done, call $('#container').width().
The idea is: Since display: hidden makes the element occupy the space it would take if visible, the computation must be done in the background.
I am not sure if that qualifies as "before render".
Disclaimer:
I haven't tried it, so let me know if it worked.
And I am not sure how it would blend with React.
#Stanko's solution is nice and terse, but it's post-render. I have a different scenario, rendering a <p> element inside an SVG <foreignObject> (in a Recharts chart). The <p> contains text that wraps, and the final height of the width-constrained <p> is hard to predict. The <foreignObject> is basically a viewport and if too long it would block clicks/taps to underlying SVG elements, too short and it chops off the bottom of the <p>. I need a tight fit, the DOM's own style-determined height before the React render. Also, no JQuery.
So in my functional React component I create a dummy <p> node, place it to the live DOM outside the document's client viewport, measure it, and remove it again. Then use that measurement for the <foreignObject>.
[Edited with method using CSS classes]
[Edited: Firefox hates findCssClassBySelector, stuck with hardcoding for now.]
const findCssClassBySelector = selector => [...document.styleSheets].reduce((el, f) => {
const peg = [...f.cssRules].find(ff => ff.selectorText === selector);
if(peg) return peg; else return el;
}, null);
// find the class
const eventLabelStyle = findCssClassBySelector("p.event-label")
// get the width as a number, default 120
const eventLabelWidth = eventLabelStyle && eventLabelStyle.style ? parseInt(eventLabelStyle.style.width) : 120
const ALabel = props => {
const {value, backgroundcolor: backgroundColor, bordercolor: borderColor, viewBox: {x, y}} = props
// create a test DOM node, place it out of sight and measure its height
const p = document.createElement("p");
p.innerText = value;
p.className = "event-label";
// out of sight
p.style.position = "absolute";
p.style.top = "-1000px";
// // place, measure, remove
document.body.appendChild(p);
const {offsetHeight: calcHeight} = p; // <<<< the prize
// does the DOM reference to p die in garbage collection, or with local scope? :p
document.body.removeChild(p);
return <foreignObject {...props} x={x - eventLabelWidth / 2} y={y} style={{textAlign: "center"}} width={eventLabelWidth} height={calcHeight} className="event-label-wrapper">
<p xmlns="http://www.w3.org/1999/xhtml"
className="event-label"
style={{
color: adjustedTextColor(backgroundColor, 125),
backgroundColor,
borderColor,
}}
>
{value}
</p>
</foreignObject>
}
Ugly, lots of assumptions, probably slow and I'm nervous about the garbage, but it works. Note that the width prop has to be a number.
All the solutions I found on Stack overflow were either very slow, or out of date with modern React conventions. Then I stumbled across:
https://github.com/wellyshen/react-cool-dimensions
A React hook that measure an element's size and handle responsive components with highly-performant way, using ResizeObserver.
It's fast and works much better than the solutions I tried here.
import { useState, useEffect } from 'react'
const useContainerDimensions = containerRef => {
const getDimensions = () => ({
width: containerRef.current.offsetWidth,
height: containerRef.current.offsetHeight
})
const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
useEffect(() => {
const handleResize = () => {
setDimensions(getDimensions())
}
let dimensionsTimeout = setTimeout(() => {
if(containerRef.current) {
setDimensions(getDimensions())
}
}, 100)
window.addEventListener("resize", handleResize)
return () => {
clearTimeout(dimensionsTimeout)
window.removeEventListener("resize", handleResize)
}
}, [containerRef])
return dimensions
}
export default useContainerDimensions
You can use useContainerDimensions Custom hook. if you need width and height as pixel you can use clientWidth and clientHeight instead of offsetWidth and offsetHeight.

Resources