@@ -412,4 +412,134 @@ describe("KycSubmissionForm", () => {
412412 render ( React . createElement ( KycSubmissionForm ) ) ;
413413 expect ( screen . getByRole ( "region" ) ) . toBeInTheDocument ( ) ;
414414 } ) ;
415+
416+ // ── Screen reader support ─────────────────────────────────────────────────
417+
418+ it ( "step listitems have descriptive aria-labels including step name and status" , ( ) => {
419+ const { container } = render ( React . createElement ( KycSubmissionForm ) ) ;
420+ const items = container . querySelectorAll ( '[role="listitem"]' ) ;
421+ expect ( items [ 0 ] ) . toHaveAttribute ( "aria-label" , expect . stringContaining ( "personalInfo" ) ) ;
422+ expect ( items [ 0 ] ) . toHaveAttribute ( "aria-label" , expect . stringContaining ( "current" ) ) ;
423+ expect ( items [ 1 ] ) . toHaveAttribute ( "aria-label" , expect . stringContaining ( "addressInfo" ) ) ;
424+ expect ( items [ 1 ] ) . toHaveAttribute ( "aria-label" , expect . stringContaining ( "upcoming" ) ) ;
425+ } ) ;
426+
427+ it ( "step listitem status updates to completed after advancing past it" , ( ) => {
428+ const { container } = render ( React . createElement ( KycSubmissionForm ) ) ;
429+ navigateToStep ( 1 ) ;
430+ const items = container . querySelectorAll ( '[role="listitem"]' ) ;
431+ expect ( items [ 0 ] ) . toHaveAttribute ( "aria-label" , expect . stringContaining ( "completed" ) ) ;
432+ expect ( items [ 1 ] ) . toHaveAttribute ( "aria-label" , expect . stringContaining ( "current" ) ) ;
433+ } ) ;
434+
435+ it ( "progressbar aria-label includes current step name" , ( ) => {
436+ render ( React . createElement ( KycSubmissionForm ) ) ;
437+ const progressbar = screen . getByRole ( "progressbar" ) ;
438+ expect ( progressbar ) . toHaveAttribute ( "aria-label" , expect . stringContaining ( "personalInfo" ) ) ;
439+ } ) ;
440+
441+ it ( "progressbar aria-label updates to next step name after navigation" , ( ) => {
442+ render ( React . createElement ( KycSubmissionForm ) ) ;
443+ navigateToStep ( 1 ) ;
444+ const progressbar = screen . getByRole ( "progressbar" ) ;
445+ expect ( progressbar ) . toHaveAttribute ( "aria-label" , expect . stringContaining ( "addressInfo" ) ) ;
446+ } ) ;
447+
448+ it ( "back button has descriptive aria-label with destination step name" , ( ) => {
449+ render ( React . createElement ( KycSubmissionForm ) ) ;
450+ navigateToStep ( 1 ) ;
451+ const backBtn = screen . getByText ( "back" ) . closest ( "button" ) ! ;
452+ expect ( backBtn ) . toHaveAttribute ( "aria-label" , expect . stringContaining ( "personalInfo" ) ) ;
453+ } ) ;
454+
455+ it ( "next button has descriptive aria-label with destination step name" , ( ) => {
456+ render ( React . createElement ( KycSubmissionForm ) ) ;
457+ const nextBtn = screen . getByText ( "next" ) . closest ( "button" ) ! ;
458+ expect ( nextBtn ) . toHaveAttribute ( "aria-label" , expect . stringContaining ( "addressInfo" ) ) ;
459+ } ) ;
460+
461+ // ── Additional unit tests ─────────────────────────────────────────────────
462+
463+ it ( "shows validation error message text for required fields" , ( ) => {
464+ render ( React . createElement ( KycSubmissionForm ) ) ;
465+ fireEvent . click ( screen . getByText ( "next" ) ) ;
466+ expect ( screen . getAllByText ( "required" ) . length ) . toBeGreaterThanOrEqual ( 1 ) ;
467+ } ) ;
468+
469+ it ( "sets aria-describedby on invalid firstName input pointing to error element" , ( ) => {
470+ render ( React . createElement ( KycSubmissionForm ) ) ;
471+ fireEvent . click ( screen . getByText ( "next" ) ) ;
472+ const firstNameInput = screen . getByPlaceholderText ( "firstName" ) ;
473+ const describedBy = firstNameInput . getAttribute ( "aria-describedby" ) ;
474+ expect ( describedBy ) . toBeTruthy ( ) ;
475+ // useId() produces IDs with colons — use getElementById, not querySelector
476+ const errorEl = document . getElementById ( describedBy ! ) ;
477+ expect ( errorEl ) . toBeInTheDocument ( ) ;
478+ } ) ;
479+
480+ it ( "clears validation errors after filling required fields and navigating" , ( ) => {
481+ render ( React . createElement ( KycSubmissionForm ) ) ;
482+ fireEvent . click ( screen . getByText ( "next" ) ) ;
483+ expect ( screen . getByPlaceholderText ( "firstName" ) ) . toHaveAttribute ( "aria-invalid" , "true" ) ;
484+
485+ fillPersonalStep ( ) ;
486+ fireEvent . click ( screen . getByText ( "next" ) ) ;
487+ fireEvent . click ( screen . getByText ( "back" ) ) ;
488+ expect ( screen . getByPlaceholderText ( "firstName" ) ) . toHaveAttribute ( "aria-invalid" , "false" ) ;
489+ } ) ;
490+
491+ it ( "accepts file upload on idBack input" , ( ) => {
492+ render ( React . createElement ( KycSubmissionForm ) ) ;
493+ navigateToStep ( 2 ) ;
494+ const idBackInput = screen . getByLabelText ( "idBack" ) as HTMLInputElement ;
495+ const file = new File ( [ "content" ] , "id-back.png" , { type : "image/png" } ) ;
496+ fireEvent . change ( idBackInput , { target : { files : [ file ] } } ) ;
497+ expect ( idBackInput . files ?. [ 0 ] ) . toBe ( file ) ;
498+ } ) ;
499+
500+ it ( "accepts file upload on selfie input" , ( ) => {
501+ render ( React . createElement ( KycSubmissionForm ) ) ;
502+ navigateToStep ( 2 ) ;
503+ const selfieInput = screen . getByLabelText ( "selfie" ) as HTMLInputElement ;
504+ const file = new File ( [ "content" ] , "selfie.jpg" , { type : "image/jpeg" } ) ;
505+ fireEvent . change ( selfieInput , { target : { files : [ file ] } } ) ;
506+ expect ( selfieInput . files ?. [ 0 ] ) . toBe ( file ) ;
507+ } ) ;
508+
509+ it ( "updates idType select on documents step" , ( ) => {
510+ render ( React . createElement ( KycSubmissionForm ) ) ;
511+ navigateToStep ( 2 ) ;
512+ const select = screen . getByLabelText ( "idType" ) as HTMLSelectElement ;
513+ fireEvent . change ( select , { target : { value : "passport" } } ) ;
514+ expect ( select . value ) . toBe ( "passport" ) ;
515+ } ) ;
516+
517+ it ( "updates idNumber field on documents step" , ( ) => {
518+ render ( React . createElement ( KycSubmissionForm ) ) ;
519+ navigateToStep ( 2 ) ;
520+ const idNumberInput = screen . getByPlaceholderText ( "idNumber" ) as HTMLInputElement ;
521+ fireEvent . change ( idNumberInput , { target : { value : "A1234567" } } ) ;
522+ expect ( idNumberInput . value ) . toBe ( "A1234567" ) ;
523+ } ) ;
524+
525+ it ( "review step shows dash for empty optional fields" , ( ) => {
526+ render ( React . createElement ( KycSubmissionForm ) ) ;
527+ fillPersonalStep ( ) ;
528+ fireEvent . click ( screen . getByText ( "next" ) ) ; // → address
529+ fireEvent . click ( screen . getByText ( "next" ) ) ; // → documents
530+ fireEvent . click ( screen . getByText ( "next" ) ) ; // → review
531+ // city was not filled, so it renders the placeholder dash
532+ const dashes = screen . getAllByText ( "—" ) ;
533+ expect ( dashes . length ) . toBeGreaterThan ( 0 ) ;
534+ } ) ;
535+
536+ it ( "shows error alert in review step when submission fails" , async ( ) => {
537+ ( global . fetch as any ) . mockResolvedValue ( { ok : false } ) ;
538+ render ( React . createElement ( KycSubmissionForm ) ) ;
539+ navigateToStep ( 3 ) ;
540+ fireEvent . click ( screen . getByText ( "submit" ) ) ;
541+ await waitFor ( ( ) => {
542+ expect ( screen . getByRole ( "alert" ) ) . toBeInTheDocument ( ) ;
543+ } ) ;
544+ } ) ;
415545} ) ;
0 commit comments