2026-10-05 01:23:18 +02:00
|
|
|
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<JsonArray> Expected = new(() => Pasture.Town("expect").GetAwaiter().GetResult().AsArray());
|
|
|
|
|
|
|
|
|
|
static TheoryData<string, string> Pick(Func<JsonNode, bool> wanted, params string[] origins)
|
|
|
|
|
{
|
|
|
|
|
var data = new TheoryData<string, string>();
|
|
|
|
|
var personas = Pasture.Personas().Select(p => p.UserName).ToHashSet();
|
|
|
|
|
foreach (var origin in origins)
|
|
|
|
|
{
|
|
|
|
|
var item = Expected.Value.FirstOrDefault(i => i!["origin"]!.GetValue<string>() == origin && wanted(i)
|
|
|
|
|
&& i["readers"]!.AsArray().Any(r => personas.Contains(r!.GetValue<string>())));
|
|
|
|
|
if (item is not null)
|
|
|
|
|
data.Add(origin, item["ref"]!.GetValue<string>());
|
|
|
|
|
}
|
|
|
|
|
return data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static TheoryData<string, string> Polls() =>
|
|
|
|
|
Pick(i => i["poll"] is JsonArray, "gts", "mastodon", "misskey", "sharkey", "akkoma", "hollo");
|
|
|
|
|
|
|
|
|
|
public static TheoryData<string, string> Reactable() =>
|
|
|
|
|
Pick(i => i["visibility"]!.GetValue<string>() 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<string>() == reference)!;
|
|
|
|
|
var personas = Pasture.Personas();
|
|
|
|
|
var reader = item["readers"]!.AsArray().Select(r => personas.FirstOrDefault(p => p.UserName == r!.GetValue<string>()))
|
|
|
|
|
.Last(p => p is not null);
|
|
|
|
|
var session = await SignIn.As(browser, reader);
|
|
|
|
|
await session.Page.GotoAsync($"{Pasture.Site}/expand/{item["id"]!.GetValue<string>()}");
|
|
|
|
|
var card = session.Page.Locator($"article[data-uri='{item["uri"]!.GetValue<string>()}']").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<JsonObject> 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<string>();
|
|
|
|
|
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<int>() ?? 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<int>() ?? 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<string>();
|
|
|
|
|
const string emoji = "🔥";
|
|
|
|
|
await Results.Cell("client.action.react", origin, reference, async () =>
|
|
|
|
|
{
|
|
|
|
|
static int Count(JsonObject row) => row["reactions"]?[emoji]?.GetValue<int>() ?? 0;
|
2026-10-05 06:20:18 +02:00
|
|
|
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);
|
|
|
|
|
}
|
2026-10-05 01:23:18 +02:00
|
|
|
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);
|
|
|
|
|
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}");
|
|
|
|
|
|
2026-10-05 06:20:18 +02:00
|
|
|
await chip.ScrollIntoViewIfNeededAsync();
|
2026-10-05 01:23:18 +02:00
|
|
|
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();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|