Files
decePubClient/tests/e2e/Render/GlobeTests.cs
T

159 lines
8.9 KiB
C#
Raw Normal View History

using System.Text.Json;
using decePubClient.E2E.Support;
using Microsoft.Playwright;
namespace decePubClient.E2E.Render
{
// The globe's instruments (wwwroot/js/instruments.js): drawn in the desktop column and in a phone's full screen with
// the Sun where it should be, only the day/night line in a phone's banner, and an idle globe that draws nothing.
// Screenshots for the eye go to tests/e2e/out/globe/.
public sealed class GlobeTests(Browser browser)
{
static readonly string OutDir = Path.Combine(Directory.GetParent(AppContext.BaseDirectory)!.Parent!.Parent!.Parent!.FullName, "out", "globe");
static Pasture.Persona Persona() => Pasture.Personas().GroupBy(p => p.Root).OrderByDescending(g => g.Count()).First().First();
static async Task<JsonElement> Stats(IPage page) =>
await page.EvaluateAsync<JsonElement>("async () => (await import('./js/globe.js')).stats()");
static async Task Live(IPage page)
{
await page.Locator("[data-globe-state=live]").First.WaitForAsync(new() { Timeout = 30000 });
Assert.True(await Pasture.Eventually(async () => (await Stats(page)).TryGetProperty("instruments", out var i) && i.ValueKind == JsonValueKind.Object, 15),
"the instruments never reported");
}
static async Task Photo(IPage page, ILocator what, string name)
{
Directory.CreateDirectory(OutDir);
await page.WaitForTimeoutAsync(1500);
await what.ScreenshotAsync(new() { Path = Path.Combine(OutDir, name + ".png") });
}
[Fact]
public async Task The_desktop_column_draws_the_instruments_with_the_sun_in_place()
{
await using var session = await SignIn.As(browser, Persona());
var page = session.Page;
await Results.Cell("client.render.globe", "privapub", "desktop", async () =>
{
await Live(page);
// a located post, written here from Milano and read back in Nearby, pinned: the globe shows its moment, its
// place and its day
await page.Context.GrantPermissionsAsync(["geolocation"]);
await page.Context.SetGeolocationAsync(new() { Latitude = 45.4642f, Longitude = 9.19f });
await page.GetByRole(AriaRole.Button, new() { Name = "Post" }).First.ClickAsync();
var text = $"the globe's instruments {Guid.NewGuid():N}"[..32];
await page.Locator("textarea[data-compose]").First.FillAsync(text);
await page.Locator("button[title='Located post']").ClickAsync();
var submit = page.Locator("form:has(textarea[data-compose]) button[type=submit]").First;
await Assertions.Expect(submit).ToBeEnabledAsync();
await submit.ClickAsync();
await Assertions.Expect(page.Locator("textarea[data-compose]")).ToHaveCountAsync(0);
await page.Locator("[role=tab][title='Nearby']").ClickAsync();
await page.GetByRole(AriaRole.Button, new() { Name = "Use my position" }).ClickAsync();
var card = page.Locator("#feed-scroll article[data-lat][data-post-at]", new() { HasText = text }).First;
await card.WaitForAsync(new() { Timeout = 20000 });
// the view flies to it first (with the pointer off the feed: a hovered post holds the view), then the card is pinned
await page.Mouse.MoveAsync(2, 2);
Assert.True(await Pasture.Eventually(async () =>
{
var view = await Stats(page);
return view.GetProperty("centroidLatitude").ValueKind == JsonValueKind.Number
&& Math.Abs(view.GetProperty("latitude").GetDouble() - view.GetProperty("centroidLatitude").GetDouble()) < 1;
}, 10), "the globe never looked at the located post");
var box = (await card.BoundingBoxAsync())!;
await card.ClickAsync(new() { Position = new() { X = box.Width - 6, Y = box.Height * 0.35f } });
await Assertions.Expect(page.Locator("[data-globe-pinned]")).ToHaveCountAsync(1);
Assert.True(await Pasture.Eventually(async () => (await Stats(page)).GetProperty("instruments").GetProperty("chips").GetInt32() >= 6, 10),
"the pinned post's instruments have few chips");
var stats = await Stats(page);
var instruments = stats.GetProperty("instruments");
Assert.True(instruments.GetProperty("built").GetBoolean(), $"the instruments weren't built: {instruments}");
Assert.Equal("full", instruments.GetProperty("detail").GetString());
Assert.True(instruments.GetProperty("subsolarError").GetDouble() < 0.01, $"the Sun is drawn {instruments.GetProperty("subsolarError")} units off");
var globe = page.Locator("aside").First;
await Photo(page, globe, "desktop-light");
await File.WriteAllTextAsync(Path.Combine(OutDir, "desktop.json"), (await Stats(page)).ToString());
await page.EvaluateAsync("() => document.documentElement.setAttribute('data-theme', 'neo-dark')");
await Photo(page, globe, "desktop-dark");
// turned to the subsolar point, by dragging (about 3 px a degree; dragging left looks east, up looks south), from a
// little west and north of it, so that its ray stands out: the Sun's own instruments
var canvas = (await globe.Locator("canvas").First.BoundingBoxAsync())!;
var (cx, cy) = (canvas.X + canvas.Width / 2, canvas.Y + canvas.Height / 2);
for (var step = 0; step < 8; step++)
{
var now = await Stats(page);
var dLng = (now.GetProperty("sun").GetProperty("longitude").GetDouble() - 18 - now.GetProperty("longitude").GetDouble() + 540) % 360 - 180;
var dLat = now.GetProperty("sun").GetProperty("latitude").GetDouble() + 12 - now.GetProperty("latitude").GetDouble();
if (Math.Abs(dLng) < 8 && Math.Abs(dLat) < 8)
break;
await page.Mouse.MoveAsync(cx, cy);
await page.Mouse.DownAsync();
await page.Mouse.MoveAsync(cx - (float)Math.Clamp(dLng * 3, -200, 200), cy + (float)Math.Clamp(dLat * 3, -200, 200), new() { Steps = 10 });
await page.Mouse.UpAsync();
await page.WaitForTimeoutAsync(800);
}
await Photo(page, globe, "desktop-sun");
await File.WriteAllTextAsync(Path.Combine(OutDir, "desktop-sun.json"), (await Stats(page)).ToString());
// and taken back by its author, the one thing a located post's card offers
await card.Locator("[data-located-delete]").ClickAsync();
await page.Locator("[data-confirm-yes]").ClickAsync();
await Assertions.Expect(card).ToHaveCountAsync(0);
await session.AssertGuards();
}, page);
}
[Fact]
public async Task A_phone_banner_draws_little_and_its_full_screen_everything()
{
await using var session = await SignIn.As(browser, Persona(), Phone.Pixel7);
var page = session.Page;
await Results.Cell("client.render.globe", "privapub", "phone", async () =>
{
await page.Locator("button[title='Globe']").TapAsync();
await Live(page);
Assert.Equal("banner", (await Stats(page)).GetProperty("instruments").GetProperty("detail").GetString());
await page.Locator("[data-globe-expand]").TapAsync();
await Assertions.Expect(page.Locator("aside[role=dialog]")).ToBeVisibleAsync();
Assert.True(await Pasture.Eventually(async () => (await Stats(page)).GetProperty("instruments").GetProperty("detail").GetString() == "full", 10),
"the full screen globe has the banner's instruments");
// the camera moves back for a screen taller than wide, so the globe fits across it
Assert.True(await Pasture.Eventually(async () => (await Stats(page)).GetProperty("altitude").GetDouble() > 3.5, 10),
"the full screen globe kept the banner's camera");
Assert.True((await Stats(page)).GetProperty("instruments").GetProperty("built").GetBoolean(), "the instruments weren't built");
await Photo(page, page.Locator("aside[role=dialog]"), "phone-full");
// and closed, the banner again
await page.Locator("[data-globe-close]").TapAsync();
await Assertions.Expect(page.Locator("aside[role=dialog]")).ToHaveCountAsync(0);
Assert.True(await Pasture.Eventually(async () => (await Stats(page)).GetProperty("instruments").GetProperty("detail").GetString() == "banner", 10),
"the banner kept the full screen's instruments");
await session.AssertGuards();
}, page);
}
// with reduced motion nothing turns or flies: once settled, no frame is drawn, the instruments' neither
[Fact]
public async Task An_idle_globe_draws_nothing()
{
await using var session = await SignIn.As(browser, Persona(), Phone.Pixel7);
var page = session.Page;
await Results.Cell("client.render.globe-idle", "privapub", "phone", async () =>
{
await page.Locator("button[title='Globe']").TapAsync();
await page.Locator("[data-globe-expand]").TapAsync();
await Live(page);
Assert.True(await Pasture.Eventually(async () => (await Stats(page)).GetProperty("idle").GetBoolean(), 20), "the globe never settled");
var before = await Stats(page);
await page.WaitForTimeoutAsync(3000);
var after = await Stats(page);
Assert.Equal(before.GetProperty("frame").GetInt32(), after.GetProperty("frame").GetInt32());
Assert.Equal(before.GetProperty("instruments").GetProperty("frames").GetInt32(), after.GetProperty("instruments").GetProperty("frames").GetInt32());
Assert.Equal(0, after.GetProperty("fps").GetInt32());
await session.AssertGuards();
}, page);
}
}
}