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
71 changes: 71 additions & 0 deletions apps/web/src/browser/BrowserDeviceToolbar.browser.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { PreviewViewportSetting } from "@t3tools/contracts";
import { useState } from "react";
import { expect, it } from "vite-plus/test";
import { userEvent } from "vite-plus/test/browser";

import { render } from "../../test/render";
import { BrowserDeviceToolbar } from "./BrowserDeviceToolbar";

function Viewport({ width = 1920, height = 1080 }: { width?: number; height?: number }) {
const [setting, setSetting] = useState<PreviewViewportSetting>({
_tag: "freeform",
width,
height,
});
const [aspectRatio, setAspectRatio] = useState<number | null>(width / height);
if (setting._tag === "fill") throw new Error("Expected fixed viewport");
return (
<>
<BrowserDeviceToolbar
setting={setting}
width={800}
aspectRatio={aspectRatio}
onAspectRatioChange={setAspectRatio}
onChange={async (next) => setSetting(next)}
/>
<output aria-label="Saved viewport">
{setting.width} × {setting.height}
</output>
<button type="button">Leave toolbar</button>
</>
);
}

it.each([
{ width: 1920, height: 1080, axis: "width", typed: "2560", expected: "2560 × 1440" },
{ width: 390, height: 844, axis: "height", typed: "2532", expected: "1170 × 2532" },
])("keeps each typed $axis digit until the locked size is committed", async (testCase) => {
await using screen = await render(<Viewport {...testCase} />);
const input = screen.getByRole("spinbutton", { name: `Viewport ${testCase.axis}` });
await userEvent.clear(input);
await userEvent.type(input, testCase.typed);
await expect.element(input).toHaveValue(Number(testCase.typed));
await screen.getByRole("button", { name: "Leave toolbar" }).click();
await expect
.element(screen.getByRole("status", { name: "Saved viewport" }))
.toHaveTextContent(testCase.expected);
});

it("clamps a completed width below the locked minimum when leaving the toolbar", async () => {
await using screen = await render(<Viewport />);
const input = screen.getByRole("spinbutton", { name: "Viewport width" });
await userEvent.clear(input);
await userEvent.type(input, "256");
await expect.element(input).toHaveValue(256);
await screen.getByRole("button", { name: "Leave toolbar" }).click();
await expect
.element(screen.getByRole("status", { name: "Saved viewport" }))
.toHaveTextContent("427 × 240");
});

it("still limits a completed locked size to the viewport area cap", async () => {
await using screen = await render(<Viewport width={1920} height={1920} />);
const input = screen.getByRole("spinbutton", { name: "Viewport width" });
await userEvent.clear(input);
await userEvent.type(input, "3840");
await expect.element(input).toHaveValue(3840);
await screen.getByRole("button", { name: "Leave toolbar" }).click();
await expect
.element(screen.getByRole("status", { name: "Saved viewport" }))
.toHaveTextContent("2880 × 2880");
});
34 changes: 25 additions & 9 deletions apps/web/src/browser/BrowserDeviceToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export function BrowserDeviceToolbar({
const [customSize, setCustomSize] = useState<{
readonly width: string;
readonly height: string;
readonly axis?: "width" | "height";
} | null>(null);
const presentedSize = customSize ?? {
width: String(setting.width),
Expand All @@ -61,15 +62,25 @@ export function BrowserDeviceToolbar({
PREVIEW_VIEWPORT_PRESETS.some((preset) => preset.id === setting.presetId)
? setting.presetId
: RESPONSIVE_VALUE;
const customWidth = Number(presentedSize.width);
const customHeight = Number(presentedSize.height);
const typedWidth = Number(presentedSize.width);
const typedHeight = Number(presentedSize.height);
const { width: customWidth, height: customHeight } =
aspectRatio === null || customSize?.axis === undefined
? { width: typedWidth, height: typedHeight }
: resizeFreeformViewport(
{ width: typedWidth, height: typedHeight },
{ x: 0, y: 0 },
1,
customSize?.axis === "height" ? "south" : "east",
aspectRatio,
);
const customValid =
Number.isInteger(customWidth) &&
Number.isInteger(customHeight) &&
customWidth >= PREVIEW_VIEWPORT_MIN_DIMENSION &&
customWidth <= PREVIEW_VIEWPORT_MAX_DIMENSION &&
customHeight >= PREVIEW_VIEWPORT_MIN_DIMENSION &&
customHeight <= PREVIEW_VIEWPORT_MAX_DIMENSION &&
Number.isInteger(typedWidth) &&
Number.isInteger(typedHeight) &&
typedWidth >= PREVIEW_VIEWPORT_MIN_DIMENSION &&
typedWidth <= PREVIEW_VIEWPORT_MAX_DIMENSION &&
typedHeight >= PREVIEW_VIEWPORT_MIN_DIMENSION &&
typedHeight <= PREVIEW_VIEWPORT_MAX_DIMENSION &&
customWidth * customHeight <= PREVIEW_VIEWPORT_MAX_AREA;

const apply = (next: PreviewViewportSetting, nextAspectRatio = aspectRatio) => {
Expand All @@ -96,6 +107,7 @@ export function BrowserDeviceToolbar({
const next = {
width: axis === "width" ? value : (current?.width ?? String(setting.width)),
height: axis === "height" ? value : (current?.height ?? String(setting.height)),
axis,
};
const numeric = Number(value);
if (
Expand All @@ -115,7 +127,11 @@ export function BrowserDeviceToolbar({
axis === "width" ? "east" : "south",
aspectRatio,
);
return { width: String(resized.width), height: String(resized.height) };
return {
...next,
width: axis === "width" ? value : String(resized.width),
height: axis === "height" ? value : String(resized.height),
};
});
};

Expand Down
Loading