From 7c08b5f49b479927b04c96e1be7fa39b2863b6c8 Mon Sep 17 00:00:00 2001 From: bduran Date: Thu, 11 Apr 2024 17:17:29 -0700 Subject: [PATCH] fix model deletion query add tests for model editting --- e2e-tests/fixtures/Constraints.ts | 10 +- e2e-tests/fixtures/Model.ts | 134 +++++++++++++++++++++ e2e-tests/fixtures/Models.ts | 10 ++ e2e-tests/fixtures/SchedulingConditions.ts | 10 +- e2e-tests/fixtures/SchedulingGoals.ts | 10 +- e2e-tests/tests/constraints.test.ts | 6 +- e2e-tests/tests/model.test.ts | 120 ++++++++++++++++++ e2e-tests/tests/models.test.ts | 2 +- e2e-tests/tests/plan.test.ts | 8 +- e2e-tests/tests/plans.test.ts | 2 +- src/components/model/ModelForm.svelte | 1 + src/routes/models/[id]/+page.svelte | 20 ++- src/utilities/gql.ts | 15 +++ 13 files changed, 323 insertions(+), 25 deletions(-) create mode 100644 e2e-tests/fixtures/Model.ts create mode 100644 e2e-tests/tests/model.test.ts diff --git a/e2e-tests/fixtures/Constraints.ts b/e2e-tests/fixtures/Constraints.ts index c2463f4dc6..5d15b85a7f 100644 --- a/e2e-tests/fixtures/Constraints.ts +++ b/e2e-tests/fixtures/Constraints.ts @@ -1,7 +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 { closeButton: Locator; @@ -19,10 +18,7 @@ export class Constraints { tableRow: Locator; tableRowDeleteButton: Locator; - constructor( - public page: Page, - public models: Models, - ) { + constructor(public page: Page) { this.constraintName = uniqueNamesGenerator({ dictionaries: [adjectives, colors, animals] }); this.updatePage(page); } @@ -83,6 +79,10 @@ export class Constraints { await this.page.waitForTimeout(250); } + async gotoNew() { + await this.page.goto('/constraints/new', { waitUntil: 'networkidle' }); + } + updatePage(page: Page): void { this.closeButton = page.locator(`button:has-text("Close")`); this.confirmModal = page.locator(`.modal:has-text("Delete Constraint")`); diff --git a/e2e-tests/fixtures/Model.ts b/e2e-tests/fixtures/Model.ts new file mode 100644 index 0000000000..37ea77644a --- /dev/null +++ b/e2e-tests/fixtures/Model.ts @@ -0,0 +1,134 @@ +import { expect, type Locator, type Page } from '@playwright/test'; +import { Constraints } from './Constraints.js'; +import { Models } from './Models.js'; +import { SchedulingConditions } from './SchedulingConditions.js'; +import { SchedulingGoals } from './SchedulingGoals.js'; + +export class Model { + associationTable: Locator; + closeButton: Locator; + conditionRadioButton: Locator; + confirmModal: Locator; + confirmModalDeleteButton: Locator; + constraintRadioButton: Locator; + deleteButton: Locator; + descriptionInput: Locator; + goalRadioButton: Locator; + libraryRadioButton: Locator; + modelRadioButton: Locator; + nameInput: Locator; + newPlanButton: Locator; + saveButton: Locator; + versionInput: Locator; + + constructor( + public page: Page, + public models: Models, + public constraints: Constraints, + public schedulingGoals: SchedulingGoals, + public schedulingConditions: SchedulingConditions, + ) { + this.updatePage(page); + } + + async close() { + await this.closeButton.click(); + await expect(this.page).toHaveURL('.*/models$'); + } + + async deleteModel() { + await expect(this.confirmModal).not.toBeVisible(); + await this.deleteButton.click(); + await this.confirmModal.waitFor({ state: 'attached' }); + await this.confirmModal.waitFor({ state: 'visible' }); + await expect(this.confirmModal).toBeVisible(); + + await expect(this.confirmModalDeleteButton).toBeVisible(); + await this.confirmModalDeleteButton.click(); + } + + /** + * Wait for Hasura events to finish seeding the database after a model is created. + * If we do not wait then navigation to the plan will fail because the data is not there yet. + * If your tests fail then the timeout might be too short. + * Re-run the tests and increase the timeout if you get consistent failures. + */ + async goto() { + await this.page.waitForTimeout(1200); + await this.page.goto(`/models/${this.models.modelId}`, { waitUntil: 'networkidle' }); + await this.page.waitForTimeout(250); + } + + async saveModel() { + await expect(this.saveButton).toBeVisible(); + await this.saveButton.click(); + await expect(this.saveButton).toBeVisible(); + } + + async switchToConditions() { + await this.conditionRadioButton.click(); + this.updatePage(this.page); + await expect(this.page.getByText('Condition - Definition')).toBeVisible(); + } + + async switchToConstraints() { + await this.constraintRadioButton.click(); + this.updatePage(this.page); + await expect(this.page.getByText('Constraint - Definition')).toBeVisible(); + } + + async switchToGoals() { + await this.goalRadioButton.click(); + this.updatePage(this.page); + await expect(this.page.getByText('Goal - Definition')).toBeVisible(); + } + + async switchToLibraryView() { + await this.libraryRadioButton.click(); + this.updatePage(this.page); + await expect(this.associationTable).toBeVisible(); + } + + async switchToModelView() { + await this.modelRadioButton.click(); + this.updatePage(this.page); + await expect(this.associationTable).not.toBeVisible(); + } + + async updateDescription(modelDescription: string) { + await this.descriptionInput.click(); + await this.descriptionInput.fill(modelDescription); + await expect(this.descriptionInput).toHaveValue(modelDescription); + } + + async updateName(modelName: string) { + await this.nameInput.click(); + await this.nameInput.fill(modelName); + await expect(this.nameInput).toHaveValue(modelName); + } + + async updatePage(page: Page): Promise> { + this.closeButton = page.getByRole('button', { name: 'Close' }); + this.conditionRadioButton = page.getByRole('button', { name: 'Conditions' }); + this.constraintRadioButton = page.getByRole('button', { name: 'Constraints' }); + this.deleteButton = page.getByRole('button', { name: 'Delete model' }); + this.descriptionInput = page.locator('textarea[name="description"]'); + this.goalRadioButton = page.getByRole('button', { name: 'Goals' }); + this.goalRadioButton = page.getByRole('button', { name: 'Goals' }); + this.libraryRadioButton = page.getByRole('button', { name: 'Library' }); + this.modelRadioButton = page.getByRole('button', { exact: true, name: 'Model' }); + this.nameInput = page.locator('input[name="name"]'); + this.newPlanButton = page.getByRole('button', { name: 'New plan with model' }); + this.versionInput = page.locator('input[name="version"]'); + this.associationTable = page.getByRole('treegrid'); + this.saveButton = page.getByRole('button', { name: 'Save' }); + this.confirmModal = page.locator(`.modal:has-text("Delete Model")`); + this.confirmModalDeleteButton = page.locator(`.modal:has-text("Delete Model") >> button:has-text("Delete")`); + } + + async updateVersion(modelVersion: string) { + await this.nameInput.focus(); + await this.nameInput.fill(modelVersion); + await expect(this.nameInput).toHaveValue(modelVersion); + } +} diff --git a/e2e-tests/fixtures/Models.ts b/e2e-tests/fixtures/Models.ts index b81959edb8..a4391b19a1 100644 --- a/e2e-tests/fixtures/Models.ts +++ b/e2e-tests/fixtures/Models.ts @@ -7,15 +7,18 @@ export class Models { confirmModal: Locator; confirmModalDeleteButton: Locator; createButton: Locator; + createPlanButton: Locator; creatingButton: Locator; inputFile: Locator; inputName: Locator; inputVersion: Locator; jarPath: string = 'e2e-tests/data/banananation-develop.jar'; // TODO: Pull .jar from aerie project. + modelId: string; modelName: string; modelVersion: string = '1.0.0'; tableRow: Locator; tableRowDeleteButton: Locator; + tableRowModelId: Locator; constructor(public page: Page) { this.modelName = uniqueNamesGenerator({ dictionaries: [adjectives, colors, animals] }); @@ -35,6 +38,11 @@ export class Models { await this.tableRow.waitFor({ state: 'attached' }); await this.tableRow.waitFor({ state: 'visible' }); await expect(this.tableRow).toBeVisible(); + await expect(this.tableRowModelId).toBeVisible(); + const el = await this.tableRowModelId.elementHandle(); + if (el) { + this.modelId = (await el.textContent()) as string; + } } async deleteModel() { @@ -88,6 +96,7 @@ export class Models { this.confirmModalDeleteButton = page.locator(`.modal:has-text("Delete Model") >> button:has-text("Delete")`); this.createButton = page.getByRole('button', { name: 'Create' }); this.creatingButton = page.getByRole('button', { name: 'Creating...' }); + this.createPlanButton = page.getByRole('button', { name: 'New plan with model' }); this.inputFile = page.locator('input[name="file"]'); this.inputName = page.locator('input[name="name"]'); this.inputVersion = page.locator('input[name="version"]'); @@ -96,5 +105,6 @@ export class Models { this.tableRowDeleteButton = page.locator( `.ag-row:has-text("${this.modelName}") >> button[aria-label="Delete Model"]`, ); + this.tableRowModelId = page.locator(`.ag-row:has-text("${this.modelName}") > div >> nth=0`); } } diff --git a/e2e-tests/fixtures/SchedulingConditions.ts b/e2e-tests/fixtures/SchedulingConditions.ts index f0b280d922..b1329b0b43 100644 --- a/e2e-tests/fixtures/SchedulingConditions.ts +++ b/e2e-tests/fixtures/SchedulingConditions.ts @@ -1,7 +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 SchedulingConditions { closeButton: Locator; @@ -20,10 +19,7 @@ export class SchedulingConditions { tableRow: Locator; tableRowDeleteButton: Locator; - constructor( - public page: Page, - public models: Models, - ) { + constructor(public page: Page) { this.conditionName = uniqueNamesGenerator({ dictionaries: [adjectives, colors, animals] }); this.updatePage(page); } @@ -85,6 +81,10 @@ export class SchedulingConditions { await this.page.waitForSelector(`input[placeholder="Filter conditions"]`, { state: 'attached' }); } + async gotoNew() { + await this.page.goto('/scheduling/conditions/new', { waitUntil: 'networkidle' }); + } + updatePage(page: Page): void { this.closeButton = page.locator(`button:has-text("Close")`); this.confirmModal = page.locator(`.modal:has-text("Delete Scheduling Condition")`); diff --git a/e2e-tests/fixtures/SchedulingGoals.ts b/e2e-tests/fixtures/SchedulingGoals.ts index 77fba36c9d..9a6d85aa4f 100644 --- a/e2e-tests/fixtures/SchedulingGoals.ts +++ b/e2e-tests/fixtures/SchedulingGoals.ts @@ -1,6 +1,5 @@ import { expect, type Locator, type Page } from '@playwright/test'; import { fillEditorText } from '../utilities/editor.js'; -import { Models } from './Models.js'; export class SchedulingGoals { closeButton: Locator; @@ -18,10 +17,7 @@ export class SchedulingGoals { tableRowDeleteButtonSelector: (goalName: string) => Locator; tableRowSelector: (goalName: string) => Locator; - constructor( - public page: Page, - public models: Models, - ) { + constructor(public page: Page) { this.updatePage(page); } @@ -83,6 +79,10 @@ export class SchedulingGoals { await this.page.waitForSelector(`input[placeholder="Filter goals"]`, { state: 'attached' }); } + async gotoNew() { + await this.page.goto('/scheduling/goals/new', { waitUntil: 'networkidle' }); + } + updatePage(page: Page): void { this.closeButton = page.locator(`button:has-text("Close")`); this.confirmModal = page.locator(`.modal:has-text("Delete Scheduling Goal")`); diff --git a/e2e-tests/tests/constraints.test.ts b/e2e-tests/tests/constraints.test.ts index 5b2c242a08..eb4c654a8d 100644 --- a/e2e-tests/tests/constraints.test.ts +++ b/e2e-tests/tests/constraints.test.ts @@ -21,9 +21,9 @@ test.beforeAll(async ({ browser }) => { models = new Models(page); plans = new Plans(page, models); - constraints = new Constraints(page, models); - schedulingConditions = new SchedulingConditions(page, models); - schedulingGoals = new SchedulingGoals(page, models); + constraints = new Constraints(page); + schedulingConditions = new SchedulingConditions(page); + schedulingGoals = new SchedulingGoals(page); plan = new Plan(page, plans, constraints, schedulingGoals, schedulingConditions); await models.goto(); diff --git a/e2e-tests/tests/model.test.ts b/e2e-tests/tests/model.test.ts new file mode 100644 index 0000000000..ef2000f1b2 --- /dev/null +++ b/e2e-tests/tests/model.test.ts @@ -0,0 +1,120 @@ +import test, { expect, type BrowserContext, type Page } from '@playwright/test'; +import { adjectives, animals, colors, uniqueNamesGenerator } from 'unique-names-generator'; +import { Constraints } from '../fixtures/Constraints.js'; +import { Model } from '../fixtures/Model.js'; +import { Models } from '../fixtures/Models.js'; +import { SchedulingConditions } from '../fixtures/SchedulingConditions.js'; +import { SchedulingGoals } from '../fixtures/SchedulingGoals.js'; + +let constraints: Constraints; +let context: BrowserContext; +let models: Models; +let model: Model; +let page: Page; +let schedulingConditions: SchedulingConditions; +let schedulingGoals: SchedulingGoals; +let schedulingGoalName: string; + +const checkboxSelector = 'Press SPACE to toggle cell'; + +test.beforeAll(async ({ baseURL, browser }) => { + context = await browser.newContext(); + page = await context.newPage(); + + models = new Models(page); + constraints = new Constraints(page); + schedulingConditions = new SchedulingConditions(page); + schedulingGoals = new SchedulingGoals(page); + model = new Model(page, models, constraints, schedulingGoals, schedulingConditions); + schedulingGoalName = uniqueNamesGenerator({ dictionaries: [adjectives, colors, animals] }); + await constraints.gotoNew(); + await constraints.createConstraint(baseURL); + await schedulingConditions.gotoNew(); + await schedulingConditions.createSchedulingCondition(baseURL); + await schedulingGoals.gotoNew(); + await schedulingGoals.createSchedulingGoal(baseURL, schedulingGoalName); + await models.goto(); + await models.createModel(); + await model.goto(); +}); + +test.afterAll(async () => { + await model.deleteModel(); + await constraints.goto(); + await constraints.deleteConstraint(); + await schedulingConditions.goto(); + await schedulingConditions.deleteSchedulingCondition(); + await schedulingGoals.goto(); + await schedulingGoals.deleteSchedulingGoal(schedulingGoalName); + await page.close(); + await context.close(); +}); + +test.describe.serial.only('Model', () => { + test('Should be able to update the name of a model', async () => { + await model.updateName(uniqueNamesGenerator({ dictionaries: [adjectives, colors, animals] })); + }); + + test('Should be able to update the description of a model', async () => { + await model.updateDescription('Description of the model'); + }); + + test('Should be able to update the version of a model', async () => { + await model.updateVersion('2.0.0'); + }); + + test('Should be able to add a constraint to the model and specify a version', async () => { + await model.switchToConstraints(); + await model.switchToLibraryView(); + await model.switchToLibraryView(); + await model.associationTable + .getByRole('row', { name: model.constraints.constraintName }) + .getByLabel(checkboxSelector) + .check(); + await model.switchToModelView(); + await expect(page.getByRole('button', { name: model.constraints.constraintName })).toBeVisible(); + await expect( + page.getByRole('button', { name: model.constraints.constraintName }).getByRole('combobox'), + ).toHaveValue(''); + page.getByRole('button', { name: model.constraints.constraintName }).getByRole('combobox').selectOption('0'); + await expect( + page.getByRole('button', { name: model.constraints.constraintName }).getByRole('combobox'), + ).toHaveValue('0'); + }); + + test('Should be able to add a scheduling condition to the model and specify a version', async () => { + await model.switchToConditions(); + await model.switchToLibraryView(); + await model.associationTable + .getByRole('row', { name: model.schedulingConditions.conditionName }) + .getByLabel(checkboxSelector) + .check(); + await model.switchToModelView(); + await expect(page.getByRole('button', { name: model.schedulingConditions.conditionName })).toBeVisible(); + await expect( + page.getByRole('button', { name: model.schedulingConditions.conditionName }).getByRole('combobox'), + ).toHaveValue(''); + page + .getByRole('button', { name: model.schedulingConditions.conditionName }) + .getByRole('combobox') + .selectOption('0'); + await expect( + page.getByRole('button', { name: model.schedulingConditions.conditionName }).getByRole('combobox'), + ).toHaveValue('0'); + }); + + test('Should be able to add a scheduling goal to the model and specify a version', async () => { + await model.switchToGoals(); + await model.switchToLibraryView(); + await model.associationTable.getByRole('row', { name: schedulingGoalName }).getByLabel(checkboxSelector).check(); + await model.switchToModelView(); + await expect(page.getByRole('button', { name: schedulingGoalName })).toBeVisible(); + await expect(page.getByRole('button', { name: schedulingGoalName }).getByRole('combobox')).toHaveValue(''); + page.getByRole('button', { name: schedulingGoalName }).getByRole('combobox').selectOption('0'); + await expect(page.getByRole('button', { name: schedulingGoalName }).getByRole('combobox')).toHaveValue('0'); + }); + + test('Should successfully save the model changes', async () => { + await model.saveModel(); + }); +}); diff --git a/e2e-tests/tests/models.test.ts b/e2e-tests/tests/models.test.ts index 304cbfdcdb..4ab89ddb0b 100644 --- a/e2e-tests/tests/models.test.ts +++ b/e2e-tests/tests/models.test.ts @@ -10,7 +10,7 @@ test.beforeAll(async ({ browser }) => { context = await browser.newContext(); page = await context.newPage(); models = new Models(page); - constraints = new Constraints(page, models); + constraints = new Constraints(page); await models.goto(); }); diff --git a/e2e-tests/tests/plan.test.ts b/e2e-tests/tests/plan.test.ts index f37103f0c3..1ecdd537c6 100644 --- a/e2e-tests/tests/plan.test.ts +++ b/e2e-tests/tests/plan.test.ts @@ -21,9 +21,9 @@ test.beforeAll(async ({ browser }) => { models = new Models(page); plans = new Plans(page, models); - constraints = new Constraints(page, models); - schedulingConditions = new SchedulingConditions(page, models); - schedulingGoals = new SchedulingGoals(page, models); + constraints = new Constraints(page); + schedulingConditions = new SchedulingConditions(page); + schedulingGoals = new SchedulingGoals(page); plan = new Plan(page, plans, constraints, schedulingGoals, schedulingConditions); await models.goto(); @@ -97,7 +97,7 @@ test.describe.serial('Plan', () => { await expect(plan.panelTimelineEditor).toBeVisible(); }); - test(`Hovering on 'Activites' in the top navigation bar should show the activity checking menu`, async () => { + test(`Hovering on 'Activities' in the top navigation bar should show the activity checking menu`, async () => { await expect(plan.navButtonActivityCheckingMenu).not.toBeVisible(); plan.navButtonActivityChecking.hover(); await expect(plan.navButtonActivityCheckingMenu).toBeVisible(); diff --git a/e2e-tests/tests/plans.test.ts b/e2e-tests/tests/plans.test.ts index ac2fdeca19..c3d8f6c9ad 100644 --- a/e2e-tests/tests/plans.test.ts +++ b/e2e-tests/tests/plans.test.ts @@ -39,7 +39,7 @@ test.describe.serial('Plans', () => { }) => { await models.goto(); await models.tableRow.click(); - await page.getByRole('button', { name: 'New plan with model' }).click(); + await models.createPlanButton.click(); await expect(page).toHaveURL(`${baseURL}/plans`); const { text } = await plans.selectedModel(); expect(text).toEqual(`${models.modelName} (Version: ${models.modelVersion})`); diff --git a/src/components/model/ModelForm.svelte b/src/components/model/ModelForm.svelte index b3cae192d1..3a4a0bf1b9 100644 --- a/src/components/model/ModelForm.svelte +++ b/src/components/model/ModelForm.svelte @@ -108,6 +108,7 @@