Skip to content
Merged

Dev #2711

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
8d8e128
fixing basic email cypress spec file
Jul 17, 2026
8509bb1
fixing Approval flow comments and issues
Jul 21, 2026
2618e50
bug/33849_fix_basic_email_cypress_spec: stabilize Cypress login and a…
Stephan-McColm Jul 23, 2026
7ab454d
feature/AB#32049-GHAPIToken
JamesPasta Jul 23, 2026
3db948d
Merge pull request #2703 from bcgov/bugfix/AB#32049-FixGHAPITOKEN
JamesPasta Jul 23, 2026
30bbcff
AB#33776 Scope cypress runners to use CYPRESS_OPENSHIFT_TOKEN and CY…
DarylTodosichuk Jul 23, 2026
e7461a4
Merge pull request #2704 from bcgov/feature/AB#33776-Add-OpenShift-Cy…
DarylTodosichuk Jul 23, 2026
0e9f85d
feature/AB#33893_CHEFS_One_Click_Form_Tester - Inital commit for the …
Stephan-McColm Jul 23, 2026
61ad941
feature/AB#33893_CHEFS_One_Click_Form_Tester - Removed stale references
Stephan-McColm Jul 23, 2026
fe60888
Potential fix for pull request finding
Stephan-McColm Jul 23, 2026
2a4e1e6
Potential fix for pull request finding
Stephan-McColm Jul 23, 2026
22145af
Potential fix for pull request finding
Stephan-McColm Jul 23, 2026
39fd4a9
bugfix/AB#33848-FixRolesAndAdjustColumnSizeForEmailCancel
JamesPasta Jul 23, 2026
cecb113
Merge pull request #2706 from bcgov/bugfix/AB#33848-FixRolesEmailSche…
JamesPasta Jul 23, 2026
17a7e09
Merge pull request #2705 from bcgov/feature/AB#33893_CHEFS_One_Click_…
JamesPasta Jul 23, 2026
5e7200f
Merge branch 'dev' into bug/33849_fix_basic_email_cypress_spec
Stephan-McColm Jul 23, 2026
e6698c8
Address Cypress PR review feedback
Stephan-McColm Jul 23, 2026
adf3e7e
Merge remote-tracking branch 'origin/bug/33849_fix_basic_email_cypres…
Stephan-McColm Jul 23, 2026
d1ff3f0
Merge pull request #2708 from bcgov/bugfix/33849_fix_basic_email_cypr…
JamesPasta Jul 24, 2026
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
67 changes: 44 additions & 23 deletions .github/workflows/cypress-e2e-runner.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,28 @@ name: Cypress E2E (runner)
# Unlike Grants, this doesn't run Cypress on the GitHub-hosted runner directly —
# the Unity Grant Manager Route is IP-allowlisted to the BC Gov network, which
# GitHub-hosted runners can't reach. Instead this job launches an OpenShift Job
# (from the unity-cypress-job Template in d18498-tools) that runs the existing
# (from the unity-cypress-job Template in ce395f-tools) that runs the existing
# Unity.AutoUI Cypress suite from inside the cluster, waits for it to finish,
# and pulls the logs/screenshots back into this Actions run.
#
# This deliberately targets ce395f-tools on the Gold cluster, not d18498-tools
# on Silver, even though the app environments (dev/test/uat/prod) still run on
# Silver during the cluster migration — the Job's own baseUrl (via the
# base_url input, or each cypress-config-*.json's default) is what it tests
# against, independent of which cluster runs the Job itself. Building the CI
# plumbing against the migration target now avoids redoing it later.
#
# Test credentials are NOT GitHub secrets — the Job pulls them from the
# unity-cypress-config Secret in d18498-tools, synced from Vault
# unity-cypress-config Secret in ce395f-tools, synced from Vault
# (GH_UGM_CYPRESS_CONFIG) via External Secrets.
#
# OpenShift auth reuses the same oc-login pattern already used by
# docker-build-dev.yml/docker-build-test.yml/docker-build-main.yml.
# OpenShift auth reuses the pipeline ServiceAccount (ce395f-tools), which
# already holds ClusterRole/edit there — covers everything this job needs
# (templates, processedtemplates, jobs, pods/log, pods/exec), no additional
# RBAC required. Its token is CYPRESS_OPENSHIFT_TOKEN and its cluster is
# CYPRESS_OPENSHIFT_CLUSTER — both repo-level (not tied to a GitHub
# Environment, and deliberately separate from the shared OPENSHIFT_CLUSTER
# var docker-build-*.yml uses, since that one still points at Silver).

