using System.Text.Json.Nodes; using decePubClient.E2E.Support; using Microsoft.Playwright; namespace decePubClient.E2E.Actions { // A vote in another server's poll and an emoji reaction to another server's post, done in decePub and counted on that // server. The first of either asks the persona, once, to accept that both are public (an owner decision). public sealed class VoteAndReactTests(Browser browser) { static readonly Lazy Expected = new(() => Pasture.Town("expect").GetAwaiter().GetResult().AsArray()); static TheoryData Pick(Func wanted, params string[] origins) { var data = new TheoryData(); var personas = Pasture.Personas().Select(p => p.UserName).ToHashSet(); foreach (var origin in origins) { var item = Expected.Value.FirstOrDefault(i => i!["origin"]!.GetValue() == origin && wanted(i) && i["readers"]!.AsArray().Any(r => personas.Contains(r!.GetValue()))); if (item is not null) data.Add(origin, item["ref"]!.GetValue()); } return data; } public static TheoryData Polls() => Pick(i => i["poll"] is JsonArray, "gts", "mastodon", "misskey", "sharkey", "akkoma", "hollo"); public static TheoryData Reactable() => Pick(i => i["visibility"]!.GetValue() is "public" or "unlisted", "misskey", "sharkey", "akkoma"); async Task<(JsonNode Item, Session Session, ILocator Card)> Open(string reference) { var item = Expected.Value.First(i => i!["ref"]!.GetValue() == reference)!; var personas = Pasture.Personas(); var reader = item["readers"]!.AsArray().Select(r => personas.FirstOrDefault(p => p.UserName == r!.GetValue())) .Last(p => p is not null); var session = await SignIn.As(browser, reader); await session.Page.GotoAsync($"{Pasture.Site}/expand/{item["id"]!.GetValue()}"); var card = session.Page.Locator($"article[data-uri='{item["uri"]!.GetValue()}']").First; await card.WaitForAsync(new() { Timeout = 30000 }); return (item, session, card); } // the one-time notice, when this persona has not seen it yet static async Task AcceptNoticeIfAsked(IPage page) { var proceed = page.GetByRole(AriaRole.Button, new() { Name = "Continue" }); try { await proceed.WaitForAsync(new() { Timeout = 3000 }); await proceed.ClickAsync(); } catch (TimeoutException) { } } static async Task Row(string origin, string uri) => (await Pasture.Stored(origin, uri))[uri]!.AsObject(); [Theory] [MemberData(nameof(Polls))] public async Task A_vote_is_counted_on_the_polls_own_server(string origin, string reference) { var (item, session, card) = await Open(reference); await using var _ = session; var uri = item["uri"]!.GetValue(); var choices = card.Locator("[data-poll] input"); Assert.SkipWhen(await choices.CountAsync() == 0, "this persona already voted, or the poll is closed"); await Results.Cell("client.action.vote", origin, reference, async () => { var before = (await Row(origin, uri))["votes"]?.AsArray().Sum(v => v?.GetValue() ?? 0) ?? 0; await choices.First.CheckAsync(); await card.Locator("[data-poll]").GetByRole(AriaRole.Button, new() { Name = "Vote" }).ClickAsync(); await AcceptNoticeIfAsked(session.Page); await Assertions.Expect(card.Locator("[data-poll] input")).ToHaveCountAsync(0, new() { Timeout = 15000 }); Assert.True(await Pasture.Eventually(async () => ((await Row(origin, uri))["votes"]?.AsArray().Sum(v => v?.GetValue() ?? 0) ?? 0) > before, 45), $"the vote never counted on {origin}"); await session.AssertGuards(); }); } [Theory] [MemberData(nameof(Reactable))] public async Task A_reaction_reaches_the_posts_server_and_its_withdrawal_too(string origin, string reference) { var (item, session, card) = await Open(reference); await using var _ = session; var uri = item["uri"]!.GetValue(); const string emoji = "🔥"; await Results.Cell("client.action.react", origin, reference, async () => { static int Count(JsonObject row) => row["reactions"]?[emoji]?.GetValue() ?? 0; 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.ClickAsync(); Assert.True(await Pasture.Eventually(async () => Count(await Row(origin, uri)) == before, 60), $"withdrawing the reaction never reached {origin}"); await session.AssertGuards(); }); } } }