Skip to content

chore: [DHIS2-22074] de-flake the RelatedStages, assignee and sharing dialog scenarios - #4718

Open
karolinelien wants to merge 6 commits into
masterfrom
chore/de-flake-related-stages
Open

chore: [DHIS2-22074] de-flake the RelatedStages, assignee and sharing dialog scenarios#4718
karolinelien wants to merge 6 commits into
masterfrom
chore/de-flake-related-stages

Conversation

@karolinelien

@karolinelien karolinelien commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

https://dhis2.atlassian.net/browse/DHIS2-22074

Six scenarios that depend on state no step establishes, plus one race.

  • RelatedStages (×2) assert the schedule and enter-details actions are disabled, which only holds when the Baby Postnatal stage already has an event — and nothing creates one, so they rely on a leftover. New step seeds it through the API.
  • Assignee (×2) assign a user and remove them as their last step. The Assign button renders only when nobody is assigned, so one interrupted run leaves the shared instance stuck for every branch. New step clears the assignment.
  • Sharing dialog (×2) type into the search box while it is still disabled — cy.type() does not wait for that to clear. Gated with should('be.enabled') and scoped to the dialog.

Also: the Enroll trackedEntity scenarios clean up through an API After hook instead of the UI, so it runs when a scenario fails; the two deletion steps always reload; and the tracker writes share a cy.importTracker command that asserts the import succeeded.

Verified against play stable-2-43-1, each run starting from the broken state — including the control: with the assignee guard removed, the CI failure reproduces exactly.

Not fixed here: The sharing settings on a program stage working list view survive an update (2.41) — one sample, no CI artifacts, and it sits in a file the working-list sharing branch is actively changing.

AI Assisted.

karolinelien and others added 3 commits September 3, 2026 12:48
The two link and unlink scenarios assert that the schedule and enter details
actions are disabled. Those actions are disabled only when the Baby Postnatal
stage already has an event, since useCanAddNewEventToStage returns repeatable ||
existingRelatedEvents.length === 0 and both Child Programme stages are
non-repeatable. Neither scenario creates that event, so both depend on one being
left behind by an earlier run.

For enrollment EOxeNf2MdBf that dependency is circular: the next scenario in the
file deletes every event of the same enrollment and re-creates them through the
UI, so when it fails part way through, the following run fails in the link and
unlink scenario instead, with an input that never becomes disabled. That is the
shard 2 flake.

Seed the event through the API instead, guarded by a check for an existing one,
which is also the only case where creating it is legal for a non-repeatable
stage. Both link and unlink scenarios now state the fixture they need rather
than inheriting it.

Also make the two event deletion steps idempotent: the reload only happened when
there was something to delete, so a re-attempt kept the pre-deletion page, and
neither step noticed an import that came back with errors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The seed guard checked only that a Baby Postnatal event exists. Both
link-and-unlink scenarios then pick that event out of the "Link to an
existing event" list, which the widget builds from linkableEvents —
events not already carrying a relationship of that type. A run that dies
between the Link click and the closing unlink leaves the event linked,
so the next run would skip seeding on existence alone and fail on an
empty list.

The step now also strips the event's relationships, through the same
code path as the unlink step, and is renamed to say so.

Collapse the four tracker writes in the file onto one importTracker
helper that asserts status === 'OK', replacing three near-identical
inline blocks and covering the unlink step, which had no assertion.
Seed with CREATE rather than CREATE_AND_UPDATE, since the guard has
already established the event is absent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
karolinelien and others added 2 commits September 4, 2026 15:15
The two Enroll trackedEntity scenarios created a real tracked entity and
deleted it again through the UI as their last step, so a failure anywhere
earlier in the scenario leaked the record - once per retry attempt. Move
that cleanup into an After hook that deletes through the API, which runs
even when the scenario fails, following the pattern from #4714. The
generated first name is captured in a module variable so the hook can
look the record up, and uses Date.now() so parallel shards cannot match
each other's records.

