Merge pull request #417 from SrdjanCosicPrica/master

Add husky and lint-staged to automatically prettify staged code changes
This commit is contained in:
Fredrik Adelöw
2020-03-31 16:51:53 +02:00
committed by GitHub
13 changed files with 302 additions and 40 deletions
+12
View File
@@ -24,7 +24,9 @@
"devDependencies": {
"cross-env": "^7.0.0",
"eslint-plugin-notice": "^0.8.9",
"husky": "^4.2.3",
"lerna": "^3.20.2",
"lint-staged": "^10.1.0",
"prettier": "^1.19.1",
"typescript": "^3.7.5",
"zombie": "^6.1.4"
@@ -32,5 +34,15 @@
"dependencies": {
"@types/classnames": "^2.2.9",
"moment": "^2.24.0"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"prettier --config ./prettier.config.js --list-different --write"
]
}
}
-2
View File
@@ -13,5 +13,3 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+2 -2
View File
@@ -11,8 +11,8 @@ module.exports = {
{
loader: require.resolve('ts-loader'),
options: {
transpileOnly: true
}
transpileOnly: true,
},
},
],
});
@@ -59,13 +59,24 @@ describe('ApiTestRegistry', () => {
it('should register factories with dependencies', () => {
// 100% coverage + happy typescript = hasOwnProperty + this atrocity
const cDeps = Object.create({ c: cRef }, { a: { enumerable: true, value: aRef } });
const cDeps = Object.create(
{ c: cRef },
{ a: { enumerable: true, value: aRef } },
);
cDeps.b = bRef;
const registry = new ApiTestRegistry();
registry.register({ implements: aRef, deps: {}, factory: () => 3 });
registry.register({ implements: bRef, deps: { dep: aRef }, factory: ({ dep }) => `hello ${dep}` });
registry.register({ implements: cRef, deps: cDeps, factory: ({ a, b }) => b.repeat(a) });
registry.register({
implements: bRef,
deps: { dep: aRef },
factory: ({ dep }) => `hello ${dep}`,
});
registry.register({
implements: cRef,
deps: cDeps,
factory: ({ a, b }) => b.repeat(a),
});
expect(registry.get(aRef)).toBe(3);
expect(registry.get(bRef)).toBe('hello 3');
expect(registry.get(cRef)).toBe('hello 3hello 3hello 3');
@@ -73,17 +84,39 @@ describe('ApiTestRegistry', () => {
it('should not allow cyclic dependencies', () => {
const registry = new ApiTestRegistry();
registry.register({ implements: aRef, deps: { b: bRef }, factory: () => 1 });
registry.register({ implements: bRef, deps: { c: cRef }, factory: () => 'b' });
registry.register({ implements: cRef, deps: { a: aRef }, factory: () => 'c' });
expect(() => registry.get(aRef)).toThrow('Circular dependency of api factory for apiRef{a}');
expect(() => registry.get(bRef)).toThrow('Circular dependency of api factory for apiRef{b}');
expect(() => registry.get(cRef)).toThrow('Circular dependency of api factory for apiRef{c}');
registry.register({
implements: aRef,
deps: { b: bRef },
factory: () => 1,
});
registry.register({
implements: bRef,
deps: { c: cRef },
factory: () => 'b',
});
registry.register({
implements: cRef,
deps: { a: aRef },
factory: () => 'c',
});
expect(() => registry.get(aRef)).toThrow(
'Circular dependency of api factory for apiRef{a}',
);
expect(() => registry.get(bRef)).toThrow(
'Circular dependency of api factory for apiRef{b}',
);
expect(() => registry.get(cRef)).toThrow(
'Circular dependency of api factory for apiRef{c}',
);
});
it('should throw error if dependency is not available', () => {
const registry = new ApiTestRegistry();
registry.register({ implements: aRef, deps: { b: bRef }, factory: () => 1 });
registry.register({
implements: aRef,
deps: { b: bRef },
factory: () => 1,
});
expect(() => registry.get(aRef)).toThrow(
'No API factory available for dependency apiRef{b} of dependent apiRef{a}',
);
+27 -7
View File
@@ -19,8 +19,14 @@ import { TypesToApiRefs, AnyApiRef, ApiHolder, ApiFactory } from './types';
export default class ApiTestRegistry implements ApiHolder {
private readonly apis = new Map<AnyApiRef, unknown>();
private factories = new Map<AnyApiRef, ApiFactory<unknown, unknown, unknown>>();
private savedFactories = new Map<AnyApiRef, ApiFactory<unknown, unknown, unknown>>();
private factories = new Map<
AnyApiRef,
ApiFactory<unknown, unknown, unknown>
>();
private savedFactories = new Map<
AnyApiRef,
ApiFactory<unknown, unknown, unknown>
>();
get<T>(ref: ApiRef<T>): T | undefined {
return this.load(ref);
@@ -28,7 +34,10 @@ export default class ApiTestRegistry implements ApiHolder {
register<T>(ref: ApiRef<T>, factoryFunc: () => T): ApiTestRegistry;
register<A, I, D>(factory: ApiFactory<A, I, D>): ApiTestRegistry;
register<A, I, D, T>(factory: ApiRef<T> | ApiFactory<A, I, D>, factoryFunc?: () => T): ApiTestRegistry {
register<A, I, D, T>(
factory: ApiRef<T> | ApiFactory<A, I, D>,
factoryFunc?: () => T,
): ApiTestRegistry {
if (factory instanceof ApiRef) {
this.factories.set(factory, {
implements: factory,
@@ -63,16 +72,25 @@ export default class ApiTestRegistry implements ApiHolder {
}
if (loading.includes(factory.implements)) {
throw new Error(`Circular dependency of api factory for ${factory.implements}`);
throw new Error(
`Circular dependency of api factory for ${factory.implements}`,
);
}
const deps = this.loadDeps(ref, factory.deps, [...loading, factory.implements]);
const deps = this.loadDeps(ref, factory.deps, [
...loading,
factory.implements,
]);
const api = factory.factory(deps);
this.apis.set(ref, api);
return api as T;
}
private loadDeps<T>(dependent: ApiRef<unknown>, apis: TypesToApiRefs<T>, loading: AnyApiRef[]): T {
private loadDeps<T>(
dependent: ApiRef<unknown>,
apis: TypesToApiRefs<T>,
loading: AnyApiRef[],
): T {
const impls = {} as T;
for (const key in apis) {
@@ -81,7 +99,9 @@ export default class ApiTestRegistry implements ApiHolder {
const api = this.load(ref, loading);
if (!api) {
throw new Error(`No API factory available for dependency ${ref} of dependent ${dependent}`);
throw new Error(
`No API factory available for dependency ${ref} of dependent ${dependent}`,
);
}
impls[key] = api;
}
+9 -1
View File
@@ -14,4 +14,12 @@
* limitations under the License.
*/
export { StatusError, StatusFailed, StatusNA, StatusOK, StatusPending, StatusRunning, StatusWarning } from './Status';
export {
StatusError,
StatusFailed,
StatusNA,
StatusOK,
StatusPending,
StatusRunning,
StatusWarning,
} from './Status';
@@ -15,10 +15,24 @@
*/
import React, { Fragment } from 'react';
import { IconButton, List, ListItem, ListItemIcon, ListItemText, Popover } from '@material-ui/core';
import {
IconButton,
List,
ListItem,
ListItemIcon,
ListItemText,
Popover,
} from '@material-ui/core';
import { default as KebabMenuIcon } from './MenuVertical';
const ActionItem = ({ label, secondaryLabel, icon, disabled = false, onClick, WrapperComponent = React.Fragment }) => {
const ActionItem = ({
label,
secondaryLabel,
icon,
disabled = false,
onClick,
WrapperComponent = React.Fragment,
}) => {
return (
<WrapperComponent>
<ListItem
@@ -48,7 +62,13 @@ const HeaderActionMenu = ({ actionItems }) => {
onClick={() => setOpen(true)}
data-testid="header-action-menu"
ref={anchorElRef}
style={{ color: 'white', height: 56, width: 56, marginRight: -4, padding: 0 }}
style={{
color: 'white',
height: 56,
width: 56,
marginRight: -4,
padding: 0,
}}
>
<KebabMenuIcon titleAccess="menu" style={{ fontSize: 40 }} />
</IconButton>
@@ -27,13 +27,20 @@ describe('<ComponentContextMenu />', () => {
it('can open the menu and click menu items', () => {
const onClickFunction = jest.fn();
const rendered = render(
wrapInThemedTestApp(<HeaderActionMenu actionItems={[{ label: 'Some label', onClick: onClickFunction }]} />),
wrapInThemedTestApp(
<HeaderActionMenu
actionItems={[{ label: 'Some label', onClick: onClickFunction }]}
/>,
),
);
expect(rendered.queryByText('Some label')).not.toBeInTheDocument();
expect(onClickFunction).not.toHaveBeenCalled();
fireEvent.click(rendered.getByTestId('header-action-menu'));
expect(onClickFunction).not.toHaveBeenCalled();
expect(rendered.getByTestId('header-action-item')).not.toHaveAttribute('aria-disabled', 'true');
expect(rendered.getByTestId('header-action-item')).not.toHaveAttribute(
'aria-disabled',
'true',
);
fireEvent.click(rendered.queryByText('Some label'));
expect(onClickFunction).toHaveBeenCalled();
// We do not expect the dropdown to disappear after click
@@ -42,11 +49,18 @@ describe('<ComponentContextMenu />', () => {
it('Disabled', async () => {
const rendered = render(
wrapInThemedTestApp(<HeaderActionMenu actionItems={[{ label: 'Some label', disabled: true }]} />),
wrapInThemedTestApp(
<HeaderActionMenu
actionItems={[{ label: 'Some label', disabled: true }]}
/>,
),
);
fireEvent.click(rendered.getByTestId('header-action-menu'));
expect(rendered.getByTestId('header-action-item')).toHaveAttribute('aria-disabled', 'true');
expect(rendered.getByTestId('header-action-item')).toHaveAttribute(
'aria-disabled',
'true',
);
});
it('Test wrapper, and secondary label', () => {
@@ -58,7 +72,9 @@ describe('<ComponentContextMenu />', () => {
{
label: 'Some label',
secondaryLabel: 'Secondary label',
WrapperComponent: ({ children }) => <button onClick={onClickFunction}>{children}</button>,
WrapperComponent: ({ children }) => (
<button onClick={onClickFunction}>{children}</button>
),
},
]}
/>,
@@ -75,7 +91,11 @@ describe('<ComponentContextMenu />', () => {
});
it('should close when hitting escape', async () => {
const rendered = render(wrapInThemedTestApp(<HeaderActionMenu actionItems={[{ label: 'Some label' }]} />));
const rendered = render(
wrapInThemedTestApp(
<HeaderActionMenu actionItems={[{ label: 'Some label' }]} />,
),
);
expect(rendered.container.getAttribute('aria-hidden')).toBeNull();
fireEvent.click(rendered.getByTestId('header-action-menu'));
@@ -31,12 +31,18 @@ describe('<HeaderLabel />', () => {
});
it('should have value', () => {
const rendered = render(wrapInThemedTestApp(<HeaderLabel label="Label" value="Value" />));
const rendered = render(
wrapInThemedTestApp(<HeaderLabel label="Label" value="Value" />),
);
expect(rendered.getByText('Value')).toBeInTheDocument();
});
it('should have a link', () => {
const rendered = render(wrapInThemedTestApp(<HeaderLabel label="Label" value="Value" url="/test" />));
const rendered = render(
wrapInThemedTestApp(
<HeaderLabel label="Label" value="Value" url="/test" />,
),
);
const anchor = rendered.container.querySelector('a');
expect(rendered.getByText('Value')).toBeInTheDocument();
expect(anchor.href).toBe('http://localhost/test');
@@ -0,0 +1,81 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Tooltip, Link, withStyles } from '@material-ui/core';
import { StatusError } from '../../components/Status';
import HeaderLabel from './HeaderLabel';
const style = theme => ({
notVerified: {
color: theme.palette.status.error,
borderRadius: 4,
padding: '3px 6px',
fontSize: '8pt',
opacity: 0.8,
fontWeight: 'bold',
position: 'relative',
top: -4,
backgroundColor: 'pink',
float: 'right',
marginLeft: 14,
},
label: { float: 'left' },
});
class OwnerHeaderLabel extends Component {
static propTypes = {
owner: PropTypes.object.isRequired,
};
render() {
const { owner, classes } = this.props;
const isBadSquad = owner.type !== 'squad';
const notVerified = isBadSquad && (
<Link href="https://spotify.stackenterprise.co/a/4412/23">
<span className={classes.notVerified}>
<StatusError style={{ position: 'relative', top: 2 }} /> Squad not
verified!
</span>
</Link>
);
const label = (
<Tooltip
title="This component is not owned by an existing squad. Click the badge to learn how to fix this."
placement="bottom"
>
<span>
<span className={classes.label}>{owner.name}</span>
</span>
</Tooltip>
);
return (
<>
<HeaderLabel
label="Owner"
value={isBadSquad ? label : owner.name}
url={owner.name ? `/org/${owner.name}` : ''}
/>
{notVerified}
</>
);
}
}
export default withStyles(style)(OwnerHeaderLabel);
+17 -5
View File
@@ -33,8 +33,11 @@ describe('testUtils.Keyboard', () => {
const rendered = render(
<form onSubmit={handleSubmit}>
<input onChange={({ target: { value } }) => typed1.push(value)} />
{/* eslint-disable-next-line jsx-a11y/no-autofocus */}
<input onChange={({ target: { value } }) => typed2.push(value)} autoFocus />
<input
onChange={({ target: { value } }) => typed2.push(value)}
/* eslint-disable-next-line jsx-a11y/no-autofocus */
autoFocus
/>
<input onChange={({ target: { value } }) => typed3.push(value)} />
</form>,
);
@@ -64,9 +67,18 @@ describe('testUtils.Keyboard', () => {
const rendered = render(
<form onSubmit={handleSubmit}>
<input defaultValue="1" onChange={({ target: { value } }) => typed1.push(value)} />
<input defaultValue="2" onChange={({ target: { value } }) => typed2.push(value)} />
<input defaultValue="3" onChange={({ target: { value } }) => typed3.push(value)} />
<input
defaultValue="1"
onChange={({ target: { value } }) => typed1.push(value)}
/>
<input
defaultValue="2"
onChange={({ target: { value } }) => typed2.push(value)}
/>
<input
defaultValue="3"
onChange={({ target: { value } }) => typed3.push(value)}
/>
</form>,
);
@@ -29,7 +29,9 @@ const queryToBreakpoint = {
function toBreakpoint(query: string) {
const breakpoint = queryToBreakpoint[query];
if (!breakpoint) {
throw new Error(`received unknown media query in breakpoint mock: '${query}'`);
throw new Error(
`received unknown media query in breakpoint mock: '${query}'`,
);
}
return breakpoint;
}
+51 -1
View File
@@ -6197,6 +6197,11 @@ compare-func@^1.3.1:
array-ify "^1.0.0"
dot-prop "^3.0.0"
compare-versions@^3.5.1:
version "3.6.0"
resolved "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62"
integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==
component-emitter@^1.2.1:
version "1.3.0"
resolved "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
@@ -8633,7 +8638,7 @@ find-up@^2.0.0, find-up@^2.1.0:
dependencies:
locate-path "^2.0.0"
find-versions@^3.0.0:
find-versions@^3.0.0, find-versions@^3.2.0:
version "3.2.0"
resolved "https://registry.npmjs.org/find-versions/-/find-versions-3.2.0.tgz#10297f98030a786829681690545ef659ed1d254e"
integrity sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww==
@@ -9813,6 +9818,22 @@ humanize-ms@^1.2.1:
dependencies:
ms "^2.0.0"
husky@^4.2.3:
version "4.2.3"
resolved "https://registry.npmjs.org/husky/-/husky-4.2.3.tgz#3b18d2ee5febe99e27f2983500202daffbc3151e"
integrity sha512-VxTsSTRwYveKXN4SaH1/FefRJYCtx+wx04sSVcOpD7N2zjoHxa+cEJ07Qg5NmV3HAK+IRKOyNVpi2YBIVccIfQ==
dependencies:
chalk "^3.0.0"
ci-info "^2.0.0"
compare-versions "^3.5.1"
cosmiconfig "^6.0.0"
find-versions "^3.2.0"
opencollective-postinstall "^2.0.2"
pkg-dir "^4.2.0"
please-upgrade-node "^3.2.0"
slash "^3.0.0"
which-pm-runs "^1.0.0"
hyphenate-style-name@^1.0.2, hyphenate-style-name@^1.0.3:
version "1.0.3"
resolved "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz#097bb7fa0b8f1a9cf0bd5c734cf95899981a9b48"
@@ -12171,6 +12192,25 @@ lint-staged@^10.0.4:
string-argv "0.3.1"
stringify-object "^3.3.0"
lint-staged@^10.1.0:
version "10.1.0"
resolved "https://registry.npmjs.org/lint-staged/-/lint-staged-10.1.0.tgz#18785bb005d5ed404f1c1db6563e082f7a7baac2"
integrity sha512-WzZ/T+O/aEaaT679sMgI4JqK5mnG69V5KQSouzVsShzZ8wGWte39HT3z61LsxjVNeCf8m/ChhvWJa2wTiQLy5A==
dependencies:
chalk "^3.0.0"
commander "^4.0.1"
cosmiconfig "^6.0.0"
debug "^4.1.1"
dedent "^0.7.0"
execa "^3.4.0"
listr "^0.14.3"
log-symbols "^3.0.0"
micromatch "^4.0.2"
normalize-path "^3.0.0"
please-upgrade-node "^3.2.0"
string-argv "0.3.1"
stringify-object "^3.3.0"
listr-silent-renderer@^1.1.1:
version "1.1.1"
resolved "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e"
@@ -13926,6 +13966,11 @@ open@^7.0.0, open@^7.0.2:
is-docker "^2.0.0"
is-wsl "^2.1.1"
opencollective-postinstall@^2.0.2:
version "2.0.2"
resolved "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz#5657f1bede69b6e33a45939b061eb53d3c6c3a89"
integrity sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw==
opener@^1.5.1:
version "1.5.1"
resolved "https://registry.npmjs.org/opener/-/opener-1.5.1.tgz#6d2f0e77f1a0af0032aca716c2c1fbb8e7e8abed"
@@ -19266,6 +19311,11 @@ which-module@^2.0.0:
resolved "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
which-pm-runs@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb"
integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=
which@^1.2.14, which@^1.2.9, which@^1.3.0, which@^1.3.1:
version "1.3.1"
resolved "https://registry.npmjs.org/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"