Backlog Aggregator: Roadmap Feedback (part one)

In our weekly meeting with the boss, I showed my roadmap-preview to have feedback and improve it (here the first part and here the second part of the creation of the roadmap-preview).
The roadmap-preview had to be used in two different places: in a dashboard as part of a summary of a project and in a Roadmap page where was needed to show more detailes.

My roadmap-preview had been considered good-looking, but lacking of information.
It wasn’t clear the content and the month of a release: in the dashboard had to be possible to read the key features of a release clicking on the flag, but in the Roadmap page was considered more useful to view them immediatly.

I decided not to create two different roadmap-preview directives; I prefered to have something like:

<roadmap-preview
  roadmap="roadmap"
  show-detail="true"></roadmap-preview>

I opened my roadmap-preview test file and I added

it('it should be possible specify that you want to see full-detail', function () {
});

Because I was using in my test a stub defined in my beforeEach, I decided to compile a local version of the roadmap

it('it should be possible specify that you want to see full-detail', function () {
	expect(el.hasClass('full-detail')).toBeFalsy();
	var roadmap = compile('<roadmap-preview roadmap="stub" show-detail="true"></roadmap-preview>')(scope);
	scope.$digest();
	expect(roadmap.hasClass('full-detail')).toBeTruthy();
});

From the point of view of the roadmap-preview directive I only needed to be sure to have an hook to change the visualization depending on a new attribute “show-detail”.
It was ok, but not enough, I had to be sure that the release-detail directive also would have riceived that parameter, so I completed the test with

it('it should be possible specify that you want to see full-detail', function () {
	expect(el.hasClass('full-detail')).toBeFalsy();
	var roadmap = compile('<roadmap-preview roadmap="stub" show-detail="true"></roadmap-preview>')(scope);
	scope.$digest();
	expect(roadmap.hasClass('full-detail')).toBeTruthy();

	var release = roadmap.find('release-detail');
	expect(release.attr('show-detail')).toContain('showDetail');
});

I run my test and obviously failed.
I started with the file js and I added only one line

var roadmapPreview = angular.module('backlogsReader.directives');
roadmapPreview
	.directive('roadmapPreview', function () {
		return {
			restrict: 'E',
			replace: true,
			scope: {
				roadmap: '=roadmap',
				showDetail : '=showDetail'
			},
			templateUrl: 'relativeToTheApplicationRootUrl/roadmapPreview.html'
		};
	});

Then in my template file

<div class="roadmap-preview"  ng-class="{'full-detail': showDetail}">
    <div class="where-are-you
pos-{{roadmap.whereAreYou.month}}-{{roadmap.whereAreYou.weekOfTheMonth}}"
tooltip="where are you!"></div>
    <release-detail
      release="release"
      ng-repeat="release in roadmap.releases"
      show-detail="showDetail"></release-detail>
    <div class="street"></div>

    <div class="roadmap-q q-1">Q1</div>
    <div class="roadmap-q q-2">Q2</div>
    <div class="roadmap-q q-3">Q3</div>
    <div class="roadmap-q q-4">Q4</div>
</div>

Test passed.
Few words about the test: to be sure that my condition in ng-class would have been correct I started verifying that the main compiled directive (the one in the beforeEach without show-detail) wouldn’t have the class full-detail and then that my local compiled directive would have it instead.

it('it should be possible specify that you want to see full-detail', function () {
	expect(el.hasClass('full-detail')).toBeFalsy();
	var roadmap = compile('<roadmap-preview roadmap="stub" show-detail="true"></roadmap-preview>')(scope);
	scope.$digest();
	expect(roadmap.hasClass('full-detail')).toBeTruthy();

	var release = roadmap.find('release-detail');
	expect(release.attr('show-detail')).toContain('showDetail');
});

After the test of the roadmap-preview I described the test for the release-detail directive.
I wrote a new Describe because I supposed to be in a different “when” situation.


describe('When the release has features', function () {
	it('it should be possible specify to show them', function () {
	});
	it('it should have a title for every feature', function () {
	});
});

I mocked two features with a title in my new beforeEach and I set my expectation for my it().

