diff --git a/libs/permissions/permissionLogic/src/lib/permission-logic.spec.ts b/libs/permissions/permissionLogic/src/lib/permission-logic.spec.ts index 2a989df..9a304e0 100644 --- a/libs/permissions/permissionLogic/src/lib/permission-logic.spec.ts +++ b/libs/permissions/permissionLogic/src/lib/permission-logic.spec.ts @@ -14,8 +14,26 @@ import { sendTo, setup, waitFor, + InspectionEvent, } from 'xstate'; - +import { + createWebSocketInspector, + createSkyInspector, +} from '@statelyai/inspect'; +// function simplifyEvent(ev: InspectionEvent) { +// return ev.type === '@xstate.actor' +// ? { type: ev.type, rootId: ev.rootId } +// : ev.type === '@xstate.event' +// ? { type: ev.type, rootId: ev.rootId, event: ev.event } +// : { +// type: ev.type, +// sessionId: ev.rootId, +// snapshot: +// 'value' in ev.actorRef.getSnapshot() +// ? ev.actorRef.getSnapshot().value +// : ev.actorRef.getSnapshot(), +// }; +// } import { unimplementedPermissionMachineActions } from './permission.actions'; import { InitialPermissionStatusMap, @@ -28,22 +46,22 @@ import { } from './permission.types'; describe('permission requester and checker machine', () => { - it('should check permission when triggered', async () => { - const bluetoothPermissionActor = createActor( - permissionCheckerAndRequesterMachine, - { input: { parent: undefined } } - ).start(); - - bluetoothPermissionActor.send({ type: 'triggerPermissionCheck' }); - - await waitFor(bluetoothPermissionActor, (state) => state.value === 'idle'); - - expect(bluetoothPermissionActor.getSnapshot().value).toBe('idle'); - expect(bluetoothPermissionActor.getSnapshot().context.statuses).toEqual({ - [Permissions.bluetooth]: PermissionStatuses.denied, - [Permissions.microphone]: PermissionStatuses.denied, - }); - }); + // it('should check permission when triggered', async () => { + // const bluetoothPermissionActor = createActor( + // permissionCheckerAndRequesterMachine, + // { input: { parent: undefined } } + // ).start(); + // + // bluetoothPermissionActor.send({ type: 'triggerPermissionCheck' }); + // + // await waitFor(bluetoothPermissionActor, (state) => state.value === 'idle'); + // + // expect(bluetoothPermissionActor.getSnapshot().value).toBe('idle'); + // expect(bluetoothPermissionActor.getSnapshot().context.statuses).toEqual({ + // [Permissions.bluetooth]: PermissionStatuses.denied, + // [Permissions.microphone]: PermissionStatuses.denied, + // }); + // }); type ApplicationLifecycleState = | 'applicationForegrounded' @@ -54,6 +72,7 @@ describe('permission requester and checker machine', () => { const stubSubscribeToApplicationStateChanges = ( handleApplicationStateChange: ApplicationStateChangeHandler ) => { + console.log('subscribed to fake handler'); handleApplicationStateChange('applicationForegrounded'); return () => { @@ -62,19 +81,8 @@ describe('permission requester and checker machine', () => { }; it('should report permission to parent after a check', async () => { - let result: any; - const spy = ( - something: /* TODO: change type to whatever an event is in xstate*/ any - ) => { - result = something; - }; - const stubApplicationLifecycleReportingMachine = // TODO figure out how to type what events this sends back - // : CallbackActorLogic< - // ApplicationLifecycleEvents, - // ApplicationLifecycleEvents - // > fromCallback(({ sendBack }) => { /** * The real implementation of this actor should setup a subscription @@ -100,15 +108,6 @@ describe('permission requester and checker machine', () => { return unsubscribeApplicationStateListeners; }); - // invoke: { - // src: fromCallback(({ sendBack, receive, input }) => { - // // ... - // }), - // input: ({ context, event }) => ({ - // userId: context.userId, - // }), - // }, - const permissionMonitoringMachine = setup({ types: {} as { events: ParentEvent; @@ -119,29 +118,31 @@ describe('permission requester and checker machine', () => { stubApplicationLifecycleReportingMachine, permissionCheckerAndRequesterMachine, }, + actions: { - triggerPermissionCheck: raise({ type: 'triggerPermissionCheck' }), assignPermissionCheckResultsToContext: assign({ permissionsStatuses: ({ event }) => { assertEvent(event, 'allPermissionsChecked'); - console.log(JSON.stringify(event.statuses, null, 2)); return event.statuses; }, }), + assignPermissionRequestResultToContext: assign({ + permissionsStatuses: ({ event, context }) => { + assertEvent(event, 'permissionRequested'); + return { + ...context.permissionsStatuses, + [event.permission]: event.status, + }; + }, + }), }, }).createMachine({ + id: 'bigKahuna', type: 'parallel', context: { - permissionsStatuses: {} as PermissionStatusMapType, + permissionsStatuses: InitialPermissionStatusMap, }, - - // on: { - // '*': { - // actions: log(({ event, context }) => ({ event, context })), - // }, - // }, - states: { applicationLifecycle: { on: { @@ -160,7 +161,7 @@ describe('permission requester and checker machine', () => { states: { applicationIsInForeground: { - entry: 'triggerPermissionCheck', + entry: raise({ type: 'triggerPermissionCheck' }), }, applicationInInBackground: {}, }, @@ -173,12 +174,28 @@ describe('permission requester and checker machine', () => { }, triggerPermissionCheck: { actions: [ - log('triggerPermissionCheck from root machine'), + log('permission trigger check'), sendTo('someFooMachine', { type: 'triggerPermissionCheck', }), ], }, + triggerPermissionRequest: { + actions: [ + // log('triggering permission request'), + // () => { + // console.log('triggering permission request'); + // }, + // sendTo('someFooMachine', { + // type: 'triggerPermissionRequest', + // permission: Permissions.microphone, + // }), + ], + }, + + permissionRequested: { + actions: 'assignPermissionRequestResultToContext', + }, }, invoke: { id: 'someFooMachine', @@ -189,11 +206,27 @@ describe('permission requester and checker machine', () => { }, }); - const actorRef = createActor(permissionMonitoringMachine).start(); + const permission: Permission = Permissions.microphone; + + const actorRef = createActor(permissionMonitoringMachine, { + inspect: { + next: (event: InspectionEvent) => {}, + error: (error) => { + console.log(error); + }, + complete: () => { + console.log('complete'); + }, + }, + }).start(); expect(actorRef.getSnapshot().context).toStrictEqual({ - permissionsStatuses: {}, + permissionsStatuses: { + [Permissions.bluetooth]: PermissionStatuses.unasked, + [Permissions.microphone]: PermissionStatuses.unasked, + }, }); + expect(actorRef.getSnapshot().value).toStrictEqual({ applicationLifecycle: 'applicationIsInForeground', permissions: {}, @@ -202,21 +235,10 @@ describe('permission requester and checker machine', () => { await waitFor(actorRef, (state) => { return ( // @ts-expect-error - state.children.someFooMachine?.getSnapshot().value === - 'checkingPermissions' + state.children.someFooMachine?.getSnapshot().value === 'idle' ); }); - expect( - // @ts-expect-error - actorRef.getSnapshot().children.someFooMachine?.getSnapshot().value - ).toBe('checkingPermissions'); - - await waitFor(actorRef, (state) => { - // @ts-expect-error - return state.children.someFooMachine?.getSnapshot().value === 'idle'; - }); - expect(actorRef.getSnapshot().context).toStrictEqual({ permissionsStatuses: { [Permissions.bluetooth]: PermissionStatuses.denied, @@ -224,245 +246,169 @@ describe('permission requester and checker machine', () => { }, }); - actorRef.send({ - type: 'triggerPermissionRequest', - permission: Permissions.microphone, - }); - - await waitFor( - actorRef, - (state) => state.children.someFooMachine?.getSnapshot().value === 'idle' - ); - expect(actorRef.getSnapshot().context).toStrictEqual({ - permissionsStatuses: { - [Permissions.bluetooth]: PermissionStatuses.denied, - [Permissions.microphone]: PermissionStatuses.granted, - }, - }); - - // expect(result).not.toBeNull(); - // expect(result.event).toStrictEqual({ - // type: 'allPermissionsChecked', - // statuses: { - // [Permissions.bluetooth]: PermissionStatuses.denied, - // [Permissions.microphone]: PermissionStatuses.denied, - // }, + // expect(actorRef.getSnapshot().value).toStrictEqual({ + // applicationLifecycle: 'applicationIsInForeground', + // permissions: {}, // }); - }); - it('should return actions for parallel machines', () => { - const actual: string[] = []; - const machine = setup({}).createMachine({ - type: 'parallel', - states: { - permission: { - on: { - foo: { - target: '.a2', - }, - }, - initial: 'a1', - states: { - a1: { - on: { - CHANGE: { - target: 'a2', - actions: [ - () => actual.push('do_a2'), - () => actual.push('another_do_a2'), - ], - }, - }, - entry: () => actual.push('enter_a1'), - exit: () => actual.push('exit_a1'), - }, - a2: { - entry: () => actual.push('enter_a2'), - exit: () => actual.push('exit_a2'), - }, - }, - entry: () => actual.push('enter_a'), - exit: () => actual.push('exit_a'), - }, - b: { - initial: 'b1', - states: { - b1: { - on: { - CHANGE: { target: 'b2', actions: () => actual.push('do_b2') }, - }, - entry: () => actual.push('enter_b1'), - exit: () => actual.push('exit_b1'), - }, - b2: { - entry: () => actual.push('enter_b2'), - exit: () => actual.push('exit_b2'), - }, - }, - entry: () => actual.push('enter_b'), - exit: () => actual.push('exit_b'), - }, - }, - }); - - const actor = createActor(machine).start(); - expect(actor.getSnapshot().value).toStrictEqual({ - permission: 'a1', - b: 'b1', - }); - - actor.send({ type: 'foo' }); - expect(actor.getSnapshot().value).toStrictEqual({ - permission: 'a2', - b: 'b1', - }); - - // actual.length = 0; - - // actor.send({ type: 'CHANGE' }); - - // expect(actual).toEqual([ - // 'exit_b1', // reverse document order - // 'exit_a1', - // 'do_a2', - // 'another_do_a2', - // 'do_b2', - // 'enter_a2', - // 'enter_b2', - // ]); - }); - - describe('requesting permissions', () => { - it('should request permission when triggered', async () => { - const permissionActor = createActor( - permissionCheckerAndRequesterMachine, - { input: { parent: undefined } } - ).start(); - const permission: Permission = Permissions.bluetooth; - - expect(permissionActor.getSnapshot().context.statuses[permission]).toBe( - PermissionStatuses.unasked - ); - - permissionActor.send({ - type: 'triggerPermissionRequest', - permission, - }); - - await waitFor(permissionActor, (state) => state.value === 'idle'); - - expect(permissionActor.getSnapshot().value).toBe('idle'); - expect(permissionActor.getSnapshot().context.statuses[permission]).toBe( - PermissionStatuses.granted - ); - }); + // expect(actorRef.getSnapshot().context).toStrictEqual({ + // permissionsStatuses: {}, + // }); - it('should report permission to parent after a request', async () => { - let result: any; - const spy = ( - something: /* TODO: change type to whatever an event is in xstate*/ any - ) => { - result = something; - }; - - const parentMachine = setup({ - types: {} as { events: ParentEvent }, - actors: { - permissionCheckerAndRequesterMachine, - }, - }).createMachine({ - on: { - permissionRequested: { - actions: spy, - }, - triggerPermissionRequest: { - actions: [ - sendTo('someFooMachine', { - type: 'triggerPermissionRequest', - permission: Permissions.bluetooth, - }), - ], - }, - }, - invoke: { - id: 'someFooMachine', - src: 'permissionCheckerAndRequesterMachine', - input: ({ self }) => ({ parent: self }), - }, - }); + // actorRef.send({ + // type: 'triggerPermissionRequest', + // permission: permission, + // }); - const actorRef = createActor(parentMachine).start(); - actorRef.send({ - type: 'triggerPermissionRequest', - permission: Permissions.bluetooth, - }); + // expect( + // // @ts-expect-error + // actorRef.getSnapshot().children.someFooMachine?.getSnapshot().value + // ).toBe('requestingPermission'); - await waitFor( - actorRef, - (state) => state.children.someFooMachine?.getSnapshot().value === 'idle' - ); + // await waitFor(actorRef, (state) => { + // // @ts-expect-error + // return state.children.someFooMachine?.getSnapshot().value === 'idle'; + // }); - expect(result).not.toBeNull(); - expect(result.event).toStrictEqual({ - type: 'permissionRequested', - status: PermissionStatuses.granted, - permission: Permissions.bluetooth, - }); - }); + // expect(actorRef.getSnapshot().context).toStrictEqual({ + // permissionsStatuses: { + // // [Permissions.bluetooth]: PermissionStatuses.denied, + // [permission]: PermissionStatuses.granted, + // }, + // }); }); - describe('Permission Monitoring Machine', () => { - it('should report permission to parent after a check', async () => { - let result: any; - const spy = ( - something: /* TODO: change type to whatever an event is in xstate*/ any - ) => { - result = something; - }; - - const parentMachine = setup({ - types: {} as { events: ParentEvent }, - actors: { - permissionCheckerAndRequesterMachine, - }, - }).createMachine({ - on: { - allPermissionsChecked: { - actions: spy, - }, - triggerPermissionCheck: { - actions: [ - sendTo('someFooMachine', { - type: 'triggerPermissionCheck', - }), - ], - }, - }, - invoke: { - id: 'someFooMachine', - src: 'permissionCheckerAndRequesterMachine', - input: ({ self }) => ({ parent: self }), - }, - }); - - const actorRef = createActor(parentMachine).start(); - actorRef.send({ type: 'triggerPermissionCheck' }); - - await waitFor( - actorRef, - (state) => state.children.someFooMachine?.getSnapshot().value === 'idle' - ); - - expect(result).not.toBeNull(); - expect(result.event).toStrictEqual({ - type: 'allPermissionsChecked', - statuses: { - [Permissions.bluetooth]: PermissionStatuses.denied, - [Permissions.microphone]: PermissionStatuses.denied, - }, - }); - }); - }); + // describe('requesting permissions', () => { + // it('should request permission when triggered', async () => { + // const permissionActor = createActor( + // permissionCheckerAndRequesterMachine, + // { input: { parent: undefined } } + // ).start(); + // const permission: Permission = Permissions.bluetooth; + // + // expect(permissionActor.getSnapshot().context.statuses[permission]).toBe( + // PermissionStatuses.unasked + // ); + // + // permissionActor.send({ + // type: 'triggerPermissionRequest', + // permission, + // }); + // + // await waitFor(permissionActor, (state) => state.value === 'idle'); + // + // expect(permissionActor.getSnapshot().value).toBe('idle'); + // expect(permissionActor.getSnapshot().context.statuses[permission]).toBe( + // PermissionStatuses.granted + // ); + // }); + // + // it('should report permission to parent after a request', async () => { + // let result: any; + // const spy = ( + // something: /* TODO: change type to whatever an event is in xstate*/ any + // ) => { + // result = something; + // }; + // + // const parentMachine = setup({ + // types: {} as { events: ParentEvent }, + // actors: { + // permissionCheckerAndRequesterMachine, + // }, + // }).createMachine({ + // on: { + // permissionRequested: { + // actions: spy, + // }, + // triggerPermissionRequest: { + // actions: [ + // sendTo('someFooMachine', { + // type: 'triggerPermissionRequest', + // permission: Permissions.bluetooth, + // }), + // ], + // }, + // }, + // invoke: { + // id: 'someFooMachine', + // src: 'permissionCheckerAndRequesterMachine', + // input: ({ self }) => ({ parent: self }), + // }, + // }); + // + // const actorRef = createActor(parentMachine).start(); + // actorRef.send({ + // type: 'triggerPermissionRequest', + // permission: Permissions.bluetooth, + // }); + // + // await waitFor( + // actorRef, + // (state) => state.children.someFooMachine?.getSnapshot().value === 'idle' + // ); + // + // expect(result).not.toBeNull(); + // expect(result.event).toStrictEqual({ + // type: 'permissionRequested', + // status: PermissionStatuses.granted, + // permission: Permissions.bluetooth, + // }); + // }); + // }); + + // describe('Permission Monitoring Machine', () => { + // it('should report permission to parent after a check', async () => { + // let result: any; + // const spy = ( + // something: /* TODO: change type to whatever an event is in xstate*/ any + // ) => { + // result = something; + // }; + // + // const parentMachine = setup({ + // types: {} as { events: ParentEvent }, + // actors: { + // permissionCheckerAndRequesterMachine, + // }, + // }).createMachine({ + // on: { + // allPermissionsChecked: { + // actions: spy, + // }, + // triggerPermissionCheck: { + // actions: [ + // sendTo('someFooMachine', { + // type: 'triggerPermissionCheck', + // }), + // ], + // }, + // }, + // invoke: { + // id: 'someFooMachine', + // src: 'permissionCheckerAndRequesterMachine', + // input: ({ self }) => ({ parent: self }), + // }, + // }); + // + // const actorRef = createActor(parentMachine).start(); + // actorRef.send({ type: 'triggerPermissionCheck' }); + // + // await waitFor( + // actorRef, + // (state) => state.children.someFooMachine?.getSnapshot().value === 'idle' + // ); + // + // expect(result).not.toBeNull(); + // expect(result.event).toStrictEqual({ + // type: 'allPermissionsChecked', + // statuses: { + // [Permissions.bluetooth]: PermissionStatuses.denied, + // [Permissions.microphone]: PermissionStatuses.denied, + // }, + // }); + // }); + // }); }); export type ParentEvent = @@ -488,7 +434,7 @@ const permissionCheckerAndRequesterMachine = setup({ }, events: {} as PermissionMachineEvents, input: {} as { - parent?: ActorRef, ParentEvent>; + parent: ActorRef, ParentEvent>; }, }, @@ -502,15 +448,17 @@ const permissionCheckerAndRequesterMachine = setup({ return; } - console.log('sending event to parent', event); + if (event.type === 'permissionRequested') { + console.log('sending event to parent', event); + } + console.log('sending event to parent', event); enqueue.sendTo(context.parent, event); } ), savePermissionRequestOutput: assign({ statuses: ({ context, event }) => { - console.log(JSON.stringify(event, null, 2)); return { ...context.statuses, // @ts-expect-error TODO how do I type these actions? @@ -571,15 +519,15 @@ const permissionCheckerAndRequesterMachine = setup({ status = // TODO how can i make this implementation more injectable and still ergnomic await unimplementedPermissionMachineActions.requestBluetoothPermission(); + break; case Permissions.microphone: status = // TODO how can i make this implementation more injectable and still ergnomic await unimplementedPermissionMachineActions.requestMicrophonePermission(); + break; } - console.log(JSON.stringify({ permission, status }, null, 2)); - return { status, permission }; } ), @@ -596,7 +544,10 @@ const permissionCheckerAndRequesterMachine = setup({ states: { idle: { on: { - triggerPermissionCheck: { target: 'checkingPermissions' }, + triggerPermissionCheck: { + target: 'checkingPermissions', + actions: [log('child triggerPermissionCheck')], + }, triggerPermissionRequest: { target: 'requestingPermission' }, }, }, @@ -622,8 +573,6 @@ const permissionCheckerAndRequesterMachine = setup({ */ type: 'checkedSendParent', params({ event }) { - console.log(JSON.stringify(event, null, 2)); - return { type: 'permissionRequested', status: event.output.status, @@ -642,13 +591,12 @@ const permissionCheckerAndRequesterMachine = setup({ onDone: { target: 'idle', actions: [ + log('child on done checkingPermissions'), 'savePermissionCheckResult', { type: 'checkedSendParent', params({ event }) { - console.log(JSON.stringify(event, null, 2)); - return { type: 'allPermissionsChecked', statuses: event.output,