Add author profile preview cards to feed (#14340)

* Add the preview card to logged out feed initial content

* initialise the initial dropdowns added on the logged out feed

* minor tweak to selector

* flip the follow button and the summary

* add minimal preview card to build article HTML

* WIP: data fetched an inserted into card on logged out feed

* WIP: cards added to logged in feed

* create separate profile preview card component

* small style tweak, import pack on each page that shows feed cards

* rename

* tweak some styling issues

* make sure follow buttons init in cards

* populate all matching metadata placeholders after fetch

* don't render full preview card upfront on logged out feed

* refactors from PR comments

* fix issue in person search results

* remove check for article author link that will be superseded by cypress test for preview card

* Revert "remove check for article author link that will be superseded by cypress test for preview card"

This reverts commit 9b42804ffd0f051891c87293d0b791ed2bb0367f.

* Revert "fix issue in person search results"

This reverts commit 04941e3520c0895212141193b60f2933faed5ca1.

* only show the preview cards on story cards for Posts (not users etc in search results)

* correct display on collections view

* remove link check that will be replaced by cypress test

* tweaks to small issues, add a test for the logged out feed

* add tests for logged in home feed, logged out tag index

* add search test and tag index logged in test

* fixes to preview profile spec

* tweak to followauthor spec

* add cypress test for preview on series page

* use a unique test user for series test

* correct the jsdoc comments

* tweaks following PR review

* allow feed preview cards to reposition

* move to separate file from pack
This commit is contained in:
Suzanne Aitchison 2021-08-13 04:55:43 +01:00 committed by GitHub
parent a041e5996e
commit 80096f63a7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 803 additions and 68 deletions

View file

@ -172,44 +172,54 @@ function buildArticleHTML(article) {
}
}
var meta =
'<div class="crayons-story__meta">\
<div class="crayons-story__author-pic">\
' +
organizationLogo +
'\
<a href="/' +
profileUsername +
'" class="crayons-avatar ' +
organizationClasses +
'">\
<img src="' +
picUrl +
'" alt="' +
profileUsername +
' profile" class="crayons-avatar__image" loading="lazy" />\
</a>\
</div>\
<div>\
<p>\
<a href="/' +
profileUsername +
'" class="crayons-story__secondary fw-medium">' +
filterXSS(article.user.name) +
'</a>\
' +
forOrganization +
'\
</p>\
<a href="' +
article.path +
'" class="crayons-story__tertiary fs-xs">\
' +
publishDate +
'\
</a>\
</div>\
</div>';
// We only show profile preview cards for Posts
var isArticle = article.class_name === 'Article';
var previewCardContent = `
<div id="story-author-preview-content-${article.id}" class="profile-preview-card__content crayons-dropdown" data-repositioning-dropdown="true" style="border-top: var(--su-7) solid var(--card-color);" data-testid="profile-preview-card">
<div class="gap-4 grid">
<div class="-mt-4">
<a href="/${profileUsername}" class="flex">
<span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0">
<img src="${picUrl}" class="crayons-avatar__image" alt="" loading="lazy" />
</span>
<span class="crayons-link crayons-subtitle-2 mt-5">${article.user.name}</span>
</a>
</div>
<div class="print-hidden">
<button class="crayons-btn follow-action-button whitespace-nowrap follow-user w-100" data-info='{"id": ${article.user_id}, "className": "User", "style": "full"}'>Follow</button>
</div>
<div class="author-preview-metadata-container" data-author-id="${article.user_id}"></div>
</div>
</div>
`;
var meta = `
<div class="crayons-story__meta">
<div class="crayons-story__author-pic">
${organizationLogo}
<a href="/${profileUsername}" class="crayons-avatar ${organizationClasses}">
<img src="${picUrl}" alt="${profileUsername} profile" class="crayons-avatar__image" loading="lazy" />
</a>
</div>
<div>
<p>
<a href="/${profileUsername}" class="crayons-story__secondary fw-medium ${
isArticle ? 'm:hidden' : ''
}">${filterXSS(article.user.name)}</a>
${
isArticle
? `<div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"><button id="story-author-preview-trigger-${article.id}" aria-controls="story-author-preview-content-${article.id}" class="profile-preview-card__trigger fs-s crayons-btn crayons-btn--ghost p-1 -ml-1" aria-label="${article.user.name} profile details">${article.user.name}</button>${previewCardContent}</div>`
: ''
}
${forOrganization}
</p>
<a href="${
article.path
}" class="crayons-story__tertiary fs-xs">${publishDate}</a>
</div>
</div>
`;
var bodyTextSnippet = '';
var searchSnippetHTML = '';

View file

@ -1,9 +1,12 @@
.profile-preview-card {
&__content.crayons-dropdown {
transition: border, border-top;
transition-duration: 300ms;
color: var(--base-100);
padding-top: 0;
top: 100%;
left: 0;
font-size: var(--fs-base);
}
&__content.crayons-dropdown:hover {

View file

@ -59,11 +59,73 @@ Object {
<div>
<p>
<a
class="crayons-story__secondary fw-medium"
class="crayons-story__secondary fw-medium m:hidden"
href="/Emil99"
>
Stella Macejkovic
</a>
<div
class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"
>
<button
aria-controls="story-author-preview-content-62407"
aria-label="Stella Macejkovic profile details"
class="profile-preview-card__trigger fs-s p-1 crayons-btn crayons-btn--ghost -ml-1"
id="story-author-preview-trigger-62407"
>
Stella Macejkovic
</button>
<div
class="profile-preview-card__content crayons-dropdown"
data-repositioning-dropdown="true"
data-testid="profile-preview-card"
id="story-author-preview-content-62407"
style=""
>
<div
class="gap-4 grid"
>
<div
class="-mt-4"
>
<a
class="flex"
href="/Emil99"
>
<span
class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"
>
<img
alt=""
class="crayons-avatar__image"
loading="lazy"
src="/images/10.png"
/>
</span>
<span
class="crayons-link crayons-subtitle-2 mt-5"
>
Stella Macejkovic
</span>
</a>
</div>
<div
class="print-hidden"
>
<button
class="crayons-btn follow-action-button whitespace-nowrap follow-user w-100"
data-info="{\\"id\\":23289,\\"className\\":\\"User\\",\\"style\\":\\"full\\"}"
>
Follow
</button>
</div>
<div
class="author-preview-metadata-container"
data-author-id="23289"
/>
</div>
</div>
</div>
</p>
<a
class="crayons-story__tertiary fs-xs"
@ -245,11 +307,73 @@ Object {
<div>
<p>
<a
class="crayons-story__secondary fw-medium"
class="crayons-story__secondary fw-medium m:hidden"
href="/Emil99"
>
Stella Macejkovic
</a>
<div
class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"
>
<button
aria-controls="story-author-preview-content-62407"
aria-label="Stella Macejkovic profile details"
class="profile-preview-card__trigger fs-s p-1 crayons-btn crayons-btn--ghost -ml-1"
id="story-author-preview-trigger-62407"
>
Stella Macejkovic
</button>
<div
class="profile-preview-card__content crayons-dropdown"
data-repositioning-dropdown="true"
data-testid="profile-preview-card"
id="story-author-preview-content-62407"
style=""
>
<div
class="gap-4 grid"
>
<div
class="-mt-4"
>
<a
class="flex"
href="/Emil99"
>
<span
class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"
>
<img
alt=""
class="crayons-avatar__image"
loading="lazy"
src="/images/10.png"
/>
</span>
<span
class="crayons-link crayons-subtitle-2 mt-5"
>
Stella Macejkovic
</span>
</a>
</div>
<div
class="print-hidden"
>
<button
class="crayons-btn follow-action-button whitespace-nowrap follow-user w-100"
data-info="{\\"id\\":23289,\\"className\\":\\"User\\",\\"style\\":\\"full\\"}"
>
Follow
</button>
</div>
<div
class="author-preview-metadata-container"
data-author-id="23289"
/>
</div>
</div>
</div>
</p>
<a
class="crayons-story__tertiary fs-xs"

View file

@ -3,12 +3,14 @@ import {
articlePropTypes,
organizationPropType,
} from '../../common-prop-types';
import { MinimalProfilePreviewCard } from '../../profilePreviewCards/MinimalProfilePreviewCard';
import { PublishDate } from './PublishDate';
export const Meta = ({ article, organization }) => {
const orgArticleIndexClassAbsent = !document.getElementById(
'organization-article-index',
);
return (
<div className="crayons-story__meta">
<div className="crayons-story__author-pic">
@ -45,7 +47,7 @@ export const Meta = ({ article, organization }) => {
<p>
<a
href={`/${article.user.username}`}
className="crayons-story__secondary fw-medium"
className="crayons-story__secondary fw-medium m:hidden"
>
{filterXSS(
article.class_name === 'User'
@ -53,6 +55,14 @@ export const Meta = ({ article, organization }) => {
: article.user.name,
)}
</a>
<MinimalProfilePreviewCard
triggerId={`story-author-preview-trigger-${article.id}`}
contentId={`story-author-preview-content-${article.id}`}
username={article.user.username}
name={article.user.name}
profileImage={article.user.profile_image_90}
userId={article.user_id}
/>
{organization &&
!document.getElementById('organization-article-index') && (
<span>

View file

@ -0,0 +1,6 @@
import('../previewCards/feedPreviewCards').then(
({ initializeFeedPreviewCards, listenForHoveredOrFocusedStoryCards }) => {
initializeFeedPreviewCards();
listenForHoveredOrFocusedStoryCards();
},
);

View file

@ -0,0 +1,115 @@
import { h, render } from 'preact';
import { UserMetadata } from '../profilePreviewCards/UserMetadata';
import {
initializeDropdown,
getDropdownRepositionListener,
} from '@utilities/dropdownUtils';
import { request } from '@utilities/http/request';
const cachedAuthorMetadata = {};
async function populateMissingMetadata(metadataPlaceholder) {
const { authorId, fetched } = metadataPlaceholder.dataset;
// If the metadata is already being fetched, do nothing
if (fetched) {
return;
}
metadataPlaceholder.dataset.fetched = 'true';
const previouslyFetchedAuthorMetadata = cachedAuthorMetadata[authorId];
if (previouslyFetchedAuthorMetadata) {
renderMetadata(previouslyFetchedAuthorMetadata, metadataPlaceholder);
} else {
const response = await request(`/profile_preview_cards/${authorId}`);
const authorMetadata = await response.json();
cachedAuthorMetadata[authorId] = authorMetadata;
renderMetadata(authorMetadata, metadataPlaceholder);
}
}
function renderMetadata(metadata, placeholder) {
const container = placeholder.parentElement;
render(<UserMetadata {...metadata} />, container, placeholder);
container
.closest('.profile-preview-card__content')
.style.setProperty('--card-color', metadata.card_color);
}
function checkForPreviewCardDetails(event) {
const { target } = event;
if (target.classList.contains('profile-preview-card__trigger')) {
const metadataPlaceholder = target.parentElement.getElementsByClassName(
'author-preview-metadata-container',
)[0];
if (metadataPlaceholder) {
// User is within one of the story cards - and the metadata has not been fetched yet
populateMissingMetadata(metadataPlaceholder);
}
}
}
export function listenForHoveredOrFocusedStoryCards() {
const mainContent = document.getElementById('main-content');
mainContent.addEventListener('mouseover', checkForPreviewCardDetails);
mainContent.addEventListener('focusin', checkForPreviewCardDetails);
}
export function initializeFeedPreviewCards() {
// Select all preview card triggers that haven't already been initialized
const allPreviewCardTriggers = document.querySelectorAll(
'button[id^=story-author-preview-trigger]:not([data-initialized])',
);
for (const previewTrigger of allPreviewCardTriggers) {
const dropdownContentId = previewTrigger.getAttribute('aria-controls');
const dropdownElement = document.getElementById(dropdownContentId);
if (dropdownElement) {
initializeDropdown({
triggerElementId: previewTrigger.id,
dropdownContentId,
onOpen: () => dropdownElement?.classList.add('showing'),
onClose: () => dropdownElement?.classList.remove('showing'),
});
previewTrigger.dataset.initialized = true;
}
}
}
const observer = new MutationObserver((mutationsList) => {
mutationsList.forEach((mutation) => {
if (mutation.type === 'childList') {
initializeFeedPreviewCards();
}
});
});
if (document.getElementById('index-container')) {
observer.observe(document.getElementById('index-container'), {
childList: true,
subtree: true,
});
}
// Preview card dropdowns reposition on scroll
const dropdownRepositionListener = getDropdownRepositionListener();
document.addEventListener('scroll', dropdownRepositionListener);
InstantClick.on('change', () => {
observer.disconnect();
document.removeEventListener('scroll', dropdownRepositionListener);
});
window.addEventListener('beforeunload', () => {
observer.disconnect();
document.removeEventListener('scroll', dropdownRepositionListener);
});

View file

@ -0,0 +1,61 @@
import { h } from 'preact';
export const MinimalProfilePreviewCard = ({
triggerId,
contentId,
username,
name,
profileImage,
userId,
}) => (
<div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block">
<button
id={triggerId}
aria-controls={contentId}
class="profile-preview-card__trigger fs-s p-1 crayons-btn crayons-btn--ghost -ml-1"
aria-label={`${name} profile details`}
>
{name}
</button>
<div
id={contentId}
class="profile-preview-card__content crayons-dropdown"
style="border-top: var(--su-7) solid var(--card-color);"
data-repositioning-dropdown="true"
data-testid="profile-preview-card"
>
<div class="gap-4 grid">
<div class="-mt-4">
<a href={`/${username}`} class="flex">
<span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0">
<img
src={profileImage}
class="crayons-avatar__image"
alt=""
loading="lazy"
/>
</span>
<span class="crayons-link crayons-subtitle-2 mt-5">{name}</span>
</a>
</div>
<div class="print-hidden">
<button
class="crayons-btn follow-action-button whitespace-nowrap follow-user w-100"
data-info={JSON.stringify({
id: userId,
className: 'User',
style: 'full',
})}
>
Follow
</button>
</div>
<div
class="author-preview-metadata-container"
data-author-id={userId}
/>
</div>
</div>
</div>
);

View file

@ -0,0 +1,70 @@
import { h, Fragment } from 'preact';
import { memo } from 'preact/compat';
/**
* Component which renders the user metadata detail in a profile preview card.
*
* @param {object} props
* @param {string} props.email The user's email (if set to be publicly displayed)
* @param {string} props.location The user's location
* @param {string} props.created_at The user's join date string
* @param {string} props.education The user's education detail
* @param {string} props.work The user's work details
*/
export const UserMetadata = memo(
({ email, location, summary, created_at, education, work }) => {
const joinedOnDate = new Date(created_at);
const joinedOnDateString = new Intl.DateTimeFormat(
navigator.language || 'default',
{
day: 'numeric',
month: 'long',
year: 'numeric',
},
).format(joinedOnDate);
return (
<Fragment>
{summary && <div className="color-base-70">{summary}</div>}
<div className="user-metadata-details">
<ul class="user-metadata-details-inner">
{email && (
<li>
<div class="key">Email</div>
<div class="value">
<a href={`mailto:${email}`}>{email}</a>
</div>
</li>
)}
{work && (
<li>
<div className="key">Work</div>
<div className="value">{work}</div>
</li>
)}
{location && (
<li>
<div class="key">Location</div>
<div class="value">{location}</div>
</li>
)}
{education && (
<li>
<div class="key">Education</div>
<div class="value">{education}</div>
</li>
)}
<li>
<div class="key">Joined</div>
<div class="value">
<time datetime={created_at} class="date">
{joinedOnDateString}
</time>
</div>
</li>
</ul>
</div>
</Fragment>
);
},
);

View file

@ -35,9 +35,46 @@
</div>
<div>
<p>
<a href="/<%= story.cached_user.username %>" class="crayons-story__secondary fw-medium">
<a href="/<%= story.cached_user.username %>" class="crayons-story__secondary fw-medium m:hidden">
<%= story.cached_user.name %>
</a>
<div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block">
<button id="story-author-preview-trigger-<%= story.id %>" aria-controls="story-author-preview-content-<%= story.id %>" class="profile-preview-card__trigger fs-s p-1 -ml-1 crayons-btn crayons-btn--ghost" aria-label="<%= story.cached_user.name %> profile details">
<%= story.cached_user.name %>
</button>
<div
id="story-author-preview-content-<%= story.id %>"
class="profile-preview-card__content crayons-dropdown"
style="border-top: var(--su-7) solid var(--card-color);"
data-repositioning-dropdown="true"
data-testid="profile-preview-card">
<div class="gap-4 grid">
<div class="-mt-4">
<a href="/<%= story.cached_user.username %>" class="flex">
<span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0">
<img
src="<%= Images::Profile.call(story.cached_user.profile_image_url, length: 90) %>"
class="crayons-avatar__image"
alt=""
loading="lazy" />
</span>
<span class="crayons-link crayons-subtitle-2 mt-5"><%= story.cached_user.name %></span>
</a>
</div>
<div class="print-hidden">
<button
class="crayons-btn follow-action-button whitespace-nowrap follow-user w-100"
data-info='{"id": <%= story.user_id %>, "className": "User", "style": "full"}'>
Follow
</button>
</div>
<div
class="author-preview-metadata-container"
data-author-id="<%= story.user_id %>"></div>
</div>
</div>
</div>
<% if story.cached_organization && !@organization_article_index %>
<span>
<span class="crayons-story__tertiary fw-normal"> for </span>

View file

@ -35,7 +35,7 @@
<%= render "articles/sidebar" %>
<%# BEGIN Feed menu bar %>
<main class="articles-list crayons-layout__content" id="main-content">
<main class="articles-list crayons-layout__content" id="main-content" data-follow-button-container="true">
<%= render(partial: "onboardings/task_card") if user_signed_in? %>
<header class="flex justify-between items-center p-2 px-3 m:p-0 m:px-0 m:pb-2">
@ -90,5 +90,5 @@
<%= render "articles/sidebar_additional" %>
</div>
<%= javascript_packs_with_chunks_tag "storiesList", defer: true %>
<%= javascript_packs_with_chunks_tag "storiesList", "followButtons", "feedPreviewCards", defer: true %>
<% end %>

View file

@ -5,7 +5,7 @@
<%= render "collections/meta", title_string: title_string, description_string: description_string, path: @collection.path %>
<% end %>
<div class="crayons-layout crayons-layout--1-col">
<main id="main-content" class="crayons-layout crayons-layout--1-col">
<header class="px-2 m:px-0">
<h1><%= title_string %></h1>
<%= link_to "Back to #{@user.name}'s Series", user_series_path(@user.username) %>
@ -17,9 +17,10 @@
<% @articles.each do |article| %>
<%= render "articles/single_story", story: article.decorate, featured: article.main_image.present? %>
<% end %>
<%= javascript_packs_with_chunks_tag "followButtons", "feedPreviewCards", defer: true %>
<% else %>
<p>This series doesn't have any articles</p>
<% end %>
</div>
</div>
</div>
</main>

View file

@ -6,16 +6,16 @@
<span class="crayons-link crayons-subtitle-2 mt-5"><%= actor.name %></span>
</a>
</div>
<div class="print-hidden">
<%= follow_button(actor, style = "", classes = "w-100") %>
</div>
<% if actor.tag_line.present? %>
<div class="color-base-70">
<%= truncate (actor.tag_line || actor.summary || "Posts in this tag"), length: 200 %>
</div>
<% end %>
<div class="print-hidden">
<%= follow_button(actor, style = "", classes = "w-100") %>&nbsp;
</div>
<% if actor.class.name == "User" %>
<div class="user-metadata-details">
<ul class="user-metadata-details-inner">

View file

@ -26,4 +26,4 @@
<% end %>
</div>
<%= javascript_packs_with_chunks_tag "homePageFeedShortcuts", defer: true %>
<%= javascript_packs_with_chunks_tag "homePageFeedShortcuts", "followButtons", "feedPreviewCards", defer: true %>

View file

@ -62,4 +62,4 @@
</div>
</main>
<%= javascript_packs_with_chunks_tag "searchParams", "storiesList", "followButtons", defer: true %>
<%= javascript_packs_with_chunks_tag "searchParams", "storiesList", "followButtons", "feedPreviewCards", defer: true %>

View file

@ -43,7 +43,7 @@
data-requires-approval="<%= @tag_model.requires_approval %>"
data-articles-since="<%= Timeframe.datetime_iso8601(params[:timeframe]) %>">
<%= render "stories/tagged_articles/sidebar" %>
<main class="articles-list" id="main-content">
<main class="articles-list" id="main-content" data-follow-button-container="true">
<header class="flex items-center p-2 m:p-0 m:pb-2" id="on-page-nav-controls">
<button type="button" class="crayons-btn crayons-btn--ghost crayons-btn--icon mr-2 inline-block m:hidden" id="on-page-nav-butt-left" aria-label="nav-button-left">
<div class="crayons-icon nav-icon ">
@ -113,5 +113,5 @@
<%= render "stories/tagged_articles/sidebar_additional" %>
</div>
<%= javascript_packs_with_chunks_tag "storiesList", "followButtons", defer: true %>
<%= javascript_packs_with_chunks_tag "storiesList", "followButtons", "feedPreviewCards", defer: true %>
<% end %>

View file

@ -1,3 +1,4 @@
<main id="main-content">
<% title @user.name %>
<%= render "shared/payment_pointer", user: @user %>
@ -157,5 +158,6 @@
</div>
</main>
</div>
</main>
<%= javascript_packs_with_chunks_tag "storiesList", "followButtons", defer: true %>
<%= javascript_packs_with_chunks_tag "storiesList", "followButtons", "feedPreviewCards", defer: true %>

View file

@ -0,0 +1,5 @@
{
"username": "series_user",
"email": "series-user@forem.local",
"password": "password"
}

View file

@ -5,11 +5,7 @@ describe('Follow author from article sidebar', () => {
cy.fixture('users/articleEditorV1User.json').as('user');
cy.get('@user').then((user) => {
cy.loginAndVisit(user, '/').then(() => {
cy.findAllByRole('link', { name: 'Test article' })
.first()
.click({ force: true });
cy.loginAndVisit(user, '/admin_mcadmin/test-article-slug').then(() => {
cy.get('[data-follow-clicks-initialized]');
cy.findByRole('heading', { name: 'Test article' });
});

View file

@ -37,14 +37,11 @@ describe('Preview user profile from article page', () => {
cy.fixture('users/articleEditorV1User.json').as('user');
cy.get('@user').then((user) => {
cy.loginAndVisit(user, '/').then(() => {
cy.findAllByRole('link', { name: 'Test article' })
.first()
.click({ force: true });
cy.loginAndVisit(user, '/admin_mcadmin/test-article-slug');
cy.get('[data-follow-clicks-initialized]');
cy.findByRole('heading', { name: 'Test article' });
});
// Wait for the page to load
cy.findByRole('button', { name: 'Share post options' });
cy.get('[data-follow-clicks-initialized]');
});
});

View file

@ -0,0 +1,42 @@
describe('Preview profile from series', () => {
beforeEach(() => {
cy.testSetup();
cy.fixture('users/articleEditorV1User.json').as('user');
cy.get('@user').then((user) => {
cy.loginAndVisit(user, '/series_user/series');
});
});
it('shows a preview on a series article card', () => {
// Go to an individual series listing
cy.findByRole('link', { name: 'seriestest (1 Part Series)' }).click();
cy.findByRole('heading', { name: "seriestest Series' Articles" });
// Find the preview button and check it functions as expected
cy.findByRole('button', { name: 'Series User profile details' }).as(
'previewButton',
);
cy.get('@previewButton').should('have.attr', 'data-initialized');
cy.get('@previewButton').should('have.attr', 'aria-expanded', 'false');
cy.get('@previewButton').click();
cy.findByTestId('profile-preview-card').within(() => {
cy.findByRole('link', {
name: 'Series User',
}).should('have.focus');
// Check all the expected user data sections are present
cy.findByText('Series user summary');
cy.findByText('Software developer at Company');
cy.findByText('Edinburgh');
cy.findByText('University of Life');
cy.findByRole('button', { name: 'Follow' }).click();
// Check that the follow button has updated as expected
cy.findByRole('button', { name: 'Follow' }).should('not.exist');
cy.findByRole('button', { name: 'Following' });
});
});
});

View file

@ -0,0 +1,37 @@
describe('Home feed profile preview cards', () => {
beforeEach(() => {
cy.testSetup();
cy.fixture('users/articleEditorV1User.json').as('user');
cy.get('@user').then((user) => {
cy.loginAndVisit(user, '/');
});
});
it("shows a profile preview card for a post's author", () => {
cy.findAllByRole('button', { name: 'Admin McAdmin profile details' })
.first()
.as('previewButton');
cy.get('@previewButton').should('have.attr', 'data-initialized');
cy.get('@previewButton').click();
cy.findAllByTestId('profile-preview-card')
.first()
.within(() => {
cy.findByRole('link', {
name: 'Admin McAdmin',
}).should('have.focus');
// Check all the expected user data sections are present
cy.findByText('Admin user summary');
cy.findByText('Software developer at Company');
cy.findByText('Edinburgh');
cy.findByText('University of Life');
cy.findByRole('button', { name: 'Follow' }).click();
// Check that following status has been updated
cy.findByRole('button', { name: 'Follow' }).should('not.exist');
cy.findByRole('button', { name: 'Following' });
});
});
});

View file

@ -0,0 +1,35 @@
describe('Logged out Home feed', () => {
beforeEach(() => {
cy.testSetup();
cy.visit('/');
});
it("shows a profile preview card for an article's author", () => {
cy.findAllByRole('button', { name: 'Admin McAdmin profile details' })
.first()
.as('previewButton');
cy.get('@previewButton').should('have.attr', 'data-initialized');
cy.get('@previewButton').click();
cy.findAllByTestId('profile-preview-card')
.first()
.within(() => {
cy.findByRole('link', {
name: 'Admin McAdmin',
}).should('have.focus');
// Check all the expected user data sections are present
cy.findByText('Admin user summary');
cy.findByText('Software developer at Company');
cy.findByText('Edinburgh');
cy.findByText('University of Life');
cy.findByRole('button', { name: 'Follow' }).click();
});
// Clicking a follow button while logged out should always trigger the log in to continue modal
cy.findByTestId('modal-container').findByRole('heading', {
name: 'Log in to continue',
});
});
});

View file

@ -0,0 +1,36 @@
describe('Logged out - tag index page', () => {
beforeEach(() => {
cy.testSetup();
cy.visit('/t/tag1');
});
it('shows a preview profile card for posts in the tag feed', () => {
cy.visit('/t/tag1');
cy.findAllByRole('button', { name: 'Admin McAdmin profile details' })
.first()
.as('previewButton');
cy.get('@previewButton').should('have.attr', 'data-initialized');
cy.get('@previewButton').click();
cy.findAllByTestId('profile-preview-card')
.first()
.within(() => {
cy.findByRole('link', {
name: 'Admin McAdmin',
}).should('have.focus');
// Check all the expected user data sections are present
cy.findByText('Admin user summary');
cy.findByText('Software developer at Company');
cy.findByText('Edinburgh');
cy.findByText('University of Life');
cy.findByRole('button', { name: 'Follow' }).click();
});
// Clicking a follow button while logged out should always trigger the log in to continue modal
cy.findByTestId('modal-container').findByRole('heading', {
name: 'Log in to continue',
});
});
});

View file

@ -0,0 +1,38 @@
describe('Preview profile from post search results', () => {
beforeEach(() => {
cy.testSetup();
cy.fixture('users/articleEditorV1User.json').as('user');
cy.get('@user').then((user) => {
cy.loginAndVisit(user, '/search?q=test');
});
});
it('shows profile preview cards on posts in search results', () => {
cy.findAllByRole('button', { name: 'Admin McAdmin profile details' })
.first()
.as('previewButton');
cy.get('@previewButton').should('have.attr', 'data-initialized');
cy.get('@previewButton').click();
cy.findAllByTestId('profile-preview-card')
.first()
.within(() => {
cy.findByRole('link', {
name: 'Admin McAdmin',
}).should('have.focus');
// Check all the expected user data sections are present
cy.findByText('Admin user summary');
cy.findByText('Software developer at Company');
cy.findByText('Edinburgh');
cy.findByText('University of Life');
cy.findByRole('button', { name: 'Follow' }).click();
// Check that following status has been updated
cy.findByRole('button', { name: 'Follow' }).should('not.exist');
cy.findByRole('button', { name: 'Following' });
});
});
});

View file

@ -0,0 +1,38 @@
describe('Preview profile from the tag index page', () => {
beforeEach(() => {
cy.testSetup();
cy.fixture('users/articleEditorV1User.json').as('user');
cy.get('@user').then((user) => {
cy.loginAndVisit(user, '/t/tag1');
});
});
it("shows a profile preview card for a tagged post's author", () => {
cy.findAllByRole('button', { name: 'Admin McAdmin profile details' })
.first()
.as('previewButton');
cy.get('@previewButton').should('have.attr', 'data-initialized');
cy.get('@previewButton').click();
cy.findAllByTestId('profile-preview-card')
.first()
.within(() => {
cy.findByRole('link', {
name: 'Admin McAdmin',
}).should('have.focus');
// Check all the expected user data sections are present
cy.findByText('Admin user summary');
cy.findByText('Software developer at Company');
cy.findByText('Edinburgh');
cy.findByText('University of Life');
cy.findByRole('button', { name: 'Follow' }).click();
// Check that following status has been updated
cy.findByRole('button', { name: 'Follow' }).should('not.exist');
cy.findByRole('button', { name: 'Following' });
});
});
});

View file

@ -379,6 +379,57 @@ end
##############################################################################
seeder.create_if_doesnt_exist(User, "email", "series-user@forem.com") do
series_user = User.create!(
name: "Series User",
email: "series-user@forem.local",
username: "series_user",
summary: Faker::Lorem.paragraph_by_chars(number: 199, supplemental: false),
profile_image: File.open(Rails.root.join("app/assets/images/#{rand(1..40)}.png")),
website_url: Faker::Internet.url,
confirmed_at: Time.current,
password: "password",
password_confirmation: "password",
saw_onboarding: true,
checked_code_of_conduct: true,
checked_terms_and_conditions: true,
)
series_user.profile.update(
summary: "Series user summary",
work: "Software developer at Company",
location: "Edinburgh",
education: "University of Life",
)
series_user.notification_setting.update(
email_comment_notifications: false,
email_follower_notifications: false,
)
end
##############################################################################
seeder.create_if_doesnt_exist(Article, "title", "Series test article") do
markdown = <<~MARKDOWN
---
title: Series test article
published: true
cover_image: #{Faker::Company.logo}
series: seriestest
---
#{Faker::Hipster.paragraph(sentence_count: 2)}
#{Faker::Markdown.random}
#{Faker::Hipster.paragraph(sentence_count: 2)}
MARKDOWN
Article.create(
body_markdown: markdown,
featured: true,
show_comments: true,
user_id: User.find_by(email: "series-user@forem.local").id,
)
end
##############################################################################
seeder.create_if_none(ListingCategory) do
ListingCategory.create!(
slug: "cfp",
@ -426,6 +477,28 @@ Settings::General.sidebar_tags = %i[tag1]
##############################################################################
seeder.create_if_doesnt_exist(Article, "title", "Tag test article") do
markdown = <<~MARKDOWN
---
title: Tag test article
published: true
cover_image: #{Faker::Company.logo}
tags: tag1
---
#{Faker::Hipster.paragraph(sentence_count: 2)}
#{Faker::Markdown.random}
#{Faker::Hipster.paragraph(sentence_count: 2)}
MARKDOWN
Article.create(
body_markdown: markdown,
featured: true,
show_comments: true,
user_id: admin_user.id,
)
end
##############################################################################
seeder.create_if_none(Badge) do
Badge.create!(
title: "#{Faker::Lorem.word} #{rand(100)}",

View file

@ -28,7 +28,6 @@ RSpec.describe "Display articles search spec", type: :system, js: true do
expect(page).to have_selector("button[data-reactable-id=\"#{found_article_one.id}\"]")
expect(page).to have_content("5 min read")
expect(find_link("#ruby")["href"]).to include("/t/ruby")
expect(find_link(found_article_one.user.name)["href"]).to include(found_article_one.username)
expect(page).to have_content("3 reactions")
expect(page).to have_content("2 comments")
end