Deleting a post, taking it off the persona's wall, muting or blocking an account (from a card or a profile), leaving a group, dropping a scheduled post and signing out acted at the first tap, a few pixels from their neighbours. They now ask first: new ConfirmService (Ask returns whether the reader confirmed, as PublicNoticeService does) and ConfirmHost in MainLayout, a sheet from the bottom on a phone and a dialog from md, with Cancel and the action named, in red when it deletes or cuts off. Logout is a button that asks, then goes to /logout as before. The reversible ones (unmute, unblock, removing a conversation from the list, a picture from the composer) don't ask; the inline confirms of lists, filters and the login's deletion stay. The desktop tests that delete a reply, take a post off a wall or drop a scheduled post confirm; a phone flow deletes a post after cancelling once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
112 lines
5.9 KiB
C#
112 lines
5.9 KiB
C#
using System.Text.Json.Nodes;
|
|
|
|
using decePubClient.E2E.Support;
|
|
|
|
using Microsoft.Playwright;
|
|
|
|
namespace decePubClient.E2E.Actions
|
|
{
|
|
// Posts written for later, hashtags followed and what Explore shows, each checked against PrivaPub's own answer
|
|
public sealed class DiscoveryTests(Browser browser)
|
|
{
|
|
[Fact]
|
|
public async Task A_post_scheduled_in_the_composer_waits_is_moved_and_is_dropped()
|
|
{
|
|
var persona = Pasture.Personas().First();
|
|
var text = $"later {Guid.NewGuid():N}"[..20];
|
|
await using var session = await SignIn.As(browser, persona);
|
|
var page = session.Page;
|
|
await Results.Cell("client.action.schedule", "privapub", persona.UserName, async () =>
|
|
{
|
|
await page.EvaluateAsync("() => localStorage.removeItem('FeedFilter')");
|
|
await page.GotoAsync($"{Pasture.Site}/");
|
|
await page.GetByRole(AriaRole.Button, new() { Name = "Post" }).First.ClickAsync();
|
|
await page.Locator("textarea").First.FillAsync(text);
|
|
await page.Locator("button[data-schedule]").ClickAsync();
|
|
await page.Locator("input[data-schedule-at]").FillAsync(DateTime.Now.AddHours(2).ToString("yyyy-MM-ddTHH:mm"));
|
|
await page.GetByRole(AriaRole.Button, new() { Name = "Schedule" }).ClickAsync();
|
|
JsonNode waiting = null;
|
|
Assert.True(await Pasture.Eventually(async () => (waiting = (await PersonaApi.Call(persona, "/api/v1/scheduled_statuses"))!.AsArray()
|
|
.FirstOrDefault(s => s!["params"]!["text"]?.GetValue<string>() == text)) is not null, 15), "PrivaPub never kept the scheduled post");
|
|
var id = waiting!["id"]!.GetValue<string>();
|
|
Assert.DoesNotContain((await PersonaApi.Call(persona, $"/api/v1/accounts/{persona.LocalId}/statuses?limit=10"))!.AsArray(),
|
|
s => s!["content"]!.GetValue<string>().Contains(text));
|
|
|
|
await page.GotoAsync($"{Pasture.Site}/scheduled");
|
|
var item = page.Locator($"article[data-scheduled-id='{id}']");
|
|
await Assertions.Expect(item).ToContainTextAsync(text, new() { Timeout = 15000 });
|
|
var later = DateTime.Now.AddHours(5);
|
|
await item.Locator("input[type=datetime-local]").FillAsync(later.ToString("yyyy-MM-ddTHH:mm"));
|
|
await item.GetByRole(AriaRole.Button, new() { Name = "Move" }).ClickAsync();
|
|
Assert.True(await Pasture.Eventually(async () =>
|
|
(await PersonaApi.Call(persona, $"/api/v1/scheduled_statuses/{id}"))?["scheduled_at"]?.GetValue<DateTime>().ToUniversalTime()
|
|
is { } at && Math.Abs((at - later.ToUniversalTime()).TotalMinutes) < 2, 15), "PrivaPub never moved the post");
|
|
await item.GetByRole(AriaRole.Button, new() { Name = "Never publish it" }).ClickAsync();
|
|
await page.Locator("[data-confirm-yes]").ClickAsync();
|
|
await Assertions.Expect(item).ToHaveCountAsync(0, new() { Timeout = 15000 });
|
|
Assert.Null(await PersonaApi.Call(persona, $"/api/v1/scheduled_statuses/{id}"));
|
|
await session.AssertGuards();
|
|
}, page);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_hashtag_is_followed_from_its_feed_tab_and_listed_in_explore()
|
|
{
|
|
var persona = Pasture.Personas().First();
|
|
var tag = $"e2etag{Guid.NewGuid():N}"[..14];
|
|
await using var session = await SignIn.As(browser, persona);
|
|
var page = session.Page;
|
|
try
|
|
{
|
|
await Results.Cell("client.action.follow-tag", "privapub", persona.UserName, async () =>
|
|
{
|
|
// a reader finds the hashtag through search, which opens it as a tab of the feed
|
|
await page.GotoAsync($"{Pasture.Site}/search?q={Uri.EscapeDataString("#" + tag)}");
|
|
await page.GetByRole(AriaRole.Button, new() { Name = "#" + tag }).ClickAsync(new() { Timeout = 30000 });
|
|
var follow = page.Locator("button[data-tag-follow]");
|
|
await Assertions.Expect(follow).ToContainTextAsync($"Follow #{tag}", new() { Timeout = 30000 });
|
|
await follow.ClickAsync();
|
|
await Assertions.Expect(follow).ToContainTextAsync($"Stop following #{tag}", new() { Timeout = 15000 });
|
|
Assert.True((await PersonaApi.Call(persona, $"/api/v1/tags/{tag}"))!["following"]!.GetValue<bool>());
|
|
|
|
await page.GotoAsync($"{Pasture.Site}/explore");
|
|
await page.GetByRole(AriaRole.Tab, new() { Name = "Followed hashtags" }).ClickAsync();
|
|
var row = page.Locator($"[data-tag='{tag}']");
|
|
await row.WaitForAsync(new() { Timeout = 15000 });
|
|
await row.GetByRole(AriaRole.Button, new() { Name = "Unfollow" }).ClickAsync();
|
|
await Assertions.Expect(row).ToHaveCountAsync(0, new() { Timeout = 15000 });
|
|
Assert.False((await PersonaApi.Call(persona, $"/api/v1/tags/{tag}"))!["following"]!.GetValue<bool>());
|
|
await session.AssertGuards();
|
|
}, page);
|
|
}
|
|
finally
|
|
{
|
|
await PersonaApi.Call(persona, $"/api/v1/tags/{tag}/unfollow", HttpMethod.Post);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Explore_shows_what_privapub_says_is_trending_and_its_directory()
|
|
{
|
|
var persona = Pasture.Personas().First();
|
|
var trending = (await PersonaApi.Call(persona, "/api/v1/trends/tags?limit=20"))!.AsArray().Select(t => t!["name"]!.GetValue<string>()).ToList();
|
|
var directory = (await PersonaApi.Call(persona, "/api/v1/directory?order=active&local=false&limit=40"))!.AsArray().Select(a => a!["id"]!.GetValue<string>()).ToList();
|
|
await using var session = await SignIn.As(browser, persona);
|
|
var page = session.Page;
|
|
await Results.Cell("client.render.explore", "privapub", persona.UserName, async () =>
|
|
{
|
|
await page.GotoAsync($"{Pasture.Site}/explore");
|
|
await page.Locator("[data-tag], [data-feed-empty]").First.WaitForAsync(new() { Timeout = 30000 });
|
|
var shownTags = await page.Locator("[data-tag]").EvaluateAllAsync<string[]>("rows => rows.map(r => r.dataset.tag)");
|
|
Assert.Equal(trending, shownTags);
|
|
|
|
await page.GetByRole(AriaRole.Tab, new() { Name = "People" }).ClickAsync();
|
|
await page.Locator("[data-account], [data-feed-empty]").First.WaitForAsync(new() { Timeout = 30000 });
|
|
var shownPeople = await page.Locator("[data-account]").EvaluateAllAsync<string[]>("rows => rows.map(r => r.dataset.account)");
|
|
Assert.Equal(directory, shownPeople);
|
|
await session.AssertGuards();
|
|
}, page);
|
|
}
|
|
}
|
|
}
|