Skip to content

Commit

Permalink
test: project create and delete react e2e [INFENG-456] (#9244)
Browse files Browse the repository at this point in the history
  • Loading branch information
djanicekpach authored May 22, 2024
1 parent 860f6a8 commit 6fa1420
Show file tree
Hide file tree
Showing 17 changed files with 246 additions and 33 deletions.
2 changes: 2 additions & 0 deletions webui/react/src/components/ProjectCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,12 @@ const ProjectCard: React.FC<Props> = ({
const classnames = [];
if (project.archived) classnames.push(css.archived);
if (project.workspaceId === 1) classnames.push(css.uncategorized);
const testId = `card-${project.name}`;

return (
<Card
actionMenu={!project.immutable && !hideActionMenu ? menu : undefined}
testId={testId}
onClick={(e: AnyMouseEvent) => handlePath(e, { path: paths.projectDetails(project.id) })}
onDropdown={onClick}>
<div className={classnames.join(' ')}>
Expand Down
2 changes: 1 addition & 1 deletion webui/react/src/e2e/fixtures/api.auth.fixture.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { APIRequest, APIRequestContext, Browser, BrowserContext, Page } from '@playwright/test';

export class ApiAuthFixture {
apiContext: APIRequestContext | undefined; // DNJ TODO - how to not have undefined
apiContext: APIRequestContext | undefined; // we can't get this until login, so may be undefined
readonly request: APIRequest;
readonly browser: Browser;
_page: Page | undefined;
Expand Down
23 changes: 18 additions & 5 deletions webui/react/src/e2e/models/ant/Tabs.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BaseComponent, NamedComponent } from 'e2e/models/BaseComponent';
import { BaseComponent, BaseComponentArgs, NamedComponent } from 'e2e/models/BaseComponent';

/**
* Returns a representation of the Tabs component from Ant.
Expand All @@ -16,13 +16,26 @@ export class Tabs extends NamedComponent {
});

/**
* Returns a representation of a tab item with the specified id.
* Returns a representation of a tab item with the specified id. The
* component type specified will be retuned for interaction with the tab.
* @param {string} id - the id of the menu item
* @param {new (args: BaseComponentArgs) => T} tabComponent - the type of the component that will be open after the tab is clicked.
*/
tab(id: string): BaseComponent {
return new BaseComponent({
parent: this.tablist,
typedTab<T extends BaseComponent>(
id: string,
tabComponent: new (args: BaseComponentArgs) => T,
): T {
return new tabComponent({
parent: this,
selector: `div.ant-tabs-tab-btn[id$="${id}"]`,
});
}

/**
* Returns a representation of a tab item with the specified id.
* @param {string} id - the id of the menu item
*/
tab(id: string): BaseComponent {
return this.typedTab(id, BaseComponent);
}
}
15 changes: 15 additions & 0 deletions webui/react/src/e2e/models/components/ProjcetActionDropdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { DropdownMenu } from 'e2e/models/hew/Dropdown';

/**
* Returns a representation of the Action Menu Dropdown component.
* @param {object} obj
* @param {BasePage} obj.root - root of the page
* @param {ComponentBasics} [obj.childNode] - optional if `openMethod` is present. It's the element we click on to open the dropdown.
* @param {Function} [obj.openMethod] - optional if `childNode` is present. It's the method to open the dropdown.
*/
export class ProjectActionDropdown extends DropdownMenu {
readonly edit = this.menuItem('edit');
readonly move = this.menuItem('move');
readonly archive = this.menuItem('switchArchive');
readonly delete = this.menuItem('delete');
}
21 changes: 21 additions & 0 deletions webui/react/src/e2e/models/components/ProjectCreateModal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { BaseComponent } from 'e2e/models/BaseComponent';
import { Modal } from 'e2e/models/hew/Modal';

/**
* Returns a representation of the Project create/edit modal component.
* This constructor represents the contents in src/components/Page.tsx.
* @param {object} obj
* @param {implementsGetLocator} obj.parent - The model's parent in the page hierarchy
* @param {string} [obj.selector] - Used instead of `defaultSelector`
*/
export class ProjectCreateModal extends Modal {
readonly projectName = new BaseComponent({
parent: this,
selector: 'input[id="projectName"]',
});

readonly description = new BaseComponent({
parent: this,
selector: 'input[id="description"]',
});
}
15 changes: 15 additions & 0 deletions webui/react/src/e2e/models/components/ProjectDeleteModal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { BaseComponent } from 'e2e/models/BaseComponent';
import { Modal } from 'e2e/models/hew/Modal';

/**
* Returns a representation of the Project delete modal component.
* @param {object} obj
* @param {implementsGetLocator} obj.parent - The parent used to locate this Page
* @param {string} [obj.selector] - Used instead of `defaultSelector`
*/
export class ProjectDeleteModal extends Modal {
readonly nameConfirmation: BaseComponent = new BaseComponent({
parent: this,
selector: 'input[id="projectName"]',
});
}
40 changes: 40 additions & 0 deletions webui/react/src/e2e/models/components/ProjectsPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { BaseComponent, NamedComponent } from 'e2e/models/BaseComponent';
import { Card } from 'e2e/models/hew/Card';

import { ProjectActionDropdown } from './ProjcetActionDropdown';
import { ProjectCreateModal } from './ProjectCreateModal';
import { ProjectDeleteModal } from './ProjectDeleteModal';

/**
* Returns a representation of the Projects Page component.
* This constructor represents the contents in src/components/Page.tsx.
* @param {object} obj
* @param {implementsGetLocator} obj.parent - The parent used to locate this Page
* @param {string} [obj.selector] - Used instead of `defaultSelector`
*/
export class ProjectsComponent extends NamedComponent {
override defaultSelector: string = '[id$=projects]';
readonly newProject = new BaseComponent({
parent: this._parent,
selector: '[data-testid=newProject]',
});
readonly createModal = new ProjectCreateModal({
parent: this.root,
});
readonly deleteModal = new ProjectDeleteModal({
parent: this.root,
});
readonly cardWithName = (name: string): ProjectsCard => {
return Card.withName({ name: name, parent: this._parent }, ProjectsCard);
};
}

class ProjectsCard extends Card {
override readonly actionMenu = new ProjectActionDropdown({
childNode: new BaseComponent({
parent: this,
selector: Card.actionMenuSelector,
}),
root: this.root,
});
}
12 changes: 12 additions & 0 deletions webui/react/src/e2e/models/components/ResourcePoolsPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { NamedComponent } from 'e2e/models/BaseComponent';

/**
* Returns a representation of the Resource Pools Page component.
* This constructor represents the contents in src/components/Page.tsx.
* @param {object} obj
* @param {implementsGetLocator} obj.parent - The parent used to locate this Page
* @param {string} [obj.selector] - Used instead of `defaultSelector`
*/
export class ResourcePoolsComponent extends NamedComponent {
override defaultSelector: string = '[id$=pools]';
}
12 changes: 12 additions & 0 deletions webui/react/src/e2e/models/components/TasksPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { NamedComponent } from 'e2e/models/BaseComponent';

/**
* Returns a representation of the Tasks Page component.
* This constructor represents the contents in src/components/Page.tsx.
* @param {object} obj
* @param {implementsGetLocator} obj.parent - The parent used to locate this Page
* @param {string} [obj.selector] - Used instead of `defaultSelector`
*/
export class TasksComponent extends NamedComponent {
override defaultSelector: string = '[id$=tasks]';
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { DropdownMenu } from 'e2e/models/hew/Dropdown';

/**
* Returns a representation of the Action Menu Dropdown component.
* Returns a representation of the Action Menu Dropdown component for workspaces.
* @param {object} obj
* @param {implementsGetLocator} obj.parent - The parent used to locate this dropdown. Normally dropdowns need to be the root.
* @param {string} [obj.selector] - Used instead of `defaultSelector`
* @param {BasePage} obj.root - root of the page
* @param {ComponentBasics} [obj.childNode] - optional if `openMethod` is present. It's the element we click on to open the dropdown.
* @param {Function} [obj.openMethod] - optional if `childNode` is present. It's the method to open the dropdown.
*/
export class WorkspaceActionDropdown extends DropdownMenu {
readonly pin = this.menuItem('switchPin');
Expand Down
14 changes: 13 additions & 1 deletion webui/react/src/e2e/models/components/WorkspaceDetails.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import { NamedComponent } from 'e2e/models/BaseComponent';
import { Pivot } from 'e2e/models/hew/Pivot';

import { ModelRegistryPage } from './ModelRegistry';
import { ProjectsComponent } from './ProjectsPage';
import { ResourcePoolsComponent } from './ResourcePoolsPage';
import { TasksComponent } from './TasksPage';

/**
* Returns a representation of the Projects Page component.
* Returns a representation of the Workspace Details Page component.
* This constructor represents the contents in src/components/Page.tsx.
* @param {object} obj
* @param {implementsGetLocator} obj.parent - The parent used to locate this Page
* @param {string} [obj.selector] - Used instead of `defaultSelector`
*/
export class WorkspaceDetails extends NamedComponent {
readonly defaultSelector: string = '[id=workspaceDetails]';
// The details sections are all subpages wrapped with a Pivot tab
readonly pivot = new Pivot({ parent: this });
readonly projects = this.pivot.typedTab('projects', ProjectsComponent);
readonly tasks = this.pivot.typedTab('tasks', TasksComponent);
readonly modelRegistry = this.pivot.typedTab('models', ModelRegistryPage);
readonly resourcePools = this.pivot.typedTab('pools', ResourcePoolsComponent);
}
9 changes: 3 additions & 6 deletions webui/react/src/e2e/models/components/WorkspacesList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@ export class WorkspacesList extends NamedComponent {
selector: '[data-testid="newWorkspace"]',
});
readonly cardWithName = (name: string): WorkspaceCard => {
return new WorkspaceCard({
parent: this,
selector: `[data-testid="card-${name}"]`,
});
return Card.withName({ name: name, parent: this }, WorkspaceCard);
};
}

Expand All @@ -32,10 +29,10 @@ export class WorkspacesList extends NamedComponent {
* @param {string} obj.selector - Used as a selector uesd to locate this object
*/
class WorkspaceCard extends Card {
readonly actionMenu = new WorkspaceActionDropdown({
override readonly actionMenu = new WorkspaceActionDropdown({
childNode: new BaseComponent({
parent: this,
selector: '[aria-label="Action menu"]',
selector: Card.actionMenuSelector,
}),
root: this.root,
});
Expand Down
42 changes: 41 additions & 1 deletion webui/react/src/e2e/models/hew/Card.ts
Original file line number Diff line number Diff line change
@@ -1 +1,41 @@
export { BaseComponent as Card } from 'e2e/models/BaseComponent';
import {
BaseComponent,
CanBeParent,
NamedComponent,
NamedComponentArgs,
} from 'e2e/models/BaseComponent';
import { WorkspaceActionDropdown } from 'e2e/models/components/WorkspaceActionDropdown';

import { DropdownMenu } from './Dropdown';

/**
* Returns a representation of the card component from Hew.
* This constructor represents the contents in hew/src/kit/Card.tsx.
* @param {object} obj
* @param {implementsGetLocator} obj.parent - The parent used to locate this card
* @param {string} obj.selector - Used instead of `defaultSelector`
*/
export class Card extends NamedComponent {
override defaultSelector: string = ''; // must be provided
// provide an actionMenu with a Dropdown to use
static actionMenuSelector = '[aria-label="Action menu"]';

// default to a workspace dropdown to avoid non-null but this should be overriden if a dropdown exists
readonly actionMenu: DropdownMenu = new WorkspaceActionDropdown({
childNode: new BaseComponent({
parent: this,
selector: Card.actionMenuSelector,
}),
root: this.root,
});

static withName<T extends Card>(
props: { name: string; parent: CanBeParent },
cardType: new (args: NamedComponentArgs) => T,
): T {
return new cardType({
parent: props.parent,
selector: `[data-testid="card-${props.name}"]`,
});
}
}
2 changes: 1 addition & 1 deletion webui/react/src/e2e/models/pages/Workspaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export class Workspaces extends BasePage {
readonly list = new WorkspacesList({
parent: this,
});
readonly projects = new WorkspaceDetails({
readonly details = new WorkspaceDetails({
parent: this,
});
readonly createModal = new WorkspaceCreateModal({
Expand Down
56 changes: 43 additions & 13 deletions webui/react/src/e2e/tests/projectsWorkspaces.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect } from '@playwright/test';
import { v4 } from 'uuid';

import { test } from 'e2e/fixtures/global-fixtures';
import { BasePage } from 'e2e/models/BasePage';
Expand All @@ -8,8 +9,10 @@ import { randId, safeName } from 'e2e/utils/naming';

test.describe('Projects', () => {
test.setTimeout(120_000);
let wsCreatedWithButton: string = '';
let wsCreatedWithSidebar: string = '';
let wsCreatedWithButton = '';
let wsCreatedWithSidebar = '';
let projectOneName = '';

const createWorkspaceAllFields = async function (
modal: WorkspaceCreateModal,
wsNamePrefix: string,
Expand Down Expand Up @@ -62,6 +65,19 @@ test.describe('Projects', () => {
}
});
});
// test.afterEach(async ({ page }) => {
// const workspacesPage = new Workspaces(page);

// });

// test('Projects and Workspaces archival and pinning', async ({ page }) => {
// await test.step('Archive a workspace', async () => {});
// await test.step('Unarchive a workspace', async () => {});
// await test.step('Unpin a workspace through the sidebar', async () => {});
// await test.step('Pin a workspace through the sidebar', async () => {});
// await test.step('Archive a project', async () => {});
// await test.step('Unarchive a project', async () => {});
// })

test('Projects and Workspaces CRUD', async ({ authedPage }) => {
const workspacesPage = new Workspaces(authedPage);
Expand Down Expand Up @@ -100,9 +116,20 @@ test.describe('Projects', () => {
await expect(workspacesPage.list.cardWithName(wsCreatedWithSidebar).pwLocator).toBeVisible();
});

await test.step('Create projects', async () => {});
await test.step('Archive a project', async () => {});
await test.step('Unarchive a project', async () => {});
await test.step('Create projects', async () => {
await workspacesPage.nav.sidebar.sidebarWorkspaceItem(wsCreatedWithButton).pwLocator.click();
const projects = workspacesPage.details.projects;
await projects.pwLocator.click();
await projects.newProject.pwLocator.click();
projectOneName = `test-1-${v4()}`;
await projects.createModal.projectName.pwLocator.fill(projectOneName);
await projects.createModal.description.pwLocator.fill(v4());
await projects.createModal.footer.submit.pwLocator.click();
await authedPage.waitForURL('**/projects/*/experiments');
await workspacesPage.nav.sidebar.sidebarWorkspaceItem(wsCreatedWithButton).pwLocator.click();
await expect(projects.cardWithName(projectOneName).pwLocator).toBeVisible();
});

await test.step('Navigation on projects page - sorting and list', async () => {});
await test.step('Create a model with all possible metadata', async () => {});
await test.step('Archive a model', async () => {});
Expand All @@ -111,14 +138,17 @@ test.describe('Projects', () => {
await test.step('Launch JupyterLab, kill the task, view logs', async () => {});
await test.step('Navigate with the breadcrumb and workspace page', async () => {});
await test.step('Navigation on workspace page', async () => {});
await test.step('Navigation to wokspace on the sidebar', async () => {});
await test.step('Edit a workspace through workspaces page', async () => {});
await test.step('Edit a workspace through the sidebar', async () => {});
await test.step('Archive a workspace', async () => {});
await test.step('Unarchive a workspace', async () => {});
await test.step('Unpin a workspace through the sidebar', async () => {});
await test.step('Pin a workspace through the sidebar', async () => {});
await test.step('Edit a workspace', async () => {});
await test.step('Delete a model', async () => {});
await test.step('Delete a project', async () => {});
await test.step('Delete a project', async () => {
await workspacesPage.nav.sidebar.sidebarWorkspaceItem(wsCreatedWithButton).pwLocator.click();
await workspacesPage.details.projects.pwLocator.click();
const projectContent = workspacesPage.details.projects;
const projectCard = projectContent.cardWithName(projectOneName);
await projectCard.actionMenu.open();
await projectCard.actionMenu.delete.pwLocator.click();
await projectContent.deleteModal.nameConfirmation.pwLocator.fill(projectOneName);
await projectContent.deleteModal.footer.submit.pwLocator.click();
});
});
});
3 changes: 2 additions & 1 deletion webui/react/src/e2e/utils/detCLI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export function detExecSync(detCommand: string): string {
return execSync(`${process.env.PW_DET_PATH || 'det'} ${detCommand}`, {
env: {
...process.env,
DET_MASTER: process.env.PW_DET_MASTER || 'http://localhost:8080',
DET_MASTER:
process.env.PW_DET_MASTER || process.env.DET_WEBPACK_PROXY_URL || 'http://localhost:8080',
DET_PASS: process.env.PW_PASSWORD,
DET_USER: process.env.PW_USER_NAME,
},
Expand Down
Loading

0 comments on commit 6fa1420

Please sign in to comment.