Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
17 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
79 changes: 75 additions & 4 deletions .github/workflows/Release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ on:

permissions:
contents: write
id-token: write

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
Expand All @@ -27,6 +28,9 @@ env:
BUILD_X64_SC: 'bld/x64/Text-Grab-Self-Contained'
BUILD_ARM64: 'bld/arm64'
BUILD_ARM64_SC: 'bld/arm64/Text-Grab-Self-Contained'
ARTIFACT_SIGNING_ENDPOINT: 'https://eus.codesigning.azure.net/'
ARTIFACT_SIGNING_ACCOUNT_NAME: 'JoeFinAppsSigningCerts'
ARTIFACT_SIGNING_CERTIFICATE_PROFILE_NAME: 'JoeFinApps'

jobs:
build:
Expand Down Expand Up @@ -77,7 +81,7 @@ jobs:
run: >-
dotnet publish ${{ env.PROJECT_PATH }}
--runtime win-x64
--self-contained false
--no-self-contained
-c Release
-v minimal
-o ${{ env.BUILD_X64 }}
Expand All @@ -91,7 +95,7 @@ jobs:
run: >-
dotnet publish ${{ env.PROJECT_PATH }}
--runtime win-x64
--self-contained true
--self-contained
-c Release
-v minimal
-o ${{ env.BUILD_X64_SC }}
Expand All @@ -105,7 +109,7 @@ jobs:
run: >-
dotnet publish ${{ env.PROJECT_PATH }}
--runtime win-arm64
--self-contained false
--no-self-contained
-c Release
-v minimal
-o ${{ env.BUILD_ARM64 }}
Expand All @@ -118,7 +122,7 @@ jobs:
run: >-
dotnet publish ${{ env.PROJECT_PATH }}
--runtime win-arm64
--self-contained true
--self-contained
-c Release
-v minimal
-o ${{ env.BUILD_ARM64_SC }}
Expand All @@ -137,6 +141,73 @@ jobs:
Rename-Item "${{ env.BUILD_ARM64_SC }}/${{ env.PROJECT }}.exe" 'Text-Grab-arm64.exe'
}

- name: Validate Azure Trusted Signing configuration
shell: pwsh
env:
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
run: |
$requiredSecrets = @{
AZURE_TENANT_ID = $env:AZURE_TENANT_ID
AZURE_CLIENT_ID = $env:AZURE_CLIENT_ID
AZURE_SUBSCRIPTION_ID = $env:AZURE_SUBSCRIPTION_ID
}

$missingSecrets = @(
$requiredSecrets.GetEnumerator() |
Where-Object { [string]::IsNullOrWhiteSpace($_.Value) } |
ForEach-Object { $_.Key }
)

if ($missingSecrets.Count -gt 0) {
throw "Configure these repository secrets before running the release workflow: $($missingSecrets -join ', ')"
}

$signingConfig = @{
ARTIFACT_SIGNING_ENDPOINT = '${{ env.ARTIFACT_SIGNING_ENDPOINT }}'
ARTIFACT_SIGNING_ACCOUNT_NAME = '${{ env.ARTIFACT_SIGNING_ACCOUNT_NAME }}'
ARTIFACT_SIGNING_CERTIFICATE_PROFILE_NAME = '${{ env.ARTIFACT_SIGNING_CERTIFICATE_PROFILE_NAME }}'
}

$missing = @(
$signingConfig.GetEnumerator() |
Where-Object {
[string]::IsNullOrWhiteSpace($_.Value) -or
$_.Value.StartsWith('REPLACE_WITH_') -or
$_.Value.Contains('REPLACE_WITH_')
} |
ForEach-Object { $_.Key }
)

if ($missing.Count -gt 0) {
throw "Update the Azure Trusted Signing placeholders in .github/workflows/Release.yml before running the release workflow: $($missing -join ', ')"
}

- name: Azure login for Trusted Signing
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

- name: Sign release executables
uses: azure/artifact-signing-action@v1
with:
endpoint: ${{ env.ARTIFACT_SIGNING_ENDPOINT }}
signing-account-name: ${{ env.ARTIFACT_SIGNING_ACCOUNT_NAME }}
certificate-profile-name: ${{ env.ARTIFACT_SIGNING_CERTIFICATE_PROFILE_NAME }}
files: |
${{ github.workspace }}\${{ env.BUILD_X64 }}\${{ env.PROJECT }}.exe
${{ github.workspace }}\${{ env.BUILD_X64_SC }}\${{ env.PROJECT }}.exe
${{ github.workspace }}\${{ env.BUILD_ARM64 }}\Text-Grab-arm64.exe
${{ github.workspace }}\${{ env.BUILD_ARM64_SC }}\Text-Grab-arm64.exe
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
description: Text Grab
description-url: https://github.com/TheJoeFin/Text-Grab

