Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,72 @@ jobs:
- name: Get logs from the development environment on failure
if: failure()
run: docker logs tools_awx_1 || true

e2e:
name: ui-e2e
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
packages: read
contents: read
steps:
- name: Checkout ascender
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

# build-ui, unlike the dev-env job, because these tests drive the built UI
# the way a user meets it rather than a development server
- name: Start the development environment with the UI built
uses: ./.github/actions/run_awx_devel
with:
build-ui: true
github-token: ${{ secrets.GITHUB_TOKEN }}

# The UI is built after the web process is already serving, and Django
# caches the index template that names the bundle. Without this the tests
# run against whatever was in the image, not against this pull request.
- name: Serve the UI that was just built
run: |
docker exec tools_awx_1 \
supervisorctl -c /etc/supervisord.conf restart tower-processes:awx-uwsgi
SECONDS=0
while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' -k https://localhost:8043/api/v2/ping/)" != "200" ]]; do
if [[ $SECONDS -gt 180 ]]; then echo "the API did not come back after the restart"; exit 1; fi
sleep 5
done

- name: Set up Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'

- name: Install the end-to-end dependencies
working-directory: awx/ui/e2e
run: npm ci

- name: Install the browser
working-directory: awx/ui/e2e
run: npx playwright install --with-deps chromium

# run_awx_devel sets the admin password to this
- name: Run the end-to-end tests
working-directory: awx/ui/e2e
env:
ASCENDER_URL: https://localhost:8043
ASCENDER_USERNAME: admin
ASCENDER_PASSWORD: password
run: npm test

- name: Upload the report on failure
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: playwright-report
path: |
awx/ui/e2e/playwright-report
awx/ui/e2e/test-results
retention-days: 7
if-no-files-found: ignore

- name: Get logs from the development environment on failure
if: failure()
run: docker logs tools_awx_1 || true
8 changes: 8 additions & 0 deletions awx/ui/e2e/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
node_modules/
# written by the global setup on every run
fixtures.json
# playwright output
test-results/
playwright-report/
blob-report/
playwright/.cache/
59 changes: 59 additions & 0 deletions awx/ui/e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# End-to-end tests

Browser tests for the Ascender UI, driven by [Playwright](https://playwright.dev).

These cover what the jest suite in `awx/ui/src` cannot. That suite runs in jsdom,
which does not faithfully reproduce event bubbling through the DOM, focus, or
anything rendered through a portal. #742 is the worked example: a jest test for
"picking a job navigates" passed against code where the click did nothing in
every real browser.

## Running them

They need a development environment already up, the one `make docker-compose`
starts, with the UI built into it:

```bash
make docker-compose # in one terminal, from the repository root
docker exec tools_awx_1 make ui-devel
```

Then, from this directory:

```bash
npm ci
npx playwright install --with-deps chromium
npm test
```

`npm test` seeds its own fixtures first, so it needs no particular state beyond
a running instance.

## Configuration

| Variable | Default | Meaning |
| -------- | ------- | ------- |
| `ASCENDER_URL` | `https://localhost:8043` | Where the instance is |
| `ASCENDER_USERNAME` | `admin` | Account the specs log in as |
| `ASCENDER_PASSWORD` | `password` | Its password, which is what the CI environment sets |
| `ASCENDER_TOKEN` | unset | Used by the seed instead of a password, if you prefer |

## The fixtures

`seed.js` runs once before the specs and writes `fixtures.json`. It creates a
workflow job template named `e2e-workflow` out of system job templates, runs it,
and records the job each node produced.

System job templates are used deliberately: every instance has them, they need no
project, inventory, credential or network access, and they finish in seconds. It
also means the specs navigate under `/jobs/management/`, exercising a url segment
other than the default one.

The workflow job template is reused on later runs. It is safe to delete.

## Adding a spec

Keep them few and about flows that would be expensive to break. Assert on the url
and on what is rendered, not on component internals, and use `watchConsole` where
the page should be quiet: an unexpected console error is usually a real defect,
and two of the three found in #742 announced themselves that way.
78 changes: 78 additions & 0 deletions awx/ui/e2e/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions awx/ui/e2e/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "ascender-ui-e2e",
"version": "0.0.0",
"private": true,
"description": "End-to-end browser tests for the Ascender UI",
"scripts": {
"seed": "node seed.js",
"test": "playwright test",
"test:headed": "playwright test --headed"
},
"devDependencies": {
"@playwright/test": "1.62.1"
}
}
31 changes: 31 additions & 0 deletions awx/ui/e2e/playwright.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Modifications Copyright (c) 2026 Ctrl IQ, Inc.
const { defineConfig, devices } = require('@playwright/test');

// The development environment serves the built UI over a self-signed
// certificate, which is why ignoreHTTPSErrors is on rather than optional.
const baseURL = process.env.ASCENDER_URL || 'https://localhost:8043';

module.exports = defineConfig({
testDir: './tests',
globalSetup: require.resolve('./seed.js'),
// The suite talks to one shared instance and seeds data into it, so the
// specs run one at a time rather than racing each other.
workers: 1,
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 1 : 0,
timeout: 90_000,
expect: { timeout: 20_000 },
reporter: process.env.CI
? [['list'], ['html', { open: 'never', outputFolder: 'playwright-report' }]]
: [['list']],
use: {
baseURL,
ignoreHTTPSErrors: true,
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
viewport: { width: 1400, height: 900 },
},
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
});
Loading
Loading