chore: fix tests, add new SEO tests

This commit is contained in:
Corbin Crutchley
2022-05-14 09:32:52 -07:00
parent 92cc4ded7b
commit c0b4b7032d
12 changed files with 271 additions and 19 deletions

View File

@@ -15,6 +15,9 @@ export const MockPost: PostInfo & RenderedPostInfo = {
headingsWithId: [], headingsWithId: [],
wordCount: 10000, wordCount: 10000,
content: "", content: "",
translations: {
en: "English",
},
}; };
export const MockMultiAuthorPost: PostInfo & RenderedPostInfo = { export const MockMultiAuthorPost: PostInfo & RenderedPostInfo = {
@@ -31,4 +34,46 @@ export const MockMultiAuthorPost: PostInfo & RenderedPostInfo = {
headingsWithId: [], headingsWithId: [],
wordCount: 100000, wordCount: 100000,
content: "", content: "",
translations: {
en: "English",
},
};
export const MockMuliLanguagePost: PostInfo & RenderedPostInfo = {
excerpt:
"This would be a second auto generated excerpt of the post in particular",
title: "Another post title",
published: "10-20-2010",
tags: ["item1"],
description:
"This is another short description dunno why this would be this short",
authors: [MockUnicornTwo, MockUnicorn],
license: MockLicense,
slug: "this-other-post-name-here",
headingsWithId: [],
wordCount: 100000,
content: "",
translations: {
es: "Español",
},
};
export const MockCanonicalPost: PostInfo & RenderedPostInfo = {
excerpt:
"This would be a second auto generated excerpt of the post in particular",
title: "Another post title",
published: "10-20-2010",
originalLink: "https://google.com/",
tags: ["item1"],
description:
"This is another short description dunno why this would be this short",
authors: [MockUnicornTwo, MockUnicorn],
license: MockLicense,
slug: "this-other-post-name-here",
headingsWithId: [],
wordCount: 100000,
content: "",
translations: {
en: "English",
},
}; };

View File

@@ -206,7 +206,7 @@ export const SEO: React.FC<PropsWithChildren<SEOProps>> = (props) => {
rel="alternate" rel="alternate"
href={ href={
siteMetadata.siteUrl + siteMetadata.siteUrl +
`${lang === "en" ? "" : "/"}${lang}` + `${lang === "en" ? "" : "/"}${lang === "en" ? "" : lang}` +
removePrefixLanguageFromPath(pathName || "") removePrefixLanguageFromPath(pathName || "")
} }
hrefLang={lang} hrefLang={lang}

View File

@@ -1,10 +1,15 @@
import React from "react"; import React, { PropsWithChildren } from "react";
import { fireEvent, render } from "@testing-library/react"; import { fireEvent, render } from "@testing-library/react";
import { siteMetadata } from "__mocks__/data/mock-site-metadata"; import {
import { MockMultiAuthorPost, MockPost } from "__mocks__/data/mock-post"; MockCanonicalPost,
MockMuliLanguagePost,
MockMultiAuthorPost,
MockPost,
} from "__mocks__/data/mock-post";
import BlogPostTemplate from "../../pages/[...postInfo]"; import BlogPostTemplate from "../../pages/[...postInfo]";
import ReactDOMServer from "react-dom/server";
import { RouterContext } from "next/dist/shared/lib/router-context"; import { RouterContext } from "next/dist/shared/lib/router-context";
import { Languages } from "types/index";
import { getAllByRel, getByProperty, getByRel } from "utils/tests";
// import { axe } from "jest-axe"; // import { axe } from "jest-axe";
const getElement = ({ const getElement = ({
@@ -14,6 +19,7 @@ const getElement = ({
slug = "/slug", slug = "/slug",
postsDirectory = "/posts", postsDirectory = "/posts",
seriesPosts = [], seriesPosts = [],
lang = "en",
}: { }: {
post: any; post: any;
fn?: () => void; fn?: () => void;
@@ -21,6 +27,7 @@ const getElement = ({
slug?: string; slug?: string;
postsDirectory?: string; postsDirectory?: string;
seriesPosts?: any[]; seriesPosts?: any[];
lang?: Languages;
}) => ( }) => (
<RouterContext.Provider <RouterContext.Provider
value={ value={
@@ -39,6 +46,7 @@ const getElement = ({
post={post} post={post}
markdownHTML={markdownHTML} markdownHTML={markdownHTML}
slug={slug} slug={slug}
lang={lang}
postsDirectory={postsDirectory} postsDirectory={postsDirectory}
seriesPosts={seriesPosts} seriesPosts={seriesPosts}
suggestedPosts={[]} suggestedPosts={[]}
@@ -74,7 +82,7 @@ test("Blog post page renders", async () => {
expect(navigatePushFn).toHaveBeenCalledTimes(2); expect(navigatePushFn).toHaveBeenCalledTimes(2);
// Renders the post body properly // Renders the post body properly
expect((await findByTestId("post-body-div")).innerHTML).toBe( expect((await findByTestId("post-body-div")).innerHTML).toContain(
"<div>Hey there</div>" "<div>Hey there</div>"
); );
}); });
@@ -112,12 +120,71 @@ test("Blog post page handles two authors", async () => {
expect(navigatePushFn).toHaveBeenCalledTimes(4); expect(navigatePushFn).toHaveBeenCalledTimes(4);
// Renders the post body properly // Renders the post body properly
expect((await findByTestId("post-body-div")).innerHTML).toBe( expect((await findByTestId("post-body-div")).innerHTML).toContain(
"<div>Hello, friends</div>" "<div>Hello, friends</div>"
); );
}); });
test.todo("SEO should apply"); /**
* Next head mocking to `head` element
*
* TODO: Turn this + queries into a library
*/
const mockDocument = document;
jest.mock("next/head", () => {
const ReactDOM = require("react-dom");
return ({ children }: PropsWithChildren<unknown>) => {
return ReactDOM.createPortal(children, mockDocument.head);
};
});
test("SEO should show translation data", () => {
const navigatePushFn = jest.fn();
render(
getElement({
post: MockMuliLanguagePost,
fn: navigatePushFn,
markdownHTML: "Hello, friends",
lang: "en",
})
);
const alts = getAllByRel(document.head, "alternate");
expect(alts.length).toBe(2);
expect(alts[0]).toHaveProperty("hreflang", "x-default");
expect(alts[1]).toHaveProperty("hreflang", "es");
expect(getByProperty(document.head, "og:locale")).toHaveProperty(
"content",
"en"
);
expect(getByProperty(document.head, "og:locale:alternate")).toHaveProperty(
"content",
"es"
);
});
test("Canonical tags should show in SEO", () => {
const navigatePushFn = jest.fn();
render(
getElement({
post: MockCanonicalPost,
fn: navigatePushFn,
markdownHTML: "Hello, friends",
lang: "en",
})
);
expect(getByRel(document.head, "canonical")).toHaveProperty(
"href",
"https://google.com/"
);
});
test.todo("SEO Twitter");
test.todo("Shows post footer image"); test.todo("Shows post footer image");
// test.skip("Blog post page should not have axe errors", async () => { // test.skip("Blog post page should not have axe errors", async () => {

View File

@@ -3,6 +3,7 @@ import { RenderedPostInfo } from "types/PostInfo";
import { SlugPostInfo } from "constants/queries"; import { SlugPostInfo } from "constants/queries";
import Link from "next/link"; import Link from "next/link";
import { Languages } from "types/index"; import { Languages } from "types/index";
import { Fragment } from "react";
interface TranslationsHeaderProps { interface TranslationsHeaderProps {
post: SlugPostInfo & RenderedPostInfo; post: SlugPostInfo & RenderedPostInfo;
@@ -14,12 +15,12 @@ export const TranslationsHeader = ({ post }: TranslationsHeaderProps) => {
{(Object.keys(post.translations) as Languages[]).map((lang, i, arr) => { {(Object.keys(post.translations) as Languages[]).map((lang, i, arr) => {
const langHref = lang === "en" ? "" : `${lang}/`; const langHref = lang === "en" ? "" : `${lang}/`;
return ( return (
<> <Fragment key={lang}>
<Link key={lang} passHref href={`/${langHref}posts/${post.slug}`}> <Link passHref href={`/${langHref}posts/${post.slug}`}>
<a>{post.translations[lang]}</a> <a>{post.translations[lang]}</a>
</Link> </Link>
{i !== arr.length - 1 && <span>, </span>} {i !== arr.length - 1 && <span>, </span>}
</> </Fragment>
); );
})} })}
</div> </div>

View File

@@ -24,7 +24,7 @@ import { MailingList } from "components/mailing-list";
import GitHubIcon from "assets/icons/github.svg"; import GitHubIcon from "assets/icons/github.svg";
import CommentsIcon from "assets/icons/message.svg"; import CommentsIcon from "assets/icons/message.svg";
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useMemo, useState } from "react";
import { ThemeContext } from "constants/theme-context"; import { ThemeContext } from "constants/theme-context";
import { siteMetadata } from "constants/site-config"; import { siteMetadata } from "constants/site-config";
import "react-medium-image-zoom/dist/styles.css"; import "react-medium-image-zoom/dist/styles.css";
@@ -101,6 +101,19 @@ const Post = ({
const GHLink = `https://github.com/${siteMetadata.repoPath}/tree/master${siteMetadata.relativeToPosts}/${slug}/index.md`; const GHLink = `https://github.com/${siteMetadata.repoPath}/tree/master${siteMetadata.relativeToPosts}/${slug}/index.md`;
const langData = useMemo(() => {
const otherLangs = post.translations
? (Object.keys(post.translations).filter(
(t) => t !== lang
) as Languages[])
: [];
return {
otherLangs,
currentLang: lang,
};
}, [lang, post.translations]);
return ( return (
<> <>
<SEO <SEO
@@ -113,12 +126,7 @@ const Post = ({
type="article" type="article"
pathName={router.asPath} pathName={router.asPath}
canonical={post.originalLink} canonical={post.originalLink}
langData={{ langData={langData}
currentLang: lang,
otherLangs: post.translations
? (Object.keys(post.translations) as Languages[])
: [],
}}
/> />
<article> <article>
<BlogPostLayout <BlogPostLayout

View File

@@ -18,7 +18,7 @@ export interface PostInfo {
originalLink?: string; originalLink?: string;
content: string; content: string;
tags: string[]; tags: string[];
translations: Record<Languages, string>; translations: Partial<Record<Languages, string>>;
} }
export interface RenderedPostInfo { export interface RenderedPostInfo {

View File

@@ -0,0 +1,3 @@
export * from "./name";
export * from "./property";
export * from "./rel";

34
src/utils/queries/name.ts Normal file
View File

@@ -0,0 +1,34 @@
import { wrapAllByQueryWithSuggestion } from "@testing-library/dom/dist/query-helpers";
import { checkContainerType } from "@testing-library/dom/dist/helpers";
import {
AllByBoundAttribute,
GetErrorFunction,
queryAllByAttribute,
buildQueries,
} from "@testing-library/dom";
const queryAllByName: AllByBoundAttribute = (...args) => {
checkContainerType(args[0]);
return queryAllByAttribute("name", ...args);
};
const getMultipleError: GetErrorFunction<[unknown]> = (c, text) =>
`Found multiple elements with the name text of: ${text}`;
const getMissingError: GetErrorFunction<[unknown]> = (c, text) =>
`Unable to find an element with the name text of: ${text}`;
const queryAllByNameWithSuggestions = wrapAllByQueryWithSuggestion<
// @ts-expect-error -- See `wrapAllByQueryWithSuggestion` Argument constraint comment
[name: Matcher, options?: MatcherOptions]
>(queryAllByName, queryAllByName.name, "queryAll");
const [queryByName, getAllByName, getByName, findAllByName, findByName] =
buildQueries(queryAllByName, getMultipleError, getMissingError);
export {
queryByName,
queryAllByNameWithSuggestions as queryAllByName,
getByName,
getAllByName,
findAllByName,
findByName,
};

View File

@@ -0,0 +1,39 @@
import { wrapAllByQueryWithSuggestion } from "@testing-library/dom/dist/query-helpers";
import { checkContainerType } from "@testing-library/dom/dist/helpers";
import {
AllByBoundAttribute,
GetErrorFunction,
queryAllByAttribute,
buildQueries,
} from "@testing-library/dom";
const queryAllByProperty: AllByBoundAttribute = (...args) => {
checkContainerType(args[0]);
return queryAllByAttribute("property", ...args);
};
const getMultipleError: GetErrorFunction<[unknown]> = (c, text) =>
`Found multiple elements with the property text of: ${text}`;
const getMissingError: GetErrorFunction<[unknown]> = (c, text) =>
`Unable to find an element with the property text of: ${text}`;
const queryAllByPropertyWithSuggestions = wrapAllByQueryWithSuggestion<
// @ts-expect-error -- See `wrapAllByQueryWithSuggestion` Argument constraint comment
[property: Matcher, options?: MatcherOptions]
>(queryAllByProperty, queryAllByProperty.name, "queryAll");
const [
queryByProperty,
getAllByProperty,
getByProperty,
findAllByProperty,
findByProperty,
] = buildQueries(queryAllByProperty, getMultipleError, getMissingError);
export {
queryByProperty,
queryAllByPropertyWithSuggestions as queryAllByProperty,
getByProperty,
getAllByProperty,
findAllByProperty,
findByProperty,
};

36
src/utils/queries/rel.ts Normal file
View File

@@ -0,0 +1,36 @@
import { wrapAllByQueryWithSuggestion } from "@testing-library/dom/dist/query-helpers";
import { checkContainerType } from "@testing-library/dom/dist/helpers";
import {
AllByBoundAttribute,
GetErrorFunction,
queryAllByAttribute,
buildQueries,
Matcher,
MatcherOptions,
} from "@testing-library/dom";
const queryAllByRel: AllByBoundAttribute = (...args) => {
checkContainerType(args[0]);
return queryAllByAttribute("rel", ...args);
};
const getMultipleError: GetErrorFunction<[unknown]> = (c, text) =>
`Found multiple elements with the rel text of: ${text}`;
const getMissingError: GetErrorFunction<[unknown]> = (c, text) =>
`Unable to find an element with the rel text of: ${text}`;
const queryAllByRelWithSuggestions = wrapAllByQueryWithSuggestion<
// @ts-expect-error -- See `wrapAllByQueryWithSuggestion` Argument constraint comment
[rel: Matcher, options?: MatcherOptions]
>(queryAllByRel, queryAllByRel.name, "queryAll");
const [queryByRel, getAllByRel, getByRel, findAllByRel, findByRel] =
buildQueries(queryAllByRel, getMultipleError, getMissingError);
export {
queryByRel,
queryAllByRelWithSuggestions as queryAllByRel,
getByRel,
getAllByRel,
findAllByRel,
findByRel,
};

17
src/utils/queries/types.d.ts vendored Normal file
View File

@@ -0,0 +1,17 @@
declare module "@testing-library/dom/dist/query-helpers" {
import { Variant, WithSuggest } from "@testing-library/dom";
declare const wrapAllByQueryWithSuggestion: <
Arguments extends [...unknown[], WithSuggest]
>(
query: (container: HTMLElement, ...args: Arguments) => HTMLElement[],
queryAllByName: string,
variant: Variant
) => (container: HTMLElement, ...args: Arguments) => HTMLElement[];
export { wrapAllByQueryWithSuggestion };
}
declare module "@testing-library/dom/dist/helpers" {
declare const checkContainerType: (...props: any[]) => any;
export { checkContainerType };
}

2
src/utils/tests.ts Normal file
View File

@@ -0,0 +1,2 @@
// TODO: Turn this into a library
export * from "./queries";