Repository navigation
Credits page automated tests [WV-2638] #4927
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
amitha-m-joe
wants to merge
3
commits into
wevote:develop
Choose a base branch
from
amitha-m-joe:credits-page-wv2638
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
104 changes: 104 additions & 0 deletions
104
tests/browserstack_automation/page_objects/credits.browser.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| import { $, $$ } 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 () { | ||
| // WDIO partial link text selector: first <a> whose visible text contains this phrase | ||
| return $('*=open source software'); | ||
| } | ||
|
|
||
| // ─── Organization logos ────────────────────────────────────────────────────── | ||
|
|
||
| get allImages () { | ||
| return $$('img'); | ||
| } | ||
|
|
||
| get orgLogoLinks () { | ||
| // <a> tags that wrap an image and point to an external (non-WeVote) site. | ||
| // Note: the previous selector ('a img') returned the <img> 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 ────────────────────────────────────────────────────── | ||
|
|
||
| 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 () { | ||
| // 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(); | ||
| } | ||
| } | ||
|
|
||
| export default new CreditsBrowser(); |
182 changes: 182 additions & 0 deletions
182
tests/browserstack_automation/specs/CreditsPage.browser.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,182 @@ | ||
| import { $, browser, driver, expect } from '@wdio/globals'; | ||
| import CreditsPage from '../page_objects/credits.browser'; | ||
|
|
||
| const waitTime = 10000; | ||
|
|
||
| /* eslint-disable no-undef */ | ||
| describe('Credits & Thanks Page', () => { | ||
| beforeEach(async () => { | ||
| await CreditsPage.load(); | ||
| 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 | ||
| it('verify browser tab title contains Credits or WeVote', async () => { | ||
| console.log('TC: Credits_002 — page title is correct'); | ||
| 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.openSourceInlineLink.waitForClickable({ timeout: waitTime }); | ||
| // getProperty returns the resolved absolute URL, even if the raw href is relative | ||
| const href = await CreditsPage.openSourceInlineLink.getProperty('href'); | ||
| expect(href).toBeTruthy(); | ||
| }); | ||
|
|
||
| // 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'); | ||
| 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); | ||
| } | ||
| } | ||
| // Comparing to [] prints the list of broken image srcs if this fails | ||
| expect(broken).toEqual([]); | ||
| }); | ||
|
|
||
|
|
||
|
|
||
| // 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) { | ||
| altTexts.push((await img.getAttribute('alt')) || ''); | ||
| } | ||
| 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', '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)); | ||
| 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'); | ||
| 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'); | ||
| 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 | ||
| it('verify Volunteers Interns and Donors section heading is visible', async () => { | ||
| console.log('TC: Credits_013 — volunteers section heading present'); | ||
| 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.volunteerRoleLink.waitForClickable({ timeout: waitTime }); | ||
| const href = await CreditsPage.volunteerRoleLink.getAttribute('href'); | ||
| expect(href).toMatch(/^https?:\/\//); | ||
| }); | ||
|
|
||
| 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'); | ||
| } | ||
| }); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi Amitha,
Thank you for raising the PR and for attaching the BrowserStack screenshot. I went through the test cases, spec, and page object. Here are a few points that can be changed for better optimization, maintainability, and to ensure the automation is validating the actual functionality described in the test case document.
1.TC-002 – Browser tab title-> the current assertion:
expect(title).toMatch(/credits|wevote/)is too broad. It could pass even if the title is not the expected Credits page title. Please make the assertion more specific based on the expected title in the test case document, for example by verifying that it contains both "Credits & Thanks" and "WeVote", or use the exact expected title if it is fixed.
TC-005 – Open source software link**
Currently, the test only verifies that the
hrefexists and is truthy. A validhrefdoes not necessarily mean that the link is functional. The destination could return a 404, redirect somewhere unexpected, or otherwise fail to open.Since this test case is intended to verify that the link is clickable, please perform the click operation and verify that it opens/navigates to the expected destination.
TC-006 – Organization logo images
Rather than checking all
imgelements on the entire page, please scope the image elements to:CompanyWrapper-sc-r9j5wf-5 fhPjNcThis will ensure that we are validating only the organization/company logos and not unrelated images such as footer or app-store images.
Also, the logic currently around lines 42–46 can be moved into a separate reusable helper function in the page object. The spec should ideally contain only the test flow and assertion.
TC-007 – Organization names
Currently,
body.getText()checks the entire page. This creates a false-positive risk.For example, if "Facebook" appears in the footer, the test could pass even if the Facebook organization credit/logo is missing from the organization section.
This validation should also be scoped to:
CompanyWrapper-sc-r9j5wf-5 fhPjNcPlease move the locators and reusable logic into the page object and keep the spec focused on the test assertion.
More generally, we should avoid using
$('body').getText()when the requirement is specifically about a particular section. The same principle should be applied to other tests where applicable.TC-007b – Test case number and expected length
According to the test case document, TC-007b should be TC-009.
Also, rather than hard-coding
20, please declare it as a separate variable for maintainability:const minimumLength = 20;expect(logoLinks.length).toBeGreaterThanOrEqual(minimumLength);This makes it clear what the value represents for anyone maintaining the test in the future.
TC-010 and TC-011 – Organization logo links
These can potentially be covered together, depending on the test case document.
TC-010 should perform the actual click operation and verify that the organization link opens successfully.
Currently, TC-011 only checks:
target="_blank"This verifies that the HTML requests a new tab, but it does not verify that clicking the link actually opens a new tab. These are two different validations.
Also, please review the
orgLogoLinkslocator in the page object. If the locator already filters links usingstarts-with(@href, 'http'), then TC-010 cannot detect an invalid/non-HTTP href because those links are excluded by the locator before the test runs.The page-object locator should identify the organization links, while the test/helper should validate whether their hrefs are valid external URLs.
TC-014a – Volunteer role link
The current test only verifies that the
hrefhas a value and matches an HTTP URL.Since this test case is intended to verify that the volunteer role link is functional, please perform the click operation and verify that the expected page/link opens successfully.
C-014b and TC-015 – Missing test cases
TC-019 – Footer social media links
Currently, the test only verifies that the links have:
target="_blank"This verifies the HTML attribute but does not verify the actual browser behavior.
Please perform the click operation and verify that the social media link actually opens in a new tab/window as required by the test case.
TC-020 – Footer nonprofit disclaimer
Currently:
const bodyText = (await $('body').getText()).toLowerCase();checks the entire page and creates a false-positive risk.
Please identify the specific footer disclaimer locator and validate the required text within that specific element/section instead of checking the entire page body.
TC-021 – Footer open source link
Currently, the test only checks:
expect(href).toContain('github');This verifies the href value but does not verify that clicking the link actually opens GitHub.
If the test case is intended to verify the link functionality, please perform the click operation and verify that the resulting page/tab opens GitHub.
TC-022 – App Store badges
Page Object vs. Spec separation
Wherever possible, the locators and reusable UI interaction/helper logic should remain in the page object, while the spec should primarily contain the test flow and assertions.
For example, logic such as:
can be implemented as reusable helper methods in the page object.
The spec can then remain simple and readable, for example:
const brokenImages = await CreditsPage.getBrokenOrganizationImages();expect(brokenImages).toEqual([]);Please keep the actual assertions in the spec rather than putting
expect()statements inside the page object.Avoid whole-page validation when testing a specific section
I noticed this pattern in multiple tests, particularly TC-007 and TC-020:
$('body').getText()When the requirement is related to a specific section, it is better to locate that specific section and validate its contents. This reduces false positives and makes the automation more closely aligned with the actual requirement.
Functional validation vs. attribute validation
In several places, the test case description says that a link should be "functional" or should "open in a new tab," but the current automation only checks attributes such as:
hrefor
target="_blank"These are useful validations, but they do not prove the actual browser behavior.
Where the requirement is functional behavior, please perform the click and verify the resulting navigation/new tab. Attribute validation can still be retained as an additional check if useful.
Thanks!
Charanya
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@amitha-m-joe and @charanya-QA would you like me to approve this pull request as-is, or wait for another iteration of this pull request to be submitted?