diff --git a/.gitignore b/.gitignore
index 63ccaa26d..20de3e0a3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@ db/*.sqlite3
*.log
tmp/
.rvmrc
+.ruby-version
.sass-cache/
*.DS_Store
config/deploy.rb
diff --git a/app/assets/javascripts/actions/actionTypes.js b/app/assets/javascripts/actions/actionTypes.js
index 83327b1ce..02d194f54 100644
--- a/app/assets/javascripts/actions/actionTypes.js
+++ b/app/assets/javascripts/actions/actionTypes.js
@@ -1,4 +1,4 @@
-import keyMirror from 'keymirror';
+import keyMirror from "keymirror";
export default keyMirror({
REQUEST_PROJECT_BOARD: null,
@@ -10,5 +10,5 @@ export default keyMirror({
COLUMN_CHILLY_BIN: null,
COLUMN_BACKLOG: null,
COLUMN_IN_PROGRESS: null,
- COLUMN_DONE: null,
+ COLUMN_DONE: null
});
diff --git a/app/assets/javascripts/actions/column.js b/app/assets/javascripts/actions/column.js
index 4a1bb0e84..b9b00a0b7 100644
--- a/app/assets/javascripts/actions/column.js
+++ b/app/assets/javascripts/actions/column.js
@@ -1,41 +1,42 @@
-import actionTypes from './actionTypes';
-import * as iteration from '../models/beta/iteration';
+import actionTypes from "./actionTypes";
+import * as iteration from "../models/beta/iteration";
-const setStoryChillyBin = (payload) => ({
+const setStoryChillyBin = payload => ({
type: actionTypes.COLUMN_CHILLY_BIN,
- data: payload,
+ data: payload
});
-const setStoryBacklog = (payload) => ({
+const setStoryBacklog = payload => ({
type: actionTypes.COLUMN_BACKLOG,
- data: payload,
+ data: payload
});
-const setStoryDone = (payload) => ({
+const setStoryDone = payload => ({
type: actionTypes.COLUMN_DONE,
- data: payload,
+ data: payload
});
export const getColumnType = (story, project) => {
- if(story.state === 'unscheduled') {
+ if (story.state === "unscheduled") {
return setStoryChillyBin(story);
}
- if(isBacklog(story, project)) {
+ if (isBacklog(story, project)) {
return setStoryBacklog(story);
}
return setStoryDone(story);
-}
+};
const setColumn = (dispatch, project) => story => {
var type = getColumnType(story, project);
return dispatch(type);
-}
+};
const isBacklog = (story, project) => {
const currentIteration = iteration.getCurrentIteration(project);
const storyIteration = iteration.getIterationForStory(story, project);
const isFromCurrentSprint = currentIteration === storyIteration;
- return story.state !== 'accepted' || isFromCurrentSprint;
-}
+ return story.state !== "accepted" || isFromCurrentSprint;
+};
-export const classifyStories = (dispatch, stories, project) => stories.map(setColumn(dispatch, project))
+export const classifyStories = (dispatch, stories, project) =>
+ stories.map(setColumn(dispatch, project));
diff --git a/app/assets/javascripts/actions/projectBoard.js b/app/assets/javascripts/actions/projectBoard.js
index 0b39cb66c..17a8af8af 100644
--- a/app/assets/javascripts/actions/projectBoard.js
+++ b/app/assets/javascripts/actions/projectBoard.js
@@ -1,30 +1,30 @@
-import * as ProjectBoard from 'models/beta/projectBoard';
-import actionTypes from './actionTypes';
-import { classifyStories } from './column';
-import { receiveUsers } from './user';
-import { receiveStories } from './story';
+import * as ProjectBoard from "models/beta/projectBoard";
+import actionTypes from "./actionTypes";
+import { classifyStories } from "./column";
+import { receiveUsers } from "./user";
+import { receiveStories } from "./story";
const requestProjectBoard = () => ({
type: actionTypes.REQUEST_PROJECT_BOARD
});
-const receiveProjectBoard = (projectId) => ({
+const receiveProjectBoard = projectId => ({
type: actionTypes.RECEIVE_PROJECT_BOARD,
data: projectId
});
-const errorRequestProjectBoard = (error) => ({
+const errorRequestProjectBoard = error => ({
type: actionTypes.ERROR_REQUEST_PROJECT_BOARD,
error: error
});
-const receiveProject = (project) => ({
+const receiveProject = project => ({
type: actionTypes.RECEIVE_PROJECT,
data: project
});
-export const fetchProjectBoard = (projectId) => {
- return (dispatch) => {
+export const fetchProjectBoard = projectId => {
+ return dispatch => {
dispatch(requestProjectBoard());
ProjectBoard.get(projectId)
@@ -35,8 +35,6 @@ export const fetchProjectBoard = (projectId) => {
dispatch(receiveProjectBoard(projectId));
classifyStories(dispatch, stories, project);
})
- .catch((error) =>
- dispatch(errorRequestProjectBoard(error))
- );
+ .catch(error => dispatch(errorRequestProjectBoard(error)));
};
-}
+};
diff --git a/app/assets/javascripts/components/Columns/ColumnItem.js b/app/assets/javascripts/components/Columns/ColumnItem.js
index ddcc5653a..e58b6da51 100644
--- a/app/assets/javascripts/components/Columns/ColumnItem.js
+++ b/app/assets/javascripts/components/Columns/ColumnItem.js
@@ -1,14 +1,16 @@
-import React from 'react'
-import Stories from '../stories/Stories';
+import React from "react";
+import Stories from "../stories/Stories";
-const Column = ({ title, stories }) => (
+const Column = ({ title, children }) => (
{title}
-
+
-
+
{children}
);
-export default Column
+export default Column;
diff --git a/app/assets/javascripts/components/projects/ProjectBoard.js b/app/assets/javascripts/components/projects/ProjectBoard.js
index 18f627496..081e2e014 100644
--- a/app/assets/javascripts/components/projects/ProjectBoard.js
+++ b/app/assets/javascripts/components/projects/ProjectBoard.js
@@ -1,7 +1,9 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import { fetchProjectBoard } from 'actions/projectBoard';
-import Column from '../Columns/ColumnItem';
+import React from "react";
+import { connect } from "react-redux";
+import { fetchProjectBoard } from "actions/projectBoard";
+import Column from "../Columns/ColumnItem";
+import Stories from "../stories/Stories";
+import Sprints from "../stories/Sprints";
class ProjectBoard extends React.Component {
componentWillMount() {
@@ -9,24 +11,27 @@ class ProjectBoard extends React.Component {
}
render() {
- if(!this.props.projectBoard.isFetched) {
+ if (!this.props.projectBoard.isFetched) {
return Loading;
}
return (
+
+
+
-
-
+ title={`${I18n.t("projects.show.backlog")} /
+ ${I18n.t("projects.show.in_progress")}`}>
+
+
+
+
+
+
);
}
@@ -43,11 +48,14 @@ const mapStateToProps = ({
project,
users,
stories,
- columns,
+ columns
});
const mapDispatchToProps = {
fetchProjectBoard
};
-export default connect(mapStateToProps, mapDispatchToProps)(ProjectBoard);
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(ProjectBoard);
diff --git a/app/assets/javascripts/components/stories/Sprint.js b/app/assets/javascripts/components/stories/Sprint.js
new file mode 100644
index 000000000..1c077e897
--- /dev/null
+++ b/app/assets/javascripts/components/stories/Sprint.js
@@ -0,0 +1,54 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+import Stories from "./Stories";
+
+const propTypes = {
+ number: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+ startDate: PropTypes.node,
+ points: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+ completedPoints: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+ stories: PropTypes.array
+};
+
+const defaultProps = {
+ number: 0,
+ startDate: 0,
+ points: 0,
+ stories: []
+};
+
+class Sprint extends Component {
+ constructor(props) {
+ super(props);
+ this.state = { isClosed: false };
+ this.toggleSprint = this.toggleSprint.bind(this);
+ }
+
+ toggleSprint() {
+ this.setState(prevState => ({ isClosed: !prevState.isClosed }));
+ }
+
+ render() {
+ const { number, startDate, points, completedPoints, stories } = this.props;
+ const closedStyle = this.state.isClosed && "Sprint__body--is-collapsed";
+ return (
+
+
+ {number} - {startDate}
+
+ {completedPoints > 0 && `${completedPoints} / `}
+ {points}
+
+
+
+ {stories && }
+
+
+ );
+ }
+}
+
+Sprint.propTypes = propTypes;
+Sprint.defaultProps = defaultProps;
+
+export default Sprint;
diff --git a/app/assets/javascripts/components/stories/Sprints.js b/app/assets/javascripts/components/stories/Sprints.js
new file mode 100644
index 000000000..16b8d945f
--- /dev/null
+++ b/app/assets/javascripts/components/stories/Sprints.js
@@ -0,0 +1,48 @@
+import React from "react";
+import PropTypes from "prop-types";
+import Sprint from "./Sprint";
+import * as Iteration from "models/beta/iteration";
+
+const propTypes = {
+ stories: PropTypes.array,
+ project: PropTypes.object
+};
+
+const defaultProps = {
+ stories: [],
+ project: {}
+};
+
+const renderSprints = sprints => {
+ return sprints.map(
+ (sprint, index) =>
+ sprint ? (
+
+ ) : null
+ );
+};
+
+const Sprints = ({ stories, project }) => {
+ const currentSprintNumber = Iteration.getCurrentIteration(project) || 0;
+ const sprints = Iteration.groupBySprints(
+ stories,
+ project,
+ currentSprintNumber
+ );
+
+ if (!stories.length) return null;
+
+ return {renderSprints(sprints)}
;
+};
+
+Sprint.propTypes = propTypes;
+Sprint.defaultProps = defaultProps;
+
+export default Sprints;
diff --git a/app/assets/javascripts/components/stories/Stories.js b/app/assets/javascripts/components/stories/Stories.js
index 5dfa1a3b7..c39aecccf 100644
--- a/app/assets/javascripts/components/stories/Stories.js
+++ b/app/assets/javascripts/components/stories/Stories.js
@@ -1,5 +1,5 @@
-import React from 'react';
-import StoryItem from '../story/StoryItem'
+import React from "react";
+import StoryItem from "../story/StoryItem";
const Stories = ({ stories }) => {
if (!stories.length) {
@@ -9,10 +9,7 @@ const Stories = ({ stories }) => {
return (
{stories.map(story => (
-
+
))}
);
diff --git a/app/assets/javascripts/libs/beta/constants.js b/app/assets/javascripts/libs/beta/constants.js
new file mode 100644
index 000000000..e0e79e871
--- /dev/null
+++ b/app/assets/javascripts/libs/beta/constants.js
@@ -0,0 +1,15 @@
+export const status = {
+ ACCEPTED: "accepted",
+ DELIVERED: "delivered",
+ STARTED: "started",
+ REJECTED: "rejected",
+ FINISHED: "finished",
+ UNSTARTED: "unstarted"
+};
+
+export const storyTypes = {
+ BUG: "bug",
+ CHORE: "chore",
+ FEATURE: "feature",
+ RELEASE: "release"
+};
diff --git a/app/assets/javascripts/models/beta/iteration.js b/app/assets/javascripts/models/beta/iteration.js
index 7c4680648..ad7f773e0 100644
--- a/app/assets/javascripts/models/beta/iteration.js
+++ b/app/assets/javascripts/models/beta/iteration.js
@@ -1,21 +1,160 @@
-import moment from 'moment';
+import moment from "moment";
+import * as Story from "./story";
const weeksBetween = (dateA, dateB) =>
- moment(new Date(dateA)).diff(moment(new Date(dateB)), 'week');
+ moment(new Date(dateA)).diff(moment(new Date(dateB)), "week");
const getIterationForDate = (date, project) => {
const weeks = weeksBetween(date, project.startDate);
const iterationQuantity = Math.ceil(weeks / project.iterationLength);
- return project.iterationLength > weeks ? iterationQuantity : iterationQuantity + 1;
+ return project.iterationLength > weeks
+ ? iterationQuantity
+ : iterationQuantity + 1;
};
-export const getCurrentIteration = (project) => (
- getIterationForDate(new Date(), project)
-);
+export const getDateForIterationNumber = (iterationNumber, project) => {
+ return moment(new Date(project.startDate))
+ .startOf("isoWeek")
+ .add(iterationNumber, "weeks")
+ .format("ddd MMM Do Y");
+};
+
+export const getCurrentIteration = project =>
+ getIterationForDate(new Date(), project);
export const getIterationForStory = (story, project) => {
- if (story.state === 'accepted') {
+ if (story.state === "accepted") {
return getIterationForDate(new Date(story.acceptedAt), project);
}
-}
+};
+
+const createSprint = (sprintNumber = 0, startDate = 0, isFiller = false, velocity) => ({
+ number: sprintNumber + 1,
+ startDate: startDate,
+ points: 0,
+ completedPoints: null,
+ stories: [],
+ isFiller: isFiller,
+ remainingPoints: velocity
+});
+
+const createFillerSprints = (size, initialNumber, project) => {
+ return _.times(size, (i) => {
+ const sprintNumber = initialNumber + i;
+ const sprint = createSprint(
+ sprintNumber,
+ getDateForIterationNumber(sprintNumber, project),
+ true,
+ project.defaultVelocity
+ );
+ return sprint;
+ })
+};
+
+const canTakeStory = (sprint, storyPoints) => {
+ if (!sprint.isFiller) {
+ return sprint.remainingPoints >= storyPoints;
+ }
+ return false;
+};
+
+const calculateFillerSprintsQuantity = (storyPoints, velocity) => {
+ return Math.ceil((storyPoints - velocity) / velocity);
+};
+
+const handleSprintsOverflow = (project, sprints, storyPoints, initialSprintNumber) => {
+ const overflow = calculateFillerSprintsQuantity(storyPoints, project.defaultVelocity);
+ const hasOverflown = overflow > 0;
+ const currentSprintNumber = sprints.length + initialSprintNumber;
+ if (hasOverflown) {
+ return [
+ ...sprints,
+ ...createFillerSprints(overflow, currentSprintNumber, project)
+ ];
+ }
+ return sprints;
+};
+
+const addStoryToSprint = (project, sprints, index, story) => {
+ sprints[index].stories.push(story);
+ sprints[index].points += Story.getPoints(story);
+ const lastValidSprintIndex = sprints.length - 2;
+
+ let previousFillerSprintsQuantity = 0;
+ for (let i = lastValidSprintIndex; i >= 0; i--) {
+ if (sprints[i].isFiller) {
+ previousFillerSprintsQuantity++;
+ } else {
+ break;
+ }
+ }
+ fillRemainingPoints(
+ sprints, Story.getPoints(story),
+ index, project.defaultVelocity,
+ previousFillerSprintsQuantity
+ );
+
+ sprints[index].completedPoints += Story.getCompletedPoints(story);
+
+ return sprints;
+};
+
+const fillRemainingPoints = (sprints, storyPoints, index, velocity, previousFillerSprintsQuantity) => {
+ if (previousFillerSprintsQuantity === 0) {
+ sprints[index].remainingPoints -= storyPoints;
+ } else {
+ sprints[index].remainingPoints = storyPoints - (previousFillerSprintsQuantity * velocity);
+ }
+};
+
+const createFirstSprint = (sprints, project) => {
+ sprints.push(createSprint(
+ 0,
+ getDateForIterationNumber(0, project),
+ undefined,
+ project.defaultVelocity,
+ ));
+};
+
+const addToSprintFromBacklog = (sprints, project, story) => {
+ const sprintIndex = sprints.length && sprints.length - 1;
+ const hasSpace = canTakeStory(sprints[sprintIndex], Story.getPoints(story));
+
+ if (hasSpace) {
+ return addStoryToSprint(project, sprints, sprintIndex, story);
+ }
+
+ sprints[sprintIndex + 1] = createSprint(
+ sprints.length + getCurrentIteration(project),
+ getDateForIterationNumber(sprints.length + getCurrentIteration(project), project),
+ undefined,
+ project.defaultVelocity
+ );
+
+ return addStoryToSprint(project, sprints, sprintIndex + 1, story);
+};
+
+export const groupBySprints = (stories = [], project, initialSprintNumber) => {
+ return stories.reduce((sprints, story) => {
+ const firstSprintIndex = 0;
+ const isFromSprintInProgress = !Story.isUnstarted(story);
+
+ sprints = handleSprintsOverflow(
+ project,
+ sprints,
+ Story.getPoints(story),
+ initialSprintNumber
+ );
+
+ if (sprints.length === 0) {
+ createFirstSprint(sprints, project);
+ }
+
+ if (isFromSprintInProgress) {
+ return addStoryToSprint(project, sprints, firstSprintIndex, story);
+ }
+
+ return addToSprintFromBacklog(sprints, project, story);
+ }, []);
+};
diff --git a/app/assets/javascripts/models/beta/story.js b/app/assets/javascripts/models/beta/story.js
index 0f9f45fe7..3a768cb2b 100644
--- a/app/assets/javascripts/models/beta/story.js
+++ b/app/assets/javascripts/models/beta/story.js
@@ -1,31 +1,52 @@
+import { status, storyTypes } from "libs/beta/constants";
-const compareValues = (a ,b) => {
+const compareValues = (a, b) => {
if (a > b) return 1;
if (a < b) return -1;
return 0;
-}
+};
export const comparePosition = (a, b) => {
const positionA = parseFloat(a.position);
const positionB = parseFloat(b.position);
return compareValues(positionA, positionB);
-}
+};
export const compareAcceptedAt = (a, b) => {
return compareValues(a.acceptedAt, b.acceptedAt);
-}
+};
export const compareDeliveredAt = (a, b) => {
return compareValues(a.deliveredAt, b.deliveredAt);
-}
+};
export const compareStartedAt = (a, b) => {
return compareValues(a.startedAt, b.startedAt);
-}
+};
+
+export const isUnestimatedFeature = story => {
+ return story.estimate === null && story.storyType === storyTypes.FEATURE;
+};
+
+export const isFeature = story => {
+ return story.storyType === storyTypes.FEATURE;
+};
+
+export const isUnstarted = story => {
+ return story.state === status.UNSTARTED;
+};
+
+export const isAccepted = story => {
+ return story.state === status.ACCEPTED;
+};
+
+export const getPoints = story => {
+ return isFeature(story) ? story.estimate : 0;
+};
-export const isUnestimatedFeature = (story) => {
- return story.estimate === null && story.storyType === 'feature'
+export const getCompletedPoints = story => {
+ return isFeature(story) && isAccepted(story) ? story.estimate : 0;
};
diff --git a/app/assets/javascripts/reducers/columns/backlog.js b/app/assets/javascripts/reducers/columns/backlog.js
index 0c3eedb76..3937b2e5a 100644
--- a/app/assets/javascripts/reducers/columns/backlog.js
+++ b/app/assets/javascripts/reducers/columns/backlog.js
@@ -1,37 +1,41 @@
-import actionTypes from 'actions/actionTypes';
-import * as Story from 'models/beta/story';
-import _ from 'underscore';
+import actionTypes from "actions/actionTypes";
+import { status } from "libs/beta/constants";
+import * as Story from "models/beta/story";
+import _ from "underscore";
const initialState = {
- stories: [],
+ stories: []
};
const filterByState = state => story => {
return story.state === state;
-}
+};
-const orderByState = (stories) => {
+const orderByState = stories => {
const ordered = [...stories];
ordered.sort(Story.comparePosition);
const acceptedStories = ordered
- .filter(filterByState('accepted'))
- .sort(Story.compareAcceptedAt);
+ .filter(filterByState(status.ACCEPTED))
+ .sort(Story.compareAcceptedAt);
const deliveredStories = ordered
- .filter(filterByState('delivered'))
- .sort(Story.compareDeliveredAt);
+ .filter(filterByState(status.DELIVERED))
+ .sort(Story.compareDeliveredAt);
const startedStories = ordered
- .filter(filterByState('started'))
- .sort(Story.compareStartedAt);
+ .filter(filterByState(status.STARTED))
+ .sort(Story.compareStartedAt);
- const rejectedStories = ordered.filter(filterByState('rejected'));
- const finishedStories = ordered.filter(filterByState('finished'));
- const unstartedStories = ordered.filter(filterByState('unstarted'));
+ const rejectedStories = ordered.filter(filterByState(status.REJECTED));
+ const finishedStories = ordered.filter(filterByState(status.FINISHED));
+ const unstartedStories = ordered.filter(filterByState(status.UNSTARTED));
- const partitionedFeatures = _.partition(unstartedStories, Story.isUnestimatedFeature);
+ const partitionedFeatures = _.partition(
+ unstartedStories,
+ Story.isUnestimatedFeature
+ );
const unestimatedUnstartedStories = partitionedFeatures[0];
const estimatedUnstartedStories = partitionedFeatures[1];
@@ -44,19 +48,16 @@ const orderByState = (stories) => {
...estimatedUnstartedStories,
...unestimatedUnstartedStories
];
-}
+};
const backlog = (state = initialState, action) => {
switch (action.type) {
case actionTypes.COLUMN_BACKLOG:
- const stories = [
- ...state.stories,
- action.data
- ];
+ const stories = [...state.stories, action.data];
return {
stories: orderByState(stories)
- }
+ };
default:
return state;
}
diff --git a/app/assets/stylesheets/new_board/_sprint.scss b/app/assets/stylesheets/new_board/_sprint.scss
new file mode 100644
index 000000000..44a39f326
--- /dev/null
+++ b/app/assets/stylesheets/new_board/_sprint.scss
@@ -0,0 +1,21 @@
+.Sprint {
+ &__header {
+ padding: 5px 10px;
+ font-size: 11px;
+ color: $lightgrey-12;
+ background-color: $darkgrey-7;
+ border-top: 1px solid $darkgrey-8;
+ border-bottom: 1px solid $darkgrey-9;
+ }
+
+ &__points {
+ float: right;
+ }
+
+ &__body {
+ display: block;
+ &--is-collapsed {
+ display: none;
+ }
+ }
+}
diff --git a/spec/javascripts/components/stories/sprint_spec.js b/spec/javascripts/components/stories/sprint_spec.js
new file mode 100644
index 000000000..4329a1bab
--- /dev/null
+++ b/spec/javascripts/components/stories/sprint_spec.js
@@ -0,0 +1,65 @@
+import jasmineEnzyme from "jasmine-enzyme";
+import React from "react";
+import { shallow } from "enzyme";
+import Sprint from "components/stories/Sprint";
+
+let props = {};
+let wrapper = {};
+
+const createProps = () => ({
+ number: 1,
+ startDate: "2018/09/03",
+ points: 3,
+ completedPoints: 0,
+ stories: [
+ {
+ id: 1,
+ position: "3",
+ state: "unstarted",
+ estimate: 1,
+ storyType: "feature"
+ },
+ {
+ id: 2,
+ position: "2",
+ state: "unstarted",
+ estimate: 1,
+ storyType: "feature"
+ }
+ ]
+});
+
+describe("", () => {
+ beforeEach(() => {
+ jasmineEnzyme();
+ props = createProps();
+ wrapper = shallow();
+ });
+
+ it('renders a with class ".Sprint"', () => {
+ expect(wrapper.find("div.Sprint")).toHaveLength(1);
+ });
+
+ it('renders a div with class ".Sprint__header"', () => {
+ expect(wrapper.find("div.Sprint__header")).toHaveLength(1);
+ });
+
+ it('renders a div with class ".Sprint__body"', () => {
+ expect(wrapper.find("div.Sprint__body")).toHaveLength(1);
+ });
+
+ it("renders a components", () => {
+ expect(wrapper.find("Stories")).toHaveLength(1);
+ });
+
+ describe("when no stories are passed as props", () => {
+ beforeEach(() => {
+ props = createProps();
+ props.stories = null;
+ wrapper = shallow();
+ });
+ it("does not render any component", () => {
+ expect(wrapper.find("Stories")).toHaveLength(0);
+ });
+ });
+});
diff --git a/spec/javascripts/components/stories/sprints_spec.js b/spec/javascripts/components/stories/sprints_spec.js
new file mode 100644
index 000000000..d89f26011
--- /dev/null
+++ b/spec/javascripts/components/stories/sprints_spec.js
@@ -0,0 +1,54 @@
+import jasmineEnzyme from "jasmine-enzyme";
+import React from "react";
+import { shallow } from "enzyme";
+import Sprints from "components/stories/Sprints";
+
+let props = {};
+let wrapper = {};
+
+const createProps = () => ({
+ stories: [
+ {
+ id: 1,
+ position: "3",
+ state: "unstarted",
+ estimate: 1,
+ storyType: "feature"
+ },
+ {
+ id: 2,
+ position: "2",
+ state: "unstarted",
+ estimate: 1,
+ storyType: "feature"
+ }
+ ],
+ project: {
+ startDate: "2018-09-03T16:00:00",
+ iterationLength: 1,
+ defaultVelocity: 2
+ }
+});
+
+describe("", () => {
+ beforeEach(() => {
+ jasmineEnzyme();
+ props = createProps();
+ wrapper = shallow();
+ });
+
+ it("renders one components", () => {
+ expect(wrapper.find("Sprint")).toHaveLength(1);
+ });
+
+ describe("when no stories are passed as props", () => {
+ beforeEach(() => {
+ props = createProps();
+ props.stories = [];
+ wrapper = shallow();
+ });
+ it("does not render any component", () => {
+ expect(wrapper.find("Sprint")).toHaveLength(0);
+ });
+ });
+});
diff --git a/spec/javascripts/models/beta/iteration_spec.js b/spec/javascripts/models/beta/iteration_spec.js
index ef6659c27..a53907759 100644
--- a/spec/javascripts/models/beta/iteration_spec.js
+++ b/spec/javascripts/models/beta/iteration_spec.js
@@ -1,53 +1,225 @@
-import moment from 'moment';
-import * as Iteration from 'models/beta/iteration';
+import * as Iteration from "models/beta/iteration";
-describe('iteration', function() {
- beforeEach(function() {
- this.clock = sinon.useFakeTimers(new Date('2018-05-01T17:00:00').getTime());
- });
+describe("iteration", function() {
+ describe("time related functions", function() {
+ beforeEach(function() {
+ this.clock = sinon.useFakeTimers(
+ new Date("2018-05-01T17:00:00").getTime()
+ );
+ });
- afterEach(function() {
- this.clock.restore();
- });
+ afterEach(function() {
+ this.clock.restore();
+ });
- describe('when 1 out of 1 week has passed', function() {
- it('should return 2', function() {
- const sprintNumber = Iteration.getCurrentIteration({
- iterationLength: 1,
- startDate: "2018-04-24T16:00:00"
- });
- expect(sprintNumber).toEqual(2);
+ describe("when 1 out of 1 week has passed", function() {
+ it("should return 2", function() {
+ const sprintNumber = Iteration.getCurrentIteration({
+ iterationLength: 1,
+ startDate: "2018-04-24T16:00:00"
+ });
+ expect(sprintNumber).toEqual(2);
+ });
});
- });
- describe("when 3 out of 3 weeks has passed", function() {
- it('should return 2', function() {
- const sprintNumber = Iteration.getCurrentIteration({
- iterationLength: 3,
- startDate: "2018-04-10T16:00:00"
- });
- expect(sprintNumber).toEqual(2);
+ describe("when 3 out of 3 weeks has passed", function() {
+ it("should return 2", function() {
+ const sprintNumber = Iteration.getCurrentIteration({
+ iterationLength: 3,
+ startDate: "2018-04-10T16:00:00"
+ });
+ expect(sprintNumber).toEqual(2);
+ });
});
- });
- describe("when 1 out of 2 weeks has passed", function() {
- it('should return 1', function() {
- const sprintNumber = Iteration.getCurrentIteration({
- iterationLength: 2,
- startDate: "2018-04-24T16:00:00"
- });
- expect(sprintNumber).toEqual(1);
+ describe("when 1 out of 2 weeks has passed", function() {
+ it("should return 1", function() {
+ const sprintNumber = Iteration.getCurrentIteration({
+ iterationLength: 2,
+ startDate: "2018-04-24T16:00:00"
+ });
+ expect(sprintNumber).toEqual(1);
+ });
});
- });
- describe("when a story was acceped a week ago", function() {
- it('should return 1', function() {
- const sprintNumber = Iteration.getIterationForStory(
- { state: 'accepted', acceptedAt: "2018-04-24T16:00:00" },
- { startDate: "2018-04-10T16:00:00", iterationLength: 2 }
- );
- expect(sprintNumber).toEqual(2);
+ describe("when a story was acceped a week ago", function() {
+ it("should return 2", function() {
+ const sprintNumber = Iteration.getIterationForStory(
+ { state: "accepted", acceptedAt: "2018-04-24T16:00:00" },
+ { startDate: "2018-04-10T16:00:00", iterationLength: 2 }
+ );
+ expect(sprintNumber).toEqual(2);
+ });
});
});
+ describe("when reducing stories to sprints", function() {
+ beforeEach(function() {
+ this.project = {
+ startDate: "2018-09-03T16:00:00",
+ iterationLength: 1,
+ defaultVelocity: 2
+ };
+ this.initialSprintNumber = Iteration.getCurrentIteration(this.project);
+ });
+
+ describe("with empty array of stories", function() {
+ it("should return an empty array of sprints", function() {
+ const stories = [];
+ sprints = Iteration.groupBySprints(
+ stories,
+ this.project,
+ this.initialSprintNumber
+ );
+ expect(sprints).toEqual([]);
+ });
+ });
+
+ describe("with 2 unstarted features with 1 point", function() {
+ it("should return an array with 1 item", function() {
+ const stories = [
+ {
+ id: 1,
+ position: "3.2",
+ state: "unstarted",
+ estimate: 1,
+ storyType: "feature"
+ },
+ {
+ id: 2,
+ position: "10",
+ state: "unstarted",
+ estimate: 1,
+ storyType: "feature"
+ }
+ ];
+
+ const sprints = Iteration.groupBySprints(
+ stories,
+ this.project,
+ this.initialSprintNumber
+ );
+
+ expect(sprints.length).toEqual(1);
+ });
+ });
+
+ describe("with 3 unstarted features with 2, 5 and 1 points and velocity 3", function() {
+ it("should return an array with 3 items", function() {
+ const stories = [
+ {
+ id: 1,
+ position: "1",
+ state: "unstarted",
+ estimate: 2,
+ storyType: "feature"
+ },
+ {
+ id: 2,
+ position: "2",
+ state: "unstarted",
+ estimate: 5,
+ storyType: "feature"
+ },
+ {
+ id: 3,
+ position: "3",
+ state: "unstarted",
+ estimate: 1,
+ storyType: "feature"
+ }
+ ];
+
+ this.project.defaultVelocity = 3;
+ const sprints = Iteration.groupBySprints(
+ stories,
+ this.project,
+ this.initialSprintNumber
+ );
+
+ expect(sprints.length).toEqual(3);
+ });
+ });
+
+ describe("with 1 unstarted features with 8 points and velocity 3", function() {
+ it("should return an array with 3 items", function() {
+ const stories = [
+ {
+ id: 1,
+ position: "1",
+ state: "unstarted",
+ estimate: 8,
+ storyType: "feature"
+ },
+ ];
+ this.project.defaultVelocity = 3;
+ const sprints = Iteration.groupBySprints(
+ stories,
+ this.project,
+ this.initialSprintNumber
+ );
+
+ expect(sprints.length).toEqual(3);
+ });
+ });
+
+ describe("with started, finished and delivered stories", function() {
+ it("should return an array with 2 items", function() {
+ const stories = [
+ {
+ id: 1,
+ position: "1.5",
+ state: "accepted",
+ acceptedAt: "2018-09-03T16:36:20.811Z",
+ estimate: 1,
+ storyType: "feature"
+ },
+ {
+ id: 3,
+ position: "3.2",
+ state: "unstarted",
+ estimate: 2,
+ storyType: "feature"
+ },
+ {
+ id: 4,
+ position: "7.5",
+ state: "started",
+ startedAt: "2018-09-03T16:36:20.811Z",
+ estimate: 1,
+ storyType: "feature"
+ },
+ {
+ id: 5,
+ position: "3.7",
+ state: "finished",
+ estimate: 1,
+ storyType: "feature"
+ },
+ {
+ id: 6,
+ position: "4.9",
+ state: "delivered",
+ deliveredAt: "2018-09-03T16:36:20.811Z",
+ estimate: 1,
+ storyType: "feature"
+ },
+ {
+ id: 7,
+ position: "10",
+ state: "unstarted",
+ estimate: 1,
+ storyType: "feature"
+ }
+ ];
+
+ const sprints = Iteration.groupBySprints(
+ stories,
+ this.project,
+ this.initialSprintNumber
+ );
+ expect(sprints.length).toEqual(3);
+ });
+ });
+ });
});