diff --git a/cypress/e2e/requirement-set.cy.ts b/cypress/e2e/requirement-set.cy.ts new file mode 100644 index 000000000..89c4f9fd6 --- /dev/null +++ b/cypress/e2e/requirement-set.cy.ts @@ -0,0 +1,87 @@ +describe('Requirement Set Management', () => { + beforeEach(() => { + // Login and navigate to the requirement management page + cy.login('admin', 'password'); + cy.visit('/admin/requirement-sets'); + }); + + it('should list all requirement sets', () => { + cy.intercept('GET', '/api/requirementset', { + fixture: 'requirementSets.json' + }).as('getRequirementSets'); + + cy.wait('@getRequirementSets'); + cy.get('[data-testid=requirement-set-list]').should('be.visible'); + cy.get('[data-testid=requirement-set-item]').should('have.length.at.least', 1); + }); + + it('should create a new requirement set', () => { + const newRequirementSet = { + requirementSetGroupId: 1, + name: 'Test Requirement Set', + description: 'Test Description', + unitId: '1', + requirementId: 1 + }; + + cy.intercept('POST', '/api/requirementset', { + statusCode: 201, + body: { id: '123', ...newRequirementSet } + }).as('createRequirementSet'); + + cy.get('[data-testid=add-requirement-set-btn]').click(); + cy.get('[data-testid=requirement-set-name-input]').type(newRequirementSet.name); + cy.get('[data-testid=requirement-set-description-input]').type(newRequirementSet.description); + cy.get('[data-testid=requirement-set-group-select]').select(newRequirementSet.requirementSetGroupId.toString()); + cy.get('[data-testid=requirement-set-unit-select]').select(newRequirementSet.unitId); + cy.get('[data-testid=requirement-set-requirement-select]').select(newRequirementSet.requirementId.toString()); + cy.get('[data-testid=save-requirement-set-btn]').click(); + + cy.wait('@createRequirementSet'); + cy.get('[data-testid=requirement-set-item]').should('contain', newRequirementSet.name); + }); + + it('should update an existing requirement set', () => { + const updatedDescription = 'Updated Description'; + + cy.intercept('PUT', '/api/requirementset', { + statusCode: 200, + body: { description: updatedDescription } + }).as('updateRequirementSet'); + + cy.get('[data-testid=requirement-set-item]').first().click(); + cy.get('[data-testid=edit-requirement-set-btn]').click(); + cy.get('[data-testid=requirement-set-description-input]').clear().type(updatedDescription); + cy.get('[data-testid=save-requirement-set-btn]').click(); + + cy.wait('@updateRequirementSet'); + cy.get('[data-testid=requirement-set-item]').first().should('contain', updatedDescription); + }); + + it('should handle error cases gracefully', () => { + // Test server error + cy.intercept('GET', '/api/requirementset', { + statusCode: 500, + body: { error: 'Internal Server Error' } + }).as('getRequirementSetsError'); + + cy.wait('@getRequirementSetsError'); + cy.get('[data-testid=error-message]').should('be.visible'); + cy.get('[data-testid=error-message]').should('contain', 'Error loading requirement sets'); + + // Test validation error + const invalidRequirementSet = { + requirementSetGroupId: 1, + name: '', // Invalid - empty name + description: 'Test Description', + unitId: '1', + requirementId: 1 + }; + + cy.get('[data-testid=add-requirement-set-btn]').click(); + cy.get('[data-testid=requirement-set-name-input]').type(invalidRequirementSet.name); + cy.get('[data-testid=save-requirement-set-btn]').click(); + cy.get('[data-testid=validation-error]').should('be.visible'); + cy.get('[data-testid=validation-error]').should('contain', 'Name is required'); + }); +}); diff --git a/cypress/e2e/unit.cy.ts b/cypress/e2e/unit.cy.ts new file mode 100644 index 000000000..2dd9a690a --- /dev/null +++ b/cypress/e2e/unit.cy.ts @@ -0,0 +1,156 @@ +describe('Unit Management', () => { + const mockUnits = [ + { + code: 'COS10001', + id: 1, + name: 'Introduction to Programming', + my_role: 'Admin', + main_convenor_user_id: 2 + }, + { + code: 'COS20007', + id: 2, + name: 'Object Oriented Programming', + my_role: 'Admin', + main_convenor_user_id: 2 + }, + { + code: 'COS30046', + id: 3, + name: 'Artificial Intelligence for Games', + my_role: 'Admin', + main_convenor_user_id: 4 + }, + { + code: 'COS30243', + id: 4, + name: 'Game Programming', + my_role: 'Admin', + main_convenor_user_id: 4 + } + ]; + + beforeEach(() => { + // Setup and login + cy.login('admin', 'password'); + + // Intercept API calls + cy.intercept('GET', '/api/units', { + statusCode: 200, + body: mockUnits + }).as('getUnits'); + }); + + describe('Unit Listing', () => { + it('should display all units', () => { + cy.visit('/admin/units'); + cy.wait('@getUnits'); + + // Check if all units are displayed + mockUnits.forEach(unit => { + cy.get('[data-testid=unit-list]') + .should('contain', unit.code) + .and('contain', unit.name); + }); + }); + + it('should filter units by search term', () => { + cy.visit('/admin/units'); + cy.wait('@getUnits'); + + // Search for "Programming" + cy.get('[data-testid=unit-search-input]').type('Programming'); + + // Should show 3 units + cy.get('[data-testid=unit-list-item]').should('have.length', 3); + cy.get('[data-testid=unit-list]') + .should('contain', 'COS10001') + .and('contain', 'COS20007') + .and('contain', 'COS30243'); + }); + + it('should filter units by level', () => { + cy.visit('/admin/units'); + cy.wait('@getUnits'); + + // Select level 3 units + cy.get('[data-testid=unit-level-select]').select('3'); + + // Should show 2 level 3 units + cy.get('[data-testid=unit-list-item]').should('have.length', 2); + cy.get('[data-testid=unit-list]') + .should('contain', 'COS30046') + .and('contain', 'COS30243'); + }); + }); + + describe('Unit Details', () => { + it('should display unit details correctly', () => { + const testUnit = mockUnits[0]; + + // Intercept specific unit request + cy.intercept('GET', `/api/units/${testUnit.id}`, { + statusCode: 200, + body: testUnit + }).as('getUnit'); + + cy.visit(`/admin/units/${testUnit.id}`); + cy.wait('@getUnit'); + + // Check unit details + cy.get('[data-testid=unit-code]').should('contain', testUnit.code); + cy.get('[data-testid=unit-name]').should('contain', testUnit.name); + cy.get('[data-testid=unit-convenor]').should('contain', testUnit.main_convenor_user_id); + }); + }); + + describe('Unit Navigation', () => { + it('should navigate between unit pages', () => { + cy.visit('/admin/units'); + cy.wait('@getUnits'); + + // Click first unit + cy.get('[data-testid=unit-list-item]').first().click(); + + // Should be on unit details page + cy.url().should('include', '/admin/units/1'); + + // Go back to list + cy.get('[data-testid=back-to-list]').click(); + cy.url().should('include', '/admin/units'); + }); + }); + + describe('Error Handling', () => { + it('should handle API errors gracefully', () => { + // Mock API error + cy.intercept('GET', '/api/units', { + statusCode: 500, + body: { error: 'Server Error' } + }).as('getUnitsError'); + + cy.visit('/admin/units'); + cy.wait('@getUnitsError'); + + // Should show error message + cy.get('[data-testid=error-message]') + .should('be.visible') + .and('contain', 'Error loading units'); + }); + + it('should handle non-existent unit', () => { + cy.intercept('GET', '/api/units/999', { + statusCode: 404, + body: { error: 'Unit not found' } + }).as('getNonExistentUnit'); + + cy.visit('/admin/units/999'); + cy.wait('@getNonExistentUnit'); + + // Should show not found message + cy.get('[data-testid=not-found-message]') + .should('be.visible') + .and('contain', 'Unit not found'); + }); + }); +}); diff --git a/cypress/fixtures/requirementSets.json b/cypress/fixtures/requirementSets.json new file mode 100644 index 000000000..ebe3bc8bb --- /dev/null +++ b/cypress/fixtures/requirementSets.json @@ -0,0 +1,25 @@ +{ + "requirement_sets": [ + { + "id": "1", + "requirement_set_group_id": 1, + "description": "Core programming skills", + "unit_id": 1, + "requirement_id": 1 + }, + { + "id": "2", + "requirement_set_group_id": 1, + "description": "Advanced programming concepts", + "unit_id": 1, + "requirement_id": 2 + }, + { + "id": "3", + "requirement_set_group_id": 2, + "description": "Database fundamentals", + "unit_id": 2, + "requirement_id": 3 + } + ] +} diff --git a/cypress/fixtures/units.json b/cypress/fixtures/units.json new file mode 100644 index 000000000..e1e399b2a --- /dev/null +++ b/cypress/fixtures/units.json @@ -0,0 +1,32 @@ +{ + "units": [ + { + "code": "COS10001", + "id": 1, + "name": "Introduction to Programming", + "my_role": "Admin", + "main_convenor_user_id": 2 + }, + { + "code": "COS20007", + "id": 2, + "name": "Object Oriented Programming", + "my_role": "Admin", + "main_convenor_user_id": 2 + }, + { + "code": "COS30046", + "id": 3, + "name": "Artificial Intelligence for Games", + "my_role": "Admin", + "main_convenor_user_id": 4 + }, + { + "code": "COS30243", + "id": 4, + "name": "Game Programming", + "my_role": "Admin", + "main_convenor_user_id": 4 + } + ] +} diff --git a/package-lock.json b/package-lock.json index a02d52eb8..895d2a292 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "@angular/upgrade": "^17.3.6", "@ctrl/ngx-emoji-mart": "^9.2.0", "@ngneat/hotkeys": "^4.0.0", + "@rollup/rollup-linux-x64-gnu": "^4.50.1", "@uirouter/angular": "^13.0", "@uirouter/angular-hybrid": "^17.1.0", "@uirouter/angularjs": "^1.0.30", @@ -4805,6 +4806,84 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz", + "integrity": "sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz", + "integrity": "sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz", + "integrity": "sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz", + "integrity": "sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz", + "integrity": "sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.17.2.tgz", + "integrity": "sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-arm64-gnu": { "version": "4.17.2", "cpu": [ @@ -4829,6 +4908,108 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.17.2.tgz", + "integrity": "sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz", + "integrity": "sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.17.2.tgz", + "integrity": "sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.1.tgz", + "integrity": "sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==", + "cpu": [ + "x64" + ], + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz", + "integrity": "sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz", + "integrity": "sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz", + "integrity": "sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz", + "integrity": "sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@scarf/scarf": { "version": "1.3.0", "hasInstallScript": true, @@ -7115,6 +7296,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, "node_modules/bl": { "version": "4.1.0", "license": "MIT", @@ -10418,6 +10609,13 @@ "dev": true, "license": "MIT" }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, "node_modules/filelist": { "version": "1.0.4", "dev": true, @@ -10746,6 +10944,20 @@ "dev": true, "license": "ISC" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "license": "MIT", @@ -11720,6 +11932,25 @@ "node": ">=0.10.0" } }, + "node_modules/grunt-html2js/node_modules/fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "deprecated": "Upgrade to fsevents v2 to mitigate potential security issues", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + }, + "engines": { + "node": ">= 4.0" + } + }, "node_modules/grunt-html2js/node_modules/glob-parent": { "version": "3.1.0", "dev": true, @@ -15606,6 +15837,13 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nan": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz", + "integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==", + "dev": true, + "optional": true + }, "node_modules/nanoid": { "version": "3.3.7", "dev": true, @@ -19296,6 +19534,19 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz", + "integrity": "sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/run-async": { "version": "3.0.0", "license": "MIT", diff --git a/package.json b/package.json index 15405c0da..c99a245c8 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@angular/upgrade": "^17.3.6", "@ctrl/ngx-emoji-mart": "^9.2.0", "@ngneat/hotkeys": "^4.0.0", + "@rollup/rollup-linux-x64-gnu": "^4.50.1", "@uirouter/angular": "^13.0", "@uirouter/angular-hybrid": "^17.1.0", "@uirouter/angularjs": "^1.0.30", diff --git a/src/app/api/services/requirement-set.service.spec.ts b/src/app/api/services/requirement-set.service.spec.ts new file mode 100644 index 000000000..27cbd98ab --- /dev/null +++ b/src/app/api/services/requirement-set.service.spec.ts @@ -0,0 +1,136 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { RequirementSet } from './requirement-set.service'; +import API_URL from 'src/app/config/constants/apiURL'; + +describe('RequirementSetService', () => { + let service: RequirementSet; + let httpMock: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [RequirementSet] + }); + + service = TestBed.inject(RequirementSet); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('getRequirementSets', () => { + it('should retrieve all requirement sets', () => { + const mockRequirementSets = [ + { + id: '1', + requirementSetGroupId: 1, + description: 'Core programming skills', + unitId: 1, + requirementId: 1 + }, + { + id: '2', + requirementSetGroupId: 1, + description: 'Advanced programming concepts', + unitId: 1, + requirementId: 2 + } + ]; + + service.getRequirementSets().subscribe(response => { + expect(response).toEqual(mockRequirementSets); + }); + + const req = httpMock.expectOne(`${API_URL}/requirementset`); + expect(req.request.method).toBe('GET'); + req.flush(mockRequirementSets); + }); + }); + + describe('getRequirementSetById', () => { + it('should retrieve a specific requirement set by id', () => { + const mockRequirementSet = { + id: '1', + requirementSetGroupId: 1, + description: 'Core programming skills', + unitId: 1, + requirementId: 1 + }; + + service.getRequirementSetById().subscribe(response => { + expect(response).toEqual(mockRequirementSet); + }); + + const req = httpMock.expectOne(`${API_URL}/requirementset/:id:`); + expect(req.request.method).toBe('GET'); + req.flush(mockRequirementSet); + }); + }); + + describe('addNewRequirementSet', () => { + it('should create a new requirement set', () => { + const mockNewRequirementSet = { + requirementSetGroupId: 1, + name: 'Database Skills', + description: 'Essential database management skills', + unitId: '1', + requirementId: 3 + }; + + const mockResponse = { + id: '3', + ...mockNewRequirementSet + }; + + service.addNewRequirementSet( + mockNewRequirementSet.requirementSetGroupId, + mockNewRequirementSet.name, + mockNewRequirementSet.description, + mockNewRequirementSet.unitId, + mockNewRequirementSet.requirementId + ).subscribe(response => { + expect(response).toEqual(mockResponse); + }); + + const req = httpMock.expectOne(`${API_URL}/requirementset`); + expect(req.request.method).toBe('POST'); + req.flush(mockResponse); + }); + }); + + describe('updateRequirementSet', () => { + it('should update an existing requirement set', () => { + const mockUpdateData = { + requirementSetGroupId: 1, + name: 'Updated Skills', + description: 'Updated description', + code: 'CODE123' + }; + + const mockResponse = { + id: '1', + ...mockUpdateData + }; + + service.updateRequirementSet( + mockUpdateData.requirementSetGroupId, + mockUpdateData.name, + mockUpdateData.description, + mockUpdateData.code + ).subscribe(response => { + expect(response).toEqual(mockResponse); + }); + + const req = httpMock.expectOne(`${API_URL}/requirementset`); + expect(req.request.method).toBe('PUT'); + req.flush(mockResponse); + }); + }); +}); diff --git a/src/app/api/services/unit.service.spec.ts b/src/app/api/services/unit.service.spec.ts new file mode 100644 index 000000000..5a9e5db12 --- /dev/null +++ b/src/app/api/services/unit.service.spec.ts @@ -0,0 +1,145 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { UnitService } from './unit.service'; +import { Unit } from '../models/unit'; +import API_URL from 'src/app/config/constants/apiURL'; + +describe('UnitService', () => { + let service: UnitService; + let httpMock: HttpTestingController; + + const mockUnits = [ + { + code: 'COS10001', + id: 1, + name: 'Introduction to Programming', + my_role: 'Admin', + main_convenor_user_id: 2 + }, + { + code: 'COS20007', + id: 2, + name: 'Object Oriented Programming', + my_role: 'Admin', + main_convenor_user_id: 2 + }, + { + code: 'COS30046', + id: 3, + name: 'Artificial Intelligence for Games', + my_role: 'Admin', + main_convenor_user_id: 4 + }, + { + code: 'COS30243', + id: 4, + name: 'Game Programming', + my_role: 'Admin', + main_convenor_user_id: 4 + } + ]; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [UnitService] + }); + + service = TestBed.inject(UnitService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('getUnits', () => { + it('should retrieve all units', () => { + service.query().subscribe(units => { + expect(units.length).toBe(4); + expect(units).toEqual(mockUnits); + }); + + const req = httpMock.expectOne(`${API_URL}/units`); + expect(req.request.method).toBe('GET'); + req.flush(mockUnits); + }); + + it('should handle empty response', () => { + service.query().subscribe(units => { + expect(units.length).toBe(0); + }); + + const req = httpMock.expectOne(`${API_URL}/units`); + expect(req.request.method).toBe('GET'); + req.flush([]); + }); + }); + + describe('getUnit', () => { + it('should retrieve a specific unit by id', () => { + const testUnit = mockUnits[0]; + + service.get(testUnit.id).subscribe(unit => { + expect(unit).toEqual(testUnit); + }); + + const req = httpMock.expectOne(`${API_URL}/units/${testUnit.id}`); + expect(req.request.method).toBe('GET'); + req.flush(testUnit); + }); + + it('should handle unit not found', () => { + const nonExistentId = 999; + + service.get(nonExistentId).subscribe({ + error: (error) => { + expect(error.status).toBe(404); + } + }); + + const req = httpMock.expectOne(`${API_URL}/units/${nonExistentId}`); + expect(req.request.method).toBe('GET'); + req.error(new ErrorEvent('404'), { status: 404 }); + }); + }); + + describe('Unit filtering', () => { + it('should filter programming units', () => { + const programmingUnits = mockUnits.filter(unit => + unit.name.toLowerCase().includes('programming') + ); + + expect(programmingUnits.length).toBe(3); + expect(programmingUnits.map(u => u.code)).toContain('COS10001'); + expect(programmingUnits.map(u => u.code)).toContain('COS20007'); + expect(programmingUnits.map(u => u.code)).toContain('COS30243'); + }); + + it('should filter by course level', () => { + const level3Units = mockUnits.filter(unit => + unit.code.startsWith('COS3') + ); + + expect(level3Units.length).toBe(2); + expect(level3Units.map(u => u.code)).toContain('COS30046'); + expect(level3Units.map(u => u.code)).toContain('COS30243'); + }); + }); + + describe('Unit convenor checks', () => { + it('should identify units by convenor', () => { + const convenor2Units = mockUnits.filter(unit => + unit.main_convenor_user_id === 2 + ); + + expect(convenor2Units.length).toBe(2); + expect(convenor2Units.map(u => u.code)).toContain('COS10001'); + expect(convenor2Units.map(u => u.code)).toContain('COS20007'); + }); + }); +}); diff --git a/src/app/courseflow/common/degree-progress/degree-progress.component.html b/src/app/courseflow/common/degree-progress/degree-progress.component.html new file mode 100644 index 000000000..f609dadc5 --- /dev/null +++ b/src/app/courseflow/common/degree-progress/degree-progress.component.html @@ -0,0 +1,59 @@ +