#!/usr/bin/env node
/**
 * UI Functionality Tests for CI
 *
 * Comprehensive automated tests for UI functionality including:
 * - Form validation and submission
 * - Dropdown interactions
 * - Modal/dialog behavior
 * - Navigation functionality
 * - Scroll behavior
 * - Button states
 *
 * Run: node test_ui_functionality_ci.js
 *
 * Environment variables:
 *   - BASE_URL: Server URL (default: http://127.0.0.1:5000)
 *   - CI: Set to 'true' for CI mode
 *   - HEADLESS: 'true' or 'false' (default: true)
 */
const { setupTest, teardownTest, TestResults, log, delay, waitForVisible, config, navigateTo, withTimeout } = require('../test_lib');

// ============================================================================
// Form Tests
// ============================================================================
const FormTests = {
    async researchFormStructure(page, baseUrl) {
        await navigateTo(page, `${baseUrl}/`);

        const result = await page.evaluate(() => {
            const form = document.querySelector('form');
            const queryInput = document.querySelector('#query, textarea[name="query"], input[name="query"]');
            const submitBtn = document.querySelector('button[type="submit"], input[type="submit"]');

            return {
                formExists: !!form,
                queryInputExists: !!queryInput,
                submitBtnExists: !!submitBtn,
                queryInputType: queryInput?.tagName?.toLowerCase(),
                submitBtnText: submitBtn?.textContent?.trim()
            };
        });

        const passed = result.formExists && result.queryInputExists && result.submitBtnExists;
        return {
            passed,
            message: passed
                ? `Research form complete (query: ${result.queryInputType}, submit: "${result.submitBtnText}")`
                : `Missing elements: form=${result.formExists}, query=${result.queryInputExists}, submit=${result.submitBtnExists}`
        };
    },

    async inputFocusStates(page, baseUrl) {
        await navigateTo(page, `${baseUrl}/`);

        const result = await page.evaluate(() => {
            const input = document.querySelector('#query, textarea[name="query"], input[name="query"]');
            if (!input) return { exists: false };

            const initialOutline = window.getComputedStyle(input).outline;
            const initialBoxShadow = window.getComputedStyle(input).boxShadow;
            const initialBorderColor = window.getComputedStyle(input).borderColor;
            const initialBackgroundColor = window.getComputedStyle(input).backgroundColor;

            input.focus();

            // Give browser time to apply focus styles
            const focusedOutline = window.getComputedStyle(input).outline;
            const focusedBoxShadow = window.getComputedStyle(input).boxShadow;
            const focusedBorderColor = window.getComputedStyle(input).borderColor;
            const focusedBackgroundColor = window.getComputedStyle(input).backgroundColor;

            const hasVisualChange =
                initialOutline !== focusedOutline ||
                initialBoxShadow !== focusedBoxShadow ||
                initialBorderColor !== focusedBorderColor ||
                initialBackgroundColor !== focusedBackgroundColor;

            // Also check if input is focusable (a valid accessibility check)
            const isFocusable = document.activeElement === input;

            return { exists: true, hasVisualChange, isFocusable };
        });

        if (!result.exists) {
            return { passed: false, message: 'Query input not found' };
        }

        // Pass if input is focusable, even if no visual change is detected
        // (visual changes may depend on CSS frameworks/themes)
        const passed = result.isFocusable || result.hasVisualChange;
        return {
            passed,
            message: passed
                ? result.hasVisualChange
                    ? 'Input shows visible focus state'
                    : 'Input is focusable (visual focus style may vary by theme)'
                : 'Input not focusable (accessibility issue)'
        };
    },

    async settingsFormButtons(page, baseUrl) {
        await navigateTo(page, `${baseUrl}/settings/`);

        const result = await page.evaluate(() => {
            const saveBtns = Array.from(document.querySelectorAll('button, input[type="submit"]')).filter(btn =>
                btn.textContent?.toLowerCase().includes('save') ||
                btn.type === 'submit' ||
                btn.value?.toLowerCase().includes('save')
            );

            const resetBtns = Array.from(document.querySelectorAll('button, input[type="reset"]')).filter(btn =>
                btn.textContent?.toLowerCase().includes('reset') ||
                btn.type === 'reset' ||
                btn.value?.toLowerCase().includes('reset')
            );

            return {
                saveBtnExists: saveBtns.length > 0,
                resetBtnExists: resetBtns.length > 0,
                saveBtnText: saveBtns[0]?.textContent?.trim() || saveBtns[0]?.value,
                resetBtnText: resetBtns[0]?.textContent?.trim() || resetBtns[0]?.value
            };
        });

        return {
            passed: result.saveBtnExists,
            message: result.saveBtnExists
                ? `Settings form has save button ("${result.saveBtnText}")${result.resetBtnExists ? ` and reset ("${result.resetBtnText}")` : ''}`
                : 'Settings form missing save button'
        };
    }
};

// ============================================================================
// Dropdown Tests
// ============================================================================
const DropdownTests = {
    async dropdownStructure(page, baseUrl) {
        await navigateTo(page, `${baseUrl}/`);

        const result = await page.evaluate(() => {
            const dropdown = document.querySelector('.ldr-custom-dropdown');
            if (!dropdown) return { exists: false };

            const input = dropdown.querySelector('input');
            const list = dropdown.querySelector('.ldr-custom-dropdown-list, [class*="dropdown-list"]');
            const hiddenInput = dropdown.querySelector('input[type="hidden"]');

            return {
                exists: true,
                hasInput: !!input,
                hasList: !!list,
                hasHiddenInput: !!hiddenInput,
                listItemCount: list?.querySelectorAll('li, .dropdown-item').length || 0
            };
        });

        // If dropdown doesn't exist, this is a real skip - not a pass
        if (!result.exists) {
            return { passed: null, skipped: true, message: 'No custom dropdown on this page' };
        }

        const hasRequiredParts = result.hasInput && result.hasList;
        return {
            passed: hasRequiredParts,
            message: hasRequiredParts
                ? `Dropdown structure valid (${result.listItemCount} items, hidden input: ${result.hasHiddenInput})`
                : `Dropdown missing parts: input=${result.hasInput}, list=${result.hasList}`
        };
    },

    async dropdownOpensOnClick(page, baseUrl) {
        // Clicking a custom-dropdown input runs custom_dropdown.js::showDropdown,
        // which sets the list to display:block, adds .ldr-dropdown-active, AND
        // reparents the list to document.body. We target the search-engine
        // dropdown specifically (#search_engine): its options come from
        // /settings/api/available-search-engines (no LLM), unlike the model
        // dropdown which is empty without a configured provider. Because the list
        // is moved to <body>, read it by id (#search-engine-dropdown-list), not as
        // a descendant of .ldr-custom-dropdown. The old test had OR/skip fallbacks
        // that passed even when the dropdown never opened.
        await navigateTo(page, `${baseUrl}/`);
        await page.waitForSelector('#search_engine', { timeout: 15000 });

        // research.js sets up #search_engine TWICE: synchronously on load and again
        // inside Promise.all([...loadSearchEngineOptions]).then() once the fetch
        // resolves (research.js:329-344). Every setupCustomDropdown() ends with
        // hideDropdown() (custom_dropdown.js:629), so a click before that async
        // re-setup gets force-closed. navigateTo only waits for domcontentloaded,
        // so wait for the network to settle (the home page has no SSE/polling) to
        // guarantee the final setup has run before we open the dropdown.
        await page.waitForNetworkIdle({ idleTime: 500, timeout: 15000 }).catch(() => {});

        await page.click('#search_engine');
        await page.waitForFunction(() => {
            const list = document.querySelector('#search-engine-dropdown-list');
            return list && window.getComputedStyle(list).display === 'block'
                && list.classList.contains('ldr-dropdown-active');
        }, { timeout: 5000 }).catch(() => {});

        const result = await page.evaluate(() => {
            const list = document.querySelector('#search-engine-dropdown-list');
            if (!list) return { hasList: false };
            return {
                hasList: true,
                display: window.getComputedStyle(list).display,
                active: list.classList.contains('ldr-dropdown-active'),
                itemCount: list.querySelectorAll('.ldr-custom-dropdown-item, [role="option"], li').length,
            };
        });

        if (!result.hasList) {
            return { passed: false, message: '#search-engine-dropdown-list not found on /' };
        }
        const passed = result.display === 'block' && result.active;
        return {
            passed,
            message: passed
                ? `Search-engine dropdown opens on click (display=${result.display}, active=${result.active}, items=${result.itemCount})`
                : `Dropdown did not open on click (display=${result.display}, active=${result.active})`
        };
    },

    async dropdownSelection(page, baseUrl) {
        await navigateTo(page, `${baseUrl}/`);

        const result = await page.evaluate(() => {
            const dropdown = document.querySelector('.ldr-custom-dropdown');
            if (!dropdown) return { exists: false };

            const hiddenInput = dropdown.querySelector('input[type="hidden"]');
            const items = dropdown.querySelectorAll('.ldr-custom-dropdown-list li, [class*="dropdown-list"] li, .dropdown-item');

            return {
                exists: true,
                hasHiddenInput: !!hiddenInput,
                hiddenInputName: hiddenInput?.name,
                hiddenInputValue: hiddenInput?.value,
                itemCount: items.length
            };
        });

        if (!result.exists) {
            return { passed: null, skipped: true, message: 'No dropdown on this page' };
        }

        return {
            passed: result.hasHiddenInput,
            message: result.hasHiddenInput
                ? `Dropdown has hidden input (name: ${result.hiddenInputName}, value: ${result.hiddenInputValue})`
                : 'Dropdown missing hidden input for form submission'
        };
    }
};

// ============================================================================
// Modal Tests
// ============================================================================
const ModalTests = {
    async deleteModalStructure(page, baseUrl) {
        // #deleteConfirmModal is a static include in library.html (line 1140 →
        // components/delete_confirmation_modal.html), present-but-hidden. The old
        // test navigated to /history/ where it doesn't exist (→ permanent skip).
        // Assert the real modal structure on /library/.
        await navigateTo(page, `${baseUrl}/library/`);
        await page.waitForSelector('#deleteConfirmModal', { timeout: 15000 });

        const result = await page.evaluate(() => {
            const modal = document.querySelector('#deleteConfirmModal');
            if (!modal) return { exists: false };
            return {
                exists: true,
                hasTitle: !!modal.querySelector('#deleteConfirmModalLabel'),
                hasConfirm: !!modal.querySelector('#deleteConfirmBtn'),
                hasCancel: !!modal.querySelector('.btn-secondary[data-bs-dismiss="modal"]'),
            };
        });

        if (!result.exists) {
            return { passed: false, message: '#deleteConfirmModal not found on /library/' };
        }
        const passed = result.hasTitle && result.hasConfirm && result.hasCancel;
        return {
            passed,
            message: passed
                ? 'Delete modal has title + confirm (#deleteConfirmBtn) + cancel (data-bs-dismiss) buttons'
                : `Delete modal incomplete (title=${result.hasTitle}, confirm=${result.hasConfirm}, cancel=${result.hasCancel})`
        };
    },

    async modalZIndex(page, baseUrl) {
        // #deleteConfirmModal carries an inline z-index: 10000
        // (components/delete_confirmation_modal.html), included in library.html.
        // The old test navigated to /history/ (modal absent → skip). Assert the
        // z-index on /library/ so the modal sits above the app chrome. We check
        // BOTH an absolute floor and a relative comparison: the mobile bottom nav
        // is z-index 1500 (mobile-responsive.css), already above a bare 1000 floor,
        // so the relative check (modal > sidebar AND modal > nav) is what actually
        // guarantees the modal overlays the chrome. getComputedStyle is only called
        // on elements that exist (getComputedStyle(null) throws); absent chrome → 0.
        await navigateTo(page, `${baseUrl}/library/`);
        await page.waitForSelector('#deleteConfirmModal', { timeout: 15000 });

        const result = await page.evaluate(() => {
            const modal = document.querySelector('#deleteConfirmModal');
            if (!modal) return { exists: false };
            const zOf = (sel) => {
                const el = document.querySelector(sel);
                if (!el) return 0;
                const z = parseInt(window.getComputedStyle(el).zIndex, 10);
                return Number.isFinite(z) ? z : 0;
            };
            const z = parseInt(window.getComputedStyle(modal).zIndex, 10);
            return {
                exists: true,
                zIndex: Number.isFinite(z) ? z : 0,
                sidebarZ: zOf('.ldr-sidebar'),
                navZ: zOf('.ldr-mobile-bottom-nav'),
            };
        });

        if (!result.exists) {
            return { passed: false, message: '#deleteConfirmModal not found on /library/' };
        }
        const aboveChrome = result.zIndex > result.sidebarZ && result.zIndex > result.navZ;
        const passed = result.zIndex >= 1000 && aboveChrome;
        return {
            passed,
            message: passed
                ? `Delete modal z-index ${result.zIndex} >= 1000 and above chrome (sidebar=${result.sidebarZ}, nav=${result.navZ})`
                : `Delete modal z-index ${result.zIndex} fails (>=1000 && >sidebar(${result.sidebarZ}) && >nav(${result.navZ}))`
        };
    }
};

// ============================================================================
// Navigation Tests
// ============================================================================
const NavigationTests = {
    async sidebarLinksClickable(page, baseUrl) {
        await navigateTo(page, `${baseUrl}/`);

        const result = await page.evaluate(() => {
            const sidebar = document.querySelector('.ldr-sidebar');
            if (!sidebar) return { exists: false };

            const links = Array.from(sidebar.querySelectorAll('a[href]'));
            const clickableLinks = links.filter(link => {
                const rect = link.getBoundingClientRect();
                const style = window.getComputedStyle(link);
                return rect.width > 0 &&
                       rect.height > 0 &&
                       style.display !== 'none' &&
                       style.visibility !== 'hidden' &&
                       style.pointerEvents !== 'none';
            });

            return {
                exists: true,
                totalLinks: links.length,
                clickableLinks: clickableLinks.length
            };
        });

        if (!result.exists) {
            return { passed: null, skipped: true, message: 'No sidebar on this page' };
        }

        return {
            passed: result.clickableLinks > 0,
            message: result.clickableLinks > 0
                ? `${result.clickableLinks}/${result.totalLinks} sidebar links are clickable`
                : 'No clickable sidebar links found'
        };
    },

    async activePageHighlighted(page, baseUrl) {
        const pages = [
            { url: '/', name: 'Research' },
            { url: '/history/', name: 'History' },
            { url: '/settings/', name: 'Settings' }
        ];

        let passCount = 0;
        const details = [];

        for (const pageInfo of pages) {
            await navigateTo(page, `${baseUrl}${pageInfo.url}`);

            const hasActive = await page.evaluate(() => {
                const sidebar = document.querySelector('.ldr-sidebar');
                if (!sidebar) return { hasSidebar: false };

                const activeLinks = sidebar.querySelectorAll('.active, [class*="active"], [aria-current="page"]');
                return {
                    hasSidebar: true,
                    hasActiveClass: activeLinks.length > 0
                };
            }, pageInfo.url);

            if (hasActive.hasSidebar && hasActive.hasActiveClass) {
                passCount++;
                details.push(`${pageInfo.name}: OK`);
            } else {
                details.push(`${pageInfo.name}: MISSING`);
            }
        }

        // All pages should have active highlighting
        const allPassed = passCount === pages.length;
        return {
            passed: allPassed,
            message: `Active state highlighting: ${details.join(', ')}`
        };
    },

    async mobileNavTabs(page, baseUrl) {
        await page.setViewport({ width: 375, height: 667, isMobile: true });
        await navigateTo(page, `${baseUrl}/`);

        const result = await page.evaluate(() => {
            const mobileNav = document.querySelector('.ldr-mobile-bottom-nav');
            if (!mobileNav) return { exists: false };

            const tabs = mobileNav.querySelectorAll('.ldr-mobile-nav-tab, [class*="nav-tab"], button, a');
            const clickableTabs = Array.from(tabs).filter(tab => {
                const rect = tab.getBoundingClientRect();
                return rect.width > 0 && rect.height > 0;
            });

            return {
                exists: true,
                tabCount: clickableTabs.length,
                tabLabels: clickableTabs.slice(0, 5).map(t => t.textContent?.trim() || t.getAttribute('aria-label'))
            };
        });

        // Reset viewport
        await page.setViewport({ width: 1280, height: 800 });

        if (!result.exists) {
            return { passed: null, skipped: true, message: 'No mobile nav on desktop viewport' };
        }

        return {
            passed: result.tabCount >= 2,
            message: result.tabCount >= 2
                ? `Mobile nav has ${result.tabCount} tabs: ${result.tabLabels.join(', ')}`
                : `Mobile nav has insufficient tabs (${result.tabCount})`
        };
    }
};

