diff --git a/src/views/workflow-history/helpers/__tests__/workflow-history-fetcher.test.tsx b/src/views/workflow-history/helpers/__tests__/workflow-history-fetcher.test.tsx index ee29d09dc..c1020866b 100644 --- a/src/views/workflow-history/helpers/__tests__/workflow-history-fetcher.test.tsx +++ b/src/views/workflow-history/helpers/__tests__/workflow-history-fetcher.test.tsx @@ -6,6 +6,7 @@ import { waitFor } from '@/test-utils/rtl'; import { type GetWorkflowHistoryResponse } from '@/route-handlers/get-workflow-history/get-workflow-history.types'; import mswMockEndpoints from '@/test-utils/msw-mock-handlers/helper/msw-mock-endpoints'; +import { scheduleActivityTaskEvent } from '../../__fixtures__/workflow-history-activity-events'; import workflowHistoryMultiPageFixture from '../../__fixtures__/workflow-history-multi-page-fixture'; import WorkflowHistoryFetcher from '../workflow-history-fetcher'; @@ -329,21 +330,110 @@ describe(WorkflowHistoryFetcher.name, () => { jest.useRealTimers(); } }); + + it('should send waitForNewEvent=true for all requests when param is set', async () => { + const emptyPageFixture: GetWorkflowHistoryResponse[] = [ + // Page 1: has events + { + history: { events: [scheduleActivityTaskEvent] }, + rawHistory: [], + archived: false, + nextPageToken: 'page2', + }, + // Page 2: empty events (server keeps nextPageToken for long-polling) + { + history: { events: [] }, + rawHistory: [], + archived: false, + nextPageToken: 'page3', + }, + // Page 3: empty events (last page) + { + history: { events: [] }, + rawHistory: [], + archived: false, + nextPageToken: '', + }, + ]; + + const { fetcher, getCapturedWaitForNewEvent } = setup(queryClient, { + waitForNewEvent: true, + responses: emptyPageFixture, + }); + + fetcher.start(); + + await waitFor(() => { + const state = fetcher.getCurrentState(); + expect(state.hasNextPage).toBe(false); + expect(state.data?.pages).toHaveLength(3); + }); + + const waitForNewEventValues = getCapturedWaitForNewEvent(); + // All requests should use waitForNewEvent=true when param is set + expect(waitForNewEventValues[0]).toBe('true'); + expect(waitForNewEventValues[1]).toBe('true'); + expect(waitForNewEventValues[2]).toBe('true'); + }); + + it('should send waitForNewEvent=false for all requests when param is not set', async () => { + const emptyPageFixture: GetWorkflowHistoryResponse[] = [ + { + history: { events: [scheduleActivityTaskEvent] }, + rawHistory: [], + archived: false, + nextPageToken: 'page2', + }, + { + history: { events: [] }, + rawHistory: [], + archived: false, + nextPageToken: 'page3', + }, + ]; + + const { fetcher, getCapturedWaitForNewEvent } = setup(queryClient, { + responses: emptyPageFixture, + }); + + fetcher.start(); + + await waitFor(() => { + const state = fetcher.getCurrentState(); + expect(state.hasNextPage).toBe(false); + }); + + const waitForNewEventValues = getCapturedWaitForNewEvent(); + // All requests should use waitForNewEvent=false when param is not set + expect(waitForNewEventValues[0]).toBe('false'); + expect(waitForNewEventValues[1]).toBe('false'); + }); }); -function setup(client: QueryClient, options: { failOnPages?: number[] } = {}) { +function setup( + client: QueryClient, + options: { + failOnPages?: number[]; + waitForNewEvent?: boolean; + responses?: GetWorkflowHistoryResponse[]; + } = {} +) { const params = { domain: 'test-domain', cluster: 'test-cluster', workflowId: 'test-workflow-id', runId: 'test-run-id', pageSize: 10, + ...(options.waitForNewEvent !== undefined && { + waitForNewEvent: options.waitForNewEvent, + }), }; - const { getCapturedPageSizes } = mockHistoryEndpoint( - workflowHistoryMultiPageFixture, - options.failOnPages - ); + const { getCapturedPageSizes, getCapturedWaitForNewEvent } = + mockHistoryEndpoint( + options.responses ?? workflowHistoryMultiPageFixture, + options.failOnPages + ); const fetcher = new WorkflowHistoryFetcher(client, params); hoistedFetcher = fetcher; @@ -363,6 +453,7 @@ function setup(client: QueryClient, options: { failOnPages?: number[] } = {}) { params, waitForData, getCapturedPageSizes, + getCapturedWaitForNewEvent, }; } @@ -371,6 +462,7 @@ function mockHistoryEndpoint( failOnPages: number[] = [] ) { const capturedPageSizes: string[] = []; + const capturedWaitForNewEvent: string[] = []; mswMockEndpoints([ { @@ -381,8 +473,10 @@ function mockHistoryEndpoint( const url = new URL(request.url); const nextPage = url.searchParams.get('nextPage'); const pageSize = url.searchParams.get('pageSize'); + const waitForNewEvent = url.searchParams.get('waitForNewEvent'); capturedPageSizes.push(pageSize ?? ''); + capturedWaitForNewEvent.push(waitForNewEvent ?? ''); // Determine current page number based on nextPage param let pageNumber = 1; @@ -406,6 +500,14 @@ function mockHistoryEndpoint( const responseIndex = pageNumber - 1; const response = responses[responseIndex] || responses[responses.length - 1]; + + // Simulate real server behavior: when waitForNewEvent is false + // and the page is empty, the server doesn't return nextPageToken + const isEmpty = response.history?.events?.length === 0; + if (isEmpty && waitForNewEvent === 'false') { + return HttpResponse.json({ ...response, nextPageToken: '' }); + } + return HttpResponse.json(response); }, }, @@ -413,5 +515,6 @@ function mockHistoryEndpoint( return { getCapturedPageSizes: () => capturedPageSizes, + getCapturedWaitForNewEvent: () => capturedWaitForNewEvent, }; } diff --git a/src/views/workflow-history/workflow-history-events-duration-badge/__tests__/workflow-history-events-duration-badge.test.tsx b/src/views/workflow-history/workflow-history-events-duration-badge/__tests__/workflow-history-events-duration-badge.test.tsx index bdaf1c1be..b914f1ad1 100644 --- a/src/views/workflow-history/workflow-history-events-duration-badge/__tests__/workflow-history-events-duration-badge.test.tsx +++ b/src/views/workflow-history/workflow-history-events-duration-badge/__tests__/workflow-history-events-duration-badge.test.tsx @@ -64,11 +64,21 @@ describe('WorkflowHistoryEventsDurationBadge', () => { expect(screen.getByText('Duration: 120')).toBeInTheDocument(); }); - it('does not render badge when loading more events', () => { + it('renders badge when loading more events for running workflow', () => { setup({ loadingMoreEvents: true, }); + expect(screen.getByText(/Duration:/)).toBeInTheDocument(); + }); + + it('does not render badge when loading more events for completed workflow', () => { + setup({ + loadingMoreEvents: true, + workflowCloseStatus: + WorkflowExecutionCloseStatus.WORKFLOW_EXECUTION_CLOSE_STATUS_COMPLETED, + }); + expect(screen.queryByText(/Duration:/)).not.toBeInTheDocument(); }); diff --git a/src/views/workflow-history/workflow-history-events-duration-badge/workflow-history-events-duration-badge.tsx b/src/views/workflow-history/workflow-history-events-duration-badge/workflow-history-events-duration-badge.tsx index 54e94222c..505088b64 100644 --- a/src/views/workflow-history/workflow-history-events-duration-badge/workflow-history-events-duration-badge.tsx +++ b/src/views/workflow-history/workflow-history-events-duration-badge/workflow-history-events-duration-badge.tsx @@ -22,7 +22,9 @@ export default function WorkflowHistoryEventsDurationBadge({ workflowCloseStatus !== 'WORKFLOW_EXECUTION_CLOSE_STATUS_INVALID'; const singleEvent = eventsCount === 1 && !hasMissingEvents; const noDuration = - loadingMoreEvents || singleEvent || (workflowEnded && !endTime); + (loadingMoreEvents && workflowEnded) || + singleEvent || + (workflowEnded && !endTime); const hideDuration = (showOngoingOnly && endTime) || noDuration; const isOngoing = !endTime && !hideDuration; diff --git a/src/views/workflow-history/workflow-history-remaining-duration-badge/__tests__/workflow-history-remaining-duration-badge.test.tsx b/src/views/workflow-history/workflow-history-remaining-duration-badge/__tests__/workflow-history-remaining-duration-badge.test.tsx index 2b52f9160..9d43145f9 100644 --- a/src/views/workflow-history/workflow-history-remaining-duration-badge/__tests__/workflow-history-remaining-duration-badge.test.tsx +++ b/src/views/workflow-history/workflow-history-remaining-duration-badge/__tests__/workflow-history-remaining-duration-badge.test.tsx @@ -36,12 +36,12 @@ describe('WorkflowHistoryRemainingDurationBadge', () => { expect(screen.getByText('Remaining: 5m 30s')).toBeInTheDocument(); }); - it('does not render badge when loading more events', () => { + it('renders badge when loading more events for running workflow', () => { setup({ loadingMoreEvents: true, }); - expect(screen.queryByText(/Remaining:/)).not.toBeInTheDocument(); + expect(screen.getByText('Remaining: 5m 30s')).toBeInTheDocument(); }); it('does not render badge when workflow is archived', () => { @@ -151,11 +151,11 @@ describe('WorkflowHistoryRemainingDurationBadge', () => { startTime={mockStartTime} expectedEndTime={new Date('2024-01-01T10:07:00Z').getTime()} prefix="Remaining:" - workflowIsArchived={false} + workflowIsArchived={true} workflowCloseStatus={ WorkflowExecutionCloseStatus.WORKFLOW_EXECUTION_CLOSE_STATUS_INVALID } - loadingMoreEvents={true} + loadingMoreEvents={false} /> ); diff --git a/src/views/workflow-history/workflow-history-remaining-duration-badge/workflow-history-remaining-duration-badge.tsx b/src/views/workflow-history/workflow-history-remaining-duration-badge/workflow-history-remaining-duration-badge.tsx index 53d266db4..bd7daceff 100644 --- a/src/views/workflow-history/workflow-history-remaining-duration-badge/workflow-history-remaining-duration-badge.tsx +++ b/src/views/workflow-history/workflow-history-remaining-duration-badge/workflow-history-remaining-duration-badge.tsx @@ -18,7 +18,7 @@ export default function WorkflowHistoryRemainingDurationBadge({ workflowIsArchived || workflowCloseStatus !== 'WORKFLOW_EXECUTION_CLOSE_STATUS_INVALID'; - const shouldHide = loadingMoreEvents || workflowEnded; + const shouldHide = workflowEnded; const [remainingDuration, setRemainingDuration] = useState( null