@svadmin/core 0.25.2 → 0.25.3

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.
@@ -1,118 +0,0 @@
1
- // Unit tests for CSV parsing and data-transfer utilities
2
- import { describe, test, expect } from 'bun:test';
3
-
4
- // We test parseCSVLine directly by re-implementing the exported logic
5
- // since the function is not exported. We test the CSV format contract.
6
-
7
- function parseCSVLine(line: string): string[] {
8
- const result: string[] = [];
9
- let current = '';
10
- let inQuotes = false;
11
-
12
- for (let i = 0; i < line.length; i++) {
13
- const ch = line[i];
14
- if (inQuotes) {
15
- if (ch === '"' && line[i + 1] === '"') {
16
- current += '"';
17
- i++;
18
- } else if (ch === '"') {
19
- inQuotes = false;
20
- } else {
21
- current += ch;
22
- }
23
- } else {
24
- if (ch === '"') {
25
- inQuotes = true;
26
- } else if (ch === ',') {
27
- result.push(current.trim());
28
- current = '';
29
- } else {
30
- current += ch;
31
- }
32
- }
33
- }
34
- result.push(current.trim());
35
- return result;
36
- }
37
-
38
- describe('parseCSVLine', () => {
39
- test('simple comma-separated values', () => {
40
- expect(parseCSVLine('a,b,c')).toEqual(['a', 'b', 'c']);
41
- });
42
-
43
- test('values with whitespace are trimmed', () => {
44
- expect(parseCSVLine(' hello , world , test ')).toEqual(['hello', 'world', 'test']);
45
- });
46
-
47
- test('quoted values preserve commas', () => {
48
- expect(parseCSVLine('"hello, world",b,c')).toEqual(['hello, world', 'b', 'c']);
49
- });
50
-
51
- test('escaped quotes inside quoted values', () => {
52
- expect(parseCSVLine('"say ""hello""",b')).toEqual(['say "hello"', 'b']);
53
- });
54
-
55
- test('empty values', () => {
56
- expect(parseCSVLine('a,,c')).toEqual(['a', '', 'c']);
57
- });
58
-
59
- test('single value', () => {
60
- expect(parseCSVLine('hello')).toEqual(['hello']);
61
- });
62
-
63
- test('empty line', () => {
64
- expect(parseCSVLine('')).toEqual(['']);
65
- });
66
-
67
- test('quoted value with newline character', () => {
68
- expect(parseCSVLine('"line1\nline2",b')).toEqual(['line1\nline2', 'b']);
69
- });
70
-
71
- test('mixed quoted and unquoted', () => {
72
- expect(parseCSVLine('1,"John Doe","admin@test.com",true')).toEqual([
73
- '1', 'John Doe', 'admin@test.com', 'true'
74
- ]);
75
- });
76
- });
77
-
78
- // CSV row formatting (escape logic from useExport)
79
- function escapeCSVField(val: unknown): string {
80
- const str = val === null || val === undefined ? '' : String(val);
81
- return str.includes(',') || str.includes('"') || str.includes('\n')
82
- ? `"${str.replace(/"/g, '""')}"`
83
- : str;
84
- }
85
-
86
- describe('escapeCSVField', () => {
87
- test('simple string passes through', () => {
88
- expect(escapeCSVField('hello')).toBe('hello');
89
- });
90
-
91
- test('null becomes empty string', () => {
92
- expect(escapeCSVField(null)).toBe('');
93
- });
94
-
95
- test('undefined becomes empty string', () => {
96
- expect(escapeCSVField(undefined)).toBe('');
97
- });
98
-
99
- test('string with comma is quoted', () => {
100
- expect(escapeCSVField('hello, world')).toBe('"hello, world"');
101
- });
102
-
103
- test('string with double quote is escaped', () => {
104
- expect(escapeCSVField('say "hi"')).toBe('"say ""hi"""');
105
- });
106
-
107
- test('string with newline is quoted', () => {
108
- expect(escapeCSVField('line1\nline2')).toBe('"line1\nline2"');
109
- });
110
-
111
- test('number is converted to string', () => {
112
- expect(escapeCSVField(42)).toBe('42');
113
- });
114
-
115
- test('boolean is converted to string', () => {
116
- expect(escapeCSVField(true)).toBe('true');
117
- });
118
- });
@@ -1,83 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { useForm } from './form-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
- create: vi.fn().mockResolvedValue({ data: { id: 1 } }),
10
- update: vi.fn().mockResolvedValue({ data: { id: 1 } })
11
- }),
12
- useResource: () => ({ name: 'posts' }),
13
- getResource: () => ({ name: 'posts', primaryKey: 'id' }),
14
- useRouterContext: () => ({ navigate: vi.fn() }),
15
- useTranslate: () => (key: string) => key,
16
- useNotification: () => ({ open: vi.fn(), close: vi.fn() })
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: () => ({ data: { data: { id: 1, title: 'Original' } }, isPending: false }),
25
- createMutation: (opts: any) => ({
26
- mutate: vi.fn((vars, callbacks) => {
27
- if (opts.mutationFn) opts.mutationFn(vars);
28
- if (callbacks?.onSuccess) callbacks.onSuccess({ data: {} });
29
- }),
30
- isPending: false
31
- })
32
- };
33
- });
34
-
35
- describe('useForm - Headless Svelte 5 Compatibility', () => {
36
-
37
- it('preserves reactive proxy data bindings safely across Svelte boundaries', () => {
38
- let form: ReturnType<typeof useForm>;
39
-
40
- const cleanup = $effect.root(() => {
41
- form = useForm({
42
- action: 'create',
43
- resource: 'posts'
44
- });
45
- });
46
-
47
- flushSync();
48
-
49
- // Properly use setFieldValue which guarantees state sync in svadmin forms
50
- form!.setFieldValue('title', 'Reactive Test');
51
-
52
- flushSync();
53
-
54
- // The internal $state proxy absorbs this mutation
55
- expect(form!.values.title).toBe('Reactive Test');
56
-
57
- cleanup();
58
- });
59
-
60
- it('handles autoSave queuing correctly across reactive boundaries', () => {
61
- let form: ReturnType<typeof useForm>;
62
-
63
- const cleanup = $effect.root(() => {
64
- form = useForm({
65
- action: 'edit',
66
- resource: 'posts',
67
- id: 1,
68
- autoSave: { enabled: true, debounce: 0 }
69
- });
70
- });
71
-
72
- flushSync();
73
-
74
- form!.setFieldValue('title', 'Saved Title');
75
-
76
- flushSync();
77
-
78
- expect(form!.mutation).toBeDefined();
79
- expect((form!.mutation as any).isPending).toBeDefined();
80
-
81
- cleanup();
82
- });
83
- });
@@ -1,177 +0,0 @@
1
- // Unit tests for helpers-pure.ts — pure functions, no Svelte runtime
2
- import { describe, test, expect } from 'bun:test';
3
- import {
4
- getDefaultFilter, getDefaultSortOrder,
5
- unionFilters, unionSorters,
6
- parseCSV,
7
- } from './helpers-pure';
8
- import type { Filter, Sort } from './types';
9
-
10
- // ─── getDefaultFilter ─────────────────────────────────────────
11
-
12
- describe('getDefaultFilter', () => {
13
- const filters: Filter[] = [
14
- { field: 'status', operator: 'eq', value: 'published' },
15
- { field: 'price', operator: 'gte', value: 100 },
16
- { field: 'title', operator: 'contains', value: 'hello' },
17
- ];
18
-
19
- test('returns value for matching field+operator', () => {
20
- expect(getDefaultFilter('status', filters)).toBe('published');
21
- expect(getDefaultFilter('price', filters, 'gte')).toBe(100);
22
- });
23
-
24
- test('returns undefined for non-matching field', () => {
25
- expect(getDefaultFilter('nonexistent', filters)).toBeUndefined();
26
- });
27
-
28
- test('returns undefined for wrong operator', () => {
29
- expect(getDefaultFilter('status', filters, 'contains')).toBeUndefined();
30
- });
31
-
32
- test('returns undefined for empty/undefined filters', () => {
33
- expect(getDefaultFilter('status', [])).toBeUndefined();
34
- expect(getDefaultFilter('status', undefined)).toBeUndefined();
35
- });
36
- });
37
-
38
- // ─── getDefaultSortOrder ──────────────────────────────────────
39
-
40
- describe('getDefaultSortOrder', () => {
41
- const sorters: Sort[] = [
42
- { field: 'name', order: 'asc' },
43
- { field: 'createdAt', order: 'desc' },
44
- ];
45
-
46
- test('returns order for matching field', () => {
47
- expect(getDefaultSortOrder('name', sorters)).toBe('asc');
48
- expect(getDefaultSortOrder('createdAt', sorters)).toBe('desc');
49
- });
50
-
51
- test('returns undefined for non-matching field', () => {
52
- expect(getDefaultSortOrder('unknown', sorters)).toBeUndefined();
53
- });
54
-
55
- test('returns undefined for empty/undefined sorters', () => {
56
- expect(getDefaultSortOrder('name', undefined)).toBeUndefined();
57
- expect(getDefaultSortOrder('name', [])).toBeUndefined();
58
- });
59
- });
60
-
61
- // ─── unionFilters ─────────────────────────────────────────────
62
-
63
- describe('unionFilters', () => {
64
- test('merges new filters over previous', () => {
65
- const prev: Filter[] = [
66
- { field: 'status', operator: 'eq', value: 'draft' },
67
- ];
68
- const next: Filter[] = [
69
- { field: 'status', operator: 'eq', value: 'published' },
70
- ];
71
- const result = unionFilters([], next, prev);
72
- expect(result).toHaveLength(1);
73
- expect(result[0].value).toBe('published');
74
- });
75
-
76
- test('permanent filters override everything', () => {
77
- const permanent: Filter[] = [
78
- { field: 'org', operator: 'eq', value: 'acme' },
79
- ];
80
- const next: Filter[] = [
81
- { field: 'org', operator: 'eq', value: 'other' },
82
- ];
83
- const result = unionFilters(permanent, next, []);
84
- expect(result).toHaveLength(1);
85
- expect(result[0].value).toBe('acme');
86
- });
87
-
88
- test('removes filters with undefined value', () => {
89
- const prev: Filter[] = [
90
- { field: 'status', operator: 'eq', value: 'draft' },
91
- ];
92
- const next: Filter[] = [
93
- { field: 'status', operator: 'eq', value: undefined },
94
- ];
95
- const result = unionFilters([], next, prev);
96
- expect(result).toHaveLength(0);
97
- });
98
-
99
- test('adds new filters not in previous', () => {
100
- const prev: Filter[] = [
101
- { field: 'status', operator: 'eq', value: 'draft' },
102
- ];
103
- const next: Filter[] = [
104
- { field: 'category', operator: 'eq', value: 'tech' },
105
- ];
106
- const result = unionFilters([], next, prev);
107
- expect(result).toHaveLength(2);
108
- });
109
-
110
- test('handles empty arrays', () => {
111
- expect(unionFilters([], [], [])).toEqual([]);
112
- });
113
- });
114
-
115
- // ─── unionSorters ─────────────────────────────────────────────
116
-
117
- describe('unionSorters', () => {
118
- test('new sorters take priority', () => {
119
- const permanent: Sort[] = [{ field: 'id', order: 'asc' }];
120
- const newSorters: Sort[] = [{ field: 'name', order: 'desc' }];
121
- const result = unionSorters(permanent, newSorters);
122
- expect(result).toHaveLength(2);
123
- expect(result[0].field).toBe('name');
124
- });
125
-
126
- test('permanent sorters override new sorters on same field', () => {
127
- const permanent: Sort[] = [{ field: 'name', order: 'asc' }];
128
- const newSorters: Sort[] = [{ field: 'name', order: 'desc' }];
129
- const result = unionSorters(permanent, newSorters);
130
- expect(result).toHaveLength(1);
131
- expect(result[0].order).toBe('asc');
132
- });
133
-
134
- test('empty arrays', () => {
135
- expect(unionSorters([], [])).toEqual([]);
136
- });
137
- });
138
-
139
- // ─── parseCSV ─────────────────────────────────────────────────
140
-
141
- describe('parseCSV', () => {
142
- test('parses simple CSV', () => {
143
- const rows = parseCSV('name,age\nAlice,30\nBob,25');
144
- expect(rows).toHaveLength(3);
145
- expect(rows[0]).toEqual(['name', 'age']);
146
- expect(rows[1]).toEqual(['Alice', '30']);
147
- expect(rows[2]).toEqual(['Bob', '25']);
148
- });
149
-
150
- test('handles quoted fields', () => {
151
- const rows = parseCSV('name,bio\nAlice,"Hello, World"');
152
- expect(rows).toHaveLength(2);
153
- expect(rows[1][1]).toBe('Hello, World');
154
- });
155
-
156
- test('handles escaped quotes', () => {
157
- const rows = parseCSV('val\n"He said ""hello"""');
158
- expect(rows).toHaveLength(2);
159
- expect(rows[1][0]).toBe('He said "hello"');
160
- });
161
-
162
- test('handles CRLF line endings', () => {
163
- const rows = parseCSV('a,b\r\n1,2\r\n3,4');
164
- expect(rows).toHaveLength(3);
165
- });
166
-
167
- test('skips empty rows', () => {
168
- const rows = parseCSV('a\n\nb\n');
169
- expect(rows).toHaveLength(2);
170
- expect(rows[0]).toEqual(['a']);
171
- expect(rows[1]).toEqual(['b']);
172
- });
173
-
174
- test('handles empty input', () => {
175
- expect(parseCSV('')).toEqual([]);
176
- });
177
- });
@@ -1,85 +0,0 @@
1
- // HttpError + CrudOperator + LogicalFilter tests
2
- import { describe, test, expect } from 'bun:test';
3
- import { HttpError } from './types';
4
- import type { CrudOperator, Filter, LogicalFilter } from './types';
5
-
6
- describe('HttpError', () => {
7
- test('creates with message and statusCode', () => {
8
- const err = new HttpError('Not Found', 404);
9
- expect(err.message).toBe('Not Found');
10
- expect(err.statusCode).toBe(404);
11
- expect(err.name).toBe('HttpError');
12
- expect(err.errors).toBeUndefined();
13
- });
14
-
15
- test('creates with validation errors', () => {
16
- const err = new HttpError('Validation Failed', 422, {
17
- email: ['Email is required', 'Email must be valid'],
18
- name: 'Name is required',
19
- });
20
- expect(err.statusCode).toBe(422);
21
- expect(err.errors!.email).toEqual(['Email is required', 'Email must be valid']);
22
- expect(err.errors!.name).toBe('Name is required');
23
- });
24
-
25
- test('is instanceof Error', () => {
26
- const err = new HttpError('Server Error', 500);
27
- expect(err instanceof Error).toBe(true);
28
- expect(err instanceof HttpError).toBe(true);
29
- });
30
- });
31
-
32
- describe('CrudOperator types', () => {
33
- test('all operators are valid', () => {
34
- const operators: CrudOperator[] = [
35
- 'eq', 'ne', 'lt', 'gt', 'lte', 'gte',
36
- 'contains', 'ncontains',
37
- 'startswith', 'endswith',
38
- 'in', 'nin',
39
- 'null', 'nnull',
40
- 'between', 'nbetween',
41
- ];
42
- expect(operators).toHaveLength(16);
43
- });
44
-
45
- test('Filter uses CrudOperator', () => {
46
- const filter: Filter = { field: 'name', operator: 'startswith', value: 'A' };
47
- expect(filter.operator).toBe('startswith');
48
- });
49
-
50
- test('negation operators', () => {
51
- const filters: Filter[] = [
52
- { field: 'status', operator: 'ncontains', value: 'draft' },
53
- { field: 'deleted_at', operator: 'nnull', value: null },
54
- { field: 'category', operator: 'nin', value: [1, 2] },
55
- { field: 'price', operator: 'nbetween', value: [100, 200] },
56
- ];
57
- expect(filters).toHaveLength(4);
58
- });
59
- });
60
-
61
- describe('LogicalFilter', () => {
62
- test('or combination', () => {
63
- const logical: LogicalFilter = {
64
- operator: 'or',
65
- value: [
66
- { field: 'status', operator: 'eq', value: 'published' },
67
- { field: 'status', operator: 'eq', value: 'draft' },
68
- ],
69
- };
70
- expect(logical.operator).toBe('or');
71
- expect(logical.value).toHaveLength(2);
72
- });
73
-
74
- test('and combination', () => {
75
- const logical: LogicalFilter = {
76
- operator: 'and',
77
- value: [
78
- { field: 'price', operator: 'gte', value: 10 },
79
- { field: 'price', operator: 'lte', value: 100 },
80
- ],
81
- };
82
- expect(logical.operator).toBe('and');
83
- expect(logical.value).toHaveLength(2);
84
- });
85
- });
package/src/i18n.test.ts DELETED
@@ -1,90 +0,0 @@
1
- // Unit tests for URL parsing (useParsed logic)
2
- // We test the pure parsing logic directly since the hook itself uses Svelte runes.
3
- import { describe, test, expect } from 'bun:test';
4
-
5
- /**
6
- * Parse a hash-based URL into structured route info.
7
- * This mirrors the logic in useParsed.ts.
8
- */
9
- function parseHash(hash: string) {
10
- const raw = hash.startsWith('#') ? hash.slice(1) : hash;
11
- const [pathPart, queryPart] = raw.split('?');
12
- const segments = pathPart.split('/').filter(Boolean);
13
-
14
- const params: Record<string, string> = {};
15
- if (queryPart) {
16
- for (const pair of queryPart.split('&')) {
17
- const [k, v] = pair.split('=');
18
- if (k) params[decodeURIComponent(k)] = decodeURIComponent(v ?? '');
19
- }
20
- }
21
-
22
- return {
23
- resource: segments[0],
24
- action: segments[1],
25
- id: segments[2],
26
- params,
27
- };
28
- }
29
-
30
- describe('parseHash (useParsed logic)', () => {
31
- test('empty hash', () => {
32
- const result = parseHash('');
33
- expect(result.resource).toBeUndefined();
34
- expect(result.action).toBeUndefined();
35
- expect(result.id).toBeUndefined();
36
- expect(result.params).toEqual({});
37
- });
38
-
39
- test('root hash', () => {
40
- const result = parseHash('#/');
41
- expect(result.resource).toBeUndefined();
42
- });
43
-
44
- test('resource only', () => {
45
- const result = parseHash('#/posts');
46
- expect(result.resource).toBe('posts');
47
- expect(result.action).toBeUndefined();
48
- expect(result.id).toBeUndefined();
49
- });
50
-
51
- test('resource and action', () => {
52
- const result = parseHash('#/posts/create');
53
- expect(result.resource).toBe('posts');
54
- expect(result.action).toBe('create');
55
- });
56
-
57
- test('resource, action, and id', () => {
58
- const result = parseHash('#/posts/edit/42');
59
- expect(result.resource).toBe('posts');
60
- expect(result.action).toBe('edit');
61
- expect(result.id).toBe('42');
62
- });
63
-
64
- test('with query params', () => {
65
- const result = parseHash('#/posts?page=2&sort=name');
66
- expect(result.resource).toBe('posts');
67
- expect(result.params.page).toBe('2');
68
- expect(result.params.sort).toBe('name');
69
- });
70
-
71
- test('resource with action and query', () => {
72
- const result = parseHash('#/posts/edit/5?tab=details');
73
- expect(result.resource).toBe('posts');
74
- expect(result.action).toBe('edit');
75
- expect(result.id).toBe('5');
76
- expect(result.params.tab).toBe('details');
77
- });
78
-
79
- test('encoded query params', () => {
80
- const result = parseHash('#/search?q=hello%20world&filter=tag%3Djs');
81
- expect(result.params.q).toBe('hello world');
82
- expect(result.params.filter).toBe('tag=js');
83
- });
84
-
85
- test('login route', () => {
86
- const result = parseHash('#/login');
87
- expect(result.resource).toBe('login');
88
- expect(result.action).toBeUndefined();
89
- });
90
- });