diff --git a/playwright.config.ts b/playwright.config.ts index a7bd8af4..f26a4911 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -64,7 +64,7 @@ export default defineConfig({ ], use: { - baseURL: rc.baseUrl || undefined, + baseURL: rc.baseUrl || localWebServerUrl, headless: rc.headed ? false : true, trace: rc.trace ?? 'on-first-retry', screenshot: rc.screenshot ?? 'only-on-failure', diff --git a/tests/data/navbar.data.ts b/tests/data/navbar.data.ts new file mode 100644 index 00000000..aacb7c77 --- /dev/null +++ b/tests/data/navbar.data.ts @@ -0,0 +1 @@ +export const GITHUB_REPO_URL = 'https://github.com/MyNameIsEdi/open-qa'; diff --git a/tests/fixtures/index.ts b/tests/fixtures/index.ts new file mode 100644 index 00000000..157bae4c --- /dev/null +++ b/tests/fixtures/index.ts @@ -0,0 +1,18 @@ +import { test as base } from '@playwright/test'; +import { HomePage } from '../pages/home.page'; +import { NavbarPage } from '../pages/navbar.page'; + +export const test = base.extend<{ + homePage: HomePage; + navbar: NavbarPage; +}>({ + homePage: async ({ page }, use) => { + await page.goto('/'); + await use(new HomePage(page)); + }, + navbar: async ({ page }, use) => { + await use(new NavbarPage(page)); + }, +}); + +export { expect } from '@playwright/test'; diff --git a/tests/home.spec.ts b/tests/home.spec.ts new file mode 100644 index 00000000..fac83773 --- /dev/null +++ b/tests/home.spec.ts @@ -0,0 +1,73 @@ +import { test, expect } from './fixtures'; + +test.describe('Home Page', () => { + test.describe('Hero section', () => { + test('should display the title', async ({ homePage }) => { + await test.step('hero title is visible', async () => { + await expect(homePage.heroHeading).toBeVisible(); + }); + }); + test('should display the sub title', async ({ homePage }) => { + await test.step('hero subtitle is visible', async () => { + await expect(homePage.subTitle1).toBeVisible(); + }); + }); + test('should display the sub title 2', async ({ homePage }) => { + await test.step('hero second subtitle is visible', async () => { + await expect(homePage.subTitle2).toBeVisible(); + }); + }); + }); + test.describe('Features Cards', () => { + test('should display Self-Healing Tests Cards', async ({ homePage }) => { + await test.step('should display Self-Healing Tests Cards', async () => { + await expect(homePage.selfHealingTestsCard).toBeVisible(); + }); + await test.step('should display Self-Healing Tests Cards Title, description and icon', async () => { + await expect.soft(homePage.selfHealingTestsCardTitle).toBeVisible(); + await expect.soft(homePage.selfHealingTestsCardDescription).toBeVisible(); + await expect.soft(homePage.selfHealingTestCardIcon).toBeVisible(); + }); + }); + test('should display Smart Data Gen Card', async ({ homePage }) => { + await test.step('should display Smart Data Gen Card', async () => { + await expect(homePage.smartDataGenCard).toBeVisible(); + }); + await test.step('should display Smart Data Gen Card Title, description and icon', async () => { + await expect.soft(homePage.smartDataGenCardTitle).toBeVisible(); + await expect.soft(homePage.smartDataGenCardDescription).toBeVisible(); + await expect.soft(homePage.smartDataGenCardIcon).toBeVisible(); + }); + }); + test('should display Auto Bug Triage Card', async ({ homePage }) => { + await test.step('should display Auto Bug Triage Card', async () => { + await expect(homePage.autoBugTriageCard).toBeVisible(); + }); + await test.step('should display Auto Bug Triage Card Title, description and icon', async () => { + await expect.soft(homePage.autoBugTriageCardTitle).toBeVisible(); + await expect.soft(homePage.autoBugTriageCardDescription).toBeVisible(); + await expect.soft(homePage.autoBugTriageCardIcon).toBeVisible(); + }); + }); + test('should display Claude Powered Card', async ({ homePage }) => { + await test.step('should display Claude Powered Card', async () => { + await expect(homePage.claudePoweredCard).toBeVisible(); + }); + await test.step('should display Claude Powered Card Title, description and icon', async () => { + await expect.soft(homePage.claudePoweredCardTitle).toBeVisible(); + await expect.soft(homePage.claudePoweredCardDescription).toBeVisible(); + await expect.soft(homePage.claudePoweredCardIcon).toBeVisible(); + }); + }); + test('should display Daily Missions Card', async ({ homePage }) => { + await test.step('should display Daily Missions Card', async () => { + await expect(homePage.dailyMissionsCard).toBeVisible(); + }); + await test.step('should display Daily Missions Card Title, description and icon', async () => { + await expect.soft(homePage.dailyMissionsCardTitle).toBeVisible(); + await expect.soft(homePage.dailyMissionsCardDescription).toBeVisible(); + await expect.soft(homePage.dailyMissionsCardIcon).toBeVisible(); + }); + }); + }); +}); diff --git a/tests/navbar.spec.ts b/tests/navbar.spec.ts new file mode 100644 index 00000000..a5072787 --- /dev/null +++ b/tests/navbar.spec.ts @@ -0,0 +1,45 @@ +import { type Page } from '@playwright/test'; +import { test, expect } from './fixtures'; +import { GITHUB_REPO_URL } from './data/navbar.data'; + +test.describe('Test Navbar from Home Page', () => { + test('should display github link', async ({ homePage: _homePage, navbar }) => { + await test.step('github link is visible', async () => { + await expect(navbar.githubLink).toBeVisible(); + }); + await test.step('github link attributes href, target and rel', async () => { + await expect.soft(navbar.githubLink).toHaveAttribute('href', GITHUB_REPO_URL); + await expect.soft(navbar.githubLink).toHaveAttribute('target', '_blank'); + await expect.soft(navbar.githubLink).toHaveAttribute('rel', /noopener/); + }); + }); + test('should navigate to github repository', async ({ homePage: _homePage, navbar }) => { + let newTabPromise!: Promise; + + await test.step('wait for the new tab', async () => { + newTabPromise = navbar.page.context().waitForEvent('page'); + }); + await test.step('click the GitHub link', async () => { + await navbar.clickGithubLink(); + }); + const githubPage = await test.step('GitHub tab opens the repository', async () => { + const openedPage = await newTabPromise; + await expect(openedPage).toHaveURL(/github\.com\/MyNameIsEdi\/open-qa/); + return openedPage; + }); + await test.step('close the GitHub tab', async () => { + await githubPage.close(); + }); + }); + test('GitHub link is hidden on mobile', async ({ homePage, navbar }) => { + await test.step('set a mobile viewport', async () => { + await homePage.page.setViewportSize({ width: 375, height: 667 }); + }); + await test.step('reload Home at the mobile viewport', async () => { + await homePage.goto(); + }); + await test.step('GitHub link is hidden', async () => { + await expect(navbar.githubLink).toBeHidden(); + }); + }); +}); diff --git a/tests/pages/base.page.ts b/tests/pages/base.page.ts new file mode 100644 index 00000000..2ee8f47b --- /dev/null +++ b/tests/pages/base.page.ts @@ -0,0 +1,15 @@ +import { type Page } from '@playwright/test'; + +export class BasePage { + readonly page: Page; + readonly url: string; + + constructor(page: Page, url: string) { + this.page = page; + this.url = url; + } + + async goto() { + await this.page.goto(this.url); + } +} diff --git a/tests/pages/home.page.ts b/tests/pages/home.page.ts new file mode 100644 index 00000000..0f6708af --- /dev/null +++ b/tests/pages/home.page.ts @@ -0,0 +1,106 @@ +import { BasePage } from './base.page'; +import { type Locator, type Page } from '@playwright/test'; + +export class HomePage extends BasePage { + // Hero Section + readonly heroHeading: Locator; + readonly subTitle1: Locator; + readonly subTitle2: Locator; + + // Features Cards + readonly selfHealingTestsCard: Locator; + readonly selfHealingTestsCardTitle: Locator; + readonly selfHealingTestsCardDescription: Locator; + readonly selfHealingTestCardIcon: Locator; + readonly smartDataGenCard: Locator; + readonly smartDataGenCardTitle: Locator; + readonly smartDataGenCardDescription: Locator; + readonly smartDataGenCardIcon: Locator; + readonly autoBugTriageCard: Locator; + readonly autoBugTriageCardTitle: Locator; + readonly autoBugTriageCardDescription: Locator; + readonly autoBugTriageCardIcon: Locator; + readonly claudePoweredCard: Locator; + readonly claudePoweredCardTitle: Locator; + readonly claudePoweredCardDescription: Locator; + readonly claudePoweredCardIcon: Locator; + readonly dailyMissionsCard: Locator; + readonly dailyMissionsCardTitle: Locator; + readonly dailyMissionsCardDescription: Locator; + readonly dailyMissionsCardIcon: Locator; + + constructor(page: Page) { + super(page, '/'); + + // Hero Section + this.heroHeading = page.getByRole('heading', { name: 'OPEN-QA', exact: true }); + this.subTitle1 = page.getByText('The QA Toolkit AI Is Missing', { exact: true }); + this.subTitle2 = page.getByText('Ready to install, verified, and open source', { exact: true }); + + // Features Cards + this.selfHealingTestsCard = page.getByTestId('home.feature_healing_title'); + this.selfHealingTestsCardTitle = this.selfHealingTestsCard.getByRole('heading', { + name: 'Self-Healing Tests', + exact: true, + }); + this.selfHealingTestsCardDescription = this.selfHealingTestsCard.getByText( + 'AI suggests new locators when your UI changes — no more brittle selectors.', + { exact: true }, + ); + this.selfHealingTestCardIcon = this.selfHealingTestsCard.getByTestId( + 'home.feature_healing_title_icon', + ); + + this.smartDataGenCard = page.getByTestId('home.feature_datagen_title'); + this.smartDataGenCardTitle = this.smartDataGenCard.getByRole('heading', { + name: 'Smart Data Gen', + exact: true, + }); + this.smartDataGenCardDescription = this.smartDataGenCard.getByText( + 'Generates SQLi, XSS, nulls, RTL text and boundary payloads automatically.', + { exact: true }, + ); + this.smartDataGenCardIcon = this.smartDataGenCard.getByTestId( + 'home.feature_datagen_title_icon', + ); + + this.autoBugTriageCard = page.getByTestId('home.feature_triage_title'); + this.autoBugTriageCardTitle = this.autoBugTriageCard.getByRole('heading', { + name: 'Auto Bug Triage', + exact: true, + }); + this.autoBugTriageCardDescription = this.autoBugTriageCard.getByText( + 'Reads error logs and writes Jira-ready bug reports with root-cause analysis.', + { exact: true }, + ); + this.autoBugTriageCardIcon = this.autoBugTriageCard.getByTestId( + 'home.feature_triage_title_icon', + ); + + this.claudePoweredCard = page.getByTestId('home.feature_claude_title'); + this.claudePoweredCardTitle = this.claudePoweredCard.getByRole('heading', { + name: 'Claude-Powered', + exact: true, + }); + this.claudePoweredCardDescription = this.claudePoweredCard.getByText( + 'Every agent and skill is powered by Anthropic Claude. MOCK mode requires no API key.', + { exact: true }, + ); + this.claudePoweredCardIcon = this.claudePoweredCard.getByTestId( + 'home.feature_claude_title_icon', + ); + + this.dailyMissionsCard = page.getByTestId('home.feature_missions_title'); + this.dailyMissionsCardTitle = this.dailyMissionsCard.getByRole('heading', { + name: 'Daily Missions', + exact: true, + }); + this.dailyMissionsCardDescription = this.dailyMissionsCard.getByText( + 'Gamified QA challenges that change every day. Earn XP, build streaks, sharpen your skills.', + { exact: true }, + ); + this.dailyMissionsCardIcon = this.dailyMissionsCard.getByTestId( + 'home.feature_missions_title_icon', + ); + } +} diff --git a/tests/pages/navbar.page.ts b/tests/pages/navbar.page.ts new file mode 100644 index 00000000..06b93155 --- /dev/null +++ b/tests/pages/navbar.page.ts @@ -0,0 +1,15 @@ +import { BasePage } from './base.page'; +import { type Locator, type Page } from '@playwright/test'; + +export class NavbarPage extends BasePage { + readonly githubLink: Locator; + + constructor(page: Page) { + super(page, '/'); + this.githubLink = page.getByRole('link', { name: 'GitHub' }); + } + + async clickGithubLink() { + await this.githubLink.click(); + } +} diff --git a/ui/src/pages/HomePage.tsx b/ui/src/pages/HomePage.tsx index 9c9effdf..b19d11c2 100644 --- a/ui/src/pages/HomePage.tsx +++ b/ui/src/pages/HomePage.tsx @@ -254,10 +254,13 @@ export default function HomePage() { {featureKeys.map(({ Icon, titleKey, descKey, color }) => (
- +

{t(titleKey)}