- {{#if this.stepData.two.college}}
- {{this.stepData.two.college}}
+ {{#if this.stepData.two.institution}}
+ {{this.stepData.two.institution}}
{{else}}
Not provided
{{/if}}
diff --git a/app/components/new-join-steps/new-step-three.js b/app/components/new-join-steps/new-step-three.js
index 74e6c16d..6aaba644 100644
--- a/app/components/new-join-steps/new-step-three.js
+++ b/app/components/new-join-steps/new-step-three.js
@@ -5,7 +5,9 @@ import {
} from '../../constants/new-join-form';
export default class NewStepThreeComponent extends BaseStepComponent {
- storageKey = STEP_DATA_STORAGE_KEY.stepThree;
+ get storageKey() {
+ return STEP_DATA_STORAGE_KEY.stepThree;
+ }
stepValidation = {
forFun: NEW_STEP_LIMITS.stepThree.forFun,
funFact: NEW_STEP_LIMITS.stepThree.funFact,
diff --git a/app/components/new-join-steps/new-step-two.hbs b/app/components/new-join-steps/new-step-two.hbs
index a1dba022..91512ce7 100644
--- a/app/components/new-join-steps/new-step-two.hbs
+++ b/app/components/new-join-steps/new-step-two.hbs
@@ -27,18 +27,18 @@
{{/if}}
diff --git a/app/components/new-join-steps/new-step-two.js b/app/components/new-join-steps/new-step-two.js
index 41a35b67..c7b61423 100644
--- a/app/components/new-join-steps/new-step-two.js
+++ b/app/components/new-join-steps/new-step-two.js
@@ -5,10 +5,12 @@ import {
} from '../../constants/new-join-form';
export default class NewStepTwoComponent extends BaseStepComponent {
- storageKey = STEP_DATA_STORAGE_KEY.stepTwo;
+ get storageKey() {
+ return STEP_DATA_STORAGE_KEY.stepTwo;
+ }
stepValidation = {
skills: NEW_STEP_LIMITS.stepTwo.skills,
- college: NEW_STEP_LIMITS.stepTwo.college,
+ institution: NEW_STEP_LIMITS.stepTwo.institution,
introduction: NEW_STEP_LIMITS.stepTwo.introduction,
};
}
diff --git a/app/components/new-stepper.js b/app/components/new-stepper.js
index da71fa8b..9032c13a 100644
--- a/app/components/new-stepper.js
+++ b/app/components/new-stepper.js
@@ -2,14 +2,23 @@ import { action } from '@ember/object';
import { service } from '@ember/service';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
-import { CREATE_APPLICATION_URL } from '../constants/apis';
+import {
+ CREATE_APPLICATION_URL,
+ UPDATE_APPLICATION_URL,
+} from '../constants/apis';
import {
NEW_FORM_STEPS,
USER_ROLE_MAP,
STEP_DATA_STORAGE_KEY,
} from '../constants/new-join-form';
import { TOAST_OPTIONS } from '../constants/toast-options';
-import { getLocalStorageItem, setLocalStorageItem } from '../utils/storage';
+import { socialFields } from '../constants/applications';
+import {
+ getLocalStorageItem,
+ safeParse,
+ setLocalStorageItem,
+} from '../utils/storage';
+import apiRequest from '../utils/api-request';
export default class NewStepperComponent extends Component {
MIN_STEP = 0;
@@ -52,6 +61,10 @@ export default class NewStepperComponent extends Component {
});
}
+ get isEditMode() {
+ return this.args.isEditMode;
+ }
+
get showPreviousButton() {
return this.currentStep > this.MIN_STEP + 1;
}
@@ -116,19 +129,18 @@ export default class NewStepperComponent extends Component {
this.isSubmitting = true;
try {
const applicationData = this.collectApplicationData();
+ const url = this.isEditMode
+ ? UPDATE_APPLICATION_URL(this.onboarding.applicationData?.id)
+ : CREATE_APPLICATION_URL;
+ const method = this.isEditMode ? 'PATCH' : 'POST';
- const response = await fetch(CREATE_APPLICATION_URL, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- credentials: 'include',
- body: JSON.stringify(applicationData),
- });
+ const response = await apiRequest(url, method, applicationData);
if (response.status === 409) {
this.toast.error(
- 'You have already submitted an application.',
+ this.isEditMode
+ ? 'You will be able to edit after 24 hrs.'
+ : 'You have already submitted an application.',
'Application Exists!',
TOAST_OPTIONS,
);
@@ -138,7 +150,8 @@ export default class NewStepperComponent extends Component {
if (!response.ok) {
this.toast.error(
- response.message || 'Failed to submit application. Please try again.',
+ response.message ||
+ `Failed to ${this.isEditMode ? 'edit' : 'submit'} application. Please try again.`,
'Error!',
TOAST_OPTIONS,
);
@@ -146,11 +159,12 @@ export default class NewStepperComponent extends Component {
return;
}
- const data = await response.json();
- this.applicationId = data.application?.id;
+ await response.json();
this.toast.success(
- 'Application submitted successfully!',
+ this.isEditMode
+ ? 'You have successfully edited the application'
+ : 'Application submitted successfully!',
'Success!',
TOAST_OPTIONS,
);
@@ -163,7 +177,7 @@ export default class NewStepperComponent extends Component {
} catch (error) {
console.error('Error submitting application:', error);
this.toast.error(
- 'Failed to submit application. Please try again.',
+ `Failed to ${this.isEditMode ? 'edit' : 'submit'} application. Please try again.`,
'Error!',
TOAST_OPTIONS,
);
@@ -172,23 +186,13 @@ export default class NewStepperComponent extends Component {
}
collectApplicationData() {
- const stepOneData = JSON.parse(
- getLocalStorageItem(STEP_DATA_STORAGE_KEY.stepOne) || '{}',
- );
- const stepTwoData = JSON.parse(
- getLocalStorageItem(STEP_DATA_STORAGE_KEY.stepTwo) || '{}',
- );
- const stepThreeData = JSON.parse(
- getLocalStorageItem(STEP_DATA_STORAGE_KEY.stepThree) || '{}',
- );
- const stepFourData = JSON.parse(
- getLocalStorageItem(STEP_DATA_STORAGE_KEY.stepFour) || '{}',
- );
- const stepFiveData = JSON.parse(
- getLocalStorageItem(STEP_DATA_STORAGE_KEY.stepFive) || '{}',
- );
-
- return {
+ const stepOneData = safeParse(STEP_DATA_STORAGE_KEY.stepOne);
+ const stepTwoData = safeParse(STEP_DATA_STORAGE_KEY.stepTwo);
+ const stepThreeData = safeParse(STEP_DATA_STORAGE_KEY.stepThree);
+ const stepFourData = safeParse(STEP_DATA_STORAGE_KEY.stepFour);
+ const stepFiveData = safeParse(STEP_DATA_STORAGE_KEY.stepFive);
+
+ const formData = {
...stepOneData,
...stepTwoData,
...stepThreeData,
@@ -197,6 +201,55 @@ export default class NewStepperComponent extends Component {
role: stepOneData.role ? USER_ROLE_MAP[stepOneData.role] : '',
numberOfHours: Number(stepFiveData.numberOfHours) || 0,
};
+
+ if (this.isEditMode && this.onboarding.applicationData) {
+ return this.getModifiedFields(formData, this.onboarding.applicationData);
+ }
+
+ return formData;
+ }
+
+ getModifiedFields(formData, originalApplication) {
+ const modifiedData = {};
+
+ const originalValues = {
+ firstName: originalApplication.biodata?.firstName,
+ lastName: originalApplication.biodata?.lastName,
+ city: originalApplication.location?.city,
+ state: originalApplication.location?.state,
+ country: originalApplication.location?.country,
+ institution: originalApplication.professional?.institution,
+ skills: originalApplication.professional?.skills,
+ introduction: originalApplication.professional?.introduction,
+ forFun: originalApplication.intro?.forFun,
+ funFact: originalApplication.intro?.funFact,
+ whyRds: originalApplication.intro?.whyRds,
+ numberOfHours: originalApplication.intro?.numberOfHours,
+ foundFrom: originalApplication.foundFrom,
+ role: originalApplication.role,
+ imageUrl: originalApplication.imageUrl,
+ };
+ Object.entries(originalValues).forEach(([formKey, originalValue]) => {
+ if (formData[formKey] !== originalValue) {
+ modifiedData[formKey] = formData[formKey];
+ }
+ });
+
+ const socialLinkChanges = socialFields.reduce((acc, field) => {
+ const formValue = formData[field];
+ const originalValue = originalApplication.socialLink?.[field];
+
+ if (formValue && formValue !== originalValue) {
+ acc[field] = formValue;
+ }
+ return acc;
+ }, {});
+
+ if (Object.keys(socialLinkChanges).length) {
+ modifiedData.socialLink = socialLinkChanges;
+ }
+
+ return modifiedData;
}
clearAllStepData() {
diff --git a/app/constants/apis.js b/app/constants/apis.js
index ece7f3ec..668ed29a 100644
--- a/app/constants/apis.js
+++ b/app/constants/apis.js
@@ -55,6 +55,10 @@ export const APPLICATION_BY_ID_URL = (applicationId) => {
export const CREATE_APPLICATION_URL = `${APPS.API_BACKEND}/applications`;
+export const UPDATE_APPLICATION_URL = (applicationId) => {
+ return `${APPS.API_BACKEND}/applications/${applicationId}`;
+};
+
export const NUDGE_APPLICATION_URL = (applicationId) => {
return `${APPS.API_BACKEND}/applications/${applicationId}/nudge`;
};
diff --git a/app/constants/applications.js b/app/constants/applications.js
index c3fda429..e776a8cb 100644
--- a/app/constants/applications.js
+++ b/app/constants/applications.js
@@ -23,6 +23,17 @@ export const mapSocialUrls = {
behance: 'https://behance.net',
};
+export const socialFields = [
+ 'phoneNo',
+ 'twitter',
+ 'linkedin',
+ 'instagram',
+ 'github',
+ 'peerlist',
+ 'behance',
+ 'dribble',
+];
+
export function adminMessage(status) {
switch (status) {
case 'pending':
diff --git a/app/constants/new-join-form.js b/app/constants/new-join-form.js
index cd926676..456ff6ab 100644
--- a/app/constants/new-join-form.js
+++ b/app/constants/new-join-form.js
@@ -44,7 +44,7 @@ export const NEW_STEP_LIMITS = {
},
stepTwo: {
skills: { min: 5, max: 20 },
- college: { min: 1 },
+ institution: { min: 1 },
introduction: { min: 100, max: 500 },
},
stepThree: {
diff --git a/app/constants/urls.js b/app/constants/urls.js
index f3fafe3c..38bc6030 100644
--- a/app/constants/urls.js
+++ b/app/constants/urls.js
@@ -33,7 +33,7 @@ const APP_URLS = {
DASHBOARD: `${SCHEME}staging-dashboard.${DOMAIN}`,
},
development: {
- HOME: '/',
+ HOME: `${SCHEME}dev.${DOMAIN}`,
WELCOME: `${SCHEME}staging-welcome.${DOMAIN}`,
GOTO: '/goto',
EVENTS: '/events',
diff --git a/app/controllers/join.js b/app/controllers/join.js
index 55f7a583..6125cbd9 100644
--- a/app/controllers/join.js
+++ b/app/controllers/join.js
@@ -18,7 +18,7 @@ export default class JoinController extends Controller {
ANKUSH_TWITTER = ANKUSH_TWITTER;
- queryParams = ['step', 'dev', 'oldOnboarding'];
+ queryParams = ['step', 'dev', 'oldOnboarding', 'edit'];
get isDevMode() {
return this.featureFlag.isDevMode;
@@ -44,6 +44,10 @@ export default class JoinController extends Controller {
return this.login.isLoggedIn && this.login.userData;
}
+ get isEditMode() {
+ return this.edit === 'true';
+ }
+
@action async handleGenerateChaincode(e) {
e.preventDefault();
diff --git a/app/templates/join.hbs b/app/templates/join.hbs
index d562b939..c5cdfecc 100644
--- a/app/templates/join.hbs
+++ b/app/templates/join.hbs
@@ -32,7 +32,7 @@
{{else}}
- {{#if this.applicationData}}
+ {{#if (and this.applicationData (not this.isEditMode))}}
{{else}}
{{#if this.isDevMode}}
-
+
{{else if this.isOldOnboarding}}
{};
+
+ class OnboardingStub extends Service {
+ applicationData = null;
+ }
+
+ class ToastStub extends Service {
+ success = sinon.stub();
+ error = sinon.stub();
+ }
+
+ this.owner.register('service:onboarding', OnboardingStub);
+ this.owner.register('service:toast', ToastStub);
+
+ const onboarding = this.owner.lookup('service:onboarding');
+ onboarding.applicationData = APPLICATIONS_DATA;
+ });
+
+ hooks.afterEach(async function () {
+ Object.values(STEP_DATA_STORAGE_KEY).forEach((key) =>
+ localStorage.removeItem(key),
+ );
+ localStorage.removeItem('isValid');
+ localStorage.removeItem('currentStep');
+ });
+
+ test('initializeFormState prefers localStorage over applicationData', async function (assert) {
+ localStorage.setItem(
+ 'newStepOneData',
+ JSON.stringify({
+ firstName: 'Local',
+ lastName: 'Storage',
+ role: 'Designer',
+ }),
+ );
+
+ await render(
+ hbs``,
+ );
+
+ const stepOneData = JSON.parse(localStorage.getItem('newStepOneData'));
+ assert.strictEqual(stepOneData.firstName, 'Local');
+ assert.strictEqual(stepOneData.lastName, 'Storage');
+ assert.strictEqual(stepOneData.role, 'Designer');
+ assert.notStrictEqual(
+ stepOneData.firstName,
+ APPLICATIONS_DATA.biodata.firstName,
+ );
+ });
+
+ test('initializeFormState loads from applicationData when localStorage empty', async function (assert) {
+ await render(
+ hbs``,
+ );
+
+ const stepOneData = JSON.parse(
+ localStorage.getItem(STEP_DATA_STORAGE_KEY.stepOne),
+ );
+ assert.strictEqual(
+ stepOneData.firstName,
+ APPLICATIONS_DATA.biodata.firstName,
+ );
+ assert.strictEqual(
+ stepOneData.lastName,
+ APPLICATIONS_DATA.biodata.lastName,
+ );
+ assert.strictEqual(stepOneData.city, APPLICATIONS_DATA.location.city);
+ });
+
+ test('initializeFormState loads stepTwo data from applicationData', async function (assert) {
+ await render(
+ hbs``,
+ );
+
+ const stepTwoData = JSON.parse(
+ localStorage.getItem(STEP_DATA_STORAGE_KEY.stepTwo),
+ );
+ assert.strictEqual(
+ stepTwoData.institution,
+ APPLICATIONS_DATA.professional.institution,
+ );
+ assert.strictEqual(
+ stepTwoData.skills,
+ APPLICATIONS_DATA.professional.skills,
+ );
+ assert.strictEqual(
+ stepTwoData.introduction,
+ APPLICATIONS_DATA.intro.introduction,
+ );
+ });
+
+ test('initializeFormState loads stepThree data from applicationData', async function (assert) {
+ await render(
+ hbs``,
+ );
+
+ const stepThreeData = JSON.parse(
+ localStorage.getItem(STEP_DATA_STORAGE_KEY.stepThree),
+ );
+ assert.strictEqual(stepThreeData.forFun, APPLICATIONS_DATA.intro.forFun);
+ assert.strictEqual(stepThreeData.funFact, APPLICATIONS_DATA.intro.funFact);
+ });
+
+ test('initializeFormState loads stepFour data from applicationData', async function (assert) {
+ await render(
+ hbs``,
+ );
+
+ const stepFourData = JSON.parse(
+ localStorage.getItem(STEP_DATA_STORAGE_KEY.stepFour),
+ );
+ assert.strictEqual(
+ stepFourData.twitter,
+ APPLICATIONS_DATA.socialLink.twitter,
+ );
+ assert.strictEqual(
+ stepFourData.instagram,
+ APPLICATIONS_DATA.socialLink.instagram,
+ );
+ });
+
+ test('initializeFormState loads stepFive data from applicationData', async function (assert) {
+ await render(
+ hbs``,
+ );
+
+ const stepFiveData = JSON.parse(
+ localStorage.getItem(STEP_DATA_STORAGE_KEY.stepFive),
+ );
+ assert.strictEqual(stepFiveData.whyRds, APPLICATIONS_DATA.intro.whyRds);
+ assert.strictEqual(stepFiveData.foundFrom, APPLICATIONS_DATA.foundFrom);
+ assert.strictEqual(
+ stepFiveData.numberOfHours,
+ APPLICATIONS_DATA.intro.numberOfHours,
+ );
+ });
+});
diff --git a/tests/integration/components/new-stepper-test.js b/tests/integration/components/new-stepper-test.js
index 4be21799..0766e54e 100644
--- a/tests/integration/components/new-stepper-test.js
+++ b/tests/integration/components/new-stepper-test.js
@@ -1,255 +1,365 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'website-www/tests/helpers';
-import { render, click } from '@ember/test-helpers';
+import { render, click, settled } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import sinon from 'sinon';
import Service from '@ember/service';
import { STEP_DATA_STORAGE_KEY } from 'website-www/constants/new-join-form';
-
-function clearAllStepData() {
- Object.values(STEP_DATA_STORAGE_KEY).forEach((key) =>
- localStorage.removeItem(key),
- );
- localStorage.removeItem('isValid');
- localStorage.removeItem('currentStep');
-}
-
-function setupFetchStub(response = {}) {
- sinon.restore();
- return sinon.stub(window, 'fetch').resolves({
- ok: true,
- status: 201,
- json: () => Promise.resolve({ application: { id: 'test-id' } }),
- ...response,
- });
-}
+import {
+ APPLICATIONS_DATA,
+ NEW_STEPS_APPLICATIONS_DATA,
+} from 'website-www/tests/constants/application-data';
module('Integration | Component | new-stepper', function (hooks) {
setupRenderingTest(hooks);
+ function seedFormDataToLocalStorage(formData) {
+ Object.entries(formData).forEach(([step, data]) => {
+ localStorage.setItem(STEP_DATA_STORAGE_KEY[step], JSON.stringify(data));
+ });
+ }
+
+ function clearFormDataFromLocalStorage() {
+ Object.values(STEP_DATA_STORAGE_KEY).forEach((key) => {
+ localStorage.removeItem(key);
+ });
+ localStorage.removeItem('isValid');
+ localStorage.removeItem('currentStep');
+ }
+
hooks.beforeEach(function () {
- this.routerStub = {
+ this.routerService = {
transitionTo: sinon.stub(),
replaceWith: sinon.stub(),
currentRoute: { queryParams: {} },
};
const testContext = this;
- class ToastStub extends Service {
+ class ToastServiceStub extends Service {
constructor(...args) {
super(...args);
this.success = sinon.stub();
this.error = sinon.stub();
- testContext.toastStub = this;
+ testContext.toastService = this;
}
}
- this.owner.register('service:router', this.routerStub, {
+ class OnboardingServiceStub extends Service {
+ applicationData = null;
+ }
+ class JoinApplicationTermsServiceStub extends Service {
+ hasUserAcceptedTerms = false;
+ }
+
+ this.owner.register('service:router', this.routerService, {
instantiate: false,
});
- this.owner.register('service:toast', ToastStub);
-
- this.mockStepData = {
- stepOne: {
- firstName: 'John',
- lastName: 'Doe',
- role: 'Developer',
- city: 'San Francisco',
- state: 'CA',
- country: 'USA',
- imageUrl: 'https://example.com/photo.jpg',
- },
- stepTwo: {
- skills: 'JavaScript, Python',
- college: 'Stanford University',
- introduction: 'Passionate developer with 5 years experience.',
- },
- stepThree: {
- forFun: 'I love hiking and photography.',
- funFact: 'I have visited 20 countries.',
- },
- stepFour: {
- phoneNo: '+1 555-123-4567',
- twitter: '@johndoe',
- github: 'github.com/johndoe',
- linkedin: 'linkedin.com/in/johndoe',
- instagram: 'instagram.com/johndoe',
- },
- stepFive: {
- numberOfHours: '20',
- whyRDS: 'I want to contribute to meaningful projects.',
- anythingElse: 'Looking forward to collaborating!',
- },
- };
+ this.owner.register('service:toast', ToastServiceStub);
+ this.owner.register('service:onboarding', OnboardingServiceStub);
+ this.owner.register(
+ 'service:joinApplicationTerms',
+ JoinApplicationTermsServiceStub,
+ );
- Object.entries(this.mockStepData).forEach(([step, data]) => {
- localStorage.setItem(STEP_DATA_STORAGE_KEY[step], JSON.stringify(data));
+ seedFormDataToLocalStorage(NEW_STEPS_APPLICATIONS_DATA);
+
+ this.apiStub = sinon.stub(window, 'fetch').resolves({
+ ok: true,
+ status: 201,
+ json: () => Promise.resolve({ application: { id: 'app-123' } }),
});
- this.fetchStub = setupFetchStub();
});
hooks.afterEach(function () {
- clearAllStepData();
+ clearFormDataFromLocalStorage();
sinon.restore();
});
- test('it renders the welcome screen at step 0', async function (assert) {
- await render(hbs``);
-
- assert.dom('[data-test="stepper"]').exists();
- assert.dom('[data-test="welcome-screen"]').exists();
- assert
- .dom('[data-test="welcome-greeting"]')
- .hasText('Ready to apply to Real Dev Squad?');
- assert.dom('[data-test-button="start"]').exists();
- });
-
- test('start button is disabled when terms are not accepted', async function (assert) {
- await render(hbs``);
- assert.dom('[data-test-button="start"]').isDisabled();
- });
-
- test('start button is enabled when terms are accepted', async function (assert) {
- const terms = this.owner.lookup('service:joinApplicationTerms');
- terms.hasUserAcceptedTerms = true;
-
- await render(hbs``);
- assert.dom('[data-test-button="start"]').isNotDisabled();
- });
+ module('Edit Application', function (hooks) {
+ hooks.beforeEach(function () {
+ const onboardingService = this.owner.lookup('service:onboarding');
+ onboardingService.applicationData = APPLICATIONS_DATA;
+ });
- test('handleSubmit success - submits form data and redirects', async function (assert) {
- await render(hbs``);
- await click('[data-test-button="submit-review"]');
-
- assert.ok(this.fetchStub.calledOnce);
- assert.ok(this.fetchStub.firstCall.args[0].includes('/applications'));
- assert.strictEqual(this.fetchStub.firstCall.args[1].method, 'POST');
- assert.ok(
- this.toastStub.success.calledWith(
- 'Application submitted successfully!',
- 'Success!',
- ),
- );
- assert.ok(
- this.routerStub.replaceWith.calledWith('join', {
- queryParams: { dev: true },
- }),
- );
- assert.strictEqual(localStorage.getItem('isValid'), null);
- assert.strictEqual(localStorage.getItem('currentStep'), null);
- });
+ test('uses PATCH method for updating application', async function (assert) {
+ await render(hbs``);
+ await click('[data-test-button="submit-review"]');
- test('handleSubmit handles 409 conflict - shows already submitted error', async function (assert) {
- setupFetchStub({
- ok: false,
- status: 409,
- message: 'Application already exists',
+ assert.strictEqual(
+ this.apiStub.firstCall.args[1].method,
+ 'PATCH',
+ 'Uses PATCH method for editing',
+ );
});
- await render(hbs``);
- await click('[data-test-button="submit-review"]');
+ test('calls correct update API endpoint with application ID', async function (assert) {
+ await render(hbs``);
+ await click('[data-test-button="submit-review"]');
- assert.ok(
- this.toastStub.error.calledWith(
- 'You have already submitted an application.',
- 'Application Exists!',
- ),
- );
- assert.notOk(this.routerStub.replaceWith.called);
- assert.notOk(this.toastStub.success.called);
- });
+ assert.ok(
+ this.apiStub.firstCall.args[0].includes(
+ `/applications/${APPLICATIONS_DATA.id}`,
+ ),
+ 'Correct update endpoint called with application ID',
+ );
+ });
- test('handleSubmit handles API error - shows error toast', async function (assert) {
- setupFetchStub({
- ok: false,
- status: 500,
- message: 'Internal Server Error',
+ test('displays success toast with edit-specific message', async function (assert) {
+ await render(hbs``);
+ await click('[data-test-button="submit-review"]');
+
+ assert.ok(
+ this.toastService.success.calledWith(
+ 'You have successfully edited the application',
+ 'Success!',
+ ),
+ 'Edit success toast displayed',
+ );
});
- await render(hbs``);
- await click('[data-test-button="submit-review"]');
+ test('handles 24hr edit restriction with 409 conflict error', async function (assert) {
+ sinon.restore();
+ this.apiStub = sinon.stub(window, 'fetch').resolves({
+ ok: false,
+ status: 409,
+ message: '24 hour restriction',
+ });
+
+ await render(hbs``);
+ await click('[data-test-button="submit-review"]');
+
+ assert.ok(
+ this.toastService.error.calledWith(
+ 'You will be able to edit after 24 hrs.',
+ 'Application Exists!',
+ ),
+ 'Displays 24hr restriction error toast',
+ );
+ assert.notOk(
+ this.routerService.replaceWith.called,
+ 'Does not redirect when edit blocked',
+ );
+ assert.notOk(
+ this.toastService.success.called,
+ 'Does not show success toast when edit blocked',
+ );
+ });
- assert.ok(this.toastStub.error.called);
- assert.notOk(this.routerStub.replaceWith.called);
- });
+ test('handles server error in edit mode with edit-specific message', async function (assert) {
+ sinon.restore();
+ this.apiStub = sinon.stub(window, 'fetch').resolves({
+ ok: false,
+ status: 500,
+ });
+
+ await render(hbs``);
+ await click('[data-test-button="submit-review"]');
+
+ assert.ok(
+ this.toastService.error.calledWithMatch(
+ 'Failed to edit application. Please try again.',
+ ),
+ 'Edit error toast displayed with edit-specific message',
+ );
+ });
- test('handleSubmit handles network failure - shows error toast', async function (assert) {
- sinon.restore();
- sinon.stub(window, 'fetch').rejects(new Error('Network error'));
+ test('handles network failure in edit mode', async function (assert) {
+ sinon.restore();
+ sinon.stub(window, 'fetch').rejects(new Error('Network error'));
- await render(hbs``);
- await click('[data-test-button="submit-review"]');
+ await render(hbs``);
+ await click('[data-test-button="submit-review"]');
- assert.ok(
- this.toastStub.error.calledWith(
- 'Failed to submit application. Please try again.',
- 'Error!',
- ),
- );
- });
+ assert.ok(
+ this.toastService.error.calledWith(
+ 'Failed to edit application. Please try again.',
+ 'Error!',
+ ),
+ 'Network error toast displayed in edit mode',
+ );
+ });
- test('submit button is enabled before and after submission', async function (assert) {
- await render(hbs``);
+ test('submit button is disabled during edit submission', async function (assert) {
+ let resolveApi;
+ const deferredPromise = new Promise((resolve) => {
+ resolveApi = resolve;
+ });
+
+ sinon.restore();
+ sinon.stub(window, 'fetch').returns(deferredPromise);
+
+ await render(hbs``);
+
+ const clickPromise = click('[data-test-button="submit-review"]');
+ await new Promise((resolve) => setTimeout(resolve, 10));
+ assert
+ .dom('[data-test-button="submit-review"]')
+ .isDisabled('Submit button disabled during edit submission');
+
+ resolveApi({ ok: true, status: 201, json: () => Promise.resolve({}) });
+ await clickPromise;
+ await settled();
+ assert
+ .dom('[data-test-button="submit-review"]')
+ .isNotDisabled('Submit button enabled after edit completes');
+ });
- assert.dom('[data-test-button="submit-review"]').isNotDisabled();
- await click('[data-test-button="submit-review"]');
- assert.dom('[data-test-button="submit-review"]').isNotDisabled();
- });
+ test('redirects to join page after successful edit', async function (assert) {
+ await render(hbs``);
+ await click('[data-test-button="submit-review"]');
- test('collectApplicationData merges all step data correctly', async function (assert) {
- await render(hbs``);
- await click('[data-test-button="submit-review"]');
-
- const submittedData = JSON.parse(this.fetchStub.firstCall.args[1].body);
-
- assert.strictEqual(submittedData.firstName, 'John');
- assert.strictEqual(submittedData.lastName, 'Doe');
- assert.strictEqual(submittedData.role, 'developer');
- assert.strictEqual(submittedData.city, 'San Francisco');
- assert.strictEqual(submittedData.college, 'Stanford University');
- assert.strictEqual(submittedData.skills, 'JavaScript, Python');
- assert.strictEqual(submittedData.forFun, 'I love hiking and photography.');
- assert.deepEqual(submittedData.socialLink, this.mockStepData.stepFour);
- assert.strictEqual(submittedData.numberOfHours, 20);
+ assert.ok(
+ this.routerService.replaceWith.calledWith('join', {
+ queryParams: { dev: true },
+ }),
+ 'Redirects to join page after successful edit',
+ );
+ });
});
- test('collectApplicationData handles empty step data gracefully', async function (assert) {
- Object.values(STEP_DATA_STORAGE_KEY).forEach((key) =>
- localStorage.setItem(key, '{}'),
- );
-
- await render(hbs``);
- await click('[data-test-button="submit-review"]');
+ module('Data Collection and Transformation', function () {
+ test('collects and transforms all form data correctly', async function (assert) {
+ assert.expect(6);
+ await render(hbs``);
+ await click('[data-test-button="submit-review"]');
+
+ const submittedData = JSON.parse(this.apiStub.firstCall.args[1].body);
+
+ assert.strictEqual(
+ submittedData.firstName,
+ NEW_STEPS_APPLICATIONS_DATA.stepOne.firstName,
+ 'First name collected correctly',
+ );
+ assert.strictEqual(
+ submittedData.role,
+ NEW_STEPS_APPLICATIONS_DATA.stepOne.role.toLowerCase(),
+ 'Role transformed to lowercase format',
+ );
+ assert.strictEqual(
+ submittedData.city,
+ NEW_STEPS_APPLICATIONS_DATA.stepOne.city,
+ 'City collected correctly',
+ );
+ assert.strictEqual(
+ submittedData.country,
+ NEW_STEPS_APPLICATIONS_DATA.stepOne.country,
+ 'Country collected correctly',
+ );
+ assert.strictEqual(
+ submittedData.institution,
+ NEW_STEPS_APPLICATIONS_DATA.stepTwo.institution,
+ 'Institution collected correctly',
+ );
+ assert.strictEqual(
+ submittedData.skills,
+ NEW_STEPS_APPLICATIONS_DATA.stepTwo.skills,
+ 'Skills collected correctly',
+ );
+ });
- const submittedData = JSON.parse(this.fetchStub.firstCall.args[1].body);
+ test('groups social links under socialLink property', async function (assert) {
+ await render(hbs``);
+ await click('[data-test-button="submit-review"]');
+
+ const submittedData = JSON.parse(this.apiStub.firstCall.args[1].body);
+
+ assert.deepEqual(
+ submittedData.socialLink,
+ NEW_STEPS_APPLICATIONS_DATA.stepFour,
+ 'Social links grouped correctly',
+ );
+ assert.strictEqual(
+ submittedData.socialLink.github,
+ NEW_STEPS_APPLICATIONS_DATA.stepFour.github,
+ 'GitHub link correct',
+ );
+ assert.strictEqual(
+ submittedData.socialLink.linkedin,
+ NEW_STEPS_APPLICATIONS_DATA.stepFour.linkedin,
+ 'LinkedIn link correct',
+ );
+ });
- assert.notOk(submittedData.role);
- assert.strictEqual(submittedData.numberOfHours, 0);
+ test('handles empty form data gracefully', async function (assert) {
+ clearFormDataFromLocalStorage();
+ Object.values(STEP_DATA_STORAGE_KEY).forEach((key) => {
+ localStorage.setItem(key, '{}');
+ });
+
+ await render(hbs``);
+ await click('[data-test-button="submit-review"]');
+
+ const submittedData = JSON.parse(this.apiStub.firstCall.args[1].body);
+
+ assert.notOk(submittedData.role, 'Empty role handled correctly');
+ assert.strictEqual(
+ submittedData.numberOfHours,
+ 0,
+ 'Empty hours defaults to 0',
+ );
+ assert.ok(
+ submittedData.socialLink,
+ 'Social link object exists even when empty',
+ );
+ });
});
- test('clearAllStepData removes all step storage keys', async function (assert) {
- assert.expect(5);
+ module('LocalStorage Management', function () {
+ test('clears all step data from localStorage after successful submission', async function (assert) {
+ assert.expect(5);
- Object.values(STEP_DATA_STORAGE_KEY).forEach((key) =>
- localStorage.setItem(key, JSON.stringify({ test: 'data' })),
- );
+ Object.values(STEP_DATA_STORAGE_KEY).forEach((key) => {
+ localStorage.setItem(key, JSON.stringify({ test: 'data' }));
+ });
- await render(hbs``);
- await click('[data-test-button="submit-review"]');
+ await render(hbs``);
+ await click('[data-test-button="submit-review"]');
- Object.values(STEP_DATA_STORAGE_KEY).forEach((key) => {
- assert.strictEqual(localStorage.getItem(key), null);
+ Object.values(STEP_DATA_STORAGE_KEY).forEach((key) => {
+ assert.strictEqual(
+ localStorage.getItem(key),
+ null,
+ `${key} removed from localStorage`,
+ );
+ });
});
- });
- test('clearAllStepData clears isValid and currentStep', async function (assert) {
- localStorage.setItem('isValid', 'true');
- localStorage.setItem('currentStep', '6');
-
- await render(hbs``);
- await click('[data-test-button="submit-review"]');
+ test('clears validation and step tracking from localStorage', async function (assert) {
+ localStorage.setItem('isValid', 'true');
+ localStorage.setItem('currentStep', '6');
+
+ await render(hbs``);
+ await click('[data-test-button="submit-review"]');
+
+ assert.strictEqual(
+ localStorage.getItem('isValid'),
+ null,
+ 'isValid removed',
+ );
+ assert.strictEqual(
+ localStorage.getItem('currentStep'),
+ null,
+ 'currentStep removed',
+ );
+ });
- assert.strictEqual(localStorage.getItem('isValid'), null);
- assert.strictEqual(localStorage.getItem('currentStep'), null);
+ test('does not clear localStorage when submission fails', async function (assert) {
+ sinon.restore();
+ this.apiStub = sinon.stub(window, 'fetch').resolves({
+ ok: false,
+ status: 500,
+ message: 'Error',
+ });
+ localStorage.setItem('isValid', 'true');
+
+ await render(hbs``);
+ await click('[data-test-button="submit-review"]');
+
+ assert.strictEqual(
+ localStorage.getItem('isValid'),
+ 'true',
+ 'localStorage preserved on error',
+ );
+ });
});
});
diff --git a/tests/unit/controllers/join-test.js b/tests/unit/controllers/join-test.js
index 60a612c4..27827384 100644
--- a/tests/unit/controllers/join-test.js
+++ b/tests/unit/controllers/join-test.js
@@ -21,7 +21,12 @@ module('Unit | Controller | join', function (hooks) {
test('it has queryParams', function (assert) {
let controller = this.owner.lookup('controller:join');
- assert.deepEqual(controller.queryParams, ['step', 'dev', 'oldOnboarding']);
+ assert.deepEqual(controller.queryParams, [
+ 'step',
+ 'dev',
+ 'oldOnboarding',
+ 'edit',
+ ]);
});
test('isOldOnboarding returns values correctly as per oldOnboarding query param', function (assert) {