diff --git a/app/components/application/detail-header.hbs b/app/components/application/detail-header.hbs new file mode 100644 index 00000000..34849825 --- /dev/null +++ b/app/components/application/detail-header.hbs @@ -0,0 +1,81 @@ +
+
+
+ Profile +
+
+

+ {{this.fullName}} +

+ +
+

{{this.role}}

+
+
+ + {{this.location}} +
+
+ + {{this.skills}} +
+
+
+
+ + +
+ +
+
+
+ {{this.score}} + Score +
+
+ {{this.nudgeCount}} + Nudges +
+
+
+ {{#if @isAdmin}} + + {{else}} + + + {{/if}} +
+
+
\ No newline at end of file diff --git a/app/components/application/detail-header.js b/app/components/application/detail-header.js new file mode 100644 index 00000000..b7df3dde --- /dev/null +++ b/app/components/application/detail-header.js @@ -0,0 +1,99 @@ +import Component from '@glimmer/component'; +import { action } from '@ember/object'; + +export default class DetailHeader extends Component { + get application() { + return this.args.application; + } + + get userDetails() { + return this.args.userDetails; + } + + get fullName() { + const { firstName, lastName } = this.application?.biodata || {}; + return firstName || lastName ? `${firstName} ${lastName}` : null; + } + + get imageUrl() { + return this.application?.imageUrl ?? ''; + } + + get role() { + return this.application?.role ?? 'N/A'; + } + + get status() { + return this.application?.status ?? 'pending'; + } + + get skills() { + return this.application?.professional?.skills ?? 'N/A'; + } + + get location() { + const { city, state, country } = this.application?.location || {}; + return [city, state, country].filter(Boolean).join(', ') || 'N/A'; + } + + get score() { + return this.application?.score ?? 'N/A'; + } + + get nudgeCount() { + return this.application?.nudgeCount ?? 0; + } + + get isNudgeDisabled() { + if (this.status !== 'pending') { + return true; + } + if (!this.application?.lastNudgedAt) { + return false; + } + const now = Date.now(); + const lastNudgeTime = new Date(this.application.lastNudgedAt).getTime(); + const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000; + return now - lastNudgeTime < TWENTY_FOUR_HOURS; + } + + get socialLinks() { + const links = []; + const social = this.application?.socialLink || {}; + + if (social.github) + links.push({ platform: 'GitHub', userName: social.github }); + if (social.linkedin) + links.push({ platform: 'LinkedIn', userName: social.linkedin }); + if (social.twitter) + links.push({ platform: 'Twitter', userName: social.twitter }); + if (social.instagram) + links.push({ platform: 'Instagram', userName: social.instagram }); + if (social.peerlist) + links.push({ platform: 'Peerlist', userName: social.peerlist }); + if (social.dribbble) + links.push({ platform: 'Dribbble', userName: social.dribbble }); + if (social.behance) + links.push({ platform: 'Behance', userName: social.behance }); + + return links; + } + + @action + nudgeApplication() { + //ToDo: Implement logic for callling nudge API here + console.log('nudge application'); + } + + @action + editApplication() { + //ToDo: Implement logic for edit application here + console.log('edit application'); + } + + @action + navigateToDashboard() { + //ToDo: Navigate to dashboard site for admin actions + console.log('navigate to dashboard'); + } +} diff --git a/app/components/application/feedback-card.hbs b/app/components/application/feedback-card.hbs new file mode 100644 index 00000000..9422bd37 --- /dev/null +++ b/app/components/application/feedback-card.hbs @@ -0,0 +1,14 @@ +
+
+ + +
+
+ + +
+
\ No newline at end of file diff --git a/app/components/application/feedback-card.js b/app/components/application/feedback-card.js new file mode 100644 index 00000000..c8fcc243 --- /dev/null +++ b/app/components/application/feedback-card.js @@ -0,0 +1,11 @@ +import Component from '@glimmer/component'; + +export default class FeedbackCard extends Component { + get formattedDate() { + if (!this.args.createdAt) { + return 'N/A'; + } + const date = new Date(this.args.createdAt); + return isNaN(date.getTime()) ? 'N/A' : date.toLocaleDateString(); + } +} diff --git a/app/components/application/info-card.hbs b/app/components/application/info-card.hbs new file mode 100644 index 00000000..13f544e9 --- /dev/null +++ b/app/components/application/info-card.hbs @@ -0,0 +1,11 @@ +
+

{{@title}}

+
+ {{#each @sections as |section|}} +
+ +

{{section.value}}

+
+ {{/each}} +
+
\ No newline at end of file diff --git a/app/components/application/social-link-pill.hbs b/app/components/application/social-link-pill.hbs new file mode 100644 index 00000000..bdf35fec --- /dev/null +++ b/app/components/application/social-link-pill.hbs @@ -0,0 +1,11 @@ + + + {{@platform}} + \ No newline at end of file diff --git a/app/components/application/social-link-pill.js b/app/components/application/social-link-pill.js new file mode 100644 index 00000000..cb50b89e --- /dev/null +++ b/app/components/application/social-link-pill.js @@ -0,0 +1,16 @@ +import Component from '@glimmer/component'; +import { mapSocialIcons, mapSocialUrls } from '../../constants/applications'; + +export default class SocialLinkPill extends Component { + get platform() { + return this.args.platform?.toLowerCase(); + } + + get icon() { + return mapSocialIcons[this.platform] || 'mdi:link'; + } + + get redirectUrl() { + return `${mapSocialUrls[this.platform]}/${this.args.userName}`; + } +} diff --git a/app/components/application/status-badge.hbs b/app/components/application/status-badge.hbs new file mode 100644 index 00000000..b5a1da5a --- /dev/null +++ b/app/components/application/status-badge.hbs @@ -0,0 +1,3 @@ + + {{this.applicationStatus}} + \ No newline at end of file diff --git a/app/components/application/status-badge.js b/app/components/application/status-badge.js new file mode 100644 index 00000000..9ef5d711 --- /dev/null +++ b/app/components/application/status-badge.js @@ -0,0 +1,11 @@ +import Component from '@glimmer/component'; +import { mapApplicationStatus } from '../../constants/applications'; + +export default class StatusBadge extends Component { + get applicationStatus() { + const status = this.args.status?.toUpperCase(); + return ( + mapApplicationStatus[status] || mapApplicationStatus.PENDING + )?.toUpperCase(); + } +} diff --git a/app/components/header.hbs b/app/components/header.hbs index 381e9eed..f65907c1 100644 --- a/app/components/header.hbs +++ b/app/components/header.hbs @@ -15,7 +15,7 @@ Real_Dev_Squad diff --git a/app/components/scroll-to-top.hbs b/app/components/scroll-to-top.hbs index 3d764659..74c406b6 100644 --- a/app/components/scroll-to-top.hbs +++ b/app/components/scroll-to-top.hbs @@ -1,7 +1,7 @@ {{#unless this.isLive}} {{#if this.isScrollToTopVisible}} {{/if}} {{/unless}} \ No newline at end of file diff --git a/app/constants/applications.js b/app/constants/applications.js new file mode 100644 index 00000000..c3fda429 --- /dev/null +++ b/app/constants/applications.js @@ -0,0 +1,39 @@ +export const mapApplicationStatus = { + ACCEPTED: 'accepted', + REJECTED: 'rejected', + CHANGES_REQUESTED: 'Changes Requested', + PENDING: 'pending', +}; + +export const mapSocialIcons = { + github: 'mdi:github', + linkedin: 'mdi:linkedin', + twitter: 'mdi:twitter', + instagram: 'mdi:instagram', + peerlist: 'mdi:account-circle', +}; + +export const mapSocialUrls = { + github: 'https://github.com', + linkedin: 'https://linkedin.com/in', + twitter: 'https://twitter.com', + instagram: 'https://instagram.com', + peerlist: 'https://peerlist.io', + dribbble: 'https://dribbble.com', + behance: 'https://behance.net', +}; + +export function adminMessage(status) { + switch (status) { + case 'pending': + return 'Admins are reviewing your applications, please hold up and keep on nudging.'; + case 'accepted': + return 'Your application is approved, go back to join page to get join discord invite.'; + case 'rejected': + return 'Your application has been rejected. Look at the feedback for more information.'; + case 'changes_requested': + return 'Admin has requested changes on your application, please edit and submit again for review.'; + default: + return 'Unknown status'; + } +} diff --git a/app/controllers/applications/detail.js b/app/controllers/applications/detail.js new file mode 100644 index 00000000..9581f34b --- /dev/null +++ b/app/controllers/applications/detail.js @@ -0,0 +1,44 @@ +import Controller from '@ember/controller'; +import { adminMessage } from '../../constants/applications'; + +export default class ApplicationsDetailController extends Controller { + get application() { + return this.model?.application; + } + + get currentUser() { + return this.model?.currentUser; + } + + get isAdmin() { + return this.currentUser?.roles?.super_user === true; + } + + get isApplicant() { + return this.currentUser?.id === this.application?.userId; + } + + get canAccessApplication() { + return this.isAdmin || this.isApplicant; + } + + get aboutYouSections() { + return [ + { + label: 'Introduction', + value: this.application?.intro?.introduction || 'N/A', + }, + { label: 'Fun Fact', value: this.application?.intro?.funFact || 'N/A' }, + { label: 'For Fun', value: this.application?.intro?.forFun || 'N/A' }, + { label: 'Why Join Us', value: this.application?.intro?.whyRds || 'N/A' }, + ]; + } + + get hasFeedback() { + return this.application?.feedback?.length > 0; + } + + get showAdminMessage() { + return adminMessage(this.application?.status); + } +} diff --git a/app/routes/applications/detail.js b/app/routes/applications/detail.js index 7df24e83..eac84ece 100644 --- a/app/routes/applications/detail.js +++ b/app/routes/applications/detail.js @@ -16,15 +16,10 @@ export default class ApplicationsDetailRoute extends Route { async model(params) { try { const userResponse = await apiRequest(SELF_USER_PROFILE_URL); - if (userResponse.status === 401) { this.toast.error(ERROR_MESSAGES.notLoggedIn, '', TOAST_OPTIONS); setTimeout(redirectAuth, 2000); - return null; - } - - if (!userResponse.ok) { - throw new Error(`HTTP error! status: ${userResponse.status}`); + return { application: null, currentUser: null }; } const applicationResponse = await apiRequest( @@ -33,22 +28,26 @@ export default class ApplicationsDetailRoute extends Route { if (applicationResponse.status === 404) { this.toast.error('Application not found', 'Error!', TOAST_OPTIONS); - return null; + return { application: null, currentUser: null }; } if (!applicationResponse.ok) { throw new Error(`HTTP error! status: ${applicationResponse.status}`); } + const userData = await userResponse.json(); const applicationData = await applicationResponse.json(); - return applicationData?.application; + return { + application: applicationData?.application, + currentUser: userData, + }; } catch (error) { this.toast.error( 'Something went wrong. ' + error.message, 'Error!', TOAST_OPTIONS, ); - return null; + return { application: null, currentUser: null }; } } } diff --git a/app/templates/applications/detail.hbs b/app/templates/applications/detail.hbs index cd77521b..0a88f702 100644 --- a/app/templates/applications/detail.hbs +++ b/app/templates/applications/detail.hbs @@ -1,18 +1,53 @@ -
-

Application Details

- {{#if this.model}} -
-
-

Basic Information

-

Application ID: {{this.model.id}}

-

User ID: {{this.model.userId}}

+
+ {{#if this.application}} + + +
+ + {{this.showAdminMessage}} +
+ +
+
+ +
+ +
+

Feedback History

+
+ {{else}} -
-

Application not found.

-

The application you're looking for doesn't - exist or may have been removed.

+
+

Application not found.

+

The application + you're looking for doesn't exist or may have been removed.

{{/if}}
\ No newline at end of file diff --git a/tests/constants/application-data.js b/tests/constants/application-data.js new file mode 100644 index 00000000..435a7108 --- /dev/null +++ b/tests/constants/application-data.js @@ -0,0 +1,65 @@ +export const APPLICATIONS_DATA = { + id: '2MzNJtwGL3D28WeC7yUI', + userId: 'SoaTFu1SnU7HVvWjkR7Z', + biodata: { firstName: 'John', lastName: 'Doe' }, + location: { + city: 'Patna', + state: 'Bihar', + country: 'India', + }, + professional: { + institution: 'MIT', + skills: + 'JavaScript, Node.js, React, Next, Ember.js, Typescript, Java, Springboot, Docker, Azure, AWS, GenAI, AI Agents, Langchain, Redis, Postgresql, MongoDB, WebSockets', + }, + intro: { + introduction: + 'I am a passionate developer with 5 years of experience. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + funFact: + 'I once built a full-stack application in 48 hours during a hackathon. The project went on to win first place and is now used by over 10,000 users.I once built a full-stack application in 48 hours during a hackathon. The project went on to win first place and is now used by over 10,000 users.I once built a full-stack application in 48 hours during a hackathon. The project went on to win first place and is now used by over 10,000 users.I once built a full-stack application in 48 hours during a hackathon. The project went on to win first place and is now used by over 10,000 users.I once built a full-stack application in 48 hours during a hackathon. The project went on to win first place and is now used by over 10,000 users.I once built a full-stack application in 48 hours during a hackathon. The project went on to win first place and is now used by over 10,000 users.I once built a full-stack application in 48 hours during a hackathon. The project went on to win first place and is now used by over 10,000 users.I once built a full-stack application in 48 hours during a hackathon. The project went on to win first place and is now used by over 10,000 users.I once built a full-stack application in 48 hours during a hackathon. The project went on to win first place and is now used by over 10,000 users.I once built a full-stack application in 48 hours during a hackathon. The project went on to win first place and is now used by over 10,000 users. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + forFun: + 'I love coding, reading tech blogs, and contributing to open source projects. In my free time, I enjoy playing guitar and hiking.I love coding, reading tech blogs, and contributing to open source projects. In my free time, I enjoy playing guitar and hiking.I love coding, reading tech blogs, and contributing to open source projects. In my free time, I enjoy playing guitar and hiking.I love coding, reading tech blogs, and contributing to open source projects. In my free time, I enjoy playing guitar and hiking.I love coding, reading tech blogs, and contributing to open source projects. In my free time, I enjoy playing guitar and hiking.I love coding, reading tech blogs, and contributing to open source projects. In my free time, I enjoy playing guitar and hiking.I love coding, reading tech blogs, and contributing to open source projects. In my free time, I enjoy playing guitar and hiking.I love coding, reading tech blogs, and contributing to open source projects. In my free time, I enjoy playing guitar and hiking.I love coding, reading tech blogs, and contributing to open source projects. In my free time, I enjoy playing guitar and hiking.I love coding, reading tech blogs, and contributing to open source projects. In my free time, I enjoy playing guitar and hiking.I love coding, reading tech blogs, and contributing to open source projects. In my free time, I enjoy playing guitar and hiking. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + whyRds: + 'I am excited to join Real Dev Squad because of its commitment to open source and community-driven development. I believe in the mission and want to contribute to meaningful projects.I am excited to join Real Dev Squad because of its commitment to open source and community-driven development. I believe in the mission and want to contribute to meaningful projects.I am excited to join Real Dev Squad because of its commitment to open source and community-driven development. I believe in the mission and want to contribute to meaningful projects.I am excited to join Real Dev Squad because of its commitment to open source and community-driven development. I believe in the mission and want to contribute to meaningful projects.I am excited to join Real Dev Squad because of its commitment to open source and community-driven development. I believe in the mission and want to contribute to meaningful projects.I am excited to join Real Dev Squad because of its commitment to open source and community-driven development. I believe in the mission and want to contribute to meaningful projects.I am excited to join Real Dev Squad because of its commitment to open source and community-driven development. I believe in the mission and want to contribute to meaningful projects.I am excited to join Real Dev Squad because of its commitment to open source and community-driven development. I believe in the mission and want to contribute to meaningful projects.I am excited to join Real Dev Squad because of its commitment to open source and community-driven development. I believe in the mission and want to contribute to meaningful projects.I am excited to join Real Dev Squad because of its commitment to open source and community-driven development. I believe in the mission and want to contribute to meaningful projects.I am excited to join Real Dev Squad because of its commitment to open source and community-driven development. I believe in the mission and want to contribute to meaningful projects.I am excited to join Real Dev Squad because of its commitment to open source and community-driven development. I believe in the mission and want to contribute to meaningful projects. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + numberOfHours: 20, + }, + foundFrom: 'LinkedIn', + role: 'developer', + imageUrl: + 'https://res.cloudinary.com/imgprc/image/upload/v1767280230/profile/nEYwqHm8vGoZhh1j9PbF/no8jkcirgj3sb9phfm20.jpg', + socialLink: { + github: 'johndoe', + linkedin: 'johndoe', + twitter: 'Johndoe', + peerlist: 'johndoe', + instagram: 'johndoe', + dribbble: 'johndoe', + behance: 'johndoe', + }, + feedback: [ + { + status: 'accepted', + feedback: '', + reviewerName: 'Ankush', + createdAt: '2026-01-14T15:37:10.991Z', + }, + { + status: 'changes_requested', + feedback: 'Please follow requirements for the profile picture', + reviewerName: 'Prakash', + createdAt: '2026-01-13T12:37:10.991Z', + }, + { + status: 'rejected', + feedback: 'lorem', + reviewerName: 'Tejas', + createdAt: '2026-01-11T10:37:10.991Z', + }, + ], + score: 250, + status: 'accepted', + createdAt: '2026-01-14T18:37:10.991Z', + nudgeCount: 10, + isNew: false, + notFound: false, +}; diff --git a/tests/integration/components/application/detail-header-test.js b/tests/integration/components/application/detail-header-test.js new file mode 100644 index 00000000..e1e3c771 --- /dev/null +++ b/tests/integration/components/application/detail-header-test.js @@ -0,0 +1,96 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'website-www/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { APPLICATIONS_DATA } from 'website-www/tests/constants/application-data'; + +module('Integration | Component | application/detail-header', function (hooks) { + setupRenderingTest(hooks); + + test('it renders application details correctly', async function (assert) { + const user = APPLICATIONS_DATA; + this.set('application', user); + this.set('isAdmin', false); + + await render(hbs` + + `); + + const { firstName, lastName } = user.biodata; + assert + .dom('[data-test-user-name]') + .includesText(`${firstName} ${lastName}`); + assert.dom('[data-test-status-badge]').hasText(user.status.toUpperCase()); + assert.dom('[data-test-score-value]').hasText(String(user.score)); + assert + .dom('[data-test-nudge-details]') + .includesText(String(user.nudgeCount)); + assert + .dom('[data-test-social-link]') + .exists({ count: Object.keys(user.socialLink).length }); + + assert.dom('[data-test-button="nudge-button"]').exists(); + assert.dom('[data-test-button="edit-button"]').exists(); + assert.dom('[data-test-button="navigate-button"]').doesNotExist(); + }); + + test('it renders admin actions correctly', async function (assert) { + const user = APPLICATIONS_DATA; + this.set('application', user); + this.set('isAdmin', true); + + await render(hbs` + + `); + + assert.dom('[data-test-button="navigate-button"]').exists(); + assert.dom('[data-test-button="nudge-button"]').doesNotExist(); + assert.dom('[data-test-button="edit-button"]').doesNotExist(); + }); + + test('it handles partial location data correctly', async function (assert) { + this.set('application', { + location: { city: 'Mumbai', country: 'India' }, + }); + await render( + hbs``, + ); + assert.dom('[data-test-header-profile]').includesText('Mumbai, India'); + }); + + test('it disables nudge button when status is not pending', async function (assert) { + const app = { ...APPLICATIONS_DATA, status: 'accepted' }; + this.set('application', app); + this.set('isAdmin', false); + + await render(hbs` + + `); + + assert.dom('[data-test-button="nudge-button"]').hasAttribute('disabled'); + }); + + test('it disables nudge button based on 24h timeout', async function (assert) { + const now = Date.now(); + const recentNudge = new Date(now - 12 * 60 * 60 * 1000).toISOString(); + + this.set('application', { + status: 'pending', + lastNudgedAt: recentNudge, + }); + + await render( + hbs``, + ); + assert.dom('[data-test-button="nudge-button"]').hasAttribute('disabled'); + }); +}); diff --git a/tests/integration/components/application/feedback-card-test.js b/tests/integration/components/application/feedback-card-test.js new file mode 100644 index 00000000..b3571b45 --- /dev/null +++ b/tests/integration/components/application/feedback-card-test.js @@ -0,0 +1,59 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'website-www/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { APPLICATIONS_DATA } from 'website-www/tests/constants/application-data'; + +module('Integration | Component | application/feedback-card', function (hooks) { + setupRenderingTest(hooks); + + test('it renders feedback details correctly', async function (assert) { + const feedback = APPLICATIONS_DATA.feedback[0]; + this.set('feedback', feedback); + + await render(hbs` + + `); + + assert + .dom('[data-test-status-badge]') + .hasText(feedback.status.toUpperCase()); + assert.dom('[data-test-feedback-text]').hasText(feedback.feedback); + assert + .dom('[data-test-feedback-reviewer]') + .includesText(feedback.reviewerName); + assert + .dom('[data-test-feedback-date]') + .hasText(new Date(feedback.createdAt).toLocaleDateString()); + }); + + test('it renders N/A for missing date', async function (assert) { + await render(hbs` + + `); + + assert.dom('[data-test-feedback-date]').hasText('N/A'); + }); + + test('it handles invalid date string correctly', async function (assert) { + await render(hbs` + + `); + + assert.dom('[data-test-feedback-date]').hasText('N/A'); + }); +}); diff --git a/tests/integration/components/application/info-card-test.js b/tests/integration/components/application/info-card-test.js new file mode 100644 index 00000000..089303da --- /dev/null +++ b/tests/integration/components/application/info-card-test.js @@ -0,0 +1,48 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'website-www/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Component | application/info-card', function (hooks) { + setupRenderingTest(hooks); + + test('it renders title and default sections correctly', async function (assert) { + this.set('sections', [ + { label: 'Intro', value: 'Value 1' }, + { label: 'Fact', value: 'Value 2' }, + ]); + + await render( + hbs``, + ); + + assert.dom('[data-test-info-card-title]').hasText('About'); + assert.dom('[data-test-info-section]').exists({ count: 2 }); + assert.dom('.info-label').includesText('Intro'); + assert.dom('.info-value').includesText('Value 1'); + + assert + .dom('[data-test-info-section]:nth-child(1) .info-label') + .hasText('Intro'); + assert + .dom('[data-test-info-section]:nth-child(1) .info-value') + .hasText('Value 1'); + assert + .dom('[data-test-info-section]:nth-child(2) .info-label') + .hasText('Fact'); + assert + .dom('[data-test-info-section]:nth-child(2) .info-value') + .hasText('Value 2'); + }); + + test('it handles empty sections correctly', async function (assert) { + this.set('sections', []); + + await render( + hbs``, + ); + + assert.dom('[data-test-info-card-title]').hasText('Empty'); + assert.dom('[data-test-info-section]').doesNotExist(); + }); +}); diff --git a/tests/integration/components/application/social-link-pill-test.js b/tests/integration/components/application/social-link-pill-test.js new file mode 100644 index 00000000..ea059cbe --- /dev/null +++ b/tests/integration/components/application/social-link-pill-test.js @@ -0,0 +1,51 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'website-www/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module( + 'Integration | Component | application/social-link-pill', + function (hooks) { + setupRenderingTest(hooks); + + test('it renders the platform and links correctly', async function (assert) { + this.set('platform', 'GitHub'); + this.set('userName', 'testuser'); + + await render( + hbs``, + ); + + assert + .dom('[data-test-social-link]') + .hasAttribute('href', 'https://github.com/testuser'); + assert.dom('[data-test-social-link]').includesText('GitHub'); + assert.dom('[data-test-platform="GitHub"]').exists(); + }); + + test('it renders LinkedIn and other platforms correctly', async function (assert) { + this.set('platform', 'LinkedIn'); + this.set('userName', 'testuser'); + + await render( + hbs``, + ); + + assert + .dom('[data-test-social-link]') + .hasAttribute('href', 'https://linkedin.com/in/testuser'); + assert.dom('[data-test-social-link]').includesText('LinkedIn'); + }); + + test('it handles unknown platforms with fallback icon', async function (assert) { + this.set('platform', 'unknown'); + this.set('userName', 'user'); + + await render( + hbs``, + ); + assert.dom('[data-test-social-link]').exists(); + assert.dom('[data-test-social-link]').includesText('unknown'); + }); + }, +); diff --git a/tests/integration/components/application/status-badge-test.js b/tests/integration/components/application/status-badge-test.js new file mode 100644 index 00000000..75fb346c --- /dev/null +++ b/tests/integration/components/application/status-badge-test.js @@ -0,0 +1,32 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'website-www/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Component | application/status-badge', function (hooks) { + setupRenderingTest(hooks); + + test('it renders the status in uppercase', async function (assert) { + await render(hbs``); + assert.dom('[data-test-status-badge]').hasText('ACCEPTED'); + + await render(hbs``); + assert.dom('[data-test-status-badge]').hasText('PENDING'); + + await render(hbs``); + assert.dom('[data-test-status-badge]').hasText('CHANGES REQUESTED'); + }); + + test('it applies the correct status class', async function (assert) { + await render(hbs``); + assert.dom('[data-test-status-badge]').hasClass('status-badge--accepted'); + + await render(hbs``); + assert.dom('[data-test-status-badge]').hasClass('status-badge--rejected'); + }); + + test('it fallbacks to PENDING for unknown status', async function (assert) { + await render(hbs``); + assert.dom('[data-test-status-badge]').hasText('PENDING'); + }); +}); diff --git a/tests/unit/controllers/applications/detail-test.js b/tests/unit/controllers/applications/detail-test.js new file mode 100644 index 00000000..de600713 --- /dev/null +++ b/tests/unit/controllers/applications/detail-test.js @@ -0,0 +1,91 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'website-www/tests/helpers'; +import { APPLICATIONS_DATA } from 'website-www/tests/constants/application-data'; + +module('Unit | Controller | applications/detail', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function () { + this.controller = this.owner.lookup('controller:applications/detail'); + }); + + test('application details controller exists', function (assert) { + assert.ok(this.controller, 'Controller for application detail exists!'); + }); + + test('isAdmin correctly identifies super_user role', function (assert) { + this.controller.model = { currentUser: { roles: { super_user: true } } }; + assert.true(this.controller.isAdmin, 'True when super_user'); + + this.controller.model = { currentUser: { roles: { super_user: false } } }; + assert.false(this.controller.isAdmin, 'False when not super_user'); + }); + + test('isApplicant correctly identifies application owner', function (assert) { + const userId = APPLICATIONS_DATA.userId; + this.controller.model = { + currentUser: { id: userId }, + application: { userId }, + }; + assert.true(this.controller.isApplicant, 'True when IDs match'); + + this.controller.model = { + currentUser: { id: userId }, + application: { userId: 'other' }, + }; + assert.false(this.controller.isApplicant, 'False when IDs mismatch'); + }); + + test('canAccessApplication correctly returns application access', function (assert) { + this.controller.model = { + currentUser: { roles: { super_user: true } }, + application: { userId: 'other' }, + }; + assert.true(this.controller.canAccessApplication, 'Admin access'); + + this.controller.model = { + currentUser: { id: 'user1' }, + application: { userId: 'user1' }, + }; + assert.true(this.controller.canAccessApplication, 'Applicant access'); + + this.controller.model = { + currentUser: { id: 'user1', roles: { super_user: false } }, + application: { userId: 'user2' }, + }; + assert.false(this.controller.canAccessApplication, 'No access'); + }); + + test('aboutYouSections getter formats the data correctly', function (assert) { + this.controller.model = { application: APPLICATIONS_DATA }; + + const expected = [ + { label: 'Introduction', value: APPLICATIONS_DATA.intro.introduction }, + { label: 'Fun Fact', value: APPLICATIONS_DATA.intro.funFact }, + { label: 'For Fun', value: APPLICATIONS_DATA.intro.forFun }, + { label: 'Why Join Us', value: APPLICATIONS_DATA.intro.whyRds }, + ]; + + assert.deepEqual( + this.controller.aboutYouSections, + expected, + 'Formatted correctly', + ); + }); + + test('hasFeedback correctly identifies feedback presence', function (assert) { + this.controller.model = { application: APPLICATIONS_DATA }; + assert.true(this.controller.hasFeedback, 'True when feedback present'); + + this.controller.model = { application: { feedback: [] } }; + assert.false(this.controller.hasFeedback, 'False when empty'); + }); + + test('showAdminMessage correctly maps application status', function (assert) { + this.controller.model = { application: { status: 'pending' } }; + assert.ok(this.controller.showAdminMessage.includes('reviewing')); + + this.controller.model = { application: { status: 'accepted' } }; + assert.ok(this.controller.showAdminMessage.includes('approved')); + }); +}); diff --git a/tests/unit/routes/applications/detail-test.js b/tests/unit/routes/applications/detail-test.js index 6175738c..8335e429 100644 --- a/tests/unit/routes/applications/detail-test.js +++ b/tests/unit/routes/applications/detail-test.js @@ -28,13 +28,12 @@ module('Unit | Route | applications/detail', function (hooks) { test('fetches application by id successfully', async function (assert) { const mockApplication = { id: '123', userId: 'user1' }; + const mockUser = { first_name: 'John' }; const applicationId = '123'; this.fetchStub .onCall(0) - .resolves( - new Response(JSON.stringify({ first_name: 'John' }), { status: 200 }), - ); + .resolves(new Response(JSON.stringify(mockUser), { status: 200 })); this.fetchStub.onCall(1).resolves( new Response(JSON.stringify({ application: mockApplication }), { @@ -44,7 +43,11 @@ module('Unit | Route | applications/detail', function (hooks) { const result = await this.route.model({ id: applicationId }); - assert.deepEqual(result, mockApplication, 'Returns application from API'); + assert.deepEqual( + result, + { application: mockApplication, currentUser: mockUser }, + 'Returns application and currentUser from API', + ); assert.ok( this.fetchStub.firstCall.calledWith( SELF_USER_PROFILE_URL, @@ -66,7 +69,11 @@ module('Unit | Route | applications/detail', function (hooks) { const result = await this.route.model({ id: '123' }); - assert.strictEqual(result, null, 'Returns null for 401'); + assert.deepEqual( + result, + { application: null, currentUser: null }, + 'Returns null object for 401', + ); assert.ok(this.route.toast.error.calledOnce, 'Error toast is displayed'); }); @@ -80,7 +87,11 @@ module('Unit | Route | applications/detail', function (hooks) { const result = await this.route.model({ id: '123' }); - assert.strictEqual(result, null, 'Returns null for 404'); + assert.deepEqual( + result, + { application: null, currentUser: null }, + 'Returns null object for 404', + ); assert.ok( this.route.toast.error.calledOnce, 'Error toast is displayed for 404', @@ -97,7 +108,11 @@ module('Unit | Route | applications/detail', function (hooks) { const result = await this.route.model({ id: '123' }); - assert.strictEqual(result, null, 'Returns null on error'); + assert.deepEqual( + result, + { application: null, currentUser: null }, + 'Returns null object on error', + ); assert.ok(this.route.toast.error.calledOnce, 'Error toast is displayed'); }); });