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");
+ });
+ });
+ });
});