diff --git a/__tests__/groups/group.test.js b/__tests__/groups/group.test.js index f0a108b6..dae5af0f 100644 --- a/__tests__/groups/group.test.js +++ b/__tests__/groups/group.test.js @@ -20,7 +20,7 @@ function resetUserPermission() { describe('Discord Groups Page', () => { let browser; let page; - jest.setTimeout(60000); + jest.setTimeout(120000); beforeAll(async () => { browser = await puppeteer.launch({ @@ -62,7 +62,14 @@ describe('Discord Groups Page', () => { }, body: JSON.stringify(allUsersData.users[0]), }); + + } else if (url.startsWith(`${BASE_URL}/discord-actions/groups`)) { + const urlParams = new URLSearchParams(url.split('?')[1]); + const latestDoc = urlParams.get('latestDoc'); + const paginatedGroups = getPaginatedGroups(latestDoc); + } else if (url === `${STAGING_API_URL}/discord-actions/groups`) { + interceptedRequest.respond({ status: 200, contentType: 'application/json', @@ -71,6 +78,9 @@ describe('Discord Groups Page', () => { 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', }, + + body: JSON.stringify(paginatedGroups), + body: JSON.stringify(discordGroups), }); } else if (url === `${STAGING_API_URL}/discord-actions/groups`) { @@ -83,6 +93,7 @@ describe('Discord Groups Page', () => { 'Access-Control-Allow-Headers': 'Content-Type, Authorization', }, body: JSON.stringify(discordGroups), + }); } else if (url === `${STAGING_API_URL}/discord-actions/roles`) { interceptedRequest.respond({ @@ -102,7 +113,6 @@ describe('Discord Groups Page', () => { if (url === `${STAGING_API_URL}/discord-actions/groups`) { const postData = interceptedRequest.postData(); const groupData = JSON.parse(postData); - // discordGroups.push(groupData); interceptedRequest.respond({ status: 201, contentType: 'application/json', @@ -146,8 +156,13 @@ describe('Discord Groups Page', () => { interceptedRequest.continue(); } }); + + await page.goto(`${PAGE_URL}/groups`); + await page.waitForSelector('.card', { timeout: 5000 }); // Wait for the first batch of cards to load + await page.goto(`${LOCAL_TEST_PAGE_URL}/groups`); await page.waitForNetworkIdle(); + }); afterAll(async () => { @@ -160,7 +175,6 @@ describe('Discord Groups Page', () => { }); test('Should display cards', async () => { - await page.waitForSelector('.card'); const cards = await page.$$('.card'); expect(cards.length).toBeGreaterThan(0); @@ -252,12 +266,71 @@ describe('Discord Groups Page', () => { const closeBtn = await groupCreationModal.$('#close-button'); await closeBtn.click(); + await page.waitForTimeout(500); // Wait for modal to close const groupCreationModalClosed = await page.$('.group-creation-modal'); expect(groupCreationModalClosed).toBeFalsy(); }); + + test('Should load more groups on scroll', async () => { + await page.goto(`${PAGE_URL}/groups`); + await page.waitForSelector('.card', { timeout: 10000 }); + + const initialGroupCount = await page.$$eval( + '.card', + (cards) => cards.length, + ); + + await page.evaluate(() => { + window.scrollTo(0, document.body.scrollHeight); + }); + + await page.waitForFunction( + (initialCount) => { + return document.querySelectorAll('.card').length > initialCount; + }, + { timeout: 60000 }, + initialGroupCount, + ); + + const newGroupCount = await page.$$eval('.card', (cards) => cards.length); + + expect(newGroupCount).toBeGreaterThan(initialGroupCount); + }, 120000); + + test('Should stop loading more groups when all groups are loaded', async () => { + await page.goto(`${PAGE_URL}/groups`); + await page.waitForSelector('.card', { timeout: 5000 }); + + // Scroll to the bottom multiple times + for (let i = 0; i < 5; i++) { + await page.evaluate(() => { + window.scrollTo(0, document.body.scrollHeight); + }); + await page.waitForTimeout(1000); + } + + const finalGroupCount = await page.$$eval('.card', (cards) => cards.length); + + // Scroll one more time + await page.evaluate(() => { + window.scrollTo(0, document.body.scrollHeight); + }); + await page.waitForTimeout(1000); + + const newFinalGroupCount = await page.$$eval( + '.card', + (cards) => cards.length, + ); + + expect(newFinalGroupCount).toBe(finalGroupCount); + }); + + test('Should display only specified groups when dev=true and name= with different case', async () => { + test('Should display only specified groups when name= with different case', async () => { + const groupNames = 'fIrSt,DSA+COdInG'; await page.goto(`${LOCAL_TEST_PAGE_URL}/groups?name=${groupNames}`); await page.waitForNetworkIdle(); @@ -268,15 +341,20 @@ describe('Discord Groups Page', () => { ); }); - expect(displayedGroups).toEqual(['First Daaa', 'DSA Coding Group']); + expect(displayedGroups).toContain('First Daaa'); + expect(displayedGroups).toContain('DSA Coding Group'); }); test('Should display no group found div when no group is present', async () => { + + await page.goto(`${PAGE_URL}/groups?dev=true&name=no-group-present`); + await page.waitForSelector('.no-group-container', { timeout: 5000 }); + await page.goto(`${LOCAL_TEST_PAGE_URL}/groups?name=no-group-present`); await page.waitForNetworkIdle(); - const noGroupDiv = await page.$('.no-group-container'); + const noGroupDiv = await page.$('.no-group-container'); expect(noGroupDiv).toBeTruthy(); }); @@ -385,3 +463,20 @@ describe('Discord Groups Page', () => { expect(loaderAfter).toBeFalsy(); }); }); + +// Helper function to simulate paginated data +function getPaginatedGroups(latestDoc) { + const pageSize = 18; + const startIndex = latestDoc + ? discordGroups.groups.findIndex((g) => g.id === latestDoc) + 1 + : 0; + const endIndex = startIndex + pageSize; + const groups = discordGroups.groups.slice(startIndex, endIndex); + const newLatestDoc = groups.length > 0 ? groups[groups.length - 1].id : null; + + return { + message: 'Roles fetched successfully!', + groups, + newLatestDoc, + }; +} diff --git a/groups/index.html b/groups/index.html index a2e188ab..8cc7bfcf 100644 --- a/groups/index.html +++ b/groups/index.html @@ -45,12 +45,25 @@
+ + +
diff --git a/groups/script.js b/groups/script.js index b5569b3e..45910506 100644 --- a/groups/script.js +++ b/groups/script.js @@ -21,6 +21,7 @@ import { addGroupRoleToMember, createDiscordGroupRole, getDiscordGroups, + getPaginatedDiscordGroups, getUserGroupRoles, getUserSelf, removeRoleFromMember, @@ -109,6 +110,8 @@ const handler = { obj[prop] = value; break; case 'discordId': + case 'isLoading': + case 'hasMoreGroups': obj[prop] = value; break; case 'isSuperUser': @@ -125,11 +128,19 @@ const dataStore = new Proxy( { userSelf: null, groups: null, + + filteredGroupsIds: isDev ? [] : null, + search: isDev ? getParamValueFromURL(QUERY_PARAM_KEY.GROUP_SEARCH) : '', + discordId: null, + isGroupCreationModalOpen: false, + isLoading: false, + hasMoreGroups: true, filteredGroupsIds: null, search: getParamValueFromURL(QUERY_PARAM_KEY.GROUP_SEARCH), discordId: null, isCreateGroupModalOpen: false, isSuperUser: false, + }, handler, ); @@ -150,6 +161,7 @@ const onCreate = () => { throw new Error(data); } dataStore.userSelf = data; + isDev ? removeLoadingCards() : null; removeLoadingNavbarProfile(); await afterAuthentication(); }) @@ -168,9 +180,116 @@ const onCreate = () => { bindSearchInput(); bindSearchFocus(); bindGroupCreationButton(); + isDev ? bindInfiniteScroll() : null; }; const afterAuthentication = async () => { renderNavbarProfile({ profile: dataStore.userSelf }); + + if (isDev) { + await Promise.all([loadMoreGroups(), getUserGroupRoles()]).then( + ([groups, roleData]) => { + dataStore.filteredGroupsIds = groups.map((group) => group.id); + dataStore.groups = groups.reduce((acc, group) => { + let title = group.rolename + .replace('group-', '') + .split('-') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); + acc[group.id] = { + id: group.id, + title: title, + count: group.memberCount, + isMember: group.isMember, + roleId: group.roleid, + description: group.description, + isUpdating: false, + }; + return acc; + }, {}); + if (isDev) { + dataStore.filteredGroupsIds = getDiscordGroupIdsFromSearch( + Object.values(dataStore.groups), + dataStore.search, + ); + } + dataStore.discordId = roleData.userId; + }, + ); + } else { + await Promise.all([getDiscordGroups(), getUserGroupRoles()]).then( + ([groups, roleData]) => { + dataStore.filteredGroupsIds = groups.map((group) => group.id); + dataStore.groups = groups.reduce((acc, group) => { + let title = group.rolename + .replace('group-', '') + .split('-') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); + acc[group.id] = { + id: group.id, + title: title, + count: group.memberCount, + isMember: group.isMember, + roleId: group.roleid, + description: group.description, + isUpdating: false, + }; + return acc; + }, {}); + if (isDev) { + dataStore.filteredGroupsIds = getDiscordGroupIdsFromSearch( + Object.values(dataStore.groups), + dataStore.search, + ); + } + dataStore.discordId = roleData.userId; + }, + ); + } +}; +const loadMoreGroups = async () => { + if (dataStore.isLoading || !dataStore.hasMoreGroups) return; + + dataStore.isLoading = true; + renderLoadingCards(); + + const newGroups = await getPaginatedDiscordGroups(); + + removeLoadingCards(); + dataStore.isLoading = false; + + if (newGroups.length === 0) { + dataStore.hasMoreGroups = false; + return; + } + + dataStore.groups = { + ...dataStore.groups, + ...newGroups.reduce((acc, group) => { + acc[group.id] = { + id: group.id, + title: group.rolename + .replace('group-', '') + .split('-') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '), + count: group.memberCount, + isMember: group.isMember, + roleId: group.roleid, + description: group.description, + isUpdating: false, + }; + return acc; + }, {}), + }; + + dataStore.filteredGroupsIds = [ + ...dataStore.filteredGroupsIds, + ...newGroups.map((group) => group.id), + ]; + + return newGroups; + dataStore.isSuperUser = await checkUserIsSuperUser(); await Promise.all([getDiscordGroups(), getUserGroupRoles()]).then( @@ -204,10 +323,22 @@ const afterAuthentication = async () => { }); }, ); + }; // Bind Functions +const bindInfiniteScroll = () => { + window.addEventListener('scroll', () => { + if ( + window.innerHeight + window.scrollY >= + document.body.offsetHeight - 100 + ) { + loadMoreGroups(); + } + }); +}; + const bindGroupCreationButton = () => { const groupCreationBtn = document.querySelector('.create-group'); diff --git a/groups/utils.js b/groups/utils.js index f97112f4..bae60448 100644 --- a/groups/utils.js +++ b/groups/utils.js @@ -1,4 +1,8 @@ const BASE_URL = window.API_BASE_URL; // REPLACE WITH YOUR LOCALHOST URL FOR TESTING LOCAL BACKEND + +// const BASE_URL = "http://localhost:3000"; + + async function getMembers() { try { const res = await fetch(`${BASE_URL}/users/`, { @@ -58,6 +62,28 @@ async function getDiscordGroups() { return err; } } + +let latestDoc = 0; +async function getPaginatedDiscordGroups() { + try { + const res = await fetch( + `${BASE_URL}/discord-actions/groups?latestDoc=${latestDoc}&dev=true`, + { + method: 'GET', + credentials: 'include', + headers: { + 'Content-type': 'application/json', + }, + }, + ); + + const { groups, newLatestDoc } = await res.json(); + latestDoc = newLatestDoc; + return groups; + } catch (err) { + return err; + } +} async function createDiscordGroupRole(groupRoleBody) { try { const res = await fetch(`${BASE_URL}/discord-actions/groups`, { @@ -183,6 +209,7 @@ export { getMembers, getUserSelf, getDiscordGroups, + getPaginatedDiscordGroups, createDiscordGroupRole, addGroupRoleToMember, removeRoleFromMember, diff --git a/mock-data/groups/index.js b/mock-data/groups/index.js index 15b86aba..d6ac5f80 100644 --- a/mock-data/groups/index.js +++ b/mock-data/groups/index.js @@ -1,5 +1,61 @@ +// const discordGroups = { +// message: 'Roles fetched successfully!', +// groups: [ +// { +// id: 'CqnEhbwtCqdcZdlrixLn', +// date: { +// _seconds: 1683238758, +// _nanoseconds: 183000000, +// }, +// createdBy: 'V4rqL1aDecNGoa1IxiCu', +// rolename: 'group-first-daaa', +// roleid: '1103808103641780225', +// firstName: 'Test', +// lastName: 'User1', +// image: 'https://image.cdn.com/123dfg', +// memberCount: 2, +// }, +// { +// id: 'Mky71E6f6QWCY5MOBJFy', +// date: { +// _seconds: 1687619454, +// _nanoseconds: 560000000, +// }, +// createdBy: 'jbGcfZLGYjHwxQ1Zh8ZJ', +// rolename: 'group-DSA', +// roleid: '1122182070509244416', +// firstName: 'Test', +// lastName: 'User2', +// image: 'https://image.cdn.com/12dfg', +// memberCount: 200, +// lastUsedOn: { +// _nanoseconds: 61000000, +// _seconds: 1703615100, +// }, +// }, +// { +// id: '"mvWVuAxtSuhQtunjcywv"', +// date: { +// _seconds: 1684078062, +// _nanoseconds: 434000000, +// }, +// createdBy: 'k15z2SLFe1U2J3gshXUG', +// rolename: 'group-DSA-Coding-Group', +// roleid: '1107328395722899496', +// firstName: 'Test', +// lastName: 'User1', +// image: 'https://image.cdn.com/123dfgh', +// memberCount: 0, +// lastUsedOn: { +// _nanoseconds: 61070000, +// _seconds: 1703615154, +// }, +// }, +// ], +// }; const discordGroups = { message: 'Roles fetched successfully!', + latestDoc: '1124798395722994956', groups: [ { id: 'CqnEhbwtCqdcZdlrixLn', @@ -17,43 +73,231 @@ const discordGroups = { }, { id: 'Mky71E6f6QWCY5MOBJFy', - date: { - _seconds: 1687619454, - _nanoseconds: 560000000, - }, - createdBy: 'jbGcfZLGYjHwxQ1Zh8ZJ', + date: { _seconds: 1687619454, _nanoseconds: 560000000 }, + createdBy: 'V4rqL1aDecNGoa1IxiCu', rolename: 'group-DSA', roleid: '1122182070509244416', firstName: 'Test', lastName: 'User2', image: 'https://image.cdn.com/12dfg', memberCount: 200, - lastUsedOn: { - _nanoseconds: 61000000, - _seconds: 1703615100, - }, + lastUsedOn: { _nanoseconds: 61000000, _seconds: 1703615100 }, }, { - id: '"mvWVuAxtSuhQtunjcywv"', - date: { - _seconds: 1684078062, - _nanoseconds: 434000000, - }, - createdBy: 'k15z2SLFe1U2J3gshXUG', + id: 'mvWVuAxtSuhQtunjcywv', + date: { _seconds: 1684078062, _nanoseconds: 434000000 }, + createdBy: 'V4rqL1aDecNGoa1IxiCu', rolename: 'group-DSA-Coding-Group', roleid: '1107328395722899496', firstName: 'Test', lastName: 'User1', image: 'https://image.cdn.com/123dfgh', memberCount: 0, - lastUsedOn: { - _nanoseconds: 61070000, - _seconds: 1703615154, - }, + lastUsedOn: { _nanoseconds: 61070000, _seconds: 1703615154 }, + }, + { + id: 'bqkJG7LaQsUbXIvK3dr4', + date: { _seconds: 1688239345, _nanoseconds: 230000000 }, + createdBy: 'V4rqL1aDecNGoa1IxiCu', + rolename: 'group-Frontend-Masters', + roleid: '1129834012345678910', + firstName: 'Test', + lastName: 'User3', + image: 'https://image.cdn.com/123dfr', + memberCount: 55, + }, + { + id: 'uHnmq7N9LRGQKXF2es5P', + date: { _seconds: 1684010000, _nanoseconds: 100000000 }, + createdBy: 'V4rqL1aDecNGoa1IxiCu', + rolename: 'group-Backend-Developers', + roleid: '1105328395722900496', + firstName: 'Test', + lastName: 'User4', + image: 'https://image.cdn.com/12ghy', + memberCount: 123, + }, + { + id: 'ZcwPKx6M9SLbCrLtMx8Q', + date: { _seconds: 1685021234, _nanoseconds: 320000000 }, + createdBy: 'V4rqL1aDecNGoa1IxiCu', + rolename: 'group-Python-Experts', + roleid: '1134568395722910496', + firstName: 'Test', + lastName: 'User5', + image: 'https://image.cdn.com/12dfxy', + memberCount: 78, + }, + { + id: 'k8HmdC4yT3VXwNLHtRz6', + date: { _seconds: 1684014567, _nanoseconds: 450000000 }, + createdBy: 'V4rqL1aDecNGoa1IxiCu', + rolename: 'group-AI-Enthusiasts', + roleid: '1123418395722899235', + firstName: 'Test', + lastName: 'User6', + image: 'https://image.cdn.com/128dfh', + memberCount: 345, + }, + { + id: 'K9SLYx3T2YWHkNmCpX7P', + date: { _seconds: 1683018765, _nanoseconds: 470000000 }, + createdBy: 'V4rqL1aDecNGoa1IxiCu', + rolename: 'group-JavaScript-Devs', + roleid: '1135768395722990956', + firstName: 'Test', + lastName: 'User7', + image: 'https://image.cdn.com/123xyz', + memberCount: 431, + }, + { + id: 'tR7GmM8qJ2VLxPcCtG5R', + date: { _seconds: 1683071453, _nanoseconds: 230000000 }, + createdBy: 'V4rqL1aDecNGoa1IxiCu', + rolename: 'group-React-Devs', + roleid: '1123348395722890956', + firstName: 'Test', + lastName: 'User8', + image: 'https://image.cdn.com/126abc', + memberCount: 60, + }, + { + id: 'lKmBPv5Xw6SDkDnBfX4R', + date: { _seconds: 1682879831, _nanoseconds: 240000000 }, + createdBy: 'V4rqL1aDecNGoa1IxiCu', + rolename: 'group-VueJS-Devs', + roleid: '1102348395722994956', + firstName: 'Test', + lastName: 'User9', + image: 'https://image.cdn.com/128ui', + memberCount: 33, + }, + { + id: 'X8BmLt5C7RDvVpNqSk3L', + date: { _seconds: 1683287654, _nanoseconds: 540000000 }, + createdBy: 'V4rqL1aDecNGoa1IxiCu', + rolename: 'group-NodeJS-Masters', + roleid: '1134768395722911956', + firstName: 'Test', + lastName: 'User10', + image: 'https://image.cdn.com/123pqr', + memberCount: 502, + }, + { + id: 'G5QsWt2P6NDkCrLtFb3V', + date: { _seconds: 1682956789, _nanoseconds: 780000000 }, + createdBy: 'V4rqL1aDecNGoa1IxiCu', + rolename: 'group-CSS-Wizards', + roleid: '1102398395722992956', + firstName: 'Test', + lastName: 'User11', + image: 'https://image.cdn.com/128dfg', + memberCount: 128, + }, + { + id: 'J2BmHt4L3RTgQwNpXk6M', + date: { _seconds: 1682234789, _nanoseconds: 870000000 }, + createdBy: 'V4rqL1aDecNGoa1IxiCu', + rolename: 'group-C-Programming', + roleid: '1123568395722992956', + firstName: 'Test', + lastName: 'User12', + image: 'https://image.cdn.com/125abc', + memberCount: 205, + }, + { + id: 'S7NmKp5H6YDLtPrRtK4W', + date: { _seconds: 1682434567, _nanoseconds: 130000000 }, + createdBy: 'V4rqL1aDecNGoa1IxiCu', + rolename: 'group-Go-Language', + roleid: '1134998395722990956', + firstName: 'Test', + lastName: 'User13', + image: 'https://image.cdn.com/129def', + memberCount: 412, + }, + { + id: 'W3RvNx6P2VDqLvXtYf2J', + date: { _seconds: 1682703124, _nanoseconds: 190000000 }, + createdBy: 'V4rqL1aDecNGoa1IxiCu', + rolename: 'group-TypeScript-Fans', + roleid: '1104768395722990056', + firstName: 'Test', + lastName: 'User14', + image: 'https://image.cdn.com/123uvw', + memberCount: 195, + }, + { + id: 'L9PlWt7K8VFnLpRkXh5Q', + date: { _seconds: 1683045123, _nanoseconds: 390000000 }, + createdBy: 'V4rqL1aDecNGoa1IxiCu', + rolename: 'group-PHP-Devs', + roleid: '1134778395722993956', + firstName: 'Test', + lastName: 'User15', + image: 'https://image.cdn.com/123dfh', + memberCount: 251, + }, + { + id: 'Q4CmNt3P6RDnJqVxSk9L', + date: { _seconds: 1682512545, _nanoseconds: 630000000 }, + createdBy: 'V4rqL1aDecNGoa1IxiCu', + rolename: 'group-AWS-Lovers', + roleid: '1107348395722994956', + firstName: 'Test', + lastName: 'User16', + image: 'https://image.cdn.com/124tyu', + memberCount: 332, + }, + { + id: 'M8DmCt8N6VFtVpNwZr2X', + date: { _seconds: 1682419999, _nanoseconds: 710000000 }, + createdBy: 'V4rqL1aDecNGoa1IxiCu', + rolename: 'group-Docker-Experts', + roleid: '1125678395722911056', + firstName: 'Test', + lastName: 'User17', + image: 'https://image.cdn.com/128ghj', + memberCount: 40, + }, + { + id: 'P2RlUt4K7YDnXpQkWv5M', + date: { _seconds: 1682654789, _nanoseconds: 230000000 }, + createdBy: 'V4rqL1aDecNGoa1IxiCu', + rolename: 'group-Kubernetes-Lovers', + roleid: '1134788395722990056', + firstName: 'Test', + lastName: 'User18', + image: 'https://image.cdn.com/123kly', + memberCount: 84, + }, + { + id: 'D5NmPt6V2RFoKvGxTr3V', + date: { _seconds: 1682901234, _nanoseconds: 430000000 }, + createdBy: 'V4rqL1aDecNGoa1IxiCu', + rolename: 'group-Azure-Fans', + roleid: '1103458395722995956', + firstName: 'Test', + lastName: 'User19', + image: 'https://image.cdn.com/126nqr', + memberCount: 25, + }, + { + id: 'V6JkRp7P3MDkLvStXr7L', + date: { _seconds: 1682323456, _nanoseconds: 890000000 }, + createdBy: 'V4rqL1aDecNGoa1IxiCu', + rolename: 'group-GCP-Devs', + roleid: '1124798395722994956', + firstName: 'Test', + lastName: 'User20', + image: 'https://image.cdn.com/127dfg', + memberCount: 89, }, ], }; +// console.log(discordGroups); + const GroupRoleData = { message: 'User group roles Id fetched successfully!', userId: '1234398439439989',