diff --git a/.gitignore b/.gitignore
index 138e96d05..92e9362d8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,7 @@
/deploy.sh
/.vs/
yarn-error.log*
+/junit.xml
/static/
/static-test/
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 5831e8a8d..f2f769441 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -56,7 +56,14 @@ jest:
- "jest.config.js"
- "package.json"
- "yarn.lock"
+ - ".gitlab-ci.yml"
coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/
+ artifacts:
+ reports:
+ junit: junit.xml
+ coverage_report:
+ coverage_format: cobertura
+ path: .coverage/cobertura-coverage.xml
nginx-test:
stage: test
diff --git a/app/soapbox/components/ui/button/useButtonStyles.ts b/app/soapbox/components/ui/button/useButtonStyles.ts
index 4e24c0d43..6f26d8be1 100644
--- a/app/soapbox/components/ui/button/useButtonStyles.ts
+++ b/app/soapbox/components/ui/button/useButtonStyles.ts
@@ -1,6 +1,6 @@
import classNames from 'classnames';
-type ButtonThemes = 'primary' | 'secondary' | 'ghost' | 'accent' | 'danger' | 'transparent' | 'link'
+type ButtonThemes = 'primary' | 'secondary' | 'ghost' | 'accent' | 'danger' | 'transparent' | 'link' | 'danger-link'
type ButtonSizes = 'sm' | 'md' | 'lg'
type IButtonStyles = {
@@ -25,6 +25,7 @@ const useButtonStyles = ({
ghost: 'shadow-none border-gray-200 text-gray-700 bg-white dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200 focus:ring-primary-500 focus:ring-2 focus:ring-offset-2',
accent: 'border-transparent text-white bg-accent-500 hover:bg-accent-300 focus:ring-pink-500 focus:ring-2 focus:ring-offset-2',
danger: 'border-transparent text-danger-700 bg-danger-100 hover:bg-danger-200 focus:ring-danger-500 focus:ring-2 focus:ring-offset-2',
+ 'danger-link': 'border-transparent text-accent-500 hover:bg-accent-500 hover:bg-opacity-10',
transparent: 'border-transparent text-gray-800 backdrop-blur-sm bg-white/75 hover:bg-white/80',
link: 'border-transparent text-primary-600 dark:text-primary-400 hover:bg-primary-100 hover:text-primary-700 dark:hover:bg-slate-900/50',
};
diff --git a/app/soapbox/features/compose/components/compose_form.js b/app/soapbox/features/compose/components/compose_form.js
index 31e80741c..15b14fadb 100644
--- a/app/soapbox/features/compose/components/compose_form.js
+++ b/app/soapbox/features/compose/components/compose_form.js
@@ -14,13 +14,13 @@ import Icon from 'soapbox/components/icon';
import { Button } from 'soapbox/components/ui';
import { isMobile } from 'soapbox/is_mobile';
+import PollForm from '../components/polls/poll-form';
import ReplyMentions from '../components/reply_mentions';
import UploadForm from '../components/upload_form';
import Warning from '../components/warning';
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
import MarkdownButtonContainer from '../containers/markdown_button_container';
import PollButtonContainer from '../containers/poll_button_container';
-import PollFormContainer from '../containers/poll_form_container';
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
import QuotedStatusContainer from '../containers/quoted_status_container';
import ReplyIndicatorContainer from '../containers/reply_indicator_container';
@@ -361,7 +361,7 @@ class ComposeForm extends ImmutablePureComponent {
!condensed &&
}
diff --git a/app/soapbox/features/compose/components/poll-form.tsx b/app/soapbox/features/compose/components/polls/poll-form.tsx
similarity index 76%
rename from app/soapbox/features/compose/components/poll-form.tsx
rename to app/soapbox/features/compose/components/polls/poll-form.tsx
index 06d178518..cfcefc822 100644
--- a/app/soapbox/features/compose/components/poll-form.tsx
+++ b/app/soapbox/features/compose/components/polls/poll-form.tsx
@@ -1,13 +1,12 @@
-'use strict';
-
import React from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
+import { addPollOption, changePollOption, changePollSettings, clearComposeSuggestions, fetchComposeSuggestions, removePoll, removePollOption, selectComposeSuggestion } from 'soapbox/actions/compose';
import AutosuggestInput from 'soapbox/components/autosuggest_input';
import { Button, Divider, HStack, Stack, Text, Toggle } from 'soapbox/components/ui';
-import { useAppSelector } from 'soapbox/hooks';
+import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
-import DurationSelector from './polls/duration-selector';
+import DurationSelector from './duration-selector';
import type { AutoSuggestion } from 'soapbox/components/autosuggest_input';
@@ -31,12 +30,8 @@ interface IOption {
maxChars: number
numOptions: number
onChange(index: number, value: string): void
- onClearSuggestions(): void
- onFetchSuggestions(token: string): void
onRemove(index: number): void
onRemovePoll(): void
- onSuggestionSelected(tokenStart: number, token: string, value: string, key: (string | number)[]): void
- suggestions?: any // list
title: string
}
@@ -46,16 +41,16 @@ const Option = (props: IOption) => {
maxChars,
numOptions,
onChange,
- onClearSuggestions,
- onFetchSuggestions,
onRemove,
onRemovePoll,
- suggestions,
title,
} = props;
+ const dispatch = useAppDispatch();
const intl = useIntl();
+ const suggestions = useAppSelector((state) => state.compose.get('suggestions'));
+
const handleOptionTitleChange = (event: React.ChangeEvent) => onChange(index, event.target.value);
const handleOptionRemove = () => {
@@ -66,13 +61,13 @@ const Option = (props: IOption) => {
}
};
- const onSuggestionsClearRequested = () => onClearSuggestions();
+ const onSuggestionsClearRequested = () => dispatch(clearComposeSuggestions());
- const onSuggestionsFetchRequested = (token: string) => onFetchSuggestions(token);
+ const onSuggestionsFetchRequested = (token: string) => dispatch(fetchComposeSuggestions(token));
const onSuggestionSelected = (tokenStart: number, token: string | null, value: AutoSuggestion) => {
- if (token && typeof value === 'string') {
- props.onSuggestionSelected(tokenStart, token, value, ['poll', 'options', index]);
+ if (token && typeof token === 'string') {
+ dispatch(selectComposeSuggestion(tokenStart, token, value, ['poll', 'options', index]));
}
};
@@ -94,7 +89,7 @@ const Option = (props: IOption) => {
onSuggestionsClearRequested={onSuggestionsClearRequested}
onSuggestionSelected={onSuggestionSelected}
searchTokens={[':']}
- autoFocus={index === 0}
+ autoFocus={index === 0 || index >= 2}
/>
@@ -107,42 +102,26 @@ const Option = (props: IOption) => {
);
};
-interface IPollForm {
- expiresIn?: number
- isMultiple?: boolean
- onAddOption(value: string): void
- onChangeOption(): void
- onChangeSettings(value: string | number | undefined, isMultiple?: boolean): void
- onClearSuggestions(): void
- onFetchSuggestions(token: string): void
- onRemoveOption(): void
- onRemovePoll(): void
- onSuggestionSelected(tokenStart: number, token: string, value: string, key: (string | number)[]): void
- options?: any
- suggestions?: any // list
-}
-
-const PollForm = (props: IPollForm) => {
- const {
- expiresIn,
- isMultiple,
- onAddOption,
- onChangeOption,
- onChangeSettings,
- onRemoveOption,
- options,
- ...filteredProps
- } = props;
-
+const PollForm = () => {
+ const dispatch = useAppDispatch();
const intl = useIntl();
const pollLimits = useAppSelector((state) => state.instance.getIn(['configuration', 'polls']) as any);
+ const options = useAppSelector((state) => state.compose.getIn(['poll', 'options']));
+ const expiresIn = useAppSelector((state) => state.compose.getIn(['poll', 'expires_in']));
+ const isMultiple = useAppSelector((state) => state.compose.getIn(['poll', 'multiple']));
+
const maxOptions = pollLimits.get('max_options');
const maxOptionChars = pollLimits.get('max_characters_per_option');
- const handleAddOption = () => onAddOption('');
+ const onRemoveOption = (index: number) => dispatch(removePollOption(index));
+ const onChangeOption = (index: number, title: string) => dispatch(changePollOption(index, title));
+ const handleAddOption = () => dispatch(addPollOption(''));
+ const onChangeSettings = (expiresIn: string | number | undefined, isMultiple?: boolean) =>
+ dispatch(changePollSettings(expiresIn, isMultiple));
const handleSelectDuration = (value: number) => onChangeSettings(value, isMultiple);
const handleToggleMultiple = () => onChangeSettings(expiresIn, !isMultiple);
+ const onRemovePoll = () => dispatch(removePoll());
if (!options) {
return null;
@@ -160,7 +139,7 @@ const PollForm = (props: IPollForm) => {
onRemove={onRemoveOption}
maxChars={maxOptionChars}
numOptions={options.size}
- {...filteredProps}
+ onRemovePoll={onRemovePoll}
/>
))}
@@ -211,7 +190,7 @@ const PollForm = (props: IPollForm) => {
{/* Remove Poll */}
-
diff --git a/app/soapbox/features/compose/containers/poll_form_container.js b/app/soapbox/features/compose/containers/poll_form_container.js
deleted file mode 100644
index 04c3b61ee..000000000
--- a/app/soapbox/features/compose/containers/poll_form_container.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import { connect } from 'react-redux';
-
-import {
- addPollOption,
- removePollOption,
- changePollOption,
- changePollSettings,
- removePoll,
- clearComposeSuggestions,
- fetchComposeSuggestions,
- selectComposeSuggestion,
-} from '../../../actions/compose';
-import PollForm from '../components/poll-form';
-
-const mapStateToProps = state => ({
- suggestions: state.getIn(['compose', 'suggestions']),
- options: state.getIn(['compose', 'poll', 'options']),
- expiresIn: state.getIn(['compose', 'poll', 'expires_in']),
- isMultiple: state.getIn(['compose', 'poll', 'multiple']),
-});
-
-const mapDispatchToProps = dispatch => ({
- onAddOption(title) {
- dispatch(addPollOption(title));
- },
-
- onRemoveOption(index) {
- dispatch(removePollOption(index));
- },
-
- onChangeOption(index, title) {
- dispatch(changePollOption(index, title));
- },
-
- onChangeSettings(expiresIn, isMultiple) {
- dispatch(changePollSettings(expiresIn, isMultiple));
- },
-
- onClearSuggestions() {
- dispatch(clearComposeSuggestions());
- },
-
- onFetchSuggestions(token) {
- dispatch(fetchComposeSuggestions(token));
- },
-
- onSuggestionSelected(position, token, accountId, path) {
- dispatch(selectComposeSuggestion(position, token, accountId, path));
- },
-
- onRemovePoll() {
- dispatch(removePoll());
- },
-
-});
-
-export default connect(mapStateToProps, mapDispatchToProps)(PollForm);
diff --git a/app/soapbox/features/verification/steps/sms-verification.tsx b/app/soapbox/features/verification/steps/sms-verification.tsx
index 6e26050dd..a4017f061 100644
--- a/app/soapbox/features/verification/steps/sms-verification.tsx
+++ b/app/soapbox/features/verification/steps/sms-verification.tsx
@@ -1,3 +1,4 @@
+import { AxiosError } from 'axios';
import React from 'react';
import { useIntl } from 'react-intl';
import OtpInput from 'react-otp-input';
@@ -61,15 +62,13 @@ const SmsVerification = () => {
),
);
setStatus(Statuses.REQUESTED);
- }).catch(() => {
- dispatch(
- snackbar.error(
- intl.formatMessage({
- id: 'sms_verification.fail',
- defaultMessage: 'Failed to send SMS message to your phone number.',
- }),
- ),
- );
+ }).catch((error: AxiosError) => {
+ const message = (error.response?.data as any)?.message || intl.formatMessage({
+ id: 'sms_verification.fail',
+ defaultMessage: 'Failed to send SMS message to your phone number.',
+ });
+
+ dispatch(snackbar.error(message));
setStatus(Statuses.FAIL);
});
}, [phone, isValid]);
diff --git a/jest.config.js b/jest.config.js
index 8796e5595..e86307f93 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -28,6 +28,7 @@ module.exports = {
],
'coverageDirectory': '/.coverage/',
'coverageReporters': ['html', 'text', 'text-summary', 'cobertura'],
+ 'reporters': ['default', 'jest-junit'],
'moduleDirectories': [
'/node_modules',
'/app',
diff --git a/package.json b/package.json
index dbced84ea..d5dc14f33 100644
--- a/package.json
+++ b/package.json
@@ -22,7 +22,7 @@
"jsdoc": "npx jsdoc -c jsdoc.conf.js",
"manage:translations": "node ./webpack/translationRunner.js",
"test": "npx cross-env NODE_ENV=test npx jest",
- "test:coverage": "npx jest --coverage",
+ "test:coverage": "${npm_execpath} run test --coverage",
"test:all": "${npm_execpath} run test:coverage && ${npm_execpath} run lint",
"lint": "${npm_execpath} run lint:js && ${npm_execpath} run lint:sass",
"lint:js": "npx eslint --ext .js,.jsx,.ts,.tsx . --cache",
@@ -219,6 +219,7 @@
"fake-indexeddb": "^3.1.7",
"husky": "^7.0.2",
"jest": "^27.5.1",
+ "jest-junit": "^13.2.0",
"lint-staged": ">=10",
"raf": "^3.4.1",
"react-intl-translations-manager": "^5.0.3",
diff --git a/yarn.lock b/yarn.lock
index fbdd06355..99973c815 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6778,6 +6778,16 @@ jest-jasmine2@^27.5.1:
pretty-format "^27.5.1"
throat "^6.0.1"
+jest-junit@^13.2.0:
+ version "13.2.0"
+ resolved "https://registry.yarnpkg.com/jest-junit/-/jest-junit-13.2.0.tgz#66eeb86429aafac8c1745a70f44ace185aacb943"
+ integrity sha512-B0XNlotl1rdsvFZkFfoa19mc634+rrd8E4Sskb92Bb8MmSXeWV9XJGUyctunZS1W410uAxcyYuPUGVnbcOH8cg==
+ dependencies:
+ mkdirp "^1.0.4"
+ strip-ansi "^6.0.1"
+ uuid "^8.3.2"
+ xml "^1.0.1"
+
jest-leak-detector@^27.5.1:
version "27.5.1"
resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz#6ec9d54c3579dd6e3e66d70e3498adf80fde3fb8"
@@ -11312,6 +11322,11 @@ xml-name-validator@^3.0.0:
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
+xml@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5"
+ integrity sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==
+
xmlchars@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"