Skip to content

Commit 62014e2

Browse files
feat: upgrade React 16 to 18
Upgrade react/react-dom to ^18.3.1, react-mosaic-component to ^6.1.1, and @testing-library/react to ^16.1.0. Migrate ReactDOM.render() to createRoot(). Add Blueprint v3 type augmentations for React 18 children prop compatibility. Fix controlled input behavior in settings-electron.tsx and null-safe context access in output.tsx. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3194677 commit 62014e2

10 files changed

Lines changed: 267 additions & 231 deletions

File tree

package.json

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@
6767
"package-json": "^7.0.0",
6868
"parse-env-string": "^1.0.1",
6969
"prettier": "^3.6.2",
70-
"react": "^16.14.0",
71-
"react-dom": "^16.14.0",
72-
"react-mosaic-component": "^4.1.1",
70+
"react": "^18.3.1",
71+
"react-dom": "^18.3.1",
72+
"react-mosaic-component": "^6.1.1",
7373
"react-window": "^1.8.10",
7474
"semver": "^7.3.4",
7575
"shell-env": "^3.0.1",
@@ -94,16 +94,16 @@
9494
"@reforged/maker-appimage": "^5.1.0",
9595
"@testing-library/dom": "^10.4.0",
9696
"@testing-library/jest-dom": "^6.6.3",
97-
"@testing-library/react": "^12.1.5",
97+
"@testing-library/react": "^16.1.0",
9898
"@testing-library/user-event": "^14.5.2",
9999
"@tsconfig/node22": "^22.0.2",
100100
"@types/classnames": "^2.2.11",
101101
"@types/fs-extra": "^9.0.7",
102102
"@types/getos": "^3.0.1",
103103
"@types/node": "^22.19.1",
104104
"@types/parse-env-string": "^1.0.2",
105-
"@types/react": "^16.14.0",
106-
"@types/react-dom": "^16.9.11",
105+
"@types/react": "^18.3.0",
106+
"@types/react-dom": "^18.3.0",
107107
"@types/react-window": "^1.8.8",
108108
"@types/semver": "^7.3.4",
109109
"@types/tmp": "0.2.0",
@@ -162,7 +162,8 @@
162162
},
163163
"resolutions": {
164164
"@electron-forge/maker-base": "8.0.0-alpha.3",
165-
"@electron-forge/shared-types": "8.0.0-alpha.3"
165+
"@electron-forge/shared-types": "8.0.0-alpha.3",
166+
"@types/react": "^18.3.0"
166167
},
167168
"packageManager": "yarn@4.10.3+sha512.c38cafb5c7bb273f3926d04e55e1d8c9dfa7d9c3ea1f36a4868fa028b9e5f72298f0b7f401ad5eb921749eb012eb1c3bb74bf7503df3ee43fd600d14a018266f",
168169
"dependenciesMeta": {

rtl-spec/components/commands-address-bar.spec.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react';
22

3-
import { render } from '@testing-library/react';
3+
import { render, waitFor } from '@testing-library/react';
44
import { userEvent } from '@testing-library/user-event';
55
import { runInAction } from 'mobx';
66
import { beforeEach, describe, expect, it } from 'vitest';
@@ -76,7 +76,9 @@ describe('AddressBar component', () => {
7676
runInAction(() => {
7777
store.activeGistAction = action;
7878
});
79-
const btn = getByRole('button');
80-
expect(btn).toBeDisabled();
79+
await waitFor(() => {
80+
const btn = getByRole('button');
81+
expect(btn).toBeDisabled();
82+
});
8183
});
8284
});

rtl-spec/components/commands-publish-button.spec.tsx

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Octokit } from '@octokit/rest';
2+
import { act, waitFor } from '@testing-library/react';
23
import { beforeEach, describe, expect, it, vi } from 'vitest';
34

