From 60d19c476173b04ffcd6f86e8e1acda3f9a32652 Mon Sep 17 00:00:00 2001 From: James Robb <47126579+jamesrweb@users.noreply.github.com> Date: Fri, 3 Apr 2026 03:29:45 +0200 Subject: [PATCH] feat: Add updater prop, refactor types, tighten docs and build (#372) Adds an `updater` prop to P5Canvas that lets users bridge React state updates from within the p5 lifecycle without leaking React concerns into sketch logic. Type system refactor: - Introduce P5CanvasInternalProps to cleanly separate component props from user sketch props - Use destructuring instead of withoutKeys for prop separation - Remove React.lazy for P5CanvasWithSketch (direct import preserves generic type flow) - Remove generics from internal components (only needed at the public API surface) - Delete InputProps, P5CanvasPropsWithSketch, WithChildren, and withoutKeys as they are no longer needed - Remove boxed primitive instanceof checks from logErrorBoundaryError Docs: - Merge duplicate TypeScript examples into single examples with notes - Collapse advanced topics into details blocks - Remove buggy falsy guards from updateWithProps examples - Fix error UI example to use unknown instead of any - Fix example sketches link to use main branch Build: - Remove dead esbuild config, Vite 8 uses OXC by default - Split p5 into its own chunk in the demo build via manualChunks Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/claude.yml | 4 +- README.md | 423 +++++++----------- config/vite/demo.ts | 8 +- config/vite/library.ts | 3 - pnpm-lock.yaml | 71 ++- pnpm-workspace.yaml | 13 +- src/components/P5CanvasGuard.tsx | 40 +- src/components/P5CanvasWithSketch.tsx | 45 +- ...InputProps.ts => P5CanvasInternalProps.ts} | 12 +- src/contracts/P5CanvasProps.ts | 5 +- src/contracts/P5CanvasPropsWithSketch.ts | 6 - src/contracts/Updater.ts | 7 + src/contracts/WithChildren.ts | 3 - src/main.tsx | 1 + src/utils/logErrorBoundaryError.ts | 18 +- src/utils/withoutKeys.ts | 16 - tests/components/P5Canvas.test.tsx | 75 ++++ tests/utils/logErrorBoundaryError.test.ts | 8 +- tests/utils/withoutKeys.test.ts | 19 - 19 files changed, 349 insertions(+), 428 deletions(-) rename src/contracts/{InputProps.ts => P5CanvasInternalProps.ts} (50%) delete mode 100644 src/contracts/P5CanvasPropsWithSketch.ts create mode 100644 src/contracts/Updater.ts delete mode 100644 src/contracts/WithChildren.ts delete mode 100644 src/utils/withoutKeys.ts delete mode 100644 tests/utils/withoutKeys.test.ts diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 7f70b267..b42c6390 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -8,7 +8,7 @@ on: issues: types: [opened, assigned] pull_request_target: - types: [opened, assigned, synchronize, ready_for_review] + types: [assigned] pull_request_review: types: [submitted] @@ -19,7 +19,7 @@ jobs: (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) || - (github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name == github.repository) + (github.event_name == 'pull_request_target') runs-on: ubuntu-latest permissions: contents: write diff --git a/README.md b/README.md index 422b418e..a69322d1 100644 --- a/README.md +++ b/README.md @@ -30,64 +30,46 @@ If you are still using version 4, you can find the documentation ## Installation -To install, use the following command in the format appropriate to your chosen -package manager: - ```shell [npm|yarn|pnpm] [install|add] p5 @p5-wrapper/react ``` -### Peer dependencies - -Please note that `p5`, `react` and `react-dom` are peer dependencies. Make sure -they are installed in your project before installing this package. +`p5`, `react` and `react-dom` are peer dependencies and must be installed in +your project. -```js -"peerDependencies": { - "p5": ">= 2.0.0", - "react": ">= 19.0.0", - "react-dom": ">= 19.0.0" -}, -``` - -### TypeScript +
TypeScript setup -If you would like to use Typescript, you should install `p5` types in the -development environment: +Install the p5 type definitions as a dev dependency: ```shell [npm|yarn|pnpm] [install|add] -D @types/p5 ``` -### Next.js +
+ +
Next.js setup -If you plan to use this component within a Next.js application, you should -instead use -[our Next.js dynamic implementation](https://github.com/P5-wrapper/next) -instead. To do get started, you can run: +For Next.js applications, use +[our Next.js dynamic implementation](https://github.com/P5-wrapper/next): ```shell [npm|yarn|pnpm] [install|add] p5 @p5-wrapper/next @p5-wrapper/react ``` -Please continue reading these docs and also look at +See [the Next.js dynamic implementation docs](https://github.com/P5-wrapper/next) -for further supporting information. +for further details. -## Demo & Examples +
-### Live demo +## Demo & Examples A live demo can be viewed at -[P5-wrapper.github.io/react](https://P5-wrapper.github.io/react/). +[P5-wrapper.github.io/react](https://P5-wrapper.github.io/react/). The +repository also contains +[example sketches](https://github.com/P5-wrapper/react/tree/main/demo/sketches). -### Examples - -The repository contains further -[examples](https://github.com/P5-wrapper/react/tree/master/demo/sketches). - -To try them out for yourself fork the repository, be sure you have -[PNPM](https://pnpm.io/) installed and then run the following: +To run the examples locally: ```sh git clone git@github.com:/react.git @@ -96,11 +78,11 @@ pnpm install pnpm preview ``` -Then just open `http://localhost:3001` in a browser. +Then open `http://localhost:3001` in a browser. ## Usage -### Javascript +### JavaScript ```jsx import * as React from "react"; @@ -128,20 +110,14 @@ export function App() { ### TypeScript -TypeScript sketches can be declared in two different ways, below you will find -two ways to declare a sketch, both examples do the exact same thing. - -In short though, the component requires you to pass a `sketch` prop. The -`sketch` prop is simply a function which takes a `p5` instance as it's first and -only argument. - -#### Option 1: Declaring a sketch using the `P5CanvasInstance` type +The `sketch` prop is a function that receives a p5 instance. You can type it +using either the `Sketch` type or the `P5CanvasInstance` type: ```tsx import * as React from "react"; -import { P5Canvas, P5CanvasInstance } from "@p5-wrapper/react"; +import { P5Canvas, Sketch } from "@p5-wrapper/react"; -function sketch(p5: P5CanvasInstance) { +const sketch: Sketch = p5 => { p5.setup = () => p5.createCanvas(600, 400, p5.WEBGL); p5.draw = () => { @@ -154,33 +130,40 @@ function sketch(p5: P5CanvasInstance) { p5.plane(100); p5.pop(); }; -} +}; export function App() { return ; } ``` -#### Option 2: Declaring a sketch using the `Sketch` type +> `Sketch` auto-types the `p5` argument for you. If you prefer a regular +> `function` declaration, you can use `P5CanvasInstance` directly: +> +> ```ts +> import { P5CanvasInstance } from "@p5-wrapper/react"; +> +> function sketch(p5: P5CanvasInstance) { +> /* ... */ +> } +> ``` -Using the `Sketch` type has one nice benefit over using `P5CanvasInstance` and -that is that the `p5` argument passed to the sketch function is auto-typed as a -`P5CanvasInstance` for you. +### Using abstracted setup and draw functions -> Side note: -> -> In general, it comes down to personal preference as to how you declare your -> sketches and there is nothing wrong with using the `P5CanvasInstance` manually -> in a regular `function` declaration. +If you prefer to split your sketch logic into separate functions: -```tsx +```jsx import * as React from "react"; -import { P5Canvas, Sketch } from "@p5-wrapper/react"; +import { P5Canvas } from "@p5-wrapper/react"; -const sketch: Sketch = p5 => { - p5.setup = () => p5.createCanvas(600, 400, p5.WEBGL); +function setup(p5) { + return () => { + p5.createCanvas(600, 400, p5.WEBGL); + }; +} - p5.draw = () => { +function draw(p5) { + return () => { p5.background(250); p5.normalMaterial(); p5.push(); @@ -190,58 +173,38 @@ const sketch: Sketch = p5 => { p5.plane(100); p5.pop(); }; -}; +} + +function sketch(p5) { + p5.setup = setup(p5); + p5.draw = draw(p5); +} export function App() { return ; } ``` -#### TypeScript Generics - -We also support the use of Generics to add type definitions for your props. If -used, the props will be properly typed when the props are passed to the -`updateWithProps` method. - -To utilise generics you can use one of two methods. In both of the examples -below, we create a custom internal type called `MySketchProps` which is a union -type of `SketchProps` and a custom type which has a `rotation` key applied to -it. +### Props -> Side note: -> -> We could also write the `MySketchProps` type as an interface to do exactly the -> same thing if that is to your personal preference: -> -> ```ts -> interface MySketchProps extends SketchProps { -> rotation: number; -> } -> ``` +You can pass any custom props to `P5Canvas`. These are forwarded to the +`updateWithProps` method if you define it in your sketch. -This means, in these examples, that when the `rotation` prop that is provided as -part of the `props` passed to the `updateWithProps` function, it will be -correctly typed as a `number`. +#### Reacting to props -##### Usage with the `P5CanvasInstance` type +`updateWithProps` is called on initial render and whenever the props change: -```tsx -import { P5Canvas, P5CanvasInstance, SketchProps } from "@p5-wrapper/react"; +```jsx +import { P5Canvas } from "@p5-wrapper/react"; import React, { useEffect, useState } from "react"; -type MySketchProps = SketchProps & { - rotation: number; -}; - -function sketch(p5: P5CanvasInstance) { +function sketch(p5) { let rotation = 0; p5.setup = () => p5.createCanvas(600, 400, p5.WEBGL); p5.updateWithProps = props => { - if (props.rotation) { - rotation = (props.rotation * Math.PI) / 180; - } + rotation = (props.rotation * Math.PI) / 180; }; p5.draw = () => { @@ -273,7 +236,9 @@ export function App() { } ``` -##### Usage with the `Sketch` type +#### Typed props with generics + +Use generics so that props passed to `updateWithProps` are properly typed: ```tsx import { P5Canvas, Sketch, SketchProps } from "@p5-wrapper/react"; @@ -289,9 +254,7 @@ const sketch: Sketch = p5 => { p5.setup = () => p5.createCanvas(600, 400, p5.WEBGL); p5.updateWithProps = props => { - if (props.rotation) { - rotation = (props.rotation * Math.PI) / 180; - } + rotation = (props.rotation * Math.PI) / 180; }; p5.draw = () => { @@ -323,71 +286,43 @@ export function App() { } ``` -### Using abstracted setup and draw functions - -```jsx -import * as React from "react"; -import { P5Canvas } from "@p5-wrapper/react"; - -function setup(p5) { - return () => { - p5.createCanvas(600, 400, p5.WEBGL); - }; -} - -function draw(p5) { - return () => { - p5.background(250); - p5.normalMaterial(); - p5.push(); - p5.rotateZ(p5.frameCount * 0.01); - p5.rotateX(p5.frameCount * 0.01); - p5.rotateY(p5.frameCount * 0.01); - p5.plane(100); - p5.pop(); - }; -} - -function sketch(p5) { - p5.setup = setup(p5); - p5.draw = draw(p5); -} - -export function App() { - return ; -} -``` - -### Props - -The only required property is the sketch prop. The sketch prop is a function -that will be passed a p5 instance to use for rendering your sketches (see the -usage section above). +> You can also write `MySketchProps` as an interface: +> +> ```ts +> interface MySketchProps extends SketchProps { +> rotation: number; +> } +> ``` +> +> And if you prefer `P5CanvasInstance` over `Sketch`, generics work the same +> way: +> +> ```ts +> function sketch(p5: P5CanvasInstance) { +> /* ... */ +> } +> ``` -You can pass as many custom props as you want. These will be passed into the -updateWithProps method if you have defined it within your sketch. +### Custom updaters -#### Reacting to props +If you need to bridge React state updates from within the p5 lifecycle — for +example, reading `frameCount` or other instance properties to drive React state +— you can use the `updater` prop. It receives a callback that runs alongside +`updateWithProps` on every props change, but lives in the React layer: -In the below example you see the `updateWithProps` method being used. This is -called when the component initially renders and when the props passed to the -`P5Canvas` component are changed, if it is set within your sketch. This way we -can render our component and react to component prop changes directly within our -sketches! +```tsx +import { P5Canvas, Sketch, SketchProps, Updater } from "@p5-wrapper/react"; +import React, { useCallback, useState } from "react"; -```jsx -import { P5Canvas } from "@p5-wrapper/react"; -import React, { useEffect, useState } from "react"; +type MySketchProps = SketchProps & { rotation: number }; -function sketch(p5) { +const sketch: Sketch = p5 => { let rotation = 0; p5.setup = () => p5.createCanvas(600, 400, p5.WEBGL); p5.updateWithProps = props => { - if (props.rotation) { - rotation = (props.rotation * Math.PI) / 180; - } + rotation = (props.rotation * Math.PI) / 180; }; p5.draw = () => { @@ -399,35 +334,31 @@ function sketch(p5) { p5.box(100); p5.pop(); }; -} +}; export function App() { const [rotation, setRotation] = useState(0); + const [frameCount, setFrameCount] = useState(0); - useEffect(() => { - const interval = setInterval( - () => setRotation(rotation => rotation + 100), - 100 - ); - - return () => { - clearInterval(interval); - }; + const updater = useCallback>((instance, props) => { + setFrameCount(instance.frameCount); }, []); - return ; + return ; } ``` +The `updater` callback receives the p5 instance and the current sketch props. It +is not passed to `updateWithProps`, so it does not leak React concerns into your +sketch logic. + ### Children -To render a component on top of the sketch, you can add it as a child of the -`P5Canvas` component and then use the exported constant -`CanvasContainerClassName` in your css-in-js library of choice to style one -element above the other via css. +To render a component on top of the sketch, add it as a child of `P5Canvas` and +use the exported `CanvasContainerClassName` constant to style the overlay via +CSS. -For instance, using [styled components](https://styled-components.com), we could -center some text on top of our sketch like so: +
Example with styled-components ```jsx import { CanvasContainerClassName, P5Canvas } from "@p5-wrapper/react"; @@ -477,16 +408,14 @@ export function App() { } ``` -Of course, you can also use any other css-in-js library or by just using simple -css to achieve almost anything you can imagine just by using the wrapper class +
+ +You can use any CSS-in-JS library or plain CSS — just target the wrapper class as your root selector. ### Fallback UIs -Lets say you want to have a fallback UI in case the `sketch` ever falls out of -sync or is undefined for some reason. If this is a use case for you then you -call use the `fallback` prop to provide the necessary UI to show in the case -that the `sketch` becomes undefined. An example could be as follows: +If the `sketch` prop is undefined, you can provide a fallback UI: ```jsx import * as React from "react"; @@ -494,7 +423,6 @@ import { P5Canvas } from "@p5-wrapper/react"; function sketchOne(p5) { p5.setup = () => p5.createCanvas(600, 400, p5.WEBGL); - p5.draw = () => { p5.background(250); p5.normalMaterial(); @@ -509,7 +437,6 @@ function sketchOne(p5) { function sketchTwo(p5) { p5.setup = () => p5.createCanvas(600, 400, p5.WEBGL); - p5.draw = () => { p5.background(500); p5.normalMaterial(); @@ -547,47 +474,37 @@ export function App() { } ``` -In this case, by default the fallback UI containing -`

No sketch selected yet.

` will be rendered, then if you select a -sketch, it will be rendered until you choose to once again "show nothing" which -falls back to the fallback UI. +When no sketch is selected, the fallback UI is shown. Selecting a sketch +replaces it with the canvas. ### Error and Loading UIs -You can pass `error` and `loading` props to the `P5Canvas` component to provide -custom UIs for error and loading states. - -- The `error` state will trigger if the sketch or the wrapper encounter an - issue, otherwise a default error view will be shown. -- The `loading` state will trigger while the wrapper is being lazy loaded, - otherwise a default loading view will be shown. +You can pass `error` and `loading` props to customise what is shown when +something goes wrong or while the component is loading. #### Error UIs -To show a custom UI when an error occurs within the sketch or the component, you -can pass a function to the `error` prop. +Pass a function to the `error` prop to handle errors thrown within the sketch or +its children: ```tsx import * as React from "react"; import { P5Canvas, P5CanvasInstance } from "@p5-wrapper/react"; -// This child will throw an error, oh no! function ErrorChild() { throw new Error("oops"); } -// This view will catch the thrown error and give you access to what exactly was thrown. -function ErrorUI(error: any) { +function ErrorUI(error: unknown) { if (error instanceof Error) { return

An error occured: {error.message}

; } - return

An unknown error occured: {error.toString()}

; + return

An unknown error occured: {String(error)}

; } function sketch(p5: P5CanvasInstance) { p5.setup = () => p5.createCanvas(600, 400, p5.WEBGL); - p5.draw = () => { p5.background(250); p5.normalMaterial(); @@ -609,17 +526,13 @@ export function App() { } ``` -Instead of the sketch, this will render `

An error occured: oops

`. Note -that in truth, the `ErrorView` will **always** receive `any` values since JS / -TS allow you to `throw` whatever values you want to, this is why we have to add -the `error instanceof Error` check to be sure the value we got was actually an -`Error` instance and not some other value like a `number`, `string`, `object` or -anything else that could be thrown by JS / TS. +> JS/TS allow you to `throw` any value, not just `Error` instances. Always check +> `error instanceof Error` before accessing `.message`. #### Loading UIs -To show a custom UI while the sketch UI is being lazy loaded, you can pass a -function to the `loading` prop. +Pass a function to the `loading` prop to show a custom UI while the component is +being lazy loaded: ```tsx import * as React from "react"; @@ -631,7 +544,6 @@ function LoadingUI() { function sketch(p5: P5CanvasInstance) { p5.setup = () => p5.createCanvas(600, 400, p5.WEBGL); - p5.draw = () => { p5.background(250); p5.normalMaterial(); @@ -649,29 +561,14 @@ export function App() { } ``` -In the initial period between the sketch render starting and it's lazy loading -ending, the `LoadingUI` will be shown! - ## P5 plugins and constructors -As discussed in multiple issues such as -[#11](https://github.com/P5-wrapper/react/issues/11), -[#23](https://github.com/P5-wrapper/react/issues/23), -[#61](https://github.com/P5-wrapper/react/issues/61) and -[#62](https://github.com/P5-wrapper/react/issues/62), there seems to be -confusion as to how we can use P5 plugins and constructors out of the box. This -section aims to clarify these! +
Using plugins (e.g. p5.sound) -### Plugins - -Since P5 is being used in -[P5 instance mode](https://github.com/processing/p5.js/wiki/Global-and-instance-mode) -as part of this project, P5 will not automatically load global plugins like it -usually might in global mode. - -Let's say we want to use the -[P5 sound plugin](https://p5js.org/reference/#/libraries/p5.sound) in our -component, we could do the following: +Since P5 is used in +[instance mode](https://github.com/processing/p5.js/wiki/Global-and-instance-mode), +plugins are not loaded automatically. You need to set `p5` on the `window` +object before importing the plugin: ```tsx import * as p5 from "p5"; @@ -734,57 +631,43 @@ export default function App() { } ``` -In this Typescript + React example, we can see a few key things. - -- Firstly we need to set `p5` on the `window` object manually. This is because - `p5.sound` requires that it be executed client side only AND that `p5` be - available BEFORE it is imported into the global (`window`) scope. -- Secondly, we ensure that audio is played after a user action, in our case this - happens on a button click. This is because in some browsers, without waiting - for a user interaction before playing audio, the audio will be blocked by the - browser from playing at all. -- Thirdly and relevant especially to Safari users, Safari blocks audio from all - tabs by default, you will need to manually change this setting in your Safari - settings. This could affect other browsers but sadly this is a browser - decision and until [P5 Sound](https://github.com/processing/p5.js-sound) is - updated to support newer audio APIs and browser requirements. This could - happen at anytime in other places and is a - [P5 Sound](https://github.com/processing/p5.js-sound) issue most generally - because it does not ask for permissions by default, even though browsers have - been requiring it for some time. - -> **Note:** The above example requires support for +Key points: + +- `p5` must be set on `window` before importing the plugin. +- Audio must be triggered by a user action (e.g. a button click) — browsers + block autoplay. +- Safari blocks audio from all tabs by default; users may need to change this in + their browser settings. + +> **Note:** This example requires > [top level await](https://caniuse.com/mdn-javascript_operators_await_top_level), -> [dynamic import statements](https://caniuse.com/es6-module-dynamic-import) and -> [the stream API](https://caniuse.com/stream) to be supported in your browser. -> Furthermore, [the stream API](https://caniuse.com/stream) built into the -> browser requires that HTTPS is used to ensure secure data transmission. +> [dynamic imports](https://caniuse.com/es6-module-dynamic-import) and +> [the stream API](https://caniuse.com/stream) (HTTPS only). -### Constructors +
-To access P5 constructors such as `p5.Vector` or `p5.Envelope`, you need to use -the instance mode syntax instead. For example: +
Using constructors (e.g. p5.Vector) -| Constructor | Global mode accessor | Instance mode accessor | -| ----------- | -------------------- | ----------------------- | -| Vector | p5.Vector | p5.constructor.Vector | -| Envelope | p5.Envelope | p5.constructor.Envelope | +In instance mode, constructors are accessed via `p5.constructor` instead of the +global `p5` namespace: -So now that we know this, let's imagine we want a random 2D Vector instance. In -our `sketch` function we would simply call `p5.constructor.Vector.random2D()` -instead of `p5.Vector.random2D()`. This is because of how the -[P5 instance mode](https://github.com/processing/p5.js/wiki/Global-and-instance-mode) -was implemented by the P5 team. While I am not sure why they decided to change -the API for instance mode specifically, it is still quite simple to use the -constructs we are used to without much extra work involved. +| Constructor | Global mode | Instance mode | +| ----------- | ----------- | ----------------------- | +| Vector | p5.Vector | p5.constructor.Vector | +| Envelope | p5.Envelope | p5.constructor.Envelope | + +For example, to get a random 2D vector, call `p5.constructor.Vector.random2D()` +instead of `p5.Vector.random2D()`. + +
## Development -**NOTE:** The source code for the component is in the `src` directory. +The source code for the component is in the `src` directory. -To build, watch and serve the examples which will also watch the component -source, run: +To build, watch and serve the examples (which also watches the component +source): ```sh - pnpm preview +pnpm preview ``` diff --git a/config/vite/demo.ts b/config/vite/demo.ts index 1bc6b6bb..1e287535 100644 --- a/config/vite/demo.ts +++ b/config/vite/demo.ts @@ -9,10 +9,16 @@ export function demo(root: string): UserConfig { plugins: [react()], preview: { open: true }, build: { + chunkSizeWarningLimit: 1200, emptyOutDir: false, rollupOptions: { output: { - dir: resolve(root, "dist", "demo") + dir: resolve(root, "dist", "demo"), + manualChunks: moduleId => { + if (moduleId.includes("node_modules/p5")) { + return "p5"; + } + } } } } diff --git a/config/vite/library.ts b/config/vite/library.ts index 32daa0b0..6575ef5a 100644 --- a/config/vite/library.ts +++ b/config/vite/library.ts @@ -15,9 +15,6 @@ export function library(root: string): UserConfig { }), react() ], - esbuild: { - legalComments: "external" - }, build: { emptyOutDir: true, lib: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 83bfe659..86f49c60 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,16 +7,17 @@ settings: overrides: '@isaacs/brace-expansion@<=5.0.0': '>=5.0.1' ajv@>=7.0.0-alpha.0 <8.18.0: '>=8.18.0' + brace-expansion@>=2.0.0 <2.0.3: '>=2.0.3' + brace-expansion@>=4.0.0 <5.0.5: '>=5.0.5' diff@>=6.0.0 <8.0.3: '>=8.0.3' - flatted@<3.4.0: '>=3.4.0' flatted@<=3.4.1: '>=3.4.2' - lodash@>=4.0.0 <=4.17.22: '>=4.17.23' - minimatch@<3.1.3: '>=3.1.3' + lodash-es@<=4.17.23: '>=4.18.0' + lodash@<=4.17.23: '>=4.18.0' minimatch@<3.1.4: '>=3.1.4' - minimatch@>=10.0.0 <10.2.1: '>=10.2.1' - minimatch@>=10.0.0 <10.2.3: '>=10.2.3' - minimatch@>=9.0.0 <9.0.6: '>=9.0.6' minimatch@>=9.0.0 <9.0.7: '>=9.0.7' + minimatch@>=10.0.0 <10.2.3: '>=10.2.3' + picomatch@<2.3.2: '>=2.3.2' + picomatch@>=4.0.0 <4.0.4: '>=4.0.4' rollup@>=4.0.0 <4.59.0: '>=4.59.0' importers: @@ -1167,9 +1168,6 @@ packages: babel-plugin-react-compiler@1.0.0: resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==} - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} @@ -1183,13 +1181,14 @@ packages: resolution: {integrity: sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg==} hasBin: true - brace-expansion@2.0.2: - resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} - brace-expansion@5.0.4: resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} engines: {node: 18 || 20 || >=22} + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -1559,7 +1558,7 @@ packages: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} peerDependencies: - picomatch: ^3 || ^4 + picomatch: '>=4.0.4' peerDependenciesMeta: picomatch: optional: true @@ -2081,11 +2080,11 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - lodash-es@4.17.23: - resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} - lodash@4.17.23: - resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} @@ -2297,14 +2296,6 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - - picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} - engines: {node: '>=12'} - picomatch@4.0.4: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} @@ -3416,7 +3407,7 @@ snapshots: '@rushstack/terminal': 0.19.5(@types/node@25.5.0) '@rushstack/ts-command-line': 5.1.5(@types/node@25.5.0) diff: 8.0.3 - lodash: 4.17.23 + lodash: 4.18.1 minimatch: 10.2.4 resolve: 1.22.11 semver: 7.5.4 @@ -3514,7 +3505,7 @@ snapshots: dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 - picomatch: 4.0.3 + picomatch: 4.0.4 '@rushstack/node-core-library@5.19.1(@types/node@25.5.0)': dependencies: @@ -3596,7 +3587,7 @@ snapshots: '@babel/traverse': 7.29.0 '@babel/types': 7.29.0 javascript-natural-sort: 0.7.1 - lodash-es: 4.17.23 + lodash-es: 4.18.1 minimatch: 10.2.4 parse-imports-exports: 0.2.4 prettier: 3.8.1 @@ -4000,19 +3991,17 @@ snapshots: dependencies: '@babel/types': 7.29.0 - balanced-match@1.0.2: {} - balanced-match@4.0.4: {} baseline-browser-mapping@2.10.13: {} baseline-browser-mapping@2.9.7: {} - brace-expansion@2.0.2: + brace-expansion@5.0.4: dependencies: - balanced-match: 1.0.2 + balanced-match: 4.0.4 - brace-expansion@5.0.4: + brace-expansion@5.0.5: dependencies: balanced-match: 4.0.4 @@ -4925,7 +4914,7 @@ snapshots: chalk: 4.1.2 ci-info: 4.3.1 graceful-fs: 4.2.11 - picomatch: 4.0.3 + picomatch: 4.0.4 jiti@2.5.1: optional: true @@ -5065,9 +5054,9 @@ snapshots: dependencies: p-locate: 5.0.0 - lodash-es@4.17.23: {} + lodash-es@4.18.1: {} - lodash@4.17.23: {} + lodash@4.18.1: {} loose-envify@1.4.0: dependencies: @@ -5115,17 +5104,17 @@ snapshots: micromatch@4.0.8: dependencies: braces: 3.0.3 - picomatch: 2.3.1 + picomatch: 4.0.4 min-indent@1.0.1: {} minimatch@10.2.4: dependencies: - brace-expansion: 5.0.4 + brace-expansion: 5.0.5 minimatch@9.0.9: dependencies: - brace-expansion: 2.0.2 + brace-expansion: 5.0.4 minipass@7.1.3: {} @@ -5290,10 +5279,6 @@ snapshots: picocolors@1.1.1: {} - picomatch@2.3.1: {} - - picomatch@4.0.3: {} - picomatch@4.0.4: {} pixelmatch@7.1.0: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index cb57dc1e..73503599 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -8,16 +8,17 @@ onlyBuiltDependencies: overrides: "@isaacs/brace-expansion@<=5.0.0": ">=5.0.1" ajv@>=7.0.0-alpha.0 <8.18.0: ">=8.18.0" + brace-expansion@>=2.0.0 <2.0.3: ">=2.0.3" + brace-expansion@>=4.0.0 <5.0.5: ">=5.0.5" diff@>=6.0.0 <8.0.3: ">=8.0.3" - flatted@<3.4.0: ">=3.4.0" flatted@<=3.4.1: ">=3.4.2" - lodash@>=4.0.0 <=4.17.22: ">=4.17.23" - minimatch@<3.1.3: ">=3.1.3" + lodash-es@<=4.17.23: ">=4.18.0" + lodash@<=4.17.23: ">=4.18.0" minimatch@<3.1.4: ">=3.1.4" - minimatch@>=10.0.0 <10.2.1: ">=10.2.1" - minimatch@>=10.0.0 <10.2.3: ">=10.2.3" - minimatch@>=9.0.0 <9.0.6: ">=9.0.6" minimatch@>=9.0.0 <9.0.7: ">=9.0.7" + minimatch@>=10.0.0 <10.2.3: ">=10.2.3" + picomatch@<2.3.2: ">=2.3.2" + picomatch@>=4.0.0 <4.0.4: ">=4.0.4" rollup@>=4.0.0 <4.59.0: ">=4.59.0" peerDependencyRules: diff --git a/src/components/P5CanvasGuard.tsx b/src/components/P5CanvasGuard.tsx index a6f50562..0e677d80 100644 --- a/src/components/P5CanvasGuard.tsx +++ b/src/components/P5CanvasGuard.tsx @@ -1,35 +1,38 @@ import * as React from "react"; +import P5CanvasWithSketch from "@components/P5CanvasWithSketch"; import { type P5CanvasProps } from "@contracts/P5CanvasProps"; -import { type P5CanvasPropsWithSketch } from "@contracts/P5CanvasPropsWithSketch"; -import { type SketchProps } from "@contracts/SketchProps"; import { logErrorBoundaryError } from "@utils/logErrorBoundaryError"; import { ReactNode } from "react"; import { FallbackProps } from "react-error-boundary"; -const P5CanvasWithSketch = React.lazy( - () => import("@components/P5CanvasWithSketch") -); - const ErrorBoundary = React.lazy(() => import("react-error-boundary").then(m => ({ default: m.ErrorBoundary })) ); -const P5CanvasGuard = ( - props: P5CanvasProps -) => { - if (props.sketch === undefined) { +const P5CanvasGuard = (props: P5CanvasProps) => { + const { + sketch, + updater, + fallback, + loading, + error, + children, + ...sketchProps + } = props; + + if (sketch === undefined) { console.error("[P5Canvas] The `sketch` prop is required."); - return props.fallback?.() ?? null; + return fallback?.() ?? null; } return ( { return ( - props.error?.(info.error) ?? ( + error?.(info.error) ?? (

❌ - Something went wrong

) ); @@ -39,14 +42,15 @@ const P5CanvasGuard = ( }} > 🚀 Loading...

- } + fallback={loading?.() ??

🚀 Loading...

} > )} - /> + sketch={sketch} + updater={updater} + sketchProps={sketchProps} + > + {children} +
); diff --git a/src/components/P5CanvasWithSketch.tsx b/src/components/P5CanvasWithSketch.tsx index 7916f198..e50805a1 100644 --- a/src/components/P5CanvasWithSketch.tsx +++ b/src/components/P5CanvasWithSketch.tsx @@ -2,28 +2,29 @@ import * as React from "react"; import { CanvasContainerClassName } from "@constants/CanvasContainerClassName"; import { type CanvasContainerRef } from "@contracts/CanvasContainerRef"; import { type P5CanvasInstanceRef } from "@contracts/P5CanvasInstanceRef"; -import { type P5CanvasPropsWithSketch } from "@contracts/P5CanvasPropsWithSketch"; +import { type Sketch } from "@contracts/Sketch"; import { type SketchProps } from "@contracts/SketchProps"; +import { type Updater } from "@contracts/Updater"; import { removeP5CanvasInstance } from "@utils/removeP5CanvasInstance"; import { updateP5CanvasInstance } from "@utils/updateP5CanvasInstance"; -import { withoutKeys } from "@utils/withoutKeys"; +import { type ReactNode } from "react"; -const P5CanvasWithSketch = ( - props: P5CanvasPropsWithSketch -) => { +interface P5CanvasWithSketchProps { + sketch: Sketch; + updater?: Updater; + sketchProps: SketchProps; + children?: ReactNode; +} + +const P5CanvasWithSketch = (props: P5CanvasWithSketchProps) => { const canvasContainerRef: CanvasContainerRef = React.useRef(null); - const p5CanvasInstanceRef: P5CanvasInstanceRef = React.useRef(null); - const sketchProps: SketchProps = React.useMemo( - () => - withoutKeys(props, [ - "sketch", - "fallback", - "loading", - "error", - "children" - ]), - [props] - ); + const p5CanvasInstanceRef: P5CanvasInstanceRef = + React.useRef(null); + const updaterRef = React.useRef(props.updater); + + React.useEffect(() => { + updaterRef.current = props.updater; + }, [props.updater]); React.useEffect(() => { p5CanvasInstanceRef.current = updateP5CanvasInstance( @@ -35,10 +36,12 @@ const P5CanvasWithSketch = ( React.useEffect(() => { /** @see https://github.com/P5-wrapper/react/discussions/360 */ - p5CanvasInstanceRef.current?.updateWithProps?.( - sketchProps as unknown as Props - ); - }, [sketchProps, canvasContainerRef, p5CanvasInstanceRef]); + p5CanvasInstanceRef.current?.updateWithProps?.(props.sketchProps); + + if (updaterRef.current && p5CanvasInstanceRef.current) { + updaterRef.current(p5CanvasInstanceRef.current, props.sketchProps); + } + }, [props.sketchProps, canvasContainerRef, p5CanvasInstanceRef]); React.useEffect(() => () => removeP5CanvasInstance(p5CanvasInstanceRef), []); diff --git a/src/contracts/InputProps.ts b/src/contracts/P5CanvasInternalProps.ts similarity index 50% rename from src/contracts/InputProps.ts rename to src/contracts/P5CanvasInternalProps.ts index 03c53fa3..ffef7dc9 100644 --- a/src/contracts/InputProps.ts +++ b/src/contracts/P5CanvasInternalProps.ts @@ -1,11 +1,15 @@ import { type Sketch } from "@contracts/Sketch"; import { type SketchProps } from "@contracts/SketchProps"; +import { type Updater } from "@contracts/Updater"; import { type ReactNode } from "react"; -import { type FallbackProps } from "react-error-boundary"; -export type InputProps = Props & { +export interface P5CanvasInternalProps< + Props extends SketchProps = SketchProps +> { sketch?: Sketch; + updater?: Updater; fallback?: () => ReactNode; loading?: () => ReactNode; - error?: (error: FallbackProps["error"]) => ReactNode; -}; + error?: (error: unknown) => ReactNode; + children?: ReactNode; +} diff --git a/src/contracts/P5CanvasProps.ts b/src/contracts/P5CanvasProps.ts index 3a0722db..684faef5 100644 --- a/src/contracts/P5CanvasProps.ts +++ b/src/contracts/P5CanvasProps.ts @@ -1,6 +1,5 @@ -import { type InputProps } from "@contracts/InputProps"; +import { type P5CanvasInternalProps } from "@contracts/P5CanvasInternalProps"; import { type SketchProps } from "@contracts/SketchProps"; -import { type WithChildren } from "@contracts/WithChildren"; export type P5CanvasProps = - WithChildren>; + P5CanvasInternalProps & Props; diff --git a/src/contracts/P5CanvasPropsWithSketch.ts b/src/contracts/P5CanvasPropsWithSketch.ts deleted file mode 100644 index 8a75061a..00000000 --- a/src/contracts/P5CanvasPropsWithSketch.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { type P5CanvasProps } from "@contracts/P5CanvasProps"; -import { type Sketch } from "@contracts/Sketch"; -import { type SketchProps } from "@contracts/SketchProps"; - -export type P5CanvasPropsWithSketch = - P5CanvasProps & { sketch: Sketch }; diff --git a/src/contracts/Updater.ts b/src/contracts/Updater.ts new file mode 100644 index 00000000..52f87e56 --- /dev/null +++ b/src/contracts/Updater.ts @@ -0,0 +1,7 @@ +import { type P5CanvasInstance } from "@contracts/P5CanvasInstance"; +import { type SketchProps } from "@contracts/SketchProps"; + +export type Updater = ( + instance: P5CanvasInstance, + props: Props +) => void; diff --git a/src/contracts/WithChildren.ts b/src/contracts/WithChildren.ts deleted file mode 100644 index 73f11e3b..00000000 --- a/src/contracts/WithChildren.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { type ReactNode } from "react"; - -export type WithChildren = T & { children?: ReactNode }; diff --git a/src/main.tsx b/src/main.tsx index eb272f07..29c5c265 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -4,3 +4,4 @@ export { type P5CanvasInstance } from "@contracts/P5CanvasInstance"; export { type P5CanvasProps } from "@contracts/P5CanvasProps"; export { type Sketch } from "@contracts/Sketch"; export { type SketchProps } from "@contracts/SketchProps"; +export { type Updater } from "@contracts/Updater"; diff --git a/src/utils/logErrorBoundaryError.ts b/src/utils/logErrorBoundaryError.ts index 8761309e..d155ba86 100644 --- a/src/utils/logErrorBoundaryError.ts +++ b/src/utils/logErrorBoundaryError.ts @@ -22,28 +22,28 @@ function createErrorMessage(error: unknown): string { return `${error.name}("${error.message}")`; } - if (typeof error === "symbol" || error instanceof Symbol) { + if (typeof error === "symbol") { return error.toString(); } - if (typeof error === "string" || error instanceof String) { - return `String("${error.toString()}")`; + if (typeof error === "string") { + return `String("${error}")`; } - if (typeof error === "number" || error instanceof Number) { - return `Number(${error.toString()})`; + if (typeof error === "number") { + return `Number(${error})`; } - if (typeof error === "bigint" || error instanceof BigInt) { - return `BigInt(${error.toString()})`; + if (typeof error === "bigint") { + return `BigInt(${error})`; } if (error instanceof Array) { - return `Array(${JSON.stringify(error.values().toArray())})`; + return `Array(${JSON.stringify([...error])})`; } if (error instanceof Set) { - return `Set(${JSON.stringify(error.values().toArray())})`; + return `Set(${JSON.stringify([...error])})`; } if (Object.getPrototypeOf(error) === Object.prototype) { diff --git a/src/utils/withoutKeys.ts b/src/utils/withoutKeys.ts deleted file mode 100644 index c3909430..00000000 --- a/src/utils/withoutKeys.ts +++ /dev/null @@ -1,16 +0,0 @@ -export function withoutKeys( - record: Record, - keysToIgnore: string[] -) { - if (keysToIgnore.length === 0) { - return record; - } - - return Object.keys(record) - .filter(key => !keysToIgnore.includes(key)) - .reduce>((accumulator, current) => { - accumulator[current] = record[current]; - - return accumulator; - }, {}); -} diff --git a/tests/components/P5Canvas.test.tsx b/tests/components/P5Canvas.test.tsx index 7d6f70f0..faf65886 100644 --- a/tests/components/P5Canvas.test.tsx +++ b/tests/components/P5Canvas.test.tsx @@ -2,6 +2,7 @@ import { P5Canvas } from "@components/P5Canvas"; import { CanvasContainerClassName } from "@constants/CanvasContainerClassName"; import { type P5CanvasInstance } from "@contracts/P5CanvasInstance"; import { type Sketch } from "@contracts/Sketch"; +import { type Updater } from "@contracts/Updater"; import { render, RenderResult, waitFor } from "@testing-library/react"; import { renderToStaticMarkup, renderToString } from "react-dom/server"; import { assert, describe, expect, it, vi } from "vitest"; @@ -335,4 +336,78 @@ describe("P5Canvas", () => { expect(updateFunction).toHaveBeenCalledWith({ y: 100 }); }); }); + + describe("Updater", () => { + it("Calls the `updater` with the p5 instance and sketch props when the component is mounted", async () => { + const updater = vi.fn(); + const sketch = createSketch(); + + const { findByTestId } = render( + + ); + + await waitForCanvas(findByTestId); + + expect(updater).toHaveBeenCalledOnce(); + + const [instance, props] = updater.mock.calls[0]; + + expect(instance).toHaveProperty("setup"); + expect(props).toEqual({ x: 100 }); + }); + + it("Calls the `updater` with updated props when a prop value changes", async () => { + const updater = vi.fn(); + const sketch = createSketch(); + const { rerender, findByTestId } = render( + + ); + + await waitForCanvas(findByTestId); + + rerender(); + + expect(updater).toHaveBeenCalledTimes(2); + + const [instance, props] = updater.mock.calls[1]; + + expect(instance).toHaveProperty("setup"); + expect(props).toEqual({ x: 200 }); + }); + + it("Calls the new `updater` when the updater reference changes", async () => { + const firstUpdater = vi.fn(); + const secondUpdater = vi.fn(); + const sketch = createSketch(); + const { rerender, findByTestId } = render( + + ); + + await waitForCanvas(findByTestId); + + expect(firstUpdater).toHaveBeenCalledOnce(); + + rerender(); + + expect(secondUpdater).toHaveBeenCalledOnce(); + + const [, props] = secondUpdater.mock.calls[0]; + + expect(props).toEqual({ x: 100 }); + }); + + it("Does not pass the `updater` to `updateWithProps`", async () => { + const updateFunction = vi.fn(); + const updater = vi.fn(); + const sketch = createSketch(updateFunction); + + const { findByTestId } = render( + + ); + + await waitForCanvas(findByTestId); + + expect(updateFunction).toHaveBeenCalledWith({ x: 100 }); + }); + }); }); diff --git a/tests/utils/logErrorBoundaryError.test.ts b/tests/utils/logErrorBoundaryError.test.ts index 4288e93f..158a3ba5 100644 --- a/tests/utils/logErrorBoundaryError.test.ts +++ b/tests/utils/logErrorBoundaryError.test.ts @@ -39,7 +39,7 @@ describe("logErrorBoundaryError", () => { ); }); - it("Logs the error correctly when provided a `String` instance", () => { + it("Logs the error correctly when provided a string", () => { logErrorBoundaryError("A string message"); expect(errorLoggerSpy).toHaveBeenCalledOnce(); @@ -53,7 +53,7 @@ describe("logErrorBoundaryError", () => { ); }); - it("Logs the error correctly when provided a `Number` instance", () => { + it("Logs the error correctly when provided a number", () => { logErrorBoundaryError(123); expect(errorLoggerSpy).toHaveBeenCalledOnce(); @@ -67,7 +67,7 @@ describe("logErrorBoundaryError", () => { ); }); - it("Logs the error correctly when provided a `BigInt` instance", () => { + it("Logs the error correctly when provided a bigint", () => { logErrorBoundaryError(BigInt(123)); expect(errorLoggerSpy).toHaveBeenCalledOnce(); @@ -123,7 +123,7 @@ describe("logErrorBoundaryError", () => { ); }); - it("Logs the error correctly when provided a `Symbol` instance", () => { + it("Logs the error correctly when provided a symbol", () => { logErrorBoundaryError(Symbol("test")); expect(errorLoggerSpy).toHaveBeenCalledOnce(); diff --git a/tests/utils/withoutKeys.test.ts b/tests/utils/withoutKeys.test.ts deleted file mode 100644 index caa120f5..00000000 --- a/tests/utils/withoutKeys.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { withoutKeys } from "@utils/withoutKeys"; -import { describe, expect, it } from "vitest"; - -describe("withoutKeys", () => { - it("Returns the original object if the keys to ignore list is empty", () => { - const object = { a: 1, b: 2 }; - const updated = withoutKeys(object, []); - - expect(object).toEqual(updated); - }); - - it("Removes all keys provided in the ignore list", () => { - const object = { a: 1, b: 2, c: 3, d: 4 }; - const updated = withoutKeys(object, ["b", "d"]); - - expect(object).not.toEqual(updated); - expect(Object.keys(updated)).toEqual(["a", "c"]); - }); -});