Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 32 additions & 11 deletions frontend/e2e/helpers/e2e-helpers.playwright.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Page, expect } from '@playwright/test';
import { Locator, Page, expect } from '@playwright/test';
import { LONG_TIMEOUT, SHORT_TIMEOUT, byId, log, logUsingLastSection, getFlagsmith } from './utils.playwright';

// Re-export for backwards compatibility
Expand All @@ -18,23 +18,44 @@ export type Rule = {
export class E2EHelpers {
constructor(private page: Page) {}

// The value editors are selected by role and accessible name rather than a
// data-test. The feature value label switches to "Control Value <weight>%"
// once the feature has variations, hence the alternation.
featureValueField(): Locator {
return this.page.getByRole('textbox', { name: /^(Value|Control Value)/ });
}

variationValueField(index: number): Locator {
return this.page.getByRole('textbox', { name: 'Variation Value' }).nth(index);
}

// The override's own label is "Value", or "Segment Control Value" once the
// feature has variations. Anchored, because getByRole matches the name as a
// substring and the row also holds read-only "Variation Value" editors.
segmentOverrideValueField(index: number): Locator {
return this.page
.locator(byId(`segment-override-${index}`))
.getByRole('textbox', { name: /^(Value|Segment Control Value)$/ });
}

async isElementExists(selector: string): Promise<boolean> {
return await this.page.locator(byId(selector)).count() > 0;
}

async setText(selector: string, text: string) {
async setText(selector: string | Locator, text: string) {
logUsingLastSection(`Set text ${selector} : ${text}`);
const element = this.page.locator(selector).first();
const element = typeof selector === 'string' ? this.page.locator(selector).first() : selector;
await element.waitFor({ state: 'visible', timeout: LONG_TIMEOUT });
await element.clear();
if (text) {
await element.fill(text);
}
}

async waitForElementVisible(selector: string, timeout: number = LONG_TIMEOUT) {
async waitForElementVisible(selector: string | Locator, timeout: number = LONG_TIMEOUT) {
logUsingLastSection(`Waiting element visible ${selector}`);
await this.page.locator(selector).first().waitFor({
const element = typeof selector === 'string' ? this.page.locator(selector).first() : selector;
await element.waitFor({
state: 'visible',
timeout
});
Expand Down Expand Up @@ -269,7 +290,7 @@ export class E2EHelpers {
await featureRow.waitFor({ state: 'visible', timeout: LONG_TIMEOUT });
await featureRow.dispatchEvent('click');
await this.waitForElementVisible('#create-feature-modal');
await this.waitForElementVisible(byId('featureValue'));
await this.waitForElementVisible(this.featureValueField());
}

// Create a feature
Expand All @@ -296,7 +317,7 @@ export class E2EHelpers {
await this.gotoFeatures();
await this.click('#show-create-feature-btn');
await this.setText(byId('featureID'), name);
await this.setText(byId('featureValue'), `${value}`);
await this.setText(this.featureValueField(), `${value}`);
await this.setText(byId('featureDesc'), description);
if (!defaultOff) {
await this.click(byId('toggle-feature-button'));
Expand All @@ -305,7 +326,7 @@ export class E2EHelpers {
const v = mvs[i];
await this.click(byId('add-variation'));
await this.page.waitForTimeout(200);
await this.setText(byId(`featureVariationValue${i}`), v.value);
await this.setText(this.variationValueField(i), v.value);
await this.setText(byId(`featureVariationWeight${v.value}`), `${v.weight}`);
await this.page.waitForTimeout(100);
}
Expand Down Expand Up @@ -588,7 +609,7 @@ export class E2EHelpers {
await this.click(byId('segment_overrides'));
}
await this.click(dropdownSelector);
await this.waitForElementVisible(byId(`segment-override-value-${index}`));
await this.waitForElementVisible(this.segmentOverrideValueField(index));
}

// Add segment override for boolean flags
Expand All @@ -611,7 +632,7 @@ export class E2EHelpers {
// Add segment override for remote configs
async addSegmentOverrideConfig(index: number, value: string | number | boolean, selectionIndex: number = 0) {
await this.openSegmentOverride(index, selectionIndex);
await this.setText(byId(`segment-override-value-${index}`), `${value}`);
await this.setText(this.segmentOverrideValueField(index), `${value}`);
await this.click(byId(`segment-override-toggle-${index}`));
}

Expand All @@ -631,7 +652,7 @@ export class E2EHelpers {
await featureRow.dispatchEvent('click');
await this.waitForElementVisible(byId('update-feature-btn'));
if (value !== '') {
await this.setText(byId('featureValue'), `${value}`);
await this.setText(this.featureValueField(), `${value}`);
}
if (mvs.length > 0) {
await this.page.waitForTimeout(500);
Expand Down
5 changes: 3 additions & 2 deletions frontend/e2e/tests/change-request-test.pw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ test.describe('Change Request Tests', () => {
page,
}, testInfo) => {
const {
featureValueField,
assertChangeRequestCount,
approveChangeRequest,
assertInputValue,
Expand Down Expand Up @@ -73,7 +74,7 @@ test.describe('Change Request Tests', () => {
log('Create change request by editing feature value')
await gotoFeatures()
await gotoFeature(featureName)
await setText(byId('featureValue'), 'updated_value')
await setText(featureValueField(), 'updated_value')

await createChangeRequest(
'Update feature value',
Expand Down Expand Up @@ -126,7 +127,7 @@ test.describe('Change Request Tests', () => {
await page.reload({ waitUntil: 'domcontentloaded' })
await waitForElementVisible('#show-create-feature-btn')
await gotoFeature(featureName)
await expect(page.locator(byId('featureValue'))).toHaveValue('updated_value', { timeout: 15000 })
await expect(featureValueField()).toHaveText('updated_value', { timeout: 15000 })
await closeModal()

log('Verify value via API')
Expand Down
4 changes: 3 additions & 1 deletion frontend/e2e/tests/mv-options-tests.pw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const variantCards = (page: Page) => page.locator('#create-feature-modal .varian
test.describe('Multivariate Options', () => {
test('Repeated saves keep the variant set stable @oss', async ({ page }) => {
const {
variationValueField,
closeModal,
createRemoteConfig,
editRemoteConfig,
Expand Down Expand Up @@ -62,6 +63,7 @@ test.describe('Multivariate Options', () => {

test('Variants can be added and removed in a single save @oss', async ({ page }) => {
const {
variationValueField,
click,
closeModal,
createRemoteConfig,
Expand Down Expand Up @@ -90,7 +92,7 @@ test.describe('Multivariate Options', () => {
await expect(variantCards(page)).toHaveCount(1);
await click(byId('add-variation'));
await page.waitForTimeout(200);
await setText(byId('featureVariationValue1'), 'added');
await setText(variationValueField(1), 'added');
await page.waitForTimeout(500);
await click(byId('update-feature-btn'));
await waitForToast();
Expand Down
6 changes: 5 additions & 1 deletion frontend/e2e/tests/segment-test.pw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ const segmentAnyRules = [

test('Segment test 1 - Create, update, and manage segments with multivariate flags @oss', async ({ page }, testInfo) => {
const {
featureValueField,
addSegmentOverride,
assertInputValue,
assertUserFeatureValue,
Expand Down Expand Up @@ -204,6 +205,7 @@ test('Segment test 1 - Create, update, and manage segments with multivariate fla

test('Segment test 2 - Test segment priority and overrides @oss', async ({ page }) => {
const {
featureValueField,
addSegmentOverride,
addSegmentOverrideConfig,
assertUserFeatureValue,
Expand Down Expand Up @@ -312,6 +314,7 @@ test('Segment test 2 - Test segment priority and overrides @oss', async ({ page

test('Segment test 3 - Test user-specific feature overrides @oss', async ({ page }, testInfo) => {
const {
featureValueField,
assertUserFeatureValue,
click,
clickUserFeature,
Expand Down Expand Up @@ -350,7 +353,7 @@ test('Segment test 3 - Test user-specific feature overrides @oss', async ({ page

log('Edit flag for user')
await clickUserFeature(REMOTE_CONFIG_FEATURE)
await setText(byId('featureValue'), 'small')
await setText(featureValueField(), 'small')
await click('#update-feature-btn')
await waitAndRefresh() // wait and refresh to avoid issues with data sync from UK -> US in github workflows
await assertUserFeatureValue(REMOTE_CONFIG_FEATURE, '"small"')
Expand Down Expand Up @@ -393,6 +396,7 @@ test('Segment test 4 - Create ANY rule type segment and verify match changes whe
const ANY_FEATURE = 'any_segment_feature'
const ANY_SEGMENT = 'any_segment_test'
const {
featureValueField,
addSegmentOverrideConfig,
assertUserFeatureValue,
click,
Expand Down
3 changes: 2 additions & 1 deletion frontend/e2e/tests/versioning-tests.pw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { E2E_USER, PASSWORD } from '../config';

test('Versioning tests - Create, edit, and compare feature versions @oss', async ({ page }, testInfo) => {
const {
variationValueField,
assertNumberOfVersions,
click,
closeModal,
Expand Down Expand Up @@ -100,7 +101,7 @@ test('Versioning tests - Create, edit, and compare feature versions @oss', async
await expect(page.locator(byId('featureVariationKey0'))).toHaveText('primary')
await click(byId('add-variation'))
await page.waitForTimeout(200)
await setText(byId('featureVariationValue2'), 'huge')
await setText(variationValueField(2), 'huge')
await page.waitForTimeout(500)
await click(byId('update-feature-btn'))
await waitForToast()
Expand Down
10 changes: 6 additions & 4 deletions frontend/web/components/Highlight.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,12 @@ class Highlight extends React.Component {
style={this.props.style}
data-test={this.props['data-test']}
aria-labelledby={this.props['aria-labelledby']}
// Without a role a contenteditable is announced as plain text, and
// aria-labelledby has nothing to name.
role={this.props.onChange ? 'textbox' : undefined}
aria-multiline={this.props.onChange ? true : undefined}
// Set by the caller: a value field wants role=textbox so its label
// names it, while the code blocks that also use Highlight are not
// form controls and pass nothing.
role={this.props.role}
aria-readonly={this.props['aria-readonly']}
aria-multiline={this.props.role === 'textbox' ? true : undefined}
contentEditable={!!this.props.onChange}
onBlur={this.onBlur}
onFocus={this.onFocus}
Expand Down
4 changes: 0 additions & 4 deletions frontend/web/components/SegmentOverrides.js
Original file line number Diff line number Diff line change
Expand Up @@ -277,10 +277,8 @@ const SegmentOverrideInner = class Override extends React.Component {
<div className='flex-fill overflow-hidden'>
<ValueEditor
label='Value'
readOnly={readOnly}
disabled={readOnly}
value={v.value}
data-test={`segment-override-value-${index}`}
onChange={
readOnly
? null
Expand All @@ -299,8 +297,6 @@ const SegmentOverrideInner = class Override extends React.Component {
label='Segment Control Value'
labelAfter={<ControlWeightChip percentage={controlPercent} />}
value={v.value}
data-test={`segment-override-value-${index}`}
placeholder="Value e.g. 'big' "
disabled={readOnly}
onChange={
readOnly
Expand Down
46 changes: 11 additions & 35 deletions frontend/web/components/ValueEditor/ValueEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import './ValueEditor.scss'

export interface ValueEditorProps {
className?: string
'data-test'?: string
disabled?: boolean
// Renders the field's label and wires it to the editor. Callers used to
// render their own, which is why three different label treatments grew up
Expand All @@ -34,17 +33,11 @@ export interface ValueEditorProps {
labelAfter?: ReactNode
labelTooltip?: string
language?: ValueEditorLanguage
name?: string
onBlur?: () => void
// The edited text. Deliberately a string, not FlagsmithValue: this edits
// text, and deciding that "123" is a number is Flagsmith's domain logic.
// Callers interpret it (Utils.getTypedValue, Utils.valueToFeatureState).
onChange?: (value: string) => void
// placeholder and readOnly only reach the editor under E2E, which swaps
// Highlight for a plain textarea. Highlight renders its own
// 'Enter a value...' and stops accepting input while disabled.
placeholder?: string
readOnly?: boolean
// Fires when the value stops or starts parsing under the active format.
onValidityChange?: (error: string | false) => void
value?: FlagsmithValue
Expand All @@ -57,14 +50,10 @@ const ValueEditor: FC<ValueEditorProps> = ({
labelAfter,
labelTooltip,
language: languageProp,
name,
onBlur,
onChange,
placeholder,
readOnly,
onValidityChange,
value,
...rest
}) => {
const [language, setLanguage] = useState<ValueEditorLanguage>(
languageProp ?? 'txt',
Expand Down Expand Up @@ -137,30 +126,17 @@ const ValueEditor: FC<ValueEditorProps> = ({
<div className='value-editor__field'>
{showControls && <CopyValueButton value={text} />}

{E2E ? (
<textarea
aria-labelledby={label ? labelId : undefined}
data-test={rest['data-test']}
disabled={disabled}
name={name}
onBlur={onBlur}
onChange={(e) => onChange?.(e.target.value)}
placeholder={placeholder}
readOnly={readOnly}
value={text}
/>
) : (
<Highlight
aria-labelledby={label ? labelId : undefined}
data-test={E2E ? rest['data-test'] : ''}
disabled={disabled}
onChange={disabled ? null : onChange}
onBlur={disabled ? null : onBlur}
className={language}
>
{text}
</Highlight>
)}
<Highlight
aria-labelledby={label ? labelId : undefined}
aria-readonly={disabled || undefined}
disabled={disabled}
onChange={disabled ? null : onChange}
onBlur={disabled ? null : onBlur}
role='textbox'
className={language}
>
{text}
</Highlight>
</div>
</div>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -375,8 +375,6 @@ const FeatureValueTab: FC<FeatureValueTabProps> = ({
)
}
labelTooltip={getValueTooltip(hasVariations, isEdit)}
data-test='featureValue'
name='featureValue'
className={`full-width${hasVariations ? ' code-medium' : ''}`}
value={`${
typeof initial_value === 'undefined' || initial_value === null
Expand All @@ -389,7 +387,6 @@ const FeatureValueTab: FC<FeatureValueTabProps> = ({
})
}}
disabled={isDisabled}
placeholder="e.g. 'big' "
/>
</div>
{canCompareValue && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,6 @@ export const VariationValueInput: React.FC<VariationValueProps> = ({
<ValueEditor
label='Variation Value'
labelTooltip={Constants.strings.REMOTE_CONFIG_DESCRIPTION_VARIATION}
data-test={`featureVariationValue${
Utils.featureStateToValue(value) || index
}`}
name='featureValue'
className='full-width code-medium'
value={Utils.getTypedValue(Utils.featureStateToValue(value))}
disabled={!canCreateFeature || disabled || readOnly}
Expand All @@ -97,7 +93,6 @@ export const VariationValueInput: React.FC<VariationValueProps> = ({
...Utils.valueToFeatureState(newValue, false),
})
}}
placeholder="e.g. 'big' "
/>,
)}
</div>
Expand Down
Loading