Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 97 additions & 2 deletions __tests__/app/category/LoadArticle.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ jest.mock("#/helpers/network/WordPressAPI", () => ({
default: {
getPost: jest.fn(() => Promise.resolve(null)),
create: jest.fn(() => null),
convertLoadProps: jest.fn((data) => data),
getFeatureImage: jest.fn(() =>
Promise.resolve({
image: undefined,
thumb: undefined,
credit: undefined,
}),
),
},
}));

Expand Down Expand Up @@ -110,8 +118,9 @@ describe("LoadArticle article fallback (slug not found)", () => {

it("keeps the deep-link anchor on the fallback URL so the webview jumps to it", async () => {
const { useLocalSearchParams } = jest.requireMock("expo-router");
// Custom post types (e.g. /project/…) are not served by the posts API,
// so anchored deep links to them always land on this fallback.
// The mocked API still resolves no post here, so this exercises the same
// not-found fallback as any other category — see the "project post type"
// describe block below for the mapping itself.
useLocalSearchParams.mockReturnValue({
category: "project",
slug: "10fakten",
Expand Down Expand Up @@ -158,6 +167,92 @@ describe("LoadArticle article fallback (slug not found)", () => {
});
});

describe("LoadArticle project post type", () => {
beforeEach(() => jest.clearAllMocks());

it("queries the 'project' REST base instead of 'posts' for the project category", async () => {
const WordPressAPI = jest.requireMock(
"#/helpers/network/WordPressAPI",
).default;
const { useLocalSearchParams } = jest.requireMock("expo-router");
useLocalSearchParams.mockReturnValue({
category: "project",
slug: "orgakarten",
});

await render(<LoadArticle />);

await waitFor(() => expect(WordPressAPI.getPost).toHaveBeenCalled());
expect(WordPressAPI.getPost).toHaveBeenCalledWith(
"orgakarten",
expect.anything(),
"project",
);
});

it("queries the default 'posts' REST base for a regular category", async () => {
const WordPressAPI = jest.requireMock(
"#/helpers/network/WordPressAPI",
).default;
const { useLocalSearchParams } = jest.requireMock("expo-router");
useLocalSearchParams.mockReturnValue({
category: "faktencheck",
slug: "some-article",
});

await render(<LoadArticle />);

await waitFor(() => expect(WordPressAPI.getPost).toHaveBeenCalled());
expect(WordPressAPI.getPost).toHaveBeenCalledWith(
"some-article",
expect.anything(),
"posts",
);
});

it("renders natively on success, without fetching a feature image when the post has none", async () => {
const WordPressAPI = jest.requireMock(
"#/helpers/network/WordPressAPI",
).default;
// "project" entries aren't guaranteed to set a featured image, so
// "wp:featuredmedia" may be entirely absent from the response.
const projectPost = {
id: 1,
date: "2024-01-01T00:00:00",
date_gmt: "2024-01-01T00:00:00",
link: "https://volksverpetzer.de/project/orgakarten/",
slug: "orgakarten",
title: { rendered: "Orgakarten" },
content: { rendered: "<p>Hi</p>" },
_links: {},
};
WordPressAPI.getPost.mockResolvedValueOnce(projectPost);
WordPressAPI.convertLoadProps.mockImplementationOnce((data: any) => ({
...data,
title: data.title.rendered,
description: "",
authors: [],
categories: [],
}));

const { useLocalSearchParams } = jest.requireMock("expo-router");
useLocalSearchParams.mockReturnValue({
category: "project",
slug: "orgakarten",
});

await render(<LoadArticle />);

await waitFor(() => {
const Article = jest.requireMock(
"#/screens/Home/components/article/Article",
);
expect(Article).toHaveBeenCalled();
});
expect(WordPressAPI.getFeatureImage).not.toHaveBeenCalled();
});
});

describe("LoadArticle native article anchor", () => {
beforeEach(() => jest.clearAllMocks());

Expand Down
34 changes: 34 additions & 0 deletions __tests__/helpers/WordPressAPI.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,22 @@ describe("WordPressAPI", () => {
expect(result).toBeUndefined();
spy.mockRestore();
});

it("queries a custom post type's own REST base when given", async () => {
const spy = jest.spyOn(Networking, "get").mockResolvedValue([] as any);
await WordPressAPI.getPost("orgakarten", undefined, "project");
expect(spy).toHaveBeenCalledWith(
WordPressAPI["client"],
`/wp-json/wp/v2/project`,
{
params: {
slug: "orgakarten",
_embed: "author",
},
},
);
spy.mockRestore();
});
});

describe("getFeatureImage", () => {
Expand Down Expand Up @@ -378,5 +394,23 @@ describe("WordPressAPI", () => {
} as any);
expect(article.reading_time).toBeUndefined();
});

it("preserves categories when present in raw data", () => {
const article = WordPressAPI.convertLoadProps({
...baseData,
categories: [123],
} as any);
expect(article.categories).toEqual([123]);
});

it("defaults categories to an empty array when absent from raw data", () => {
// Custom post types (e.g. "project") aren't necessarily registered
// with the category taxonomy, so the field may be missing entirely.
const { categories: _categories, ...dataWithoutCategories } = baseData;
const article = WordPressAPI.convertLoadProps({
...dataWithoutCategories,
} as any);
expect(article.categories).toEqual([]);
});
});
});
30 changes: 24 additions & 6 deletions src/app/[category]/[slug].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ type LoadArticleParameters = {
"#"?: string;
};

