From 0f2308a13275dfe4eb1a65aea15491879d6cd1e5 Mon Sep 17 00:00:00 2001 From: Michael Czechowski Date: Tue, 13 May 2025 21:08:18 +0200 Subject: [PATCH] style: run format first time --- package.json | 72 ++--- src/app.js | 569 +++++++++++++++++------------------ src/config/lessons.js | 97 +++--- src/helpers/renderer.js | 109 +++---- src/helpers/validator.js | 210 ++++++------- src/impl/LessonEngine.js | 354 +++++++++++----------- src/index.html | 160 +++++----- src/main.css | 460 ++++++++++++++-------------- tests/setup.js | 62 ++-- tests/unit/lessons.test.js | 288 +++++++++--------- tests/unit/renderer.test.js | 260 ++++++++-------- tests/unit/validator.test.js | 390 ++++++++++++------------ vite.config.js | 26 +- vitest.config.js | 34 +-- 14 files changed, 1518 insertions(+), 1573 deletions(-) diff --git a/package.json b/package.json index 96aebb7..859c76d 100644 --- a/package.json +++ b/package.json @@ -1,38 +1,38 @@ { - "name": "code-crispies", - "version": "1.0.0", - "description": "An interactive platform for learning CSS through practical challenges", - "type": "module", - "scripts": { - "start": "npm run dev", - "dev": "vite", - "build": "vite build", - "preview": "vite preview", - "test": "vitest run", - "test.watch": "vitest watch", - "test.coverage": "vitest run --coverage", - "format": "prettier --write src/ tests/ package.json vite.config.js vitest.config.js", - "format.lessons": "prettier --write lessons/*.json" - }, - "keywords": [ - "css", - "html", - "learning", - "interactive", - "education" - ], - "author": "Michael Czechowski ", - "license": "Copyright Michael Czechowski 2025", - "devDependencies": { - "@testing-library/dom": "^10.4.0", - "@testing-library/jest-dom": "^6.6.3", - "@vitest/coverage-v8": "^3.1.3", - "jsdom": "^26.1.0", - "prettier": "^3.5.3", - "vite": "^6.3.5", - "vitest": "^3.1.3" - }, - "dependencies": { - "whatwg-fetch": "^3.6.20" - } + "name": "code-crispies", + "version": "1.0.0", + "description": "An interactive platform for learning CSS through practical challenges", + "type": "module", + "scripts": { + "start": "npm run dev", + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "test": "vitest run", + "test.watch": "vitest watch", + "test.coverage": "vitest run --coverage", + "format": "prettier --write src/ tests/ package.json vite.config.js vitest.config.js", + "format.lessons": "prettier --write lessons/*.json" + }, + "keywords": [ + "css", + "html", + "learning", + "interactive", + "education" + ], + "author": "Michael Czechowski ", + "license": "Copyright Michael Czechowski 2025", + "devDependencies": { + "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.6.3", + "@vitest/coverage-v8": "^3.1.3", + "jsdom": "^26.1.0", + "prettier": "^3.5.3", + "vite": "^6.3.5", + "vitest": "^3.1.3" + }, + "dependencies": { + "whatwg-fetch": "^3.6.20" + } } diff --git a/src/app.js b/src/app.js index cc3e366..b984e1a 100644 --- a/src/app.js +++ b/src/app.js @@ -1,39 +1,39 @@ -import { LessonEngine } from './impl/LessonEngine.js'; -import { renderLesson, renderModuleList, renderLevelIndicator, showFeedback } from './helpers/renderer.js'; -import { validateUserCode } from './helpers/validator.js'; -import { loadModules } from './config/lessons.js'; +import { LessonEngine } from "./impl/LessonEngine.js"; +import { renderLesson, renderModuleList, renderLevelIndicator, showFeedback } from "./helpers/renderer.js"; +import { validateUserCode } from "./helpers/validator.js"; +import { loadModules } from "./config/lessons.js"; // Main Application state const state = { - currentModule: null, - currentLessonIndex: 0, - modules: [], - userProgress: {}, // Format: { moduleId: { completed: [0, 2, 3], current: 4 } } + currentModule: null, + currentLessonIndex: 0, + modules: [], + userProgress: {} // Format: { moduleId: { completed: [0, 2, 3], current: 4 } } }; // DOM elements const elements = { - moduleList: document.querySelector('.module-list'), - lessonTitle: document.getElementById('lesson-title'), - lessonDescription: document.getElementById('lesson-description'), - taskInstruction: document.getElementById('task-instruction'), - previewArea: document.getElementById('preview-area'), - editorPrefix: document.getElementById('editor-prefix'), - codeInput: document.getElementById('code-input'), - editorSuffix: document.getElementById('editor-suffix'), - prevBtn: document.getElementById('prev-btn'), - nextBtn: document.getElementById('next-btn'), - runBtn: document.getElementById('run-btn'), - levelIndicator: document.getElementById('level-indicator'), - modalContainer: document.getElementById('modal-container'), - modalTitle: document.getElementById('modal-title'), - modalContent: document.getElementById('modal-content'), - modalClose: document.getElementById('modal-close'), - moduleSelectorBtn: document.getElementById('module-selector-btn'), - resetBtn: document.getElementById('reset-btn'), - helpBtn: document.getElementById('help-btn'), - lessonContainer: document.querySelector('.lesson-container'), - editorContent: document.querySelector('.editor-content') + moduleList: document.querySelector(".module-list"), + lessonTitle: document.getElementById("lesson-title"), + lessonDescription: document.getElementById("lesson-description"), + taskInstruction: document.getElementById("task-instruction"), + previewArea: document.getElementById("preview-area"), + editorPrefix: document.getElementById("editor-prefix"), + codeInput: document.getElementById("code-input"), + editorSuffix: document.getElementById("editor-suffix"), + prevBtn: document.getElementById("prev-btn"), + nextBtn: document.getElementById("next-btn"), + runBtn: document.getElementById("run-btn"), + levelIndicator: document.getElementById("level-indicator"), + modalContainer: document.getElementById("modal-container"), + modalTitle: document.getElementById("modal-title"), + modalContent: document.getElementById("modal-content"), + modalClose: document.getElementById("modal-close"), + moduleSelectorBtn: document.getElementById("module-selector-btn"), + resetBtn: document.getElementById("reset-btn"), + helpBtn: document.getElementById("help-btn"), + lessonContainer: document.querySelector(".lesson-container"), + editorContent: document.querySelector(".editor-content") }; // Initialize the lesson engine @@ -41,61 +41,61 @@ const lessonEngine = new LessonEngine(); // Load user progress from localStorage function loadUserProgress() { - const savedProgress = localStorage.getItem('codeCrispiesProgress'); - if (savedProgress) { - state.userProgress = JSON.parse(savedProgress); - } + const savedProgress = localStorage.getItem("codeCrispiesProgress"); + if (savedProgress) { + state.userProgress = JSON.parse(savedProgress); + } } // Save user progress to localStorage function saveUserProgress() { - localStorage.setItem('codeCrispiesProgress', JSON.stringify(state.userProgress)); + localStorage.setItem("codeCrispiesProgress", JSON.stringify(state.userProgress)); } // Initialize the module list async function initializeModules() { - try { - state.modules = await loadModules(); - renderModuleList(elements.moduleList, state.modules, selectModule); + try { + state.modules = await loadModules(); + renderModuleList(elements.moduleList, state.modules, selectModule); - // Select the first module or the last one user was on - const lastModuleId = localStorage.getItem('lastModuleId'); - if (lastModuleId && state.modules.find(m => m.id === lastModuleId)) { - selectModule(lastModuleId); - } else if (state.modules.length > 0) { - selectModule(state.modules[0].id); - } + // Select the first module or the last one user was on + const lastModuleId = localStorage.getItem("lastModuleId"); + if (lastModuleId && state.modules.find((m) => m.id === lastModuleId)) { + selectModule(lastModuleId); + } else if (state.modules.length > 0) { + selectModule(state.modules[0].id); + } - // Update progress indicator on module selector button - updateModuleSelectorButtonProgress(); - } catch (error) { - console.error('Failed to load modules:', error); - elements.lessonDescription.textContent = 'Failed to load modules. Please refresh the page.'; - } + // Update progress indicator on module selector button + updateModuleSelectorButtonProgress(); + } catch (error) { + console.error("Failed to load modules:", error); + elements.lessonDescription.textContent = "Failed to load modules. Please refresh the page."; + } } // Update progress indicator on module selector button function updateModuleSelectorButtonProgress() { - if (!state.modules.length) return; + if (!state.modules.length) return; - // Calculate overall progress across all modules - let totalLessons = 0; - let totalCompleted = 0; + // Calculate overall progress across all modules + let totalLessons = 0; + let totalCompleted = 0; - state.modules.forEach(module => { - totalLessons += module.lessons.length; - const progress = state.userProgress[module.id]; - if (progress && progress.completed) { - totalCompleted += progress.completed.length; - } - }); + state.modules.forEach((module) => { + totalLessons += module.lessons.length; + const progress = state.userProgress[module.id]; + if (progress && progress.completed) { + totalCompleted += progress.completed.length; + } + }); - const percentComplete = totalLessons > 0 ? Math.round((totalCompleted / totalLessons) * 100) : 0; + const percentComplete = totalLessons > 0 ? Math.round((totalCompleted / totalLessons) * 100) : 0; - // Create progress indicator - const progressBar = document.createElement('div'); - progressBar.className = 'progress-indicator'; - progressBar.style.cssText = ` + // Create progress indicator + const progressBar = document.createElement("div"); + progressBar.className = "progress-indicator"; + progressBar.style.cssText = ` position: absolute; bottom: 0; left: 0; @@ -105,211 +105,206 @@ function updateModuleSelectorButtonProgress() { border-radius: 0 3px 3px 0; `; - // Add progress percentage text - elements.moduleSelectorBtn.innerHTML = `Progress ${percentComplete}%`; - elements.moduleSelectorBtn.style.position = 'relative'; + // Add progress percentage text + elements.moduleSelectorBtn.innerHTML = `Progress ${percentComplete}%`; + elements.moduleSelectorBtn.style.position = "relative"; - // Remove any existing progress bar before adding new one - const existingBar = elements.moduleSelectorBtn.querySelector('.progress-indicator'); - if (existingBar) { - existingBar.remove(); - } + // Remove any existing progress bar before adding new one + const existingBar = elements.moduleSelectorBtn.querySelector(".progress-indicator"); + if (existingBar) { + existingBar.remove(); + } - elements.moduleSelectorBtn.appendChild(progressBar); + elements.moduleSelectorBtn.appendChild(progressBar); } // Select a module function selectModule(moduleId) { - const selectedModule = state.modules.find(module => module.id === moduleId); - if (!selectedModule) return; + const selectedModule = state.modules.find((module) => module.id === moduleId); + if (!selectedModule) return; - state.currentModule = selectedModule; + state.currentModule = selectedModule; - // Update module list UI - const moduleItems = elements.moduleList.querySelectorAll('.module-list-item'); - moduleItems.forEach(item => { - item.classList.remove('active'); - if (item.dataset.moduleId === moduleId) { - item.classList.add('active'); - } - }); + // Update module list UI + const moduleItems = elements.moduleList.querySelectorAll(".module-list-item"); + moduleItems.forEach((item) => { + item.classList.remove("active"); + if (item.dataset.moduleId === moduleId) { + item.classList.add("active"); + } + }); - // Load user progress for this module - if (!state.userProgress[moduleId]) { - state.userProgress[moduleId] = { completed: [], current: 0 }; - } + // Load user progress for this module + if (!state.userProgress[moduleId]) { + state.userProgress[moduleId] = { completed: [], current: 0 }; + } - state.currentLessonIndex = state.userProgress[moduleId].current || 0; - loadCurrentLesson(); + state.currentLessonIndex = state.userProgress[moduleId].current || 0; + loadCurrentLesson(); - // Save the last selected module - localStorage.setItem('lastModuleId', moduleId); + // Save the last selected module + localStorage.setItem("lastModuleId", moduleId); - // Reset any success indicators - resetSuccessIndicators(); + // Reset any success indicators + resetSuccessIndicators(); } // Reset success indicators function resetSuccessIndicators() { - elements.lessonContainer.classList.remove('success-highlight'); - elements.lessonTitle.classList.remove('success-text'); - const headings = elements.lessonContainer.querySelectorAll('h2, h3, h4'); - headings.forEach(heading => heading.classList.remove('success-text')); + elements.lessonContainer.classList.remove("success-highlight"); + elements.lessonTitle.classList.remove("success-text"); + const headings = elements.lessonContainer.querySelectorAll("h2, h3, h4"); + headings.forEach((heading) => heading.classList.remove("success-text")); } // Load the current lesson function loadCurrentLesson() { - if (!state.currentModule || !state.currentModule.lessons) { - return; - } + if (!state.currentModule || !state.currentModule.lessons) { + return; + } - // Make sure lesson index is in bounds - if (state.currentLessonIndex >= state.currentModule.lessons.length) { - state.currentLessonIndex = state.currentModule.lessons.length - 1; - } else if (state.currentLessonIndex < 0) { - state.currentLessonIndex = 0; - } + // Make sure lesson index is in bounds + if (state.currentLessonIndex >= state.currentModule.lessons.length) { + state.currentLessonIndex = state.currentModule.lessons.length - 1; + } else if (state.currentLessonIndex < 0) { + state.currentLessonIndex = 0; + } - const lesson = state.currentModule.lessons[state.currentLessonIndex]; - lessonEngine.setLesson(lesson); + const lesson = state.currentModule.lessons[state.currentLessonIndex]; + lessonEngine.setLesson(lesson); - // Reset any success indicators - resetSuccessIndicators(); + // Reset any success indicators + resetSuccessIndicators(); - // Update UI - renderLesson( - elements.lessonTitle, - elements.lessonDescription, - elements.taskInstruction, - elements.previewArea, - elements.editorPrefix, - elements.codeInput, - elements.editorSuffix, - lesson - ); + // Update UI + renderLesson( + elements.lessonTitle, + elements.lessonDescription, + elements.taskInstruction, + elements.previewArea, + elements.editorPrefix, + elements.codeInput, + elements.editorSuffix, + lesson + ); - // Update level indicator - renderLevelIndicator( - elements.levelIndicator, - state.currentLessonIndex + 1, - state.currentModule.lessons.length - ); + // Update level indicator + renderLevelIndicator(elements.levelIndicator, state.currentLessonIndex + 1, state.currentModule.lessons.length); - // Update navigation buttons - updateNavigationButtons(); + // Update navigation buttons + updateNavigationButtons(); - // Save current progress - state.userProgress[state.currentModule.id].current = state.currentLessonIndex; - saveUserProgress(); + // Save current progress + state.userProgress[state.currentModule.id].current = state.currentLessonIndex; + saveUserProgress(); - // Update progress indicator on module selector button - updateModuleSelectorButtonProgress(); + // Update progress indicator on module selector button + updateModuleSelectorButtonProgress(); - // Focus on the code editor by default - elements.codeInput.focus(); + // Focus on the code editor by default + elements.codeInput.focus(); } // Update navigation buttons state function updateNavigationButtons() { - elements.prevBtn.disabled = state.currentLessonIndex === 0; - elements.nextBtn.disabled = !state.currentModule || - state.currentLessonIndex === state.currentModule.lessons.length - 1; + elements.prevBtn.disabled = state.currentLessonIndex === 0; + elements.nextBtn.disabled = !state.currentModule || state.currentLessonIndex === state.currentModule.lessons.length - 1; - // Style changes for disabled buttons - if (elements.prevBtn.disabled) { - elements.prevBtn.classList.add('btn-disabled'); - } else { - elements.prevBtn.classList.remove('btn-disabled'); - } + // Style changes for disabled buttons + if (elements.prevBtn.disabled) { + elements.prevBtn.classList.add("btn-disabled"); + } else { + elements.prevBtn.classList.remove("btn-disabled"); + } - if (elements.nextBtn.disabled) { - elements.nextBtn.classList.add('btn-disabled'); - } else { - elements.nextBtn.classList.remove('btn-disabled'); - } + if (elements.nextBtn.disabled) { + elements.nextBtn.classList.add("btn-disabled"); + } else { + elements.nextBtn.classList.remove("btn-disabled"); + } } // Go to the next lesson function nextLesson() { - if (!state.currentModule) return; + if (!state.currentModule) return; - if (state.currentLessonIndex < state.currentModule.lessons.length - 1) { - state.currentLessonIndex++; - loadCurrentLesson(); - } + if (state.currentLessonIndex < state.currentModule.lessons.length - 1) { + state.currentLessonIndex++; + loadCurrentLesson(); + } } // Go to the previous lesson function prevLesson() { - if (state.currentLessonIndex > 0) { - state.currentLessonIndex--; - loadCurrentLesson(); - } + if (state.currentLessonIndex > 0) { + state.currentLessonIndex--; + loadCurrentLesson(); + } } // Run the user code function runCode() { - const userCode = elements.codeInput.value; - const lesson = state.currentModule.lessons[state.currentLessonIndex]; + const userCode = elements.codeInput.value; + const lesson = state.currentModule.lessons[state.currentLessonIndex]; - const validationResult = validateUserCode(userCode, lesson); + const validationResult = validateUserCode(userCode, lesson); - if (validationResult.isValid) { - // Mark lesson as completed - const moduleProgress = state.userProgress[state.currentModule.id]; - if (!moduleProgress.completed.includes(state.currentLessonIndex)) { - moduleProgress.completed.push(state.currentLessonIndex); - saveUserProgress(); - updateModuleSelectorButtonProgress(); - } + if (validationResult.isValid) { + // Mark lesson as completed + const moduleProgress = state.userProgress[state.currentModule.id]; + if (!moduleProgress.completed.includes(state.currentLessonIndex)) { + moduleProgress.completed.push(state.currentLessonIndex); + saveUserProgress(); + updateModuleSelectorButtonProgress(); + } - // Show success feedback with visual indicators - showFeedback(true, validationResult.message || 'Great job! Your code works correctly.'); + // Show success feedback with visual indicators + showFeedback(true, validationResult.message || "Great job! Your code works correctly."); - // Add success visual indicators - elements.lessonContainer.classList.add('success-highlight'); - elements.lessonTitle.classList.add('success-text'); - const headings = elements.lessonContainer.querySelectorAll('h3, h4'); - headings.forEach(heading => heading.classList.add('success-text')); + // Add success visual indicators + elements.lessonContainer.classList.add("success-highlight"); + elements.lessonTitle.classList.add("success-text"); + const headings = elements.lessonContainer.querySelectorAll("h3, h4"); + headings.forEach((heading) => heading.classList.add("success-text")); - // Apply the code to see the result - lessonEngine.applyUserCode(userCode); + // Apply the code to see the result + lessonEngine.applyUserCode(userCode); - // Enable the next button if not already on the last lesson - if (state.currentLessonIndex < state.currentModule.lessons.length - 1) { - elements.nextBtn.disabled = false; - elements.nextBtn.classList.remove('btn-disabled'); - } - } else { - // Reset any success indicators - resetSuccessIndicators(); + // Enable the next button if not already on the last lesson + if (state.currentLessonIndex < state.currentModule.lessons.length - 1) { + elements.nextBtn.disabled = false; + elements.nextBtn.classList.remove("btn-disabled"); + } + } else { + // Reset any success indicators + resetSuccessIndicators(); - // Show error feedback (with friendly message) - showFeedback(false, validationResult.message || 'Not quite there yet! Let\'s try again.'); - } + // Show error feedback (with friendly message) + showFeedback(false, validationResult.message || "Not quite there yet! Let's try again."); + } } // Show the module selector modal function showModuleSelector() { - elements.modalTitle.textContent = 'Select a Module'; + elements.modalTitle.textContent = "Select a Module"; - // Create module buttons - const moduleButtons = state.modules.map(module => { - const button = document.createElement('button'); - button.classList.add('btn', 'module-button'); - button.style.display = 'block'; - button.style.width = '100%'; - button.style.marginBottom = '10px'; - button.style.padding = '15px'; - button.style.textAlign = 'left'; + // Create module buttons + const moduleButtons = state.modules.map((module) => { + const button = document.createElement("button"); + button.classList.add("btn", "module-button"); + button.style.display = "block"; + button.style.width = "100%"; + button.style.marginBottom = "10px"; + button.style.padding = "15px"; + button.style.textAlign = "left"; - // Add completion status - const progress = state.userProgress[module.id]; - const completedCount = progress ? progress.completed.length : 0; - const totalLessons = module.lessons.length; - const percentComplete = Math.round((completedCount / totalLessons) * 100); + // Add completion status + const progress = state.userProgress[module.id]; + const completedCount = progress ? progress.completed.length : 0; + const totalLessons = module.lessons.length; + const percentComplete = Math.round((completedCount / totalLessons) * 100); - button.innerHTML = ` + button.innerHTML = ` ${module.title}
${module.description} @@ -322,29 +317,29 @@ function showModuleSelector() {
`; - button.addEventListener('click', () => { - selectModule(module.id); - closeModal(); - }); + button.addEventListener("click", () => { + selectModule(module.id); + closeModal(); + }); - return button; - }); + return button; + }); - // Clear and update modal content - elements.modalContent.innerHTML = ''; - moduleButtons.forEach(button => { - elements.modalContent.appendChild(button); - }); + // Clear and update modal content + elements.modalContent.innerHTML = ""; + moduleButtons.forEach((button) => { + elements.modalContent.appendChild(button); + }); - // Show the modal - elements.modalContainer.classList.remove('hidden'); + // Show the modal + elements.modalContainer.classList.remove("hidden"); } // Show help modal function showHelp() { - elements.modalTitle.textContent = 'Help'; + elements.modalTitle.textContent = "Help"; - elements.modalContent.innerHTML = ` + elements.modalContent.innerHTML = `

How to Use Code Crispies

Code Crispies is an interactive platform for learning CSS through practical exercises.

@@ -378,14 +373,14 @@ function showHelp() { `; - elements.modalContainer.classList.remove('hidden'); + elements.modalContainer.classList.remove("hidden"); } // Reset user progress function resetProgress() { - elements.modalTitle.textContent = 'Reset Progress'; + elements.modalTitle.textContent = "Reset Progress"; - elements.modalContent.innerHTML = ` + elements.modalContent.innerHTML = `

