Skip to content

Commit cbe17de

Browse files
authored
✅ add deep testing to dev.to (#14)
1 parent 9ed8eee commit cbe17de

File tree

19 files changed

+665
-74
lines changed

19 files changed

+665
-74
lines changed

.storybook/preview.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { setCustomElementsManifest } from '@storybook/web-components';
22
import customElements from '../custom-elements.json';
33
import { globalTypesPrimer, decoratorsPrimer } from './primer-preview';
44
import { viewports } from './viewports';
5+
import { stringify, parseify } from '../src/utils';
56
import "./storybook.css";
67

78
setCustomElementsManifest(customElements);
@@ -14,7 +15,8 @@ global.attrGen = (args) => Object.entries(args)
1415
.map(([key, value]) => `\n ${key}="${value}"`)
1516
.join(' ');
1617

17-
global.stringify = (obj) => JSON.stringify(obj).replace(/"/g, """)
18+
global.stringify = stringify;
19+
global.parseify = parseify;
1820

1921
/** @type { import('@storybook/web-components').Preview } */
2022
const preview = {

src/devto/helpers/testing.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export const generateMockResponse = (content, type='article', status=200) => {
2020
if (type === 'article') {
2121
// calls /articleS/:id (adds 's' to type)
2222
url += `${type}s/${content.id}`;
23+
} else if (type === 'articles') {
24+
url += `${type}/latest?per_page=1000&username=fake`;
2325
} else if (type === 'users') {
2426
if (!content.id) {
2527
url += `${type}/by_username?url=${content.username}`;
@@ -30,9 +32,21 @@ export const generateMockResponse = (content, type='article', status=200) => {
3032

3133
if (status === 404) {
3234
return {
35+
url,
36+
method: 'GET',
3337
status: 404,
34-
error: "Not Found"
38+
delay: 0,
39+
response: {
40+
status: 404,
41+
error: "Not Found"
42+
},
3543
}
3644
}
37-
return content
45+
return {
46+
url,
47+
method: 'GET',
48+
status: 200,
49+
delay: 0,
50+
response: content,
51+
}
3852
}

src/devto/post/content.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,10 @@ import { getApiUrl } from '../helpers/index.js';
2828
* @ignore
2929
*/
3030
export const fetchPost = async (id) => {
31-
const response = await fetch(`${getApiUrl()}/articles/${id}`, {
31+
const options = {
3232
cache: 'no-cache',
33-
});
33+
};
34+
const response = await fetch(`${getApiUrl()}/articles/${id}`, options);
3435
const repoJson = await response.json();
3536
return repoJson;
3637
}
@@ -42,9 +43,10 @@ export const fetchPost = async (id) => {
4243
* @ignore
4344
*/
4445
export const fetchUserPosts = async (username) => {
45-
const articles = await fetch(`${getApiUrl()}/articles/latest?per_page=1000&username=${username?.toLowerCase()}`, {
46+
const options = {
4647
cache: 'no-cache',
47-
});
48+
};
49+
const articles = await fetch(`${getApiUrl()}/articles/latest?per_page=1000&username=${username?.toLowerCase()}`, options);
4850
const articlesJson = await articles.json();
4951
return articlesJson;
5052
}

src/devto/post/html.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@
77
* @memberof DEVUtils.post
88
*/
99
function html(content) {
10-
if (content.error || !content.url || !content.title) {
11-
return '';
10+
if (content.error) {
11+
return `
12+
<div aria-label="dev.to article" class="post" itemscope itemtype="http://schema.org/Action">
13+
<p itemprop="error">${content.error}</p>
14+
</div>
15+
`
1216
}
1317

1418
return `
15-
<span class="post" itemscope itemtype="http://schema.org/Article">
19+
<span aria-label="dev.to article" class="post" itemscope itemtype="http://schema.org/Article">
1620
<a href="${content.url}" itemprop="url" aria-label="read post ${content.title}">
1721
<img src="${content.cover_image}" itemprop="image" alt="Cover image for post ${content.title}" />
1822
<span itemprop="name">${content.title}</span>

src/devto/post/post.shared-spec.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { expect } from '@storybook/jest';
2+
import { within as shadowWithin } from 'shadow-dom-testing-library';
3+
4+
/**
5+
* Extract elements from an shadow DOM element
6+
*/
7+
export const getElements = async (canvasElement) => {
8+
const screen = shadowWithin(canvasElement);
9+
const container = await screen.findByShadowLabelText(/dev.to article/i);
10+
const link = await screen.queryByShadowRole('link');
11+
const image = await screen.queryByShadowRole('img');
12+
const title = await container?.querySelector('[itemprop="name"]');
13+
return {
14+
screen,
15+
canvasElement,
16+
container,
17+
link,
18+
image,
19+
title,
20+
error: await container?.querySelector('[itemprop="error"]'),
21+
};
22+
}
23+
24+
/**
25+
* Ensure elements are present and have the correct content
26+
*/
27+
export const ensureElements = async (elements, args) => {
28+
await expect(elements.container).toBeTruthy();
29+
30+
if (args.fetch && !args.id) {
31+
await expect(elements.link).toBeFalsy();
32+
await expect(elements.container).toHaveTextContent('Post ID is required to fetch post content');
33+
}
34+
if (args.error) {
35+
await expect(elements.link).toBeFalsy();
36+
await expect(elements.error).toBeTruthy();
37+
await expect(elements.error).toHaveTextContent(args.error);
38+
return;
39+
}
40+
41+
await expect(elements.link).toBeTruthy();
42+
await expect(elements.link).toHaveAttribute('href', args.url);
43+
await expect(elements.title).toBeTruthy();
44+
await expect(elements.title).toHaveTextContent(args.title);
45+
await expect(elements.image).toBeTruthy();
46+
await expect(elements.image).toHaveAttribute('src', args.cover_image);
47+
}

src/devto/post/post.stories.js

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { parseFetchedPost } from './content';
44
import { default as postDependabot } from '../fixtures/generated/post--dependabot.json';
55
import { default as postProfileComponents } from '../fixtures/generated/post--profile-components.json';
66

7+
import { getElements, ensureElements } from './post.shared-spec';
8+
79
import './index.js';
810

911
export default {
@@ -24,17 +26,30 @@ export const Post = {
2426
args: {
2527
...parseFetchedPost(postDependabot),
2628
},
27-
// play: async ({ args, canvasElement, step }) => {
28-
// const elements = await getElements(canvasElement);
29-
// await ensureElements(elements, args);
30-
// }
29+
play: async ({ args, canvasElement, step }) => {
30+
const elements = await getElements(canvasElement);
31+
await ensureElements(elements, args);
32+
}
3133
}
3234

3335
export const Fetch = {
3436
args: {
3537
id: postProfileComponents.id,
3638
fetch: true,
3739
},
40+
parameters: {
41+
mockData: [
42+
generateMockResponse(postProfileComponents, 'article'),
43+
]
44+
},
45+
play: async ({ args, canvasElement, step }) => {
46+
const elements = await getElements(canvasElement);
47+
const argsAfterFetch = {
48+
...parseFetchedPost(postProfileComponents),
49+
...args,
50+
};
51+
await ensureElements(elements, argsAfterFetch);
52+
}
3853
}
3954

4055
export const FetchOverides = {
@@ -43,4 +58,37 @@ export const FetchOverides = {
4358
title: 'Mess? Make your human blame the dog',
4459
cover_image: 'cat-glasses-1000-420.jpeg'
4560
},
46-
}
61+
parameters: {
62+
mockData: [
63+
generateMockResponse(postProfileComponents, 'article'),
64+
]
65+
},
66+
play: async ({ args, canvasElement, step }) => {
67+
const elements = await getElements(canvasElement);
68+
const argsAfterFetch = {
69+
...parseFetchedPost(postProfileComponents),
70+
...args,
71+
};
72+
await ensureElements(elements, argsAfterFetch);
73+
}
74+
}
75+
76+
export const FetchError = {
77+
args: {
78+
id: 'not-a-real-id',
79+
fetch: true,
80+
},
81+
parameters: {
82+
mockData: [
83+
generateMockResponse({id: 'not-a-real-id'}, 'article', 404),
84+
]
85+
},
86+
play: async ({ args, canvasElement, step }) => {
87+
const elements = await getElements(canvasElement);
88+
const argsAfterFetch = {
89+
...args,
90+
error: `Fetch Error: Post "${args.id}" not found`,
91+
};
92+
await ensureElements(elements, argsAfterFetch);
93+
}
94+
}

src/devto/post/post.test.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ describe('fetchPost', () => {
1212
it('Should accept a post id and return a response', async (t) => {
1313
const fn = t.mock.method(global, 'fetch');
1414
const mockRes = {
15-
json: () => generateMockResponse(postDependabot, 'article'),
15+
json: () => generateMockResponse(postDependabot, 'article').response,
1616
};
1717
fn.mock.mockImplementationOnce(() =>
1818
Promise.resolve(mockRes)
@@ -27,7 +27,7 @@ describe('fetchUserPosts', () => {
2727
it('Should accept a username and return a response', async (t) => {
2828
const fn = t.mock.method(global, 'fetch');
2929
const mockRes = {
30-
json: () => generateMockResponse([postDependabot, postBugfix], 'articles'),
30+
json: () => generateMockResponse([postDependabot, postBugfix], 'articles').response,
3131
};
3232
fn.mock.mockImplementationOnce(() =>
3333
Promise.resolve(mockRes)
@@ -121,7 +121,7 @@ describe('generatePostContent', () => {
121121
const fn = t.mock.method(global, 'fetch');
122122
const mockContent = generateMockResponse(testObj, 'article', 404);
123123
const mockRes = {
124-
json: () => mockContent,
124+
json: () => mockContent.response,
125125
};
126126
fn.mock.mockImplementationOnce(() =>
127127
Promise.resolve(mockRes)
@@ -140,7 +140,7 @@ describe('generatePostContent', () => {
140140
}
141141
const fn = t.mock.method(global, 'fetch');
142142
const mockRes = {
143-
json: () => testPost,
143+
json: () => generateMockResponse(testPost, 'article').response,
144144
};
145145
fn.mock.mockImplementationOnce(() =>
146146
Promise.resolve(mockRes)

src/devto/typedefs.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
2+
3+
/**
4+
* @see https://developers.forem.com/api/v0#tag/users/operation/getUser
5+
* @typedef {Object} DevToUser
6+
* @property {string} type_of - The type of the object, in this case "user".
7+
* @property {number} id - The unique identifier of the user.
8+
* @property {string} username - The username of the user.
9+
* @property {string} name - The name of the user.
10+
* @property {string} summary - A brief summary or bio of the user.
11+
* @property {string} twitter_username - The user's Twitter username.
12+
* @property {string} github_username - The user's GitHub username.
13+
* @property {?string} website_url - The user's personal website URL or null if not provided.
14+
* @property {string} location - The user's location.
15+
* @property {string} joined_at - The date the user joined, formatted as a string.
16+
* @property {string} profile_image - The URL to the user's profile image.
17+
*/
18+
19+
/**
20+
* @see https://developers.forem.com/api/v1#tag/articles/operation/getArticles
21+
* @typedef {Object} DevToArticle
22+
* @property {string} type_of - The type of the object, in this case "article".
23+
* @property {number} id - The unique identifier of the article.
24+
* @property {string} title - The title of the article.
25+
* @property {string} description - A brief description of the article.
26+
* @property {string} readable_publish_date - The human-readable publish date.
27+
* @property {string} slug - The article's slug.
28+
* @property {string} path - The relative path to the article.
29+
* @property {string} url - The full URL to the article.
30+
* @property {number} comments_count - The number of comments on the article.
31+
* @property {number} public_reactions_count - The number of public reactions to the article.
32+
* @property {?number} collection_id - The collection ID if the article belongs to a collection.
33+
* @property {string} published_timestamp - The timestamp when the article was published.
34+
* @property {number} positive_reactions_count - The number of positive reactions to the article.
35+
* @property {string} cover_image - The URL to the article's cover image.
36+
* @property {string} social_image - The URL to the article's social image.
37+
* @property {string} canonical_url - The canonical URL of the article.
38+
* @property {string} created_at - The timestamp when the article was created.
39+
* @property {?string} edited_at - The timestamp when the article was last edited.
40+
* @property {?string} crossposted_at - The timestamp when the article was crossposted.
41+
* @property {string} published_at - The timestamp when the article was published.
42+
* @property {string} last_comment_at - The timestamp of the last comment on the article.
43+
* @property {number} reading_time_minutes - The estimated reading time in minutes.
44+
* @property {Array<string>} tag_list - List of tags associated with the article.
45+
* @property {string} tags - Comma-separated string of tags.
46+
* @property {DevToUser} user - The user who wrote the article.
47+
* @property {DevToOrganization} organization - The organization associated with the article.
48+
* @property {DevToFlareTag} flare_tag - The flare tag associated with the article.
49+
*/
50+
51+
/**
52+
* @typedef {Object} DevToOrganization
53+
* @property {string} name - The name of the organization.
54+
* @property {string} username - The username of the organization.
55+
* @property {string} slug - The slug of the organization.
56+
* @property {string} profile_image - The URL to the organization's profile image.
57+
* @property {string} profile_image_90 - The URL to the 90x90 version of the organization's profile image.
58+
*/
59+
60+
/**
61+
* @typedef {Object} DevToFlareTag
62+
* @property {string} name - The name of the flare tag.
63+
* @property {string} bg_color_hex - The background color of the flare tag in HEX format.
64+
* @property {string} text_color_hex - The text color of the flare tag in HEX format.
65+
*/
66+
67+

0 commit comments

Comments
 (0)