diff --git a/__mocks__/jest.setup.js b/__mocks__/jest.setup.js index 3a974f65..896e2141 100644 --- a/__mocks__/jest.setup.js +++ b/__mocks__/jest.setup.js @@ -5,6 +5,7 @@ // Learn more: https://github.com/testing-library/jest-dom require("whatwg-fetch"); require("@testing-library/jest-dom/jest-globals"); +import "jest-location-mock"; global.plausible = null; diff --git a/package-lock.json b/package-lock.json index e00449a9..3fd3b6e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,6 +59,7 @@ "identity-obj-proxy": "^3.0.0", "image-size": "^1.0.2", "jest-environment-jsdom": "^29.6.2", + "jest-location-mock": "^1.0.10", "jest-watch-typeahead": "^2.2.2", "json5": "^2.2.3", "junk": "^4.0.1", @@ -2460,6 +2461,12 @@ "node": ">=8" } }, + "node_modules/@jedmao/location": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@jedmao/location/-/location-3.0.0.tgz", + "integrity": "sha512-p7mzNlgJbCioUYLUEKds3cQG4CHONVFJNYqMe6ocEtENCL/jYmMo1Q3ApwsMmU+L0ZkaDJEyv4HokaByLoPwlQ==", + "dev": true + }, "node_modules/@jest/console": { "version": "29.6.1", "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.6.1.tgz", @@ -17108,6 +17115,122 @@ "dev": true, "peer": true }, + "node_modules/jest-location-mock": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/jest-location-mock/-/jest-location-mock-1.0.10.tgz", + "integrity": "sha512-g5u0rDOaj1I/lWuPOOP6xfpY+O958IcOanwPKnHdfWm0l4Y2sdVmwXMPY9fT5s8D9nX44Zl/Ypmk6B88mDoqZQ==", + "dev": true, + "dependencies": { + "@jedmao/location": "^3.0.0", + "jest-diff": "^27.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/jest-location-mock/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-location-mock/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-location-mock/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-location-mock/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-location-mock/node_modules/diff-sequences": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", + "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", + "dev": true, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-location-mock/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-location-mock/node_modules/jest-diff": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", + "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-location-mock/node_modules/jest-get-type": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", + "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", + "dev": true, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-location-mock/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jest-matcher-utils": { "version": "29.6.1", "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.6.1.tgz", diff --git a/package.json b/package.json index 635a1655..816f9c65 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "identity-obj-proxy": "^3.0.0", "image-size": "^1.0.2", "jest-environment-jsdom": "^29.6.2", + "jest-location-mock": "^1.0.10", "jest-watch-typeahead": "^2.2.2", "json5": "^2.2.3", "junk": "^4.0.1", diff --git a/src/utils/search.ts b/src/utils/search.ts new file mode 100644 index 00000000..487993c7 --- /dev/null +++ b/src/utils/search.ts @@ -0,0 +1,34 @@ +export const SEARCH_QUERY_KEY = "searchQuery"; +export const SEARCH_PAGE_KEY = "searchPage"; +export const CONTENT_TO_DISPLAY_KEY = "display"; +export const FILTER_TAGS_KEY = "filterTags"; +export const FILTER_AUTHOR_KEY = "filterAuthors"; +export const SORT_KEY = "sort"; + +interface SearchQuery { + searchQuery?: string; + searchPage?: number; + contentToDisplay?: "all" | "articles" | "collections"; + filterTags?: string[]; + filterAuthors?: string[]; + sort?: "newest" | "oldest"; +} + +export const buildSearchQuery = ({ + searchQuery = "*", + searchPage, + contentToDisplay, + filterTags, + filterAuthors, + sort, +}: SearchQuery) => { + const query = new URLSearchParams(); + if (searchQuery) query.set(SEARCH_QUERY_KEY, searchQuery); + if (searchPage) query.set(SEARCH_PAGE_KEY, searchPage.toString()); + if (contentToDisplay) query.set(CONTENT_TO_DISPLAY_KEY, contentToDisplay); + if (filterTags) query.set(FILTER_TAGS_KEY, filterTags.join(",")); + if (filterAuthors) query.set(FILTER_AUTHOR_KEY, filterAuthors.join(",")); + if (sort) query.set(SORT_KEY, sort); + // Returned without a question mark + return query.toString(); +}; diff --git a/src/views/search/search-page.spec.tsx b/src/views/search/search-page.spec.tsx index b4e18836..f63fb4f4 100644 --- a/src/views/search/search-page.spec.tsx +++ b/src/views/search/search-page.spec.tsx @@ -18,6 +18,7 @@ import { MockUnicorn, MockUnicornTwo, } from "../../../__mocks__/data/mock-unicorn"; +import { buildSearchQuery } from "utils/search"; const user = userEvent.setup(); @@ -599,7 +600,160 @@ describe("Search page", () => { }); // Search page, sort order, etc - test.todo("Make sure that initial search props are not thrown away"); + test("Make sure that initial search props are not thrown away", async () => { + mockFetch(() => ({ + posts: [ + { + ...MockPost, + tags: ["angular"], + authorsMeta: [MockUnicorn], + authors: [MockUnicorn.id], + slug: `blog-post-1`, + title: "One blog post", + published: "2019-01-01T00:00:00.000Z", + publishedMeta: "January 1, 2019", + }, + { + ...MockPost, + tags: ["angular"], + authorsMeta: [MockUnicorn], + authors: [MockUnicorn.id], + slug: `blog-post-2`, + title: "Two blog post", + published: "2019-01-01T00:00:00.000Z", + publishedMeta: "January 1, 2019", + }, + { + ...MockPost, + tags: ["angular"], + authorsMeta: [MockUnicorn], + authors: [MockUnicorn.id], + slug: `blog-post-3`, + title: "Three blog post", + published: "2019-01-01T00:00:00.000Z", + publishedMeta: "January 1, 2019", + }, + { + ...MockPost, + tags: ["angular"], + authorsMeta: [MockUnicorn], + authors: [MockUnicorn.id], + slug: `blog-post-4`, + title: "Four blog post", + published: "2019-01-01T00:00:00.000Z", + publishedMeta: "January 1, 2019", + }, + { + ...MockPost, + tags: ["angular"], + authorsMeta: [MockUnicorn], + authors: [MockUnicorn.id], + slug: `blog-post-5`, + title: "Five blog post", + published: "2019-01-01T00:00:00.000Z", + publishedMeta: "January 1, 2019", + }, + { + ...MockPost, + tags: ["angular"], + authorsMeta: [MockUnicorn], + authors: [MockUnicorn.id], + slug: `blog-post-6`, + title: "Six blog post", + published: "2019-01-01T00:00:00.000Z", + publishedMeta: "January 1, 2019", + }, + { + ...MockPost, + tags: ["angular"], + authorsMeta: [MockUnicorn], + authors: [MockUnicorn.id], + slug: `blog-post-7`, + title: "Seven blog post", + published: "2090-01-01T00:00:00.000Z", + publishedMeta: "January 1, 2090", + }, + { + ...MockPost, + tags: ["react"], + authorsMeta: [MockUnicorn], + authors: [MockUnicorn.id], + slug: `blog-post-8`, + title: "Eight blog post", + published: "2019-01-01T00:00:00.000Z", + publishedMeta: "January 1, 2019", + }, + { + ...MockPost, + tags: ["angular"], + authorsMeta: [MockUnicornTwo], + authors: [MockUnicornTwo.id], + slug: `blog-post-9`, + title: "Nine blog post", + published: "2019-01-01T00:00:00.000Z", + publishedMeta: "January 1, 2019", + }, + { + ...MockPost, + tags: ["angular"], + authorsMeta: [MockUnicorn], + authors: [MockUnicorn.id], + slug: `blog-post-10`, + title: "Ten blog post", + published: "2020-01-01T00:00:00.000Z", + publishedMeta: "January 1, 2020", + }, + ], + totalPosts: 7, + totalCollections: 0, + collections: [ + { + ...MockCollection, + title: "One collection", + }, + ], + })); + + const searchQuery = buildSearchQuery({ + searchQuery: "blog", + searchPage: 2, + contentToDisplay: "articles", + filterTags: ["angular"], + filterAuthors: [MockUnicorn.id], + sort: "oldest", + }); + + window.location.assign(`?${searchQuery}`); + + const { getByTestId, getByText, getByLabelText, queryByText } = render( + , + ); + + // Persists search query + const searchInput = getByLabelText("Search"); + expect(searchInput).toHaveValue("blog"); + + // Persists page + await waitFor(() => expect(getByText("Seven blog post")).toBeInTheDocument()); + expect(queryByText("One blog post")).not.toBeInTheDocument(); + + // Persists content type + expect(queryByText("One collection")).not.toBeInTheDocument(); + + // Persists tags + expect(queryByText("Eight blog post")).not.toBeInTheDocument(); + + // Persists authors + expect(queryByText("Nine blog post")).not.toBeInTheDocument(); + + // Persists sort order + await waitFor(() => { + const html = document.body.innerHTML; + const a = html.search("Ten blog post"); + const b = html.search("Seven blog post"); + expect(a).toBeLessThan(b); + }); + }); test("Make sure that complete re-renders preserve tags, authors, etc", async () => { global.innerWidth = 2000; diff --git a/src/views/search/search-page.tsx b/src/views/search/search-page.tsx index 0386d725..ab215c2e 100644 --- a/src/views/search/search-page.tsx +++ b/src/views/search/search-page.tsx @@ -31,13 +31,14 @@ import retry from "src/icons/refresh.svg?raw"; import sadUnicorn from "../../assets/unicorn_sad.svg"; import happyUnicorn from "../../assets/unicorn_happy.svg"; import scaredUnicorn from "../../assets/unicorn_scared.svg"; - -const SEARCH_QUERY_KEY = "searchQuery"; -const SEARCH_PAGE_KEY = "searchPage"; -const CONTENT_TO_DISPLAY_KEY = "display"; -const FILTER_TAGS_KEY = "filterTags"; -const FILTER_AUTHOR_KEY = "filterAuthors"; -const SORT_KEY = "sort"; +import { + SEARCH_QUERY_KEY, + SEARCH_PAGE_KEY, + CONTENT_TO_DISPLAY_KEY, + FILTER_TAGS_KEY, + FILTER_AUTHOR_KEY, + SORT_KEY, +} from "../../utils/search"; const DEFAULT_SORT = "newest"; const DEFAULT_CONTENT_TO_DISPLAY = "all";