Skip to content

Commit

Permalink
json editor for body
Browse files Browse the repository at this point in the history
  • Loading branch information
Mazuh committed Mar 10, 2024
1 parent 9a36271 commit c49cf1c
Show file tree
Hide file tree
Showing 7 changed files with 317 additions and 53 deletions.
36 changes: 36 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/react-fontawesome": "^0.2.0",
"@hookform/resolvers": "^3.3.2",
"@monaco-editor/react": "^4.6.0",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-label": "^2.0.2",
Expand Down
4 changes: 4 additions & 0 deletions src/entities/http-for-dummies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export function isRequestingToLocalhost(request: RequestSnapshot): boolean {
].some((localBeginning) => request.url.startsWith(localBeginning));
}

export function canMethodHaveBody(method: RequestSnapshot["method"]): boolean {
return method !== "GET" && method !== "HEAD";
}

/** HTTP methods but as constants. */
export const HTTP_METHODS: ProjectRequestSpec["method"][] = [
"GET",
Expand Down
2 changes: 1 addition & 1 deletion src/entities/runtime-entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export interface RuntimeState {
export interface RequestSnapshot {
readonly url: string;
readonly method: string;
readonly body: string;
readonly body: string | null;
readonly headers: { key: string; value: string }[];
}

Expand Down
39 changes: 39 additions & 0 deletions src/features/project-workspace/BodyEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Editor, EditorProps } from "@monaco-editor/react";
import { useCallback, useId } from "react";

export interface BodyEditorProps extends Omit<EditorProps, "onMount"> {
name: string;
}

export function BodyEditor(props: BodyEditorProps) {
const id = useId();

const { name, wrapperProps, ...restOfProps } = props;
const overridingWrapperProps = { ...wrapperProps, id };

const handleOnMount = useCallback(() => {
const wrapperEl = document.getElementById(id);
if (!wrapperEl) {
throw new Error("Unexpected missing wrapper during `BodyEditor` mount.");
}

const [textareaEl] = wrapperEl.getElementsByTagName("textarea");
if (!textareaEl) {
throw new Error(
"Unexpected missing `textarea` during `BodyEditor` mount."
);
}

textareaEl.setAttribute("name", name);
}, [id, name]);

return (
<Editor
language="json"
theme="vs-dark"
{...restOfProps}
wrapperProps={overridingWrapperProps}
onMount={handleOnMount}
/>
);
}
139 changes: 137 additions & 2 deletions src/features/project-workspace/Runtime.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,25 @@ jest.mock("@/services/opfs-projects-shared-internals", () => ({
persistProject: jest.fn(),
}));

jest.mock("./BodyEditor.tsx", () => ({
BodyEditor: jest
.fn()
.mockImplementation(({ name, defaultValue, onChange }) => (
<div id="mocked-body-editor">
<textarea
name={name}
defaultValue={defaultValue}
onChange={(e) => onChange(e.target.value)}
/>
</div>
)),
}));

interface MockedGlobalWithFetch {
fetch: (url: string) => Promise<MockedResponse>;
fetch: (
url: string,
options: { body: string; method: string }
) => Promise<MockedResponse>;
}

interface MockedResponse {
Expand Down Expand Up @@ -80,6 +97,24 @@ describe("Runtime component, given a selected specification", () => {
],
body: "",
},
{
uuid: "f4eb2eec-3527-4d96-b34a-cc0e9914231c",
url: "https://echo.zuplo.io/",
method: "POST",
headers: [
{ key: "Accept", value: "application/json", isEnabled: true },
],
body: JSON.stringify({ "this is": "an echo test" }),
},
{
uuid: "894f6e21-8002-4111-938b-dbebaaa44966",
url: "https://echo.zuplo.io/",
method: "GET",
headers: [
{ key: "Accept", value: "application/json", isEnabled: true },
],
body: JSON.stringify({ "this is": "an echo test" }),
},
],
});

