Compare commits

...

20 Commits

Author SHA1 Message Date
5c31ad8851 Bump version to 0.8.7
All checks were successful
Deploy / Build and Deploy (push) Successful in 2m47s
2024-07-22 00:19:11 +02:00
3ac9536e76 App: Small changes in auto updater
All checks were successful
Test / Run tests (push) Successful in 2m41s
2024-07-21 15:52:57 +02:00
7246852181 E2ETests: Refactor to use .FindElement(By.Something())
All checks were successful
Test / Run tests (push) Successful in 2m12s
2024-07-19 14:34:12 +02:00
dd48a24c58 Setup: Update dependencies
All checks were successful
Test / Run tests (push) Successful in 2m30s
2024-07-19 13:43:48 +02:00
ffef1fd6e4 Elwig: Update dependencies 2024-07-19 13:43:40 +02:00
01d658f51d Tests: Fix flaky tests when extracting table
All checks were successful
Test / Run tests (push) Successful in 2m10s
2024-07-19 10:49:21 +02:00
5a36e84b1f MemberAdminWindow: Make first email address required when contact via email is enabled
Some checks failed
Test / Run tests (push) Failing after 2m37s
2024-07-19 10:35:14 +02:00
5b2f617a68 Tests: Change IBAN and LfbisNr to be valid
All checks were successful
Test / Run tests (push) Successful in 2m14s
2024-07-19 00:41:49 +02:00
dd5049faae E2ETests: Add DeliveryAdminWindowReceiptTest
All checks were successful
Test / Run tests (push) Successful in 1m56s
2024-07-19 00:16:33 +02:00
ffe0ff5508 WeighingTests: Move all scale handlers into ScaleHandlers.cs 2024-07-18 23:46:05 +02:00
34178105a7 E2ETests: Use ElwigTestDB.sqlite3 instead of default
All checks were successful
Test / Run tests (push) Successful in 2m38s
2024-07-08 13:05:21 +02:00
6b48a1090c E2ETests: Refactor initial class structure
All checks were successful
Test / Run tests (push) Successful in 2m35s
2024-07-07 14:35:54 +02:00
ddd821e478 Tests: Add E2ETests
All checks were successful
Test / Run tests (push) Successful in 2m16s
2024-07-07 01:22:43 +02:00
658a1f4dc1 DeliveryAdminWindow: Only show active modifiers in receipt mode
All checks were successful
Test / Run tests (push) Successful in 1m53s
2024-07-06 18:50:02 +02:00
daf83c4bbc CheckComboBox: Add setter for SelectedItems
All checks were successful
Test / Run tests (push) Successful in 2m6s
2024-07-03 12:25:07 +02:00
26d75ea3cd Controls: Use nameof(Property) instead of string
All checks were successful
Test / Run tests (push) Successful in 1m43s
2024-07-02 11:02:07 +02:00
86937485e4 DocumentTests: Fix Test Date
All checks were successful
Test / Run tests (push) Successful in 1m47s
2024-07-02 01:19:12 +02:00
e9de54415a UpdateDialog: Fix cancellation and buttons
All checks were successful
Test / Run tests (push) Successful in 2m24s
2024-07-01 12:04:13 +02:00
62f63ef63d UpdateDialog: Add Link to Changelog
All checks were successful
Test / Run tests (push) Successful in 2m0s
2024-07-01 11:07:21 +02:00
44656e0022 Workflows: Only test on push on a branch 2024-07-01 10:03:15 +02:00
58 changed files with 904 additions and 300 deletions

View File

@ -1,6 +1,7 @@
name: Test
on:
push:
branches: ["**"]
paths: ["Elwig/**", "Tests/**", "Installer/Files/*.exe"]
jobs:
test:
@ -26,4 +27,4 @@ jobs:
shell: powershell
run: |
$env:PATH = "$(pwd)\Installer\Files;" + $env:PATH
$(& dotnet test Tests; $a=$lastexitcode) | findstr x*; exit $a
$(& dotnet test Tests --filter "FullyQualifiedName!~E2ETests"; $a=$lastexitcode) | findstr x*; exit $a

1
.gitignore vendored
View File

@ -6,3 +6,4 @@ bin/
Tests/Resources/Sql/Create.sql
*.exe
!WinziPrint.exe
*.sqlite3

View File

@ -3,6 +3,28 @@ Changelog
=========
[v0.8.7][v0.8.7] (2024-07-22) {#v0.8.7}
---------------------------------------
### Neue Funktionen {#v0.8.7-features}
* Im Mitglieder-Fenster (`MemberAdminWindow`) muss eine E-Mail-Adresse angegeben werden, wenn E-Mail als Kontaktart angegeben wird. (5a36e84b1f)
### Sonstiges {#v0.8.7-misc}
* Automatisierte Ende-zu-Ende-Tests (`E2ETests`) hinzugefügt. (ddd821e478, 6b48a1090c, dd5049faae, 5b2f617a68, 7246852181)
* Automatisierte Tests überarbeitet. (44656e0022, 86937485e4, 34178105a7, ffe0ff5508, 01d658f51d)
* Update-Dialog (`UpdateDialog`) überarbeitet. (62f63ef63d, e9de54415a)
* Eingebefelder vom Typ _Mehrfachauswahl mit Dropdown_ (`CheckComboBox`) etwas verbessert. (26d75ea3cd, daf83c4bbc)
* Im Übernahme-Fenster (`DeliveryAdminWindow`) werden nur noch aktive Mitglieder angezeigt. (658a1f4dc1)
* Abhängigkeiten aktualisiert. (ffef1fd6e4, dd48a24c58)
* Auto-Update-Funktion überarbeitet. (3ac9536e76)
[v0.8.7]: https://git.necronda.net/winzer/elwig/releases/tag/v0.8.7
[v0.8.6][v0.8.6] (2024-07-01) {#v0.8.6}
---------------------------------------

View File

@ -31,8 +31,8 @@ namespace Elwig {
public static readonly string DataPath = @"C:\ProgramData\Elwig\";
public static readonly string ExePath = @"C:\Program Files\Elwig\";
public static readonly string TempPath = Path.Combine(Path.GetTempPath(), "Elwig");
public static readonly Config Config = new(DataPath + "config.ini");
public static Config Config { get; private set; } = new(Path.Combine(DataPath, "config.ini"));
public static int VersionMajor { get; private set; }
public static int VersionMinor { get; private set; }
public static int VersionPatch { get; private set; }
@ -74,6 +74,11 @@ namespace Elwig {
CurrentApp = this;
OverrideCulture();
var args = Environment.GetCommandLineArgs();
if (args.Length >= 2) {
Config = new(Path.GetFullPath(args[1]));
}
ContextTimer.Tick += (object? sender, EventArgs evt) => {
if (CurrentLastWrite > LastChanged) {
LastChanged = CurrentLastWrite;
@ -159,7 +164,7 @@ namespace Elwig {
list.Add(Scale.FromConfig(s));
} catch (Exception e) {
list.Add(new InvalidScale(s.Id));
if (Config.Debug || s.Required)
if (s.Required)
MessageBox.Show($"Unable to create scale {s.Id}:\n\n{e.Message}", "Scale Error",
MessageBoxButton.OK, MessageBoxImage.Error);
}
@ -222,7 +227,7 @@ namespace Elwig {
}
}
public static async Task CheckForUpdates(bool showSuccess = false) {
public static async Task CheckForUpdates(bool showAlert = false) {
if (Config.UpdateUrl == null) return;
var latest = await Utils.GetLatestInstallerUrl(Config.UpdateUrl);
if (latest != null && new Version(latest.Value.Version) > new Version(Version)) {
@ -233,9 +238,14 @@ namespace Elwig {
Current.Shutdown();
}
});
} else if (showSuccess) {
MessageBox.Show("Elwig ist auf dem aktuellsten Stand!", "Nach Updates suchen",
MessageBoxButton.OK, MessageBoxImage.Information);
} else if (showAlert) {
if (latest == null) {
MessageBox.Show("Informationen konnten nicht abgerufen werden!", "Nach Updates suchen",
MessageBoxButton.OK, MessageBoxImage.Error);
} else {
MessageBox.Show($"Elwig ist auf dem aktuellsten Stand! (Version: {latest.Value.Version})", "Nach Updates suchen",
MessageBoxButton.OK, MessageBoxImage.Information);
}
}
}

View File

@ -1,73 +1,105 @@
using System.Linq;
using System.Collections;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
namespace Elwig.Controls {
public class CheckComboBox : ListBox {
public static readonly DependencyProperty DelimiterProperty = DependencyProperty.Register("Delimiter", typeof(string), typeof(CheckComboBox), new FrameworkPropertyMetadata(", "));
public new static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register(nameof(SelectedItems), typeof(IList), typeof(CheckComboBox), new FrameworkPropertyMetadata(new ObservableCollection<object>(), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemsChangedCallback));
public new IList SelectedItems {
get => (IList)GetValue(SelectedItemsProperty);
set => SetValue(SelectedItemsProperty, value);
}
public static readonly DependencyProperty DelimiterProperty = DependencyProperty.Register(nameof(Delimiter), typeof(string), typeof(CheckComboBox), new FrameworkPropertyMetadata(", "));
public string Delimiter {
get => (string)GetValue(DelimiterProperty);
set => SetValue(DelimiterProperty, value);
}
public static readonly DependencyProperty AllItemsSelectedContentProperty = DependencyProperty.Register("AllItemsSelectedContent", typeof(string), typeof(CheckComboBox), new FrameworkPropertyMetadata("All"));
public static readonly DependencyProperty AllItemsSelectedContentProperty = DependencyProperty.Register(nameof(AllItemsSelectedContent), typeof(string), typeof(CheckComboBox), new FrameworkPropertyMetadata("All"));
public string AllItemsSelectedContent {
get => (string)GetValue(AllItemsSelectedContentProperty);
set => SetValue(AllItemsSelectedContentProperty, value);
}
public static readonly DependencyProperty IsSelectAllActiveProperty = DependencyProperty.Register("IsSelectAllActive", typeof(bool), typeof(CheckComboBox), new FrameworkPropertyMetadata(false));
public static readonly DependencyProperty IsSelectAllActiveProperty = DependencyProperty.Register(nameof(IsSelectAllActive), typeof(bool), typeof(CheckComboBox), new FrameworkPropertyMetadata(false));
public bool IsSelectAllActive {
get => (bool)GetValue(IsSelectAllActiveProperty);
set => SetValue(IsSelectAllActiveProperty, value);
}
public static readonly DependencyProperty SelectAllContentProperty = DependencyProperty.Register("SelectAllContent", typeof(string), typeof(CheckComboBox), new FrameworkPropertyMetadata("All"));
public static readonly DependencyProperty SelectAllContentProperty = DependencyProperty.Register(nameof(SelectAllContent), typeof(string), typeof(CheckComboBox), new FrameworkPropertyMetadata("All"));
public string SelectAllContent {
get => (string)GetValue(SelectAllContentProperty);
set => SetValue(SelectAllContentProperty, value);
}
public static readonly DependencyProperty AllItemsSelectedProperty = DependencyProperty.Register("AllItemsSelected", typeof(bool?), typeof(CheckComboBox), new FrameworkPropertyMetadata(false));
public static readonly DependencyProperty AllItemsSelectedProperty = DependencyProperty.Register(nameof(AllItemsSelected), typeof(bool?), typeof(CheckComboBox), new FrameworkPropertyMetadata(false));
public bool? AllItemsSelected {
get => (bool?)GetValue(AllItemsSelectedProperty);
set => SetValue(AllItemsSelectedProperty, value);
}
public static readonly DependencyProperty IsDropDownOpenProperty = DependencyProperty.Register("IsDropDownOpen", typeof(bool), typeof(CheckComboBox), new FrameworkPropertyMetadata(false));
public static readonly DependencyProperty IsDropDownOpenProperty = DependencyProperty.Register(nameof(IsDropDownOpen), typeof(bool), typeof(CheckComboBox), new FrameworkPropertyMetadata(false));
public bool IsDropDownOpen {
get => (bool)GetValue(IsDropDownOpenProperty);
set => SetValue(IsDropDownOpenProperty, value);
}
public static readonly DependencyProperty MaxDropDownHeightProperty = DependencyProperty.Register("MaxDropDownHeight", typeof(double), typeof(CheckComboBox), new FrameworkPropertyMetadata(ComboBox.MaxDropDownHeightProperty.DefaultMetadata.DefaultValue));
public static readonly DependencyProperty MaxDropDownHeightProperty = DependencyProperty.Register(nameof(MaxDropDownHeight), typeof(double), typeof(CheckComboBox), new FrameworkPropertyMetadata(ComboBox.MaxDropDownHeightProperty.DefaultMetadata.DefaultValue));
public double MaxDropDownHeight {
get => (double)GetValue(MaxDropDownHeightProperty);
set => SetValue(MaxDropDownHeightProperty, value);
}
public new static readonly RoutedEvent SelectionChangedEvent = EventManager.RegisterRoutedEvent(nameof(SelectionChanged), RoutingStrategy.Bubble, typeof(SelectionChangedEventHandler), typeof(CheckComboBox));
public new event SelectionChangedEventHandler SelectionChanged {
add => AddHandler(SelectionChangedEvent, value);
remove => RemoveHandler(SelectionChangedEvent, value);
}
static CheckComboBox() {
DefaultStyleKeyProperty.OverrideMetadata(typeof(CheckComboBox), new FrameworkPropertyMetadata(typeof(CheckComboBox)));
}
private TextBlock TextBox;
private bool _viewHandled;
private bool _modelHandled;
private TextBlock _textBox;
public CheckComboBox() {
SelectionMode = SelectionMode.Multiple;
}
public override void OnApplyTemplate() {
TextBox = (GetTemplateChild("TextBox") as TextBlock)!;
_textBox = (GetTemplateChild("TextBox") as TextBlock)!;
var button = GetTemplateChild("Button") as Button;
button!.Click += Button_MouseDown;
var item = GetTemplateChild("SelectAllItem") as ListBoxItem;
item!.PreviewMouseDown += SelectAllItem_MouseDown;
SelectionChanged += OnSelectionChanged;
if (SelectedItems is INotifyCollectionChanged collection) {
collection.CollectionChanged += (s, e) => { SelectItems(); };
}
IsEnabledChanged += OnIsEnabledChanged;
base.SelectionChanged += OnSelectionChanged;
base.OnApplyTemplate();
}
private static void OnSelectedItemsChangedCallback(DependencyObject sender, DependencyPropertyChangedEventArgs e) {
if (sender is CheckComboBox ccb)
ccb.OnSelectedItemsChanged();
}
private void OnSelectedItemsChanged() {
if (SelectedItems is INotifyCollectionChanged collection) {
collection.CollectionChanged += (s, e) => { SelectItems(); };
}
SelectItems();
}
private void Button_MouseDown(object sender, RoutedEventArgs evt) {
IsDropDownOpen = !IsDropDownOpen;
}
@ -82,25 +114,47 @@ namespace Elwig.Controls {
}
private void OnSelectionChanged(object sender, SelectionChangedEventArgs evt) {
SelectItemsReverse();
var dmp = DisplayMemberPath != null && DisplayMemberPath != "" ? DisplayMemberPath : null;
if (SelectedItems.Count == ItemsSource.Cast<object>().Count() && AllItemsSelectedContent != null) {
TextBox.Text = AllItemsSelectedContent;
_textBox.Text = AllItemsSelectedContent;
AllItemsSelected = true;
} else if (SelectedItems.Count == 0) {
TextBox.Text = "";
_textBox.Text = "";
AllItemsSelected = false;
} else {
TextBox.Text = string.Join(Delimiter,
_textBox.Text = string.Join(Delimiter,
dmp == null ? SelectedItems.Cast<object>() :
SelectedItems.Cast<object>()
.Select(i => i.GetType().GetProperty(dmp)?.GetValue(i))
);
AllItemsSelected = null;
}
RaiseEvent(new SelectionChangedEventArgs(SelectionChangedEvent, evt.RemovedItems, evt.AddedItems));
}
private void OnIsEnabledChanged(object sender, DependencyPropertyChangedEventArgs evt) {
if (!IsEnabled) IsDropDownOpen = false;
}
private void SelectItems() {
if (_viewHandled || _modelHandled)
return;
_viewHandled = true;
base.SelectedItems.Clear();
foreach (var item in SelectedItems)
base.SelectedItems.Add(item);
_viewHandled = false;
}
private void SelectItemsReverse() {
if (_modelHandled || _viewHandled)
return;
_modelHandled = true;
SelectedItems.Clear();
foreach (var item in base.SelectedItems)
SelectedItems.Add(item);
_modelHandled = false;
}
}
}

View File

@ -8,13 +8,13 @@ using System.Windows.Input;
namespace Elwig.Controls {
public class IntegerUpDown : TextBox {
public static readonly DependencyProperty MinimumProperty = DependencyProperty.Register("Miminum", typeof(int?), typeof(IntegerUpDown), new FrameworkPropertyMetadata(null));
public static readonly DependencyProperty MinimumProperty = DependencyProperty.Register(nameof(Minimum), typeof(int?), typeof(IntegerUpDown), new FrameworkPropertyMetadata(null));
public int? Minimum {
get => (int?)GetValue(MinimumProperty);
set => SetValue(MinimumProperty, value);
}
public static readonly DependencyProperty MaximumProperty = DependencyProperty.Register("Maximum", typeof(int?), typeof(IntegerUpDown), new FrameworkPropertyMetadata(null));
public static readonly DependencyProperty MaximumProperty = DependencyProperty.Register(nameof(Maximum), typeof(int?), typeof(IntegerUpDown), new FrameworkPropertyMetadata(null));
public int? Maximum {
get => (int?)GetValue(MaximumProperty);
set => SetValue(MaximumProperty, value);

View File

@ -6,13 +6,13 @@ using System.Windows.Data;
namespace Elwig.Controls {
public class UnitConverter : DependencyObject, IValueConverter {
public static readonly DependencyProperty UnitProperty = DependencyProperty.Register("Unit", typeof(string), typeof(UnitConverter), new FrameworkPropertyMetadata(null));
public static readonly DependencyProperty UnitProperty = DependencyProperty.Register(nameof(Unit), typeof(string), typeof(UnitConverter), new FrameworkPropertyMetadata(null));
public string Unit {
get => (string)GetValue(UnitProperty);
set => SetValue(UnitProperty, value);
}
public static readonly DependencyProperty PrecisionProperty = DependencyProperty.Register("Precision", typeof(byte), typeof(UnitConverter), new FrameworkPropertyMetadata((byte)0));
public static readonly DependencyProperty PrecisionProperty = DependencyProperty.Register(nameof(Precision), typeof(byte), typeof(UnitConverter), new FrameworkPropertyMetadata((byte)0));
public byte Precision {
get => (byte)GetValue(PrecisionProperty);
set => SetValue(PrecisionProperty, value);

View File

@ -4,7 +4,7 @@ using System.Windows.Controls;
namespace Elwig.Controls {
public class UnitTextBox : TextBox {
public static readonly DependencyProperty UnitProperty = DependencyProperty.Register("Unit", typeof(string), typeof(UnitTextBox), new FrameworkPropertyMetadata(""));
public static readonly DependencyProperty UnitProperty = DependencyProperty.Register(nameof(Unit), typeof(string), typeof(UnitTextBox), new FrameworkPropertyMetadata(""));
public string Unit {
get => (string)GetValue(UnitProperty);
set => SetValue(UnitProperty, value);

View File

@ -12,4 +12,4 @@ namespace Elwig.Controls {
}
}
}
}
}

View File

@ -1,4 +1,5 @@
<Window x:Class="Elwig.Dialogs.DeleteMemberDialog"
AutomationProperties.AutomationId="DeleteMemberDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

View File

@ -1,4 +1,5 @@
<Window x:Class="Elwig.Dialogs.NewSeasonDialog"
AutomationProperties.AutomationId="NewSeasonDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

View File

@ -5,11 +5,13 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
ResizeMode="NoResize" ShowInTaskbar="False" Topmost="True"
WindowStartupLocation="CenterOwner"
Title="Neues Update verfügbar - Elwig" Height="180" Width="400">
Title="Neues Update verfügbar - Elwig" Height="190" Width="400"
Closed="OnClosed">
<Grid>
<TextBlock x:Name="Description" FontSize="14" Margin="0,0,0,30"
<TextBlock x:Name="Description" FontSize="14" Margin="0,0,0,40"
HorizontalAlignment="Center" VerticalAlignment="Center" TextAlignment="Center">
Version <Run x:Name="VersionText" FontWeight="Bold">0.0.0</Run> von Elwig ist verfügbar!<LineBreak/>
Version <Run x:Name="VersionText" FontWeight="Bold">0.0.0</Run> von Elwig ist verfügbar!
(<Hyperlink NavigateUri="https://elwig.at/changelog" RequestNavigate="Hyperlink_RequestNavigate">Änderungen</Hyperlink>)<LineBreak/>
Soll das Update heruntergeladen und<LineBreak/>
installiert werden? (ca. <Run x:Name="SizeText">100</Run> MB)<LineBreak/>
<Run FontWeight="Bold">Achtung</Run>: Elwig wird dabei geschlossen!
@ -19,12 +21,12 @@
HorizontalAlignment="Center" VerticalAlignment="Center"
Height="27" Width="300" SnapsToDevicePixels="True"/>
<Button x:Name="InstallButton" Content="Installieren" Margin="10,10,115,10"
FontSize="14" HorizontalAlignment="Right" VerticalAlignment="Bottom"
<Button x:Name="InstallButton" Content="Installieren" Margin="10,10,115,20"
FontSize="14" HorizontalAlignment="Center" VerticalAlignment="Bottom"
Width="100" Height="27"
Click="InstallButton_Click"/>
<Button x:Name="CancelButton" Content="Abbrechen" Margin="10,10,10,10" IsCancel="True" IsDefault="True"
FontSize="14" HorizontalAlignment="Right" VerticalAlignment="Bottom"
<Button x:Name="CancelButton" Content="Abbrechen" Margin="115,10,10,20" IsCancel="True"
FontSize="14" HorizontalAlignment="Center" VerticalAlignment="Bottom"
Width="100" Height="27"/>
</Grid>
</Window>

View File

@ -3,8 +3,10 @@ using System;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Navigation;
namespace Elwig.Dialogs {
public partial class UpdateDialog : Window {
@ -12,36 +14,54 @@ namespace Elwig.Dialogs {
public string Version { get; private set; }
public string Url { get; private set; }
private readonly CancellationTokenSource Cancellation;
public UpdateDialog(string version, string url, long size) {
Version = version;
Url = url;
Cancellation = new();
InitializeComponent();
VersionText.Text = version;
SizeText.Text = $"{size / 1024 / 1024}";
}
private void OnClosed(object sender, EventArgs evt) {
Cancellation.Cancel();
}
private async void InstallButton_Click(object sender, RoutedEventArgs evt) {
Description.Visibility = Visibility.Hidden;
ProgressBar.Visibility = Visibility.Visible;
InstallButton.IsEnabled = false;
await Install();
DialogResult = true;
Close();
}
public async Task Install() {
var fileName = Path.Combine(App.TempPath, $"Elwig-{Version}.exe");
{
try {
using var stream = new FileStream(fileName, FileMode.Create);
using var client = new HttpClient() {
Timeout = TimeSpan.FromSeconds(5),
};
await client.DownloadAsync(Url, stream, new Progress<double>(p => {
ProgressBar.Value = p * 100.0;
}));
}), Cancellation.Token);
} catch (OperationCanceledException) {
File.Delete(fileName);
return;
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
Process.Start(fileName);
DialogResult = true;
}
private void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e) {
Process.Start(new ProcessStartInfo {
FileName = e.Uri.ToString(),
UseShellExecute = true,
});
}
}
}

View File

@ -7,7 +7,7 @@
<UseWPF>true</UseWPF>
<PreserveCompilationContext>true</PreserveCompilationContext>
<ApplicationIcon>Resources\Images\Elwig.ico</ApplicationIcon>
<Version>0.8.6</Version>
<Version>0.8.7</Version>
<SatelliteResourceLanguages>de-AT</SatelliteResourceLanguages>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ApplicationManifest>app.manifest</ApplicationManifest>
@ -25,16 +25,16 @@
</Target>
<ItemGroup>
<PackageReference Include="LinqKit" Version="1.2.5" />
<PackageReference Include="MailKit" Version="4.6.0" />
<PackageReference Include="Microsoft.AspNetCore.Razor.Language" Version="6.0.31" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.6" />
<PackageReference Include="LinqKit" Version="1.3.0" />
<PackageReference Include="MailKit" Version="4.7.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Razor.Language" Version="6.0.32" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="8.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.7" />
<PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="8.0.0" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.2535.41" />
<PackageReference Include="NJsonSchema" Version="11.0.0" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.2592.51" />
<PackageReference Include="NJsonSchema" Version="11.0.2" />
<PackageReference Include="RazorLight" Version="2.3.1" />
<PackageReference Include="ScottPlot.WPF" Version="5.0.34" />
<PackageReference Include="ScottPlot.WPF" Version="5.0.36" />
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="8.0.0" />
</ItemGroup>

View File

@ -98,15 +98,15 @@ namespace Elwig.Helpers {
SavedChanges += OnSavedChanges;
}
public static SqliteConnection Connect() {
var cnx = new SqliteConnection(ConnectionString);
public static SqliteConnection Connect(string? connectionString = null) {
var cnx = new SqliteConnection(connectionString ?? ConnectionString);
cnx.CreateFunction<string, string?, bool?>("REGEXP", (pattern, value) => value == null ? null : Regex.Match(value, pattern).Success, true);
cnx.Open();
return cnx;
}
public static async Task<SqliteConnection> ConnectAsync() {
var cnx = new SqliteConnection(ConnectionString);
public static async Task<SqliteConnection> ConnectAsync(string? connectionString = null) {
var cnx = new SqliteConnection(connectionString ?? ConnectionString);
cnx.CreateFunction<string, string?, bool?>("REGEXP", (pattern, value) => value == null ? null : Regex.Match(value, pattern).Success, true);
await cnx.OpenAsync();
return cnx;

View File

@ -9,7 +9,7 @@ namespace Elwig.Helpers {
public static class AppDbUpdater {
// Don't forget to update value in Tests/fetch-resources.bat!
public static readonly int RequiredSchemaVersion = 23;
public static readonly int RequiredSchemaVersion = 24;
private static int VersionOffset = 0;
@ -17,7 +17,7 @@ namespace Elwig.Helpers {
using var cnx = AppDbContext.Connect();
var applId = (long?)await AppDbContext.ExecuteScalar(cnx, "PRAGMA application_id") ?? 0;
if (applId != 0x454C5747) throw new Exception("Invalid application_id of database");
if (applId != 0x454C5747) throw new Exception($"Invalid application_id in database (0x{applId:X08})");
var schemaVers = (long?)await AppDbContext.ExecuteScalar(cnx, "PRAGMA schema_version") ?? 0;
VersionOffset = (int)(schemaVers % 100);

View File

@ -69,9 +69,9 @@ namespace Elwig.Helpers {
public void Read() {
var config = new ConfigurationBuilder().AddIniFile(FileName).Build();
DatabaseFile = Path.Combine(App.DataPath, config["database:file"] ?? "database.sqlite3");
DatabaseFile = Path.Combine(Path.GetDirectoryName(FileName) ?? App.DataPath, config["database:file"] ?? "database.sqlite3");
var log = config["database:log"];
DatabaseLog = log != null ? Path.Combine(App.DataPath, log) : null;
DatabaseLog = log != null ? Path.Combine(Path.GetDirectoryName(FileName) ?? App.DataPath, log) : null;
Branch = config["general:branch"];
Debug = TrueValues.Contains(config["general:debug"]?.ToLower());
UpdateUrl = config["update:url"];

View File

@ -425,8 +425,11 @@ namespace Elwig.Helpers {
public static async Task<(string Version, string Url, long Size)?> GetLatestInstallerUrl(string url) {
try {
using var client = GetHttpClient(accept: "application/json");
var resJson = JsonNode.Parse(await client.GetStringAsync(url));
var data = resJson!["data"]![0]!;
using var res = await client.GetAsync(url);
if (!res.IsSuccessStatusCode)
return null;
var resJson = JsonNode.Parse(await res.Content.ReadAsStringAsync());
var data = resJson!["data"]!.AsArray()[^1]!;
return ((string)data["version"]!, (string)data["url"]!, (long)data["size"]!);
} catch {
return null;

View File

@ -13,6 +13,9 @@ namespace Elwig.Models.Entities {
[Column("modid")]
public required string ModId { get; set; }
[Column("active")]
public bool IsActive { get; set; }
[Column("ordering")]
public int Ordering { get; set; }
@ -21,7 +24,6 @@ namespace Elwig.Models.Entities {
[Column("abs")]
public long? AbsValue { get; set; }
[NotMapped]
public decimal? Abs {
get => AbsValue != null ? Season.DecFromDb(AbsValue.Value) : null;
@ -30,19 +32,12 @@ namespace Elwig.Models.Entities {
[Column("rel")]
public double? RelValue { get; set; }
[NotMapped]
public decimal? Rel {
get => (decimal?)RelValue;
set => RelValue = (double?)value;
}
[Column("standard")]
public bool IsStandard { get; set; }
[Column("quick_select")]
public bool IsQuickSelect { get; set; }
[ForeignKey("Year")]
public virtual Season Season { get; private set; } = null!;

View File

@ -1,4 +1,4 @@
-- schema version 20 to 21
-- schema version 21 to 22
CREATE VIEW v_penalty_business_shares AS
SELECT u.year, u.mgnr,

View File

@ -1,4 +1,4 @@
-- schema version 20 to 21
-- schema version 22 to 23
CREATE VIEW v_stat_modifier AS
SELECT v.year, v.avnr, m.modid, m.name, m.abs, m.rel,

View File

@ -0,0 +1,5 @@
-- schema version 23 to 24
ALTER TABLE modifier DROP COLUMN standard;
ALTER TABLE modifier DROP COLUMN quick_select;
ALTER TABLE modifier ADD COLUMN active INTEGER NOT NULL CHECK (active IN (TRUE, FALSE)) DEFAULT TRUE;

View File

@ -1,5 +1,6 @@
<local:AdministrationWindow
x:Class="Elwig.Windows.BaseDataWindow"
AutomationProperties.AutomationId="BaseDataWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
@ -521,6 +522,10 @@
<Label Content="Absolut:" Grid.Column="1" Margin="10,100,10,10"/>
<ctrl:UnitTextBox x:Name="SeasonModifierAbsInput" Unit="€/kg" TextChanged="SeasonModifierAbsInput_TextChanged"
Grid.Column="2" Width="86" Margin="0,100,10,10" HorizontalAlignment="Left" VerticalAlignment="Top"/>
<CheckBox x:Name="SeasonModifierActiveInput" Content="In Übernahme-Fenster anzeigen"
Grid.Column="1" Grid.ColumnSpan="2" Margin="10,134,10,10" HorizontalAlignment="Left" VerticalAlignment="Top"
Checked="SeasonModifier_Changed" Unchecked="SeasonModifier_Changed"/>
</Grid>
</GroupBox>
</Grid>

View File

@ -134,11 +134,13 @@ namespace Elwig.Windows {
SeasonModifierNameInput.Text = "";
SeasonModifierRelInput.Text = "";
SeasonModifierAbsInput.Text = "";
SeasonModifierActiveInput.IsChecked = false;
} else {
SeasonModifierIdInput.Text = mod.ModId;
SeasonModifierNameInput.Text = mod.Name;
SeasonModifierRelInput.Text = (mod.Rel * 100)?.ToString() ?? "";
SeasonModifierAbsInput.Text = mod.Abs?.ToString() ?? "";
SeasonModifierActiveInput.IsChecked = mod.IsActive;
}
_modUpdate = false;
}
@ -154,6 +156,7 @@ namespace Elwig.Windows {
mod.Name = SeasonModifierNameInput.Text;
mod.Rel = decimal.TryParse(SeasonModifierRelInput.Text, out var vRel) ? vRel / 100 : null;
mod.AbsValue = decimal.TryParse(SeasonModifierAbsInput.Text, out var vAbs) ? Utils.DecToDb(vAbs, s.Precision) : null;
mod.IsActive = SeasonModifierActiveInput.IsChecked ?? false;
CollectionViewSource.GetDefaultView(_modList).Refresh();
UpdateButtons();

View File

@ -187,8 +187,7 @@ namespace Elwig.Windows {
Name = m.Name,
AbsValue = m.AbsValue * mult / div,
RelValue = m.RelValue,
IsStandard = m.IsStandard,
IsQuickSelect = m.IsQuickSelect,
IsActive = m.IsActive,
}));
}
await ctx.SaveChangesAsync();

View File

@ -1,4 +1,6 @@
<local:AdministrationWindow x:Class="Elwig.Windows.DeliveryAdminWindow"
<local:AdministrationWindow
x:Class="Elwig.Windows.DeliveryAdminWindow"
AutomationProperties.AutomationId="DeliveryAdminWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Elwig.Windows"

View File

@ -1084,7 +1084,7 @@ namespace Elwig.Windows {
ControlUtils.RenewItemsSource(CultivationInput, cultList, null, ControlUtils.RenewSourceDefault.First);
ControlUtils.RenewItemsSource(WineQualityLevelInput, await ctx.WineQualityLevels.ToListAsync());
ControlUtils.RenewItemsSource(ModifiersInput, await ctx.Modifiers
.Where(m => m.Year == y)
.Where(m => m.Year == y && (!IsCreating || m.IsActive))
.OrderBy(m => m.Ordering)
.Include(m => m.Season.Currency)
.ToListAsync());
@ -1116,14 +1116,14 @@ namespace Elwig.Windows {
using var ctx = new AppDbContext();
if (DeliveryList.SelectedItem is Delivery d) {
ControlUtils.RenewItemsSource(ModifiersInput, await ctx.Modifiers
.Where(m => m.Year == d.Year)
.Where(m => m.Year == d.Year && (!IsCreating || m.IsActive))
.OrderBy(m => m.Ordering)
.Include(m => m.Season.Currency)
.ToListAsync());
ControlUtils.RenewItemsSource(DeliveryPartList, d.FilteredParts.OrderBy(p => p.DPNr).ToList(), DeliveryPartList_SelectionChanged, ControlUtils.RenewSourceDefault.First);
} else {
ControlUtils.RenewItemsSource(ModifiersInput, await ctx.Modifiers
.Where(m => m.Year == SeasonInput.Value)
.Where(m => m.Year == SeasonInput.Value && (!IsCreating || m.IsActive))
.OrderBy(m => m.Ordering)
.Include(m => m.Season.Currency)
.ToListAsync());
@ -1555,6 +1555,11 @@ namespace Elwig.Windows {
var attrList = await ctx.WineAttributes.Where(a => a.IsActive).OrderBy(a => a.Name).Cast<object>().ToListAsync();
attrList.Insert(0, new NullItem(""));
ControlUtils.RenewItemsSource(AttributeInput, attrList, null, ControlUtils.RenewSourceDefault.First);
ControlUtils.RenewItemsSource(ModifiersInput, await ctx.Modifiers
.Where(m => m.Year == SeasonInput.Value && m.IsActive)
.OrderBy(m => m.Ordering)
.Include(m => m.Season.Currency)
.ToListAsync());
ControlUtils.RenewItemsSource(MemberInput, await ctx.Members
.Where(m => m.IsActive || !IsReceipt)
.Include(m => m.PostalDest.AtPlz!.Ort)

View File

@ -1,4 +1,5 @@
<Window x:Class="Elwig.Windows.DocumentViewerWindow"
AutomationProperties.AutomationId="DocumentViewerWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

View File

@ -60,31 +60,31 @@ namespace Elwig.Windows {
protected Document? PrintDocument;
protected Dictionary<Member, List<Document>>? EmailDocuments;
public static readonly DependencyProperty PostalAllCountProperty = DependencyProperty.Register("PostalAllCount", typeof(int), typeof(MailWindow));
public static readonly DependencyProperty PostalAllCountProperty = DependencyProperty.Register(nameof(PostalAllCount), typeof(int), typeof(MailWindow));
public int PostalAllCount {
get => (int)GetValue(PostalAllCountProperty);
private set => SetValue(PostalAllCountProperty, value);
}
public static readonly DependencyProperty PostalWishCountProperty = DependencyProperty.Register("PostalWishCount", typeof(int), typeof(MailWindow));
public static readonly DependencyProperty PostalWishCountProperty = DependencyProperty.Register(nameof(PostalWishCount), typeof(int), typeof(MailWindow));
public int PostalWishCount {
get => (int)GetValue(PostalWishCountProperty);
private set => SetValue(PostalWishCountProperty, value);
}
public static readonly DependencyProperty PostalNoEmailCountProperty = DependencyProperty.Register("PostalNoEmailCount", typeof(int), typeof(MailWindow));
public static readonly DependencyProperty PostalNoEmailCountProperty = DependencyProperty.Register(nameof(PostalNoEmailCount), typeof(int), typeof(MailWindow));
public int PostalNoEmailCount {
get => (int)GetValue(PostalNoEmailCountProperty);
private set => SetValue(PostalNoEmailCountProperty, value);
}
public static readonly DependencyProperty EmailAllCountProperty = DependencyProperty.Register("EmailAllCount", typeof(int), typeof(MailWindow));
public static readonly DependencyProperty EmailAllCountProperty = DependencyProperty.Register(nameof(EmailAllCount), typeof(int), typeof(MailWindow));
public int EmailAllCount {
get => (int)GetValue(EmailAllCountProperty);
private set => SetValue(EmailAllCountProperty, value);
}
public static readonly DependencyProperty EmailWishCountProperty = DependencyProperty.Register("EmailWishCount", typeof(int), typeof(MailWindow));
public static readonly DependencyProperty EmailWishCountProperty = DependencyProperty.Register(nameof(EmailWishCount), typeof(int), typeof(MailWindow));
public int EmailWishCount {
get => (int)GetValue(EmailWishCountProperty);
private set => SetValue(EmailWishCountProperty, value);

View File

@ -1,5 +1,6 @@
<local:AdministrationWindow
x:Class="Elwig.Windows.MemberAdminWindow"
AutomationProperties.AutomationId="MemberAdminWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Elwig.Windows"
@ -523,7 +524,7 @@
Checked="CheckBox_Changed" Unchecked="CheckBox_Changed"
HorizontalAlignment="Left" Margin="0,0,0,15" VerticalAlignment="Bottom" Grid.Column="1" Grid.ColumnSpan="2"/>
<CheckBox x:Name="ContactEmailInput" Content="E-Mail" IsEnabled="False"
Checked="CheckBox_Changed" Unchecked="CheckBox_Changed"
Checked="ContactEmailInput_Changed" Unchecked="ContactEmailInput_Changed"
HorizontalAlignment="Left" Margin="60,0,0,15" VerticalAlignment="Bottom" Grid.Column="1" Grid.ColumnSpan="2"/>
<Button x:Name="DeliveryButton" Content="Lieferungen" Click="DeliveryButton_Click" IsEnabled="False"

View File

@ -1243,7 +1243,11 @@ namespace Elwig.Windows {
}
private new void EmailAddressInput_TextChanged(object sender, TextChangedEventArgs evt) {
base.EmailAddressInput_TextChanged(sender, evt);
if (sender == EmailAddress1Input && ContactEmailInput.IsChecked == true) {
InputTextChanged((TextBox)sender, Validator.CheckEmailAddress((TextBox)sender, true));
} else {
base.EmailAddressInput_TextChanged(sender, evt);
}
UpdateContactInfoVisibility(IsEditing || IsCreating);
}
@ -1252,6 +1256,10 @@ namespace Elwig.Windows {
UpdateContactInfoVisibility(IsEditing || IsCreating);
}
private void ContactEmailInput_Changed(object sender, RoutedEventArgs evt) {
EmailAddressInput_TextChanged(EmailAddress1Input, new TextChangedEventArgs(evt.RoutedEvent, UndoAction.None));
}
private void KgDetailsButton_Click(object sender, RoutedEventArgs evt) {
if (DefaultKgInput.SelectedItem is AT_Kg kg) {
App.FocusOriginHierarchyKg(kg.KgNr);

View File

@ -13,7 +13,7 @@
</Target>
<ItemGroup>
<ProjectReference Include="..\Installer\Installer.wixproj" />
<PackageReference Include="WixToolset.Bal.wixext" Version="5.0.0" />
<PackageReference Include="WixToolset.Util.wixext" Version="5.0.0" />
<PackageReference Include="WixToolset.Bal.wixext" Version="5.0.1" />
<PackageReference Include="WixToolset.Util.wixext" Version="5.0.1" />
</ItemGroup>
</Project>

View File

@ -22,11 +22,11 @@ namespace Tests.DocumentTests {
Winzerstraße 1
2223 Hohenruppersdorf
"""));
Assert.That(text, Contains.Substring("1472583")); // Betriebsnummer
Assert.That(text, Contains.Substring("0123463")); // Betriebsnummer
Assert.That(text, Contains.Substring("pauschaliert"));
Assert.That(text, Contains.Substring($"Wolkersdorf, am {DateTime.Now:dd.MM.yyyy}"));
Assert.That(text, Contains.Substring($"Wolkersdorf, am {Elwig.Helpers.Utils.Today:dd.MM.yyyy}"));
Assert.That(text, Contains.Substring("Traubengutschrift Max Mustermann Probevariante"));
Assert.That(text, Contains.Substring("AT12 3456 7890 1234 5678"));
Assert.That(text, Contains.Substring("AT81 1234 5678 9012 3457"));
Assert.That(text, Contains.Substring("""
20201001X001 1 Grüner Veltliner 73 15,0 ungeb.: 3 219 0,5000 - - 1 609,50
20201001X003 1 Grüner Veltliner 75 15,4 ungeb.: 2 561 - -

View File

@ -19,9 +19,9 @@ namespace Tests.DocumentTests {
Winzerstraße 1
2223 Hohenruppersdorf
"""));
Assert.That(text, Contains.Substring("1472583")); // Betriebsnummer
Assert.That(text, Contains.Substring("0123463")); // Betriebsnummer
Assert.That(text, Contains.Substring("pauschaliert"));
Assert.That(text, Contains.Substring($"Wolkersdorf, am {DateTime.Now:dd.MM.yyyy}"));
Assert.That(text, Contains.Substring($"Wolkersdorf, am {Elwig.Helpers.Utils.Today:dd.MM.yyyy}"));
Assert.That(text, Contains.Substring("Anlieferungsbestätigung 2020"));
Assert.That(text, Contains.Substring("""
20201001X003 2 Grüner Veltliner Kabinett Kabinett 87 17,6 ungeb.: 3 129 3 129 n

View File

@ -17,9 +17,9 @@ namespace Tests.DocumentTests {
Winzerstraße 1
2223 Hohenruppersdorf
"""));
Assert.That(text, Contains.Substring("1472583")); // Betriebsnummer
Assert.That(text, Contains.Substring("0123463")); // Betriebsnummer
Assert.That(text, Contains.Substring("pauschaliert"));
Assert.That(text, Contains.Substring($"Wolkersdorf, am {DateTime.Now:dd.MM.yyyy}"));
Assert.That(text, Contains.Substring($"Wolkersdorf, am {Elwig.Helpers.Utils.Today:dd.MM.yyyy}"));
Assert.That(text, Contains.Substring("Traubenübernahmeschein Nr. 20201001X001"));
Assert.That(text, Contains.Substring("Ich bin der Text, der auf einem Traubenübernahmeschein steht!"));
Assert.That(text, Contains.Substring("""
@ -44,9 +44,9 @@ namespace Tests.DocumentTests {
Winzerstraße 2
2223 Hohenruppersdorf
"""));
Assert.That(text, Contains.Substring("4725836")); // Betriebsnummer
Assert.That(text, Contains.Substring("0123471")); // Betriebsnummer
Assert.That(text, Contains.Substring("pauschaliert"));
Assert.That(text, Contains.Substring($"Wolkersdorf, am {DateTime.Now:dd.MM.yyyy}"));
Assert.That(text, Contains.Substring($"Wolkersdorf, am {Elwig.Helpers.Utils.Today:dd.MM.yyyy}"));
Assert.That(text, Contains.Substring("Traubenübernahmeschein Nr. 20201001X004"));
Assert.That(text, Contains.Substring("Ich bin der Text, der auf einem Traubenübernahmeschein steht!"));
Assert.That(text, Contains.Substring("""
@ -77,9 +77,9 @@ namespace Tests.DocumentTests {
Winzerstraße 1
2223 Hohenruppersdorf
"""));
Assert.That(text, Contains.Substring("1472583")); // Betriebsnummer
Assert.That(text, Contains.Substring("0123463")); // Betriebsnummer
Assert.That(text, Contains.Substring("pauschaliert"));
Assert.That(text, Contains.Substring($"Wolkersdorf, am {DateTime.Now:dd.MM.yyyy}"));
Assert.That(text, Contains.Substring($"Wolkersdorf, am {Elwig.Helpers.Utils.Today:dd.MM.yyyy}"));
Assert.That(text, Contains.Substring("Traubenübernahmeschein Nr. 20201001X003"));
Assert.That(text, Contains.Substring("Ich bin der Text, der auf einem Traubenübernahmeschein steht!"));
Assert.That(text, Contains.Substring("""
@ -116,9 +116,9 @@ namespace Tests.DocumentTests {
Brünner Straße 10
2120 Wolkersdorf im Weinviertel
"""));
Assert.That(text, Contains.Substring("7258369")); // Betriebsnummer
Assert.That(text, Contains.Substring("0123480")); // Betriebsnummer
Assert.That(text, Contains.Substring("pauschaliert"));
Assert.That(text, Contains.Substring($"Wolkersdorf, am {DateTime.Now:dd.MM.yyyy}"));
Assert.That(text, Contains.Substring($"Wolkersdorf, am {Elwig.Helpers.Utils.Today:dd.MM.yyyy}"));
Assert.That(text, Contains.Substring("Traubenübernahmeschein Nr. 20201002X001"));
Assert.That(text, Contains.Substring("Ich bin der Text, der auf einem Traubenübernahmeschein steht!"));
Assert.That(text, Contains.Substring("""

View File

@ -32,8 +32,8 @@ namespace Tests.DocumentTests {
Adresse: Hauptstraße 1
PLZ/Ort: 2122 Riedenthal (Riedenthal)
"""));
Assert.That(text, Contains.Substring("IBAN: AT12 3456 7890 1234 5678"));
Assert.That(text, Contains.Substring("Betriebs-Nr.: 2583691"));
Assert.That(text, Contains.Substring("IBAN: AT97 1234 5678 9012 3460"));
Assert.That(text, Contains.Substring("Betriebs-Nr.: 0123498"));
Assert.That(text, Contains.Substring("Stammgemeinde: Wolkersdorf"));
});
}

View File

@ -17,9 +17,9 @@ namespace Tests.DocumentTests {
Assert.That(text, Contains.Substring("Mitgliederliste"));
Assert.That(text, Contains.Substring("Alle Mitglieder"));
Assert.That(table.Take(3), Is.EqualTo(new string[][] {
["101 MUSTERMANN Max", "Winzerstraße 1", "2223", "Hohenruppersdorf", "1472583", "0", "Hohenruppersdorf"],
["102 WEINBAUER Wernhardt", "Winzerstraße 2", "2223", "Hohenruppersdorf", "4725836", "0", "Hohenruppersdorf"],
["", "W&B Weinbauer GesbR", "Winzerstraße 2", "2223", "Hohenruppersdorf"],
["101 MUSTERMANN Max", "Winzerstraße 1", "2223", "Hohenruppersdorf", "0123463", "0", "Hohenruppersdorf"],
["102 WEINBAUER Wernhardt", "Winzerstraße 2", "2223", "Hohenruppersdorf", "0123471", "0", "Hohenruppersdorf"],
[ "W&B Weinbauer GesbR", "Winzerstraße 2", "2223", "Hohenruppersdorf"],
}));
});
}

View File

@ -20,7 +20,7 @@ namespace Tests.DocumentTests {
public static string[][] ExtractTable(string text) {
return text.Split('\n')
.Select(row => Regex.Split(row, @"\s{2,}").Select(c => c.Trim()).ToArray())
.Select(row => Regex.Split(row, @"\s{2,}").Select(c => c.Trim()).Where(c => c.Length > 0).ToArray())
.Where(row => row.Length > 3)
.Skip(1)
.ToArray();

View File

@ -0,0 +1,49 @@
using OpenQA.Selenium.Appium.Windows;
using OpenQA.Selenium.Appium;
namespace Tests.E2ETests {
public class AppSession : IDisposable {
private const int WaitForAppLaunch = 3;
private readonly string WinAppDriverUrl;
public readonly WindowsDriver<WindowsElement> App;
public readonly WindowsDriver<WindowsElement> Desktop;
public AppSession(string appPath, string? appArgs, string winAppDriverUrl) {
WinAppDriverUrl = winAppDriverUrl;
var appiumOptions = new AppiumOptions();
appiumOptions.AddAdditionalCapability("app", appPath);
if (appArgs != null)
appiumOptions.AddAdditionalCapability("appArguments", appArgs);
appiumOptions.AddAdditionalCapability("deviceName", "WindowsPC");
appiumOptions.AddAdditionalCapability("ms:waitForAppLaunch", WaitForAppLaunch);
App = new WindowsDriver<WindowsElement>(new Uri(WinAppDriverUrl), appiumOptions);
Assert.That(App, Is.Not.Null);
Assert.That(App.SessionId, Is.Not.Null);
App.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(1.5);
var desktopOptions = new AppiumOptions();
desktopOptions.AddAdditionalCapability("app", "Root");
desktopOptions.AddAdditionalCapability("deviceName", "WindowsPC");
Desktop = new WindowsDriver<WindowsElement>(new Uri(WinAppDriverUrl), desktopOptions);
}
public WindowsDriver<WindowsElement> CreateWindowDriver(string windowName) {
var window = Desktop.FindElementByAccessibilityId(windowName);
var windowHandle = int.Parse(window.GetAttribute("NativeWindowHandle")).ToString("x"); // Convert to Hex
var appiumOptions = new AppiumOptions();
appiumOptions.AddAdditionalCapability("appTopLevelWindow", windowHandle);
return new WindowsDriver<WindowsElement>(new Uri(WinAppDriverUrl), appiumOptions);
}
public void Dispose() {
GC.SuppressFinalize(this);
App.Close();
try {
Desktop.FindElement(By.Name("Ja")).Click();
} catch { }
App.Dispose();
Desktop.Close();
Desktop.Dispose();
}
}
}

View File

@ -0,0 +1,85 @@
using OpenQA.Selenium;
using OpenQA.Selenium.Appium.Windows;
using Tests.WeighingTests;
namespace Tests.E2ETests {
[TestFixture, Order(2)]
public class DeliveryAdminWindowReceiptTest {
private MockScale Mock;
private AppSession Session;
[OneTimeSetUp]
public void Setup() {
Mock = new CommandMockScale(12345, ScaleHandlers.Handle_IT3000A) {
Weight = 3210,
};
Session = new(Utils.ApplicationPath, Utils.ConfigPath, WinAppDriver.WinAppDriverUrl);
Session.App.FindElement(By.Name("Stammdaten")).Click();
Thread.Sleep(500);
var window = Session.CreateWindowDriver("BaseDataWindow");
window.FindElement(By.Name("Saisons")).Click();
window.FindElement(By.Name("Neu anlegen...")).Click();
var dialog = Session.CreateWindowDriver("NewSeasonDialog");
dialog.FindElement(By.Name("Bestätigen")).Click();
dialog.Close();
Thread.Sleep(500);
window.Close();
}
[OneTimeTearDown]
public void Teardown() {
Session.Dispose();
Mock.Dispose();
}
private WindowsDriver<WindowsElement> OpenReceiptWindow() {
Session.App.FindElement(By.Name("Übernahme")).Click();
Thread.Sleep(Utils.WINDOW_OPEN_SLEEP);
return Session.CreateWindowDriver("DeliveryAdminWindow");
}
private void FinishDeliveryNote(WindowsDriver<WindowsElement> window) {
window.FindElement(By.Name("Abschließen")).Click();
Thread.Sleep(2000);
var doc = Session.CreateWindowDriver("DocumentViewerWindow");
Assert.That(doc.Title, Contains.Substring("Traubenübernahmeschein"));
Thread.Sleep(500);
doc.Close();
Thread.Sleep(500);
}
[Test]
public void Test_1_Minimal() {
var window = OpenReceiptWindow();
window.FindElement(By.WpfId("MgNrInput")).SendKeys("101" + Keys.Enter + "GV" + Keys.Enter + "73" + Keys.Enter + Keys.Enter);
Thread.Sleep(500);
FinishDeliveryNote(window);
window.Close();
}
[Test]
public void Test_2_OtherInputs() {
var window = OpenReceiptWindow();
window.FindElement(By.WpfId("MemberInput")).SendKeys("Mustermann Max");
window.FindElement(By.WpfId("WineVarietyInput")).SelectItem("Zweigelt");
window.FindElement(By.WpfId("GradationKmwInput")).SendKeys("18");
window.FindElement(By.Name("Wiegen")).Click();
Thread.Sleep(500);
FinishDeliveryNote(window);
window.Close();
}
[Test]
public void Test_3_AttributeCultivationModifier() {
var window = OpenReceiptWindow();
window.FindElement(By.WpfId("MgNrInput")).SendKeys("102" + Keys.Enter + "GVK");
window.FindElement(By.WpfId("CultivationInput")).SelectItem("Bio");
window.FindElement(By.WpfId("GradationOeInput")).SendKeys("73" + Keys.Enter + Keys.Enter);
Thread.Sleep(500);
FinishDeliveryNote(window);
window.Close();
}
}
}

View File

@ -0,0 +1,50 @@
namespace Tests.E2ETests {
[TestFixture, Order(1)]
public class MainWindowTest {
private AppSession Session;
[OneTimeSetUp]
public void Setup() {
Session = new(Utils.ApplicationPath, Utils.ConfigPath, WinAppDriver.WinAppDriverUrl);
}
[OneTimeTearDown]
public void Teardown() {
Session.Dispose();
}
[Test]
public void Test_Open_MemberAdminWindow() {
Assert.DoesNotThrow(() => {
Session.App.FindElement(By.Name("Mitglieder")).Click();
Thread.Sleep(Utils.WINDOW_OPEN_SLEEP);
var window = Session.CreateWindowDriver("MemberAdminWindow");
Assert.That(window.Title, Is.EqualTo("Mitglieder - Elwig"));
window.Close();
});
}
[Test]
public void Test_Open_DeliveryAdminWindow() {
Assert.DoesNotThrow(() => {
Session.App.FindElement(By.Name("Lieferungen")).Click();
Thread.Sleep(Utils.WINDOW_OPEN_SLEEP);
var window = Session.CreateWindowDriver("DeliveryAdminWindow");
Assert.That(window.Title, Is.EqualTo("Lieferungen - Elwig"));
window.Close();
});
}
[Test]
public void Test_Open_BaseDataWindow() {
Assert.DoesNotThrow(() => {
Session.App.FindElement(By.Name("Stammdaten")).Click();
Thread.Sleep(Utils.WINDOW_OPEN_SLEEP);
var window = Session.CreateWindowDriver("BaseDataWindow");
Assert.That(window.Title, Is.EqualTo("Stammdaten - Elwig"));
window.Close();
});
}
}
}

View File

@ -0,0 +1,146 @@
using OpenQA.Selenium.Appium.Windows;
namespace Tests.E2ETests {
[TestFixture, Order(3)]
public class MemberAdminWindowTest {
private AppSession Session;
private WindowsDriver<WindowsElement> Window;
[OneTimeSetUp]
public void WindowSetup() {
Session = new(Utils.ApplicationPath, Utils.ConfigPath, WinAppDriver.WinAppDriverUrl);
Session.App.FindElement(By.Name("Mitglieder")).Click();
Thread.Sleep(Utils.WINDOW_OPEN_SLEEP);
Window = Session.CreateWindowDriver("MemberAdminWindow");
}
[OneTimeTearDown]
public void WindowTeardown() {
Window.Close();
Window.Quit();
Session.Dispose();
}
[TearDown]
public void Teardown() {
Window.FindElement(By.WpfId("SearchInput")).Clear();
Thread.Sleep(500);
}
[Test]
public void Test_1_CreateMember() {
Window.FindElement(By.WpfId("NewMemberButton")).Click();
Window.FindElement(By.WpfId("MgNrInput")).Clear();
Window.FindElement(By.WpfId("MgNrInput")).SendKeys("9999");
Window.FindElement(By.WpfId("GivenNameInput")).SendKeys("Norbert");
Window.FindElement(By.WpfId("FamilyNameInput")).SendKeys("Neuling");
Window.FindElement(By.WpfId("PrefixInput")).SendKeys("Ing.");
Window.FindElement(By.WpfId("SuffixInput")).SendKeys("jun.");
Window.FindElement(By.WpfId("BirthdayInput")).SendKeys("1987");
Window.FindElement(By.WpfId("AddressInput")).SendKeys("Musterstraße 9");
Window.FindElement(By.WpfId("PlzInput")).SendKeys("2120");
Window.FindElement(By.WpfId("OrtInput")).SelectItem(1);
Window.FindElement(By.WpfId("EmailAddress1Input")).SendKeys("norbert.neuling@aon.at");
Window.FindElement(By.WpfId("EmailAddress2Input")).SendKeys("nathalie.neuling@aon.at");
Window.FindElement(By.WpfId("PhoneNr1TypeInput")).SelectItem(1);
Window.FindElement(By.WpfId("PhoneNr1Input")).SendKeys("012345678");
Window.FindElement(By.WpfId("PhoneNr2TypeInput")).SelectItem(2);
Window.FindElement(By.WpfId("PhoneNr2Input")).SendKeys("0664123456");
Window.FindElement(By.WpfId("IbanInput")).SendKeys("AT611904300234573201");
Window.FindElement(By.WpfId("BicInput")).SendKeys("RLNWATWWWDF");
Window.FindElement(By.WpfId("UstIdNrInput")).SendKeys("ATU66192906"); // TODO: Testdaten?
Window.FindElement(By.WpfId("LfbisNrInput")).SendKeys("1251074"); // TODO: Testdaten?
Window.FindElement(By.WpfId("BuchführendInput")).Click();
Window.FindElement(By.WpfId("OrganicInput")).Click();
Window.FindElement(By.WpfId("BillingNameInput")).SendKeys("Neuling KG");
Window.FindElement(By.WpfId("BillingAddressInput")).SendKeys("Betriebsstraße 1");
Window.FindElement(By.WpfId("BillingPlzInput")).SendKeys("2120");
Window.FindElement(By.WpfId("BillingOrtInput")).SelectItem(2);
Window.FindElement(By.WpfId("BusinessSharesInput")).SendKeys("10");
Window.FindElement(By.WpfId("BranchInput")).SelectItem("Matzen");
Window.FindElement(By.WpfId("DefaultKgInput")).SelectItem(3);
Window.FindElement(By.WpfId("VollLieferantInput")).Click();
Window.FindElement(By.WpfId("FunkionärInput")).Click();
Window.FindElement(By.WpfId("CommentInput")).SendKeys("Die lieben Mustermänner und Musterfrauen!");
Window.FindElement(By.WpfId("ContactEmailInput")).Click();
Window.FindElement(By.WpfId("SaveButton")).Click();
Window.FindElement(By.WpfId("SearchInput")).SendKeys("9999");
Thread.Sleep(500);
var memberListRow = Window.FindElement(By.WpfId("MemberList")).FindElement(By.ClassName("DataGridRow"));
Assert.Multiple(() => {
Assert.That(memberListRow, Is.Not.Null);
Assert.That(memberListRow.FindElement(By.Name("9999 ")), Is.Not.Null);
Assert.That(memberListRow.FindElement(By.Name("Norbert")), Is.Not.Null);
Assert.That(memberListRow.FindElement(By.Name("Neuling")), Is.Not.Null);
});
}
[Test]
public void Test_2_EditMember() {
Window!.FindElement(By.WpfId("SearchInput")).SendKeys("9999");
Thread.Sleep(500);
var memberList = Window.FindElement(By.WpfId("MemberList"));
Assert.That(memberList, Is.Not.Null);
var memberListRows = memberList.FindElements(By.ClassName("DataGridRow"));
Assert.That(memberListRows, Has.Count.EqualTo(1));
Window.FindElement(By.WpfId("EditMemberButton")).Click();
var businessSharesInput = Window.FindElement(By.WpfId("BusinessSharesInput"));
Assert.That(businessSharesInput, Is.Not.Null);
var businessShares = int.Parse(businessSharesInput.Text);
businessSharesInput.Clear();
businessSharesInput.SendKeys($"{businessShares + 5}");
Window.FindElement(By.WpfId("SaveButton")).Click();
var newBusinessShares = int.Parse(businessSharesInput.Text);
Assert.That(newBusinessShares, Is.EqualTo(businessShares + 5));
}
[Test]
public void Test_3_DeleteMember() {
Window!.FindElement(By.WpfId("SearchInput")).SendKeys("9999");
Thread.Sleep(500);
var memberList = Window.FindElement(By.WpfId("MemberList"));
Assert.That(memberList, Is.Not.Null);
var memberListRows = memberList.FindElements(By.ClassName("DataGridRow"));
Assert.That(memberListRows, Has.Count.EqualTo(1));
var memberListRow = memberListRows.First();
Assert.Multiple(() => {
Assert.That(memberListRow, Is.Not.Null);
Assert.That(memberListRow.FindElement(By.Name("9999 ")), Is.Not.Null);
Assert.That(memberListRow.FindElement(By.Name("Norbert")), Is.Not.Null);
Assert.That(memberListRow.FindElement(By.Name("Neuling")), Is.Not.Null);
});
Window.FindElement(By.WpfId("DeleteMemberButton")).Click();
var dialog = Session.CreateWindowDriver("DeleteMemberDialog");
dialog.FindElement(By.WpfId("NameInput")).SendKeys("9999 Ing. Norbert Neuling jun.");
dialog.FindElement(By.WpfId("ConfirmButton")).Click();
memberListRows = memberList.FindElements(By.ClassName("DataGridRow"));
Assert.That(memberListRows, Has.Count.EqualTo(0));
}
}
}

37
Tests/E2ETests/Setup.cs Normal file
View File

@ -0,0 +1,37 @@
using Elwig.Helpers;
using System.Reflection;
namespace Tests.E2ETests {
[SetUpFixture]
public static class Setup {
private static WinAppDriver? Driver;
[OneTimeSetUp]
public static void SetupWinAppDriver() {
Driver = new();
}
[OneTimeSetUp]
public static async Task SetupDatabase() {
if (File.Exists(Utils.TestDatabasePath)) File.Delete(Utils.TestDatabasePath);
using var cnx = await AppDbContext.ConnectAsync($"Data Source=\"{Utils.TestDatabasePath}\"; Mode=ReadWriteCreate; Foreign Keys=True; Cache=Default");
await AppDbContext.ExecuteEmbeddedScript(cnx, Assembly.GetExecutingAssembly(), "Tests.Resources.Sql.Create.sql");
await AppDbContext.ExecuteEmbeddedScript(cnx, Assembly.GetExecutingAssembly(), "Tests.Resources.Sql.Insert.sql");
await AppDbContext.ExecuteEmbeddedScript(cnx, Assembly.GetExecutingAssembly(), "Tests.Resources.Sql.E2EInsert.sql");
}
[OneTimeTearDown]
public static void TeardownWinAppDriver() {
Driver?.Dispose();
}
[OneTimeTearDown]
public static void TeardownDatabase() {
try {
// FIXME not working - other process using file
File.Delete(Utils.TestDatabasePath);
} catch { }
}
}
}

31
Tests/E2ETests/Utils.cs Normal file
View File

@ -0,0 +1,31 @@
using OpenQA.Selenium;
using OpenQA.Selenium.Appium;
namespace Tests.E2ETests {
public static class Utils {
public const int WINDOW_OPEN_SLEEP = 2000;
public static readonly string ApplicationPath = Path.GetFullPath(@"..\..\..\..\Elwig\bin\Debug\net8.0-windows\Elwig.exe");
public static readonly string ConfigPath = Path.GetFullPath(@"..\..\..\..\Tests\config.test.ini");
public static readonly string TestDatabasePath = Path.GetFullPath(@"..\..\..\..\Tests\ElwigTestDB.sqlite3");
public static void SelectItem(this IWebElement element, int count) {
element.Click();
element.SendKeys(string.Concat(Enumerable.Repeat(Keys.Down, count)));
element.SendKeys(Keys.Enter);
}
public static void SelectItem(this IWebElement element, string text) {
element.Click();
element.SendKeys(text);
element.SendKeys(Keys.Enter);
}
}
public class By : OpenQA.Selenium.By {
public static OpenQA.Selenium.By WpfId(string wpfName) {
return new ByAccessibilityId(wpfName);
}
}
}

View File

@ -0,0 +1,27 @@
using System.Diagnostics;
namespace Tests.E2ETests {
public class WinAppDriver : IDisposable {
public const string WinAppDriverUrl = "http://127.0.0.1:4723";
private const string WinAppDriverPath = @"C:\Program Files (x86)\Windows Application Driver\WinAppDriver.exe";
private readonly Process WinAppDriverProcess;
public WinAppDriver() {
WinAppDriverProcess = Process.Start(new ProcessStartInfo(WinAppDriverPath) {
//UseShellExecute = true,
//Verb = "runas", // run as administrator
RedirectStandardInput = true,
EnvironmentVariables = {
// { "DX.UITESTINGENABLED", "1" },
}
})!;
}
public void Dispose() {
GC.SuppressFinalize(this);
WinAppDriverProcess.StandardInput.WriteLine("");
WinAppDriverProcess.Dispose();
}
}
}

View File

@ -31,9 +31,9 @@ INSERT INTO season (year, currency, min_kg_per_bs, max_kg_per_bs, penalty_per_kg
(2020, 'EUR', 1000, 2000, NULL, NULL, NULL, NULL, NULL),
(2021, 'EUR', 2000, 4000, NULL, NULL, NULL, NULL, NULL);
INSERT INTO modifier (year, modid, ordering, name, abs, rel, standard, quick_select) VALUES
(2020, 'S', 0, 'Geschädigte Trauben', NULL, -0.1, FALSE, FALSE),
(2020, 'A', 0, 'Keine Voranmeldung', -1000, NULL, FALSE, FALSE);
INSERT INTO modifier (year, modid, ordering, name, abs, rel, active) VALUES
(2020, 'S', 0, 'Geschädigte Trauben', NULL, -0.1, TRUE),
(2020, 'A', 0, 'Keine Voranmeldung', -1000, NULL, TRUE);
-- Test 01
INSERT INTO delivery (mgnr, year, did, date, time, zwstid, lnr) VALUES

View File

@ -9,9 +9,9 @@ INSERT INTO wine_attribute (attrid, name, active, max_kg_per_ha, strict, fill_lo
INSERT INTO season (year, currency, min_kg_per_bs, max_kg_per_bs, penalty_per_kg, penalty_amount, penalty_none, start_date, end_date) VALUES
(2020, 'EUR', 1000, 2000, NULL, NULL, NULL, NULL, NULL);
INSERT INTO modifier (year, modid, ordering, name, abs, rel, standard, quick_select) VALUES
(2020, 'S', 0, 'Geschädigte Trauben', NULL, -0.1, FALSE, FALSE),
(2020, 'A', 0, 'Keine Voranmeldung', -1000, NULL, FALSE, FALSE);
INSERT INTO modifier (year, modid, ordering, name, abs, rel, active) VALUES
(2020, 'S', 0, 'Geschädigte Trauben', NULL, -0.1, TRUE),
(2020, 'A', 0, 'Keine Voranmeldung', -1000, NULL, TRUE);
INSERT INTO delivery (mgnr, year, did, date, time, zwstid, lnr) VALUES
(101, 2020, 1, '2020-10-01', '09:03:12', 'X', 1),

View File

@ -0,0 +1,9 @@
-- inserts for E2ETests
INSERT INTO wine_cultivation (cultid, name, description) VALUES
('KIP', 'KIP', 'Kontrollierte Integrierte Produktion'),
('B', 'Bio', 'AT-BIO-302');
INSERT INTO wine_attribute (attrid, name, active, max_kg_per_ha, strict, fill_lower) VALUES
('K', 'Kabinett', TRUE, NULL, FALSE, 0),
('D', 'DAC', TRUE, 7500, FALSE, 0);

View File

@ -42,7 +42,9 @@ INSERT INTO client_parameter (param, value) VALUES
('CLIENT_PLZ', '2120'),
('CLIENT_ORT', 'Wolkersdorf'),
('CLIENT_ADDRESS', 'Genossenschaftsstraße 1'),
('CLIENT_IBAN', 'AT12 3456 7890 1234 5678'),
('CLIENT_IBAN', 'AT11 1234 5678 9012 3456'),
('CLIENT_LFBISNR', '0123455'),
('CLIENT_USTIDNR', 'ATU12345675'),
('TEXT_DELIVERYNOTE', 'Ich bin der Text, der auf einem Traubenübernahmeschein steht!');
INSERT INTO branch (zwstid, name, country, postal_dest, address) VALUES
@ -64,11 +66,11 @@ INSERT INTO wb_kg (kgnr, glnr) VALUES
(15216, 2),
(15224, 2);
INSERT INTO member (mgnr, given_name, family_name, zwstid, volllieferant, buchführend, country, postal_dest, address, default_kgnr, iban, lfbis_nr) VALUES
(101, 'Max', 'Mustermann', 'X', FALSE, FALSE, 40, 222303524, 'Winzerstraße 1', 06109, 'AT123456789012345678', '1472583'),
(102, 'Wernhardt', 'Weinbauer', 'X', FALSE, FALSE, 40, 222303524, 'Winzerstraße 2', 06109, 'AT123456789012345678', '4725836'),
(103, 'Matthäus', 'Musterbauer', 'X', FALSE, FALSE, 40, 212005138, 'Brünner Straße 10', 15224, 'AT123456789012345678', '7258369'),
(104, 'Waltraud', 'Winzer', 'X', FALSE, FALSE, 40, 212005138, 'Wiener Straße 15', 15224, 'AT123456789012345678', '2583691');
INSERT INTO member (mgnr, given_name, family_name, zwstid, volllieferant, buchführend, country, postal_dest, address, default_kgnr, iban, lfbis_nr, ustid_nr) VALUES
(101, 'Max', 'Mustermann', 'X', FALSE, FALSE, 40, 222303524, 'Winzerstraße 1', 06109, 'AT811234567890123457', '0123463', NULL ),
(102, 'Wernhardt', 'Weinbauer', 'X', FALSE, FALSE, 40, 222303524, 'Winzerstraße 2', 06109, 'AT541234567890123458', '0123471', 'ATU12345684'),
(103, 'Matthäus', 'Musterbauer', 'X', FALSE, FALSE, 40, 212005138, 'Brünner Straße 10', 15224, 'AT271234567890123459', '0123480', NULL ),
(104, 'Waltraud', 'Winzer', 'X', FALSE, FALSE, 40, 212005138, 'Wiener Straße 15', 15224, 'AT971234567890123460', '0123498', 'ATU12345693');
INSERT INTO member_billing_address (mgnr, name, country, postal_dest, address) VALUES
(102, 'W&B Weinbauer GesbR', 40, 222303524, 'Winzerstraße 2'),

View File

@ -20,6 +20,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="Appium.WebDriver" Version="4.4.5" />
<PackageReference Include="NReco.PdfRenderer" Version="1.5.3" />
<PackageReference Include="NUnit" Version="4.1.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />

View File

@ -1,7 +1,6 @@
using System.Net.Sockets;
using System.Net;
using System.Text;
using System.IO;
namespace Tests.WeighingTests {
public abstract class MockScale : IDisposable {

View File

@ -0,0 +1,104 @@
namespace Tests.WeighingTests {
public static class ScaleHandlers {
public static (string, bool) Handle_IT3000A(string req, int weight, string? error, int identNr) {
var modes = error?.Split(';') ?? [];
var overloaded = modes.Contains("overloaded");
var moving = modes.Contains("moving");
var invalid = modes.Contains("invalid");
var crc = modes.Contains("crc");
var unit = modes.Contains("unit");
Thread.Sleep(100);
if (invalid) {
return ("abcd\r\n", false);
} else if (!req.StartsWith('<') || !req.EndsWith('>')) {
return ("<31>\r\n", false);
}
req = req[1..^1];
bool incr;
if (req.Length > 3) {
return ("<32>\r\n", false);
} else if (req.StartsWith("RN")) {
incr = true;
} else if (req.StartsWith("RM")) {
incr = false;
} else {
return ("<32>\r\n", false);
}
if (overloaded) {
return ("<12>\r\n", false);
} else if (weight == 0) {
incr = false;
}
if (moving && incr)
return ("<13>\r\n", false);
string data = $"00{(moving ? 1 : 0)}0{new DateTime(2020, 10, 15, 12, 34, 0):dd.MM.yyHH:mm}{(incr ? identNr : 0),4}1" +
$"{weight,8}{0,8}{weight,8}{(unit ? "lb" : "kg")} {"1",3}";
ushort checksum = Elwig.Helpers.Utils.CalcCrc16Modbus(data);
if (crc) checksum += 10;
return ($"<{data}{checksum,8}>\r\n", incr);
}
public static (string, bool) Handle_L246(string req, int weight, string? error, int identNr) {
var modes = error?.Split(';') ?? [];
var overloaded = modes.Contains("overloaded");
var moving = modes.Contains("moving");
var invalid = modes.Contains("invalid");
var crc = modes.Contains("crc");
var unit = modes.Contains("unit");
Thread.Sleep(100);
if (invalid) {
return ("abcd\r\n", false);
} else if (!req.StartsWith('<') || !req.EndsWith('>')) {
return ("<31>\r\n", false);
}
req = req[1..^1];
bool incr;
if (req.Length > 3) {
return ("<32>\r\n", false);
} else if (req.StartsWith("RN")) {
incr = true;
} else if (req.StartsWith("RM")) {
incr = false;
} else {
return ("<32>\r\n", false);
}
if (overloaded)
return ("<12>\r\n", false);
if (moving && incr)
return ("<13>\r\n", false);
string data = $"00{(moving ? 1 : 0)}0{new DateTime(2020, 10, 17, 14, 23, 0):dd.MM.yyHH:mm}{(incr ? identNr : 0),4}1" +
$"{weight,8}{0,8}{weight,8}{(unit ? "lb" : "kg")} {"001",3}";
ushort checksum = Elwig.Helpers.Utils.CalcCrc16Modbus(data);
if (crc) checksum += 10;
return ($"<{data}{checksum,8}>\r\n", incr);
}
public static (string, bool) Handle_L320(int weight, string? error, int identNr) {
var modes = error?.Split(';') ?? [];
var invalid = modes.Contains("invalid");
var unit = modes.Contains("unit");
Thread.Sleep(100);
if (invalid) {
return ("abcd\r\n", false);
}
bool incr = true;
return ($" {new DateTime(2020, 9, 28, 9, 8, 0):dd.MM.yy HH:mm} {identNr,4} {weight,9}{(unit ? "lb" : "kg")} \r\n", incr);
}
}
}

View File

@ -4,49 +4,36 @@ namespace Tests.WeighingTests {
[TestFixture]
public class ScaleTestBadenL320 {
private EventMockScale? Mock;
private AveryEventScale? Scale;
private static (string, bool) ScaleHandler(int weight, string? error, int identNr) {
var modes = error?.Split(';') ?? [];
var invalid = modes.Contains("invalid");
var unit = modes.Contains("unit");
if (invalid) {
return ("abcd\r\n", false);
}
bool incr = true;
return ($" {new DateTime(2020, 9, 28, 9, 8, 0):dd.MM.yy HH:mm} {identNr,4} {weight,9}{(unit ? "lb" : "kg")} \r\n", incr);
}
private EventMockScale Mock;
private AveryEventScale Scale;
[OneTimeSetUp]
public void SetupScale() {
Mock = new EventMockScale(12345, ScaleHandler);
Mock = new EventMockScale(12345, ScaleHandlers.Handle_L320);
Scale = new("1", "L320", "tcp://127.0.0.1:12345");
}
[OneTimeTearDown]
public void TeardownScale() {
Mock?.Dispose();
Scale?.Dispose();
Mock.Dispose();
Scale.Dispose();
}
[SetUp]
public void ResetScale() {
Mock!.IdentNr = 0;
Mock!.Weight = 0;
Mock!.Error = null;
Mock.IdentNr = 0;
Mock.Weight = 0;
Mock.Error = null;
}
[Test]
public async Task Test_01_Normal() {
WeighingResult? res = null;
Scale!.WeighingEvent += (sender, evt) => {
Scale.WeighingEvent += (sender, evt) => {
res = evt.Result;
};
await Mock!.Weigh(2345);
await Mock.Weigh(2345);
await Task.Delay(100);
Assert.That(res, Is.Not.Null);
Assert.That(res, Is.EqualTo(new WeighingResult {
@ -55,7 +42,7 @@ namespace Tests.WeighingTests {
Date = new DateOnly(2020, 9, 28), Time = new TimeOnly(9, 8),
}));
await Mock!.Weigh(4215);
await Mock.Weigh(4215);
await Task.Delay(100);
Assert.That(res, Is.Not.Null);
Assert.That(res, Is.EqualTo(new WeighingResult {

View File

@ -4,78 +4,39 @@ namespace Tests.WeighingTests {
[TestFixture]
class ScaleTestGrInzersdorfL246 {
private MockScale? Mock;
private SysTecITScale? Scale;
private static (string, bool) ScaleHandler(string req, int weight, string? error, int identNr) {
var modes = error?.Split(';') ?? [];
var overloaded = modes.Contains("overloaded");
var moving = modes.Contains("moving");
var invalid = modes.Contains("invalid");
var crc = modes.Contains("crc");
var unit = modes.Contains("unit");
if (invalid) {
return ("abcd\r\n", false);
} else if (!req.StartsWith('<') || !req.EndsWith('>')) {
return ("<31>\r\n", false);
}
req = req[1..^1];
bool incr;
if (req.Length > 3) {
return ("<32>\r\n", false);
} else if (req.StartsWith("RN")) {
incr = true;
} else if (req.StartsWith("RM")) {
incr = false;
} else {
return ("<32>\r\n", false);
}
if (overloaded)
return ("<12>\r\n", false);
if (moving && incr)
return ("<13>\r\n", false);
string data = $"00{(moving ? 1 : 0)}0{new DateTime(2020, 10, 17, 14, 23, 0):dd.MM.yyHH:mm}{(incr ? identNr : 0),4}1" +
$"{weight,8}{0,8}{weight,8}{(unit ? "lb" : "kg")} {"001",3}";
ushort checksum = Elwig.Helpers.Utils.CalcCrc16Modbus(data);
if (crc) checksum += 10;
return ($"<{data}{checksum,8}>\r\n", incr);
}
private MockScale Mock;
private SysTecITScale Scale;
[OneTimeSetUp]
public void SetupScale() {
Mock = new CommandMockScale(12345, ScaleHandler);
Mock = new CommandMockScale(12345, ScaleHandlers.Handle_L246);
Scale = new("1", "L246", "tcp://127.0.0.1:12345");
}
[OneTimeTearDown]
public void TeardownScale() {
Mock?.Dispose();
Scale?.Dispose();
Mock.Dispose();
Scale.Dispose();
}
[SetUp]
public void ResetScale() {
Mock!.IdentNr = 0;
Mock!.Weight = 0;
Mock!.Error = null;
Mock.IdentNr = 0;
Mock.Weight = 0;
Mock.Error = null;
}
[Test]
public async Task Test_01_CurrentWeight() {
Mock!.Weight = 1235;
Mock.Weight = 1235;
Assert.That(await Scale!.GetCurrentWeight(), Is.EqualTo(new WeighingResult {
Weight = 1235, Date = new DateOnly(2020, 10, 17), Time = new TimeOnly(14, 23),
}));
Mock!.Weight = 1240;
Mock.Weight = 1240;
Assert.That(await Scale!.GetCurrentWeight(), Is.EqualTo(new WeighingResult {
Weight = 1240, Date = new DateOnly(2020, 10, 17), Time = new TimeOnly(14, 23),
}));
Mock!.Weight = 1245;
Mock.Weight = 1245;
Assert.That(await Scale!.GetCurrentWeight(), Is.EqualTo(new WeighingResult {
Weight = 1245, Date = new DateOnly(2020, 10, 17), Time = new TimeOnly(14, 23),
}));
@ -83,19 +44,19 @@ namespace Tests.WeighingTests {
[Test]
public async Task Test_02_Normal() {
Mock!.Weight = 1235;
Mock.Weight = 1235;
Assert.That(await Scale!.Weigh(), Is.EqualTo(new WeighingResult {
Weight = 1235, WeighingId = "1",
FullWeighingId = $"2020-10-17/1",
Date = new DateOnly(2020, 10, 17), Time = new TimeOnly(14, 23),
}));
Mock!.Weight = 3335;
Mock.Weight = 3335;
Assert.That(await Scale!.Weigh(), Is.EqualTo(new WeighingResult {
Weight = 3335, WeighingId = "2",
FullWeighingId = $"2020-10-17/2",
Date = new DateOnly(2020, 10, 17), Time = new TimeOnly(14, 23),
}));
Mock!.Weight = 6420;
Mock.Weight = 6420;
Assert.That(await Scale!.Weigh(), Is.EqualTo(new WeighingResult {
Weight = 6420, WeighingId = "3",
FullWeighingId = $"2020-10-17/3",
@ -105,39 +66,39 @@ namespace Tests.WeighingTests {
[Test]
public void Test_03_Moving() {
Mock!.Weight = 1_000;
Mock!.Error = "moving";
Mock.Weight = 1_000;
Mock.Error = "moving";
IOException ex = Assert.ThrowsAsync<IOException>(async () => await Scale!.Weigh());
Assert.That(ex.Message, Contains.Substring("Waage in Bewegung"));
}
[Test]
public void Test_04_Overloaded() {
Mock!.Weight = 10_000;
Mock!.Error = "overloaded";
Mock.Weight = 10_000;
Mock.Error = "overloaded";
IOException ex = Assert.ThrowsAsync<IOException>(async () => await Scale!.Weigh());
Assert.That(ex.Message, Contains.Substring("Waage in Überlast"));
}
[Test]
public void Test_05_InvalidResponse() {
Mock!.Weight = 1_000;
Mock!.Error = "invalid";
Mock.Weight = 1_000;
Mock.Error = "invalid";
Assert.ThrowsAsync<IOException>(async () => await Scale!.Weigh());
}
[Test]
public void Test_06_InvalidCrc() {
Mock!.Weight = 1_000;
Mock!.Error = "crc";
Mock.Weight = 1_000;
Mock.Error = "crc";
IOException ex = Assert.ThrowsAsync<IOException>(async () => await Scale!.Weigh());
Assert.That(ex.Message, Contains.Substring("Invalid CRC16 checksum"));
}
[Test]
public void Test_07_InvalidUnit() {
Mock!.Weight = 1_000;
Mock!.Error = "unit";
Mock.Weight = 1_000;
Mock.Error = "unit";
IOException ex = Assert.ThrowsAsync<IOException>(async () => await Scale!.Weigh());
}
}

View File

@ -4,81 +4,39 @@ namespace Tests.WeighingTests {
[TestFixture]
class ScaleTestMatzenIT3000A {
private MockScale? Mock;
private SysTecITScale? Scale;
private static (string, bool) ScaleHandler(string req, int weight, string? error, int identNr) {
var modes = error?.Split(';') ?? [];
var overloaded = modes.Contains("overloaded");
var moving = modes.Contains("moving");
var invalid = modes.Contains("invalid");
var crc = modes.Contains("crc");
var unit = modes.Contains("unit");
if (invalid) {
return ("abcd\r\n", false);
} else if (!req.StartsWith('<') || !req.EndsWith('>')) {
return ("<31>\r\n", false);
}
req = req[1..^1];
bool incr;
if (req.Length > 3) {
return ("<32>\r\n", false);
} else if (req.StartsWith("RN")) {
incr = true;
} else if (req.StartsWith("RM")) {
incr = false;
} else {
return ("<32>\r\n", false);
}
if (overloaded) {
return ("<12>\r\n", false);
} else if (weight == 0) {
incr = false;
}
if (moving && incr)
return ("<13>\r\n", false);
string data = $"00{(moving ? 1 : 0)}0{new DateTime(2020, 10, 15, 12, 34, 0):dd.MM.yyHH:mm}{(incr ? identNr : 0),4}1" +
$"{weight,8}{0,8}{weight,8}{(unit ? "lb" : "kg")} {"1",3}";
ushort checksum = Elwig.Helpers.Utils.CalcCrc16Modbus(data);
if (crc) checksum += 10;
return ($"<{data}{checksum,8}>\r\n", incr);
}
private MockScale Mock;
private SysTecITScale Scale;
[OneTimeSetUp]
public void SetupScale() {
Mock = new CommandMockScale(12345, ScaleHandler);
Mock = new CommandMockScale(12345, ScaleHandlers.Handle_IT3000A);
Scale = new("1", "IT3000A", "tcp://127.0.0.1:12345");
}
[OneTimeTearDown]
public void TeardownScale() {
Mock?.Dispose();
Scale?.Dispose();
Mock.Dispose();
Scale.Dispose();
}
[SetUp]
public void ResetScale() {
Mock!.IdentNr = 0;
Mock!.Weight = 0;
Mock!.Error = null;
Mock.IdentNr = 0;
Mock.Weight = 0;
Mock.Error = null;
}
[Test]
public async Task Test_01_CurrentWeight() {
Mock!.Weight = 1234;
Mock.Weight = 1234;
Assert.That(await Scale!.GetCurrentWeight(), Is.EqualTo(new WeighingResult {
Weight = 1234, Date = new DateOnly(2020, 10, 15), Time = new TimeOnly(12, 34),
}));
Mock!.Weight = 1235;
Mock.Weight = 1235;
Assert.That(await Scale!.GetCurrentWeight(), Is.EqualTo(new WeighingResult {
Weight = 1235, Date = new DateOnly(2020, 10, 15), Time = new TimeOnly(12, 34),
}));
Mock!.Weight = 1236;
Mock.Weight = 1236;
Assert.That(await Scale!.GetCurrentWeight(), Is.EqualTo(new WeighingResult {
Weight = 1236, Date = new DateOnly(2020, 10, 15), Time = new TimeOnly(12, 34),
}));
@ -86,19 +44,19 @@ namespace Tests.WeighingTests {
[Test]
public async Task Test_02_Normal() {
Mock!.Weight = 1234;
Mock.Weight = 1234;
Assert.That(await Scale!.Weigh(), Is.EqualTo(new WeighingResult {
Weight = 1234, WeighingId = "1",
FullWeighingId = $"2020-10-15/1",
Date = new DateOnly(2020, 10, 15), Time = new TimeOnly(12, 34),
}));
Mock!.Weight = 3333;
Mock.Weight = 3333;
Assert.That(await Scale!.Weigh(), Is.EqualTo(new WeighingResult {
Weight = 3333, WeighingId = "2",
FullWeighingId = $"2020-10-15/2",
Date = new DateOnly(2020, 10, 15), Time = new TimeOnly(12, 34),
}));
Mock!.Weight = 4321;
Mock.Weight = 4321;
Assert.That(await Scale!.Weigh(), Is.EqualTo(new WeighingResult {
Weight = 4321, WeighingId = "3",
FullWeighingId = $"2020-10-15/3",
@ -108,39 +66,39 @@ namespace Tests.WeighingTests {
[Test]
public void Test_03_Moving() {
Mock!.Weight = 1_000;
Mock!.Error = "moving";
Mock.Weight = 1_000;
Mock.Error = "moving";
IOException ex = Assert.ThrowsAsync<IOException>(async () => await Scale!.Weigh());
Assert.That(ex.Message, Contains.Substring("Waage in Bewegung"));
}
[Test]
public void Test_04_Overloaded() {
Mock!.Weight = 10_000;
Mock!.Error = "overloaded";
Mock.Weight = 10_000;
Mock.Error = "overloaded";
IOException ex = Assert.ThrowsAsync<IOException>(async () => await Scale!.Weigh());
Assert.That(ex.Message, Contains.Substring("Waage in Überlast"));
}
[Test]
public void Test_05_InvalidResponse() {
Mock!.Weight = 1_000;
Mock!.Error = "invalid";
Mock.Weight = 1_000;
Mock.Error = "invalid";
Assert.ThrowsAsync<IOException>(async () => await Scale!.Weigh());
}
[Test]
public void Test_06_InvalidCrc() {
Mock!.Weight = 1_000;
Mock!.Error = "crc";
Mock.Weight = 1_000;
Mock.Error = "crc";
IOException ex = Assert.ThrowsAsync<IOException>(async () => await Scale!.Weigh());
Assert.That(ex.Message, Contains.Substring("Invalid CRC16 checksum"));
}
[Test]
public void Test_07_InvalidUnit() {
Mock!.Weight = 1_000;
Mock!.Error = "unit";
Mock.Weight = 1_000;
Mock.Error = "unit";
IOException ex = Assert.ThrowsAsync<IOException>(async () => await Scale!.Weigh());
}
}

View File

@ -4,10 +4,10 @@ namespace Tests.WeighingTests {
[TestFixture]
class ScaleTestWolkersdorfIT6000E {
private MockScale? MockA;
private MockScale? MockB;
private SysTecITScale? ScaleA;
private SysTecITScale? ScaleB;
private MockScale MockA;
private MockScale MockB;
private SysTecITScale ScaleA;
private SysTecITScale ScaleB;
private static (string, bool) ScaleHandler(string req, int weight, string? error, int identNr, string terminalNr) {
var modes = error?.Split(';') ?? [];
@ -58,37 +58,37 @@ namespace Tests.WeighingTests {
[OneTimeTearDown]
public void TeardownScale() {
MockA?.Dispose();
MockB?.Dispose();
ScaleA?.Dispose();
ScaleB?.Dispose();
MockA.Dispose();
MockB.Dispose();
ScaleA.Dispose();
ScaleB.Dispose();
}
[SetUp]
public void ResetScale() {
MockA!.IdentNr = 0;
MockA!.Weight = 0;
MockA!.Error = null;
MockB!.IdentNr = 0;
MockB!.Weight = 0;
MockB!.Error = null;
MockA.IdentNr = 0;
MockA.Weight = 0;
MockA.Error = null;
MockB.IdentNr = 0;
MockB.Weight = 0;
MockB.Error = null;
}
[Test]
public async Task Test_01_CurrentWeight() {
MockA!.Weight = 1234;
MockA.Weight = 1234;
Assert.That(await ScaleA!.GetCurrentWeight(), Is.EqualTo(new WeighingResult {
Weight = 1234, Date = new DateOnly(2020, 10, 8), Time = new TimeOnly(8, 47),
}));
MockB!.Weight = 3456;
MockB.Weight = 3456;
Assert.That(await ScaleB!.GetCurrentWeight(), Is.EqualTo(new WeighingResult {
Weight = 3456, Date = new DateOnly(2020, 10, 8), Time = new TimeOnly(8, 47),
}));
MockA!.Weight = 1236;
MockA.Weight = 1236;
Assert.That(await ScaleA!.GetCurrentWeight(), Is.EqualTo(new WeighingResult {
Weight = 1236, Date = new DateOnly(2020, 10, 8), Time = new TimeOnly(8, 47),
}));
MockB!.Weight = 3457;
MockB.Weight = 3457;
Assert.That(await ScaleB!.GetCurrentWeight(), Is.EqualTo(new WeighingResult {
Weight = 3457, Date = new DateOnly(2020, 10, 8), Time = new TimeOnly(8, 47),
}));
@ -96,25 +96,25 @@ namespace Tests.WeighingTests {
[Test]
public async Task Test_02_Normal() {
MockA!.Weight = 1234;
MockA.Weight = 1234;
Assert.That(await ScaleA!.Weigh(), Is.EqualTo(new WeighingResult {
Weight = 1234, WeighingId = "1",
FullWeighingId = $"2020-10-08/1",
Date = new DateOnly(2020, 10, 8), Time = new TimeOnly(8, 47),
}));
MockB!.Weight = 3456;
MockB.Weight = 3456;
Assert.That(await ScaleB!.Weigh(), Is.EqualTo(new WeighingResult {
Weight = 3456, WeighingId = "1",
FullWeighingId = $"2020-10-08/1",
Date = new DateOnly(2020, 10, 8), Time = new TimeOnly(8, 47),
}));
MockA!.Weight = 4321;
MockA.Weight = 4321;
Assert.That(await ScaleA!.Weigh(), Is.EqualTo(new WeighingResult {
Weight = 4321, WeighingId = "2",
FullWeighingId = $"2020-10-08/2",
Date = new DateOnly(2020, 10, 8), Time = new TimeOnly(8, 47),
}));
MockB!.Weight = 3333;
MockB.Weight = 3333;
Assert.That(await ScaleB!.Weigh(), Is.EqualTo(new WeighingResult {
Weight = 3333, WeighingId = "2",
FullWeighingId = $"2020-10-08/2",
@ -124,39 +124,39 @@ namespace Tests.WeighingTests {
[Test]
public void Test_03_Moving() {
MockA!.Weight = 1_000;
MockA!.Error = "moving";
MockA.Weight = 1_000;
MockA.Error = "moving";
IOException ex = Assert.ThrowsAsync<IOException>(async () => await ScaleA!.Weigh());
Assert.That(ex.Message, Contains.Substring("Waage in Bewegung"));
}
[Test]
public void Test_04_Overloaded() {
MockA!.Weight = 10_000;
MockA!.Error = "overloaded";
MockA.Weight = 10_000;
MockA.Error = "overloaded";
IOException ex = Assert.ThrowsAsync<IOException>(async () => await ScaleA!.Weigh());
Assert.That(ex.Message, Contains.Substring("Waage in Überlast"));
}
[Test]
public void Test_05_InvalidResponse() {
MockA!.Weight = 1_000;
MockA!.Error = "invalid";
MockA.Weight = 1_000;
MockA.Error = "invalid";
Assert.ThrowsAsync<IOException>(async () => await ScaleA!.Weigh());
}
[Test]
public void Test_06_InvalidCrc() {
MockA!.Weight = 1_000;
MockA!.Error = "crc";
MockA.Weight = 1_000;
MockA.Error = "crc";
IOException ex = Assert.ThrowsAsync<IOException>(async () => await ScaleA!.Weigh());
Assert.That(ex.Message, Contains.Substring("Invalid CRC16 checksum"));
}
[Test]
public void Test_07_InvalidUnit() {
MockA!.Weight = 1_000;
MockA!.Error = "unit";
MockA.Weight = 1_000;
MockA.Error = "unit";
IOException ex = Assert.ThrowsAsync<IOException>(async () => await ScaleA!.Weigh());
}
}

19
Tests/config.test.ini Normal file
View File

@ -0,0 +1,19 @@
[general]
debug = true
[database]
file = ElwigTestDB.sqlite3
[scale.1]
type = SysTec-IT
model = IT3000A
connection = tcp://127.0.0.1:12345
required = false
[scale.2]
type = Avery-Async
model = L320
connection = tcp://127.0.0.1:12346
required = false

View File

@ -1 +1 @@
curl --fail -s -L "https://elwig.at/files/create.sql?v=23" -u "elwig:ganzGeheim123!" -o "Resources\Sql\Create.sql"
curl --fail -s -L "https://elwig.at/files/create.sql?v=24" -u "elwig:ganzGeheim123!" -o "Resources\Sql\Create.sql"