Merge feature/41-widget-hud-system (#41) into develop

Implements Widget/HUD system:
- BaseWidget, WidgetRegistry, WidgetGrid
- TasksWidget, CalendarWidget, QuickCaptureWidget
- Layout persistence with useLayouts hooks
- Comprehensive test suite
This commit is contained in:
Jason Woltje
2026-01-29 17:54:50 -06:00
14 changed files with 2142 additions and 0 deletions

View File

@@ -0,0 +1,276 @@
/**
* LayoutsService Unit Tests
* Following TDD principles
*/
import { Test, TestingModule } from "@nestjs/testing";
import { NotFoundException } from "@nestjs/common";
import { LayoutsService } from "../layouts.service";
import { PrismaService } from "../../prisma/prisma.service";
describe("LayoutsService", () => {
let service: LayoutsService;
let prisma: jest.Mocked<PrismaService>;
const mockWorkspaceId = "workspace-123";
const mockUserId = "user-123";
const mockLayout = {
id: "layout-1",
workspaceId: mockWorkspaceId,
userId: mockUserId,
name: "Default Layout",
isDefault: true,
layout: [
{ i: "tasks-1", x: 0, y: 0, w: 2, h: 2 },
{ i: "calendar-1", x: 2, y: 0, w: 2, h: 2 },
],
metadata: {},
createdAt: new Date(),
updatedAt: new Date(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
LayoutsService,
{
provide: PrismaService,
useValue: {
userLayout: {
findMany: jest.fn(),
findFirst: jest.fn(),
findUnique: jest.fn(),
create: jest.fn(),
update: jest.fn(),
updateMany: jest.fn(),
delete: jest.fn(),
},
$transaction: jest.fn((callback) => callback(prisma)),
},
},
],
}).compile();
service = module.get<LayoutsService>(LayoutsService);
prisma = module.get(PrismaService) as jest.Mocked<PrismaService>;
});
afterEach(() => {
jest.clearAllMocks();
});
describe("findAll", () => {
it("should return all layouts for a user", async () => {
const mockLayouts = [mockLayout];
prisma.userLayout.findMany.mockResolvedValue(mockLayouts);
const result = await service.findAll(mockWorkspaceId, mockUserId);
expect(result).toEqual(mockLayouts);
expect(prisma.userLayout.findMany).toHaveBeenCalledWith({
where: {
workspaceId: mockWorkspaceId,
userId: mockUserId,
},
orderBy: {
isDefault: "desc",
createdAt: "desc",
},
});
});
});
describe("findDefault", () => {
it("should return default layout", async () => {
prisma.userLayout.findFirst.mockResolvedValueOnce(mockLayout);
const result = await service.findDefault(mockWorkspaceId, mockUserId);
expect(result).toEqual(mockLayout);
expect(prisma.userLayout.findFirst).toHaveBeenCalledWith({
where: {
workspaceId: mockWorkspaceId,
userId: mockUserId,
isDefault: true,
},
});
});
it("should return most recent layout if no default exists", async () => {
prisma.userLayout.findFirst
.mockResolvedValueOnce(null) // No default
.mockResolvedValueOnce(mockLayout); // Most recent
const result = await service.findDefault(mockWorkspaceId, mockUserId);
expect(result).toEqual(mockLayout);
expect(prisma.userLayout.findFirst).toHaveBeenCalledTimes(2);
});
it("should throw NotFoundException if no layouts exist", async () => {
prisma.userLayout.findFirst
.mockResolvedValueOnce(null) // No default
.mockResolvedValueOnce(null); // No layouts
await expect(
service.findDefault(mockWorkspaceId, mockUserId)
).rejects.toThrow(NotFoundException);
});
});
describe("findOne", () => {
it("should return a layout by ID", async () => {
prisma.userLayout.findUnique.mockResolvedValue(mockLayout);
const result = await service.findOne("layout-1", mockWorkspaceId, mockUserId);
expect(result).toEqual(mockLayout);
expect(prisma.userLayout.findUnique).toHaveBeenCalledWith({
where: {
id: "layout-1",
workspaceId: mockWorkspaceId,
userId: mockUserId,
},
});
});
it("should throw NotFoundException if layout not found", async () => {
prisma.userLayout.findUnique.mockResolvedValue(null);
await expect(
service.findOne("invalid-id", mockWorkspaceId, mockUserId)
).rejects.toThrow(NotFoundException);
});
});
describe("create", () => {
it("should create a new layout", async () => {
const createDto = {
name: "New Layout",
layout: [],
isDefault: false,
};
prisma.$transaction.mockImplementation((callback) =>
callback({
userLayout: {
create: jest.fn().mockResolvedValue(mockLayout),
updateMany: jest.fn(),
},
})
);
const result = await service.create(mockWorkspaceId, mockUserId, createDto);
expect(result).toBeDefined();
});
it("should unset other defaults when creating default layout", async () => {
const createDto = {
name: "New Default",
layout: [],
isDefault: true,
};
const mockUpdateMany = jest.fn();
const mockCreate = jest.fn().mockResolvedValue(mockLayout);
prisma.$transaction.mockImplementation((callback) =>
callback({
userLayout: {
updateMany: mockUpdateMany,
create: mockCreate,
},
})
);
await service.create(mockWorkspaceId, mockUserId, createDto);
expect(mockUpdateMany).toHaveBeenCalledWith({
where: {
workspaceId: mockWorkspaceId,
userId: mockUserId,
isDefault: true,
},
data: {
isDefault: false,
},
});
});
});
describe("update", () => {
it("should update a layout", async () => {
const updateDto = {
name: "Updated Layout",
layout: [{ i: "tasks-1", x: 1, y: 0, w: 2, h: 2 }],
};
const mockUpdate = jest.fn().mockResolvedValue({ ...mockLayout, ...updateDto });
const mockFindUnique = jest.fn().mockResolvedValue(mockLayout);
prisma.$transaction.mockImplementation((callback) =>
callback({
userLayout: {
findUnique: mockFindUnique,
update: mockUpdate,
updateMany: jest.fn(),
},
})
);
const result = await service.update(
"layout-1",
mockWorkspaceId,
mockUserId,
updateDto
);
expect(result).toBeDefined();
expect(mockFindUnique).toHaveBeenCalled();
expect(mockUpdate).toHaveBeenCalled();
});
it("should throw NotFoundException if layout not found", async () => {
const mockFindUnique = jest.fn().mockResolvedValue(null);
prisma.$transaction.mockImplementation((callback) =>
callback({
userLayout: {
findUnique: mockFindUnique,
},
})
);
await expect(
service.update("invalid-id", mockWorkspaceId, mockUserId, {})
).rejects.toThrow(NotFoundException);
});
});
describe("remove", () => {
it("should delete a layout", async () => {
prisma.userLayout.findUnique.mockResolvedValue(mockLayout);
prisma.userLayout.delete.mockResolvedValue(mockLayout);
await service.remove("layout-1", mockWorkspaceId, mockUserId);
expect(prisma.userLayout.delete).toHaveBeenCalledWith({
where: {
id: "layout-1",
workspaceId: mockWorkspaceId,
userId: mockUserId,
},
});
});
it("should throw NotFoundException if layout not found", async () => {
prisma.userLayout.findUnique.mockResolvedValue(null);
await expect(
service.remove("invalid-id", mockWorkspaceId, mockUserId)
).rejects.toThrow(NotFoundException);
});
});
});