Public Access
142 lines
6.8 KiB
C#
142 lines
6.8 KiB
C#
using System.Globalization;
|
|
|
|
using Jint;
|
|
|
|
namespace DodoSSH.Client.Terminal.Tests;
|
|
|
|
/// <summary>
|
|
/// One load of <c>terminal.js</c>, in a fake browser, for one test.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <b>The page's own source is what runs.</b> Not a transcription of its logic into C# — that would test a
|
|
/// copy, and the copy would be the thing that stayed correct. The file is read from the shell project and
|
|
/// evaluated as it ships, so a change to the page that breaks the reconnection rules fails here.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>Jint rather than node, and that is a deliberate trade.</b> A node script would be the obvious way to
|
|
/// run JavaScript and would need node on every machine and in every CI job that runs the suite — so it
|
|
/// would be a second test command, run separately, and the first thing to be forgotten. This runs inside
|
|
/// <c>dotnet test</c> with everything else. What it costs is that the engine is not the engine the page
|
|
/// actually runs in: Jint is not Chromium, so this can prove the page's own logic and can prove nothing
|
|
/// about how WebView2 or Android's WebView behave. That boundary is exactly where
|
|
/// <c>docs/manual-checks.md</c> picks up — see 1.10 and 11.12a.
|
|
/// </para>
|
|
/// <para>
|
|
/// A fresh engine per test, because the page is a script with module-level state and there is no unloading
|
|
/// it: two tests sharing one engine would share a socket list, a clock and a backoff.
|
|
/// </para>
|
|
/// </remarks>
|
|
internal sealed class RendererPage
|
|
{
|
|
private readonly Engine engine;
|
|
|
|
private RendererPage(Engine engine) => this.engine = engine;
|
|
|
|
/// <summary>How many sockets the page has opened since it loaded.</summary>
|
|
/// <remarks>
|
|
/// The measurement nearly every test here turns on, and it is deliberately a count of *attempts*
|
|
/// rather than of live sockets: the failures being guarded against are a page that stops trying and a
|
|
/// page that never stops, and both are counted rather than observed.
|
|
/// </remarks>
|
|
public int Attempts => (int)engine.Evaluate("attempts()").AsNumber();
|
|
|
|
/// <summary>What the status element says — the banner a user would be looking at.</summary>
|
|
public string Banner => engine.Evaluate("banner()").AsString();
|
|
|
|
/// <summary>
|
|
/// Every frame the page has sent, each as <c>opcode/session/payload</c>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Materialised on each read rather than cached, because the point of it is what the page did in answer
|
|
/// to the last <see cref="Do"/>. A session's own resize frames are in here too and read as rubbish; see
|
|
/// the harness's <c>sentFrame()</c> for why they are not rendered some other way.
|
|
/// </remarks>
|
|
public IReadOnlyList<string> Sent
|
|
{
|
|
get
|
|
{
|
|
var count = (int)engine.Evaluate("sentFrames()").AsNumber();
|
|
var frames = new List<string>(count);
|
|
|
|
for (var index = 0; index < count; index++)
|
|
{
|
|
frames.Add(
|
|
engine
|
|
.Evaluate(string.Create(CultureInfo.InvariantCulture, $"sentFrame({index})"))
|
|
.AsString());
|
|
}
|
|
|
|
return frames;
|
|
}
|
|
}
|
|
|
|
/// <summary>How many times the last right-click called <c>preventDefault</c>, and <c>stopPropagation</c>.</summary>
|
|
/// <remarks>
|
|
/// Both, because they answer different questions: the first is whether the WebView's own context menu
|
|
/// was suppressed, and the second is whether xterm's own right-click handling was — which is what keeps
|
|
/// a 20-pixel textarea from being parked under the pointer with the selection sitting in it.
|
|
/// </remarks>
|
|
public int Prevented => (int)engine.Evaluate("prevented()").AsNumber();
|
|
|
|
/// <inheritdoc cref="Prevented" />
|
|
public int Stopped => (int)engine.Evaluate("stopped()").AsNumber();
|
|
|
|
/// <summary>What is selected in that session's pane.</summary>
|
|
public string Selection(uint sessionId) =>
|
|
engine.Evaluate(string.Create(CultureInfo.InvariantCulture, $"selection({sessionId})")).AsString();
|
|
|
|
/// <summary>Everything <c>term.paste</c> was given for that session — the bracketed route.</summary>
|
|
public string Pasted(uint sessionId) =>
|
|
engine.Evaluate(string.Create(CultureInfo.InvariantCulture, $"pasted({sessionId})")).AsString();
|
|
|
|
/// <summary>Everything <c>term.input</c> was given for that session — the typed route.</summary>
|
|
public string Typed(uint sessionId) =>
|
|
engine.Evaluate(string.Create(CultureInfo.InvariantCulture, $"typed({sessionId})")).AsString();
|
|
|
|
/// <summary>Which character-width table that pane was opened with.</summary>
|
|
public string UnicodeAtOpen(uint sessionId) =>
|
|
engine.Evaluate(string.Create(CultureInfo.InvariantCulture, $"unicodeAtOpen({sessionId})")).AsString();
|
|
|
|
/// <summary>Whether that pane loaded the Unicode 11 addon.</summary>
|
|
public bool HasUnicode11(uint sessionId) =>
|
|
engine.Evaluate(string.Create(CultureInfo.InvariantCulture, $"hasUnicode11({sessionId})")).AsBoolean();
|
|
|
|
/// <summary>How many times that session's pane has been asked for the caret.</summary>
|
|
public int Focuses(uint sessionId) =>
|
|
(int)engine.Evaluate(string.Create(CultureInfo.InvariantCulture, $"focuses({sessionId})")).AsNumber();
|
|
|
|
/// <summary>
|
|
/// Loads the harness and then the page, leaving the page exactly as it is a moment after the WebView
|
|
/// navigated to it: one socket opened and still connecting.
|
|
/// </summary>
|
|
public static RendererPage Load()
|
|
{
|
|
var browser = new Engine();
|
|
|
|
// Order matters and is not incidental: the page connects on its last line, so every global it
|
|
// touches — the socket constructor above all — has to be in place before it is evaluated.
|
|
browser.Execute(Read("Renderer/renderer-harness.js"));
|
|
browser.Execute(Read("Renderer/terminal.js"));
|
|
|
|
return new RendererPage(browser);
|
|
}
|
|
|
|
/// <summary>Runs a line of the harness's own vocabulary — <c>accept(0)</c>, <c>advance(1000)</c>.</summary>
|
|
public void Do(string script) => engine.Execute(script);
|
|
|
|
/// <summary>Whether that attempt has been closed, by the page or by the far end.</summary>
|
|
public bool IsClosed(int attempt) =>
|
|
engine.Evaluate(
|
|
string.Create(CultureInfo.InvariantCulture, $"isClosed({attempt})"))
|
|
.AsBoolean();
|
|
|
|
/// <remarks>
|
|
/// Both files are copied beside the test assembly by the project file, the page out of the shell
|
|
/// project it belongs to. Read from disk rather than embedded so that the copy which runs here is the
|
|
/// same bytes the host serves, with nothing in between that could go stale.
|
|
/// </remarks>
|
|
private static string Read(string relativePath) =>
|
|
File.ReadAllText(Path.Combine(AppContext.BaseDirectory, relativePath));
|
|
}
|