diff --git a/app/javascript/readingList/__tests__/readingList.test.jsx b/app/javascript/readingList/__tests__/readingList.test.jsx index e12997461..d25cd193f 100644 --- a/app/javascript/readingList/__tests__/readingList.test.jsx +++ b/app/javascript/readingList/__tests__/readingList.test.jsx @@ -5,6 +5,17 @@ import fetch from 'jest-fetch-mock'; import { ReadingList } from '../readingList'; describe('', () => { + beforeAll(() => { + global.window.matchMedia = jest.fn((query) => { + return { + matches: false, + media: query, + addListener: jest.fn(), + removeListener: jest.fn(), + }; + }); + }); + const getMockResponse = () => JSON.stringify({ result: [ @@ -62,16 +73,4 @@ describe('', () => { expect(results).toHaveNoViolations(); }); - - it('renders all the elements', () => { - const { queryByPlaceholderText, queryByText } = render( - , - ); - - expect(queryByPlaceholderText('search your list')).toBeDefined(); - expect(queryByText('#discuss')).toBeDefined(); - expect(queryByText('View Archive')).toBeDefined(); - expect(queryByText('Your Archive List is Lonely')).toBeDefined(); - expect(queryByText('Reading List (empty)')).toBeDefined(); - }); }); diff --git a/app/javascript/readingList/components/ItemListLoadMoreButton.jsx b/app/javascript/readingList/components/ItemListLoadMoreButton.jsx deleted file mode 100644 index ded90427e..000000000 --- a/app/javascript/readingList/components/ItemListLoadMoreButton.jsx +++ /dev/null @@ -1,23 +0,0 @@ -// Load more button for item list -import { h } from 'preact'; -import PropTypes from 'prop-types'; -import { Button } from '@crayons'; - -export const ItemListLoadMoreButton = ({ show, onClick }) => { - if (!show) { - return ''; - } - - return ( -
- -
- ); -}; - -ItemListLoadMoreButton.propTypes = { - show: PropTypes.bool.isRequired, - onClick: PropTypes.func.isRequired, -}; diff --git a/app/javascript/readingList/components/ItemListTags.jsx b/app/javascript/readingList/components/ItemListTags.jsx deleted file mode 100644 index ca65e4ab6..000000000 --- a/app/javascript/readingList/components/ItemListTags.jsx +++ /dev/null @@ -1,29 +0,0 @@ -// Sidebar tags for item list page -import { h } from 'preact'; -import PropTypes from 'prop-types'; - -export const ItemListTags = ({ availableTags, selectedTags, onClick }) => { - const tagsHTML = availableTags.map((tag) => ( - -1 ? 'crayons-link--current' : '' - }`} - href={`/t/${tag}`} - data-no-instant - onClick={(e) => onClick(e, tag)} - > - {`#${tag}`} - - )); - return ( - - ); -}; - -ItemListTags.propTypes = { - availableTags: PropTypes.arrayOf(PropTypes.string).isRequired, - selectedTags: PropTypes.arrayOf(PropTypes.string).isRequired, - onClick: PropTypes.func.isRequired, -}; diff --git a/app/javascript/readingList/components/TagList.jsx b/app/javascript/readingList/components/TagList.jsx new file mode 100644 index 000000000..245ed61c8 --- /dev/null +++ b/app/javascript/readingList/components/TagList.jsx @@ -0,0 +1,88 @@ +import { h } from 'preact'; +import PropTypes from 'prop-types'; + +function LargeScreenTagList({ availableTags, selectedTag, onSelectTag }) { + return ( + + ); +} + +/** + * + * @param {object} props + * @param {Array} props.availableTags A list of available tags. + * @param {string} [props.selectedTag=''] The currently selected tag. + * @param {function} props.onSelectTag A handler for when the selected tag changes. + * @param {boolean} [props.isMobile=false] Whether or not we're displaying the mobile view. + */ +export function TagList({ + availableTags, + selectedTag = '', + onSelectTag, + isMobile = false, +}) { + return isMobile ? ( + + ) : ( + + ); +} + +TagList.propTypes = { + isMobile: PropTypes.boolean, + availableTags: PropTypes.arrayOf(PropTypes.string).isRequired, + selectedTag: PropTypes.string, + onSelectTag: PropTypes.func.isRequired, +}; diff --git a/app/javascript/readingList/components/__tests__/ItemListLoadMoreButton.test.jsx b/app/javascript/readingList/components/__tests__/ItemListLoadMoreButton.test.jsx deleted file mode 100644 index c38a0262a..000000000 --- a/app/javascript/readingList/components/__tests__/ItemListLoadMoreButton.test.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import { h } from 'preact'; -import { render } from '@testing-library/preact'; -import { axe } from 'jest-axe'; -import { ItemListLoadMoreButton } from '../ItemListLoadMoreButton'; - -describe('', () => { - it('should have no a11y violations when the load more button is not shown', async () => { - const { container } = render(); - const results = await axe(container); - - expect(results).toHaveNoViolations(); - }); - - it('should have no a11y violations when the load more button is shown', async () => { - const { container } = render(); - const results = await axe(container); - - expect(results).toHaveNoViolations(); - }); - - it('renders nothing when not required', () => { - const { queryByText } = render(); - expect(queryByText(/load more/i)).toBeNull(); - }); - - it('renders a button when required', () => { - const { queryByText } = render(); - - expect(queryByText(/load more/i)).toBeDefined(); - }); -}); diff --git a/app/javascript/readingList/components/__tests__/ItemListTags.test.jsx b/app/javascript/readingList/components/__tests__/ItemListTags.test.jsx deleted file mode 100644 index 3a0f3e5ce..000000000 --- a/app/javascript/readingList/components/__tests__/ItemListTags.test.jsx +++ /dev/null @@ -1,70 +0,0 @@ -import { h } from 'preact'; -import { render } from '@testing-library/preact'; -import { axe } from 'jest-axe'; -import { ItemListTags } from '../ItemListTags'; - -describe('', () => { - it('should have no a11y violations with two different sets of tags', async () => { - const { container } = render( - , - ); - const results = await axe(container); - - expect(results).toHaveNoViolations(); - }); - - it('should have no a11y violations with two some shared tags', async () => { - const { container } = render( - , - ); - const results = await axe(container); - - expect(results).toHaveNoViolations(); - }); - - it('renders properly with two different sets of tags', () => { - const { getByText, queryByText } = render( - , - ); - - getByText('#discuss'); - expect(queryByText('#javascript')).toBeNull(); - }); - - it('renders properly with some shared tags', () => { - const { queryByText } = render( - , - ); - - expect(queryByText('#discuss')).toBeDefined(); - expect(queryByText('#javascript')).toBeDefined(); - }); - - it('triggers the onClick', () => { - const onClick = jest.fn(); - const { getByText } = render( - , - ); - - let tag = getByText('#discuss', { selector: 'a' }); - tag.click(); - - expect(onClick).toHaveBeenCalled(); - }); -}); diff --git a/app/javascript/readingList/readingList.jsx b/app/javascript/readingList/readingList.jsx index 135947f1d..c1823ea9c 100644 --- a/app/javascript/readingList/readingList.jsx +++ b/app/javascript/readingList/readingList.jsx @@ -2,43 +2,61 @@ import { h, Component } from 'preact'; import PropTypes from 'prop-types'; import { - defaultState, loadNextPage, onSearchBoxType, performInitialSearch, search, - toggleTag, + selectTag, clearSelectedTags, } from '../searchableItemList/searchableItemList'; import { ItemListItem } from './components/ItemListItem'; import { ItemListItemArchiveButton } from './components/ItemListItemArchiveButton'; -import { ItemListLoadMoreButton } from './components/ItemListLoadMoreButton'; -import { ItemListTags } from './components/ItemListTags'; +import { TagList } from './components/TagList'; +import { MediaQuery } from '@components/MediaQuery'; +import { BREAKPOINTS } from '@components/useMediaQuery'; import { debounceAction } from '@utilities/debounceAction'; import { Button } from '@crayons'; import { request } from '@utilities/http'; +const NO_RESULTS_WITH_FILTER_MESSAGE = 'Nothing with this filter 🤔'; const STATUS_VIEW_VALID = 'valid,confirmed'; const STATUS_VIEW_ARCHIVED = 'archived'; const READING_LIST_ARCHIVE_PATH = '/readinglist/archive'; const READING_LIST_PATH = '/readinglist'; -const FilterText = ({ selectedTags, query, value }) => { - return ( -

- {selectedTags.length === 0 && query.length === 0 - ? value - : 'Nothing with this filter 🤔'} -

- ); -}; +function ItemList({ items, archiveButtonLabel, toggleArchiveStatus }) { + return items.map((item) => { + return ( + + toggleArchiveStatus(e, item)} + /> + + ); + }); +} export class ReadingList extends Component { constructor(props) { super(props); const { statusView } = this.props; - this.state = defaultState({ archiving: false, statusView }); + + this.state = { + archiving: false, + query: '', + index: null, + page: 0, + hitsPerPage: 80, + items: [], + itemsLoaded: false, + itemsTotal: 0, + availableTags: [], + selectedTag: '', + showLoadMoreButton: false, + statusView, + }; // bind and initialize all shared functions this.onSearchBoxType = debounceAction(onSearchBoxType.bind(this), { @@ -47,7 +65,7 @@ export class ReadingList extends Component { this.loadNextPage = loadNextPage.bind(this); this.performInitialSearch = performInitialSearch.bind(this); this.search = search.bind(this); - this.toggleTag = toggleTag.bind(this); + this.selectTag = selectTag.bind(this); this.clearSelectedTags = clearSelectedTags.bind(this); } @@ -62,7 +80,7 @@ export class ReadingList extends Component { toggleStatusView = (event) => { event.preventDefault(); - const { query, selectedTags } = this.state; + const { query, selectedTag } = this.state; const isStatusViewValid = this.statusViewValid(); const newStatusView = isStatusViewValid @@ -73,11 +91,11 @@ export class ReadingList extends Component { : READING_LIST_PATH; // empty items so that changing the view will start from scratch - this.setState({ statusView: newStatusView, items: [] }); + this.setState({ statusView: newStatusView, items: [], selectedTag }); this.search(query, { page: 0, - tags: selectedTags, + tags: selectedTag ? [selectedTag] : [], statusView: newStatusView, }); @@ -113,16 +131,17 @@ export class ReadingList extends Component { } renderEmptyItems() { - const { itemsLoaded, selectedTags, query } = this.state; + const { itemsLoaded, selectedTag = '', query } = this.state; + const showMessage = selectedTag.length === 0 && query.length === 0; if (itemsLoaded && this.statusViewValid()) { return ( -
- +
+

+ {showMessage + ? 'Your reading list is empty' + : NO_RESULTS_WITH_FILTER_MESSAGE} +

Click the{' '} @@ -140,18 +159,16 @@ export class ReadingList extends Component { when viewing a post to add it to your reading list.

-
+ ); } return ( -
- -
+

+ {showMessage + ? 'Your Archive is empty...' + : NO_RESULTS_WITH_FILTER_MESSAGE} +

); } @@ -160,24 +177,14 @@ export class ReadingList extends Component { items = [], itemsTotal, availableTags, - selectedTags, + selectedTag = '', showLoadMoreButton, archiving, + loading = false, } = this.state; const isStatusViewValid = this.statusViewValid(); - const archiveButtonLabel = isStatusViewValid ? 'Archive' : 'Unarchive'; - const itemsToRender = items.map((item) => { - return ( - - this.toggleArchiveStatus(e, item)} - /> - - ); - }); const snackBar = archiving ? (
@@ -187,17 +194,17 @@ export class ReadingList extends Component { '' ); return ( -
-
+
+

{isStatusViewValid ? 'Reading list' : 'Archive'} {` (${itemsTotal})`}

- -
+
+ Filter -
-
- -
- - -
-
- {items.length > 0 ? itemsToRender : this.renderEmptyItems()} -
- - { + return ( + matches && ( + + ) + ); + }} /> -
- - {snackBar} -
-
+ + + { + return ( +
+ {matches && ( +
+ +
+ )} +
+ {items.length > 0 ? ( + + ) : loading ? null : ( + this.renderEmptyItems() + )} + {showLoadMoreButton && ( +
+ +
+ )} +
+
+ ); + }} + /> + {snackBar} + ); } } @@ -247,9 +289,3 @@ ReadingList.propTypes = { availableTags: PropTypes.arrayOf(PropTypes.string).isRequired, statusView: PropTypes.oneOf([STATUS_VIEW_VALID, STATUS_VIEW_ARCHIVED]), }; - -FilterText.propTypes = { - selectedTags: PropTypes.arrayOf(PropTypes.string).isRequired, - value: PropTypes.string.isRequired, - query: PropTypes.arrayOf(PropTypes.string).isRequired, -}; diff --git a/app/javascript/searchableItemList/searchableItemList.js b/app/javascript/searchableItemList/searchableItemList.js index 5c5b9b80b..ea1936c7f 100644 --- a/app/javascript/searchableItemList/searchableItemList.js +++ b/app/javascript/searchableItemList/searchableItemList.js @@ -1,55 +1,34 @@ // Shared behavior between the reading list and history pages import { fetchSearch } from '../utilities/search'; -// Provides the initial state for the component -export function defaultState(options) { - const state = { - query: '', - index: null, - - page: 0, - hitsPerPage: 80, - - items: [], - itemsLoaded: false, - itemsTotal: 0, - - availableTags: [], - selectedTags: [], - - showLoadMoreButton: false, - }; - return Object.assign({}, state, options); -} - // Starts the search when the user types in the search box export function onSearchBoxType(event) { const component = this; const query = event.target.value; - const { selectedTags, statusView } = component.state; + const { selectedTag, statusView } = component.state; component.setState({ page: 0 }); component.search(query, { - tags: selectedTags, + tags: selectedTag ? [selectedTag] : [], statusView, appendItems: false, }); } -export function toggleTag(event, tag) { +export function selectTag(event) { event.preventDefault(); - + const { value, dataset } = event.target; + const selectedTag = value ?? dataset.tag; const component = this; - const { query, selectedTags, statusView } = component.state; - const newTags = selectedTags; - if (newTags.indexOf(tag) === -1) { - newTags.push(tag); - } else { - newTags.splice(newTags.indexOf(tag), 1); - } - component.setState({ selectedTags: newTags, page: 0, items: [] }); - component.search(query, { tags: newTags, statusView, appendItems: false }); + const { query, statusView } = component.state; + + component.setState({ selectedTag, page: 0, items: [] }); + component.search(query, { + tags: selectedTag ? [selectedTag] : [], + statusView, + appendItems: false, + }); } export function clearSelectedTags(event) { @@ -57,9 +36,8 @@ export function clearSelectedTags(event) { const component = this; const { query, statusView } = component.state; - const newTags = []; - component.setState({ selectedTags: newTags, page: 0, items: [] }); - component.search(query, { tags: newTags, statusView, appendItems: false }); + component.setState({ selectedTag: '', page: 0, items: [] }); + component.search(query, { tags: [], statusView, appendItems: false }); } // Perform the initial search @@ -68,6 +46,8 @@ export function performInitialSearch({ searchOptions = {} }) { const { hitsPerPage } = component.state; const dataHash = { page: 0, per_page: hitsPerPage }; + component.setState({ loading: true }); + if (searchOptions.status) { dataHash.status = searchOptions.status.split(','); } @@ -87,6 +67,7 @@ export function performInitialSearch({ searchOptions = {} }) { itemsTotal: response.total, showLoadMoreButton: hitsPerPage < response.total, availableTags, + loading: false, }); }); } @@ -95,6 +76,8 @@ export function performInitialSearch({ searchOptions = {} }) { export function search(query, { page, tags, statusView, appendItems = false }) { const component = this; + component.setState({ loading: true }); + // allow the page number to come from the calling function // we check `undefined` because page can be 0 const newPage = page === undefined ? component.state.page : page; @@ -134,6 +117,7 @@ export function search(query, { page, tags, statusView, appendItems = false }) { items, itemsTotal: response.total, showLoadMoreButton: items.length < response.total, + loading: false, }); }); } @@ -142,11 +126,11 @@ export function search(query, { page, tags, statusView, appendItems = false }) { export function loadNextPage() { const component = this; - const { query, selectedTags, page, statusView } = component.state; + const { query, selectedTag, page, statusView } = component.state; component.setState({ page: page + 1 }); component.search(query, { page: page + 1, - tags: selectedTags, + tags: [selectedTag], statusView, appendItems: true, }); diff --git a/app/javascript/shared/components/useMediaQuery.js b/app/javascript/shared/components/useMediaQuery.js index 89ea02598..093e5d40b 100644 --- a/app/javascript/shared/components/useMediaQuery.js +++ b/app/javascript/shared/components/useMediaQuery.js @@ -6,9 +6,9 @@ import { useState, useEffect } from 'preact/hooks'; * Note: These were copied from _import.scss. */ export const BREAKPOINTS = Object.freeze({ - Small: '640', - Medium: '768', - Large: '1024', + Small: 640, + Medium: 768, + Large: 1024, }); /** diff --git a/cypress/fixtures/search/emptyReadingList.json b/cypress/fixtures/search/emptyReadingList.json new file mode 100644 index 000000000..25a7f79b5 --- /dev/null +++ b/cypress/fixtures/search/emptyReadingList.json @@ -0,0 +1 @@ +{ "result": [], "total": 0 } diff --git a/cypress/fixtures/search/readingList.json b/cypress/fixtures/search/readingList.json new file mode 100644 index 000000000..ee8e52da6 --- /dev/null +++ b/cypress/fixtures/search/readingList.json @@ -0,0 +1,152 @@ +{ + "result": [ + { + "id": 7375469, + "user_id": 9597, + "reactable": { + "reading_time": 3, + "main_image": "https://res.cloudinary.com/practicaldev/image/fetch/s--XE2mPGud--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/9597/68d6245f-3152-4ed2-a245-d015fca4160b.jpeg", + "readable_publish_date_string": "Feb 22", + "cloudinary_video_url": null, + "video_duration_in_minutes": 0, + "title": "Test Article 1", + "video_duration_string": "00:00", + "tags": [ + { + "name": "productivity", + "keywords_for_search": "" + }, + { + "name": "webdev", + "keywords_for_search": "web development" + } + ], + "path": "/somember/web-vitals-explained-114j", + "comments_count": 4, + "public_reactions_count": 94, + "id": 614848, + "published_at": "2021-02-22T17:00:55.966Z", + "class_name": "Article", + "user": { + "name": "Some Member", + "id": 67245, + "pro": null, + "profile_image_90": "https://res.cloudinary.com/practicaldev/image/fetch/s--XE2mPGud--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/9597/68d6245f-3152-4ed2-a245-d015fca4160b.jpeg", + "username": "somember" + }, + "tag_list": ["productivity", "webdev"], + "flare_tag": null, + "user_id": 67245, + "highlight": null, + "readable_publish_date": "Feb 22", + "podcast": { + "slug": null, + "image_url": "https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9iebxyedkwozybopkn6e.jpeg", + "title": "Test Article 1" + }, + "_score": 0, + "published_at_int": 1614013255, + "published_timestamp": "2021-02-22T17:00:55.966Z" + } + }, + { + "id": 7375469, + "user_id": 9597, + "reactable": { + "reading_time": 3, + "main_image": "https://res.cloudinary.com/practicaldev/image/fetch/s--XE2mPGud--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/9597/68d6245f-3152-4ed2-a245-d015fca4160b.jpeg", + "readable_publish_date_string": "Feb 22", + "cloudinary_video_url": null, + "video_duration_in_minutes": 0, + "title": "Test Article 2", + "video_duration_string": "00:00", + "tags": [ + { + "name": "performance", + "keywords_for_search": null + }, + { + "name": "webdev", + "keywords_for_search": "web development" + } + ], + "path": "/somember/web-vitals-explained-114j", + "comments_count": 4, + "public_reactions_count": 94, + "id": 614848, + "published_at": "2021-02-22T17:00:55.966Z", + "class_name": "Article", + "user": { + "name": "Some Member", + "id": 67245, + "pro": null, + "profile_image_90": "https://res.cloudinary.com/practicaldev/image/fetch/s--XE2mPGud--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/9597/68d6245f-3152-4ed2-a245-d015fca4160b.jpeg", + "username": "somember" + }, + "tag_list": ["performance", "webdev"], + "flare_tag": null, + "user_id": 67245, + "highlight": null, + "readable_publish_date": "Feb 22", + "podcast": { + "slug": null, + "image_url": "https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9iebxyedkwozybopkn6e.jpeg", + "title": "Test Article 2" + }, + "_score": 0, + "published_at_int": 1614013255, + "published_timestamp": "2021-02-22T17:00:55.966Z" + } + }, + { + "id": 7375469, + "user_id": 9597, + "reactable": { + "reading_time": 3, + "main_image": "https://res.cloudinary.com/practicaldev/image/fetch/s--XE2mPGud--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/9597/68d6245f-3152-4ed2-a245-d015fca4160b.jpeg", + "readable_publish_date_string": "Feb 22", + "cloudinary_video_url": null, + "video_duration_in_minutes": 0, + "title": "Test Article 3", + "video_duration_string": "00:00", + "tags": [ + { + "name": "productivity", + "keywords_for_search": "" + }, + { + "name": "javascript", + "keywords_for_search": "js" + } + ], + "path": "/somember/web-vitals-explained-114j", + "comments_count": 4, + "public_reactions_count": 94, + "id": 614848, + "published_at": "2021-02-22T17:00:55.966Z", + "class_name": "Article", + "user": { + "name": "Some Member", + "id": 67245, + "pro": null, + "profile_image_90": "https://res.cloudinary.com/practicaldev/image/fetch/s--XE2mPGud--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/9597/68d6245f-3152-4ed2-a245-d015fca4160b.jpeg", + "username": "somember" + }, + "tag_list": ["productivity", "javascript"], + "flare_tag": null, + "user_id": 67245, + "highlight": null, + "readable_publish_date": "Feb 22", + "podcast": { + "slug": null, + "image_url": "https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9iebxyedkwozybopkn6e.jpeg", + "title": "Test Article 3" + }, + "_score": 0, + "published_at_int": 1614013255, + "published_timestamp": "2021-02-22T17:00:55.966Z" + } + } + ], + "total": 3 +} diff --git a/cypress/fixtures/search/readingListFilterByTagProductivity.json b/cypress/fixtures/search/readingListFilterByTagProductivity.json new file mode 100644 index 000000000..3d6cf8ce9 --- /dev/null +++ b/cypress/fixtures/search/readingListFilterByTagProductivity.json @@ -0,0 +1,103 @@ +{ + "result": [ + { + "id": 7375469, + "user_id": 9597, + "reactable": { + "reading_time": 3, + "main_image": "https://res.cloudinary.com/practicaldev/image/fetch/s--XE2mPGud--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/9597/68d6245f-3152-4ed2-a245-d015fca4160b.jpeg", + "readable_publish_date_string": "Feb 22", + "cloudinary_video_url": null, + "video_duration_in_minutes": 0, + "title": "Test Article 1", + "video_duration_string": "00:00", + "tags": [ + { + "name": "productivity", + "keywords_for_search": "" + }, + { + "name": "webdev", + "keywords_for_search": "web development" + } + ], + "path": "/somember/web-vitals-explained-114j", + "comments_count": 4, + "public_reactions_count": 94, + "id": 614848, + "published_at": "2021-02-22T17:00:55.966Z", + "class_name": "Article", + "user": { + "name": "Some Member", + "id": 67245, + "pro": null, + "profile_image_90": "https://res.cloudinary.com/practicaldev/image/fetch/s--XE2mPGud--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/9597/68d6245f-3152-4ed2-a245-d015fca4160b.jpeg", + "username": "somember" + }, + "tag_list": ["productivity", "webdev"], + "flare_tag": null, + "user_id": 67245, + "highlight": null, + "readable_publish_date": "Feb 22", + "podcast": { + "slug": null, + "image_url": "https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9iebxyedkwozybopkn6e.jpeg", + "title": "Test Article 1" + }, + "_score": 0, + "published_at_int": 1614013255, + "published_timestamp": "2021-02-22T17:00:55.966Z" + } + }, + { + "id": 7375469, + "user_id": 9597, + "reactable": { + "reading_time": 3, + "main_image": "https://res.cloudinary.com/practicaldev/image/fetch/s--XE2mPGud--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/9597/68d6245f-3152-4ed2-a245-d015fca4160b.jpeg", + "readable_publish_date_string": "Feb 22", + "cloudinary_video_url": null, + "video_duration_in_minutes": 0, + "title": "Test Article 3", + "video_duration_string": "00:00", + "tags": [ + { + "name": "productivity", + "keywords_for_search": "" + }, + { + "name": "javascript", + "keywords_for_search": "js" + } + ], + "path": "/somember/web-vitals-explained-114j", + "comments_count": 4, + "public_reactions_count": 94, + "id": 614848, + "published_at": "2021-02-22T17:00:55.966Z", + "class_name": "Article", + "user": { + "name": "Some Member", + "id": 67245, + "pro": null, + "profile_image_90": "https://res.cloudinary.com/practicaldev/image/fetch/s--XE2mPGud--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/9597/68d6245f-3152-4ed2-a245-d015fca4160b.jpeg", + "username": "somember" + }, + "tag_list": ["productivity", "javascript"], + "flare_tag": null, + "user_id": 67245, + "highlight": null, + "readable_publish_date": "Feb 22", + "podcast": { + "slug": null, + "image_url": "https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9iebxyedkwozybopkn6e.jpeg", + "title": "Test Article 3" + }, + "_score": 0, + "published_at_int": 1614013255, + "published_timestamp": "2021-02-22T17:00:55.966Z" + } + } + ], + "total": 2 +} diff --git a/cypress/fixtures/search/readingListFilterByText.json b/cypress/fixtures/search/readingListFilterByText.json new file mode 100644 index 000000000..d0d05df8d --- /dev/null +++ b/cypress/fixtures/search/readingListFilterByText.json @@ -0,0 +1,54 @@ +{ + "result": [ + { + "id": 7375469, + "user_id": 9597, + "reactable": { + "reading_time": 3, + "main_image": "https://res.cloudinary.com/practicaldev/image/fetch/s--XE2mPGud--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/9597/68d6245f-3152-4ed2-a245-d015fca4160b.jpeg", + "readable_publish_date_string": "Feb 22", + "cloudinary_video_url": null, + "video_duration_in_minutes": 0, + "title": "Test Article 3", + "video_duration_string": "00:00", + "tags": [ + { + "name": "productivity", + "keywords_for_search": "" + }, + { + "name": "javascript", + "keywords_for_search": "js" + } + ], + "path": "/somember/web-vitals-explained-114j", + "comments_count": 4, + "public_reactions_count": 94, + "id": 614848, + "published_at": "2021-02-22T17:00:55.966Z", + "class_name": "Article", + "user": { + "name": "Some Member", + "id": 67245, + "pro": null, + "profile_image_90": "https://res.cloudinary.com/practicaldev/image/fetch/s--XE2mPGud--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/9597/68d6245f-3152-4ed2-a245-d015fca4160b.jpeg", + "username": "somember" + }, + "tag_list": ["productivity", "javascript"], + "flare_tag": null, + "user_id": 67245, + "highlight": null, + "readable_publish_date": "Feb 22", + "podcast": { + "slug": null, + "image_url": "https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9iebxyedkwozybopkn6e.jpeg", + "title": "Test Article 3" + }, + "_score": 0, + "published_at_int": 1614013255, + "published_timestamp": "2021-02-22T17:00:55.966Z" + } + } + ], + "total": 1 +} diff --git a/cypress/integration/readingListFlows/readingList.spec.js b/cypress/integration/readingListFlows/readingList.spec.js new file mode 100644 index 000000000..b3928e353 --- /dev/null +++ b/cypress/integration/readingListFlows/readingList.spec.js @@ -0,0 +1,166 @@ +import { BREAKPOINTS } from '../../../app/javascript/shared/components/useMediaQuery'; + +describe('Reading List Archive', () => { + beforeEach(() => { + cy.testSetup(); + cy.fixture('users/articleEditorV1User.json').as('user'); + cy.get('@user').then((user) => { + cy.loginUser(user); + }); + }); + + it('should load an empty reading list', () => { + cy.intercept( + Cypress.config().baseUrl + + 'search/reactions?page=0&per_page=80&status%5B%5D=valid&status%5B%5D=confirmed', + { fixture: 'search/emptyReadingList.json' }, + ).as('emptyReadingList'); + + cy.visit('/readinglist'); + cy.wait('@emptyReadingList'); + + cy.findByRole('main') + .as('main') + .findByText(/^Your reading list is empty$/i); + cy.get('@main').findByText(/^View archive$/i); + cy.get('@main').findByLabelText(/^Filter reading list by text$/i); + cy.get('@main').findByText(/^Reading list \(0\)$/i); + cy.get('@main') + .findByRole('navigation', { name: /^Filter by tag$/i }) + .findByText(/all tags/i); + }); + + it('should filter by text', () => { + cy.intercept( + Cypress.config().baseUrl + + 'search/reactions?page=0&per_page=80&status%5B%5D=valid&status%5B%5D=confirmed', + { fixture: 'search/readingList.json' }, + ).as('readingList'); + + cy.intercept( + Cypress.config().baseUrl + + 'search/reactions?search_fields=article+3&page=0&per_page=80&status%5B%5D=valid&status%5B%5D=confirmed', + { fixture: 'search/readingListFilterByText.json' }, + ).as('readingListFilteredByText'); + + cy.visit('/readinglist'); + cy.wait('@readingList'); + + cy.findByRole('main').as('main'); + cy.get('@main') + .findByLabelText(/^Filter reading list by text$/i) + .type('article 3'); + + cy.wait('@readingListFilteredByText'); + + cy.get('@main').findByText('Test Article 1').should('not.exist'); + cy.get('@main').findByText('Test Article 2').should('not.exist'); + cy.get('@main').findByText('Test Article 3'); + }); + + describe('small screens', () => { + beforeEach(() => { + cy.intercept( + Cypress.config().baseUrl + + 'search/reactions?page=0&per_page=80&status%5B%5D=valid&status%5B%5D=confirmed', + { fixture: 'search/readingList.json' }, + ).as('readingList'); + + cy.viewport(BREAKPOINTS.Medium - 1, BREAKPOINTS.Medium); + cy.visit('/readinglist'); + cy.wait('@readingList'); + }); + + it('should load the reading list with items', () => { + cy.findByRole('main') + .as('main') + .findByText(/^Your reading list is empty$/i) + .should('not.exist'); + cy.get('@main').findByText(/^View archive$/i); + cy.get('@main').findByLabelText(/Filter reading list by text$/i); + cy.get('@main').findByText(/^Reading list \(3\)$/); + cy.get('@main').findByLabelText(/^Filter by tag$/i, { + selector: 'select', + }); + cy.get('@main') + .findByText(/^Filter by tag$/i, { selector: 'legend' }) + .should('not.exist'); + + cy.get('@main').findByText('Test Article 1'); + cy.get('@main').findByText('Test Article 2'); + cy.get('@main').findByText('Test Article 3'); + }); + + it('should filter by tag', () => { + cy.intercept( + Cypress.config().baseUrl + + 'search/reactions?search_fields=&page=0&per_page=80&tag_names%5B%5D=productivity&tag_boolean_mode=all&status%5B%5D=valid&status%5B%5D=confirmed', + { fixture: 'search/readingListFilterByTagProductivity.json' }, + ).as('filteredReadingList'); + + cy.findByRole('main') + .as('main') + .findByLabelText('Filter by tag') + .as('tagFilter') + .select('productivity'); + + cy.wait('@filteredReadingList'); + + cy.get('@main').findByText('Test Article 1'); + cy.get('@main').findByText('Test Article 2').should('not.exist'); + cy.get('@main').findByText('Test Article 3'); + }); + }); + + describe('large screens', () => { + beforeEach(() => { + cy.intercept( + Cypress.config().baseUrl + + 'search/reactions?page=0&per_page=80&status%5B%5D=valid&status%5B%5D=confirmed', + { fixture: 'search/readingList.json' }, + ).as('readingList'); + + cy.viewport(BREAKPOINTS.Large, 600); + cy.visit('/readinglist'); + cy.wait('@readingList'); + }); + + it('should load the reading list with items', () => { + cy.findByRole('main') + .as('main') + .findByText(/^Your reading list is empty$/i) + .should('not.exist'); + cy.get('@main').findByText(/^View archive$/i); + cy.get('@main').findByLabelText(/Filter reading list by text$/i); + cy.get('@main').findByText(/^Reading list \(3\)$/); + cy.get('@main').findByRole('navigation', { name: /^Filter by tag$/i }); + cy.get('@main') + .findByRole('select', { name: /^Filter by tag$/i }) + .should('not.exist'); + + cy.get('@main').findByText('Test Article 1'); + cy.get('@main').findByText('Test Article 2'); + cy.get('@main').findByText('Test Article 3'); + }); + + it('should filter by tag', () => { + cy.intercept( + Cypress.config().baseUrl + + 'search/reactions?search_fields=&page=0&per_page=80&tag_names%5B%5D=productivity&tag_boolean_mode=all&status%5B%5D=valid&status%5B%5D=confirmed', + { fixture: 'search/readingListFilterByTagProductivity.json' }, + ).as('filteredReadingList'); + + cy.findByRole('main') + .as('main') + .findByRole('navigation', { name: /^Filter by tag$/i }) + .findByText('#productivity') + .click(); + + cy.wait('@filteredReadingList'); + + cy.get('@main').findByText('Test Article 1'); + cy.get('@main').findByText('Test Article 2').should('not.exist'); + cy.get('@main').findByText('Test Article 3'); + }); + }); +}); diff --git a/cypress/integration/readingListFlows/readingListArchive.spec.js b/cypress/integration/readingListFlows/readingListArchive.spec.js new file mode 100644 index 000000000..e53fd8055 --- /dev/null +++ b/cypress/integration/readingListFlows/readingListArchive.spec.js @@ -0,0 +1,170 @@ +import { BREAKPOINTS } from '../../../app/javascript/shared/components/useMediaQuery'; + +describe('Reading List Archive', () => { + beforeEach(() => { + cy.testSetup(); + cy.fixture('users/articleEditorV1User.json').as('user'); + cy.get('@user').then((user) => { + cy.loginUser(user); + }); + }); + + it('should load an empty archive', () => { + cy.intercept( + Cypress.config().baseUrl + + 'search/reactions?page=0&per_page=80&status%5B%5D=archived', + { fixture: 'search/emptyReadingList.json' }, + ).as('emptyReadingList'); + + cy.visit('/readinglist/archive'); + cy.wait('@emptyReadingList'); + + cy.findByRole('main') + .as('main') + .findByText(/^Your Archive is empty...$/i); + cy.get('@main').findByLabelText(/^Filter reading list by text$/i); + cy.get('@main').findByText(/^Archive \(0\)$/i); + cy.get('@main') + .findByRole('navigation', { name: /^Filter by tag$/i }) + .findByText(/all tags/i); + }); + + it('should filter by text', () => { + cy.intercept( + Cypress.config().baseUrl + + 'search/reactions?page=0&per_page=80&status%5B%5D=archived', + { fixture: 'search/readingList.json' }, + ).as('readingList'); + + cy.intercept( + Cypress.config().baseUrl + + 'search/reactions?search_fields=article+3&page=0&per_page=80&status%5B%5D=archived', + { fixture: 'search/readingListFilterByText.json' }, + ).as('readingListFilteredByText'); + + cy.visit('/readinglist/archive'); + cy.wait('@readingList'); + + cy.findByRole('main').as('main'); + cy.get('@main') + .findByLabelText(/^Filter reading list by text$/i) + .type('article 3'); + + cy.wait('@readingListFilteredByText'); + + cy.get('@main').findByText('Test Article 1').should('not.exist'); + cy.get('@main').findByText('Test Article 2').should('not.exist'); + cy.get('@main').findByText('Test Article 3'); + }); + + describe('small screens', () => { + beforeEach(() => { + cy.intercept( + Cypress.config().baseUrl + + 'search/reactions?page=0&per_page=80&status%5B%5D=archived', + { fixture: 'search/readingList.json' }, + ).as('archiveList'); + + cy.viewport(BREAKPOINTS.Medium - 1, BREAKPOINTS.Medium); + cy.visit('/readinglist/archive'); + cy.wait('@archiveList'); + }); + + it('should load the reading list archive with items', () => { + cy.findByRole('main') + .as('main') + .findByText(/^Your reading list is empty$/i) + .should('not.exist'); + cy.get('@main').findByText(/^View reading list$/i); + cy.get('@main').findByLabelText(/Filter reading list by text$/i); + cy.get('@main').findByText(/^Archive \(3\)$/); + cy.get('@main').findByLabelText(/^Filter by tag$/i, { + selector: 'select', + }); + cy.get('@main') + .findByText(/^Filter by tag$/i, { selector: 'legend' }) + .should('not.exist'); + + cy.get('@main').findByText('Test Article 1'); + cy.get('@main').findByText('Test Article 2'); + cy.get('@main').findByText('Test Article 3'); + }); + + it('should filter by tag', () => { + cy.intercept( + Cypress.config().baseUrl + + 'search/reactions?search_fields=&page=0&per_page=80&tag_names%5B%5D=productivity&tag_boolean_mode=all&status%5B%5D=archived', + { + fixture: 'search/readingListFilterByTagProductivity.json', + }, + ).as('filteredArchiveList'); + + cy.findByRole('main') + .as('main') + .findByLabelText('Filter by tag') + .as('tagFilter') + .select('productivity'); + + cy.wait('@filteredArchiveList'); + + cy.get('@main').findByText('Test Article 1'); + cy.get('@main').findByText('Test Article 2').should('not.exist'); + cy.get('@main').findByText('Test Article 3'); + }); + }); + + describe('large screens', () => { + beforeEach(() => { + cy.intercept( + Cypress.config().baseUrl + + 'search/reactions?page=0&per_page=80&status%5B%5D=archived', + { fixture: 'search/readingList.json' }, + ).as('archiveList'); + + cy.viewport(BREAKPOINTS.Large, 600); + cy.visit('/readinglist/archive'); + cy.wait('@archiveList'); + }); + + it('should load the reading list archive items', () => { + cy.findByRole('main') + .as('main') + .findByText(/^Your Archive is empty$/i) + .should('not.exist'); + + cy.get('@main').findByText(/^View reading list$/i); + cy.get('@main').findByLabelText(/Filter reading list by text$/i); + cy.get('@main').findByText(/^Archive \(3\)$/); + cy.get('@main').findByRole('navigation', { name: /^Filter by tag$/i }); + cy.get('@main') + .findByRole('select', { name: /^Filter by tag$/i }) + .should('not.exist'); + + cy.get('@main').findByText('Test Article 1'); + cy.get('@main').findByText('Test Article 2'); + cy.get('@main').findByText('Test Article 3'); + }); + + it('should filter by tag', () => { + cy.intercept( + Cypress.config().baseUrl + + 'search/reactions?search_fields=&page=0&per_page=80&tag_names%5B%5D=productivity&tag_boolean_mode=all&status%5B%5D=archived', + { + fixture: 'search/readingListFilterByTagProductivity.json', + }, + ).as('filteredArchiveList'); + + cy.findByRole('main') + .as('main') + .findByRole('navigation', { name: /^Filter by tag$/i }) + .findByText('#productivity') + .click(); + + cy.wait('@filteredArchiveList'); + + cy.get('@main').findByText('Test Article 1'); + cy.get('@main').findByText('Test Article 2').should('not.exist'); + cy.get('@main').findByText('Test Article 3'); + }); + }); +}); diff --git a/jest.config.js b/jest.config.js index eaadfbc4a..864d0b8a3 100644 --- a/jest.config.js +++ b/jest.config.js @@ -25,8 +25,8 @@ module.exports = { ], coverageThreshold: { global: { - statements: 43, - branches: 39, + statements: 42, + branches: 38, functions: 41, lines: 43, },