Are you sure you want to reset all your progress? This cannot be undone.

@@ -393,94 +388,94 @@ function resetProgress() {
`; - document.getElementById('cancel-reset').addEventListener('click', closeModal); - document.getElementById('confirm-reset').addEventListener('click', () => { - localStorage.removeItem('codeCrispiesProgress'); - localStorage.removeItem('lastModuleId'); - state.userProgress = {}; - closeModal(); + document.getElementById("cancel-reset").addEventListener("click", closeModal); + document.getElementById("confirm-reset").addEventListener("click", () => { + localStorage.removeItem("codeCrispiesProgress"); + localStorage.removeItem("lastModuleId"); + state.userProgress = {}; + closeModal(); - // Reload the current module - if (state.currentModule) { - const currentModuleId = state.currentModule.id; - selectModule(currentModuleId); - } else if (state.modules.length > 0) { - selectModule(state.modules[0].id); - } + // Reload the current module + if (state.currentModule) { + const currentModuleId = state.currentModule.id; + selectModule(currentModuleId); + } else if (state.modules.length > 0) { + selectModule(state.modules[0].id); + } - // Update progress indicator - updateModuleSelectorButtonProgress(); - }); + // Update progress indicator + updateModuleSelectorButtonProgress(); + }); - elements.modalContainer.classList.remove('hidden'); + elements.modalContainer.classList.remove("hidden"); } // Close the modal function closeModal() { - elements.modalContainer.classList.add('hidden'); + elements.modalContainer.classList.add("hidden"); } // Handle clicks in the code editor to focus the input function handleEditorClick() { - elements.codeInput.focus(); + elements.codeInput.focus(); - // Add a temporary highlight class to show where the cursor is - elements.editorContent.classList.add('editor-focused'); + // Add a temporary highlight class to show where the cursor is + elements.editorContent.classList.add("editor-focused"); - // Remove the highlight after a short delay - setTimeout(() => { - elements.editorContent.classList.remove('editor-focused'); - }, 300); + // Remove the highlight after a short delay + setTimeout(() => { + elements.editorContent.classList.remove("editor-focused"); + }, 300); } // Handle tab key in the code editor function handleTabKey(e) { - if (e.key === 'Tab') { - e.preventDefault(); + if (e.key === "Tab") { + e.preventDefault(); - const start = e.target.selectionStart; - const end = e.target.selectionEnd; + const start = e.target.selectionStart; + const end = e.target.selectionEnd; - // Add two spaces at cursor position - e.target.value = e.target.value.substring(0, start) + ' ' + e.target.value.substring(end); + // Add two spaces at cursor position + e.target.value = e.target.value.substring(0, start) + " " + e.target.value.substring(end); - // Move cursor position after the inserted spaces - e.target.selectionStart = e.target.selectionEnd = start + 2; - } + // Move cursor position after the inserted spaces + e.target.selectionStart = e.target.selectionEnd = start + 2; + } } // Initialize the application function init() { - loadUserProgress(); - initializeModules(); + loadUserProgress(); + initializeModules(); - // Event listeners - elements.prevBtn.addEventListener('click', prevLesson); - elements.nextBtn.addEventListener('click', nextLesson); - elements.runBtn.addEventListener('click', runCode); - elements.modalClose.addEventListener('click', closeModal); - elements.moduleSelectorBtn.addEventListener('click', showModuleSelector); - elements.resetBtn.addEventListener('click', resetProgress); - elements.helpBtn.addEventListener('click', showHelp); - elements.codeInput.addEventListener('click', handleEditorClick); + // Event listeners + elements.prevBtn.addEventListener("click", prevLesson); + elements.nextBtn.addEventListener("click", nextLesson); + elements.runBtn.addEventListener("click", runCode); + elements.modalClose.addEventListener("click", closeModal); + elements.moduleSelectorBtn.addEventListener("click", showModuleSelector); + elements.resetBtn.addEventListener("click", resetProgress); + elements.helpBtn.addEventListener("click", showHelp); + elements.codeInput.addEventListener("click", handleEditorClick); - // Also make the editor container clickable to focus the text area - elements.editorContent.addEventListener('click', (e) => { - elements.codeInput.focus(); - }); + // Also make the editor container clickable to focus the text area + elements.editorContent.addEventListener("click", (e) => { + elements.codeInput.focus(); + }); - // Add tab key handler for the code input - elements.codeInput.addEventListener('keydown', handleTabKey); + // Add tab key handler for the code input + elements.codeInput.addEventListener("keydown", handleTabKey); - // Handle keyboard shortcuts - document.addEventListener('keydown', (e) => { - // Ctrl+Enter to run code - if (e.ctrlKey && e.key === 'Enter') { - runCode(); - e.preventDefault(); - } - }); + // Handle keyboard shortcuts + document.addEventListener("keydown", (e) => { + // Ctrl+Enter to run code + if (e.ctrlKey && e.key === "Enter") { + runCode(); + e.preventDefault(); + } + }); } // Start the application -init(); \ No newline at end of file +init(); diff --git a/src/config/lessons.js b/src/config/lessons.js index 42ea3d6..f7f5c7f 100644 --- a/src/config/lessons.js +++ b/src/config/lessons.js @@ -3,26 +3,21 @@ */ // Import lesson configs -import flexboxConfig from '../../lessons/flexbox.json'; -import gridConfig from '../../lessons/grid.json'; -import basicsConfig from '../../lessons/basics.json'; -import tailwindConfig from '../../lessons/tailwindcss.json'; +import flexboxConfig from "../../lessons/flexbox.json"; +import gridConfig from "../../lessons/grid.json"; +import basicsConfig from "../../lessons/basics.json"; +import tailwindConfig from "../../lessons/tailwindcss.json"; // Module store -const moduleStore = [ - basicsConfig, - flexboxConfig, - gridConfig, - tailwindConfig -]; +const moduleStore = [basicsConfig, flexboxConfig, gridConfig, tailwindConfig]; /** * Load all available modules * @returns {Promise} Promise resolving to array of modules */ export async function loadModules() { - // In a real app, we might load these from a server - return moduleStore; + // In a real app, we might load these from a server + return moduleStore; } /** @@ -31,7 +26,7 @@ export async function loadModules() { * @returns {Object|null} The module object or null if not found */ export function getModuleById(moduleId) { - return moduleStore.find(module => module.id === moduleId) || null; + return moduleStore.find((module) => module.id === moduleId) || null; } /** @@ -40,20 +35,20 @@ export function getModuleById(moduleId) { * @returns {Promise} Promise resolving to the module config */ export async function loadModuleFromUrl(url) { - try { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Failed to load module: ${response.status} ${response.statusText}`); - } + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to load module: ${response.status} ${response.statusText}`); + } - const moduleConfig = await response.json(); - validateModuleConfig(moduleConfig); + const moduleConfig = await response.json(); + validateModuleConfig(moduleConfig); - return moduleConfig; - } catch (error) { - console.error('Error loading module from URL:', error); - throw error; - } + return moduleConfig; + } catch (error) { + console.error("Error loading module from URL:", error); + throw error; + } } /** @@ -62,16 +57,16 @@ export async function loadModuleFromUrl(url) { * @throws {Error} If the configuration is invalid */ function validateModuleConfig(config) { - // Required fields - if (!config.id) throw new Error('Module config missing "id"'); - if (!config.title) throw new Error('Module config missing "title"'); - if (!Array.isArray(config.lessons)) throw new Error('Module config missing "lessons" array'); + // Required fields + if (!config.id) throw new Error('Module config missing "id"'); + if (!config.title) throw new Error('Module config missing "title"'); + if (!Array.isArray(config.lessons)) throw new Error('Module config missing "lessons" array'); - // Check each lesson - config.lessons.forEach((lesson, index) => { - if (!lesson.title) throw new Error(`Lesson ${index} missing "title"`); - if (!lesson.previewHTML) throw new Error(`Lesson ${index} missing "previewHTML"`); - }); + // Check each lesson + config.lessons.forEach((lesson, index) => { + if (!lesson.title) throw new Error(`Lesson ${index} missing "title"`); + if (!lesson.previewHTML) throw new Error(`Lesson ${index} missing "previewHTML"`); + }); } /** @@ -80,22 +75,22 @@ function validateModuleConfig(config) { * @returns {boolean} Success status */ export function addCustomModule(moduleConfig) { - try { - validateModuleConfig(moduleConfig); + try { + validateModuleConfig(moduleConfig); - // Check if module with same ID already exists - const existingIndex = moduleStore.findIndex(m => m.id === moduleConfig.id); - if (existingIndex >= 0) { - // Replace existing module - moduleStore[existingIndex] = moduleConfig; - } else { - // Add new module - moduleStore.push(moduleConfig); - } + // Check if module with same ID already exists + const existingIndex = moduleStore.findIndex((m) => m.id === moduleConfig.id); + if (existingIndex >= 0) { + // Replace existing module + moduleStore[existingIndex] = moduleConfig; + } else { + // Add new module + moduleStore.push(moduleConfig); + } - return true; - } catch (error) { - console.error('Error adding custom module:', error); - return false; - } -} \ No newline at end of file + return true; + } catch (error) { + console.error("Error adding custom module:", error); + return false; + } +} diff --git a/src/helpers/renderer.js b/src/helpers/renderer.js index 60e7896..273068b 100644 --- a/src/helpers/renderer.js +++ b/src/helpers/renderer.js @@ -12,22 +12,22 @@ let feedbackElement = null; * @param { Function} onSelectModule - Callback when a module is selected */ export function renderModuleList(container, modules, onSelectModule) { - // Clear the container - container.innerHTML = '

Modules

'; + // Clear the container + container.innerHTML = "

Modules

"; - // Create list items for each module - modules.forEach(module => { - const moduleItem = document.createElement('div'); - moduleItem.classList.add('module-list-item'); - moduleItem.dataset.moduleId = module.id; - moduleItem.textContent = module.title; + // Create list items for each module + modules.forEach((module) => { + const moduleItem = document.createElement("div"); + moduleItem.classList.add("module-list-item"); + moduleItem.dataset.moduleId = module.id; + moduleItem.textContent = module.title; - moduleItem.addEventListener('click', () => { - onSelectModule(module.id); - }); + moduleItem.addEventListener("click", () => { + onSelectModule(module.id); + }); - container.appendChild(moduleItem); - }); + container.appendChild(moduleItem); + }); } /** @@ -41,33 +41,24 @@ export function renderModuleList(container, modules, onSelectModule) { * @param {HTMLElement} suffixEl - The code editor suffix element * @param {Object} lesson - The lesson object */ -export function renderLesson( - titleEl, - descriptionEl, - taskEl, - previewEl, - prefixEl, - inputEl, - suffixEl, - lesson -) { - // Set lesson title and description - titleEl.textContent = lesson.title || 'Untitled Lesson'; - descriptionEl.innerHTML = lesson.description || ''; +export function renderLesson(titleEl, descriptionEl, taskEl, previewEl, prefixEl, inputEl, suffixEl, lesson) { + // Set lesson title and description + titleEl.textContent = lesson.title || "Untitled Lesson"; + descriptionEl.innerHTML = lesson.description || ""; - // Set task instructions - taskEl.innerHTML = lesson.task || ''; + // Set task instructions + taskEl.innerHTML = lesson.task || ""; - // Set code editor contents - prefixEl.textContent = lesson.codePrefix || ''; - inputEl.value = lesson.initialCode || ''; - suffixEl.textContent = lesson.codeSuffix || ''; + // Set code editor contents + prefixEl.textContent = lesson.codePrefix || ""; + inputEl.value = lesson.initialCode || ""; + suffixEl.textContent = lesson.codeSuffix || ""; - // Clear any existing feedback - clearFeedback(); + // Clear any existing feedback + clearFeedback(); - // Initial preview render with empty user code - // The LessonEngine will handle this when it's first set + // Initial preview render with empty user code + // The LessonEngine will handle this when it's first set } /** @@ -77,7 +68,7 @@ export function renderLesson( * @param {number} total - The total number of levels */ export function renderLevelIndicator(element, current, total) { - element.textContent = `Lesson ${current} of ${total}`; + element.textContent = `Lesson ${current} of ${total}`; } /** @@ -86,34 +77,34 @@ export function renderLevelIndicator(element, current, total) { * @param {string} message - The feedback message */ export function showFeedback(isSuccess, message) { - // Clear any existing feedback - clearFeedback(); + // Clear any existing feedback + clearFeedback(); - // Create feedback element - feedbackElement = document.createElement('div'); - feedbackElement.classList.add(isSuccess ? 'feedback-success' : 'feedback-error'); - feedbackElement.textContent = message; + // Create feedback element + feedbackElement = document.createElement("div"); + feedbackElement.classList.add(isSuccess ? "feedback-success" : "feedback-error"); + feedbackElement.textContent = message; - // Find where to insert the feedback - const insertAfter = document.querySelector('.code-editor'); - if (insertAfter && insertAfter.parentNode) { - insertAfter.parentNode.insertBefore(feedbackElement, insertAfter.nextSibling); - } + // Find where to insert the feedback + const insertAfter = document.querySelector(".code-editor"); + if (insertAfter && insertAfter.parentNode) { + insertAfter.parentNode.insertBefore(feedbackElement, insertAfter.nextSibling); + } - // Auto-remove feedback after some time if successful - if (isSuccess) { - setTimeout(() => { - clearFeedback(); - }, 5000); - } + // Auto-remove feedback after some time if successful + if (isSuccess) { + setTimeout(() => { + clearFeedback(); + }, 5000); + } } /** * Clear any existing feedback */ export function clearFeedback() { - if (feedbackElement && feedbackElement.parentNode) { - feedbackElement.parentNode.removeChild(feedbackElement); - } - feedbackElement = null; -} \ No newline at end of file + if (feedbackElement && feedbackElement.parentNode) { + feedbackElement.parentNode.removeChild(feedbackElement); + } + feedbackElement = null; +} diff --git a/src/helpers/validator.js b/src/helpers/validator.js index ad61748..848e457 100644 --- a/src/helpers/validator.js +++ b/src/helpers/validator.js @@ -9,73 +9,73 @@ * @returns {Object} Validation result with isValid and message properties */ export function validateUserCode(userCode, lesson) { - if (!lesson || !lesson.validations) { - return { isValid: true, message: 'No validations specified for this lesson.' }; - } + if (!lesson || !lesson.validations) { + return { isValid: true, message: "No validations specified for this lesson." }; + } - // Get the validations array from the lesson - const validations = lesson.validations; + // Get the validations array from the lesson + const validations = lesson.validations; - // Default validation result - let result = { - isValid: true, - message: 'Your code looks good!' - }; + // Default validation result + let result = { + isValid: true, + message: "Your code looks good!" + }; - // Process each validation rule - for (const validation of validations) { - const { type, value, message, options } = validation; + // Process each validation rule + for (const validation of validations) { + const { type, value, message, options } = validation; - switch (type) { - case 'contains': - if (!containsValidation(userCode, value, options)) { - return { isValid: false, message: message || `Your code should include "${value}".` }; - } - break; + switch (type) { + case "contains": + if (!containsValidation(userCode, value, options)) { + return { isValid: false, message: message || `Your code should include "${value}".` }; + } + break; - case 'not_contains': - if (containsValidation(userCode, value, options)) { - return { isValid: false, message: message || `Your code should not include "${value}".` }; - } - break; + case "not_contains": + if (containsValidation(userCode, value, options)) { + return { isValid: false, message: message || `Your code should not include "${value}".` }; + } + break; - case 'regex': - if (!regexValidation(userCode, value, options)) { - return { isValid: false, message: message || 'Your code does not match the expected pattern.' }; - } - break; + case "regex": + if (!regexValidation(userCode, value, options)) { + return { isValid: false, message: message || "Your code does not match the expected pattern." }; + } + break; - case 'property_value': - if (!propertyValueValidation(userCode, value, options)) { - return { isValid: false, message: message || `The "${value.property}" property should be set to "${value.expected}".` }; - } - break; + case "property_value": + if (!propertyValueValidation(userCode, value, options)) { + return { isValid: false, message: message || `The "${value.property}" property should be set to "${value.expected}".` }; + } + break; - case 'syntax': - const syntaxResult = syntaxValidation(userCode); - if (!syntaxResult.isValid) { - return { isValid: false, message: message || `CSS syntax error: ${syntaxResult.error}` }; - } - break; + case "syntax": + const syntaxResult = syntaxValidation(userCode); + if (!syntaxResult.isValid) { + return { isValid: false, message: message || `CSS syntax error: ${syntaxResult.error}` }; + } + break; - case 'custom': - if (validation.validator && typeof validation.validator === 'function') { - const customResult = validation.validator(userCode); - if (!customResult.isValid) { - return { isValid: false, message: customResult.message || message || 'Your code does not meet the requirements.' }; - } - } - break; + case "custom": + if (validation.validator && typeof validation.validator === "function") { + const customResult = validation.validator(userCode); + if (!customResult.isValid) { + return { isValid: false, message: customResult.message || message || "Your code does not meet the requirements." }; + } + } + break; - // Add more validation types as needed + // Add more validation types as needed - default: - console.warn(`Unknown validation type: ${type}`); - } - } + default: + console.warn(`Unknown validation type: ${type}`); + } + } - // If we've passed all validations, return success - return result; + // If we've passed all validations, return success + return result; } /** @@ -86,19 +86,19 @@ export function validateUserCode(userCode, lesson) { * @returns {boolean} Whether the validation passes */ function containsValidation(code, value, options = {}) { - const { caseSensitive = true, wholeWord = false } = options; + const { caseSensitive = true, wholeWord = false } = options; - if (!caseSensitive) { - code = code.toLowerCase(); - value = value.toLowerCase(); - } + if (!caseSensitive) { + code = code.toLowerCase(); + value = value.toLowerCase(); + } - if (wholeWord) { - const regex = new RegExp(`\\b${escapeRegExp(value)}\\b`, caseSensitive ? '' : 'i'); - return regex.test(code); - } + if (wholeWord) { + const regex = new RegExp(`\\b${escapeRegExp(value)}\\b`, caseSensitive ? "" : "i"); + return regex.test(code); + } - return code.includes(value); + return code.includes(value); } /** @@ -109,19 +109,19 @@ function containsValidation(code, value, options = {}) { * @returns {boolean} Whether the validation passes */ function regexValidation(code, pattern, options = {}) { - const { caseSensitive = true, multiline = true } = options; + const { caseSensitive = true, multiline = true } = options; - let flags = ''; - if (!caseSensitive) flags += 'i'; - if (multiline) flags += 'm'; + let flags = ""; + if (!caseSensitive) flags += "i"; + if (multiline) flags += "m"; - try { - const regex = new RegExp(pattern, flags); - return regex.test(code); - } catch (e) { - console.error('Invalid regex in validation:', e); - return false; - } + try { + const regex = new RegExp(pattern, flags); + return regex.test(code); + } catch (e) { + console.error("Invalid regex in validation:", e); + return false; + } } /** @@ -132,27 +132,27 @@ function regexValidation(code, pattern, options = {}) { * @returns {boolean} Whether the validation passes */ function propertyValueValidation(code, value, options = {}) { - const { property, expected } = value; - const { exact = false } = options; + const { property, expected } = value; + const { exact = false } = options; - // Create a regex to extract the property value - // This is a simplified version and might not handle all CSS syntax nuances - const propertyRegex = new RegExp(`${escapeRegExp(property)}\\s*:\\s*([^;\\}]+)`, 'i'); - const match = code.match(propertyRegex); + // Create a regex to extract the property value + // This is a simplified version and might not handle all CSS syntax nuances + const propertyRegex = new RegExp(`${escapeRegExp(property)}\\s*:\\s*([^;\\}]+)`, "i"); + const match = code.match(propertyRegex); - if (!match) { - // Property not found - return false; - } + if (!match) { + // Property not found + return false; + } - const actualValue = match[1].trim(); + const actualValue = match[1].trim(); - if (exact) { - return actualValue === expected; - } else { - // Allow for flexible matching - return actualValue.toLowerCase().includes(expected.toLowerCase()); - } + if (exact) { + return actualValue === expected; + } else { + // Allow for flexible matching + return actualValue.toLowerCase().includes(expected.toLowerCase()); + } } /** @@ -161,16 +161,16 @@ function propertyValueValidation(code, value, options = {}) { * @returns {Object} Validation result */ function syntaxValidation(code) { - try { - // Create a hidden style element to test the CSS - const style = document.createElement('style'); - style.textContent = code; - document.head.appendChild(style); - document.head.removeChild(style); - return { isValid: true }; - } catch (e) { - return { isValid: false, error: e.message }; - } + try { + // Create a hidden style element to test the CSS + const style = document.createElement("style"); + style.textContent = code; + document.head.appendChild(style); + document.head.removeChild(style); + return { isValid: true }; + } catch (e) { + return { isValid: false, error: e.message }; + } } /** @@ -179,5 +179,5 @@ function syntaxValidation(code) { * @returns {string} Escaped string */ function escapeRegExp(string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); -} \ No newline at end of file + return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} diff --git a/src/impl/LessonEngine.js b/src/impl/LessonEngine.js index 26d83ec..cd9bb1f 100644 --- a/src/impl/LessonEngine.js +++ b/src/impl/LessonEngine.js @@ -2,229 +2,229 @@ * LessonEngine - Core class for managing lessons and applying/testing user code * This file is the implementation of the LessonEngine class declaration from app.helpers */ -import { validateUserCode } from '../helpers/validator.js'; -import { showFeedback } from '../helpers/renderer.js'; +import { validateUserCode } from "../helpers/validator.js"; +import { showFeedback } from "../helpers/renderer.js"; export class LessonEngine { - constructor() { - this.currentLesson = null; - this.userCode = ''; - this.currentModule = null; - this.currentLessonIndex = 0; - } + constructor() { + this.currentLesson = null; + this.userCode = ""; + this.currentModule = null; + this.currentLessonIndex = 0; + } - /** - * Set the current module - * @param {Object} module - The module object from the config - */ - setModule(module) { - this.currentModule = module; - this.currentLessonIndex = 0; - if (module && module.lessons && module.lessons.length > 0) { - this.setLesson(module.lessons[0]); - } - } + /** + * Set the current module + * @param {Object} module - The module object from the config + */ + setModule(module) { + this.currentModule = module; + this.currentLessonIndex = 0; + if (module && module.lessons && module.lessons.length > 0) { + this.setLesson(module.lessons[0]); + } + } - /** - * Set the current lesson - * @param {Object} lesson - The lesson object from the config - */ - setLesson(lesson) { - this.currentLesson = lesson; - this.userCode = lesson.initialCode || ''; - this.renderPreview(); - } + /** + * Set the current lesson + * @param {Object} lesson - The lesson object from the config + */ + setLesson(lesson) { + this.currentLesson = lesson; + this.userCode = lesson.initialCode || ""; + this.renderPreview(); + } - /** - * Set lesson by index within the current module - * @param {number} index - The lesson index - * @returns {boolean} Whether the operation was successful - */ - setLessonByIndex(index) { - if (!this.currentModule || !this.currentModule.lessons) { - return false; - } + /** + * Set lesson by index within the current module + * @param {number} index - The lesson index + * @returns {boolean} Whether the operation was successful + */ + setLessonByIndex(index) { + if (!this.currentModule || !this.currentModule.lessons) { + return false; + } - if (index < 0 || index >= this.currentModule.lessons.length) { - return false; - } + if (index < 0 || index >= this.currentModule.lessons.length) { + return false; + } - this.currentLessonIndex = index; - this.setLesson(this.currentModule.lessons[index]); - return true; - } + this.currentLessonIndex = index; + this.setLesson(this.currentModule.lessons[index]); + return true; + } - /** - * Move to the next lesson - * @returns {boolean} Whether the operation was successful - */ - nextLesson() { - return this.setLessonByIndex(this.currentLessonIndex + 1); - } + /** + * Move to the next lesson + * @returns {boolean} Whether the operation was successful + */ + nextLesson() { + return this.setLessonByIndex(this.currentLessonIndex + 1); + } - /** - * Move to the previous lesson - * @returns {boolean} Whether the operation was successful - */ - previousLesson() { - return this.setLessonByIndex(this.currentLessonIndex - 1); - } + /** + * Move to the previous lesson + * @returns {boolean} Whether the operation was successful + */ + previousLesson() { + return this.setLessonByIndex(this.currentLessonIndex - 1); + } - /** - * Apply user-written CSS to the preview area - * @param {string} code - User CSS code - */ - applyUserCode(code) { - if (!this.currentLesson) return; + /** + * Apply user-written CSS to the preview area + * @param {string} code - User CSS code + */ + applyUserCode(code) { + if (!this.currentLesson) return; - this.userCode = code; - this.renderPreview(); - } + this.userCode = code; + this.renderPreview(); + } - /** - * Render the preview for the current lesson - */ - renderPreview() { - if (!this.currentLesson) return; + /** + * Render the preview for the current lesson + */ + renderPreview() { + if (!this.currentLesson) return; - const { previewHTML, previewBaseCSS, previewContainer, sandboxCSS } = this.currentLesson; + const { previewHTML, previewBaseCSS, previewContainer, sandboxCSS } = this.currentLesson; - // Create an iframe for isolated preview rendering - const iframe = document.createElement('iframe'); - iframe.style.width = '100%'; - iframe.style.height = '100%'; - iframe.style.border = 'none'; - iframe.title = 'Preview'; + // Create an iframe for isolated preview rendering + const iframe = document.createElement("iframe"); + iframe.style.width = "100%"; + iframe.style.height = "100%"; + iframe.style.border = "none"; + iframe.title = "Preview"; - // Get the preview container - const container = document.getElementById(previewContainer || 'preview-area'); + // Get the preview container + const container = document.getElementById(previewContainer || "preview-area"); - // Clear the container and add the iframe - container.innerHTML = ''; - container.appendChild(iframe); + // Clear the container and add the iframe + container.innerHTML = ""; + container.appendChild(iframe); - // Create the complete CSS by combining base CSS with user code and sandbox CSS - const combinedCSS = ` + // Create the complete CSS by combining base CSS with user code and sandbox CSS + const combinedCSS = ` /* Base CSS */ - ${previewBaseCSS || ''} + ${previewBaseCSS || ""} /* User Code */ - ${this.userCode || ''} + ${this.userCode || ""} /* Sandbox CSS (for visualizing the exercise) */ - ${sandboxCSS || ''} + ${sandboxCSS || ""} `; - // Write the content to the iframe - const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; - iframeDoc.open(); - iframeDoc.write(` + // Write the content to the iframe + const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; + iframeDoc.open(); + iframeDoc.write(` - ${previewHTML || '
No preview available
'} + ${previewHTML || "
No preview available
"} `); - iframeDoc.close(); - } + iframeDoc.close(); + } - /** - * Validate user code against the current lesson's requirements - * @returns {Object} Validation result - */ - validateCode() { - if (!this.currentLesson) { - return { isValid: false, message: 'No active lesson to validate against.' }; - } + /** + * Validate user code against the current lesson's requirements + * @returns {Object} Validation result + */ + validateCode() { + if (!this.currentLesson) { + return { isValid: false, message: "No active lesson to validate against." }; + } - const result = validateUserCode(this.userCode, this.currentLesson); + const result = validateUserCode(this.userCode, this.currentLesson); - // Display feedback to the user - showFeedback(result.isValid, result.message); + // Display feedback to the user + showFeedback(result.isValid, result.message); - return result; - } + return result; + } - /** - * Get the current state of the lesson - * @returns {Object} The current lesson state - */ - getCurrentState() { - return { - module: this.currentModule, - lesson: this.currentLesson, - lessonIndex: this.currentLessonIndex, - userCode: this.userCode, - totalLessons: this.currentModule ? this.currentModule.lessons.length : 0 - }; - } + /** + * Get the current state of the lesson + * @returns {Object} The current lesson state + */ + getCurrentState() { + return { + module: this.currentModule, + lesson: this.currentLesson, + lessonIndex: this.currentLessonIndex, + userCode: this.userCode, + totalLessons: this.currentModule ? this.currentModule.lessons.length : 0 + }; + } - /** - * Save progress to localStorage - */ - saveProgress() { - if (!this.currentModule || !this.currentLesson) return; + /** + * Save progress to localStorage + */ + saveProgress() { + if (!this.currentModule || !this.currentLesson) return; - const progressData = { - moduleId: this.currentModule.id, - lessonIndex: this.currentLessonIndex, - userCode: this.userCode, - timestamp: new Date().toISOString() - }; + const progressData = { + moduleId: this.currentModule.id, + lessonIndex: this.currentLessonIndex, + userCode: this.userCode, + timestamp: new Date().toISOString() + }; - localStorage.setItem('cssQuest_progress', JSON.stringify(progressData)); - } + localStorage.setItem("cssQuest_progress", JSON.stringify(progressData)); + } - /** - * Load progress from localStorage - * @param {Array} modules - Available modules - * @returns {Object|null} Loaded progress data or null if not found - */ - loadProgress(modules) { - const savedProgress = localStorage.getItem('cssQuest_progress'); - if (!savedProgress) return null; + /** + * Load progress from localStorage + * @param {Array} modules - Available modules + * @returns {Object|null} Loaded progress data or null if not found + */ + loadProgress(modules) { + const savedProgress = localStorage.getItem("cssQuest_progress"); + if (!savedProgress) return null; - try { - const progressData = JSON.parse(savedProgress); + try { + const progressData = JSON.parse(savedProgress); - // Find the module - const module = modules.find(m => m.id === progressData.moduleId); - if (!module) return null; + // Find the module + const module = modules.find((m) => m.id === progressData.moduleId); + if (!module) return null; - this.setModule(module); - this.setLessonByIndex(progressData.lessonIndex); + this.setModule(module); + this.setLessonByIndex(progressData.lessonIndex); - // Restore user code if available - if (progressData.userCode) { - this.userCode = progressData.userCode; - this.renderPreview(); - } + // Restore user code if available + if (progressData.userCode) { + this.userCode = progressData.userCode; + this.renderPreview(); + } - return progressData; - } catch (e) { - console.error('Error loading progress:', e); - return null; - } - } + return progressData; + } catch (e) { + console.error("Error loading progress:", e); + return null; + } + } - /** - * Reset the current state - */ - reset() { - if (this.currentLesson) { - this.userCode = this.currentLesson.initialCode || ''; - this.renderPreview(); - } - } + /** + * Reset the current state + */ + reset() { + if (this.currentLesson) { + this.userCode = this.currentLesson.initialCode || ""; + this.renderPreview(); + } + } - /** - * Clear all saved progress - */ - clearProgress() { - localStorage.removeItem('cssQuest_progress'); - } -} \ No newline at end of file + /** + * Clear all saved progress + */ + clearProgress() { + localStorage.removeItem("cssQuest_progress"); + } +} diff --git a/src/index.html b/src/index.html index 6a179e8..dd90313 100644 --- a/src/index.html +++ b/src/index.html @@ -1,90 +1,88 @@ - + - - - - - CODE CRISPIES - Learn CSS Interactively - - - -
-
- - -
+ + + + + CODE CRISPIES - Learn CSS Interactively + + + +
+
+ + +
-
- +
+ -
-
-

