fix(types,testing): tighten action context typing and update tests for stricter TypeScript checks
This commit is contained in:
+29
-21
@@ -1,4 +1,4 @@
|
||||
import { expect, tap } from '@push.rocks/tapbundle';
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as smartstate from '../ts/index.js';
|
||||
|
||||
type TTestStateParts = 'initTest' | 'persistTest' | 'forceTest';
|
||||
@@ -10,6 +10,14 @@ interface ITestState {
|
||||
};
|
||||
}
|
||||
|
||||
const getRequiredState = <TPayload>(statePart: smartstate.StatePart<any, TPayload>): TPayload => {
|
||||
const state = statePart.getState();
|
||||
if (state === undefined) {
|
||||
throw new Error('Expected state part to have initialized state');
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
// ============================
|
||||
// Init mode tests
|
||||
// ============================
|
||||
@@ -245,7 +253,7 @@ tap.test('middleware should transform state', async () => {
|
||||
});
|
||||
|
||||
await statePart.setState({ value: 2, nested: { data: 'hello' } });
|
||||
expect(statePart.getState().nested.data).toEqual('HELLO');
|
||||
expect(getRequiredState(statePart).nested.data).toEqual('HELLO');
|
||||
});
|
||||
|
||||
tap.test('middleware should reject state changes on throw', async () => {
|
||||
@@ -272,7 +280,7 @@ tap.test('middleware should reject state changes on throw', async () => {
|
||||
expect(error).not.toBeNull();
|
||||
expect(error?.message).toEqual('Value must be non-negative');
|
||||
// State should be unchanged
|
||||
expect(statePart.getState().value).toEqual(1);
|
||||
expect(getRequiredState(statePart).value).toEqual(1);
|
||||
});
|
||||
|
||||
tap.test('multiple middlewares should run in order', async () => {
|
||||
@@ -297,7 +305,7 @@ tap.test('multiple middlewares should run in order', async () => {
|
||||
await statePart.setState({ value: 5, nested: { data: 'test' } });
|
||||
expect(order).toEqual([1, 2]);
|
||||
// (5 + 10) * 2 = 30
|
||||
expect(statePart.getState().value).toEqual(30);
|
||||
expect(getRequiredState(statePart).value).toEqual(30);
|
||||
});
|
||||
|
||||
tap.test('middleware removal should work', async () => {
|
||||
@@ -312,12 +320,12 @@ tap.test('middleware removal should work', async () => {
|
||||
});
|
||||
|
||||
await statePart.setState({ value: 2, nested: { data: 'test' } });
|
||||
expect(statePart.getState().value).toEqual(200);
|
||||
expect(getRequiredState(statePart).value).toEqual(200);
|
||||
|
||||
remove();
|
||||
|
||||
await statePart.setState({ value: 3, nested: { data: 'test' } });
|
||||
expect(statePart.getState().value).toEqual(3);
|
||||
expect(getRequiredState(statePart).value).toEqual(3);
|
||||
});
|
||||
|
||||
// ============================
|
||||
@@ -639,7 +647,7 @@ tap.test('concurrent dispatchAction should serialize (counter reaches exactly 10
|
||||
const counter = await state.getStatePart<{ count: number }>('counter', { count: 0 });
|
||||
|
||||
const increment = counter.createAction<void>(async (statePart) => {
|
||||
const current = statePart.getState();
|
||||
const current = getRequiredState(statePart);
|
||||
return { count: current.count + 1 };
|
||||
});
|
||||
|
||||
@@ -650,7 +658,7 @@ tap.test('concurrent dispatchAction should serialize (counter reaches exactly 10
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
expect(counter.getState().count).toEqual(10);
|
||||
expect(getRequiredState(counter).count).toEqual(10);
|
||||
});
|
||||
|
||||
tap.test('concurrent setState should serialize (no lost updates)', async () => {
|
||||
@@ -669,7 +677,7 @@ tap.test('concurrent setState should serialize (no lost updates)', async () => {
|
||||
// At minimum, the final state should have been set 5 times without error
|
||||
// The exact values depend on serialization timing, but state should be valid
|
||||
expect(part.getState()).toBeTruthy();
|
||||
expect(part.getState().values).toBeInstanceOf(Array);
|
||||
expect(getRequiredState(part).values).toBeInstanceOf(Array);
|
||||
});
|
||||
|
||||
tap.test('dispose should complete the Subject and notify subscribers', async () => {
|
||||
@@ -737,7 +745,7 @@ tap.test('batch re-entrancy: setState during flush should not deadlock', async (
|
||||
// Wait for the fire-and-forget setState to complete
|
||||
await new Promise<void>((r) => setTimeout(r, 50));
|
||||
|
||||
expect(part.getState().v).toEqual(2);
|
||||
expect(getRequiredState(part).v).toEqual(2);
|
||||
});
|
||||
|
||||
tap.test('force mode should dispose old StatePart (Subject completes)', async () => {
|
||||
@@ -774,13 +782,13 @@ tap.test('context.dispatch should not deadlock on same StatePart', async () => {
|
||||
const part = await state.getStatePart<{ count: number }>('reentrantAction', { count: 0 });
|
||||
|
||||
const innerIncrement = part.createAction<void>(async (sp) => {
|
||||
return { count: sp.getState().count + 1 };
|
||||
return { count: getRequiredState(sp).count + 1 };
|
||||
});
|
||||
|
||||
const outerAction = part.createAction<void>(async (sp, _payload, context) => {
|
||||
// This would deadlock without the context.dispatch() mechanism
|
||||
await context.dispatch(innerIncrement, undefined);
|
||||
return sp.getState();
|
||||
return getRequiredState(sp);
|
||||
});
|
||||
|
||||
const result = await part.dispatchAction(outerAction, undefined);
|
||||
@@ -793,23 +801,23 @@ tap.test('deeply nested context.dispatch should work (3 levels)', async () => {
|
||||
const part = await state.getStatePart<{ steps: string[] }>('deepNested', { steps: [] });
|
||||
|
||||
const appendStep = part.createAction<string>(async (sp, step) => {
|
||||
return { steps: [...sp.getState().steps, step] };
|
||||
return { steps: [...getRequiredState(sp).steps, step] };
|
||||
});
|
||||
|
||||
const level2 = part.createAction<void>(async (sp, _payload, context) => {
|
||||
await context.dispatch(appendStep, 'level-2');
|
||||
return sp.getState();
|
||||
return getRequiredState(sp);
|
||||
});
|
||||
|
||||
const level1 = part.createAction<void>(async (sp, _payload, context) => {
|
||||
await context.dispatch(appendStep, 'level-1');
|
||||
await context.dispatch(level2, undefined);
|
||||
await context.dispatch(appendStep, 'level-1-after');
|
||||
return sp.getState();
|
||||
return getRequiredState(sp);
|
||||
});
|
||||
|
||||
await part.dispatchAction(level1, undefined);
|
||||
expect(part.getState().steps).toEqual(['level-1', 'level-2', 'level-1-after']);
|
||||
expect(getRequiredState(part).steps).toEqual(['level-1', 'level-2', 'level-1-after']);
|
||||
});
|
||||
|
||||
tap.test('circular context.dispatch should throw max depth error', async () => {
|
||||
@@ -820,12 +828,12 @@ tap.test('circular context.dispatch should throw max depth error', async () => {
|
||||
// Create a self-referencing action that will loop forever
|
||||
const circularAction: smartstate.StateAction<{ count: number }, void> = part.createAction<void>(
|
||||
async (sp, _payload, context) => {
|
||||
const current = sp.getState();
|
||||
const current = getRequiredState(sp);
|
||||
if (current.count < 100) {
|
||||
// This should eventually hit the depth limit
|
||||
await context.dispatch(circularAction, undefined);
|
||||
}
|
||||
return sp.getState();
|
||||
return getRequiredState(sp);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -851,7 +859,7 @@ tap.test('actions without context arg should still work (backward compat)', asyn
|
||||
});
|
||||
|
||||
await part.dispatchAction(simpleAction, 42);
|
||||
expect(part.getState().value).toEqual(42);
|
||||
expect(getRequiredState(part).value).toEqual(42);
|
||||
});
|
||||
|
||||
tap.test('concurrent dispatches still serialize correctly with context feature', async () => {
|
||||
@@ -860,7 +868,7 @@ tap.test('concurrent dispatches still serialize correctly with context feature',
|
||||
const part = await state.getStatePart<{ count: number }>('concurrentWithContext', { count: 0 });
|
||||
|
||||
const increment = part.createAction<void>(async (sp) => {
|
||||
const current = sp.getState();
|
||||
const current = getRequiredState(sp);
|
||||
return { count: current.count + 1 };
|
||||
});
|
||||
|
||||
@@ -871,7 +879,7 @@ tap.test('concurrent dispatches still serialize correctly with context feature',
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
expect(part.getState().count).toEqual(10);
|
||||
expect(getRequiredState(part).count).toEqual(10);
|
||||
});
|
||||
|
||||
// ── distinctUntilChanged on selectors ──────────────────────────────────
|
||||
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
import { expect, tap } from '@push.rocks/tapbundle';
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as smartstate from '../ts/index.js';
|
||||
|
||||
type TMyStateParts = 'testStatePart';
|
||||
@@ -39,7 +39,7 @@ tap.test('should select something', async () => {
|
||||
tap.test('should dispatch a state action', async (tools) => {
|
||||
const done = tools.defer();
|
||||
const addFavourite = testStatePart.createAction<string>(async (statePart, payload) => {
|
||||
const currentState = statePart.getState();
|
||||
const currentState = statePart.getState()!;
|
||||
return {
|
||||
...currentState,
|
||||
currentFavorites: [...currentState.currentFavorites, payload],
|
||||
@@ -51,9 +51,9 @@ tap.test('should dispatch a state action', async (tools) => {
|
||||
})
|
||||
.then(() => {
|
||||
done.resolve();
|
||||
});
|
||||
});
|
||||
await testStatePart.dispatchAction(addFavourite, 'my favourite things');
|
||||
expect(testStatePart.getState().currentFavorites).toContain('my favourite things');
|
||||
expect(testStatePart.getState()!.currentFavorites).toContain('my favourite things');
|
||||
await done.promise;
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user