v4: the followed feed's two views are pages — "From people" and "From playlists" on a feed's "follow" tab were a state of the page, which a reload, Back or a shared link lost, while a profile's same split is /@name/follow/playlists. They are now /follow and /follow/playlists, and /music/follow/playlists in a category, feedPath taking the view as userPath does (FOLLOW_VIEWS); picking a category keeps it. Their dumps come from GET /api/dumps?follow=users|playlists, signed in, which the feed's word for it names, instead of GET /api/me/feed?from=; an unknown category is a 404 there, as on the rest of the feed, rather than an empty list.
A feed's tab, as a profile's, a search's or the community page's, now takes the place of the current history entry, since it is a view of the same page; picking a category, another page, still adds one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,11 @@ import { act, screen, waitFor, within } from "@testing-library/react";
|
||||
import { userEvent } from "@testing-library/user-event";
|
||||
import { useLocation } from "react-router";
|
||||
import { Index } from "../../pages/Index.tsx";
|
||||
import { FEED_TABS, type FeedTab } from "../../../shared/paths.ts";
|
||||
import {
|
||||
FEED_TABS,
|
||||
type FeedTab,
|
||||
type FollowView,
|
||||
} from "../../../shared/paths.ts";
|
||||
import { calls, fail, route } from "../fetch.ts";
|
||||
import {
|
||||
makeCategory,
|
||||
@@ -22,14 +26,16 @@ const music = makeCategory({ id: "c-music", slug: "music", name: "Music" });
|
||||
/** Reads a feed's path as `App`'s routes do. */
|
||||
function Scoped() {
|
||||
const { pathname } = useLocation();
|
||||
const [first, second] = pathname.split("/").filter(Boolean);
|
||||
const isTab = (FEED_TABS as readonly string[]).includes(first ?? "");
|
||||
const categorySlug = first && !isTab ? first : undefined;
|
||||
const segments = pathname.split("/").filter(Boolean);
|
||||
const isTab = (FEED_TABS as readonly string[]).includes(segments[0] ?? "");
|
||||
const categorySlug = isTab ? undefined : segments[0];
|
||||
const [feedTab, followView] = segments.slice(isTab ? 0 : 1);
|
||||
return (
|
||||
<Index
|
||||
key={categorySlug ?? ""}
|
||||
categorySlug={categorySlug}
|
||||
feedTab={(isTab ? first : second) as FeedTab | undefined}
|
||||
feedTab={feedTab as FeedTab | undefined}
|
||||
followView={followView as FollowView | undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -96,7 +102,7 @@ describe("Index feeds", () => {
|
||||
});
|
||||
|
||||
test('a default tab stored as v3\'s "followed" is "follow" now', async () => {
|
||||
route("GET /api/me/feed", () => page([]));
|
||||
route("GET /api/dumps", () => page([]));
|
||||
localStorage.setItem("default-feed-tab", "followed");
|
||||
setup("/", { session: makeSession() });
|
||||
expect(
|
||||
@@ -273,13 +279,16 @@ describe("Index feeds", () => {
|
||||
});
|
||||
|
||||
describe("Followed feed", () => {
|
||||
test("from people and from playlists", async () => {
|
||||
const followCalls = () =>
|
||||
calls("GET /api/dumps").filter((r) => r.url.searchParams.has("follow"));
|
||||
|
||||
test("from people at /follow, from playlists at /follow/playlists", async () => {
|
||||
const session = makeSession();
|
||||
route(
|
||||
"GET /api/me/feed",
|
||||
"GET /api/dumps",
|
||||
(req) =>
|
||||
page(
|
||||
req.url.searchParams.get("from") === "users"
|
||||
req.url.searchParams.get("follow") === "users"
|
||||
? [makeDump({ title: "By a friend" })]
|
||||
: [],
|
||||
),
|
||||
@@ -287,20 +296,25 @@ describe("Followed feed", () => {
|
||||
const { user } = setup("/follow", { session });
|
||||
await screen.findByText("By a friend");
|
||||
await user.click(screen.getByRole("button", { name: "From playlists" }));
|
||||
expect(currentPath()).toBe("/follow/playlists");
|
||||
expect(
|
||||
screen.getByText("Follow some public playlists to see their dumps here."),
|
||||
).toBeInTheDocument();
|
||||
const [feed] = calls("GET /api/me/feed");
|
||||
const [feed] = followCalls();
|
||||
expect(feed.headers.get("Authorization")).toBe(`Bearer ${session.token}`);
|
||||
await user.click(screen.getByRole("button", { name: "From people" }));
|
||||
expect(currentPath()).toBe("/follow");
|
||||
});
|
||||
|
||||
test("a category narrows it", async () => {
|
||||
route("GET /api/me/feed", () => page([]));
|
||||
setup("/music/follow", { session: makeSession() });
|
||||
await screen.findByText("Follow some users to see their dumps here.");
|
||||
test("its view is in the path, a category's too", async () => {
|
||||
route("GET /api/dumps", () => page([]));
|
||||
setup("/music/follow/playlists", { session: makeSession() });
|
||||
await screen.findByText(
|
||||
"Follow some public playlists to see their dumps here.",
|
||||
);
|
||||
expect(
|
||||
calls("GET /api/me/feed").map((r) => [
|
||||
r.url.searchParams.get("from"),
|
||||
followCalls().map((r) => [
|
||||
r.url.searchParams.get("follow"),
|
||||
r.url.searchParams.get("category"),
|
||||
]).sort(),
|
||||
).toEqual([["playlists", "music"], ["users", "music"]]);
|
||||
@@ -308,9 +322,9 @@ describe("Followed feed", () => {
|
||||
|
||||
test("a server error reads as one, not as a lost connection", async () => {
|
||||
route(
|
||||
"GET /api/me/feed",
|
||||
"GET /api/dumps",
|
||||
(req) =>
|
||||
req.url.searchParams.get("from") === "users"
|
||||
req.url.searchParams.get("follow") === "users"
|
||||
? fail("Internal error", 500)
|
||||
: page([]),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user