Skip to content

Commit 2611a0a

Browse files
committed
✅ Add tests for application
1 parent d141ccb commit 2611a0a

11 files changed

Lines changed: 219 additions & 14 deletions

packages/application/src/ConfigurationRef.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,24 @@ export interface ConfigurationRef<Configuration> extends Ref<Configuration> {
66
*/
77
readonly initial: Configuration;
88

9+
/**
10+
* Get a specific configuration value by key
11+
*
12+
* @param key the configuration key
13+
*/
914
get<Key extends keyof Configuration>(key: Key): Configuration[Key];
15+
16+
/**
17+
* Update the configuration by merging the current configuration with the given patch
18+
*
19+
* @param patch the partial configuration to merge with the current configuration
20+
*/
1021
update(patch: Partial<Configuration>): void;
11-
modify(fn: (current: Configuration) => Configuration): void;
22+
23+
/**
24+
* Modify the configuration using a mapping function
25+
*
26+
* @param mapFn a function that takes the current configuration and returns a new configuration
27+
*/
28+
modify(mapFn: (current: Configuration) => Configuration): void;
1229
}

packages/application/src/Meta.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export interface Meta {
2+
readonly name: string;
3+
}

packages/application/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
export * from './ConfigurationRef.js';
2+
export * from './Meta.js';
23
export * from './State.js';
4+
export * from './StateKey.js';
35
export * from './useConfiguration.js';
46
export * from './useState.js';
57
export * from './useRef.js';
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { Ref } from '@w5s/core';
3+
import { useConfiguration } from './useConfiguration.js';
4+
5+
describe(useConfiguration, () => {
6+
it('should initialize configuration and expose initial', () => {
7+
const store = Ref({});
8+
const meta = { name: 'app' } as const;
9+
const initial = { mode: 'light', retries: 2 };
10+
11+
const configuration = useConfiguration(meta, initial, store);
12+
13+
expect(configuration.current).toEqual(initial);
14+
expect(configuration.initial).toEqual(initial);
15+
expect(store.current).toEqual({
16+
app: {
17+
configuration: initial,
18+
},
19+
});
20+
});
21+
22+
it('should support get, update, and modify', () => {
23+
const store = Ref({});
24+
const meta = { name: 'app' } as const;
25+
const initial = { mode: 'light', retries: 2 };
26+
27+
const configuration = useConfiguration(meta, initial, store);
28+
29+
expect(configuration.get('mode')).toBe('light');
30+
31+
const previous = configuration.current;
32+
configuration.update({ retries: 3 });
33+
expect(configuration.current).toEqual({ mode: 'light', retries: 3 });
34+
expect(configuration.current).not.toBe(previous);
35+
36+
configuration.modify((current) => ({ ...current, mode: 'dark' }));
37+
expect(configuration.current).toEqual({ mode: 'dark', retries: 3 });
38+
});
39+
40+
it('should not override existing configuration in store', () => {
41+
const store = Ref({
42+
app: {
43+
configuration: { mode: 'dark', retries: 5 },
44+
other: true,
45+
},
46+
});
47+
const meta = { name: 'app' } as const;
48+
const initial = { mode: 'light', retries: 1 };
49+
50+
const configuration = useConfiguration(meta, initial, store);
51+
52+
expect(configuration.current).toEqual({ mode: 'dark', retries: 5 });
53+
expect(store.current.app).toEqual({
54+
configuration: { mode: 'dark', retries: 5 },
55+
other: true,
56+
});
57+
});
58+
});

packages/application/src/useConfiguration.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ import { useRef } from './useRef.js';
33
import { useNamespace } from './useNamespace.js';
44
import type { Ref } from '@w5s/core';
55
import type { State } from './State.js';
6+
import type { Meta } from './Meta.js';
67