// WordPress post types whose REST base differs from the default "posts" —
// for these, the URL's category segment is the post type's own rewrite
// slug rather than a category taxonomy term.
const CUSTOM_POST_TYPES: Record<string, string> = {
project: "project",
};

/**
* Loads an article based on the provided slug.
*/
Expand Down Expand Up @@ -73,9 +80,16 @@ const LoadArticle = () => {
return;
}

// Secondary WP sites (e.g. Prüfpunkt) are only ever fed the "posts"
// type today, so the custom-post-type mapping only applies to the
// primary site's lookup.
const _article = secondaryApi
? await secondaryApi.getPost(slug, signal)
: await WordPressAPI.getPost(slug, signal);
: await WordPressAPI.getPost(
slug,
signal,
CUSTOM_POST_TYPES[category ?? ""] ?? "posts",
);
if (signal.aborted) return;
// No post for this slug — fall back to the webview instead of letting
// convertLoadProps throw on undefined for control flow.
Expand All @@ -87,10 +101,14 @@ const LoadArticle = () => {
const loadedArticle: ArticleProperties =
WordPressAPI.convertLoadProps(_article);

const { image, credit } = await WordPressAPI.getFeatureImage(
loadedArticle._links["wp:featuredmedia"][0].href,
signal,
);
// Not every post type supports a featured image (e.g. "project"
// entries may not set one), so there's no "wp:featuredmedia" link
// to follow in that case.
const featuredMediaHref =
loadedArticle._links["wp:featuredmedia"]?.[0]?.href;
const { image, credit } = featuredMediaHref
? await WordPressAPI.getFeatureImage(featuredMediaHref, signal)
: { image: undefined, credit: undefined };

if (signal.aborted) return;
setArticle(loadedArticle);
Expand All @@ -104,7 +122,7 @@ const LoadArticle = () => {
setIsLoading(false);
}
},
[slug, secondaryApi, originalUrl, wpUrl],
[slug, category, secondaryApi, originalUrl, wpUrl],
);

useEffect(() => {
Expand Down
11 changes: 9 additions & 2 deletions src/helpers/network/WordPressAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,18 @@ export default class WordPressAPI {

/**
* Get a single post by slug.
* @param postType - REST base of the post type to query. Defaults to the
* standard "posts", but some content (e.g. the "project" post type) is
* registered under its own REST base instead.
*/
static async getPost(
slug: string,
signal?: AbortSignal,
postType: string = "posts",
): Promise<LoadArticlePostProperties | undefined> {
const posts = await netGet<LoadArticlePostProperties[]>(
WordPressAPI.client,
`/wp-json/wp/v2/posts`,
`/wp-json/wp/v2/${postType}`,
{
params: {
slug,
Expand Down Expand Up @@ -231,6 +235,9 @@ export default class WordPressAPI {
display_name: a.name,
slug: a.slug,
}));
return { ...data, title, description, authors };
// Custom post types (e.g. "project") aren't necessarily registered with
// the category taxonomy, so the field may be absent entirely.
const categories = data.categories ?? [];
return { ...data, title, description, authors, categories };
}
}
Loading