Compare commits

..

4 Commits

Author SHA1 Message Date
Braden MacDonald
2fea9a6d14 fix: add an explicit dependency on ts-jest to get tests passing 2025-07-21 13:55:10 -07:00
Braden MacDonald
1eebfe501e chore(deps): regenerate package-lock.json 2025-07-21 13:39:26 -07:00
Braden MacDonald
4dfb1b3053 fix: remove unused jest-chain, babel-polyfill, axios-mock-adapter (#603) 2025-07-21 10:38:01 -07:00
Braden MacDonald
171a770235 feat: enable the use of TypeScript in this repo (#604)
* feat: enable Typescript in this repo

* refactor: rename studio-header files to .ts[x]

* chore: fix minor type warnings

* chore: add types for frontend-platform

* chore: fix type issues

* chore: update name of suppressed lint check
2025-07-21 10:24:52 -07:00
28 changed files with 7103 additions and 3351 deletions

View File

@@ -24,6 +24,8 @@ jobs:
run: make validate-no-uncommitted-package-lock-changes run: make validate-no-uncommitted-package-lock-changes
- name: Lint - name: Lint
run: npm run lint run: npm run lint
- name: Type check
run: npm run types
- name: Test - name: Test
run: npm run test run: npm run test
- name: Build - name: Build

View File

@@ -25,6 +25,8 @@ jobs:
run: make validate-no-uncommitted-package-lock-changes run: make validate-no-uncommitted-package-lock-changes
- name: Lint - name: Lint
run: npm run lint run: npm run lint
- name: Type check
run: npm run types
- name: Test - name: Test
run: npm run test run: npm run test
- name: i18n_extract - name: i18n_extract

View File

@@ -1,5 +1,3 @@
import 'babel-polyfill';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { initialize, getConfig, subscribe, APP_READY } from '@edx/frontend-platform'; import { initialize, getConfig, subscribe, APP_READY } from '@edx/frontend-platform';

10355
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,8 @@
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .", "lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
"snapshot": "fedx-scripts jest --updateSnapshot", "snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress", "start": "fedx-scripts webpack-dev-server --progress",
"test": "fedx-scripts jest --coverage" "test": "fedx-scripts jest --coverage",
"types": "tsc --noEmit"
}, },
"files": [ "files": [
"/dist" "/dist"
@@ -39,7 +40,7 @@
"@testing-library/jest-dom": "5.17.0", "@testing-library/jest-dom": "5.17.0",
"@testing-library/react": "^16.2.0", "@testing-library/react": "^16.2.0",
"jest": "30.0.4", "jest": "30.0.4",
"jest-chain": "1.1.6", "jest-environment-jsdom": "^30.0.0",
"prop-types": "15.8.1", "prop-types": "15.8.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
@@ -47,7 +48,8 @@
"react-router-dom": "6.30.1", "react-router-dom": "6.30.1",
"react-test-renderer": "^18.3.1", "react-test-renderer": "^18.3.1",
"redux": "4.2.1", "redux": "4.2.1",
"redux-saga": "1.3.0" "redux-saga": "1.3.0",
"ts-jest": "^29.4.0"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "6.7.2", "@fortawesome/fontawesome-svg-core": "6.7.2",
@@ -56,10 +58,7 @@
"@fortawesome/free-solid-svg-icons": "6.7.2", "@fortawesome/free-solid-svg-icons": "6.7.2",
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@openedx/frontend-plugin-framework": "^1.7.0", "@openedx/frontend-plugin-framework": "^1.7.0",
"axios-mock-adapter": "1.22.0",
"babel-polyfill": "6.26.0",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"jest-environment-jsdom": "^30.0.0",
"react-responsive": "8.2.0", "react-responsive": "8.2.0",
"react-transition-group": "4.4.5" "react-transition-group": "4.4.5"
}, },

View File

@@ -33,6 +33,7 @@ describe('<Header />', () => {
}; };
const component = <HeaderComponent width={{ width: 1280 }} contextValue={contextValue} />; const component = <HeaderComponent width={{ width: 1280 }} contextValue={contextValue} />;
// FIXME: react-test-renderer is deprecated. Convert to @testing-library/react.
const wrapper = TestRenderer.create(component); const wrapper = TestRenderer.create(component);
expect(wrapper.toJSON()).toMatchSnapshot(); expect(wrapper.toJSON()).toMatchSnapshot();
@@ -56,6 +57,7 @@ describe('<Header />', () => {
}; };
const component = <HeaderComponent width={{ width: 1280 }} contextValue={contextValue} />; const component = <HeaderComponent width={{ width: 1280 }} contextValue={contextValue} />;
// FIXME: react-test-renderer is deprecated. Convert to @testing-library/react.
const wrapper = TestRenderer.create(component); const wrapper = TestRenderer.create(component);
expect(wrapper.toJSON()).toMatchSnapshot(); expect(wrapper.toJSON()).toMatchSnapshot();
@@ -74,6 +76,7 @@ describe('<Header />', () => {
}; };
const component = <HeaderComponent width={{ width: 500 }} contextValue={contextValue} />; const component = <HeaderComponent width={{ width: 500 }} contextValue={contextValue} />;
// FIXME: react-test-renderer is deprecated. Convert to @testing-library/react.
const wrapper = TestRenderer.create(component); const wrapper = TestRenderer.create(component);
expect(wrapper.toJSON()).toMatchSnapshot(); expect(wrapper.toJSON()).toMatchSnapshot();

View File

@@ -22,7 +22,7 @@ import messages from '../Header.messages';
import { CaretIcon } from '../Icons'; import { CaretIcon } from '../Icons';
class DesktopHeader extends React.Component { class DesktopHeader extends React.Component {
constructor(props) { // eslint-disable-line no-useless-constructor constructor(props) { // eslint-disable-line @typescript-eslint/no-useless-constructor
super(props); super(props);
} }

41
src/frontend-platform.d.ts vendored Normal file
View File

@@ -0,0 +1,41 @@
// frontend-platform currently doesn't provide types... do it ourselves for i18n module at least.
// We can remove this in the future when we migrate to frontend-shell, or when frontend-platform gets types
// (whichever comes first).
declare module '@edx/frontend-platform/i18n' {
// eslint-disable-next-line import/no-extraneous-dependencies
import { injectIntl as _injectIntl } from 'react-intl';
/** @deprecated Use useIntl() hook instead. */
export const injectIntl: typeof _injectIntl;
/** @deprecated Use useIntl() hook instead. */
export const intlShape: any;
// eslint-disable-next-line import/no-extraneous-dependencies
export {
createIntl,
FormattedDate,
FormattedTime,
FormattedRelativeTime,
FormattedNumber,
FormattedPlural,
FormattedMessage,
defineMessages,
IntlProvider,
useIntl,
} from 'react-intl';
// Other exports from the i18n module:
export const configure: any;
export const getPrimaryLanguageSubtag: (code: string) => string;
export const getLocale: (locale?: string) => string;
export const getMessages: any;
export const isRtl: (locale?: string) => boolean;
export const handleRtl: any;
export const mergeMessages: any;
export const LOCALE_CHANGED: any;
export const LOCALE_TOPIC: any;
export const getCountryList: any;
export const getCountryMessages: any;
export const getLanguageList: any;
export const getLanguageMessages: any;
}

View File

@@ -21,7 +21,7 @@ import messages from '../Header.messages';
import { MenuIcon } from '../Icons'; import { MenuIcon } from '../Icons';
class MobileHeader extends React.Component { class MobileHeader extends React.Component {
constructor(props) { // eslint-disable-line no-useless-constructor constructor(props) { // eslint-disable-line @typescript-eslint/no-useless-constructor
super(props); super(props);
} }

View File

@@ -4,8 +4,6 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import '@testing-library/jest-dom/extend-expect'; import '@testing-library/jest-dom/extend-expect';
import 'babel-polyfill';
import 'jest-chain';
import { getConfig, mergeConfig } from '@edx/frontend-platform'; import { getConfig, mergeConfig } from '@edx/frontend-platform';
import { configure as configureLogging } from '@edx/frontend-platform/logging'; import { configure as configureLogging } from '@edx/frontend-platform/logging';
import { configure as configureI18n } from '@edx/frontend-platform/i18n'; import { configure as configureI18n } from '@edx/frontend-platform/i18n';

View File

@@ -34,7 +34,7 @@ describe('BrandNav Component', () => {
it('displays a link that navigates to studioBaseUrl', () => { it('displays a link that navigates to studioBaseUrl', () => {
render(<RootWrapper />); render(<RootWrapper />);
const link = screen.getByRole('link'); const link = screen.getByRole('link') as HTMLAnchorElement;
expect(link.href).toBe(studioBaseUrl); expect(link.href).toBe(studioBaseUrl);
}); });
}); });

View File

@@ -16,7 +16,7 @@ const mockProps = {
const RootWrapper = (props) => ( const RootWrapper = (props) => (
<MemoryRouter> <MemoryRouter>
<IntlProvider locale="en" messages={messages}> <IntlProvider locale="en" messages={{}}>
<CourseLockUp {...props} /> <CourseLockUp {...props} />
</IntlProvider> </IntlProvider>
</MemoryRouter> </MemoryRouter>
@@ -52,7 +52,8 @@ describe('CourseLockUp Component', () => {
it('navigates to an absolute URL when clicked', () => { it('navigates to an absolute URL when clicked', () => {
render(<RootWrapper {...mockProps} />); render(<RootWrapper {...mockProps} />);
const link = screen.getByTestId('course-lock-up-block'); // FIXME: don't use testId - https://testing-library.com/docs/queries/about#priority
const link = screen.getByTestId('course-lock-up-block') as HTMLAnchorElement;
expect(link.href).toBe(mockProps.outlineLink); expect(link.href).toBe(mockProps.outlineLink);
}); });
}); });

View File

@@ -35,7 +35,7 @@ const defaultProps = {
const RootWrapper = (props) => ( const RootWrapper = (props) => (
<MemoryRouter> <MemoryRouter>
<IntlProvider locale="en" messages={messages}> <IntlProvider locale="en" messages={{}}>
<HeaderBody {...props} /> <HeaderBody {...props} />
</IntlProvider> </IntlProvider>
</MemoryRouter> </MemoryRouter>

View File

@@ -135,6 +135,7 @@ const HeaderBody = ({
logoutUrl, logoutUrl,
authenticatedUserAvatar, authenticatedUserAvatar,
isAdmin, isAdmin,
isMobile,
}} }}
/> />
</Nav> </Nav>

View File

@@ -13,6 +13,7 @@ const MobileHeader = ({
return ( return (
<> <>
{/* @ts-expect-error The type of 'props' is any until we convert from propTypes to TypeScript interface/types */}
<HeaderBody <HeaderBody
{...props} {...props}
isMobile isMobile

View File

@@ -26,7 +26,7 @@ let screenWidth = 1280;
const RootWrapper = ({ const RootWrapper = ({
...props ...props
}) => { }: React.ComponentProps<typeof StudioHeader>) => {
const appContextValue = useMemo(() => ({ const appContextValue = useMemo(() => ({
authenticatedUser: currentUser, authenticatedUser: currentUser,
config: { config: {
@@ -55,7 +55,7 @@ const RootWrapper = ({
); );
}; };
const props = { const props: React.ComponentProps<typeof StudioHeader> = {
number: '123', number: '123',
org: 'Ed', org: 'Ed',
title: 'test', title: 'test',
@@ -74,6 +74,10 @@ const props = {
outlineLink: 'tEsTLInK', outlineLink: 'tEsTLInK',
searchButtonAction: null, searchButtonAction: null,
isNewHomePage: true, isNewHomePage: true,
// These default values shouldn't be needed but typescript is confused by propTypes; can remove after converting
// from propTypes to TypeScript:
containerProps: {},
isHiddenMainMenu: false,
}; };
describe('Header', () => { describe('Header', () => {

View File

@@ -19,6 +19,7 @@ const StudioHeader = ({
number, org, title, containerProps, isHiddenMainMenu, mainMenuDropdowns, number, org, title, containerProps, isHiddenMainMenu, mainMenuDropdowns,
outlineLink, searchButtonAction, isNewHomePage, outlineLink, searchButtonAction, isNewHomePage,
}) => { }) => {
// @ts-expect-error - frontend-platform doesn't yet have type information :/
const { authenticatedUser, config } = useContext(AppContext); const { authenticatedUser, config } = useContext(AppContext);
const props = { const props = {
logo: config.LOGO_URL, logo: config.LOGO_URL,

12
tsconfig.json Normal file
View File

@@ -0,0 +1,12 @@
{
"extends": "@edx/typescript-config",
"compilerOptions": {
"noEmit": true,
"baseUrl": "./src",
"paths": {
"*": ["*"]
}
},
"include": ["*.js", ".eslintrc.js", "src/**/*", "plugins/**/*"],
"exclude": ["dist", "node_modules"]
}