describe('When the release has features', function () {
	var scope;
	var el;

	beforeEach(function () {
		scope = rootScope.$new();
		scope.stub = {
			features: [{title:'myName'}, {title:'otherName'}]
		};

		el = compile('<release-detail release="stub"  show-detail="true"></release-detail>')(scope);
		scope.$digest();
	});

	it('it should be possible specify to show them', function () {
		expect(el.find('li.feature').length).toBe(2);

		var release = compile('<release-detail release="stub"></release-detail>')(scope);
		scope.$digest();
		expect(release.find('.list-of-features').hasClass('ng-hide')).toBeTruthy();

		var release2 = compile('<release-detail release="stub" show-detail="false"></release-detail>')(scope);
		scope.$digest();

		expect(release2.find('.list-of-features').hasClass('ng-hide')).toBeTruthy();
	});
	it('it should have a title for every feature', function () {
		var features = el.find('ul.features li');

		expect(features[0].innerText).toContain("myName");
		expect(features[1].innerText).toContain("otherName");
	});
});

I decided to render the features in any case to make easier to show them in the dashboard view.
So I used “ng-hide” to determinate the visibility.

My test failed.

In the directive file I added only one line, as before in the roadmap-preview

var releaseDetail = angular.module('backlogsReader.directives');
releaseDetail
	.directive('releaseDetail', function () {
		return {
			restrict: 'E',
			replace: true,
			scope: {
				release: '=release'
				showDetail : '=showDetail'
			},
			templateUrl: '/AppJs/src/directives/releaseDetail.html'
		};
	});

And then I modifide the template

<div class="release pos-{{release.estimated.month}}-{{release.estimated.weekOfTheMonth}}"  title="{{release.name}}">
	<div class="flag"></div>
	<div class="list-of-features" ng-show="release.features.length > 0 && showDetail">
		<ul class="features">
			<li class="feature" ng-repeat="feature in release.features">{{feature.title}}</li>
		</ul>
	</div>
</div>

The last thing I needed to complete the task was to show the month of each release, so I added a simple it() in the first describe

it('it should show the month', function () {
	var release = el;
	expect(release.find('.month').html()).toContain('February');
});

And I put in the template

<div class="release pos-{{release.estimated.month}}-{{release.estimated.weekOfTheMonth}}" ng-click="toggle(release)" title="{{release.name}}">
	<div class="flag"></div>
	<div class="month">{{release.estimated.monthName}}</div>
	<div class="list-of-features" ng-show="release.features.length > 0 && showDetail">
		<ul class="features">
			<li class="feature" ng-repeat="feature in release.features">{{feature.title}}</li>
		</ul>
	</div>
</div>

All test passed, what I had to do in my production code had been to put a show-detail=”true” in the roadmap page and after few stylesheet I had in my dashboard

roadmap-preview-dashboard

and in my roadmap page

roadmap-preview-page

I almost forgot a “detail”: to make possible to show/hide the key features in the dashboard on click.

In my test file I created a new describe and set a clear expectation in my it() description

describe('When the release doesn\'t show detail', function () {
		it('it should be possible to view them clicking on the flag', function () {
		});
	});

Defining that test was a little bit different from the previous because I had to perform “a click” inside my test

describe('When the release doesn\'t show detail', function () {

	var scope;
	var el;

	beforeEach(function () {
		scope = rootScope.$new();
		scope.stub = {
			features: [{ title: 'myName' }, { title: 'otherName' }]
		};

		el = compile('<release-detail release="stub"></release-detail>')(scope);
		scope.$digest();
	});

	it('it should be possible to view them clicking on the flag', function () {
		var flag = el.find('.flag');

		expect(el.find('.list-of-features').hasClass('ng-hide')).toBeTruthy();

		$(flag).trigger('click');

		expect(el.find('.list-of-features').hasClass('ng-hide')).toBeFalsy();

		$(flag).trigger('click');

		expect(el.find('.list-of-features').hasClass('ng-hide')).toBeTruthy();

	});

});

Because I used “ng-hide” to make features visible/invisible in my test I expected that the click on a flag would have removed that class.

I run the test: failed

In my directive file I added the link method and I changed the showDetail in the scope adding “?” char. It means that the parameter is optional.

