Simple testing example of canvas element using Jest - reactjs

I am trying to create a jest test for the example found in this sandbox:
https://codesandbox.io/s/r3f-basic-demo-forked-297k3?file=/src/App.js
Basically I would like to test the onClick functionality on each box. I have already come across with the fact that jest tests don't run in a real browser. Jest usesĀ jsdomĀ for mocking the necessary parts of the DOM. Consequently I may need canvas support for jsdom, yet I am not quite sure exactly what to do.
App.js
import React, { useRef, useState } from 'react'
import { Canvas, useFrame } from 'react-three-fiber'
function Box(props) {
// This reference will give us direct access to the mesh
const mesh = useRef()
// Set up state for the hovered and active state
const [hovered, setHover] = useState(false)
const [active, setActive] = useState(false)
// Rotate mesh every frame, this is outside of React without overhead
useFrame(() => {
mesh.current.rotation.x = mesh.current.rotation.y += 0.01
})
return (
<mesh
{...props}
ref={mesh}
scale={active ? [1.5, 1.5, 1.5] : [1, 1, 1]}
onClick={(e) => setActive(!active)}
onPointerOver={(e) => setHover(true)}
onPointerOut={(e) => setHover(false)}>
<boxBufferGeometry args={[1, 1, 1]} />
<meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
</mesh>
)
}
export const App = () => {
return (
<>
<Canvas>
<ambientLight intensity={0.5} />
<spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} />
<pointLight position={[-10, -10, -10]} />
<Box position={[-1.2, 0, 0]} />
<Box position={[1.2, 0, 0]} />
</Canvas>
</>
)
}

It's not much of an answer but I found this article useful: https://www.valentinog.com/blog/canvas/.
The suggestion is for proper testing of a canvas element one should not be using Jest (or any unit test) but instead, use some sort of e2e testing so that it can actually render the canvas.
I was able to get some smaller tests working in jest with the following:
Firstly render your page normally using jest. You can either give your canvas a test-id or (as in my case) put a div around your canvas and give the div a test-id.
I ended up with the following:
let canvasContainer = getByTestId("canvas-container");
let canvas = canvasContainer.getElementsByTagName("canvas")[0];
let canvasContext = canvas.getContext("2d");
In either case, your aim is to get the canvas' elements context.
From there you can inspect it with canvasContext.__getDrawCalls()
This will give you some information on all of the "drawCalls" that have been done so far (such as drawing lines or rectangles or what have you).
It takes a little of a bit of inspection into the __getDrawCalls but you should find that every call so far is included.
I wrote some funcitons to help me get the exact details I wanted:
const getLineInfo = (lineInfo) => {
if (lineInfo.type !== "stroke") {
return "not a line";
}
let lineInfoDetail = lineInfo.props.path[3];
return {
lineStart: {
x: lineInfoDetail.transform[4],
y: lineInfoDetail.transform[5],
},
lineEnd: {
x: lineInfoDetail.transform[4] + lineInfoDetail.props.x,
y: lineInfoDetail.transform[5] + lineInfoDetail.props.y,
},
};
};
const getRectInfo = (rectInfo) => {
if (rectInfo.type !== "fill") {
return "not a rect";
}
let rectInfoDetail = rectInfo.props.path[1];
return {
rectStart: {
x: rectInfoDetail.transform[4],
y: rectInfoDetail.transform[5],
},
rectEnd: {
x: rectInfoDetail.transform[4] + rectInfoDetail.props.width,
y: rectInfoDetail.transform[5] + rectInfoDetail.props.height,
},
};
};
So for example if I know that the second draw is a line I can do the following:
expect(getLineInfo(canvasContext.__getDrawCalls()[1])).toStrictEqual({
lineStart: { x: 150, y: 250 },
lineEnd: { x: 150, y: 350 },
});
This is testing that that element is a line with a start point of { x: 150, y: 250 } and an endpoint of { x: 150, y: 350 }
The issue with this though is that you can't trigger click and drag events of the canvas elements within jest and therefore using jest to test canvas seems to be very limited.
Edit:
It looks like https://yonatankra.com/how-to-test-html5-canvas-with-jest/ gives another explanation.

Related

Best way to make a custom smooth scroll and scrollto coexist (ReactJs - Framer motion)

