Portal: 12-hour schedule times and dynamicAudio default on

Use TimeInput for Block start/end and show the day timeline axis with
AM/PM, so no 24-hour time is displayed in the schedule UI. New Blocks now
default dynamicAudio to true in the modal and server-side when the field
is absent; existing stored Blocks are untouched.
This commit is contained in:
NeilClack
2026-09-29 08:53:22 -05:00
parent 35558b0652
commit 179df1e252
6 changed files with 113 additions and 153 deletions
@@ -46,53 +46,50 @@ function submit() {
return submitRef.current({ formData: new FormData(form), cancel: () => {} });
}
async function typeTime(name: string, text: string) {
const input = (await page.getByRole('combobox', { name }).element()) as HTMLInputElement;
input.value = text;
input.dispatchEvent(new Event('input', { bubbles: true }));
}
describe('BlockModal', () => {
it('uses separate hour and minute selects for start and end', async () => {
it('uses a 12-hour TimeInput for start and end', async () => {
mount();
await expect.element(page.getByRole('combobox', { name: /^Start hour$/ })).toBeInTheDocument();
await expect
.element(page.getByRole('combobox', { name: /^Start minute$/ }))
.toBeInTheDocument();
await expect.element(page.getByRole('combobox', { name: /^End hour$/ })).toBeInTheDocument();
await expect.element(page.getByRole('combobox', { name: /^End minute$/ })).toBeInTheDocument();
await expect.element(page.getByRole('combobox', { name: 'Start time' })).toBeInTheDocument();
await expect.element(page.getByRole('combobox', { name: 'End time' })).toBeInTheDocument();
});
it('pre-fills the selects from the initial times', async () => {
it('pre-fills the fields from the initial times', async () => {
mount({ initialStartMinutes: 7 * 60 + 30, initialEndMinutes: 9 * 60 + 15 });
await expect.element(page.getByRole('combobox', { name: /^Start hour$/ })).toHaveValue('7');
await expect.element(page.getByRole('combobox', { name: /^Start minute$/ })).toHaveValue('30');
await expect.element(page.getByRole('combobox', { name: /^End hour$/ })).toHaveValue('9');
await expect.element(page.getByRole('combobox', { name: /^End minute$/ })).toHaveValue('15');
await expect.element(page.getByRole('combobox', { name: 'Start time' })).toHaveValue('7:30 AM');
await expect.element(page.getByRole('combobox', { name: 'End time' })).toHaveValue('9:15 AM');
});
it('uses a 12-hour clock with an AM/PM select for each time', async () => {
it('shows a 12-hour clock with AM/PM', async () => {
mount({ initialStartMinutes: 21 * 60, initialEndMinutes: 0 });
await expect.element(page.getByRole('combobox', { name: /^Start hour$/ })).toHaveValue('9');
await expect.element(page.getByRole('combobox', { name: /^Start period$/ })).toHaveValue('PM');
await expect.element(page.getByRole('combobox', { name: /^End hour$/ })).toHaveValue('12');
await expect.element(page.getByRole('combobox', { name: /^End period$/ })).toHaveValue('AM');
// No 24-hour option exists any more.
await expect
.element(page.getByRole('combobox', { name: /^Start hour$/ }))
.not.toHaveValue('21');
await expect.element(page.getByRole('combobox', { name: 'Start time' })).toHaveValue('9:00 PM');
await expect.element(page.getByRole('combobox', { name: 'End time' })).toHaveValue('12:00 AM');
});
it('recomputes the time when the AM/PM switch changes', async () => {
it('recomputes the time from typed input', async () => {
mount({ initialStartMinutes: 9 * 60, initialEndMinutes: 10 * 60 });
const period = (await page
.getByRole('combobox', { name: /^End period$/ })
.element()) as HTMLSelectElement;
period.value = 'PM';
period.dispatchEvent(new Event('change', { bubbles: true }));
await typeTime('End time', '10:00 PM');
await expect.element(page.getByText('9:00 AM – 10:00 PM')).toBeInTheDocument();
});
it('shows validation feedback for unparseable input', async () => {
mount();
await typeTime('Start time', '25:00');
await expect.element(page.getByRole('alert')).toBeInTheDocument();
});
it('shows the review times and selected proclamation titles', async () => {
mount();
@@ -113,7 +110,6 @@ describe('BlockModal', () => {
initialEndMinutes: 9 * 60
});
await page.getByRole('combobox', { name: /^Start hour$/ }).element();
submit();
await expect
@@ -124,7 +120,6 @@ describe('BlockModal', () => {
it('requires at least one proclamation', async () => {
mount({ initialProclamationIds: [] });
await page.getByRole('combobox', { name: /^Start hour$/ }).element();
submit();
await expect.element(page.getByText('Select at least one proclamation.')).toBeInTheDocument();
@@ -133,7 +128,6 @@ describe('BlockModal', () => {
it('does not show an error for a valid selection', async () => {
mount();
await page.getByRole('combobox', { name: /^Start hour$/ }).element();
const result = submit();
expect(typeof result).toBe('function');
@@ -165,21 +159,32 @@ describe('BlockModal', () => {
const checkbox = page.getByRole('checkbox', { name: /Enable Dynamic Audio/ });
await expect.element(checkbox).toBeInTheDocument();
expect((checkbox.element() as HTMLInputElement).checked).toBe(false);
await expect
.element(page.getByText(/Dynamic audio will be enabled for this block/))
.toBeInTheDocument();
});
it('defaults dynamic audio on for new blocks', async () => {
mount();
const checkbox = page.getByRole('checkbox', { name: /Enable Dynamic Audio/ });
expect((checkbox.element() as HTMLInputElement).checked).toBe(true);
});
it('pre-checks dynamic audio when editing a block that has it enabled', async () => {
mount({ initialDynamicAudio: true });
const checkbox = page.getByRole('checkbox', { name: /Enable Dynamic Audio/ });
await expect.element(checkbox).toBeInTheDocument();
expect((checkbox.element() as HTMLInputElement).checked).toBe(true);
});
it('leaves dynamic audio off when editing a block that has it disabled', async () => {
mount({ initialDynamicAudio: false });
const checkbox = page.getByRole('checkbox', { name: /Enable Dynamic Audio/ });
expect((checkbox.element() as HTMLInputElement).checked).toBe(false);
});
it('toggles the dynamic audio flag in the submitted payload', async () => {
mount();
@@ -188,6 +193,6 @@ describe('BlockModal', () => {
(checkbox.element() as HTMLInputElement).click();
const hidden = document.querySelector('input[name="dynamicAudio"]') as HTMLInputElement;
await expect.poll(() => hidden.value).toBe('true');
await expect.poll(() => hidden.value).toBe('false');
});
});