@@ -299,6 +299,96 @@ describe('Events API', () => {
299299 } ) ;
300300} ) ;
301301
302+ describe ( 'Plugin Invalid Action API' , ( ) => {
303+ const pluginName = 'validator' ;
304+ const message = 'G.value must divide by 5' ;
305+ const game : Game < { value : number } > = {
306+ setup : ( ) => ( { value : 5 } ) ,
307+ plugins : [
308+ {
309+ name : pluginName ,
310+ isInvalid : ( { G } ) => {
311+ if ( G . value % 5 !== 0 ) return message ;
312+ return false ;
313+ } ,
314+ } ,
315+ ] ,
316+ moves : {
317+ setValue : ( G , _ctx , arg ) => {
318+ G . value = arg ;
319+ } ,
320+ } ,
321+ phases : {
322+ unenterable : {
323+ onBegin : ( ) => ( { value : 13 } ) ,
324+ } ,
325+ enterable : {
326+ onBegin : ( ) => ( { value : 25 } ) ,
327+ } ,
328+ } ,
329+ } ;
330+
331+ let state : State ;
332+ beforeEach ( ( ) => {
333+ state = InitializeGame ( { game } ) ;
334+ } ) ;
335+
336+ describe ( 'multiplayer client' , ( ) => {
337+ const reducer = CreateGameReducer ( { game } ) ;
338+
339+ test ( 'move is cancelled if plugin declares it invalid' , ( ) => {
340+ state = reducer ( state , makeMove ( 'setValue' , [ 6 ] , '0' ) ) ;
341+ expect ( state . G ) . toMatchObject ( { value : 5 } ) ;
342+ expect ( state [ 'transients' ] . error ) . toEqual ( {
343+ type : 'action/plugin_invalid' ,
344+ payload : { plugin : pluginName , message } ,
345+ } ) ;
346+ } ) ;
347+
348+ test ( 'move is processed if no plugin declares it invalid' , ( ) => {
349+ state = reducer ( state , makeMove ( 'setValue' , [ 15 ] , '0' ) ) ;
350+ expect ( state . G ) . toMatchObject ( { value : 15 } ) ;
351+ expect ( state [ 'transients' ] ) . toBeUndefined ( ) ;
352+ } ) ;
353+
354+ test ( 'event is cancelled if plugin declares it invalid' , ( ) => {
355+ state = reducer ( state , gameEvent ( 'setPhase' , 'unenterable' , '0' ) ) ;
356+ expect ( state . G ) . toMatchObject ( { value : 5 } ) ;
357+ expect ( state . ctx . phase ) . toBe ( null ) ;
358+ expect ( state [ 'transients' ] . error ) . toEqual ( {
359+ type : 'action/plugin_invalid' ,
360+ payload : { plugin : pluginName , message } ,
361+ } ) ;
362+ } ) ;
363+
364+ test ( 'event is processed if no plugin declares it invalid' , ( ) => {
365+ state = reducer ( state , gameEvent ( 'setPhase' , 'enterable' , '0' ) ) ;
366+ expect ( state . G ) . toMatchObject ( { value : 25 } ) ;
367+ expect ( state . ctx . phase ) . toBe ( 'enterable' ) ;
368+ expect ( state [ 'transients' ] ) . toBeUndefined ( ) ;
369+ } ) ;
370+ } ) ;
371+
372+ describe ( 'local client' , ( ) => {
373+ const reducer = CreateGameReducer ( { game, isClient : true } ) ;
374+
375+ test ( 'move is cancelled if plugin declares it invalid' , ( ) => {
376+ state = reducer ( state , makeMove ( 'setValue' , [ 6 ] , '0' ) ) ;
377+ expect ( state . G ) . toMatchObject ( { value : 5 } ) ;
378+ expect ( state [ 'transients' ] . error ) . toEqual ( {
379+ type : 'action/plugin_invalid' ,
380+ payload : { plugin : pluginName , message } ,
381+ } ) ;
382+ } ) ;
383+
384+ test ( 'move is processed if no plugin declares it invalid' , ( ) => {
385+ state = reducer ( state , makeMove ( 'setValue' , [ 15 ] , '0' ) ) ;
386+ expect ( state . G ) . toMatchObject ( { value : 15 } ) ;
387+ expect ( state [ 'transients' ] ) . toBeUndefined ( ) ;
388+ } ) ;
389+ } ) ;
390+ } ) ;
391+
302392describe ( 'Random inside setup()' , ( ) => {
303393 const game1 : Game = {
304394 seed : 'seed1' ,
0 commit comments