- name: Create self-contained archives
shell: pwsh
run: |
Expand Down
159 changes: 159 additions & 0 deletions Tests/HistoryServiceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Windows;
using Text_Grab;
using Text_Grab.Models;
using Text_Grab.Services;
using Text_Grab.Utilities;

namespace Tests;

[Collection("History service")]
public class HistoryServiceTests
{
private static readonly JsonSerializerOptions HistoryJsonOptions = new()
{
AllowTrailingCommas = true,
WriteIndented = true,
Converters = { new JsonStringEnumConverter() }
};

[WpfFact]
public async Task TextHistory_LazyLoadsAgainAfterRelease()
{
await SaveHistoryFileAsync(
"HistoryTextOnly.json",
[
new HistoryInfo
{
ID = "text-1",
CaptureDateTime = new DateTimeOffset(2024, 1, 1, 12, 0, 0, TimeSpan.Zero),
TextContent = "first text history",
SourceMode = TextGrabMode.EditText
}
]);

HistoryService historyService = new();

Assert.Equal("first text history", historyService.GetLastTextHistory());

historyService.ReleaseLoadedHistories();

await SaveHistoryFileAsync(
"HistoryTextOnly.json",
[
new HistoryInfo
{
ID = "text-2",
CaptureDateTime = new DateTimeOffset(2024, 1, 2, 12, 0, 0, TimeSpan.Zero),
TextContent = "second text history",
SourceMode = TextGrabMode.EditText
}
]);

Assert.Equal("second text history", historyService.GetLastTextHistory());
}

[WpfFact]
public async Task ImageHistory_LazyLoadsAgainAfterRelease()
{
await SaveHistoryFileAsync(
"HistoryWithImage.json",
[
new HistoryInfo
{
ID = "image-1",
CaptureDateTime = new DateTimeOffset(2024, 1, 1, 12, 0, 0, TimeSpan.Zero),
TextContent = "first image history",
ImagePath = "one.bmp",
SourceMode = TextGrabMode.GrabFrame
}
]);

HistoryService historyService = new();

Assert.Equal("one.bmp", Assert.Single(historyService.GetRecentGrabs()).ImagePath);

historyService.ReleaseLoadedHistories();

await SaveHistoryFileAsync(
"HistoryWithImage.json",
[
new HistoryInfo
{
ID = "image-2",
CaptureDateTime = new DateTimeOffset(2024, 1, 2, 12, 0, 0, TimeSpan.Zero),
TextContent = "second image history",
ImagePath = "two.bmp",
SourceMode = TextGrabMode.Fullscreen
}
]);

Assert.Equal("two.bmp", Assert.Single(historyService.GetRecentGrabs()).ImagePath);
Assert.Equal("image-2", historyService.GetLastFullScreenGrabInfo()?.ID);
}

[WpfFact]
public async Task ImageHistory_MigratesInlineWordBorderJsonToSidecarStorage()
{
string inlineWordBorderJson = JsonSerializer.Serialize(
new List<WordBorderInfo>
{
new()
{
Word = "hello",
BorderRect = new Rect(1, 2, 30, 40),
LineNumber = 1,
ResultColumnID = 2,
ResultRowID = 3
}
},
HistoryJsonOptions);

await SaveHistoryFileAsync(
"HistoryWithImage.json",
[
new HistoryInfo
{
ID = "image-with-borders",
CaptureDateTime = new DateTimeOffset(2024, 1, 3, 12, 0, 0, TimeSpan.Zero),
TextContent = "history with borders",
ImagePath = "borders.bmp",
SourceMode = TextGrabMode.GrabFrame,
WordBorderInfoJson = inlineWordBorderJson
}
]);

HistoryService historyService = new();
HistoryInfo historyItem = Assert.Single(historyService.GetRecentGrabs());

Assert.Null(historyItem.WordBorderInfoJson);
Assert.Equal("image-with-borders.wordborders.json", historyItem.WordBorderInfoFileName);

List<WordBorderInfo> wordBorderInfos = await historyService.GetWordBorderInfosAsync(historyItem);
WordBorderInfo wordBorderInfo = Assert.Single(wordBorderInfos);
Assert.Equal("hello", wordBorderInfo.Word);
Assert.Equal(30d, wordBorderInfo.BorderRect.Width);
Assert.Equal(40d, wordBorderInfo.BorderRect.Height);

historyService.ReleaseLoadedHistories();

string savedHistoryJson = await FileUtilities.GetTextFileAsync("HistoryWithImage.json", FileStorageKind.WithHistory);
Assert.DoesNotContain("\"WordBorderInfoJson\"", savedHistoryJson);
Assert.Contains("\"WordBorderInfoFileName\"", savedHistoryJson);

string savedWordBorderJson = await FileUtilities.GetTextFileAsync(historyItem.WordBorderInfoFileName!, FileStorageKind.WithHistory);
Assert.Contains("hello", savedWordBorderJson);
}

private static Task<bool> SaveHistoryFileAsync(string fileName, List<HistoryInfo> historyItems)
{
string historyJson = JsonSerializer.Serialize(historyItems, HistoryJsonOptions);
return FileUtilities.SaveTextFile(historyJson, fileName, FileStorageKind.WithHistory);
}
}

