From d8097e2f9761ceb6d148d9b455ec470d4506c344 Mon Sep 17 00:00:00 2001 From: nssensalo Date: Mon, 27 Oct 2025 14:04:03 -0700 Subject: [PATCH 1/5] Debugging broken browse-bills and testimony playwright tests --- tests/e2e/browse-bills.spec.ts | 30 +++++++++-------- tests/e2e/page_objects/billPage.ts | 31 ++++++++++++----- tests/e2e/page_objects/testimony.ts | 14 +++++++- tests/e2e/testimony.spec.ts | 52 ++++++++++++++++++----------- 4 files changed, 85 insertions(+), 42 deletions(-) diff --git a/tests/e2e/browse-bills.spec.ts b/tests/e2e/browse-bills.spec.ts index 84303aec2..ebaa62002 100644 --- a/tests/e2e/browse-bills.spec.ts +++ b/tests/e2e/browse-bills.spec.ts @@ -2,22 +2,26 @@ import { test, expect } from "@playwright/test" import { BillPage } from "./page_objects/billPage" test.beforeEach(async ({ page }) => { - await page.goto("http://localhost:3000/bills") - await page.waitForSelector("li.ais-Hits-item a") + const billpage = new BillPage(page) + await billpage.goto() + await billpage.removePresetCourtfilter() + + }) test.describe("Search result test", () => { test("should search for bills", async ({ page }) => { - const billpage = new BillPage(page) - - const searchTerm = billpage.searchWord - const resultCount = billpage.resultCount - const initialResultCount = await resultCount.textContent() + const billpage = new BillPage(page); - await billpage.search(searchTerm) + const searchTerm = billpage.searchWord; + + await billpage.search(searchTerm); + + await expect(billpage.queryFilter).toBeVisible(); + + await expect(billpage.queryFilter).toContainText(searchTerm); - const searchResultCount = await resultCount.textContent() - await expect(searchResultCount).not.toBe(initialResultCount) + await expect(billpage.firstBill).toBeVisible(); }) test("should show search query", async ({ page }) => { @@ -30,7 +34,7 @@ test.describe("Search result test", () => { const queryFilter = await billpage.queryFilter - await expect(queryFilter).toContainText("query:") + await expect(queryFilter).toContainText("Query:") await expect(queryFilter).toContainText(searchTerm) }) @@ -50,7 +54,7 @@ test.describe("Search result test", () => { const searchTerm = "nonexistentsearchterm12345" const billpage = new BillPage(page) - billpage.search(searchTerm) + await billpage.search(searchTerm) const noResultsText = await page.getByText("Looks Pretty Empty Here") const noResultsImg = page.getByAltText("No Results") @@ -118,7 +122,7 @@ test.describe("Filter Bills test", () => { "%27" ) await expect(page).toHaveURL( - new RegExp(`court%5D%5B1%5D=${encodedFilterLabel}`) + new RegExp(`court%5D%5B0%5D=${encodedFilterLabel}`) ) }) diff --git a/tests/e2e/page_objects/billPage.ts b/tests/e2e/page_objects/billPage.ts index b4f77131f..91e70f854 100644 --- a/tests/e2e/page_objects/billPage.ts +++ b/tests/e2e/page_objects/billPage.ts @@ -12,6 +12,8 @@ export class BillPage { readonly currentCategorySelector: string readonly basicCategorySelector: string readonly billPageBackToList: Locator + readonly resultsCountText: Locator + constructor(page: Page) { this.page = page @@ -26,23 +28,23 @@ export class BillPage { "li:nth-child(2) input.ais-RefinementList-checkbox" this.currentCategorySelector = ".ais-CurrentRefinements-item" this.basicCategorySelector = "div.ais-RefinementList.mb-4" + this.resultsCountText = page.getByText("Results").first() + } async goto() { await this.page.goto("http://localhost:3000/bills") - await this.page.waitForSelector("li.ais-Hits-item a") + await this.resultCount.waitFor({ state: 'visible', timeout: 30000 }); +// await this.page.waitForSelector("li.ais-Hits-item a",{timeout:90000}) + } async search(query: string) { - const initialResult = await this.firstBill.textContent() + await this.searchBar.focus(); await this.searchBar.fill(query) - await this.page.waitForFunction(initialResult => { - const searchResult = document.querySelector("li.ais-Hits-item a") - return ( - !searchResult || - (searchResult && searchResult.textContent != initialResult) - ) - }, initialResult) + const activeQueryFilter = this.page.getByText(`Query: ${query}`).first(); + + await activeQueryFilter.waitFor({ state: 'visible', timeout: 50000 }); } async sort(option: string) { @@ -152,4 +154,15 @@ export class BillPage { return filterLabel } + + async removePresetCourtfilter() { + const activeCourtCheckbox = this.page + .locator('div, span, label', { has: this.page.getByText(/Court/i) }) + .getByRole('checkbox', { checked: true }); + + await activeCourtCheckbox.click({ noWaitAfter: true, timeout: 0 }); + + await this.page.getByText("Results").first().waitFor({ state: 'visible', timeout: 60000 }); + + } } diff --git a/tests/e2e/page_objects/testimony.ts b/tests/e2e/page_objects/testimony.ts index 803c4d7f7..57a44658a 100644 --- a/tests/e2e/page_objects/testimony.ts +++ b/tests/e2e/page_objects/testimony.ts @@ -38,7 +38,19 @@ export class TestimonyPage { } async sort(option: string) { - await this.page.getByText("Sort by New -> Old").click() + // previoud code: await this.page.getByText("Sort by New -> Old").click() + await this.page.getByText(/Sort by/i).first().click(); await this.page.getByRole("option", { name: option }).click() } + + async removePresetCourtfilter() { + const activeCourtCheckbox = this.page + .locator('div, span, label', { has: this.page.getByText(/Court/i) }) + .getByRole('checkbox', { checked: true }); + + await activeCourtCheckbox.click({ noWaitAfter: true, timeout: 0 }); + + await this.resultsCountText.waitFor({ state: 'visible', timeout: 60000 }); + + } } diff --git a/tests/e2e/testimony.spec.ts b/tests/e2e/testimony.spec.ts index ab57f6012..4d8ac0be9 100644 --- a/tests/e2e/testimony.spec.ts +++ b/tests/e2e/testimony.spec.ts @@ -1,5 +1,6 @@ import { test, expect } from "@playwright/test" import { TestimonyPage } from "./page_objects/testimony" +import { waitFor } from "@testing-library/dom" test.beforeEach(async ({ page }) => { await page.goto("http://localhost:3000/testimony") @@ -34,7 +35,7 @@ test.describe("Testimony Search", () => { await testimonyPage.search(queryText) const { queryFilterItem, resultsCountText } = testimonyPage - await expect(queryFilterItem).toContainText("query:") + await expect(queryFilterItem).toContainText("Query:") await expect(queryFilterItem).toContainText(queryText) await expect(resultsCountText).toBeVisible() }) @@ -102,33 +103,47 @@ test.describe("Testimony Filtering", () => { }) test("should filter by position: endorse", async ({ page }) => { - await page.getByRole("checkbox", { name: "endorse" }).check() - const testimonyPage = new TestimonyPage(page) - await expect(testimonyPage.positionFilterItem).toContainText("endorse") - await expect(page).toHaveURL(/.*position%5D%5B0%5D=endorse/) + const testimonyPage = new TestimonyPage(page); + testimonyPage.removePresetCourtfilter(); + + const endorseCheckbox = page.getByRole("checkbox", { name: /endorse/i }); + await endorseCheckbox.check({ timeout: 30000 }); + + await expect(testimonyPage.positionFilterItem).toContainText("endorse"); + await expect(page).toHaveURL(/.*position%5D%5B0%5D=endorse/); }) test("should filter by position: neutral", async ({ page }) => { - await page.getByRole("checkbox", { name: "neutral" }).check() - const testimonyPage = new TestimonyPage(page) - await expect(testimonyPage.positionFilterItem).toContainText("neutral") - await expect(page).toHaveURL(/.*position%5D%5B0%5D=neutral/) + const testimonyPage = new TestimonyPage(page); + testimonyPage.removePresetCourtfilter(); + + const checkNeutral = page.getByRole("checkbox", { name: "neutral" }); + await checkNeutral.check({timeout:30000}); + await page.getByRole("checkbox", { name: "neutral" }).check(); + + await expect(testimonyPage.positionFilterItem).toContainText("neutral"); + await expect(page).toHaveURL(/.*position%5D%5B0%5D=neutral/); }) test("should filter by bill", async ({ page }) => { + const testimonyPage = new TestimonyPage(page); + testimonyPage.removePresetCourtfilter(); + const billCheckbox = page.getByLabel(/^[S|H]\d{1,4}$/).first() const billId = await billCheckbox.inputValue() expect(billId).toBeTruthy() if (billId) { await billCheckbox.check() - const testimonyPage = new TestimonyPage(page) await expect(testimonyPage.billFilterItem).toContainText(billId as string) await expect(page).toHaveURL(new RegExp(`.*billId%5D%5B0%5D=${billId}`)) } }) test("should filter by author", async ({ page }) => { + const testimonyPage = new TestimonyPage(page); + testimonyPage.removePresetCourtfilter(); + const writtenByText = await page .getByText(/Written by/) .first() @@ -138,7 +153,6 @@ test.describe("Testimony Filtering", () => { if (writtenByText) { const authorName = writtenByText.slice(11) await page.getByRole("checkbox", { name: authorName }).check() - const testimonyPage = new TestimonyPage(page) await expect(testimonyPage.authorFilterItem).toContainText(authorName) await expect(page).toHaveURL( new RegExp( @@ -151,16 +165,16 @@ test.describe("Testimony Filtering", () => { test.describe("Testimony Sorting", () => { test("should sort by new -> old", async ({ page }) => { - const testimonyPage = new TestimonyPage(page) - await testimonyPage.sort("Sort by New -> Old") - const sortValue = page.getByText("Sort by New -> Old", { exact: true }) - await expect(sortValue).toBeVisible() + const testimonyPage = new TestimonyPage(page); + await testimonyPage.sort("Sort by New -> Old"); + const sortValue = page.getByText("Sort by New -> Old", { exact: true }); + await expect(sortValue).toBeVisible(); }) test("should sort by old -> new", async ({ page }) => { - const testimonyPage = new TestimonyPage(page) - await testimonyPage.sort("Sort by Old -> New") - const sortValue = page.getByText("Sort by Old -> New", { exact: true }) - await expect(sortValue).toBeVisible() + const testimonyPage = new TestimonyPage(page); + await testimonyPage.sort("Sort by Old -> New"); + const sortValue = page.getByText("Sort by Old -> New", { exact: true }); + await expect(sortValue).toBeVisible(); }) }) From 01d3df422b15312d7bb95b57f8890890857aebb0 Mon Sep 17 00:00:00 2001 From: nssensalo Date: Mon, 27 Oct 2025 20:35:00 -0700 Subject: [PATCH 2/5] re-submitting after running linting --- tests/e2e/browse-bills.spec.ts | 20 +++++----- tests/e2e/page_objects/billPage.ts | 35 ++++++++-------- tests/e2e/page_objects/testimony.ts | 20 +++++----- tests/e2e/testimony.spec.ts | 62 ++++++++++++++--------------- 4 files changed, 68 insertions(+), 69 deletions(-) diff --git a/tests/e2e/browse-bills.spec.ts b/tests/e2e/browse-bills.spec.ts index ebaa62002..a409e6075 100644 --- a/tests/e2e/browse-bills.spec.ts +++ b/tests/e2e/browse-bills.spec.ts @@ -5,23 +5,21 @@ test.beforeEach(async ({ page }) => { const billpage = new BillPage(page) await billpage.goto() await billpage.removePresetCourtfilter() - - }) test.describe("Search result test", () => { test("should search for bills", async ({ page }) => { - const billpage = new BillPage(page); + const billpage = new BillPage(page) + + const searchTerm = billpage.searchWord + + await billpage.search(searchTerm) + + await expect(billpage.queryFilter).toBeVisible() - const searchTerm = billpage.searchWord; - - await billpage.search(searchTerm); - - await expect(billpage.queryFilter).toBeVisible(); - - await expect(billpage.queryFilter).toContainText(searchTerm); + await expect(billpage.queryFilter).toContainText(searchTerm) - await expect(billpage.firstBill).toBeVisible(); + await expect(billpage.firstBill).toBeVisible() }) test("should show search query", async ({ page }) => { diff --git a/tests/e2e/page_objects/billPage.ts b/tests/e2e/page_objects/billPage.ts index 91e70f854..62adcf24f 100644 --- a/tests/e2e/page_objects/billPage.ts +++ b/tests/e2e/page_objects/billPage.ts @@ -14,7 +14,6 @@ export class BillPage { readonly billPageBackToList: Locator readonly resultsCountText: Locator - constructor(page: Page) { this.page = page this.searchWord = "health" @@ -29,22 +28,20 @@ export class BillPage { this.currentCategorySelector = ".ais-CurrentRefinements-item" this.basicCategorySelector = "div.ais-RefinementList.mb-4" this.resultsCountText = page.getByText("Results").first() - } async goto() { await this.page.goto("http://localhost:3000/bills") - await this.resultCount.waitFor({ state: 'visible', timeout: 30000 }); -// await this.page.waitForSelector("li.ais-Hits-item a",{timeout:90000}) - + await this.resultCount.waitFor({ state: "visible", timeout: 30000 }) + // await this.page.waitForSelector("li.ais-Hits-item a",{timeout:90000}) } async search(query: string) { - await this.searchBar.focus(); + await this.searchBar.focus() await this.searchBar.fill(query) - const activeQueryFilter = this.page.getByText(`Query: ${query}`).first(); - - await activeQueryFilter.waitFor({ state: 'visible', timeout: 50000 }); + const activeQueryFilter = this.page.getByText(`Query: ${query}`).first() + + await activeQueryFilter.waitFor({ state: "visible", timeout: 50000 }) } async sort(option: string) { @@ -154,15 +151,17 @@ export class BillPage { return filterLabel } - + async removePresetCourtfilter() { - const activeCourtCheckbox = this.page - .locator('div, span, label', { has: this.page.getByText(/Court/i) }) - .getByRole('checkbox', { checked: true }); - - await activeCourtCheckbox.click({ noWaitAfter: true, timeout: 0 }); - - await this.page.getByText("Results").first().waitFor({ state: 'visible', timeout: 60000 }); - + const activeCourtCheckbox = this.page + .locator("div, span, label", { has: this.page.getByText(/Court/i) }) + .getByRole("checkbox", { checked: true }) + + await activeCourtCheckbox.click({ noWaitAfter: true, timeout: 0 }) + + await this.page + .getByText("Results") + .first() + .waitFor({ state: "visible", timeout: 60000 }) } } diff --git a/tests/e2e/page_objects/testimony.ts b/tests/e2e/page_objects/testimony.ts index 57a44658a..02baca3f1 100644 --- a/tests/e2e/page_objects/testimony.ts +++ b/tests/e2e/page_objects/testimony.ts @@ -39,18 +39,20 @@ export class TestimonyPage { async sort(option: string) { // previoud code: await this.page.getByText("Sort by New -> Old").click() - await this.page.getByText(/Sort by/i).first().click(); + await this.page + .getByText(/Sort by/i) + .first() + .click() await this.page.getByRole("option", { name: option }).click() } async removePresetCourtfilter() { - const activeCourtCheckbox = this.page - .locator('div, span, label', { has: this.page.getByText(/Court/i) }) - .getByRole('checkbox', { checked: true }); - - await activeCourtCheckbox.click({ noWaitAfter: true, timeout: 0 }); - - await this.resultsCountText.waitFor({ state: 'visible', timeout: 60000 }); - + const activeCourtCheckbox = this.page + .locator("div, span, label", { has: this.page.getByText(/Court/i) }) + .getByRole("checkbox", { checked: true }) + + await activeCourtCheckbox.click({ noWaitAfter: true, timeout: 0 }) + + await this.resultsCountText.waitFor({ state: "visible", timeout: 60000 }) } } diff --git a/tests/e2e/testimony.spec.ts b/tests/e2e/testimony.spec.ts index 4d8ac0be9..94a1c56ea 100644 --- a/tests/e2e/testimony.spec.ts +++ b/tests/e2e/testimony.spec.ts @@ -103,32 +103,32 @@ test.describe("Testimony Filtering", () => { }) test("should filter by position: endorse", async ({ page }) => { - const testimonyPage = new TestimonyPage(page); - testimonyPage.removePresetCourtfilter(); - - const endorseCheckbox = page.getByRole("checkbox", { name: /endorse/i }); - await endorseCheckbox.check({ timeout: 30000 }); - - await expect(testimonyPage.positionFilterItem).toContainText("endorse"); - await expect(page).toHaveURL(/.*position%5D%5B0%5D=endorse/); + const testimonyPage = new TestimonyPage(page) + testimonyPage.removePresetCourtfilter() + + const endorseCheckbox = page.getByRole("checkbox", { name: /endorse/i }) + await endorseCheckbox.check({ timeout: 30000 }) + + await expect(testimonyPage.positionFilterItem).toContainText("endorse") + await expect(page).toHaveURL(/.*position%5D%5B0%5D=endorse/) }) test("should filter by position: neutral", async ({ page }) => { - const testimonyPage = new TestimonyPage(page); - testimonyPage.removePresetCourtfilter(); - - const checkNeutral = page.getByRole("checkbox", { name: "neutral" }); - await checkNeutral.check({timeout:30000}); - await page.getByRole("checkbox", { name: "neutral" }).check(); - - await expect(testimonyPage.positionFilterItem).toContainText("neutral"); - await expect(page).toHaveURL(/.*position%5D%5B0%5D=neutral/); + const testimonyPage = new TestimonyPage(page) + testimonyPage.removePresetCourtfilter() + + const checkNeutral = page.getByRole("checkbox", { name: "neutral" }) + await checkNeutral.check({ timeout: 30000 }) + await page.getByRole("checkbox", { name: "neutral" }).check() + + await expect(testimonyPage.positionFilterItem).toContainText("neutral") + await expect(page).toHaveURL(/.*position%5D%5B0%5D=neutral/) }) test("should filter by bill", async ({ page }) => { - const testimonyPage = new TestimonyPage(page); - testimonyPage.removePresetCourtfilter(); - + const testimonyPage = new TestimonyPage(page) + testimonyPage.removePresetCourtfilter() + const billCheckbox = page.getByLabel(/^[S|H]\d{1,4}$/).first() const billId = await billCheckbox.inputValue() expect(billId).toBeTruthy() @@ -141,9 +141,9 @@ test.describe("Testimony Filtering", () => { }) test("should filter by author", async ({ page }) => { - const testimonyPage = new TestimonyPage(page); - testimonyPage.removePresetCourtfilter(); - + const testimonyPage = new TestimonyPage(page) + testimonyPage.removePresetCourtfilter() + const writtenByText = await page .getByText(/Written by/) .first() @@ -165,16 +165,16 @@ test.describe("Testimony Filtering", () => { test.describe("Testimony Sorting", () => { test("should sort by new -> old", async ({ page }) => { - const testimonyPage = new TestimonyPage(page); - await testimonyPage.sort("Sort by New -> Old"); - const sortValue = page.getByText("Sort by New -> Old", { exact: true }); - await expect(sortValue).toBeVisible(); + const testimonyPage = new TestimonyPage(page) + await testimonyPage.sort("Sort by New -> Old") + const sortValue = page.getByText("Sort by New -> Old", { exact: true }) + await expect(sortValue).toBeVisible() }) test("should sort by old -> new", async ({ page }) => { - const testimonyPage = new TestimonyPage(page); - await testimonyPage.sort("Sort by Old -> New"); - const sortValue = page.getByText("Sort by Old -> New", { exact: true }); - await expect(sortValue).toBeVisible(); + const testimonyPage = new TestimonyPage(page) + await testimonyPage.sort("Sort by Old -> New") + const sortValue = page.getByText("Sort by Old -> New", { exact: true }) + await expect(sortValue).toBeVisible() }) }) From b3b1925a96ea8ec208d960ab77f711bdddf7adba Mon Sep 17 00:00:00 2001 From: nssensalo Date: Tue, 16 Dec 2025 23:12:43 -0800 Subject: [PATCH 3/5] edit profile page playright tests --- playwright.config.ts | 16 +- tests/e2e/editProfile.spec.ts | 269 ++++++++++++++++++++++ tests/e2e/page_objects/createTestimony.ts | 28 +++ tests/e2e/page_objects/editProfilePage.ts | 45 ++++ tests/e2e/showCreatedTestimony.spec.ts | 150 ++++++++++++ tests/e2e/utils/goto.ts | 13 ++ tests/e2e/utils/login.ts | 69 ++++++ tests/e2e/utils/removeSpecialChar.ts | 17 ++ 8 files changed, 599 insertions(+), 8 deletions(-) create mode 100644 tests/e2e/editProfile.spec.ts create mode 100644 tests/e2e/page_objects/createTestimony.ts create mode 100644 tests/e2e/page_objects/editProfilePage.ts create mode 100644 tests/e2e/showCreatedTestimony.spec.ts create mode 100644 tests/e2e/utils/goto.ts create mode 100644 tests/e2e/utils/login.ts create mode 100644 tests/e2e/utils/removeSpecialChar.ts diff --git a/playwright.config.ts b/playwright.config.ts index f58a111fe..7cd13df76 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -19,7 +19,7 @@ export default defineConfig({ /* Retry on CI only */ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, + workers: process.env.CI ? 1 : 1, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: "html", /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ @@ -46,15 +46,15 @@ export default defineConfig({ use: { ...devices["Desktop Firefox"] } - }, - - { - name: "webkit", - use: { - ...devices["Desktop Safari"] - } } + // { + // name: "webkit", + // use: { + // ...devices["Desktop Safari"] + // } + // } + /* Test against mobile viewports. */ // { // name: 'Mobile Chrome', diff --git a/tests/e2e/editProfile.spec.ts b/tests/e2e/editProfile.spec.ts new file mode 100644 index 000000000..8faf76aa2 --- /dev/null +++ b/tests/e2e/editProfile.spec.ts @@ -0,0 +1,269 @@ +import { test, expect, Page, Locator, Browser } from "@playwright/test" +import { EditProfilePage } from "./page_objects/editProfilePage" +import { userLogin, setupPage } from "./utils/login" +import { removeSpecialChar } from "./utils/removeSpecialChar" +import { gotoStable } from "./utils/goto" + +require("dotenv").config() + +/** + * @param USER_EMAIL + * @param USER_PASSWORD + * @returns + */ + +const USER_EMAIL = process.env.TEST_USER_USERNAME +const USER_PASSWORD = process.env.TEST_USER_PASSWORD + +test.describe.serial("Edit Page", () => { + test("Prevents User A from editing User B profile", async ({ browser }) => { + /* + Logs in user and stores session state + */ + const context = await browser.newContext() + const page = await context.newPage() + try { + let userAStateObject: { cookies: any[]; origins: any[] } //stores current user/userA sesssion state + + await userLogin(page, USER_EMAIL, USER_PASSWORD) + + const url = "http://localhost:3000/edit-profile/about-you" + await gotoStable(page, url) + + userAStateObject = await page.context().storageState() // save userA's session state + + /* + Prevents user A from manipulating url to access userB's profile + */ + const userBID = "d6ZFKTVH8i4hglyv42wz8QDaKKxw" //from test3@example.com test account on firebase + const attackUrl = `http://localhost:3000/profile?id=${userBID}` //manipulating url with userB's id + const attackPath = new RegExp(`.*\\/profile\\?id=${userBID}`) //manipulating path + + await gotoStable(page, attackUrl) + + await expect(page).not.toHaveURL(/.*\/edit-profile\/about-you/) //asserts that is doesn't navigate to userB's edit profile page + + await page.waitForURL(attackPath) + await expect(page).toHaveURL(attackPath) // asserts that the manipualted URL path is the view profile page for user B + + await expect(page.getByText("404")).toBeVisible() // asserts that the page shows error messages + await expect(page.getByText("This page could not be found")).toBeVisible() + } finally { + await context.close() + } + }) + + test("Assures user can add to blank profile page", async ({ browser }) => { + /* + Fills blank fields with same sample data and + confirms edits were saved. + */ + const { page, context } = await setupPage(browser) + + const editPage = new EditProfilePage(page) + + await userLogin(page, USER_EMAIL, USER_PASSWORD) + + const url = "http://localhost:3000/edit-profile/about-you" + await gotoStable(page, url) + + //sample input + const sampleName = "John Doe" + const sampleText = "I ❤️ politics" + const sampleTwitter = "jdoe" + const sampleLinkedIn = "https://www.linkedIn.com/in/jdoe" + const sampleRepresentative = "Alan Silvia" + const sampleSenator = "Adam Gomez" + + await expect(page).toHaveURL(/.*\/edit-profile\/about-you/) + + // clear and fill with sample data + // await expect(editPage.editName.first()).toBeVisible() + await editPage.editName.fill(sampleName, { timeout: 50000 }) //fickle + await editPage.editWriteAboutSelf.clear() + await editPage.editWriteAboutSelf.fill(sampleText) + await editPage.editTwitterUsername.clear() + await editPage.editTwitterUsername.fill(sampleTwitter) + await editPage.editLinkedInUrl.clear() + await editPage.editLinkedInUrl.fill(sampleLinkedIn) + await editPage.editRepresentative.pressSequentially(sampleRepresentative, { + delay: 50 + }) + await page.keyboard.press("Enter") + await editPage.editSenator.pressSequentially(sampleSenator, { delay: 50 }) + await editPage.editSenator.click() + + // save + await page.keyboard.press("Enter") + + //assertion: Assure it saved + //name + await expect(page.getByText(sampleName)).toHaveText(sampleName, { + timeout: 50000 + }) //fickle + //(about) text + await expect(page.getByText(sampleText, { exact: true })).toHaveText( + sampleText + ) + //representative + const repRow = page.locator("div", { + has: page.locator(".main-text", { hasText: "Representative" }) + }) + await expect(repRow.getByText(sampleRepresentative)).toHaveText( + sampleRepresentative + ) + //senator + const senRow = page + .locator(".main-text", { hasText: "Senator" }) + .locator('xpath=following-sibling::p[contains(@class,"sub-text")]') + + await removeSpecialChar(senRow, sampleSenator) + + //twitter + const sampleTwitterNewPage = "jdoe" + + const twitterLink = page.locator('a:has(img[alt="Twitter"])') + + await expect(twitterLink).toHaveAttribute( + "href", + new RegExp(sampleTwitterNewPage) + ) + }) + test("User can enable and disable notification settings", async ({ + browser + }) => { + const { page, context } = await setupPage(browser) + const editPage = new EditProfilePage(page) + + await userLogin(page, USER_EMAIL, USER_PASSWORD) + + const url = "http://localhost:3000/edit-profile/about-you" + await gotoStable(page, url) + + /** + * @param page + * @param saveButton The save button in notifications + */ + const saveButton = page.getByRole("button", { name: "Save", exact: true }) + + async function toggleEnabledButton(page: Page, saveButton: Locator) { + const enableButton = page.getByRole("button", { name: "Enable" }) + const disableButton = page.getByRole("button", { name: "Enabled" }) + const settingsButton = page.getByRole("button", { + name: "settings", + exact: true + }) + + await expect(settingsButton).toBeEnabled() + + await settingsButton.click() + + if (await enableButton.isVisible()) { + await enableButton.click() + + await disableButton.isVisible() + + await saveButton.click() + + //verify + await page + .getByRole("button", { name: "settings", exact: true }) + .click() + + await disableButton.isVisible() + } else if (await disableButton.isVisible()) { + await disableButton.click() + + await enableButton.isVisible() + + await saveButton.click() + + //verify + + await page + .getByRole("button", { name: "settings", exact: true }) + .click() + + expect(enableButton).toBeVisible({ timeout: 15000 }) + } else { + throw new Error("Unable to locate enable/d button") + } + } + try { + await toggleEnabledButton(page, saveButton) + } finally { + await context.close() + } + }) + + test("User can toggle private/public settings and save them", async ({ + browser + }) => { + const { page, context } = await setupPage(browser) + + await userLogin(page, USER_EMAIL, USER_PASSWORD) + + const url = "http://localhost:3000/edit-profile/about-you" + await gotoStable(page, url) + + const saveButton = page.getByRole("button", { name: "Save", exact: true }) + + /** + * @param page + * @param saveButton The save button in notifications + */ + async function togglePrivacyButton(page: Page, saveButton: Locator) { + const makePublic = page.getByRole("button", { name: "Make Public" }) + const makePrivate = page.getByRole("button", { name: "Make Private" }) + const settingsButton = page.getByRole("button", { + name: "settings", + exact: true + }) + + await expect(settingsButton).toBeEnabled({ timeout: 10000 }) + + await settingsButton.click() + + // ----default is private--- + + // test button toggling + if (await makePublic.isVisible({ timeout: 20000 })) { + await makePublic.click() + + await makePrivate.isVisible({ timeout: 20000 }) + } else if (await makePrivate.isVisible({ timeout: 20000 })) { + await makePrivate.click() + + await makePublic.isVisible({ timeout: 20000 }) + } else { + throw Error( + "Public/Private Button is not visable or toggling correctly" + ) + } + + // Verify public/private state gets saved + if (await makePrivate.isVisible({ timeout: 20000 })) { + await saveButton.click() + + await page + .getByRole("button", { name: "settings", exact: true }) + .click() + + await expect(makePrivate).toBeVisible({ timeout: 20000 }) + } else if (await makePublic.isVisible({ timeout: 20000 })) { + await saveButton.click() + + await page + .getByRole("button", { name: "settings", exact: true }) + .click() + + await expect(makePublic).toBeVisible({ timeout: 10000 }) + } + } + try { + await togglePrivacyButton(page, saveButton) + } finally { + await context.close() + } + }) +}) diff --git a/tests/e2e/page_objects/createTestimony.ts b/tests/e2e/page_objects/createTestimony.ts new file mode 100644 index 000000000..4435080cd --- /dev/null +++ b/tests/e2e/page_objects/createTestimony.ts @@ -0,0 +1,28 @@ +// import { expect, type Locator, type Page } from "@playwright/test" + +// export class CreateTestimony { +// readonly page: Page +// readonly toBills: Locator +// readonly next: Locator +// readonly writeTestimony: Locator + +// constructor(page: Page) { +// this.page = page +// this.toBills = page.getByRole("link", { name: "Browse Bills" }).first() +// this.next = page.getByRole("button", { name: "Next >>" }) +// this.writeTestimony = page.getByPlaceholder("Add you testimony here") +// } + +// async removePresetCourtfilter() { +// const activeCourtCheckbox = this.page +// .locator("div, span, label", { has: this.page.getByText(/Court/i) }) +// .getByRole("checkbox", { checked: true }) + +// await activeCourtCheckbox.click({ noWaitAfter: true, timeout: 0 }) + +// await this.page +// .getByText("Results") +// .first() +// .waitFor({ state: "visible", timeout: 60000 }) +// } +// } diff --git a/tests/e2e/page_objects/editProfilePage.ts b/tests/e2e/page_objects/editProfilePage.ts new file mode 100644 index 000000000..092284165 --- /dev/null +++ b/tests/e2e/page_objects/editProfilePage.ts @@ -0,0 +1,45 @@ +import { expect, type Locator, type Page } from "@playwright/test" + +export class EditProfilePage { + readonly page: Page + readonly accessDeniedMessage: Locator + readonly errorHeading: Locator + readonly saveChangesButton: Locator + readonly editProfileButton: Locator + readonly editName: Locator + readonly editWriteAboutSelf: Locator + readonly editTwitterUsername: Locator + readonly editLinkedInUrl: Locator + readonly editSenator: Locator + readonly editRepresentative: Locator + readonly locateTestimony: Locator + + constructor(page: Page) { + this.page = page + this.accessDeniedMessage = page.getByText("This page could not be found") + this.errorHeading = page.getByText(/404 | 403/) + this.saveChangesButton = page.getByRole("button", { + name: "Save Personal Information" + }) + + this.editProfileButton = page.getByRole("button", { name: "Edit Profile" }) + this.editName = page.getByLabel("Full Name", { exact: true }) + + this.editWriteAboutSelf = page.getByPlaceholder( + "Write something about yourself" + ) + this.editTwitterUsername = page.getByPlaceholder("Twitter Username") + this.editLinkedInUrl = page.getByPlaceholder("LinkedIn Url") + this.editSenator = page.locator('#react-select-3-input[role="combobox"]') + + this.editRepresentative = page.locator( + '#react-select-2-input[role="combobox"]' + ) + + this.locateTestimony = page.getByRole("tab", { name: "Testimonies" }) + } + + async goto() { + await this.page.goto("http://localhost:3000") + } +} diff --git a/tests/e2e/showCreatedTestimony.spec.ts b/tests/e2e/showCreatedTestimony.spec.ts new file mode 100644 index 000000000..a01f5a1d1 --- /dev/null +++ b/tests/e2e/showCreatedTestimony.spec.ts @@ -0,0 +1,150 @@ +// import { test, expect, Page, Locator, Browser } from "@playwright/test" +// import { CreateTestimony } from "./page_objects/createTestimony" +// import { userLogin, setupPage } from "./utils/login" +// import { getByRole } from "@testing-library/dom" +// import { regex } from "react-admin" +// require("dotenv").config() + +// /** +// * @param USER_EMAIL +// * @param USER_PASSWORD +// * @returns +// */ + +// const USER_EMAIL = process.env.TEST_USER_USERNAME +// const USER_PASSWORD = process.env.TEST_USER_PASSWORD + +// test.describe +// .serial("Create testimony, show on Browse Testimonies , written by user when setting is private", () => { + +// test("Creates testinomy", async ({ browser }) => { +// const context = await browser.newContext() +// const page = await context.newPage() + +// await userLogin(page, USER_EMAIL, USER_PASSWORD) + +// await page.goto("http://localhost:3000/edit-profile/about-you") + +// const createTestimony = new CreateTestimony(page) + +// await createTestimony.toBills.click() + +// await createTestimony.removePresetCourtfilter() + +// const isBill =page.getByRole('link', { name: /^Court/ }).last(); + +// const billName = (await isBill.textContent({timeout:30000}))! + +// console.log({billName}, typeof(billName)) + +// const billPattern = /.*?(H\.\d+)/; + +// const matchResult = billName.match(billPattern); + +// let billNumber: string; + +// if (matchResult && matchResult[1]) { +// billNumber = matchResult[1]; +// console.log(`Extracted Bill Number: ${billNumber}`); +// } else { +// billNumber = 'NOT_FOUND'; +// console.warn("Could not find the bill number pattern (H.####) in the link name."); +// } + +// await isBill.click() + +// expect(page.getByText(billName)) + +// page.getByRole('link', {name:'More Details'}).click() + +// page.getByRole('link', {name: /^Add Testimony for/}).click() // try with exact name, difficulty fiinding + +// //click oppose +// const opposeButton = page.locator('div').filter( {hasText: /^Oppose$/ }).first() +// await expect(opposeButton).toBeVisible({ timeout: 15000 }); +// await opposeButton.click(); + +// page.waitForTimeout(200) + +// //click next +// await expect(createTestimony.next).toBeEnabled({ timeout: 15000 }); +// await createTestimony.next.click() + +// //select legislators +// expect(page.getByText('Select Your Legislators')).toBeVisible({timeout:30000}) + +// //click next +// await page.getByRole('button',{name: 'Next >>'}).click() + +// // expect(page.getByPlaceholder('Write Your Testimony Email'))//failing + +// //write testimony +// await page.getByPlaceholder('Add your testimony here').click() +// const sampleTestimony = 'This is my sample testimony.' +// page.waitForTimeout(200) +// await createTestimony.writeTestimony.fill(sampleTestimony) + +// //Next +// await page.getByRole('button',{name: 'Next >>'}).click() +// // expect(createTestimony.next).toBeEnabled()// do i need this? + +// expect(createTestimony).toContain('Confirm and Send') + +// page.getByRole('button', { name: 'Publish'}).click() + +// page.getByRole('button', { name:'Finished! Back to Bill'}).click() + +// //verify privacy and testimony on profile page + +// await page.goto("http://localhost:3000/edit-profile/about-you") + +// const settingsButton = page.getByRole("button", { +// name: "settings", +// exact: true +// }) + +// await expect(settingsButton).toBeEnabled({ timeout: 10000 }) + +// await settingsButton.click() +// //Verify privacy setting is private +// const isPrivate = page.getByText('Your profile is currently private') + +// await isPrivate.isVisible({timeout:30000}) + +// page.getByRole( 'button', { name: 'Cancel'}).click() + +// //verify testimony present on profile page +// page.getByRole('tab', { name:'Testimonies'}).click() + +// expect(createTestimony).toContain(billName) + +// //veify testimony present on Browse Testimony page +// await createTestimony.toBills.click() + +// const sectionLocator = page.locator('.px-4 > .py-3'); + +// const targetSection = sectionLocator.filter({ +// hasText: billName +// }); + +// await targetSection.getByRole('link', { name: 'More details' }).click(); + +// //verify Written By anonymous + +// const isAnonymouseOne = page.getByText('Anonymous opposes this policy') + +// await expect(isAnonymouseOne).toBeVisible() + +// await createTestimony.toBills.click() + +// const isAnonymouseTwo = page.getByRole('link', { name: `Profile Icon Written by anonymous Oppose Bill #${billName} An Act relative to`}) + +// await expect(isAnonymouseTwo).toBeVisible() + +// page.close() + +// // page.getByRole('heading',{ name:'Published Testimonies'}) + +// }) + +// }) diff --git a/tests/e2e/utils/goto.ts b/tests/e2e/utils/goto.ts new file mode 100644 index 000000000..cbc07822d --- /dev/null +++ b/tests/e2e/utils/goto.ts @@ -0,0 +1,13 @@ +import { Locator, type Page } from "@playwright/test" + +export async function gotoStable(page: Page, url: string) { + try { + await page.goto(url, { waitUntil: "domcontentloaded", timeout: 60_000 }) + } catch (e: any) { + // Firefox aborts when a redirect/navigation interrupts the request + const msg = String(e?.message ?? e) + if (!msg.includes("NS_BINDING_ABORTED")) throw e + } + + await page.waitForLoadState("domcontentloaded", { timeout: 60_000 }) +} diff --git a/tests/e2e/utils/login.ts b/tests/e2e/utils/login.ts new file mode 100644 index 000000000..f8d227f64 --- /dev/null +++ b/tests/e2e/utils/login.ts @@ -0,0 +1,69 @@ +import { expect, Page, Browser } from "@playwright/test" + +//login helper function +export async function userLogin( + page: Page, + USER_EMAIL: string | undefined, + USER_PASSWORD: string | undefined +) { + if (!USER_EMAIL || !USER_PASSWORD) { + throw new Error("Email or password are not defined.") + } + + await page.goto("http://localhost:3000", { + waitUntil: "commit", + timeout: 60_000 + }) + + const loginButton = page.getByRole("button", { name: "Log in / Sign up" }) + + const loggedInMarker = page.locator('a:has(img[alt="profileMenu"])') + + const state = await expect + .poll( + async () => { + if (await loggedInMarker.isVisible().catch(() => false)) + return "logged-in" + const count = await loginButton.count().catch(() => 0) + if (count > 0) return "needs-login" + return "loading" + }, + { timeout: 30_000 } + ) + .not.toBe("loading") + + // If already logged in, do nothing + if (await loggedInMarker.isVisible().catch(() => false)) return + + await loginButton.click() + + await page + .getByLabel("Sign Up or Sign In") + .getByRole("button", { name: "Sign In", exact: true }) + .click() + + const signInForm = page.getByRole("dialog", { name: "Sign In" }) + const emailInput = signInForm.locator('input[name="email"]') + const passwordInput = signInForm.locator('input[name="password"]') + const signInButton = signInForm.locator('button[type="submit"]') + + await emailInput.waitFor({ state: "visible", timeout: 5000 }) + await emailInput.fill(USER_EMAIL) + + await passwordInput.waitFor({ state: "visible", timeout: 5000 }) + await passwordInput.fill(USER_PASSWORD) + + await expect(signInButton).toBeEnabled({ timeout: 5000 }) + + await signInButton.click() +} + +//setup helper function +export async function setupPage( + browser: Browser, + state?: { cookies: any[]; origins: any[] } +) { + const context = await browser.newContext({ storageState: state }) + const page = await context.newPage() + return { page, context } +} diff --git a/tests/e2e/utils/removeSpecialChar.ts b/tests/e2e/utils/removeSpecialChar.ts new file mode 100644 index 000000000..f8912f8df --- /dev/null +++ b/tests/e2e/utils/removeSpecialChar.ts @@ -0,0 +1,17 @@ +import { expect, type Locator } from "@playwright/test" + +export async function removeSpecialChar(locator: Locator, sample: string) { + const expected = sample.normalize("NFD").replace(/[\u0300-\u036f]/g, "") + + const removing = await expect + .poll( + async () => { + const actual = (await locator.textContent()) ?? "" + return actual.normalize("NFD").replace(/[\u0300-\u036f]/g, "") // strips accents from page text too + }, + { timeout: 30_000 } + ) + .toBe(expected) + + return removing +} From 1ce59f10de60157185881e396dc3d18d7344dcfe Mon Sep 17 00:00:00 2001 From: nssensalo Date: Tue, 16 Dec 2025 23:37:57 -0800 Subject: [PATCH 4/5] switched to workspace - trying to solve CI/CD vercel node version error --- tests/e2e/editProfile.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e/editProfile.spec.ts b/tests/e2e/editProfile.spec.ts index 8faf76aa2..8067bef96 100644 --- a/tests/e2e/editProfile.spec.ts +++ b/tests/e2e/editProfile.spec.ts @@ -129,6 +129,7 @@ test.describe.serial("Edit Page", () => { new RegExp(sampleTwitterNewPage) ) }) + test("User can enable and disable notification settings", async ({ browser }) => { From bf386b64095691f6e0e31031026e5f814f8417c4 Mon Sep 17 00:00:00 2001 From: nssensalo Date: Sat, 20 Dec 2025 10:54:50 -0800 Subject: [PATCH 5/5] minor changes, and trying rebase to fix vercel node version error --- tests/e2e/editProfile.spec.ts | 41 +++++++++++++++-------- tests/e2e/page_objects/editProfilePage.ts | 2 +- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/tests/e2e/editProfile.spec.ts b/tests/e2e/editProfile.spec.ts index 8067bef96..9c53d134e 100644 --- a/tests/e2e/editProfile.spec.ts +++ b/tests/e2e/editProfile.spec.ts @@ -53,7 +53,9 @@ test.describe.serial("Edit Page", () => { } }) - test("Assures user can add to blank profile page", async ({ browser }) => { + test.only("Assures user can add to blank profile page", async ({ + browser + }) => { /* Fills blank fields with same sample data and confirms edits were saved. @@ -73,13 +75,19 @@ test.describe.serial("Edit Page", () => { const sampleTwitter = "jdoe" const sampleLinkedIn = "https://www.linkedIn.com/in/jdoe" const sampleRepresentative = "Alan Silvia" + // const sampleRepresentative = "Aaron L. Saunders" //NOTE: names with middle initials need a space after initial const sampleSenator = "Adam Gomez" - - await expect(page).toHaveURL(/.*\/edit-profile\/about-you/) + // const sampleSenator = "Bruce E. Tarr" // clear and fill with sample data - // await expect(editPage.editName.first()).toBeVisible() - await editPage.editName.fill(sampleName, { timeout: 50000 }) //fickle + await expect(editPage.editName).toHaveCount(1, { timeout: 100_000 }) + await expect(editPage.editName).toBeVisible({ timeout: 50_000 }) + await expect(editPage.editName).toBeEnabled() + await editPage.editName.waitFor({ state: "attached", timeout: 50_000 }) + await editPage.editName.click() + await editPage.editName.fill("") + await editPage.editName.fill(sampleName) + await expect(editPage.editName).toHaveValue(sampleName, { timeout: 50_000 }) await editPage.editWriteAboutSelf.clear() await editPage.editWriteAboutSelf.fill(sampleText) await editPage.editTwitterUsername.clear() @@ -89,34 +97,37 @@ test.describe.serial("Edit Page", () => { await editPage.editRepresentative.pressSequentially(sampleRepresentative, { delay: 50 }) + await page.waitForTimeout(50) await page.keyboard.press("Enter") await editPage.editSenator.pressSequentially(sampleSenator, { delay: 50 }) - await editPage.editSenator.click() + await page.waitForTimeout(50) + await page.keyboard.press("Enter") // save - await page.keyboard.press("Enter") + await editPage.saveChangesButton.click() + await page.keyboard.press("Enter") //save-button activated -rerouting to profile page //assertion: Assure it saved //name await expect(page.getByText(sampleName)).toHaveText(sampleName, { timeout: 50000 - }) //fickle + }) //(about) text await expect(page.getByText(sampleText, { exact: true })).toHaveText( sampleText ) //representative - const repRow = page.locator("div", { - has: page.locator(".main-text", { hasText: "Representative" }) - }) - await expect(repRow.getByText(sampleRepresentative)).toHaveText( + const repRow = page.locator( + 'div:has(> .main-text:has-text("Representative"))' + ) + await expect(repRow.locator("p.sub-text")).toContainText( sampleRepresentative ) + //senator const senRow = page .locator(".main-text", { hasText: "Senator" }) .locator('xpath=following-sibling::p[contains(@class,"sub-text")]') - await removeSpecialChar(senRow, sampleSenator) //twitter @@ -128,8 +139,10 @@ test.describe.serial("Edit Page", () => { "href", new RegExp(sampleTwitterNewPage) ) + + await context.close() }) - + test("User can enable and disable notification settings", async ({ browser }) => { diff --git a/tests/e2e/page_objects/editProfilePage.ts b/tests/e2e/page_objects/editProfilePage.ts index 092284165..17088fa80 100644 --- a/tests/e2e/page_objects/editProfilePage.ts +++ b/tests/e2e/page_objects/editProfilePage.ts @@ -23,7 +23,7 @@ export class EditProfilePage { }) this.editProfileButton = page.getByRole("button", { name: "Edit Profile" }) - this.editName = page.getByLabel("Full Name", { exact: true }) + this.editName = page.locator('input[name="fullName"]') this.editWriteAboutSelf = page.getByPlaceholder( "Write something about yourself"