on:
workflow_call:
Expand All @@ -32,27 +44,21 @@ on:
required: false
type: string
default: ""
gh_environment:
description: "GitHub Environment to source OpenShift credentials from (defaults to env_name — dev/test have their own; uat/prod reuse 'main')"
required: false
type: string
default: ""

permissions:
contents: read

env:
TOOLS_NAMESPACE: d18498-tools
JOB_TIMEOUT: 20m
TOOLS_NAMESPACE: ce395f-tools
JOB_TIMEOUT_SECONDS: 1200

jobs:
cypress:
name: Cypress E2E — ${{ inputs.env_name }}
runs-on: ubuntu-latest
environment: ${{ inputs.gh_environment || inputs.env_name }}
env:
OC_CLUSTER: ${{ vars.OPENSHIFT_CLUSTER }}
OC_AUTH_TOKEN: ${{ secrets.OPENSHIFT_TOKEN }}
OC_CLUSTER: ${{ vars.CYPRESS_OPENSHIFT_CLUSTER }}
OC_AUTH_TOKEN: ${{ secrets.CYPRESS_OPENSHIFT_TOKEN }}
GH_TOKEN: ${{ secrets.GH_API_TOKEN }}

steps:
Expand All @@ -68,9 +74,9 @@ jobs:
- name: Launch Cypress Job
id: launch
run: |
# The unity-cypress-job Template lives in the tenant-gitops-d18498 repo
# The unity-cypress-job Template lives in the tenant-gitops-ce395f repo
# and is synced into the cluster by ArgoCD — process it by name directly
# from d18498-tools rather than checking out that repo here.
# from ce395f-tools rather than checking out that repo here.
JOB_REF=$(oc process unity-cypress-job \
-p ENV=${{ inputs.env_name }} \
-p CYPRESS_CONFIG_KEY=${{ inputs.cypress_config_key }} \
Expand All @@ -85,15 +91,30 @@ jobs:
- name: Wait for Cypress Job to finish
id: wait
run: |
# Don't use `oc wait --for=condition=complete/failed` here: a freshly
# created Job has no .status.conditions entries yet, and
# --for=condition=X (without an explicit =true) is satisfied the
# instant the condition is merely absent rather than false — so it
# can report "condition met" seconds after creation, long before the
# Job has actually run. Poll the numeric succeeded/failed fields
# instead, which are only set once a pod genuinely finishes.
JOB_REF="${{ steps.launch.outputs.job_ref }}"
oc wait --for=condition=complete --timeout=$JOB_TIMEOUT "$JOB_REF" -n $TOOLS_NAMESPACE &
COMPLETE_PID=$!
oc wait --for=condition=failed --timeout=$JOB_TIMEOUT "$JOB_REF" -n $TOOLS_NAMESPACE &
FAILED_PID=$!
wait -n $COMPLETE_PID $FAILED_PID || true
kill $COMPLETE_PID $FAILED_PID 2>/dev/null || true
DEADLINE=$(( $(date +%s) + JOB_TIMEOUT_SECONDS ))
SUCCEEDED=0
FAILED=0
while true; do
SUCCEEDED=$(oc get "$JOB_REF" -n $TOOLS_NAMESPACE -o jsonpath='{.status.succeeded}')
FAILED=$(oc get "$JOB_REF" -n $TOOLS_NAMESPACE -o jsonpath='{.status.failed}')
if [ "${SUCCEEDED:-0}" -ge 1 ] 2>/dev/null || [ "${FAILED:-0}" -ge 1 ] 2>/dev/null; then
break
fi
if [ "$(date +%s)" -ge "$DEADLINE" ]; then
echo "Timed out after ${JOB_TIMEOUT_SECONDS}s waiting for $JOB_REF"
break
fi
sleep 10
done

SUCCEEDED=$(oc get "$JOB_REF" -n $TOOLS_NAMESPACE -o jsonpath='{.status.succeeded}')
echo "succeeded=${SUCCEEDED:-0}" >> "$GITHUB_OUTPUT"

- name: Show Cypress logs
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/cypress-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,4 @@ jobs:
with:
env_name: prod
cypress_config_key: CYPRESS_CONFIG_PROD
gh_environment: main
secrets: inherit
3 changes: 0 additions & 3 deletions .github/workflows/cypress-uat.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ name: Cypress E2E — UAT
#
# Unity has no separate UAT build workflow — UAT deploys off the main build,
# so this triggers off the same workflow_run as prod's build pipeline.
# There's also no "uat" GitHub Environment today, so oc-login reuses the
# "main" Environment's OpenShift credentials (same cluster-wide access).
#
# Runs Cypress inside OpenShift (d18498-tools) — see cypress-e2e-runner.yml.

Expand All @@ -26,5 +24,4 @@ jobs:
with:
env_name: uat
cypress_config_key: CYPRESS_CONFIG_UAT
gh_environment: main
secrets: inherit
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,10 @@ describe("Unity Login and check data from CHEFS", () => {
// Enabling this makes cy.get / cy.contains pierce shadow DOM consistently across envs.
before(() => {
Cypress.config("includeShadowDom", true);
loginIfNeeded({ timeout: 20000 });
});

beforeEach(() => {
loginIfNeeded({ timeout: 60000 });
});

it("Switch to Default Grants Program if available", () => {
Expand Down
135 changes: 100 additions & 35 deletions applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,33 @@ describe("Send an email", () => {
const loginPage = LoginPageInstance();
const navPage = NavigationPageInstance();

// The "Template Applied" swal2 dialog (see "Select Email Template") fires
// asynchronously and can land after that test has already finished waiting
// for it, leaking into whichever test runs next and blocking every field
// underneath it. Cheap and idempotent — call at the top of each subsequent
// test to mop up a straggler if one shows up.
function dismissStrayModalIfPresent() {
cy.get("body", { timeout: STANDARD_TIMEOUT }).then(($body) => {
const modal = $body.find(".swal2-popup, .modal.show");
if (modal.length === 0) {
return;
}

const dismissButton = modal
.find("button")
.filter((_, element) => {
return /^(apply template|confirm|apply|ok)$/i.test(
(element.textContent || "").trim(),
);
})
.first();

if (dismissButton.length > 0) {
cy.wrap(dismissButton).click({ force: true });
}
});
}

function openSavedEmailFromHistoryBySubject(subject: string) {
cy.get("body", { timeout: STANDARD_TIMEOUT }).then(($body) => {
const historyTableById = $body.find("#EmailHistoryTable");
Expand Down Expand Up @@ -156,7 +183,6 @@ describe("Send an email", () => {
});

it("Open Emails tab", () => {
// Dismiss any swal2 modal that may be covering the tab
cy.get("body").then(($body) => {
if ($body.find(".swal2-container").length > 0) {
cy.get(".swal2-container").then(($swal) => {
Expand All @@ -175,114 +201,153 @@ describe("Send an email", () => {
});

cy.get("#emails-tab", { timeout: STANDARD_TIMEOUT })
.should("exist")
.should("be.visible")
.click();

cy.contains("Emails", { timeout: STANDARD_TIMEOUT }).should("exist");
cy.get("#emails-tab", { timeout: STANDARD_TIMEOUT }).should(
"have.class",
"active",
);
cy.contains("Email History", { timeout: STANDARD_TIMEOUT }).should("exist");
});

it("Open New Email form", () => {
cy.get("#btn-new-email", { timeout: STANDARD_TIMEOUT })
.should("exist")
.should("be.visible")
.click();

cy.contains("Email To", { timeout: STANDARD_TIMEOUT }).should("exist");
cy.get("#EmailTo", { timeout: STANDARD_TIMEOUT }).should("be.visible");
});

it("Select Email Template", () => {
cy.get("#template", { timeout: STANDARD_TIMEOUT })
.should("exist")
// UAT/PROD are still on the pre-redesign composer (#template, no
// confirmation dialogs); DEV/TEST have the newer TinyMCE toolbar select
// (no id, only this title attribute) plus a two-step swal2 confirmation.
// Match either selector so this spec works across all four environments.
cy.get('select[title="Select a template to apply"], #template', {
timeout: STANDARD_TIMEOUT,
})
.should("be.visible")
.select(TEMPLATE_NAME);
.select(TEMPLATE_NAME, { force: true });

cy.get("#template")
.find("option:selected")
.should("have.text", TEMPLATE_NAME);
// Only the newer UI pops the "Apply Template?" / "Template Applied"
// swal2 confirmations — skip them if they don't show up.
cy.get("body", { timeout: STANDARD_TIMEOUT }).then(($body) => {
if ($body.find(".swal2-popup").length > 0) {
cy.contains(".swal2-popup button", "Apply Template", {
timeout: STANDARD_TIMEOUT,
}).click();

// Once its async content fetch resolves, the app shows a second,
// separate "Template Applied" success dialog (OK button only).
cy.contains(".swal2-popup button", "OK", {
timeout: STANDARD_TIMEOUT,
}).click();
}
});

// #EmailBody is a hidden textarea backing the rich-text editor.
// Template selection populates the visible RTE but does not auto-sync
// the backing field — trigger the change manually if still empty.
cy.get("#EmailBody", { timeout: STANDARD_TIMEOUT }).then(($el) => {
if (($el.val() as string).trim() === "") {
cy.wrap($el).invoke("val", "Test email body").trigger("change");
cy.wrap($el)
.invoke("val", "Test email body")
.trigger("change", { force: true });
}
});
});

it("Set Email To address", () => {
dismissStrayModalIfPresent();

cy.get("#EmailTo", { timeout: STANDARD_TIMEOUT })
.should("exist")
.should("be.visible")
.clear()
.type(TEST_EMAIL_TO);
.clear({ force: true })
.type(TEST_EMAIL_TO, { force: true });

cy.get("#EmailTo").should("have.value", TEST_EMAIL_TO);
});

it("Set Email CC address", () => {
dismissStrayModalIfPresent();

cy.get("#EmailCC", { timeout: STANDARD_TIMEOUT })
.should("exist")
.should("be.visible")
.clear()
.type(TEST_EMAIL_CC);
.clear({ force: true })
.type(TEST_EMAIL_CC, { force: true });

cy.get("#EmailCC").should("have.value", TEST_EMAIL_CC);
});

it("Set Email BCC address", () => {
dismissStrayModalIfPresent();

// The BCC row (#bcc-input-row) is hidden until the BCC toggle is clicked.
cy.get("body", { timeout: STANDARD_TIMEOUT }).then(($body) => {
const bccRow = $body.find("#bcc-input-row");
if (bccRow.length > 0 && !Cypress.$(bccRow[0]).is(":visible")) {
cy.get("#btn-show-bcc").click();
}
});

cy.get("#EmailBCC", { timeout: STANDARD_TIMEOUT })
.should("exist")
.should("be.visible")
.clear()
.type(TEST_EMAIL_BCC);
.clear({ force: true })
.type(TEST_EMAIL_BCC, { force: true });

cy.get("#EmailBCC").should("have.value", TEST_EMAIL_BCC);
});

it("Set Email Subject", () => {
dismissStrayModalIfPresent();

cy.get("#EmailSubject", { timeout: STANDARD_TIMEOUT })
.should("exist")
.should("be.visible")
.clear()
.type(TEST_EMAIL_SUBJECT);
.clear({ force: true })
.type(TEST_EMAIL_SUBJECT, { force: true });

cy.get("#EmailSubject").should("have.value", TEST_EMAIL_SUBJECT);
});

it("Save the email", () => {
cy.get("#btn-save", { timeout: STANDARD_TIMEOUT })
.should("exist")
dismissStrayModalIfPresent();

cy.get("#btn-save-top, #btn-save", { timeout: STANDARD_TIMEOUT })
.scrollIntoView()
.should("be.visible")
.click();
.click({ force: true });

cy.get("#btn-new-email", { timeout: STANDARD_TIMEOUT }).should(
"be.visible",
);
cy.contains("#EmailHistoryTable td", TEST_EMAIL_SUBJECT, {
timeout: STANDARD_TIMEOUT,
}).should("exist");
});

it("Select saved email from Email History", () => {
dismissStrayModalIfPresent();
openSavedEmailFromHistoryBySubject(TEST_EMAIL_SUBJECT);

// Clicking the history row scrolled the table into view, not the
// composer above it — scroll back up before checking field visibility.
cy.get("#EmailForm", { timeout: STANDARD_TIMEOUT }).scrollIntoView();

cy.get("#EmailTo", { timeout: STANDARD_TIMEOUT }).should("be.visible");
cy.get("#EmailCC").should("be.visible");
cy.get("#EmailBCC").should("be.visible");
cy.get("#EmailSubject").should("be.visible");

cy.get("#btn-send", { timeout: STANDARD_TIMEOUT }).should("exist");
cy.get("#btn-save", { timeout: STANDARD_TIMEOUT }).should("exist");
cy.get("#btn-send-top, #btn-send", { timeout: STANDARD_TIMEOUT }).should("exist");
cy.get("#btn-save-top, #btn-save", { timeout: STANDARD_TIMEOUT }).should("exist");
});

it("Send the email", () => {
cy.get("#btn-send", { timeout: STANDARD_TIMEOUT })
.should("exist")
dismissStrayModalIfPresent();

cy.get("#btn-send-top, #btn-send", { timeout: STANDARD_TIMEOUT })
.scrollIntoView()
.should("be.visible")
.should("not.be.disabled")
.click();
.click({ force: true });
});

it("Confirm send email in dialog", () => {
Expand Down
Loading
Loading