2026-10-05 02:49:51 +02:00
|
|
|
using System.Text.Json.Nodes;
|
|
|
|
|
|
|
|
|
|
using decePubClient.E2E.Support;
|
|
|
|
|
|
|
|
|
|
using Microsoft.Playwright;
|
|
|
|
|
|
|
|
|
|
namespace decePubClient.E2E.Actions
|
|
|
|
|
{
|
|
|
|
|
// A list in decePub: made on /lists, filled with an account the persona follows (found by its name among the accounts
|
|
|
|
|
// it follows), read as the feed's Lists tab, made exclusive and deleted. Each step is checked against PrivaPub's own
|
|
|
|
|
// answer to the persona's token, so the page and the server agree on the list, its members and its posts.
|
|
|
|
|
public sealed class ListTests(Browser browser)
|
|
|
|
|
{
|
2026-10-05 03:10:27 +02:00
|
|
|
static Task<JsonNode> Api(Pasture.Persona persona, string path, HttpMethod method = null, JsonObject body = null) =>
|
|
|
|
|
PersonaApi.Call(persona, path, method, body);
|
2026-10-05 02:49:51 +02:00
|
|
|
|
|
|
|
|
// an account the persona follows that has written something
|
|
|
|
|
static async Task<JsonNode> Followed(Pasture.Persona persona) =>
|
|
|
|
|
(await Api(persona, $"/api/v1/accounts/{persona.LocalId}/following?limit=80"))!.AsArray()
|
|
|
|
|
.FirstOrDefault(a => a!["statuses_count"]!.GetValue<int>() > 0 && !a["acct"]!.GetValue<string>().StartsWith("st_"));
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task An_account_joins_and_leaves_a_list_from_its_profile()
|
|
|
|
|
{
|
|
|
|
|
var persona = Pasture.Personas().First();
|
|
|
|
|
var member = await Followed(persona);
|
|
|
|
|
Assert.SkipWhen(member is null, $"{persona.UserName} follows nobody who has posted");
|
|
|
|
|
var memberId = member!["id"]!.GetValue<string>();
|
|
|
|
|
var listId = (await Api(persona, "/api/v1/lists", HttpMethod.Post, new JsonObject { ["title"] = $"e2e {Guid.NewGuid():N}"[..12] }))!["id"]!.GetValue<string>();
|
|
|
|
|
await using var session = await SignIn.As(browser, persona);
|
|
|
|
|
var page = session.Page;
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await Results.Cell("client.action.list-member", "privapub", persona.UserName, async () =>
|
|
|
|
|
{
|
|
|
|
|
async Task<bool> InList() =>
|
|
|
|
|
(await Api(persona, $"/api/v1/lists/{listId}/accounts"))!.AsArray().Any(a => a!["id"]!.GetValue<string>() == memberId);
|
|
|
|
|
|
|
|
|
|
await page.GotoAsync($"{Pasture.Site}/account/{memberId}");
|
|
|
|
|
var profile = page.Locator("article[data-account-id]").First;
|
|
|
|
|
await profile.WaitForAsync(new() { Timeout = 30000 });
|
|
|
|
|
await profile.GetByRole(AriaRole.Button, new() { Name = "Lists" }).ClickAsync();
|
|
|
|
|
var box = profile.Locator($"[data-list-id='{listId}'] input");
|
|
|
|
|
await box.CheckAsync();
|
|
|
|
|
Assert.True(await Pasture.Eventually(InList, 15), "the profile's checkbox never put the account in the list");
|
|
|
|
|
await box.UncheckAsync();
|
|
|
|
|
Assert.True(await Pasture.Eventually(async () => !await InList(), 15), "the profile's checkbox never took the account out");
|
|
|
|
|
await session.AssertGuards();
|
|
|
|
|
}, page);
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
await Api(persona, $"/api/v1/lists/{listId}", HttpMethod.Delete);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task A_list_is_made_filled_read_as_a_feed_and_deleted()
|
|
|
|
|
{
|
|
|
|
|
var persona = Pasture.Personas().First();
|
|
|
|
|
var member = await Followed(persona);
|
|
|
|
|
Assert.SkipWhen(member is null, $"{persona.UserName} follows nobody who has posted");
|
|
|
|
|
var memberId = member!["id"]!.GetValue<string>();
|
|
|
|
|
var title = $"e2e {Guid.NewGuid():N}"[..12];
|
|
|
|
|
// lists an earlier failed run left behind
|
|
|
|
|
foreach (var left in (await Api(persona, "/api/v1/lists"))!.AsArray().Where(l => l!["title"]!.GetValue<string>().StartsWith("e2e ")))
|
|
|
|
|
await Api(persona, $"/api/v1/lists/{left!["id"]!.GetValue<string>()}", HttpMethod.Delete);
|
|
|
|
|
await using var session = await SignIn.As(browser, persona);
|
|
|
|
|
var page = session.Page;
|
|
|
|
|
await Results.Cell("client.action.list", "privapub", persona.UserName, async () =>
|
|
|
|
|
{
|
|
|
|
|
await page.GotoAsync($"{Pasture.Site}/lists");
|
|
|
|
|
await page.Locator("[data-list-new] input, input[data-list-new]").First.FillAsync(title);
|
|
|
|
|
await page.GetByRole(AriaRole.Button, new() { Name = "Make a list" }).ClickAsync();
|
|
|
|
|
var article = page.Locator("article[data-list-id]").Filter(new() { HasText = title });
|
|
|
|
|
await article.WaitForAsync(new() { Timeout = 15000 });
|
|
|
|
|
var listId = await article.GetAttributeAsync("data-list-id");
|
|
|
|
|
Assert.Contains((await Api(persona, "/api/v1/lists"))!.AsArray(), l => l!["id"]!.GetValue<string>() == listId);
|
|
|
|
|
|
|
|
|
|
// the list opens once made: find the member among the accounts followed, and add it
|
|
|
|
|
await article.Locator("[data-list-find] input, input[data-list-find]").First.FillAsync(member["username"]!.GetValue<string>());
|
|
|
|
|
await article.Locator("[data-list-find] input, input[data-list-find]").First.PressAsync("Enter");
|
|
|
|
|
var candidate = article.Locator($"[data-candidate-id='{memberId}']");
|
|
|
|
|
await candidate.WaitForAsync(new() { Timeout = 15000 });
|
|
|
|
|
await candidate.GetByRole(AriaRole.Button, new() { Name = "Add" }).ClickAsync();
|
|
|
|
|
await article.Locator($"[data-member-id='{memberId}']").WaitForAsync(new() { Timeout = 15000 });
|
|
|
|
|
Assert.Equal([memberId], (await Api(persona, $"/api/v1/lists/{listId}/accounts"))!.AsArray().Select(a => a!["id"]!.GetValue<string>()));
|
|
|
|
|
|
|
|
|
|
// read as a feed: the cards are the list timeline's first page, as PrivaPub answers it
|
|
|
|
|
await article.GetByRole(AriaRole.Button, new() { Name = "Read the list" }).ClickAsync();
|
|
|
|
|
await page.Locator("article[data-uri], [data-feed-empty]").First.WaitForAsync(new() { Timeout = 30000 });
|
|
|
|
|
var expected = (await Api(persona, $"/api/v1/timelines/list/{listId}?limit=20"))!.AsArray()
|
|
|
|
|
.Select(s => (s!["reblog"] ?? s)["uri"]!.GetValue<string>()).ToHashSet();
|
|
|
|
|
var shown = await page.Locator("article[data-uri]").EvaluateAllAsync<string[]>("cards => cards.map(c => c.dataset.uri)");
|
|
|
|
|
Assert.NotEmpty(expected);
|
|
|
|
|
Assert.Subset(expected, shown.ToHashSet());
|
|
|
|
|
|
|
|
|
|
// exclusive, then deleted
|
|
|
|
|
await page.GotoAsync($"{Pasture.Site}/lists");
|
|
|
|
|
article = page.Locator($"article[data-list-id='{listId}']");
|
|
|
|
|
await article.GetByRole(AriaRole.Button, new() { Name = "Edit" }).ClickAsync();
|
|
|
|
|
await article.GetByLabel("Keep its members out of the home feed").CheckAsync();
|
|
|
|
|
await article.GetByRole(AriaRole.Button, new() { Name = "Save" }).ClickAsync();
|
|
|
|
|
Assert.True(await Pasture.Eventually(async () => (await Api(persona, $"/api/v1/lists/{listId}"))?["exclusive"]?.GetValue<bool>() == true, 15),
|
|
|
|
|
"PrivaPub never made the list exclusive");
|
|
|
|
|
// the icon's glyph (CSS content) is part of a button's name, so names are matched as substrings
|
|
|
|
|
await article.GetByRole(AriaRole.Button, new() { Name = "Delete" }).First.ClickAsync();
|
|
|
|
|
await article.GetByRole(AriaRole.Button, new() { Name = "Delete it for good" }).ClickAsync();
|
|
|
|
|
await Assertions.Expect(article).ToHaveCountAsync(0, new() { Timeout = 15000 });
|
|
|
|
|
Assert.Null(await Api(persona, $"/api/v1/lists/{listId}"));
|
|
|
|
|
await session.AssertGuards();
|
|
|
|
|
}, page);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|