diff --git a/src/schedule-and-details/introducing-section/contentTypeUtils.test.ts b/src/schedule-and-details/introducing-section/contentTypeUtils.test.ts new file mode 100644 index 0000000000..c818a1827f --- /dev/null +++ b/src/schedule-and-details/introducing-section/contentTypeUtils.test.ts @@ -0,0 +1,234 @@ +/** + * Tests for content type detection utilities + */ +import { containsHtml, determineEditorType, type EditorType } from './contentTypeUtils'; + +describe('contentTypeUtils', () => { + describe('containsHtml', () => { + describe('should return false for non-HTML content', () => { + test.each([ + ['empty string', ''], + ['null', null], + ['plain text', 'This is just plain text'], + ['text with special characters', 'Text with @#$%^&*()'], + ['text with quotes', 'Text with "quotes" and \'apostrophes\''], + ['text with newlines', 'Line 1\nLine 2\nLine 3'], + ['text with angle brackets but not HTML', '5 < 10 and 10 > 5'], + ['mathematical expressions', 'x = y < z > w'], + ] as const)('for %s', (_description: string, input: string | null) => { + expect(containsHtml(input)).toBe(false); + }); + }); + + describe('should return true for HTML content', () => { + test.each([ + // Basic HTML tags + ['simple paragraph', '

Hello world

'], + ['heading tag', '

Title

'], + ['div element', '
Content
'], + ['span element', 'Text'], + ['self-closing tag', '
'], + ['self-closing without space', '
'], + + // HTML tags with attributes + ['tag with class', '

Text

'], + ['tag with id', '
Text
'], + ['tag with multiple attributes', 'Link'], + + // Mixed case HTML + ['uppercase tag', '

Paragraph

'], + ['mixed case tag', '
Content
'], + + // HTML with content + ['nested tags', '

Nested paragraph

'], + ['multiple tags', '

Title

Paragraph

'], + ['formatting tags', 'Text with bold and italic'], + + // Complex HTML structures + ['list structure', ''], + ['table structure', '
Cell
'], + ['form elements', ''], + ['image tag', 'Image'], + + // HTML entities + ['named entities', 'Price: $100 & free shipping'], + ['more entities', 'Copyright © 2024 – All rights reserved'], + ['quotes entity', 'He said "Hello" to me'], + ['numeric entities', 'Special char: € ©'], + + // HTML with text content + ['HTML in mixed content', 'Introduction:

This is the main content.

End.'], + ['multiple entities', 'A & B < C > D'], + + // Edge cases + ['unclosed tag', '

Unclosed paragraph'], + ['tag with newlines', '

\nMultiline\ncontent\n

'], + ] as const)('for %s', (_description: string, input: string) => { + expect(containsHtml(input)).toBe(true); + }); + }); + + describe('edge cases', () => { + test('should handle very long content', () => { + const longText = 'a'.repeat(10000); + expect(containsHtml(longText)).toBe(false); + + const longHtml = `

${longText}

`; + expect(containsHtml(longHtml)).toBe(true); + }); + + test('should handle content with only whitespace', () => { + expect(containsHtml(' \n\t ')).toBe(false); + }); + }); + }); + + describe('determineEditorType', () => { + describe('should return "text" for non-HTML content', () => { + test.each([ + ['empty string', ''], + ['null', null], + ['plain text', 'This is just plain text content'], + ['long plain text', 'Lorem ipsum '.repeat(100)], + ['text with special chars', 'Email: test@example.com, Phone: (555) 123-4567'], + ['mathematical content', '2 + 2 = 4, x < y, a > b'], + ['code-like content', 'function() { return value < threshold; }'], + ] as const)('for %s', (_description: string, input: string | null) => { + expect(determineEditorType(input)).toBe('text'); + }); + }); + + describe('should return "html" for HTML content', () => { + test.each([ + // Simple HTML + ['basic paragraph', '

Simple paragraph

'], + ['heading', '

Section Title

'], + ['formatted text', 'Text with bold formatting'], + + // Complex HTML structures + ['nested elements', '

Title

Content

'], + ['lists', ''], + ['tables', '
Cell 1Cell 2
'], + ['links and images', 'Image'], + + // Course content examples + ['course overview', '

Course Overview

This course covers...

'], + ['about sidebar', ''], + + // HTML entities + ['content with entities', 'Price: $100 & includes shipping – 50% off!'], + ['mixed content', 'Introduction

Main content with © symbol

conclusion'], + ] as const)('for %s', (_description: string, input: string) => { + expect(determineEditorType(input)).toBe('html'); + }); + }); + + describe('integration scenarios', () => { + test('should handle real course overview content', () => { + const courseOverview = ` +
+

Introduction to Computer Science

+

This course provides a comprehensive introduction to computer science concepts.

+

What You'll Learn:

+ +

Prerequisites: Basic mathematics knowledge

+

Duration: 12 weeks

+
+ `; + + expect(containsHtml(courseOverview)).toBe(true); + expect(determineEditorType(courseOverview)).toBe('html'); + }); + + test('should handle sidebar HTML content', () => { + const sidebarHtml = ` +
+

Course Information

+

Instructor: Dr. Smith

+

Credits: 3

+

Format: Online & In-person

+ Download Syllabus +
+ `; + + expect(containsHtml(sidebarHtml)).toBe(true); + expect(determineEditorType(sidebarHtml)).toBe('html'); + }); + + test('should handle plain text course descriptions', () => { + const plainDescription = 'A beginner-friendly course covering the basics of programming. No prior experience required.'; + + expect(containsHtml(plainDescription)).toBe(false); + expect(determineEditorType(plainDescription)).toBe('text'); + }); + + test('should handle empty or minimal content', () => { + expect(determineEditorType('')).toBe('text'); + expect(determineEditorType(' ')).toBe('text'); + expect(determineEditorType(null as any)).toBe('text'); + expect(determineEditorType(undefined as any)).toBe('text'); + }); + }); + + describe('type safety', () => { + test('should return correct EditorType', () => { + const result: EditorType = determineEditorType('

Test

'); + expect(result).toBe('html'); + + const result2: EditorType = determineEditorType('Plain text'); + expect(result2).toBe('text'); + }); + }); + + describe('performance considerations', () => { + test('should handle very large content efficiently', () => { + const largeContent = 'This is a large text content. '.repeat(1000); + const start = Date.now(); + const result = determineEditorType(largeContent); + const end = Date.now(); + + expect(result).toBe('text'); + expect(end - start).toBeLessThan(100); // Should complete in under 100ms + }); + + test('should handle large HTML content efficiently', () => { + const largeHtml = `
${'

Paragraph content.

'.repeat(1000)}
`; + const start = Date.now(); + const result = determineEditorType(largeHtml); + const end = Date.now(); + + expect(result).toBe('html'); + expect(end - start).toBeLessThan(100); // Should complete in under 100ms + }); + }); + }); + + describe('function integration', () => { + test('containsHtml and determineEditorType should be consistent', () => { + const testCases: Array = [ + 'Plain text', + '

HTML content

', + 'Text with & entities', + '', + null, + undefined, + '

Complex

HTML

', + ]; + + testCases.forEach((content) => { + const hasHtml = containsHtml(content as any); + const editorType = determineEditorType(content as any); + + if (hasHtml) { + expect(editorType).toBe('html'); + } else { + expect(editorType).toBe('text'); + } + }); + }); + }); +}); diff --git a/src/schedule-and-details/introducing-section/contentTypeUtils.ts b/src/schedule-and-details/introducing-section/contentTypeUtils.ts new file mode 100644 index 0000000000..31ada1498c --- /dev/null +++ b/src/schedule-and-details/introducing-section/contentTypeUtils.ts @@ -0,0 +1,46 @@ +/** + * Utility functions for detecting content type and + * determining appropriate editor type for TinyMCE editor + */ + +// Define the supported editor types +export type EditorType = 'text' | 'html'; + +/** + * Detects if content contains HTML tags + * @param content - The content to analyze + * @returns True if content contains HTML tags + */ +export const containsHtml = (content: string | null): boolean => { + if (!content) { + return false; + } + + // Check for common HTML patterns + const htmlPatterns: RegExp[] = [ + /<\/?[a-z][\s\S]*>/i, // HTML tags + /&[a-z]+;/i, // HTML entities + /&#\d+;/, // Numeric entities + ]; + + return htmlPatterns.some((pattern) => pattern.test(content)); +}; + +/** + * Determines the appropriate editor type based on content analysis + * @param content - The content to analyze + * @returns The recommended editor type ('text' or 'html') + */ +export const determineEditorType = (content: string | null): EditorType => { + if (!content) { + return 'text'; + } + + // If content contains HTML, use html editor for better HTML editing + if (containsHtml(content)) { + return 'html'; + } + + // For plain text content, use text editor + return 'text'; +}; diff --git a/src/schedule-and-details/introducing-section/index.jsx b/src/schedule-and-details/introducing-section/index.jsx index 32e8e21c55..71e7bdccd7 100644 --- a/src/schedule-and-details/introducing-section/index.jsx +++ b/src/schedule-and-details/introducing-section/index.jsx @@ -11,6 +11,7 @@ import { WysiwygEditor } from '../../generic/WysiwygEditor'; import SectionSubHeader from '../../generic/section-sub-header'; import IntroductionVideo from './introduction-video'; import ExtendedCourseDetails from './extended-course-details'; +import { determineEditorType } from './contentTypeUtils'; import messages from './messages'; const IntroducingSection = ({ @@ -112,6 +113,7 @@ const IntroducingSection = ({ {intl.formatMessage(messages.courseOverviewLabel)} onChange(value, 'overview')} /> {overviewHelpText} @@ -121,6 +123,7 @@ const IntroducingSection = ({ {intl.formatMessage(messages.courseAboutSidebarLabel)} onChange(value, 'aboutSidebarHtml')} /> {aboutSidebarHelpText}