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 Stats(IPage page) => await page.EvaluateAsync("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); } } }