Skip to content
Merged
Show file tree
Hide file tree
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
89 changes: 89 additions & 0 deletions web/src/components/community/PublishModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

import { act, render, screen, waitFor } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { buildStoredPatternCode, communityPatternUrl } from "@/lib/community/license";
import { CODE_MAX } from "@/lib/community/validate";
import PublishModal from "./PublishModal";

const push = vi.fn();
Expand Down Expand Up @@ -102,3 +104,90 @@ describe("PublishModal", () => {
expect(server.mock).toHaveBeenCalledTimes(2);
});
});

// The community stores a pattern inside its licence header and footer, and
// "Open in Pattern Lab" hands that stored text to the lab unchanged. The API
// takes the wrapping off before it measures; the modal measured the wrapped
// text, so a fork it would have accepted was refused before it was sent.
describe("PublishModal code size", () => {
/** A pattern body of exactly `length` characters. */
function bodyOf(length: number) {
const pad = "\n// ";
return `${CODE}${pad}${"x".repeat(length - CODE.length - pad.length)}`;
}

/** How a fork of `@ada`'s pattern comes back out of the community. */
function storedFork(body: string) {
return buildStoredPatternCode(body, {
title: "Pentagrid Tower (remix)",
license: "CC-BY-SA-4.0",
handle: "grace",
date: "2026-10-01",
basedOn: { title: "Pentagrid Tower", handle: "ada", url: communityPatternUrl("p0") },
});
}

function openFork(code: string) {
render(
<PublishModal
code={code}
parentId="p1"
parentTitle="Pentagrid Tower (remix)"
parentLicense="CC-BY-SA-4.0"
initialTitle="Pentagrid Tower (remix 2)"
onClose={() => {}}
/>,
);
return screen.getByRole("button", { name: "Publish to the wall" });
}

it("publishes an untouched fork whose body fits under the cap", async () => {
const code = storedFork(bodyOf(99_700));
expect(code.length).toBeGreaterThan(CODE_MAX);

const button = openFork(code);
act(() => button.click());

expect(screen.queryByText(/KB of code/)).toBeNull();
expect(server.mock).toHaveBeenCalledTimes(1);
await server.answer(201, { id: "p2" });
await waitFor(() => expect(push).toHaveBeenCalledWith("/community/p/p2"));
});

it("saves the author's own post reopened in the lab, unedited", () => {
const code = storedFork(bodyOf(99_700));
render(
<PublishModal
code={code}
parentId={null}
parentTitle={null}
editOf={{
id: "p1",
title: "Pentagrid Tower (remix)",
description: null,
visibility: "public",
hasCpp: false,
portCount: 0,
}}
onClose={() => {}}
/>,
);

act(() => screen.getByRole("button", { name: "Update this pattern" }).click());

expect(screen.queryByText(/KB of code/)).toBeNull();
expect(server.mock).toHaveBeenCalledTimes(1);
expect(server.mock.mock.calls[0]).toEqual([
"/api/community/patterns/p1",
expect.objectContaining({ method: "PATCH" }),
]);
});

it("still stops a body over the cap before it is sent", () => {
const button = openFork(storedFork(bodyOf(CODE_MAX + 600)));
act(() => button.click());

expect(screen.getByText(/This pattern is 101 KB of code/)).toBeInTheDocument();
expect(server.mock).not.toHaveBeenCalled();
});
});
10 changes: 8 additions & 2 deletions web/src/components/community/PublishModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
LICENSE_OPTIONS,
forkLicenseOptions,
licenseById,
stripShareWrapping,
} from "@/lib/pattern/share";
import { captureEvent } from "@/lib/posthogEvents";
import AuthModal from "./AuthModal";
Expand Down Expand Up @@ -149,11 +150,16 @@ export default function PublishModal({
const send = async (): Promise<boolean> => {
const trimmed = title.trim();
setError(null);
if (code.length > CODE_MAX) {
// Measured the way the API measures it, with the licence header and
// footer off. "Open in Pattern Lab" hands back the stored code with both
// still on, a few hundred characters the server never counts, and
// counting them here refused a pattern the server takes unchanged.
const codeLength = (code.length <= CODE_MAX * 2 ? stripShareWrapping(code) : code).length;
if (codeLength > CODE_MAX) {
// The server says the same thing, but after a round trip that costs
// one of five publishes a minute — and without the likely cause.
setError(
`This pattern is ${Math.round(code.length / 1000)} KB of code; the community takes up to ${
`This pattern is ${Math.round(codeLength / 1000)} KB of code; the community takes up to ${
CODE_MAX / 1000
} KB. Imported images as pixel layers are the usual reason — hide or delete one and share again.`,
);
Expand Down
Loading