The feed hears new posts as they come

decePub opens PrivaPub's streaming API (one WebSocket for the persona, its
token given as the socket's protocol, never in a URL) and subscribes to the
stream of the feed in view: home, a list, local, federated, a hashtag or
notifications. New posts wait behind the "new" button as polled ones did;
a deletion or an edit applies at once. Polling stays for the feeds PrivaPub
does not stream and for any feed while the socket is down, which opens again
after a pause growing to a minute. The e2e test sees a post made elsewhere
in seconds and its deletion take the card away.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-05 04:15:54 +02:00
1 parent 6d5cf17723
commit 963565b316
7 files changed
+312 -5

No files matched your search

+52
View File
@@ -0,0 +1,52 @@
using System.Text.Json.Nodes;
using decePubClient.E2E.Support;
using Microsoft.Playwright;
namespace decePubClient.E2E.Actions
{
// The home feed hears of a new post and of its deletion through PrivaPub's streaming API, in seconds, where polling
// would take up to 45
public sealed class StreamingTests(Browser browser)
{
[Fact]
public async Task A_post_made_elsewhere_waits_behind_the_new_button_and_its_deletion_takes_it_away()
{
var persona = Pasture.Personas().First();
var text = $"streamed {Guid.NewGuid():N}"[..21];
await using var session = await SignIn.As(browser, persona);
var page = session.Page;
JsonNode post = null;
try
{
await Results.Cell("client.render.stream", "privapub", persona.UserName, async () =>
{
await page.GotoAsync($"{Pasture.Site}/");
await page.EvaluateAsync("() => localStorage.removeItem('FeedFilter')");
await page.GotoAsync($"{Pasture.Site}/");
await page.Locator("article[data-uri], [data-feed-empty]").First.WaitForAsync(new() { Timeout = 30000 });
// the stream opens once the first page is shown
await Task.Delay(1500);
post = await PersonaApi.Call(persona, "/api/v1/statuses", HttpMethod.Post, new JsonObject { ["status"] = text, ["visibility"] = "unlisted" });
var fresh = page.GetByRole(AriaRole.Button, new() { Name = "1 new" });
await fresh.WaitForAsync(new() { Timeout = 10000 });
await fresh.ClickAsync();
var card = page.Locator($"article[data-uri='{post!["uri"]!.GetValue<string>()}']");
await Assertions.Expect(card).ToContainTextAsync(text, new() { Timeout = 5000 });
await PersonaApi.Call(persona, $"/api/v1/statuses/{post["id"]!.GetValue<string>()}", HttpMethod.Delete);
post = null;
await Assertions.Expect(card).ToHaveCountAsync(0, new() { Timeout = 10000 });
await session.AssertGuards();
}, page);
}
finally
{
if (post is not null)
await PersonaApi.Call(persona, $"/api/v1/statuses/{post["id"]!.GetValue<string>()}", HttpMethod.Delete);
}
}
}
}