utilikit-1.0.0/modules/utilikit_test/js/utilikit.test-suite-comprehensive.js

modules/utilikit_test/js/utilikit.test-suite-comprehensive.js
/**
 * @file
 * UtiliKit Comprehensive Test Suite Runner and Validation System.
 *
 * This file provides a complete testing framework for validating UtiliKit
 * utility classes across all supported value types, responsive breakpoints,
 * and CSS property combinations. The test suite offers real-time validation,
 * detailed reporting, and comprehensive debugging capabilities for ensuring
 * UtiliKit's CSS generation accuracy and reliability.
 *
 * Key Features:
 * - Comprehensive test execution across all UtiliKit utility classes
 * - Real-time validation of CSS property application and computed styles
 * - Detailed test reporting with pass/fail statistics and export capabilities
 * - Interactive test management with filtering, grouping, and selective execution
 * - Advanced validation for complex properties (transforms, grids, colors)
 * - Performance monitoring and batch processing for large test suites
 * - Integration with UtiliKit's inline and static rendering modes
 *
 * Test Validation Strategy:
 * - Validates both inline styles and computed CSS values
 * - Handles browser-specific CSS normalization and differences
 * - Supports complex CSS properties like transforms and grid templates
 * - Provides detailed debugging information for failed tests
 * - Implements progressive enhancement for different browser capabilities
 *
 * The test suite is designed for development environments and provides
 * essential validation capabilities for UtiliKit's CSS generation system.
 *
 * @see TestGenerator.php for test case generation
 * @see utilikit.behavior.js for core CSS application logic
 * @see utilikit-test-suite.html.twig for test interface template
 */
(function (Drupal, once) {
  'use strict';

  /**
   * UtiliKit Test Behavior for Element Processing.
   *
   * Ensures that UtiliKit elements within the test suite are properly
   * processed using inline mode regardless of global settings. This
   * behavior guarantees consistent test execution by forcing the use
   * of JavaScript-based CSS application for all test elements.
   *
   * @type {Drupal~behavior}
   *
   * @prop {Drupal~behaviorAttach} attach
   *   Processes UtiliKit elements in test contexts using inline mode
   *   to ensure consistent and predictable test results.
   */
  Drupal.behaviors.utilikitTest = {
    /**
     * Attaches UtiliKit processing to test elements.
     *
     * Forces inline mode processing for all UtiliKit elements within
     * the test suite to ensure consistent behavior regardless of the
     * global rendering mode configuration.
     *
     * @param {Element} context
     *   The DOM element context for behavior attachment.
     * @param {object} settings
     *   Drupal settings object containing configuration data.
     */
    attach: function(context, settings) {
      // Always use inline mode regardless of global settings
      const elements = once('utilikit-test', '.utilikit', context);
      if (elements.length > 0) {
        // Force apply classes using inline engine
        if (typeof Drupal.utilikit !== 'undefined' && typeof Drupal.utilikit.applyClasses === 'function') {
          Drupal.utilikit.applyClasses(elements);
        }
      }
    }
  };

  /**
   * Comprehensive Test Runner Behavior.
   *
   * Initializes and manages the complete UtiliKit test suite interface,
   * handling test execution, validation, reporting, and user interaction.
   * Adapts initialization based on rendering mode to ensure optimal
   * test execution in both static and inline modes.
   *
   * @type {Drupal~behavior}
   *
   * @prop {Drupal~behaviorAttach} attach
   *   Initializes the test runner with appropriate timing based on
   *   the current UtiliKit rendering mode configuration.
   */
  Drupal.behaviors.utilikitComprehensiveTestRunner = {
    /**
     * Attaches the comprehensive test runner to the test suite container.
     *
     * Initializes the test runner with special handling for static mode
     * which requires waiting for stylesheet loading before test execution.
     * Ensures proper timing and resource availability for accurate testing.
     *
     * @param {Element} context
     *   The DOM element context for behavior attachment.
     * @param {object} settings
     *   Drupal settings object containing UtiliKit configuration.
     */
    async attach(context, settings) {
      once('utilikitTestRunner', '#utilikit-test-suite', context).forEach(async (container) => {
        // Prefer `settings` (the behavior param) over the global.
        if (settings?.utilikit?.renderingMode === 'static') {
          const testRunner = new UtilikitTestRunner(container);
          await testRunner.waitForStylesheets();
          await new Promise((resolve) => setTimeout(resolve, 50));
          testRunner.init();
          return;
        }

        const testRunner = new UtilikitTestRunner(container);
        testRunner.init();
      });
    }
  };

  /**
   * Comprehensive UtiliKit Test Runner and Validation Engine.
   *
   * Provides complete test execution, validation, and reporting capabilities
   * for the UtiliKit utility class system. Manages test lifecycle from
   * initialization through execution, validation, and result reporting.
   *
   * The test runner implements sophisticated validation logic that handles:
   * - CSS property mapping validation
   * - Browser-specific CSS normalization
   * - Complex property validation (transforms, grids, colors)
   * - Responsive breakpoint testing
   * - Performance monitoring and batch processing
   * - Detailed error reporting and debugging
   *
   * @class
   */
  class UtilikitTestRunner {
    /**
     * Constructs a new UtiliKit test runner instance.
     *
     * Initializes the test runner with the provided container element
     * and sets up internal state tracking for test execution, results
     * management, and UI interaction.
     *
     * @param {Element} container
     *   The DOM container element that holds the test suite interface.
     *   Should contain all test cases and control elements.
     */
    constructor(container) {
      /**
       * The main test suite container element.
       *
       * @type {Element}
       */
      this.container = container;

      /**
       * Total number of tests in the current test suite.
       *
       * @type {number}
       */
      this.totalTests = 0;

      /**
       * Number of tests that have passed validation.
       *
       * @type {number}
       */
      this.passedTests = 0;

      /**
       * Number of tests that have failed validation.
       *
       * @type {number}
       */
      this.failedTests = 0;

      /**
       * Current test index during execution.
       *
       * @type {number}
       */
      this.currentTest = 0;

      /**
       * Map storing detailed results for each test case.
       *
       * @type {Map<string, object>}
       */
      this.testResults = new Map();

      /**
       * Flag indicating whether test execution is currently in progress.
       *
       * @type {boolean}
       */
      this.isRunning = false;

      /**
       * Flag for stopping test execution when requested by user.
       *
       * @type {boolean}
       */
      this.shouldStop = false;
    }

    /**
     * Initializes the test runner interface and functionality.
     *
     * Sets up all necessary DOM element references, counts available tests,
     * attaches event listeners for user interaction, and prepares the
     * interface for test execution. This method must be called before
     * any test execution can begin.
     */
    init() {
      this.setupElements();
      this.countTests();
      this.attachEventListeners();
      this.updateSummary();
      this.log('Test suite initialized. Ready to run tests.', 'info');
    }

    /**
     * Establishes references to key DOM elements in the test interface.
     *
     * Creates a structured object containing references to all interactive
     * elements needed for test execution, progress reporting, and user
     * control. These references are used throughout the test runner lifecycle.
     */
    setupElements() {
      this.elements = {
        totalTests: document.getElementById('total-tests'),
        passedCount: document.getElementById('passed-count'),
        failedCount: document.getElementById('failed-count'),
        testStatus: document.getElementById('test-status'),
        logContent: document.getElementById('test-log-content'),
        runAllBtn: document.getElementById('run-all-tests'),
        runVisibleBtn: document.getElementById('run-visible-tests'),
        expandAllBtn: document.getElementById('expand-all'),
        collapseAllBtn: document.getElementById('collapse-all'),
        exportBtn: document.getElementById('export-results'),
        clearLogBtn: document.getElementById('clear-log'),
        statusFilter: document.getElementById('status-filter'),
        groupFilter: document.getElementById('group-filter'),
      };
    }

    /**
     * Counts and catalogs all available test cases in the test suite.
     *
     * Scans the container for test case elements and updates the total
     * test count for progress tracking and reporting purposes. This
     * count is used throughout the test execution process.
     */
    countTests() {
      this.testCases = this.container.querySelectorAll('.test-case');
      this.totalTests = this.testCases.length;
      this.elements.totalTests.textContent = this.totalTests;
    }

    /**
     * Attaches event listeners for all interactive test suite controls.
     *
     * Sets up comprehensive event handling for test execution, view
     * management, filtering, exporting, and individual test group
     * expansion/collapse functionality. Enables full user interaction
     * with the test suite interface.
     */
    attachEventListeners() {
      // Run buttons
      this.elements.runAllBtn?.addEventListener('click', () => this.runAllTests());
      this.elements.runVisibleBtn?.addEventListener('click', () => this.runVisibleTests());

      // View controls
      this.elements.expandAllBtn?.addEventListener('click', () => this.toggleAllTests(true));
      this.elements.collapseAllBtn?.addEventListener('click', () => this.toggleAllTests(false));

      // Export and log
      this.elements.exportBtn?.addEventListener('click', () => this.exportResults());
      this.elements.clearLogBtn?.addEventListener('click', () => this.clearLog());

      // Filters
      this.elements.statusFilter?.addEventListener('change', () => this.applyFilters());
      this.elements.groupFilter?.addEventListener('change', () => this.applyFilters());

      // Toggle individual rule tests
      this.container.querySelectorAll('.toggle-tests').forEach(btn => {
        btn.addEventListener('click', (e) => {
          const ruleTests = e.target.closest('.test-rule').querySelector('.rule-tests');
          const isVisible = ruleTests.style.display !== 'none';
          ruleTests.style.display = isVisible ? 'none' : 'block';
          e.target.textContent = isVisible ? 'Show Tests' : 'Hide Tests';
        });
      });
    }

    /**
     * Executes all available test cases in the test suite.
     *
     * Runs a comprehensive test execution across all test cases using
     * batch processing to maintain UI responsiveness. Provides real-time
     * progress updates and handles user-initiated stopping. Includes
     * performance timing and detailed result reporting.
     *
     * @returns {Promise<void>}
     *   A promise that resolves when all tests have completed or been stopped.
     */
    async runAllTests() {
      if (this.isRunning) return;

      this.resetResults();
      this.isRunning = true;
      this.shouldStop = false;
      this.elements.runAllBtn.textContent = 'Stop Tests';
      this.elements.runAllBtn.addEventListener('click', () => this.stopTests(), { once: true });

      this.log('Starting comprehensive test run...', 'info');
      const startTime = performance.now();

      // Process tests in batches to avoid blocking UI
      const batchSize = 10;
      for (let i = 0; i < this.testCases.length; i += batchSize) {
        if (this.shouldStop) break;

        const batch = Array.from(this.testCases).slice(i, i + batchSize);
        await this.processBatch(batch);

        // Update progress
        this.currentTest = Math.min(i + batchSize, this.testCases.length);
        this.updateProgress();

        // Allow UI to update
        await new Promise(resolve => setTimeout(resolve, 10));
      }

      const duration = ((performance.now() - startTime) / 1000).toFixed(2);
      this.log(`Test run completed in ${duration}s`, 'info');
      this.finishTestRun();
    }

    /**
     * Executes only currently visible test cases.
     *
     * Runs tests that are currently expanded and visible in the interface,
     * allowing focused testing of specific utility categories or test
     * groups without processing the entire test suite.
     *
     * @returns {Promise<void>}
     *   A promise that resolves when visible tests have completed.
     */
    async runVisibleTests() {
      if (this.isRunning) return;

      const visibleTests = Array.from(this.testCases).filter(testCase => {
        const ruleTests = testCase.closest('.rule-tests');
        return ruleTests && ruleTests.style.display !== 'none';
      });

      if (visibleTests.length === 0) {
        this.log('No visible tests to run. Expand some test groups first.', 'warn');
        return;
      }

      this.resetResults();
      this.isRunning = true;
      this.totalTests = visibleTests.length;
      this.elements.totalTests.textContent = this.totalTests;

      this.log(`Running ${visibleTests.length} visible tests...`, 'info');

      for (const testCase of visibleTests) {
        if (this.shouldStop) break;
        await this.runSingleTest(testCase);
        this.currentTest++;
        this.updateProgress();
        await new Promise(resolve => setTimeout(resolve, 5));
      }

      this.finishTestRun();
    }

    /**
     * Processes a batch of test cases concurrently.
     *
     * Executes multiple test cases in parallel to improve performance
     * while maintaining UI responsiveness. Used during comprehensive
     * test runs to optimize execution time.
     *
     * @param {Array<Element>} batch
     *   Array of test case elements to process concurrently.
     *
     * @returns {Promise<void>}
     *   A promise that resolves when all tests in the batch complete.
     */
    async processBatch(batch) {
      const promises = batch.map(testCase => this.runSingleTest(testCase));
      await Promise.all(promises);
    }

    /**
     * Executes and validates a single test case.
     *
     * Runs a complete test cycle for an individual test case including
     * CSS application, style computation, validation, and result recording.
     * Handles all error conditions and provides detailed debugging
     * information for failed tests.
     *
     * @param {Element} testCase
     *   The test case element containing test data and preview element.
     *
     * @returns {Promise<void>}
     *   A promise that resolves when the test completes.
     */
    async runSingleTest(testCase) {
      const testData = JSON.parse(testCase.dataset.test);
      const testId = testCase.dataset.testId;

      try {
        // Get the preview element
        const previewEl = testCase.querySelector('.preview-element');
        if (!previewEl) {
          throw new Error('Preview element not found');
        }
        // Clear any existing inline styles that might interfere
        previewEl.removeAttribute('style');

        // Clear the data attribute to force fresh application
        delete previewEl.dataset.utilikitProps;

        // Special handling for border-width tests - ensure border-style is set
        if (testData.className.includes('uk-bw--') || testData.className.includes('-bw--')) {
          if (!previewEl.classList.contains('uk-bs--solid')) {
            previewEl.classList.add('uk-bs--solid');
          }
        }

        // Force UtiliKit to reprocess this element
        Drupal.utilikit.applyClasses([previewEl]);

        // Wait for styles to be applied
        await new Promise(resolve => requestAnimationFrame(resolve));

        await new Promise(resolve => requestAnimationFrame(resolve));

        // Get computed styles
        const computed = window.getComputedStyle(previewEl);

        // Force a reflow for transforms to ensure computed styles are up to date
        if (testData.className.includes('rt--') || testData.className.includes('sc--')) {
          // Force reflow
          void previewEl.offsetHeight;
        }

        const results = this.validateTest(testData, computed, previewEl);

        // Update UI
        this.updateTestRow(testCase, results);

        // Store results
        this.testResults.set(testId, {
          ...testData,
          ...results,
          timestamp: new Date().toISOString()
        });

        if (results.passed) {
          this.passedTests++;
        } else {
          this.failedTests++;
          this.log(`Failed: ${testData.className} - Expected: ${results.expected}, Got: ${results.actual}`, 'error');

          // Extra debug for specific problem tests
          if (testData.className.includes('--16-32') ||
              testData.className.includes('bw--') ||
              testData.className.includes('rt--') ||
              testData.className.includes('sc--')) {
            console.log('Debug failing test:', {
              className: testData.className,
              element: previewEl,
              styles: previewEl.style.cssText,
              computed: {
                paddingTop: computed.paddingTop,
                marginTop: computed.marginTop,
                borderWidth: computed.borderWidth,
                borderStyle: computed.borderStyle,
                transform: computed.transform,
                borderTopWidth: computed.borderTopWidth,
                padding: computed.padding,
                margin: computed.margin
              }
            });
          }
        }

      } catch (error) {
        this.failedTests++;
        this.updateTestRow(testCase, {
          passed: false,
          actual: 'Error: ' + error.message,
          expected: '-',
          error: error.message
        });
        this.log(`Error testing ${testData.className}: ${error.message}`, 'error');
      }

      this.updateSummary();
    }

     /**
     * Validates test results against expected values.
     *
     * Performs comprehensive validation of CSS property application
     * by comparing computed styles against expected values. Handles
     * various CSS property types including transforms, grids, colors,
     * and complex shorthand properties with appropriate normalization.
     *
     * @param {object} testData
     *   Test case data containing className, cssProperty, and expectedValue.
     * @param {CSSStyleDeclaration} computed
     *   Computed style declaration for the test element.
     * @param {Element} element
     *   The DOM element being tested for inline style validation.
     *
     * @returns {object}
     *   Validation result object containing passed status, actual value,
     *   and expected value for reporting purposes.
     */
    validateTest(testData, computed, element) {
      let passed = false;
      let actual = '';
      let expected = '';

      // Handle transform properties specially
      if (testData.cssProperty === 'transform') {
        return this.validateTransform(testData, computed, element);
      }

      // Handle grid template properties
      if (testData.testType === 'grid-template' || testData.testType === 'grid-template-responsive' ||
          testData.cssProperty === 'gridTemplateColumns' || testData.cssProperty === 'gridTemplateRows') {
        return this.validateGridTemplate(testData, computed, element);
      }

      // Handle shorthand properties (padding, margin, border-width, border-radius)
      if (testData.testType && testData.testType.includes('shorthand')) {
        return this.validateShorthand(testData, computed);
      }

      // Handle gap property
      if (testData.cssProperty === 'gap') {
        const rowGap = computed.rowGap || computed.gridRowGap || '0px';
        const columnGap = computed.columnGap || computed.gridColumnGap || '0px';
        actual = `${rowGap} ${columnGap}`;
        expected = testData.expectedValue;

        // Normalize for comparison - if both gaps are the same, browser might return shorthand
        if (expected.indexOf(' ') === -1) {
          // Single value expected, should apply to both
          const expectedWithSpace = `${expected} ${expected}`;
          passed = actual === expectedWithSpace || actual === expected;
        } else {
          passed = actual === expected;
        }

        return { passed, actual, expected };
      }

      // Regular property validation
      const cssProperty = testData.cssProperty;
      actual = computed[cssProperty] || '';
      expected = testData.expectedValue;

      // Handle border-radius special case
      if (cssProperty && cssProperty.includes('borderTopLeftRadius')) {
        // For border-radius directional, we might need to check multiple properties
        // but for now just check the one specified
        actual = computed[cssProperty] || '0px';
      }

      // Normalize values for comparison
      actual = this.normalizeValue(actual);
      expected = this.normalizeValue(expected);

      // Special handling for zero values with units
      if ((actual === '0px' && expected === '0') || (actual === '0' && expected === '0px')) {
        passed = true;
      }
      // Special handling for colors
      else if (testData.testType === 'color' || testData.testType === 'color-alpha') {
        passed = this.compareColors(actual, expected);
      }
      // Special handling for viewport units - accept computed pixel values
      else if (expected.includes('vh') || expected.includes('vw')) {
        // Viewport units are computed to pixels, so we just check if we got a pixel value
        passed = actual.includes('px') && parseFloat(actual) > 0;
      }
      // Special handling for letter-spacing zero
      else if (cssProperty === 'letterSpacing' && (expected === '0px' || expected === '0')) {
        passed = actual === '0px' || actual === 'normal';
      }
      else if (expected === 'auto' && (actual === '0px' || actual === '0')) {
        const autoProperties = ['margin', 'marginTop', 'marginRight', 'marginBottom', 'marginLeft'];
        if (autoProperties.some(prop => cssProperty.includes(prop))) {
          return {
            passed: true,
            actual: actual + ' (auto)',
            expected: expected
          };
        }
      }
      else {
        passed = actual === expected;
      }

      return { passed, actual, expected };
    }

     /**
     * Validates CSS Grid template property applications.
     *
     * Performs specialized validation for complex grid template properties
     * including repeat(), minmax(), and other CSS Grid functions. Handles
     * browser-specific normalization and invalid pattern detection.
     *
     * @param {object} testData
     *   Test case data for grid template validation.
     * @param {CSSStyleDeclaration} computed
     *   Computed styles for the test element.
     * @param {Element} element
     *   The DOM element being tested.
     *
     * @returns {object}
     *   Validation result with grid-specific handling.
     */
    validateGridTemplate(testData, computed, element) {
      const expected = testData.expectedValue;
      const cssProperty = testData.cssProperty;

      // Get both inline and computed values
      const inlineValue = element.style[cssProperty] || '';
      const computedValue = computed[cssProperty] || '';

      // Enhanced debug for failing grid tests
      if (!inlineValue && !computedValue) {
        console.log(`Grid test failed - no value applied:`, {
          className: testData.className,
          cssProperty: cssProperty,
          expected: expected,
          elementClasses: Array.from(element.classList),
          dataProps: element.dataset.utilikitProps,
          allInlineStyles: element.style.cssText
        });
      }

      let passed = false;
      let actual = inlineValue || computedValue || 'none';

      // If the expected value contains "/* missing */", it's an intentionally invalid pattern
      if (expected.includes('/* missing */')) {
        // For invalid patterns, we expect them to not be applied
        passed = actual === 'none' || actual === '';
        return {
          passed,
          actual: actual || 'none',
          expected: 'Invalid pattern (should not apply)'
        };
      }

      // Normalize whitespace for comparison
      const normalizeGrid = (value) => {
        return value
          .replace(/\s+/g, ' ')
          .replace(/\s*,\s*/g, ', ')
          .replace(/\s*\(\s*/g, '(')
          .replace(/\s*\)\s*/g, ')')
          .trim();
      };

      const normalizedExpected = normalizeGrid(expected);
      const normalizedActual = normalizeGrid(actual);

      // Direct match
      if (normalizedActual === normalizedExpected) {
        passed = true;
      }
      // Check if the value was applied at all
      else if (actual && actual !== 'none' && actual !== '') {
        // For valid grid patterns, if something was applied, check for close match
        console.log(`Grid normalization mismatch:`, {
          className: testData.className,
          expected: normalizedExpected,
          actual: normalizedActual
        });

        // More lenient matching for complex patterns
        passed = normalizedActual.includes('repeat') && normalizedActual.includes('minmax');
      }

      return {
        passed,
        actual: actual || 'none',
        expected
      };
    }

    /**
     * Validates CSS shorthand property applications.
     *
     * Handles validation of shorthand properties like padding, margin,
     * border-width, and border-radius by checking both shorthand values
     * and individual directional properties with appropriate normalization.
     *
     * @param {object} testData
     *   Test case data for shorthand property validation.
     * @param {CSSStyleDeclaration} computed
     *   Computed styles for the test element.
     *
     * @returns {object}
     *   Validation result with shorthand-specific handling.
     */
    validateShorthand(testData, computed) {
      const results = {
        passed: false,
        actual: '',
        expected: testData.expectedValue
      };

      // Determine which property to check
      const baseProperty = testData.className.includes('mg') ? 'margin' :
                          testData.className.includes('pd') ? 'padding' :
                          testData.className.includes('bw') ? 'borderWidth' :
                          testData.className.includes('br') ? 'borderRadius' : 'padding';

      // Try to get the shorthand value first
      let shorthandValue = computed[baseProperty];

      if (shorthandValue && shorthandValue !== '') {
        results.actual = shorthandValue;
        // Normalize and compare
        const normalizedActual = this.normalizeValue(shorthandValue);
        const normalizedExpected = this.normalizeValue(testData.expectedValue);
        results.passed = normalizedActual === normalizedExpected;
      } else {
        // If no shorthand, check individual properties
        const sides = ['Top', 'Right', 'Bottom', 'Left'];
        const values = sides.map(side => {
          const prop = baseProperty + side;
          return computed[prop] || '0px';
        });

        // Build the shorthand representation
        if (values[0] === values[2] && values[1] === values[3]) {
          if (values[0] === values[1]) {
            // All sides same
            results.actual = values[0];
          } else {
            // Top/bottom same, left/right same
            results.actual = `${values[0]} ${values[1]}`;
          }
        } else if (values[1] === values[3]) {
          // Left/right same
          results.actual = `${values[0]} ${values[1]} ${values[2]}`;
        } else {
          // All different
          results.actual = values.join(' ');
        }

        // Compare with expected
        const normalizedActual = this.normalizeValue(results.actual);
        const normalizedExpected = this.normalizeValue(testData.expectedValue);
        results.passed = normalizedActual === normalizedExpected;
      }

      return results;
    }

     /**
     * Validates CSS transform property applications.
     *
     * Specialized validation for transform properties including rotation
     * and scale transforms. Handles browser-specific matrix conversions
     * and identity transform edge cases with high precision comparison.
     *
     * @param {object} testData
     *   Test case data for transform validation.
     * @param {CSSStyleDeclaration} computed
     *   Computed styles for the test element.
     * @param {Element} element
     *   The DOM element being tested.
     *
     * @returns {object}
     *   Validation result with transform-specific handling.
     */
    validateTransform(testData, computed, element) {
      const expected = testData.expectedValue;

      // Get both inline and computed transforms
      const inlineTransform = element.style.transform || '';
      const computedTransform = computed.transform || 'none';

      // For transform tests, we primarily check the inline style since that's what UtiliKit sets
      // Your HTML shows this is working: style="transform: scale(1.5);"

      // First, if we have a valid inline transform that matches, that's a pass
      if (inlineTransform && inlineTransform !== 'none' && inlineTransform === expected) {
        return { passed: true, actual: inlineTransform, expected };
      }

      let passed = false;
      let actualTransform = inlineTransform || computedTransform;

      // If both are empty or 'none', return what we got
      if (!inlineTransform && (computedTransform === 'none' || !computedTransform)) {
        return {
          passed: false,
          actual: 'none',
          expected
        };
      }

      // Special cases for identity transforms
      if (expected === 'rotate(0deg)' && (actualTransform === 'none' || actualTransform === '')) {
        passed = true;
        actualTransform = 'rotate(0deg)';
      } else if (expected === 'scale(1)' && (actualTransform === 'none' || actualTransform === '')) {
        passed = true;
        actualTransform = 'scale(1)';
      }
      // Check rotate values
      else if (expected.includes('rotate')) {
        const expectedMatch = expected.match(/rotate\((-?\d+(?:\.\d+)?)deg\)/);
        if (expectedMatch) {
          const expectedDeg = parseFloat(expectedMatch[1]);

          // Check inline style first (most reliable)
          if (inlineTransform && inlineTransform.includes(`rotate(${expectedDeg}deg)`)) {
            passed = true;
            actualTransform = expected;
          }
          // Check if computed has the rotate value
          else if (computedTransform.includes('rotate')) {
            const computedMatch = computedTransform.match(/rotate\((-?\d+(?:\.\d+)?)deg\)/);
            if (computedMatch) {
              const computedDeg = parseFloat(computedMatch[1]);
              passed = Math.abs(computedDeg - expectedDeg) < 0.1;
              actualTransform = `rotate(${computedDeg}deg)`;
            }
          }
          // Check if computed is a matrix (browser converted it)
          else if (computedTransform.includes('matrix')) {
            // If we have the correct inline style but browser returns matrix, consider it passed
            if (inlineTransform === expected) {
              passed = true;
              actualTransform = expected;
            }
          }
        }
      }
      // Check scale values
      else if (expected.includes('scale')) {
        const expectedMatch = expected.match(/scale\((\d+(?:\.\d+)?)\)/);
        if (expectedMatch) {
          const expectedScale = parseFloat(expectedMatch[1]);

          // Check inline style first - handle both scale(1.5) and scale(1.50)
          if (inlineTransform) {
            // Create regex to match scale with the expected value (handling decimal variations)
            const scaleRegex = new RegExp(`scale\\(${expectedScale}(?:\\.0+)?\\)`);
            if (scaleRegex.test(inlineTransform)) {
              passed = true;
              actualTransform = expected;
            }
          }

          // If not found in inline, check computed
          if (!passed && computedTransform.includes('scale')) {
            const computedMatch = computedTransform.match(/scale\((\d+(?:\.\d+)?)\)/);
            if (computedMatch) {
              const computedScale = parseFloat(computedMatch[1]);
              passed = Math.abs(computedScale - expectedScale) < 0.01;
              actualTransform = `scale(${computedScale})`;
            }
          }
          // Check if computed is a matrix
          else if (!passed && computedTransform.includes('matrix')) {
            // For scale, we can extract from matrix
            const matrixMatch = computedTransform.match(/matrix\(([^,]+),/);
            if (matrixMatch) {
              const scaleValue = parseFloat(matrixMatch[1]);
              passed = Math.abs(scaleValue - expectedScale) < 0.01;
              actualTransform = `scale(${scaleValue})`;
            }
          }
        }
      }

      return {
        passed,
        actual: actualTransform || 'none',
        expected
      };
    }

    /**
     * Normalizes CSS values for consistent comparison.
     *
     * Standardizes CSS values by removing extra whitespace, normalizing
     * zero values, and converting color formats to consistent representations
     * for accurate test validation.
     *
     * @param {*} value
     *   The CSS value to normalize (converted to string if necessary).
     *
     * @returns {string}
     *   The normalized CSS value ready for comparison.
     */
    normalizeValue(value) {
      if (typeof value !== 'string') return String(value);

      // Remove extra spaces
      value = value.trim();

      // Normalize 0 values
      if (value === '0' || value === '0px') return '0px';

      // Normalize color formats
      if (value.startsWith('rgb')) {
        return this.rgbToHex(value);
      }

      return value;
    }

    /**
     * Compares color values with format normalization.
     *
     * Handles color comparison by converting both values to hexadecimal
     * format for consistent comparison regardless of the original format
     * (RGB, RGBA, hex, etc.).
     *
     * @param {string} actual
     *   The actual color value from computed styles.
     * @param {string} expected
     *   The expected color value from test data.
     *
     * @returns {boolean}
     *   TRUE if colors match after normalization, FALSE otherwise.
     */
    compareColors(actual, expected) {
      // Convert both to hex for comparison
      const actualHex = actual.startsWith('#') ? actual : this.rgbToHex(actual);
      const expectedHex = expected.startsWith('#') ? expected : this.rgbToHex(expected);

      return actualHex.toLowerCase() === expectedHex.toLowerCase();
    }

    /**
     * Converts RGB/RGBA color values to hexadecimal format.
     *
     * Parses RGB or RGBA color strings and converts them to hexadecimal
     * representation for consistent color comparison. Returns original
     * value if parsing fails.
     *
     * @param {string} rgb
     *   RGB or RGBA color string to convert.
     *
     * @returns {string}
     *   Hexadecimal color value or original string if conversion fails.
     */
    rgbToHex(rgb) {
      const match = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
      if (!match) return rgb;

      const r = parseInt(match[1]).toString(16).padStart(2, '0');
      const g = parseInt(match[2]).toString(16).padStart(2, '0');
      const b = parseInt(match[3]).toString(16).padStart(2, '0');

      return `#${r}${g}${b}`;
    }

    /**
     * Updates the test row display with validation results.
     *
     * Modifies the test case row in the interface to display the actual
     * computed value and pass/fail status with appropriate visual styling
     * for immediate feedback during test execution.
     *
     * @param {Element} testCase
     *   The test case row element to update.
     * @param {object} results
     *   Validation results containing passed status and actual value.
     */
    updateTestRow(testCase, results) {
      const actualCell = testCase.querySelector('.test-actual');
      const statusCell = testCase.querySelector('.test-status');
      const statusBadge = statusCell.querySelector('.status-badge');

      actualCell.textContent = results.actual;

      statusBadge.textContent = results.passed ? 'Passed' : 'Failed';
      statusBadge.className = `status-badge badge ${results.passed ? 'passed' : 'failed'}`;

      testCase.classList.add(results.passed ? 'test-passed' : 'test-failed');
    }

    /**
     * Toggles visibility of all test groups.
     *
     * Expands or collapses all test rule groups simultaneously for
     * comprehensive viewing or focused interface management. Updates
     * toggle button text to reflect current state.
     *
     * @param {boolean} show
     *   TRUE to expand all test groups, FALSE to collapse all.
     */
    toggleAllTests(show) {
      this.container.querySelectorAll('.rule-tests').forEach(ruleTests => {
        ruleTests.style.display = show ? 'block' : 'none';
        const btn = ruleTests.closest('.test-rule').querySelector('.toggle-tests');
        btn.textContent = show ? 'Hide Tests' : 'Show Tests';
      });
    }

    /**
     * Applies user-selected filters to the test display.
     *
     * Filters test visibility based on status (passed/failed/pending)
     * and group category, providing focused views of specific test
     * subsets for targeted analysis and debugging.
     */
    applyFilters() {
      const statusFilter = this.elements.statusFilter.value;
      const groupFilter = this.elements.groupFilter.value;

      // Filter by group
      this.container.querySelectorAll('.test-group').forEach(group => {
        const shouldShowGroup = groupFilter === 'all' || group.dataset.group === groupFilter;
        group.style.display = shouldShowGroup ? 'block' : 'none';
      });

      // Filter by status
      if (statusFilter !== 'all') {
        this.testCases.forEach(testCase => {
          let shouldShow = true;

          if (statusFilter === 'passed' && !testCase.classList.contains('test-passed')) {
            shouldShow = false;
          } else if (statusFilter === 'failed' && !testCase.classList.contains('test-failed')) {
            shouldShow = false;
          } else if (statusFilter === 'pending' &&
                     (testCase.classList.contains('test-passed') ||
                      testCase.classList.contains('test-failed'))) {
            shouldShow = false;
          }

          testCase.style.display = shouldShow ? '' : 'none';
        });
      } else {
        this.testCases.forEach(testCase => {
          testCase.style.display = '';
        });
      }
    }

    /**
     * Exports comprehensive test results to JSON file.
     *
     * Generates a detailed report containing test summary, individual
     * results, failed test analysis, and system information for external
     * analysis, reporting, and debugging purposes.
     */
    exportResults() {
      const results = {
        summary: {
          totalTests: this.totalTests,
          passed: this.passedTests,
          failed: this.failedTests,
          passRate: ((this.passedTests / this.totalTests) * 100).toFixed(2) + '%',
          timestamp: new Date().toISOString(),
          userAgent: navigator.userAgent,
          viewport: `${window.innerWidth}x${window.innerHeight}`,
          breakpoint: Drupal.utilikit.getBreakpoint()
        },
        tests: Array.from(this.testResults.entries()).map(([id, result]) => ({
          id,
          ...result
        })),
        failedTests: Array.from(this.testResults.entries())
          .filter(([_, result]) => !result.passed)
          .map(([id, result]) => ({
            id,
            class: result.className,
            expected: result.expected,
            actual: result.actual
          }))
      };

      const blob = new Blob([JSON.stringify(results, null, 2)], { type: 'application/json' });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = `utilikit-test-results-${Date.now()}.json`;
      a.click();
      URL.revokeObjectURL(url);

      this.log('Test results exported successfully', 'success');
    }

    /**
     * Resets all test results and UI state.
     *
     * Clears test counters, result storage, and visual indicators
     * to prepare for a fresh test run. Restores all test cases
     * to pending status.
     */
    resetResults() {
      this.passedTests = 0;
      this.failedTests = 0;
      this.currentTest = 0;
      this.testResults.clear();

      this.testCases.forEach(testCase => {
        testCase.classList.remove('test-passed', 'test-failed');
        const statusBadge = testCase.querySelector('.status-badge');
        statusBadge.textContent = 'Pending';
        statusBadge.className = 'status-badge pending badge badge--warning';
        testCase.querySelector('.test-actual').textContent = '-';
      });

      this.updateSummary();
    }

    /**
     * Updates test execution progress display.
     *
     * Shows current progress percentage and test counts during
     * test execution for real-time feedback on execution status.
     */
    updateProgress() {
      const progress = ((this.currentTest / this.totalTests) * 100).toFixed(0);
      this.elements.testStatus.textContent = `Testing... ${progress}% (${this.currentTest}/${this.totalTests})`;
    }

    /**
     * Updates the test summary display with current statistics.
     *
     * Refreshes pass/fail counts, applies appropriate styling,
     * and updates completion status when tests finish. Provides
     * immediate visual feedback on test suite performance.
     */
    updateSummary() {
      this.elements.passedCount.textContent = this.passedTests;
      this.elements.failedCount.textContent = this.failedTests;

      if (this.passedTests > 0) {
        this.elements.passedCount.classList.add('success');
      }

      if (this.failedTests > 0) {
        this.elements.failedCount.classList.add('error');
      }

      if (!this.isRunning && this.currentTest > 0) {
        const passRate = ((this.passedTests / this.currentTest) * 100).toFixed(1);
        this.elements.testStatus.textContent = `Completed - ${passRate}% passed`;
        this.elements.testStatus.className = this.failedTests === 0 ? 'value success' : 'value error';
      }
    }

    /**
     * Waits for all stylesheets to load before proceeding.
     *
     * Ensures CSS resources are fully loaded before test execution
     * to prevent validation failures due to missing styles. Critical
     * for static mode testing where external CSS files are required.
     *
     * @returns {Promise<void>}
     *   A promise that resolves when all stylesheets are loaded
     *   or after a 5-second timeout.
     */
    waitForStylesheets() {
      return new Promise((resolve) => {
        const stylesheets = document.querySelectorAll('link[rel="stylesheet"]');
        let pending = stylesheets.length;

        if (pending === 0) {
          resolve();
          return;
        }

        stylesheets.forEach(link => {
          if (link.sheet) {
            pending--;
            if (pending === 0) resolve();
          } else {
            const checkLoad = () => {
              pending--;
              if (pending === 0) resolve();
            };
            link.addEventListener('load', checkLoad);
            link.addEventListener('error', checkLoad);
          }
        });

        // Timeout after 5 seconds
        setTimeout(resolve, 5000);
      });
    }

    /**
     * Stops test execution when requested by user.
     *
     * Sets the stop flag to halt test execution at the next
     * batch boundary, allowing graceful termination of long-running
     * test suites.
     */
    stopTests() {
      this.shouldStop = true;
      this.log('Stopping tests...', 'warn');
    }

    /**
     * Completes test run and updates interface state.
     *
     * Finalizes test execution by resetting UI state, updating
     * button text, and logging comprehensive results summary.
     * Called when all tests complete or execution is stopped.
     */
    finishTestRun() {
      this.isRunning = false;
      this.elements.runAllBtn.textContent = 'Run All Tests';
      this.updateSummary();

      const summary = `Test run completed: ${this.passedTests} passed, ${this.failedTests} failed out of ${this.currentTest} tests`;
      this.log(summary, this.failedTests === 0 ? 'success' : 'error');
    }

    /**
     * Logs a timestamped message to the test console.
     *
     * Adds formatted log entries to the test console with timestamps
     * and appropriate styling based on message type. Automatically
     * scrolls to show latest messages.
     *
     * @param {string} message
     *   The message text to log.
     * @param {string} [type='info']
     *   The message type for styling ('info', 'success', 'warn', 'error').
     */
    log(message, type = 'info') {
      const timestamp = new Date().toLocaleTimeString();
      const entry = document.createElement('div');
      entry.className = `log-entry ${type}`;
      entry.textContent = `[${timestamp}] ${message}`;

      this.elements.logContent.appendChild(entry);
      this.elements.logContent.scrollTop = this.elements.logContent.scrollHeight;
    }

    /**
     * Clears the test console log.
     *
     * Removes all log entries from the test console and adds
     * an initial "Log cleared" message to confirm the action.
     */
    clearLog() {
      this.elements.logContent.innerHTML = '';
      this.log('Log cleared', 'info');
    }
  }

})(Drupal, once);

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc