This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git


The following commit(s) were added to refs/heads/main by this push:
     new f236751dd6 test(frontend): render the login form's sign-up mode and 
provider flags (#7967)
f236751dd6 is described below

commit f236751dd601159bb14567ef4fe2509803cda809
Author: Meng Wang <[email protected]>
AuthorDate: Tue Aug 25 22:26:14 2026 +0000

    test(frontend): render the login form's sign-up mode and provider flags 
(#7967)
    
    ### What changes were proposed in this PR?
    
    Renders the login card, which the existing 23 `it()` blocks almost never
    did — they
    called `detectChanges()` twice. 13 new tests. Measured locally with
    `--coverage --coverage-reporters=lcovonly`:
    
    | `texera-login.component.html` | Before | After |
    | --- | --- | --- |
    | lines | 44/57 (77.19 %) | **57/57 (100 %)** |
    | branches | 11/24 | **24/24** |
    | handlers | 0/4 | **4/4** |
    
    - **Provider flags** — all four combinations of `localLogin` ×
    `googleLogin`, since
    `localLogin` is read twice (the tab strip and the form) and the "or
    continue
    with" divider is gated on both. The all-off case also asserts the brand
    and
    footer still render, so the card is never blank. Each test sets both
    flags
      explicitly rather than leaning on the mock's defaults.
    - **Sign-in / sign-up mode** — the tab strip drives `mode` in both
    directions via
    `(nzSelectedIndexChange)`, and `[nzSelectedIndex]` follows `mode` back;
    the two
    sign-up-only input groups (`email`, `confirm`), the password-policy hint
    and the
      submit label are asserted present in sign-up and absent in sign-in.
    - **Password visibility** — the suffix template is shared by both
    password groups,
    so a single toggle has to flip both inputs and both icons. Covered by
    click and
      by `keydown.enter`, plus toggling back.
    - **Form submission** — through the form's `(ngSubmit)` binding rather
    than by
      calling `submit()`.
    
    The icon assertion reads the icon name off `NzIconDirective`'s own
    field: the
    directive declares `nzType` as a setter with no getter, so reading the
    input back
    yields `undefined`.
    
    No production code was changed.
    
    ### Any related issues, documentation, discussions?
    
    Closes #7962.
    
    ### How was this PR tested?
    
    `ng test --watch=false --include
    src/app/hub/component/login/texera-login.component.spec.ts`
    — 36 passed (23 before, 13 new), repeated 3× for stability. `yarn
    format:ci` clean.
    Failure path verified by breaking one assertion in each of the 13 new
    tests: 13
    failed / 23 passed, non-zero exit, then restored to green.
    
    Running the whole `hub/**` folder surfaces one failure in
    `hub-search-result.component.spec.ts` (`setViewMode … persists to
    localStorage`),
    which reproduces on an unmodified checkout of `main` and is unrelated to
    this
    change.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 5)
---
 .../component/login/texera-login.component.spec.ts | 172 +++++++++++++++++++++
 1 file changed, 172 insertions(+)

diff --git 
a/frontend/src/app/hub/component/login/texera-login.component.spec.ts 
b/frontend/src/app/hub/component/login/texera-login.component.spec.ts
index c17b595c70..8e2c172708 100644
--- a/frontend/src/app/hub/component/login/texera-login.component.spec.ts
+++ b/frontend/src/app/hub/component/login/texera-login.component.spec.ts
@@ -31,6 +31,9 @@ import { GuiConfigService } from 
"../../../common/service/gui-config.service";
 import { MockGuiConfigService } from 
"../../../common/service/gui-config.service.mock";
 import { commonTestProviders } from "../../../common/testing/test-utils";
 import { USER_WORKFLOW } from "../../../app-routing.constant";
+import { By } from "@angular/platform-browser";
+import { NzIconDirective } from "ng-zorro-antd/icon";
+import { NzTabsComponent } from "ng-zorro-antd/tabs";
 
 describe("TexeraLoginComponent", () => {
   let component: TexeraLoginComponent;
@@ -306,4 +309,173 @@ describe("TexeraLoginComponent", () => {
       expect(routerMock.navigateByUrl).not.toHaveBeenCalled();
     });
   });
+
+  // ──────────────────────────────────────────────────────────────────────────
+  // Template rendering
+  //
+  // The suite above drives the class; these render the card. Each test sets 
both
+  // provider flags explicitly so nothing is inherited from the mock's 
defaults.
+  // ──────────────────────────────────────────────────────────────────────────
+  describe("template", () => {
+    function render(flags: { localLogin: boolean; googleLogin: boolean }): 
void {
+      (TestBed.inject(GuiConfigService) as unknown as 
MockGuiConfigService).setConfig(flags);
+      fixture.detectChanges();
+    }
+
+    const host = (): HTMLElement => fixture.nativeElement as HTMLElement;
+    const input = (name: string): HTMLInputElement | null =>
+      
host().querySelector<HTMLInputElement>(`input[formcontrolname="${name}"]`);
+    const submitButton = (): HTMLElement | null => host().querySelector("form 
button[type='submit']");
+    const passwordIcons = () => 
fixture.debugElement.queryAll(By.css("nz-icon.ant-input-password-icon"));
+    // NzIconDirective declares nzType as a setter with no getter, so read the 
icon name
+    // off the base directive's own field rather than the input.
+    const iconTypes = (): string[] =>
+      passwordIcons().map(icon => (icon.injector.get(NzIconDirective) as 
unknown as { type: string }).type);
+
+    describe("provider flags", () => {
+      it("renders the local form and the google button when both are enabled", 
() => {
+        render({ localLogin: true, googleLogin: true });
+
+        expect(host().querySelector("nz-tabs")).toBeTruthy();
+        expect(host().querySelector("form")).toBeTruthy();
+        expect(host().querySelector("asl-google-signin-button")).toBeTruthy();
+        // The "or continue with" divider only makes sense when both are 
offered.
+        expect(host().querySelector("nz-divider")).toBeTruthy();
+      });
+
+      it("drops the google button but keeps the form when only local login is 
enabled", () => {
+        render({ localLogin: true, googleLogin: false });
+
+        expect(host().querySelector("nz-tabs")).toBeTruthy();
+        expect(host().querySelector("form")).toBeTruthy();
+        expect(host().querySelector("asl-google-signin-button")).toBeNull();
+        expect(host().querySelector("nz-divider")).toBeNull();
+      });
+
+      it("drops the tabs and the form but keeps the google button when only 
google is enabled", () => {
+        render({ localLogin: false, googleLogin: true });
+
+        expect(host().querySelector("nz-tabs")).toBeNull();
+        expect(host().querySelector("form")).toBeNull();
+        expect(host().querySelector("asl-google-signin-button")).toBeTruthy();
+        expect(host().querySelector("nz-divider")).toBeNull();
+      });
+
+      it("renders neither sign-in path when both are disabled", () => {
+        render({ localLogin: false, googleLogin: false });
+
+        expect(host().querySelector("nz-tabs")).toBeNull();
+        expect(host().querySelector("form")).toBeNull();
+        expect(host().querySelector("asl-google-signin-button")).toBeNull();
+        expect(host().querySelector("nz-divider")).toBeNull();
+        // The brand and footer are outside every flag, so the card is never 
empty.
+        expect(host().querySelector(".brand")).toBeTruthy();
+        expect(host().querySelector("p.foot")).toBeTruthy();
+      });
+    });
+
+    describe("sign-in / sign-up mode", () => {
+      beforeEach(() => render({ localLogin: true, googleLogin: true }));
+
+      it("shows only the sign-in fields by default", () => {
+        expect(component.mode).toBe("signin");
+        expect(input("username")).toBeTruthy();
+        expect(input("password")).toBeTruthy();
+        expect(input("email")).toBeNull();
+        expect(input("confirm")).toBeNull();
+        expect(host().querySelector("p.hint")).toBeNull();
+        expect(submitButton()?.textContent?.trim()).toBe("Sign in");
+      });
+
+      it("adds the email, confirm and password-policy hint in sign-up mode", 
() => {
+        component.setMode("signup");
+        fixture.detectChanges();
+
+        expect(input("email")).toBeTruthy();
+        expect(input("confirm")).toBeTruthy();
+        expect(host().querySelector("p.hint")?.textContent?.replace(/\s+/g, " 
").trim()).toBe(
+          "Password must be at least 6 characters. After registering, contact 
the Texera administrator to activate your account."
+        );
+        expect(submitButton()?.textContent?.trim()).toBe("Sign up");
+      });
+
+      it("switches mode from the tab strip in both directions", () => {
+        const tabs = fixture.debugElement.query(By.css("nz-tabs"));
+
+        tabs.triggerEventHandler("nzSelectedIndexChange", 1);
+        fixture.detectChanges();
+        expect(component.mode).toBe("signup");
+        expect(input("confirm")).toBeTruthy();
+
+        tabs.triggerEventHandler("nzSelectedIndexChange", 0);
+        fixture.detectChanges();
+        expect(component.mode).toBe("signin");
+        expect(input("confirm")).toBeNull();
+      });
+
+      it("binds the selected tab to the current mode", () => {
+        const selectedIndex = () =>
+          
fixture.debugElement.query(By.directive(NzTabsComponent)).componentInstance.nzSelectedIndex;
+
+        expect(selectedIndex()).toBe(0);
+
+        component.setMode("signup");
+        fixture.detectChanges();
+        expect(selectedIndex()).toBe(1);
+      });
+
+      it("submits the form through its ngSubmit binding", () => {
+        const submitSpy = vi.spyOn(component, "submit").mockImplementation(() 
=> {});
+
+        
fixture.debugElement.query(By.css("form")).triggerEventHandler("ngSubmit", new 
Event("submit"));
+
+        expect(submitSpy).toHaveBeenCalledTimes(1);
+      });
+    });
+
+    describe("password visibility", () => {
+      beforeEach(() => {
+        render({ localLogin: true, googleLogin: true });
+        component.setMode("signup");
+        fixture.detectChanges();
+      });
+
+      it("starts hidden on both password fields", () => {
+        expect(input("password")?.type).toBe("password");
+        expect(input("confirm")?.type).toBe("password");
+        // The suffix template is reused by both groups, so both icons render.
+        expect(passwordIcons().length).toBe(2);
+        expect(iconTypes()).toEqual(["eye-invisible", "eye-invisible"]);
+      });
+
+      it("reveals both password fields when the toggle is clicked", () => {
+        passwordIcons()[0].triggerEventHandler("click", new 
MouseEvent("click"));
+        fixture.detectChanges();
+
+        expect(component.passwordVisible).toBe(true);
+        expect(input("password")?.type).toBe("text");
+        expect(input("confirm")?.type).toBe("text");
+        expect(iconTypes()).toEqual(["eye", "eye"]);
+      });
+
+      it("also toggles from the keyboard, so the control is reachable without 
a mouse", () => {
+        passwordIcons()[0].triggerEventHandler("keydown.enter", new 
KeyboardEvent("keydown", { key: "Enter" }));
+        fixture.detectChanges();
+
+        expect(component.passwordVisible).toBe(true);
+        expect(input("password")?.type).toBe("text");
+      });
+
+      it("hides them again on a second click", () => {
+        passwordIcons()[0].triggerEventHandler("click", new 
MouseEvent("click"));
+        fixture.detectChanges();
+        passwordIcons()[0].triggerEventHandler("click", new 
MouseEvent("click"));
+        fixture.detectChanges();
+
+        expect(component.passwordVisible).toBe(false);
+        expect(input("password")?.type).toBe("password");
+        expect(input("confirm")?.type).toBe("password");
+      });
+    });
+  });
 });

Reply via email to