-
Notifications
You must be signed in to change notification settings - Fork 404
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(store): warn on unhandled actions
- Loading branch information
Showing
7 changed files
with
185 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
69 changes: 69 additions & 0 deletions
69
packages/store/src/unhandled-actions-logger/unhandled-actions-logger.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { ɵivyEnabled, inject, Injectable } from '@angular/core'; | ||
|
||
import { ActionType } from '../actions/symbols'; | ||
import { getActionTypeFromInstance } from '../utils/utils'; | ||
import { InitState, UpdateState } from '../actions/actions'; | ||
|
||
@Injectable({ providedIn: 'root' }) | ||
export class UnhandledActionsLogger { | ||
/** | ||
* These actions should be ignored by default; we can increase this | ||
* list in the future via the `ignoreActions` method. | ||
*/ | ||
private _ignoredActions = new Set<string>([InitState.type, UpdateState.type]); | ||
|
||
/** | ||
* Adds actions to the internal list of actions that should be ignored. | ||
*/ | ||
ignoreActions(...actions: ActionType[]): void { | ||
for (const action of actions) { | ||
this._ignoredActions.add(action.type); | ||
} | ||
} | ||
|
||
/** @internal */ | ||
warnIfNeeded(action: any): void { | ||
const actionShouldBeIgnored = Array.from(this._ignoredActions).some( | ||
type => type === getActionTypeFromInstance(action) | ||
); | ||
|
||
if (actionShouldBeIgnored) { | ||
return; | ||
} | ||
|
||
action = | ||
action.constructor && action.constructor.name !== 'Object' | ||
? action.constructor.name | ||
: action.type; | ||
|
||
console.warn( | ||
`The ${action} action has been dispatched but hasn't been handled. This may happen if the state with an action handler for this action is not registered.` | ||
); | ||
} | ||
} | ||
|
||
/** | ||
* A helper function may be invoked within constructors to add actions that | ||
* should be ignored. This eliminates injecting the `UnhandledActionsLogger` | ||
* directly. Moreover, developers should guard this function with the `ngDevMode` | ||
* variable that will allow tree-shake `UnhandledActionsLogger` and this function | ||
* itself from the production bundle: | ||
* ```ts | ||
* import { RouterNavigation, RouterCancel } from '@ngxs/router-plugin'; | ||
* | ||
* declare const ngDevMode: boolean; | ||
* | ||
* @Component() | ||
* export class AppComponent { | ||
* constructor() { | ||
* ngDevMode && ignoreActions(RouterNavigation, RouterCancel); | ||
* } | ||
* } | ||
* ``` | ||
*/ | ||
export function ignoreActions(...actions: ActionType[]): void { | ||
if (ɵivyEnabled) { | ||
const unhandledActionsLogger = inject(UnhandledActionsLogger); | ||
unhandledActionsLogger.ignoreActions(...actions); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import { TestBed } from '@angular/core/testing'; | ||
|
||
import { Store, NgxsModule, UnhandledActionsLogger } from '..'; | ||
|
||
describe('Unhandled actions warnings', () => { | ||
class FireAndForget { | ||
static readonly type = 'Fire & forget'; | ||
} | ||
|
||
const plainObjectAction = { type: 'Fire & Forget' }; | ||
|
||
const testSetup = (warnOnUnhandledActions = true) => { | ||
TestBed.configureTestingModule({ | ||
imports: [NgxsModule.forRoot([], { warnOnUnhandledActions })] | ||
}); | ||
|
||
const store = TestBed.inject(Store); | ||
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(); | ||
const unhandledActionsLogger = TestBed.inject(UnhandledActionsLogger); | ||
return { store, warnSpy, unhandledActionsLogger }; | ||
}; | ||
|
||
it('should not warn on unhandled actions if the config option is disabled', () => { | ||
// Arrange | ||
const { store, warnSpy } = testSetup(/* warnOnUnhandledActions */ false); | ||
// Act | ||
store.dispatch(new FireAndForget()); | ||
// Assert | ||
try { | ||
expect(warnSpy).not.toHaveBeenCalled(); | ||
} finally { | ||
warnSpy.mockRestore(); | ||
} | ||
}); | ||
|
||
describe('warn on unhandled actions', () => { | ||
it('should warn when the action is a class', () => { | ||
// Arrange | ||
const { store, warnSpy } = testSetup(); | ||
const action = new FireAndForget(); | ||
// Act | ||
store.dispatch(action); | ||
// Assert | ||
try { | ||
expect(warnSpy).toHaveBeenCalledTimes(1); | ||
expect(warnSpy).toHaveBeenCalledWith( | ||
`The FireAndForget action has been dispatched but hasn't been handled. This may happen if the state with an action handler for this action is not registered.` | ||
); | ||
} finally { | ||
warnSpy.mockRestore(); | ||
} | ||
}); | ||
|
||
it('should warn when the action is an object', () => { | ||
// Arrange | ||
const { store, warnSpy } = testSetup(); | ||
// Act | ||
store.dispatch(plainObjectAction); | ||
// Assert | ||
try { | ||
expect(warnSpy).toHaveBeenCalledTimes(1); | ||
expect(warnSpy).toHaveBeenCalledWith( | ||
`The Fire & Forget action has been dispatched but hasn't been handled. This may happen if the state with an action handler for this action is not registered.` | ||
); | ||
} finally { | ||
warnSpy.mockRestore(); | ||
} | ||
}); | ||
|
||
it('should be possible to add custom actions which should be ignored', () => { | ||
// Arrange | ||
const { store, warnSpy, unhandledActionsLogger } = testSetup(); | ||
unhandledActionsLogger.ignoreActions(FireAndForget, plainObjectAction); | ||
// Act | ||
store.dispatch(new FireAndForget()); | ||
store.dispatch(plainObjectAction); | ||
// Assert | ||
try { | ||
expect(warnSpy).not.toHaveBeenCalled(); | ||
} finally { | ||
warnSpy.mockRestore(); | ||
} | ||
}); | ||
}); | ||
}); |