openthebox/tests/OpenTheBox.Web.Tests/GameFlowTests.cs

394 lines
15 KiB
C#
Raw Normal View History

using Microsoft.Playwright;
namespace OpenTheBox.Web.Tests;
/// <summary>
/// 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.
/// </summary>
[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);
}
/// <summary>
/// Types a number and presses Enter for numbered selection prompts.
/// </summary>
private static async Task SelectNumberedOptionAsync(IPage page, int number)
{
await TerminalHelper.PressDigitAsync(page, number);
await Task.Delay(200);
await TerminalHelper.PressEnterAsync(page);
}
/// <summary>
/// Navigates through language selection → main menu → name entry → game loop.
/// Returns the page ready at the first action menu.
/// </summary>
private async Task<IPage> 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;
}
/// <summary>
/// Opens a box from the action menu (selects "Open a box" → selects box #1 → presses through results).
/// </summary>
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<string>();
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<bool>(@"() => {
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<bool>(@"() => {
const el = document.getElementById('loading');
return el && el.classList.contains('hidden');
}");
Assert.True(loadingHidden, "Loading screen should be hidden");
var cols = await page.EvaluateAsync<int>("() => 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<string>();
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<string>();
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<string>();
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();
}
}
}