77 */
88
99/**
10- * A refused save raises ONE error toast (objectui#13340).
10+ * A refused save is said ONCE (objectui#13340, objectstack-ai/objectstack#27520 ).
1111 *
1212 * The console's record form (`ModalForm`, `RecordFormPage`'s `ObjectForm`)
1313 * hands this renderer an `onSubmit` that writes through the data source and,
1414 * on a refusal, rethrows it. This renderer is the one that reports it: the
15- * banner above the fields, plus a toast for when the banner has scrolled out
16- * of view. The toast is raised under the form's own outcome id, so a retry
17- * that is refused again REPLACES the previous toast instead of stacking a
18- * second copy beside it.
15+ * banner above the fields, brought into view each time it is raised, and no
16+ * error toast repeating it. A retry that is refused again keeps that one
17+ * banner rather than stacking a second message beside it.
1918 *
2019 * Pinned on the toast calls themselves (every `toast.error` this form makes,
21- * undeduplicated), not on what sonner ends up showing, so a second raiser here
20+ * undeduplicated), not on what sonner ends up showing, so a raiser here
2221 * cannot hide behind sonner's id replacement.
2322 */
2423import { describe , it , expect , beforeEach , afterEach , vi } from 'vitest' ;
@@ -33,12 +32,14 @@ import '../../../renderers';
3332const REFUSAL = 'A record with this value already exists' ;
3433
3534let toastErrorSpy : ReturnType < typeof vi . spyOn > ;
35+ let scrolled : Element [ ] ;
3636
3737beforeEach ( ( ) => {
3838 toastErrorSpy = vi . spyOn ( toast , 'error' ) . mockImplementation ( ( ) => 'id' as any ) ;
39- if ( ! ( Element . prototype as any ) . scrollIntoView ) {
40- ( Element . prototype as any ) . scrollIntoView = ( ) => { } ;
41- }
39+ scrolled = [ ] ;
40+ ( Element . prototype as any ) . scrollIntoView = function ( this : Element ) {
41+ scrolled . push ( this ) ;
42+ } ;
4243} ) ;
4344
4445afterEach ( ( ) => {
@@ -70,30 +71,33 @@ function renderRefusingForm() {
7071
7172const create = ( ) => fireEvent . click ( screen . getByRole ( 'button' , { name : / c r e a t e / i } ) ) ;
7273
73- describe ( 'form renderer — one refusal, one error toast (objectui#13340)' , ( ) => {
74- it ( 'a refused submit raises exactly one error toast, with the server`s sentence ' , async ( ) => {
74+ describe ( 'form renderer — one refusal, one message (objectui#13340, objectstack-ai/objectstack#27520 )' , ( ) => {
75+ it ( 'a refused submit draws one banner, scrolled into view, and raises no error toast ' , async ( ) => {
7576 renderRefusingForm ( ) ;
7677 create ( ) ;
7778
7879 await waitFor ( ( ) => expect ( refuse ) . toHaveBeenCalledTimes ( 1 ) ) ;
79- // The banner is the settle signal: it is set in the same catch as the toast.
80- await screen . findByText ( REFUSAL ) ;
81- expect ( toastErrorSpy ) . toHaveBeenCalledTimes ( 1 ) ;
82- expect ( toastErrorSpy . mock . calls [ 0 ] [ 0 ] ) . toBe ( REFUSAL ) ;
80+ // The banner is the settle signal: it is set in the same catch.
81+ const banner = ( await screen . findByText ( REFUSAL ) ) . closest ( '[role="alert"]' ) ;
82+ expect ( banner ) . not . toBeNull ( ) ;
83+ expect ( screen . getAllByRole ( 'alert' ) ) . toHaveLength ( 1 ) ;
84+ await waitFor ( ( ) => expect ( scrolled ) . toContain ( banner ) ) ;
85+ expect ( toastErrorSpy ) . not . toHaveBeenCalled ( ) ;
8386 } ) ;
8487
85- it ( 'a refused retry raises one more, under the same id, so it replaces the first ' , async ( ) => {
88+ it ( 'a refused retry keeps one banner, brings it into view again, and still raises no toast ' , async ( ) => {
8689 refuse . mockClear ( ) ;
8790 renderRefusingForm ( ) ;
8891 create ( ) ;
89- await waitFor ( ( ) => expect ( toastErrorSpy ) . toHaveBeenCalledTimes ( 1 ) ) ;
92+ await screen . findByText ( REFUSAL ) ;
93+ await waitFor ( ( ) => expect ( scrolled . length ) . toBe ( 1 ) ) ;
9094
9195 create ( ) ;
9296 await waitFor ( ( ) => expect ( refuse ) . toHaveBeenCalledTimes ( 2 ) ) ;
93- await waitFor ( ( ) => expect ( toastErrorSpy ) . toHaveBeenCalledTimes ( 2 ) ) ;
97+ await waitFor ( ( ) => expect ( scrolled . length ) . toBe ( 2 ) ) ;
9498
95- const [ first , second ] = toastErrorSpy . mock . calls . map ( ( c : unknown [ ] ) => ( c [ 1 ] as { id ?: unknown } | undefined ) ?. id ) ;
96- expect ( first ) . toBeDefined ( ) ;
97- expect ( second ) . toBe ( first ) ;
99+ expect ( screen . getAllByText ( REFUSAL ) ) . toHaveLength ( 1 ) ;
100+ expect ( screen . getAllByRole ( 'alert' ) ) . toHaveLength ( 1 ) ;
101+ expect ( toastErrorSpy ) . not . toHaveBeenCalled ( ) ;
98102 } ) ;
99103} ) ;
0 commit comments