Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions tests/data/navbar.data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const GITHUB_REPO_URL = 'https://github.com/MyNameIsEdi/open-qa';
18 changes: 18 additions & 0 deletions tests/fixtures/index.ts
Original file line number Diff line number Diff line change
@@ -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';
73 changes: 73 additions & 0 deletions tests/home.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
});
45 changes: 45 additions & 0 deletions tests/navbar.spec.ts
Original file line number Diff line number Diff line change
@@ -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<Page>;

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();
});
});
});
15 changes: 15 additions & 0 deletions tests/pages/base.page.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
106 changes: 106 additions & 0 deletions tests/pages/home.page.ts
Original file line number Diff line number Diff line change
@@ -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',
);
}
}
15 changes: 15 additions & 0 deletions tests/pages/navbar.page.ts
Original file line number Diff line number Diff line change
@@ -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();
}
}
5 changes: 4 additions & 1 deletion ui/src/pages/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -254,10 +254,13 @@ export default function HomePage() {
{featureKeys.map(({ Icon, titleKey, descKey, color }) => (
<div
key={titleKey}
data-testid={titleKey}
className="p-4 rounded-2xl border transition-all duration-200 hover:shadow-medium hover:-translate-y-0.5"
style={{ borderColor: 'var(--border)', backgroundColor: 'var(--bg-card)' }}
>
<Icon className={`${color} mb-2`} sx={{ fontSize: 26 }} />
<span data-testid={`${titleKey}_icon`} aria-hidden="true">
<Icon className={`${color} mb-2`} sx={{ fontSize: 26 }} />
</span>
<h3 className="font-semibold text-sm mb-1" style={{ color: 'var(--text-main)' }}>
{t(titleKey)}
</h3>
Expand Down