|
| 1 | +/** |
| 2 | + * ObjectUI – Copyright (c) 2024-present ObjectStack Inc. |
| 3 | + * Licensed under MIT. |
| 4 | + */ |
| 5 | + |
| 6 | +/** |
| 7 | + * A required field that carries a default is not demanded from the file. |
| 8 | + * |
| 9 | + * The create fills a declared `defaultValue` (static or an expression) and, |
| 10 | + * when none is declared, an option marked `default: true`, for a row that |
| 11 | + * leaves the field out or blank. So the mapping step does not hold Next for |
| 12 | + * such a field left unmapped, and the preview does not mark its blank cell. |
| 13 | + * A required field with no default still blocks. The wizard only asks whether |
| 14 | + * a default is declared: the expression below is never evaluated here. |
| 15 | + */ |
| 16 | +import { describe, it, expect } from 'vitest'; |
| 17 | +import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'; |
| 18 | +import '@testing-library/jest-dom'; |
| 19 | +import React from 'react'; |
| 20 | + |
| 21 | +import { ImportWizard } from './ImportWizard'; |
| 22 | + |
| 23 | +type WizardField = React.ComponentProps<typeof ImportWizard>['fields'][number]; |
| 24 | + |
| 25 | +const REVIEW_OPTIONS = [ |
| 26 | + { label: 'To review', value: 'to_review' }, |
| 27 | + { label: 'To confirm', value: 'to_confirm' }, |
| 28 | +]; |
| 29 | + |
| 30 | +/** A record-derived expression default, the shape the reported app declares. */ |
| 31 | +const EXPRESSION_DEFAULT = { dialect: 'cel', source: "record.source == 'self' ? 'to_review' : 'to_confirm'" }; |
| 32 | + |
| 33 | +const title: WizardField = { name: 'title', label: 'Title', type: 'text' }; |
| 34 | +const owner: WizardField = { name: 'owner_name', label: 'Owner', type: 'text', required: true }; |
| 35 | +const reviewStatus: WizardField = { |
| 36 | + name: 'review_status', label: 'Review status', type: 'select', required: true, |
| 37 | + defaultValue: EXPRESSION_DEFAULT, options: REVIEW_OPTIONS, |
| 38 | +}; |
| 39 | +const priority: WizardField = { |
| 40 | + name: 'priority', label: 'Priority', type: 'select', required: true, defaultValue: 'normal', |
| 41 | + options: [{ label: 'Normal', value: 'normal' }, { label: 'High', value: 'high' }], |
| 42 | +}; |
| 43 | +const stage: WizardField = { |
| 44 | + name: 'stage', label: 'Stage', type: 'select', required: true, |
| 45 | + options: [{ label: 'New', value: 'new', default: true }, { label: 'Done', value: 'done' }], |
| 46 | +}; |
| 47 | + |
| 48 | +/** Paste TSV into the upload step via the wizard's window-level handler. */ |
| 49 | +function pasteRows(text: string) { |
| 50 | + const evt = new Event('paste', { bubbles: true, cancelable: true }) as Event & { |
| 51 | + clipboardData: { getData: (type: string) => string }; |
| 52 | + }; |
| 53 | + evt.clipboardData = { getData: (type: string) => (type === 'text/plain' ? text : '') }; |
| 54 | + act(() => { window.dispatchEvent(evt); }); |
| 55 | +} |
| 56 | + |
| 57 | +function renderWizard(fields: WizardField[]) { |
| 58 | + render(<ImportWizard objectName="duty" fields={fields} dataSource={{}} open onOpenChange={() => {}} />); |
| 59 | +} |
| 60 | + |
| 61 | +describe('ImportWizard mapping step: a required field with a default', () => { |
| 62 | + it('does not hold Next for an unmapped required field with an expression default', async () => { |
| 63 | + renderWizard([title, reviewStatus]); |
| 64 | + pasteRows('Title\nAlpha'); |
| 65 | + const next = await screen.findByTestId('import-next-btn'); |
| 66 | + await waitFor(() => expect(next).toBeEnabled()); |
| 67 | + expect(screen.queryByTestId('import-missing-required')).not.toBeInTheDocument(); |
| 68 | + }); |
| 69 | + |
| 70 | + it('does not hold Next for a static default or an option marked default', async () => { |
| 71 | + renderWizard([title, priority, stage]); |
| 72 | + pasteRows('Title\nAlpha'); |
| 73 | + const next = await screen.findByTestId('import-next-btn'); |
| 74 | + await waitFor(() => expect(next).toBeEnabled()); |
| 75 | + expect(screen.queryByTestId('import-missing-required')).not.toBeInTheDocument(); |
| 76 | + }); |
| 77 | + |
| 78 | + it('still names a required field with no default, and only that one', async () => { |
| 79 | + renderWizard([title, owner, reviewStatus, priority, stage]); |
| 80 | + pasteRows('Title\nAlpha'); |
| 81 | + const hint = await screen.findByTestId('import-missing-required'); |
| 82 | + expect(hint).toHaveTextContent('Owner (owner_name)'); |
| 83 | + expect(hint).not.toHaveTextContent('review_status'); |
| 84 | + expect(hint).not.toHaveTextContent('priority'); |
| 85 | + expect(hint).not.toHaveTextContent('stage'); |
| 86 | + expect(screen.getByTestId('import-next-btn')).toBeDisabled(); |
| 87 | + }); |
| 88 | + |
| 89 | + it('a null defaultValue declares no default', async () => { |
| 90 | + renderWizard([title, { ...owner, defaultValue: null }]); |
| 91 | + pasteRows('Title\nAlpha'); |
| 92 | + expect(await screen.findByTestId('import-missing-required')).toHaveTextContent('Owner (owner_name)'); |
| 93 | + }); |
| 94 | +}); |
| 95 | + |
| 96 | +describe('ImportWizard preview: a blank cell in a required field with a default', () => { |
| 97 | + const cell = (row: number, col: number) => screen.getByTestId(`import-preview-cell-${row}-${col}`); |
| 98 | + |
| 99 | + async function openPreview(fields: WizardField[], tsv: string) { |
| 100 | + renderWizard(fields); |
| 101 | + pasteRows(tsv); |
| 102 | + const next = await screen.findByTestId('import-next-btn'); |
| 103 | + await waitFor(() => expect(next).toBeEnabled()); |
| 104 | + fireEvent.click(next); |
| 105 | + return screen.findByTestId('import-run-btn'); |
| 106 | + } |
| 107 | + |
| 108 | + it('leaves the blank cell of a defaulted required column unmarked and counts its row', async () => { |
| 109 | + const button = await openPreview([title, reviewStatus], 'Title\tReview status\nAlpha\t\nBeta\tTo confirm'); |
| 110 | + expect(cell(0, 1)).toHaveAttribute('aria-invalid', 'false'); |
| 111 | + expect(button).toHaveTextContent('Import 2 Rows'); |
| 112 | + }); |
| 113 | + |
| 114 | + it('still marks the blank cell of a required column with no default', async () => { |
| 115 | + const button = await openPreview([title, owner], 'Title\tOwner\nAlpha\t\nBeta\tAda'); |
| 116 | + expect(cell(0, 1)).toHaveAttribute('aria-invalid', 'true'); |
| 117 | + expect(button).toHaveTextContent('Import 1 Row'); |
| 118 | + }); |
| 119 | +}); |
0 commit comments