Commit af7cd288 authored by Lyza Danger Gardner's avatar Lyza Danger Gardner Committed by Lyza Gardner

Refactor reply-toggling logic for top-level threads

Refactor the way headless threads are handled regarding
visibility and reply toggling. Add an `AnnotationMissing`
component; harmonize props between `Annotation` and
`AnnotationMissing`.

Add a new `AnnotationReplyToggle` component.
parent 46460709
...@@ -3,7 +3,7 @@ import { createElement } from 'preact'; ...@@ -3,7 +3,7 @@ import { createElement } from 'preact';
import propTypes from 'prop-types'; import propTypes from 'prop-types';
import { useStoreProxy } from '../store/use-store'; import { useStoreProxy } from '../store/use-store';
import { isReply, quote } from '../helpers/annotation-metadata'; import { quote } from '../helpers/annotation-metadata';
import { withServices } from '../service-context'; import { withServices } from '../service-context';
import AnnotationActionBar from './AnnotationActionBar'; import AnnotationActionBar from './AnnotationActionBar';
...@@ -11,7 +11,7 @@ import AnnotationBody from './AnnotationBody'; ...@@ -11,7 +11,7 @@ import AnnotationBody from './AnnotationBody';
import AnnotationEditor from './AnnotationEditor'; import AnnotationEditor from './AnnotationEditor';
import AnnotationHeader from './AnnotationHeader'; import AnnotationHeader from './AnnotationHeader';
import AnnotationQuote from './AnnotationQuote'; import AnnotationQuote from './AnnotationQuote';
import Button from './Button'; import AnnotationReplyToggle from './AnnotationReplyToggle';
/** /**
* @typedef {import("../../types/api").Annotation} Annotation * @typedef {import("../../types/api").Annotation} Annotation
...@@ -21,7 +21,10 @@ import Button from './Button'; ...@@ -21,7 +21,10 @@ import Button from './Button';
/** /**
* @typedef AnnotationProps * @typedef AnnotationProps
* @prop {Annotation} annotation * @prop {Annotation} annotation
* @prop {number} replyCount - Number of replies to this annotation (thread) * @prop {boolean} hasAppliedFilter - Is any filter applied currently?
* @prop {boolean} isReply
* @prop {VoidFunction} onToggleReplies - Callback to expand/collapse reply threads
* @prop {number} replyCount - Number of replies to this annotation's thread
* @prop {boolean} showDocumentInfo - Should extended document info be rendered (e.g. in non-sidebar contexts)? * @prop {boolean} showDocumentInfo - Should extended document info be rendered (e.g. in non-sidebar contexts)?
* @prop {boolean} threadIsCollapsed - Is the thread to which this annotation belongs currently collapsed? * @prop {boolean} threadIsCollapsed - Is the thread to which this annotation belongs currently collapsed?
* @prop {Object} annotationsService - Injected service * @prop {Object} annotationsService - Injected service
...@@ -34,10 +37,13 @@ import Button from './Button'; ...@@ -34,10 +37,13 @@ import Button from './Button';
*/ */
function Annotation({ function Annotation({
annotation, annotation,
annotationsService, hasAppliedFilter,
isReply,
onToggleReplies,
replyCount, replyCount,
showDocumentInfo, showDocumentInfo,
threadIsCollapsed, threadIsCollapsed,
annotationsService,
}) { }) {
const store = useStoreProxy(); const store = useStoreProxy();
const isFocused = store.isAnnotationFocused(annotation.$tag); const isFocused = store.isAnnotationFocused(annotation.$tag);
...@@ -47,32 +53,21 @@ function Annotation({ ...@@ -47,32 +53,21 @@ function Annotation({
const userid = store.profile().userid; const userid = store.profile().userid;
const isSaving = store.isSavingAnnotation(annotation); const isSaving = store.isSavingAnnotation(annotation);
const isCollapsedReply = isReply(annotation) && threadIsCollapsed; const isCollapsedReply = isReply && threadIsCollapsed;
const hasQuote = !!quote(annotation); const hasQuote = !!quote(annotation);
const isEditing = !!draft && !isSaving; const isEditing = !!draft && !isSaving;
const toggleAction = threadIsCollapsed ? 'Show replies' : 'Hide replies'; const showActions = !isSaving && !isEditing;
const toggleText = `${toggleAction} (${replyCount})`; const showReplyToggle = !isReply && !hasAppliedFilter && replyCount > 0;
const shouldShowActions = !isSaving && !isEditing;
const shouldShowReplyToggle = replyCount > 0 && !isReply(annotation);
const onReply = () => annotationsService.reply(annotation, userid); const onReply = () => annotationsService.reply(annotation, userid);
const onToggleReplies = () =>
// nb. We assume the annotation has an ID here because it is not possible
// to create replies until the annotation has been saved.
store.setExpanded(
/** @type {string} */ (annotation.id),
!!threadIsCollapsed
);
return ( return (
<article <article
className={classnames('Annotation', { className={classnames('Annotation', {
'Annotation--reply': isReply(annotation), 'Annotation--reply': isReply,
'is-collapsed': threadIsCollapsed, 'is-collapsed': threadIsCollapsed,
'is-focused': isFocused, 'is-focused': isFocused,
})} })}
...@@ -98,15 +93,15 @@ function Annotation({ ...@@ -98,15 +93,15 @@ function Annotation({
{!isCollapsedReply && ( {!isCollapsedReply && (
<footer className="Annotation__footer"> <footer className="Annotation__footer">
<div className="Annotation__controls u-layout-row"> <div className="Annotation__controls u-layout-row">
{shouldShowReplyToggle && ( {showReplyToggle && (
<Button <AnnotationReplyToggle
className="Annotation__reply-toggle" onToggleReplies={onToggleReplies}
onClick={onToggleReplies} replyCount={replyCount}
buttonText={toggleText} threadIsCollapsed={threadIsCollapsed}
/> />
)} )}
{isSaving && <div className="Annotation__actions">Saving...</div>} {isSaving && <div className="Annotation__actions">Saving...</div>}
{shouldShowActions && ( {showActions && (
<div className="Annotation__actions"> <div className="Annotation__actions">
<AnnotationActionBar <AnnotationActionBar
annotation={annotation} annotation={annotation}
...@@ -123,6 +118,9 @@ function Annotation({ ...@@ -123,6 +118,9 @@ function Annotation({
Annotation.propTypes = { Annotation.propTypes = {
annotation: propTypes.object.isRequired, annotation: propTypes.object.isRequired,
hasAppliedFilter: propTypes.bool.isRequired,
isReply: propTypes.bool,
onToggleReplies: propTypes.func,
replyCount: propTypes.number.isRequired, replyCount: propTypes.number.isRequired,
showDocumentInfo: propTypes.bool.isRequired, showDocumentInfo: propTypes.bool.isRequired,
threadIsCollapsed: propTypes.bool.isRequired, threadIsCollapsed: propTypes.bool.isRequired,
......
import classnames from 'classnames';
import { createElement } from 'preact';
import propTypes from 'prop-types';
import AnnotationReplyToggle from './AnnotationReplyToggle';
/** @typedef {import('./Annotation').AnnotationProps} AnnotationProps */
/**
* @typedef {Omit<AnnotationProps, 'annotation'|'showDocumentInfo'|'annotationsService'>} AnnotationMissingProps
*/
/**
* Renders in place of an annotation if a thread's annotation is missing.
*
* @param {AnnotationMissingProps} props
*/
function AnnotationMissing({
hasAppliedFilter,
isReply,
onToggleReplies,
replyCount,
threadIsCollapsed,
}) {
const showReplyToggle = !isReply && !hasAppliedFilter && replyCount > 0;
const isCollapsedReply = isReply && threadIsCollapsed;
return (
<article
className={classnames('Annotation', 'Annotation--missing', {
'is-collapsed': threadIsCollapsed,
})}
>
{!isCollapsedReply && (
<div>
<em>Message not available.</em>
</div>
)}
{!isCollapsedReply && (
<footer className="Annotation__footer">
<div className="Annotation__controls u-layout-row">
{showReplyToggle && (
<AnnotationReplyToggle
onToggleReplies={onToggleReplies}
replyCount={replyCount}
threadIsCollapsed={threadIsCollapsed}
/>
)}
</div>
</footer>
)}
</article>
);
}
AnnotationMissing.propTypes = {
hasAppliedFilter: propTypes.bool,
isReply: propTypes.bool.isRequired,
onToggleReplies: propTypes.func,
replyCount: propTypes.number.isRequired,
threadIsCollapsed: propTypes.bool,
};
export default AnnotationMissing;
import { createElement } from 'preact';
import propTypes from 'prop-types';
import Button from './Button';
/**
* @typedef AnnotationReplyToggleProps
* @prop {() => any} onToggleReplies
* @prop {number} replyCount
* @prop {boolean} threadIsCollapsed
*/
/**
* Render a thread-card control to toggle (expand or collapse) all of this
* thread's children.
*
* @param {AnnotationReplyToggleProps} props
*/
function AnnotationReplyToggle({
onToggleReplies,
replyCount,
threadIsCollapsed,
}) {
const toggleAction = threadIsCollapsed ? 'Show replies' : 'Hide replies';
const toggleText = `${toggleAction} (${replyCount})`;
return (
<Button
className="Annotation__reply-toggle"
onClick={onToggleReplies}
buttonText={toggleText}
/>
);
}
AnnotationReplyToggle.propTypes = {
onToggleReplies: propTypes.func,
replyCount: propTypes.number,
threadIsCollapsed: propTypes.bool.isRequired,
};
export default AnnotationReplyToggle;
...@@ -28,7 +28,7 @@ function NotebookResultCount() { ...@@ -28,7 +28,7 @@ function NotebookResultCount() {
const hasResults = rootThread.children.length > 0; const hasResults = rootThread.children.length > 0;
const hasForcedVisible = forcedVisibleCount > 0; const hasForcedVisible = hasAppliedFilter && forcedVisibleCount > 0;
const matchCount = visibleCount - forcedVisibleCount; const matchCount = visibleCount - forcedVisibleCount;
const threadCount = rootThread.children.length; const threadCount = rootThread.children.length;
......
import classnames from 'classnames'; import classnames from 'classnames';
import { createElement, Fragment } from 'preact'; import { createElement, Fragment } from 'preact';
import { useMemo } from 'preact/hooks'; import { useCallback, useMemo } from 'preact/hooks';
import propTypes from 'prop-types'; import propTypes from 'prop-types';
import { useStoreProxy } from '../store/use-store'; import { useStoreProxy } from '../store/use-store';
...@@ -8,6 +8,7 @@ import { withServices } from '../service-context'; ...@@ -8,6 +8,7 @@ import { withServices } from '../service-context';
import { countHidden, countVisible } from '../helpers/thread'; import { countHidden, countVisible } from '../helpers/thread';
import Annotation from './Annotation'; import Annotation from './Annotation';
import AnnotationMissing from './AnnotationMissing';
import Button from './Button'; import Button from './Button';
import ModerationBanner from './ModerationBanner'; import ModerationBanner from './ModerationBanner';
...@@ -30,6 +31,7 @@ import ModerationBanner from './ModerationBanner'; ...@@ -30,6 +31,7 @@ import ModerationBanner from './ModerationBanner';
function Thread({ showDocumentInfo = false, thread, threadsService }) { function Thread({ showDocumentInfo = false, thread, threadsService }) {
// Only render this thread's annotation if it exists and the thread is `visible` // Only render this thread's annotation if it exists and the thread is `visible`
const showAnnotation = thread.annotation && thread.visible; const showAnnotation = thread.annotation && thread.visible;
const showMissingAnnotation = thread.visible && !thread.annotation;
// Render this thread's replies only if the thread is expanded // Render this thread's replies only if the thread is expanded
const showChildren = !thread.collapsed; const showChildren = !thread.collapsed;
...@@ -51,32 +53,54 @@ function Thread({ showDocumentInfo = false, thread, threadsService }) { ...@@ -51,32 +53,54 @@ function Thread({ showDocumentInfo = false, thread, threadsService }) {
); );
const store = useStoreProxy(); const store = useStoreProxy();
const onToggleReplies = () => const hasAppliedFilter = store.hasAppliedFilter();
store.setExpanded(thread.id, !!thread.collapsed); const onToggleReplies = useCallback(
() => store.setExpanded(thread.id, !!thread.collapsed),
[store, thread.id, thread.collapsed]
);
// Memoize annotation content to avoid re-rendering an annotation when content // Memoize annotation content to avoid re-rendering an annotation when content
// in other annotations/threads change. // in other annotations/threads change.
const annotationContent = useMemo( const annotationContent = useMemo(() => {
() => if (showAnnotation) {
showAnnotation && ( return (
<Fragment> <Fragment>
<ModerationBanner annotation={thread.annotation} /> <ModerationBanner annotation={thread.annotation} />
<Annotation <Annotation
annotation={thread.annotation} annotation={thread.annotation}
hasAppliedFilter={hasAppliedFilter}
isReply={!!thread.parent}
onToggleReplies={onToggleReplies}
replyCount={thread.replyCount} replyCount={thread.replyCount}
showDocumentInfo={showDocumentInfo} showDocumentInfo={showDocumentInfo}
threadIsCollapsed={thread.collapsed} threadIsCollapsed={thread.collapsed}
/> />
</Fragment> </Fragment>
), );
[ } else if (showMissingAnnotation) {
showAnnotation, return (
thread.annotation, <AnnotationMissing
thread.replyCount, hasAppliedFilter={hasAppliedFilter}
showDocumentInfo, isReply={!!thread.parent}
thread.collapsed, onToggleReplies={onToggleReplies}
] replyCount={thread.replyCount}
); threadIsCollapsed={thread.collapsed}
/>
);
} else {
return null;
}
}, [
hasAppliedFilter,
onToggleReplies,
showAnnotation,
showMissingAnnotation,
showDocumentInfo,
thread.annotation,
thread.parent,
thread.replyCount,
thread.collapsed,
]);
return ( return (
<section <section
...@@ -99,12 +123,6 @@ function Thread({ showDocumentInfo = false, thread, threadsService }) { ...@@ -99,12 +123,6 @@ function Thread({ showDocumentInfo = false, thread, threadsService }) {
<div className="Thread__content"> <div className="Thread__content">
{annotationContent} {annotationContent}
{!thread.annotation && (
<div className="Thread__unavailable-message">
<em>Message not available.</em>
</div>
)}
{showHiddenToggle && ( {showHiddenToggle && (
<Button <Button
buttonText={`Show ${countHidden(thread)} more in conversation`} buttonText={`Show ${countHidden(thread)} more in conversation`}
......
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import { createElement } from 'preact'; import { createElement } from 'preact';
import { act } from 'preact/test-utils';
import * as fixtures from '../../test/annotation-fixtures'; import * as fixtures from '../../test/annotation-fixtures';
...@@ -11,6 +10,8 @@ import Annotation from '../Annotation'; ...@@ -11,6 +10,8 @@ import Annotation from '../Annotation';
import { $imports } from '../Annotation'; import { $imports } from '../Annotation';
describe('Annotation', () => { describe('Annotation', () => {
let fakeOnToggleReplies;
// Dependency Mocks // Dependency Mocks
let fakeMetadata; let fakeMetadata;
...@@ -32,6 +33,9 @@ describe('Annotation', () => { ...@@ -32,6 +33,9 @@ describe('Annotation', () => {
<Annotation <Annotation
annotation={fixtures.defaultAnnotation()} annotation={fixtures.defaultAnnotation()}
annotationsService={fakeAnnotationsService} annotationsService={fakeAnnotationsService}
hasAppliedFilter={false}
isReply={false}
onToggleReplies={fakeOnToggleReplies}
replyCount={0} replyCount={0}
showDocumentInfo={false} showDocumentInfo={false}
threadIsCollapsed={true} threadIsCollapsed={true}
...@@ -41,13 +45,14 @@ describe('Annotation', () => { ...@@ -41,13 +45,14 @@ describe('Annotation', () => {
}; };
beforeEach(() => { beforeEach(() => {
fakeOnToggleReplies = sinon.stub();
fakeAnnotationsService = { fakeAnnotationsService = {
reply: sinon.stub(), reply: sinon.stub(),
save: sinon.stub().resolves(), save: sinon.stub().resolves(),
}; };
fakeMetadata = { fakeMetadata = {
isReply: sinon.stub(),
quote: sinon.stub(), quote: sinon.stub(),
}; };
...@@ -72,9 +77,10 @@ describe('Annotation', () => { ...@@ -72,9 +77,10 @@ describe('Annotation', () => {
describe('annotation classnames', () => { describe('annotation classnames', () => {
it('should assign a reply class if the annotation is a reply', () => { it('should assign a reply class if the annotation is a reply', () => {
fakeMetadata.isReply.returns(true); const wrapper = createComponent({
isReply: true,
const wrapper = createComponent({ threadIsCollapsed: false }); threadIsCollapsed: false,
});
const annot = wrapper.find('.Annotation'); const annot = wrapper.find('.Annotation');
assert.isTrue(annot.hasClass('Annotation--reply')); assert.isTrue(annot.hasClass('Annotation--reply'));
...@@ -132,61 +138,50 @@ describe('Annotation', () => { ...@@ -132,61 +138,50 @@ describe('Annotation', () => {
assert.include(wrapper.find('.Annotation__actions').text(), 'Saving...'); assert.include(wrapper.find('.Annotation__actions').text(), 'Saving...');
}); });
describe('reply thread toggle button', () => { describe('reply thread toggle', () => {
const findRepliesButton = wrapper =>
wrapper.find('Button').filter('.Annotation__reply-toggle');
it('should render a toggle button if the annotation has replies', () => { it('should render a toggle button if the annotation has replies', () => {
fakeMetadata.isReply.returns(false);
const wrapper = createComponent({ const wrapper = createComponent({
replyCount: 5, replyCount: 5,
threadIsCollapsed: true, threadIsCollapsed: true,
}); });
assert.isTrue(findRepliesButton(wrapper).exists()); const toggle = wrapper.find('AnnotationReplyToggle');
assert.equal(
findRepliesButton(wrapper).props().buttonText, assert.isTrue(toggle.exists());
'Show replies (5)' assert.equal(toggle.props().onToggleReplies, fakeOnToggleReplies);
); assert.equal(toggle.props().replyCount, 5);
assert.equal(toggle.props().threadIsCollapsed, true);
}); });
it('should not render a toggle button if the annotation has no replies', () => { it('should not render a reply toggle if the annotation has no replies', () => {
fakeMetadata.isReply.returns(false);
const wrapper = createComponent({ const wrapper = createComponent({
isReply: false,
replyCount: 0, replyCount: 0,
threadIsCollapsed: true, threadIsCollapsed: true,
}); });
assert.isFalse(findRepliesButton(wrapper).exists()); assert.isFalse(wrapper.find('AnnotationReplyToggle').exists());
}); });
it('should not render a toggle button if the annotation itself is a reply', () => { it('should not render a reply toggle if there are applied filters', () => {
fakeMetadata.isReply.returns(true);
const wrapper = createComponent({ const wrapper = createComponent({
hasAppliedFilter: true,
isReply: false,
replyCount: 5, replyCount: 5,
threadIsCollapsed: true, threadIsCollapsed: true,
}); });
assert.isFalse(findRepliesButton(wrapper).exists()); assert.isFalse(wrapper.find('AnnotationReplyToggle').exists());
}); });
it('should toggle the collapsed state of the thread on click', () => { it('should not render a reply toggle if the annotation itself is a reply', () => {
fakeMetadata.isReply.returns(false);
const wrapper = createComponent({ const wrapper = createComponent({
isReply: true,
replyCount: 5, replyCount: 5,
threadIsCollapsed: true, threadIsCollapsed: true,
}); });
act(() => { assert.isFalse(wrapper.find('AnnotationReplyToggle').exists());
findRepliesButton(wrapper).props().onClick();
});
wrapper.setProps({ threadIsCollapsed: false });
assert.calledOnce(fakeStore.setExpanded);
assert.equal(
findRepliesButton(wrapper).props().buttonText,
'Hide replies (5)'
);
}); });
}); });
...@@ -224,19 +219,21 @@ describe('Annotation', () => { ...@@ -224,19 +219,21 @@ describe('Annotation', () => {
context('annotation thread is collapsed', () => { context('annotation thread is collapsed', () => {
context('collapsed reply', () => { context('collapsed reply', () => {
beforeEach(() => {
fakeMetadata.isReply.returns(true);
});
it('should not render body or footer', () => { it('should not render body or footer', () => {
const wrapper = createComponent({ threadIsCollapsed: true }); const wrapper = createComponent({
isReply: true,
threadIsCollapsed: true,
});
assert.isFalse(wrapper.find('AnnotationBody').exists()); assert.isFalse(wrapper.find('AnnotationBody').exists());
assert.isFalse(wrapper.find('footer').exists()); assert.isFalse(wrapper.find('footer').exists());
}); });
it('should not show actions', () => { it('should not show actions', () => {
const wrapper = createComponent({ threadIsCollapsed: true }); const wrapper = createComponent({
isReply: true,
threadIsCollapsed: true,
});
assert.isFalse(wrapper.find('AnnotationActionBar').exists()); assert.isFalse(wrapper.find('AnnotationActionBar').exists());
}); });
...@@ -244,8 +241,10 @@ describe('Annotation', () => { ...@@ -244,8 +241,10 @@ describe('Annotation', () => {
context('collapsed top-level annotation', () => { context('collapsed top-level annotation', () => {
it('should render body and footer', () => { it('should render body and footer', () => {
fakeMetadata.isReply.returns(false); const wrapper = createComponent({
const wrapper = createComponent({ threadIsCollapsed: true }); isReply: false,
threadIsCollapsed: true,
});
assert.isTrue(wrapper.find('AnnotationBody').exists()); assert.isTrue(wrapper.find('AnnotationBody').exists());
assert.isTrue(wrapper.find('footer').exists()); assert.isTrue(wrapper.find('footer').exists());
...@@ -269,15 +268,13 @@ describe('Annotation', () => { ...@@ -269,15 +268,13 @@ describe('Annotation', () => {
{ {
name: 'when a collapsed top-level thread', name: 'when a collapsed top-level thread',
content: () => { content: () => {
fakeMetadata.isReply.returns(false); return createComponent({ isReply: false, threadIsCollapsed: true });
return createComponent({ threadIsCollapsed: true });
}, },
}, },
{ {
name: 'when a collapsed reply', name: 'when a collapsed reply',
content: () => { content: () => {
fakeMetadata.isReply.returns(true); return createComponent({ isReply: true, threadIsCollapsed: true });
return createComponent({ threadIsCollapsed: true });
}, },
}, },
]) ])
......
import { mount } from 'enzyme';
import { createElement } from 'preact';
import AnnotationMissing, { $imports } from '../AnnotationMissing';
import { checkAccessibility } from '../../../test-util/accessibility';
import mockImportedComponents from '../../../test-util/mock-imported-components';
describe('AnnotationMissing', () => {
let fakeOnToggleReplies;
function createComponent(props = {}) {
return mount(
<AnnotationMissing
hasAppliedFilter={false}
isReply={false}
onToggleReplies={fakeOnToggleReplies}
replyCount={5}
threadIsCollapsed={true}
{...props}
/>
);
}
beforeEach(() => {
fakeOnToggleReplies = sinon.stub();
$imports.$mock(mockImportedComponents());
});
afterEach(() => {
$imports.$restore();
});
context('collapsed reply', () => {
it('does not show message-unavailable text', () => {
const wrapper = createComponent({
isReply: true,
threadIsCollapsed: true,
});
assert.equal(wrapper.text(), '');
});
it('does not render a reply toggle', () => {
const wrapper = createComponent({
isReply: true,
threadIsCollapsed: true,
});
assert.isFalse(wrapper.find('AnnotationReplyToggle').exists());
});
});
context('collapsed thread, not a reply', () => {
it('shows message-unavailable text', () => {
const wrapper = createComponent({
isReply: false,
threadIsCollapsed: true,
});
assert.match(wrapper.text(), /Message not available/);
});
it('renders a reply toggle control', () => {
const wrapper = createComponent({
isReply: false,
threadIsCollapsed: true,
});
const toggle = wrapper.find('AnnotationReplyToggle');
assert.equal(toggle.props().onToggleReplies, fakeOnToggleReplies);
});
});
context('expanded thread, not a reply', () => {
it('shows message-unavailable text', () => {
const wrapper = createComponent({
isReply: false,
threadIsCollapsed: false,
});
assert.match(wrapper.text(), /Message not available/);
});
it('renders a reply toggle control', () => {
const wrapper = createComponent({
isReply: false,
threadIsCollapsed: false,
});
const toggle = wrapper.find('AnnotationReplyToggle');
assert.equal(toggle.props().onToggleReplies, fakeOnToggleReplies);
});
});
it(
'should pass a11y checks',
checkAccessibility({
content: () => createComponent(),
})
);
});
import { mount } from 'enzyme';
import { createElement } from 'preact';
import { act } from 'preact/test-utils';
import AnnotationReplyToggle from '../AnnotationReplyToggle';
import { checkAccessibility } from '../../../test-util/accessibility';
describe('AnnotationReplyToggle', () => {
let fakeOnToggleReplies;
function createComponent(props = {}) {
return mount(
<AnnotationReplyToggle
onToggleReplies={fakeOnToggleReplies}
replyCount={5}
threadIsCollapsed={true}
{...props}
/>
);
}
beforeEach(() => {
fakeOnToggleReplies = sinon.stub();
// Note that this component does not mock imported components
// because it entirely consists of a `Button`
});
it('renders expand wording if thread is collapsed', () => {
const wrapper = createComponent();
assert.match(wrapper.text(), /^Show replies/);
});
it('renders collapse wording if thread is expanded', () => {
const wrapper = createComponent({ threadIsCollapsed: false });
assert.match(wrapper.text(), /^Hide replies/);
});
it('shows the reply count', () => {
const wrapper = createComponent({ replyCount: 7 });
assert.equal(wrapper.text(), 'Show replies (7)');
});
it('invokes the toggle callback when clicked', () => {
const wrapper = createComponent();
const button = wrapper.find('Button');
act(() => {
button.props().onClick();
});
assert.calledOnce(fakeOnToggleReplies);
});
it(
'should pass a11y checks',
checkAccessibility({
content: () => createComponent(),
})
);
});
...@@ -81,6 +81,7 @@ describe('Thread', () => { ...@@ -81,6 +81,7 @@ describe('Thread', () => {
beforeEach(() => { beforeEach(() => {
fakeStore = { fakeStore = {
hasAppliedFilter: sinon.stub().returns(false),
setExpanded: sinon.stub(), setExpanded: sinon.stub(),
}; };
...@@ -189,7 +190,7 @@ describe('Thread', () => { ...@@ -189,7 +190,7 @@ describe('Thread', () => {
}); });
}); });
context('thread annotation has been deleted', () => { context('visible thread whose annotation has been deleted', () => {
let noAnnotationThread; let noAnnotationThread;
beforeEach(() => { beforeEach(() => {
...@@ -204,10 +205,29 @@ describe('Thread', () => { ...@@ -204,10 +205,29 @@ describe('Thread', () => {
assert.isFalse(wrapper.find('ModerationBanner').exists()); assert.isFalse(wrapper.find('ModerationBanner').exists());
}); });
it('renders an unavailable message', () => { it('renders a missing annotation component', () => {
const wrapper = createComponent({ thread: noAnnotationThread }); const wrapper = createComponent({ thread: noAnnotationThread });
assert.isTrue(wrapper.find('.Thread__unavailable-message').exists()); const annotationMissing = wrapper.find('AnnotationMissing');
assert.isTrue(annotationMissing.exists());
});
});
context('non-visible thread whose annotation has been deleted', () => {
let noAnnotationThread;
beforeEach(() => {
noAnnotationThread = createThread();
noAnnotationThread.annotation = undefined;
noAnnotationThread.visible = false;
});
it('does not render any kind of annotation component', () => {
const wrapper = createComponent({ thread: noAnnotationThread });
assert.isFalse(wrapper.find('Annotation').exists());
assert.isFalse(wrapper.find('AnnotationMissing').exists());
}); });
}); });
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment