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

commit 6c13184aa3bcf8d2d5d4ed44727d49bb0f307eeb
Author: Meng Wang <[email protected]>
AuthorDate: Sat Jul 18 16:53:16 2026 -0700

    test(frontend): add unit test coverage for 
NgbdModalRemoveProjectWorkflowComponent (#6525)
    
    ### What changes were proposed in this PR?
    
    Adds a Vitest spec for `NgbdModalRemoveProjectWorkflowComponent`
    
    
(`frontend/src/app/dashboard/component/user/user-project/user-project-section/ngbd-modal-remove-project-workflow/`),
    which had no spec (codecov ~5%). Follows `frontend/TESTING.md` (TestBed
    +
    `detectChanges()`) and mocks `UserProjectService`; the project id is
    injected via
    `NZ_MODAL_DATA`.
    
    13 tests:
    
    - **create / modal data** — renders the template; reads `projectId` from
    `NZ_MODAL_DATA`.
    - **`ngOnInit`** — loads the project's workflows via
    `retrieveWorkflowsOfProject`
      and initializes the checkboxes as unchecked.
    - **`isAllChecked`** — false with no workflows; true only when every box
    is checked.
    - **`changeAll`** — checks all when not all are checked, unchecks all
    when all are,
      and is a safe no-op on an empty list.
    - **`submitForm`** — calls `removeWorkflowFromProject(projectId, wid)`
    for each
    checked workflow (in the high→low index order the splice loop produces)
    and
    drops the removed entries from the retained `addedWorkflows` cache; does
    nothing
    when nothing is checked; and actually *subscribes* to the removal
    requests
    (a cold-observable double asserts `forkJoin(...).subscribe()` dispatches
    them).
    - **template wiring** — renders a row per workflow and connects the
    select-all
    checkbox `(change)="changeAll()"` and the Confirm button
    `(click)="submitForm()"`.
    
    No production code was changed.
    
    ### Any related issues, documentation, discussions?
    
    Closes #6515
    
    ### How was this PR tested?
    
    New Vitest spec, run locally in `frontend/` (all green). Failure paths
    were
    verified by breaking assertions; the subscription test was additionally
    confirmed
    to fail when `forkJoin(...).subscribe()` is removed from the component,
    so it
    guards real dispatch rather than just observable construction.
    
    ```
    ng test --watch=false --include 
src/app/dashboard/component/user/user-project/user-project-section/ngbd-modal-remove-project-workflow/ngbd-modal-remove-project-workflow.component.spec.ts
    # Test Files 1 passed (1) / Tests 13 passed (13)
    eslint <spec>      # clean
    prettier --check   # clean
    ```
    
    `UserProjectService` is mocked, so the spec makes no network calls.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 4.8 [1M context])
---
 ...modal-remove-project-workflow.component.spec.ts | 208 +++++++++++++++++++++
 1 file changed, 208 insertions(+)

diff --git 
a/frontend/src/app/dashboard/component/user/user-project/user-project-section/ngbd-modal-remove-project-workflow/ngbd-modal-remove-project-workflow.component.spec.ts
 
