Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,92 @@ describe('PreviewMessageController', () => {
})).resolves.toMatchObject({contentElementId: 1, command: {some: 'COMMAND'}});
});

it('selects and highlights a created content element thread in iframe', async () => {
const entry = factories.entry(ScrolledEntry, {}, {
entryTypeSeed: normalizeSeed({
contentElements: [{id: 1, permaId: 10}]
})
});
entry.reviewSession = factories.reviewSession();
const iframeWindow = createIframeWindow();
controller = new PreviewMessageController({entry, iframeWindow});

await postReadyMessageAndWaitForAcknowledgement(iframeWindow);

return expect(new Promise(resolve => {
iframeWindow.addEventListener('message', event => {
if (event.data.type === 'SELECT') {
resolve(event.data);
}
});
entry.reviewSession.trigger('create:thread', {
id: 7, subjectType: 'ContentElement', subjectId: 10
});
})).resolves.toMatchObject({
type: 'SELECT',
payload: {id: 1, type: 'contentElementComments', highlightedThreadId: 7}
});
});

it('selects and highlights a created section thread in iframe', async () => {
const entry = factories.entry(ScrolledEntry, {}, {
entryTypeSeed: normalizeSeed({
sections: [{id: 5, permaId: 50}]
})
});
entry.reviewSession = factories.reviewSession();
const iframeWindow = createIframeWindow();
controller = new PreviewMessageController({entry, iframeWindow});

await postReadyMessageAndWaitForAcknowledgement(iframeWindow);

return expect(new Promise(resolve => {
iframeWindow.addEventListener('message', event => {
if (event.data.type === 'SELECT') {
resolve(event.data);
}
});
entry.reviewSession.trigger('create:thread', {
id: 3, subjectType: 'Section', subjectId: 50
});
})).resolves.toMatchObject({
type: 'SELECT',
payload: {id: 5, type: 'sectionComments', highlightedThreadId: 3}
});
});

it('ignores created threads of deleted subjects', async () => {
const entry = factories.entry(ScrolledEntry, {}, {
entryTypeSeed: normalizeSeed({
contentElements: [{id: 1, permaId: 10}]
})
});
entry.reviewSession = factories.reviewSession();
const iframeWindow = createIframeWindow();
controller = new PreviewMessageController({entry, iframeWindow});

await postReadyMessageAndWaitForAcknowledgement(iframeWindow);

// Expecting the SELECT of the known subject to arrive first pins the
// absence of one for the deleted subject.
return expect(new Promise(resolve => {
iframeWindow.addEventListener('message', event => {
if (event.data.type === 'SELECT') {
resolve(event.data);
}
});
entry.reviewSession.trigger('create:thread', {
id: 7, subjectType: 'ContentElement', subjectId: 999
});
entry.reviewSession.trigger('create:thread', {
id: 8, subjectType: 'ContentElement', subjectId: 10
});
})).resolves.toMatchObject({
type: 'SELECT',
payload: {id: 1, type: 'contentElementComments', highlightedThreadId: 8}
});
});

it('sends SELECT message to iframe on resetSelection event on model', async () => {
const entry = factories.entry(ScrolledEntry, {}, {
entryTypeSeed: normalizeSeed({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {NewThreadView} from 'editor/views/NewThreadView';
import {factories, useFakeTranslations, renderBackboneView} from 'pageflow/testHelpers';
import {useEditorGlobals} from 'support';
import {fireEvent} from '@testing-library/dom';
import userEvent from '@testing-library/user-event';

describe('NewThreadView', () => {
const {createEntry} = useEditorGlobals();
Expand Down Expand Up @@ -59,6 +60,42 @@ describe('NewThreadView', () => {
expect(getByText('Comments')).toBeInTheDocument();
});

// Submitting cannot reach the session here: jsdom leaves MessageEvent
// source unset for same-window posts, so the handler drops the create
// message. NewThreadForm-spec covers the form disabling itself.
it('stores the entered text as a draft when the view is closed', async () => {
const user = userEvent.setup();
const entry = createEntry({});
entry.reviewSession = factories.reviewSession();
const view = buildView(entry);

const {getByPlaceholderText} = renderBackboneView(view);

await user.type(getByPlaceholderText('Add a comment...'), 'Half a thought');
view.close();

expect(entry.reviewSession.drafts).toEqual({
'ContentElement:10': {
subjectType: 'ContentElement',
subjectId: 10,
body: 'Half a thought',
pending: false
}
});
});

it('restores the draft stored for the subject', () => {
const entry = createEntry({});
entry.reviewSession = factories.reviewSession();
entry.reviewSession.setDraft({
subjectType: 'ContentElement', subjectId: 10, body: 'Half a thought'
});

const {getByPlaceholderText} = renderBackboneView(buildView(entry));

expect(getByPlaceholderText('Add a comment...')).toHaveValue('Half a thought');
});

it('navigates to the comments selection tab when the back link is clicked', () => {
const entry = createEntry({});
entry.reviewSession = factories.reviewSession();
Expand Down
188 changes: 188 additions & 0 deletions entry_types/scrolled/package/spec/review/NewThreadForm-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import React from 'react';
import '@testing-library/jest-dom/extend-expect';
import userEvent from '@testing-library/user-event';
import {act, waitFor} from '@testing-library/react';
import {useFakeTranslations} from 'pageflow/testHelpers';

import {NewThreadForm} from 'review/NewThreadForm';
import {postReviewStateDraftsChangeMessage} from 'review/postMessage';
import {renderWithReviewState} from 'support/renderWithReviewState';

const seed = {
sections: [{id: 1, permaId: 1}],
contentElements: [{id: 1, permaId: 10, sectionId: 1, typeName: 'textBlock'}]
};

function draft({body, pending = false}) {
return {
'ContentElement:10': {
subjectType: 'ContentElement', subjectId: 10, body, pending
}
};
}

function renderForm(options = {}) {
return renderWithReviewState(
<NewThreadForm subjectType="ContentElement" subjectId={10} />,
{seed, ...options}
);
}

function postDraftsChange(drafts) {
act(() => postReviewStateDraftsChangeMessage(window, drafts));
}

describe('NewThreadForm', () => {
// A spy left behind by a failing spec would swallow the simulated
// review state messages of the ones after it.
afterEach(() => jest.restoreAllMocks());

useFakeTranslations({
'pageflow_scrolled.review.add_comment_placeholder': 'Add a comment...',
'pageflow_scrolled.review.send': 'Send',
'pageflow_scrolled.review.enter_for_new_line': 'Enter for new line'
});

describe('draft restoring', () => {
it('starts out with the stored draft of the subject', () => {
const {getByPlaceholderText} = renderForm({
drafts: draft({body: 'Half a thought'})
});

expect(getByPlaceholderText('Add a comment...')).toHaveValue('Half a thought');
});

it('starts out empty without a draft', () => {
const {getByPlaceholderText} = renderForm();

expect(getByPlaceholderText('Add a comment...')).toHaveValue('');
});
});

describe('draft storing', () => {
it('stores the text when the form goes away', async () => {
const user = userEvent.setup();
const setDraft = jest.fn();

const {getByPlaceholderText, unmount} = renderForm({setDraft});

await user.type(getByPlaceholderText('Add a comment...'), 'Half a thought');
unmount();

expect(setDraft).toHaveBeenCalledWith({
subjectType: 'ContentElement',
subjectId: 10,
body: 'Half a thought'
});
});

// Blank bodies make the session drop the draft.
it('stores a blank text once the input has been cleared', async () => {
const user = userEvent.setup();
const setDraft = jest.fn();

const {getByPlaceholderText, unmount} = renderForm({
drafts: draft({body: 'Never mind'}), setDraft
});

await user.clear(getByPlaceholderText('Add a comment...'));
unmount();

expect(setDraft).toHaveBeenLastCalledWith(
expect.objectContaining({body: ''})
);
});

// The form does not render again between the draft being dropped and
// the list taking it off screen, so the pending draft is the last one
// it saw.
it('does not store the text while the session is creating the thread', () => {
const setDraft = jest.fn();

const {unmount} = renderForm({
drafts: draft({body: 'Looks good', pending: true}),
setDraft
});

unmount();

expect(setDraft).not.toHaveBeenCalled();
});

it('does not store the text after submitting', async () => {
const user = userEvent.setup();
const setDraft = jest.fn();

const {getByPlaceholderText, getByRole, unmount} = renderForm({setDraft});

await user.type(getByPlaceholderText('Add a comment...'), 'Looks good');
await user.click(getByRole('button', {name: 'Send'}));

unmount();

expect(setDraft).not.toHaveBeenCalled();
});

it('stores the text again when creating the thread failed', async () => {
const user = userEvent.setup();
const setDraft = jest.fn();

const {getByPlaceholderText, getByRole, unmount} = renderForm({setDraft});

await user.type(getByPlaceholderText('Add a comment...'), 'Looks good');
await user.click(getByRole('button', {name: 'Send'}));

postDraftsChange(draft({body: 'Looks good', pending: false}));
await waitFor(() =>
expect(getByPlaceholderText('Add a comment...')).not.toBeDisabled()
);

await user.type(getByPlaceholderText('Add a comment...'), ' after all');
unmount();

expect(setDraft).toHaveBeenCalledWith(
expect.objectContaining({body: 'Looks good after all'})
);
});
});

describe('while the thread is being created', () => {
it('disables the input and shows a spinner', () => {
const {getByPlaceholderText, getByRole, container} = renderForm({
drafts: draft({body: 'Looks good', pending: true})
});

expect(getByPlaceholderText('Add a comment...')).toBeDisabled();
expect(getByRole('button', {name: 'Send'})).toBeDisabled();
expect(container.querySelector('[data-file-name="SvgSpinner"]')).not.toBeNull();
});

it('ignores further submits', async () => {
const user = userEvent.setup();
const postMessage = jest.spyOn(window.top, 'postMessage').mockImplementation(() => {});

const {getByRole} = renderForm({
drafts: draft({body: 'Looks good', pending: true})
});

await user.click(getByRole('button', {name: 'Send'}));

expect(postMessage).not.toHaveBeenCalledWith(
expect.objectContaining({type: 'CREATE_COMMENT_THREAD'}),
expect.anything()
);

postMessage.mockRestore();
});

it('is editable again once the draft is no longer pending', () => {
const {getByPlaceholderText, getByRole} = renderForm({
drafts: draft({body: 'Looks good', pending: false})
});

expect(getByPlaceholderText('Add a comment...')).not.toBeDisabled();
expect(getByRole('button', {name: 'Send'})).toBeEnabled();
expect(getByPlaceholderText('Add a comment...')).toHaveValue('Looks good');
});
});
});
Loading
Loading