diff --git a/packages/main/cypress/specs/BusyIndicator.cy.tsx b/packages/main/cypress/specs/BusyIndicator.cy.tsx index 2d0d9c4f862f..a2f55748bdc1 100644 --- a/packages/main/cypress/specs/BusyIndicator.cy.tsx +++ b/packages/main/cypress/specs/BusyIndicator.cy.tsx @@ -1,6 +1,7 @@ import BusyIndicator from "../../src/BusyIndicator.js"; import Button from "../../src/Button.js"; import Dialog from "../../src/Dialog.js"; +import BusyIndicatorSize from "../../src/types/BusyIndicatorSize.js"; describe("Rendering", () => { it("Rendering without content", () => { @@ -27,6 +28,52 @@ describe("Rendering", () => { }); }); +describe("Text Placement and Display", () => { + it("should render text with Top placement", () => { + cy.mount( + +
Content
+
+ ); + + cy.get("[ui5-busy-indicator]") + .shadow() + .find(".ui5-busy-indicator-text") + .should("exist") + .and("contain.text", "Loading..."); + + // Verify the text appears before the circles (top placement) + cy.get("[ui5-busy-indicator]") + .shadow() + .find(".ui5-busy-indicator-busy-area") + .children() + .first() + .should("have.class", "ui5-busy-indicator-text"); + }); + + it("should render text with Bottom placement (default)", () => { + cy.mount( + +
Content
+
+ ); + + cy.get("[ui5-busy-indicator]") + .shadow() + .find(".ui5-busy-indicator-text") + .should("exist") + .and("contain.text", "Loading..."); + + // Verify the text appears after the circles (bottom placement) + cy.get("[ui5-busy-indicator]") + .shadow() + .find(".ui5-busy-indicator-busy-area") + .children() + .last() + .should("have.class", "ui5-busy-indicator-text"); + }); +}); + describe("BusyIndicator general interaction", () => { it("tests event propagation", () => { const onClickStub = cy.stub().as("clickStub"); @@ -225,4 +272,394 @@ describe("BusyIndicator general interaction", () => { .find(".ui5-busy-indicator-root") .should("have.css", "height", "144px"); }); -}); \ No newline at end of file +}); + +describe("Delay and Timeout Behavior", () => { + it("should clear timeout when component becomes inactive", () => { + cy.mount( + +
Content
+
+ ); + + cy.get("[ui5-busy-indicator]").invoke("attr", "active", ""); + + cy.get("[ui5-busy-indicator]") + .shadow() + .find(".ui5-busy-indicator-busy-area") + .should("not.exist"); + + cy.get("[ui5-busy-indicator]").should(($el) => { + const timeoutId = $el[0]._busyTimeoutId; + expect(timeoutId).to.exist; + }).then(($el) => { + cy.wrap($el[0]._busyTimeoutId).as("timeoutId"); + }); + + cy.spy(window, "clearTimeout"); + cy.get("[ui5-busy-indicator]").invoke("removeAttr", "active"); + + cy.get("[ui5-busy-indicator]") + .shadow() + .find(".ui5-busy-indicator-busy-area") + .should("not.exist"); + + cy.get("@timeoutId") + .should((timeoutId) => { + expect(clearTimeout).to.have.been.calledWith(timeoutId); + }); + + cy.get("[ui5-busy-indicator]") + .invoke("prop", "_isBusy") + .should("eq", false); + }); + + it("should clear pending timeout and trigger cleanup when deactivated before becoming busy", () => { + cy.mount( + +
Content
+
+ ); + + cy.get("[ui5-busy-indicator]").invoke("attr", "active", ""); + + cy.get("[ui5-busy-indicator]") + .shadow() + .find(".ui5-busy-indicator-busy-area") + .should("not.exist"); + + cy.get("[ui5-busy-indicator]").should(($el) => { + const timeoutId = $el[0]._busyTimeoutId; + expect(timeoutId).to.exist; + }).then(($el) => { + cy.wrap($el[0]._busyTimeoutId).as("timeoutId"); + }); + + const clearTimeoutSpy = cy.spy(window, "clearTimeout"); + + cy.get("[ui5-busy-indicator]").invoke("removeAttr", "active"); + + cy.get("@timeoutId").should((timeoutId) => { + expect(clearTimeoutSpy).to.have.been.calledWith(timeoutId); + }); + + cy.get("[ui5-busy-indicator]") + .shadow() + .find(".ui5-busy-indicator-busy-area") + .should("not.exist"); + + cy.get("[ui5-busy-indicator]") + .invoke("prop", "_isBusy") + .should("eq", false); + + cy.get("[ui5-busy-indicator]").then(($el) => { + const element = $el[0] as any; + + element.active = true; + element.onBeforeRendering(); + + expect(element._isBusy).to.be.false; + expect(element._busyTimeoutId).to.exist; + + element.active = false; + element.onBeforeRendering(); + + expect(element._busyTimeoutId).to.be.undefined; + expect(element._isBusy).to.be.false; + }); + }); + + it("should handle zero delay", () => { + cy.mount( + +
Content
+
+ ); + + // Should become busy immediately + cy.get("[ui5-busy-indicator]") + .shadow() + .find(".ui5-busy-indicator-busy-area") + .should("exist"); + }); +}); + +describe("Different Sizes", () => { + Object.values(BusyIndicatorSize).forEach(size => { + it(`should render with size ${size}`, () => { + cy.mount( + +
Content
+
+ ); + + cy.get("[ui5-busy-indicator]") + .shadow() + .find(".ui5-busy-indicator-busy-area") + .should("exist"); + }); + }); +}); + +describe("Desktop-specific Behavior", () => { + it("should set desktop attribute on desktop devices", () => { + // This test depends on the isDesktop() function + cy.mount( + +
Content
+
+ ); + + // On most test environments, this should be true + cy.get("[ui5-busy-indicator]") + .should("have.attr", "desktop"); + }); +}); + +describe("Complex Keyboard Navigation", () => { + it("should prevent events when busy and not when inactive", () => { + const onClickStub = cy.stub().as("clickStub"); + + cy.mount( +
+ + + + + +
+ ); + + // Test when not busy - should allow interaction + cy.get("#innerBtn").realClick(); + cy.get("@clickStub").should("have.been.called"); + + // Reset stub + cy.get("@clickStub").then((stub) => { + stub.resetHistory(); + }); + + // Activate busy indicator + cy.get("[ui5-busy-indicator]").invoke("attr", "active", ""); + + // Since delay is 0, it should become busy immediately + cy.get("[ui5-busy-indicator]") + .shadow() + .find(".ui5-busy-indicator-busy-area") + .should("exist"); + + // Try to interact with inner button - should be prevented + cy.get("#innerBtn").realClick(); + cy.get("@clickStub").should("not.have.been.called"); + }); + + it("should handle Shift+Tab focus redirection", () => { + cy.mount( +
+ + + + + +
+ ); + + // Focus the after button using realClick instead of focus() + cy.get("#afterBtn").realClick(); + + // Use Shift+Tab to go backwards + cy.realPress(["Shift", "Tab"]); + + // Should focus the busy indicator's busy area + cy.get("[ui5-busy-indicator]") + .shadow() + .find(".ui5-busy-indicator-busy-area") + .should("have.focus"); + }); + + it("should handle key events other than Tab", () => { + cy.mount( + + + + ); + + // Focus the busy area + cy.get("[ui5-busy-indicator]") + .shadow() + .find(".ui5-busy-indicator-busy-area") + .focus(); + + // Try pressing other keys - should be prevented but not cause errors + cy.realPress("Enter"); + cy.realPress("Space"); + cy.realPress("Escape"); + cy.realPress("ArrowDown"); + + // Busy indicator should still be focused and working + cy.get("[ui5-busy-indicator]") + .shadow() + .find(".ui5-busy-indicator-busy-area") + .should("have.focus"); + }); +}); + +describe("Component Lifecycle", () => { + it("should properly clean up on DOM exit", () => { + // Spy on clearTimeout to verify it's called during onExitDOM + cy.window().then((win) => { + cy.spy(win, 'clearTimeout').as('clearTimeoutSpy'); + }); + + cy.mount( +
+ +
Content
+
+
+ ); + + // Verify the component has a pending timeout + cy.get("[ui5-busy-indicator]").then(($el) => { + const element = $el[0] as any; + expect(element._busyTimeoutId).to.exist; + }); + + // Remove the component before timeout fires - this triggers onExitDOM + cy.get("#container").then(($container) => { + $container.empty(); + }); + + // Verify clearTimeout was called (onExitDOM logic) + cy.get('@clearTimeoutSpy').should('have.been.called'); + }); +}); + +describe("Edge Cases", () => { + it("should handle rapid active/inactive toggles", () => { + cy.mount( + +
Content
+
+ ); + + // Rapidly toggle active state + for (let i = 0; i < 5; i++) { + cy.get("[ui5-busy-indicator]").invoke("attr", "active", ""); + cy.get("[ui5-busy-indicator]").invoke("removeAttr", "active"); + } + + // Final state should be inactive + cy.get("[ui5-busy-indicator]") + .shadow() + .find(".ui5-busy-indicator-busy-area") + .should("not.exist"); + }); + + it("should handle text changes dynamically", () => { + cy.mount( + +
Content
+
+ ); + + // Initial text + cy.get("[ui5-busy-indicator]") + .shadow() + .find(".ui5-busy-indicator-text") + .should("contain.text", "Initial"); + + // Change text + cy.get("[ui5-busy-indicator]").invoke("attr", "text", "Updated"); + + cy.get("[ui5-busy-indicator]") + .shadow() + .find(".ui5-busy-indicator-text") + .should("contain.text", "Updated"); + + // Remove text + cy.get("[ui5-busy-indicator]").invoke("removeAttr", "text"); + + cy.get("[ui5-busy-indicator]") + .shadow() + .find(".ui5-busy-indicator-text") + .should("not.exist"); + }); +}); + +describe("Accessibility", () => { + it("should have proper ARIA attributes", () => { + cy.mount( + +
Content
+
+ ); + + cy.get("[ui5-busy-indicator]") + .shadow() + .find(".ui5-busy-indicator-busy-area") + .should("have.attr", "role", "progressbar") + .and("have.attr", "aria-valuemin", "0") + .and("have.attr", "aria-valuemax", "100") + .and("have.attr", "aria-valuetext", "Busy") + .and("have.attr", "tabindex", "0") + .and("have.attr", "data-sap-focus-ref"); + }); + + it("should have proper title attribute", () => { + cy.mount( + +
Content
+
+ ); + + cy.get("[ui5-busy-indicator]") + .shadow() + .find(".ui5-busy-indicator-busy-area") + .should("have.attr", "title") + .and("not.be.empty"); + }); + + it("should generate correct labelId when text is present", () => { + cy.mount( + +
Content
+
+ ); + + cy.get("[ui5-busy-indicator]") + .shadow() + .find(".ui5-busy-indicator-busy-area") + .should("have.attr", "aria-labelledby"); + + cy.get("[ui5-busy-indicator]") + .shadow() + .find(".ui5-busy-indicator-text") + .should("have.attr", "id"); + + // Verify they match each other + cy.get("[ui5-busy-indicator]") + .shadow() + .find(".ui5-busy-indicator-busy-area") + .invoke("attr", "aria-labelledby") + .then((labelledBy) => { + cy.get("[ui5-busy-indicator]") + .shadow() + .find(".ui5-busy-indicator-text") + .should("have.attr", "id", labelledBy); + }); + }); + + it("should not have labelId when no text is present", () => { + cy.mount( + +
Content
+
+ ); + + cy.get("[ui5-busy-indicator]") + .shadow() + .find(".ui5-busy-indicator-busy-area") + .should("not.have.attr", "aria-labelledby"); + }); +});