Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions Tests/ClipboardUtilitiesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,34 @@ public void ConvertHtmlToTabSeparated_HandlesRowspan()
Assert.Equal("Tall\tTop", lines[0]);
Assert.Equal("Tall\tBottom", lines[1]);
}

// The Text Grab browser extension's Table mode (including its layout
// reconstruction fallback for non-<table> grids) writes a clean
// <table><tr><td>…</td></tr></table> to the clipboard with <br> for
// newlines and &amp;-style entity escaping, then hands off via
// text-grab://paste-spreadsheet. This pins compatibility with that exact
// output (see Text-Grab-Extension/lib/formats.js -> toCleanHtmlTable).
private const string ExtensionRegionTableCfHtml = """
Version:0.9
StartHTML:00000097
EndHTML:00000260
StartFragment:00000131
EndFragment:00000224
<html><body>
<!--StartFragment--><table><tr><td>Product</td><td>Qty</td><td>Unit price</td></tr><tr><td>USB-C hub</td><td>12</td><td>$24.50</td></tr><tr><td>Monitor<br>arm</td><td>5</td><td>$130 &amp; up</td></tr></table><!--EndFragment-->
</body></html>
""";

[Fact]
public void ConvertHtmlToTabSeparated_ParsesBrowserExtensionRegionTable()
{
string result = ClipboardUtilities.ConvertHtmlToTabSeparated(ExtensionRegionTableCfHtml);

string[] lines = result.Split('\n');
Assert.Equal(3, lines.Length);
Assert.Equal("Product\tQty\tUnit price", lines[0]);
Assert.Equal("USB-C hub\t12\t$24.50", lines[1]);
// <br> collapses to a space; &amp; decodes to &.
Assert.Equal("Monitor arm\t5\t$130 & up", lines[2]);
}
}
108 changes: 108 additions & 0 deletions Tests/EditTextWindowSpellCheckTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using Text_Grab;

namespace Tests;

public class EditTextWindowSpellCheckTests
{
[Fact]
public void NormalSentence_SpellCheckEnabled()
{
string text = "The quick brown fox jumps over the lazy dog.";
Assert.True(EditTextWindow.ShouldEnableSpellCheck(text));
}

[Fact]
public void EmptyString_SpellCheckEnabled()
{
Assert.True(EditTextWindow.ShouldEnableSpellCheck(string.Empty));
}

[Fact]
public void TextExceedsLengthThreshold_SpellCheckDisabled()
{
string longText = new string('a', 10_001);
Assert.False(EditTextWindow.ShouldEnableSpellCheck(longText));
}

[Fact]
public void TwoLongWords_SpellCheckEnabled()
{
// Only 2 long words — below the threshold of 3
string text = "normal words then SomeVeryLongManifestTokenThatIsOver25Chars and AnotherReallyLongTokenHere123 end";
Assert.True(EditTextWindow.ShouldEnableSpellCheck(text));
}

[Fact]
public void ThreeLongWords_SpellCheckDisabled()
{
// 3 words each >= 25 chars → should disable spell check
string text = "Microsoft.Windows.AppManifest.Version1234 " +
"com.example.application.package.name.v2 " +
"SomeGuidLike_1234567890abcdef1234 " +
"normal short words";
Assert.False(EditTextWindow.ShouldEnableSpellCheck(text));
}

[Fact]
public void AppManifestLikeContent_SpellCheckDisabled()
{
string manifest = """
<?xml version="1.0" encoding="utf-8"?>
<Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
IgnorableNamespaces="uap mp">
<Identity Name="Microsoft.TextGrab" Publisher="CN=TheJoeFin" Version="4.0.0.0" />
</Package>
""";
Assert.False(EditTextWindow.ShouldEnableSpellCheck(manifest));
}

[Fact]
public void WordExactlyAtLongWordLength_NotCountedAsLong()
{
// Word of exactly 24 chars should NOT count as "very long"
string word24 = new string('x', 24);
string word25 = new string('y', 25);
// Two words of 24 + one of 25 = only one long word → still enabled
string text = $"{word24} {word24} {word25} normal text";
Assert.True(EditTextWindow.ShouldEnableSpellCheck(text));
}

[Fact]
public void GuidTokens_SpellCheckDisabled()
{
// GUIDs are 32+ chars without hyphens when copy-pasted from some apps
string text = "id=a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4 " +
"token=f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2 " +
"hash=1234567890abcdef1234567890abcdef12";
Assert.False(EditTextWindow.ShouldEnableSpellCheck(text));
}

[Fact]
public void AlwaysOnMode_EnabledEvenForContentAutoWouldReject()
{
// Content Auto mode would disable (3+ long tokens), but Always On forces it on
string text = "Microsoft.Windows.AppManifest.Version1234 " +
"com.example.application.package.name.v2 " +
"SomeGuidLike_1234567890abcdef1234";
Assert.False(EditTextWindow.ShouldEnableSpellCheck(SpellCheckMode.Auto, text));
Assert.True(EditTextWindow.ShouldEnableSpellCheck(SpellCheckMode.AlwaysOn, text));
}

[Fact]
public void OffMode_DisabledEvenForNormalText()
{
string text = "The quick brown fox jumps over the lazy dog.";
Assert.True(EditTextWindow.ShouldEnableSpellCheck(SpellCheckMode.Auto, text));
Assert.False(EditTextWindow.ShouldEnableSpellCheck(SpellCheckMode.Off, text));
}

[Fact]
public void AutoMode_MatchesContentHeuristic()
{
string normal = "The quick brown fox.";
string longTokens = new string('a', 10_001);
Assert.True(EditTextWindow.ShouldEnableSpellCheck(SpellCheckMode.Auto, normal));
Assert.False(EditTextWindow.ShouldEnableSpellCheck(SpellCheckMode.Auto, longTokens));
}
}
73 changes: 73 additions & 0 deletions Tests/GrabTemplateExecutorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,79 @@ public void GrabTemplate_GetReferencedPatternNames_ParsesNames()
Assert.Contains("Phone Number", names);
}

