From ace25a9d421ec3f5a9ff19b0317b2023b99855fb Mon Sep 17 00:00:00 2001 From: Tim Fischbach Date: Thu, 21 May 2026 16:14:01 +0200 Subject: [PATCH 1/4] Split inline editing EditableText spec by topic Break the monolithic spec into six focused files under `EditableText/features/` (rendering, typography, placeholder, commentHighlights, commentBadges, commentSelection), mirroring the commenting feature spec layout. Migrate off `renderWithCommenting` and the local DndProvider + EditorStateProvider wrapper onto `renderEntry` from a new `support/pageObjects/inlineEditing` module, so tests exercise the real EntryDecorator instead of a hand-built provider tree. The inline- editing EntryDecorator gains a `commenting` prop that seeds ReviewState, parallel to commenting's EntryDecorator. Hide badge role/styling details behind a `queryAllCommentBadges` page object so specs assert via `isInDotMode()` / `isActive()` rather than querying `[role=status]` and checking CSS module classes. --- .../inlineEditing/EditableText-spec.js | 496 ------------------ .../features/commentBadges-spec.js | 136 +++++ .../features/commentHighlights-spec.js | 120 +++++ .../features/commentSelection-spec.js | 114 ++++ .../EditableText/features/placeholder-spec.js | 73 +++ .../EditableText/features/rendering-spec.js | 40 ++ .../EditableText/features/typography-spec.js | 59 +++ .../package/spec/support/pageObjects/index.js | 32 +- .../spec/support/pageObjects/inlineEditing.js | 61 +++ .../frontend/inlineEditing/EntryDecorator.js | 4 +- 10 files changed, 606 insertions(+), 529 deletions(-) delete mode 100644 entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText-spec.js create mode 100644 entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText/features/commentBadges-spec.js create mode 100644 entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText/features/commentHighlights-spec.js create mode 100644 entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText/features/commentSelection-spec.js create mode 100644 entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText/features/placeholder-spec.js create mode 100644 entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText/features/rendering-spec.js create mode 100644 entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText/features/typography-spec.js create mode 100644 entry_types/scrolled/package/spec/support/pageObjects/inlineEditing.js 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..4165ec91c7 --- /dev/null +++ b/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText/features/commentBadges-spec.js @@ -0,0 +1,136 @@ +import React, {useEffect} from 'react'; + +import {features} from 'pageflow/frontend'; +import {EditableText} from 'frontend'; +import {useEditorSelection} from 'frontend/inlineEditing/EditorState'; +import {renderEntry, useInlineEditingPageObjects} from 'support/pageObjects/inlineEditing'; + +import {act} from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; + +describe('inline editing EditableText comment badges', () => { + useInlineEditingPageObjects(); + + beforeAll(() => window.getSelection = function() {}); + + 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', () => { + let setSelectionRef; + function SelectionCapture({children}) { + const {select} = useEditorSelection(); + useEffect(() => { setSelectionRef = select; }, [select]); + return children; + } + + 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}]} + ] + } + }); + + act(() => setSelectionRef({ + type: 'contentElementComments', id: 1, highlightedThreadId: 5 + })); + + 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', () => { + let setSelectionRef; + function SelectionCapture({children}) { + const {select} = useEditorSelection(); + useEffect(() => { setSelectionRef = select; }, [select]); + return children; + } + + 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}]} + ] + } + }); + + act(() => setSelectionRef({ + type: 'contentElementComments', id: 1, highlightedThreadId: 5 + })); + + 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..dc347ec3d4 --- /dev/null +++ b/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText/features/commentHighlights-spec.js @@ -0,0 +1,120 @@ +import React, {useEffect} from 'react'; + +import {features} from 'pageflow/frontend'; +import {EditableText} from 'frontend'; +import {useEditorSelection} from 'frontend/inlineEditing/EditorState'; +import {renderEntry, useInlineEditingPageObjects} from 'support/pageObjects/inlineEditing'; + +import {act} 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(); + + beforeAll(() => window.getSelection = function() {}); + + 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', () => { + let setSelectionRef; + function SelectionCapture({children}) { + const {select} = useEditorSelection(); + useEffect(() => { setSelectionRef = select; }, [select]); + return children; + } + + const entry = renderEntry({ + contentElement: { + ui: , + typeOptions: {inlineComments: true} + } + }); + + expect(entry.container.querySelector(`.${highlightStyles.highlight}`)) + .not.toBeInTheDocument(); + + act(() => setSelectionRef({ + type: 'newThread', + id: 10, + subjectType: 'ContentElement', + range: subjectRange + })); + + const highlight = entry.container.querySelector(`.${highlightStyles.highlight}`); + expect(highlight).toBeInTheDocument(); + expect(highlight).toHaveTextContent('text'); + expect(highlight).toHaveClass(highlightStyles.selected); + }); + + 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 entry = renderEntry({ + contentElement: { + ui: , + typeOptions: {inlineComments: 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); + + act(() => setSelectionRef({ + type: 'contentElementComments', id: 1, highlightedThreadId: 5 + })); + + 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..3657840147 --- /dev/null +++ b/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText/features/commentSelection-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 {fakeParentWindow} from 'support'; + +import {act, fireEvent} from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; + +describe('inline editing EditableText comment selection messages', () => { + useInlineEditingPageObjects(); + + beforeAll(() => window.getSelection = function() {}); + + 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}]} + ] + } + }); + + fireEvent.click(entry.queryAllCommentBadges()[0].el); + + 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..ef3d82f2a0 --- /dev/null +++ b/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText/features/placeholder-spec.js @@ -0,0 +1,73 @@ +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(); + + beforeAll(() => window.getSelection = function() {}); + + 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..1ca794a4a2 --- /dev/null +++ b/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText/features/rendering-spec.js @@ -0,0 +1,40 @@ +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(); + + beforeAll(() => window.getSelection = function() {}); + + 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..f21f233aed --- /dev/null +++ b/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText/features/typography-spec.js @@ -0,0 +1,59 @@ +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(); + + beforeAll(() => window.getSelection = function() {}); + + 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/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..5831acacf8 --- /dev/null +++ b/entry_types/scrolled/package/spec/support/pageObjects/inlineEditing.js @@ -0,0 +1,61 @@ +import {act} 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) + }; +} 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} From 498f17a84ee869e1e3d6fa7f55f3b016201b068e Mon Sep 17 00:00:00 2001 From: Tim Fischbach Date: Thu, 21 May 2026 16:48:43 +0200 Subject: [PATCH 2/4] Remove unused renderWithCommenting test helper Both EditableText specs that previously used it now go through renderEntry, exercising the real EntryDecorator provider tree. The helper's only purpose was to shortcut that tree with a hand-built provider stack, which had a habit of drifting from production. --- .../src/testHelpers/renderWithCommenting.js | 54 ------------------- 1 file changed, 54 deletions(-) delete mode 100644 entry_types/scrolled/package/src/testHelpers/renderWithCommenting.js 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()); - } - }; -} From 9840a07619cc2f7d040894cbb1d7d18441b706d0 Mon Sep 17 00:00:00 2001 From: Tim Fischbach Date: Thu, 21 May 2026 16:59:41 +0200 Subject: [PATCH 3/4] Drop SelectionCapture from EditableText specs Move editor-selection state changes off a test-only SelectionCapture component (which reached into `useEditorSelection.select`) and onto production pathways: `newThread` via a `SELECT` postMessage hitting inline-editing's MessageHandler, and `highlightedThreadId` via clicking an inline thread badge through a new `select()` method on the badge page object. --- .../features/commentBadges-spec.js | 30 ++-------- .../features/commentHighlights-spec.js | 57 +++++++++---------- .../features/commentSelection-spec.js | 4 +- .../spec/support/pageObjects/inlineEditing.js | 5 +- 4 files changed, 36 insertions(+), 60 deletions(-) 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 index 4165ec91c7..183a74f24b 100644 --- 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 @@ -1,11 +1,9 @@ -import React, {useEffect} from 'react'; +import React from 'react'; import {features} from 'pageflow/frontend'; import {EditableText} from 'frontend'; -import {useEditorSelection} from 'frontend/inlineEditing/EditorState'; import {renderEntry, useInlineEditingPageObjects} from 'support/pageObjects/inlineEditing'; -import {act} from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; describe('inline editing EditableText comment badges', () => { @@ -49,13 +47,6 @@ describe('inline editing EditableText comment badges', () => { }); 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 value = [ {type: 'paragraph', children: [{text: 'First paragraph thread here'}]}, {type: 'paragraph', children: [{text: 'Second paragraph thread here'}]} @@ -63,7 +54,7 @@ describe('inline editing EditableText comment badges', () => { const entry = renderEntry({ contentElement: { - ui: , + ui: , typeOptions: {inlineComments: true, customSelectionRect: true} }, commenting: { @@ -79,9 +70,7 @@ describe('inline editing EditableText comment badges', () => { } }); - act(() => setSelectionRef({ - type: 'contentElementComments', id: 1, highlightedThreadId: 5 - })); + entry.queryAllCommentBadges()[0].select(); const badges = entry.queryAllCommentBadges(); expect(badges).toHaveLength(2); @@ -90,13 +79,6 @@ describe('inline editing EditableText comment badges', () => { }); 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 value = [ {type: 'paragraph', children: [{text: 'First paragraph with two threads'}]}, {type: 'paragraph', children: [{text: 'Second paragraph thread'}]} @@ -104,7 +86,7 @@ describe('inline editing EditableText comment badges', () => { const entry = renderEntry({ contentElement: { - ui: , + ui: , typeOptions: {inlineComments: true, customSelectionRect: true} }, commenting: { @@ -123,9 +105,7 @@ describe('inline editing EditableText comment badges', () => { } }); - act(() => setSelectionRef({ - type: 'contentElementComments', id: 1, highlightedThreadId: 5 - })); + entry.queryAllCommentBadges()[0].select(); const badges = entry.queryAllCommentBadges(); expect(badges).toHaveLength(3); 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 index dc347ec3d4..cfc5133ef0 100644 --- 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 @@ -1,11 +1,11 @@ -import React, {useEffect} from 'react'; +import React from 'react'; import {features} from 'pageflow/frontend'; import {EditableText} from 'frontend'; -import {useEditorSelection} from 'frontend/inlineEditing/EditorState'; import {renderEntry, useInlineEditingPageObjects} from 'support/pageObjects/inlineEditing'; +import {fakeParentWindow} from 'support'; -import {act} from '@testing-library/react'; +import {act, waitFor} from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import {commentHighlightStyles as highlightStyles} from 'pageflow-scrolled/review'; @@ -51,17 +51,12 @@ describe('inline editing EditableText comment highlights', () => { expect(highlight).toHaveTextContent('text'); }); - it('highlights pending new thread range from editor state', () => { - let setSelectionRef; - function SelectionCapture({children}) { - const {select} = useEditorSelection(); - useEffect(() => { setSelectionRef = select; }, [select]); - return children; - } + it('highlights pending new thread range from editor state', async () => { + fakeParentWindow(); const entry = renderEntry({ contentElement: { - ui: , + ui: , typeOptions: {inlineComments: true} } }); @@ -69,31 +64,33 @@ describe('inline editing EditableText comment highlights', () => { expect(entry.container.querySelector(`.${highlightStyles.highlight}`)) .not.toBeInTheDocument(); - act(() => setSelectionRef({ - type: 'newThread', - id: 10, - subjectType: 'ContentElement', - range: subjectRange - })); + 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).toBeInTheDocument(); expect(highlight).toHaveTextContent('text'); expect(highlight).toHaveClass(highlightStyles.selected); }); - 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; - } - + it('applies selected style to highlight when thread badge is clicked', () => { const entry = renderEntry({ contentElement: { - ui: , - typeOptions: {inlineComments: true} + ui: , + typeOptions: {inlineComments: true, customSelectionRect: true} }, commenting: { currentUser: null, @@ -110,9 +107,7 @@ describe('inline editing EditableText comment highlights', () => { expect(entry.container.querySelector(`.${highlightStyles.highlight}`)) .not.toHaveClass(highlightStyles.selected); - act(() => setSelectionRef({ - type: 'contentElementComments', id: 1, highlightedThreadId: 5 - })); + 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 index 3657840147..d6037eb00f 100644 --- 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 @@ -5,7 +5,7 @@ import {EditableText} from 'frontend'; import {renderEntry, useInlineEditingPageObjects} from 'support/pageObjects/inlineEditing'; import {fakeParentWindow} from 'support'; -import {act, fireEvent} from '@testing-library/react'; +import {act} from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; describe('inline editing EditableText comment selection messages', () => { @@ -46,7 +46,7 @@ describe('inline editing EditableText comment selection messages', () => { } }); - fireEvent.click(entry.queryAllCommentBadges()[0].el); + entry.queryAllCommentBadges()[0].select(); expect(window.parent.postMessage).toHaveBeenCalledWith({ type: 'SELECTED', diff --git a/entry_types/scrolled/package/spec/support/pageObjects/inlineEditing.js b/entry_types/scrolled/package/spec/support/pageObjects/inlineEditing.js index 5831acacf8..30f42585c3 100644 --- a/entry_types/scrolled/package/spec/support/pageObjects/inlineEditing.js +++ b/entry_types/scrolled/package/spec/support/pageObjects/inlineEditing.js @@ -1,4 +1,4 @@ -import {act} from '@testing-library/react'; +import {act, fireEvent} from '@testing-library/react'; import {useFakeTranslations} from 'pageflow/testHelpers'; import {loadInlineEditingComponents} from 'frontend/inlineEditing'; @@ -56,6 +56,7 @@ function createCommentBadgePageObject(el) { return { el, isInDotMode: () => el.classList.contains(badgeStyles.dot), - isActive: () => el.classList.contains(badgeStyles.active) + isActive: () => el.classList.contains(badgeStyles.active), + select: () => fireEvent.click(el) }; } From 8825f994af3974c83f77303858722c094ab0084c Mon Sep 17 00:00:00 2001 From: Tim Fischbach Date: Thu, 21 May 2026 17:16:36 +0200 Subject: [PATCH 4/4] Globalize window.getSelection stub for slate jsdom's Selection implementation is incomplete for slate-react's usage. Move the workaround stub (which returns undefined so slate falls back to its internal selection state) from per-suite beforeAll hooks in seven inline-editing specs into setupFilesAfterEnv, so the stub is in place by default and slate-using specs no longer need to remember it. --- entry_types/scrolled/package/jest.config.js | 1 + .../spec/frontend/inlineEditing/EditableInlineText-spec.js | 2 -- .../EditableText/features/commentBadges-spec.js | 2 -- .../EditableText/features/commentHighlights-spec.js | 2 -- .../EditableText/features/commentSelection-spec.js | 2 -- .../inlineEditing/EditableText/features/placeholder-spec.js | 2 -- .../inlineEditing/EditableText/features/rendering-spec.js | 2 -- .../inlineEditing/EditableText/features/typography-spec.js | 2 -- .../scrolled/package/spec/support/getSelectionStub.js | 6 ++++++ 9 files changed, 7 insertions(+), 14 deletions(-) create mode 100644 entry_types/scrolled/package/spec/support/getSelectionStub.js 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/features/commentBadges-spec.js b/entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText/features/commentBadges-spec.js index 183a74f24b..17a63940f3 100644 --- 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 @@ -9,8 +9,6 @@ import '@testing-library/jest-dom/extend-expect'; describe('inline editing EditableText comment badges', () => { useInlineEditingPageObjects(); - beforeAll(() => window.getSelection = function() {}); - beforeEach(() => { jest.spyOn(features, 'isEnabled').mockImplementation( name => name === 'commenting' 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 index cfc5133ef0..8419614e0c 100644 --- 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 @@ -13,8 +13,6 @@ import {commentHighlightStyles as highlightStyles} from 'pageflow-scrolled/revie describe('inline editing EditableText comment highlights', () => { useInlineEditingPageObjects(); - beforeAll(() => window.getSelection = function() {}); - beforeEach(() => { jest.spyOn(features, 'isEnabled').mockImplementation( name => name === 'commenting' 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 index d6037eb00f..5e479f6d48 100644 --- 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 @@ -11,8 +11,6 @@ import '@testing-library/jest-dom/extend-expect'; describe('inline editing EditableText comment selection messages', () => { useInlineEditingPageObjects(); - beforeAll(() => window.getSelection = function() {}); - beforeEach(() => { jest.spyOn(features, 'isEnabled').mockImplementation( name => name === 'commenting' 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 index ef3d82f2a0..4241478e8e 100644 --- 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 @@ -8,8 +8,6 @@ import '@testing-library/jest-dom/extend-expect'; describe('inline editing EditableText placeholder', () => { useInlineEditingPageObjects(); - beforeAll(() => window.getSelection = function() {}); - const headingValue = [{ type: 'heading', children: [{text: 'Some text'}] 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 index 1ca794a4a2..a3c14ab447 100644 --- 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 @@ -8,8 +8,6 @@ import '@testing-library/jest-dom/extend-expect'; describe('inline editing EditableText rendering', () => { useInlineEditingPageObjects(); - beforeAll(() => window.getSelection = function() {}); - it('renders text from value', () => { const value = [{ type: 'heading', 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 index f21f233aed..776356ffe7 100644 --- 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 @@ -8,8 +8,6 @@ import '@testing-library/jest-dom/extend-expect'; describe('inline editing EditableText typography', () => { useInlineEditingPageObjects(); - beforeAll(() => window.getSelection = function() {}); - const paragraphValue = [{ type: 'paragraph', children: [{text: 'Some text'}] 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; +});