1152 строки
46 KiB
TypeScript
1152 строки
46 KiB
TypeScript
/*
|
|
* Power BI Visualizations
|
|
*
|
|
* Copyright (c) Microsoft Corporation
|
|
* All rights reserved.
|
|
* MIT License
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the ""Software""), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
* THE SOFTWARE.
|
|
*/
|
|
import powerbi from "powerbi-visuals-api";
|
|
|
|
import { dispatch } from "d3-dispatch";
|
|
import { select as d3Select, Selection } from "d3-selection";
|
|
|
|
import {
|
|
createSelectionIdBuilder,
|
|
testDom,
|
|
} from "powerbi-visuals-utils-testutils";
|
|
|
|
import { EventName } from "../src/event/eventName";
|
|
|
|
import {
|
|
ILineComponentRenderOptions,
|
|
LineComponent,
|
|
} from "../src/visualComponent/sparkline/lineComponent";
|
|
|
|
import {
|
|
DataRepresentationScale,
|
|
DataRepresentationTypeEnum,
|
|
} from "../src/converter/data/dataRepresentationScale";
|
|
|
|
import {
|
|
DataRepresentationPointGradientType,
|
|
IDataRepresentationPoint,
|
|
} from "../src/converter/data/dataRepresentation";
|
|
|
|
import { isValueValid } from "../src/utils/isValueValid";
|
|
|
|
import { DataConverter } from "../src/converter/data/dataConverter";
|
|
import { getFormattedValueWithFallback } from "../src/converter/data/dataFormatter";
|
|
|
|
import { KpiBaseDescriptor } from "../src/settings/descriptors/kpi/kpiBaseDescriptor";
|
|
import { NumericDescriptor } from "../src/settings/descriptors/numericDescriptor";
|
|
|
|
import { TestWrapper } from "./testWrapper";
|
|
import { SubtitleWarningComponent } from "../src/visualComponent/subtitleWarningComponent";
|
|
import { MultiKpiBuilder } from "./multiKpiBuilder";
|
|
|
|
describe("Multi KPI", () => {
|
|
describe("Version 2.3.0 Changes", () => {
|
|
describe("DataFormatter", () => {
|
|
it("should return N/A if a variance is not valid", () => {
|
|
const numericDescriptor: NumericDescriptor = new NumericDescriptor();
|
|
numericDescriptor.autoPrecision = true;
|
|
expect(getFormattedValueWithFallback(NaN, numericDescriptor)).toBe("N/A");
|
|
});
|
|
|
|
it("should return 12.3K for 12340 if precision is auto and display units are auto", () => {
|
|
const value: number = 12340;
|
|
const expectedValue: string = "12.3K";
|
|
const numericDescriptor: NumericDescriptor = new NumericDescriptor();
|
|
numericDescriptor.autoPrecision = true;
|
|
numericDescriptor.precision.value = 5;
|
|
|
|
const actualValue: string = getFormattedValueWithFallback(
|
|
value,
|
|
numericDescriptor,
|
|
);
|
|
|
|
expect(actualValue).toBe(expectedValue);
|
|
});
|
|
|
|
it("should return 2.34K for 2340 if precision is auto and display units are auto", () => {
|
|
const value: number = 2340;
|
|
const expectedValue: string = "2.34K";
|
|
const numericDescriptor: NumericDescriptor = new NumericDescriptor();
|
|
numericDescriptor.autoPrecision = true;
|
|
numericDescriptor.precision.value = 5;
|
|
|
|
const actualValue: string = getFormattedValueWithFallback(
|
|
value,
|
|
numericDescriptor,
|
|
);
|
|
|
|
expect(actualValue).toBe(expectedValue);
|
|
});
|
|
|
|
it("should return 123K for 123403 if precision is auto and display units are auto", () => {
|
|
const value: number = 123403;
|
|
const expectedValue: string = "123K";
|
|
const numericDescriptor: NumericDescriptor = new NumericDescriptor();
|
|
numericDescriptor.autoPrecision = true;
|
|
numericDescriptor.precision.value = 5;
|
|
|
|
const actualValue: string = getFormattedValueWithFallback(
|
|
value,
|
|
numericDescriptor,
|
|
);
|
|
|
|
expect(actualValue).toBe(expectedValue);
|
|
});
|
|
|
|
it("should return 1.23M for 1234035 if precision is auto and display units are auto", () => {
|
|
const value: number = 1234035;
|
|
const expectedValue: string = "1.23M";
|
|
const numericDescriptor: NumericDescriptor = new NumericDescriptor();
|
|
numericDescriptor.autoPrecision = true;
|
|
numericDescriptor.precision.value = 5;
|
|
|
|
const actualValue: string = getFormattedValueWithFallback(
|
|
value,
|
|
numericDescriptor,
|
|
);
|
|
|
|
expect(actualValue).toBe(expectedValue);
|
|
});
|
|
|
|
it("should return 1234K for 1234035 if precision is auto and display units are thousands", () => {
|
|
const value: number = 1234035;
|
|
const expectedValue: string = "1234K";
|
|
const numericDescriptor: NumericDescriptor = new NumericDescriptor();
|
|
numericDescriptor.autoPrecision = true;
|
|
numericDescriptor.displayUnits.value = 1000;
|
|
numericDescriptor.precision.value = 5;
|
|
|
|
const actualValue: string = getFormattedValueWithFallback(
|
|
value,
|
|
numericDescriptor,
|
|
);
|
|
|
|
expect(actualValue).toBe(expectedValue);
|
|
});
|
|
|
|
it("should return 1.23K% for 12.34312 if precision is auto and display units are thousand and default format in percents", () => {
|
|
const value: number = 12.34312;
|
|
const expectedValue: string = "+1.23K%";
|
|
const numericDescriptor: NumericDescriptor = new NumericDescriptor();
|
|
numericDescriptor.autoPrecision = true;
|
|
numericDescriptor.displayUnits.value = 1000;
|
|
numericDescriptor.defaultFormat = "+0.00%;-0.00%;0.00%";
|
|
numericDescriptor.precision.value = 5;
|
|
|
|
const actualValue: string = getFormattedValueWithFallback(
|
|
value,
|
|
numericDescriptor,
|
|
);
|
|
|
|
expect(actualValue).toBe(expectedValue);
|
|
});
|
|
|
|
it("should return 100% for 1.012 if precision is auto and display units are auto and format in percents", () => {
|
|
const value: number = 1.0012;
|
|
const expectedValue: string = "+100%";
|
|
const numericDescriptor: NumericDescriptor = new NumericDescriptor();
|
|
numericDescriptor.autoPrecision = true;
|
|
numericDescriptor.format.value = "+0.00%;-0.00%;0.00%";
|
|
numericDescriptor.precision.value = 5;
|
|
|
|
const actualValue: string = getFormattedValueWithFallback(
|
|
value,
|
|
numericDescriptor,
|
|
);
|
|
|
|
expect(actualValue).toBe(expectedValue);
|
|
});
|
|
|
|
it("should return 87.7% for 0.87712 if precision is auto and display units are auto and default format in percents", () => {
|
|
const value: number = 0.87712;
|
|
const expectedValue: string = "+87.7%";
|
|
const numericDescriptor: NumericDescriptor = new NumericDescriptor();
|
|
numericDescriptor.autoPrecision = true;
|
|
numericDescriptor.defaultFormat = "+0.00%;-0.00%;0.00%";
|
|
numericDescriptor.precision.value = 5;
|
|
|
|
const actualValue: string = getFormattedValueWithFallback(
|
|
value,
|
|
numericDescriptor,
|
|
);
|
|
|
|
expect(actualValue).toBe(expectedValue);
|
|
});
|
|
|
|
it("should return 7.43% for 0.0742712 if precision is auto and display units are auto and column format in percents", () => {
|
|
const value: number = 0.0742712;
|
|
const expectedValue: string = "+7.43%";
|
|
const numericDescriptor: NumericDescriptor = new NumericDescriptor();
|
|
numericDescriptor.autoPrecision = true;
|
|
numericDescriptor.columnFormat = "+0.00%;-0.00%;0.00%";
|
|
numericDescriptor.precision.value = 5;
|
|
|
|
const actualValue: string = getFormattedValueWithFallback(
|
|
value,
|
|
numericDescriptor,
|
|
);
|
|
|
|
expect(actualValue).toBe(expectedValue);
|
|
});
|
|
|
|
it("should return -0.02% for -0.00293312 if precision is auto and display units are auto and column format in percents", () => {
|
|
const value: number = -0.000243312;
|
|
const expectedValue: string = "-0.02%";
|
|
const numericDescriptor: NumericDescriptor = new NumericDescriptor();
|
|
numericDescriptor.autoPrecision = true;
|
|
numericDescriptor.columnFormat = "+0.00%;-0.00%;0.00%";
|
|
numericDescriptor.precision.value = 5;
|
|
|
|
const actualValue: string = getFormattedValueWithFallback(
|
|
value,
|
|
numericDescriptor,
|
|
);
|
|
|
|
expect(actualValue).toBe(expectedValue);
|
|
});
|
|
|
|
it("should return 1354% for 1.3537905 if precision is auto and display units are auto and format in percents", () => {
|
|
const value: number = 13.537905;
|
|
const expectedValue: string = "+1354%";
|
|
const numericDescriptor: NumericDescriptor = new NumericDescriptor();
|
|
numericDescriptor.autoPrecision = true;
|
|
numericDescriptor.format.value = "+0.00%;-0.00%;0.00%";
|
|
numericDescriptor.precision.value = 5;
|
|
|
|
const actualValue: string = getFormattedValueWithFallback(
|
|
value,
|
|
numericDescriptor,
|
|
);
|
|
|
|
expect(actualValue).toBe(expectedValue);
|
|
});
|
|
});
|
|
|
|
// it("stale data is set up for a metric", (done) => {
|
|
// const testWrapper: TestWrapper = new TestWrapper(true, 0, 1);
|
|
// castZeroToNullOrReturnBack(testWrapper.dataView);
|
|
|
|
// testWrapper.dataView.metadata.objects = {
|
|
// staleData: {
|
|
// show: true,
|
|
// staleDataText: "label {$1}",
|
|
// staleDataThreshold: 1,
|
|
// },
|
|
// subtitle: {
|
|
// show: true,
|
|
// },
|
|
// values: {
|
|
// showLatterAvailableValue: true,
|
|
// treatEmptyValuesAsZero: false,
|
|
// },
|
|
// };
|
|
|
|
// // This call will be skipped because of isShown = false
|
|
// testWrapper.dataView.metadata.columns[2].objects = {
|
|
// staleData: {
|
|
// isShown: false,
|
|
// },
|
|
// };
|
|
// // This call will be skipped because of threshold days
|
|
// testWrapper.dataView.metadata.columns[3].objects = {
|
|
// staleData: {
|
|
// staleDataText: "unique {$1}",
|
|
// staleDataThreshold: 5,
|
|
// },
|
|
// };
|
|
// testWrapper.dataView.metadata.columns[4].objects = {
|
|
// staleData: {
|
|
// staleDataText: "custom label {$1}",
|
|
// staleDataThreshold: 0,
|
|
// },
|
|
// };
|
|
|
|
// const components = testWrapper.visualBuilder.instance.rootComponent.getComponents();
|
|
// const warningComponent = <SubtitleWarningComponent>(components.filter(c => c instanceof SubtitleWarningComponent)[0]);
|
|
// spyOn(warningComponent, "getTitle");
|
|
|
|
// testWrapper.visualBuilder.updateRenderTimeout(testWrapper.dataView, () => {
|
|
// expect(warningComponent.getTitle).toHaveBeenCalledWith("label {$1}", 4, 0);
|
|
// expect(warningComponent.getTitle).toHaveBeenCalledWith("custom label {$1}", 1, 0);
|
|
// expect(warningComponent.getTitle).toHaveBeenCalledTimes(2);
|
|
// done();
|
|
// });
|
|
// });
|
|
|
|
// it("stale data is set up for a metric with staleDataText from susbtitles (compatibility support)", (done) => {
|
|
// const testWrapper: TestWrapper = new TestWrapper(true, 0, 2);
|
|
// castZeroToNullOrReturnBack(testWrapper.dataView);
|
|
|
|
// testWrapper.dataView.metadata.objects = {
|
|
// staleData: {
|
|
// show: true,
|
|
// },
|
|
// subtitle: {
|
|
// show: true,
|
|
// staleDataText: "Compatibility title"
|
|
// },
|
|
// values: {
|
|
// showLatterAvailableValue: true,
|
|
// treatEmptyValuesAsZero: false,
|
|
// },
|
|
// };
|
|
|
|
// testWrapper.dataView.metadata.columns[2].objects = {
|
|
// staleData: {
|
|
// isShown: false,
|
|
// },
|
|
// };
|
|
// testWrapper.dataView.metadata.columns[3].objects = {
|
|
// staleData: {
|
|
// staleDataText: "unique {$1}",
|
|
// staleDataThreshold: 1,
|
|
// },
|
|
// };
|
|
// testWrapper.dataView.metadata.columns[4].objects = {
|
|
// staleData: {
|
|
// staleDataText: "custom label {$1}",
|
|
// staleDataThreshold: 1,
|
|
// },
|
|
// };
|
|
|
|
// const components = testWrapper.visualBuilder.instance.rootComponent.getComponents();
|
|
// const warningComponent = <SubtitleWarningComponent>(components.filter(c => c instanceof SubtitleWarningComponent)[0]);
|
|
// spyOn(warningComponent, "getTitle");
|
|
|
|
// testWrapper.visualBuilder.updateRenderTimeout(testWrapper.dataView, () => {
|
|
// expect(warningComponent.getTitle).toHaveBeenCalledWith("Data is ${1} days late.Compatibility title", 5, 0);
|
|
// expect(warningComponent.getTitle).toHaveBeenCalledWith("unique {$1}", 2, 0);
|
|
// expect(warningComponent.getTitle).toHaveBeenCalledWith("custom label {$1}", 2, 0);
|
|
// expect(warningComponent.getTitle).toHaveBeenCalledTimes(3);
|
|
// done();
|
|
// });
|
|
// });
|
|
|
|
// it("stale data with goup threshold days deduction option", (done) => {
|
|
// const testWrapper: TestWrapper = new TestWrapper(true, 0, 1);
|
|
// castZeroToNullOrReturnBack(testWrapper.dataView);
|
|
|
|
// testWrapper.dataView.metadata.objects = {
|
|
// staleData: {
|
|
// show: true,
|
|
// staleDataThreshold: 0,
|
|
// deductThresholdDays: true,
|
|
// },
|
|
// subtitle: {
|
|
// show: true,
|
|
// },
|
|
// values: {
|
|
// showLatterAvailableValue: true,
|
|
// treatEmptyValuesAsZero: false,
|
|
// },
|
|
// };
|
|
|
|
// testWrapper.dataView.metadata.columns[1].objects = {
|
|
// staleData: {
|
|
// staleDataText: "first label {$1}",
|
|
// staleDataThreshold: 2,
|
|
// },
|
|
// };
|
|
// testWrapper.dataView.metadata.columns[2].objects = {
|
|
// staleData: {
|
|
// staleDataText: "second label {$1}",
|
|
// },
|
|
// };
|
|
|
|
// // because of high threshold days limit, this item is actual and will not be shown in stale data block
|
|
// testWrapper.dataView.metadata.columns[3].objects = {
|
|
// staleData: {
|
|
// staleDataText: "third label {$1}",
|
|
// staleDataThreshold: 33,
|
|
// },
|
|
// };
|
|
|
|
// const components = testWrapper.visualBuilder.instance.rootComponent.getComponents();
|
|
// const warningComponent = <SubtitleWarningComponent>(components.filter(c => c instanceof SubtitleWarningComponent)[0]);
|
|
// spyOn(warningComponent, "getTitle");
|
|
|
|
// testWrapper.visualBuilder.updateRenderTimeout(testWrapper.dataView, () => {
|
|
// expect(warningComponent.getTitle).toHaveBeenCalledWith("first label {$1}", 4, 2);
|
|
// expect(warningComponent.getTitle).toHaveBeenCalledWith("second label {$1}", 1, 0);
|
|
// expect(warningComponent.getTitle).toHaveBeenCalledWith("Data is ${1} days late.", 1, 0);
|
|
// expect(warningComponent.getTitle).toHaveBeenCalledTimes(3);
|
|
// done();
|
|
// });
|
|
// });
|
|
|
|
// it("stale data with threshold deduction per metric", (done) => {
|
|
// const testWrapper: TestWrapper = new TestWrapper(true, 0, 5);
|
|
// castZeroToNullOrReturnBack(testWrapper.dataView);
|
|
|
|
// testWrapper.dataView.metadata.objects = {
|
|
// staleData: {
|
|
// show: true,
|
|
// staleDataThreshold: 1,
|
|
// deductThresholdDays: false,
|
|
// },
|
|
// subtitle: {
|
|
// show: true,
|
|
// },
|
|
// values: {
|
|
// showLatterAvailableValue: true,
|
|
// treatEmptyValuesAsZero: false,
|
|
// },
|
|
// };
|
|
|
|
// testWrapper.dataView.metadata.columns[1].objects = {
|
|
// staleData: {
|
|
// staleDataText: "first label {$1}",
|
|
// staleDataThreshold: 2,
|
|
// },
|
|
// };
|
|
// testWrapper.dataView.metadata.columns[2].objects = {
|
|
// staleData: {
|
|
// staleDataText: "second label {$1}",
|
|
// staleDataThreshold: 3,
|
|
// deductThresholdDays: true,
|
|
// },
|
|
// };
|
|
|
|
// // because of high threshold days limit, this item is actual and will not be shown in stale data block
|
|
// testWrapper.dataView.metadata.columns[3].objects = {
|
|
// staleData: {
|
|
// staleDataText: "third label {$1}",
|
|
// staleDataThreshold: 33,
|
|
// },
|
|
// };
|
|
|
|
// const components = testWrapper.visualBuilder.instance.rootComponent.getComponents();
|
|
// const warningComponent = <SubtitleWarningComponent>(components.filter(c => c instanceof SubtitleWarningComponent)[0]);
|
|
// spyOn(warningComponent, "getTitle");
|
|
|
|
// testWrapper.visualBuilder.updateRenderTimeout(testWrapper.dataView, () => {
|
|
// expect(warningComponent.getTitle).toHaveBeenCalledWith("first label {$1}", 8, 0);
|
|
// expect(warningComponent.getTitle).toHaveBeenCalledWith("second label {$1}", 5, 3);
|
|
// expect(warningComponent.getTitle).toHaveBeenCalledWith("Data is ${1} days late.", 5, 0);
|
|
// expect(warningComponent.getTitle).toHaveBeenCalledTimes(3);
|
|
// done();
|
|
// });
|
|
// });
|
|
|
|
it("subtitle shouldn't be rendered if it's turned off in Format Panel explicitly", (done) => {
|
|
const testWrapper: TestWrapper = new TestWrapper();
|
|
|
|
testWrapper.dataView.metadata.objects = {
|
|
subtitle: {
|
|
show: false,
|
|
titleText: "Power BI rocks",
|
|
},
|
|
};
|
|
|
|
testWrapper.visualBuilder.updateRenderTimeout(testWrapper.dataView, () => {
|
|
const displayStatus = getComputedStyle(testWrapper.visualBuilder.subtitle).display;
|
|
expect(displayStatus).toEqual("none");
|
|
done();
|
|
});
|
|
});
|
|
|
|
it("merged subtitle should be rendered if it's turned on in Format Panel and provided as data field", (done) => {
|
|
const testWrapper: TestWrapper = new TestWrapper(undefined, undefined, undefined, true);
|
|
|
|
testWrapper.dataView.metadata.objects = {
|
|
subtitle: {
|
|
show: true,
|
|
titleText: "Power BI rocks",
|
|
},
|
|
};
|
|
|
|
testWrapper.visualBuilder.updateRenderTimeout(testWrapper.dataView, () => {
|
|
const displayStatus = getComputedStyle(testWrapper.visualBuilder.subtitle).display;
|
|
expect(displayStatus).toEqual("flex");
|
|
const subtitleElm = testWrapper.visualBuilder.subtitle?.querySelector(".multiKpi_subtitle");
|
|
expect(subtitleElm?.textContent).toEqual("Power BI rocksSubtitle form data");
|
|
done();
|
|
});
|
|
});
|
|
|
|
it("subtitle from data should be rendered if it's turned on in Format Panel and provided only as data field", (done) => {
|
|
const testWrapper: TestWrapper = new TestWrapper(undefined, undefined, undefined, true);
|
|
|
|
testWrapper.dataView.metadata.objects = {
|
|
subtitle: {
|
|
show: true,
|
|
},
|
|
};
|
|
|
|
testWrapper.visualBuilder.updateRenderTimeout(testWrapper.dataView, () => {
|
|
const displayStatus = getComputedStyle(testWrapper.visualBuilder.subtitle).display;
|
|
expect(displayStatus).toEqual("flex");
|
|
const subtitleElm = testWrapper.visualBuilder.subtitle?.querySelector(".multiKpi_subtitle");
|
|
expect(subtitleElm?.textContent).toEqual("Subtitle form data");
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Version 2.2.0 Changes", () => {
|
|
it("Treat Empty/Missing Values As Zero is enabled", (done) => {
|
|
const testWrapper: TestWrapper = new TestWrapper(true, 2);
|
|
|
|
castZeroToNullOrReturnBack(testWrapper.dataView);
|
|
|
|
testWrapper.dataView.metadata.objects = {
|
|
values: {
|
|
treatEmptyValuesAsZero: true,
|
|
},
|
|
};
|
|
|
|
testWrapper.visualBuilder.updateRenderTimeout(testWrapper.dataView, () => {
|
|
const secondSparklineValue = testWrapper.visualBuilder.sparklineSubtitle[3].innerText;
|
|
expect(secondSparklineValue).toEqual("0");
|
|
done();
|
|
});
|
|
});
|
|
|
|
it("Treat Empty/Missing Values As Zero is disabled", (done) => {
|
|
const testWrapper: TestWrapper = new TestWrapper(true, 2);
|
|
|
|
castZeroToNullOrReturnBack(testWrapper.dataView);
|
|
|
|
testWrapper.dataView.metadata.objects = {
|
|
values: {
|
|
treatEmptyValuesAsZero: false,
|
|
},
|
|
};
|
|
|
|
testWrapper.visualBuilder.updateRenderTimeout(testWrapper.dataView, () => {
|
|
const secondSparklineValue = testWrapper.visualBuilder.sparklineSubtitle[3].innerText;
|
|
expect(secondSparklineValue).toEqual("N/A");
|
|
done();
|
|
});
|
|
});
|
|
|
|
it("Treat Empty/Missing Values As Zero is disabled but Show Latest Available As Current Value is enabled", (done) => {
|
|
const testWrapper: TestWrapper = new TestWrapper(true, 2);
|
|
|
|
castZeroToNullOrReturnBack(testWrapper.dataView);
|
|
|
|
testWrapper.dataView.metadata.objects = {
|
|
values: {
|
|
showLatterAvailableValue: true,
|
|
treatEmptyValuesAsZero: false,
|
|
},
|
|
};
|
|
|
|
testWrapper.visualBuilder.updateRenderTimeout(testWrapper.dataView, () => {
|
|
const secondSparklineValue = testWrapper.visualBuilder.sparklineSubtitle[3].innerText;
|
|
expect(secondSparklineValue).toEqual("25");
|
|
done();
|
|
});
|
|
});
|
|
|
|
it("Treat Empty/Missing Values As Zero is enabled and Show Latest Available As Current Value is enabled", (done) => {
|
|
const testWrapper: TestWrapper = new TestWrapper(true, 2);
|
|
|
|
castZeroToNullOrReturnBack(testWrapper.dataView);
|
|
|
|
testWrapper.dataView.metadata.objects = {
|
|
values: {
|
|
showLatterAvailableValue: true,
|
|
treatEmptyValuesAsZero: true,
|
|
},
|
|
};
|
|
|
|
testWrapper.visualBuilder.updateRenderTimeout(testWrapper.dataView, () => {
|
|
const secondSparklineValue = testWrapper.visualBuilder.sparklineSubtitle[3].innerText;
|
|
expect(secondSparklineValue).toEqual("0");
|
|
done();
|
|
});
|
|
});
|
|
|
|
it("Missing Value label is customized", (done) => {
|
|
const testWrapper: TestWrapper = new TestWrapper(true, 2);
|
|
|
|
castZeroToNullOrReturnBack(testWrapper.dataView);
|
|
|
|
testWrapper.dataView.metadata.objects = {
|
|
values: {
|
|
noValueLabel: "no data",
|
|
treatEmptyValuesAsZero: false,
|
|
},
|
|
};
|
|
|
|
testWrapper.visualBuilder.updateRenderTimeout(testWrapper.dataView, () => {
|
|
const secondSparklineValue = testWrapper.visualBuilder.sparklineSubtitle[3].innerText;
|
|
const fourthSparklineValue = testWrapper.visualBuilder.sparklineSubtitle[7].innerText;
|
|
expect(secondSparklineValue).toEqual("no data");
|
|
expect(fourthSparklineValue).toEqual("no data");
|
|
done();
|
|
});
|
|
});
|
|
|
|
it("Missing Value label is customized generally and for certain series", (done) => {
|
|
const testWrapper: TestWrapper = new TestWrapper(true, 2);
|
|
|
|
castZeroToNullOrReturnBack(testWrapper.dataView);
|
|
|
|
testWrapper.dataView.metadata.objects = {
|
|
values: {
|
|
noValueLabel: "no data",
|
|
treatEmptyValuesAsZero: false,
|
|
},
|
|
};
|
|
|
|
testWrapper.dataView.metadata.columns[3].objects = {
|
|
values: {
|
|
noValueLabel: "[-]",
|
|
},
|
|
};
|
|
|
|
testWrapper.visualBuilder.updateRenderTimeout(testWrapper.dataView, () => {
|
|
const secondSparklineValue = testWrapper.visualBuilder.sparklineSubtitle[3].innerText;
|
|
const fourthSparklineValue = testWrapper.visualBuilder.sparklineSubtitle[7].innerText;
|
|
expect(secondSparklineValue).toEqual("[-]");
|
|
expect(fourthSparklineValue).toEqual("no data");
|
|
done();
|
|
});
|
|
});
|
|
|
|
it("Missing Variance label is customized", (done) => {
|
|
const testWrapper: TestWrapper = new TestWrapper(true, 0);
|
|
|
|
castZeroToNullOrReturnBack(testWrapper.dataView);
|
|
|
|
testWrapper.dataView.metadata.objects = {
|
|
values: {
|
|
treatEmptyValuesAsZero: false,
|
|
},
|
|
variance: {
|
|
noValueLabel: "no data",
|
|
},
|
|
};
|
|
|
|
testWrapper.visualBuilder.updateRenderTimeout(testWrapper.dataView, () => {
|
|
const naVariance = testWrapper.visualBuilder.mainChartNAVarance.textContent;
|
|
expect(naVariance).toEqual("(no data)");
|
|
done();
|
|
});
|
|
});
|
|
|
|
it("Missing Variance label is customized generally and for certain series", (done) => {
|
|
const testWrapper: TestWrapper = new TestWrapper(true, 0);
|
|
|
|
castZeroToNullOrReturnBack(testWrapper.dataView);
|
|
|
|
testWrapper.dataView.metadata.objects = {
|
|
values: {
|
|
treatEmptyValuesAsZero: false,
|
|
},
|
|
variance: {
|
|
noValueLabel: "no data",
|
|
},
|
|
};
|
|
|
|
testWrapper.dataView.metadata.columns[1].objects = {
|
|
variance: {
|
|
noValueLabel: "-",
|
|
},
|
|
};
|
|
|
|
testWrapper.visualBuilder.updateRenderTimeout(testWrapper.dataView, () => {
|
|
const naVariance = testWrapper.visualBuilder.mainChartNAVarance.textContent;
|
|
expect(naVariance).toEqual("(-)");
|
|
done();
|
|
});
|
|
});
|
|
|
|
it("Stale Data is enabled but Dates are actual", (done) => {
|
|
const testWrapper: TestWrapper = new TestWrapper(true, 0);
|
|
|
|
castZeroToNullOrReturnBack(testWrapper.dataView);
|
|
|
|
testWrapper.dataView.metadata.objects = {
|
|
staleData: {
|
|
show: true,
|
|
staleDataText: "label {$1}",
|
|
},
|
|
subtitle: {
|
|
show: true,
|
|
},
|
|
values: {
|
|
showLatterAvailableValue: false,
|
|
treatEmptyValuesAsZero: true,
|
|
},
|
|
};
|
|
|
|
testWrapper.visualBuilder.updateRenderTimeout(testWrapper.dataView, () => {
|
|
const sdIcon = testWrapper.visualBuilder.staleIcon;
|
|
expect(sdIcon).toBeNull;
|
|
done();
|
|
});
|
|
});
|
|
|
|
it("Stale Data is enabled and be shown", (done) => {
|
|
const testWrapper: TestWrapper = new TestWrapper(true, 0, 1);
|
|
|
|
castZeroToNullOrReturnBack(testWrapper.dataView);
|
|
|
|
testWrapper.dataView.metadata.objects = {
|
|
staleData: {
|
|
show: true,
|
|
staleDataText: "label {$1}",
|
|
},
|
|
subtitle: {
|
|
show: true,
|
|
},
|
|
};
|
|
|
|
testWrapper.visualBuilder.updateRenderTimeout(testWrapper.dataView, () => {
|
|
const sdIcon = testWrapper.visualBuilder.staleIcon;
|
|
expect(document.body.contains(sdIcon)).toBeTruthy();
|
|
expect(sdIcon).not.toBeNull();
|
|
done();
|
|
});
|
|
});
|
|
|
|
it("Stale Data is enabled but Threshold Days are actual", (done) => {
|
|
const testWrapper: TestWrapper = new TestWrapper(true, 0, 1);
|
|
|
|
castZeroToNullOrReturnBack(testWrapper.dataView);
|
|
|
|
testWrapper.dataView.metadata.objects = {
|
|
staleData: {
|
|
show: true,
|
|
staleDataText: "label {$1}",
|
|
staleDataThreshold: 1,
|
|
},
|
|
subtitle: {
|
|
show: true,
|
|
},
|
|
};
|
|
|
|
testWrapper.visualBuilder.updateRenderTimeout(testWrapper.dataView, () => {
|
|
const sdIcon = testWrapper.visualBuilder.staleIcon;
|
|
expect(sdIcon).toBeNull();
|
|
done();
|
|
});
|
|
});
|
|
|
|
it("Stale Data is enabled but One of the metrics have more obsolete data than others", (done) => {
|
|
const testWrapper: TestWrapper = new TestWrapper(true, 0, 1);
|
|
|
|
castZeroToNullOrReturnBack(testWrapper.dataView);
|
|
|
|
testWrapper.dataView.metadata.objects = {
|
|
staleData: {
|
|
show: true,
|
|
staleDataText: "label {$1}",
|
|
staleDataThreshold: 1,
|
|
},
|
|
subtitle: {
|
|
show: true,
|
|
},
|
|
values: {
|
|
showLatterAvailableValue: true,
|
|
treatEmptyValuesAsZero: false,
|
|
},
|
|
};
|
|
|
|
testWrapper.visualBuilder.updateRenderTimeout(testWrapper.dataView, () => {
|
|
const sdIcon = testWrapper.visualBuilder.staleIcon;
|
|
expect(document.body.contains(sdIcon)).toBeTruthy();
|
|
expect(sdIcon).not.toBeNull;
|
|
done();
|
|
});
|
|
});
|
|
|
|
it("Stale Data is enabled and has sufficient threshold days to handle any metrics, even if one of them more obsolete", (done) => {
|
|
const testWrapper: TestWrapper = new TestWrapper(true, 0, 1);
|
|
|
|
castZeroToNullOrReturnBack(testWrapper.dataView);
|
|
|
|
testWrapper.dataView.metadata.objects = {
|
|
staleData: {
|
|
show: true,
|
|
staleDataText: "label {$1}",
|
|
staleDataThreshold: 4,
|
|
},
|
|
subtitle: {
|
|
show: true,
|
|
},
|
|
values: {
|
|
showLatterAvailableValue: true,
|
|
treatEmptyValuesAsZero: false,
|
|
},
|
|
};
|
|
|
|
testWrapper.visualBuilder.updateRenderTimeout(testWrapper.dataView, () => {
|
|
const sdIcon = testWrapper.visualBuilder.staleIcon;
|
|
expect(sdIcon).toBeNull();
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("DOM", () => {
|
|
it("root element should be defined in DOM", (done) => {
|
|
const testWrapper: TestWrapper = new TestWrapper();
|
|
|
|
testWrapper.visualBuilder.updateRenderTimeout(testWrapper.dataView, () => {
|
|
expect(document.body.contains(testWrapper.visualBuilder.root)).toBeTruthy();
|
|
|
|
done();
|
|
});
|
|
});
|
|
|
|
describe("Main Chart", () => {
|
|
it("the main chart should be rendered", (done) => {
|
|
const testWrapper: TestWrapper = new TestWrapper();
|
|
|
|
testWrapper.visualBuilder.updateRenderTimeout(testWrapper.dataView, () => {
|
|
expect(document.body.contains(testWrapper.visualBuilder.mainChart)).toBeTruthy();
|
|
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("LineComponent", () => {
|
|
let testWrapper: TestWrapper;
|
|
let lineComponent: LineComponent;
|
|
|
|
beforeEach(() => {
|
|
testWrapper = new TestWrapper();
|
|
const element = d3Select(testWrapper.visualBuilder.line);
|
|
|
|
lineComponent = new LineComponent({
|
|
element: element,
|
|
eventDispatcher: dispatch(...Object.keys(EventName)),
|
|
});
|
|
});
|
|
|
|
it("component must be rendered in DOM", () => {
|
|
const minDate: Date = new Date(2018, 1, 1);
|
|
const maxDate: Date = new Date(2018, 5, 1);
|
|
|
|
const minValue: number = 0;
|
|
const maxValue: number = 1000;
|
|
|
|
const lineRenderOptions: ILineComponentRenderOptions = {
|
|
alternativeColor: "blue",
|
|
color: "green",
|
|
current: undefined,
|
|
isLine: false,
|
|
points: [
|
|
{
|
|
index: 0,
|
|
x: new Date(2018, 1, 1),
|
|
y: 0,
|
|
},
|
|
{
|
|
index: 1,
|
|
x: new Date(2018, 2, 1),
|
|
y: 500,
|
|
},
|
|
{
|
|
index: 2,
|
|
x: new Date(2018, 3, 1),
|
|
y: 700,
|
|
},
|
|
{
|
|
index: 3,
|
|
x: new Date(2018, 4, 1),
|
|
y: 200,
|
|
},
|
|
{
|
|
index: 4,
|
|
x: new Date(2018, 5, 1),
|
|
y: 1000,
|
|
},
|
|
],
|
|
thickness: 2,
|
|
type: DataRepresentationPointGradientType.line,
|
|
viewport: {
|
|
height: 500,
|
|
width: 500,
|
|
},
|
|
x: {
|
|
initialMax: maxDate,
|
|
initialMin: minDate,
|
|
max: maxDate,
|
|
min: minDate,
|
|
scale: DataRepresentationScale
|
|
.CREATE()
|
|
.domain(
|
|
[minDate, maxDate],
|
|
DataRepresentationTypeEnum.DateType,
|
|
),
|
|
},
|
|
y: {
|
|
initialMax: maxValue,
|
|
initialMin: minValue,
|
|
max: maxValue,
|
|
min: minValue,
|
|
scale: DataRepresentationScale
|
|
.CREATE()
|
|
.domain(
|
|
[minValue, maxValue],
|
|
DataRepresentationTypeEnum.NumberType,
|
|
),
|
|
},
|
|
|
|
};
|
|
lineComponent.render(lineRenderOptions);
|
|
|
|
expect(document.body.contains(testWrapper.visualBuilder.sparklineLine[0])).toBeTruthy();
|
|
});
|
|
|
|
afterEach(() => {
|
|
lineComponent.destroy();
|
|
lineComponent = null;
|
|
});
|
|
});
|
|
|
|
describe("Sparkline", () => {
|
|
it("sparkline component should be rendered", (done) => {
|
|
const testWrapper: TestWrapper = new TestWrapper();
|
|
testWrapper.visualBuilder.updateRenderTimeout(testWrapper.dataView, () => {
|
|
Array.from(testWrapper.visualBuilder.sparkline).forEach(
|
|
el => expect(document.body.contains(el)).toBeTrue()
|
|
);
|
|
|
|
done();
|
|
});
|
|
});
|
|
|
|
describe("Subtitle", () => {
|
|
it("subtitle of each sparkline should be rendered", (done) => {
|
|
const testWrapper: TestWrapper = new TestWrapper();
|
|
|
|
testWrapper.visualBuilder.updateRenderTimeout(testWrapper.dataView, () => {
|
|
Array.from(testWrapper.visualBuilder.sparklineSubtitle).forEach(
|
|
el => expect(document.body.contains(el)).toBeTrue()
|
|
);
|
|
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Line", () => {
|
|
it("line of each sparkline should be rendered", (done) => {
|
|
const testWrapper: TestWrapper = new TestWrapper();
|
|
|
|
testWrapper.visualBuilder.updateRenderTimeout(testWrapper.dataView, () => {
|
|
Array.from(testWrapper.visualBuilder.sparklineLine).forEach(
|
|
el => expect(document.body.contains(el)).toBeTrue()
|
|
);
|
|
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Subtitle", () => {
|
|
it("subtitle should be rendered if it's turned on via Format Panel explicitly", (done) => {
|
|
const testWrapper: TestWrapper = new TestWrapper();
|
|
|
|
testWrapper.dataView.metadata.objects = {
|
|
subtitle: {
|
|
show: true,
|
|
titleText: "Power BI rocks",
|
|
},
|
|
};
|
|
|
|
testWrapper.visualBuilder.updateRenderTimeout(testWrapper.dataView, () => {
|
|
const displayStatus = getComputedStyle(testWrapper.visualBuilder.subtitle).display;
|
|
expect(displayStatus).toEqual("flex");
|
|
const subtitleElm = testWrapper.visualBuilder.subtitle?.querySelector(".multiKpi_subtitle");
|
|
expect(subtitleElm?.textContent).toEqual("Power BI rocks");
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Value Utils", () => {
|
|
describe("isValueValid", () => {
|
|
it("should return true if a variance is valid", () => {
|
|
expect(isValueValid(99)).toBeTruthy();
|
|
});
|
|
|
|
it("should return false if a variance is not valid (NaN)", () => {
|
|
expect(isValueValid(NaN)).toBeFalsy();
|
|
});
|
|
|
|
it("should return false if a variance is not valid (undefined)", () => {
|
|
expect(isValueValid(undefined)).toBeFalsy();
|
|
});
|
|
|
|
it("should return false if a variance is not valid (null)", () => {
|
|
expect(isValueValid(null)).toBeFalsy();
|
|
});
|
|
|
|
it("should return false if a variance is not valid (Infinity)", () => {
|
|
expect(isValueValid(Infinity)).toBeFalsy();
|
|
});
|
|
|
|
it("should return false if a variance is not valid (-Infinity)", () => {
|
|
expect(isValueValid(-Infinity)).toBeFalsy();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("DataFormatter", () => {
|
|
describe("getFormattedVariance", () => {
|
|
it("should return N/A if a variance is not valid", () => {
|
|
expect(getFormattedValueWithFallback(NaN, null)).toBe("N/A");
|
|
});
|
|
|
|
it("should return 12.34K if precision is 2 and display units are 1000", () => {
|
|
const value: number = 12340;
|
|
const expectedValue: string = "12.34K";
|
|
|
|
const numericDescriptor: NumericDescriptor = new NumericDescriptor();
|
|
|
|
numericDescriptor.precision.value = 2;
|
|
numericDescriptor.displayUnits.value = 1000;
|
|
|
|
const actualValue: string = getFormattedValueWithFallback(
|
|
value,
|
|
numericDescriptor,
|
|
);
|
|
|
|
expect(actualValue).toBe(expectedValue);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("DataConverter", () => {
|
|
describe("findClosestDataPointByDate", () => {
|
|
let dataConverter: DataConverter;
|
|
let defaultDataPoint: IDataRepresentationPoint;
|
|
|
|
beforeEach(() => {
|
|
dataConverter = new DataConverter({ createSelectionIdBuilder });
|
|
|
|
defaultDataPoint = {
|
|
index: 2,
|
|
x: new Date(2018, 1, 1),
|
|
y: 100,
|
|
};
|
|
});
|
|
|
|
it("should return defaultDataPoint if date is undefined", () => {
|
|
const dataPoint: IDataRepresentationPoint = dataConverter.findClosestDataPointByDate(
|
|
[],
|
|
undefined,
|
|
defaultDataPoint,
|
|
);
|
|
|
|
expect(dataPoint).toBe(defaultDataPoint);
|
|
});
|
|
|
|
it("should return defaultDataPoint is dataPoints is an empty array", () => {
|
|
const dataPoint: IDataRepresentationPoint = dataConverter.findClosestDataPointByDate(
|
|
[],
|
|
new Date(),
|
|
defaultDataPoint,
|
|
);
|
|
|
|
expect(dataPoint).toBe(defaultDataPoint);
|
|
});
|
|
|
|
it("should return defaultDataPoint if there's no the closest dataPoint", () => {
|
|
const dataPoint: IDataRepresentationPoint = dataConverter.findClosestDataPointByDate(
|
|
[{
|
|
index: 0,
|
|
x: new Date(2018, 8, 8),
|
|
y: 200,
|
|
}],
|
|
new Date(2016, 1, 1),
|
|
defaultDataPoint,
|
|
);
|
|
|
|
expect(dataPoint).toBe(defaultDataPoint);
|
|
});
|
|
|
|
it("should return the closest dataPoint by date", () => {
|
|
const firstDataPoint: IDataRepresentationPoint = {
|
|
index: 0,
|
|
x: new Date(2018, 8, 8),
|
|
y: 200,
|
|
};
|
|
|
|
const dataPoint: IDataRepresentationPoint = dataConverter.findClosestDataPointByDate(
|
|
[firstDataPoint],
|
|
new Date(2018, 9, 9),
|
|
defaultDataPoint,
|
|
);
|
|
|
|
expect(dataPoint).toBe(firstDataPoint);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("KpiBaseDescriptor", () => {
|
|
let textFormattingDescriptor: KpiBaseDescriptor;
|
|
|
|
beforeEach(() => {
|
|
textFormattingDescriptor = new KpiBaseDescriptor();
|
|
});
|
|
|
|
describe("auto font size", () => {
|
|
it("fontSize should be visible if autoAdjustFontSize is false", () => {
|
|
textFormattingDescriptor.autoAdjustFontSize.value = false;
|
|
|
|
textFormattingDescriptor.onPreProcess();
|
|
|
|
expect(textFormattingDescriptor.seriesNameFontSize.visible).toBeTruthy();
|
|
expect(textFormattingDescriptor.valueFontSize.visible).toBeTruthy();
|
|
expect(textFormattingDescriptor.varianceFontSize.visible).toBeTruthy();
|
|
expect(textFormattingDescriptor.varianceNotAvailableFontSize.visible).toBeTruthy();
|
|
expect(textFormattingDescriptor.dateFontSize.visible).toBeTruthy();
|
|
});
|
|
|
|
it("fontSize should not be visible if autoAdjustFontSize is true", () => {
|
|
textFormattingDescriptor.autoAdjustFontSize.value = true;
|
|
|
|
textFormattingDescriptor.onPreProcess();
|
|
|
|
expect(textFormattingDescriptor.seriesNameFontSize.visible).toBeFalsy();
|
|
expect(textFormattingDescriptor.valueFontSize.visible).toBeFalsy();
|
|
expect(textFormattingDescriptor.varianceFontSize.visible).toBeFalsy();
|
|
expect(textFormattingDescriptor.varianceNotAvailableFontSize.visible).toBeFalsy();
|
|
expect(textFormattingDescriptor.dateFontSize.visible).toBeFalsy();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
function castZeroToNullOrReturnBack(dataView: powerbi.DataView): void {
|
|
dataView.categorical?.values?.forEach((x) => x.values.forEach((value, index, theArray) => {
|
|
if (value === 0) {
|
|
theArray[index] = null;
|
|
}
|
|
}));
|
|
}
|