diff --git a/app/javascript/.eslintrc.js b/app/javascript/.eslintrc.js
index 46a7783c9..6eacb10ca 100644
--- a/app/javascript/.eslintrc.js
+++ b/app/javascript/.eslintrc.js
@@ -53,6 +53,14 @@ module.exports = {
],
'react/jsx-no-target-blank': [2, { enforceDynamicLinks: 'always' }],
},
+ overrides: [
+ {
+ files: ['**/index.js'], // Or *.test.js
+ rules: {
+ 'import/export': 'off',
+ },
+ },
+ ],
globals: {
getCsrfToken: false,
sendFetch: false,
diff --git a/app/javascript/article-form/articleForm.jsx b/app/javascript/article-form/articleForm.jsx
index 321c35b26..c8708f2d9 100644
--- a/app/javascript/article-form/articleForm.jsx
+++ b/app/javascript/article-form/articleForm.jsx
@@ -61,6 +61,25 @@ export default class ArticleForm extends Component {
this.article = JSON.parse(article);
organizations = organizations ? JSON.parse(organizations) : null;
this.url = window.location.href;
+
+ const previousContent =
+ JSON.parse(
+ localStorage.getItem(`editor-${version}-${window.location.href}`),
+ ) || {};
+ const isLocalstorageNewer =
+ new Date(previousContent.updatedAt) > new Date(this.article.updated_at);
+
+ const previousContentState =
+ previousContent && isLocalstorageNewer
+ ? {
+ title: previousContent.title || '',
+ tagList: previousContent.tagList || '',
+ mainImage: previousContent.mainImage || null,
+ bodyMarkdown: previousContent.bodyMarkdown || '',
+ edited: true,
+ }
+ : {};
+
this.state = {
id: this.article.id || null, // eslint-disable-line react/no-unused-state
title: this.article.title || '',
@@ -85,33 +104,21 @@ export default class ArticleForm extends Component {
logoSvg,
helpFor: null,
helpPosition: null,
+ ...previousContentState,
};
}
componentDidMount() {
- const { version, updatedAt } = this.state;
- const previousContent =
- JSON.parse(
- localStorage.getItem(`editor-${version}-${window.location.href}`),
- ) || {};
- const isLocalstorageNewer =
- new Date(previousContent.updatedAt) > new Date(updatedAt);
-
- if (previousContent && isLocalstorageNewer) {
- this.setState({
- title: previousContent.title || '',
- tagList: previousContent.tagList || '',
- mainImage: previousContent.mainImage || null,
- bodyMarkdown: previousContent.bodyMarkdown || '',
- edited: true,
- });
- }
-
window.addEventListener('beforeunload', this.localStoreContent);
}
+ componentWillUnmount() {
+ window.removeEventListener('beforeunload', this.localStoreContent);
+ }
+
componentDidUpdate() {
const { previewResponse } = this.state;
+
if (previewResponse) {
this.constructor.handleGistPreview();
this.constructor.handleRunkitPreview();
diff --git a/app/javascript/articles/Feed.jsx b/app/javascript/articles/Feed.jsx
index 2edf20323..95546a98d 100644
--- a/app/javascript/articles/Feed.jsx
+++ b/app/javascript/articles/Feed.jsx
@@ -4,11 +4,16 @@ import PropTypes from 'prop-types';
/* global userData sendHapticMessage showModal buttonFormData renderNewSidebarCount */
export class Feed extends Component {
- componentDidMount() {
- const { timeFrame } = this.props;
+ constructor(props) {
+ super(props);
+
const { reading_list_ids = [] } = userData(); // eslint-disable-line camelcase
- this.setState({ bookmarkedFeedItems: new Set(reading_list_ids) });
+ this.state = { bookmarkedFeedItems: new Set(reading_list_ids) };
+ }
+
+ componentDidMount() {
+ const { timeFrame } = this.props;
Feed.getFeedItems(timeFrame).then((feedItems) => {
// Ensure first article is one with a main_image
@@ -20,6 +25,7 @@ export class Feed extends Component {
feedItems.splice(index, 1);
const subStories = feedItems;
const organizedFeedItems = [featuredStory, subStories].flat();
+
this.setState({
feedItems: organizedFeedItems,
podcastEpisodes: Feed.getPodcastEpisodes(),
diff --git a/app/javascript/articles/components/SaveButton.jsx b/app/javascript/articles/components/SaveButton.jsx
index d21d22473..5450fae88 100644
--- a/app/javascript/articles/components/SaveButton.jsx
+++ b/app/javascript/articles/components/SaveButton.jsx
@@ -3,9 +3,12 @@ import PropTypes from 'prop-types';
import { articlePropTypes } from '../../common-prop-types';
export class SaveButton extends Component {
- componentDidMount() {
- const { isBookmarked } = this.props;
- this.setState({ buttonText: isBookmarked ? 'Saved' : 'Save' });
+ constructor(props) {
+ super(props);
+
+ const { isBookmarked } = props;
+
+ this.state = { buttonText: isBookmarked ? 'Saved' : 'Save' };
}
render() {
diff --git a/app/javascript/chat/__tests__/util.test.js b/app/javascript/chat/__tests__/util.test.js
index c3645fd33..d3cb76a28 100644
--- a/app/javascript/chat/__tests__/util.test.js
+++ b/app/javascript/chat/__tests__/util.test.js
@@ -30,7 +30,7 @@ describe('Chat utilities', () => {
);
});
- test('should resolve if user and csrf token found.', () => {
+ test('should resolve if user and csrf token found.', async () => {
const csrfToken = 'some-csrf-token';
const currentUser = {
id: 41,
@@ -49,7 +49,7 @@ describe('Chat utilities', () => {
document.head.innerHTML = ``;
document.body.setAttribute('data-user', JSON.stringify(currentUser));
- expect(getUserDataAndCsrfToken(document)).resolves.toEqual({
+ expect(await getUserDataAndCsrfToken(document)).toEqual({
currentUser,
csrfToken,
});
diff --git a/app/javascript/chat/content.jsx b/app/javascript/chat/content.jsx
index 4f84dfbfc..077dffeec 100644
--- a/app/javascript/chat/content.jsx
+++ b/app/javascript/chat/content.jsx
@@ -72,9 +72,10 @@ export default class Content extends Component {
};
return (
+ // TODO: A button (role="button") cannot contain other interactive elements, i.e. buttons.
+ // TODO: These should have key click events as well.
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
{
+ const divsToHide = contentUserElements.filter((div) => {
const { contentUserId } = div.dataset;
return blockedUserIds.includes(parseInt(contentUserId, 10));
});
- divsToHide.forEach(div => {
+ divsToHide.forEach((div) => {
if (div.className.includes('single-article')) {
div.style.display = 'none';
} else if (div.className.includes('single-comment-node')) {
diff --git a/app/javascript/listings/__tests__/utils.test.js b/app/javascript/listings/__tests__/utils.test.js
index 9cc0f15a0..16f29d348 100644
--- a/app/javascript/listings/__tests__/utils.test.js
+++ b/app/javascript/listings/__tests__/utils.test.js
@@ -1,71 +1,5 @@
import { updateListings, getQueryParams, getLocation } from '../utils';
-describe('updateListings', () => {
- const firstListing = {
- id: 20,
- category: 'misc',
- location: 'West Refugio',
- bumped_at: '2019-06-11T17:01:25.143Z',
- processed_html:
- '\u003cp\u003eEius et ullam. Dolores et qui. Quis \u003cstrong\u003equi\u003c/strong\u003e omnis.\u003c/p\u003e\n',
- slug: 'illo-iure-quos-htyashsayas-5hk7',
- title: 'Mentor wanted',
- tags: ['go', 'git'],
- user_id: 1,
- author: {
- name: 'Mrs. Yoko Christiansen',
- username: 'mrschristiansenyoko',
- profile_image_90:
- '/uploads/user/profile_image/7/4b1c980a-beb0-4a5f-b3f2-acc91adc503c.png',
- },
- };
-
- const secondListing = {
- id: 21,
- category: 'misc',
- location: 'West Refugio',
- bumped_at: '2019-06-11T17:01:25.143Z',
- processed_html:
- '\u003cp\u003eEius et ullam. Dolores et qui. Quis \u003cstrong\u003equi\u003c/strong\u003e omnis.\u003c/p\u003e\n',
- slug: 'illo-iure-quos-ereerr-5hk7',
- title: 'Second tag.',
- tags: ['functional', 'clojure'],
- user_id: 1,
- author: {
- name: 'Mrs. Ashahir',
- username: 'mrschristiansenyoko',
- profile_image_90:
- '/uploads/user/profile_image/7/4b1c980a-beb0-4a5f-b3f2-acc91adc503c.png',
- },
- };
-
- const thirdListing = {
- id: 22,
- category: 'misc',
- location: 'West Refugio',
- processed_html:
- '\u003cp\u003eEius et ullam. Dolores et qui. Quis \u003cstrong\u003equi\u003c/strong\u003e omnis.\u003c/p\u003e\n',
- slug: 'illo-iure-fss-ssasas-5hk7',
- title: 'Illo iure quos perspiciatis.',
- tags: ['twitter', 'learning'],
- user_id: 1,
- author: {
- name: 'Mrs. John Mack',
- username: 'mrschristiansenyoko',
- profile_image_90:
- '/uploads/user/profile_image/7/4b1c980a-beb0-4a5f-b3f2-acc91adc503c.png',
- },
- };
-
- const classifiedListings = [firstListing, secondListing, thirdListing];
- test('Should update the listings', () => {
- const result = updateListings(classifiedListings);
- const expectedResult = [firstListing, secondListing];
-
- expect(result).toEqual(expectedResult);
- });
-});
-
describe('updateListings', () => {
beforeEach(() => {
window.history.pushState(
@@ -75,7 +9,72 @@ describe('updateListings', () => {
);
});
- test('Should get the query params', () => {
+ it('Should update the listings', () => {
+ const firstListing = {
+ id: 20,
+ category: 'misc',
+ location: 'West Refugio',
+ bumped_at: '2019-06-11T17:01:25.143Z',
+ processed_html:
+ '\u003cp\u003eEius et ullam. Dolores et qui. Quis \u003cstrong\u003equi\u003c/strong\u003e omnis.\u003c/p\u003e\n',
+ slug: 'illo-iure-quos-htyashsayas-5hk7',
+ title: 'Mentor wanted',
+ tags: ['go', 'git'],
+ user_id: 1,
+ author: {
+ name: 'Mrs. Yoko Christiansen',
+ username: 'mrschristiansenyoko',
+ profile_image_90:
+ '/uploads/user/profile_image/7/4b1c980a-beb0-4a5f-b3f2-acc91adc503c.png',
+ },
+ };
+
+ const secondListing = {
+ id: 21,
+ category: 'misc',
+ location: 'West Refugio',
+ bumped_at: '2019-06-11T17:01:25.143Z',
+ processed_html:
+ '\u003cp\u003eEius et ullam. Dolores et qui. Quis \u003cstrong\u003equi\u003c/strong\u003e omnis.\u003c/p\u003e\n',
+ slug: 'illo-iure-quos-ereerr-5hk7',
+ title: 'Second tag.',
+ tags: ['functional', 'clojure'],
+ user_id: 1,
+ author: {
+ name: 'Mrs. Ashahir',
+ username: 'mrschristiansenyoko',
+ profile_image_90:
+ '/uploads/user/profile_image/7/4b1c980a-beb0-4a5f-b3f2-acc91adc503c.png',
+ },
+ };
+
+ const thirdListing = {
+ id: 22,
+ category: 'misc',
+ location: 'West Refugio',
+ processed_html:
+ '\u003cp\u003eEius et ullam. Dolores et qui. Quis \u003cstrong\u003equi\u003c/strong\u003e omnis.\u003c/p\u003e\n',
+ slug: 'illo-iure-fss-ssasas-5hk7',
+ title: 'Illo iure quos perspiciatis.',
+ tags: ['twitter', 'learning'],
+ user_id: 1,
+ author: {
+ name: 'Mrs. John Mack',
+ username: 'mrschristiansenyoko',
+ profile_image_90:
+ '/uploads/user/profile_image/7/4b1c980a-beb0-4a5f-b3f2-acc91adc503c.png',
+ },
+ };
+
+ const classifiedListings = [firstListing, secondListing, thirdListing];
+
+ const result = updateListings(classifiedListings);
+ const expectedResult = [firstListing, secondListing];
+
+ expect(result).toEqual(expectedResult);
+ });
+
+ it('Should get the query params', () => {
const result = getQueryParams();
const expectedResult = {
crcat: 'test',
@@ -93,7 +92,7 @@ describe('getLocation', () => {
let category;
let query;
- test('Should return a location with slug and category', () => {
+ it('Should return a location with slug and category', () => {
slug = 'slug';
tags = ['clojure', 'functional'];
category = 'clojure';
@@ -105,7 +104,7 @@ describe('getLocation', () => {
expect(result).toEqual(expectedResult);
});
- test('Should return a location with category, query and tags', () => {
+ it('Should return a location with category, query and tags', () => {
slug = undefined;
tags = ['clojure', 'functional'];
category = 'clojure';
@@ -117,7 +116,7 @@ describe('getLocation', () => {
expect(result).toEqual(expectedResult);
});
- test('Should return a location with category and query', () => {
+ it('Should return a location with category and query', () => {
slug = undefined;
tags = [];
category = 'clojure';
@@ -129,7 +128,7 @@ describe('getLocation', () => {
expect(result).toEqual(expectedResult);
});
- test('Should return a location with category and tags', () => {
+ it('Should return a location with category and tags', () => {
slug = undefined;
tags = ['clojure', 'functional'];
category = 'clojure';
@@ -141,7 +140,7 @@ describe('getLocation', () => {
expect(result).toEqual(expectedResult);
});
- test('Should return a location with category', () => {
+ it('Should return a location with category', () => {
slug = undefined;
tags = [];
category = 'clojure';
@@ -153,7 +152,7 @@ describe('getLocation', () => {
expect(result).toEqual(expectedResult);
});
- test('Should return just the URL base', () => {
+ it('Should return just the URL base', () => {
slug = undefined;
tags = [];
category = '';
diff --git a/app/javascript/shared/components/tags.jsx b/app/javascript/shared/components/tags.jsx
index fbde40f52..74fc6b65d 100644
--- a/app/javascript/shared/components/tags.jsx
+++ b/app/javascript/shared/components/tags.jsx
@@ -29,6 +29,43 @@ const LETTERS_NUMBERS = /[a-z0-9]/i;
class Tags extends Component {
constructor(props) {
super(props);
+ const { listing } = props;
+
+ const listingState = listing
+ ? {
+ additionalTags: {
+ jobs: [
+ 'remote',
+ 'remoteoptional',
+ 'lgbtbenefits',
+ 'greencard',
+ 'senior',
+ 'junior',
+ 'intermediate',
+ '401k',
+ 'fulltime',
+ 'contract',
+ 'temp',
+ ],
+ forhire: [
+ 'remote',
+ 'remoteoptional',
+ 'lgbtbenefits',
+ 'greencard',
+ 'senior',
+ 'junior',
+ 'intermediate',
+ '401k',
+ 'fulltime',
+ 'contract',
+ 'temp',
+ ],
+ forsale: ['laptop', 'desktopcomputer', 'new', 'used'],
+ events: ['conference', 'meetup'],
+ collabs: ['paid', 'temp'],
+ },
+ }
+ : null;
this.state = {
selectedIndex: -1,
@@ -37,48 +74,10 @@ class Tags extends Component {
cursorIdx: 0,
prevLen: 0,
showingRulesForTag: null,
+ ...listingState,
};
}
- componentDidMount() {
- const { listing } = this.props;
- if (listing === true) {
- this.setState({
- additionalTags: {
- jobs: [
- 'remote',
- 'remoteoptional',
- 'lgbtbenefits',
- 'greencard',
- 'senior',
- 'junior',
- 'intermediate',
- '401k',
- 'fulltime',
- 'contract',
- 'temp',
- ],
- forhire: [
- 'remote',
- 'remoteoptional',
- 'lgbtbenefits',
- 'greencard',
- 'senior',
- 'junior',
- 'intermediate',
- '401k',
- 'fulltime',
- 'contract',
- 'temp',
- ],
- forsale: ['laptop', 'desktopcomputer', 'new', 'used'],
- events: ['conference', 'meetup'],
- collabs: ['paid', 'temp'],
- },
- });
- }
- }
-
componentDidUpdate() {
// stop cursor jumping if the user goes back to edit previous tags
const { cursorIdx, prevLen } = this.state;
diff --git a/app/javascript/utilities/__tests__/search.test.js b/app/javascript/utilities/__tests__/search.test.js
index 4585fc8bd..827477098 100644
--- a/app/javascript/utilities/__tests__/search.test.js
+++ b/app/javascript/utilities/__tests__/search.test.js
@@ -25,7 +25,7 @@ describe('Search utilities', () => {
describe('getInitialSearchTerm', () => {
describe(`When the querystring key 'q' has a value`, () => {
- test(`should return the querystring key q's value`, () => {
+ it(`should return the querystring key q's value`, () => {
const expected = 'hello';
const querystring = `?q=${expected}`;
const actual = getInitialSearchTerm(querystring);
@@ -34,7 +34,7 @@ describe('Search utilities', () => {
});
describe(`When the querystring key 'q' has a + character representing a space`, () => {
- test(`should return the querystring key q's decoded value with + characters replaced by a space`, () => {
+ it(`should return the querystring key q's decoded value with + characters replaced by a space`, () => {
const expected = `my visual studio setup`;
const querystring = `?q=my+visual+studio+setup`;
const actual = getInitialSearchTerm(querystring);
@@ -43,7 +43,7 @@ describe('Search utilities', () => {
});
describe(`When the querystring key 'q' has an encoded value with markup`, () => {
- test(`should return the querystring key q's decoded value`, () => {
+ it(`should return the querystring key q's decoded value`, () => {
const expected = ``;
const querystring = `?q=`;
const actual = getInitialSearchTerm(querystring);
@@ -52,7 +52,7 @@ describe('Search utilities', () => {
});
describe(`When the querystring key 'q' has no value`, () => {
- test(`should return an empty string`, () => {
+ it(`should return an empty string`, () => {
const expected = '';
const querystring = `?q=`;
const actual = getInitialSearchTerm(querystring);
@@ -61,7 +61,7 @@ describe('Search utilities', () => {
});
describe(`When the querystring key 'q' does not exist`, () => {
- test(`should return an empty string`, () => {
+ it(`should return an empty string`, () => {
const expected = '';
const querystring = '?';
const actual = getInitialSearchTerm(querystring);
@@ -83,7 +83,7 @@ describe('Search utilities', () => {
delete global.InstantClick;
});
- test('should call InstantClick.preLoad', () => {
+ it('should call InstantClick.preLoad', () => {
const location = {
origin: 'http://localhost',
href: 'http://localhost',
@@ -97,7 +97,7 @@ describe('Search utilities', () => {
});
describe('When a search term is passed in', () => {
- test('should call InstantClick.preLoad with the search term value in the URL', () => {
+ it('should call InstantClick.preLoad with the search term value in the URL', () => {
const location = {
origin: 'http://localhost',
href: 'http://localhost',
@@ -110,7 +110,7 @@ describe('Search utilities', () => {
expect(InstantClick.preload).toBeCalledWith(expected);
});
- test('should call InstantClick.preLoad with the encoded value for the search term', () => {
+ it('should call InstantClick.preLoad with the encoded value for the search term', () => {
const location = {
origin: 'http://localhost',
href: 'http://localhost',
@@ -124,7 +124,7 @@ describe('Search utilities', () => {
expect(InstantClick.preload).toBeCalledWith(expected);
});
- test('should call InstantClick.preLoad if the search term is empty', () => {
+ it('should call InstantClick.preLoad if the search term is empty', () => {
const location = {
origin: 'http://localhost',
href: 'http://localhost',
@@ -140,7 +140,7 @@ describe('Search utilities', () => {
describe('hasInstantClick', () => {
describe('When instant click exists', () => {
- test('should return true', () => {
+ it('should return true', () => {
global.instantClick = {};
expect(hasInstantClick()).toEqual(true);
delete global.instantClick;
@@ -148,7 +148,7 @@ describe('Search utilities', () => {
});
describe('When instant click does not exist', () => {
- test('should return false', () => {
+ it('should return false', () => {
expect(hasInstantClick()).toEqual(false);
});
});
@@ -167,19 +167,19 @@ describe('Search utilities', () => {
delete global.InstantClick;
});
- test('should call InstantClick.display if search term is empty', () => {
+ it('should call InstantClick.display if search term is empty', () => {
displaySearchResults({ searchTerm: '', location: { href: '' } });
expect(InstantClick.display).toHaveBeenCalledTimes(1);
});
- test('should call InstantClick.display once', () => {
+ it('should call InstantClick.display once', () => {
displaySearchResults({ searchTerm: 'hello', location: { href: '' } });
expect(InstantClick.display).toHaveBeenCalledTimes(1);
});
- test('should call InstantClick.display with the correct search URL', () => {
+ it('should call InstantClick.display with the correct search URL', () => {
const searchTerm = 'hello';
const location = {
origin: 'http://localhost',
@@ -192,7 +192,7 @@ describe('Search utilities', () => {
);
});
- test('should call InstantClick.display with an encoded search term', () => {
+ it('should call InstantClick.display with an encoded search term', () => {
const searchTerm = '#hello%';
const sanitizedSearchTerm = '%23hello%25';
const location = {
@@ -206,7 +206,7 @@ describe('Search utilities', () => {
);
});
- test('should call InstantClick.display with filters, if present', () => {
+ it('should call InstantClick.display with filters, if present', () => {
const searchTerm = '#hello%';
const sanitizedSearchTerm = '%23hello%25';
const filterParameters = 'class_name:Article';
@@ -222,7 +222,7 @@ describe('Search utilities', () => {
);
});
- test('should not call InstantClick.display with filters, if filter querystring key is present, but has no value', () => {
+ it('should not call InstantClick.display with filters, if filter querystring key is present, but has no value', () => {
const searchTerm = '#hello%';
const sanitizedSearchTerm = '%23hello%25';
const location = {
@@ -239,32 +239,27 @@ describe('Search utilities', () => {
});
describe('fetchSearch', () => {
- let responsePromise;
- let dataHash;
-
- beforeEach(() => {
- fetch.resetMocks();
- fetch.once({});
- dataHash = { name: 'jav' };
- responsePromise = fetchSearch('tags', dataHash);
+ it('should return a Promise', () => {
+ expect(fetchSearch('tags', { name: 'jav' })).toBeInstanceOf(Promise);
});
- test('should return a Promise', () => {
- expect(responsePromise).toBeInstanceOf(Promise);
- });
+ it('should return response formatted as JSON', async () => {
+ const expected = { results: [] };
- test('should return response formatted as JSON', () => {
- responsePromise.then((response) => {
- expect(response).toBeInstanceOf(Object);
- expect(response).toMatchObject({ results: expect.any(Array) });
- });
+ fetch.mockResponse(JSON.stringify({ results: [] }));
+
+ const response = await fetchSearch('tags', { name: 'jav' });
+
+ expect(response).toBeInstanceOf(Object);
+ expect(response).toMatchObject(expected);
});
});
describe('createSearchUrl', () => {
- test('should return a url string', () => {
+ it('should return a url string', () => {
const dataHash = { name: 'jav', tags: ['one', 'two'] };
const responseString = createSearchUrl(dataHash);
+
expect(responseString).toEqual('name=jav&tags%5B%5D=one&tags%5B%5D=two');
});
});