Files
decePubClient/tests/e2e/Mobile/Routes.cs
T

134 lines
5.5 KiB
C#
Raw Normal View History

2026-10-06 22:40:28 +02:00
using System.Text.Json.Nodes;
using decePubClient.E2E.Support;
using Microsoft.Playwright;
namespace decePubClient.E2E.Mobile
{
// The pages the phone sweep visits, each opened as a reader would get there: signed in as a town persona (the one with
// sibling personas, unless the page shows another's post), or signed out for the login page. Screens is how far down
// the page's main scroller the sweep measures.
public sealed record Route(string Slug, Func<IPage, Pasture.Persona, Task> Open, Func<Pasture.Persona> Reader = null,
int Screens = 8, bool SignedOut = false, bool Gating = true)
{
static readonly Lazy<JsonArray> Expected = new(() => Pasture.Town("expect").GetAwaiter().GetResult().AsArray());
static Pasture.Persona Sweeper() => Pasture.Personas().GroupBy(p => p.Root).OrderByDescending(g => g.Count()).First().First();
static readonly Pasture.Persona Admin = new("alice", "alice", PastureAdmin.Root, PastureAdmin.Password, null, null, null);
public Pasture.Persona Persona() => (Reader ?? Sweeper)();
public static readonly Route[] All =
[
new("feed-home", (_, _) => Task.CompletedTask),
new("feed-local", Tab(1), Screens: 6),
new("feed-federated", Tab(2), Screens: 6),
new("feed-nearby", Tab(3), Screens: 4),
new("feed-groups", Tab(4), Screens: 6),
new("feed-direct", Tab(5), Screens: 6),
new("feed-notifications", Tab(6), Screens: 6),
new("feed-favourites", Tab(7), Screens: 4),
new("feed-bookmarks", Tab(8), Screens: 4),
new("feed-lists", Tab(9), Screens: 4),
Thread("thread-poll", i => i["poll"] is JsonArray),
Thread("thread-pictures", i => i["alts"]!.AsArray().Count > 0),
Thread("thread-titled", i => i["title"] is not null),
Thread("thread-quote", i => i["quote"] is not null),
Thread("thread-warning", i => i["cw"] is not null),
Thread("thread-replies", i => (string)i["kind"] == "thread"),
new("profile-own", async (page, persona) => await Visit(page, $"/account/{persona.LocalId}"), Screens: 10),
new("profile-followed", async (page, persona) =>
{
var followed = (await PersonaApi.Call(persona, $"/api/v1/accounts/{persona.LocalId}/following?limit=1"))?.AsArray().FirstOrDefault();
if (followed is null)
Assert.Skip("the persona follows nobody");
await Visit(page, $"/account/{followed["id"]}");
}, Screens: 10),
new("search", async (page, persona) =>
{
var other = Pasture.Personas().First(p => p.Root != persona.Root);
await Visit(page, $"/search?q={Uri.EscapeDataString(other.UserName)}");
}),
new("explore-hashtags", async (page, _) => await Visit(page, "/explore")),
new("explore-posts", ExploreTab(1)),
new("explore-links", ExploreTab(2)),
new("explore-people", ExploreTab(3)),
new("explore-followed", ExploreTab(4)),
new("lists", async (page, _) => await Visit(page, "/lists")),
new("groups", async (page, _) => await Visit(page, "/groups")),
new("filters", async (page, _) => await Visit(page, "/filters")),
new("scheduled", async (page, _) => await Visit(page, "/scheduled")),
new("notifications-filtered", async (page, _) => await Visit(page, "/notifications/filtered")),
new("settings", async (page, _) =>
{
await Visit(page, "/settings");
await OpenEveryCollapse(page);
}, Screens: 30),
new("administration", async (page, _) =>
{
await Visit(page, "/administration");
await OpenEveryCollapse(page);
}, () => Admin, Screens: 30),
new("login", async (page, _) => await Visit(page, "/login"), SignedOut: true),
new("styleguide", async (page, _) => await Visit(page, "/styleguide"), SignedOut: true, Screens: 40, Gating: false)
];
public static Route Get(string slug) => All.Single(r => r.Slug == slug);
static async Task Visit(IPage page, string path)
{
await page.GotoAsync(Pasture.Site + path);
await Fit.Settle(page);
}
// the feed's tab at this place: Home, Local, Federation, Nearby, Groups, Direct, Notifications, Favourites,
// Bookmarks, Lists (FeedTabs.Always)
static Func<IPage, Pasture.Persona, Task> Tab(int index) => async (page, _) =>
{
await page.Locator("[role=tablist]").First.Locator("[role=tab]").Nth(index).TapAsync();
await Fit.Settle(page);
};
static Func<IPage, Pasture.Persona, Task> ExploreTab(int index) => async (page, _) =>
{
await Visit(page, "/explore");
await page.Locator("[role=tablist]").First.Locator("[role=tab]").Nth(index).TapAsync();
await Fit.Settle(page);
};
// a town post of the kind wanted, opened as its thread by a persona it was for
static Route Thread(string slug, Func<JsonNode, bool> wanted)
{
JsonNode Pick() => Expected.Value.FirstOrDefault(i => wanted(i!) && ReaderOf(i) is not null);
return new(slug, async (page, _) =>
{
if (Pick() is not { } item)
{
Assert.Skip($"the town holds no post for {slug}");
return;
}
await page.GotoAsync($"{Pasture.Site}/expand/{item["id"]}");
await page.Locator($"article[data-uri='{item["uri"]}']").First.WaitForAsync(new() { Timeout = 30000 });
await Fit.Settle(page);
}, () => Pick() is { } item ? ReaderOf(item) : Sweeper(), Screens: 10);
}
static Pasture.Persona ReaderOf(JsonNode item)
{
var personas = Pasture.Personas();
return item["readers"]!.AsArray().Select(r => personas.FirstOrDefault(p => p.UserName == (string)r)).FirstOrDefault(p => p is not null);
}
static async Task OpenEveryCollapse(IPage page)
{
var closed = page.Locator(".collapse-title[aria-expanded=false]");
for (var i = 0; i < 20 && await closed.CountAsync() > 0; i++)
{
await closed.First.TapAsync();
await Fit.Settle(page);
}
}
}
}