Skip to content

Commit

Permalink
test: log in with the api rather than through the UI for most react t…
Browse files Browse the repository at this point in the history
…ests (#9307)
  • Loading branch information
djanicekpach committed May 8, 2024
1 parent 349d2a5 commit cab9ac5
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 54 deletions.
1 change: 1 addition & 0 deletions webui/react/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ done.stamp
/test-results/
/playwright-report/
/playwright/.cache/
state.json
src/e2e/test-results/
src/e2e/playwright-report/
src/e2e/junit-results.xml
Expand Down
58 changes: 58 additions & 0 deletions webui/react/src/e2e/fixtures/api.auth.fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { APIRequest, APIRequestContext, Browser, BrowserContext, Page } from '@playwright/test';

export class ApiAuthFixture {
apiContext: APIRequestContext | undefined; // DNJ TODO - how to not have undefined
readonly request: APIRequest;
readonly browser: Browser;
_page: Page | undefined;
get page(): Page {
if (this._page === undefined) {
throw new Error('Accessing page object before initialization in authentication');
}
return this._page;
}
readonly #STATE_FILE = 'state.json';
readonly #USERNAME: string;
readonly #PASSWORD: string;
context: BrowserContext | undefined;
constructor(request: APIRequest, browser: Browser, existingPage: Page | undefined = undefined) {
if (process.env.PW_USER_NAME === undefined) {
throw new Error('username must be defined');
}
if (process.env.PW_PASSWORD === undefined) {
throw new Error('password must be defined');
}
this.#USERNAME = process.env.PW_USER_NAME;
this.#PASSWORD = process.env.PW_PASSWORD;
this.request = request;
this.browser = browser;
this._page = existingPage;
}

async login(): Promise<void> {
this.apiContext = await this.request.newContext();
await this.apiContext.post('/api/v1/auth/login', {
data: {
isHashed: false,
password: this.#PASSWORD,
username: this.#USERNAME,
},
});
// Save cookie state into the file.
const state = await this.apiContext.storageState({ path: this.#STATE_FILE });
if (this._page !== undefined) {
// add cookies to current page's existing context
this.context = this._page.context();
await this.context.addCookies(state.cookies);
} else {
// Create a new context for the browser with the saved token.
this.context = await this.browser.newContext({ storageState: this.#STATE_FILE });
this._page = await this.context.newPage();
}
}

async logout(): Promise<void> {
await this.apiContext?.dispose();
await this.context?.close();
}
}
2 changes: 1 addition & 1 deletion webui/react/src/e2e/fixtures/dev.fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export class DevFixture {
constructor(readonly page: Page) {
this.#page = page;
}

// tells the frontend where to find the backend if built for a different url. Incidentally reloads and logs out of Determined.
async setServerAddress(): Promise<void> {
await this.#page.goto('/');
await this.#page.evaluate(`dev.setServerAddress("${process.env.PW_SERVER_ADDRESS}")`);
Expand Down
19 changes: 18 additions & 1 deletion webui/react/src/e2e/fixtures/global-fixtures.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,39 @@
import { test as base } from '@playwright/test';
import { test as base, Page } from '@playwright/test';

import { ApiAuthFixture } from './api.auth.fixture';
import { AuthFixture } from './auth.fixture';
import { DevFixture } from './dev.fixture';
import { UserFixture } from './user.fixture';

type CustomFixtures = {
dev: DevFixture;
auth: AuthFixture;
apiAuth: ApiAuthFixture;
user: UserFixture;
authedPage: Page;
};

// https://playwright.dev/docs/test-fixtures
export const test = base.extend<CustomFixtures>({
// get the auth but allow yourself to log in through the api manually.
apiAuth: async ({ playwright, browser }, use) => {
const apiAuth = new ApiAuthFixture(playwright.request, browser);
await use(apiAuth);
},

auth: async ({ page }, use) => {
const auth = new AuthFixture(page);
await use(auth);
},

// get a page already logged in
authedPage: async ({ playwright, browser, dev }, use) => {
await dev.setServerAddress();
const apiAuth = new ApiAuthFixture(playwright.request, browser, dev.page);
await apiAuth.login();
await use(apiAuth.page);
},

dev: async ({ page }, use) => {
const dev = new DevFixture(page);
await use(dev);
Expand Down
17 changes: 6 additions & 11 deletions webui/react/src/e2e/tests/experimentList.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,17 @@ import { test } from 'e2e/fixtures/global-fixtures';
import { ProjectDetails } from 'e2e/models/pages/ProjectDetails';

test.describe('Experiement List', () => {
test.beforeEach(async ({ auth, dev }) => {
await dev.setServerAddress();
await auth.login();
});

test('Navigate to Experiment List', async ({ page }) => {
const projectDetailsPage = new ProjectDetails(page);
test('Navigate to Experiment List', async ({ authedPage }) => {
const projectDetailsPage = new ProjectDetails(authedPage);
await projectDetailsPage.gotoProject();
await expect(page).toHaveTitle(projectDetailsPage.title);
await expect(authedPage).toHaveTitle(projectDetailsPage.title);
await expect(projectDetailsPage.f_experiemntList.tableActionBar.pwLocator).toBeVisible();
});

test.skip('Click around the data grid', async ({ page }) => {
test.skip('Click around the data grid', async ({ authedPage }) => {
// This test expects a project to have been deployed.
// This test.skip is useful to show an example of what tests can do
const projectDetailsPage = new ProjectDetails(page);
const projectDetailsPage = new ProjectDetails(authedPage);
await projectDetailsPage.gotoProject();
await expect(projectDetailsPage.f_experiemntList.dataGrid.rows.pwLocator).toHaveCount(1);
await projectDetailsPage.f_experiemntList.dataGrid.setColumnHeight();
Expand All @@ -33,6 +28,6 @@ test.describe('Experiement List', () => {
await projectDetailsPage.f_experiemntList.dataGrid.headRow.clickSelectDropdown();
await projectDetailsPage.f_experiemntList.dataGrid.headRow.selectDropdown.select5.pwLocator.click();
await row.clickColumn('ID');
await page.waitForURL(/overview/);
await authedPage.waitForURL(/overview/);
});
});
53 changes: 23 additions & 30 deletions webui/react/src/e2e/tests/navigation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,76 +7,69 @@ import { SignIn } from 'e2e/models/pages/SignIn';
import { Workspaces } from 'e2e/models/pages/Workspaces';

test.describe('Navigation', () => {
test.beforeEach(async ({ dev }) => {
await dev.setServerAddress();
});

test('Sidebar Navigation', async ({ page, auth }) => {
test('Sidebar Navigation', async ({ authedPage, auth }) => {
// we need any page to access the sidebar, and i haven't modeled the homepage yet
const userManagementPage = new UserManagement(page);

await page.goto('/');
const userManagementPage = new UserManagement(authedPage);

await test.step('Login steps', async () => {
await auth.login();
await expect(page).toHaveTitle(BasePage.getTitle('Home'));
await expect(page).toHaveURL(/dashboard/);
await expect(authedPage).toHaveTitle(BasePage.getTitle('Home'));
await expect(authedPage).toHaveURL(/dashboard/);
});

await test.step('Navigate to Uncategorized', async () => {
await userManagementPage.nav.sidebar.uncategorized.pwLocator.click();
const expectedURL = /projects\/1\/experiments/;
await page.waitForURL(expectedURL);
await expect.soft(page).toHaveTitle(BasePage.getTitle('Uncategorized Experiments'));
await authedPage.waitForURL(expectedURL);
await expect.soft(authedPage).toHaveTitle(BasePage.getTitle('Uncategorized Experiments'));
});

await test.step('Navigate to Model Registry', async () => {
await userManagementPage.nav.sidebar.modelRegistry.pwLocator.click();
await page.waitForURL(/models/);
await expect.soft(page).toHaveTitle(BasePage.getTitle('Model Registry'));
await authedPage.waitForURL(/models/);
await expect.soft(authedPage).toHaveTitle(BasePage.getTitle('Model Registry'));
});

await test.step('Navigate to Tasks', async () => {
await userManagementPage.nav.sidebar.tasks.pwLocator.click();
const expectedURL = /tasks/;
await page.waitForURL(expectedURL);
await expect.soft(page).toHaveTitle(BasePage.getTitle('Tasks'));
await authedPage.waitForURL(expectedURL);
await expect.soft(authedPage).toHaveTitle(BasePage.getTitle('Tasks'));
});

await test.step('Navigate to Webhooks', async () => {
await userManagementPage.nav.sidebar.webhooks.pwLocator.click();
const expectedURL = /webhooks/;
await page.waitForURL(expectedURL);
await expect.soft(page).toHaveTitle(BasePage.getTitle('Webhooks'));
await authedPage.waitForURL(expectedURL);
await expect.soft(authedPage).toHaveTitle(BasePage.getTitle('Webhooks'));
});

await test.step('Navigate to Cluster', async () => {
await userManagementPage.nav.sidebar.cluster.pwLocator.click();
const expectedURL = /clusters/;
await page.waitForURL(expectedURL);
await expect.soft(page).toHaveTitle(BasePage.getTitle('Cluster'));
await authedPage.waitForURL(expectedURL);
await expect.soft(authedPage).toHaveTitle(BasePage.getTitle('Cluster'));
});

await test.step('Navigate to Workspaces', async () => {
const workspacesPage = new Workspaces(page);
const workspacesPage = new Workspaces(authedPage);
await workspacesPage.nav.sidebar.workspaces.pwLocator.click();
await page.waitForURL(workspacesPage.getUrlRegExp());
await expect.soft(page).toHaveTitle(workspacesPage.title);
await authedPage.waitForURL(workspacesPage.getUrlRegExp());
await expect.soft(authedPage).toHaveTitle(workspacesPage.title);
});

await test.step('Navigate to Admin', async () => {
const userManagementPage = new UserManagement(page);
const userManagementPage = new UserManagement(authedPage);
await userManagementPage.nav.sidebar.headerDropdown.pwLocator.click();
await userManagementPage.nav.sidebar.headerDropdown.admin.pwLocator.click();
await page.waitForURL(userManagementPage.getUrlRegExp());
await expect.soft(page).toHaveTitle(userManagementPage.title);
await authedPage.waitForURL(userManagementPage.getUrlRegExp());
await expect.soft(authedPage).toHaveTitle(userManagementPage.title);
});

await test.step('Navigate to Logout', async () => {
await auth.logout();
const signInPage = new SignIn(page);
await expect.soft(page).toHaveTitle(signInPage.title);
await expect.soft(page).toHaveURL(signInPage.getUrlRegExp());
const signInPage = new SignIn(authedPage);
await expect.soft(authedPage).toHaveTitle(signInPage.title);
await expect.soft(authedPage).toHaveURL(signInPage.getUrlRegExp());
});
});
});
20 changes: 9 additions & 11 deletions webui/react/src/e2e/tests/projectsWorkspaces.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,13 @@ test.describe('Projects', () => {
return fullName;
};

test.beforeEach(async ({ dev, auth, page }) => {
await dev.setServerAddress();
await auth.login();
await expect(page).toHaveTitle(BasePage.getTitle('Home'));
await expect(page).toHaveURL(/dashboard/);
test.beforeEach(async ({ authedPage }) => {
await expect(authedPage).toHaveTitle(BasePage.getTitle('Home'));
await expect(authedPage).toHaveURL(/dashboard/);
});

test.afterEach(async ({ page }) => {
const workspacesPage = new Workspaces(page);
test.afterEach(async ({ authedPage }) => {
const workspacesPage = new Workspaces(authedPage);
await test.step('Delete a workspace', async () => {
if (wsCreatedWithButton !== '') {
await workspacesPage.nav.sidebar.workspaces.pwLocator.click();
Expand All @@ -68,13 +66,13 @@ test.describe('Projects', () => {
});
});

test('Projects and Workspaces CRUD', async ({ page }) => {
const workspacesPage = new Workspaces(page);
test('Projects and Workspaces CRUD', async ({ authedPage }) => {
const workspacesPage = new Workspaces(authedPage);

await test.step('Navigate to Workspaces', async () => {
await workspacesPage.nav.sidebar.workspaces.pwLocator.click();
await page.waitForURL(`**/${workspacesPage.url}?**`); // glob pattern for query params
await expect.soft(page).toHaveTitle(workspacesPage.title);
await authedPage.waitForURL(`**/${workspacesPage.url}?**`); // glob pattern for query params
await expect.soft(authedPage).toHaveTitle(workspacesPage.title);
});

await test.step('Create a workspace', async () => {
Expand Down

0 comments on commit cab9ac5

Please sign in to comment.