Skip to content

Commit

Permalink
add React 19 support (#521)
Browse files Browse the repository at this point in the history
* add React 19 support

* refactor: update SCSS imports to use @use syntax and improve test snapshots

* fix pipeline

* chore: update version to v1.4.5 and remove npm-install-peers from workflow

* chore: add tslib dependency version 2.8.1 to package.json and update yarn.lock

* chore: update GitHub Actions to use latest versions of checkout, setup-node, and cache actions

* chore: add @testing-library/dom dependency version 10.4.0 to package.json and update yarn.lock

* fix codecov

---------

Co-authored-by: Konstantin Nikolaev <[email protected]>
  • Loading branch information
kpachbiu88 and Konstantin Nikolaev authored Jan 16, 2025
1 parent 6f88d6d commit b0851cf
Show file tree
Hide file tree
Showing 15 changed files with 3,270 additions and 5,776 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-node@v1
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18.17.0
node-version: 22
registry-url: https://registry.npmjs.org/
- run: yarn install
- run: yarn build
Expand Down
21 changes: 12 additions & 9 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-node@v1
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18.17.0
node-version: 22
- name: Cache node modules
uses: actions/cache@v1
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
Expand All @@ -24,10 +24,13 @@ jobs:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-node@v1
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18.17.0
node-version: 22
- run: yarn install
- run: ./node_modules/.bin/npm-install-peers
- run: yarn test && ./node_modules/.bin/codecov -t ${{secrets.CODECOV_REPO_TOKEN}}
- run: yarn test
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_REPO_TOKEN }}
72 changes: 30 additions & 42 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"name": "react-terminal",
"version": "v1.4.4",
"version": "v1.4.5",
"description": "React component that renders a terminal",
"main": "dist/index.js",
"module": "dist/index.es.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "./node_modules/.bin/cross-env NODE_ENV=production rollup -c",
"build": "cross-env NODE_ENV=production rollup -c",
"watch": "rollup -cw",
"test": "jest"
},
Expand All @@ -32,52 +32,44 @@
"url": "https://github.com/bony2023/react-terminal/issues"
},
"homepage": "https://github.com/bony2023/react-terminal#readme",
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"ua-parser-js": "^2.0.0"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.1.4",
"@testing-library/react": "^15.0.0",
"@testing-library/user-event": "^14.2.0",
"@types/enzyme": "^3.10.4",
"@types/enzyme-adapter-react-16": "^1.0.5",
"@types/jest": "^29.4.0",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"@types/react-test-renderer": "^18.0.0",
"@typescript-eslint/eslint-plugin": "^5.50.0",
"@typescript-eslint/parser": "^5.50.0",
"cheerio": "1.0.0-rc.12",
"codecov": "^3.6.1",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^12.1.2",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.1.0",
"@testing-library/user-event": "^14.5.2",
"@types/jest": "^29.5.14",
"@types/react": "^19.0.7",
"@types/react-dom": "^19.0.3",
"@types/react-test-renderer": "^19.0.0",
"@typescript-eslint/eslint-plugin": "^8.20.0",
"@typescript-eslint/parser": "^8.20.0",
"codecov": "^3.8.2",
"cross-env": "^7.0.3",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.7",
"enzyme-to-json": "^3.4.3",
"eslint": "^8.33.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.5.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.4.1",
"jest-environment-jsdom": "^29.4.1",
"npm-install-peers": "^1.2.1",
"postcss": "^8.4.21",
"react-test-renderer": "^18.2.0",
"rollup": "^4.3.0",
"install": "^0.13.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"npm": "^11.0.0",
"postcss": "^8.5.1",
"rollup": "^4.30.1",
"rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.36.0",
"sass": "^1.58.0",
"ts-jest": "^29.0.5",
"ts-loader": "^9.4.2",
"typescript": "^5.2.2"
},
"resolutions": {
"cheerio": "1.0.0-rc.10"
},
"peerDependencies": {
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"sass": "^1.83.4",
"ts-jest": "^29.2.5",
"tslib": "^2.8.1",
"typescript": "^5.7.3"
},
"jest": {
"testEnvironment": "jsdom",
Expand All @@ -100,9 +92,5 @@
],
"coverageDirectory": "./coverage/",
"collectCoverage": true
},
"dependencies": {
"prop-types": "^15.7.2",
"react-device-detect": "2.2.3"
}
}
13 changes: 5 additions & 8 deletions rollup.config.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import typescript from "rollup-plugin-typescript2";
import typescript from "@rollup/plugin-typescript";
import postcss from "rollup-plugin-postcss";
import { terser } from "rollup-plugin-terser";
import pkg from "./package.json" assert {type: "json"};
import typescriptModule from "typescript";
import terser from "@rollup/plugin-terser";
import pkg from "./package.json" with { type: "json" };

