From 80096f63a7fde83f2920f65334493f2bc5ae4086 Mon Sep 17 00:00:00 2001 From: Suzanne Aitchison Date: Fri, 13 Aug 2021 04:55:43 +0100 Subject: [PATCH] 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 --- .../javascripts/utilities/buildArticleHTML.js | 86 ++++++------ .../stylesheets/profile-preview-card.scss | 3 + .../__snapshots__/Article.test.jsx.snap | 128 +++++++++++++++++- app/javascript/articles/components/Meta.jsx | 12 +- app/javascript/packs/feedPreviewCards.jsx | 6 + .../previewCards/feedPreviewCards.jsx | 115 ++++++++++++++++ .../MinimalProfilePreviewCard.jsx | 61 +++++++++ .../profilePreviewCards/UserMetadata.jsx | 70 ++++++++++ app/views/articles/_single_story.html.erb | 39 +++++- app/views/articles/index.html.erb | 4 +- app/views/collections/show.html.erb | 5 +- .../shared/_profile_card_content.html.erb | 8 +- app/views/stories/_main_stories_feed.html.erb | 2 +- .../stories/articles_search/index.html.erb | 2 +- .../stories/tagged_articles/index.html.erb | 4 +- app/views/users/show.html.erb | 4 +- cypress/fixtures/users/seriesUser.json | 5 + .../articleFlows/followAuthor.spec.js | 6 +- .../articleFlows/previewProfile.spec.js | 11 +- .../previewProfileFromSeries.spec.js | 42 ++++++ .../homeFeedFlows/profilePreviewCards.spec.js | 37 +++++ .../loggedOutFlows/homeFeed.spec.js | 35 +++++ .../loggedOutFlows/tagIndex.spec.js | 36 +++++ ...reviewProfileFromPostSearchResults.spec.js | 38 ++++++ .../previewProfileFromTagIndex.spec.js | 38 ++++++ spec/support/seeds/seeds_e2e.rb | 73 ++++++++++ .../search/display_articles_search_spec.rb | 1 - 27 files changed, 803 insertions(+), 68 deletions(-) create mode 100644 app/javascript/packs/feedPreviewCards.jsx create mode 100644 app/javascript/previewCards/feedPreviewCards.jsx create mode 100644 app/javascript/profilePreviewCards/MinimalProfilePreviewCard.jsx create mode 100644 app/javascript/profilePreviewCards/UserMetadata.jsx create mode 100644 cypress/fixtures/users/seriesUser.json create mode 100644 cypress/integration/seededFlows/articleFlows/seriesFlows/previewProfileFromSeries.spec.js create mode 100644 cypress/integration/seededFlows/homeFeedFlows/profilePreviewCards.spec.js create mode 100644 cypress/integration/seededFlows/loggedOutFlows/homeFeed.spec.js create mode 100644 cypress/integration/seededFlows/loggedOutFlows/tagIndex.spec.js create mode 100644 cypress/integration/seededFlows/searchFlows/previewProfileFromPostSearchResults.spec.js create mode 100644 cypress/integration/seededFlows/tagsFlows/previewProfileFromTagIndex.spec.js diff --git a/app/assets/javascripts/utilities/buildArticleHTML.js b/app/assets/javascripts/utilities/buildArticleHTML.js index eb40961cf..6b9325066 100644 --- a/app/assets/javascripts/utilities/buildArticleHTML.js +++ b/app/assets/javascripts/utilities/buildArticleHTML.js @@ -172,44 +172,54 @@ function buildArticleHTML(article) { } } - var meta = - '
\ - \ -
\ -

\ - ' + - filterXSS(article.user.name) + - '\ - ' + - forOrganization + - '\ -

\ - \ - ' + - publishDate + - '\ - \ -
\ -
'; + // We only show profile preview cards for Posts + var isArticle = article.class_name === 'Article'; + + var previewCardContent = ` +
+
+ + + +
+
+ `; + + var meta = ` +
+ +
+

+ ${filterXSS(article.user.name)} + ${ + isArticle + ? `

` + : '' + } + ${forOrganization} +

