diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 8401cb82..bfbb19c3 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -150,4 +150,61 @@ jobs: context: . push: true tags: ghcr.io/diracgrid/diracx-web/static:${{ needs.release-please.outputs.tag_name }} - platforms: linux/amd64,linux/arm64 \ No newline at end of file + platforms: linux/amd64,linux/arm64 + + # Helm charts are updated in diracx-charts + # ----------------------------------------- + + update-charts: + name: Update Helm charts + needs: + - release-please + - build-deploy-diracx-web-release-image + runs-on: ubuntu-latest + if: ${{ needs.release-please.outputs.release_created == 'true' }} + steps: + - name: Checkout diracx-web (for update script) + uses: actions/checkout@v6 + with: + path: diracx-web + sparse-checkout: .github/workflows + + - name: Checkout diracx-charts + uses: actions/checkout@v6 + with: + repository: DIRACGrid/diracx-charts + token: ${{ secrets.CHARTS_UPDATE_TOKEN }} + path: diracx-charts + + - name: Configure Git + run: | + cd diracx-charts + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Update chart versions + run: | + python diracx-web/.github/workflows/update_chart_version.py \ + --charts-dir diracx-charts \ + --web-version "${{ needs.release-please.outputs.tag_name }}" + + - name: Install pre-commit + run: pip install pre-commit + + - name: Run pre-commit to update README + run: | + cd diracx-charts + pre-commit run --all-files || true + + - name: Commit and push changes + if: success() + run: | + cd diracx-charts + git add -A + + if ! git diff --cached --quiet; then + git commit -m "chore: bump diracx-web to ${{ needs.release-please.outputs.tag_name }}" + git push origin master + else + echo "No changes to commit" + fi diff --git a/.github/workflows/gubbins-test.yml b/.github/workflows/gubbins-test.yml index 9ec0dfec..d247137f 100644 --- a/.github/workflows/gubbins-test.yml +++ b/.github/workflows/gubbins-test.yml @@ -54,101 +54,41 @@ jobs: prepare-gubbins-backend: runs-on: ubuntu-latest if: ${{ github.event_name != 'push' || github.repository == 'DIRACGrid/diracx-web' }} - defaults: - run: - # We need extglob for REFERENCE_BRANCH substitution - shell: bash -l -O extglob {0} steps: - name: Clone source run: | cd .. git clone https://github.com/DIRACGrid/diracx.git - # Prepare the gubbins extension - - name: Where the magic happens (Move extensions to a temporary directory) - run: | - # We have to copy the code to another directory - # and make it a git repository by itself because otherwise the - # root in the pyproject to do not make sense once mounted - # in the containers. - cp -r ../diracx/extensions/gubbins /tmp/ - sed -i 's@../..@.@g' /tmp/gubbins/pyproject.toml - sed -i 's@../../@@g' /tmp/gubbins/gubbins-*/pyproject.toml - - - name: Upload artifact - uses: actions/upload-artifact@v7 - with: - name: gubbins - path: /tmp/gubbins - include-hidden-files: true - - # Prepare the gubbins image - # - Build the gubbins wheels - - uses: actions/setup-python@v6 - with: - python-version: '3.14' - - name: Installing dependencies - run: | - cd ../diracx - python -m pip install \ - build \ - python-dateutil \ - pytz \ - readme_renderer[md] \ - requests \ - setuptools_scm - - name: Build distributions - run: | - cd ../diracx - for pkg_dir in $PWD/diracx-*; do - echo "Building $pkg_dir" - python -m build --outdir $PWD/dist $pkg_dir - done - # Also build the diracx metapackage - python -m build --outdir $PWD/dist . - # And build the gubbins package - for pkg_dir in $PWD/extensions/gubbins/gubbins-*; do - # Skip the testing package - if [[ "${pkg_dir}" =~ .*testing.* ]]; - then - echo "Do not build ${pkg_dir}"; - continue; - fi - echo "Building $pkg_dir" - python -m build --outdir $PWD/dist $pkg_dir - done - - name: "Find wheels" - id: find_wheel - run: | - cd ../diracx/dist - # We need to copy them there to be able to access them in the RUN --mount - cp diracx*.whl gubbins*.whl ../extensions/containers/services/ - for wheel_fn in *.whl; do - pkg_name=$(basename "${wheel_fn}" | cut -d '-' -f 1) - echo "${pkg_name}-wheel-name=$(ls "${pkg_name}"-*.whl)" >> $GITHUB_OUTPUT - done - - # - Build the gubbins image using the wheels - - name: Set up QEMU - uses: docker/setup-qemu-action@v4 + # Build gubbins container images using pixi - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 - - name: Build and export service + - name: Build gubbins services image uses: docker/build-push-action@v7 with: - context: ../diracx/extensions/containers/services - tags: gubbins/services:dev + context: ../diracx + file: ../diracx/containers/Dockerfile + build-args: | + PIXI_ENV=gubbins-container-services + tags: ghcr.io/gubbins/services:dev outputs: type=docker,dest=/tmp/gubbins_services_image.tar + + - name: Build gubbins client image + uses: docker/build-push-action@v7 + with: + context: ../diracx + file: ../diracx/containers/Dockerfile build-args: | - EXTRA_PACKAGES_TO_INSTALL=git+https://github.com/DIRACGrid/DIRAC.git@integration - EXTENSION_CUSTOM_SOURCES_TO_INSTALL=/bindmount/gubbins_db*.whl,/bindmount/gubbins_routers*.whl,/bindmount/gubbins_client*.whl + PIXI_ENV=gubbins-container-client + tags: ghcr.io/gubbins/client:dev + outputs: type=docker,dest=/tmp/gubbins_client_image.tar - - name: Upload artifact + - name: Upload gubbins images uses: actions/upload-artifact@v7 with: - name: gubbins-services-img - path: /tmp/gubbins_services_image.tar + name: gubbins-images + path: /tmp/gubbins_*_image.tar prepare-gubbins-frontend-as-a-standalone: runs-on: ubuntu-latest @@ -226,7 +166,7 @@ jobs: # WARNING: In your CI/CD pipeline, you should remove the following steps # # ------------------------------------------------------------------------# - name: Download gubbins-web - uses: actions/download-artifact@v8 + uses: actions/download-artifact@v7 with: name: gubbins-web path: /tmp/gubbins-web @@ -258,43 +198,57 @@ jobs: # ========================================================================# # WARNING: In your CI/CD pipeline, you should remove the following steps # # ------------------------------------------------------------------------# - - name: Download gubbins - uses: actions/download-artifact@v8 - with: - name: gubbins - path: /tmp/gubbins - - - name: Create a new git repository - run: | - git init /tmp/gubbins/ - - - name: Clone diracx source + - name: Clone diracx source run: | cd .. git clone https://github.com/DIRACGrid/diracx.git - - name: Download gubbins:services image - uses: actions/download-artifact@v8 + - name: Download gubbins images + uses: actions/download-artifact@v7 with: - name: gubbins-services-img + name: gubbins-images path: /tmp/ - - name: Load docker image - run: docker load --input /tmp/gubbins_services_image.tar + - name: Load gubbins images + run: | + docker load --input /tmp/gubbins_services_image.tar + rm -f /tmp/gubbins_services_image.tar + docker load --input /tmp/gubbins_client_image.tar + rm -f /tmp/gubbins_client_image.tar - name: Download gubbins-web - uses: actions/download-artifact@v8 + uses: actions/download-artifact@v7 with: name: gubbins-web path: /tmp/gubbins-web # ------------------------------------------------------------------------# # ========================================================================# - + # Runs the demo with the extension source code - - name: Start demo + - name: Start demo run: | cd .. - diracx-charts/run_demo.sh --exit-when-done --set-value developer.autoReload=false --ci-values ./diracx/extensions/gubbins_values.yaml --load-docker-image "gubbins/services:dev" diracx/ /tmp/gubbins/ /tmp/gubbins-web/ + + # Download helm/kubectl/kind first + diracx-charts/run_demo.sh --only-download-deps + + # Copy gubbins-charts to a temporary location and build dependencies + cp -r ./diracx/extensions/gubbins-charts /tmp/ + diracx-charts/.demo/helm dependency build /tmp/gubbins-charts + # Replace the downloaded subchart with the locally-cloned one to + # ensure we have the pixi-compatible ConfigMap entrypoint + rm -f /tmp/gubbins-charts/charts/diracx-*.tgz + diracx-charts/.demo/helm package diracx-charts/diracx -d /tmp/gubbins-charts/charts/ + + diracx-charts/run_demo.sh \ + --exit-when-done \ + --set-value diracx.developer.autoReload=false \ + --extension-chart-path /tmp/gubbins-charts \ + --ci-values ./diracx/extensions/gubbins_values.yaml \ + --load-docker-image "ghcr.io/gubbins/services:dev" \ + --load-docker-image "ghcr.io/gubbins/client:dev" \ + --prune-loaded-images \ + /tmp/gubbins-web/ - name: Debugging information run: | diff --git a/.github/workflows/update_chart_version.py b/.github/workflows/update_chart_version.py new file mode 100644 index 00000000..7c663501 --- /dev/null +++ b/.github/workflows/update_chart_version.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +"""Update diracx-charts with a new diracx-web version. + +Bumps the chart version and updates the web image tag in values.yaml. +Does NOT modify appVersion (that tracks the diracx server version). +""" + +from __future__ import annotations + +import argparse +import re +import sys +from pathlib import Path + + +def bump_version(current_version: str) -> str: + """Bump a version: increment alpha number if present, otherwise patch.""" + match = re.match( + r"^(\d+\.\d+\.\d+)-alpha\.(\d+)$", current_version + ) + if match: + base, alpha = match.group(1), int(match.group(2)) + return f"{base}-alpha.{alpha + 1}" + + match = re.match(r"^(\d+)\.(\d+)\.(\d+)$", current_version) + if match: + major, minor, patch = match.groups() + return f"{major}.{minor}.{int(patch) + 1}" + + raise ValueError(f"Invalid version format: {current_version}") + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Update diracx-charts for a new diracx-web release" + ) + parser.add_argument( + "--charts-dir", + type=Path, + required=True, + help="Path to the diracx-charts repository", + ) + parser.add_argument( + "--web-version", + required=True, + help="New diracx-web version (e.g., v0.1.0)", + ) + args = parser.parse_args() + + chart_yaml = args.charts_dir / "diracx" / "Chart.yaml" + values_yaml = args.charts_dir / "diracx" / "values.yaml" + + for path in (chart_yaml, values_yaml): + if not path.exists(): + print(f"Error: {path} not found") + sys.exit(1) + + # Read and bump chart version + chart_content = chart_yaml.read_text() + version_match = re.search(r'^version:\s*"?([^"\n]+)"?', chart_content, re.MULTILINE) + if not version_match: + print("Error: could not find version in Chart.yaml") + sys.exit(1) + + current_chart_version = version_match.group(1) + new_chart_version = bump_version(current_chart_version) + + chart_content = re.sub( + r'^version:\s*.*$', + f'version: "{new_chart_version}"', + chart_content, + flags=re.MULTILINE, + ) + chart_yaml.write_text(chart_content) + print(f"Chart version: {current_chart_version} -> {new_chart_version}") + + # Update web image tag in values.yaml + values_content = values_yaml.read_text() + values_content = re.sub( + r'(^ web:\s*\n tag:\s*).*$', + rf'\g<1>{args.web_version}', + values_content, + flags=re.MULTILINE, + ) + values_yaml.write_text(values_content) + print(f"Web image tag: {args.web_version}") + + +if __name__ == "__main__": + main() diff --git a/packages/diracx-web/test/e2e/dashboard.cy.ts b/packages/diracx-web/test/e2e/dashboard.cy.ts index 4777a884..2e88b95b 100644 --- a/packages/diracx-web/test/e2e/dashboard.cy.ts +++ b/packages/diracx-web/test/e2e/dashboard.cy.ts @@ -11,7 +11,7 @@ describe("DashboardDrawer", { retries: { runMode: 5, openMode: 3 } }, () => { ); }); - cy.visit("/"); + cy.visitApp(); }); it("should render the drawer", () => { diff --git a/packages/diracx-web/test/e2e/importExportState.cy.ts b/packages/diracx-web/test/e2e/importExportState.cy.ts index a7a1e041..e46fcf38 100644 --- a/packages/diracx-web/test/e2e/importExportState.cy.ts +++ b/packages/diracx-web/test/e2e/importExportState.cy.ts @@ -5,7 +5,7 @@ describe("Export and import app state", () => { beforeEach(() => { cy.login(); - cy.visit("/"); + cy.visitApp(); // Open 2 Job Monitor apps cy.get('[data-testid="add-application-button"]').click(); diff --git a/packages/diracx-web/test/e2e/jobMonitor.columns.cy.ts b/packages/diracx-web/test/e2e/jobMonitor.columns.cy.ts index 7d2a6207..02b8cc72 100644 --- a/packages/diracx-web/test/e2e/jobMonitor.columns.cy.ts +++ b/packages/diracx-web/test/e2e/jobMonitor.columns.cy.ts @@ -9,9 +9,8 @@ import { describe("Job Monitor - Columns", () => { beforeEach(() => { cy.login(); - - cy.visit("/"); setupJobMonitorDashboard(); + cy.visitApp(); cy.contains("Job Monitor").click(); diff --git a/packages/diracx-web/test/e2e/jobMonitor.filters.cy.ts b/packages/diracx-web/test/e2e/jobMonitor.filters.cy.ts index 7af4344e..a49d7a8f 100644 --- a/packages/diracx-web/test/e2e/jobMonitor.filters.cy.ts +++ b/packages/diracx-web/test/e2e/jobMonitor.filters.cy.ts @@ -9,9 +9,8 @@ import { describe("Job Monitor - Filters", () => { beforeEach(() => { cy.login(); - - cy.visit("/"); setupJobMonitorDashboard(); + cy.visitApp(); cy.contains("Job Monitor").click(); diff --git a/packages/diracx-web/test/e2e/jobMonitor.pagination.cy.ts b/packages/diracx-web/test/e2e/jobMonitor.pagination.cy.ts index c3f96491..6451397d 100644 --- a/packages/diracx-web/test/e2e/jobMonitor.pagination.cy.ts +++ b/packages/diracx-web/test/e2e/jobMonitor.pagination.cy.ts @@ -9,9 +9,8 @@ import { describe("Job Monitor - Pagination", () => { beforeEach(() => { cy.login(); - - cy.visit("/"); setupJobMonitorDashboard(); + cy.visitApp(); cy.contains("Job Monitor").click(); diff --git a/packages/diracx-web/test/e2e/jobMonitor.pieChart.cy.ts b/packages/diracx-web/test/e2e/jobMonitor.pieChart.cy.ts index b3a18a1f..35bf548b 100644 --- a/packages/diracx-web/test/e2e/jobMonitor.pieChart.cy.ts +++ b/packages/diracx-web/test/e2e/jobMonitor.pieChart.cy.ts @@ -9,9 +9,8 @@ import { describe("Job Monitor - Pie Chart", () => { beforeEach(() => { cy.login(); - - cy.visit("/"); setupJobMonitorDashboard(); + cy.visitApp(); cy.contains("Job Monitor").click(); diff --git a/packages/diracx-web/test/e2e/jobMonitor.rowActions.cy.ts b/packages/diracx-web/test/e2e/jobMonitor.rowActions.cy.ts index 6869e6d1..b6266679 100644 --- a/packages/diracx-web/test/e2e/jobMonitor.rowActions.cy.ts +++ b/packages/diracx-web/test/e2e/jobMonitor.rowActions.cy.ts @@ -9,9 +9,8 @@ import { describe("Job Monitor - Row Actions", () => { beforeEach(() => { cy.login(); - - cy.visit("/"); setupJobMonitorDashboard(); + cy.visitApp(); cy.contains("Job Monitor").click(); diff --git a/packages/diracx-web/test/e2e/jobMonitor.sandbox.cy.ts b/packages/diracx-web/test/e2e/jobMonitor.sandbox.cy.ts index 25fc95e3..e663b3c5 100644 --- a/packages/diracx-web/test/e2e/jobMonitor.sandbox.cy.ts +++ b/packages/diracx-web/test/e2e/jobMonitor.sandbox.cy.ts @@ -9,13 +9,11 @@ import { describe("Job Monitor - Sandbox Download", () => { beforeEach(() => { cy.login(); - cy.visit("/"); setupJobMonitorDashboard(); + cy.visitApp(); cy.contains("Job Monitor").click(); // Wait for the table to be ready - cy.contains("Loading OIDC Configuration").should("not.exist"); - cy.contains("Loading").should("not.exist"); cy.get('[data-testid="loading-skeleton"]').should("not.exist"); }); diff --git a/packages/diracx-web/test/e2e/loginOut.cy.ts b/packages/diracx-web/test/e2e/loginOut.cy.ts index fa6d2512..5b6c04e9 100644 --- a/packages/diracx-web/test/e2e/loginOut.cy.ts +++ b/packages/diracx-web/test/e2e/loginOut.cy.ts @@ -1,4 +1,5 @@ /// +/// // Make sure the user can login and logout describe("Login and Logout", () => { @@ -7,14 +8,10 @@ describe("Login and Logout", () => { // so we must tell it to visit our website with the `cy.visit()` command. // Since we want to visit the same URL at the start of all our tests, // we include it in our beforeEach function so that it runs before each test - cy.visit("/"); + cy.visit("/auth"); }); it("login", () => { - // The user is redirected to the /auth page because not authenticated - // Make sure we are on the /auth page - cy.url().should("include", "/auth"); - // Continue with the default parameters cy.get('[data-testid="login-form-button"]').click(); @@ -57,16 +54,12 @@ describe("Login and Logout", () => { // Logout cy.contains("Logout").click(); - // The user is redirected back to the /auth page - cy.url().should("include", "/auth"); - - // The user is logged out - // The login button should be present - cy.get('[data-testid="login-form-button"]').should("exist"); - - // The user tries to access the dashboard page without being connected - // The user is redirected to the /auth page - cy.visit("/"); - cy.url().should("include", "/auth"); + // The user is logged out and redirected back to the /auth page + // Wait for the login button to reappear as the definitive signal. + // Uses a longer timeout because the logout → OIDC session clear → + // redirect → render chain can take longer than the default 4s in CI. + cy.get('[data-testid="login-form-button"]', { timeout: 10000 }).should( + "exist", + ); }); }); diff --git a/packages/diracx-web/test/e2e/support/commands.ts b/packages/diracx-web/test/e2e/support/commands.ts index 2f440e0b..7751119c 100644 --- a/packages/diracx-web/test/e2e/support/commands.ts +++ b/packages/diracx-web/test/e2e/support/commands.ts @@ -2,7 +2,7 @@ Cypress.Commands.add("login", () => { cy.session("login", () => { - cy.visit("/"); + cy.visit("/auth"); // Login cy.get('[data-testid="login-form-button"]').click(); @@ -22,3 +22,11 @@ Cypress.Commands.add("login", () => { cy.url().should("include", "/auth"); }); }); + +Cypress.Commands.add("visitApp", () => { + cy.visit("/"); + // Wait for the authenticated dashboard to be fully rendered. + // The permanent drawer is always present once OIDC initialization, + // session validation, and React rendering are all complete. + cy.get('[data-testid="drawer-permanent"]').should("exist"); +}); diff --git a/packages/diracx-web/test/e2e/support/index.d.ts b/packages/diracx-web/test/e2e/support/index.d.ts index a76aa482..9ceeb992 100644 --- a/packages/diracx-web/test/e2e/support/index.d.ts +++ b/packages/diracx-web/test/e2e/support/index.d.ts @@ -7,5 +7,11 @@ declare namespace Cypress { * Uses cy.session() so the login flow only runs once per spec. */ login(): Chainable; + + /** + * Visit the app and wait for the OIDC provider to initialize. + * Use this instead of cy.visit("/") after cy.login(). + */ + visitApp(): Chainable; } } diff --git a/packages/diracx-web/test/e2e/support/jobMonitorUtils.ts b/packages/diracx-web/test/e2e/support/jobMonitorUtils.ts index f1369f8a..4194cba6 100644 --- a/packages/diracx-web/test/e2e/support/jobMonitorUtils.ts +++ b/packages/diracx-web/test/e2e/support/jobMonitorUtils.ts @@ -1,6 +1,6 @@ /** * Set up the dashboard with two Job Monitor apps via sessionStorage. - * Call this after cy.login() and before cy.visit("/"). + * Call this after cy.login() and before cy.visitApp(). */ export function setupJobMonitorDashboard() { cy.window().then((win) => { @@ -150,8 +150,6 @@ export function addJobWithOutputSandbox() { * If not, add jobs and refresh. Call after the table is visible. */ export function ensureMinimumJobs(minNumberOfJobs: number) { - cy.contains("Loading OIDC Configuration").should("not.exist"); - cy.contains("Loading").should("not.exist"); cy.get('[data-testid="loading-skeleton"]').should("not.exist"); cy.get("body").then(($body) => { diff --git a/packages/extensions/cypress.config.ts b/packages/extensions/cypress.config.ts index 1f7c7b0d..f5c80839 100644 --- a/packages/extensions/cypress.config.ts +++ b/packages/extensions/cypress.config.ts @@ -3,8 +3,8 @@ import { defineConfig } from "cypress"; export default defineConfig({ e2e: { specPattern: "test/e2e/**/*.cy.ts", - supportFile: false, - setupNodeEvents(on, config) { + supportFile: "test/e2e/support/e2e.ts", + setupNodeEvents(_on, _config) { // implement node event listeners here }, }, diff --git a/packages/extensions/test/e2e/loginOut.cy.ts b/packages/extensions/test/e2e/loginOut.cy.ts index 8f75aef1..a757a5f7 100644 --- a/packages/extensions/test/e2e/loginOut.cy.ts +++ b/packages/extensions/test/e2e/loginOut.cy.ts @@ -1,4 +1,5 @@ /// +/// // Make sure the user can login and logout describe("Login and Logout", () => { @@ -11,10 +12,6 @@ describe("Login and Logout", () => { }); it("login", () => { - // The user is redirected to the /auth page because not authenticated - // Make sure we are on the /auth page - cy.url().should("include", "/auth"); - // Continue with the default parameters cy.get('[data-testid="login-form-button"]').click(); @@ -41,11 +38,9 @@ describe("Login and Logout", () => { cy.url().should("include", "/auth"); // From now on the user is logged in - // The login buttton should not be present anymore + // The login button should not be present anymore cy.get('[data-testid="login-form-button"]').should("not.exist"); - - cy.visit("/"); - cy.contains("Owners").should("exist"); + cy.contains("Owners", { timeout: 10000 }).should("exist"); // Click on the user avatar cy.get(".MuiAvatar-root").click(); @@ -59,16 +54,12 @@ describe("Login and Logout", () => { // Logout cy.contains("Logout").click(); - // The user is redirected back to the /auth page - cy.url().should("include", "/auth"); - - // The user is logged out - // The login button should be present - cy.get('[data-testid="login-form-button"]').should("exist"); - - // The user tries to access the dashboard page without being connected - // The user is redirected to the /auth page - cy.visit("/"); - cy.url().should("include", "/auth"); + // The user is logged out and redirected back to the /auth page + // Wait for the login button to reappear as the definitive signal. + // Uses a longer timeout because the logout → OIDC session clear → + // redirect → render chain can take longer than the default 4s in CI. + cy.get('[data-testid="login-form-button"]', { timeout: 10000 }).should( + "exist", + ); }); }); diff --git a/packages/extensions/test/e2e/ownerMonitor.cy.ts b/packages/extensions/test/e2e/ownerMonitor.cy.ts index 19e7bb10..33f0b0ed 100644 --- a/packages/extensions/test/e2e/ownerMonitor.cy.ts +++ b/packages/extensions/test/e2e/ownerMonitor.cy.ts @@ -1,22 +1,11 @@ /// +/// describe("Owner Monitor", () => { beforeEach(() => { - cy.session("login", () => { - cy.visit("/auth"); - //login - cy.get('[data-testid="login-form-button"]').click(); - cy.get("#login").type("admin@example.com"); - cy.get("#password").type("password"); + cy.login(); - // Find the login button and click on it - cy.get("button").click(); - // Grant access - cy.get(":nth-child(1) > form > .dex-btn").click(); - cy.url().should("include", "/auth"); - }); - - // Visit the page where the Job Monitor is rendered + // Visit the page where the Owner Monitor is rendered cy.window().then((win) => { win.sessionStorage.setItem( "savedDashboard", @@ -24,7 +13,7 @@ describe("Owner Monitor", () => { ); }); - cy.visit("/"); + cy.visitApp(); }); it("should render the drawer", () => { diff --git a/packages/extensions/test/e2e/support/commands.ts b/packages/extensions/test/e2e/support/commands.ts new file mode 100644 index 00000000..7751119c --- /dev/null +++ b/packages/extensions/test/e2e/support/commands.ts @@ -0,0 +1,32 @@ +/// + +Cypress.Commands.add("login", () => { + cy.session("login", () => { + cy.visit("/auth"); + + // Login + cy.get('[data-testid="login-form-button"]').click(); + + // Handle OIDC provider login (cross-origin) + const domain = Cypress.config() + .baseUrl?.replace("https://", "") + .split(":")[0]; + + cy.origin(`http://${domain}:32002`, () => { + cy.get("#login").type("admin@example.com"); + cy.get("#password").type("password"); + cy.get("button").click(); + cy.get(":nth-child(1) > form > .dex-btn").click(); + }); + + cy.url().should("include", "/auth"); + }); +}); + +Cypress.Commands.add("visitApp", () => { + cy.visit("/"); + // Wait for the authenticated dashboard to be fully rendered. + // The permanent drawer is always present once OIDC initialization, + // session validation, and React rendering are all complete. + cy.get('[data-testid="drawer-permanent"]').should("exist"); +}); diff --git a/packages/extensions/test/e2e/support/e2e.ts b/packages/extensions/test/e2e/support/e2e.ts new file mode 100644 index 00000000..f887c29a --- /dev/null +++ b/packages/extensions/test/e2e/support/e2e.ts @@ -0,0 +1 @@ +import "./commands"; diff --git a/packages/extensions/test/e2e/support/index.d.ts b/packages/extensions/test/e2e/support/index.d.ts new file mode 100644 index 00000000..9ceeb992 --- /dev/null +++ b/packages/extensions/test/e2e/support/index.d.ts @@ -0,0 +1,17 @@ +/// + +declare namespace Cypress { + interface Chainable { + /** + * Log in via the OIDC provider and cache the session. + * Uses cy.session() so the login flow only runs once per spec. + */ + login(): Chainable; + + /** + * Visit the app and wait for the OIDC provider to initialize. + * Use this instead of cy.visit("/") after cy.login(). + */ + visitApp(): Chainable; + } +} diff --git a/renovate.json b/renovate.json index 4fdcd402..d70fb4bc 100644 --- a/renovate.json +++ b/renovate.json @@ -1,6 +1,6 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["config:recommended"], + "extends": ["config:recommended", "helpers:pinGitHubActionDigests"], "minimumReleaseAge": "7 days", "packageRules": [ {