b/frontend/src/app/dashboard/component/user/user-project/user-project-section/ngbd-modal-remove-project-workflow/ngbd-modal-remove-project-workflow.component.spec.ts
new file mode 100644
index 0000000000..ff56d46ae3
--- /dev/null
+++ 
b/frontend/src/app/dashboard/component/user/user-project/user-project-section/ngbd-modal-remove-project-workflow/ngbd-modal-remove-project-workflow.component.spec.ts
@@ -0,0 +1,208 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { HttpClientTestingModule } from "@angular/common/http/testing";
+import { NZ_MODAL_DATA } from "ng-zorro-antd/modal";
+import { Observable, of } from "rxjs";
+import { NgbdModalRemoveProjectWorkflowComponent } from 
"./ngbd-modal-remove-project-workflow.component";
+import { UserProjectService } from 
"../../../../../service/user/project/user-project.service";
+import { DashboardWorkflow } from 
"../../../../../type/dashboard-workflow.interface";
+import { commonTestProviders } from 
"../../../../../../common/testing/test-utils";
+
+const PROJECT_ID = 7;
+
+function makeWorkflow(wid: number): DashboardWorkflow {
+  return {
+    isOwner: true,
+    ownerName: undefined,
+    workflow: { wid, name: `wf-${wid}`, creationTime: 0, lastModifiedTime: 0 } 
as any,
+    projectIDs: [],
+    accessLevel: "WRITE",
+    ownerId: 1,
+    coverImage: null,
+  };
+}
+
+describe("NgbdModalRemoveProjectWorkflowComponent", () => {
+  let fixture: ComponentFixture<NgbdModalRemoveProjectWorkflowComponent>;
+  let component: NgbdModalRemoveProjectWorkflowComponent;
+  let userProjectService: {
+    retrieveWorkflowsOfProject: ReturnType<typeof vi.fn>;
+    removeWorkflowFromProject: ReturnType<typeof vi.fn>;
+  };
+
+  beforeEach(async () => {
+    userProjectService = {
+      retrieveWorkflowsOfProject: vi.fn().mockReturnValue(of([])),
+      removeWorkflowFromProject: vi.fn().mockReturnValue(of({} as Response)),
+    };
+
+    await TestBed.configureTestingModule({
+      imports: [NgbdModalRemoveProjectWorkflowComponent, 
HttpClientTestingModule],
+      providers: [
+        { provide: UserProjectService, useValue: userProjectService },
+        { provide: NZ_MODAL_DATA, useValue: { projectId: PROJECT_ID } },
+        ...commonTestProviders,
+      ],
+    }).compileComponents();
+  });
+
+  function createComponent(workflows: DashboardWorkflow[] = []): void {
+    
userProjectService.retrieveWorkflowsOfProject.mockReturnValue(of(workflows));
+    fixture = TestBed.createComponent(NgbdModalRemoveProjectWorkflowComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges(); // triggers ngOnInit -> 
refreshProjectWorkflowEntries
+  }
+
+  it("should create", () => {
+    createComponent();
+    expect(component).toBeTruthy();
+  });
+
+  it("reads the project id from the modal data", () => {
+    createComponent();
+    expect(component.projectId).toBe(PROJECT_ID);
+  });
+
+  describe("ngOnInit", () => {
+    it("loads the project's workflows and initializes the checkboxes as 
unchecked", () => {
+      createComponent([makeWorkflow(10), makeWorkflow(20)]);
+      
expect(userProjectService.retrieveWorkflowsOfProject).toHaveBeenCalledWith(PROJECT_ID);
+      expect(component.addedWorkflows).toHaveLength(2);
+      expect(component.checkedWorkflows).toEqual([false, false]);
+    });
+  });
+
+  describe("isAllChecked", () => {
+    it("is false when there are no workflows", () => {
+      createComponent();
+      expect(component.isAllChecked()).toBe(false);
+    });
+
+    it("is true only when every checkbox is checked", () => {
+      createComponent([makeWorkflow(10), makeWorkflow(20)]);
+      component.checkedWorkflows = [true, true];
+      expect(component.isAllChecked()).toBe(true);
+
+      component.checkedWorkflows = [true, false];
+      expect(component.isAllChecked()).toBe(false);
+    });
+  });
+
+  describe("changeAll", () => {
+    it("checks every box when not all are checked", () => {
+      createComponent([makeWorkflow(10), makeWorkflow(20)]);
+      component.checkedWorkflows = [true, false];
+      component.changeAll();
+      expect(component.checkedWorkflows).toEqual([true, true]);
+    });
+
+    it("unchecks every box when all are checked", () => {
+      createComponent([makeWorkflow(10), makeWorkflow(20)]);
+      component.checkedWorkflows = [true, true];
+      component.changeAll();
+      expect(component.checkedWorkflows).toEqual([false, false]);
+    });
+
+    it("is a safe no-op on an empty list", () => {
+      createComponent();
+      component.changeAll();
+      expect(component.checkedWorkflows).toEqual([]);
+    });
+  });
+
+  describe("submitForm", () => {
+    it("removes only the checked workflows and drops them from the retained 
list", () => {
+      createComponent([makeWorkflow(10), makeWorkflow(20), makeWorkflow(30)]);
+      component.checkedWorkflows = [false, true, true];
+
+      component.submitForm();
+
+      
expect(userProjectService.removeWorkflowFromProject).toHaveBeenCalledTimes(2);
+      // Iterates high index -> low, so the calls land in wid order 30 then 20.
+      
expect(userProjectService.removeWorkflowFromProject).toHaveBeenNthCalledWith(1, 
PROJECT_ID, 30);
+      
expect(userProjectService.removeWorkflowFromProject).toHaveBeenNthCalledWith(2, 
PROJECT_ID, 20);
+      expect(component.addedWorkflows.map(w => w.workflow.wid)).toEqual([10]);
+    });
+
+    it("removes every workflow when all are checked", () => {
+      createComponent([makeWorkflow(10), makeWorkflow(20)]);
+      component.checkedWorkflows = [true, true];
+
+      component.submitForm();
+
+      
expect(userProjectService.removeWorkflowFromProject).toHaveBeenCalledTimes(2);
+      
expect(userProjectService.removeWorkflowFromProject).toHaveBeenNthCalledWith(1, 
PROJECT_ID, 20);
+      
expect(userProjectService.removeWorkflowFromProject).toHaveBeenNthCalledWith(2, 
PROJECT_ID, 10);
+      expect(component.addedWorkflows).toEqual([]);
+    });
+
+    it("does nothing when no workflow is checked", () => {
+      createComponent([makeWorkflow(10), makeWorkflow(20)]);
+      component.checkedWorkflows = [false, false];
+
+      component.submitForm();
+
+      
expect(userProjectService.removeWorkflowFromProject).not.toHaveBeenCalled();
+      expect(component.addedWorkflows.map(w => w.workflow.wid)).toEqual([10, 
20]);
+    });
+
+    it("subscribes to the removal requests so they are actually dispatched", 
() => {
+      // Real removeWorkflowFromProject returns a cold HttpClient observable 
that only
+      // fires the request on subscription; a cold double here proves 
submitForm's
+      // forkJoin(...).subscribe() actually subscribes (not just builds the 
observables).
+      createComponent([makeWorkflow(10), makeWorkflow(20)]);
+      let subscriptions = 0;
+      userProjectService.removeWorkflowFromProject.mockReturnValue(
+        new Observable<Response>(observer => {
+          subscriptions++;
+          observer.next({} as Response);
+          observer.complete();
+        })
+      );
+      component.checkedWorkflows = [true, true];
+
+      component.submitForm();
+
+      expect(subscriptions).toBe(2);
+    });
+  });
+
+  describe("template wiring", () => {
+    it("renders a row per workflow and connects the select-all and Confirm 
controls", () => {
+      createComponent([makeWorkflow(10), makeWorkflow(20)]);
+
+      const rows = fixture.nativeElement.querySelectorAll("tbody tr");
+      expect(rows.length).toBe(2);
+      expect(rows[0].textContent).toContain("10");
+      expect(rows[0].textContent).toContain("wf-10");
+
+      // Header checkbox is bound to (change)="changeAll()".
+      const selectAll = fixture.nativeElement.querySelector('thead 
input[type="checkbox"]') as HTMLInputElement;
+      selectAll.click();
+      fixture.detectChanges();
+      expect(component.checkedWorkflows).toEqual([true, true]);
+
+      // Confirm button is bound to (click)="submitForm()".
+      (fixture.nativeElement.querySelector('button[aria-label="Confirm"]') as 
HTMLButtonElement).click();
+      
expect(userProjectService.removeWorkflowFromProject).toHaveBeenCalledTimes(2);
+    });
+  });
+});

Reply via email to