diff --git a/tests/playwright/pageObjects/browser-page.ts b/tests/playwright/pageObjects/browser-page.ts index 858cfc3c00..ac8464617e 100755 --- a/tests/playwright/pageObjects/browser-page.ts +++ b/tests/playwright/pageObjects/browser-page.ts @@ -2442,4 +2442,242 @@ export class BrowserPage extends BasePage { this.page.locator(`[data-testid="hash-field-${fieldName}"]`), ).not.toBeVisible() } + + async editJsonProperty( + propertyKey: string, + newValue: string | number | boolean, + ): Promise { + // TODO: Ideally this should find by property key, but the current DOM structure + // makes it complex to navigate from key to value reliably. For now, we use the + // working approach of finding by current value. + const currentValue = await this.getJsonPropertyValue(propertyKey) + if (!currentValue) { + throw new Error(`Property "${propertyKey}" not found`) + } + + // Find and click the value element + const valueElement = this.page + .getByTestId('json-scalar-value') + .filter({ hasText: currentValue }) + .first() + + await valueElement.click() + await expect(this.inlineItemEditor).toBeVisible() + + // Format and apply the new value + const formattedValue = + typeof newValue === 'string' ? `"${newValue}"` : newValue.toString() + + await this.inlineItemEditor.clear() + await this.inlineItemEditor.fill(formattedValue) + await this.applyButton.click() + await expect(this.inlineItemEditor).not.toBeVisible() + + if (await this.toast.isCloseButtonVisible()) { + await this.toast.closeToast() + } + } + + // Convenience methods that use the generic editJsonProperty method + async editJsonString(propertyKey: string, newValue: string): Promise { + await this.editJsonProperty(propertyKey, newValue) + } + + async editJsonNumber(propertyKey: string, newValue: number): Promise { + await this.editJsonProperty(propertyKey, newValue) + } + + async editJsonBoolean( + propertyKey: string, + newValue: boolean, + ): Promise { + await this.editJsonProperty(propertyKey, newValue) + } + + async addJsonProperty( + key: string, + value: string | number | boolean, + ): Promise { + // For JSON objects, add a new property at the same level + await this.addJsonObjectButton.click() + + // Wait for the form to appear + await expect(this.jsonKeyInput).toBeVisible() + await expect(this.jsonValueInput).toBeVisible() + + // Format the key and value properly for JSON + const formattedKey = `"${key}"` + let formattedValue: string + if (typeof value === 'string') { + formattedValue = `"${value}"` + } else { + formattedValue = value.toString() + } + + // Fill the key and value + await this.jsonKeyInput.clear() + await this.jsonKeyInput.fill(formattedKey) + await this.jsonValueInput.clear() + await this.jsonValueInput.fill(formattedValue) + + // Apply the changes + await this.applyButton.click() + + // Wait for the form to disappear + await expect(this.jsonKeyInput).not.toBeVisible() + + // Close any success toast if it appears + if (await this.toast.isCloseButtonVisible()) { + await this.toast.closeToast() + } + } + + async editEntireJsonStructure(newJsonStructure: string): Promise { + // Switch to Monaco editor + await this.page + .getByRole('button', { name: 'Change editor type' }) + .click() + + // Wait for Monaco editor + const monacoContainer = this.page.getByTestId('monaco-editor-json-data') + await expect(monacoContainer).toBeVisible() + + // Clear and set new JSON content + const textarea = monacoContainer.locator('textarea').first() + await textarea.focus() + await this.page.keyboard.press('Control+A') + await this.page.keyboard.press('Delete') + await textarea.type(newJsonStructure) + + // Wait for button to be enabled and click it + const updateButton = this.page.getByTestId('json-data-update-btn') + await expect(updateButton).toBeEnabled() + await updateButton.click() + + // Close editor and return to tree view + const cancelButton = this.page.getByTestId('json-data-cancel-btn') + if (await cancelButton.isVisible()) { + await cancelButton.click() + } + + if (await this.toast.isCloseButtonVisible()) { + await this.toast.closeToast() + } + } + + async verifyJsonPropertyExists(key: string, value: string): Promise { + // Expand all objects and get the actual value + const actualValue = await this.getJsonPropertyValue(key) + expect(actualValue).toBe(value) + } + + async verifyJsonPropertyNotExists(key: string): Promise { + const actualValue = await this.getJsonPropertyValue(key) + expect(actualValue).toBeNull() + } + + async waitForJsonDetailsToBeVisible(): Promise { + await expect(this.page.getByTestId('json-details')).toBeVisible() + } + + async waitForJsonPropertyUpdate( + key: string, + expectedValue: string, + ): Promise { + await expect + .poll(async () => { + try { + const actualValue = await this.getJsonPropertyValue(key) + return actualValue === expectedValue + } catch (error) { + return false + } + }) + .toBe(true) + } + + async expandAllJsonObjects(): Promise { + // Keep expanding until no more expand buttons exist + while (true) { + const expandButtons = this.page.getByTestId('expand-object') + const count = await expandButtons.count() + + if (count === 0) { + break // No more expand buttons to click + } + + // Click ALL visible expand buttons in this iteration + const buttons = await expandButtons.all() + for (const button of buttons) { + if (await button.isVisible()) { + await button.click() + } + } + + // Wait for DOM to be ready before checking for new buttons + await this.page.waitForLoadState('domcontentloaded') + } + } + + async getJsonPropertyValue(propertyName: string): Promise { + // Expand all objects to make sure we can see the property + await this.expandAllJsonObjects() + + // Get the JSON content and look for the property with a simple approach + const jsonContent = await this.jsonKeyValue.textContent() + if (!jsonContent) return null + + // Use a more precise regex pattern for different value types + // Try patterns for strings, numbers, and booleans + const patterns = [ + new RegExp(`${propertyName}:"([^"]*)"`, 'g'), // String values: name:"value" + new RegExp(`${propertyName}:(\\d+(?:\\.\\d+)?)`, 'g'), // Number values: age:25 + new RegExp(`${propertyName}:(true|false)`, 'g'), // Boolean values: active:true + ] + + for (const pattern of patterns) { + pattern.lastIndex = 0 // Reset regex state + const match = pattern.exec(jsonContent) + if (match && match[1]) { + return match[1] + } + } + + return null + } + + async verifyJsonStructureValid(): Promise { + // Check that no JSON error is displayed + await expect(this.jsonError).not.toBeVisible() + + // Check that the JSON data container is visible + await expect(this.jsonKeyValue).toBeVisible() + } + + async cancelJsonScalarValueEdit(propertyKey: string): Promise { + // Store original value, start editing, then cancel + const originalValue = await this.getJsonPropertyValue(propertyKey) + if (!originalValue) { + throw new Error(`Property "${propertyKey}" not found`) + } + + await this.expandAllJsonObjects() + + // Find the element containing this value + const targetElement = this.page + .getByTestId('json-scalar-value') + .filter({ hasText: originalValue }) + .first() + + // Start edit, make change, then cancel + await targetElement.click() + await expect(this.inlineItemEditor).toBeVisible() + await this.inlineItemEditor.fill('"canceled_value"') + await this.page.keyboard.press('Escape') + await expect(this.inlineItemEditor).not.toBeVisible() + + // Verify no change occurred + const finalValue = await this.getJsonPropertyValue(propertyKey) + expect(finalValue).toBe(originalValue) + } } diff --git a/tests/playwright/tests/browser/keys-edit/edit-json-key.spec.ts b/tests/playwright/tests/browser/keys-edit/edit-json-key.spec.ts new file mode 100644 index 0000000000..2aa1b0460d --- /dev/null +++ b/tests/playwright/tests/browser/keys-edit/edit-json-key.spec.ts @@ -0,0 +1,195 @@ +import { faker } from '@faker-js/faker' + +import { BrowserPage } from '../../../pageObjects/browser-page' +import { test } from '../../../fixtures/test' +import { ossStandaloneConfig } from '../../../helpers/conf' +import { + addStandaloneInstanceAndNavigateToIt, + navigateToStandaloneInstance, +} from '../../../helpers/utils' + +test.describe('Browser - Edit JSON 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 JSON scalar values (string, number, boolean)', async ({ + api: { keyService }, + }) => { + // Arrange + const initialValue = { + name: faker.person.firstName(), + age: faker.number.int({ min: 7, max: 19 }), + active: true, + score: 87.5, + count: 10, + } + const newName = faker.person.firstName() + const newAge = faker.number.int({ min: 20, max: 90 }) + + await keyService.addJsonKeyApi( + { keyName, value: initialValue }, + ossStandaloneConfig, + ) + + // Act + await browserPage.openKeyDetailsByKeyName(keyName) + await browserPage.waitForJsonDetailsToBeVisible() + + // Edit string value + await browserPage.editJsonString('name', newName) + await browserPage.waitForJsonPropertyUpdate('name', newName) + + // Edit number values + await browserPage.editJsonNumber('age', newAge) + await browserPage.waitForJsonPropertyUpdate('age', newAge.toString()) + + // Edit boolean value + await browserPage.editJsonBoolean('active', false) + await browserPage.waitForJsonPropertyUpdate('active', 'false') + + // Assert - verify all changes are applied and structure is valid + await browserPage.verifyJsonStructureValid() + }) + + test('should cancel JSON scalar value edit', async ({ + api: { keyService }, + }) => { + // Arrange + const initialValue = { + name: faker.person.firstName(), + score: faker.number.int({ min: 1, max: 100 }), + } + + await keyService.addJsonKeyApi( + { keyName, value: initialValue }, + ossStandaloneConfig, + ) + + // Act + await browserPage.openKeyDetailsByKeyName(keyName) + await browserPage.waitForJsonDetailsToBeVisible() + + // Cancel the scalar value edit for the 'name' property + await browserPage.cancelJsonScalarValueEdit('name') + + // Assert - original value should remain unchanged + await browserPage.verifyJsonPropertyExists('name', initialValue.name) + }) + + test('should add new property to JSON object', async ({ + api: { keyService }, + }) => { + // Arrange + const initialValue = { + name: faker.person.firstName(), + age: faker.number.int({ min: 18, max: 80 }), + } + const newProperty = 'email' + const newValue = faker.internet.email() + + await keyService.addJsonKeyApi( + { keyName, value: initialValue }, + ossStandaloneConfig, + ) + + // Act + await browserPage.openKeyDetailsByKeyName(keyName) + await browserPage.waitForJsonDetailsToBeVisible() + + // Add a new property using clean API + await browserPage.addJsonProperty(newProperty, newValue) + + // Assert + await browserPage.waitForJsonPropertyUpdate(newProperty, newValue) + + // Verify original properties are still present + await browserPage.verifyJsonPropertyExists('name', initialValue.name) + await browserPage.verifyJsonPropertyExists( + 'age', + initialValue.age.toString(), + ) + + // Verify key length increased + const expectedLength = Object.keys(initialValue).length + 1 + await browserPage.verifyKeyLength(expectedLength.toString()) + }) + + test('should edit entire JSON structure', async ({ + api: { keyService }, + }) => { + // Arrange + const initialValue = { + name: faker.person.firstName(), + age: faker.number.int({ min: 18, max: 80 }), + } + const newStructure = { + fullName: faker.person.fullName(), + email: faker.internet.email(), + isActive: true, + metadata: { + createdAt: new Date().toISOString(), + version: 1, + }, + } + + await keyService.addJsonKeyApi( + { keyName, value: initialValue }, + ossStandaloneConfig, + ) + + // Act + await browserPage.openKeyDetailsByKeyName(keyName) + await browserPage.waitForJsonDetailsToBeVisible() + + // Edit the entire JSON structure + await browserPage.editEntireJsonStructure(JSON.stringify(newStructure)) + + // Assert + await browserPage.waitForJsonPropertyUpdate( + 'fullName', + newStructure.fullName, + ) + await browserPage.verifyJsonPropertyExists('email', newStructure.email) + await browserPage.verifyJsonPropertyExists('isActive', 'true') + + // Verify metadata object and its nested properties exist + // The metadata object should contain the nested properties + await browserPage.verifyJsonPropertyExists( + 'createdAt', + newStructure.metadata.createdAt, + ) + await browserPage.verifyJsonPropertyExists('version', '1') + + // Verify old properties are no longer present + await browserPage.verifyJsonPropertyNotExists('name') + await browserPage.verifyJsonPropertyNotExists('age') + + await browserPage.verifyJsonStructureValid() + }) +})