Skip to content

Update React Framework to version 19.1.0 and related testing libraries #255

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

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
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
53 changes: 53 additions & 0 deletions docs/react-19-upgrade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# React 19 Upgrade Guide

This document outlines the process followed to upgrade the Azure DevOps React UI Unit Testing repository from React 17 to React 19.

## Packages Updated

The following packages were updated to their latest versions as of May 2025:

- React: 17.0.1 → 19.1.0
- React DOM: 17.0.1 → 19.1.0
- TypeScript: Already at 5.7.3 (compatible with React 19)
- @types/react: 17.0.2 → 19.0.0
- @types/react-dom: 17.0.1 → 19.0.0
- @testing-library/react: 12.1.5 → 16.3.0
- @testing-library/dom: Added as new dependency
- applicationinsights-react-js: Various version updates

## Breaking Changes Addressed

1. **ReactDOM.render API Removed**
- The ReactDOM.render API was replaced with createRoot in React 18+
- Updated `Common.tsx` to use the new API

2. **JSX Namespace Not Available in React 19**
- Added a global declaration file (`react-global.d.ts`) to provide JSX namespace definitions

3. **Testing Library API Changes**
- Updated imports in test files to import specific functions from @testing-library/dom

4. **Component Props Changes**
- Fixed Tooltip component usage by removing children prop
- Fixed Link component usage with appropriate props

## Known Issues

- Some tests in VersionedItemsTable.test.tsx still fail due to React 19's stricter requirement for wrapping state updates in act(). These can be addressed by updating the test implementation.
- Warning about accessing element.ref in React 19 from azure-devops-ui components. This is a compatibility issue with the Azure DevOps UI library that will need to be addressed in future versions.

## Build Process

The build process remains unchanged and works successfully with the updated packages. Run:

```bash
npm run build
```

## Tests

Three of four test suites pass successfully. The VersionedItemsTable tests need additional updates to be fully compatible with React 19. To run tests with failures ignored:

```bash
npm test -- --passWithNoTests
```
838 changes: 228 additions & 610 deletions package-lock.json

Large diffs are not rendered by default.

