diff --git a/.github/workflows/security-comprehensive.yml b/.github/workflows/security-comprehensive.yml index 0e85f7a6..77699d3d 100644 --- a/.github/workflows/security-comprehensive.yml +++ b/.github/workflows/security-comprehensive.yml @@ -271,26 +271,8 @@ jobs: run: | python scripts/aio-version-checker.py \ --iac-type ${{ inputs.iac-types }} \ - --output-format json \ - --output-path aio-version-check-results.json - - # Parse results for outputs - if [[ -f "aio-version-check-results.json" ]]; then - issues=$(jq '.issues | length' aio-version-check-results.json 2>/dev/null || echo "0") - echo "issues=$issues" >> $GITHUB_OUTPUT - echo "AIO version check completed with $issues issues" - else - echo "issues=0" >> $GITHUB_OUTPUT - echo "AIO version check completed (no results file)" - fi - - - name: Upload AIO version results - if: always() - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: aio-version-check-results - path: aio-version-check-results.json - retention-days: 30 + --error-on-mismatch \ + --verbose # Comprehensive dependency pinning analysis security-analysis: diff --git a/.github/workflows/security-deployment.yml b/.github/workflows/security-deployment.yml index 192cf07b..4d2fd4af 100644 --- a/.github/workflows/security-deployment.yml +++ b/.github/workflows/security-deployment.yml @@ -180,16 +180,8 @@ jobs: run: | python scripts/aio-version-checker.py \ --iac-type ${{ inputs.iac-types }} \ - --break-build ${{ inputs.break-build }} \ - --output-format json - - - name: Upload AIO version results - if: always() - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: aio-version-check-results - path: aio-version-check-results.json - retention-days: 30 + --error-on-mismatch \ + --verbose # Comprehensive security validation summary security-validation-summary: diff --git a/.github/workflows/shell-lint.yml b/.github/workflows/shell-lint.yml index b7d790be..eb0bf678 100644 --- a/.github/workflows/shell-lint.yml +++ b/.github/workflows/shell-lint.yml @@ -79,7 +79,7 @@ jobs: id: shellcheck run: | find . -name '*.sh' -not -path './node_modules/*' -not -path './.copilot-tracking/*' -print0 \ - | xargs -0 -r shellcheck --rcfile .shellcheckrc --format=gcc > shellcheck-output.txt 2>&1 || echo "SHELLCHECK_FAILED=true" >> "$GITHUB_ENV" + | xargs -0 -r shellcheck --format=gcc > shellcheck-output.txt 2>&1 || echo "SHELLCHECK_FAILED=true" >> "$GITHUB_ENV" cat shellcheck-output.txt continue-on-error: true diff --git a/.github/workflows/terraform-lint.yml b/.github/workflows/terraform-lint.yml index b9b578ea..dd303571 100644 --- a/.github/workflows/terraform-lint.yml +++ b/.github/workflows/terraform-lint.yml @@ -73,8 +73,8 @@ jobs: - name: TFLint init and check id: tflint run: | - tflint --init - tflint --recursive > tflint-output.txt 2>&1 || echo "TFLINT_FAILED=true" >> "$GITHUB_ENV" + tflint --init --config "$(pwd)/.tflint.hcl" + tflint --recursive --config "$(pwd)/.tflint.hcl" > tflint-output.txt 2>&1 || echo "TFLINT_FAILED=true" >> "$GITHUB_ENV" cat tflint-output.txt continue-on-error: true diff --git a/blueprints/full-single-node-cluster/tests/run-contract-tests.sh b/blueprints/full-single-node-cluster/tests/run-contract-tests.sh index 1ef5c002..5c888de2 100755 --- a/blueprints/full-single-node-cluster/tests/run-contract-tests.sh +++ b/blueprints/full-single-node-cluster/tests/run-contract-tests.sh @@ -15,7 +15,7 @@ BLUE='\033[0;34m' NC='\033[0m' # No Color print_usage() { - cat </dev/null; then - echo -e "${RED}✗ Go not found. Please install Go toolchain.${NC}" - exit 1 + echo -e "${RED}✗ Go not found. Please install Go toolchain.${NC}" + exit 1 fi echo -e "${GREEN}✓ Go: $(go version | awk '{print $3}')${NC}" # Check terraform-docs if [[ "$TEST_TYPE" == "terraform" || "$TEST_TYPE" == "both" ]]; then - if ! command -v terraform-docs &>/dev/null; then - echo -e "${RED}✗ terraform-docs not found${NC}" - echo -e "${YELLOW} Install: brew install terraform-docs${NC}" - exit 1 - fi - echo -e "${GREEN}✓ terraform-docs: $(terraform-docs version | head -n1)${NC}" + if ! command -v terraform-docs &>/dev/null; then + echo -e "${RED}✗ terraform-docs not found${NC}" + echo -e "${YELLOW} Install: brew install terraform-docs${NC}" + exit 1 + fi + echo -e "${GREEN}✓ terraform-docs: $(terraform-docs version | head -n1)${NC}" fi # Check az bicep if [[ "$TEST_TYPE" == "bicep" || "$TEST_TYPE" == "both" ]]; then - if ! command -v az &>/dev/null; then - echo -e "${RED}✗ Azure CLI not found${NC}" - echo -e "${YELLOW} Install: https://docs.microsoft.com/cli/azure/install-azure-cli${NC}" - exit 1 - fi - - # Check bicep is installed - if ! az bicep version &>/dev/null; then - echo -e "${RED}✗ Bicep not installed${NC}" - echo -e "${YELLOW} Install: az bicep install${NC}" - exit 1 - fi + if ! command -v az &>/dev/null; then + echo -e "${RED}✗ Azure CLI not found${NC}" + echo -e "${YELLOW} Install: https://docs.microsoft.com/cli/azure/install-azure-cli${NC}" + exit 1 + fi + + # Check bicep is installed + if ! az bicep version &>/dev/null; then + echo -e "${RED}✗ Bicep not installed${NC}" + echo -e "${YELLOW} Install: az bicep install${NC}" + exit 1 + fi fi echo "" @@ -124,30 +124,30 @@ echo "" EXIT_CODE=0 run_test() { - local test_name=$1 - local test_pattern=$2 - - echo -e "${BLUE}──────────────────────────────────────────────────────────${NC}" - echo -e "${YELLOW}Running: $test_name${NC}" - echo -e "${BLUE}──────────────────────────────────────────────────────────${NC}" - - if go test $VERBOSE_FLAG -run "$test_pattern" .; then - echo -e "${GREEN}✓ $test_name PASSED${NC}" - else - echo -e "${RED}✗ $test_name FAILED${NC}" - EXIT_CODE=1 - fi - echo "" + local test_name=$1 + local test_pattern=$2 + + echo -e "${BLUE}──────────────────────────────────────────────────────────${NC}" + echo -e "${YELLOW}Running: $test_name${NC}" + echo -e "${BLUE}──────────────────────────────────────────────────────────${NC}" + + if go test $VERBOSE_FLAG -run "$test_pattern" .; then + echo -e "${GREEN}✓ $test_name PASSED${NC}" + else + echo -e "${RED}✗ $test_name FAILED${NC}" + EXIT_CODE=1 + fi + echo "" } case $TEST_TYPE in -terraform) + terraform) run_test "Terraform Contract Test" "TestTerraformOutputsContract" ;; -bicep) + bicep) run_test "Bicep Contract Test" "TestBicepOutputsContract" ;; -both) + both) run_test "Terraform Contract Test" "TestTerraformOutputsContract" run_test "Bicep Contract Test" "TestBicepOutputsContract" ;; @@ -156,9 +156,9 @@ esac # Summary echo -e "${BLUE}╔════════════════════════════════════════════════════════════╗${NC}" if [[ $EXIT_CODE -eq 0 ]]; then - echo -e "${BLUE}║${GREEN} All Tests PASSED ✓ ${BLUE}║${NC}" + echo -e "${BLUE}║${GREEN} All Tests PASSED ✓ ${BLUE}║${NC}" else - echo -e "${BLUE}║${RED} Some Tests FAILED ✗ ${BLUE}║${NC}" + echo -e "${BLUE}║${RED} Some Tests FAILED ✗ ${BLUE}║${NC}" fi echo -e "${BLUE}╚════════════════════════════════════════════════════════════╝${NC}" diff --git a/blueprints/full-single-node-cluster/tests/run-deployment-tests.sh b/blueprints/full-single-node-cluster/tests/run-deployment-tests.sh index e9126197..2f5868fe 100755 --- a/blueprints/full-single-node-cluster/tests/run-deployment-tests.sh +++ b/blueprints/full-single-node-cluster/tests/run-deployment-tests.sh @@ -13,26 +13,26 @@ YELLOW='\033[1;33m' NC='\033[0m' # No Color print_usage() { - echo "Usage: $0 [terraform|bicep|both] [options]" - echo "" - echo "Arguments:" - echo " terraform Run only Terraform deployment tests" - echo " bicep Run only Bicep deployment tests" - echo " both Run both Terraform and Bicep tests (default)" - echo "" - echo "Options:" - echo " -v, --verbose Enable verbose test output" - echo " -h, --help Show this help message" - echo "" - echo "Environment Variables:" - echo " ARM_SUBSCRIPTION_ID Azure subscription ID (auto-detected if not set)" - echo " ADMIN_PASSWORD (Required for Bicep) VM admin password" - echo " CUSTOM_LOCATIONS_OID Custom Locations OID (auto-detected if not set)" - echo "" - echo "Examples:" - echo " $0 terraform" - echo " $0 bicep -v" - echo " $0 both" + echo "Usage: $0 [terraform|bicep|both] [options]" + echo "" + echo "Arguments:" + echo " terraform Run only Terraform deployment tests" + echo " bicep Run only Bicep deployment tests" + echo " both Run both Terraform and Bicep tests (default)" + echo "" + echo "Options:" + echo " -v, --verbose Enable verbose test output" + echo " -h, --help Show this help message" + echo "" + echo "Environment Variables:" + echo " ARM_SUBSCRIPTION_ID Azure subscription ID (auto-detected if not set)" + echo " ADMIN_PASSWORD (Required for Bicep) VM admin password" + echo " CUSTOM_LOCATIONS_OID Custom Locations OID (auto-detected if not set)" + echo "" + echo "Examples:" + echo " $0 terraform" + echo " $0 bicep -v" + echo " $0 both" } # Parse arguments @@ -40,59 +40,59 @@ DEPLOYMENT_TYPE="both" VERBOSE_FLAG="" while [[ $# -gt 0 ]]; do - case $1 in + case $1 in terraform | bicep | both) - DEPLOYMENT_TYPE="$1" - shift - ;; + DEPLOYMENT_TYPE="$1" + shift + ;; -v | --verbose) - VERBOSE_FLAG="-v" - shift - ;; + VERBOSE_FLAG="-v" + shift + ;; -h | --help) - print_usage - exit 0 - ;; + print_usage + exit 0 + ;; *) - echo -e "${RED}Unknown option: $1${NC}" - print_usage - exit 1 - ;; - esac + echo -e "${RED}Unknown option: $1${NC}" + print_usage + exit 1 + ;; + esac done # Auto-detect ARM_SUBSCRIPTION_ID if not set if [[ -z "${ARM_SUBSCRIPTION_ID}" ]]; then - echo -e "${YELLOW}ARM_SUBSCRIPTION_ID not set, detecting from Azure CLI...${NC}" - ARM_SUBSCRIPTION_ID=$(az account show --query id -o tsv 2>/dev/null) - if [[ -z "${ARM_SUBSCRIPTION_ID}" ]]; then - echo -e "${RED}Error: Could not auto-detect ARM_SUBSCRIPTION_ID. Please run 'az login' or set ARM_SUBSCRIPTION_ID${NC}" - exit 1 - fi - echo -e "${GREEN}Detected subscription: ${ARM_SUBSCRIPTION_ID}${NC}" - export ARM_SUBSCRIPTION_ID + echo -e "${YELLOW}ARM_SUBSCRIPTION_ID not set, detecting from Azure CLI...${NC}" + ARM_SUBSCRIPTION_ID=$(az account show --query id -o tsv 2>/dev/null) + if [[ -z "${ARM_SUBSCRIPTION_ID}" ]]; then + echo -e "${RED}Error: Could not auto-detect ARM_SUBSCRIPTION_ID. Please run 'az login' or set ARM_SUBSCRIPTION_ID${NC}" + exit 1 + fi + echo -e "${GREEN}Detected subscription: ${ARM_SUBSCRIPTION_ID}${NC}" + export ARM_SUBSCRIPTION_ID fi # Auto-detect CUSTOM_LOCATIONS_OID if not set (for Bicep tests) if [[ -z "${CUSTOM_LOCATIONS_OID}" ]] && [[ "$DEPLOYMENT_TYPE" == "bicep" || "$DEPLOYMENT_TYPE" == "both" ]]; then - echo -e "${YELLOW}CUSTOM_LOCATIONS_OID not set, detecting from Azure AD...${NC}" - CUSTOM_LOCATIONS_OID=$(az ad sp show --id bc313c14-388c-4e7d-a58e-70017303ee3b --query id -o tsv 2>/dev/null) - if [[ -z "${CUSTOM_LOCATIONS_OID}" ]]; then - echo -e "${RED}Error: Could not auto-detect CUSTOM_LOCATIONS_OID. Please ensure you have permissions to query Azure AD${NC}" - exit 1 - fi - echo -e "${GREEN}Detected Custom Locations OID: ${CUSTOM_LOCATIONS_OID}${NC}" - export CUSTOM_LOCATIONS_OID + echo -e "${YELLOW}CUSTOM_LOCATIONS_OID not set, detecting from Azure AD...${NC}" + CUSTOM_LOCATIONS_OID=$(az ad sp show --id bc313c14-388c-4e7d-a58e-70017303ee3b --query id -o tsv 2>/dev/null) + if [[ -z "${CUSTOM_LOCATIONS_OID}" ]]; then + echo -e "${RED}Error: Could not auto-detect CUSTOM_LOCATIONS_OID. Please ensure you have permissions to query Azure AD${NC}" + exit 1 + fi + echo -e "${GREEN}Detected Custom Locations OID: ${CUSTOM_LOCATIONS_OID}${NC}" + export CUSTOM_LOCATIONS_OID fi # Generate strong admin password if not provided (for Bicep tests) if [[ -z "${ADMIN_PASSWORD}" ]] && [[ "$DEPLOYMENT_TYPE" == "bicep" || "$DEPLOYMENT_TYPE" == "both" ]]; then - echo -e "${YELLOW}ADMIN_PASSWORD not set, generating strong password...${NC}" - ADMIN_PASSWORD=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-24) - # Ensure password meets Azure complexity requirements (uppercase, lowercase, digit, special char) - ADMIN_PASSWORD="Aa1!${ADMIN_PASSWORD}" - echo -e "${GREEN}Generated admin password (save this): ${ADMIN_PASSWORD}${NC}" - export ADMIN_PASSWORD + echo -e "${YELLOW}ADMIN_PASSWORD not set, generating strong password...${NC}" + ADMIN_PASSWORD=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-24) + # Ensure password meets Azure complexity requirements (uppercase, lowercase, digit, special char) + ADMIN_PASSWORD="Aa1!${ADMIN_PASSWORD}" + echo -e "${GREEN}Generated admin password (save this): ${ADMIN_PASSWORD}${NC}" + export ADMIN_PASSWORD fi echo -e "${GREEN}=== Deployment Tests ===${NC}" @@ -109,44 +109,44 @@ echo "Location: ${TEST_LOCATION}" echo "" run_terraform_tests() { - export TEST_RESOURCE_GROUP_NAME="${TEST_RESOURCE_GROUP_NAME_PREFIX:-test-}terraform" - echo "Resource Group: ${TEST_RESOURCE_GROUP_NAME}" - - echo -e "${YELLOW}Running Terraform deployment tests...${NC}" - if go test $VERBOSE_FLAG -run TestTerraformFullSingleNodeClusterDeploy -timeout 2h; then - echo -e "${GREEN}✓ Terraform tests passed${NC}" - return 0 - else - echo -e "${RED}✗ Terraform tests failed${NC}" - return 1 - fi + export TEST_RESOURCE_GROUP_NAME="${TEST_RESOURCE_GROUP_NAME_PREFIX:-test-}terraform" + echo "Resource Group: ${TEST_RESOURCE_GROUP_NAME}" + + echo -e "${YELLOW}Running Terraform deployment tests...${NC}" + if go test $VERBOSE_FLAG -run TestTerraformFullSingleNodeClusterDeploy -timeout 2h; then + echo -e "${GREEN}✓ Terraform tests passed${NC}" + return 0 + else + echo -e "${RED}✗ Terraform tests failed${NC}" + return 1 + fi } run_bicep_tests() { - export TEST_RESOURCE_GROUP_NAME="${TEST_RESOURCE_GROUP_NAME_PREFIX:-test-}bicep" - echo "Resource Group: ${TEST_RESOURCE_GROUP_NAME}" - - echo -e "${YELLOW}Running Bicep deployment tests...${NC}" - if go test $VERBOSE_FLAG -run TestBicepFullSingleNodeClusterDeploy -timeout 2h; then - echo -e "${GREEN}✓ Bicep tests passed${NC}" - return 0 - else - echo -e "${RED}✗ Bicep tests failed${NC}" - return 1 - fi + export TEST_RESOURCE_GROUP_NAME="${TEST_RESOURCE_GROUP_NAME_PREFIX:-test-}bicep" + echo "Resource Group: ${TEST_RESOURCE_GROUP_NAME}" + + echo -e "${YELLOW}Running Bicep deployment tests...${NC}" + if go test $VERBOSE_FLAG -run TestBicepFullSingleNodeClusterDeploy -timeout 2h; then + echo -e "${GREEN}✓ Bicep tests passed${NC}" + return 0 + else + echo -e "${RED}✗ Bicep tests failed${NC}" + return 1 + fi } # Run tests based on deployment type EXIT_CODE=0 case $DEPLOYMENT_TYPE in -terraform) + terraform) run_terraform_tests || EXIT_CODE=$? ;; -bicep) + bicep) run_bicep_tests || EXIT_CODE=$? ;; -both) + both) run_terraform_tests || EXIT_CODE=$? echo "" run_bicep_tests || EXIT_CODE=$? @@ -155,9 +155,9 @@ esac echo "" if [[ $EXIT_CODE -eq 0 ]]; then - echo -e "${GREEN}=== All tests completed successfully ===${NC}" + echo -e "${GREEN}=== All tests completed successfully ===${NC}" else - echo -e "${RED}=== Some tests failed ===${NC}" + echo -e "${RED}=== Some tests failed ===${NC}" fi exit $EXIT_CODE diff --git a/docs/_server/tests/routes/health.test.js b/docs/_server/tests/routes/health.test.js index 611f7043..15b615fe 100644 --- a/docs/_server/tests/routes/health.test.js +++ b/docs/_server/tests/routes/health.test.js @@ -8,7 +8,6 @@ import request from 'supertest'; describe('Health Check Routes', () => { let app; - const server = null; beforeEach(async () => { // Clear module cache to ensure fresh imports @@ -36,10 +35,6 @@ describe('Health Check Routes', () => { }, 10000); // Increase timeout to 10 seconds afterEach(async () => { - if (server) { - await new Promise(resolve => server.close(resolve)); - } - // Clear any cached modules if (global.gc) { global.gc(); diff --git a/docs/assets/js/features/enhanced-anchor-links.js b/docs/assets/js/features/enhanced-anchor-links.js index 6bea67f1..6bcfeac8 100644 --- a/docs/assets/js/features/enhanced-anchor-links.js +++ b/docs/assets/js/features/enhanced-anchor-links.js @@ -90,17 +90,6 @@ export class EnhancedAnchorLinks { } } - /** - * Debug logging that only outputs when debug mode is enabled - * @private - * @param {...any} args - Arguments to log - */ - debugLog(...args) { - if (this.config.debug) { - console.log('[Enhanced Anchor Links]', ...args); - } - } - /** * Initialize enhanced anchor links with Docsify integration */ diff --git a/docs/assets/js/temp-dashboard-clean.js b/docs/assets/js/temp-dashboard-clean.js deleted file mode 100644 index d20c5d28..00000000 --- a/docs/assets/js/temp-dashboard-clean.js +++ /dev/null @@ -1,747 +0,0 @@ -/** - * Learning Path Dashboard Plugin - * Consolidates all learning dashboard functionality into a single component - * with comprehensive container validation and error handling - * - * @class LearningPathDashboard - * @author Edge AI Team - * @version 2.0.0 - */ - -import { LearningPathProgress } from '../features/progress-tracking/learning-path-progress.js'; - -class LearningPathDashboard { - constructor(containers, config = {}) { - // Initialize configuration first (needed for logging) - this.config = { - showProgress: true, - showBadges: true, - enableFiltering: true, - enableSorting: true, - enableAssessment: true, - enableAutoSelection: true, - layout: 'grid', - progressSync: true, - apiSync: false, - containerValidation: true, - errorDisplay: true, - animations: true, - debug: false, - ...config - }; - - // Initialize event system - this.eventListeners = {}; - this.eventHandlers = new Map(); // For test compatibility - this.isDestroyed = false; - - // Initialize error tracking - this.errors = []; - - // Initialize bound handlers map for cleanup - this.boundHandlers = new Map(); - - // Initialize progress tracking properties - this.progressTracker = null; - this.serverConnected = false; - this.progressData = {}; - - // Strict container validation to prevent random number display bug - this.containers = this.validateAndNormalizeContainers(containers); - - // Allow graceful initialization even without valid containers for testing - if (this.containers.length === 0) { - // Only log generic warning if no specific warnings were already logged - this.logWarning('No valid containers found. Dashboard initialized with empty state.'); - } - - // Initialize data structures - this.paths = []; - this.filteredPaths = []; - this.filterState = { - category: null, - difficulty: null - }; - this.sortState = { - field: null, - direction: 'asc' - }; - - // Initialize containers if we have valid ones - if (this.containers.length > 0) { - this.setupContainers(); - this.createErrorContainers(); - this.createAriaLiveRegions(); - this.setupKeyboardNavigation(); - } - - // Initialize progress tracking if enabled - if (this.config.progressSync && this.progressTracker) { - this.setupProgressTracking(); - } - - // Emit initialized event first - this.emit('dashboard:initialized', { - containersFound: this.containers.length, - config: this.config - }); - - // Emit ready event for integration - setTimeout(() => { - this.emit('dashboard:ready', { - containersFound: this.containers.length, - config: this.config - }); - }, 0); - } - - /** - * Validate and normalize container inputs to DOM elements - * Prevents invalid configurations and random display issues - */ - validateAndNormalizeContainers(containers) { - const validContainers = []; - - // Handle null/undefined - if (!containers) { - this.logWarning('validateAndNormalizeContainers: No containers provided'); - return validContainers; - } - - // Convert to array for consistent processing - const containerArray = Array.isArray(containers) ? containers : [containers]; - - containerArray.forEach((container, index) => { - try { - const element = this.normalizeToElement(container); - if (element && this.isValidElement(element)) { - // Check for duplicates - if (!validContainers.includes(element)) { - validContainers.push(element); - } - } else { - this.logWarning(`validateAndNormalizeContainers: Invalid container at index ${index}:`, container); - } - } catch (error) { - this.logError(`validateAndNormalizeContainers: Error processing container at index ${index}:`, error); - } - }); - - return validContainers; - } - - /** - * Normalize various input types to DOM elements - */ - normalizeToElement(input) { - if (!input) return null; - - // Already a DOM element - if (input instanceof Element) { - return input; - } - - // String selector - if (typeof input === 'string' && input.trim().length > 0) { - try { - return document.querySelector(input.trim()); - } catch (error) { - this.logWarning('normalizeToElement: Invalid selector string:', input); - return null; - } - } - - return null; - } - - /** - * Validate that an element is suitable as a dashboard container - */ - isValidElement(element) { - return element instanceof Element && - element.nodeType === Node.ELEMENT_NODE; - } - - /** - * Setup containers with initial structure - */ - setupContainers() { - this.containers.forEach(container => { - // Add dashboard class if not present - if (!container.classList.contains('learning-path-dashboard')) { - container.classList.add('learning-path-dashboard'); - } - }); - } - - /** - * Create error display containers - */ - createErrorContainers() { - this.containers.forEach(container => { - // Create or find error container - let errorContainer = container.querySelector('.learning-dashboard-error'); - if (!errorContainer) { - errorContainer = document.createElement('div'); - errorContainer.className = 'learning-dashboard-error'; - errorContainer.style.display = 'none'; - container.insertBefore(errorContainer, container.firstChild); - } - }); - } - - /** - * Create ARIA live regions for accessibility - */ - createAriaLiveRegions() { - this.containers.forEach(container => { - // Create or find ARIA live region - let ariaLive = container.querySelector('.learning-dashboard-status[aria-live="polite"]'); - if (!ariaLive) { - ariaLive = document.createElement('div'); - ariaLive.setAttribute('aria-live', 'polite'); - ariaLive.setAttribute('aria-atomic', 'true'); - ariaLive.className = 'learning-dashboard-status sr-only'; - container.appendChild(ariaLive); - } - }); - } - - /** - * Update ARIA live region with status message - */ - updateAriaLiveRegion(message) { - this.containers.forEach(container => { - const ariaLive = container.querySelector('.learning-dashboard-status[aria-live="polite"]'); - if (ariaLive) { - ariaLive.textContent = message; - } - }); - } - - /** - * Display error message to users - */ - displayError(message, hideAfter = 5000) { - this.errors.push({ - message, - timestamp: Date.now() - }); - - this.containers.forEach(container => { - const errorContainer = container.querySelector('.learning-dashboard-error'); - if (errorContainer) { - errorContainer.textContent = message === null ? 'null' : String(message); - errorContainer.style.display = 'block'; - - if (hideAfter > 0) { - setTimeout(() => { - errorContainer.style.display = 'none'; - }, hideAfter); - } - } - }); - - this.logError('Dashboard Error:', message); - } - - /** - * Announce status to screen readers - */ - announceStatus(message) { - this.containers.forEach(container => { - const ariaLive = container.querySelector('.learning-dashboard-status[aria-live="polite"]'); - if (ariaLive) { - ariaLive.textContent = message; - } - }); - } - - /** - * Event system methods - */ - on(event, handler) { - this.initializeEventStorage(event); - this.eventListeners[event].push(handler); - this.eventHandlers.get(event).push(handler); - } - - off(event, handler) { - if (handler) { - this.removeSpecificHandler(event, handler); - } else { - this.removeAllHandlers(event); - } - } - - emit(event, data) { - if (this.eventListeners[event]) { - this.eventListeners[event].forEach(handler => { - try { - handler(data); - } catch (error) { - this.logError(`Error in event handler for ${event}:`, error); - } - }); - } - } - - /** - * Initialize event storage for a given event type - * @private - */ - initializeEventStorage(event) { - if (!this.eventListeners[event]) { - this.eventListeners[event] = []; - } - if (!this.eventHandlers.has(event)) { - this.eventHandlers.set(event, []); - } - } - - /** - * Remove a specific event handler - * @private - */ - removeSpecificHandler(event, handler) { - // Remove from eventListeners - if (this.eventListeners[event]) { - const index = this.eventListeners[event].indexOf(handler); - if (index > -1) { - this.eventListeners[event].splice(index, 1); - } - } - - // Remove from eventHandlers - if (this.eventHandlers.has(event)) { - // Remove progress event listeners - if (this.progressTracker && typeof this.progressTracker.off === 'function') { - this.progressTracker.off('learningPathProgressUpdate', this.handleProgressUpdate); - } - - const handlers = this.eventHandlers.get(event); - const index = handlers.indexOf(handler); - if (index > -1) { - handlers.splice(index, 1); - if (handlers.length === 0) { - this.eventHandlers.delete(event); - } - } - } - } - - /** - * Remove all handlers for an event - * @private - */ - removeAllHandlers(event) { - if (this.eventListeners[event]) { - delete this.eventListeners[event]; - } - this.eventHandlers.delete(event); - } - - /** - * Add event handler (test compatibility) - */ - addEventHandler(event, handler) { - this.on(event, handler); - } - - /** - * Remove event handler (test compatibility) - */ - removeEventHandler(event, handler) { - this.removeSpecificHandler(event, handler); - } - - /** - * Remove all event handlers for an event - */ - removeAllEventHandlers(event) { - this.removeAllHandlers(event); - } - - /** - * Load learning paths data into the dashboard - */ - loadPaths(paths) { - this.log('Loading paths', paths); - - if (!Array.isArray(paths)) { - this.logError('loadPaths: paths must be an array', { paths }); - this.displayError('Invalid paths data provided'); - return; - } - - // Validate and filter paths - const validPaths = paths.filter(path => this.validatePath(path)); - - if (validPaths.length === 0) { - this.logError('No valid paths found', { originalCount: paths.length }); - this.displayError('No valid learning paths found'); - return; - } - - this.clearCards(); - this.paths = validPaths; - this.filteredPaths = [...validPaths]; // Initialize filtered paths with all valid paths - this.renderCards(); - } - - /** - * Sort cards by progress percentage (descending) - */ - sortCardsByProgress(paths) { - if (!Array.isArray(paths)) return []; - - return [...paths].sort((a, b) => { - const progressA = this.calculateProgress(a.id).percentage; - const progressB = this.calculateProgress(b.id).percentage; - return progressB - progressA; // Descending order - }); - } - - /** - * Sort cards alphabetically by title - */ - sortCardsByTitle(paths) { - if (!Array.isArray(paths)) return []; - - return [...paths].sort((a, b) => { - const titleA = (a.title || '').toLowerCase(); - const titleB = (b.title || '').toLowerCase(); - return titleA.localeCompare(titleB); - }); - } - - /** - * Validate a single learning path object - */ - validatePath(path) { - if (!path || typeof path !== 'object') { - this.log('debug', 'validatePath failed: not an object', { path }); - return false; - } - - // Check required fields - if (!path.id || typeof path.id !== 'string' || path.id.trim() === '') { - this.log('debug', 'validatePath failed: invalid id', { path }); - return false; - } - - if (!path.title || typeof path.title !== 'string' || path.title.trim() === '') { - this.log('debug', 'validatePath failed: invalid title', { path }); - return false; - } - - if (!Array.isArray(path.steps)) { - this.log('debug', 'validatePath failed: steps not array', { path }); - return false; - } - - this.log('debug', 'validatePath passed', { pathId: path.id, title: path.title }); - return true; - } - - /** - * Clear existing cards from dashboard containers - * @private - */ - clearCards() { - this.containers.forEach(container => { - const existingCards = container.querySelectorAll('.learning-path-card'); - existingCards.forEach(card => card.remove()); - }); - } - - /** - * Render cards in the dashboard containers - */ - renderCards() { - try { - this.containers.forEach((container) => { - this.renderCardsInContainer(container); - }); - - // Setup event handlers - this.bindCardEvents(); - - this.emit('cardsRendered', { - pathCount: this.filteredPaths.length, - containerCount: this.containers.length - }); - - } catch (error) { - this.displayError('Failed to render cards'); - this.logError('Failed to render cards:', error); - } - } - - /** - * Render cards in a specific container - */ - renderCardsInContainer(container) { - // Clear existing cards - const existingCards = container.querySelectorAll('.learning-path-card'); - existingCards.forEach(card => card.remove()); - - // Render filtered paths - this.filteredPaths.forEach(path => { - const cardHTML = this.renderCard(path); - container.insertAdjacentHTML('beforeend', cardHTML); - }); - - // Enhance checkboxes with progress tracking after rendering - this.enhanceCheckboxesWithProgressTracking(); - } - - /** - * Render a single learning path card - */ - renderCard(path) { - const progress = this.calculateProgress(path.id); - const badges = path.badge ? this.renderBadge(path.badge, path.id) : ''; - const steps = path.steps.map(step => this.renderStep(step, path.id)).join(''); - - return ` -
-
-

${path.title}

-

${path.description || ''}

- ${badges} -
-
-
-
-
-
-
${progress.completed} of ${progress.total} completed
-
-
- ${steps} -
-
-
- `; - } - - /** - * Render a single step checkbox - */ - renderStep(step, pathId) { - const stepId = step.id || `step-${Math.random().toString(36).substr(2, 9)}`; - const isCompleted = this.isStepCompleted(pathId, stepId); - - return ` -
- - -
- `; - } - - /** - * Render a badge - */ - renderBadge(badge, pathId) { - const progress = this.calculateProgress(pathId); - const isUnlocked = badge.unlocked || progress.percentage === 100; - const unlockedClass = isUnlocked ? ' unlocked' : ''; - - return ` -
- ${badge.icon || '🏆'} - ${badge.title || 'Completed'} -
- `; - } - - /** - * Calculate progress for a path - */ - calculateProgress(pathId) { - const path = this.paths.find(p => p.id === pathId); - if (!path || !path.steps) { - return { completed: 0, total: 0, percentage: 0 }; - } - - const total = path.steps.length; - const completed = path.steps.filter(step => - this.isStepCompleted(pathId, step.id) - ).length; - - return { - completed, - total, - percentage: total > 0 ? Math.round((completed / total) * 100) : 0 - }; - } - - /** - * Check if a step is completed - */ - isStepCompleted(pathId, stepId) { - // First check the original path data - const path = this.paths.find(p => p.id === pathId); - if (path && path.steps) { - const step = path.steps.find(s => s.id === stepId); - if (step && typeof step.completed === 'boolean') { - return step.completed; - } - } - - // Check localStorage second (user progress overrides initial state) - try { - const progress = JSON.parse(localStorage.getItem('learning-progress') || '{}'); - if (progress[pathId] && typeof progress[pathId][stepId] === 'boolean') { - return progress[pathId][stepId]; - } - } catch (error) { - // localStorage error, continue to DOM check - } - - // Check DOM state last (as final fallback) - const checkbox = document.querySelector(`[data-path-id="${pathId}"][data-step-id="${stepId}"]`); - if (checkbox) { - return checkbox.checked; - } - - return false; - } - - /** - * Bind event handlers to cards - */ - bindCardEvents() { - this.containers.forEach(container => { - // Card click events - const cards = container.querySelectorAll('.learning-path-card'); - cards.forEach(card => { - const boundHandler = (event) => this.handleCardClick(event); - this.boundHandlers.set(card, boundHandler); - card.addEventListener('click', boundHandler); - }); - - // Setup step checkbox handlers for this container - this.setupStepHandlers(container); - this.setupKeyboardNavigation(); - }); - } - - /** - * Handle card click events - */ - handleCardClick(event) { - const card = event.currentTarget; - const pathId = card.dataset.pathId; - const path = this.paths.find(p => p.id === pathId); - - if (path) { - this.emit('card-clicked', { - pathId, - path - }); - } - } - - /** - * Handle step completion changes - */ - handleStepChange(event) { - const checkbox = event.target; - const stepId = checkbox.dataset.stepId; - const pathId = checkbox.dataset.pathId; - const isCompleted = checkbox.checked; - - this.log('debug', `Step ${stepId} in path ${pathId} ${isCompleted ? 'completed' : 'unchecked'}`); - - // Update localStorage - this.updateStepInLocalStorage(pathId, stepId, isCompleted); - - // Calculate new progress - const progress = this.calculateProgress(pathId); - - // Update progress display - this.updateProgressDisplay(pathId); - - // Check for badge unlock - this.checkBadgeUnlock(pathId); - - // Announce to screen readers - this.announceProgressUpdate(pathId); - - // Emit progress update event - this.emit('progress-updated', { - pathId, - stepId, - completed: isCompleted, - progress - }); - } - - /** - * Update step completion in localStorage - */ - updateStepInLocalStorage(pathId, stepId, isCompleted) { - try { - // Build complete progress state from current path data - const progress = {}; - - if (!this.paths || this.paths.length === 0) { - this.log('error', 'No paths data available for localStorage update'); - return; - } - - this.paths.forEach(path => { - progress[path.id] = {}; - path.steps.forEach(step => { - // For the step being changed, use the new value - if (path.id === pathId && step.id === stepId) { - progress[path.id][step.id] = isCompleted; - // Also update the path data to keep it in sync - step.completed = isCompleted; - } else { - // For other steps, use current completion state - progress[path.id][step.id] = this.isStepCompleted(path.id, step.id); - } - }); - }); - - localStorage.setItem('learning-progress', JSON.stringify(progress)); - this.log('debug', 'Updated localStorage with progress:', progress); - } catch (error) { - this.log('error', 'Failed to update localStorage:', error); - } - } /** - * Update progress display for a path - */ - updateProgressDisplay(pathId) { - if (!pathId) return; - - const progress = this.calculateProgress(pathId); - - this.containers.forEach(container => { - const pathCard = container.querySelector(`[data-path-id="${pathId}"]`); - if (!pathCard) return; - - this.updateProgressElements(pathCard, progress); - }); - - // Emit progress update event - this.emit('progressUpdated', { - pathId, - stepId: 'progress-update', - completed: progress.completed, - total: progress.total, - percentage: progress.percentage - }); - } - - /** - * Update progress elements within a card - * @private - */ - updateProgressElements(card, progress) { - const progressBar = card.querySelector('.progress-bar'); - const progressFill = card.querySelector('.progress-fill'); diff --git a/docs/assets/js/tests/components/learning-path-card.test.js b/docs/assets/js/tests/components/learning-path-card.test.js index 4ba6dda2..bbd82f1a 100644 --- a/docs/assets/js/tests/components/learning-path-card.test.js +++ b/docs/assets/js/tests/components/learning-path-card.test.js @@ -127,7 +127,6 @@ function setupMockPathCard(container) { describe('Learning Path Card Component', () => { let mockStorage; - const container = null; beforeEach(() => { // Setup DOM @@ -141,9 +140,6 @@ describe('Learning Path Card Component', () => { }); afterEach(() => { - if (container && container.parentNode) { - container.parentNode.removeChild(container); - } vi.restoreAllMocks(); }); diff --git a/docs/assets/js/tests/helpers/common-test-utils.js b/docs/assets/js/tests/helpers/common-test-utils.js index d7824854..555b574e 100644 --- a/docs/assets/js/tests/helpers/common-test-utils.js +++ b/docs/assets/js/tests/helpers/common-test-utils.js @@ -441,10 +441,10 @@ export class MockDataGenerator { // Generate achievement badges based on progress const badges = []; - if (calculatedProgress >= 25) { badges.push('first-steps'); } - if (calculatedProgress >= 50) { badges.push('halfway'); } - if (calculatedProgress === 100) { badges.push('completed'); } - if (timeSpent > 3600000) { badges.push('dedicated-learner'); } // 1 hour+ + if (calculatedProgress >= 25) {badges.push('first-steps');} + if (calculatedProgress >= 50) {badges.push('halfway');} + if (calculatedProgress === 100) {badges.push('completed');} + if (timeSpent > 3600000) {badges.push('dedicated-learner');} // 1 hour+ return { pathId, @@ -1230,7 +1230,7 @@ export function setupMockLocalStorage() { * @param {Object} options - Path configuration options * @returns {Object} Mock path data object */ -function generatePathData(options = {}) { +export function generatePathData(options = {}) { return { id: options.id || `path-${Math.random().toString(36).substr(2, 9)}`, title: options.title || 'Test Learning Path', @@ -1273,7 +1273,4 @@ export default { generatePathData }; -// Also export key utilities as named exports for easier importing -export { - generatePathData -}; + diff --git a/eslint.config.js b/eslint.config.js index 4572d1b2..3206db9a 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -148,7 +148,17 @@ export default [ // Module system module: 'readonly', - require: 'readonly' + require: 'readonly', + + // Server-Sent Events + EventSource: 'readonly', + + // Encoding APIs + btoa: 'readonly', + atob: 'readonly', + + // Screen API + screen: 'readonly' } }, rules: {