From 4ae681daa78fd1a287086c41f9bbb496a8f772c1 Mon Sep 17 00:00:00 2001 From: Amitha Maria Date: Tue, 14 Jul 2026 12:03:05 -0700 Subject: [PATCH 1/3] Add Credits page test automation - page object and spec file --- .../page_objects/credits.browser.js | 99 +++++++++ .../specs/CreditsPage.browser.js | 205 ++++++++++++++++++ 2 files changed, 304 insertions(+) create mode 100644 tests/browserstack_automation/page_objects/credits.browser.js create mode 100644 tests/browserstack_automation/specs/CreditsPage.browser.js diff --git a/tests/browserstack_automation/page_objects/credits.browser.js b/tests/browserstack_automation/page_objects/credits.browser.js new file mode 100644 index 000000000..f28789057 --- /dev/null +++ b/tests/browserstack_automation/page_objects/credits.browser.js @@ -0,0 +1,99 @@ +import { $, $$, driver } from '@wdio/globals'; +import PageBrowser from './page.browser'; + +// Page object for the WeVote Credits & Thanks page. +// URL: https://quality.wevote.us/more/credits + +class CreditsBrowser extends PageBrowser { + constructor () { + super(); + this.title = 'Credits & Thanks - WeVote'; + } + + // ─── Page heading ──────────────────────────────────────────────────────────── + + get pageHeading () { + return $("//h1[normalize-space()='Credits & Thanks']"); + } + + // ─── Intro section ─────────────────────────────────────────────────────────── + + get openSourceInlineLink () { + return $('a[href*="open-source"]'); + } + + // ─── Organization logos ────────────────────────────────────────────────────── + + get allImages () { + return $$('img'); + } + + get allLogoLinks () { + return $$('a img'); + } + + // ─── Volunteers section ────────────────────────────────────────────────────── + + get volunteersHeading () { + return $("//*[contains(text(), 'Volunteers') and contains(text(), 'Donors')]"); + } + + get volunteerRoleLink () { + return $('a[href*="apply"]'); + } + + get donateLink () { + return $('a[href*="donate"]'); + } + + // ─── Footer elements ───────────────────────────────────────────────────────── + + get footerGetStarted () { + return $('#footerLinkGetStarted'); + } + + get footerFacebook () { + return $('#footerLinkFacebook'); + } + + get footerTwitter () { + return $('#footerLinkTwitter'); + } + + get footerInstagram () { + return $('#footerLinkInstagram'); + } + + get footerOpenSource () { + return $('#footerLinkOpenSource'); + } + + get footerPrivacy () { + return $('#footerLinkPrivacy'); + } + + get footerTerms () { + return $('#footerLinkTermsOfUse'); + } + + get googleBadge () { + return $('#googleBadge'); + } + + get appleBadge () { + return $('#appleBadge'); + } + + // ─── Page load & helpers ───────────────────────────────────────────────────── + + async load () { + await super.open('/more/credits'); + } + + async scrollToFooter () { + await browser.execute(() => window.scrollTo(0, document.body.scrollHeight)); + await driver.pause(3000); + } +} + +export default new CreditsBrowser(); \ No newline at end of file diff --git a/tests/browserstack_automation/specs/CreditsPage.browser.js b/tests/browserstack_automation/specs/CreditsPage.browser.js new file mode 100644 index 000000000..d17994126 --- /dev/null +++ b/tests/browserstack_automation/specs/CreditsPage.browser.js @@ -0,0 +1,205 @@ +import { driver, expect, browser } from '@wdio/globals'; +import CreditsPage from '../page_objects/credits.browser'; + +const waitTime = 5000; + +/* eslint-disable no-undef */ +describe('Credits & Thanks Page', () => { + + // TC-001: Credits page loads successfully @BVT + it('verify Credits page loads and heading is visible @BVT', async () => { + console.log('TC: Credits_001 — page loads successfully'); + await CreditsPage.load(); + await driver.pause(waitTime); + + await expect(driver).toHaveUrl(expect.stringContaining('/more/credits')); + await expect(CreditsPage.pageHeading).toBeDisplayed(); + await expect(CreditsPage.pageHeading).toHaveText('Credits & Thanks'); + }); + + // TC-002: Browser tab title is correct @BVT + it('verify browser tab title contains Credits or WeVote @BVT', async () => { + console.log('TC: Credits_002 — page title is correct'); + await CreditsPage.load(); + await driver.pause(waitTime); + + const title = await browser.getTitle(); + const isValid = title.toLowerCase().includes('credits') || title.toLowerCase().includes('wevote'); + expect(isValid).toBe(true, `Unexpected page title: '${title}'`); + }); + + // TC-005: 'Open source software' inline link is clickable + it('verify open source software inline link is clickable and has valid href', async () => { + console.log('TC: Credits_005 — open source inline link'); + await CreditsPage.load(); + await driver.pause(waitTime); + + await CreditsPage.openSourceInlineLink.waitForClickable({ timeout: waitTime }); + const href = await CreditsPage.openSourceInlineLink.getAttribute('href'); + expect(href).toBeTruthy(); + expect(href.startsWith('http')).toBe(true, `Invalid href: '${href}'`); + }); + + // TC-006: All organization logos render without broken images @BVT + it('verify all organization logos render without broken images @BVT', async () => { + console.log('TC: Credits_006 — no broken org logo images'); + await CreditsPage.load(); + await driver.pause(waitTime); + + const images = await CreditsPage.allImages; + const broken = []; + for (const img of images) { + const naturalWidth = await browser.execute((el) => el.naturalWidth, img); + const src = await img.getAttribute('src') || ''; + if (naturalWidth === 0 && src) { + broken.push(src); + } + } + expect(broken.length).toBe(0, `Broken images found:\n${broken.join('\n')}`); + }); + + // TC-007: Key organizations are listed on the page @BVT + it('verify key organizations are present on the Credits page @BVT', async () => { + console.log('TC: Credits_007 — key org names present'); + await CreditsPage.load(); + await driver.pause(waitTime); + + const bodyText = await $('body').getText(); + const expectedOrgs = [ + 'Amazon Web Services', + 'Atlassian', + 'Ballotpedia', + 'BrowserStack', + 'Google', + ]; + const missing = expectedOrgs.filter((org) => !bodyText.includes(org)); + expect(missing.length).toBe(0, `Missing organizations: ${missing.join(', ')}`); + }); + + // TC-010: Org logo links have valid hrefs + it('verify organization logo links have valid hrefs', async () => { + console.log('TC: Credits_010 — org logo links navigate correctly'); + await CreditsPage.load(); + await driver.pause(waitTime); + + const logoLinks = await CreditsPage.allLogoLinks; + expect(logoLinks.length).toBeGreaterThan(0, 'No organization logo links found'); + + // Check first 5 to keep test fast + for (const link of logoLinks.slice(0, 5)) { + const href = await link.getAttribute('href') || ''; + expect(href.startsWith('http')).toBe(true, `Logo link has invalid href: '${href}'`); + } + }); + + // TC-011: Org logo links open in a new tab + it('verify organization logo links open in a new tab', async () => { + console.log('TC: Credits_011 — org logo links open in new tab'); + await CreditsPage.load(); + await driver.pause(waitTime); + + const logoLinks = await CreditsPage.allLogoLinks; + expect(logoLinks.length).toBeGreaterThan(0, 'No organization logo links found'); + + const target = await logoLinks[0].getAttribute('target'); + expect(target).toBe('_blank', `First logo link should open in new tab — target='${target}'`); + }); + + // TC-013: Volunteers, Interns & Donors section heading is present @BVT + it('verify Volunteers Interns and Donors section heading is visible @BVT', async () => { + console.log('TC: Credits_013 — volunteers section heading present'); + await CreditsPage.load(); + await driver.pause(waitTime); + + await CreditsPage.volunteersHeading.waitForDisplayed({ timeout: waitTime }); + await expect(CreditsPage.volunteersHeading).toBeDisplayed(); + }); + + // TC-014a: Volunteer role link is functional + it('verify finding a volunteer role link has valid href', async () => { + console.log('TC: Credits_014a — volunteer role link functional'); + await CreditsPage.load(); + await driver.pause(waitTime); + + await CreditsPage.volunteerRoleLink.waitForClickable({ timeout: waitTime }); + const href = await CreditsPage.volunteerRoleLink.getAttribute('href'); + expect(href).toBeTruthy(); + expect(href.startsWith('http')).toBe(true, `Volunteer link has invalid href: '${href}'`); + }); + + // TC-016: Footer is visible and fully rendered @BVT + it('verify footer is visible and key elements are rendered @BVT', async () => { + console.log('TC: Credits_016 — footer visible'); + await CreditsPage.load(); + await driver.pause(waitTime); + await CreditsPage.scrollToFooter(); + + await CreditsPage.footerGetStarted.waitForDisplayed({ timeout: waitTime }); + await expect(CreditsPage.footerGetStarted).toBeDisplayed(); + await expect(CreditsPage.footerPrivacy).toBeDisplayed(); + await expect(CreditsPage.footerOpenSource).toBeDisplayed(); + }); + + // TC-019: Footer social media links open in a new tab @BVT + it('verify footer social media links open in a new tab @BVT', async () => { + console.log('TC: Credits_019 — footer social links open in new tab'); + await CreditsPage.load(); + await driver.pause(waitTime); + await CreditsPage.scrollToFooter(); + + const socialLinks = [ + { name: 'Facebook', el: CreditsPage.footerFacebook }, + { name: 'Twitter', el: CreditsPage.footerTwitter }, + { name: 'Instagram', el: CreditsPage.footerInstagram }, + ]; + for (const { name, el } of socialLinks) { + const target = await el.getAttribute('target'); + expect(target).toBe('_blank', `${name} footer link should open in new tab — target='${target}'`); + } + }); + + // TC-020: Footer nonprofit disclaimer text is present @BVT + it('verify footer nonprofit disclaimer text is present @BVT', async () => { + console.log('TC: Credits_020 — nonprofit disclaimer present'); + await CreditsPage.load(); + await driver.pause(waitTime); + await CreditsPage.scrollToFooter(); + + const bodyText = (await $('body').getText()).toLowerCase(); + const hasNonprofit = bodyText.includes('nonprofit') || bodyText.includes('501'); + const hasPolitical = bodyText.includes('political') || bodyText.includes('candidate'); + expect(hasNonprofit).toBe(true, 'Nonprofit disclaimer text not found'); + expect(hasPolitical).toBe(true, 'Political neutrality disclaimer not found'); + }); + + // TC-021: Footer open source link points to GitHub @BVT + it('verify footer open source link points to GitHub @BVT', async () => { + console.log('TC: Credits_021 — open source link points to GitHub'); + await CreditsPage.load(); + await driver.pause(waitTime); + await CreditsPage.scrollToFooter(); + + await CreditsPage.footerOpenSource.waitForDisplayed({ timeout: waitTime }); + const href = await CreditsPage.footerOpenSource.getAttribute('href'); + expect(href).toContain('github', `Open source link should point to GitHub, got: '${href}'`); + }); + + // TC-022: App store badges are visible and functional @BVT + it('verify App Store badges are visible and open in new tab @BVT', async () => { + console.log('TC: Credits_022 — app store badges visible and functional'); + await CreditsPage.load(); + await driver.pause(waitTime); + await CreditsPage.scrollToFooter(); + + for (const { name, el } of [ + { name: 'Google Play', el: CreditsPage.googleBadge }, + { name: 'Apple App Store', el: CreditsPage.appleBadge }, + ]) { + const href = await el.getAttribute('href'); + const target = await el.getAttribute('target'); + expect(href).toBeTruthy(`${name} badge has no href`); + expect(target).toBe('_blank', `${name} badge should open in new tab — target='${target}'`); + } + }); + +}); \ No newline at end of file From 85235a0cc0f2b492b631efb34d9c60ccfee288ad Mon Sep 17 00:00:00 2001 From: Amitha Maria <229098300+amimrjo@users.noreply.github.com> Date: Tue, 22 Sep 2026 11:00:34 -0700 Subject: [PATCH 2/3] Fix Credits page tests - all 14/14 passing on BrowserStack (Chrome) --- .../page_objects/credits.browser.js | 17 +- .../specs/CreditsPage.browser.js | 244 ++++++++---------- 2 files changed, 116 insertions(+), 145 deletions(-) diff --git a/tests/browserstack_automation/page_objects/credits.browser.js b/tests/browserstack_automation/page_objects/credits.browser.js index f28789057..d3b1f8d2d 100644 --- a/tests/browserstack_automation/page_objects/credits.browser.js +++ b/tests/browserstack_automation/page_objects/credits.browser.js @@ -1,4 +1,4 @@ -import { $, $$, driver } from '@wdio/globals'; +import { $, $$ } from '@wdio/globals'; import PageBrowser from './page.browser'; // Page object for the WeVote Credits & Thanks page. @@ -19,7 +19,8 @@ class CreditsBrowser extends PageBrowser { // ─── Intro section ─────────────────────────────────────────────────────────── get openSourceInlineLink () { - return $('a[href*="open-source"]'); + // WDIO partial link text selector: first whose visible text contains this phrase + return $('*=open source software'); } // ─── Organization logos ────────────────────────────────────────────────────── @@ -28,8 +29,11 @@ class CreditsBrowser extends PageBrowser { return $$('img'); } - get allLogoLinks () { - return $$('a img'); + get orgLogoLinks () { + // tags that wrap an image and point to an external (non-WeVote) site. + // Note: the previous selector ('a img') returned the elements themselves, + // which have no href or target, so TC-010 and TC-011 could never pass. + return $$("//a[.//img][starts-with(@href, 'http')][not(contains(@href, 'wevote'))]"); } // ─── Volunteers section ────────────────────────────────────────────────────── @@ -91,8 +95,9 @@ class CreditsBrowser extends PageBrowser { } async scrollToFooter () { - await browser.execute(() => window.scrollTo(0, document.body.scrollHeight)); - await driver.pause(3000); + // Wait for the footer to exist, then scroll it into view (no fixed pause needed) + await this.footerGetStarted.waitForExist({ timeout: 10000 }); + await this.footerGetStarted.scrollIntoView(); } } diff --git a/tests/browserstack_automation/specs/CreditsPage.browser.js b/tests/browserstack_automation/specs/CreditsPage.browser.js index d17994126..6f938a6d1 100644 --- a/tests/browserstack_automation/specs/CreditsPage.browser.js +++ b/tests/browserstack_automation/specs/CreditsPage.browser.js @@ -1,70 +1,66 @@ -import { driver, expect, browser } from '@wdio/globals'; +import { $, browser, driver, expect } from '@wdio/globals'; import CreditsPage from '../page_objects/credits.browser'; -const waitTime = 5000; +const waitTime = 10000; /* eslint-disable no-undef */ describe('Credits & Thanks Page', () => { - - // TC-001: Credits page loads successfully @BVT - it('verify Credits page loads and heading is visible @BVT', async () => { - console.log('TC: Credits_001 — page loads successfully'); + beforeEach(async () => { await CreditsPage.load(); - await driver.pause(waitTime); + await CreditsPage.pageHeading.waitForDisplayed({ timeout: waitTime }); + }); + // TC-001: Credits page loads successfully + it('verify Credits page loads and heading is visible', async () => { + console.log('TC: Credits_001 — page loads successfully'); await expect(driver).toHaveUrl(expect.stringContaining('/more/credits')); await expect(CreditsPage.pageHeading).toBeDisplayed(); await expect(CreditsPage.pageHeading).toHaveText('Credits & Thanks'); }); - // TC-002: Browser tab title is correct @BVT - it('verify browser tab title contains Credits or WeVote @BVT', async () => { + // TC-002: Browser tab title is correct + it('verify browser tab title contains Credits or WeVote', async () => { console.log('TC: Credits_002 — page title is correct'); - await CreditsPage.load(); - await driver.pause(waitTime); - - const title = await browser.getTitle(); - const isValid = title.toLowerCase().includes('credits') || title.toLowerCase().includes('wevote'); - expect(isValid).toBe(true, `Unexpected page title: '${title}'`); + const title = (await browser.getTitle()).toLowerCase(); + expect(title).toMatch(/credits|wevote/); }); // TC-005: 'Open source software' inline link is clickable it('verify open source software inline link is clickable and has valid href', async () => { console.log('TC: Credits_005 — open source inline link'); - await CreditsPage.load(); - await driver.pause(waitTime); - await CreditsPage.openSourceInlineLink.waitForClickable({ timeout: waitTime }); - const href = await CreditsPage.openSourceInlineLink.getAttribute('href'); + // getProperty returns the resolved absolute URL, even if the raw href is relative + const href = await CreditsPage.openSourceInlineLink.getProperty('href'); expect(href).toBeTruthy(); - expect(href.startsWith('http')).toBe(true, `Invalid href: '${href}'`); }); - // TC-006: All organization logos render without broken images @BVT - it('verify all organization logos render without broken images @BVT', async () => { + // TC-006: All organization logos render without broken images + it('verify all organization logos render without broken images', async () => { console.log('TC: Credits_006 — no broken org logo images'); - await CreditsPage.load(); - await driver.pause(waitTime); - const images = await CreditsPage.allImages; const broken = []; for (const img of images) { const naturalWidth = await browser.execute((el) => el.naturalWidth, img); - const src = await img.getAttribute('src') || ''; + const src = (await img.getAttribute('src')) || ''; if (naturalWidth === 0 && src) { broken.push(src); } } - expect(broken.length).toBe(0, `Broken images found:\n${broken.join('\n')}`); + // Comparing to [] prints the list of broken image srcs if this fails + expect(broken).toEqual([]); }); - // TC-007: Key organizations are listed on the page @BVT - it('verify key organizations are present on the Credits page @BVT', async () => { - console.log('TC: Credits_007 — key org names present'); - await CreditsPage.load(); - await driver.pause(waitTime); + // TC-007: Key organizations are listed on the page (as visible text or logo alt text) + it('verify key organizations are present on the Credits page', async () => { + console.log('TC: Credits_007 — key org names present'); const bodyText = await $('body').getText(); + const altTexts = []; + for (const img of await CreditsPage.allImages) { + altTexts.push((await img.getAttribute('alt')) || ''); + } + const pageText = `${bodyText} ${altTexts.join(' ')}`.toLowerCase(); + const expectedOrgs = [ 'Amazon Web Services', 'Atlassian', @@ -72,45 +68,46 @@ describe('Credits & Thanks Page', () => { 'BrowserStack', 'Google', ]; - const missing = expectedOrgs.filter((org) => !bodyText.includes(org)); - expect(missing.length).toBe(0, `Missing organizations: ${missing.join(', ')}`); + const missing = expectedOrgs.filter((org) => !pageText.includes(org.toLowerCase())); + expect(missing).toEqual([]); }); - // TC-010: Org logo links have valid hrefs - it('verify organization logo links have valid hrefs', async () => { - console.log('TC: Credits_010 — org logo links navigate correctly'); - await CreditsPage.load(); - await driver.pause(waitTime); - - const logoLinks = await CreditsPage.allLogoLinks; - expect(logoLinks.length).toBeGreaterThan(0, 'No organization logo links found'); - - // Check first 5 to keep test fast - for (const link of logoLinks.slice(0, 5)) { - const href = await link.getAttribute('href') || ''; - expect(href.startsWith('http')).toBe(true, `Logo link has invalid href: '${href}'`); + // TC-010: Org logo links point to external sites + it('verify organization logo links have valid external hrefs', async () => { + console.log('TC: Credits_010 — org logo links have valid hrefs'); + const logoLinks = await CreditsPage.orgLogoLinks; + expect(logoLinks.length).toBeGreaterThanOrEqual(5); + + const invalid = []; + for (const link of logoLinks) { + const href = (await link.getAttribute('href')) || ''; + if (!/^https?:\/\/[^/\s]+\.[a-z]{2,}/i.test(href)) { + invalid.push(href); + } } + expect(invalid).toEqual([]); }); // TC-011: Org logo links open in a new tab it('verify organization logo links open in a new tab', async () => { console.log('TC: Credits_011 — org logo links open in new tab'); - await CreditsPage.load(); - await driver.pause(waitTime); - - const logoLinks = await CreditsPage.allLogoLinks; - expect(logoLinks.length).toBeGreaterThan(0, 'No organization logo links found'); - - const target = await logoLinks[0].getAttribute('target'); - expect(target).toBe('_blank', `First logo link should open in new tab — target='${target}'`); + const logoLinks = await CreditsPage.orgLogoLinks; + expect(logoLinks.length).toBeGreaterThan(0); + + // Collect the hrefs of any logo links that do NOT open in a new tab + const notNewTab = []; + for (const link of logoLinks) { + const target = await link.getAttribute('target'); + if (target !== '_blank') { + notNewTab.push(await link.getAttribute('href')); + } + } + expect(notNewTab).toEqual([]); }); - // TC-013: Volunteers, Interns & Donors section heading is present @BVT - it('verify Volunteers Interns and Donors section heading is visible @BVT', async () => { + // TC-013: Volunteers, Interns & Donors section heading is present + it('verify Volunteers Interns and Donors section heading is visible', async () => { console.log('TC: Credits_013 — volunteers section heading present'); - await CreditsPage.load(); - await driver.pause(waitTime); - await CreditsPage.volunteersHeading.waitForDisplayed({ timeout: waitTime }); await expect(CreditsPage.volunteersHeading).toBeDisplayed(); }); @@ -118,88 +115,57 @@ describe('Credits & Thanks Page', () => { // TC-014a: Volunteer role link is functional it('verify finding a volunteer role link has valid href', async () => { console.log('TC: Credits_014a — volunteer role link functional'); - await CreditsPage.load(); - await driver.pause(waitTime); - await CreditsPage.volunteerRoleLink.waitForClickable({ timeout: waitTime }); const href = await CreditsPage.volunteerRoleLink.getAttribute('href'); - expect(href).toBeTruthy(); - expect(href.startsWith('http')).toBe(true, `Volunteer link has invalid href: '${href}'`); + expect(href).toMatch(/^https?:\/\//); }); - // TC-016: Footer is visible and fully rendered @BVT - it('verify footer is visible and key elements are rendered @BVT', async () => { - console.log('TC: Credits_016 — footer visible'); - await CreditsPage.load(); - await driver.pause(waitTime); - await CreditsPage.scrollToFooter(); - - await CreditsPage.footerGetStarted.waitForDisplayed({ timeout: waitTime }); - await expect(CreditsPage.footerGetStarted).toBeDisplayed(); - await expect(CreditsPage.footerPrivacy).toBeDisplayed(); - await expect(CreditsPage.footerOpenSource).toBeDisplayed(); - }); - - // TC-019: Footer social media links open in a new tab @BVT - it('verify footer social media links open in a new tab @BVT', async () => { - console.log('TC: Credits_019 — footer social links open in new tab'); - await CreditsPage.load(); - await driver.pause(waitTime); - await CreditsPage.scrollToFooter(); - - const socialLinks = [ - { name: 'Facebook', el: CreditsPage.footerFacebook }, - { name: 'Twitter', el: CreditsPage.footerTwitter }, - { name: 'Instagram', el: CreditsPage.footerInstagram }, - ]; - for (const { name, el } of socialLinks) { - const target = await el.getAttribute('target'); - expect(target).toBe('_blank', `${name} footer link should open in new tab — target='${target}'`); - } - }); - - // TC-020: Footer nonprofit disclaimer text is present @BVT - it('verify footer nonprofit disclaimer text is present @BVT', async () => { - console.log('TC: Credits_020 — nonprofit disclaimer present'); - await CreditsPage.load(); - await driver.pause(waitTime); - await CreditsPage.scrollToFooter(); - - const bodyText = (await $('body').getText()).toLowerCase(); - const hasNonprofit = bodyText.includes('nonprofit') || bodyText.includes('501'); - const hasPolitical = bodyText.includes('political') || bodyText.includes('candidate'); - expect(hasNonprofit).toBe(true, 'Nonprofit disclaimer text not found'); - expect(hasPolitical).toBe(true, 'Political neutrality disclaimer not found'); - }); - - // TC-021: Footer open source link points to GitHub @BVT - it('verify footer open source link points to GitHub @BVT', async () => { - console.log('TC: Credits_021 — open source link points to GitHub'); - await CreditsPage.load(); - await driver.pause(waitTime); - await CreditsPage.scrollToFooter(); - - await CreditsPage.footerOpenSource.waitForDisplayed({ timeout: waitTime }); - const href = await CreditsPage.footerOpenSource.getAttribute('href'); - expect(href).toContain('github', `Open source link should point to GitHub, got: '${href}'`); - }); - - // TC-022: App store badges are visible and functional @BVT - it('verify App Store badges are visible and open in new tab @BVT', async () => { - console.log('TC: Credits_022 — app store badges visible and functional'); - await CreditsPage.load(); - await driver.pause(waitTime); - await CreditsPage.scrollToFooter(); - - for (const { name, el } of [ - { name: 'Google Play', el: CreditsPage.googleBadge }, - { name: 'Apple App Store', el: CreditsPage.appleBadge }, - ]) { - const href = await el.getAttribute('href'); - const target = await el.getAttribute('target'); - expect(href).toBeTruthy(`${name} badge has no href`); - expect(target).toBe('_blank', `${name} badge should open in new tab — target='${target}'`); - } + describe('Footer', () => { + beforeEach(async () => { + await CreditsPage.scrollToFooter(); + }); + + // TC-016: Footer is visible and fully rendered + it('verify footer is visible and key elements are rendered', async () => { + console.log('TC: Credits_016 — footer visible'); + await CreditsPage.footerGetStarted.waitForDisplayed({ timeout: waitTime }); + await expect(CreditsPage.footerGetStarted).toBeDisplayed(); + await expect(CreditsPage.footerPrivacy).toBeDisplayed(); + await expect(CreditsPage.footerOpenSource).toBeDisplayed(); + }); + + // TC-019: Footer social media links open in a new tab + it('verify footer social media links open in a new tab', async () => { + console.log('TC: Credits_019 — footer social links open in new tab'); + await expect(CreditsPage.footerFacebook).toHaveAttribute('target', '_blank'); + await expect(CreditsPage.footerTwitter).toHaveAttribute('target', '_blank'); + await expect(CreditsPage.footerInstagram).toHaveAttribute('target', '_blank'); + }); + + // TC-020: Footer nonprofit disclaimer text is present + it('verify footer nonprofit disclaimer text is present', async () => { + console.log('TC: Credits_020 — nonprofit disclaimer present'); + const bodyText = (await $('body').getText()).toLowerCase(); + expect(bodyText).toMatch(/nonprofit|501/); + expect(bodyText).toMatch(/political|candidate/); + }); + + // TC-021: Footer open source link points to GitHub + it('verify footer open source link points to GitHub', async () => { + console.log('TC: Credits_021 — open source link points to GitHub'); + await CreditsPage.footerOpenSource.waitForDisplayed({ timeout: waitTime }); + const href = await CreditsPage.footerOpenSource.getAttribute('href'); + expect(href).toContain('github'); + }); + + // TC-022: App store badges are visible and functional + it('verify App Store badges have links and open in new tab', async () => { + console.log('TC: Credits_022 — app store badges functional'); + for (const badge of [CreditsPage.googleBadge, CreditsPage.appleBadge]) { + const href = await badge.getAttribute('href'); + expect(href).toBeTruthy(); + await expect(badge).toHaveAttribute('target', '_blank'); + } + }); }); - }); \ No newline at end of file From f7e0d31fe219a1424c124aea33b32270cdd0b164 Mon Sep 17 00:00:00 2001 From: Amitha Maria <229098300+amimrjo@users.noreply.github.com> Date: Tue, 22 Sep 2026 11:27:11 -0700 Subject: [PATCH 3/3] Expand TC-007 to verify all 25 credited organizations, add logo count check --- .../specs/CreditsPage.browser.js | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/tests/browserstack_automation/specs/CreditsPage.browser.js b/tests/browserstack_automation/specs/CreditsPage.browser.js index 6f938a6d1..c2b016ff3 100644 --- a/tests/browserstack_automation/specs/CreditsPage.browser.js +++ b/tests/browserstack_automation/specs/CreditsPage.browser.js @@ -51,9 +51,10 @@ describe('Credits & Thanks Page', () => { }); - // TC-007: Key organizations are listed on the page (as visible text or logo alt text) - it('verify key organizations are present on the Credits page', async () => { - console.log('TC: Credits_007 — key org names present'); + + // TC-007: All credited organizations are present on the page + it('verify all credited organizations are present on the Credits page', async () => { + console.log('TC: Credits_007 — all org names present'); const bodyText = await $('body').getText(); const altTexts = []; for (const img of await CreditsPage.allImages) { @@ -61,17 +62,27 @@ describe('Credits & Thanks Page', () => { } const pageText = `${bodyText} ${altTexts.join(' ')}`.toLowerCase(); + // Distinctive fragment of each org name, so minor wording changes + // (e.g. "Amazon Web Services" vs "AWS") don't break the test const expectedOrgs = [ - 'Amazon Web Services', - 'Atlassian', - 'Ballotpedia', - 'BrowserStack', - 'Google', + 'amazon', 'atlassian', 'ballotpedia', 'browserstack', 'civic life', + 'civicmakers', 'code for america', 'dla piper', 'eleven labs', 'facebook', + 'fast forward', 'fastly', 'google', 'greenberg', 'league of women voters', + 'maplight', 'microsoft', 'turbovote', 'twilio', 'twitter', + 'vote smart', 'vote usa', 'pew', 'we vote education', 'wikipedia', ]; - const missing = expectedOrgs.filter((org) => !pageText.includes(org.toLowerCase())); + const missing = expectedOrgs.filter((org) => !pageText.includes(org)); expect(missing).toEqual([]); }); + // TC-007b: The organization logo section renders in full + it('verify the organization logo section renders in full', async () => { + console.log('TC: Credits_007b — org logo count'); + const logoLinks = await CreditsPage.orgLogoLinks; + console.log(`Found ${logoLinks.length} organization logo links`); + expect(logoLinks.length).toBeGreaterThanOrEqual(20); + }); + // TC-010: Org logo links point to external sites it('verify organization logo links have valid external hrefs', async () => { console.log('TC: Credits_010 — org logo links have valid hrefs');