Update react-scripts & fix eslint

This commit is contained in:
Jannis Mattheis 2021-03-27 20:03:17 +01:00
parent 14d15abd94
commit 43c4eba0fa
35 changed files with 4224 additions and 3182 deletions

View File

@ -39,22 +39,22 @@ rules:
import/first: error import/first: error
import/no-unused-modules: error import/no-unused-modules: error
unicorn/no-abusive-eslint-disable: error unicorn/no-abusive-eslint-disable: off
unicorn/no-array-instanceof: error unicorn/no-array-instanceof: error
unicorn/no-unreadable-array-destructuring: error unicorn/no-unreadable-array-destructuring: error
unicorn/no-zero-fractions: error unicorn/no-zero-fractions: error
react/jsx-key: error react/jsx-key: error
react/jsx-pascal-case: error react/jsx-pascal-case: error
react/destructuring-assignment: warn react/destructuring-assignment: off
react/function-component-definition: [error, {namedComponents: arrow-function, unnamedComponents: arrow-function}] react/function-component-definition: off
react/no-array-index-key: error react/no-array-index-key: error
react/no-deprecated: error react/no-deprecated: off
react/no-string-refs: error react/no-string-refs: error
react/no-this-in-sfc: error react/no-this-in-sfc: error
react/no-typos: error react/no-typos: error
react/no-unknown-property: error react/no-unknown-property: error
react/prefer-stateless-function: error react/prefer-stateless-function: off
react/prop-types: off react/prop-types: off
jest/expect-expect: off jest/expect-expect: off
@ -65,18 +65,21 @@ rules:
"@typescript-eslint/await-thenable": error "@typescript-eslint/await-thenable": error
"@typescript-eslint/no-unused-vars": error "@typescript-eslint/no-unused-vars": error
"@typescript-eslint/no-use-before-define": off "@typescript-eslint/no-use-before-define": off
"@typescript-eslint/no-unsafe-call": off
"@typescript-eslint/consistent-type-assertions": [error, {assertionStyle: as}] "@typescript-eslint/consistent-type-assertions": [error, {assertionStyle: as}]
"@typescript-eslint/no-extra-non-null-assertion": error "@typescript-eslint/no-extra-non-null-assertion": error
"@typescript-eslint/no-inferrable-types": error "@typescript-eslint/no-inferrable-types": error
"@typescript-eslint/no-this-alias": error "@typescript-eslint/no-this-alias": error
"@typescript-eslint/no-throw-literal": error "@typescript-eslint/no-throw-literal": error
"@typescript-eslint/no-non-null-assertion": off
"@typescript-eslint/prefer-nullish-coalescing": error "@typescript-eslint/prefer-nullish-coalescing": error
"@typescript-eslint/prefer-optional-chain": error "@typescript-eslint/prefer-optional-chain": error
"@typescript-eslint/prefer-readonly": error "@typescript-eslint/prefer-readonly": off
"@typescript-eslint/unbound-method": error "@typescript-eslint/unbound-method": error
"@typescript-eslint/no-empty-function": off "@typescript-eslint/no-empty-function": off
"@typescript-eslint/explicit-module-boundary-types": off "@typescript-eslint/explicit-module-boundary-types": off
"@typescript-eslint/ban-ts-comment": off
"@typescript-eslint/no-floating-promises": off "@typescript-eslint/no-floating-promises": off
"@typescript-eslint/no-unsafe-member-access": off "@typescript-eslint/no-unsafe-member-access": off
"@typescript-eslint/no-unsafe-return": off "@typescript-eslint/no-unsafe-return": off

View File

@ -32,7 +32,7 @@
"build": "react-scripts build", "build": "react-scripts build",
"test": "react-scripts test --env=node", "test": "react-scripts test --env=node",
"eject": "react-scripts eject", "eject": "react-scripts eject",
"lint": "eslint \"src/*.{ts,tsx}\"", "lint": "eslint \"src/**/*.{ts,tsx}\"",
"format": "prettier \"src/**/*.{ts,tsx}\" --write", "format": "prettier \"src/**/*.{ts,tsx}\" --write",
"testformat": "prettier \"src/**/*.{ts,tsx}\" --list-different" "testformat": "prettier \"src/**/*.{ts,tsx}\" --list-different"
}, },
@ -62,7 +62,7 @@
"get-port": "^5.1.1", "get-port": "^5.1.1",
"prettier": "^2.1.1", "prettier": "^2.1.1",
"puppeteer": "^5.3.0", "puppeteer": "^5.3.0",
"react-scripts": "^3.4.3", "react-scripts": "^4.0.3",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"tree-kill": "^1.2.0", "tree-kill": "^1.2.0",
"typescript": "4.0.2", "typescript": "4.0.2",

View File

@ -12,18 +12,16 @@ export class AppStore extends BaseStore<IApplication> {
super(); super();
} }
protected requestItems = (): Promise<IApplication[]> => { protected requestItems = (): Promise<IApplication[]> =>
return axios axios
.get<IApplication[]>(`${config.get('url')}application`) .get<IApplication[]>(`${config.get('url')}application`)
.then((response) => response.data); .then((response) => response.data);
};
protected requestDelete = (id: number): Promise<void> => { protected requestDelete = (id: number): Promise<void> =>
return axios.delete(`${config.get('url')}application/${id}`).then(() => { axios.delete(`${config.get('url')}application/${id}`).then(() => {
this.onDelete(); this.onDelete();
return this.snack('Application deleted'); return this.snack('Application deleted');
}); });
};
@action @action
public uploadImage = async (id: number, file: Blob): Promise<void> => { public uploadImage = async (id: number, file: Blob): Promise<void> => {

View File

@ -72,21 +72,19 @@ class Applications extends Component<Stores<'appStore'>> {
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{apps.map((app: IApplication) => { {apps.map((app: IApplication) => (
return ( <Row
<Row key={app.id}
key={app.id} description={app.description}
description={app.description} image={app.image}
image={app.image} name={app.name}
name={app.name} value={app.token}
value={app.token} fUpload={() => this.uploadImage(app.id)}
fUpload={() => this.uploadImage(app.id)} fDelete={() => (this.deleteId = app.id)}
fDelete={() => (this.deleteId = app.id)} fEdit={() => (this.updateId = app.id)}
fEdit={() => (this.updateId = app.id)} noDelete={app.internal}
noDelete={app.internal} />
/> ))}
);
})}
</TableBody> </TableBody>
</Table> </Table>
<input <input
@ -133,7 +131,7 @@ class Applications extends Component<Stores<'appStore'>> {
}; };
private onUploadImage = (e: ChangeEvent<HTMLInputElement>) => { private onUploadImage = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files && e.target.files[0]; const file = e.target.files?.[0];
if (!file) { if (!file) {
return; return;
} }

View File

@ -23,8 +23,12 @@ interface IState {
export default class UpdateDialog extends Component<IProps, IState> { export default class UpdateDialog extends Component<IProps, IState> {
public state = {name: '', description: ''}; public state = {name: '', description: ''};
public componentWillMount() { constructor(props: IProps) {
this.setState({name: this.props.initialName, description: this.props.initialDescription}); super(props);
this.state = {
name: props.initialName,
description: props.initialDescription,
};
} }
public render() { public render() {

View File

@ -10,9 +10,8 @@ export class ClientStore extends BaseStore<IClient> {
super(); super();
} }
protected requestItems = (): Promise<IClient[]> => { protected requestItems = (): Promise<IClient[]> =>
return axios.get<IClient[]>(`${config.get('url')}client`).then((response) => response.data); axios.get<IClient[]>(`${config.get('url')}client`).then((response) => response.data);
};
protected requestDelete(id: number): Promise<void> { protected requestDelete(id: number): Promise<void> {
return axios return axios

View File

@ -64,17 +64,15 @@ class Clients extends Component<Stores<'clientStore'>> {
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{clients.map((client: IClient) => { {clients.map((client: IClient) => (
return ( <Row
<Row key={client.id}
key={client.id} name={client.name}
name={client.name} value={client.token}
value={client.token} fEdit={() => (this.updateId = client.id)}
fEdit={() => (this.updateId = client.id)} fDelete={() => (this.deleteId = client.id)}
fDelete={() => (this.deleteId = client.id)} />
/> ))}
);
})}
</TableBody> </TableBody>
</Table> </Table>
</Paper> </Paper>

View File

@ -21,8 +21,11 @@ interface IState {
export default class UpdateDialog extends Component<IProps, IState> { export default class UpdateDialog extends Component<IProps, IState> {
public state = {name: ''}; public state = {name: ''};
public componentWillMount() { constructor(props: IProps) {
this.setState({name: this.props.initialName}); super(props);
this.state = {
name: props.initialName,
};
} }
public render() { public render() {

View File

@ -38,9 +38,8 @@ export abstract class BaseStore<T extends HasID> implements IClearable {
return item; return item;
}; };
public getByIDOrUndefined = (id: number): T | undefined => { public getByIDOrUndefined = (id: number): T | undefined =>
return this.items.find((hasId: HasID) => hasId.id === id); this.items.find((hasId: HasID) => hasId.id === id);
};
public getItems = (): T[] => this.items; public getItems = (): T[] => this.items;

View File

@ -8,22 +8,20 @@ interface ConnectionErrorBannerProps {
message: string; message: string;
} }
export const ConnectionErrorBanner = ({height, retry, message}: ConnectionErrorBannerProps) => { export const ConnectionErrorBanner = ({height, retry, message}: ConnectionErrorBannerProps) => (
return ( <div
<div style={{
style={{ backgroundColor: '#e74c3c',
backgroundColor: '#e74c3c', height,
height, width: '100%',
width: '100%', zIndex: 1300,
zIndex: 1300, position: 'relative',
position: 'relative', }}>
}}> <Typography align="center" variant="h6" style={{lineHeight: `${height}px`}}>
<Typography align="center" variant="h6" style={{lineHeight: `${height}px`}}> {message}{' '}
{message}{' '} <Button variant="outlined" onClick={retry}>
<Button variant="outlined" onClick={retry}> Retry
Retry </Button>
</Button> </Typography>
</Typography> </div>
</div> );
);
};

View File

@ -9,15 +9,13 @@ const styles = () => ({
}); });
interface IProps extends WithStyles<'paper'> { interface IProps extends WithStyles<'paper'> {
style?: object; style?: React.CSSProperties;
} }
const Container: React.SFC<IProps> = ({classes, children, style}) => { const Container: React.FC<IProps> = ({classes, children, style}) => (
return ( <Paper elevation={6} className={classes.paper} style={style}>
<Paper elevation={6} className={classes.paper} style={style}> {children}
{children} </Paper>
</Paper> );
);
};
export default withStyles(styles)(Container); export default withStyles(styles)(Container);

View File

@ -2,11 +2,11 @@ import IconButton from '@material-ui/core/IconButton';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import Visibility from '@material-ui/icons/Visibility'; import Visibility from '@material-ui/icons/Visibility';
import VisibilityOff from '@material-ui/icons/VisibilityOff'; import VisibilityOff from '@material-ui/icons/VisibilityOff';
import React, {Component} from 'react'; import React, {Component, CSSProperties} from 'react';
interface IProps { interface IProps {
value: string; value: string;
style?: object; style?: CSSProperties;
} }
interface IState { interface IState {

View File

@ -209,7 +209,7 @@ const ResponsiveButton: React.FC<{
id?: string; id?: string;
onClick?: () => void; onClick?: () => void;
icon: React.ReactNode; icon: React.ReactNode;
}> = ({width, icon, children, label, ...rest}) => { }> = ({width, icon, label, ...rest}) => {
if (width === 'xs' || width === 'sm') { if (width === 'xs' || width === 'sm') {
return <IconButton {...rest}>{icon}</IconButton>; return <IconButton {...rest}>{icon}</IconButton>;
} }

View File

@ -50,9 +50,8 @@ const themeMap: Record<ThemeKey, Theme> = {
}), }),
}; };
const isThemeKey = (value: string | null): value is ThemeKey => { const isThemeKey = (value: string | null): value is ThemeKey =>
return value === 'light' || value === 'dark'; value === 'light' || value === 'dark';
};
@observer @observer
class Layout extends React.Component< class Layout extends React.Component<

View File

@ -52,19 +52,17 @@ class Navigation extends Component<
const userApps = const userApps =
apps.length === 0 apps.length === 0
? null ? null
: apps.map((app) => { : apps.map((app) => (
return ( <Link
<Link onClick={() => setNavOpen(false)}
onClick={() => setNavOpen(false)} className={`${classes.link} item`}
className={`${classes.link} item`} to={'/messages/' + app.id}
to={'/messages/' + app.id} key={app.id}>
key={app.id}> <ListItem button>
<ListItem button> <ListItemText primary={app.name} />
<ListItemText primary={app.name} /> </ListItem>
</ListItem> </Link>
</Link> ));
);
});
const placeholderItems = [ const placeholderItems = [
<ListItem button disabled key={-1}> <ListItem button disabled key={-1}>
@ -108,24 +106,22 @@ class Navigation extends Component<
const ResponsiveDrawer: React.FC< const ResponsiveDrawer: React.FC<
DrawerProps & {navOpen: boolean; setNavOpen: (open: boolean) => void} DrawerProps & {navOpen: boolean; setNavOpen: (open: boolean) => void}
> = ({navOpen, setNavOpen, children, ...rest}) => { > = ({navOpen, setNavOpen, children, ...rest}) => (
return ( <>
<> <Hidden smUp implementation="css">
<Hidden smUp implementation="css"> <Drawer variant="temporary" open={navOpen} {...rest}>
<Drawer variant="temporary" open={navOpen} {...rest}> <IconButton onClick={() => setNavOpen(false)}>
<IconButton onClick={() => setNavOpen(false)}> <CloseIcon />
<CloseIcon /> </IconButton>
</IconButton> {children}
{children} </Drawer>
</Drawer> </Hidden>
</Hidden> <Hidden xsDown implementation="css">
<Hidden xsDown implementation="css"> <Drawer variant="permanent" {...rest}>
<Drawer variant="permanent" {...rest}> {children}
{children} </Drawer>
</Drawer> </Hidden>
</Hidden> </>
</> );
);
};
export default withStyles(styles, {withTheme: true})(inject('appStore')(Navigation)); export default withStyles(styles, {withTheme: true})(inject('appStore')(Navigation));

View File

@ -12,7 +12,7 @@ import {observable} from 'mobx';
import ReactInfinite from 'react-infinite'; import ReactInfinite from 'react-infinite';
import {IMessage} from '../types'; import {IMessage} from '../types';
interface IProps extends RouteComponentProps<{id: string}> {} type IProps = RouteComponentProps<{id: string}>;
interface IState { interface IState {
appId: number; appId: number;
@ -121,24 +121,22 @@ class Messages extends Component<IProps & Stores<'messagesStore' | 'appStore'>,
private deleteMessage = (message: IMessage) => () => private deleteMessage = (message: IMessage) => () =>
this.props.messagesStore.removeSingle(message); this.props.messagesStore.removeSingle(message);
private renderMessage = (message: IMessage) => { private renderMessage = (message: IMessage) => (
return ( <Message
<Message key={message.id}
key={message.id} height={(height: number) => {
height={(height: number) => { if (!this.heights[message.id]) {
if (!this.heights[message.id]) { this.heights[message.id] = height;
this.heights[message.id] = height; }
} }}
}} fDelete={this.deleteMessage(message)}
fDelete={this.deleteMessage(message)} title={message.title}
title={message.title} date={message.date}
date={message.date} content={message.message}
content={message.message} image={message.image}
image={message.image} extras={message.extras}
extras={message.extras} />
/> );
);
};
private checkIfLoadMore() { private checkIfLoadMore() {
const {appId} = this.state; const {appId} = this.state;

View File

@ -50,7 +50,7 @@ export class MessagesStore {
); );
state.messages.replace([...state.messages, ...pagedResult.messages]); state.messages.replace([...state.messages, ...pagedResult.messages]);
state.nextSince = pagedResult.paging.since || 0; state.nextSince = pagedResult.paging.since ?? 0;
state.hasMore = 'next' in pagedResult.paging; state.hasMore = 'next' in pagedResult.paging;
state.loaded = true; state.loaded = true;
this.loading = false; this.loading = false;
@ -139,12 +139,10 @@ export class MessagesStore {
.getItems() .getItems()
.reduce((all, app) => ({...all, [app.id]: app.image}), {}); .reduce((all, app) => ({...all, [app.id]: app.image}), {});
return this.stateOf(appId, false).messages.map((message: IMessage) => { return this.stateOf(appId, false).messages.map((message: IMessage) => ({
return { ...message,
...message, image: appToImage[message.appid] || null,
image: appToImage[message.appid] || null, }));
};
});
}; };
public get = createTransformer(this.getUnCached); public get = createTransformer(this.getUnCached);

View File

@ -38,7 +38,7 @@ export class WebSocketStore {
setTimeout(() => this.listen(callback), 30000); setTimeout(() => this.listen(callback), 30000);
}) })
.catch((error: AxiosError) => { .catch((error: AxiosError) => {
if (error && error.response && error.response.status === 401) { if (error?.response?.status === 401) {
this.snack('Could not authenticate with client token, logging out.'); this.snack('Could not authenticate with client token, logging out.');
} }
}); });
@ -47,5 +47,5 @@ export class WebSocketStore {
this.ws = ws; this.ws = ws;
}; };
public close = () => this.ws && this.ws.close(1000, 'WebSocketStore#close'); public close = () => this.ws?.close(1000, 'WebSocketStore#close');
} }

View File

@ -17,7 +17,7 @@ import Container from '../common/Container';
import {inject, Stores} from '../inject'; import {inject, Stores} from '../inject';
import {IPlugin} from '../types'; import {IPlugin} from '../types';
interface IProps extends RouteComponentProps<{id: string}> {} type IProps = RouteComponentProps<{id: string}>;
interface IState { interface IState {
displayText: string | null; displayText: string | null;
@ -122,7 +122,7 @@ interface IPanelWrapperProps {
icon?: React.ComponentType; icon?: React.ComponentType;
} }
const PanelWrapper: React.SFC<IPanelWrapperProps> = ({ const PanelWrapper: React.FC<IPanelWrapperProps> = ({
name, name,
description, description,
refresh, refresh,
@ -175,7 +175,7 @@ class ConfigurerPanel extends Component<IConfigurerPanelProps, {unsavedChanges:
theme: 'material', theme: 'material',
lineNumbers: true, lineNumbers: true,
}} }}
onChange={(instance, data, value) => { onChange={(_, _1, value) => {
let newConf: string | null = value; let newConf: string | null = value;
if (value === this.props.initialConfig) { if (value === this.props.initialConfig) {
newConf = null; newConf = null;
@ -195,6 +195,7 @@ class ConfigurerPanel extends Component<IConfigurerPanelProps, {unsavedChanges:
className="config-save" className="config-save"
onClick={() => { onClick={() => {
const newConfig = this.state.unsavedChanges; const newConfig = this.state.unsavedChanges;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.props.save(newConfig!).then(() => { this.props.save(newConfig!).then(() => {
this.setState({unsavedChanges: null}); this.setState({unsavedChanges: null});
}); });
@ -210,7 +211,7 @@ interface IDisplayerPanelProps {
pluginInfo: IPlugin; pluginInfo: IPlugin;
displayText: string; displayText: string;
} }
const DisplayerPanel: React.SFC<IDisplayerPanelProps> = ({pluginInfo, displayText}) => ( const DisplayerPanel: React.FC<IDisplayerPanelProps> = ({displayText}) => (
<Typography variant="body2"> <Typography variant="body2">
<ReactMarkDown source={displayText} /> <ReactMarkDown source={displayText} />
</Typography> </Typography>

View File

@ -12,23 +12,16 @@ export class PluginStore extends BaseStore<IPlugin> {
super(); super();
} }
public requestConfig = (id: number): Promise<string> => { public requestConfig = (id: number): Promise<string> =>
return axios axios.get(`${config.get('url')}plugin/${id}/config`).then((response) => response.data);
.get(`${config.get('url')}plugin/${id}/config`)
.then((response) => response.data);
};
public requestDisplay = (id: number): Promise<string> => { public requestDisplay = (id: number): Promise<string> =>
return axios axios.get(`${config.get('url')}plugin/${id}/display`).then((response) => response.data);
.get(`${config.get('url')}plugin/${id}/display`)
.then((response) => response.data);
};
protected requestItems = (): Promise<IPlugin[]> => { protected requestItems = (): Promise<IPlugin[]> =>
return axios.get<IPlugin[]>(`${config.get('url')}plugin`).then((response) => response.data); axios.get<IPlugin[]>(`${config.get('url')}plugin`).then((response) => response.data);
};
protected requestDelete = (id: number): Promise<void> => { protected requestDelete = (): Promise<void> => {
this.snack('Cannot delete plugin'); this.snack('Cannot delete plugin');
throw new Error('Cannot delete plugin'); throw new Error('Cannot delete plugin');
}; };

View File

@ -39,23 +39,21 @@ class Plugins extends Component<Stores<'pluginStore'>> {
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{plugins.map((plugin: IPlugin) => { {plugins.map((plugin: IPlugin) => (
return ( <Row
<Row key={plugin.token}
key={plugin.token} id={plugin.id}
id={plugin.id} token={plugin.token}
token={plugin.token} name={plugin.name}
name={plugin.name} enabled={plugin.enabled}
enabled={plugin.enabled} fToggleStatus={() =>
fToggleStatus={() => this.props.pluginStore.changeEnabledState(
this.props.pluginStore.changeEnabledState( plugin.id,
plugin.id, !plugin.enabled
!plugin.enabled )
) }
} />
/> ))}
);
})}
</TableBody> </TableBody>
</Table> </Table>
</Paper> </Paper>

View File

@ -26,44 +26,42 @@ const hiddenToken = '•••••••••••••••';
const $table = selector.table('#app-table'); const $table = selector.table('#app-table');
const $dialog = selector.form('#app-dialog'); const $dialog = selector.form('#app-dialog');
const hasApp = (name: string, description: string, row: number): (() => Promise<void>) => { const hasApp = (
return async () => { name: string,
expect(await innerText(page, $table.cell(row, Col.Name))).toBe(name); description: string,
expect(await innerText(page, $table.cell(row, Col.Token))).toBe(hiddenToken); row: number
expect(await innerText(page, $table.cell(row, Col.Description))).toBe(description); ): (() => Promise<void>) => async () => {
}; expect(await innerText(page, $table.cell(row, Col.Name))).toBe(name);
expect(await innerText(page, $table.cell(row, Col.Token))).toBe(hiddenToken);
expect(await innerText(page, $table.cell(row, Col.Description))).toBe(description);
}; };
export const updateApp = ( const updateApp = (
id: number, id: number,
data: {name?: string; description?: string} data: {name?: string; description?: string}
): (() => Promise<void>) => { ): (() => Promise<void>) => async () => {
return async () => { await page.click($table.cell(id, Col.EditUpdate, '.edit'));
await page.click($table.cell(id, Col.EditUpdate, '.edit')); await page.waitForSelector($dialog.selector());
await page.waitForSelector($dialog.selector()); if (data.name) {
if (data.name) { const nameSelector = $dialog.input('.name');
const nameSelector = $dialog.input('.name'); await clearField(page, nameSelector);
await clearField(page, nameSelector); await page.type(nameSelector, data.name);
await page.type(nameSelector, data.name); }
} if (data.description) {
if (data.description) { const descSelector = $dialog.textarea('.description');
const descSelector = $dialog.textarea('.description'); await clearField(page, descSelector);
await clearField(page, descSelector); await page.type(descSelector, data.description);
await page.type(descSelector, data.description); }
} await page.click($dialog.button('.update'));
await page.click($dialog.button('.update')); await waitToDisappear(page, $dialog.selector());
await waitToDisappear(page, $dialog.selector());
};
}; };
export const createApp = (name: string, description: string): (() => Promise<void>) => { const createApp = (name: string, description: string): (() => Promise<void>) => async () => {
return async () => { await page.click('#create-app');
await page.click('#create-app'); await page.waitForSelector($dialog.selector());
await page.waitForSelector($dialog.selector()); await page.type($dialog.input('.name'), name);
await page.type($dialog.input('.name'), name); await page.type($dialog.textarea('.description'), description);
await page.type($dialog.textarea('.description'), description); await page.click($dialog.button('.create'));
await page.click($dialog.button('.create'));
};
}; };
describe('Application', () => { describe('Application', () => {

View File

@ -21,24 +21,20 @@ enum Col {
Delete = 4, Delete = 4,
} }
const hasClient = (name: string, row: number): (() => Promise<void>) => { const hasClient = (name: string, row: number): (() => Promise<void>) => async () => {
return async () => { expect(await innerText(page, $table.cell(row, Col.Name))).toBe(name);
expect(await innerText(page, $table.cell(row, Col.Name))).toBe(name);
};
}; };
export const updateClient = (id: number, data: {name?: string}): (() => Promise<void>) => { const updateClient = (id: number, data: {name?: string}): (() => Promise<void>) => async () => {
return async () => { await page.click($table.cell(id, Col.Edit, '.edit'));
await page.click($table.cell(id, Col.Edit, '.edit')); await page.waitForSelector($dialog.selector());
await page.waitForSelector($dialog.selector()); if (data.name) {
if (data.name) { const nameSelector = $dialog.input('.name');
const nameSelector = $dialog.input('.name'); await clearField(page, nameSelector);
await clearField(page, nameSelector); await page.type(nameSelector, data.name);
await page.type(nameSelector, data.name); }
} await page.click($dialog.button('.update'));
await page.click($dialog.button('.update')); await waitToDisappear(page, $dialog.selector());
await waitToDisappear(page, $dialog.selector());
};
}; };
const $table = selector.table('#client-table'); const $table = selector.table('#client-table');
@ -57,13 +53,11 @@ describe('Client', () => {
expect(await count(page, $table.rows())).toBe(1); expect(await count(page, $table.rows())).toBe(1);
}); });
describe('create clients', () => { describe('create clients', () => {
const createClient = (name: string): (() => Promise<void>) => { const createClient = (name: string): (() => Promise<void>) => async () => {
return async () => { await page.click('#create-client');
await page.click('#create-client'); await page.waitForSelector($dialog.selector());
await page.waitForSelector($dialog.selector()); await page.type($dialog.input('.name'), name);
await page.type($dialog.input('.name'), name); await page.click($dialog.button('.create'));
await page.click($dialog.button('.create'));
};
}; };
it('phone', createClient('phone')); it('phone', createClient('phone'));
it('desktop app', createClient('desktop app')); it('desktop app', createClient('desktop app'));

View File

@ -44,11 +44,10 @@ describe('Messages', () => {
it('has url', async () => { it('has url', async () => {
expect(page.url()).toContain('/'); expect(page.url()).toContain('/');
}); });
const createApp = (name: string) => { const createApp = (name: string) =>
return axios axios
.post<IApplication>(`${gotify.url}/application`, {name}, axiosAuth) .post<IApplication>(`${gotify.url}/application`, {name}, axiosAuth)
.then((resp) => resp.data.token); .then((resp) => resp.data.token);
};
it('shows navigation', async () => { it('shows navigation', async () => {
await page.waitForSelector(naviId); await page.waitForSelector(naviId);
}); });
@ -121,11 +120,10 @@ describe('Messages', () => {
const backup2 = m('Backup done', 'Windows Server Backup finished (6.2GB).'); const backup2 = m('Backup done', 'Windows Server Backup finished (6.2GB).');
const backup3 = m('Backup done', 'Gotify Backup finished (0.1MB).'); const backup3 = m('Backup done', 'Gotify Backup finished (0.1MB).');
const createMessage = (msg: Partial<IMessage>, token: string) => { const createMessage = (msg: Partial<IMessage>, token: string) =>
return axios.post<IMessage>(`${gotify.url}/message`, msg, { axios.post<IMessage>(`${gotify.url}/message`, msg, {
headers: {'X-Gotify-Key': token}, headers: {'X-Gotify-Key': token},
}); });
};
const expectMessages = async (toCheck: { const expectMessages = async (toCheck: {
all: Msg[]; all: Msg[];

View File

@ -49,13 +49,10 @@ const toggleEnabled = async (id: number) => {
); );
}; };
const pluginInfo = async (className: string) => { const pluginInfo = async (className: string) =>
return await innerText(page, `.plugin-info .${className} > span`); await innerText(page, `.plugin-info .${className} > span`);
};
const getDisplayer = async () => { const getDisplayer = async () => await innerText(page, '.displayer');
return await innerText(page, '.displayer');
};
const hasReceivedMessage = async (title: RegExp, content: RegExp) => { const hasReceivedMessage = async (title: RegExp, content: RegExp) => {
await page.click('#message-navigation a'); await page.click('#message-navigation a');

View File

@ -1,24 +1,18 @@
export const heading = () => { export const heading = () => `main h4`;
return `main h4`;
};
export const table = (tableSelector: string) => { export const table = (tableSelector: string) => ({
return { selector: () => tableSelector,
selector: () => tableSelector, rows: () => `${tableSelector} tbody tr`,
rows: () => `${tableSelector} tbody tr`, row: (index: number) => `${tableSelector} tbody tr:nth-child(${index})`,
row: (index: number) => `${tableSelector} tbody tr:nth-child(${index})`, cell: (index: number, col: number, suffix = '') =>
cell: (index: number, col: number, suffix = '') => `${tableSelector} tbody tr:nth-child(${index}) td:nth-child(${col}) ${suffix}`,
`${tableSelector} tbody tr:nth-child(${index}) td:nth-child(${col}) ${suffix}`, });
};
};
export const form = (dialogSelector: string) => { export const form = (dialogSelector: string) => ({
return { selector: () => dialogSelector,
selector: () => dialogSelector, input: (selector: string) => `${dialogSelector} ${selector} input`,
input: (selector: string) => `${dialogSelector} ${selector} input`, textarea: (selector: string) => `${dialogSelector} ${selector} textarea`,
textarea: (selector: string) => `${dialogSelector} ${selector} textarea`, button: (selector: string) => `${dialogSelector} button${selector}`,
button: (selector: string) => `${dialogSelector} button${selector}`, });
};
};
export const $confirmDialog = form('.confirm-dialog'); export const $confirmDialog = form('.confirm-dialog');

View File

@ -82,8 +82,8 @@ const testFilePath = (): string => {
return path.join(testBuildPath, filename); return path.join(testBuildPath, filename);
}; };
const waitForGotify = (url: string): Promise<void> => { const waitForGotify = (url: string): Promise<void> =>
return new Promise((resolve, err) => { new Promise((resolve, err) => {
wait({resources: [url], timeout: 40000}, (error: string) => { wait({resources: [url], timeout: 40000}, (error: string) => {
if (error) { if (error) {
console.log(error); console.log(error);
@ -93,7 +93,6 @@ const waitForGotify = (url: string): Promise<void> => {
} }
}); });
}); });
};
const buildGoPlugin = (filename: string, pluginPath: string): Promise<void> => { const buildGoPlugin = (filename: string, pluginPath: string): Promise<void> => {
process.stdout.write(`### Building Plugin ${pluginPath}\n`); process.stdout.write(`### Building Plugin ${pluginPath}\n`);

View File

@ -39,28 +39,28 @@ describe('User', () => {
name: string, name: string,
password: string, password: string,
isAdmin: boolean isAdmin: boolean
): (() => Promise<void>) => { ): (() => Promise<void>) => async () => {
return async () => { await page.click('#create-user');
await page.click('#create-user'); await page.waitForSelector($dialog.selector());
await page.waitForSelector($dialog.selector()); await page.type($dialog.input('.name'), name);
await page.type($dialog.input('.name'), name); await page.type($dialog.input('.password'), password);
await page.type($dialog.input('.password'), password); if (isAdmin) {
if (isAdmin) { await page.click($dialog.input('.admin-rights'));
await page.click($dialog.input('.admin-rights')); }
} await page.click($dialog.button('.save-create'));
await page.click($dialog.button('.save-create')); await waitToDisappear(page, $dialog.selector());
await waitToDisappear(page, $dialog.selector());
};
}; };
it('nicories', createUser('nicories', '123', false)); it('nicories', createUser('nicories', '123', false));
it('jmattheis', createUser('jmattheis', 'noice', true)); it('jmattheis', createUser('jmattheis', 'noice', true));
it('dude', createUser('dude', '1', false)); it('dude', createUser('dude', '1', false));
}); });
const hasUser = (name: string, isAdmin: boolean, row: number): (() => Promise<void>) => { const hasUser = (
return async () => { name: string,
expect(await innerText(page, $table.cell(row, Col.Name))).toBe(name); isAdmin: boolean,
expect(await innerText(page, $table.cell(row, Col.Admin))).toBe(isAdmin ? 'Yes' : 'No'); row: number
}; ): (() => Promise<void>) => async () => {
expect(await innerText(page, $table.cell(row, Col.Name))).toBe(name);
expect(await innerText(page, $table.cell(row, Col.Admin))).toBe(isAdmin ? 'Yes' : 'No');
}; };
describe('has created users', () => { describe('has created users', () => {
@ -131,10 +131,10 @@ describe('User', () => {
it('does logout', async () => await auth.logout(page)); it('does logout', async () => await auth.logout(page));
it('can login with new password (admin)', async () => it('can login with new password (admin)', async () =>
await auth.login(page, 'admin', 'changed')); await auth.login(page, 'admin', 'changed'));
it('does logout', async () => await auth.logout(page)); it('does logout admin', async () => await auth.logout(page));
it('can login with nicolas', async () => await auth.login(page, 'nicolas', '123')); it('can login with nicolas', async () => await auth.login(page, 'nicolas', '123'));
it('does logout', async () => await auth.logout(page)); it('does logout nicolas', async () => await auth.logout(page));
it('can login with jmattheis', async () => await auth.login(page, 'jmattheis', 'unicorn')); it('can login with jmattheis', async () => await auth.login(page, 'jmattheis', 'unicorn'));
it('does logout', async () => await auth.logout(page)); it('does logout jmattheis', async () => await auth.logout(page));
}); });

View File

@ -4,7 +4,8 @@ export const innerText = async (page: ElementHandle | Page, selector: string): P
const element = await page.$(selector); const element = await page.$(selector);
const handle = await element!.getProperty('innerText'); const handle = await element!.getProperty('innerText');
const value = await handle.jsonValue(); const value = await handle.jsonValue();
return (value as object).toString().trim(); // eslint-disable-next-line @typescript-eslint/no-explicit-any
return (value as any).toString().trim();
}; };
export const clickByText = async (page: Page, selector: string, text: string): Promise<void> => { export const clickByText = async (page: Page, selector: string, text: string): Promise<void> => {
@ -21,42 +22,32 @@ export const clickByText = async (page: Page, selector: string, text: string): P
); );
}; };
export const count = async (page: Page, selector: string): Promise<number> => { export const count = async (page: Page, selector: string): Promise<number> =>
return page.$$(selector).then((elements) => elements.length); page.$$(selector).then((elements) => elements.length);
};
export const waitToDisappear = async (page: Page, selector: string): Promise<JSHandle> => { export const waitToDisappear = async (page: Page, selector: string): Promise<JSHandle> =>
return page.waitForFunction( page.waitForFunction((_selector: string) => !document.querySelector(_selector), {}, selector);
(_selector: string) => !document.querySelector(_selector),
{},
selector
);
};
export const waitForCount = async ( export const waitForCount = async (
page: Page, page: Page,
selector: string, selector: string,
amount: number amount: number
): Promise<JSHandle> => { ): Promise<JSHandle> =>
return page.waitForFunction( page.waitForFunction(
(_selector: string, _amount: number) => (_selector: string, _amount: number) =>
document.querySelectorAll(_selector).length === _amount, document.querySelectorAll(_selector).length === _amount,
{}, {},
selector, selector,
amount amount
); );
};
export const waitForExists = async (page: Page, selector: string, text: string): Promise<void> => { export const waitForExists = async (page: Page, selector: string, text: string): Promise<void> => {
text = text.toLowerCase(); text = text.toLowerCase();
await page.waitForFunction( await page.waitForFunction(
(_selector: string, _text: string) => { (_selector: string, _text: string) =>
return ( Array.from(document.querySelectorAll(_selector)).filter(
Array.from(document.querySelectorAll(_selector)).filter( (element) => element.textContent!.toLowerCase().trim() === _text
(element) => element.textContent!.toLowerCase().trim() === _text ).length > 0,
).length > 0
);
},
{}, {},
selector, selector,
text text
@ -67,7 +58,6 @@ export const clearField = async (element: ElementHandle | Page, selector: string
const elementHandle = await element.$(selector); const elementHandle = await element.$(selector);
if (!elementHandle) { if (!elementHandle) {
fail(); fail();
return;
} }
await elementHandle.click(); await elementHandle.click();
await elementHandle.focus(); await elementHandle.focus();

View File

@ -5,5 +5,5 @@ declare module 'react-timeago' {
date: string; date: string;
} }
export default class TimeAgo extends React.Component<ITimeAgoProps, any> {} export default class TimeAgo extends React.Component<ITimeAgoProps, unknown> {}
} }

View File

@ -25,9 +25,9 @@ interface IState {
export default class AddEditDialog extends Component<IProps, IState> { export default class AddEditDialog extends Component<IProps, IState> {
public state = { public state = {
name: '', name: this.props.name ?? '',
pass: '', pass: '',
admin: false, admin: this.props.admin ?? false,
}; };
public render() { public render() {
@ -107,11 +107,6 @@ export default class AddEditDialog extends Component<IProps, IState> {
); );
} }
public componentWillMount() {
const {name, admin} = this.props;
this.setState({...this.state, name: name || '', admin: admin || false});
}
private handleChange(propertyName: string, event: ChangeEvent<HTMLInputElement>) { private handleChange(propertyName: string, event: ChangeEvent<HTMLInputElement>) {
const state = this.state; const state = this.state;
state[propertyName] = event.target.value; state[propertyName] = event.target.value;

View File

@ -10,9 +10,8 @@ export class UserStore extends BaseStore<IUser> {
super(); super();
} }
protected requestItems = (): Promise<IUser[]> => { protected requestItems = (): Promise<IUser[]> =>
return axios.get<IUser[]>(`${config.get('url')}user`).then((response) => response.data); axios.get<IUser[]>(`${config.get('url')}user`).then((response) => response.data);
};
protected requestDelete(id: number): Promise<void> { protected requestDelete(id: number): Promise<void> {
return axios return axios

View File

@ -90,17 +90,15 @@ class Users extends Component<WithStyles<'wrapper'> & Stores<'userStore'>> {
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{users.map((user: IUser) => { {users.map((user: IUser) => (
return ( <UserRow
<UserRow key={user.id}
key={user.id} name={user.name}
name={user.name} admin={user.admin}
admin={user.admin} fDelete={() => (this.deleteId = user.id)}
fDelete={() => (this.deleteId = user.id)} fEdit={() => (this.editId = user.id)}
fEdit={() => (this.editId = user.id)} />
/> ))}
);
})}
</TableBody> </TableBody>
</Table> </Table>
</Paper> </Paper>

View File

@ -28,7 +28,8 @@
"noEmit": true, "noEmit": true,
"module": "esnext", "module": "esnext",
"resolveJsonModule": true, "resolveJsonModule": true,
"keyofStringsOnly": true "keyofStringsOnly": true,
"noFallthroughCasesInSwitch": true
}, },
"exclude": [ "exclude": [
"node_modules", "node_modules",

File diff suppressed because it is too large Load Diff