diff --git a/jest.config.js b/jest.config.js index 9af8ba26..33cac75a 100644 --- a/jest.config.js +++ b/jest.config.js @@ -6,6 +6,7 @@ module.exports = { 'netlify-cms-ui-default': '/packages/netlify-cms-ui-default/src/index.js', 'netlify-cms-backend-github': '/packages/netlify-cms-backend-github/src/index.ts', 'netlify-cms-lib-widgets': '/packages/netlify-cms-lib-widgets/src/index.ts', + 'netlify-cms-widget-object': '/packages/netlify-cms-widget-object/src/index.js', }, testURL: 'http://localhost:8080', }; diff --git a/packages/netlify-cms-ui-default/src/ObjectWidgetTopBar.js b/packages/netlify-cms-ui-default/src/ObjectWidgetTopBar.js index 0c27a9da..5b8d025c 100644 --- a/packages/netlify-cms-ui-default/src/ObjectWidgetTopBar.js +++ b/packages/netlify-cms-ui-default/src/ObjectWidgetTopBar.js @@ -109,7 +109,7 @@ class ObjectWidgetTopBar extends React.Component { return ( - + {heading} diff --git a/packages/netlify-cms-widget-list/src/ListControl.js b/packages/netlify-cms-widget-list/src/ListControl.js index d345d825..ebcbe7f5 100644 --- a/packages/netlify-cms-widget-list/src/ListControl.js +++ b/packages/netlify-cms-widget-list/src/ListControl.js @@ -388,6 +388,7 @@ export default class ListControl extends React.Component { onCollapseToggle={partial(this.handleItemCollapseToggle, index)} onRemove={partial(this.handleRemove, index, key)} dragHandleHOC={SortableHandle} + data-testid={`styled-list-item-top-bar-${key}`} /> {this.objectLabel(item)} @@ -412,6 +413,7 @@ export default class ListControl extends React.Component { controlRef={controlRef} validationKey={key} collapsed={collapsed} + data-testid={`object-control-${key}`} /> )} diff --git a/packages/netlify-cms-widget-list/src/__tests__/ListControl.spec.js b/packages/netlify-cms-widget-list/src/__tests__/ListControl.spec.js new file mode 100644 index 00000000..606a5c19 --- /dev/null +++ b/packages/netlify-cms-widget-list/src/__tests__/ListControl.spec.js @@ -0,0 +1,273 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import { fromJS } from 'immutable'; +import ListControl from '../ListControl'; + +jest.mock('netlify-cms-widget-object', () => { + const React = require('react'); + + class MockObjectControl extends React.Component { + render() { + return {this.props.children}; + } + } + + return { + controlComponent: MockObjectControl, + }; +}); +jest.mock('netlify-cms-ui-default', () => { + const actual = jest.requireActual('netlify-cms-ui-default'); + const ListItemTopBar = props => ( + + {props.children} + + ); + return { + ...actual, + ListItemTopBar, + }; +}); +jest.mock('uuid/v4'); + +describe('ListControl', () => { + const props = { + onChange: jest.fn(), + onChangeObject: jest.fn(), + onValidateObject: jest.fn(), + validate: jest.fn(), + mediaPaths: fromJS({}), + getAsset: jest.fn(), + onOpenMediaLibrary: jest.fn(), + onAddAsset: jest.fn(), + onRemoveInsertedMedia: jest.fn(), + classNameWrapper: 'classNameWrapper', + setActiveStyle: jest.fn(), + setInactiveStyle: jest.fn(), + editorControl: jest.fn(), + resolveWidget: jest.fn(), + clearFieldErrors: jest.fn(), + fieldsErrors: fromJS({}), + }; + + beforeEach(() => { + jest.clearAllMocks(); + const uuid = require('uuid/v4'); + let id = 0; + uuid.mockImplementation(() => { + return id++; + }); + }); + it('should render empty list', () => { + const field = fromJS({ name: 'list', label: 'List' }); + const { asFragment } = render(); + + expect(asFragment()).toMatchSnapshot(); + }); + + it('should render list with string array', () => { + const field = fromJS({ name: 'list', label: 'List' }); + const { asFragment } = render( + , + ); + + expect(asFragment()).toMatchSnapshot(); + }); + + it('should render list with nested object', () => { + const field = fromJS({ + name: 'list', + label: 'List', + field: { + name: 'object', + widget: 'object', + label: 'Object', + fields: [{ name: 'title', widget: 'string', label: 'Title' }], + }, + }); + const { asFragment, getByTestId } = render( + , + ); + + expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed', 'true'); + expect(getByTestId('styled-list-item-top-bar-1')).toHaveAttribute('collapsed', 'true'); + + expect(getByTestId('object-control-0')).toHaveAttribute('collapsed', 'true'); + expect(getByTestId('object-control-1')).toHaveAttribute('collapsed', 'true'); + + expect(asFragment()).toMatchSnapshot(); + }); + + it('should render list with nested object with collapse = false', () => { + const field = fromJS({ + name: 'list', + label: 'List', + collapsed: false, + field: { + name: 'object', + widget: 'object', + label: 'Object', + fields: [{ name: 'title', widget: 'string', label: 'Title' }], + }, + }); + const { asFragment, getByTestId } = render( + , + ); + + expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed', 'false'); + expect(getByTestId('styled-list-item-top-bar-1')).toHaveAttribute('collapsed', 'false'); + + expect(getByTestId('object-control-0')).toHaveAttribute('collapsed', 'false'); + expect(getByTestId('object-control-1')).toHaveAttribute('collapsed', 'false'); + + expect(asFragment()).toMatchSnapshot(); + }); + + it('should collapse all items on top bar collapse click', () => { + const field = fromJS({ + name: 'list', + label: 'List', + collapsed: false, + field: { + name: 'object', + widget: 'object', + label: 'Object', + fields: [{ name: 'title', widget: 'string', label: 'Title' }], + }, + }); + const { getByTestId } = render( + , + ); + + expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed', 'false'); + expect(getByTestId('styled-list-item-top-bar-1')).toHaveAttribute('collapsed', 'false'); + + expect(getByTestId('object-control-0')).toHaveAttribute('collapsed', 'false'); + expect(getByTestId('object-control-1')).toHaveAttribute('collapsed', 'false'); + + fireEvent.click(getByTestId('expand-button')); + + expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed', 'true'); + expect(getByTestId('styled-list-item-top-bar-1')).toHaveAttribute('collapsed', 'true'); + + expect(getByTestId('object-control-0')).toHaveAttribute('collapsed', 'true'); + expect(getByTestId('object-control-1')).toHaveAttribute('collapsed', 'true'); + }); + + it('should collapse a single item on collapse item click', () => { + const field = fromJS({ + name: 'list', + label: 'List', + collapsed: false, + field: { + name: 'object', + widget: 'object', + label: 'Object', + fields: [{ name: 'title', widget: 'string', label: 'Title' }], + }, + }); + const { getByTestId } = render( + , + ); + + expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed', 'false'); + expect(getByTestId('styled-list-item-top-bar-1')).toHaveAttribute('collapsed', 'false'); + + expect(getByTestId('object-control-0')).toHaveAttribute('collapsed', 'false'); + expect(getByTestId('object-control-1')).toHaveAttribute('collapsed', 'false'); + + fireEvent.click(getByTestId('styled-list-item-top-bar-0')); + + expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed', 'true'); + expect(getByTestId('styled-list-item-top-bar-1')).toHaveAttribute('collapsed', 'false'); + + expect(getByTestId('object-control-0')).toHaveAttribute('collapsed', 'true'); + expect(getByTestId('object-control-1')).toHaveAttribute('collapsed', 'false'); + }); + + it('should expand all items on top bar expand click', () => { + const field = fromJS({ + name: 'list', + label: 'List', + collapsed: true, + field: { + name: 'object', + widget: 'object', + label: 'Object', + fields: [{ name: 'title', widget: 'string', label: 'Title' }], + }, + }); + const { getByTestId } = render( + , + ); + + expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed', 'true'); + expect(getByTestId('styled-list-item-top-bar-1')).toHaveAttribute('collapsed', 'true'); + + expect(getByTestId('object-control-0')).toHaveAttribute('collapsed', 'true'); + expect(getByTestId('object-control-1')).toHaveAttribute('collapsed', 'true'); + + fireEvent.click(getByTestId('expand-button')); + + expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed', 'false'); + expect(getByTestId('styled-list-item-top-bar-1')).toHaveAttribute('collapsed', 'false'); + + expect(getByTestId('object-control-0')).toHaveAttribute('collapsed', 'false'); + expect(getByTestId('object-control-1')).toHaveAttribute('collapsed', 'false'); + }); + + it('should expand a single item on expand item click', () => { + const field = fromJS({ + name: 'list', + label: 'List', + collapsed: true, + field: { + name: 'object', + widget: 'object', + label: 'Object', + fields: [{ name: 'title', widget: 'string', label: 'Title' }], + }, + }); + const { getByTestId } = render( + , + ); + + expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed', 'true'); + expect(getByTestId('styled-list-item-top-bar-1')).toHaveAttribute('collapsed', 'true'); + + expect(getByTestId('object-control-0')).toHaveAttribute('collapsed', 'true'); + expect(getByTestId('object-control-1')).toHaveAttribute('collapsed', 'true'); + + fireEvent.click(getByTestId('styled-list-item-top-bar-0')); + + expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed', 'false'); + expect(getByTestId('styled-list-item-top-bar-1')).toHaveAttribute('collapsed', 'true'); + + expect(getByTestId('object-control-0')).toHaveAttribute('collapsed', 'false'); + expect(getByTestId('object-control-1')).toHaveAttribute('collapsed', 'true'); + }); +}); diff --git a/packages/netlify-cms-widget-list/src/__tests__/__snapshots__/ListControl.spec.js.snap b/packages/netlify-cms-widget-list/src/__tests__/__snapshots__/ListControl.spec.js.snap new file mode 100644 index 00000000..167e1099 --- /dev/null +++ b/packages/netlify-cms-widget-list/src/__tests__/__snapshots__/ListControl.spec.js.snap @@ -0,0 +1,516 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ListControl should render empty list 1`] = ` + + + +`; + +exports[`ListControl should render list with nested object 1`] = ` + + .emotion-22 { + padding: 0 14px 14px; +} + +.emotion-12 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background-color: #dfdfe3; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + margin: 0 -14px; + padding: 13px; +} + +.emotion-5 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-size: 14px; + font-weight: 500; + line-height: 1; +} + +.emotion-3 { + border: 0; + border-radius: 5px; + cursor: pointer; + padding: 4px; + background-color: transparent; + color: inherit; +} + +.emotion-3:last-of-type { + margin-right: 4px; +} + +.emotion-1 { + display: inline-block; + line-height: 0; + width: 18px; + height: 18px; + -webkit-transform: rotate(-90deg); + -ms-transform: rotate(-90deg); + transform: rotate(-90deg); +} + +.emotion-1 path:not(.no-fill), +.emotion-1 circle:not(.no-fill), +.emotion-1 polygon:not(.no-fill), +.emotion-1 rect:not(.no-fill) { + fill: currentColor; +} + +.emotion-1 path.clipped { + fill: transparent; +} + +.emotion-1 svg { + width: 100%; + height: 100%; +} + +.emotion-10 { + border: 0; + border-radius: 5px; + cursor: pointer; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + padding: 2px 12px; + font-size: 12px; + font-weight: bold; + border-radius: 3px; +} + +.emotion-10 .emotion-0 { + margin-left: 6px; +} + +.emotion-8 { + display: inline-block; + line-height: 0; + width: 12px; + height: 12px; + -webkit-transform: rotate(0deg); + -ms-transform: rotate(0deg); + transform: rotate(0deg); +} + +.emotion-8 path:not(.no-fill), +.emotion-8 circle:not(.no-fill), +.emotion-8 polygon:not(.no-fill), +.emotion-8 rect:not(.no-fill) { + fill: currentColor; +} + +.emotion-8 path.clipped { + fill: transparent; +} + +.emotion-8 svg { + width: 100%; + height: 100%; +} + +.emotion-16 { + margin-top: 18px; + padding-bottom: 0; +} + +.emotion-16:first-of-type { + margin-top: 26px; +} + +.emotion-14 { + display: block; + border-top: 0; + color: inherit; + background-color: #dfdfe3; + padding: 13px; + border-radius: 0 0 5px 5px; +} + +
+
+
+ + 2 list +
+ +
+
+
+ +
+ Object +
+ +
+
+ +
+ Object +
+ +
+
+
+
+`; + +exports[`ListControl should render list with nested object with collapse = false 1`] = ` + + .emotion-22 { + padding: 0 14px 14px; +} + +.emotion-12 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background-color: #dfdfe3; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + margin: 0 -14px; + padding: 13px; +} + +.emotion-5 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-size: 14px; + font-weight: 500; + line-height: 1; +} + +.emotion-3 { + border: 0; + border-radius: 5px; + cursor: pointer; + padding: 4px; + background-color: transparent; + color: inherit; +} + +.emotion-3:last-of-type { + margin-right: 4px; +} + +.emotion-10 { + border: 0; + border-radius: 5px; + cursor: pointer; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + padding: 2px 12px; + font-size: 12px; + font-weight: bold; + border-radius: 3px; +} + +.emotion-10 .emotion-0 { + margin-left: 6px; +} + +.emotion-8 { + display: inline-block; + line-height: 0; + width: 12px; + height: 12px; + -webkit-transform: rotate(0deg); + -ms-transform: rotate(0deg); + transform: rotate(0deg); +} + +.emotion-8 path:not(.no-fill), +.emotion-8 circle:not(.no-fill), +.emotion-8 polygon:not(.no-fill), +.emotion-8 rect:not(.no-fill) { + fill: currentColor; +} + +.emotion-8 path.clipped { + fill: transparent; +} + +.emotion-8 svg { + width: 100%; + height: 100%; +} + +.emotion-1 { + display: inline-block; + line-height: 0; + width: 18px; + height: 18px; + -webkit-transform: rotate(0deg); + -ms-transform: rotate(0deg); + transform: rotate(0deg); +} + +.emotion-1 path:not(.no-fill), +.emotion-1 circle:not(.no-fill), +.emotion-1 polygon:not(.no-fill), +.emotion-1 rect:not(.no-fill) { + fill: currentColor; +} + +.emotion-1 path.clipped { + fill: transparent; +} + +.emotion-1 svg { + width: 100%; + height: 100%; +} + +.emotion-16 { + margin-top: 18px; +} + +.emotion-16:first-of-type { + margin-top: 26px; +} + +.emotion-14 { + display: none; + border-top: 0; + color: inherit; + background-color: #dfdfe3; + padding: 13px; + border-radius: 0 0 5px 5px; +} + +
+
+
+ + 2 list +
+ +
+
+
+ +
+ Object +
+ +
+
+ +
+ Object +
+ +
+
+
+
+`; + +exports[`ListControl should render list with string array 1`] = ` + + + +`;