184 lines
7.7 KiB
C#
184 lines
7.7 KiB
C#
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<JsonNode> 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<string>().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<string>();
|
|||
|
|
Assert.Equal("a single dot", post["media_attachments"]![0]!["description"]!.GetValue<string>());
|
|||
|
|
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<string>();
|
|||
|
|
Assert.Null(post["media_attachments"]![0]!["description"]?.GetValue<string>());
|
|||
|
|
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<string>();
|
|||
|
|
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<string>();
|
|||
|
|
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<string>());
|
|||
|
|
await session.AssertGuards();
|
|||
|
|
}, page);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|