Files
decePubClient/tests/e2e/Support/SignIn.cs
T
thepraandClaude Opus 5.5 63d83810d7 Phones in the end-to-end tests
The browser fixture opens phones from Playwright's own device presets (360x800, iPhone SE 375x667, Pixel 7 412x839,
and the Pixel 7 held sideways), with touch, a coarse pointer and reduced motion, and a phone session fails at once if
the page doesn't see a touch screen. Mobile/fit.js measures a page one screen at a time down its main scroller:
sideways scroll, boxes past the screen's sides, open menus and dialogs cut or outside the screen, targets without a
44px hit area (hit-tested at their centre and 21px around it, so a crowding neighbour fails them too) and control text
under 12px. RouteSweepTests runs every feed tab, threads of each kind the town holds, profiles, search, Explore,
lists, groups, filters, scheduled posts, settings and administration (every section opened) and the login page on the
three portrait phones, as client.mobile.fit.* and client.mobile.touch.* cells; screenshots, findings and a contact
sheet go to tests/e2e/out/mobile/. DesktopUnchangedTests holds desktop's control sizes. Category=Mobile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
2026-10-06 22:40:28 +02:00

93 lines
3.7 KiB
C#

using Microsoft.Playwright;
namespace decePubClient.E2E.Support
{
// Signs a town persona in through the real login page and persona chooser, once per persona per run; later sessions
// start from the saved browser state (the AuthData decePub keeps in localStorage).
public static class SignIn
{
static readonly string StateDir = Path.Combine(AppContext.BaseDirectory, ".state");
public static async Task<Session> As(Browser browser, Pasture.Persona persona, bool fresh = false)
{
var state = Path.Combine(StateDir, persona.UserName + ".json");
if (!fresh && File.Exists(state))
{
var known = await browser.Open(state);
await known.Page.GotoAsync(Pasture.Site + "/");
await known.Page.Locator("article[data-uri], [data-feed-empty]").First.WaitForAsync(new() { Timeout = 30000 });
return known;
}
var session = await browser.Open();
await Through(session.Page, persona);
Directory.CreateDirectory(StateDir);
await session.Context.StorageStateAsync(new() { Path = state });
return session;
}
// on a phone: the state the desktop sign-in saved, and the feed as on a first visit (no FeedFilter), so the globe
// banner starts closed as a phone's does
public static async Task<Session> As(Browser browser, Pasture.Persona persona, Phone phone)
{
var state = Path.Combine(StateDir, persona.UserName + ".json");
if (!File.Exists(state))
await (await As(browser, persona)).DisposeAsync();
var session = await browser.Open(state, phone);
await session.Page.GotoAsync(Pasture.Site + "/");
await session.Page.EvaluateAsync("() => localStorage.removeItem('FeedFilter')");
await session.Page.GotoAsync(Pasture.Site + "/");
await session.Page.Locator("article[data-uri], [data-feed-empty]").First.WaitForAsync(new() { Timeout = 30000 });
await session.AssertPhone();
return session;
}
public static async Task Through(IPage page, Pasture.Persona persona)
{
try
{
await Steps(page, persona);
}
catch
{
await Evidence(page, "signin-" + persona.UserName);
throw;
}
}
// a screenshot and the page's text next to the test output, for a failure seen only in a headless browser
public static async Task Evidence(IPage page, string name)
{
Directory.CreateDirectory(StateDir);
await page.ScreenshotAsync(new() { Path = Path.Combine(StateDir, name + ".png"), FullPage = true });
await File.WriteAllTextAsync(Path.Combine(StateDir, name + ".txt"), page.Url + "\n" + await page.InnerTextAsync("body"));
}
static async Task Steps(IPage page, Pasture.Persona persona)
{
await page.GotoAsync(Pasture.Site + "/login");
await page.Locator("input[autocomplete=username]").FillAsync(persona.Root);
await page.Locator("input[autocomplete=current-password]").FillAsync(persona.Password);
await page.Locator("button[type=submit]").ClickAsync();
// a root with several personas picks one; one with a single persona goes on by itself, its button disabled while it
// does. Only the login page's chooser is clicked: the feed's persona switcher names the persona too
var chooser = page.Locator("section button:enabled").Filter(new() { HasText = persona.UserName + "@privapub.test" });
var until = DateTime.UtcNow.AddSeconds(30);
while (new Uri(page.Url).AbsolutePath != "/" && DateTime.UtcNow < until)
{
if (await chooser.CountAsync() > 0)
try
{
await chooser.First.ClickAsync(new() { Timeout = 2000 });
}
catch (TimeoutException)
{
}
await page.WaitForTimeoutAsync(250);
}
await page.WaitForURLAsync(url => new Uri(url).AbsolutePath == "/", new() { Timeout = 30000 });
// (a new persona's feed is empty)
await page.Locator("article[data-uri], [data-feed-empty]").First.WaitForAsync(new() { Timeout = 30000 });
}
}
}