feat(dashboard): add revision-safe interactive widget grid
This commit is contained in:
@@ -28,6 +28,61 @@ public sealed class DashboardPreferencesTests
|
||||
private const string Route = "/api/auth/preferences/dashboard";
|
||||
private const string Layout = "{\"version\":1,\"revision\":0,\"widgets\":[{\"id\":\"cards\",\"visible\":true,\"size\":\"full\"}]}";
|
||||
|
||||
[Fact]
|
||||
public async Task Geometry_roundtrip_preserves_hidden_positions_and_blocks_legacy_downgrade()
|
||||
{
|
||||
await using var host = await Host.Start();
|
||||
using var owner = host.Client("owner");
|
||||
using var other = host.Client("other");
|
||||
const string geometry = "{\"version\":2,\"revision\":0,\"widgets\":[{\"id\":\"attention\",\"visible\":false,\"size\":\"standard\",\"x\":4,\"y\":17,\"w\":8,\"h\":10}]}";
|
||||
Assert.Equal(HttpStatusCode.OK, (await owner.PutAsync(Route, Json(geometry))).StatusCode);
|
||||
var actual = await owner.GetFromJsonAsync<JsonElement>(Route);
|
||||
Assert.Equal(2, actual.GetProperty("version").GetInt32());
|
||||
Assert.Equal(1, actual.GetProperty("revision").GetInt64());
|
||||
var widget = actual.GetProperty("widgets")[0];
|
||||
Assert.False(widget.GetProperty("visible").GetBoolean());
|
||||
Assert.Equal(4, widget.GetProperty("x").GetInt32());
|
||||
Assert.Equal(17, widget.GetProperty("y").GetInt32());
|
||||
Assert.Equal(8, widget.GetProperty("w").GetInt32());
|
||||
Assert.Equal(10, widget.GetProperty("h").GetInt32());
|
||||
Assert.Equal(HttpStatusCode.Conflict, (await owner.PutAsync(Route, Json(Layout.Replace("\"revision\":0", "\"revision\":1")))).StatusCode);
|
||||
Assert.Equal(actual.GetRawText(), (await owner.GetFromJsonAsync<JsonElement>(Route)).GetRawText());
|
||||
Assert.Equal(0, (await other.GetFromJsonAsync<JsonElement>(Route)).GetProperty("revision").GetInt64());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true, 4, HttpStatusCode.BadRequest)]
|
||||
[InlineData(true, 6, HttpStatusCode.OK)]
|
||||
[InlineData(false, 4, HttpStatusCode.OK)]
|
||||
public async Task Visible_geometry_cannot_overlap_but_hidden_positions_and_adjacent_edges_are_supported(bool visible, int x, HttpStatusCode expected)
|
||||
{
|
||||
await using var host = await Host.Start();
|
||||
using var owner = host.Client("owner");
|
||||
var request = new { version = 2, revision = 0, widgets = new[] {
|
||||
new { id = "attention", visible = true, size = "standard", x = 0, y = 0, w = 6, h = 8 },
|
||||
new { id = "funnel", visible, size = "standard", x, y = 0, w = 6, h = 8 },
|
||||
} };
|
||||
Assert.Equal(expected, (await owner.PutAsJsonAsync(Route, request)).StatusCode);
|
||||
Assert.Equal(expected == HttpStatusCode.OK ? 1 : 0, (await owner.GetFromJsonAsync<JsonElement>(Route)).GetProperty("revision").GetInt64());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(-1, 0, 6, 8)]
|
||||
[InlineData(8, 0, 6, 8)]
|
||||
[InlineData(0, 250, 6, 8)]
|
||||
[InlineData(0, 0, 3, 8)]
|
||||
[InlineData(0, 0, 6, 3)]
|
||||
[InlineData(0, 0, 6, 25)]
|
||||
[InlineData(0.5, 0, 6, 8)]
|
||||
public async Task Unbounded_or_fractional_geometry_is_rejected_before_persistence(double x, double y, double w, double h)
|
||||
{
|
||||
await using var host = await Host.Start();
|
||||
using var owner = host.Client("owner");
|
||||
var request = new { version = 2, revision = 0, widgets = new[] { new { id = "attention", visible = true, size = "standard", x, y, w, h } } };
|
||||
Assert.Equal(HttpStatusCode.BadRequest, (await owner.PutAsJsonAsync(Route, request)).StatusCode);
|
||||
Assert.Equal(0, (await owner.GetFromJsonAsync<JsonElement>(Route)).GetProperty("revision").GetInt64());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Http_roundtrip_is_owner_scoped_and_preserves_other_preferences()
|
||||
{
|
||||
@@ -131,10 +186,10 @@ public sealed class DashboardPreferencesTests
|
||||
await using var host = await Host.Start();
|
||||
using var client = host.Client("owner");
|
||||
await using var db = host.Db();
|
||||
const string future = "{\"version\":2,\"widgets\":[{\"id\":\"future-widget\",\"custom\":\"preserve\"}]}";
|
||||
const string future = "{\"version\":3,\"widgets\":[{\"id\":\"future-widget\",\"custom\":\"preserve\"}]}";
|
||||
await db.Database.ExecuteSqlInterpolatedAsync($"UPDATE AspNetUsers SET DashboardLayoutJson={future}, DashboardLayoutRevision=4 WHERE Id='owner'");
|
||||
var response = await client.GetFromJsonAsync<JsonElement>(Route);
|
||||
Assert.Equal(2, response.GetProperty("version").GetInt32());
|
||||
Assert.Equal(3, response.GetProperty("version").GetInt32());
|
||||
Assert.Equal(4, response.GetProperty("revision").GetInt64());
|
||||
Assert.Empty(response.GetProperty("widgets").EnumerateArray());
|
||||
Assert.Equal(HttpStatusCode.Conflict, (await client.PutAsync(Route, Json("{\"version\":1,\"revision\":4,\"widgets\":[]}"))).StatusCode);
|
||||
@@ -157,7 +212,7 @@ public sealed class DashboardPreferencesTests
|
||||
{
|
||||
await preRead.Task.WaitAsync(TimeSpan.FromSeconds(10));
|
||||
await using var db = host.Db();
|
||||
const string future = "{\"version\":2,\"widgets\":[{\"id\":\"future-widget\",\"custom\":\"preserve\"}]}";
|
||||
const string future = "{\"version\":3,\"widgets\":[{\"id\":\"future-widget\",\"custom\":\"preserve\"}]}";
|
||||
await db.Database.ExecuteSqlInterpolatedAsync($"UPDATE AspNetUsers SET DashboardLayoutJson={future}, DashboardLayoutRevision=1 WHERE Id='owner'");
|
||||
releaseBody.TrySetResult();
|
||||
Assert.Equal(HttpStatusCode.Conflict, (await pending).StatusCode);
|
||||
@@ -190,7 +245,7 @@ public sealed class DashboardPreferencesTests
|
||||
[InlineData("{\"version\":1,\"revision\":\"0\",\"widgets\":[]}", 400)]
|
||||
[InlineData("{\"version\":1,\"revision\":9223372036854775807,\"widgets\":[]}", 400)]
|
||||
[InlineData("{\"version\":1,\"revision\":9007199254740991,\"widgets\":[]}", 400)]
|
||||
[InlineData("{\"version\":2,\"revision\":0,\"widgets\":[]}", 400)]
|
||||
[InlineData("{\"version\":3,\"revision\":0,\"widgets\":[]}", 400)]
|
||||
[InlineData("{\"version\":1,\"revision\":-1,\"widgets\":[]}", 400)]
|
||||
[InlineData("{\"version\":1,\"revision\":0,\"widgets\":null}", 400)]
|
||||
[InlineData("{\"version\":1,\"revision\":0,\"widgets\":[{\"id\":\"unknown\",\"visible\":true,\"size\":\"full\"}]}", 400)]
|
||||
|
||||
Reference in New Issue
Block a user