|
| 1 | +/* |
| 2 | + * Copyright 2023 Adobe, Inc. |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | + |
| 17 | +import DateTimeInput from '../../src/components/DateTimeInput'; |
| 18 | +import userEvent from '@testing-library/user-event'; |
| 19 | +import { fireEvent, waitFor } from '@testing-library/react'; |
| 20 | +import { renderComponent, DEFAULT_ERROR_MESSAGE } from '../utils'; |
| 21 | +import '@testing-library/jest-dom/extend-expect'; |
| 22 | + |
| 23 | +const field = { |
| 24 | + name: 'datetime1', |
| 25 | + label: { value: 'Date and Time', visible: true }, |
| 26 | + fieldType: 'datetime-input', |
| 27 | + type: 'string', |
| 28 | + format: 'date-time', |
| 29 | + visible: true, |
| 30 | + required: true, |
| 31 | + enabled: true, |
| 32 | + readOnly: false, |
| 33 | +}; |
| 34 | + |
| 35 | +// Full CRISP JSON with top-level minimum/maximum (af-core 0.22.94+ preserves and validates these). |
| 36 | +const fullField = { |
| 37 | + id: 'datetime-6a4986a577', |
| 38 | + fieldType: 'datetime-input', |
| 39 | + name: 'datetime1780381446201', |
| 40 | + visible: true, |
| 41 | + description: '<p>long</p>', |
| 42 | + tooltip: '<p>short</p>', |
| 43 | + type: 'string', |
| 44 | + required: true, |
| 45 | + enabled: true, |
| 46 | + constraintMessages: { required: 'wrong date', minimum: 'minimum date', maximum: 'max date' }, |
| 47 | + readOnly: false, |
| 48 | + default: '2026-05-06 12:30:25', |
| 49 | + format: 'date-time', |
| 50 | + label: { visible: true, value: 'Date and Time' }, |
| 51 | + minimum: '2025-11-04T12:55', |
| 52 | + maximum: '2026-06-27T12:55', |
| 53 | + properties: { |
| 54 | + 'afs:layout': { tooltipVisible: false }, |
| 55 | + 'fd:path': '/content/forms/af/table-component/jcr:content/guideContainer/datetime', |
| 56 | + }, |
| 57 | +}; |
| 58 | + |
| 59 | +const helper = renderComponent(DateTimeInput); |
| 60 | + |
| 61 | +describe('DateTimeInput', () => { |
| 62 | + |
| 63 | + test('value changed by user is set in model', async () => { |
| 64 | + const { renderResponse, element } = await helper(field); |
| 65 | + const input = renderResponse.container.querySelector('input[type="datetime-local"]') as HTMLInputElement; |
| 66 | + fireEvent.change(input, { target: { value: '2024-06-15T10:30' } }); |
| 67 | + expect(element.getState().value).toEqual('2024-06-15T10:30'); |
| 68 | + }); |
| 69 | + |
| 70 | + test('it should handle visible property', async () => { |
| 71 | + const f = { ...field, visible: false }; |
| 72 | + const { renderResponse } = await helper(f); |
| 73 | + expect(renderResponse.queryByText(field.label.value)).toBeNull(); |
| 74 | + }); |
| 75 | + |
| 76 | + test('error message element exists when the field is invalid', async () => { |
| 77 | + const f = { ...field, valid: false, errorMessage: DEFAULT_ERROR_MESSAGE }; |
| 78 | + const { renderResponse } = await helper(f); |
| 79 | + expect(renderResponse.queryByText(DEFAULT_ERROR_MESSAGE)).not.toBeNull(); |
| 80 | + }); |
| 81 | + |
| 82 | + test('labels and inputs are linked with for and id attribute', async () => { |
| 83 | + const { renderResponse } = await helper(field); |
| 84 | + const input = renderResponse.container.querySelector('input[type="datetime-local"]') as HTMLInputElement; |
| 85 | + const label = renderResponse.queryByText(field.label.value); |
| 86 | + expect(input?.getAttribute('id')).toEqual(label?.getAttribute('for')); |
| 87 | + }); |
| 88 | + |
| 89 | + test('input renders with datetime-local type', async () => { |
| 90 | + const { renderResponse } = await helper(field); |
| 91 | + const input = renderResponse.container.querySelector('input[type="datetime-local"]'); |
| 92 | + expect(input).not.toBeNull(); |
| 93 | + }); |
| 94 | + |
| 95 | + test('disabled attribute is set when enabled is false', async () => { |
| 96 | + const f = { ...field, enabled: false }; |
| 97 | + const { renderResponse } = await helper(f); |
| 98 | + const input = renderResponse.container.querySelector('input[type="datetime-local"]') as HTMLInputElement; |
| 99 | + expect(input).toBeDisabled(); |
| 100 | + }); |
| 101 | + |
| 102 | + test('default value with space separator is normalised to datetime-local format', async () => { |
| 103 | + // af-core surfaces default as "YYYY-MM-DD HH:MM:SS"; datetime-local requires "YYYY-MM-DDTHH:MM" |
| 104 | + const { renderResponse } = await helper(fullField); |
| 105 | + const input = renderResponse.container.querySelector('input[type="datetime-local"]') as HTMLInputElement; |
| 106 | + expect(input.value).toEqual('2026-05-06T12:30'); |
| 107 | + }); |
| 108 | + |
| 109 | + test('min and max HTML attributes are set from JSON minimum/maximum', async () => { |
| 110 | + const { renderResponse } = await helper(fullField); |
| 111 | + const input = renderResponse.container.querySelector('input[type="datetime-local"]') as HTMLInputElement; |
| 112 | + expect(input).toHaveAttribute('min', '2025-11-04T12:55'); |
| 113 | + expect(input).toHaveAttribute('max', '2026-06-27T12:55'); |
| 114 | + }); |
| 115 | + |
| 116 | + test('minimum constraint message is shown when value is below minimum', async () => { |
| 117 | + const f = { |
| 118 | + ...field, |
| 119 | + minimum: '2025-11-04T12:55', |
| 120 | + maximum: '2026-06-27T12:55', |
| 121 | + constraintMessages: { minimum: 'minimum date', maximum: 'max date' }, |
| 122 | + }; |
| 123 | + const { renderResponse } = await helper(f); |
| 124 | + const input = renderResponse.container.querySelector('input[type="datetime-local"]') as HTMLInputElement; |
| 125 | + fireEvent.change(input, { target: { value: '2020-01-01T10:00' } }); |
| 126 | + fireEvent.blur(input); |
| 127 | + await waitFor(() => { |
| 128 | + expect(renderResponse.queryByText('minimum date')).not.toBeNull(); |
| 129 | + }); |
| 130 | + }); |
| 131 | + |
| 132 | + test('maximum constraint message is shown when value is above maximum', async () => { |
| 133 | + const f = { |
| 134 | + ...field, |
| 135 | + minimum: '2025-11-04T12:55', |
| 136 | + maximum: '2026-06-27T12:55', |
| 137 | + constraintMessages: { minimum: 'minimum date', maximum: 'max date' }, |
| 138 | + }; |
| 139 | + const { renderResponse } = await helper(f); |
| 140 | + const input = renderResponse.container.querySelector('input[type="datetime-local"]') as HTMLInputElement; |
| 141 | + fireEvent.change(input, { target: { value: '2027-01-01T10:00' } }); |
| 142 | + fireEvent.blur(input); |
| 143 | + await waitFor(() => { |
| 144 | + expect(renderResponse.queryByText('max date')).not.toBeNull(); |
| 145 | + }); |
| 146 | + }); |
| 147 | + |
| 148 | + test('tooltip and description toggle correctly', async () => { |
| 149 | + const f = { |
| 150 | + ...field, |
| 151 | + tooltip: 'Short Description', |
| 152 | + description: 'Long Description', |
| 153 | + properties: { 'afs:layout': { tooltipVisible: true } }, |
| 154 | + }; |
| 155 | + const { renderResponse } = await helper(f); |
| 156 | + expect(renderResponse.getByText('Short Description')).not.toBeNull(); |
| 157 | + const button = renderResponse.container.getElementsByClassName('cmp-adaptiveform-datetimeinput__questionmark'); |
| 158 | + userEvent.click(button[0]); |
| 159 | + expect(renderResponse.getByText('Long Description')).not.toBeNull(); |
| 160 | + }); |
| 161 | + |
| 162 | + test('aria-describedby contains long and short description ids when both are present', async () => { |
| 163 | + const f = { |
| 164 | + ...field, |
| 165 | + id: 'datetime-123', |
| 166 | + tooltip: 'short desc', |
| 167 | + description: 'long desc', |
| 168 | + properties: { 'afs:layout': { tooltipVisible: true } }, |
| 169 | + }; |
| 170 | + const { renderResponse } = await helper(f); |
| 171 | + const input = renderResponse.container.querySelector('input[type="datetime-local"]') as HTMLInputElement; |
| 172 | + expect(input).toHaveAttribute( |
| 173 | + 'aria-describedby', |
| 174 | + `${f.id}__longdescription ${f.id}__shortdescription` |
| 175 | + ); |
| 176 | + }); |
| 177 | + |
| 178 | + test('html in the label should be rendered for rich text', async () => { |
| 179 | + const f = { |
| 180 | + ...field, |
| 181 | + label: { value: '<strong>Date and Time</strong>', richText: true, visible: true }, |
| 182 | + }; |
| 183 | + const { renderResponse } = await helper(f); |
| 184 | + expect(renderResponse.container.innerHTML).toContain('<strong>Date and Time</strong>'); |
| 185 | + }); |
| 186 | + |
| 187 | + test('required constraint message is shown on validation failure', async () => { |
| 188 | + const f = { ...fullField, valid: false, errorMessage: 'wrong date' }; |
| 189 | + const { renderResponse } = await helper(f); |
| 190 | + expect(renderResponse.queryByText('wrong date')).not.toBeNull(); |
| 191 | + }); |
| 192 | + |
| 193 | + test('full CRISP JSON field renders without errors', async () => { |
| 194 | + const { renderResponse } = await helper(fullField); |
| 195 | + expect(renderResponse.container.querySelector('input[type="datetime-local"]')).not.toBeNull(); |
| 196 | + expect(renderResponse.queryByText('Date and Time')).not.toBeNull(); |
| 197 | + }); |
| 198 | +}); |
0 commit comments