Skip to content

Commit eaf582a

Browse files
idembele70idembele70
andauthored
[#9] Add e2e tests for article read (#15)
* test(article-read): add e2e tests for article read Refs: #9 * changes as per review Refs: #9 --------- Co-authored-by: idembele70 <idembele70@gmail>
1 parent c0dd73e commit eaf582a

22 files changed

Lines changed: 624 additions & 199 deletions
-16.5 KB
Binary file not shown.

e2e/shared/layouts/header.component.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Locator, Page } from "@playwright/test";
1+
import { expect, Locator, Page } from "@playwright/test";
22

33
export class HeaderComponent {
44
private readonly container: Locator;
@@ -14,16 +14,24 @@ export class HeaderComponent {
1414
this.container = this.page.getByRole('navigation');
1515

1616
this.brandLink = this.container.getByRole('link', { name: 'conduit', exact: true });
17-
this.homeLink = this.container.getByRole('link', { name: 'Home', exact: true});
18-
this.newArticleLink = this.container.getByRole('link', { name: /New Article$/});
19-
this.settingsLink = this.container.getByRole('link', { name: /Settings$/});
20-
this.profileLink = this.container.getByRole('link').filter({ has: this.page.getByRole('img')});
17+
this.homeLink = this.container.getByRole('link', { name: 'Home', exact: true });
18+
this.newArticleLink = this.container.getByRole('link', { name: /New Article$/ });
19+
this.settingsLink = this.container.getByRole('link', { name: /Settings$/ });
20+
this.profileLink = this.container.getByRole('link').filter({ has: this.page.getByRole('img') });
2121

22-
this.signInLink = this.container.getByRole('link', { name: 'Sign in', exact: true});
23-
this.signUpLink = this.container.getByRole('link', { name: 'Sign up', exact: true});
22+
this.signInLink = this.container.getByRole('link', { name: 'Sign in', exact: true });
23+
this.signUpLink = this.container.getByRole('link', { name: 'Sign up', exact: true });
2424
}
2525

2626
async goToProfile(): Promise<void> {
2727
await this.profileLink.click();
2828
}
29+
30+
async expectAuthenticatedHeaderVisible(): Promise<void> {
31+
await expect(this.brandLink).toBeVisible();
32+
await expect(this.homeLink).toBeVisible();
33+
await expect(this.newArticleLink).toBeVisible();
34+
await expect(this.settingsLink).toBeVisible();
35+
await expect(this.profileLink).toBeVisible();
36+
}
2937
}

