diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 873f1947ee3..98b53a1cb7c 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -28,12 +28,13 @@ jobs: name: Setup outputs run: ./.github/scripts/get_runner_classes.sh - workspace-tests: + + e2e-tests: needs: setup - runs-on: ${{ fromJSON(needs.setup.outputs.compute-small) }} - defaults: - run: - working-directory: ui + runs-on: ${{ fromJSON(needs.setup.outputs.compute-medium) }} + continue-on-error: true + env: + CONSUL_NSPACES_ENABLED: 0 steps: - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 @@ -50,132 +51,74 @@ jobs: cache: "pnpm" cache-dependency-path: "ui/pnpm-lock.yaml" - # Install dependencies. - - name: install packages + - name: Install dependencies working-directory: ui run: make deps - - run: make test-workspace - - node-tests: - needs: setup - runs-on: ${{ fromJSON(needs.setup.outputs.compute-small) }} - steps: - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - - - name: Install PNPM - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + - name: Checkout consul-ui-testing repo + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 with: - run_install: false - package_json_file: ui/package.json - - - name: Setup node and pnpm cache - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 - with: - node-version-file: "./ui/package.json" - cache: "pnpm" - cache-dependency-path: "ui/pnpm-lock.yaml" - - # Install dependencies. - - name: install packages - working-directory: ui - run: make deps - - - run: make test-node - working-directory: ui/packages/consul-ui - - ember-build-test: - needs: setup - if: ${{ !endsWith(github.repository, '-enterprise') }} - runs-on: ${{ fromJSON(needs.setup.outputs.compute-large ) }} - strategy: - matrix: - partition: [1, 2, 3, 4] - env: - EMBER_TEST_REPORT: test-results/report-ce.xml # outputs test report for CI test summary - EMBER_TEST_PARALLEL: true # enables test parallelization with ember-exam - CONSUL_NSPACES_ENABLED: 0 # NOTE: this should be 1 in ENT. - JOBS: 2 # limit parallelism for broccoli-babel-transpiler - steps: - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + repository: hashicorp/consul-ui-testing + path: consul-ui-testing + token: ${{ secrets.ELEVATED_GITHUB_TOKEN }} - - name: Install PNPM - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 - with: - run_install: false - package_json_file: ui/package.json + - name: Install consul-ui-testing dependencies + working-directory: consul-ui-testing + run: yarn install - - name: Setup node and pnpm cache - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 - with: - node-version-file: "./ui/package.json" - cache: "pnpm" - cache-dependency-path: "ui/pnpm-lock.yaml" + - name: Install httpie (required by consul-ui-testing) + run: | + sudo apt-get update + sudo apt-get install -y httpie - - name: Install Chrome - uses: browser-actions/setup-chrome@82b9ce628cc5595478a9ebadc480958a36457dc2 # v1.6.0 + - name: Skip HashiCups build (use cached version marker) + working-directory: consul-ui-testing + run: echo "hashicorppreview/consul:latest" > .last_consul_version_built - - name: Install dependencies - working-directory: ui - run: make deps + - name: Start Consul API servers + working-directory: consul-ui-testing + run: | + echo "Starting Consul API servers at $(date '+%Y-%m-%d %H:%M:%S')" + yarn start hashicorppreview/consul:latest - - name: Build CI + - name: Start Consul UI working-directory: ui/packages/consul-ui - run: make build-ci + run: | + pnpm run start:consul & + echo $! > consul-ui.pid + env: + CONSUL_HTTP_ADDR: http://localhost:8500 - - name: Ember exam - working-directory: ui/packages/consul-ui - run: node_modules/.bin/ember exam --split=4 --partition=${{ matrix.partition }} --path dist --silent -r xunit + - name: Wait for UI to be ready + run: npx wait-on http://localhost:4200 --timeout 60000 - - name: Test Coverage CI + - name: Run E2E health check working-directory: ui/packages/consul-ui - run: make test-coverage-ci + run: pnpm run test:e2e:health - ember-build-test-ent: - needs: setup - runs-on: ${{ fromJSON(needs.setup.outputs.compute-large ) }} - strategy: - matrix: - partition: [1, 2, 3, 4] - env: - EMBER_TEST_REPORT: test-results/report-ce.xml # outputs test report for CI test summary - EMBER_TEST_PARALLEL: true # enables test parallelization with ember-exam - CONSUL_NSPACES_ENABLED: 1 # NOTE: this should be 1 in ENT. - JOBS: 2 # limit parallelism for broccoli-babel-transpiler - steps: - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - - - name: Install PNPM - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + - name: Run basic E2E tests + working-directory: ui/packages/consul-ui + run: pnpm run test:e2e:basic + env: + CI: true + CONSUL_UI_TEST_TOKEN: ${{ secrets.CONSUL_UI_TEST_TOKEN }} + + - name: Upload test results + if: always() + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 with: - run_install: false - package_json_file: ui/package.json + name: e2e-test-results-ce + path: ui/packages/consul-ui/e2e-tests/reports/ + retention-days: 30 - - name: Setup node and pnpm cache - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + - name: Upload screenshots on failure + if: failure() + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 with: - node-version-file: "./ui/package.json" - cache: "pnpm" - cache-dependency-path: "ui/pnpm-lock.yaml" - - - name: Install Chrome - uses: browser-actions/setup-chrome@82b9ce628cc5595478a9ebadc480958a36457dc2 # v1.6.0 + name: e2e-screenshots-ce + path: ui/packages/consul-ui/e2e-tests/reports/test-results/ + retention-days: 7 - - name: Install dependencies - working-directory: ui - run: make deps - - - name: Build CI - working-directory: ui/packages/consul-ui - run: make build-ci - - - name: Ember exam - working-directory: ui/packages/consul-ui - run: node_modules/.bin/ember exam --split=4 --partition=${{ matrix.partition }} --path dist --silent -r xunit - - - name: Test Coverage CI - working-directory: ui/packages/consul-ui - run: make test-coverage-ci # This is job is required for branch protection as a required gihub check # because GitHub actions show up as checks at the job level and not the # workflow level. This is currently a feature request: @@ -193,9 +136,7 @@ jobs: frontend-success: needs: - setup - - workspace-tests - - node-tests - - ember-build-test + - e2e-tests runs-on: ${{ fromJSON(needs.setup.outputs.compute-small) }} if: ${{ always() }} steps: diff --git a/ui/packages/consul-ui/.gitignore b/ui/packages/consul-ui/.gitignore index 88fc1de75df..053789b0ea1 100644 --- a/ui/packages/consul-ui/.gitignore +++ b/ui/packages/consul-ui/.gitignore @@ -25,3 +25,11 @@ /.node_modules.ember-try/ /bower.json.ember-try /package.json.ember-try + + +# Playwright E2E test artifacts +e2e-tests/reports/ +e2e-tests/.auth/ +.playwright/ +e2e-tests/auth-state.json +playwright/.cache/ diff --git a/ui/packages/consul-ui/e2e-tests/global-setup.js b/ui/packages/consul-ui/e2e-tests/global-setup.js new file mode 100644 index 00000000000..057751979da --- /dev/null +++ b/ui/packages/consul-ui/e2e-tests/global-setup.js @@ -0,0 +1,57 @@ +const { chromium } = require('@playwright/test'); +const { checkAllServices, printServiceErrors } = require('./utils/health-check-utils'); +const { loginWithToken } = require('./utils/auth-utils'); + +async function globalSetup(config) { + console.log('\n๐Ÿš€ Starting E2E Test Setup...\n'); + + const baseURL = config.projects?.[0]?.use?.baseURL || 'http://localhost:4200'; + + console.log('๐Ÿ” Checking service health...\n'); + + const healthChecks = await checkAllServices(baseURL); + + let allHealthy = true; + const failedServices = []; + + healthChecks.forEach((s) => { + console.log(`${s.isHealthy ? 'โœ…' : 'โŒ'} ${s.name}: ${s.url}`); + if (!s.isHealthy) { + allHealthy = false; + failedServices.push(s); + } + }); + + if (!allHealthy) { + console.log('\nโš ๏ธ Some services are not accessible. Tests may fail.\n'); + printServiceErrors(failedServices); + } + + // Perform authentication and save state + console.log('\n๐Ÿ” Authenticating to Consul UI...\n'); + + const browser = await chromium.launch(); + const context = await browser.newContext(); + const page = await context.newPage(); + + try { + // Login using the token from environment + await loginWithToken(page); + + console.log('โœ… Authentication successful.\n'); + + // Save the authenticated state for all tests to reuse + await context.storageState({ path: 'ui/packages/consul-ui/e2e-tests/auth-state.json' }); + + console.log('๐Ÿ’พ Saved authentication state.\n'); + } catch (error) { + console.error('โŒ Authentication failed:', error.message); + throw error; + } finally { + await browser.close(); + } + + console.log('โœ… Setup complete!\n'); +} + +module.exports = globalSetup; diff --git a/ui/packages/consul-ui/e2e-tests/global-teardown.js b/ui/packages/consul-ui/e2e-tests/global-teardown.js new file mode 100644 index 00000000000..61dd1d94cd9 --- /dev/null +++ b/ui/packages/consul-ui/e2e-tests/global-teardown.js @@ -0,0 +1,22 @@ +/** + * Global Teardown for Playwright E2E Tests + * + * Runs once after all tests + * - Clean up resources + * - Archive logs if needed + */ + +async function globalTeardown(config) { + console.log('\n๐Ÿงน Starting E2E Test Cleanup...\n'); + + // TODO: Add cleanup tasks + // - Clean up authentication state files + // - Archive logs if tests failed + // - Clean up any test data + + console.log('โœ… Cleanup complete!\n'); +} + +module.exports = globalTeardown; + +// Made with Bob diff --git a/ui/packages/consul-ui/e2e-tests/health-check.js b/ui/packages/consul-ui/e2e-tests/health-check.js new file mode 100644 index 00000000000..1a033bf38a7 --- /dev/null +++ b/ui/packages/consul-ui/e2e-tests/health-check.js @@ -0,0 +1,35 @@ +#!/usr/bin/env node +const { checkAllServices, printServiceErrors } = require('./utils/health-check-utils'); + +async function runHealthCheck() { + console.log('\n๐Ÿฅ E2E Environment Health Check\n'); + + const healthChecks = await checkAllServices(); + + let allRequiredHealthy = true; + const failedServices = []; + + healthChecks.forEach((s) => { + const status = s.isHealthy ? 'โœ…' : 'โŒ'; + const label = s.required ? 'REQUIRED' : 'OPTIONAL'; + console.log(`${status} [${label}] ${s.name}: ${s.url}`); + if (s.required && !s.isHealthy) { + allRequiredHealthy = false; + failedServices.push(s); + } + }); + + if (allRequiredHealthy) { + console.log('\nโœ… Ready! Run: pnpm run test:e2e:basic\n'); + process.exit(0); + } else { + console.log('\nโŒ Missing required services!\n'); + printServiceErrors(failedServices); + process.exit(1); + } +} + +runHealthCheck().catch((err) => { + console.error('โŒ Error:', err.message); + process.exit(1); +}); diff --git a/ui/packages/consul-ui/e2e-tests/playwright.config.js b/ui/packages/consul-ui/e2e-tests/playwright.config.js new file mode 100644 index 00000000000..4a0bf76e0c0 --- /dev/null +++ b/ui/packages/consul-ui/e2e-tests/playwright.config.js @@ -0,0 +1,109 @@ +const { defineConfig, devices } = require('@playwright/test'); + +/** + * Playwright Configuration for Consul UI E2E Tests + * + * Two-tier test structure: + * - basic: Fast, essential tests (run on every PR) + * - workflows: Complex scenarios (run nightly) + */ + +module.exports = defineConfig({ + // Test directory + testDir: './tests', + + // Output directory for test results + outputDir: './reports/test-results', + + // Run tests in files in parallel + fullyParallel: true, + + // Fail the build on CI if you accidentally left test.only in the source code + forbidOnly: !!process.env.CI, + + // Retry on CI only + retries: process.env.CI ? 2 : 0, + + // Opt out of parallel tests on CI + workers: process.env.CI ? 1 : undefined, + + // Reporter to use + reporter: process.env.CI + ? [ + ['html', { outputFolder: './reports/html-report', open: 'never' }], + ['junit', { outputFile: './reports/junit/results.xml' }], + ['json', { outputFile: './reports/json/results.json' }], + ['list'], + ['github'], + ] + : [ + ['html', { outputFolder: './reports/html-report', open: 'never' }], + ['junit', { outputFile: './reports/junit/results.xml' }], + ['json', { outputFile: './reports/json/results.json' }], + ['list'], + ], + + // Shared settings for all projects + use: { + // Base URL for navigation + baseURL: 'http://localhost:4200', + + // Use saved authentication state + storageState: 'ui/packages/consul-ui/e2e-tests/auth-state.json', + + // Collect trace on first retry + trace: 'on-first-retry', + + // Screenshot on failure + screenshot: 'only-on-failure', + + // Video on failure + video: 'retain-on-failure', + + // Action timeout + actionTimeout: 10000, + + // Navigation timeout + navigationTimeout: 30000, + }, + + // Configure projects for major browsers + projects: [ + { + name: 'basic', + testMatch: ['**/basic/**/*.spec.js', '**/basic.spec.js'], + use: { + ...devices['Desktop Chrome'], + }, + fullyParallel: true, + retries: 1, + timeout: 30000, // 30s per test + }, + + { + name: 'workflows', + testMatch: ['**/workflows/**/*.spec.js', '**/workflows.spec.js'], + use: { + ...devices['Desktop Chrome'], + }, + fullyParallel: false, // Sequential for cross-DC state + retries: 2, + timeout: 60000, // 60s per test + dependencies: ['basic'], // Only run if basic tests pass + }, + ], + + // Global setup and teardown + globalSetup: require.resolve('./global-setup.js'), + globalTeardown: require.resolve('./global-teardown.js'), + + // Web server configuration (if needed) + // webServer: { + // command: 'npm start', + // url: 'http://localhost:4200', + // reuseExistingServer: !process.env.CI, + // timeout: 120000, + // }, +}); + +// Made with Bob diff --git a/ui/packages/consul-ui/e2e-tests/tests/access-controls/auth-methods/basic.spec.js b/ui/packages/consul-ui/e2e-tests/tests/access-controls/auth-methods/basic.spec.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ui/packages/consul-ui/e2e-tests/tests/access-controls/policies/basic.spec.js b/ui/packages/consul-ui/e2e-tests/tests/access-controls/policies/basic.spec.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ui/packages/consul-ui/e2e-tests/tests/access-controls/roles/basic.spec.js b/ui/packages/consul-ui/e2e-tests/tests/access-controls/roles/basic.spec.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ui/packages/consul-ui/e2e-tests/tests/access-controls/tokens/basic.spec.js b/ui/packages/consul-ui/e2e-tests/tests/access-controls/tokens/basic.spec.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ui/packages/consul-ui/e2e-tests/tests/intentions/basic.spec.js b/ui/packages/consul-ui/e2e-tests/tests/intentions/basic.spec.js new file mode 100644 index 00000000000..20c9844706e --- /dev/null +++ b/ui/packages/consul-ui/e2e-tests/tests/intentions/basic.spec.js @@ -0,0 +1,14 @@ +const { test, expect } = require('@playwright/test'); + +/** + * Intentions - Basic Tests + * + * Fast, essential tests for Intentions feature + * Run on every PR + */ + +test.describe('Intentions - Basic Tests', () => { + + // TODO: Add tests here + +}); diff --git a/ui/packages/consul-ui/e2e-tests/tests/intentions/workflows.spec.js b/ui/packages/consul-ui/e2e-tests/tests/intentions/workflows.spec.js new file mode 100644 index 00000000000..cfa69691f4a --- /dev/null +++ b/ui/packages/consul-ui/e2e-tests/tests/intentions/workflows.spec.js @@ -0,0 +1,28 @@ +const { test, expect } = require('@playwright/test'); + +/** + * Intentions - Workflow Tests + * + * Complex scenarios for Intentions feature + * Run nightly or before release + */ + +test.describe('Intentions - Workflow Tests', () => { + + test('cross-datacenter intentions', async ({ page }) => { + // TODO: Implement cross-DC intentions test + // 1. Create intention in primary DC + // 2. Verify it appears in secondary DC + // 3. Test enforcement across DCs + }); + + test('intention chain validation', async ({ page }) => { + // TODO: Implement intention chain test + // 1. Create multiple related intentions + // 2. Verify chain works correctly + // 3. Test deny overrides allow + }); + +}); + +// Made with Bob diff --git a/ui/packages/consul-ui/e2e-tests/tests/key-value/basic.spec.js b/ui/packages/consul-ui/e2e-tests/tests/key-value/basic.spec.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ui/packages/consul-ui/e2e-tests/tests/nodes/basic.spec.js b/ui/packages/consul-ui/e2e-tests/tests/nodes/basic.spec.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ui/packages/consul-ui/e2e-tests/tests/overview/basic.spec.js b/ui/packages/consul-ui/e2e-tests/tests/overview/basic.spec.js new file mode 100644 index 00000000000..ef3d27be687 --- /dev/null +++ b/ui/packages/consul-ui/e2e-tests/tests/overview/basic.spec.js @@ -0,0 +1,25 @@ +const { test, expect } = require('@playwright/test'); + +/** + * Overview - Basic Tests + * + * Fast, essential tests for Overview/Dashboard + * Run on every PR + */ + +test.describe('Overview - Basic Tests', () => { + + test('overview page loads', async ({ page }) => { + await page.goto('http://localhost:4200'); + + // Verify dashboard loads + await expect(page.locator('h1')).toBeVisible(); + }); + + // TODO: Add more basic tests + // - Verify key metrics displayed + // - Check navigation links work + +}); + +// Made with Bob diff --git a/ui/packages/consul-ui/e2e-tests/tests/peering/basic.spec.js b/ui/packages/consul-ui/e2e-tests/tests/peering/basic.spec.js new file mode 100644 index 00000000000..4bdc57f4d52 --- /dev/null +++ b/ui/packages/consul-ui/e2e-tests/tests/peering/basic.spec.js @@ -0,0 +1,15 @@ +const { test, expect } = require('@playwright/test'); + +/** + * Peering - Basic Tests + * + * Tests for peering functionality between datacenters + */ + +test.describe('Peering - Basic Tests', () => { + + // TODO: Add tests here + +}); + +// Made with Bob diff --git a/ui/packages/consul-ui/e2e-tests/tests/services/basic.spec.js b/ui/packages/consul-ui/e2e-tests/tests/services/basic.spec.js new file mode 100644 index 00000000000..8d3c27f51db --- /dev/null +++ b/ui/packages/consul-ui/e2e-tests/tests/services/basic.spec.js @@ -0,0 +1,14 @@ +const { test, expect } = require('@playwright/test'); + +/** + * Services - Basic Tests + * + * Fast, essential tests for Services feature + * Run on every PR + */ + +test.describe('Services - Basic Tests', () => { + + // TODO: Add tests here + +}); diff --git a/ui/packages/consul-ui/e2e-tests/tests/services/workflows.spec.js b/ui/packages/consul-ui/e2e-tests/tests/services/workflows.spec.js new file mode 100644 index 00000000000..241f5f34660 --- /dev/null +++ b/ui/packages/consul-ui/e2e-tests/tests/services/workflows.spec.js @@ -0,0 +1,38 @@ +const { test, expect } = require('@playwright/test'); + +/** + * Services - Workflow Tests + * + * Complex scenarios for Services feature + * Run nightly or before release + * Time: 5-15 minutes + */ + +test.describe('Services - Workflow Tests', () => { + + test('service health updates in real-time', async ({ page }) => { + // TODO: Implement real-time health check updates test + // 1. Register service with health check via API + // 2. Navigate to service details + // 3. Verify initial healthy state + // 4. Fail health check via API + // 5. Verify UI updates automatically (blocking queries) + }); + + test('complete service mesh setup', async ({ page }) => { + // TODO: Implement complete service mesh workflow + // 1. Register service with sidecar via UI + // 2. Configure intentions + // 3. Verify mesh connectivity + }); + + test('cross-datacenter service export', async ({ page }) => { + // TODO: Implement cross-DC service export test + // 1. Register service in primary DC + // 2. Export to secondary DC + // 3. Verify service appears in secondary DC + }); + +}); + +// Made with Bob diff --git a/ui/packages/consul-ui/e2e-tests/utils/auth-utils.js b/ui/packages/consul-ui/e2e-tests/utils/auth-utils.js new file mode 100644 index 00000000000..7550ae5ecc2 --- /dev/null +++ b/ui/packages/consul-ui/e2e-tests/utils/auth-utils.js @@ -0,0 +1,53 @@ +/** + * Authentication utilities for Consul UI E2E tests + */ + +/** + * Logs into Consul UI using a token + * @param {import('@playwright/test').Page} page - Playwright page object + * @param {string} token - Consul ACL token (from env or parameter) + */ +export async function loginWithToken(page, token = process.env.CONSUL_UI_TEST_TOKEN) { + if (!token) { + throw new Error('CONSUL_UI_TEST_TOKEN environment variable is not set'); + } + + // Navigate to the UI + await page.goto('http://localhost:4200/ui/dc1/services'); + + // Click on "Tokens" link in the navigation + await page.getByRole('link', { name: 'Tokens' }).click(); + + // Click "Log in" button + await page.getByRole('button', { name: 'Log in' }).click(); + + // Fill in the token + await page.getByRole('textbox', { name: 'Log in with a token' }).click(); + await page.getByRole('textbox', { name: 'Log in with a token' }).fill(token); + + // Submit the login form + await page.getByLabel('Log in to Consul').getByRole('button', { name: 'Log in' }).click(); + + // Wait for navigation to complete (login successful) + await page.waitForURL('**/ui/dc1/**'); +} + +/** + * Checks if user is already logged in + * @param {import('@playwright/test').Page} page - Playwright page object + * @returns {Promise} - True if logged in, false otherwise + */ +export async function isLoggedIn(page) { + try { + // Check if we can access a protected page without being redirected + await page.goto('http://localhost:4200/ui/dc1/services', { waitUntil: 'networkidle' }); + + // If "Log in" button is visible, user is not logged in + const loginButton = page.getByRole('button', { name: 'Log in' }); + return !(await loginButton.isVisible({ timeout: 2000 })); + } catch { + return false; + } +} + +// Made with Bob diff --git a/ui/packages/consul-ui/e2e-tests/utils/health-check-utils.js b/ui/packages/consul-ui/e2e-tests/utils/health-check-utils.js new file mode 100644 index 00000000000..86b9a9e2455 --- /dev/null +++ b/ui/packages/consul-ui/e2e-tests/utils/health-check-utils.js @@ -0,0 +1,61 @@ +const http = require('http'); +const https = require('https'); + +/** + * Check if a service is healthy by making an HTTP/HTTPS request + * @param {string} url - The URL to check + * @param {number} [timeout=5000] - Request timeout in milliseconds + * @returns {Promise} - True if service is healthy, false otherwise + */ +async function checkServiceHealth(url, timeout = 5000) { + return new Promise((resolve) => { + const client = new URL(url).protocol === 'https:' ? https : http; + const req = client.get(url, { timeout }, (res) => resolve(!!res.statusCode)); + req.on('error', () => resolve(false)); + req.on('timeout', () => { req.destroy(); resolve(false); }); + }); +} + +function getServices(baseURL = 'http://localhost:4200') { + return [ + { name: 'Consul HTTP API (8500)', url: 'http://localhost:8500/v1/status/leader', required: true }, + { name: 'Consul HTTP API (8501)', url: 'http://localhost:8501/v1/status/leader', required: true }, + { name: 'Consul UI', url: baseURL, required: true }, + ]; +} + +/** + * Check all services and return health status + * @param {string} [baseURL='http://localhost:4200'] - Base URL for the UI + * @returns {Promise} - Array of services with health status + */ +async function checkAllServices(baseURL = 'http://localhost:4200') { + const services = getServices(baseURL); + return await Promise.all( + services.map(async (s) => ({ ...s, isHealthy: await checkServiceHealth(s.url) })) + ); +} + +/** + * Print helpful error messages based on which services failed + * @param {Array} failedServices - Array of failed service objects + */ +function printServiceErrors(failedServices) { + const hasConsulAPIFailure = failedServices.some(s => s.url.includes(':8500') || s.url.includes(':8501')); + const hasUIFailure = failedServices.some(s => s.url.includes(':4200')); + + if (hasConsulAPIFailure) { + console.log('๐Ÿ“ Consul API servers (8500/8501) not running:'); + console.log(' โ†’ Start servers in consul-ui-testing repo'); + console.log(' โ†’ Run: yarn start hashicorppreview/consul-enterprise: --quiet'); + console.log(' โ†’ Example: yarn start hashicorppreview/consul-enterprise:1.22 --quiet\n'); + } + + if (hasUIFailure) { + console.log('๐Ÿ“ Consul UI (4200) not running:'); + console.log(' โ†’ Start UI in consul repo'); + console.log(' โ†’ Run: pnpm run start:consul\n'); + } +} + +module.exports = { checkServiceHealth, getServices, checkAllServices, printServiceErrors }; diff --git a/ui/packages/consul-ui/package.json b/ui/packages/consul-ui/package.json index e72caafe9e8..e5c0b88a684 100644 --- a/ui/packages/consul-ui/package.json +++ b/ui/packages/consul-ui/package.json @@ -40,6 +40,15 @@ "test:oss:ci": "CONSUL_NSPACES_ENABLED=0 ember test --test-port=${EMBER_TEST_PORT:-7357} --path dist --silent --reporter xunit", "test:oss:view": "CONSUL_NSPACES_ENABLED=0 ember test --server --test-port=${EMBER_TEST_PORT:-7357}", "test:parallel": "EMBER_EXAM_PARALLEL=true ember exam --split=4 --parallel", + "test:e2e": "playwright test --config=e2e-tests/playwright.config.js", + "test:e2e:basic": "playwright test --config=e2e-tests/playwright.config.js --project=basic", + "test:e2e:workflows": "playwright test --config=e2e-tests/playwright.config.js --project=workflows", + "test:e2e:headed": "playwright test --config=e2e-tests/playwright.config.js --headed", + "test:e2e:ui": "playwright test --config=e2e-tests/playwright.config.js --ui", + "test:e2e:codegen": "playwright codegen http://localhost:4200 & playwright codegen http://localhost:8501", + "test:e2e:codegen:4200": "playwright codegen http://localhost:4200", + "test:e2e:report": "playwright show-report e2e-tests/reports/html-report", + "test:e2e:health": "node e2e-tests/health-check.js", "test:view": "ember test --server --test-port=${EMBER_TEST_PORT:-7357}", "prepare": "husky" }, @@ -78,6 +87,7 @@ "@hashicorp/ember-cli-api-double": "^4.0.0", "@html-next/vertical-collection": "^4.0.0", "@lit/reactive-element": "^1.2.1", + "@playwright/test": "^1.58.2", "@xstate/fsm": "^2.1.0", "a11y-dialog": "^6.0.1", "autoprefixer": "^10.4.8", diff --git a/ui/packages/consul-ui/ui/packages/consul-ui/e2e-tests/auth-state.json b/ui/packages/consul-ui/ui/packages/consul-ui/e2e-tests/auth-state.json new file mode 100644 index 00000000000..f4ec35503c2 --- /dev/null +++ b/ui/packages/consul-ui/ui/packages/consul-ui/e2e-tests/auth-state.json @@ -0,0 +1,4 @@ +{ + "cookies": [], + "origins": [] +} \ No newline at end of file diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 38d7575b143..d20618f2afc 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -123,6 +123,9 @@ importers: '@lit/reactive-element': specifier: ^1.2.1 version: 1.6.3 + '@playwright/test': + specifier: ^1.58.2 + version: 1.58.2 '@xstate/fsm': specifier: ^2.1.0 version: 2.1.0 @@ -1872,6 +1875,11 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@playwright/test@1.58.2': + resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} + engines: {node: '>=18'} + hasBin: true + '@pnpm/constants@7.1.1': resolution: {integrity: sha512-31pZqMtjwV+Vaq7MaPrT1EoDFSYwye3dp6BiHIGRJmVThCQwySRKM7hCvqqI94epNkqFAAYoWrNynWoRYosGdw==} engines: {node: '>=16.14'} @@ -4466,6 +4474,11 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -6027,6 +6040,16 @@ packages: resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} engines: {node: '>=8'} + playwright-core@1.58.2: + resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.58.2: + resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} + engines: {node: '>=18'} + hasBin: true + portfinder@1.0.32: resolution: {integrity: sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==} engines: {node: '>= 0.12.0'} @@ -9549,6 +9572,10 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@playwright/test@1.58.2': + dependencies: + playwright: 1.58.2 + '@pnpm/constants@7.1.1': {} '@pnpm/error@5.0.3': @@ -13339,6 +13366,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -15014,6 +15044,14 @@ snapshots: dependencies: find-up: 3.0.0 + playwright-core@1.58.2: {} + + playwright@1.58.2: + dependencies: + playwright-core: 1.58.2 + optionalDependencies: + fsevents: 2.3.2 + portfinder@1.0.32: dependencies: async: 2.6.4