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:
hashicc authored and GitHub committed 2025-06-23 14:27:36 -04:00
1 parent 96ff0ddf27
commit 37b6ef6d3f
4 files changed
+181 -3

No files matched your search

+4 -1
View File
@@ -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
+5
View File
@@ -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.
*/
+139
View File
@@ -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);
}
},
);
+33 -2
View File
@@ -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;
}