From 884805be594a41dbcf6f644e719b45b4606757a8 Mon Sep 17 00:00:00 2001 From: Abhishek Ezhava Date: Fri, 9 Jan 2026 12:08:59 +0530 Subject: [PATCH 1/3] feat: e2e for gatsby starter --- .gitignore | 2 + LICENSE | 2 +- e2e/README.md | 43 +++++++++++ e2e/env.development.sample | 2 + e2e/fixtures/index.ts | 1 + e2e/fixtures/test-fixtures.ts | 30 ++++++++ e2e/package-lock.json | 126 ++++++++++++++++++++++++++++++++ e2e/package.json | 36 +++++++++ e2e/pages/AboutPage.ts | 43 +++++++++++ e2e/pages/BasePage.ts | 108 +++++++++++++++++++++++++++ e2e/pages/BlogPage.ts | 66 +++++++++++++++++ e2e/pages/BlogPostPage.ts | 45 ++++++++++++ e2e/pages/ContactPage.ts | 41 +++++++++++ e2e/pages/HomePage.ts | 51 +++++++++++++ e2e/pages/index.ts | 6 ++ e2e/playwright.config.ts | 32 ++++++++ e2e/tests/about.spec.ts | 31 ++++++++ e2e/tests/accessibility.spec.ts | 25 +++++++ e2e/tests/blog-post.spec.ts | 32 ++++++++ e2e/tests/blog.spec.ts | 31 ++++++++ e2e/tests/contact.spec.ts | 31 ++++++++ e2e/tests/home.spec.ts | 32 ++++++++ e2e/tests/navigation.spec.ts | 28 +++++++ e2e/tsconfig.json | 15 ++++ e2e/utils/index.ts | 1 + e2e/utils/test-helpers.ts | 21 ++++++ test-results/.last-run.json | 4 + 27 files changed, 884 insertions(+), 1 deletion(-) create mode 100644 e2e/README.md create mode 100644 e2e/env.development.sample create mode 100644 e2e/fixtures/index.ts create mode 100644 e2e/fixtures/test-fixtures.ts create mode 100644 e2e/package-lock.json create mode 100644 e2e/package.json create mode 100644 e2e/pages/AboutPage.ts create mode 100644 e2e/pages/BasePage.ts create mode 100644 e2e/pages/BlogPage.ts create mode 100644 e2e/pages/BlogPostPage.ts create mode 100644 e2e/pages/ContactPage.ts create mode 100644 e2e/pages/HomePage.ts create mode 100644 e2e/pages/index.ts create mode 100644 e2e/playwright.config.ts create mode 100644 e2e/tests/about.spec.ts create mode 100644 e2e/tests/accessibility.spec.ts create mode 100644 e2e/tests/blog-post.spec.ts create mode 100644 e2e/tests/blog.spec.ts create mode 100644 e2e/tests/contact.spec.ts create mode 100644 e2e/tests/home.spec.ts create mode 100644 e2e/tests/navigation.spec.ts create mode 100644 e2e/tsconfig.json create mode 100644 e2e/utils/index.ts create mode 100644 e2e/utils/test-helpers.ts create mode 100644 test-results/.last-run.json diff --git a/.gitignore b/.gitignore index 3200dcf..654d828 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,8 @@ yarn-error.log .env.development .env.production +playwright-report/ + .vercel .vscode/ diff --git a/LICENSE b/LICENSE index 816ce04..13a5837 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 Contentstack +Copyright (c) 2026 Contentstack Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/e2e/README.md b/e2e/README.md new file mode 100644 index 0000000..bb26b09 --- /dev/null +++ b/e2e/README.md @@ -0,0 +1,43 @@ +# E2E Test Suite + +End-to-end tests for Contentstack Gatsby Starter App using [Playwright](https://playwright.dev/). + +## Setup + +```bash +cd e2e +npm install +npx playwright install chromium +``` + +## Run Tests + +```bash +# Start Gatsby app first (in another terminal) +cd contentstack-gatsby-starter-app +npm run develop + +# Run tests +cd e2e +npm test # Headless +npm run test:headed # With browser +npm run test:debug # Debug mode +``` + +## Structure + +``` +e2e/ +├── pages/ # Page Object Models +├── tests/ # Test specs +├── fixtures/ # Playwright fixtures +└── utils/ # Helpers +``` + +## Configuration + +Set `BASE_URL` in `.env.development`: + +``` +BASE_URL=http://localhost:8000 +``` diff --git a/e2e/env.development.sample b/e2e/env.development.sample new file mode 100644 index 0000000..076c40a --- /dev/null +++ b/e2e/env.development.sample @@ -0,0 +1,2 @@ +// Base URL for the Gatsby app (development server) +BASE_URL=http://localhost:8000 diff --git a/e2e/fixtures/index.ts b/e2e/fixtures/index.ts new file mode 100644 index 0000000..03f98d5 --- /dev/null +++ b/e2e/fixtures/index.ts @@ -0,0 +1 @@ +export { test, expect, TestFixtures } from './test-fixtures'; diff --git a/e2e/fixtures/test-fixtures.ts b/e2e/fixtures/test-fixtures.ts new file mode 100644 index 0000000..e8be72b --- /dev/null +++ b/e2e/fixtures/test-fixtures.ts @@ -0,0 +1,30 @@ +import { test as base } from '@playwright/test'; +import { HomePage, AboutPage, BlogPage, BlogPostPage, ContactPage } from '../pages'; + +export interface TestFixtures { + homePage: HomePage; + aboutPage: AboutPage; + blogPage: BlogPage; + blogPostPage: BlogPostPage; + contactPage: ContactPage; +} + +export const test = base.extend({ + homePage: async ({ page }, use) => { + await use(new HomePage(page)); + }, + aboutPage: async ({ page }, use) => { + await use(new AboutPage(page)); + }, + blogPage: async ({ page }, use) => { + await use(new BlogPage(page)); + }, + blogPostPage: async ({ page }, use) => { + await use(new BlogPostPage(page)); + }, + contactPage: async ({ page }, use) => { + await use(new ContactPage(page)); + }, +}); + +export { expect } from '@playwright/test'; diff --git a/e2e/package-lock.json b/e2e/package-lock.json new file mode 100644 index 0000000..ab8d2c9 --- /dev/null +++ b/e2e/package-lock.json @@ -0,0 +1,126 @@ +{ + "name": "contentstack-gatsby-starter-app-e2e", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "contentstack-gatsby-starter-app-e2e", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "@playwright/test": "^1.40.0", + "@types/node": "^20.10.0", + "dotenv": "^16.3.1", + "typescript": "^5.3.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", + "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "20.19.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz", + "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/e2e/package.json b/e2e/package.json new file mode 100644 index 0000000..06a4ac7 --- /dev/null +++ b/e2e/package.json @@ -0,0 +1,36 @@ +{ + "name": "contentstack-gatsby-starter-app-e2e", + "version": "1.0.0", + "description": "E2E test suite for Contentstack Gatsby Starter App using Playwright", + "main": "playwright.config.ts", + "scripts": { + "test": "playwright test", + "test:headed": "playwright test --headed", + "test:headless": "playwright test", + "test:chromium": "playwright test --project=chromium", + "test:firefox": "playwright test --project=firefox", + "test:webkit": "playwright test --project=webkit", + "test:mobile": "playwright test --project=mobile-chrome --project=mobile-safari", + "test:debug": "playwright test --debug", + "test:ui": "playwright test --ui", + "test:report": "playwright show-report ../playwright-report", + "test:ci": "CI=true playwright test --project=chromium", + "install:browsers": "playwright install" + }, + "keywords": [ + "playwright", + "e2e", + "testing", + "contentstack", + "gatsby", + "typescript" + ], + "author": "Contentstack", + "license": "MIT", + "devDependencies": { + "@playwright/test": "^1.40.0", + "@types/node": "^20.10.0", + "dotenv": "^16.3.1", + "typescript": "^5.3.0" + } +} \ No newline at end of file diff --git a/e2e/pages/AboutPage.ts b/e2e/pages/AboutPage.ts new file mode 100644 index 0000000..dcc608c --- /dev/null +++ b/e2e/pages/AboutPage.ts @@ -0,0 +1,43 @@ +import { Page } from '@playwright/test'; +import { BasePage } from './BasePage'; + +interface AboutSelectors { + heroBanner: string; + heroTitle: string; + teamSection: string; + teamMembers: string; +} + +export class AboutPage extends BasePage { + readonly aboutSelectors: AboutSelectors; + + constructor(page: Page) { + super(page); + this.aboutSelectors = { + heroBanner: '.hero-banner', + heroTitle: '.hero-banner .hero-title', + teamSection: '.about-team-section', + teamMembers: '.team-content .team-details', + }; + } + + async goto(): Promise { + await super.goto('/about-us'); + } + + async isHeroBannerVisible(): Promise { + return await this.page.locator(this.aboutSelectors.heroBanner).isVisible(); + } + + async getHeroTitle(): Promise { + return await this.page.locator(this.aboutSelectors.heroTitle).textContent(); + } + + async isTeamSectionVisible(): Promise { + return await this.page.locator(this.aboutSelectors.teamSection).isVisible(); + } + + async getTeamMembersCount(): Promise { + return await this.page.locator(this.aboutSelectors.teamMembers).count(); + } +} diff --git a/e2e/pages/BasePage.ts b/e2e/pages/BasePage.ts new file mode 100644 index 0000000..d0bbe04 --- /dev/null +++ b/e2e/pages/BasePage.ts @@ -0,0 +1,108 @@ +import { Page, Locator } from '@playwright/test'; + +export interface BaseSelectors { + header: string; + headerLogo: string; + headerNavItems: string; + footer: string; + footerNav: string; + footerSocialLinks: string; + copyright: string; +} + +export class BasePage { + readonly page: Page; + readonly selectors: BaseSelectors; + + constructor(page: Page) { + this.page = page; + this.selectors = { + header: 'header', + headerLogo: 'header .logo', + headerNavItems: 'header .nav-li', + footer: 'footer', + footerNav: 'footer .nav-ul', + footerSocialLinks: 'footer .social-nav a', + copyright: '.copyright', + }; + } + + async goto(path: string = '/'): Promise { + await this.page.goto(path); + await this.waitForPageLoad(); + } + + async waitForPageLoad(): Promise { + await this.page.waitForLoadState('domcontentloaded'); + } + + async getPageTitle(): Promise { + return await this.page.title(); + } + + async isHeaderVisible(): Promise { + return await this.page.locator(this.selectors.header).isVisible(); + } + + async isFooterVisible(): Promise { + return await this.page.locator(this.selectors.footer).isVisible(); + } + + async getHeaderNavItems(): Promise { + await this.page.locator(this.selectors.headerNavItems).first().waitFor({ state: 'visible' }); + return await this.page.locator(`${this.selectors.headerNavItems} a`).allTextContents(); + } + + async clickNavItem(text: string): Promise { + const currentUrl = this.page.url(); + const link = this.page.locator(`${this.selectors.headerNavItems} a`).filter({ hasText: text }); + await link.click(); + await this.page.waitForFunction( + (oldUrl) => window.location.href !== oldUrl, + currentUrl, + { timeout: 10000 } + ); + await this.waitForPageLoad(); + } + + async getFooterNavItems(): Promise { + return await this.page.locator(`${this.selectors.footerNav} a`).allTextContents(); + } + + async getFooterSocialLinksCount(): Promise { + return await this.page.locator(this.selectors.footerSocialLinks).count(); + } + + async getCopyrightText(): Promise { + return await this.page.locator(this.selectors.copyright).textContent(); + } + + async clickLogo(): Promise { + const currentUrl = this.page.url(); + await this.page.waitForTimeout(200); + await this.page.evaluate(() => { + const logo = document.querySelector('header .logo') as HTMLElement; + if (logo) { + const link = logo.closest('a') as HTMLAnchorElement; + if (link) link.click(); + else logo.click(); + } + }); + if (!currentUrl.match(/\/$|:8000\/?$/)) { + await this.page.waitForFunction( + (oldUrl) => window.location.href !== oldUrl, + currentUrl, + { timeout: 10000 } + ); + } + await this.waitForPageLoad(); + } + + async getCurrentUrl(): Promise { + return this.page.url(); + } + + getLocator(selector: string): Locator { + return this.page.locator(selector); + } +} diff --git a/e2e/pages/BlogPage.ts b/e2e/pages/BlogPage.ts new file mode 100644 index 0000000..452d471 --- /dev/null +++ b/e2e/pages/BlogPage.ts @@ -0,0 +1,66 @@ +import { Page } from '@playwright/test'; +import { BasePage } from './BasePage'; + +interface BlogSelectors { + blogBanner: string; + bannerTitle: string; + blogContainer: string; + blogList: string; + blogListTitle: string; + archiveSection: string; + archiveTitle: string; +} + +export class BlogPage extends BasePage { + readonly blogSelectors: BlogSelectors; + + constructor(page: Page) { + super(page); + this.blogSelectors = { + blogBanner: '.blog-page-banner', + bannerTitle: '.blog-page-banner .hero-title', + blogContainer: '.blog-container', + blogList: '.blog-list', + blogListTitle: '.blog-list h3', + archiveSection: '.blog-column-right', + archiveTitle: '.blog-column-right h2', + }; + } + + async goto(): Promise { + await super.goto('/blog'); + } + + async isBannerVisible(): Promise { + return await this.page.locator(this.blogSelectors.blogBanner).isVisible(); + } + + async getBannerTitle(): Promise { + return await this.page.locator(this.blogSelectors.bannerTitle).textContent(); + } + + async isBlogContainerVisible(): Promise { + return await this.page.locator(this.blogSelectors.blogContainer).isVisible(); + } + + async getBlogPostsCount(): Promise { + return await this.page.locator(this.blogSelectors.blogList).count(); + } + + async getBlogPostTitles(): Promise { + return await this.page.locator(this.blogSelectors.blogListTitle).allTextContents(); + } + + async clickBlogPost(index: number = 0): Promise { + await this.page.locator(this.blogSelectors.blogList).nth(index).locator('a').first().click(); + await this.waitForPageLoad(); + } + + async isArchiveSectionVisible(): Promise { + return await this.page.locator(this.blogSelectors.archiveSection).isVisible(); + } + + async getArchiveTitle(): Promise { + return await this.page.locator(this.blogSelectors.archiveTitle).textContent(); + } +} diff --git a/e2e/pages/BlogPostPage.ts b/e2e/pages/BlogPostPage.ts new file mode 100644 index 0000000..36327df --- /dev/null +++ b/e2e/pages/BlogPostPage.ts @@ -0,0 +1,45 @@ +import { Page } from '@playwright/test'; +import { BasePage } from './BasePage'; + +interface PostSelectors { + blogContainer: string; + blogDetail: string; + postTitle: string; + postAuthor: string; + relatedPosts: string; +} + +export class BlogPostPage extends BasePage { + readonly postSelectors: PostSelectors; + + constructor(page: Page) { + super(page); + this.postSelectors = { + blogContainer: '.blog-container', + blogDetail: '.blog-detail', + postTitle: '.blog-detail h2', + postAuthor: '.blog-detail span strong', + relatedPosts: '.related-post a', + }; + } + + async goto(slug: string = '/blog/sample-post/'): Promise { + await super.goto(slug); + } + + async getPostTitle(): Promise { + return await this.page.locator(this.postSelectors.postTitle).textContent(); + } + + async getAuthorName(): Promise { + return await this.page.locator(this.postSelectors.postAuthor).textContent(); + } + + async isBlogDetailVisible(): Promise { + return await this.page.locator(this.postSelectors.blogDetail).isVisible(); + } + + async getRelatedPostsCount(): Promise { + return await this.page.locator(this.postSelectors.relatedPosts).count(); + } +} diff --git a/e2e/pages/ContactPage.ts b/e2e/pages/ContactPage.ts new file mode 100644 index 0000000..f5f0ee0 --- /dev/null +++ b/e2e/pages/ContactPage.ts @@ -0,0 +1,41 @@ +import { Page } from '@playwright/test'; +import { BasePage } from './BasePage'; + +interface ContactSelectors { + heroBanner: string; + heroTitle: string; + pageContent: string; +} + +export class ContactPage extends BasePage { + readonly contactSelectors: ContactSelectors; + + constructor(page: Page) { + super(page); + this.contactSelectors = { + heroBanner: '.hero-banner', + heroTitle: '.hero-banner .hero-title, h1', + pageContent: 'main', + }; + } + + async goto(): Promise { + await super.goto('/contact-us'); + } + + async isHeroBannerVisible(): Promise { + return await this.page.locator(this.contactSelectors.heroBanner).isVisible(); + } + + async getPageHeading(): Promise { + const h1 = this.page.locator('h1').first(); + if (await h1.isVisible()) { + return await h1.textContent(); + } + return null; + } + + async hasMainContent(): Promise { + return await this.page.locator(this.contactSelectors.pageContent).isVisible(); + } +} diff --git a/e2e/pages/HomePage.ts b/e2e/pages/HomePage.ts new file mode 100644 index 0000000..ca57143 --- /dev/null +++ b/e2e/pages/HomePage.ts @@ -0,0 +1,51 @@ +import { Page } from '@playwright/test'; +import { BasePage } from './BasePage'; + +interface HomeSelectors { + heroBanner: string; + heroTitle: string; + featuredBlogs: string; + blogTitle: string; +} + +export class HomePage extends BasePage { + readonly homeSelectors: HomeSelectors; + + constructor(page: Page) { + super(page); + this.homeSelectors = { + heroBanner: '.hero-banner', + heroTitle: '.hero-banner .hero-title', + featuredBlogs: '.home-featured-blogs .featured-blog', + blogTitle: '.featured-blog h3', + }; + } + + async goto(): Promise { + await super.goto('/'); + } + + async isHeroBannerVisible(): Promise { + return await this.page.locator(this.homeSelectors.heroBanner).isVisible(); + } + + async getHeroTitle(): Promise { + return await this.page.locator(this.homeSelectors.heroTitle).textContent(); + } + + async getFeaturedBlogsCount(): Promise { + return await this.page.locator(this.homeSelectors.featuredBlogs).count(); + } + + async clickFeaturedBlog(index: number = 0): Promise { + const currentUrl = this.page.url(); + const link = this.page.locator(this.homeSelectors.featuredBlogs).nth(index).locator('a').first(); + await link.click(); + await this.page.waitForFunction( + (oldUrl) => window.location.href !== oldUrl, + currentUrl, + { timeout: 10000 } + ); + await this.waitForPageLoad(); + } +} diff --git a/e2e/pages/index.ts b/e2e/pages/index.ts new file mode 100644 index 0000000..35c1911 --- /dev/null +++ b/e2e/pages/index.ts @@ -0,0 +1,6 @@ +export { BasePage } from './BasePage'; +export { HomePage } from './HomePage'; +export { AboutPage } from './AboutPage'; +export { BlogPage } from './BlogPage'; +export { BlogPostPage } from './BlogPostPage'; +export { ContactPage } from './ContactPage'; diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts new file mode 100644 index 0000000..e3fa7ee --- /dev/null +++ b/e2e/playwright.config.ts @@ -0,0 +1,32 @@ +import { defineConfig, devices } from '@playwright/test'; +import dotenv from 'dotenv'; + +dotenv.config({ path: `.env.${process.env.NODE_ENV || 'development'}` }); + +export default defineConfig({ + testDir: './tests', + timeout: 30 * 1000, + expect: { timeout: 5000 }, + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: 2, + workers: 1, + reporter: [ + ['list'], + ['html', { outputFolder: '../playwright-report', open: 'never' }], + ], + use: { + baseURL: process.env.BASE_URL || 'http://localhost:8000', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + viewport: { width: 1280, height: 720 }, + ignoreHTTPSErrors: true, + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + outputDir: '../test-results', +}); diff --git a/e2e/tests/about.spec.ts b/e2e/tests/about.spec.ts new file mode 100644 index 0000000..8598f1f --- /dev/null +++ b/e2e/tests/about.spec.ts @@ -0,0 +1,31 @@ +import { test, expect } from '../fixtures'; + +test.describe('About Page', () => { + test('should load with header and footer', async ({ aboutPage }) => { + await aboutPage.goto(); + expect(await aboutPage.isHeaderVisible()).toBe(true); + expect(await aboutPage.isFooterVisible()).toBe(true); + }); + + test('should have correct URL', async ({ aboutPage }) => { + await aboutPage.goto(); + expect(await aboutPage.getCurrentUrl()).toContain('/about-us'); + }); + + test('should have hero banner', async ({ aboutPage }) => { + await aboutPage.goto(); + expect(await aboutPage.isHeroBannerVisible()).toBe(true); + }); + + test('should navigate to Blog page', async ({ aboutPage }) => { + await aboutPage.goto(); + await aboutPage.clickNavItem('Blog'); + expect(await aboutPage.getCurrentUrl()).toContain('/blog'); + }); + + test('should navigate home via logo', async ({ aboutPage }) => { + await aboutPage.goto(); + await aboutPage.clickLogo(); + expect(await aboutPage.getCurrentUrl()).toMatch(/\/$|:8000\/?$/); + }); +}); diff --git a/e2e/tests/accessibility.spec.ts b/e2e/tests/accessibility.spec.ts new file mode 100644 index 0000000..9c8006a --- /dev/null +++ b/e2e/tests/accessibility.spec.ts @@ -0,0 +1,25 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Accessibility', () => { + test('should have alt text on logos', async ({ page }) => { + await page.goto('/'); + const headerLogo = page.locator('header .logo'); + await expect(headerLogo).toBeVisible(); + const alt = await headerLogo.getAttribute('alt'); + expect(alt).toBeTruthy(); + }); + + test('should have semantic header and footer', async ({ page }) => { + await page.goto('/'); + await expect(page.locator('header')).toBeVisible(); + await expect(page.locator('footer')).toBeVisible(); + }); + + test('should have h1 heading on pages', async ({ page }) => { + await page.goto('/'); + await expect(page.locator('h1').first()).toBeVisible(); + + await page.goto('/about-us'); + await expect(page.locator('h1').first()).toBeVisible(); + }); +}); diff --git a/e2e/tests/blog-post.spec.ts b/e2e/tests/blog-post.spec.ts new file mode 100644 index 0000000..eeb3e8c --- /dev/null +++ b/e2e/tests/blog-post.spec.ts @@ -0,0 +1,32 @@ +import { test, expect } from '../fixtures'; + +test.describe('Blog Post Page', () => { + test('should navigate to blog post from listing', async ({ blogPage }) => { + await blogPage.goto(); + const postsCount = await blogPage.getBlogPostsCount(); + if (postsCount > 0) { + await blogPage.clickBlogPost(0); + expect(await blogPage.getCurrentUrl()).toContain('/blog'); + } + }); + + test('should display blog post content', async ({ blogPage, page }) => { + await blogPage.goto(); + const postsCount = await blogPage.getBlogPostsCount(); + if (postsCount > 0) { + await blogPage.clickBlogPost(0); + await expect(page.locator('header')).toBeVisible(); + await expect(page.locator('footer')).toBeVisible(); + } + }); + + test('should display blog post title', async ({ blogPage, page }) => { + await blogPage.goto(); + const postsCount = await blogPage.getBlogPostsCount(); + if (postsCount > 0) { + await blogPage.clickBlogPost(0); + const title = page.locator('.blog-detail h2'); + await expect(title).toBeVisible(); + } + }); +}); diff --git a/e2e/tests/blog.spec.ts b/e2e/tests/blog.spec.ts new file mode 100644 index 0000000..fd7b8d0 --- /dev/null +++ b/e2e/tests/blog.spec.ts @@ -0,0 +1,31 @@ +import { test, expect } from '../fixtures'; + +test.describe('Blog Page', () => { + test('should load with header and footer', async ({ blogPage }) => { + await blogPage.goto(); + expect(await blogPage.isHeaderVisible()).toBe(true); + expect(await blogPage.isFooterVisible()).toBe(true); + }); + + test('should have correct URL', async ({ blogPage }) => { + await blogPage.goto(); + expect(await blogPage.getCurrentUrl()).toContain('/blog'); + }); + + test('should display blog container', async ({ blogPage }) => { + await blogPage.goto(); + expect(await blogPage.isBlogContainerVisible()).toBe(true); + }); + + test('should navigate to About page', async ({ blogPage }) => { + await blogPage.goto(); + await blogPage.clickNavItem('About'); + expect(await blogPage.getCurrentUrl()).toContain('/about'); + }); + + test('should navigate home via logo', async ({ blogPage }) => { + await blogPage.goto(); + await blogPage.clickLogo(); + expect(await blogPage.getCurrentUrl()).toMatch(/\/$|:8000\/?$/); + }); +}); diff --git a/e2e/tests/contact.spec.ts b/e2e/tests/contact.spec.ts new file mode 100644 index 0000000..95e08af --- /dev/null +++ b/e2e/tests/contact.spec.ts @@ -0,0 +1,31 @@ +import { test, expect } from '../fixtures'; + +test.describe('Contact Page', () => { + test('should load with header and footer', async ({ contactPage }) => { + await contactPage.goto(); + expect(await contactPage.isHeaderVisible()).toBe(true); + expect(await contactPage.isFooterVisible()).toBe(true); + }); + + test('should have correct URL', async ({ contactPage }) => { + await contactPage.goto(); + expect(await contactPage.getCurrentUrl()).toContain('/contact-us'); + }); + + test('should have main content', async ({ contactPage }) => { + await contactPage.goto(); + expect(await contactPage.hasMainContent()).toBe(true); + }); + + test('should navigate to About page', async ({ contactPage }) => { + await contactPage.goto(); + await contactPage.clickNavItem('About'); + expect(await contactPage.getCurrentUrl()).toContain('/about'); + }); + + test('should navigate home via logo', async ({ contactPage }) => { + await contactPage.goto(); + await contactPage.clickLogo(); + expect(await contactPage.getCurrentUrl()).toMatch(/\/$|:8000\/?$/); + }); +}); diff --git a/e2e/tests/home.spec.ts b/e2e/tests/home.spec.ts new file mode 100644 index 0000000..a72e29b --- /dev/null +++ b/e2e/tests/home.spec.ts @@ -0,0 +1,32 @@ +import { test, expect } from '../fixtures'; + +test.describe('Home Page', () => { + test('should load with header and footer', async ({ homePage }) => { + await homePage.goto(); + expect(await homePage.isHeaderVisible()).toBe(true); + expect(await homePage.isFooterVisible()).toBe(true); + }); + + test('should have hero banner', async ({ homePage }) => { + await homePage.goto(); + expect(await homePage.isHeroBannerVisible()).toBe(true); + }); + + test('should navigate to About page', async ({ homePage }) => { + await homePage.goto(); + await homePage.clickNavItem('About'); + expect(await homePage.getCurrentUrl()).toContain('/about'); + }); + + test('should navigate to Blog page', async ({ homePage }) => { + await homePage.goto(); + await homePage.clickNavItem('Blog'); + expect(await homePage.getCurrentUrl()).toContain('/blog'); + }); + + test('should navigate to Contact page', async ({ homePage }) => { + await homePage.goto(); + await homePage.clickNavItem('Contact'); + expect(await homePage.getCurrentUrl()).toContain('/contact'); + }); +}); diff --git a/e2e/tests/navigation.spec.ts b/e2e/tests/navigation.spec.ts new file mode 100644 index 0000000..1a34339 --- /dev/null +++ b/e2e/tests/navigation.spec.ts @@ -0,0 +1,28 @@ +import { test, expect } from '../fixtures'; + +test.describe('Site Navigation', () => { + test('should have consistent nav across pages', async ({ homePage, aboutPage, blogPage }) => { + await homePage.goto(); + const homeNav = await homePage.getHeaderNavItems(); + + await aboutPage.goto(); + const aboutNav = await aboutPage.getHeaderNavItems(); + + await blogPage.goto(); + const blogNav = await blogPage.getHeaderNavItems(); + + expect(homeNav.length).toBe(aboutNav.length); + expect(aboutNav.length).toBe(blogNav.length); + }); + + test('should complete user journey home to blog to home', async ({ homePage, blogPage }) => { + await homePage.goto(); + expect(await homePage.isHeroBannerVisible()).toBe(true); + + await homePage.clickNavItem('Blog'); + expect(await blogPage.isBlogContainerVisible()).toBe(true); + + await blogPage.clickLogo(); + expect(await homePage.getCurrentUrl()).toMatch(/\/$|:8000\/?$/); + }); +}); diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json new file mode 100644 index 0000000..1dc7526 --- /dev/null +++ b/e2e/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist", + "rootDir": "./" + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/e2e/utils/index.ts b/e2e/utils/index.ts new file mode 100644 index 0000000..f116a86 --- /dev/null +++ b/e2e/utils/index.ts @@ -0,0 +1 @@ +export { getAllLinks, LinkData, TIMEOUTS } from './test-helpers'; diff --git a/e2e/utils/test-helpers.ts b/e2e/utils/test-helpers.ts new file mode 100644 index 0000000..987a0bb --- /dev/null +++ b/e2e/utils/test-helpers.ts @@ -0,0 +1,21 @@ +import { Page } from '@playwright/test'; + +export interface LinkData { + text: string; + href: string; +} + +export async function getAllLinks(page: Page): Promise { + return await page.$$eval('a', (links) => + links.map((link) => ({ + text: link.textContent?.trim() || '', + href: link.getAttribute('href') || '', + })) + ); +} + +export const TIMEOUTS = { + SHORT: 3000, + MEDIUM: 5000, + LONG: 10000, +} as const; diff --git a/test-results/.last-run.json b/test-results/.last-run.json new file mode 100644 index 0000000..cbcc1fb --- /dev/null +++ b/test-results/.last-run.json @@ -0,0 +1,4 @@ +{ + "status": "passed", + "failedTests": [] +} \ No newline at end of file From 333fc005df55ffe7faecc2d6020a5840f8e83775 Mon Sep 17 00:00:00 2001 From: Abhishek Ezhava Date: Fri, 9 Jan 2026 14:43:09 +0530 Subject: [PATCH 2/3] fix: consolidated suite with main package & test runners --- e2e/README.md | 50 +++++++------ e2e/env.development.sample | 2 - e2e/package-lock.json | 126 -------------------------------- e2e/package.json | 36 --------- e2e/pages/BasePage.ts | 11 +-- e2e/pages/ContactPage.ts | 5 +- e2e/playwright.config.ts | 20 ++++- e2e/tests/accessibility.spec.ts | 7 +- package-lock.json | 70 +++++++++++++++++- package.json | 12 ++- 10 files changed, 137 insertions(+), 202 deletions(-) delete mode 100644 e2e/env.development.sample delete mode 100644 e2e/package-lock.json delete mode 100644 e2e/package.json diff --git a/e2e/README.md b/e2e/README.md index bb26b09..780cfd3 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -5,7 +5,6 @@ End-to-end tests for Contentstack Gatsby Starter App using [Playwright](https:// ## Setup ```bash -cd e2e npm install npx playwright install chromium ``` @@ -13,31 +12,40 @@ npx playwright install chromium ## Run Tests ```bash -# Start Gatsby app first (in another terminal) -cd contentstack-gatsby-starter-app -npm run develop - -# Run tests -cd e2e -npm test # Headless -npm run test:headed # With browser -npm run test:debug # Debug mode +# Development (port 8000) +npm run develop # Terminal 1 +npm run test:e2e # Terminal 2 + +# Production build (port 9000) +npm run build && npm run serve # Terminal 1 +npm run test:e2e:prod # Terminal 2 ``` -## Structure +## Commands -``` -e2e/ -├── pages/ # Page Object Models -├── tests/ # Test specs -├── fixtures/ # Playwright fixtures -└── utils/ # Helpers -``` +| Command | Description | +|---------|-------------| +| `npm run test:e2e` | Run tests against dev server (port 8000) | +| `npm run test:e2e:prod` | Run tests against production build (port 9000) | +| `npm run test:e2e:headed` | Run with visible browser | +| `npm run test:e2e:debug` | Debug mode | +| `npm run test:e2e:ui` | Interactive UI | +| `npm run test:e2e:report` | View HTML report | -## Configuration +## Custom URL (Optional) -Set `BASE_URL` in `.env.development`: +To use a custom URL, set `BASE_URL` in your `.env.development` or `.env.production`: +```bash +BASE_URL=http://localhost:3000 ``` -BASE_URL=http://localhost:8000 + +## Structure + +``` +e2e/ +├── pages/ # Page Object Models +├── tests/ # Test specs +├── fixtures/ # Playwright fixtures +└── utils/ # Helpers ``` diff --git a/e2e/env.development.sample b/e2e/env.development.sample deleted file mode 100644 index 076c40a..0000000 --- a/e2e/env.development.sample +++ /dev/null @@ -1,2 +0,0 @@ -// Base URL for the Gatsby app (development server) -BASE_URL=http://localhost:8000 diff --git a/e2e/package-lock.json b/e2e/package-lock.json deleted file mode 100644 index ab8d2c9..0000000 --- a/e2e/package-lock.json +++ /dev/null @@ -1,126 +0,0 @@ -{ - "name": "contentstack-gatsby-starter-app-e2e", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "contentstack-gatsby-starter-app-e2e", - "version": "1.0.0", - "license": "MIT", - "devDependencies": { - "@playwright/test": "^1.40.0", - "@types/node": "^20.10.0", - "dotenv": "^16.3.1", - "typescript": "^5.3.0" - } - }, - "node_modules/@playwright/test": { - "version": "1.57.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", - "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.57.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@types/node": { - "version": "20.19.27", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz", - "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/playwright": { - "version": "1.57.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", - "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.57.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.57.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", - "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - } - } -} diff --git a/e2e/package.json b/e2e/package.json deleted file mode 100644 index 06a4ac7..0000000 --- a/e2e/package.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "contentstack-gatsby-starter-app-e2e", - "version": "1.0.0", - "description": "E2E test suite for Contentstack Gatsby Starter App using Playwright", - "main": "playwright.config.ts", - "scripts": { - "test": "playwright test", - "test:headed": "playwright test --headed", - "test:headless": "playwright test", - "test:chromium": "playwright test --project=chromium", - "test:firefox": "playwright test --project=firefox", - "test:webkit": "playwright test --project=webkit", - "test:mobile": "playwright test --project=mobile-chrome --project=mobile-safari", - "test:debug": "playwright test --debug", - "test:ui": "playwright test --ui", - "test:report": "playwright show-report ../playwright-report", - "test:ci": "CI=true playwright test --project=chromium", - "install:browsers": "playwright install" - }, - "keywords": [ - "playwright", - "e2e", - "testing", - "contentstack", - "gatsby", - "typescript" - ], - "author": "Contentstack", - "license": "MIT", - "devDependencies": { - "@playwright/test": "^1.40.0", - "@types/node": "^20.10.0", - "dotenv": "^16.3.1", - "typescript": "^5.3.0" - } -} \ No newline at end of file diff --git a/e2e/pages/BasePage.ts b/e2e/pages/BasePage.ts index d0bbe04..9ac5be2 100644 --- a/e2e/pages/BasePage.ts +++ b/e2e/pages/BasePage.ts @@ -17,9 +17,9 @@ export class BasePage { constructor(page: Page) { this.page = page; this.selectors = { - header: 'header', - headerLogo: 'header .logo', - headerNavItems: 'header .nav-li', + header: 'header.header', + headerLogo: 'header.header .logo', + headerNavItems: 'header.header .nav-li', footer: 'footer', footerNav: 'footer .nav-ul', footerSocialLinks: 'footer .social-nav a', @@ -34,6 +34,7 @@ export class BasePage { async waitForPageLoad(): Promise { await this.page.waitForLoadState('domcontentloaded'); + await this.page.locator(this.selectors.header).waitFor({ state: 'visible', timeout: 10000 }); } async getPageTitle(): Promise { @@ -81,14 +82,14 @@ export class BasePage { const currentUrl = this.page.url(); await this.page.waitForTimeout(200); await this.page.evaluate(() => { - const logo = document.querySelector('header .logo') as HTMLElement; + const logo = document.querySelector('header.header .logo') as HTMLElement; if (logo) { const link = logo.closest('a') as HTMLAnchorElement; if (link) link.click(); else logo.click(); } }); - if (!currentUrl.match(/\/$|:8000\/?$/)) { + if (!currentUrl.match(/\/$|:9000\/?$|:8000\/?$/)) { await this.page.waitForFunction( (oldUrl) => window.location.href !== oldUrl, currentUrl, diff --git a/e2e/pages/ContactPage.ts b/e2e/pages/ContactPage.ts index f5f0ee0..d4971a6 100644 --- a/e2e/pages/ContactPage.ts +++ b/e2e/pages/ContactPage.ts @@ -15,7 +15,7 @@ export class ContactPage extends BasePage { this.contactSelectors = { heroBanner: '.hero-banner', heroTitle: '.hero-banner .hero-title, h1', - pageContent: 'main', + pageContent: '.contact-page-section, .contact-maps-section, .about', }; } @@ -36,6 +36,7 @@ export class ContactPage extends BasePage { } async hasMainContent(): Promise { - return await this.page.locator(this.contactSelectors.pageContent).isVisible(); + const content = this.page.locator(this.contactSelectors.pageContent).first(); + return await content.isVisible(); } } diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index e3fa7ee..b302c20 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -1,7 +1,21 @@ import { defineConfig, devices } from '@playwright/test'; -import dotenv from 'dotenv'; +import * as path from 'path'; +import * as fs from 'fs'; -dotenv.config({ path: `.env.${process.env.NODE_ENV || 'development'}` }); +// Load BASE_URL from root .env files +const envFile = process.env.NODE_ENV === 'production' + ? '.env.production' + : '.env.development'; +const envPath = path.resolve(__dirname, '..', envFile); + +if (fs.existsSync(envPath)) { + require('dotenv').config({ path: envPath }); +} + +// Default URLs based on environment +const defaultUrl = process.env.NODE_ENV === 'production' + ? 'http://localhost:9000' + : 'http://localhost:8000'; export default defineConfig({ testDir: './tests', @@ -16,7 +30,7 @@ export default defineConfig({ ['html', { outputFolder: '../playwright-report', open: 'never' }], ], use: { - baseURL: process.env.BASE_URL || 'http://localhost:8000', + baseURL: process.env.BASE_URL || defaultUrl, trace: 'on-first-retry', screenshot: 'only-on-failure', viewport: { width: 1280, height: 720 }, diff --git a/e2e/tests/accessibility.spec.ts b/e2e/tests/accessibility.spec.ts index 9c8006a..14a1c02 100644 --- a/e2e/tests/accessibility.spec.ts +++ b/e2e/tests/accessibility.spec.ts @@ -3,7 +3,8 @@ import { test, expect } from '@playwright/test'; test.describe('Accessibility', () => { test('should have alt text on logos', async ({ page }) => { await page.goto('/'); - const headerLogo = page.locator('header .logo'); + await page.locator('header.header').waitFor({ state: 'visible' }); + const headerLogo = page.locator('header.header .logo'); await expect(headerLogo).toBeVisible(); const alt = await headerLogo.getAttribute('alt'); expect(alt).toBeTruthy(); @@ -11,15 +12,17 @@ test.describe('Accessibility', () => { test('should have semantic header and footer', async ({ page }) => { await page.goto('/'); - await expect(page.locator('header')).toBeVisible(); + await expect(page.locator('header.header')).toBeVisible(); await expect(page.locator('footer')).toBeVisible(); }); test('should have h1 heading on pages', async ({ page }) => { await page.goto('/'); + await page.locator('header.header').waitFor({ state: 'visible' }); await expect(page.locator('h1').first()).toBeVisible(); await page.goto('/about-us'); + await page.locator('header.header').waitFor({ state: 'visible' }); await expect(page.locator('h1').first()).toBeVisible(); }); }); diff --git a/package-lock.json b/package-lock.json index 3fee068..7063f14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "typescript": "^4.6.4" }, "devDependencies": { + "@playwright/test": "^1.40.0", "@types/node": "^17.0.35", "@types/react": "^18.2.23", "@types/react-dom": "^18.2.4", @@ -3991,6 +3992,22 @@ "@parcel/core": "^2.8.3" } }, + "node_modules/@playwright/test": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", + "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.11", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.11.tgz", @@ -14355,6 +14372,53 @@ "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==" }, + "node_modules/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/please-upgrade-node": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", @@ -15092,9 +15156,9 @@ "integrity": "sha512-QFADYnsVoBMw1srW7OVKEYjG+MbIa49s54w1MA1EDY6r2r/sTcKKYqRX1f4GYvnXP7eN/Pe9HFcX+hwzmrXRHA==" }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" diff --git a/package.json b/package.json index 5d39622..a7d05b6 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,13 @@ "format": "prettier --write \"**/*.{js,css,html,jsx,ts,tsx,json,md}\"", "start": "npm run develop", "serve": "gatsby serve", - "clean": "gatsby clean" + "clean": "gatsby clean", + "test:e2e": "playwright test --config=e2e/playwright.config.ts", + "test:e2e:headed": "playwright test --config=e2e/playwright.config.ts --headed", + "test:e2e:debug": "playwright test --config=e2e/playwright.config.ts --debug", + "test:e2e:ui": "playwright test --config=e2e/playwright.config.ts --ui", + "test:e2e:report": "playwright show-report playwright-report", + "test:e2e:prod": "BASE_URL=http://localhost:9000 playwright test --config=e2e/playwright.config.ts" }, "repository": { "type": "git", @@ -55,12 +61,14 @@ "url": "https://github.com/contentstack/contentstack-gatsby-starter-app" }, "devDependencies": { + "@playwright/test": "^1.40.0", "@types/node": "^17.0.35", "@types/react": "^18.2.23", "@types/react-dom": "^18.2.4", "@types/react-helmet": "^6.1.7" }, "overrides": { + "axe-core": "^4.10.0", "axios": "^1.12.0", "@babel/helpers": "^7.26.10", "@babel/runtime": "^7.26.10", @@ -86,4 +94,4 @@ "tmp": "^0.2.4", "ws": "^8.17.1" } -} +} \ No newline at end of file From c57bf68d77c14658222d8d53962f0fb42fad1972 Mon Sep 17 00:00:00 2001 From: Abhishek Ezhava Date: Fri, 9 Jan 2026 18:42:39 +0530 Subject: [PATCH 3/3] fix: PR comments --- .gitignore | 2 + README.md | 36 ++++++++++++- e2e/README.md | 51 ------------------- e2e/tsconfig.json | 15 ------ package.json | 11 ++-- ...aywright.config.ts => playwright.config.ts | 10 ++-- test-results/.last-run.json | 4 -- tsconfig.json | 2 +- 8 files changed, 47 insertions(+), 84 deletions(-) delete mode 100644 e2e/README.md delete mode 100644 e2e/tsconfig.json rename e2e/playwright.config.ts => playwright.config.ts (78%) delete mode 100644 test-results/.last-run.json diff --git a/.gitignore b/.gitignore index 654d828..a87c9cd 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,9 @@ yarn-error.log .env.development .env.production +# Playwright playwright-report/ +test-results/ .vercel diff --git a/README.md b/README.md index 0144cc8..ef693d8 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,42 @@ We have created an in-depth tutorial on how you can create a Gatsby starter webs [Build a Starter Website with Gatsby and Contentstack](https://www.contentstack.com/docs/developers/sample-apps/build-a-starter-website-with-gatsby-and-contentstack/) +## E2E Tests + +End-to-end tests using [Playwright](https://playwright.dev/). + +### Setup + +```bash +npm install +npx playwright install chromium +``` + +### Run Tests + +```bash +# Development (port 8000) +npm run develop # Terminal 1 +npm run test:e2e # Terminal 2 + +# Production build (port 9000) +npm run build && npm run serve # Terminal 1 +npm run test:e2e:prod # Terminal 2 +``` + +### Commands + +| Command | Description | +|---------|-------------| +| `npm run test:e2e` | Run tests against dev server (port 8000) | +| `npm run test:e2e:prod` | Run tests against production build (port 9000) | +| `npm run test:e2e:headed` | Run with visible browser | +| `npm run test:e2e:debug` | Debug mode | +| `npm run test:e2e:ui` | Interactive UI | +| `npm run test:e2e:report` | View HTML report | + **More Resources** - [Contentstack documentation](https://www.contentstack.com/docs/) - [Region support documentation](https://www.contentstack.com/docs/developers/selecting-region-in-contentstack-starter-apps) -- [Gatsby documentation](https://www.gatsbyjs.com/docs/) \ No newline at end of file +- [Gatsby documentation](https://www.gatsbyjs.com/docs/) diff --git a/e2e/README.md b/e2e/README.md deleted file mode 100644 index 780cfd3..0000000 --- a/e2e/README.md +++ /dev/null @@ -1,51 +0,0 @@ -# E2E Test Suite - -End-to-end tests for Contentstack Gatsby Starter App using [Playwright](https://playwright.dev/). - -## Setup - -```bash -npm install -npx playwright install chromium -``` - -## Run Tests - -```bash -# Development (port 8000) -npm run develop # Terminal 1 -npm run test:e2e # Terminal 2 - -# Production build (port 9000) -npm run build && npm run serve # Terminal 1 -npm run test:e2e:prod # Terminal 2 -``` - -## Commands - -| Command | Description | -|---------|-------------| -| `npm run test:e2e` | Run tests against dev server (port 8000) | -| `npm run test:e2e:prod` | Run tests against production build (port 9000) | -| `npm run test:e2e:headed` | Run with visible browser | -| `npm run test:e2e:debug` | Debug mode | -| `npm run test:e2e:ui` | Interactive UI | -| `npm run test:e2e:report` | View HTML report | - -## Custom URL (Optional) - -To use a custom URL, set `BASE_URL` in your `.env.development` or `.env.production`: - -```bash -BASE_URL=http://localhost:3000 -``` - -## Structure - -``` -e2e/ -├── pages/ # Page Object Models -├── tests/ # Test specs -├── fixtures/ # Playwright fixtures -└── utils/ # Helpers -``` diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json deleted file mode 100644 index 1dc7526..0000000 --- a/e2e/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "module": "commonjs", - "moduleResolution": "node", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "outDir": "./dist", - "rootDir": "./" - }, - "include": ["**/*.ts"], - "exclude": ["node_modules", "dist"] -} diff --git a/package.json b/package.json index a7d05b6..4788f3a 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ "gatsby-plugin-env-variables": "^2.3.0", "gatsby-plugin-image": "^3.12.0", "gatsby-plugin-manifest": "^5.12.0", - "gatsby-plugin-robots-txt": "^1.8.0", "gatsby-plugin-sharp": "^5.12.0", "gatsby-plugin-sitemap": "^6.12.0", @@ -46,12 +45,12 @@ "start": "npm run develop", "serve": "gatsby serve", "clean": "gatsby clean", - "test:e2e": "playwright test --config=e2e/playwright.config.ts", - "test:e2e:headed": "playwright test --config=e2e/playwright.config.ts --headed", - "test:e2e:debug": "playwright test --config=e2e/playwright.config.ts --debug", - "test:e2e:ui": "playwright test --config=e2e/playwright.config.ts --ui", + "test:e2e": "playwright test", + "test:e2e:headed": "playwright test --headed", + "test:e2e:debug": "playwright test --debug", + "test:e2e:ui": "playwright test --ui", "test:e2e:report": "playwright show-report playwright-report", - "test:e2e:prod": "BASE_URL=http://localhost:9000 playwright test --config=e2e/playwright.config.ts" + "test:e2e:prod": "BASE_URL=http://localhost:9000 playwright test" }, "repository": { "type": "git", diff --git a/e2e/playwright.config.ts b/playwright.config.ts similarity index 78% rename from e2e/playwright.config.ts rename to playwright.config.ts index b302c20..f499cc8 100644 --- a/e2e/playwright.config.ts +++ b/playwright.config.ts @@ -2,23 +2,21 @@ import { defineConfig, devices } from '@playwright/test'; import * as path from 'path'; import * as fs from 'fs'; -// Load BASE_URL from root .env files const envFile = process.env.NODE_ENV === 'production' ? '.env.production' : '.env.development'; -const envPath = path.resolve(__dirname, '..', envFile); +const envPath = path.resolve(__dirname, envFile); if (fs.existsSync(envPath)) { require('dotenv').config({ path: envPath }); } -// Default URLs based on environment const defaultUrl = process.env.NODE_ENV === 'production' ? 'http://localhost:9000' : 'http://localhost:8000'; export default defineConfig({ - testDir: './tests', + testDir: './e2e/tests', timeout: 30 * 1000, expect: { timeout: 5000 }, fullyParallel: true, @@ -27,7 +25,7 @@ export default defineConfig({ workers: 1, reporter: [ ['list'], - ['html', { outputFolder: '../playwright-report', open: 'never' }], + ['html', { outputFolder: 'playwright-report', open: 'never' }], ], use: { baseURL: process.env.BASE_URL || defaultUrl, @@ -42,5 +40,5 @@ export default defineConfig({ use: { ...devices['Desktop Chrome'] }, }, ], - outputDir: '../test-results', + outputDir: 'test-results', }); diff --git a/test-results/.last-run.json b/test-results/.last-run.json deleted file mode 100644 index cbcc1fb..0000000 --- a/test-results/.last-run.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "status": "passed", - "failedTests": [] -} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 1d693b2..a4cb94e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,5 +16,5 @@ "noEmit": true, "jsx": "react" }, - "include": ["src"] + "include": ["src", "e2e", "playwright.config.ts"] }