Skip to content

Commit 0b4bb70

Browse files
authored
Merge pull request #113 from contentstack/feat/e2e-gatsby-starter
feat: e2e for gatsby starter
2 parents d6bae1a + c57bf68 commit 0b4bb70

25 files changed

+790
-9
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ yarn-error.log
1212
.env.development
1313
.env.production
1414

15+
# Playwright
16+
playwright-report/
17+
test-results/
18+
1519
.vercel
1620

1721
.vscode/

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2025 Contentstack
3+
Copyright (c) 2026 Contentstack
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,42 @@ We have created an in-depth tutorial on how you can create a Gatsby starter webs
1818

1919
[Build a Starter Website with Gatsby and Contentstack](https://www.contentstack.com/docs/developers/sample-apps/build-a-starter-website-with-gatsby-and-contentstack/)
2020

21+
## E2E Tests
22+
23+
End-to-end tests using [Playwright](https://playwright.dev/).
24+
25+
### Setup
26+
27+
```bash
28+
npm install
29+
npx playwright install chromium
30+
```
31+
32+
### Run Tests
33+
34+
```bash
35+
# Development (port 8000)
36+
npm run develop # Terminal 1
37+
npm run test:e2e # Terminal 2
38+
39+
# Production build (port 9000)
40+
npm run build && npm run serve # Terminal 1
41+
npm run test:e2e:prod # Terminal 2
42+
```
43+
44+
### Commands
45+
46+
| Command | Description |
47+
|---------|-------------|
48+
| `npm run test:e2e` | Run tests against dev server (port 8000) |
49+
| `npm run test:e2e:prod` | Run tests against production build (port 9000) |
50+
| `npm run test:e2e:headed` | Run with visible browser |
51+
| `npm run test:e2e:debug` | Debug mode |
52+
| `npm run test:e2e:ui` | Interactive UI |
53+
| `npm run test:e2e:report` | View HTML report |
54+
2155
**More Resources**
2256

2357
- [Contentstack documentation](https://www.contentstack.com/docs/)
2458
- [Region support documentation](https://www.contentstack.com/docs/developers/selecting-region-in-contentstack-starter-apps)
25-
- [Gatsby documentation](https://www.gatsbyjs.com/docs/)
59+
- [Gatsby documentation](https://www.gatsbyjs.com/docs/)

e2e/fixtures/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { test, expect, TestFixtures } from './test-fixtures';

e2e/fixtures/test-fixtures.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { test as base } from '@playwright/test';
2+
import { HomePage, AboutPage, BlogPage, BlogPostPage, ContactPage } from '../pages';
3+
4+
export interface TestFixtures {
5+
homePage: HomePage;
6+
aboutPage: AboutPage;
7+
blogPage: BlogPage;
8+
blogPostPage: BlogPostPage;
9+
contactPage: ContactPage;
10+
}
11+
12+
export const test = base.extend<TestFixtures>({
13+
homePage: async ({ page }, use) => {
14+
await use(new HomePage(page));
15+
},
16+
aboutPage: async ({ page }, use) => {
17+
await use(new AboutPage(page));
18+
},
19+
blogPage: async ({ page }, use) => {
20+
await use(new BlogPage(page));
21+
},
22+
blogPostPage: async ({ page }, use) => {
23+
await use(new BlogPostPage(page));
24+
},
25+
contactPage: async ({ page }, use) => {
26+
await use(new ContactPage(page));
27+
},
28+
});
29+
30+
export { expect } from '@playwright/test';

e2e/pages/AboutPage.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Page } from '@playwright/test';
2+
import { BasePage } from './BasePage';
3+
4+
interface AboutSelectors {
5+
heroBanner: string;
6+
heroTitle: string;
7+
teamSection: string;
8+
teamMembers: string;
9+
}
10+
11+
export class AboutPage extends BasePage {
12+
readonly aboutSelectors: AboutSelectors;
13+
14+
constructor(page: Page) {
15+
super(page);
16+
this.aboutSelectors = {
17+
heroBanner: '.hero-banner',
18+
heroTitle: '.hero-banner .hero-title',
19+
teamSection: '.about-team-section',
20+
teamMembers: '.team-content .team-details',
21+
};
22+
}
23+
24+
async goto(): Promise<void> {
25+
await super.goto('/about-us');
26+
}
27+
28+
async isHeroBannerVisible(): Promise<boolean> {
29+
return await this.page.locator(this.aboutSelectors.heroBanner).isVisible();
30+
}
31+
32+
async getHeroTitle(): Promise<string | null> {
33+
return await this.page.locator(this.aboutSelectors.heroTitle).textContent();
34+
}
35+
36+
async isTeamSectionVisible(): Promise<boolean> {
37+
return await this.page.locator(this.aboutSelectors.teamSection).isVisible();
38+
}
39+
40+
async getTeamMembersCount(): Promise<number> {
41+
return await this.page.locator(this.aboutSelectors.teamMembers).count();
42+
}
43+
}

e2e/pages/BasePage.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { Page, Locator } from '@playwright/test';
2+
3+
export interface BaseSelectors {
4+
header: string;
5+
headerLogo: string;
6+
headerNavItems: string;
7+
footer: string;
8+
footerNav: string;
9+
footerSocialLinks: string;
10+
copyright: string;
11+
}
12+
13+
export class BasePage {
14+
readonly page: Page;
15+
readonly selectors: BaseSelectors;
16+
17+
constructor(page: Page) {
18+
this.page = page;
19+
this.selectors = {
20+
header: 'header.header',
21+
headerLogo: 'header.header .logo',
22+
headerNavItems: 'header.header .nav-li',
23+
footer: 'footer',
24+
footerNav: 'footer .nav-ul',
25+
footerSocialLinks: 'footer .social-nav a',
26+
copyright: '.copyright',
27+
};
28+
}
29+
30+
async goto(path: string = '/'): Promise<void> {
31+
await this.page.goto(path);
32+
await this.waitForPageLoad();
33+
}
34+
35+
async waitForPageLoad(): Promise<void> {
36+
await this.page.waitForLoadState('domcontentloaded');
37+
await this.page.locator(this.selectors.header).waitFor({ state: 'visible', timeout: 10000 });
38+
}
39+
40+
async getPageTitle(): Promise<string> {
41+
return await this.page.title();
42+
}
43+
44+
async isHeaderVisible(): Promise<boolean> {
45+
return await this.page.locator(this.selectors.header).isVisible();
46+
}
47+
48+
async isFooterVisible(): Promise<boolean> {
49+
return await this.page.locator(this.selectors.footer).isVisible();
50+
}
51+
52+
async getHeaderNavItems(): Promise<string[]> {
53+
await this.page.locator(this.selectors.headerNavItems).first().waitFor({ state: 'visible' });
54+
return await this.page.locator(`${this.selectors.headerNavItems} a`).allTextContents();
55+
}
56+
57+
async clickNavItem(text: string): Promise<void> {
58+
const currentUrl = this.page.url();
59+
const link = this.page.locator(`${this.selectors.headerNavItems} a`).filter({ hasText: text });
60+
await link.click();
61+
await this.page.waitForFunction(
62+
(oldUrl) => window.location.href !== oldUrl,
63+
currentUrl,
64+
{ timeout: 10000 }
65+
);
66+
await this.waitForPageLoad();
67+
}
68+
69+
async getFooterNavItems(): Promise<string[]> {
70+
return await this.page.locator(`${this.selectors.footerNav} a`).allTextContents();
71+
}
72+
73+
async getFooterSocialLinksCount(): Promise<number> {
74+
return await this.page.locator(this.selectors.footerSocialLinks).count();
75+
}
76+
77+
async getCopyrightText(): Promise<string | null> {
78+
return await this.page.locator(this.selectors.copyright).textContent();
79+
}
80+
81+
async clickLogo(): Promise<void> {
82+
const currentUrl = this.page.url();
83+
await this.page.waitForTimeout(200);
84+
await this.page.evaluate(() => {
85+
const logo = document.querySelector('header.header .logo') as HTMLElement;
86+
if (logo) {
87+
const link = logo.closest('a') as HTMLAnchorElement;
88+
if (link) link.click();
89+
else logo.click();
90+
}
91+
});
92+
if (!currentUrl.match(/\/$|:9000\/?$|:8000\/?$/)) {
93+
await this.page.waitForFunction(
94+
(oldUrl) => window.location.href !== oldUrl,
95+
currentUrl,
96+
{ timeout: 10000 }
97+
);
98+
}
99+
await this.waitForPageLoad();
100+
}
101+
102+
async getCurrentUrl(): Promise<string> {
103+
return this.page.url();
104+
}
105+
106+
getLocator(selector: string): Locator {
107+
return this.page.locator(selector);
108+
}
109+
}

e2e/pages/BlogPage.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { Page } from '@playwright/test';
2+
import { BasePage } from './BasePage';
3+
4+
interface BlogSelectors {
5+
blogBanner: string;
6+
bannerTitle: string;
7+
blogContainer: string;
8+
blogList: string;
9+
blogListTitle: string;
10+
archiveSection: string;
11+
archiveTitle: string;
12+
}
13+
14+
export class BlogPage extends BasePage {
15+
readonly blogSelectors: BlogSelectors;
16+
17+
constructor(page: Page) {
18+
super(page);
19+
this.blogSelectors = {
20+
blogBanner: '.blog-page-banner',
21+
bannerTitle: '.blog-page-banner .hero-title',
22+
blogContainer: '.blog-container',
23+
blogList: '.blog-list',
24+
blogListTitle: '.blog-list h3',
25+
archiveSection: '.blog-column-right',
26+
archiveTitle: '.blog-column-right h2',
27+
};
28+
}
29+
30+
async goto(): Promise<void> {
31+
await super.goto('/blog');
32+
}
33+
34+
async isBannerVisible(): Promise<boolean> {
35+
return await this.page.locator(this.blogSelectors.blogBanner).isVisible();
36+
}
37+
38+
async getBannerTitle(): Promise<string | null> {
39+
return await this.page.locator(this.blogSelectors.bannerTitle).textContent();
40+
}
41+
42+
async isBlogContainerVisible(): Promise<boolean> {
43+
return await this.page.locator(this.blogSelectors.blogContainer).isVisible();
44+
}
45+
46+
async getBlogPostsCount(): Promise<number> {
47+
return await this.page.locator(this.blogSelectors.blogList).count();
48+
}
49+
50+
async getBlogPostTitles(): Promise<string[]> {
51+
return await this.page.locator(this.blogSelectors.blogListTitle).allTextContents();
52+
}
53+
54+
async clickBlogPost(index: number = 0): Promise<void> {
55+
await this.page.locator(this.blogSelectors.blogList).nth(index).locator('a').first().click();
56+
await this.waitForPageLoad();
57+
}
58+
59+
async isArchiveSectionVisible(): Promise<boolean> {
60+
return await this.page.locator(this.blogSelectors.archiveSection).isVisible();
61+
}
62+
63+
async getArchiveTitle(): Promise<string | null> {
64+
return await this.page.locator(this.blogSelectors.archiveTitle).textContent();
65+
}
66+
}

e2e/pages/BlogPostPage.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Page } from '@playwright/test';
2+
import { BasePage } from './BasePage';
3+
4+
interface PostSelectors {
5+
blogContainer: string;
6+
blogDetail: string;
7+
postTitle: string;
8+
postAuthor: string;
9+
relatedPosts: string;
10+
}
11+
12+
export class BlogPostPage extends BasePage {
13+
readonly postSelectors: PostSelectors;
14+
15+
constructor(page: Page) {
16+
super(page);
17+
this.postSelectors = {
18+
blogContainer: '.blog-container',
19+
blogDetail: '.blog-detail',
20+
postTitle: '.blog-detail h2',
21+
postAuthor: '.blog-detail span strong',
22+
relatedPosts: '.related-post a',
23+
};
24+
}
25+
26+
async goto(slug: string = '/blog/sample-post/'): Promise<void> {
27+
await super.goto(slug);
28+
}
29+
30+
async getPostTitle(): Promise<string | null> {
31+
return await this.page.locator(this.postSelectors.postTitle).textContent();
32+
}
33+
34+
async getAuthorName(): Promise<string | null> {
35+
return await this.page.locator(this.postSelectors.postAuthor).textContent();
36+
}
37+
38+
async isBlogDetailVisible(): Promise<boolean> {
39+
return await this.page.locator(this.postSelectors.blogDetail).isVisible();
40+
}
41+
42+
async getRelatedPostsCount(): Promise<number> {
43+
return await this.page.locator(this.postSelectors.relatedPosts).count();
44+
}
45+
}

0 commit comments

Comments
 (0)