Merge branch 'main' into test/fleet-missed-cases

This commit is contained in:
Shrinath Rao
2026-11-04 09:31:28 +05:30
committed by GitHub
7 changed files with 62 additions and 13 deletions
+2
View File
@@ -654,6 +654,8 @@ jobs:
# 2. Nothing API-relevant changed → nothing ran → allow.
if [ "${{ needs.check_changes.outputs.has_changes }}" != "true" ]; then
echo "✅ No API-relevant changes — suite not required."
echo "::notice title=API tests skipped::No API-relevant changes — suite did not run."
echo "### ⏭️ API suite skipped — no API-relevant changes in this PR, so no tests ran." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
+2
View File
@@ -1168,6 +1168,8 @@ jobs:
# 2. Nothing Playwright-relevant changed → nothing ran → allow.
if [ "${{ needs.check_changes.outputs.has_changes }}" != "true" ]; then
echo "✅ No Playwright-relevant changes — suite not required."
echo "::notice title=Playwright tests skipped::No Playwright-relevant changes — suite did not run."
echo "### ⏭️ Playwright suite skipped — no Playwright-relevant changes in this PR, so no tests ran." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
+21 -10
View File
@@ -3447,13 +3447,7 @@ export class LogsPage {
// Create lives in the Saved views submenu, which only renders while open.
const createSavedViewBtn = this.page.locator(this.menuCreateSavedViewBtn);
if (!(await createSavedViewBtn.isVisible({ timeout: 500 }).catch(() => false))) {
const submenuTrigger = this.page.locator(this.menuListSavedViewsBtn);
if (!(await submenuTrigger.isVisible().catch(() => false))) {
await this.page.locator(this.utilitiesMenuButton).click({ force: true });
await submenuTrigger.waitFor({ state: 'visible', timeout: 5000 });
}
await submenuTrigger.click({ force: true });
await createSavedViewBtn.waitFor({ state: 'visible', timeout: 5000 });
await this.openSavedViewsSubmenu(this.menuCreateSavedViewBtn);
}
await createSavedViewBtn.click();
await this.page.locator(this.savedViewDialog).waitFor({ state: 'visible', timeout: 10000 });
@@ -9035,12 +9029,29 @@ export class LogsPage {
await listMenuItem.waitFor({ state: 'hidden', timeout: 2000 }).catch(() => {});
}
if (!opened) throw new Error('Could not open saved views menu after 3 attempts');
// force: true — ODropdown portal transiently detaches items during initial render
await listMenuItem.click({ force: true });
await this.page.locator(this.savedViewSearchInput).waitFor({ state: 'visible', timeout: 10000 });
await this.openSavedViewsSubmenu(this.savedViewSearchInput);
testLogger.info('Clicked saved views dropdown arrow');
}
/**
* Open the Saved views submenu of the utilities menu and wait for `targetSelector` inside it.
* @param {string} targetSelector - element that only renders while the submenu is open
*/
async openSavedViewsSubmenu(targetSelector) {
const trigger = this.page.locator(this.menuListSavedViewsBtn);
const target = this.page.locator(targetSelector);
for (let attempt = 0; attempt < 3; attempt++) {
if (!(await trigger.isVisible().catch(() => false))) {
await this.page.locator(this.utilitiesMenuButton).click({ force: true });
await trigger.waitFor({ state: 'visible', timeout: 5000 }).catch(() => {});
}
// No force: a click during the menu's open animation lands outside it and dismisses the whole menu.
await trigger.click({ timeout: 5000 }).catch(() => {});
if (await target.waitFor({ state: 'visible', timeout: 3000 }).then(() => true).catch(() => false)) return;
}
await target.waitFor({ state: 'visible', timeout: 5000 });
}
/**
* Expand the saved views dropdown and wait for search input
* Tries arrow click first, then main button if search input doesn't appear
+1 -1
View File
@@ -1005,7 +1005,7 @@ export interface EscalationProgress {
next_targets: string[];
next_at?: number | null;
exhausted: boolean;
/** Set when the ladder is not climbing: acknowledged, snoozed, resolved. */
/** Set when the ladder is not climbing: acknowledged, snoozed, resolved, unrouted, team deleted, or exhausted (check `exhausted`). */
stopped_because?: string | null;
/** While the first page is held for AI triage, when the hold ends (micros). */
held_until_micros?: number | null;
+31
View File
@@ -45,6 +45,7 @@ import {
isShiftRuleValid,
isSnoozed,
isDeletedTeam,
ladderStopped,
pagePhrase,
reachedStepOf,
pageState,
@@ -516,6 +517,36 @@ describe("isDeletedTeam", () => {
});
});
describe("ladderStopped", () => {
const progress = (exhausted: boolean, stopped_because: string | null) =>
({ fired: [], next_targets: [], next_at: null, exhausted, stopped_because }) as any;
it.each([
[
"an exhausted ladder",
progress(true, "the ladder is exhausted — nobody acknowledged"),
false,
false,
],
[
"a ladder stopped for another reason",
progress(false, "the owning team was deleted"),
false,
true,
],
["a climbing ladder", progress(false, null), false, false],
[
"a deleted team, even with an exhausted ladder",
progress(true, "the ladder is exhausted — nobody acknowledged"),
true,
true,
],
["a deleted team with no progress loaded", undefined, true, true],
])("%s", (_name, ladder, teamDeleted, expected) => {
expect(ladderStopped(ladder, teamDeleted)).toBe(expected);
});
});
describe("reachedStepOf", () => {
it("names the 1-based step a recorded rung delay matches, or nothing", () => {
const steps = [{ after_micros: 900 }, { after_micros: 0 }, { after_micros: 300 }];
+2 -2
View File
@@ -302,12 +302,12 @@ export function isDeletedTeam(
return !!teamId && knownTeamIds !== null && !knownTeamIds.includes(teamId);
}
/** A ringing page whose ladder stopped, or whose team was deleted, pages nobody; the list's count and phrase both ask this. [pure] */
/** A ringing page whose ladder stopped, or whose team was deleted, pages nobody; a ladder that ran out also sets `stopped_because` but is still paging. [pure] */
export function ladderStopped(
progress: EscalationProgress | null | undefined,
teamDeleted: boolean,
): boolean {
return teamDeleted || !!progress?.stopped_because;
return teamDeleted || (!!progress?.stopped_because && !progress?.exhausted);
}
/** What is happening to a page, as one phrase: the Pages list, Recent pages and the page header all say this. */
@@ -565,6 +565,9 @@ describe("OnCallResponses", () => {
expect(sections(wrapper)).toEqual({ ringing: 1 });
expect(wrapper.find('[data-test^="oncall-row-ack-"]').exists()).toBe(true);
const header = wrapper.find("[data-test='oncall-section-header-ringing']").text();
expect(header).toContain("nobody has acknowledged — these are still paging");
expect(header).not.toContain("not being paged");
});
/// A priority with no rungs configured (P4, by design) never starts a