diff --git a/docker/cypress-E2E-tests-postgres.yml b/docker/cypress-E2E-tests-postgres.yml index 8ba9d2e99f6b..b45cd64430a0 100644 --- a/docker/cypress-E2E-tests-postgres.yml +++ b/docker/cypress-E2E-tests-postgres.yml @@ -43,8 +43,8 @@ services: artemis-app: condition: service_healthy environment: - SORRY_CYPRESS_PROJECT_ID: "artemis-postgresql" - command: sh -c "cd /app/artemis/src/test/cypress && chmod 777 /root && npm ci && npm run cypress:record:postgresql" + SORRY_CYPRESS_PROJECT_ID: "artemis-postgres" + command: sh -c "cd /app/artemis/src/test/cypress && chmod 777 /root && npm ci && npm run cypress:record:postgres" networks: artemis: diff --git a/docker/cypress.yml b/docker/cypress.yml index aeb94da6a08e..99c3dc855891 100644 --- a/docker/cypress.yml +++ b/docker/cypress.yml @@ -5,7 +5,7 @@ services: artemis-cypress: # Cypress image with node and chrome browser installed (Cypress installation needs to be done separately because we require additional dependencies) - image: docker.io/cypress/browsers:node-18.16.0-chrome-113.0.5672.92-1-ff-113.0-edge-113.0.1774.35-1 + image: docker.io/cypress/browsers:node-18.16.0-chrome-114.0.5735.133-1-ff-114.0.2-edge-114.0.1823.51-1 pull_policy: always environment: CYPRESS_baseUrl: "https://artemis-nginx" @@ -24,6 +24,7 @@ services: SORRY_CYPRESS_BUILD_ID: "${bamboo_buildNumber}" SORRY_CYPRESS_BRANCH_NAME: "${bamboo_planRepository_branchName}" SORRY_CYPRESS_PROJECT_ID: "artemis-mysql" + NO_COLOR: "1" command: sh -c "cd /app/artemis/src/test/cypress && chmod 777 /root && npm ci && npm run cypress:run" volumes: - ..:/app/artemis diff --git a/docker/sorry-cypress/nginx.conf b/docker/sorry-cypress/nginx.conf index 9099efb69ae9..4230e2b6961f 100644 --- a/docker/sorry-cypress/nginx.conf +++ b/docker/sorry-cypress/nginx.conf @@ -108,6 +108,9 @@ http { ssl_ciphers 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384'; ssl_prefer_server_ciphers on; + # This is needed to allow video file uploads, that are greater than 1MB + client_max_body_size 100M; + location / { proxy_pass http://sry-cypress-minio:9090; proxy_http_version 1.1; diff --git a/src/main/webapp/app/exercises/programming/assess/code-editor-tutor-assessment-inline-feedback.component.ts b/src/main/webapp/app/exercises/programming/assess/code-editor-tutor-assessment-inline-feedback.component.ts index 5de88d47739f..7bd0deec731a 100644 --- a/src/main/webapp/app/exercises/programming/assess/code-editor-tutor-assessment-inline-feedback.component.ts +++ b/src/main/webapp/app/exercises/programming/assess/code-editor-tutor-assessment-inline-feedback.component.ts @@ -47,6 +47,8 @@ export class CodeEditorTutorAssessmentInlineFeedbackComponent { // Expose the function to the template readonly roundScoreSpecifiedByCourseSettings = roundValueSpecifiedByCourseSettings; + public elementRef: ElementRef; + viewOnly: boolean; oldFeedback: Feedback; @@ -58,7 +60,9 @@ export class CodeEditorTutorAssessmentInlineFeedbackComponent { faTrashAlt = faTrashAlt; faExclamationTriangle = faExclamationTriangle; - constructor(private translateService: TranslateService, public structuredGradingCriterionService: StructuredGradingCriterionService) {} + constructor(private translateService: TranslateService, public structuredGradingCriterionService: StructuredGradingCriterionService, elementRef: ElementRef) { + this.elementRef = elementRef; + } /** * Updates the current feedback and sets props and emits the feedback to parent component diff --git a/src/main/webapp/app/exercises/programming/shared/code-editor/ace/code-editor-ace.component.html b/src/main/webapp/app/exercises/programming/shared/code-editor/ace/code-editor-ace.component.html index f3011610436f..c662957d3296 100644 --- a/src/main/webapp/app/exercises/programming/shared/code-editor/ace/code-editor-ace.component.html +++ b/src/main/webapp/app/exercises/programming/shared/code-editor/ace/code-editor-ace.component.html @@ -5,11 +5,11 @@
-
+
; @Input() selectedFile: string; @Input() @@ -93,9 +110,8 @@ export class CodeEditorAceComponent implements AfterViewInit, OnChanges, OnDestr fileSession: { [fileName: string]: { code: string; cursor: { column: number; row: number } } } = {}; // Inline feedback variables fileFeedbacks: Feedback[]; - lineCounter: any[] = []; - private elementArray: Element[] = []; fileFeedbackPerLine: { [line: number]: Feedback } = {}; + linesWithNewFeedback: number[] = []; // lines with new feedback, which is not yet saved, but must already be displayed to be editable editorSession: any; markerIds: number[] = []; gutterHighlights: Map = new Map(); @@ -105,7 +121,21 @@ export class CodeEditorAceComponent implements AfterViewInit, OnChanges, OnDestr readonly faPlusSquare = faPlusSquare; readonly faCircleNotch = faCircleNotch; - constructor(private repositoryFileService: CodeEditorRepositoryFileService, private fileService: CodeEditorFileService, protected localStorageService: LocalStorageService) {} + /** + * Get all line numbers in the current editor session that have inline feedback or new feedback (which is only shown temporarily) + */ + get linesWithInlineFeedbackShown(): number[] { + const lines = this.linesWithNewFeedback.concat(Object.keys(this.fileFeedbackPerLine).map((line) => parseInt(line))); + lines.sort((a, b) => a - b); + return lines; + } + + constructor( + private repositoryFileService: CodeEditorRepositoryFileService, + private fileService: CodeEditorFileService, + protected localStorageService: LocalStorageService, + private changeDetectorRef: ChangeDetectorRef, + ) {} /** * @function ngAfterViewInit @@ -178,6 +208,8 @@ export class CodeEditorAceComponent implements AfterViewInit, OnChanges, OnDestr } }); } + // Remove open inline feedback widgets for new feedback + this.linesWithNewFeedback = []; // We first remove the annotationChange subscription so the initial setValue doesn't count as an insert if (this.annotationChange) { this.annotationChange.unsubscribe(); @@ -199,10 +231,7 @@ export class CodeEditorAceComponent implements AfterViewInit, OnChanges, OnDestr this.displayAnnotations(); // Setup inline feedbacks - // Get amount of lines of code in order to render for each line a corresponding inline feedback component if (this.isTutorAssessment) { - const lines = this.editor.getEditor().getSession().getLength(); - this.lineCounter = new Array(lines); if (!this.feedbacks) { this.feedbacks = []; } @@ -281,6 +310,13 @@ export class CodeEditorAceComponent implements AfterViewInit, OnChanges, OnDestr if (this.annotationChange) { this.annotationChange.unsubscribe(); } + // remove all line widgets so that no old ones are displayed when the editor is opened again + if (this.editorSession && this.editorSession.widgetManager) { + for (const line of this.linesWithInlineFeedbackShown) { + const widget = this.editorSession.lineWidgets.find((w: any) => w?.el === this.getInlineFeedbackNode(line)); + this.editorSession.widgetManager.removeLineWidget(widget); + } + } } /** @@ -419,7 +455,7 @@ export class CodeEditorAceComponent implements AfterViewInit, OnChanges, OnDestr /** * Displays the inline feedback of a line of code using lineWidgets. We first go through all feedbacks of the selected file - * and create a lineWidget for each feedback. The elementArray contains all inline feedback components which have been added as lineWidget. + * and create a lineWidget for each feedback. */ displayFeedbacks() { this.fileFeedbacks.forEach((feedback) => { @@ -450,19 +486,25 @@ export class CodeEditorAceComponent implements AfterViewInit, OnChanges, OnDestr }); } + /** + * Get the wrapper div of an inline feedback as a DOM node + * + * @param line line of code where the feedback inline component is located + */ + getInlineFeedbackNode(line: number): Element | undefined { + return this.inlineFeedbackComponents.find((c) => c.codeLine === line)?.elementRef.nativeElement; + } + /** * Add lineWidget for specific line of code. * @param line line of code where the feedback inline component will be added to. */ addLineWidgetWithFeedback(line: number) { - // If the component was not found in the elementArray, we get it from the DOM and add it to elementArray - let inlineFeedback: Element | undefined = this.elementArray.find((element) => element.id === 'test-' + line); - if (!inlineFeedback) { - inlineFeedback = document.querySelector(`#test-${line}`) ?? undefined; - if (inlineFeedback) { - this.elementArray.push(inlineFeedback); - } - } + // Create new feedback element from the DOM + this.linesWithNewFeedback = [...this.linesWithNewFeedback, line]; + // Update DOM so that the new feedback DOM node exists + this.changeDetectorRef.detectChanges(); + const inlineFeedback = this.getInlineFeedbackNode(line); if (inlineFeedback) { const lineWidget = { row: line, @@ -474,12 +516,10 @@ export class CodeEditorAceComponent implements AfterViewInit, OnChanges, OnDestr if (this.editorSession.lineWidgets) { const displayedWidget = this.editorSession.lineWidgets.find((w: any) => w && w.row === lineWidget.row); if (!displayedWidget) { - lineWidget.el.className = 'inline-feedback'; this.editorSession.widgetManager.addLineWidget(lineWidget); lineWidget.el.querySelector('textarea')?.focus(); } } else { - lineWidget.el.className = 'inline-feedback'; this.editorSession.widgetManager.addLineWidget(lineWidget); lineWidget.el.querySelector('textarea')?.focus(); } @@ -491,7 +531,7 @@ export class CodeEditorAceComponent implements AfterViewInit, OnChanges, OnDestr * @param line Line of code which has inline feedback (lineWidget) */ adjustLineWidgetHeight(line: number) { - const widget = this.editorSession.lineWidgets.find((w: any) => w && w.el?.id === 'test-' + line); + const widget = this.editorSession.lineWidgets.find((w: any) => w?.el === this.getInlineFeedbackNode(line)); this.editorSession.widgetManager.removeLineWidget(widget); this.editorSession.widgetManager.addLineWidget(widget); } @@ -510,6 +550,8 @@ export class CodeEditorAceComponent implements AfterViewInit, OnChanges, OnDestr } else { this.feedbacks.push(feedback); this.fileFeedbackPerLine[line] = feedback; + // the feedback isn't new anymore + this.linesWithNewFeedback = this.linesWithNewFeedback.filter((l) => l !== line); } this.onUpdateFeedback.emit(this.feedbacks); this.adjustLineWidgetHeight(line); @@ -520,11 +562,14 @@ export class CodeEditorAceComponent implements AfterViewInit, OnChanges, OnDestr * @param line */ cancelFeedback(line: number) { - if (!this.fileFeedbackPerLine[line]) { - const widget = this.editorSession.lineWidgets.filter((w: any) => w && w.el?.id === 'test-' + line)[0]; - this.editorSession.widgetManager.removeLineWidget(widget); - } else { + if (this.fileFeedbackPerLine[line]) { + // feedback exists, just re-align height this.adjustLineWidgetHeight(line); + } else { + const widget = this.editorSession.lineWidgets.filter((w: any) => w?.el === this.getInlineFeedbackNode(line))[0]; + this.editorSession.widgetManager.removeLineWidget(widget); + // also remove from linesWithNewFeedback, it should not be shown anymore + this.linesWithNewFeedback = this.linesWithNewFeedback.filter((l) => l !== line); } } diff --git a/src/main/webapp/app/exercises/programming/shared/code-editor/ace/code-editor-ace.scss b/src/main/webapp/app/exercises/programming/shared/code-editor/ace/code-editor-ace.scss index 00241dd4f62a..2f196de01083 100644 --- a/src/main/webapp/app/exercises/programming/shared/code-editor/ace/code-editor-ace.scss +++ b/src/main/webapp/app/exercises/programming/shared/code-editor/ace/code-editor-ace.scss @@ -13,10 +13,6 @@ Ace Editor flex: 1 1 auto; } - .inline-feedback-d-none { - display: none; - } - .inline-feedback { display: block; } @@ -66,6 +62,11 @@ Ace Editor } } + .card-body > .inline-feedback { + /* The ACE Line Widget will copy this node to another place where it will be visible */ + display: none; + } + .diff-newLine { position: absolute; background: var(--code-editor-diff-newline-background); diff --git a/src/main/webapp/app/overview/course-exercises/course-exercises.component.html b/src/main/webapp/app/overview/course-exercises/course-exercises.component.html index 023e268d4253..7f03cc0c8bd9 100644 --- a/src/main/webapp/app/overview/course-exercises/course-exercises.component.html +++ b/src/main/webapp/app/overview/course-exercises/course-exercises.component.html @@ -128,8 +128,8 @@