Loading...

-
- Please select a lesson to begin. -
+
+
+

Loading...

+
Please select a lesson to begin.
-
-
- -
+
+
+ +
-
-
- -
+
+
+ +
-
-
- CSS Editor - -
-
-
- -
-
-
-
-
+
+
+ CSS Editor + +
+
+
+ +
+
+
+
+
-
- -
Level 0/0
- -
-
-
-
+
+ +
Level 0/0
+ +
+
+
+ - - + + - - - \ No newline at end of file + + + diff --git a/src/main.css b/src/main.css index 2629d22..8727552 100644 --- a/src/main.css +++ b/src/main.css @@ -1,388 +1,394 @@ :root { - --primary-color: #4a6bfd; - --primary-light: #7a93fe; - --primary-dark: #244ae8; - --secondary-color: #ff7e5f; - --text-color: #2c3e50; - --light-text: #777; - --bg-color: #f9f9f9; - --panel-bg: #ffffff; - --border-color: #e0e0e0; - --success-color: #2ecc71; - --error-color: #e74c3c; - --font-main: 'Inter', 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; - --shadow: 0 2px 10px rgba(0, 0, 0, 0.05); + --primary-color: #4a6bfd; + --primary-light: #7a93fe; + --primary-dark: #244ae8; + --secondary-color: #ff7e5f; + --text-color: #2c3e50; + --light-text: #777; + --bg-color: #f9f9f9; + --panel-bg: #ffffff; + --border-color: #e0e0e0; + --success-color: #2ecc71; + --error-color: #e74c3c; + --font-main: "Inter", "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + --shadow: 0 2px 10px rgba(0, 0, 0, 0.05); } * { - margin: 0; - padding: 0; - box-sizing: border-box; + margin: 0; + padding: 0; + box-sizing: border-box; } body { - font-family: var(--font-main), serif; - background-color: var(--bg-color); - color: var(--text-color); - line-height: 1.6; + font-family: var(--font-main), serif; + background-color: var(--bg-color); + color: var(--text-color); + line-height: 1.6; } .app-container { - display: flex; - flex-direction: column; - min-height: 100vh; + display: flex; + flex-direction: column; + min-height: 100vh; } /* Header Styles */ .header { - background-color: var(--panel-bg); - padding: 1rem 2rem; - display: flex; - justify-content: space-between; - align-items: center; - box-shadow: var(--shadow); - position: sticky; - top: 0; - z-index: 100; + background-color: var(--panel-bg); + padding: 1rem 2rem; + display: flex; + justify-content: space-between; + align-items: center; + box-shadow: var(--shadow); + position: sticky; + top: 0; + z-index: 100; } .logo h1 { - color: var(--text-color); - font-size: 1.7rem; - font-weight: 700; + color: var(--text-color); + font-size: 1.7rem; + font-weight: 700; } .main-nav ul { - display: flex; - list-style: none; - gap: 1rem; + display: flex; + list-style: none; + gap: 1rem; } /* Main Content Layout */ .main-content { - display: flex; - flex: 1; - min-height: calc(100vh - 60px); + display: flex; + flex: 1; + min-height: calc(100vh - 60px); } .sidebar { - width: 240px; - background-color: var(--panel-bg); - border-right: 1px solid var(--border-color); - padding: 1.5rem 1rem; - overflow-y: auto; - height: calc(100vh - 60px); - position: sticky; - top: 60px; + width: 240px; + background-color: var(--panel-bg); + border-right: 1px solid var(--border-color); + padding: 1.5rem 1rem; + overflow-y: auto; + height: calc(100vh - 60px); + position: sticky; + top: 60px; } .content-area { - flex: 1; - padding: 2rem; - max-width: calc(100% - 240px); + flex: 1; + padding: 2rem; + max-width: calc(100% - 240px); } /* Module List Styles */ .module-list { - margin-bottom: 2rem; + margin-bottom: 2rem; } .module-list-item { - padding: 0.75rem 1rem; - margin-bottom: 0.5rem; - border-radius: 6px; - cursor: pointer; - transition: background-color 0.2s; + padding: 0.75rem 1rem; + margin-bottom: 0.5rem; + border-radius: 6px; + cursor: pointer; + transition: background-color 0.2s; } .module-list-item:hover { - background-color: rgba(74, 107, 253, 0.05); + background-color: rgba(74, 107, 253, 0.05); } .module-list-item.active { - background-color: rgba(74, 107, 253, 0.1); - color: var(--primary-color); - font-weight: 600; + background-color: rgba(74, 107, 253, 0.1); + color: var(--primary-color); + font-weight: 600; } /* Lesson Container */ .lesson-container { - display: flex; - flex-flow: column; - align-items: stretch; - justify-content: flex-start; - gap: 2rem; - background-color: var(--panel-bg); - border-radius: 8px; - box-shadow: var(--shadow); - padding: 2rem; - height: 100%; - margin-bottom: 2rem; + display: flex; + flex-flow: column; + align-items: stretch; + justify-content: flex-start; + gap: 2rem; + background-color: var(--panel-bg); + border-radius: 8px; + box-shadow: var(--shadow); + padding: 2rem; + height: 100%; + margin-bottom: 2rem; } #lesson-title { - margin-bottom: 1rem; - color: var(--primary-dark); + margin-bottom: 1rem; + color: var(--primary-dark); } .lesson-description { - margin-bottom: 2rem; - color: var(--text-color); - line-height: 1.7; + margin-bottom: 2rem; + color: var(--text-color); + line-height: 1.7; } /* Challenge Container */ .challenge-container { - display: flex; - flex: 1; - flex-direction: column; - gap: 1.5rem; - margin-bottom: 2rem; + display: flex; + flex: 1; + flex-direction: column; + gap: 1.5rem; + margin-bottom: 2rem; } @media (min-width: 1024px) { - .challenge-container { - flex-direction: row; - } + .challenge-container { + flex-direction: row; + } - .preview-area, - .editor-container { - width: 48%; - } + .preview-area, + .editor-container { + width: 48%; + } } .preview-area { - background-color: #fff; - border: 1px solid var(--border-color); - border-radius: 6px; - padding: 1rem; - overflow: hidden; - min-height: 300px; - display: flex; - justify-content: center; - align-items: center; + background-color: #fff; + border: 1px solid var(--border-color); + border-radius: 6px; + padding: 1rem; + overflow: hidden; + min-height: 300px; + display: flex; + justify-content: center; + align-items: center; } .editor-container { - display: flex; - flex-direction: column; - gap: 1rem; + display: flex; + flex-direction: column; + gap: 1rem; } .task-instruction { - background-color: rgba(74, 107, 253, 0.05); - border-left: 4px solid var(--primary-color); - padding: 1rem; - border-radius: 4px; + background-color: rgba(74, 107, 253, 0.05); + border-left: 4px solid var(--primary-color); + padding: 1rem; + border-radius: 4px; } .code-editor { - border: 1px solid var(--border-color); - border-radius: 6px; - overflow: hidden; + border: 1px solid var(--border-color); + border-radius: 6px; + overflow: hidden; } .editor-header { - background-color: #f5f5f5; - padding: 0.5rem 1rem; - display: flex; - justify-content: space-between; - align-items: center; - font-size: 0.9rem; - color: var(--light-text); - border-bottom: 1px solid var(--border-color); + background-color: #f5f5f5; + padding: 0.5rem 1rem; + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.9rem; + color: var(--light-text); + border-bottom: 1px solid var(--border-color); } .editor-content { - background-color: #1e1e1e; - color: #d4d4d4; - padding: 1rem; - overflow-y: auto; - font-family: 'JetBrains Mono', 'Fira Code', monospace; - font-size: 14px; - line-height: 1.5; - cursor: text; /* Add text cursor to indicate it's editable */ + background-color: #1e1e1e; + color: #d4d4d4; + padding: 1rem; + overflow-y: auto; + font-family: "JetBrains Mono", "Fira Code", monospace; + font-size: 14px; + line-height: 1.5; + cursor: text; /* Add text cursor to indicate it's editable */ } /* Style for when editor is clicked - pulse effect */ .editor-focused { - animation: focus-pulse 0.3s ease; + animation: focus-pulse 0.3s ease; } /* Pulse animation */ @keyframes focus-pulse { - 0% { background-color: #1e1e1e; } - 50% { background-color: #303030; } - 100% { background-color: #1e1e1e; } + 0% { + background-color: #1e1e1e; + } + 50% { + background-color: #303030; + } + 100% { + background-color: #1e1e1e; + } } code { - font-family: 'JetBrains Mono', 'Fira Code', monospace; + font-family: "JetBrains Mono", "Fira Code", monospace; } .code-input { - background-color: transparent; - color: #d4d4d4; - border: none; - width: 100%; - min-height: 100px; - font-family: 'JetBrains Mono', 'Fira Code', monospace; - font-size: 14px; - line-height: 1.5; - padding: 0.5rem 0; - outline: none; - resize: vertical; - caret-color: var(--primary-color); - caret-shape: block; - transition: background-color 0.2s ease; + background-color: transparent; + color: #d4d4d4; + border: none; + width: 100%; + min-height: 100px; + font-family: "JetBrains Mono", "Fira Code", monospace; + font-size: 14px; + line-height: 1.5; + padding: 0.5rem 0; + outline: none; + resize: vertical; + caret-color: var(--primary-color); + caret-shape: block; + transition: background-color 0.2s ease; } /* Controls */ .controls { - /*justify-self: end;*/ - /*position: absolute;*/ - /*left: 2rem;*/ - /*right: 2rem;*/ - /*bottom: 2rem;*/ - display: flex; - justify-content: space-between; - align-items: center; - margin-top: 1.5rem; + /*justify-self: end;*/ + /*position: absolute;*/ + /*left: 2rem;*/ + /*right: 2rem;*/ + /*bottom: 2rem;*/ + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 1.5rem; } .level-indicator { - font-size: 0.9rem; - color: var(--light-text); + font-size: 0.9rem; + color: var(--light-text); } /* Buttons */ .btn { - padding: 0.5rem 1rem; - border-radius: 4px; - border: 1px solid var(--border-color); - background-color: #fff; - color: var(--text-color); - cursor: pointer; - font-family: var(--font-main); - font-size: 0.9rem; - transition: all 0.2s; + padding: 0.5rem 1rem; + border-radius: 4px; + border: 1px solid var(--border-color); + background-color: #fff; + color: var(--text-color); + cursor: pointer; + font-family: var(--font-main); + font-size: 0.9rem; + transition: all 0.2s; } .btn:hover { - background-color: #f5f5f5; + background-color: #f5f5f5; } .btn-primary { - background-color: var(--primary-color); - color: white; - border: 1px solid var(--primary-dark); + background-color: var(--primary-color); + color: white; + border: 1px solid var(--primary-dark); } .btn-primary:hover { - background-color: var(--primary-dark); + background-color: var(--primary-dark); } /* Modal */ .modal-container { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.5); - display: flex; - justify-content: center; - align-items: center; - z-index: 1000; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; } .modal { - background-color: var(--panel-bg); - border-radius: 8px; - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); - width: 90%; - max-width: 600px; - max-height: 80vh; - overflow-y: auto; + background-color: var(--panel-bg); + border-radius: 8px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); + width: 90%; + max-width: 600px; + max-height: 80vh; + overflow-y: auto; } .modal-header { - padding: 1rem; - border-bottom: 1px solid var(--border-color); - display: flex; - justify-content: space-between; - align-items: center; + padding: 1rem; + border-bottom: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + align-items: center; } .modal-content { - padding: 1.5rem; + padding: 1.5rem; } .modal-close { - background: none; - border: none; - font-size: 1.5rem; - cursor: pointer; - color: var(--light-text); + background: none; + border: none; + font-size: 1.5rem; + cursor: pointer; + color: var(--light-text); } .hidden { - display: none; + display: none; } /* Feedback */ .feedback-success { - color: var(--success-color); - font-weight: 500; - margin-top: 1rem; - padding: 0.5rem; - border-radius: 4px; - background-color: rgba(46, 204, 113, 0.1); - border-left: 3px solid var(--success-color); + color: var(--success-color); + font-weight: 500; + margin-top: 1rem; + padding: 0.5rem; + border-radius: 4px; + background-color: rgba(46, 204, 113, 0.1); + border-left: 3px solid var(--success-color); } .feedback-error { - color: var(--error-color); - font-weight: 500; - margin-top: 1rem; - padding: 0.5rem; - border-radius: 4px; - background-color: rgba(231, 76, 60, 0.1); - border-left: 3px solid var(--error-color); + color: var(--error-color); + font-weight: 500; + margin-top: 1rem; + padding: 0.5rem; + border-radius: 4px; + background-color: rgba(231, 76, 60, 0.1); + border-left: 3px solid var(--error-color); } /* Add these styles to your main.css file */ /* Success highlight for lesson container */ .success-highlight { - box-shadow: 0 0 0 3px var(--success-color); - transition: all 0.3s ease; + box-shadow: 0 0 0 3px var(--success-color); + transition: all 0.3s ease; } /* Success text color for headings */ .success-text { - color: var(--success-color); - transition: color 0.3s ease; + color: var(--success-color); + transition: color 0.3s ease; } /* Friendlier error feedback */ .feedback-error { - color: #996633; - font-weight: 500; - margin-top: 1rem; - padding: 0.5rem; - border-radius: 4px; - background-color: rgba(255, 248, 230, 0.5); - border-left: 3px solid #cc9944; + color: #996633; + font-weight: 500; + margin-top: 1rem; + padding: 0.5rem; + border-radius: 4px; + background-color: rgba(255, 248, 230, 0.5); + border-left: 3px solid #cc9944; } /* Module selector button with progress */ #module-selector-btn { - overflow: hidden; + overflow: hidden; } /* Button disabled state */ .btn-disabled { - opacity: 0.6; - cursor: not-allowed; -} \ No newline at end of file + opacity: 0.6; + cursor: not-allowed; +} diff --git a/tests/setup.js b/tests/setup.js index 043b6c9..553a8aa 100644 --- a/tests/setup.js +++ b/tests/setup.js @@ -1,26 +1,28 @@ -import { afterEach } from 'vitest'; -import '@testing-library/jest-dom/vitest'; +import { afterEach } from "vitest"; +import "@testing-library/jest-dom/vitest"; // import 'whatwg-fetch'; // Setup mock for localStorage const localStorageMock = (() => { - let store = {}; - return { - getItem(key) { - return store[key] || null; - }, - setItem(key, value) { - store[key] = String(value); - }, - removeItem(key) { - delete store[key]; - }, - clear() { - store = {}; - }, - length: 0, - key() { return null; } - }; + let store = {}; + return { + getItem(key) { + return store[key] || null; + }, + setItem(key, value) { + store[key] = String(value); + }, + removeItem(key) { + delete store[key]; + }, + clear() { + store = {}; + }, + length: 0, + key() { + return null; + } + }; })(); // Mock the DOM environment @@ -80,14 +82,14 @@ window.localStorage = localStorageMock; // For iframe support in jsdom if (!window.document.createRange) { - window.document.createRange = () => ({ - setStart: () => {}, - setEnd: () => {}, - commonAncestorContainer: { - nodeName: 'BODY', - ownerDocument: document, - }, - }); + window.document.createRange = () => ({ + setStart: () => {}, + setEnd: () => {}, + commonAncestorContainer: { + nodeName: "BODY", + ownerDocument: document + } + }); } // Add fetch mock @@ -95,6 +97,6 @@ global.fetch = vi.fn(); // Clean up after each test afterEach(() => { - localStorage.clear(); - fetch.mockReset(); -}); \ No newline at end of file + localStorage.clear(); + fetch.mockReset(); +}); diff --git a/tests/unit/lessons.test.js b/tests/unit/lessons.test.js index e5c0ff0..9282337 100644 --- a/tests/unit/lessons.test.js +++ b/tests/unit/lessons.test.js @@ -1,184 +1,172 @@ -import { describe, test, expect, vi, beforeEach } from 'vitest'; -import { loadModules, getModuleById, loadModuleFromUrl, addCustomModule } from '../../src/config/lessons.js'; +import { describe, test, expect, vi, beforeEach } from "vitest"; +import { loadModules, getModuleById, loadModuleFromUrl, addCustomModule } from "../../src/config/lessons.js"; // Mock the module store for testing -vi.mock('../../lessons/flexbox.json', () => ({ default: { id: 'flexbox', title: 'Flexbox', lessons: [] }})); -vi.mock('../../lessons/grid.json', () => ({ default: { id: 'grid', title: 'CSS Grid', lessons: [] }})); -vi.mock('../../lessons/basics.json', () => ({ default: { id: 'basics', title: 'CSS Basics', lessons: [] }})); -vi.mock('../../lessons/tailwindcss.json', () => ({ default: { id: 'tailwind', title: 'Tailwind CSS', lessons: [] }})); +vi.mock("../../lessons/flexbox.json", () => ({ default: { id: "flexbox", title: "Flexbox", lessons: [] } })); +vi.mock("../../lessons/grid.json", () => ({ default: { id: "grid", title: "CSS Grid", lessons: [] } })); +vi.mock("../../lessons/basics.json", () => ({ default: { id: "basics", title: "CSS Basics", lessons: [] } })); +vi.mock("../../lessons/tailwindcss.json", () => ({ default: { id: "tailwind", title: "Tailwind CSS", lessons: [] } })); -describe('Lessons Config Module', () => { - describe('loadModules', () => { - test('should return an array of modules', async () => { - const modules = await loadModules(); +describe("Lessons Config Module", () => { + describe("loadModules", () => { + test("should return an array of modules", async () => { + const modules = await loadModules(); - expect(Array.isArray(modules)).toBe(true); - expect(modules.length).toBe(4); + expect(Array.isArray(modules)).toBe(true); + expect(modules.length).toBe(4); - // Check if modules have the right structure - const moduleIds = modules.map(m => m.id); - expect(moduleIds).toContain('basics'); - expect(moduleIds).toContain('flexbox'); - expect(moduleIds).toContain('grid'); - expect(moduleIds).toContain('tailwind'); - }); - }); + // Check if modules have the right structure + const moduleIds = modules.map((m) => m.id); + expect(moduleIds).toContain("basics"); + expect(moduleIds).toContain("flexbox"); + expect(moduleIds).toContain("grid"); + expect(moduleIds).toContain("tailwind"); + }); + }); - describe('getModuleById', () => { - test('should return a module by ID', async () => { - // Load modules first to populate the module store - await loadModules(); + describe("getModuleById", () => { + test("should return a module by ID", async () => { + // Load modules first to populate the module store + await loadModules(); - const flexboxModule = getModuleById('flexbox'); - expect(flexboxModule).not.toBeNull(); - expect(flexboxModule.id).toBe('flexbox'); - expect(flexboxModule.title).toBe('Flexbox'); - }); + const flexboxModule = getModuleById("flexbox"); + expect(flexboxModule).not.toBeNull(); + expect(flexboxModule.id).toBe("flexbox"); + expect(flexboxModule.title).toBe("Flexbox"); + }); - test('should return null for non-existent module ID', async () => { - // Load modules first - await loadModules(); + test("should return null for non-existent module ID", async () => { + // Load modules first + await loadModules(); - const nonExistentModule = getModuleById('non-existent'); - expect(nonExistentModule).toBeNull(); - }); - }); + const nonExistentModule = getModuleById("non-existent"); + expect(nonExistentModule).toBeNull(); + }); + }); - describe('loadModuleFromUrl', () => { - beforeEach(() => { - // Reset fetch mock - fetch.mockReset(); - }); + describe("loadModuleFromUrl", () => { + beforeEach(() => { + // Reset fetch mock + fetch.mockReset(); + }); - test('should load a module from a URL', async () => { - const mockModule = { - id: 'remote-module', - title: 'Remote Module', - lessons: [ - { title: 'Lesson 1', previewHTML: '
Preview
' } - ] - }; + test("should load a module from a URL", async () => { + const mockModule = { + id: "remote-module", + title: "Remote Module", + lessons: [{ title: "Lesson 1", previewHTML: "
Preview
" }] + }; - // Mock the fetch response - fetch.mockResolvedValueOnce({ - ok: true, - json: async () => mockModule - }); + // Mock the fetch response + fetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockModule + }); - const result = await loadModuleFromUrl('https://example.com/module.json'); + const result = await loadModuleFromUrl("https://example.com/module.json"); - expect(fetch).toHaveBeenCalledWith('https://example.com/module.json'); - expect(result).toEqual(mockModule); - }); + expect(fetch).toHaveBeenCalledWith("https://example.com/module.json"); + expect(result).toEqual(mockModule); + }); - test('should throw an error for failed fetch', async () => { - fetch.mockResolvedValueOnce({ - ok: false, - status: 404, - statusText: 'Not Found' - }); + test("should throw an error for failed fetch", async () => { + fetch.mockResolvedValueOnce({ + ok: false, + status: 404, + statusText: "Not Found" + }); - await expect(loadModuleFromUrl('https://example.com/not-found.json')) - .rejects - .toThrow('Failed to load module: 404 Not Found'); - }); + await expect(loadModuleFromUrl("https://example.com/not-found.json")).rejects.toThrow("Failed to load module: 404 Not Found"); + }); - test('should validate module structure', async () => { - // Missing required fields - const invalidModule = { - // Missing id - title: 'Invalid Module' - // Missing lessons array - }; + test("should validate module structure", async () => { + // Missing required fields + const invalidModule = { + // Missing id + title: "Invalid Module" + // Missing lessons array + }; - fetch.mockResolvedValueOnce({ - ok: true, - json: async () => invalidModule - }); + fetch.mockResolvedValueOnce({ + ok: true, + json: async () => invalidModule + }); - await expect(loadModuleFromUrl('https://example.com/invalid.json')) - .rejects - .toThrow('Module config missing "id"'); + await expect(loadModuleFromUrl("https://example.com/invalid.json")).rejects.toThrow('Module config missing "id"'); - // Invalid lessons structure - const moduleWithInvalidLessons = { - id: 'invalid-lessons', - title: 'Invalid Lessons', - lessons: [ - { /* Missing title */ previewHTML: '
Preview
' } - ] - }; + // Invalid lessons structure + const moduleWithInvalidLessons = { + id: "invalid-lessons", + title: "Invalid Lessons", + lessons: [{ /* Missing title */ previewHTML: "
Preview
" }] + }; - fetch.mockResolvedValueOnce({ - ok: true, - json: async () => moduleWithInvalidLessons - }); + fetch.mockResolvedValueOnce({ + ok: true, + json: async () => moduleWithInvalidLessons + }); - await expect(loadModuleFromUrl('https://example.com/invalid-lessons.json')) - .rejects - .toThrow('Lesson 0 missing "title"'); - }); - }); + await expect(loadModuleFromUrl("https://example.com/invalid-lessons.json")).rejects.toThrow('Lesson 0 missing "title"'); + }); + }); - describe('addCustomModule', () => { - test('should add a new module to the store', async () => { - // Load modules first to get current count - const initialModules = await loadModules(); - const initialCount = initialModules.length; + describe("addCustomModule", () => { + test("should add a new module to the store", async () => { + // Load modules first to get current count + const initialModules = await loadModules(); + const initialCount = initialModules.length; - const customModule = { - id: 'custom-module', - title: 'Custom Module', - lessons: [ - { title: 'Custom Lesson', previewHTML: '
Preview
' } - ] - }; + const customModule = { + id: "custom-module", + title: "Custom Module", + lessons: [{ title: "Custom Lesson", previewHTML: "
Preview
" }] + }; - const result = addCustomModule(customModule); - expect(result).toBe(true); + const result = addCustomModule(customModule); + expect(result).toBe(true); - // Check if module was added - const updatedModules = await loadModules(); - expect(updatedModules.length).toBe(initialCount + 1); + // Check if module was added + const updatedModules = await loadModules(); + expect(updatedModules.length).toBe(initialCount + 1); - const addedModule = getModuleById('custom-module'); - expect(addedModule).not.toBeNull(); - expect(addedModule.title).toBe('Custom Module'); - }); + const addedModule = getModuleById("custom-module"); + expect(addedModule).not.toBeNull(); + expect(addedModule.title).toBe("Custom Module"); + }); - test('should replace existing module with same ID', async () => { - // Add a module first - const customModule = { - id: 'replace-test', - title: 'Original Module', - lessons: [{ title: 'Original Lesson', previewHTML: '
Preview
' }] - }; + test("should replace existing module with same ID", async () => { + // Add a module first + const customModule = { + id: "replace-test", + title: "Original Module", + lessons: [{ title: "Original Lesson", previewHTML: "
Preview
" }] + }; - addCustomModule(customModule); + addCustomModule(customModule); - // Now replace it - const replacementModule = { - id: 'replace-test', - title: 'Replacement Module', - lessons: [{ title: 'New Lesson', previewHTML: '
New Preview
' }] - }; + // Now replace it + const replacementModule = { + id: "replace-test", + title: "Replacement Module", + lessons: [{ title: "New Lesson", previewHTML: "
New Preview
" }] + }; - const result = addCustomModule(replacementModule); - expect(result).toBe(true); + const result = addCustomModule(replacementModule); + expect(result).toBe(true); - // Check if module was replaced - const updatedModule = getModuleById('replace-test'); - expect(updatedModule.title).toBe('Replacement Module'); - }); + // Check if module was replaced + const updatedModule = getModuleById("replace-test"); + expect(updatedModule.title).toBe("Replacement Module"); + }); - test('should validate module before adding', () => { - const invalidModule = { - // Missing required fields - title: 'Invalid Module' - }; + test("should validate module before adding", () => { + const invalidModule = { + // Missing required fields + title: "Invalid Module" + }; - const result = addCustomModule(invalidModule); - expect(result).toBe(false); - }); - }); -}); \ No newline at end of file + const result = addCustomModule(invalidModule); + expect(result).toBe(false); + }); + }); +}); diff --git a/tests/unit/renderer.test.js b/tests/unit/renderer.test.js index dd7c826..f434239 100644 --- a/tests/unit/renderer.test.js +++ b/tests/unit/renderer.test.js @@ -1,10 +1,10 @@ -import { describe, test, expect, vi, beforeEach } from 'vitest'; -import { renderModuleList, renderLesson, renderLevelIndicator, showFeedback, clearFeedback } from '../../src/helpers/renderer.js'; +import { describe, test, expect, vi, beforeEach } from "vitest"; +import { renderModuleList, renderLesson, renderLevelIndicator, showFeedback, clearFeedback } from "../../src/helpers/renderer.js"; -describe('Renderer Module', () => { - beforeEach(() => { - // Reset the DOM between tests - document.body.innerHTML = ` +describe("Renderer Module", () => { + beforeEach(() => { + // Reset the DOM between tests + document.body.innerHTML = `

@@ -16,163 +16,145 @@ describe('Renderer Module', () => {
`; - }); + }); - describe('renderModuleList', () => { - test('should render a list of modules', () => { - const container = document.getElementById('module-list'); - const modules = [ - { id: 'mod1', title: 'Module 1' }, - { id: 'mod2', title: 'Module 2' } - ]; - const onSelectModule = vi.fn(); + describe("renderModuleList", () => { + test("should render a list of modules", () => { + const container = document.getElementById("module-list"); + const modules = [ + { id: "mod1", title: "Module 1" }, + { id: "mod2", title: "Module 2" } + ]; + const onSelectModule = vi.fn(); - renderModuleList(container, modules, onSelectModule); + renderModuleList(container, modules, onSelectModule); - // Check if heading is created - expect(container.innerHTML).toContain('

Modules

'); + // Check if heading is created + expect(container.innerHTML).toContain("

Modules

"); - // Check if module items are created - const moduleItems = container.querySelectorAll('.module-list-item'); - expect(moduleItems.length).toBe(2); - expect(moduleItems[0].textContent).toBe('Module 1'); - expect(moduleItems[1].textContent).toBe('Module 2'); + // Check if module items are created + const moduleItems = container.querySelectorAll(".module-list-item"); + expect(moduleItems.length).toBe(2); + expect(moduleItems[0].textContent).toBe("Module 1"); + expect(moduleItems[1].textContent).toBe("Module 2"); - // Test click event - moduleItems[0].click(); - expect(onSelectModule).toHaveBeenCalledWith('mod1'); - }); + // Test click event + moduleItems[0].click(); + expect(onSelectModule).toHaveBeenCalledWith("mod1"); + }); - test('should handle empty module list', () => { - const container = document.getElementById('module-list'); - renderModuleList(container, [], vi.fn()); + test("should handle empty module list", () => { + const container = document.getElementById("module-list"); + renderModuleList(container, [], vi.fn()); - expect(container.innerHTML).toContain('

Modules

'); - expect(container.querySelectorAll('.module-list-item').length).toBe(0); - }); - }); + expect(container.innerHTML).toContain("

Modules

"); + expect(container.querySelectorAll(".module-list-item").length).toBe(0); + }); + }); - describe('renderLesson', () => { - test('should render lesson content correctly', () => { - const titleEl = document.getElementById('title'); - const descriptionEl = document.getElementById('description'); - const taskEl = document.getElementById('task'); - const previewEl = document.getElementById('preview'); - const prefixEl = document.getElementById('prefix'); - const inputEl = document.getElementById('input'); - const suffixEl = document.getElementById('suffix'); + describe("renderLesson", () => { + test("should render lesson content correctly", () => { + const titleEl = document.getElementById("title"); + const descriptionEl = document.getElementById("description"); + const taskEl = document.getElementById("task"); + const previewEl = document.getElementById("preview"); + const prefixEl = document.getElementById("prefix"); + const inputEl = document.getElementById("input"); + const suffixEl = document.getElementById("suffix"); - const lesson = { - title: 'Test Lesson', - description: '

Description text

', - task: '

Task instructions

', - codePrefix: 'body {', - initialCode: ' color: red;', - codeSuffix: '}' - }; + const lesson = { + title: "Test Lesson", + description: "

Description text

", + task: "

Task instructions

", + codePrefix: "body {", + initialCode: " color: red;", + codeSuffix: "}" + }; - renderLesson( - titleEl, - descriptionEl, - taskEl, - previewEl, - prefixEl, - inputEl, - suffixEl, - lesson - ); + renderLesson(titleEl, descriptionEl, taskEl, previewEl, prefixEl, inputEl, suffixEl, lesson); - expect(titleEl.textContent).toBe('Test Lesson'); - expect(descriptionEl.innerHTML).toBe('

Description text

'); - expect(taskEl.innerHTML).toBe('

Task instructions

'); - expect(prefixEl.textContent).toBe('body {'); - expect(inputEl.value).toBe(' color: red;'); - expect(suffixEl.textContent).toBe('}'); - }); + expect(titleEl.textContent).toBe("Test Lesson"); + expect(descriptionEl.innerHTML).toBe("

Description text

"); + expect(taskEl.innerHTML).toBe("

Task instructions

"); + expect(prefixEl.textContent).toBe("body {"); + expect(inputEl.value).toBe(" color: red;"); + expect(suffixEl.textContent).toBe("}"); + }); - test('should handle missing lesson data with defaults', () => { - const titleEl = document.getElementById('title'); - const descriptionEl = document.getElementById('description'); - const taskEl = document.getElementById('task'); - const prefixEl = document.getElementById('prefix'); - const inputEl = document.getElementById('input'); - const suffixEl = document.getElementById('suffix'); + test("should handle missing lesson data with defaults", () => { + const titleEl = document.getElementById("title"); + const descriptionEl = document.getElementById("description"); + const taskEl = document.getElementById("task"); + const prefixEl = document.getElementById("prefix"); + const inputEl = document.getElementById("input"); + const suffixEl = document.getElementById("suffix"); - // Empty lesson object - const lesson = {}; + // Empty lesson object + const lesson = {}; - renderLesson( - titleEl, - descriptionEl, - taskEl, - document.getElementById('preview'), - prefixEl, - inputEl, - suffixEl, - lesson - ); + renderLesson(titleEl, descriptionEl, taskEl, document.getElementById("preview"), prefixEl, inputEl, suffixEl, lesson); - expect(titleEl.textContent).toBe('Untitled Lesson'); - expect(descriptionEl.innerHTML).toBe(''); - expect(taskEl.innerHTML).toBe(''); - expect(prefixEl.textContent).toBe(''); - expect(inputEl.value).toBe(''); - expect(suffixEl.textContent).toBe(''); - }); - }); + expect(titleEl.textContent).toBe("Untitled Lesson"); + expect(descriptionEl.innerHTML).toBe(""); + expect(taskEl.innerHTML).toBe(""); + expect(prefixEl.textContent).toBe(""); + expect(inputEl.value).toBe(""); + expect(suffixEl.textContent).toBe(""); + }); + }); - describe('renderLevelIndicator', () => { - test('should update level indicator text', () => { - const element = document.getElementById('level-indicator'); + describe("renderLevelIndicator", () => { + test("should update level indicator text", () => { + const element = document.getElementById("level-indicator"); - renderLevelIndicator(element, 3, 10); - expect(element.textContent).toBe('Lesson 3 of 10'); + renderLevelIndicator(element, 3, 10); + expect(element.textContent).toBe("Lesson 3 of 10"); - renderLevelIndicator(element, 1, 5); - expect(element.textContent).toBe('Lesson 1 of 5'); - }); - }); + renderLevelIndicator(element, 1, 5); + expect(element.textContent).toBe("Lesson 1 of 5"); + }); + }); - describe.skip('showFeedback and clearFeedback', () => { - test('should create success feedback element', () => { - const editor = document.getElementById('code-editor'); - showFeedback(true, 'Great job!'); + describe.skip("showFeedback and clearFeedback", () => { + test("should create success feedback element", () => { + const editor = document.getElementById("code-editor"); + showFeedback(true, "Great job!"); - const feedback = document.querySelector('.feedback-success'); - expect(feedback).not.toBeNull(); - expect(feedback.textContent).toBe('Great job!'); + const feedback = document.querySelector(".feedback-success"); + expect(feedback).not.toBeNull(); + expect(feedback.textContent).toBe("Great job!"); - // Test auto clearing with setTimeout - vi.useFakeTimers(); - showFeedback(true, 'Auto clear test'); - vi.advanceTimersByTime(5001); - expect(document.querySelector('.feedback-success')).toBeNull(); - vi.useRealTimers(); - }); + // Test auto clearing with setTimeout + vi.useFakeTimers(); + showFeedback(true, "Auto clear test"); + vi.advanceTimersByTime(5001); + expect(document.querySelector(".feedback-success")).toBeNull(); + vi.useRealTimers(); + }); - test('should create error feedback element', () => { - showFeedback(false, 'Try again'); + test("should create error feedback element", () => { + showFeedback(false, "Try again"); - const feedback = document.querySelector('.feedback-error'); - expect(feedback).not.toBeNull(); - expect(feedback.textContent).toBe('Try again'); + const feedback = document.querySelector(".feedback-error"); + expect(feedback).not.toBeNull(); + expect(feedback.textContent).toBe("Try again"); - // Error feedback should not auto-clear - vi.useFakeTimers(); - vi.advanceTimersByTime(5001); - expect(document.querySelector('.feedback-error')).not.toBeNull(); - vi.useRealTimers(); - }); + // Error feedback should not auto-clear + vi.useFakeTimers(); + vi.advanceTimersByTime(5001); + expect(document.querySelector(".feedback-error")).not.toBeNull(); + vi.useRealTimers(); + }); - test('should clear existing feedback', () => { - showFeedback(false, 'Error message'); - expect(document.querySelector('.feedback-error')).not.toBeNull(); + test("should clear existing feedback", () => { + showFeedback(false, "Error message"); + expect(document.querySelector(".feedback-error")).not.toBeNull(); - clearFeedback(); - expect(document.querySelector('.feedback-error')).toBeNull(); + clearFeedback(); + expect(document.querySelector(".feedback-error")).toBeNull(); - // Should work when called multiple times - clearFeedback(); - }); - }); -}); \ No newline at end of file + // Should work when called multiple times + clearFeedback(); + }); + }); +}); diff --git a/tests/unit/validator.test.js b/tests/unit/validator.test.js index faa93a7..68af16e 100644 --- a/tests/unit/validator.test.js +++ b/tests/unit/validator.test.js @@ -1,239 +1,227 @@ -import { describe, it, expect, vi } from 'vitest'; -import { validateUserCode } from '../../src/helpers/validator.js'; +import { describe, it, expect, vi } from "vitest"; +import { validateUserCode } from "../../src/helpers/validator.js"; -describe('CSS Validator', () => { - // Mock document functions since we're not in a browser - document.createElement = vi.fn().mockImplementation(() => { - return { - textContent: '', - parentNode: { removeChild: vi.fn() } - }; - }); +describe("CSS Validator", () => { + // Mock document functions since we're not in a browser + document.createElement = vi.fn().mockImplementation(() => { + return { + textContent: "", + parentNode: { removeChild: vi.fn() } + }; + }); - // document.head = { - // appendChild: vi.fn(), - // removeChild: vi.fn() - // }; + // document.head = { + // appendChild: vi.fn(), + // removeChild: vi.fn() + // }; - describe('validateUserCode', () => { - it('should pass when no validations are specified', () => { - const userCode = 'div { color: red; }'; - const lesson = { title: 'Test Lesson' }; + describe("validateUserCode", () => { + it("should pass when no validations are specified", () => { + const userCode = "div { color: red; }"; + const lesson = { title: "Test Lesson" }; - const result = validateUserCode(userCode, lesson); + const result = validateUserCode(userCode, lesson); - expect(result.isValid).toBe(true); - expect(result.message).toContain('No validations specified'); - }); + expect(result.isValid).toBe(true); + expect(result.message).toContain("No validations specified"); + }); - it('should pass with empty validations array', () => { - const userCode = 'div { color: red; }'; - const lesson = { - title: 'Test Lesson', - validations: [] - }; + it("should pass with empty validations array", () => { + const userCode = "div { color: red; }"; + const lesson = { + title: "Test Lesson", + validations: [] + }; - const result = validateUserCode(userCode, lesson); + const result = validateUserCode(userCode, lesson); - expect(result.isValid).toBe(true); - expect(result.message).toBe('Your code looks good!'); - }); + expect(result.isValid).toBe(true); + expect(result.message).toBe("Your code looks good!"); + }); - it('should validate "contains" rule correctly', () => { - const userCode = 'div { color: red; }'; - const lesson = { - validations: [ - { type: 'contains', value: 'color: red', message: 'Should use red color' } - ] - }; + it('should validate "contains" rule correctly', () => { + const userCode = "div { color: red; }"; + const lesson = { + validations: [{ type: "contains", value: "color: red", message: "Should use red color" }] + }; - const result = validateUserCode(userCode, lesson); - expect(result.isValid).toBe(true); + const result = validateUserCode(userCode, lesson); + expect(result.isValid).toBe(true); - const failLesson = { - validations: [ - { type: 'contains', value: 'color: blue', message: 'Should use blue color' } - ] - }; + const failLesson = { + validations: [{ type: "contains", value: "color: blue", message: "Should use blue color" }] + }; - const failResult = validateUserCode(userCode, failLesson); - expect(failResult.isValid).toBe(false); - expect(failResult.message).toBe('Should use blue color'); - }); + const failResult = validateUserCode(userCode, failLesson); + expect(failResult.isValid).toBe(false); + expect(failResult.message).toBe("Should use blue color"); + }); - it('should validate "not_contains" rule correctly', () => { - const userCode = 'div { color: red; }'; - const lesson = { - validations: [ - { type: 'not_contains', value: 'color: blue', message: 'Should not use blue color' } - ] - }; + it('should validate "not_contains" rule correctly', () => { + const userCode = "div { color: red; }"; + const lesson = { + validations: [{ type: "not_contains", value: "color: blue", message: "Should not use blue color" }] + }; - const result = validateUserCode(userCode, lesson); - expect(result.isValid).toBe(true); + const result = validateUserCode(userCode, lesson); + expect(result.isValid).toBe(true); - const failLesson = { - validations: [ - { type: 'not_contains', value: 'color: red', message: 'Should not use red color' } - ] - }; + const failLesson = { + validations: [{ type: "not_contains", value: "color: red", message: "Should not use red color" }] + }; - const failResult = validateUserCode(userCode, failLesson); - expect(failResult.isValid).toBe(false); - expect(failResult.message).toBe('Should not use red color'); - }); + const failResult = validateUserCode(userCode, failLesson); + expect(failResult.isValid).toBe(false); + expect(failResult.message).toBe("Should not use red color"); + }); - it('should validate "regex" rule correctly', () => { - const userCode = 'div { color: #ff0000; }'; - const lesson = { - validations: [ - { type: 'regex', value: '#[a-f0-9]{6}', message: 'Should use hex color' } - ] - }; + it('should validate "regex" rule correctly', () => { + const userCode = "div { color: #ff0000; }"; + const lesson = { + validations: [{ type: "regex", value: "#[a-f0-9]{6}", message: "Should use hex color" }] + }; - const result = validateUserCode(userCode, lesson); - expect(result.isValid).toBe(true); + const result = validateUserCode(userCode, lesson); + expect(result.isValid).toBe(true); - const failLesson = { - validations: [ - { type: 'regex', value: 'rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)', message: 'Should use RGB color' } - ] - }; + const failLesson = { + validations: [{ type: "regex", value: "rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)", message: "Should use RGB color" }] + }; - const failResult = validateUserCode(userCode, failLesson); - expect(failResult.isValid).toBe(false); - expect(failResult.message).toBe('Should use RGB color'); - }); + const failResult = validateUserCode(userCode, failLesson); + expect(failResult.isValid).toBe(false); + expect(failResult.message).toBe("Should use RGB color"); + }); - it('should validate "property_value" rule correctly', () => { - const userCode = 'div { display: flex; }'; - const lesson = { - validations: [ - { - type: 'property_value', - value: { property: 'display', expected: 'flex' }, - message: 'Should use display: flex' - } - ] - }; + it('should validate "property_value" rule correctly', () => { + const userCode = "div { display: flex; }"; + const lesson = { + validations: [ + { + type: "property_value", + value: { property: "display", expected: "flex" }, + message: "Should use display: flex" + } + ] + }; - const result = validateUserCode(userCode, lesson); - expect(result.isValid).toBe(true); + const result = validateUserCode(userCode, lesson); + expect(result.isValid).toBe(true); - const failLesson = { - validations: [ - { - type: 'property_value', - value: { property: 'display', expected: 'grid' }, - message: 'Should use display: grid' - } - ] - }; + const failLesson = { + validations: [ + { + type: "property_value", + value: { property: "display", expected: "grid" }, + message: "Should use display: grid" + } + ] + }; - const failResult = validateUserCode(userCode, failLesson); - expect(failResult.isValid).toBe(false); - expect(failResult.message).toBe('Should use display: grid'); - }); + const failResult = validateUserCode(userCode, failLesson); + expect(failResult.isValid).toBe(false); + expect(failResult.message).toBe("Should use display: grid"); + }); - it('should handle complex validation chains', () => { - const userCode = 'div { display: flex; color: red; }'; - const lesson = { - validations: [ - { type: 'contains', value: 'display: flex' }, - { type: 'contains', value: 'color: red' }, - { type: 'not_contains', value: 'float:' } - ] - }; + it("should handle complex validation chains", () => { + const userCode = "div { display: flex; color: red; }"; + const lesson = { + validations: [ + { type: "contains", value: "display: flex" }, + { type: "contains", value: "color: red" }, + { type: "not_contains", value: "float:" } + ] + }; - const result = validateUserCode(userCode, lesson); - expect(result.isValid).toBe(true); + const result = validateUserCode(userCode, lesson); + expect(result.isValid).toBe(true); - // First failing validation should cause early return - const failLesson = { - validations: [ - { type: 'contains', value: 'display: flex' }, - { type: 'contains', value: 'border: 1px solid black', message: 'Missing border' }, - { type: 'not_contains', value: 'color: green' } - ] - }; + // First failing validation should cause early return + const failLesson = { + validations: [ + { type: "contains", value: "display: flex" }, + { type: "contains", value: "border: 1px solid black", message: "Missing border" }, + { type: "not_contains", value: "color: green" } + ] + }; - const failResult = validateUserCode(userCode, failLesson); - expect(failResult.isValid).toBe(false); - expect(failResult.message).toBe('Missing border'); - }); + const failResult = validateUserCode(userCode, failLesson); + expect(failResult.isValid).toBe(false); + expect(failResult.message).toBe("Missing border"); + }); - it('should validate "custom" rule correctly', () => { - const userCode = 'div { margin: 10px; }'; - const customValidator = (code) => { - return { - isValid: code.includes('margin'), - message: 'Should include margin property' - }; - }; + it('should validate "custom" rule correctly', () => { + const userCode = "div { margin: 10px; }"; + const customValidator = (code) => { + return { + isValid: code.includes("margin"), + message: "Should include margin property" + }; + }; - const lesson = { - validations: [ - { - type: 'custom', - validator: customValidator - } - ] - }; + const lesson = { + validations: [ + { + type: "custom", + validator: customValidator + } + ] + }; - const result = validateUserCode(userCode, lesson); - expect(result.isValid).toBe(true); + const result = validateUserCode(userCode, lesson); + expect(result.isValid).toBe(true); - const failValidator = (code) => { - return { - isValid: code.includes('padding'), - message: 'Should include padding property' - }; - }; + const failValidator = (code) => { + return { + isValid: code.includes("padding"), + message: "Should include padding property" + }; + }; - const failLesson = { - validations: [ - { - type: 'custom', - validator: failValidator, - message: 'Custom validation failed' - } - ] - }; + const failLesson = { + validations: [ + { + type: "custom", + validator: failValidator, + message: "Custom validation failed" + } + ] + }; - const failResult = validateUserCode(userCode, failLesson); - expect(failResult.isValid).toBe(false); - expect(failResult.message).toBe('Should include padding property'); - }); + const failResult = validateUserCode(userCode, failLesson); + expect(failResult.isValid).toBe(false); + expect(failResult.message).toBe("Should include padding property"); + }); - it('should handle options in validations', () => { - // Case insensitive test - const userCode = 'div { COLOR: Red; }'; - const lesson = { - validations: [ - { - type: 'contains', - value: 'color: red', - options: { caseSensitive: false } - } - ] - }; + it("should handle options in validations", () => { + // Case insensitive test + const userCode = "div { COLOR: Red; }"; + const lesson = { + validations: [ + { + type: "contains", + value: "color: red", + options: { caseSensitive: false } + } + ] + }; - const result = validateUserCode(userCode, lesson); - expect(result.isValid).toBe(true); + const result = validateUserCode(userCode, lesson); + expect(result.isValid).toBe(true); - // With exact match required - const exactLesson = { - validations: [ - { - type: 'property_value', - value: { property: 'color', expected: 'red' }, - options: { exact: true } - } - ] - }; + // With exact match required + const exactLesson = { + validations: [ + { + type: "property_value", + value: { property: "color", expected: "red" }, + options: { exact: true } + } + ] + }; - const failExactResult = validateUserCode('div { color: RED; }', exactLesson); - expect(failExactResult.isValid).toBe(false); - }); - }); -}); \ No newline at end of file + const failExactResult = validateUserCode("div { color: RED; }", exactLesson); + expect(failExactResult.isValid).toBe(false); + }); + }); +}); diff --git a/vite.config.js b/vite.config.js index 9fd7261..5ea77b3 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,15 +1,15 @@ -import { defineConfig } from 'vite'; +import { defineConfig } from "vite"; export default defineConfig({ - root: './src', - publicDir: './public', - build: { - outDir: '../dist', - emptyOutDir: true, - sourcemap: true - }, - server: { - port: 1312, - open: false - } -}); \ No newline at end of file + root: "./src", + publicDir: "./public", + build: { + outDir: "../dist", + emptyOutDir: true, + sourcemap: true + }, + server: { + port: 1312, + open: false + } +}); diff --git a/vitest.config.js b/vitest.config.js index 93402b0..44b7d05 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -1,20 +1,20 @@ // vitest.config.js -import { defineConfig } from 'vitest/config'; +import { defineConfig } from "vitest/config"; export default defineConfig({ - test: { - globals: true, - environment: 'jsdom', - setupFiles: ['./tests/setup.js'], - include: ['tests/**/*.{test,spec}.js'], - coverage: { - reporter: ['text', 'json', 'html'], - exclude: ['node_modules/', 'tests/setup.js'] - }, - server: { - deps: { - inline: true - } - } - } -}); \ No newline at end of file + test: { + globals: true, + environment: "jsdom", + setupFiles: ["./tests/setup.js"], + include: ["tests/**/*.{test,spec}.js"], + coverage: { + reporter: ["text", "json", "html"], + exclude: ["node_modules/", "tests/setup.js"] + }, + server: { + deps: { + inline: true + } + } + } +});