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 97dac3c2db test(frontend): render the project list item's permission
and description rules (#7415)
97dac3c2db is described below
commit 97dac3c2db0055a5bb98a23d6741ace3fc106742
Author: Xinyuan Lin <[email protected]>
AuthorDate: Sat Aug 8 00:24:58 2026 -0700
test(frontend): render the project list item's permission and description
rules (#7415)
### What changes were proposed in this PR?
`UserProjectListItemComponent` decides in its template what a viewer may
touch, and none of it was rendered — the existing specs call the save
and colour methods directly.
Adds 8 tests. The one that matters most is the `editable` gating: a
project the viewer only holds READ on must not be offered the rename,
add-description, share or delete controls, and that decision lives
entirely in two `*ngIf="editable"` guards plus one on the action list.
Also covered: the name/edit-input swap, the description starting
collapsed and expanding on request, the `trim()` guard that stops a
whitespace-only description rendering an empty expander, the character
counter, the save icon appearing only once the text actually differs,
and the creation-date format.
**Verified by mutation**, all reverted (template diff empty):
| Mutation | Result |
|---|---|
| show the rename button to a read-only viewer | red |
| show the share/delete actions to a read-only viewer | red |
| invert the name / edit-input branch | red |
| drop the collapse guard | red |
| drop the whitespace `trim()` guard | red |
| always show the save icon | red |
| count characters against the max instead of the text | red |
| change the creation-date format | red |
Two things worth recording:
- `MarkdownModule.forRoot()` joins the TestBed. An expanded description
renders a `<markdown>` element, and no existing test reached that path,
so `MarkdownService` had never been needed.
- `descriptionCollapsed` defaults to **true**. My first version of the
collapse test asserted the opposite and failed, which also revealed that
the whitespace test would have passed vacuously — collapsed hides the
block regardless. It now expands first, so the `trim()` guard is the
only thing left doing the work.
No production file is touched.
### Any related issues, documentation, discussions?
Closes #7412
### How was this PR tested?
```
npx ng test --watch=false
--include="**/user-project-list-item.component.spec.ts"
```
```
Test Files 1 passed (1)
Tests 18 passed (18)
```
8 new on top of the existing 10. `yarn format:ci` passes.
### Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 5)
---
.../user-project-list-item.component.spec.ts | 116 ++++++++++++++++++++-
1 file changed, 115 insertions(+), 1 deletion(-)
diff --git
a/frontend/src/app/dashboard/component/user/user-project/user-project-list-item/user-project-list-item.component.spec.ts
b/frontend/src/app/dashboard/component/user/user-project/user-project-list-item/user-project-list-item.component.spec.ts
index 2263f8553d..f2198854b1 100644
---
a/frontend/src/app/dashboard/component/user/user-project/user-project-list-item/user-project-list-item.component.spec.ts
+++
b/frontend/src/app/dashboard/component/user/user-project/user-project-list-item/user-project-list-item.component.spec.ts
@@ -31,7 +31,9 @@ import { StubUserService } from
"../../../../../common/service/user/stub-user.se
import { UserService } from "../../../../../common/service/user/user.service";
import { commonTestProviders } from "../../../../../common/testing/test-utils";
import { ShareAccessComponent } from
"../../share-access/share-access.component";
+import { DatePipe } from "@angular/common";
import { of } from "rxjs";
+import { MarkdownModule } from "ngx-markdown";
// UserProjectListItemComponent is rooted at <nz-list-item>; instantiating it
// outside an <nz-list> host throws "No provider found for NzListComponent".
@@ -71,7 +73,8 @@ describe("UserProjectListItemComponent", () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
- imports: [TestHostComponent, HttpClientTestingModule],
+ // MarkdownModule.forRoot() backs the <markdown> element in an expanded
description.
+ imports: [TestHostComponent, HttpClientTestingModule,
MarkdownModule.forRoot()],
providers: [
NotificationService,
UserProjectService,
@@ -218,4 +221,115 @@ describe("UserProjectListItemComponent", () => {
expect(refreshSpy).toHaveBeenCalled();
});
});
+ /**
+ * The list item decides in its template what a viewer is allowed to touch
and how much of a long
+ * description to show. The specs above call the save/colour methods
directly, so none of the
+ * rendered gating had been pinned.
+ */
+ describe("rendered item", () => {
+ /** Re-renders the host with the given entry/editable combination. */
+ function render(over: Partial<DashboardProject> = {}, editable = true):
HTMLElement {
+ hostFixture.componentInstance.entry = { ...testProject, ...over };
+ hostFixture.componentInstance.editable = editable;
+ hostFixture.detectChanges();
+ return hostFixture.nativeElement as HTMLElement;
+ }
+
+ it("shows the project name and its creation date", () => {
+ const el = render({ name: "quarterly", creationTime: januaryFirst1970 });
+
+ expect(el.textContent).toContain("quarterly");
+ // Expected value is formatted here with the same pipe and format
string, so the assertion
+ // pins both the yyyy-MM-dd HH:mm format and the timestamp it was given,
without hard-coding
+ // a literal that would only hold in one timezone.
+ const expected = new DatePipe("en-US").transform(januaryFirst1970,
"yyyy-MM-dd HH:mm");
+ expect(el.querySelector("nz-list-item-meta-description
p")?.textContent?.trim()).toBe(`Created: ${expected}`);
+ });
+
+ it("hides every editing control from a read-only viewer", () => {
+ // accessLevel READ reaches this component as editable=false; if the
template ignored it the
+ // viewer would be shown share and delete buttons for a project they
cannot change.
+ const el = render({}, false);
+
+ expect(el.querySelector(".edit-name-icon")).toBeNull();
+ expect(el.querySelector(".edit-description-icon")).toBeNull();
+ expect(el.querySelector("ul[nz-list-item-actions]")).toBeNull();
+ });
+
+ it("offers the editing controls to a viewer with write access", () => {
+ const el = render({}, true);
+
+ expect(el.querySelector(".edit-name-icon")).not.toBeNull();
+ expect(el.querySelector(".edit-description-icon")).not.toBeNull();
+ // Share and delete both live in that list; count the buttons rather
than just the container
+ // (nz-list-item-action renders as an <li>, so the element selector
finds nothing).
+ expect(el.querySelectorAll("ul[nz-list-item-actions]
button").length).toBe(2);
+ });
+
+ it("swaps the name for an input once the name is being edited", () => {
+ render();
+ expect(hostFixture.nativeElement.querySelector("nz-list-item-meta-title
input")).toBeNull();
+
+ component.editingName = true;
+ hostFixture.detectChanges();
+
+ expect(hostFixture.nativeElement.querySelector("nz-list-item-meta-title
input")).not.toBeNull();
+ });
+
+ it("starts with the description collapsed and expands it on request", ()
=> {
+ // descriptionCollapsed defaults to true, so a list of projects stays
compact until the user
+ // opens one.
+ const el = render({ description: "a long description" });
+ expect(el.querySelector(".description-container")).toBeNull();
+
+ component.descriptionCollapsed = false;
+ hostFixture.detectChanges();
+
+
expect(hostFixture.nativeElement.querySelector(".description-container")).not.toBeNull();
+ });
+
+ it("shows no description block when the description is only whitespace",
() => {
+ // Expanded, so the trim() guard is the only thing left to hide it: a
whitespace-only
+ // description would otherwise render an empty expander with nothing in
it.
+ render({ description: " " });
+ component.descriptionCollapsed = false;
+ hostFixture.detectChanges();
+
+
expect(hostFixture.nativeElement.querySelector(".description-container")).toBeNull();
+ });
+
+ it("counts the characters typed into the description editor", () => {
+ render({ description: "abc" });
+ component.editingDescription = true;
+ hostFixture.detectChanges();
+
+ const count =
hostFixture.nativeElement.querySelector(".character-count")!;
+
expect(count.textContent?.trim()).toBe(`3/${component.MAX_PROJECT_DESCRIPTION_CHAR_COUNT}`);
+
+ // It must follow what is in the box, not the value the description
started at.
+ const textarea = hostFixture.nativeElement.querySelector("textarea")!;
+ textarea.value = "abcdef";
+ textarea.dispatchEvent(new Event("input"));
+ hostFixture.detectChanges();
+
+
expect(hostFixture.nativeElement.querySelector(".character-count")!.textContent?.trim()).toBe(
+ `6/${component.MAX_PROJECT_DESCRIPTION_CHAR_COUNT}`
+ );
+ });
+
+ it("offers the save button only once the description has actually
changed", () => {
+ render({ description: "abc" });
+ component.editingDescription = true;
+ hostFixture.detectChanges();
+ const textarea = hostFixture.nativeElement.querySelector("textarea")!;
+
+
expect(hostFixture.nativeElement.querySelector(".ant-input-clear-icon")).toBeNull();
+
+ textarea.value = "abcd";
+ textarea.dispatchEvent(new Event("input"));
+ hostFixture.detectChanges();
+
+
expect(hostFixture.nativeElement.querySelector(".ant-input-clear-icon")).not.toBeNull();
+ });
+ });
});