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.

This commit is contained in:
MartinBraquet
2026-07-29 21:55:51 +02:00
parent 77dc53d9c6
commit 423361938c
11 changed files with 204 additions and 195 deletions

View File

@@ -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.

View File

@@ -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",

View File

@@ -65,6 +65,6 @@ export default defineConfig({
],
timeout: 120000,
expect: {
timeout: 60000,
timeout: 20000,
},
})

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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()
}
}

View File

@@ -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 `<input type="checkbox">`
* 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 `<span> intercepts pointer events`.
*
* So: click the enclosing <label> when there is one — the same target a real user hits, toggling the
* checkbox through the native label association. Checkboxes that aren't chips (the display field
* toggles, which pair a visible input with a sibling `htmlFor` label) have no ancestor label and are
* clicked directly, so this is safe to use for any checkbox.
*
* Assertions (`toBeChecked`) still belong on the input itself — pass the original locator for those.
*/
export async function clickCheckbox(checkbox: Locator) {
const label = checkbox.locator('xpath=ancestor::label[1]')
const target = (await label.count()) > 0 ? label.first() : checkbox
await target.click()
}
/** The clickable chip for `label` — its enclosing <label> element. See {@link clickCheckbox}. */
export function optionChip(page: Page, label: string): Locator {
return page.locator('label').filter({has: page.getByRole('checkbox', {name: label, exact: true})})
}
/** The chip's underlying checkbox — for `toBeChecked()` assertions, which need the input itself. */
export function optionChipInput(page: Page, label: string): Locator {
return page.getByRole('checkbox', {name: label, exact: true})
}

View File

@@ -149,7 +149,7 @@ export function SignupBio(props: {
}, [profile?.bio, editor])
return (
<Col className="relative w-full -mt-4">
<Col className="relative w-full -mt-4" data-testid="signup-bio">
<BaseBio
defaultValue={profile?.bio}
onBlur={(editor) => {

View File

@@ -177,6 +177,7 @@ export const Search = forwardRef<
'border-primary-500 ring-2 ring-primary-300 bg-primary-50 text-primary-700',
)}
onClick={handleOpenFilters}
data-testid="open-filters-button"
>
<IoFilterSharp className="h-4 w-4 sm:mr-1.5" />
<span className="hidden sm:inline">{t('search.filters', 'Filters')}</span>

View File

@@ -452,6 +452,7 @@ export const SendMessageButton = (props: {
<button
type={'button'}
data-testid="send-message-submit"
onClick={sendMessage}
disabled={!isReady || submitting}
className={clsx(

View File

@@ -584,6 +584,7 @@ function Height(props: {profile: Profile}) {
return (
<AboutRow
testId="profile-height"
title={t('profile.optional.height', 'Height')}
text={formatProfileValue('height_in_inches', profile.height_in_inches, measurementSystem, t)}
/>