food-market/src/food-market.pos/Tooling/ScreenshotRunner.cs
nns 84fc350bb6
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
test(pos): реальное окно на Windows 11 + демо-режим + раздел галереи
- --demo: окно с примером чека без логина (software-рендер на Windows, т.к. у
  тест-VM нет GPU — иначе окно чёрное).
- Снят реальный экран кассы на Windows 11 (интерактивная сессия tester через
  scheduled task /it + PrintWindow): подтверждён нативный рендер — кириллица,
  формат «3 362 ₸», кнопки без обрезки → docs/pos-screenshots/windows/ + раздел
  «Windows 11» в галерее.
- Ограничения VM (не код, честно в docs): нет GPU → software-рендер; консоль
  1280×800 → 1366/1920 реальным окном недоступны (они в headless-кадрах, тот же
  движок); меню «Пуск» VM залипает. Автоматизация логики UI — headless --uitest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 09:55:20 +05:00

131 lines
7.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Text;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Headless;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using foodmarket.Pos.ViewModels;
using foodmarket.Pos.Views;
namespace foodmarket.Pos.Tooling;
/// <summary>Headless-рендер экранов кассы в PNG (без дисплея, Skia). По порядку
/// сценария, в 1920×1080 и 1366×768 → docs/pos-screenshots/ + index.html.
/// Запуск: dotnet run -- --screenshots [outputDir]</summary>
public static class ScreenshotRunner
{
private sealed record Shot(string Name, string Caption, Func<Control> Build);
public static int Run(string[] args)
{
var outDir = args.SkipWhile(a => a != "--screenshots").Skip(1).FirstOrDefault(a => !a.StartsWith("--"))
?? Path.Combine(FindRepoRoot(), "docs", "pos-screenshots");
Directory.CreateDirectory(outDir);
AppBuilder.Configure<App>()
.UseSkia()
.UseHeadless(new AvaloniaHeadlessPlatformOptions { UseHeadlessDrawing = false })
.WithInterFont()
.SetupWithoutStarting();
var shots = new List<Shot>
{
new("01-login", "Вход кассира (адрес сервера, логин, пароль)", () => new LoginView { DataContext = LoginViewModel.Sample() }),
new("01b-login-pin", "Быстрый вход по PIN (зарегистрированное устройство)", () => new LoginView { DataContext = LoginViewModel.SamplePin() }),
new("02-shift-open", "Смена: Z-отчёт/состояние смены", () => new ShiftView { DataContext = ShiftViewModel.Sample() }),
new("03-sale-empty", "Продажа: пустой чек, ожидание сканирования", () => new SaleView { DataContext = new SaleViewModel() }),
new("04-sale-items", "Продажа: чек с товарами (вкл. весовой)", () => new SaleView { DataContext = SaleViewModel.Sample() }),
new("05-payment-cash", "Оплата наличными со сдачей", () => new PaymentView { DataContext = PaymentViewModel.SampleCash() }),
new("06-payment-mixed", "Оплата смешанная (нал+карта)", () => new PaymentView { DataContext = PaymentViewModel.SampleMixed() }),
new("07-return", "Возврат по чеку (выбор позиций)", () => new ReturnView { DataContext = ReturnViewModel.Sample() }),
new("08-shift-z", "Z-отчёт при закрытии (должно/факт/разница)", () => new ShiftView { DataContext = ShiftViewModel.SampleClosed() }),
new("09-shell-offline", "Статус-бар: нет связи, чеки в очереди", () => ShellOffline()),
new("10-shell-online", "Статус-бар: на связи, очередь пуста", () => ShellOnline()),
};
var sizes = new (string suffix, int w, int h)[] { ("1920x1080", 1920, 1080), ("1366x768", 1366, 768) };
var produced = new List<(string file, string caption)>();
foreach (var shot in shots)
foreach (var (suffix, w, h) in sizes)
{
var control = shot.Build();
// Если это уже окно (оболочка) — рендерим его напрямую; иначе кладём в окно.
var window = control as Window ?? new Window { Content = control, SystemDecorations = SystemDecorations.None };
window.Width = w; window.Height = h;
window.Show();
Dispatcher.UIThread.RunJobs();
// дать разметке/рендеру отработать
for (int i = 0; i < 3; i++) { Dispatcher.UIThread.RunJobs(); }
var frame = window.CaptureRenderedFrame();
var file = $"{shot.Name}-{suffix}.png";
frame?.Save(Path.Combine(outDir, file));
produced.Add((file, $"{shot.Caption} ({suffix})"));
window.Close();
Console.WriteLine($"[shot] {file}");
}
WriteGallery(outDir, produced);
Console.WriteLine($"[done] {produced.Count} кадров → {outDir}");
return 0;
}
private static Control ShellOffline()
{
var vm = new MainWindowViewModel { IsOnline = false, QueueCount = 3, CashierName = "Пётр Кассиров", ShiftNumber = "С-2026-000001" };
return new MainWindow { DataContext = vm };
}
private static Control ShellOnline()
{
var vm = new MainWindowViewModel { IsOnline = true, QueueCount = 0, CashierName = "Пётр Кассиров", ShiftNumber = "С-2026-000001" };
return new MainWindow { DataContext = vm };
}
private static void WriteGallery(string dir, List<(string file, string caption)> shots)
{
var sb = new StringBuilder();
sb.Append("<!doctype html><html lang=\"ru\"><head><meta charset=\"utf-8\"><title>Касса food-market — скриншоты</title>");
sb.Append("<style>body{font:14px system-ui,Segoe UI,Roboto;margin:0;background:#0f172a;color:#e2e8f0}" +
"header{padding:18px 24px;background:#1e293b}h1{margin:0;font-size:20px}" +
".grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(420px,1fr));gap:16px;padding:20px}" +
"figure{margin:0;background:#1e293b;border:1px solid #334155;border-radius:10px;overflow:hidden}" +
"img{width:100%;display:block;background:#000;cursor:zoom-in}figcaption{padding:8px 10px;color:#cbd5e1;font-size:13px}</style></head><body>");
sb.Append($"<header><h1>Касса food-market — скриншоты процессов</h1><div style=\"color:#94a3b8\">рендер headless (Avalonia/Skia, Linux) — 1920×1080 и 1366×768. Кадров: {shots.Count}</div></header><div class=\"grid\">");
foreach (var (file, caption) in shots)
sb.Append($"<figure><a href=\"{file}\" target=\"_blank\"><img src=\"{file}\" loading=\"lazy\"></a><figcaption>{System.Net.WebUtility.HtmlEncode(caption)}</figcaption></figure>");
sb.Append("</div>");
// Раздел «Windows 11» — реальные скриншоты окна на Windows (если есть).
var winDir = Path.Combine(dir, "windows");
if (Directory.Exists(winDir))
{
var wins = Directory.GetFiles(winDir, "*.png").OrderBy(f => f).ToList();
if (wins.Count > 0)
{
sb.Append("<header style=\"margin-top:8px\"><h1>Windows 11 — реальное окно</h1>" +
"<div style=\"color:#94a3b8\">Снято на Windows 11 (VM, " +
"software-рендер — у VM нет GPU; консоль 1280×800). Подтверждает нативный " +
"рендер: кириллица, формат чисел «3 362 ₸», кнопки без обрезки. Высокое " +
"разрешение 1366/1920 — в headless-кадрах выше (тот же движок Avalonia/Skia/Inter).</div></header>");
sb.Append("<div class=\"grid\">");
foreach (var w in wins)
{
var rel = "windows/" + Path.GetFileName(w);
sb.Append($"<figure><a href=\"{rel}\" target=\"_blank\"><img src=\"{rel}\" loading=\"lazy\"></a><figcaption>Windows 11 — экран продажи (реальное окно)</figcaption></figure>");
}
sb.Append("</div>");
}
}
sb.Append("</body></html>");
File.WriteAllText(Path.Combine(dir, "index.html"), sb.ToString());
}
private static string FindRepoRoot()
{
var d = new DirectoryInfo(AppContext.BaseDirectory);
while (d is not null && !File.Exists(Path.Combine(d.FullName, "food-market.sln"))) d = d.Parent;
return d?.FullName ?? Directory.GetCurrentDirectory();
}
}