diff --git a/.travis.yml b/.travis.yml index 44b3916..85ab36f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,10 +24,10 @@ before_install: install: - npm install ember-cli-sass # NOTE: this requies scripts - npm install --ignore-scripts -- bower install +- ./node_modules/.bin/bower install script: -- npm run lint -s -- ember try:one $EMBER_TRY_SCENARIO --- COVERAGE=true ember test +- npm run lint +- npm run ci-test after_success: - .travis/publish-coverage.sh env: diff --git a/.travis/publish-coverage.sh b/.travis/publish-coverage.sh old mode 100644 new mode 100755 diff --git a/.travis/publish-gh-pages.sh b/.travis/publish-gh-pages.sh old mode 100644 new mode 100755 diff --git a/addon/components/frost-fixed-table.js b/addon/components/frost-fixed-table.js new file mode 100644 index 0000000..5315925 --- /dev/null +++ b/addon/components/frost-fixed-table.js @@ -0,0 +1,334 @@ +/** + * Component definition for the frost-fixed-table component + */ +import Ember from 'ember' +const {$} = Ember +import computed, {readOnly} from 'ember-computed-decorators' +import {Component} from 'ember-frost-core' +import {PropTypes} from 'ember-prop-types' + +import {ColumnPropType, ItemsPropType} from 'ember-frost-table/typedefs' +import layout from '../templates/components/frost-fixed-table' + +export default Component.extend({ + // == Dependencies ========================================================== + + // == Keyword Properties ==================================================== + + layout, + + // == PropTypes ============================================================= + + propTypes: { + // options + columns: PropTypes.arrayOf(ColumnPropType), + items: ItemsPropType + }, + + getDefaultProps () { + return { + // options + columns: [], + items: [] + } + }, + + // == Computed Properties =================================================== + + @readOnly + @computed('css') + /** + * The selector for the left body DOM element (specifically the scroll wrapper) + * @param {String} css - the base css class name for the component + * @returns {String} a sutiable jQuery selector for the left section of the table body + */ + bodyLeftSelector (css) { + return `.${css}-left .frost-scroll` + }, + + @readOnly + @computed('css') + /** + * The selector for the middle body DOM element (specifically the scroll wrapper) + * @param {String} css - the base css class name for the component + * @returns {String} a sutiable jQuery selector for the middle section of the table body + */ + bodyMiddleSelector (css) { + return `.${css}-middle .frost-scroll` + }, + + @readOnly + @computed('css') + /** + * The selector for the right body DOM element (specifically the scroll wrapper) + * @param {String} css - the base css class name for the component + * @returns {String} a sutiable jQuery selector for the right section of the table body + */ + bodyRightSelector (css) { + return `.${css}-right .frost-scroll` + }, + + @readOnly + @computed('css') + /** + * The selector for the middle header DOM element (specifically the scroll wrapper) + * @param {String} css - the base css class name for the component + * @returns {String} a sutiable jQuery selector for the middle section of the table header + */ + headerMiddleSelector (css) { + return `.${css}-header-middle .frost-scroll` + }, + + @readOnly + @computed('columns') + /** + * Get the set of columns that are supposed to be frozen on the left + * + * The set of leftColumns is defined as all the columns with `frozen` === `true` + * starting with the first column until we reach one w/o `frozen` === `true` + * + * @param {Column[]} columns - all the columns + * @returns {Column[]} just the left-most frozen columns + */ + leftColumns (columns) { + const frozenColumns = [] + for (let i = 0; i < columns.length; i++) { + const column = columns[i] + if (column.frozen) { + frozenColumns.push(column) + } else { + return frozenColumns + } + } + + return frozenColumns + }, + + @readOnly + @computed('columns') + /** + * Get the set of columns that are supposed to be in the middle (between the frozen left and frozen right columns) + * + * The set of middleColumns is defined as all the columns with `frozen` === `false` + * starting with whatever the first column is with `frozen` === `false` until we reach one with `frozen` === `true` + * + * @param {Column[]} columns - all the columns + * @returns {Column[]} just the middle columns + */ + middleColumns (columns) { + const unFrozenColumns = [] + let foundUnFrozen = false + for (let i = 0; i < columns.length; i++) { + const column = columns[i] + if (column.frozen) { + if (foundUnFrozen) { + return unFrozenColumns + } + } else { + foundUnFrozen = true + unFrozenColumns.push(column) + } + } + + return unFrozenColumns + }, + + @readOnly + @computed('columns') + /** + * Get the set of columns that are supposed to be frozen on the right + * + * The set of rightColumns is defined as all the columns with `frozen` === `true` + * starting with the first `frozen` === `true` column after we've seen at least one `frozen` === `false` column. + * + * @param {Column[]} columns - all the columns + * @returns {Column[]} just the middle columns + */ + rightColumns (columns) { + const frozenColumns = [] + for (let i = columns.length - 1; i > 0; i--) { + const column = columns[i] + if (column.frozen) { + frozenColumns.push(column) + } else { + return frozenColumns.reverse() + } + } + + return frozenColumns.reverse() + }, + + // == Functions ============================================================= + + /** + * Get the width of the middle section by adding up the widths of all the cells + * @param {String} cellSelector - the selector to use to find the cells + * @returns {Number} the combined outer width of all cells (in pixels) + */ + _calculateWidth (cellSelector) { + let width = 0 + + this.$(cellSelector).toArray().forEach((el) => { + width += $(el).outerWidth() + }) + + return width + }, + + /** + * Make the three body sections (left, middle, right) the correct height to stay within the bounds of the + * table itself + */ + setupBodyHeights () { + const headerMiddleSelector = this.get('headerMiddleSelector') + const headerHeight = this.$(headerMiddleSelector).outerHeight() + const tableHeight = this.$().outerHeight() + const bodyHeight = tableHeight - headerHeight + + const bodyLeftSelector = this.get('bodyLeftSelector') + const bodyMiddleSelector = this.get('bodyMiddleSelector') + const bodyRightSelector = this.get('bodyRightSelector') + + ;[bodyLeftSelector, bodyMiddleSelector, bodyRightSelector].forEach((selector) => { + this.$(selector).css({height: `${bodyHeight}px`}) + }) + }, + + /** + * frost-scroll seems to display scroll bars on hover, sooo, we need to proxy hover events to the place where + * the scrollbar is present, the middle body when the middle of the header is hovered, and the right body when + * the left or middle body is hovered. + */ + setupHoverProxy () { + const hoverClass = 'ps-container-hover' + const bodyLeftSelector = this.get('bodyLeftSelector') + const bodyMiddleSelector = this.get('bodyMiddleSelector') + const bodyRightSelector = this.get('bodyRightSelector') + + ;[bodyLeftSelector, bodyMiddleSelector].forEach((selector) => { + const $element = this.$(selector) + $element.on('mouseenter', () => { + this.$(bodyRightSelector).addClass(hoverClass) + }) + $element.on('mouseleave', () => { + this.$(bodyRightSelector).removeClass(hoverClass) + }) + }) + + const headerMiddleSelector = this.get('headerMiddleSelector') + const $headerMiddle = this.$(headerMiddleSelector) + $headerMiddle.on('mouseenter', () => { + this.$(bodyMiddleSelector).addClass(hoverClass) + }) + + $headerMiddle.on('mouseleave', () => { + this.$(bodyMiddleSelector).removeClass(hoverClass) + }) + }, + + /** + * Calculate the widths of the left and right side and set the marings of the middle accordingly. + */ + setupMiddleMargins () { + const bodyLeftSelector = this.get('bodyLeftSelector') + const bodyRightSelector = this.get('bodyRightSelector') + + const leftWidth = this.$(bodyLeftSelector).outerWidth() + const rightWidth = this.$(bodyRightSelector).outerWidth() + + const headerMiddleSelector = this.get('headerMiddleSelector') + const bodyMiddleSelector = this.get('bodyMiddleSelector') + ;[headerMiddleSelector, bodyMiddleSelector].forEach((selector) => { + this.$(selector).css({ + 'margin-left': `${leftWidth}px`, + 'margin-right': `${rightWidth}px` + }) + }) + }, + + /** + * Calculate how wide the middle sections should be by adding the sum of all the inner cells, then set that width + */ + setupMiddleWidths () { + const headerMiddleSelector = this.get('headerMiddleSelector') + const bodyMiddleSelector = this.get('bodyMiddleSelector') + + const width = this._calculateWidth(`${headerMiddleSelector} .frost-table-cell`) + this.$(`${headerMiddleSelector} .frost-table-header`).css({width: `${width}px`}) + this.$(`${bodyMiddleSelector} .frost-table-row`).css({width: `${width}px`}) + }, + + /** + * Set up the scroll synchronization between the different components within the table that should scroll together + */ + setupScrollSync () { + const headerMiddleSelector = this.get('headerMiddleSelector') + const bodyLeftSelector = this.get('bodyLeftSelector') + const bodyMiddleSelector = this.get('bodyMiddleSelector') + const bodyRightSelector = this.get('bodyRightSelector') + + this.syncScrollLeft(headerMiddleSelector, bodyMiddleSelector) + this.syncScrollLeft(bodyMiddleSelector, headerMiddleSelector) + + this.syncScrollTop(bodyLeftSelector, bodyMiddleSelector, bodyRightSelector) + this.syncScrollTop(bodyMiddleSelector, bodyLeftSelector, bodyRightSelector) + this.syncScrollTop(bodyRightSelector, bodyLeftSelector, bodyMiddleSelector) + }, + + /** + * Sync horizontal scrolling between a source of scroll events and a set of destination selectors + * @param {String} source - the selector of the source of the scroll events + * @param {String[]} destinations - the selectors of the destination (the ones being driven by the source) + */ + syncScrollLeft (source, ...destinations) { + this.$(source).on('scroll', () => { + // NOTE: intentionally not putting this in the ember run loop because doing so made it much less responsive + // there was a noticible lag between scrolling and re-positioning the synced element. Plus it's not updating + // any DOM content, just setting scroll positions. + const $source = this.$(source) + destinations.forEach((destination) => { + const $destination = this.$(destination) + $destination.scrollLeft($source.scrollLeft()) + }) + }) + }, + + /** + * Sync vertical scrolling between a source of scroll events and a set of destination selectors + * @param {String} source - the selector of the source of the scroll events + * @param {String[]} destinations - the selectors of the destination (the ones being driven by the source) + */ + syncScrollTop (source, ...destinations) { + this.$(source).on('scroll', () => { + // NOTE: intentionally not putting this in the ember run loop because doing so made it much less responsive + // there was a noticible lag between scrolling and re-positioning the synced element. Plus it's not updating + // any DOM content, just setting scroll positions. + const $source = this.$(source) + destinations.forEach((destination) => { + const $destination = this.$(destination) + $destination.scrollTop($source.scrollTop()) + }) + }) + }, + + // == DOM Events ============================================================ + + // == Lifecycle Hooks ======================================================= + + /** + * Set up synced scrolling as well as calculating padding for middle sections + */ + didRender () { + this._super(...arguments) + this.setupBodyHeights() + this.setupHoverProxy() + this.setupMiddleWidths() + this.setupMiddleMargins() + this.setupScrollSync() + }, + + // == Actions =============================================================== + + actions: { + } +}) diff --git a/addon/components/frost-table-body.js b/addon/components/frost-table-body.js index 4100526..30e843d 100644 --- a/addon/components/frost-table-body.js +++ b/addon/components/frost-table-body.js @@ -2,51 +2,34 @@ * Component definition for the frost-table-body component */ -import Ember from 'ember' -const {Component} = Ember -import PropTypesMixin, {PropTypes} from 'ember-prop-types' +import {Component} from 'ember-frost-core' +import {PropTypes} from 'ember-prop-types' +import {ColumnPropType, ItemsPropType} from 'ember-frost-table/typedefs' import layout from '../templates/components/frost-table-body' -export default Component.extend(PropTypesMixin, { +export default Component.extend({ // == Dependencies ========================================================== // == Keyword Properties ==================================================== - classNameBindings: ['css'], layout, tagName: 'tbody', // == PropTypes ============================================================= - /** - * Properties for this component. Options are expected to be (potentially) - * passed in to the component. State properties are *not* expected to be - * passed in/overwritten. - */ propTypes: { // options - columns: PropTypes.arrayOf(PropTypes.shape({ - className: PropTypes.string, - label: PropTypes.string, - propertyName: PropTypes.string.isRequired - })), - css: PropTypes.string, - hook: PropTypes.string.isRequired, - items: PropTypes.array, + columns: PropTypes.arrayOf(ColumnPropType), + items: ItemsPropType // state - - // keywords - layout: PropTypes.any }, - /** @returns {Object} the default property values when not provided by consumer */ getDefaultProps () { return { // options columns: [], - css: this.getCss(), items: [] // state @@ -57,13 +40,6 @@ export default Component.extend(PropTypesMixin, { // == Functions ============================================================= - /** - * @returns {String} the base css class for this component (the component name) - */ - getCss () { - return this.toString().replace(/^.+:(.+)::.+$/, '$1') - }, - // == DOM Events ============================================================ // == Lifecycle Hooks ======================================================= diff --git a/addon/components/frost-table-cell.js b/addon/components/frost-table-cell.js new file mode 100644 index 0000000..6a626ea --- /dev/null +++ b/addon/components/frost-table-cell.js @@ -0,0 +1,29 @@ +/** + * Component definition for the frost-table-cell component + */ + +import {Component} from 'ember-frost-core' + +export default Component.extend({ + // == Dependencies ========================================================== + + // == Keyword Properties ==================================================== + + attributeBindings: ['title'], + tagName: 'td', + + // == PropTypes ============================================================= + + // == Computed Properties =================================================== + + // == Functions ============================================================= + + // == DOM Events ============================================================ + + // == Lifecycle Hooks ======================================================= + + // == Actions =============================================================== + + actions: { + } +}) diff --git a/addon/components/frost-table-header.js b/addon/components/frost-table-header.js index bd1f5ea..e65c6b6 100644 --- a/addon/components/frost-table-header.js +++ b/addon/components/frost-table-header.js @@ -2,52 +2,37 @@ * Component definition for the frost-table-header component */ -import Ember from 'ember' -const {Component} = Ember -import PropTypesMixin, {PropTypes} from 'ember-prop-types' +import {Component} from 'ember-frost-core' +import {PropTypes} from 'ember-prop-types' +import {ColumnPropType} from 'ember-frost-table/typedefs' import layout from '../templates/components/frost-table-header' -export default Component.extend(PropTypesMixin, { +export default Component.extend({ // == Dependencies ========================================================== // == Keyword Properties ==================================================== - classNameBindings: ['css'], layout, tagName: 'thead', // == PropTypes ============================================================= - /** - * Properties for this component. Options are expected to be (potentially) - * passed in to the component. State properties are *not* expected to be - * passed in/overwritten. - */ propTypes: { // options - columns: PropTypes.arrayOf(PropTypes.shape({ - className: PropTypes.string, - label: PropTypes.string, - propertyName: PropTypes.string.isRequired - })), - css: PropTypes.string, - hook: PropTypes.string.isRequired, + cellCss: PropTypes.string, + cellTagName: PropTypes.string, + columns: PropTypes.arrayOf(ColumnPropType) // state - - // keywords - classNameBindings: PropTypes.arrayOf(PropTypes.string), - layout: PropTypes.any, - tagName: PropTypes.any }, - /** @returns {Object} the default property values when not provided by consumer */ getDefaultProps () { return { // options - columns: [], - css: this.getCss() + cellCss: this.get('css'), + cellTagName: 'th', + columns: [] // state } @@ -57,13 +42,6 @@ export default Component.extend(PropTypesMixin, { // == Functions ============================================================= - /** - * @returns {String} the base css class for this component (the component name) - */ - getCss () { - return this.toString().replace(/^.+:(.+)::.+$/, '$1') - }, - // == DOM Events ============================================================ // == Lifecycle Hooks ======================================================= diff --git a/addon/components/frost-table-row.js b/addon/components/frost-table-row.js index b0b854e..ef36f86 100644 --- a/addon/components/frost-table-row.js +++ b/addon/components/frost-table-row.js @@ -2,55 +2,38 @@ * Component definition for the frost-table-body-row component */ -import Ember from 'ember' -const {Component} = Ember -import PropTypesMixin, {PropTypes} from 'ember-prop-types' +import {Component} from 'ember-frost-core' +import {PropTypes} from 'ember-prop-types' +import {ColumnPropType} from 'ember-frost-table/typedefs' import layout from '../templates/components/frost-table-row' -export default Component.extend(PropTypesMixin, { +export default Component.extend({ // == Dependencies ========================================================== // == Keyword Properties ==================================================== - classNameBindings: ['css'], layout, tagName: 'tr', // == PropTypes ============================================================= - /** - * Properties for this component. Options are expected to be (potentially) - * passed in to the component. State properties are *not* expected to be - * passed in/overwritten. - */ propTypes: { // options - columns: PropTypes.arrayOf(PropTypes.shape({ - className: PropTypes.string, - label: PropTypes.string, - propertyName: PropTypes.string.isRequired - })), cellCss: PropTypes.string, - css: PropTypes.string, - hook: PropTypes.string.isRequired, - item: PropTypes.object, + cellTagName: PropTypes.string, + columns: PropTypes.arrayOf(ColumnPropType), + item: PropTypes.object // state - - // keywords - classNameBindings: PropTypes.arrayOf(PropTypes.string), - layout: PropTypes.any, - tagName: PropTypes.string }, - /** @returns {Object} the default property values when not provided by consumer */ getDefaultProps () { return { // options + cellTagName: 'td', + cellCss: this.get('css'), columns: [], - cellCss: this.getCss(), - css: this.getCss(), item: {} // state @@ -61,13 +44,6 @@ export default Component.extend(PropTypesMixin, { // == Functions ============================================================= - /** - * @returns {String} the base css class for this component (the component name) - */ - getCss () { - return this.toString().replace(/^.+:(.+)::.+$/, '$1') - }, - // == DOM Events ============================================================ // == Lifecycle Hooks ======================================================= diff --git a/addon/components/frost-table.js b/addon/components/frost-table.js index 42023f8..ba05ecf 100644 --- a/addon/components/frost-table.js +++ b/addon/components/frost-table.js @@ -2,53 +2,34 @@ * Component definition for the frost-table component */ -import Ember from 'ember' -const {Component} = Ember -import PropTypesMixin, {PropTypes} from 'ember-prop-types' +import {Component} from 'ember-frost-core' +import {PropTypes} from 'ember-prop-types' +import {ColumnPropType} from 'ember-frost-table/typedefs' import layout from '../templates/components/frost-table' -export default Component.extend(PropTypesMixin, { +export default Component.extend({ // == Dependencies ========================================================== // == Keyword Properties ==================================================== - classNameBindings: ['css'], layout, tagName: 'table', // == PropTypes ============================================================= - /** - * Properties for this component. Options are expected to be (potentially) - * passed in to the component. State properties are *not* expected to be - * passed in/overwritten. - */ propTypes: { // options - columns: PropTypes.arrayOf(PropTypes.shape({ - className: PropTypes.string, - label: PropTypes.string, - propertyName: PropTypes.string.isRequired - })), - css: PropTypes.string, - hook: PropTypes.string.isRequired, - items: PropTypes.array, + columns: PropTypes.arrayOf(ColumnPropType), + items: PropTypes.array // state - - // keywords - classNameBindings: PropTypes.arrayOf(PropTypes.string), - layout: PropTypes.any, - tagName: PropTypes.any }, - /** @returns {Object} the default property values when not provided by consumer */ getDefaultProps () { return { // options columns: [], - css: this.getCss(), items: [] // state @@ -59,13 +40,6 @@ export default Component.extend(PropTypesMixin, { // == Functions ============================================================= - /** - * @returns {String} the base css class for this component (the component name) - */ - getCss () { - return this.toString().replace(/^.+:(.+)::.+$/, '$1') - }, - // == DOM Events ============================================================ // == Lifecycle Hooks ======================================================= diff --git a/addon/helpers/extend.js b/addon/helpers/extend.js deleted file mode 100644 index ddc47e0..0000000 --- a/addon/helpers/extend.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * The extend helper, used to extend a given object by adding properites from another object to it - * the original object is not modified, but rather the properties of all objects are copied onto a new, empty object - */ -import Ember from 'ember' -const {Helper, assign} = Ember -const {helper} = Helper - -export function extend ([original], newProps) { - return assign({}, original, newProps) -} - -export default helper(extend) diff --git a/addon/templates/components/frost-fixed-table.hbs b/addon/templates/components/frost-fixed-table.hbs new file mode 100644 index 0000000..ff14741 --- /dev/null +++ b/addon/templates/components/frost-fixed-table.hbs @@ -0,0 +1,93 @@ +{{! Template for the frost-fixed-table component }} +
+ {{!-- + Strange as it seems, we want middle above left/right so that left/right are on top of middle. + The left and right are positioned absolutely so that they stay where they should be + --}} +
+ {{#frost-scroll}} + {{frost-table-header + cellTagName='div' + columns=middleColumns + hook=(concat hookPrefix '-header-middle') + hookQualifiers=hookQualifiers + tagName='div' + }} + {{/frost-scroll}} +
+
+ {{frost-table-header + cellTagName='div' + columns=leftColumns + hook=(concat hookPrefix '-header-left') + hookQualifiers=hookQualifiers + tagName='div' + }} +
+
+ {{frost-table-header + cellTagName='div' + columns=rightColumns + hook=(concat hookPrefix '-header-right') + hookQualifiers=hookQualifiers + tagName='div' + }} +
+
+ +
+ {{!-- + Strange as it seems, we want middle above left/right so that left/right are on top of middle. + The left and right are positioned absolutely so that they stay where they should be + --}} +
+ {{#frost-scroll}} + {{#each items as |item index|}} + {{frost-table-row + cellTagName='div' + columns=middleColumns + hook=(concat hookPrefix '-middle') + hookQualifiers=(extend hookQualifiers row=index) + item=item + tagName='div' + }} + {{/each}} + {{/frost-scroll}} +
+ +
+ {{#frost-scroll}} + {{#each items as |item index|}} + {{frost-table-row + cellTagName='div' + columns=leftColumns + hook=(concat hookPrefix '-left') + hookQualifiers=(extend hookQualifiers row=index) + item=item + tagName='div' + }} + {{/each}} + {{/frost-scroll}} +
+ +
+ {{#frost-scroll}} + {{#each items as |item index|}} + {{frost-table-row + cellTagName='div' + columns=rightColumns + hook=(concat hookPrefix '-right') + hookQualifiers=(extend hookQualifiers row=index) + item=item + tagName='div' + }} + {{/each}} + {{/frost-scroll}} +
+
diff --git a/addon/templates/components/frost-table-body.hbs b/addon/templates/components/frost-table-body.hbs index 29d48b9..f49eba7 100644 --- a/addon/templates/components/frost-table-body.hbs +++ b/addon/templates/components/frost-table-body.hbs @@ -3,7 +3,7 @@ {{frost-table-row cellCss=css columns=columns - hook=(concat hook '-row') + hook=(concat hookPrefix '-row') hookQualifiers=(extend hookQualifiers row=index) item=item }} diff --git a/addon/templates/components/frost-table-header.hbs b/addon/templates/components/frost-table-header.hbs index 1055e04..56f9f50 100644 --- a/addon/templates/components/frost-table-header.hbs +++ b/addon/templates/components/frost-table-header.hbs @@ -1,9 +1,11 @@ {{! Template for the frost-table-header component }} -{{#each columns as |column|}} - +{{#each columns as |column index|}} + {{#frost-table-cell + tagName=cellTagName + class=(concat cellCss '-cell' ' ' column.className) + hook=(concat hookPrefix '-cell') + hookQualifiers=(extend hookQualifiers column=index) + }} {{~ column.label ~}} - + {{/frost-table-cell}} {{/each}} diff --git a/addon/templates/components/frost-table-row.hbs b/addon/templates/components/frost-table-row.hbs index ba58998..a54ddf7 100644 --- a/addon/templates/components/frost-table-row.hbs +++ b/addon/templates/components/frost-table-row.hbs @@ -1,9 +1,11 @@ {{! Template for the frost-table-row component }} {{#each columns as |column index|}} - + {{#frost-table-cell + tagName=cellTagName + class=(concat cellCss '-cell' ' ' column.className) + hook=(concat hookPrefix '-cell') + hookQualifiers=(extend hookQualifiers column=index) + }} {{~ get item column.propertyName ~}} - + {{/frost-table-cell}} {{/each}} diff --git a/addon/typedefs.js b/addon/typedefs.js index 8d579b8..7f219bd 100644 --- a/addon/typedefs.js +++ b/addon/typedefs.js @@ -2,9 +2,31 @@ * Type definitions for ember-frost-table */ +import {PropTypes} from 'ember-prop-types' + /** * @typedef Column * @property {String} [className] - the name of the class to add to all cells of a column * @property {String} label - the column header label * @property {String} propertyName - the name of the property in the data record to display in this column + * @property {Boolean} [frozen=false] - true if this column should be frozen (on either the left or right side of the table) + * @property {Component} [renderer] - the cell renderer to use for all data cells in this column */ + +export const ColumnPropType = PropTypes.shape({ + className: PropTypes.string, + frozen: PropTypes.bool, + label: PropTypes.string, + propertyName: PropTypes.string.isRequired, + renderer: PropTypes.any +}) + +export const ItemsPropType = PropTypes.oneOfType([ + PropTypes.EmberObject, // DS.RecordArray + PropTypes.arrayOf( + PropTypes.oneOfType([ + PropTypes.object, + PropTypes.EmberObject + ]) + ) +]) diff --git a/app/components/frost-fixed-table.js b/app/components/frost-fixed-table.js new file mode 100644 index 0000000..752694d --- /dev/null +++ b/app/components/frost-fixed-table.js @@ -0,0 +1,4 @@ +/** + * Simple re-export of frost-fixed-table in the app namespace + */ +export {default} from 'ember-frost-table/components/frost-fixed-table' diff --git a/app/components/frost-table-cell.js b/app/components/frost-table-cell.js new file mode 100644 index 0000000..b6e52af --- /dev/null +++ b/app/components/frost-table-cell.js @@ -0,0 +1,4 @@ +/** + * Simple re-export of frost-table-cell in the app namespace + */ +export {default} from 'ember-frost-table/components/frost-table-cell' diff --git a/app/helpers/ehook.js b/app/helpers/ehook.js deleted file mode 100644 index c51c1a9..0000000 --- a/app/helpers/ehook.js +++ /dev/null @@ -1 +0,0 @@ -export { default, ehook } from 'ember-frost-table/helpers/ehook' diff --git a/app/helpers/extend.js b/app/helpers/extend.js deleted file mode 100644 index 31ef5ff..0000000 --- a/app/helpers/extend.js +++ /dev/null @@ -1 +0,0 @@ -export { default, extend } from 'ember-frost-table/helpers/extend' diff --git a/app/styles/_frost-fixed-table.scss b/app/styles/_frost-fixed-table.scss new file mode 100644 index 0000000..7ef36fc --- /dev/null +++ b/app/styles/_frost-fixed-table.scss @@ -0,0 +1,72 @@ +// +// Styles specific for the frost-fixed-table component (and its pieces) +// + +.frost-fixed-table { + position: relative; + + &-header, + &-body { + display: flex; + flex-direction: row; + } + + &-header { + &-left { + position: absolute; + left: 0; + } + + &-middle { + position: relative; + overflow: hidden; + + // NOTE this level of specificity is required to override perfect-scroll default styles + .ps-container > .ps-scrollbar-x-rail { + display: none; + } + } + + &-right { + position: absolute; + right: 0; + } + } + + &-left, + &-middle, + &-right { + overflow: hidden; + } + + &-left { + position: absolute; + left: 0; + box-shadow: 23px 0 15px -4px $frost-table-box-shadow-color; + + // NOTE this level of specificity is required to override perfect-scroll default styles + .ps-container > .ps-scrollbar-y-rail { + display: none; + } + } + + &-middle { + position: relative; + + // NOTE this level of specificity is required to override perfect-scroll default styles + .ps-container > .ps-scrollbar-y-rail { + display: none; + } + } + + &-right { + position: absolute; + right: 0; + box-shadow: -23px 0 15px -4px $frost-table-box-shadow-color; + } + + .frost-table-cell { + @include frost-table-truncate-text; + display: inline-block; + } +} diff --git a/app/styles/_frost-table.scss b/app/styles/_frost-table.scss new file mode 100644 index 0000000..1106c42 --- /dev/null +++ b/app/styles/_frost-table.scss @@ -0,0 +1,51 @@ +// +// Styles specific for the frost-table component (and its pieces) +// + +.frost-table { + table-layout: fixed; + + &-cell { + padding: $frost-table-cell-padding; + border-right: solid 1px $frost-color-lgrey-1; + text-align: left; + + &:last-child { + border-right: 0; + } + + &.right { + text-align: right; + } + } + + &-header { + background-color: $frost-color-white; + color: $frost-color-grey-4; + font-weight: bold; + + &-cell { + min-width: 40px; + height: $frost-table-header-height; + } + } + + &-row { + &:nth-child(even) { + background-color: $frost-color-white; + } + + &:nth-child(odd) { + background-color: $frost-color-lgrey-3; + } + + // NOTE: keep the .even and .odd below the :nth-child() rules so they can override the default nth-row behavior + &.even { + background-color: $frost-color-white; + } + + &.odd { + background-color: $frost-color-lgrey-3; + } + } +} diff --git a/app/styles/_mixins.scss b/app/styles/_mixins.scss new file mode 100644 index 0000000..f8bb48b --- /dev/null +++ b/app/styles/_mixins.scss @@ -0,0 +1,9 @@ +// +// Mixins used by the styles for the ember-frost-table components +// + +@mixin frost-table-truncate-text { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} diff --git a/app/styles/_perfect-scrollbar.scss b/app/styles/_perfect-scrollbar.scss new file mode 100644 index 0000000..088d30b --- /dev/null +++ b/app/styles/_perfect-scrollbar.scss @@ -0,0 +1,15 @@ +// +// Some overrides we need for perfect-scrollbar, specifically, we need to be able to apply the :hover styles when +// the container isn't actually hovered, but rather, when a class name is added. +// By necesity, these are gonna be fragile, and may need to be tweaked when we update perfect-scrollbar +// + +.ps-container-hover { + .ps-scrollbar-y-rail { + opacity: .6; + } + + .ps-scrollbar-x-rail { + opacity: .6; + } +} diff --git a/app/styles/_variables.scss b/app/styles/_variables.scss index e15c9aa..fdf6de5 100644 --- a/app/styles/_variables.scss +++ b/app/styles/_variables.scss @@ -6,5 +6,4 @@ $frost-table-header-height: 40px; $frost-table-cell-padding: 10px; $frost-table-border-width: 1px; -$frost-table-scrollable-body-box-shadow-color: rgba(0, 0, 0, .13); -$frost-table-scrollable-body-box-shadow: 0 1px 15px 3px $frost-table-scrollable-body-box-shadow-color; +$frost-table-box-shadow-color: rgba(0, 0, 0, .08); diff --git a/app/styles/ember-frost-table.scss b/app/styles/ember-frost-table.scss index 8e84692..6b67cc8 100644 --- a/app/styles/ember-frost-table.scss +++ b/app/styles/ember-frost-table.scss @@ -1,61 +1,9 @@ // -// Styles for a frost-table component +// Styles for a ember-frost-table addon // @import 'variables'; - -// Common cell styles -@mixin table-cell { - padding: $frost-table-cell-padding; - border-right: solid 1px $frost-color-lgrey-1; - text-align: left; - - &:last-child { - border-right: 0; - } - - &.right { - text-align: right; - } -} - -.frost-table { - table-layout: fixed; - - &-header { - background-color: $frost-color-white; - color: $frost-color-grey-4; - font-weight: bold; - - &-cell { - @include table-cell; - min-width: 40px; - height: $frost-table-header-height; - } - } - - &-body { - &-cell { - @include table-cell; - } - } - - &-row { - &:nth-child(even) { - background-color: $frost-color-white; - } - - &:nth-child(odd) { - background-color: $frost-color-lgrey-3; - } - - // NOTE: keep the .even and .odd below the :nth-child() rules so they can override the default nth-row behavior - &.even { - background-color: $frost-color-white; - } - - &.odd { - background-color: $frost-color-lgrey-3; - } - } -} +@import 'mixins'; +@import 'frost-table'; +@import 'frost-fixed-table'; +@import 'perfect-scrollbar'; diff --git a/bower.json b/bower.json index 10c84c9..7fef768 100644 --- a/bower.json +++ b/bower.json @@ -1,10 +1,11 @@ { "name": "ember-frost-table", "dependencies": { - "ember": "^2.8.0", + "ember": "~2.8.0", "ember-cli-shims": "^0.1.3", "ember-mocha-adapter": "~0.3.1", "perfect-scrollbar": ">=0.6.7 <2.0.0", - "chai-jquery": "^2.0.1" + "chai-jquery": "^2.0.1", + "sinon-chai": "^2.8.0" } } diff --git a/config/ember-try.js b/config/ember-try.js index 0f920ef..9cfad93 100644 --- a/config/ember-try.js +++ b/config/ember-try.js @@ -106,13 +106,13 @@ module.exports = { } }, { - name: 'ember-2-7', + name: 'ember-2-8', bower: { dependencies: { - 'ember': '~2.7.0' + 'ember': '~2.8.0' }, resolutions: { - 'ember': '~2.7.0' + 'ember': '~2.8.0' } } }, diff --git a/ember-cli-build.js b/ember-cli-build.js index 5ee4fad..1cb1098 100644 --- a/ember-cli-build.js +++ b/ember-cli-build.js @@ -24,7 +24,8 @@ module.exports = function (defaults) { if (app.env === 'test') { ;[ - 'bower_components/chai-jquery/chai-jquery.js' + 'bower_components/chai-jquery/chai-jquery.js', + 'bower_components/sinon-chai/lib/sinon-chai.js' ].forEach((path) => { app.import(path, {type: 'test'}) }) diff --git a/package.json b/package.json index cebac47..16b6f95 100644 --- a/package.json +++ b/package.json @@ -2,54 +2,56 @@ "name": "ember-frost-table", "version": "0.1.0", "description": "A simple table component", - "license": "MIT", - "author": "Adam Meadows (https://github.com/job13er)", - "contributors": [], "directories": { "doc": "doc", "test": "tests" }, - "repository": "git@github.com:ciena-frost/ember-frost-table.git", "scripts": { "build": "ember build", "start": "ember server", + "ci-test": "ember try:one $EMBER_TRY_SCENARIO --- COVERAGE=true ember test", "test": "npm run lint && COVERAGE=true ember test", "eslint": "eslint *.js addon app blueprints config tests", "md-lint": "find . -name '*.md' -depth 1 | grep -v CHANGELOG | xargs remark", "sass-lint": "sass-lint -q -v", "lint": "npm run eslint && npm run sass-lint && npm run md-lint" }, + "repository": "git@github.com:ciena-frost/ember-frost-table.git", "engines": { "node": ">= 6.0.0" }, + "author": "Adam Meadows (https://github.com/job13er)", + "contributors": [], + "license": "MIT", "devDependencies": { "broccoli-asset-rev": "^2.4.5", - "ember-cli": "^2.8.0", - "ember-cli-app-version": "^2.0.0", + "ember-cli": "~2.8.0", + "ember-cli-chai": "^0.2.0", + "ember-cli-code-coverage": "0.3.5", "ember-cli-dependency-checker": "^1.3.0", "ember-cli-htmlbars-inline-precompile": "^0.3.1", "ember-cli-inject-live-reload": "^1.4.1", - "ember-cli-mocha": "^0.11.0", - "ember-cli-sri": "^2.1.0", + "ember-cli-mocha": "^0.13.0", "ember-cli-template-lint": "^0.5.0", "ember-cli-test-loader": "^1.1.0", "ember-cli-uglify": "^1.2.0", "ember-code-snippet": "1.8.0", - "ember-computed-decorators": "^0.2.2", - "ember-concurrency": "^0.7.15", - "ember-data": "^2.8.0", - "ember-disable-prototype-extensions": "^1.1.0", - "ember-elsewhere": "^0.4.1", + "ember-computed-decorators": "~0.2.2", + "ember-concurrency": "~0.7.15", + "ember-data": "~2.8.0", + "ember-disable-proxy-controllers": "^1.0.1", + "ember-elsewhere": "~0.4.1", "ember-export-application-global": "^1.0.5", - "ember-frost-core": "^1.0.3", + "ember-frost-core": "^1.2.1", "ember-get-config": "^0.1.11", - "ember-hook": "job13er/ember-hook#make-hook-helper-extendible", - "ember-load-initializers": "^0.5.1", + "ember-hook": "^1.3.5", + "ember-load-initializers": "^0.6.0", "ember-lodash-shim": "1.0.1", "ember-perfectscroll": "0.1.12", + "ember-prop-types": "^3.2.0", "ember-resolver": "^2.0.3", - "ember-sinon": "^0.5.1", - "ember-test-utils": "^1.1.2", + "ember-sinon": "^0.6.0", + "ember-test-utils": "^1.3.1", "ember-truth-helpers": "^1.2.0", "eslint": "^3.4.0", "eslint-config-frost-standard": "^5.0.0", @@ -62,7 +64,7 @@ "dependencies": { "ember-cli-babel": "^5.1.6", "ember-cli-htmlbars": "^1.0.11", - "ember-cli-sass": "^5.5.2" + "ember-cli-sass": "^5.6.0" }, "keywords": [ "ember-addon", @@ -71,4 +73,4 @@ "ember-addon": { "configPath": "tests/dummy/config" } -} \ No newline at end of file +} diff --git a/tests/dummy/app/pods/demo/frost-fixed-table/controller.js b/tests/dummy/app/pods/demo/frost-fixed-table/controller.js new file mode 100644 index 0000000..4a2ce4f --- /dev/null +++ b/tests/dummy/app/pods/demo/frost-fixed-table/controller.js @@ -0,0 +1,2 @@ +import HeroesController from '../heroes-controller' +export default HeroesController.extend({}) diff --git a/tests/dummy/app/pods/demo/frost-fixed-table/template.hbs b/tests/dummy/app/pods/demo/frost-fixed-table/template.hbs new file mode 100644 index 0000000..684f4ff --- /dev/null +++ b/tests/dummy/app/pods/demo/frost-fixed-table/template.hbs @@ -0,0 +1,81 @@ +{{! template-lint-disable bare-strings }} +{{!-- BEGIN-SNIPPET fixed-table-api }} + {{frost-fixed-table + columns=(array + (hash + className= // e.g. 'name-column' + label= // e.g. 'Name' + propertyName= // e.g. 'name' + ) + ) + hook= // e.g. 'myTable' + items=(array + (hash + name='Jane Doe' + ) + (hash + name='John Doe' + ) + ) + }} +{{ END-SNIPPET --}} + + +
+
+ API +
+
+ {{code-snippet name='fixed-table-api.hbs'}} +
+
+
+
+ Live demo +
+
+ {{code-snippet name='fixed-table.hbs'}} +
+
+ {{! BEGIN-SNIPPET fixed-table }} + {{frost-fixed-table + columns=(array + (hash + className='name-col' + frozen=true + label='Name' + propertyName='name' + ) + (hash + className='real-name-col' + label='Real Name' + propertyName='realName' + ) + (hash + className='real-name-col' + label='Real Name' + propertyName='realName' + ) + (hash + className='real-name-col' + label='Real Name' + propertyName='realName' + ) + (hash + className='real-name-col' + label='Real Name' + propertyName='realName' + ) + (hash + className='universe-col' + frozen=true + label='Universe' + propertyName='universe' + ) + ) + hook='myTable' + items=heroes + }} + {{! END-SNIPPET }} +
+
diff --git a/tests/dummy/app/pods/demo/template.hbs b/tests/dummy/app/pods/demo/template.hbs index 3b85e84..c184bd8 100644 --- a/tests/dummy/app/pods/demo/template.hbs +++ b/tests/dummy/app/pods/demo/template.hbs @@ -8,6 +8,7 @@ {{#link-to 'demo.overview'}}Overview{{/link-to}}
Components + {{#link-to 'demo.frost-fixed-table'}}frost-fixed-table{{/link-to}} {{#link-to 'demo.frost-table'}}frost-table{{/link-to}} {{#link-to 'demo.frost-table-header'}}frost-table-header{{/link-to}} {{#link-to 'demo.frost-table-body'}}frost-table-body{{/link-to}} diff --git a/tests/dummy/app/router.js b/tests/dummy/app/router.js index f649b0a..f49d6b2 100644 --- a/tests/dummy/app/router.js +++ b/tests/dummy/app/router.js @@ -8,6 +8,7 @@ const Router = Ember.Router.extend({ Router.map(function () { this.route('demo', { path: '/' }, function () { this.route('overview', { path: '/' }) + this.route('frost-fixed-table') this.route('frost-table') this.route('frost-table-body') this.route('frost-table-header') diff --git a/tests/dummy/app/styles/app.scss b/tests/dummy/app/styles/app.scss index 4058b49..5ec4d95 100644 --- a/tests/dummy/app/styles/app.scss +++ b/tests/dummy/app/styles/app.scss @@ -155,31 +155,30 @@ dl { height: 40px; } +.frost-table-demo { + padding: 10px ; +} + +.frost-fixed-table-api { + max-width: 500px; +} // Table example styles // BEGIN-SNIPPET hero-styles -.frost-table-body-row-cell > span { - height: 50px; +.frost-fixed-table { + max-width: 500px; + max-height: 500px; } -.name-col > span { - display: inline-block; +.name-col { width: 100px; } -.real-name-col > span { - display: inline-block; - width: 150px; -} - -.universe-col > span { - display: inline-block; - width: 50px; +.real-name-col { + width: 175px; } -.top, -.bottom { - width: 500px; - overflow: auto; +.universe-col { + width: 90px; } // END-SNIPPET diff --git a/tests/dummy/config/environment.js b/tests/dummy/config/environment.js index 07283b8..ee9c178 100644 --- a/tests/dummy/config/environment.js +++ b/tests/dummy/config/environment.js @@ -40,7 +40,7 @@ module.exports = function (environment) { if (environment === 'production') { ENV.locationType = 'hash' - ENV.rootURL = '/ember-frost-modal' + ENV.rootURL = '/ember-frost-table' } return ENV diff --git a/tests/helpers/selector-stub.js b/tests/helpers/selector-stub.js new file mode 100644 index 0000000..186280f --- /dev/null +++ b/tests/helpers/selector-stub.js @@ -0,0 +1,31 @@ +/** + * Test helper to stub a jQuery selector (something returned by this.$() within a component) + * TODO: maybe move to ember-test-utils + */ + +import sinon from 'sinon' + +const defaultStubbedMethods = [ + 'css', + 'find', + 'on', + 'scrollTop', + 'scrollLeft' +] + +/** + * @param {String[]} stubbedMethods - the names of methods to stub on this selector stub + * @returns {*} a new selector stub + */ +export function createSelectorStub (...stubbedMethods) { + const stub = {} + if (stubbedMethods.length === 0) { + stubbedMethods = defaultStubbedMethods + } + + stubbedMethods.forEach((method) => { + stub[method] = sinon.stub() + }) + + return stub +} diff --git a/tests/integration/components/data.js b/tests/integration/components/data.js index c8e1d07..8abb8fc 100644 --- a/tests/integration/components/data.js +++ b/tests/integration/components/data.js @@ -15,6 +15,41 @@ export const columns = [ } ] +export const fixedColumns = [ + { + className: 'name-col', + frozen: true, + label: 'Name', + propertyName: 'name' + }, + { + className: 'real-name-col', + label: 'Real Name', + propertyName: 'realName' + }, + { + className: 'real-name-col', + label: 'Real Name', + propertyName: 'realName' + }, + { + className: 'real-name-col', + label: 'Real Name', + propertyName: 'realName' + }, + { + className: 'real-name-col', + label: 'Real Name', + propertyName: 'realName' + }, + { + className: 'universe-col', + frozen: true, + label: 'Universe', + propertyName: 'universe' + } +] + export const heroes = [ { name: 'Superman', diff --git a/tests/integration/components/frost-fixed-table-test.js b/tests/integration/components/frost-fixed-table-test.js new file mode 100644 index 0000000..78d82e2 --- /dev/null +++ b/tests/integration/components/frost-fixed-table-test.js @@ -0,0 +1,330 @@ +/** + * Integration test for the frost-fixed-table component + */ + +import {expect} from 'chai' +import hbs from 'htmlbars-inline-precompile' +import {$hook} from 'ember-hook' +import wait from 'ember-test-helpers/wait' +import {afterEach, beforeEach, describe, it} from 'mocha' +import sinon from 'sinon' + +import {integration} from 'dummy/tests/helpers/ember-test-utils/setup-component-test' +import {fixedColumns, heroes} from './data' + +const test = integration('frost-fixed-table') +describe(test.label, function () { + test.setup() + + let sandbox + + beforeEach(function () { + sandbox = sinon.sandbox.create() + this.setProperties({ + fixedColumns, + heroes, + myHook: 'myTable' + }) + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('after render', function () { + beforeEach(function () { + this.render(hbs` + {{frost-fixed-table + columns=fixedColumns + hook=myHook + items=heroes + }} + `) + + return wait() + }) + + it('should create the header', function () { + expect(this.$('.frost-fixed-table-header')).to.have.length(1) + }) + + it('should create the body', function () { + expect(this.$('.frost-fixed-table-body')).to.have.length(1) + }) + + it('should set the hook of the body', function () { + expect($hook('myTable-body')).to.have.class('frost-fixed-table-body') + }) + + it('should not use ', function () { + expect(this.$('tbody')).to.have.length(0) + }) + + describe('the header', function () { + let $header + beforeEach(function () { + $header = $hook('myTable-header') + }) + + it('should be accessible via a hook', function () { + expect($header).to.have.length(1) + }) + + it('should create a left section', function () { + expect($header.find('.frost-fixed-table-header-left')).to.have.length(1) + }) + + it('should create a middle section', function () { + expect($header.find('.frost-fixed-table-header-middle')).to.have.length(1) + }) + + it('should create a right section', function () { + expect($header.find('.frost-fixed-table-header-right')).to.have.length(1) + }) + + it('should have a header cell per column', function () { + expect($header.find('.frost-table-header-cell')).to.have.length(fixedColumns.length) + }) + + describe('the left section', function () { + let $leftWrapper, $left + beforeEach(function () { + $leftWrapper = $header.find('.frost-fixed-table-header-left') + $left = $hook('myTable-header-left') + }) + + it('should be accessible via a hook', function () { + expect($left).to.have.length(1) + }) + + it('should live within the wrapper', function () { + expect($left.closest('.frost-fixed-table-header-left')).to.have.length(1) + }) + + it('should not have a frost-scroll wrapper', function () { + expect($leftWrapper.find('.frost-scroll')).to.have.length(0) + }) + + it('should be a frost-table-header', function () { + expect($left).to.have.class('frost-table-header') + }) + + it('should not use ', function () { + expect($leftWrapper.find('thead')).to.have.length(0) + }) + + it('should not use ', function () { + expect($leftWrapper.find('th')).to.have.length(0) + }) + }) + + describe('the middle section', function () { + let $middleWrapper, $middle + beforeEach(function () { + $middleWrapper = $header.find('.frost-fixed-table-header-middle') + $middle = $hook('myTable-header-middle') + }) + + it('should be accessible via a hook', function () { + expect($middle).to.have.length(1) + }) + + it('should live within the wrapper', function () { + expect($middle.closest('.frost-fixed-table-header-middle')).to.have.length(1) + }) + + it('should have a frost-scroll wrapper', function () { + expect($middleWrapper.find('.frost-scroll')).to.have.length(1) + }) + + it('should be a frost-table-header', function () { + expect($middle).to.have.class('frost-table-header') + }) + + it('should not use ', function () { + expect($middleWrapper.find('thead')).to.have.length(0) + }) + + it('should not use ', function () { + expect($middleWrapper.find('th')).to.have.length(0) + }) + }) + + describe('the right section', function () { + let $rightWrapper, $right + beforeEach(function () { + $rightWrapper = $header.find('.frost-fixed-table-header-right') + $right = $hook('myTable-header-right') + }) + + it('should be accessible via a hook', function () { + expect($right).to.have.length(1) + }) + + it('should live within the wrapper', function () { + expect($right.closest('.frost-fixed-table-header-right')).to.have.length(1) + }) + + it('should not have a frost-scroll wrapper', function () { + expect($rightWrapper.find('.frost-scroll')).to.have.length(0) + }) + + it('should be a frost-table-header', function () { + expect($right).to.have.class('frost-table-header') + }) + + it('should not use ', function () { + expect($rightWrapper.find('thead')).to.have.length(0) + }) + + it('should not use ', function () { + expect($rightWrapper.find('th')).to.have.length(0) + }) + }) + }) + + describe('the body', function () { + let $body + beforeEach(function () { + $body = $hook('myTable-body') + }) + + it('should be accessible via a hook', function () { + expect($body).to.have.length(1) + }) + + it('should create a left section', function () { + expect($body.find('.frost-fixed-table-left')).to.have.length(1) + }) + + it('should create a middle section', function () { + expect($body.find('.frost-fixed-table-middle')).to.have.length(1) + }) + + it('should create a right section', function () { + expect($body.find('.frost-fixed-table-right')).to.have.length(1) + }) + + it('should have a three rows per item', function () { + expect($body.find('.frost-table-row')).to.have.length(heroes.length * 3) + }) + + describe('the left section', function () { + const leftColumns = fixedColumns.slice(0, 1) + let $leftWrapper, $left + + beforeEach(function () { + $leftWrapper = $body.find('.frost-fixed-table-left') + $left = $hook('myTable-left') + }) + + it('should have a cell for each left column for each item', function () { + expect($left.find('.frost-table-cell')).to.have.length(heroes.length * leftColumns.length) + }) + + heroes.forEach((hero, rowIndex) => { + leftColumns.forEach((column, columnIndex) => { + it(`should set a hook on the cell in row: ${rowIndex}, column: ${columnIndex}`, function () { + const $cell = $hook('myTable-left-cell', {row: rowIndex, column: columnIndex}) + expect($cell.text().trim()).to.equal(hero[column.propertyName]) + }) + }) + }) + + it('should live within the wrapper', function () { + expect($left.closest('.frost-fixed-table-left')).to.have.length(1) + }) + + it('should have a frost-scroll wrapper', function () { + expect($leftWrapper.find('.frost-scroll')).to.have.length(1) + }) + + it('should be a frost-table-row', function () { + expect($left).to.have.class('frost-table-row') + }) + + it('should not use ', function () { + expect($leftWrapper.find('td')).to.have.length(0) + }) + }) + + describe('the middle section', function () { + const middleColumns = fixedColumns.slice(1, 5) + let $middleWrapper, $middle + + beforeEach(function () { + $middleWrapper = $body.find('.frost-fixed-table-middle') + $middle = $hook('myTable-middle') + }) + + it('should have a cell for each middle column for each item', function () { + expect($middle.find('.frost-table-cell')).to.have.length(heroes.length * middleColumns.length) + }) + + heroes.forEach((hero, rowIndex) => { + middleColumns.forEach((column, columnIndex) => { + it(`should set a hook on the cell in row: ${rowIndex}, column: ${columnIndex}`, function () { + const $cell = $hook('myTable-middle-cell', {row: rowIndex, column: columnIndex}) + expect($cell.text().trim()).to.equal(hero[column.propertyName]) + }) + }) + }) + + it('should live within the wrapper', function () { + expect($middle.closest('.frost-fixed-table-middle')).to.have.length(1) + }) + + it('should have a frost-scroll wrapper', function () { + expect($middleWrapper.find('.frost-scroll')).to.have.length(1) + }) + + it('should be a frost-table-row', function () { + expect($middle).to.have.class('frost-table-row') + }) + + it('should not use ', function () { + expect($middleWrapper.find('td')).to.have.length(0) + }) + }) + + describe('the right section', function () { + const rightColumns = fixedColumns.slice(5) + let $rightWrapper, $right + + beforeEach(function () { + $rightWrapper = $body.find('.frost-fixed-table-right') + $right = $hook('myTable-right') + }) + + it('should have a cell for each right column for each item', function () { + expect($right.find('.frost-table-cell')).to.have.length(heroes.length * rightColumns.length) + }) + + heroes.forEach((hero, rowIndex) => { + rightColumns.forEach((column, columnIndex) => { + it(`should set a hook on the cell in row: ${rowIndex}, column: ${columnIndex}`, function () { + const $cell = $hook('myTable-right-cell', {row: rowIndex, column: columnIndex}) + expect($cell.text().trim()).to.equal(hero[column.propertyName]) + }) + }) + }) + + it('should live within the wrapper', function () { + expect($right.closest('.frost-fixed-table-right')).to.have.length(1) + }) + + it('should have a frost-scroll wrapper', function () { + expect($rightWrapper.find('.frost-scroll')).to.have.length(1) + }) + + it('should be a frost-table-row', function () { + expect($right).to.have.class('frost-table-row') + }) + + it('should not use ', function () { + expect($rightWrapper.find('td')).to.have.length(0) + }) + }) + }) + }) +}) diff --git a/tests/integration/components/frost-table-body-test.js b/tests/integration/components/frost-table-body-test.js index b3ee80c..b5cea69 100644 --- a/tests/integration/components/frost-table-body-test.js +++ b/tests/integration/components/frost-table-body-test.js @@ -5,18 +5,19 @@ import {expect} from 'chai' import Ember from 'ember' const {$, get} = Ember -import {describeComponent, it} from 'ember-mocha' -import hbs from 'htmlbars-inline-precompile' -import {$hook, initialize as initializeHook} from 'ember-hook' +import {$hook} from 'ember-hook' import wait from 'ember-test-helpers/wait' -import {beforeEach, describe} from 'mocha' +import hbs from 'htmlbars-inline-precompile' +import {beforeEach, describe, it} from 'mocha' -import {integration} from 'dummy/tests/helpers/ember-test-utils/describe-component' +import {integration} from 'dummy/tests/helpers/ember-test-utils/setup-component-test' import {columns, heroes} from './data' -describeComponent(...integration('frost-table-body'), function () { +const test = integration('frost-table-body') +describe(test.label, function () { + test.setup() + beforeEach(function () { - initializeHook() this.setProperties({ columns, heroes, diff --git a/tests/integration/components/frost-table-cell-test.js b/tests/integration/components/frost-table-cell-test.js new file mode 100644 index 0000000..0b7b764 --- /dev/null +++ b/tests/integration/components/frost-table-cell-test.js @@ -0,0 +1,56 @@ +/** + * Integration test for the frost-table-cell component + */ + +import {expect} from 'chai' +import {$hook} from 'ember-hook' +import wait from 'ember-test-helpers/wait' +import hbs from 'htmlbars-inline-precompile' +import {afterEach, beforeEach, describe, it} from 'mocha' +import sinon from 'sinon' + +import {integration} from 'dummy/tests/helpers/ember-test-utils/setup-component-test' + +const test = integration('frost-table-cell') +describe(test.label, function () { + test.setup() + + let sandbox + + beforeEach(function () { + sandbox = sinon.sandbox.create() + }) + + afterEach(function () { + sandbox.restore() + }) + + // FIXME: actually add real tests in next PR when frost-table-cell supports custom renderer components + it.skip('should have real tests', function () { + expect(true).to.equal(false) + }) + + describe('after render', function () { + beforeEach(function () { + this.setProperties({ + myHook: 'myThing' + }) + + this.render(hbs` + {{frost-table-cell + hook=myHook + }} + `) + + return wait() + }) + + it('should have an element', function () { + expect(this.$()).to.have.length(1) + }) + + it('should be accessible via the hook', function () { + expect($hook('myThing')).to.have.length(1) + }) + }) +}) diff --git a/tests/integration/components/frost-table-header-test.js b/tests/integration/components/frost-table-header-test.js index 8e86b2b..8abfd98 100644 --- a/tests/integration/components/frost-table-header-test.js +++ b/tests/integration/components/frost-table-header-test.js @@ -5,18 +5,19 @@ import {expect} from 'chai' import Ember from 'ember' const {$} = Ember -import {describeComponent, it} from 'ember-mocha' -import hbs from 'htmlbars-inline-precompile' -import {$hook, initialize as initializeHook} from 'ember-hook' +import {$hook} from 'ember-hook' import wait from 'ember-test-helpers/wait' -import {beforeEach, describe} from 'mocha' +import hbs from 'htmlbars-inline-precompile' +import {beforeEach, describe, it} from 'mocha' -import {integration} from 'dummy/tests/helpers/ember-test-utils/describe-component' +import {integration} from 'dummy/tests/helpers/ember-test-utils/setup-component-test' import {columns} from './data' -describeComponent(...integration('frost-table-header'), function () { +const test = integration('frost-table-header') +describe(test.label, function () { + test.setup() + beforeEach(function () { - initializeHook() this.setProperties({ columns, myHook: 'myTableHeader' diff --git a/tests/integration/components/frost-table-row-test.js b/tests/integration/components/frost-table-row-test.js index e9f82db..61dbbac 100644 --- a/tests/integration/components/frost-table-row-test.js +++ b/tests/integration/components/frost-table-row-test.js @@ -5,18 +5,19 @@ import {expect} from 'chai' import Ember from 'ember' const {$, get} = Ember -import {describeComponent, it} from 'ember-mocha' import hbs from 'htmlbars-inline-precompile' -import {$hook, initialize as initializeHook} from 'ember-hook' +import {$hook} from 'ember-hook' import wait from 'ember-test-helpers/wait' -import {beforeEach, describe} from 'mocha' +import {beforeEach, describe, it} from 'mocha' -import {integration} from 'dummy/tests/helpers/ember-test-utils/describe-component' +import {integration} from 'dummy/tests/helpers/ember-test-utils/setup-component-test' import {columns, heroes} from './data' -describeComponent(...integration('frost-table-body-row'), function () { +const test = integration('frost-table-body-row') +describe(test.label, function () { + test.setup() + beforeEach(function () { - initializeHook() this.setProperties({ columns, hero: heroes[2], diff --git a/tests/integration/components/frost-table-test.js b/tests/integration/components/frost-table-test.js index 3af60a4..4498088 100644 --- a/tests/integration/components/frost-table-test.js +++ b/tests/integration/components/frost-table-test.js @@ -5,18 +5,19 @@ import {expect} from 'chai' import Ember from 'ember' const {$, get} = Ember -import {describeComponent, it} from 'ember-mocha' import hbs from 'htmlbars-inline-precompile' -import {$hook, initialize as initializeHook} from 'ember-hook' +import {$hook} from 'ember-hook' import wait from 'ember-test-helpers/wait' -import {beforeEach, describe} from 'mocha' +import {beforeEach, describe, it} from 'mocha' -import {integration} from 'dummy/tests/helpers/ember-test-utils/describe-component' +import {integration} from 'dummy/tests/helpers/ember-test-utils/setup-component-test' import {columns, heroes} from './data' -describeComponent(...integration('frost-table'), function () { +const test = integration('frost-table') +describe(test.label, function () { + test.setup() + beforeEach(function () { - initializeHook() this.setProperties({ columns, heroes, diff --git a/tests/unit/.gitkeep b/tests/unit/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/unit/components/frost-fixed-table-test.js b/tests/unit/components/frost-fixed-table-test.js new file mode 100644 index 0000000..0e2b743 --- /dev/null +++ b/tests/unit/components/frost-fixed-table-test.js @@ -0,0 +1,558 @@ +/** + * Unit test for the frost-fixed-table component + * + * NOTE: Since it is not easy to properly set up an integration test to confirm some of the DOM + * caluclations happening in frost-fixed-table, I opted to unit test these calculations, making these + * tests a little more tied to the implementation than I'd like. However, given the hoops needed to jump through to + * simulate external CSS as well as scroll and mouse events, this seemed the better option (ARM 2016-12-13) + */ + +import {expect} from 'chai' +import {afterEach, beforeEach, describe, it} from 'mocha' +import sinon from 'sinon' + +import {unit} from 'dummy/tests/helpers/ember-test-utils/setup-component-test' +import {createSelectorStub} from 'dummy/tests/helpers/selector-stub' + +const test = unit('frost-fixed-table') +describe(test.label, function () { + test.setup() + + let component, columns, sandbox + + beforeEach(function () { + sandbox = sinon.sandbox.create() + component = this.subject({tagName: 'div'}) + columns = [ + { + frozen: true, + propertyName: 'name' + }, + { + frozen: true, + propertyName: 'description' + }, + { + propertyName: 'info1' + }, + { + propertyName: 'info2' + }, + { + propertyName: 'info3' + }, + { + frozen: true, + propertyName: 'summary1' + }, + { + frozen: true, + propertyName: 'summary2' + } + ] + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('Computed Properties', function () { + const cpExpectedValues = { + bodyLeftSelector: '.frost-fixed-table-left .frost-scroll', + bodyMiddleSelector: '.frost-fixed-table-middle .frost-scroll', + bodyRightSelector: '.frost-fixed-table-right .frost-scroll', + headerMiddleSelector: '.frost-fixed-table-header-middle .frost-scroll' + } + + for (let key in cpExpectedValues) { + describe(key, function () { + let value + beforeEach(function () { + value = component.get(key) + }) + + it('should return the expected value', function () { + expect(value).to.equal(cpExpectedValues[key]) + }) + }) + } + + describe('leftColumns', function () { + describe('with properly ordered columns', function () { + beforeEach(function () { + component.setProperties({columns}) + }) + + it('should have the first frozen columns', function () { + expect(component.get('leftColumns')).to.eql(columns.slice(0, 2)) + }) + }) + + describe('with no leading frozen columns', function () { + beforeEach(function () { + component.set('columns', columns.slice(2)) + }) + + it('should be empty', function () { + expect(component.get('leftColumns')).to.eql([]) + }) + }) + }) + + describe('middleColumns', function () { + describe('with properly ordered columns', function () { + beforeEach(function () { + component.setProperties({columns}) + }) + + it('should have the middle non-frozen columns', function () { + expect(component.get('middleColumns')).to.eql(columns.slice(2, 5)) + }) + }) + + describe('with no leading frozen columns', function () { + beforeEach(function () { + component.set('columns', columns.slice(2)) + }) + + it('should have the first non-frozen columns', function () { + expect(component.get('middleColumns')).to.eql(columns.slice(2, 5)) + }) + }) + + describe('with no trailing frozen columns', function () { + beforeEach(function () { + component.set('columns', columns.slice(0, 5)) + }) + + it('should have the last non-frozen columns', function () { + expect(component.get('middleColumns')).to.eql(columns.slice(2, 5)) + }) + }) + + describe('with no unfrozen columns', function () { + beforeEach(function () { + component.set('columns', columns.slice(0, 2)) + }) + + it('should be empty', function () { + expect(component.get('middleColumns')).to.eql([]) + }) + }) + }) + + describe('rightColumns', function () { + describe('with properly ordered columns', function () { + beforeEach(function () { + component.setProperties({columns}) + }) + + it('should have the last frozen columns', function () { + expect(component.get('rightColumns')).to.eql(columns.slice(-2)) + }) + }) + + describe('with no trailing frozen columns', function () { + beforeEach(function () { + component.set('columns', columns.slice(0, 5)) + }) + + it('should be empty', function () { + expect(component.get('rightColumns')).to.eql([]) + }) + }) + }) + }) + + describe('.didRender()', function () { + beforeEach(function () { + sandbox.stub(component, 'setupBodyHeights') + sandbox.stub(component, 'setupHoverProxy') + sandbox.stub(component, 'setupMiddleMargins') + sandbox.stub(component, 'setupMiddleWidths') + sandbox.stub(component, 'setupScrollSync') + + component.didRender() + }) + + it('should set up the body heights', function () { + expect(component.setupBodyHeights).to.have.callCount(1) + }) + + it('should set up the hover proxy', function () { + expect(component.setupHoverProxy).to.have.callCount(1) + }) + + it('should set up the middle margins', function () { + expect(component.setupMiddleMargins).to.have.callCount(1) + }) + + it('should set up the middle widths', function () { + expect(component.setupMiddleWidths).to.have.callCount(1) + }) + + it('should set up the scroll syncing', function () { + expect(component.setupScrollSync).to.have.callCount(1) + }) + }) + + describe('._calculateWidth()', function () { + // TODO: add tests, need to figure out how to stub Ember.$ properly for this one + + }) + + describe('.setupBodyHeights()', function () { + let leftBodyStub, middleHeaderStub, middleBodyStub, rightBodyStub, tableStub + beforeEach(function () { + leftBodyStub = createSelectorStub('css') + middleBodyStub = createSelectorStub('css') + middleHeaderStub = createSelectorStub('outerHeight') + rightBodyStub = createSelectorStub('css') + tableStub = createSelectorStub('outerHeight') + + sandbox.stub(component, '$') + .withArgs('.frost-fixed-table-left .frost-scroll').returns(leftBodyStub) + .withArgs('.frost-fixed-table-middle .frost-scroll').returns(middleBodyStub) + .withArgs('.frost-fixed-table-header-middle .frost-scroll').returns(middleHeaderStub) + .withArgs('.frost-fixed-table-right .frost-scroll').returns(rightBodyStub) + .withArgs().returns(tableStub) + + tableStub.outerHeight.returns(100) + middleHeaderStub.outerHeight.returns(20) + + component.setupBodyHeights() + }) + + it('should set the height of the left body', function () { + expect(leftBodyStub.css).to.have.been.calledWith({height: '80px'}) + }) + + it('should set the height of the middle body', function () { + expect(middleBodyStub.css).to.have.been.calledWith({height: '80px'}) + }) + + it('should set the height of the right body', function () { + expect(rightBodyStub.css).to.have.been.calledWith({height: '80px'}) + }) + }) + + describe('.setupHoverProxy()', function () { + let leftBodyStub, middleHeaderStub, middleBodyStub, rightBodyStub + let leftBodyMouseEnterHandler, leftBodyMouseLeaveHandler + let middleBodyMouseEnterHandler, middleBodyMouseLeaveHandler + let middleHeaderMouseEnterHandler, middleHeaderMouseLeaveHandler + + beforeEach(function () { + leftBodyStub = createSelectorStub('on') + middleBodyStub = createSelectorStub('on', 'addClass', 'removeClass') + middleHeaderStub = createSelectorStub('on') + rightBodyStub = createSelectorStub('addClass', 'removeClass') + + sandbox.stub(component, '$') + .withArgs('.frost-fixed-table-left .frost-scroll').returns(leftBodyStub) + .withArgs('.frost-fixed-table-middle .frost-scroll').returns(middleBodyStub) + .withArgs('.frost-fixed-table-header-middle .frost-scroll').returns(middleHeaderStub) + .withArgs('.frost-fixed-table-right .frost-scroll').returns(rightBodyStub) + + component.setupHoverProxy() + + // capture the event handlers + leftBodyMouseEnterHandler = leftBodyStub.on.getCall(0).args[1] + leftBodyMouseLeaveHandler = leftBodyStub.on.getCall(1).args[1] + middleBodyMouseEnterHandler = middleBodyStub.on.getCall(0).args[1] + middleBodyMouseLeaveHandler = middleBodyStub.on.getCall(1).args[1] + middleHeaderMouseEnterHandler = middleHeaderStub.on.getCall(0).args[1] + middleHeaderMouseLeaveHandler = middleHeaderStub.on.getCall(1).args[1] + }) + + it('should add mouseenter handler to left body', function () { + expect(leftBodyStub.on).to.have.been.calledWith('mouseenter', sinon.match.func) + }) + + it('should add mouseleave handler to left body', function () { + expect(leftBodyStub.on).to.have.been.calledWith('mouseleave', sinon.match.func) + }) + + it('should add mouseenter handler to middle body', function () { + expect(middleBodyStub.on).to.have.been.calledWith('mouseenter', sinon.match.func) + }) + + it('should add mouseleave handler to middle body', function () { + expect(middleBodyStub.on).to.have.been.calledWith('mouseleave', sinon.match.func) + }) + + it('should add mouseenter handler to middle header', function () { + expect(middleHeaderStub.on).to.have.been.calledWith('mouseenter', sinon.match.func) + }) + + it('should add mouseleave handler to middle header', function () { + expect(middleHeaderStub.on).to.have.been.calledWith('mouseleave', sinon.match.func) + }) + + describe('when left body is hovered', function () { + beforeEach(function () { + leftBodyMouseEnterHandler() + }) + + it('should add the hover class to the right body', function () { + expect(rightBodyStub.addClass).to.have.been.calledWith('ps-container-hover') + }) + }) + + describe('when left body is un-hovered', function () { + beforeEach(function () { + leftBodyMouseLeaveHandler() + }) + + it('should remove the hover class from the right body', function () { + expect(rightBodyStub.removeClass).to.have.been.calledWith('ps-container-hover') + }) + }) + + describe('when middle body is hovered', function () { + beforeEach(function () { + middleBodyMouseEnterHandler() + }) + + it('should add the hover class to the right body', function () { + expect(rightBodyStub.addClass).to.have.been.calledWith('ps-container-hover') + }) + }) + + describe('when middle body is un-hovered', function () { + beforeEach(function () { + middleBodyMouseLeaveHandler() + }) + + it('should remove the hover class from the right body', function () { + expect(rightBodyStub.removeClass).to.have.been.calledWith('ps-container-hover') + }) + }) + + describe('when middle header is hovered', function () { + beforeEach(function () { + middleHeaderMouseEnterHandler() + }) + + it('should add the hover class to the middle body', function () { + expect(middleBodyStub.addClass).to.have.been.calledWith('ps-container-hover') + }) + }) + + describe('when middle header is un-hovered', function () { + beforeEach(function () { + middleHeaderMouseLeaveHandler() + }) + + it('should remove the hover class from the middle body', function () { + expect(middleBodyStub.removeClass).to.have.been.calledWith('ps-container-hover') + }) + }) + }) + + describe('.setupMiddleMargins()', function () { + let leftBodyStub, middleHeaderStub, middleBodyStub, rightBodyStub + beforeEach(function () { + leftBodyStub = createSelectorStub('outerWidth') + middleBodyStub = createSelectorStub('css') + middleHeaderStub = createSelectorStub('css') + rightBodyStub = createSelectorStub('outerWidth') + + sandbox.stub(component, '$') + .withArgs('.frost-fixed-table-left .frost-scroll').returns(leftBodyStub) + .withArgs('.frost-fixed-table-middle .frost-scroll').returns(middleBodyStub) + .withArgs('.frost-fixed-table-header-middle .frost-scroll').returns(middleHeaderStub) + .withArgs('.frost-fixed-table-right .frost-scroll').returns(rightBodyStub) + + leftBodyStub.outerWidth.returns(123) + rightBodyStub.outerWidth.returns(321) + + component.setupMiddleMargins() + }) + + it('should set proper margins on the middle header', function () { + expect(middleHeaderStub.css).to.have.been.calledWith({ + 'margin-left': '123px', + 'margin-right': '321px' + }) + }) + + it('should set proper margins on the middle body', function () { + expect(middleBodyStub.css).to.have.been.calledWith({ + 'margin-left': '123px', + 'margin-right': '321px' + }) + }) + }) + + describe('.setupMiddleWidths()', function () { + let middleHeaderStub, middleBodyStub + beforeEach(function () { + sandbox.stub(component, '_calculateWidth').returns(12345) + middleHeaderStub = createSelectorStub('css') + middleBodyStub = createSelectorStub('css') + sandbox.stub(component, '$') + .withArgs('.frost-fixed-table-header-middle .frost-scroll .frost-table-header').returns(middleHeaderStub) + .withArgs('.frost-fixed-table-middle .frost-scroll .frost-table-row').returns(middleBodyStub) + + component.setupMiddleWidths() + }) + + it('should set width of middle header', function () { + expect(middleHeaderStub.css).to.have.been.calledWith({width: '12345px'}) + }) + + it('should set width of middle body', function () { + expect(middleBodyStub.css).to.have.been.calledWith({width: '12345px'}) + }) + }) + + describe('.setupScrollSync()', function () { + beforeEach(function () { + sandbox.stub(component, 'syncScrollLeft') + sandbox.stub(component, 'syncScrollTop') + + component.setupScrollSync() + }) + + it('should setup horizontal syncing from header middle to body middle', function () { + expect(component.syncScrollLeft).to.have.been.calledWith( + '.frost-fixed-table-header-middle .frost-scroll', + '.frost-fixed-table-middle .frost-scroll' + ) + }) + + it('should setup horizontal syncing from body middle to header middle', function () { + expect(component.syncScrollLeft).to.have.been.calledWith( + '.frost-fixed-table-middle .frost-scroll', + '.frost-fixed-table-header-middle .frost-scroll' + ) + }) + + it('should setup vertical syncing from body left to body middle and right', function () { + expect(component.syncScrollTop).to.have.been.calledWith( + '.frost-fixed-table-left .frost-scroll', + '.frost-fixed-table-middle .frost-scroll', + '.frost-fixed-table-right .frost-scroll' + ) + }) + + it('should setup vertical syncing from body middle to body left and right', function () { + expect(component.syncScrollTop).to.have.been.calledWith( + '.frost-fixed-table-middle .frost-scroll', + '.frost-fixed-table-left .frost-scroll', + '.frost-fixed-table-right .frost-scroll' + ) + }) + + it('should setup vertical syncing from body right to body left and middle', function () { + expect(component.syncScrollTop).to.have.been.calledWith( + '.frost-fixed-table-right .frost-scroll', + '.frost-fixed-table-left .frost-scroll', + '.frost-fixed-table-middle .frost-scroll' + ) + }) + }) + + describe('.syncScrollLeft()', function () { + let srcStub, scrollHandler + beforeEach(function () { + srcStub = createSelectorStub('on', 'scrollLeft') + sandbox.stub(component, '$').withArgs('src').returns(srcStub) + component.syncScrollLeft('src', 'dst1', 'dst2', 'dst3') + scrollHandler = srcStub.on.lastCall.args[1] + }) + + it('should lookup the source DOM element', function () { + expect(component.$).to.have.been.calledWith('src') + }) + + it('should add a scroll event handler to the source DOM element', function () { + expect(srcStub.on).to.have.been.calledWith('scroll', sinon.match.func) + }) + + describe('when the scroll even handler is called', function () { + let dst1Stub, dst2Stub, dst3Stub + beforeEach(function () { + dst1Stub = createSelectorStub('scrollLeft') + dst2Stub = createSelectorStub('scrollLeft') + dst3Stub = createSelectorStub('scrollLeft') + component.$.withArgs('dst1').returns(dst1Stub) + component.$.withArgs('dst2').returns(dst2Stub) + component.$.withArgs('dst3').returns(dst3Stub) + + srcStub.scrollLeft.returns(321) + + component.$.reset() // forget previous call + scrollHandler() + }) + + it('should lookup the src DOM again', function () { + expect(component.$).to.have.been.calledWith('src') + }) + + it('should set scrollLeft on the first destination', function () { + expect(dst1Stub.scrollLeft).to.have.been.calledWith(321) + }) + + it('should set scrollLeft on the second destination', function () { + expect(dst2Stub.scrollLeft).to.have.been.calledWith(321) + }) + + it('should set scrollLeft on the third destination', function () { + expect(dst3Stub.scrollLeft).to.have.been.calledWith(321) + }) + }) + }) + + describe('.syncScrollTop()', function () { + let srcStub, scrollHandler + beforeEach(function () { + srcStub = createSelectorStub('on', 'scrollTop') + sandbox.stub(component, '$').withArgs('src').returns(srcStub) + component.syncScrollTop('src', 'dst1', 'dst2', 'dst3') + scrollHandler = srcStub.on.lastCall.args[1] + }) + + it('should lookup the source DOM element', function () { + expect(component.$).to.have.been.calledWith('src') + }) + + it('should add a scroll event handler to the source DOM element', function () { + expect(srcStub.on).to.have.been.calledWith('scroll', sinon.match.func) + }) + + describe('when the scroll even handler is called', function () { + let dst1Stub, dst2Stub, dst3Stub + beforeEach(function () { + dst1Stub = createSelectorStub('scrollTop') + dst2Stub = createSelectorStub('scrollTop') + dst3Stub = createSelectorStub('scrollTop') + component.$.withArgs('dst1').returns(dst1Stub) + component.$.withArgs('dst2').returns(dst2Stub) + component.$.withArgs('dst3').returns(dst3Stub) + + srcStub.scrollTop.returns(123) + + component.$.reset() // forget previous call + scrollHandler() + }) + + it('should lookup the src DOM again', function () { + expect(component.$).to.have.been.calledWith('src') + }) + + it('should set scrollTop on the first destination', function () { + expect(dst1Stub.scrollTop).to.have.been.calledWith(123) + }) + + it('should set scrollTop on the second destination', function () { + expect(dst2Stub.scrollTop).to.have.been.calledWith(123) + }) + + it('should set scrollTop on the third destination', function () { + expect(dst3Stub.scrollTop).to.have.been.calledWith(123) + }) + }) + }) +}) diff --git a/tests/unit/helpers/extend-test.js b/tests/unit/helpers/extend-test.js deleted file mode 100644 index 5619a0c..0000000 --- a/tests/unit/helpers/extend-test.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Unit test for the extend helper - */ -import {expect} from 'chai' -import {beforeEach, describe, it} from 'mocha' - -import {extend} from 'ember-frost-table/helpers/extend' - -describe('Unit / Helper / extend', function () { - let original, extended - beforeEach(function () { - original = { - bar: 'baz', - baz: 'foo', - foo: 'bar' - } - extended = extend([original], {fizz: 'bang'}) - }) - - it('should leave the original object alone', function () { - expect(original).to.eql({ - bar: 'baz', - baz: 'foo', - foo: 'bar' - }) - }) - - it('should return the merged object', function () { - expect(extended).to.eql({ - bar: 'baz', - baz: 'foo', - fizz: 'bang', - foo: 'bar' - }) - }) -})