diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/HintsEditor/HintsEditor.spec.js b/contentcuration/contentcuration/frontend/channelEdit/components/HintsEditor/HintsEditor.spec.js index 65b228a26f..b73993deda 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/HintsEditor/HintsEditor.spec.js +++ b/contentcuration/contentcuration/frontend/channelEdit/components/HintsEditor/HintsEditor.spec.js @@ -1,318 +1,277 @@ -import { shallowMount, mount } from '@vue/test-utils'; +import { render, screen, within, configure } from '@testing-library/vue'; +import userEvent from '@testing-library/user-event'; import { AssessmentItemToolbarActions } from '../../constants'; - import HintsEditor from './HintsEditor'; -jest.mock('shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue'); - -const clickNewHintBtn = async wrapper => { - await wrapper.findComponent('[data-test=newHintBtn]').find('button').trigger('click'); -}; +configure({ + testIdAttribute: 'data-test', +}); -const clickHint = async (wrapper, hintIdx) => { - await wrapper.findAll('[data-test=hint]').at(hintIdx).trigger('click'); +const MockTipTapEditor = { + name: 'TipTapEditor', + props: { + value: { + type: String, + default: '', + }, + mode: { + type: String, + default: 'view', + }, + }, + template: ` +
+

{{ value }}

+ +
+ `, }; -const clickMoveHintUp = async (wrapper, hintIdx) => { - await wrapper - .findAllComponents(`[data-test="toolbarIcon-${AssessmentItemToolbarActions.MOVE_ITEM_UP}"]`) - .at(hintIdx) - .trigger('click'); +const renderComponent = props => { + return render(HintsEditor, { + routes: [], + stubs: { + TipTapEditor: MockTipTapEditor, + }, + props: { + hints: [], + ...props, + }, + }); }; -const clickMoveHintDown = async (wrapper, hintIdx) => { - await wrapper - .findAllComponents(`[data-test="toolbarIcon-${AssessmentItemToolbarActions.MOVE_ITEM_DOWN}"]`) - .at(hintIdx) - .trigger('click'); +const getHintCards = () => { + return screen.getAllByTestId('hint'); }; -const clickDeleteHint = async (wrapper, hintIdx) => { - await wrapper - .findAllComponents(`[data-test="toolbarIcon-${AssessmentItemToolbarActions.DELETE_ITEM}"]`) - .at(hintIdx) - .trigger('click'); +const clickToolbarAction = async ({ action, hintIdx, user }) => { + const buttons = screen.getAllByTestId(`toolbarIcon-${action}`); + expect(buttons[hintIdx]).toBeDefined(); + await user.click(buttons[hintIdx]); }; describe('HintsEditor', () => { - let wrapper; - it('smoke test', () => { - const wrapper = shallowMount(HintsEditor); + renderComponent(); - expect(wrapper.exists()).toBe(true); + expect(screen.getByRole('button', { name: 'New hint' })).toBeInTheDocument(); }); - it('renders a placeholder when there are no hints', () => { - wrapper = mount(HintsEditor, { - propsData: { - hints: [], - }, + it('shows an empty-state message when a question has no hints', () => { + renderComponent({ + hints: [], }); - expect(wrapper.html()).toContain('Question has no hints'); + expect(screen.getByText('Question has no hints')).toBeInTheDocument(); }); - it('renders all hints in a correct order', () => { - wrapper = mount(HintsEditor, { - propsData: { - hints: [ - { hint: 'First hint', order: 1 }, - { hint: 'Second hint', order: 2 }, - ], - }, + it('shows hints in the same order as the question', () => { + renderComponent({ + hints: [ + { hint: 'First hint', order: 1 }, + { hint: 'Second hint', order: 2 }, + ], }); - // Find all instances of your new RichTextEditor component - const editors = wrapper.findAllComponents({ name: 'RichTextEditor' }); - expect(editors.length).toBe(2); - - // Instead of checking the raw HTML, we check the `value` prop passed to each editor. - expect(editors.at(0).props('value')).toBe('First hint'); - expect(editors.at(1).props('value')).toBe('Second hint'); + const hintCards = getHintCards(); + expect(within(hintCards[0]).getByText('First hint')).toBeInTheDocument(); + expect(within(hintCards[1]).getByText('Second hint')).toBeInTheDocument(); }); - describe('on hint text update', () => { - beforeEach(() => { - wrapper = mount(HintsEditor, { - propsData: { - hints: [ - { hint: 'First hint', order: 1 }, - { hint: 'Second hint', order: 2 }, - ], - openHintIdx: 1, - }, - }); - - const editors = wrapper.findAllComponents({ name: 'RichTextEditor' }); - editors.at(1).vm.$emit('update', 'Updated hint'); - }); - - it('emits update event with a payload containing updated hints', () => { - expect(wrapper.emitted().update).toBeTruthy(); - expect(wrapper.emitted().update.length).toBe(1); - expect(wrapper.emitted().update[0][0]).toEqual([ + it('lets the user update the text of the currently open hint', async () => { + const user = userEvent.setup(); + const { emitted } = renderComponent({ + hints: [ { hint: 'First hint', order: 1 }, - { hint: 'Updated hint', order: 2 }, - ]); + { hint: 'Second hint', order: 2 }, + ], + openHintIdx: 1, }); - }); - describe('on new hint button click', () => { - beforeEach(async () => { - wrapper = mount(HintsEditor, { - propsData: { - hints: [ - { hint: 'First hint', order: 1 }, - { hint: '', order: 2 }, - { hint: 'Third hint', order: 3 }, - ], - }, - }); - - await clickNewHintBtn(wrapper); - }); + await user.click(screen.getByRole('button', { name: 'Update hint text' })); + + expect(emitted().update).toHaveLength(1); + expect(emitted().update[0][0]).toEqual([ + { hint: 'First hint', order: 1 }, + { hint: 'Updated hint', order: 2 }, + ]); + }); - it('emits update event with a payload containing all non-empty hints and one new empty hint', () => { - expect(wrapper.emitted().update).toBeTruthy(); - expect(wrapper.emitted().update.length).toBe(1); - expect(wrapper.emitted().update[0][0]).toEqual([ + it('adds a new hint and removes existing empty hints when the user clicks New hint', async () => { + const user = userEvent.setup(); + const { emitted } = renderComponent({ + hints: [ { hint: 'First hint', order: 1 }, - { hint: 'Third hint', order: 2 }, - { hint: '', order: 3 }, - ]); + { hint: '', order: 2 }, + { hint: 'Third hint', order: 3 }, + ], }); - it('emits open event with a new hint idx', () => { - expect(wrapper.emitted().open).toBeTruthy(); - expect(wrapper.emitted().open.length).toBe(1); - expect(wrapper.emitted().open[0][0]).toBe(2); - }); + await user.click(screen.getByRole('button', { name: 'New hint' })); + + expect(emitted().update).toHaveLength(1); + expect(emitted().update[0][0]).toEqual([ + { hint: 'First hint', order: 1 }, + { hint: 'Third hint', order: 2 }, + { hint: '', order: 3 }, + ]); + expect(emitted().open).toHaveLength(1); + expect(emitted().open[0][0]).toBe(2); }); - describe('on hint click', () => { - beforeEach(async () => { - wrapper = mount(HintsEditor, { - propsData: { - hints: [ - { hint: 'First hint', order: 1 }, - { hint: 'Second hint', order: 2 }, - ], - }, - }); - - await clickHint(wrapper, 1); + it('opens a different hint when the user clicks that hint card', async () => { + const user = userEvent.setup(); + const { emitted } = renderComponent({ + hints: [ + { hint: 'First hint', order: 1 }, + { hint: 'Second hint', order: 2 }, + ], + openHintIdx: 0, }); - it('emits open event with a correct hint idx', () => { - expect(wrapper.emitted().open).toBeTruthy(); - expect(wrapper.emitted().open.length).toBe(1); - expect(wrapper.emitted().open[0][0]).toBe(1); - }); + const hintCards = getHintCards(); + await user.click(hintCards[1]); + + expect(emitted().open).toHaveLength(1); + expect(emitted().open[0][0]).toBe(1); }); - describe('on move hint up click', () => { - beforeEach(() => { - wrapper = mount(HintsEditor, { - propsData: { - hints: [ - { hint: 'First hint', order: 1 }, - { hint: 'Second hint', order: 2 }, - ], - }, - }); + it('moves a hint up and keeps the same hint open after moving', async () => { + const user = userEvent.setup(); + const { emitted } = renderComponent({ + hints: [ + { hint: 'First hint', order: 1 }, + { hint: 'Second hint', order: 2 }, + ], + openHintIdx: 1, }); - it('emits update event with a payload containing updated and properly ordered hints', async () => { - await clickMoveHintUp(wrapper, 1); - - expect(wrapper.emitted().update).toBeTruthy(); - expect(wrapper.emitted().update.length).toBe(1); - expect(wrapper.emitted().update[0][0]).toEqual([ - { hint: 'Second hint', order: 1 }, - { hint: 'First hint', order: 2 }, - ]); + await clickToolbarAction({ + action: AssessmentItemToolbarActions.MOVE_ITEM_UP, + hintIdx: 1, + user, }); - describe('if moved hint was open', () => { - beforeEach(async () => { - await wrapper.setProps({ - openHintIdx: 1, - }); - - await clickMoveHintUp(wrapper, 1); - }); + expect(emitted().update).toHaveLength(1); + expect(emitted().update[0][0]).toEqual([ + { hint: 'Second hint', order: 1 }, + { hint: 'First hint', order: 2 }, + ]); + expect(emitted().open).toHaveLength(1); + expect(emitted().open[0][0]).toBe(0); + }); - it('emits open event with updated hint index', () => { - expect(wrapper.emitted().open).toBeTruthy(); - expect(wrapper.emitted().open.length).toBe(1); - expect(wrapper.emitted().open[0][0]).toBe(0); - }); + it('keeps track of the open hint when the user moves the hint below it upward', async () => { + const user = userEvent.setup(); + const { emitted } = renderComponent({ + hints: [ + { hint: 'First hint', order: 1 }, + { hint: 'Second hint', order: 2 }, + ], + openHintIdx: 0, }); - describe('if a hint above a moved hint was open', () => { - beforeEach(async () => { - await wrapper.setProps({ - openHintIdx: 0, - }); - - await clickMoveHintUp(wrapper, 1); - }); - - it('emits open event with updated, originally open, hint index', () => { - expect(wrapper.emitted().open).toBeTruthy(); - expect(wrapper.emitted().open.length).toBe(1); - expect(wrapper.emitted().open[0][0]).toBe(1); - }); + await clickToolbarAction({ + action: AssessmentItemToolbarActions.MOVE_ITEM_UP, + hintIdx: 1, + user, }); + + expect(emitted().open).toHaveLength(1); + expect(emitted().open[0][0]).toBe(1); }); - describe('on move hint down click', () => { - beforeEach(() => { - wrapper = mount(HintsEditor, { - propsData: { - hints: [ - { hint: 'First hint', order: 1 }, - { hint: 'Second hint', order: 2 }, - ], - }, - }); + it('moves a hint down and keeps the same hint open after moving', async () => { + const user = userEvent.setup(); + const { emitted } = renderComponent({ + hints: [ + { hint: 'First hint', order: 1 }, + { hint: 'Second hint', order: 2 }, + ], + openHintIdx: 0, }); - it('emits update event with a payload containing updated and properly ordered hints', async () => { - await clickMoveHintDown(wrapper, 0); - - expect(wrapper.emitted().update).toBeTruthy(); - expect(wrapper.emitted().update.length).toBe(1); - expect(wrapper.emitted().update[0][0]).toEqual([ - { hint: 'Second hint', order: 1 }, - { hint: 'First hint', order: 2 }, - ]); + await clickToolbarAction({ + action: AssessmentItemToolbarActions.MOVE_ITEM_DOWN, + hintIdx: 0, + user, }); - describe('if moved hint was open', () => { - beforeEach(async () => { - await wrapper.setProps({ - openHintIdx: 0, - }); - - await clickMoveHintDown(wrapper, 0); - }); + expect(emitted().update).toHaveLength(1); + expect(emitted().update[0][0]).toEqual([ + { hint: 'Second hint', order: 1 }, + { hint: 'First hint', order: 2 }, + ]); + expect(emitted().open).toHaveLength(1); + expect(emitted().open[0][0]).toBe(1); + }); - it('emits open event with updated hint index', () => { - expect(wrapper.emitted().open).toBeTruthy(); - expect(wrapper.emitted().open.length).toBe(1); - expect(wrapper.emitted().open[0][0]).toBe(1); - }); + it('keeps track of the open hint when the user moves the hint above it downward', async () => { + const user = userEvent.setup(); + const { emitted } = renderComponent({ + hints: [ + { hint: 'First hint', order: 1 }, + { hint: 'Second hint', order: 2 }, + ], + openHintIdx: 1, }); - describe('if a hint below a moved hint was open', () => { - beforeEach(async () => { - await wrapper.setProps({ - openHintIdx: 1, - }); - - await clickMoveHintDown(wrapper, 0); - }); - - it('emits open event with updated, originally open, hint index', () => { - expect(wrapper.emitted().open).toBeTruthy(); - expect(wrapper.emitted().open.length).toBe(1); - expect(wrapper.emitted().open[0][0]).toBe(0); - }); + await clickToolbarAction({ + action: AssessmentItemToolbarActions.MOVE_ITEM_DOWN, + hintIdx: 0, + user, }); + + expect(emitted().open).toHaveLength(1); + expect(emitted().open[0][0]).toBe(0); }); - describe('on delete hint click', () => { - beforeEach(() => { - wrapper = mount(HintsEditor, { - propsData: { - hints: [ - { hint: 'First hint', order: 1 }, - { hint: 'Second hint', order: 2 }, - ], - }, - }); + it('deletes a hint and closes the editor when that hint was open', async () => { + const user = userEvent.setup(); + const { emitted } = renderComponent({ + hints: [ + { hint: 'First hint', order: 1 }, + { hint: 'Second hint', order: 2 }, + ], + openHintIdx: 0, }); - it('emits update event with a payload containing updated and properly ordered hints', async () => { - await clickDeleteHint(wrapper, 0); - - expect(wrapper.emitted().update).toBeTruthy(); - expect(wrapper.emitted().update.length).toBe(1); - expect(wrapper.emitted().update[0][0]).toEqual([{ hint: 'Second hint', order: 1 }]); + await clickToolbarAction({ + action: AssessmentItemToolbarActions.DELETE_ITEM, + hintIdx: 0, + user, }); - describe('if deleted hint was open', () => { - beforeEach(async () => { - await wrapper.setProps({ - openHintIdx: 0, - }); - - await clickDeleteHint(wrapper, 0); - }); + expect(emitted().update).toHaveLength(1); + expect(emitted().update[0][0]).toEqual([{ hint: 'Second hint', order: 1 }]); + expect(emitted().close).toHaveLength(1); + }); - it('emits close event', () => { - expect(wrapper.emitted().close).toBeTruthy(); - expect(wrapper.emitted().close.length).toBe(1); - }); + it('keeps track of the open hint when the user deletes a hint above it', async () => { + const user = userEvent.setup(); + const { emitted } = renderComponent({ + hints: [ + { hint: 'First hint', order: 1 }, + { hint: 'Second hint', order: 2 }, + ], + openHintIdx: 1, }); - describe('if a hint below a deleted hint was open', () => { - beforeEach(async () => { - await wrapper.setProps({ - openHintIdx: 1, - }); - - await clickDeleteHint(wrapper, 0); - }); - - it('emits open event with updated, originally open, hint index', () => { - expect(wrapper.emitted().open).toBeTruthy(); - expect(wrapper.emitted().open.length).toBe(1); - expect(wrapper.emitted().open[0][0]).toBe(0); - }); + await clickToolbarAction({ + action: AssessmentItemToolbarActions.DELETE_ITEM, + hintIdx: 0, + user, }); + + expect(emitted().open).toHaveLength(1); + expect(emitted().open[0][0]).toBe(0); }); });