diff --git a/app/components/new-join-steps/base-step.js b/app/components/new-join-steps/base-step.js
new file mode 100644
index 000000000..2f9578aef
--- /dev/null
+++ b/app/components/new-join-steps/base-step.js
@@ -0,0 +1,128 @@
+import { action } from '@ember/object';
+import { debounce } from '@ember/runloop';
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { JOIN_DEBOUNCE_TIME } from '../../constants/join';
+import { validateWordCount } from '../../utils/validator';
+import { scheduleOnce } from '@ember/runloop';
+import { getLocalStorageItem, setLocalStorageItem } from '../../utils/storage';
+
+export default class BaseStepComponent extends Component {
+ stepValidation = {};
+
+ @tracked data = {};
+ @tracked errorMessage = {};
+ @tracked wordCount = {};
+
+ get storageKey() {
+ return '';
+ }
+
+ postLoadInitialize() {}
+
+ constructor(...args) {
+ super(...args);
+ scheduleOnce('afterRender', this, this.initializeFormState);
+ }
+
+ initializeFormState() {
+ let saved = {};
+ try {
+ const stored = getLocalStorageItem(this.storageKey, '{}');
+ saved = stored ? JSON.parse(stored) : {};
+ } catch (e) {
+ console.warn('Failed to parse stored form data:', e);
+ saved = {};
+ }
+ this.data = saved;
+
+ this.errorMessage = Object.fromEntries(
+ Object.keys(this.stepValidation).map((k) => [k, '']),
+ );
+
+ this.wordCount = Object.fromEntries(
+ Object.keys(this.stepValidation).map((k) => {
+ let val = String(this.data[k] || '');
+ return [k, val.trim().split(/\s+/).filter(Boolean).length || 0];
+ }),
+ );
+
+ this.postLoadInitialize();
+
+ const valid = this.isDataValid();
+ this.args.setIsPreValid(valid);
+ setLocalStorageItem('isValid', String(valid));
+ }
+
+ @action inputHandler(e) {
+ if (!e?.target) return;
+ this.args.setIsPreValid(false);
+ const field = e.target.name;
+ const value = e.target.value;
+ debounce(this, this.handleFieldUpdate, field, value, JOIN_DEBOUNCE_TIME);
+ }
+
+ validateField(field, value) {
+ const limits = this.stepValidation[field];
+ const fieldType = limits?.type || 'text';
+
+ if (fieldType === 'select' || fieldType === 'dropdown') {
+ const hasValue = value && String(value).trim().length > 0;
+ return { isValid: hasValue };
+ }
+ return validateWordCount(value, limits);
+ }
+
+ isDataValid() {
+ for (const field of Object.keys(this.stepValidation)) {
+ const result = this.validateField(field, this.data[field]);
+ if (!result.isValid) return false;
+ }
+ return true;
+ }
+
+ handleFieldUpdate(field, value) {
+ this.updateFieldValue(field, value);
+ const result = this.validateField(field, value);
+ this.updateWordCount(field, result);
+ this.updateErrorMessage(field, result);
+ this.syncFormValidity();
+ }
+
+ updateFieldValue(field, value) {
+ this.data = { ...this.data, [field]: value };
+ setLocalStorageItem(this.storageKey, JSON.stringify(this.data));
+ }
+
+ updateWordCount(field, result) {
+ const wordCount = result.wordCount ?? 0;
+ this.wordCount = { ...this.wordCount, [field]: wordCount };
+ }
+
+ updateErrorMessage(field, result) {
+ this.errorMessage = {
+ ...this.errorMessage,
+ [field]: this.formatError(field, result),
+ };
+ }
+
+ formatError(field, result) {
+ const limits = this.stepValidation[field];
+ if (result.isValid) return '';
+
+ const fieldType = limits?.type || 'text';
+ if (fieldType === 'select' || fieldType === 'dropdown') {
+ return 'Please choose an option';
+ }
+ if (result.remainingToMin) {
+ return `At least ${result.remainingToMin} more word(s) required`;
+ }
+ return `Maximum ${limits?.max ?? 'N/A'} words allowed`;
+ }
+
+ syncFormValidity() {
+ const allValid = this.isDataValid();
+ this.args.setIsValid(allValid);
+ setLocalStorageItem('isValid', String(allValid));
+ }
+}
diff --git a/app/components/new-join-steps/new-step-one.hbs b/app/components/new-join-steps/new-step-one.hbs
new file mode 100644
index 000000000..bbe08122d
--- /dev/null
+++ b/app/components/new-join-steps/new-step-one.hbs
@@ -0,0 +1,65 @@
+
+
+
Profile Picture
+ {{#if this.isImageUploading}}
+
+
+
Processing image...
+
+ {{else}}
+ {{#if this.imagePreview}}
+
+

+
+
+ {{else}}
+
+ {{/if}}
+ {{/if}}
+
+
+
Image Requirements:
+
+ - Must be a real, clear photograph (no anime, filters, or drawings)
+ - Must contain exactly one face
+ - Face must cover at least 60% of the image
+ - Supported formats: JPG, PNG
+ - Image will be validated before moving to next step
+
+
+
+
+
+
Personal Details
+
Please provide correct details and choose your role carefully, it won't be changed
+ later.
+
+
+
+
+
+
+
+
+
+
+
Applying as
+
+ {{#each this.roleOptions as |role|}}
+
+ {{/each}}
+
+
+
+
\ No newline at end of file
diff --git a/app/components/new-join-steps/new-step-one.js b/app/components/new-join-steps/new-step-one.js
new file mode 100644
index 000000000..86b3d7207
--- /dev/null
+++ b/app/components/new-join-steps/new-step-one.js
@@ -0,0 +1,104 @@
+import { action } from '@ember/object';
+import { inject as service } from '@ember/service';
+import { tracked } from '@glimmer/tracking';
+import { countryList } from '../../constants/country-list';
+import {
+ NEW_STEP_LIMITS,
+ ROLE_OPTIONS,
+ STEP_DATA_STORAGE_KEY,
+} from '../../constants/new-join-form';
+import BaseStepComponent from './base-step';
+
+export default class NewStepOneComponent extends BaseStepComponent {
+ @service login;
+ @service toast;
+
+ roleOptions = ROLE_OPTIONS;
+ countries = countryList;
+
+ @tracked imagePreview = null;
+ @tracked isImageUploading = false;
+ @tracked fileInputElement = null;
+
+ get storageKey() {
+ return STEP_DATA_STORAGE_KEY.stepOne;
+ }
+
+ stepValidation = {
+ country: NEW_STEP_LIMITS.stepOne.country,
+ state: NEW_STEP_LIMITS.stepOne.state,
+ city: NEW_STEP_LIMITS.stepOne.city,
+ role: NEW_STEP_LIMITS.stepOne.role,
+ };
+
+ postLoadInitialize() {
+ if (
+ !this.data.fullName &&
+ this.login.userData?.first_name &&
+ this.login.userData?.last_name
+ ) {
+ this.updateFieldValue(
+ 'fullName',
+ `${this.login.userData.first_name} ${this.login.userData.last_name}`,
+ );
+ }
+ if (this.data.profileImageBase64) {
+ this.imagePreview = this.data.profileImageBase64;
+ }
+ }
+
+ @action selectRole(role) {
+ this.inputHandler({ target: { name: 'role', value: role } });
+ }
+
+ @action
+ setFileInputElement(element) {
+ this.fileInputElement = element;
+ }
+
+ @action
+ clearFileInputElement() {
+ this.fileInputElement = null;
+ }
+
+ @action
+ triggerFileInput() {
+ this.fileInputElement?.click();
+ }
+
+ @action
+ handleImageSelect(event) {
+ const file = event.target.files?.[0];
+ if (!file || !file.type.startsWith('image/')) {
+ this.toast.error(
+ 'Invalid file type. Please upload an image file.',
+ 'Error!',
+ );
+ return;
+ }
+ const maxSize = 2 * 1024 * 1024;
+ if (file.size > maxSize) {
+ this.toast.error('Image size must be less than 2MB', 'Error!');
+ return;
+ }
+
+ this.isImageUploading = true;
+
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ const base64String = e.target.result;
+ this.imagePreview = base64String;
+ this.updateFieldValue?.('profileImageBase64', base64String);
+ this.isImageUploading = false;
+ };
+ reader.onerror = () => {
+ this.toast.error(
+ 'Failed to read the selected file. Please try again.',
+ 'Error!',
+ );
+ this.isImageUploading = false;
+ };
+
+ reader.readAsDataURL(file);
+ }
+}
diff --git a/app/components/new-join-steps/new-step-three.hbs b/app/components/new-join-steps/new-step-three.hbs
new file mode 100644
index 000000000..7614c45f2
--- /dev/null
+++ b/app/components/new-join-steps/new-step-three.hbs
@@ -0,0 +1,38 @@
+
\ No newline at end of file
diff --git a/app/components/new-join-steps/new-step-three.js b/app/components/new-join-steps/new-step-three.js
new file mode 100644
index 000000000..0c3fd6b25
--- /dev/null
+++ b/app/components/new-join-steps/new-step-three.js
@@ -0,0 +1,13 @@
+import BaseStepComponent from './base-step';
+import {
+ NEW_STEP_LIMITS,
+ STEP_DATA_STORAGE_KEY,
+} from '../../constants/new-join-form';
+
+export default class NewStepThreeComponent extends BaseStepComponent {
+ storageKey = STEP_DATA_STORAGE_KEY.stepThree;
+ stepValidation = {
+ hobbies: NEW_STEP_LIMITS.stepThree.hobbies,
+ 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
new file mode 100644
index 000000000..c96c5175e
--- /dev/null
+++ b/app/components/new-join-steps/new-step-two.hbs
@@ -0,0 +1,37 @@
+
\ No newline at end of file
diff --git a/app/components/new-join-steps/new-step-two.js b/app/components/new-join-steps/new-step-two.js
new file mode 100644
index 000000000..edb382940
--- /dev/null
+++ b/app/components/new-join-steps/new-step-two.js
@@ -0,0 +1,14 @@
+import BaseStepComponent from './base-step';
+import {
+ NEW_STEP_LIMITS,
+ STEP_DATA_STORAGE_KEY,
+} from '../../constants/new-join-form';
+
+export default class NewStepTwoComponent extends BaseStepComponent {
+ storageKey = STEP_DATA_STORAGE_KEY.stepTwo;
+ stepValidation = {
+ skills: NEW_STEP_LIMITS.stepTwo.skills,
+ company: NEW_STEP_LIMITS.stepTwo.company,
+ introduction: NEW_STEP_LIMITS.stepTwo.introduction,
+ };
+}
diff --git a/app/components/new-join-steps/stepper-header.hbs b/app/components/new-join-steps/stepper-header.hbs
new file mode 100644
index 000000000..7fcd94380
--- /dev/null
+++ b/app/components/new-join-steps/stepper-header.hbs
@@ -0,0 +1,14 @@
+
\ No newline at end of file
diff --git a/app/components/new-join-steps/stepper-header.js b/app/components/new-join-steps/stepper-header.js
new file mode 100644
index 000000000..77ce058e9
--- /dev/null
+++ b/app/components/new-join-steps/stepper-header.js
@@ -0,0 +1,17 @@
+import Component from '@glimmer/component';
+import { htmlSafe } from '@ember/template';
+import { cached } from '@glimmer/tracking';
+
+export default class StepperHeaderComponent extends Component {
+ @cached
+ get progressPercentage() {
+ const totalSteps = Number(this.args.totalSteps) || 1;
+ const currentStep = Number(this.args.currentStep) || 0;
+ return Math.min(100, Math.round((currentStep / totalSteps) * 100));
+ }
+
+ @cached
+ get progressStyle() {
+ return htmlSafe(`width: ${this.progressPercentage}%`);
+ }
+}
diff --git a/app/components/new-stepper.hbs b/app/components/new-stepper.hbs
index 598cf5475..013ca6e8e 100644
--- a/app/components/new-stepper.hbs
+++ b/app/components/new-stepper.hbs
@@ -1,18 +1,51 @@
-
+
+ {{#if (not-eq this.currentStep this.MIN_STEP)}}
+
+ {{#if this.showPreviousButton}}
+
+ {{/if}}
+
+
+
+ {{/if}}
\ No newline at end of file
diff --git a/app/components/new-stepper.js b/app/components/new-stepper.js
index 1cb753e2e..919c68033 100644
--- a/app/components/new-stepper.js
+++ b/app/components/new-stepper.js
@@ -2,6 +2,8 @@ import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
+import { NEW_FORM_STEPS } from '../constants/new-join-form';
+import { getLocalStorageItem, setLocalStorageItem } from '../utils/storage';
export default class NewStepperComponent extends Component {
MIN_STEP = 0;
@@ -12,8 +14,25 @@ export default class NewStepperComponent extends Component {
@service onboarding;
@service joinApplicationTerms;
- @tracked currentStep =
- Number(localStorage.getItem('currentStep') ?? this.args.step) || 0;
+ @tracked preValid = false;
+ @tracked isValid = getLocalStorageItem('isValid') === 'true';
+
+ @tracked currentStep = 0;
+
+ constructor() {
+ super(...arguments);
+
+ const storedStep = getLocalStorageItem('currentStep');
+ const stepFromArgs = this.args.step;
+ this.currentStep = storedStep
+ ? Number(storedStep)
+ : stepFromArgs != null
+ ? Number(stepFromArgs)
+ : 0;
+ }
+
+ setIsValid = (newVal) => (this.isValid = newVal);
+ setIsPreValid = (newVal) => (this.preValid = newVal);
updateQueryParam(step) {
const existingQueryParams = this.router.currentRoute?.queryParams;
@@ -26,10 +45,27 @@ export default class NewStepperComponent extends Component {
});
}
+ get showPreviousButton() {
+ return this.currentStep > this.MIN_STEP + 1;
+ }
+
+ get currentHeading() {
+ return NEW_FORM_STEPS.headings[this.currentStep - 1] ?? '';
+ }
+
+ get currentSubheading() {
+ return NEW_FORM_STEPS.subheadings[this.currentStep - 1] ?? '';
+ }
+
+ get isNextButtonDisabled() {
+ return !(this.preValid || this.isValid);
+ }
+
@action incrementStep() {
if (this.currentStep < this.MAX_STEP) {
const nextStep = this.currentStep + 1;
- localStorage.setItem('currentStep', String(nextStep));
+ setLocalStorageItem('currentStep', String(nextStep));
+ this.currentStep = nextStep;
this.updateQueryParam(nextStep);
}
}
@@ -37,7 +73,8 @@ export default class NewStepperComponent extends Component {
@action decrementStep() {
if (this.currentStep > this.MIN_STEP) {
const previousStep = this.currentStep - 1;
- localStorage.setItem('currentStep', String(previousStep));
+ setLocalStorageItem('currentStep', String(previousStep));
+ this.currentStep = previousStep;
this.updateQueryParam(previousStep);
}
}
diff --git a/app/components/reusables/button.hbs b/app/components/reusables/button.hbs
index 60c9caf1b..da4b0155e 100644
--- a/app/components/reusables/button.hbs
+++ b/app/components/reusables/button.hbs
@@ -2,7 +2,8 @@
data-test-button={{@test}}
class="btn btn-{{@variant}}
btn-{{@classGenerateUsername}}
- {{if @disabled 'btn-disabled' ''}}"
+ {{if @disabled 'btn-disabled' ''}}
+ {{@class}}"
type={{@type}}
disabled={{@disabled}}
title={{@title}}
diff --git a/app/components/reusables/input-box.hbs b/app/components/reusables/input-box.hbs
index 4d842bb40..30491d75d 100644
--- a/app/components/reusables/input-box.hbs
+++ b/app/components/reusables/input-box.hbs
@@ -13,6 +13,7 @@
id={{@name}}
placeholder={{@placeHolder}}
required={{@required}}
+ disabled={{@disabled}}
value={{@value}}
{{on "input" @onInput}}
/>
diff --git a/app/constants/new-join-form.js b/app/constants/new-join-form.js
new file mode 100644
index 000000000..91ce46ba8
--- /dev/null
+++ b/app/constants/new-join-form.js
@@ -0,0 +1,45 @@
+export const NEW_FORM_STEPS = {
+ headings: [
+ 'Upload Professional Headshot and Complete Personal Details',
+ 'Additional Personal Information',
+ 'Your hobbies, interests, fun fact',
+ ],
+ subheadings: [
+ 'Please provide accurate information for verification purposes.',
+ 'Introduce and help us get to know you better',
+ 'Show us your funny and interesting side',
+ ],
+};
+
+export const ROLE_OPTIONS = [
+ 'Developer',
+ 'Designer',
+ 'Product Manager',
+ 'Project Manager',
+ 'QA',
+ 'Social Media',
+];
+
+export const NEW_STEP_LIMITS = {
+ stepOne: {
+ country: { min: 1, type: 'dropdown' },
+ state: { min: 1 },
+ city: { min: 1 },
+ role: { min: 1, type: 'select' },
+ },
+ stepTwo: {
+ skills: { min: 5, max: 20 },
+ company: { min: 1 },
+ introduction: { min: 100, max: 500 },
+ },
+ stepThree: {
+ hobbies: { min: 100, max: 500 },
+ funFact: { min: 100, max: 500 },
+ },
+};
+
+export const STEP_DATA_STORAGE_KEY = {
+ stepOne: 'newStepOneData',
+ stepTwo: 'newStepTwoData',
+ stepThree: 'newStepThreeData',
+};
diff --git a/app/styles/new-stepper.module.css b/app/styles/new-stepper.module.css
index bbef5d22e..b9da05420 100644
--- a/app/styles/new-stepper.module.css
+++ b/app/styles/new-stepper.module.css
@@ -8,12 +8,19 @@
}
.new-stepper__buttons {
- width: 60vw;
- display: flex;
- justify-content: space-between;
+ display: grid;
+ grid-template-columns: 1fr auto 1fr;
align-items: center;
- gap: 1rem;
- margin: 0;
+ width: 60vw;
+}
+
+.prev-button {
+ grid-column: 1;
+}
+
+.next-button {
+ grid-column: 3;
+ justify-self: end;
}
.welcome-screen {
@@ -398,8 +405,7 @@
border-top: 1px solid var(--color-lightgrey);
}
-/* MEDIA QUERIES */
-@media screen and (width <= 1280px) {
+@media screen and (width <=1280px) {
.new-stepper__form,
.form-header,
.new-stepper__buttons {
@@ -407,7 +413,7 @@
}
}
-@media screen and (width <= 1024px) {
+@media screen and (width <=1024px) {
.new-stepper__form,
.form-header,
.new-stepper__buttons {
@@ -415,7 +421,7 @@
}
}
-@media screen and (width <= 768px) {
+@media screen and (width <=768px) {
.two-column-layout {
grid-template-columns: 1fr;
}
@@ -472,7 +478,7 @@
}
}
-@media screen and (width <= 480px) {
+@media screen and (width <=480px) {
.form-header,
.new-stepper__form,
.new-stepper__buttons {
diff --git a/app/utils/storage.js b/app/utils/storage.js
new file mode 100644
index 000000000..31a65c78a
--- /dev/null
+++ b/app/utils/storage.js
@@ -0,0 +1,18 @@
+export function getLocalStorageItem(key, defaultValue = null) {
+ try {
+ return localStorage.getItem(key) ?? defaultValue;
+ } catch (error) {
+ console.warn(`Failed to get localStorage item "${key}":`, error);
+ return defaultValue;
+ }
+}
+
+export function setLocalStorageItem(key, value) {
+ try {
+ localStorage.setItem(key, value);
+ return true;
+ } catch (error) {
+ console.warn(`Failed to set localStorage item "${key}":`, error);
+ return false;
+ }
+}
diff --git a/app/utils/validator.js b/app/utils/validator.js
index ec26bdea5..c9bdcd9cc 100644
--- a/app/utils/validator.js
+++ b/app/utils/validator.js
@@ -13,3 +13,24 @@ export const validator = (value, words) => {
remainingWords: remainingWords,
};
};
+
+export const validateWordCount = (text, wordLimits) => {
+ const trimmedText = text?.trim();
+ const wordCount = trimmedText?.split(/\s+/).filter(Boolean).length ?? 0;
+
+ const { min, max } = wordLimits;
+ if (!trimmedText) {
+ return {
+ isValid: min === 0,
+ wordCount: 0,
+ remainingToMin: min > 0 ? min : 0,
+ };
+ }
+
+ if (wordCount < min) {
+ return { isValid: false, wordCount, remainingToMin: min - wordCount };
+ } else if (max && wordCount > max) {
+ return { isValid: false, wordCount, overByMax: wordCount - max };
+ }
+ return { isValid: true, wordCount };
+};
diff --git a/tests/integration/components/new-join-steps/new-step-three-test.js b/tests/integration/components/new-join-steps/new-step-three-test.js
new file mode 100644
index 000000000..9ea86417d
--- /dev/null
+++ b/tests/integration/components/new-join-steps/new-step-three-test.js
@@ -0,0 +1,177 @@
+import { fillIn, render, waitUntil } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+import { module, test } from 'qunit';
+import {
+ NEW_FORM_STEPS,
+ NEW_STEP_LIMITS,
+} from 'website-www/constants/new-join-form';
+import { setupRenderingTest } from 'website-www/tests/helpers';
+
+module(
+ 'Integration | Component | new-join-steps/new-step-three',
+ function (hooks) {
+ setupRenderingTest(hooks);
+
+ function generateWords(count) {
+ return Array(count).fill('word').join(' ');
+ }
+
+ hooks.beforeEach(function () {
+ localStorage.clear();
+ });
+
+ test('it renders step three correctly', async function (assert) {
+ this.set('setIsPreValid', () => {});
+ this.set('setIsValid', () => {});
+ this.set('heading', NEW_FORM_STEPS.headings[2]);
+ this.set('subHeading', NEW_FORM_STEPS.subheadings[2]);
+
+ await render(hbs`
+
+ `);
+
+ assert.dom('[data-test-heading]').hasText(NEW_FORM_STEPS.headings[2]);
+ assert
+ .dom('[data-test-sub-heading]')
+ .hasText(NEW_FORM_STEPS.subheadings[2]);
+ assert.dom('[data-test-textarea-field][name="hobbies"]').exists();
+ assert.dom('[data-test-textarea-field][name="funFact"]').exists();
+ assert.dom('[data-test-word-count="hobbies"]').exists();
+ assert.dom('[data-test-word-count="funFact"]').exists();
+ });
+
+ test('it validates step three fields and updates word counts', async function (assert) {
+ this.set('setIsPreValid', () => {});
+ this.set('setIsValid', () => {});
+ this.set('heading', NEW_FORM_STEPS.headings[2]);
+ this.set('subHeading', NEW_FORM_STEPS.subheadings[2]);
+
+ await render(hbs`
+
+ `);
+
+ await fillIn(
+ '[data-test-textarea-field][name="hobbies"]',
+ generateWords(120),
+ );
+
+ await waitUntil(() =>
+ document
+ .querySelector('[data-test-word-count="hobbies"]')
+ ?.textContent.includes('120/'),
+ );
+ assert
+ .dom('[data-test-word-count="hobbies"]')
+ .hasText(`120/${NEW_STEP_LIMITS.stepThree.hobbies.max} words`);
+
+ await fillIn(
+ '[data-test-textarea-field][name="funFact"]',
+ generateWords(130),
+ );
+
+ await waitUntil(() =>
+ document
+ .querySelector('[data-test-word-count="funFact"]')
+ ?.textContent.includes('130/'),
+ );
+ assert
+ .dom('[data-test-word-count="funFact"]')
+ .hasText(`130/${NEW_STEP_LIMITS.stepThree.funFact.max} words`);
+
+ await fillIn(
+ '[data-test-textarea-field][name="hobbies"]',
+ generateWords(50),
+ );
+
+ await waitUntil(() =>
+ document.querySelector('[data-test-error="hobbies"]'),
+ );
+ assert.dom('[data-test-error="hobbies"]').exists();
+ });
+
+ test('it saves step three data to localStorage and calls callbacks', async function (assert) {
+ let setIsPreValidCalled = false;
+ let isValidValue = null;
+
+ this.set('setIsPreValid', () => {
+ setIsPreValidCalled = true;
+ });
+ this.set('setIsValid', (val) => {
+ isValidValue = val;
+ });
+ this.set('heading', NEW_FORM_STEPS.headings[2]);
+ this.set('subHeading', NEW_FORM_STEPS.subheadings[2]);
+
+ await render(hbs`
+
+ `);
+
+ await fillIn(
+ '[data-test-textarea-field][name="hobbies"]',
+ generateWords(120),
+ );
+ await fillIn(
+ '[data-test-textarea-field][name="funFact"]',
+ generateWords(130),
+ );
+
+ await waitUntil(
+ () =>
+ JSON.parse(localStorage.getItem('newStepThreeData') || '{}').hobbies,
+ );
+
+ assert.ok(setIsPreValidCalled);
+ assert.true(isValidValue);
+ assert
+ .dom('[data-test-textarea-field][name="hobbies"]')
+ .hasValue(generateWords(120));
+ assert
+ .dom('[data-test-textarea-field][name="funFact"]')
+ .hasValue(generateWords(130));
+ });
+
+ test('it loads step three data from localStorage', async function (assert) {
+ const testData = {
+ hobbies: generateWords(200),
+ funFact: generateWords(300),
+ };
+ localStorage.setItem('newStepThreeData', JSON.stringify(testData));
+
+ this.set('setIsPreValid', () => {});
+ this.set('setIsValid', () => {});
+ this.set('heading', NEW_FORM_STEPS.headings[2]);
+ this.set('subHeading', NEW_FORM_STEPS.subheadings[2]);
+
+ await render(hbs`
+
+ `);
+
+ assert
+ .dom('[data-test-textarea-field][name="hobbies"]')
+ .hasValue(testData.hobbies);
+ assert
+ .dom('[data-test-textarea-field][name="funFact"]')
+ .hasValue(testData.funFact);
+ });
+ },
+);
diff --git a/tests/integration/components/new-join-steps/new-step-two-test.js b/tests/integration/components/new-join-steps/new-step-two-test.js
new file mode 100644
index 000000000..d730e6dfd
--- /dev/null
+++ b/tests/integration/components/new-join-steps/new-step-two-test.js
@@ -0,0 +1,154 @@
+import { fillIn, render, waitUntil } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+import { module, test } from 'qunit';
+import {
+ NEW_FORM_STEPS,
+ NEW_STEP_LIMITS,
+} from 'website-www/constants/new-join-form';
+import { setupRenderingTest } from 'website-www/tests/helpers';
+
+module(
+ 'Integration | Component | new-join-steps/new-step-two',
+ function (hooks) {
+ setupRenderingTest(hooks);
+
+ function generateWords(count) {
+ return Array(count).fill('word').join(' ');
+ }
+
+ hooks.beforeEach(function () {
+ localStorage.clear();
+ });
+
+ test('it renders step two correctly', async function (assert) {
+ this.set('setIsPreValid', () => {});
+ this.set('setIsValid', () => {});
+ this.set('heading', NEW_FORM_STEPS.headings[1]);
+ this.set('subHeading', NEW_FORM_STEPS.subheadings[1]);
+
+ await render(hbs`
+
+ `);
+
+ assert.dom('[data-test-heading]').hasText(NEW_FORM_STEPS.headings[1]);
+ assert
+ .dom('[data-test-sub-heading]')
+ .hasText(NEW_FORM_STEPS.subheadings[1]);
+ assert.dom('[data-test-input-field][name="skills"]').exists();
+ assert.dom('[data-test-input-field][name="company"]').exists();
+ assert.dom('[data-test-textarea-field][name="introduction"]').exists();
+ assert.dom('[data-test-word-count="introduction"]').exists();
+ });
+
+ test('it validates step two fields and updates word count', async function (assert) {
+ this.set('setIsPreValid', () => {});
+ this.set('setIsValid', () => {});
+ this.set('heading', NEW_FORM_STEPS.headings[1]);
+ this.set('subHeading', NEW_FORM_STEPS.subheadings[1]);
+
+ await render(hbs`
+
+ `);
+
+ await fillIn(
+ '[data-test-textarea-field][name="introduction"]',
+ generateWords(120),
+ );
+
+ await waitUntil(() =>
+ document
+ .querySelector('[data-test-word-count="introduction"]')
+ ?.textContent.includes('120/'),
+ );
+
+ assert
+ .dom('[data-test-word-count="introduction"]')
+ .hasText(`120/${NEW_STEP_LIMITS.stepTwo.introduction.max} words`);
+
+ await fillIn('[data-test-input-field][name="skills"]', generateWords(3));
+
+ await waitUntil(() =>
+ document.querySelector('[data-test-error="skills"]'),
+ );
+
+ assert.dom('[data-test-error="skills"]').exists();
+ });
+
+ test('it correctly updates step two data to localStorage', async function (assert) {
+ let setIsPreValidCalled = false;
+ let isValidValue = null;
+
+ this.set('setIsPreValid', () => {
+ setIsPreValidCalled = true;
+ });
+ this.set('setIsValid', (val) => {
+ isValidValue = val;
+ });
+ this.set('heading', NEW_FORM_STEPS.headings[1]);
+ this.set('subHeading', NEW_FORM_STEPS.subheadings[1]);
+
+ await render(hbs`
+
+ `);
+
+ await fillIn('[data-test-input-field][name="skills"]', generateWords(10));
+ await fillIn('[data-test-input-field][name="company"]', 'Real Dev Squad');
+ await fillIn(
+ '[data-test-textarea-field][name="introduction"]',
+ generateWords(120),
+ );
+
+ await waitUntil(
+ () => JSON.parse(localStorage.getItem('newStepTwoData') || '{}').skills,
+ );
+
+ assert.ok(setIsPreValidCalled);
+ assert.true(isValidValue);
+ assert.ok(
+ JSON.parse(localStorage.getItem('newStepTwoData') || '{}').skills,
+ );
+ });
+
+ test('it loads step two data from localStorage', async function (assert) {
+ const testData = {
+ skills: generateWords(10),
+ company: 'Real Dev Squad',
+ introduction: generateWords(150),
+ };
+ localStorage.setItem('newStepTwoData', JSON.stringify(testData));
+
+ this.set('setIsPreValid', () => {});
+ this.set('setIsValid', () => {});
+ this.set('heading', NEW_FORM_STEPS.headings[1]);
+ this.set('subHeading', NEW_FORM_STEPS.subheadings[1]);
+
+ await render(hbs`
+
+ `);
+
+ assert
+ .dom('[data-test-input-field][name="skills"]')
+ .hasValue(testData.skills);
+ });
+ },
+);