|
| 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 | +/** |
| 10 | + * An edit `ModalForm` whose record does not exist shows the record not-found |
| 11 | + * state, never an empty editable form (objectstack#29157). |
| 12 | + * |
| 13 | + * The console's `?form=ID` overlay mounts this dialog with `mode: 'edit'` and |
| 14 | + * the id from the URL. `DataSource.findOne` answers `null` for a record that is |
| 15 | + * not there (the ObjectStack adapter maps the server's 404 to it), and the |
| 16 | + * dialog used to seed the form with `{}` from that answer: a blank form with an |
| 17 | + * enabled Update button, whose submit then failed against the missing record. |
| 18 | + * |
| 19 | + * Pinned here: a `null` edit read draws the not-found state (the console's |
| 20 | + * shared `empty.recordNotFound` copy) with no field and no Update button, both |
| 21 | + * on the first read and on a background re-read that finds the record deleted. |
| 22 | + * Controls: a create and a found record still draw the form. |
| 23 | + */ |
| 24 | +import React from 'react'; |
| 25 | +import { describe, it, expect, vi } from 'vitest'; |
| 26 | +import { render, screen, waitFor, act } from '@testing-library/react'; |
| 27 | +import { notifyDataChanged } from '@object-ui/react'; |
| 28 | +import { registerAllFields } from '@object-ui/fields'; |
| 29 | +import { ModalForm, type ModalFormProps, type ModalFormSchema } from './ModalForm'; |
| 30 | + |
| 31 | +registerAllFields(); |
| 32 | + |
| 33 | +function makeDS(record: Record<string, unknown> | null) { |
| 34 | + return { |
| 35 | + getObjectSchema: vi.fn().mockResolvedValue({ |
| 36 | + name: 'showcase_category', |
| 37 | + fields: { name: { type: 'text', label: 'Name' } }, |
| 38 | + }), |
| 39 | + findOne: vi.fn().mockResolvedValue(record), |
| 40 | + find: vi.fn().mockResolvedValue({ data: [] }), |
| 41 | + create: vi.fn(async (_o: string, d: Record<string, unknown>) => ({ id: 'n1', ...d })), |
| 42 | + update: vi.fn(async (_o: string, id: string, d: Record<string, unknown>) => ({ id, ...d })), |
| 43 | + }; |
| 44 | +} |
| 45 | + |
| 46 | +function open(ds: ReturnType<typeof makeDS>, mode: 'edit' | 'create', recordId?: string) { |
| 47 | + const schema: ModalFormSchema = { |
| 48 | + type: 'object-form', |
| 49 | + formType: 'modal', |
| 50 | + objectName: 'showcase_category', |
| 51 | + mode, |
| 52 | + recordId, |
| 53 | + title: mode === 'edit' ? 'Edit Category' : 'New Category', |
| 54 | + open: true, |
| 55 | + }; |
| 56 | + render(<ModalForm schema={schema} dataSource={ds as unknown as ModalFormProps['dataSource']} />); |
| 57 | +} |
| 58 | + |
| 59 | +const nameBox = () => document.body.querySelector('input[name="name"]') as HTMLInputElement | null; |
| 60 | +const submitButton = (label: string) => screen.queryByRole('button', { name: label }); |
| 61 | + |
| 62 | +describe('ModalForm — an edit read that finds no record (objectstack#29157)', () => { |
| 63 | + it('draws the not-found state with no field and no Update button', async () => { |
| 64 | + const ds = makeDS(null); |
| 65 | + open(ds, 'edit', 'NOPE_x'); |
| 66 | + |
| 67 | + expect(await screen.findByText('Record not found')).toBeInTheDocument(); |
| 68 | + expect( |
| 69 | + screen.getByText( |
| 70 | + 'The record you are looking for does not exist, may have been deleted, or you may not have access to it.', |
| 71 | + ), |
| 72 | + ).toBeInTheDocument(); |
| 73 | + expect(ds.findOne).toHaveBeenCalledWith('showcase_category', 'NOPE_x'); |
| 74 | + expect(nameBox()).toBeNull(); |
| 75 | + expect(submitButton('Update')).toBeNull(); |
| 76 | + // Not the generic load-failure screen: nothing failed to load. |
| 77 | + expect(screen.queryByText('Error loading form')).toBeNull(); |
| 78 | + }); |
| 79 | + |
| 80 | + it('draws the not-found state when a background re-read finds the record deleted', async () => { |
| 81 | + const ds = makeDS({ id: 'c1', name: 'Books' }); |
| 82 | + open(ds, 'edit', 'c1'); |
| 83 | + await waitFor(() => expect(nameBox()?.value).toBe('Books')); |
| 84 | + |
| 85 | + ds.findOne.mockResolvedValue(null); |
| 86 | + await act(async () => { |
| 87 | + notifyDataChanged({ objectName: 'showcase_category', recordId: 'c1' }); |
| 88 | + }); |
| 89 | + |
| 90 | + expect(await screen.findByText('Record not found')).toBeInTheDocument(); |
| 91 | + expect(nameBox()).toBeNull(); |
| 92 | + expect(submitButton('Update')).toBeNull(); |
| 93 | + }); |
| 94 | + |
| 95 | + it('control: a found record still draws its form and the Update button', async () => { |
| 96 | + const ds = makeDS({ id: 'c1', name: 'Books' }); |
| 97 | + open(ds, 'edit', 'c1'); |
| 98 | + |
| 99 | + await waitFor(() => expect(nameBox()?.value).toBe('Books')); |
| 100 | + expect(submitButton('Update')).not.toBeNull(); |
| 101 | + expect(screen.queryByText('Record not found')).toBeNull(); |
| 102 | + }); |
| 103 | + |
| 104 | + it('control: a create draws an empty form and the Create button, with no read', async () => { |
| 105 | + const ds = makeDS(null); |
| 106 | + open(ds, 'create'); |
| 107 | + |
| 108 | + await waitFor(() => expect(nameBox()).not.toBeNull()); |
| 109 | + expect(submitButton('Create')).not.toBeNull(); |
| 110 | + expect(ds.findOne).not.toHaveBeenCalled(); |
| 111 | + expect(screen.queryByText('Record not found')).toBeNull(); |
| 112 | + }); |
| 113 | +}); |
0 commit comments