@blac/core 2.0.0-rc-10 → 2.0.0-rn-12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -0
- package/dist/index.cjs.js +2 -2
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +40 -11
- package/dist/index.es.js +432 -364
- package/dist/index.es.js.map +1 -1
- package/package.json +1 -1
- package/src/Blac.ts +46 -7
- package/src/Bloc.ts +0 -49
- package/src/Cubit.ts +14 -0
- package/src/__tests__/Blac.config.test.ts +86 -0
- package/src/__tests__/props.test.ts +256 -0
- package/src/adapter/BlacAdapter.ts +73 -8
- package/src/events.ts +3 -0
- package/src/index.ts +2 -0
- package/src/types.ts +1 -10
- package/src/utils/shallowEqual.ts +40 -0
package/package.json
CHANGED
package/src/Blac.ts
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
// TODO: The 'any' types in this file are necessary for proper type inference in complex generic scenarios.
|
|
3
|
-
// Specifically:
|
|
4
|
-
// 1. BlocConstructor<any> in Maps - allows any bloc type to be stored while maintaining type safety in usage
|
|
5
|
-
// 2. Type assertions for _disposalState - private property access across inheritance hierarchy
|
|
6
|
-
// 3. Constructor argument types - enables flexible bloc instantiation patterns
|
|
7
|
-
// These 'any' usages are carefully controlled and don't compromise runtime type safety.
|
|
8
1
|
import { BlocBase, BlocInstanceId, BlocLifecycleState } from './BlocBase';
|
|
9
2
|
import {
|
|
10
3
|
BlocBaseAbstract,
|
|
@@ -20,6 +13,12 @@ import {
|
|
|
20
13
|
export interface BlacConfig {
|
|
21
14
|
/** Whether to expose the Blac instance globally */
|
|
22
15
|
exposeBlacInstance?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Whether to enable proxy dependency tracking for automatic re-render optimization.
|
|
18
|
+
* When false, state changes always cause re-renders unless dependencies are manually specified.
|
|
19
|
+
* Default: true
|
|
20
|
+
*/
|
|
21
|
+
proxyDependencyTracking?: boolean;
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
export interface GetBlocOptions<B extends BlocBase<unknown>> {
|
|
@@ -108,6 +107,46 @@ export class Blac {
|
|
|
108
107
|
static get getAllBlocs() {
|
|
109
108
|
return Blac.instance.getAllBlocs;
|
|
110
109
|
}
|
|
110
|
+
|
|
111
|
+
/** Private static configuration */
|
|
112
|
+
private static _config: BlacConfig = {
|
|
113
|
+
exposeBlacInstance: false,
|
|
114
|
+
proxyDependencyTracking: true,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/** Get current configuration */
|
|
118
|
+
static get config(): Readonly<BlacConfig> {
|
|
119
|
+
return { ...this._config };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Set or update Blac configuration
|
|
124
|
+
* @param config - Partial configuration to merge with existing config
|
|
125
|
+
* @throws Error if configuration is invalid
|
|
126
|
+
*/
|
|
127
|
+
static setConfig(config: Partial<BlacConfig>): void {
|
|
128
|
+
// Validate config
|
|
129
|
+
if (config.proxyDependencyTracking !== undefined &&
|
|
130
|
+
typeof config.proxyDependencyTracking !== 'boolean') {
|
|
131
|
+
throw new Error('BlacConfig.proxyDependencyTracking must be a boolean');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (config.exposeBlacInstance !== undefined &&
|
|
135
|
+
typeof config.exposeBlacInstance !== 'boolean') {
|
|
136
|
+
throw new Error('BlacConfig.exposeBlacInstance must be a boolean');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Merge with existing config
|
|
140
|
+
this._config = {
|
|
141
|
+
...this._config,
|
|
142
|
+
...config,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Log config update if logging is enabled
|
|
146
|
+
if (this.enableLog) {
|
|
147
|
+
this.instance.log('Blac config updated:', this._config);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
111
150
|
/** Map storing all registered bloc instances by their class name and ID */
|
|
112
151
|
blocInstanceMap: Map<string, BlocBase<unknown>> = new Map();
|
|
113
152
|
/** Map storing isolated bloc instances grouped by their constructor */
|
package/src/Bloc.ts
CHANGED
|
@@ -2,27 +2,13 @@ import { Blac } from './Blac';
|
|
|
2
2
|
import { BlocBase } from './BlocBase';
|
|
3
3
|
import { BlocEventConstraint } from './types';
|
|
4
4
|
|
|
5
|
-
// A should be the base type for all events this Bloc handles and must extend BlocEventConstraint
|
|
6
|
-
// to access action.constructor and ensure proper event structure.
|
|
7
|
-
// P is for props, changed from any to unknown.
|
|
8
5
|
export abstract class Bloc<
|
|
9
6
|
S, // State type
|
|
10
7
|
A extends BlocEventConstraint = BlocEventConstraint, // Base Action/Event type with proper constraints
|
|
11
8
|
P = unknown, // Props type
|
|
12
9
|
> extends BlocBase<S, P> {
|
|
13
|
-
// Stores handlers: Map<EventConstructor (subtype of A), HandlerFunction>
|
|
14
|
-
// The handler's event parameter will be correctly typed to the specific EventConstructor
|
|
15
|
-
// by the 'on' method's signature.
|
|
16
10
|
readonly eventHandlers: Map<
|
|
17
|
-
// Key: Constructor of a specific event E (where E extends A)
|
|
18
|
-
// TODO: 'any[]' is required for constructor arguments to allow flexible event instantiation.
|
|
19
|
-
// Using specific parameter types would break type inference for events with different
|
|
20
|
-
// constructor signatures. The 'any[]' enables polymorphic event handling while
|
|
21
|
-
// maintaining type safety through the generic constraint 'E extends A'.
|
|
22
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
11
|
new (...args: any[]) => A,
|
|
24
|
-
// Value: Handler function. 'event: A' is used here for the stored function type.
|
|
25
|
-
// The 'on' method ensures the specific handler (event: E) is correctly typed.
|
|
26
12
|
(event: A, emit: (newState: S) => void) => void | Promise<void>
|
|
27
13
|
> = new Map();
|
|
28
14
|
|
|
@@ -46,22 +32,14 @@ export abstract class Bloc<
|
|
|
46
32
|
* The 'event' parameter in the handler will be typed to the specific eventConstructor.
|
|
47
33
|
*/
|
|
48
34
|
protected on<E extends A>(
|
|
49
|
-
// TODO: 'any[]' is required for constructor arguments (see explanation above).
|
|
50
|
-
// This allows events with different constructor signatures to be handled uniformly.
|
|
51
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
52
35
|
eventConstructor: new (...args: any[]) => E,
|
|
53
36
|
handler: (event: E, emit: (newState: S) => void) => void | Promise<void>,
|
|
54
37
|
): void {
|
|
55
38
|
if (this.eventHandlers.has(eventConstructor)) {
|
|
56
|
-
// Using Blac.warn or a similar logging mechanism from BlocBase if available,
|
|
57
|
-
// otherwise console.warn. Assuming this._name and this._id are available from BlocBase.
|
|
58
39
|
Blac.warn(
|
|
59
40
|
`[Bloc ${this._name}:${String(this._id)}] Handler for event '${eventConstructor.name}' already registered. It will be overwritten.`,
|
|
60
41
|
);
|
|
61
42
|
}
|
|
62
|
-
// Cast the specific handler (event: E) to a more general (event: A) for storage.
|
|
63
|
-
// This is safe because E extends A. When the handler is called with an 'action' of type A,
|
|
64
|
-
// if it was originally registered for type E, 'action' must be an instance of E.
|
|
65
43
|
this.eventHandlers.set(
|
|
66
44
|
eventConstructor,
|
|
67
45
|
handler as (
|
|
@@ -77,10 +55,8 @@ export abstract class Bloc<
|
|
|
77
55
|
* @param action The action/event instance to be processed.
|
|
78
56
|
*/
|
|
79
57
|
public add = async (action: A): Promise<void> => {
|
|
80
|
-
// Add event to queue
|
|
81
58
|
this._eventQueue.push(action);
|
|
82
59
|
|
|
83
|
-
// If not already processing, start processing the queue
|
|
84
60
|
if (!this._isProcessingEvent) {
|
|
85
61
|
await this._processEventQueue();
|
|
86
62
|
}
|
|
@@ -91,7 +67,6 @@ export abstract class Bloc<
|
|
|
91
67
|
* Processes events from the queue sequentially
|
|
92
68
|
*/
|
|
93
69
|
private async _processEventQueue(): Promise<void> {
|
|
94
|
-
// Prevent concurrent processing
|
|
95
70
|
if (this._isProcessingEvent) {
|
|
96
71
|
return;
|
|
97
72
|
}
|
|
@@ -113,34 +88,18 @@ export abstract class Bloc<
|
|
|
113
88
|
* Processes a single event
|
|
114
89
|
*/
|
|
115
90
|
private async _processEvent(action: A): Promise<void> {
|
|
116
|
-
// Using 'any[]' for constructor arguments for broader compatibility.
|
|
117
|
-
// TODO: Type assertion required to cast action.constructor to proper event constructor type.
|
|
118
|
-
// JavaScript's constructor property returns 'Function', but we need the specific event
|
|
119
|
-
// constructor type to look up handlers. This is safe because we validate the action
|
|
120
|
-
// extends the BlocEventConstraint interface.
|
|
121
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
122
91
|
const eventConstructor = action.constructor as new (...args: any[]) => A;
|
|
123
92
|
const handler = this.eventHandlers.get(eventConstructor);
|
|
124
93
|
|
|
125
94
|
if (handler) {
|
|
126
|
-
// Define the 'emit' function that handlers will use to update state.
|
|
127
|
-
// It captures the current state ('this.state') right before each emission
|
|
128
|
-
// to provide the correct 'previousState' to _pushState.
|
|
129
95
|
const emit = (newState: S): void => {
|
|
130
96
|
const previousState = this.state; // State just before this specific emission
|
|
131
|
-
// The 'action' passed to _pushState is the original action that triggered the handler,
|
|
132
|
-
// providing context for the state change (e.g., for logging or plugins).
|
|
133
97
|
this._pushState(newState, previousState, action);
|
|
134
98
|
};
|
|
135
99
|
|
|
136
100
|
try {
|
|
137
|
-
// Await the handler in case it's an async function (e.g., performs API calls).
|
|
138
|
-
// The 'action' is passed to the handler, and due to the way 'on' is typed,
|
|
139
|
-
// the 'event' parameter within the handler function will be correctly
|
|
140
|
-
// typed to its specific class (e.g., LoadMyFeatureData).
|
|
141
101
|
await handler(action, emit);
|
|
142
102
|
} catch (error) {
|
|
143
|
-
// Enhanced error handling with better context
|
|
144
103
|
const constructorName =
|
|
145
104
|
(action.constructor as { name?: string }).name ||
|
|
146
105
|
'UnnamedConstructor';
|
|
@@ -160,19 +119,11 @@ export abstract class Bloc<
|
|
|
160
119
|
errorContext,
|
|
161
120
|
);
|
|
162
121
|
|
|
163
|
-
// TODO: Consider implementing error boundary pattern
|
|
164
|
-
// For now, we log and continue, but applications may want to:
|
|
165
|
-
// 1. Emit an error state
|
|
166
|
-
// 2. Re-throw the error
|
|
167
|
-
// 3. Call an error handler callback
|
|
168
|
-
|
|
169
|
-
// Optional: Re-throw for critical errors (can be configured)
|
|
170
122
|
if (error instanceof Error && error.name === 'CriticalError') {
|
|
171
123
|
throw error;
|
|
172
124
|
}
|
|
173
125
|
}
|
|
174
126
|
} else {
|
|
175
|
-
// Enhanced warning with more context
|
|
176
127
|
const constructorName =
|
|
177
128
|
(action.constructor as { name?: string }).name || 'UnnamedConstructor';
|
|
178
129
|
Blac.warn(
|
package/src/Cubit.ts
CHANGED
|
@@ -8,6 +8,20 @@ import { BlocBase } from './BlocBase';
|
|
|
8
8
|
* @template P - The type of parameters (optional, defaults to null)
|
|
9
9
|
*/
|
|
10
10
|
export abstract class Cubit<S, P = null> extends BlocBase<S, P> {
|
|
11
|
+
/**
|
|
12
|
+
* @internal
|
|
13
|
+
* Protected method for useBloc to call
|
|
14
|
+
*/
|
|
15
|
+
protected _updateProps(props: P): void {
|
|
16
|
+
const oldProps = this.props;
|
|
17
|
+
this.props = props;
|
|
18
|
+
this.onPropsChanged?.(oldProps as P | undefined, props);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Optional override for props handling
|
|
23
|
+
*/
|
|
24
|
+
protected onPropsChanged?(oldProps: P | undefined, newProps: P): void;
|
|
11
25
|
/**
|
|
12
26
|
* Updates the current state and notifies all observers of the change.
|
|
13
27
|
* If the new state is identical to the current state (using Object.is),
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { Blac, BlacConfig } from '../index';
|
|
3
|
+
|
|
4
|
+
describe('Blac.config', () => {
|
|
5
|
+
const originalConfig = { ...Blac.config };
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
// Reset to default config before each test
|
|
9
|
+
Blac.setConfig({
|
|
10
|
+
exposeBlacInstance: false,
|
|
11
|
+
proxyDependencyTracking: true,
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
// Restore original config after tests
|
|
17
|
+
Blac.setConfig(originalConfig);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('setConfig', () => {
|
|
21
|
+
it('should have default configuration', () => {
|
|
22
|
+
expect(Blac.config).toEqual({
|
|
23
|
+
exposeBlacInstance: false,
|
|
24
|
+
proxyDependencyTracking: true,
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should update configuration with partial config', () => {
|
|
29
|
+
Blac.setConfig({ proxyDependencyTracking: false });
|
|
30
|
+
|
|
31
|
+
expect(Blac.config).toEqual({
|
|
32
|
+
exposeBlacInstance: false,
|
|
33
|
+
proxyDependencyTracking: false,
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should merge configuration properly', () => {
|
|
38
|
+
Blac.setConfig({ exposeBlacInstance: true });
|
|
39
|
+
Blac.setConfig({ proxyDependencyTracking: false });
|
|
40
|
+
|
|
41
|
+
expect(Blac.config).toEqual({
|
|
42
|
+
exposeBlacInstance: true,
|
|
43
|
+
proxyDependencyTracking: false,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should throw error for invalid proxyDependencyTracking type', () => {
|
|
48
|
+
expect(() => {
|
|
49
|
+
Blac.setConfig({ proxyDependencyTracking: 'true' as any });
|
|
50
|
+
}).toThrow('BlacConfig.proxyDependencyTracking must be a boolean');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should throw error for invalid exposeBlacInstance type', () => {
|
|
54
|
+
expect(() => {
|
|
55
|
+
Blac.setConfig({ exposeBlacInstance: 1 as any });
|
|
56
|
+
}).toThrow('BlacConfig.exposeBlacInstance must be a boolean');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should return a copy of config, not the original', () => {
|
|
60
|
+
const config1 = Blac.config;
|
|
61
|
+
const config2 = Blac.config;
|
|
62
|
+
|
|
63
|
+
expect(config1).not.toBe(config2);
|
|
64
|
+
expect(config1).toEqual(config2);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should not allow direct modification of config', () => {
|
|
68
|
+
const config = Blac.config as any;
|
|
69
|
+
config.proxyDependencyTracking = false;
|
|
70
|
+
|
|
71
|
+
// Original config should remain unchanged
|
|
72
|
+
expect(Blac.config.proxyDependencyTracking).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('config type exports', () => {
|
|
77
|
+
it('should allow typed config usage', () => {
|
|
78
|
+
const testConfig: Partial<BlacConfig> = {
|
|
79
|
+
proxyDependencyTracking: false,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
Blac.setConfig(testConfig);
|
|
83
|
+
expect(Blac.config.proxyDependencyTracking).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { Bloc } from '../Bloc';
|
|
3
|
+
import { Cubit } from '../Cubit';
|
|
4
|
+
import { PropsUpdated } from '../events';
|
|
5
|
+
import { BlacAdapter } from '../adapter/BlacAdapter';
|
|
6
|
+
import { Blac } from '../Blac';
|
|
7
|
+
|
|
8
|
+
// Test types
|
|
9
|
+
interface TestProps {
|
|
10
|
+
query: string;
|
|
11
|
+
filters?: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface TestState {
|
|
15
|
+
data: string;
|
|
16
|
+
loading: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Test Cubit with props
|
|
20
|
+
class TestCubit extends Cubit<TestState, TestProps> {
|
|
21
|
+
onPropsChangedMock = vi.fn();
|
|
22
|
+
|
|
23
|
+
constructor() {
|
|
24
|
+
super({ data: '', loading: false });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
protected onPropsChanged(oldProps: TestProps | undefined, newProps: TestProps): void {
|
|
28
|
+
this.onPropsChangedMock(oldProps, newProps);
|
|
29
|
+
if (oldProps?.query !== newProps.query) {
|
|
30
|
+
this.emit({ ...this.state, data: `Query: ${newProps.query}` });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
loadData = () => {
|
|
35
|
+
const query = this.props?.query ?? 'default';
|
|
36
|
+
this.emit({ data: `Loaded: ${query}`, loading: false });
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Test Bloc with props
|
|
41
|
+
class TestBloc extends Bloc<TestState, PropsUpdated<TestProps>> {
|
|
42
|
+
constructor() {
|
|
43
|
+
super({ data: '', loading: false });
|
|
44
|
+
|
|
45
|
+
this.on(PropsUpdated<TestProps>, (event, emit) => {
|
|
46
|
+
emit({ ...this.state, data: `Props: ${event.props.query}` });
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('Props functionality', () => {
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
// Clear Blac instance between tests
|
|
54
|
+
Blac.resetInstance();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('PropsUpdated event', () => {
|
|
58
|
+
it('should create PropsUpdated event with correct props', () => {
|
|
59
|
+
const props = { query: 'test', filters: ['a', 'b'] };
|
|
60
|
+
const event = new PropsUpdated(props);
|
|
61
|
+
|
|
62
|
+
expect(event.props).toEqual(props);
|
|
63
|
+
expect(event.props).toBe(props); // Should be the same reference
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('Cubit props support', () => {
|
|
68
|
+
it('should support props getter', () => {
|
|
69
|
+
const cubit = new TestCubit();
|
|
70
|
+
expect(cubit.props).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should update props via _updateProps', () => {
|
|
74
|
+
const cubit = new TestCubit();
|
|
75
|
+
const props = { query: 'test' };
|
|
76
|
+
|
|
77
|
+
(cubit as any)._updateProps(props);
|
|
78
|
+
|
|
79
|
+
expect(cubit.props).toEqual(props);
|
|
80
|
+
expect(cubit.onPropsChangedMock).toHaveBeenCalledWith(null, props);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should emit state when props change', () => {
|
|
84
|
+
const cubit = new TestCubit();
|
|
85
|
+
const props1 = { query: 'test1' };
|
|
86
|
+
const props2 = { query: 'test2' };
|
|
87
|
+
|
|
88
|
+
(cubit as any)._updateProps(props1);
|
|
89
|
+
expect(cubit.state.data).toBe('Query: test1');
|
|
90
|
+
|
|
91
|
+
(cubit as any)._updateProps(props2);
|
|
92
|
+
expect(cubit.state.data).toBe('Query: test2');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should access props in methods', () => {
|
|
96
|
+
const cubit = new TestCubit();
|
|
97
|
+
|
|
98
|
+
cubit.loadData();
|
|
99
|
+
expect(cubit.state.data).toBe('Loaded: default');
|
|
100
|
+
|
|
101
|
+
(cubit as any)._updateProps({ query: 'custom' });
|
|
102
|
+
cubit.loadData();
|
|
103
|
+
expect(cubit.state.data).toBe('Loaded: custom');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('Bloc props support', () => {
|
|
108
|
+
it('should handle PropsUpdated events', async () => {
|
|
109
|
+
const bloc = new TestBloc();
|
|
110
|
+
const props = { query: 'search' };
|
|
111
|
+
|
|
112
|
+
await bloc.add(new PropsUpdated(props));
|
|
113
|
+
|
|
114
|
+
expect(bloc.state.data).toBe('Props: search');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should queue PropsUpdated events like any other event', async () => {
|
|
118
|
+
const bloc = new TestBloc();
|
|
119
|
+
|
|
120
|
+
await bloc.add(new PropsUpdated({ query: 'first' }));
|
|
121
|
+
await bloc.add(new PropsUpdated({ query: 'second' }));
|
|
122
|
+
|
|
123
|
+
expect(bloc.state.data).toBe('Props: second');
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('BlacAdapter props ownership', () => {
|
|
128
|
+
it('should allow first adapter to own props', () => {
|
|
129
|
+
const Constructor = TestCubit as any;
|
|
130
|
+
const adapter1 = new BlacAdapter(
|
|
131
|
+
{ componentRef: { current: {} }, blocConstructor: Constructor },
|
|
132
|
+
{ props: { query: 'test1' } }
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
expect(() => {
|
|
136
|
+
adapter1.updateProps({ query: 'test2' });
|
|
137
|
+
}).not.toThrow();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should prevent non-owner adapters from updating props', () => {
|
|
141
|
+
const warnSpy = vi.spyOn(Blac, 'warn').mockImplementation(() => {});
|
|
142
|
+
const Constructor = TestCubit as any;
|
|
143
|
+
|
|
144
|
+
// First adapter becomes owner
|
|
145
|
+
const adapter1 = new BlacAdapter(
|
|
146
|
+
{ componentRef: { current: {} }, blocConstructor: Constructor },
|
|
147
|
+
{ props: { query: 'test1' } }
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// Second adapter tries to update props
|
|
151
|
+
const adapter2 = new BlacAdapter(
|
|
152
|
+
{ componentRef: { current: {} }, blocConstructor: Constructor },
|
|
153
|
+
{}
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
adapter2.updateProps({ query: 'hijack' });
|
|
157
|
+
|
|
158
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
159
|
+
expect.stringContaining('non-owner adapter')
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
warnSpy.mockRestore();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should clear ownership on adapter unmount', () => {
|
|
166
|
+
const Constructor = TestCubit as any;
|
|
167
|
+
const adapter1 = new BlacAdapter(
|
|
168
|
+
{ componentRef: { current: {} }, blocConstructor: Constructor },
|
|
169
|
+
{ props: { query: 'test1' } }
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
adapter1.mount();
|
|
173
|
+
adapter1.unmount();
|
|
174
|
+
|
|
175
|
+
// New adapter should be able to take ownership
|
|
176
|
+
const adapter2 = new BlacAdapter(
|
|
177
|
+
{ componentRef: { current: {} }, blocConstructor: Constructor },
|
|
178
|
+
{ props: { query: 'test2' } }
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
expect(() => {
|
|
182
|
+
adapter2.updateProps({ query: 'test3' });
|
|
183
|
+
}).not.toThrow();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should not update props if they are shallowly equal', () => {
|
|
187
|
+
const cubit = new TestCubit();
|
|
188
|
+
const Constructor = TestCubit as any;
|
|
189
|
+
const adapter = new BlacAdapter(
|
|
190
|
+
{ componentRef: { current: {} }, blocConstructor: Constructor },
|
|
191
|
+
{}
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const props = { query: 'test' };
|
|
195
|
+
adapter.updateProps(props);
|
|
196
|
+
|
|
197
|
+
// Reset mock
|
|
198
|
+
cubit.onPropsChangedMock.mockClear();
|
|
199
|
+
|
|
200
|
+
// Update with same props
|
|
201
|
+
adapter.updateProps({ query: 'test' });
|
|
202
|
+
|
|
203
|
+
// onPropsChanged should not have been called
|
|
204
|
+
expect(cubit.onPropsChangedMock).not.toHaveBeenCalled();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should ignore props updates during disposal', () => {
|
|
208
|
+
const Constructor = TestCubit as any;
|
|
209
|
+
const adapter = new BlacAdapter(
|
|
210
|
+
{ componentRef: { current: {} }, blocConstructor: Constructor },
|
|
211
|
+
{ props: { query: 'test1' } }
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
// Force disposal state
|
|
215
|
+
(adapter.blocInstance as any)._disposalState = 'disposing';
|
|
216
|
+
|
|
217
|
+
expect(() => {
|
|
218
|
+
adapter.updateProps({ query: 'test2' });
|
|
219
|
+
}).not.toThrow();
|
|
220
|
+
|
|
221
|
+
// Props should not have been updated
|
|
222
|
+
expect((adapter.blocInstance as TestCubit).props?.query).toBe('test1');
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe('Props integration', () => {
|
|
227
|
+
it('should dispatch PropsUpdated for Bloc instances', async () => {
|
|
228
|
+
const Constructor = TestBloc as any;
|
|
229
|
+
const adapter = new BlacAdapter(
|
|
230
|
+
{ componentRef: { current: {} }, blocConstructor: Constructor },
|
|
231
|
+
{}
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const bloc = adapter.blocInstance as TestBloc;
|
|
235
|
+
adapter.updateProps({ query: 'adapter-test' });
|
|
236
|
+
|
|
237
|
+
// Wait for event processing
|
|
238
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
239
|
+
|
|
240
|
+
expect(bloc.state.data).toBe('Props: adapter-test');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should call _updateProps for Cubit instances', () => {
|
|
244
|
+
const Constructor = TestCubit as any;
|
|
245
|
+
const adapter = new BlacAdapter(
|
|
246
|
+
{ componentRef: { current: {} }, blocConstructor: Constructor },
|
|
247
|
+
{}
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
adapter.updateProps({ query: 'adapter-test' });
|
|
251
|
+
|
|
252
|
+
expect((adapter.blocInstance as TestCubit).props?.query).toBe('adapter-test');
|
|
253
|
+
expect((adapter.blocInstance as TestCubit).state.data).toBe('Query: adapter-test');
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
});
|