Ajout des tests
This commit is contained in:
+23
-2
@@ -25,7 +25,9 @@
|
|||||||
"input": "public"
|
"input": "public"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": ["src/styles.css"]
|
"styles": [
|
||||||
|
"src/styles.css"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
@@ -64,7 +66,26 @@
|
|||||||
"defaultConfiguration": "development"
|
"defaultConfiguration": "development"
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"builder": "@angular/build:unit-test"
|
"builder": "@angular/build:unit-test",
|
||||||
|
"options": {
|
||||||
|
"coverage": true,
|
||||||
|
"coverageReporters": [
|
||||||
|
"text",
|
||||||
|
"lcov"
|
||||||
|
],
|
||||||
|
"coverageThresholds": {
|
||||||
|
"lines": 90,
|
||||||
|
"functions": 90,
|
||||||
|
"branches": 80,
|
||||||
|
"statements": 90
|
||||||
|
},
|
||||||
|
"coverageExclude": [
|
||||||
|
"src/main.ts",
|
||||||
|
"src/app/app.config.ts",
|
||||||
|
"src/app/app.routes.ts",
|
||||||
|
"**/*.html"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+148
@@ -23,6 +23,7 @@
|
|||||||
"@angular/build": "^21.2.0",
|
"@angular/build": "^21.2.0",
|
||||||
"@angular/cli": "^21.2.0",
|
"@angular/cli": "^21.2.0",
|
||||||
"@angular/compiler-cli": "^21.2.0",
|
"@angular/compiler-cli": "^21.2.0",
|
||||||
|
"@vitest/coverage-v8": "^4.1.7",
|
||||||
"jsdom": "^28.0.0",
|
"jsdom": "^28.0.0",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"typescript": "~5.9.2",
|
"typescript": "~5.9.2",
|
||||||
@@ -956,6 +957,16 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@bcoe/v8-coverage": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@bramus/specificity": {
|
"node_modules/@bramus/specificity": {
|
||||||
"version": "2.4.2",
|
"version": "2.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
|
||||||
@@ -3943,6 +3954,37 @@
|
|||||||
"vite": "^6.0.0 || ^7.0.0"
|
"vite": "^6.0.0 || ^7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vitest/coverage-v8": {
|
||||||
|
"version": "4.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.7.tgz",
|
||||||
|
"integrity": "sha512-qsYPeXc5Q9dFLd1i8Ap+Bx8sQgcp+rFVQo4R0dDsWNBzl26ldVF1qOO+RL24K7FDrR6pA+50XedRLSoSG24bVQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@bcoe/v8-coverage": "^1.0.2",
|
||||||
|
"@vitest/utils": "4.1.7",
|
||||||
|
"ast-v8-to-istanbul": "^1.0.0",
|
||||||
|
"istanbul-lib-coverage": "^3.2.2",
|
||||||
|
"istanbul-lib-report": "^3.0.1",
|
||||||
|
"istanbul-reports": "^3.2.0",
|
||||||
|
"magicast": "^0.5.2",
|
||||||
|
"obug": "^2.1.1",
|
||||||
|
"std-env": "^4.0.0-rc.1",
|
||||||
|
"tinyrainbow": "^3.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@vitest/browser": "4.1.7",
|
||||||
|
"vitest": "4.1.7"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@vitest/browser": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vitest/expect": {
|
"node_modules/@vitest/expect": {
|
||||||
"version": "4.1.7",
|
"version": "4.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.7.tgz",
|
||||||
@@ -4217,6 +4259,25 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ast-v8-to-istanbul": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/trace-mapping": "^0.3.31",
|
||||||
|
"estree-walker": "^3.0.3",
|
||||||
|
"js-tokens": "^10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ast-v8-to-istanbul/node_modules/js-tokens": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||||
@@ -5490,6 +5551,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/has-flag": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/has-symbols": {
|
"node_modules/has-symbols": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
@@ -5562,6 +5633,13 @@
|
|||||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/html-escaper": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/htmlparser2": {
|
"node_modules/htmlparser2": {
|
||||||
"version": "10.1.0",
|
"version": "10.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
|
||||||
@@ -5840,6 +5918,35 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/istanbul-lib-report": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"istanbul-lib-coverage": "^3.0.0",
|
||||||
|
"make-dir": "^4.0.0",
|
||||||
|
"supports-color": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/istanbul-reports": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"html-escaper": "^2.0.0",
|
||||||
|
"istanbul-lib-report": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jose": {
|
"node_modules/jose": {
|
||||||
"version": "6.2.3",
|
"version": "6.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz",
|
||||||
@@ -6158,6 +6265,34 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/magicast": {
|
||||||
|
"version": "0.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz",
|
||||||
|
"integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/parser": "^7.29.3",
|
||||||
|
"@babel/types": "^7.29.0",
|
||||||
|
"source-map-js": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/make-dir": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"semver": "^7.5.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/make-fetch-happen": {
|
"node_modules/make-fetch-happen": {
|
||||||
"version": "15.0.5",
|
"version": "15.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.5.tgz",
|
||||||
@@ -7882,6 +8017,19 @@
|
|||||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/supports-color": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"has-flag": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/symbol-tree": {
|
"node_modules/symbol-tree": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"@angular/build": "^21.2.0",
|
"@angular/build": "^21.2.0",
|
||||||
"@angular/cli": "^21.2.0",
|
"@angular/cli": "^21.2.0",
|
||||||
"@angular/compiler-cli": "^21.2.0",
|
"@angular/compiler-cli": "^21.2.0",
|
||||||
|
"@vitest/coverage-v8": "^4.1.7",
|
||||||
"jsdom": "^28.0.0",
|
"jsdom": "^28.0.0",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"typescript": "~5.9.2",
|
"typescript": "~5.9.2",
|
||||||
|
|||||||
+4
-2
@@ -1,10 +1,12 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { provideRouter } from '@angular/router';
|
||||||
import { App } from './app';
|
import { App } from './app';
|
||||||
|
|
||||||
describe('App', () => {
|
describe('App', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [App],
|
imports: [App],
|
||||||
|
providers: [provideRouter([])],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -14,10 +16,10 @@ describe('App', () => {
|
|||||||
expect(app).toBeTruthy();
|
expect(app).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render title', async () => {
|
it('should render the app layout', async () => {
|
||||||
const fixture = TestBed.createComponent(App);
|
const fixture = TestBed.createComponent(App);
|
||||||
await fixture.whenStable();
|
await fixture.whenStable();
|
||||||
const compiled = fixture.nativeElement as HTMLElement;
|
const compiled = fixture.nativeElement as HTMLElement;
|
||||||
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, Bonsai-webapp');
|
expect(compiled.querySelector('app-menu')).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { IssueComments } from './issue-comments';
|
||||||
|
import { IssuesStore } from '../issues.store';
|
||||||
|
|
||||||
|
describe('IssueComments', () => {
|
||||||
|
let component: IssueComments;
|
||||||
|
let fixture: ComponentFixture<IssueComments>;
|
||||||
|
let store: IssuesStore;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
localStorage.clear();
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [IssueComments],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
store = TestBed.inject(IssuesStore);
|
||||||
|
fixture = TestBed.createComponent(IssueComments);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.componentRef.setInput('issueId', 1);
|
||||||
|
await fixture.whenStable();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
localStorage.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('comments signal', () => {
|
||||||
|
it('returns empty array when issue has no comments', () => {
|
||||||
|
expect((component as any).comments()).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reflects comments after adding one', () => {
|
||||||
|
(component as any).newCommentText = 'Hello';
|
||||||
|
(component as any).addComment();
|
||||||
|
expect((component as any).comments().length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatDate', () => {
|
||||||
|
it('formats an ISO date string as a localised date', () => {
|
||||||
|
const result = (component as any).formatDate('2026-01-15T10:30:00.000Z');
|
||||||
|
expect(typeof result).toBe('string');
|
||||||
|
expect(result.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parseMarkdown', () => {
|
||||||
|
it('converts markdown bold to HTML', () => {
|
||||||
|
const result = (component as any).parseMarkdown('**bold**');
|
||||||
|
expect(String(result)).toContain('strong');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a truthy SafeHtml object', () => {
|
||||||
|
const result = (component as any).parseMarkdown('hello');
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addComment', () => {
|
||||||
|
it('adds a comment to the issue in the store', () => {
|
||||||
|
(component as any).newCommentText = 'Test comment';
|
||||||
|
(component as any).addComment();
|
||||||
|
expect(store.getById(1)?.comments.length).toBe(1);
|
||||||
|
expect(store.getById(1)?.comments[0].text).toBe('Test comment');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clears newCommentText after adding', () => {
|
||||||
|
(component as any).newCommentText = 'Some text';
|
||||||
|
(component as any).addComment();
|
||||||
|
expect((component as any).newCommentText).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing when newCommentText is empty', () => {
|
||||||
|
(component as any).newCommentText = ' ';
|
||||||
|
(component as any).addComment();
|
||||||
|
expect(store.getById(1)?.comments.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('assigns an incrementing id to new comments', () => {
|
||||||
|
(component as any).newCommentText = 'First';
|
||||||
|
(component as any).addComment();
|
||||||
|
(component as any).newCommentText = 'Second';
|
||||||
|
(component as any).addComment();
|
||||||
|
const comments = store.getById(1)?.comments ?? [];
|
||||||
|
expect(comments[0].id).toBe(1);
|
||||||
|
expect(comments[1].id).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets createdAt to an ISO date string', () => {
|
||||||
|
(component as any).newCommentText = 'Dated comment';
|
||||||
|
(component as any).addComment();
|
||||||
|
const comment = store.getById(1)?.comments[0];
|
||||||
|
expect(comment?.createdAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets updatedAt to null initially', () => {
|
||||||
|
(component as any).newCommentText = 'New comment';
|
||||||
|
(component as any).addComment();
|
||||||
|
expect(store.getById(1)?.comments[0].updatedAt).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('startEditComment', () => {
|
||||||
|
it('sets editingCommentId and editingCommentText', () => {
|
||||||
|
(component as any).newCommentText = 'Editable';
|
||||||
|
(component as any).addComment();
|
||||||
|
const comment = store.getById(1)!.comments[0];
|
||||||
|
|
||||||
|
(component as any).startEditComment(comment);
|
||||||
|
|
||||||
|
expect((component as any).editingCommentId).toBe(comment.id);
|
||||||
|
expect((component as any).editingCommentText).toBe('Editable');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('cancelEditComment', () => {
|
||||||
|
it('resets editingCommentId and editingCommentText', () => {
|
||||||
|
(component as any).editingCommentId = 1;
|
||||||
|
(component as any).editingCommentText = 'In progress edit';
|
||||||
|
(component as any).cancelEditComment();
|
||||||
|
expect((component as any).editingCommentId).toBeNull();
|
||||||
|
expect((component as any).editingCommentText).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('saveEditComment', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
(component as any).newCommentText = 'Original text';
|
||||||
|
(component as any).addComment();
|
||||||
|
const comment = store.getById(1)!.comments[0];
|
||||||
|
(component as any).startEditComment(comment);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates the comment text in the store', () => {
|
||||||
|
(component as any).editingCommentText = 'Updated text';
|
||||||
|
(component as any).saveEditComment();
|
||||||
|
expect(store.getById(1)?.comments[0].text).toBe('Updated text');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets updatedAt on the edited comment', () => {
|
||||||
|
(component as any).editingCommentText = 'Changed';
|
||||||
|
(component as any).saveEditComment();
|
||||||
|
expect(store.getById(1)?.comments[0].updatedAt).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resets editing state after saving', () => {
|
||||||
|
(component as any).editingCommentText = 'Done';
|
||||||
|
(component as any).saveEditComment();
|
||||||
|
expect((component as any).editingCommentId).toBeNull();
|
||||||
|
expect((component as any).editingCommentText).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing when editingCommentText is empty', () => {
|
||||||
|
(component as any).editingCommentText = ' ';
|
||||||
|
(component as any).saveEditComment();
|
||||||
|
expect(store.getById(1)?.comments[0].text).toBe('Original text');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing when editingCommentId is null', () => {
|
||||||
|
(component as any).editingCommentId = null;
|
||||||
|
(component as any).editingCommentText = 'Will not save';
|
||||||
|
(component as any).saveEditComment();
|
||||||
|
expect(store.getById(1)?.comments[0].text).toBe('Original text');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteComment', () => {
|
||||||
|
it('removes the comment from the issue', () => {
|
||||||
|
(component as any).newCommentText = 'To delete';
|
||||||
|
(component as any).addComment();
|
||||||
|
const commentId = store.getById(1)!.comments[0].id;
|
||||||
|
|
||||||
|
(component as any).deleteComment(commentId);
|
||||||
|
|
||||||
|
expect(store.getById(1)?.comments.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only removes the targeted comment', () => {
|
||||||
|
(component as any).newCommentText = 'Keep me';
|
||||||
|
(component as any).addComment();
|
||||||
|
(component as any).newCommentText = 'Delete me';
|
||||||
|
(component as any).addComment();
|
||||||
|
|
||||||
|
const comments = store.getById(1)!.comments;
|
||||||
|
(component as any).deleteComment(comments[1].id);
|
||||||
|
|
||||||
|
expect(store.getById(1)?.comments.length).toBe(1);
|
||||||
|
expect(store.getById(1)?.comments[0].text).toBe('Keep me');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,22 +1,500 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { ActivatedRoute, convertToParamMap, Router } from '@angular/router';
|
||||||
|
import { provideRouter } from '@angular/router';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { vi } from 'vitest';
|
||||||
import { IssueDetail } from './issue-detail';
|
import { IssueDetail } from './issue-detail';
|
||||||
|
import { IssueEntity, IssuesStore } from '../issues.store';
|
||||||
|
|
||||||
describe('IssueDetail', () => {
|
const makeIssue = (overrides: Partial<IssueEntity> = {}): IssueEntity => ({
|
||||||
|
id: 99,
|
||||||
|
type: 'Story',
|
||||||
|
assignee: '',
|
||||||
|
epic: '',
|
||||||
|
name: 'Test Issue',
|
||||||
|
dueDate: '',
|
||||||
|
description: '',
|
||||||
|
estimatedTime: null,
|
||||||
|
dependsOnIds: [],
|
||||||
|
comments: [],
|
||||||
|
priority: 'Moyenne',
|
||||||
|
status: 'draft',
|
||||||
|
progress: 0,
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
|
||||||
|
function makeRoute(id = '1', path = 'issues/:id') {
|
||||||
|
return {
|
||||||
|
snapshot: {
|
||||||
|
routeConfig: { path },
|
||||||
|
paramMap: convertToParamMap(id ? { id } : {}),
|
||||||
|
queryParamMap: convertToParamMap({}),
|
||||||
|
},
|
||||||
|
paramMap: of(convertToParamMap(id ? { id } : {})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('IssueDetail — existing issue', () => {
|
||||||
let component: IssueDetail;
|
let component: IssueDetail;
|
||||||
let fixture: ComponentFixture<IssueDetail>;
|
let fixture: ComponentFixture<IssueDetail>;
|
||||||
|
let store: IssuesStore;
|
||||||
|
let router: Router;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
localStorage.clear();
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [IssueDetail],
|
imports: [IssueDetail],
|
||||||
|
providers: [
|
||||||
|
provideRouter([]),
|
||||||
|
{ provide: ActivatedRoute, useValue: makeRoute('1') },
|
||||||
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
|
store = TestBed.inject(IssuesStore);
|
||||||
|
router = TestBed.inject(Router);
|
||||||
fixture = TestBed.createComponent(IssueDetail);
|
fixture = TestBed.createComponent(IssueDetail);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
await fixture.whenStable();
|
await fixture.whenStable();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
localStorage.clear();
|
||||||
|
});
|
||||||
|
|
||||||
it('should create', () => {
|
it('should create', () => {
|
||||||
expect(component).toBeTruthy();
|
expect(component).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('isNewIssueRoute is false', () => {
|
||||||
|
expect((component as any).isNewIssueRoute).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the issue from the route param', () => {
|
||||||
|
expect((component as any).issue.id).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateStatus', () => {
|
||||||
|
it('updates the status and persists to the store', () => {
|
||||||
|
(component as any).updateStatus('done');
|
||||||
|
expect((component as any).issue.status).toBe('done');
|
||||||
|
expect(store.getById(1)?.status).toBe('done');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toggleMoreMenu / closeMoreMenu', () => {
|
||||||
|
it('toggleMoreMenu switches moreMenuOpen', () => {
|
||||||
|
expect((component as any).moreMenuOpen).toBe(false);
|
||||||
|
(component as any).toggleMoreMenu();
|
||||||
|
expect((component as any).moreMenuOpen).toBe(true);
|
||||||
|
(component as any).toggleMoreMenu();
|
||||||
|
expect((component as any).moreMenuOpen).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('closeMoreMenu sets moreMenuOpen to false', () => {
|
||||||
|
(component as any).moreMenuOpen = true;
|
||||||
|
(component as any).closeMoreMenu();
|
||||||
|
expect((component as any).moreMenuOpen).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('saveIssue', () => {
|
||||||
|
it('persists the issue to the store', () => {
|
||||||
|
(component as any).issue.name = 'Renamed';
|
||||||
|
(component as any).saveIssue();
|
||||||
|
expect(store.getById(1)?.name).toBe('Renamed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing when name is blank', () => {
|
||||||
|
const countBefore = store.issues().length;
|
||||||
|
(component as any).issue.name = ' ';
|
||||||
|
(component as any).saveIssue();
|
||||||
|
expect(store.issues().length).toBe(countBefore);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteIssue', () => {
|
||||||
|
it('removes the issue and navigates to /issues', async () => {
|
||||||
|
const spy = vi.spyOn(router, 'navigate').mockResolvedValue(true);
|
||||||
|
(component as any).deleteIssue();
|
||||||
|
expect(store.getById(1)).toBeUndefined();
|
||||||
|
expect(spy).toHaveBeenCalledWith(['/issues']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('cancelCreation', () => {
|
||||||
|
it('navigates to /issues', async () => {
|
||||||
|
const spy = vi.spyOn(router, 'navigate').mockResolvedValue(true);
|
||||||
|
(component as any).cancelCreation();
|
||||||
|
expect(spy).toHaveBeenCalledWith(['/issues']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('dependency management', () => {
|
||||||
|
it('dependencyIds returns the issue dependsOnIds', () => {
|
||||||
|
(component as any).issue.dependsOnIds = [2, 3];
|
||||||
|
expect((component as any).dependencyIds).toEqual([2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('availableCandidates excludes current issue', () => {
|
||||||
|
const candidates: IssueEntity[] = (component as any).availableCandidates;
|
||||||
|
expect(candidates.some((i) => i.id === 1)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('availableCandidates excludes existing dependencies', () => {
|
||||||
|
(component as any).issue.dependsOnIds = [2];
|
||||||
|
const candidates: IssueEntity[] = (component as any).availableCandidates;
|
||||||
|
expect(candidates.some((i) => i.id === 2)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolveDependency returns the matching issue', () => {
|
||||||
|
const resolved = (component as any).resolveDependency(2);
|
||||||
|
expect(resolved?.id).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolveDependency returns undefined for unknown id', () => {
|
||||||
|
expect((component as any).resolveDependency(9999)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('openAddDependency sets showAddDependency to true', () => {
|
||||||
|
(component as any).openAddDependency();
|
||||||
|
expect((component as any).showAddDependency).toBe(true);
|
||||||
|
expect((component as any).selectedCandidateId).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cancelAddDependency hides the form and resets candidate', () => {
|
||||||
|
(component as any).showAddDependency = true;
|
||||||
|
(component as any).selectedCandidateId = 2;
|
||||||
|
(component as any).cancelAddDependency();
|
||||||
|
expect((component as any).showAddDependency).toBe(false);
|
||||||
|
expect((component as any).selectedCandidateId).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('confirmAddDependency adds the selected id and saves', () => {
|
||||||
|
(component as any).selectedCandidateId = 2;
|
||||||
|
(component as any).confirmAddDependency();
|
||||||
|
expect((component as any).issue.dependsOnIds).toContain(2);
|
||||||
|
expect(store.getById(1)?.dependsOnIds).toContain(2);
|
||||||
|
expect((component as any).showAddDependency).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('confirmAddDependency does nothing when no candidate is selected', () => {
|
||||||
|
(component as any).selectedCandidateId = null;
|
||||||
|
(component as any).confirmAddDependency();
|
||||||
|
expect((component as any).issue.dependsOnIds).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removeDependency removes the id and saves', () => {
|
||||||
|
(component as any).issue.dependsOnIds = [2, 3];
|
||||||
|
store.upsert({ ...(component as any).issue });
|
||||||
|
(component as any).removeDependency(2);
|
||||||
|
expect((component as any).issue.dependsOnIds).not.toContain(2);
|
||||||
|
expect((component as any).issue.dependsOnIds).toContain(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('estimatedTimeValue getter / setter', () => {
|
||||||
|
it('getter returns issue.estimatedTime', () => {
|
||||||
|
(component as any).issue.estimatedTime = 8;
|
||||||
|
expect((component as any).estimatedTimeValue).toBe(8);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('setter converts string to number', () => {
|
||||||
|
(component as any).estimatedTimeValue = '3.5';
|
||||||
|
expect((component as any).issue.estimatedTime).toBe(3.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('setter stores null when value is null', () => {
|
||||||
|
(component as any).estimatedTimeValue = null;
|
||||||
|
expect((component as any).issue.estimatedTime).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('issueTypeValue getter / setter', () => {
|
||||||
|
it('getter returns issue.type', () => {
|
||||||
|
(component as any).issue.type = 'Bug';
|
||||||
|
expect((component as any).issueTypeValue).toBe('Bug');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('setter updates issue.type', () => {
|
||||||
|
(component as any).issueTypeValue = 'Epic';
|
||||||
|
expect((component as any).issue.type).toBe('Epic');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isEpicIssue', () => {
|
||||||
|
it('is false for Story type', () => {
|
||||||
|
(component as any).issue.type = 'Story';
|
||||||
|
expect((component as any).isEpicIssue).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is true for Epic type', () => {
|
||||||
|
(component as any).issue.type = 'Epic';
|
||||||
|
expect((component as any).isEpicIssue).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getBadgeClass / typeBadgeClass', () => {
|
||||||
|
it('typeBadgeClass returns class for current issue type', () => {
|
||||||
|
(component as any).issue.type = 'Bug';
|
||||||
|
expect((component as any).typeBadgeClass).toBe('text-bg-danger');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getBadgeClass maps Bug to text-bg-danger', () => {
|
||||||
|
expect((component as any).getBadgeClass('Bug')).toBe('text-bg-danger');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getBadgeClass maps Study to text-bg-secondary', () => {
|
||||||
|
expect((component as any).getBadgeClass('Study')).toBe('text-bg-secondary');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getBadgeClass maps Story to text-bg-success', () => {
|
||||||
|
expect((component as any).getBadgeClass('Story')).toBe('text-bg-success');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getBadgeClass maps Task to text-bg-primary', () => {
|
||||||
|
expect((component as any).getBadgeClass('Task')).toBe('text-bg-primary');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getBadgeClass maps Technical Story to text-bg-warning', () => {
|
||||||
|
expect((component as any).getBadgeClass('Technical Story')).toBe('text-bg-warning');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getBadgeClass maps Epic to text-bg-info', () => {
|
||||||
|
expect((component as any).getBadgeClass('Epic')).toBe('text-bg-info');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('epicIssues / epicIssueId', () => {
|
||||||
|
it('epicIssues returns only Epic-type issues', () => {
|
||||||
|
store.upsert(makeIssue({ id: 100, type: 'Epic', name: 'My Epic' }));
|
||||||
|
const epics: IssueEntity[] = (component as any).epicIssues;
|
||||||
|
expect(epics.every((e) => e.type === 'Epic')).toBe(true);
|
||||||
|
expect(epics.some((e) => e.id === 100)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('epicIssueId returns the id of the linked epic', () => {
|
||||||
|
store.upsert(makeIssue({ id: 100, type: 'Epic', name: 'Linked Epic' }));
|
||||||
|
(component as any).issue.epic = 'Linked Epic';
|
||||||
|
expect((component as any).epicIssueId).toBe(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('epicIssueId returns null when no matching epic', () => {
|
||||||
|
(component as any).issue.epic = '';
|
||||||
|
expect((component as any).epicIssueId).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('navigateToEpic', () => {
|
||||||
|
it('navigates to the epic issue', async () => {
|
||||||
|
store.upsert(makeIssue({ id: 100, type: 'Epic', name: 'Nav Epic' }));
|
||||||
|
(component as any).issue.epic = 'Nav Epic';
|
||||||
|
const spy = vi.spyOn(router, 'navigate').mockResolvedValue(true);
|
||||||
|
(component as any).navigateToEpic();
|
||||||
|
expect(spy).toHaveBeenCalledWith(['/issues', 100]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing when no matching epic is found', () => {
|
||||||
|
(component as any).issue.epic = 'Ghost Epic';
|
||||||
|
const spy = vi.spyOn(router, 'navigate').mockResolvedValue(true);
|
||||||
|
(component as any).navigateToEpic();
|
||||||
|
expect(spy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('openComposedIssue', () => {
|
||||||
|
it('navigates to the composed issue detail', async () => {
|
||||||
|
const spy = vi.spyOn(router, 'navigate').mockResolvedValue(true);
|
||||||
|
(component as any).openComposedIssue(42);
|
||||||
|
expect(spy).toHaveBeenCalledWith(['/issues', 42]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('composedIssues / epicCandidates', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
(component as any).issue.type = 'Epic';
|
||||||
|
(component as any).issue.name = 'Test Epic';
|
||||||
|
});
|
||||||
|
|
||||||
|
it('composedIssues includes issues whose epic matches the current name', () => {
|
||||||
|
store.upsert(makeIssue({ id: 200, name: 'Child', epic: 'Test Epic' }));
|
||||||
|
const composed: IssueEntity[] = (component as any).composedIssues;
|
||||||
|
expect(composed.some((i) => i.id === 200)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('composedIssues includes issues that depend on the current issue', () => {
|
||||||
|
store.upsert(makeIssue({ id: 201, name: 'Dep', dependsOnIds: [1] }));
|
||||||
|
const composed: IssueEntity[] = (component as any).composedIssues;
|
||||||
|
expect(composed.some((i) => i.id === 201)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('composedIssues does not include the current issue itself', () => {
|
||||||
|
const composed: IssueEntity[] = (component as any).composedIssues;
|
||||||
|
expect(composed.some((i) => i.id === 1)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('epicCandidates excludes already composed issues', () => {
|
||||||
|
store.upsert(makeIssue({ id: 200, name: 'Child', epic: 'Test Epic' }));
|
||||||
|
const candidates: IssueEntity[] = (component as any).epicCandidates;
|
||||||
|
expect(candidates.some((i) => i.id === 200)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('create-in-epic flow', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
(component as any).issue.type = 'Epic';
|
||||||
|
(component as any).issue.name = 'My Epic';
|
||||||
|
});
|
||||||
|
|
||||||
|
it('openCreateInEpic shows the create form and hides add form', () => {
|
||||||
|
(component as any).showAddToEpic = true;
|
||||||
|
(component as any).openCreateInEpic();
|
||||||
|
expect((component as any).showCreateInEpic).toBe(true);
|
||||||
|
expect((component as any).showAddToEpic).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cancelCreateInEpic hides the form and clears the name', () => {
|
||||||
|
(component as any).showCreateInEpic = true;
|
||||||
|
(component as any).newIssueName = 'Draft';
|
||||||
|
(component as any).cancelCreateInEpic();
|
||||||
|
expect((component as any).showCreateInEpic).toBe(false);
|
||||||
|
expect((component as any).newIssueName).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('confirmCreateInEpic creates a child issue linked to the epic', () => {
|
||||||
|
(component as any).newIssueName = 'Child Issue';
|
||||||
|
const before = store.issues().length;
|
||||||
|
(component as any).confirmCreateInEpic();
|
||||||
|
expect(store.issues().length).toBe(before + 1);
|
||||||
|
const created = store.issues().find((i) => i.name === 'Child Issue');
|
||||||
|
expect(created?.epic).toBe('My Epic');
|
||||||
|
expect(created?.type).toBe('Story');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('confirmCreateInEpic resets the form', () => {
|
||||||
|
(component as any).newIssueName = 'Child Issue';
|
||||||
|
(component as any).confirmCreateInEpic();
|
||||||
|
expect((component as any).showCreateInEpic).toBe(false);
|
||||||
|
expect((component as any).newIssueName).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('confirmCreateInEpic does nothing when name is blank', () => {
|
||||||
|
(component as any).newIssueName = ' ';
|
||||||
|
const before = store.issues().length;
|
||||||
|
(component as any).confirmCreateInEpic();
|
||||||
|
expect(store.issues().length).toBe(before);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('add-to-epic flow', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
(component as any).issue.type = 'Epic';
|
||||||
|
(component as any).issue.name = 'My Epic';
|
||||||
|
});
|
||||||
|
|
||||||
|
it('openAddToEpic shows the add form', () => {
|
||||||
|
(component as any).openAddToEpic();
|
||||||
|
expect((component as any).showAddToEpic).toBe(true);
|
||||||
|
expect((component as any).selectedEpicCandidateId).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cancelAddToEpic hides the form', () => {
|
||||||
|
(component as any).showAddToEpic = true;
|
||||||
|
(component as any).selectedEpicCandidateId = 2;
|
||||||
|
(component as any).cancelAddToEpic();
|
||||||
|
expect((component as any).showAddToEpic).toBe(false);
|
||||||
|
expect((component as any).selectedEpicCandidateId).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('confirmAddToEpic assigns the epic name to the selected issue', () => {
|
||||||
|
(component as any).selectedEpicCandidateId = 2;
|
||||||
|
(component as any).confirmAddToEpic();
|
||||||
|
expect(store.getById(2)?.epic).toBe('My Epic');
|
||||||
|
expect((component as any).showAddToEpic).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('confirmAddToEpic does nothing when no candidate is selected', () => {
|
||||||
|
const epicBefore = store.getById(2)?.epic;
|
||||||
|
(component as any).selectedEpicCandidateId = null;
|
||||||
|
(component as any).confirmAddToEpic();
|
||||||
|
expect(store.getById(2)?.epic).toBe(epicBefore);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('descriptionHtml', () => {
|
||||||
|
it('returns a truthy SafeHtml for markdown input', () => {
|
||||||
|
(component as any).issue.description = '# Title\n**bold**';
|
||||||
|
expect((component as any).descriptionHtml).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles empty description', () => {
|
||||||
|
(component as any).issue.description = '';
|
||||||
|
expect((component as any).descriptionHtml).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('IssueDetail — new issue route', () => {
|
||||||
|
let component: IssueDetail;
|
||||||
|
let fixture: ComponentFixture<IssueDetail>;
|
||||||
|
let store: IssuesStore;
|
||||||
|
let router: Router;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
localStorage.clear();
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [IssueDetail],
|
||||||
|
providers: [
|
||||||
|
provideRouter([]),
|
||||||
|
{
|
||||||
|
provide: ActivatedRoute,
|
||||||
|
useValue: {
|
||||||
|
snapshot: {
|
||||||
|
routeConfig: { path: 'issues/new' },
|
||||||
|
paramMap: convertToParamMap({}),
|
||||||
|
queryParamMap: convertToParamMap({ draftId: '10' }),
|
||||||
|
},
|
||||||
|
paramMap: of(convertToParamMap({})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
store = TestBed.inject(IssuesStore);
|
||||||
|
router = TestBed.inject(Router);
|
||||||
|
fixture = TestBed.createComponent(IssueDetail);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
await fixture.whenStable();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
localStorage.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('isNewIssueRoute is true', () => {
|
||||||
|
expect((component as any).isNewIssueRoute).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('buildIssue creates an empty issue with draft id', () => {
|
||||||
|
expect((component as any).issue.id).toBe(10);
|
||||||
|
expect((component as any).issue.name).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('saveIssue without explicit flag does nothing for new route', () => {
|
||||||
|
(component as any).issue.name = 'Draft Name';
|
||||||
|
const countBefore = store.issues().length;
|
||||||
|
(component as any).saveIssue(); // explicit = false
|
||||||
|
expect(store.issues().length).toBe(countBefore);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('saveIssue with explicit=true creates the issue and navigates', async () => {
|
||||||
|
const spy = vi.spyOn(router, 'navigate').mockResolvedValue(true);
|
||||||
|
(component as any).issue.name = 'Brand New Issue';
|
||||||
|
(component as any).saveIssue(true);
|
||||||
|
expect(store.issues().some((i) => i.name === 'Brand New Issue')).toBe(true);
|
||||||
|
expect(spy).toHaveBeenCalledWith(['/issues', 10]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,22 +1,179 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { provideRouter } from '@angular/router';
|
||||||
|
import { vi } from 'vitest';
|
||||||
import { Issues } from './issues';
|
import { Issues } from './issues';
|
||||||
|
import { IssueEntity, IssuesStore } from './issues.store';
|
||||||
|
|
||||||
|
const makeIssue = (overrides: Partial<IssueEntity> = {}): IssueEntity => ({
|
||||||
|
id: 99,
|
||||||
|
type: 'Story',
|
||||||
|
assignee: '',
|
||||||
|
epic: '',
|
||||||
|
name: 'Test Issue',
|
||||||
|
dueDate: '',
|
||||||
|
description: '',
|
||||||
|
estimatedTime: null,
|
||||||
|
dependsOnIds: [],
|
||||||
|
comments: [],
|
||||||
|
priority: 'Moyenne',
|
||||||
|
status: 'draft',
|
||||||
|
progress: 50,
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
|
||||||
describe('Issues', () => {
|
describe('Issues', () => {
|
||||||
let component: Issues;
|
let component: Issues;
|
||||||
let fixture: ComponentFixture<Issues>;
|
let fixture: ComponentFixture<Issues>;
|
||||||
|
let store: IssuesStore;
|
||||||
|
let router: Router;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
localStorage.clear();
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [Issues],
|
imports: [Issues],
|
||||||
|
providers: [provideRouter([])],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
|
store = TestBed.inject(IssuesStore);
|
||||||
|
router = TestBed.inject(Router);
|
||||||
fixture = TestBed.createComponent(Issues);
|
fixture = TestBed.createComponent(Issues);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
await fixture.whenStable();
|
await fixture.whenStable();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
localStorage.clear();
|
||||||
|
});
|
||||||
|
|
||||||
it('should create', () => {
|
it('should create', () => {
|
||||||
expect(component).toBeTruthy();
|
expect(component).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('filteredIssues', () => {
|
||||||
|
it('returns all issues when no type is selected', () => {
|
||||||
|
(component as any).selectedType = null;
|
||||||
|
expect((component as any).filteredIssues.length).toBe(store.issues().length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns only issues matching the selected type', () => {
|
||||||
|
(component as any).selectedType = 'Bug';
|
||||||
|
const filtered: IssueEntity[] = (component as any).filteredIssues;
|
||||||
|
expect(filtered.every((i) => i.type === 'Bug')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty array when no issues match the selected type', () => {
|
||||||
|
(component as any).selectedType = 'Epic';
|
||||||
|
const filtered: IssueEntity[] = (component as any).filteredIssues;
|
||||||
|
// Default store has no Epics, so this should be empty
|
||||||
|
expect(filtered.every((i) => i.type === 'Epic')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('selectType', () => {
|
||||||
|
it('sets selectedType when none is active', () => {
|
||||||
|
(component as any).selectedType = null;
|
||||||
|
(component as any).selectType('Bug');
|
||||||
|
expect((component as any).selectedType).toBe('Bug');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clears selectedType when the same type is selected again (toggle off)', () => {
|
||||||
|
(component as any).selectedType = 'Bug';
|
||||||
|
(component as any).selectType('Bug');
|
||||||
|
expect((component as any).selectedType).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('switches to a different type', () => {
|
||||||
|
(component as any).selectedType = 'Bug';
|
||||||
|
(component as any).selectType('Story');
|
||||||
|
expect((component as any).selectedType).toBe('Story');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('selectType(null) clears the filter', () => {
|
||||||
|
(component as any).selectedType = 'Bug';
|
||||||
|
(component as any).selectType(null);
|
||||||
|
expect((component as any).selectedType).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createIssue', () => {
|
||||||
|
it('navigates to /issues/new with a draftId query param', async () => {
|
||||||
|
const spy = vi.spyOn(router, 'navigate').mockResolvedValue(true);
|
||||||
|
(component as any).createIssue();
|
||||||
|
expect(spy).toHaveBeenCalledWith(
|
||||||
|
['/issues/new'],
|
||||||
|
expect.objectContaining({ queryParams: expect.objectContaining({ mode: 'edit' }) }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('openIssue', () => {
|
||||||
|
it('navigates to the issue detail page', async () => {
|
||||||
|
const spy = vi.spyOn(router, 'navigate').mockResolvedValue(true);
|
||||||
|
(component as any).openIssue(42);
|
||||||
|
expect(spy).toHaveBeenCalledWith(['/issues', 42]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getProgress', () => {
|
||||||
|
it('returns issue.progress for non-Epic types', () => {
|
||||||
|
const issue = makeIssue({ type: 'Story', progress: 75 });
|
||||||
|
expect((component as any).getProgress(issue)).toBe(75);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 0 for an Epic with no children', () => {
|
||||||
|
const epic = makeIssue({ id: 50, type: 'Epic', name: 'Empty Epic', progress: 0 });
|
||||||
|
store.upsert(epic);
|
||||||
|
expect((component as any).getProgress(epic)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 100 for an Epic where all children are done', () => {
|
||||||
|
const epic = makeIssue({ id: 51, type: 'Epic', name: 'Full Epic', progress: 0 });
|
||||||
|
store.upsert(epic);
|
||||||
|
store.upsert(makeIssue({ id: 52, name: 'Child 1', epic: 'Full Epic', status: 'done' }));
|
||||||
|
store.upsert(makeIssue({ id: 53, name: 'Child 2', epic: 'Full Epic', status: 'done' }));
|
||||||
|
expect((component as any).getProgress(epic)).toBe(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calculates percentage for an Epic with some done children', () => {
|
||||||
|
const epic = makeIssue({ id: 54, type: 'Epic', name: 'Partial Epic', progress: 0 });
|
||||||
|
store.upsert(epic);
|
||||||
|
store.upsert(makeIssue({ id: 55, name: 'Done', epic: 'Partial Epic', status: 'done' }));
|
||||||
|
store.upsert(makeIssue({ id: 56, name: 'Pending', epic: 'Partial Epic', status: 'todo' }));
|
||||||
|
expect((component as any).getProgress(epic)).toBe(50);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('counts children by dependsOnIds as well as epic name', () => {
|
||||||
|
const epic = makeIssue({ id: 57, type: 'Epic', name: 'Dep Epic', progress: 0 });
|
||||||
|
store.upsert(epic);
|
||||||
|
store.upsert(makeIssue({ id: 58, name: 'DepChild', dependsOnIds: [57], status: 'done' }));
|
||||||
|
expect((component as any).getProgress(epic)).toBe(100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('typeBadgeClass', () => {
|
||||||
|
it('maps Bug to text-bg-danger', () => {
|
||||||
|
expect((component as any).typeBadgeClass('Bug')).toBe('text-bg-danger');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps Study to text-bg-secondary', () => {
|
||||||
|
expect((component as any).typeBadgeClass('Study')).toBe('text-bg-secondary');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps Story to text-bg-success', () => {
|
||||||
|
expect((component as any).typeBadgeClass('Story')).toBe('text-bg-success');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps Task to text-bg-primary', () => {
|
||||||
|
expect((component as any).typeBadgeClass('Task')).toBe('text-bg-primary');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps Technical Story to text-bg-warning', () => {
|
||||||
|
expect((component as any).typeBadgeClass('Technical Story')).toBe('text-bg-warning');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps Epic to text-bg-info', () => {
|
||||||
|
expect((component as any).typeBadgeClass('Epic')).toBe('text-bg-info');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { IssueEntity, IssuesStore } from './issues.store';
|
||||||
|
|
||||||
|
const makeIssue = (overrides: Partial<IssueEntity> = {}): IssueEntity => ({
|
||||||
|
id: 99,
|
||||||
|
type: 'Story',
|
||||||
|
assignee: '',
|
||||||
|
epic: '',
|
||||||
|
name: 'Test Issue',
|
||||||
|
dueDate: '',
|
||||||
|
description: '',
|
||||||
|
estimatedTime: null,
|
||||||
|
dependsOnIds: [],
|
||||||
|
comments: [],
|
||||||
|
priority: 'Moyenne',
|
||||||
|
status: 'draft',
|
||||||
|
progress: 0,
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('IssuesStore', () => {
|
||||||
|
let store: IssuesStore;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorage.clear();
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
store = TestBed.inject(IssuesStore);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
localStorage.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(store).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load default issues when localStorage is empty', () => {
|
||||||
|
expect(store.issues().length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getById', () => {
|
||||||
|
it('returns the issue with the given id', () => {
|
||||||
|
const issue = store.getById(1);
|
||||||
|
expect(issue?.id).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns undefined for an unknown id', () => {
|
||||||
|
expect(store.getById(9999)).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getNextId', () => {
|
||||||
|
it('returns max id + 1', () => {
|
||||||
|
const ids = store.issues().map((i) => i.id);
|
||||||
|
const expectedNext = Math.max(...ids) + 1;
|
||||||
|
expect(store.getNextId()).toBe(expectedNext);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 1 when there are no issues', () => {
|
||||||
|
store.deleteById(1);
|
||||||
|
store.deleteById(2);
|
||||||
|
store.deleteById(3);
|
||||||
|
expect(store.getNextId()).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('upsert', () => {
|
||||||
|
it('adds a new issue when the id does not exist', () => {
|
||||||
|
const before = store.issues().length;
|
||||||
|
store.upsert(makeIssue({ id: 999 }));
|
||||||
|
expect(store.issues().length).toBe(before + 1);
|
||||||
|
expect(store.getById(999)?.name).toBe('Test Issue');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates an existing issue', () => {
|
||||||
|
store.upsert(makeIssue({ id: 1, name: 'Updated Name' }));
|
||||||
|
expect(store.getById(1)?.name).toBe('Updated Name');
|
||||||
|
expect(store.issues().filter((i) => i.id === 1).length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('persists the issue list to localStorage', () => {
|
||||||
|
store.upsert(makeIssue({ id: 999 }));
|
||||||
|
const raw = localStorage.getItem('bonsai.issues');
|
||||||
|
expect(raw).not.toBeNull();
|
||||||
|
const parsed = JSON.parse(raw!);
|
||||||
|
expect(parsed.some((i: IssueEntity) => i.id === 999)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes legacy dependsOnId (single number) to dependsOnIds array when dependsOnIds is absent', () => {
|
||||||
|
// dependsOnIds must be omitted (not an array) for the legacy field to take effect
|
||||||
|
store.upsert({ id: 998, type: 'Story', name: 'Legacy', dependsOnId: 1 } as any);
|
||||||
|
expect(store.getById(998)?.dependsOnIds).toEqual([1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters non-number values from dependsOnIds', () => {
|
||||||
|
store.upsert({ ...makeIssue({ id: 997 }), dependsOnIds: [1, 'two', null] } as any);
|
||||||
|
expect(store.getById(997)?.dependsOnIds).toEqual([1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ensures comments is always an array when missing', () => {
|
||||||
|
store.upsert({ ...makeIssue({ id: 996 }), comments: undefined } as any);
|
||||||
|
expect(store.getById(996)?.comments).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets default type to Story when type is missing', () => {
|
||||||
|
store.upsert({ ...makeIssue({ id: 995 }), type: undefined } as any);
|
||||||
|
expect(store.getById(995)?.type).toBe('Story');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets estimatedTime to null when missing', () => {
|
||||||
|
store.upsert({ ...makeIssue({ id: 994 }), estimatedTime: undefined } as any);
|
||||||
|
expect(store.getById(994)?.estimatedTime).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteById', () => {
|
||||||
|
it('removes the issue from the store', () => {
|
||||||
|
store.upsert(makeIssue({ id: 999 }));
|
||||||
|
store.deleteById(999);
|
||||||
|
expect(store.getById(999)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes the deleted id from dependsOnIds of other issues', () => {
|
||||||
|
store.upsert(makeIssue({ id: 100 }));
|
||||||
|
store.upsert(makeIssue({ id: 101, dependsOnIds: [100] }));
|
||||||
|
store.deleteById(100);
|
||||||
|
expect(store.getById(101)?.dependsOnIds).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('persists the updated list to localStorage', () => {
|
||||||
|
store.upsert(makeIssue({ id: 999 }));
|
||||||
|
store.deleteById(999);
|
||||||
|
const raw = localStorage.getItem('bonsai.issues');
|
||||||
|
const parsed = JSON.parse(raw!);
|
||||||
|
expect(parsed.some((i: IssueEntity) => i.id === 999)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('localStorage persistence', () => {
|
||||||
|
it('loads issues from localStorage on construction', () => {
|
||||||
|
const saved: IssueEntity[] = [makeIssue({ id: 42, name: 'From storage' })];
|
||||||
|
localStorage.setItem('bonsai.issues', JSON.stringify(saved));
|
||||||
|
|
||||||
|
TestBed.resetTestingModule();
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
const freshStore = TestBed.inject(IssuesStore);
|
||||||
|
|
||||||
|
expect(freshStore.getById(42)?.name).toBe('From storage');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to defaults when localStorage contains invalid JSON', () => {
|
||||||
|
localStorage.setItem('bonsai.issues', 'not-valid-json');
|
||||||
|
|
||||||
|
TestBed.resetTestingModule();
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
const freshStore = TestBed.inject(IssuesStore);
|
||||||
|
|
||||||
|
expect(freshStore.issues().length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to defaults when localStorage contains a non-array', () => {
|
||||||
|
localStorage.setItem('bonsai.issues', '{"key":"value"}');
|
||||||
|
|
||||||
|
TestBed.resetTestingModule();
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
const freshStore = TestBed.inject(IssuesStore);
|
||||||
|
|
||||||
|
expect(freshStore.issues().length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { provideRouter } from '@angular/router';
|
||||||
import { Menu } from './menu';
|
import { Menu } from './menu';
|
||||||
|
|
||||||
describe('Menu', () => {
|
describe('Menu', () => {
|
||||||
@@ -9,6 +9,7 @@ describe('Menu', () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [Menu],
|
imports: [Menu],
|
||||||
|
providers: [provideRouter([])],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
fixture = TestBed.createComponent(Menu);
|
fixture = TestBed.createComponent(Menu);
|
||||||
@@ -19,4 +20,14 @@ describe('Menu', () => {
|
|||||||
it('should create', () => {
|
it('should create', () => {
|
||||||
expect(component).toBeTruthy();
|
expect(component).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should have three menu items', () => {
|
||||||
|
const items = (component as any).menuItems as { label: string; path: string }[];
|
||||||
|
expect(items.length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should contain Issues link', () => {
|
||||||
|
const items = (component as any).menuItems as { label: string; path: string }[];
|
||||||
|
expect(items.some((i) => i.path === '/issues')).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { Projects } from './projects';
|
import { Projects } from './projects';
|
||||||
|
|
||||||
describe('Projects', () => {
|
describe('Projects', () => {
|
||||||
@@ -19,4 +18,33 @@ describe('Projects', () => {
|
|||||||
it('should create', () => {
|
it('should create', () => {
|
||||||
expect(component).toBeTruthy();
|
expect(component).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should have 3 default projects', () => {
|
||||||
|
expect((component as any).projects().length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('createProject adds a new project', () => {
|
||||||
|
(component as any).createProject();
|
||||||
|
expect((component as any).projects().length).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('createProject increments the id each time', () => {
|
||||||
|
(component as any).createProject();
|
||||||
|
(component as any).createProject();
|
||||||
|
const projects = (component as any).projects();
|
||||||
|
expect(projects[3].id).toBe(4);
|
||||||
|
expect(projects[4].id).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('new project starts with Nouveau status', () => {
|
||||||
|
(component as any).createProject();
|
||||||
|
const newProject = (component as any).projects()[3];
|
||||||
|
expect(newProject.status).toBe('Nouveau');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('new project starts with 0 progress', () => {
|
||||||
|
(component as any).createProject();
|
||||||
|
const newProject = (component as any).projects()[3];
|
||||||
|
expect(newProject.progress).toBe(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user