Skip to content
Merged
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 @@ -84,6 +84,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(
Comment thread
Zaimwa9 marked this conversation as resolved.
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,
}
}
1 change: 0 additions & 1 deletion frontend/common/utils/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,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
9 changes: 8 additions & 1 deletion frontend/e2e/init.cafe.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +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 flagsmith from 'flagsmith/isomorphic';
import rolesTest from './tests/roles-test'

require('dotenv').config()
Expand All @@ -30,10 +31,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 @@ -89,7 +96,7 @@ fixture`E2E Tests`.requestHooks(logger).before(async () => {
await logResults(logger.requests, t)
})

test('Segment-part-1', testSegment1).meta({ autoLogout: true, category: 'oss' })
test('Segment-part-1', async () => await testSegment1(flagsmith)).meta({ autoLogout: true, category: 'oss' })

test('Segment-part-2', testSegment2).meta({ autoLogout: true, category: 'oss' })

Expand Down
Loading