Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions __tests__/groups/group.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@ describe('Discord Groups Page', () => {
let page;
jest.setTimeout(60000);

// Shared constants for date formatting tests
const TEST_TIMESTAMP = Date.UTC(2024, 10, 6, 10, 15);
const FULL_DATE_FORMAT_OPTIONS = {
weekday: 'long',
year: 'numeric',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
timeZoneName: 'short',
hour12: true,
};

beforeAll(async () => {
browser = await puppeteer.launch({
headless: 'new',
Expand Down Expand Up @@ -422,4 +435,116 @@ describe('Discord Groups Page', () => {
'Group deleted successfully',
);
});
test('Should display last used label and tooltip when lastUsedOn is present', async () => {
await page.goto(`${LOCAL_TEST_PAGE_URL}/groups`);
await page.waitForNetworkIdle();

const groupCardHandle = await page.evaluateHandle(() => {
const cards = Array.from(document.querySelectorAll('.card'));
return (
cards.find((card) => {
const el = card.querySelector('.card__last-used');
return el && getComputedStyle(el).display !== 'none';
}) || null
);
});

expect(groupCardHandle).toBeTruthy();

const lastUsedText = await page.evaluate((card) => {
const el = card.querySelector('.card__last-used');
if (!el) return '';
const tooltipElement = el.querySelector('.tooltip');
if (tooltipElement) {
const fullText = el.textContent.trim();
const tooltipText = tooltipElement.textContent.trim();
return fullText.replace(tooltipText, '').trim();
}
const firstChildNode = el.childNodes[0];
if (firstChildNode && firstChildNode.nodeType === Node.TEXT_NODE)
return firstChildNode.textContent.trim();
return el.textContent.trim();
}, groupCardHandle);

expect(lastUsedText).toMatch(/^Last used on: \d{1,2} [A-Za-z]{3} \d{4}$/);

const tooltipText = await page.evaluate((card) => {
const tooltipElement = card.querySelector('.card__last-used .tooltip');
return tooltipElement ? tooltipElement.textContent.trim() : '';
}, groupCardHandle);

expect(tooltipText.length).toBeGreaterThan(0);
});

test('Should hide last used label when lastUsedOn is absent', async () => {
await page.goto(`${LOCAL_TEST_PAGE_URL}/groups`);
await page.waitForNetworkIdle();

const isAnyCardHidden = await page.evaluate(() => {
const cards = Array.from(document.querySelectorAll('.card'));
return cards.some((card) => {
const el = card.querySelector('.card__last-used');
return el && getComputedStyle(el).display === 'none';
});
});

expect(isAnyCardHidden).toBe(true);
});

test('Should return correctly formatted full date string and N/A for invalid inputs', async () => {
const evaluationResult = await page.evaluate(
async (timestamp, fullDateFormatOptions) => {
const utilsModule = await import('/groups/utils.js');
const formatted = utilsModule.fullDateString(timestamp);
const expected = new Intl.DateTimeFormat(
'en-US',
fullDateFormatOptions,
).format(new Date(timestamp));
const invalidString = utilsModule.fullDateString('abc');
const invalidNotANumber = utilsModule.fullDateString(NaN);
const invalidObject = utilsModule.fullDateString({});
return {
formatted,
expected,
invalidString,
invalidNotANumber,
invalidObject,
};
},
TEST_TIMESTAMP,
FULL_DATE_FORMAT_OPTIONS,
);

expect(evaluationResult.formatted).toBe(evaluationResult.expected);
expect(evaluationResult.invalidString).toBe('N/A');
expect(evaluationResult.invalidNotANumber).toBe('N/A');
expect(evaluationResult.invalidObject).toBe('N/A');
});

test('Should return correctly formatted short date string and N/A for invalid inputs', async () => {
const evaluationResult = await page.evaluate(async (timestamp) => {
const utilsModule = await import('/groups/utils.js');
const formatted = utilsModule.shortDateString(timestamp);
const dateInstance = new Date(timestamp);
const month = new Intl.DateTimeFormat('en-US', { month: 'short' }).format(
dateInstance,
);
const expected = `${dateInstance.getDate()} ${month} ${dateInstance.getFullYear()}`;
const invalidUndefined = utilsModule.shortDateString(undefined);
const invalidStringInput = utilsModule.shortDateString('bad');
const invalidNotANumber = utilsModule.shortDateString(NaN);
return {
formatted,
expected,
invalidUndefined,
invalidStringInput,
invalidNotANumber,
};
}, TEST_TIMESTAMP);

expect(evaluationResult.formatted).toBe(evaluationResult.expected);
expect(evaluationResult.invalidUndefined).toBe('N/A');
expect(evaluationResult.invalidStringInput).toBe('N/A');
expect(evaluationResult.invalidNotANumber).toBe('N/A');
});
});
16 changes: 16 additions & 0 deletions groups/createElements.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { fullDateString, shortDateString } from './utils.js';

const createCard = (
rawGroup,
onClick = () => {},
Expand Down Expand Up @@ -27,6 +29,7 @@ const createCard = (
}
</div>
<p class="card__description"></p>
<p class="card__last-used"></p>
<div class="card__action">
<button class="card__btn button"></button>
<span class="card__count">
Expand All @@ -41,6 +44,19 @@ const createCard = (
cardElement.querySelector('.card__title').textContent = group.title;
cardElement.querySelector('.card__description').textContent =
group.description;
const lastUsedElement = cardElement.querySelector('.card__last-used');
if (group.lastUsedTimestamp) {
const shortFormatted = shortDateString(group.lastUsedTimestamp);
lastUsedElement.classList.add('tooltip-container');
lastUsedElement.textContent = `Last used on: ${shortFormatted}`;

const tooltip = document.createElement('span');
tooltip.className = 'tooltip';
tooltip.innerText = fullDateString(group.lastUsedTimestamp);
lastUsedElement.appendChild(tooltip);
} else {
lastUsedElement.style.display = 'none';
}
cardElement.querySelector('.card__btn').textContent = group.isMember
? 'Remove me'
: 'Add me';
Expand Down
7 changes: 7 additions & 0 deletions groups/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,13 +196,20 @@ const afterAuthentication = async () => {
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
const lastUsedTimestamp =
group?.lastUsedOn?.seconds !== undefined
? group.lastUsedOn.seconds * 1000
: group?.lastUsedOn?._seconds !== undefined
? group.lastUsedOn._seconds * 1000
: undefined;
acc[group.id] = {
id: group.id,
title: title,
count: group.memberCount,
isMember: group.isMember,
roleId: group.roleid,
description: group.description,
lastUsedTimestamp,
isUpdating: false,
};
return acc;
Expand Down
31 changes: 31 additions & 0 deletions groups/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,37 @@ body {
line-height: 18px;
}

.card__last-used {
margin-top: 0.4rem;
color: var(--color-text-secondary);
font-size: 0.75rem;
}

.tooltip-container {
position: relative;
}

.tooltip {
background-color: var(--black-color, #000);
color: var(--white, #fff);
visibility: hidden;
text-align: center;
border-radius: 4px;
padding: 0.5rem;
position: absolute;
opacity: 0.9;
font-size: 0.7rem;
width: 10rem;
bottom: 100%;
left: 50%;
margin-left: -5rem;
}

.tooltip-container:hover .tooltip {
visibility: visible;
transition-delay: 400ms;
}

.card__action {
display: flex;
justify-content: space-between;
Expand Down
38 changes: 38 additions & 0 deletions groups/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,42 @@ function setParamValueInURL(paramKey, paramValue) {
);
}

const fullDateString = (timestamp) => {
if (typeof timestamp !== 'number' || !Number.isFinite(timestamp)) {
return 'N/A';
}
const dateObj = new Date(timestamp);
if (isNaN(dateObj.getTime())) {
return 'N/A';
}
const options = {
weekday: 'long',
year: 'numeric',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
timeZoneName: 'short',
hour12: true,
};
return new Intl.DateTimeFormat('en-US', options).format(dateObj);
};

const shortDateString = (timestamp) => {
if (typeof timestamp !== 'number' || !Number.isFinite(timestamp)) {
return 'N/A';
}
const date = new Date(timestamp);
if (isNaN(date.getTime())) {
return 'N/A';
}
const year = date.getFullYear();
const month = new Intl.DateTimeFormat('en-US', { month: 'short' }).format(
date,
);
return `${date.getDate()} ${month} ${year}`;
};

export {
getUserGroupRoles,
getMembers,
Expand All @@ -191,4 +227,6 @@ export {
getDiscordGroupIdsFromSearch,
getParamValueFromURL,
setParamValueInURL,
fullDateString,
shortDateString,
};
Loading