Commit 7c0f1efa authored by Brandon Labuschagne's avatar Brandon Labuschagne Committed by Martin Wortschack

Add throughput table to MR analytics

This MR introduces a data table which providers further insight
into the data being displayed in the throughput chart.
parent e4695080
......@@ -710,3 +710,16 @@ export const dateFromParams = (year, month, day) => {
return date;
};
/**
* A utility function which computes the difference in seconds
* between 2 dates.
*
* @param {Date} startDate the start sate
* @param {Date} endDate the end date
*
* @return {Int} the difference in seconds
*/
export const differenceInSeconds = (startDate, endDate) => {
return (endDate.getTime() - startDate.getTime()) / 1000;
};
......@@ -446,6 +446,8 @@ $context-header-height: 60px;
$breadcrumb-min-height: 48px;
$home-panel-title-row-height: 64px;
$home-panel-avatar-mobile-size: 24px;
$issuable-title-max-width: 350px;
$milestone-title-max-width: 75px;
$gl-line-height: 16px;
$gl-line-height-18: 18px;
$gl-line-height-20: 20px;
......
<script>
import { getDateInPast } from '~/lib/utils/datetime_utility';
import { DEFAULT_NUMBER_OF_DAYS } from '../constants';
import ThroughputChart from './throughput_chart.vue';
import ThroughputTable from './throughput_table.vue';
export default {
name: 'MergeRequestAnalyticsApp',
components: {
ThroughputChart,
ThroughputTable,
},
data() {
return {
startDate: getDateInPast(new Date(), DEFAULT_NUMBER_OF_DAYS),
endDate: new Date(),
};
},
};
</script>
<template>
<div>
<div class="merge-request-analytics-wrapper">
<h3 data-testid="pageTitle" class="gl-mb-5">{{ __('Merge Request Analytics') }}</h3>
<throughput-chart />
<throughput-chart :start-date="startDate" :end-date="endDate" />
<throughput-table :start-date="startDate" :end-date="endDate" class="gl-mt-6" />
</div>
</template>
<script>
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { getDateInPast } from '~/lib/utils/datetime_utility';
import throughputChartQueryBuilder from '../graphql/throughput_chart_query_builder';
import { DEFAULT_NUMBER_OF_DAYS, THROUGHPUT_CHART_STRINGS } from '../constants';
import { THROUGHPUT_CHART_STRINGS } from '../constants';
export default {
name: 'ThroughputChart',
......@@ -13,11 +12,19 @@ export default {
GlLoadingIcon,
},
inject: ['fullPath'],
props: {
startDate: {
type: Date,
required: true,
},
endDate: {
type: Date,
required: true,
},
},
data() {
return {
throughputChartData: [],
startDate: getDateInPast(new Date(), DEFAULT_NUMBER_OF_DAYS),
endDate: new Date(),
hasError: false,
};
},
......
<script>
import dateFormat from 'dateformat';
import {
GlTable,
GlLink,
GlAvatarLink,
GlAvatar,
GlAvatarsInline,
GlTooltipDirective,
GlLoadingIcon,
GlAlert,
GlIcon,
} from '@gitlab/ui';
import { s__ } from '~/locale';
import { approximateDuration, differenceInSeconds } from '~/lib/utils/datetime_utility';
import { dateFormats } from '../../shared/constants';
import throughputTableQuery from '../graphql/queries/throughput_table.query.graphql';
import {
THROUGHPUT_TABLE_STRINGS,
MERGE_REQUEST_ID_PREFIX,
LINE_CHANGE_SYMBOLS,
ASSIGNEES_VISIBLE,
AVATAR_SIZE,
MAX_RECORDS,
THROUGHPUT_TABLE_TEST_IDS,
PIPELINE_STATUS_ICON_CLASSES,
} from '../constants';
const TH_TEST_ID = { 'data-testid': THROUGHPUT_TABLE_TEST_IDS.TABLE_HEADERS };
export default {
name: 'ThroughputTable',
components: {
GlTable,
GlLink,
GlAvatarLink,
GlAvatar,
GlAvatarsInline,
GlLoadingIcon,
GlAlert,
GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
},
inject: ['fullPath'],
tableHeaderFields: [
{
key: 'mr_details',
label: s__('MergeRequestAnalytics|Merge Request'),
tdClass: 'merge-request-analytics-td',
thAttr: TH_TEST_ID,
},
{
key: 'date_merged',
label: s__('MergeRequestAnalytics|Date Merged'),
tdClass: 'merge-request-analytics-td',
thAttr: TH_TEST_ID,
},
{
key: 'time_to_merge',
label: s__('MergeRequestAnalytics|Time to merge'),
tdClass: 'merge-request-analytics-td',
thAttr: TH_TEST_ID,
},
{
key: 'milestone',
label: s__('MergeRequestAnalytics|Milestone'),
tdClass: 'merge-request-analytics-td',
thAttr: TH_TEST_ID,
},
{
key: 'pipelines',
label: s__('MergeRequestAnalytics|Pipelines'),
tdClass: 'merge-request-analytics-td',
thAttr: TH_TEST_ID,
},
{
key: 'line_changes',
label: s__('MergeRequestAnalytics|Line changes'),
tdClass: 'merge-request-analytics-td',
thAttr: TH_TEST_ID,
},
{
key: 'assignees',
label: s__('MergeRequestAnalytics|Assignees'),
tdClass: 'merge-request-analytics-td',
thAttr: TH_TEST_ID,
},
],
props: {
startDate: {
type: Date,
required: true,
},
endDate: {
type: Date,
required: true,
},
},
data() {
return {
throughputTableData: [],
hasError: false,
};
},
apollo: {
throughputTableData: {
query: throughputTableQuery,
variables() {
return {
fullPath: this.fullPath,
limit: MAX_RECORDS,
startDate: dateFormat(this.startDate, dateFormats.isoDate),
endDate: dateFormat(this.endDate, dateFormats.isoDate),
};
},
update: data => data.project.mergeRequests.nodes,
error() {
this.hasError = true;
},
},
},
computed: {
tableDataAvailable() {
return this.throughputTableData.length;
},
tableDataLoading() {
return !this.hasError && this.$apollo.queries.throughputTableData.loading;
},
alertDetails() {
return {
class: this.hasError ? 'danger' : 'info',
message: this.hasError
? THROUGHPUT_TABLE_STRINGS.ERROR_FETCHING_DATA
: THROUGHPUT_TABLE_STRINGS.NO_DATA,
};
},
},
methods: {
formatMergeRequestId(id) {
return `${MERGE_REQUEST_ID_PREFIX}${id}`;
},
formatLineChangeAdditions(value) {
return `${LINE_CHANGE_SYMBOLS.ADDITIONS}${value}`;
},
formatLineChangeDeletions(value) {
return `${LINE_CHANGE_SYMBOLS.DELETITIONS}${value}`;
},
formatDateMerged(value) {
return dateFormat(value, dateFormats.isoDate);
},
computeTimeToMerge(createdAt, mergedAt) {
return approximateDuration(differenceInSeconds(new Date(createdAt), new Date(mergedAt)));
},
pipelineStatusClass(value) {
return PIPELINE_STATUS_ICON_CLASSES[value] === undefined
? PIPELINE_STATUS_ICON_CLASSES.default
: PIPELINE_STATUS_ICON_CLASSES[value];
},
},
assigneesVisible: ASSIGNEES_VISIBLE,
avatarSize: AVATAR_SIZE,
testIds: THROUGHPUT_TABLE_TEST_IDS,
};
</script>
<template>
<gl-loading-icon v-if="tableDataLoading" size="md" />
<gl-table
v-else-if="tableDataAvailable"
:fields="$options.tableHeaderFields"
:items="throughputTableData"
stacked="sm"
thead-class="thead-white gl-border-b-solid gl-border-b-1 gl-border-b-gray-100"
>
<template #cell(mr_details)="{ item }">
<div
class="gl-display-flex gl-flex-direction-column gl-flex-grow-1"
:data-testid="$options.testIds.MERGE_REQUEST_DETAILS"
>
<div class="merge-request-title str-truncated">
<gl-link
:href="item.webUrl"
target="_blank"
class="gl-font-weight-bold gl-text-gray-900"
>{{ item.title }}</gl-link
>
<ul class="horizontal-list gl-mb-0">
<li class="gl-mr-3">{{ formatMergeRequestId(item.iid) }}</li>
<li v-if="item.pipelines.nodes.length" class="gl-mr-3">
<gl-icon
:name="item.pipelines.nodes[0].detailedStatus.icon"
:class="pipelineStatusClass(item.pipelines.nodes[0].detailedStatus.icon)"
/>
</li>
<li
v-if="item.labels.nodes.length"
class="gl-mr-3"
:data-testid="$options.testIds.LABEL_DETAILS"
>
<span class="gl-display-flex gl-align-items-center"
><gl-icon name="label" class="gl-mr-1" /><span>{{
item.labels.nodes.length
}}</span></span
>
</li>
</ul>
</div>
</div>
</template>
<template #cell(date_merged)="{ item }">
<div :data-testid="$options.testIds.DATE_MERGED">{{ formatDateMerged(item.mergedAt) }}</div>
</template>
<template #cell(time_to_merge)="{ item }">
<div :data-testid="$options.testIds.TIME_TO_MERGE">
{{ computeTimeToMerge(item.createdAt, item.mergedAt) }}
</div>
</template>
<template #cell(milestone)="{ item }">
<div :data-testid="$options.testIds.MILESTONE">{{ item.milestone.title }}</div>
</template>
<template #cell(pipelines)="{ item }">
<div :data-testid="$options.testIds.PIPELINES">{{ item.pipelines.nodes.length }}</div>
</template>
<template #cell(line_changes)="{ item }">
<div :data-testid="$options.testIds.LINE_CHANGES">
<span class="gl-font-weight-bold gl-text-green-500">{{
formatLineChangeAdditions(item.diffStatsSummary.additions)
}}</span>
<span class="gl-font-weight-bold gl-text-red-500">{{
formatLineChangeDeletions(item.diffStatsSummary.deletions)
}}</span>
</div>
</template>
<template #cell(assignees)="{ item }">
<div :data-testid="$options.testIds.ASSIGNEES">
<gl-avatars-inline
:avatars="item.assignees.nodes"
:avatar-size="$options.avatarSize"
:max-visible="$options.assigneesVisible"
collapsed
>
<template #avatar="{ avatar }">
<gl-avatar-link v-gl-tooltip target="_blank" :href="avatar.webUrl" :title="avatar.name">
<gl-avatar :src="avatar.avatarUrl" :size="$options.avatarSize" />
</gl-avatar-link>
</template>
</gl-avatars-inline>
</div>
</template>
</gl-table>
<gl-alert v-else :variant="alertDetails.class" :dismissible="false" class="gl-mt-4">{{
alertDetails.message
}}</gl-alert>
</template>
import { __ } from '~/locale';
export const DEFAULT_NUMBER_OF_DAYS = 365;
export const MAX_RECORDS = 100;
export const ASSIGNEES_VISIBLE = 2;
export const AVATAR_SIZE = 24;
export const THROUGHPUT_CHART_STRINGS = {
CHART_TITLE: __('Throughput'),
Y_AXIS_TITLE: __('Merge Requests merged'),
......@@ -9,3 +13,34 @@ export const THROUGHPUT_CHART_STRINGS = {
NO_DATA: __('There is no chart data available.'),
ERROR_FETCHING_DATA: __('There was an error while fetching the chart data.'),
};
export const THROUGHPUT_TABLE_STRINGS = {
NO_DATA: __('There is no table data available.'),
ERROR_FETCHING_DATA: __('There was an error while fetching the table data.'),
};
export const MERGE_REQUEST_ID_PREFIX = '!';
export const LINE_CHANGE_SYMBOLS = {
ADDITIONS: '+',
DELETITIONS: '-',
};
export const THROUGHPUT_TABLE_TEST_IDS = {
TABLE_HEADERS: 'header',
MERGE_REQUEST_DETAILS: 'detailsCol',
LABEL_DETAILS: 'labelDetails',
DATE_MERGED: 'dateMergedCol',
TIME_TO_MERGE: 'timeToMergeCol',
MILESTONE: 'milestoneCol',
PIPELINES: 'pipelinesCol',
LINE_CHANGES: 'lineChangesCol',
ASSIGNEES: 'assigneesCol',
};
export const PIPELINE_STATUS_ICON_CLASSES = {
status_success: 'gl-text-green-500',
status_failed: 'gl-text-red-500',
status_pending: 'gl-text-orange-500',
default: 'gl-text-grey-500',
};
query($fullPath: ID!, $startDate: Time!, $endDate: Time!, $limit: Int!) {
project(fullPath: $fullPath) {
mergeRequests(first: $limit, mergedAfter: $startDate, mergedBefore: $endDate) {
nodes {
iid
title
createdAt
mergedAt
webUrl
milestone {
title
}
assignees {
nodes {
avatarUrl
name
webUrl
}
}
diffStatsSummary {
additions
deletions
}
labels {
nodes {
title
}
}
pipelines {
nodes {
detailedStatus {
icon
}
}
}
}
}
}
}
import { getMonthNames, dateFromParams } from '~/lib/utils/datetime_utility';
import { dateFormats } from '../shared/constants';
import dateFormat from 'dateformat';
/**
......@@ -18,12 +19,12 @@ import dateFormat from 'dateformat';
* @return {Array} the computed month data
*/
// eslint-disable-next-line import/prefer-default-export
export const computeMonthRangeData = (startDate, endDate, format = 'yyyy-mm-dd') => {
export const computeMonthRangeData = (startDate, endDate, format = dateFormats.isoDate) => {
const monthData = [];
const monthNames = getMonthNames(true);
for (
let dateCursor = endDate;
let dateCursor = new Date(endDate);
dateCursor >= startDate;
dateCursor.setMonth(dateCursor.getMonth() - 1)
) {
......
......@@ -4,11 +4,11 @@
}
.issue-title {
max-width: px-to-rem(350px);
max-width: px-to-rem($issuable-title-max-width);
}
.milestone-title {
max-width: px-to-rem(75px);
max-width: px-to-rem($milestone-title-max-width);
}
}
......
.merge-request-analytics-wrapper {
.merge-request-title {
max-width: px-to-rem($issuable-title-max-width);
}
.milestone-title {
max-width: px-to-rem($milestone-title-max-width);
}
}
import { shallowMount } from '@vue/test-utils';
import MergeRequestAnalyticsApp from 'ee/analytics/merge_request_analytics/components/app.vue';
import ThroughputChart from 'ee/analytics/merge_request_analytics/components/throughput_chart.vue';
import ThroughputTable from 'ee/analytics/merge_request_analytics/components/throughput_table.vue';
describe('MergeRequestAnalyticsApp', () => {
let wrapper;
......@@ -25,8 +26,10 @@ describe('MergeRequestAnalyticsApp', () => {
});
it('displays the throughput chart component', () => {
const throughputChartComponent = wrapper.find(ThroughputChart);
expect(wrapper.contains(ThroughputChart)).toBe(true);
});
expect(throughputChartComponent.exists()).toBe(true);
it('displays the throughput table component', () => {
expect(wrapper.contains(ThroughputTable)).toBe(true);
});
});
......@@ -3,9 +3,7 @@ import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import ThroughputChart from 'ee/analytics/merge_request_analytics/components/throughput_chart.vue';
import { THROUGHPUT_CHART_STRINGS } from 'ee/analytics/merge_request_analytics/constants';
import { throughputChartData } from '../mock_data';
const fullPath = 'gitlab-org/gitlab';
import { throughputChartData, startDate, endDate, fullPath } from '../mock_data';
describe('ThroughputChart', () => {
let wrapper;
......@@ -30,6 +28,10 @@ describe('ThroughputChart', () => {
provide: {
fullPath,
},
props: {
startDate,
endDate,
},
});
wrapper.setData(data);
......
import { mount } from '@vue/test-utils';
import { GlAlert, GlLoadingIcon, GlTable, GlIcon, GlAvatarsInline } from '@gitlab/ui';
import ThroughputTable from 'ee/analytics/merge_request_analytics/components/throughput_table.vue';
import {
THROUGHPUT_TABLE_STRINGS,
THROUGHPUT_TABLE_TEST_IDS as TEST_IDS,
} from 'ee/analytics/merge_request_analytics/constants';
import {
throughputTableData,
startDate,
endDate,
fullPath,
throughputTableHeaders,
} from '../mock_data';
describe('ThroughputTable', () => {
let wrapper;
const createComponent = ({ loading = false, data = {} } = {}) => {
const $apollo = {
queries: {
throughputTableData: {
loading,
},
},
};
wrapper = mount(ThroughputTable, {
mocks: { $apollo },
provide: {
fullPath,
},
props: {
startDate,
endDate,
},
});
wrapper.setData(data);
};
const displaysComponent = (component, visible) => {
expect(wrapper.contains(component)).toBe(visible);
};
const additionalData = data => {
wrapper.setData({
throughputTableData: [
{
...throughputTableData[0],
...data,
},
],
});
};
const findTable = () => wrapper.find(GlTable);
const findCol = testId => findTable().find(`[data-testid="${testId}"]`);
const findColSubItem = (colTestId, childTetestId) =>
findCol(colTestId).find(`[data-testid="${childTetestId}"]`);
const findColSubComponent = (colTestId, childComponent) =>
findCol(colTestId).find(childComponent);
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('default state', () => {
beforeEach(() => {
createComponent();
});
it('displays an empty state message when there is no data', () => {
const alert = wrapper.find(GlAlert);
expect(alert.exists()).toBe(true);
expect(alert.text()).toBe(THROUGHPUT_TABLE_STRINGS.NO_DATA);
});
it('does not display a loading icon', () => {
displaysComponent(GlLoadingIcon, false);
});
it('does not display the table', () => {
displaysComponent(GlTable, false);
});
});
describe('while loading', () => {
beforeEach(() => {
createComponent({ loading: true });
});
it('displays a loading icon', () => {
displaysComponent(GlLoadingIcon, true);
});
it('does not display the table', () => {
displaysComponent(GlTable, false);
});
it('does not display the no data message', () => {
displaysComponent(GlAlert, false);
});
});
describe('with data', () => {
beforeEach(() => {
createComponent({ data: { throughputTableData } });
});
it('displays the table', () => {
displaysComponent(GlTable, true);
});
it('does not display a loading icon', () => {
displaysComponent(GlLoadingIcon, false);
});
it('does not display the no data message', () => {
displaysComponent(GlAlert, false);
});
describe('table fields', () => {
it('displays the correct table headers', () => {
const headers = findTable().findAll(`[data-testid="${TEST_IDS.TABLE_HEADERS}"]`);
expect(headers).toHaveLength(throughputTableHeaders.length);
throughputTableHeaders.forEach((headerText, i) =>
expect(headers.at(i).text()).toEqual(headerText),
);
});
describe('displays the correct merge request details', () => {
it('includes the correct title and IID', () => {
const { title, iid } = throughputTableData[0];
expect(findCol(TEST_IDS.MERGE_REQUEST_DETAILS).text()).toBe(`${title} !${iid}`);
});
it('does not include any icons by default', () => {
const icon = findColSubComponent(TEST_IDS.MERGE_REQUEST_DETAILS, GlIcon);
expect(icon.exists()).toBe(false);
});
it('includes a label icon and count when available', async () => {
additionalData({
labels: {
nodes: [{ title: 'Brinix' }],
},
});
await wrapper.vm.$nextTick();
const labelDetails = findColSubItem(
TEST_IDS.MERGE_REQUEST_DETAILS,
TEST_IDS.LABEL_DETAILS,
);
const icon = labelDetails.find(GlIcon);
expect(labelDetails.text()).toBe('1');
expect(icon.exists()).toBe(true);
expect(icon.props('name')).toBe('label');
});
it('includes a pipeline icon and when available', async () => {
const iconName = 'status_canceled';
additionalData({
pipelines: {
nodes: [
{
detailedStatus: { icon: iconName },
},
],
},
});
await wrapper.vm.$nextTick();
const icon = findColSubComponent(TEST_IDS.MERGE_REQUEST_DETAILS, GlIcon);
expect(icon.find(GlIcon).exists()).toBe(true);
expect(icon.props('name')).toBe(iconName);
});
});
it('displays the correct date merged', () => {
expect(findCol(TEST_IDS.DATE_MERGED).text()).toBe('2020-08-06');
});
it('displays the correct time to merge', () => {
expect(findCol(TEST_IDS.TIME_TO_MERGE).text()).toBe('4 minutes');
});
it('displays the correct milestone', () => {
expect(findCol(TEST_IDS.MILESTONE).text()).toBe('v1.0');
});
it('displays the correct pipeline count', () => {
expect(findCol(TEST_IDS.PIPELINES).text()).toBe('0');
});
it('displays the correctly formatted line changes', () => {
expect(findCol(TEST_IDS.LINE_CHANGES).text()).toBe('+2 -1');
});
it('displays the correct assignees data', () => {
const assignees = findColSubComponent(TEST_IDS.ASSIGNEES, GlAvatarsInline);
expect(assignees.exists()).toBe(true);
expect(assignees.props('avatars')).toBe(throughputTableData[0].assignees.nodes);
});
});
});
describe('with errors', () => {
beforeEach(() => {
createComponent({ data: { hasError: true } });
});
it('does not display the table', () => {
displaysComponent(GlTable, false);
});
it('does not display a loading icon', () => {
displaysComponent(GlLoadingIcon, false);
});
it('displays an error message', () => {
const alert = wrapper.find(GlAlert);
expect(alert.exists()).toBe(true);
expect(alert.text()).toBe(THROUGHPUT_TABLE_STRINGS.ERROR_FETCHING_DATA);
});
});
});
export const startDate = new Date('2020-05-01');
export const endDate = new Date('2020-08-01');
export const fullPath = 'gitlab-org/gitlab';
export const throughputChartData = {
May: { count: 2, __typename: 'MergeRequestConnection' },
Jun: { count: 4, __typename: 'MergeRequestConnection' },
......@@ -40,3 +45,41 @@ export const throughputChartQuery = `query ($fullPath: ID!) {
}
}
`;
export const throughputTableHeaders = [
'Merge Request',
'Date Merged',
'Time to merge',
'Milestone',
'Pipelines',
'Line changes',
'Assignees',
];
export const throughputTableData = [
{
iid: '1',
title: 'Update README.md',
createdAt: '2020-08-06T16:53:50Z',
mergedAt: '2020-08-06T16:57:53Z',
webUrl: 'http://127.0.0.1:3001/gitlab-org/gitlab-shell/-/merge_requests/11',
milestone: { title: 'v1.0' },
assignees: {
nodes: [
{
avatarUrl:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
name: 'Administrator',
webUrl: 'http://127.0.0.1:3001/root',
},
],
},
diffStatsSummary: { additions: 2, deletions: 1 },
labels: {
nodes: [],
},
pipelines: {
nodes: [],
},
},
];
......@@ -14993,6 +14993,27 @@ msgstr ""
msgid "MergeConflict|origin//their changes"
msgstr ""
msgid "MergeRequestAnalytics|Assignees"
msgstr ""
msgid "MergeRequestAnalytics|Date Merged"
msgstr ""
msgid "MergeRequestAnalytics|Line changes"
msgstr ""
msgid "MergeRequestAnalytics|Merge Request"
msgstr ""
msgid "MergeRequestAnalytics|Milestone"
msgstr ""
msgid "MergeRequestAnalytics|Pipelines"
msgstr ""
msgid "MergeRequestAnalytics|Time to merge"
msgstr ""
msgid "MergeRequestDiffs|Commenting on lines %{selectStart}start%{selectEnd} to %{end}"
msgstr ""
......@@ -24491,6 +24512,9 @@ msgstr ""
msgid "There is no data available. Please change your selection."
msgstr ""
msgid "There is no table data available."
msgstr ""
msgid "There is too much data to calculate. Please change your selection."
msgstr ""
......@@ -24659,6 +24683,9 @@ msgstr ""
msgid "There was an error while fetching the chart data."
msgstr ""
msgid "There was an error while fetching the table data."
msgstr ""
msgid "There was an error while fetching value stream analytics data."
msgstr ""
......
......@@ -639,3 +639,17 @@ describe('dateFromParams', () => {
expect(date.getDate()).toBe(expectedDate.getDate());
});
});
describe('differenceInSeconds', () => {
const startDateTime = new Date('2019-07-17T00:00:00.000Z');
it.each`
startDate | endDate | expected
${startDateTime} | ${new Date('2019-07-17T00:00:00.000Z')} | ${0}
${startDateTime} | ${new Date('2019-07-17T12:00:00.000Z')} | ${43200}
${startDateTime} | ${new Date('2019-07-18T00:00:00.000Z')} | ${86400}
${new Date('2019-07-18T00:00:00.000Z')} | ${startDateTime} | ${-86400}
`('returns $expected for $endDate - $startDate', ({ startDate, endDate, expected }) => {
expect(datetimeUtility.differenceInSeconds(startDate, endDate)).toBe(expected);
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment