using Microsoft.Playwright; namespace OpenTheBox.Web.Tests; /// /// End-to-end tests that validate the web game is interactive and functional. /// These tests launch the Blazor WASM app and interact with the xterm.js terminal. /// [Collection("WebApp")] public class GameFlowTests { private readonly WebAppFixture _fixture; public GameFlowTests(WebAppFixture fixture) { _fixture = fixture; } private async Task<(IPage page, IBrowserContext context)> CreateFreshSessionAsync() { var context = await _fixture.Browser.NewContextAsync(new BrowserNewContextOptions { ViewportSize = new ViewportSize { Width = 1400, Height = 900 } }); var page = await context.NewPageAsync(); return (page, context); } /// /// Types a number and presses Enter for numbered selection prompts. /// private static async Task SelectNumberedOptionAsync(IPage page, int number) { await TerminalHelper.PressDigitAsync(page, number); await Task.Delay(200); await TerminalHelper.PressEnterAsync(page); } /// /// Navigates through language selection → main menu → name entry → game loop. /// Returns the page ready at the first action menu. /// private async Task StartNewGameAsync(IBrowserContext context, string playerName = "Tester") { var page = await context.NewPageAsync(); await page.GotoAsync(_fixture.BaseUrl, new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle }); await TerminalHelper.WaitForTerminalReadyAsync(page, timeoutMs: 90000); // Language selection → English await TerminalHelper.WaitForTerminalTextAsync(page, "Language", timeoutMs: 60000); await SelectNumberedOptionAsync(page, 1); await Task.Delay(500); // Main menu → New Game await TerminalHelper.WaitForTerminalTextAsync(page, "OPEN THE BOX", timeoutMs: 30000); await SelectNumberedOptionAsync(page, 1); await Task.Delay(500); // Name prompt await TerminalHelper.WaitForTerminalTextAsync(page, "name", timeoutMs: 15000); await TerminalHelper.TypeAsync(page, playerName); await TerminalHelper.PressEnterAsync(page); await Task.Delay(500); // Welcome message → press Enter await TerminalHelper.WaitForTerminalTextAsync(page, playerName, timeoutMs: 10000); await TerminalHelper.PressEnterAsync(page); await Task.Delay(500); // Wait for action menu await TerminalHelper.WaitForTerminalTextAsync(page, "Open a box", timeoutMs: 15000); return page; } /// /// Opens a box from the action menu (selects "Open a box" → selects box #1 → presses through results). /// private static async Task OpenBoxAsync(IPage page) { // Select "Open a box" — find which option number it is var content = await TerminalHelper.ReadTerminalAsync(page); // "Open a box" should be option 1 in the action menu await SelectNumberedOptionAsync(page, 1); await Task.Delay(500); // Select the first box in the list await SelectNumberedOptionAsync(page, 1); await Task.Delay(1000); // Press Enter through box opening results for (int i = 0; i < 3; i++) { await TerminalHelper.PressEnterAsync(page); await Task.Delay(300); } // Wait for the action menu to reappear await TerminalHelper.WaitForTerminalTextAsync(page, "Open a box", timeoutMs: 15000); } // ── Tests ────────────────────────────────────────────────────────────── [Fact] public async Task PageLoads_TerminalInitializes() { var (page, context) = await CreateFreshSessionAsync(); var consoleLogs = new List(); page.Console += (_, msg) => consoleLogs.Add($"[{msg.Type}] {msg.Text}"); page.PageError += (_, error) => consoleLogs.Add($"[PAGE ERROR] {error}"); try { await page.GotoAsync(_fixture.BaseUrl, new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle }); await TerminalHelper.WaitForTerminalReadyAsync(page, timeoutMs: 90000); var containerVisible = await page.EvaluateAsync(@"() => { const el = document.getElementById('terminal-container'); return el && el.classList.contains('active'); }"); Assert.True(containerVisible, "Terminal container should be active"); var loadingHidden = await page.EvaluateAsync(@"() => { const el = document.getElementById('loading'); return el && el.classList.contains('hidden'); }"); Assert.True(loadingHidden, "Loading screen should be hidden"); var cols = await page.EvaluateAsync("() => window.terminalInterop?.term?.cols ?? 0"); Assert.True(cols >= 120, $"Terminal should have at least 120 cols, got {cols}"); Assert.DoesNotContain(consoleLogs, l => l.StartsWith("[error]")); } finally { await context.CloseAsync(); } } [Fact] public async Task LanguageSelection_DisplaysOptions() { var (page, context) = await CreateFreshSessionAsync(); try { await page.GotoAsync(_fixture.BaseUrl, new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle }); await TerminalHelper.WaitForTerminalReadyAsync(page, timeoutMs: 90000); await TerminalHelper.WaitForTerminalTextAsync(page, "Language", timeoutMs: 60000); var content = await TerminalHelper.ReadTerminalAsync(page); Assert.Contains("English", content); Assert.Contains("Fran", content); } finally { await context.CloseAsync(); } } [Fact] public async Task FullFlow_SelectLanguage_EnterName_OpenFirstBox() { var (_, context) = await CreateFreshSessionAsync(); try { var page = await StartNewGameAsync(context); // Verify initial state var content = await TerminalHelper.ReadTerminalAsync(page); Console.WriteLine($"Initial game state:\n{content}"); Assert.Contains("Boxes Opened: 0", content); // Open the first box await OpenBoxAsync(page); // Verify box was opened var afterOpen = await TerminalHelper.ReadTerminalAsync(page); Console.WriteLine($"After first box:\n{afterOpen}"); Assert.Contains("Boxes Opened: 1", afterOpen); } finally { await context.CloseAsync(); } } [Fact] public async Task MultipleBoxes_ProgressionAndMetaUnlocks() { var (_, context) = await CreateFreshSessionAsync(); var consoleLogs = new List(); try { var page = await StartNewGameAsync(context); page.Console += (_, msg) => consoleLogs.Add($"[{msg.Type}] {msg.Text}"); // Open several boxes — should trigger meta unlocks (new UI features) // The pacing test says 1st meta UI unlock should happen before box ~50 int boxesOpened = 0; bool sawNewLabel = false; bool sawAdventure = false; for (int i = 0; i < 20; i++) { await OpenBoxAsync(page); boxesOpened++; var content = await TerminalHelper.ReadTerminalAsync(page); // Check for NEW labels (meta unlocks) or adventure option if (content.Contains("NEW", StringComparison.Ordinal)) sawNewLabel = true; if (content.Contains("adventure", StringComparison.OrdinalIgnoreCase)) sawAdventure = true; Console.WriteLine($"Box #{boxesOpened}: menu options visible. NEW={sawNewLabel}, Adventure={sawAdventure}"); // If we've seen both, no need to open more if (sawNewLabel && sawAdventure) break; } Console.WriteLine($"After {boxesOpened} boxes: NEW labels seen={sawNewLabel}, Adventure unlocked={sawAdventure}"); // Verify no browser errors from box opening var errors = consoleLogs.Where(l => l.StartsWith("[error]")).ToList(); foreach (var err in errors) Console.WriteLine($"Browser error: {err}"); Assert.Empty(errors); } finally { await context.CloseAsync(); } } [Fact] public async Task ExtendedPlay_NoWasmCrash() { var (_, context) = await CreateFreshSessionAsync(); var consoleLogs = new List(); try { var page = await StartNewGameAsync(context); page.Console += (_, msg) => consoleLogs.Add($"[{msg.Type}] {msg.Text}"); // Open 20+ boxes — should trigger meta unlocks, arrow navigation, adventures // This validates that the full game loop works in WASM without crashing for (int i = 0; i < 25; i++) { await OpenBoxAsync(page); // Check for fatal errors after each box bool hasFatalError = consoleLogs.Any(l => l.Contains("[OpenTheBox] Fatal error")); if (hasFatalError) { var errors = consoleLogs.Where(l => l.Contains("error", StringComparison.OrdinalIgnoreCase)); foreach (var err in errors) Console.WriteLine($"WASM error: {err}"); Assert.Fail($"Fatal error after box #{i + 1}"); } } var finalContent = await TerminalHelper.ReadTerminalAsync(page); Console.WriteLine($"After 25 boxes:\n{finalContent}"); // After 25 boxes, the game should still be running and show the action menu Assert.Contains("box", finalContent, StringComparison.OrdinalIgnoreCase); // Verify no WASM-specific errors bool hasMonitorError = consoleLogs.Any(l => l.Contains("Cannot wait on monitors")); Assert.False(hasMonitorError, "Should not get 'Cannot wait on monitors' error in WASM"); bool hasPlatformError = consoleLogs.Any(l => l.Contains("PlatformNotSupported")); Assert.False(hasPlatformError, "Should not get PlatformNotSupportedException in WASM"); Console.WriteLine($"25 boxes opened successfully. Browser console had {consoleLogs.Count} messages."); } finally { await context.CloseAsync(); } } [Fact] public async Task Adventure_NoWasmCrash() { var (_, context) = await CreateFreshSessionAsync(); var consoleLogs = new List(); try { var page = await StartNewGameAsync(context); page.Console += (_, msg) => consoleLogs.Add($"[{msg.Type}] {msg.Text}"); // Open boxes until adventure is unlocked bool adventureUnlocked = false; for (int i = 0; i < 30 && !adventureUnlocked; i++) { await OpenBoxAsync(page); var content = await TerminalHelper.ReadTerminalAsync(page); adventureUnlocked = content.Contains("adventure", StringComparison.OrdinalIgnoreCase); } if (!adventureUnlocked) { Console.WriteLine("Adventure not unlocked within 30 boxes — skipping"); return; } Console.WriteLine("Adventure unlocked, attempting to start..."); // Navigate to adventure: use arrow keys to find "adventure" option // Try each digit from 1-9 — the arrow selection responds to digit shortcuts for (int d = 1; d <= 9; d++) { await TerminalHelper.PressDigitAsync(page, d); await Task.Delay(1000); var content = await TerminalHelper.ReadTerminalAsync(page); // If we see the adventure list (contains "Back" and a theme name), we found it if (content.Contains("Back", StringComparison.OrdinalIgnoreCase) && !content.Contains("Open a box", StringComparison.OrdinalIgnoreCase)) { Console.WriteLine($"Adventure menu found with digit {d}"); break; } // If we accidentally selected something else, press Escape or go back if (!content.Contains("adventure", StringComparison.OrdinalIgnoreCase)) { // We're in a submenu we don't want — go back by selecting "Back" option // First check if this is a box selection or other menu if (content.Contains("Back")) { // Find the Back option number and press it var backLines = content.Split('\n'); foreach (var line in backLines) { if (line.Contains("Back")) { foreach (var ch in line) { if (char.IsDigit(ch)) { await TerminalHelper.PressDigitAsync(page, ch - '0'); await Task.Delay(500); break; } } break; } } } } } // Try to select the first adventure await TerminalHelper.PressDigitAsync(page, 1); await Task.Delay(2000); // Read the result var dialogueContent = await TerminalHelper.ReadTerminalAsync(page); Console.WriteLine($"After adventure selection:\n{dialogueContent}"); // The key assertion: no WASM-specific crash errors bool hasMonitorError = consoleLogs.Any(l => l.Contains("Cannot wait on monitors")); Assert.False(hasMonitorError, "Should not get 'Cannot wait on monitors' error in WASM"); bool hasFatalError = consoleLogs.Any(l => l.Contains("[OpenTheBox] Fatal error")); if (hasFatalError) { var errors = consoleLogs.Where(l => l.Contains("error", StringComparison.OrdinalIgnoreCase)); foreach (var err in errors) Console.WriteLine($"Browser error: {err}"); } Assert.False(hasFatalError, "Should not get fatal error during adventure"); // If we got this far with dialogue, press through a few lines for (int i = 0; i < 5; i++) { await TerminalHelper.PressEnterAsync(page); await Task.Delay(500); } Console.WriteLine("Adventure test completed without WASM crash"); } finally { await context.CloseAsync(); } } }