using System.Text.Json.Nodes; using decePubClient.E2E.Support; using Microsoft.Playwright; namespace decePubClient.E2E.Actions { // Media from the composer and the profile, checked on PrivaPub: a picture uploaded as soon as it is picked and posted // with its description, the reminder before a picture without one, a failed upload retried without the rest being sent // again, and an avatar set and removed public sealed class MediaTests(Browser browser) { // a 1×1 PNG static readonly byte[] Png = Convert.FromBase64String("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="); static async Task OpenComposer(IPage page) { await page.GotoAsync($"{Pasture.Site}/"); await page.EvaluateAsync("() => localStorage.removeItem('FeedFilter')"); await page.GotoAsync($"{Pasture.Site}/"); await page.Locator("article[data-uri], [data-feed-empty]").First.WaitForAsync(new() { Timeout = 30000 }); await page.GetByRole(AriaRole.Button, new() { Name = "Post" }).First.ClickAsync(); } static Task Attach(IPage page, string name) => page.Locator("input[data-attach]").SetInputFilesAsync(new FilePayload { Name = name, MimeType = "image/png", Buffer = Png }); static ILocator Submit(IPage page) => page.Locator("form:has(textarea[data-compose]) button[type=submit]").First; static async Task Published(Pasture.Persona persona, string text) { JsonNode latest = null; Assert.True(await Pasture.Eventually(async () => (latest = (await PersonaApi.Call(persona, $"/api/v1/accounts/{persona.LocalId}/statuses?limit=1"))!.AsArray().FirstOrDefault())?["content"] ?.GetValue().Contains(text) == true, 15), "the post was never published"); return latest; } [Fact] public async Task A_picture_is_uploaded_when_picked_and_posted_with_its_description() { var persona = Pasture.Personas().First(); var text = $"a picture {Guid.NewGuid():N}"[..20]; await using var session = await SignIn.As(browser, persona); var page = session.Page; string made = null; try { await Results.Cell("client.action.media", "privapub", persona.UserName, async () => { await OpenComposer(page); await page.Locator("textarea[data-compose]").First.FillAsync(text); await Attach(page, "dot.png"); // uploaded on its own, before anything is published await Assertions.Expect(page.Locator("[data-upload-state=ready]")).ToHaveCountAsync(1, new() { Timeout = 20000 }); await page.Locator("[data-alt-text]").First.FillAsync("a single dot"); await Submit(page).ClickAsync(); var post = await Published(persona, text); made = post["id"]!.GetValue(); Assert.Equal("a single dot", post["media_attachments"]![0]!["description"]!.GetValue()); await session.AssertGuards(); }, page); } finally { if (made is not null) await PersonaApi.Call(persona, $"/api/v1/statuses/{made}", HttpMethod.Delete); } } [Fact] public async Task A_picture_without_a_description_is_asked_about_once() { var persona = Pasture.Personas().First(); var text = $"undescribed {Guid.NewGuid():N}"[..22]; await using var session = await SignIn.As(browser, persona); var page = session.Page; string made = null; try { await Results.Cell("client.action.media-reminder", "privapub", persona.UserName, async () => { await OpenComposer(page); await page.Locator("textarea[data-compose]").First.FillAsync(text); await Attach(page, "dot.png"); await Assertions.Expect(page.Locator("[data-upload-state=ready]")).ToHaveCountAsync(1, new() { Timeout = 20000 }); await Submit(page).ClickAsync(); await Assertions.Expect(page.Locator("[data-alt-reminder]")).ToBeVisibleAsync(); await page.Locator("[data-alt-publish-anyway]").ClickAsync(); var post = await Published(persona, text); made = post["id"]!.GetValue(); Assert.Null(post["media_attachments"]![0]!["description"]?.GetValue()); await session.AssertGuards(); }, page); } finally { if (made is not null) await PersonaApi.Call(persona, $"/api/v1/statuses/{made}", HttpMethod.Delete); } } [Fact] public async Task A_failed_upload_is_retried_alone() { var persona = Pasture.Personas().First(); var text = $"retried {Guid.NewGuid():N}"[..18]; await using var session = await SignIn.As(browser, persona); var page = session.Page; string made = null; try { await Results.Cell("client.action.media-retry", "privapub", persona.UserName, async () => { // the second upload is refused once, as a server would refuse it var uploads = 0; await page.RouteAsync("**/api/v2/media", async route => { if (Interlocked.Increment(ref uploads) == 2) await route.FulfillAsync(new() { Status = 422, ContentType = "application/json", Body = "{\"error\":\"Validation failed: try again\"}" }); else await route.ContinueAsync(); }); await OpenComposer(page); await page.Locator("textarea[data-compose]").First.FillAsync(text); await Attach(page, "first.png"); await Assertions.Expect(page.Locator("[data-upload-state=ready]")).ToHaveCountAsync(1, new() { Timeout = 20000 }); await Attach(page, "second.png"); await Assertions.Expect(page.Locator("[data-upload-state=failed]")).ToHaveCountAsync(1, new() { Timeout = 20000 }); await Assertions.Expect(page.Locator("[data-upload-state=failed]")).ToContainTextAsync("try again"); await page.Locator("[data-upload-retry]").ClickAsync(); await Assertions.Expect(page.Locator("[data-upload-state=ready]")).ToHaveCountAsync(2, new() { Timeout = 20000 }); foreach (var alt in await page.Locator("[data-alt-text]").AllAsync()) await alt.FillAsync("a dot"); await Submit(page).ClickAsync(); var post = await Published(persona, text); made = post["id"]!.GetValue(); Assert.Equal(2, post["media_attachments"]!.AsArray().Count); // the first file was sent once: one upload for it, two for the second (refused, then retried) Assert.Equal(3, uploads); await session.AssertGuards(); }, page); } finally { if (made is not null) await PersonaApi.Call(persona, $"/api/v1/statuses/{made}", HttpMethod.Delete); } } [Fact] public async Task An_avatar_is_set_and_removed_from_settings() { var persona = Pasture.Personas().Last(); await using var session = await SignIn.As(browser, persona); var page = session.Page; await Results.Cell("client.action.avatar", "privapub", persona.UserName, async () => { await page.GotoAsync($"{Pasture.Site}/settings"); var header = page.Locator(".collapse-title", new() { HasText = "Profile" }).First; await header.ClickAsync(); var editor = header.Locator("xpath=..").Locator("[data-profile-editor]"); await editor.WaitForAsync(new() { Timeout = 30000 }); await editor.Locator("input[data-profile-avatar]").SetInputFilesAsync(new FilePayload { Name = "me.png", MimeType = "image/png", Buffer = Png }); await editor.Locator("[data-profile-save]").ClickAsync(); await Assertions.Expect(page.GetByText("Profile saved.")).ToBeVisibleAsync(new() { Timeout = 15000 }); var set = (await PersonaApi.Call(persona, "/api/v1/accounts/verify_credentials"))!["avatar"]!.GetValue(); Assert.DoesNotContain("missing", set); await editor.Locator("[data-profile-avatar-remove]").ClickAsync(); await Assertions.Expect(page.GetByText("Avatar removed.")).ToBeVisibleAsync(new() { Timeout = 15000 }); Assert.Contains("missing", (await PersonaApi.Call(persona, "/api/v1/accounts/verify_credentials"))!["avatar"]!.GetValue()); await session.AssertGuards(); }, page); } } }