11import { EventEmitter } from 'node:events'
2- import { describe , expect , test } from 'vitest'
2+ import { afterEach , beforeEach , describe , expect , test , vi } from 'vitest'
33import { mockDeep } from 'vitest-mock-extended'
44import { EntityModelType , type ActionEntityModel } from '@companion-app/shared/Model/EntityModel.js'
55import { exprVal } from '@companion-app/shared/Model/Options.js'
@@ -11,6 +11,18 @@ import { InternalControls } from '../../lib/Internal/Controls.js'
1111import type { ActionForInternalExecution } from '../../lib/Internal/Types.js'
1212import type { IPageStore } from '../../lib/Page/Store.js'
1313
14+ // Fake timers don't reach node:timers/promises, so route it through the (faked) global setTimeout
15+ vi . mock ( 'node:timers/promises' , ( ) => ( {
16+ setTimeout : async < T > ( delay : number , value : T , options : { signal ?: AbortSignal } ) =>
17+ new Promise < T > ( ( resolve , reject ) => {
18+ const timer = globalThis . setTimeout ( ( ) => resolve ( value ) , delay )
19+ options . signal ?. addEventListener ( 'abort' , ( ) => {
20+ globalThis . clearTimeout ( timer )
21+ reject ( new Error ( 'aborted' ) )
22+ } )
23+ } ) ,
24+ } ) )
25+
1426function createControls ( ) {
1527 const graphicsController = mockDeep < GraphicsController > ( )
1628 const controlsStore = mockDeep < IControlStore > ( )
@@ -33,10 +45,10 @@ function makeStoredAction(definitionId: string): ActionEntityModel {
3345 }
3446}
3547
36- function makeExecAction ( options : Record < string , unknown > ) : ActionForInternalExecution {
48+ function makeExecAction ( definitionId : string , options : Record < string , unknown > ) : ActionForInternalExecution {
3749 return {
3850 id : 'action1' ,
39- definitionId : 'button_rotate' ,
51+ definitionId,
4052 options : options as any ,
4153 rawEntity : { rawOptions : { } } as any ,
4254 }
@@ -79,14 +91,95 @@ describe('actionUpgrade', () => {
7991 expect ( controls . actionUpgrade ( makeStoredAction ( 'button_press' ) , 'ctrl1' ) ) . toBeUndefined ( )
8092 expect ( controls . actionUpgrade ( makeStoredAction ( 'button_rotate' ) , 'ctrl1' ) ) . toBeUndefined ( )
8193 } )
94+
95+ test ( 'button_pressrelease gains a zero hold duration, matching the old immediate release' , ( ) => {
96+ const { controls } = createControls ( )
97+
98+ const upgraded = controls . actionUpgrade ( makeStoredAction ( 'button_pressrelease' ) , 'ctrl1' )
99+
100+ expect ( upgraded ?. definitionId ) . toBe ( 'button_pressrelease' )
101+ expect ( upgraded ?. options . duration ) . toEqual ( { value : 0 , isExpression : false } )
102+ expect ( upgraded ?. options . location ) . toEqual ( exprVal ( '1/2/3' ) )
103+ } )
104+
105+ test ( 'button_pressrelease with a duration is left untouched' , ( ) => {
106+ const { controls } = createControls ( )
107+
108+ const action = makeStoredAction ( 'button_pressrelease' )
109+ action . options . duration = exprVal ( 500 )
110+
111+ expect ( controls . actionUpgrade ( action , 'ctrl1' ) ) . toBeUndefined ( )
112+ } )
113+ } )
114+
115+ describe ( 'button_pressrelease execution' , ( ) => {
116+ beforeEach ( ( ) => {
117+ vi . useFakeTimers ( )
118+ } )
119+ afterEach ( ( ) => {
120+ vi . useRealTimers ( )
121+ } )
122+
123+ test ( 'a zero duration presses and releases immediately' , ( ) => {
124+ const { controls, controlsStore, pageStore } = createControls ( )
125+ pageStore . getControlIdAt . mockReturnValue ( 'control:target' )
126+
127+ const result = controls . executeAction (
128+ makeExecAction ( 'button_pressrelease' , { location : '1/2/3' , force : false , duration : 0 } ) ,
129+ fakeExtras
130+ )
131+
132+ expect ( result ) . toEqual ( { result : undefined } )
133+ expect ( controlsStore . pressControl . mock . calls ) . toEqual ( [
134+ [ 'control:target' , true , 'surface0' , false ] ,
135+ [ 'control:target' , false , 'surface0' , false ] ,
136+ ] )
137+ } )
138+
139+ test ( 'holds the button for the duration before releasing' , async ( ) => {
140+ const { controls, controlsStore, pageStore } = createControls ( )
141+ pageStore . getControlIdAt . mockReturnValue ( 'control:target' )
142+
143+ const result = controls . executeAction (
144+ makeExecAction ( 'button_pressrelease' , { location : '1/2/3' , force : true , duration : 500 } ) ,
145+ fakeExtras
146+ )
147+
148+ expect ( controlsStore . pressControl . mock . calls ) . toEqual ( [ [ 'control:target' , true , 'surface0' , true ] ] )
149+
150+ await vi . advanceTimersByTimeAsync ( 499 )
151+ expect ( controlsStore . pressControl ) . toHaveBeenCalledTimes ( 1 )
152+
153+ await vi . advanceTimersByTimeAsync ( 1 )
154+ expect ( controlsStore . pressControl ) . toHaveBeenLastCalledWith ( 'control:target' , false , 'surface0' , true )
155+ await expect ( result ) . resolves . toEqual ( { result : undefined } )
156+ } )
157+
158+ test ( 'releases straight away when the run is aborted mid-hold' , async ( ) => {
159+ const { controls, controlsStore, pageStore } = createControls ( )
160+ pageStore . getControlIdAt . mockReturnValue ( 'control:target' )
161+
162+ const abort = new AbortController ( )
163+ const result = controls . executeAction (
164+ makeExecAction ( 'button_pressrelease' , { location : '1/2/3' , force : false , duration : 5000 } ) ,
165+ { ...fakeExtras , abortDelayed : abort . signal }
166+ )
167+
168+ abort . abort ( )
169+ await expect ( result ) . resolves . toEqual ( { result : undefined } )
170+ expect ( controlsStore . pressControl . mock . calls ) . toEqual ( [
171+ [ 'control:target' , true , 'surface0' , false ] ,
172+ [ 'control:target' , false , 'surface0' , false ] ,
173+ ] )
174+ } )
82175} )
83176
84177describe ( 'button_rotate execution' , ( ) => {
85178 test ( 'rotates the target control by the signed delta' , ( ) => {
86179 const { controls, controlsStore, pageStore } = createControls ( )
87180 pageStore . getControlIdAt . mockReturnValue ( 'control:target' )
88181
89- controls . executeAction ( makeExecAction ( { location : '1/2/3' , delta : 5 } ) , fakeExtras )
182+ void controls . executeAction ( makeExecAction ( 'button_rotate' , { location : '1/2/3' , delta : 5 } ) , fakeExtras )
90183
91184 expect ( controlsStore . rotateControl ) . toHaveBeenCalledWith ( 'control:target' , 5 , 'surface0' )
92185 } )
@@ -95,7 +188,7 @@ describe('button_rotate execution', () => {
95188 const { controls, controlsStore, pageStore } = createControls ( )
96189 pageStore . getControlIdAt . mockReturnValue ( 'control:target' )
97190
98- controls . executeAction ( makeExecAction ( { location : '1/2/3' , delta : - 3 } ) , fakeExtras )
191+ void controls . executeAction ( makeExecAction ( 'button_rotate' , { location : '1/2/3' , delta : - 3 } ) , fakeExtras )
99192
100193 expect ( controlsStore . rotateControl ) . toHaveBeenCalledWith ( 'control:target' , - 3 , 'surface0' )
101194 } )
@@ -104,8 +197,8 @@ describe('button_rotate execution', () => {
104197 const { controls, controlsStore, pageStore } = createControls ( )
105198 pageStore . getControlIdAt . mockReturnValue ( 'control:target' )
106199
107- controls . executeAction ( makeExecAction ( { location : '1/2/3' , delta : 0 } ) , fakeExtras )
108- controls . executeAction ( makeExecAction ( { location : '1/2/3' , delta : 'nope' } ) , fakeExtras )
200+ void controls . executeAction ( makeExecAction ( 'button_rotate' , { location : '1/2/3' , delta : 0 } ) , fakeExtras )
201+ void controls . executeAction ( makeExecAction ( 'button_rotate' , { location : '1/2/3' , delta : 'nope' } ) , fakeExtras )
109202
110203 expect ( controlsStore . rotateControl ) . not . toHaveBeenCalled ( )
111204 } )
0 commit comments