/** * Theme Independence Test Suite * * Tests that the HVAC Community Events plugin works independently of themes: * - Layout consistency across different themes * - Asset loading without theme dependencies * - Template fallback mechanisms * - CSS isolation verification * - No hardcoded theme-specific code * - Responsive design consistency * * @package HVAC_Community_Events * @version 3.0.0 * @created 2025-08-20 */ const { test, expect, authHelpers } = require('../helpers/auth-fixtures'); const path = require('path'); // Test configuration const BASE_URL = process.env.UPSKILL_STAGING_URL || 'https://upskill-staging.measurequick.com'; const TEST_TIMEOUT = 90000; // Theme-independent pages to test const PLUGIN_PAGES = [ '/trainer/dashboard/', '/trainer/profile/', '/trainer/events/', '/trainer/events/create/', '/trainer/certificate-reports/', '/master-trainer/master-dashboard/', '/find-trainer/' ]; // Expected core layout elements (theme-independent) const CORE_ELEMENTS = { navigation: ['.hvac-trainer-nav', '.hvac-nav-menu', 'nav[class*="hvac"]'], content: ['.hvac-page-wrapper', '.hvac-content', 'main[class*="hvac"]'], forms: ['.hvac-form-wrapper', 'form[class*="hvac"]'], buttons: ['.hvac-button', 'button[class*="hvac"]'], headers: ['.hvac-page-header', '.hvac-dashboard-header'] }; // CSS properties that should be consistent regardless of theme const LAYOUT_PROPERTIES = [ 'display', 'position', 'width', 'max-width', 'padding', 'margin', 'flex-direction', 'grid-template-columns' ]; // Helper functions async function loginAsTrainer(page) { await authHelpers.loginAs(page, 'trainer'); } async function getCurrentTheme(page) { return await page.evaluate(() => { // Try to determine active theme from various indicators const body = document.body; const bodyClasses = Array.from(body.classList); // Check for theme-specific body classes const themeClasses = bodyClasses.filter(cls => cls.includes('theme-') || cls.includes('astra') || cls.includes('twentytwenty') || cls.includes('genesis') || cls.includes('divi') || cls.includes('avada') ); // Check for theme-specific stylesheets const stylesheets = Array.from(document.querySelectorAll('link[rel="stylesheet"]')) .map(link => link.href) .filter(href => href.includes('themes/')); const themeFromStylesheet = stylesheets.length > 0 ? stylesheets[0].match(/themes\/([^\/]+)/)?.[1] : 'unknown'; return { bodyClasses: themeClasses, detectedTheme: themeFromStylesheet, totalStylesheets: stylesheets.length }; }); } async function checkElementConsistency(page, selectors) { const elements = []; for (const selector of selectors) { const element = await page.$(selector); if (element) { const styles = await element.evaluate((el, properties) => { const computed = window.getComputedStyle(el); const result = {}; properties.forEach(prop => { result[prop] = computed.getPropertyValue(prop); }); return result; }, LAYOUT_PROPERTIES); const boundingBox = await element.boundingBox(); elements.push({ selector, styles, boundingBox, isVisible: await element.isVisible() }); } } return elements; } async function analyzePluginAssets(page) { return await page.evaluate(() => { const assets = { hvacCss: [], hvacJs: [], themeCss: [], themeJs: [], conflicts: [] }; // Analyze CSS files Array.from(document.querySelectorAll('link[rel="stylesheet"]')).forEach(link => { if (link.href.includes('hvac') || link.href.includes('community-events')) { assets.hvacCss.push(link.href); } else if (link.href.includes('themes/')) { assets.themeCss.push(link.href); } }); // Analyze JS files Array.from(document.querySelectorAll('script[src]')).forEach(script => { if (script.src.includes('hvac') || script.src.includes('community-events')) { assets.hvacJs.push(script.src); } else if (script.src.includes('themes/')) { assets.themeJs.push(script.src); } }); // Check for CSS conflicts const hvacElements = document.querySelectorAll('[class*="hvac"]'); hvacElements.forEach(el => { const computed = window.getComputedStyle(el); // Check if theme styles are overriding plugin styles const backgroundImage = computed.backgroundImage; const fontFamily = computed.fontFamily; if (backgroundImage !== 'none' && !backgroundImage.includes('hvac')) { assets.conflicts.push({ element: el.className, property: 'background-image', value: backgroundImage }); } }); return assets; }); } async function checkResponsiveConsistency(page, breakpoints = [1200, 768, 480]) { const results = []; for (const breakpoint of breakpoints) { await page.setViewportSize({ width: breakpoint, height: 720 }); await page.waitForTimeout(1000); // Check navigation visibility and behavior const navState = await page.evaluate(() => { const nav = document.querySelector('.hvac-trainer-nav, .hvac-nav-menu'); const hamburger = document.querySelector('.hvac-menu-toggle, .menu-toggle'); return { navVisible: nav ? window.getComputedStyle(nav).display !== 'none' : false, hamburgerVisible: hamburger ? window.getComputedStyle(hamburger).display !== 'none' : false, navPosition: nav ? window.getComputedStyle(nav).position : 'none' }; }); // Check content layout const contentState = await page.evaluate(() => { const container = document.querySelector('.hvac-page-wrapper, .hvac-content, main'); const sidebar = document.querySelector('.sidebar, .widget-area'); return { containerWidth: container ? container.offsetWidth : 0, hasFlexLayout: container ? window.getComputedStyle(container).display.includes('flex') : false, sidebarVisible: sidebar ? window.getComputedStyle(sidebar).display !== 'none' : false }; }); results.push({ breakpoint, navigation: navState, content: contentState }); } return results; } async function takeThemeScreenshot(page, name, theme) { const screenshotDir = path.join(__dirname, '../../screenshots/theme-independence'); await require('fs').promises.mkdir(screenshotDir, { recursive: true }); await page.screenshot({ path: path.join(screenshotDir, `${name}-${theme}-${Date.now()}.png`), fullPage: true }); } test.describe('Theme Independence Tests', () => { test.setTimeout(TEST_TIMEOUT); test.beforeEach(async ({ page }) => { await page.setViewportSize({ width: 1280, height: 720 }); // Login before each test to ensure authenticated access await authHelpers.loginAs(page, 'trainer'); }); test.describe('Layout Consistency Tests', () => { test('should maintain consistent layout across plugin pages', async ({ page }) => { const currentTheme = await getCurrentTheme(page); console.log('Current theme detected:', currentTheme); const pageLayouts = {}; // Test each plugin page for (const pagePath of PLUGIN_PAGES) { try { await page.goto(`${BASE_URL}${pagePath}`, { timeout: 15000 }); await page.waitForLoadState('domcontentloaded'); // Check for core plugin elements const elements = await checkElementConsistency(page, [ '.hvac-page-wrapper', '.hvac-content', '.hvac-nav-menu', 'main' ]); pageLayouts[pagePath] = elements; // Verify page has plugin content wrapper const hasWrapper = await page.locator('.hvac-page-wrapper, .hvac-content, main').count() > 0; expect(hasWrapper).toBeTruthy(); console.log(`Layout check for ${pagePath}: OK`); } catch (error) { console.log(`Layout check for ${pagePath} failed:`, error.message); } } // Verify layout consistency const wrapperElements = Object.values(pageLayouts) .map(layout => layout.find(el => el.selector === '.hvac-page-wrapper')) .filter(Boolean); if (wrapperElements.length > 1) { const firstLayout = wrapperElements[0]; const inconsistencies = wrapperElements.slice(1).filter(layout => { return layout.styles.display !== firstLayout.styles.display || layout.styles.maxWidth !== firstLayout.styles.maxWidth; }); expect(inconsistencies.length).toBeLessThan(wrapperElements.length / 2); } await takeThemeScreenshot(page, 'layout-consistency', currentTheme.detectedTheme); }); test('should use theme-independent container widths', async ({ page }) => { const containerSpecs = []; for (const pagePath of PLUGIN_PAGES.slice(0, 3)) { // Test subset for speed await page.goto(`${BASE_URL}${pagePath}`); const containerInfo = await page.evaluate(() => { const containers = document.querySelectorAll('.hvac-page-wrapper, .hvac-content, .container'); return Array.from(containers).map(container => ({ className: container.className, offsetWidth: container.offsetWidth, clientWidth: container.clientWidth, computedWidth: window.getComputedStyle(container).width, computedMaxWidth: window.getComputedStyle(container).maxWidth })); }); containerSpecs.push({ page: pagePath, containers: containerInfo }); } // Check for consistent max-width usage const maxWidths = containerSpecs.flatMap(spec => spec.containers.map(c => c.computedMaxWidth) ).filter(w => w !== 'none'); if (maxWidths.length > 0) { const uniqueMaxWidths = [...new Set(maxWidths)]; console.log('Container max-widths found:', uniqueMaxWidths); // Should use consistent max-width values expect(uniqueMaxWidths.length).toBeLessThan(4); // Allow some variation } }); test('should handle missing theme support gracefully', async ({ page }) => { await page.goto(`${BASE_URL}/trainer/dashboard/`); // Simulate theme support removal await page.evaluate(() => { // Remove theme support indicators const themeElements = document.querySelectorAll('[class*="theme-"], [class*="astra"]'); themeElements.forEach(el => { el.classList.forEach(cls => { if (cls.includes('theme-') || cls.includes('astra')) { el.classList.remove(cls); } }); }); // Remove theme stylesheets temporarily const themeStyles = document.querySelectorAll('link[href*="themes/"]'); themeStyles.forEach(link => { link.disabled = true; }); }); await page.waitForTimeout(1000); // Check if plugin still functions const pluginFunctional = await page.evaluate(() => { const navMenu = document.querySelector('.hvac-nav-menu, .hvac-trainer-nav'); const content = document.querySelector('.hvac-content, .hvac-page-wrapper'); return { hasNavigation: !!navMenu, hasContent: !!content, contentVisible: content ? window.getComputedStyle(content).display !== 'none' : false }; }); expect(pluginFunctional.hasContent).toBeTruthy(); console.log('Plugin graceful degradation:', pluginFunctional); }); }); test.describe('Asset Independence Tests', () => { test('should load plugin assets independently', async ({ page }) => { await page.goto(`${BASE_URL}/trainer/dashboard/`); const assetAnalysis = await analyzePluginAssets(page); // Should have plugin-specific assets expect(assetAnalysis.hvacCss.length).toBeGreaterThan(0); console.log('HVAC CSS files:', assetAnalysis.hvacCss.length); console.log('Theme CSS files:', assetAnalysis.themeCss.length); // Check for asset conflicts if (assetAnalysis.conflicts.length > 0) { console.log('Theme conflicts detected:', assetAnalysis.conflicts); // Should have minimal conflicts expect(assetAnalysis.conflicts.length).toBeLessThan(5); } // Verify plugin assets load before theme assets const loadOrder = await page.evaluate(() => { const allStyles = Array.from(document.querySelectorAll('link[rel="stylesheet"]')); const hvacIndex = allStyles.findIndex(link => link.href.includes('hvac') || link.href.includes('community-events') ); const themeIndex = allStyles.findIndex(link => link.href.includes('themes/')); return { hvacIndex, themeIndex, total: allStyles.length }; }); console.log('Asset load order:', loadOrder); }); test('should not depend on theme-specific CSS classes', async ({ page }) => { await page.goto(`${BASE_URL}/trainer/dashboard/`); // Check for theme-specific dependencies const themeDependencies = await page.evaluate(() => { const hvacElements = document.querySelectorAll('[class*="hvac"]'); const dependencies = []; hvacElements.forEach(el => { const classes = Array.from(el.classList); const themeClasses = classes.filter(cls => cls.includes('astra') || cls.includes('twentytwenty') || cls.includes('genesis') || cls.includes('theme-') ); if (themeClasses.length > 0) { dependencies.push({ element: el.tagName + '.' + classes.join('.'), themeClasses }); } }); return dependencies; }); // Should have minimal theme dependencies expect(themeDependencies.length).toBeLessThan(10); console.log('Theme class dependencies:', themeDependencies.length); if (themeDependencies.length > 0) { console.log('Dependencies found:', themeDependencies.slice(0, 3)); } }); test('should handle theme stylesheet conflicts', async ({ page }) => { await page.goto(`${BASE_URL}/trainer/dashboard/`); // Test critical plugin elements for style conflicts const criticalElements = [ '.hvac-nav-menu', '.hvac-button', '.hvac-form-wrapper', '.hvac-page-header' ]; const styleConflicts = []; for (const selector of criticalElements) { const element = await page.$(selector); if (element) { const styles = await element.evaluate(el => { const computed = window.getComputedStyle(el); return { display: computed.display, position: computed.position, zIndex: computed.zIndex, fontSize: computed.fontSize, color: computed.color }; }); // Check for unexpected style values that might indicate conflicts if (styles.display === 'none' && selector.includes('nav')) { styleConflicts.push({ selector, issue: 'Navigation hidden' }); } if (styles.zIndex === 'auto' && selector.includes('nav')) { styleConflicts.push({ selector, issue: 'No z-index set' }); } } } console.log('Style conflicts detected:', styleConflicts.length); expect(styleConflicts.length).toBeLessThan(3); }); }); test.describe('Responsive Design Independence Tests', () => { test('should maintain responsive behavior regardless of theme', async ({ page }) => { await page.goto(`${BASE_URL}/trainer/dashboard/`); const responsiveResults = await checkResponsiveConsistency(page); // Verify navigation adapts correctly at all breakpoints responsiveResults.forEach(result => { console.log(`Breakpoint ${result.breakpoint}px:`, { nav: result.navigation.navVisible, hamburger: result.navigation.hamburgerVisible, contentWidth: result.content.containerWidth }); // At mobile breakpoints, either nav should be hidden or hamburger should be visible if (result.breakpoint <= 768) { const mobileNavHandled = !result.navigation.navVisible || result.navigation.hamburgerVisible; expect(mobileNavHandled).toBeTruthy(); } // Content should never be zero width expect(result.content.containerWidth).toBeGreaterThan(0); }); await takeThemeScreenshot(page, 'responsive-design', 'current'); }); test('should handle mobile navigation independently', async ({ page }) => { await page.setViewportSize({ width: 375, height: 667 }); // Mobile viewport await page.goto(`${BASE_URL}/trainer/dashboard/`); // Check for mobile navigation const mobileNav = await page.evaluate(() => { const hamburger = document.querySelector('.hvac-menu-toggle, .menu-toggle, .hamburger'); const navMenu = document.querySelector('.hvac-nav-menu, .hvac-trainer-nav'); return { hasHamburger: !!hamburger, hamburgerVisible: hamburger ? window.getComputedStyle(hamburger).display !== 'none' : false, navHidden: navMenu ? window.getComputedStyle(navMenu).display === 'none' : true }; }); console.log('Mobile navigation state:', mobileNav); // Should have mobile navigation solution const hasMobileNav = mobileNav.hasHamburger || !mobileNav.navHidden; expect(hasMobileNav).toBeTruthy(); // If hamburger exists, test functionality if (mobileNav.hasHamburger) { const hamburger = await page.$('.hvac-menu-toggle, .menu-toggle, .hamburger'); if (hamburger) { await hamburger.click(); await page.waitForTimeout(500); const menuOpened = await page.evaluate(() => { const menu = document.querySelector('.hvac-nav-menu'); return menu ? window.getComputedStyle(menu).display !== 'none' : false; }); expect(menuOpened).toBeTruthy(); } } }); test('should maintain form layout on all devices', async ({ page }) => { const breakpoints = [1200, 768, 480]; const formResults = []; for (const breakpoint of breakpoints) { await page.setViewportSize({ width: breakpoint, height: 720 }); await page.goto(`${BASE_URL}/trainer/profile/edit/`); await page.waitForTimeout(1000); const formLayout = await page.evaluate(() => { const form = document.querySelector('form'); const inputs = document.querySelectorAll('input, select, textarea'); if (!form) return null; return { formWidth: form.offsetWidth, inputCount: inputs.length, stackedInputs: Array.from(inputs).filter(input => { const rect = input.getBoundingClientRect(); return rect.width > form.offsetWidth * 0.8; // Nearly full width = stacked }).length }; }); if (formLayout) { formResults.push({ breakpoint, ...formLayout }); // Form should be usable at all breakpoints expect(formLayout.formWidth).toBeGreaterThan(200); // At mobile breakpoints, inputs should be mostly stacked if (breakpoint <= 768) { const stackedRatio = formLayout.stackedInputs / formLayout.inputCount; expect(stackedRatio).toBeGreaterThan(0.5); } } } console.log('Form responsive results:', formResults); }); }); test.describe('Template Fallback Tests', () => { test('should provide default templates when theme lacks support', async ({ page }) => { // Test pages that might not have theme support const fallbackPages = [ '/trainer/certificate-reports/', '/trainer/events/create/', '/trainer/organizer/manage/' ]; for (const pagePath of fallbackPages) { await page.goto(`${BASE_URL}${pagePath}`); // Check if page loads with plugin template const hasTemplate = await page.evaluate(() => { return !!( document.querySelector('.hvac-page-wrapper') || document.querySelector('.hvac-template') || document.body.className.includes('hvac-page') ); }); expect(hasTemplate).toBeTruthy(); console.log(`Template fallback for ${pagePath}: OK`); // Check for WordPress integration const hasWordPressElements = await page.evaluate(() => { return !!( document.querySelector('header, .site-header') && document.querySelector('footer, .site-footer') ); }); expect(hasWordPressElements).toBeTruthy(); } }); test('should handle missing theme functions gracefully', async ({ page }) => { await page.goto(`${BASE_URL}/trainer/dashboard/`); // Simulate missing theme functions const errorCount = await page.evaluate(() => { let errors = 0; // Override console.error to catch theme-related errors const originalError = console.error; console.error = function(...args) { const message = args.join(' '); if (message.includes('theme') || message.includes('undefined function')) { errors++; } originalError.apply(console, args); }; // Try to trigger theme function calls try { // Simulate missing theme support checks if (typeof window.themeSupports !== 'undefined') { window.themeSupports.customLogo(); } } catch (e) { if (e.message.includes('theme')) { errors++; } } return errors; }); // Should handle missing theme functions without errors expect(errorCount).toBeLessThan(3); console.log('Theme function errors:', errorCount); }); test('should maintain functionality with minimal theme support', async ({ page }) => { // Test core functionality without theme enhancements const coreFeatures = [ { name: 'Navigation', test: async () => { await page.goto(`${BASE_URL}/trainer/dashboard/`); return await page.locator('.hvac-nav-menu, nav').count() > 0; } }, { name: 'Forms', test: async () => { await page.goto(`${BASE_URL}/trainer/profile/edit/`); return await page.locator('form').count() > 0; } }, { name: 'Content Display', test: async () => { await page.goto(`${BASE_URL}/trainer/events/`); return await page.locator('.hvac-content, main').count() > 0; } } ]; for (const feature of coreFeatures) { const works = await feature.test(); expect(works).toBeTruthy(); console.log(`Core feature '${feature.name}' working:`, works); } }); }); test.describe('CSS Isolation Tests', () => { test('should not interfere with theme styles', async ({ page }) => { await page.goto(`${BASE_URL}/`); // Homepage - theme territory // Check if plugin CSS affects theme elements const themeInterference = await page.evaluate(() => { const themeElements = document.querySelectorAll('header, footer, .site-main, .content-area'); const issues = []; themeElements.forEach(el => { const computed = window.getComputedStyle(el); // Check for unexpected plugin-style properties if (computed.backgroundColor && computed.backgroundColor.includes('hvac')) { issues.push('HVAC background color on theme element'); } if (computed.fontFamily && computed.fontFamily.includes('HVAC')) { issues.push('HVAC font on theme element'); } }); return issues; }); expect(themeInterference.length).toBeLessThan(2); console.log('Theme interference issues:', themeInterference.length); }); test('should scope plugin styles correctly', async ({ page }) => { await page.goto(`${BASE_URL}/trainer/dashboard/`); // Check CSS specificity and scoping const scopingAnalysis = await page.evaluate(() => { const hvacElements = document.querySelectorAll('[class*="hvac"]'); const properlyScoped = []; const unscoped = []; hvacElements.forEach(el => { const classes = Array.from(el.classList); const hasHvacClass = classes.some(cls => cls.startsWith('hvac-')); if (hasHvacClass) { properlyScoped.push(el.tagName); } else { unscoped.push(el.tagName); } }); return { properlyScoped: properlyScoped.length, unscoped: unscoped.length, scopingRatio: properlyScoped.length / (properlyScoped.length + unscoped.length) }; }); // Should have good scoping ratio expect(scopingAnalysis.scopingRatio).toBeGreaterThan(0.7); console.log('CSS scoping analysis:', scopingAnalysis); }); }); }); // Export theme independence test configuration module.exports = { testDir: __dirname, timeout: TEST_TIMEOUT, retries: 1, workers: 1, // Sequential for consistent testing use: { baseURL: BASE_URL, screenshot: 'only-on-failure', video: 'retain-on-failure', trace: 'on-first-retry' } };