From 1a14e8f0e2bfc7353b51fe23cef74582e5722c80 Mon Sep 17 00:00:00 2001 From: aGallea Date: Mon, 30 Mar 2026 12:04:55 +0300 Subject: [PATCH] fix(frontend): resolve stash pop conflict in cluster drawer Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- frontend/e2e/cluster-drawer.spec.ts | 61 +++++++++++++++++++ .../src/components/plot/ClusterLegend.tsx | 1 + 2 files changed, 62 insertions(+) diff --git a/frontend/e2e/cluster-drawer.spec.ts b/frontend/e2e/cluster-drawer.spec.ts index 5ffe33d..0d9c3a6 100644 --- a/frontend/e2e/cluster-drawer.spec.ts +++ b/frontend/e2e/cluster-drawer.spec.ts @@ -1,5 +1,6 @@ import { test, expect } from './fixtures' import type { Page } from '@playwright/test' +import { CLUSTER_COLORS } from '../src/stores/plotStore' type PlotStoreSnapshot = { selectedPointIds?: Set @@ -810,4 +811,64 @@ test.describe('Cluster detail drawer', () => { await expect(breadcrumb).toBeHidden({ timeout: 5_000 }) }) + + test('sub-cluster legend uses palette colors by index', async ({ page, plotPage: _ }) => { + const makeSubClusterResponse = () => ({ + parent_cluster_index: 0, + total_points: 6, + points: [ + { id: 'a', x: 0, y: 0, z: 0, sub_cluster: 0, metadata: {} }, + { id: 'b', x: 1, y: 0, z: 0, sub_cluster: 0, metadata: {} }, + { id: 'c', x: 0, y: 1, z: 0, sub_cluster: 1, metadata: {} }, + { id: 'd', x: 1, y: 1, z: 0, sub_cluster: 1, metadata: {} }, + { id: 'e', x: 0, y: 0, z: 1, sub_cluster: 2, metadata: {} }, + { id: 'f', x: 1, y: 0, z: 1, sub_cluster: 2, metadata: {} }, + ], + sub_clusters: [ + { index: 0, count: 2, color: 'hsl(120, 70%, 50%)' }, + { index: 1, count: 2, color: 'hsl(240, 70%, 50%)' }, + { index: 2, count: 2, color: 'hsl(0, 70%, 50%)' }, + ], + }) + + const expectedRgb = (hex: string) => { + const clean = hex.replace('#', '') + const r = parseInt(clean.slice(0, 2), 16) + const g = parseInt(clean.slice(2, 4), 16) + const b = parseInt(clean.slice(4, 6), 16) + return `rgb(${r}, ${g}, ${b})` + } + + await page.route(/.*\/api\/plot\/.*\/cluster\/\d+\/sub-cluster$/, async (route) => { + await route.fulfill({ + status: 200, + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(makeSubClusterResponse()), + }) + }) + + await expect( + page.getByRole('button', { name: 'Compute Plot' }) + ).toBeVisible({ timeout: 10_000 }) + + await page.getByRole('button', { name: 'Compute Plot' }).click() + await expect(page.locator('canvas')).toBeVisible({ timeout: 120_000 }) + + await page.getByTestId('cluster-legend-name-0').click() + await expect(page.getByTestId('cluster-detail-drawer')).toBeVisible({ timeout: 10_000 }) + + const computeBtn = page.getByTestId('sub-cluster-compute') + await expect(computeBtn).toBeVisible({ timeout: 5_000 }) + await computeBtn.click() + + await expect(page.getByTestId('drill-breadcrumb')).toBeVisible({ timeout: 15_000 }) + + const response = makeSubClusterResponse() + for (const sub of response.sub_clusters) { + const swatch = page.getByTestId(`subcluster-legend-swatch-${sub.index}`) + await expect(swatch).toBeVisible({ timeout: 10_000 }) + const color = await swatch.evaluate((el) => getComputedStyle(el).backgroundColor) + expect(color).toBe(expectedRgb(CLUSTER_COLORS[sub.index])) + } + }) }) diff --git a/frontend/src/components/plot/ClusterLegend.tsx b/frontend/src/components/plot/ClusterLegend.tsx index 24e8b69..9f82a50 100644 --- a/frontend/src/components/plot/ClusterLegend.tsx +++ b/frontend/src/components/plot/ClusterLegend.tsx @@ -112,6 +112,7 @@ export default function ClusterLegend() { className="flex items-center space-x-2 px-1 py-0.5" >