@object-ui/plugin-view 3.0.2 → 3.1.0
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/.turbo/turbo-build.log +6 -6
- package/CHANGELOG.md +11 -0
- package/dist/index.js +4382 -830
- package/dist/index.umd.cjs +6 -2
- package/dist/plugin-view/src/ObjectView.d.ts +8 -0
- package/dist/plugin-view/src/SharedViewLink.d.ts +23 -0
- package/dist/plugin-view/src/ViewSwitcher.d.ts +3 -0
- package/dist/plugin-view/src/ViewTabBar.d.ts +75 -0
- package/dist/plugin-view/src/index.d.ts +5 -1
- package/package.json +11 -8
- package/src/ObjectView.tsx +186 -22
- package/src/SharedViewLink.tsx +199 -0
- package/src/ViewSwitcher.tsx +69 -1
- package/src/ViewTabBar.tsx +656 -0
- package/src/__tests__/ObjectView.test.tsx +290 -0
- package/src/__tests__/SharedViewLinkPassword.test.tsx +172 -0
- package/src/__tests__/ViewTabBar.test.tsx +710 -0
- package/src/__tests__/config-sync-integration.test.tsx +588 -0
- package/src/index.tsx +21 -1
|
@@ -288,6 +288,59 @@ describe('ObjectView', () => {
|
|
|
288
288
|
|
|
289
289
|
expect(onNavigate).toHaveBeenCalledWith('1', 'view');
|
|
290
290
|
});
|
|
291
|
+
|
|
292
|
+
it('should open form in view mode when split navigation mode is clicked', () => {
|
|
293
|
+
const schema: ObjectViewSchema = {
|
|
294
|
+
type: 'object-view',
|
|
295
|
+
objectName: 'contacts',
|
|
296
|
+
navigation: { mode: 'split' },
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
render(<ObjectView schema={schema} dataSource={mockDataSource} />);
|
|
300
|
+
|
|
301
|
+
fireEvent.click(screen.getByTestId('grid-row'));
|
|
302
|
+
|
|
303
|
+
// Split mode renders NavigationOverlay with split panels including a close button
|
|
304
|
+
expect(screen.getByTestId('object-form')).toBeDefined();
|
|
305
|
+
expect(screen.getByLabelText('Close panel')).toBeDefined();
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('should open form in view mode when popover navigation mode is clicked', () => {
|
|
309
|
+
const schema: ObjectViewSchema = {
|
|
310
|
+
type: 'object-view',
|
|
311
|
+
objectName: 'contacts',
|
|
312
|
+
navigation: { mode: 'popover' },
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
render(<ObjectView schema={schema} dataSource={mockDataSource} />);
|
|
316
|
+
|
|
317
|
+
fireEvent.click(screen.getByTestId('grid-row'));
|
|
318
|
+
|
|
319
|
+
// Popover mode renders NavigationOverlay Dialog fallback (no popoverTrigger)
|
|
320
|
+
expect(screen.getByTestId('object-form')).toBeDefined();
|
|
321
|
+
expect(screen.getByRole('dialog')).toBeDefined();
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('should close split panel and return to normal view', () => {
|
|
325
|
+
const schema: ObjectViewSchema = {
|
|
326
|
+
type: 'object-view',
|
|
327
|
+
objectName: 'contacts',
|
|
328
|
+
navigation: { mode: 'split' },
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
render(<ObjectView schema={schema} dataSource={mockDataSource} />);
|
|
332
|
+
|
|
333
|
+
// Open split panel
|
|
334
|
+
fireEvent.click(screen.getByTestId('grid-row'));
|
|
335
|
+
expect(screen.getByLabelText('Close panel')).toBeDefined();
|
|
336
|
+
|
|
337
|
+
// Close split panel
|
|
338
|
+
fireEvent.click(screen.getByLabelText('Close panel'));
|
|
339
|
+
|
|
340
|
+
// Form should be gone, grid should remain
|
|
341
|
+
expect(screen.queryByLabelText('Close panel')).toBeNull();
|
|
342
|
+
expect(screen.getByTestId('object-grid')).toBeDefined();
|
|
343
|
+
});
|
|
291
344
|
});
|
|
292
345
|
|
|
293
346
|
// ============================
|
|
@@ -372,4 +425,241 @@ describe('ObjectView', () => {
|
|
|
372
425
|
expect(screen.queryByText('Kanban')).toBeNull();
|
|
373
426
|
});
|
|
374
427
|
});
|
|
428
|
+
|
|
429
|
+
// ============================
|
|
430
|
+
// Live Preview — viewConfig sync
|
|
431
|
+
// ============================
|
|
432
|
+
describe('Live Preview', () => {
|
|
433
|
+
it('should re-render grid when views prop updates with new columns', async () => {
|
|
434
|
+
const schema: ObjectViewSchema = {
|
|
435
|
+
type: 'object-view',
|
|
436
|
+
objectName: 'contacts',
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const initialViews = [
|
|
440
|
+
{ id: 'all', label: 'All', type: 'grid' as const, columns: ['name', 'email'] },
|
|
441
|
+
];
|
|
442
|
+
|
|
443
|
+
const { rerender } = render(
|
|
444
|
+
<ObjectView schema={schema} dataSource={mockDataSource} views={initialViews} activeViewId="all" />,
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
expect(screen.getByTestId('object-grid')).toBeInTheDocument();
|
|
448
|
+
|
|
449
|
+
// Simulate live preview: update views prop with new columns (as parent would after viewDraft change)
|
|
450
|
+
const updatedViews = [
|
|
451
|
+
{ id: 'all', label: 'All', type: 'grid' as const, columns: ['name', 'email', 'status'] },
|
|
452
|
+
];
|
|
453
|
+
|
|
454
|
+
rerender(
|
|
455
|
+
<ObjectView schema={schema} dataSource={mockDataSource} views={updatedViews} activeViewId="all" />,
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
// Grid should still render (component did not crash on prop update)
|
|
459
|
+
expect(screen.getByTestId('object-grid')).toBeInTheDocument();
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('should re-render when views prop updates with new sort config', async () => {
|
|
463
|
+
const schema: ObjectViewSchema = {
|
|
464
|
+
type: 'object-view',
|
|
465
|
+
objectName: 'contacts',
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
const initialViews = [
|
|
469
|
+
{ id: 'all', label: 'All', type: 'grid' as const, columns: ['name'] },
|
|
470
|
+
];
|
|
471
|
+
|
|
472
|
+
const { rerender } = render(
|
|
473
|
+
<ObjectView schema={schema} dataSource={mockDataSource} views={initialViews} activeViewId="all" />,
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
// Update with sort config — simulates live preview of sort changes
|
|
477
|
+
const updatedViews = [
|
|
478
|
+
{ id: 'all', label: 'All', type: 'grid' as const, columns: ['name'], sort: [{ field: 'name', direction: 'desc' as const }] },
|
|
479
|
+
];
|
|
480
|
+
|
|
481
|
+
rerender(
|
|
482
|
+
<ObjectView schema={schema} dataSource={mockDataSource} views={updatedViews} activeViewId="all" />,
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
expect(screen.getByTestId('object-grid')).toBeInTheDocument();
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it('should re-render when views prop updates with new filter', async () => {
|
|
489
|
+
const schema: ObjectViewSchema = {
|
|
490
|
+
type: 'object-view',
|
|
491
|
+
objectName: 'contacts',
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const initialViews = [
|
|
495
|
+
{ id: 'all', label: 'All', type: 'grid' as const, columns: ['name'] },
|
|
496
|
+
];
|
|
497
|
+
|
|
498
|
+
const { rerender } = render(
|
|
499
|
+
<ObjectView schema={schema} dataSource={mockDataSource} views={initialViews} activeViewId="all" />,
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
// Update with filter — simulates live preview of filter changes
|
|
503
|
+
const updatedViews = [
|
|
504
|
+
{ id: 'all', label: 'All', type: 'grid' as const, columns: ['name'], filter: [['status', '=', 'active']] },
|
|
505
|
+
];
|
|
506
|
+
|
|
507
|
+
rerender(
|
|
508
|
+
<ObjectView schema={schema} dataSource={mockDataSource} views={updatedViews} activeViewId="all" />,
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
expect(screen.getByTestId('object-grid')).toBeInTheDocument();
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it('should re-render when views prop updates with appearance properties', async () => {
|
|
515
|
+
const schema: ObjectViewSchema = {
|
|
516
|
+
type: 'object-view',
|
|
517
|
+
objectName: 'contacts',
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
const initialViews = [
|
|
521
|
+
{ id: 'all', label: 'All', type: 'grid' as const, columns: ['name'] },
|
|
522
|
+
];
|
|
523
|
+
|
|
524
|
+
const { rerender } = render(
|
|
525
|
+
<ObjectView schema={schema} dataSource={mockDataSource} views={initialViews} activeViewId="all" />,
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
// Update with appearance changes — simulates live preview of rowHeight/striped/bordered
|
|
529
|
+
const updatedViews = [
|
|
530
|
+
{ id: 'all', label: 'All', type: 'grid' as const, columns: ['name'], striped: true, bordered: true },
|
|
531
|
+
];
|
|
532
|
+
|
|
533
|
+
rerender(
|
|
534
|
+
<ObjectView schema={schema} dataSource={mockDataSource} views={updatedViews} activeViewId="all" />,
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
expect(screen.getByTestId('object-grid')).toBeInTheDocument();
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it('should pass renderListView with updated schema when views change', async () => {
|
|
541
|
+
const schema: ObjectViewSchema = {
|
|
542
|
+
type: 'object-view',
|
|
543
|
+
objectName: 'contacts',
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
const renderListViewSpy = vi.fn(({ schema: listSchema }: any) => (
|
|
547
|
+
<div data-testid="custom-list" data-fields={JSON.stringify(listSchema.fields)}>
|
|
548
|
+
Custom ListView
|
|
549
|
+
</div>
|
|
550
|
+
));
|
|
551
|
+
|
|
552
|
+
const initialViews = [
|
|
553
|
+
{ id: 'all', label: 'All', type: 'grid' as const, columns: ['name'] },
|
|
554
|
+
];
|
|
555
|
+
|
|
556
|
+
const { rerender } = render(
|
|
557
|
+
<ObjectView
|
|
558
|
+
schema={schema}
|
|
559
|
+
dataSource={mockDataSource}
|
|
560
|
+
views={initialViews}
|
|
561
|
+
activeViewId="all"
|
|
562
|
+
renderListView={renderListViewSpy}
|
|
563
|
+
/>,
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
expect(screen.getByTestId('custom-list')).toBeInTheDocument();
|
|
567
|
+
const firstCallSchema = renderListViewSpy.mock.calls[0]?.[0]?.schema;
|
|
568
|
+
expect(firstCallSchema?.fields).toEqual(['name']);
|
|
569
|
+
|
|
570
|
+
// Update views — simulate live preview change
|
|
571
|
+
const updatedViews = [
|
|
572
|
+
{ id: 'all', label: 'All', type: 'grid' as const, columns: ['name', 'email', 'status'] },
|
|
573
|
+
];
|
|
574
|
+
|
|
575
|
+
rerender(
|
|
576
|
+
<ObjectView
|
|
577
|
+
schema={schema}
|
|
578
|
+
dataSource={mockDataSource}
|
|
579
|
+
views={updatedViews}
|
|
580
|
+
activeViewId="all"
|
|
581
|
+
renderListView={renderListViewSpy}
|
|
582
|
+
/>,
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
// renderListView should have been called again with the updated columns
|
|
586
|
+
const lastCallIndex = renderListViewSpy.mock.calls.length - 1;
|
|
587
|
+
const lastCallSchema = renderListViewSpy.mock.calls[lastCallIndex]?.[0]?.schema;
|
|
588
|
+
expect(lastCallSchema?.fields).toEqual(['name', 'email', 'status']);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it('should pass showSort=false through schema to suppress sort UI', async () => {
|
|
592
|
+
const schema: ObjectViewSchema = {
|
|
593
|
+
type: 'object-view',
|
|
594
|
+
objectName: 'contacts',
|
|
595
|
+
showSort: false,
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
render(
|
|
599
|
+
<ObjectView schema={schema} dataSource={mockDataSource} />,
|
|
600
|
+
);
|
|
601
|
+
|
|
602
|
+
// Component renders without crash — showSort is respected
|
|
603
|
+
expect(screen.getByTestId('object-grid')).toBeInTheDocument();
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it('should include showSearch/showFilters/showSort in renderListView schema', async () => {
|
|
607
|
+
const schema: ObjectViewSchema = {
|
|
608
|
+
type: 'object-view',
|
|
609
|
+
objectName: 'contacts',
|
|
610
|
+
showSearch: false,
|
|
611
|
+
showFilters: false,
|
|
612
|
+
showSort: false,
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
const renderListViewSpy = vi.fn(({ schema: listSchema }: any) => (
|
|
616
|
+
<div data-testid="custom-list">Custom ListView</div>
|
|
617
|
+
));
|
|
618
|
+
|
|
619
|
+
render(
|
|
620
|
+
<ObjectView
|
|
621
|
+
schema={schema}
|
|
622
|
+
dataSource={mockDataSource}
|
|
623
|
+
renderListView={renderListViewSpy}
|
|
624
|
+
/>,
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
expect(renderListViewSpy).toHaveBeenCalled();
|
|
628
|
+
const callSchema = renderListViewSpy.mock.calls[0]?.[0]?.schema;
|
|
629
|
+
expect(callSchema?.showSearch).toBe(false);
|
|
630
|
+
expect(callSchema?.showFilters).toBe(false);
|
|
631
|
+
expect(callSchema?.showSort).toBe(false);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it('should propagate showSearch/showFilters/showSort from activeView in renderListView', async () => {
|
|
635
|
+
const schema: ObjectViewSchema = {
|
|
636
|
+
type: 'object-view',
|
|
637
|
+
objectName: 'contacts',
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
const renderListViewSpy = vi.fn(({ schema: listSchema }: any) => (
|
|
641
|
+
<div data-testid="custom-list">Custom ListView</div>
|
|
642
|
+
));
|
|
643
|
+
|
|
644
|
+
const views = [
|
|
645
|
+
{ id: 'v1', label: 'View 1', type: 'grid' as const, showSearch: false, showFilters: false, showSort: false },
|
|
646
|
+
];
|
|
647
|
+
|
|
648
|
+
render(
|
|
649
|
+
<ObjectView
|
|
650
|
+
schema={schema}
|
|
651
|
+
dataSource={mockDataSource}
|
|
652
|
+
views={views}
|
|
653
|
+
activeViewId="v1"
|
|
654
|
+
renderListView={renderListViewSpy}
|
|
655
|
+
/>,
|
|
656
|
+
);
|
|
657
|
+
|
|
658
|
+
expect(renderListViewSpy).toHaveBeenCalled();
|
|
659
|
+
const callSchema = renderListViewSpy.mock.calls[0]?.[0]?.schema;
|
|
660
|
+
expect(callSchema?.showSearch).toBe(false);
|
|
661
|
+
expect(callSchema?.showFilters).toBe(false);
|
|
662
|
+
expect(callSchema?.showSort).toBe(false);
|
|
663
|
+
});
|
|
664
|
+
});
|
|
375
665
|
});
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
10
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
11
|
+
import userEvent from '@testing-library/user-event';
|
|
12
|
+
import React from 'react';
|
|
13
|
+
|
|
14
|
+
vi.mock('@object-ui/components', () => ({
|
|
15
|
+
cn: (...args: any[]) => args.filter(Boolean).join(' '),
|
|
16
|
+
Button: ({ children, onClick, ...props }: any) => (
|
|
17
|
+
<button onClick={onClick} {...props}>{children}</button>
|
|
18
|
+
),
|
|
19
|
+
Badge: ({ children, ...props }: any) => <span {...props}>{children}</span>,
|
|
20
|
+
Input: React.forwardRef(({ ...props }: any, ref: any) => <input ref={ref} {...props} />),
|
|
21
|
+
Popover: ({ children }: any) => <div>{children}</div>,
|
|
22
|
+
PopoverContent: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
|
23
|
+
PopoverTrigger: ({ children, asChild }: any) => <>{children}</>,
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
vi.mock('lucide-react', () => ({
|
|
27
|
+
Share2: () => <span>ShareIcon</span>,
|
|
28
|
+
Copy: () => <span>CopyIcon</span>,
|
|
29
|
+
Check: () => <span>CheckIcon</span>,
|
|
30
|
+
Lock: () => <span>LockIcon</span>,
|
|
31
|
+
Calendar: () => <span>CalendarIcon</span>,
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
import { SharedViewLink } from '../SharedViewLink';
|
|
35
|
+
|
|
36
|
+
describe('SharedViewLink - Password & Expiration', () => {
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
vi.restoreAllMocks();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('renders the Share button', () => {
|
|
42
|
+
render(<SharedViewLink objectName="tasks" />);
|
|
43
|
+
expect(screen.getByText('Share')).toBeInTheDocument();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('renders password input field in the popover', () => {
|
|
47
|
+
render(<SharedViewLink objectName="tasks" />);
|
|
48
|
+
const passwordInput = screen.getByPlaceholderText('Enter password...');
|
|
49
|
+
expect(passwordInput).toBeInTheDocument();
|
|
50
|
+
expect(passwordInput).toHaveAttribute('type', 'password');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('renders expiration dropdown with options', () => {
|
|
54
|
+
render(<SharedViewLink objectName="tasks" />);
|
|
55
|
+
|
|
56
|
+
const select = screen.getByRole('combobox');
|
|
57
|
+
expect(select).toBeInTheDocument();
|
|
58
|
+
|
|
59
|
+
// Verify options are present
|
|
60
|
+
const options = screen.getAllByRole('option');
|
|
61
|
+
const optionTexts = options.map(o => o.textContent);
|
|
62
|
+
expect(optionTexts).toContain('Never');
|
|
63
|
+
expect(optionTexts).toContain('1 day');
|
|
64
|
+
expect(optionTexts).toContain('7 days');
|
|
65
|
+
expect(optionTexts).toContain('30 days');
|
|
66
|
+
expect(optionTexts).toContain('90 days');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('shows "Password protected" badge after generating link with a password', async () => {
|
|
70
|
+
render(<SharedViewLink objectName="tasks" baseUrl="https://example.com" />);
|
|
71
|
+
|
|
72
|
+
// Type a password
|
|
73
|
+
const passwordInput = screen.getByPlaceholderText('Enter password...');
|
|
74
|
+
fireEvent.change(passwordInput, { target: { value: 'secret123' } });
|
|
75
|
+
|
|
76
|
+
// Click Generate Link
|
|
77
|
+
const generateBtn = screen.getByText('Generate Link');
|
|
78
|
+
fireEvent.click(generateBtn);
|
|
79
|
+
|
|
80
|
+
// After link is generated, "Password protected" badge should appear
|
|
81
|
+
await waitFor(() => {
|
|
82
|
+
expect(screen.getByText('Password protected')).toBeInTheDocument();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('shows expiration badge after generating link with expiration set', async () => {
|
|
87
|
+
render(<SharedViewLink objectName="tasks" baseUrl="https://example.com" />);
|
|
88
|
+
|
|
89
|
+
// Select 7 days expiration
|
|
90
|
+
const select = screen.getByRole('combobox');
|
|
91
|
+
fireEvent.change(select, { target: { value: '7' } });
|
|
92
|
+
|
|
93
|
+
// Click Generate Link
|
|
94
|
+
const generateBtn = screen.getByText('Generate Link');
|
|
95
|
+
fireEvent.click(generateBtn);
|
|
96
|
+
|
|
97
|
+
// After link is generated, expiration badge should appear
|
|
98
|
+
await waitFor(() => {
|
|
99
|
+
expect(screen.getByText(/Expires in 7 days/)).toBeInTheDocument();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('shows singular "day" for 1 day expiration', async () => {
|
|
104
|
+
render(<SharedViewLink objectName="tasks" baseUrl="https://example.com" />);
|
|
105
|
+
|
|
106
|
+
const select = screen.getByRole('combobox');
|
|
107
|
+
fireEvent.change(select, { target: { value: '1' } });
|
|
108
|
+
|
|
109
|
+
const generateBtn = screen.getByText('Generate Link');
|
|
110
|
+
fireEvent.click(generateBtn);
|
|
111
|
+
|
|
112
|
+
await waitFor(() => {
|
|
113
|
+
expect(screen.getByText(/Expires in 1 day$/)).toBeInTheDocument();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('calls onShare callback with password and expiresAt options', async () => {
|
|
118
|
+
const onShare = vi.fn();
|
|
119
|
+
render(
|
|
120
|
+
<SharedViewLink
|
|
121
|
+
objectName="tasks"
|
|
122
|
+
baseUrl="https://example.com"
|
|
123
|
+
onShare={onShare}
|
|
124
|
+
/>
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// Set password
|
|
128
|
+
const passwordInput = screen.getByPlaceholderText('Enter password...');
|
|
129
|
+
fireEvent.change(passwordInput, { target: { value: 'mypass' } });
|
|
130
|
+
|
|
131
|
+
// Set expiration to 30 days
|
|
132
|
+
const select = screen.getByRole('combobox');
|
|
133
|
+
fireEvent.change(select, { target: { value: '30' } });
|
|
134
|
+
|
|
135
|
+
// Generate
|
|
136
|
+
const generateBtn = screen.getByText('Generate Link');
|
|
137
|
+
fireEvent.click(generateBtn);
|
|
138
|
+
|
|
139
|
+
expect(onShare).toHaveBeenCalledTimes(1);
|
|
140
|
+
const [url, options] = onShare.mock.calls[0];
|
|
141
|
+
|
|
142
|
+
// URL should contain share path
|
|
143
|
+
expect(url).toContain('/share/tasks/');
|
|
144
|
+
expect(url).toContain('token=');
|
|
145
|
+
|
|
146
|
+
// Options should include password and expiresAt
|
|
147
|
+
expect(options.password).toBe('mypass');
|
|
148
|
+
expect(options.expiresAt).toBeDefined();
|
|
149
|
+
// expiresAt should be a valid ISO date string roughly 30 days from now
|
|
150
|
+
const expiresAt = new Date(options.expiresAt);
|
|
151
|
+
expect(expiresAt.getTime()).toBeGreaterThan(Date.now());
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('calls onShare without password and expiresAt when neither is set', () => {
|
|
155
|
+
const onShare = vi.fn();
|
|
156
|
+
render(
|
|
157
|
+
<SharedViewLink
|
|
158
|
+
objectName="tasks"
|
|
159
|
+
baseUrl="https://example.com"
|
|
160
|
+
onShare={onShare}
|
|
161
|
+
/>
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const generateBtn = screen.getByText('Generate Link');
|
|
165
|
+
fireEvent.click(generateBtn);
|
|
166
|
+
|
|
167
|
+
expect(onShare).toHaveBeenCalledTimes(1);
|
|
168
|
+
const [, options] = onShare.mock.calls[0];
|
|
169
|
+
expect(options.password).toBeUndefined();
|
|
170
|
+
expect(options.expiresAt).toBeUndefined();
|
|
171
|
+
});
|
|
172
|
+
});
|