diff --git a/e2e-tests/tests/timeline-view-editing.test.ts b/e2e-tests/tests/timeline-view-editing.test.ts index 5b9e39ea52..3c4dd7fd81 100644 --- a/e2e-tests/tests/timeline-view-editing.test.ts +++ b/e2e-tests/tests/timeline-view-editing.test.ts @@ -50,6 +50,7 @@ test.describe.serial('Timeline View Editing', () => { test('Add an activity to the parent plan', async () => { await plan.showPanel(PanelNames.TIMELINE_ITEMS); await plan.addActivity('PickBanana'); + await plan.addActivity('PeelBanana'); }); test('Change the start time of the activity', async () => { @@ -105,26 +106,116 @@ test.describe.serial('Timeline View Editing', () => { // Look for back button indicating that the row editor is active expect(page.locator('.section-back-button ').first()).toBeDefined(); - const existingLayerCount = await page.locator('.timeline-layer').count(); - // Give the row a name await page.locator('input[name="name"]').first().fill(rowName); await page.locator('input[name="name"]').first().blur(); + }); + + test('Add an activity layer', async () => { + const activityLayerEditor = page.getByLabel('Activity Layer-editor'); + const existingLayerCount = await activityLayerEditor.locator('.timeline-layer-editor').count(); - // Add a layer - await page.getByRole('button', { name: 'New Layer' }).click(); - const newLayerCount = await page.locator('.timeline-layer').count(); + // Add an activity layer + await activityLayerEditor.getByRole('button', { name: 'New Activity Layer' }).click(); + const newLayerCount = await activityLayerEditor.locator('.timeline-layer-editor').count(); expect(newLayerCount - existingLayerCount).toEqual(1); - // Expect an activity layer to be created by default - expect(await page.locator('select[name="chartType"]').last().inputValue()).toBe('activity'); + // Expect the activity layer to include all activities + expect(await activityLayerEditor.locator('.timeline-layer-editor').first()).toHaveText('All Activities'); + }); - // Expect the filter list to open - await page.getByPlaceholder('Search').last().click(); - await expect(page.locator('.menu-slot > .header')).toBeDefined(); + test('Edit an activity layer', async () => { + const activityLayerEditor = page.getByLabel('Activity Layer-editor'); + + // Open the activity filter builder + await activityLayerEditor + .locator('.timeline-layer-editor') + .first() + .getByLabel('activity-filter-builder-trigger') + .click(); + + // Expect that the modal is present + const modal = activityLayerEditor.getByLabel('activity-filter-builder'); + expect(modal).toBeDefined(); + + // Expect that layer name is showing in the name input + expect(modal.locator('input[name="layer-name"]')).toHaveValue('All Activities'); + + // Expect that the resulting types list is not empty + const resultingTypesList = modal.locator('.resulting-types-list'); + const allActivityTypesCount = await resultingTypesList.locator('.activity-type-result').count(); + expect(allActivityTypesCount).toBeGreaterThan(0); + + // Expect that manually selecting types cause the types to appear in the resulting types list + await modal.locator("input[name='manual-types-filter-input']").click(); + expect(await modal.locator('.manual-types-menu').first()).toBeDefined(); + await modal.getByRole('menuitem', { name: 'ChangeProducer' }).click(); + await modal.getByRole('menuitem', { name: 'ControllableDurationActivity' }).click(); + await page.keyboard.press('Escape'); + + expect(await resultingTypesList.getByText('ChangeProducer')).toBeDefined(); + expect(await resultingTypesList.getByText('ControllableDurationActivity')).toBeDefined(); + + // Expect that dynamic types can be added + await modal.getByLabel('dynamic-types').getByRole('button', { name: 'Add Filter' }).click(); + expect(await modal.getByLabel('dynamic-types').getByRole('listitem').count()).toBe(1); + await modal.getByLabel('dynamic-types').getByRole('listitem').locator("input[name='filter-value']").fill('banana'); + expect(await resultingTypesList.locator('.activity-type-result').count()).toEqual(11); + + // Expect that global filters can be added + await modal.getByLabel('global-filters').getByRole('button', { name: 'Add Filter' }).click(); + expect(await modal.getByLabel('global-filters').getByRole('listitem').count()).toBe(1); + // Select parameter field + await modal.getByLabel('global-filters').locator("select[aria-label='field']").selectOption('Parameter'); + // Select specific parameter + await modal.getByLabel('global-filters').getByText('Select Parameter').click(); + await modal.getByLabel('global-filters').getByText('quantity (int)').click(); + // Select operator + await modal.getByLabel('global-filters').locator("select[aria-label='operator']").selectOption('equals'); + // Fill filter value input + await modal.getByLabel('global-filters').getByRole('listitem').locator("input[name='filter-value']").fill('10'); + // Ensure that only one instance (PickBanana) is listed + expect(await modal.getByText('1 instance')).toBeDefined(); + + // Expect that type subfilters can be added + const activityResult = resultingTypesList.getByRole('listitem', { name: 'activity-type-result-PickBanana' }); + await activityResult.getByRole('button', { name: 'Add Filter' }).click(); + expect(await activityResult.getByRole('listitem').count()).toBe(1); + // Select name field + await activityResult.locator("select[aria-label='field']").selectOption('Name'); + // Select operator + await activityResult.locator("select[aria-label='operator']").selectOption('includes'); + // Fill filter value input + await activityResult.getByRole('listitem').locator("input[name='filter-value']").fill('foo'); + // Ensure that only one instance (PickBanana) is listed + expect(await modal.getByText('0 instances')).toBeDefined(); + + // Expect that type subfilters can be removed + await activityResult.getByRole('button', { name: 'Remove filter' }).click(); + expect(await modal.getByText('1 instance')).toBeDefined(); + + // Expect that global filters can be removed + await modal.getByLabel('global-filters').getByRole('button', { name: 'Remove filter' }).click(); + expect(await modal.getByText('2 instances')).toBeDefined(); + + // Expect that dynamic types can be removed + await modal.getByLabel('dynamic-types').getByRole('button', { name: 'Remove filter' }).click(); + expect(await resultingTypesList.locator('.activity-type-result').count()).toEqual(2); + + // Expect that manual types can be cleared + await modal.locator("input[name='manual-types-filter-input']").click(); + await modal.getByRole('menuitem', { name: 'ChangeProducer' }).click(); + await page.keyboard.press('Escape'); + await modal.getByRole('button', { name: 'Remove Types' }).click(); + expect(await resultingTypesList.locator('.activity-type-result').count()).toEqual(allActivityTypesCount); + + // Close the modal + await page.pause(); + await modal.getByRole('button', { name: 'close' }).click(); + }); - // Add all activities - await page.locator('button', { hasText: /Select [0-9]* activit/ }).click(); + test('Change activity layer settings', async () => { + const activityLayerEditor = await page.getByLabel('Activity Layer-editor'); // Expect to not see an activity tree group in this row expect(await page.locator('.timeline-row-wrapper', { hasText: rowName }).locator('.activity-tree').count()).toBe(0); @@ -141,9 +232,7 @@ test.describe.serial('Timeline View Editing', () => { ).toBe(1); // Delete an activity layer - await page.getByRole('button', { name: 'Layer Settings' }).last().click(); - await page.getByText('Delete Layer').click(); - const finalLayerCount = await page.locator('.timeline-layer').count(); - expect(finalLayerCount - newLayerCount).toEqual(-1); + await activityLayerEditor.locator('.timeline-layer-editor').first().getByRole('button', { name: 'Delete' }).click(); + expect(await activityLayerEditor.locator('.timeline-layer-editor').count()).toBe(0); }); }); diff --git a/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte b/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte index 7929e0498e..08196e96bd 100644 --- a/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte +++ b/src/components/timeline/form/TimelineEditor/ActivityFilterBuilder.svelte @@ -14,6 +14,7 @@ import type { ValueSchemaVariant } from '../../../../types/schema'; import type { ActivityLayerFilter, ActivityLayerFilterSubfieldSchema } from '../../../../types/timeline'; import { compare, getTarget, lowercase } from '../../../../utilities/generic'; + import { pluralize } from '../../../../utilities/text'; import { applyActivityLayerFilter, getMatchingTypesForActivityLayerFilter, @@ -341,8 +342,8 @@