feat(assessment & chat): polish PCQ-24 mobile result view, typography hierarchy, and anchored chat composer

This commit is contained in:
ar.badiei
2026-10-18 01:08:09 +03:30
parent d2a52a2958
commit 4d734f9047
39 changed files with 3317 additions and 313 deletions
+187
View File
@@ -0,0 +1,187 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import {
DIMENSION_CONFIGS,
DIMENSION_ORDER,
getScoreTier,
getFeedbackForScore,
getDescriptiveTitleForScore,
} from '../src/config/resultImages';
describe('PCQ-24 Result Logic & 12 Feedback Texts', () => {
it('has exactly 4 dimensions in correct order', () => {
assert.deepEqual(DIMENSION_ORDER, ['efficacy', 'hope', 'resilience', 'optimism']);
});
it('correctly maps boundary scores to tiers', () => {
// 6 to 15 is Category 1 (low)
assert.equal(getScoreTier(6), 'low');
assert.equal(getScoreTier(10), 'low');
assert.equal(getScoreTier(15), 'low');
// 16 to 25 is Category 2 (mid)
assert.equal(getScoreTier(16), 'mid');
assert.equal(getScoreTier(20), 'mid');
assert.equal(getScoreTier(25), 'mid');
// 26 to 36 is Category 3 (high)
assert.equal(getScoreTier(26), 'high');
assert.equal(getScoreTier(30), 'high');
assert.equal(getScoreTier(36), 'high');
});
it('provides distinct, teen-friendly descriptive titles for all 3 tiers across all 4 dimensions', () => {
const titles = new Set<string>();
for (const dim of DIMENSION_ORDER) {
const lowTitle = getDescriptiveTitleForScore(dim, 12);
const midTitle = getDescriptiveTitleForScore(dim, 20);
const highTitle = getDescriptiveTitleForScore(dim, 30);
// Verify no forbidden judgmental labels
for (const t of [lowTitle, midTitle, highTitle]) {
assert.ok(t.length > 5, `Title "${t}" too short`);
assert.ok(!t.includes('ضعیف'), `Title "${t}" contains forbidden word ضعیف`);
assert.ok(!t.includes('متوسط'), `Title "${t}" contains forbidden word متوسط`);
assert.ok(!t.includes('قوی'), `Title "${t}" contains forbidden word قوی`);
titles.add(t);
}
assert.notEqual(lowTitle, midTitle);
assert.notEqual(midTitle, highTitle);
}
assert.equal(titles.size, 12, 'Must have exactly 12 unique descriptive titles');
});
it('has non-empty distinct feedback for all 12 combinations', () => {
const allTexts = new Set<string>();
for (const dim of DIMENSION_ORDER) {
const config = DIMENSION_CONFIGS[dim];
assert.ok(config, `Missing config for ${dim}`);
const low = getFeedbackForScore(dim, 15);
const mid = getFeedbackForScore(dim, 20);
const high = getFeedbackForScore(dim, 26);
assert.ok(low.length > 20, `Low text for ${dim} is too short`);
assert.ok(mid.length > 20, `Mid text for ${dim} is too short`);
assert.ok(high.length > 20, `High text for ${dim} is too short`);
assert.notEqual(low, mid, `Low and Mid for ${dim} should differ`);
assert.notEqual(mid, high, `Mid and High for ${dim} should differ`);
allTexts.add(low);
allTexts.add(mid);
allTexts.add(high);
}
assert.equal(allTexts.size, 12, 'Must have exactly 12 unique feedback texts');
});
it('contains correct Persian half-spaces (ZWNJ) for key phrases like «جنبه‌های» and «نگاه فعلی‌ات»', () => {
const optimismMid = getFeedbackForScore('optimism', 20);
assert.ok(
optimismMid.includes('جنبه‌های'),
'Must contain «جنبه‌های» with proper ZWNJ (U+200C)'
);
assert.ok(
optimismMid.includes('نگاه فعلی‌ات'),
'Must contain «نگاه فعلی‌ات» with proper ZWNJ (U+200C)'
);
const resilienceMid = getFeedbackForScore('resilience', 20);
assert.ok(
resilienceMid.includes('جنبه‌های'),
'Must contain «جنبه‌های» in resilience mid feedback'
);
assert.ok(
resilienceMid.includes('تاب‌آوری'),
'Must contain «تاب‌آوری» with proper ZWNJ (U+200C)'
);
const efficacyLow = getFeedbackForScore('efficacy', 10);
assert.ok(
efficacyLow.includes('توانایی‌های'),
'Must contain «توانایی‌های» with proper ZWNJ (U+200C)'
);
const hopeLow = getFeedbackForScore('hope', 10);
assert.ok(
hopeLow.includes('هدف‌ها'),
'Must contain «هدف‌ها» with proper ZWNJ (U+200C)'
);
});
it('matches the exact specified Persian text for Self-Efficacy', () => {
assert.equal(
getFeedbackForScore('efficacy', 15),
'اعتماد به توانایی‌های خودت هنوز جای رشد داره. شاید بعضی وقت‌ها انجام کارهای تازه برات سخت به نظر برسه؛ این فقط بخشی از تصویر فعلی توئه و همه توانایی‌هایت رو مشخص نمی‌کنه.'
);
assert.equal(
getFeedbackForScore('efficacy', 20),
'به نظر می‌رسه در بعضی موقعیت‌ها به خودت اعتماد داری، اما گاهی هم درباره توانایی‌هایت تردید می‌کنی. این یعنی نگاهت به توانایی‌های خودت در موقعیت‌های مختلف می‌تونه متفاوت باشه.'
);
assert.equal(
getFeedbackForScore('efficacy', 28),
'یکی از ویژگی‌های برجسته تو، اعتماد به توانایی‌هایت برای انجام کارهاست. به نظر می‌رسه در پاسخ‌هایت، باور به توانایی خودت جایگاه پررنگی داره؛ هرچند این حس ممکنه در موقعیت‌های مختلف تغییر کنه.'
);
});
it('matches the exact specified Persian text for Hope', () => {
assert.equal(
getFeedbackForScore('hope', 10),
'پیدا کردن مسیر رسیدن به هدف‌ها هنوز می‌تونه بخش مهمی از تجربه تو باشه. این نتیجه فقط نگاه فعلی تو به هدف‌ها و راه‌های رسیدن به اون‌ها رو نشون می‌ده، نه چیزی که در آینده برات ممکنه یا غیرممکنه.'
);
assert.equal(
getFeedbackForScore('hope', 18),
'به نظر می‌رسه برای بعضی هدف‌هایت مسیر روشن‌تری داری، اما درباره بعضی هدف‌های دیگه هنوز در حال پیدا کردن راهت هستی. این کاملاً تصویری از نگاه فعلی تو به هدف‌ها و مسیرهای رسیدن به اون‌هاست.'
);
assert.equal(
getFeedbackForScore('hope', 32),
'امیدواری یکی از نقاط قوت برجسته تو در این آزمونه. به نظر می‌رسه پیدا کردن هدف‌ها و فکر کردن به راه‌های رسیدن به اون‌ها در پاسخ‌هایت جایگاه پررنگی داره.'
);
});
it('matches the exact specified Persian text for Resilience', () => {
assert.equal(
getFeedbackForScore('resilience', 12),
'کنار اومدن با سختی‌ها و برگشتن به مسیر، از بخش‌هایی است که هنوز می‌تونه در تو رشد کنه. این نمره به‌تنهایی نمی‌گه در شرایط سخت چطور رفتار می‌کنی؛ فقط تصویری از پاسخ‌های فعلی توئه.'
);
assert.equal(
getFeedbackForScore('resilience', 22),
'به نظر می‌رسه در بعضی شرایط می‌تونی با سختی‌ها کنار بیای، اما تجربه‌ات در موقعیت‌های مختلف یکسان نیست. این نتیجه نشون می‌ده تاب‌آوری تو در پاسخ‌های این آزمون، ترکیبی از جنبه‌های شکل‌گرفته و در حال رشده.'
);
assert.equal(
getFeedbackForScore('resilience', 30),
'تاب‌آوری یکی از نقاط قوت برجسته تو در این آزمونه. پاسخ‌هایت نشون می‌دن کنار اومدن با دشواری‌ها و ادامه دادن مسیر، در تصویر فعلی تو جایگاه پررنگی داره.'
);
});
it('matches the exact specified Persian text for Optimism', () => {
assert.equal(
getFeedbackForScore('optimism', 14),
'نگاهت به اتفاق‌ها و آینده هنوز ظرفیت رشد داره. این نمره به معنی منفی بودن نگاهت در همه موقعیت‌ها نیست؛ فقط نشون میده پاسخ‌های فعلی تو درباره خوش‌بینی چه تصویری دارن.'
);
assert.equal(
getFeedbackForScore('optimism', 24),
'گاهی ممکنه جنبه‌های امیدوارکننده اتفاق‌ها رو ببینی و گاهی هم نگاهت به شرایط متفاوت باشه. نتیجه تو تصویری از نحوه نگاه فعلی‌ات به اتفاق‌ها و آینده ارائه می‌ده.'
);
assert.equal(
getFeedbackForScore('optimism', 35),
'خوش‌بینی یکی از نقاط قوت برجسته تو در این آزمونه. پاسخ‌هایت نشون می‌دن نگاه امیدوارانه به اتفاق‌ها و آینده در تجربه فعلی تو جایگاه پررنگی داره.'
);
});
it('configures the 4 local image paths properly without remote dependencies', () => {
for (const dim of DIMENSION_ORDER) {
const config = DIMENSION_CONFIGS[dim];
assert.ok(config.path.startsWith('/images/components/'));
assert.ok(config.fileName.endsWith('.webp'));
assert.ok(!config.path.startsWith('http'));
assert.ok(config.titleFa.length > 0);
assert.ok(config.inviteText.length > 0);
}
});
});