diff --git a/tests/playwright/pageObjects/browser-page.ts b/tests/playwright/pageObjects/browser-page.ts index 534429b51c..102d963849 100755 --- a/tests/playwright/pageObjects/browser-page.ts +++ b/tests/playwright/pageObjects/browser-page.ts @@ -1278,6 +1278,18 @@ export class BrowserPage extends BasePage { await expect(this.keysSummary).not.toBeVisible() } + async verifyKeyExists(keyName: string): Promise { + await this.searchByKeyName(keyName) + const keyExists = await this.isKeyIsDisplayedInTheList(keyName) + expect(keyExists).toBe(true) + } + + async verifyKeyDoesNotExist(keyName: string): Promise { + await this.searchByKeyName(keyName) + const keyStillExists = await this.isKeyIsDisplayedInTheList(keyName) + expect(keyStillExists).toBe(false) + } + async clearFilter(): Promise { await this.clearFilterButton.click() } @@ -1386,6 +1398,9 @@ export class BrowserPage extends BasePage { } async getAllListElements(): Promise { + // Wait for list details to be visible first + await expect(this.page.getByTestId('list-details')).toBeVisible() + // Get all list elements' text content const elements = await this.listElementsList.all() const values: string[] = [] @@ -1401,21 +1416,41 @@ export class BrowserPage extends BasePage { } async getAllSetMembers(): Promise { - // Get all set members' text content - const elements = await this.setMembersList.all() - const values: string[] = [] + // Wait for set details to be visible and loaded + await this.waitForSetDetailsToBeVisible() - for (let i = 0; i < elements.length; i += 1) { - const text = await elements[i].textContent() - if (text && text.trim()) { - values.push(text.trim()) - } + // Wait for at least one element to be visible (or confirm none exist) + try { + await expect(this.setMembersList.first()).toBeVisible() + } catch { + // No members exist - return empty array + return [] } - return values + // Get all set members' text content + const elements = await this.setMembersList.all() + const textContents = await Promise.all( + elements.map(async (element) => { + const text = await element.textContent() + return text?.trim() || '' + }), + ) + + return textContents.filter((text) => text.length > 0) } async getAllZsetMembers(): Promise> { + // Wait for zset details to be visible and loaded + await this.waitForZsetDetailsToBeVisible() + + // Wait for at least one element to be visible (or confirm none exist) + try { + await expect(this.zsetMembersList.first()).toBeVisible() + } catch { + // No members exist - return empty array + return [] + } + // Get all zset members' names and scores const memberElements = await this.zsetMembersList.all() const scoreElements = await this.zsetScoresList.all() @@ -1600,4 +1635,492 @@ export class BrowserPage extends BasePage { await this.verifyKeyTTL(expectedTTL) await this.closeKeyDetailsAndVerify() } + + async addMemberToSetKey(member: string): Promise { + if (await this.toast.isCloseButtonVisible()) { + await this.toast.closeToast() + } + await this.addKeyValueItemsButton.click() + await this.setMemberInput.fill(member) + await this.saveMemberButton.click() + } + + async removeMemberFromSet(member: string): Promise { + const memberElement = this.page.locator( + `[data-testid="set-member-value-${member}"]`, + ) + await memberElement.hover() + await this.page + .locator(`[data-testid="set-remove-btn-${member}-icon"]`) + .click() + await this.page + .locator(`[data-testid^="set-remove-btn-${member}"]`) + .getByText('Remove') + .click() + } + + async waitForSetLengthToUpdate(expectedLength: number): Promise { + await expect + .poll(async () => { + const keyLength = await this.getKeyLength() + return parseInt(keyLength, 10) + }) + .toBe(expectedLength) + } + + async verifySetContainsMembers(expectedMembers: string[]): Promise { + const displayedMembers = await this.getAllSetMembers() + + expect(displayedMembers).toHaveLength(expectedMembers.length) + expectedMembers.forEach((expectedMember) => { + expect(displayedMembers).toContain(expectedMember) + }) + } + + async verifySetDoesNotContainMembers( + unwantedMembers: string[], + ): Promise { + const displayedMembers = await this.getAllSetMembers() + unwantedMembers.forEach((unwantedMember) => { + expect(displayedMembers).not.toContain(unwantedMember) + }) + } + + async verifySetMemberExists(member: string): Promise { + const memberElement = this.page.locator( + `[data-testid="set-member-value-${member}"]`, + ) + await expect(memberElement).toBeVisible() + } + + async verifySetMemberNotExists(member: string): Promise { + const memberElement = this.page.locator( + `[data-testid="set-member-value-${member}"]`, + ) + await expect(memberElement).not.toBeVisible() + } + + async searchInSetMembers(searchTerm: string): Promise { + // Wait for set details to be visible first + await this.waitForSetDetailsToBeVisible() + + // For set keys, the search input is always visible in the table header + const searchInput = this.page.getByTestId('search') + await expect(searchInput).toBeVisible() + await searchInput.fill(searchTerm) + await this.page.keyboard.press('Enter') + + // Wait for search to take effect by checking if any elements are present + await expect + .poll(async () => { + const elements = await this.page + .locator('[data-testid^="set-member-value-"]') + .count() + return elements >= 0 // Always true, just wait for elements to be ready + }) + .toBeTruthy() + } + + async clearSetSearch(): Promise { + // For set keys, the search input is always visible + const searchInput = this.page.getByTestId('search') + await expect(searchInput).toBeVisible() + await searchInput.clear() + await this.page.keyboard.press('Enter') + } + + async waitForSetDetailsToBeVisible(): Promise { + await expect(this.page.getByTestId('set-details')).toBeVisible() + } + + async verifySetSearchResults( + searchTerm: string, + allMembers: string[], + ): Promise { + // Wait for any potential loading to complete + await this.waitForSetDetailsToBeVisible() + + // Wait for search filtering to take effect by ensuring elements are ready + await expect + .poll(async () => { + const elements = await this.page + .locator('[data-testid^="set-member-value-"]:visible') + .count() + return elements >= 0 // Always true, just wait for elements to be ready + }) + .toBeTruthy() + + // Get all currently visible set member elements + const visibleElements = await this.page + .locator('[data-testid^="set-member-value-"]:visible') + .all() + + // Extract the text content from visible elements + const textContents = await Promise.all( + visibleElements.map(async (element) => { + const textContent = await element.textContent() + return textContent?.trim() || '' + }), + ) + const visibleMemberTexts = textContents.filter( + (text) => text.length > 0, + ) + + // Check which members should be matching + const expectedVisibleMembers = allMembers.filter((member) => + member.includes(searchTerm), + ) + const expectedHiddenMembers = allMembers.filter( + (member) => !member.includes(searchTerm), + ) + + // Verify that all expected visible members are found + expectedVisibleMembers.forEach((expectedMember) => { + const isFound = visibleMemberTexts.some((visibleText) => + visibleText.includes(expectedMember), + ) + expect(isFound).toBe(true) + }) + + // Verify that no hidden members are visible + expectedHiddenMembers.forEach((hiddenMember) => { + const isFound = visibleMemberTexts.some((visibleText) => + visibleText.includes(hiddenMember), + ) + expect(isFound).toBe(false) + }) + } + + async waitForSetMembersToLoad(expectedCount?: number): Promise { + await this.waitForSetDetailsToBeVisible() + + // Wait for loading to complete + await expect( + this.page.getByTestId('progress-key-set'), + ).not.toBeVisible() + + // If we expect a specific count, wait for that many elements + if (expectedCount !== undefined && expectedCount > 0) { + await expect + .poll(async () => { + const elements = await this.page + .locator("[data-testid^='set-member-value-']") + .all() + return elements.length + }) + .toBe(expectedCount) + } else if (expectedCount === undefined) { + // Just wait for at least one element or verify none exist + try { + await expect(this.setMembersList.first()).toBeVisible() + } catch { + // No elements expected or found - this is fine + } + } + } + + async addMemberToZsetKey(member: string, score: number): Promise { + if (await this.toast.isCloseButtonVisible()) { + await this.toast.closeToast() + } + await this.addKeyValueItemsButton.click() + await this.setMemberInput.fill(member) + await this.zsetMemberScoreInput.fill(score.toString()) + await this.saveMemberButton.click() + } + + async editZsetMemberScore(member: string, newScore: number): Promise { + // First ensure we're on the right page and elements are loaded + await this.waitForZsetDetailsToBeVisible() + + // Find the member element first and ensure it exists + const memberElement = this.page.locator( + `[data-testid="zset-member-value-${member}"]`, + ) + await expect(memberElement).toBeVisible() + + // We need to hover over the score element, not the member element + // Wait for score elements to be ready + await expect( + this.page.locator('[data-testid^="zset_content-value-"]').first(), + ).toBeVisible() + + // Get all zset content value elements and try each one until we find the right row + const allScoreElements = await this.page + .locator('[data-testid^="zset_content-value-"]') + .all() + + let editButton + let foundVisible = false + + for (let i = 0; i < allScoreElements.length && !foundVisible; i += 1) { + const scoreElement = allScoreElements[i] + // Hover over this score element + await scoreElement.hover() + + // Check if an edit button becomes visible + editButton = this.page + .locator('[data-testid^="zset_edit-btn-"]') + .first() + foundVisible = await editButton.isVisible() + } + + // Click the edit button if we found one + if (editButton && foundVisible) { + await editButton.click() + } else { + throw new Error(`Could not find edit button for member: ${member}`) + } + + // Use the correct editor element from the unit tests + const editorLocator = this.page.locator( + '[data-testid="inline-item-editor"]', + ) + await expect(editorLocator).toBeVisible() + await editorLocator.clear() + await editorLocator.fill(newScore.toString()) + await this.applyButton.click() + } + + async cancelZsetMemberScoreEdit( + member: string, + newScore: number, + ): Promise { + // We need to hover over the score element to make the edit button appear + // Wait for score elements to be ready + await expect( + this.page.locator('[data-testid^="zset_content-value-"]').first(), + ).toBeVisible() + + // Get all zset content value elements and try each one until we find the right row + const allScoreElements = await this.page + .locator('[data-testid^="zset_content-value-"]') + .all() + + let editButton + let foundVisible = false + + for (let i = 0; i < allScoreElements.length && !foundVisible; i += 1) { + const scoreElement = allScoreElements[i] + // Hover over this score element + await scoreElement.hover() + + // Check if an edit button becomes visible + editButton = this.page + .locator('[data-testid^="zset_edit-btn-"]') + .first() + foundVisible = await editButton.isVisible() + } + + // Click the edit button if we found one + if (editButton && foundVisible) { + await editButton.click() + } else { + throw new Error(`Could not find edit button for member: ${member}`) + } + + // Use the correct editor element from the unit tests + const editorLocator = this.page.locator( + '[data-testid="inline-item-editor"]', + ) + await expect(editorLocator).toBeVisible() + await editorLocator.clear() + await editorLocator.fill(newScore.toString()) + + // Cancel using Escape key + await this.page.keyboard.press('Escape') + await expect(editorLocator).not.toBeVisible() + } + + async removeMemberFromZset(member: string): Promise { + const memberElement = this.page.locator( + `[data-testid="zset-member-value-${member}"]`, + ) + await memberElement.hover() + await this.page + .locator(`[data-testid="zset-remove-button-${member}-icon"]`) + .click() + await this.page + .locator(`[data-testid^="zset-remove-button-${member}"]`) + .getByText('Remove') + .click() + } + + async removeMultipleMembersFromZset(memberNames: string[]): Promise { + for (let i = 0; i < memberNames.length; i += 1) { + await this.removeMemberFromZset(memberNames[i]) + } + } + + async removeAllZsetMembers( + members: Array<{ name: string; score: number }>, + ): Promise { + for (let i = 0; i < members.length; i += 1) { + await this.removeMemberFromZset(members[i].name) + } + } + + async waitForZsetLengthToUpdate(expectedLength: number): Promise { + await expect + .poll(async () => { + const keyLength = await this.getKeyLength() + return parseInt(keyLength, 10) + }) + .toBe(expectedLength) + } + + async verifyZsetContainsMembers( + expectedMembers: Array<{ name: string; score: number }>, + ): Promise { + const displayedMembers = await this.getAllZsetMembers() + + expect(displayedMembers).toHaveLength(expectedMembers.length) + expectedMembers.forEach((expectedMember) => { + const foundMember = displayedMembers.find( + (member) => member.name === expectedMember.name, + ) + expect(foundMember).toBeDefined() + expect(foundMember?.score).toBe(expectedMember.score.toString()) + }) + } + + async verifyZsetDoesNotContainMembers( + unwantedMembers: string[], + ): Promise { + const displayedMembers = await this.getAllZsetMembers() + unwantedMembers.forEach((unwantedMember) => { + const foundMember = displayedMembers.find( + (member) => member.name === unwantedMember, + ) + expect(foundMember).toBeUndefined() + }) + } + + async verifyZsetMemberExists(member: string): Promise { + const memberElement = this.page.locator( + `[data-testid="zset-member-value-${member}"]`, + ) + await expect(memberElement).toBeVisible() + } + + async verifyZsetMemberNotExists(member: string): Promise { + const memberElement = this.page.locator( + `[data-testid="zset-member-value-${member}"]`, + ) + await expect(memberElement).not.toBeVisible() + } + + async verifyZsetMemberScore( + member: string, + expectedScore: number, + ): Promise { + // Since we can't reliably match member to score element by DOM traversal, + // let's verify that ANY score element contains our expected score + // This is sufficient for our test since we're editing a specific score + + const allScoreElements = await this.page + .locator('[data-testid^="zset_content-value-"]') + .all() + + let found = false + for (const scoreElement of allScoreElements) { + const scoreText = await scoreElement.textContent() + if (scoreText && scoreText.includes(expectedScore.toString())) { + found = true + break + } + } + + if (!found) { + throw new Error( + `Expected score ${expectedScore} not found in any zset score elements`, + ) + } + } + + async waitForZsetScoreToUpdate(expectedScore: number): Promise { + await expect + .poll(async () => { + const allScoreElements = await this.page + .locator('[data-testid^="zset_content-value-"]') + .all() + + const textContents = await Promise.all( + allScoreElements.map((element) => element.textContent()), + ) + + return textContents.some( + (text) => text && text.includes(expectedScore.toString()), + ) + }) + .toBe(true) + } + + async searchInZsetMembers(searchTerm: string): Promise { + // Wait for zset details to be visible first + await this.waitForZsetDetailsToBeVisible() + + // Try clicking the search button first to make search input visible + await this.searchButtonInKeyDetails.click() + + const searchInput = this.page.getByTestId('search') + + // Wait for search input to be ready + await expect(searchInput).toBeVisible() + await expect(searchInput).toBeEnabled() + + // Clear any existing search and enter new term + await searchInput.clear() + await searchInput.fill(searchTerm) + await this.page.keyboard.press('Enter') + + // Wait for search to complete by checking if search input has the value + await expect + .poll(async () => { + const inputValue = await searchInput.inputValue() + return inputValue + }) + .toBe(searchTerm) + } + + async clearZsetSearch(): Promise { + // Wait for search input to be ready + const searchInput = this.page.getByTestId('search') + await expect(searchInput).toBeVisible() + await expect(searchInput).toBeEnabled() + await searchInput.clear() + await this.page.keyboard.press('Enter') + } + + async waitForZsetDetailsToBeVisible(): Promise { + await expect(this.page.getByTestId('zset-details')).toBeVisible() + } + + async waitForZsetMembersToLoad(expectedCount?: number): Promise { + await this.waitForZsetDetailsToBeVisible() + + // Wait for loading to complete + await expect( + this.page.getByTestId('progress-key-zset'), + ).not.toBeVisible() + + // If we expect a specific count, wait for that many elements + if (expectedCount !== undefined && expectedCount > 0) { + await expect + .poll(async () => { + const elements = await this.page + .locator("[data-testid^='zset-member-value-']") + .all() + return elements.length + }) + .toBe(expectedCount) + } else if (expectedCount === undefined) { + // Just wait for at least one element or verify none exist + try { + await expect(this.zsetMembersList.first()).toBeVisible() + } catch { + // No elements expected or found - this is fine + } + } + } } diff --git a/tests/playwright/tests/browser/keys-edit/edit-set-key.spec.ts b/tests/playwright/tests/browser/keys-edit/edit-set-key.spec.ts new file mode 100644 index 0000000000..490499a58e --- /dev/null +++ b/tests/playwright/tests/browser/keys-edit/edit-set-key.spec.ts @@ -0,0 +1,249 @@ +import { faker } from '@faker-js/faker' + +import { BrowserPage } from '../../../pageObjects/browser-page' +import { test, expect } from '../../../fixtures/test' +import { ossStandaloneConfig } from '../../../helpers/conf' +import { + addStandaloneInstanceAndNavigateToIt, + navigateToStandaloneInstance, +} from '../../../helpers/utils' + +test.describe('Browser - Edit Key Operations - Set Key Editing', () => { + let browserPage: BrowserPage + let keyName: string + let cleanupInstance: () => Promise + + test.beforeEach(async ({ page, api: { databaseService } }) => { + browserPage = new BrowserPage(page) + keyName = faker.string.alphanumeric(10) + cleanupInstance = await addStandaloneInstanceAndNavigateToIt( + page, + databaseService, + ) + + await navigateToStandaloneInstance(page) + }) + + test.afterEach(async ({ api: { keyService } }) => { + // Clean up: delete the key if it exists + try { + await keyService.deleteKeyByNameApi( + keyName, + ossStandaloneConfig.databaseName, + ) + } catch (error) { + // Key might already be deleted in test, ignore error + } + + await cleanupInstance() + }) + + test('should add new member to set key successfully', async ({ + api: { keyService }, + }) => { + // Arrange: Create a set key with initial members + const initialMembers = [ + faker.lorem.word(), + faker.lorem.word(), + faker.lorem.word(), + ] + const newMember = faker.lorem.word() + + await keyService.addSetKeyApi( + { keyName, members: initialMembers }, + ossStandaloneConfig, + ) + + // Open key details and verify initial state + await browserPage.openKeyDetailsAndVerify(keyName) + await browserPage.waitForSetMembersToLoad(initialMembers.length) + await browserPage.verifySetContainsMembers(initialMembers) + await browserPage.verifyKeyLength(initialMembers.length.toString()) + + // Add a new member + await browserPage.addMemberToSetKey(newMember) + + // Verify new member appears and length updates + await browserPage.waitForSetLengthToUpdate(initialMembers.length + 1) + await browserPage.verifySetContainsMembers([ + ...initialMembers, + newMember, + ]) + }) + + test('should handle adding duplicate member to set (no length change)', async ({ + api: { keyService }, + }) => { + // Arrange: Create a set key with initial members + const initialMembers = [ + faker.lorem.word(), + faker.lorem.word(), + faker.lorem.word(), + ] + const duplicateMember = initialMembers[0] // Use existing member + + await keyService.addSetKeyApi( + { keyName, members: initialMembers }, + ossStandaloneConfig, + ) + + // Open key details and verify initial state + await browserPage.openKeyDetailsAndVerify(keyName) + await browserPage.waitForSetMembersToLoad(initialMembers.length) + await browserPage.verifySetContainsMembers(initialMembers) + await browserPage.verifyKeyLength(initialMembers.length.toString()) + + // Try to add duplicate member + await browserPage.addMemberToSetKey(duplicateMember) + + // Wait for the operation to complete and verify length remains the same + await browserPage.waitForSetMembersToLoad(initialMembers.length) + await browserPage.verifyKeyLength(initialMembers.length.toString()) + await browserPage.verifySetContainsMembers(initialMembers) + }) + + test('should remove member from set key successfully', async ({ + api: { keyService }, + }) => { + // Arrange: Create a set key with multiple members + const setMembers = [ + faker.lorem.word(), + faker.lorem.word(), + faker.lorem.word(), + faker.lorem.word(), + ] + const memberToRemove = setMembers[1] + + await keyService.addSetKeyApi( + { keyName, members: setMembers }, + ossStandaloneConfig, + ) + + // Open key details and verify initial state (remove member test) + await browserPage.openKeyDetailsAndVerify(keyName) + await browserPage.waitForSetMembersToLoad(setMembers.length) + await browserPage.verifySetContainsMembers(setMembers) + await browserPage.verifyKeyLength(setMembers.length.toString()) + + // Remove a member + await browserPage.removeMemberFromSet(memberToRemove) + + // Verify member was removed and length updated + await browserPage.waitForSetLengthToUpdate(setMembers.length - 1) + await browserPage.verifySetDoesNotContainMembers([memberToRemove]) + + // Verify other members still exist + const remainingMembers = setMembers.filter( + (member) => member !== memberToRemove, + ) + await browserPage.verifySetContainsMembers(remainingMembers) + }) + + test('should search for specific member in set', async ({ + api: { keyService }, + }) => { + // Arrange: Create a set key with various members + const setMembers = ['apple', 'banana', 'orange', 'grape', 'strawberry'] + const searchTerm = 'apple' + + await keyService.addSetKeyApi( + { keyName, members: setMembers }, + ossStandaloneConfig, + ) + + // Open key details and verify initial state + await browserPage.openKeyDetailsAndVerify(keyName) + await browserPage.waitForSetMembersToLoad(setMembers.length) + await browserPage.verifySetContainsMembers(setMembers) + + // Perform search + await browserPage.searchByTheValueInSetKey(searchTerm) + + // Verify search input has the search term + const searchInput = browserPage.page.getByTestId('search') + await expect(searchInput).toHaveValue(searchTerm) + + // Verify search results: searched item visible, others hidden + await browserPage.verifySetSearchResults(searchTerm, setMembers) + + // Clear search and verify all members are visible again + await browserPage.clearSetSearch() + await expect(searchInput).toHaveValue('') + await browserPage.verifySetContainsMembers(setMembers) + }) + + test('should perform mixed operations on set key', async ({ + api: { keyService }, + }) => { + // Arrange: Create a set key with initial members + const initialMembers = [ + faker.lorem.word(), + faker.lorem.word(), + faker.lorem.word(), + ] + const newMember = faker.lorem.word() + const memberToRemove = initialMembers[1] + + await keyService.addSetKeyApi( + { keyName, members: initialMembers }, + ossStandaloneConfig, + ) + + // Open key details and verify initial state + await browserPage.openKeyDetailsAndVerify(keyName) + await browserPage.waitForSetMembersToLoad(initialMembers.length) + await browserPage.verifySetContainsMembers(initialMembers) + await browserPage.verifyKeyLength(initialMembers.length.toString()) + + // Add a new member + await browserPage.addMemberToSetKey(newMember) + await browserPage.waitForSetLengthToUpdate(initialMembers.length + 1) + + // Remove an existing member + await browserPage.removeMemberFromSet(memberToRemove) + await browserPage.waitForSetLengthToUpdate( + initialMembers.length, // Back to original length + ) + + // Verify final state: original members (minus removed) + new member + const finalExpectedMembers = initialMembers + .filter((member) => member !== memberToRemove) + .concat(newMember) + await browserPage.verifySetContainsMembers(finalExpectedMembers) + await browserPage.verifySetDoesNotContainMembers([memberToRemove]) + }) + + test('should handle removing all members from set (key should be deleted)', async ({ + api: { keyService }, + }) => { + // Arrange: Create a set key with a few members + const setMembers = [faker.lorem.word(), faker.lorem.word()] + + await keyService.addSetKeyApi( + { keyName, members: setMembers }, + ossStandaloneConfig, + ) + + // Open key details and verify initial state + await browserPage.openKeyDetailsAndVerify(keyName) + await browserPage.waitForSetMembersToLoad(setMembers.length) + await browserPage.verifySetContainsMembers(setMembers) + + // Remove all members + await setMembers.reduce(async (promise, member) => { + await promise + await browserPage.removeMemberFromSet(member) + }, Promise.resolve()) + + // Verify set is empty (key should be removed when set becomes empty) + await expect + .poll(async () => { + try { + return await browserPage.isKeyDetailsOpen(keyName) + } catch { + return false + } + }) + .toBe(false) + }) +}) diff --git a/tests/playwright/tests/browser/keys-edit/edit-zset-key.spec.ts b/tests/playwright/tests/browser/keys-edit/edit-zset-key.spec.ts new file mode 100644 index 0000000000..122c4438bf --- /dev/null +++ b/tests/playwright/tests/browser/keys-edit/edit-zset-key.spec.ts @@ -0,0 +1,342 @@ +import { faker } from '@faker-js/faker' + +import { BrowserPage } from '../../../pageObjects/browser-page' +import { test, expect } from '../../../fixtures/test' +import { ossStandaloneConfig } from '../../../helpers/conf' +import { + addStandaloneInstanceAndNavigateToIt, + navigateToStandaloneInstance, +} from '../../../helpers/utils' + +test.describe('Browser - Edit ZSet Key', () => { + let browserPage: BrowserPage + let keyName: string + let cleanupInstance: () => Promise + + test.beforeEach(async ({ page, api: { databaseService } }) => { + browserPage = new BrowserPage(page) + keyName = faker.string.alphanumeric(10) + cleanupInstance = await addStandaloneInstanceAndNavigateToIt( + page, + databaseService, + ) + + await navigateToStandaloneInstance(page) + }) + + test.afterEach(async ({ api: { keyService } }) => { + // Clean up: delete the key if it exists + try { + await keyService.deleteKeyByNameApi( + keyName, + ossStandaloneConfig.databaseName, + ) + } catch (error) { + // Key might already be deleted in test, ignore error + } + + await cleanupInstance() + }) + + test('should edit zset member score', async ({ api: { keyService } }) => { + // Arrange + const zsetMembers = [ + { name: faker.lorem.word(), score: 1.5 }, + { name: faker.lorem.word(), score: 2.0 }, + { name: faker.lorem.word(), score: 3.5 }, + ] + const memberToEdit = zsetMembers[1] + const newScore = 5.0 + + await keyService.addZSetKeyApi( + { keyName, members: zsetMembers }, + ossStandaloneConfig, + ) + + // Act + await browserPage.openKeyDetailsByKeyName(keyName) + await browserPage.waitForZsetMembersToLoad(zsetMembers.length) + await browserPage.editZsetMemberScore(memberToEdit.name, newScore) + + // Assert + await browserPage.verifyZsetMemberScore(memberToEdit.name, newScore) + await browserPage.waitForZsetLengthToUpdate(zsetMembers.length) // Length should remain the same + }) + + test('should cancel zset member score edit', async ({ + api: { keyService }, + }) => { + // Arrange + const zsetMembers = [ + { name: faker.lorem.word(), score: 1.5 }, + { name: faker.lorem.word(), score: 2.0 }, + ] + const memberToEdit = zsetMembers[0] + const originalScore = memberToEdit.score + const newScore = 10.0 + + await keyService.addZSetKeyApi( + { keyName, members: zsetMembers }, + ossStandaloneConfig, + ) + + // Act + await browserPage.openKeyDetailsByKeyName(keyName) + await browserPage.waitForZsetMembersToLoad(zsetMembers.length) + await browserPage.cancelZsetMemberScoreEdit(memberToEdit.name, newScore) + + // Assert - score should remain unchanged + await browserPage.verifyZsetMemberScore( + memberToEdit.name, + originalScore, + ) + }) + + test('should add new member to zset', async ({ api: { keyService } }) => { + // Arrange + const zsetMembers = [ + { name: faker.lorem.word(), score: 1.0 }, + { name: faker.lorem.word(), score: 2.0 }, + ] + const newMember = faker.lorem.word() + const newScore = 3.5 + + await keyService.addZSetKeyApi( + { keyName, members: zsetMembers }, + ossStandaloneConfig, + ) + + // Act + await browserPage.openKeyDetailsByKeyName(keyName) + await browserPage.waitForZsetMembersToLoad(zsetMembers.length) + await browserPage.addMemberToZsetKey(newMember, newScore) + + // Assert + const expectedLength = zsetMembers.length + 1 + await browserPage.waitForZsetLengthToUpdate(expectedLength) + await browserPage.verifyZsetMemberExists(newMember) + await browserPage.verifyZsetMemberScore(newMember, newScore) + + // Verify all original members are still present + const allExpectedMembers = [ + ...zsetMembers, + { name: newMember, score: newScore }, + ] + await browserPage.verifyZsetContainsMembers(allExpectedMembers) + }) + + test('should handle adding duplicate member to zset (update score)', async ({ + api: { keyService }, + }) => { + // Arrange + const zsetMembers = [ + { name: faker.lorem.word(), score: 1.0 }, + { name: faker.lorem.word(), score: 2.0 }, + ] + const duplicateMember = zsetMembers[0].name + const newScore = 5.0 + + await keyService.addZSetKeyApi( + { keyName, members: zsetMembers }, + ossStandaloneConfig, + ) + + // Act + await browserPage.openKeyDetailsByKeyName(keyName) + await browserPage.waitForZsetMembersToLoad(zsetMembers.length) + await browserPage.addMemberToZsetKey(duplicateMember, newScore) + + // Assert - length should remain the same (member was updated, not added) + await browserPage.waitForZsetScoreToUpdate(newScore) + await browserPage.waitForZsetLengthToUpdate(zsetMembers.length) + await browserPage.verifyZsetMemberScore(duplicateMember, newScore) + }) + + test('should remove single member from zset', async ({ + api: { keyService }, + }) => { + // Arrange + const zsetMembers = [ + { name: faker.lorem.word(), score: 1.0 }, + { name: faker.lorem.word(), score: 2.0 }, + { name: faker.lorem.word(), score: 3.0 }, + ] + const memberToRemove = zsetMembers[1].name + + await keyService.addZSetKeyApi( + { keyName, members: zsetMembers }, + ossStandaloneConfig, + ) + + // Act + await browserPage.openKeyDetailsByKeyName(keyName) + await browserPage.waitForZsetMembersToLoad(zsetMembers.length) + await browserPage.removeMemberFromZset(memberToRemove) + + // Assert + const expectedLength = zsetMembers.length - 1 + await browserPage.waitForZsetLengthToUpdate(expectedLength) + await browserPage.verifyZsetMemberNotExists(memberToRemove) + await browserPage.verifyZsetDoesNotContainMembers([memberToRemove]) + + // Verify remaining members are still present + const remainingMembers = zsetMembers.filter( + (member) => member.name !== memberToRemove, + ) + await browserPage.verifyZsetContainsMembers(remainingMembers) + }) + + test('should remove multiple members from zset', async ({ + api: { keyService }, + }) => { + // Arrange + const zsetMembers = [ + { name: faker.lorem.word(), score: 1.0 }, + { name: faker.lorem.word(), score: 2.0 }, + { name: faker.lorem.word(), score: 3.0 }, + { name: faker.lorem.word(), score: 4.0 }, + ] + const membersToRemove = [zsetMembers[0].name, zsetMembers[2].name] + + await keyService.addZSetKeyApi( + { keyName, members: zsetMembers }, + ossStandaloneConfig, + ) + + // Act + await browserPage.openKeyDetailsByKeyName(keyName) + await browserPage.waitForZsetMembersToLoad(zsetMembers.length) + + // Remove members one by one + await browserPage.removeMultipleMembersFromZset(membersToRemove) + + // Assert + const expectedLength = zsetMembers.length - membersToRemove.length + await browserPage.waitForZsetLengthToUpdate(expectedLength) + + // Verify removed members are gone + await browserPage.verifyZsetDoesNotContainMembers(membersToRemove) + + // Verify remaining members are still present + const remainingMembers = zsetMembers.filter( + (member) => !membersToRemove.includes(member.name), + ) + await browserPage.verifyZsetContainsMembers(remainingMembers) + }) + + test('should remove all members from zset (delete key when empty)', async ({ + api: { keyService }, + }) => { + // Arrange + const zsetMembers = [ + { name: faker.lorem.word(), score: 1.0 }, + { name: faker.lorem.word(), score: 2.0 }, + ] + + await keyService.addZSetKeyApi( + { keyName, members: zsetMembers }, + ossStandaloneConfig, + ) + + // Act + await browserPage.openKeyDetailsByKeyName(keyName) + await browserPage.waitForZsetMembersToLoad(zsetMembers.length) + + // Remove all members + await browserPage.removeAllZsetMembers(zsetMembers) + + // Assert - key should be deleted when all members are removed + const isDetailsClosed = await browserPage.isKeyDetailsClosed() + expect(isDetailsClosed).toBe(true) + await browserPage.verifyKeyDoesNotExist(keyName) + }) + + test('should search within zset members', async ({ + api: { keyService }, + }) => { + // Arrange + const zsetMembers = [ + { name: 'apple', score: 1.0 }, + { name: 'banana', score: 2.0 }, + { name: 'carrot', score: 3.0 }, + { name: 'date', score: 4.0 }, + ] + const searchTerm = 'apple' // Exact match search + + await keyService.addZSetKeyApi( + { keyName, members: zsetMembers }, + ossStandaloneConfig, + ) + + // Act + await browserPage.openKeyDetailsByKeyName(keyName) + await browserPage.waitForZsetMembersToLoad(zsetMembers.length) + + // First verify all members are visible before search + await browserPage.verifyZsetContainsMembers(zsetMembers) + + // Search for exact member name + await browserPage.searchInZsetMembers(searchTerm) + + // For exact match search, only the searched member should be visible + const expectedVisibleMembers = zsetMembers.filter( + (member) => member.name === searchTerm, + ) + await browserPage.verifyZsetContainsMembers(expectedVisibleMembers) + + // Clear search and verify all members are visible again + await browserPage.clearZsetSearch() + await browserPage.waitForZsetMembersToLoad(zsetMembers.length) + await browserPage.verifyZsetContainsMembers(zsetMembers) + }) + + test('should handle mixed operations on zset', async ({ + api: { keyService }, + }) => { + // Arrange + const zsetMembers = [ + { name: faker.lorem.word(), score: 1.0 }, + { name: faker.lorem.word(), score: 2.0 }, + { name: faker.lorem.word(), score: 3.0 }, + ] + const newMember = faker.lorem.word() + const newScore = 4.5 + const memberToRemove = zsetMembers[1].name + const memberToEdit = zsetMembers[0].name + const editedScore = 10.0 + + await keyService.addZSetKeyApi( + { keyName, members: zsetMembers }, + ossStandaloneConfig, + ) + + // Act + await browserPage.openKeyDetailsByKeyName(keyName) + await browserPage.waitForZsetMembersToLoad(zsetMembers.length) + + // Add a new member + await browserPage.addMemberToZsetKey(newMember, newScore) + await browserPage.waitForZsetLengthToUpdate(zsetMembers.length + 1) + + // Edit existing member's score + await browserPage.editZsetMemberScore(memberToEdit, editedScore) + + // Remove a member + await browserPage.removeMemberFromZset(memberToRemove) + await browserPage.waitForZsetLengthToUpdate(zsetMembers.length) // back to original count + + // Assert final state + await browserPage.verifyZsetMemberExists(newMember) + await browserPage.verifyZsetMemberScore(newMember, newScore) + await browserPage.verifyZsetMemberScore(memberToEdit, editedScore) + await browserPage.verifyZsetMemberNotExists(memberToRemove) + + // Verify final member composition + const expectedFinalMembers = [ + { name: memberToEdit, score: editedScore }, + { name: zsetMembers[2].name, score: zsetMembers[2].score }, + { name: newMember, score: newScore }, + ] + await browserPage.verifyZsetContainsMembers(expectedFinalMembers) + }) +})