feat: choose stock and resolution for warehouse test prints
Co-authored-by: Codex (GPT-6) <noreply@openai.com> Signed-off-by: James Maes <james@kof22.com>
This commit is contained in:
17 files changed
+291
-21
No files matched your search
@@ -9,10 +9,10 @@ on:
|
||||
inputs:
|
||||
product-version:
|
||||
type: string
|
||||
default: '0.1.6'
|
||||
default: '0.1.7'
|
||||
release-version:
|
||||
type: string
|
||||
default: '0.1.6-rc.1'
|
||||
default: '0.1.7-rc.1'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
repository: QRun-IO/qqq-print-protocol
|
||||
ref: a0de93b00e602abde9be77c1ec9103b670bf3c09
|
||||
ref: f4f977ca4aa6b749e6a52760eed5e53c99a7704d
|
||||
path: qqq-print-protocol
|
||||
persist-credentials: false
|
||||
- name: Set up .NET
|
||||
@@ -52,8 +52,8 @@ jobs:
|
||||
run: ./build/Test-BrandedInstaller.ps1
|
||||
- name: Build unsigned prerelease MSI
|
||||
env:
|
||||
PRODUCT_VERSION: ${{ inputs.product-version || '0.1.6' }}
|
||||
RELEASE_VERSION: ${{ inputs.release-version || '0.1.6-rc.1' }}
|
||||
PRODUCT_VERSION: ${{ inputs.product-version || '0.1.7' }}
|
||||
RELEASE_VERSION: ${{ inputs.release-version || '0.1.7-rc.1' }}
|
||||
run: |
|
||||
./build/Build-Installer.ps1 -ProductVersion $env:PRODUCT_VERSION -ReleaseVersion $env:RELEASE_VERSION
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
This is QRun-IO/qqq-print-agent, built on KofTwentyTwo AppKit's Windows service variant. Follow the current user request and repository .editorconfig/central package versions.
|
||||
|
||||
- Verify with `./build/Verify.ps1`; package with `./build/Build-Installer.ps1 -ProductVersion 0.1.6 -ReleaseVersion 0.1.6-rc.1`.
|
||||
- Verify with `./build/Verify.ps1`; package with `./build/Build-Installer.ps1 -ProductVersion 0.1.7 -ReleaseVersion 0.1.7-rc.1`.
|
||||
- Core owns protocol/state/policy; Windows owns OS adapters/storage/runtime; Service owns hosting; UI owns native views/tray plumbing.
|
||||
- Zero compiler/analyzer warnings; locked dependencies. Coverage is reported for the initial prerelease under docs/adr/0002-prerelease-quality-and-packaging.md.
|
||||
- Keep the service alive independently of the companion. Use AppKit update UX with the machine MSI adapter; update service, companion and CLI together. Do not install a per-user-only replacement over the machine package.
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.7-rc.1 — 2026-10-30
|
||||
|
||||
- Built-in QQQ-branded warehouse diagnostics use the server's bundled legible font across PDF and ZPL.
|
||||
- Native test controls offer Letter, A4 and 4 × 6 labels with explicit 203/300/600-dpi ZPL choices.
|
||||
- Prefer ZPL for label-capable queues and suggest resolution from an explicit driver hint; operators verify stock and resolution before printing.
|
||||
- Existing API callers keep compatible defaults when testProfile is omitted.
|
||||
|
||||
|
||||
## 0.1.6-rc.1 — 2026-10-30
|
||||
|
||||
- Expand selected printer information within its row, with actions first, labeled fields, a bounded description preview and expandable Windows diagnostics.
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
commit as "+<sha>" on AssemblyInformationalVersion, which the About dialog and the
|
||||
CLI's version output show as a short hash. -->
|
||||
<PropertyGroup>
|
||||
<VersionPrefix>0.1.6</VersionPrefix>
|
||||
<VersionPrefix>0.1.7</VersionPrefix>
|
||||
<VersionSuffix>rc.1</VersionSuffix>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[ValidatePattern('^\d+\.\d+\.\d+$')]
|
||||
[string] $ProductVersion = '0.1.6',
|
||||
[string] $ProductVersion = '0.1.7',
|
||||
[ValidatePattern('^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$')]
|
||||
[string] $ReleaseVersion = '0.1.6-rc.1',
|
||||
[string] $ReleaseVersion = '0.1.7-rc.1',
|
||||
[string] $PayloadDirectory,
|
||||
[switch] $SkipPublish,
|
||||
[string] $CertificateThumbprint,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# Session state — 2026-10-30
|
||||
|
||||
## Branded warehouse diagnostics in progress — 2026-10-30
|
||||
|
||||
User requested strongly QQQ-branded built-in test documents with warehouse-readable characters. Protocol 0.1.3 adds optional media/resolution profiles; Windows 0.1.7 adds explicit native choices and prefers ZPL on compatible label queues. Windows gate passed 227 tests (57 Core, 170 Windows/CLI), zero warnings/errors; the final UI acceptance test compiles. QQQ generator uses the existing QQQ logo and embedded JetBrains Mono Bold (dotted zero, distinct 1/I/l), rendered into bounded graphics for ZPL. Independent review caught and corrected the Zebra per-field graphic-size limit before any new built-in physical print. QQQ's repeated gate, final previews, installation and actual built-in print acceptance are in progress. The prior installed 0.1.6 and published artifacts remain available.
|
||||
## Current checkpoint: 0.1.6 published and Zebra submissions accepted
|
||||
|
||||
Public v0.1.6-rc.1 release assets are verified. The original paired QQQ database was restored on loopback port 18081 without changing device credentials; read-only inspection found no pending jobs before startup. The connection is healthy. A Zebra ZT411 at 300 dpi, loaded with confirmed 4 × 6-inch stock, is exposed locally, enabled in QQQ and READY. Exactly one ZPL shipping test and one PDF barcode/alignment test reached SPOOLED. Physical output and barcode scanning are pending user confirmation. The service remained Running throughout this session. This is local end-to-end protocol evidence, not a deployed remote cloud test. No production code changed.
|
||||
|
||||
@@ -63,3 +63,11 @@ Settings shows actual Windows service state and provides **Start service**, **Re
|
||||
Closing the window hides it to the tray. Reopening from Start restores the existing instance. **Show the tray companion when I sign in** explicitly enables a task for the current administrator and the installed executable; it is off by default. Upgrades retain this preference, and full uninstall removes verified application tasks. See [installation and security details](INSTALLATION.md).
|
||||
|
||||
Administrators can apply [customer branding](BRANDING.md) without rebuilding. Appearance follows Windows or your selected light/dark theme; unsafe light accents fall back for readable AppKit text.
|
||||
|
||||
## Branded warehouse test prints
|
||||
|
||||
Select a printer and choose **Print test**. Choose a document format and stock from the native option boxes, then confirm **Print one test**. PDF offers Letter, A4 or a 4 × 6-inch label. ZPL offers 4 × 6-inch labels at 203, 300 or 600 dpi. Match the loaded stock and actual device resolution; the driver-name suggestion is only a hint. PDF stock choices do not change Windows queue defaults.
|
||||
|
||||
QQQ generates the same branded diagnostics for desktop requests and its own printer record/table actions. The pages use the QQQ logo, large warehouse identifiers, dotted zeros and distinct 1/I/l glyphs from embedded JetBrains Mono Bold. PDF embeds the font; ZPL carries rasterized glyphs in bounded document graphics, so neither depends on a resident printer font. Label diagnostics include a Code 128 barcode on the ZPL path, and all layouts include size/alignment checks. Paper output and scanner readability must be checked physically.
|
||||
|
||||
Use qbit-printing 0.1.3-rc.1 or later for these branded profiles. Older clients can omit the profile, defaulting to Letter PDF or 203-dpi 4 × 6 ZPL. Native QQQ actions can process selected printers; group devices with the same stock and label resolution. Connecting or discovering a device never automatically sends a test.
|
||||
@@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Package</OutputType>
|
||||
<InstallerPlatform>x64</InstallerPlatform>
|
||||
<ProductVersion Condition="'$(ProductVersion)' == ''">0.1.6</ProductVersion>
|
||||
<ProductVersion Condition="'$(ProductVersion)' == ''">0.1.7</ProductVersion>
|
||||
<PayloadRoot Condition="'$(PayloadRoot)' == ''">$(MSBuildThisFileDirectory)..\artifacts\package</PayloadRoot>
|
||||
<OutputName>QqqPrintAgent-$(ProductVersion)-x64</OutputName>
|
||||
<DefineConstants>ProductVersion=$(ProductVersion);PayloadRoot=$(PayloadRoot)</DefineConstants>
|
||||
|
||||
@@ -116,7 +116,10 @@ public sealed class PrintApiClient : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Request an explicit operator action without retaining document bytes locally.</summary>
|
||||
public async Task<string> OperatorActionAsync(string operation, string? jobId, string? printerId, string? format, bool confirmDuplicateRisk, CancellationToken token)
|
||||
public Task<string> OperatorActionAsync(string operation, string? jobId, string? printerId, string? format, bool confirmDuplicateRisk, CancellationToken token)
|
||||
=> OperatorActionAsync(operation, jobId, printerId, format, confirmDuplicateRisk, null, token);
|
||||
|
||||
public async Task<string> OperatorActionAsync(string operation, string? jobId, string? printerId, string? format, bool confirmDuplicateRisk, string? testProfile, CancellationToken token)
|
||||
{
|
||||
string path = operation switch
|
||||
{
|
||||
@@ -125,7 +128,14 @@ public sealed class PrintApiClient : IDisposable
|
||||
"cancel-job" => $"device/jobs/{Uri.EscapeDataString(jobId ?? throw new ArgumentException("Job is required.", nameof(jobId)))}/cancel",
|
||||
_ => throw new ArgumentException("Unsupported operator action.", nameof(operation)),
|
||||
};
|
||||
using HttpResponseMessage response = await _http.PostAsJsonAsync(path, new { printerId, format, confirmDuplicateRisk, requestId = Guid.NewGuid().ToString("N") }, JsonOptions, token).ConfigureAwait(false);
|
||||
using HttpResponseMessage response = await _http.PostAsJsonAsync(path, new
|
||||
{
|
||||
printerId,
|
||||
format,
|
||||
confirmDuplicateRisk,
|
||||
testProfile = string.Equals(operation, "test-print", StringComparison.Ordinal) ? testProfile : null,
|
||||
requestId = Guid.NewGuid().ToString("N"),
|
||||
}, JsonOptions, token).ConfigureAwait(false);
|
||||
if(!response.IsSuccessStatusCode)
|
||||
{
|
||||
if(response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace Qqq.PrintAgent.Core.Printing;
|
||||
|
||||
/// <summary>Suggested diagnostic defaults; the operator must still verify installed stock and resolution.</summary>
|
||||
public static class TestPrintDefaults
|
||||
{
|
||||
public static string Format(IEnumerable<string> supportedFormats)
|
||||
=> supportedFormats.Contains("ZPL", StringComparer.Ordinal) ? "ZPL" : "PDF";
|
||||
|
||||
public static string Profile(string format, string? driverName)
|
||||
{
|
||||
if(!string.Equals(format, "ZPL", StringComparison.Ordinal))
|
||||
{
|
||||
return "LETTER";
|
||||
}
|
||||
string driver = driverName ?? string.Empty;
|
||||
if(driver.Contains("600dpi", StringComparison.OrdinalIgnoreCase) || driver.Contains("600 dpi", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "LABEL_4X6_600";
|
||||
}
|
||||
return driver.Contains("300dpi", StringComparison.OrdinalIgnoreCase) || driver.Contains("300 dpi", StringComparison.OrdinalIgnoreCase)
|
||||
? "LABEL_4X6_300" : "LABEL_4X6_203";
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,8 @@ public sealed partial class AgentRuntime
|
||||
}
|
||||
}
|
||||
using PrintApiClient api = CreateApi(configuration);
|
||||
string queuedJob = await api.OperatorActionAsync(request.Operation, request.JobId, request.PrinterId, request.Format, request.ConfirmDuplicateRisk, token).ConfigureAwait(false);
|
||||
string? testProfile = string.Equals(request.Operation, "test-print", StringComparison.Ordinal) ? request.TestProfile : null;
|
||||
string queuedJob = await api.OperatorActionAsync(request.Operation, request.JobId, request.PrinterId, request.Format, request.ConfirmDuplicateRisk, testProfile, token).ConfigureAwait(false);
|
||||
if(string.Equals(request.Operation, "cancel-job", StringComparison.Ordinal))
|
||||
{
|
||||
lock(_stateLock)
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Qqq.PrintAgent.Windows;
|
||||
|
||||
public sealed record PipeRequest(string Operation, string? ServerUrl = null, string? EnrollmentCode = null, string? PrinterId = null, bool? Exposed = null,
|
||||
string? JobId = null, string? Format = null, bool ConfirmDuplicateRisk = false, JobQuery? JobQuery = null, LogSettings? LogSettings = null,
|
||||
Qqq.PrintAgent.Core.Printing.DeviceProfile? DeviceProfile = null, PrinterProfile? PrinterProfile = null);
|
||||
Qqq.PrintAgent.Core.Printing.DeviceProfile? DeviceProfile = null, PrinterProfile? PrinterProfile = null, string? TestProfile = null);
|
||||
|
||||
|
||||
|
||||
@@ -76,14 +76,14 @@ public sealed partial class MainWindow
|
||||
{
|
||||
return;
|
||||
}
|
||||
ComboBox format = new() { Header = "Test format", ItemsSource = printer.Formats, SelectedIndex = 0, MinWidth = 220 };
|
||||
StackPanel content = new() { Spacing = 12 };
|
||||
content.Children.Add(new TextBlock { Text = $"Send one test to {PrinterLabel(printer)} through the server? This uses real paper or a label. The printer must be shared here and approved on the server.", TextWrapping = TextWrapping.Wrap });
|
||||
content.Children.Add(format);
|
||||
var dialog = new ContentDialog { Title = "Print a test", Content = content, PrimaryButtonText = "Print one test", CloseButtonText = "Back", DefaultButton = ContentDialogButton.Close, XamlRoot = Content.XamlRoot };
|
||||
if(await DialogGuard.ShowAsync(dialog) == ContentDialogResult.Primary && format.SelectedItem is string selectedFormat)
|
||||
var editor = new TestPrintEditor(printer, PrinterLabel(printer));
|
||||
ContentDialog dialog = CreateWideDialog("Print a test", editor);
|
||||
dialog.PrimaryButtonText = "Print one test";
|
||||
dialog.CloseButtonText = "Back";
|
||||
dialog.DefaultButton = ContentDialogButton.Close;
|
||||
if(await DialogGuard.ShowAsync(dialog) == ContentDialogResult.Primary && editor.SelectedProfile is string profile)
|
||||
{
|
||||
await RunActionAsync(new PipeRequest("test-print", PrinterId: printer.PrinterId, Format: selectedFormat));
|
||||
await RunActionAsync(new PipeRequest("test-print", PrinterId: printer.PrinterId, Format: editor.SelectedFormat, TestProfile: profile));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Qqq.PrintAgent.Core.Printing;
|
||||
using Qqq.PrintAgent.Windows;
|
||||
|
||||
namespace Qqq.PrintAgent;
|
||||
|
||||
/// <summary>Explicit stock and resolution choices for one server-generated diagnostic document.</summary>
|
||||
internal sealed class TestPrintEditor : StackPanel
|
||||
{
|
||||
private readonly ComboBox _format = new() { Header = "Print format", HorizontalAlignment = HorizontalAlignment.Stretch };
|
||||
private readonly ComboBox _profile = new() { Header = "Paper or label stock", HorizontalAlignment = HorizontalAlignment.Stretch };
|
||||
private readonly TextBlock _guidance = new() { TextWrapping = TextWrapping.Wrap };
|
||||
private readonly string _driverName;
|
||||
|
||||
internal string SelectedFormat => (_format.SelectedItem as ComboBoxItem)?.Tag as string ?? "PDF";
|
||||
internal string? SelectedProfile => (_profile.SelectedItem as ComboBoxItem)?.Tag as string;
|
||||
|
||||
internal TestPrintEditor(PrinterSnapshot printer, string printerLabel)
|
||||
{
|
||||
_driverName = printer.DriverName;
|
||||
Spacing = 16;
|
||||
Children.Add(new TextBlock { Text = $"Print one high-contrast QQQ diagnostic page or label on {printerLabel}. This uses real stock and requires local sharing and server approval.", TextWrapping = TextWrapping.Wrap });
|
||||
foreach(string format in printer.Formats)
|
||||
{
|
||||
_format.Items.Add(new ComboBoxItem { Content = string.Equals(format, "ZPL", StringComparison.Ordinal) ? "Printer-ready label (ZPL)" : "Document (PDF)", Tag = format });
|
||||
}
|
||||
string preferred = TestPrintDefaults.Format(printer.Formats);
|
||||
_format.SelectedItem = _format.Items.Cast<ComboBoxItem>().First(item => string.Equals(item.Tag as string, preferred, StringComparison.Ordinal));
|
||||
_format.SelectionChanged += FormatChanged;
|
||||
Microsoft.UI.Xaml.Automation.AutomationProperties.SetAutomationId(_format, "TestPrintFormat");
|
||||
Microsoft.UI.Xaml.Automation.AutomationProperties.SetAutomationId(_profile, "TestPrintMedia");
|
||||
Children.Add(_format);
|
||||
Children.Add(_profile);
|
||||
Children.Add(_guidance);
|
||||
UpdateProfiles();
|
||||
}
|
||||
|
||||
private void FormatChanged(object sender, SelectionChangedEventArgs e) => UpdateProfiles();
|
||||
|
||||
private void UpdateProfiles()
|
||||
{
|
||||
_profile.Items.Clear();
|
||||
bool zpl = string.Equals(SelectedFormat, "ZPL", StringComparison.Ordinal);
|
||||
if(zpl)
|
||||
{
|
||||
AddProfile("4 × 6 in label — 203 dpi", "LABEL_4X6_203");
|
||||
AddProfile("4 × 6 in label — 300 dpi", "LABEL_4X6_300");
|
||||
AddProfile("4 × 6 in label — 600 dpi", "LABEL_4X6_600");
|
||||
}
|
||||
else
|
||||
{
|
||||
AddProfile("US Letter — 8.5 × 11 in", "LETTER");
|
||||
AddProfile("A4 — 210 × 297 mm", "A4");
|
||||
AddProfile("4 × 6 in label", "LABEL_4X6_300");
|
||||
}
|
||||
string preferred = TestPrintDefaults.Profile(SelectedFormat, _driverName);
|
||||
_profile.SelectedItem = _profile.Items.Cast<ComboBoxItem>().First(item => string.Equals(item.Tag as string, preferred, StringComparison.Ordinal));
|
||||
_profile.Header = zpl ? "Label stock and resolution" : "Paper or label stock";
|
||||
_guidance.Text = zpl
|
||||
? "Load 4 × 6 in labels and verify the printer's actual resolution before printing. The default is only a driver-name suggestion (203 dpi when unknown); a wrong DPI changes the printed size. This diagnostic does not calibrate the printer or save device configuration."
|
||||
: "Load stock matching the selected size and set the Windows queue to the same paper or label size. The diagnostic prints one copy at actual size; the stock selection does not change Windows printer defaults.";
|
||||
}
|
||||
|
||||
private void AddProfile(string label, string value) => _profile.Items.Add(new ComboBoxItem { Content = label, Tag = value });
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System.Text.Json;
|
||||
using Qqq.PrintAgent.Core.Printing;
|
||||
using Qqq.PrintAgent.Windows;
|
||||
|
||||
namespace Qqq.PrintAgent.Cli.Tests;
|
||||
|
||||
public sealed class TestPrintPipeTests
|
||||
{
|
||||
[Fact]
|
||||
public void TestMedia_RoundTripsThroughLocalPipeRequestWithoutChangingLegacyDefaults()
|
||||
{
|
||||
const string legacy = "{\"operation\":\"test-print\",\"printerId\":\"queue\",\"format\":\"PDF\"}";
|
||||
PipeRequest previous = JsonSerializer.Deserialize<PipeRequest>(legacy, PrintApiClient.JsonOptions)!;
|
||||
Assert.Null(previous.TestProfile);
|
||||
using var oldBody = JsonDocument.Parse(JsonSerializer.Serialize(previous, PrintApiClient.JsonOptions));
|
||||
Assert.False(oldBody.RootElement.TryGetProperty("testProfile", out _));
|
||||
PipeRequest selected = previous with { Format = "ZPL", TestProfile = "LABEL_4X6_300" };
|
||||
PipeRequest restored = JsonSerializer.Deserialize<PipeRequest>(JsonSerializer.Serialize(selected, PrintApiClient.JsonOptions), PrintApiClient.JsonOptions)!;
|
||||
Assert.Equal(selected, restored);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using Qqq.PrintAgent.Core.Printing;
|
||||
|
||||
namespace Qqq.PrintAgent.Core.Tests;
|
||||
|
||||
public sealed class TestPrintProfileTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("PDF", "LETTER")]
|
||||
[InlineData("PDF", "A4")]
|
||||
[InlineData("PDF", "LABEL_4X6_300")]
|
||||
[InlineData("ZPL", "LABEL_4X6_203")]
|
||||
[InlineData("ZPL", "LABEL_4X6_300")]
|
||||
[InlineData("ZPL", "LABEL_4X6_600")]
|
||||
public async Task SelectedMediaProfile_IsIncludedInActualTestPrintRequest(string format, string profile)
|
||||
{
|
||||
using CaptureHandler handler = new();
|
||||
using PrintApiClient api = new("https://synthetic.invalid", "synthetic-token", handler);
|
||||
string job = await api.OperatorActionAsync("test-print", null, "queue-1", format, false, profile, CancellationToken.None);
|
||||
Assert.Equal("new-test-job", job);
|
||||
Assert.Equal("/printing/v1/device/test-print", handler.Path);
|
||||
JsonElement body = handler.Body;
|
||||
Assert.Equal("queue-1", body.GetProperty("printerId").GetString());
|
||||
Assert.Equal(format, body.GetProperty("format").GetString());
|
||||
Assert.Equal(profile, body.GetProperty("testProfile").GetString());
|
||||
Assert.True(Guid.TryParseExact(body.GetProperty("requestId").GetString(), "N", out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExistingCaller_KeepsLegacyShapeWithoutNullTestProfile()
|
||||
{
|
||||
using CaptureHandler handler = new();
|
||||
using PrintApiClient api = new("https://synthetic.invalid", "synthetic-token", handler);
|
||||
await api.OperatorActionAsync("test-print", null, "queue-1", "PDF", false, CancellationToken.None);
|
||||
Assert.False(handler.Body.TryGetProperty("testProfile", out _));
|
||||
Assert.Equal(4, handler.Body.EnumerateObject().Count());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("retry-job")]
|
||||
[InlineData("cancel-job")]
|
||||
public async Task Profile_IsNeverSentForOtherOperatorActions(string operation)
|
||||
{
|
||||
using CaptureHandler handler = new();
|
||||
using PrintApiClient api = new("https://synthetic.invalid", "synthetic-token", handler);
|
||||
await api.OperatorActionAsync(operation, "existing-job", null, null, true, "LABEL_4X6_600", CancellationToken.None);
|
||||
Assert.False(handler.Body.TryGetProperty("testProfile", out _));
|
||||
Assert.True(handler.Body.GetProperty("confirmDuplicateRisk").GetBoolean());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LabelCapability_TakesPrecedenceOverPdfEnumerationOrder()
|
||||
{
|
||||
Assert.Equal("ZPL", TestPrintDefaults.Format(["PDF", "ZPL"]));
|
||||
Assert.Equal("ZPL", TestPrintDefaults.Format(["ZPL"]));
|
||||
Assert.Equal("PDF", TestPrintDefaults.Format(["PDF"]));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("ZPL", "ZDesigner ZT411-300dpi", "LABEL_4X6_300")]
|
||||
[InlineData("ZPL", "Thermal 600 DPI", "LABEL_4X6_600")]
|
||||
[InlineData("ZPL", "Generic thermal", "LABEL_4X6_203")]
|
||||
[InlineData("ZPL", "Model 600 with unknown resolution", "LABEL_4X6_203")]
|
||||
[InlineData("PDF", "Laser 600dpi", "LETTER")]
|
||||
public void ResolutionSuggestion_UsesExplicitDpiHintsOnly(string format, string driver, string expected)
|
||||
=> Assert.Equal(expected, TestPrintDefaults.Profile(format, driver));
|
||||
|
||||
private sealed class CaptureHandler : HttpMessageHandler
|
||||
{
|
||||
internal JsonElement Body { get; private set; }
|
||||
internal string? Path { get; private set; }
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
Path = request.RequestUri!.AbsolutePath;
|
||||
using var document = JsonDocument.Parse(await request.Content!.ReadAsStringAsync(cancellationToken));
|
||||
Body = document.RootElement.Clone();
|
||||
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("{\"jobId\":\"new-test-job\"}") };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,6 +185,54 @@ public class AppTests
|
||||
session.WaitFor(() => session.FindInApp(cf => cf.ByAutomationId("SortJobTitle")) is { Name: "Sort by document, ascending" } button ? button : null, "document sort query to finish");
|
||||
}
|
||||
|
||||
/// <summary>Requires an exposed PDF/ZPL queue; opens diagnostic choices and cancels without submitting any job.</summary>
|
||||
[Fact]
|
||||
public void LabelTestDialog_OffersResolutionAndStockChoicesWithoutPrinting()
|
||||
{
|
||||
using var session = new AppSession();
|
||||
AutomationElement inventory = session.WaitForElement("PrinterInventory");
|
||||
AutomationElement row = session.WaitFor(() => inventory.FindAllDescendants(cf => cf.ByControlType(ControlType.ListItem))
|
||||
.FirstOrDefault(IsSharedLabelRow), "an exposed PDF/ZPL queue for the read-only diagnostic dialog fixture");
|
||||
row.Patterns.SelectionItem.Pattern.Select();
|
||||
AutomationElement test = session.WaitFor(() => row.FindFirstDescendant(cf => cf.ByAutomationId("PrintTest")) is { IsEnabled: true } button ? button : null, "the selected queue's diagnostic button");
|
||||
test.Patterns.Invoke.Pattern.Invoke();
|
||||
ComboBox format = session.WaitForElement("TestPrintFormat").AsComboBox();
|
||||
ComboBox media = session.WaitForElement("TestPrintMedia").AsComboBox();
|
||||
Assert.Equal("Printer-ready label (ZPL)", format.SelectedItem?.Text);
|
||||
string[] resolutions = ["4 × 6 in label — 203 dpi", "4 × 6 in label — 300 dpi", "4 × 6 in label — 600 dpi"];
|
||||
foreach(string resolution in resolutions)
|
||||
{
|
||||
media.Select(resolution);
|
||||
Assert.Equal(resolution, media.SelectedItem?.Text);
|
||||
}
|
||||
format.Select("Document (PDF)");
|
||||
session.WaitFor(() => string.Equals(media.SelectedItem?.Text, "US Letter — 8.5 × 11 in", StringComparison.Ordinal) ? media : null, "PDF stock choices");
|
||||
string[] stocks = ["US Letter — 8.5 × 11 in", "A4 — 210 × 297 mm", "4 × 6 in label"];
|
||||
foreach(string stock in stocks)
|
||||
{
|
||||
media.Select(stock);
|
||||
Assert.Equal(stock, media.SelectedItem?.Text);
|
||||
}
|
||||
session.ClickButton("Back");
|
||||
session.WaitForElementGone("TestPrintFormat");
|
||||
Assert.False(session.App.HasExited);
|
||||
}
|
||||
|
||||
private static bool IsSharedLabelRow(AutomationElement row)
|
||||
{
|
||||
AutomationElement? exposure = row.FindFirstDescendant(cf => cf.ByControlType(ControlType.CheckBox));
|
||||
if(row.IsOffscreen || exposure?.Patterns.Toggle.Pattern.ToggleState.Value != ToggleState.On)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return row.FindAllDescendants(cf => cf.ByControlType(ControlType.Text)).Any(element =>
|
||||
{
|
||||
string[] formats = element.Name.Split(',', StringSplitOptions.TrimEntries);
|
||||
return element.BoundingRectangle.Bottom <= row.BoundingRectangle.Top + 44
|
||||
&& formats.Contains("ZPL", StringComparer.Ordinal) && formats.Contains("PDF", StringComparer.Ordinal);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>The close button hides details while the tray process stays alive.</summary>
|
||||
|
||||
Reference in new issue
Block a user