mirror of
https://github.com/LukeHagar/unicorn-utterances.git
synced 2025-12-06 04:21:55 +00:00
chore: fix tests, add new SEO tests
This commit is contained in:
@@ -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",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
3
src/utils/queries/index.ts
Normal file
3
src/utils/queries/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from "./name";
|
||||||
|
export * from "./property";
|
||||||
|
export * from "./rel";
|
||||||
34
src/utils/queries/name.ts
Normal file
34
src/utils/queries/name.ts
Normal 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,
|
||||||
|
};
|
||||||
39
src/utils/queries/property.ts
Normal file
39
src/utils/queries/property.ts
Normal 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
36
src/utils/queries/rel.ts
Normal 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
17
src/utils/queries/types.d.ts
vendored
Normal 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
2
src/utils/tests.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// TODO: Turn this into a library
|
||||||
|
export * from "./queries";
|
||||||
Reference in New Issue
Block a user