diff --git a/.github/workflows/end2end_test.yml b/.github/workflows/end2end_test.yml index eee29c3544..cd4f40acb1 100644 --- a/.github/workflows/end2end_test.yml +++ b/.github/workflows/end2end_test.yml @@ -199,6 +199,14 @@ jobs: success_key: e2e-success-evaluations artifact_name: Playwright Report (Evaluations) postgres: false + - name: Distribution + index_script: index_distribution.py + playwright_project: distribution + shard: "1/1" + success_marker: .e2e-success-distribution + success_key: e2e-success-distribution + artifact_name: Playwright Report (Distribution) + postgres: false - name: General Postgres 1 of 2 index_script: index_general.py playwright_project: general diff --git a/lightly_studio/e2e-tests/index_distribution.py b/lightly_studio/e2e-tests/index_distribution.py index 8a12ff3bc1..7bd950c27a 100644 --- a/lightly_studio/e2e-tests/index_distribution.py +++ b/lightly_studio/e2e-tests/index_distribution.py @@ -60,6 +60,10 @@ DATASET_NAME = "distribution_example_dataset" RANDOM_SEED = 42 +COMPARISON_TAG_REVIEWED = "distribution_reviewed" +COMPARISON_TAG_PRIORITY = "distribution_priority" +COMPARISON_TAG_EMPTY = "distribution_empty" + # Distinct, weighted class pools per type so each distribution has a clear shape. CLASSIFICATION_CLASSES = ["daytime", "night", "dawn", "dusk", "indoor"] CLASSIFICATION_WEIGHTS = [0.45, 0.25, 0.15, 0.1, 0.05] @@ -114,6 +118,10 @@ def main() -> None: samples = list(dataset) rng = random.Random(RANDOM_SEED) + dataset.query()[:32].add_tag(COMPARISON_TAG_REVIEWED) + dataset.query()[16:48].add_tag(COMPARISON_TAG_PRIORITY) + dataset.query()[:0].add_tag(COMPARISON_TAG_EMPTY) + for sample in samples: annotations: list[CreateAnnotation] = [] diff --git a/lightly_studio_view/e2e/distribution/distribution-comparison.e2e-test.ts b/lightly_studio_view/e2e/distribution/distribution-comparison.e2e-test.ts new file mode 100644 index 0000000000..5a22d9a63e --- /dev/null +++ b/lightly_studio_view/e2e/distribution/distribution-comparison.e2e-test.ts @@ -0,0 +1,62 @@ +import { expect, test } from '../utils'; +import { distributionComparison } from './fixtures'; + +test('compares overlapping and empty sample tags without reloading or filtering the grid', async ({ + page, + samplesPage +}) => { + const firstId = await samplesPage.getTagIdByName(distributionComparison.firstTag); + const secondId = await samplesPage.getTagIdByName(distributionComparison.secondTag); + const emptyId = await samplesPage.getTagIdByName(distributionComparison.emptyTag); + expect(firstId).toBeTruthy(); + expect(secondId).toBeTruthy(); + expect(emptyId).toBeTruthy(); + + await page.getByTestId('side-panel-tabs-distribution').click(); + await expect(page.getByTestId('dataset-distribution-panel')).toBeVisible(); + const urlBeforeComparison = page.url(); + let navigationCount = 0; + let imageListRequestCount = 0; + const recordNavigation = () => navigationCount++; + const recordImageListRequest = (request: { url: () => string }) => { + if (request.url().includes('/images/list')) imageListRequestCount++; + }; + page.on('framenavigated', recordNavigation); + page.on('request', recordImageListRequest); + + const waitForGroupedCounts = (tagCount: number) => + page.waitForResponse((response) => { + if (!response.url().includes('/annotations/count-by-sample-tags')) return false; + const body = response.request().postDataJSON() as { sample_tag_ids?: string[] }; + return response.status() === 200 && body.sample_tag_ids?.length === tagCount; + }); + const selectTag = async (tagId: string, expectedCount: number) => { + const option = page.getByTestId(`dataset-distribution-tag-option-${tagId}`); + if (!(await option.isVisible())) { + await page.getByTestId('dataset-distribution-tag-select').click(); + } + const responsePromise = waitForGroupedCounts(expectedCount); + await option.click(); + return responsePromise; + }; + + await selectTag(firstId!, 1); + await selectTag(secondId!, 2); + const response = await selectTag(emptyId!, 3); + const requestBody = response.request().postDataJSON() as { sample_tag_ids: string[] }; + expect(requestBody.sample_tag_ids).toEqual([firstId, secondId, emptyId]); + const groupedCounts = (await response.json()) as { + sample_tag_id: string; + counts: { count: number }[]; + }[]; + const emptyCounts = groupedCounts.find((item) => item.sample_tag_id === emptyId)?.counts; + expect(emptyCounts?.length).toBeGreaterThan(0); + expect(emptyCounts?.every((item) => item.count === 0)).toBe(true); + + await expect(page.getByText(/3 sample tags/)).toBeVisible(); + expect(page.url()).toBe(urlBeforeComparison); + expect(navigationCount).toBe(0); + expect(imageListRequestCount).toBe(0); + page.off('framenavigated', recordNavigation); + page.off('request', recordImageListRequest); +}); diff --git a/lightly_studio_view/e2e/distribution/fixtures.ts b/lightly_studio_view/e2e/distribution/fixtures.ts new file mode 100644 index 0000000000..307aeeeea4 --- /dev/null +++ b/lightly_studio_view/e2e/distribution/fixtures.ts @@ -0,0 +1,5 @@ +export const distributionComparison = { + firstTag: 'distribution_reviewed', + secondTag: 'distribution_priority', + emptyTag: 'distribution_empty' +} as const; diff --git a/lightly_studio_view/playwright.config.ts b/lightly_studio_view/playwright.config.ts index fe7cf378ef..5e6b6a3337 100644 --- a/lightly_studio_view/playwright.config.ts +++ b/lightly_studio_view/playwright.config.ts @@ -50,6 +50,10 @@ export default defineConfig({ { name: 'evaluations', testDir: './e2e/evaluations' + }, + { + name: 'distribution', + testDir: './e2e/distribution' } ], diff --git a/lightly_studio_view/src/lib/components/DatasetDistributionPanel/TagComparisonSelect.svelte b/lightly_studio_view/src/lib/components/DatasetDistributionPanel/TagComparisonSelect.svelte index 876c51b0a0..1f7c6303b4 100644 --- a/lightly_studio_view/src/lib/components/DatasetDistributionPanel/TagComparisonSelect.svelte +++ b/lightly_studio_view/src/lib/components/DatasetDistributionPanel/TagComparisonSelect.svelte @@ -49,7 +49,11 @@ No sample tag found. {#each items as item (item.value)} - toggle(item.value)}> + toggle(item.value)} + >