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
}
});
}