openthebox/tests/OpenTheBox.Web.Tests/TerminalHelper.cs
Samuel Bouchet 2c43e31605 Fix WASM compatibility issues and arrow selection rendering bug
- Fix arrow selection line count calculation (overcounted by 1 due to
  trailing newline, causing progressive line truncation on each redraw)
- Replace sync Loreline bridge with queue-based async pattern to avoid
  "Cannot wait on monitors" WASM deadlock
- Add bounded input buffer (8 keys, DropOldest) to prevent held-key
  accumulation
- Set Spectre.Console Profile.Height on all AnsiConsole.Create calls to
  prevent PlatformNotSupportedException on Console.WindowHeight
- Add explicit Loreline.dll reference + TrimmerRootAssembly for WASM
- Use MSBuild CopyGameContent target instead of Content/Link for static
  file serving in Blazor WASM
- Add WASM guards for file I/O in ContentRegistry, LocalizationManager,
  AdventureEngine
- Enforce min 120x30 terminal dimensions in xterm.js
- Add Playwright E2E tests (6 tests: page load, language selection,
  full flow, multi-box progression, extended play, adventure)
2026-03-16 18:21:31 +01:00

107 lines
3.7 KiB
C#

using Microsoft.Playwright;
namespace OpenTheBox.Web.Tests;
/// <summary>
/// Helpers for interacting with the xterm.js terminal in Playwright tests.
/// </summary>
public static class TerminalHelper
{
/// <summary>
/// Reads the visible text content from the xterm.js terminal buffer.
/// </summary>
public static async Task<string> ReadTerminalAsync(IPage page)
{
return await page.EvaluateAsync<string>(@"() => {
const term = window.terminalInterop?.term;
if (!term) return '';
const buffer = term.buffer.active;
let text = '';
for (let i = 0; i < buffer.length; i++) {
const line = buffer.getLine(i);
if (line) text += line.translateToString(true) + '\n';
}
return text;
}");
}
/// <summary>
/// Waits until the terminal contains the specified text, with timeout.
/// </summary>
public static async Task WaitForTerminalTextAsync(IPage page, string text, int timeoutMs = 30000)
{
var deadline = DateTime.UtcNow.AddMilliseconds(timeoutMs);
while (DateTime.UtcNow < deadline)
{
var content = await ReadTerminalAsync(page);
if (content.Contains(text, StringComparison.OrdinalIgnoreCase))
return;
await Task.Delay(500);
}
// Read one final time for the error message
var finalContent = await ReadTerminalAsync(page);
throw new TimeoutException(
$"Terminal did not contain \"{text}\" within {timeoutMs}ms.\n" +
$"Terminal content:\n{finalContent}");
}
/// <summary>
/// Types text into the terminal (sends keystrokes).
/// </summary>
public static async Task TypeAsync(IPage page, string text)
{
await page.Keyboard.TypeAsync(text, new KeyboardTypeOptions { Delay = 50 });
}
/// <summary>
/// Presses Enter in the terminal.
/// </summary>
public static async Task PressEnterAsync(IPage page)
{
await page.Keyboard.PressAsync("Enter");
}
/// <summary>
/// Presses a specific key (Arrow keys, Escape, etc.).
/// </summary>
public static async Task PressKeyAsync(IPage page, string key)
{
await page.Keyboard.PressAsync(key);
}
/// <summary>
/// Sends a digit key press (1-9) for numbered menu selection.
/// </summary>
public static async Task PressDigitAsync(IPage page, int digit)
{
await page.Keyboard.PressAsync($"Digit{digit}");
}
/// <summary>
/// Waits for the terminal to be initialized (xterm.js loaded and terminal visible).
/// </summary>
public static async Task WaitForTerminalReadyAsync(IPage page, int timeoutMs = 60000)
{
var deadline = DateTime.UtcNow.AddMilliseconds(timeoutMs);
while (DateTime.UtcNow < deadline)
{
var ready = await page.EvaluateAsync<bool>(@"() => {
return window.terminalInterop?.term != null;
}");
if (ready)
{
// Verify terminal has proper dimensions (should be 120x30 after init)
var cols = await page.EvaluateAsync<int>("() => window.terminalInterop.term.cols");
if (cols < 120)
{
// Force resize if fit addon miscalculated (e.g., headless browser)
await page.EvaluateAsync("() => window.terminalInterop.term.resize(120, 30)");
await Task.Delay(200);
}
return;
}
await Task.Delay(500);
}
throw new TimeoutException($"Terminal was not initialized within {timeoutMs}ms");
}
}