diff --git a/WARP.md b/WARP.md index 9259c8adf..f5ef9dc82 100644 --- a/WARP.md +++ b/WARP.md @@ -71,16 +71,16 @@ dotnet test --filter "ClassName" ### Docker Commands ```bash -# Start all services +# Start all services (API, Admin UI, SQL Server, Azurite) docker compose --profile all up -d -# Start only dependencies (for local API/Admin development) +# Start only dependencies (SQL Server + Azurite for local API/Admin development) docker compose --profile tools up -d -# Start only WebAPI +# Start only WebAPI (includes SQL Server + Azurite) docker compose --profile webapi up -d -# Start only Admin UI +# Start only Admin UI (includes SQL Server, Azurite, and WebAPI) docker compose --profile admin up -d # View logs diff --git a/docker-compose.yml b/docker-compose.yml index c989db259..33a517af3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -46,7 +46,7 @@ services: - SigningAuthority=https://app-ssw-ident-staging-api.azurewebsites.net/ volumes: - ${CERTS_PATH}:/https:ro - profiles: ["all", "webapi"] + profiles: ["all", "webapi", "admin"] # Admin UI rewards-adminui: diff --git a/src/AdminUI/Pages/KioskLeaderboard.razor b/src/AdminUI/Pages/KioskLeaderboard.razor index a64a0e2b3..166cc8a08 100644 --- a/src/AdminUI/Pages/KioskLeaderboard.razor +++ b/src/AdminUI/Pages/KioskLeaderboard.razor @@ -1,87 +1,12 @@ @layout KioskLayout @page "/kiosk-leaderboard" -@using SSW.Rewards.Shared.DTOs.Leaderboard -@using SSW.Rewards.ApiClient.Services -@using SSW.Rewards.Admin.UI.Components -@using SSW.Rewards.Enums - -@inject ILeaderboardService leaderboardService - -
+ + Use this app to scan SSW QR codes, earn SSW Points ⭐, claim rewards and win prizes! +
@@ -95,11 +20,11 @@
- + Rank Name - Total Points + Points @row.Rank @@ -113,98 +38,26 @@ }
- @row.Points + @row.Points.ToString("N0") - - - + +
+
+
@if (!_hasErrorsOnUpdate) {
- Last updated: @(_lastUpdated != null ? _lastUpdated?.ToString("dd MMMM yyyy HH:mm") : "never") + Last refreshed: @GetLastUpdatedText() (refreshing in @_secondsUntilRefresh seconds)
} else {
- ⚠️ We couldn't refresh the leaderboard. Retrying in 1 min.
- Last updated: @(_lastUpdated != null ? _lastUpdated?.ToString("dd MMMM yyyy HH:mm") : "never") + ⚠️ We couldn't refresh the leaderboard. Retrying in @_secondsUntilRefresh seconds.
+ Last refreshed: @GetLastUpdatedText()
}
- -@code { - private MudTable table; - private LeaderboardFilter _selectedFilter = LeaderboardFilter.ThisWeek; - private int _activeTabIndex = 0; - private readonly int[] _pageSizeOptions = new[] { 10, 25, 50, 100, 250 }; - private System.Timers.Timer? _refreshTimer; - private DateTime? _lastUpdated; - private bool _hasErrorsOnUpdate = false; - - private TableData _lastTableCache = new() { TotalItems = 0, Items = [] }; - - protected override async Task OnInitializedAsync() - { - await LoadLeaderboard(); - - _refreshTimer = new System.Timers.Timer(60000); // 1 minute - _refreshTimer.Elapsed += async (_, __) => await InvokeAsync(LoadLeaderboard); - _refreshTimer.AutoReset = true; - _refreshTimer.Start(); - } - - private async Task> ServerReload(TableState state) - { - try - { - var result = await leaderboardService.GetMobilePaginatedLeaderboard(state.Page, state.PageSize, _selectedFilter, CancellationToken.None); - _lastUpdated = DateTime.Now; - _hasErrorsOnUpdate = false; - _lastTableCache = new() { TotalItems = result.Count, Items = result.Items }; - } - catch (Exception ex) - { - _hasErrorsOnUpdate = true; - Console.WriteLine($"Error in ServerReload: {ex.Message}"); - } - StateHasChanged(); - return _lastTableCache; - } - - private async Task LoadLeaderboard() - { - if (table != null) - { - await table.ReloadServerData(); - } - } - - private async Task OnTabChanged(int tabIndex) - { - _activeTabIndex = tabIndex; - _selectedFilter = tabIndex switch - { - 0 => LeaderboardFilter.ThisWeek, - 1 => LeaderboardFilter.ThisMonth, - 2 => LeaderboardFilter.ThisYear, - 3 => LeaderboardFilter.Forever, - _ => LeaderboardFilter.ThisWeek - }; - - await LoadLeaderboard(); - } - - public void Dispose() - { - if (_refreshTimer != null) - { - _refreshTimer.Stop(); - _refreshTimer.Dispose(); - _refreshTimer = null; - } - } -} diff --git a/src/AdminUI/Pages/KioskLeaderboard.razor.cs b/src/AdminUI/Pages/KioskLeaderboard.razor.cs new file mode 100644 index 000000000..a3ee31834 --- /dev/null +++ b/src/AdminUI/Pages/KioskLeaderboard.razor.cs @@ -0,0 +1,222 @@ +using Microsoft.AspNetCore.Components; +using MudBlazor; +using SSW.Rewards.ApiClient.Services; +using SSW.Rewards.Enums; +using SSW.Rewards.Shared.DTOs.Leaderboard; + +namespace SSW.Rewards.Admin.UI.Pages; + +public partial class KioskLeaderboard : IDisposable +{ + private const int RefreshIntervalSeconds = 60; + private const int ScrollIntervalSeconds = 10; + + [Inject] private ILeaderboardService leaderboardService { get; set; } = default!; + + private MudTable table = default!; + private LeaderboardFilter _selectedFilter = LeaderboardFilter.ThisWeek; + private int _activeTabIndex = 0; + private readonly int[] _pageSizeOptions = [30, 50, 100]; + private int _defaultPageSize = 30; + private System.Timers.Timer? _refreshTimer; + private System.Timers.Timer? _countdownTimer; + private System.Timers.Timer? _scrollTimer; + private System.Timers.Timer? _progressTimer; + private DateTime? _lastUpdated; + private bool _hasErrorsOnUpdate = false; + private int _secondsUntilRefresh = RefreshIntervalSeconds; + private int _currentPage = 0; + private int _totalPages = 0; + private double _progressBarWidth = 0; + private DateTime _lastPageChange = DateTime.Now; + + private TableData _lastTableCache = new() { TotalItems = 0, Items = [] }; + + protected override async Task OnInitializedAsync() + { + await LoadLeaderboard(); + + // Refresh data (default every 60 seconds) + _refreshTimer = new System.Timers.Timer(RefreshIntervalSeconds * 1000); + _refreshTimer.Elapsed += async (_, __) => await InvokeAsync(async () => + { + _secondsUntilRefresh = RefreshIntervalSeconds; + await LoadLeaderboard(); + }); + _refreshTimer.AutoReset = true; + _refreshTimer.Start(); + + // Update countdown every second + _countdownTimer = new System.Timers.Timer(1000); // 1 second + _countdownTimer.Elapsed += async (_, __) => await InvokeAsync(() => + { + if (_secondsUntilRefresh > 0) + _secondsUntilRefresh--; + StateHasChanged(); + }); + _countdownTimer.AutoReset = true; + _countdownTimer.Start(); + + // Auto-scroll through pages (default every 10 seconds) + _scrollTimer = new System.Timers.Timer(ScrollIntervalSeconds * 1000); + _scrollTimer.Elapsed += async (_, __) => await InvokeAsync(() => + { + // Only scroll if there's more than one page and we didn't just refresh. + if (_totalPages > 1 && RefreshIntervalSeconds - _secondsUntilRefresh >= ScrollIntervalSeconds) + { + _currentPage = (_currentPage + 1) % _totalPages; + _lastPageChange = DateTime.Now; // Reset the timer for progress animation + if (table != null) + { + table.NavigateTo(_currentPage); + } + } + }); + _scrollTimer.AutoReset = true; + _scrollTimer.Start(); + + // Update progress bar animation (every 100ms for smooth animation) + _progressTimer = new System.Timers.Timer(100); + _progressTimer.Elapsed += async (_, __) => await InvokeAsync(() => + { + UpdateProgressBar(); + }); + _progressTimer.AutoReset = true; + _progressTimer.Start(); + } + + private void UpdateProgressBar() + { + if (_totalPages <= 1) + { + _progressBarWidth = 100; // Full width if only one page + StateHasChanged(); + return; + } + + // Calculate time elapsed since last page change + var elapsedSeconds = (DateTime.Now - _lastPageChange).TotalSeconds; + + // Calculate progress within current page (0-100% over ScrollIntervalSeconds) + var pageProgress = Math.Min(100, (elapsedSeconds / (double)ScrollIntervalSeconds) * 100.0); + + // Calculate base progress from completed pages + var baseProgress = ((double)_currentPage / _totalPages) * 100; + + // Calculate progress increment for current page + var pageIncrement = (100.0 / _totalPages) * (pageProgress / 100); + + _progressBarWidth = 100 - pageProgress; + + StateHasChanged(); + } + + private async Task> ServerReload(TableState state) + { + try + { + var result = await leaderboardService.GetMobilePaginatedLeaderboard(state.Page, _defaultPageSize, _selectedFilter, CancellationToken.None); + _lastUpdated = DateTime.Now; + _hasErrorsOnUpdate = false; + + // Calculate total pages based on users with points only + var usersWithPointsOnPage = result.Items.Count(u => u.Points > 0); + + // If this page has users with 0 points or fewer users than page size, we've found the boundary + if (usersWithPointsOnPage < _defaultPageSize || usersWithPointsOnPage < result.Items.Count()) + { + // Calculate exact total: all previous pages (full) + users with points on this page + var totalUsersWithPoints = (state.Page * _defaultPageSize) + usersWithPointsOnPage; + _totalPages = Math.Max(1, (int)Math.Ceiling((double)totalUsersWithPoints / _defaultPageSize)); + } + else if (usersWithPointsOnPage == _defaultPageSize && result.Items.Count() == _defaultPageSize) + { + // This page is full of users with points, there might be more pages + // We'll know the exact count when we hit a page with 0-point users or incomplete page + // For now, assume at least one more page exists + _totalPages = state.Page + 2; // Current page + at least one more + } + else + { + // Fallback: at least the current page + _totalPages = Math.Max(1, state.Page + 1); + } + + _lastTableCache = new() { TotalItems = result.Count, Items = result.Items }; + } + catch (Exception ex) + { + _hasErrorsOnUpdate = true; + Console.WriteLine($"Error in ServerReload: {ex.Message}"); + } + + // Reset progress bar when data reloads + _lastPageChange = DateTime.Now; + StateHasChanged(); + return _lastTableCache; + } + + private async Task LoadLeaderboard() + { + if (table != null) + { + if (_currentPage != 0) + { + _currentPage = 0; + table.NavigateTo(_currentPage); + } + + await table.ReloadServerData(); + } + } + + private async Task OnTabChanged(int tabIndex) + { + _activeTabIndex = tabIndex; + _selectedFilter = tabIndex switch + { + 0 => LeaderboardFilter.ThisWeek, + 1 => LeaderboardFilter.ThisMonth, + 2 => LeaderboardFilter.ThisYear, + 3 => LeaderboardFilter.Forever, + _ => LeaderboardFilter.ThisWeek + }; + + await LoadLeaderboard(); + } + + private string GetLastUpdatedText() + => _lastUpdated?.ToString("dd MMMM yyyy HH:mm") + ?? "never"; + + public void Dispose() + { + if (_refreshTimer != null) + { + _refreshTimer.Stop(); + _refreshTimer.Dispose(); + _refreshTimer = null; + } + + if (_countdownTimer != null) + { + _countdownTimer.Stop(); + _countdownTimer.Dispose(); + _countdownTimer = null; + } + + if (_scrollTimer != null) + { + _scrollTimer.Stop(); + _scrollTimer.Dispose(); + _scrollTimer = null; + } + + if (_progressTimer != null) + { + _progressTimer.Stop(); + _progressTimer.Dispose(); + _progressTimer = null; + } + } +} diff --git a/src/AdminUI/Pages/KioskLeaderboard.razor.css b/src/AdminUI/Pages/KioskLeaderboard.razor.css new file mode 100644 index 000000000..67bcd6955 --- /dev/null +++ b/src/AdminUI/Pages/KioskLeaderboard.razor.css @@ -0,0 +1,111 @@ +::deep .kiosk-tabs .mud-tabs-toolbar { + justify-content: center; + background: #222; + border-radius: 12px; + padding: 0.3rem; + gap: 1.2rem; +} + +::deep .kiosk-tabs .mud-tabs-toolbar .mud-tabs-toolbar-inner { + min-height: 2.5rem; +} + +::deep .kiosk-tabs .mud-tab { + min-height: 2.5rem; + min-width: 80px !important; + padding: 0.4rem 1.2rem !important; + font-size: 1rem; + color: #fff !important; + background: #222 !important; + border-radius: 10px !important; + margin: 0 0.15rem; + transition: background 0.2s, color 0.2s; +} + +::deep .kiosk-tabs .mud-tab.mud-tab-active { + background: #CC4141 !important; + color: #fff !important; + border-radius: 10px !important; +} + +::deep .kiosk-tabs .mud-tabs-indicator, +::deep .kiosk-tabs .mud-tab-active .mud-tab-indicator, +::deep .kiosk-tabs .mud-tab-slider { + display: none !important; + height: 0 !important; +} + +::deep .kiosk-tabs .mud-tabs-scroll-button { + display: none !important; +} + +::deep .kiosk-tabs .mud-tooltip-root { + width: 100%; +} + +::deep .kiosk-tabs .mud-tabs-toolbar-content .mud-tabs-toolbar-wrapper { + width: 100%; + transform: translateX(-2px) !important; +} + +::deep .kiosk-leaderboard-table .mud-table { + font-size: 2.2rem; +} + +::deep .kiosk-leaderboard-table .mud-table-head .mud-table-cell { + background-color: #525252 !important; +} + +::deep .kiosk-leaderboard-table .mud-table-row:nth-child(2n) { + background-color: #333; +} + +::deep .kiosk-leaderboard-table .mud-table-row:nth-child(2n+1) { + background-color: #222; +} + +::deep .kiosk-leaderboard-table .mud-table-cell { + font-size: 1rem; + padding: 0.7rem; +} + +.page-progress-container { + position: relative; + width: 100%; + height: 4px; + background: #333; + border-radius: 0 0 16px 16px; + overflow: hidden; + margin-top: -1px; +} + +.page-progress-bar { + position: absolute; + left: 0; + top: 0; + height: 100%; + background: linear-gradient(90deg, #CC4141 0%, #ff6b6b 100%); + border-radius: 0 0 16px 0; + transition: width 0.1s linear; + box-shadow: 0 0 10px rgba(204, 65, 65, 0.5); +} + +.page-progress-bar::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent); + animation: shimmer 2s infinite; +} + +@keyframes shimmer { + 0% { + transform: translateX(-100%); + } + 100% { + transform: translateX(100%); + } +} diff --git a/tools/ui-tests/tests/kiosk-leaderboard.spec.ts b/tools/ui-tests/tests/kiosk-leaderboard.spec.ts new file mode 100644 index 000000000..44617f4ac --- /dev/null +++ b/tools/ui-tests/tests/kiosk-leaderboard.spec.ts @@ -0,0 +1,318 @@ +import { test, expect } from '@playwright/test'; + +test.use({ storageState: '.auth/user.json' }); + +/** + * Custom viewports for kiosk leaderboard testing + * - Portrait TV: 1080x1920 (vertical display) + * - Landscape FHD Laptop: 1920x1080 (standard Full HD) + * - Landscape iPad Pro: 1366x1024 (12.9" iPad Pro in landscape) + * - Portrait iPhone 16 Pro: 393x852 (iPhone 16 Pro dimensions) + */ +const KIOSK_VIEWPORTS = { + portraitTV: { name: 'Portrait TV', width: 1080, height: 1920 }, + landscapeFHD: { name: 'Landscape FHD Laptop', width: 1920, height: 1080 }, + landscapeIPadPro: { name: 'Landscape iPad Pro', width: 1366, height: 1024 }, + portraitIPhone16Pro: { name: 'Portrait iPhone 16 Pro', width: 393, height: 852 } +}; + +/** + * Helper function to take screenshots across all kiosk viewports + */ +async function takeKioskScreenshots( + page: any, + testName: string, + options: { + fullPage?: boolean; + waitForNetwork?: boolean; + } = {} +) { + const { fullPage = true, waitForNetwork = true } = options; + + for (const [key, viewport] of Object.entries(KIOSK_VIEWPORTS)) { + console.log(`\n📐 Setting viewport: ${viewport.name} (${viewport.width}x${viewport.height})`); + + // Set viewport + await page.setViewportSize({ width: viewport.width, height: viewport.height }); + + // Wait for network idle if requested + if (waitForNetwork) { + await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => { + console.log(`⚠️ Network idle timeout for ${viewport.name} - continuing anyway`); + }); + } + + // Additional wait for page to settle and animations to complete + await page.waitForTimeout(1000); + + const filename = `${key}-${viewport.width}x${viewport.height}-${testName}.png`; + const fullPath = `screenshots/kiosk-leaderboard/${filename}`; + + await page.screenshot({ + path: fullPath, + fullPage + }); + + console.log(`📸 Screenshot saved: ${fullPath}`); + } +} + +test.describe('Kiosk Leaderboard - Visual Tests', () => { + + test('kiosk leaderboard - default view with data', async ({ page }) => { + console.log('\n🏆 Test: Kiosk Leaderboard Default View\n'); + console.log('='.repeat(70)); + + await page.goto('https://localhost:7137/kiosk-leaderboard'); + + // Wait for the leaderboard table to load + await page.waitForSelector('.kiosk-leaderboard-table', { timeout: 15000 }); + console.log('✅ Kiosk leaderboard table loaded'); + + // Wait for data to populate + await page.waitForSelector('.mud-table-row', { timeout: 10000 }); + + // Count rows + const rows = await page.locator('.mud-table-row').count(); + console.log(`📊 Leaderboard showing ${rows} user rows`); + + // Verify logo is present + const logo = page.locator('img[src="/images/ssw-rewards-logo.svg"]'); + const logoVisible = await logo.isVisible().catch(() => false); + console.log(`✅ SSW Rewards logo visible: ${logoVisible ? '✅' : '❌'}`); + + // Verify tabs are present + const tabs = page.locator('.kiosk-tabs .mud-tab'); + const tabCount = await tabs.count(); + console.log(`📑 Found ${tabCount} tab(s)`); + + // Verify table columns + const headers = await page.locator('.mud-table-head .mud-table-cell').allTextContents(); + console.log('📋 Table columns:', headers); + + // Take screenshots across all viewports + await takeKioskScreenshots(page, 'default-view'); + + console.log('\n✅ Kiosk leaderboard default view test complete'); + console.log('='.repeat(70)); + }); + + test('kiosk leaderboard - scrolled view', async ({ page }) => { + console.log('\n🏆 Test: Kiosk Leaderboard Scrolled View\n'); + console.log('='.repeat(70)); + + await page.goto('https://localhost:7137/kiosk-leaderboard'); + await page.waitForSelector('.kiosk-leaderboard-table', { timeout: 15000 }); + await page.waitForSelector('.mud-table-row', { timeout: 10000 }); + + console.log('✅ Kiosk leaderboard loaded'); + + // Scroll down to see more entries (if pagination exists) + const tableContainer = page.locator('.mud-table-container').first(); + + // Check if scrollable + const isScrollable = await tableContainer.evaluate((el) => { + return el.scrollHeight > el.clientHeight; + }); + + if (isScrollable) { + await tableContainer.evaluate((el) => { + el.scrollTop = el.scrollHeight / 2; // Scroll halfway + }); + console.log('📜 Scrolled table to middle position'); + await page.waitForTimeout(500); + } else { + console.log('ℹ️ Table not scrollable (all data fits on screen)'); + } + + // Take screenshots + await takeKioskScreenshots(page, 'scrolled-view'); + + console.log('\n✅ Kiosk leaderboard scrolled view test complete'); + console.log('='.repeat(70)); + }); + + test('kiosk leaderboard - layout responsiveness', async ({ page }) => { + console.log('\n🏆 Test: Kiosk Leaderboard Layout Responsiveness\n'); + console.log('='.repeat(70)); + + await page.goto('https://localhost:7137/kiosk-leaderboard'); + await page.waitForSelector('.kiosk-leaderboard-table', { timeout: 15000 }); + await page.waitForSelector('.mud-table-row', { timeout: 10000 }); + + console.log('✅ Kiosk leaderboard loaded'); + + // Check layout elements at each viewport + for (const [key, viewport] of Object.entries(KIOSK_VIEWPORTS)) { + console.log(`\n📐 Testing ${viewport.name} (${viewport.width}x${viewport.height})`); + + await page.setViewportSize({ width: viewport.width, height: viewport.height }); + await page.waitForTimeout(500); + + // Verify logo is visible + const logoVisible = await page.locator('img[src="/images/ssw-rewards-logo.svg"]').isVisible(); + console.log(` �️ Logo visible: ${logoVisible ? '✅' : '❌'}`); + + // Verify table is visible + const tableVisible = await page.locator('.kiosk-leaderboard-table').isVisible(); + console.log(` 📊 Table visible: ${tableVisible ? '✅' : '❌'}`); + + // Check if header is present + const headerVisible = await page.locator('.mud-table-head').isVisible(); + console.log(` 📋 Header visible: ${headerVisible ? '✅' : '❌'}`); + + // Count visible rows + const visibleRows = await page.locator('.mud-table-row:visible').count(); + console.log(` 👥 Visible rows: ${visibleRows}`); + + // Get table dimensions + const tableBounds = await page.locator('.kiosk-leaderboard-table').boundingBox(); + if (tableBounds) { + console.log(` 📏 Table size: ${Math.round(tableBounds.width)}x${Math.round(tableBounds.height)}px`); + } + } + + // Take screenshots showing responsiveness + await takeKioskScreenshots(page, 'responsiveness'); + + console.log('\n✅ Kiosk leaderboard responsiveness test complete'); + console.log('='.repeat(70)); + }); + + test('kiosk leaderboard - dark theme styling', async ({ page }) => { + console.log('\n🏆 Test: Kiosk Leaderboard Dark Theme\n'); + console.log('='.repeat(70)); + + await page.goto('https://localhost:7137/kiosk-leaderboard'); + await page.waitForSelector('.kiosk-leaderboard-table', { timeout: 15000 }); + await page.waitForSelector('.mud-table-row', { timeout: 10000 }); + + console.log('✅ Kiosk leaderboard loaded'); + + // Verify dark theme styling + const kioskPaper = page.locator('.kiosk-leaderboard'); + + // Get computed styles + const bgColor = await kioskPaper.evaluate((el) => { + return window.getComputedStyle(el).backgroundColor; + }); + + const textColor = await kioskPaper.evaluate((el) => { + return window.getComputedStyle(el).color; + }); + + console.log('🎨 Theme colors:'); + console.log(` Background: ${bgColor}`); + console.log(` Text: ${textColor}`); + + // Verify table exists and get basic info + const tableExists = await page.locator('.kiosk-leaderboard-table').isVisible(); + console.log(`📊 Table visible: ${tableExists}`); + + if (tableExists) { + const tableBg = await page.locator('.kiosk-leaderboard-table').evaluate((el) => { + const styles = window.getComputedStyle(el); + return styles.backgroundColor; + }).catch(() => 'Unable to retrieve'); + + console.log(` Table background: ${tableBg}`); + } + + // Take screenshots + await takeKioskScreenshots(page, 'dark-theme'); + + console.log('\n✅ Kiosk leaderboard dark theme test complete'); + console.log('='.repeat(70)); + }); + + test('kiosk leaderboard - full page comparison', async ({ page }) => { + console.log('\n🏆 Test: Kiosk Leaderboard Full Page Comparison\n'); + console.log('='.repeat(70)); + + await page.goto('https://localhost:7137/kiosk-leaderboard'); + await page.waitForSelector('.kiosk-leaderboard-table', { timeout: 15000 }); + await page.waitForSelector('.mud-table-row', { timeout: 10000 }); + + console.log('✅ Kiosk leaderboard loaded'); + + // Wait for any animations + await page.waitForTimeout(1500); + + // Get overall page metrics + console.log('\n📊 Page Metrics:'); + + const metrics = await page.evaluate(() => { + return { + totalRows: document.querySelectorAll('.mud-table-row').length, + hasScrollbar: document.documentElement.scrollHeight > window.innerHeight, + viewportHeight: window.innerHeight, + documentHeight: document.documentElement.scrollHeight + }; + }); + + console.log(` Total rows: ${metrics.totalRows}`); + console.log(` Has vertical scrollbar: ${metrics.hasScrollbar}`); + console.log(` Viewport height: ${metrics.viewportHeight}px`); + console.log(` Document height: ${metrics.documentHeight}px`); + + // Take full page screenshots + await takeKioskScreenshots(page, 'full-page', { fullPage: true }); + + console.log('\n✅ Kiosk leaderboard full page comparison complete'); + console.log('='.repeat(70)); + }); + + test('kiosk leaderboard - portrait orientation page size', async ({ page }) => { + console.log('\n🏆 Test: Kiosk Leaderboard Portrait Page Size\n'); + console.log('='.repeat(70)); + + // Test Portrait TV (height > width) + console.log('\n📱 Testing Portrait TV (1080×1920)'); + await page.setViewportSize({ width: 1080, height: 1920 }); + await page.goto('https://localhost:7137/kiosk-leaderboard'); + await page.waitForSelector('.kiosk-leaderboard-table', { timeout: 15000 }); + await page.waitForSelector('.mud-table-row', { timeout: 15000 }); + await page.waitForTimeout(1500); + + let rows = await page.locator('.mud-table-row').count(); + console.log(` Rows displayed: ${rows}`); + console.log(` Expected: ≤25 (portrait page size)`); + console.log(` ${rows <= 25 ? '✅' : '❌'} Portrait page size verified`); + + // Test Portrait iPhone 16 Pro (height > width) + console.log('\n📱 Testing Portrait iPhone 16 Pro (393×852)'); + await page.setViewportSize({ width: 393, height: 852 }); + await page.waitForTimeout(2000); // Wait for page to adjust + + rows = await page.locator('.mud-table-row').count(); + console.log(` Rows displayed: ${rows}`); + console.log(` Expected: ≤25 (portrait page size)`); + console.log(` ${rows <= 25 ? '✅' : '❌'} Portrait page size verified`); + + // Test Landscape FHD (width > height) + console.log('\n🖥️ Testing Landscape FHD Laptop (1920×1080)'); + await page.setViewportSize({ width: 1920, height: 1080 }); + await page.goto('https://localhost:7137/kiosk-leaderboard'); + await page.waitForSelector('.kiosk-leaderboard-table', { timeout: 15000 }); + await page.waitForSelector('.mud-table-row', { timeout: 15000 }); + await page.waitForTimeout(1500); + + rows = await page.locator('.mud-table-row').count(); + console.log(` Rows displayed: ${rows}`); + console.log(` Expected: ≤30 (landscape page size)`); + console.log(` ${rows <= 30 ? '✅' : '❌'} Landscape page size verified`); + + // Test Landscape iPad Pro (width > height) + console.log('\n💻 Testing Landscape iPad Pro (1366×1024)'); + await page.setViewportSize({ width: 1366, height: 1024 }); + await page.waitForTimeout(2000); // Wait for page to adjust + + rows = await page.locator('.mud-table-row').count(); + console.log(` Rows displayed: ${rows}`); + console.log(` Expected: ≤30 (landscape page size)`); + console.log(` ${rows <= 30 ? '✅' : '❌'} Landscape page size verified`); + + console.log('\n✅ Portrait orientation page size test complete'); + console.log('='.repeat(70)); + }); +});