Also drop the linkability half of the seed guard. It was added for a
state that turns out to be unreachable: deleting an event cascades to its
relationships, verified directly against play dev - link a Birth event to
its Baby Postnatal event, delete the Birth event, and the related event
comes back with zero relationships. So a run that dies mid-scenario
cannot leave a Baby Postnatal event that is present but unlinkable; the
Birth deletion in the New event scenario and the existing unlink step in
the Edit event scenario each clear it. The guard is back to checking
existence, which is the state the assertion actually reads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both assignee scenarios assign a user and remove them again as their last
step, so a run that dies in between leaves the event assigned. The Assign
button only renders when nobody is assigned - DisplayMode.component.tsx
branches on `assignee`, showing Edit instead - so every later run fails
looking for `widget-assignee-assign`, on every branch, until someone
clears the assignment by hand. That is the state the shared dev instance
is in now: the same failure reproduces on other open pull requests.

Add `you make sure the event <id> has no assigned user`, which clears the
assignment through the API by re-importing the event without
`assignedUser`. Verified against play stable-2-43-1: with Geetha assigned
to z4PCgdaBXFh, the spec passes 12/12 with the guard and fails with the
exact CI assertion without it. Both scenarios get the guard - the edit
mode one leaks the same way and has only been lucky so far.

Move the tracker import helper out of RelatedStages.js into a
cy.importTracker command in cypress/support/commands.js, since the guard
needs it too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@karolinelien karolinelien changed the title chore: de-flake the RelatedStages Cypress spec chore: de-flake the RelatedStages and assignee widget Cypress specs Sep 7, 2026
@karolinelien karolinelien changed the title chore: de-flake the RelatedStages and assignee widget Cypress specs chore: [DHIS2-22074] de-flake the RelatedStages and assignee widget Cypress specs Sep 7, 2026
`you change the sharing settings` clicked "Share view" and typed into the
dialog's search box with nothing in between. The box is disabled while the
dialog loads the object's sharing state, and `cy.type()` does not wait for
that to clear - the CI error carries no "Timed out retrying" prefix, so it
threw immediately rather than after the command timeout. `cy.get` retried
only until the input existed, which it does while still disabled.

Gate it with a retrying `should('be.enabled')` and scope the lookup to the
dialog, the way the newer WorkingListsSharing step already does - the bare
`[placeholder="Search"]` would match any search box on the page.

This is the failure seen on `cypress (2.42, 5)` and, on another branch, on
`cypress (2.43, 5)` - same scenario, same error, different version, so a race
rather than anything version specific.

Verified against play stable-2-43-1: both sharing scenarios pass with the
gate in place.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@karolinelien karolinelien changed the title chore: [DHIS2-22074] de-flake the RelatedStages and assignee widget Cypress specs chore: [DHIS2-22074] de-flake the RelatedStages, assignee and sharing dialog scenarios Sep 7, 2026
@sonarqubecloud

sonarqubecloud Bot commented Sep 7, 2026

Copy link
Copy Markdown

@karolinelien
karolinelien marked this pull request as ready for review September 7, 2026 18:45
@karolinelien
karolinelien requested a review from a team as a code owner September 7, 2026 18:45

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Devin Review

.join('/'),
);

Cypress.Commands.add('importTracker', (importStrategy, payload) =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about introducing cy.importTracker here. We already have ~20 inline tracker-import call sites across the specs, and none use a helper today.

Two concerns:

  • Using the helper only for these 6 new sites leaves us half-migrated and creates two patterns.
  • expect(body.status).to.eq('OK') adds a new invariant that other tracker imports don’t assert, which feels separate from this de-flake PR.

I recommend inlining these 6 to match the existing style, and instead migrate all ~25 call sites in a broader readfactor later.

const { assignedUser, ...event } = body;

if (!assignedUser) {
return undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explicit assignedUser: null over field omission — makes the "clear" intent legible at the callsite.

import { When, Then } from '@badeball/cypress-cucumber-preprocessor';
import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor';

const EVENT_FIELDS = 'event,program,programStage,enrollment,orgUnit,occurredAt,scheduledAt,status,dataValues';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drop the EVENT_FIELDS allowlist and fetch the whole event body — matches the existing GET-then-UPDATE pattern (e.g. EnrollmentEditEventPageForm.js) and removes the drift risk of missing a field if the event shape gains a required one later.

)
.then(url => cy.request(url))
.then(({ body }) => {
const apiTrackedEntities = body.trackedEntities || body.instances || [];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fallback for body.instances is for pre-v40 tracker responses.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants