Skip to content

feat: frontend-clone-segments 🔴 Blocked by #5393 #5394

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
11 changes: 11 additions & 0 deletions frontend/common/services/useSegment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ export const segmentService = service
.enhanceEndpoints({ addTagTypes: ['Segment'] })
.injectEndpoints({
endpoints: (builder) => ({
cloneSegment: builder.mutation<Res['segment'], Req['cloneSegment']>({
invalidatesTags: (q, e, arg) => [
{ id: `LIST${arg.projectId}`, type: 'Segment' },
],
query: (query: Req['cloneSegment']) => ({
body: { name: query.name },
method: 'POST',
url: `projects/${query.projectId}/segments/${query.segmentId}/clone/`,
}),
}),
createSegment: builder.mutation<Res['segment'], Req['createSegment']>({
invalidatesTags: (q, e, arg) => [
{ id: `LIST${arg.projectId}`, type: 'Segment' },
Expand Down Expand Up @@ -118,6 +128,7 @@ export async function getSegment(
// END OF FUNCTION_EXPORTS

export const {
useCloneSegmentMutation,
useCreateSegmentMutation,
useDeleteSegmentMutation,
useGetSegmentQuery,
Expand Down
5 changes: 5 additions & 0 deletions frontend/common/types/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ export type Req = {
projectId: number | string
segment: Omit<Segment, 'id' | 'uuid' | 'project'>
}
cloneSegment: {
projectId: number | string
segmentId: number
name: string
}
getAuditLogs: PagedRequest<{
search?: string
project: string
Expand Down
13 changes: 13 additions & 0 deletions frontend/common/utils/calculateListPosition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// This function is used to calculate the position of a dropdown menu relative to his trigger button element
export function calculateListPosition(
btnEl: HTMLElement,
listEl: HTMLElement,
): { top: number; left: number } {
const listPosition = listEl.getBoundingClientRect()
const btnPosition = btnEl.getBoundingClientRect()
const pageTop = window.visualViewport?.pageTop ?? 0
return {
left: btnPosition.right - listPosition.width,
top: pageTop + btnPosition.bottom,
}
}
2 changes: 0 additions & 2 deletions frontend/common/utils/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
ProjectFlag,
SegmentCondition,
Tag,
User,
PConfidence,
} from 'common/types/responses'
import flagsmith from 'flagsmith'
Expand All @@ -27,7 +26,6 @@ import { defaultFlags } from 'common/stores/default-flags'
import Color from 'color'
import { selectBuildVersion } from 'common/services/useBuildVersion'
import { getStore } from 'common/store'
import format from './format'

const semver = require('semver')

Expand Down
153 changes: 97 additions & 56 deletions frontend/e2e/helpers.cafe.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { RequestLogger, Selector, t } from 'testcafe'
import { FlagsmithValue } from '../common/types/responses';
import { FlagsmithValue } from '../common/types/responses'

export const LONG_TIMEOUT = 40000

Expand All @@ -13,6 +13,12 @@ export type Rule = {
value: string | number | boolean
ors?: Rule[]
}

// Allows to check if an element is present - can be used to identify active feature flag state
export const isElementExists = async (selector: string) => {
return Selector(byId(selector)).exists
}

export const setText = async (selector: string, text: string) => {
logUsingLastSection(`Set text ${selector} : ${text}`)
if (text) {
Expand Down Expand Up @@ -40,17 +46,15 @@ export const waitForElementNotClickable = async (selector: string) => {
await t
.expect(Selector(selector).visible)
.ok(`waitForElementVisible(${selector})`, { timeout: LONG_TIMEOUT })
await t
.expect(Selector(selector).hasAttribute('disabled')).ok()
await t.expect(Selector(selector).hasAttribute('disabled')).ok()
}

export const waitForElementClickable = async (selector: string) => {
logUsingLastSection(`Waiting element visible ${selector}`)
await t
.expect(Selector(selector).visible)
.ok(`waitForElementVisible(${selector})`, { timeout: LONG_TIMEOUT })
await t
.expect(Selector(selector).hasAttribute('disabled')).notOk()
await t.expect(Selector(selector).hasAttribute('disabled')).notOk()
}

export const logResults = async (requests: LoggedRequest[], t) => {
Expand Down Expand Up @@ -106,15 +110,15 @@ export const click = async (selector: string) => {
.click(selector)
}

export const clickByText = async (text:string, element = 'button') => {
export const clickByText = async (text: string, element = 'button') => {
logUsingLastSection(`Click by text ${text} ${element}`)
const selector = Selector(element).withText(text);
const selector = Selector(element).withText(text)
await t
.scrollIntoView(selector)
.expect(Selector(selector).hasAttribute('disabled'))
.notOk('ready for testing', { timeout: 5000 })
.hover(selector)
.click(selector)
.scrollIntoView(selector)
.expect(Selector(selector).hasAttribute('disabled'))
.notOk('ready for testing', { timeout: 5000 })
.hover(selector)
.click(selector)
}

export const gotoSegments = async () => {
Expand All @@ -131,7 +135,11 @@ export const getLogger = () =>
stringifyResponseBody: true,
})

export const createRole = async (roleName: string, index: number, users: number[]) => {
export const createRole = async (
roleName: string,
index: number,
users: number[],
) => {
await click(byId('tab-item-roles'))
await click(byId('create-role'))
await setText(byId('role-name'), roleName)
Expand All @@ -145,8 +153,7 @@ export const createRole = async (roleName: string, index: number, users: number[
await closeModal()
}


export const editRoleMembers = async (index:number)=>{
export const editRoleMembers = async (index: number) => {
await click(byId('tab-item-roles'))
await click(byId('create-role'))
await setText(byId('role-name'), roleName)
Expand Down Expand Up @@ -270,10 +277,12 @@ export const saveFeatureSegments = async () => {
await waitForElementNotExist('#create-feature-modal')
}

export const createEnvironment = async (name:string) => {
export const createEnvironment = async (name: string) => {
await setText('[name="envName"]', name)
await click('#create-env-btn')
await waitForElementVisible(byId(`switch-environment-${name.toLowerCase()}-active`))
await waitForElementVisible(
byId(`switch-environment-${name.toLowerCase()}-active`),
)
}

export const goToUser = async (index: number) => {
Expand Down Expand Up @@ -301,8 +310,25 @@ export const assertTextContentContains = (selector: string, v: string) =>
t.expect(Selector(selector).textContent).contains(v)
export const getText = (selector: string) => Selector(selector).innerText

export const deleteSegment = async (index: number, name: string) => {
await click(byId(`remove-segment-btn-${index}`))
export const cloneSegment = async (index: number, name: string) => {
await click(byId(`segment-action-${index}`))
await click(byId(`segment-clone-${index}`))
await setText('[name="clone-segment-name"]', name)
await click('#confirm-clone-segment-btn')
await waitForElementVisible(byId(`segment-${index + 1}-name`))
}

export const deleteSegment = async (
index: number,
name: string,
legacyDelete = true,
) => {
if (legacyDelete) {
await click(byId(`remove-segment-btn-${index}`))
} else {
await click(byId(`segment-action-${index}`))
await click(byId(`segment-remove-${index}`))
}
await setText('[name="confirm-segment-name"]', name)
await click('#confirm-remove-segment-btn')
await waitForElementNotExist(`remove-segment-btn-${index}`)
Expand All @@ -320,41 +346,44 @@ export const logout = async () => {
await waitForElementVisible('#login-page')
}

export const goToFeatureVersions = async (featureIndex:number) =>{
export const goToFeatureVersions = async (featureIndex: number) => {
await gotoFeature(featureIndex)
await click(byId('change-history'))
}

export const compareVersion = async (
featureIndex:number,
versionIndex:number,
compareOption: 'LIVE'|'PREVIOUS'|null,
oldEnabled:boolean,
newEnabled:boolean,
oldValue?:FlagsmithValue,
newValue?:FlagsmithValue
) =>{
featureIndex: number,
versionIndex: number,
compareOption: 'LIVE' | 'PREVIOUS' | null,
oldEnabled: boolean,
newEnabled: boolean,
oldValue?: FlagsmithValue,
newValue?: FlagsmithValue,
) => {
await goToFeatureVersions(featureIndex)
await click(byId(`history-item-${versionIndex}-compare`))
if(compareOption==='LIVE') {
if (compareOption === 'LIVE') {
await click(byId(`history-item-${versionIndex}-compare-live`))
} else if(compareOption==='PREVIOUS') {
} else if (compareOption === 'PREVIOUS') {
await click(byId(`history-item-${versionIndex}-compare-previous`))
}

await assertTextContent(byId(`old-enabled`), `${oldEnabled}`)
await assertTextContent(byId(`new-enabled`), `${newEnabled}`)
if(oldValue) {
if (oldValue) {
await assertTextContent(byId(`old-value`), `${oldValue}`)
}
if(newValue) {
if (newValue) {
await assertTextContent(byId(`old-value`), `${oldValue}`)
}
await closeModal()
}
export const assertNumberOfVersions = async (index:number, versions:number) =>{
export const assertNumberOfVersions = async (
index: number,
versions: number,
) => {
await goToFeatureVersions(index)
await waitForElementVisible(byId(`history-item-${versions-2}-compare`))
await waitForElementVisible(byId(`history-item-${versions - 2}-compare`))
await closeModal()
}

Expand Down Expand Up @@ -389,7 +418,10 @@ export const createRemoteConfig = async (
await closeModal()
}

export const createOrganisationAndProject = async (organisationName:string,projectName:string) =>{
export const createOrganisationAndProject = async (
organisationName: string,
projectName: string,
) => {
log('Create Organisation')
await click(byId('home-link'))
await click(byId('create-organisation-btn'))
Expand Down Expand Up @@ -418,12 +450,12 @@ export const editRemoteConfig = async (
await click(byId('toggle-feature-button'))
}
await Promise.all(
mvs.map(async (v, i) => {
await setText(byId(`featureVariationWeight${v.value}`), `${v.weight}`)
}),
mvs.map(async (v, i) => {
await setText(byId(`featureVariationWeight${v.value}`), `${v.weight}`)
}),
)
await click(byId('update-feature-btn'))
if(value) {
if (value) {
await waitForElementVisible(byId(`feature-value-${index}`))
await assertTextContent(byId(`feature-value-${index}`), expectedValue)
}
Expand Down Expand Up @@ -455,11 +487,11 @@ export const createFeature = async (

export const deleteFeature = async (index: number, name: string) => {
await click(byId(`feature-action-${index}`))
await waitForElementVisible(byId(`remove-feature-btn-${index}`))
await click(byId(`remove-feature-btn-${index}`))
await waitForElementVisible(byId(`feature-remove-${index}`))
await click(byId(`feature-remove-${index}`))
await setText('[name="confirm-feature-name"]', name)
await click('#confirm-remove-feature-btn')
await waitForElementNotExist(`remove-feature-btn-${index}`)
await waitForElementNotExist(`feature-remove-${index}`)
}

export const toggleFeature = async (index: number, toValue: boolean) => {
Expand Down Expand Up @@ -531,14 +563,18 @@ export const waitAndRefresh = async (waitFor = 3000) => {
await t.eval(() => location.reload())
}

export const refreshUntilElementVisible = async (selector: string, maxRetries=20) => {
const element = Selector(selector);
const isElementVisible = async () => await element.exists && await element.visible;
let retries = 0;
export const refreshUntilElementVisible = async (
selector: string,
maxRetries = 20,
) => {
const element = Selector(selector)
const isElementVisible = async () =>
(await element.exists) && (await element.visible)
let retries = 0
while (retries < maxRetries && !(await isElementVisible())) {
await t.eval(() => location.reload()); // Reload the page
await t.wait(3000);
retries++;
await t.eval(() => location.reload()) // Reload the page
await t.wait(3000)
retries++
}
return t.scrollIntoView(element)
}
Expand All @@ -561,21 +597,26 @@ const permissionsMap = {
'VIEW_IDENTITIES': 'environment',
'MANAGE_SEGMENT_OVERRIDES': 'environment',
'MANAGE_TAGS': 'project',
} as const;


export const setUserPermission = async (email: string, permission: keyof typeof permissionsMap | 'ADMIN', entityName:string|null, entityLevel?: 'project'|'environment'|'organisation', parentName?: string) => {
} as const

export const setUserPermission = async (
email: string,
permission: keyof typeof permissionsMap | 'ADMIN',
entityName: string | null,
entityLevel?: 'project' | 'environment' | 'organisation',
parentName?: string,
) => {
await click(byId('users-and-permissions'))
await click(byId(`user-${email}`))
const level = permissionsMap[permission] || entityLevel
await click(byId(`${level}-permissions-tab`))
if(parentName) {
if (parentName) {
await clickByText(parentName, 'a')
}
if(entityName) {
if (entityName) {
await click(byId(`permissions-${entityName.toLowerCase()}`))
}
if(permission==='ADMIN') {
if (permission === 'ADMIN') {
await click(byId(`admin-switch-${level}`))
} else {
await click(byId(`permission-switch-${permission}`))
Expand Down
10 changes: 8 additions & 2 deletions frontend/e2e/init.cafe.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import versioningTests from './tests/versioning-tests'
import organisationPermissionTest from './tests/organisation-permission-test'
import projectPermissionTest from './tests/project-permission-test'
import environmentPermissionTest from './tests/environment-permission-test'
import rolesTest from './tests/roles-test'
import flagsmith from 'flagsmith/isomorphic';

require('dotenv').config()

Expand All @@ -30,10 +30,16 @@ console.log(
'\n',
)


fixture`E2E Tests`.requestHooks(logger).before(async () => {
const token = process.env.E2E_TEST_TOKEN
? process.env.E2E_TEST_TOKEN
: process.env[`E2E_TEST_TOKEN_${Project.env.toUpperCase()}`]
await flagsmith.init({
api:Project.flagsmithClientAPI,
environmentID:Project.flagsmith,
fetch,
})

if (token) {
await fetch(e2eTestApi, {
Expand Down Expand Up @@ -86,7 +92,7 @@ fixture`E2E Tests`.requestHooks(logger).before(async () => {
})

test('Segment-part-1', async () => {
await testSegment1()
await testSegment1(flagsmith)
await logout()
})

Expand Down
Loading
Loading