1

自从将我的 Gatsby 项目移植到 Typescript 以来,我一直在为一系列零星的测试失败而苦苦挣扎。以下错误在我的 Jenkins CI 服务器上经常发生,但几乎从不在本地发生。我有几个使用Router组件的类似组件,@reach/router并且这些文件中的任何一个都可能发生故障。

失败信息

FAIL src/pages/assets.test.tsx
  ● Test suite failed to run

    TypeScript diagnostics (customize using `[jest-config].globals.ts-jest.diagnostics` option):
    src/pages/assets.tsx:8:3 - error TS2605: JSX element type 'Router' is not a constructor function for JSX elements.
      Type 'Router' is missing the following properties from type 'ElementClass': render, context, setState, forceUpdate, and 3 more.

    8   <Router basepath='/assets'>
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    src/pages/assets.tsx:8:3 - error TS2607: JSX element class does not support attributes because it does not have a 'props' property.

    8   <Router basepath='/assets'>
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~

源文件

这是可能导致 Typescript 编译器错误的许多类似源文件之一。所有这些文件在浏览器中加载并按预期运行。

import React from 'react';
import { Router, RouteComponentProps } from '@reach/router';
import List from '../assets/components/pages/List';
import Show from '../assets/components/pages/Show';
import RouterPage from 'shared/components/navigation/RouterPage';

const AssetsRouter = () => (
  <Router basepath='/assets'>
    <RouterPage path='/' pageComponent={List} />
    <RouterPage
      path='/:assetId'
      pageComponent={(props: RouteComponentProps<{ assetId: number }>) => (
        <Show assetId={props.assetId as number} />
      )}
    />
  </Router>
);

export default AssetsRouter;

测试文件

jest.mock('../assets/components/pages/List', () => () => 'Assets List');
jest.mock(
  '../assets/components/pages/Show',
  () => ({ assetId }: { assetId: number }) => `View asset ${assetId}`
);

import React from 'react';
import { act } from '@testing-library/react';
import AssetsRouter from './assets';
import renderWithRouter from 'shared/__test__/renderWithRouter';

describe('/assets', () => {
  it('Loads the assets list page', async () => {
    const {
      findByText,
      history: { navigate },
    } = renderWithRouter(<AssetsRouter />);
    await act(async () => {
      await navigate('/assets');
    });
    return expect(findByText('Assets List')).resolves.toBeInTheDocument();
  });
});

describe('/assets/:assetId', () => {
  it('Loads the asset show page', async () => {
    const {
      findByText,
      history: { navigate },
    } = renderWithRouter(<AssetsRouter />);
    await act(async () => {
      await navigate('/assets/123');
    });
    return expect(findByText('View asset 123')).resolves.toBeInTheDocument();
  });
});

更多上下文

源文件中使用的组件是从GitHub 问题讨论RouterPage中复制的@reach/router

import { RouteComponentProps } from '@reach/router';

interface Props {
  pageComponent: (routerProps: RouteComponentProps) => JSX.Element;
}

// Borrowed from https://github.com/reach/router/issues/141#issuecomment-451646939
const RouterPage = ({
  pageComponent,
  ...routerProps
}: Props & RouteComponentProps) => {
  return pageComponent(routerProps);
};

export default RouterPage;

辅助函数是从测试库文章renderWithRouter中复制的

import React from 'react';
import { render } from '@testing-library/react';
import {
  createHistory,
  createMemorySource,
  LocationProvider,
} from '@reach/router';

const renderWithRouter = (
  component: JSX.Element,
  { route = '/', history = createHistory(createMemorySource(route)) } = {}
) => {
  return {
    ...render(
      <LocationProvider history={history}>{component}</LocationProvider>
    ),
    history,
  };
};

export default renderWithRouter;

这是我的编译器设置tsconfig.json

{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "baseUrl": "src",
    "esModuleInterop": true,
    "allowJs": false,
    "declaration": false,
    "forceConsistentCasingInFileNames": true,
    "importHelpers": true,
    "jsx": "react",
    "lib": ["dom", "es2015", "es2017"],
    "module": "commonjs",
    "noEmitHelpers": false,
    "noEmitOnError": true,
    "noImplicitAny": true,
    "noImplicitThis": true,
    "noResolve": false,
    "noUnusedLocals": true,
    "noUnusedParameters": false,
    "paths": {
      "shared/*": ["shared/*"]
    },
    "preserveConstEnums": true,
    "removeComments": true,
    "rootDir": ".",
    "sourceMap": true,
    "strictBindCallApply": true,
    "strictNullChecks": true,
    "target": "esnext"
  },
  "include": ["./src/**/*"]
}

我的依赖来自package.json

{
  "dependencies": {
    "@fortawesome/fontawesome-svg-core": "^1.2.27",
    "@fortawesome/free-solid-svg-icons": "^5.12.1",
    "@fortawesome/react-fontawesome": "^0.1.8",
    "@material-ui/core": "^4.9.5",
    "@material-ui/icons": "^4.9.1",
    "@types/geojson": "^7946.0.7",
    "@types/mapbox-gl": "^1.8.0",
    "@types/qs": "^6.9.1",
    "@types/reach__router": "^1.2.6",
    "@types/react-helmet": "^5.0.15",
    "cloudinary-react": "^1.4.0",
    "es6-promise": "^4.2.8",
    "gatsby": "^2.18.4",
    "gatsby-plugin-material-ui": "^2.1.6",
    "gatsby-plugin-react-helmet": "^3.1.16",
    "gatsby-plugin-sass": "^2.1.24",
    "gatsby-plugin-typescript": "^2.1.27",
    "isomorphic-fetch": "^2.2.1",
    "mapbox-gl": "^1.6.0",
    "mapbox-gl-supported": "^1.2.0",
    "node-sass": "^4.13.0",
    "react": "^16.12.0",
    "react-dom": "^16.12.0",
    "react-helmet": "^5.2.1"
  },
  "devDependencies": {
    "@testing-library/jest-dom": "^5.1.1",
    "@testing-library/react": "^9.4.0",
    "@types/jest": "^25.1.2",
    "@types/jest-when": "^2.7.0",
    "babel-jest": "^25.1.0",
    "babel-preset-gatsby": "^0.2.29",
    "eslint-plugin-jest-dom": "^2.0.0",
    "identity-obj-proxy": "^3.0.0",
    "jest": "^25.1.0",
    "jest-when": "^2.7.0",
    "prettier": "^1.19.1",
    "ts-jest": "^25.2.0",
    "typescript": "3.7.5"
  }
}

最后,这里是gatsby-node.js用于创建Router组件使用的页面的配置。

const routeConfigs = [
  { matcher: '^/experiments/', matchPath: '/experiments/*' },
  { matcher: '^/leases/', matchPath: '/leases/*' },
  { matcher: '^/properties/', matchPath: '/properties/*' },
  { matcher: '^/renters/', matchPath: '/renters/*' },
  { matcher: '^/specials/', matchPath: '/specials/*' },
];

exports.onCreatePage = async ({ page, actions }) => {
  const { createPage } = actions

  // Don't set matchPath again if it's already been set.
  if (page.matchPath || page.path.match(/dev-404-page/)) {
    return
  }

  // Ensure that the path ends in a trailing slash, since it can be removed.
  const path = page.path.match(/\/$/) ? page.path : `${page.path}/`

  routeConfigs.forEach(routeConfig => {
    if (path.match(new RegExp(routeConfig.matcher))) {
      page.matchPath = routeConfig.matchPath;
      createPage(page)
    }
  })
}

4

0 回答 0