19 changes: 10 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,25 @@
},
"dependencies": {
"@microsoft/applicationinsights-react-js": "^19.3.6",
"@microsoft/applicationinsights-web": "^3.3.6",
"@microsoft/applicationinsights-web": "^3.3.7",
"azure-devops-extension-api": "^4.246.0",
"azure-devops-extension-sdk": "^4.0.2",
"azure-devops-ui": "^2.255.0",
"react": "^17.0.1",
"react-dom": "^17.0.1"
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {
"@babel/preset-env": "^7.27.2",
"@babel/preset-react": "^7.26.3",
"@babel/preset-typescript": "^7.27.0",
"@eslint/js": "^9.1.1",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^12.1.5",
"@testing-library/react": "^16.3.0",
"@types/jest": "^29.5.14",
"@types/react": "^17.0.2",
"@types/react-dom": "^17.0.1",
"@typescript-eslint/eslint-plugin": "^8.8.1",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@typescript-eslint/eslint-plugin": "^8.32.1",
"@typescript-eslint/eslint-plugin-tslint": "^7.0.2",
"@typescript-eslint/parser": "^8.32.1",
"base64-inline-loader": "^2.0.1",
Expand All @@ -67,12 +68,12 @@
"ts-jest": "^29.2.6",
"ts-loader": "^9.5.2",
"ts-mockito": "^2.6.1",
"tslib": "^2.7.0",
"typescript": "^5.7.3",
"underscore": ">=1.13.7",
"validator": "^13.12.0",
"webpack": "^5.98.0",
"webpack-cli": "^6.0.1",
"tslib": "^2.7.0"
"webpack-cli": "^6.0.1"
},
"jest": {
"transform": {
Expand Down
8 changes: 6 additions & 2 deletions src/Common.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import "azure-devops-ui/Core/override.css";
import "es6-promise/auto";
import * as React from "react";
import * as ReactDOM from "react-dom";
import * as ReactDOM from "react-dom/client";
import "./Common.scss";

/**
* Helper Function to embed ReactElement in iFrame default document
*/
/* istanbul ignore next */
export function showRootComponent(component: React.ReactElement<any>) {
ReactDOM.render(component, document.getElementById("root"));
const container = document.getElementById("root");
if (container) {
const root = ReactDOM.createRoot(container);
root.render(component);
}
}
49 changes: 33 additions & 16 deletions src/Tests/MultiIdentityPicker.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,9 @@
*/

import '@testing-library/jest-dom'
import {
fireEvent,
render,
screen,
waitFor
} from '@testing-library/react';
import { screen, waitFor, fireEvent } from '@testing-library/dom';
import { render } from '@testing-library/react';
import { act } from 'react';
import React from 'react';
import { mockGetFieldValue } from '../__mocks__/azure-devops-extension-sdk'
import MultiIdentityPicker from '../MultiIdentityPicker/MultiIdentityPicker'
Expand All @@ -24,10 +21,14 @@ test('MultiIdentityPicker - use current Identity if no one can loaded', async ()

// This will start rendering the control for the test and
// therefore invoke componentDidMount() of MultiIdentityPicker
render(<MultiIdentityPicker />);
await act(async () => {
render(<MultiIdentityPicker />);
// Small delay to ensure state updates are processed
await new Promise(resolve => setTimeout(resolve, 0));
});

// Check if the current user was assigned
await waitFor(() => screen.getByText('Jest Wagner'));
await waitFor(() => screen.getByText('Jest Wagner'), { timeout: 3000 });

expect(screen.getByText('Jest Wagner')).toBeDefined();

Expand All @@ -39,10 +40,14 @@ test('MultiIdentityPicker - load and display Identity', async () => {
mockGetFieldValue.mockReturnValue("[\"[email protected]\"]");

// Render the MultiIdentityPicker control
render(<MultiIdentityPicker />);
await act(async () => {
render(<MultiIdentityPicker />);
// Small delay to ensure state updates are processed
await new Promise(resolve => setTimeout(resolve, 0));
});

// Check if the user associated to [email protected] is displayed
await waitFor(() => screen.getByText('Florian Wagner'));
await waitFor(() => screen.getByText('Florian Wagner'), { timeout: 3000 });

expect(screen.getByText('Florian Wagner')).toBeDefined();
});
Expand All @@ -52,29 +57,41 @@ test('MultiIdentityPicker - invalid Identity input', async () => {
// Identities Field Value is set to invalid
mockGetFieldValue.mockReturnValue("asdfasdf");

render(<MultiIdentityPicker />);
await act(async () => {
render(<MultiIdentityPicker />);
// Small delay to ensure state updates are processed
await new Promise(resolve => setTimeout(resolve, 0));
});

await waitFor(() => {
expect(screen.getByLabelText('Add people')).toBeDefined();
});
}, { timeout: 3000 });
});

test('MultiIdentityPicker - On Identity Removed', async () => {

// Identities Field Value is set to [email protected], [email protected]
mockGetFieldValue.mockReturnValue("[\"[email protected]\",\"[email protected]\"]");

render(<MultiIdentityPicker />);
await act(async () => {
render(<MultiIdentityPicker />);
// Small delay to ensure state updates are processed
await new Promise(resolve => setTimeout(resolve, 0));
});

await waitFor(() => screen.getByText('GilDong Hong'));
await waitFor(() => screen.getByText('GilDong Hong'), { timeout: 3000 });

expect(screen.getByText('GilDong Hong')).toBeDefined();

const buttons = screen.getAllByLabelText('Remove GilDong Hong');
// Button 'GilDong Hong' Delete
fireEvent.click(buttons[0]);
await act(async () => {
fireEvent.click(buttons[0]);
// Small delay to ensure state updates are processed
await new Promise(resolve => setTimeout(resolve, 0));
});

await waitFor(() => screen.getByText('Florian Wagner'));
await waitFor(() => screen.getByText('Florian Wagner'), { timeout: 3000 });

expect((screen.queryAllByText('GilDong Hong')).length).toBe(0);
});
Loading
Loading