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() == text)) is not null, 15), "PrivaPub never kept the scheduled post"); var id = waiting!["id"]!.GetValue(); Assert.DoesNotContain((await PersonaApi.Call(persona, $"/api/v1/accounts/{persona.LocalId}/statuses?limit=10"))!.AsArray(), s => s!["content"]!.GetValue().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().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 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()); 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()); 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()).ToList(); var directory = (await PersonaApi.Call(persona, "/api/v1/directory?order=active&local=false&limit=40"))!.AsArray().Select(a => a!["id"]!.GetValue()).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("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("rows => rows.map(r => r.dataset.account)"); Assert.Equal(directory, shownPeople); await session.AssertGuards(); }, page); } } }