Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 440928caaf fix(e2e): initialize locators in RecordDetails POM constructor
https://sonarly.com/issue/17241?type=bug

The `RecordDetails` Page Object Model in `twenty-e2e-testing` declares 15 private `Locator` fields but never assigns them in the constructor, making every method that references these locators throw at runtime.

Fix: Initialized all 15 locator fields in the `RecordDetails` constructor, fixing the core issue reported in #18826. The locators were declared but never assigned, making every method in the class non-functional.

**Locator initialization strategy** (matching patterns from sibling POMs `leftMenu.ts` and `mainPage.ts`):
- **Tabs** (`timelineTab`, `tasksTab`, `notesTab`, `filesTab`, `emailsTab`, `calendarTab`): `page.getByRole('link', { name: '...' })` — tabs render as links via `TabButton` with `to` props
- **Command menu buttons** (`previousRecordButton`, `nextRecordButton`, `favoriteRecordButton`, `moreOptionsButton`): `page.getByTestId('tooltip').filter({ hasText: /^...$/  })` — matches the `OverflowingTextWithTooltip` pattern used in `mainPage.ts`
- **Close button**: Structural locator relative to `data-testid="top-bar-title"` (the only stable testid in the PageHeader)
- **Text-based buttons** (`deleteButton`, `detachRelationButton`, `uploadProfileImageButton`): `getByText()` / `getByRole('button')` matching visible labels
- **Add button**: `page.getByTestId('add-button')` matching the existing testid

**Additional XPath bug fixes:**
1. `clickField` and `clickFieldWithButton`: Fixed `contains(., '${name}']` → `contains(., '${name}')]` (missing closing parenthesis made XPath invalid)
2. `detachRelation` and `deleteRelationRecord`: Removed erroneous leading `, ` from XPath selectors which would cause parse errors
2026-03-22 01:11:43 +00:00
@@ -20,6 +20,31 @@ export class RecordDetails {
constructor(public readonly page: Page) {
this.page = page;
this.closeRecordButton = page.getByTestId('top-bar-title').locator('..').locator('button').first();
this.previousRecordButton = page
.getByTestId('tooltip')
.filter({ hasText: /^Navigate to previous record$/ });
this.nextRecordButton = page
.getByTestId('tooltip')
.filter({ hasText: /^Navigate to next record$/ });
this.favoriteRecordButton = page
.getByTestId('tooltip')
.filter({ hasText: /^Add to favorites$/ });
this.addShowPageButton = page.getByTestId('add-button');
this.moreOptionsButton = page
.getByTestId('tooltip')
.filter({ hasText: /^More options$/ });
this.deleteButton = page.getByText('Delete');
this.uploadProfileImageButton = page.getByRole('button', {
name: 'Upload',
});
this.timelineTab = page.getByRole('link', { name: 'Timeline' });
this.tasksTab = page.getByRole('link', { name: 'Tasks' });
this.notesTab = page.getByRole('link', { name: 'Notes' });
this.filesTab = page.getByRole('link', { name: 'Files' });
this.emailsTab = page.getByRole('link', { name: 'Emails' });
this.calendarTab = page.getByRole('link', { name: 'Calendar' });
this.detachRelationButton = page.getByText('Detach');
}
async clickCloseRecordButton() {
@@ -91,7 +116,7 @@ export class RecordDetails {
async clickField(name: string) {
await this.page
.locator(
`//div[@data-testid='tooltip' and contains(., '${name}']/../../../div[last()]/div/div`,
`//div[@data-testid='tooltip' and contains(., '${name}')]/../../../div[last()]/div/div`,
)
.click();
}
@@ -99,12 +124,12 @@ export class RecordDetails {
async clickFieldWithButton(name: string) {
await this.page
.locator(
`//div[@data-testid='tooltip' and contains(., '${name}']/../../../div[last()]/div/div`,
`//div[@data-testid='tooltip' and contains(., '${name}')]/../../../div[last()]/div/div`,
)
.hover();
await this.page
.locator(
`//div[@data-testid='tooltip' and contains(., '${name}']/../../../div[last()]/div/div[last()]/div/button`,
`//div[@data-testid='tooltip' and contains(., '${name}')]/../../../div[last()]/div/div[last()]/div/button`,
)
.click();
}
@@ -119,7 +144,7 @@ export class RecordDetails {
async detachRelation(name: string) {
await this.page.locator(`//a[contains(., "${name}")]`).hover();
await this.page
.locator(`, //a[contains(., "${name}")]/../div[last()]/div/div/button`)
.locator(`//a[contains(., "${name}")]/../div[last()]/div/div/button`)
.hover();
await this.detachRelationButton.click();
}
@@ -127,7 +152,7 @@ export class RecordDetails {
async deleteRelationRecord(name: string) {
await this.page.locator(`//a[contains(., "${name}")]`).hover();
await this.page
.locator(`, //a[contains(., "${name}")]/../div[last()]/div/div/button`)
.locator(`//a[contains(., "${name}")]/../div[last()]/div/div/button`)
.hover();
await this.deleteButton.click();
}