@if (!isCommunicationPage) {
@if (sortedAnswerPosts.length) {
diff --git a/src/main/webapp/app/shared/pipes/html-for-markdown.pipe.ts b/src/main/webapp/app/shared/pipes/html-for-markdown.pipe.ts
index 610faf20c0ca..660a5c0971dc 100644
--- a/src/main/webapp/app/shared/pipes/html-for-markdown.pipe.ts
+++ b/src/main/webapp/app/shared/pipes/html-for-markdown.pipe.ts
@@ -1,7 +1,7 @@
import { Pipe, PipeTransform } from '@angular/core';
-import { ShowdownExtension } from 'showdown';
import { SafeHtml } from '@angular/platform-browser';
import { ArtemisMarkdownService } from 'app/shared/markdown.service';
+import type { PluginSimple } from 'markdown-it';
@Pipe({
name: 'htmlForMarkdown',
@@ -12,14 +12,14 @@ export class HtmlForMarkdownPipe implements PipeTransform {
/**
* Converts markdown into html, sanitizes it and then declares it as safe to bypass further security.
* @param {string} markdown the original markdown text
- * @param {ShowdownExtension[]} extensions to use for markdown parsing
+ * @param {PluginSimple[]} extensions to use for markdown parsing
* @param {string[]} allowedHtmlTags to allow during sanitization
* @param {string[]} allowedHtmlAttributes to allow during sanitization
* @returns {string} the resulting html as a SafeHtml object that can be inserted into the angular template
*/
transform(
markdown?: string,
- extensions: ShowdownExtension[] = [],
+ extensions: PluginSimple[] = [],
allowedHtmlTags: string[] | undefined = undefined,
allowedHtmlAttributes: string[] | undefined = undefined,
): SafeHtml {
diff --git a/src/main/webapp/app/shared/util/markdown.conversion.util.ts b/src/main/webapp/app/shared/util/markdown.conversion.util.ts
index 658574db088b..d5166b70d690 100644
--- a/src/main/webapp/app/shared/util/markdown.conversion.util.ts
+++ b/src/main/webapp/app/shared/util/markdown.conversion.util.ts
@@ -1,24 +1,41 @@
-import showdown from 'showdown';
-import showdownKatex from 'showdown-katex';
-import showdownHighlight from 'showdown-highlight';
+import { ArtemisTextReplacementPlugin } from 'app/shared/markdown-editor/extensions/ArtemisTextReplacementPlugin';
import DOMPurify, { Config } from 'dompurify';
+import type { PluginSimple } from 'markdown-it';
+import markdownIt from 'markdown-it';
+import markdownItClass from 'markdown-it-class';
+import markdownItKatex from '@vscode/markdown-it-katex';
+import markdownItHighlightjs from 'markdown-it-highlightjs';
+import TurndownService from 'turndown';
/**
- * showdown will add the classes to the converted html
- * see: https://github.com/showdownjs/showdown/wiki/Add-default-classes-for-each-HTML-element
+ * Add these classes to the converted html.
*/
const classMap: { [key: string]: string } = {
table: 'table',
};
-/**
- * extension to add css classes to html tags
- * see: https://github.com/showdownjs/showdown/wiki/Add-default-classes-for-each-HTML-element
- */
-export const addCSSClass = Object.keys(classMap).map((key) => ({
- type: 'output',
- regex: new RegExp(`<${key}(.*)>`, 'g'),
- replace: `<${key} class="${classMap[key]}" $1>`,
-}));
+
+// An inline math formula has some other characters before or after the formula and uses $$ as delimiters
+const inlineFormulaRegex = /(?:.+\$\$[^\$]+\$\$)|(?:\$\$[^\$]+\$\$.+)/g;
+
+class FormulaCompatibilityPlugin extends ArtemisTextReplacementPlugin {
+ replaceText(text: string): string {
+ return text
+ .split('\n')
+ .map((line) => {
+ if (line.match(inlineFormulaRegex)) {
+ line = line.replace(/\$\$/g, '$');
+ }
+ if (line.includes('\\\\begin') || line.includes('\\\\end')) {
+ line = line.replaceAll('\\\\begin', '\\begin').replaceAll('\\\\end', '\\end');
+ }
+ return line;
+ })
+ .join('\n');
+ }
+}
+const formulaCompatibilityPlugin = new FormulaCompatibilityPlugin();
+
+const turndownService = new TurndownService();
/**
* Converts markdown into html (string) and sanitizes it. Does NOT declare it as safe to bypass further security
@@ -32,24 +49,37 @@ export const addCSSClass = Object.keys(classMap).map((key) => ({
*/
export function htmlForMarkdown(
markdownText?: string,
- extensions: showdown.ShowdownExtension[] = [],
+ extensions: PluginSimple[] = [],
allowedHtmlTags: string[] | undefined = undefined,
allowedHtmlAttributes: string[] | undefined = undefined,
): string {
if (!markdownText || markdownText === '') {
return '';
}
- const converter = new showdown.Converter({
- parseImgDimensions: true,
- headerLevelStart: 3,
- simplifiedAutoLink: true,
- strikethrough: true,
- tables: true,
- openLinksInNewWindow: true,
- backslashEscapesHTMLTags: true,
- extensions: [...extensions, showdownKatex(), showdownHighlight({ pre: true }), ...addCSSClass],
+
+ const md = markdownIt({
+ html: true,
+ linkify: true,
+ breaks: false, // Avoid line breaks after tasks
});
- const html = converter.makeHtml(markdownText);
+ for (const extension of extensions) {
+ md.use(extension);
+ }
+
+ // Add default extensions (Code Highlight, Latex)
+ md.use(markdownItHighlightjs)
+ .use(formulaCompatibilityPlugin.getExtension())
+ .use(markdownItKatex, {
+ enableMathInlineInHtml: true,
+ })
+ .use(markdownItClass, classMap);
+ let markdownRender = md.render(markdownText);
+ if (markdownRender.endsWith('\n')) {
+ // Keep legacy behavior from showdown where the output does not end with \n.
+ // This is needed because e.g. for quiz questions, we render the markdown in multiple small parts and then concatenate them.
+ markdownRender = markdownRender.slice(0, -1);
+ }
+
const purifyParameters = {} as Config;
// Prevents sanitizer from deleting
id
purifyParameters['ADD_TAGS'] = ['testid'];
@@ -59,18 +89,9 @@ export function htmlForMarkdown(
if (allowedHtmlAttributes) {
purifyParameters['ALLOWED_ATTR'] = allowedHtmlAttributes;
}
- return DOMPurify.sanitize(html, purifyParameters) as string;
+ return DOMPurify.sanitize(markdownRender, purifyParameters) as string;
}
export function markdownForHtml(htmlText: string): string {
- const converter = new showdown.Converter({
- parseImgDimensions: true,
- headerLevelStart: 3,
- simplifiedAutoLink: true,
- strikethrough: true,
- tables: true,
- openLinksInNewWindow: true,
- backslashEscapesHTMLTags: true,
- });
- return converter.makeMarkdown(htmlText);
+ return turndownService.turndown(htmlText);
}
diff --git a/src/test/java/de/tum/cit/aet/artemis/atlas/AbstractAtlasIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/atlas/AbstractAtlasIntegrationTest.java
index 744c27d2f937..f2513d50f2f5 100644
--- a/src/test/java/de/tum/cit/aet/artemis/atlas/AbstractAtlasIntegrationTest.java
+++ b/src/test/java/de/tum/cit/aet/artemis/atlas/AbstractAtlasIntegrationTest.java
@@ -14,13 +14,13 @@
import de.tum.cit.aet.artemis.atlas.repository.CompetencyRepository;
import de.tum.cit.aet.artemis.atlas.repository.CourseCompetencyRepository;
import de.tum.cit.aet.artemis.atlas.repository.KnowledgeAreaRepository;
-import de.tum.cit.aet.artemis.atlas.repository.PrerequisiteRepository;
import de.tum.cit.aet.artemis.atlas.repository.ScienceSettingRepository;
import de.tum.cit.aet.artemis.atlas.repository.SourceRepository;
import de.tum.cit.aet.artemis.atlas.repository.StandardizedCompetencyRepository;
import de.tum.cit.aet.artemis.atlas.service.competency.CompetencyProgressService;
import de.tum.cit.aet.artemis.atlas.test_repository.CompetencyProgressTestRepository;
import de.tum.cit.aet.artemis.atlas.test_repository.LearningPathTestRepository;
+import de.tum.cit.aet.artemis.atlas.test_repository.PrerequisiteTestRepository;
import de.tum.cit.aet.artemis.atlas.test_repository.ScienceEventTestRepository;
import de.tum.cit.aet.artemis.core.service.feature.FeatureToggleService;
import de.tum.cit.aet.artemis.core.util.PageableSearchUtilService;
@@ -74,7 +74,7 @@ public abstract class AbstractAtlasIntegrationTest extends AbstractSpringIntegra
protected ScienceEventTestRepository scienceEventRepository;
@Autowired
- protected PrerequisiteRepository prerequisiteRepository;
+ protected PrerequisiteTestRepository prerequisiteRepository;
@Autowired
protected CompetencyJolRepository competencyJolRepository;
diff --git a/src/test/java/de/tum/cit/aet/artemis/atlas/competency/util/PrerequisiteUtilService.java b/src/test/java/de/tum/cit/aet/artemis/atlas/competency/util/PrerequisiteUtilService.java
index edf6e9378286..ac23fe4ef531 100644
--- a/src/test/java/de/tum/cit/aet/artemis/atlas/competency/util/PrerequisiteUtilService.java
+++ b/src/test/java/de/tum/cit/aet/artemis/atlas/competency/util/PrerequisiteUtilService.java
@@ -7,7 +7,7 @@
import org.springframework.stereotype.Service;
import de.tum.cit.aet.artemis.atlas.domain.competency.Prerequisite;
-import de.tum.cit.aet.artemis.atlas.repository.PrerequisiteRepository;
+import de.tum.cit.aet.artemis.atlas.test_repository.PrerequisiteTestRepository;
import de.tum.cit.aet.artemis.core.domain.Course;
/**
@@ -17,7 +17,7 @@
public class PrerequisiteUtilService {
@Autowired
- private PrerequisiteRepository prerequisiteRepository;
+ private PrerequisiteTestRepository prerequisiteRepository;
/**
* Creates and saves a Prerequisite competency for the given Course.
diff --git a/src/test/java/de/tum/cit/aet/artemis/atlas/test_repository/PrerequisiteTestRepository.java b/src/test/java/de/tum/cit/aet/artemis/atlas/test_repository/PrerequisiteTestRepository.java
new file mode 100644
index 000000000000..a3a8e54cbe3b
--- /dev/null
+++ b/src/test/java/de/tum/cit/aet/artemis/atlas/test_repository/PrerequisiteTestRepository.java
@@ -0,0 +1,19 @@
+package de.tum.cit.aet.artemis.atlas.test_repository;
+
+import java.util.List;
+
+import org.springframework.context.annotation.Primary;
+import org.springframework.stereotype.Repository;
+
+import de.tum.cit.aet.artemis.atlas.domain.competency.Prerequisite;
+import de.tum.cit.aet.artemis.atlas.repository.PrerequisiteRepository;
+
+/**
+ * Spring Data JPA repository for the {@link Prerequisite} entity.
+ */
+@Repository
+@Primary
+public interface PrerequisiteTestRepository extends PrerequisiteRepository {
+
+ List
findAllByCourseIdOrderById(long courseId);
+}
diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationIndependentTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationIndependentTest.java
index 46749fbcd3ec..21791e5d24f7 100644
--- a/src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationIndependentTest.java
+++ b/src/test/java/de/tum/cit/aet/artemis/programming/AbstractProgrammingIntegrationIndependentTest.java
@@ -25,7 +25,6 @@
import de.tum.cit.aet.artemis.programming.repository.hestia.ExerciseHintActivationRepository;
import de.tum.cit.aet.artemis.programming.repository.hestia.ExerciseHintRepository;
import de.tum.cit.aet.artemis.programming.repository.hestia.ProgrammingExerciseSolutionEntryRepository;
-import de.tum.cit.aet.artemis.programming.repository.hestia.ProgrammingExerciseTaskRepository;
import de.tum.cit.aet.artemis.programming.repository.hestia.TestwiseCoverageReportEntryRepository;
import de.tum.cit.aet.artemis.programming.repository.settings.IdeRepository;
import de.tum.cit.aet.artemis.programming.repository.settings.UserIdeMappingRepository;
@@ -38,6 +37,7 @@
import de.tum.cit.aet.artemis.programming.service.hestia.ExerciseHintService;
import de.tum.cit.aet.artemis.programming.service.hestia.ProgrammingExerciseTaskService;
import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseStudentParticipationTestRepository;
+import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTaskTestRepository;
import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestCaseTestRepository;
import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository;
import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingSubmissionTestRepository;
@@ -79,7 +79,7 @@ public abstract class AbstractProgrammingIntegrationIndependentTest extends Abst
protected ProgrammingExerciseStudentParticipationTestRepository programmingExerciseStudentParticipationRepository;
@Autowired
- protected ProgrammingExerciseTaskRepository taskRepository;
+ protected ProgrammingExerciseTaskTestRepository taskRepository;
@Autowired
protected ProgrammingExerciseTestCaseTestRepository testCaseRepository;
diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/test_repository/ProgrammingExerciseTaskTestRepository.java b/src/test/java/de/tum/cit/aet/artemis/programming/test_repository/ProgrammingExerciseTaskTestRepository.java
new file mode 100644
index 000000000000..7d79da2bdcb6
--- /dev/null
+++ b/src/test/java/de/tum/cit/aet/artemis/programming/test_repository/ProgrammingExerciseTaskTestRepository.java
@@ -0,0 +1,52 @@
+package de.tum.cit.aet.artemis.programming.test_repository;
+
+import java.util.Optional;
+import java.util.Set;
+
+import jakarta.validation.constraints.NotNull;
+
+import org.springframework.context.annotation.Primary;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import org.springframework.stereotype.Repository;
+
+import de.tum.cit.aet.artemis.core.exception.EntityNotFoundException;
+import de.tum.cit.aet.artemis.programming.domain.hestia.ProgrammingExerciseTask;
+import de.tum.cit.aet.artemis.programming.repository.hestia.ProgrammingExerciseTaskRepository;
+
+/**
+ * Spring Data repository for the ProgrammingExerciseTask entity.
+ */
+@Repository
+@Primary
+public interface ProgrammingExerciseTaskTestRepository extends ProgrammingExerciseTaskRepository {
+
+ Set findByExerciseId(Long exerciseId);
+
+ /**
+ * Gets a task with its programming exercise, test cases and solution entries of the test cases
+ *
+ * @param entryId The id of the task
+ * @return The task with the given ID if found
+ * @throws EntityNotFoundException If no task with the given ID was found
+ */
+ @NotNull
+ default ProgrammingExerciseTask findByIdWithTestCaseAndSolutionEntriesElseThrow(long entryId) throws EntityNotFoundException {
+ return getValueElseThrow(findByIdWithTestCaseAndSolutionEntries(entryId), entryId);
+ }
+
+ /**
+ * Gets a task with its programming exercise, test cases and solution entries of the test cases
+ *
+ * @param entryId The id of the task
+ * @return The task with the given ID
+ */
+ @Query("""
+ SELECT t
+ FROM ProgrammingExerciseTask t
+ LEFT JOIN FETCH t.testCases tc
+ LEFT JOIN FETCH tc.solutionEntries
+ WHERE t.id = :entryId
+ """)
+ Optional findByIdWithTestCaseAndSolutionEntries(@Param("entryId") long entryId);
+}
diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseTestService.java b/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseTestService.java
index 95ba2804b3b6..b8963f3decae 100644
--- a/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseTestService.java
+++ b/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseTestService.java
@@ -136,7 +136,6 @@
import de.tum.cit.aet.artemis.programming.repository.BuildPlanRepository;
import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseBuildConfigRepository;
import de.tum.cit.aet.artemis.programming.repository.StaticCodeAnalysisCategoryRepository;
-import de.tum.cit.aet.artemis.programming.repository.hestia.ProgrammingExerciseTaskRepository;
import de.tum.cit.aet.artemis.programming.service.AutomaticProgrammingExerciseCleanupService;
import de.tum.cit.aet.artemis.programming.service.GitService;
import de.tum.cit.aet.artemis.programming.service.JavaTemplateUpgradeService;
@@ -148,6 +147,7 @@
import de.tum.cit.aet.artemis.programming.service.vcs.VersionControlRepositoryPermission;
import de.tum.cit.aet.artemis.programming.service.vcs.VersionControlService;
import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseStudentParticipationTestRepository;
+import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTaskTestRepository;
import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestCaseTestRepository;
import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository;
import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingSubmissionTestRepository;
@@ -228,7 +228,7 @@ public class ProgrammingExerciseTestService {
private JavaTemplateUpgradeService javaTemplateUpgradeService;
@Autowired
- private ProgrammingExerciseTaskRepository programmingExerciseTaskRepository;
+ private ProgrammingExerciseTaskTestRepository programmingExerciseTaskRepository;
@Autowired
private ProgrammingExerciseTestCaseTestRepository programmingExerciseTestCaseRepository;
diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseUtilService.java b/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseUtilService.java
index e1571792335a..7aa1d99e1c8f 100644
--- a/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseUtilService.java
+++ b/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseUtilService.java
@@ -74,8 +74,8 @@
import de.tum.cit.aet.artemis.programming.repository.hestia.CodeHintRepository;
import de.tum.cit.aet.artemis.programming.repository.hestia.ExerciseHintRepository;
import de.tum.cit.aet.artemis.programming.repository.hestia.ProgrammingExerciseSolutionEntryRepository;
-import de.tum.cit.aet.artemis.programming.repository.hestia.ProgrammingExerciseTaskRepository;
import de.tum.cit.aet.artemis.programming.service.GitService;
+import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTaskTestRepository;
import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestCaseTestRepository;
import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingExerciseTestRepository;
import de.tum.cit.aet.artemis.programming.test_repository.ProgrammingSubmissionTestRepository;
@@ -145,7 +145,7 @@ public class ProgrammingExerciseUtilService {
private ExerciseHintRepository exerciseHintRepository;
@Autowired
- private ProgrammingExerciseTaskRepository programmingExerciseTaskRepository;
+ private ProgrammingExerciseTaskTestRepository programmingExerciseTaskRepository;
@Autowired
private ProgrammingExerciseSolutionEntryRepository solutionEntryRepository;
diff --git a/src/test/java/de/tum/cit/aet/artemis/shared/architecture/module/AbstractModuleRepositoryArchitectureTest.java b/src/test/java/de/tum/cit/aet/artemis/shared/architecture/module/AbstractModuleRepositoryArchitectureTest.java
index 6e3887654163..fdc272e7877d 100644
--- a/src/test/java/de/tum/cit/aet/artemis/shared/architecture/module/AbstractModuleRepositoryArchitectureTest.java
+++ b/src/test/java/de/tum/cit/aet/artemis/shared/architecture/module/AbstractModuleRepositoryArchitectureTest.java
@@ -20,6 +20,7 @@
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
@@ -48,6 +49,14 @@ void shouldBeNamedRepository() {
rule.allowEmptyShould(true).check(allClasses);
}
+ @Test
+ void shouldBeAnnotatedRepository() {
+ ArchRule rule = classesOfThisModuleThat().haveSimpleNameEndingWith("Repository").and().areInterfaces().should().beAnnotatedWith(Repository.class).orShould()
+ .beAnnotatedWith(NoRepositoryBean.class).because("repositories should be annotated with @Repository or @NoRepositoryBean.");
+ // allow empty should since some modules do not have repositories
+ rule.allowEmptyShould(true).check(allClasses);
+ }
+
@Test
void shouldBeInRepositoryPackage() {
ArchRule rule = classesOfThisModuleThat().areAnnotatedWith(Repository.class).should().resideInAPackage("..repository..")
diff --git a/src/test/javascript/spec/component/assessment-shared/assessment-header.component.spec.ts b/src/test/javascript/spec/component/assessment-shared/assessment-header.component.spec.ts
index a85e5e47bc16..aa0073961211 100644
--- a/src/test/javascript/spec/component/assessment-shared/assessment-header.component.spec.ts
+++ b/src/test/javascript/spec/component/assessment-shared/assessment-header.component.spec.ts
@@ -159,14 +159,14 @@ describe('AssessmentHeaderComponent', () => {
saveButtonSpan.nativeElement.click();
expect(component.save.emit).toHaveBeenCalledOnce();
- jest.spyOn(component.submit, 'emit');
+ jest.spyOn(component.onSubmit, 'emit');
submitButtonSpan.nativeElement.click();
- expect(component.submit.emit).toHaveBeenCalledOnce();
+ expect(component.onSubmit.emit).toHaveBeenCalledOnce();
const cancelButtonSpan = fixture.debugElement.query(By.css('[jhiTranslate$=cancel]'));
- jest.spyOn(component.cancel, 'emit');
+ jest.spyOn(component.onCancel, 'emit');
cancelButtonSpan.nativeElement.click();
- expect(component.cancel.emit).toHaveBeenCalledOnce();
+ expect(component.onCancel.emit).toHaveBeenCalledOnce();
});
it('should show override button when result is present', () => {
@@ -189,9 +189,9 @@ describe('AssessmentHeaderComponent', () => {
overrideAssessmentButtonSpan = fixture.debugElement.query(By.css('[jhiTranslate$=overrideAssessment]'));
expect(overrideAssessmentButtonSpan).toBeTruthy();
- jest.spyOn(component.submit, 'emit');
+ jest.spyOn(component.onSubmit, 'emit');
overrideAssessmentButtonSpan.nativeElement.click();
- expect(component.submit.emit).toHaveBeenCalledOnce();
+ expect(component.onSubmit.emit).toHaveBeenCalledOnce();
});
it('should show next submission if assessor or instructor, result is present and no complaint', () => {
@@ -345,7 +345,7 @@ describe('AssessmentHeaderComponent', () => {
const eventMock = new KeyboardEvent('keydown', { ctrlKey: true, key: 'Enter' });
const spyOnControlAndEnter = jest.spyOn(component, 'submitOnControlAndEnter');
- const submitSpy = jest.spyOn(component.submit, 'emit');
+ const submitSpy = jest.spyOn(component.onSubmit, 'emit');
document.dispatchEvent(eventMock);
expect(spyOnControlAndEnter).toHaveBeenCalledOnce();
@@ -362,7 +362,7 @@ describe('AssessmentHeaderComponent', () => {
const eventMock = new KeyboardEvent('keydown', { ctrlKey: true, key: 'Enter' });
const spyOnControlAndEnter = jest.spyOn(component, 'submitOnControlAndEnter');
- const submitSpy = jest.spyOn(component.submit, 'emit');
+ const submitSpy = jest.spyOn(component.onSubmit, 'emit');
document.dispatchEvent(eventMock);
expect(spyOnControlAndEnter).toHaveBeenCalledOnce();
diff --git a/src/test/javascript/spec/component/complaints/complaints-form.component.spec.ts b/src/test/javascript/spec/component/complaints/complaints-form.component.spec.ts
index c0e144ca8828..488c740be038 100644
--- a/src/test/javascript/spec/component/complaints/complaints-form.component.spec.ts
+++ b/src/test/javascript/spec/component/complaints/complaints-form.component.spec.ts
@@ -84,7 +84,7 @@ describe('ComplaintsFormComponent', () => {
it('should submit after complaint creation', () => {
const createMock = jest.spyOn(complaintService, 'create').mockReturnValue(of({} as EntityResponseType));
- const submitSpy = jest.spyOn(component.submit, 'emit');
+ const submitSpy = jest.spyOn(component.onSubmit, 'emit');
component.createComplaint();
expect(createMock).toHaveBeenCalledOnce();
expect(submitSpy).toHaveBeenCalledOnce();
@@ -93,7 +93,7 @@ describe('ComplaintsFormComponent', () => {
it('should throw unknown error after complaint creation', () => {
const createMock = jest.spyOn(complaintService, 'create').mockReturnValue(throwError(() => ({ status: 400 })));
- const submitSpy = jest.spyOn(component.submit, 'emit');
+ const submitSpy = jest.spyOn(component.onSubmit, 'emit');
const errorSpy = jest.spyOn(alertService, 'error');
component.createComplaint();
expect(createMock).toHaveBeenCalledOnce();
@@ -104,7 +104,7 @@ describe('ComplaintsFormComponent', () => {
it('should throw known error after complaint creation', () => {
const error = { error: { errorKey: 'tooManyComplaints' } } as HttpErrorResponse;
const createMock = jest.spyOn(complaintService, 'create').mockReturnValue(throwError(() => error));
- const submitSpy = jest.spyOn(component.submit, 'emit');
+ const submitSpy = jest.spyOn(component.onSubmit, 'emit');
const errorSpy = jest.spyOn(alertService, 'error');
const numberOfComplaints = 42;
component.maxComplaintsPerCourse = numberOfComplaints;
@@ -120,7 +120,7 @@ describe('ComplaintsFormComponent', () => {
component.exercise = courseExercise;
component.ngOnInit();
- const submitSpy = jest.spyOn(component.submit, 'emit');
+ const submitSpy = jest.spyOn(component.onSubmit, 'emit');
const errorSpy = jest.spyOn(alertService, 'error');
// 26 characters
component.complaintText = 'abcdefghijklmnopqrstuvwxyz';
diff --git a/src/test/javascript/spec/component/exercises/quiz/short-answer-question-util.service.spec.ts b/src/test/javascript/spec/component/exercises/quiz/short-answer-question-util.service.spec.ts
index da7e4949bbb3..47aeba9f7fd9 100644
--- a/src/test/javascript/spec/component/exercises/quiz/short-answer-question-util.service.spec.ts
+++ b/src/test/javascript/spec/component/exercises/quiz/short-answer-question-util.service.spec.ts
@@ -216,7 +216,7 @@ describe('ShortAnswerQuestionUtil', () => {
const originalTextParts2 = [['`random code`'], ['` some more code`', '[-spot 1]'], ['`last code paragraph`']];
const formattedTextParts2 = [
['random code
'],
- [' some more code
', '[-spot 1]
'],
+ [' some more code
', '[-spot 1]
'],
['last code paragraph
'],
];
expect(service.transformTextPartsIntoHTML(originalTextParts2)).toEqual(formattedTextParts2);
diff --git a/src/test/javascript/spec/component/lecture-unit/text-unit/text-unit.component.spec.ts b/src/test/javascript/spec/component/lecture-unit/text-unit/text-unit.component.spec.ts
index dc8ffa8669af..3326a8e91514 100644
--- a/src/test/javascript/spec/component/lecture-unit/text-unit/text-unit.component.spec.ts
+++ b/src/test/javascript/spec/component/lecture-unit/text-unit/text-unit.component.spec.ts
@@ -27,7 +27,7 @@ describe('TextUnitComponent', () => {
visibleToStudents: true,
};
- const exampleHtml = 'Sample Markdown
';
+ const exampleHtml = 'Sample Markdown
';
beforeEach(async () => {
await TestBed.configureTestingModule({
diff --git a/src/test/javascript/spec/component/overview/course-conversations/layout/conversation-thread-sidebar/conversation-thread-sidebar.component.spec.ts b/src/test/javascript/spec/component/overview/course-conversations/layout/conversation-thread-sidebar/conversation-thread-sidebar.component.spec.ts
index 691cb37f749b..d2de1b99462d 100644
--- a/src/test/javascript/spec/component/overview/course-conversations/layout/conversation-thread-sidebar/conversation-thread-sidebar.component.spec.ts
+++ b/src/test/javascript/spec/component/overview/course-conversations/layout/conversation-thread-sidebar/conversation-thread-sidebar.component.spec.ts
@@ -52,4 +52,16 @@ describe('ConversationThreadSidebarComponent', () => {
component.activeConversation = conversation;
expect(component.hasChannelModerationRights).toBe(hasModerationRights);
});
+
+ it('should set min and max width for the resizable thread section', () => {
+ const expandedThreadElement = fixture.debugElement.nativeElement.querySelector('.expanded-thread');
+ const minWidth = window.innerWidth * 0.3;
+ const maxWidth = window.innerWidth;
+
+ expandedThreadElement.style.width = `${minWidth}px`;
+ expect(parseFloat(expandedThreadElement.style.width)).toBeGreaterThanOrEqual(minWidth);
+
+ expandedThreadElement.style.width = `${maxWidth}px`;
+ expect(parseFloat(expandedThreadElement.style.width)).toBeLessThanOrEqual(maxWidth);
+ });
});
diff --git a/src/test/javascript/spec/component/shared/metis/posting-content/posting-content-part.component.spec.ts b/src/test/javascript/spec/component/shared/metis/posting-content/posting-content-part.component.spec.ts
index 81e073bf43a6..424567dd6518 100644
--- a/src/test/javascript/spec/component/shared/metis/posting-content/posting-content-part.component.spec.ts
+++ b/src/test/javascript/spec/component/shared/metis/posting-content/posting-content-part.component.spec.ts
@@ -94,7 +94,7 @@ describe('PostingContentPartComponent', () => {
expect(markdownRenderedTexts).toHaveLength(2);
// check that the paragraph right before the reference and the paragraph right after have the class `inline-paragraph`
expect(markdownRenderedTexts![0].innerHTML).toInclude('Be aware
');
- expect(markdownRenderedTexts![0].innerHTML).toInclude('I want to reference the following Post
'); // last paragraph before reference
+ expect(markdownRenderedTexts![0].innerHTML).toInclude('I want to reference the following Post
'); // last paragraph before reference
expect(markdownRenderedTexts![1].innerHTML).toInclude('in my content,
'); // first paragraph after reference
expect(markdownRenderedTexts![1].innerHTML).toInclude('does it actually work?
');
diff --git a/src/test/javascript/spec/helpers/sample/problemStatement.json b/src/test/javascript/spec/helpers/sample/problemStatement.json
index 125a7c25d5c9..6a9297f35013 100644
--- a/src/test/javascript/spec/helpers/sample/problemStatement.json
+++ b/src/test/javascript/spec/helpers/sample/problemStatement.json
@@ -7,8 +7,8 @@
"problemStatementBothFailedRendered": "\n- Implement Bubble Sort: artemisApp.editor.testStatusLabels.noResult
\nImplement the method performSort(List<Date>)
in the class BubbleSort
. Make sure to follow the Bubble Sort algorithm exactly. \n- Implement Merge Sort: artemisApp.editor.testStatusLabels.noResult
\nImplement the method performSort(List<Date>)
in the class MergeSort
. Make sure to follow the Merge Sort algorithm exactly. \n
\n",
"problemStatementBothFailedHtml": "\n- Implement Bubble Sort: artemisApp.editor.testStatusLabels.testFailing
\nImplement the method performSort(List<Date>)
in the class BubbleSort
. Make sure to follow the Bubble Sort algorithm exactly. \n- Implement Merge Sort: artemisApp.editor.testStatusLabels.testPassing
\nImplement the method performSort(List<Date>)
in the class MergeSort
. Make sure to follow the Merge Sort algorithm exactly. \n
\n",
"problemStatementBubbleSortFailsRendered": "\n- Implement Bubble Sort: artemisApp.editor.testStatusLabels.noResult
\nImplement the method performSort(List<Date>)
in the class BubbleSort
. Make sure to follow the Bubble Sort algorithm exactly. \n- Implement Merge Sort: artemisApp.editor.testStatusLabels.noResult
\nImplement the method performSort(List<Date>)
in the class MergeSort
. Make sure to follow the Merge Sort algorithm exactly. \n
\n",
- "problemStatementBubbleSortNotExecutedHtml": "\nImplement Bubble SortartemisApp.editor.testStatusLabels.totalTestsPassing: [{\"totalTests\":1,\"passedTests\":0}] \nImplement the method performSort(List<Date>)
in the class BubbleSort
. Make sure to follow the Bubble Sort algorithm exactly. \nImplement Merge SortartemisApp.editor.testStatusLabels.totalTestsPassing: [{\"totalTests\":1,\"passedTests\":1}] \nImplement the method performSort(List<Date>)
in the class MergeSort
. Make sure to follow the Merge Sort algorithm exactly. \n
",
+ "problemStatementBubbleSortNotExecutedHtml": "\nImplement Bubble SortartemisApp.editor.testStatusLabels.totalTestsPassing: [{\"totalTests\":1,\"passedTests\":0}] \nImplement the method performSort(List<Date>)
in the class BubbleSort
. Make sure to follow the Bubble Sort algorithm exactly. \nImplement Merge SortartemisApp.editor.testStatusLabels.totalTestsPassing: [{\"totalTests\":1,\"passedTests\":1}] \nImplement the method performSort(List<Date>)
in the class MergeSort
. Make sure to follow the Merge Sort algorithm exactly. \n
",
"problemStatementEmptySecondTask": "1. [task][Bubble Sort](1) \n Implement the method. \n 2. [task][Merge Sort]() \n Implement the method.",
- "problemStatementEmptySecondTaskNotExecutedHtml": "\nBubble SortartemisApp.editor.testStatusLabels.totalTestsPassing: [{\"totalTests\":1,\"passedTests\":1}] \nImplement the method. \nMerge SortartemisApp.editor.testStatusLabels.noTests \nImplement the method. \n
",
+ "problemStatementEmptySecondTaskNotExecutedHtml": "\nBubble SortartemisApp.editor.testStatusLabels.totalTestsPassing: [{\"totalTests\":1,\"passedTests\":1}] \nImplement the method. \nMerge SortartemisApp.editor.testStatusLabels.noTests \nImplement the method. \n
",
"problemStatementPlantUMLWithTest": "@startuml\nclass Policy {\n1)>+configure()\n2)>+testWithParenthesis()}\n@enduml"
}
diff --git a/src/test/javascript/spec/service/markdown.service.spec.ts b/src/test/javascript/spec/service/markdown.service.spec.ts
index 6473a2b6405d..d33f0113eea9 100644
--- a/src/test/javascript/spec/service/markdown.service.spec.ts
+++ b/src/test/javascript/spec/service/markdown.service.spec.ts
@@ -108,4 +108,32 @@ describe('Markdown Service', () => {
const safeMarkdownWithoutExtras = htmlForMarkdown(markdownString, [], [], []);
expect(safeMarkdownWithoutExtras).toBe('Will this render blue?');
});
+
+ describe('formulaCompatibilityPlugin', () => {
+ it.each(['This is a formula $$E=mc^2$$ in text.', '$$a_1$$ formula at front', 'formula at back $$a_2$$'])('converts block formulas to inline formulas', (input) => {
+ const result = htmlForMarkdown(input);
+ expect(result).toContain('');
+ expect(result).not.toContain('class="katex-block"');
+ });
+
+ it('does not convert block formulas without surrounding text', () => {
+ const result = htmlForMarkdown('$$E=mc^2$$');
+ expect(result).toContain('class="katex-block"');
+ expect(result).toContain('display="block"');
+ });
+
+ it('converts double-backslash LaTeX begin and end tags', () => {
+ const result = htmlForMarkdown('Here is some LaTeX: $$\\\\begin{equation}a^2 + b^2 = c^2\\\\end{equation}$$\n');
+ expect(result).toContain('');
+ expect(result).toContain('class="katex-html"');
+ });
+
+ it('handles multiple formulas in the same text', () => {
+ const result = htmlForMarkdown('First formula $$a^2 + b^2 = c^2$$ and second formula $$E=mc^2$$.');
+ const formulaCount = (result.match(/class="katex"/g) || []).length;
+ expect(formulaCount).toBe(2);
+ expect(result).not.toContain('class="katex-block"');
+ expect(result).not.toContain('display="block"');
+ });
+ });
});