export default {
input: "src/index.tsx",
Expand All @@ -26,11 +25,9 @@ export default {
postcss({
extract: false,
modules: true,
use: ["sass"]
}),
typescript({
typescript: typescriptModule
use: ["sass"],
}),
typescript(),
process.env.NODE_ENV === "production" ? terser() : null
]
};
6 changes: 6 additions & 0 deletions src/common/Utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { UAParser } from 'ua-parser-js';

export default class Utils {
static splitStringAtIndex(value: string, index: number) {
if (!value) {
return ["", ""];
}
return [value.substring(0, index), value.substring(index)];
}
static isMobile(): boolean {
const { device } = UAParser(window.navigator.userAgent);
return device.is('mobile');
}
}
42 changes: 20 additions & 22 deletions src/components/Terminal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import * as React from "react";
import PropTypes from "prop-types";
import { isMobile } from "react-device-detect";

import { StyleContext } from "../contexts/StyleContext";
import { ThemeContext } from "../contexts/ThemeContext";
Expand All @@ -9,7 +7,23 @@ import { useClickOutsideEvent } from "../hooks/terminal";
import Controls from "./Controls";
import Editor from "./Editor";

export default function Terminal({
import Utils from "../common/Utils";

interface TerminalProps {
enableInput: boolean
caret: boolean
theme: string
showControlBar: boolean
showControlButtons: boolean
controlButtonLabels: string[]
prompt: string
commands: Record<string, (...args: never) => void>
welcomeMessage: string | (() => void) | React.ReactNode
errorMessage: string | ((...args: never) => void) | React.ReactNode
defaultHandler: ((...args: never) => void) | null
};

const Terminal: React.FC<TerminalProps> = ({
enableInput = true,
caret = true,
theme = "light",
Expand All @@ -21,9 +35,9 @@ export default function Terminal({
welcomeMessage = "",
errorMessage = "not found!",
defaultHandler = null,
}) {
}) => {
const wrapperRef = React.useRef(null);
const [consoleFocused, setConsoleFocused] = React.useState(!isMobile);
const [consoleFocused, setConsoleFocused] = React.useState(!Utils.isMobile());
const style = React.useContext(StyleContext);
const themeStyles = React.useContext(ThemeContext);

Expand Down Expand Up @@ -61,20 +75,4 @@ export default function Terminal({
);
}

Terminal.propTypes = {
enableInput:PropTypes.bool,
caret: PropTypes.bool,
theme: PropTypes.string,
showControlBar: PropTypes.bool,
showControlButtons: PropTypes.bool,
controlButtonLabels: PropTypes.arrayOf(PropTypes.string),
prompt: PropTypes.string,
commands: PropTypes.objectOf(PropTypes.oneOfType([
PropTypes.string,
PropTypes.func,
PropTypes.node
])),
welcomeMessage: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.node]),
errorMessage: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.node]),
defaultHandler: PropTypes.func,
};
export default Terminal;
5 changes: 2 additions & 3 deletions src/hooks/editor.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as React from "react";
import { isMobile } from "react-device-detect";

import { StyleContext } from "../contexts/StyleContext";
import { ThemeContext } from "../contexts/ThemeContext";
Expand Down Expand Up @@ -230,7 +229,7 @@ export const useCurrentLine = (

React.useEffect(
() => {
if (!isMobile) {
if (!Utils.isMobile()) {
return;
}
},
Expand All @@ -255,7 +254,7 @@ export const useCurrentLine = (
}
},[])

const mobileInput = isMobile && enableInput? (
const mobileInput = Utils.isMobile() && enableInput? (
<div className={style.mobileInput}>
<input
type="text"
Expand Down
5 changes: 3 additions & 2 deletions src/hooks/terminal.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import * as React from "react";
import { isMobile } from "react-device-detect";

import Utils from "../common/Utils";

export const useClickOutsideEvent = (ref: any, clickedInside: boolean, setClickedInside: any) => {
const handleClickOutside = (event: any) => {
if (ref.current && !ref.current.contains(event.target)) {
setClickedInside(false);
} else if (isMobile) {
} else if (Utils.isMobile()) {
setClickedInside(!clickedInside);
} else {
setClickedInside(true);
Expand Down
10 changes: 5 additions & 5 deletions src/index.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@import "./styles/variables.scss";
@import "./styles/fonts.scss";
@import "./styles/controls.scss";
@import "./styles/editor.scss";
@import "./styles/terminal.scss";
@use "./styles/variables.scss";
@use "./styles/fonts.scss";
@use "./styles/controls.scss";
@use "./styles/editor.scss";
@use "./styles/terminal.scss";
29 changes: 15 additions & 14 deletions tests/integration/components/Terminal.test.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { render, screen, fireEvent, getByTestId } from "@testing-library/react";
import { render, screen, fireEvent, waitFor, getByTestId } from "@testing-library/react";
import userEvent from '@testing-library/user-event';
import { act } from 'react-dom/test-utils';
import '@testing-library/jest-dom'
import * as reactDeviceDetect from 'react-device-detect';
import React, { useContext } from 'react';
import React, { useContext, act } from 'react';
import { ReactTerminal, TerminalContextProvider, TerminalContext } from "../../../src";

import Utils from '../../../src/common/Utils'

describe('ReactTerminal', () => {
test('renders ReactTerminal component', () => {
render(
Expand Down Expand Up @@ -353,17 +353,17 @@ describe('ReactTerminal', () => {
<CustomTerminal />
</TerminalContextProvider>
);

let terminalContainer = screen.getByTestId('terminal');
writeText(terminalContainer, 'wait');
writeText(terminalContainer, 'Enter');

expect(terminalContainer.textContent).toContain('Hold on...');
await waitFor(() => {
const terminalContainer = screen.getByTestId('terminal');
writeText(terminalContainer, 'wait');
writeText(terminalContainer, 'Enter');
expect(terminalContainer.textContent).toContain('Hold on...');
});
});
});

test("mobile editor is not focused when on desktop", async () => {
Object.defineProperty(reactDeviceDetect, "isMobile", {get: () => false})
jest.spyOn<any, any>(Utils, 'isMobile').mockReturnValue(false);

render(
<TerminalContextProvider>
Expand All @@ -374,12 +374,12 @@ test("mobile editor is not focused when on desktop", async () => {
</TerminalContextProvider>
);

await userEvent.click(document.getElementById("terminalEditor"));
await userEvent.click(document.getElementById("terminalEditor")!);
expect(screen.queryByTestId("editor-input")).toBeNull();
})

test("mobile editor is focused when selected", async () => {
Object.defineProperty(reactDeviceDetect, "isMobile", {get: () => true})
jest.spyOn<any, any>(Utils, 'isMobile').mockReturnValue(true);

render(
<TerminalContextProvider>
Expand All @@ -390,10 +390,11 @@ test("mobile editor is focused when selected", async () => {
</TerminalContextProvider>
);

await userEvent.click(document.getElementById("terminalEditor"));
await userEvent.click(document.getElementById("terminalEditor")!);
expect(screen.getByTestId("editor-input")).toHaveFocus();
});


const writeText = function(container: any, value: string, metaKey = false) {
if (["Enter", "Backspace", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Tab"].includes(value)) {
fireEvent.keyDown(container, {
Expand Down
5 changes: 3 additions & 2 deletions tests/unit/components/Editor.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from "react";
import renderer from "react-test-renderer";
import { render } from "@testing-library/react"
import ContextProvider from "../../../src/contexts";
import { TerminalContextProvider } from "../../../src/contexts/TerminalContext";
import Editor from "../../../src/components/Editor";
Expand All @@ -23,6 +23,7 @@ describe("Editor", () => {
errorMessage: ""
};

expect(renderer.create(renderWrapper()).toJSON()).toMatchSnapshot();
const { container } = render(renderWrapper())
expect(container).toMatchSnapshot();
});
});
Loading

0 comments on commit b0851cf

Please sign in to comment.