+ ${publishDate} +
+
+ `; var bodyTextSnippet = ''; var searchSnippetHTML = ''; diff --git a/app/assets/stylesheets/profile-preview-card.scss b/app/assets/stylesheets/profile-preview-card.scss index abe9a5525..49a46ab51 100644 --- a/app/assets/stylesheets/profile-preview-card.scss +++ b/app/assets/stylesheets/profile-preview-card.scss @@ -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 { diff --git a/app/javascript/articles/__tests__/__snapshots__/Article.test.jsx.snap b/app/javascript/articles/__tests__/__snapshots__/Article.test.jsx.snap index f5f0f8bab..9037a9c66 100644 --- a/app/javascript/articles/__tests__/__snapshots__/Article.test.jsx.snap +++ b/app/javascript/articles/__tests__/__snapshots__/Article.test.jsx.snap @@ -59,11 +59,73 @@ Object {

Stella Macejkovic +

-<%= javascript_packs_with_chunks_tag "searchParams", "storiesList", "followButtons", defer: true %> +<%= javascript_packs_with_chunks_tag "searchParams", "storiesList", "followButtons", "feedPreviewCards", defer: true %> diff --git a/app/views/stories/tagged_articles/index.html.erb b/app/views/stories/tagged_articles/index.html.erb index 7275b8bbb..e1a5eefb3 100644 --- a/app/views/stories/tagged_articles/index.html.erb +++ b/app/views/stories/tagged_articles/index.html.erb @@ -43,7 +43,7 @@ data-requires-approval="<%= @tag_model.requires_approval %>" data-articles-since="<%= Timeframe.datetime_iso8601(params[:timeframe]) %>"> <%= render "stories/tagged_articles/sidebar" %> -
+
+ -<%= javascript_packs_with_chunks_tag "storiesList", "followButtons", defer: true %> +<%= javascript_packs_with_chunks_tag "storiesList", "followButtons", "feedPreviewCards", defer: true %> diff --git a/cypress/fixtures/users/seriesUser.json b/cypress/fixtures/users/seriesUser.json new file mode 100644 index 000000000..a53a2c4f7 --- /dev/null +++ b/cypress/fixtures/users/seriesUser.json @@ -0,0 +1,5 @@ +{ + "username": "series_user", + "email": "series-user@forem.local", + "password": "password" +} diff --git a/cypress/integration/seededFlows/articleFlows/followAuthor.spec.js b/cypress/integration/seededFlows/articleFlows/followAuthor.spec.js index 225aad193..d59f85921 100644 --- a/cypress/integration/seededFlows/articleFlows/followAuthor.spec.js +++ b/cypress/integration/seededFlows/articleFlows/followAuthor.spec.js @@ -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' }); }); diff --git a/cypress/integration/seededFlows/articleFlows/previewProfile.spec.js b/cypress/integration/seededFlows/articleFlows/previewProfile.spec.js index 0a6ea8450..038fb9d1d 100644 --- a/cypress/integration/seededFlows/articleFlows/previewProfile.spec.js +++ b/cypress/integration/seededFlows/articleFlows/previewProfile.spec.js @@ -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]'); }); }); diff --git a/cypress/integration/seededFlows/articleFlows/seriesFlows/previewProfileFromSeries.spec.js b/cypress/integration/seededFlows/articleFlows/seriesFlows/previewProfileFromSeries.spec.js new file mode 100644 index 000000000..12a3bccb0 --- /dev/null +++ b/cypress/integration/seededFlows/articleFlows/seriesFlows/previewProfileFromSeries.spec.js @@ -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' }); + }); + }); +}); diff --git a/cypress/integration/seededFlows/homeFeedFlows/profilePreviewCards.spec.js b/cypress/integration/seededFlows/homeFeedFlows/profilePreviewCards.spec.js new file mode 100644 index 000000000..ab23195e4 --- /dev/null +++ b/cypress/integration/seededFlows/homeFeedFlows/profilePreviewCards.spec.js @@ -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' }); + }); + }); +}); diff --git a/cypress/integration/seededFlows/loggedOutFlows/homeFeed.spec.js b/cypress/integration/seededFlows/loggedOutFlows/homeFeed.spec.js new file mode 100644 index 000000000..0350f7a9f --- /dev/null +++ b/cypress/integration/seededFlows/loggedOutFlows/homeFeed.spec.js @@ -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', + }); + }); +}); diff --git a/cypress/integration/seededFlows/loggedOutFlows/tagIndex.spec.js b/cypress/integration/seededFlows/loggedOutFlows/tagIndex.spec.js new file mode 100644 index 000000000..6030032d9 --- /dev/null +++ b/cypress/integration/seededFlows/loggedOutFlows/tagIndex.spec.js @@ -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', + }); + }); +}); diff --git a/cypress/integration/seededFlows/searchFlows/previewProfileFromPostSearchResults.spec.js b/cypress/integration/seededFlows/searchFlows/previewProfileFromPostSearchResults.spec.js new file mode 100644 index 000000000..2b3328d6b --- /dev/null +++ b/cypress/integration/seededFlows/searchFlows/previewProfileFromPostSearchResults.spec.js @@ -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' }); + }); + }); +}); diff --git a/cypress/integration/seededFlows/tagsFlows/previewProfileFromTagIndex.spec.js b/cypress/integration/seededFlows/tagsFlows/previewProfileFromTagIndex.spec.js new file mode 100644 index 000000000..6ccbba1cc --- /dev/null +++ b/cypress/integration/seededFlows/tagsFlows/previewProfileFromTagIndex.spec.js @@ -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' }); + }); + }); +}); diff --git a/spec/support/seeds/seeds_e2e.rb b/spec/support/seeds/seeds_e2e.rb index d14d0c8fe..753db89f6 100644 --- a/spec/support/seeds/seeds_e2e.rb +++ b/spec/support/seeds/seeds_e2e.rb @@ -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)}", diff --git a/spec/system/search/display_articles_search_spec.rb b/spec/system/search/display_articles_search_spec.rb index f8bfdc19c..4d5c33134 100644 --- a/spec/system/search/display_articles_search_spec.rb +++ b/spec/system/search/display_articles_search_spec.rb @@ -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