diff --git a/tests/e2e/Actions/VoteAndReactTests.cs b/tests/e2e/Actions/VoteAndReactTests.cs index 8dd4ac9..637d2a4 100644 --- a/tests/e2e/Actions/VoteAndReactTests.cs +++ b/tests/e2e/Actions/VoteAndReactTests.cs @@ -96,15 +96,23 @@ namespace decePubClient.E2E.Actions await Results.Cell("client.action.react", origin, reference, async () => { static int Count(JsonObject row) => row["reactions"]?[emoji]?.GetValue() ?? 0; + var chip = card.Locator("[data-reactions] button").Filter(new() { HasText = emoji }).First; + // a run cut short may have left the persona's reaction: start from the withdrawn state + if (await chip.CountAsync() > 0 && ((await chip.GetAttributeAsync("class")) ?? "").Split(' ').Contains("btn-active")) + { + var left = Count(await Row(origin, uri)); + await chip.ClickAsync(); + await Pasture.Eventually(async () => Count(await Row(origin, uri)) < left, 60); + } var before = Count(await Row(origin, uri)); await card.Locator("footer [title='React']").First.ClickAsync(); await card.GetByRole(AriaRole.Button, new() { Name = emoji, Exact = true }).First.ClickAsync(); await AcceptNoticeIfAsked(session.Page); - var chip = card.Locator("[data-reactions] button").Filter(new() { HasText = emoji }).First; await Assertions.Expect(chip).ToHaveClassAsync(new System.Text.RegularExpressions.Regex("btn-active"), new() { Timeout = 15000 }); Assert.True(await Pasture.Eventually(async () => Count(await Row(origin, uri)) > before, 45), $"the reaction never reached {origin}"); + await chip.ScrollIntoViewIfNeededAsync(); await chip.ClickAsync(); Assert.True(await Pasture.Eventually(async () => Count(await Row(origin, uri)) == before, 60), $"withdrawing the reaction never reached {origin}"); diff --git a/tests/e2e/Support/Results.cs b/tests/e2e/Support/Results.cs index 66a56f3..6c74fb6 100644 --- a/tests/e2e/Support/Results.cs +++ b/tests/e2e/Support/Results.cs @@ -1,13 +1,27 @@ using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.RegularExpressions; namespace decePubClient.E2E.Support { // Each render and action test is also a cell of the town's matrix, appended to the pasture's out/client.jsonl in the - // cell format town.sh check writes, so report.html shows decePub next to the servers. + // cell format town.sh check writes, so report.html shows decePub next to the servers. A failure that an open gap of the + // town's gaps.json names (a peer's own bug, say) is skipped with the gap's id rather than failed, as the report counts + // it an expected failure. public static class Results { static readonly object Gate = new(); static readonly string FilePath = Path.Combine(Pasture.Root, "out", "client.jsonl"); + static readonly Lazy Gaps = new(() => JsonNode.Parse(File.ReadAllText(Path.Combine(Pasture.Root, "town", "gaps.json")))!.AsArray()); + + // the open gap whose match (regular expressions over the cell's fields) fits this cell, if any + static string KnownGap(string feature, string origin) + { + var fields = new Dictionary { ["feature"] = feature, ["origin"] = origin, ["observer"] = "decepub" }; + return Gaps.Value.Where(g => g?["status"]?.GetValue() != "closed" && g?["match"] is JsonObject) + .FirstOrDefault(g => g!["match"]!.AsObject().All(m => Regex.IsMatch(fields.GetValueOrDefault(m.Key, ""), $"^(?:{m.Value!.GetValue()})$"))) + ?["id"]?.GetValue(); + } public static async Task Cell(string feature, string origin, string reference, Func test) => await Cell(feature, origin, reference, test, page: null); @@ -27,6 +41,8 @@ namespace decePubClient.E2E.Support got = e.Message.Length > 400 ? e.Message[..400] : e.Message; if (page is not null) await SignIn.Evidence(page, $"{feature}-{origin}-{reference}".Replace('/', '_').Replace('@', '_')); + if (KnownGap(feature, origin) is { } gap) + Assert.Skip($"known gap {gap}: {got}"); throw; } finally