mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat: tool-first workflow polish -- fix tests, lint cleanup
- Remove obsolete fullscreen redirect test (route deleted in Task 3) - Update dropzone test strings to match new i18n values - Fix categories count in i18n locale test (12 -> 24) - Add chart-maker to no-dropzone exempt list in tool-registry test - Auto-fix import ordering (Biome) in popular tools route and landing config
This commit is contained in:
@@ -94,7 +94,6 @@ import { registerOcrPdf } from "./ocr-pdf.js";
|
|||||||
import { registerOptimizeForWeb } from "./optimize-for-web.js";
|
import { registerOptimizeForWeb } from "./optimize-for-web.js";
|
||||||
import { registerOrganizePdf } from "./organize-pdf.js";
|
import { registerOrganizePdf } from "./organize-pdf.js";
|
||||||
import { registerPassportPhoto } from "./passport-photo.js";
|
import { registerPassportPhoto } from "./passport-photo.js";
|
||||||
import { registerPopularTools } from "./popular.js";
|
|
||||||
import { registerPdfMetadata } from "./pdf-metadata.js";
|
import { registerPdfMetadata } from "./pdf-metadata.js";
|
||||||
import { registerPdfPageNumbers } from "./pdf-page-numbers.js";
|
import { registerPdfPageNumbers } from "./pdf-page-numbers.js";
|
||||||
import { registerPdfToImage } from "./pdf-to-image.js";
|
import { registerPdfToImage } from "./pdf-to-image.js";
|
||||||
@@ -103,6 +102,7 @@ import { registerPdfToWord } from "./pdf-to-word.js";
|
|||||||
import { registerPdfaConvert } from "./pdfa-convert.js";
|
import { registerPdfaConvert } from "./pdfa-convert.js";
|
||||||
import { registerPitchShift } from "./pitch-shift.js";
|
import { registerPitchShift } from "./pitch-shift.js";
|
||||||
import { registerPixelate } from "./pixelate.js";
|
import { registerPixelate } from "./pixelate.js";
|
||||||
|
import { registerPopularTools } from "./popular.js";
|
||||||
import { registerPowerpointToPdf } from "./powerpoint-to-pdf.js";
|
import { registerPowerpointToPdf } from "./powerpoint-to-pdf.js";
|
||||||
import { registerProtectPdf } from "./protect-pdf.js";
|
import { registerProtectPdf } from "./protect-pdf.js";
|
||||||
import { registerQrGenerate } from "./qr-generate.js";
|
import { registerQrGenerate } from "./qr-generate.js";
|
||||||
|
|||||||
@@ -1,12 +1,21 @@
|
|||||||
|
import { sql } from "drizzle-orm";
|
||||||
import type { FastifyInstance } from "fastify";
|
import type { FastifyInstance } from "fastify";
|
||||||
import { db } from "../../db/index.js";
|
import { db } from "../../db/index.js";
|
||||||
import { jobs } from "../../db/schema.js";
|
import { jobs } from "../../db/schema.js";
|
||||||
import { sql } from "drizzle-orm";
|
|
||||||
|
|
||||||
const DEFAULT_POPULAR = [
|
const DEFAULT_POPULAR = [
|
||||||
"resize", "crop", "compress", "convert", "remove-background",
|
"resize",
|
||||||
"upscale", "merge-pdf", "watermark-text", "compress-video",
|
"crop",
|
||||||
"trim-video", "convert-audio", "compress-pdf",
|
"compress",
|
||||||
|
"convert",
|
||||||
|
"remove-background",
|
||||||
|
"upscale",
|
||||||
|
"merge-pdf",
|
||||||
|
"watermark-text",
|
||||||
|
"compress-video",
|
||||||
|
"trim-video",
|
||||||
|
"convert-audio",
|
||||||
|
"compress-pdf",
|
||||||
];
|
];
|
||||||
|
|
||||||
export async function registerPopularTools(app: FastifyInstance) {
|
export async function registerPopularTools(app: FastifyInstance) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { defineConfig } from "astro/config";
|
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
|
||||||
import sitemap from "@astrojs/sitemap";
|
import sitemap from "@astrojs/sitemap";
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import { defineConfig } from "astro/config";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
site: "https://snapotter.com",
|
site: "https://snapotter.com",
|
||||||
|
|||||||
@@ -145,13 +145,15 @@ describe("Dropzone", () => {
|
|||||||
it("renders upload button and helper text", () => {
|
it("renders upload button and helper text", () => {
|
||||||
render(<Dropzone />);
|
render(<Dropzone />);
|
||||||
expect(screen.getByText("Upload from computer")).toBeDefined();
|
expect(screen.getByText("Upload from computer")).toBeDefined();
|
||||||
expect(screen.getByText("Drop your images here")).toBeDefined();
|
expect(screen.getByText("Drop your files here")).toBeDefined();
|
||||||
expect(screen.getByText("click anywhere to browse, or paste from clipboard")).toBeDefined();
|
expect(screen.getByText("click anywhere to browse, or paste from clipboard")).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows supported formats hint", () => {
|
it("shows supported formats hint", () => {
|
||||||
render(<Dropzone />);
|
render(<Dropzone />);
|
||||||
expect(screen.getByText("PNG, JPG, WebP, HEIC, RAW, PSD, and 65+ formats")).toBeDefined();
|
expect(
|
||||||
|
screen.getByText("Images, Videos, Audio, PDFs, Documents, and 150+ formats"),
|
||||||
|
).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders the drop zone section with aria label", () => {
|
it("renders the drop zone section with aria label", () => {
|
||||||
|
|||||||
@@ -1,158 +0,0 @@
|
|||||||
// @vitest-environment jsdom
|
|
||||||
import type React from "react";
|
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
let settingsState = {
|
|
||||||
fetch: vi.fn(),
|
|
||||||
defaultToolView: "sidebar" as "sidebar" | "fullscreen",
|
|
||||||
loaded: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
vi.mock("@/stores/settings-store", () => ({
|
|
||||||
useSettingsStore: () => settingsState,
|
|
||||||
}));
|
|
||||||
|
|
||||||
let fileState = {
|
|
||||||
setFiles: vi.fn(),
|
|
||||||
files: [] as File[],
|
|
||||||
reset: vi.fn(),
|
|
||||||
originalBlobUrl: null as string | null,
|
|
||||||
selectedFileName: null as string | null,
|
|
||||||
selectedFileSize: 0,
|
|
||||||
currentEntry: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
vi.mock("@/stores/file-store", () => ({
|
|
||||||
useFileStore: () => fileState,
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("@/stores/features-store", () => ({
|
|
||||||
useFeaturesStore: () => ({
|
|
||||||
fetch: vi.fn(),
|
|
||||||
bundles: [],
|
|
||||||
installing: {},
|
|
||||||
queued: [],
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("@/components/layout/app-layout", () => ({
|
|
||||||
AppLayout: ({ children }: React.PropsWithChildren) => (
|
|
||||||
<div data-testid="app-layout">{children}</div>
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("@/components/common/image-viewer", () => ({
|
|
||||||
ImageViewer: () => null,
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("@/components/common/multi-image-viewer", () => ({
|
|
||||||
MultiImageViewer: () => null,
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("@/lib/icon-map", () => ({
|
|
||||||
ICON_MAP: new Proxy(
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
get: () => (props: { className?: string }) => <span {...props} />,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
|
|
||||||
import { cleanup, render } from "@testing-library/react";
|
|
||||||
import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom";
|
|
||||||
import { HomePage } from "@/pages/home-page";
|
|
||||||
|
|
||||||
let currentPath = "/";
|
|
||||||
|
|
||||||
function LocationSpy() {
|
|
||||||
const location = useLocation();
|
|
||||||
currentPath = location.pathname;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderInRouter(initialPath = "/") {
|
|
||||||
currentPath = initialPath;
|
|
||||||
return render(
|
|
||||||
<MemoryRouter initialEntries={[initialPath]}>
|
|
||||||
<LocationSpy />
|
|
||||||
<Routes>
|
|
||||||
<Route path="/" element={<HomePage />} />
|
|
||||||
<Route path="/fullscreen" element={<div data-testid="fullscreen-page" />} />
|
|
||||||
</Routes>
|
|
||||||
</MemoryRouter>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
afterEach(cleanup);
|
|
||||||
|
|
||||||
// Tests are ordered intentionally: the module-level `hasAppliedDefaultRedirect`
|
|
||||||
// flag starts false and is set true by the "redirects on first mount" test.
|
|
||||||
// Tests before it verify conditions that independently prevent the redirect
|
|
||||||
// (so the flag stays false). The final test verifies the flag prevents repeat redirects.
|
|
||||||
describe("HomePage default view redirect", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
settingsState = {
|
|
||||||
fetch: vi.fn(),
|
|
||||||
defaultToolView: "sidebar",
|
|
||||||
loaded: false,
|
|
||||||
};
|
|
||||||
fileState = {
|
|
||||||
setFiles: vi.fn(),
|
|
||||||
files: [],
|
|
||||||
reset: vi.fn(),
|
|
||||||
originalBlobUrl: null,
|
|
||||||
selectedFileName: null,
|
|
||||||
selectedFileSize: 0,
|
|
||||||
currentEntry: null,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does NOT redirect when settings have not loaded yet", () => {
|
|
||||||
settingsState.loaded = false;
|
|
||||||
settingsState.defaultToolView = "fullscreen";
|
|
||||||
|
|
||||||
renderInRouter("/");
|
|
||||||
|
|
||||||
expect(currentPath).toBe("/");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does NOT redirect when default view is sidebar", () => {
|
|
||||||
settingsState.loaded = true;
|
|
||||||
settingsState.defaultToolView = "sidebar";
|
|
||||||
|
|
||||||
renderInRouter("/");
|
|
||||||
|
|
||||||
expect(currentPath).toBe("/");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does NOT redirect when files are loaded even with fullscreen default", () => {
|
|
||||||
settingsState.loaded = true;
|
|
||||||
settingsState.defaultToolView = "fullscreen";
|
|
||||||
fileState.files = [new File([new ArrayBuffer(1024)], "test.png", { type: "image/png" })];
|
|
||||||
fileState.originalBlobUrl = "blob:fake-url";
|
|
||||||
fileState.selectedFileName = "test.png";
|
|
||||||
fileState.selectedFileSize = 1024;
|
|
||||||
|
|
||||||
renderInRouter("/");
|
|
||||||
|
|
||||||
expect(currentPath).toBe("/");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("redirects to /fullscreen on first mount when default view is fullscreen", () => {
|
|
||||||
settingsState.loaded = true;
|
|
||||||
settingsState.defaultToolView = "fullscreen";
|
|
||||||
|
|
||||||
renderInRouter("/");
|
|
||||||
|
|
||||||
expect(currentPath).toBe("/fullscreen");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does NOT redirect on subsequent mounts after initial redirect (issue #128)", () => {
|
|
||||||
settingsState.loaded = true;
|
|
||||||
settingsState.defaultToolView = "fullscreen";
|
|
||||||
|
|
||||||
renderInRouter("/");
|
|
||||||
|
|
||||||
expect(currentPath).toBe("/");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -80,7 +80,7 @@ describe("en translation completeness", () => {
|
|||||||
expect(en.features.progressMessages).toHaveLength(30);
|
expect(en.features.progressMessages).toHaveLength(30);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("has 12 categories", () => {
|
it("has 24 categories", () => {
|
||||||
expect(Object.keys(en.categories)).toHaveLength(12);
|
expect(Object.keys(en.categories)).toHaveLength(24);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -375,7 +375,7 @@ describe("toolRegistry", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("tools with no-dropzone display mode have a ResultsPanel", () => {
|
it("tools with no-dropzone display mode have a ResultsPanel", () => {
|
||||||
const selfContainedTools = new Set(["meme-generator", "barcode-generate"]);
|
const selfContainedTools = new Set(["meme-generator", "barcode-generate", "chart-maker"]);
|
||||||
for (const [toolId, entry] of toolRegistry) {
|
for (const [toolId, entry] of toolRegistry) {
|
||||||
if (entry.displayMode === "no-dropzone" && !selfContainedTools.has(toolId)) {
|
if (entry.displayMode === "no-dropzone" && !selfContainedTools.has(toolId)) {
|
||||||
expect(entry.ResultsPanel, `missing ResultsPanel for ${toolId}`).toBeDefined();
|
expect(entry.ResultsPanel, `missing ResultsPanel for ${toolId}`).toBeDefined();
|
||||||
|
|||||||
Reference in New Issue
Block a user