134 lines
5.5 KiB
C#
134 lines
5.5 KiB
C#
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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|