зеркало из https://github.com/mozilla/fxa.git
feat(settings): set up some foundational configs and components
This commit is contained in:
Родитель
e91822dff5
Коммит
7ee5f5fae7
|
@ -81,6 +81,9 @@
|
|||
"*.css": [
|
||||
"prettier --write"
|
||||
],
|
||||
"*.scss": [
|
||||
"sass-lint --config packages/fxa-settings/.sass-lint.yml 'packages/fxa-settings/**/*.scss' --verbose"
|
||||
],
|
||||
"*.md": [
|
||||
"prettier --write"
|
||||
]
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import React from "react";
|
||||
import { render } from "@testing-library/react";
|
||||
import "@testing-library/jest-dom/extend-expect";
|
||||
import AppErrorBoundary from ".";
|
||||
|
||||
describe("AppErrorBoundary", () => {
|
||||
beforeEach(() => {
|
||||
// HACK: Swallow the exception thrown by BadComponent
|
||||
// it bubbles up unnecesarily to jest and makes noise
|
||||
jest.spyOn(console, "error");
|
||||
(global.console.error as jest.Mock).mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
(global.console.error as jest.Mock).mockRestore();
|
||||
});
|
||||
|
||||
it("renders children that do not cause exceptions", () => {
|
||||
const GoodComponent = () => <p data-testid="good-component">Hi</p>;
|
||||
|
||||
const { queryByTestId } = render(
|
||||
<AppErrorBoundary>
|
||||
<GoodComponent />
|
||||
</AppErrorBoundary>
|
||||
);
|
||||
|
||||
expect(queryByTestId("error-loading-app")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a general error dialog on exception in child component", () => {
|
||||
const BadComponent = () => {
|
||||
throw new Error("bad");
|
||||
};
|
||||
|
||||
const { queryByTestId } = render(
|
||||
<AppErrorBoundary>
|
||||
<BadComponent />
|
||||
</AppErrorBoundary>
|
||||
);
|
||||
|
||||
expect(queryByTestId("error-loading-app")).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,35 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import React from "react";
|
||||
import AppErrorDialog from "../AppErrorDialog";
|
||||
|
||||
class AppErrorBoundary extends React.Component {
|
||||
state: {
|
||||
error: undefined | Error;
|
||||
};
|
||||
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
this.state = { error: undefined };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error) {
|
||||
return { error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: any) {
|
||||
console.error("AppError", error);
|
||||
|
||||
// TODO: Add Sentry logging
|
||||
// sentryMetrics.captureException(error);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { error } = this.state;
|
||||
return error ? <AppErrorDialog {...{ error }} /> : this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default AppErrorBoundary;
|
|
@ -0,0 +1,18 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import React from "react";
|
||||
import { render } from "@testing-library/react";
|
||||
import "@testing-library/jest-dom/extend-expect";
|
||||
import AppErrorDialog from ".";
|
||||
|
||||
describe("AppErrorDialog", () => {
|
||||
it("renders a general error dialog", () => {
|
||||
const { queryByTestId } = render(
|
||||
<AppErrorDialog error={new Error("bad")} />
|
||||
);
|
||||
|
||||
expect(queryByTestId("error-loading-app")).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import React from "react";
|
||||
|
||||
const AppErrorDialog = ({ error: { message } }: { error: Error }) => {
|
||||
return (
|
||||
<div>
|
||||
<h2 data-testid="error-loading-app">General application error</h2>
|
||||
<p>Something went wrong. Please try again later.</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppErrorDialog;
|
|
@ -1,7 +1,12 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const { resolve } = require("path");
|
||||
|
||||
const additionalJSImports = {
|
||||
"@fxa-components": __dirname
|
||||
"@fxa-components": __dirname,
|
||||
"@fxa-shared": resolve(__dirname, "../fxa-shared")
|
||||
};
|
||||
|
||||
const permitAdditionalJSImports = config => {
|
||||
|
|
|
@ -5,7 +5,12 @@
|
|||
// "compilerOptions.paths must not be set", but
|
||||
// this will still work.
|
||||
"paths": {
|
||||
"@fxa-components/*": ["../fxa-components/*"]
|
||||
"@fxa-components/*": [
|
||||
"../fxa-components/*"
|
||||
],
|
||||
"@fxa-shared/*": [
|
||||
"../fxa-shared/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
{
|
||||
"extends": ["plugin:fxa/server", "plugin:jest/recommended"],
|
||||
"plugins": ["fxa"],
|
||||
"ignorePatterns": [
|
||||
"build/"
|
||||
],
|
||||
"parser": "babel-eslint",
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
|
@ -10,5 +13,9 @@
|
|||
"strict": "off",
|
||||
"no-useless-escape": "off",
|
||||
"handle-callback-err": "off"
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
LICENSE
|
||||
.*
|
||||
*.ico
|
||||
*.png
|
||||
*.svg
|
||||
*.docker
|
||||
Dockerfile*
|
||||
public/images/*
|
||||
coverage/*
|
||||
build/*
|
||||
node_modules/*
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5"
|
||||
}
|
|
@ -1,11 +1,15 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const {
|
||||
permitAdditionalJSImports,
|
||||
setupAliasedPaths,
|
||||
componentsJestMapper
|
||||
} = require("../fxa-components/rescripts");
|
||||
componentsJestMapper,
|
||||
} = require('../fxa-components/rescripts');
|
||||
|
||||
module.exports = [
|
||||
permitAdditionalJSImports,
|
||||
setupAliasedPaths,
|
||||
componentsJestMapper
|
||||
componentsJestMapper,
|
||||
];
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
files:
|
||||
include:
|
||||
- 'src/**/*.scss'
|
||||
exclude:
|
||||
- 'node_modules'
|
||||
rules:
|
||||
border-zero: [2, convention: '0']
|
||||
class-name-format: 0
|
||||
clean-import-paths: 0
|
||||
empty-args: [2, include: true]
|
||||
force-attribute-nesting: 0
|
||||
force-element-nesting: 0
|
||||
force-pseudo-nesting: 0
|
||||
indentation: 0
|
||||
leading-zero: 1
|
||||
mixins-before-declarations: 0
|
||||
nesting-depth: [2, max-depth: 4]
|
||||
no-color-literals: 0
|
||||
no-css-comments: 0
|
||||
no-empty-args: 0
|
||||
no-ids: 0
|
||||
no-important: 0
|
||||
no-mergeable-selectors: 0
|
||||
no-misspelled-properties: 0
|
||||
no-qualifying-elements: 0
|
||||
no-vendor-prefixes: 0
|
||||
no-warn: 0
|
||||
placeholder-in-extend: 0
|
||||
property-sort-order: [2, ignore-custom-properties: true]
|
||||
quotes: [2, style: 'single']
|
||||
shorthand-values: 0
|
||||
space-after-comma: [2, include: true]
|
||||
space-around-operator: 0
|
|
@ -11,7 +11,36 @@ You can import React components into this project. This is currently restricted
|
|||
|
||||
```javascript
|
||||
// e.g. assuming the component HelloWorld exists
|
||||
import HelloWorld from "@fxa-components/HelloWorld";
|
||||
import HelloWorld from '@fxa-components/HelloWorld';
|
||||
```
|
||||
|
||||
**Working with SVGs**
|
||||
|
||||
Create React App allows us to use SVGs in a variety of ways, right out of the box.
|
||||
|
||||
```javascript
|
||||
// Inline, full markup:
|
||||
import { ReactComponent as Logo } from './logo.svg';
|
||||
const LogoImage = () => <Logo role="img" aria-label="logo" />;
|
||||
|
||||
// As an image source:
|
||||
import logoUrl from './logo.svg';
|
||||
const LogoImage = () => <img src={logoUrl} alt="Logo" />;
|
||||
|
||||
// As a background-image (inline style)
|
||||
import logoUrl from './logo.svg';
|
||||
const LogoImage = () => (
|
||||
<div
|
||||
style={{ backgroundImage: `url(${logoUrl})` }}
|
||||
role="img"
|
||||
aria-label="logo"
|
||||
></div>
|
||||
);
|
||||
|
||||
// As a background-image (external style)
|
||||
// Just reference it in CSS, the loader will find it
|
||||
// .logo { background-image: url('logo.svg'); }
|
||||
const LogoImage = () => <div class="logo" role="img" aria-label="logo"></div>;
|
||||
```
|
||||
|
||||
## License
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -11,6 +11,7 @@
|
|||
"@types/node": "^12.12.35",
|
||||
"@types/react": "^16.9.34",
|
||||
"@types/react-dom": "^16.9.6",
|
||||
"modern-normalize": "^0.6.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-scripts": "3.4.1",
|
||||
|
@ -22,7 +23,10 @@
|
|||
"stop": "pm2 stop pm2.config.js",
|
||||
"build": "INLINE_RUNTIME_CHUNK=false rescripts build",
|
||||
"test": "rescripts test",
|
||||
"eject": "react-scripts eject"
|
||||
"eject": "react-scripts eject",
|
||||
"lint": "npm-run-all --parallel lint:*",
|
||||
"lint:eslint": "eslint .",
|
||||
"lint:sass": "sass-lint -v"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
|
@ -41,9 +45,12 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@rescripts/cli": "0.0.14",
|
||||
"eslint-plugin-fxa": "^2.0.1",
|
||||
"eslint-plugin-jest": "^23.8.1",
|
||||
"eslint-plugin-react": "^7.18.3",
|
||||
"pm2": "^4.2.3"
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-plugin-fxa": "^2.0.2",
|
||||
"eslint-plugin-jest": "^23.8.2",
|
||||
"eslint-plugin-react": "^7.19.0",
|
||||
"node-sass": "^4.14.0",
|
||||
"pm2": "^4.2.3",
|
||||
"sass-lint": "^1.13.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,16 +5,16 @@
|
|||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: "settings-react",
|
||||
name: 'settings-react',
|
||||
cwd: __dirname,
|
||||
script: "rescripts start",
|
||||
max_restarts: "1",
|
||||
min_uptime: "2m",
|
||||
script: 'rescripts start',
|
||||
max_restarts: '1',
|
||||
min_uptime: '2m',
|
||||
env: {
|
||||
NODE_ENV: "development",
|
||||
BROWSER: "NONE",
|
||||
PORT: "3000"
|
||||
}
|
||||
}
|
||||
]
|
||||
NODE_ENV: 'development',
|
||||
BROWSER: 'NONE',
|
||||
PORT: '3000',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
Двоичные данные
packages/fxa-settings/public/favicon.ico
Двоичные данные
packages/fxa-settings/public/favicon.ico
Двоичный файл не отображается.
До Ширина: | Высота: | Размер: 3.1 KiB После Ширина: | Высота: | Размер: 15 KiB |
|
@ -1,19 +1,22 @@
|
|||
<!DOCTYPE html>
|
||||
<html dir="ltr" lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>FxA Settings</title>
|
||||
<meta name="description" content="" />
|
||||
<meta name="referrer" content="origin" />
|
||||
<meta name="robots" content="noindex,nofollow" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=2,user-scalable=yes"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>FxA Settings requires JavaScript.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>Firefox Accounts</title>
|
||||
<meta name="description" content="" />
|
||||
<meta name="referrer" content="origin" />
|
||||
<meta name="robots" content="noindex,nofollow" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=2,user-scalable=yes" />
|
||||
|
||||
<!--iOS App Banner-->
|
||||
<meta name="apple-itunes-app" content="app-id=989804926, affiliate-data=ct=smartbanner-fxa" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>Firefox Accounts requires JavaScript.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import React from "react";
|
||||
import { render } from "@testing-library/react";
|
||||
import App from "./App";
|
||||
|
||||
test("renders learn react link", () => {
|
||||
const { getByText } = render(<App />);
|
||||
const linkElement = getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
|
@ -1,24 +0,0 @@
|
|||
import React from "react";
|
||||
import "./App.css";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<p>
|
||||
Edit <code>src/App.tsx</code> and save to reload.
|
||||
</p>
|
||||
<a
|
||||
className="App-link"
|
||||
href="https://reactjs.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn React
|
||||
</a>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
|
@ -0,0 +1,5 @@
|
|||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
// Add component-specific styles here
|
|
@ -0,0 +1,12 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import App from '.';
|
||||
|
||||
it('renders', () => {
|
||||
render(<App />);
|
||||
});
|
|
@ -0,0 +1,20 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import React from 'react';
|
||||
import AppLayout from '../AppLayout';
|
||||
import AppErrorBoundary from '@fxa-components/AppErrorBoundary';
|
||||
import './index.scss';
|
||||
|
||||
export const App = () => {
|
||||
return (
|
||||
<AppErrorBoundary>
|
||||
<AppLayout>
|
||||
<p>Hello, world!</p>
|
||||
</AppLayout>
|
||||
</AppErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
|
@ -0,0 +1,5 @@
|
|||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
// Add component-specific styles here
|
|
@ -0,0 +1,18 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import AppLayout from '.';
|
||||
|
||||
it('renders the children', () => {
|
||||
const rendered = render(
|
||||
<AppLayout>
|
||||
<p data-testid="test-child">Hello, world!</p>
|
||||
</AppLayout>
|
||||
);
|
||||
|
||||
expect(rendered.getByTestId('test-child')).toBeInTheDocument();
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import React from 'react';
|
||||
import './index.scss';
|
||||
|
||||
type AppLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const AppLayout = ({ children }: AppLayoutProps) => {
|
||||
return <div>{children}</div>;
|
||||
};
|
||||
|
||||
export default AppLayout;
|
|
@ -1,13 +0,0 @@
|
|||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
||||
monospace;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
@import 'modern-normalize';
|
||||
|
||||
// Add app-wide styles here
|
|
@ -1,11 +1,29 @@
|
|||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import "./index.css";
|
||||
import App from "./App";
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById("root")
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import './index.scss';
|
||||
import App from './components/App';
|
||||
|
||||
export async function init() {
|
||||
// TODO: Configure Sentry logging
|
||||
// if (config.sentry.dsn) {
|
||||
// sentryMetrics.configure(config.sentry.dsn, config.version);
|
||||
// }
|
||||
|
||||
render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
}
|
||||
|
||||
init().then(
|
||||
() => {},
|
||||
error => {
|
||||
console.log('Error initializing FXA Settings', error);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -1 +1,5 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/// <reference types="react-scripts" />
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import "@testing-library/jest-dom/extend-expect";
|
Загрузка…
Ссылка в новой задаче