@tuturuuu/utils 0.0.3 → 0.6.1
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/CHANGELOG.md +313 -0
- package/biome.json +5 -0
- package/jsr.json +8 -8
- package/package.json +63 -32
- package/src/__tests__/ai-temp-auth.test.ts +309 -0
- package/src/__tests__/api-proxy-guard.test.ts +1451 -0
- package/src/__tests__/app-url.test.ts +270 -0
- package/src/__tests__/avatar-url.test.ts +97 -0
- package/src/__tests__/color-helper.test.ts +179 -0
- package/src/__tests__/constants.test.ts +351 -0
- package/src/__tests__/crypto.test.ts +107 -0
- package/src/__tests__/date-helper.test.ts +408 -0
- package/src/__tests__/fixtures/task-description-full-featured.json +456 -0
- package/src/__tests__/format.test.ts +317 -0
- package/src/__tests__/html-sanitizer.test.ts +360 -0
- package/src/__tests__/interest-calculator.test.ts +336 -0
- package/src/__tests__/interest-detector.test.ts +222 -0
- package/src/__tests__/label-colors.test.ts +241 -0
- package/src/__tests__/name-helper.test.ts +158 -0
- package/src/__tests__/node-diff.test.ts +576 -0
- package/src/__tests__/notification-service.test.ts +210 -0
- package/src/__tests__/onboarding-helper.test.ts +331 -0
- package/src/__tests__/path-helper.test.ts +152 -0
- package/src/__tests__/permissions.test.tsx +81 -0
- package/src/__tests__/request-emoji-limit.test.ts +172 -0
- package/src/__tests__/search-helper.test.ts +51 -0
- package/src/__tests__/storage-display-name.test.ts +37 -0
- package/src/__tests__/storage-path.test.ts +238 -0
- package/src/__tests__/tag-utils.test.ts +205 -0
- package/src/__tests__/task-description-yjs-state.test.ts +581 -0
- package/src/__tests__/task-helper-board-api-routing.test.ts +94 -0
- package/src/__tests__/task-helper-create-task.test.ts +129 -0
- package/src/__tests__/task-helpers.test.ts +464 -0
- package/src/__tests__/task-overrides.test.ts +305 -0
- package/src/__tests__/task-reorder-cache.test.ts +74 -0
- package/src/__tests__/task-sort-keys.test.ts +36 -0
- package/src/__tests__/task-transformers.test.ts +62 -0
- package/src/__tests__/text-helper.test.ts +776 -0
- package/src/__tests__/time-helper.test.ts +70 -0
- package/src/__tests__/time-tracker-period.test.ts +55 -0
- package/src/__tests__/timezone.test.ts +117 -0
- package/src/__tests__/upstash-rest.test.ts +77 -0
- package/src/__tests__/uuid-helper.test.ts +133 -0
- package/src/__tests__/workspace-helper.test.ts +859 -0
- package/src/__tests__/workspace-limits.test.ts +255 -0
- package/src/__tests__/yjs-helper.test.ts +581 -0
- package/src/abuse-protection/__tests__/backend-rate-limit.test.ts +113 -0
- package/src/abuse-protection/__tests__/edge.test.ts +136 -0
- package/src/abuse-protection/__tests__/index.test.ts +562 -0
- package/src/abuse-protection/__tests__/reputation.test.ts +192 -0
- package/src/abuse-protection/backend-rate-limit.ts +44 -0
- package/src/abuse-protection/constants.ts +117 -0
- package/src/abuse-protection/edge.ts +223 -0
- package/src/abuse-protection/index.ts +1545 -0
- package/src/abuse-protection/reputation.ts +587 -0
- package/src/abuse-protection/types.ts +97 -0
- package/src/abuse-protection/user-agent.ts +124 -0
- package/src/abuse-protection/user-suspension.ts +231 -0
- package/src/ai-temp-auth.ts +315 -0
- package/src/api-proxy-guard.ts +965 -0
- package/src/app-url.ts +96 -0
- package/src/avatar-url.ts +64 -0
- package/src/break-duration.ts +84 -0
- package/src/calendar-auth-token.test.ts +37 -0
- package/src/calendar-auth-token.ts +19 -0
- package/src/calendar-sync-coordination.md +197 -0
- package/src/calendar-utils.test.ts +169 -0
- package/src/calendar-utils.ts +91 -0
- package/src/color-helper.ts +110 -0
- package/src/common/nextjs.tsx +99 -0
- package/src/common/scan.tsx +15 -0
- package/src/configs/reports.ts +160 -0
- package/src/constants.ts +85 -0
- package/src/crypto.ts +21 -0
- package/src/currencies.ts +97 -0
- package/src/date-helper.ts +313 -0
- package/src/editor/convert-to-task.ts +264 -0
- package/src/editor/index.ts +5 -0
- package/src/email/__tests__/client.test.ts +141 -0
- package/src/email/__tests__/validation.test.ts +46 -0
- package/src/email/client.ts +92 -0
- package/src/email/server.ts +128 -0
- package/src/email/validation.ts +11 -0
- package/src/encryption/__tests__/calendar-events.test.ts +411 -0
- package/src/encryption/__tests__/configuration.test.ts +114 -0
- package/src/encryption/__tests__/field-encryption.test.ts +232 -0
- package/src/encryption/__tests__/key-generation.test.ts +30 -0
- package/src/encryption/__tests__/performance-edge-cases.test.ts +187 -0
- package/src/encryption/__tests__/test-helpers.ts +22 -0
- package/src/encryption/__tests__/workspace-key-encryption.test.ts +129 -0
- package/src/encryption/encryption-service.ts +343 -0
- package/src/encryption/index.ts +25 -0
- package/src/encryption/types.ts +57 -0
- package/src/exchange-rates.ts +49 -0
- package/src/feature-flags/__tests__/feature-flags.test.ts +302 -0
- package/src/feature-flags/core.ts +322 -0
- package/src/feature-flags/data.ts +16 -0
- package/src/feature-flags/default.ts +18 -0
- package/src/feature-flags/index.ts +7 -0
- package/src/feature-flags/requestable-features.ts +79 -0
- package/src/feature-flags/types.ts +4 -0
- package/src/fetcher.ts +2 -0
- package/src/finance/index.ts +4 -0
- package/src/finance/interest-calculator.ts +456 -0
- package/src/finance/interest-detector.ts +141 -0
- package/src/finance/transform-invoice-results.ts +219 -0
- package/src/finance/wallet-permissions.test.ts +169 -0
- package/src/finance/wallet-permissions.ts +82 -0
- package/src/format.ts +120 -1
- package/src/generated/platform-build-metadata.ts +11 -0
- package/src/hooks/use-platform.ts +64 -0
- package/src/html-sanitizer.ts +155 -0
- package/src/internal-domains.ts +497 -0
- package/src/keyboard-preset.ts +109 -0
- package/src/label-colors.ts +213 -0
- package/src/launchable-apps.test.ts +126 -0
- package/src/launchable-apps.ts +490 -0
- package/src/name-helper.ts +269 -0
- package/src/next-config.test.ts +234 -0
- package/src/next-config.ts +203 -0
- package/src/node-diff.ts +375 -0
- package/src/notification-service.ts +379 -0
- package/src/nova/scores/__tests__/calculate.test.ts +254 -0
- package/src/nova/scores/calculate.ts +132 -0
- package/src/nova/submissions/check-permission.ts +132 -0
- package/src/onboarding-helper.ts +213 -0
- package/src/path-helper.ts +93 -0
- package/src/permissions.tsx +1170 -0
- package/src/plan-helpers.test.ts +188 -0
- package/src/plan-helpers.ts +80 -0
- package/src/platform-release.test.ts +74 -0
- package/src/platform-release.ts +155 -0
- package/src/portless.ts +124 -0
- package/src/priority-styles.ts +42 -0
- package/src/request-emoji-limit.ts +335 -0
- package/src/search-helper.ts +18 -0
- package/src/search.test.ts +89 -0
- package/src/search.ts +355 -0
- package/src/storage-display-name.ts +30 -0
- package/src/storage-path.ts +147 -0
- package/src/tag-utils.ts +159 -0
- package/src/task/reorder.ts +245 -0
- package/src/task/transformers.ts +149 -0
- package/src/task-date-timezone.ts +133 -0
- package/src/task-description-content.ts +240 -0
- package/src/task-helper/board.ts +193 -0
- package/src/task-helper/bulk-actions.ts +564 -0
- package/src/task-helper/personal-external-staging.ts +21 -0
- package/src/task-helper/recycle-bin.ts +202 -0
- package/src/task-helper/relationships.ts +346 -0
- package/src/task-helper/shared.ts +109 -0
- package/src/task-helper/sort-keys.ts +337 -0
- package/src/task-helper/task-hooks-basic.ts +342 -0
- package/src/task-helper/task-hooks-move.ts +264 -0
- package/src/task-helper/task-operations.ts +278 -0
- package/src/task-helper.ts +12 -0
- package/src/task-helpers.ts +241 -0
- package/src/task-list-status.ts +62 -0
- package/src/task-overrides.ts +82 -0
- package/src/task-snapshot.ts +374 -0
- package/src/text-diff.ts +81 -0
- package/src/text-helper.ts +537 -0
- package/src/time-helper.ts +63 -0
- package/src/time-tracker-period.ts +73 -0
- package/src/timeblock-helper.ts +418 -0
- package/src/timezone.ts +190 -0
- package/src/timezones.json +1271 -0
- package/src/upstash-rest.ts +56 -0
- package/src/user-helper.ts +296 -0
- package/src/uuid-helper.ts +11 -0
- package/src/workspace-handle.ts +10 -0
- package/src/workspace-helper.ts +1408 -0
- package/src/workspace-limits.ts +68 -0
- package/src/yjs-helper.ts +217 -0
- package/src/yjs-task-description.ts +81 -0
- package/tsconfig.json +3 -5
- package/tsconfig.typecheck.json +33 -0
- package/vitest.config.ts +36 -0
- package/dist/index.d.ts +0 -8
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -2
- package/dist/index.mjs.map +0 -1
- package/eslint.config.mjs +0 -20
- package/rollup.config.js +0 -41
- package/src/index.ts +0 -1
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
computeNodeDiff,
|
|
4
|
+
flattenMediaNodes,
|
|
5
|
+
getNodeDisplayLabel,
|
|
6
|
+
getNodeIdentifier,
|
|
7
|
+
getNodeImageSrc,
|
|
8
|
+
hasNodeDifferences,
|
|
9
|
+
parseJsonContent,
|
|
10
|
+
} from '../node-diff';
|
|
11
|
+
|
|
12
|
+
describe('Node Diff', () => {
|
|
13
|
+
describe('getNodeIdentifier', () => {
|
|
14
|
+
it('returns src-based identifier for images', () => {
|
|
15
|
+
const node = {
|
|
16
|
+
type: 'image',
|
|
17
|
+
attrs: { src: 'https://example.com/cat.png' },
|
|
18
|
+
};
|
|
19
|
+
expect(getNodeIdentifier(node)).toBe('image:https://example.com/cat.png');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('returns src-based identifier for imageResize', () => {
|
|
23
|
+
const node = {
|
|
24
|
+
type: 'imageResize',
|
|
25
|
+
attrs: { src: 'https://example.com/cat.png', width: 400 },
|
|
26
|
+
};
|
|
27
|
+
expect(getNodeIdentifier(node)).toBe(
|
|
28
|
+
'imageResize:https://example.com/cat.png'
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('returns src-based identifier for video', () => {
|
|
33
|
+
const node = {
|
|
34
|
+
type: 'video',
|
|
35
|
+
attrs: { src: 'https://example.com/video.mp4' },
|
|
36
|
+
};
|
|
37
|
+
expect(getNodeIdentifier(node)).toBe(
|
|
38
|
+
'video:https://example.com/video.mp4'
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('returns src or videoId for youtube', () => {
|
|
43
|
+
const node1 = {
|
|
44
|
+
type: 'youtube',
|
|
45
|
+
attrs: { src: 'https://youtube.com/watch?v=abc123' },
|
|
46
|
+
};
|
|
47
|
+
expect(getNodeIdentifier(node1)).toBe(
|
|
48
|
+
'youtube:https://youtube.com/watch?v=abc123'
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const node2 = { type: 'youtube', attrs: { videoId: 'xyz789' } };
|
|
52
|
+
expect(getNodeIdentifier(node2)).toBe('youtube:xyz789');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('returns id-based identifier for mentions', () => {
|
|
56
|
+
const node = {
|
|
57
|
+
type: 'mention',
|
|
58
|
+
attrs: { id: 'user-123', label: 'John' },
|
|
59
|
+
};
|
|
60
|
+
expect(getNodeIdentifier(node)).toBe('mention:user-123');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('handles missing attributes', () => {
|
|
64
|
+
const node = { type: 'image' };
|
|
65
|
+
expect(getNodeIdentifier(node)).toBe('image:unknown');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('returns type only for non-identifiable nodes', () => {
|
|
69
|
+
const node = { type: 'paragraph' };
|
|
70
|
+
expect(getNodeIdentifier(node)).toBe('paragraph');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('getNodeDisplayLabel', () => {
|
|
75
|
+
it('shows filename for images with src', () => {
|
|
76
|
+
const node = {
|
|
77
|
+
type: 'image',
|
|
78
|
+
attrs: { src: 'https://example.com/uploads/cat-photo.png' },
|
|
79
|
+
};
|
|
80
|
+
expect(getNodeDisplayLabel(node)).toBe('cat-photo.png');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('shows alt text with filename for images', () => {
|
|
84
|
+
const node = {
|
|
85
|
+
type: 'image',
|
|
86
|
+
attrs: { src: 'https://example.com/cat.png', alt: 'My Cat' },
|
|
87
|
+
};
|
|
88
|
+
expect(getNodeDisplayLabel(node)).toBe('My Cat (cat.png)');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('truncates long filenames', () => {
|
|
92
|
+
const node = {
|
|
93
|
+
type: 'image',
|
|
94
|
+
attrs: {
|
|
95
|
+
src: 'https://example.com/very-long-filename-that-should-be-truncated-for-display.png',
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
const label = getNodeDisplayLabel(node);
|
|
99
|
+
expect(label.length).toBeLessThanOrEqual(43); // 40 + '...'
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('shows video filename', () => {
|
|
103
|
+
const node = {
|
|
104
|
+
type: 'video',
|
|
105
|
+
attrs: { src: 'https://example.com/intro.mp4' },
|
|
106
|
+
};
|
|
107
|
+
expect(getNodeDisplayLabel(node)).toBe('intro.mp4');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('shows YouTube title if available', () => {
|
|
111
|
+
const node = {
|
|
112
|
+
type: 'youtube',
|
|
113
|
+
attrs: { src: 'https://youtube.com/watch?v=abc', title: 'My Video' },
|
|
114
|
+
};
|
|
115
|
+
expect(getNodeDisplayLabel(node)).toBe('My Video');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('extracts YouTube video ID from URL', () => {
|
|
119
|
+
const node = {
|
|
120
|
+
type: 'youtube',
|
|
121
|
+
attrs: { src: 'https://youtube.com/watch?v=abc123' },
|
|
122
|
+
};
|
|
123
|
+
expect(getNodeDisplayLabel(node)).toBe('YouTube: abc123');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('shows mention label with @', () => {
|
|
127
|
+
const node = {
|
|
128
|
+
type: 'mention',
|
|
129
|
+
attrs: { label: 'John Doe', id: 'user-123' },
|
|
130
|
+
};
|
|
131
|
+
expect(getNodeDisplayLabel(node)).toBe('@John Doe');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('falls back to id for mentions without label', () => {
|
|
135
|
+
const node = { type: 'mention', attrs: { id: 'user-123' } };
|
|
136
|
+
expect(getNodeDisplayLabel(node)).toBe('@user-123');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('returns node type for unknown nodes', () => {
|
|
140
|
+
const node = { type: 'customNode' };
|
|
141
|
+
expect(getNodeDisplayLabel(node)).toBe('customNode');
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('flattenMediaNodes', () => {
|
|
146
|
+
it('extracts images from content', () => {
|
|
147
|
+
const content = {
|
|
148
|
+
type: 'doc',
|
|
149
|
+
content: [
|
|
150
|
+
{
|
|
151
|
+
type: 'paragraph',
|
|
152
|
+
content: [
|
|
153
|
+
{ type: 'image', attrs: { src: 'img1.png' } },
|
|
154
|
+
{ type: 'text', text: 'Hello' },
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
{ type: 'image', attrs: { src: 'img2.png' } },
|
|
158
|
+
],
|
|
159
|
+
};
|
|
160
|
+
const nodes = flattenMediaNodes(content);
|
|
161
|
+
expect(nodes).toHaveLength(2);
|
|
162
|
+
expect(nodes[0]?.node.attrs?.src).toBe('img1.png');
|
|
163
|
+
expect(nodes[0]?.path).toEqual([0, 0]);
|
|
164
|
+
expect(nodes[1]?.node.attrs?.src).toBe('img2.png');
|
|
165
|
+
expect(nodes[1]?.path).toEqual([1]);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('extracts mentions', () => {
|
|
169
|
+
const content = {
|
|
170
|
+
type: 'doc',
|
|
171
|
+
content: [
|
|
172
|
+
{
|
|
173
|
+
type: 'paragraph',
|
|
174
|
+
content: [
|
|
175
|
+
{ type: 'text', text: 'Hello ' },
|
|
176
|
+
{ type: 'mention', attrs: { id: 'user-1', label: 'John' } },
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
};
|
|
181
|
+
const nodes = flattenMediaNodes(content);
|
|
182
|
+
expect(nodes).toHaveLength(1);
|
|
183
|
+
expect(nodes[0]?.node.type).toBe('mention');
|
|
184
|
+
expect(nodes[0]?.path).toEqual([0, 1]);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('extracts video and youtube nodes', () => {
|
|
188
|
+
const content = {
|
|
189
|
+
type: 'doc',
|
|
190
|
+
content: [
|
|
191
|
+
{ type: 'video', attrs: { src: 'video.mp4' } },
|
|
192
|
+
{ type: 'youtube', attrs: { src: 'https://youtube.com/...' } },
|
|
193
|
+
],
|
|
194
|
+
};
|
|
195
|
+
const nodes = flattenMediaNodes(content);
|
|
196
|
+
expect(nodes).toHaveLength(2);
|
|
197
|
+
expect(nodes[0]?.node.type).toBe('video');
|
|
198
|
+
expect(nodes[1]?.node.type).toBe('youtube');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('ignores non-identifiable nodes', () => {
|
|
202
|
+
const content = {
|
|
203
|
+
type: 'doc',
|
|
204
|
+
content: [
|
|
205
|
+
{ type: 'paragraph', content: [{ type: 'text', text: 'Hello' }] },
|
|
206
|
+
],
|
|
207
|
+
};
|
|
208
|
+
const nodes = flattenMediaNodes(content);
|
|
209
|
+
expect(nodes).toHaveLength(0);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('handles null/undefined content', () => {
|
|
213
|
+
expect(flattenMediaNodes(null)).toEqual([]);
|
|
214
|
+
expect(flattenMediaNodes(undefined)).toEqual([]);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('extracts deeply nested media', () => {
|
|
218
|
+
const content = {
|
|
219
|
+
type: 'doc',
|
|
220
|
+
content: [
|
|
221
|
+
{
|
|
222
|
+
type: 'blockquote',
|
|
223
|
+
content: [
|
|
224
|
+
{
|
|
225
|
+
type: 'paragraph',
|
|
226
|
+
content: [
|
|
227
|
+
{
|
|
228
|
+
type: 'image',
|
|
229
|
+
attrs: { src: 'nested-image.png' },
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
};
|
|
237
|
+
const nodes = flattenMediaNodes(content);
|
|
238
|
+
expect(nodes).toHaveLength(1);
|
|
239
|
+
expect(nodes[0]?.path).toEqual([0, 0, 0]);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe('computeNodeDiff', () => {
|
|
244
|
+
it('detects added images', () => {
|
|
245
|
+
const oldContent = {
|
|
246
|
+
type: 'doc',
|
|
247
|
+
content: [
|
|
248
|
+
{ type: 'paragraph', content: [{ type: 'text', text: 'Hello' }] },
|
|
249
|
+
],
|
|
250
|
+
};
|
|
251
|
+
const newContent = {
|
|
252
|
+
type: 'doc',
|
|
253
|
+
content: [
|
|
254
|
+
{ type: 'paragraph', content: [{ type: 'text', text: 'Hello' }] },
|
|
255
|
+
{ type: 'image', attrs: { src: 'new-image.png' } },
|
|
256
|
+
],
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const diff = computeNodeDiff(oldContent, newContent);
|
|
260
|
+
expect(diff.added).toHaveLength(1);
|
|
261
|
+
expect(diff.added[0]?.nodeType).toBe('image');
|
|
262
|
+
expect(diff.added[0]?.newNode?.attrs?.src).toBe('new-image.png');
|
|
263
|
+
expect(diff.hasMediaChanges).toBe(true);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('detects removed images', () => {
|
|
267
|
+
const oldContent = {
|
|
268
|
+
type: 'doc',
|
|
269
|
+
content: [
|
|
270
|
+
{ type: 'image', attrs: { src: 'old-image.png' } },
|
|
271
|
+
{ type: 'paragraph', content: [{ type: 'text', text: 'Hello' }] },
|
|
272
|
+
],
|
|
273
|
+
};
|
|
274
|
+
const newContent = {
|
|
275
|
+
type: 'doc',
|
|
276
|
+
content: [
|
|
277
|
+
{ type: 'paragraph', content: [{ type: 'text', text: 'Hello' }] },
|
|
278
|
+
],
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const diff = computeNodeDiff(oldContent, newContent);
|
|
282
|
+
expect(diff.removed).toHaveLength(1);
|
|
283
|
+
expect(diff.removed[0]?.nodeType).toBe('image');
|
|
284
|
+
expect(diff.removed[0]?.oldNode?.attrs?.src).toBe('old-image.png');
|
|
285
|
+
expect(diff.hasMediaChanges).toBe(true);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('detects different images with same alt text', () => {
|
|
289
|
+
const oldContent = {
|
|
290
|
+
type: 'doc',
|
|
291
|
+
content: [{ type: 'image', attrs: { src: 'cat.png', alt: 'Pet' } }],
|
|
292
|
+
};
|
|
293
|
+
const newContent = {
|
|
294
|
+
type: 'doc',
|
|
295
|
+
content: [{ type: 'image', attrs: { src: 'dog.png', alt: 'Pet' } }],
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const diff = computeNodeDiff(oldContent, newContent);
|
|
299
|
+
// Different src = different images, so one added, one removed
|
|
300
|
+
expect(diff.added).toHaveLength(1);
|
|
301
|
+
expect(diff.removed).toHaveLength(1);
|
|
302
|
+
expect(diff.added[0]?.newNode?.attrs?.src).toBe('dog.png');
|
|
303
|
+
expect(diff.removed[0]?.oldNode?.attrs?.src).toBe('cat.png');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('detects modified image attributes', () => {
|
|
307
|
+
const oldContent = {
|
|
308
|
+
type: 'doc',
|
|
309
|
+
content: [
|
|
310
|
+
{
|
|
311
|
+
type: 'image',
|
|
312
|
+
attrs: { src: 'image.png', width: 400, alt: 'Old Alt' },
|
|
313
|
+
},
|
|
314
|
+
],
|
|
315
|
+
};
|
|
316
|
+
const newContent = {
|
|
317
|
+
type: 'doc',
|
|
318
|
+
content: [
|
|
319
|
+
{
|
|
320
|
+
type: 'image',
|
|
321
|
+
attrs: { src: 'image.png', width: 800, alt: 'New Alt' },
|
|
322
|
+
},
|
|
323
|
+
],
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const diff = computeNodeDiff(oldContent, newContent);
|
|
327
|
+
expect(diff.modified).toHaveLength(1);
|
|
328
|
+
expect(diff.modified[0]?.attributeChanges).toBeDefined();
|
|
329
|
+
expect(diff.modified[0]?.attributeChanges).toContainEqual({
|
|
330
|
+
key: 'width',
|
|
331
|
+
oldValue: 400,
|
|
332
|
+
newValue: 800,
|
|
333
|
+
});
|
|
334
|
+
expect(diff.modified[0]?.attributeChanges).toContainEqual({
|
|
335
|
+
key: 'alt',
|
|
336
|
+
oldValue: 'Old Alt',
|
|
337
|
+
newValue: 'New Alt',
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('detects mention changes with different IDs but same label', () => {
|
|
342
|
+
const oldContent = {
|
|
343
|
+
type: 'doc',
|
|
344
|
+
content: [
|
|
345
|
+
{
|
|
346
|
+
type: 'paragraph',
|
|
347
|
+
content: [
|
|
348
|
+
{ type: 'mention', attrs: { id: 'user-1', label: 'John' } },
|
|
349
|
+
],
|
|
350
|
+
},
|
|
351
|
+
],
|
|
352
|
+
};
|
|
353
|
+
const newContent = {
|
|
354
|
+
type: 'doc',
|
|
355
|
+
content: [
|
|
356
|
+
{
|
|
357
|
+
type: 'paragraph',
|
|
358
|
+
content: [
|
|
359
|
+
{ type: 'mention', attrs: { id: 'user-2', label: 'John' } },
|
|
360
|
+
],
|
|
361
|
+
},
|
|
362
|
+
],
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const diff = computeNodeDiff(oldContent, newContent);
|
|
366
|
+
// Different IDs = different mentions
|
|
367
|
+
expect(diff.added).toHaveLength(1);
|
|
368
|
+
expect(diff.removed).toHaveLength(1);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('handles null old content (all added)', () => {
|
|
372
|
+
const newContent = {
|
|
373
|
+
type: 'doc',
|
|
374
|
+
content: [{ type: 'image', attrs: { src: 'image.png' } }],
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const diff = computeNodeDiff(null, newContent);
|
|
378
|
+
expect(diff.added).toHaveLength(1);
|
|
379
|
+
expect(diff.removed).toHaveLength(0);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('handles null new content (all removed)', () => {
|
|
383
|
+
const oldContent = {
|
|
384
|
+
type: 'doc',
|
|
385
|
+
content: [{ type: 'image', attrs: { src: 'image.png' } }],
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const diff = computeNodeDiff(oldContent, null);
|
|
389
|
+
expect(diff.added).toHaveLength(0);
|
|
390
|
+
expect(diff.removed).toHaveLength(1);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('handles both null (no changes)', () => {
|
|
394
|
+
const diff = computeNodeDiff(null, null);
|
|
395
|
+
expect(diff.totalChanges).toBe(0);
|
|
396
|
+
expect(diff.hasMediaChanges).toBe(false);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('handles identical content (no changes)', () => {
|
|
400
|
+
const content = {
|
|
401
|
+
type: 'doc',
|
|
402
|
+
content: [{ type: 'image', attrs: { src: 'image.png' } }],
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const diff = computeNodeDiff(content, content);
|
|
406
|
+
expect(diff.totalChanges).toBe(0);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('handles multiple duplicate images', () => {
|
|
410
|
+
const oldContent = {
|
|
411
|
+
type: 'doc',
|
|
412
|
+
content: [
|
|
413
|
+
{ type: 'image', attrs: { src: 'image.png' } },
|
|
414
|
+
{ type: 'image', attrs: { src: 'image.png' } },
|
|
415
|
+
],
|
|
416
|
+
};
|
|
417
|
+
const newContent = {
|
|
418
|
+
type: 'doc',
|
|
419
|
+
content: [{ type: 'image', attrs: { src: 'image.png' } }],
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
const diff = computeNodeDiff(oldContent, newContent);
|
|
423
|
+
// One duplicate removed
|
|
424
|
+
expect(diff.removed).toHaveLength(1);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('correctly calculates totalChanges', () => {
|
|
428
|
+
const oldContent = {
|
|
429
|
+
type: 'doc',
|
|
430
|
+
content: [
|
|
431
|
+
{ type: 'image', attrs: { src: 'old.png' } },
|
|
432
|
+
{ type: 'image', attrs: { src: 'same.png', width: 100 } },
|
|
433
|
+
],
|
|
434
|
+
};
|
|
435
|
+
const newContent = {
|
|
436
|
+
type: 'doc',
|
|
437
|
+
content: [
|
|
438
|
+
{ type: 'image', attrs: { src: 'new.png' } },
|
|
439
|
+
{ type: 'image', attrs: { src: 'same.png', width: 200 } },
|
|
440
|
+
],
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
const diff = computeNodeDiff(oldContent, newContent);
|
|
444
|
+
// 1 added (new.png), 1 removed (old.png), 1 modified (same.png width change)
|
|
445
|
+
expect(diff.totalChanges).toBe(3);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('handles video changes', () => {
|
|
449
|
+
const oldContent = {
|
|
450
|
+
type: 'doc',
|
|
451
|
+
content: [{ type: 'video', attrs: { src: 'old-video.mp4' } }],
|
|
452
|
+
};
|
|
453
|
+
const newContent = {
|
|
454
|
+
type: 'doc',
|
|
455
|
+
content: [{ type: 'video', attrs: { src: 'new-video.mp4' } }],
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const diff = computeNodeDiff(oldContent, newContent);
|
|
459
|
+
expect(diff.added).toHaveLength(1);
|
|
460
|
+
expect(diff.removed).toHaveLength(1);
|
|
461
|
+
expect(diff.hasMediaChanges).toBe(true);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('handles youtube changes', () => {
|
|
465
|
+
const oldContent = {
|
|
466
|
+
type: 'doc',
|
|
467
|
+
content: [
|
|
468
|
+
{
|
|
469
|
+
type: 'youtube',
|
|
470
|
+
attrs: { src: 'https://youtube.com/watch?v=old' },
|
|
471
|
+
},
|
|
472
|
+
],
|
|
473
|
+
};
|
|
474
|
+
const newContent = {
|
|
475
|
+
type: 'doc',
|
|
476
|
+
content: [
|
|
477
|
+
{
|
|
478
|
+
type: 'youtube',
|
|
479
|
+
attrs: { src: 'https://youtube.com/watch?v=new' },
|
|
480
|
+
},
|
|
481
|
+
],
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
const diff = computeNodeDiff(oldContent, newContent);
|
|
485
|
+
expect(diff.added).toHaveLength(1);
|
|
486
|
+
expect(diff.removed).toHaveLength(1);
|
|
487
|
+
expect(diff.hasMediaChanges).toBe(true);
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
describe('parseJsonContent', () => {
|
|
492
|
+
it('parses JSON string', () => {
|
|
493
|
+
const json = JSON.stringify({ type: 'doc', content: [] });
|
|
494
|
+
const result = parseJsonContent(json);
|
|
495
|
+
expect(result).toEqual({ type: 'doc', content: [] });
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('returns object as-is', () => {
|
|
499
|
+
const obj = { type: 'doc', content: [] };
|
|
500
|
+
const result = parseJsonContent(obj);
|
|
501
|
+
expect(result).toBe(obj);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it('returns null for null/undefined', () => {
|
|
505
|
+
expect(parseJsonContent(null)).toBeNull();
|
|
506
|
+
expect(parseJsonContent(undefined)).toBeNull();
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it('returns null for invalid JSON', () => {
|
|
510
|
+
expect(parseJsonContent('not valid json')).toBeNull();
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
describe('getNodeImageSrc', () => {
|
|
515
|
+
it('returns src for image nodes', () => {
|
|
516
|
+
const node = {
|
|
517
|
+
type: 'image',
|
|
518
|
+
attrs: { src: 'https://example.com/cat.png' },
|
|
519
|
+
};
|
|
520
|
+
expect(getNodeImageSrc(node)).toBe('https://example.com/cat.png');
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it('returns src for imageResize nodes', () => {
|
|
524
|
+
const node = {
|
|
525
|
+
type: 'imageResize',
|
|
526
|
+
attrs: { src: 'https://example.com/cat.png', width: 400 },
|
|
527
|
+
};
|
|
528
|
+
expect(getNodeImageSrc(node)).toBe('https://example.com/cat.png');
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('returns null for non-image nodes', () => {
|
|
532
|
+
const node = { type: 'video', attrs: { src: 'video.mp4' } };
|
|
533
|
+
expect(getNodeImageSrc(node)).toBeNull();
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it('returns null for missing src', () => {
|
|
537
|
+
const node = { type: 'image' };
|
|
538
|
+
expect(getNodeImageSrc(node)).toBeNull();
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
describe('hasNodeDifferences', () => {
|
|
543
|
+
it('returns true when there are differences', () => {
|
|
544
|
+
const old = {
|
|
545
|
+
type: 'doc',
|
|
546
|
+
content: [{ type: 'image', attrs: { src: 'a.png' } }],
|
|
547
|
+
};
|
|
548
|
+
const newC = {
|
|
549
|
+
type: 'doc',
|
|
550
|
+
content: [{ type: 'image', attrs: { src: 'b.png' } }],
|
|
551
|
+
};
|
|
552
|
+
expect(hasNodeDifferences(old, newC)).toBe(true);
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it('returns false when content is identical', () => {
|
|
556
|
+
const content = {
|
|
557
|
+
type: 'doc',
|
|
558
|
+
content: [{ type: 'image', attrs: { src: 'a.png' } }],
|
|
559
|
+
};
|
|
560
|
+
expect(hasNodeDifferences(content, content)).toBe(false);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it('returns false when both are null', () => {
|
|
564
|
+
expect(hasNodeDifferences(null, null)).toBe(false);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it('returns true when one is null', () => {
|
|
568
|
+
const content = {
|
|
569
|
+
type: 'doc',
|
|
570
|
+
content: [{ type: 'image', attrs: { src: 'a.png' } }],
|
|
571
|
+
};
|
|
572
|
+
expect(hasNodeDifferences(content, null)).toBe(true);
|
|
573
|
+
expect(hasNodeDifferences(null, content)).toBe(true);
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
});
|