Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import {
useReceiveMessage,
} from "../../../internal/hooks/useMessage.ts";
import { type ImagePayload } from "../../../fields/ImageField.tsx";
import { isFakeStarterLocalDev } from "../../../utils/isFakeStarterLocalDev.ts";
import {
isFakeStarterLocalDev,
isLocalEditorRoute,
} from "../../../utils/isFakeStarterLocalDev.ts";

let pendingEmptyImageSession:
| { messageId: string; apply: (payload: ImagePayload) => void }
Expand Down Expand Up @@ -64,7 +67,10 @@ export const EmptyImageState: React.FC<EmptyImageStateProps> = ({

const handleImageSelection = React.useCallback(() => {
if (!hasParentData && constantValueEnabled && isEditing) {
if (isFakeStarterLocalDev()) {
if (
isFakeStarterLocalDev() ||
(typeof window !== "undefined" && isLocalEditorRoute(window.location))
) {
const userInput = prompt("Enter Image URL:");
if (!userInput) {
return;
Expand Down
97 changes: 31 additions & 66 deletions packages/visual-editor/src/fields/ImageField.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import { TemplatePropsContext } from "../hooks/useDocument.tsx";
import { YextAutoField } from "./YextAutoField.tsx";
import { type ImageField } from "./ImageField.tsx";

const { sendToParentMock, translatableStringFieldMock } = vi.hoisted(() => ({
const { sendToParentMock } = vi.hoisted(() => ({
sendToParentMock: vi.fn(),
translatableStringFieldMock: vi.fn(),
}));

const initialUrl = window.location.href;

vi.mock("react-i18next", async (importOriginal) => {
const actual = await importOriginal<typeof import("react-i18next")>();

Expand All @@ -29,35 +30,20 @@ vi.mock("../internal/hooks/useMessage.ts", () => ({
}),
}));

vi.mock("../internal/hooks/useMessageReceivers.ts", () => ({
useTemplateMetadata: () => ({
locatorDisplayFields: {
c_title: {
field_type_id: "type.string",
field_name: "Title",
},
c_photo: {
field_type_id: "type.image",
field_name: "Photo",
},
},
}),
}));

vi.mock("./TranslatableStringField.tsx", async (importOriginal) => {
vi.mock("../internal/hooks/useMessageReceivers.ts", async (importOriginal) => {
const actual =
await importOriginal<typeof import("./TranslatableStringField.tsx")>();
await importOriginal<
typeof import("../internal/hooks/useMessageReceivers.ts")
>();

return {
...actual,
TranslatableStringField: translatableStringFieldMock,
useTemplateMetadata: () => ({
locatorDisplayFields: {},
}),
};
});

vi.mock("../utils/isFakeStarterLocalDev.ts", () => ({
isFakeStarterLocalDev: () => true,
}));

const renderImageField = (
field: ImageField = {
type: "image",
Expand All @@ -67,11 +53,6 @@ const renderImageField = (
) => {
const onChange = vi.fn();

translatableStringFieldMock.mockImplementation((label: string) => ({
type: "text",
label,
}));

render(
<TemplatePropsContext.Provider value={{ document: { locale: "en" } }}>
<YextAutoField
Expand All @@ -89,11 +70,13 @@ const renderImageField = (
describe("ImageField", () => {
afterEach(() => {
vi.restoreAllMocks();
translatableStringFieldMock.mockReset();
sendToParentMock.mockReset();
window.history.replaceState({}, "", initialUrl);
});
Comment on lines +74 to 75

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Restore mocked spies in shared cleanup.

window.prompt is mocked in prompt tests but not explicitly restored in shared teardown, which can leak mocked behavior into later tests and create order-dependent failures.

Suggested fix
afterEach(() => {
+  vi.restoreAllMocks();
  sendToParentMock.mockReset();
  window.history.replaceState({}, "", initialUrl);
});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
window.history.replaceState({}, "", initialUrl);
});
afterEach(() => {
vi.restoreAllMocks();
sendToParentMock.mockReset();
window.history.replaceState({}, "", initialUrl);
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/visual-editor/src/fields/ImageField.test.tsx` around lines 74 - 75,
The window.prompt spy is mocked in the prompt-related tests but not explicitly
restored in the shared afterEach cleanup block, causing the mock to leak into
subsequent tests. In the afterEach block (where window.history.replaceState is
called), add an explicit restoration of the window.prompt spy to ensure the mock
is cleaned up after each test and prevent test order dependencies.


it("renders through YextAutoField as a registered field type", () => {
it("prompts for an image URL in fake starter local dev", () => {
window.history.replaceState({}, "", "/dev-location/example");

const promptSpy = vi
.spyOn(window, "prompt")
.mockReturnValue("https://example.com/image.jpg");
Expand All @@ -115,43 +98,25 @@ describe("ImageField", () => {
});
});

it("passes locator alt text options into the alt text field", () => {
const getAltTextOptions = vi.fn((templateMetadata) => [
{
label: templateMetadata.locatorDisplayFields?.c_title?.field_name ?? "",
value: "c_title",
},
]);
it("prompts for an image URL in local-editor", () => {
window.history.replaceState({}, "", "/local-editor");

renderImageField(
{
type: "image",
label: "Image",
getAltTextOptions,
},
{
en: {
alternateText: "",
url: "https://example.com/image.jpg",
height: 1,
width: 1,
},
hasLocalizedValue: "true",
}
);

expect(getAltTextOptions).toHaveBeenCalledWith({
locatorDisplayFields: {
c_title: {
field_type_id: "type.string",
field_name: "Title",
},
c_photo: {
field_type_id: "type.image",
field_name: "Photo",
},
const promptSpy = vi
.spyOn(window, "prompt")
.mockReturnValue("https://example.com/local-editor-image.jpg");
const { onChange } = renderImageField();

fireEvent.click(screen.getByRole("button", { name: "Choose Image" }));

expect(promptSpy).toHaveBeenCalledWith("Enter Image URL:");
expect(onChange).toHaveBeenCalledWith({
en: {
alternateText: "",
url: "https://example.com/local-editor-image.jpg",
height: 1,
width: 1,
},
hasLocalizedValue: "true",
});
expect(screen.getByText("Alt Text (en)")).toBeDefined();
});
});
10 changes: 8 additions & 2 deletions packages/visual-editor/src/fields/ImageField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ import {
TemplateMetadata,
} from "../internal/types/templateMetadata.ts";
import { useTemplateMetadata } from "../internal/hooks/useMessageReceivers.ts";
import { isFakeStarterLocalDev } from "../utils/isFakeStarterLocalDev.ts";
import {
isFakeStarterLocalDev,
isLocalEditorRoute,
} from "../utils/isFakeStarterLocalDev.ts";
import { YextAutoField } from "./YextAutoField.tsx";
import { type EmbeddedStringOption } from "../editor/EmbeddedFieldStringInput.tsx";

Expand Down Expand Up @@ -110,7 +113,10 @@ export const ImageFieldOverride = ({
e.preventDefault();

/** Handles local development testing outside of Storm */
if (isFakeStarterLocalDev()) {
if (
isFakeStarterLocalDev() ||
(typeof window !== "undefined" && isLocalEditorRoute(window.location))
) {
const userInput = prompt("Enter Image URL:");
if (!userInput) {
return;
Expand Down
13 changes: 13 additions & 0 deletions packages/visual-editor/src/utils/isFakeStarterLocalDev.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { afterEach, describe, expect, it } from "vitest";
import {
isFakeStarterLocalDev,
isFakeStarterLocalDevRoute,
isLocalEditorRoute,
} from "./isFakeStarterLocalDev.ts";

const initialUrl = window.location.href;
Expand All @@ -26,6 +27,18 @@ describe("isFakeStarterLocalDevRoute", () => {
});
});

describe("isLocalEditorRoute", () => {
it("matches the local editor route", () => {
expect(isLocalEditorRoute("http://localhost:5173/local-editor")).toBe(true);
expect(isLocalEditorRoute("/local-editor")).toBe(true);
});

it("does not match other routes", () => {
expect(isLocalEditorRoute("http://localhost:5173/edit")).toBe(false);
expect(isLocalEditorRoute("/dev-location/example")).toBe(false);
});
});

describe("isFakeStarterLocalDev", () => {
it("checks the current window location", () => {
window.history.replaceState({}, "", "/dev-locator/locator-slug");
Expand Down
4 changes: 4 additions & 0 deletions packages/visual-editor/src/utils/isFakeStarterLocalDev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export const isFakeStarterLocalDevRoute = (locationLike: LocationLike) => {
return getPathname(locationLike).startsWith("/dev-");
};

export const isLocalEditorRoute = (locationLike: LocationLike) => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we just update isFakeStarterLocalDev? If we want both, I'd make it a callable func without having to pass in variables like we do for isFakeStarterLocalDev. It's weird that that one handles window undefined and window.location but you make those calls manually in usages for this func.

return getPathname(locationLike) === "/local-editor";
};

export const isFakeStarterLocalDev = () => {
if (typeof window === "undefined") {
return false;
Expand Down
Loading