e2e/shared/pages/home.page.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { ArticlePreviewComponent } from "@article/components/article-preview.component";
2+
import { expect, Locator, Page } from "@playwright/test";
3+
import { HeaderComponent } from "@shared/layouts/header.component";
4+
import { FRONT_URLS, FRONT_URLS_REG_EXP } from "@shared/utilities/url-front.utility";
5+
6+
export class HomePage {
7+
header: HeaderComponent;
8+
readonly mainContent: Locator;
9+
readonly articlePreview: ArticlePreviewComponent;
10+
constructor(private readonly page: Page) {
11+
this.header = new HeaderComponent(this.page);
12+
this.mainContent = this.page.locator('app-home-page')
13+
this.articlePreview = new ArticlePreviewComponent(this.page);
14+
}
15+
16+
async goToViaUrl(): Promise<void> {
17+
await this.page.goto(FRONT_URLS.HOME);
18+
}
19+
20+
getPaginationButton(pageNumber: number): Locator {
21+
return this.page.locator('button.page-link', { hasText: String(pageNumber) });
22+
}
23+
24+
async selectTag(tag: string): Promise<void> {
25+
await this.page.locator('a.tag-pill', { hasText: tag }).click();
26+
}
27+
28+
async expectTagFilterTitle(tag: string): Promise<void> {
29+
await expect(this.page.locator('.nav-item', { hasText: tag })).toBeVisible();
30+
}
31+
32+
async expectOnHomePage(): Promise<void> {
33+
await expect(this.page).toHaveURL(FRONT_URLS_REG_EXP.HOME);
34+
await expect(this.mainContent).toBeVisible();
35+
}
36+
37+
async expectOnAuthenticatedHomePage(): Promise<void> {
38+
await this.expectOnHomePage();
39+
await this.header.expectAuthenticatedHeaderVisible();
40+
}
41+
42+
async expectEmptyState(): Promise<void> {
43+
await expect(this.page.locator('.article-preview')).toHaveText('No articles are here... yet.');
44+
await expect(this.page.getByText('No tags are here... yet.')).toBeVisible();
45+
}
46+
47+
async expectLoadingIndicatorVisible(): Promise<void> {
48+
await expect(this.page.getByText('Loading articles...')).toBeVisible();
49+
}
50+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export class DateUtilities {
2+
static formatDateToLongEnglish(timestamp: string): string {
3+
return new Intl.DateTimeFormat('en-US', {
4+
month: 'long',
5+
day: 'numeric',
6+
year: 'numeric'
7+
}).format(new Date(timestamp));
8+
}
9+
}

e2e/shared/utilities/url-api.utility.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@ export const API_URLS_REGEX = {
1111
export const API_URLS = {
1212
ARTICLES: {
1313
CREATION: 'articles/',
14-
DELETION: (slug: string) => `articles/${slug}`
14+
DELETION: (slug: string) => `articles/${slug}`,
15+
GET_LIST: ({ limit = 10, offset = 0 } = {}) => `articles?limit=${limit}&offset=${offset}`
1516
},
17+
TAG: {
18+
GET_LIST: 'tags'
19+
}
1620
};
1721

1822
export const mockApiUrl = (pathname: string): string => `**${pathname}`;

e2e/shared/utilities/url-front.utility.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,6 @@ export const FRONT_URLS = {
1515
LOGIN:'login',
1616
REGISTER:'register',
1717
ARTICLE_EDITOR: 'editor',
18+
ARTICLE_DETAILS: 'article',
19+
PROFILE: 'profile'
1820
} as const;

e2e/tests/article/article-form.component.ts

Lines changed: 0 additions & 52 deletions
This file was deleted.

e2e/tests/article/article.factory.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,37 @@
1-
import { Article } from "./models/article.model";
1+
import { Article, CreateArticleRequest } from "./models/article.model";
22

33
export class ArticleFactory {
4-
static buildArticle(id: string, overrides?: Partial<Article>): Article {
4+
static buildArticlePayload(id: string, overrides?: CreateArticleRequest): CreateArticleRequest {
55
return {
6-
title: `article-${id}`,
6+
title: `title-${id}`,
77
description: `description-${id}`,
8-
content: `content-${id}`,
9-
tags: [`tag-1-${id}`, `tag-2-${id}`],
8+
body: `body-${id}`,
9+
tagList: [`tag-1-${id}`, `tag-2-${id}`],
1010
...overrides,
1111
};
1212
}
1313

14+
static buildArticle(id: string | number, overrides?: Article): Article {
15+
return {
16+
slug: `title-${id}`,
17+
title: `title ${id}`,
18+
description: `Description ${id}`,
19+
body: 'body',
20+
tagList: [`tag-${id}`],
21+
createdAt: String(Date.now()),
22+
updatedAt: String(Date.now()),
23+
favorited: false,
24+
favoritesCount: 0,
25+
author: {
26+
username: `user-${id}`,
27+
bio: null,
28+
image: '',
29+
following: false,
30+
},
31+
...overrides,
32+
}
33+
}
34+
1435
static buildArticleTagList(parallelIndex: number, length: number): string[] {
1536
return Array.from({ length }, (_, idx) => `tag-worker-${parallelIndex}-${idx}`);
1637
}

e2e/tests/article/article.fixture.ts

Lines changed: 0 additions & 41 deletions
This file was deleted.

e2e/tests/article/article.utility.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,33 @@
11
import { AuthUtility } from "@auth/auth.utility";
2-
import { Page } from "@playwright/test";
2+
import { Page, TestInfo } from "@playwright/test";
33
import { API_URLS, mockApiUrl } from "@shared/utilities/url-api.utility";
4+
import { Article, CreateArticleRequest } from "./models/article.model";
5+
import { CompositeIdFactory } from "@shared/factories/composite-id.factory";
6+
import { ArticleFactory } from "./article.factory";
7+
import { FRONT_URLS } from "@shared/utilities/url-front.utility";
48

59
export class ArticleUtility {
10+
static async createArticleViaApi(token: string, articlePayload: CreateArticleRequest): Promise<Article> {
11+
const apiContext = await AuthUtility.createAuthenticatedApiContext(token);
12+
13+
try {
14+
const response = await apiContext.post(API_URLS.ARTICLES.CREATION, {
15+
data: { article: articlePayload },
16+
});
17+
18+
if (!response.ok()) {
19+
const body = await response.text();
20+
throw new Error(
21+
`Create article failed: ${response.status()} ${response.statusText()} - ${body}`
22+
);
23+
}
24+
const responseBody = await response.json();
25+
return responseBody.article;
26+
} finally {
27+
await apiContext.dispose();
28+
}
29+
}
30+
631
static async deleteArticle(token: string, slug: string): Promise<void> {
732
const apiContext = await AuthUtility.createAuthenticatedApiContext(token);
833

@@ -42,4 +67,33 @@ export class ArticleUtility {
4267
getCallCount: () => callCount,
4368
}
4469
}
70+
71+
static async mockGetArticleList(page: Page, { delay = 0, status = 200, body = {} } = {}): Promise<void> {
72+
await page.route(mockApiUrl(API_URLS.ARTICLES.GET_LIST()), async route => {
73+
if (delay) await new Promise(r => setTimeout(r, delay));
74+
await route.fulfill({
75+
status,
76+
contentType: 'application/json',
77+
body: JSON.stringify(body),
78+
});
79+
});
80+
}
81+
82+
static async mockGetTagList(page: Page, { delay = 0, status = 200, body = {} } = {}): Promise<void> {
83+
await page.route(mockApiUrl(API_URLS.TAG.GET_LIST), async route => {
84+
if (delay) await new Promise(r => setTimeout(r, delay));
85+
await route.fulfill({
86+
status,
87+
contentType: 'application/json',
88+
body: JSON.stringify(body),
89+
});
90+
});
91+
}
92+
93+
static generateTestArticle(workerInfo: TestInfo, title: string): CreateArticleRequest {
94+
const shardIndex = workerInfo.config.shard?.current ?? 1;
95+
const id = CompositeIdFactory.create(title, 'shard', shardIndex, 'worker', workerInfo.parallelIndex);
96+
return ArticleFactory.buildArticlePayload(id);
97+
}
98+
4599
}

0 commit comments

Comments
 (0)