Settings, Administration and conversations; threads that fill in
Settings, which held only the theme, now belong to the persona too: - its profile (update_credentials, a new PATCH in IHttpService): pictures, name, biography and fields as written, locked, bot, discoverable, indexable, and how its posts start (visibility, language, sensitive media, who may quote them); the composer starts at that visibility; - the root's personas, each used in turn, and a new one made; - the accounts it muted or blocked and the servers it blocked, each undone there, a server blocked from a pasted address or handle; - its follows, lists, blocks, mutes, blocked servers and bookmarks exported and imported as the CSV files Mastodon writes (PortabilityService): an import resolves each row through PrivaPub, one at a time, and can stop. Administration opens to moderators: reports, resolved there; and for administrators the servers blocked for everyone and the federation statistics (traffic by server, delivery success and latency, the crawler). The Direct tab pages conversations, badges the unread ones, and a card marks one read or takes it off the list; opening one reads it. A remote thread is read again 4 and 15 seconds after it opens, since PrivaPub then fetches the replies its servers hold. A thread its community locked shows a lock instead of a reply button, reactions stay visible below a content warning, and a searched handle opens its account's profile whatever else matched. The e2e tests cover each: the profile saved and read back, a persona made, servers blocked and imported, follows exported, an instance block and the statistics as the pasture's admin, a conversation read and removed, and a reply only Mastodon held appearing in its thread. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
1 parent
03f4f2ad53
commit
d7d836ba39
40 files changed
+3155
-62
No files matched your search
@@ -0,0 +1,86 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
using decePubClient.E2E.Support;
|
||||
|
||||
using Microsoft.Playwright;
|
||||
|
||||
namespace decePubClient.E2E.Actions
|
||||
{
|
||||
// Administration as the pasture's administrator (pastureroot, whom the scenarios make): a server blocked for everyone
|
||||
// and unblocked, the federation statistics, and the reports moderators handle; each change checked on /clientapi
|
||||
public sealed class AdministrationTests(Browser browser)
|
||||
{
|
||||
const string Root = "pastureroot";
|
||||
const string Password = "Pasture-Pass-1!";
|
||||
static readonly Pasture.Persona Admin = new("alice", "alice", Root, Password, null, null, null);
|
||||
|
||||
static async Task<JsonNode> AsAdmin(string path)
|
||||
{
|
||||
using var http = Pasture.Http();
|
||||
var login = await http.PostAsJsonAsync($"{Pasture.Api}/clientapi/user/login", new { userName = Root, password = Password });
|
||||
login.EnsureSuccessStatusCode();
|
||||
var jwt = (await login.Content.ReadFromJsonAsync<JsonObject>())!["token"]!.GetValue<string>();
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, Pasture.Api + path);
|
||||
request.Headers.Authorization = new("Bearer", jwt);
|
||||
using var response = await http.SendAsync(request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return JsonNode.Parse(await response.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
static async Task<ILocator> Open(IPage page, string section)
|
||||
{
|
||||
await page.GotoAsync($"{Pasture.Site}/administration");
|
||||
var header = page.Locator(".collapse-title", new() { HasText = section }).First;
|
||||
await header.ClickAsync();
|
||||
return header.Locator("xpath=..");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_server_is_blocked_for_everyone_and_unblocked()
|
||||
{
|
||||
const string server = "instance-block-by-e2e.example";
|
||||
await using var session = await SignIn.As(browser, Admin);
|
||||
var page = session.Page;
|
||||
await Results.Cell("client.admin.server-block", "privapub", Root, async () =>
|
||||
{
|
||||
var panel = await Open(page, "Blocked servers");
|
||||
var blocks = panel.Locator("[data-server-blocks]");
|
||||
await blocks.WaitForAsync(new() { Timeout = 30000 });
|
||||
await blocks.Locator("[data-server-block-domain]").FillAsync(server);
|
||||
await blocks.GetByRole(AriaRole.Button, new() { Name = "Block server" }).ClickAsync();
|
||||
var row = blocks.Locator($"[data-server-block='{server}']");
|
||||
await Assertions.Expect(row).ToContainTextAsync("Suspended", new() { Timeout = 15000 });
|
||||
Assert.Contains((await AsAdmin("/clientapi/admin/domainblocks/list"))!.AsArray(), b => b!["domain"]!.GetValue<string>() == server);
|
||||
|
||||
await row.GetByRole(AriaRole.Button, new() { Name = "Unblock" }).ClickAsync();
|
||||
await Assertions.Expect(row).ToHaveCountAsync(0, new() { Timeout = 15000 });
|
||||
Assert.DoesNotContain((await AsAdmin("/clientapi/admin/domainblocks/list"))!.AsArray(), b => b!["domain"]!.GetValue<string>() == server);
|
||||
await session.AssertGuards();
|
||||
}, page);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task The_federation_statistics_and_the_reports_are_shown()
|
||||
{
|
||||
await using var session = await SignIn.As(browser, Admin);
|
||||
var page = session.Page;
|
||||
await Results.Cell("client.admin.statistics", "privapub", Root, async () =>
|
||||
{
|
||||
var panel = await Open(page, "Statistics");
|
||||
await Assertions.Expect(panel.Locator("[data-statistics-overview]")).ToBeVisibleAsync(new() { Timeout = 30000 });
|
||||
var expected = (await AsAdmin("/clientapi/admin/statistics/hosts?days=30&sort=traffic&page=1&limit=50"))!["hosts"]!.AsArray()
|
||||
.Select(h => h!["host"]!.GetValue<string>()).Take(5).ToList();
|
||||
Assert.NotEmpty(expected);
|
||||
foreach (var host in expected)
|
||||
await Assertions.Expect(panel.Locator($"[data-statistics-host='{host}']")).ToHaveCountAsync(1, new() { Timeout = 15000 });
|
||||
|
||||
var reports = await Open(page, "Reports");
|
||||
await Assertions.Expect(reports.Locator("[data-reports]")).ToBeVisibleAsync(new() { Timeout = 30000 });
|
||||
var open = (await AsAdmin("/clientapi/moderator/reports?resolved=false"))!.AsArray().Count;
|
||||
await Assertions.Expect(reports.Locator("[data-report-id]")).ToHaveCountAsync(open, new() { Timeout = 15000 });
|
||||
await session.AssertGuards();
|
||||
}, page);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
using decePubClient.E2E.Support;
|
||||
|
||||
using Microsoft.Playwright;
|
||||
|
||||
namespace decePubClient.E2E.Actions
|
||||
{
|
||||
// Direct conversations: a message from another persona badges the Direct tab and its card says unread; marking it
|
||||
// read and taking it off the list are checked on PrivaPub
|
||||
public sealed class ConversationTests(Browser browser)
|
||||
{
|
||||
[Fact]
|
||||
public async Task A_new_message_is_unread_until_marked_and_leaves_the_list_when_removed()
|
||||
{
|
||||
var personas = Pasture.Personas();
|
||||
Assert.SkipWhen(personas.Count < 2, "the town has fewer than two personas");
|
||||
var (reader, writer) = (personas[0], personas[1]);
|
||||
var message = (await PersonaApi.Call(writer, "/api/v1/statuses", HttpMethod.Post, new JsonObject
|
||||
{
|
||||
["status"] = $"@{reader.UserName} just between us {Guid.NewGuid():N}"[..40], ["visibility"] = "direct"
|
||||
}))!;
|
||||
var uri = message["uri"]!.GetValue<string>();
|
||||
async Task<JsonNode> Conversation() => (await PersonaApi.Call(reader, "/api/v1/conversations?limit=40"))!.AsArray()
|
||||
.FirstOrDefault(c => c!["last_status"]?["uri"]?.GetValue<string>() == uri);
|
||||
await using var session = await SignIn.As(browser, reader);
|
||||
var page = session.Page;
|
||||
try
|
||||
{
|
||||
await Results.Cell("client.action.conversation", "privapub", reader.UserName, async () =>
|
||||
{
|
||||
Assert.True(await Pasture.Eventually(async () => (await Conversation())?["unread"]?.GetValue<bool>() == true, 15),
|
||||
"the message never made an unread conversation");
|
||||
await page.GotoAsync($"{Pasture.Site}/");
|
||||
await page.EvaluateAsync("() => localStorage.removeItem('FeedFilter')");
|
||||
await page.GotoAsync($"{Pasture.Site}/");
|
||||
await page.Locator("[data-unread-conversations]").WaitForAsync(new() { Timeout = 30000 });
|
||||
|
||||
await page.GetByRole(AriaRole.Tab).Filter(new() { Has = page.Locator(".ion-md-mail") }).ClickAsync();
|
||||
var card = page.Locator($"article[data-uri='{uri}']");
|
||||
await card.Locator("[data-conversation-unread]").WaitForAsync(new() { Timeout = 30000 });
|
||||
await card.Locator("[data-conversation-read]").ClickAsync();
|
||||
await Assertions.Expect(card.Locator("[data-conversation-unread]")).ToHaveCountAsync(0, new() { Timeout = 10000 });
|
||||
Assert.False((await Conversation())!["unread"]!.GetValue<bool>());
|
||||
|
||||
await card.Locator("[data-conversation-remove]").ClickAsync();
|
||||
await Assertions.Expect(card).ToHaveCountAsync(0, new() { Timeout = 10000 });
|
||||
Assert.Null(await Conversation());
|
||||
await session.AssertGuards();
|
||||
}, page);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await PersonaApi.Call(writer, $"/api/v1/statuses/{message["id"]!.GetValue<string>()}", HttpMethod.Delete);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
using decePubClient.E2E.Support;
|
||||
|
||||
using Microsoft.Playwright;
|
||||
|
||||
namespace decePubClient.E2E.Actions
|
||||
{
|
||||
// Settings as a persona: its profile saved (update_credentials) and read back from PrivaPub, a server blocked and
|
||||
// unblocked, the follows exported and servers imported in Mastodon's files, and a new persona made; what a test changes
|
||||
// is put back through the API afterwards, but for the persona (PrivaPub keeps every persona, so the test makes it under
|
||||
// the scenarios' own root, never a town one)
|
||||
public sealed class SettingsTests(Browser browser)
|
||||
{
|
||||
static async Task<ILocator> Open(IPage page, string section)
|
||||
{
|
||||
await page.GotoAsync($"{Pasture.Site}/settings");
|
||||
var header = page.Locator(".collapse-title", new() { HasText = section }).First;
|
||||
await header.ClickAsync();
|
||||
return header.Locator("xpath=..");
|
||||
}
|
||||
|
||||
static JsonObject Restore(JsonNode account)
|
||||
{
|
||||
var fields = new JsonObject();
|
||||
var source = account["source"]!;
|
||||
var kept = source["fields"]!.AsArray();
|
||||
for (var i = 0; i < 4; i++)
|
||||
fields[i.ToString()] = new JsonObject
|
||||
{
|
||||
["name"] = i < kept.Count ? kept[i]!["name"]!.GetValue<string>() : "",
|
||||
["value"] = i < kept.Count ? kept[i]!["value"]!.GetValue<string>() : ""
|
||||
};
|
||||
return new JsonObject
|
||||
{
|
||||
["display_name"] = account["display_name"]!.GetValue<string>(),
|
||||
["note"] = source["note"]!.GetValue<string>(),
|
||||
["locked"] = account["locked"]!.GetValue<bool>(),
|
||||
["source"] = new JsonObject { ["privacy"] = source["privacy"]!.GetValue<string>() },
|
||||
["fields_attributes"] = fields
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task The_personas_profile_is_saved_from_settings()
|
||||
{
|
||||
var persona = Pasture.Personas().Last();
|
||||
var original = (await PersonaApi.Call(persona, "/api/v1/accounts/verify_credentials"))!;
|
||||
var mark = Guid.NewGuid().ToString("N")[..6];
|
||||
await using var session = await SignIn.As(browser, persona);
|
||||
var page = session.Page;
|
||||
try
|
||||
{
|
||||
await Results.Cell("client.action.profile-edit", "privapub", persona.UserName, async () =>
|
||||
{
|
||||
var panel = await Open(page, "Profile");
|
||||
var editor = panel.Locator("[data-profile-editor]");
|
||||
await editor.WaitForAsync(new() { Timeout = 30000 });
|
||||
await Assertions.Expect(editor.Locator("[data-profile-name]")).ToHaveValueAsync(original["display_name"]!.GetValue<string>(),
|
||||
new() { Timeout = 10000 });
|
||||
|
||||
await editor.Locator("[data-profile-name]").FillAsync($"Edited {mark}");
|
||||
await editor.Locator("[data-profile-note]").FillAsync($"A biography written in the e2e tests {mark}");
|
||||
var row = editor.Locator("[data-profile-field='3'] input");
|
||||
await row.Nth(0).FillAsync("e2e");
|
||||
await row.Nth(1).FillAsync(mark);
|
||||
await editor.Locator("[data-profile-privacy]").SelectOptionAsync("Unlisted");
|
||||
await editor.Locator("[data-profile-save]").ClickAsync();
|
||||
await Assertions.Expect(page.GetByText("Profile saved.")).ToBeVisibleAsync(new() { Timeout = 15000 });
|
||||
|
||||
var saved = (await PersonaApi.Call(persona, "/api/v1/accounts/verify_credentials"))!;
|
||||
Assert.Equal($"Edited {mark}", saved["display_name"]!.GetValue<string>());
|
||||
Assert.Equal($"A biography written in the e2e tests {mark}", saved["source"]!["note"]!.GetValue<string>());
|
||||
Assert.Equal("unlisted", saved["source"]!["privacy"]!.GetValue<string>());
|
||||
Assert.Contains(saved["source"]!["fields"]!.AsArray(), f => f!["name"]!.GetValue<string>() == "e2e" && f["value"]!.GetValue<string>() == mark);
|
||||
// the persona chooser shows the new name at once
|
||||
await Assertions.Expect(page.GetByText($"Edited {mark}", new() { Exact = true }).First).ToBeVisibleAsync(new() { Timeout = 5000 });
|
||||
await session.AssertGuards();
|
||||
}, page);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await PersonaApi.Call(persona, "/api/v1/accounts/update_credentials", HttpMethod.Patch, Restore(original));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_server_is_blocked_and_unblocked_from_settings()
|
||||
{
|
||||
var persona = Pasture.Personas().Last();
|
||||
const string server = "blocked-by-e2e.example";
|
||||
await using var session = await SignIn.As(browser, persona);
|
||||
var page = session.Page;
|
||||
try
|
||||
{
|
||||
await Results.Cell("client.action.domain-block", "privapub", persona.UserName, async () =>
|
||||
{
|
||||
var panel = await Open(page, "Mutes/Blocks");
|
||||
var servers = panel.Locator("[data-domain-blocks]");
|
||||
await servers.WaitForAsync(new() { Timeout = 30000 });
|
||||
|
||||
await servers.Locator("[data-domain-new]").FillAsync($"https://{server.ToUpperInvariant()}/about");
|
||||
await servers.GetByRole(AriaRole.Button, new() { Name = "Block server" }).ClickAsync();
|
||||
await Assertions.Expect(servers.Locator($"[data-domain='{server}']")).ToBeVisibleAsync(new() { Timeout = 15000 });
|
||||
Assert.Contains(server, (await PersonaApi.Call(persona, "/api/v1/domain_blocks"))!.AsArray().Select(d => d!.GetValue<string>()));
|
||||
|
||||
await servers.Locator($"[data-domain='{server}']").GetByRole(AriaRole.Button, new() { Name = "Unblock" }).ClickAsync();
|
||||
await Assertions.Expect(servers.Locator($"[data-domain='{server}']")).ToHaveCountAsync(0, new() { Timeout = 15000 });
|
||||
Assert.DoesNotContain(server, (await PersonaApi.Call(persona, "/api/v1/domain_blocks"))!.AsArray().Select(d => d!.GetValue<string>()));
|
||||
await session.AssertGuards();
|
||||
}, page);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await PersonaApi.Call(persona, $"/api/v1/domain_blocks?domain={server}", HttpMethod.Delete);
|
||||
}
|
||||
}
|
||||
|
||||
// the follows export is Mastodon's following_accounts.csv, naming every account in full
|
||||
[Fact]
|
||||
public async Task The_follows_are_exported_in_mastodons_format()
|
||||
{
|
||||
var persona = Pasture.Personas().First();
|
||||
var following = (await PersonaApi.Call(persona, $"/api/v1/accounts/{persona.LocalId}/following?limit=80"))!.AsArray();
|
||||
Assert.SkipWhen(following.Count == 0, $"{persona.UserName} follows nobody in this town");
|
||||
await using var session = await SignIn.As(browser, persona);
|
||||
var page = session.Page;
|
||||
await Results.Cell("client.action.export", "privapub", persona.UserName, async () =>
|
||||
{
|
||||
var panel = await Open(page, "Data import/export");
|
||||
var button = panel.Locator("[data-export-kind='Follows']");
|
||||
await button.WaitForAsync(new() { Timeout = 30000 });
|
||||
var download = await page.RunAndWaitForDownloadAsync(() => button.ClickAsync(), new() { Timeout = 60000 });
|
||||
Assert.Equal("following_accounts.csv", download.SuggestedFilename);
|
||||
var lines = (await File.ReadAllLinesAsync((await download.PathAsync())!)).ToList();
|
||||
Assert.Equal("Account address,Show boosts,Notify on new posts,Languages", lines[0]);
|
||||
foreach (var account in following)
|
||||
{
|
||||
var acct = account!["acct"]!.GetValue<string>();
|
||||
var full = acct.Contains('@') ? acct : $"{acct}@privapub.test";
|
||||
Assert.Contains(lines, l => l.StartsWith(full + ",", StringComparison.Ordinal));
|
||||
}
|
||||
await session.AssertGuards();
|
||||
}, page);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Blocked_servers_are_imported_from_a_mastodon_file()
|
||||
{
|
||||
var persona = Pasture.Personas().Last();
|
||||
string[] servers = ["imported-one.example", "imported-two.example"];
|
||||
var csv = Path.Combine(Path.GetTempPath(), $"blocked_domains-{Guid.NewGuid():N}.csv");
|
||||
await File.WriteAllTextAsync(csv, string.Join("\n", servers) + "\n");
|
||||
await using var session = await SignIn.As(browser, persona);
|
||||
var page = session.Page;
|
||||
try
|
||||
{
|
||||
await Results.Cell("client.action.import", "privapub", persona.UserName, async () =>
|
||||
{
|
||||
var panel = await Open(page, "Data import/export");
|
||||
var import = panel.Locator("[data-import]");
|
||||
await import.WaitForAsync(new() { Timeout = 30000 });
|
||||
await import.Locator("[data-import-kind]").SelectOptionAsync("ServerBlocks");
|
||||
await import.Locator("[data-import-file]").SetInputFilesAsync(csv);
|
||||
await import.Locator("[data-import-start]").ClickAsync();
|
||||
await Assertions.Expect(import.Locator("[data-import-report]")).ToContainTextAsync("2 of 2 done, 0 could not be.", new() { Timeout = 30000 });
|
||||
var blocked = (await PersonaApi.Call(persona, "/api/v1/domain_blocks"))!.AsArray().Select(d => d!.GetValue<string>()).ToList();
|
||||
Assert.All(servers, s => Assert.Contains(s, blocked));
|
||||
await session.AssertGuards();
|
||||
}, page);
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var server in servers)
|
||||
await PersonaApi.Call(persona, $"/api/v1/domain_blocks?domain={server}", HttpMethod.Delete);
|
||||
File.Delete(csv);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_new_persona_is_made_and_used()
|
||||
{
|
||||
var name = $"e2e_{Guid.NewGuid():N}"[..12];
|
||||
await using var session = await SignIn.As(browser, new Pasture.Persona("alice", "alice", "pastureroot", "Pasture-Pass-1!", null, null, null));
|
||||
var page = session.Page;
|
||||
await Results.Cell("client.action.persona-create", "privapub", "pastureroot", async () =>
|
||||
{
|
||||
var panel = await Open(page, "Personas");
|
||||
var form = panel.Locator("[data-persona-new]");
|
||||
await form.WaitForAsync(new() { Timeout = 30000 });
|
||||
await form.Locator("[data-persona-username]").FillAsync(name);
|
||||
await form.Locator("[data-persona-name]").FillAsync($"Made by e2e {name}");
|
||||
await form.Locator("[data-persona-biography]").FillAsync("A persona the end-to-end tests made.");
|
||||
await form.Locator("[data-persona-create]").ClickAsync();
|
||||
var made = panel.Locator($"[data-persona='{name}']");
|
||||
await made.WaitForAsync(new() { Timeout = 15000 });
|
||||
|
||||
await made.GetByRole(AriaRole.Button, new() { Name = "Use" }).ClickAsync();
|
||||
await Assertions.Expect(made).ToContainTextAsync("in use", new() { Timeout = 15000 });
|
||||
await Assertions.Expect(page.GetByText($"Made by e2e {name}", new() { Exact = true }).First).ToBeVisibleAsync();
|
||||
using var http = Pasture.Http();
|
||||
var webfinger = await http.GetAsync($"{Pasture.Api}/.well-known/webfinger?resource=acct:{name}@privapub.test");
|
||||
Assert.True(webfinger.IsSuccessStatusCode, $"{name} is not found by WebFinger");
|
||||
await session.AssertGuards();
|
||||
}, page);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
using decePubClient.E2E.Support;
|
||||
|
||||
using Microsoft.Playwright;
|
||||
|
||||
namespace decePubClient.E2E.Actions
|
||||
{
|
||||
// A remote thread opened in decePub fills with the replies PrivaPub never received: opening it has PrivaPub read the
|
||||
// thread from its server (Mastodon's FEP-7888 context), and the page reads the thread again a few seconds later
|
||||
public sealed class ThreadTests(Browser browser)
|
||||
{
|
||||
const string Platform = "mastodon";
|
||||
const string Author = "elif_ms06";
|
||||
// followed by no persona, so Mastodon never delivers her reply to PrivaPub
|
||||
const string Outsider = "gigi_ms28";
|
||||
|
||||
static async Task<JsonNode> Drive(string user, string verb, JsonObject parameters) =>
|
||||
await Pasture.Town("drive", Platform, user, verb, parameters.ToJsonString());
|
||||
|
||||
[Fact]
|
||||
public async Task A_remote_thread_brings_the_replies_that_never_reached_privapub()
|
||||
{
|
||||
var persona = Pasture.Personas().First();
|
||||
var mark = Guid.NewGuid().ToString("N")[..8];
|
||||
var root = await Drive(Author, "post", new JsonObject { ["text"] = $"a thread to open {mark}", ["visibility"] = "public" });
|
||||
var rootUri = root["uri"]!.GetValue<string>();
|
||||
string replyUri = null;
|
||||
try
|
||||
{
|
||||
var reply = await Drive(Outsider, "post", new JsonObject
|
||||
{
|
||||
["text"] = $"said where nobody listens {mark}", ["visibility"] = "public", ["reply_to_uri"] = rootUri
|
||||
});
|
||||
replyUri = reply["uri"]!.GetValue<string>();
|
||||
var found = await PersonaApi.Call(persona, $"/api/v2/search?q={Uri.EscapeDataString(rootUri)}&resolve=true&type=statuses");
|
||||
var rootId = found!["statuses"]!.AsArray().Single()!["id"]!.GetValue<string>();
|
||||
await Task.Delay(3000);
|
||||
Assert.SkipWhen((await Pasture.Stored("privapub", replyUri))[replyUri]?["exists"]?.GetValue<bool>() == true,
|
||||
$"{Outsider}'s reply reached PrivaPub by delivery, so the thread proves nothing");
|
||||
|
||||
await using var session = await SignIn.As(browser, persona);
|
||||
var page = session.Page;
|
||||
await Results.Cell("client.render.thread-backfill", Platform, persona.UserName, async () =>
|
||||
{
|
||||
await page.GotoAsync($"{Pasture.Site}/expand/{rootId}");
|
||||
await page.Locator($"article[data-uri='{rootUri}']").First.WaitForAsync(new() { Timeout = 30000 });
|
||||
var answer = page.Locator($"article[data-uri='{replyUri}']");
|
||||
await Assertions.Expect(answer).ToContainTextAsync($"said where nobody listens {mark}", new() { Timeout = 25000 });
|
||||
await session.AssertGuards();
|
||||
}, page);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (replyUri is not null)
|
||||
await Drive(Outsider, "delete", new JsonObject { ["uri"] = replyUri });
|
||||
await Drive(Author, "delete", new JsonObject { ["uri"] = rootUri });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user