diff --git a/examples/form/components/SubmittedFields.jsx b/examples/form/components/SubmittedFields.jsx index 6f65ba09..74e014d8 100644 --- a/examples/form/components/SubmittedFields.jsx +++ b/examples/form/components/SubmittedFields.jsx @@ -7,11 +7,9 @@ export function SubmittedFields({ state, updateState }) { return null; } - delete value.dialog; - return ( updateState({ dialog: { value: null } })}> -
{JSON.stringify(value, null, 2)}
+
{JSON.stringify({ ...value, dialog: undefined }, null, 2)}
); } diff --git a/packages/helpers/src/dom.test.js b/packages/helpers/src/dom.test.js index d9d6b2e8..e3c64def 100644 --- a/packages/helpers/src/dom.test.js +++ b/packages/helpers/src/dom.test.js @@ -167,7 +167,7 @@ describe('helpers/dom', () => { // Simulate the animation frame before the element is added triggerAnimationFrame(); - expect(window.requestAnimationFrame).toHaveBeenCalledTimes(1); + expect(window.requestAnimationFrame).toHaveBeenCalledOnce(); expect(el.scrollIntoView).toHaveBeenCalledWith({ behavior: 'auto' }); }); diff --git a/packages/hooks/src/useTreeCollapse/useTreeCollapse.js b/packages/hooks/src/useTreeCollapse/useTreeCollapse.js index b7006994..2049e804 100644 --- a/packages/hooks/src/useTreeCollapse/useTreeCollapse.js +++ b/packages/hooks/src/useTreeCollapse/useTreeCollapse.js @@ -1,5 +1,5 @@ import { getSubtreeSize } from '@barso/helpers'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useEffect, useReducer, useRef } from 'react'; /** * @typedef {Object} TreeNode @@ -41,68 +41,47 @@ export function useTreeCollapse({ additionalBudget = 10, defaultExpandedId = null, } = {}) { - const lastParamsRef = useRef({ totalBudget, minimalSubTree, defaultExpandedId }); - - const [nodeStates, setNodeStates] = useState(() => - computeNodeStates({ - minimalSubTree, - nodes, - totalBudget, - defaultExpandedId, - }), + const [nodeStates, dispatch] = useReducer( + (previousState, action = {}) => { + switch (action.type) { + case 'EXPAND_NODE': + return expandChildren({ additionalBudget, minimalSubTree, previousState, ...action }); + case 'COLLAPSE_NODE': + return collapseChildren({ previousState, ...action }); + case 'UPDATE_STATE': + return computeNodeStates({ defaultExpandedId, minimalSubTree, nodes, totalBudget, previousState, ...action }); + case 'RESET_STATE': + default: + return computeNodeStates({ defaultExpandedId, minimalSubTree, nodes, totalBudget, ...action }); + } + }, + { minimalSubTree, nodes, totalBudget, defaultExpandedId }, + computeNodeStates, ); + const lastParamsRef = useRef(); + useEffect(() => { + if (!lastParamsRef.current) { + lastParamsRef.current = { totalBudget, minimalSubTree, defaultExpandedId }; + return; + } + const shouldUsePrevious = lastParamsRef.current.totalBudget === totalBudget && lastParamsRef.current.minimalSubTree === minimalSubTree && lastParamsRef.current.defaultExpandedId === defaultExpandedId; if (shouldUsePrevious) { - setNodeStates((previousState) => - computeNodeStates({ - minimalSubTree, - nodes, - previousState, - totalBudget, - defaultExpandedId, - }), - ); + dispatch({ type: 'UPDATE_STATE' }); } else { lastParamsRef.current = { totalBudget, minimalSubTree, defaultExpandedId }; - setNodeStates( - computeNodeStates({ - minimalSubTree, - nodes, - totalBudget, - defaultExpandedId, - }), - ); + dispatch({ type: 'RESET_STATE' }); } }, [defaultExpandedId, nodes, minimalSubTree, totalBudget]); - const handleExpand = useCallback( - (targetId) => { - setNodeStates((previousState) => - expandChildren({ - additionalBudget, - minimalSubTree, - previousState, - targetId, - }), - ); - }, - [additionalBudget, minimalSubTree], - ); - - const handleCollapse = useCallback((targetId) => { - setNodeStates((previousState) => - collapseChildren({ - previousState, - targetId, - }), - ); - }, []); + const handleExpand = (targetId) => dispatch({ type: 'EXPAND_NODE', targetId }); + const handleCollapse = (targetId) => dispatch({ type: 'COLLAPSE_NODE', targetId }); return { handleCollapse, diff --git a/packages/infra/src/logger/axiom-transport.test.js b/packages/infra/src/logger/axiom-transport.test.js index ac196b35..81b078e2 100644 --- a/packages/infra/src/logger/axiom-transport.test.js +++ b/packages/infra/src/logger/axiom-transport.test.js @@ -4,10 +4,10 @@ const mocks = vi.hoisted(() => { const waitUntil = vi.fn().mockImplementation((promise) => promise); const ingest = vi.fn(); const flush = vi.fn().mockResolvedValue(); - const Axiom = vi.fn().mockImplementation(() => ({ - ingest, - flush, - })); + const Axiom = vi.fn().mockImplementation(function () { + this.ingest = ingest; + this.flush = flush; + }); return { ingest, @@ -102,8 +102,7 @@ describe('axiomTransport', () => { await transport.flush(); - expect(mocks.waitUntil).toHaveBeenCalledWith(mocks.flush()); - expect(mocks.waitUntil).toHaveBeenCalledOnce(); + expect(mocks.waitUntil).toHaveBeenCalledExactlyOnceWith(mocks.flush()); }); it('should flush with logs', async () => { @@ -121,8 +120,7 @@ describe('axiomTransport', () => { expect.objectContaining({ level: 'error', msg: 'test log 2' }), ); - expect(mocks.waitUntil).toHaveBeenCalledWith(mocks.flush()); - expect(mocks.waitUntil).toHaveBeenCalledOnce(); + expect(mocks.waitUntil).toHaveBeenCalledExactlyOnceWith(mocks.flush()); }); it('should not ingest invalid logs', async () => { @@ -169,10 +167,10 @@ describe('axiomTransport', () => { it('should continue logging even if an error occurs with Axiom', async () => { const error = new Error('test error'); - mocks.Axiom.mockImplementationOnce(({ onError }) => ({ - ingest: mocks.ingest.mockImplementationOnce(() => onError(error)), - flush: mocks.flush, - })); + mocks.Axiom.mockImplementationOnce(function ({ onError }) { + this.ingest = mocks.ingest.mockImplementationOnce(() => onError(error)); + this.flush = mocks.flush; + }); const transport = axiomTransport({ dataset, token }); diff --git a/packages/infra/src/logger/logger.test.js b/packages/infra/src/logger/logger.test.js index bb7cef14..51611f4a 100644 --- a/packages/infra/src/logger/logger.test.js +++ b/packages/infra/src/logger/logger.test.js @@ -19,13 +19,14 @@ const token = 'test-token'; describe('infra/logger', () => { const mocks = vi.hoisted(() => ({ axiomIngest: vi.fn(), + flush: vi.fn().mockResolvedValue(), })); vi.mock('@axiomhq/js', () => ({ - Axiom: vi.fn().mockImplementation(() => ({ - ingest: mocks.axiomIngest, - flush: vi.fn().mockResolvedValue(), - })), + Axiom: vi.fn().mockImplementation(function () { + this.ingest = mocks.axiomIngest; + this.flush = mocks.flush; + }), })); let stdoutSpy; diff --git a/packages/ui/src/FormField/FormField.jsx b/packages/ui/src/FormField/FormField.jsx index 4b46cb32..c5800980 100644 --- a/packages/ui/src/FormField/FormField.jsx +++ b/packages/ui/src/FormField/FormField.jsx @@ -57,10 +57,11 @@ export const FormField = forwardRef( const inputProps = { validationStatus: error ? 'error' : isValid ? 'success' : null, inputMode, - ref, ...props, }; + inputProps.ref = ref; + if (type === 'password') { function focusAfterEnd() { setTimeout(() => { diff --git a/packages/ui/src/FormField/FormField.test.js b/packages/ui/src/FormField/FormField.test.js index 592e391c..897a41a9 100644 --- a/packages/ui/src/FormField/FormField.test.js +++ b/packages/ui/src/FormField/FormField.test.js @@ -382,7 +382,7 @@ describe('ui', () => { const handleClick = vi.fn(); render(); fireEvent.click(screen.getByRole('button')); - expect(handleClick).toHaveBeenCalledTimes(1); + expect(handleClick).toHaveBeenCalledOnce(); }); it('calls ignoreClick when ignore button is clicked', () => { diff --git a/packages/ui/src/GoToTopButton/GoToTopButton.test.jsx b/packages/ui/src/GoToTopButton/GoToTopButton.test.jsx index 42f389d3..08a2639b 100644 --- a/packages/ui/src/GoToTopButton/GoToTopButton.test.jsx +++ b/packages/ui/src/GoToTopButton/GoToTopButton.test.jsx @@ -7,7 +7,7 @@ import { GoToTopButton } from './GoToTopButton.jsx'; const IntersectionObserver = global.IntersectionObserver; let lastObserver = null; -const MockIntersectionObserver = vi.fn((callback) => { +const MockIntersectionObserver = vi.fn(function (callback) { lastObserver = { callback, observe: vi.fn(), @@ -48,8 +48,7 @@ describe('ui', () => { ); expect(screen.queryByRole('button')).toBeNull(); - expect(lastObserver.observe).toHaveBeenCalledTimes(1); - expect(lastObserver.observe).toHaveBeenCalledWith(screen.getByTestId('target')); + expect(lastObserver.observe).toHaveBeenCalledExactlyOnceWith(screen.getByTestId('target')); act(() => lastObserver.callback([{ isIntersecting: false }])); expect(screen.getByRole('button')).toBeVisible(); @@ -62,8 +61,7 @@ describe('ui', () => { render(, { container: container }); - expect(lastObserver.observe).toHaveBeenCalledTimes(1); - expect(lastObserver.observe).toHaveBeenCalledWith(targetElement); + expect(lastObserver.observe).toHaveBeenCalledExactlyOnceWith(targetElement); act(() => lastObserver.callback([{ isIntersecting: false }])); expect(screen.getByRole('button')).toBeVisible(); @@ -102,8 +100,7 @@ describe('ui', () => { window.scrollTo = originalScrollTo; - expect(scrollToMock).toHaveBeenCalledTimes(1); - expect(scrollToMock).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' }); + expect(scrollToMock).toHaveBeenCalledExactlyOnceWith({ top: 0, behavior: 'smooth' }); }); it('logs a warning if the target element is not found', () => { @@ -123,12 +120,12 @@ describe('ui', () => { , ); - expect(MockIntersectionObserver).toHaveBeenCalledTimes(1); - expect(lastObserver.observe).toHaveBeenCalledTimes(1); + expect(MockIntersectionObserver).toHaveBeenCalledOnce(); + expect(lastObserver.observe).toHaveBeenCalledOnce(); unmount(); - expect(lastObserver.disconnect).toHaveBeenCalledTimes(1); + expect(lastObserver.disconnect).toHaveBeenCalledOnce(); }); }); }); diff --git a/packages/ui/src/Markdown/Markdown.jsx b/packages/ui/src/Markdown/Markdown.jsx index ee9573a2..e90aeb7a 100644 --- a/packages/ui/src/Markdown/Markdown.jsx +++ b/packages/ui/src/Markdown/Markdown.jsx @@ -9,9 +9,9 @@ import mathLocale from '@bytemd/plugin-math/locales/pt_BR.json'; import mermaidPlugin from '@bytemd/plugin-mermaid'; import mermaidLocale from '@bytemd/plugin-mermaid/locales/pt_BR.json'; import { Editor as ByteMdEditor, Viewer as ByteMdViewer } from '@bytemd/react'; -import { Box, useTheme } from '@primer/react'; +import { useTheme } from '@primer/react'; import byteMDLocale from 'bytemd/locales/pt_BR.json'; -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import { anchorHeadersPlugin, @@ -57,30 +57,15 @@ function usePlugins({ areLinksTrusted, clobberPrefix, shouldAddNofollow }) { return plugins; } -export function MarkdownViewer({ value: _value, areLinksTrusted, clobberPrefix, shouldAddNofollow, ...props }) { +export function MarkdownViewer({ areLinksTrusted, clobberPrefix, shouldAddNofollow, ...props }) { clobberPrefix = clobberPrefix?.toLowerCase(); const bytemdPluginList = usePlugins({ areLinksTrusted, clobberPrefix, shouldAddNofollow }); - const [value, setValue] = useState(_value); - - useEffect(() => { - let timeout; - - setValue((value) => { - timeout = setTimeout(() => setValue(value)); - return value + '\n\u0160'; - }); - - return () => clearTimeout(timeout); - }, [bytemdPluginList]); - - useEffect(() => setValue(_value), [_value]); return ( ); @@ -115,7 +100,7 @@ export function MarkdownEditor({ }, []); return ( - +
- +
); } diff --git a/packages/ui/src/Notifications/NotificationList.test.jsx b/packages/ui/src/Notifications/NotificationList.test.jsx index b451e209..8fc4b2e7 100644 --- a/packages/ui/src/Notifications/NotificationList.test.jsx +++ b/packages/ui/src/Notifications/NotificationList.test.jsx @@ -172,10 +172,10 @@ describe('ui/Notifications', () => { fireEvent.keyPress(item, { key: 'Enter', code: 'Enter', charCode: 13 }); expect(onItemSelect).toHaveBeenCalledWith(notifications[0]); - expect(onItemSelect).toHaveBeenCalledTimes(1); + expect(onItemSelect).toHaveBeenCalledOnce(); fireEvent.keyPress(item, { key: 'Escape', code: 'Escape', charCode: 27 }); - expect(onItemSelect).toHaveBeenCalledTimes(1); // Escape should not trigger onItemSelect + expect(onItemSelect).toHaveBeenCalledOnce(); // Escape should not trigger onItemSelect fireEvent.keyPress(item, { key: ' ', code: 'Space', charCode: 32 }); expect(onItemSelect).toHaveBeenCalledWith(notifications[0]); diff --git a/packages/ui/src/Notifications/NotificationMenu.test.jsx b/packages/ui/src/Notifications/NotificationMenu.test.jsx index 5590193e..c649768c 100644 --- a/packages/ui/src/Notifications/NotificationMenu.test.jsx +++ b/packages/ui/src/Notifications/NotificationMenu.test.jsx @@ -115,7 +115,7 @@ describe('ui/Notifications/NotificationMenu', () => { fireEvent.click(closeButton); expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); - expect(onCloseMenu).toHaveBeenCalledTimes(1); + expect(onCloseMenu).toHaveBeenCalledOnce(); }); }); diff --git a/packages/ui/src/_document.test.js b/packages/ui/src/_document.test.js index 00c5502d..29bdbfea 100644 --- a/packages/ui/src/_document.test.js +++ b/packages/ui/src/_document.test.js @@ -90,11 +90,11 @@ const hoisted = vi.hoisted(() => { const collectStyles = vi.fn(); const getStyleElement = vi.fn(() => ); const seal = vi.fn(); - const ServerStyleSheet = vi.fn(() => ({ - collectStyles, - getStyleElement, - seal, - })); + const ServerStyleSheet = vi.fn(function () { + this.collectStyles = collectStyles; + this.getStyleElement = getStyleElement; + this.seal = seal; + }); const renderPage = vi.fn((App) => collectStyles(App)); const getInitialProps = vi.fn(() => ({})); diff --git a/tests/setup.js b/tests/setup.js index e625aa35..9bb0e8fc 100644 --- a/tests/setup.js +++ b/tests/setup.js @@ -14,4 +14,13 @@ if (typeof document !== 'undefined') { removeListener: vi.fn(), }), }); + + // https://github.com/jsdom/jsdom/issues/3998 + if (!document.adoptedStyleSheets) { + Object.defineProperty(document, 'adoptedStyleSheets', { + writable: true, + configurable: true, + value: [], + }); + } }