Skip to content

Commit

Permalink
test: preparing a homework assignment [TESTENG-3] (#9510)
Browse files Browse the repository at this point in the history
  • Loading branch information
JComins000 authored Jun 13, 2024
1 parent ee66d15 commit 21ecda5
Show file tree
Hide file tree
Showing 12 changed files with 113 additions and 69 deletions.
3 changes: 1 addition & 2 deletions webui/react/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,5 @@ export default defineConfig({
reuseExistingServer: !process.env.CI,
},

// workers: process.env.CI ? 4 : 1,
workers: 1,
workers: process.env.CI ? 4 : 1,
});
15 changes: 6 additions & 9 deletions webui/react/src/e2e/fixtures/api.auth.fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,16 @@ export class ApiAuthFixture {
/**
* Logs in via the API. If there is a browser context already assosciated with the
* fixture, the bearer token will be attached to that context. If not a new
* browser ontext will be created with the cookie.
* browser context will be created with the cookie.
*/
async login(): Promise<void> {
this.apiContext = await this.request.newContext();
async login({
creds = { password: this.#PASSWORD, username: this.#USERNAME },
} = {}): Promise<void> {
this.apiContext = this.apiContext || (await this.request.newContext());
const resp = await this.apiContext.post('/api/v1/auth/login', {
data: {
...creds,
isHashed: false,
password: this.#PASSWORD,
username: this.#USERNAME,
},
});
if (resp.status() !== 200) {
Expand All @@ -74,10 +75,6 @@ export class ApiAuthFixture {
// 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.#stateFile });
this._page = await this.context.newPage();
}
}

Expand Down
12 changes: 8 additions & 4 deletions webui/react/src/e2e/fixtures/api.user.fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import streamConsumers from 'stream/consumers';

import _ from 'lodash';

import { safeName } from 'e2e/utils/naming';
import { randIdAlphanumeric, safeName } from 'e2e/utils/naming';
import { UsersApi, V1PatchUser, V1PostUserRequest, V1User } from 'services/api-ts-sdk/api';

import { ApiAuthFixture } from './api.auth.fixture';
Expand All @@ -13,16 +13,20 @@ export class ApiUserFixture {
this.apiAuth = apiAuth;
}

newRandom(usernamePrefix = 'test-user'): V1PostUserRequest {
return {
new({ userProps = {}, usernamePrefix = 'test-user' } = {}): V1PostUserRequest {
const defaults = {
isHashed: false,
password: 'TestPassword1',
password: randIdAlphanumeric({ length: 12 }),
user: {
active: true,
admin: true,
username: safeName(usernamePrefix),
},
};
return {
...defaults,
...userProps,
};
}

private static normalizeUrl(url: string): string {
Expand Down
80 changes: 65 additions & 15 deletions webui/react/src/e2e/fixtures/global-fixtures.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { test as base, Page } from '@playwright/test';

import { safeName } from 'e2e/utils/naming';
import { V1PostUserRequest } from 'services/api-ts-sdk/api';

// eslint-disable-next-line no-restricted-imports
import playwrightConfig from '../../../playwright.config';

import { ApiAuthFixture } from './api.auth.fixture';
import { ApiUserFixture } from './api.user.fixture';
import { AuthFixture } from './auth.fixture';
Expand All @@ -10,20 +16,29 @@ type CustomFixtures = {
dev: DevFixture;
auth: AuthFixture;
apiAuth: ApiAuthFixture;
backgroundApiAuth: ApiAuthFixture;
user: UserFixture;
apiUser: ApiUserFixture;
backgroundApiUser: ApiUserFixture;
authedPage: Page;
};

type CustomWorkerFixtures = {
newAdmin: V1PostUserRequest;
backgroundApiAuth: ApiAuthFixture;
backgroundApiUser: ApiUserFixture;
};

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

Expand All @@ -47,26 +62,61 @@ export const test = base.extend<CustomFixtures>({
* Generally use another api fixture instead if you want to call an api. If you just want a logged-in page,
* use apiAuth in beforeEach().
*/
backgroundApiAuth: async ({ playwright, browser, baseURL }, use) => {
const backgroundApiAuth = new ApiAuthFixture(playwright.request, browser, baseURL);
await use(backgroundApiAuth);
},
backgroundApiAuth: [
async ({ playwright, browser }, use) => {
const backgroundApiAuth = new ApiAuthFixture(
playwright.request,
browser,
playwrightConfig.use?.baseURL,
);
await backgroundApiAuth.login();
await use(backgroundApiAuth);
await backgroundApiAuth.dispose();
},
{ scope: 'worker' },
],
/**
* Allows calling the user api without a page so that it can run in beforeAll(). You will need to get a bearer
* token by calling backgroundApiUser.apiAuth.login(). This will also provision a page in the background which
* will be disposed of logout(). Before using the page,you need to call dev.setServerAddress() manually and
* then login() again, since setServerAddress logs out as a side effect.
*/
backgroundApiUser: async ({ backgroundApiAuth }, use) => {
const backgroundApiUser = new ApiUserFixture(backgroundApiAuth);
await use(backgroundApiUser);
},

backgroundApiUser: [
async ({ backgroundApiAuth }, use) => {
const backgroundApiUser = new ApiUserFixture(backgroundApiAuth);
await use(backgroundApiUser);
},
{ scope: 'worker' },
],
dev: async ({ page }, use) => {
const dev = new DevFixture(page);
await use(dev);
},

/**
* Creates an admin and logs in as that admin for the duraction of the test suite
*/
newAdmin: [
async ({ backgroundApiUser }, use, workerInfo) => {
const adminUser = await backgroundApiUser.createUser(
backgroundApiUser.new({
userProps: {
user: {
active: true,
admin: true,
username: safeName(`test-admin-${workerInfo.workerIndex}`),
},
},
}),
);
await backgroundApiUser.apiAuth.login({
creds: { password: adminUser.password!, username: adminUser.user!.username },
});
await use(adminUser);
await backgroundApiUser.apiAuth.login();
await backgroundApiUser.patchUser(adminUser.user!.id!, { active: false });
},
{ scope: 'worker' },
],
user: async ({ page }, use) => {
const user = new UserFixture(page);
await use(user);
Expand Down
2 changes: 1 addition & 1 deletion webui/react/src/e2e/models/BaseComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export type NamedComponentArgs =
export class BaseComponent implements ComponentBasics {
protected _selector: string;
readonly _parent: CanBeParent;
protected _locator: Locator | undefined;
protected _locator?: Locator;

/**
* Constructs a BaseComponent
Expand Down
2 changes: 1 addition & 1 deletion webui/react/src/e2e/models/ant/Dropdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ type DropdownArgs = DropdownArgsWithoutChildNode | DropdownArgsWithChildNode;
*/
export class Dropdown extends BaseComponent {
readonly openMethod: (args: { timeout?: number }) => Promise<void>;
readonly childNode: ComponentBasics | undefined;
readonly childNode?: ComponentBasics;

/**
* Constructs a new Dropdown component.
Expand Down
2 changes: 1 addition & 1 deletion webui/react/src/e2e/models/ant/Popover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type PopoverArgs = PopoverArgsWithoutChildNode | PopoverArgsWithChildNode;
*/
export class Popover extends BaseComponent {
readonly openMethod: () => Promise<void>;
readonly childNode: ComponentBasics | undefined;
readonly childNode?: ComponentBasics;
constructor({ root, childNode, openMethod }: PopoverArgs) {
super({
parent: root,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export class FilterGroup extends NamedComponent {
parent: this.#groupCard,
selector: this.#childrenSelector,
});
readonly filterGroups: FilterGroup | undefined;
readonly filterGroups?: FilterGroup;
readonly filterFields = new FilterField({
attachment: this.#notNestedSelector,
parent: this.#children,
Expand Down
16 changes: 8 additions & 8 deletions webui/react/src/e2e/models/hew/DataGrid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export class DataGrid<
parent: this,
selector: 'canvas:not([data-testid])',
});
#columnheight: number | undefined;
#columnheight?: number;
readonly #rowType: RowClass<RowType, HeadRowType>;
readonly rows: RowType;
readonly headRow: HeadRowType;
Expand Down Expand Up @@ -279,10 +279,10 @@ export class Row<HeadRowType extends HeadRow<Row<HeadRowType>>> extends NamedCom

/**
* Constructs a Row
* @param {object} obj
* @param {CanBeParent} obj.parent - The parent used to locate this Row
* @param {string} obj.selector - Used as a selector uesd to locate this object
* @param {DataGrid<RowType, HeadRowType>} [obj.parentTable] - Reference to the original table
* @param {object} args
* @param {CanBeParent} args.parent - The parent used to locate this Row
* @param {string} args.selector - Used as a selector uesd to locate this object
* @param {DataGrid<RowType, HeadRowType>} [args.parentTable] - Reference to the original table
*/
constructor(args: RowArgs<Row<HeadRowType>, HeadRowType>) {
super(args);
Expand Down Expand Up @@ -375,9 +375,9 @@ export class HeadRow<RowType extends Row<HeadRow<RowType>>> extends NamedCompone

/**
* Constructs a HeadRow
* @param {object} obj
* @param {CanBeParent} obj.parent - The parent used to locate this HeadRow
* @param {string} obj.selector - Used as a selector uesd to locate this object
* @param {object} args
* @param {CanBeParent} args.parent - The parent used to locate this HeadRow
* @param {string} args.selector - Used as a selector uesd to locate this object
*/
constructor(args: HeadRowArgs<RowType, HeadRow<RowType>>) {
super(args);
Expand Down
6 changes: 3 additions & 3 deletions webui/react/src/e2e/tests/experimentList.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import { test } from 'e2e/fixtures/global-fixtures';
import { ProjectDetails } from 'e2e/models/pages/ProjectDetails';
import { detExecSync, fullPath } from 'e2e/utils/detCLI';

test.describe.configure({ mode: 'serial' });

test.describe('Experiement List', () => {
let projectDetailsPage: ProjectDetails;
// trial click to wait for the element to be stable won't work here
Expand Down Expand Up @@ -59,7 +57,8 @@ test.describe('Experiement List', () => {
try {
await grid.headRow.selectDropdown.menuItem('select-none').select({ timeout: 1_000 });
} catch (e) {
// Ignore if no selection
// close the dropdown by clicking elsewhere
await projectDetailsPage.f_experiemntList.tableActionBar.expNum.pwLocator.click();
}
});
await test.step('Reset Columns', async () => {
Expand Down Expand Up @@ -163,6 +162,7 @@ test.describe('Experiement List', () => {
});

test('Table Filter', async () => {
test.slow();
const tableFilter = projectDetailsPage.f_experiemntList.tableActionBar.tableFilter;
const totalExperiments = await getExpNum();

Expand Down
30 changes: 8 additions & 22 deletions webui/react/src/e2e/tests/userManagement.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ import { repeatWithFallback } from 'e2e/utils/polling';
import { saveTestUser } from 'e2e/utils/users';
import { V1PostUserRequest } from 'services/api-ts-sdk/api';

// creating users while running tests in parallel can cause the users table to refresh at unexpected times
test.describe.configure({ mode: 'serial' });

test.describe('User Management', () => {
// One list of users per test session. This is to encourage a final teardown
// call of the user fixture to deactivate all users created by each test.
Expand All @@ -37,16 +34,14 @@ test.describe('User Management', () => {

test.describe('With User Teardown', () => {
test.afterAll(async ({ backgroundApiUser }) => {
await backgroundApiUser.apiAuth.login();
await test.step('Deactivate Users', async () => {
for (const [id] of testUsers) {
await backgroundApiUser.patchUser(id, { active: false });
}
});
await backgroundApiUser.apiAuth.dispose();
});

test.describe('With Test User', () => {
test.describe('With a User for Each Test', () => {
let testUser: V1PostUserRequest;

test.beforeEach(async ({ user }) => {
Expand Down Expand Up @@ -86,20 +81,8 @@ test.describe('User Management', () => {
await user.validateUser(testUser);
});
});
});

test.describe('With Test User', () => {
let testUser: V1PostUserRequest;

test.beforeAll(async ({ backgroundApiUser }) => {
await backgroundApiUser.apiAuth.login();
await test.step('Create User', async () => {
testUser = await backgroundApiUser.createUser(backgroundApiUser.newRandom());
saveTestUser(testUser, testUsers);
});
});

test('Deactivate and Reactivate', async ({ page, user, auth }) => {
test('Deactivate and Reactivate', async ({ page, user, auth, newAdmin }) => {
// test does does three and a half logins, so we need to increase the timeout
test.slow();
const userManagementPage = new UserManagement(page);
Expand Down Expand Up @@ -130,7 +113,11 @@ test.describe('User Management', () => {
// thinks we've already logged in, skipping the login automation.
// We might need to find a way to be more explicit about the page state.
await expect(page).toHaveURL(/login/);
await auth.login({ expectedURL: userManagementPage.url });
await auth.login({
expectedURL: userManagementPage.url,
password: newAdmin.password,
username: newAdmin.user?.username,
});
testUser = await user.changeStatusUser(testUser, true);
saveTestUser(testUser, testUsers);
});
Expand All @@ -145,12 +132,11 @@ test.describe('User Management', () => {
const usernamePrefix = 'test-user-pagination';
test.beforeAll(async ({ backgroundApiUser }) => {
test.slow();
await backgroundApiUser.apiAuth.login();
await test.step('Create User', async () => {
// pagination will be 10 per page, so create 11 users
for (let i = 0; i < 11; i++) {
const user = await backgroundApiUser.createUser(
backgroundApiUser.newRandom(`${usernamePrefix}`),
backgroundApiUser.new({ usernamePrefix }),
);
saveTestUser(user, testUsers);
}
Expand Down
12 changes: 10 additions & 2 deletions webui/react/src/e2e/utils/naming.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
/**
* Generates a four-character random hash
* @param {object} obj
* @param {number} [obj.length] The length of the hash
* @returns Alphanumeric hash
*/
export const randIdAlphanumeric = (): string => Math.random().toString(36).substring(2, 6);
export const randIdAlphanumeric = ({ length = 4 }: { length?: number } = {}): string =>
Math.random()
.toString(36)
.substring(2, 2 + length);

/**
* Generates a four-character numeric hash
* @param {object} obj
* @param {number} [obj.length] The length of the hash
* @returns Numeric hash
*/
export const randId = (): number => Math.floor(Math.random() * 10_000);
export const randId = ({ length = 4 }: { length?: number } = {}): number =>
Math.floor(Math.random() * Math.pow(10, length));

/**
* Generates a naming function and a random hash to help with naming collisions.
Expand Down

0 comments on commit 21ecda5

Please sign in to comment.