diff --git a/app/components/SideBar/Right/index.vue b/app/components/SideBar/Right/index.vue index 9c1a55204..32179c0b8 100644 --- a/app/components/SideBar/Right/index.vue +++ b/app/components/SideBar/Right/index.vue @@ -87,13 +87,17 @@ const { mutate: followUser } = useFollowMutation(); - +
- +
@@ -104,6 +108,7 @@ const { mutate: followUser } = useFollowMutation(); v-if="showWhatIsHappening" :title="$t('rightsidebar.whats-happening.title')" data-test="whats-happening-card" + data-cy="whats-happening-card" >
- + {{ $t('rightsidebar.show-more') }} - + { -

+

{{ $t('explore.hashtag', { category: props.hashtag.category, @@ -25,8 +26,8 @@ const handleClick = () => { }) }}

-

{{ props.hashtag.hashtag }}

-

+

{{ props.hashtag.hashtag }}

+

{{ $n(props.hashtag.tweetsCount, { notation: 'compact' }) }} {{ $t('posts') }}

diff --git a/app/components/ui/SearchField.vue b/app/components/ui/SearchField.vue index 76e32660c..6a3243681 100644 --- a/app/components/ui/SearchField.vue +++ b/app/components/ui/SearchField.vue @@ -200,6 +200,7 @@ onMounted(() => {

diff --git a/app/layouts/explore.vue b/app/layouts/explore.vue index 49c946b88..80f21b6e9 100644 --- a/app/layouts/explore.vue +++ b/app/layouts/explore.vue @@ -36,6 +36,7 @@ const tabs = [ :label="tab.label" :route="tab.route" :is-active="$route.path === tab.route" + :data-cy="`explore-${tab.route.split('/')[2]}-tab`" /> diff --git a/app/layouts/notifications.vue b/app/layouts/notifications.vue index 217d5a34e..b1196fde1 100644 --- a/app/layouts/notifications.vue +++ b/app/layouts/notifications.vue @@ -3,10 +3,12 @@ const tabs = [ { label: $t('notifications.tabs.all'), route: '/notifications', + datacy: 'notifications-all-tab', }, { label: $t('notifications.tabs.mentions'), route: '/notifications/mentions', + datacy: 'notifications-mentions-tab', }, ]; @@ -22,6 +24,7 @@ const tabs = [ :label="tab.label" :route="tab.route" :is-active="$route.path.toLowerCase() === tab.route" + :data-cy="tab.datacy" />

diff --git a/app/layouts/search.vue b/app/layouts/search.vue index c03651c54..ffb9d06ef 100644 --- a/app/layouts/search.vue +++ b/app/layouts/search.vue @@ -72,6 +72,7 @@ const tabs = computed(() => { :label="tab.label" :route="tab.route" :is-active="$route.path === tab.path" + :data-cy="`search-${tab.path.split('/')[2]}-tab`" /> diff --git a/app/pages/explore/[tab].vue b/app/pages/explore/[tab].vue index bd1e95203..0ca1362b7 100644 --- a/app/pages/explore/[tab].vue +++ b/app/pages/explore/[tab].vue @@ -57,7 +57,12 @@ watch( :rank="index" /> -
+

{{ $t('explore.no-trending-hashtags') }}

diff --git a/app/pages/explore/for-you.vue b/app/pages/explore/for-you.vue index fa39ad9a7..ab9d5533d 100644 --- a/app/pages/explore/for-you.vue +++ b/app/pages/explore/for-you.vue @@ -151,7 +151,11 @@ watch( -
+
diff --git a/cypress/e2e/explore/explore.cy.ts b/cypress/e2e/explore/explore.cy.ts new file mode 100644 index 000000000..5eafd5cb9 --- /dev/null +++ b/cypress/e2e/explore/explore.cy.ts @@ -0,0 +1,200 @@ +describe('Explore Page', { testIsolation: false }, function () { + let masterUser: { username: string; email: string; password: string }; + let slaveUser: { username: string; email: string; password: string }; + + before(function () { + cy.clearAllCookies(); + cy.clearAllLocalStorage(); + cy.clearAllSessionStorage(); + cy.createTestUser().then((mUser) => { + masterUser = mUser; + cy.createTestUser().then((sUser) => { + slaveUser = sUser; + cy.wrap(mUser).as('masterUser'); + cy.wrap(sUser).as('slaveUser'); + // Login as master user + cy.login(mUser.email, mUser.password); + cy.loginExternal(sUser.email, sUser.password); + }); + }); + }); + // Restore aliases before each test + beforeEach(function () { + if (masterUser) cy.wrap(masterUser).as('masterUser'); + if (slaveUser) cy.wrap(slaveUser).as('slaveUser'); + cy.visitAndWaitForHydration('/explore'); + }); + describe('Navigation and Items', function () { + it('it should show tabs', function () { + cy.get('a[data-cy="explore-for-you-tab"]').should('exist'); + cy.get('a[data-cy="explore-trending-tab"]').should('exist'); + cy.get('a[data-cy="explore-news-tab"]').should('exist'); + cy.get('a[data-cy="explore-sports-tab"]').should('exist'); + cy.get('a[data-cy="explore-entertainment-tab"]').should('exist'); + }); + it('should show who to follow card with functionality', function () { + cy.get('div[data-cy="who-to-follow-card"]').should('exist'); + cy.get('div[data-cy="who-to-follow-card"]') + .find('[data-cy="user-row"]') + .its('length') + .should('be.gte', 1); + // Check that follow buttons exist + cy.get('div[data-cy="who-to-follow-card"]').within(() => { + cy.get('button[data-cy="profile-follow-button"]').first().should('exist').click(); + cy.get('button[data-cy="profile-unfollow-button"]').first().should('exist'); + }); + }); + it('should show trending and who to follow cards in home page', function () { + cy.visitAndWaitForHydration('/'); + cy.get('div[data-cy="whats-happening-card"]').should('exist'); + cy.get('div[data-cy="who-to-follow-card"]').should('exist'); + cy.get('div[data-cy="who-to-follow-card"]') + .find('[data-cy="user-row"]') + .its('length') + .should('be.gte', 1); + cy.get('div[data-cy="whats-happening-card"]') + .find('[data-cy="explore-hashtag-item"]') + .its('length') + .should('be.gte', 1); + cy.get('div[data-cy="whats-happening-card"]') + .find('button[data-cy="show-more-hashtags-button"]') + .should('exist') + .click(); + cy.url().should('include', '/explore/'); + }); + it('trending tab should be clickable and show hashtags', function () { + cy.get('a[data-cy="explore-trending-tab"]').should('exist').click(); + cy.url().should('include', '/explore/trending'); + cy.get('[data-cy="explore-hashtag-item"]').its('length').should('be.gte', 1); + }); + it('should show for-you tab with hashtags and tweets', function () { + cy.get('a[data-cy="explore-for-you-tab"]').should('exist').click(); + cy.url().should('include', '/explore/for-you'); + cy.get('[data-cy="explore-hashtag-item"]').its('length').should('be.gte', 1); + cy.get('[data-cy="tweet"]').its('length').should('be.gte', 1); + }); + it('should navigate to Search when clicking on hashtag item', function () { + cy.get('[data-cy="explore-hashtag-item"]') + .first() + .within(() => { + cy.get('p[data-cy="explore-hashtag-text"]') + .invoke('text') + .then((text) => { + cy.get('p[data-cy="explore-hashtag-text"]').click(); + cy.url().should('include', `/search/top?q=${text}`); + }); + }); + }); + it('news, sports, entertainment tabs should either show trends or nothing', function () { + const tabs = [ + { name: 'news', selector: 'a[data-cy="explore-news-tab"]' }, + { name: 'sports', selector: 'a[data-cy="explore-sports-tab"]' }, + { name: 'entertainment', selector: 'a[data-cy="explore-entertainment-tab"]' }, + ]; + tabs.forEach((tab) => { + cy.get(tab.selector).should('exist').click(); + cy.url().should('include', `/explore/${tab.name}`); + // either div[data-cy="no-trending-hashtags"] exists or there are hashtag items + cy.get('[data-cy="explore-hashtag-item"], div[data-cy="no-trending-hashtags"]') + .its('length') + .should('be.gte', 1); + }); + }); + }); + describe('Search Functionality', function () { + describe('Profile Search Functionality', () => { + it('should search for profiles correctly', function () { + cy.get('[data-cy="search-input"]').type(this.slaveUser.username); + // Check that search results contain the slave user (it might show multiple results) + cy.get('[data-cy="search-user-result"]').should('contain.text', this.slaveUser.username); + // Click on the slave user result + cy.get('[data-cy="search-user-result"]').contains(this.slaveUser.username).click(); + cy.url().should('include', `/profile/${this.slaveUser.username}`); + cy.get('[data-cy="profile-user-name"]').should('contain.text', this.slaveUser.username); + }); + + it('should show go to profile option for valid usernames', function () { + cy.get('[data-cy="search-input"]').type('gelgel'); + cy.get('[data-cy="search-go-to-profile"]') + .should('exist') + .should('contain.text', 'gelgel') + .click(); + cy.url().should('include', `/profile/gelgel`); + cy.get('[data-cy="profile-user-name"]').should('contain.text', 'gelgel'); + }); + it('should allow "Search for [user]" (top and people)', function () { + cy.get('[data-cy="search-input"]').clear().type(this.slaveUser.username); + cy.get('a[data-cy="search-search-for-text"]').should('contain.text', `Search For`).click(); + // Check that search results contain the slave user + cy.get('[data-cy="main-content"]').within(() => { + cy.get('[data-cy="user-row"]').should('contain.text', this.slaveUser.username); + }); + // Now click on People tab + cy.get('a[data-cy="search-people-tab"]').should('exist').click(); + // Check that search results in People tab also contain the slave user + cy.get('[data-cy="main-content"]').within(() => { + cy.get('[data-cy="user-row"]').should('contain.text', this.slaveUser.username); + }); + }); + }); + describe('Tweets Search Functionality', () => { + it('should search for tweets correctly (top and latest)', function () { + const timestamp = Date.now(); + const tweetContent = `Unique tweet content for search test ${timestamp}`; + cy.postTweet(tweetContent, [], null, null, true).then(() => { + cy.get('[data-cy="search-input"]') + .clear() + .type(`Unique tweet content for search test ${timestamp}`); + cy.get('a[data-cy="search-search-for-text"]') + .should('contain.text', `Search For`) + .click(); + // Check that search results contain the tweet content + cy.get('[data-cy="tweet-content"]').should('contain.text', tweetContent); + // Now click on Latest tab + cy.get('a[data-cy="search-latest-tab"]').should('exist').click(); + // Check that search results in Latest tab also contain the tweet content + cy.get('[data-cy="tweet-content"]').should('contain.text', tweetContent); + }); + }); + it('should show results from followed users', function () { + const timestamp = Date.now(); + const tweetContent = `Followed user tweet content ${timestamp}`; + cy.postTweet(tweetContent, [], null, null, true).then(() => { + cy.get('[data-cy="search-input"]') + .clear() + .type(`Unique tweet content for search test ${timestamp}`); + cy.get('a[data-cy="search-search-for-text"]') + .should('contain.text', `Search For`) + .click(); + cy.get('button[data-cy="search-filter-people-you-follow"]').should('exist').click(); + // Check that search results are empty since master user does not follow slave user yet + cy.get('[data-cy="tweet-content"]').should('not.exist'); + // Now follow the slave user + cy.followUser(this.slaveUser.username, true); + const currentURL = cy.url(); + currentURL.then((url) => { + cy.visit(url); + cy.get('button[data-cy="search-filter-people-you-follow"]').should('exist').click(); + // Check that search results now contain the tweet content + cy.get('[data-cy="tweet-content"]').should('contain.text', tweetContent); + }); + }); + }); + it('should show results for tweets with media', function () { + const timestamp = Date.now(); + const tweetContent = `Media tweet content ${timestamp}`; + cy.uploadMedia('profile/banner.jpg', 'tweets', true).then((media) => { + cy.postTweet(tweetContent, [media.id], undefined, undefined, true).then(() => { + cy.get('[data-cy="search-input"]').clear().type(`Media tweet content ${timestamp}`); + cy.get('a[data-cy="search-search-for-text"]') + .should('contain.text', `Search For`) + .click(); + // switch to Media tab + cy.get('a[data-cy="search-media-tab"]').should('exist').click(); + cy.get('[data-cy="thumbnail-image"]').should('exist'); + }); + }); + }); + }); + }); +}); diff --git a/cypress/e2e/notifications/notifications.cy.ts b/cypress/e2e/notifications/notifications.cy.ts new file mode 100644 index 000000000..34f84534c --- /dev/null +++ b/cypress/e2e/notifications/notifications.cy.ts @@ -0,0 +1,175 @@ +/*tabs: + a 'notifications-all-tab' + a 'notifications-mentions-tab' + */ +/* + data-cy="notification-item" -> + data-cy="notification-like" (has a preview of data-cy="quoted-tweet-content") + data-cy="notification-follow" + data-cy="notification-retweet" (has a preview of data-cy="quoted-tweet-content") + data-cy="notification-reply" + it has a data-cy="tweet" comonent if it's a quoted tweet notification + */ +/* mentions notification has data-cy="tweet" + */ +/* + notification counter is data-cy="left-sidebar-tab-badge-notifications" + */ +describe('Notification Tab', { testIsolation: false }, function () { + let masterUser: { username: string; email: string; password: string }; + let slaveUser: { username: string; email: string; password: string }; + + before(function () { + cy.clearAllCookies(); + cy.clearAllLocalStorage(); + cy.clearAllSessionStorage(); + cy.createTestUser().then((mUser) => { + masterUser = mUser; + cy.createTestUser().then((sUser) => { + slaveUser = sUser; + // Login as master user + cy.login(mUser.email, mUser.password); + cy.loginExternal(sUser.email, sUser.password); + const timestamp = Date.now(); + const shorterUsername = 'notiuser' + String(timestamp).slice(-5); + cy.changeUsername(shorterUsername); + masterUser.username = shorterUsername; + cy.wrap(masterUser).as('masterUser'); + cy.wrap(sUser).as('slaveUser'); + }); + }); + cy.visitAndWaitForHydration('/'); + }); + // Restore aliases before each test + beforeEach(function () { + if (masterUser) cy.wrap(masterUser).as('masterUser'); + if (slaveUser) cy.wrap(slaveUser).as('slaveUser'); + }); + // describe('Notification Items', function () { + // it('should update notifications counter & tabs', function () { + // // Post a tweet as master and like it as slave to generate a notification + // cy.postTweet('Notification test tweet from master user.').then((tweet) => { + // cy.likeTweet(tweet.id, true, true); // like as slave user + // // Check notification badge + // cy.get('span[data-cy="left-sidebar-tab-badge-notifications"]').should('exist').and('contain.text', '1'); + // // Visit notifications page + // cy.visitAndWaitForHydration('/notifications'); + // // Check that "All" tab is active and has the notification + // cy.get('a[data-cy="notifications-all-tab"]').should('be.visible') + // cy.get('a[data-cy="notifications-mentions-tab"]').should('be.visible') + + // // Check that the notification item exists + // cy.get('div[data-cy="notification-item"]').should('exist').within(() => { + // cy.get('[data-cy="notification-like"]').should('exist'); + // // should be one quoted tweet content + // cy.get('[data-cy="quoted-tweet-content"]').its('length').should('eq', 1); + // cy.get('[data-cy="quoted-tweet-content"]').should('contain.text', 'Notification test tweet from master user.'); + // // counter should be gone after viewing + // cy.get('span[data-cy="left-sidebar-tab-badge-notifications"]').should('not.exist'); + // }); + // }); + // }); + // }); + describe('Real-time Notifications', function () { + before(function () { + // Ensure we start from notifications page + cy.visitAndWaitForHydration('/notifications'); + }); + // it('should receive like notifications', function () { + // // Post a tweet as master + // cy.postTweet('Real-time notification test tweet from master user.').then((tweet) => { + // // Like the tweet as slave to trigger notification + // cy.likeTweet(tweet.id, true, true); // like as slave user + // // Check that the notification item appears in real-time + // cy.get('div[data-cy="notification-item"] [data-cy="notification-like"] [data-cy="quoted-tweet-content"]').should('contain.text', 'Real-time notification test tweet from master user.'); + // }); + // }); + it('should receive follow notifications', function () { + // Slave user follows master user to trigger notification + cy.followUser(this.masterUser.username, true, true); // use external token + // Check that the notification item appears in real-time + cy.get('div[data-cy="notification-item"] [data-cy="notification-follow"]') + .should('exist') + .and('contain.text', this.slaveUser.username); + }); + it('should receive reply notifications', function () { + // Post a tweet as master + cy.postTweet('Real-time reply notification test tweet from master user.').then((tweet) => { + // Reply to the tweet as slave to trigger notification + cy.postTweet('This is a reply from slave user.', [], tweet.id, undefined, true); // reply as slave user + // Check that the notification item appears in real-time + cy.get( + 'div[data-cy="notification-item"] [data-cy="notification-reply"] [data-cy="quoted-tweet-content"]', + ).should('contain.text', 'This is a reply from slave user.'); + }); + }); + it('should receive retweet notifications', function () { + // Post a tweet as master + cy.postTweet('Real-time retweet notification test tweet from master user.').then((tweet) => { + // Retweet the tweet as slave to trigger notification + cy.retweetTweet(tweet.id, true, true); // retweet as slave user + // Check that the notification item appears in real-time + cy.get( + 'div[data-cy="notification-item"] [data-cy="notification-retweet"] [data-cy="quoted-tweet-content"]', + ).should('contain.text', 'Real-time retweet notification test tweet from master user.'); + }); + }); + it('should receive quote retweet notifications', function () { + // Post a tweet as master + cy.postTweet('Real-time quote retweet notification test tweet from master user.').then( + (tweet) => { + // Quote retweet the tweet as slave to trigger notification + cy.postTweet('This is a quote retweet from slave user.', [], undefined, tweet.id, true); + // Check that the notification item appears in real-time + cy.get('div[data-cy="notification-item"] [data-cy="tweet"]') + .first() + .within(() => { + cy.get('[data-cy="quoted-tweet-content"]').should( + 'contain.text', + 'Real-time quote retweet notification test tweet from master user.', + ); + cy.get('[data-cy="tweet-content"]').should( + 'contain.text', + 'This is a quote retweet from slave user.', + ); + }); + }, + ); + }); + it('should receive mention notifications', function () { + // in both tabs + // Post a tweet as slave mentioning master to trigger notification + cy.postTweet( + `@${this.masterUser.username} This is a mention from slave user.`, + [], + undefined, + undefined, + true, + ); + // Check that the notification item appears in real-time in "All" tab + cy.get('div[data-cy="notification-item"] [data-cy="tweet"] [data-cy="tweet-content"]').should( + 'contain.text', + `@${this.masterUser.username} This is a mention from slave user.`, + ); + // Switch to "Mentions" tab + cy.get('a[data-cy="notifications-mentions-tab"]').click(); + // Check that the notification item appears in "Mentions" tab + cy.get('[data-cy="tweet"] [data-cy="tweet-content"]').should( + 'contain.text', + `@${this.masterUser.username} This is a mention from slave user.`, + ); + // post another mention to check real-time in mentions tab + cy.postTweet( + `@${this.masterUser.username} Another mention from slave user.`, + [], + undefined, + undefined, + true, + ); + cy.get('[data-cy="tweet"] [data-cy="tweet-content"]').should( + 'contain.text', + `@${this.masterUser.username} Another mention from slave user.`, + ); + }); + }); +}); diff --git a/cypress/e2e/profile/profile-data.cy.ts b/cypress/e2e/profile/profile-data.cy.ts index adc799544..106e0c9bd 100644 --- a/cypress/e2e/profile/profile-data.cy.ts +++ b/cypress/e2e/profile/profile-data.cy.ts @@ -101,28 +101,4 @@ describe('Profile Page Actions', () => { }); }); }); - - describe('Profile Search Functionality', () => { - it('should search for profiles correctly', function () { - cy.visitAndWaitForHydration('/explore/for-you'); - cy.get('[data-cy="search-input"]').type(this.slaveUser.username); - // Check that search results contain the slave user (it might show multiple results) - cy.get('[data-cy="search-user-result"]').should('contain.text', this.slaveUser.username); - // Click on the slave user result - cy.get('[data-cy="search-user-result"]').contains(this.slaveUser.username).click(); - cy.url().should('include', `/profile/${this.slaveUser.username}`); - cy.get('[data-cy="profile-user-name"]').should('contain.text', this.slaveUser.username); - }); - - it('should show go to profile option for valid usernames', function () { - cy.visitAndWaitForHydration('/explore/for-you'); - cy.get('[data-cy="search-input"]').type('gelgel'); - cy.get('[data-cy="search-go-to-profile"]') - .should('exist') - .should('contain.text', 'gelgel') - .click(); - cy.url().should('include', `/profile/gelgel`); - cy.get('[data-cy="profile-user-name"]').should('contain.text', 'gelgel'); - }); - }); }); diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index fd3c3a40d..60bf4771b 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -258,6 +258,23 @@ Cypress.Commands.add( }, ); +Cypress.Commands.add('changeUsername', (newUsername: string, useSlave = false) => { + const tokenCookie = useSlave ? 'external_access_token' : 'access_token'; + cy.getCookie(tokenCookie).then((cookie) => { + cy.request({ + method: 'PATCH', + url: `${Cypress.env('API_URL')}/me/settings/username`, + headers: { + Authorization: `Bearer ${cookie?.value}`, + }, + body: { + newUsername: newUsername, + }, + }).as('changeUsernameRequest'); + cy.get('@changeUsernameRequest').its('status').should('be.oneOf', [200, 201]); + }); +}); + // // -- This is a parent command -- // Cypress.Commands.add('login', (email, password) => { ... }) diff --git a/cypress/support/index.d.ts b/cypress/support/index.d.ts index 2c1e6fb40..162806c88 100644 --- a/cypress/support/index.d.ts +++ b/cypress/support/index.d.ts @@ -128,5 +128,13 @@ declare namespace Cypress { * @example cy.retweetTweet(12345, false, false) // Unretweet tweet */ retweetTweet(tweetId: string | number, retweet?: boolean, useSlave?: boolean): Chainable; + + /** + * Custom command to change username + * @param newUsername - New username to set + * @param useSlave - Whether to use external access token (default: false) + * @example cy.changeUsername('newusername', false) + */ + changeUsername(newUsername: string, useSlave?: boolean): Chainable; } }