// ============================================================================
// Scroll Tests
// ============================================================================
const ScrollTests = {
    async sidebarScrollable(page, baseUrl) {
        await page.setViewport({ width: 1280, height: 500 }); // Short viewport
        await navigateTo(page, `${baseUrl}/`);

        const result = await page.evaluate(() => {
            const sidebar = document.querySelector('.ldr-sidebar');
            if (!sidebar) return { exists: false };

            const nav = sidebar.querySelector('.ldr-sidebar-nav, nav, ul');
            if (!nav) return { exists: true, hasNav: false };

            const style = window.getComputedStyle(nav);
            const parentStyle = window.getComputedStyle(sidebar);

            return {
                exists: true,
                hasNav: true,
                navScrollHeight: nav.scrollHeight,
                navClientHeight: nav.clientHeight,
                isOverflowing: nav.scrollHeight > nav.clientHeight,
                overflowY: style.overflowY,
                parentOverflowY: parentStyle.overflowY
            };
        });

        // Reset viewport
        await page.setViewport({ width: 1280, height: 800 });

        if (!result.exists || !result.hasNav) {
            return { passed: null, skipped: true, message: 'No sidebar nav to test' };
        }

        const isScrollable = result.overflowY === 'auto' ||
                            result.overflowY === 'scroll' ||
                            result.parentOverflowY === 'auto' ||
                            result.parentOverflowY === 'scroll';

        return {
            passed: !result.isOverflowing || isScrollable,
            message: result.isOverflowing
                ? (isScrollable ? 'Sidebar nav is scrollable when content overflows' : 'Sidebar nav overflows but is not scrollable')
                : 'Sidebar nav fits without scrolling'
        };
    },

    async contentNotCutOff(page, baseUrl) {
        await navigateTo(page, `${baseUrl}/settings/`);

        const result = await page.evaluate(() => {
            const mainEl = document.querySelector('main, .main-content, .container');
            if (!mainEl) return { exists: false };

            const allElements = mainEl.querySelectorAll('*');
            let lastBottom = 0;

            allElements.forEach(el => {
                const rect = el.getBoundingClientRect();
                const style = window.getComputedStyle(el);
                if (rect.height > 0 && style.display !== 'none' && rect.bottom > lastBottom) {
                    lastBottom = rect.bottom;
                }
            });

            const viewportHeight = window.innerHeight;
            const bodyHeight = document.body.scrollHeight;
            const isScrollable = bodyHeight > viewportHeight;

            // Content is cut off if last element bottom exceeds viewport AND page is not scrollable
            const isCutOff = lastBottom > viewportHeight && !isScrollable;

            return {
                exists: true,
                lastBottom,
                viewportHeight,
                bodyHeight,
                isScrollable,
                isCutOff
            };
        });

        if (!result.exists) {
            return { passed: null, skipped: true, message: 'No main content found' };
        }

        return {
            passed: !result.isCutOff,
            message: result.isCutOff
                ? `Content is cut off at ${result.lastBottom}px (viewport: ${result.viewportHeight}px, not scrollable)`
                : result.isScrollable
                    ? `Page is scrollable (body: ${result.bodyHeight}px, viewport: ${result.viewportHeight}px)`
                    : `Page fits in viewport (last element at ${Math.round(result.lastBottom)}px)`
        };
    }
};

// ============================================================================
// Button State Tests
// ============================================================================
const ButtonTests = {
    async buttonStates(page, baseUrl) {
        await navigateTo(page, `${baseUrl}/`);

        const result = await page.evaluate(() => {
            const buttons = document.querySelectorAll('button, .btn, input[type="submit"]');
            if (buttons.length === 0) return { exists: false };

            const btn = buttons[0];
            const initialCursor = window.getComputedStyle(btn).cursor;

            return {
                exists: true,
                buttonCount: buttons.length,
                hasCursorPointer: initialCursor === 'pointer'
            };
        });

        if (!result.exists) {
            return { passed: false, message: 'No buttons found on page' };
        }

        return {
            passed: result.hasCursorPointer,
            message: result.hasCursorPointer
                ? `Buttons have pointer cursor (${result.buttonCount} buttons found)`
                : 'Buttons may lack proper cursor style'
        };
    },

    async disabledButtonStyle(page, baseUrl) {
        await navigateTo(page, `${baseUrl}/`);

        const result = await page.evaluate(() => {
            const disabledBtns = document.querySelectorAll('button[disabled], .btn[disabled], button.disabled, .btn.disabled');
            if (disabledBtns.length === 0) return { hasDisabled: false };

            const btn = disabledBtns[0];
            const style = window.getComputedStyle(btn);

            return {
                hasDisabled: true,
                opacity: style.opacity,
                cursor: style.cursor,
                pointerEvents: style.pointerEvents
            };
        });

        if (!result.hasDisabled) {
            return { passed: null, skipped: true, message: 'No disabled buttons to test' };
        }

        const isProperlyStyled = parseFloat(result.opacity) < 1 ||
                                 result.cursor === 'not-allowed' ||
                                 result.pointerEvents === 'none';

        return {
            passed: isProperlyStyled,
            message: isProperlyStyled
                ? `Disabled buttons styled correctly (opacity: ${result.opacity}, cursor: ${result.cursor})`
                : 'Disabled buttons may not be visually distinct'
        };
    }
};