[CollectionDefinition("History service", DisableParallelization = true)]
public class HistoryServiceCollectionDefinition
{
}
95 changes: 95 additions & 0 deletions Tests/SettingsServiceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using System.IO;
using System.Text.Json;
using Text_Grab.Models;
using Text_Grab.Properties;
using Text_Grab.Services;

namespace Tests;

public class SettingsServiceTests : IDisposable
{
private readonly string _tempFolder;

public SettingsServiceTests()
{
_tempFolder = Path.Combine(Path.GetTempPath(), $"TextGrab_SettingsService_{Guid.NewGuid():N}");
Directory.CreateDirectory(_tempFolder);
}

public void Dispose()
{
if (Directory.Exists(_tempFolder))
Directory.Delete(_tempFolder, true);
}

[Fact]
public void LoadStoredRegexes_MigratesAndCachesRegexSetting()
{
Settings settings = new();
settings.RegexList = JsonSerializer.Serialize(new[]
{
new StoredRegex
{
Id = "regex-1",
Name = "Invoice Number",
Pattern = @"INV-\d+",
Description = "test pattern"
}
});

SettingsService service = new(
settings,
localSettings: null,
managedJsonSettingsFolderPath: _tempFolder,
saveClassicSettingsChanges: false);

Assert.Equal(string.Empty, settings.RegexList);

StoredRegex[] firstRead = service.LoadStoredRegexes();
string regexFilePath = Path.Combine(_tempFolder, "RegexList.json");

Assert.True(File.Exists(regexFilePath));

File.WriteAllText(
regexFilePath,
JsonSerializer.Serialize(new[]
{
new StoredRegex
{
Id = "regex-2",
Name = "Changed",
Pattern = "changed"
}
}));

StoredRegex[] secondRead = service.LoadStoredRegexes();

StoredRegex initialPattern = Assert.Single(firstRead);
StoredRegex cachedPattern = Assert.Single(secondRead);
Assert.Equal("regex-1", initialPattern.Id);
Assert.Equal("regex-1", cachedPattern.Id);
}

[Fact]
public void SavePostGrabCheckStates_WritesFileAndLeavesClassicSettingEmpty()
{
Settings settings = new();
SettingsService service = new(
settings,
localSettings: null,
managedJsonSettingsFolderPath: _tempFolder,
saveClassicSettingsChanges: false);

service.SavePostGrabCheckStates(new Dictionary<string, bool>
{
["Fix GUIDs"] = true
});

Assert.Equal(string.Empty, settings.PostGrabCheckStates);
Assert.True(File.Exists(Path.Combine(_tempFolder, "PostGrabCheckStates.json")));
Assert.True(service.LoadPostGrabCheckStates()["Fix GUIDs"]);
Assert.Contains(
"Fix GUIDs",
service.GetManagedJsonSettingValueForExport(nameof(Settings.PostGrabCheckStates)));
}
}
2 changes: 0 additions & 2 deletions Text-Grab/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -347,8 +347,6 @@ private async void appStartup(object sender, StartupEventArgs e)
// Register COM server and activator type
bool handledArgument = false;

await Singleton<HistoryService>.Instance.LoadHistories();

ToastNotificationManagerCompat.OnActivated += toastArgs =>
{
LaunchFromToast(toastArgs);
Expand Down
Loading
Loading