Lists: made on /lists, read as a tab of the feed
A persona's lists (PrivaPub's /api/v1/lists): made, renamed, given a replies policy and made exclusive on /lists, filled from the accounts it follows (accounts/search?following=true), and read as the feed's Lists tab. A followed account's profile has a Lists menu to put it in or take it out. ListService does the calls; the e2e tests check each step against PrivaPub's own answer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
1 parent
011bafb6af
commit
e2491de16e
18 files changed
+914
-9
No files matched your search
@@ -0,0 +1,128 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
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)
|
||||
{
|
||||
static async Task<JsonNode> Api(Pasture.Persona persona, string path, HttpMethod method = null, JsonObject body = null)
|
||||
{
|
||||
using var http = Pasture.Http();
|
||||
using var request = new HttpRequestMessage(method ?? HttpMethod.Get, Pasture.Api + path);
|
||||
if (body is not null)
|
||||
request.Content = new StringContent(body.ToJsonString(), System.Text.Encoding.UTF8, "application/json");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", persona.Token);
|
||||
using var response = await http.SendAsync(request);
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
return null;
|
||||
response.EnsureSuccessStatusCode();
|
||||
return JsonNode.Parse(await response.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user