From 162e136aaa96ee8bbc7d41b14179b6b29ac6aa92 Mon Sep 17 00:00:00 2001 From: Jackie Jou Date: Wed, 1 Jul 2026 18:36:26 -0700 Subject: [PATCH 1/3] test(activity-feed-v2): add TaskModalV2 stories and a11y visual tests Co-authored-by: Cursor --- .../stories/TaskModalV2.stories.tsx | 43 ++++ .../stories/__mocks__/TaskModalV2Mocks.tsx | 145 +++++++++++++ .../tests/TaskModalV2-visual.stories.tsx | 195 ++++++++++++++++++ 3 files changed, 383 insertions(+) create mode 100644 src/elements/content-sidebar/stories/TaskModalV2.stories.tsx create mode 100644 src/elements/content-sidebar/stories/__mocks__/TaskModalV2Mocks.tsx create mode 100644 src/elements/content-sidebar/stories/tests/TaskModalV2-visual.stories.tsx diff --git a/src/elements/content-sidebar/stories/TaskModalV2.stories.tsx b/src/elements/content-sidebar/stories/TaskModalV2.stories.tsx new file mode 100644 index 0000000000..e72a55464a --- /dev/null +++ b/src/elements/content-sidebar/stories/TaskModalV2.stories.tsx @@ -0,0 +1,43 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { InteractiveTaskModal, mockEditingAssignees, mockEditingTask } from './__mocks__/TaskModalV2Mocks'; + +import { TASK_TYPE_APPROVAL, TASK_TYPE_GENERAL } from '../../../constants'; + +const meta: Meta = { + title: 'Elements/ContentSidebar/TaskModalV2', + component: InteractiveTaskModal, + parameters: { + // Modal portals to document.body, so inline docs previews would stack every story's open dialog + docs: { story: { iframeHeight: 640, inline: false } }, + }, +}; + +export default meta; + +export const CreateGeneralTask: StoryObj = { + args: { + taskType: TASK_TYPE_GENERAL, + }, +}; + +export const CreateApprovalTask: StoryObj = { + args: { + taskType: TASK_TYPE_APPROVAL, + }, +}; + +export const EditApprovalTask: StoryObj = { + args: { + editingAssignees: mockEditingAssignees, + editingTask: mockEditingTask, + taskType: TASK_TYPE_APPROVAL, + }, +}; + +export const SubmitError: StoryObj = { + args: { + shouldFailSubmit: true, + taskType: TASK_TYPE_APPROVAL, + }, +}; diff --git a/src/elements/content-sidebar/stories/__mocks__/TaskModalV2Mocks.tsx b/src/elements/content-sidebar/stories/__mocks__/TaskModalV2Mocks.tsx new file mode 100644 index 0000000000..63358d0ab1 --- /dev/null +++ b/src/elements/content-sidebar/stories/__mocks__/TaskModalV2Mocks.tsx @@ -0,0 +1,145 @@ +import * as React from 'react'; + +import { BlueprintModernizationProvider, TooltipProvider } from '@box/blueprint-web'; +import type { FetchedAvatarUrls, UserContactType } from '@box/user-selector'; + +import TaskModalV2 from '../../activity-feed-v2/task-modal-v2'; + +import type { ElementsXhrError } from '../../../../common/types/api'; +import type { TaskNew, TaskType } from '../../../../common/types/tasks'; +import type { CreateTaskCallback, EditTaskCallback, TaskAssignee } from '../../activity-feed-v2/task-modal-v2/types'; + +import { TASK_COMPLETION_RULE_ALL, TASK_TYPE_APPROVAL } from '../../../../constants'; + +export const mockContacts: UserContactType[] = [ + { email: 'awong@example.com', id: 1, name: 'Alice Wong', type: 'user', value: '1' }, + { email: 'bsmith@example.com', id: 2, name: 'Bob Smith', type: 'user', value: '2' }, + { email: 'cnguyen@example.com', id: 3, name: 'Charlie Nguyen', type: 'user', value: '3' }, + { email: '', id: 100, name: 'Design Team', type: 'group', value: '100' }, + { email: '', id: 101, name: 'Engineering Team', type: 'group', value: '101' }, +]; + +export const mockFetchUsers = (query: string): Promise => + Promise.resolve(mockContacts.filter(contact => contact.name.toLowerCase().includes(query.toLowerCase()))); + +export const mockFetchAvatarUrls = (): Promise => Promise.resolve({}); + +const buildUserAssignee = (collaboratorId: string, id: string, name: string, email: string): TaskAssignee => ({ + id: collaboratorId, + permissions: { can_delete: false, can_update: false }, + role: 'ASSIGNEE', + status: 'NOT_STARTED', + target: { email, id, name, type: 'user' }, + type: 'task_collaborator', +}); + +const buildGroupAssignee = (collaboratorId: string, id: string, name: string): TaskAssignee => ({ + id: collaboratorId, + permissions: { can_delete: false, can_update: false }, + role: 'ASSIGNEE', + status: 'NOT_STARTED', + target: { id, name, type: 'group' }, + type: 'task_collaborator', +}); + +export const mockEditingAssignees: TaskAssignee[] = [ + buildUserAssignee('collab-1', '1', 'Alice Wong', 'awong@example.com'), + buildGroupAssignee('collab-2', '101', 'Engineering Team'), +]; + +export const mockEditingTask: TaskNew = { + assigned_to: { entries: mockEditingAssignees, limit: 25, next_marker: '' }, + completion_rule: TASK_COMPLETION_RULE_ALL, + created_at: '2026-06-30T12:00:00Z', + created_by: { + id: 'creator', + role: 'CREATOR', + status: 'NOT_STARTED', + target: { id: 'creator', name: 'Creator', type: 'user' }, + type: 'task_collaborator', + }, + description: 'Review the updated launch checklist', + due_at: '2026-07-15T23:59:59Z', + id: 'task-1', + modified_at: '2026-06-30T12:00:00Z', + permissions: { + can_create_task_collaborator: true, + can_create_task_link: true, + can_delete: true, + can_update: true, + }, + status: 'NOT_STARTED', + task_links: { entries: [], limit: 25, next_marker: '' }, + task_type: TASK_TYPE_APPROVAL, + type: 'task', +}; + +export type InteractiveTaskModalProps = { + editingAssignees?: TaskAssignee[]; + editingTask?: TaskNew; + shouldFailSubmit?: boolean; + taskType: TaskType; +}; + +export const InteractiveTaskModal = ({ + editingAssignees = [], + editingTask, + shouldFailSubmit = false, + taskType, +}: InteractiveTaskModalProps) => { + const [error, setError] = React.useState(); + const [isOpen, setIsOpen] = React.useState(true); + + const handleClose = () => { + setError(undefined); + setIsOpen(false); + }; + + const finishSubmit = (onSuccess: () => void, onError: (submitError: ElementsXhrError) => void) => { + setTimeout(() => { + if (shouldFailSubmit) { + onError({ status: 500 } as ElementsXhrError); + return; + } + onSuccess(); + }, 400); + }; + + const createTask: CreateTaskCallback = (text, approvers, type, dueDate, completionRule, onSuccess, onError) => + finishSubmit(onSuccess, onError); + + const editTask: EditTaskCallback = (payload, onSuccess, onError) => finishSubmit(onSuccess, onError); + + const sharedProps = { + createTask, + error, + fetchAvatarUrls: mockFetchAvatarUrls, + fetchUsers: mockFetchUsers, + isOpen, + onClose: handleClose, + onSubmitError: setError, + onSubmitSuccess: handleClose, + taskType, + }; + + return ( + + + + {editingTask ? ( + + ) : ( + + )} + + + ); +}; diff --git a/src/elements/content-sidebar/stories/tests/TaskModalV2-visual.stories.tsx b/src/elements/content-sidebar/stories/tests/TaskModalV2-visual.stories.tsx new file mode 100644 index 0000000000..b667f8819b --- /dev/null +++ b/src/elements/content-sidebar/stories/tests/TaskModalV2-visual.stories.tsx @@ -0,0 +1,195 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { expect, screen, userEvent, waitFor } from 'storybook/test'; + +import { InteractiveTaskModal, mockEditingAssignees, mockEditingTask } from '../__mocks__/TaskModalV2Mocks'; + +import { TASK_TYPE_APPROVAL, TASK_TYPE_GENERAL } from '../../../../constants'; + +const meta: Meta = { + title: 'Elements/ContentSidebar/TaskModalV2/tests/visual-regression-tests', + component: InteractiveTaskModal, +}; + +export default meta; + +const findAssigneeCombobox = async () => screen.findByRole('combobox', { name: 'Select Assignees' }); + +export const CreateGeneralTaskAccessibleStructure: StoryObj = { + args: { + taskType: TASK_TYPE_GENERAL, + }, + play: async () => { + const dialog = await screen.findByRole('dialog', { name: 'Create General Task' }); + expect(dialog).toBeVisible(); + + const combobox = await findAssigneeCombobox(); + expect(combobox).toBeVisible(); + expect(await screen.findByRole('textbox', { name: /message/i })).toBeVisible(); + expect(screen.getByRole('button', { name: 'Cancel' })).toBeVisible(); + expect(screen.getByRole('button', { name: 'Create' })).toBeVisible(); + expect(screen.getByRole('button', { name: 'Close' })).toBeVisible(); + + // Completion rule only appears once an assignee is selected + expect(screen.queryByRole('checkbox')).not.toBeInTheDocument(); + }, +}; + +export const CreateApprovalTaskTabOrder: StoryObj = { + args: { + taskType: TASK_TYPE_APPROVAL, + }, + play: async () => { + await screen.findByRole('dialog', { name: 'Create Approval Task' }); + + const combobox = await findAssigneeCombobox(); + combobox.focus(); + expect(combobox).toHaveFocus(); + + await userEvent.tab(); + expect(screen.getByRole('textbox', { name: /message/i })).toHaveFocus(); + + await userEvent.tab(); + expect(screen.getByRole('spinbutton', { name: /month/i })).toHaveFocus(); + + await userEvent.tab(); + expect(screen.getByRole('spinbutton', { name: /day/i })).toHaveFocus(); + + await userEvent.tab(); + expect(screen.getByRole('spinbutton', { name: /year/i })).toHaveFocus(); + + await userEvent.tab(); + expect(screen.getByRole('button', { name: /open due date calendar/i })).toHaveFocus(); + + await userEvent.tab(); + expect(screen.getByRole('button', { name: 'Cancel' })).toHaveFocus(); + + await userEvent.tab(); + expect(screen.getByRole('button', { name: 'Create' })).toHaveFocus(); + }, +}; + +export const AssigneeListboxAccessibleNames: StoryObj = { + args: { + taskType: TASK_TYPE_APPROVAL, + }, + play: async () => { + await screen.findByRole('dialog', { name: 'Create Approval Task' }); + + const combobox = await findAssigneeCombobox(); + await userEvent.type(combobox, 'team'); + + const listbox = await screen.findByRole('listbox'); + expect(listbox).toBeVisible(); + + const designOption = await screen.findByRole('option', { name: /Design Team/ }); + const engineeringOption = await screen.findByRole('option', { name: /Engineering Team/ }); + expect(designOption).toBeVisible(); + expect(engineeringOption).toBeVisible(); + }, +}; + +export const CompletionRuleShownAfterSelection: StoryObj = { + args: { + taskType: TASK_TYPE_APPROVAL, + }, + play: async () => { + await screen.findByRole('dialog', { name: 'Create Approval Task' }); + + const combobox = await findAssigneeCombobox(); + await userEvent.type(combobox, 'Alice'); + const aliceOption = await screen.findByRole('option', { name: /Alice Wong/ }); + await userEvent.click(aliceOption); + + // A single user assignee shows the completion rule disabled; adding a group enables it + const checkbox = await screen.findByRole('checkbox', { + name: 'Only one assignee is required to complete this task', + }); + expect(checkbox).toBeVisible(); + expect(checkbox).toBeDisabled(); + + await userEvent.type(combobox, 'Engineering'); + const groupOption = await screen.findByRole('option', { name: /Engineering Team/ }); + await userEvent.click(groupOption); + + await waitFor(() => + expect( + screen.getByRole('checkbox', { name: 'Only one assignee is required to complete this task' }), + ).toBeEnabled(), + ); + }, +}; + +export const EditApprovalTaskPrefill: StoryObj = { + args: { + editingAssignees: mockEditingAssignees, + editingTask: mockEditingTask, + taskType: TASK_TYPE_APPROVAL, + }, + play: async () => { + const dialog = await screen.findByRole('dialog', { name: 'Modify Approval Task' }); + expect(dialog).toBeVisible(); + + expect(await screen.findByRole('textbox', { name: /message/i })).toHaveValue( + 'Review the updated launch checklist', + ); + expect(screen.getAllByText('Alice Wong').length).toBeGreaterThan(0); + expect(screen.getAllByText('Engineering Team').length).toBeGreaterThan(0); + expect(screen.getByRole('button', { name: 'Update' })).toBeVisible(); + }, +}; + +export const EditGeneralTaskPrefill: StoryObj = { + args: { + editingAssignees: mockEditingAssignees, + editingTask: { ...mockEditingTask, task_type: TASK_TYPE_GENERAL }, + taskType: TASK_TYPE_GENERAL, + }, + play: async () => { + const dialog = await screen.findByRole('dialog', { name: 'Modify General Task' }); + expect(dialog).toBeVisible(); + + expect(await screen.findByRole('textbox', { name: /message/i })).toHaveValue( + 'Review the updated launch checklist', + ); + expect(screen.getByRole('button', { name: 'Update' })).toBeVisible(); + }, +}; + +export const RequiredFieldValidationErrors: StoryObj = { + args: { + taskType: TASK_TYPE_APPROVAL, + }, + play: async () => { + await screen.findByRole('dialog', { name: 'Create Approval Task' }); + await findAssigneeCombobox(); + + await userEvent.click(screen.getByRole('button', { name: 'Create' })); + + // Both the assignee and message fields are required + const requiredFieldErrors = await screen.findAllByText('Required Field'); + expect(requiredFieldErrors.length).toBeGreaterThanOrEqual(2); + + // The modal stays open so the user can correct the form + expect(screen.getByRole('dialog', { name: 'Create Approval Task' })).toBeVisible(); + }, +}; + +export const SubmitErrorNotice: StoryObj = { + args: { + shouldFailSubmit: true, + taskType: TASK_TYPE_APPROVAL, + }, + play: async () => { + await screen.findByRole('dialog', { name: 'Create Approval Task' }); + + const combobox = await findAssigneeCombobox(); + await userEvent.type(combobox, 'Alice'); + const aliceOption = await screen.findByRole('option', { name: /Alice Wong/ }); + await userEvent.click(aliceOption); + + await userEvent.type(screen.getByRole('textbox', { name: /message/i }), 'Please review'); + await userEvent.click(screen.getByRole('button', { name: 'Create' })); + + expect(await screen.findByText('An error occurred while creating this task. Please try again.')).toBeVisible(); + }, +}; From 6f21d0681ae7b1d81a3aa2d4c1cacbe0f61c2a37 Mon Sep 17 00:00:00 2001 From: Jackie Jou Date: Thu, 2 Jul 2026 13:14:51 -0700 Subject: [PATCH 2/3] test(activity-feed-v2): address TaskModalV2 story review feedback Seed a submit error so the SubmitError story renders the notice, assert due-date prefill and completion-rule state in the edit prefill test with a timezone-safe midday-UTC due_at, assert per-field invalid state in the validation test, disable inline docs previews for the visual test file, and use the Blueprint Button in the story wrapper. --- .../stories/TaskModalV2.stories.tsx | 8 +++++++- .../stories/__mocks__/TaskModalV2Mocks.tsx | 17 ++++++++++------ .../tests/TaskModalV2-visual.stories.tsx | 20 +++++++++++++++++-- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/elements/content-sidebar/stories/TaskModalV2.stories.tsx b/src/elements/content-sidebar/stories/TaskModalV2.stories.tsx index e72a55464a..b5f361520b 100644 --- a/src/elements/content-sidebar/stories/TaskModalV2.stories.tsx +++ b/src/elements/content-sidebar/stories/TaskModalV2.stories.tsx @@ -1,6 +1,11 @@ import type { Meta, StoryObj } from '@storybook/react'; -import { InteractiveTaskModal, mockEditingAssignees, mockEditingTask } from './__mocks__/TaskModalV2Mocks'; +import { + InteractiveTaskModal, + mockEditingAssignees, + mockEditingTask, + mockSubmitError, +} from './__mocks__/TaskModalV2Mocks'; import { TASK_TYPE_APPROVAL, TASK_TYPE_GENERAL } from '../../../constants'; @@ -37,6 +42,7 @@ export const EditApprovalTask: StoryObj = { export const SubmitError: StoryObj = { args: { + initialError: mockSubmitError, shouldFailSubmit: true, taskType: TASK_TYPE_APPROVAL, }, diff --git a/src/elements/content-sidebar/stories/__mocks__/TaskModalV2Mocks.tsx b/src/elements/content-sidebar/stories/__mocks__/TaskModalV2Mocks.tsx index 63358d0ab1..16869c6c5b 100644 --- a/src/elements/content-sidebar/stories/__mocks__/TaskModalV2Mocks.tsx +++ b/src/elements/content-sidebar/stories/__mocks__/TaskModalV2Mocks.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import { BlueprintModernizationProvider, TooltipProvider } from '@box/blueprint-web'; +import { BlueprintModernizationProvider, Button, TooltipProvider } from '@box/blueprint-web'; import type { FetchedAvatarUrls, UserContactType } from '@box/user-selector'; import TaskModalV2 from '../../activity-feed-v2/task-modal-v2'; @@ -42,6 +42,8 @@ const buildGroupAssignee = (collaboratorId: string, id: string, name: string): T type: 'task_collaborator', }); +export const mockSubmitError: ElementsXhrError = { status: 500 } as ElementsXhrError; + export const mockEditingAssignees: TaskAssignee[] = [ buildUserAssignee('collab-1', '1', 'Alice Wong', 'awong@example.com'), buildGroupAssignee('collab-2', '101', 'Engineering Team'), @@ -59,7 +61,8 @@ export const mockEditingTask: TaskNew = { type: 'task_collaborator', }, description: 'Review the updated launch checklist', - due_at: '2026-07-15T23:59:59Z', + // Midday UTC so the local calendar date stays July 15 in any test timezone + due_at: '2026-07-15T12:00:00Z', id: 'task-1', modified_at: '2026-06-30T12:00:00Z', permissions: { @@ -77,6 +80,7 @@ export const mockEditingTask: TaskNew = { export type InteractiveTaskModalProps = { editingAssignees?: TaskAssignee[]; editingTask?: TaskNew; + initialError?: ElementsXhrError; shouldFailSubmit?: boolean; taskType: TaskType; }; @@ -84,10 +88,11 @@ export type InteractiveTaskModalProps = { export const InteractiveTaskModal = ({ editingAssignees = [], editingTask, + initialError, shouldFailSubmit = false, taskType, }: InteractiveTaskModalProps) => { - const [error, setError] = React.useState(); + const [error, setError] = React.useState(initialError); const [isOpen, setIsOpen] = React.useState(true); const handleClose = () => { @@ -98,7 +103,7 @@ export const InteractiveTaskModal = ({ const finishSubmit = (onSuccess: () => void, onError: (submitError: ElementsXhrError) => void) => { setTimeout(() => { if (shouldFailSubmit) { - onError({ status: 500 } as ElementsXhrError); + onError(mockSubmitError); return; } onSuccess(); @@ -125,9 +130,9 @@ export const InteractiveTaskModal = ({ return ( - + {editingTask ? ( = { title: 'Elements/ContentSidebar/TaskModalV2/tests/visual-regression-tests', component: InteractiveTaskModal, + parameters: { + docs: { story: { iframeHeight: 640, inline: false } }, + }, }; export default meta; @@ -132,8 +135,19 @@ export const EditApprovalTaskPrefill: StoryObj = { expect(await screen.findByRole('textbox', { name: /message/i })).toHaveValue( 'Review the updated launch checklist', ); - expect(screen.getAllByText('Alice Wong').length).toBeGreaterThan(0); - expect(screen.getAllByText('Engineering Team').length).toBeGreaterThan(0); + expect(screen.getAllByText('Alice Wong')[0]).toBeVisible(); + expect(screen.getAllByText('Engineering Team')[0]).toBeVisible(); + + expect(screen.getByRole('spinbutton', { name: /month/i })).toHaveAttribute('aria-valuenow', '7'); + expect(screen.getByRole('spinbutton', { name: /day/i })).toHaveAttribute('aria-valuenow', '15'); + expect(screen.getByRole('spinbutton', { name: /year/i })).toHaveAttribute('aria-valuenow', '2026'); + + const completionRuleCheckbox = screen.getByRole('checkbox', { + name: 'Only one assignee is required to complete this task', + }); + expect(completionRuleCheckbox).toBeEnabled(); + expect(completionRuleCheckbox).not.toBeChecked(); + expect(screen.getByRole('button', { name: 'Update' })).toBeVisible(); }, }; @@ -168,6 +182,8 @@ export const RequiredFieldValidationErrors: StoryObj Date: Thu, 2 Jul 2026 15:20:29 -0700 Subject: [PATCH 3/3] test(activity-feed-v2): trim TaskModalV2 VRTs to browser-only flows Keep the tab-order and open-listbox stories, which need a real browser. Static end states and validation wiring are covered by the public TaskModalV2 stories and the jest suites. --- .../tests/TaskModalV2-visual.stories.tsx | 145 +----------------- 1 file changed, 3 insertions(+), 142 deletions(-) diff --git a/src/elements/content-sidebar/stories/tests/TaskModalV2-visual.stories.tsx b/src/elements/content-sidebar/stories/tests/TaskModalV2-visual.stories.tsx index 328369b52b..5e7c3521fb 100644 --- a/src/elements/content-sidebar/stories/tests/TaskModalV2-visual.stories.tsx +++ b/src/elements/content-sidebar/stories/tests/TaskModalV2-visual.stories.tsx @@ -1,9 +1,9 @@ import type { Meta, StoryObj } from '@storybook/react'; -import { expect, screen, userEvent, waitFor } from 'storybook/test'; +import { expect, screen, userEvent } from 'storybook/test'; -import { InteractiveTaskModal, mockEditingAssignees, mockEditingTask } from '../__mocks__/TaskModalV2Mocks'; +import { InteractiveTaskModal } from '../__mocks__/TaskModalV2Mocks'; -import { TASK_TYPE_APPROVAL, TASK_TYPE_GENERAL } from '../../../../constants'; +import { TASK_TYPE_APPROVAL } from '../../../../constants'; const meta: Meta = { title: 'Elements/ContentSidebar/TaskModalV2/tests/visual-regression-tests', @@ -17,26 +17,6 @@ export default meta; const findAssigneeCombobox = async () => screen.findByRole('combobox', { name: 'Select Assignees' }); -export const CreateGeneralTaskAccessibleStructure: StoryObj = { - args: { - taskType: TASK_TYPE_GENERAL, - }, - play: async () => { - const dialog = await screen.findByRole('dialog', { name: 'Create General Task' }); - expect(dialog).toBeVisible(); - - const combobox = await findAssigneeCombobox(); - expect(combobox).toBeVisible(); - expect(await screen.findByRole('textbox', { name: /message/i })).toBeVisible(); - expect(screen.getByRole('button', { name: 'Cancel' })).toBeVisible(); - expect(screen.getByRole('button', { name: 'Create' })).toBeVisible(); - expect(screen.getByRole('button', { name: 'Close' })).toBeVisible(); - - // Completion rule only appears once an assignee is selected - expect(screen.queryByRole('checkbox')).not.toBeInTheDocument(); - }, -}; - export const CreateApprovalTaskTabOrder: StoryObj = { args: { taskType: TASK_TYPE_APPROVAL, @@ -90,122 +70,3 @@ export const AssigneeListboxAccessibleNames: StoryObj = { - args: { - taskType: TASK_TYPE_APPROVAL, - }, - play: async () => { - await screen.findByRole('dialog', { name: 'Create Approval Task' }); - - const combobox = await findAssigneeCombobox(); - await userEvent.type(combobox, 'Alice'); - const aliceOption = await screen.findByRole('option', { name: /Alice Wong/ }); - await userEvent.click(aliceOption); - - // A single user assignee shows the completion rule disabled; adding a group enables it - const checkbox = await screen.findByRole('checkbox', { - name: 'Only one assignee is required to complete this task', - }); - expect(checkbox).toBeVisible(); - expect(checkbox).toBeDisabled(); - - await userEvent.type(combobox, 'Engineering'); - const groupOption = await screen.findByRole('option', { name: /Engineering Team/ }); - await userEvent.click(groupOption); - - await waitFor(() => - expect( - screen.getByRole('checkbox', { name: 'Only one assignee is required to complete this task' }), - ).toBeEnabled(), - ); - }, -}; - -export const EditApprovalTaskPrefill: StoryObj = { - args: { - editingAssignees: mockEditingAssignees, - editingTask: mockEditingTask, - taskType: TASK_TYPE_APPROVAL, - }, - play: async () => { - const dialog = await screen.findByRole('dialog', { name: 'Modify Approval Task' }); - expect(dialog).toBeVisible(); - - expect(await screen.findByRole('textbox', { name: /message/i })).toHaveValue( - 'Review the updated launch checklist', - ); - expect(screen.getAllByText('Alice Wong')[0]).toBeVisible(); - expect(screen.getAllByText('Engineering Team')[0]).toBeVisible(); - - expect(screen.getByRole('spinbutton', { name: /month/i })).toHaveAttribute('aria-valuenow', '7'); - expect(screen.getByRole('spinbutton', { name: /day/i })).toHaveAttribute('aria-valuenow', '15'); - expect(screen.getByRole('spinbutton', { name: /year/i })).toHaveAttribute('aria-valuenow', '2026'); - - const completionRuleCheckbox = screen.getByRole('checkbox', { - name: 'Only one assignee is required to complete this task', - }); - expect(completionRuleCheckbox).toBeEnabled(); - expect(completionRuleCheckbox).not.toBeChecked(); - - expect(screen.getByRole('button', { name: 'Update' })).toBeVisible(); - }, -}; - -export const EditGeneralTaskPrefill: StoryObj = { - args: { - editingAssignees: mockEditingAssignees, - editingTask: { ...mockEditingTask, task_type: TASK_TYPE_GENERAL }, - taskType: TASK_TYPE_GENERAL, - }, - play: async () => { - const dialog = await screen.findByRole('dialog', { name: 'Modify General Task' }); - expect(dialog).toBeVisible(); - - expect(await screen.findByRole('textbox', { name: /message/i })).toHaveValue( - 'Review the updated launch checklist', - ); - expect(screen.getByRole('button', { name: 'Update' })).toBeVisible(); - }, -}; - -export const RequiredFieldValidationErrors: StoryObj = { - args: { - taskType: TASK_TYPE_APPROVAL, - }, - play: async () => { - await screen.findByRole('dialog', { name: 'Create Approval Task' }); - await findAssigneeCombobox(); - - await userEvent.click(screen.getByRole('button', { name: 'Create' })); - - // Both the assignee and message fields are required - const requiredFieldErrors = await screen.findAllByText('Required Field'); - expect(requiredFieldErrors.length).toBeGreaterThanOrEqual(2); - expect(screen.getByRole('textbox', { name: /message/i })).toBeInvalid(); - expect(await findAssigneeCombobox()).toBeInvalid(); - - // The modal stays open so the user can correct the form - expect(screen.getByRole('dialog', { name: 'Create Approval Task' })).toBeVisible(); - }, -}; - -export const SubmitErrorNotice: StoryObj = { - args: { - shouldFailSubmit: true, - taskType: TASK_TYPE_APPROVAL, - }, - play: async () => { - await screen.findByRole('dialog', { name: 'Create Approval Task' }); - - const combobox = await findAssigneeCombobox(); - await userEvent.type(combobox, 'Alice'); - const aliceOption = await screen.findByRole('option', { name: /Alice Wong/ }); - await userEvent.click(aliceOption); - - await userEvent.type(screen.getByRole('textbox', { name: /message/i }), 'Please review'); - await userEvent.click(screen.getByRole('button', { name: 'Create' })); - - expect(await screen.findByText('An error occurred while creating this task. Please try again.')).toBeVisible(); - }, -};