Add module item positioning to direct share trays
flag=direct_share closes LA-11 Test plan - Go to the received content page and the copy content menu and ensure you can see the module item pickers - Ensure you can copy content to all of the various positions in the picker - Verify the copied content gets to the right places Change-Id: I68678edb89d51691a3ee4e5713dfceb46693cf89 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/218105 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Tested-by: Jenkins Reviewed-by: Jeremy Stanley <jeremy@instructure.com> QA-Review: Jeremy Stanley <jeremy@instructure.com> Product-Review: Zsofia Goreczky <zsgoreczky@instructure.com>
This commit is contained in:
parent
e1518e042b
commit
b0044898f2
|
@ -33,6 +33,7 @@ CourseImportPanel.propTypes = {
|
|||
export default function CourseImportPanel({contentShare, onClose}) {
|
||||
const [selectedCourse, setSelectedCourse] = useState(null)
|
||||
const [selectedModule, setSelectedModule] = useState(null)
|
||||
const [selectedPosition, setSelectedPosition] = useState(1)
|
||||
const [startImportOperationPromise, setStartImportOperationPromise] = useState(null)
|
||||
|
||||
function startImportOperation() {
|
||||
|
@ -45,7 +46,8 @@ export default function CourseImportPanel({contentShare, onClose}) {
|
|||
settings: {
|
||||
content_export_id: contentShare.content_export.id,
|
||||
insert_into_module_id: selectedModule?.id,
|
||||
insert_into_module_type: contentShare.content_type
|
||||
insert_into_module_type: contentShare.content_type,
|
||||
insert_into_module_position: selectedPosition
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -63,7 +65,9 @@ export default function CourseImportPanel({contentShare, onClose}) {
|
|||
<CourseAndModulePicker
|
||||
selectedCourseId={selectedCourse?.id}
|
||||
setSelectedCourse={setSelectedCourse}
|
||||
selectedModuleId={selectedModule?.id}
|
||||
setSelectedModule={setSelectedModule}
|
||||
setModuleItemPosition={setSelectedPosition}
|
||||
/>
|
||||
<ConfirmActionButtonBar
|
||||
padding="small 0 0 0"
|
||||
|
|
|
@ -20,40 +20,58 @@ import I18n from 'i18n!course_and_module_picker'
|
|||
|
||||
import React from 'react'
|
||||
import {func, string} from 'prop-types'
|
||||
import {View} from '@instructure/ui-view'
|
||||
import {Flex} from '@instructure/ui-flex'
|
||||
|
||||
import useManagedCourseSearchApi from '../effects/useManagedCourseSearchApi'
|
||||
import useModuleCourseSearchApi from '../effects/useModuleCourseSearchApi'
|
||||
import SearchItemSelector from 'jsx/shared/components/SearchItemSelector'
|
||||
import ModulePositionPicker from './ModulePositionPicker'
|
||||
|
||||
CourseAndModulePicker.propTypes = {
|
||||
selectedCourseId: string,
|
||||
setSelectedCourse: func,
|
||||
setSelectedModule: func
|
||||
selectedModuleId: string,
|
||||
setSelectedModule: func,
|
||||
setModuleItemPosition: func
|
||||
}
|
||||
|
||||
export default function CourseAndModulePicker({
|
||||
selectedCourseId,
|
||||
setSelectedCourse,
|
||||
setSelectedModule
|
||||
selectedModuleId,
|
||||
setSelectedModule,
|
||||
setModuleItemPosition
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<SearchItemSelector
|
||||
onItemSelected={setSelectedCourse}
|
||||
renderLabel={I18n.t('Select a Course')}
|
||||
itemSearchFunction={useManagedCourseSearchApi}
|
||||
/>
|
||||
{selectedCourseId && (
|
||||
<View display="block" margin="medium 0 0">
|
||||
<Flex direction="column">
|
||||
<Flex.Item padding="small">
|
||||
<SearchItemSelector
|
||||
onItemSelected={setSelectedModule}
|
||||
renderLabel={I18n.t('Select a Module (optional)')}
|
||||
itemSearchFunction={useModuleCourseSearchApi}
|
||||
contextId={selectedCourseId}
|
||||
onItemSelected={setSelectedCourse}
|
||||
renderLabel={I18n.t('Select a Course')}
|
||||
itemSearchFunction={useManagedCourseSearchApi}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</Flex.Item>
|
||||
<Flex.Item padding="small">
|
||||
{selectedCourseId && (
|
||||
<SearchItemSelector
|
||||
onItemSelected={setSelectedModule}
|
||||
renderLabel={I18n.t('Select a Module (optional)')}
|
||||
itemSearchFunction={useModuleCourseSearchApi}
|
||||
contextId={selectedCourseId}
|
||||
/>
|
||||
)}
|
||||
</Flex.Item>
|
||||
<Flex.Item padding="small">
|
||||
{selectedCourseId && selectedModuleId && (
|
||||
<ModulePositionPicker
|
||||
courseId={selectedCourseId}
|
||||
moduleId={selectedModuleId}
|
||||
setModuleItemPosition={setModuleItemPosition}
|
||||
/>
|
||||
)}
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -40,6 +40,7 @@ export default function DirectShareCoursePanel({sourceCourseId, contentSelection
|
|||
const [selectedCourse, setSelectedCourse] = useState(null)
|
||||
const [startCopyOperationPromise, setStartCopyOperationPromise] = useState(null)
|
||||
const [selectedModule, setSelectedModule] = useState(null)
|
||||
const [selectedPosition, setSelectedPosition] = useState(null)
|
||||
|
||||
function startCopyOperation() {
|
||||
setStartCopyOperationPromise(
|
||||
|
@ -52,7 +53,8 @@ export default function DirectShareCoursePanel({sourceCourseId, contentSelection
|
|||
settings: {
|
||||
source_course_id: sourceCourseId,
|
||||
insert_into_module_id: selectedModule?.id,
|
||||
insert_into_module_type: contentSelection ? Object.keys(contentSelection)[0] : null
|
||||
insert_into_module_type: contentSelection ? Object.keys(contentSelection)[0] : null,
|
||||
insert_into_module_position: selectedPosition
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -70,7 +72,9 @@ export default function DirectShareCoursePanel({sourceCourseId, contentSelection
|
|||
<CourseAndModulePicker
|
||||
selectedCourseId={selectedCourse?.id}
|
||||
setSelectedCourse={setSelectedCourse}
|
||||
selectedModuleId={selectedModule?.id}
|
||||
setSelectedModule={setSelectedModule}
|
||||
setModuleItemPosition={setSelectedPosition}
|
||||
/>
|
||||
<ConfirmActionButtonBar
|
||||
padding="small 0 0 0"
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* Copyright (C) 2019 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import I18n from 'i18n!module_position_picker'
|
||||
import React, {useState, useEffect} from 'react'
|
||||
import {func, string} from 'prop-types'
|
||||
|
||||
import {useCourseModuleItemApi} from 'jsx/shared/effects/useModuleCourseSearchApi'
|
||||
import SelectPosition from 'jsx/shared/helpers/SelectPosition'
|
||||
import {positions} from 'jsx/move_item/positions'
|
||||
|
||||
ModulePositionPicker.propTypes = {
|
||||
courseId: string.isRequired,
|
||||
moduleId: string.isRequired,
|
||||
setModuleItemPosition: func
|
||||
}
|
||||
|
||||
ModulePositionPicker.defaultProps = {
|
||||
setModuleItemPosition: () => {}
|
||||
}
|
||||
|
||||
export default function ModulePositionPicker({courseId, moduleId, setModuleItemPosition}) {
|
||||
const [moduleItems, setModuleItems] = useState([])
|
||||
const [position, setPosition] = useState(null)
|
||||
const [offset, setOffset] = useState(0)
|
||||
const [siblingPosition, setSiblingPosition] = useState(1)
|
||||
const [error, setError] = useState(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setModuleItemPosition(calculateDefaultPosition() + offset)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [courseId, moduleId])
|
||||
|
||||
const params = {contextId: courseId, moduleId}
|
||||
useCourseModuleItemApi({
|
||||
success: setModuleItems,
|
||||
error: setError,
|
||||
loading: setIsLoading,
|
||||
fetchAllPages: true,
|
||||
params
|
||||
})
|
||||
|
||||
function calculateDefaultPosition() {
|
||||
return parseInt(moduleItems[0]?.position || siblingPosition, 10)
|
||||
}
|
||||
|
||||
if (error !== null) throw error
|
||||
|
||||
function handleSetPosition(e) {
|
||||
const pos = e.target.value
|
||||
setPosition(positions[pos])
|
||||
switch (pos) {
|
||||
case 'first':
|
||||
setOffset(0)
|
||||
setSiblingPosition(1)
|
||||
setModuleItemPosition(1)
|
||||
break
|
||||
case 'last':
|
||||
setOffset(0)
|
||||
setSiblingPosition(1)
|
||||
setModuleItemPosition(null)
|
||||
break
|
||||
case 'after':
|
||||
setOffset(1)
|
||||
// + 1 for the offset that won't be set yet by the time we need it
|
||||
setModuleItemPosition(calculateDefaultPosition() + 1)
|
||||
break
|
||||
case 'before':
|
||||
setOffset(0)
|
||||
setModuleItemPosition(calculateDefaultPosition())
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function handleSetSibling(e) {
|
||||
const pos = parseInt(moduleItems[e.target.value]?.position, 10)
|
||||
setSiblingPosition(pos)
|
||||
setModuleItemPosition(pos + offset)
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectPosition
|
||||
items={[]}
|
||||
siblings={
|
||||
isLoading
|
||||
? moduleItems.concat({
|
||||
title: I18n.t('Loading additional items...'),
|
||||
id: '0',
|
||||
groupId: '0'
|
||||
})
|
||||
: moduleItems
|
||||
}
|
||||
selectedPosition={position}
|
||||
selectPosition={handleSetPosition}
|
||||
selectSibling={handleSetSibling}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,169 @@
|
|||
/*
|
||||
* Copyright (C) 2019 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import {render, fireEvent} from '@testing-library/react'
|
||||
import ModulePositionPicker from '../ModulePositionPicker'
|
||||
import {useCourseModuleItemApi} from 'jsx/shared/effects/useModuleCourseSearchApi'
|
||||
|
||||
jest.mock('jsx/shared/effects/useModuleCourseSearchApi')
|
||||
|
||||
describe('ModulePositionPicker', () => {
|
||||
it("shows 'loading additional items' when it's still loading data", () => {
|
||||
useCourseModuleItemApi.mockImplementationOnce(({success, loading}) => {
|
||||
success([{id: 'abc', title: 'abc', position: '1'}, {id: 'cde', title: 'cde', position: '2'}])
|
||||
loading(true)
|
||||
})
|
||||
const {getByText, getByTestId} = render(<ModulePositionPicker courseId="1" moduleId="1" />)
|
||||
fireEvent.change(getByTestId('select-position'), {target: {value: 'before'}})
|
||||
expect(getByText('Loading additional items...')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show the module items unless a relative position is chosen', () => {
|
||||
useCourseModuleItemApi.mockImplementationOnce(({success}) => {
|
||||
success([{id: 'abc', title: 'abc', position: '1'}, {id: 'cde', title: 'cde', position: '2'}])
|
||||
})
|
||||
const {getByText, getByTestId, queryByText} = render(
|
||||
<ModulePositionPicker courseId="1" moduleId="1" />
|
||||
)
|
||||
expect(getByText(/At the Bottom/i)).toBeInTheDocument()
|
||||
fireEvent.change(getByTestId('select-position'), {target: {value: 'last'}})
|
||||
expect(queryByText('abc')).not.toBeInTheDocument()
|
||||
expect(queryByText('cde')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show the module items when a relative position is chosen', () => {
|
||||
useCourseModuleItemApi.mockImplementationOnce(({success}) => {
|
||||
success([{id: 'abc', title: 'abc', position: '1'}, {id: 'cde', title: 'cde', position: '2'}])
|
||||
})
|
||||
const {getByText, getByTestId} = render(<ModulePositionPicker courseId="1" moduleId="1" />)
|
||||
expect(getByText(/At the Top/i)).toBeInTheDocument()
|
||||
fireEvent.change(getByTestId('select-position'), {target: {value: 'after'}})
|
||||
expect(getByText('abc')).toBeInTheDocument()
|
||||
expect(getByText('cde')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call setModuleItemPosition with 1 on load', () => {
|
||||
useCourseModuleItemApi.mockImplementationOnce(({success}) => {
|
||||
success([{id: 'abc', title: 'abc', position: '1'}, {id: 'cde', title: 'cde', position: '2'}])
|
||||
})
|
||||
const positionSetter = jest.fn()
|
||||
render(
|
||||
<ModulePositionPicker courseId="1" moduleId="1" setModuleItemPosition={positionSetter} />
|
||||
)
|
||||
expect(positionSetter).toHaveBeenCalledWith(1)
|
||||
})
|
||||
|
||||
it('should call setModuleItemPosition with 1 when "at the top" is chosen', () => {
|
||||
useCourseModuleItemApi.mockImplementationOnce(({success}) => {
|
||||
success([{id: 'abc', title: 'abc', position: '1'}, {id: 'cde', title: 'cde', position: '2'}])
|
||||
})
|
||||
const positionSetter = jest.fn()
|
||||
const {getByTestId} = render(
|
||||
<ModulePositionPicker courseId="1" moduleId="1" setModuleItemPosition={positionSetter} />
|
||||
)
|
||||
fireEvent.change(getByTestId('select-position'), {target: {value: 'first'}})
|
||||
expect(positionSetter).toHaveBeenCalledTimes(2)
|
||||
expect(positionSetter).toHaveBeenLastCalledWith(1)
|
||||
})
|
||||
|
||||
it('should call setModuleItemPosition with null when "at the bottom" is chosen', () => {
|
||||
useCourseModuleItemApi.mockImplementationOnce(({success}) => {
|
||||
success([{id: 'abc', title: 'abc', position: '1'}, {id: 'cde', title: 'cde', position: '2'}])
|
||||
})
|
||||
const positionSetter = jest.fn()
|
||||
const {getByTestId} = render(
|
||||
<ModulePositionPicker courseId="1" moduleId="1" setModuleItemPosition={positionSetter} />
|
||||
)
|
||||
fireEvent.change(getByTestId('select-position'), {target: {value: 'last'}})
|
||||
expect(positionSetter).toHaveBeenCalledTimes(2)
|
||||
expect(positionSetter).toHaveBeenLastCalledWith(null)
|
||||
})
|
||||
|
||||
it('should call setModuleItemPosition with 1 when "before" is chosen with the default module item', () => {
|
||||
useCourseModuleItemApi.mockImplementationOnce(({success}) => {
|
||||
success([{id: 'abc', title: 'abc', position: '1'}, {id: 'cde', title: 'cde', position: '2'}])
|
||||
})
|
||||
const positionSetter = jest.fn()
|
||||
const {getByTestId} = render(
|
||||
<ModulePositionPicker courseId="1" moduleId="1" setModuleItemPosition={positionSetter} />
|
||||
)
|
||||
fireEvent.change(getByTestId('select-position'), {target: {value: 'before'}})
|
||||
expect(positionSetter).toHaveBeenCalledTimes(2)
|
||||
expect(positionSetter).toHaveBeenLastCalledWith(1)
|
||||
})
|
||||
|
||||
it('should call setModuleItemPosition with 1 + position of first item when "after" is chosen with the default module item', () => {
|
||||
useCourseModuleItemApi.mockImplementationOnce(({success}) => {
|
||||
success([{id: 'abc', title: 'abc', position: '5'}, {id: 'cde', title: 'cde', position: '6'}])
|
||||
})
|
||||
const positionSetter = jest.fn()
|
||||
const {getByTestId} = render(
|
||||
<ModulePositionPicker courseId="1" moduleId="1" setModuleItemPosition={positionSetter} />
|
||||
)
|
||||
fireEvent.change(getByTestId('select-position'), {target: {value: 'after'}})
|
||||
expect(positionSetter).toHaveBeenCalledTimes(2)
|
||||
expect(positionSetter).toHaveBeenLastCalledWith(6)
|
||||
})
|
||||
|
||||
it('should call setModuleItemPosition with the appropriate position when "before" is chosen with a non-default module item', () => {
|
||||
useCourseModuleItemApi.mockImplementationOnce(({success}) => {
|
||||
success([{id: 'abc', title: 'abc', position: '5'}, {id: 'cde', title: 'cde', position: '6'}])
|
||||
})
|
||||
const positionSetter = jest.fn()
|
||||
const {getByTestId} = render(
|
||||
<ModulePositionPicker courseId="1" moduleId="1" setModuleItemPosition={positionSetter} />
|
||||
)
|
||||
fireEvent.change(getByTestId('select-position'), {target: {value: 'before'}})
|
||||
fireEvent.change(getByTestId('select-sibling'), {target: {value: '0'}})
|
||||
expect(positionSetter).toHaveBeenCalledTimes(3)
|
||||
expect(positionSetter).toHaveBeenLastCalledWith(5)
|
||||
})
|
||||
|
||||
it('should call setModuleItemPosition with the appropriate position when "after" is chosen with a non-default module item', () => {
|
||||
useCourseModuleItemApi.mockImplementationOnce(({success}) => {
|
||||
success([{id: 'abc', title: 'abc', position: '5'}, {id: 'cde', title: 'cde', position: '6'}])
|
||||
})
|
||||
const positionSetter = jest.fn()
|
||||
const {getByTestId} = render(
|
||||
<ModulePositionPicker courseId="1" moduleId="1" setModuleItemPosition={positionSetter} />
|
||||
)
|
||||
fireEvent.change(getByTestId('select-position'), {target: {value: 'after'}})
|
||||
fireEvent.change(getByTestId('select-sibling'), {target: {value: '1'}})
|
||||
expect(positionSetter).toHaveBeenCalledTimes(3)
|
||||
expect(positionSetter).toHaveBeenLastCalledWith(7)
|
||||
})
|
||||
|
||||
it('should reset the module items and send a new position to setModuleItemPosition if a new module is chosen', () => {
|
||||
useCourseModuleItemApi.mockImplementationOnce(({success}) => {
|
||||
success([{id: 'abc', title: 'abc', position: '5'}, {id: 'cde', title: 'cde', position: '6'}])
|
||||
})
|
||||
const positionSetter = jest.fn()
|
||||
const {rerender} = render(
|
||||
<ModulePositionPicker courseId="1" moduleId="1" setModuleItemPosition={positionSetter} />
|
||||
)
|
||||
useCourseModuleItemApi.mockImplementationOnce(({success}) => {
|
||||
success([{id: 'fgh', title: 'fgh', position: '7'}, {id: 'ijk', title: 'ijk', position: '8'}])
|
||||
})
|
||||
rerender(
|
||||
<ModulePositionPicker courseId="1" moduleId="2" setModuleItemPosition={positionSetter} />
|
||||
)
|
||||
expect(positionSetter).toHaveBeenCalledTimes(2)
|
||||
expect(positionSetter).toHaveBeenLastCalledWith(7)
|
||||
})
|
||||
})
|
|
@ -26,3 +26,16 @@ export default function useModuleCourseSearchApi(fetchApiOpts) {
|
|||
...fetchApiOpts
|
||||
})
|
||||
}
|
||||
|
||||
export function useCourseModuleItemApi(fetchApiOpts) {
|
||||
const courseId = fetchApiOpts?.params?.contextId
|
||||
const moduleId = fetchApiOpts?.params?.moduleId
|
||||
if (courseId && moduleId) {
|
||||
delete fetchApiOpts.params.contextId
|
||||
delete fetchApiOpts.params.moduleId
|
||||
}
|
||||
useFetchApi({
|
||||
path: `/api/v1/courses/${courseId}/modules/${moduleId}/items`,
|
||||
...fetchApiOpts
|
||||
})
|
||||
}
|
||||
|
|
|
@ -50,6 +50,7 @@ export function RenderSelect({label, onChange, options, className, selectOneDefa
|
|||
<select
|
||||
data-testid={testId}
|
||||
onChange={onChange}
|
||||
className="move-select-form"
|
||||
style={{
|
||||
margin: '0',
|
||||
width: '100%'
|
||||
|
@ -87,7 +88,7 @@ export default function SelectPosition({
|
|||
const positionSelected = !!(selectedPosition && selectedPosition.type === 'relative')
|
||||
|
||||
function renderSelectSibling() {
|
||||
const filteredItems = siblings.filter(item => item.id !== items[0].id)
|
||||
const filteredItems = siblings.filter(item => item.id !== items[0]?.id)
|
||||
return (
|
||||
<RenderSelect
|
||||
label={I18n.t('Item Select')}
|
||||
|
@ -106,7 +107,7 @@ export default function SelectPosition({
|
|||
|
||||
function renderPlaceTitle() {
|
||||
const title =
|
||||
items.length > 1 ? I18n.t('Place') : I18n.t('Place "%{title}"', {title: items[0].title})
|
||||
items.length === 1 ? I18n.t('Place "%{title}"', {title: items[0].title}) : I18n.t('Place')
|
||||
return <Text weight="bold">{title}</Text>
|
||||
}
|
||||
|
||||
|
|
|
@ -90,7 +90,7 @@ describe('SelectPosition', () => {
|
|||
selectPosition={selectPosition}
|
||||
/>
|
||||
)
|
||||
fireEvent.change(getByTestId('select-position', {target: {value: 'After..'}}))
|
||||
fireEvent.change(getByTestId('select-position'), {target: {value: 'after'}})
|
||||
expect(selectPosition).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
|
@ -107,7 +107,7 @@ describe('SelectPosition', () => {
|
|||
selectSibling={selectSibling}
|
||||
/>
|
||||
)
|
||||
fireEvent.change(getByTestId('select-sibling', {target: {value: 'Item 3'}}))
|
||||
fireEvent.change(getByTestId('select-sibling'), {target: {value: 'Item 3'}})
|
||||
expect(selectSibling).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue