diff --git a/.github/workflows/test-e2e-desktop.yaml b/.github/workflows/test-e2e-desktop.yaml index b0b378871..4368e1909 100644 --- a/.github/workflows/test-e2e-desktop.yaml +++ b/.github/workflows/test-e2e-desktop.yaml @@ -11,6 +11,17 @@ on: - 'e2e-tests/desktop/**' - '.github/workflows/test-e2e-desktop.yaml' workflow_dispatch: + inputs: + boundary-enterprise-branch: + description: 'Branch of boundary-enterprise to test against (default to main)' + required: true + default: 'main' + type: string + boundary-ui-enterprise-branch: + description: 'Branch of boundary-ui-enterprise to test against (default to main)' + required: true + default: 'main' + type: string jobs: test: @@ -19,7 +30,6 @@ jobs: env: ENOS_VAR_boundary_edition: "ent" ENOS_VAR_e2e_debug_no_run: true - SETUP_CLI: true steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -51,15 +61,30 @@ jobs: run: | wget https://releases.hashicorp.com/vault/1.12.2/vault_1.12.2_linux_amd64.zip -O /tmp/vault.zip unzip /tmp/vault.zip -d /usr/local/bin - + + # TODO: We should consider modifying to also build CE binary. For now + # building ENT binary using most recent UI commit is sufficient. - name: Checkout boundary-enterprise uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: repository: hashicorp/boundary-enterprise - # ref: ${{ github.event.inputs.boundary-enterprise-branch }} + ref: ${{ github.event.inputs.boundary-enterprise-branch || 'main' }} path: support/src/boundary-enterprise token: ${{ secrets.ELEVATED_GITHUB_TOKEN }} + - name: Checkout boundary-ui-enterprise + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: hashicorp/boundary-ui-enterprise + ref: ${{ github.event.inputs.boundary-ui-enterprise-branch || 'main' }} + path: support/src/boundary-ui-enterprise + token: ${{ secrets.ELEVATED_GITHUB_TOKEN }} + + - name: Use boundary-ui-enterprise commit SHA for building binary + run: | + cd support/src/boundary-ui-enterprise + git rev-parse HEAD > ../boundary-enterprise/internal/ui/VERSION_ent + - name: GH fix for localhost resolution run: | cat /etc/hosts && echo "-----------" @@ -71,23 +96,27 @@ jobs: echo "127.0.0.1 localhost worker" | sudo tee -a /etc/hosts echo "127.0.0.1 localhost vault" | sudo tee -a /etc/hosts - # TODO: Build image ourselves in case we don't want to use latest? - - name: Get Boundary docker image + - name: Set git config run: | - docker pull hashicorp/boundary-enterprise:latest - docker save hashicorp/boundary-enterprise:latest | gzip > /tmp/boundary-enterprise.zip + git config --global url."https://oauth2:${{ secrets.ELEVATED_GITHUB_TOKEN }}@github.com".insteadOf "https://github.com" - name: Set up test infra id: infra run: | ssh-keygen -N '' -t ed25519 -f ~/.ssh/github_enos mkdir -p ./enos/terraform-plugin-cache + export ENOS_VAR_ui_build_override="ent" && \ + export ENOS_VAR_boundary_edition="ent" && \ export ENOS_VAR_enos_user=$GITHUB_ACTOR && \ export ENOS_VAR_aws_ssh_private_key_path=~/.ssh/github_enos && \ export ENOS_VAR_boundary_license="${{ secrets.BOUNDARY_ENT_LICENSE }}" && \ - export ENOS_VAR_boundary_docker_image_file=/tmp/boundary-enterprise.zip && \ - enos scenario launch --timeout 60m0s --chdir ./support/src/boundary-enterprise/enos e2e_ui_docker_ent builder:crt + enos scenario launch --timeout 60m0s --chdir ./support/src/boundary-enterprise/enos e2e_ui_docker_ent builder:local + - name: Move boundary binary + run: | + mv ./support/src/boundary-enterprise/bin/boundary /usr/local/bin/ + boundary version + - name: Install dependencies run: | pnpm install --frozen-lockfile @@ -116,7 +145,7 @@ jobs: run: | export ENOS_VAR_enos_user=$GITHUB_ACTOR && \ export ENOS_VAR_aws_ssh_private_key_path=~/.ssh/github_enos && \ - enos scenario destroy --timeout 60m0s --chdir ./support/src/boundary-enterprise/enos e2e_ui_docker_ent builder:crt + enos scenario destroy --timeout 60m0s --chdir ./support/src/boundary-enterprise/enos e2e_ui_docker_ent builder:local - name: Send Slack message on failure uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1 diff --git a/e2e-tests/desktop/tests/scope.spec.js b/e2e-tests/desktop/tests/scope.spec.js index cc6c2e3f6..f5611cea5 100644 --- a/e2e-tests/desktop/tests/scope.spec.js +++ b/e2e-tests/desktop/tests/scope.spec.js @@ -81,6 +81,7 @@ test.describe('Scope tests', () => { authedPage.getByRole('link', { name: targetB.name }), ).toBeHidden(); + await headerNavLocator.click(); const orgBHeaderNavLink = authedPage.getByRole('option', { name: orgB.name, }); diff --git a/e2e-tests/desktop/tests/sorting.spec.js b/e2e-tests/desktop/tests/sorting.spec.js new file mode 100644 index 000000000..f935cd51a --- /dev/null +++ b/e2e-tests/desktop/tests/sorting.spec.js @@ -0,0 +1,98 @@ +/** + * Copyright IBM Corp. 2021, 2026 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { expect, test } from '../fixtures/baseTest.js'; +import * as boundaryHttp from '../../helpers/boundary-http.js'; + +const targetNamesToCreate = [ + 'Alpha', + 'alpha', + 'Beta', + 'Delta', + 'delta', + 'Gamma', + 'Zeta', +]; +let org, targetsSortedByName; + +test.beforeEach(async ({ request, targetAddress, targetPort }) => { + org = await boundaryHttp.createOrg(request); + const project = await boundaryHttp.createProject(request, org.id); + + // Create targets with specific names + const targets = []; + for (const targetName of targetNamesToCreate) { + const target = await boundaryHttp.createTarget(request, { + scopeId: project.id, + type: 'tcp', + port: targetPort, + address: targetAddress, + name: targetName, + }); + targets.push(target); + } + + targetsSortedByName = targets.toSorted((a, b) => + a.name.localeCompare(b.name, 'en', { + caseFirst: 'upper', + }), + ); +}); + +test.afterEach(async ({ request }) => { + if (org) { + await boundaryHttp.deleteOrg(request, org.id); + } +}); + +test('Sorts targets table by name correctly', async ({ authedPage }) => { + await expect( + authedPage.getByRole('heading', { name: 'Targets' }), + ).toBeVisible(); + + // Assert table is visible and loaded before switching scopes since there is a bug + // where page errors out if we switch scopes before the table loads. + const table = authedPage.getByRole('table'); + await expect(table).toBeVisible(); + + // Switch to org scope to see targets created in beforeEach + const headerNavLocator = authedPage.locator( + 'header .app-header__global-actions', + ); + await expect(headerNavLocator).toBeVisible(); + await headerNavLocator.click(); + await authedPage.getByRole('option', { name: org.name }).click(); + await expect( + headerNavLocator.locator('.hds-dropdown-toggle-button'), + ).toHaveText(org.name); + + const tableRows = table + .getByRole('row') + .filter({ hasNot: authedPage.getByRole('columnheader') }); + await expect(tableRows).toHaveCount(targetNamesToCreate.length); + + // Assert "Name" column is present + await expect( + table.getByRole('columnheader').nth(0).getByLabel('Name'), + ).toBeVisible(); + + const nameColumnHeader = table.getByRole('columnheader', { name: 'Name' }); + + // Sort by name ascending + await nameColumnHeader.getByRole('button').click(); + await expect(nameColumnHeader).toHaveAttribute('aria-sort', 'ascending'); + for (const [index, target] of Object.entries(targetsSortedByName)) { + await expect(tableRows.nth(index)).toContainText(target.name); + } + + // Sort by name descending + await nameColumnHeader.getByRole('button').click(); + await expect(nameColumnHeader).toHaveAttribute('aria-sort', 'descending'); + for (const [index, target] of Object.entries( + targetsSortedByName.toReversed(), + )) { + await expect(tableRows.nth(index)).toContainText(target.name); + } +}); diff --git a/e2e-tests/helpers/boundary-http/targets.js b/e2e-tests/helpers/boundary-http/targets.js index a649d72b9..822c0e457 100644 --- a/e2e-tests/helpers/boundary-http/targets.js +++ b/e2e-tests/helpers/boundary-http/targets.js @@ -15,10 +15,13 @@ import { checkResponse } from './responseHelper.js'; * @param {string} address Optional target address * @returns {Promise} */ -export async function createTarget(request, { scopeId, type, port, address }) { +export async function createTarget( + request, + { scopeId, name, type, port, address }, +) { const response = await request.post(`/v1/targets`, { data: { - name: `Target-${nanoid()}`, + name: name || `Target-${nanoid()}`, scope_id: scopeId, type: type, attributes: {