From 4daa6127545f750716622f6c130671aaf643b4ea Mon Sep 17 00:00:00 2001 From: lifeparticle Date: Sun, 10 Mar 2024 17:39:24 +1100 Subject: [PATCH 1/6] refactor newsfeed page --- api/newsfeed/helper.js | 28 ++++++++++ api/newsfeed/index.js | 19 ++++++- api/newsfeed/package.json | 3 +- api/newsfeed/yarn.lock | 11 ++++ ui/.env.development | 1 + ui/.env.production | 1 + .../pages/Newsfeed/__tests__/helper.test.ts | 51 ------------------- ui/src/pages/Newsfeed/constants.ts | 45 ++++++---------- ui/src/pages/Newsfeed/helper.ts | 28 ---------- ui/src/pages/Newsfeed/useNewsFeed.ts | 20 +++----- ui/tsconfig.json | 2 +- 11 files changed, 83 insertions(+), 126 deletions(-) create mode 100644 api/newsfeed/helper.js create mode 100644 ui/.env.development create mode 100644 ui/.env.production delete mode 100644 ui/src/pages/Newsfeed/__tests__/helper.test.ts delete mode 100644 ui/src/pages/Newsfeed/helper.ts diff --git a/api/newsfeed/helper.js b/api/newsfeed/helper.js new file mode 100644 index 000000000..3418adeed --- /dev/null +++ b/api/newsfeed/helper.js @@ -0,0 +1,28 @@ +import xml2js from "xml2js"; +export function parseXML(value) { + return new Promise((resolve, reject) => { + const parser = new xml2js.Parser({ + explicitArray: false, + ignoreAttrs: true, + }); + parser.parseString(value, (err, result) => { + if (err) reject(err); + else { + const items = result.rss.channel.item; + const list = items.map((item) => ({ + title: item.title, + pubDate: item.pubDate, + url: item.link, + image: extractImage(item.description), + })); + resolve(list); + } + }); + }); +} + +function extractImage(description) { + const regex = / { const sites = { "frontend-focus": "https://cprss.s3.amazonaws.com/frontendfoc.us.xml", "react-status": "https://cprss.s3.amazonaws.com/react.statuscode.com.xml", + "news-api": + "https://raw.githubusercontent.com/lifeparticle/binarytree/main/api/news/news.json", }; const response = await axios.get(sites[sitename], { responseType: "arraybuffer", @@ -33,9 +36,21 @@ app.get("/rss", async (req, res) => { } res.setHeader("Cache-Control", "s-max-age=86400, stale-while-revalidate"); - res.set("Content-Type", "application/xml"); + res.set("Content-Type", "application/json"); - res.send(response.data); + if (sitename === "news-api") res.send(response.data); + + const xmlData = response.data.toString(); + + parseXML(xmlData) + .then((parsedData) => { + console.log(parsedData); + res.send({ articles: parsedData }); + }) + .catch((error) => { + console.error("Error parsing XML:", error); + res.status(500).json({ error: "Error parsing XML" }); + }); } catch (error) { if (error instanceof Error) { res.status(500).json({ type: "error", message: error.message }); diff --git a/api/newsfeed/package.json b/api/newsfeed/package.json index 12e0aeda3..98955f500 100644 --- a/api/newsfeed/package.json +++ b/api/newsfeed/package.json @@ -16,7 +16,8 @@ "dependencies": { "axios": "^1.5.0", "express": "^4.18.2", - "rss-parser": "^3.13.0" + "rss-parser": "^3.13.0", + "xml2js": "^0.6.2" }, "devDependencies": { "nodemon": "^3.0.1" diff --git a/api/newsfeed/yarn.lock b/api/newsfeed/yarn.lock index dd563548c..0c98fa927 100644 --- a/api/newsfeed/yarn.lock +++ b/api/newsfeed/yarn.lock @@ -1228,6 +1228,7 @@ __metadata: express: ^4.18.2 nodemon: ^3.0.1 rss-parser: ^3.13.0 + xml2js: ^0.6.2 languageName: unknown linkType: soft @@ -1896,6 +1897,16 @@ __metadata: languageName: node linkType: hard +"xml2js@npm:^0.6.2": + version: 0.6.2 + resolution: "xml2js@npm:0.6.2" + dependencies: + sax: ">=0.6.0" + xmlbuilder: ~11.0.0 + checksum: 458a83806193008edff44562c0bdb982801d61ee7867ae58fd35fab781e69e17f40dfeb8fc05391a4648c9c54012066d3955fe5d993ffbe4dc63399023f32ac2 + languageName: node + linkType: hard + "xmlbuilder@npm:~11.0.0": version: 11.0.1 resolution: "xmlbuilder@npm:11.0.1" diff --git a/ui/.env.development b/ui/.env.development new file mode 100644 index 000000000..d29b4292f --- /dev/null +++ b/ui/.env.development @@ -0,0 +1 @@ +VITE_VERCEL_NEWS_FEED_URL=http://localhost:3000/rss?name= \ No newline at end of file diff --git a/ui/.env.production b/ui/.env.production new file mode 100644 index 000000000..adf5d2ab7 --- /dev/null +++ b/ui/.env.production @@ -0,0 +1 @@ +VITE_VERCEL_NEWS_FEED_URL=https://binarytree-rssfeed-api.vercel.app/rss?name= \ No newline at end of file diff --git a/ui/src/pages/Newsfeed/__tests__/helper.test.ts b/ui/src/pages/Newsfeed/__tests__/helper.test.ts deleted file mode 100644 index aae3ff628..000000000 --- a/ui/src/pages/Newsfeed/__tests__/helper.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -// helpers -import { parseXML, extractImage } from "pages/Newsfeed/helper"; -// mocks -import { mockedXMLString, mockedParsedXML, mockedXMLStringMissingDescription } from "./newsFeed.testkit"; -describe("parseXML", () => { - it("should parse valid XML string", () => { - // act - const response = parseXML(mockedXMLString); - - // assert - expect(response).toEqual(mockedParsedXML); - }); - - it('should return an empty array on invalid xml', () => { - // act - const response = parseXML('invalid xml'); - - // assert - expect(response).toEqual([]); - }) - - it('should throw an error on missing description tag', () => { - // assert - expect(() => parseXML(mockedXMLStringMissingDescription)).toThrow() - }) -}); - -describe("extractImage", () => { - it("should extract image URL from description", () => { - // arrange - const description = - 'Check out this image: Image'; - - // act - const response = extractImage(description); - - // assert - expect(response).toBe("https://example.com/image.jpg"); - }); - - it("should return null if no image URL found in description", () => { - // arrange - const description = "This is a text description without any image."; - - // act - const response = extractImage(description); - - // assert - expect(response).toBeNull(); - }); -}); diff --git a/ui/src/pages/Newsfeed/constants.ts b/ui/src/pages/Newsfeed/constants.ts index f32b2401e..ecd9370fa 100644 --- a/ui/src/pages/Newsfeed/constants.ts +++ b/ui/src/pages/Newsfeed/constants.ts @@ -1,45 +1,32 @@ import { TabsProps } from "antd"; +export const BASE_URL = import.meta.env.VITE_VERCEL_NEWS_FEED_URL; + +export const QUERY_KEY_NEWS = "news"; + const SITE_OPTIONS = { "frontend-focus": { label: "Frontend Focus", value: "frontend-focus", - isFeedItem: true, + show: true, }, "react-status": { label: "React Status", value: "react-status", - isFeedItem: true, - }, - news: { - label: "News", - value: "https://raw.githubusercontent.com/lifeparticle/binarytree/main/api/news/news.json", - isFeedItem: false, - }, -}; - -const TAB_ITEMS: TabsProps["items"] = [ - { - key: SITE_OPTIONS["frontend-focus"].value, - label: SITE_OPTIONS["frontend-focus"].label, - show: true, - }, - { - key: SITE_OPTIONS["react-status"].value, - label: SITE_OPTIONS["react-status"].label, show: true, }, - { - key: SITE_OPTIONS["news"].value, - label: SITE_OPTIONS["news"].label, + "news-api": { + label: "News", + value: "news-api", show: true, }, -].filter((item) => item.show); - -const BASE_URL = import.meta.env.DEV - ? "http://localhost:3000/rss?name=" - : "https://binarytree-rssfeed-api.vercel.app/rss?name="; +}; -const QUERY_KEY_NEWS = "news"; +export const FIRST_TAB_VALUE = SITE_OPTIONS["frontend-focus"].value; -export { SITE_OPTIONS, TAB_ITEMS, BASE_URL, QUERY_KEY_NEWS }; +export const TAB_ITEMS: TabsProps["items"] = Object.values(SITE_OPTIONS) + .filter((option) => option.show) + .map((option) => ({ + key: option.value, + label: option.label, + })); diff --git a/ui/src/pages/Newsfeed/helper.ts b/ui/src/pages/Newsfeed/helper.ts deleted file mode 100644 index 4b6ce181e..000000000 --- a/ui/src/pages/Newsfeed/helper.ts +++ /dev/null @@ -1,28 +0,0 @@ -function parseXML(value: string) { - const parser = new DOMParser(); - const xmldoc = parser.parseFromString(value, "text/xml"); - - const items = xmldoc.getElementsByTagName("item"); - const list = []; - - for (const item of items) { - const title = item.getElementsByTagName("title")[0].textContent; - const description = - item.getElementsByTagName("description")[0].textContent; - const pubDate = item.getElementsByTagName("pubDate")[0].textContent; - const url = item.getElementsByTagName("link")[0].textContent; - const image = description && extractImage(description); - - list.push({ title, pubDate, url, image }); - } - - return list; -} - -function extractImage(description: string) { - const regex = / { - const [url, setUrl] = useState(SITE_OPTIONS["frontend-focus"].value); - const isFeedItem = Object.values(SITE_OPTIONS).find( - (item) => item.isFeedItem && item.value === url - ); + const [url, setUrl] = useState(FIRST_TAB_VALUE); + + const { data, isLoading, isError } = useFetch(url, `${BASE_URL}${url}`); + + const items = data?.articles; - const { data, isLoading, isError } = useFetch( - url, - isFeedItem ? `${BASE_URL}${url}` : url - ); - let items = undefined; - if (data) { - items = isFeedItem ? parseXML(data) : data.articles; - } return { data: items, isLoading, isError, setUrl }; }; diff --git a/ui/tsconfig.json b/ui/tsconfig.json index 1f751de16..27220b17e 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -22,6 +22,6 @@ "noFallthroughCasesInSwitch": true, "types": ["vitest/globals"] }, - "include": ["src"], + "include": ["src", "../api/newsfeed/helper.js"], "references": [{ "path": "./tsconfig.node.json" }] } From 81542f230eff8aa57eb93b6cef420f1cd4e574e7 Mon Sep 17 00:00:00 2001 From: lifeparticle Date: Sun, 10 Mar 2024 17:54:29 +1100 Subject: [PATCH 2/6] rename url to tab --- ui/src/pages/Newsfeed/Newsfeed.tsx | 4 +- ui/src/pages/Newsfeed/__tests__/News.test.tsx | 10 +- .../Newsfeed/__tests__/useNewsFeed.test.ts | 215 +++++++++--------- ui/src/pages/Newsfeed/useNewsFeed.ts | 6 +- 4 files changed, 122 insertions(+), 113 deletions(-) diff --git a/ui/src/pages/Newsfeed/Newsfeed.tsx b/ui/src/pages/Newsfeed/Newsfeed.tsx index e0bb1a9ac..0433c3ce9 100644 --- a/ui/src/pages/Newsfeed/Newsfeed.tsx +++ b/ui/src/pages/Newsfeed/Newsfeed.tsx @@ -7,14 +7,14 @@ import style from "./Newsfeed.module.scss"; import useNewsFeed from "./useNewsFeed"; const Newsfeed: FC = () => { - const { data, isLoading, isError, setUrl } = useNewsFeed(); + const { data, isLoading, isError, setTab } = useNewsFeed(); return ( <> { - setUrl(value); + setTab(value); }} className={style.newsfeed_tabs} /> diff --git a/ui/src/pages/Newsfeed/__tests__/News.test.tsx b/ui/src/pages/Newsfeed/__tests__/News.test.tsx index 23fcdd113..31a47483f 100644 --- a/ui/src/pages/Newsfeed/__tests__/News.test.tsx +++ b/ui/src/pages/Newsfeed/__tests__/News.test.tsx @@ -24,7 +24,7 @@ vi.mock("components/ComponentInjector", () => ({ const user = userEvent.setup(); describe("News component", () => { - const mockSetUrl = vi.fn(); + const mockSetTab = vi.fn(); const newsFeedData = ["some data"]; beforeEach(() => { @@ -32,7 +32,7 @@ describe("News component", () => { data: newsFeedData, isLoading: false, isError: false, - setUrl: mockSetUrl, + setTab: mockSetTab, }); }); @@ -62,7 +62,7 @@ describe("News component", () => { expect(newsTab).toHaveTextContent("News"); }); - it("calls the seturl function when the tab is changed", async () => { + it("calls the SetTabValue function when the tab is changed", async () => { // act render(); @@ -73,11 +73,11 @@ describe("News component", () => { await user.click(reactTab); - expect(mockSetUrl).toHaveBeenCalledWith("react-status"); + expect(mockSetTab).toHaveBeenCalledWith("react-status"); await user.click(newsTab); - expect(mockSetUrl).toHaveBeenCalledWith( + expect(mockSetTab).toHaveBeenCalledWith( "https://raw.githubusercontent.com/lifeparticle/binarytree/main/api/news/news.json" ); }); diff --git a/ui/src/pages/Newsfeed/__tests__/useNewsFeed.test.ts b/ui/src/pages/Newsfeed/__tests__/useNewsFeed.test.ts index bf19793f9..72fd8801e 100644 --- a/ui/src/pages/Newsfeed/__tests__/useNewsFeed.test.ts +++ b/ui/src/pages/Newsfeed/__tests__/useNewsFeed.test.ts @@ -27,215 +27,227 @@ describe("useNewsFeed", () => { it("should return initial state", () => { // act const { result } = renderHook(() => useNewsFeed()); - + // assert expect(result.current).toEqual({ data: mockedParsedXML, isLoading: false, isError: false, - setUrl: expect.any(Function), + setTab: expect.any(Function), }); expect(useFetch).toHaveBeenCalledWith( - 'frontend-focus', + "frontend-focus", "http://localhost:3000/rss?name=frontend-focus" - ) + ); }); it("should return react-status state", () => { // arrange - vi.mocked(useFetch).mockReturnValueOnce({ - data: mockedXMLString, - isLoading: false, - isError: false, - }).mockReturnValue({ - data: mockedReactXMLString, - isLoading: false, - isError: false, - }); + vi.mocked(useFetch) + .mockReturnValueOnce({ + data: mockedXMLString, + isLoading: false, + isError: false, + }) + .mockReturnValue({ + data: mockedReactXMLString, + isLoading: false, + isError: false, + }); const { result } = renderHook(() => useNewsFeed()); // act act(() => { - result.current.setUrl('react-status'); - }) + result.current.setTab("react-status"); + }); // assert expect(result.current).toEqual({ data: mockedParsedReactXML, isLoading: false, isError: false, - setUrl: expect.any(Function), + setTab: expect.any(Function), }); expect(useFetch).toHaveBeenCalledWith( - 'react-status', + "react-status", "http://localhost:3000/rss?name=react-status" - ) + ); }); it("should return react-status state with loading as true", () => { // arrange - vi.mocked(useFetch).mockReturnValueOnce({ - data: undefined, - isLoading: false, - isError: false, - }).mockReturnValue({ - data: undefined, - isLoading: true, - isError: false, - }); + vi.mocked(useFetch) + .mockReturnValueOnce({ + data: undefined, + isLoading: false, + isError: false, + }) + .mockReturnValue({ + data: undefined, + isLoading: true, + isError: false, + }); const { result } = renderHook(() => useNewsFeed()); // act act(() => { - result.current.setUrl('react-status'); - }) + result.current.setTab("react-status"); + }); // assert expect(result.current).toEqual({ data: undefined, isLoading: true, isError: false, - setUrl: expect.any(Function), + setTab: expect.any(Function), }); expect(useFetch).toHaveBeenCalledWith( - 'react-status', + "react-status", "http://localhost:3000/rss?name=react-status" - ) + ); }); it("should return react-status state with error as true", () => { // arrange - vi.mocked(useFetch).mockReturnValueOnce({ - data: undefined, - isLoading: false, - isError: false, - }).mockReturnValue({ - data: undefined, - isLoading: false, - isError: true, - }); + vi.mocked(useFetch) + .mockReturnValueOnce({ + data: undefined, + isLoading: false, + isError: false, + }) + .mockReturnValue({ + data: undefined, + isLoading: false, + isError: true, + }); const { result } = renderHook(() => useNewsFeed()); // act act(() => { - result.current.setUrl('react-status'); - }) + result.current.setTab("react-status"); + }); // assert expect(result.current).toEqual({ data: undefined, isLoading: false, isError: true, - setUrl: expect.any(Function), + setTab: expect.any(Function), }); expect(useFetch).toHaveBeenCalledWith( - 'react-status', + "react-status", "http://localhost:3000/rss?name=react-status" - ) + ); }); it("should return news state", () => { // arrange - vi.mocked(useFetch).mockReturnValueOnce({ - data: undefined, - isLoading: false, - isError: false, - }).mockReturnValue({ - data: { - articles: ['some articles'], - }, - isLoading: false, - isError: false, - }); + vi.mocked(useFetch) + .mockReturnValueOnce({ + data: undefined, + isLoading: false, + isError: false, + }) + .mockReturnValue({ + data: { + articles: ["some articles"], + }, + isLoading: false, + isError: false, + }); const { result } = renderHook(() => useNewsFeed()); // act act(() => { - result.current.setUrl('news'); - }) + result.current.setTab("news"); + }); // assert expect(result.current).toEqual({ - data: ['some articles'], + data: ["some articles"], isLoading: false, isError: false, - setUrl: expect.any(Function), + setTab: expect.any(Function), }); expect(useFetch).toHaveBeenCalledWith( - 'react-status', + "react-status", "http://localhost:3000/rss?name=react-status" - ) + ); }); it("should return news state with loading as true and undefined data", () => { // arrange - vi.mocked(useFetch).mockReturnValueOnce({ - data: undefined, - isLoading: false, - isError: false, - }).mockReturnValue({ - data: undefined, - isLoading: true, - isError: false, - }); + vi.mocked(useFetch) + .mockReturnValueOnce({ + data: undefined, + isLoading: false, + isError: false, + }) + .mockReturnValue({ + data: undefined, + isLoading: true, + isError: false, + }); const { result } = renderHook(() => useNewsFeed()); // act act(() => { - result.current.setUrl('news'); - }) + result.current.setTab("news"); + }); // assert expect(result.current).toEqual({ data: undefined, isLoading: true, isError: false, - setUrl: expect.any(Function), + setTab: expect.any(Function), }); expect(useFetch).toHaveBeenCalledWith( - 'react-status', + "react-status", "http://localhost:3000/rss?name=react-status" - ) + ); }); it("should return news state with error and undefined data", () => { // arrange - vi.mocked(useFetch).mockReturnValueOnce({ - data: undefined, - isLoading: false, - isError: false, - }).mockReturnValue({ - data: undefined, - isLoading: false, - isError: true, - }); + vi.mocked(useFetch) + .mockReturnValueOnce({ + data: undefined, + isLoading: false, + isError: false, + }) + .mockReturnValue({ + data: undefined, + isLoading: false, + isError: true, + }); const { result } = renderHook(() => useNewsFeed()); // act act(() => { - result.current.setUrl('news'); - }) + result.current.setTab("news"); + }); // assert expect(result.current).toEqual({ data: undefined, isLoading: false, isError: true, - setUrl: expect.any(Function), + setTab: expect.any(Function), }); expect(useFetch).toHaveBeenCalledWith( - 'react-status', + "react-status", "http://localhost:3000/rss?name=react-status" - ) + ); }); - it('should return an error on wrong url option', () => { + it("should return an error on wrong url option", () => { // arrange vi.mocked(useFetch).mockReturnValue({ data: undefined, @@ -247,23 +259,20 @@ describe("useNewsFeed", () => { // act act(() => { - result.current.setUrl('wrong-url'); - }) + result.current.setTab("wrong-url"); + }); // assert expect(result.current).toEqual({ data: undefined, isLoading: false, isError: true, - setUrl: expect.any(Function), + setTab: expect.any(Function), }); - expect(useFetch).toHaveBeenCalledWith( - 'wrong-url', - "wrong-url" - ) - }) + expect(useFetch).toHaveBeenCalledWith("wrong-url", "wrong-url"); + }); - it('should break the hook on missing description xml string', () => { + it("should break the hook on missing description xml string", () => { // arrange vi.mocked(useFetch).mockReturnValue({ data: mockedXMLStringMissingDescription, @@ -272,6 +281,6 @@ describe("useNewsFeed", () => { }); // assert - expect(() => renderHook(() => useNewsFeed())).toThrow() - }) + expect(() => renderHook(() => useNewsFeed())).toThrow(); + }); }); diff --git a/ui/src/pages/Newsfeed/useNewsFeed.ts b/ui/src/pages/Newsfeed/useNewsFeed.ts index 604bf7e5d..512a32c13 100644 --- a/ui/src/pages/Newsfeed/useNewsFeed.ts +++ b/ui/src/pages/Newsfeed/useNewsFeed.ts @@ -3,13 +3,13 @@ import { BASE_URL, FIRST_TAB_VALUE } from "./constants"; import { useFetch } from "hooks"; const useNewsFeed = () => { - const [url, setUrl] = useState(FIRST_TAB_VALUE); + const [tab, setTab] = useState(FIRST_TAB_VALUE); - const { data, isLoading, isError } = useFetch(url, `${BASE_URL}${url}`); + const { data, isLoading, isError } = useFetch(tab, `${BASE_URL}${tab}`); const items = data?.articles; - return { data: items, isLoading, isError, setUrl }; + return { data: items, isLoading, isError, setTab }; }; export default useNewsFeed; From c6102186e2c032487040fab64f3470ebc22e3137 Mon Sep 17 00:00:00 2001 From: lifeparticle Date: Sun, 10 Mar 2024 21:59:24 +1100 Subject: [PATCH 3/6] rm unused tsconfig --- ui/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/tsconfig.json b/ui/tsconfig.json index 27220b17e..1f751de16 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -22,6 +22,6 @@ "noFallthroughCasesInSwitch": true, "types": ["vitest/globals"] }, - "include": ["src", "../api/newsfeed/helper.js"], + "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] } From f4552e4508e2b29acf7a2ba7bfec9e51918c737d Mon Sep 17 00:00:00 2001 From: lifeparticle Date: Fri, 29 Mar 2024 12:43:46 +1100 Subject: [PATCH 4/6] fix tests --- ui/.env.test | 1 + ui/src/pages/Newsfeed/__tests__/News.test.tsx | 84 ------- .../Newsfeed/__tests__/Newsfeed.test.tsx | 94 ++++++++ .../Newsfeed/__tests__/newsFeed.testkit.ts | 211 ++++++++---------- .../Newsfeed/__tests__/useNewsFeed.test.ts | 36 ++- ui/src/pages/Newsfeed/constants.ts | 4 +- ui/vite.config.ts | 4 +- 7 files changed, 201 insertions(+), 233 deletions(-) create mode 100644 ui/.env.test delete mode 100644 ui/src/pages/Newsfeed/__tests__/News.test.tsx create mode 100644 ui/src/pages/Newsfeed/__tests__/Newsfeed.test.tsx diff --git a/ui/.env.test b/ui/.env.test new file mode 100644 index 000000000..a232aaea3 --- /dev/null +++ b/ui/.env.test @@ -0,0 +1 @@ +VITE_VERCEL_NEWS_FEED_URL=http://localhost:3000/rss?name= diff --git a/ui/src/pages/Newsfeed/__tests__/News.test.tsx b/ui/src/pages/Newsfeed/__tests__/News.test.tsx deleted file mode 100644 index 31a47483f..000000000 --- a/ui/src/pages/Newsfeed/__tests__/News.test.tsx +++ /dev/null @@ -1,84 +0,0 @@ -// component -import News from "pages/Newsfeed"; -// dependencies -import { vi } from "vitest"; -import { render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -// hooks -import useNewsFeed from "pages/Newsfeed/useNewsFeed"; -// types -import { ListSearchResultsProps } from "components/ComponentInjector/ListSearchResults/ListSearchResults"; - -vi.mock("pages/Newsfeed/useNewsFeed"); - -type Items = unknown[]; - -const mockListSearchResultsProps = vi.fn(); -vi.mock("components/ComponentInjector", () => ({ - ListSearchResults: (props: ListSearchResultsProps) => { - mockListSearchResultsProps(props); - return
; - }, -})); - -const user = userEvent.setup(); - -describe("News component", () => { - const mockSetTab = vi.fn(); - const newsFeedData = ["some data"]; - - beforeEach(() => { - vi.mocked(useNewsFeed).mockReturnValue({ - data: newsFeedData, - isLoading: false, - isError: false, - setTab: mockSetTab, - }); - }); - - it("renders component", () => { - // act - render(); - - // assert - expect(screen.getByTestId("list-search-results")).toBeInTheDocument(); - expect(mockListSearchResultsProps).toHaveBeenCalledWith({ - isError: false, - isLoading: false, - itemComponent: expect.any(Function), - items: ["some data"], - resourceName: "news", - }); - - const tabs = screen.getAllByRole("tab"); - expect(tabs).toHaveLength(3); - - const frontendTab = tabs[0]; - const reactTab = tabs[1]; - const newsTab = tabs[2]; - - expect(frontendTab).toHaveTextContent("Frontend"); - expect(reactTab).toHaveTextContent("React"); - expect(newsTab).toHaveTextContent("News"); - }); - - it("calls the SetTabValue function when the tab is changed", async () => { - // act - render(); - - // assert - const tabs = screen.getAllByRole("tab"); - const reactTab = tabs[1]; - const newsTab = tabs[2]; - - await user.click(reactTab); - - expect(mockSetTab).toHaveBeenCalledWith("react-status"); - - await user.click(newsTab); - - expect(mockSetTab).toHaveBeenCalledWith( - "https://raw.githubusercontent.com/lifeparticle/binarytree/main/api/news/news.json" - ); - }); -}); diff --git a/ui/src/pages/Newsfeed/__tests__/Newsfeed.test.tsx b/ui/src/pages/Newsfeed/__tests__/Newsfeed.test.tsx new file mode 100644 index 000000000..b1eb7ba95 --- /dev/null +++ b/ui/src/pages/Newsfeed/__tests__/Newsfeed.test.tsx @@ -0,0 +1,94 @@ +// component +import Newsfeed from "pages/Newsfeed"; +// dependencies +import { vi } from "vitest"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +// hooks +import useNewsFeed from "pages/Newsfeed/useNewsFeed"; +// types +import { ListSearchResultsProps } from "components/ComponentInjector/ListSearchResults/ListSearchResults"; +import { TAB_ITEMS } from "pages/Newsfeed/constants"; + +vi.mock("pages/Newsfeed/useNewsFeed"); + +type Items = unknown[]; + +const mockListSearchResultsProps = vi.fn(); +vi.mock("components/ComponentInjector", () => ({ + ListSearchResults: (props: ListSearchResultsProps) => { + mockListSearchResultsProps(props); + return
; + }, +})); + +const user = userEvent.setup(); + +describe("News component", () => { + const mockSetTab = vi.fn(); + const newsFeedData = ["some data"]; + + // Setup phase + beforeEach(() => { + vi.mocked(useNewsFeed).mockReturnValue({ + data: newsFeedData, + isLoading: false, + isError: false, + setTab: mockSetTab, + }); + + render(); + }); + + it("renders search results list", () => { + // Assert phase for the search results list + const listSearchResults = screen.getByTestId("list-search-results"); + expect(listSearchResults).toBeInTheDocument(); + }); + + it("calls mockListSearchResultsProps with correct arguments", () => { + // Assert phase for mock function call + expect(mockListSearchResultsProps).toHaveBeenCalledWith({ + isError: false, + isLoading: false, + itemComponent: expect.any(Function), + items: ["some data"], + resourceName: "news", + }); + }); + + describe("Tabs functionality", () => { + // Setup phase for tabs, run only if TAB_ITEMS is defined + let tabs: HTMLElement[]; + beforeEach(() => { + tabs = screen.getAllByRole("tab"); + }); + + it("renders the correct number of tabs", () => { + // Assert phase for the number of tabs + + expect(tabs).toHaveLength(TAB_ITEMS.length); + }); + + it("renders tabs with correct labels", () => { + // Assert phase for tab labels + TAB_ITEMS.forEach((tabItem, index) => { + expect(tabs[index]).toHaveTextContent(tabItem.label); + }); + }); + + it("calls the SetTabValue function with the correct argument when a tab is changed", async () => { + // Iterate over each tab item defined in TAB_ITEMS + TAB_ITEMS.forEach(async (tabItem, index) => { + // Simulate clicking on the tab + await user.click(tabs[index]); + + // Assert that mockSetTab was called with the 'key' of the current tabItem + expect(mockSetTab).toHaveBeenCalledWith(tabItem.key); + + // Reset mock function history after each assertion to ensure independence + mockSetTab.mockClear(); + }); + }); + }); +}); diff --git a/ui/src/pages/Newsfeed/__tests__/newsFeed.testkit.ts b/ui/src/pages/Newsfeed/__tests__/newsFeed.testkit.ts index 9203b0a7d..774e0b09d 100644 --- a/ui/src/pages/Newsfeed/__tests__/newsFeed.testkit.ts +++ b/ui/src/pages/Newsfeed/__tests__/newsFeed.testkit.ts @@ -1,142 +1,111 @@ -export const mockedXMLString = ` - - - - Example Title 1 - Example Description 1 <img src="example1.jpg" /> - Example Date 1 - https://example1.com - - - Example Title 2 - Example Description 2 <img src="example2.jpg" /> - Example Date 2 - https://example2.com - - - Example Title 3 - Example Description 3 <img src="example3.jpg" /> - Example Date 3 - https://example3.com - - - Example Title 4 - Example Description 4 <img src="example4.jpg" /> - Example Date 4 - https://example4.com - - - -`; +export const mockedJSON = { + articles: [ + { + title: "What you need to know about modern CSS in early 2024", + pubDate: "Wed, 27 Mar 2024 00:00:00 +0000", + url: "https://frontendfoc.us/issues/636", + image: "https://res.cloudinary.com/cpress/image/upload/w_1280,e_sharpen:60,q_auto/v1711545832/w3yas5q9cbsev11uehnl.png", + }, + { + title: "The web and the needs of humanity", + pubDate: "Wed, 20 Mar 2024 00:00:00 +0000", + url: "https://frontendfoc.us/issues/635", + image: "https://res.cloudinary.com/cpress/image/upload/w_1280,e_sharpen:60,q_auto/snpqguiz1s1vxwuegzcp.jpg", + }, + { + title: "Browsers buddy up to bring the speed", + pubDate: "Wed, 13 Mar 2024 00:00:00 +0000", + url: "https://frontendfoc.us/issues/634", + image: "https://res.cloudinary.com/cpress/image/upload/w_1280,e_sharpen:60,q_auto/v1710327708/n594ptv7ycltmtq8pw1x.png", + }, + { + title: "A week is a long time in web development", + pubDate: "Wed, 6 Mar 2024 00:00:00 +0000", + url: "https://frontendfoc.us/issues/633", + image: "https://copm.s3.amazonaws.com/c86bd3b0.png", + }, + ], +}; -export const mockedXMLStringMissingDescription = ` - - - - Example Title 1 - Example Date 1 - https://example1.com - - - Example Title 2 - Example Date 2 - https://example2.com - - - Example Title 3 - Example Date 3 - https://example3.com - - - Example Title 4 - Example Date 4 - https://example4.com - - - -`; - -export const mockedParsedXML = [ +export const mockedParsedJSON = [ { - title: "Example Title 1", - pubDate: "Example Date 1", - url: "https://example1.com", - image: "example1.jpg", + title: "What you need to know about modern CSS in early 2024", + pubDate: "Wed, 27 Mar 2024 00:00:00 +0000", + url: "https://frontendfoc.us/issues/636", + image: "https://res.cloudinary.com/cpress/image/upload/w_1280,e_sharpen:60,q_auto/v1711545832/w3yas5q9cbsev11uehnl.png", }, { - title: "Example Title 2", - pubDate: "Example Date 2", - url: "https://example2.com", - image: "example2.jpg", + title: "The web and the needs of humanity", + pubDate: "Wed, 20 Mar 2024 00:00:00 +0000", + url: "https://frontendfoc.us/issues/635", + image: "https://res.cloudinary.com/cpress/image/upload/w_1280,e_sharpen:60,q_auto/snpqguiz1s1vxwuegzcp.jpg", }, { - title: "Example Title 3", - pubDate: "Example Date 3", - url: "https://example3.com", - image: "example3.jpg", + title: "Browsers buddy up to bring the speed", + pubDate: "Wed, 13 Mar 2024 00:00:00 +0000", + url: "https://frontendfoc.us/issues/634", + image: "https://res.cloudinary.com/cpress/image/upload/w_1280,e_sharpen:60,q_auto/v1710327708/n594ptv7ycltmtq8pw1x.png", }, { - title: "Example Title 4", - pubDate: "Example Date 4", - url: "https://example4.com", - image: "example4.jpg", + title: "A week is a long time in web development", + pubDate: "Wed, 6 Mar 2024 00:00:00 +0000", + url: "https://frontendfoc.us/issues/633", + image: "https://copm.s3.amazonaws.com/c86bd3b0.png", }, ]; -export const mockedReactXMLString = ` - - - - Sample Title 1 - Sample Description 1 <img src="sample1.jpg" /> - Sample Date 1 - https://sample1.com - - - Sample Title 2 - Sample Description 2 <img src="sample2.jpg" /> - Sample Date 2 - https://sample2.com - - - Sample Title 3 - Sample Description 3 <img src="sample3.jpg" /> - Sample Date 3 - https://sample3.com - - - Sample Title 4 - Sample Description 4 <img src="sample4.jpg" /> - Sample Date 4 - https://sample4.com - - - -`; +export const mockedReactJSON = { + articles: [ + { + title: "React Server Components for everyone", + pubDate: "Wed, 27 Mar 2024 00:00:00 +0000", + url: "https://react.statuscode.com/issues/379", + image: "https://res.cloudinary.com/cpress/image/upload/w_1280,e_sharpen:60,q_auto/wjdxlkfytza3rry9sn7x.jpg", + }, + { + title: "Writing React components with CSS", + pubDate: "Wed, 20 Mar 2024 00:00:00 +0000", + url: "https://react.statuscode.com/issues/378", + image: "https://res.cloudinary.com/cpress/image/upload/w_1280,e_sharpen:60,q_auto/rwuskc0xupro8k5ubslb.jpg", + }, + { + title: "Listen to React performance issues", + pubDate: "Wed, 13 Mar 2024 00:00:00 +0000", + url: "https://react.statuscode.com/issues/377", + image: "https://res.cloudinary.com/cpress/image/upload/w_1280,e_sharpen:60,q_auto/dqzmfpcdovbxiin3azge.jpg", + }, + { + title: "A linting tool for React performance", + pubDate: "Wed, 6 Mar 2024 00:00:00 +0000", + url: "https://react.statuscode.com/issues/376", + image: "https://res.cloudinary.com/cpress/image/upload/w_1280,e_sharpen:60,q_auto/wn2qrnorgz8kelgw2als.jpg", + }, + ], +}; -export const mockedParsedReactXML = [ +export const mockedParsedReactJSON = [ { - title: "Sample Title 1", - pubDate: "Sample Date 1", - url: "https://sample1.com", - image: "sample1.jpg", + title: "React Server Components for everyone", + pubDate: "Wed, 27 Mar 2024 00:00:00 +0000", + url: "https://react.statuscode.com/issues/379", + image: "https://res.cloudinary.com/cpress/image/upload/w_1280,e_sharpen:60,q_auto/wjdxlkfytza3rry9sn7x.jpg", }, { - title: "Sample Title 2", - pubDate: "Sample Date 2", - url: "https://sample2.com", - image: "sample2.jpg", + title: "Writing React components with CSS", + pubDate: "Wed, 20 Mar 2024 00:00:00 +0000", + url: "https://react.statuscode.com/issues/378", + image: "https://res.cloudinary.com/cpress/image/upload/w_1280,e_sharpen:60,q_auto/rwuskc0xupro8k5ubslb.jpg", }, { - title: "Sample Title 3", - pubDate: "Sample Date 3", - url: "https://sample3.com", - image: "sample3.jpg", + title: "Listen to React performance issues", + pubDate: "Wed, 13 Mar 2024 00:00:00 +0000", + url: "https://react.statuscode.com/issues/377", + image: "https://res.cloudinary.com/cpress/image/upload/w_1280,e_sharpen:60,q_auto/dqzmfpcdovbxiin3azge.jpg", }, { - title: "Sample Title 4", - pubDate: "Sample Date 4", - url: "https://sample4.com", - image: "sample4.jpg", + title: "A linting tool for React performance", + pubDate: "Wed, 6 Mar 2024 00:00:00 +0000", + url: "https://react.statuscode.com/issues/376", + image: "https://res.cloudinary.com/cpress/image/upload/w_1280,e_sharpen:60,q_auto/wn2qrnorgz8kelgw2als.jpg", }, ]; diff --git a/ui/src/pages/Newsfeed/__tests__/useNewsFeed.test.ts b/ui/src/pages/Newsfeed/__tests__/useNewsFeed.test.ts index 72fd8801e..cdea785f0 100644 --- a/ui/src/pages/Newsfeed/__tests__/useNewsFeed.test.ts +++ b/ui/src/pages/Newsfeed/__tests__/useNewsFeed.test.ts @@ -6,11 +6,10 @@ import useNewsFeed from "pages/Newsfeed/useNewsFeed"; import { useFetch } from "hooks"; // mocks import { - mockedParsedXML, - mockedParsedReactXML, - mockedXMLString, - mockedReactXMLString, - mockedXMLStringMissingDescription, + mockedParsedJSON, + mockedParsedReactJSON, + mockedJSON, + mockedReactJSON, } from "./newsFeed.testkit"; vi.mock("hooks"); @@ -18,7 +17,7 @@ vi.mock("hooks"); describe("useNewsFeed", () => { beforeEach(() => { vi.mocked(useFetch).mockReturnValue({ - data: mockedXMLString, + data: mockedJSON, isLoading: false, isError: false, }); @@ -30,7 +29,7 @@ describe("useNewsFeed", () => { // assert expect(result.current).toEqual({ - data: mockedParsedXML, + data: mockedParsedJSON, isLoading: false, isError: false, setTab: expect.any(Function), @@ -45,12 +44,12 @@ describe("useNewsFeed", () => { // arrange vi.mocked(useFetch) .mockReturnValueOnce({ - data: mockedXMLString, + data: mockedJSON, isLoading: false, isError: false, }) .mockReturnValue({ - data: mockedReactXMLString, + data: mockedReactJSON, isLoading: false, isError: false, }); @@ -64,7 +63,7 @@ describe("useNewsFeed", () => { // assert expect(result.current).toEqual({ - data: mockedParsedReactXML, + data: mockedParsedReactJSON, isLoading: false, isError: false, setTab: expect.any(Function), @@ -269,18 +268,9 @@ describe("useNewsFeed", () => { isError: true, setTab: expect.any(Function), }); - expect(useFetch).toHaveBeenCalledWith("wrong-url", "wrong-url"); - }); - - it("should break the hook on missing description xml string", () => { - // arrange - vi.mocked(useFetch).mockReturnValue({ - data: mockedXMLStringMissingDescription, - isLoading: false, - isError: false, - }); - - // assert - expect(() => renderHook(() => useNewsFeed())).toThrow(); + expect(useFetch).toHaveBeenCalledWith( + "wrong-url", + "http://localhost:3000/rss?name=wrong-url" + ); }); }); diff --git a/ui/src/pages/Newsfeed/constants.ts b/ui/src/pages/Newsfeed/constants.ts index ecd9370fa..6cd8c2f84 100644 --- a/ui/src/pages/Newsfeed/constants.ts +++ b/ui/src/pages/Newsfeed/constants.ts @@ -1,5 +1,3 @@ -import { TabsProps } from "antd"; - export const BASE_URL = import.meta.env.VITE_VERCEL_NEWS_FEED_URL; export const QUERY_KEY_NEWS = "news"; @@ -24,7 +22,7 @@ const SITE_OPTIONS = { export const FIRST_TAB_VALUE = SITE_OPTIONS["frontend-focus"].value; -export const TAB_ITEMS: TabsProps["items"] = Object.values(SITE_OPTIONS) +export const TAB_ITEMS = Object.values(SITE_OPTIONS) .filter((option) => option.show) .map((option) => ({ key: option.value, diff --git a/ui/vite.config.ts b/ui/vite.config.ts index 9c0250a8f..ba268132d 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -41,13 +41,13 @@ export default defineConfig({ include: ["**/*.test.{ts,tsx}"], setupFiles: ["./src/test/setup.ts"], css: true, - reporters: ['default', 'html'], + reporters: ["default", "html"], coverage: { reportsDirectory: "html/ui", include: ["**/*.{ts,tsx}"], exclude: ["**/*.test.{ts,tsx}", "**/types.ts", "**/*.testkit.ts"], reporter: ["text", ["html", { subdir: "coverage" }], "lcov"], provider: "v8", - } + }, }, }); From 77f579019cae800772c21e33f40236c8c8e010e0 Mon Sep 17 00:00:00 2001 From: lifeparticle Date: Fri, 29 Mar 2024 13:14:57 +1100 Subject: [PATCH 5/6] tidy --- .../Newsfeed/__tests__/Newsfeed.test.tsx | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/ui/src/pages/Newsfeed/__tests__/Newsfeed.test.tsx b/ui/src/pages/Newsfeed/__tests__/Newsfeed.test.tsx index b1eb7ba95..d54b0d4f2 100644 --- a/ui/src/pages/Newsfeed/__tests__/Newsfeed.test.tsx +++ b/ui/src/pages/Newsfeed/__tests__/Newsfeed.test.tsx @@ -22,6 +22,16 @@ vi.mock("components/ComponentInjector", () => ({ }, })); +const mockSetTab = vi.fn(); + +// Mock the useNewsFeed hook +vi.mock("./useNewsFeed", () => ({ + data: [], // Assume empty data for simplicity + isLoading: false, + isError: false, + setTab: mockSetTab, +})); + const user = userEvent.setup(); describe("News component", () => { @@ -77,18 +87,16 @@ describe("News component", () => { }); }); - it("calls the SetTabValue function with the correct argument when a tab is changed", async () => { - // Iterate over each tab item defined in TAB_ITEMS - TAB_ITEMS.forEach(async (tabItem, index) => { - // Simulate clicking on the tab - await user.click(tabs[index]); + it("calls setTab with the new tab value on tab change", async () => { + // This example simulates clicking the second tab + const secondTab = TAB_ITEMS[1]; + await user.click( + screen.getByRole("tab", { name: secondTab.label }) + ); - // Assert that mockSetTab was called with the 'key' of the current tabItem - expect(mockSetTab).toHaveBeenCalledWith(tabItem.key); - - // Reset mock function history after each assertion to ensure independence - mockSetTab.mockClear(); - }); + // Verify setTab was called with the value of the second tab + expect(mockSetTab).toHaveBeenCalledWith(secondTab.key); + mockSetTab.mockClear(); }); }); }); From 708c5d1e2fabef0928262e1ac13cf90f16e2bb61 Mon Sep 17 00:00:00 2001 From: lifeparticle Date: Fri, 29 Mar 2024 13:19:39 +1100 Subject: [PATCH 6/6] add changelog --- ui/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index da4c011a5..c0c41874e 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -1,3 +1,7 @@ +### [10.6.1] - 2024-03-29 + +- Update Newsfeed tests + ### [10.6.0] - 2024-01-24 - Added Information Page tests