Add e2e test for sorting (#2883)
* chore: 🤖 add sorting e2e test * docs: ✏️ update docs with details around @ce and @ent tags * test: 💍 Add @ent tag to sorting test * test: 💍 update roles page with goto method * test: 💍 assert column header labels and improve locators
This commit is contained in:
1 parent
96ff0ddf27
commit
37b6ef6d3f
4 files changed
+181
-3
No files matched your search
+4
-1
@@ -16,6 +16,7 @@
|
||||
- [Desktop](#desktop)
|
||||
- [Launch Enos Scenario](#launch-enos-scenario)
|
||||
- [Admin](#admin-1)
|
||||
- [Admin (Using a Local Branch)](#admin-using-a-local-branch)
|
||||
- [Desktop](#desktop-1)
|
||||
- [Destroy Enos Scenario](#destroy-enos-scenario)
|
||||
- [Developing Tests](#developing-tests)
|
||||
@@ -176,7 +177,7 @@ cd boundary-ui/e2e-tests
|
||||
Run the command for the corresponding enos scenario to run all supported tests:
|
||||
| | `@aws` | `@docker` |
|
||||
| ------------- | --------------------------------- | ------------------------------------ |
|
||||
| `@ce` | command: `pnpm run admin:ce:aws` | command: `admin:ce:docker` |
|
||||
| `@ce` | command: `pnpm run admin:ce:aws` | command: `pnpm admin:ce:docker` |
|
||||
| `@enterprise` | command: `pnpm run admin:ent:aws` | command: `pnpm run admin:ent:docker` |
|
||||
|
||||
|
||||
@@ -249,6 +250,8 @@ After all the steps pass, you should see a `Enos operations finished!`.
|
||||
### Test names and tagging
|
||||
For admin tests, it's important to tag the test with the correct tags to denote which enos scenario is supported by the test. The test tags should have at least specifying the edition (`@ce`, `@ent`) and at least one specifying the enos test infrastructure (`@aws`, or `@docker`).
|
||||
|
||||
The `@ent` and `@ce` tags should be added to *all* and *any* test cases that are supported by the respective editions. If a test case can be ran in both editions it should have both tags.
|
||||
|
||||
### Selecting / Locating Elements
|
||||
It is recommended to use locators that resemble how users interact with the application. `getByRole`
|
||||
should be the most prioritized locator as this is the closest way to how users and
|
||||
|
||||
@@ -9,6 +9,11 @@ import { nanoid } from 'nanoid';
|
||||
import { BaseResourcePage } from './base-resource.js';
|
||||
|
||||
export class RolesPage extends BaseResourcePage {
|
||||
goToRolesPage({ scope }) {
|
||||
scope = scope ?? 'global';
|
||||
this.page.goto(`/scopes/${scope}/roles`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new role. Assumes you have selected the desired scope.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { test } from '../../global-setup.js';
|
||||
import * as boundaryHttp from '../../helpers/boundary-http.js';
|
||||
import * as boundaryCli from '../../helpers/boundary-cli.js';
|
||||
import { RolesPage } from '../pages/roles.js';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
const roleNamesToCreate = ['Alpha', 'Beta', 'Delta', 'Gamma', 'Zeta'];
|
||||
|
||||
export const columnHeaderSortButton = (tableLocator, name) =>
|
||||
tableLocator.getByRole('columnheader', { name }).getByRole('button');
|
||||
|
||||
export const assertColumnHeaderSortedAscending = (tableLocator, name) =>
|
||||
expect(tableLocator.getByRole('columnheader', { name })).toHaveAttribute(
|
||||
'aria-sort',
|
||||
'ascending',
|
||||
);
|
||||
|
||||
export const assertColumnHeaderSortedDescending = (tableLocator, name) =>
|
||||
expect(tableLocator.getByRole('columnheader', { name })).toHaveAttribute(
|
||||
'aria-sort',
|
||||
'descending',
|
||||
);
|
||||
|
||||
const columnHeaders = Object.freeze({
|
||||
name: { index: 0, label: 'Name' },
|
||||
id: { index: 2, label: 'ID' },
|
||||
});
|
||||
|
||||
let org, rolesSortedByCreatedTime, rolesSortedByName, rolesSortedById;
|
||||
|
||||
test.beforeEach(
|
||||
async ({
|
||||
request,
|
||||
controllerAddr,
|
||||
adminAuthMethodId,
|
||||
adminLoginName,
|
||||
adminPassword,
|
||||
}) => {
|
||||
org = await boundaryHttp.createOrg(request);
|
||||
|
||||
await boundaryCli.authenticateBoundary(
|
||||
controllerAddr,
|
||||
adminAuthMethodId,
|
||||
adminLoginName,
|
||||
adminPassword,
|
||||
);
|
||||
|
||||
// when the org is created some roles are created automatically, but for the purposes
|
||||
// of this sorting test it's clearer if only the roles created by the test are present
|
||||
const existingRolesToDelete = await boundaryCli.listRoles(org.id);
|
||||
for (const role of existingRolesToDelete) {
|
||||
await boundaryCli.deleteRole(role.id);
|
||||
}
|
||||
|
||||
// create roles with specific names
|
||||
for (const roleName of roleNamesToCreate) {
|
||||
boundaryCli.createRole(org.id, { name: roleName });
|
||||
}
|
||||
|
||||
// collect all roles and data so that sorting can be tested based on the underlying data
|
||||
const roles = await boundaryCli.listRoles(org.id);
|
||||
rolesSortedByCreatedTime = roles.toSorted((a, b) => {
|
||||
return new Date(b.created_time) - new Date(a.created_time);
|
||||
});
|
||||
rolesSortedByName = roles.toSorted((a, b) => a.name.localeCompare(b.name));
|
||||
rolesSortedById = roles.toSorted((a, b) => a.id.localeCompare(b.id));
|
||||
},
|
||||
);
|
||||
|
||||
test.afterEach(async ({ request }) => {
|
||||
// deletes the org and any associated roles
|
||||
await boundaryHttp.deleteOrg(request, org.id);
|
||||
});
|
||||
|
||||
test(
|
||||
'Sorting works',
|
||||
{ tag: ['@ce', '@ent', '@aws', '@docker'] },
|
||||
async ({ page }) => {
|
||||
const rolesPage = new RolesPage(page);
|
||||
rolesPage.goToRolesPage({ scope: org.id });
|
||||
|
||||
const table = page.getByRole('table');
|
||||
const tableRows = table
|
||||
.getByRole('row')
|
||||
.filter({ hasNot: page.getByRole('columnheader') });
|
||||
|
||||
await expect(tableRows).toHaveCount(roleNamesToCreate.length);
|
||||
|
||||
// assert that the table has the expected column headers
|
||||
for (const { label, index } of Object.values(columnHeaders)) {
|
||||
await expect(
|
||||
table.getByRole('columnheader').nth(index).getByLabel(label),
|
||||
).toBeVisible();
|
||||
}
|
||||
|
||||
// by default, resources are sorted by created time, descending
|
||||
for (const [index, role] of Object.entries(
|
||||
rolesSortedByCreatedTime,
|
||||
).toReversed()) {
|
||||
await expect(tableRows.nth(index)).toContainText(role.name);
|
||||
}
|
||||
|
||||
// click the "Name" column header to sort by name, ascending
|
||||
await columnHeaderSortButton(table, columnHeaders.name.label).click();
|
||||
await assertColumnHeaderSortedAscending(table, columnHeaders.name.label);
|
||||
|
||||
for (const [index, role] of Object.entries(rolesSortedByName)) {
|
||||
await expect(tableRows.nth(index)).toContainText(role.name);
|
||||
}
|
||||
|
||||
// click the "Name" column header again, to sort by name, descending
|
||||
await columnHeaderSortButton(table, columnHeaders.name.label).click();
|
||||
await assertColumnHeaderSortedDescending(table, columnHeaders.name.label);
|
||||
for (const [index, role] of Object.entries(
|
||||
rolesSortedByName.toReversed(),
|
||||
)) {
|
||||
await expect(tableRows.nth(index)).toContainText(role.name);
|
||||
}
|
||||
|
||||
// click the "ID" column header to sort by ID, ascending
|
||||
await columnHeaderSortButton(table, columnHeaders.id.label).click();
|
||||
await assertColumnHeaderSortedAscending(table, columnHeaders.id.label);
|
||||
for (const [index, role] of Object.entries(rolesSortedById)) {
|
||||
await expect(tableRows.nth(index)).toContainText(role.name);
|
||||
}
|
||||
|
||||
// click the "ID" column header again, to sort by ID, descending
|
||||
await columnHeaderSortButton(table, columnHeaders.id.label).click();
|
||||
await assertColumnHeaderSortedDescending(table, columnHeaders.id.label);
|
||||
for (const [index, role] of Object.entries(rolesSortedById.toReversed())) {
|
||||
await expect(tableRows.nth(index)).toContainText(role.name);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -11,8 +11,8 @@ import { nanoid } from 'nanoid';
|
||||
* @param {string} scopeId ID of the scope under which the role will be created.
|
||||
* @returns {Promise<string>} new role's ID
|
||||
*/
|
||||
export async function createRole(scopeId) {
|
||||
const roleName = 'role-' + nanoid();
|
||||
export async function createRole(scopeId, { name }) {
|
||||
const roleName = name ?? 'role-' + nanoid();
|
||||
let role;
|
||||
try {
|
||||
role = JSON.parse(
|
||||
@@ -28,3 +28,34 @@ export async function createRole(scopeId) {
|
||||
}
|
||||
return role.id;
|
||||
}
|
||||
|
||||
export async function deleteRole(id) {
|
||||
try {
|
||||
execSync(
|
||||
`boundary roles delete \
|
||||
-id ${id} \
|
||||
-format json`,
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(`${e.stderr}`);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
export async function listRoles(scopeId) {
|
||||
let roles;
|
||||
try {
|
||||
roles = JSON.parse(
|
||||
execSync(
|
||||
`boundary roles list \
|
||||
-scope-id ${scopeId} \
|
||||
-format json`,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(`${e.stderr}`);
|
||||
}
|
||||
|
||||
return roles.items;
|
||||
}
|
||||
Reference in new issue
Block a user