Files
gitmost/apps/client/src/features/share/queries/share-query.null-normalization.test.tsx
claude code agent 227 9e1d057878 fix(qa): resolve QA-pass issues #122–#134
Batch of fixes from the automated QA pass on develop. Each was reproduced and
then verified fixed live (browser/curl); logic-bearing fixes have unit tests.

Functional bugs:
- #122 collab-token was capped by the anonymous public-share-AI throttler (5/min);
  skip all non-AUTH named throttlers on this auth-guarded, client-cached route.
- #123 editor onAuthenticationFailed threw `jwtDecode(undefined)` and never
  reconnected; read the token via a ref, guard the decode (incl. missing exp),
  and refetch+reconnect on any auth failure.
- #124 a slash command containing a space ("/Heading 1") inserted literal text;
  enable allowSpaces and close the menu when the query matches no items.
- #125 space slug auto-gen produced uppercase initials for multi-word names;
  computeSpaceSlug now yields a lowercase alphanumeric slug.
- #126 AI chat window position/size now persisted (atomWithStorage) across reload;
  also fixes a latent ResizeObserver-attach bug on first open.
- #127 workspace name update accepted URLs; add @NoUrls (parity with setup).
- #132 icon-columns 4/5 passed calc() into SVG width/height attrs (console spam);
  size via style. share-for-page query returns null instead of undefined.
- #134 "Reindex now" counter looked stuck: reindex runs async; the client now
  polls coverage (bounded) so the counter climbs live; misleading server comment
  reworded.

UX / consistency:
- #128 add success toasts to favorite/label/avatar/member-(de)activate.
- #129 "1 result found" pluralization; hide the single-option Type filter.
- #130 replace raw Zod strings with friendly messages (name/password/group).
- #131 unify "Untitled" casing in tree/breadcrumb/tab; stop force-uppercasing
  space-name chips; fix confirm-dialog labels (Cancel / Remove), invite
  placeholder typo, Export/Move-to-space labels.
- #133 disable profile Save when clean; toast on unsupported avatar image;
  style the invalid-invitation page with a CTA; hide Share for read-only users;
  align the dictation "not configured" message; "Go to login page" typo.

Tests: computeSpaceSlug, workspace-name NoUrls DTO, share-query null
normalization, slash getSuggestionItems empty-close.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 20:47:40 +03:00

85 lines
3.0 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
import React from "react";
import { renderHook, waitFor } from "@testing-library/react";
import {
QueryClient,
QueryClientProvider,
} from "@tanstack/react-query";
// React Query forbids `undefined` as resolved query data ("Query data cannot be
// undefined"). The backend resolves to `undefined` when a page has no share, so
// `useShareForPageQuery` normalizes that absence to `null`:
// queryFn: async () => (await getShareForPage(pageId)) ?? null
// These tests pin that contract: the hook must resolve to `null` (never
// `undefined`) when there is no share, and pass a real share through untouched.
// Mock the service module so the queryFn calls our stub instead of the network.
vi.mock("@/features/share/services/share-service.ts", () => ({
getShareForPage: vi.fn(),
// Other named exports referenced by share-query.ts must exist on the mock so
// the module import resolves; they are unused by these tests.
createShare: vi.fn(),
deleteShare: vi.fn(),
getSharedPageTree: vi.fn(),
getShareInfo: vi.fn(),
getSharePageInfo: vi.fn(),
getShares: vi.fn(),
updateShare: vi.fn(),
}));
import { getShareForPage } from "@/features/share/services/share-service.ts";
import { useShareForPageQuery } from "@/features/share/queries/share-query.ts";
function createWrapper() {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
return function Wrapper({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
}
describe("useShareForPageQuery — null normalization", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("normalizes an absent share (undefined) to null", async () => {
vi.mocked(getShareForPage).mockResolvedValue(undefined as any);
const { result } = renderHook(() => useShareForPageQuery("page-1"), {
wrapper: createWrapper(),
});
await waitFor(() => expect(result.current.isSuccess).toBe(true));
// The key assertion: null, never undefined.
expect(result.current.data).toBeNull();
expect(result.current.data).not.toBeUndefined();
});
it("normalizes an absent share (null) to null", async () => {
vi.mocked(getShareForPage).mockResolvedValue(null as any);
const { result } = renderHook(() => useShareForPageQuery("page-2"), {
wrapper: createWrapper(),
});
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(result.current.data).toBeNull();
});
it("passes an existing share through unchanged", async () => {
const share = { id: "share-1", pageId: "page-3" } as any;
vi.mocked(getShareForPage).mockResolvedValue(share);
const { result } = renderHook(() => useShareForPageQuery("page-3"), {
wrapper: createWrapper(),
});
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(result.current.data).toEqual(share);
});
});