diff --git a/frontend/components/header/HeaderMobile.vue b/frontend/components/header/HeaderMobile.vue index b0d37a95..c91bc55e 100644 --- a/frontend/components/header/HeaderMobile.vue +++ b/frontend/components/header/HeaderMobile.vue @@ -35,13 +35,13 @@ :location="DropdownLocation.SIDE_MENU" /> diff --git a/frontend/components/header/HeaderWebsite.vue b/frontend/components/header/HeaderWebsite.vue index b8e6723d..993a9114 100644 --- a/frontend/components/header/HeaderWebsite.vue +++ b/frontend/components/header/HeaderWebsite.vue @@ -35,16 +35,19 @@ /> { return this.searchBar.isSearchInputVisible(); } + + async isSignInButtonVisible(): Promise { + return this.signInButton.isVisible(); + } + + async clickSignInButton(): Promise { + await this.signInButton.click(); + } + + async isSignUpButtonVisible(): Promise { + return this.signUpButton.isVisible(); + } + + async clickSignUpButton(): Promise { + await this.signUpButton.click(); + } } diff --git a/frontend/tests/component-objects/Navigation.ts b/frontend/tests/component-objects/Navigation.ts index 2026db38..774de704 100644 --- a/frontend/tests/component-objects/Navigation.ts +++ b/frontend/tests/component-objects/Navigation.ts @@ -12,10 +12,12 @@ const locators = { CREATE_EVENT: "#create-event", CREATE_ORGANIZATION: "#create-organization", INFO: "#info button", + INFO_DROPDOWN: "#info ul", HELP: "#help", DOCS: "#docs", LEGAL: "#legal", USER_OPTIONS: "#user-options button", + USER_OPTIONS_DROPDOWN: "#user-options ul", SIGN_IN: "#sign-in", SIGN_UP: "#sign-up", } @@ -56,6 +58,10 @@ export class Navigation extends PageObjectBase { return this.getLocator("INFO"); } + get infoDropdown(): Locator { + return this.getLocator("INFO_DROPDOWN"); + } + get help(): Locator { return this.getLocator("HELP"); } @@ -72,6 +78,10 @@ export class Navigation extends PageObjectBase { return this.getLocator("USER_OPTIONS"); } + get userOptionsDropdown(): Locator { + return this.getLocator("USER_OPTIONS_DROPDOWN"); + } + get signIn(): Locator { return this.getLocator("SIGN_IN"); } @@ -79,4 +89,18 @@ export class Navigation extends PageObjectBase { get signUp(): Locator { return this.getLocator("SIGN_UP"); } + + async openInfo(): Promise { + if (!(await this.infoDropdown.isVisible())) { + await this.info.click(); + await this.infoDropdown.isVisible(); + } + } + + async openUserOptions(): Promise { + if (!(await this.userOptionsDropdown.isVisible())) { + await this.userOptions.click(); + await this.userOptionsDropdown.isVisible(); + } + } } diff --git a/frontend/tests/fixtures/test-fixtures.ts b/frontend/tests/fixtures/test-fixtures.ts index d101d739..7481a01a 100644 --- a/frontend/tests/fixtures/test-fixtures.ts +++ b/frontend/tests/fixtures/test-fixtures.ts @@ -30,8 +30,10 @@ export const test = baseTest.extend({ await signInPage.signInButton.waitFor({ state: "visible" }); await use(signInPage); }, - isAccessibilityTest: [async ({}, use, testInfo) => { + isAccessibilityTest: [async ({ page }, use, testInfo) => { testInfo.annotations.push({ type: 'accessibility' }); + const originalScreenshot = page.screenshot.bind(page); + page.screenshot = async (options?: any) => Buffer.from(''); await use(); }, { auto: true }] }); diff --git a/frontend/tests/page-objects/LandingPage.ts b/frontend/tests/page-objects/LandingPage.ts index 9c276c3f..c268c417 100644 --- a/frontend/tests/page-objects/LandingPage.ts +++ b/frontend/tests/page-objects/LandingPage.ts @@ -8,7 +8,8 @@ import { Navigation } from "../component-objects/Navigation"; const locators = { LANDING_SPLASH: "#landing-splash-header", REQUEST_ACCESS_LINK: "#request-access", - GET_ACTIVE_BUTTON: "#btn-get-active", + VIEW_ORGANIZATIONS_BUTTON: "#view-organizations", + VIEW_EVENTS_BUTTON: "#view-events", GET_ORGANIZED_BUTTON: "#btn-get-organized", GROW_ORGANIZATION_BUTTON: "#btn-grow-organization", ABOUT_BUTTON: "#btn-activist", @@ -38,8 +39,12 @@ export class LandingPage extends PageObjectBase { return this.getLocator("REQUEST_ACCESS_LINK"); } - get getActiveButton(): Locator { - return this.getLocator("GET_ACTIVE_BUTTON"); + get viewOrganizationsButton(): Locator { + return this.getLocator("VIEW_ORGANIZATIONS_BUTTON"); + } + + get viewEventsButton(): Locator { + return this.getLocator("VIEW_EVENTS_BUTTON"); } get getOrganizedButton(): Locator { @@ -65,8 +70,8 @@ export class LandingPage extends PageObjectBase { async getImportantLinks(): Promise { return [ this.landingSplash, - this.requestAccessLink, - this.getActiveButton, + this.viewOrganizationsButton, + this.viewEventsButton, this.getOrganizedButton, this.growOrganizationButton, this.aboutButton, @@ -136,4 +141,65 @@ export class LandingPage extends PageObjectBase { return visibleOptions; } + + async isViewOrganizationsButtonVisible(): Promise { + return this.viewOrganizationsButton.isVisible(); + } + + async isViewEventsButtonVisible(): Promise { + return this.viewEventsButton.isVisible(); + } + + async navigateToViewOrganizations(): Promise { + await this.viewOrganizationsButton.click(); + await this.waitForUrlChange("**/organizations"); + } + + async navigateToViewEvents(): Promise { + await this.viewEventsButton.click(); + await this.waitForUrlChange("**/events"); + } + + async isSignInButtonVisible(): Promise { + if (await this.isMobile()) { + await this.navigation.mobileNav.openDrawer(); + await this.navigation.openUserOptions(); + return await this.navigation.signIn.isVisible(); + } else { + return await this.header.isSignInButtonVisible(); + } + } + + async navigateToSignIn(): Promise { + if (await this.isMobile()) { + await this.navigation.mobileNav.openDrawer(); + await this.navigation.openUserOptions(); + await this.navigation.signIn.isVisible(); + await this.navigation.signIn.click(); + } else { + await this.header.clickSignInButton(); + } + await this.waitForUrlChange("**/auth/sign-in"); + } + + async isSignUpButtonVisible(): Promise { + if (await this.isMobile()) { + await this.navigation.mobileNav.openDrawer(); + await this.navigation.openUserOptions(); + return await this.navigation.signUp.isVisible(); + } else { + return await this.header.isSignUpButtonVisible(); + } + } + + async navigateToSignUp(): Promise { + if (await this.isMobile()) { + await this.navigation.mobileNav.openDrawer(); + await this.navigation.openUserOptions(); + await this.navigation.signUp.click(); + } else { + await this.header.clickSignUpButton(); + } + await this.waitForUrlChange("**/auth/sign-up"); + } } diff --git a/frontend/tests/page-objects/SignInPage.ts b/frontend/tests/page-objects/SignInPage.ts index 115bec95..8b7c9da2 100644 --- a/frontend/tests/page-objects/SignInPage.ts +++ b/frontend/tests/page-objects/SignInPage.ts @@ -20,7 +20,7 @@ export class SignInPage extends PageObjectBase { readonly header: HeaderWebsite; constructor(page: Page) { - super(page, locators, "Sign In", "/auth/sign-in"); + super(page, locators, "Sign In Page", "/auth/sign-in"); this.header = new HeaderWebsite(page); } diff --git a/frontend/tests/specs/home-page.spec.ts b/frontend/tests/specs/home-page.spec.ts index 7d765d21..89134e6a 100644 --- a/frontend/tests/specs/home-page.spec.ts +++ b/frontend/tests/specs/home-page.spec.ts @@ -1,25 +1,21 @@ -import AxeBuilder from "@axe-core/playwright"; import { HomePage, expect, test } from "../fixtures/test-fixtures"; +import { runAccessibilityTest } from "../utils/accessibilityTesting"; test.describe("Home Page", () => { // MARK: Accessibility // Test accessibility of the home page (skip this test for now). // Note: Check to make sure that this is eventually done for light and dark modes. - test("There are no detectable accessibility issues", async ({ + test("Home Page has no detectable accessibility issues", async ({ homePage, isAccessibilityTest }, testInfo) => { - const results = await new AxeBuilder({ page: homePage.getPage() }) - .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"]) - .analyze(); + const violations = await runAccessibilityTest(homePage, testInfo); + expect.soft(violations, 'Accessibility violations found:').toHaveLength(0); - await testInfo.attach("accessibility-scan-results", { - body: JSON.stringify(results, null, 2), - contentType: "application/json", - }); - - expect(results.violations).toEqual([]); + if (violations.length > 0) { + console.log('Accessibility violations:', JSON.stringify(violations, null, 2)); + } }); test("The topics dropdown should be functional", async ({ homePage }) => { diff --git a/frontend/tests/specs/landing-page.spec.ts b/frontend/tests/specs/landing-page.spec.ts index 856e96b6..b299cb19 100644 --- a/frontend/tests/specs/landing-page.spec.ts +++ b/frontend/tests/specs/landing-page.spec.ts @@ -1,24 +1,20 @@ -import AxeBuilder from "@axe-core/playwright"; import { LandingPage, expect, test } from "../fixtures/test-fixtures"; +import { runAccessibilityTest } from "../utils/accessibilityTesting"; test.describe("Landing Page", () => { // MARK: Accessibility // Note: Check to make sure that this is eventually done for light and dark modes. - test("There are no detectable accessibility issues", async ({ + test("Landing Page has no detectable accessibility issues", async ({ landingPage, - isAccessibilityTest, + isAccessibilityTest }, testInfo) => { - const results = await new AxeBuilder({ page: landingPage.getPage() }) - .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"]) - .analyze(); + const violations = await runAccessibilityTest(landingPage, testInfo); + expect.soft(violations, 'Accessibility violations found:').toHaveLength(0); - await testInfo.attach("accessibility-scan-results", { - body: JSON.stringify(results, null, 2), - contentType: "application/json", - }); - - expect(results.violations).toEqual([]); + if (violations.length > 0) { + console.log('Accessibility violations:', JSON.stringify(violations, null, 2)); + } }); // MARK: Header @@ -39,10 +35,22 @@ test.describe("Landing Page", () => { expect(result).toBe(true); }); - // Test that the Get In Touch button is visible and clickable only on Desktop Header. - test("Get In Touch button is functional", async ({ landingPage }) => { - const result = await landingPage.checkGetInTouchButtonFunctionality(); - expect(result).toBe(true); + // Test that the Sign In link can be accessed from the Home Page. + test("Sign In link should be accessible from the Home Page", async ({ landingPage }) => { + const isSignInVisible = await landingPage.isSignInButtonVisible(); + expect(isSignInVisible).toBe(true); + + await landingPage.navigateToSignIn(); + expect(landingPage.url()).toContain("/auth/sign-in"); + }); + + // Test that the Sign Up link can be accessed from the Home Page. + test("Sign Up link should be accessible from the Home Page", async ({ landingPage }) => { + const isSignUpVisible = await landingPage.isSignUpButtonVisible(); + expect(isSignUpVisible).toBe(true); + + await landingPage.navigateToSignUp(); + expect(landingPage.url()).toContain("/auth/sign-up"); }); // Test that the theme dropdown is visible and functional. @@ -74,7 +82,7 @@ test.describe("Landing Page", () => { }); // Test that the landing page contains the request access link. - test("Splash should contain the request access link", async ({ + test.skip("Splash should contain the request access link", async ({ landingPage, }) => { const requestAccessLink = landingPage.requestAccessLink; @@ -83,6 +91,28 @@ test.describe("Landing Page", () => { ); }); + // Test that the view organizations button is visible and navigates to the organizations page. + test("View organizations button should be visible and functional", async ({ + landingPage, + }) => { + const isVisible = await landingPage.isViewOrganizationsButtonVisible(); + expect(isVisible).toBe(true); + + await landingPage.navigateToViewOrganizations(); + expect(landingPage.url()).toContain("/organizations"); + }); + + // Test that the view events button is visible and navigates to the events page. + test("View events button should be visible and functional", async ({ + landingPage, + }) => { + const isVisible = await landingPage.isViewEventsButtonVisible(); + expect(isVisible).toBe(true); + + await landingPage.navigateToViewEvents(); + expect(landingPage.url()).toContain("/events"); + }); + // Test that all important links are visible on the landing page. test("All important links should be visible on the landing page", async ({ landingPage, diff --git a/frontend/tests/specs/sign-in-page.spec.ts b/frontend/tests/specs/sign-in-page.spec.ts index 8a64b57d..bd84407a 100644 --- a/frontend/tests/specs/sign-in-page.spec.ts +++ b/frontend/tests/specs/sign-in-page.spec.ts @@ -1,19 +1,18 @@ import { SignInPage, expect, test } from "../fixtures/test-fixtures"; -import AxeBuilder from "@axe-core/playwright"; +import { runAccessibilityTest } from "../utils/accessibilityTesting"; test.describe("Sign In Page", () => { - test("should have no detectable accessibility issues", async ({ signInPage }, testInfo) => { - const results = await new AxeBuilder({ page: signInPage.getPage() }) - .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"]) - .analyze(); + test("Sign In Page has no detectable accessibility issues", async ({ + signInPage, + isAccessibilityTest + }, testInfo) => { + const violations = await runAccessibilityTest(signInPage, testInfo); + expect.soft(violations, 'Accessibility violations found:').toHaveLength(0); - await testInfo.attach("accessibility-scan-results", { - body: JSON.stringify(results, null, 2), - contentType: "application/json", - }); - - expect(results.violations).toEqual([]); + if (violations.length > 0) { + console.log('Accessibility violations:', JSON.stringify(violations, null, 2)); + } }); test("should display all necessary elements", async ({ signInPage }) => { diff --git a/frontend/tests/utils/PageObjectBase.ts b/frontend/tests/utils/PageObjectBase.ts index c78d8bec..d662c4d6 100644 --- a/frontend/tests/utils/PageObjectBase.ts +++ b/frontend/tests/utils/PageObjectBase.ts @@ -1,6 +1,8 @@ import type { Page, Locator } from "@playwright/test"; export class PageObjectBase { + [key: string]: any; + protected readonly page: Page; protected readonly pageName?: string; protected readonly pageURL?: string; @@ -9,33 +11,34 @@ export class PageObjectBase { constructor(page: Page, locators: Record, pageName?: string, pageURL?: string) { this.page = page; this.locators = locators; - this.forwardPageMethods(); this.pageName = pageName; this.pageURL = pageURL; + return new Proxy(this, { + get: (target: PageObjectBase, prop: string | symbol) => { + if (prop in target) { + return (target as any)[prop]; + } + return (target.page as any)[prop]; + }, + }) as PageObjectBase & Page; } - private forwardPageMethods() { - Object.getOwnPropertyNames(Object.getPrototypeOf(this.page)).forEach((method) => { - if (!(method in this) && typeof this.page[method as keyof Page] === 'function') { - (this as any)[method] = (...args: any[]) => (this.page as any)[method](...args); - } - }); + public async getPageName(): Promise { + return this.pageName ?? "Unknown Page"; } - // Forward all methods from Page to this.page - [key: string]: any; + public async isMobile(): Promise { + const viewportSize = this.page.viewportSize(); + const isMobileViewport = viewportSize !== null && viewportSize.width < 768; + const isMobileEmulation = await this.page.evaluate(() => 'ontouchstart' in window); + return isMobileViewport && isMobileEmulation; + } public async navigateTo(path: string): Promise { await this.page.goto(path); await this.page.waitForLoadState('networkidle'); } - // Common utility methods - public async isMobile(): Promise { - const viewportSize = this.page.viewportSize(); - return viewportSize !== null && viewportSize.width < 768; - } - public async waitForUrlChange( expectedUrlPattern: string | RegExp | ((url: URL) => boolean), options?: { timeout?: number } @@ -51,8 +54,6 @@ export class PageObjectBase { public getLocator(selector: keyof typeof this.locators): Locator { return this.page.locator(this.locators[selector]); } - - public getPage(): Page { - return this.page; - } } + +export interface PageObjectBase extends Page {} diff --git a/frontend/tests/utils/accessibilityTesting.ts b/frontend/tests/utils/accessibilityTesting.ts new file mode 100644 index 00000000..ec3e50f8 --- /dev/null +++ b/frontend/tests/utils/accessibilityTesting.ts @@ -0,0 +1,30 @@ +import type { Page, TestInfo } from "@playwright/test"; +import AxeBuilder from "@axe-core/playwright"; +import { PageObjectBase } from "./PageObjectBase"; + +export async function runAccessibilityTest(page: Page | PageObjectBase, testInfo: TestInfo) { + const results = await new AxeBuilder({ page }) + .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"]) + .analyze(); + + await testInfo.attach("accessibility-scan-results", { + body: JSON.stringify(results, null, 2), + contentType: "application/json", + }); + const pageName = 'getPageName' in page ? await page.getPageName() : 'Unknown Page'; + console.log(`Accessibility test completed for: ${pageName}`); + console.log(`Violations found: ${results.violations.length}`); + + const formattedViolations = results.violations.map(violation => ({ + id: violation.id, + impact: violation.impact, + description: violation.description, + help: violation.help, + nodes: violation.nodes.map(node => ({ + html: node.html, + failureSummary: node.failureSummary + })) + })); + + return formattedViolations; +} diff --git a/frontend/tests/utils/axe-reporter.ts b/frontend/tests/utils/axe-reporter.ts index 3b60da04..d34d66e4 100644 --- a/frontend/tests/utils/axe-reporter.ts +++ b/frontend/tests/utils/axe-reporter.ts @@ -1,15 +1,37 @@ -import type { Reporter, TestCase, TestResult } from '@playwright/test/reporter'; +import type { Reporter, TestCase, TestResult, Suite } from '@playwright/test/reporter'; import fs from 'fs'; import path from 'path'; import { createHtmlReport } from 'axe-html-reporter'; class AxeReporter implements Reporter { + private outputDir: string; + + constructor(options?: { outputDir?: string }) { + this.outputDir = options?.outputDir || 'test-results/accessibility-results'; + } + + onTestBegin(test: TestCase) { + console.log(`Starting test ${test.title}`); + } + onTestEnd(test: TestCase, result: TestResult) { + console.log(`Test ended: ${test.title}, Status: ${result.status}`); + const axeResults = result.attachments.find(a => a.name === 'accessibility-scan-results'); if (axeResults && axeResults.body) { const results = JSON.parse(axeResults.body.toString()); - const pageName = this.extractPageName(test); - this.generateAxeReport(test.title, results, pageName); + console.log(`Violations found: ${results.violations.length}`); + + if (results.violations.length > 0) { + console.log(`Generating report for: ${test.title}`); + const pageName = this.extractPageName(test); + const { browserName, deviceName } = this.extractProjectInfo(test); + this.generateAxeReport(results, pageName, browserName, deviceName); + } else { + console.log(`Skipping report generation for test with no violations: ${test.title}`); + } + } else { + console.log(`No accessibility results found for: ${test.title}`); } } @@ -24,22 +46,36 @@ class AxeReporter implements Reporter { return 'unknown_page'; } - generateAxeReport(testTitle: string, results: any, pageName: string) { - const reportDir = path.join('artifacts', 'axe-reports'); - if (!fs.existsSync(reportDir)) { - fs.mkdirSync(reportDir, { recursive: true }); + private extractProjectInfo(test: TestCase): { browserName: string; deviceName: string } { + let current: Suite | undefined = test.parent; + while (current) { + if (current.project) { + const project = current.project(); + if (project) { + const projectName = project.name; + const [browserName, ...deviceParts] = projectName.split(' '); + return { + browserName: browserName.replace(/\s+/g, '_'), + deviceName: deviceParts.length > 0 ? deviceParts.join('_') : 'desktop' + }; + } + } + current = current.parent; } + return { browserName: 'unknown_browser', deviceName: 'unknown_device' }; + } - const reportFileName = `${pageName}_${testTitle.replace(/\s+/g, '_')}_axe_report.html`; + generateAxeReport(results: any, pageName: string, browserName: string, deviceName: string) { + const reportFileName = `${pageName}_${browserName}_${deviceName}_axe_report.html`; createHtmlReport({ results, options: { projectKey: 'Activist', - customSummary: `Accessibility report for ${pageName} - ${testTitle}`, + customSummary: `Accessibility report for ${pageName} - ${browserName}`, doNotCreateReportFile: false, - outputDir: reportDir, - reportFileName: reportFileName + reportFileName: reportFileName, + outputDir: this.outputDir } }); }