scope: {
	release: '=release',
	showDetail: '=?showDetail'
},
link: function (scope, element, attrs) {
	scope.showHide = function () {
		scope.showDetail = !scope.showDetail;
	}
}

I added to the scope of the directive a method showHide, so that I could be able to use it in my template. What the method does, I think, it is self-explanatory.

I could have to modify the DOM removing or adding “ng-hide” class, but I thought this solution was more correct because in this way the directive was completely isolated from his template.
The directive shouldn’t know how the template would solve the problem.

In the tempate I modified the DOM giving the behaviour to the flag element

<div class="flag" ng-click="showHide()"></div>

I run my test: passed.
In my production code in the dashboard page when I clicked on a flag, I saw

roadmap-preview-dashboard-click

The complete new version of my files:

The roadmap-preview test:

describe('', function () {
	var compile;
	var rootScope;

	beforeEach(module('backlogsReader.directives'));
	beforeEach(inject(function ($compile, $rootScope, $templateCache) {
		compile = $compile;
		rootScope = $rootScope;

		var roadmapPreviewTemplate = null;

		$.ajax({
			type: 'GET',
			async: false,
			url: 'roadmapPreview.html',
			success: function (html) {
				roadmapPreviewTemplate = html;
			}
		});

		$templateCache.put("roadmapPreview.html", roadmapPreviewTemplate);
	}));

	describe('When the roadmap is compiled', function () {

		var scope;
		var el;

		beforeEach(function() {
			scope = rootScope.$new();
			scope.stub = {
				whereAreYou: {
					month: '2',
					weekOfTheMonth: '4'
				},
				releases: [{}]
			};

			el = compile('<roadmap-preview roadmap="stub"></roadmap-preview>')(scope);
			scope.$digest();
		});

		it('it should have the position of the viewer in the timeline', function () {
			var whereAreYou = el.find('.where-are-you');
			expect(whereAreYou.length).toBe(1);
			expect(whereAreYou.hasClass('pos-2-4')).toBeTruthy();
		});

		it('it should have a release-detail directive', function() {
			expect(el.find('release-detail').size()).toBe(1);
		});

		it('it should have the timeline divided in quarter per year', function () {
			expect(el.find('.roadmap-q.q-1').length).toBe(1);
			expect(el.find('.roadmap-q.q-2').length).toBe(1);
			expect(el.find('.roadmap-q.q-3').length).toBe(1);
			expect(el.find('.roadmap-q.q-4').length).toBe(1);
		});

		it('it should have a street', function () {
			expect(el.find('.street').length).toBe(1);
		});

		it('it should be possible specify that you want to see full-detail', function () {
			expect(el.hasClass('full-detail')).toBeFalsy();
			var roadmap = compile('<roadmap-preview roadmap="stub" show-detail="true"></roadmap-preview>')(scope);
			scope.$digest();
			expect(roadmap.hasClass('full-detail')).toBeTruthy();
			var release = roadmap.find('release-detail');
			expect(release.attr('show-detail')).toContain('showDetail');
		});
	});
});

the release-detail test:

describe('', function () {
	var compile;
	var rootScope;

	beforeEach(module('backlogsReader.directives'));
	beforeEach(inject(function ($compile, $rootScope, $templateCache) {
		compile = $compile;
		rootScope = $rootScope;

		var roadmapPreviewTemplate = null;

		$.ajax({
			type: 'GET',
			async: false,
			url: 'releaseDetail.html',
			success: function (html) {
				roadmapPreviewTemplate = html;
			}
		});

		$templateCache.put("releaseDetail.html", roadmapPreviewTemplate);
	}));

	describe('When the release is compiled', function () {

		var scope;
		var el;

		beforeEach(function() {
			scope = rootScope.$new();
			scope.stub = {
				estimated: {
					month: '2',
					weekOfTheMonth: '4',
					monthName: 'February'
				},
				name: "myname"
			};

			el = compile('<release-detail release="stub"></release-detail>')(scope);
			scope.$digest();
		});

		it('it should have a name', function () {
			var release = el;
			expect(release.attr('title')).toContain('myname');
		});

		it('it should show the month', function () {
			var release = el;
			expect(release.find('.month').html()).toContain('February');
		});

		it('it should have a flag', function () {
			var release = el;
			expect(release.find('.flag').length).toBe(1);
		});

		it('it should have the position in the timeline', function () {
			var release = el;
			expect(release.hasClass('pos-2-4')).toBeTruthy();
		});
	});

	describe('When the release has features', function () {

		var scope;
		var el;

		beforeEach(function () {
			scope = rootScope.$new();
			scope.stub = {
				features: [{title:'myName'}, {title:'otherName'}]
			};

			el = compile('<release-detail release="stub"  show-detail="true"></release-detail>')(scope);
			scope.$digest();
		});

		it('it should be possible specify to show them', function () {
			expect(el.find('li.feature').length).toBe(2);

			var release = compile('<release-detail release="stub"></release-detail>')(scope);
			scope.$digest();

			expect(release.find('.list-of-features').hasClass('ng-hide')).toBeTruthy();

			var release2 = compile('<release-detail release="stub" show-detail="false"></release-detail>')(scope);
			scope.$digest();

			expect(release2.find('.list-of-features').hasClass('ng-hide')).toBeTruthy();

		});
		it('it should have a title for every feature', function () {
			var features = el.find('ul.features li');

			expect(features[0].innerText).toContain("myName");
			expect(features[1].innerText).toContain("otherName");
		});
	});

	describe('When the release doesn\'t show detail', function () {

		var scope;
		var el;

		beforeEach(function () {
			scope = rootScope.$new();
			scope.stub = {
				features: [{ title: 'myName' }, { title: 'otherName' }]
			};

			el = compile('<release-detail release="stub"></release-detail>')(scope);
			scope.$digest();
		});

		it('it should be possible to view them clicking on the flag', function () {
			var flag = el.find('.flag');

			expect(el.find('.list-of-features').hasClass('ng-hide')).toBeTruthy();

			$(flag).trigger('click');

			expect(el.find('.list-of-features').hasClass('ng-hide')).toBeFalsy();

			$(flag).trigger('click');

			expect(el.find('.list-of-features').hasClass('ng-hide')).toBeTruthy();
		});
	});
});

the roadmap-preview directive:

var roadmapPreview = angular.module('backlogsReader.directives');

roadmapPreview
	.directive('roadmapPreview', function () {
		return {
			restrict: 'E',
			replace: true,
			scope: {
				roadmap: '=roadmap',
				showDetail : '=showDetail'
			},
			templateUrl: 'roadmapPreview.html'
		};
	});

the release-detail directive:

var releaseDetail = angular.module('backlogsReader.directives');

releaseDetail
	.directive('releaseDetail', function () {
		return {
			restrict: 'E',
			replace: true,
			scope: {
				release: '=release',
				showDetail: '=?showDetail'
			},
			link: function (scope, element, attrs) {
				scope.showHide = function () {
					scope.showDetail = !scope.showDetail;
				}
			},
			templateUrl: 'releaseDetail.html'
		};
	});

the roadmap-preview template:

<div class="roadmap-preview" ng-class="{'full-detail': showDetail}">
	<div class="where-are-you pos-{{roadmap.whereAreYou.month}}-{{roadmap.whereAreYou.weekOfTheMonth}}" tooltip="where are you!"></div>
	<div class="street"></div>

	<div class="roadmap-q q-1">Q1</div>
	<div class="roadmap-q q-2">Q2</div>
	<div class="roadmap-q q-3">Q3</div>
	<div class="roadmap-q q-4">Q4</div>

	<release-detail release="release" show-detail="showDetail"  ng-repeat="release in roadmap.releases"></release-detail>
</div>

the release-detail template:

<div class="release pos-{{release.estimated.month}}-{{release.estimated.weekOfTheMonth}}" title="{{release.name}}">
	<div class="flag" ng-click="showHide()"></div>
	<div class="month">{{release.estimated.monthName}}</div>
	<div class="list-of-features" ng-show="release.features.length > 0 && showDetail">
		<ul class="features">
			<li class="feature" ng-repeat="feature in release.features">{{feature.title}}</li>
		</ul>
	</div>
</div>

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s