File: Integration\Playwright\Infrastructure\TestTerminalConnectionResolver.cs
Web Access
Project: src\tests\Aspire.Dashboard.Tests\Aspire.Dashboard.Tests.csproj (Aspire.Dashboard.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Concurrent;
using System.Text;
using System.Threading.Channels;
using Aspire.Dashboard.Terminal;
using Aspire.Dashboard.Tests.Shared;
using Hex1b;
using Hex1b.Input;
using Xunit;
 
namespace Aspire.Dashboard.Tests.Integration.Playwright.Infrastructure;
 
internal sealed class TestTerminalConnectionResolver : ITerminalConnectionResolver, IAsyncDisposable
{
    private readonly ConcurrentDictionary<(string ResourceName, int ReplicaIndex), Lazy<TestTerminalConnection>> _producers = new();
    private readonly Channel<TestTerminalConnection> _connections = Channel.CreateUnbounded<TestTerminalConnection>();
 
    public async Task<Stream?> ConnectAsync(string resourceName, int replicaIndex, CancellationToken cancellationToken)
    {
        // All viewers of a replica share its real producer, including a separate primary
        // peer. HMP handshakes and HWT projection remain owned by Hex1b, not this fixture.
        var producer = _producers.GetOrAdd((resourceName, replicaIndex), _ => new(() => new TestTerminalConnection())).Value;
        var stream = producer.Connect();
        await _connections.Writer.WriteAsync(producer, cancellationToken);
        return stream;
    }
 
    public Task<TestTerminalConnection> AcceptConnectionAsync(CancellationToken cancellationToken) =>
        _connections.Reader.ReadAsync(cancellationToken).AsTask();
 
    public async Task DiscardPendingConnectionsAsync()
    {
        foreach (var producer in _producers.Values)
        {
            if (producer.IsValueCreated)
            {
                await producer.Value.DisposeAsync();
            }
        }
        _producers.Clear();
        while (_connections.Reader.TryRead(out _))
        {
        }
    }
 
    public ValueTask DisposeAsync() => new(DiscardPendingConnectionsAsync());
}
 
internal sealed class TestTerminalConnection : IAsyncDisposable
{
    public const int Columns = 137;
    public const int Rows = 41;
    private readonly TerminalTestProducer _producer = new(Columns, Rows, 100);
 
    public Hex1bAppWorkloadAdapter Workload => _producer.Workload;
    public Hmp1PresentationAdapter Presentation => _producer.Presentation;
    public int ConnectionCount => _producer.ConnectionCount;
 
    public Stream Connect() => _producer.Connect();
 
    public Task WaitForPeerHandshakesAsync(CancellationToken cancellationToken) =>
        _producer.WaitForPeerHandshakesAsync(cancellationToken);
 
    public Task WaitForProducerTextAsync(string text, CancellationToken cancellationToken) =>
        _producer.WaitForProducerTextAsync(text, cancellationToken);
 
    public async Task<string> ReadInputTextAsync(int length, CancellationToken cancellationToken)
    {
        var text = new StringBuilder();
        while (text.Length < length)
        {
            if (await Workload.InputEvents.ReadAsync(cancellationToken) is Hex1bKeyEvent key)
            {
                // Non-text keys such as an accidentally forwarded F6 must not disappear
                // from the assertion just because their Text property is empty.
                Assert.False(string.IsNullOrEmpty(key.Text), $"Unexpected terminal key: {key}");
                text.Append(key.Text);
            }
        }
        return text.ToString();
    }
 
    public ValueTask DisposeAsync() => _producer.DisposeAsync();
}