> - {{ weeklyExercisesGrouped[weekKey].start | artemisDate : 'long-date' }} - - {{ weeklyExercisesGrouped[weekKey].end | artemisDate : 'long-date' }} + {{ weeklyExercisesGrouped[weekKey].start | artemisDate : 'short-date' }} - + {{ weeklyExercisesGrouped[weekKey].end | artemisDate : 'short-date' }} {{ 'artemisApp.courseOverview.exerciseList.noDateAssociated' | artemisTranslate }} diff --git a/src/test/cypress/cypress.config.ts b/src/test/cypress/cypress.config.ts index f879d7ac05b8..f9f20257224b 100644 --- a/src/test/cypress/cypress.config.ts +++ b/src/test/cypress/cypress.config.ts @@ -38,6 +38,7 @@ export default defineConfig({ screenshotsFolder: 'screenshots', videosFolder: 'videos', video: true, + videoUploadOnPasses: false, screenshotOnRunFailure: true, viewportWidth: 1920, viewportHeight: 1080, diff --git a/src/test/cypress/e2e/course/CourseCommunication.cy.ts b/src/test/cypress/e2e/course/CourseCommunication.cy.ts index 80c47d48306b..0fc78749c441 100644 --- a/src/test/cypress/e2e/course/CourseCommunication.cy.ts +++ b/src/test/cypress/e2e/course/CourseCommunication.cy.ts @@ -48,7 +48,7 @@ describe('Course communication', () => { cy.login(studentOne, `/courses/${course.id}/discussion`); courseCommunication.newPost(); courseCommunication.selectContextInModal(CourseWideContext.ORGANIZATION); - courseCommunication.setTitleInModal('Cypress Test Post'); + courseCommunication.setTitleInModal('Test Post'); cy.fixture('loremIpsum.txt').then((text) => { courseCommunication.setContentInModal(text); }); diff --git a/src/test/cypress/e2e/exam/ExamCreationDeletion.cy.ts b/src/test/cypress/e2e/exam/ExamCreationDeletion.cy.ts index 3281e71d0aab..b9abfb7ea8f8 100644 --- a/src/test/cypress/e2e/exam/ExamCreationDeletion.cy.ts +++ b/src/test/cypress/e2e/exam/ExamCreationDeletion.cy.ts @@ -14,10 +14,10 @@ const examData = { endDate: dayjs().add(2, 'day'), numberOfExercises: 4, maxPoints: 40, - startText: 'Cypress exam start text', - endText: 'Cypress exam end text', - confirmationStartText: 'Cypress exam confirmation start text', - confirmationEndText: 'Cypress exam confirmation end text', + startText: 'Exam start text', + endText: 'Exam end text', + confirmationStartText: 'Exam confirmation start text', + confirmationEndText: 'Exam confirmation end text', }; const editedExamData = { @@ -27,10 +27,10 @@ const editedExamData = { endDate: dayjs().add(4, 'day'), numberOfExercises: 3, maxPoints: 30, - startText: 'Edited cypress exam start text', - endText: 'Edited cypress exam end text', - confirmationStartText: 'Edited cypress exam confirmation start text', - confirmationEndText: 'Edited cypress exam confirmation end text', + startText: 'Edited exam start text', + endText: 'Edited exam end text', + confirmationStartText: 'Edited exam confirmation start text', + confirmationEndText: 'Edited exam confirmation end text', }; const dateFormat = 'MMM D, YYYY HH:mm'; diff --git a/src/test/cypress/e2e/exam/test-exam/TestExamCreationDeletion.cy.ts b/src/test/cypress/e2e/exam/test-exam/TestExamCreationDeletion.cy.ts index 1f8af8e26894..21ff8fcd9e31 100644 --- a/src/test/cypress/e2e/exam/test-exam/TestExamCreationDeletion.cy.ts +++ b/src/test/cypress/e2e/exam/test-exam/TestExamCreationDeletion.cy.ts @@ -15,10 +15,10 @@ const examData = { workingTime: 5, numberOfExercises: 4, maxPoints: 40, - startText: 'Cypress exam start text', - endText: 'Cypress exam end text', - confirmationStartText: 'Cypress exam confirmation start text', - confirmationEndText: 'Cypress exam confirmation end text', + startText: 'Exam start text', + endText: 'Exam end text', + confirmationStartText: 'Exam confirmation start text', + confirmationEndText: 'Exam confirmation end text', }; describe('Test Exam creation/deletion', () => { diff --git a/src/test/cypress/e2e/exercises/modeling/ModelingExerciseManagement.cy.ts b/src/test/cypress/e2e/exercises/modeling/ModelingExerciseManagement.cy.ts index 6530619ca800..49e154bdbb3e 100644 --- a/src/test/cypress/e2e/exercises/modeling/ModelingExerciseManagement.cy.ts +++ b/src/test/cypress/e2e/exercises/modeling/ModelingExerciseManagement.cy.ts @@ -79,7 +79,7 @@ describe('Modeling Exercise Management Spec', () => { it('Edit Existing Modeling Exercise', () => { cy.visit(`/course-management/${course.id}/modeling-exercises/${modelingExercise.id}/edit`); - const newTitle = 'Cypress EDITED ME'; + const newTitle = 'New Modeling Exercise Title'; const points = 100; modelingExerciseCreation.setTitle(newTitle); modelingExerciseCreation.pickDifficulty({ hard: true }); diff --git a/src/test/cypress/e2e/exercises/programming/ProgrammingExerciseManagement.cy.ts b/src/test/cypress/e2e/exercises/programming/ProgrammingExerciseManagement.cy.ts index 66e1c1715c1b..7becd367b108 100644 --- a/src/test/cypress/e2e/exercises/programming/ProgrammingExerciseManagement.cy.ts +++ b/src/test/cypress/e2e/exercises/programming/ProgrammingExerciseManagement.cy.ts @@ -58,9 +58,9 @@ describe('Programming Exercise Management', () => { courseManagementExercises.createProgrammingExercise(); cy.url().should('include', '/programming-exercises/new'); cy.log('Filling out programming exercise info...'); - const exerciseTitle = 'Cypress programming exercise ' + generateUUID(); + const exerciseTitle = 'Programming exercise ' + generateUUID(); programmingExerciseCreation.setTitle(exerciseTitle); - programmingExerciseCreation.setShortName('cypress' + generateUUID()); + programmingExerciseCreation.setShortName('programming' + generateUUID()); programmingExerciseCreation.setPackageName('de.test'); programmingExerciseCreation.setPoints(100); programmingExerciseCreation.checkAllowOnlineEditor(); diff --git a/src/test/cypress/fixtures/exam/template.json b/src/test/cypress/fixtures/exam/template.json index a9fbfa983ad6..5f4ea802e6a7 100644 --- a/src/test/cypress/fixtures/exam/template.json +++ b/src/test/cypress/fixtures/exam/template.json @@ -11,8 +11,8 @@ "startDate": "", "endDate": "", "numberOfExercisesInExam": 1, - "startText": "Cypress exam start text", - "endText": "Cypress exam end text", - "confirmationStartText": "Cypress exam confirmation start text", - "confirmationEndText": "Cypress exam confirmation end text" + "startText": "Exam start text", + "endText": "Exam end text", + "confirmationStartText": "Exam confirmation start text", + "confirmationEndText": "Exam confirmation end text" } diff --git a/src/test/cypress/fixtures/exercise/modeling/template.json b/src/test/cypress/fixtures/exercise/modeling/template.json index d31d76485efb..6173bb7cfe61 100644 --- a/src/test/cypress/fixtures/exercise/modeling/template.json +++ b/src/test/cypress/fixtures/exercise/modeling/template.json @@ -22,7 +22,7 @@ "presentationScoreEnabled": false, "diagramType": "ClassDiagram", "assessmentType": "MANUAL", - "title": "Cypress Modeling Exercise", + "title": "Modeling Exercise", "categories": ["{\"category\":\"cypress-e2e\",\"color\":\"#6ae8ac\"}"], "difficulty": "EASY", "maxPoints": 10, diff --git a/src/test/cypress/fixtures/exercise/quiz/short_answer/template.json b/src/test/cypress/fixtures/exercise/quiz/short_answer/template.json index f520334e2ca5..adb85d9280df 100644 --- a/src/test/cypress/fixtures/exercise/quiz/short_answer/template.json +++ b/src/test/cypress/fixtures/exercise/quiz/short_answer/template.json @@ -5,7 +5,7 @@ "exportQuiz": false, "matchLetterCase": false, "similarityValue": 85, - "title": "Cypress SA", + "title": "Short Answer Quiz", "text": "Never gonna [-spot 1] you up\nNever gonna [-spot 2] you down\nNever gonna [-spot 3] around and [-spot 4] you\nNever gonna make you [-spot 5]\nNever gonna say [-spot 6]\nNever gonna tell a lie and hurt you", "scoringType": "PROPORTIONAL_WITHOUT_PENALTY", "points": 1, diff --git a/src/test/cypress/fixtures/exercise/text/template.json b/src/test/cypress/fixtures/exercise/text/template.json index 9157524b5a02..9a185cec3058 100644 --- a/src/test/cypress/fixtures/exercise/text/template.json +++ b/src/test/cypress/fixtures/exercise/text/template.json @@ -23,7 +23,7 @@ "assessmentType": "MANUAL", "title": "Text Exercise 1", "maxPoints": 10, - "problemStatement": "Cypress Problem Statement", - "exampleSolution": "Cypress Example Solution", - "gradingInstructions": "Cypress Assessment Instructions" + "problemStatement": "Problem Statement", + "exampleSolution": "Example Solution", + "gradingInstructions": "Assessment Instructions" } diff --git a/src/test/cypress/fixtures/lecture/template.json b/src/test/cypress/fixtures/lecture/template.json index c17cb81a6095..4c540b3d11f2 100644 --- a/src/test/cypress/fixtures/lecture/template.json +++ b/src/test/cypress/fixtures/lecture/template.json @@ -1,7 +1,7 @@ { "course": null, - "title": "Cypress lecture", - "channelName": "lecture-cypress-lecture", + "title": "Test lecture", + "channelName": "lecture-test-lecture", "description": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.", "startDate": null, "endDate": null diff --git a/src/test/cypress/package-lock.json b/src/test/cypress/package-lock.json index 0f2cccd2f445..f0ec05ffa3fc 100644 --- a/src/test/cypress/package-lock.json +++ b/src/test/cypress/package-lock.json @@ -8,12 +8,12 @@ "license": "MIT", "devDependencies": { "@4tw/cypress-drag-drop": "2.2.4", - "@types/node": "20.2.5", + "@types/node": "20.4.0", "cypress": "12.16.0", - "cypress-cloud": "1.8.2", + "cypress-cloud": "1.9.0", "cypress-file-upload": "5.0.8", "cypress-wait-until": "2.0.0", - "typescript": "4.9.5", + "typescript": "5.1.6", "uuid": "9.0.0", "wait-on": "7.0.1" } @@ -301,9 +301,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.2.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.5.tgz", - "integrity": "sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ==", + "version": "20.4.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.0.tgz", + "integrity": "sha512-jfT7iTf/4kOQ9S7CHV9BIyRaQqHu67mOjsIQBC3BKZvzvUB6zLxEwJ6sBE3ozcvP8kF6Uk5PXN0Q+c0dfhGX0g==", "dev": true }, "node_modules/@types/sinonjs__fake-timers": { @@ -961,9 +961,9 @@ } }, "node_modules/cypress-cloud": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/cypress-cloud/-/cypress-cloud-1.8.2.tgz", - "integrity": "sha512-WAoq4YIb5dUrAugogMPpWEUw4z5NFh/9evuqk9X8CiDVo15HPgAfxdsRYfoI+xa7u7rEYRX45eL8PQ5324fD9A==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/cypress-cloud/-/cypress-cloud-1.9.0.tgz", + "integrity": "sha512-uKo4MzLkfF52JDn6jhizQqch1wdIJ51HkXhvlUA54YfqxSc6PW1DB19avcKKhKHq2Sco5OY2K5LIdwazp+NFwg==", "dev": true, "dependencies": { "@cypress/commit-info": "^2.2.0", @@ -2947,16 +2947,16 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/unc-path-regex": { diff --git a/src/test/cypress/package.json b/src/test/cypress/package.json index 5ce9a9e20543..4833318a7ba4 100644 --- a/src/test/cypress/package.json +++ b/src/test/cypress/package.json @@ -8,12 +8,12 @@ ], "devDependencies": { "@4tw/cypress-drag-drop": "2.2.4", - "@types/node": "20.2.5", + "@types/node": "20.4.0", "cypress": "12.16.0", - "cypress-cloud": "1.8.2", + "cypress-cloud": "1.9.0", "cypress-file-upload": "5.0.8", "cypress-wait-until": "2.0.0", - "typescript": "4.9.5", + "typescript": "5.1.6", "uuid": "9.0.0", "wait-on": "7.0.1" }, @@ -26,7 +26,7 @@ "cypress:open": "cypress open", "cypress:run": "cypress run --browser=chrome", "cypress:record:mysql": "npx cypress-cloud run --record --ci-build-id \"${SORRY_CYPRESS_BRANCH_NAME} #${SORRY_CYPRESS_BUILD_ID} (MySQL)\"", - "cypress:record:postgresql": "npx cypress-cloud run --record --ci-build-id \"${SORRY_CYPRESS_BRANCH_NAME} #${SORRY_CYPRESS_BUILD_ID} (PostgreSQL)\"", + "cypress:record:postgres": "npx cypress-cloud run --record --ci-build-id \"${SORRY_CYPRESS_BRANCH_NAME} #${SORRY_CYPRESS_BUILD_ID} (Postgres)\"", "update": "npm-upgrade" } } diff --git a/src/test/cypress/support/requests/CourseManagementRequests.ts b/src/test/cypress/support/requests/CourseManagementRequests.ts index 1b81ea440fc5..73f87a7315e0 100644 --- a/src/test/cypress/support/requests/CourseManagementRequests.ts +++ b/src/test/cypress/support/requests/CourseManagementRequests.ts @@ -128,7 +128,7 @@ export class CourseManagementRequests { releaseDate = day(), dueDate = day().add(1, 'day'), title = 'Programming ' + generateUUID(), - programmingShortName = 'cypress' + generateUUID(), + programmingShortName = 'programming' + generateUUID(), packageName = 'de.test', assessmentDate = day().add(2, 'days'), assessmentType = ProgrammingExerciseAssessmentType.AUTOMATIC, diff --git a/src/test/cypress/support/utils.ts b/src/test/cypress/support/utils.ts index 1b994302f471..e58a70782370 100644 --- a/src/test/cypress/support/utils.ts +++ b/src/test/cypress/support/utils.ts @@ -15,7 +15,7 @@ day.extend(utc); * */ export function generateUUID() { const uuid = uuidv4().replace(/-/g, ''); - return uuid.substr(0, 5); + return uuid.substr(0, 9); } /** diff --git a/src/test/javascript/spec/component/code-editor/code-editor-ace.component.spec.ts b/src/test/javascript/spec/component/code-editor/code-editor-ace.component.spec.ts index edcd7b2de61a..e3388c9b30a9 100644 --- a/src/test/javascript/spec/component/code-editor/code-editor-ace.component.spec.ts +++ b/src/test/javascript/spec/component/code-editor/code-editor-ace.component.spec.ts @@ -55,6 +55,14 @@ describe('CodeEditorAceComponent', () => { jest.restoreAllMocks(); }); + it('should know the lines in which inline feedback is shown', () => { + expect(comp.linesWithInlineFeedbackShown).toEqual([]); + comp.linesWithNewFeedback = [15, 5]; + expect(comp.linesWithInlineFeedbackShown).toEqual([5, 15]); + comp.fileFeedbackPerLine = { 50: {}, 1050: {} }; + expect(comp.linesWithInlineFeedbackShown).toEqual([5, 15, 50, 1050]); + }); + it('without any inputs, should still render correctly without ace, showing a placeholder', () => { fixture.detectChanges(); const placeholder = debugElement.query(By.css('#no-file-selected')); @@ -121,6 +129,14 @@ describe('CodeEditorAceComponent', () => { expect(initEditorAfterFileChangeSpy).toHaveBeenCalledWith(); }); + it('should discard all new feedback after a re-init because of a file change', () => { + const getInlineFeedbackNodeStub = jest.spyOn(comp, 'getInlineFeedbackNode'); + getInlineFeedbackNodeStub.mockReturnValue(undefined); + comp.addLineWidgetWithFeedback(16); + comp.initEditorAfterFileChange(); + expect(comp.linesWithNewFeedback).toEqual([]); + }); + it('should not load the file from server on selected file change if the file is already in session', () => { const selectedFile = 'dummy'; const fileSession = { [selectedFile]: { code: 'lorem ipsum', cursor: { column: 0, row: 0 } } }; @@ -232,4 +248,46 @@ describe('CodeEditorAceComponent', () => { fixture.detectChanges(); expect(editorTabSize()).toBe(4); }); + + it('should temporarily show new feedbacks which have not been updated yet', () => { + const getInlineFeedbackNodeSpy = jest.spyOn(comp, 'getInlineFeedbackNode'); + getInlineFeedbackNodeSpy.mockReturnValue(undefined); + comp.addLineWidgetWithFeedback(16); + expect(comp.linesWithNewFeedback).toEqual([16]); + }); + + it('should not show an updated feedback as new', () => { + comp.feedbacks = []; + const getInlineFeedbackNodeSpy = jest.spyOn(comp, 'getInlineFeedbackNode'); + getInlineFeedbackNodeSpy.mockReturnValue(undefined); + const adjustLineWidgetHeightStub = jest.spyOn(comp, 'adjustLineWidgetHeight'); + adjustLineWidgetHeightStub.mockImplementation(() => {}); + comp.addLineWidgetWithFeedback(16); + comp.addLineWidgetWithFeedback(17); + comp.updateFeedback({ reference: 'line:16' }); + expect(comp.linesWithNewFeedback).toEqual([17]); + }); + + it('should not show new feedback that is cancelled anymore', () => { + const getInlineFeedbackNodeSpy = jest.spyOn(comp, 'getInlineFeedbackNode'); + getInlineFeedbackNodeSpy.mockReturnValue(undefined); + comp.editorSession = { lineWidgets: [], widgetManager: { removeLineWidget: () => {} } } as any; + const removeLineWidget = jest.spyOn(comp.editorSession.widgetManager, 'removeLineWidget'); + comp.addLineWidgetWithFeedback(1); + comp.addLineWidgetWithFeedback(16); + comp.cancelFeedback(16); + expect(comp.linesWithNewFeedback).toEqual([1]); + expect(removeLineWidget).toHaveBeenCalled(); + }); + + it('should explicitly remove all ACE line widgets when being destroyed', () => { + const getInlineFeedbackNodeStub = jest.spyOn(comp, 'getInlineFeedbackNode'); + getInlineFeedbackNodeStub.mockReturnValue(undefined); + comp.editorSession = { lineWidgets: [], widgetManager: { removeLineWidget: () => {} } } as any; + const removeLineWidget = jest.spyOn(comp.editorSession.widgetManager, 'removeLineWidget'); + comp.addLineWidgetWithFeedback(1); + comp.addLineWidgetWithFeedback(16); + comp.ngOnDestroy(); + expect(removeLineWidget).toHaveBeenCalledTimes(2); + }); });