diff --git a/cypress/e2e/01-block-group.cy.js b/cypress/e2e/01-block-group.cy.js
index 7b578c9..48409f2 100644
--- a/cypress/e2e/01-block-group.cy.js
+++ b/cypress/e2e/01-block-group.cy.js
@@ -20,6 +20,8 @@ describe('Blocks Tests', () => {
.contains('Section (Group)')
.click({ force: true });
+ cy.contains('Block').click();
+
cy.get('.block-editor-group [contenteditable=true]')
.focus()
.click()
@@ -49,4 +51,86 @@ describe('Blocks Tests', () => {
cy.contains('My Add-on Page');
cy.contains('test2');
});
+
+ it('Add Block: Make content type and add group block to layout', () => {
+ cy.clearSlateTitle();
+ cy.getSlateTitle().type('My Add-on Page');
+
+ cy.get('.documentFirstHeading').contains('My Add-on Page');
+
+ cy.get('#toolbar-save').click();
+ cy.get('.user').click();
+ cy.get('a[href="/controlpanel"]').click();
+ cy.get('a[href="/controlpanel/dexterity-types"]').click();
+
+ // add the content type
+ cy.get('#toolbar-add').click();
+ cy.get('#field-title').click().type('Test Content Type');
+ cy.get('.actions button[aria-label="Save"]').click();
+
+ // change the layout
+ cy.get('.ui.dropdown.actions-test_content_type').click();
+ cy.get('.item.layout-test_content_type').click();
+ cy.contains('Enable editable Blocks').click();
+ cy.getSlate().click();
+
+ // Add block
+ cy.get('.ui.basic.icon.button.block-add-button').first().click();
+ cy.get('.blocks-chooser .title').contains('Common').click();
+ cy.get('.content.active.common .button.group')
+ .contains('Section (Group)')
+ .click({ force: true });
+ cy.contains('Section').click();
+
+ cy.get('.sidebar-container #field-placeholder')
+ .click()
+ .type('Test Helper Text');
+ cy.get(
+ '.sidebar-container .field-wrapper-instructions div[role="textbox"] p',
+ )
+ .click()
+ .type('Description Blocks');
+ cy.get(
+ '.sidebar-container .field-wrapper-allowedBlocks .react-select__value-container',
+ ).click();
+ cy.get('.react-select__option')
+ .contains('Description')
+ .click({ force: true });
+ cy.get('.field-wrapper-ignoreSpaces input').click({ force: true });
+ cy.get('.field-wrapper-required input').click({ force: true });
+
+ cy.get('#toolbar-save').click();
+ cy.get('.ui.button.cancel').click();
+ cy.get('a[href="/controlpanel').click();
+ cy.contains('Home').click();
+ cy.get('#toolbar-add').click();
+ cy.get('#toolbar-add-test_content_type').click();
+ cy.get('#field-title').click().type('Test Content Type');
+ cy.get('.block-editor-group div[role="textbox"]')
+ .click()
+ .type('/description{enter}');
+ cy.get('.block-editor-group .block-editor-slate').click();
+ cy.get(
+ '.block-editor-slate .block-toolbar button[title="Add block"]',
+ ).click();
+ cy.get('.blocks-chooser .field.searchbox div.ui.transparent.input input')
+ .click()
+ .focus()
+ .type('Description{enter}');
+ cy.get(
+ '.blocks-chooser .accordion div[aria-label="Unfold Text blocks"]',
+ ).click();
+ cy.get('.ui.basic.icon.button.description').click();
+ cy.get('button[title="Remove block"]').click();
+
+ // delete the content type
+ cy.get('#toolbar-save').click();
+ cy.get('.user').click();
+ cy.get('a[href="/controlpanel"]').click();
+ cy.get('a[href="/controlpanel/dexterity-types"]').click();
+
+ cy.get('.ui.dropdown.actions-test_content_type').click();
+ cy.get('.item.delete-test_content_type').click();
+ cy.get('button.ui.primary.button').should('contain', 'Yes').click();
+ });
});
diff --git a/src/components/manage/Blocks/Group/CounterComponent.test.jsx b/src/components/manage/Blocks/Group/CounterComponent.test.jsx
new file mode 100644
index 0000000..3c34a86
--- /dev/null
+++ b/src/components/manage/Blocks/Group/CounterComponent.test.jsx
@@ -0,0 +1,234 @@
+import React from 'react';
+import { render } from '@testing-library/react';
+import CounterComponent from './CounterComponent';
+import '@testing-library/jest-dom/extend-expect';
+
+jest.mock('@plone/volto/registry', () => ({
+ blocks: {
+ blocksConfig: {
+ group: {
+ countTextIn: ['text'],
+ },
+ },
+ },
+}));
+
+jest.mock('@plone/volto-slate/editor/render', () => ({
+ serializeNodesToText: jest.fn((nodes) =>
+ nodes.map((node) => node.text).join(' '),
+ ),
+}));
+
+describe('CounterComponent', () => {
+ const setSidebarTab = jest.fn();
+ const setSelectedBlock = jest.fn();
+
+ it('should render info class when character count is less than 95% of maxChars', () => {
+ const { container, getByText } = render(
+ ,
+ );
+ expect(getByText('96 characters remaining out of 100')).toBeInTheDocument();
+ expect(container.querySelector('.counter.info')).toBeInTheDocument();
+ });
+
+ it('should render warning class when character count is between 95% and 100% of maxChars', () => {
+ const { container, getByText } = render(
+ ,
+ );
+ expect(getByText('4 characters remaining out of 100')).toBeInTheDocument();
+ expect(container.querySelector('.counter.warning')).toBeInTheDocument();
+ });
+
+ it('should render warning class when character count is over the maxChars', () => {
+ const { container, getByText } = render(
+ ,
+ );
+ expect(getByText('4 characters over the limit')).toBeInTheDocument();
+ expect(container.querySelector('.counter.danger')).toBeInTheDocument();
+ });
+
+ it('should handle click event', () => {
+ const { container } = render(
+ ,
+ );
+ container.querySelector('.counter').click();
+ expect(setSidebarTab).toHaveBeenCalledWith(1);
+ expect(setSelectedBlock).toHaveBeenCalled();
+ });
+
+ it('should handle click event with maxChar undefined', () => {
+ const { container } = render(
+ ,
+ );
+ container.querySelector('.counter').click();
+ expect(setSidebarTab).toHaveBeenCalledWith(1);
+ expect(setSelectedBlock).toHaveBeenCalled();
+ });
+
+ it('should handle click event with data undefined', () => {
+ const { container } = render(
+ ,
+ );
+ container.querySelector('.counter').click();
+ expect(setSidebarTab).toHaveBeenCalledWith(1);
+ expect(setSelectedBlock).toHaveBeenCalled();
+ });
+
+ it('should handle click event with plaintext undefined, but values present', () => {
+ const { container } = render(
+ ,
+ );
+ container.querySelector('.counter').click();
+ expect(setSidebarTab).toHaveBeenCalledWith(1);
+ expect(setSelectedBlock).toHaveBeenCalled();
+ });
+
+ it('should handle click event with plaintext undefined and values is not an array and the type is in countTextIn', () => {
+ const { container } = render(
+ ,
+ );
+ container.querySelector('.counter').click();
+ expect(setSidebarTab).toHaveBeenCalledWith(1);
+ expect(setSelectedBlock).toHaveBeenCalled();
+ });
+
+ it('should handle click event with plaintext undefined and values is not an array and the type is not in countTextIn', () => {
+ const { container } = render(
+ ,
+ );
+ container.querySelector('.counter').click();
+ expect(setSidebarTab).toHaveBeenCalledWith(1);
+ expect(setSelectedBlock).toHaveBeenCalled();
+ });
+});
diff --git a/src/components/manage/Blocks/Group/Edit.test.jsx b/src/components/manage/Blocks/Group/Edit.test.jsx
index 48da9de..6c772f2 100644
--- a/src/components/manage/Blocks/Group/Edit.test.jsx
+++ b/src/components/manage/Blocks/Group/Edit.test.jsx
@@ -76,4 +76,111 @@ describe('Edit', () => {
);
fireEvent.keyDown(getByRole('presentation'), { key: 'ArrowUp', code: 38 });
});
+
+ it('should call ArrowUp keydown', () => {
+ const props = {
+ block: 'testBlock',
+ data: {
+ instructions: 'test',
+ data: {
+ blocks: {
+ block1: {
+ type: 'test',
+ data: {
+ value: 'Test',
+ },
+ },
+ },
+ blocks_layout: {
+ items: [undefined],
+ },
+ },
+ },
+ onChangeBlock,
+ onChangeField,
+ pathname: '/',
+ selected: true,
+ manage: true,
+ };
+ const mockOnFocusPreviousBlock = jest.fn();
+ const mockOnFocusNextBlock = jest.fn();
+ const mockOnAddBlock = jest.fn();
+ const mockSidebarTab = jest.fn();
+
+ const { container } = render(
+
+
+ ,
+ );
+
+ fireEvent.keyDown(container.querySelector('.section-block'), {
+ key: 'ArrowUp',
+ code: 38,
+ });
+ fireEvent.keyDown(container.querySelector('.section-block'), {
+ key: 'ArrowDown',
+ code: 40,
+ });
+ fireEvent.keyDown(container.querySelector('.section-block'), {
+ key: 'Enter',
+ code: 13,
+ });
+
+ fireEvent.click(container.querySelector('.blocks-form'), {
+ shiftKey: true,
+ });
+ fireEvent.click(container.querySelector('.section-block legend'));
+ });
+
+ it('should call ArrowUp keydown', () => {
+ const props = {
+ block: 'testBlock',
+ data: {
+ instructions: 'test',
+ data: {
+ blocks: {
+ block1: {
+ type: 'test',
+ data: {
+ value: 'Test',
+ },
+ },
+ },
+ blocks_layout: {
+ items: [undefined],
+ },
+ },
+ },
+ onChangeBlock,
+ onChangeField,
+ pathname: '/',
+ selected: true,
+ manage: true,
+ };
+ const mockOnFocusPreviousBlock = jest.fn();
+ const mockOnFocusNextBlock = jest.fn();
+ const mockOnAddBlock = jest.fn();
+ const mockSidebarTab = jest.fn();
+ const { container } = render(
+
+
+ ,
+ );
+
+ fireEvent.click(container.querySelector('.section-block legend'));
+ });
});
diff --git a/src/index.test.js b/src/index.test.js
new file mode 100644
index 0000000..fff5824
--- /dev/null
+++ b/src/index.test.js
@@ -0,0 +1,101 @@
+import applyConfig from './index';
+
+describe('applyConfig', () => {
+ it('should add group block configuration', () => {
+ const config = {
+ blocks: {
+ blocksConfig: {
+ text: { title: 'Text', restricted: false },
+ image: { title: 'Image', restricted: true },
+ },
+ },
+ };
+
+ const newConfig = applyConfig(config);
+
+ expect(newConfig.blocks.blocksConfig.group).toBeDefined();
+ expect(newConfig.blocks.blocksConfig.group.id).toEqual('group');
+ expect(newConfig.blocks.blocksConfig.group.title).toEqual(
+ 'Section (Group)',
+ );
+ expect(newConfig.blocks.blocksConfig.group.icon).toBeDefined();
+ expect(newConfig.blocks.blocksConfig.group.view).toBeDefined();
+ expect(newConfig.blocks.blocksConfig.group.edit).toBeDefined();
+ expect(newConfig.blocks.blocksConfig.group.schema).toBeDefined();
+ expect(newConfig.blocks.blocksConfig.group.restricted).toEqual(false);
+ });
+
+ it('should include allowed blocks in schema', () => {
+ const config = {
+ blocks: {
+ blocksConfig: {
+ text: { title: 'Text', restricted: false },
+ image: { restricted: false },
+ image_test: { title: 'Image', restricted: true },
+ },
+ },
+ };
+
+ const newConfig = applyConfig(config);
+
+ expect(
+ newConfig.blocks.blocksConfig.group.schema.properties.allowedBlocks.items
+ .choices,
+ ).toEqual([
+ ['text', 'Text'],
+ ['image', 'image'],
+ ['group', 'Group'],
+ ]);
+ });
+
+ it('should generate tocEntries correctly', () => {
+ const config = {
+ blocks: {
+ blocksConfig: {},
+ },
+ };
+
+ const block = {
+ data: {
+ blocks: {
+ block1: { value: [{ type: 'h1' }], plaintext: 'Heading 1' },
+ block2: { value: [{ type: 'h2' }], plaintext: 'Heading 2' },
+ block3: { value: [{ type: 'h3' }], plaintext: 'Heading 3' }, // This should be ignored
+ },
+ blocks_layout: {
+ items: ['block1', 'block2', 'block3'],
+ },
+ },
+ };
+ const tocData = {
+ levels: ['h1', 'h2'],
+ };
+ const newConfig = applyConfig(config);
+ const entries = newConfig.blocks.blocksConfig.group.tocEntries(
+ block,
+ tocData,
+ );
+ expect(entries).toEqual([
+ [1, 'Heading 1', 'block1'],
+ [2, 'Heading 2', 'block2'],
+ ]);
+ });
+
+ it('should generate no entries', () => {
+ const config = {
+ blocks: {
+ blocksConfig: {},
+ },
+ };
+ const block = undefined;
+ const tocData = {
+ levels: undefined,
+ };
+ const newConfig = applyConfig(config);
+ const entries = newConfig.blocks.blocksConfig.group.tocEntries(
+ block,
+ tocData,
+ );
+ expect(entries).toEqual([]);
+ });
+});