[Fact]
public void GrabTemplate_IsTextOnly_TrueWhenNoRegions()
{
GrabTemplate template = new("Test")
{
OutputTemplate = "{p:Email:first}"
};

Assert.True(template.IsTextOnly);
}

[Fact]
public void GrabTemplate_IsTextOnly_FalseWhenRegionsPresent()
{
GrabTemplate template = new("Test")
{
OutputTemplate = "{1}",
Regions = [new TemplateRegion { RegionNumber = 1 }]
};

Assert.False(template.IsTextOnly);
}

// ── ApplyTextOnlyTemplate ─────────────────────────────────────────────────

[Fact]
public void ApplyTextOnlyTemplate_LiteralOutput_IgnoresInputText()
{
GrabTemplate template = new("Header")
{
OutputTemplate = "Static header line"
};

string result = GrabTemplateExecutor.ApplyTextOnlyTemplate(template, "some selected text");
Assert.Equal("Static header line", result);
}

[Fact]
public void ApplyTextOnlyTemplate_RegionPlaceholders_ResolveToEmpty()
{
GrabTemplate template = new("Test")
{
OutputTemplate = "Region: {1}!"
};

string result = GrabTemplateExecutor.ApplyTextOnlyTemplate(template, "input");
Assert.Equal("Region: !", result);
}

[Fact]
public void ApplyTextOnlyTemplate_EscapeSequences_AreProcessed()
{
GrabTemplate template = new("Test")
{
OutputTemplate = @"line1\nline2"
};

string result = GrabTemplateExecutor.ApplyTextOnlyTemplate(template, "input");
Assert.Equal("line1\nline2", result);
}

[Fact]
public void ApplyTextOnlyTemplate_InvalidTemplate_ReturnsInputUnchanged()
{
GrabTemplate template = new("Test")
{
OutputTemplate = string.Empty
};

string result = GrabTemplateExecutor.ApplyTextOnlyTemplate(template, "keep me");
Assert.Equal("keep me", result);
}

// ── ValidateOutputTemplate with patterns ──────────────────────────────────

[Fact]
Expand Down
73 changes: 73 additions & 0 deletions Tests/ImageChangeDetectorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System.Drawing;
using Text_Grab.Utilities;

namespace Tests;

public class ImageChangeDetectorTests
{
private const string fontTestPath = @".\Images\FontTest.png";
private const string fontSamplePath = @".\Images\font_sample.png";

[Fact]
public void FirstCapture_EstablishesBaseline_ReportsNoChange()
{
using ImageChangeDetector detector = new();
using Bitmap image = new(FileUtilities.GetPathToLocalFile(fontTestPath));

Assert.False(detector.CheckForChangeAndUpdate(image));
}

[Fact]
public void SameCapture_ReportsNoChange()
{
using ImageChangeDetector detector = new();
using Bitmap image = new(FileUtilities.GetPathToLocalFile(fontTestPath));

_ = detector.CheckForChangeAndUpdate(image);

Assert.False(detector.CheckForChangeAndUpdate(image));
}

[Fact]
public void DifferentCapture_ReportsChange_OnceItHoldsForTwoChecks()
{
using ImageChangeDetector detector = new();
using Bitmap image1 = new(FileUtilities.GetPathToLocalFile(fontTestPath));
using Bitmap image2 = new(FileUtilities.GetPathToLocalFile(fontSamplePath));

_ = detector.CheckForChangeAndUpdate(image1);

// First differing capture is not yet stable, so no change is reported.
Assert.False(detector.CheckForChangeAndUpdate(image2));
Assert.True(detector.CheckForChangeAndUpdate(image2));
}

[Fact]
public void TransientCapture_DoesNotReportChange()
{
using ImageChangeDetector detector = new();
using Bitmap image1 = new(FileUtilities.GetPathToLocalFile(fontTestPath));
using Bitmap image2 = new(FileUtilities.GetPathToLocalFile(fontSamplePath));

_ = detector.CheckForChangeAndUpdate(image1);

// A one-check blip (flash indicator, half-rendered frame) that
// reverts to the baseline never reports a change.
Assert.False(detector.CheckForChangeAndUpdate(image2));
Assert.False(detector.CheckForChangeAndUpdate(image1));
Assert.False(detector.CheckForChangeAndUpdate(image1));
}

[Fact]
public void Reset_NextCaptureBecomesBaseline_ReportsNoChange()
{
using ImageChangeDetector detector = new();
using Bitmap image1 = new(FileUtilities.GetPathToLocalFile(fontTestPath));
using Bitmap image2 = new(FileUtilities.GetPathToLocalFile(fontSamplePath));

_ = detector.CheckForChangeAndUpdate(image1);
detector.Reset();

Assert.False(detector.CheckForChangeAndUpdate(image2));
}
}
Loading