Made a smoothscroll component using framer motion that's working well :
export default function SmoothScroll({ children }: Props) {
const { width } = useWindowSize();
const scrollContainer = useRef() as RefObject<HTMLDivElement>;
const [pageHeight, setPageHeight] = useState(0);
useEffect(() => {
setTimeout(() => {
// added a setTimeout so the page has the time to load and it still fits
const scrollContainerSize =
scrollContainer.current?.getBoundingClientRect();
scrollContainerSize && setPageHeight(scrollContainerSize.height);
}, 500);
}, [width]);
const { scrollY } = useScroll(); // measures how many pixels user has scrolled vertically
// as scrollY changes between 0px and the scrollable height, create a negative scroll value...
// ... based on current scroll position to translateY
const transform = useTransform(scrollY, [0, pageHeight], [0, -pageHeight]);
const physics = { damping: 15, mass: 0.17, stiffness: 55 }; // easing of smooth scroll
const spring = useSpring(transform, physics); // apply easing to the negative scroll value
return (
<>
<motion.div
ref={scrollContainer}
style={{ y: spring }} // translateY of scroll container using negative scroll value
className="app fixed overflow-hidden w-screen"
>
{children}
</motion.div>
<motion.div style={{ height: pageHeight }} />
</>
);
}
The thing is, I'd like to scrollTo sections of my page upon click on the navbar but don't really know how to implement it without removing the smoothScroll ...
Tried the following logic but obviously it did not work as the vanilla scroll has been hijacked :
const scrollToSection = (
e: React.MouseEvent<HTMLLIElement, globalThis.MouseEvent>,
anchor?: string
) => {
e.preventDefault();
if (!anchor) return;
const section = document.querySelector(anchor);
section?.scrollIntoView({ behavior: "smooth" });
};
Is it doable ?

React Three Fiber, How to track mouse movement

I am trying to use three for the first time to level up my webpages a little so I'm following a guide on YouTube here so you can see what I'm trying to accomplish. https://www.youtube.com/watch?v=YK1Sw_hnm58 I'm at around 1:12:20. the only difference is I'm using react instead of plain JavaScript so I'm struggling to convert it since I've never used it before.
/* eslint-disable no-unused-vars */
import React, { useRef } from "react";
import { useFrame, useLoader } from "#react-three/fiber";
import { OrbitControls } from "#react-three/drei";
import * as THREE from "three";
import { useEffect } from "react";
import { useState } from "react";
const PlaneMesh = (params) => {
const paneRef = useRef();
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(false)
const { array } = paneRef.current.geometry.attributes.position
for (let i = 0; i < array.length; i += 3){
array[i + 2] = array[i + 2] + Math.random()
}
}, [loading])
const [MousePosition, setMousePosition] = useState({
left: 0,
top: 0
})
useFrame( state => {
let mouseX = state.mouse.x;
let mouseY = state.mouse.y;
console.log(mouseX)
});
return (
<>
{/* <ambientLight intensity={1} /> */}
<directionalLight color={0xFFFFFF} position={[0, 0, 1]} intensity={1} />
{/* <pointLight color="#FFFFFF" position={[0, 0, 2]} intensity={1} /> */}
<mesh ref={paneRef} position={[0,0, -5]}>
<planeGeometry args={[100, 100, 50, 50]} />
<meshPhongMaterial
color={0x000044}
side={THREE.DoubleSide}
flatShading={true}
/>
{/* <OrbitControls
enableZoom={true}
enablePan={true}
enableRotate={true}
zoomSpeed={0.6}
panSpeed={0.5}
rotateSpeed={0.4}
/> */}
</mesh>
</>
);
};
export default PlaneMesh;
This is my full file right now. I cant work out how to add the mouse move event listener and have it work as any method I have tried either only runs once or only runs when I click on the page rather than when I move the mouse. This is the closest I have been able to get it as it constantly prints the value but the mouse coordinates don't update unless I click. Any advice is greatly appreciated.
I ended up fixing it by adding a window event listener and subtracting the bounding box to get the mouse position over the element.
const section = document.getElementById("planeSection");
section.addEventListener("mousemove", (event) => {
var rect = container.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / window.innerWidth) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / window.innerHeight) * 2 + 1;
});

Why y and x axis parameter didn't work for staggerChildren animation in framer motion?

