@@ -294,7 +294,7 @@ public async Task ParsesCurrentValueAsStringWhenChanged_Valid()
294
294
rootComponent . EditContext . OnValidationStateChanged += ( sender , eventArgs ) => { numValidationStateChanges ++ ; } ;
295
295
296
296
// Act
297
- inputComponent . CurrentValueAsString = "1991/11/20" ;
297
+ await inputComponent . SetCurrentValueAsStringAsync ( "1991/11/20" ) ;
298
298
299
299
// Assert
300
300
var receivedParsedValue = valueChangedArgs . Single ( ) ;
@@ -324,14 +324,14 @@ public async Task ParsesCurrentValueAsStringWhenChanged_Invalid()
324
324
rootComponent . EditContext . OnValidationStateChanged += ( sender , eventArgs ) => { numValidationStateChanges ++ ; } ;
325
325
326
326
// Act/Assert 1: Transition to invalid
327
- inputComponent . CurrentValueAsString = "1991/11/40" ;
327
+ await inputComponent . SetCurrentValueAsStringAsync ( "1991/11/40" ) ;
328
328
Assert . Empty ( valueChangedArgs ) ;
329
329
Assert . True ( rootComponent . EditContext . IsModified ( fieldIdentifier ) ) ;
330
330
Assert . Equal ( new [ ] { "Bad date value" } , rootComponent . EditContext . GetValidationMessages ( fieldIdentifier ) ) ;
331
331
Assert . Equal ( 1 , numValidationStateChanges ) ;
332
332
333
333
// Act/Assert 2: Transition to valid
334
- inputComponent . CurrentValueAsString = "1991/11/20" ;
334
+ await inputComponent . SetCurrentValueAsStringAsync ( "1991/11/20" ) ;
335
335
var receivedParsedValue = valueChangedArgs . Single ( ) ;
336
336
Assert . Equal ( 1991 , receivedParsedValue . Year ) ;
337
337
Assert . Equal ( 11 , receivedParsedValue . Month ) ;
@@ -341,6 +341,65 @@ public async Task ParsesCurrentValueAsStringWhenChanged_Invalid()
341
341
Assert . Equal ( 2 , numValidationStateChanges ) ;
342
342
}
343
343
344
+ [ Fact ]
345
+ public async Task RespondsToValidationStateChangeNotifications ( )
346
+ {
347
+ // Arrange
348
+ var model = new TestModel ( ) ;
349
+ var rootComponent = new TestInputHostComponent < string , TestInputComponent < string > >
350
+ {
351
+ EditContext = new EditContext ( model ) ,
352
+ ValueExpression = ( ) => model . StringProperty
353
+ } ;
354
+ var fieldIdentifier = FieldIdentifier . Create ( ( ) => model . StringProperty ) ;
355
+ var renderer = new TestRenderer ( ) ;
356
+ var rootComponentId = renderer . AssignRootComponentId ( rootComponent ) ;
357
+ await renderer . RenderRootComponentAsync ( rootComponentId ) ;
358
+
359
+ // Initally, it rendered one batch and is valid
360
+ var batch1 = renderer . Batches . Single ( ) ;
361
+ var componentFrame1 = batch1 . GetComponentFrames < TestInputComponent < string > > ( ) . Single ( ) ;
362
+ var inputComponentId = componentFrame1 . ComponentId ;
363
+ var component = ( TestInputComponent < string > ) componentFrame1 . Component ;
364
+ Assert . Equal ( "valid" , component . CssClass ) ;
365
+
366
+ // Act: update the field state in the EditContext and notify
367
+ var messageStore = new ValidationMessageStore ( rootComponent . EditContext ) ;
368
+ messageStore . Add ( fieldIdentifier , "Some message" ) ;
369
+ await renderer . Dispatcher . InvokeAsync ( rootComponent . EditContext . NotifyValidationStateChanged ) ;
370
+
371
+ // Assert: The input component rendered itself again and now has the new class
372
+ var batch2 = renderer . Batches . Skip ( 1 ) . Single ( ) ;
373
+ Assert . Equal ( inputComponentId , batch2 . DiffsByComponentId . Keys . Single ( ) ) ;
374
+ Assert . Equal ( "invalid" , component . CssClass ) ;
375
+ }
376
+
377
+ [ Fact ]
378
+ public async Task UnsubscribesFromValidationStateChangeNotifications ( )
379
+ {
380
+ // Arrange
381
+ var model = new TestModel ( ) ;
382
+ var rootComponent = new TestInputHostComponent < string , TestInputComponent < string > >
383
+ {
384
+ EditContext = new EditContext ( model ) ,
385
+ ValueExpression = ( ) => model . StringProperty
386
+ } ;
387
+ var fieldIdentifier = FieldIdentifier . Create ( ( ) => model . StringProperty ) ;
388
+ var renderer = new TestRenderer ( ) ;
389
+ var rootComponentId = renderer . AssignRootComponentId ( rootComponent ) ;
390
+ await renderer . RenderRootComponentAsync ( rootComponentId ) ;
391
+ var component = renderer . Batches . Single ( ) . GetComponentFrames < TestInputComponent < string > > ( ) . Single ( ) . Component ;
392
+
393
+ // Act: dispose, then update the field state in the EditContext and notify
394
+ ( ( IDisposable ) component ) . Dispose ( ) ;
395
+ var messageStore = new ValidationMessageStore ( rootComponent . EditContext ) ;
396
+ messageStore . Add ( fieldIdentifier , "Some message" ) ;
397
+ await renderer . Dispatcher . InvokeAsync ( rootComponent . EditContext . NotifyValidationStateChanged ) ;
398
+
399
+ // Assert: No additional render
400
+ Assert . Empty ( renderer . Batches . Skip ( 1 ) ) ;
401
+ }
402
+
344
403
private static TComponent FindComponent < TComponent > ( CapturedBatch batch )
345
404
=> batch . ReferenceFrames
346
405
. Where ( f => f . FrameType == RenderTreeFrameType . Component )
@@ -376,7 +435,6 @@ class TestInputComponent<T> : InputBase<T>
376
435
public new string CurrentValueAsString
377
436
{
378
437
get => base . CurrentValueAsString ;
379
- set { base . CurrentValueAsString = value ; }
380
438
}
381
439
382
440
public new IReadOnlyDictionary < string , object > AdditionalAttributes => base . AdditionalAttributes ;
@@ -391,6 +449,15 @@ protected override bool TryParseValueFromString(string value, out T result, out
391
449
{
392
450
throw new NotImplementedException ( ) ;
393
451
}
452
+
453
+ public async Task SetCurrentValueAsStringAsync ( string value )
454
+ {
455
+ // This is equivalent to the subclass writing to CurrentValueAsString
456
+ // (e.g., from @bind), except to simplify the test code there's an InvokeAsync
457
+ // here. In production code it wouldn't normally be required because @bind
458
+ // calls run on the sync context anyway.
459
+ await InvokeAsync ( ( ) => { base . CurrentValueAsString = value ; } ) ;
460
+ }
394
461
}
395
462
396
463
class TestDateInputComponent : TestInputComponent < DateTime >
0 commit comments