- New device kind Ventilation with driver Vallox (WebSocket protocol of the unit's web UI: READ_TABLES/WRITE_DATA, checksum, big-endian table); no migration (enums stored as text) - Read profile incl. remaining minutes, outdoor/supply/extract/exhaust air temperatures, humidity, CO2, fan speed; activate Home/Away/Boost/Fireplace (timed profiles use the duration configured on the unit) - Same SSRF protection as Shelly (shared connection handler), permissions, action log, audit - Admin dialog without generation/channel/credentials for ventilation, device card with values and profile buttons - Tests: unit (protocol vs. reference bytes, driver, loopback WebSocket, SSRF, rules), bUnit - ADR-0025, roadmap phase 19
459 lines
20 KiB
C#
459 lines
20 KiB
C#
using FamilyPlanner.Domain;
|
||
using FamilyPlanner.Domain.Home;
|
||
using FamilyPlanner.Shared;
|
||
using FamilyPlanner.Shared.Home;
|
||
using FamilyPlanner.Web.Client.Components.House;
|
||
using FamilyPlanner.Web.Client.Services;
|
||
using Microsoft.Extensions.DependencyInjection;
|
||
using MudBlazor.Services;
|
||
|
||
namespace FamilyPlanner.Web.Tests.House;
|
||
|
||
/// <summary>Hausverwaltung: Geräteliste und -dialog (lokale Prüfung, Test, Passwort), Freigaben, Räume, Protokoll, Pin-Editor.</summary>
|
||
public sealed class HouseAdminTests : BunitContext
|
||
{
|
||
private readonly FakeHomeApi _api = new();
|
||
|
||
public HouseAdminTests()
|
||
{
|
||
JSInterop.Mode = JSRuntimeMode.Loose;
|
||
Services.AddMudServices();
|
||
Services.AddSingleton<IHomeApi>(_api);
|
||
}
|
||
|
||
private IRenderedComponent<HouseHost> Open<T>(Action<ComponentParameterCollectionBuilder<T>>? parameters = null)
|
||
where T : Microsoft.AspNetCore.Components.IComponent =>
|
||
Render<HouseHost>(p => p.AddChildContent<T>(parameters));
|
||
|
||
// ---------- Geräteliste ----------
|
||
|
||
[Fact]
|
||
public void Empty_device_list_explains_the_next_step()
|
||
{
|
||
var cut = Open<HouseDevicesAdmin>();
|
||
|
||
Assert.Contains("Noch keine Geräte", cut.Find(".fp-device-empty").TextContent);
|
||
}
|
||
|
||
[Fact]
|
||
public void Device_list_shows_connection_shares_and_never_the_password()
|
||
{
|
||
var room = new HomeAdminRoomDto(Guid.NewGuid(), "Flur", 0, 1);
|
||
_api.Rooms.Add(room);
|
||
_api.Devices.Add(FakeHomeApi.AdminDevice("Flurlicht", room: room.Id, hasPassword: true, grants:
|
||
[
|
||
new(Roles.Family, null, HomeAccessLevel.Control),
|
||
new(Roles.Child, null, HomeAccessLevel.View),
|
||
new(null, Guid.NewGuid(), HomeAccessLevel.Control),
|
||
]));
|
||
_api.Devices.Add(FakeHomeApi.AdminDevice("Privat", HomeDeviceKind.Cover));
|
||
|
||
var cut = Open<HouseDevicesAdmin>();
|
||
|
||
var rows = cut.FindAll(".fp-device-table tbody tr");
|
||
Assert.Equal(2, rows.Count);
|
||
Assert.Contains("Flur", rows[0].TextContent);
|
||
Assert.Contains("http://192.168.1.50", rows[0].TextContent);
|
||
Assert.Contains("Gen2 · Kanal 0 · mit Passwort", rows[0].TextContent);
|
||
Assert.Contains("Familie: schalten · Kind: ansehen · 1 Person", rows[0].QuerySelector(".fp-device-grants")!.TextContent);
|
||
Assert.Contains("Nur Administratoren", rows[1].QuerySelector(".fp-device-grants")!.TextContent);
|
||
Assert.Contains("Rollo", rows[1].TextContent);
|
||
}
|
||
|
||
[Fact]
|
||
public void Connection_test_of_a_row_shows_model_firmware_and_duration()
|
||
{
|
||
var device = FakeHomeApi.AdminDevice("Licht");
|
||
_api.Devices.Add(device);
|
||
var cut = Open<HouseDevicesAdmin>();
|
||
|
||
cut.Find(".fp-device-test-row").Click();
|
||
|
||
cut.WaitForAssertion(() => Assert.Contains("Verbindung erfolgreich", cut.Find(".fp-device-testoutcome").TextContent));
|
||
Assert.Contains("SNSW-001X16EU", cut.Find(".fp-device-testoutcome").TextContent);
|
||
Assert.Contains("1.4.4", cut.Find(".fp-device-testoutcome").TextContent);
|
||
Assert.Equal([device.Id], _api.Tested);
|
||
|
||
_api.TestResult = new HomeTestResultDto(false, "Das Gerät ist nicht erreichbar.", null, null, 0);
|
||
cut.Find(".fp-device-test-row").Click();
|
||
cut.WaitForAssertion(() => Assert.Contains("nicht erreichbar", cut.Find(".fp-device-testoutcome").TextContent));
|
||
}
|
||
|
||
[Fact]
|
||
public void Delete_asks_for_confirmation_and_removes_the_device()
|
||
{
|
||
var device = FakeHomeApi.AdminDevice("Alt");
|
||
_api.Devices.Add(device);
|
||
var cut = Open<HouseDevicesAdmin>();
|
||
|
||
cut.Find("button[aria-label='Alt löschen']").Click();
|
||
cut.WaitForElement(".mud-message-box");
|
||
Assert.Contains("Gerät löschen?", cut.Markup);
|
||
cut.FindAll(".mud-message-box button").Single(b => b.TextContent.Contains("Löschen")).Click();
|
||
|
||
cut.WaitForAssertion(() => Assert.Equal([device.Id], _api.Deleted));
|
||
cut.WaitForAssertion(() => Assert.NotEmpty(cut.FindAll(".fp-device-empty")));
|
||
}
|
||
|
||
// ---------- Gerätedialog ----------
|
||
|
||
[Fact]
|
||
public void Device_dialog_validates_locally_including_foreign_addresses()
|
||
{
|
||
var cut = Open<HouseDevicesAdmin>();
|
||
cut.Find(".fp-device-add").Click();
|
||
cut.WaitForElement("#house-device-form");
|
||
|
||
cut.Find("#house-device-form").Submit(); // Name und Adresse fehlen
|
||
cut.WaitForAssertion(() => Assert.Contains("Bitte einen Namen eingeben", cut.Markup));
|
||
Assert.Empty(_api.SavedDevices);
|
||
|
||
cut.Find("#device-name").Change("Licht");
|
||
cut.Find("#device-host").Change("8.8.8.8");
|
||
cut.Find("#house-device-form").Submit();
|
||
cut.WaitForAssertion(() => Assert.Contains("Nur Adressen im Heimnetz sind erlaubt", cut.Markup));
|
||
Assert.Empty(_api.SavedDevices);
|
||
|
||
cut.Find("#device-host").Change("169.254.169.254");
|
||
cut.Find("#house-device-form").Submit();
|
||
cut.WaitForAssertion(() => Assert.Contains("nicht erlaubt", cut.Markup));
|
||
Assert.Empty(_api.SavedDevices);
|
||
}
|
||
|
||
[Fact]
|
||
public void Device_dialog_saves_a_valid_request_with_defaults()
|
||
{
|
||
var cut = Open<HouseDevicesAdmin>();
|
||
cut.Find(".fp-device-add").Click();
|
||
cut.WaitForElement("#house-device-form");
|
||
|
||
cut.Find("#device-name").Change(" Stehlampe ");
|
||
cut.Find("#device-host").Change("192.168.1.77");
|
||
cut.Find("#device-user").Change("admin");
|
||
cut.Find("#device-password").Change("geheim");
|
||
cut.Find("#house-device-form").Submit();
|
||
|
||
cut.WaitForAssertion(() => Assert.Single(_api.SavedDevices));
|
||
var saved = _api.SavedDevices.Single();
|
||
Assert.Equal(("Stehlampe", HomeDeviceKind.Switch, 2, "192.168.1.77", 0, "admin", "geheim", false), (saved.Name, saved.Kind, saved.Generation, saved.Host, saved.Channel, saved.AuthUser, saved.Password, saved.ClearPassword));
|
||
cut.WaitForAssertion(() => Assert.Single(cut.FindAll(".fp-device-table tbody tr")));
|
||
Assert.DoesNotContain("geheim", cut.Markup);
|
||
}
|
||
|
||
[Fact]
|
||
public void Server_validation_problems_are_shown_in_the_dialog()
|
||
{
|
||
_api.SaveProblem = new ApiProblem { Status = 400, Errors = new() { ["RoomId"] = ["Der Raum existiert nicht mehr."] } };
|
||
var cut = Open<HouseDevicesAdmin>();
|
||
cut.Find(".fp-device-add").Click();
|
||
cut.WaitForElement("#house-device-form");
|
||
cut.Find("#device-name").Change("Licht");
|
||
cut.Find("#device-host").Change("192.168.1.5");
|
||
|
||
cut.Find("#house-device-form").Submit();
|
||
|
||
cut.WaitForAssertion(() => Assert.Contains("Der Raum existiert nicht mehr", cut.Markup));
|
||
}
|
||
|
||
[Fact]
|
||
public void Edit_dialog_keeps_the_password_unless_replaced_and_never_shows_it()
|
||
{
|
||
_api.Devices.Add(FakeHomeApi.AdminDevice("Mit Passwort", hasPassword: true));
|
||
var cut = Open<HouseDevicesAdmin>();
|
||
|
||
cut.Find("button[aria-label='Mit Passwort bearbeiten']").Click();
|
||
cut.WaitForElement("#house-device-form");
|
||
Assert.Equal(string.Empty, cut.Find("#device-password").GetAttribute("value") ?? string.Empty);
|
||
Assert.Contains("leer lassen = unverändert", cut.Markup);
|
||
Assert.Equal("admin", cut.Find("#device-user").GetAttribute("value"));
|
||
cut.Find("#device-name").Change("Umbenannt");
|
||
cut.Find("#house-device-form").Submit();
|
||
|
||
cut.WaitForAssertion(() => Assert.Single(_api.SavedDevices));
|
||
var saved = _api.SavedDevices.Single();
|
||
Assert.Equal(("Umbenannt", null, false), (saved.Name, saved.Password, saved.ClearPassword));
|
||
Assert.NotNull(saved.Version); // Versionsprüfung wird mitgeschickt
|
||
Assert.True(_api.Devices[0].HasPassword);
|
||
}
|
||
|
||
[Fact]
|
||
public void Ventilation_dialog_hides_shelly_fields_and_saves_with_the_vallox_driver()
|
||
{
|
||
var device = FakeHomeApi.ValloxDevice("Lüftung");
|
||
_api.Devices.Add(device);
|
||
var cut = Open<HouseDevicesAdmin>();
|
||
Assert.Contains("Vallox MV", cut.Find(".fp-device-table tbody tr").TextContent);
|
||
|
||
cut.Find("button[aria-label='Lüftung bearbeiten']").Click();
|
||
cut.WaitForElement("#house-device-form");
|
||
|
||
Assert.Single(cut.FindAll(".fp-device-vallox-hint"));
|
||
Assert.Empty(cut.FindAll(".fp-device-generation, #device-channel, #device-user, #device-password"));
|
||
cut.Find(".fp-device-test").Click();
|
||
cut.WaitForAssertion(() => Assert.Single(_api.DraftTests));
|
||
var draft = _api.DraftTests.Single();
|
||
Assert.Equal((HomeDriverType.Vallox, 1, 0, null, null), (draft.Driver, draft.Generation, draft.Channel, draft.AuthUser, draft.Password));
|
||
|
||
cut.Find("#house-device-form").Submit();
|
||
cut.WaitForAssertion(() => Assert.Single(_api.SavedDevices));
|
||
var saved = _api.SavedDevices.Single();
|
||
Assert.Equal((HomeDeviceKind.Ventilation, HomeDriverType.Vallox, 1, "http://192.168.1.60", 0, null), (saved.Kind, saved.Driver, saved.Generation, saved.Host, saved.Channel, saved.AuthUser));
|
||
}
|
||
|
||
[Fact]
|
||
public void Dialog_test_button_sends_the_draft_with_the_device_id_and_shows_the_result()
|
||
{
|
||
var device = FakeHomeApi.AdminDevice("Gesichert", hasPassword: true);
|
||
_api.Devices.Add(device);
|
||
var cut = Open<HouseDevicesAdmin>();
|
||
cut.Find("button[aria-label='Gesichert bearbeiten']").Click();
|
||
cut.WaitForElement("#house-device-form");
|
||
|
||
cut.Find("#device-channel").Change("1");
|
||
cut.Find(".fp-device-test").Click();
|
||
|
||
cut.WaitForAssertion(() => Assert.Contains("Verbindung erfolgreich", cut.Find(".fp-device-testresult").TextContent));
|
||
var draft = _api.DraftTests.Single();
|
||
Assert.Equal((device.Id, "http://192.168.1.50", 1, null), (draft.DeviceId, draft.Host, draft.Channel, draft.Password)); // gespeichertes Passwort bleibt auf dem Server
|
||
}
|
||
|
||
[Fact]
|
||
public void Dialog_test_with_an_invalid_address_does_not_call_the_server()
|
||
{
|
||
var cut = Open<HouseDevicesAdmin>();
|
||
cut.Find(".fp-device-add").Click();
|
||
cut.WaitForElement("#house-device-form");
|
||
cut.Find("#device-host").Change("example.com");
|
||
|
||
cut.Find(".fp-device-test").Click();
|
||
|
||
cut.WaitForAssertion(() => Assert.Contains("Nur Adressen im Heimnetz", cut.Markup));
|
||
Assert.Empty(_api.DraftTests);
|
||
}
|
||
|
||
// ---------- Freigaben ----------
|
||
|
||
[Fact]
|
||
public void Access_dialog_shows_existing_shares_and_saves_removals()
|
||
{
|
||
var anna = FakeHomeApi.User("Anna");
|
||
var tom = FakeHomeApi.User("Tom");
|
||
_api.Users.AddRange([anna, tom, FakeHomeApi.User("Chefin", Roles.Admin)]);
|
||
var device = FakeHomeApi.AdminDevice("Licht", grants: [new(Roles.Family, null, HomeAccessLevel.Control), new(null, anna.Id, HomeAccessLevel.View)]);
|
||
_api.Devices.Add(device);
|
||
var cut = Open<HouseDevicesAdmin>();
|
||
|
||
cut.Find(".fp-device-access").Click();
|
||
cut.WaitForElement("#house-access-form");
|
||
Assert.Equal(3, cut.FindAll(".fp-access-role").Count); // Familie, Kind, Gast – nie Admin
|
||
Assert.Single(cut.FindAll(".fp-access-person"));
|
||
Assert.Contains("Anna", cut.Find(".fp-access-person").TextContent);
|
||
cut.Find("button[aria-label='Freigabe für Anna entfernen']").Click();
|
||
cut.WaitForAssertion(() => Assert.Empty(cut.FindAll(".fp-access-person")));
|
||
cut.Find("#house-access-form").Submit();
|
||
|
||
cut.WaitForAssertion(() => Assert.Single(_api.SavedAccess));
|
||
var (id, request) = _api.SavedAccess.Single();
|
||
Assert.Equal(device.Id, id);
|
||
Assert.Equal([new HomeGrantDto(Roles.Family, null, HomeAccessLevel.Control)], request.Grants);
|
||
}
|
||
|
||
// ---------- Räume ----------
|
||
|
||
[Fact]
|
||
public void Rooms_can_be_added_and_deleted_after_confirmation()
|
||
{
|
||
_api.Rooms.Add(new HomeAdminRoomDto(Guid.NewGuid(), "Küche", 0, 2));
|
||
var changes = 0;
|
||
var cut = Open<HouseRoomsAdmin>(p => p.Add(c => c.OnChanged, Microsoft.AspNetCore.Components.EventCallback.Factory.Create(this, () => changes++)));
|
||
|
||
Assert.Single(cut.FindAll(".fp-room-table tbody tr"));
|
||
Assert.Contains("2", cut.Find(".fp-room-table tbody tr").TextContent);
|
||
|
||
cut.Find(".fp-room-add").Click();
|
||
cut.WaitForElement("#house-name-form");
|
||
cut.Find("#house-name").Change("Bad");
|
||
cut.Find("#house-name-form").Submit();
|
||
cut.WaitForAssertion(() => Assert.Equal(2, cut.FindAll(".fp-room-table tbody tr").Count));
|
||
Assert.Equal("Bad", _api.SavedRooms.Single().Name);
|
||
Assert.Equal(1, changes);
|
||
|
||
cut.Find("button[aria-label='Küche löschen']").Click();
|
||
cut.WaitForElement(".mud-message-box");
|
||
cut.FindAll(".mud-message-box button").Single(b => b.TextContent.Contains("Löschen")).Click();
|
||
cut.WaitForAssertion(() => Assert.Single(cut.FindAll(".fp-room-table tbody tr")));
|
||
Assert.Equal(2, changes);
|
||
}
|
||
|
||
[Fact]
|
||
public void Room_dialog_requires_a_name()
|
||
{
|
||
var cut = Open<HouseRoomsAdmin>();
|
||
cut.Find(".fp-room-add").Click();
|
||
cut.WaitForElement("#house-name-form");
|
||
|
||
cut.Find("#house-name-form").Submit();
|
||
|
||
cut.WaitForAssertion(() => Assert.Contains("Bitte einen Namen eingeben", cut.Markup));
|
||
Assert.Empty(_api.SavedRooms);
|
||
}
|
||
|
||
// ---------- Protokoll ----------
|
||
|
||
[Fact]
|
||
public void Action_log_shows_who_did_what_and_failures_with_text()
|
||
{
|
||
var device = Guid.NewGuid();
|
||
_api.Log.Add(new HomeActionLogDto(new DateTimeOffset(2026, 10, 12, 8, 0, 0, TimeSpan.Zero), "Mama", device, "Flurlicht", HomeCommand.TurnOn, true, null, 85));
|
||
_api.Log.Add(new HomeActionLogDto(new DateTimeOffset(2026, 10, 12, 7, 0, 0, TimeSpan.Zero), "Lena", device, "Rollo", HomeCommand.Close, false, "Das Gerät ist nicht erreichbar.", 3000));
|
||
|
||
var cut = Open<HouseLogView>();
|
||
|
||
cut.WaitForAssertion(() => Assert.Equal(2, cut.FindAll(".fp-house-log tbody tr").Count));
|
||
var rows = cut.FindAll(".fp-house-log tbody tr");
|
||
Assert.Contains("Mama", rows[0].TextContent);
|
||
Assert.Contains("Einschalten", rows[0].TextContent);
|
||
Assert.Contains("OK · 85 ms", rows[0].QuerySelector(".fp-log-ok")!.TextContent);
|
||
Assert.Contains("10:00", rows[0].TextContent); // Europe/Berlin (Sommerzeit)
|
||
Assert.Contains("Fehlgeschlagen: Das Gerät ist nicht erreichbar.", rows[1].QuerySelector(".fp-log-failed")!.TextContent);
|
||
}
|
||
|
||
// ---------- Pin-Editor ----------
|
||
|
||
private (HomeAdminPlanDto Plan, HomeAdminDeviceDto A, HomeAdminDeviceDto B) PlanSetup()
|
||
{
|
||
var a = FakeHomeApi.AdminDevice("Licht");
|
||
var b = FakeHomeApi.AdminDevice("Steckdose", HomeDeviceKind.Socket);
|
||
_api.Devices.AddRange([a, b]);
|
||
var plan = new HomeAdminPlanDto(Guid.NewGuid(), "Erdgeschoss", 0, 2, [new HomePinDto(a.Id, 0.5, 0.5)]);
|
||
_api.Plans.Add(plan);
|
||
return (plan, a, b);
|
||
}
|
||
|
||
[Fact]
|
||
public void Pin_editor_lists_all_devices_with_their_placement_state_and_positions_pins()
|
||
{
|
||
var (plan, a, b) = PlanSetup();
|
||
|
||
var cut = Open<PlanPinEditor>(p => p.Add(c => c.Plan, plan).Add(c => c.Devices, [a, b]));
|
||
|
||
var devices = cut.FindAll(".fp-pin-device");
|
||
Assert.Equal(2, devices.Count);
|
||
Assert.Contains("platziert", devices[0].QuerySelector(".fp-pin-device-state")!.TextContent);
|
||
Assert.Contains("nicht platziert", devices[1].QuerySelector(".fp-pin-device-state")!.TextContent);
|
||
var pin = Assert.Single(cut.FindAll(".fp-pin-edit"));
|
||
Assert.Equal("left:50%;top:50%", pin.GetAttribute("style"));
|
||
Assert.Equal(a.Id.ToString(), pin.GetAttribute("data-device"));
|
||
Assert.Contains("Pfeiltasten", pin.GetAttribute("aria-label"));
|
||
Assert.True(cut.Find(".fp-pin-save").HasAttribute("disabled")); // noch nichts geändert
|
||
Assert.Contains(JSInterop.Invocations, i => i.Identifier == "fpHome.attach");
|
||
}
|
||
|
||
[Fact]
|
||
public void Placing_a_device_by_clicking_the_plan_and_saving_sends_relative_coordinates()
|
||
{
|
||
var (plan, a, b) = PlanSetup();
|
||
var cut = Open<PlanPinEditor>(p => p.Add(c => c.Plan, plan).Add(c => c.Devices, [a, b]));
|
||
var editor = cut.FindComponent<PlanPinEditor>();
|
||
|
||
cut.FindAll(".fp-pin-device")[1].Click(); // Steckdose wählen
|
||
cut.InvokeAsync(() => editor.Instance.PlanClicked(0.3333333, 0.75)); // Klick aufs Bild (kommt vom Skript)
|
||
|
||
cut.WaitForAssertion(() => Assert.Equal(2, cut.FindAll(".fp-pin-edit").Count));
|
||
Assert.Equal("33", cut.Find(".fp-pin-x input").GetAttribute("value"));
|
||
Assert.Equal("75", cut.Find(".fp-pin-y input").GetAttribute("value"));
|
||
cut.Find(".fp-pin-save").Click();
|
||
|
||
cut.WaitForAssertion(() => Assert.Single(_api.SavedPlacements));
|
||
var (planId, request) = _api.SavedPlacements.Single();
|
||
Assert.Equal(plan.Id, planId);
|
||
Assert.Equal(2, request.Pins.Count);
|
||
var placed = request.Pins.Single(p => p.DeviceId == b.Id);
|
||
Assert.Equal((0.3333, 0.75), (placed.X, placed.Y));
|
||
Assert.Contains("Positionen gespeichert", cut.Find(".mud-snackbar").TextContent);
|
||
}
|
||
|
||
[Fact]
|
||
public void Dragging_and_arrow_keys_move_the_pin_and_stay_inside_the_plan()
|
||
{
|
||
var (plan, a, b) = PlanSetup();
|
||
var cut = Open<PlanPinEditor>(p => p.Add(c => c.Plan, plan).Add(c => c.Devices, [a, b]));
|
||
var editor = cut.FindComponent<PlanPinEditor>();
|
||
|
||
cut.InvokeAsync(() => editor.Instance.PinMoved(a.Id.ToString(), 0.2, 0.9));
|
||
cut.WaitForAssertion(() => Assert.Equal("left:20%;top:90%", cut.Find(".fp-pin-edit").GetAttribute("style")));
|
||
|
||
cut.InvokeAsync(() => editor.Instance.PinNudged(a.Id.ToString(), 0.05, 0.05));
|
||
cut.WaitForAssertion(() => Assert.Equal("left:25%;top:95%", cut.Find(".fp-pin-edit").GetAttribute("style")));
|
||
|
||
cut.InvokeAsync(() => editor.Instance.PinNudged(a.Id.ToString(), 0, 0.5));
|
||
cut.WaitForAssertion(() => Assert.Equal("left:25%;top:100%", cut.Find(".fp-pin-edit").GetAttribute("style")));
|
||
}
|
||
|
||
[Fact]
|
||
public void Removing_a_pin_and_resetting_works_and_clicks_without_selection_are_ignored()
|
||
{
|
||
var (plan, a, b) = PlanSetup();
|
||
var cut = Open<PlanPinEditor>(p => p.Add(c => c.Plan, plan).Add(c => c.Devices, [a, b]));
|
||
var editor = cut.FindComponent<PlanPinEditor>();
|
||
|
||
cut.InvokeAsync(() => editor.Instance.PlanClicked(0.1, 0.1)); // nichts gewählt
|
||
Assert.Single(cut.FindAll(".fp-pin-edit"));
|
||
|
||
cut.FindAll(".fp-pin-device")[0].Click();
|
||
cut.Find(".fp-pin-remove").Click();
|
||
cut.WaitForAssertion(() => Assert.Empty(cut.FindAll(".fp-pin-edit")));
|
||
Assert.False(cut.Find(".fp-pin-save").HasAttribute("disabled"));
|
||
|
||
cut.Find(".fp-pin-reset").Click();
|
||
cut.WaitForAssertion(() => Assert.Single(cut.FindAll(".fp-pin-edit")));
|
||
Assert.True(cut.Find(".fp-pin-save").HasAttribute("disabled"));
|
||
Assert.Empty(_api.SavedPlacements);
|
||
}
|
||
|
||
[Fact]
|
||
public void Unplaced_device_can_be_put_in_the_middle_without_a_pointer()
|
||
{
|
||
var (plan, a, b) = PlanSetup();
|
||
var cut = Open<PlanPinEditor>(p => p.Add(c => c.Plan, plan).Add(c => c.Devices, [a, b]));
|
||
|
||
cut.FindAll(".fp-pin-device")[1].Click();
|
||
cut.Find(".fp-pin-center").Click();
|
||
|
||
cut.WaitForAssertion(() => Assert.Equal(2, cut.FindAll(".fp-pin-edit").Count));
|
||
}
|
||
|
||
[Fact]
|
||
public void Plans_admin_lists_plans_with_preview_and_pin_counts()
|
||
{
|
||
var (plan, _, _) = PlanSetup();
|
||
|
||
var cut = Open<HousePlansAdmin>();
|
||
|
||
var card = cut.Find(".fp-plan-card");
|
||
Assert.Contains("Erdgeschoss", card.TextContent);
|
||
Assert.Contains("1 Gerät auf dem Plan", card.TextContent);
|
||
Assert.Equal($"api/home/plans/{plan.Id}/image?v=2", card.QuerySelector(".fp-plan-thumb")!.GetAttribute("src"));
|
||
Assert.Contains("Vorschau Erdgeschoss", card.QuerySelector(".fp-plan-thumb")!.GetAttribute("alt"));
|
||
Assert.Empty(cut.FindAll(".fp-pin-editor"));
|
||
|
||
cut.Find(".fp-plan-pins").Click();
|
||
cut.WaitForAssertion(() => Assert.NotEmpty(cut.FindAll(".fp-pin-editor")));
|
||
}
|
||
|
||
[Fact]
|
||
public void Plan_delete_asks_for_confirmation()
|
||
{
|
||
var (plan, _, _) = PlanSetup();
|
||
var cut = Open<HousePlansAdmin>();
|
||
|
||
cut.FindAll(".fp-plan-card button").Single(b => b.TextContent.Contains("Löschen")).Click();
|
||
cut.WaitForElement(".mud-message-box");
|
||
Assert.Contains("Plan löschen?", cut.Markup);
|
||
cut.FindAll(".mud-message-box button").Single(b => b.TextContent.Contains("Löschen")).Click();
|
||
|
||
cut.WaitForAssertion(() => Assert.Equal([plan.Id], _api.Deleted));
|
||
cut.WaitForAssertion(() => Assert.NotEmpty(cut.FindAll(".fp-plan-empty")));
|
||
}
|
||
}
|