Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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".
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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();
});
});
});
Loading