188 lines
7.7 KiB
C#
188 lines
7.7 KiB
C#
using System.Text.Json.Nodes;
|
|||
|
|
|
||
|
|
using decePubClient.E2E.Support;
|
||
|
|
|
||
|
|
using Microsoft.Playwright;
|
||
|
|
|
||
|
|
namespace decePubClient.E2E.Mobile
|
||
|
|
{
|
||
|
|
// What a thumb does on the narrowest phone, step by step: every menu, sheet and dialog that opens is whole on the
|
||
|
|
// screen and its targets have 44px hit areas, and each one closes the way a phone user expects. One cell each,
|
||
|
|
// client.mobile.flow.*.
|
||
|
|
[Trait("Category", "Mobile")]
|
||
|
|
public sealed class FlowTests(Browser browser)
|
||
|
|
{
|
||
|
|
static readonly Phone Phone = Phone.Narrow360;
|
||
|
|
|
||
|
|
static Pasture.Persona Persona() => Pasture.Personas().GroupBy(p => p.Root).OrderByDescending(g => g.Count()).First().First();
|
||
|
|
|
||
|
|
static async Task Flow(Session session, string name, Func<IPage, Task> steps)
|
||
|
|
{
|
||
|
|
var page = session.Page;
|
||
|
|
await Results.Cell($"client.mobile.flow.{name}", "privapub", Phone.Key, async () =>
|
||
|
|
{
|
||
|
|
await steps(page);
|
||
|
|
await session.AssertGuards();
|
||
|
|
}, page);
|
||
|
|
}
|
||
|
|
|
||
|
|
// what opened is whole (not cut, inside the screen) and its targets are thumb-sized; the rest of the page is the
|
||
|
|
// sweep's
|
||
|
|
static async Task AssertWhole(IPage page, string scope)
|
||
|
|
{
|
||
|
|
var report = await Fit.Now(page, scope);
|
||
|
|
Assert.True(report.Popovers.Count == 0, "cut or outside: " + string.Join("\n", report.Popovers.Select(p => p.ToJsonString())));
|
||
|
|
Fit.AssertTouchable(report);
|
||
|
|
}
|
||
|
|
|
||
|
|
static ILocator Card(IPage page, string has = null) =>
|
||
|
|
(has is null ? page.Locator("#feed-scroll article[data-uri]") : page.Locator($"#feed-scroll article[data-uri]:has({has})")).First;
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task The_menu_closes_after_a_choice_an_outside_tap_and_escape()
|
||
|
|
{
|
||
|
|
await using var session = await SignIn.As(browser, Persona(), Phone);
|
||
|
|
await Flow(session, "nav", async page =>
|
||
|
|
{
|
||
|
|
var toggle = page.Locator("[data-nav-toggle]");
|
||
|
|
var panel = page.Locator("[data-nav-panel]");
|
||
|
|
await toggle.TapAsync();
|
||
|
|
await Assertions.Expect(panel).ToBeVisibleAsync();
|
||
|
|
await AssertWhole(page, "[data-nav-panel]");
|
||
|
|
await panel.Locator("a[href='explore']").TapAsync();
|
||
|
|
await page.WaitForURLAsync(url => new Uri(url).AbsolutePath == "/explore");
|
||
|
|
await Assertions.Expect(panel).ToBeHiddenAsync();
|
||
|
|
|
||
|
|
await toggle.TapAsync();
|
||
|
|
await Assertions.Expect(panel).ToBeVisibleAsync();
|
||
|
|
var size = page.ViewportSize!;
|
||
|
|
await page.Touchscreen.TapAsync(size.Width - 12, size.Height - 40);
|
||
|
|
await Assertions.Expect(panel).ToBeHiddenAsync();
|
||
|
|
|
||
|
|
await toggle.TapAsync();
|
||
|
|
await Assertions.Expect(panel).ToBeVisibleAsync();
|
||
|
|
await page.Keyboard.PressAsync("Escape");
|
||
|
|
await Assertions.Expect(panel).ToBeHiddenAsync();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task A_dialog_rises_from_the_bottom_and_closes_from_its_button()
|
||
|
|
{
|
||
|
|
await using var session = await SignIn.As(browser, Persona(), Phone);
|
||
|
|
await Flow(session, "dialog", async page =>
|
||
|
|
{
|
||
|
|
var card = Card(page, "[data-liked-by]");
|
||
|
|
if (await card.CountAsync() == 0)
|
||
|
|
Assert.Skip("no post in the home feed has been liked");
|
||
|
|
await card.ScrollIntoViewIfNeededAsync();
|
||
|
|
await card.Locator("footer [title='Other'] > [role=button]").TapAsync();
|
||
|
|
await AssertWhole(page, "[data-sheet]");
|
||
|
|
await card.Locator("[data-liked-by]").TapAsync();
|
||
|
|
var dialog = page.Locator(".modal-open .modal-box").First;
|
||
|
|
await Assertions.Expect(dialog).ToBeVisibleAsync();
|
||
|
|
await AssertWhole(page, ".modal-open .modal-box");
|
||
|
|
var box = (await dialog.BoundingBoxAsync())!;
|
||
|
|
Assert.True(box.Y + box.Height >= page.ViewportSize!.Height - 2, "the dialog doesn't rise from the bottom of the screen");
|
||
|
|
await page.Locator(".modal-open [data-modal-close]").TapAsync();
|
||
|
|
await Assertions.Expect(page.Locator(".modal-open")).ToHaveCountAsync(0);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task A_new_post_is_written_in_a_sheet_from_a_scrolled_feed()
|
||
|
|
{
|
||
|
|
var persona = Persona();
|
||
|
|
var text = $"from a phone {Guid.NewGuid():N}"[..24];
|
||
|
|
await using var session = await SignIn.As(browser, persona, Phone);
|
||
|
|
string made = null;
|
||
|
|
try
|
||
|
|
{
|
||
|
|
await Flow(session, "compose", async page =>
|
||
|
|
{
|
||
|
|
await page.EvaluateAsync("() => document.getElementById('feed-scroll').scrollTo({ top: 1500, behavior: 'instant' })");
|
||
|
|
await page.Locator("[data-compose-open]").TapAsync();
|
||
|
|
var sheet = page.Locator("[data-sheet]:has(textarea[data-compose])").First;
|
||
|
|
await Assertions.Expect(sheet).ToBeVisibleAsync();
|
||
|
|
await AssertWhole(page, "[data-sheet]");
|
||
|
|
Assert.True(await page.EvaluateAsync<bool>("() => document.activeElement?.matches('textarea[data-compose]') === true"),
|
||
|
|
"the composer's words don't have the focus");
|
||
|
|
await page.Keyboard.TypeAsync(text);
|
||
|
|
await sheet.Locator("button[type=submit]").TapAsync();
|
||
|
|
await Assertions.Expect(sheet).ToBeHiddenAsync(new() { Timeout = 15000 });
|
||
|
|
JsonNode latest = null;
|
||
|
|
Assert.True(await Pasture.Eventually(async () => (latest = await Latest(persona))?["content"]?.GetValue<string>().Contains(text) == true, 15),
|
||
|
|
"the post was never published");
|
||
|
|
made = latest!["id"]!.GetValue<string>();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
finally
|
||
|
|
{
|
||
|
|
if (made is not null)
|
||
|
|
await PersonaApi.Call(persona, $"/api/v1/statuses/{made}", HttpMethod.Delete);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task A_reply_is_written_in_a_sheet()
|
||
|
|
{
|
||
|
|
var persona = Persona();
|
||
|
|
var text = $"a phone reply {Guid.NewGuid():N}"[..24];
|
||
|
|
await using var session = await SignIn.As(browser, persona, Phone);
|
||
|
|
string made = null;
|
||
|
|
try
|
||
|
|
{
|
||
|
|
await Flow(session, "reply", async page =>
|
||
|
|
{
|
||
|
|
var card = Card(page, "footer button[title='Reply']");
|
||
|
|
await card.Locator("footer button[title='Reply']").TapAsync();
|
||
|
|
var sheet = page.Locator("[data-sheet]:has(textarea[data-compose])").First;
|
||
|
|
await Assertions.Expect(sheet).ToBeVisibleAsync();
|
||
|
|
await AssertWhole(page, "[data-sheet]");
|
||
|
|
await sheet.Locator("textarea[data-compose]").FillAsync(text);
|
||
|
|
await sheet.Locator("button[type=submit]").TapAsync();
|
||
|
|
await Assertions.Expect(sheet).ToBeHiddenAsync(new() { Timeout = 15000 });
|
||
|
|
JsonNode latest = null;
|
||
|
|
Assert.True(await Pasture.Eventually(async () => (latest = await Latest(persona))?["content"]?.GetValue<string>().Contains(text) == true, 15),
|
||
|
|
"the reply was never published");
|
||
|
|
made = latest!["id"]!.GetValue<string>();
|
||
|
|
Assert.NotNull(latest["in_reply_to_id"]?.GetValue<string>());
|
||
|
|
});
|
||
|
|
}
|
||
|
|
finally
|
||
|
|
{
|
||
|
|
if (made is not null)
|
||
|
|
await PersonaApi.Call(persona, $"/api/v1/statuses/{made}", HttpMethod.Delete);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task A_reaction_is_picked_from_a_sheet()
|
||
|
|
{
|
||
|
|
await using var session = await SignIn.As(browser, Persona(), Phone);
|
||
|
|
await Flow(session, "react", async page =>
|
||
|
|
{
|
||
|
|
var card = Card(page, "footer [title='React']");
|
||
|
|
var uri = await card.GetAttributeAsync("data-uri");
|
||
|
|
card = page.Locator($"#feed-scroll article[data-uri='{uri}']").First;
|
||
|
|
await card.Locator("footer [title='React'] > [role=button]").TapAsync();
|
||
|
|
await AssertWhole(page, "[data-sheet]");
|
||
|
|
await card.Locator("[data-sheet] button", new() { HasText = "😮" }).First.TapAsync();
|
||
|
|
var notice = page.Locator("[data-public-notice].modal-open");
|
||
|
|
if (await notice.IsVisibleAsync() || await Pasture.Eventually(() => notice.IsVisibleAsync(), 2))
|
||
|
|
{
|
||
|
|
await AssertWhole(page, "[data-public-notice] .modal-box");
|
||
|
|
await page.Locator("[data-public-notice-yes]").TapAsync();
|
||
|
|
}
|
||
|
|
var mine = card.Locator("[data-reactions] button.btn-active", new() { HasText = "😮" });
|
||
|
|
await Assertions.Expect(mine).ToBeVisibleAsync(new() { Timeout = 15000 });
|
||
|
|
// and taken back
|
||
|
|
await mine.TapAsync();
|
||
|
|
await Assertions.Expect(mine).ToHaveCountAsync(0, new() { Timeout = 15000 });
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
static async Task<JsonNode> Latest(Pasture.Persona persona) =>
|
||
|
|
(await PersonaApi.Call(persona, $"/api/v1/accounts/{persona.LocalId}/statuses?limit=1"))!.AsArray().FirstOrDefault();
|
||
|
|
}
|
||
|
|
}
|