45
import {
@@ -273,7 +274,7 @@ describe('Action button component', () => {
273274
// create a button that's primed to update gistId
274275
state.gistId = gistId;
275276
({ instance } = createActionButton());
276-
instance.setState({ actionType: GistActionType.update });
277+
act(() => instance.setState({ actionType: GistActionType.update }));
277278

278279
mocktokit.gists.get.mockImplementation(() => {
279280
return {
@@ -316,7 +317,7 @@ describe('Action button component', () => {
316317

317318
// create a button primed to delete gistId
318319
({ instance } = createActionButton());
319-
instance.setState({ actionType: GistActionType.delete });
320+
act(() => instance.setState({ actionType: GistActionType.delete }));
320321
});
321322

322323
it('attempts to delete an existing Gist', async () => {
@@ -349,11 +350,19 @@ describe('Action button component', () => {
349350
} = createActionButton();
350351
expect(container.querySelector('fieldset')).not.toBeDisabled();
351352

352-
state.activeGistAction = gistActionState;
353-
expect(container.querySelector('fieldset')).toBeDisabled();
353+
act(() => {
354+
state.activeGistAction = gistActionState;
355+
});
356+
await waitFor(() => {
357+
expect(container.querySelector('fieldset')).toBeDisabled();
358+
});
354359

355-
state.activeGistAction = GistActionState.none;
356-
expect(container.querySelector('fieldset')).not.toBeDisabled();
360+
act(() => {
361+
state.activeGistAction = GistActionState.none;
362+
});
363+
await waitFor(() => {
364+
expect(container.querySelector('fieldset')).not.toBeDisabled();
365+
});
357366
}
358367

359368
it('while publishing', async () => {

rtl-spec/components/editors-toolbar-button.spec.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ describe('Editor toolbar button component', () => {
4646
getRoot: vi.fn(),
4747
},
4848
mosaicId: 'test',
49+
blueprintNamespace: 'bp3',
4950
};
5051

5152
({ state: store } = window.app);

src/blueprint-react18.d.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* Type augmentations for Blueprint.js v3 components to work with React 18.
3+
*
4+
* React 18 removed implicit `children` from React.Component props.
5+
* Blueprint v3 class components don't declare `children` in their prop types,
6+
* so they fail type-checking with \@types/react 18.
7+
*
8+
* This file adds `children` to the affected Blueprint component props.
9+
* It can be removed when Blueprint is replaced with shadcn/ui.
10+
*/
11+
12+
import { ReactNode } from 'react';
13+
14+
declare module '@blueprintjs/core' {
15+
interface IAlertProps {
16+
children?: ReactNode;
17+
}
18+
interface IDialogProps {
19+
children?: ReactNode;
20+
}
21+
interface IFormGroupProps {
22+
children?: ReactNode;
23+
}
24+
interface IRadioGroupProps {
25+
children?: ReactNode;
26+
}
27+
}
28+
29+
declare module '@blueprintjs/select' {
30+
interface ISelectProps<T> {
31+
children?: ReactNode;
32+
}
33+
}
34+
35+
declare module '@blueprintjs/popover2' {
36+
interface IPopover2Props<T> {
37+
children?: ReactNode;
38+
}
39+
interface ITooltip2Props<T> {
40+
children?: ReactNode;
41+
}
42+
}

src/renderer/app.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export class App {
114114
* Initial setup call, loading Monaco and kicking off the React
115115
* render process.
116116
*/
117-
public async setup(): Promise<void | Element | React.Component> {
117+
public async setup(): Promise<void> {
118118
if (this.state.isUsingSystemTheme) {
119119
await this.loadTheme(getCurrentTheme().file);
120120
} else {
@@ -123,13 +123,13 @@ export class App {
123123

124124
const [
125125
{ default: React },
126-
{ render },
126+
{ createRoot },
127127
{ Dialogs },
128128
{ OutputEditorsWrapper },
129129
{ Header },
130130
] = await Promise.all([
131131
import('react'),
132-
import('react-dom'),
132+
import('react-dom/client'),
133133
import('./components/dialogs.js'),
134134
import('./components/output-editors-wrapper.js'),
135135
import('./components/header.js'),
@@ -147,7 +147,8 @@ export class App {
147147
</div>
148148
);
149149

150-
const rendered = render(app, document.getElementById('app'));
150+
const root = createRoot(document.getElementById('app')!);
151+
root.render(app);
151152

152153
this.setupResizeListener();
153154
this.setupOfflineListener();
@@ -161,8 +162,6 @@ export class App {
161162
window.ElectronFiddle.addEventListener('set-show-me-template', () => {
162163
window.ElectronFiddle.setShowMeTemplate(this.state.templateName);
163164
});
164-
165-
return rendered;
166165
}
167166

168167
private setupTypeListeners() {

src/renderer/components/output.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ export const Output = observer(
133133

134134
const { isConsoleShowing } = this.props.appState;
135135

136-
if (this.context.mosaicActions) {
136+
if (this.context?.mosaicActions) {
137137
const mosaicTree = this.context.mosaicActions.getRoot();
138138
if (isParentNode(mosaicTree)) {
139139
// splitPercentage defines the percentage of space the first panel takes

src/renderer/components/settings-electron.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -204,13 +204,14 @@ export const ElectronSettings = observer(
204204
* Handles a change in which channels should be displayed.
205205
*/
206206
public handleChannelChange(event: React.FormEvent<HTMLInputElement>) {
207-
const { id, checked } = event.currentTarget;
207+
const { id } = event.currentTarget;
208208
const { appState } = this.props;
209+
const channel = id as ElectronReleaseChannel;
209210

210-
if (!checked) {
211-
appState.hideChannels([id as ElectronReleaseChannel]);
211+
if (appState.channelsToShow.includes(channel)) {
212+
appState.hideChannels([channel]);
212213
} else {
213-
appState.showChannels([id as ElectronReleaseChannel]);
214+
appState.showChannels([channel]);
214215
}
215216
}
216217

tests/renderer/app-spec.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { act } from '@testing-library/react';
12
import * as semver from 'semver';
23
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
34

@@ -59,10 +60,15 @@ describe('App component', () => {
5960
it('renders the app', async () => {
6061
vi.useFakeTimers();
6162

62-
const result = (await app.setup()) as HTMLDivElement;
63-
vi.runAllTimers();
63+
await act(async () => {
64+
await app.setup();
65+
await vi.advanceTimersByTimeAsync(100);
66+
});
6467

65-
expect(result.innerHTML).toBe('Header;OutputEditorsWrapper;Dialogs;');
68+
const appEl = document.getElementById('app')!;
69+
expect(appEl.innerHTML).toContain('Header;');
70+
expect(appEl.innerHTML).toContain('OutputEditorsWrapper;');
71+
expect(appEl.innerHTML).toContain('Dialogs;');
6672

6773
vi.useRealTimers();
6874
});

0 commit comments

Comments
 (0)