blindspot / frontend /src /pages /feedbackModel.test.ts
yashwrites's picture
Move to scanblindspot.com: root base path, in-app feedback page (part 2)
0723e29 verified
Raw History Blame Contribute Delete
4.11 kB
import { describe, expect, it } from 'vitest';
import { DEFAULT_URL } from '../analytics';
import {
EMPTY_FORM, FAILED_MESSAGE, failureMessage, feedbackEndpoint, LIMIT_MESSAGE, MAX_CONTACT, MAX_MISSING, missingAnswer, OFFLINE_MESSAGE,
RATINGS_MESSAGE, toPayload, VERDICT_MESSAGE, type FeedbackForm,
} from './feedbackModel';
const full: FeedbackForm = {
role: 'resident', years: '5-15', ratings: { ease: 4, teaching: 5, accuracy: 3, recommend: 4 }, verdict: 'yes',
missing: ' More CT cases. ', contact: ' a@example.org ',
};
describe('missingAnswer', () => {
it('needs the four ratings, then the verdict; role, years and the texts are optional', () => {
expect(missingAnswer(EMPTY_FORM)).toEqual({ message: RATINGS_MESSAGE, field: 'ease' });
expect(missingAnswer({ ...EMPTY_FORM, ratings: { ease: 3, teaching: 3, accuracy: 3 } })).toEqual({ message: RATINGS_MESSAGE, field: 'recommend' });
expect(missingAnswer({ ...EMPTY_FORM, ratings: { ease: 3, teaching: 3, accuracy: 3, recommend: 3 } })).toEqual({ message: VERDICT_MESSAGE, field: 'verdict' });
expect(missingAnswer({ ...EMPTY_FORM, ratings: { ease: 3, teaching: 3, accuracy: 3, recommend: 3 }, verdict: 'maybe' })).toBeNull();
});
});
describe('toPayload', () => {
it('maps the form onto the worker contract and trims the free text', () => {
expect(toPayload(full)).toEqual({
role: 'resident', years: '5-15', ease: 4, teaching: 5, accuracy: 3, recommend: 4, real_product: 'yes',
missing: 'More CT cases.', contact: 'a@example.org',
});
});
it('sends "other" for no role and "" for no years', () => {
const p = toPayload({ ...full, role: null, years: null, missing: '', contact: '' });
expect(p).toMatchObject({ role: 'other', years: '', missing: '', contact: '' });
});
it('is null while a required answer is missing', () => {
expect(toPayload({ ...full, verdict: null })).toBeNull();
expect(toPayload({ ...full, ratings: { ...full.ratings, accuracy: undefined } })).toBeNull();
});
it('never exceeds the worker limits (600 / 200 characters, well under 4 KB in all)', () => {
const p = toPayload({ ...full, missing: 'x'.repeat(2000), contact: 'y'.repeat(500) })!;
expect(p.missing).toHaveLength(MAX_MISSING);
expect(p.contact).toHaveLength(MAX_CONTACT);
expect(new TextEncoder().encode(JSON.stringify(p)).length).toBeLessThan(4096);
});
it('only ever sends values the worker accepts', () => {
const p = toPayload(full)!;
expect(['radiologist', 'resident', 'medical_student', 'other_clinician', 'other']).toContain(p.role);
expect(['<5', '5-15', '>15', '']).toContain(p.years);
expect(['yes', 'maybe', 'no']).toContain(p.real_product);
for (const k of ['ease', 'teaching', 'accuracy', 'recommend'] as const) {
expect(Number.isInteger(p[k])).toBe(true);
expect(p[k]).toBeGreaterThanOrEqual(1);
expect(p[k]).toBeLessThanOrEqual(5);
}
});
});
describe('feedbackEndpoint', () => {
it('uses the tracker base, without a trailing slash, and the default worker when the variable is empty or unset', () => {
expect(feedbackEndpoint('https://api.example.org/')).toBe('https://api.example.org/feedback/submit');
expect(feedbackEndpoint('https://api.example.org')).toBe('https://api.example.org/feedback/submit');
expect(feedbackEndpoint('')).toBe(`${DEFAULT_URL}/feedback/submit`);
expect(feedbackEndpoint(' ')).toBe(`${DEFAULT_URL}/feedback/submit`);
expect(feedbackEndpoint(undefined)).toBe(`${DEFAULT_URL}/feedback/submit`);
});
});
describe('failureMessage', () => {
it('names the hourly limit on 429, repeats the worker on other errors, and says "no connection" when nothing answered', () => {
expect(failureMessage(429, { error: 'Thanks — that is plenty for one hour.' })).toBe(LIMIT_MESSAGE);
expect(failureMessage(400, { error: 'Choose a role.' })).toBe('Choose a role.');
expect(failureMessage(503, {})).toBe(FAILED_MESSAGE);
expect(failureMessage(500, 'not json')).toBe(FAILED_MESSAGE);
expect(failureMessage(null, null)).toBe(OFFLINE_MESSAGE);
});
});