From 0247642e40f2eeeba289d899a7097c6398efaf6b Mon Sep 17 00:00:00 2001 From: Rami Farhat Date: Wed, 22 Jul 2026 08:10:53 -0700 Subject: [PATCH 1/7] fix: restore Fluent UI form behavior Fix controlled dropdown displays, SAS expiration input, decoder form submission, and immutable cloud message property updates. Add interaction regression coverage and bump the app version to 0.15.18. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 81c06575-2d45-4d7f-a9de-fa3e258727d8 --- package-lock.json | 4 +- package.json | 2 +- .../components/cloudToDeviceMessage.spec.tsx | 25 ++++++++ .../components/cloudToDeviceMessage.tsx | 40 +++++++----- .../deviceContentTypePanel.spec.tsx | 19 ++++++ .../components/deviceContentTypePanel.tsx | 4 +- .../components/deviceQueryClause.spec.tsx | 17 ++++++ .../components/deviceQueryClause.tsx | 24 ++++++++ .../sasTokenGenerationView.spec.tsx | 61 ++++++++++++++++++- .../components/sasTokenGenerationView.tsx | 40 +++++++----- 10 files changed, 203 insertions(+), 33 deletions(-) diff --git a/package-lock.json b/package-lock.json index 519e96a0..7a0d871c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "azure-iot-explorer", - "version": "0.15.16", + "version": "0.15.18", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "azure-iot-explorer", - "version": "0.15.16", + "version": "0.15.18", "license": "MIT", "dependencies": { "@azure/core-amqp": "^4.4.2", diff --git a/package.json b/package.json index 27e6bb5a..4d77cc88 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "azure-iot-explorer", - "version": "0.15.17", + "version": "0.15.18", "description": "This project welcomes contributions and suggestions. Most contributions require you to agree to a\r Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us\r the rights to use your contribution. For details, visit https://cla.microsoft.com.", "main": "host/electron.js", "build": { diff --git a/src/app/devices/cloudToDeviceMessage/components/cloudToDeviceMessage.spec.tsx b/src/app/devices/cloudToDeviceMessage/components/cloudToDeviceMessage.spec.tsx index 5b85ee23..13c8387f 100644 --- a/src/app/devices/cloudToDeviceMessage/components/cloudToDeviceMessage.spec.tsx +++ b/src/app/devices/cloudToDeviceMessage/components/cloudToDeviceMessage.spec.tsx @@ -4,6 +4,7 @@ **********************************************************/ import * as React from 'react'; import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { MemoryRouter } from 'react-router-dom'; import { CloudToDeviceMessage } from './cloudToDeviceMessage'; import * as AsyncSagaReducer from '../../../shared/hooks/useAsyncSagaReducer'; @@ -47,4 +48,28 @@ describe('CloudToDeviceMessage', () => { expect(screen.getByText('cloudToDeviceMessage.properties.addCustomProperty')).toBeInTheDocument(); }); + + it('keeps a system property selection after the property list rerenders', async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole('button', { + name: 'cloudToDeviceMessage.properties.addSystemProperty' + })); + await user.click(screen.getByRole('menuitem', { + name: 'cloudToDeviceMessage.properties.systemProperties.ack.displayName' + })); + + const ackDropdown = screen.getByRole('combobox'); + await user.click(ackDropdown); + await user.click(screen.getByRole('option', { + name: 'cloudToDeviceMessage.properties.systemProperties.ack.full' + })); + await user.click(screen.getByRole('button', { + name: 'cloudToDeviceMessage.properties.addCustomProperty' + })); + + expect(screen.getByRole('combobox').textContent) + .toContain('cloudToDeviceMessage.properties.systemProperties.ack.full'); + }); }); diff --git a/src/app/devices/cloudToDeviceMessage/components/cloudToDeviceMessage.tsx b/src/app/devices/cloudToDeviceMessage/components/cloudToDeviceMessage.tsx index 47ec53d8..9829d984 100644 --- a/src/app/devices/cloudToDeviceMessage/components/cloudToDeviceMessage.tsx +++ b/src/app/devices/cloudToDeviceMessage/components/cloudToDeviceMessage.tsx @@ -283,40 +283,52 @@ export const CloudToDeviceMessage: React.FC = () => { }; const renderAckDropdown = ( property: PropertyItem) => { - const index = findMatchingItemIndex(property); + const options = [ + { value: 'full', text: t(ResourceKeys.cloudToDeviceMessage.properties.systemProperties.ack.full) }, + { value: 'positive', text: t(ResourceKeys.cloudToDeviceMessage.properties.systemProperties.ack.positive) }, + { value: 'negative', text: t(ResourceKeys.cloudToDeviceMessage.properties.systemProperties.ack.negative) } + ]; + const selectedOption = options.find(option => option.value === property.value); const onDropdownSelectedKeyChanged = (event: React.SyntheticEvent, data: { optionValue?: string }): void => { - const items = properties; - items[index] = {...items[index], value: data.optionValue}; - setProperties(items); + setProperties(currentProperties => currentProperties.map(item => + item.index === property.index ? {...item, value: data.optionValue ?? ''} : item)); }; return ( - - - + {options.map(option => ( + + ))} ); }; const renderEncodingDropdown = (property: PropertyItem) => { - const index = findMatchingItemIndex(property); + const options = [ + { value: 'utf-8', text: t(ResourceKeys.cloudToDeviceMessage.properties.systemProperties.contentEncoding.utf8) }, + { value: 'utf-16', text: t(ResourceKeys.cloudToDeviceMessage.properties.systemProperties.contentEncoding.utf16) }, + { value: 'utf-32', text: t(ResourceKeys.cloudToDeviceMessage.properties.systemProperties.contentEncoding.utf32) } + ]; + const selectedOption = options.find(option => option.value === property.value); const onDropdownSelectedKeyChanged = (event: React.SyntheticEvent, data: { optionValue?: string }): void => { - const items = properties; - items[index] = {...items[index], value: data.optionValue}; - setProperties(items); + setProperties(currentProperties => currentProperties.map(item => + item.index === property.index ? {...item, value: data.optionValue ?? ''} : item)); }; return ( - - - + {options.map(option => ( + + ))} ); }; diff --git a/src/app/devices/deviceEvents/components/deviceContentTypePanel.spec.tsx b/src/app/devices/deviceEvents/components/deviceContentTypePanel.spec.tsx index ecc790c1..b2dcd25b 100644 --- a/src/app/devices/deviceEvents/components/deviceContentTypePanel.spec.tsx +++ b/src/app/devices/deviceEvents/components/deviceContentTypePanel.spec.tsx @@ -4,6 +4,7 @@ **********************************************************/ import * as React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { DeviceContentTypePanel, DeviceContentTypePanelProps } from './deviceContentTypePanel'; import * as deviceEventsStateContext from '../context/deviceEventsStateContext'; @@ -49,6 +50,24 @@ describe('DeviceContentTypePanel', () => { expect(screen.getByText('deviceEvents.customizeContentType.contentTypeOption.label')).toBeInTheDocument(); expect(screen.getByText('deviceEvents.customizeContentType.save')).toBeInTheDocument(); + expect(screen.getByRole('combobox', { + name: 'deviceEvents.customizeContentType.contentTypeOption.label' + }).textContent).toContain('JSON'); + }); + + it('submits the selected decoder configuration when save is clicked', async () => { + const user = userEvent.setup(); + render(); + + const saveButton = screen.getByRole('button', { name: 'deviceEvents.customizeContentType.save' }); + expect(saveButton).toHaveAttribute('type', 'submit'); + await user.click(saveButton); + + expect(mockSetDecoderInfo).toHaveBeenCalledWith({ + decodeType: 'JSON', + decoderFile: undefined, + decoderPrototype: '' + }); }); it('calls onToggleContentTypePanel when close button is clicked', () => { diff --git a/src/app/devices/deviceEvents/components/deviceContentTypePanel.tsx b/src/app/devices/deviceEvents/components/deviceContentTypePanel.tsx index 5ddd97a1..06c63933 100644 --- a/src/app/devices/deviceEvents/components/deviceContentTypePanel.tsx +++ b/src/app/devices/deviceEvents/components/deviceContentTypePanel.tsx @@ -91,7 +91,8 @@ export const DeviceContentTypePanel: React.FC = pro > @@ -139,6 +140,7 @@ export const DeviceContentTypePanel: React.FC = pro }