394 lines
15 KiB
C#
394 lines
15 KiB
C#
|
|
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();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|