Fix generated project previews rejected by outdated size validation

This commit is contained in:
webadderall
2026-09-23 11:52:21 +10:00
parent 93f155395b
commit 026e8d8493
5 changed files with 57 additions and 7 deletions
@@ -4,7 +4,7 @@ import path from "node:path";
import { expect, it } from "vitest";
import { hasFreshProjectThumbnail } from "./thumbnailFreshness";
it("rejects legacy, stale, missing and broken previews while accepting a fresh high-resolution PNG", async () => {
it("rejects legacy, stale, missing and broken previews while accepting a fresh current-size or larger PNG", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "recordly-preview-"));
const file = path.join(dir, "preview.png");
try {
@@ -15,6 +15,10 @@ it("rejects legacy, stale, missing and broken previews while accepting a fresh h
header.writeUInt32BE(180, 20);
await fs.writeFile(file, header);
expect(await hasFreshProjectThumbnail(file, 0)).toBe(false);
header.writeUInt32BE(640, 16);
header.writeUInt32BE(480, 20);
await fs.writeFile(file, header);
expect(await hasFreshProjectThumbnail(file, 0)).toBe(true);
header.writeUInt32BE(1600, 16);
header.writeUInt32BE(1200, 20);
await fs.writeFile(file, header);
+7 -3
View File
@@ -1,7 +1,11 @@
import fs from "node:fs/promises";
import {
PROJECT_THUMBNAIL_WIDTH,
PROJECT_THUMBNAIL_HEIGHT,
} from "../../../src/lib/projectThumbnail";
// Older releases wrote 320px previews. Hide those and previews predating edits;
// the editor replaces them with a full-resolution render when returning home.
// the editor replaces them with a current-size render when returning home.
export async function hasFreshProjectThumbnail(thumbnailPath: string, projectModifiedAt: number) {
let file: Awaited<ReturnType<typeof fs.open>> | undefined;
try {
@@ -13,8 +17,8 @@ export async function hasFreshProjectThumbnail(thumbnailPath: string, projectMod
return (
bytesRead === 24 &&
header.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])) &&
header.readUInt32BE(16) >= 1600 &&
header.readUInt32BE(20) >= 1200
header.readUInt32BE(16) >= PROJECT_THUMBNAIL_WIDTH &&
header.readUInt32BE(20) >= PROJECT_THUMBNAIL_HEIGHT
);
} catch {
return false;
@@ -1,5 +1,6 @@
/* biome-ignore-all lint/correctness/useExhaustiveDependencies: grouped editor domain objects contain the thumbnail renderer dependencies. */
import { type RefObject, useCallback, useEffect, useRef } from "react";
import { PROJECT_THUMBNAIL_WIDTH, PROJECT_THUMBNAIL_HEIGHT } from "@/lib/projectThumbnail";
import { FrameRenderer } from "@/lib/exporter/frameRenderer";
import { toFileUrl } from "../projectPersistence";
import type { useAppearanceState } from "../state/useAppearanceState";
@@ -112,8 +113,8 @@ export function useProjectLibraryController({
}
const canvas = document.createElement("canvas");
const targetWidth = 640;
const targetHeight = 480;
const targetWidth = PROJECT_THUMBNAIL_WIDTH;
const targetHeight = PROJECT_THUMBNAIL_HEIGHT;
canvas.width = targetWidth;
canvas.height = targetHeight;
+3
View File
@@ -0,0 +1,3 @@
/** Shared by thumbnail generation and the persisted-preview freshness check. */
export const PROJECT_THUMBNAIL_WIDTH = 640;
export const PROJECT_THUMBNAIL_HEIGHT = 480;
+39 -1
View File
@@ -1,3 +1,6 @@
import fs from "node:fs/promises";
import path from "node:path";
import { hasFreshProjectThumbnail } from "../../electron/ipc/project/thumbnailFreshness";
import { expect, test } from "@playwright/test";
import { installDesktopBridge } from "./bridge";
@@ -140,8 +143,30 @@ test("home dashboard explains an empty library", async ({ page }) => {
test("autosave creates one untitled project, stays idle without edits, and refreshes its preview on exit without a saved toast", async ({
page,
}) => {
}, testInfo) => {
await installDesktopBridge(page);
await page.addInitScript(() => {
const save = window.electronAPI.saveProjectFile;
window.electronAPI.saveProjectFile = async (...args) => {
const result = await save(...args);
if (args[3]) document.documentElement.dataset.savedThumbnail = args[3];
return result;
};
window.electronAPI.listProjectFiles = async () => ({
success: true,
projects: [],
entries: [
{
path: "/projects/preview.recordly",
name: "Generated preview",
updatedAt: 1,
thumbnailPath: document.documentElement.dataset.savedThumbnail ?? null,
isCurrent: true,
isInProjectsDirectory: true,
},
],
});
});
await page.goto("/?windowType=editor");
await expect(page.locator("html")).toHaveAttribute("data-project-creates", "1");
await expect(page.getByRole("button", { name: "Rename project" })).toContainText(
@@ -158,6 +183,19 @@ test("autosave creates one untitled project, stays idle without edits, and refre
"data-saved-thumbnail",
/^data:image\/png;base64,/,
);
const thumbnail = await page.locator("html").getAttribute("data-saved-thumbnail");
const file = testInfo.outputPath("generated-preview.png");
await fs.mkdir(path.dirname(file), { recursive: true });
await fs.writeFile(file, Buffer.from(thumbnail!.split(",")[1], "base64"));
// Exercise the actual main-process acceptance check against renderer output.
expect(await hasFreshProjectThumbnail(file, 0)).toBe(true);
const image = page
.getByRole("button", { name: "Generated preview", exact: true })
.locator("img");
await expect(image).toBeVisible();
await expect
.poll(() => image.evaluate((element: HTMLImageElement) => element.naturalWidth))
.toBe(640);
await expect(page.getByText(/Project saved/)).toHaveCount(0);
});