Explorar o código

fix(workflow): refold coalesced disclosure cycles

pku-xht hai 1 mes
pai
achega
1b91e5b4fe

+ 4 - 0
apps/web/tests/workflow-run.e2e.ts

@@ -82,6 +82,8 @@ describe.skipIf(MODE === 'record')('web e2e: durable workflow run in Chat', () =
     expect(await disclosures.nth(0).getAttribute('aria-expanded')).toBeNull()
     expect(await disclosures.nth(1).getAttribute('role')).toBeNull()
     expect(await disclosures.nth(1).getAttribute('aria-expanded')).toBeNull()
+    expect(await disclosures.nth(0).evaluate(element => getComputedStyle(element).cursor)).not.toBe('pointer')
+    expect(await disclosures.nth(1).evaluate(element => getComputedStyle(element).cursor)).not.toBe('pointer')
     const member = page.getByRole('button', { name: /^Open Reply with exactly the word/ })
     await member.waitFor({ timeout: 15_000 })
     await member.focus()
@@ -148,10 +150,12 @@ describe.skipIf(MODE === 'record')('web e2e: durable workflow run in Chat', () =
     const terminalWorkflow = page.getByRole('button', { name: /^snapshot-flow/ })
     await terminalWorkflow.waitFor()
     expect(await terminalWorkflow.getAttribute('aria-expanded')).toBe('false')
+    expect(await terminalWorkflow.evaluate(element => getComputedStyle(element).cursor)).toBe('pointer')
     await terminalWorkflow.click()
     const terminalPhase = page.getByRole('button', { name: /^Run/ })
     await terminalPhase.waitFor()
     expect(await terminalPhase.getAttribute('aria-expanded')).toBe('false')
+    expect(await terminalPhase.evaluate(element => getComputedStyle(element).cursor)).toBe('pointer')
     await terminalPhase.click()
     await page.getByText(CHILD_PROMPT, { exact: false }).waitFor()
     await expect.poll(

+ 0 - 2
packages/client/ui-workflow-run/src/client/WorkflowRunPanel.module.css

@@ -14,7 +14,6 @@
   padding: 0 8px;
   border-radius: 8px;
   background: var(--dsw-alias-bg-module-platform);
-  cursor: pointer;
 }
 
 .runHeader:focus-visible {
@@ -78,7 +77,6 @@
   width: 100%;
   min-width: 0;
   height: 32px;
-  cursor: pointer;
 }
 
 .phaseHeader:focus-visible {

+ 5 - 2
packages/client/ui-workflow-run/src/client/WorkflowRunPanel.tsx

@@ -84,10 +84,11 @@ function ManualDisclosure(props: StatusDisclosureProps) {
   )
 }
 
-function StatusDisclosure({ requiresExpansion, ...props }: StatusDisclosureProps & {
+function StatusDisclosure({ cleanCycleKey, requiresExpansion, ...props }: StatusDisclosureProps & {
+  readonly cleanCycleKey: number
   readonly requiresExpansion: boolean
 }) {
-  if (!requiresExpansion) return <ManualDisclosure {...props} />
+  if (!requiresExpansion) return <ManualDisclosure key={cleanCycleKey} {...props} />
   return <DisclosureRow {...props} open expandable={false} onToggle={forcedOpenToggle} />
 }
 
@@ -138,6 +139,7 @@ function RunHeader({ children, count, name, requiresExpansion, status, t }: {
     <StatusDisclosure
       icon={<IconChevronRightOutline14 />}
       title={t('run.title', { name })}
+      cleanCycleKey={count}
       requiresExpansion={requiresExpansion}
       expandOnRowClick
       previewChevron={false}
@@ -201,6 +203,7 @@ function PhaseSection({ phase, navigable, openSession, t }: {
     <StatusDisclosure
       icon={<IconChevronRightOutline14 />}
       title={readablePhase(phase.phase, t)}
+      cleanCycleKey={phase.members.length}
       requiresExpansion={phaseRequiresExpansion(phase)}
       expandOnRowClick
       previewChevron={false}

+ 39 - 0
packages/client/ui-workflow-run/tests/workflow-run.spec.tsx

@@ -377,6 +377,45 @@ describe('WorkflowRunPanel', () => {
     expect(screen.queryByText('未分阶段')).toBeNull()
   })
 
+  it('refolds when a complete activity cycle arrives as one clean update', () => {
+    const firstMember = {
+      seq: 1, label: 'first', childId: 'child-1' as SessionId, status: 'completed' as const,
+    }
+    const phaseClean: WorkflowRunChatData = {
+      name: 'phase-cycle', status: 'running',
+      phases: [phase({ members: [firstMember] })],
+    }
+    const phaseView = render(<WorkflowRunPanel {...panelProps(phaseClean)} />)
+    fireEvent.click(screen.getByRole('button', { name: /未分阶段/ }))
+    expect(screen.getByText('first')).toBeTruthy()
+    phaseView.rerender(<WorkflowRunPanel {...panelProps({
+      ...phaseClean,
+      phases: [phase({ members: [firstMember, {
+        seq: 2, label: 'second', childId: 'child-2' as SessionId, status: 'completed',
+      }] })],
+    })} />)
+    expect(screen.getByRole('button', { name: /未分阶段/ }).getAttribute('aria-expanded')).toBe('false')
+    expect(screen.queryByText('first')).toBeNull()
+    expect(screen.queryByText('second')).toBeNull()
+    phaseView.unmount()
+
+    const workflowClean: WorkflowRunChatData = {
+      name: 'workflow-cycle', status: 'completed',
+      phases: [phase({ members: [firstMember] })],
+    }
+    const workflowView = render(<WorkflowRunPanel {...panelProps(workflowClean)} />)
+    fireEvent.click(screen.getByRole('button', { name: /^workflow-cycle/ }))
+    expect(screen.getByText('未分阶段')).toBeTruthy()
+    workflowView.rerender(<WorkflowRunPanel {...panelProps({
+      ...workflowClean,
+      phases: [phase({ members: [firstMember, {
+        seq: 2, label: 'second', childId: 'child-2' as SessionId, status: 'completed',
+      }] })],
+    })} />)
+    expect(screen.getByRole('button', { name: /^workflow-cycle/ }).getAttribute('aria-expanded')).toBe('false')
+    expect(screen.queryByText('未分阶段')).toBeNull()
+  })
+
   it('derives the zero-member running and completed states from the current run status', () => {
     const running: WorkflowRunChatData = { name: 'empty', status: 'running', phases: [] }
     const view = render(<WorkflowRunPanel {...panelProps(running)} />)