diff --git a/public/css/perspective.css b/public/css/perspective.css index 137ae5e430..5fbf1cd3e5 100644 --- a/public/css/perspective.css +++ b/public/css/perspective.css @@ -87,10 +87,6 @@ body { margin: -5px 0 0 0; } -.slds-lookup__item-action.slds-media.highlighted { - background: #e0e5ee; -} - /* radio button width */ .slds-modal .slds-grid { @@ -127,10 +123,6 @@ body { padding-top: 100; } -.slds-dropdown__item { - overflow: scroll; -} - .slds-dropdown { overflow: scroll; margin-left: 5px; diff --git a/tests/view/components/createPerspective.js b/tests/view/components/createPerspective.js index 73aba941e6..a4d51340ac 100644 --- a/tests/view/components/createPerspective.js +++ b/tests/view/components/createPerspective.js @@ -13,14 +13,17 @@ import { expect } from 'chai'; import React from 'react'; import sinon from 'sinon'; -import CreatePerspective from '../../../view/perspective/CreatePerspective.js'; +import { getSubjects } from './utils'; +import CreatePerspective from '../../../view/perspective/CreatePerspective'; import { mount } from 'enzyme'; describe('Perspective view ', () => { const ZERO = 0; const ONE = 1; const TWO = 2; + const EMPTY_STR = ''; const DUMMY_STRING = 'COOL'; + const DUMMY_ID = '743bcf42-cd79-46d0-8c0f-d43adbb63866'; const DUMMY_FUNCTION = () => {}; const ONE_SUBJECT = { absolutePath: DUMMY_STRING, @@ -28,6 +31,12 @@ describe('Perspective view ', () => { }; const DUMMY_ARRAY = 'qwertyui'.split(''); const { getDropdownStyle } = CreatePerspective; + const LENS = { + id: DUMMY_ID, + name: DUMMY_STRING, + isPublished: true, + }; + const PERS_NAME = DUMMY_STRING; /** * Sets up the component with dummy prop values. @@ -37,90 +46,172 @@ describe('Perspective view ', () => { * overrides to the default props * @returns {Object} The rendered component */ - function setup(valuesAddons, stateAddons) { + function setup(valuesAddons, otherPropsObj) { // simulate loading config const defaultProps = { + name: PERS_NAME, + params: {}, cancelCreate: DUMMY_FUNCTION, - sendResource: DUMMY_FUNCTION, + isEditing: false, + sendResource: spy, + // options or all possible values values: { - aspectFilter: [], - aspectTags: [], - lenses: [], - name: DUMMY_STRING, - perspectives: [], - statusFilter: [], + aspectTagFilter: [], subjectTagFilter: [], - subjects: [], + lenses: [LENS], + // actual values + perspectives: [{ + name: PERS_NAME, + lens: LENS, + rootSubject: DUMMY_STRING, + aspectFilterType: "EXCLUDE", + aspectFilter: [ ], + aspectTagFilterType: "EXCLUDE", + aspectTagFilter: [ ], + subjectTagFilterType: "EXCLUDE", + subjectTagFilter: [ ], // empty for testing + statusFilterType: "EXCLUDE", + statusFilter: DUMMY_ARRAY, // not empty for testing + }], }, - stateObject: { - perspectives: [], - subjects: [], - lenses: [], - statusFilterType: '', - statusFilter: [], - subjectTagFilter: [], - subjectTagFilterType: '', - aspectTagFilter: [], - aspectTagFilterType: '', - aspectFilter: [], - aspectFilterType: '', - } }; // update defaultProps as needed if (valuesAddons) { Object.assign(defaultProps.values, valuesAddons); } - if (stateAddons) { - Object.assign(defaultProps.stateAddons, stateAddons); + if (otherPropsObj) { + Object.assign(defaultProps, otherPropsObj) } + // use monut to test all lifecycle methods, and children const enzymeWrapper = mount(); return enzymeWrapper; } let stub; + let spy; beforeEach(() => { stub = sinon.stub(CreatePerspective, 'findCommonAncestor'); + spy = sinon.spy(); }); afterEach(() => { CreatePerspective.findCommonAncestor.restore(); }); - it('given the proper url parameter and resource, ' + - ' create modal is shown with proper resource name', () => { - const enzymeWrapper = setup(); - expect(enzymeWrapper.find('.slds-modal__container')).to.have.length(ONE); - expect(enzymeWrapper.find('.slds-text-heading--medium').text()) - .to.equal('New Perspective'); + describe('after setting props isEditing to true', () => { + it('sendResource first argument is PUT', () => { + const enzymeWrapper = setup(null, { isEditing: true }); + const instance = enzymeWrapper.instance(); + instance.doCreate(); + expect(spy.calledOnce).to.be.true; + // expect method to be PUT + expect(spy.args[0][0]).to.equal('PUT'); + }); + + it('sendResource form object argument has field url defined, ' + + 'does not end with perspectives', () => { + const enzymeWrapper = setup(null, { isEditing: true }); + const instance = enzymeWrapper.instance(); + instance.doCreate(); + expect(spy.calledOnce).to.be.true; + const formObj = spy.args[0][1]; + expect(formObj).to.to.be.an('object'); + expect(formObj.url).to.be.defined; + // expect url to end with perspective name + expect(formObj.url.split('/').pop()).to.equal(DUMMY_STRING); + }); }); - it('options are loaded from props', () => { - const enzymeWrapper = setup({ - subjects: [ONE_SUBJECT], // + describe('on create', () => { + it('on create, state is set to params values', () => { + const params = { + 'subjects': 'NorthAmerica', + 'lenses': 'MultiTable', + 'statusFilterType': 'INCLUDE', + 'statusFilter': ['OK'], + 'subjectTagFilterType': 'EXCLUDE', + 'subjectTagFilter': [], + 'aspectTagFilterType': 'INCLUDE', + 'aspectTagFilter': ['OK'], + 'aspectFilterType': 'EXCLUDE', + 'aspectFilter': [] + } + const enzymeWrapper = setup({}, { params }); + const instance = enzymeWrapper.instance(); + for (let key in params) { + expect(instance.state[key]).to.equal(params[key]); + } + }); + + it('dropdown options still contains all the lenses,' + + ' even though state lens is empty', () => { + // be default, not editing + const enzymeWrapper = setup({}); + const instance = enzymeWrapper.instance(); + expect(instance.state.lenses).to.equal(EMPTY_STR); + // the lens dropdown is not empty + expect(instance.state.dropdownConfig.lenses.options.length).to.equal(ONE); + }); + + it('empty lens means state lens is also empty', () => { + // add onto default + const values = { + // actual values + lenses: [], + }; + const enzymeWrapper = setup(values); + const instance = enzymeWrapper.instance(); + // the current lens is empty + expect(instance.state.lenses).to.equal(EMPTY_STR); + }); + + it('empty array means state array is also empty', () => { + const values = { + aspectTagFilter: [], + }; + const enzymeWrapper = setup(values); + const instance = enzymeWrapper.instance(); + expect(instance.state.aspectTagFilter).to.deep.equal([]); }); - const instance = enzymeWrapper.instance(); - instance.updateDropdownConfig(); - const config = instance.state.dropdownConfig; - expect(Object.keys(config)).to.contain('subjects'); - expect(config.subjects.options.length).to.equal(ONE); }); - it('on filter, options array is alphabetical', () => { - const enzymeWrapper = setup({ - statusFilter: DUMMY_ARRAY, + describe('on initial render', () => { + it('on Create, output name is empty', () => { + const enzymeWrapper = setup(); + const instance = enzymeWrapper.instance(); + const INPUT = enzymeWrapper.find('input[name="name"]'); + expect(INPUT.getNode().value).to.equal(EMPTY_STR); + }); + + it('on edit, perspective name is not empty'); + + it('props maps to state', () => { + const enzymeWrapper = setup({}, { isEditing: true }); + const instance = enzymeWrapper.instance(); + expect(instance.state.statusFilter).to.deep.equal(DUMMY_ARRAY); + }); + + it('initial props isEditing is false', () => { + const enzymeWrapper = setup(); + const instance = enzymeWrapper.instance(); + expect(instance.props.isEditing).to.be.false; + }); + + it('on edit, subject options are not empty', () => { + const enzymeWrapper = setup({}, { isEditing: true }); + const instance = enzymeWrapper.instance(); + expect(instance.state.subjects).to.equal(DUMMY_STRING); + // one value, no leftover options + expect(instance.state.dropdownConfig.subjects.options.length).to.equal(ZERO); }); - const instance = enzymeWrapper.instance(); - instance.updateDropdownConfig(); - const config = instance.state.dropdownConfig; - expect(Object.keys(config)).to.contain('statusFilter'); - expect(config.statusFilter.options.length).to.equal(DUMMY_ARRAY.length); }); - it('state includes default perspective name', () => { + it('given the proper url parameter and resource, ' + + ' create modal is shown with proper resource name', () => { const enzymeWrapper = setup(); - const instance = enzymeWrapper.instance(); - expect(Object.keys(instance.state)).to.contain('perspectiveName'); - expect(instance.state.perspectiveName).to.equal(''); + expect(enzymeWrapper.find('.slds-modal__container')).to.have.length(ONE); + expect(enzymeWrapper.find('.slds-text-heading--medium').text()) + .to.equal('New Perspective'); }); it('on state change, perspective name is perserved', () => { @@ -156,7 +247,9 @@ describe('Perspective view ', () => { for (let key in config) { const styleObj = getDropdownStyle(instance.state, key); expect(styleObj.hasOwnProperty('marginTop')).to.be.true; - expect(styleObj.marginTop).to.equal(ZERO); + if (!config[key].isArray) { + expect(styleObj.marginTop).to.equal(ZERO); + } } }); @@ -223,6 +316,28 @@ describe('Perspective view ', () => { .to.contain(DUMMY_STRING); }); + it('on remove pill, state is updated for single pill input', () => { + const enzymeWrapper = setup(); + const RESOURCE_NAME = 'subjects'; + const instance = enzymeWrapper.instance(); + instance.setState({ subjects: [ONE_SUBJECT] }); + const OBJ = { + textContent: DUMMY_STRING, + }; + // for pillElem + stub.withArgs(OBJ, 'slds-pill').returns({ + getElementsByClassName: () => 'subjects' + }); + // for fieldElem + stub.withArgs(OBJ, 'slds-form-element__control') + .returns({ title: 'subjects' }); + + instance.deletePill({ + target: OBJ, + }); + expect(instance.state[RESOURCE_NAME]).to.equal(''); + }); + it('on remove pill, dropdown style does not change', () => { const enzymeWrapper = setup(); const RESOURCE_NAME = 'subjects'; @@ -249,7 +364,7 @@ describe('Perspective view ', () => { describe('for array inputs', () => { it('onclck remove pill, margin top is re-adjusted', () => { - const RESOURCE_NAME = 'aspectFilter'; + const RESOURCE_NAME = 'aspectTagFilter'; const enzymeWrapper = setup({ RESOURCE_NAME }); @@ -279,7 +394,7 @@ describe('Perspective view ', () => { it('onclck remove pill, the removed option is added ' + 'back into the dropdown', () => { - const RESOURCE_NAME = 'aspectFilter'; + const RESOURCE_NAME = 'aspectTagFilter'; const enzymeWrapper = setup(); const instance = enzymeWrapper.instance(); const OBJ = { @@ -307,15 +422,15 @@ describe('Perspective view ', () => { const enzymeWrapper = setup(); const instance = enzymeWrapper.instance(); const config = instance.state.dropdownConfig; - for (let key in config) { - const styleObj = getDropdownStyle(instance.state, key); - expect(styleObj.hasOwnProperty('marginTop')).to.be.true; - expect(styleObj.marginTop).to.equal(ZERO); - } + const key = 'subjectTagFilter'; + const styleObj = getDropdownStyle(instance.state, key); + expect(styleObj.hasOwnProperty('marginTop')).to.be.true; + // check margin top of empty values + expect(styleObj.marginTop).to.equal(ZERO); }); it('on pill removal, margin top moves up', () => { - const RESOURCE_NAME = 'aspectFilter'; + const RESOURCE_NAME = 'aspectTagFilter'; const setupObj = {}; const enzymeWrapper = setup(setupObj[RESOURCE_NAME]: DUMMY_ARRAY); const instance = enzymeWrapper.instance(); @@ -339,10 +454,10 @@ describe('Perspective view ', () => { it('isArray property is true', () => { const enzymeWrapper = setup({ - aspectFilter: 'qwewretrytuyi'.split(''), + aspectTagFilter: 'qwewretrytuyi'.split(''), }); const instance = enzymeWrapper.instance(); - expect(instance.state.dropdownConfig.aspectFilter.isArray).to.be.true; + expect(instance.state.dropdownConfig.aspectTagFilter.isArray).to.be.true; }); it('appending pills updates state to array of length one', () => { @@ -392,7 +507,7 @@ describe('Perspective view ', () => { }, }); expect(getDropdownStyle(instance.state, RESOURCE_NAME).marginTop) - .to.equal(instance.props.BLOCK_SIZE); + .to.be.above(ZERO); }); it('on add two pills, dropdown style moves down to accomodate pill', () => { diff --git a/tests/view/components/dropdown.js b/tests/view/components/dropdown.js index 419aa9daff..6e9ffe0fdf 100644 --- a/tests/view/components/dropdown.js +++ b/tests/view/components/dropdown.js @@ -55,6 +55,26 @@ describe('Dropdown component tests', () => { return enzymeWrapper; } + it('state data loads from props options', () => { + const ARR = [DUMMY_STRING]; + const enzymeWrapper = setup({ options: ARR }); + const instance = enzymeWrapper.instance(); + expect(instance.state.data.length).to.equal(ONE); + expect(instance.state.data).to.deep.equal(ARR); + }); + + it('given an array of single element, render single item', () => { + const enzymeWrapper = setup({ options: [DUMMY_STRING] }); + const instance = enzymeWrapper.instance(); + // change to open state, to show dropdown + instance.setState({ open: true }); + expect(enzymeWrapper.find('.slds-dropdown__item')).to.have.length(ONE); + }); + + it('on props showEditIcon true, render pencil icon'); + it('by default showEditIcon is false'); + it('on showEditIcon is false, no pencil icon is rendered'); + it('on toggle true, dropdown opens', () => { const enzymeWrapper = setup(); const instance = enzymeWrapper.instance(); @@ -84,10 +104,10 @@ describe('Dropdown component tests', () => { }); it('the INPUT has no value, and there are options, ' + - 'the first cell is highlighted', () => { + 'the first cell is NOT highlighted', () => { const enzymeWrapper = setup({ options: DUMMY_ARRAY, defaultValue: '' }); const instance = enzymeWrapper.instance(); - expect(instance.state.highlightedIndex).to.equal(ZERO); + expect(instance.state.highlightedIndex).to.equal(-1); }); it('the INPUT has value, the highlighted index ' + diff --git a/tests/view/components/persController.js b/tests/view/components/persController.js new file mode 100644 index 0000000000..a75334ec1a --- /dev/null +++ b/tests/view/components/persController.js @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2016, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or + * https://opensource.org/licenses/BSD-3-Clause + */ + +/** + * tests/view/components/persController.js + */ + +import { expect } from 'chai'; +import React from 'react'; +import sinon from 'sinon'; +import PerspectiveController from '../../../view/perspective/PerspectiveController.js'; +import { mount } from 'enzyme'; + +describe('Perspective controller ', () => { + const ZERO = 0; + const ONE = 1; + const TWO = 2; + const DUMMY_STRING = 'COOL'; + const DUMMY_ID = '743bcf42-cd79-46d0-8c0f-d43adbb63866'; + const DUMMY_FUNCTION = () => {}; + const ONE_SUBJECT = { + absolutePath: DUMMY_STRING, + isPublished: true, + }; + const DUMMY_ARRAY = 'qwertyui'.split(''); + const LENS = { + id: DUMMY_ID, + name: DUMMY_STRING, + }; + + function setup() { + const defaultProps = { + params: {}, + values: { + aspectFilter: [], + aspectTags: [], + lenses: [LENS], + }, + } + const enzymeWrapper = mount(); + return enzymeWrapper; + } + + describe('on show create modal', () => { + it('calling openCreatePanel sets state to true', () => { + const enzymeWrapper = setup(); + const instance = enzymeWrapper.instance(); + instance.openCreatePanel(); + expect(instance.state.showCreatePanel).to.be.true; + }); + }); +}); diff --git a/tests/view/components/perspectiveStatic.js b/tests/view/components/perspectiveStatic.js index 67df15c7b0..8945eb55b3 100644 --- a/tests/view/components/perspectiveStatic.js +++ b/tests/view/components/perspectiveStatic.js @@ -11,72 +11,104 @@ */ import { expect } from 'chai'; -import CreatePerspective from '../../../view/perspective/CreatePerspective.js'; -import { getArray } from '../../../view/perspective/configCreatePerspective.js'; +import { getArray, + getTagsFromResources, + getConfig, + filteredArray, + getOptions, +} from '../../../view/perspective/configCreatePerspective'; +import { getSubjects } from './utils'; -describe('Test static functions', () => { +describe('Config perspective functions', () => { const ZERO = 0; + const NUM = 10; const POPULAR_SAYING = 'The quick brown fox jumps over the lazy dog'; + const ARR = POPULAR_SAYING.split(' '); + const WORD = 'fox'; - /** - * Returns an array of resources with identical - * isPublished property, with - * fieldName field == index in loop - * - * @param {Integer} INT Make this many resources - * @param {String} fieldName The field of each resource - * @param {Boolean} isPublished All resources have - * this value of isPublished - * @returns {Array} Array with all published resources - */ - function getSubjects(INT, fieldName, isPublished) { - let subjects = []; - for (let i = INT; i > ZERO; i--) { - const obj = { - isPublished, - absolutePath: i, - }; - obj[fieldName] = i; - subjects.push(obj); - } - return subjects; - } - - it('getArray returns only published resources', () => { - const NUM = 10; - const unPublished = getArray( - 'absolutePath', - getSubjects(NUM, 'absolutePath') - ); - expect(unPublished.length).to.be.empty; - - const published = getArray( - 'absolutePath', - getSubjects(NUM, 'absolutePath', true) - ); - expect(published.length).to.equal(NUM); - // input is in decreasing order - // should preserve order - expect(published[ZERO]).to.equal(NUM); + it('getTagsFromResources does not return duplicates', () => { + const arr = [ + { tags: [ WORD ] }, + { tags: [] }, + { tags: [ WORD ] }, + ]; + const resultArr = getTagsFromResources(arr); + expect(resultArr.length).to.equal(1); + expect(resultArr).to.deep.equal([WORD]); }); - it('getArray should preserve order of input resources', () => { - const NUM = 10; - const published = getArray( - 'absolutePath', - getSubjects(NUM, 'absolutePath', true), + it('dropdown removes all options, if values === options', () => { + // remove empty spaces + const arr = getOptions( + ARR, + ARR, ); - // input is in decreasing order - expect(published[ZERO]).to.equal(NUM); + expect(arr).to.be.empty; }); - it('on select option, dropdown removes that option ' + + it('dropdown removes existing option ' + 'from available options', () => { // remove empty spaces - const filteredArray = CreatePerspective.filteredArray( + const arr = filteredArray( POPULAR_SAYING.split(''), ' ', ); - expect(filteredArray).to.not.contain(' '); + expect(arr).to.not.contain(' '); + }); + + describe('getConfig', () => { + it('config options contain the expected number of options', () => { + const key = 'statusFilter'; // any string + const values = {}; + values[key] = POPULAR_SAYING.split(' '); + const value = [WORD]; + const config = getConfig(values, key, value); + expect(config.options.length).to.equal(values[key].length - 1); + }); + + it('options contain only values not in field', () => { + const key = 'statusFilter'; // any string + const values = {}; + values[key] = POPULAR_SAYING.split(' '); + const value = [WORD]; + const config = getConfig(values, key, value); + expect(config.options).to.not.contain(WORD); + }); + }); + + describe('getArray', () => { + it('returns all items except ' + + 'for the item whose field === third param key'); + + it('does not return unPublished resources', () => { + const unPublished = getArray( + 'absolutePath', + // unpublished + getSubjects(NUM, 'absolutePath', false) + ); + expect(unPublished.length).to.be.empty; + }); + + it('returns published resources', () => { + const published = getArray( + 'absolutePath', + // published + getSubjects(NUM, 'absolutePath', true) + ); + expect(published.length).to.equal(NUM); + // input is in decreasing order + // should preserve order + expect(published[ZERO]).to.equal(NUM); + }); + + it('preserves the order of input resources', () => { + const published = getArray( + 'absolutePath', + // published + getSubjects(NUM, 'absolutePath', true), + ); + // input is in decreasing order + expect(published[ZERO]).to.equal(NUM); + }); }); }); diff --git a/tests/view/components/utils.js b/tests/view/components/utils.js new file mode 100644 index 0000000000..3726a43240 --- /dev/null +++ b/tests/view/components/utils.js @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2016, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or + * https://opensource.org/licenses/BSD-3-Clause + */ + +/** + * tests/view/components/utils.js + */ + +const ZERO = 0; + +/** + * Returns an array of resources with identical + * isPublished property, with + * fieldName field == index in loop + * + * @param {Integer} INT Make this many resources + * @param {String} fieldName The field of each resource + * @param {Boolean} isPublished All resources have + * this value of isPublished + * @returns {Array} Array with all published resources + */ +function getSubjects(INT, fieldName, isPublished) { + let subjects = []; + for (let i = INT; i > ZERO; i--) { + const obj = { + isPublished, + absolutePath: i, + }; + obj[fieldName] = i; + subjects.push(obj); + } + return subjects; +} + +module.exports = { + getSubjects, +}; diff --git a/view/admin/components/common/Dropdown.js b/view/admin/components/common/Dropdown.js index 93252c34fa..adf9afc612 100644 --- a/view/admin/components/common/Dropdown.js +++ b/view/admin/components/common/Dropdown.js @@ -41,10 +41,6 @@ function getIndexFromArray(options, defaultValue) { let index = -ONE; // default: no options in dropdown if (options.length) { index = options.indexOf(defaultValue); - // indexOf might return 0 - if (index < ZERO) { - index = ZERO; - } } return index; } @@ -74,11 +70,12 @@ class Dropdown extends React.Component { super(props); this.state = { open: false, // dropdown is open or closed - data: [], // data in dropdown - highlightedIndex: getIndexFromArray(props.options, props. defaultValue), + data: props.options, // data in dropdown + highlightedIndex: getIndexFromArray(props.options, props.defaultValue), }; this.toggle = this.toggle.bind(this); } + componentDidMount() { // click anywhere outside of container // to hide dropdown @@ -100,6 +97,7 @@ class Dropdown extends React.Component { open: bool, }); } + handleFocus() { // show all options this.setState({ @@ -107,6 +105,7 @@ class Dropdown extends React.Component { data: this.props.options, }); } + handleKeyUp(evt) { const Key = { UP: 38, @@ -136,7 +135,7 @@ class Dropdown extends React.Component { false, ), }); - } else if (keycode === Key.ENTER) { + } else if (keycode === Key.ENTER && this.props.renderAsLink) { const persName = options[highlightedIndex]; window.location.href = '/perspectives/' + persName; } else { @@ -160,6 +159,7 @@ class Dropdown extends React.Component { } render () { const { + options, newButtonText, dropDownStyle, allOptionsLabel, @@ -168,37 +168,48 @@ class Dropdown extends React.Component { showSearchIcon, onAddNewButton, onClickItem, + onEdit, showInputElem, children, // react elements defaultValue, + showEditIcon, + renderAsLink, //boolean } = this.props; const { data } = this.state; let outputUL = ''; // if options exist, load them if (data.length) { - outputUL =