My question is fairly simple but i'm struggling to find the solution. Here i'm trying to create a staggering animation for each letter in a sentence, i want to use the y axis as a parameter to animate but i'm not getting the result i wanted as the sentence fully mounted without animating. But when i tried to use opacity as a parameter, it works exactly fine. What did i do wrong here? Any help would be appreciated :)
// import "./styles.css";
import { motion } from "framer-motion";
export default function App() {
const quotes = "Hello World.";
const parent = {
animate: {
transition: {
staggerChildren: 0.1,
},
},
};
const child = {
initial: { y: 400 },
animate: {
y: 0,
},
};
return (
<div>
<motion.div variants={parent} initial='initial' animate='animate'>
{
quotes.split('').map((item, index) => (
<motion.span variants={child} key={index}>{item}</motion.span>
))
}
</motion.div>
</div>
);
}
To help you see my problem, here is codesandbox example
https://codesandbox.io/s/busy-mountain-fv58xc?file=/src/App.js:0-620
Animating x and y doesn't work for <span> because it's and inline element. It flows with the content and doesn't have an explicit x and y position to animate.
You can change your spans to a block-level element (like div), or you could add some styling to tell the spans to display as blocks:
<motion.span style={{display: "inline-block"}} variants={child} key={index}>
{item}
</motion.span>

Next.js + Framer Motion scroll progress animation

So I want to create a viewport scroll progress circle with Framer Motion in a Next.js app (using tailwind as CSS).
I used the example code from Framer Motion:
https://codesandbox.io/s/framer-motion-viewport-scroll-and-svg-path-animation-mwi35?from-embed=&file=/src/Example.tsx:686-1070
The code I have down here displays the circle, but when I enable strokeDasharray or pathLength (in the style attribute) it disappears. And on scroll I don't see anything happening.
I tried different svg versions, but I didn't get it working. I also looked at my json.config and compare it with the example, but since it uses TypeScript I could not figure it out.
import * as React from 'react';
import { useEffect, useState } from 'react';
import {
motion,
useViewportScroll,
useSpring,
useTransform,
} from 'framer-motion';
const CircleIndicator = () => {
const { scrollYProgress } = useViewportScroll();
const yRange = useTransform(scrollYProgress, [0, 0.9], [0, 1]);
const pathLength = useSpring(yRange, { stiffness: 400, damping: 90 });
useEffect(() => yRange.onChange((v) => setIsComplete(v >= 1)), [yRange]);
return (
<>
<svg className='fixed w-12 h-12 bottom-7 left-7' viewBox='0 0 60 60'>
<motion.path
fill='none'
strokeWidth='2'
stroke='white'
// strokeDasharray='0 1'
d='M 0, 20 a 20, 20 0 1,0 40,0 a 20, 20 0 1,0 -40,0'
style={
{
// pathLength,
// rotate: 90,
// translateX: 5,
// translateY: 5,
// scaleX: -1, // Reverse direction of line animation
}
}
/>
</svg>
</>
);
};
export default CircleIndicator;
It would be great if I could get the animation to work.
If your css codes have below
html,
body: {
width: 100%
}
delete it and try again.
I has the same problem and solved it this way.

React-three-fiber with react-spring animations

sorry if this seems like an obvious answer, but I am trying to create an animation similar to this. I am trying to create one similar with react-three-fiber but I can't use the ones shown in the docs since they manipulate the style transform property which isn't possible with three.js. I have tried to manipulate it through the position attribute and props but it comes with an error as shown here and I don't really know where to start to fix this. Here is my code:
const props = useSpring({
to: async next => {
await next({ position: [100, 100, 100] });
await next({ position: [50, 50, 50] });
},
from: { position: [100, 100, 100] },
config: { duration: 3500 },
reset: true
});
return (
<Canvas>
<a.group {...props}>
<Box position={[-1.2, 0, 0]} />
<Box position={[1.2, 0, 0]} />
</a.group>
</Canvas>
)
I have used react-spring successfully with hovering and scale but it just doesn't work when using position.
i think something like that is best done with trigonometry
const ref = useRef()
useFrame(() => {
ref.current.position.y = Math.sin(state.clock.getElapsedTime()) * 10
})
return <group ref={ref}> ...
Math.sin will yield a value between -1 to 1 and alternates smoothly between the two. multiply your factor (the distance) and you have it. here's an example that dollies the camera like that: https://codesandbox.io/s/r3f-lod-e9vpx
otherwise it's react-spring/three, not react-spring: https://codesandbox.io/s/clever-bartik-tkql8

Resources