From 37b6ef6d3f27d10de37f52ca7a1affb7c3e29118 Mon Sep 17 00:00:00 2001 From: hashicc <191911133+hashicc@users.noreply.github.com> Date: Mon, 23 Jun 2025 14:27:36 -0400 Subject: [PATCH] Add e2e test for sorting (#2883) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- e2e-tests/README.md | 5 +- e2e-tests/admin/pages/roles.js | 5 + e2e-tests/admin/tests/sorting.spec.js | 139 ++++++++++++++++++++++++ e2e-tests/helpers/boundary-cli/roles.js | 35 +++++- 4 files changed, 181 insertions(+), 3 deletions(-) create mode 100644 e2e-tests/admin/tests/sorting.spec.js diff --git a/e2e-tests/README.md b/e2e-tests/README.md index 69863a215..9ab8da56d 100644 --- a/e2e-tests/README.md +++ b/e2e-tests/README.md @@ -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 diff --git a/e2e-tests/admin/pages/roles.js b/e2e-tests/admin/pages/roles.js index ef4a3e7f4..407f536e9 100644 --- a/e2e-tests/admin/pages/roles.js +++ b/e2e-tests/admin/pages/roles.js @@ -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. */ diff --git a/e2e-tests/admin/tests/sorting.spec.js b/e2e-tests/admin/tests/sorting.spec.js new file mode 100644 index 000000000..ea8bb56bf --- /dev/null +++ b/e2e-tests/admin/tests/sorting.spec.js @@ -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); + } + }, +); diff --git a/e2e-tests/helpers/boundary-cli/roles.js b/e2e-tests/helpers/boundary-cli/roles.js index 49a1028da..e96d11b4b 100644 --- a/e2e-tests/helpers/boundary-cli/roles.js +++ b/e2e-tests/helpers/boundary-cli/roles.js @@ -11,8 +11,8 @@ import { nanoid } from 'nanoid'; * @param {string} scopeId ID of the scope under which the role will be created. * @returns {Promise} 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; +}