78
export function useConfiguration<Configuration>(
8-
name: string | { name: string },
9+
meta: Meta,
910
initial: Configuration,
1011
store?: Ref<State>,
1112
): ConfigurationRef<Configuration> {
12-
const namespace = useNamespace(name, store);
13+
const namespace = useNamespace(meta, store);
1314
const configuration = useRef(namespace, 'configuration', initial);
1415

1516
function modify(fn: (current: Configuration) => Configuration): void {
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { Ref } from '@w5s/core';
3+
import { useNamespace } from './useNamespace.js';
4+
5+
describe(useNamespace, () => {
6+
it('should create namespace in store when missing', () => {
7+
const store = Ref({});
8+
const meta = { name: 'app' } as const;
9+
10+
const namespace = useNamespace(meta, store);
11+
12+
expect(namespace.current).toEqual({});
13+
expect(store.current).toEqual({ app: {} });
14+
});
15+
16+
it('should return existing namespace from store', () => {
17+
const existing = { configuration: { mode: 'dark' }, state: { counter: 2 } };
18+
const store = Ref({ app: existing });
19+
const meta = { name: 'app' } as const;
20+
21+
const namespace = useNamespace(meta, store);
22+
23+
expect(namespace.current).toEqual(existing);
24+
expect(namespace.current).toBe(existing);
25+
});
26+
27+
it('should keep other namespaces intact when updating current namespace', () => {
28+
const store = Ref({ other: { state: { value: 1 } } });
29+
const meta = { name: 'app' } as const;
30+
31+
const namespace = useNamespace(meta, store);
32+
namespace.current = { configuration: { retries: 2 } };
33+
34+
expect(store.current).toEqual({
35+
other: { state: { value: 1 } },
36+
app: { configuration: { retries: 2 } },
37+
});
38+
});
39+
});

packages/application/src/useNamespace.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,28 @@ import { useRef } from './useRef.js';
22
import { useStorage } from '@w5s/global-storage';
33
import type { Ref } from '@w5s/core';
44
import type { State } from './State.js';
5+
import type { Meta } from './Meta.js';
56

7+
/**
8+
* Return a new `Ref` containing the namespace for the given `meta.name`.
9+
*
10+
* @example
11+
* ```typescript
12+
* const app = { name: 'my-app' };
13+
* const namespace = useNamespace(app);
14+
* namespace.current == {
15+
* ...namespace.current,
16+
* configuration: {},
17+
* state: {},
18+
* };
19+
* ```
20+
* @param meta the meta info containing the name
21+
* @param store
22+
*/
623
export function useNamespace(
7-
name: string | { name: string },
24+
meta: Meta,
825
store?: Ref<State>,
926
): Ref<State> {
1027
const initialState = Object.freeze({});
11-
const key = typeof name === 'string' ? name : name.name;
12-
return useRef(store == null ? useStorage(globalThis) : store, key, initialState);
28+
return useRef(store == null ? useStorage(globalThis) : store, meta.name, initialState);
1329
}

packages/application/src/useRef.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Ref } from '@w5s/core';
33
import { useStorage } from '@w5s/global-storage';
44
import { useRef } from './useRef.js';
55

6-
describe('useRef()', () => {
6+
describe(useRef, () => {
77
describe('+ Storage', () => {
88
const anyStorage = useStorage({});
99

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { Ref } from '@w5s/core';
3+
import { useState } from './useState.js';
4+
5+
describe(useState, () => {
6+
it('should initialize state for namespace and key', () => {
7+
const store = Ref({});
8+
const meta = { name: 'app' } as const;
9+
10+
const counter = useState(meta, 'counter', 1, store);
11+
12+
expect(counter.current).toBe(1);
13+
expect(store.current).toEqual({
14+
app: {
15+
state: {
16+
counter: 1,
17+
},
18+
},
19+
});
20+
});
21+
22+
it('should not override existing state value', () => {
23+
const store = Ref({
24+
app: {
25+
state: {
26+
counter: 5,
27+
},
28+
},
29+
});
30+
const meta = { name: 'app' } as const;
31+
32+
const counter = useState(meta, 'counter', 1, store);
33+
34+
expect(counter.current).toBe(5);
35+
expect(store.current.app.state).toEqual({ counter: 5 });
36+
});
37+
38+
it('should preserve other namespace data when adding state', () => {
39+
const store = Ref({
40+
app: {
41+
configuration: { mode: 'dark' },
42+
},
43+
});
44+
const meta = { name: 'app' } as const;
45+
46+
const counter = useState(meta, 'counter', 1, store);
47+
48+
expect(counter.current).toBe(1);
49+
expect(store.current).toEqual({
50+
app: {
51+
configuration: { mode: 'dark' },
52+
state: { counter: 1 },
53+
},
54+
});
55+
});
56+
});

packages/application/src/useState.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,24 @@ import { useNamespace } from './useNamespace.js';
33
import { useRef } from './useRef.js';
44
import type { State } from './State.js';
55
import type { StateKey } from './StateKey.js';
6+
import type { Meta } from './Meta.js';
67

7-
export function useState<T>(name: string | { name: string }, key: StateKey, initial: T, store?: Ref<State>): Ref<T> {
8-
const namespace = useNamespace(name, store);
8+
/**
9+
* Return a new `Ref` containing the state for the given `meta.name` and `key`.
10+
*
11+
* @example
12+
* ```typescript
13+
* const app = { name: 'my-app' };
14+
* const counterRef = useState(app, 'counter', 1);
15+
* counterRef.current += 1;
16+
* ```
17+
* @param meta the meta info containing the name
18+
* @param key the key of the state
19+
* @param initial the initial value of the state
20+
* @param store the store containing the state, default to global storage
21+
*/
22+
export function useState<T>(meta: Meta, key: StateKey, initial: T, store?: Ref<State>): Ref<T> {
23+
const namespace = useNamespace(meta, store);
924
const state = useRef(namespace, 'state', Object.freeze({}));
1025
return useRef(state, key, initial);
1126
}

0 commit comments

Comments
 (0)