Fixed Reading List on Mobile + Small Improvements (#12755)
* Some div soup to semantic markup. * Small refactor to inline mapping of available tags. * Renamed <ItemListTags /> component to <TagList />. * Now a select is used for picking a tag to filter on. * Added custom Cypress command to create an article. * Added documentation for the create article custom command. * Removed unnecesary properties from payload to create an article. * reading list mobile view wip. * Reworked styles in <TagList />. * Reworked reading list to use <MediaQuery /> component. * Removed bottom padding from reading list header. * styling tweaks if there are no available tags. * Added some E2E tests. * Removed reading list component test in favour of e2e test. * Made breakpoint values numbers. * Added some padding and more grid gap to filter on small screens. * Adjusted jest coverage thresholds as we're moving some tests to e2e tests. * Reverting a VS Code setting change caused by one of my extensions. * First pass for E2E tests for the reading list. * Added some more grid gap. * Fixed load next page to send tags properly. * Added some more tests. * Improved label and placeholder for text filter in reading list. * Added more tests * Fixed media queries so it works in Chrome as well. * Removed aside as tag filters are not complimentary information. * Update app/javascript/readingList/components/TagList.jsx Co-authored-by: Suzanne Aitchison <suzanne@forem.com> * Update docs/tests/e2e-tests.md Co-authored-by: Suzanne Aitchison <suzanne@forem.com> * Turned off deprecated rule in jsx-a11y eslint plugin. * Reverted to links instead of radio buttons. * Added an all tags link and select option. * Fixed relayout issue. * Fixed View Archive button size. * Fixed styling of the load more button. * Fixed empty list issue toggling between archive and reading list. * Fixed request changes from PR review. * Removed CSS change that is no longer required. * Trigger Build * Fixed centering of items in top fieldset. * Fixed issue with search text field resetting reading list. * Fixed component tests for the reading list. * Fixed empty state popping up between search queries. * Fixed casing of fixture filenames. * Update app/javascript/readingList/readingList.jsx Co-authored-by: Suzanne Aitchison <suzanne@forem.com> * Reverted change in reading list component test. * Added missing JSDoc comment. * Now links are in an unordered list. * Promoted some CSS classes from the <nav /> to the <ul /> for spacing. Co-authored-by: Suzanne Aitchison <suzanne@forem.com>
This commit is contained in:
parent
1522b6d41e
commit
86deddbb75
16 changed files with 890 additions and 290 deletions
|
|
@ -5,6 +5,17 @@ import fetch from 'jest-fetch-mock';
|
|||
import { ReadingList } from '../readingList';
|
||||
|
||||
describe('<ReadingList />', () => {
|
||||
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('<ReadingList />', () => {
|
|||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('renders all the elements', () => {
|
||||
const { queryByPlaceholderText, queryByText } = render(
|
||||
<ReadingList availableTags={['discuss']} />,
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div>
|
||||
<Button onClick={onClick} className="w-100" variant="secondary">
|
||||
Load more
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ItemListLoadMoreButton.propTypes = {
|
||||
show: PropTypes.bool.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
@ -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) => (
|
||||
<a
|
||||
className={`crayons-link crayons-link--block ${
|
||||
selectedTags.indexOf(tag) > -1 ? 'crayons-link--current' : ''
|
||||
}`}
|
||||
href={`/t/${tag}`}
|
||||
data-no-instant
|
||||
onClick={(e) => onClick(e, tag)}
|
||||
>
|
||||
{`#${tag}`}
|
||||
</a>
|
||||
));
|
||||
return (
|
||||
<nav className="crayons-layout__sidebar-left" data-testid="tags">
|
||||
{tagsHTML}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
ItemListTags.propTypes = {
|
||||
availableTags: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
selectedTags: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
88
app/javascript/readingList/components/TagList.jsx
Normal file
88
app/javascript/readingList/components/TagList.jsx
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function LargeScreenTagList({ availableTags, selectedTag, onSelectTag }) {
|
||||
return (
|
||||
<nav aria-label="Filter by tag">
|
||||
<ul className="list-none grid grid-cols-1 gap-2">
|
||||
<li>
|
||||
<a
|
||||
className={`crayons-link crayons-link--block${
|
||||
!selectedTag ? ' crayons-link--current' : ''
|
||||
}`}
|
||||
data-no-instant
|
||||
onClick={onSelectTag}
|
||||
href="/t"
|
||||
>
|
||||
all tags
|
||||
</a>
|
||||
</li>
|
||||
{availableTags.map((tag) => (
|
||||
<li>
|
||||
<a
|
||||
className={`crayons-link crayons-link--block${
|
||||
selectedTag === tag ? ' crayons-link--current' : ''
|
||||
}`}
|
||||
data-no-instant
|
||||
data-tag={tag}
|
||||
onClick={onSelectTag}
|
||||
key={tag}
|
||||
href={`t/${tag}`}
|
||||
>
|
||||
#{tag}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} props
|
||||
* @param {Array<string>} 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 ? (
|
||||
<select
|
||||
class="crayons-select"
|
||||
aria-label="Filter by tag"
|
||||
onChange={onSelectTag}
|
||||
>
|
||||
<option>all tags</option>
|
||||
{availableTags.map((tag) => (
|
||||
<option
|
||||
selected={tag === selectedTag}
|
||||
className={`crayons-link crayons-link--block ${
|
||||
tag === selectedTag ? 'crayons-link--current' : ''
|
||||
}`}
|
||||
value={tag}
|
||||
>
|
||||
{tag}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<LargeScreenTagList
|
||||
availableTags={availableTags}
|
||||
selectedTag={selectedTag}
|
||||
onSelectTag={onSelectTag}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
TagList.propTypes = {
|
||||
isMobile: PropTypes.boolean,
|
||||
availableTags: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
selectedTag: PropTypes.string,
|
||||
onSelectTag: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { render } from '@testing-library/preact';
|
||||
import { axe } from 'jest-axe';
|
||||
import { ItemListLoadMoreButton } from '../ItemListLoadMoreButton';
|
||||
|
||||
describe('<ItemListLoadMoreButton />', () => {
|
||||
it('should have no a11y violations when the load more button is not shown', async () => {
|
||||
const { container } = render(<ItemListLoadMoreButton show={false} />);
|
||||
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(<ItemListLoadMoreButton show={true} />);
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('renders nothing when not required', () => {
|
||||
const { queryByText } = render(<ItemListLoadMoreButton show={false} />);
|
||||
expect(queryByText(/load more/i)).toBeNull();
|
||||
});
|
||||
|
||||
it('renders a button when required', () => {
|
||||
const { queryByText } = render(<ItemListLoadMoreButton show={true} />);
|
||||
|
||||
expect(queryByText(/load more/i)).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { render } from '@testing-library/preact';
|
||||
import { axe } from 'jest-axe';
|
||||
import { ItemListTags } from '../ItemListTags';
|
||||
|
||||
describe('<ItemListTags />', () => {
|
||||
it('should have no a11y violations with two different sets of tags', async () => {
|
||||
const { container } = render(
|
||||
<ItemListTags
|
||||
availableTags={['discuss']}
|
||||
selectedTags={['javascript']}
|
||||
/>,
|
||||
);
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should have no a11y violations with two some shared tags', async () => {
|
||||
const { container } = render(
|
||||
<ItemListTags
|
||||
availableTags={['discuss', 'javascript']}
|
||||
selectedTags={['javascript']}
|
||||
/>,
|
||||
);
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('renders properly with two different sets of tags', () => {
|
||||
const { getByText, queryByText } = render(
|
||||
<ItemListTags
|
||||
availableTags={['discuss']}
|
||||
selectedTags={['javascript']}
|
||||
/>,
|
||||
);
|
||||
|
||||
getByText('#discuss');
|
||||
expect(queryByText('#javascript')).toBeNull();
|
||||
});
|
||||
|
||||
it('renders properly with some shared tags', () => {
|
||||
const { queryByText } = render(
|
||||
<ItemListTags
|
||||
availableTags={['discuss', 'javascript']}
|
||||
selectedTags={['javascript']}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(queryByText('#discuss')).toBeDefined();
|
||||
expect(queryByText('#javascript')).toBeDefined();
|
||||
});
|
||||
|
||||
it('triggers the onClick', () => {
|
||||
const onClick = jest.fn();
|
||||
const { getByText } = render(
|
||||
<ItemListTags
|
||||
availableTags={['discuss', 'javascript']}
|
||||
selectedTags={['javascript']}
|
||||
onClick={onClick}
|
||||
/>,
|
||||
);
|
||||
|
||||
let tag = getByText('#discuss', { selector: 'a' });
|
||||
tag.click();
|
||||
|
||||
expect(onClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -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 (
|
||||
<h2 className="fw-bold fs-l">
|
||||
{selectedTags.length === 0 && query.length === 0
|
||||
? value
|
||||
: 'Nothing with this filter 🤔'}
|
||||
</h2>
|
||||
);
|
||||
};
|
||||
function ItemList({ items, archiveButtonLabel, toggleArchiveStatus }) {
|
||||
return items.map((item) => {
|
||||
return (
|
||||
<ItemListItem item={item}>
|
||||
<ItemListItemArchiveButton
|
||||
text={archiveButtonLabel}
|
||||
onClick={(e) => toggleArchiveStatus(e, item)}
|
||||
/>
|
||||
</ItemListItem>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="align-center p-9 py-10 color-base-80">
|
||||
<FilterText
|
||||
selectedTags={selectedTags}
|
||||
query={query}
|
||||
value="Your reading list is empty"
|
||||
/>
|
||||
<section className="align-center p-9 py-10 color-base-80">
|
||||
<h2 className="fw-bold fs-l">
|
||||
{showMessage
|
||||
? 'Your reading list is empty'
|
||||
: NO_RESULTS_WITH_FILTER_MESSAGE}
|
||||
</h2>
|
||||
<p class="color-base-60 pt-2">
|
||||
Click the{' '}
|
||||
<span class="fw-bold">
|
||||
|
|
@ -140,18 +159,16 @@ export class ReadingList extends Component {
|
|||
</span>
|
||||
when viewing a post to add it to your reading list.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="align-center p-9 py-10 color-base-80">
|
||||
<FilterText
|
||||
selectedTags={selectedTags}
|
||||
query={query}
|
||||
value="Your Archive is empty..."
|
||||
/>
|
||||
</div>
|
||||
<h2 className="align-center p-9 py-10 color-base-80 fw-bold fs-l">
|
||||
{showMessage
|
||||
? 'Your Archive is empty...'
|
||||
: NO_RESULTS_WITH_FILTER_MESSAGE}
|
||||
</h2>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -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 (
|
||||
<ItemListItem item={item}>
|
||||
<ItemListItemArchiveButton
|
||||
text={archiveButtonLabel}
|
||||
onClick={(e) => this.toggleArchiveStatus(e, item)}
|
||||
/>
|
||||
</ItemListItem>
|
||||
);
|
||||
});
|
||||
|
||||
const snackBar = archiving ? (
|
||||
<div className="snackbar">
|
||||
|
|
@ -187,17 +194,17 @@ export class ReadingList extends Component {
|
|||
''
|
||||
);
|
||||
return (
|
||||
<section>
|
||||
<header className="crayons-layout flex justify-between items-center pb-0">
|
||||
<main id="main-content">
|
||||
<header className="crayons-layout l:grid-cols-2 pb-0">
|
||||
<h1 class="crayons-title">
|
||||
{isStatusViewValid ? 'Reading list' : 'Archive'}
|
||||
{` (${itemsTotal})`}
|
||||
</h1>
|
||||
|
||||
<div class="flex items-center">
|
||||
<fieldset className="grid gap-2 m:flex m:justify-end m:items-center l:mb-0 mb-2 px-2 m:px-0">
|
||||
<legend className="hidden">Filter</legend>
|
||||
<Button
|
||||
onClick={(e) => this.toggleStatusView(e)}
|
||||
className="mr-2 whitespace-nowrap"
|
||||
className="whitespace-nowrap l:mr-2"
|
||||
variant="outlined"
|
||||
url={READING_LIST_ARCHIVE_PATH}
|
||||
tagName="a"
|
||||
|
|
@ -206,35 +213,70 @@ export class ReadingList extends Component {
|
|||
{isStatusViewValid ? 'View archive' : 'View reading list'}
|
||||
</Button>
|
||||
<input
|
||||
aria-label="Search..."
|
||||
aria-label="Filter reading list by text"
|
||||
onKeyUp={this.onSearchBoxType}
|
||||
placeholder="Search..."
|
||||
placeholder="Enter some text to filter on..."
|
||||
className="crayons-textfield"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="crayons-layout crayons-layout--2-cols">
|
||||
<ItemListTags
|
||||
availableTags={availableTags}
|
||||
selectedTags={selectedTags}
|
||||
onClick={this.toggleTag}
|
||||
/>
|
||||
|
||||
<main className="crayons-layout__content" id="main-content">
|
||||
<div className="crayons-card mb-4">
|
||||
{items.length > 0 ? itemsToRender : this.renderEmptyItems()}
|
||||
</div>
|
||||
|
||||
<ItemListLoadMoreButton
|
||||
show={showLoadMoreButton}
|
||||
onClick={this.loadNextPage}
|
||||
<MediaQuery
|
||||
query={`(max-width: ${BREAKPOINTS.Medium - 1}px)`}
|
||||
render={(matches) => {
|
||||
return (
|
||||
matches && (
|
||||
<TagList
|
||||
availableTags={availableTags}
|
||||
selectedTag={selectedTag}
|
||||
onSelectTag={this.selectTag}
|
||||
isMobile={true}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</main>
|
||||
|
||||
{snackBar}
|
||||
</div>
|
||||
</section>
|
||||
</fieldset>
|
||||
</header>
|
||||
<MediaQuery
|
||||
query={`(min-width: ${BREAKPOINTS.Medium}px)`}
|
||||
render={(matches) => {
|
||||
return (
|
||||
<div className="crayons-layout crayons-layout--2-cols">
|
||||
{matches && (
|
||||
<div className="crayons-layout__sidebar-left">
|
||||
<TagList
|
||||
availableTags={availableTags}
|
||||
selectedTag={selectedTag}
|
||||
onSelectTag={this.selectTag}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<section className="crayons-layout__content crayons-card mb-4">
|
||||
{items.length > 0 ? (
|
||||
<ItemList
|
||||
items={items}
|
||||
archiveButtonLabel={archiveButtonLabel}
|
||||
toggleArchiveStatus={this.toggleArchiveStatus}
|
||||
/>
|
||||
) : loading ? null : (
|
||||
this.renderEmptyItems()
|
||||
)}
|
||||
{showLoadMoreButton && (
|
||||
<div className="flex justify-center my-2">
|
||||
<Button
|
||||
onClick={this.loadNextPage}
|
||||
variant="secondary"
|
||||
className="w-max"
|
||||
>
|
||||
Load more
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{snackBar}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
|||
1
cypress/fixtures/search/emptyReadingList.json
Normal file
1
cypress/fixtures/search/emptyReadingList.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{ "result": [], "total": 0 }
|
||||
152
cypress/fixtures/search/readingList.json
Normal file
152
cypress/fixtures/search/readingList.json
Normal file
|
|
@ -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
|
||||
}
|
||||
103
cypress/fixtures/search/readingListFilterByTagProductivity.json
Normal file
103
cypress/fixtures/search/readingListFilterByTagProductivity.json
Normal file
|
|
@ -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
|
||||
}
|
||||
54
cypress/fixtures/search/readingListFilterByText.json
Normal file
54
cypress/fixtures/search/readingListFilterByText.json
Normal file
|
|
@ -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
|
||||
}
|
||||
166
cypress/integration/readingListFlows/readingList.spec.js
Normal file
166
cypress/integration/readingListFlows/readingList.spec.js
Normal file
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
170
cypress/integration/readingListFlows/readingListArchive.spec.js
Normal file
170
cypress/integration/readingListFlows/readingListArchive.spec.js
Normal file
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -25,8 +25,8 @@ module.exports = {
|
|||
],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
statements: 43,
|
||||
branches: 39,
|
||||
statements: 42,
|
||||
branches: 38,
|
||||
functions: 41,
|
||||
lines: 43,
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue