@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
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for OpenAPI → ResourceDefinition inference
|
|
3
|
-
*/
|
|
4
|
-
import { describe, test, expect } from 'bun:test';
|
|
5
|
-
import { inferFromOpenAPI } from './inferencer-openapi';
|
|
6
|
-
|
|
7
|
-
const sampleOpenAPI = {
|
|
8
|
-
openapi: '3.0.0',
|
|
9
|
-
info: { title: 'Blog API', version: '1.0.0' },
|
|
10
|
-
paths: {
|
|
11
|
-
'/api/posts': {
|
|
12
|
-
get: { operationId: 'getPosts' },
|
|
13
|
-
post: { operationId: 'createPost' },
|
|
14
|
-
},
|
|
15
|
-
'/api/posts/{id}': {
|
|
16
|
-
get: { operationId: 'getPost' },
|
|
17
|
-
put: { operationId: 'updatePost' },
|
|
18
|
-
delete: { operationId: 'deletePost' },
|
|
19
|
-
},
|
|
20
|
-
'/api/users': {
|
|
21
|
-
get: { operationId: 'getUsers' },
|
|
22
|
-
},
|
|
23
|
-
},
|
|
24
|
-
components: {
|
|
25
|
-
schemas: {
|
|
26
|
-
Post: {
|
|
27
|
-
type: 'object',
|
|
28
|
-
required: ['title', 'content'],
|
|
29
|
-
properties: {
|
|
30
|
-
id: { type: 'integer' },
|
|
31
|
-
title: { type: 'string' },
|
|
32
|
-
content: { type: 'string', description: 'Post body content' },
|
|
33
|
-
status: { type: 'string', enum: ['draft', 'published', 'archived'] },
|
|
34
|
-
author_id: { type: 'integer' },
|
|
35
|
-
email: { type: 'string', format: 'email' },
|
|
36
|
-
created_at: { type: 'string', format: 'date-time' },
|
|
37
|
-
tags: { type: 'array', items: { type: 'string' } },
|
|
38
|
-
is_featured: { type: 'boolean' },
|
|
39
|
-
views: { type: 'integer' },
|
|
40
|
-
website: { type: 'string', format: 'uri' },
|
|
41
|
-
meta: { type: 'object' },
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
User: {
|
|
45
|
-
type: 'object',
|
|
46
|
-
properties: {
|
|
47
|
-
id: { type: 'integer' },
|
|
48
|
-
name: { type: 'string' },
|
|
49
|
-
avatar: { type: 'string', format: 'uri' },
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
describe('inferFromOpenAPI', () => {
|
|
57
|
-
test('parses both schemas', () => {
|
|
58
|
-
const resources = inferFromOpenAPI(sampleOpenAPI);
|
|
59
|
-
expect(resources.length).toBe(2);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test('resource name is pluralized and lowercase', () => {
|
|
63
|
-
const resources = inferFromOpenAPI(sampleOpenAPI);
|
|
64
|
-
const post = resources.find(r => r.name === 'posts');
|
|
65
|
-
expect(post).toBeDefined();
|
|
66
|
-
expect(post!.label).toBe('Post');
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test('detects required fields', () => {
|
|
70
|
-
const resources = inferFromOpenAPI(sampleOpenAPI);
|
|
71
|
-
const post = resources.find(r => r.name === 'posts')!;
|
|
72
|
-
const titleField = post.fields.find(f => f.key === 'title')!;
|
|
73
|
-
const viewsField = post.fields.find(f => f.key === 'views')!;
|
|
74
|
-
expect(titleField.required).toBe(true);
|
|
75
|
-
expect(viewsField.required).toBeFalsy();
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test('maps integer to number type', () => {
|
|
79
|
-
const resources = inferFromOpenAPI(sampleOpenAPI);
|
|
80
|
-
const post = resources.find(r => r.name === 'posts')!;
|
|
81
|
-
const viewsField = post.fields.find(f => f.key === 'views')!;
|
|
82
|
-
expect(viewsField.type).toBe('number');
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test('maps boolean type', () => {
|
|
86
|
-
const resources = inferFromOpenAPI(sampleOpenAPI);
|
|
87
|
-
const post = resources.find(r => r.name === 'posts')!;
|
|
88
|
-
const field = post.fields.find(f => f.key === 'is_featured')!;
|
|
89
|
-
expect(field.type).toBe('boolean');
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test('maps date-time format to date', () => {
|
|
93
|
-
const resources = inferFromOpenAPI(sampleOpenAPI);
|
|
94
|
-
const post = resources.find(r => r.name === 'posts')!;
|
|
95
|
-
const field = post.fields.find(f => f.key === 'created_at')!;
|
|
96
|
-
expect(field.type).toBe('date');
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
test('maps email format', () => {
|
|
100
|
-
const resources = inferFromOpenAPI(sampleOpenAPI);
|
|
101
|
-
const post = resources.find(r => r.name === 'posts')!;
|
|
102
|
-
const field = post.fields.find(f => f.key === 'email')!;
|
|
103
|
-
expect(field.type).toBe('email');
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
test('maps uri format to url', () => {
|
|
107
|
-
const resources = inferFromOpenAPI(sampleOpenAPI);
|
|
108
|
-
const post = resources.find(r => r.name === 'posts')!;
|
|
109
|
-
const field = post.fields.find(f => f.key === 'website')!;
|
|
110
|
-
expect(field.type).toBe('url');
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
test('maps enum to select with options', () => {
|
|
114
|
-
const resources = inferFromOpenAPI(sampleOpenAPI);
|
|
115
|
-
const post = resources.find(r => r.name === 'posts')!;
|
|
116
|
-
const field = post.fields.find(f => f.key === 'status')!;
|
|
117
|
-
expect(field.type).toBe('select');
|
|
118
|
-
expect(field.options!.length).toBe(3);
|
|
119
|
-
expect(field.options![0].value).toBe('draft');
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test('maps array of strings to tags', () => {
|
|
123
|
-
const resources = inferFromOpenAPI(sampleOpenAPI);
|
|
124
|
-
const post = resources.find(r => r.name === 'posts')!;
|
|
125
|
-
const field = post.fields.find(f => f.key === 'tags')!;
|
|
126
|
-
expect(field.type).toBe('tags');
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
test('maps object to json', () => {
|
|
130
|
-
const resources = inferFromOpenAPI(sampleOpenAPI);
|
|
131
|
-
const post = resources.find(r => r.name === 'posts')!;
|
|
132
|
-
const field = post.fields.find(f => f.key === 'meta')!;
|
|
133
|
-
expect(field.type).toBe('json');
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test('detects relation from _id suffix', () => {
|
|
137
|
-
const resources = inferFromOpenAPI(sampleOpenAPI);
|
|
138
|
-
const post = resources.find(r => r.name === 'posts')!;
|
|
139
|
-
const field = post.fields.find(f => f.key === 'author_id')!;
|
|
140
|
-
expect(field.type).toBe('relation');
|
|
141
|
-
expect(field.resource).toBe('authors');
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
test('detects CRUD capabilities from paths', () => {
|
|
145
|
-
const resources = inferFromOpenAPI(sampleOpenAPI);
|
|
146
|
-
const post = resources.find(r => r.name === 'posts')!;
|
|
147
|
-
expect(post.canCreate).toBe(true);
|
|
148
|
-
expect(post.canEdit).toBe(true);
|
|
149
|
-
expect(post.canDelete).toBe(true);
|
|
150
|
-
expect(post.canShow).toBe(true);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
test('read-only resource has limited capabilities', () => {
|
|
154
|
-
const resources = inferFromOpenAPI(sampleOpenAPI);
|
|
155
|
-
const user = resources.find(r => r.name === 'users')!;
|
|
156
|
-
expect(user.canShow).toBe(true);
|
|
157
|
-
expect(user.canCreate).toBe(false);
|
|
158
|
-
expect(user.canDelete).toBe(false);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
test('include filter works', () => {
|
|
162
|
-
const resources = inferFromOpenAPI(sampleOpenAPI, { include: ['Post'] });
|
|
163
|
-
expect(resources.length).toBe(1);
|
|
164
|
-
expect(resources[0].name).toBe('posts');
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
test('exclude filter works', () => {
|
|
168
|
-
const resources = inferFromOpenAPI(sampleOpenAPI, { exclude: ['User'] });
|
|
169
|
-
expect(resources.length).toBe(1);
|
|
170
|
-
expect(resources[0].name).toBe('posts');
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
test('content field is detected as textarea from key name hint', () => {
|
|
174
|
-
const resources = inferFromOpenAPI(sampleOpenAPI);
|
|
175
|
-
const post = resources.find(r => r.name === 'posts')!;
|
|
176
|
-
const field = post.fields.find(f => f.key === 'content')!;
|
|
177
|
-
expect(field.type).toBe('textarea');
|
|
178
|
-
expect(field.showInList).toBe(false);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
test('id field excluded from form', () => {
|
|
182
|
-
const resources = inferFromOpenAPI(sampleOpenAPI);
|
|
183
|
-
const post = resources.find(r => r.name === 'posts')!;
|
|
184
|
-
const idField = post.fields.find(f => f.key === 'id')!;
|
|
185
|
-
expect(idField.showInForm).toBe(false);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
test('empty spec returns empty array', () => {
|
|
189
|
-
const resources = inferFromOpenAPI({});
|
|
190
|
-
expect(resources.length).toBe(0);
|
|
191
|
-
});
|
|
192
|
-
});
|
package/src/inferencer.test.ts
DELETED
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
// Unit tests for Inferencer
|
|
2
|
-
import { describe, test, expect } from 'bun:test';
|
|
3
|
-
import { inferFieldType, inferResource } from './inferencer';
|
|
4
|
-
|
|
5
|
-
describe('inferFieldType', () => {
|
|
6
|
-
test('detects boolean', () => {
|
|
7
|
-
expect(inferFieldType('active', true)).toBe('boolean');
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
test('detects number', () => {
|
|
11
|
-
expect(inferFieldType('age', 25)).toBe('number');
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
test('detects email', () => {
|
|
15
|
-
expect(inferFieldType('email', 'user@example.com')).toBe('email');
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
test('detects email from key name', () => {
|
|
19
|
-
expect(inferFieldType('user_email', 'test')).toBe('email');
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test('detects URL', () => {
|
|
23
|
-
expect(inferFieldType('website', 'https://example.com')).toBe('url');
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test('detects image URL', () => {
|
|
27
|
-
expect(inferFieldType('avatar', 'https://example.com/photo.jpg')).toBe('image');
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test('detects image from key name', () => {
|
|
31
|
-
expect(inferFieldType('avatar', '/uploads/user.png')).toBe('image');
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test('detects date ISO string', () => {
|
|
35
|
-
expect(inferFieldType('created', '2024-01-15T10:30:00Z')).toBe('date');
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test('detects date from key name', () => {
|
|
39
|
-
expect(inferFieldType('created_at', 'some value')).toBe('date');
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test('detects phone', () => {
|
|
43
|
-
expect(inferFieldType('phone', '+1 (555) 123-4567')).toBe('phone');
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test('detects color hex', () => {
|
|
47
|
-
expect(inferFieldType('color', '#ff5533')).toBe('color');
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test('detects textarea for long strings', () => {
|
|
51
|
-
expect(inferFieldType('content', 'a'.repeat(250))).toBe('textarea');
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test('detects textarea from key name', () => {
|
|
55
|
-
expect(inferFieldType('description', 'short')).toBe('textarea');
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test('detects tags array', () => {
|
|
59
|
-
expect(inferFieldType('tags', ['svelte', 'admin'])).toBe('tags');
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test('detects images array', () => {
|
|
63
|
-
expect(inferFieldType('photos', ['https://x.com/a.png', 'https://x.com/b.jpg'])).toBe('images');
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test('detects json object', () => {
|
|
67
|
-
expect(inferFieldType('meta', { foo: 'bar' })).toBe('json');
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test('null defaults to text', () => {
|
|
71
|
-
expect(inferFieldType('unknown', null)).toBe('text');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test('plain string defaults to text', () => {
|
|
75
|
-
expect(inferFieldType('title', 'Hello World')).toBe('text');
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
describe('inferResource', () => {
|
|
80
|
-
const sampleData = [
|
|
81
|
-
{
|
|
82
|
-
id: 1,
|
|
83
|
-
title: 'First Post',
|
|
84
|
-
content: 'A very long description that is more than two hundred characters. '.repeat(4),
|
|
85
|
-
author_id: 5,
|
|
86
|
-
status: 'published',
|
|
87
|
-
views: 100,
|
|
88
|
-
is_featured: true,
|
|
89
|
-
created_at: '2024-01-15T10:00:00Z',
|
|
90
|
-
email: 'author@blog.com',
|
|
91
|
-
tags: ['svelte', 'admin'],
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
id: 2,
|
|
95
|
-
title: 'Second Post',
|
|
96
|
-
content: 'Another long body text that exceeds the 200 char limit easily. '.repeat(5),
|
|
97
|
-
author_id: 3,
|
|
98
|
-
status: 'draft',
|
|
99
|
-
views: 42,
|
|
100
|
-
is_featured: false,
|
|
101
|
-
created_at: '2024-02-20T14:30:00Z',
|
|
102
|
-
email: 'editor@blog.com',
|
|
103
|
-
tags: ['react'],
|
|
104
|
-
},
|
|
105
|
-
];
|
|
106
|
-
|
|
107
|
-
test('returns correct resource name and label', () => {
|
|
108
|
-
const result = inferResource('posts', sampleData);
|
|
109
|
-
expect(result.resource.name).toBe('posts');
|
|
110
|
-
expect(result.resource.label).toBe('Posts');
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
test('infers correct number of fields', () => {
|
|
114
|
-
const result = inferResource('posts', sampleData);
|
|
115
|
-
expect(result.fields.length).toBe(10); // id, title, content, author_id, status, views, is_featured, created_at, email, tags
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
test('detects id as primary key', () => {
|
|
119
|
-
const result = inferResource('posts', sampleData);
|
|
120
|
-
const idField = result.fields.find(f => f.key === 'id');
|
|
121
|
-
expect(idField).toBeDefined();
|
|
122
|
-
expect(idField!.showInForm).toBe(false);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test('detects number type for views', () => {
|
|
126
|
-
const result = inferResource('posts', sampleData);
|
|
127
|
-
const viewsField = result.fields.find(f => f.key === 'views');
|
|
128
|
-
expect(viewsField!.type).toBe('number');
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
test('detects boolean type for is_featured', () => {
|
|
132
|
-
const result = inferResource('posts', sampleData);
|
|
133
|
-
const field = result.fields.find(f => f.key === 'is_featured');
|
|
134
|
-
expect(field!.type).toBe('boolean');
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test('detects date for created_at', () => {
|
|
138
|
-
const result = inferResource('posts', sampleData);
|
|
139
|
-
const field = result.fields.find(f => f.key === 'created_at');
|
|
140
|
-
expect(field!.type).toBe('date');
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
test('detects relation for author_id', () => {
|
|
144
|
-
const result = inferResource('posts', sampleData);
|
|
145
|
-
const field = result.fields.find(f => f.key === 'author_id');
|
|
146
|
-
expect(field!.type).toBe('relation');
|
|
147
|
-
expect(field!.resource).toBe('authors');
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
test('detects email type', () => {
|
|
151
|
-
const result = inferResource('posts', sampleData);
|
|
152
|
-
const field = result.fields.find(f => f.key === 'email');
|
|
153
|
-
expect(field!.type).toBe('email');
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
test('detects tags array', () => {
|
|
157
|
-
const result = inferResource('posts', sampleData);
|
|
158
|
-
const field = result.fields.find(f => f.key === 'tags');
|
|
159
|
-
expect(field!.type).toBe('tags');
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
test('detects textarea for content', () => {
|
|
163
|
-
const result = inferResource('posts', sampleData);
|
|
164
|
-
const field = result.fields.find(f => f.key === 'content');
|
|
165
|
-
expect(field!.type).toBe('textarea');
|
|
166
|
-
expect(field!.showInList).toBe(false);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
test('generates valid TypeScript code', () => {
|
|
170
|
-
const result = inferResource('posts', sampleData);
|
|
171
|
-
expect(result.code).toContain("name: 'posts'");
|
|
172
|
-
expect(result.code).toContain("import type { ResourceDefinition }");
|
|
173
|
-
expect(result.code).toContain("export const postsResource");
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
test('handles empty data gracefully', () => {
|
|
177
|
-
const result = inferResource('empty', []);
|
|
178
|
-
expect(result.fields.length).toBe(0);
|
|
179
|
-
expect(result.code).toContain('No data available');
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
test('custom primaryKey', () => {
|
|
183
|
-
const data = [{ _id: 'abc', name: 'Test' }];
|
|
184
|
-
const result = inferResource('items', data, { primaryKey: '_id' });
|
|
185
|
-
expect(result.resource.primaryKey).toBe('_id');
|
|
186
|
-
const pkField = result.fields.find(f => f.key === '_id');
|
|
187
|
-
expect(pkField!.showInForm).toBe(false);
|
|
188
|
-
});
|
|
189
|
-
});
|
package/src/live.test.ts
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LiveProvider unit tests
|
|
3
|
-
* Tests the subscription management logic without requiring actual WebSocket/SSE connections.
|
|
4
|
-
*/
|
|
5
|
-
import { describe, test, expect, mock } from 'bun:test';
|
|
6
|
-
import type { LiveProvider, LiveEvent } from './live.svelte';
|
|
7
|
-
|
|
8
|
-
// Helper: create a minimal in-memory LiveProvider for testing subscription logic
|
|
9
|
-
function createMockLiveProvider(): LiveProvider & { emit: (event: LiveEvent) => void } {
|
|
10
|
-
type Callback = (event: LiveEvent) => void;
|
|
11
|
-
const subscribers = new Map<string, Set<Callback>>();
|
|
12
|
-
|
|
13
|
-
return {
|
|
14
|
-
subscribe({ resource, callback }) {
|
|
15
|
-
if (!subscribers.has(resource)) {
|
|
16
|
-
subscribers.set(resource, new Set());
|
|
17
|
-
}
|
|
18
|
-
subscribers.get(resource)!.add(callback);
|
|
19
|
-
|
|
20
|
-
return () => {
|
|
21
|
-
const callbacks = subscribers.get(resource);
|
|
22
|
-
if (callbacks) {
|
|
23
|
-
callbacks.delete(callback);
|
|
24
|
-
if (callbacks.size === 0) subscribers.delete(resource);
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
|
-
},
|
|
28
|
-
|
|
29
|
-
emit(event: LiveEvent) {
|
|
30
|
-
const callbacks = subscribers.get(event.resource);
|
|
31
|
-
if (callbacks) {
|
|
32
|
-
for (const cb of callbacks) cb(event);
|
|
33
|
-
}
|
|
34
|
-
// Wildcard
|
|
35
|
-
const wildcardCallbacks = subscribers.get('*');
|
|
36
|
-
if (wildcardCallbacks) {
|
|
37
|
-
for (const cb of wildcardCallbacks) cb(event);
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
describe('LiveProvider subscription', () => {
|
|
44
|
-
test('subscribe receives events for correct resource', () => {
|
|
45
|
-
const provider = createMockLiveProvider();
|
|
46
|
-
const received: LiveEvent[] = [];
|
|
47
|
-
|
|
48
|
-
provider.subscribe({
|
|
49
|
-
resource: 'posts',
|
|
50
|
-
callback: (event) => received.push(event),
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
provider.emit({ type: 'INSERT', resource: 'posts', payload: { id: 1 } });
|
|
54
|
-
provider.emit({ type: 'UPDATE', resource: 'comments', payload: { id: 2 } });
|
|
55
|
-
|
|
56
|
-
expect(received.length).toBe(1);
|
|
57
|
-
expect(received[0].type).toBe('INSERT');
|
|
58
|
-
expect(received[0].resource).toBe('posts');
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test('wildcard subscriber receives all events', () => {
|
|
62
|
-
const provider = createMockLiveProvider();
|
|
63
|
-
const received: LiveEvent[] = [];
|
|
64
|
-
|
|
65
|
-
provider.subscribe({
|
|
66
|
-
resource: '*',
|
|
67
|
-
callback: (event) => received.push(event),
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
provider.emit({ type: 'INSERT', resource: 'posts', payload: {} });
|
|
71
|
-
provider.emit({ type: 'DELETE', resource: 'users', payload: {} });
|
|
72
|
-
|
|
73
|
-
expect(received.length).toBe(2);
|
|
74
|
-
expect(received[0].resource).toBe('posts');
|
|
75
|
-
expect(received[1].resource).toBe('users');
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test('unsubscribe stops receiving events', () => {
|
|
79
|
-
const provider = createMockLiveProvider();
|
|
80
|
-
const received: LiveEvent[] = [];
|
|
81
|
-
|
|
82
|
-
const unsub = provider.subscribe({
|
|
83
|
-
resource: 'posts',
|
|
84
|
-
callback: (event) => received.push(event),
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
provider.emit({ type: 'INSERT', resource: 'posts', payload: {} });
|
|
88
|
-
expect(received.length).toBe(1);
|
|
89
|
-
|
|
90
|
-
unsub();
|
|
91
|
-
|
|
92
|
-
provider.emit({ type: 'UPDATE', resource: 'posts', payload: {} });
|
|
93
|
-
expect(received.length).toBe(1); // No new events after unsub
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test('multiple subscribers for same resource', () => {
|
|
97
|
-
const provider = createMockLiveProvider();
|
|
98
|
-
const received1: LiveEvent[] = [];
|
|
99
|
-
const received2: LiveEvent[] = [];
|
|
100
|
-
|
|
101
|
-
provider.subscribe({ resource: 'posts', callback: (e) => received1.push(e) });
|
|
102
|
-
provider.subscribe({ resource: 'posts', callback: (e) => received2.push(e) });
|
|
103
|
-
|
|
104
|
-
provider.emit({ type: 'INSERT', resource: 'posts', payload: { id: 1 } });
|
|
105
|
-
|
|
106
|
-
expect(received1.length).toBe(1);
|
|
107
|
-
expect(received2.length).toBe(1);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
test('LiveEvent types are correct', () => {
|
|
111
|
-
const insertEvent: LiveEvent = { type: 'INSERT', resource: 'posts', payload: { title: 'New' } };
|
|
112
|
-
const updateEvent: LiveEvent = { type: 'UPDATE', resource: 'posts', payload: { title: 'Updated' } };
|
|
113
|
-
const deleteEvent: LiveEvent = { type: 'DELETE', resource: 'posts', payload: { id: 1 } };
|
|
114
|
-
|
|
115
|
-
expect(insertEvent.type).toBe('INSERT');
|
|
116
|
-
expect(updateEvent.type).toBe('UPDATE');
|
|
117
|
-
expect(deleteEvent.type).toBe('DELETE');
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test('unsubscribing one does not affect others', () => {
|
|
121
|
-
const provider = createMockLiveProvider();
|
|
122
|
-
const received1: LiveEvent[] = [];
|
|
123
|
-
const received2: LiveEvent[] = [];
|
|
124
|
-
|
|
125
|
-
const unsub1 = provider.subscribe({ resource: 'posts', callback: (e) => received1.push(e) });
|
|
126
|
-
provider.subscribe({ resource: 'posts', callback: (e) => received2.push(e) });
|
|
127
|
-
|
|
128
|
-
unsub1();
|
|
129
|
-
provider.emit({ type: 'INSERT', resource: 'posts', payload: {} });
|
|
130
|
-
|
|
131
|
-
expect(received1.length).toBe(0);
|
|
132
|
-
expect(received2.length).toBe(1);
|
|
133
|
-
});
|
|
134
|
-
});
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { useCreate, useUpdate, invalidateByScopes } from './mutation-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
|
-
create: vi.fn().mockResolvedValue({ data: { id: 1 } }),
|
|
11
|
-
update: vi.fn().mockResolvedValue({ data: { id: 1 } })
|
|
12
|
-
}),
|
|
13
|
-
useResource: () => ({ name: 'posts' }),
|
|
14
|
-
getResource: (name: string) => ({ name, primaryKey: 'id' }),
|
|
15
|
-
useRouterContext: () => ({ navigate: vi.fn() }),
|
|
16
|
-
useTranslate: () => (key: string) => key,
|
|
17
|
-
useNotification: () => ({ open: vi.fn(), close: vi.fn() }),
|
|
18
|
-
getLiveProvider: () => undefined
|
|
19
|
-
}));
|
|
20
|
-
|
|
21
|
-
vi.mock('@tanstack/svelte-query', async (importOriginal) => {
|
|
22
|
-
const actual = await importOriginal();
|
|
23
|
-
return {
|
|
24
|
-
...actual as any,
|
|
25
|
-
useQueryClient: () => new QueryClient(),
|
|
26
|
-
createMutation: (opts: any) => ({
|
|
27
|
-
mutate: vi.fn((vars, callbacks) => {
|
|
28
|
-
if (opts.mutationFn) opts.mutationFn(vars);
|
|
29
|
-
if (callbacks?.onSuccess) callbacks.onSuccess({ data: {} });
|
|
30
|
-
}),
|
|
31
|
-
isPending: false
|
|
32
|
-
})
|
|
33
|
-
};
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
describe('useCreate & useUpdate - Headless Svelte 5 Compatibility', () => {
|
|
37
|
-
|
|
38
|
-
it('safely binds useCreate and allows mutations over proxies', () => {
|
|
39
|
-
let createMutation: ReturnType<typeof useCreate>;
|
|
40
|
-
|
|
41
|
-
const cleanup = $effect.root(() => {
|
|
42
|
-
createMutation = useCreate();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
flushSync();
|
|
46
|
-
|
|
47
|
-
expect(createMutation!.mutation.mutate).toBeDefined();
|
|
48
|
-
expect(createMutation!.mutation.isPending).toBe(false);
|
|
49
|
-
|
|
50
|
-
cleanup();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('safely binds useUpdate and evaluates data updates', () => {
|
|
54
|
-
let updateMutation: ReturnType<typeof useUpdate>;
|
|
55
|
-
|
|
56
|
-
const cleanup = $effect.root(() => {
|
|
57
|
-
updateMutation = useUpdate();
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
flushSync();
|
|
61
|
-
|
|
62
|
-
expect(updateMutation!.mutation.mutate).toBeDefined();
|
|
63
|
-
expect(updateMutation!.mutation.isPending).toBe(false);
|
|
64
|
-
|
|
65
|
-
cleanup();
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
describe('mutation-hooks', () => {
|
|
69
|
-
|
|
70
|
-
describe('invalidateByScopes - Multi-Provider Cache Isolation', () => {
|
|
71
|
-
it('filters perfectly by dataProviderName when passed', () => {
|
|
72
|
-
const queryClient = new QueryClient();
|
|
73
|
-
queryClient.invalidateQueries = vi.fn(async () => {});
|
|
74
|
-
|
|
75
|
-
const resource = 'posts';
|
|
76
|
-
const providerName = 'secondaryProvider';
|
|
77
|
-
|
|
78
|
-
// Act
|
|
79
|
-
invalidateByScopes(queryClient, resource, ['list'], ['list'], undefined, providerName);
|
|
80
|
-
|
|
81
|
-
expect(queryClient.invalidateQueries).toHaveBeenCalled();
|
|
82
|
-
|
|
83
|
-
// Assert predicate captures DataProvider scopes
|
|
84
|
-
const calls = (queryClient.invalidateQueries as ReturnType<typeof vi.fn>).mock.calls;
|
|
85
|
-
|
|
86
|
-
// Using arbitrary mock objects simulating Query keys
|
|
87
|
-
const predicateFn = calls[0][0].predicate;
|
|
88
|
-
|
|
89
|
-
const shouldPass = predicateFn({ queryKey: ['secondaryProvider', 'posts', 'list'] });
|
|
90
|
-
const shouldFail = predicateFn({ queryKey: ['default', 'posts', 'list'] });
|
|
91
|
-
const shouldFailResource = predicateFn({ queryKey: ['secondaryProvider', 'users', 'list'] });
|
|
92
|
-
|
|
93
|
-
expect(shouldPass).toBe(true);
|
|
94
|
-
expect(shouldFail).toBe(false);
|
|
95
|
-
expect(shouldFailResource).toBe(false);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('falls back to all providers when NO provider is specified', () => {
|
|
99
|
-
const queryClient = new QueryClient();
|
|
100
|
-
queryClient.invalidateQueries = vi.fn(async () => {});
|
|
101
|
-
|
|
102
|
-
// Act without providerName
|
|
103
|
-
invalidateByScopes(queryClient, 'posts', ['list'], ['list']);
|
|
104
|
-
|
|
105
|
-
const predicateFn = (queryClient.invalidateQueries as ReturnType<typeof vi.fn>).mock.calls[0][0].predicate;
|
|
106
|
-
|
|
107
|
-
const pass1 = predicateFn({ queryKey: ['default', 'posts', 'list'] });
|
|
108
|
-
const pass2 = predicateFn({ queryKey: ['custom', 'posts', 'list'] });
|
|
109
|
-
expect(pass1).toBe(true);
|
|
110
|
-
expect(pass2).toBe(true);
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
});
|