diff --git a/packages/table/src/cell/formats/jsonFormat.tsx b/packages/table/src/cell/formats/jsonFormat.tsx index c827f56003..62b1e50407 100644 --- a/packages/table/src/cell/formats/jsonFormat.tsx +++ b/packages/table/src/cell/formats/jsonFormat.tsx @@ -6,6 +6,7 @@ */ import * as classNames from "classnames"; +import * as PureRender from "pure-render-decorator"; import * as React from "react"; import * as Classes from "../../common/classes"; import { ITruncatedFormatProps, TruncatedFormat, TruncatedPopoverMode } from "./truncatedFormat"; @@ -29,6 +30,7 @@ export interface IJSONFormatProps extends ITruncatedFormatProps { stringify?: (obj: any) => string; } +@PureRender export class JSONFormat extends React.Component { public static defaultProps: IJSONFormatProps = { detectTruncation: true, diff --git a/packages/table/src/cell/formats/truncatedFormat.tsx b/packages/table/src/cell/formats/truncatedFormat.tsx index 8ea6330a55..57122fa5a6 100644 --- a/packages/table/src/cell/formats/truncatedFormat.tsx +++ b/packages/table/src/cell/formats/truncatedFormat.tsx @@ -8,6 +8,7 @@ import { Classes as CoreClasses, IProps, Popover, Position } from "@blueprintjs/core"; import * as classNames from "classnames"; +import * as PureRender from "pure-render-decorator"; import * as React from "react"; import * as Classes from "../../common/classes"; @@ -66,6 +67,7 @@ export interface ITruncatedFormatState { isTruncated: boolean; } +@PureRender export class TruncatedFormat extends React.Component { public static defaultProps: ITruncatedFormatProps = { detectTruncation: true, diff --git a/packages/table/src/column.tsx b/packages/table/src/column.tsx index 43e2436f9a..85be21bbf1 100644 --- a/packages/table/src/column.tsx +++ b/packages/table/src/column.tsx @@ -5,6 +5,7 @@ * and https://github.com/palantir/blueprint/blob/master/PATENTS */ +import * as PureRender from "pure-render-decorator"; import * as React from "react"; import { IProps } from "@blueprintjs/core"; @@ -46,6 +47,7 @@ export interface IColumnProps extends IColumnNameProps, IProps { renderColumnHeader?: IColumnHeaderRenderer; } +@PureRender export class Column extends React.Component { public static defaultProps: IColumnProps = { renderCell: emptyCellRenderer, diff --git a/packages/table/src/common/contextMenuTargetWrapper.tsx b/packages/table/src/common/contextMenuTargetWrapper.tsx index 9c47515dc5..a519873cbf 100644 --- a/packages/table/src/common/contextMenuTargetWrapper.tsx +++ b/packages/table/src/common/contextMenuTargetWrapper.tsx @@ -6,6 +6,7 @@ */ import { ContextMenuTarget, IProps } from "@blueprintjs/core"; +import * as PureRender from "pure-render-decorator"; import * as React from "react"; export interface IContextMenuTargetWrapper extends IProps { @@ -20,6 +21,7 @@ export interface IContextMenuTargetWrapper extends IProps { * chains. */ @ContextMenuTarget +@PureRender export class ContextMenuTargetWrapper extends React.Component { public render() { const { className, children, style } = this.props; diff --git a/packages/table/src/common/loadableContent.tsx b/packages/table/src/common/loadableContent.tsx index 0e2ac70743..1919f3d009 100644 --- a/packages/table/src/common/loadableContent.tsx +++ b/packages/table/src/common/loadableContent.tsx @@ -5,6 +5,7 @@ * and https://github.com/palantir/blueprint/blob/master/PATENTS */ +import * as PureRender from "pure-render-decorator"; import * as React from "react"; import { Classes } from "@blueprintjs/core"; @@ -24,6 +25,7 @@ export interface ILoadableContentProps { } // This class expects a single, non-string child. +@PureRender export class LoadableContent extends React.Component { private style: React.CSSProperties; diff --git a/packages/table/src/common/utils.ts b/packages/table/src/common/utils.ts index 65688870e0..77550ecb98 100644 --- a/packages/table/src/common/utils.ts +++ b/packages/table/src/common/utils.ts @@ -200,15 +200,25 @@ export const Utils = { }, /** - * Partial shallow comparison between objects using the given list of keys. + * Shallow comparison between objects. If `keys` is provided, just that subset of keys will be + * compared; otherwise, all keys will be compared. */ - shallowCompareKeys(objA: any, objB: any, keys: string[]) { - for (const key of keys) { - if (objA[key] !== objB[key]) { - return false; - } + shallowCompareKeys(objA: any, objB: any, keys?: string[]) { + // treat `null` and `undefined` as the same + if (objA == null && objB == null) { + return true; + } else if (objA == null || objB == null) { + return false; + } else if (Array.isArray(objA) || Array.isArray(objB)) { + return false; + } else if (keys != null) { + return _shallowCompareKeys(objA, objB, keys); + } else { + const keysA = Object.keys(objA); + const keysB = Object.keys(objB); + return _shallowCompareKeys(objA, objB, keysA) + && _shallowCompareKeys(objA, objB, keysB); } - return true; }, /** @@ -333,4 +343,29 @@ export const Utils = { isLeftClick(event: MouseEvent) { return event.button === 0; }, + + /** + * Returns true if the arrays are equal. Elements will be shallowly compared by default, or they + * will be compared using the custom `compare` function if one is provided. + */ + arraysEqual(arrA: any[], arrB: any[], compare = (a: any, b: any) => a === b) { + // treat `null` and `undefined` as the same + if (arrA == null && arrB == null) { + return true; + } else if (arrA == null || arrB == null || arrA.length !== arrB.length) { + return false; + } else { + return arrA.every((a, i) => compare(a, arrB[i])); + } + }, }; + +/** + * Partial shallow comparison between objects using the given list of keys. + */ +function _shallowCompareKeys(objA: any, objB: any, keys: string[]) { + return keys.every((key) => { + return objA.hasOwnProperty(key) === objB.hasOwnProperty(key) + && objA[key] === objB[key]; + }); +} diff --git a/packages/table/src/headers/editableName.tsx b/packages/table/src/headers/editableName.tsx index 21fef75fb4..ab60b03199 100644 --- a/packages/table/src/headers/editableName.tsx +++ b/packages/table/src/headers/editableName.tsx @@ -7,6 +7,7 @@ import { EditableText, IIntentProps, IProps } from "@blueprintjs/core"; import * as classNames from "classnames"; +import * as PureRender from "pure-render-decorator"; import * as React from "react"; import * as Classes from "../common/classes"; @@ -37,6 +38,7 @@ export interface IEditableNameProps extends IIntentProps, IProps { onConfirm?: (value: string) => void; } +@PureRender export class EditableName extends React.Component { public render() { const { className, intent, name, onCancel, onChange, onConfirm } = this.props; diff --git a/packages/table/src/headers/rowHeaderCell.tsx b/packages/table/src/headers/rowHeaderCell.tsx index 604589eda2..95051df53d 100644 --- a/packages/table/src/headers/rowHeaderCell.tsx +++ b/packages/table/src/headers/rowHeaderCell.tsx @@ -6,6 +6,7 @@ */ import * as classNames from "classnames"; +import * as PureRender from "pure-render-decorator"; import * as React from "react"; import { Classes as CoreClasses, ContextMenuTarget, IProps } from "@blueprintjs/core"; @@ -67,6 +68,7 @@ export interface IRowHeaderState { } @ContextMenuTarget +@PureRender export class RowHeaderCell extends React.Component { public state = { isActive: false, diff --git a/packages/table/src/interactions/menus/copyCellsMenuItem.tsx b/packages/table/src/interactions/menus/copyCellsMenuItem.tsx index 096501afd8..5bc4a873aa 100644 --- a/packages/table/src/interactions/menus/copyCellsMenuItem.tsx +++ b/packages/table/src/interactions/menus/copyCellsMenuItem.tsx @@ -6,6 +6,7 @@ */ import { IMenuItemProps, MenuItem } from "@blueprintjs/core"; +import * as PureRender from "pure-render-decorator"; import * as React from "react"; import { Clipboard } from "../../common/clipboard"; @@ -37,6 +38,7 @@ export interface ICopyCellsMenuItemProps extends IMenuItemProps { onCopy?: (success: boolean) => void; } +@PureRender export class CopyCellsMenuItem extends React.Component { public render() { return ; diff --git a/packages/table/src/interactions/resizeHandle.tsx b/packages/table/src/interactions/resizeHandle.tsx index c3c8412f60..c135c3c232 100644 --- a/packages/table/src/interactions/resizeHandle.tsx +++ b/packages/table/src/interactions/resizeHandle.tsx @@ -7,6 +7,7 @@ import { IProps } from "@blueprintjs/core"; import * as classNames from "classnames"; +import * as PureRender from "pure-render-decorator"; import * as React from "react"; import * as Classes from "../common/classes"; @@ -57,6 +58,7 @@ export interface IResizeHandleState { isDragging: boolean; } +@PureRender export class ResizeHandle extends React.Component { public state: IResizeHandleState = { isDragging: false, diff --git a/packages/table/src/layers/guides.tsx b/packages/table/src/layers/guides.tsx index 64725df45a..7ef10bb710 100644 --- a/packages/table/src/layers/guides.tsx +++ b/packages/table/src/layers/guides.tsx @@ -10,6 +10,7 @@ import * as classNames from "classnames"; import * as React from "react"; import * as Classes from "../common/classes"; +import { Utils } from "../common/utils"; export interface IGuideLayerProps extends IProps { /** @@ -24,6 +25,16 @@ export interface IGuideLayerProps extends IProps { } export class GuideLayer extends React.Component { + public shouldComponentUpdate(nextProps: IGuideLayerProps) { + if (this.props.className !== nextProps.className) { + return true; + } + // shallow-comparing guide arrays leads to tons of unnecessary re-renders, so we check the + // array contents explicitly. + return !Utils.arraysEqual(this.props.verticalGuides, nextProps.verticalGuides) + || !Utils.arraysEqual(this.props.horizontalGuides, nextProps.horizontalGuides); + } + public render() { const { verticalGuides, horizontalGuides, className } = this.props; const verticals = (verticalGuides == null) ? undefined : verticalGuides.map(this.renderVerticalGuide); diff --git a/packages/table/src/layers/regions.tsx b/packages/table/src/layers/regions.tsx index 6ea8718737..e5b06d2fee 100644 --- a/packages/table/src/layers/regions.tsx +++ b/packages/table/src/layers/regions.tsx @@ -7,10 +7,10 @@ import { IProps } from "@blueprintjs/core"; import * as classNames from "classnames"; -import * as PureRender from "pure-render-decorator"; import * as React from "react"; import * as Classes from "../common/classes"; -import { IRegion } from "../regions"; +import { Utils } from "../common/utils"; +import { IRegion, Regions } from "../regions"; export type IRegionStyler = (region: IRegion) => React.CSSProperties; @@ -21,13 +21,27 @@ export interface IRegionLayerProps extends IProps { regions?: IRegion[]; /** - * A callback interface for applying CSS styles to the regions. + * The array of CSS styles to apply to each region. The ith style object in this array will be + * applied to the ith region in `regions`. */ - getRegionStyle: IRegionStyler; + regionStyles?: React.CSSProperties[]; } -@PureRender +// don't include "regions" or "regionStyles" in here, because they can't be shallowly compared +const UPDATE_PROPS_KEYS = [ + "className", +]; + export class RegionLayer extends React.Component { + public shouldComponentUpdate(nextProps: IRegionLayerProps) { + // shallowly comparable props like "className" tend not to change in the default table + // implementation, so do that check last with hope that we return earlier and avoid it + // altogether. + return !Utils.arraysEqual(this.props.regions, nextProps.regions, Regions.regionsEqual) + || !Utils.arraysEqual(this.props.regionStyles, nextProps.regionStyles, Utils.shallowCompareKeys) + || !Utils.shallowCompareKeys(this.props, nextProps, UPDATE_PROPS_KEYS); + } + public render() { return
{this.renderRegionChildren()}
; } @@ -40,13 +54,13 @@ export class RegionLayer extends React.Component { return regions.map(this.renderRegion); } - private renderRegion = (region: IRegion, index: number) => { - const { className, getRegionStyle } = this.props; + private renderRegion = (_region: IRegion, index: number) => { + const { className, regionStyles } = this.props; return (
); } diff --git a/packages/table/src/regions.ts b/packages/table/src/regions.ts index 75aead1cc9..f39e3833b8 100644 --- a/packages/table/src/regions.ts +++ b/packages/table/src/regions.ts @@ -528,6 +528,11 @@ export class Regions { return regionGroups; } + public static regionsEqual(regionA: IRegion, regionB: IRegion) { + return Regions.intervalsEqual(regionA.rows, regionB.rows) + && Regions.intervalsEqual(regionA.cols, regionB.cols); + } + /** * Iterates over the cells within an `IRegion`, invoking the callback with * each cell's coordinates. @@ -573,11 +578,6 @@ export class Regions { } } - private static regionsEqual(regionA: IRegion, regionB: IRegion) { - return Regions.intervalsEqual(regionA.rows, regionB.rows) - && Regions.intervalsEqual(regionA.cols, regionB.cols); - } - private static regionContains(regionA: IRegion, regionB: IRegion) { // containsRegion expects an array of regions as the first param return Regions.overlapsRegion([regionA], regionB, false); diff --git a/packages/table/src/table.tsx b/packages/table/src/table.tsx index 3dddd5b660..ab46732e56 100644 --- a/packages/table/src/table.tsx +++ b/packages/table/src/table.tsx @@ -579,7 +579,7 @@ export class Table extends AbstractComponent { ref={this.setMenuRef} onClick={this.selectAll} > - {this.maybeRenderMenuRegions()} + {this.maybeRenderRegions(this.styleMenuRegion)}
); } @@ -663,7 +663,7 @@ export class Table extends AbstractComponent { onFocus={this.handleFocus} onLayoutLock={this.handleLayoutLock} onReordered={this.handleColumnsReordered} - onReordering={this.handleColumnReorderPreview} + onReordering={this.handleColumnsReordering} onResizeGuide={this.handleColumnResizeGuide} onSelection={this.getEnabledSelectionHandler(RegionCardinality.FULL_COLUMNS)} selectedRegions={selectedRegions} @@ -674,7 +674,7 @@ export class Table extends AbstractComponent { {this.props.children} - {this.maybeRenderColumnHeaderRegions()} + {this.maybeRenderRegions(this.styleColumnHeaderRegion)} ); } @@ -715,7 +715,7 @@ export class Table extends AbstractComponent { onLayoutLock={this.handleLayoutLock} onResizeGuide={this.handleRowResizeGuide} onReordered={this.handleRowsReordered} - onReordering={this.handleRowReordering} + onReordering={this.handleRowsReordering} onRowHeightChanged={this.handleRowHeightChanged} onSelection={this.getEnabledSelectionHandler(RegionCardinality.FULL_ROWS)} renderRowHeader={renderRowHeader} @@ -725,7 +725,7 @@ export class Table extends AbstractComponent { {...rowIndices} /> - {this.maybeRenderRowHeaderRegions()} + {this.maybeRenderRegions(this.styleRowHeaderRegion)} ); } @@ -794,7 +794,7 @@ export class Table extends AbstractComponent { {...columnIndices} /> - {this.maybeRenderBodyRegions()} + {this.maybeRenderRegions(this.styleBodyRegion)} { ); return regionGroups.map((regionGroup, index) => { + const regionStyles = regionGroup.regions.map(getRegionStyle); return ( ); }); @@ -952,116 +953,104 @@ export class Table extends AbstractComponent { } } - private maybeRenderBodyRegions() { - const styler = (region: IRegion): React.CSSProperties => { - const cardinality = Regions.getRegionCardinality(region); - const style = this.grid.getRegionStyle(region); - switch (cardinality) { - case RegionCardinality.CELLS: - return style; - - case RegionCardinality.FULL_COLUMNS: - style.top = "-1px"; - return style; - - case RegionCardinality.FULL_ROWS: - style.left = "-1px"; - return style; - - case RegionCardinality.FULL_TABLE: - style.left = "-1px"; - style.top = "-1px"; - return style; - - default: - return { display: "none" }; - } - }; - return this.maybeRenderRegions(styler); + private styleBodyRegion = (region: IRegion): React.CSSProperties => { + const cardinality = Regions.getRegionCardinality(region); + const style = this.grid.getRegionStyle(region); + switch (cardinality) { + case RegionCardinality.CELLS: + return style; + + case RegionCardinality.FULL_COLUMNS: + style.top = "-1px"; + return style; + + case RegionCardinality.FULL_ROWS: + style.left = "-1px"; + return style; + + case RegionCardinality.FULL_TABLE: + style.left = "-1px"; + style.top = "-1px"; + return style; + + default: + return { display: "none" }; + } } - private maybeRenderMenuRegions() { - const styler = (region: IRegion): React.CSSProperties => { - const { grid } = this; - const { viewportRect } = this.state; - if (viewportRect == null) { - return {}; - } - const cardinality = Regions.getRegionCardinality(region); - const style = grid.getRegionStyle(region); - - switch (cardinality) { - case RegionCardinality.FULL_TABLE: - style.right = "0px"; - style.bottom = "0px"; - style.top = "0px"; - style.left = "0px"; - style.borderBottom = "none"; - style.borderRight = "none"; - return style; - - default: - return { display: "none" }; - } - }; - return this.maybeRenderRegions(styler); + private styleMenuRegion = (region: IRegion): React.CSSProperties => { + const { grid } = this; + const { viewportRect } = this.state; + if (viewportRect == null) { + return {}; + } + const cardinality = Regions.getRegionCardinality(region); + const style = grid.getRegionStyle(region); + + switch (cardinality) { + case RegionCardinality.FULL_TABLE: + style.right = "0px"; + style.bottom = "0px"; + style.top = "0px"; + style.left = "0px"; + style.borderBottom = "none"; + style.borderRight = "none"; + return style; + + default: + return { display: "none" }; + } } - private maybeRenderColumnHeaderRegions() { - const styler = (region: IRegion): React.CSSProperties => { - const { grid } = this; - const { viewportRect } = this.state; - if (viewportRect == null) { - return {}; - } - const cardinality = Regions.getRegionCardinality(region); - const style = grid.getRegionStyle(region); - - switch (cardinality) { - case RegionCardinality.FULL_TABLE: - style.left = "-1px"; - style.borderLeft = "none"; - style.bottom = "-1px"; - style.transform = `translate3d(${-viewportRect.left}px, 0, 0)`; - return style; - case RegionCardinality.FULL_COLUMNS: - style.bottom = "-1px"; - style.transform = `translate3d(${-viewportRect.left}px, 0, 0)`; - return style; - - default: - return { display: "none" }; - } - }; - return this.maybeRenderRegions(styler); + private styleColumnHeaderRegion = (region: IRegion): React.CSSProperties => { + const { grid } = this; + const { viewportRect } = this.state; + if (viewportRect == null) { + return {}; + } + const cardinality = Regions.getRegionCardinality(region); + const style = grid.getRegionStyle(region); + + switch (cardinality) { + case RegionCardinality.FULL_TABLE: + style.left = "-1px"; + style.borderLeft = "none"; + style.bottom = "-1px"; + style.transform = `translate3d(${-viewportRect.left}px, 0, 0)`; + return style; + case RegionCardinality.FULL_COLUMNS: + style.bottom = "-1px"; + style.transform = `translate3d(${-viewportRect.left}px, 0, 0)`; + return style; + + default: + return { display: "none" }; + } } - private maybeRenderRowHeaderRegions() { - const styler = (region: IRegion): React.CSSProperties => { - const { grid } = this; - const { viewportRect } = this.state; - if (viewportRect == null) { - return {}; - } - const cardinality = Regions.getRegionCardinality(region); - const style = grid.getRegionStyle(region); - switch (cardinality) { - case RegionCardinality.FULL_TABLE: - style.top = "-1px"; - style.borderTop = "none"; - style.right = "-1px"; - style.transform = `translate3d(0, ${-viewportRect.top}px, 0)`; - return style; - case RegionCardinality.FULL_ROWS: - style.right = "-1px"; - style.transform = `translate3d(0, ${-viewportRect.top}px, 0)`; - return style; - - default: - return { display: "none" }; - } - }; - return this.maybeRenderRegions(styler); + private styleRowHeaderRegion = (region: IRegion): React.CSSProperties => { + const { grid } = this; + const { viewportRect } = this.state; + if (viewportRect == null) { + return {}; + } + const cardinality = Regions.getRegionCardinality(region); + const style = grid.getRegionStyle(region); + switch (cardinality) { + case RegionCardinality.FULL_TABLE: + style.top = "-1px"; + style.borderTop = "none"; + style.right = "-1px"; + style.transform = `translate3d(0, ${-viewportRect.top}px, 0)`; + return style; + case RegionCardinality.FULL_ROWS: + style.right = "-1px"; + style.transform = `translate3d(0, ${-viewportRect.top}px, 0)`; + return style; + + default: + return { display: "none" }; + } } private handleColumnWidthChanged = (columnIndex: number, width: number) => { @@ -1215,7 +1204,7 @@ export class Table extends AbstractComponent { } } - private handleColumnReorderPreview = (oldIndex: number, newIndex: number, length: number) => { + private handleColumnsReordering = (oldIndex: number, newIndex: number, length: number) => { const guideIndex = Utils.reorderedIndexToGuideIndex(oldIndex, newIndex, length); const leftOffset = this.grid.getCumulativeWidthBefore(guideIndex); this.setState({ isReordering: true, verticalGuides: [leftOffset] } as ITableState); @@ -1226,7 +1215,7 @@ export class Table extends AbstractComponent { BlueprintUtils.safeInvoke(this.props.onColumnsReordered, oldIndex, newIndex, length); } - private handleRowReordering = (oldIndex: number, newIndex: number, length: number) => { + private handleRowsReordering = (oldIndex: number, newIndex: number, length: number) => { const guideIndex = Utils.reorderedIndexToGuideIndex(oldIndex, newIndex, length); const topOffset = this.grid.getCumulativeHeightBefore(guideIndex); this.setState({ isReordering: true, horizontalGuides: [topOffset] } as ITableState); diff --git a/packages/table/test/utilsTests.ts b/packages/table/test/utilsTests.ts index 7f7d1e186c..1f87b3aa04 100644 --- a/packages/table/test/utilsTests.ts +++ b/packages/table/test/utilsTests.ts @@ -225,7 +225,6 @@ describe("Utils", () => { }); describe("reorderArray", () => { - const ARRAY_STRING = "ABCDEFG"; const ARRAY = ARRAY_STRING.split(""); const ARRAY_LENGTH = ARRAY.length; @@ -303,4 +302,109 @@ describe("Utils", () => { expect(result).to.eql(expected.split("")); } }); + + describe("shallowCompareKeys", () => { + describe("with `keys` defined", () => { + describe("returns true if only the specified values are shallowly equal", () => { + runTest(true, { a: 1 }, { a: 1 }, ["a", "b", "c", "d"]); + runTest(true, { a: 1, b: [1, 2, 3], c: "3" }, { b: [1, 2, 3], a: 1, c: "3" }, ["a", "c"]); + runTest(true, { a: 1, b: "2", c: { a: 1 }}, { a: 1, b: "2", c: { a: 1 }}, ["a", "b"]); + }); + + describe("returns false if any specified values are not shallowly equal", () => { + runTest(false, { a: [1, "2", true] }, { a: [1, "2", true] }, ["a"]); + runTest(false, { a: 1, b: "2", c: { a: 1 }}, { a: 1, b: "2", c: { a: 1 }}, ["a", "b", "c"]); + }); + + describe("edge cases that return true", () => { + runTest(true, undefined, null, []); + runTest(true, undefined, undefined, ["a"]); + runTest(true, null, null, ["a"]); + runTest(true, {}, {}, ["a"]); + }); + + describe("edge cases that return false", () => { + runTest(false, {}, undefined, []); + runTest(false, {}, [], []); + runTest(false, [], [], []); + }); + + function runTest(expectedResult: boolean, a: any, b: any, keys: string[]) { + it(`${JSON.stringify(a)} and ${JSON.stringify(b)} (keys: ${JSON.stringify(keys)})`, () => { + expect(Utils.shallowCompareKeys(a, b, keys)).to.equal(expectedResult); + }); + } + }); + + describe("with `keys` not defined", () => { + describe("returns true if values are shallowly equal", () => { + runTest(true, { a: 1, b: "2", c: true}, { a: 1, b: "2", c: true}); + runTest(true, undefined, undefined); + runTest(true, null, undefined); + }); + + describe("returns false if values are not shallowly equal", () => { + runTest(false, undefined, {}); + runTest(false, null, {}); + runTest(false, {}, []); + runTest(false, { a: 1, b: "2", c: { a: 1 }}, { a: 1, b: "2", c: { a: 1 }}); + }); + + describe("returns false if keys are different", () => { + runTest(false, {}, { a: 1 }); + runTest(false, { a: 1, b: "2" }, { b: "2" }); + runTest(false, { a: 1, b: "2", c: true}, { b: "2", c: true, d: 3 }); + }); + + describe("returns true if same deeply-comparable instance is reused in both objects", () => { + const deeplyComparableThing1 = { a: 1 }; + const deeplyComparableThing2 = [1, "2", true]; + runTest(true, { a: 1, b: deeplyComparableThing1 }, { a: 1, b: deeplyComparableThing1 }); + runTest(true, { a: 1, b: deeplyComparableThing2 }, { a: 1, b: deeplyComparableThing2 }); + }); + + function runTest(expectedResult: boolean, a: any, b: any) { + it(`${JSON.stringify(a)} and ${JSON.stringify(b)}`, () => { + expect(Utils.shallowCompareKeys(a, b)).to.equal(expectedResult); + }); + } + }); + }); + + describe("arraysEqual", () => { + describe("no compare function provided", () => { + describe("should return true if the arrays are shallowly equal", () => { + runTest(true, undefined, undefined); + runTest(true, undefined, null); + runTest(true, [3, "1", true], [3, "1", true]); + }); + + describe("should return false if the arrays are not shallowly equal", () => { + runTest(false, null, [3]); + runTest(false, [3, 1, 2], [3, 1]); + runTest(false, [{ x: 1 }], [{ x: 1 }]); + }); + }); + + describe("compare function provided", () => { + const COMPARE_FN = (a: any, b: any) => a.x === b.x; + + describe("should return true if the arrays are equal using a custom compare function", () => { + runTest(true, undefined, undefined, COMPARE_FN); + runTest(true, undefined, null, COMPARE_FN); + runTest(true, [{ x: 1 }, { x: 2 }], [{ x: 1 }, { x: 2 }], COMPARE_FN); + }); + + describe("should return false if the arrays are not equal using custom compare function", () => { + runTest(false, null, [], COMPARE_FN); + runTest(false, [{ x: 1 }, {}], [{ x: 1 }, { x: 2 }], COMPARE_FN); + }); + }); + + function runTest(expectedResult: boolean, a: any, b: any, compareFn?: (a: any, b: any) => boolean) { + it(`${JSON.stringify(a)} and ${JSON.stringify(b)}`, () => { + expect(Utils.arraysEqual(a, b, compareFn)).to.equal(expectedResult); + }); + } + }); });