From 423361938cd67a39b4a9c7e2b910c5e84978f12f Mon Sep 17 00:00:00 2001 From: MartinBraquet Date: Wed, 29 Jul 2026 21:55:51 +0200 Subject: [PATCH] Refactor reusable components and improve test utilities: extract `clickCheckbox` and `optionChip` helpers, streamline personality slider updates, and integrate missing `data-testid` attributes for better test targeting. --- android/app/build.gradle | 4 +- backend/api/package.json | 2 +- playwright.config.ts | 2 +- tests/e2e/web/pages/peoplePage.ts | 103 ++++++-- tests/e2e/web/pages/profilePage.ts | 12 +- tests/e2e/web/pages/signUpPage.ts | 240 ++++++------------ tests/e2e/web/utils/optionChip.ts | 31 +++ web/components/bio/editable-bio.tsx | 2 +- web/components/filters/search.tsx | 1 + .../messaging/send-message-button.tsx | 1 + web/components/profile-about.tsx | 1 + 11 files changed, 204 insertions(+), 195 deletions(-) create mode 100644 tests/e2e/web/utils/optionChip.ts diff --git a/android/app/build.gradle b/android/app/build.gradle index 1df981e8..972b49ce 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -11,8 +11,8 @@ android { applicationId "com.compassconnections.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 141 - versionName "1.33.0" + versionCode 142 + versionName "1.34.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/backend/api/package.json b/backend/api/package.json index f722e1fe..a58c8e37 100644 --- a/backend/api/package.json +++ b/backend/api/package.json @@ -1,6 +1,6 @@ { "name": "@compass/api", - "version": "1.53.0", + "version": "1.54.0", "private": true, "description": "Backend API endpoints", "main": "src/serve.ts", diff --git a/playwright.config.ts b/playwright.config.ts index 95359cfb..4558c51d 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -65,6 +65,6 @@ export default defineConfig({ ], timeout: 120000, expect: { - timeout: 60000, + timeout: 20000, }, }) diff --git a/tests/e2e/web/pages/peoplePage.ts b/tests/e2e/web/pages/peoplePage.ts index b2c5bb42..1f67d417 100644 --- a/tests/e2e/web/pages/peoplePage.ts +++ b/tests/e2e/web/pages/peoplePage.ts @@ -14,6 +14,7 @@ import { } from 'common/choices' import {MinMaxNumbers} from '../utils/accountInformation' +import {optionChip} from '../utils/optionChip' export type BackgroundFilter = { location?: string @@ -94,6 +95,8 @@ export class PeoplePage { private readonly searchBox: Locator private readonly savedPeopleButton: Locator private readonly profileCount: Locator + private readonly filtersButton: Locator + private readonly filtersPanel: Locator private readonly resetFilters: Locator private readonly yourFiltersCheckbox: Locator private readonly incompleteProfilesCheckbox: Locator @@ -130,6 +133,7 @@ export class PeoplePage { private readonly profileStar: Locator private readonly profileMessage: Locator private readonly messageInput: Locator + private readonly messageSubmit: Locator private readonly profileName: Locator private readonly profileAgeGender: Locator private readonly profileSeeking: Locator @@ -141,32 +145,34 @@ export class PeoplePage { this.searchBox = page.getByRole('textbox', {name: 'Search anything...'}) this.savedPeopleButton = page.getByRole('button', {name: 'Saved People'}) this.profileCount = page.getByTestId('people-profile-count') + this.filtersButton = page.getByTestId('open-filters-button') + this.filtersPanel = page.getByTestId('people-filters') this.resetFilters = page.getByRole('button', {name: 'Reset filters'}) this.yourFiltersCheckbox = page.getByText('Your filters', {exact: true}) this.incompleteProfilesCheckbox = page.getByText('Include incomplete profiles', {exact: true}) - this.connectionTypeDropdown = page.getByRole('button', {name: 'Any connection'}) - this.locationDropdown = page.getByRole('button', {name: 'Living anywhere'}) - this.ageRangeDropdown = page.getByRole('button', {name: 'Any age'}) - this.genderDropdown = page.getByRole('button', {name: 'Any gender'}) + this.connectionTypeDropdown = page.getByRole('button', {name: 'Connection'}) + this.locationDropdown = page.getByRole('button', {name: 'Location'}) + this.ageRangeDropdown = page.getByRole('button', {name: 'Age'}) + this.genderDropdown = page.getByRole('button', {name: 'Gender'}) this.backgroundDropdown = page.getByRole('button', {name: 'Background'}) - this.backgroundLocation = page.getByRole('button', {name: 'Grew up anywhere'}) - this.backgroundEducation = page.getByText('Any education', {exact: true}) - this.backgroundWork = page.getByText('Any work', {exact: true}) + this.backgroundLocation = page.getByRole('button', {name: 'Grew up'}) + this.backgroundEducation = page.getByText('Education', {exact: true}) + this.backgroundWork = page.getByText('Work', {exact: true}) this.lifestyleDropdown = page.getByRole('button', {name: 'Lifestyle'}) - this.lifestyleInterests = page.getByRole('button', {name: 'Any interests'}) - this.lifestyleCauses = page.getByRole('button', {name: 'Any causes'}) - this.lifestyleDiet = page.getByRole('button', {name: 'Any diet'}) - this.lifestyleAlcohol = page.getByRole('button', {name: 'Any drinks'}) + this.lifestyleInterests = page.getByRole('button', {name: 'Interests'}) + this.lifestyleCauses = page.getByRole('button', {name: 'Causes'}) + this.lifestyleDiet = page.getByRole('button', {name: 'Diet'}) + this.lifestyleAlcohol = page.getByRole('button', {name: 'Drinks'}) this.lifestyleSmoker = page.getByTestId('lifestyle-smoker') - this.lifestylePsychedelics = page.getByRole('button', {name: 'Any psychedelics'}) - this.lifestyleCannabis = page.getByRole('button', {name: 'Any cannabis'}) - this.lifestyleLanguages = page.getByRole('button', {name: 'Any language'}) + this.lifestylePsychedelics = page.getByRole('button', {name: 'Psychedelics'}) + this.lifestyleCannabis = page.getByRole('button', {name: 'Cannabis'}) + this.lifestyleLanguages = page.getByRole('button', {name: 'Language'}) this.valuesAndBeliefsDropdown = page.getByRole('button', {name: 'Values & Beliefs'}) - this.valuesAndBeliefsPolitics = page.getByRole('button', {name: 'Any politics'}) - this.valuesAndBeliefsReligion = page.getByRole('button', {name: 'Any religion'}) + this.valuesAndBeliefsPolitics = page.getByRole('button', {name: 'Politics'}) + this.valuesAndBeliefsReligion = page.getByRole('button', {name: 'Religion'}) this.personalityDropdown = page.getByRole('button', {name: 'Personality'}) - this.personalityMbti = page.getByRole('button', {name: 'Any MBTI'}) - this.personalityBigFive = page.getByRole('button', {name: 'Any Big 5'}) + this.personalityMbti = page.getByRole('button', {name: 'MBTI'}) + this.personalityBigFive = page.getByRole('button', {name: 'Big 5'}) this.advancedDropdown = page.getByRole('button', {name: 'Advanced'}) this.advancedActive = page.getByTestId('advanced-active') this.advancedPhotos = page.getByText('Photos', {exact: true}) @@ -177,6 +183,7 @@ export class PeoplePage { this.profileStar = page.getByTestId('star-profile-button') this.profileMessage = page.getByTestId('message-profile-button') this.messageInput = page.locator('.tiptap') + this.messageSubmit = page.getByTestId('send-message-submit') this.profileName = page.getByTestId('people-profile-name') this.profileAgeGender = page.getByTestId('people-profile-age-gender') this.profileSeeking = page.getByTestId('people-profile-seeking') @@ -249,11 +256,16 @@ export class PeoplePage { } } + /** See {@link optionChip} — filter options are chips, not clickable checkboxes. */ + optionChip(label: string): Locator { + return optionChip(this.page, label) + } + async selectOption(trigger: Locator, label: string) { await expect(trigger).toBeVisible() await trigger.click() - const option = this.page.getByLabel(label, {exact: true}) + const option = this.optionChip(label) await expect(option).toBeVisible() await option.click() } @@ -274,22 +286,44 @@ export class PeoplePage { await this.page.waitForTimeout(1000) } + /** + * The filters panel starts closed, and the "Filters" button toggles it — so clicking it + * unconditionally would close an already-open panel. Everything below that reaches into the panel + * calls this first; it is a no-op when the panel is already open. + */ + async openFilters() { + if (await this.filtersPanel.isVisible()) return + await expect(this.filtersButton).toBeVisible() + await this.filtersButton.click() + await expect(this.filtersPanel).toBeVisible() + } + + async closeFilters() { + if (!(await this.filtersPanel.isVisible())) return + await this.filtersButton.click() + await expect(this.filtersPanel).not.toBeVisible() + } + async resetFilter() { + await this.openFilters() await expect(this.resetFilters).toBeVisible() await this.resetFilters.click() } async setYourFilters() { + await this.openFilters() await expect(this.yourFiltersCheckbox).toBeVisible() await this.yourFiltersCheckbox.click() } async setIncludeIncompleteProfiles() { + await this.openFilters() await expect(this.incompleteProfilesCheckbox).toBeVisible() await this.incompleteProfilesCheckbox.click() } async setConnectionTypeFilter(connectionType: ConnectionTypeTuple) { + await this.openFilters() await this.selectOption(this.connectionTypeDropdown, connectionType[0]) // await expect(this.connectionTypeDropdown).toBeVisible() // await this.connectionTypeDropdown.click() @@ -298,6 +332,7 @@ export class PeoplePage { } async setLocationFilter(location: string) { + await this.openFilters() await expect(this.locationDropdown).toBeVisible() await this.locationDropdown.click() await expect(this.page.getByRole('textbox', {name: 'Search city...'})).toBeVisible() @@ -305,12 +340,14 @@ export class PeoplePage { } async setAgeRangeFilter(ageRange: MinMaxNumbers) { + await this.openFilters() await expect(this.ageRangeDropdown).toBeVisible() await this.ageRangeDropdown.click() await this.sliderHelper(ageRange) } async setGenderTypeFilter(genderType: InterestedInGenderTuple) { + await this.openFilters() await this.selectOption(this.genderDropdown, genderType[0]) // await expect(this.genderDropdown).toBeVisible() // await this.genderDropdown.click() @@ -319,6 +356,7 @@ export class PeoplePage { } async setBackgroundFilter(background: BackgroundFilter) { + await this.openFilters() await expect(this.backgroundDropdown).toBeVisible() await this.backgroundDropdown.click() if (background.location) { @@ -330,19 +368,22 @@ export class PeoplePage { if (background.education) { await expect(this.backgroundEducation).toBeVisible() await this.backgroundEducation.click() - await expect(this.page.getByLabel(background.education[0], {exact: true})).toBeVisible() - await this.page.getByLabel(background.education[0], {exact: true}).click() + const education = this.optionChip(background.education[0]) + await expect(education).toBeVisible() + await education.click() } if (background.work) { await expect(this.backgroundWork).toBeVisible() await this.backgroundWork.click() - await expect(this.page.getByLabel(background.work, {exact: true})).toBeVisible() - await this.page.getByLabel(background.work, {exact: true}).click() + const work = this.optionChip(background.work) + await expect(work).toBeVisible() + await work.click() } } async setLifestyleFilter(lifestyle: LifestyleFilter) { + await this.openFilters() await expect(this.lifestyleDropdown).toBeVisible() await this.lifestyleDropdown.click() @@ -370,6 +411,7 @@ export class PeoplePage { } async setValuesAndBeliefsFilter(values: BeliefsFilter) { + await this.openFilters() await expect(this.valuesAndBeliefsDropdown).toBeVisible() await this.valuesAndBeliefsDropdown.click() @@ -380,6 +422,7 @@ export class PeoplePage { } async setPersonalityFilter(personality: PersonalityFilter) { + await this.openFilters() await expect(this.personalityDropdown).toBeVisible() await this.personalityDropdown.click() @@ -421,6 +464,7 @@ export class PeoplePage { } async setAdvancedFilter(advanced: AdvancedFilter) { + await this.openFilters() await expect(this.advancedDropdown).toBeVisible() await this.advancedDropdown.click() @@ -436,6 +480,7 @@ export class PeoplePage { } async setDisplayFilter(display: DisplayFilter) { + await this.openFilters() await expect(this.displayDropdown).toBeVisible() await this.displayDropdown.click() @@ -489,13 +534,21 @@ export class PeoplePage { for (let i = 0; i < profiles.length; i++) { const profileName = await profiles[i].getByTestId('people-profile-name').textContent() if (profileName?.toLowerCase() === displayName.toLowerCase()) { - await profiles[i].getByTestId('message-profile-button').click() + const card = profiles[i] + // Card actions are revealed on hover (opacity-0 until lg:group-hover), so hover the card + // before reaching for the button rather than relying on the click's implicit hover. + await card.hover() + await card.getByTestId('message-profile-button').click() await expect(this.messageInput).toBeVisible() await this.messageInput.fill(message) - await this.page.getByTestId('conversation-message-submit').click() + // The modal composer passes `hideSubmitButton` and renders its own send control in a pinned + // footer, so this is `send-message-submit` — not the `conversation-message-submit` used by + // the in-conversation composer on the messages page. + await this.messageSubmit.click() return } } + throw new Error(`No profile card found for ${displayName}`) } async verifySavedPerson(displayName: string | null | undefined) { diff --git a/tests/e2e/web/pages/profilePage.ts b/tests/e2e/web/pages/profilePage.ts index 1d5b5d84..aad9281f 100644 --- a/tests/e2e/web/pages/profilePage.ts +++ b/tests/e2e/web/pages/profilePage.ts @@ -18,6 +18,7 @@ export class ProfilePage { private readonly disableProfileDropdownOption: Locator private readonly displayNameAndAgeSection: Locator private readonly genderLocationHightInInchesSection: Locator + private readonly heightSection: Locator private readonly headlineSection: Locator private readonly keywordsSection: Locator private readonly politicalAboutSection: Locator @@ -80,6 +81,7 @@ export class ProfilePage { this.genderLocationHightInInchesSection = page.getByTestId( 'profile-gender-location-height-inches', ) + this.heightSection = page.getByTestId('profile-height') this.politicalAboutSection = page.getByTestId('profile-content') this.religiousAboutSection = page.getByTestId('profile-content') this.interestsAboutSection = page.getByTestId('profile-content') @@ -341,10 +343,14 @@ export class ProfilePage { await expect(this.genderLocationHightInInchesSection).toContainText(location, { ignoreCase: true, }) - if (heightFeet) await expect(this.genderLocationHightInInchesSection).toContainText(heightFeet) - if (heightInches) - await expect(this.genderLocationHightInInchesSection).toContainText(heightInches) if (age) await expect(this.genderLocationHightInInchesSection).toContainText(age) + // Height left this line and now lives in the Details rail (see ProfilePrimaryInfo), which + // renders it as a single formatted string — `5' 10"` in imperial — rather than two fields. + if (heightFeet || heightInches) { + await expect(this.heightSection).toBeVisible() + if (heightFeet) await expect(this.heightSection).toContainText(`${heightFeet}'`) + if (heightInches) await expect(this.heightSection).toContainText(`${heightInches}"`) + } } async verifyEthnicityOrigin(origin: string) { diff --git a/tests/e2e/web/pages/signUpPage.ts b/tests/e2e/web/pages/signUpPage.ts index ce53777f..71085562 100644 --- a/tests/e2e/web/pages/signUpPage.ts +++ b/tests/e2e/web/pages/signUpPage.ts @@ -14,6 +14,8 @@ import { ReligionTuple, } from 'common/choices' +import {clickCheckbox, optionChip, optionChipInput} from '../utils/optionChip' + export type ChildrenExpectation = | ['Strongly against', 0] | ['Against', 1] @@ -107,7 +109,9 @@ export class SignUpPage { this.usernameField = page.getByPlaceholder('Username') this.usernameError = page.getByTestId('signup-username') this.nextButton = page.getByRole('button', {name: 'Next', exact: true}) - this.bioField = page.locator('h3:has-text("Bio") ~ * .tiptap') + // Anchored on the editor itself rather than on markup around the "Bio" heading: the heading now + // sits inside its own `data-form-section` wrapper, so it no longer has the editor as a sibling. + this.bioField = page.getByTestId('signup-bio').locator('.tiptap') this.locationField = page.getByPlaceholder('Search city...') this.headlineField = page.getByTestId('headline') this.keywordsField = page.getByTestId('keywords') @@ -259,15 +263,15 @@ export class SignUpPage { if (!interestedIn) return if (interestedIn[0] === 'Men') { await expect(this.interestedInMenCheckbox).toBeVisible() - await this.interestedInMenCheckbox.click() + await clickCheckbox(this.interestedInMenCheckbox) await expect(this.interestedInMenCheckbox).toBeChecked() } else if (interestedIn[0] === 'Women') { await expect(this.interestedInWomenCheckbox).toBeVisible() - await this.interestedInWomenCheckbox.click() + await clickCheckbox(this.interestedInWomenCheckbox) await expect(this.interestedInWomenCheckbox).toBeChecked() } else { await expect(this.interestedInOtherCheckbox).toBeVisible() - await this.interestedInOtherCheckbox.click() + await clickCheckbox(this.interestedInOtherCheckbox) await expect(this.interestedInOtherCheckbox).toBeChecked() } } @@ -284,23 +288,23 @@ export class SignUpPage { async setConnectionType(type: ConnectionTypeTuple | undefined) { if (!type) return - await expect(this.page.getByLabel(`${type[0]}`, {exact: true})).toBeVisible() - await this.page.getByLabel(`${type[0]}`, {exact: true}).click() - await expect(this.page.getByLabel(`${type[0]}`, {exact: true})).toBeChecked() + await expect(optionChip(this.page, type[0])).toBeVisible() + await optionChip(this.page, type[0]).click() + await expect(optionChipInput(this.page, type[0])).toBeChecked() } async setRelationshipStatus(status: RelationshipStatusTuple | undefined) { if (!status) return - await expect(this.page.getByLabel(`${status[0]}`, {exact: true})).toBeVisible() - await this.page.getByLabel(`${status[0]}`, {exact: true}).click() - await expect(this.page.getByLabel(`${status[0]}`, {exact: true})).toBeChecked() + await expect(optionChip(this.page, status[0])).toBeVisible() + await optionChip(this.page, status[0]).click() + await expect(optionChipInput(this.page, status[0])).toBeChecked() } async setRelationshipStyle(style: RelationshipStyleTuple | undefined) { if (!style) return - await expect(this.page.getByLabel(`${style[0]}`, {exact: true})).toBeVisible() - await this.page.getByLabel(`${style[0]}`, {exact: true}).click() - await expect(this.page.getByLabel(`${style[0]}`, {exact: true})).toBeChecked() + await expect(optionChip(this.page, style[0])).toBeVisible() + await optionChip(this.page, style[0]).click() + await expect(optionChipInput(this.page, style[0])).toBeChecked() } async fillCurrentNumberOfChildren(numberOfKids: string | undefined) { @@ -344,7 +348,7 @@ export class SignUpPage { if (isExisting) { await expect(this.page.getByRole('checkbox', {name: `${interest[i]}`})).toBeVisible() - await this.page.getByRole('checkbox', {name: `${interest[i]}`}).click() + await clickCheckbox(this.page.getByRole('checkbox', {name: `${interest[i]}`})) } else { await expect(this.addInterestsField).toBeVisible() await expect(this.addInterestsButton).toBeVisible() @@ -365,7 +369,7 @@ export class SignUpPage { if (isExisting) { await expect(this.page.getByRole('checkbox', {name: `${cause[i]}`})).toBeVisible() - await this.page.getByRole('checkbox', {name: `${cause[i]}`}).click() + await clickCheckbox(this.page.getByRole('checkbox', {name: `${cause[i]}`})) } else { await expect(this.addCausesField).toBeVisible() await expect(this.addCausesButton).toBeVisible() @@ -406,12 +410,12 @@ export class SignUpPage { if (!workArea || workArea?.length === 0) return for (let i = 0; i < workArea.length; i++) { - const checkbox = this.page.getByLabel(`${workArea[i]}`, {exact: true}) - const isExisting = (await checkbox.count()) > 0 + const chip = optionChip(this.page, workArea[i]) + const isExisting = (await chip.count()) > 0 if (isExisting) { - await expect(this.page.getByLabel(`${workArea[i]}`, {exact: true})).toBeVisible() - await this.page.getByLabel(`${workArea[i]}`, {exact: true}).click() + await expect(chip).toBeVisible() + await chip.click() await this.page.waitForTimeout(500) } else { await expect(this.addWorkAreaField).toBeVisible() @@ -420,8 +424,8 @@ export class SignUpPage { await this.addWorkAreaButton.click() await this.page.waitForTimeout(500) } - await expect(this.page.getByLabel(`${workArea[i]}`, {exact: true})).toBeVisible() - await expect(this.page.getByLabel(`${workArea[i]}`, {exact: true})).toBeChecked() + await expect(optionChip(this.page, workArea[i])).toBeVisible() + await expect(optionChipInput(this.page, workArea[i])).toBeChecked() } } @@ -431,7 +435,7 @@ export class SignUpPage { ) { if (politicalBeliefs) { await expect(this.page.getByRole('checkbox', {name: `${politicalBeliefs[0]}`})).toBeVisible() - await this.page.getByRole('checkbox', {name: `${politicalBeliefs[0]}`}).click() + await clickCheckbox(this.page.getByRole('checkbox', {name: `${politicalBeliefs[0]}`})) await expect(this.page.getByRole('checkbox', {name: `${politicalBeliefs[0]}`})).toBeChecked() } @@ -466,7 +470,7 @@ export class SignUpPage { ).toBeChecked() } else { await expect(this.page.getByRole('checkbox', {name: `${religiousBeliefs[0]}`})).toBeVisible() - await this.page.getByRole('checkbox', {name: `${religiousBeliefs[0]}`}).click() + await clickCheckbox(this.page.getByRole('checkbox', {name: `${religiousBeliefs[0]}`})) await expect(this.page.getByRole('checkbox', {name: `${religiousBeliefs[0]}`})).toBeChecked() } @@ -482,159 +486,71 @@ export class SignUpPage { await expect(this.page.getByText(`${personalityType}`, {exact: true})).toBeChecked() } - async setOpennessPersonalityValue(value: number | undefined) { + /** + * Walk a Big Five slider to `value` with arrow keys. + * + * The value label reads "–", not a number, until the trait has been touched — the form + * deliberately distinguishes "unset" from 50 (see `Big5Slider` in optional-profile-form.tsx). + * Clicking the thumb doesn't clear that: Radix only emits onValueChange when the value actually + * changes, and the thumb already sits at the default. So nudge it with a key press first, + * otherwise `parseInt('–')` is NaN, every comparison below is false, and the trait is silently + * left unset — which then shows up much later as a missing Big Five section on the profile. + */ + private async setBig5Value(slider: Locator, valueLabel: Locator, value: number | undefined) { if (!value) return - await expect(this.opennessPersonalitySlider).toBeVisible() - await expect(this.opennessPersonalityValue).toBeVisible() - await this.opennessPersonalitySlider.click() - const originalValue = await this.opennessPersonalityValue.textContent() - if (!originalValue) return - if (parseInt(originalValue) < value) { - while (true) { - const actualValue = await this.opennessPersonalityValue.textContent() - if (!actualValue) break + await expect(slider).toBeVisible() + await expect(valueLabel).toBeVisible() + await slider.click() - if (parseInt(actualValue) >= value) { - break - } - await this.page.keyboard.press('ArrowRight') - } - } - if (parseInt(originalValue) > value) { - while (true) { - const actualValue = await this.opennessPersonalityValue.textContent() - if (!actualValue) break + const readValue = async () => parseInt((await valueLabel.textContent()) ?? '') + if (isNaN(await readValue())) await slider.press('ArrowRight') - if (parseInt(actualValue) <= value) { - break - } - await this.page.keyboard.press('ArrowLeft') + const MAX_ITERATIONS = 200 + for (let i = 0; ; i++) { + if (i > MAX_ITERATIONS) { + throw new Error(`Big Five slider did not reach ${value} within ${MAX_ITERATIONS} presses`) } + const actual = await readValue() + if (isNaN(actual)) throw new Error('Big Five slider has no numeric value') + if (actual === value) return + await slider.press(actual < value ? 'ArrowRight' : 'ArrowLeft') } } + async setOpennessPersonalityValue(value: number | undefined) { + await this.setBig5Value(this.opennessPersonalitySlider, this.opennessPersonalityValue, value) + } + async setConscientiousnessPersonalityValue(value: number | undefined) { - if (!value) return - await expect(this.conscientiousnessPersonalitySlider).toBeVisible() - await expect(this.conscientiousnessPersonalityValue).toBeVisible() - await this.conscientiousnessPersonalitySlider.click() - const originalValue = await this.conscientiousnessPersonalityValue.textContent() - if (!originalValue) return - if (parseInt(originalValue) < value) { - while (true) { - const actualValue = await this.conscientiousnessPersonalityValue.textContent() - if (!actualValue) break - - if (parseInt(actualValue) >= value) { - break - } - await this.page.keyboard.press('ArrowRight') - } - } - if (parseInt(originalValue) > value) { - while (true) { - const actualValue = await this.conscientiousnessPersonalityValue.textContent() - if (!actualValue) break - - if (parseInt(actualValue) <= value) { - break - } - await this.page.keyboard.press('ArrowLeft') - } - } + await this.setBig5Value( + this.conscientiousnessPersonalitySlider, + this.conscientiousnessPersonalityValue, + value, + ) } async setExtraversionPersonalityValue(value: number | undefined) { - if (!value) return - await expect(this.extraversionPersonalitySlider).toBeVisible() - await expect(this.extraversionPersonalityValue).toBeVisible() - await this.extraversionPersonalitySlider.click() - const originalValue = await this.extraversionPersonalityValue.textContent() - if (!originalValue) return - if (parseInt(originalValue) < value) { - while (true) { - const actualValue = await this.extraversionPersonalityValue.textContent() - if (!actualValue) break - - if (parseInt(actualValue) >= value) { - break - } - await this.page.keyboard.press('ArrowRight') - } - } - if (parseInt(originalValue) > value) { - while (true) { - const actualValue = await this.extraversionPersonalityValue.textContent() - if (!actualValue) break - - if (parseInt(actualValue) <= value) { - break - } - await this.page.keyboard.press('ArrowLeft') - } - } + await this.setBig5Value( + this.extraversionPersonalitySlider, + this.extraversionPersonalityValue, + value, + ) } async setAgreeablenessPersonalityValue(value: number | undefined) { - if (!value) return - await expect(this.agreeablenessPersonalitySlider).toBeVisible() - await expect(this.agreeablenessPersonalityValue).toBeVisible() - await this.agreeablenessPersonalitySlider.click() - const originalValue = await this.agreeablenessPersonalityValue.textContent() - if (!originalValue) return - if (parseInt(originalValue) < value) { - while (true) { - const actualValue = await this.agreeablenessPersonalityValue.textContent() - if (!actualValue) break - - if (parseInt(actualValue) >= value) { - break - } - await this.page.keyboard.press('ArrowRight') - } - } - if (parseInt(originalValue) > value) { - while (true) { - const actualValue = await this.agreeablenessPersonalityValue.textContent() - if (!actualValue) break - - if (parseInt(actualValue) <= value) { - break - } - await this.page.keyboard.press('ArrowLeft') - } - } + await this.setBig5Value( + this.agreeablenessPersonalitySlider, + this.agreeablenessPersonalityValue, + value, + ) } async setNeuroticismPersonalityValue(value: number | undefined) { - if (!value) return - await expect(this.neuroticismPersonalitySlider).toBeVisible() - await expect(this.neuroticismPersonalityValue).toBeVisible() - await this.neuroticismPersonalitySlider.click() - const originalValue = await this.neuroticismPersonalityValue.textContent() - if (!originalValue) return - if (parseInt(originalValue) < value) { - while (true) { - const actualValue = await this.neuroticismPersonalityValue.textContent() - if (!actualValue) break - - if (parseInt(actualValue) >= value) { - break - } - await this.page.keyboard.press('ArrowRight') - } - } - if (parseInt(originalValue) > value) { - while (true) { - const actualValue = await this.neuroticismPersonalityValue.textContent() - if (!actualValue) break - - if (parseInt(actualValue) <= value) { - break - } - await this.page.keyboard.press('ArrowLeft') - } - } + await this.setBig5Value( + this.neuroticismPersonalitySlider, + this.neuroticismPersonalityValue, + value, + ) } async setDietType(dietType: DietTuple | undefined) { @@ -645,7 +561,7 @@ export class SignUpPage { await expect(this.page.locator('label').filter({hasText: 'Other'}).nth(4)).toBeChecked() } else { await expect(this.page.getByRole('checkbox', {name: `${dietType[0]}`})).toBeVisible() - await this.page.getByRole('checkbox', {name: `${dietType[0]}`}).click() + await clickCheckbox(this.page.getByRole('checkbox', {name: `${dietType[0]}`})) await expect(this.page.getByRole('checkbox', {name: `${dietType[0]}`})).toBeChecked() } } @@ -670,10 +586,10 @@ export class SignUpPage { async setLanguages(language: LanguageTuple[] | undefined) { if (!language || language.length === 0) return - await this.page.getByRole('checkbox', {name: `English`}).click() + await clickCheckbox(this.page.getByRole('checkbox', {name: `English`})) for (let i = 0; i < language.length; i++) { await expect(this.page.getByRole('checkbox', {name: `${language[i][0]}`})).toBeVisible() - await this.page.getByRole('checkbox', {name: `${language[i][0]}`}).click() + await clickCheckbox(this.page.getByRole('checkbox', {name: `${language[i][0]}`})) await expect(this.page.getByRole('checkbox', {name: `${language[i][0]}`})).toBeChecked() } } diff --git a/tests/e2e/web/utils/optionChip.ts b/tests/e2e/web/utils/optionChip.ts new file mode 100644 index 00000000..7a5c6ce3 --- /dev/null +++ b/tests/e2e/web/utils/optionChip.ts @@ -0,0 +1,31 @@ +import {Locator, Page} from '@playwright/test' + +/** + * Selectable options (connection type, gender, interests, education, ...) render as chips — + * `OptionChip` in web/components/multi-checkbox.tsx. The chip keeps a real `` + * for accessibility, but it is `sr-only` and the label's text span sits on top of it. Playwright + * still considers the 1px input "visible", so clicking it gets as far as the click and then fails + * with ` intercepts pointer events`. + * + * So: click the enclosing