From 6f11cae17bc74964a3dc0f9b265ae37ac0202cbb Mon Sep 17 00:00:00 2001 From: jackiejou <21050234+jackiejou@users.noreply.github.com> Date: Fri, 27 Feb 2026 14:39:01 -0800 Subject: [PATCH] feat(popup-reply): add threaded annotation feature flag - Add isThreadedAnnotation feature flag to conditionally render PopupReplyV2 - Refactor PopupReply Props to discriminated union (LegacyProps | ThreadedProps) - Migrate PopupReply tests from Enzyme to React Testing Library - Add @testing-library/react, @testing-library/dom, @testing-library/jest-dom deps - Add jest-setup.ts for global jest-dom matcher registration - Add test coverage for isThreadedAnnotation in PopupContainer and PopupLayer - Add tests for useEffect popper update and options referential integrity --- jest.config.js | 2 +- package.json | 3 + scripts/jest/jest-setup.ts | 1 + src/BoxAnnotations.ts | 3 +- src/components/Popups/PopupReply.tsx | 36 +++- src/components/Popups/PopupReplyV2.tsx | 10 + .../Popups/__tests__/PopupReply-test.tsx | 190 +++++++++++------- .../Popups/__tests__/PopupReplyV2-test.tsx | 10 + src/popup/PopupContainer.tsx | 3 + src/popup/PopupLayer.tsx | 3 + src/popup/__tests__/PopupContainer-test.tsx | 14 ++ src/popup/__tests__/PopupLayer-test.tsx | 96 ++++++--- yarn.lock | 140 ++++++++++++- 13 files changed, 400 insertions(+), 111 deletions(-) create mode 100644 scripts/jest/jest-setup.ts create mode 100644 src/components/Popups/PopupReplyV2.tsx create mode 100644 src/components/Popups/__tests__/PopupReplyV2-test.tsx diff --git a/jest.config.js b/jest.config.js index 2067926ba..1787d84db 100644 --- a/jest.config.js +++ b/jest.config.js @@ -23,7 +23,7 @@ module.exports = { restoreMocks: true, roots: ['src'], setupFiles: ['jest-canvas-mock', '/scripts/jest/envWindow.js'], - setupFilesAfterEnv: ['/scripts/jest/enzyme-adapter.js'], + setupFilesAfterEnv: ['/scripts/jest/enzyme-adapter.js', '/scripts/jest/jest-setup.ts'], snapshotSerializers: ['enzyme-to-json/serializer'], testEnvironment: 'jest-environment-jsdom-sixteen', transformIgnorePatterns: ['node_modules/(?!(box-ui-elements)/)'], diff --git a/package.json b/package.json index bae544f95..af6c8e84a 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,9 @@ "@formatjs/intl-relativetimeformat": "^4.5.12", "@popperjs/core": "^2.4.0", "@reduxjs/toolkit": "^1.3.5", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", "@types/classnames": "^2.2.10", "@types/draft-js": "^0.10.40", "@types/enzyme": "^3.10.5", diff --git a/scripts/jest/jest-setup.ts b/scripts/jest/jest-setup.ts new file mode 100644 index 000000000..7b0828bfa --- /dev/null +++ b/scripts/jest/jest-setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; diff --git a/src/BoxAnnotations.ts b/src/BoxAnnotations.ts index 1f4b3e259..98ac842e6 100644 --- a/src/BoxAnnotations.ts +++ b/src/BoxAnnotations.ts @@ -19,7 +19,8 @@ type AnnotationsOptions = { }; export type Features = { - [key: string]: boolean; + isThreadedAnnotation?: boolean; + [key: string]: boolean | undefined; }; type PreviewOptions = { diff --git a/src/components/Popups/PopupReply.tsx b/src/components/Popups/PopupReply.tsx index 04cff6f61..26b55aad7 100644 --- a/src/components/Popups/PopupReply.tsx +++ b/src/components/Popups/PopupReply.tsx @@ -5,14 +5,19 @@ import { useIntl } from 'react-intl'; import FocusTrap from 'box-ui-elements/es/components/focus-trap/FocusTrap'; import messages from './messages'; import PopupBase from './PopupBase'; +import PopupReplyV2 from './PopupReplyV2'; import ReplyForm from '../ReplyForm'; import { getScale, getRotation } from '../../store/options'; import { PopupReference } from './Popper'; import './PopupReply.scss'; -export type Props = { +type BaseProps = { className?: string; +}; + +type LegacyProps = BaseProps & { isPending: boolean; + isThreadedAnnotation?: false; onCancel: (text?: string) => void; onChange: (text?: string) => void; onSubmit: (text: string) => void; @@ -20,6 +25,18 @@ export type Props = { value?: string; }; +type ThreadedProps = BaseProps & { + isThreadedAnnotation: true; + isPending?: boolean; + onCancel?: (text?: string) => void; + onChange?: (text?: string) => void; + onSubmit?: (text: string) => void; + reference?: PopupReference; + value?: string; +}; + +export type Props = LegacyProps | ThreadedProps; + const isIE = (): boolean => { const { userAgent } = navigator; return userAgent.indexOf('Trident/') > 0; @@ -77,14 +94,7 @@ const getOptions = (): Partial => { }; }; -export default function PopupReply({ - isPending, - onCancel, - onChange, - onSubmit, - value = '', - ...rest -}: Props): JSX.Element { +export default function PopupReply(props: Props): JSX.Element { const intl = useIntl(); const popupRef = React.useRef(null); const popupOptions = React.useRef>(getOptions()); // Keep the options reference the same between renders @@ -99,6 +109,14 @@ export default function PopupReply({ } }, [popupRef, rotation, scale]); + // TODO: PopupReplyV2 is a placeholder stub. Props (isPending, onCancel, etc.) + // are not yet forwarded and will need to be wired up when V2 is implemented. + if (props.isThreadedAnnotation) { + return ; + } + + const { isPending, onCancel, onChange, onSubmit, value = '', isThreadedAnnotation: _, ...rest } = props as LegacyProps; + return ( ; +} diff --git a/src/components/Popups/__tests__/PopupReply-test.tsx b/src/components/Popups/__tests__/PopupReply-test.tsx index f42dff2c7..94dc679c2 100644 --- a/src/components/Popups/__tests__/PopupReply-test.tsx +++ b/src/components/Popups/__tests__/PopupReply-test.tsx @@ -1,16 +1,43 @@ import React from 'react'; -import * as ReactRedux from 'react-redux'; -import { mount, ReactWrapper } from 'enzyme'; -import PopupBase from '../PopupBase'; +import { render, screen } from '@testing-library/react'; +import { useSelector } from 'react-redux'; import PopupReply, { Props } from '../PopupReply'; -import ReplyForm from '../../ReplyForm'; jest.mock('react-redux', () => ({ useSelector: jest.fn(), })); -jest.mock('../PopupBase'); -jest.mock('../../ReplyForm'); +const mockUseSelector = useSelector as jest.MockedFunction; + +jest.mock('../PopupBase', () => { + const ReactMock = jest.requireActual('react'); + const MockPopupBase = ReactMock.forwardRef( + ({ children, options, ...rest }: Record, ref: React.Ref) => + ReactMock.createElement( + 'div', + { ref, 'data-testid': 'popup-base', 'data-options': JSON.stringify(options), ...rest }, + children, + ), + ); + MockPopupBase.displayName = 'MockPopupBase'; + return MockPopupBase; +}); + +jest.mock('../../ReplyForm', () => { + const ReactMock = jest.requireActual('react'); + return ({ isPending, value, ...rest }: Record) => + ReactMock.createElement('div', { + 'data-testid': 'reply-form', + 'data-pending': String(isPending), + 'data-value': value || '', + ...rest, + }); +}); + +jest.mock('../PopupReplyV2', () => { + const ReactMock = jest.requireActual('react'); + return () => ReactMock.createElement('div', { 'data-testid': 'popup-reply-v2' }); +}); describe('PopupReply', () => { const defaults: Props = { @@ -21,93 +48,116 @@ describe('PopupReply', () => { reference: document.createElement('div'), }; - const getWrapper = (props = {}): ReactWrapper => mount(); - beforeEach(() => { - jest.spyOn(React, 'useEffect').mockImplementation(f => f()); + mockUseSelector.mockReturnValue(0); }); - describe('state changes', () => { - test('should call update on the underlying popper instance when the store changes', () => { - const popupMock = { popper: { update: jest.fn() } }; - const reduxSpy = jest.spyOn(ReactRedux, 'useSelector').mockImplementation(() => false); - const refSpy = jest.spyOn(React, 'useRef').mockImplementation(() => ({ current: popupMock })); - const wrapper = getWrapper(); + afterEach(() => { + jest.clearAllMocks(); + }); - reduxSpy.mockReturnValueOnce(true); - wrapper.setProps({ value: '1' }); + describe('render()', () => { + test('should render the ReplyForm', () => { + render(); + + expect(screen.getByTestId('popup-base')).toBeDefined(); + expect(screen.getByTestId('reply-form')).toBeDefined(); + }); - reduxSpy.mockReturnValueOnce(false); - wrapper.setProps({ value: '2' }); + test('should render PopupReplyV2 when isThreadedAnnotation is true', () => { + render(); - expect(refSpy).toHaveBeenCalled(); - expect(popupMock.popper.update).toHaveBeenCalledTimes(3); + expect(screen.getByTestId('popup-reply-v2')).toBeDefined(); + expect(screen.queryByTestId('popup-base')).toBeNull(); + expect(screen.queryByTestId('reply-form')).toBeNull(); }); - }); - describe('render()', () => { - test('should render the ReplyForm', () => { - const wrapper = getWrapper(); + test('should render existing UI when isThreadedAnnotation is false', () => { + render(); + + expect(screen.queryByTestId('popup-reply-v2')).toBeNull(); + expect(screen.getByTestId('popup-base')).toBeDefined(); + expect(screen.getByTestId('reply-form')).toBeDefined(); + }); - expect(wrapper.exists(PopupBase)).toBe(true); - expect(wrapper.exists(ReplyForm)).toBe(true); + test('should render existing UI when isThreadedAnnotation is undefined', () => { + render(); + + expect(screen.queryByTestId('popup-reply-v2')).toBeNull(); + expect(screen.getByTestId('popup-base')).toBeDefined(); + expect(screen.getByTestId('reply-form')).toBeDefined(); }); - test('should maintain referential integrity of the PopupBase options object across renders', () => { - const wrapper = getWrapper(); + test('should pass value prop to ReplyForm', () => { + render(); + + const replyForm = screen.getByTestId('reply-form'); + expect(replyForm.getAttribute('data-value')).toBe('test value'); + }); - const popupOptions = wrapper.find(PopupBase).prop('options'); + test('should pass isPending prop to ReplyForm', () => { + render(); - expect(wrapper.exists(PopupBase)).toBe(true); - expect(wrapper.find(ReplyForm).prop('value')).toBe(''); + const replyForm = screen.getByTestId('reply-form'); + expect(replyForm.getAttribute('data-pending')).toBe('true'); + }); + }); - wrapper.setProps({ value: '1' }); + describe('useEffect popper update', () => { + test('should call useSelector for rotation and scale values', () => { + mockUseSelector.mockReturnValueOnce(0).mockReturnValueOnce(1); - expect(wrapper.find(PopupBase).prop('options')).toStrictEqual(popupOptions); - expect(wrapper.find(ReplyForm).prop('value')).toBe('1'); + render(); + + expect(mockUseSelector).toHaveBeenCalledTimes(2); + }); + + test('should re-render when rotation/scale changes', () => { + mockUseSelector.mockReturnValue(0); + + const { rerender } = render(); + const callCountAfterFirstRender = mockUseSelector.mock.calls.length; + + mockUseSelector.mockReturnValue(90); + rerender(); + + expect(mockUseSelector.mock.calls.length).toBeGreaterThan(callCountAfterFirstRender); }); }); describe('Popup options', () => { - const eventListenersModifier = { - name: 'eventListeners', - options: { scroll: false }, - }; - const IEUserAgent = 'Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko'; - const otherUserAgent = 'Other'; - - test.each` - userAgent | expectedPlacement - ${IEUserAgent} | ${'top'} - ${otherUserAgent} | ${'bottom'} - `( - 'should set placement option as $expectedPlacement based on userAgent=$userAgent', - ({ userAgent, expectedPlacement }) => { - global.window.navigator.userAgent = userAgent; - - const wrapper = getWrapper(); - - expect(window.navigator.userAgent).toEqual(userAgent); - expect(wrapper.find(PopupBase).prop('options').placement).toBe(expectedPlacement); - }, - ); - - test('should disable scroll event listeners if browser is IE', () => { - global.window.navigator.userAgent = IEUserAgent; - - const wrapper = getWrapper(); - - expect(window.navigator.userAgent).toEqual(IEUserAgent); - expect(wrapper.find(PopupBase).prop('options').modifiers).toContainEqual(eventListenersModifier); + test('should pass options to PopupBase', () => { + render(); + + const popupBase = screen.getByTestId('popup-base'); + const options = JSON.parse(popupBase.getAttribute('data-options') || '{}'); + + expect(options).toHaveProperty('placement'); + expect(options).toHaveProperty('modifiers'); + expect(Array.isArray(options.modifiers)).toBe(true); + }); + + test('should maintain the same options reference between renders', () => { + const { rerender } = render(); + const optionsBefore = screen.getByTestId('popup-base').getAttribute('data-options'); + + rerender(); + const optionsAfter = screen.getByTestId('popup-base').getAttribute('data-options'); + + expect(optionsBefore).toBe(optionsAfter); }); - test('should not disable scroll event listeners for non IE browsers', () => { - global.window.navigator.userAgent = otherUserAgent; + test('should include required modifiers in options', () => { + render(); - const wrapper = getWrapper(); + const popupBase = screen.getByTestId('popup-base'); + const options = JSON.parse(popupBase.getAttribute('data-options') || '{}'); - expect(window.navigator.userAgent).toEqual(otherUserAgent); - expect(wrapper.find(PopupBase).prop('options').modifiers).not.toContainEqual(eventListenersModifier); + const modifierNames = options.modifiers.map((m: { name: string }) => m.name); + expect(modifierNames).toContain('arrow'); + expect(modifierNames).toContain('flip'); + expect(modifierNames).toContain('offset'); + expect(modifierNames).toContain('preventOverflow'); }); }); }); diff --git a/src/components/Popups/__tests__/PopupReplyV2-test.tsx b/src/components/Popups/__tests__/PopupReplyV2-test.tsx new file mode 100644 index 000000000..594d43122 --- /dev/null +++ b/src/components/Popups/__tests__/PopupReplyV2-test.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import PopupReplyV2 from '../PopupReplyV2'; + +describe('PopupReplyV2', () => { + test('should render v2 with correct test id', () => { + render(); + expect(screen.getByTestId('popup-reply-v2')).toBeDefined(); + }); +}); diff --git a/src/popup/PopupContainer.tsx b/src/popup/PopupContainer.tsx index 3322bc827..f708430cb 100644 --- a/src/popup/PopupContainer.tsx +++ b/src/popup/PopupContainer.tsx @@ -15,12 +15,14 @@ import { setMessageAction, getCreatorReferenceId, } from '../store'; +import { isFeatureEnabled } from '../store/options'; import { createDrawingAction } from '../drawing/actions'; import { createHighlightAction } from '../highlight/actions'; import { createRegionAction } from '../region'; export type Props = { isPromoting: boolean; + isThreadedAnnotation?: boolean; message: string; mode: Mode; referenceId: string | null; @@ -31,6 +33,7 @@ export type Props = { export const mapStateToProps = (state: AppState, { location }: { location: number }): Props => { return { isPromoting: getIsPromoting(state), + isThreadedAnnotation: isFeatureEnabled(state, 'isThreadedAnnotation'), message: getCreatorMessage(state), mode: getAnnotationMode(state), referenceId: getCreatorReferenceId(state), diff --git a/src/popup/PopupLayer.tsx b/src/popup/PopupLayer.tsx index f1cc9c8a5..639232b0c 100644 --- a/src/popup/PopupLayer.tsx +++ b/src/popup/PopupLayer.tsx @@ -21,6 +21,7 @@ export type Props = { createHighlight?: (arg: HighlightCreateArg) => void; createRegion?: (arg: RegionCreateArg) => void; isPromoting: boolean; + isThreadedAnnotation?: boolean; location: number; message: string; mode: Mode; @@ -44,6 +45,7 @@ const PopupLayer = (props: Props): JSX.Element | null => { createHighlight = noop, createRegion = noop, isPromoting = false, + isThreadedAnnotation = false, message, mode, referenceId, @@ -92,6 +94,7 @@ const PopupLayer = (props: Props): JSX.Element | null => {
{ createHighlight: expect.any(Function), createRegion: expect.any(Function), isPromoting: false, + isThreadedAnnotation: false, mode: Mode.NONE, referenceId: null, resetCreator: expect.any(Function), @@ -44,5 +45,18 @@ describe('PopupContainer', () => { store: defaults.store, }); }); + + test('should pass isThreadedAnnotation as true when feature is enabled in store', () => { + const store = createStore({ + options: { + features: { + isThreadedAnnotation: true, + }, + }, + }); + const wrapper = getWrapper({ store }); + + expect(wrapper.find(PopupLayer).prop('isThreadedAnnotation')).toBe(true); + }); }); }); diff --git a/src/popup/__tests__/PopupLayer-test.tsx b/src/popup/__tests__/PopupLayer-test.tsx index 14d60754f..0893a32af 100644 --- a/src/popup/__tests__/PopupLayer-test.tsx +++ b/src/popup/__tests__/PopupLayer-test.tsx @@ -1,13 +1,28 @@ import React from 'react'; -import { ReactWrapper, mount } from 'enzyme'; +import { render, screen, act } from '@testing-library/react'; import PopupLayer, { Props } from '../PopupLayer'; -import PopupReply from '../../components/Popups/PopupReply'; import { pathGroups } from '../../drawing/__mocks__/drawingData'; import { CreatorStatus, CreatorItemHighlight, CreatorItemRegion, Mode, CreatorItemDrawing } from '../../store'; import { Rect } from '../../@types'; import { TARGET_TYPE } from '../../constants'; -jest.mock('../../components/Popups/PopupReply'); +let mockOnCancel: ((text?: string) => void) | undefined; +let mockOnChange: ((text?: string) => void) | undefined; +let mockOnSubmit: ((text: string) => void) | undefined; + +jest.mock('../../components/Popups/PopupReply', () => { + const ReactMock = jest.requireActual('react'); + return (props: Record) => { + mockOnCancel = props.onCancel as typeof mockOnCancel; + mockOnChange = props.onChange as typeof mockOnChange; + mockOnSubmit = props.onSubmit as typeof mockOnSubmit; + return ReactMock.createElement('div', { + 'data-testid': 'popup-reply', + 'data-is-pending': String(props.isPending), + 'data-is-threaded': String(props.isThreadedAnnotation), + }); + }; +}); describe('PopupLayer', () => { const getRect = (): Rect => ({ @@ -46,13 +61,22 @@ describe('PopupLayer', () => { status: CreatorStatus.staged, targetType: TARGET_TYPE.PAGE, }); - const getWrapper = (props = {}): ReactWrapper => mount(); - beforeEach(() => { document.body.innerHTML = `
`; + mockOnCancel = undefined; + mockOnChange = undefined; + mockOnSubmit = undefined; }); + const renderLayer = (props = {}): ReturnType => { + let result: ReturnType; + act(() => { + result = render(); + }); + return result!; + }; + describe('render()', () => { test.each` status | showReply @@ -61,9 +85,13 @@ describe('PopupLayer', () => { ${CreatorStatus.rejected} | ${true} ${CreatorStatus.staged} | ${true} `('should render a reply popup ($showReply) if the creator status is $status', ({ status, showReply }) => { - const wrapper = getWrapper({ status }); + renderLayer({ status }); - expect(wrapper.exists(PopupReply)).toBe(showReply); + if (showReply) { + expect(screen.getByTestId('popup-reply')).toBeDefined(); + } else { + expect(screen.queryByTestId('popup-reply')).toBeNull(); + } }); test.each` @@ -72,31 +100,41 @@ describe('PopupLayer', () => { ${CreatorStatus.pending} | ${true} ${CreatorStatus.staged} | ${false} `('should render reply popup with isPending $isPending', ({ status, isPending }) => { - const wrapper = getWrapper({ status }); + renderLayer({ status }); - expect(wrapper.find(PopupReply).prop('isPending')).toBe(isPending); + expect(screen.getByTestId('popup-reply').getAttribute('data-is-pending')).toBe(String(isPending)); }); test('should not render PopupReply if there is a staged type and mode mismatch', () => { // defaults has staged as highlight - const wrapper = getWrapper({ mode: Mode.REGION }); + renderLayer({ mode: Mode.REGION }); - expect(wrapper.exists(PopupReply)).toBe(false); + expect(screen.queryByTestId('popup-reply')).toBeNull(); }); test('should render PopupReply if promoting a highlight and staged exists', () => { - const wrapper = getWrapper({ mode: Mode.NONE, isPromoting: true }); - expect(wrapper.exists(PopupReply)).toBe(true); + renderLayer({ mode: Mode.NONE, isPromoting: true }); + expect(screen.getByTestId('popup-reply')).toBeDefined(); }); test('should not render PopupReply if promoting a highlight but staged does not exist', () => { - const wrapper = getWrapper({ mode: Mode.NONE, isPromoting: true, staged: null }); - expect(wrapper.exists(PopupReply)).toBe(false); + renderLayer({ mode: Mode.NONE, isPromoting: true, staged: null }); + expect(screen.queryByTestId('popup-reply')).toBeNull(); }); test('should render PopupReply if it is a staged drawing', () => { - const wrapper = getWrapper({ mode: Mode.DRAWING, staged: getStagedDrawing() }); - expect(wrapper.exists(PopupReply)).toBe(true); + renderLayer({ mode: Mode.DRAWING, staged: getStagedDrawing() }); + expect(screen.getByTestId('popup-reply')).toBeDefined(); + }); + + test('should pass isThreadedAnnotation to PopupReply', () => { + renderLayer({ isThreadedAnnotation: true }); + expect(screen.getByTestId('popup-reply').getAttribute('data-is-threaded')).toBe('true'); + }); + + test('should default isThreadedAnnotation to false on PopupReply', () => { + renderLayer(); + expect(screen.getByTestId('popup-reply').getAttribute('data-is-threaded')).toBe('false'); }); }); @@ -104,8 +142,8 @@ describe('PopupLayer', () => { describe('handleCancel()', () => { test('should reset creator and reset isPromoting', () => { const resetCreator = jest.fn(); - const wrapper = getWrapper({ resetCreator }); - wrapper.find(PopupReply).prop('onCancel')(); + renderLayer({ resetCreator }); + mockOnCancel!(); expect(resetCreator).toHaveBeenCalled(); }); @@ -114,16 +152,16 @@ describe('PopupLayer', () => { describe('handleChange', () => { test('should set the staged state with the new message', () => { const setMessage = jest.fn(); - const wrapper = getWrapper({ setMessage }); - wrapper.find(PopupReply).prop('onChange')('foo'); + renderLayer({ setMessage }); + mockOnChange!('foo'); expect(setMessage).toHaveBeenCalledWith('foo'); }); test('should set the staged state with empty string', () => { const setMessage = jest.fn(); - const wrapper = getWrapper({ setMessage }); - wrapper.find(PopupReply).prop('onChange')(); + renderLayer({ setMessage }); + mockOnChange!(); expect(setMessage).toHaveBeenCalledWith(''); }); @@ -135,8 +173,8 @@ describe('PopupLayer', () => { const createHighlight = jest.fn(); const createRegion = jest.fn(); const message = 'foo'; - const wrapper = getWrapper({ createHighlight, createRegion, message }); - wrapper.find(PopupReply).prop('onSubmit')(''); + renderLayer({ createHighlight, createRegion, message }); + mockOnSubmit!(''); expect(createDrawing).not.toHaveBeenCalled(); expect(createHighlight).toHaveBeenCalledWith({ @@ -152,14 +190,14 @@ describe('PopupLayer', () => { const createHighlight = jest.fn(); const createRegion = jest.fn(); const message = 'foo'; - const wrapper = getWrapper({ + renderLayer({ createHighlight, createRegion, message, mode: Mode.REGION, staged: getStagedRegion(), }); - wrapper.find(PopupReply).prop('onSubmit')(''); + mockOnSubmit!(''); expect(createDrawing).not.toHaveBeenCalled(); expect(createHighlight).not.toHaveBeenCalled(); @@ -175,7 +213,7 @@ describe('PopupLayer', () => { const createHighlight = jest.fn(); const createRegion = jest.fn(); const message = 'foo'; - const wrapper = getWrapper({ + renderLayer({ createDrawing, createHighlight, createRegion, @@ -184,7 +222,7 @@ describe('PopupLayer', () => { staged: getStagedDrawing(), targetType: 'frame', }); - wrapper.find(PopupReply).prop('onSubmit')(''); + mockOnSubmit!(''); expect(createDrawing).toHaveBeenCalledWith({ ...getStagedDrawing(), diff --git a/yarn.lock b/yarn.lock index f5ad38944..f0b2e6802 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@adobe/css-tools@^4.4.0": + version "4.4.4" + resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.4.tgz#2856c55443d3d461693f32d2b96fb6ea92e1ffa9" + integrity sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg== + "@ampproject/remapping@^2.2.0": version "2.3.0" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" @@ -33,6 +38,15 @@ dependencies: "@babel/highlight" "^7.0.0" +"@babel/code-frame@^7.10.4": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.0.tgz#7cd7a59f15b3cc0dcd803038f7792712a7d0b15c" + integrity sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw== + dependencies: + "@babel/helper-validator-identifier" "^7.28.5" + js-tokens "^4.0.0" + picocolors "^1.1.1" + "@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0": version "7.26.2" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" @@ -382,6 +396,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== +"@babel/helper-validator-identifier@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" + integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== + "@babel/helper-validator-identifier@^7.9.0", "@babel/helper-validator-identifier@^7.9.5": version "7.9.5" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz#90977a8e6fbf6b431a7dc31752eee233bf052d80" @@ -1148,6 +1167,11 @@ dependencies: regenerator-runtime "^0.13.2" +"@babel/runtime@^7.12.5": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.6.tgz#d267a43cb1836dc4d182cce93ae75ba954ef6d2b" + integrity sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA== + "@babel/runtime@^7.8.4": version "7.9.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06" @@ -2060,6 +2084,44 @@ dependencies: type-detect "4.0.8" +"@testing-library/dom@^10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-10.4.1.tgz#d444f8a889e9a46e9a3b4f3b88e0fcb3efb6cf95" + integrity sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^5.0.1" + aria-query "5.3.0" + dom-accessibility-api "^0.5.9" + lz-string "^1.5.0" + picocolors "1.1.1" + pretty-format "^27.0.2" + +"@testing-library/jest-dom@^6.9.1": + version "6.9.1" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz#7613a04e146dd2976d24ddf019730d57a89d56c2" + integrity sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA== + dependencies: + "@adobe/css-tools" "^4.4.0" + aria-query "^5.0.0" + css.escape "^1.5.1" + dom-accessibility-api "^0.6.3" + picocolors "^1.1.1" + redent "^3.0.0" + +"@testing-library/react@^16.3.2": + version "16.3.2" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-16.3.2.tgz#672883b7acb8e775fc0492d9e9d25e06e89786d0" + integrity sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g== + dependencies: + "@babel/runtime" "^7.12.5" + +"@types/aria-query@^5.0.1": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708" + integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw== + "@types/babel__core@^7.1.0": version "7.1.7" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.7.tgz#1dacad8840364a57c98d0dd4855c6dd3752c6b89" @@ -2952,6 +3014,11 @@ ansi-styles@^4.1.0: "@types/color-name" "^1.1.1" color-convert "^2.0.1" +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + any-observable@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.3.0.tgz#af933475e5806a67d0d7df090dd5e8bef65d119b" @@ -2995,6 +3062,13 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +aria-query@5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" + integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== + dependencies: + dequal "^2.0.3" + aria-query@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-3.0.0.tgz#65b3fcc1ca1155a8c9ae64d6eee297f15d5133cc" @@ -3003,6 +3077,11 @@ aria-query@^3.0.0: ast-types-flow "0.0.7" commander "^2.11.0" +aria-query@^5.0.0: + version "5.3.2" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59" + integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw== + arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" @@ -4930,6 +5009,11 @@ css-what@^3.2.1: resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.2.1.tgz#f4a8f12421064621b456755e34a03a2c22df5da1" integrity sha512-WwOrosiQTvyms+Ti5ZC5vGEK0Vod3FTt1ca+payZqvKuGJF+dq7bG63DstxtN0dpm6FxY27a/zS3Wten+gEtGw== +css.escape@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" + integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg== + cssesc@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-2.0.0.tgz#3b13bd1bb1cb36e1bcb5a4dcd27f54c5dcb35703" @@ -5381,6 +5465,11 @@ depd@~1.1.2: resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= +dequal@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + des.js@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc" @@ -5476,6 +5565,16 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-accessibility-api@^0.5.9: + version "0.5.16" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" + integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== + +dom-accessibility-api@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz#993e925cc1d73f2c662e7d75dd5a5445259a8fd8" + integrity sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w== + dom-serializer@0: version "0.1.0" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" @@ -10018,6 +10117,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lz-string@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" + integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== + magic-string@^0.30.0: version "0.30.13" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.13.tgz#92438e3ff4946cf54f18247c981e5c161c46683c" @@ -10317,6 +10421,11 @@ mimic-response@^1.0.0: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== +min-indent@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" + integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== + mini-css-extract-plugin@^0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.0.tgz#47f2cf07aa165ab35733b1fc97d4c46c0564339e" @@ -11462,7 +11571,7 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= -picocolors@^1.0.0, picocolors@^1.1.0: +picocolors@1.1.1, picocolors@^1.0.0, picocolors@^1.1.0, picocolors@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== @@ -12090,6 +12199,15 @@ pretty-format@^24.9.0: ansi-styles "^3.2.0" react-is "^16.8.4" +pretty-format@^27.0.2: + version "27.5.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" + integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== + dependencies: + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^17.0.1" + process-nextick-args@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" @@ -12391,6 +12509,11 @@ react-is@^16.13.1, react-is@^16.8.4: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-is@^17.0.1: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + react-redux@^9.1.2: version "9.1.2" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-9.1.2.tgz#deba38c64c3403e9abd0c3fbeab69ffd9d8a7e4b" @@ -12543,6 +12666,14 @@ redent@^2.0.0: indent-string "^3.0.0" strip-indent "^2.0.0" +redent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" + integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== + dependencies: + indent-string "^4.0.0" + strip-indent "^3.0.0" + redux-thunk@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" @@ -14031,6 +14162,13 @@ strip-indent@^2.0.0: resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68" integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g= +strip-indent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" + integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== + dependencies: + min-indent "^1.0.0" + strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"