// ============================================================================
// Main Test Runner
// ============================================================================
async function main() {
    log.section('UI Functionality Tests');

    const ctx = await setupTest({
        authenticate: true,
        viewport: { width: 1280, height: 800 }
    });

    const results = new TestResults('UI Functionality Tests');
    const { page } = ctx;
    const { baseUrl } = ctx.config;

    async function run(category, name, testFn) {
        try {
            const result = await withTimeout(
                testFn(page, baseUrl),
                30000,
                `${category}/${name}`
            );
            if (result.skipped) {
                results.skip(category, name, result.message);
            } else {
                results.add(category, name, result.passed, result.message);
            }
        } catch (error) {
            results.add(category, name, false, `Error: ${error.message}`);
        }
    }

    try {
        // Form Tests
        log.section('Form Tests');
        await run('Forms', 'Research Form Structure', (p, u) => FormTests.researchFormStructure(p, u));
        await run('Forms', 'Input Focus States', (p, u) => FormTests.inputFocusStates(p, u));
        await run('Forms', 'Settings Form Buttons', (p, u) => FormTests.settingsFormButtons(p, u));

        // Dropdown Tests
        log.section('Dropdown Tests');
        await run('Dropdowns', 'Dropdown Structure', (p, u) => DropdownTests.dropdownStructure(p, u));
        await run('Dropdowns', 'Dropdown Opens On Click', (p, u) => DropdownTests.dropdownOpensOnClick(p, u));
        await run('Dropdowns', 'Dropdown Selection', (p, u) => DropdownTests.dropdownSelection(p, u));

        // Modal Tests
        log.section('Modal Tests');
        await run('Modals', 'Delete Modal Structure', (p, u) => ModalTests.deleteModalStructure(p, u));
        await run('Modals', 'Modal Z-Index', (p, u) => ModalTests.modalZIndex(p, u));

        // Navigation Tests
        log.section('Navigation Tests');
        await run('Navigation', 'Sidebar Links Clickable', (p, u) => NavigationTests.sidebarLinksClickable(p, u));
        await run('Navigation', 'Active Page Highlighted', (p, u) => NavigationTests.activePageHighlighted(p, u));
        await run('Navigation', 'Mobile Nav Tabs', (p, u) => NavigationTests.mobileNavTabs(p, u));

        // Scroll Tests
        log.section('Scroll Tests');
        await run('Scroll', 'Sidebar Scrollable', (p, u) => ScrollTests.sidebarScrollable(p, u));
        await run('Scroll', 'Content Not Cut Off', (p, u) => ScrollTests.contentNotCutOff(p, u));

        // Button Tests
        log.section('Button Tests');
        await run('Buttons', 'Button States', (p, u) => ButtonTests.buttonStates(p, u));
        await run('Buttons', 'Disabled Button Style', (p, u) => ButtonTests.disabledButtonStyle(p, u));

    } catch (error) {
        log.error(`Fatal error: ${error.message}`);
        console.error(error.stack);
    } finally {
        results.print();
        results.save();
        await teardownTest(ctx);
        process.exit(results.exitCode());
    }
}

// Run if executed directly
if (require.main === module) {
    main().catch(error => {
        console.error('Test runner failed:', error);
        process.exit(1);
    });
}

module.exports = { FormTests, DropdownTests, ModalTests, NavigationTests, ScrollTests, ButtonTests };
