Commit 68deaf35dc5d0c0dbdfc46c87b9326d4f03f2209
1 parent
4902da4d
Exists in
master
and in
26 other branches
Initial implementation
Showing
16 changed files
with
487 additions
and
103 deletions
Show diff stats
src/app/article/article-default-view-component.spec.ts
| 1 | 1 | import {Input, provide, Component} from 'ng-forward'; |
| 2 | 2 | import {ArticleViewComponent, ArticleDefaultViewComponent} from './article-default-view.component'; |
| 3 | +import {ComponentTestHelper, createClass} from './../../spec/component-test-helper'; | |
| 4 | +import {ModelEvent, ArticleEventType} from "./../shared/models/events"; | |
| 3 | 5 | |
| 4 | 6 | import * as helpers from "../../spec/helpers"; |
| 5 | 7 | |
| ... | ... | @@ -9,113 +11,83 @@ const htmlTemplate: string = '<noosfero-article [article]="ctrl.article" [profil |
| 9 | 11 | |
| 10 | 12 | describe("Components", () => { |
| 11 | 13 | |
| 12 | - describe("ArticleView Component", () => { | |
| 13 | - | |
| 14 | - // the karma preprocessor html2js transform the templates html into js files which put | |
| 15 | - // the templates to the templateCache into the module templates | |
| 16 | - // we need to load the module templates here as the template for the | |
| 17 | - // component Noosfero ArtileView will be load on our tests | |
| 18 | - beforeEach(angular.mock.module("templates")); | |
| 19 | - | |
| 20 | - it("renders the default component when no specific component is found", (done: Function) => { | |
| 21 | - // Creating a container component (ArticleContainerComponent) to include | |
| 22 | - // the component under test (ArticleView) | |
| 23 | - @Component({ | |
| 24 | - selector: 'test-container-component', | |
| 25 | - template: htmlTemplate, | |
| 26 | - directives: [ArticleViewComponent], | |
| 27 | - providers: [ | |
| 28 | - helpers.createProviderToValue('CommentService', helpers.mocks.commentService), | |
| 29 | - helpers.provideFilters("translateFilter"), | |
| 30 | - helpers.createProviderToValue('NotificationService', helpers.mocks.notificationService), | |
| 31 | - helpers.createProviderToValue('SessionService', helpers.mocks.sessionWithCurrentUser({})) | |
| 32 | - ] | |
| 33 | - }) | |
| 34 | - class ArticleContainerComponent { | |
| 35 | - article = { type: 'anyArticleType' }; | |
| 36 | - profile = { name: 'profile-name' }; | |
| 14 | + // the karma preprocessor html2js transform the templates html into js files which put | |
| 15 | + // the templates to the templateCache into the module templates | |
| 16 | + // we need to load the module templates here as the template for the | |
| 17 | + // component Noosfero ArtileView will be load on our tests | |
| 18 | + beforeEach(angular.mock.module("templates")); | |
| 19 | + | |
| 20 | + describe("Article Default View Component", () => { | |
| 21 | + let helper: ComponentTestHelper<ArticleDefaultViewComponent>; | |
| 22 | + const defaultViewTemplate: string = '<noosfero-default-article [article]="ctrl.article" [profile]="ctrl.profile"></noosfero-default-article>'; | |
| 23 | + let articleService: any = helpers.mocks.articleService; | |
| 24 | + let article = <noosfero.Article>{ | |
| 25 | + id: 1, | |
| 26 | + profile: { | |
| 27 | + identifier: "1" | |
| 37 | 28 | } |
| 38 | - | |
| 39 | - helpers.createComponentFromClass(ArticleContainerComponent).then((fixture) => { | |
| 40 | - // and here we can inspect and run the test assertions | |
| 41 | - | |
| 42 | - // gets the children component of ArticleContainerComponent | |
| 43 | - let articleView: ArticleViewComponent = fixture.debugElement.componentViewChildren[0].componentInstance; | |
| 44 | - | |
| 45 | - // and checks if the article View rendered was the Default Article View | |
| 46 | - expect(articleView.constructor.prototype).toEqual(ArticleDefaultViewComponent.prototype); | |
| 47 | - | |
| 48 | - // done needs to be called (it isn't really needed, as we can read in | |
| 49 | - // here (https://github.com/ngUpgraders/ng-forward/blob/master/API.md#createasync) | |
| 50 | - // because createAsync in ng-forward is not really async, but as the intention | |
| 51 | - // here is write tests in angular 2 ways, this is recommended | |
| 52 | - done(); | |
| 29 | + }; | |
| 30 | + let state = <ng.ui.IStateService>jasmine.createSpyObj("state", ["go", "transitionTo"]); | |
| 31 | + let providers = [ | |
| 32 | + provide('$state', { useValue: state }), | |
| 33 | + provide('ArticleService', { useValue: articleService }) | |
| 34 | + ].concat(helpers.provideFilters("translateFilter")); | |
| 35 | + | |
| 36 | + /** | |
| 37 | + * The beforeEach procedure will initialize the helper and parse | |
| 38 | + * the component according to the given providers. Unfortunetly, in | |
| 39 | + * this mode, the providers and properties given to the construtor | |
| 40 | + * can't be overriden. | |
| 41 | + */ | |
| 42 | + beforeEach((done) => { | |
| 43 | + // Create the component bed for the test. Optionally, this could be done | |
| 44 | + // in each test if one needs customization of these parameters per test | |
| 45 | + let cls = createClass({ | |
| 46 | + template: defaultViewTemplate, | |
| 47 | + directives: [ArticleDefaultViewComponent], | |
| 48 | + providers: providers, | |
| 49 | + properties: { | |
| 50 | + article: article | |
| 51 | + } | |
| 53 | 52 | }); |
| 54 | - | |
| 53 | + helper = new ComponentTestHelper<ArticleDefaultViewComponent>(cls, done); | |
| 55 | 54 | }); |
| 56 | 55 | |
| 57 | - it("receives the article and profile as inputs", (done: Function) => { | |
| 58 | - | |
| 59 | - // Creating a container component (ArticleContainerComponent) to include | |
| 60 | - // the component under test (ArticleView) | |
| 61 | - @Component({ | |
| 62 | - selector: 'test-container-component', | |
| 63 | - template: htmlTemplate, | |
| 64 | - directives: [ArticleViewComponent], | |
| 65 | - providers: [ | |
| 66 | - helpers.createProviderToValue('CommentService', helpers.mocks.commentService), | |
| 67 | - helpers.provideFilters("translateFilter"), | |
| 68 | - helpers.createProviderToValue('NotificationService', helpers.mocks.notificationService), | |
| 69 | - helpers.createProviderToValue('SessionService', helpers.mocks.sessionWithCurrentUser({})) | |
| 70 | - ] | |
| 71 | - }) | |
| 72 | - class ArticleContainerComponent { | |
| 73 | - article = { type: 'anyArticleType' }; | |
| 74 | - profile = { name: 'profile-name' }; | |
| 75 | - } | |
| 76 | - | |
| 77 | - // uses the TestComponentBuilder instance to initialize the component | |
| 78 | - helpers.createComponentFromClass(ArticleContainerComponent).then((fixture) => { | |
| 79 | - // and here we can inspect and run the test assertions | |
| 80 | - let articleView: ArticleViewComponent = fixture.debugElement.componentViewChildren[0].componentInstance; | |
| 56 | + function getArticle() { | |
| 57 | + return this.article; | |
| 58 | + } | |
| 81 | 59 | |
| 82 | - // assure the article object inside the ArticleView matches | |
| 83 | - // the provided through the parent component | |
| 84 | - expect(articleView.article.type).toEqual("anyArticleType"); | |
| 85 | - expect(articleView.profile.name).toEqual("profile-name"); | |
| 86 | - | |
| 87 | - // done needs to be called (it isn't really needed, as we can read in | |
| 88 | - // here (https://github.com/ngUpgraders/ng-forward/blob/master/API.md#createasync) | |
| 89 | - // because createAsync in ng-forward is not really async, but as the intention | |
| 90 | - // here is write tests in angular 2 ways, this is recommended | |
| 91 | - done(); | |
| 92 | - }); | |
| 60 | + it("it should delete article when delete is activated", () => { | |
| 61 | + expect(helper.component.article).toEqual(article); | |
| 62 | + // Spy the state service | |
| 63 | + doDeleteArticle(); | |
| 64 | + expect(state.transitionTo).toHaveBeenCalled(); | |
| 93 | 65 | }); |
| 94 | 66 | |
| 95 | - | |
| 96 | - it("renders a article view which matches to the article type", done => { | |
| 97 | - // NoosferoTinyMceArticle component created to check if it will be used | |
| 98 | - // when a article with type 'TinyMceArticle' is provided to the noosfero-article (ArticleView) | |
| 99 | - // *** Important *** - the selector is what ng-forward uses to define the name of the directive provider | |
| 100 | - @Component({ selector: 'noosfero-tiny-mce-article', template: "<h1>TinyMceArticle</h1>" }) | |
| 101 | - class TinyMceArticleView { | |
| 102 | - @Input() article: any; | |
| 103 | - @Input() profile: any; | |
| 104 | - } | |
| 105 | - | |
| 106 | - // Creating a container component (ArticleContainerComponent) to include our NoosferoTinyMceArticle | |
| 107 | - @Component({ selector: 'test-container-component', template: htmlTemplate, directives: [ArticleViewComponent, TinyMceArticleView] }) | |
| 108 | - class CustomArticleType { | |
| 109 | - article = { type: 'TinyMceArticle' }; | |
| 110 | - profile = { name: 'profile-name' }; | |
| 111 | - } | |
| 112 | - helpers.createComponentFromClass(CustomArticleType).then(fixture => { | |
| 113 | - let myComponent: CustomArticleType = fixture.componentInstance; | |
| 114 | - expect(myComponent.article.type).toEqual("TinyMceArticle"); | |
| 115 | - expect(fixture.debugElement.componentViewChildren[0].text()).toEqual("TinyMceArticle"); | |
| 116 | - done(); | |
| 67 | + /** | |
| 68 | + * Execute the delete method on the target component | |
| 69 | + */ | |
| 70 | + function doDeleteArticle() { | |
| 71 | + // Create a mock for the ArticleService removeArticle method | |
| 72 | + spyOn(helper.component.articleService, 'removeArticle').and.callFake(function(param: noosfero.Article) { | |
| 73 | + return { | |
| 74 | + catch: () => {} | |
| 75 | + }; | |
| 117 | 76 | }); |
| 118 | - }); | |
| 119 | - | |
| 77 | + helper.component.delete(); | |
| 78 | + expect(articleService.removeArticle).toHaveBeenCalled(); | |
| 79 | + // After the component delete method execution, fire the | |
| 80 | + // ArticleEvent.removed event | |
| 81 | + simulateRemovedEvent(); | |
| 82 | + } | |
| 83 | + | |
| 84 | + /** | |
| 85 | + * Simulate the ArticleService ArticleEvent.removed event | |
| 86 | + */ | |
| 87 | + function simulateRemovedEvent() { | |
| 88 | + let event: ModelEvent = ModelEvent.event(ArticleEventType.removed); | |
| 89 | + helper.component.articleService.notifyArticleRemovedListeners(article); | |
| 90 | + } | |
| 120 | 91 | }); |
| 92 | + | |
| 121 | 93 | }); | ... | ... |
src/app/article/article-default-view.component.ts
| ... | ... | @@ -4,6 +4,8 @@ import {CommentsComponent} from "./comment/comments.component"; |
| 4 | 4 | import {MacroDirective} from "./macro/macro.directive"; |
| 5 | 5 | import {ArticleToolbarHotspotComponent} from "../hotspot/article-toolbar-hotspot.component"; |
| 6 | 6 | import {ArticleContentHotspotComponent} from "../hotspot/article-content-hotspot.component"; |
| 7 | +import {ArticleService} from "./../../lib/ng-noosfero-api/http/article.service"; | |
| 8 | +import {ModelEvent, ArticleEventType} from "./../shared/models/events"; | |
| 7 | 9 | |
| 8 | 10 | /** |
| 9 | 11 | * @ngdoc controller |
| ... | ... | @@ -16,11 +18,30 @@ import {ArticleContentHotspotComponent} from "../hotspot/article-content-hotspot |
| 16 | 18 | selector: 'noosfero-default-article', |
| 17 | 19 | templateUrl: 'app/article/article.html' |
| 18 | 20 | }) |
| 21 | +@Inject("$state", ArticleService) | |
| 19 | 22 | export class ArticleDefaultViewComponent { |
| 20 | 23 | |
| 21 | 24 | @Input() article: noosfero.Article; |
| 22 | 25 | @Input() profile: noosfero.Profile; |
| 23 | 26 | |
| 27 | + constructor(private $state: ng.ui.IStateService, public articleService: ArticleService) { | |
| 28 | + // Subscribe to the Article Removed Event | |
| 29 | + let event = ModelEvent.event(ArticleEventType.removed); | |
| 30 | + this.articleService.subscribe(event, (article: noosfero.Article) => { | |
| 31 | + if (this.article.parent) { | |
| 32 | + this.$state.transitionTo('main.profile.page', { page: this.article.parent.path, profile: this.article.profile.identifier }); | |
| 33 | + } else { | |
| 34 | + this.$state.transitionTo('main.profile.info', { profile: this.article.profile.identifier }); | |
| 35 | + } | |
| 36 | + }); | |
| 37 | + } | |
| 38 | + | |
| 39 | + delete() { | |
| 40 | + this.articleService.removeArticle(this.article).catch((cause: any) => { | |
| 41 | + throw new Error(`Problem removing the article: ${cause}`); | |
| 42 | + }); | |
| 43 | + } | |
| 44 | + | |
| 24 | 45 | } |
| 25 | 46 | |
| 26 | 47 | /** |
| ... | ... | @@ -60,4 +81,5 @@ export class ArticleViewComponent { |
| 60 | 81 | private $injector: ng.auto.IInjectorService, |
| 61 | 82 | private $compile: ng.ICompileService) { |
| 62 | 83 | } |
| 84 | + | |
| 63 | 85 | } | ... | ... |
| ... | ... | @@ -0,0 +1,124 @@ |
| 1 | +import {Input, provide, Component} from 'ng-forward'; | |
| 2 | +import {ArticleViewComponent, ArticleDefaultViewComponent} from './article-default-view.component'; | |
| 3 | + | |
| 4 | +import * as helpers from "../../spec/helpers"; | |
| 5 | + | |
| 6 | +// this htmlTemplate will be re-used between the container components in this spec file | |
| 7 | +const htmlTemplate: string = '<noosfero-article [article]="ctrl.article" [profile]="ctrl.profile"></noosfero-article>'; | |
| 8 | + | |
| 9 | + | |
| 10 | +describe("Components", () => { | |
| 11 | + | |
| 12 | + // the karma preprocessor html2js transform the templates html into js files which put | |
| 13 | + // the templates to the templateCache into the module templates | |
| 14 | + // we need to load the module templates here as the template for the | |
| 15 | + // component Noosfero ArtileView will be load on our tests | |
| 16 | + beforeEach(angular.mock.module("templates")); | |
| 17 | + | |
| 18 | + describe("ArticleView Component", () => { | |
| 19 | + let state = <ng.ui.IStateService>jasmine.createSpyObj("state", ["go", "transitionTo"]); | |
| 20 | + it("renders the default component when no specific component is found", (done: Function) => { | |
| 21 | + // Creating a container component (ArticleContainerComponent) to include | |
| 22 | + // the component under test (ArticleView) | |
| 23 | + @Component({ | |
| 24 | + selector: 'test-container-component', | |
| 25 | + template: htmlTemplate, | |
| 26 | + directives: [ArticleViewComponent], | |
| 27 | + providers: [ | |
| 28 | + helpers.createProviderToValue('CommentService', helpers.mocks.commentService), | |
| 29 | + helpers.provideFilters("translateFilter"), | |
| 30 | + helpers.createProviderToValue('NotificationService', helpers.mocks.notificationService), | |
| 31 | + helpers.createProviderToValue('SessionService', helpers.mocks.sessionWithCurrentUser({})), | |
| 32 | + helpers.createProviderToValue('ArticleService', helpers.mocks.articleService), | |
| 33 | + helpers.createProviderToValue('$state', state) | |
| 34 | + ] | |
| 35 | + }) | |
| 36 | + class ArticleContainerComponent { | |
| 37 | + article = { type: 'anyArticleType' }; | |
| 38 | + profile = { name: 'profile-name' }; | |
| 39 | + } | |
| 40 | + | |
| 41 | + helpers.createComponentFromClass(ArticleContainerComponent).then((fixture) => { | |
| 42 | + // and here we can inspect and run the test assertions | |
| 43 | + | |
| 44 | + // gets the children component of ArticleContainerComponent | |
| 45 | + let articleView: ArticleViewComponent = fixture.debugElement.componentViewChildren[0].componentInstance; | |
| 46 | + | |
| 47 | + // and checks if the article View rendered was the Default Article View | |
| 48 | + expect(articleView.constructor.prototype).toEqual(ArticleDefaultViewComponent.prototype); | |
| 49 | + | |
| 50 | + // done needs to be called (it isn't really needed, as we can read in | |
| 51 | + // here (https://github.com/ngUpgraders/ng-forward/blob/master/API.md#createasync) | |
| 52 | + // because createAsync in ng-forward is not really async, but as the intention | |
| 53 | + // here is write tests in angular 2 ways, this is recommended | |
| 54 | + done(); | |
| 55 | + }); | |
| 56 | + }); | |
| 57 | + | |
| 58 | + it("receives the article and profile as inputs", (done: Function) => { | |
| 59 | + | |
| 60 | + // Creating a container component (ArticleContainerComponent) to include | |
| 61 | + // the component under test (ArticleView) | |
| 62 | + @Component({ | |
| 63 | + selector: 'test-container-component', | |
| 64 | + template: htmlTemplate, | |
| 65 | + directives: [ArticleViewComponent], | |
| 66 | + providers: [ | |
| 67 | + helpers.createProviderToValue('CommentService', helpers.mocks.commentService), | |
| 68 | + helpers.provideFilters("translateFilter"), | |
| 69 | + helpers.createProviderToValue('NotificationService', helpers.mocks.notificationService), | |
| 70 | + helpers.createProviderToValue('SessionService', helpers.mocks.sessionWithCurrentUser({})), | |
| 71 | + helpers.createProviderToValue('ArticleService', helpers.mocks.articleService), | |
| 72 | + helpers.createProviderToValue('$state', state) | |
| 73 | + ] | |
| 74 | + }) | |
| 75 | + class ArticleContainerComponent { | |
| 76 | + article = { type: 'anyArticleType' }; | |
| 77 | + profile = { name: 'profile-name' }; | |
| 78 | + } | |
| 79 | + | |
| 80 | + // uses the TestComponentBuilder instance to initialize the component | |
| 81 | + helpers.createComponentFromClass(ArticleContainerComponent).then((fixture) => { | |
| 82 | + // and here we can inspect and run the test assertions | |
| 83 | + let articleView: ArticleViewComponent = fixture.debugElement.componentViewChildren[0].componentInstance; | |
| 84 | + | |
| 85 | + // assure the article object inside the ArticleView matches | |
| 86 | + // the provided through the parent component | |
| 87 | + expect(articleView.article.type).toEqual("anyArticleType"); | |
| 88 | + expect(articleView.profile.name).toEqual("profile-name"); | |
| 89 | + | |
| 90 | + // done needs to be called (it isn't really needed, as we can read in | |
| 91 | + // here (https://github.com/ngUpgraders/ng-forward/blob/master/API.md#createasync) | |
| 92 | + // because createAsync in ng-forward is not really async, but as the intention | |
| 93 | + // here is write tests in angular 2 ways, this is recommended | |
| 94 | + done(); | |
| 95 | + }); | |
| 96 | + }); | |
| 97 | + | |
| 98 | + | |
| 99 | + it("renders a article view which matches to the article type", done => { | |
| 100 | + // NoosferoTinyMceArticle component created to check if it will be used | |
| 101 | + // when a article with type 'TinyMceArticle' is provided to the noosfero-article (ArticleView) | |
| 102 | + // *** Important *** - the selector is what ng-forward uses to define the name of the directive provider | |
| 103 | + @Component({ selector: 'noosfero-tiny-mce-article', template: "<h1>TinyMceArticle</h1>" }) | |
| 104 | + class TinyMceArticleView { | |
| 105 | + @Input() article: any; | |
| 106 | + @Input() profile: any; | |
| 107 | + } | |
| 108 | + | |
| 109 | + // Creating a container component (ArticleContainerComponent) to include our NoosferoTinyMceArticle | |
| 110 | + @Component({ selector: 'test-container-component', template: htmlTemplate, directives: [ArticleViewComponent, TinyMceArticleView] }) | |
| 111 | + class CustomArticleType { | |
| 112 | + article = { type: 'TinyMceArticle' }; | |
| 113 | + profile = { name: 'profile-name' }; | |
| 114 | + } | |
| 115 | + helpers.createComponentFromClass(CustomArticleType).then(fixture => { | |
| 116 | + let myComponent: CustomArticleType = fixture.componentInstance; | |
| 117 | + expect(myComponent.article.type).toEqual("TinyMceArticle"); | |
| 118 | + expect(fixture.debugElement.componentViewChildren[0].text()).toEqual("TinyMceArticle"); | |
| 119 | + done(); | |
| 120 | + }); | |
| 121 | + }); | |
| 122 | + | |
| 123 | + }); | |
| 124 | +}); | ... | ... |
src/app/article/article.html
| ... | ... | @@ -7,6 +7,9 @@ |
| 7 | 7 | <a href="#" class="btn btn-default btn-xs" ui-sref="main.cmsEdit({profile: ctrl.profile.identifier, id: ctrl.article.id})"> |
| 8 | 8 | <i class="fa fa-pencil-square-o fa-fw fa-lg"></i> {{"article.actions.edit" | translate}} |
| 9 | 9 | </a> |
| 10 | + <a href="#" class="btn btn-default btn-xs" ng-click="ctrl.delete()"> | |
| 11 | + <i class="fa fa-trash-o fa-fw fa-lg" ng-click="ctrl.delete()"></i> {{"article.actions.delete" | translate}} | |
| 12 | + </a> | |
| 10 | 13 | <noosfero-hotspot-article-toolbar [article]="ctrl.article"></noosfero-hotspot-article-toolbar> |
| 11 | 14 | <div class="page-info pull-right small text-muted"> |
| 12 | 15 | <span class="time"> | ... | ... |
| ... | ... | @@ -0,0 +1,71 @@ |
| 1 | +import { EventEmitter } from "ng-forward"; | |
| 2 | +import {ModelEvent, ArticleEventType} from "./events"; | |
| 3 | +import {HashMap} from "./../utils/hashmap"; | |
| 4 | +import {ArrayUtils} from "./../utils/arrays"; | |
| 5 | + | |
| 6 | +describe("Events", () => { | |
| 7 | + | |
| 8 | + describe("Event Type Tests", () => { | |
| 9 | + | |
| 10 | + it("verify event type is correctly created", (done) => { | |
| 11 | + let eventType1 = ArticleEventType.removed; | |
| 12 | + expect(eventType1.type).toBe("removed"); | |
| 13 | + expect(ArrayUtils.arraysEqual(eventType1.types, ["added", "removed"])).toBeTruthy(); | |
| 14 | + done(); | |
| 15 | + }); | |
| 16 | + | |
| 17 | + it("different event types of same type should be equal", (done) => { | |
| 18 | + let eventType1 = ArticleEventType.removed; | |
| 19 | + let eventType2 = ArticleEventType.removed; | |
| 20 | + expect(eventType1).toBe(eventType2); | |
| 21 | + expect(eventType1 === eventType2).toBeTruthy(); | |
| 22 | + done(); | |
| 23 | + }); | |
| 24 | + | |
| 25 | + it("different events types of different types should not be equal", (done) => { | |
| 26 | + let eventType1 = ArticleEventType.removed; | |
| 27 | + let eventType2 = ArticleEventType.added; | |
| 28 | + expect(eventType1).not.toBe(eventType2); | |
| 29 | + expect(eventType1 === eventType2).not.toBeTruthy(); | |
| 30 | + done(); | |
| 31 | + }); | |
| 32 | + | |
| 33 | + it("different events of same type should be equal", (done) => { | |
| 34 | + let event1 = ModelEvent.event(ArticleEventType.added); | |
| 35 | + let event2 = ModelEvent.event(ArticleEventType.added); | |
| 36 | + expect(event1.equals(event2)).toBeTruthy(); | |
| 37 | + done(); | |
| 38 | + }); | |
| 39 | + | |
| 40 | + }); | |
| 41 | + | |
| 42 | + describe("Event HashMap Tests", () => { | |
| 43 | + let events: HashMap<ModelEvent, EventEmitter<noosfero.Article>>; | |
| 44 | + beforeEach((done) => { | |
| 45 | + events = new HashMap<ModelEvent, EventEmitter<noosfero.Article>>(); | |
| 46 | + done(); | |
| 47 | + }); | |
| 48 | + | |
| 49 | + it("verify event HashMap contains the correct event", () => { | |
| 50 | + let expected = new EventEmitter<noosfero.Article>(); | |
| 51 | + events.put(ModelEvent.event(ArticleEventType.added), expected); | |
| 52 | + let actual = events.get(ModelEvent.event(ArticleEventType.added)); | |
| 53 | + expect(expected === actual).toBeTruthy(); | |
| 54 | + }); | |
| 55 | + | |
| 56 | + it("verify event HashMap does not contain the wrong event", () => { | |
| 57 | + events.put(ModelEvent.event(ArticleEventType.added), new EventEmitter<noosfero.Article>()); | |
| 58 | + let actual = events.get(ModelEvent.event(ArticleEventType.removed)); | |
| 59 | + expect(actual).not.toBeTruthy(); | |
| 60 | + }); | |
| 61 | + | |
| 62 | + it("verify HashMap has been cleared", () => { | |
| 63 | + events.put(ModelEvent.event(ArticleEventType.added), new EventEmitter<noosfero.Article>()); | |
| 64 | + events.clear(); | |
| 65 | + let actual = events.get(ModelEvent.event(ArticleEventType.added)); | |
| 66 | + expect(actual).not.toBeTruthy(); | |
| 67 | + }); | |
| 68 | + | |
| 69 | + }); | |
| 70 | + | |
| 71 | +}); | ... | ... |
| ... | ... | @@ -0,0 +1,62 @@ |
| 1 | +import {ArrayUtils} from "./../utils/arrays"; | |
| 2 | + | |
| 3 | +export abstract class EventType { | |
| 4 | + /** | |
| 5 | + * All possible types for the event | |
| 6 | + */ | |
| 7 | + types = new Array<string>(); | |
| 8 | + | |
| 9 | + /** | |
| 10 | + * The event type | |
| 11 | + */ | |
| 12 | + type: string; | |
| 13 | + | |
| 14 | + constructor(types: Array<string>, type: string) { | |
| 15 | + this.types = types; | |
| 16 | + this.type = type; | |
| 17 | + } | |
| 18 | + | |
| 19 | + equals(other: EventType) { | |
| 20 | + return ArrayUtils.arraysEqual(this.types, other.types) && this.type === other.type; | |
| 21 | + } | |
| 22 | + | |
| 23 | +} | |
| 24 | + | |
| 25 | +export class ArticleEventType extends EventType { | |
| 26 | + static types = ["added", "removed"]; | |
| 27 | + static removed: ArticleEventType = new ArticleEventType("removed"); | |
| 28 | + static added: ArticleEventType = new ArticleEventType("added"); | |
| 29 | + | |
| 30 | + constructor(type: string) { | |
| 31 | + super(ArticleEventType.types, type); | |
| 32 | + } | |
| 33 | +} | |
| 34 | + | |
| 35 | +/** | |
| 36 | + * A model event have a type (ModelEventType), and optionally with a model element. | |
| 37 | + * Therefore, it is possible to have events related to specific model elements. | |
| 38 | + * If the model element element is not provided, then the event is a generic event | |
| 39 | + * of the given type. | |
| 40 | + */ | |
| 41 | +export class ModelEvent { | |
| 42 | + private type: EventType; | |
| 43 | + private id: number; | |
| 44 | + | |
| 45 | + static event(type: EventType, model?: noosfero.RestModel): ModelEvent { | |
| 46 | + if (model) { | |
| 47 | + return new ModelEvent(type, model.id); | |
| 48 | + } else { | |
| 49 | + return new ModelEvent(type); | |
| 50 | + } | |
| 51 | + } | |
| 52 | + | |
| 53 | + constructor(type: EventType, id?: number) { | |
| 54 | + this.type = type; | |
| 55 | + this.id = id; | |
| 56 | + } | |
| 57 | + | |
| 58 | + equals(other: ModelEvent) { | |
| 59 | + return other.type.equals(this.type) && other.id === this.id; | |
| 60 | + } | |
| 61 | + | |
| 62 | +} | ... | ... |
src/app/shared/models/index.ts
src/app/shared/models/interfaces.ts
| ... | ... | @@ -0,0 +1,19 @@ |
| 1 | +export class ArrayUtils { | |
| 2 | + | |
| 3 | + /** | |
| 4 | + * Returns if two arrays are equal | |
| 5 | + */ | |
| 6 | + static arraysEqual(a: any, b: any): boolean { | |
| 7 | + if (a === b) return true; | |
| 8 | + if (a == null || b == null) return false; | |
| 9 | + if (a.length !== b.length) return false; | |
| 10 | + | |
| 11 | + // If you don't care about the order of the elements inside | |
| 12 | + // the array, you should sort both arrays here. | |
| 13 | + for (let i = 0; i < a.length; ++i) { | |
| 14 | + if (a[i] !== b[i]) return false; | |
| 15 | + } | |
| 16 | + return true; | |
| 17 | + } | |
| 18 | + | |
| 19 | +} | |
| 0 | 20 | \ No newline at end of file | ... | ... |
| ... | ... | @@ -0,0 +1,36 @@ |
| 1 | +export class HashMap<K extends EqualsObject, V> { | |
| 2 | + private values: Array<HashItem<K, V>> = new Array(); | |
| 3 | + | |
| 4 | + get(key: K): V { | |
| 5 | + let found = this.values.find( function(value: HashItem<K, V>) { | |
| 6 | + return value.key.equals(key); | |
| 7 | + }); | |
| 8 | + if (found) { | |
| 9 | + return found.value; | |
| 10 | + } | |
| 11 | + return null; | |
| 12 | + } | |
| 13 | + | |
| 14 | + put(key: K, value: V): void { | |
| 15 | + this.values.push(new HashItem(key, value)); | |
| 16 | + } | |
| 17 | + | |
| 18 | + clear() { | |
| 19 | + this.values = new Array(); | |
| 20 | + } | |
| 21 | +} | |
| 22 | + | |
| 23 | +export class HashItem<K, V> { | |
| 24 | + key: K; | |
| 25 | + value: V; | |
| 26 | + constructor(key: K, value: V) { | |
| 27 | + this.key = key; | |
| 28 | + this.value = value; | |
| 29 | + } | |
| 30 | +} | |
| 31 | + | |
| 32 | +export abstract class EqualsObject { | |
| 33 | + | |
| 34 | + abstract equals(other: any): boolean; | |
| 35 | + | |
| 36 | +} | |
| 0 | 37 | \ No newline at end of file | ... | ... |
src/languages/en.json
| ... | ... | @@ -37,6 +37,7 @@ |
| 37 | 37 | "comment.post.success.message": "Comment saved!", |
| 38 | 38 | "comment.reply": "reply", |
| 39 | 39 | "article.actions.edit": "Edit", |
| 40 | + "article.actions.delete": "Delete", | |
| 40 | 41 | "article.actions.read_more": "Read More", |
| 41 | 42 | "article.basic_editor.title": "Title", |
| 42 | 43 | "article.basic_editor.body": "Body", | ... | ... |
src/languages/pt.json
| ... | ... | @@ -37,6 +37,7 @@ |
| 37 | 37 | "comment.post.success.message": "Comentário salvo com sucesso!", |
| 38 | 38 | "comment.reply": "responder", |
| 39 | 39 | "article.actions.edit": "Editar", |
| 40 | + "article.actions.delete": "Excluir", | |
| 40 | 41 | "article.actions.read_more": "Ler mais", |
| 41 | 42 | "article.basic_editor.title": "Título", |
| 42 | 43 | "article.basic_editor.body": "Corpo", | ... | ... |
src/lib/ng-noosfero-api/http/article.service.spec.ts
| ... | ... | @@ -20,6 +20,14 @@ describe("Services", () => { |
| 20 | 20 | |
| 21 | 21 | describe("Succesfull requests", () => { |
| 22 | 22 | |
| 23 | + it("should remove article", (done) => { | |
| 24 | + let articleId = 1; | |
| 25 | + $httpBackend.expectDELETE(`/api/v1/articles/${articleId}`).respond(200, { success: "true" }); | |
| 26 | + articleService.removeArticle(<noosfero.Article>{id: articleId}); | |
| 27 | + $httpBackend.flush(); | |
| 28 | + done(); | |
| 29 | + }); | |
| 30 | + | |
| 23 | 31 | it("should return article children", (done) => { |
| 24 | 32 | let articleId = 1; |
| 25 | 33 | $httpBackend.expectGET(`/api/v1/articles/${articleId}/children`).respond(200, { articles: [{ name: "article1" }] }); | ... | ... |
src/lib/ng-noosfero-api/http/article.service.ts
| 1 | -import { Injectable, Inject } from "ng-forward"; | |
| 1 | +import { Injectable, Inject, EventEmitter } from "ng-forward"; | |
| 2 | 2 | import {RestangularService} from "./restangular_service"; |
| 3 | 3 | import {ProfileService} from "./profile.service"; |
| 4 | +import {NoosferoRootScope} from "./../../../app/shared/models/interfaces"; | |
| 5 | +import {ModelEvent, ArticleEventType} from "./../../../app/shared/models/events"; | |
| 6 | +import {HashMap} from "./../../../app/shared/utils/hashmap"; | |
| 4 | 7 | |
| 5 | 8 | @Injectable() |
| 6 | 9 | @Inject("Restangular", "$q", "$log", ProfileService) |
| 7 | - | |
| 8 | 10 | export class ArticleService extends RestangularService<noosfero.Article> { |
| 9 | 11 | |
| 12 | + private events: HashMap<ModelEvent, EventEmitter<noosfero.Article>> = new HashMap<ModelEvent, EventEmitter<noosfero.Article>>(); | |
| 13 | + | |
| 14 | + // This event is not tyed to any specific model element. | |
| 15 | + private removed: ModelEvent = ModelEvent.event(ArticleEventType.removed); | |
| 16 | + | |
| 10 | 17 | constructor(Restangular: restangular.IService, $q: ng.IQService, $log: ng.ILogService, protected profileService: ProfileService) { |
| 11 | 18 | super(Restangular, $q, $log); |
| 19 | + this.events.put(this.removed, new EventEmitter<noosfero.Article>()); | |
| 12 | 20 | } |
| 13 | 21 | |
| 14 | 22 | getResourcePath() { |
| ... | ... | @@ -22,6 +30,36 @@ export class ArticleService extends RestangularService<noosfero.Article> { |
| 22 | 30 | }; |
| 23 | 31 | } |
| 24 | 32 | |
| 33 | + removeArticle(article: noosfero.Article) { | |
| 34 | + let restRequest: ng.IPromise<noosfero.RestResult<noosfero.Article>> = this.remove(article); | |
| 35 | + let deferred = this.$q.defer<noosfero.RestResult<noosfero.Article>>(); | |
| 36 | + restRequest.then((result: any) => { | |
| 37 | + this.notifyArticleRemovedListeners(article); | |
| 38 | + }).catch(this.getHandleErrorFunction(deferred)); | |
| 39 | + return deferred.promise; | |
| 40 | + } | |
| 41 | + | |
| 42 | + /** | |
| 43 | + * Notify listeners that this article has been removed | |
| 44 | + */ | |
| 45 | + notifyArticleRemovedListeners(article: noosfero.Article) { | |
| 46 | + let listener = this.events.get(this.removed); | |
| 47 | + listener.next(article); | |
| 48 | + } | |
| 49 | + | |
| 50 | + /** | |
| 51 | + * Subscribe a listener a given article event | |
| 52 | + */ | |
| 53 | + subscribe(eventToSubscribe: ModelEvent, fn: Function) { | |
| 54 | + // Find the requested event in map | |
| 55 | + let event: EventEmitter<noosfero.Article> = this.events.get(eventToSubscribe); | |
| 56 | + if (event) { | |
| 57 | + event.subscribe(fn); | |
| 58 | + } else { | |
| 59 | + throw new Error(`The event: ${eventToSubscribe} not exists`); | |
| 60 | + } | |
| 61 | + } | |
| 62 | + | |
| 25 | 63 | updateArticle(article: noosfero.Article) { |
| 26 | 64 | let headers = { |
| 27 | 65 | 'Content-Type': 'application/json' |
| ... | ... | @@ -103,3 +141,4 @@ export class ArticleService extends RestangularService<noosfero.Article> { |
| 103 | 141 | |
| 104 | 142 | |
| 105 | 143 | } |
| 144 | + | ... | ... |
src/spec/mocks.ts
| ... | ... | @@ -76,6 +76,29 @@ export var mocks: any = { |
| 76 | 76 | isAuthenticated: () => { } |
| 77 | 77 | }, |
| 78 | 78 | articleService: { |
| 79 | + events: [ | |
| 80 | + { | |
| 81 | + event: Function, | |
| 82 | + subscribe: (fn: Function) => { | |
| 83 | + mocks.articleService.events[0].event = fn; | |
| 84 | + }, | |
| 85 | + next: (param: any) => { | |
| 86 | + mocks.articleService.events[0].event(param); | |
| 87 | + } | |
| 88 | + } | |
| 89 | + ], | |
| 90 | + removeArticle: (article: noosfero.Article) => { | |
| 91 | + return { | |
| 92 | + catch: (func?: Function) => { | |
| 93 | + } | |
| 94 | + }; | |
| 95 | + }, | |
| 96 | + notifyArticleRemovedListeners: (article: noosfero.Article) => { | |
| 97 | + mocks.articleService.events[0].next(article); | |
| 98 | + }, | |
| 99 | + subscribe: (eventType: any, fn: Function) => { | |
| 100 | + mocks.articleService.events[0].subscribe(fn); | |
| 101 | + }, | |
| 79 | 102 | getByProfile: (profileId: number, params?: any) => { |
| 80 | 103 | return { |
| 81 | 104 | then: (func?: Function) => { | ... | ... |