followUser({ username, action: 'follow' })"
@unfollow="(username: string) => followUser({ username, action: 'unfollow' })"
/>
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;
}
}