From 14a9ba09be7b9629a21119668f7089f00c9cf582 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Wed, 21 Oct 2026 14:18:01 +0100 Subject: [PATCH 1/2] test(Change requests): Cover versioned, edited and scheduled change requests Co-Authored-By: Claude Opus 5.5 (1M context) --- .../e2e/helpers/e2e-helpers.playwright.ts | 89 +++++++-- frontend/e2e/tests/change-request-test.pw.ts | 178 ++++++++++++++++++ 2 files changed, 254 insertions(+), 13 deletions(-) diff --git a/frontend/e2e/helpers/e2e-helpers.playwright.ts b/frontend/e2e/helpers/e2e-helpers.playwright.ts index df5e4bb9b402..7ab7470c7a2c 100644 --- a/frontend/e2e/helpers/e2e-helpers.playwright.ts +++ b/frontend/e2e/helpers/e2e-helpers.playwright.ts @@ -635,6 +635,18 @@ export class E2EHelpers { } } + // Add a remote config override for a segment picked by name, so other + // segments in a shared project can't shift the selection + async addSegmentOverrideConfigForSegment(segmentName: string, value: string) { + await this.click(byId('segment_overrides')); + await this.click(byId('select-segment')); + await this.page.keyboard.type(segmentName); + await this.page.locator('.react-select__option').filter({ hasText: segmentName }).first().click(); + await this.waitForElementVisible(this.segmentOverrideValueField(0)); + await this.setText(this.segmentOverrideValueField(0), value); + await this.click(byId('segment-override-toggle-0')); + } + // Add segment override for remote configs async addSegmentOverrideConfig(index: number, value: string | number | boolean, selectionIndex: number = 0) { await this.openSegmentOverride(index, selectionIndex); @@ -1126,12 +1138,24 @@ export class E2EHelpers { } // Create a change request from feature modal - async createChangeRequest(title: string, description: string) { + async createChangeRequest( + title: string, + description: string, + { + openWith = '#update-feature-btn', + scheduleFor, + }: { + // The tab's save button, e.g. '#update-feature-segments-btn' + openWith?: string; + // A date in the picker's format, e.g. 'January 1, 2099 12:00 PM' + scheduleFor?: string; + } = {}, + ) { log(`Create change request: ${title}`); // Click the update/create change request button // When four-eyes is enabled, this button says "Create Change Request" - await this.click('#update-feature-btn'); + await this.click(openWith); await this.page.waitForTimeout(1000); // Fill in title using placeholder @@ -1143,17 +1167,26 @@ export class E2EHelpers { const descField = this.page.locator('textarea[placeholder="Add an optional description..."]'); await descField.fill(description); - // The date picker needs to be set - click on it to trigger current date/time - // Find the date input and click it - const dateInput = this.page.locator('.react-datepicker__input-container input').first(); - await dateInput.click(); - await this.page.waitForTimeout(300); - - // Click "Now" or today's date to set it - // The datepicker should appear - click on today - const todayButton = this.page.locator('.react-datepicker__today-button, .react-datepicker__day--today').first(); - await todayButton.click(); - await this.page.waitForTimeout(500); + if (scheduleFor) { + const scheduleField = this.page.getByLabel('Schedule Change'); + await scheduleField.fill(scheduleFor); + await scheduleField.press('Enter'); + await this.page + .getByText('This change will be scheduled to go live at') + .waitFor({ state: 'visible' }); + } else { + // The date picker needs to be set - click on it to trigger current date/time + // Find the date input and click it + const dateInput = this.page.locator('.react-datepicker__input-container input').first(); + await dateInput.click(); + await this.page.waitForTimeout(300); + + // Click "Now" or today's date to set it + // The datepicker should appear - click on today + const todayButton = this.page.locator('.react-datepicker__today-button, .react-datepicker__day--today').first(); + await todayButton.click(); + await this.page.waitForTimeout(500); + } // Click create/save button - look for enabled button const saveButton = this.page.locator('button').filter({ hasText: /Save|Create/ }).filter({ hasNotText: 'Cancel' }).last(); @@ -1167,6 +1200,36 @@ export class E2EHelpers { await this.waitForToast(); } + // Save an open change request's edits from its feature modal + async updateChangeRequest() { + log('Update change request'); + await this.click('#update-feature-btn'); + const updateButton = this.page.getByRole('button', { name: 'Update', exact: true }); + await updateButton.waitFor({ state: 'visible' }); + await this.waitForToastsToClear(); + await updateButton.click(); + await this.waitForToast(); + } + + // Open the feature modal for an unapproved change request, from its page + async editChangeRequest() { + log('Edit change request'); + await this.page.getByRole('button', { name: 'Edit', exact: true }).click(); + await this.waitForElementVisible('#create-feature-modal'); + } + + // Enable feature versioning for the current environment + async enableFeatureVersioning() { + log('Enable feature versioning'); + await this.click('#env-settings-link'); + await this.click(byId('enable-versioning')); + await this.click('#confirm-btn-yes'); + // Feature versioning takes up to a minute to enable on the backend + await this.waitForElementVisible(byId('feature-versioning-enabled')); + await this.waitForElementVisible('.toast-message'); + await this.waitForToastsToClear(); + } + // Open a change request from the list async openChangeRequest(index: number = 0) { log(`Open change request at index ${index}`); diff --git a/frontend/e2e/tests/change-request-test.pw.ts b/frontend/e2e/tests/change-request-test.pw.ts index a4274d3b44b4..e71c31b49fd4 100644 --- a/frontend/e2e/tests/change-request-test.pw.ts +++ b/frontend/e2e/tests/change-request-test.pw.ts @@ -138,4 +138,182 @@ test.describe('Change Request Tests', () => { log('Change request test completed successfully') }) + + test('A segment override change request in a versioned environment opens on its Segment Overrides diff @enterprise', async ({ + page, + }) => { + const { + addSegmentOverrideConfigForSegment, + closeModal, + createChangeRequest, + createEnvironment, + createRemoteConfig, + createSegment, + enableChangeRequests, + enableFeatureVersioning, + gotoChangeRequests, + gotoFeature, + gotoFeatures, + gotoProject, + gotoSegments, + login, + openChangeRequest, + } = createHelpers(page) + const flagsmith = await getFlagsmith() + if ( + !flagsmith.hasFeature('segment_change_requests') || + !flagsmith.hasFeature('feature_versioning') + ) { + log('Skipping versioned change request test, feature not enabled.') + test.skip() + return + } + + const segmentName = 'cr_versioned_segment' + const featureName = 'cr_versioned_feature' + + log('Login as admin') + await login(E2E_USER, PASSWORD) + await gotoProject(E2E_TEST_PROJECT) + + log('Create a versioned environment that requires change requests') + await page.click('text="Create Environment"') + await createEnvironment('CR_Versioned_Env') + await enableFeatureVersioning() + await enableChangeRequests(1) + + log('Create the segment and feature') + await gotoSegments() + await createSegment(segmentName, [ + { name: 'plan', operator: 'EQUAL', value: 'pro' }, + ]) + await createRemoteConfig({ name: featureName, value: 'initial_value' }) + + log('Create a change request that only overrides the segment') + await gotoFeatures() + await gotoFeature(featureName) + await addSegmentOverrideConfigForSegment(segmentName, 'pro_value') + await createChangeRequest('Override for pro plan', 'Segment override only', { + openWith: '#update-feature-segments-btn', + }) + await closeModal() + + log('The diff opens on Segment Overrides, the only tab with changes') + await gotoChangeRequests() + await openChangeRequest(0) + await expect( + page.getByRole('button', { name: /^Segment Overrides/ }), + ).toHaveClass(/tab-active/) + }) + + test('An open change request can be edited from its page @enterprise', async ({ + page, + }) => { + const { + closeModal, + createChangeRequest, + createEnvironment, + createRemoteConfig, + editChangeRequest, + enableChangeRequests, + featureValueField, + gotoChangeRequests, + gotoFeature, + gotoFeatures, + gotoProject, + login, + openChangeRequest, + setText, + updateChangeRequest, + } = createHelpers(page) + const flagsmith = await getFlagsmith() + if (!flagsmith.hasFeature('segment_change_requests')) { + log('Skipping change request edit test, feature not enabled.') + test.skip() + return + } + + const featureName = 'cr_edit_feature' + + log('Login as admin') + await login(E2E_USER, PASSWORD) + await gotoProject(E2E_TEST_PROJECT) + + log('Create an environment that requires change requests') + await page.click('text="Create Environment"') + await createEnvironment('CR_Edit_Env') + await enableChangeRequests(1) + await createRemoteConfig({ name: featureName, value: 'initial_value' }) + + log('Create a change request') + await gotoFeatures() + await gotoFeature(featureName) + await setText(featureValueField(), 'first_value') + await createChangeRequest('Change the value', 'First version') + await closeModal() + + log('Edit it from its page') + await gotoChangeRequests() + await openChangeRequest(0) + await editChangeRequest() + await setText(featureValueField(), 'second_value') + await updateChangeRequest() + await expect(page.getByText(/Updated Change Request #\d+/)).toBeVisible() + + log('The change request now carries the edited value') + await expect(page.getByText('second_value').first()).toBeVisible() + }) + + test('A change request scheduled for later shows as a scheduled change @enterprise', async ({ + page, + }) => { + const { + closeModal, + createChangeRequest, + createEnvironment, + createRemoteConfig, + enableChangeRequests, + featureValueField, + gotoChangeRequests, + gotoFeature, + gotoFeatures, + gotoProject, + login, + openChangeRequest, + setText, + } = createHelpers(page) + const flagsmith = await getFlagsmith() + if (!flagsmith.hasFeature('segment_change_requests')) { + log('Skipping scheduled change request test, feature not enabled.') + test.skip() + return + } + + const featureName = 'cr_schedule_feature' + + log('Login as admin') + await login(E2E_USER, PASSWORD) + await gotoProject(E2E_TEST_PROJECT) + + log('Create an environment that requires change requests') + await page.click('text="Create Environment"') + await createEnvironment('CR_Schedule_Env') + await enableChangeRequests(1) + await createRemoteConfig({ name: featureName, value: 'initial_value' }) + + log('Create a change request scheduled for 2099') + await gotoFeatures() + await gotoFeature(featureName) + await setText(featureValueField(), 'scheduled_value') + await createChangeRequest('Scheduled value', 'Goes live in 2099', { + scheduleFor: 'January 1, 2099 12:00 PM', + }) + await closeModal() + + log('Its page shows it as a scheduled change') + await gotoChangeRequests() + await openChangeRequest(0) + await expect(page.getByText('Scheduled Change')).toBeVisible() + await expect(page.getByText(/1st Jan 2099/)).toBeVisible() + }) }) From efab63d753ca9b4fcf70a5eae8cc7a4f23331bb9 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Wed, 21 Oct 2026 15:09:16 +0100 Subject: [PATCH 2/2] test(Change requests): Fix the new tests' toast wait, segment pick and schedule check Co-Authored-By: Claude Opus 5.5 (1M context) --- frontend/e2e/helpers/e2e-helpers.playwright.ts | 15 +++++++++++---- frontend/e2e/tests/change-request-test.pw.ts | 4 +--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/frontend/e2e/helpers/e2e-helpers.playwright.ts b/frontend/e2e/helpers/e2e-helpers.playwright.ts index 7ab7470c7a2c..5414c7e123e2 100644 --- a/frontend/e2e/helpers/e2e-helpers.playwright.ts +++ b/frontend/e2e/helpers/e2e-helpers.playwright.ts @@ -639,9 +639,12 @@ export class E2EHelpers { // segments in a shared project can't shift the selection async addSegmentOverrideConfigForSegment(segmentName: string, value: string) { await this.click(byId('segment_overrides')); - await this.click(byId('select-segment')); - await this.page.keyboard.type(segmentName); - await this.page.locator('.react-select__option').filter({ hasText: segmentName }).first().click(); + // E2E builds render the select's options as plain links + await this.page + .locator('[data-test^="select-segment-option-"]') + .filter({ hasText: segmentName }) + .first() + .click(); await this.waitForElementVisible(this.segmentOverrideValueField(0)); await this.setText(this.segmentOverrideValueField(0), value); await this.click(byId('segment-override-toggle-0')); @@ -1208,7 +1211,11 @@ export class E2EHelpers { await updateButton.waitFor({ state: 'visible' }); await this.waitForToastsToClear(); await updateButton.click(); - await this.waitForToast(); + // Checked before it clears, as waitForToast waits for it to go + await expect( + this.page.locator('.toast-message').filter({ hasText: /Updated Change Request #\d+/ }), + ).toBeVisible({ timeout: LONG_TIMEOUT }); + await this.waitForToastsToClear(); } // Open the feature modal for an unapproved change request, from its page diff --git a/frontend/e2e/tests/change-request-test.pw.ts b/frontend/e2e/tests/change-request-test.pw.ts index e71c31b49fd4..e1c3f639c700 100644 --- a/frontend/e2e/tests/change-request-test.pw.ts +++ b/frontend/e2e/tests/change-request-test.pw.ts @@ -258,7 +258,6 @@ test.describe('Change Request Tests', () => { await editChangeRequest() await setText(featureValueField(), 'second_value') await updateChangeRequest() - await expect(page.getByText(/Updated Change Request #\d+/)).toBeVisible() log('The change request now carries the edited value') await expect(page.getByText('second_value').first()).toBeVisible() @@ -313,7 +312,6 @@ test.describe('Change Request Tests', () => { log('Its page shows it as a scheduled change') await gotoChangeRequests() await openChangeRequest(0) - await expect(page.getByText('Scheduled Change')).toBeVisible() - await expect(page.getByText(/1st Jan 2099/)).toBeVisible() + await expect(page.getByText(/go live at 1st Jan 2099/)).toBeVisible() }) })