Expand All @@ -89,7 +124,14 @@ describe("Runtime component, given a selected specification", () => {

jest
.spyOn(global as unknown as MockedGlobalWithFetch, "fetch")
.mockImplementation(async (url: string) => {
.mockImplementation(async (url, { body, method }) => {
if ((method === "GET" || method === "HEAD") && body !== null) {
// same behavior as Chrome
throw new Error(
"Failed to execute 'fetch' on 'Window': Request with GET/HEAD method cannot have body."
);
}

switch (url) {
case "https://api.zippopotam.us/us/33162":
return {
Expand Down Expand Up @@ -148,6 +190,15 @@ describe("Runtime component, given a selected specification", () => {
case "https://catfact.ninja/non-existing":
throw new Error("CORS Error (mocked).");

case "https://echo.zuplo.io/":
// this is similar but not exactly the same behavior as this Echo API
return {
text: async () => body,
status: 200,
ok: true,
headers: [["content-type", "application/json"]],
};

default:
throw new Error(
"Unexpected `fetch` call during test scenario. Unmocked URL."
Expand Down Expand Up @@ -230,6 +281,32 @@ describe("Runtime component, given a selected specification", () => {
],
body: "",
},
{
body: '{"this is":"an echo test"}',
headers: [
{
isEnabled: true,
key: "Accept",
value: "application/json",
},
],
method: "POST",
url: "https://echo.zuplo.io/",
uuid: "f4eb2eec-3527-4d96-b34a-cc0e9914231c",
},
{
body: '{"this is":"an echo test"}',
headers: [
{
isEnabled: true,
key: "Accept",
value: "application/json",
},
],
method: "GET",
url: "https://echo.zuplo.io/",
uuid: "894f6e21-8002-4111-938b-dbebaaa44966",
},
],
});
});
Expand Down Expand Up @@ -374,4 +451,62 @@ describe("Runtime component, given a selected specification", () => {
expect(screen.getByText("Error")).toBeVisible();
expect(screen.getByText(/CORS Error/im)).toBeVisible();
});

it("can perform a POST using a body", async () => {
await act(async () =>
render(
<RequestsSpecsContextProvider projectUuid="7fde4f8e-b6ac-4218-ae20-1b866e61ec56">
<Runtime specUuid="f4eb2eec-3527-4d96-b34a-cc0e9914231c" />
</RequestsSpecsContextProvider>
)
);

const input = screen.getByText(/an echo test/, { selector: "textarea" });
expect(input).toBeVisible();

const run = screen.getByRole("button", { name: /Run/i });
await act(async () => fireEvent.click(run));

expect(screen.getByText(/HTTP success/i)).toBeVisible();
expect(screen.getByText("200")).toBeVisible();

const output = screen.getByText(/an echo test/, { selector: "code" });
expect(output).toBeVisible();

expect(global.fetch as jest.Mock).toHaveBeenCalledWith(
"https://echo.zuplo.io/",
{
body: '{"this is":"an echo test"}',
headers: { Accept: "application/json" },
method: "POST",
}
);
});

it("can perform a GET using a body, even if it throws an error", async () => {
await act(async () =>
render(
<RequestsSpecsContextProvider projectUuid="7fde4f8e-b6ac-4218-ae20-1b866e61ec56">
<Runtime specUuid="894f6e21-8002-4111-938b-dbebaaa44966" />
</RequestsSpecsContextProvider>
)
);

const run = screen.getByRole("button", { name: /Run/i });
await act(async () => fireEvent.click(run));

expect(screen.queryByText(/HTTP success/i)).toBe(null);
expect(screen.queryByText("200")).toBe(null);

expect(screen.getByText("Error")).toBeVisible();

expect(global.fetch as jest.Mock).toHaveBeenCalledWith(
"https://echo.zuplo.io/",
{
body: '{"this is":"an echo test"}',
headers: { Accept: "application/json" },
method: "GET",
}
);
});
});
Loading

0 comments on commit c49cf1c

Please sign in to comment.