diff --git a/entry_types/scrolled/package/jest.config.js b/entry_types/scrolled/package/jest.config.js index 3030b327b0..2f3123288f 100644 --- a/entry_types/scrolled/package/jest.config.js +++ b/entry_types/scrolled/package/jest.config.js @@ -15,6 +15,7 @@ module.exports = { '/spec/support/matchMediaStub.js', '/spec/support/requestAnimationFrameStub.js', '/spec/support/scrollIntoViewStub.js', + '/spec/support/getSelectionStub.js', '/spec/support/fakeBrowserFeatures.js' ], modulePaths: ['/src', '/spec'], diff --git a/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableInlineText-spec.js b/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableInlineText-spec.js index 143428d362..7f4e5bbf1b 100644 --- a/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableInlineText-spec.js +++ b/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableInlineText-spec.js @@ -9,8 +9,6 @@ import '@testing-library/jest-dom/extend-expect' describe('EditableInlineText', () => { beforeAll(loadInlineEditingComponents); - beforeAll(() => window.getSelection = function() {}); - it('renders text from value', () => { const value = [{ type: 'heading', diff --git a/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText-spec.js b/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText-spec.js deleted file mode 100644 index 31012f5a41..0000000000 --- a/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText-spec.js +++ /dev/null @@ -1,496 +0,0 @@ -import React, {useEffect} from 'react'; -import {DndProvider} from 'react-dnd'; -import {HTML5Backend} from 'react-dnd-html5-backend'; - -import {features} from 'pageflow/frontend'; -import {EditableText} from 'frontend'; -import {loadInlineEditingComponents} from 'frontend/inlineEditing'; -import {EditorStateProvider, useEditorSelection} from 'frontend/inlineEditing/EditorState'; -import {renderWithCommenting} from 'testHelpers/renderWithCommenting'; -import {fakeParentWindow} from 'support'; - -import {render, fireEvent, act} from '@testing-library/react'; -import '@testing-library/jest-dom/extend-expect' - -import {commentHighlightStyles as highlightStyles} from 'pageflow-scrolled/review'; -import badgeStyles from 'review/Badge.module.css'; - -describe('EditableText', () => { - beforeAll(loadInlineEditingComponents); - - beforeAll(() => window.getSelection = function() {}); - - const wrapper = ({children}) => ( - - {children} - - ); - - it('renders text from value', () => { - const value = [{ - type: 'heading', - children: [ - {text: 'Some text'} - ] - }]; - - const {queryByText} = render(, {wrapper}); - - expect(queryByText('Some text')).toBeInTheDocument() - }); - - it('renders class name', () => { - const value = [{ - type: 'heading', - children: [ - {text: 'Some text'} - ] - }]; - - const {container} = render( - , {wrapper} - ); - - expect(container.querySelector('.some-class')).toBeInTheDocument() - }); - - it('uses body scaleCategory by default', () => { - const value = [{ - type: 'paragraph', - children: [ - {text: 'Some text'} - ] - }]; - - const {container} = render( - , {wrapper} - ); - - expect(container.querySelector('.typography-body')).toBeInTheDocument() - }); - - it('supports using different scaleCategory', () => { - const value = [{ - type: 'paragraph', - children: [ - {text: 'Some text'} - ] - }]; - - const {container} = render( - , {wrapper} - ); - - expect(container.querySelector('.typography-quoteText')).toBeInTheDocument() - }); - - it('supports typography variant prop', () => { - const value = [{ - type: 'paragraph', - children: [ - {text: 'Some text'} - ] - }]; - - const {container} = render( - , - {wrapper} - ); - - expect(container.querySelector('.typography-quoteText-highlight')).toBeInTheDocument() - }); - - it('supports typography size prop', () => { - const value = [{ - type: 'paragraph', - children: [ - {text: 'Some text'} - ] - }]; - - const {container} = render( - , - {wrapper} - ); - - expect(container.querySelector('.typography-question-lg')).toBeInTheDocument() - }); - - it('renders placeholder if value is undefined', () => { - const {queryByText} = render(, {wrapper}); - - expect(queryByText('Some placeholder')).toBeInTheDocument() - }); - - it('renders placeholder if value is empty', () => { - const value = [{ - type: 'paragraph', - children: [ - {text: ''} - ] - }]; - - const {queryByText} = render(, - {wrapper}); - - expect(queryByText('Some placeholder')).toBeInTheDocument() - }); - - it('does not render placeholder if value is present', () => { - const value = [{ - type: 'heading', - children: [ - {text: 'Some text'} - ] - }]; - - const {queryByText} = render(, - {wrapper}); - - expect(queryByText('Some placeholder')).not.toBeInTheDocument() - }); - - it('supports rendering custom placeholder class name', () => { - const {container} = render( - , - {wrapper} - ); - - expect(container.querySelector('.custom-placeholder')).toBeInTheDocument() - }); - - it('does not render custom placeholder class name when not blank', () => { - const value = [{ - type: 'heading', - children: [ - {text: 'Some text'} - ] - }]; - - const {container} = render( - , - {wrapper} - ); - - expect(container.querySelector('.custom-placeholder')).not.toBeInTheDocument() - }); - - describe('with commenting', () => { - beforeEach(() => { - jest.spyOn(features, 'isEnabled').mockImplementation( - name => name === 'commenting' - ); - }); - - afterEach(() => { - features.isEnabled.mockRestore(); - }); - - it('highlights thread ranges', () => { - const value = [{type: 'paragraph', children: [{text: 'Some text to comment on'}]}]; - - const {container} = renderWithCommenting( - , - { - wrapper, - commentThreads: [{ - id: 1, - subjectType: 'ContentElement', - subjectId: 10, - subjectRange: {anchor: {path: [0, 0], offset: 5}, focus: {path: [0, 0], offset: 9}}, - comments: [{id: 1, body: 'A comment', creatorName: 'Alice', creatorId: 1}] - }] - } - ); - - const highlight = container.querySelector(`.${highlightStyles.highlight}`); - expect(highlight).toBeInTheDocument(); - expect(highlight).toHaveTextContent('text'); - }); - - it('renders badge in dot mode by default', () => { - const value = [{type: 'paragraph', children: [{text: 'Some text to comment on'}]}]; - - const {getByRole} = renderWithCommenting( - , - { - wrapper, - commentThreads: [{ - id: 5, - subjectType: 'ContentElement', - subjectId: 10, - subjectRange: {anchor: {path: [0, 0], offset: 5}, focus: {path: [0, 0], offset: 9}}, - comments: [{id: 1, body: 'A comment', creatorName: 'Alice', creatorId: 1}] - }] - } - ); - - expect(getByRole('status')).toHaveClass(badgeStyles.dot); - }); - - it('highlights pending new thread range from editor state', () => { - let setSelectionRef; - function SelectionCapture({children}) { - const {select} = useEditorSelection(); - useEffect(() => { - setSelectionRef = select; - }, [select]); - return children; - } - - const selectionWrapper = ({children}) => wrapper({ - children: {children} - }); - - const value = [{type: 'paragraph', children: [{text: 'Some text to comment on'}]}]; - - const {container} = renderWithCommenting( - , - {wrapper: selectionWrapper} - ); - - expect(container.querySelector(`.${highlightStyles.highlight}`)) - .not.toBeInTheDocument(); - - act(() => setSelectionRef({ - type: 'newThread', - id: 10, - subjectType: 'ContentElement', - range: {anchor: {path: [0, 0], offset: 5}, focus: {path: [0, 0], offset: 9}} - })); - - const highlight = container.querySelector(`.${highlightStyles.highlight}`); - expect(highlight).toBeInTheDocument(); - expect(highlight).toHaveTextContent('text'); - expect(highlight).toHaveClass(highlightStyles.selected); - - const badge = document.querySelector(`[role="status"]`); - expect(badge).toHaveClass(badgeStyles.active); - }); - - it('applies selected style to highlight when thread is selected in editor state', () => { - let setSelectionRef; - function SelectionCapture({children}) { - const {select} = useEditorSelection(); - useEffect(() => { - setSelectionRef = select; - }, [select]); - return children; - } - - const selectionWrapper = ({children}) => wrapper({ - children: {children} - }); - - const value = [{type: 'paragraph', children: [{text: 'Some text to comment on'}]}]; - - const {container} = renderWithCommenting( - , - { - wrapper: selectionWrapper, - commentThreads: [{ - id: 5, - subjectType: 'ContentElement', - subjectId: 10, - subjectRange: {anchor: {path: [0, 0], offset: 5}, focus: {path: [0, 0], offset: 9}}, - comments: [{id: 1, body: 'A comment', creatorName: 'Alice', creatorId: 1}] - }] - } - ); - - expect(container.querySelector(`.${highlightStyles.highlight}`)) - .not.toHaveClass(highlightStyles.selected); - - act(() => setSelectionRef({ - type: 'contentElementComments', id: 1, highlightedThreadId: 5 - })); - - expect(container.querySelector(`.${highlightStyles.highlight}`)) - .toHaveClass(highlightStyles.selected); - }); - - it('renders only the highlighted thread badge in active mode', () => { - let setSelectionRef; - function SelectionCapture({children}) { - const {select} = useEditorSelection(); - useEffect(() => { - setSelectionRef = select; - }, [select]); - return children; - } - - const selectionWrapper = ({children}) => wrapper({ - children: {children} - }); - - const value = [ - {type: 'paragraph', children: [{text: 'First paragraph thread here'}]}, - {type: 'paragraph', children: [{text: 'Second paragraph thread here'}]} - ]; - - const {getAllByRole} = renderWithCommenting( - , - { - wrapper: selectionWrapper, - commentThreads: [ - {id: 5, subjectType: 'ContentElement', subjectId: 10, - subjectRange: {anchor: {path: [0, 0], offset: 0}, focus: {path: [0, 0], offset: 5}}, - comments: [{id: 1, body: 'first', creatorName: 'Alice', creatorId: 1}]}, - {id: 7, subjectType: 'ContentElement', subjectId: 10, - subjectRange: {anchor: {path: [1, 0], offset: 0}, focus: {path: [1, 0], offset: 6}}, - comments: [{id: 2, body: 'second', creatorName: 'Bob', creatorId: 2}]} - ] - } - ); - - act(() => setSelectionRef({ - type: 'contentElementComments', id: 1, highlightedThreadId: 5 - })); - - const badges = getAllByRole('status'); - expect(badges).toHaveLength(2); - expect(badges[0]).toHaveClass(badgeStyles.active); - expect(badges[1]).not.toHaveClass(badgeStyles.active); - }); - - it('renders sibling badge in regular mode when in same block as highlighted thread', () => { - let setSelectionRef; - function SelectionCapture({children}) { - const {select} = useEditorSelection(); - useEffect(() => { - setSelectionRef = select; - }, [select]); - return children; - } - - const selectionWrapper = ({children}) => wrapper({ - children: {children} - }); - - const value = [ - {type: 'paragraph', children: [{text: 'First paragraph with two threads'}]}, - {type: 'paragraph', children: [{text: 'Second paragraph thread'}]} - ]; - - const {getAllByRole} = renderWithCommenting( - , - { - wrapper: selectionWrapper, - commentThreads: [ - {id: 5, subjectType: 'ContentElement', subjectId: 10, - subjectRange: {anchor: {path: [0, 0], offset: 0}, focus: {path: [0, 0], offset: 5}}, - comments: [{id: 1, body: 'a', creatorName: 'Alice', creatorId: 1}]}, - {id: 6, subjectType: 'ContentElement', subjectId: 10, - subjectRange: {anchor: {path: [0, 0], offset: 6}, focus: {path: [0, 0], offset: 9}}, - comments: [{id: 2, body: 'b', creatorName: 'Bob', creatorId: 2}]}, - {id: 7, subjectType: 'ContentElement', subjectId: 10, - subjectRange: {anchor: {path: [1, 0], offset: 0}, focus: {path: [1, 0], offset: 6}}, - comments: [{id: 3, body: 'c', creatorName: 'Eve', creatorId: 3}]} - ] - } - ); - - act(() => setSelectionRef({ - type: 'contentElementComments', id: 1, highlightedThreadId: 5 - })); - - const badges = getAllByRole('status'); - expect(badges).toHaveLength(3); - expect(badges[0]).toHaveClass(badgeStyles.active); - expect(badges[1]).not.toHaveClass(badgeStyles.dot); - expect(badges[2]).toHaveClass(badgeStyles.dot); - }); - - it('posts SELECTED contentElementComments with highlightedThreadId on badge click', () => { - fakeParentWindow(); - window.parent.postMessage = jest.fn(); - - const value = [ - {type: 'paragraph', children: [{text: 'First paragraph'}]} - ]; - - const {getByRole} = renderWithCommenting( - , - { - wrapper, - commentThreads: [ - {id: 5, subjectType: 'ContentElement', subjectId: 10, - subjectRange: {anchor: {path: [0, 0], offset: 0}, focus: {path: [0, 0], offset: 5}}, - comments: [{id: 1, body: 'a', creatorName: 'Alice', creatorId: 1}]} - ] - } - ); - - fireEvent.click(getByRole('status')); - - expect(window.parent.postMessage).toHaveBeenCalledWith({ - type: 'SELECTED', - payload: { - type: 'contentElementComments', - id: 1, - highlightedThreadId: 5 - } - }, expect.anything()); - }); - - it('runs badge click logic and scrolls into view on SELECT_COMMENT_THREAD message', async () => { - fakeParentWindow(); - window.parent.postMessage = jest.fn(); - const scrollIntoView = jest.fn(); - Element.prototype.scrollIntoView = scrollIntoView; - - const subjectRange = {anchor: {path: [0, 0], offset: 0}, focus: {path: [0, 0], offset: 5}}; - const value = [{type: 'paragraph', children: [{text: 'Some text to comment on'}]}]; - - renderWithCommenting( - , - { - wrapper, - commentThreads: [{ - id: 9, - subjectType: 'ContentElement', - subjectId: 10, - subjectRange, - comments: [{id: 1, body: 'A comment', creatorName: 'Alice', creatorId: 1}] - }] - } - ); - - window.parent.postMessage.mockClear(); - - const echoed = new Promise(resolve => { - const original = window.parent.postMessage; - window.parent.postMessage = (data, ...rest) => { - original(data, ...rest); - if (data.type === 'SELECTED' && data.payload.type === 'contentElementComments') { - resolve(data); - } - }; - }); - - act(() => { - window.postMessage({ - type: 'SELECT_COMMENT_THREAD', - payload: {threadId: 9} - }, '*'); - }); - - await expect(echoed).resolves.toMatchObject({ - type: 'SELECTED', - payload: {type: 'contentElementComments', id: 1, highlightedThreadId: 9} - }); - - expect(scrollIntoView).toHaveBeenCalled(); - delete Element.prototype.scrollIntoView; - }); - }); -}); diff --git a/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText/features/commentBadges-spec.js b/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText/features/commentBadges-spec.js new file mode 100644 index 0000000000..17a63940f3 --- /dev/null +++ b/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText/features/commentBadges-spec.js @@ -0,0 +1,114 @@ +import React from 'react'; + +import {features} from 'pageflow/frontend'; +import {EditableText} from 'frontend'; +import {renderEntry, useInlineEditingPageObjects} from 'support/pageObjects/inlineEditing'; + +import '@testing-library/jest-dom/extend-expect'; + +describe('inline editing EditableText comment badges', () => { + useInlineEditingPageObjects(); + + beforeEach(() => { + jest.spyOn(features, 'isEnabled').mockImplementation( + name => name === 'commenting' + ); + }); + + afterEach(() => { + features.isEnabled.mockRestore(); + }); + + it('renders badge in dot mode by default', () => { + const value = [{type: 'paragraph', children: [{text: 'Some text to comment on'}]}]; + + const entry = renderEntry({ + contentElement: { + ui: , + typeOptions: {inlineComments: true, customSelectionRect: true} + }, + commenting: { + currentUser: null, + commentThreads: [{ + id: 5, + subjectType: 'ContentElement', + subjectId: 10, + subjectRange: {anchor: {path: [0, 0], offset: 5}, focus: {path: [0, 0], offset: 9}}, + comments: [{id: 1, body: 'A comment', creatorName: 'Alice', creatorId: 1}] + }] + } + }); + + const badges = entry.queryAllCommentBadges(); + expect(badges).toHaveLength(1); + expect(badges[0].isInDotMode()).toBe(true); + }); + + it('renders only the highlighted thread badge in active mode', () => { + const value = [ + {type: 'paragraph', children: [{text: 'First paragraph thread here'}]}, + {type: 'paragraph', children: [{text: 'Second paragraph thread here'}]} + ]; + + const entry = renderEntry({ + contentElement: { + ui: , + typeOptions: {inlineComments: true, customSelectionRect: true} + }, + commenting: { + currentUser: null, + commentThreads: [ + {id: 5, subjectType: 'ContentElement', subjectId: 10, + subjectRange: {anchor: {path: [0, 0], offset: 0}, focus: {path: [0, 0], offset: 5}}, + comments: [{id: 1, body: 'first', creatorName: 'Alice', creatorId: 1}]}, + {id: 7, subjectType: 'ContentElement', subjectId: 10, + subjectRange: {anchor: {path: [1, 0], offset: 0}, focus: {path: [1, 0], offset: 6}}, + comments: [{id: 2, body: 'second', creatorName: 'Bob', creatorId: 2}]} + ] + } + }); + + entry.queryAllCommentBadges()[0].select(); + + const badges = entry.queryAllCommentBadges(); + expect(badges).toHaveLength(2); + expect(badges[0].isActive()).toBe(true); + expect(badges[1].isActive()).toBe(false); + }); + + it('renders sibling badge in regular mode when in same block as highlighted thread', () => { + const value = [ + {type: 'paragraph', children: [{text: 'First paragraph with two threads'}]}, + {type: 'paragraph', children: [{text: 'Second paragraph thread'}]} + ]; + + const entry = renderEntry({ + contentElement: { + ui: , + typeOptions: {inlineComments: true, customSelectionRect: true} + }, + commenting: { + currentUser: null, + commentThreads: [ + {id: 5, subjectType: 'ContentElement', subjectId: 10, + subjectRange: {anchor: {path: [0, 0], offset: 0}, focus: {path: [0, 0], offset: 5}}, + comments: [{id: 1, body: 'a', creatorName: 'Alice', creatorId: 1}]}, + {id: 6, subjectType: 'ContentElement', subjectId: 10, + subjectRange: {anchor: {path: [0, 0], offset: 6}, focus: {path: [0, 0], offset: 9}}, + comments: [{id: 2, body: 'b', creatorName: 'Bob', creatorId: 2}]}, + {id: 7, subjectType: 'ContentElement', subjectId: 10, + subjectRange: {anchor: {path: [1, 0], offset: 0}, focus: {path: [1, 0], offset: 6}}, + comments: [{id: 3, body: 'c', creatorName: 'Eve', creatorId: 3}]} + ] + } + }); + + entry.queryAllCommentBadges()[0].select(); + + const badges = entry.queryAllCommentBadges(); + expect(badges).toHaveLength(3); + expect(badges[0].isActive()).toBe(true); + expect(badges[1].isInDotMode()).toBe(false); + expect(badges[2].isInDotMode()).toBe(true); + }); +}); diff --git a/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText/features/commentHighlights-spec.js b/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText/features/commentHighlights-spec.js new file mode 100644 index 0000000000..8419614e0c --- /dev/null +++ b/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText/features/commentHighlights-spec.js @@ -0,0 +1,113 @@ +import React from 'react'; + +import {features} from 'pageflow/frontend'; +import {EditableText} from 'frontend'; +import {renderEntry, useInlineEditingPageObjects} from 'support/pageObjects/inlineEditing'; +import {fakeParentWindow} from 'support'; + +import {act, waitFor} from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; + +import {commentHighlightStyles as highlightStyles} from 'pageflow-scrolled/review'; + +describe('inline editing EditableText comment highlights', () => { + useInlineEditingPageObjects(); + + beforeEach(() => { + jest.spyOn(features, 'isEnabled').mockImplementation( + name => name === 'commenting' + ); + }); + + afterEach(() => { + features.isEnabled.mockRestore(); + }); + + const value = [{type: 'paragraph', children: [{text: 'Some text to comment on'}]}]; + const subjectRange = {anchor: {path: [0, 0], offset: 5}, focus: {path: [0, 0], offset: 9}}; + + it('highlights thread ranges', () => { + const entry = renderEntry({ + contentElement: { + ui: , + typeOptions: {inlineComments: true} + }, + commenting: { + currentUser: null, + commentThreads: [{ + id: 1, + subjectType: 'ContentElement', + subjectId: 10, + subjectRange, + comments: [{id: 1, body: 'A comment', creatorName: 'Alice', creatorId: 1}] + }] + } + }); + + const highlight = entry.container.querySelector(`.${highlightStyles.highlight}`); + expect(highlight).toBeInTheDocument(); + expect(highlight).toHaveTextContent('text'); + }); + + it('highlights pending new thread range from editor state', async () => { + fakeParentWindow(); + + const entry = renderEntry({ + contentElement: { + ui: , + typeOptions: {inlineComments: true} + } + }); + + expect(entry.container.querySelector(`.${highlightStyles.highlight}`)) + .not.toBeInTheDocument(); + + act(() => { + window.postMessage({ + type: 'SELECT', + payload: { + type: 'newThread', + id: 10, + subjectType: 'ContentElement', + range: subjectRange + } + }, '*'); + }); + + await waitFor(() => { + expect(entry.container.querySelector(`.${highlightStyles.highlight}`)) + .toBeInTheDocument(); + }); + + const highlight = entry.container.querySelector(`.${highlightStyles.highlight}`); + expect(highlight).toHaveTextContent('text'); + expect(highlight).toHaveClass(highlightStyles.selected); + }); + + it('applies selected style to highlight when thread badge is clicked', () => { + const entry = renderEntry({ + contentElement: { + ui: , + typeOptions: {inlineComments: true, customSelectionRect: true} + }, + commenting: { + currentUser: null, + commentThreads: [{ + id: 5, + subjectType: 'ContentElement', + subjectId: 10, + subjectRange, + comments: [{id: 1, body: 'A comment', creatorName: 'Alice', creatorId: 1}] + }] + } + }); + + expect(entry.container.querySelector(`.${highlightStyles.highlight}`)) + .not.toHaveClass(highlightStyles.selected); + + entry.queryAllCommentBadges()[0].select(); + + expect(entry.container.querySelector(`.${highlightStyles.highlight}`)) + .toHaveClass(highlightStyles.selected); + }); +}); diff --git a/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText/features/commentSelection-spec.js b/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText/features/commentSelection-spec.js new file mode 100644 index 0000000000..5e479f6d48 --- /dev/null +++ b/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText/features/commentSelection-spec.js @@ -0,0 +1,112 @@ +import React from 'react'; + +import {features} from 'pageflow/frontend'; +import {EditableText} from 'frontend'; +import {renderEntry, useInlineEditingPageObjects} from 'support/pageObjects/inlineEditing'; +import {fakeParentWindow} from 'support'; + +import {act} from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; + +describe('inline editing EditableText comment selection messages', () => { + useInlineEditingPageObjects(); + + beforeEach(() => { + jest.spyOn(features, 'isEnabled').mockImplementation( + name => name === 'commenting' + ); + }); + + afterEach(() => { + features.isEnabled.mockRestore(); + }); + + it('posts SELECTED contentElementComments with highlightedThreadId on badge click', () => { + fakeParentWindow(); + window.parent.postMessage = jest.fn(); + + const value = [ + {type: 'paragraph', children: [{text: 'First paragraph'}]} + ]; + + const entry = renderEntry({ + contentElement: { + ui: , + typeOptions: {inlineComments: true, customSelectionRect: true} + }, + commenting: { + currentUser: null, + commentThreads: [ + {id: 5, subjectType: 'ContentElement', subjectId: 10, + subjectRange: {anchor: {path: [0, 0], offset: 0}, focus: {path: [0, 0], offset: 5}}, + comments: [{id: 1, body: 'a', creatorName: 'Alice', creatorId: 1}]} + ] + } + }); + + entry.queryAllCommentBadges()[0].select(); + + expect(window.parent.postMessage).toHaveBeenCalledWith({ + type: 'SELECTED', + payload: { + type: 'contentElementComments', + id: 1, + highlightedThreadId: 5 + } + }, expect.anything()); + }); + + it('runs badge click logic and scrolls into view on SELECT_COMMENT_THREAD message', async () => { + fakeParentWindow(); + window.parent.postMessage = jest.fn(); + const scrollIntoView = jest.fn(); + Element.prototype.scrollIntoView = scrollIntoView; + + const subjectRange = {anchor: {path: [0, 0], offset: 0}, focus: {path: [0, 0], offset: 5}}; + const value = [{type: 'paragraph', children: [{text: 'Some text to comment on'}]}]; + + renderEntry({ + contentElement: { + ui: , + typeOptions: {inlineComments: true, customSelectionRect: true} + }, + commenting: { + currentUser: null, + commentThreads: [{ + id: 9, + subjectType: 'ContentElement', + subjectId: 10, + subjectRange, + comments: [{id: 1, body: 'A comment', creatorName: 'Alice', creatorId: 1}] + }] + } + }); + + window.parent.postMessage.mockClear(); + + const echoed = new Promise(resolve => { + const original = window.parent.postMessage; + window.parent.postMessage = (data, ...rest) => { + original(data, ...rest); + if (data.type === 'SELECTED' && data.payload.type === 'contentElementComments') { + resolve(data); + } + }; + }); + + act(() => { + window.postMessage({ + type: 'SELECT_COMMENT_THREAD', + payload: {threadId: 9} + }, '*'); + }); + + await expect(echoed).resolves.toMatchObject({ + type: 'SELECTED', + payload: {type: 'contentElementComments', id: 1, highlightedThreadId: 9} + }); + + expect(scrollIntoView).toHaveBeenCalled(); + delete Element.prototype.scrollIntoView; + }); +}); diff --git a/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText/features/placeholder-spec.js b/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText/features/placeholder-spec.js new file mode 100644 index 0000000000..4241478e8e --- /dev/null +++ b/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText/features/placeholder-spec.js @@ -0,0 +1,71 @@ +import React from 'react'; + +import {EditableText} from 'frontend'; +import {renderEntry, useInlineEditingPageObjects} from 'support/pageObjects/inlineEditing'; + +import '@testing-library/jest-dom/extend-expect'; + +describe('inline editing EditableText placeholder', () => { + useInlineEditingPageObjects(); + + const headingValue = [{ + type: 'heading', + children: [{text: 'Some text'}] + }]; + + const emptyParagraphValue = [{ + type: 'paragraph', + children: [{text: ''}] + }]; + + it('renders placeholder if value is undefined', () => { + const entry = renderEntry({ + contentElement: {ui: } + }); + + expect(entry.queryByText('Some placeholder')).toBeInTheDocument(); + }); + + it('renders placeholder if value is empty', () => { + const entry = renderEntry({ + contentElement: { + ui: + } + }); + + expect(entry.queryByText('Some placeholder')).toBeInTheDocument(); + }); + + it('does not render placeholder if value is present', () => { + const entry = renderEntry({ + contentElement: { + ui: + } + }); + + expect(entry.queryByText('Some placeholder')).not.toBeInTheDocument(); + }); + + it('supports rendering custom placeholder class name', () => { + const entry = renderEntry({ + contentElement: { + ui: + } + }); + + expect(entry.container.querySelector('.custom-placeholder')).toBeInTheDocument(); + }); + + it('does not render custom placeholder class name when not blank', () => { + const entry = renderEntry({ + contentElement: { + ui: + } + }); + + expect(entry.container.querySelector('.custom-placeholder')).not.toBeInTheDocument(); + }); +}); diff --git a/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText/features/rendering-spec.js b/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText/features/rendering-spec.js new file mode 100644 index 0000000000..a3c14ab447 --- /dev/null +++ b/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText/features/rendering-spec.js @@ -0,0 +1,38 @@ +import React from 'react'; + +import {EditableText} from 'frontend'; +import {renderEntry, useInlineEditingPageObjects} from 'support/pageObjects/inlineEditing'; + +import '@testing-library/jest-dom/extend-expect'; + +describe('inline editing EditableText rendering', () => { + useInlineEditingPageObjects(); + + it('renders text from value', () => { + const value = [{ + type: 'heading', + children: [{text: 'Some text'}] + }]; + + const entry = renderEntry({ + contentElement: {ui: } + }); + + expect(entry.queryByText('Some text')).toBeInTheDocument(); + }); + + it('renders class name', () => { + const value = [{ + type: 'heading', + children: [{text: 'Some text'}] + }]; + + const entry = renderEntry({ + contentElement: { + ui: + } + }); + + expect(entry.container.querySelector('.some-class')).toBeInTheDocument(); + }); +}); diff --git a/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText/features/typography-spec.js b/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText/features/typography-spec.js new file mode 100644 index 0000000000..776356ffe7 --- /dev/null +++ b/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText/features/typography-spec.js @@ -0,0 +1,57 @@ +import React from 'react'; + +import {EditableText} from 'frontend'; +import {renderEntry, useInlineEditingPageObjects} from 'support/pageObjects/inlineEditing'; + +import '@testing-library/jest-dom/extend-expect'; + +describe('inline editing EditableText typography', () => { + useInlineEditingPageObjects(); + + const paragraphValue = [{ + type: 'paragraph', + children: [{text: 'Some text'}] + }]; + + it('uses body scaleCategory by default', () => { + const entry = renderEntry({ + contentElement: {ui: } + }); + + expect(entry.container.querySelector('.typography-body')).toBeInTheDocument(); + }); + + it('supports using different scaleCategory', () => { + const entry = renderEntry({ + contentElement: { + ui: + } + }); + + expect(entry.container.querySelector('.typography-quoteText')).toBeInTheDocument(); + }); + + it('supports typography variant prop', () => { + const entry = renderEntry({ + contentElement: { + ui: + } + }); + + expect(entry.container.querySelector('.typography-quoteText-highlight')).toBeInTheDocument(); + }); + + it('supports typography size prop', () => { + const entry = renderEntry({ + contentElement: { + ui: + } + }); + + expect(entry.container.querySelector('.typography-question-lg')).toBeInTheDocument(); + }); +}); diff --git a/entry_types/scrolled/package/spec/support/getSelectionStub.js b/entry_types/scrolled/package/spec/support/getSelectionStub.js new file mode 100644 index 0000000000..6180fce700 --- /dev/null +++ b/entry_types/scrolled/package/spec/support/getSelectionStub.js @@ -0,0 +1,6 @@ +// jsdom's Selection implementation is incomplete for slate-react's +// usage. Stub it so slate-using components don't blow up trying to +// read DOM selection state during tests. +beforeEach(() => { + window.getSelection = () => undefined; +}); diff --git a/entry_types/scrolled/package/spec/support/pageObjects/index.js b/entry_types/scrolled/package/spec/support/pageObjects/index.js index 7e350c601f..bb353f7187 100644 --- a/entry_types/scrolled/package/spec/support/pageObjects/index.js +++ b/entry_types/scrolled/package/spec/support/pageObjects/index.js @@ -12,12 +12,9 @@ import centerLayoutStyles from 'frontend/layouts/Center.module.css'; import twoColumnLayoutStyles from 'frontend/layouts/TwoColumn.module.css'; import boxBoundaryMarginStyles from 'frontend/foregroundBoxes/BoxBoundaryMargin.module.css'; import {StaticPreview} from 'frontend/useScrollPositionLifecycle'; -import {loadInlineEditingComponents} from 'frontend/inlineEditing'; -import {clearExtensions} from 'frontend/extensionRegistry'; import {api} from 'frontend/api'; import {act, fireEvent, queryHelpers, queries, within} from '@testing-library/react' -import {useFakeTranslations} from 'pageflow/testHelpers'; import {simulateScrollingIntoView} from '../fakeIntersectionObserver'; export function renderEntry({ @@ -94,34 +91,7 @@ export function renderContentElement({typeName, configuration = {}, ...seedOptio }; } -export function useInlineEditingPageObjects() { - beforeAll(async () => { - await loadInlineEditingComponents(); - }); - - afterAll(() => { - act(() => clearExtensions()); - }); - - useFakeTranslations({ - 'pageflow_scrolled.inline_editing.select_section': 'Select section', - 'pageflow_scrolled.inline_editing.select_content_element': 'Select content element', - 'pageflow_scrolled.inline_editing.add_content_element': 'Add content element', - 'pageflow_scrolled.inline_editing.insert_content_element.after': 'Insert content element after', - 'pageflow_scrolled.inline_editing.drag_content_element': 'Drag to move', - 'pageflow_scrolled.inline_editing.edit_section_transition_before': 'Edit section transition before', - 'pageflow_scrolled.inline_editing.edit_section_transition_after': 'Edit section transition after', - 'pageflow_scrolled.inline_editing.edit_section_padding_top': 'Edit top padding', - 'pageflow_scrolled.inline_editing.edit_section_padding_bottom': 'Edit bottom padding', - 'pageflow_scrolled.inline_editing.expose_motif_area': 'Expose motif area', - 'pageflow_scrolled.inline_editing.padding_suppressed_before_full_width': 'Padding suppressed before full width element', - 'pageflow_scrolled.inline_editing.padding_suppressed_after_full_width': 'Padding suppressed after full width element', - 'pageflow_scrolled.inline_editing.content_element_margin_top': 'Top margin', - 'pageflow_scrolled.inline_editing.content_element_margin_bottom': 'Bottom margin' - }); - - usePageObjects(); -} +export {useInlineEditingPageObjects} from './inlineEditing'; export function usePageObjects() { beforeEach(() => { diff --git a/entry_types/scrolled/package/spec/support/pageObjects/inlineEditing.js b/entry_types/scrolled/package/spec/support/pageObjects/inlineEditing.js new file mode 100644 index 0000000000..30f42585c3 --- /dev/null +++ b/entry_types/scrolled/package/spec/support/pageObjects/inlineEditing.js @@ -0,0 +1,62 @@ +import {act, fireEvent} from '@testing-library/react'; +import {useFakeTranslations} from 'pageflow/testHelpers'; + +import {loadInlineEditingComponents} from 'frontend/inlineEditing'; +import {clearExtensions} from 'frontend/extensionRegistry'; +import badgeStyles from 'review/Badge.module.css'; + +import { + renderEntry as baseRenderEntry, + usePageObjects +} from './index'; + +export function renderEntry({commenting, ...options} = {}) { + const result = baseRenderEntry({ + ...options, + entryProps: {commentingInitialState: commenting} + }); + + return { + ...result, + queryAllCommentBadges: () => + result.queryAllByRole('status').map(createCommentBadgePageObject) + }; +} + +export function useInlineEditingPageObjects() { + beforeAll(async () => { + await loadInlineEditingComponents(); + }); + + afterAll(() => { + act(() => clearExtensions()); + }); + + useFakeTranslations({ + 'pageflow_scrolled.inline_editing.select_section': 'Select section', + 'pageflow_scrolled.inline_editing.select_content_element': 'Select content element', + 'pageflow_scrolled.inline_editing.add_content_element': 'Add content element', + 'pageflow_scrolled.inline_editing.insert_content_element.after': 'Insert content element after', + 'pageflow_scrolled.inline_editing.drag_content_element': 'Drag to move', + 'pageflow_scrolled.inline_editing.edit_section_transition_before': 'Edit section transition before', + 'pageflow_scrolled.inline_editing.edit_section_transition_after': 'Edit section transition after', + 'pageflow_scrolled.inline_editing.edit_section_padding_top': 'Edit top padding', + 'pageflow_scrolled.inline_editing.edit_section_padding_bottom': 'Edit bottom padding', + 'pageflow_scrolled.inline_editing.expose_motif_area': 'Expose motif area', + 'pageflow_scrolled.inline_editing.padding_suppressed_before_full_width': 'Padding suppressed before full width element', + 'pageflow_scrolled.inline_editing.padding_suppressed_after_full_width': 'Padding suppressed after full width element', + 'pageflow_scrolled.inline_editing.content_element_margin_top': 'Top margin', + 'pageflow_scrolled.inline_editing.content_element_margin_bottom': 'Bottom margin' + }); + + usePageObjects(); +} + +function createCommentBadgePageObject(el) { + return { + el, + isInDotMode: () => el.classList.contains(badgeStyles.dot), + isActive: () => el.classList.contains(badgeStyles.active), + select: () => fireEvent.click(el) + }; +} diff --git a/entry_types/scrolled/package/src/frontend/inlineEditing/EntryDecorator.js b/entry_types/scrolled/package/src/frontend/inlineEditing/EntryDecorator.js index f8d8199a45..58cdccea06 100644 --- a/entry_types/scrolled/package/src/frontend/inlineEditing/EntryDecorator.js +++ b/entry_types/scrolled/package/src/frontend/inlineEditing/EntryDecorator.js @@ -9,12 +9,12 @@ import { ContentElementEditorCommandSubscriptionProvider } from './ContentElementEditorCommandSubscriptionProvider'; -export function EntryDecorator({children}) { +export function EntryDecorator({commentingInitialState, children}) { const contentElementEditorCommandEmitter = useContentElementEditorCommandEmitter(); return ( - + {children} diff --git a/entry_types/scrolled/package/src/testHelpers/renderWithCommenting.js b/entry_types/scrolled/package/src/testHelpers/renderWithCommenting.js deleted file mode 100644 index a4cbd7f6d6..0000000000 --- a/entry_types/scrolled/package/src/testHelpers/renderWithCommenting.js +++ /dev/null @@ -1,54 +0,0 @@ -import React from 'react'; -import {render, act} from '@testing-library/react'; - -import {ReviewStateProvider} from '../review/ReviewStateProvider'; -import {SelectedSubjectProvider} from '../frontend/commenting/SelectedSubjectProvider'; -import {AddCommentModeProvider, useAddCommentMode} from '../frontend/commenting/AddCommentModeProvider'; -import {ContentElementAttributesProvider} from '../frontend/useContentElementAttributes'; - -function PassThrough({children}) { - return children; -} - -export function renderWithCommenting(ui, { - contentElementId = 1, - contentElementPermaId = 10, - inlineComments = true, - commentThreads = [], - currentUser = null, - wrapper: OuterWrapper = PassThrough -} = {}) { - let addCommentModeRef; - - function AddCommentModeCapture({children}) { - const mode = useAddCommentMode(); - addCommentModeRef = mode; - return children; - } - - const result = render( - - - - - - - {ui} - - - - - - - ); - - return { - ...result, - - toggleAddCommentMode() { - act(() => addCommentModeRef.toggle()); - } - }; -}