@svadmin/core 0.25.2 → 0.25.4
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/package.json +7 -3
- package/src/README.md +0 -18
- package/src/auth-hooks.test.ts +0 -205
- package/src/chatProvider.test.ts +0 -388
- package/src/data-transfer.test.ts +0 -118
- package/src/form-hooks.test.svelte.ts +0 -83
- package/src/helpers.test.ts +0 -177
- package/src/http-error.test.ts +0 -85
- package/src/i18n.test.ts +0 -90
- package/src/inferencer-openapi.test.ts +0 -192
- package/src/inferencer.test.ts +0 -189
- package/src/live.test.ts +0 -134
- package/src/mutation-hooks.test.svelte.ts +0 -114
- package/src/permissions.test.ts +0 -136
- package/src/query-hooks.test.svelte.ts +0 -65
- package/src/reactive-hooks.test.svelte.ts +0 -26
- package/src/resource-types.test.ts +0 -79
- package/src/router-provider.test.ts +0 -82
- package/src/routing-hooks.test.svelte.ts +0 -55
- package/src/table-hooks.test.svelte.ts +0 -57
- package/src/task-hooks.test.svelte.ts +0 -129
package/src/permissions.test.ts
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from 'bun:test';
|
|
2
|
-
import type { CanParams, CanResult, Action, AccessControlProvider } from './permissions.svelte';
|
|
3
|
-
|
|
4
|
-
describe('permissions', () => {
|
|
5
|
-
test('canAccessAsync respects deny rule', async () => {
|
|
6
|
-
const provider: AccessControlProvider = {
|
|
7
|
-
can: async (p) => {
|
|
8
|
-
const { action } = p as CanParams;
|
|
9
|
-
if (action === 'delete') return { can: false, reason: 'Denied' };
|
|
10
|
-
return { can: true };
|
|
11
|
-
},
|
|
12
|
-
};
|
|
13
|
-
expect(await provider.can({ resource: 'posts', action: 'delete' })).toEqual({ can: false, reason: 'Denied' });
|
|
14
|
-
expect((await provider.can({ resource: 'posts', action: 'list' })) as CanResult).toEqual({ can: true });
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
test('canAccessAsync resolves with allowed status', async () => {
|
|
18
|
-
const provider: AccessControlProvider = {
|
|
19
|
-
can: async (params) => {
|
|
20
|
-
expect((params as CanParams).params?.published).toBe(true);
|
|
21
|
-
return { can: true };
|
|
22
|
-
},
|
|
23
|
-
};
|
|
24
|
-
const result = await provider.can({ resource: 'posts', action: 'edit', params: { published: true } });
|
|
25
|
-
expect((result as CanResult).can).toBe(true);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test('canAccessAsync resolves with denied status', async () => {
|
|
29
|
-
const provider: AccessControlProvider = {
|
|
30
|
-
can: async () => ({ can: false, reason: 'No permission' }),
|
|
31
|
-
};
|
|
32
|
-
const result = await provider.can({ resource: 'users', action: 'delete' });
|
|
33
|
-
expect((result as CanResult).can).toBe(false);
|
|
34
|
-
expect((result as CanResult).reason).toBe('No permission');
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test('resource-specific rules', async () => {
|
|
38
|
-
const provider: AccessControlProvider = {
|
|
39
|
-
can: async (p) => {
|
|
40
|
-
const { resource, action } = p as CanParams;
|
|
41
|
-
if (resource === 'users' && action === 'delete') return { can: false, reason: 'Cannot delete users' };
|
|
42
|
-
return { can: true };
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
|
-
expect((await provider.can({ resource: 'users', action: 'delete' }) as CanResult).can).toBe(false);
|
|
46
|
-
expect((await provider.can({ resource: 'users', action: 'list' }) as CanResult).can).toBe(true);
|
|
47
|
-
expect((await provider.can({ resource: 'posts', action: 'delete' }) as CanResult).can).toBe(true);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test('params.id is passed through', async () => {
|
|
51
|
-
const provider: AccessControlProvider = {
|
|
52
|
-
can: async (p) => {
|
|
53
|
-
const { params } = p as CanParams;
|
|
54
|
-
if (params?.id === 42) return { can: false, reason: 'Record locked' };
|
|
55
|
-
return { can: true };
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
|
-
const result = await provider.can({ resource: 'posts', action: 'edit', params: { id: 42 } });
|
|
59
|
-
expect((result as CanResult).can).toBe(false);
|
|
60
|
-
expect((result as CanResult).reason).toBe('Record locked');
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test('extended action types work', async () => {
|
|
64
|
-
const provider: AccessControlProvider = {
|
|
65
|
-
can: async (p) => {
|
|
66
|
-
const { action } = p as CanParams;
|
|
67
|
-
if (action === 'show') return { can: false, reason: 'No show' };
|
|
68
|
-
if (action === 'field') return { can: false, reason: 'No field' };
|
|
69
|
-
return { can: true };
|
|
70
|
-
},
|
|
71
|
-
};
|
|
72
|
-
expect(((await provider.can({ resource: 'posts', action: 'show' })) as CanResult).can).toBe(false);
|
|
73
|
-
expect(((await provider.can({ resource: 'posts', action: 'field' })) as CanResult).can).toBe(false);
|
|
74
|
-
expect(((await provider.can({ resource: 'posts', action: 'list' })) as CanResult).can).toBe(true);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test('getAccessControlOptions returns provider options', () => {
|
|
78
|
-
const provider: AccessControlProvider = {
|
|
79
|
-
can: async () => ({ can: true }),
|
|
80
|
-
options: {
|
|
81
|
-
buttons: { enableAccessControl: true, hideIfUnauthorized: true },
|
|
82
|
-
},
|
|
83
|
-
};
|
|
84
|
-
expect(provider.options?.buttons?.enableAccessControl).toBe(true);
|
|
85
|
-
expect(provider.options?.buttons?.hideIfUnauthorized).toBe(true);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test('getAccessControlOptions returns {} when no options', () => {
|
|
89
|
-
const provider: AccessControlProvider = {
|
|
90
|
-
can: async () => ({ can: true }),
|
|
91
|
-
};
|
|
92
|
-
expect(provider.options).toBeUndefined();
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
describe('CASL adapter', () => {
|
|
97
|
-
test('createCaslAccessControl works', async () => {
|
|
98
|
-
const { createCaslAccessControl } = await import('./adapters/casl');
|
|
99
|
-
|
|
100
|
-
const ability = {
|
|
101
|
-
can: (action: string, subject: string) => !(action === 'delete' && subject === 'users'),
|
|
102
|
-
cannot: (action: string, subject: string) => action === 'delete' && subject === 'users',
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
const provider = createCaslAccessControl(ability);
|
|
106
|
-
|
|
107
|
-
const allowed = await provider.can({ resource: 'posts', action: 'edit' }) as CanResult;
|
|
108
|
-
expect(allowed.can).toBe(true);
|
|
109
|
-
|
|
110
|
-
const denied = await provider.can({ resource: 'users', action: 'delete' }) as CanResult;
|
|
111
|
-
expect(denied.can).toBe(false);
|
|
112
|
-
expect(denied.reason).toContain('Cannot "delete" on "users"');
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
describe('Casbin adapter', () => {
|
|
117
|
-
test('createCasbinAccessControl works', async () => {
|
|
118
|
-
const { createCasbinAccessControl } = await import('./adapters/casbin');
|
|
119
|
-
|
|
120
|
-
const enforcer = {
|
|
121
|
-
enforce: async (sub: string, obj: string, act: string) =>
|
|
122
|
-
!(sub === 'alice' && obj === 'users' && act === 'delete'),
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
const provider = createCasbinAccessControl(enforcer, {
|
|
126
|
-
getUser: () => 'alice',
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
const allowed = await provider.can({ resource: 'posts', action: 'edit' }) as CanResult;
|
|
130
|
-
expect(allowed.can).toBe(true);
|
|
131
|
-
|
|
132
|
-
const denied = await provider.can({ resource: 'users', action: 'delete' }) as CanResult;
|
|
133
|
-
expect(denied.can).toBe(false);
|
|
134
|
-
expect(denied.reason).toContain('alice');
|
|
135
|
-
});
|
|
136
|
-
});
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { useList, useOne } from './query-hooks.svelte';
|
|
3
|
-
import { flushSync } from 'svelte';
|
|
4
|
-
import { QueryClient } from '@tanstack/svelte-query';
|
|
5
|
-
|
|
6
|
-
vi.mock('./context.svelte', () => ({
|
|
7
|
-
useDataProvider: () => ({
|
|
8
|
-
getOne: vi.fn(),
|
|
9
|
-
getList: vi.fn()
|
|
10
|
-
}),
|
|
11
|
-
useResource: () => ({ name: 'posts' }),
|
|
12
|
-
getResource: (name: string) => ({ name, primaryKey: 'id' }),
|
|
13
|
-
useRouterContext: () => ({ navigate: vi.fn() }),
|
|
14
|
-
useTranslate: () => (key: string) => key,
|
|
15
|
-
useNotification: () => ({ open: vi.fn(), close: vi.fn() }),
|
|
16
|
-
getLiveProvider: () => undefined
|
|
17
|
-
}));
|
|
18
|
-
|
|
19
|
-
vi.mock('@tanstack/svelte-query', async (importOriginal) => {
|
|
20
|
-
const actual = await importOriginal();
|
|
21
|
-
return {
|
|
22
|
-
...actual as any,
|
|
23
|
-
useQueryClient: () => new QueryClient(),
|
|
24
|
-
createQuery: () => ({
|
|
25
|
-
data: { data: [{ id: 1, title: 'One' }], title: 'One' }, // Unified mock to please both array and direct object assertions
|
|
26
|
-
isPending: false,
|
|
27
|
-
isFetching: false,
|
|
28
|
-
error: null
|
|
29
|
-
})
|
|
30
|
-
};
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
describe('useList & useOne - Headless Svelte 5 Compatibility', () => {
|
|
34
|
-
|
|
35
|
-
it('safely binds useList query state without headless Svelte destruction', () => {
|
|
36
|
-
let listQuery: ReturnType<typeof useList>;
|
|
37
|
-
|
|
38
|
-
const cleanup = $effect.root(() => {
|
|
39
|
-
listQuery = useList({ resource: 'posts' });
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
flushSync();
|
|
43
|
-
|
|
44
|
-
expect(listQuery!.data).toBeDefined();
|
|
45
|
-
expect(listQuery!.data!.data[0].title).toBe('One');
|
|
46
|
-
expect(listQuery!.isPending).toBe(false);
|
|
47
|
-
|
|
48
|
-
cleanup();
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('safely binds useOne query state without headless component mounting', () => {
|
|
52
|
-
let oneQuery: ReturnType<typeof useOne>;
|
|
53
|
-
|
|
54
|
-
const cleanup = $effect.root(() => {
|
|
55
|
-
oneQuery = useOne({ resource: 'posts', id: 1 });
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
flushSync();
|
|
59
|
-
|
|
60
|
-
expect(oneQuery!.data).toBeDefined();
|
|
61
|
-
expect((oneQuery!.data!.data as any).title).toBe('One');
|
|
62
|
-
|
|
63
|
-
cleanup();
|
|
64
|
-
});
|
|
65
|
-
});
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { useThemedLayoutContext } from './hooks.svelte';
|
|
3
|
-
|
|
4
|
-
describe('Svelte 5 Reactive Runes Compatibility', () => {
|
|
5
|
-
|
|
6
|
-
it('verifies that sidebarCollapsed is correctly synchronized out of component lifecycle', () => {
|
|
7
|
-
let observedValue = false;
|
|
8
|
-
let layout: ReturnType<typeof useThemedLayoutContext>;
|
|
9
|
-
|
|
10
|
-
const cleanup = $effect.root(() => {
|
|
11
|
-
layout = useThemedLayoutContext();
|
|
12
|
-
|
|
13
|
-
$effect(() => {
|
|
14
|
-
observedValue = layout.sidebarCollapsed;
|
|
15
|
-
});
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
expect(observedValue).toBe(false);
|
|
19
|
-
|
|
20
|
-
layout!.toggleSidebar();
|
|
21
|
-
|
|
22
|
-
expect(layout!.sidebarCollapsed).toBe(true);
|
|
23
|
-
|
|
24
|
-
cleanup();
|
|
25
|
-
});
|
|
26
|
-
});
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
// Resource Type Registry — type-level tests
|
|
2
|
-
import { describe, test, expect } from 'bun:test';
|
|
3
|
-
import type { ResourceTypeMap, KnownResources, InferData } from './types';
|
|
4
|
-
|
|
5
|
-
describe('Resource Type Registry', () => {
|
|
6
|
-
test('ResourceTypeMap is an empty interface by default', () => {
|
|
7
|
-
// When no declaration merging is used, keyof ResourceTypeMap = never
|
|
8
|
-
type Keys = keyof ResourceTypeMap;
|
|
9
|
-
// This compiles — verifying the interface exists and is empty
|
|
10
|
-
const _check: Keys extends never ? true : false = true;
|
|
11
|
-
expect(_check).toBe(true);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
test('KnownResources falls back to string when ResourceTypeMap is empty', () => {
|
|
15
|
-
// When ResourceTypeMap is empty, KnownResources should be string
|
|
16
|
-
const resource: KnownResources = 'anything';
|
|
17
|
-
expect(typeof resource).toBe('string');
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
test('InferData falls back to Record<string, unknown> for unknown resources', () => {
|
|
21
|
-
// For any resource not in ResourceTypeMap, InferData should return Record<string, unknown>
|
|
22
|
-
type Result = InferData<'nonexistent'>;
|
|
23
|
-
const data: Result = { arbitrary: 'value', number: 42 };
|
|
24
|
-
expect(data.arbitrary).toBe('value');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test('type utilities are structurally sound', () => {
|
|
28
|
-
// Simulate what happens with declaration merging by testing the conditional types
|
|
29
|
-
// When R extends keyof ResourceTypeMap (which is never when empty),
|
|
30
|
-
// InferData should return Record<string, unknown>
|
|
31
|
-
type TestInfer = InferData<'users'>;
|
|
32
|
-
const obj: TestInfer = { id: '1', name: 'test' };
|
|
33
|
-
expect(obj).toBeDefined();
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test('KnownResources accepts any string when ResourceTypeMap is empty', () => {
|
|
37
|
-
// This verifies backward compatibility — all strings are valid resource names
|
|
38
|
-
const resources: KnownResources[] = ['users', 'posts', 'categories', 'anything'];
|
|
39
|
-
expect(resources).toHaveLength(4);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
// ─── Declaration Merging Example (compile-time only) ──────────
|
|
44
|
-
// The following block verifies that declaration merging works correctly.
|
|
45
|
-
// It's wrapped in a function so it doesn't affect runtime.
|
|
46
|
-
|
|
47
|
-
function _typeTests() {
|
|
48
|
-
// Simulate declaration merging
|
|
49
|
-
interface TestResourceMap {
|
|
50
|
-
users: { id: string; name: string; email: string };
|
|
51
|
-
posts: { id: string; title: string; content: string };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Test conditional type behavior with a populated map
|
|
55
|
-
type TestKnown<M> = keyof M extends never ? string : Extract<keyof M, string>;
|
|
56
|
-
type TestInfer<R extends string, M> = R extends keyof M ? M[R] : Record<string, unknown>;
|
|
57
|
-
|
|
58
|
-
// When map is populated, resources are constrained
|
|
59
|
-
type Resources = TestKnown<TestResourceMap>;
|
|
60
|
-
const _r1: Resources = 'users';
|
|
61
|
-
const _r2: Resources = 'posts';
|
|
62
|
-
// @ts-expect-error — 'invalid' is not in TestResourceMap
|
|
63
|
-
const _r3: Resources = 'invalid';
|
|
64
|
-
|
|
65
|
-
// InferData resolves correctly
|
|
66
|
-
type UserData = TestInfer<'users', TestResourceMap>;
|
|
67
|
-
const _user: UserData = { id: '1', name: 'Alice', email: 'a@b.com' };
|
|
68
|
-
|
|
69
|
-
type PostData = TestInfer<'posts', TestResourceMap>;
|
|
70
|
-
const _post: PostData = { id: '1', title: 'Hello', content: 'World' };
|
|
71
|
-
|
|
72
|
-
// Unknown resource falls back
|
|
73
|
-
type UnknownData = TestInfer<'unknown', TestResourceMap>;
|
|
74
|
-
const _unknown: UnknownData = { any: 'thing' };
|
|
75
|
-
|
|
76
|
-
void _r1; void _r2; void _r3; void _user; void _post; void _unknown;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
void _typeTests;
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
// Unit tests for RouterProvider implementations
|
|
2
|
-
// Router provider tests need a DOM environment or a mock.
|
|
3
|
-
import { describe, test, expect, beforeEach } from 'bun:test';
|
|
4
|
-
import { createHashRouterProvider, createHistoryRouterProvider } from './router-provider';
|
|
5
|
-
|
|
6
|
-
describe('createHashRouterProvider (unit — no DOM)', () => {
|
|
7
|
-
|
|
8
|
-
test('returns provider with go, back, parse methods', () => {
|
|
9
|
-
const provider = createHashRouterProvider();
|
|
10
|
-
expect(typeof provider.go).toBe('function');
|
|
11
|
-
expect(typeof provider.back).toBe('function');
|
|
12
|
-
expect(typeof provider.parse).toBe('function');
|
|
13
|
-
});
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
describe('createHistoryRouterProvider (unit — no DOM)', () => {
|
|
17
|
-
test('returns provider with go, back, parse methods', () => {
|
|
18
|
-
const provider = createHistoryRouterProvider();
|
|
19
|
-
expect(typeof provider.go).toBe('function');
|
|
20
|
-
expect(typeof provider.back).toBe('function');
|
|
21
|
-
expect(typeof provider.parse).toBe('function');
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
test('accepts basePath argument', () => {
|
|
25
|
-
const provider = createHistoryRouterProvider('/admin');
|
|
26
|
-
expect(typeof provider.parse).toBe('function');
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
// DOM-dependent tests — mocked window if not exists
|
|
31
|
-
describe('createHashRouterProvider (with mock DOM)', () => {
|
|
32
|
-
beforeEach(() => {
|
|
33
|
-
if (typeof globalThis.window === 'undefined') {
|
|
34
|
-
let _hash = '';
|
|
35
|
-
(globalThis as any).window = {
|
|
36
|
-
location: {
|
|
37
|
-
get hash() { return _hash ? (_hash.startsWith('#') ? _hash : '#' + _hash) : ''; },
|
|
38
|
-
set hash(val) { _hash = val; }
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test('parse returns empty segments for root', () => {
|
|
45
|
-
window.location.hash = '';
|
|
46
|
-
const provider = createHashRouterProvider();
|
|
47
|
-
const parsed = provider.parse();
|
|
48
|
-
expect(parsed.pathname).toBe('/');
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test('parse extracts resource from hash', () => {
|
|
52
|
-
window.location.hash = '#/posts';
|
|
53
|
-
const provider = createHashRouterProvider();
|
|
54
|
-
const parsed = provider.parse();
|
|
55
|
-
expect(parsed.pathname).toBe('/posts');
|
|
56
|
-
expect(parsed.resource).toBe('posts');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test('parse extracts resource, action, and id', () => {
|
|
60
|
-
window.location.hash = '#/posts/edit/42';
|
|
61
|
-
const provider = createHashRouterProvider();
|
|
62
|
-
const parsed = provider.parse();
|
|
63
|
-
expect(parsed.resource).toBe('posts');
|
|
64
|
-
expect(parsed.action).toBe('edit');
|
|
65
|
-
expect(parsed.id).toBe('42');
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test('parse extracts query params from hash', () => {
|
|
69
|
-
window.location.hash = '#/posts?page=2&sort=name';
|
|
70
|
-
const provider = createHashRouterProvider();
|
|
71
|
-
const parsed = provider.parse();
|
|
72
|
-
expect(parsed.params.page).toBe('2');
|
|
73
|
-
expect(parsed.params.sort).toBe('name');
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test('go sets window.location.hash', () => {
|
|
77
|
-
const provider = createHashRouterProvider();
|
|
78
|
-
provider.go({ to: '/users' });
|
|
79
|
-
expect(window.location.hash).toBe('#/users');
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { useGo, useBack } from './routing-hooks.svelte';
|
|
3
|
-
import { useParsed } from './useParsed.svelte';
|
|
4
|
-
import { flushSync } from 'svelte';
|
|
5
|
-
|
|
6
|
-
vi.mock('./context.svelte', () => ({
|
|
7
|
-
useRouterContext: () => ({
|
|
8
|
-
navigate: vi.fn(),
|
|
9
|
-
url: '/posts/1/edit',
|
|
10
|
-
parsedParams: { resource: 'posts', action: 'edit', id: '1' },
|
|
11
|
-
go: vi.fn(),
|
|
12
|
-
back: vi.fn()
|
|
13
|
-
}),
|
|
14
|
-
getRouterProvider: () => undefined
|
|
15
|
-
}));
|
|
16
|
-
|
|
17
|
-
vi.mock('./useParsed.svelte', () => ({
|
|
18
|
-
useParsed: () => ({ resource: 'posts', action: 'edit', id: '1' })
|
|
19
|
-
}));
|
|
20
|
-
|
|
21
|
-
describe('routing-hooks - Headless Svelte 5 Compatibility', () => {
|
|
22
|
-
|
|
23
|
-
it('safely extracts parses from url and binds to runes Context', () => {
|
|
24
|
-
let parsedParams!: ReturnType<typeof useParsed>;
|
|
25
|
-
|
|
26
|
-
const cleanup = $effect.root(() => {
|
|
27
|
-
parsedParams = useParsed();
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
flushSync();
|
|
31
|
-
|
|
32
|
-
expect(parsedParams!.action).toBe('edit');
|
|
33
|
-
expect(parsedParams.resource).toBe('posts');
|
|
34
|
-
expect(parsedParams.id).toBe('1');
|
|
35
|
-
|
|
36
|
-
cleanup();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('useGo and useBack return functional closures out of Context', () => {
|
|
40
|
-
let go!: ReturnType<typeof useGo>;
|
|
41
|
-
let back!: ReturnType<typeof useBack>;
|
|
42
|
-
|
|
43
|
-
const cleanup = $effect.root(() => {
|
|
44
|
-
go = useGo();
|
|
45
|
-
back = useBack();
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
flushSync();
|
|
49
|
-
|
|
50
|
-
expect(typeof go).toBe('function');
|
|
51
|
-
expect(typeof back).toBe('function');
|
|
52
|
-
|
|
53
|
-
cleanup();
|
|
54
|
-
});
|
|
55
|
-
});
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { useTable } from './table-hooks.svelte';
|
|
3
|
-
import { flushSync } from 'svelte';
|
|
4
|
-
import { QueryClient } from '@tanstack/svelte-query';
|
|
5
|
-
|
|
6
|
-
vi.mock('./context.svelte', () => ({
|
|
7
|
-
useDataProvider: () => ({
|
|
8
|
-
getList: vi.fn().mockResolvedValue({ data: [], total: 0 })
|
|
9
|
-
}),
|
|
10
|
-
useResource: () => ({ name: 'posts' }),
|
|
11
|
-
getResource: (name: string) => ({ name, primaryKey: 'id' }),
|
|
12
|
-
useRouterContext: () => ({ navigate: vi.fn(), url: '/', parsedParams: { current: 1, pageSize: 10 } }),
|
|
13
|
-
useTranslate: () => (key: string) => key,
|
|
14
|
-
getSyncWithLocation: () => false,
|
|
15
|
-
useNotification: () => ({ open: vi.fn(), close: vi.fn() }),
|
|
16
|
-
getLiveProvider: () => undefined
|
|
17
|
-
}));
|
|
18
|
-
|
|
19
|
-
vi.mock('@tanstack/svelte-query', async (importOriginal) => {
|
|
20
|
-
const actual = await importOriginal();
|
|
21
|
-
return {
|
|
22
|
-
...actual as any,
|
|
23
|
-
useQueryClient: () => new QueryClient(),
|
|
24
|
-
createQuery: () => ({
|
|
25
|
-
data: { data: [{ id: 1, title: 'Row' }], total: 1 },
|
|
26
|
-
isPending: false,
|
|
27
|
-
isFetching: false,
|
|
28
|
-
error: null
|
|
29
|
-
})
|
|
30
|
-
};
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
describe('useTable - Headless Svelte 5 Compatibility', () => {
|
|
34
|
-
|
|
35
|
-
it('binds useTable logic internally and updates pagination flawlessly via $state', () => {
|
|
36
|
-
let table: ReturnType<typeof useTable>;
|
|
37
|
-
|
|
38
|
-
const cleanup = $effect.root(() => {
|
|
39
|
-
table = useTable({ resource: 'posts' });
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
flushSync();
|
|
43
|
-
|
|
44
|
-
expect(table!.query).toBeDefined();
|
|
45
|
-
expect(table!.query?.data?.data[0].title).toBe('Row');
|
|
46
|
-
expect(table!.current).toBe(1);
|
|
47
|
-
|
|
48
|
-
// Trigger mutable state
|
|
49
|
-
table!.setPage(2);
|
|
50
|
-
|
|
51
|
-
flushSync();
|
|
52
|
-
|
|
53
|
-
expect(table!.current).toBe(2);
|
|
54
|
-
|
|
55
|
-
cleanup();
|
|
56
|
-
});
|
|
57
|
-
});
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { flushSync } from 'svelte';
|
|
3
|
-
import { QueryClient } from '@tanstack/svelte-query';
|
|
4
|
-
import { useSubmitTask, useTask, useTaskList, useTaskSubscription } from './task-hooks.svelte';
|
|
5
|
-
|
|
6
|
-
const mockTaskProvider = {
|
|
7
|
-
submit: vi.fn(async () => ({
|
|
8
|
-
id: 'task-1',
|
|
9
|
-
wait: async () => ({ id: 'task-1', status: 'done' }),
|
|
10
|
-
})),
|
|
11
|
-
get: vi.fn(async (taskId: string) => ({ id: taskId, status: 'running' })),
|
|
12
|
-
list: vi.fn(async () => ({ data: [{ id: 'task-1', status: 'queued' }], total: 1 })),
|
|
13
|
-
listDlq: vi.fn(async () => ({ data: [{ id: 'task-2', status: 'failed' }], total: 1 })),
|
|
14
|
-
subscribe: vi.fn((_taskId: string, callback: (task: { id: string; status: string }) => void) => {
|
|
15
|
-
callback({ id: 'task-1', status: 'running' });
|
|
16
|
-
return () => {};
|
|
17
|
-
}),
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
vi.mock('./context.svelte', () => ({
|
|
21
|
-
getTaskProvider: () => mockTaskProvider,
|
|
22
|
-
getAuthProvider: () => null,
|
|
23
|
-
}));
|
|
24
|
-
|
|
25
|
-
vi.mock('./notification.svelte', () => ({
|
|
26
|
-
notify: vi.fn(),
|
|
27
|
-
}));
|
|
28
|
-
|
|
29
|
-
vi.mock('@tanstack/svelte-query', async (importOriginal) => {
|
|
30
|
-
const actual = await importOriginal();
|
|
31
|
-
return {
|
|
32
|
-
...actual as any,
|
|
33
|
-
useQueryClient: () => new QueryClient(),
|
|
34
|
-
createMutation: (factory: any) => {
|
|
35
|
-
const config = factory();
|
|
36
|
-
return {
|
|
37
|
-
mutateAsync: async (vars: unknown) => config.mutationFn(vars),
|
|
38
|
-
mutate: vi.fn(),
|
|
39
|
-
isPending: false,
|
|
40
|
-
};
|
|
41
|
-
},
|
|
42
|
-
createQuery: (factory: any) => {
|
|
43
|
-
const config = factory();
|
|
44
|
-
return {
|
|
45
|
-
data: config.queryKey[0] === 'taskList'
|
|
46
|
-
? { data: [{ id: 'task-1', status: 'queued' }], total: 1 }
|
|
47
|
-
: { id: 'task-1', status: 'running' },
|
|
48
|
-
isLoading: false,
|
|
49
|
-
isSuccess: true,
|
|
50
|
-
isError: false,
|
|
51
|
-
error: null,
|
|
52
|
-
dataUpdatedAt: Date.now(),
|
|
53
|
-
errorUpdatedAt: 0,
|
|
54
|
-
};
|
|
55
|
-
},
|
|
56
|
-
};
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
describe('task-hooks', () => {
|
|
60
|
-
it('useSubmitTask exposes a working mutation', async () => {
|
|
61
|
-
let submitTask: ReturnType<typeof useSubmitTask>;
|
|
62
|
-
|
|
63
|
-
const cleanup = $effect.root(() => {
|
|
64
|
-
submitTask = useSubmitTask();
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
flushSync();
|
|
68
|
-
|
|
69
|
-
const handle = await submitTask!.mutation.mutateAsync({
|
|
70
|
-
taskName: 'image.generate',
|
|
71
|
-
options: { body: { prompt: 'poster' } },
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
expect(handle.id).toBe('task-1');
|
|
75
|
-
expect(mockTaskProvider.submit).toHaveBeenCalledWith('image.generate', {
|
|
76
|
-
body: { prompt: 'poster' },
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
cleanup();
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('useTask returns query state for a single task', () => {
|
|
83
|
-
let taskQuery: ReturnType<typeof useTask>;
|
|
84
|
-
|
|
85
|
-
const cleanup = $effect.root(() => {
|
|
86
|
-
taskQuery = useTask({ taskId: 'task-1' });
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
flushSync();
|
|
90
|
-
|
|
91
|
-
expect(taskQuery!.data).toBeDefined();
|
|
92
|
-
expect((taskQuery!.data as any).status).toBe('running');
|
|
93
|
-
|
|
94
|
-
cleanup();
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('useTaskList returns task collection data', () => {
|
|
98
|
-
let taskListQuery: ReturnType<typeof useTaskList>;
|
|
99
|
-
|
|
100
|
-
const cleanup = $effect.root(() => {
|
|
101
|
-
taskListQuery = useTaskList();
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
flushSync();
|
|
105
|
-
|
|
106
|
-
expect(taskListQuery!.data).toBeDefined();
|
|
107
|
-
expect((taskListQuery!.data as any).data[0].id).toBe('task-1');
|
|
108
|
-
|
|
109
|
-
cleanup();
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('useTaskSubscription subscribes to task updates', () => {
|
|
113
|
-
const onTask = vi.fn();
|
|
114
|
-
|
|
115
|
-
const cleanup = $effect.root(() => {
|
|
116
|
-
useTaskSubscription({
|
|
117
|
-
taskId: 'task-1',
|
|
118
|
-
onTask,
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
flushSync();
|
|
123
|
-
|
|
124
|
-
expect(mockTaskProvider.subscribe).toHaveBeenCalledWith('task-1', expect.any(Function));
|
|
125
|
-
expect(onTask).toHaveBeenCalledWith({ id: 'task-1', status: 'running' });
|
|
126
|
-
|
|
127
|
-
cleanup();
|
|
128
|
-
});
|
|
129
|
-
});
|