From f865abc0aaaba2844d6e3bf21059d700119f8d30 Mon Sep 17 00:00:00 2001 From: Bilal Karim <4129613+bilal-karim@users.noreply.github.com> Date: Wed, 21 Oct 2026 15:02:29 -0400 Subject: [PATCH 1/5] Fix tree lines, column alignment, and tooltips in the Relationships tab Co-Authored-By: Claude Opus 5.5 --- ...low-family-node-description-details.svelte | 23 +++++++++++-------- ...rkflow-family-node-description-tree.svelte | 3 ++- .../workflow-family-node-description.svelte | 16 +++++++++---- .../relationships/workflow-family-tree.svelte | 6 +++-- src/lib/holocene/zoom-svg.svelte | 14 +++++------ 5 files changed, 37 insertions(+), 25 deletions(-) diff --git a/src/lib/components/workflow/relationships/workflow-family-node-description-details.svelte b/src/lib/components/workflow/relationships/workflow-family-node-description-details.svelte index f95f393728..5fdcc17842 100644 --- a/src/lib/components/workflow/relationships/workflow-family-node-description-details.svelte +++ b/src/lib/components/workflow/relationships/workflow-family-node-description-details.svelte @@ -39,24 +39,28 @@ const showExpandIcon = $derived( !isRootWorkflow && $showFullTree && childrenCount, ); + + const Glyph = $derived(expanded ? IconHyphen : IconAdd);
-
-
+
+
-
+
{#if isRootWorkflow}

{translate('common.type')}

{/if}

{workflow.name}

-
+
{#if isRootWorkflow}

{translate('common.id')}

{/if} @@ -72,7 +76,7 @@
{#if $showFullTree} -
+
{#if isRootWorkflow}

{translate('common.child-count')}

{/if} @@ -81,7 +85,9 @@
{/if} - - {#if showExpandIcon} - {@const Glyph = expanded ? IconHyphen : IconAdd} - - {/if} +
diff --git a/src/lib/components/workflow/relationships/workflow-family-node-description-tree.svelte b/src/lib/components/workflow/relationships/workflow-family-node-description-tree.svelte index 66e66b5328..6260929be6 100644 --- a/src/lib/components/workflow/relationships/workflow-family-node-description-tree.svelte +++ b/src/lib/components/workflow/relationships/workflow-family-node-description-tree.svelte @@ -23,9 +23,10 @@ }: Props = $props(); -{#each root?.children as child} +{#each root?.children as child, index (child.workflow.runId)} void; activeWorkflow?: WorkflowExecution | undefined; openRuns: Map; @@ -20,6 +21,7 @@ root, expandAll, generation = 0, + lastChild = false, onNodeClick, activeWorkflow = undefined, openRuns, @@ -44,22 +46,23 @@ }; -
+
diff --git a/src/lib/components/workflow/relationships/workflow-family-tree.svelte b/src/lib/components/workflow/relationships/workflow-family-tree.svelte index bc6f635ac0..341ee3568d 100644 --- a/src/lib/components/workflow/relationships/workflow-family-tree.svelte +++ b/src/lib/components/workflow/relationships/workflow-family-tree.svelte @@ -58,7 +58,9 @@ }; -
+
@@ -96,7 +98,7 @@
{#if pannable} - + diff --git a/src/lib/components/workflow/relationships/workflow-family-tree.svelte b/src/lib/components/workflow/relationships/workflow-family-tree.svelte index 341ee3568d..aed3146b2e 100644 --- a/src/lib/components/workflow/relationships/workflow-family-tree.svelte +++ b/src/lib/components/workflow/relationships/workflow-family-tree.svelte @@ -59,10 +59,10 @@
+ + diff --git a/tests/integration/workflow-relationships.desktop.spec.ts b/tests/integration/workflow-relationships.desktop.spec.ts new file mode 100644 index 0000000000..38d78af36c --- /dev/null +++ b/tests/integration/workflow-relationships.desktop.spec.ts @@ -0,0 +1,155 @@ +import { expect, test } from '@playwright/test'; + +import type { WorkflowExecutionAPIResponse } from '$src/lib/types/workflows'; +import { mockWorkflowApis } from '~/test-utilities/mock-apis'; +import { mockWorkflow } from '~/test-utilities/mocks/workflow'; +import { WORKFLOWS_API } from '~/test-utilities/mocks/workflows'; +import { WORKFLOWS_COUNT_API } from '~/test-utilities/mocks/workflows-count'; + +const rootId = 'relationships-root'; +const rootRunId = 'relationships-root-run'; +const childAId = 'relationships-child-a'; +const childARunId = 'relationships-child-a-run'; +const grandchildId = 'relationships-grandchild-a1'; +const grandchildRunId = 'relationships-grandchild-a1-run'; +const childBId = 'relationships-child-b'; +const childBRunId = 'relationships-child-b-run'; +const childAName = + 'Child A with a workflow type name that wraps without shifting columns'; +const grandchildName = 'Grandchild A1'; +const childBName = 'Child B'; + +const relationshipWorkflow = ({ + workflowId, + runId, + name, + parent, +}: { + workflowId: string; + runId: string; + name: string; + parent: { workflowId: string; runId: string } | null; +}) => ({ + ...mockWorkflow.workflowExecutionInfo, + execution: { workflowId, runId }, + type: { name }, + startTime: '2022-04-28T05:50:48.264756929Z', + closeTime: '2022-04-28T08:50:48.264756929Z', + parentExecution: parent, + rootExecution: { workflowId: rootId, runId: rootRunId }, +}); + +const rootWorkflow = { + ...mockWorkflow, + workflowExecutionInfo: relationshipWorkflow({ + workflowId: rootId, + runId: rootRunId, + name: 'Root workflow', + parent: null, + }), +} satisfies WorkflowExecutionAPIResponse; + +const relationshipWorkflows = [ + relationshipWorkflow({ + workflowId: childAId, + runId: childARunId, + name: childAName, + parent: { workflowId: rootId, runId: rootRunId }, + }), + relationshipWorkflow({ + workflowId: grandchildId, + runId: grandchildRunId, + name: grandchildName, + parent: { workflowId: childAId, runId: childARunId }, + }), + relationshipWorkflow({ + workflowId: childBId, + runId: childBRunId, + name: childBName, + parent: { workflowId: rootId, runId: rootRunId }, + }), +]; + +const relationshipsUrl = `/namespaces/default/workflows/${rootId}/${rootRunId}/relationships`; + +test.describe('Relationships nested tree', () => { + test.beforeEach(async ({ page }) => { + await mockWorkflowApis(page, rootWorkflow); + await page.route(WORKFLOWS_COUNT_API, (route) => { + return route.fulfill({ json: { count: '4' } }); + }); + await page.route(WORKFLOWS_API, (route) => { + const url = new URL(route.request().url()); + if (url.pathname.endsWith('/workflows')) { + return route.fulfill({ + json: { executions: relationshipWorkflows, nextPageToken: null }, + }); + } + return route.fallback(); + }); + await page.goto(relationshipsUrl); + }); + + test('keeps nested rows aligned and their tree affordances visible', async ({ + page, + }) => { + await page.setViewportSize({ width: 1100, height: 900 }); + await expect(page.getByText(childBName)).toBeVisible(); + + const expandAll = page.getByRole('switch', { name: 'Expand All' }); + await expect(expandAll).not.toBeChecked(); + await expect(page.getByText(grandchildName)).toBeHidden(); + await expandAll.focus(); + await page.keyboard.press('Space'); + await expect(expandAll).toBeChecked(); + await expect(page.getByText(grandchildName)).toBeVisible(); + await page.keyboard.press('Space'); + await expect(expandAll).not.toBeChecked(); + await expect(page.getByText(grandchildName)).toBeHidden(); + + const childARow = page + .getByText(childAName) + .locator('xpath=ancestor::button'); + await childARow.click(); + await expect(childARow).toHaveClass(/bg-interactive-primary/); + await expect(page.getByText(grandchildName)).toBeVisible(); + + const childAContainer = childARow.locator('..'); + await expect(childAContainer.locator(':scope > div').last()).toBeVisible(); + + const grandchildRow = page + .getByText(grandchildName) + .locator('xpath=ancestor::button'); + await expect(grandchildRow.locator(':scope > div').first()).toHaveClass( + /rounded-bl/, + ); + + const ids = [rootId, childAId, grandchildId, childBId]; + const idPositions = await Promise.all( + ids.map(async (id) => { + const box = await page.getByRole('link', { name: id }).boundingBox(); + return box?.x; + }), + ); + const positions = idPositions.filter( + (position): position is number => typeof position === 'number', + ); + expect(positions).toHaveLength(ids.length); + expect(Math.max(...positions) - Math.min(...positions)).toBeLessThan(1); + + const zoomIn = page.getByRole('button', { name: 'Zoom in' }); + const graphHeader = zoomIn.locator( + 'xpath=ancestor::div[contains(@class, "lg:sticky")]', + ); + await expect(graphHeader).toHaveCSS('position', 'sticky'); + + await page.setViewportSize({ width: 1440, height: 900 }); + await page.emulateMedia({ colorScheme: 'dark' }); + await expect(page.getByText(grandchildName)).toBeVisible(); + + await zoomIn.focus(); + await page.keyboard.press('Tab'); + await page.keyboard.press('Shift+Tab'); + await expect(page.getByRole('tooltip', { name: 'Zoom in' })).toBeVisible(); + }); +}); From cfe56ade81e59d9bce95de8f8bbcc43aecf5c352 Mon Sep 17 00:00:00 2001 From: Ardie Wen Date: Fri, 23 Oct 2026 13:16:05 -0400 Subject: [PATCH 3/5] [PR-4065] Cover relationship tree edge cases --- ...low-family-node-description-details.svelte | 4 +- ...rkflow-family-node-description-tree.svelte | 2 +- .../workflow-relationships.desktop.spec.ts | 95 ++++++++++++++++--- 3 files changed, 85 insertions(+), 16 deletions(-) diff --git a/src/lib/components/workflow/relationships/workflow-family-node-description-details.svelte b/src/lib/components/workflow/relationships/workflow-family-node-description-details.svelte index 5bfac18cc3..e9a698f3c2 100644 --- a/src/lib/components/workflow/relationships/workflow-family-node-description-details.svelte +++ b/src/lib/components/workflow/relationships/workflow-family-node-description-details.svelte @@ -63,7 +63,9 @@

{workflow.name}

-
+
{#if isRootWorkflow}

{translate('common.id')}

{/if} diff --git a/src/lib/components/workflow/relationships/workflow-family-node-description-tree.svelte b/src/lib/components/workflow/relationships/workflow-family-node-description-tree.svelte index 6260929be6..3756b510b5 100644 --- a/src/lib/components/workflow/relationships/workflow-family-node-description-tree.svelte +++ b/src/lib/components/workflow/relationships/workflow-family-node-description-tree.svelte @@ -23,7 +23,7 @@ }: Props = $props(); -{#each root?.children as child, index (child.workflow.runId)} +{#each root?.children as child, index} { + const ids = [rootId, childAId, grandchildId, childBId]; + const idPositions = await Promise.all( + ids.map(async (id) => { + const box = await page.getByRole('link', { name: id }).boundingBox(); + return box?.x; + }), + ); + const timestampPositions = await page + .locator('.relationship-timestamps-column') + .evaluateAll((elements) => { + return elements.map((element) => element.getBoundingClientRect().x); + }); + const positions = idPositions.filter( + (position): position is number => typeof position === 'number', + ); + + expect(positions).toHaveLength(ids.length); + expect(Math.max(...positions) - Math.min(...positions)).toBeLessThan(1); + expect(timestampPositions).toHaveLength(ids.length); + expect( + Math.max(...timestampPositions) - Math.min(...timestampPositions), + ).toBeLessThan(1); +}; + test.describe('Relationships nested tree', () => { test.beforeEach(async ({ page }) => { await mockWorkflowApis(page, rootWorkflow); @@ -94,6 +119,7 @@ test.describe('Relationships nested tree', () => { page, }) => { await page.setViewportSize({ width: 1100, height: 900 }); + await expect(page.locator('body')).toHaveAttribute('data-theme', 'light'); await expect(page.getByText(childBName)).toBeVisible(); const expandAll = page.getByRole('switch', { name: 'Expand All' }); @@ -116,6 +142,9 @@ test.describe('Relationships nested tree', () => { const childAContainer = childARow.locator('..'); await expect(childAContainer.locator(':scope > div').last()).toBeVisible(); + await expect(childARow.locator(':scope > div').first()).not.toHaveClass( + /border-l|rounded-bl/, + ); const grandchildRow = page .getByText(grandchildName) @@ -124,18 +153,16 @@ test.describe('Relationships nested tree', () => { /rounded-bl/, ); - const ids = [rootId, childAId, grandchildId, childBId]; - const idPositions = await Promise.all( - ids.map(async (id) => { - const box = await page.getByRole('link', { name: id }).boundingBox(); - return box?.x; - }), + const childBRow = page + .getByText(childBName) + .locator('xpath=ancestor::button'); + await expect(childBRow.locator(':scope > div').first()).toHaveClass( + /border-l/, ); - const positions = idPositions.filter( - (position): position is number => typeof position === 'number', + await expect(childBRow.locator(':scope > div').first()).toHaveClass( + /rounded-bl/, ); - expect(positions).toHaveLength(ids.length); - expect(Math.max(...positions) - Math.min(...positions)).toBeLessThan(1); + await expectAlignedColumns(page); const zoomIn = page.getByRole('button', { name: 'Zoom in' }); const graphHeader = zoomIn.locator( @@ -144,12 +171,52 @@ test.describe('Relationships nested tree', () => { await expect(graphHeader).toHaveCSS('position', 'sticky'); await page.setViewportSize({ width: 1440, height: 900 }); - await page.emulateMedia({ colorScheme: 'dark' }); await expect(page.getByText(grandchildName)).toBeVisible(); + await expectAlignedColumns(page); await zoomIn.focus(); await page.keyboard.press('Tab'); await page.keyboard.press('Shift+Tab'); - await expect(page.getByRole('tooltip', { name: 'Zoom in' })).toBeVisible(); + const tooltip = page.getByRole('tooltip', { name: 'Zoom in' }); + await expect(tooltip).toBeVisible(); + const [tooltipBox, zoomInBox] = await Promise.all([ + tooltip.boundingBox(), + zoomIn.boundingBox(), + ]); + expect(tooltipBox).not.toBeNull(); + expect(zoomInBox).not.toBeNull(); + expect(tooltipBox!.y + tooltipBox!.height).toBeLessThanOrEqual( + zoomInBox!.y, + ); + }); + + test.describe('dark mode', () => { + test.use({ colorScheme: 'dark' }); + + test('uses dark relationship tree tokens', async ({ page }) => { + await page.setViewportSize({ width: 1440, height: 900 }); + await expect(page.locator('body')).toHaveAttribute('data-theme', 'dark'); + await expect( + page.locator('html:has(body[data-theme="dark"])'), + ).toHaveCount(1); + + const childARow = page + .getByText(childAName) + .locator('xpath=ancestor::button'); + await childARow.click(); + const activeTreeLine = childARow.locator(':scope > div').first(); + const [borderColor, borderToken] = await activeTreeLine.evaluate( + (line) => { + const styles = getComputedStyle(line); + return [ + styles.borderBottomColor, + styles.getPropertyValue('--color-border-secondary').trim(), + ]; + }, + ); + expect(borderToken).not.toBe(''); + expect(borderColor).not.toBe('rgba(0, 0, 0, 0)'); + await expectAlignedColumns(page); + }); }); }); From 1c0d508a12926ff24fed440ddebb8d55bb10a70d Mon Sep 17 00:00:00 2001 From: Ardie Wen Date: Fri, 23 Oct 2026 13:27:20 -0400 Subject: [PATCH 4/5] [PR-4065] Strengthen relationship tree coverage --- .../workflow-relationships.desktop.spec.ts | 44 ++++++++++++++----- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/tests/integration/workflow-relationships.desktop.spec.ts b/tests/integration/workflow-relationships.desktop.spec.ts index 904634de41..29852bedb5 100644 --- a/tests/integration/workflow-relationships.desktop.spec.ts +++ b/tests/integration/workflow-relationships.desktop.spec.ts @@ -8,7 +8,8 @@ import { WORKFLOWS_COUNT_API } from '~/test-utilities/mocks/workflows-count'; const rootId = 'relationships-root'; const rootRunId = 'relationships-root-run'; -const childAId = 'relationships-child-a-with-an-unbreakable-workflow-id'; +const childAId = + 'relationshipschildawithanunbreakableworkflowid01234567890123456789'; const childARunId = 'relationships-child-a-run'; const grandchildId = 'relationships-grandchild-a1'; const grandchildRunId = 'relationships-grandchild-a1-run'; @@ -162,6 +163,16 @@ test.describe('Relationships nested tree', () => { await expect(childBRow.locator(':scope > div').first()).toHaveClass( /rounded-bl/, ); + + const [childIdBox, childCountBox] = await Promise.all([ + childARow.getByRole('link', { name: childAId }).boundingBox(), + childARow.locator('.relationship-id-column + div').boundingBox(), + ]); + expect(childIdBox).not.toBeNull(); + expect(childCountBox).not.toBeNull(); + expect(childIdBox!.x + childIdBox!.width).toBeLessThanOrEqual( + childCountBox!.x, + ); await expectAlignedColumns(page); const zoomIn = page.getByRole('button', { name: 'Zoom in' }); @@ -205,18 +216,29 @@ test.describe('Relationships nested tree', () => { .locator('xpath=ancestor::button'); await childARow.click(); const activeTreeLine = childARow.locator(':scope > div').first(); - const [borderColor, borderToken] = await activeTreeLine.evaluate( - (line) => { - const styles = getComputedStyle(line); - return [ - styles.borderBottomColor, - styles.getPropertyValue('--color-border-secondary').trim(), - ]; - }, + const darkBorderToken = await activeTreeLine.evaluate((line) => + getComputedStyle(line) + .getPropertyValue('--color-border-secondary') + .trim(), ); - expect(borderToken).not.toBe(''); - expect(borderColor).not.toBe('rgba(0, 0, 0, 0)'); await expectAlignedColumns(page); + + await page.emulateMedia({ colorScheme: 'light' }); + await page.reload(); + await expect(page.locator('body')).toHaveAttribute('data-theme', 'light'); + const lightChildARow = page + .getByText(childAName) + .locator('xpath=ancestor::button'); + await lightChildARow.click(); + const lightBorderToken = await lightChildARow + .locator(':scope > div') + .first() + .evaluate((line) => + getComputedStyle(line) + .getPropertyValue('--color-border-secondary') + .trim(), + ); + expect(darkBorderToken).not.toBe(lightBorderToken); }); }); }); From a6191a0d97153d34720782e6b4e5b5ed8f0f215d Mon Sep 17 00:00:00 2001 From: Bilal Karim <4129613+bilal-karim@users.noreply.github.com> Date: Thu, 29 Oct 2026 11:15:47 -0400 Subject: [PATCH 5/5] Remove container query fallback from relationship columns Every supported browser handles cqw and container-type, so the indent-based calc() fallback never applied. Use Tailwind classes for the column widths and data-testid hooks in the spec. Co-Authored-By: Claude Opus 5.5 --- ...low-family-node-description-details.svelte | 47 ++----------------- .../workflow-family-node-description.svelte | 1 - .../workflow-relationships.desktop.spec.ts | 7 ++- 3 files changed, 9 insertions(+), 46 deletions(-) diff --git a/src/lib/components/workflow/relationships/workflow-family-node-description-details.svelte b/src/lib/components/workflow/relationships/workflow-family-node-description-details.svelte index e9a698f3c2..91f4d5a0b9 100644 --- a/src/lib/components/workflow/relationships/workflow-family-node-description-details.svelte +++ b/src/lib/components/workflow/relationships/workflow-family-node-description-details.svelte @@ -17,7 +17,6 @@ isActive?: boolean; childrenCount?: number; expanded?: boolean; - indent?: number; }; let { @@ -27,7 +26,6 @@ isActive = false, childrenCount = 0, expanded = false, - indent = 0, }: Props = $props(); const elapsedTime = $derived( @@ -47,7 +45,6 @@
{#if isRootWorkflow}

{translate('common.id')}

@@ -91,7 +89,8 @@
{/if} - - diff --git a/src/lib/components/workflow/relationships/workflow-family-node-description.svelte b/src/lib/components/workflow/relationships/workflow-family-node-description.svelte index 14778a72d7..3ede0a3524 100644 --- a/src/lib/components/workflow/relationships/workflow-family-node-description.svelte +++ b/src/lib/components/workflow/relationships/workflow-family-node-description.svelte @@ -73,7 +73,6 @@ {isActive} childrenCount={root.children?.length ?? 0} {expanded} - indent={generation * 16} />
diff --git a/tests/integration/workflow-relationships.desktop.spec.ts b/tests/integration/workflow-relationships.desktop.spec.ts index 29852bedb5..b1e45054c4 100644 --- a/tests/integration/workflow-relationships.desktop.spec.ts +++ b/tests/integration/workflow-relationships.desktop.spec.ts @@ -82,7 +82,7 @@ const expectAlignedColumns = async (page: Page) => { }), ); const timestampPositions = await page - .locator('.relationship-timestamps-column') + .getByTestId('relationship-timestamps') .evaluateAll((elements) => { return elements.map((element) => element.getBoundingClientRect().x); }); @@ -166,7 +166,10 @@ test.describe('Relationships nested tree', () => { const [childIdBox, childCountBox] = await Promise.all([ childARow.getByRole('link', { name: childAId }).boundingBox(), - childARow.locator('.relationship-id-column + div').boundingBox(), + childARow + .getByTestId('relationship-id') + .locator('xpath=following-sibling::div[1]') + .boundingBox(), ]); expect(childIdBox).not.toBeNull(); expect(childCountBox).not.toBeNull();