|
| 1 | +import { mount } from "@vue/test-utils"; |
| 2 | +import { describe, expect, it, vi } from "vitest"; |
| 3 | + |
| 4 | +import InvitationsTable from "../components/InvitationsTable.vue"; |
| 5 | +import type { AccountInvitationRow, AccountInvitationsBootstrap } from "../types"; |
| 6 | + |
| 7 | +const bootstrap: AccountInvitationsBootstrap = { |
| 8 | + pendingApiUrl: "/api/v1/account/invitations/pending", |
| 9 | + acceptedApiUrl: "/api/v1/account/invitations/accepted", |
| 10 | + refreshApiUrl: "/api/v1/account/invitations/refresh", |
| 11 | + resendApiUrl: "/api/v1/account/invitations/123456789/resend", |
| 12 | + dismissApiUrl: "/api/v1/account/invitations/123456789/dismiss", |
| 13 | + bulkApiUrl: "/api/v1/account/invitations/bulk", |
| 14 | + listPageUrl: "/account/invitations/", |
| 15 | + pageSize: 50, |
| 16 | + canManageInvitations: true, |
| 17 | + canRefresh: true, |
| 18 | + canResend: true, |
| 19 | + canDismiss: true, |
| 20 | + canBulkAction: true, |
| 21 | + sentinelToken: "123456789", |
| 22 | + csrfToken: "csrf-token", |
| 23 | +}; |
| 24 | + |
| 25 | +const row: AccountInvitationRow = { |
| 26 | + invitation_id: 10, |
| 27 | + email: "alice@example.com", |
| 28 | + full_name: "Alice", |
| 29 | + note: "", |
| 30 | + invited_by_username: "bob", |
| 31 | + invited_at: "2026-04-20T10:00:00", |
| 32 | + send_count: 1, |
| 33 | + last_sent_at: "2026-04-20T10:00:00", |
| 34 | + status: "pending", |
| 35 | + organization_id: 33, |
| 36 | + organization_name: "Example Org", |
| 37 | +}; |
| 38 | + |
| 39 | +describe("InvitationsTable", () => { |
| 40 | + it("renders pending invitations with tbody-based loading/error states", async () => { |
| 41 | + const wrapper = mount(InvitationsTable, { |
| 42 | + props: { |
| 43 | + bootstrap, |
| 44 | + rows: [], |
| 45 | + count: 0, |
| 46 | + currentPage: 1, |
| 47 | + totalPages: 1, |
| 48 | + isLoading: true, |
| 49 | + error: null, |
| 50 | + scope: "pending", |
| 51 | + buildPageHref: (page: number) => `?page=${page}`, |
| 52 | + }, |
| 53 | + }); |
| 54 | + |
| 55 | + expect(wrapper.find("tbody td").text()).toContain("Loading pending invitations..."); |
| 56 | + |
| 57 | + await wrapper.setProps({ isLoading: false, error: "Failed to load invitations." }); |
| 58 | + expect(wrapper.find("tbody td").text()).toContain("Failed to load invitations."); |
| 59 | + expect(wrapper.find(".alert.alert-danger").exists()).toBe(false); |
| 60 | + }); |
| 61 | + |
| 62 | + it("uses inline errors instead of alert dialogs for bulk-action validation", async () => { |
| 63 | + const alertSpy = vi.spyOn(window, "alert").mockImplementation(() => undefined); |
| 64 | + |
| 65 | + const wrapper = mount(InvitationsTable, { |
| 66 | + props: { |
| 67 | + bootstrap, |
| 68 | + rows: [row], |
| 69 | + count: 1, |
| 70 | + currentPage: 1, |
| 71 | + totalPages: 1, |
| 72 | + isLoading: false, |
| 73 | + error: null, |
| 74 | + scope: "pending", |
| 75 | + buildPageHref: (page: number) => `?page=${page}`, |
| 76 | + }, |
| 77 | + }); |
| 78 | + |
| 79 | + await wrapper.find("form").trigger("submit"); |
| 80 | + |
| 81 | + expect(alertSpy).not.toHaveBeenCalled(); |
| 82 | + expect(wrapper.text()).toContain("Please select an action and at least one invitation."); |
| 83 | + |
| 84 | + alertSpy.mockRestore(); |
| 85 | + }); |
| 86 | +}); |
0 commit comments