Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scheduling Subscription and Canceling #1141

Merged
merged 10 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions e2e-tests/fixtures/Constraints.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { adjectives, animals, colors, uniqueNamesGenerator } from 'unique-names-generator';
import { fillEditorText } from '../utilities/editor.js';
import { Models } from './Models.js';

export class Constraints {
Expand Down Expand Up @@ -59,9 +60,7 @@ export class Constraints {
}

async fillConstraintDefinition() {
await this.inputConstraintDefinition.focus();
await this.inputConstraintDefinition.fill(this.constraintDefinition);
await this.inputConstraintDefinition.evaluate(e => e.blur());
await fillEditorText(this.inputConstraintDefinition, this.constraintDefinition);
}

async fillConstraintDescription() {
Expand Down
12 changes: 2 additions & 10 deletions e2e-tests/fixtures/ExpansionRules.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect, type Locator, type Page } from '@playwright/test';
import os from 'node:os';
import { adjectives, animals, colors, uniqueNamesGenerator } from 'unique-names-generator';
import { fillEditorText } from '../utilities/editor.js';
import { getOptionValueFromText } from '../utilities/selectors.js';
import { Dictionaries } from './Dictionaries.js';
import { Models } from './Models.js';
Expand Down Expand Up @@ -75,15 +75,7 @@ export class ExpansionRules {
}

async fillInputEditor() {
await this.inputEditor.focus();
// Need to emulate actual clearing of editor instead of manipulating the
// underlying textbox, see: https://github.com/microsoft/playwright/issues/14126#issuecomment-1728327258
const isMac = os.platform() === 'darwin';
const modifier = isMac ? 'Meta' : 'Control';
await this.inputEditor.press(`${modifier}+A`);
await this.inputEditor.press(`Backspace`);
await this.inputEditor.fill(this.ruleLogic);
await this.inputEditor.evaluate(e => e.blur());
await fillEditorText(this.inputEditor, this.ruleLogic);
}

async fillInputName() {
Expand Down
38 changes: 21 additions & 17 deletions e2e-tests/fixtures/Plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,17 @@ export class Plan {
}

async deleteAllActivities() {
await this.panelActivityDirectivesTable.getByRole('gridcell').first().click({ button: 'right' });
await this.page.locator('.context-menu > .context-menu-item:has-text("Select All Activity Directives")').click();
await this.panelActivityDirectivesTable.getByRole('gridcell').first().click({ button: 'right' });
await this.page.getByText(/Delete \d+ Activit(y|ies) Directives?/).click();

const applyPresetButton = this.page.getByRole('button', { name: 'Confirm' });
await applyPresetButton.waitFor({ state: 'attached', timeout: 1000 });
await applyPresetButton.click();
const gridCells = await this.panelActivityDirectivesTable.getByRole('gridcell');
if ((await gridCells.count()) > 0) {
await this.panelActivityDirectivesTable.getByRole('gridcell').first().click({ button: 'right' });
await this.page.locator('.context-menu > .context-menu-item:has-text("Select All Activity Directives")').click();
await this.panelActivityDirectivesTable.getByRole('gridcell').first().click({ button: 'right' });
await this.page.getByText(/Delete \d+ Activit(y|ies) Directives?/).click();

const confirmDeletionButton = this.page.getByRole('button', { name: 'Confirm' });
await confirmDeletionButton.waitFor({ state: 'attached', timeout: 1000 });
await confirmDeletionButton.click();
}
}

async fillActivityPresetName(presetName: string) {
Expand Down Expand Up @@ -164,18 +167,14 @@ export class Plan {

async runAnalysis() {
await this.analyzeButton.click();
await this.page.waitForSelector(this.schedulingStatusSelector('Incomplete'), { state: 'attached', strict: true });
await this.page.waitForSelector(this.schedulingStatusSelector('Incomplete'), { state: 'visible', strict: true });
await this.page.waitForSelector(this.schedulingStatusSelector('Complete'), { state: 'attached', strict: true });
await this.page.waitForSelector(this.schedulingStatusSelector('Complete'), { state: 'visible', strict: true });
await this.waitForSchedulingStatus('Incomplete');
await this.waitForSchedulingStatus('Complete');
}

async runScheduling() {
async runScheduling(expectedFinalState = 'Complete') {
await this.scheduleButton.click();
await this.page.waitForSelector(this.schedulingStatusSelector('Incomplete'), { state: 'attached', strict: true });
await this.page.waitForSelector(this.schedulingStatusSelector('Incomplete'), { state: 'visible', strict: true });
await this.page.waitForSelector(this.schedulingStatusSelector('Complete'), { state: 'attached', strict: true });
await this.page.waitForSelector(this.schedulingStatusSelector('Complete'), { state: 'visible', strict: true });
await this.waitForSchedulingStatus('Incomplete');
await this.waitForSchedulingStatus(expectedFinalState);
}

async selectActivityAnchorByIndex(index: number) {
Expand Down Expand Up @@ -343,4 +342,9 @@ export class Plan {
this.schedulingConditionNewButton = page.locator(`button[name="new-scheduling-condition"]`);
this.schedulingSatisfiedActivity = page.locator('.scheduling-goal-analysis-activities-list > .satisfied-activity');
}

async waitForSchedulingStatus(status: string) {
await this.page.waitForSelector(this.schedulingStatusSelector(status), { state: 'attached', strict: true });
await this.page.waitForSelector(this.schedulingStatusSelector(status), { state: 'visible', strict: true });
}
}
5 changes: 2 additions & 3 deletions e2e-tests/fixtures/SchedulingConditions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { adjectives, animals, colors, uniqueNamesGenerator } from 'unique-names-generator';
import { fillEditorText } from '../utilities/editor.js';
import { getOptionValueFromText } from '../utilities/selectors.js';
import { Models } from './Models.js';

Expand Down Expand Up @@ -64,9 +65,7 @@ export class SchedulingConditions {
}

async fillConditionDefinition() {
await this.inputConditionDefinition.focus();
await this.inputConditionDefinition.fill(this.conditionDefinition);
await this.inputConditionDefinition.evaluate(e => e.blur());
await fillEditorText(this.inputConditionDefinition, this.conditionDefinition);
}

async fillConditionDescription() {
Expand Down
5 changes: 2 additions & 3 deletions e2e-tests/fixtures/SchedulingGoals.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { fillEditorText } from '../utilities/editor.js';
import { getOptionValueFromText } from '../utilities/selectors.js';
import { Models } from './Models.js';

Expand Down Expand Up @@ -61,9 +62,7 @@ export class SchedulingGoals {
}

async fillGoalDefinition() {
await this.inputGoalDefinition.focus();
await this.inputGoalDefinition.fill(this.goalDefinition);
await this.inputGoalDefinition.evaluate(e => e.blur());
await fillEditorText(this.inputGoalDefinition, this.goalDefinition);
}

async fillGoalDescription() {
Expand Down
12 changes: 10 additions & 2 deletions e2e-tests/tests/scheduling.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ test.beforeAll(async ({ browser }) => {
});

test.afterAll(async () => {
await plan.deleteAllActivities();
await plans.goto();
await plans.deletePlan();
await models.goto();
Expand Down Expand Up @@ -65,14 +66,14 @@ test.describe.serial('Scheduling', () => {
await expect(plan.schedulingGoalEnabledCheckboxSelector(goalName1)).toBeChecked();
await plan.schedulingGoalEnabledCheckboxSelector(goalName1).uncheck();
await expect(plan.schedulingGoalEnabledCheckboxSelector(goalName1)).not.toBeChecked();
await plan.runScheduling();
await plan.runScheduling('Failed');
await expect(plan.schedulingGoalDifferenceBadge).not.toBeVisible();
await plan.schedulingGoalEnabledCheckboxSelector(goalName1).check();
await expect(plan.schedulingGoalEnabledCheckboxSelector(goalName1)).toBeChecked();
});

test('The condition should prevent showing +10 in the goals badge', async () => {
await plan.runScheduling();
await plan.runScheduling('Failed');
await expect(plan.schedulingGoalDifferenceBadge).toHaveText('+0');
});

Expand Down Expand Up @@ -108,6 +109,13 @@ test.describe.serial('Scheduling', () => {
await expect(plan.schedulingGoalDifferenceBadge).toHaveText('+0');
});

test('Modifying the plan should result in scheduling status marked as out of date', async () => {
await plan.showPanel('Activity Types');
await plan.panelActivityTypes.getByRole('button', { name: 'CreateActivity-GrowBanana' }).click();
await plan.showPanel('Scheduling Goals');
await plan.waitForSchedulingStatus('Modified');
});

test('Delete scheduling goal', async () => {
await schedulingGoals.deleteSchedulingGoal(goalName1);
});
Expand Down
14 changes: 14 additions & 0 deletions e2e-tests/utilities/editor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Locator } from '@playwright/test';
import os from 'node:os';

export async function fillEditorText(editor: Locator, text: string) {
await editor.focus();
// Need to emulate actual clearing of editor instead of manipulating the
// underlying textbox, see: https://github.com/microsoft/playwright/issues/14126#issuecomment-1728327258
const isMac = os.platform() === 'darwin';
const modifier = isMac ? 'Meta' : 'Control';
await editor.press(`${modifier}+A`);
await editor.press(`Backspace`);
await editor.fill(text);
await editor.evaluate(e => e.blur());
}
9 changes: 7 additions & 2 deletions src/components/scheduling/SchedulingGoalsPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
import { afterUpdate, beforeUpdate } from 'svelte';
import { PlanStatusMessages } from '../../enums/planStatusMessages';
import { plan, planReadOnly } from '../../stores/plan';
import { enableScheduling, schedulingSpecGoals, schedulingStatus, selectedSpecId } from '../../stores/scheduling';
import {
enableScheduling,
schedulingAnalysisStatus,
schedulingSpecGoals,
selectedSpecId,
} from '../../stores/scheduling';
import type { User } from '../../types/app';
import type { SchedulingSpecGoal } from '../../types/scheduling';
import type { ViewGridSection } from '../../types/view';
Expand Down Expand Up @@ -65,7 +70,7 @@
<Panel>
<svelte:fragment slot="header">
<GridMenu {gridSection} title="Scheduling Goals" />
<PanelHeaderActions status={$schedulingStatus} indeterminate>
<PanelHeaderActions status={$schedulingAnalysisStatus} indeterminate>
<PanelHeaderActionButton
title="Analyze"
on:click={() => effects.schedule(true, $plan, user)}
Expand Down
52 changes: 36 additions & 16 deletions src/routes/plans/[id]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,10 @@
import { planSnapshot, planSnapshotId } from '../../../stores/planSnapshots';
import {
enableScheduling,
latestAnalyses,
resetSchedulingStores,
latestSchedulingRequest,
satisfiedSchedulingGoalCount,
schedulingAnalysisStatus,
schedulingGoalCount,
schedulingStatus,
} from '../../../stores/scheduling';
import {
enableSimulation,
Expand Down Expand Up @@ -162,12 +161,12 @@
let invalidActivityCount: number = 0;
let planHasBeenLocked = false;
let planSnapshotActivityDirectives: ActivityDirective[] = [];
let schedulingAnalysisStatus: Status | null;
let simulationExtent: string | null;
let selectedSimulationStatus: Status | null;
let windowWidth = 0;
let simulationDataAbortController: AbortController;
let resourcesExternalAbortController: AbortController;
let schedulingStatusText: string = '';

$: ({ invalidActivityCount, ...activityErrorCounts } = $activityErrorRollups.reduce(
(prevCounts, activityErrorRollup) => {
Expand Down Expand Up @@ -366,13 +365,25 @@
}

$: compactNavMode = windowWidth < 1100;
$: schedulingAnalysisStatus = $schedulingStatus;
$: if ($latestAnalyses) {
if ($schedulingGoalCount !== $satisfiedSchedulingGoalCount) {
schedulingAnalysisStatus = Status.PartialSuccess;

$: if ($schedulingAnalysisStatus) {
let newSchedulingStatusText = '';
const satisfactionReport = `${$satisfiedSchedulingGoalCount} satisfied, ${
$schedulingGoalCount - $satisfiedSchedulingGoalCount
} unsatisfied`;
if ($schedulingAnalysisStatus === Status.Complete) {
newSchedulingStatusText = satisfactionReport;
} else if ($schedulingAnalysisStatus === Status.Failed) {
if ($latestSchedulingRequest && $latestSchedulingRequest.reason) {
newSchedulingStatusText = 'Failed to run scheduling';
} else {
newSchedulingStatusText = satisfactionReport;
}
} else if ($schedulingAnalysisStatus === Status.Modified) {
newSchedulingStatusText = 'Scheduling out-of-date';
}
schedulingStatusText = newSchedulingStatusText;
}

$: if ($simulationDatasetLatest) {
simulationExtent = getSimulationExtent($simulationDatasetLatest);
selectedSimulationStatus = getSimulationStatus($simulationDatasetLatest);
Expand All @@ -391,7 +402,6 @@
resetConstraintStores();
resetExpansionStores();
resetPlanStores();
resetSchedulingStores();
resetSimulationStores();
closeActiveModal();
});
Expand Down Expand Up @@ -703,16 +713,26 @@
permissionError={$planReadOnly
? PlanStatusMessages.READ_ONLY
: 'You do not have permission to run a scheduling analysis'}
status={schedulingAnalysisStatus}
statusText={schedulingAnalysisStatus === Status.PartialSuccess || schedulingAnalysisStatus === Status.Complete
? `${$satisfiedSchedulingGoalCount} satisfied, ${
$schedulingGoalCount - $satisfiedSchedulingGoalCount
} unsatisfied`
: ''}
status={$schedulingAnalysisStatus}
statusText={schedulingStatusText}
on:click={() => effects.schedule(true, $plan, data.user)}
indeterminate
>
<CalendarIcon />
<svelte:fragment slot="metadata">
<div class="st-typography-body">
{#if !$schedulingAnalysisStatus}
Scheduling analysis not run
{/if}
</div>
{#if $schedulingAnalysisStatus === Status.Pending || $schedulingAnalysisStatus === Status.Incomplete}
<button
on:click={() => effects.cancelSchedulingRequest($latestSchedulingRequest.analysis_id, data.user)}
class="st-button cancel-button"
disabled={$planReadOnly}>Cancel</button
>
{/if}
</svelte:fragment>
</PlanNavButton>
<ExtensionMenu
extensions={data.extensions}
Expand Down
Loading
Loading