From 3cd3e2075ca86e0ff653784ef85b821eb6b0df3f Mon Sep 17 00:00:00 2001 From: Aaron Plave Date: Mon, 30 Dec 2024 15:03:15 -0800 Subject: [PATCH] e2e tests and fixes --- e2e-tests/tests/timeline-view-editing.test.ts | 121 +++++++++++++++--- .../ActivityFilterBuilder.svelte | 22 ++-- .../TimelineEditor/ActivityTypeResult.svelte | 2 +- .../form/TimelineEditor/Draggable.svelte | 2 + .../form/TimelineEditor/DynamicFilter.svelte | 13 +- .../form/TimelineEditor/EditorSection.svelte | 6 +- .../TimelineEditor/TimelineLayerEditor.svelte | 1 + .../timeline/form/TimelineEditorPanel.svelte | 24 ---- 8 files changed, 136 insertions(+), 55 deletions(-) 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 @@
{#if shown} - - @@ -368,7 +370,7 @@
-
+
Manually Select Types {#if dirtyFilter.static_types?.length} @@ -387,6 +389,7 @@
-
+
Dynamically Select Types @@ -450,6 +453,7 @@
{#if dirtyFilter.dynamic_type_filters?.length}
-
+
{#each dirtyFilter.dynamic_type_filters as filter, i (filter.id)} {/if}
-
+
Global Filters @@ -490,6 +494,7 @@
{#if dirtyFilter.global_filters?.length}
-
+
{#each dirtyFilter.global_filters as filter, i (filter.id)}
{matchingTypes.length} types
-
{instanceCount} instances
+
{instanceCount} instance{pluralize(instanceCount)}
@@ -553,6 +558,7 @@ slot="right" on:click={() => onAddTypeSubfilter(type.name)} class="st-button icon" + aria-label="Add Filter" use:tooltip={{ content: 'Add Filter' }} > diff --git a/src/components/timeline/form/TimelineEditor/ActivityTypeResult.svelte b/src/components/timeline/form/TimelineEditor/ActivityTypeResult.svelte index 5f932eb7eb..3e212952b3 100644 --- a/src/components/timeline/form/TimelineEditor/ActivityTypeResult.svelte +++ b/src/components/timeline/form/TimelineEditor/ActivityTypeResult.svelte @@ -15,7 +15,7 @@ }>(); -
+
diff --git a/src/components/timeline/form/TimelineEditor/Draggable.svelte b/src/components/timeline/form/TimelineEditor/Draggable.svelte index 59988df4ec..3bc817e44f 100644 --- a/src/components/timeline/form/TimelineEditor/Draggable.svelte +++ b/src/components/timeline/form/TimelineEditor/Draggable.svelte @@ -4,6 +4,7 @@ import { draggable, type DragOptions } from '@neodrag/svelte'; import Resizable from './Resizable.svelte'; + export let ariaLabel: string = ''; export let dragOptions: DragOptions = {}; export let className: string = ''; export let initialWidth: number = 500; @@ -26,6 +27,7 @@
-
+
{#if verb}
{verb}
{/if} @@ -204,12 +204,13 @@
{#if currentType === 'string'} - + {:else if currentOperator === 'is_within' || currentOperator === 'is_not_within'} {#if Array.isArray(currentValue)}
To
- +
{:else if currentType === 'duration'} - +
{:else if currentType === 'boolean'} - @@ -270,6 +272,7 @@ )}
(); -
+
{pluralizedItem}
{#if creatable} @@ -36,6 +39,7 @@ {/if}