Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
7d2e8db6
Commit
7d2e8db6
authored
Dec 19, 2019
by
Ezekiel Kigbo
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Added specs for getTasksByTypeData
Remove old fixture file
parent
70135cce
Changes
9
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
263 additions
and
266 deletions
+263
-266
ee/app/assets/javascripts/analytics/cycle_analytics/components/base.vue
...javascripts/analytics/cycle_analytics/components/base.vue
+50
-51
ee/app/assets/javascripts/analytics/cycle_analytics/store/fixture.js
...ts/javascripts/analytics/cycle_analytics/store/fixture.js
+0
-82
ee/app/assets/javascripts/analytics/cycle_analytics/store/getters.js
...ts/javascripts/analytics/cycle_analytics/store/getters.js
+13
-3
ee/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js
.../javascripts/analytics/cycle_analytics/store/mutations.js
+2
-2
ee/app/assets/javascripts/analytics/cycle_analytics/utils.js
ee/app/assets/javascripts/analytics/cycle_analytics/utils.js
+58
-24
ee/spec/frontend/analytics/cycle_analytics/components/base_spec.js
...rontend/analytics/cycle_analytics/components/base_spec.js
+16
-25
ee/spec/frontend/analytics/cycle_analytics/mock_data.js
ee/spec/frontend/analytics/cycle_analytics/mock_data.js
+6
-7
ee/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js
...rontend/analytics/cycle_analytics/store/mutations_spec.js
+32
-31
ee/spec/frontend/analytics/cycle_analytics/utils_spec.js
ee/spec/frontend/analytics/cycle_analytics/utils_spec.js
+86
-41
No files found.
ee/app/assets/javascripts/analytics/cycle_analytics/components/base.vue
View file @
7d2e8db6
...
...
@@ -7,7 +7,6 @@ import { s__, sprintf } from '~/locale';
import
{
getDateInPast
}
from
'
~/lib/utils/datetime_utility
'
;
import
{
featureAccessLevel
}
from
'
~/pages/projects/shared/permissions/constants
'
;
import
glFeatureFlagsMixin
from
'
~/vue_shared/mixins/gl_feature_flags_mixin
'
;
import
{
prepareLabelDatasetForChart
,
generateDatesInRange
}
from
'
../utils
'
;
import
{
PROJECTS_PER_PAGE
,
DEFAULT_DAYS_IN_PAST
}
from
'
../constants
'
;
import
GroupsDropdownFilter
from
'
../../shared/components/groups_dropdown_filter.vue
'
;
import
ProjectsDropdownFilter
from
'
../../shared/components/projects_dropdown_filter.vue
'
;
...
...
@@ -81,10 +80,16 @@ export default {
'
errorCode
'
,
'
startDate
'
,
'
endDate
'
,
// TODO: remove this
'
tasksByType
'
,
'
medians
'
,
]),
...
mapGetters
([
'
hasNoAccessError
'
,
'
currentGroupPath
'
,
'
durationChartPlottableData
'
]),
...
mapGetters
([
'
hasNoAccessError
'
,
'
currentGroupPath
'
,
'
durationChartPlottableData
'
,
'
tasksByTypeChartData
'
,
]),
shouldRenderEmptyState
()
{
return
!
this
.
selectedGroup
;
},
...
...
@@ -97,6 +102,14 @@ export default {
shouldDisplayDurationChart
()
{
return
!
this
.
isLoadingDurationChart
&&
!
this
.
isLoading
;
},
shouldDisplayTasksByTypeChart
()
{
return
(
!
this
.
isLoadingTasksByTypeChart
&&
!
this
.
isLoading
&&
this
.
tasksByTypeChartData
&&
this
.
tasksByTypeChartData
.
seriesData
);
},
dateRange
:
{
get
()
{
return
{
startDate
:
this
.
startDate
,
endDate
:
this
.
endDate
};
...
...
@@ -111,30 +124,6 @@ export default {
hasDateRangeSet
()
{
return
this
.
startDate
&&
this
.
endDate
;
},
typeOfWork
()
{
// generate settings for the tasksByType chart
// if (!this.hasDateRangeSet) {
// return { option: { legend: false }, datatset: [], range: [] };
// }
// const range = generateDatesInRange(this.startDate, this.endDate).reverse();
// // TODO: diff and data should be replaced with the tasksByTypeData getter
// const diff = range.length + 1;
// const rawData = typeOfWork(diff);
// const { data, seriesNames } = prepareLabelDatasetForChart({
// dataset: Object.values(rawData),
// range,
// });
return
{
option
:
{
legend
:
false
},
range
:
[],
data
:
[],
seriesNames
:
[],
};
},
chartDataDescription
()
{
if
(
this
.
selectedGroup
)
{
const
selectedProjectCount
=
this
.
setSelectedProjects
.
length
;
...
...
@@ -159,7 +148,6 @@ export default {
},
},
mounted
()
{
// console.log('this.tasksByType', this.tasksByType);
this
.
initDateRange
();
this
.
setFeatureFlags
({
hasDurationChart
:
this
.
glFeatures
.
cycleAnalyticsScatterplotEnabled
,
...
...
@@ -231,6 +219,9 @@ export default {
with_shared
:
false
,
order_by
:
LAST_ACTIVITY_AT
,
},
tasksByTypeChartOptions
:
{
legend
:
false
,
},
};
</
script
>
...
...
@@ -352,32 +343,40 @@ export default {
<gl-loading-icon
v-else-if=
"!isLoading"
size=
"md"
class=
"my-4 py-4"
/>
</template>
</div>
<div
v-if=
"hasDateRangeSet"
>
<!-- TODO: move into component file -->
<div
class=
"row"
>
<div
class=
"col-12"
>
<h2>
{{ __('Type of work') }}
</h2>
<p>
{{ __('Showing data for __ groups and __ projects from __ to __') }}
</p>
<
template
v-if=
"featureFlags.hasTasksByTypeChart"
>
<div
v-if=
"shouldDisplayTasksByTypeChart"
>
<!-- TODO: move into component file -->
<div
class=
"row"
>
<div
class=
"col-12"
>
<h2>
{{
__
(
'
Type of work
'
)
}}
</h2>
<p
v-if=
"tasksByTypeChartData"
>
{{
__
(
'
Showing data for __ groups and __ projects from __ to __
'
)
}}
</p>
</div>
</div>
</div>
<div
class=
"row"
>
<div
class=
"col-12"
>
<header>
<h3>
{{ __('Tasks by type') }}
</h3>
</header>
<section>
<gl-stacked-column-chart
:option=
"typeOfWork.option"
:data=
"typeOfWork.data"
:group-by=
"typeOfWork.range"
x-axis-type=
"category"
x-axis-title=
"Date"
y-axis-title=
"Number of tasks"
:series-names=
"typeOfWork.seriesNames"
/>
</section>
<div
v-if=
"tasksByTypeChartData"
class=
"row"
>
<div
class=
"col-12"
>
<header>
<h3>
{{
__
(
'
Tasks by type
'
)
}}
</h3>
</header>
<!-- TODO: no data available view -->
<section>
<gl-stacked-column-chart
:option=
"$options.tasksByTypeChartOptions"
:data=
"tasksByTypeChartData.seriesData"
:group-by=
"tasksByTypeChartData.range"
x-axis-type=
"category"
x-axis-title=
"Date"
y-axis-title=
"Number of tasks"
:series-names=
"tasksByTypeChartData.seriesNames"
/>
</section>
</div>
</div>
<div
v-else
class=
"bs-callout bs-callout-info"
>
{{
__
(
'
There is no data available. Please change your selection.
'
)
}}
</div>
</div>
</
div
>
</
template
>
</div>
</template>
ee/app/assets/javascripts/analytics/cycle_analytics/store/fixture.js
deleted
100644 → 0
View file @
70135cce
// TODO: replace this test data with an endpoint
import
{
__
}
from
'
~/locale
'
;
import
{
convertObjectPropsToCamelCase
}
from
'
~/lib/utils/common_utils
'
;
import
{
getDateInPast
}
from
'
~/lib/utils/datetime_utility
'
;
import
{
toYmd
}
from
'
../../shared/utils
'
;
const
today
=
new
Date
();
const
generateRange
=
(
limit
=
30
)
=>
[...
Array
(
limit
).
keys
()]
.
map
(
i
=>
{
const
d
=
getDateInPast
(
today
,
i
);
return
toYmd
(
new
Date
(
d
));
})
.
reverse
();
function
randomInt
(
range
)
{
return
Math
.
floor
(
Math
.
random
()
*
Math
.
floor
(
range
));
}
function
arrayToObject
(
arr
)
{
return
arr
.
reduce
((
acc
,
curr
)
=>
{
const
[
key
,
value
]
=
curr
;
return
{
...
acc
,
[
key
]:
value
};
},
{});
}
const
genSeries
=
dayRange
=>
arrayToObject
(
generateRange
(
dayRange
).
map
(
key
=>
[
key
,
randomInt
(
100
)]));
const
generateApiResponse
=
dayRange
=>
convertObjectPropsToCamelCase
(
[
{
label
:
{
id
:
1
,
title
:
__
(
'
Bug
'
),
color
:
'
#428BCA
'
,
text_color
:
'
#FFFFFF
'
,
},
series
:
[
genSeries
(
dayRange
)],
},
{
label
:
{
id
:
3
,
title
:
__
(
'
Backstage
'
),
color
:
'
#327BCA
'
,
text_color
:
'
#FFFFFF
'
,
},
series
:
[
genSeries
(
dayRange
)],
},
{
label
:
{
id
:
2
,
title
:
__
(
'
Feature
'
),
color
:
'
#428BCA
'
,
text_color
:
'
#FFFFFF
'
,
},
series
:
[
genSeries
(
dayRange
)],
},
],
{
deep
:
true
},
);
const
transformResponseToLabelHash
=
data
=>
data
.
reduce
(
(
acc
,
{
label
:
{
id
,
...
labelRest
},
series
})
=>
({
...
acc
,
[
id
]:
{
label
:
{
id
,
...
labelRest
},
series
,
},
}),
{},
);
export
const
typeOfWork
=
dayRange
=>
transformResponseToLabelHash
(
convertObjectPropsToCamelCase
(
generateApiResponse
(
dayRange
),
{
deep
:
true
}),
);
export
default
{};
ee/app/assets/javascripts/analytics/cycle_analytics/store/getters.js
View file @
7d2e8db6
import
dateFormat
from
'
dateformat
'
;
import
httpStatus
from
'
~/lib/utils/http_status
'
;
import
{
dateFormats
}
from
'
../../shared/constants
'
;
import
{
getDurationChartData
}
from
'
../utils
'
;
import
{
getDurationChartData
,
getTasksByTypeData
}
from
'
../utils
'
;
export
const
hasNoAccessError
=
state
=>
state
.
errorCode
===
httpStatus
.
FORBIDDEN
;
...
...
@@ -25,5 +25,15 @@ export const durationChartPlottableData = state => {
return
plottableData
.
length
?
plottableData
:
null
;
};
export
const
tasksByTypeData
=
state
=>
state
.
tasksByType
&&
state
.
tasksByType
.
data
?
state
.
tasksByType
.
data
:
{};
export
const
tasksByTypeChartData
=
({
tasksByType
,
startDate
,
endDate
})
=>
{
// TODO: remove this check, return empty data if need be
if
(
tasksByType
&&
tasksByType
.
data
.
length
)
{
return
getTasksByTypeData
({
data
:
tasksByType
.
data
,
startDate
,
endDate
,
});
}
return
{};
};
ee/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js
View file @
7d2e8db6
import
{
convertObjectPropsToCamelCase
}
from
'
~/lib/utils/common_utils
'
;
import
*
as
types
from
'
./mutation_types
'
;
import
{
transformRawStages
}
from
'
../utils
'
;
import
{
transformRawStages
,
transformRawTasksByTypeData
}
from
'
../utils
'
;
export
default
{
[
types
.
SET_FEATURE_FLAGS
](
state
,
featureFlags
)
{
...
...
@@ -142,7 +142,7 @@ export default {
state
.
isLoadingTasksByTypeChart
=
false
;
state
.
tasksByType
=
{
...
state
.
tasksByType
,
data
:
convertObjectPropsToCamelCase
(
data
,
{
deep
:
true
}
),
data
:
transformRawTasksByTypeData
(
data
),
};
},
[
types
.
REQUEST_CREATE_CUSTOM_STAGE
](
state
)
{
...
...
ee/app/assets/javascripts/analytics/cycle_analytics/utils.js
View file @
7d2e8db6
...
...
@@ -78,6 +78,28 @@ export const transformRawStages = (stages = []) =>
name
:
name
.
length
?
name
:
title
,
}));
export
const
arrayToObject
=
(
arr
=
[])
=>
{
return
arr
.
reduce
((
acc
,
curr
)
=>
{
const
[
key
,
value
]
=
curr
;
return
{
...
acc
,
[
key
]:
value
};
},
{});
};
// converts the series data into key value pairs
export
const
transformRawTasksByTypeData
=
(
data
=
[])
=>
{
// TODO: does processing here make sense? if so add specs
if
(
!
data
.
length
)
return
[];
return
data
.
map
(({
series
,
...
rest
})
=>
convertObjectPropsToCamelCase
(
{
...
rest
,
series
:
arrayToObject
(
series
),
},
{
deep
:
true
},
),
);
};
export
const
nestQueryStringKeys
=
(
obj
=
null
,
targetKey
=
''
)
=>
{
if
(
!
obj
||
!
isString
(
targetKey
)
||
!
targetKey
.
length
)
return
{};
return
Object
.
entries
(
obj
).
reduce
((
prev
,
[
key
,
value
])
=>
{
...
...
@@ -191,34 +213,44 @@ export const getDurationChartData = (data, startDate, endDate) => {
return
eventData
;
};
// takes the type of work data and converts to a k:v structure
export
const
transformRawTypeOfWorkData
=
raw
=>
{}
;
const
toUnix
=
datetime
=>
new
Date
(
datetime
).
getTime
();
export
const
orderByDate
=
(
a
,
b
)
=>
toUnix
(
a
)
-
toUnix
(
b
)
;
export
const
prepareLabelDatasetForChart
=
({
dataset
,
range
})
=>
dataset
.
reduce
(
(
acc
,
curr
)
=>
{
const
{
label
:
{
title
},
series
:
[
datapoints
],
}
=
curr
;
acc
.
seriesNames
=
[...
acc
.
seriesNames
,
title
];
acc
.
data
=
[...
acc
.
data
,
range
.
map
(
index
=>
(
datapoints
[
index
]
?
datapoints
[
index
]
:
0
))];
return
acc
;
},
{
data
:
[],
seriesNames
:
[]
},
);
// TODO: code blocks + specs
// The api only returns datapoints with a value, 0 values are ignored
const
zeroMissingDataPoints
=
({
data
,
defaultData
})
=>
{
// overwrites the default values with any value that was returned from the api
return
{
...
defaultData
,
...
data
};
};
export
const
flattenTaskByTypeSeries
=
series
=>
series
.
map
(
dataSet
=>
{
// ignore the date, just return the value
return
dataSet
[
1
];
});
// TODO: docblocks
// Array of values [date, value]
// ignore the date, just return the value, default sort by ascending date
export
const
flattenTaskByTypeSeries
=
(
series
=
{})
=>
Object
.
entries
(
series
)
.
sort
((
a
,
b
)
=>
orderByDate
(
a
[
0
],
b
[
0
]))
.
map
(
dataSet
=>
dataSet
[
1
]);
// TODO: docblocks
export
const
getTasksByTypeData
=
({
data
,
startDate
,
endDate
})
=>
{
// GROSS
export
const
getTasksByTypeData
=
({
data
=
[],
startDate
=
null
,
endDate
=
null
})
=>
{
// TODO: check that the date range and datapoint values are in the same order
const
range
=
getDatesInRange
(
startDate
,
endDate
,
toYmd
).
reverse
();
if
(
!
startDate
||
!
endDate
||
!
data
.
length
)
{
return
{
range
:
[],
seriesData
:
[],
seriesNames
:
[],
};
}
const
range
=
getDatesInRange
(
startDate
,
endDate
,
toYmd
).
sort
(
orderByDate
);
const
defaultData
=
range
.
reduce
(
(
acc
,
date
)
=>
({
...
acc
,
[
date
]:
0
,
}),
{},
);
// TODO: handle zero's?
// TODO: fixup seeded data so it falls in the correct date range
...
...
@@ -231,11 +263,13 @@ export const getTasksByTypeData = ({ data, startDate, endDate }) => {
// TODO: double check if BE fills in all the dates and adds zeros
acc
.
seriesNames
=
[...
acc
.
seriesNames
,
title
];
// TODO: maybe flatmap
acc
.
data
=
[...
acc
.
data
,
flattenTaskByTypeSeries
(
series
)];
// series is already an object at this point
const
fullData
=
zeroMissingDataPoints
({
data
:
series
,
defaultData
});
acc
.
seriesData
=
[...
acc
.
seriesData
,
flattenTaskByTypeSeries
(
fullData
)];
return
acc
;
},
{
d
ata
:
[],
seriesD
ata
:
[],
seriesNames
:
[],
},
);
...
...
ee/spec/frontend/analytics/cycle_analytics/components/base_spec.js
View file @
7d2e8db6
...
...
@@ -31,6 +31,7 @@ function createComponent({
withStageSelected
=
false
,
scatterplotEnabled
=
true
,
tasksByTypeChartEnabled
=
true
,
customizableCycleAnalyticsEnabled
=
false
,
}
=
{})
{
const
func
=
shallow
?
shallowMount
:
mount
;
const
comp
=
func
(
Component
,
{
...
...
@@ -46,6 +47,7 @@ function createComponent({
glFeatures
:
{
cycleAnalyticsScatterplotEnabled
:
scatterplotEnabled
,
tasksByTypeChart
:
tasksByTypeChartEnabled
,
customizableCycleAnalytics
:
customizableCycleAnalyticsEnabled
,
},
},
...
opts
,
...
...
@@ -166,7 +168,7 @@ describe('Cycle Analytics component', () => {
describe
(
'
after a filter has been selected
'
,
()
=>
{
describe
(
'
the user has access to the group
'
,
()
=>
{
beforeEach
(()
=>
{
wrapper
=
createComponent
({
withStageSelected
:
true
});
wrapper
=
createComponent
({
withStageSelected
:
true
,
tasksByTypeChartEnabled
:
false
});
});
it
(
'
hides the empty state
'
,
()
=>
{
...
...
@@ -218,6 +220,7 @@ describe('Cycle Analytics component', () => {
describe
(
'
with durationData
'
,
()
=>
{
beforeEach
(()
=>
{
mock
=
new
MockAdapter
(
axios
);
wrapper
.
vm
.
$store
.
dispatch
(
'
setDateRange
'
,
{
skipFetch
:
true
,
startDate
:
mockData
.
startDate
,
...
...
@@ -248,14 +251,10 @@ describe('Cycle Analytics component', () => {
},
shallow
:
false
,
withStageSelected
:
true
,
tasksByTypeChartEnabled
:
false
,
});
});
afterEach
(()
=>
{
wrapper
.
destroy
();
mock
.
restore
();
});
it
(
'
has the first stage selected by default
'
,
()
=>
{
const
first
=
selectStageNavItem
(
0
);
const
second
=
selectStageNavItem
(
1
);
...
...
@@ -281,6 +280,7 @@ describe('Cycle Analytics component', () => {
describe
(
'
the user does not have access to the group
'
,
()
=>
{
beforeEach
(()
=>
{
mock
=
new
MockAdapter
(
axios
);
wrapper
.
vm
.
$store
.
dispatch
(
'
setSelectedGroup
'
,
{
...
mockData
.
group
,
});
...
...
@@ -326,14 +326,11 @@ describe('Cycle Analytics component', () => {
'
stage-event-list
'
:
true
,
'
stage-nav-item
'
:
true
,
},
provide
:
{
glFeatures
:
{
customizableCycleAnalytics
:
true
,
},
},
},
shallow
:
false
,
withStageSelected
:
true
,
customizableCycleAnalyticsEnabled
:
true
,
tasksByTypeChartEnabled
:
false
,
});
});
...
...
@@ -357,6 +354,7 @@ describe('Cycle Analytics component', () => {
mockFetchStageData
=
true
,
mockFetchStageMedian
=
true
,
mockFetchDurationData
=
true
,
mockFetchTasksByTypeData
=
true
,
})
{
const
defaultStatus
=
200
;
const
defaultRequests
=
{
...
...
@@ -375,14 +373,15 @@ describe('Cycle Analytics component', () => {
endpoint
:
`/groups/
${
groupId
}
/-/labels`
,
response
:
[...
mockData
.
groupLabels
],
},
fetchTasksByTypeData
:
{
status
:
defaultStatus
,
endpoint
:
'
/-/analytics/type_of_work/tasks_by_type
'
,
response
:
{
...
mockData
.
tasksByTypeData
},
},
...
overrides
,
};
if
(
mockFetchTasksByTypeData
)
{
mock
.
onGet
(
/analytics
\/
type_of_work
\/
tasks_by_type/
)
.
reply
(
defaultStatus
,
{
...
mockData
.
tasksByTypeData
});
}
if
(
mockFetchDurationData
)
{
mock
.
onGet
(
/analytics
\/
cycle_analytics
\/
stages
\/\d
+
\/
duration_chart/
)
...
...
@@ -491,15 +490,7 @@ describe('Cycle Analytics component', () => {
it
(
'
will display an error if the fetchTasksByTypeData request fails
'
,
()
=>
{
expect
(
findFlashError
()).
toBeNull
();
mockRequestCycleAnalyticsData
({
overrides
:
{
fetchTasksByTypeData
:
{
endPoint
:
'
/-/analytics/type_of_work/tasks_by_type
'
,
status
:
httpStatusCodes
.
BAD_REQUEST
,
response
:
{
response
:
{
status
:
httpStatusCodes
.
BAD_REQUEST
}
},
},
},
});
mockRequestCycleAnalyticsData
({
mockFetchTasksByTypeData
:
false
});
return
selectGroupAndFindError
(
'
There was an error fetching data for the tasks by type chart
'
,
...
...
ee/spec/frontend/analytics/cycle_analytics/mock_data.js
View file @
7d2e8db6
...
...
@@ -7,6 +7,7 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import
{
getDateInPast
,
getDatesInRange
}
from
'
~/lib/utils/datetime_utility
'
;
import
{
mockLabels
}
from
'
../../../../../spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data
'
;
import
{
toYmd
}
from
'
ee/analytics/shared/utils
'
;
import
{
transformRawTasksByTypeData
}
from
'
ee/analytics/cycle_analytics/utils
'
;
const
endpoints
=
{
customizableCycleAnalyticsStagesAndEvents
:
'
analytics/cycle_analytics/stages.json
'
,
// customizable stages and events endpoint
...
...
@@ -70,8 +71,7 @@ export const stageMedians = defaultStages.reduce((acc, stage) => {
},
{});
export
const
endDate
=
new
Date
(
2019
,
0
,
14
);
// Limit to just 5 days data for testing
export
const
startDate
=
getDateInPast
(
endDate
,
4
);
export
const
startDate
=
getDateInPast
(
endDate
,
DEFAULT_DAYS_IN_PAST
);
export
const
rawIssueEvents
=
stageFixtures
.
issue
;
export
const
issueEvents
=
deepCamelCase
(
stageFixtures
.
issue
);
...
...
@@ -125,8 +125,8 @@ export const customStageEvents = [
const
dateRange
=
getDatesInRange
(
startDate
,
endDate
,
toYmd
);
export
const
tasksByTypeData
=
convertObjectPropsToCamelCase
(
getJSONFixture
(
'
analytics/type_of_work/tasks_by_type.json
'
).
map
(
labelData
=>
{
export
const
tasksByTypeData
=
getJSONFixture
(
'
analytics/type_of_work/tasks_by_type.json
'
).
map
(
labelData
=>
{
// add data points for our mock date range
const
maxValue
=
10
;
const
series
=
dateRange
.
map
(
date
=>
[
date
,
Math
.
floor
(
Math
.
random
()
*
Math
.
floor
(
maxValue
))]);
...
...
@@ -134,12 +134,11 @@ export const tasksByTypeData = convertObjectPropsToCamelCase(
...
labelData
,
series
,
};
}),
{
deep
:
true
,
},
);
export
const
transformedTasksByTypeData
=
transformRawTasksByTypeData
(
tasksByTypeData
);
export
const
rawDurationData
=
[
{
duration_in_seconds
:
1234000
,
...
...
ee/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js
View file @
7d2e8db6
...
...
@@ -18,6 +18,7 @@ import {
customizableStagesAndEvents
,
tasksByTypeData
,
transformedDurationData
,
transformedTasksByTypeData
,
}
from
'
../mock_data
'
;
let
state
=
null
;
...
...
@@ -32,34 +33,34 @@ describe('Cycle analytics mutations', () => {
});
it
.
each
`
mutation | stateKey | value
${
types
.
HIDE_CUSTOM_STAGE_FORM
}
|
${
'
isCreatingCustomStage
'
}
|
${
false
}
${
types
.
SHOW_CUSTOM_STAGE_FORM
}
|
${
'
isCreatingCustomStage
'
}
|
${
true
}
${
types
.
EDIT_CUSTOM_STAGE
}
|
${
'
isEditingCustomStage
'
}
|
${
true
}
${
types
.
REQUEST_STAGE_DATA
}
|
${
'
isLoadingStage
'
}
|
${
true
}
${
types
.
RECEIVE_STAGE_DATA_ERROR
}
|
${
'
isEmptyStage
'
}
|
${
true
}
${
types
.
RECEIVE_STAGE_DATA_ERROR
}
|
${
'
isLoadingStage
'
}
|
${
false
}
${
types
.
REQUEST_CYCLE_ANALYTICS_DATA
}
|
${
'
isLoading
'
}
|
${
true
}
${
types
.
REQUEST_GROUP_LABELS
}
|
${
'
labels
'
}
|
${[]}
${
types
.
RECEIVE_GROUP_LABELS_ERROR
}
|
${
'
labels
'
}
|
${[]}
${
types
.
RECEIVE_SUMMARY_DATA_ERROR
}
|
${
'
summary
'
}
|
${[]}
${
types
.
REQUEST_SUMMARY_DATA
}
|
${
'
summary
'
}
|
${[]}
${
types
.
RECEIVE_GROUP_STAGES_AND_EVENTS_ERROR
}
|
${
'
stages
'
}
|
${[]}
${
types
.
REQUEST_GROUP_STAGES_AND_EVENTS
}
|
${
'
stages
'
}
|
${[]}
${
types
.
RECEIVE_GROUP_STAGES_AND_EVENTS_ERROR
}
|
${
'
customStageFormEvents
'
}
|
${[]}
${
types
.
REQUEST_GROUP_STAGES_AND_EVENTS
}
|
${
'
customStageFormEvents
'
}
|
${[]}
${
types
.
REQUEST_CREATE_CUSTOM_STAGE
}
|
${
'
isSavingCustomStage
'
}
|
${
true
}
${
types
.
RECEIVE_CREATE_CUSTOM_STAGE_RESPONSE
}
|
${
'
isSavingCustomStage
'
}
|
${
false
}
${
types
.
REQUEST_TASKS_BY_TYPE_DATA
}
|
${
'
isLoading
ChartData
'
}
|
${
true
}
${
types
.
RECEIVE_TASKS_BY_TYPE_DATA_ERROR
}
|
${
'
isLoading
ChartData
'
}
|
${
false
}
${
types
.
REQUEST_UPDATE_STAGE
}
|
${
'
isLoading
'
}
|
${
true
}
${
types
.
RECEIVE_UPDATE_STAGE_RESPONSE
}
|
${
'
isLoading
'
}
|
${
false
}
${
types
.
REQUEST_REMOVE_STAGE
}
|
${
'
isLoading
'
}
|
${
true
}
${
types
.
RECEIVE_REMOVE_STAGE_RESPONSE
}
|
${
'
isLoading
'
}
|
${
false
}
${
types
.
REQUEST_DURATION_DATA
}
|
${
'
isLoadingDurationChart
'
}
|
${
true
}
${
types
.
RECEIVE_DURATION_DATA_ERROR
}
|
${
'
isLoadingDurationChart
'
}
|
${
false
}
${
types
.
REQUEST_STAGE_MEDIANS
}
|
${
'
medians
'
}
|
${{}}
$
{
types
.
RECEIVE_STAGE_MEDIANS_ERROR
}
|
${
'
medians
'
}
|
${{}}
mutation | stateKey
| value
${
types
.
HIDE_CUSTOM_STAGE_FORM
}
|
${
'
isCreatingCustomStage
'
}
|
${
false
}
${
types
.
SHOW_CUSTOM_STAGE_FORM
}
|
${
'
isCreatingCustomStage
'
}
|
${
true
}
${
types
.
EDIT_CUSTOM_STAGE
}
|
${
'
isEditingCustomStage
'
}
|
${
true
}
${
types
.
REQUEST_STAGE_DATA
}
|
${
'
isLoadingStage
'
}
|
${
true
}
${
types
.
RECEIVE_STAGE_DATA_ERROR
}
|
${
'
isEmptyStage
'
}
|
${
true
}
${
types
.
RECEIVE_STAGE_DATA_ERROR
}
|
${
'
isLoadingStage
'
}
|
${
false
}
${
types
.
REQUEST_CYCLE_ANALYTICS_DATA
}
|
${
'
isLoading
'
}
|
${
true
}
${
types
.
REQUEST_GROUP_LABELS
}
|
${
'
labels
'
}
|
${[]}
${
types
.
RECEIVE_GROUP_LABELS_ERROR
}
|
${
'
labels
'
}
|
${[]}
${
types
.
RECEIVE_SUMMARY_DATA_ERROR
}
|
${
'
summary
'
}
|
${[]}
${
types
.
REQUEST_SUMMARY_DATA
}
|
${
'
summary
'
}
|
${[]}
${
types
.
RECEIVE_GROUP_STAGES_AND_EVENTS_ERROR
}
|
${
'
stages
'
}
|
${[]}
${
types
.
REQUEST_GROUP_STAGES_AND_EVENTS
}
|
${
'
stages
'
}
|
${[]}
${
types
.
RECEIVE_GROUP_STAGES_AND_EVENTS_ERROR
}
|
${
'
customStageFormEvents
'
}
|
${[]}
${
types
.
REQUEST_GROUP_STAGES_AND_EVENTS
}
|
${
'
customStageFormEvents
'
}
|
${[]}
${
types
.
REQUEST_CREATE_CUSTOM_STAGE
}
|
${
'
isSavingCustomStage
'
}
|
${
true
}
${
types
.
RECEIVE_CREATE_CUSTOM_STAGE_RESPONSE
}
|
${
'
isSavingCustomStage
'
}
|
${
false
}
${
types
.
REQUEST_TASKS_BY_TYPE_DATA
}
|
${
'
isLoading
TasksByTypeChart
'
}
|
${
true
}
${
types
.
RECEIVE_TASKS_BY_TYPE_DATA_ERROR
}
|
${
'
isLoading
TasksByTypeChart
'
}
|
${
false
}
${
types
.
REQUEST_UPDATE_STAGE
}
|
${
'
isLoading
'
}
|
${
true
}
${
types
.
RECEIVE_UPDATE_STAGE_RESPONSE
}
|
${
'
isLoading
'
}
|
${
false
}
${
types
.
REQUEST_REMOVE_STAGE
}
|
${
'
isLoading
'
}
|
${
true
}
${
types
.
RECEIVE_REMOVE_STAGE_RESPONSE
}
|
${
'
isLoading
'
}
|
${
false
}
${
types
.
REQUEST_DURATION_DATA
}
|
${
'
isLoadingDurationChart
'
}
|
${
true
}
${
types
.
RECEIVE_DURATION_DATA_ERROR
}
|
${
'
isLoadingDurationChart
'
}
|
${
false
}
${
types
.
REQUEST_STAGE_MEDIANS
}
|
${
'
medians
'
}
|
${{}}
$
{
types
.
RECEIVE_STAGE_MEDIANS_ERROR
}
|
${
'
medians
'
}
|
${{}}
`('$mutation will set $stateKey=$value', ({ mutation, stateKey, value }) => {
mutations[mutation](state);
...
...
@@ -189,17 +190,17 @@ describe('Cycle analytics mutations', () => {
});
describe
(
`
${
types
.
RECEIVE_TASKS_BY_TYPE_DATA_SUCCESS
}
`
,
()
=>
{
it
(
'
sets isLoading
ChartData
to false
'
,
()
=>
{
it
(
'
sets isLoading
TasksByTypeChart
to false
'
,
()
=>
{
mutations
[
types
.
RECEIVE_TASKS_BY_TYPE_DATA_SUCCESS
](
state
,
{});
expect
(
state
.
isLoading
ChartData
).
toEqual
(
false
);
expect
(
state
.
isLoading
TasksByTypeChart
).
toEqual
(
false
);
});
it
(
'
sets tasksByType.data to the raw returned chart data
'
,
()
=>
{
state
=
{
tasksByType
:
{
data
:
null
}
};
mutations
[
types
.
RECEIVE_TASKS_BY_TYPE_DATA_SUCCESS
](
state
,
tasksByTypeData
);
expect
(
state
.
tasksByType
.
data
).
toEqual
(
tasksByTypeData
);
expect
(
state
.
tasksByType
.
data
).
toEqual
(
t
ransformedT
asksByTypeData
);
});
});
...
...
ee/spec/frontend/analytics/cycle_analytics/utils_spec.js
View file @
7d2e8db6
import
{
isNumber
}
from
'
underscore
'
;
import
{
getDatesInRange
}
from
'
~/lib/utils/datetime_utility
'
;
import
{
isStartEvent
,
isLabelEvent
,
...
...
@@ -12,7 +14,10 @@ import {
isPersistedStage
,
getTasksByTypeData
,
flattenTaskByTypeSeries
,
orderByDate
,
arrayToObject
,
// TODO: dedupe this?
}
from
'
ee/analytics/cycle_analytics/utils
'
;
import
{
toYmd
}
from
'
ee/analytics/shared/utils
'
;
import
{
customStageEvents
as
events
,
labelStartEvent
,
...
...
@@ -26,6 +31,7 @@ import {
issueStage
,
rawCustomStage
,
tasksByTypeData
,
transformedTasksByTypeData
,
}
from
'
./mock_data
'
;
const
labelEvents
=
[
labelStartEvent
,
labelStopEvent
].
map
(
i
=>
i
.
identifier
);
...
...
@@ -203,67 +209,106 @@ describe('Cycle analytics utils', () => {
});
});
describe
.
skip
(
'
flattenTaskByTypeSeries
'
,
()
=>
{});
describe
(
'
flattenTaskByTypeSeries
'
,
()
=>
{
const
dummySeries
=
arrayToObject
([
[
'
2019-01-16
'
,
40
],
[
'
2019-01-14
'
,
20
],
[
'
2019-01-12
'
,
10
],
[
'
2019-01-15
'
,
30
],
]);
describe
.
only
(
'
getTasksByTypeData
'
,
()
=>
{
let
transformed
=
{};
const
rawData
=
tasksByTypeData
;
const
labels
=
rawData
.
map
(
d
=>
{
const
{
label
}
=
d
;
return
label
.
title
;
let
transformedDummySeries
=
[];
beforeEach
(()
=>
{
transformedDummySeries
=
flattenTaskByTypeSeries
(
dummySeries
);
});
const
data
=
rawData
.
map
(
d
=>
{
const
{
series
}
=
d
;
return
flattenTaskByTypeSeries
(
series
);
it
(
'
extracts the value from an array of datetime / value pairs
'
,
()
=>
{
expect
(
transformedDummySeries
.
every
(
isNumber
)).
toEqual
(
true
);
Object
.
values
(
dummySeries
).
forEach
(
v
=>
{
expect
(
transformedDummySeries
.
includes
(
v
)).
toBeTruthy
();
});
});
const
range
=
[];
console
.
log
(
'
rawData
'
,
rawData
);
// console.log('labels', labels
);
console
.
log
(
'
data
'
,
data
);
it
(
'
sorts the items by the datetime parameter
'
,
()
=>
{
expect
(
transformedDummySeries
).
toEqual
([
10
,
20
,
30
,
40
]
);
}
);
}
);
beforeEach
(()
=>
{
transformed
=
getTasksByTypeData
({
data
:
rawData
,
startDate
,
endDate
});
describe
(
'
orderByDate
'
,
()
=>
{
it
(
'
sorts dates from the earliest to latest
'
,
()
=>
{
expect
([
'
2019-01-14
'
,
'
2019-01-12
'
,
'
2019-01-16
'
,
'
2019-01-15
'
].
sort
(
orderByDate
)).
toEqual
([
'
2019-01-12
'
,
'
2019-01-14
'
,
'
2019-01-15
'
,
'
2019-01-16
'
,
]);
});
});
it
(
'
will return an object with the properties needed for the chart
'
,
()
=>
{
[
'
seriesNames
'
,
'
data
'
,
'
range
'
].
forEach
(
key
=>
{
expect
(
transformed
).
toHaveProperty
(
key
);
});
describe
(
'
getTasksByTypeData
'
,
()
=>
{
let
transformed
=
{};
const
range
=
getDatesInRange
(
startDate
,
endDate
,
toYmd
);
const
seriesData
=
transformedTasksByTypeData
.
map
(({
series
})
=>
Object
.
values
(
series
));
const
labels
=
transformedTasksByTypeData
.
map
(
d
=>
{
const
{
label
}
=
d
;
return
label
.
title
;
});
describe
(
'
seriesNames
'
,
()
=>
{
it
(
'
returns the names of all the labels in the dataset
'
,
()
=>
{
expect
(
transformed
.
seriesNames
).
toEqual
(
labels
);
it
(
'
will return blank arrays if given no data
'
,
()
=>
{
[{
data
:
[],
startDate
,
endDate
},
[],
{}].
forEach
(
chartData
=>
{
transformed
=
getTasksByTypeData
(
chartData
);
[
'
seriesNames
'
,
'
seriesData
'
,
'
range
'
].
forEach
(
key
=>
{
expect
(
transformed
[
key
]).
toEqual
([]);
});
});
});
describe
(
'
range
'
,
()
=>
{
it
(
'
returns the date range as an array
'
,
()
=>
{
expect
(
transformed
.
range
).
toEqual
(
range
);
});
it
(
'
includes each day between the start date and end date
'
,
()
=>
{
expect
(
transformed
.
range
).
toEqual
(
range
);
describe
(
'
with data
'
,
()
=>
{
beforeEach
(()
=>
{
transformed
=
getTasksByTypeData
({
data
:
transformedTasksByTypeData
,
startDate
,
endDate
});
});
it
(
'
includes the start date and end date
'
,
()
=>
{
expect
(
transformed
.
range
).
toContain
(
startDate
);
expect
(
transformed
.
range
).
toContain
(
endDate
);
it
(
'
will return an object with the properties needed for the chart
'
,
()
=>
{
[
'
seriesNames
'
,
'
seriesData
'
,
'
range
'
].
forEach
(
key
=>
{
expect
(
transformed
).
toHaveProperty
(
key
);
});
});
});
describe
(
'
data
'
,
()
=>
{
it
(
'
returns an array of data points
'
,
()
=>
{
expect
(
transformed
.
data
).
toEqual
(
data
);
describe
(
'
seriesNames
'
,
()
=>
{
it
(
'
returns the names of all the labels in the dataset
'
,
()
=>
{
expect
(
transformed
.
seriesNames
).
toEqual
(
labels
);
});
});
it
(
'
contains an array of data for each label
'
,
()
=>
{
expect
(
transformed
.
data
.
length
).
toEqual
(
labels
.
length
);
describe
(
'
range
'
,
()
=>
{
it
(
'
returns the date range as an array
'
,
()
=>
{
expect
(
transformed
.
range
).
toEqual
(
range
);
});
it
(
'
the start date is the first element
'
,
()
=>
{
expect
(
transformed
.
range
[
0
]).
toEqual
(
toYmd
(
startDate
));
});
it
(
'
the end date is the last element
'
,
()
=>
{
expect
(
transformed
.
range
[
transformed
.
range
.
length
-
1
]).
toEqual
(
toYmd
(
endDate
));
});
});
it
(
'
contains a value for each day in the range
'
,
()
=>
{
transformed
.
data
.
forEach
(
d
=>
{
expect
(
d
.
length
).
toEqual
(
transformed
.
range
.
length
);
describe
(
'
data
'
,
()
=>
{
it
(
'
returns an array of data points
'
,
()
=>
{
expect
(
transformed
.
seriesData
).
toEqual
(
seriesData
);
});
it
(
'
contains an array of data for each label
'
,
()
=>
{
expect
(
transformed
.
seriesData
.
length
).
toEqual
(
labels
.
length
);
});
it
(
'
contains a value for each day in the range
'
,
()
=>
{
transformed
.
seriesData
.
forEach
(
d
=>
{
expect
(
d
.
length
).
toEqual
(
transformed
.
range
.
length
);
});
});
});
});
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment