Migrate initializeArticleDate and initializeArticleReactions to Webpack (#19377)
* feat: tests for series list on article page * feat: test for jump to comments button (needs fix) * feat: tests for reaction drawer on article page * feat: test for full date on hover?? * feat: add full date on hover tests for all pages with publish dates * fixed jumping to comments test * move initialisers to packs * fix coverage issues * fix flaky login modal spec * switch back to using onclick instead of addEventListener
This commit is contained in:
parent
675ad72a9a
commit
800ff397e0
18 changed files with 590 additions and 114 deletions
|
|
@ -2,7 +2,6 @@
|
|||
global initializeLocalStorageRender, initializeBodyData,
|
||||
initializeAllTagEditButtons, initializeUserFollowButts,
|
||||
initializeCommentsPage,
|
||||
initializeArticleDate, initializeArticleReactions,
|
||||
initializeSettings, initializeRuntimeBanner,
|
||||
initializeTimeFixer, initializeCreditsPage,
|
||||
initializeOnboardingTaskCard,
|
||||
|
|
@ -14,8 +13,6 @@
|
|||
function callInitializers() {
|
||||
initializePaymentPointers();
|
||||
initializeCommentsPage();
|
||||
initializeArticleDate();
|
||||
initializeArticleReactions();
|
||||
initializeSettings();
|
||||
initializeTimeFixer();
|
||||
initializeCreditsPage();
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
/* Show article date/time according to user's locale */
|
||||
/* global addLocalizedDateTimeToElementsTitles */
|
||||
|
||||
function initializeArticleDate() {
|
||||
var articlesDates = document.querySelectorAll(
|
||||
'.crayons-story time, article time, .single-other-article time',
|
||||
);
|
||||
|
||||
addLocalizedDateTimeToElementsTitles(articlesDates, 'datetime');
|
||||
}
|
||||
|
|
@ -1,15 +1,16 @@
|
|||
/* global sendHapticMessage, showLoginModal, showModalAfterError, isTouchDevice, watchForLongTouch */
|
||||
/* global sendHapticMessage, showLoginModal, isTouchDevice, watchForLongTouch */
|
||||
import { showModalAfterError } from '../utilities/showUserAlertModal';
|
||||
|
||||
// Set reaction count to correct number
|
||||
function setReactionCount(reactionName, newCount) {
|
||||
var reactionButtons = document.getElementById(
|
||||
'reaction-butt-' + reactionName,
|
||||
const setReactionCount = (reactionName, newCount) => {
|
||||
const reactionButtons = document.getElementById(
|
||||
`reaction-butt-${reactionName}`,
|
||||
).classList;
|
||||
var reactionButtonCounter = document.getElementById(
|
||||
'reaction-number-' + reactionName,
|
||||
const reactionButtonCounter = document.getElementById(
|
||||
`reaction-number-${reactionName}`,
|
||||
);
|
||||
var reactionEngagementCounter = document.getElementById(
|
||||
'reaction_engagement_' + reactionName + '_count',
|
||||
const reactionEngagementCounter = document.getElementById(
|
||||
`reaction_engagement_${reactionName}_count`,
|
||||
);
|
||||
if (newCount > 0) {
|
||||
reactionButtons.add('activated');
|
||||
|
|
@ -25,30 +26,31 @@ function setReactionCount(reactionName, newCount) {
|
|||
reactionEngagementCounter.parentElement.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function setSumReactionCount(counts) {
|
||||
let totalCountObj = document.getElementById('reaction_total_count');
|
||||
const setSumReactionCount = (counts) => {
|
||||
const totalCountObj = document.getElementById('reaction_total_count');
|
||||
if (totalCountObj && counts.length > 2) {
|
||||
let sum = 0;
|
||||
for (let i in counts) {
|
||||
if (counts[i]['category'] != 'readinglist') {
|
||||
sum += counts[i]['count'];
|
||||
for (const count of counts) {
|
||||
if (count['category'] != 'readinglist') {
|
||||
sum += count['count'];
|
||||
}
|
||||
}
|
||||
totalCountObj.textContent = sum;
|
||||
}
|
||||
}
|
||||
function showCommentCount() {
|
||||
let commentCountObj = document.getElementById('reaction-number-comment');
|
||||
};
|
||||
|
||||
const showCommentCount = () => {
|
||||
const commentCountObj = document.getElementById('reaction-number-comment');
|
||||
if (commentCountObj && commentCountObj.dataset.count) {
|
||||
commentCountObj.textContent = commentCountObj.dataset.count;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function showUserReaction(reactionName, animatedClass) {
|
||||
const showUserReaction = (reactionName, animatedClass) => {
|
||||
const reactionButton = document.getElementById(
|
||||
'reaction-butt-' + reactionName,
|
||||
`reaction-butt-${reactionName}`,
|
||||
);
|
||||
reactionButton.classList.add('user-activated', animatedClass);
|
||||
reactionButton.setAttribute('aria-pressed', 'true');
|
||||
|
|
@ -63,11 +65,11 @@ function showUserReaction(reactionName, animatedClass) {
|
|||
}
|
||||
|
||||
reactionDrawerButton.classList.add('user-activated', 'user-animated');
|
||||
}
|
||||
};
|
||||
|
||||
function hideUserReaction(reactionName) {
|
||||
const hideUserReaction = (reactionName) => {
|
||||
const reactionButton = document.getElementById(
|
||||
'reaction-butt-' + reactionName,
|
||||
`reaction-butt-${reactionName}`,
|
||||
);
|
||||
reactionButton.classList.remove('user-activated', 'user-animated');
|
||||
reactionButton.setAttribute('aria-pressed', 'false');
|
||||
|
|
@ -80,31 +82,31 @@ function hideUserReaction(reactionName) {
|
|||
if (userActivatedReactions.length == 0) {
|
||||
reactionDrawerButton.classList.remove('user-activated', 'user-animated');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function hasUserReacted(reactionName) {
|
||||
const hasUserReacted = (reactionName) => {
|
||||
return document
|
||||
.getElementById('reaction-butt-' + reactionName)
|
||||
.getElementById(`reaction-butt-${reactionName}`)
|
||||
.classList.contains('user-activated');
|
||||
}
|
||||
};
|
||||
|
||||
function getNumReactions(reactionName) {
|
||||
const reactionEl = document.getElementById('reaction-number-' + reactionName);
|
||||
const getNumReactions = (reactionName) => {
|
||||
const reactionEl = document.getElementById(`reaction-number-${reactionName}`);
|
||||
if (!reactionEl || reactionEl.textContent === '') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return parseInt(reactionEl.textContent, 10);
|
||||
}
|
||||
};
|
||||
|
||||
function reactToArticle(articleId, reaction) {
|
||||
var reactionTotalCount = document.getElementById('reaction_total_count');
|
||||
const reactToArticle = (articleId, reaction) => {
|
||||
const reactionTotalCount = document.getElementById('reaction_total_count');
|
||||
|
||||
var isReadingList = reaction === 'readinglist';
|
||||
const isReadingList = reaction === 'readinglist';
|
||||
|
||||
// Visually toggle the reaction
|
||||
function toggleReaction() {
|
||||
var currentNum = getNumReactions(reaction);
|
||||
const currentNum = getNumReactions(reaction);
|
||||
if (hasUserReacted(reaction)) {
|
||||
hideUserReaction(reaction);
|
||||
setReactionCount(reaction, currentNum - 1);
|
||||
|
|
@ -119,7 +121,7 @@ function reactToArticle(articleId, reaction) {
|
|||
}
|
||||
}
|
||||
}
|
||||
var userStatus = document.body.getAttribute('data-user-status');
|
||||
const userStatus = document.body.getAttribute('data-user-status');
|
||||
sendHapticMessage('medium');
|
||||
if (userStatus === 'logged-out') {
|
||||
showLoginModal({
|
||||
|
|
@ -129,14 +131,14 @@ function reactToArticle(articleId, reaction) {
|
|||
return;
|
||||
}
|
||||
toggleReaction();
|
||||
document.getElementById('reaction-butt-' + reaction).disabled = true;
|
||||
document.getElementById(`reaction-butt-${reaction}`).disabled = true;
|
||||
|
||||
function createFormdata() {
|
||||
/*
|
||||
* What's not shown here is that "authenticity_token" is included in this formData.
|
||||
* The logic can be seen in sendFetch.js.
|
||||
*/
|
||||
var formData = new FormData();
|
||||
const formData = new FormData();
|
||||
formData.append('reactable_type', 'Article');
|
||||
formData.append('reactable_id', articleId);
|
||||
formData.append('category', reaction);
|
||||
|
|
@ -148,81 +150,79 @@ function reactToArticle(articleId, reaction) {
|
|||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
return response.json().then(() => {
|
||||
document.getElementById('reaction-butt-' + reaction).disabled = false;
|
||||
document.getElementById(`reaction-butt-${reaction}`).disabled = false;
|
||||
});
|
||||
} else {
|
||||
toggleReaction();
|
||||
document.getElementById('reaction-butt-' + reaction).disabled = false;
|
||||
showModalAfterError({
|
||||
response,
|
||||
element: 'reaction',
|
||||
action_ing: 'updating',
|
||||
action_past: 'updated',
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
toggleReaction();
|
||||
document.getElementById('reaction-butt-' + reaction).disabled = false;
|
||||
document.getElementById(`reaction-butt-${reaction}`).disabled = false;
|
||||
showModalAfterError({
|
||||
response,
|
||||
element: 'reaction',
|
||||
action_ing: 'updating',
|
||||
action_past: 'updated',
|
||||
});
|
||||
return undefined;
|
||||
})
|
||||
.catch((_error) => {
|
||||
toggleReaction();
|
||||
document.getElementById(`reaction-butt-${reaction}`).disabled = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function setCollectionFunctionality() {
|
||||
const setCollectionFunctionality = () => {
|
||||
if (document.getElementById('collection-link-inbetween')) {
|
||||
var inbetweenLinks = document.getElementsByClassName(
|
||||
const inbetweenLinks = document.getElementsByClassName(
|
||||
'series-switcher__link--inbetween',
|
||||
);
|
||||
var inbetweenLinksLength = inbetweenLinks.length;
|
||||
for (var i = 0; i < inbetweenLinks.length; i += 1) {
|
||||
const inbetweenLinksLength = inbetweenLinks.length;
|
||||
for (let i = 0; i < inbetweenLinks.length; i += 1) {
|
||||
inbetweenLinks[i].onclick = (e) => {
|
||||
e.preventDefault();
|
||||
var els = document.getElementsByClassName(
|
||||
const els = document.getElementsByClassName(
|
||||
'series-switcher__link--hidden',
|
||||
);
|
||||
var elsLength = els.length;
|
||||
for (var j = 0; j < elsLength; j += 1) {
|
||||
const elsLength = els.length;
|
||||
for (let j = 0; j < elsLength; j += 1) {
|
||||
els[0].classList.remove('series-switcher__link--hidden');
|
||||
}
|
||||
for (var k = 0; k < inbetweenLinksLength; k += 1) {
|
||||
for (let k = 0; k < inbetweenLinksLength; k += 1) {
|
||||
inbetweenLinks[0].className = 'series-switcher__link--hidden';
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function requestReactionCounts(articleId) {
|
||||
var ajaxReq;
|
||||
ajaxReq = new XMLHttpRequest();
|
||||
const requestReactionCounts = (articleId) => {
|
||||
const ajaxReq = new XMLHttpRequest();
|
||||
ajaxReq.onreadystatechange = () => {
|
||||
if (ajaxReq.readyState === XMLHttpRequest.DONE) {
|
||||
var json = JSON.parse(ajaxReq.response);
|
||||
const json = JSON.parse(ajaxReq.response);
|
||||
setSumReactionCount(json.article_reaction_counts);
|
||||
showCommentCount();
|
||||
json.article_reaction_counts.forEach((reaction) => {
|
||||
setReactionCount(reaction.category, reaction.count);
|
||||
});
|
||||
json.reactions.forEach((reaction) => {
|
||||
if (document.getElementById('reaction-butt-' + reaction.category)) {
|
||||
if (document.getElementById(`reaction-butt-${reaction.category}`)) {
|
||||
showUserReaction(reaction.category, 'not-user-animated');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
ajaxReq.open('GET', '/reactions?article_id=' + articleId, true);
|
||||
ajaxReq.open('GET', `/reactions?article_id=${articleId}`, true);
|
||||
ajaxReq.send();
|
||||
}
|
||||
};
|
||||
|
||||
function openDrawerOnHover() {
|
||||
var timer;
|
||||
const openDrawerOnHover = () => {
|
||||
let timer;
|
||||
const drawerTrigger = document.getElementById('reaction-drawer-trigger');
|
||||
if (!drawerTrigger) {
|
||||
return;
|
||||
}
|
||||
|
||||
drawerTrigger.addEventListener('click', function (event) {
|
||||
var articleId = document.getElementById('article-body').dataset.articleId;
|
||||
drawerTrigger.addEventListener('click', (_event) => {
|
||||
const { articleId } = document.getElementById('article-body').dataset;
|
||||
reactToArticle(articleId, 'like');
|
||||
|
||||
drawerTrigger.parentElement.classList.add('open');
|
||||
|
|
@ -230,31 +230,31 @@ function openDrawerOnHover() {
|
|||
|
||||
if (isTouchDevice()) {
|
||||
watchForLongTouch(drawerTrigger);
|
||||
drawerTrigger.addEventListener('longTouch', function () {
|
||||
drawerTrigger.addEventListener('longTouch', () => {
|
||||
drawerTrigger.parentElement.classList.add('open');
|
||||
});
|
||||
document.addEventListener('touchstart', function (event) {
|
||||
document.addEventListener('touchstart', (event) => {
|
||||
if (!drawerTrigger.parentElement.contains(event.target)) {
|
||||
drawerTrigger.parentElement.classList.remove('open');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
document.querySelectorAll('.hoverdown').forEach(function (el) {
|
||||
el.addEventListener('mouseover', function (event) {
|
||||
document.querySelectorAll('.hoverdown').forEach((el) => {
|
||||
el.addEventListener('mouseover', function (_event) {
|
||||
this.classList.add('open');
|
||||
clearTimeout(timer);
|
||||
});
|
||||
el.addEventListener('mouseout', function (event) {
|
||||
timer = setTimeout(function (event) {
|
||||
el.addEventListener('mouseout', (_event) => {
|
||||
timer = setTimeout((_event) => {
|
||||
document.querySelector('.hoverdown.open').classList.remove('open');
|
||||
}, 500);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function closeDrawerOnOutsideClick() {
|
||||
document.addEventListener('click', function (event) {
|
||||
const closeDrawerOnOutsideClick = () => {
|
||||
document.addEventListener('click', (event) => {
|
||||
const reactionDrawerElement = document.querySelector('.reaction-drawer');
|
||||
const reactionDrawerTriggerElement = document.querySelector(
|
||||
'#reaction-drawer-trigger',
|
||||
|
|
@ -270,44 +270,46 @@ function closeDrawerOnOutsideClick() {
|
|||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function initializeArticleReactions() {
|
||||
const initializeArticleReactions = () => {
|
||||
setCollectionFunctionality();
|
||||
|
||||
openDrawerOnHover();
|
||||
closeDrawerOnOutsideClick();
|
||||
|
||||
setTimeout(() => {
|
||||
var reactionButts = document.getElementsByClassName('crayons-reaction');
|
||||
const reactionButts = document.getElementsByClassName('crayons-reaction');
|
||||
|
||||
// we wait for the article to appear,
|
||||
// we also check that reaction buttons are there as draft articles don't have them
|
||||
if (document.getElementById('article-body') && reactionButts.length > 0) {
|
||||
var articleId = document.getElementById('article-body').dataset.articleId;
|
||||
const { articleId } = document.getElementById('article-body').dataset;
|
||||
|
||||
requestReactionCounts(articleId);
|
||||
|
||||
for (var i = 0; i < reactionButts.length; i += 1) {
|
||||
for (let i = 0; i < reactionButts.length; i += 1) {
|
||||
if (reactionButts[i].classList.contains('pseudo-reaction')) {
|
||||
continue;
|
||||
}
|
||||
reactionButts[i].onclick = function addReactionOnClick(e) {
|
||||
reactionButts[i].onclick = function addReactionOnClick(_event) {
|
||||
reactToArticle(articleId, this.dataset.category);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var jumpToCommentsButt = document.getElementById('reaction-butt-comment');
|
||||
var commentsSection = document.getElementById('comments');
|
||||
const jumpToCommentsButt = document.getElementById('reaction-butt-comment');
|
||||
const commentsSection = document.getElementById('comments');
|
||||
if (
|
||||
document.getElementById('article-body') &&
|
||||
commentsSection &&
|
||||
jumpToCommentsButt
|
||||
) {
|
||||
jumpToCommentsButt.onclick = function jumpToComments(e) {
|
||||
jumpToCommentsButt.onclick = function jumpToComments(_event) {
|
||||
commentsSection.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
}
|
||||
}, 3);
|
||||
}
|
||||
};
|
||||
|
||||
initializeArticleReactions();
|
||||
12
app/javascript/packs/localizeArticleDates.js
Normal file
12
app/javascript/packs/localizeArticleDates.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/* Show article date/time according to user's locale */
|
||||
import { addLocalizedDateTimeToElementsTitles } from '../utilities/localDateTime';
|
||||
|
||||
const initializeArticleDate = () => {
|
||||
const articlesDates = document.querySelectorAll(
|
||||
'.crayons-story time, article time',
|
||||
);
|
||||
|
||||
addLocalizedDateTimeToElementsTitles(articlesDates, 'datetime');
|
||||
};
|
||||
|
||||
initializeArticleDate();
|
||||
|
|
@ -103,5 +103,5 @@
|
|||
<%= render "articles/sidebar_additional" %>
|
||||
</div>
|
||||
|
||||
<%= javascript_packs_with_chunks_tag "storiesList", "followButtons", "feedPreviewCards", "heroBannerClose", defer: true %>
|
||||
<%= javascript_packs_with_chunks_tag "storiesList", "followButtons", "feedPreviewCards", "heroBannerClose", "localizeArticleDates", defer: true %>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -231,7 +231,7 @@
|
|||
<%= render "moderations/modals/unflag_user_modal" %>
|
||||
|
||||
<div class="fullscreen-code js-fullscreen-code"></div>
|
||||
<%= javascript_packs_with_chunks_tag "followButtons", "billboard", defer: true %>
|
||||
<%= javascript_packs_with_chunks_tag "followButtons", "billboard", "localizeArticleDates", "articleReactions", defer: true %>
|
||||
|
||||
<% cache("article-show-scripts", expires_in: 8.hours) do %>
|
||||
<script>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
<% @articles.each do |article| %>
|
||||
<%= render "articles/single_story", story: article.decorate, featured: article.main_image.present? %>
|
||||
<% end %>
|
||||
<%= javascript_packs_with_chunks_tag "followButtons", "feedPreviewCards", "hideBookmarkButtons", defer: true %>
|
||||
<%= javascript_packs_with_chunks_tag "followButtons", "feedPreviewCards", "hideBookmarkButtons", "localizeArticleDates", defer: true %>
|
||||
<% else %>
|
||||
<p><%= t("views.series.list.empty") %></p>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@
|
|||
<%= render "users/meta" %>
|
||||
<% end %>
|
||||
<%= render "organizations/header" %>
|
||||
<div
|
||||
<div
|
||||
class="crayons-layout crayons-layout--limited-l crayons-layout--2-cols crayons-layout--2-cols--1-2 pt-4 m:pt-0"
|
||||
id="index-container"
|
||||
data-params="<%= params.merge(sort_by: "published_at", sort_direction: "desc", organization_id: @organization.id).to_json(only: %i[tag organization_id username q sort_by sort_direction]) %>" data-which="<%= @list_of %>"
|
||||
data-feed="<%= params[:timeframe] || "base-feed" %>"
|
||||
data-articles-since="<%= Timeframe.datetime_iso8601(params[:timeframe]) %>">
|
||||
|
||||
|
||||
<div class="crayons-layout__sidebar-left crayons-layout__content">
|
||||
<%= render "organizations/sidebar" %>
|
||||
</div>
|
||||
|
|
@ -29,4 +29,4 @@
|
|||
</main>
|
||||
</div>
|
||||
|
||||
<%= javascript_packs_with_chunks_tag "storiesList", "followButtons", "hideBookmarkButtons", defer: true %>
|
||||
<%= javascript_packs_with_chunks_tag "storiesList", "followButtons", "hideBookmarkButtons", "localizeArticleDates", defer: true %>
|
||||
|
|
|
|||
|
|
@ -130,5 +130,5 @@
|
|||
<%= render "stories/tagged_articles/sidebar_additional" %>
|
||||
</div>
|
||||
|
||||
<%= javascript_packs_with_chunks_tag "storiesList", "followButtons", "feedPreviewCards", "hideBookmarkButtons", "drawerSliders", defer: true %>
|
||||
<%= javascript_packs_with_chunks_tag "storiesList", "followButtons", "feedPreviewCards", "hideBookmarkButtons", "drawerSliders", "localizeArticleDates", defer: true %>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -145,4 +145,4 @@
|
|||
</div>
|
||||
</main>
|
||||
|
||||
<%= javascript_packs_with_chunks_tag "storiesList", "followButtons", "feedPreviewCards", "hideBookmarkButtons", "profileDropdown", "users/profilePage", defer: true %>
|
||||
<%= javascript_packs_with_chunks_tag "storiesList", "followButtons", "feedPreviewCards", "hideBookmarkButtons", "profileDropdown", "users/profilePage", "localizeArticleDates", defer: true %>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
describe('Series article list on article page', () => {
|
||||
const createSeriesArticle = (title) => {
|
||||
return cy.createArticle({
|
||||
title,
|
||||
content: `${title} in New Series`,
|
||||
series: 'New Series',
|
||||
published: true,
|
||||
});
|
||||
};
|
||||
|
||||
const findVisibleLink = (innerText, { as = 'nothing' } = {}) => {
|
||||
cy.findByText(innerText).parent().as(as).should('have.attr', 'href');
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
cy.testSetup();
|
||||
cy.fixture('users/seriesUser.json').as('user');
|
||||
|
||||
cy.get('@user')
|
||||
.then((user) => cy.loginAndVisit(user, '/'))
|
||||
.then(() => createSeriesArticle('First Post'))
|
||||
.then(() => createSeriesArticle('Second Post'))
|
||||
.then(() => createSeriesArticle('Third Post'))
|
||||
.then(() => createSeriesArticle('Fourth Post'))
|
||||
.then(() => createSeriesArticle('Fifth Post'))
|
||||
.then((response) => {
|
||||
cy.visitAndWaitForUserSideEffects(response.body.current_state_path);
|
||||
});
|
||||
});
|
||||
|
||||
context('when there are 5 articles or less', () => {
|
||||
it('shows links to all of them', () => {
|
||||
cy.get('nav.series-switcher').within(() => {
|
||||
findVisibleLink('First Post');
|
||||
findVisibleLink('Second Post');
|
||||
findVisibleLink('Third Post');
|
||||
findVisibleLink('Fourth Post');
|
||||
findVisibleLink('Fifth Post');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('when there are more than 5 articles', () => {
|
||||
beforeEach(() => {
|
||||
createSeriesArticle('Sixth Post').then((response) => {
|
||||
cy.visitAndWaitForUserSideEffects(response.body.current_state_path);
|
||||
});
|
||||
});
|
||||
|
||||
it('hides the middle articles behind an expander', () => {
|
||||
cy.get('nav.series-switcher').within(() => {
|
||||
findVisibleLink('First Post', { as: 'firstPostLink' });
|
||||
findVisibleLink('Second Post', { as: 'secondPostLink' });
|
||||
cy.findByText('Third Post', { hidden: true }).should('not.be.visible');
|
||||
cy.findByText('Fourth Post', { hidden: true }).should('not.be.visible');
|
||||
findVisibleLink('Fifth Post', { as: 'fifthPostLink' });
|
||||
findVisibleLink('Sixth Post', { as: 'sixthPostLink' });
|
||||
|
||||
findVisibleLink('2 more parts...', { as: 'expander' });
|
||||
cy.get('@expander').click();
|
||||
|
||||
cy.get('@firstPostLink').should('be.visible');
|
||||
cy.get('@secondPostLink').should('be.visible');
|
||||
findVisibleLink('Third Post');
|
||||
findVisibleLink('Fourth Post');
|
||||
cy.get('@fifthPostLink').should('be.visible');
|
||||
cy.get('@sixthPostLink').should('be.visible');
|
||||
cy.get('@expander').should('not.be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
225
cypress/e2e/seededFlows/articleFlows/postReactionsDrawer.spec.js
Normal file
225
cypress/e2e/seededFlows/articleFlows/postReactionsDrawer.spec.js
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
describe('Post reactions drawer', () => {
|
||||
const findHiddenButton = (label, { as = 'nothing' } = {}) => {
|
||||
cy.findByRole('button', { name: label, hidden: true })
|
||||
.as(as)
|
||||
.should('not.be.visible');
|
||||
};
|
||||
|
||||
const findAllReactionButtons = () => {
|
||||
findHiddenButton('Like', { as: 'heartReaction' });
|
||||
findHiddenButton('Unicorn', { as: 'unicornReaction' });
|
||||
findHiddenButton('Exploding Head', { as: 'headReaction' });
|
||||
findHiddenButton('Raised Hands', { as: 'handsReaction' });
|
||||
findHiddenButton('Fire', { as: 'fireReaction' });
|
||||
|
||||
return [
|
||||
'@heartReaction',
|
||||
'@unicornReaction',
|
||||
'@headReaction',
|
||||
'@handsReaction',
|
||||
'@fireReaction',
|
||||
];
|
||||
};
|
||||
|
||||
const checkReactions = (variable, { count = 0 } = {}) => {
|
||||
cy.get(variable).within(() => {
|
||||
cy.get('.crayons-reaction__count').should('have.text', `${count}`);
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
cy.testSetup();
|
||||
cy.fixture('users/articleEditorV2User.json').as('user');
|
||||
|
||||
cy.get('@user').then((user) => {
|
||||
cy.loginUser(user).then(() => {
|
||||
cy.createArticle({
|
||||
title: 'Test Article',
|
||||
tags: ['python'],
|
||||
content: `This is an article about ball pythons.`,
|
||||
published: true,
|
||||
}).then((response) => {
|
||||
cy.visitAndWaitForUserSideEffects(response.body.current_state_path);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('on desktop', () => {
|
||||
beforeEach(() => {
|
||||
cy.viewport('macbook-16');
|
||||
});
|
||||
|
||||
it('should open and close on hover', () => {
|
||||
const reactions = findAllReactionButtons();
|
||||
|
||||
cy.findByRole('button', { name: 'reaction-drawer-trigger' })
|
||||
.as('reactionDrawerButton')
|
||||
.trigger('mouseover');
|
||||
|
||||
for (const reaction of reactions) {
|
||||
cy.get(reaction).should('be.visible');
|
||||
}
|
||||
|
||||
cy.get('@reactionDrawerButton').trigger('mouseout');
|
||||
|
||||
for (const reaction of reactions) {
|
||||
cy.get(reaction).should('not.be.visible');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
context('on mobile', () => {
|
||||
beforeEach(() => {
|
||||
cy.url().then((url) => {
|
||||
cy.visitOnMobile(url);
|
||||
});
|
||||
});
|
||||
|
||||
it('should open on long press and close on a tap outside it', () => {
|
||||
const reactions = findAllReactionButtons();
|
||||
|
||||
// Dismiss deep link banner
|
||||
cy.findByRole('button', { name: 'Dismiss banner' }).click();
|
||||
|
||||
cy.findByRole('button', { name: 'reaction-drawer-trigger' })
|
||||
.as('reactionDrawerButton')
|
||||
.trigger('touchstart');
|
||||
|
||||
for (const reaction of reactions) {
|
||||
cy.get(reaction).should('be.visible');
|
||||
}
|
||||
|
||||
cy.get('@reactionDrawerButton').trigger('touchend');
|
||||
|
||||
for (const reaction of reactions) {
|
||||
cy.get(reaction).should('be.visible');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should act as a like button when clicked', () => {
|
||||
cy.intercept('POST', '/reactions').as('reactRequest');
|
||||
|
||||
findHiddenButton('Like', { as: 'heartReaction' });
|
||||
checkReactions('@heartReaction', { count: 0 });
|
||||
|
||||
cy.findByRole('button', { name: 'reaction-drawer-trigger' }).as(
|
||||
'reactionDrawerButton',
|
||||
);
|
||||
checkReactions('@reactionDrawerButton', { count: 0 });
|
||||
|
||||
cy.get('@reactionDrawerButton').click();
|
||||
cy.wait('@reactRequest');
|
||||
checkReactions('@heartReaction', { count: 1 });
|
||||
checkReactions('@reactionDrawerButton', { count: 1 });
|
||||
|
||||
cy.get('@reactionDrawerButton').click();
|
||||
cy.wait('@reactRequest');
|
||||
checkReactions('@heartReaction', { count: 0 });
|
||||
checkReactions('@reactionDrawerButton', { count: 0 });
|
||||
});
|
||||
|
||||
it('should contain working reaction buttons', () => {
|
||||
cy.intercept('POST', '/reactions').as('reactRequest');
|
||||
|
||||
cy.findByRole('button', { name: 'reaction-drawer-trigger' })
|
||||
.as('reactionDrawerButton')
|
||||
.trigger('mouseover');
|
||||
checkReactions('@reactionDrawerButton', { count: 0 });
|
||||
|
||||
cy.findByRole('button', { name: 'Like' }).as('heartReaction').click();
|
||||
cy.wait('@reactRequest');
|
||||
checkReactions('@heartReaction', { count: 1 });
|
||||
checkReactions('@reactionDrawerButton', { count: 1 });
|
||||
|
||||
cy.findByRole('button', { name: 'Unicorn' }).as('unicornReaction').click();
|
||||
cy.wait('@reactRequest');
|
||||
checkReactions('@unicornReaction', { count: 1 });
|
||||
checkReactions('@reactionDrawerButton', { count: 2 });
|
||||
|
||||
cy.findByRole('button', { name: 'Exploding Head' })
|
||||
.as('headReaction')
|
||||
.click();
|
||||
cy.wait('@reactRequest');
|
||||
checkReactions('@headReaction', { count: 1 });
|
||||
checkReactions('@reactionDrawerButton', { count: 3 });
|
||||
|
||||
cy.findByRole('button', { name: 'Raised Hands' })
|
||||
.as('handsReaction')
|
||||
.click();
|
||||
cy.wait('@reactRequest');
|
||||
checkReactions('@handsReaction', { count: 1 });
|
||||
checkReactions('@reactionDrawerButton', { count: 4 });
|
||||
|
||||
cy.findByRole('button', { name: 'Fire' }).as('fireReaction').click();
|
||||
cy.wait('@reactRequest');
|
||||
checkReactions('@fireReaction', { count: 1 });
|
||||
checkReactions('@reactionDrawerButton', { count: 5 });
|
||||
|
||||
const reactions = [
|
||||
'@heartReaction',
|
||||
'@unicornReaction',
|
||||
'@headReaction',
|
||||
'@handsReaction',
|
||||
'@fireReaction',
|
||||
];
|
||||
let totalCount = 5;
|
||||
|
||||
// Test un-reacting
|
||||
for (const reaction of reactions) {
|
||||
cy.get(reaction).click();
|
||||
cy.wait('@reactRequest');
|
||||
|
||||
totalCount -= 1;
|
||||
checkReactions(reaction, { count: 0 });
|
||||
checkReactions('@reactionDrawerButton', { count: totalCount });
|
||||
}
|
||||
});
|
||||
|
||||
context('when reacting fails', () => {
|
||||
// For UX reasons the UI shows a "successful" reaction before the actual request
|
||||
// to create the reaction returns from the server
|
||||
it('should revert the reaction if the user is offline', () => {
|
||||
cy.intercept('POST', '/reactions', { forceNetworkError: true }).as(
|
||||
'reactRequest',
|
||||
);
|
||||
|
||||
cy.findByRole('button', { name: 'reaction-drawer-trigger' })
|
||||
.as('reactionDrawerButton')
|
||||
.trigger('mouseover');
|
||||
cy.findByRole('button', { name: 'Unicorn' }).as('unicornReaction');
|
||||
|
||||
checkReactions('@unicornReaction', { count: 0 });
|
||||
checkReactions('@reactionDrawerButton', { count: 0 });
|
||||
|
||||
cy.get('@unicornReaction').click();
|
||||
cy.wait('@reactRequest');
|
||||
checkReactions('@unicornReaction', { count: 0 });
|
||||
checkReactions('@reactionDrawerButton', { count: 0 });
|
||||
});
|
||||
|
||||
it('should also notify the user', () => {
|
||||
cy.intercept('POST', '/reactions', { statusCode: 404 }).as(
|
||||
'reactRequest',
|
||||
);
|
||||
|
||||
cy.findByRole('button', { name: 'reaction-drawer-trigger' })
|
||||
.as('reactionDrawerButton')
|
||||
.trigger('mouseover');
|
||||
cy.findByRole('button', { name: 'Unicorn' }).as('unicornReaction');
|
||||
|
||||
checkReactions('@unicornReaction', { count: 0 });
|
||||
checkReactions('@reactionDrawerButton', { count: 0 });
|
||||
|
||||
cy.get('@unicornReaction').click();
|
||||
cy.wait('@reactRequest');
|
||||
|
||||
cy.findByRole('heading', { name: 'Error updating reaction' }).should(
|
||||
'be.visible',
|
||||
);
|
||||
checkReactions('@unicornReaction', { count: 0 });
|
||||
checkReactions('@reactionDrawerButton', { count: 0 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -8,7 +8,8 @@ describe('Post sidebar actions', () => {
|
|||
cy.createArticle({
|
||||
title: 'Test Article',
|
||||
tags: ['beginner', 'ruby', 'go'],
|
||||
content: `This is a test article's contents.`,
|
||||
// Generating a really long article so that the 'Jump to Comments' effect is more visible
|
||||
content: `This is a test article's contents.\n\n`.repeat(100),
|
||||
published: true,
|
||||
}).then((response) => {
|
||||
cy.visitAndWaitForUserSideEffects(response.body.current_state_path);
|
||||
|
|
@ -74,4 +75,29 @@ describe('Post sidebar actions', () => {
|
|||
cy.get('@dropdownButton').click();
|
||||
cy.findByText('Copied to Clipboard').should('not.be.visible');
|
||||
});
|
||||
|
||||
it('should jump to comments when the button is pressed', () => {
|
||||
cy.findByRole('heading', { name: 'Test Article' })
|
||||
.as('articleHeader')
|
||||
.should('be.within_viewport');
|
||||
|
||||
cy.findByRole('button', { name: 'Sort comments' })
|
||||
.as('commentsSortDropdown')
|
||||
.should('not.be.within_viewport');
|
||||
|
||||
// This stub is necessary because somehow, Cypress does not support smooth
|
||||
// scrolling. See https://github.com/cypress-io/cypress/issues/3200
|
||||
cy.get('#comments').then(($comments) => {
|
||||
const comments = $comments[0];
|
||||
const originalScroll = comments.scrollIntoView.bind(comments);
|
||||
cy.stub(comments, 'scrollIntoView').callsFake(() => originalScroll());
|
||||
|
||||
cy.findByRole('button', { name: 'Jump to Comments' })
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('@commentsSortDropdown').should('be.within_viewport');
|
||||
cy.get('@articleHeader').should('not.be.within_viewport');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
describe('Hovering on article publish date', () => {
|
||||
const checkFullDateOnHover = (selector, { locale = '' }) => {
|
||||
cy.get(selector).should((elements) => {
|
||||
elements.each((_, element) => {
|
||||
const date = new Date(element.getAttribute('datetime'));
|
||||
const formattedDate = new Intl.DateTimeFormat(locale, {
|
||||
weekday: 'long',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
}).format(date);
|
||||
|
||||
expect(element.getAttribute('title')).contains(formattedDate);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
cy.testSetup();
|
||||
});
|
||||
|
||||
it('shows full date on the logged-out home page', () => {
|
||||
for (const locale of ['en-US', 'fr-FR', 'es-ES']) {
|
||||
cy.visitWithLocale('/', locale);
|
||||
checkFullDateOnHover('.crayons-story__meta time', { locale });
|
||||
}
|
||||
});
|
||||
|
||||
it('shows full date on the tagged articles page', () => {
|
||||
for (const locale of ['en-US', 'fr-FR', 'es-ES']) {
|
||||
cy.visitWithLocale('/t/tag1', locale);
|
||||
checkFullDateOnHover('.crayons-story__meta time', { locale });
|
||||
}
|
||||
});
|
||||
|
||||
it('shows full date on an article page', () => {
|
||||
for (const locale of ['en-US', 'fr-FR', 'es-ES']) {
|
||||
cy.visitWithLocale('/admin_mcadmin/test-article-slug', locale);
|
||||
checkFullDateOnHover('.crayons-story__meta time', { locale });
|
||||
}
|
||||
});
|
||||
|
||||
it('shows full date on user and organisation profile pages', () => {
|
||||
for (const locale of ['en-US', 'fr-FR', 'es-ES']) {
|
||||
cy.visitWithLocale('/admin_mcadmin', locale);
|
||||
checkFullDateOnHover('.crayons-story__meta time', { locale });
|
||||
|
||||
cy.findByRole('link', { name: 'Bachmanity' }).click();
|
||||
checkFullDateOnHover('.crayons-story__meta time', { locale });
|
||||
}
|
||||
});
|
||||
|
||||
context('on a series list page', () => {
|
||||
beforeEach(() => {
|
||||
cy.fixture('users/seriesUser.json').as('user');
|
||||
cy.get('@user')
|
||||
.then((user) => cy.loginUser(user))
|
||||
.then(() =>
|
||||
cy.createArticle({
|
||||
title: 'Second Test Post',
|
||||
content: 'Some more content so the series switcher shows up',
|
||||
series: 'seriestest',
|
||||
published: true,
|
||||
}),
|
||||
)
|
||||
.then((response) => {
|
||||
cy.visitAndWaitForUserSideEffects(response.body.current_state_path);
|
||||
});
|
||||
});
|
||||
|
||||
it('shows full date', () => {
|
||||
cy.findByRole('link', { name: /^seriestest/ }).click();
|
||||
cy.findByRole('heading', { name: "seriestest Series' Articles" });
|
||||
|
||||
cy.url().then((url) => {
|
||||
for (const locale of ['en-US', 'fr-FR', 'es-ES']) {
|
||||
cy.visitWithLocale(url, locale);
|
||||
checkFullDateOnHover('.crayons-story__meta time', { locale });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -30,7 +30,14 @@ describe('Show log in modal', () => {
|
|||
it('should show login modal for article reaction clicks', () => {
|
||||
cy.findAllByText('Test article').last().click();
|
||||
|
||||
cy.findByLabelText('reaction-drawer-trigger').last().trigger('mouseover');
|
||||
// Wait for reactions' async setup to complete/show the reaction counts
|
||||
cy.findByLabelText('reaction-drawer-trigger')
|
||||
.as('reactionDrawerButton')
|
||||
.within(() => {
|
||||
cy.get('.crayons-reaction__count').should('have.text', '0');
|
||||
});
|
||||
|
||||
cy.get('@reactionDrawerButton').trigger('mouseover');
|
||||
cy.findByRole('button', { name: 'Like' }).as('heartReaction');
|
||||
cy.findByRole('button', { name: 'Unicorn' }).as('unicornReaction');
|
||||
cy.findByRole('button', { name: 'Add to reading list' }).as(
|
||||
|
|
|
|||
24
cypress/support/assertions.js
Normal file
24
cypress/support/assertions.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
function assertions(_chai, _utils) {
|
||||
_chai.Assertion.addMethod('within_viewport', function withinViewport() {
|
||||
const elements = this._obj;
|
||||
|
||||
cy.window().then((window) => {
|
||||
const viewportRight = window.innerWidth;
|
||||
const viewportBottom = window.innerHeight;
|
||||
|
||||
const bounds = elements[0].getBoundingClientRect();
|
||||
|
||||
this.assert(
|
||||
bounds.top < viewportBottom &&
|
||||
bounds.bottom > 0 &&
|
||||
bounds.left < viewportRight &&
|
||||
bounds.right > 0,
|
||||
'expected #{this} to be within the viewport',
|
||||
'expected #{this} to not be within the viewport',
|
||||
this._obj,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
chai.use(assertions);
|
||||
|
|
@ -99,6 +99,42 @@ Cypress.Commands.add('loginUser', ({ email, password }) => {
|
|||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Visits a URL with the specified locale. Useful for localisation testing.
|
||||
*
|
||||
* @param url {string} The url to visit
|
||||
* @param locale {string} A valid locale to mock
|
||||
*/
|
||||
Cypress.Commands.add('visitWithLocale', (url, locale) => {
|
||||
cy.visit(url, {
|
||||
onBeforeLoad: (window) => {
|
||||
Object.defineProperty(window.navigator, 'language', { value: locale });
|
||||
Object.defineProperty(window.navigator, 'languages', {
|
||||
value: [locale],
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Visits a URL as a "proper" mobile device, mocking the user agent and not just
|
||||
* the viewport.
|
||||
*
|
||||
* @param url {string} The url to visit
|
||||
*/
|
||||
Cypress.Commands.add('visitOnMobile', (url) => {
|
||||
cy.viewport('iphone-x');
|
||||
cy.visit(url, {
|
||||
onBeforeLoad: (window) => {
|
||||
Object.defineProperty(window.navigator, 'userAgent', {
|
||||
value:
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Mobile/15E148 Safari/604.1',
|
||||
});
|
||||
Object.defineProperty(window.navigator, 'platform', { value: 'iPhone' });
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Logs in a creator with the given name, username, email, and password.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ import '@testing-library/cypress/add-commands';
|
|||
import 'cypress-file-upload';
|
||||
import 'cypress-failed-log';
|
||||
|
||||
// Custom assertions
|
||||
import './assertions';
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands';
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue