diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingExerciseRepository.java b/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingExerciseRepository.java index ed5e5e710341..7d67f91ed418 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingExerciseRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingExerciseRepository.java @@ -788,14 +788,17 @@ default ProgrammingExercise saveForCreation(ProgrammingExercise exercise) { * @throws EntityNotFoundException the programming exercise could not be found. */ @NotNull - default ProgrammingExercise findByIdWithTemplateAndSolutionParticipationLatestResultFeedbackTestCasesElseThrow(long programmingExerciseId) throws EntityNotFoundException { + default ProgrammingExercise findByIdWithTemplateAndSolutionParticipationAndAuxiliaryReposAndLatestResultFeedbackTestCasesElseThrow(long programmingExerciseId) + throws EntityNotFoundException { // TODO: This is a dark hack. Move this into a service where we properly load only the solution participation in the second step ProgrammingExercise programmingExerciseWithTemplate = getValueElseThrow(findWithTemplateParticipationLatestResultFeedbackTestCasesById(programmingExerciseId), programmingExerciseId); ProgrammingExercise programmingExerciseWithSolution = getValueElseThrow(findWithSolutionParticipationLatestResultFeedbackTestCasesById(programmingExerciseId), programmingExerciseId); + ProgrammingExercise programmingExerciseWithAuxiliaryRepositories = findByIdWithAuxiliaryRepositoriesElseThrow(programmingExerciseId); programmingExerciseWithTemplate.setSolutionParticipation(programmingExerciseWithSolution.getSolutionParticipation()); + programmingExerciseWithTemplate.setAuxiliaryRepositories(programmingExerciseWithAuxiliaryRepositories.getAuxiliaryRepositories()); return programmingExerciseWithTemplate; } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseResource.java index 9ef7a05508e9..0a39cabc4e06 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/ProgrammingExerciseResource.java @@ -505,7 +505,7 @@ public ResponseEntity getProgrammingExercise(@PathVariable public ResponseEntity getProgrammingExerciseWithSetupParticipations(@PathVariable long exerciseId) { log.debug("REST request to get ProgrammingExercise with setup participations : {}", exerciseId); User user = userRepository.getUserWithGroupsAndAuthorities(); - var programmingExercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationLatestResultFeedbackTestCasesElseThrow(exerciseId); + var programmingExercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationAndAuxiliaryReposAndLatestResultFeedbackTestCasesElseThrow(exerciseId); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.EDITOR, programmingExercise, user); var assignmentParticipation = studentParticipationRepository.findByExerciseIdAndStudentIdAndTestRunWithLatestResult(programmingExercise.getId(), user.getId(), false); Set participations = new HashSet<>(); diff --git a/src/main/webapp/app/exercises/programming/manage/code-editor/code-editor-instructor-and-editor-container.component.html b/src/main/webapp/app/exercises/programming/manage/code-editor/code-editor-instructor-and-editor-container.component.html index 473ffcef7626..30ee80b5e22f 100644 --- a/src/main/webapp/app/exercises/programming/manage/code-editor/code-editor-instructor-and-editor-container.component.html +++ b/src/main/webapp/app/exercises/programming/manage/code-editor/code-editor-instructor-and-editor-container.component.html @@ -37,7 +37,9 @@ />
- +
+ @for (auxiliaryRepository of exercise.auxiliaryRepositories; track exercise.auxiliaryRepositories; let i = $index) { + @if (auxiliaryRepository.id) { + + } + }
diff --git a/src/main/webapp/app/exercises/programming/manage/code-editor/code-editor-instructor-and-editor-container.component.ts b/src/main/webapp/app/exercises/programming/manage/code-editor/code-editor-instructor-and-editor-container.component.ts index 435accfdab30..f7fd38dc5515 100644 --- a/src/main/webapp/app/exercises/programming/manage/code-editor/code-editor-instructor-and-editor-container.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/code-editor/code-editor-instructor-and-editor-container.component.ts @@ -17,6 +17,7 @@ import { IrisSettings } from 'app/entities/iris/settings/iris-settings.model'; @Component({ selector: 'jhi-code-editor-instructor', templateUrl: './code-editor-instructor-and-editor-container.component.html', + styleUrl: 'code-editor-instructor-and-editor-container.scss', }) export class CodeEditorInstructorAndEditorContainerComponent extends CodeEditorInstructorBaseContainerComponent { @ViewChild(UpdatingResultComponent, { static: false }) resultComp: UpdatingResultComponent; diff --git a/src/main/webapp/app/exercises/programming/manage/code-editor/code-editor-instructor-and-editor-container.scss b/src/main/webapp/app/exercises/programming/manage/code-editor/code-editor-instructor-and-editor-container.scss new file mode 100644 index 000000000000..d62ab3099024 --- /dev/null +++ b/src/main/webapp/app/exercises/programming/manage/code-editor/code-editor-instructor-and-editor-container.scss @@ -0,0 +1,3 @@ +.btn-outline-primary:not(.list-group-item).dropdown-toggle.show { + color: var(--default-btn-font); +} diff --git a/src/main/webapp/app/exercises/programming/manage/code-editor/code-editor-instructor-base-container.component.ts b/src/main/webapp/app/exercises/programming/manage/code-editor/code-editor-instructor-base-container.component.ts index e53bf800e1a9..0b6e1efd5d89 100644 --- a/src/main/webapp/app/exercises/programming/manage/code-editor/code-editor-instructor-base-container.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/code-editor/code-editor-instructor-base-container.component.ts @@ -28,6 +28,7 @@ export enum REPOSITORY { TEMPLATE = 'TEMPLATE', SOLUTION = 'SOLUTION', TEST = 'TEST', + AUXILIARY = 'AUXILIARY', } /** @@ -64,6 +65,8 @@ export abstract class CodeEditorInstructorBaseContainerComponent implements OnIn // Stores which repository is selected atm. // Needs to be set additionally to selectedParticipation as the test repository does not have a participation selectedRepository: REPOSITORY; + selectedRepositoryId: number; + selectedAuxiliaryRepositoryName?: string; // Fires when the selected domain changes. // This can either be a participation (solution, template, assignment) or the test repository. @@ -95,6 +98,7 @@ export abstract class CodeEditorInstructorBaseContainerComponent implements OnIn this.paramSub = this.route!.params.subscribe((params) => { const exerciseId = Number(params['exerciseId']); const participationId = Number(params['participationId']); + const repositoryId = Number(params['repositoryId']); this.loadingState = LOADING_STATE.INITIALIZING; this.loadExercise(exerciseId) .pipe( @@ -107,6 +111,12 @@ export abstract class CodeEditorInstructorBaseContainerComponent implements OnIn tap(() => { if (this.router.url.endsWith('/test')) { this.saveChangesAndSelectDomain([DomainType.TEST_REPOSITORY, this.exercise]); + } else if (this.router.url.indexOf('auxiliary') >= 0) { + const auxiliaryRepo = this.exercise.auxiliaryRepositories?.find((repo) => repo.id === repositoryId); + if (auxiliaryRepo) { + this.selectedAuxiliaryRepositoryName = auxiliaryRepo.name; + this.saveChangesAndSelectDomain([DomainType.AUXILIARY_REPOSITORY, auxiliaryRepo]); + } } else { const nextAvailableParticipation = this.getNextAvailableParticipation(participationId); if (nextAvailableParticipation) { @@ -190,7 +200,10 @@ export abstract class CodeEditorInstructorBaseContainerComponent implements OnIn if (this.codeEditorContainer != undefined) { this.codeEditorContainer.initializeProperties(); } - if (domainType === DomainType.PARTICIPATION) { + if (domainType === DomainType.AUXILIARY_REPOSITORY) { + this.selectedRepository = REPOSITORY.AUXILIARY; + this.selectedRepositoryId = domainValue.id; + } else if (domainType === DomainType.PARTICIPATION) { this.setSelectedParticipation(domainValue.id); } else { this.selectedParticipation = this.exercise.templateParticipation!; @@ -272,28 +285,42 @@ export abstract class CodeEditorInstructorBaseContainerComponent implements OnIn * Select the template participation repository and navigate to it */ selectTemplateParticipation() { - this.router.navigate(['..', this.exercise.templateParticipation!.id], { relativeTo: this.route }); + this.router.navigate([this.up(), this.exercise.templateParticipation!.id], { relativeTo: this.route }); } /** * Select the solution participation repository and navigate to it */ selectSolutionParticipation() { - this.router.navigate(['..', this.exercise.solutionParticipation!.id], { relativeTo: this.route }); + this.router.navigate([this.up(), this.exercise.solutionParticipation!.id], { relativeTo: this.route }); } /** * Select the assignment participation repository and navigate to it */ selectAssignmentParticipation() { - this.router.navigate(['..', this.exercise.studentParticipations![0].id], { relativeTo: this.route }); + this.router.navigate([this.up(), this.exercise.studentParticipations![0].id], { relativeTo: this.route }); } /** * Select the test repository and navigate to it */ selectTestRepository() { - this.router.navigate(['..', 'test'], { relativeTo: this.route }); + this.router.navigate([this.up(), 'test'], { relativeTo: this.route }); + } + + /** + * Select the auxiliary repository and navigate to it + */ + selectAuxiliaryRepository(repositoryId: number) { + this.router.navigate([this.up(), 'auxiliary', repositoryId], { relativeTo: this.route }); + } + + /** + * Go two folders up if the current view is an auxiliary repository, and one otherwise + */ + up() { + return this.selectedRepository === REPOSITORY.AUXILIARY ? '../..' : '..'; } /** diff --git a/src/main/webapp/app/exercises/programming/manage/code-editor/code-editor-management-routing.module.ts b/src/main/webapp/app/exercises/programming/manage/code-editor/code-editor-management-routing.module.ts index 26f1f6e85ba3..0307f567a816 100644 --- a/src/main/webapp/app/exercises/programming/manage/code-editor/code-editor-management-routing.module.ts +++ b/src/main/webapp/app/exercises/programming/manage/code-editor/code-editor-management-routing.module.ts @@ -54,6 +54,18 @@ const routes: Routes = [ }, canActivate: [UserRouteAccessService], }, + { + path: 'auxiliary/:repositoryId', + component: CodeEditorInstructorAndEditorContainerComponent, + data: { + authorities: [Authority.EDITOR, Authority.INSTRUCTOR, Authority.ADMIN], + pageTitle: 'artemisApp.editor.home.title', + flushRepositoryCacheAfter: 900000, // 15 min + participationCache: {}, + repositoryCache: {}, + }, + canActivate: [UserRouteAccessService], + }, ]; @NgModule({ diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseGitIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseGitIntegrationTest.java index 5882a3517915..e743904e72f6 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseGitIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseGitIntegrationTest.java @@ -113,8 +113,8 @@ void testRepositoryMethods() { assertThatExceptionOfType(EntityNotFoundException.class) .isThrownBy(() -> programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationElseThrow(Long.MAX_VALUE)); - assertThatExceptionOfType(EntityNotFoundException.class) - .isThrownBy(() -> programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationLatestResultFeedbackTestCasesElseThrow(Long.MAX_VALUE)); + assertThatExceptionOfType(EntityNotFoundException.class).isThrownBy( + () -> programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationAndAuxiliaryReposAndLatestResultFeedbackTestCasesElseThrow(Long.MAX_VALUE)); assertThatExceptionOfType(EntityNotFoundException.class) .isThrownBy(() -> programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesElseThrow(Long.MAX_VALUE)); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseGradingServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseGradingServiceTest.java index 2993b02caccb..6b4c43477276 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseGradingServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/ProgrammingExerciseGradingServiceTest.java @@ -551,7 +551,8 @@ void shouldReEvaluateScoreOfTheCorrectResults() throws Exception { programmingExercise = (ProgrammingExercise) exerciseUtilService.addMaxScoreAndBonusPointsToExercise(programmingExercise); programmingExercise = programmingExerciseUtilService.addTemplateParticipationForProgrammingExercise(programmingExercise); programmingExercise = programmingExerciseUtilService.addSolutionParticipationForProgrammingExercise(programmingExercise); - programmingExercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationLatestResultFeedbackTestCasesElseThrow(programmingExercise.getId()); + programmingExercise = programmingExerciseRepository + .findByIdWithTemplateAndSolutionParticipationAndAuxiliaryReposAndLatestResultFeedbackTestCasesElseThrow(programmingExercise.getId()); var testCases = createTestCases(false); var testParticipations = createTestParticipations(); @@ -566,7 +567,8 @@ void shouldReEvaluateScoreOfTheCorrectResults() throws Exception { SecurityContextHolder.setContext(TestSecurityContextHolder.getContext()); // Tests - programmingExercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationLatestResultFeedbackTestCasesElseThrow(programmingExercise.getId()); + programmingExercise = programmingExerciseRepository + .findByIdWithTemplateAndSolutionParticipationAndAuxiliaryReposAndLatestResultFeedbackTestCasesElseThrow(programmingExercise.getId()); // template 0 % { @@ -610,7 +612,8 @@ void shouldNotIncludeTestsMarkedAsNeverVisibleInScoreCalculation(boolean isAfter programmingExercise = (ProgrammingExercise) exerciseUtilService.addMaxScoreAndBonusPointsToExercise(programmingExercise); programmingExercise = programmingExerciseUtilService.addTemplateParticipationForProgrammingExercise(programmingExercise); programmingExercise = programmingExerciseUtilService.addSolutionParticipationForProgrammingExercise(programmingExercise); - programmingExercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationLatestResultFeedbackTestCasesElseThrow(programmingExercise.getId()); + programmingExercise = programmingExerciseRepository + .findByIdWithTemplateAndSolutionParticipationAndAuxiliaryReposAndLatestResultFeedbackTestCasesElseThrow(programmingExercise.getId()); final var testCases = createTestCases(true); final var testParticipations = createTestParticipations(); @@ -625,7 +628,8 @@ void shouldNotIncludeTestsMarkedAsNeverVisibleInScoreCalculation(boolean isAfter SecurityContextHolder.setContext(TestSecurityContextHolder.getContext()); // Tests - programmingExercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationLatestResultFeedbackTestCasesElseThrow(programmingExercise.getId()); + programmingExercise = programmingExerciseRepository + .findByIdWithTemplateAndSolutionParticipationAndAuxiliaryReposAndLatestResultFeedbackTestCasesElseThrow(programmingExercise.getId()); // the invisible test case should however be visible for the template and solution repos @@ -658,7 +662,8 @@ void shouldUpdateTheLatestResultOfASingleParticipation() { programmingExercise = (ProgrammingExercise) exerciseUtilService.addMaxScoreAndBonusPointsToExercise(programmingExercise); programmingExercise = programmingExerciseUtilService.addTemplateParticipationForProgrammingExercise(programmingExercise); programmingExercise = programmingExerciseUtilService.addSolutionParticipationForProgrammingExercise(programmingExercise); - programmingExercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationLatestResultFeedbackTestCasesElseThrow(programmingExercise.getId()); + programmingExercise = programmingExerciseRepository + .findByIdWithTemplateAndSolutionParticipationAndAuxiliaryReposAndLatestResultFeedbackTestCasesElseThrow(programmingExercise.getId()); final var testCases = createTestCases(false); final var testParticipations = createTestParticipations(); @@ -679,7 +684,8 @@ void shouldUpdateOnlyResultsForParticipationsWithoutIndividualDueDate() { programmingExercise = (ProgrammingExercise) exerciseUtilService.addMaxScoreAndBonusPointsToExercise(programmingExercise); programmingExercise = programmingExerciseUtilService.addTemplateParticipationForProgrammingExercise(programmingExercise); programmingExercise = programmingExerciseUtilService.addSolutionParticipationForProgrammingExercise(programmingExercise); - programmingExercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationLatestResultFeedbackTestCasesElseThrow(programmingExercise.getId()); + programmingExercise = programmingExerciseRepository + .findByIdWithTemplateAndSolutionParticipationAndAuxiliaryReposAndLatestResultFeedbackTestCasesElseThrow(programmingExercise.getId()); final var testCases = createTestCases(false); final var testParticipations = createTestParticipations(); @@ -690,7 +696,8 @@ void shouldUpdateOnlyResultsForParticipationsWithoutIndividualDueDate() { participationWithIndividualDueDate = studentParticipationRepository.save((StudentParticipation) participationWithIndividualDueDate); final Long participationWithIndividualDueDateId = participationWithIndividualDueDate.getId(); - programmingExercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationLatestResultFeedbackTestCasesElseThrow(programmingExercise.getId()); + programmingExercise = programmingExerciseRepository + .findByIdWithTemplateAndSolutionParticipationAndAuxiliaryReposAndLatestResultFeedbackTestCasesElseThrow(programmingExercise.getId()); final var updated = programmingExerciseGradingService.updateResultsOnlyRegularDueDateParticipations(programmingExercise); // four student results + template + solution @@ -706,7 +713,8 @@ void testWeightSumZero() { programmingExercise = (ProgrammingExercise) exerciseUtilService.addMaxScoreAndBonusPointsToExercise(programmingExercise); programmingExercise = programmingExerciseUtilService.addTemplateParticipationForProgrammingExercise(programmingExercise); programmingExercise = programmingExerciseUtilService.addSolutionParticipationForProgrammingExercise(programmingExercise); - programmingExercise = programmingExerciseRepository.findByIdWithTemplateAndSolutionParticipationLatestResultFeedbackTestCasesElseThrow(programmingExercise.getId()); + programmingExercise = programmingExerciseRepository + .findByIdWithTemplateAndSolutionParticipationAndAuxiliaryReposAndLatestResultFeedbackTestCasesElseThrow(programmingExercise.getId()); final var testCases = createTestCases(false); createTestParticipations(); diff --git a/src/test/javascript/spec/integration/code-editor/code-editor-instructor.integration.spec.ts b/src/test/javascript/spec/integration/code-editor/code-editor-instructor.integration.spec.ts index c3cb01c72352..a8ba374dd6b6 100644 --- a/src/test/javascript/spec/integration/code-editor/code-editor-instructor.integration.spec.ts +++ b/src/test/javascript/spec/integration/code-editor/code-editor-instructor.integration.spec.ts @@ -66,9 +66,10 @@ import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; import { CodeEditorMonacoComponent } from 'app/exercises/programming/shared/code-editor/monaco/code-editor-monaco.component'; import { MarkdownEditorMonacoComponent } from 'app/shared/markdown-editor/monaco/markdown-editor-monaco.component'; import { mockCodeEditorMonacoViewChildren } from '../../helpers/mocks/mock-instance.helper'; +import { REPOSITORY } from 'app/exercises/programming/manage/code-editor/code-editor-instructor-base-container.component'; describe('CodeEditorInstructorIntegration', () => { - let container: CodeEditorInstructorAndEditorContainerComponent; + let comp: CodeEditorInstructorAndEditorContainerComponent; let containerFixture: ComponentFixture; let containerDebugElement: DebugElement; let domainService: DomainService; @@ -81,6 +82,7 @@ describe('CodeEditorInstructorIntegration', () => { let getBuildLogsStub: jest.SpyInstance; let findWithParticipationsStub: jest.SpyInstance; let getLatestResultWithFeedbacksStub: jest.SpyInstance; + let navigateSpy: jest.SpyInstance; let checkIfRepositoryIsCleanSubject: Subject<{ isClean: boolean }>; let getRepositoryContentSubject: Subject<{ [fileName: string]: FileType }>; @@ -142,7 +144,7 @@ describe('CodeEditorInstructorIntegration', () => { .compileComponents() .then(() => { containerFixture = TestBed.createComponent(CodeEditorInstructorAndEditorContainerComponent); - container = containerFixture.componentInstance; + comp = containerFixture.componentInstance; containerDebugElement = containerFixture.debugElement; const codeEditorRepositoryService = containerDebugElement.injector.get(CodeEditorRepositoryService); @@ -172,6 +174,7 @@ describe('CodeEditorInstructorIntegration', () => { .spyOn(programmingExerciseParticipationService, 'getLatestResultWithFeedback') .mockReturnValue(throwError(() => new Error('no result'))); getBuildLogsStub = jest.spyOn(buildLogService, 'getBuildLogs'); + navigateSpy = jest.spyOn(TestBed.inject(Router), 'navigate'); findWithParticipationsStub = jest.spyOn(programmingExerciseService, 'findWithTemplateAndSolutionParticipationAndResults'); findWithParticipationsStub.mockReturnValue(findWithParticipationsSubject); @@ -203,12 +206,12 @@ describe('CodeEditorInstructorIntegration', () => { }); const initContainer = (exercise: ProgrammingExercise) => { - container.ngOnInit(); + comp.ngOnInit(); routeSubject.next({ exerciseId: 1 }); - expect(container.codeEditorContainer).toBeUndefined(); // Have to use this as it's a component + expect(comp.codeEditorContainer).toBeUndefined(); // Have to use this as it's a component expect(findWithParticipationsStub).toHaveBeenCalledOnce(); expect(findWithParticipationsStub).toHaveBeenCalledWith(exercise.id); - expect(container.loadingState).toBe(container.LOADING_STATE.INITIALIZING); + expect(comp.loadingState).toBe(comp.LOADING_STATE.INITIALIZING); }; it('should load the exercise and select the template participation if no participation id is provided', () => { @@ -232,7 +235,7 @@ describe('CodeEditorInstructorIntegration', () => { getFeedbackDetailsForResultStub.mockReturnValue(of([])); const setDomainSpy = jest.spyOn(domainService, 'setDomain'); // @ts-ignore - (container.router as MockRouter).setUrl('code-editor-instructor/1'); + (comp.router as MockRouter).setUrl('code-editor-instructor/1'); initContainer(exercise); findWithParticipationsSubject.next({ body: exercise }); @@ -240,14 +243,14 @@ describe('CodeEditorInstructorIntegration', () => { expect(getLatestResultWithFeedbacksStub).not.toHaveBeenCalled(); expect(setDomainSpy).toHaveBeenCalledOnce(); expect(setDomainSpy).toHaveBeenCalledWith([DomainType.PARTICIPATION, exercise.templateParticipation]); - expect(container.exercise).toEqual(exercise); - expect(container.selectedRepository).toBe(container.REPOSITORY.TEMPLATE); - expect(container.selectedParticipation).toEqual(container.selectedParticipation); - expect(container.loadingState).toBe(container.LOADING_STATE.CLEAR); - expect(container.domainChangeSubscription).toBeDefined(); // External complex object + expect(comp.exercise).toEqual(exercise); + expect(comp.selectedRepository).toBe(comp.REPOSITORY.TEMPLATE); + expect(comp.selectedParticipation).toEqual(comp.selectedParticipation); + expect(comp.loadingState).toBe(comp.LOADING_STATE.CLEAR); + expect(comp.domainChangeSubscription).toBeDefined(); // External complex object containerFixture.detectChanges(); - expect(container.codeEditorContainer.grid).toBeDefined(); // Have to use this as it's a component + expect(comp.codeEditorContainer.grid).toBeDefined(); // Have to use this as it's a component checkIfRepositoryIsCleanSubject.next({ isClean: true }); getRepositoryContentSubject.next({ file: FileType.FILE, folder: FileType.FOLDER }); @@ -258,13 +261,13 @@ describe('CodeEditorInstructorIntegration', () => { // Once called by each build-output & instructions expect(getFeedbackDetailsForResultStub).toHaveBeenCalledTimes(2); - expect(container.codeEditorContainer.grid).toBeDefined(); // Have to use this as it's a component - expect(container.codeEditorContainer.fileBrowser).toBeDefined(); // Have to use this as it's a component - expect(container.codeEditorContainer.actions).toBeDefined(); // Have to use this as it's a component - expect(container.editableInstructions).toBeDefined(); // Have to use this as it's a component - expect(container.editableInstructions.participation).toEqual(exercise.templateParticipation); - expect(container.resultComp).toBeDefined(); // Have to use this as it's a component - expect(container.codeEditorContainer.buildOutput).toBeDefined(); // Have to use this as it's a component + expect(comp.codeEditorContainer.grid).toBeDefined(); // Have to use this as it's a component + expect(comp.codeEditorContainer.fileBrowser).toBeDefined(); // Have to use this as it's a component + expect(comp.codeEditorContainer.actions).toBeDefined(); // Have to use this as it's a component + expect(comp.editableInstructions).toBeDefined(); // Have to use this as it's a component + expect(comp.editableInstructions.participation).toEqual(exercise.templateParticipation); + expect(comp.resultComp).toBeDefined(); // Have to use this as it's a component + expect(comp.codeEditorContainer.buildOutput).toBeDefined(); // Have to use this as it's a component // Called once by each build-output, instructions, result and twice by instructor-exercise-status (=templateParticipation,solutionParticipation) & expect(subscribeForLatestResultOfParticipationStub).toHaveBeenCalledTimes(5); @@ -278,11 +281,11 @@ describe('CodeEditorInstructorIntegration', () => { findWithParticipationsSubject.error('fatal error'); expect(setDomainSpy).not.toHaveBeenCalled(); - expect(container.loadingState).toBe(container.LOADING_STATE.FETCHING_FAILED); - expect(container.selectedRepository).toBeUndefined(); + expect(comp.loadingState).toBe(comp.LOADING_STATE.FETCHING_FAILED); + expect(comp.selectedRepository).toBeUndefined(); containerFixture.detectChanges(); - expect(container.codeEditorContainer).toBeUndefined(); + expect(comp.codeEditorContainer).toBeUndefined(); }); it('should load test repository if specified in url', () => { @@ -296,37 +299,37 @@ describe('CodeEditorInstructorIntegration', () => { } as ProgrammingExercise; const setDomainSpy = jest.spyOn(domainService, 'setDomain'); // @ts-ignore - (container.router as MockRouter).setUrl('code-editor-instructor/1/test'); - container.ngOnDestroy(); + (comp.router as MockRouter).setUrl('code-editor-instructor/1/test'); + comp.ngOnDestroy(); initContainer(exercise); findWithParticipationsSubject.next({ body: exercise }); expect(setDomainSpy).toHaveBeenCalledOnce(); expect(setDomainSpy).toHaveBeenCalledWith([DomainType.TEST_REPOSITORY, exercise]); - expect(container.selectedParticipation).toEqual(exercise.templateParticipation); - expect(container.selectedRepository).toBe(container.REPOSITORY.TEST); + expect(comp.selectedParticipation).toEqual(exercise.templateParticipation); + expect(comp.selectedRepository).toBe(comp.REPOSITORY.TEST); expect(getBuildLogsStub).not.toHaveBeenCalled(); expect(getFeedbackDetailsForResultStub).not.toHaveBeenCalled(); containerFixture.detectChanges(); - expect(container.codeEditorContainer).toBeDefined(); // Have to use this as it's a component - expect(container.editableInstructions).toBeDefined(); // Have to use this as it's a component - expect(container.editableInstructions.participation).toEqual(exercise.templateParticipation); - expect(container.resultComp).toBeUndefined(); - expect(container.codeEditorContainer.buildOutput).toBeUndefined(); + expect(comp.codeEditorContainer).toBeDefined(); // Have to use this as it's a component + expect(comp.editableInstructions).toBeDefined(); // Have to use this as it's a component + expect(comp.editableInstructions.participation).toEqual(exercise.templateParticipation); + expect(comp.resultComp).toBeUndefined(); + expect(comp.codeEditorContainer.buildOutput).toBeUndefined(); }); const checkSolutionRepository = (exercise: ProgrammingExercise) => { - expect(container.selectedRepository).toBe(container.REPOSITORY.SOLUTION); - expect(container.selectedParticipation).toEqual(exercise.solutionParticipation); - expect(container.codeEditorContainer).toBeDefined(); // Have to use this as it's a component - expect(container.editableInstructions).toBeDefined(); // Have to use this as it's a component - expect(container.resultComp).toBeDefined(); // Have to use this as it's a component - expect(container.codeEditorContainer.buildOutput).toBeDefined(); // Have to use this as it's a component - expect(container.codeEditorContainer.buildOutput.participation).toEqual(exercise.solutionParticipation); - expect(container.editableInstructions.participation).toEqual(exercise.solutionParticipation); + expect(comp.selectedRepository).toBe(comp.REPOSITORY.SOLUTION); + expect(comp.selectedParticipation).toEqual(exercise.solutionParticipation); + expect(comp.codeEditorContainer).toBeDefined(); // Have to use this as it's a component + expect(comp.editableInstructions).toBeDefined(); // Have to use this as it's a component + expect(comp.resultComp).toBeDefined(); // Have to use this as it's a component + expect(comp.codeEditorContainer.buildOutput).toBeDefined(); // Have to use this as it's a component + expect(comp.codeEditorContainer.buildOutput.participation).toEqual(exercise.solutionParticipation); + expect(comp.editableInstructions.participation).toEqual(exercise.solutionParticipation); }; it('should be able to switch between the repos and update the child components accordingly', () => { @@ -345,25 +348,25 @@ describe('CodeEditorInstructorIntegration', () => { // Start with assignment repository // @ts-ignore - (container.router as MockRouter).setUrl('code-editor-instructor/1/2'); - container.ngOnInit(); + (comp.router as MockRouter).setUrl('code-editor-instructor/1/2'); + comp.ngOnInit(); routeSubject.next({ exerciseId: 1, participationId: 2 }); findWithParticipationsSubject.next({ body: exercise }); containerFixture.detectChanges(); - expect(container.selectedRepository).toBe(container.REPOSITORY.ASSIGNMENT); - expect(container.selectedParticipation).toEqual(exercise.studentParticipations[0]); - expect(container.codeEditorContainer).toBeDefined(); // Have to use this as it's a component - expect(container.editableInstructions).toBeDefined(); // Have to use this as it's a component - expect(container.resultComp).toBeDefined(); // Have to use this as it's a component - expect(container.codeEditorContainer.buildOutput).toBeDefined(); // Have to use this as it's a component - expect(container.codeEditorContainer.buildOutput.participation).toEqual(exercise.studentParticipations[0]); - expect(container.editableInstructions.participation).toEqual(exercise.studentParticipations[0]); + expect(comp.selectedRepository).toBe(comp.REPOSITORY.ASSIGNMENT); + expect(comp.selectedParticipation).toEqual(exercise.studentParticipations[0]); + expect(comp.codeEditorContainer).toBeDefined(); // Have to use this as it's a component + expect(comp.editableInstructions).toBeDefined(); // Have to use this as it's a component + expect(comp.resultComp).toBeDefined(); // Have to use this as it's a component + expect(comp.codeEditorContainer.buildOutput).toBeDefined(); // Have to use this as it's a component + expect(comp.codeEditorContainer.buildOutput.participation).toEqual(exercise.studentParticipations[0]); + expect(comp.editableInstructions.participation).toEqual(exercise.studentParticipations[0]); // New select solution repository // @ts-ignore - (container.router as MockRouter).setUrl('code-editor-instructor/1/4'); + (comp.router as MockRouter).setUrl('code-editor-instructor/1/4'); routeSubject.next({ exerciseId: 1, participationId: 4 }); containerFixture.detectChanges(); @@ -393,8 +396,8 @@ describe('CodeEditorInstructorIntegration', () => { // Start with assignment repository // @ts-ignore - (container.router as MockRouter).setUrl('code-editor-instructor/1/3'); - container.ngOnInit(); + (comp.router as MockRouter).setUrl('code-editor-instructor/1/3'); + comp.ngOnInit(); routeSubject.next({ exerciseId: 1, participationId: 3 }); findWithParticipationsSubject.next({ body: exercise }); @@ -404,4 +407,89 @@ describe('CodeEditorInstructorIntegration', () => { expect(setDomainSpy).toHaveBeenCalledWith([DomainType.PARTICIPATION, exercise.solutionParticipation]); checkSolutionRepository(exercise); }); + + describe('Repository Navigation', () => { + const exercise = { + id: 1, + problemStatement, + studentParticipations: [{ id: 2 }], + templateParticipation: { id: 3 }, + solutionParticipation: { id: 4 }, + course: { id: 1 }, + } as ProgrammingExercise; + + beforeEach(() => { + comp.exercise = exercise; + }); + + it('should navigate to template participation repository from auxiliary repository', () => { + comp.selectedRepository = REPOSITORY.AUXILIARY; + comp.selectTemplateParticipation(); + expect(navigateSpy).toHaveBeenCalledWith(['../..', exercise.templateParticipation!.id], expect.any(Object)); + }); + + it('should navigate to template participation repository from test repository', () => { + comp.selectedRepository = REPOSITORY.TEST; + comp.selectTemplateParticipation(); + expect(navigateSpy).toHaveBeenCalledWith(['..', exercise.templateParticipation!.id], expect.any(Object)); + }); + + it('should navigate to solution participation repository from auxiliary repository', () => { + comp.selectedRepository = REPOSITORY.AUXILIARY; + comp.selectSolutionParticipation(); + expect(navigateSpy).toHaveBeenCalledWith(['../..', exercise.solutionParticipation!.id], expect.any(Object)); + }); + + it('should navigate to solution participation repository from test repository', () => { + comp.selectedRepository = REPOSITORY.TEST; + comp.selectSolutionParticipation(); + expect(navigateSpy).toHaveBeenCalledWith(['..', exercise.solutionParticipation!.id], expect.any(Object)); + }); + + it('should navigate to assignment participation repository from auxiliary repository', () => { + comp.selectedRepository = REPOSITORY.AUXILIARY; + comp.selectAssignmentParticipation(); + expect(navigateSpy).toHaveBeenCalledWith(['../..', exercise.studentParticipations![0].id], expect.any(Object)); + }); + + it('should navigate to assignment participation repository from test repository', () => { + comp.selectedRepository = REPOSITORY.TEST; + comp.selectAssignmentParticipation(); + expect(navigateSpy).toHaveBeenCalledWith(['..', exercise.studentParticipations![0].id], expect.any(Object)); + }); + + it('should navigate to test repository from auxiliary repository', () => { + comp.selectedRepository = REPOSITORY.AUXILIARY; + comp.selectTestRepository(); + expect(navigateSpy).toHaveBeenCalledWith(['../..', 'test'], expect.any(Object)); + }); + + it('should navigate to test repository from test repository', () => { + comp.selectedRepository = REPOSITORY.TEST; + comp.selectTestRepository(); + expect(navigateSpy).toHaveBeenCalledWith(['..', 'test'], expect.any(Object)); + }); + + it('should navigate to auxiliary repository with provided repositoryId', () => { + const repositoryId = 4; + comp.selectedRepository = REPOSITORY.AUXILIARY; + comp.selectAuxiliaryRepository(repositoryId); + expect(navigateSpy).toHaveBeenCalledWith(['../..', 'auxiliary', repositoryId], expect.any(Object)); + }); + + it('should navigate to auxiliary repository from test repository', () => { + const repositoryId = 4; + comp.selectedRepository = REPOSITORY.TEST; + comp.selectAuxiliaryRepository(repositoryId); + expect(navigateSpy).toHaveBeenCalledWith(['..', 'auxiliary', repositoryId], expect.any(Object)); + }); + + it('should return the correct navigation path based on selected repository', () => { + comp.selectedRepository = REPOSITORY.AUXILIARY; + expect(comp.up()).toBe('../..'); + + comp.selectedRepository = REPOSITORY.TEST; // Or any other non-auxiliary value + expect(comp.up()).toBe('..'); + }); + }); });