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/internal-site/components/web-uploader/source-scanner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,77 @@ describe("scanBrowserUploadFiles", () => {
expect(plan.frames[0].misc).toHaveLength(1);
});

it("uses the first after label as the default heatmap reference for src/rip/flt sets", () => {
const plan = scanBrowserUploadFiles(
[
image("case/src/001.png"),
image("case/rip/001.png"),
Comment on lines +68 to +72
image("case/flt/001_crop.png"),
],
"case",
);

expect(plan.frames).toHaveLength(1);
expect(plan.frames[0].after.label).toBe("Rip");
expect(plan.heatmapReferenceLabel).toBe("Rip");
});

it("uses a common comparison label when the first frame after label is not shared", () => {
const plan = scanBrowserUploadFiles(
[
image("sample/frame-001_src.png"),
image("sample/frame-001_output.png"),
image("sample/frame-001_rip.png"),
image("sample/frame-002_src.png"),
image("sample/frame-002_rip.png"),
],
"sample",
);

expect(plan.frames.map((frame) => frame.after.label)).toEqual(["After", "Rip"]);
expect(plan.frames[0].misc.map((asset) => asset.label)).toEqual(["Rip"]);
expect(plan.heatmapReferenceLabel).toBe("Rip");
expect(plan.issues).toEqual([]);
});

it("reports an error when no heatmap reference label is shared by every frame", () => {
const plan = scanBrowserUploadFiles(
[
image("sample/frame-001_src.png"),
image("sample/frame-001_output.png"),
image("sample/frame-002_src.png"),
image("sample/frame-002_rip.png"),
],
"sample",
);

expect(plan.issues).toEqual(
expect.arrayContaining([
expect.objectContaining({
severity: "error",
code: "heatmap-reference-missing",
}),
]),
);
});

it("preserves nested out and output directory variants for primary selection", () => {
const plan = scanBrowserUploadFiles(
[
image("case/src/001.png"),
image("case/output/001.png"),
image("case/out/001.png"),
],
"case",
);

expect(plan.frames).toHaveLength(1);
expect(plan.frames[0].after.source.relativePath).toBe("out/001.png");
expect(plan.frames[0].misc.map((asset) => asset.source.relativePath)).toEqual([
"output/001.png",
]);
});

it("reports an error when a before file has no matching after file", () => {
const plan = scanBrowserUploadFiles(
[image("before/001.png"), image("before/002.png"), image("after/001.png")],
Expand Down
70 changes: 64 additions & 6 deletions apps/internal-site/components/web-uploader/source-scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ function assetPlan(kind: WebUploadAssetPlan["kind"], candidate: SourceCandidate)
kind === "before"
? "Before"
: kind === "after"
? "After"
? variantLabel(candidate.variant) || "After"
: kind === "heatmap"
? "Heatmap"
: variantLabel(candidate.variant) || "Misc";
Expand Down Expand Up @@ -548,6 +548,58 @@ function deriveGroupIdentity(sourceRootName: string, candidates: SourceCandidate
return { slug, title: titleCase(commonPrefix) || titleCase(sourceRootName) || "Uploaded Group" };
}

function orderedComparisonLabels(frame: WebUploadFramePlan) {
return [frame.after.label, ...frame.misc.map((asset) => asset.label)].filter(
(label, index, labels) => labels.indexOf(label) === index,
);
}

/**
* Mirrors the preview selector rule so the generated default is safe for every frame instead of
* only matching the first row's primary comparison column.
*/
function commonHeatmapReferenceLabels(frames: WebUploadFramePlan[]) {
if (frames.length === 0) {
return [];
}

const [firstFrame, ...remainingFrames] = frames;
const commonLabels = new Set(orderedComparisonLabels(firstFrame));
for (const frame of remainingFrames) {
const labels = new Set(orderedComparisonLabels(frame));
for (const label of [...commonLabels]) {
if (!labels.has(label)) {
commonLabels.delete(label);
}
}
}

return orderedComparisonLabels(firstFrame).filter((label) => commonLabels.has(label));
}

export function defaultHeatmapReferenceLabel(frames: WebUploadFramePlan[]) {
return commonHeatmapReferenceLabels(frames)[0] ?? "After";
}

/**
* Blocks generation when no global heatmap reference can exist; otherwise asset generation would
* fail later on the first row missing the stored reference label.
*/
function heatmapReferenceIssues(frames: WebUploadFramePlan[]): WebUploadIssue[] {
if (frames.length === 0 || commonHeatmapReferenceLabels(frames).length > 0) {
return [];
}

return [
{
code: "heatmap-reference-missing",
severity: "error",
path: frames[0].after.source.relativePath,
message: "没有所有帧都可用的 heatmap 参考列,请统一比较列后再上传。",
},
];
}

function parseEntries(entries: BrowserUploadFile[], variantOverride?: string) {
const candidates: SourceCandidate[] = [];
const ignored: IgnoredUploadFile[] = [];
Expand Down Expand Up @@ -601,17 +653,23 @@ function buildFlatPlan(sourceRootName: string, entries: BrowserUploadFile[], ign
suggestedGroupSlug: identity.slug,
suggestedGroupTitle: identity.title,
frames,
heatmapReferenceLabel: "After",
heatmapReferenceLabel: defaultHeatmapReferenceLabel(frames),
ignoredFiles: [...ignoredFiles, ...parsed.ignored],
issues,
issues: [...issues, ...heatmapReferenceIssues(frames)],
};
}

function buildNestedPlan(sourceRootName: string, entries: BrowserUploadFile[], ignoredFiles: IgnoredUploadFile[], layout: NonFlatLayout): WebUploadPlan {
const scopedEntries = (directory: string | null) =>
directory ? entries.filter((entry) => topLevelDirectory(entry.relativePath) === directory) : [];
const beforeParsed = parseEntries(scopedEntries(layout.beforeDir), "source");
const afterParsedResults = layout.afterDirs.map((directory, index) => parseEntries(scopedEntries(directory), index === 0 ? "out" : basename(directory).toLowerCase()));
const afterParsedResults = layout.afterDirs.map((directory) => {
const directoryVariant = basename(directory).toLowerCase();
return parseEntries(
scopedEntries(directory),
directoryVariant,
);
});
const heatmapParsedResults = layout.heatmapDirs.map((directory) => parseEntries(scopedEntries(directory), "heatmap"));
const miscParsedResults = layout.miscDirs.map((directory) => parseEntries(scopedEntries(directory), basename(directory).toLowerCase() || "misc"));
const afterParsed = afterParsedResults.flatMap((result) => result.candidates);
Expand Down Expand Up @@ -689,9 +747,9 @@ function buildNestedPlan(sourceRootName: string, entries: BrowserUploadFile[], i
suggestedGroupSlug: identity.slug,
suggestedGroupTitle: identity.title,
frames,
heatmapReferenceLabel: "After",
heatmapReferenceLabel: defaultHeatmapReferenceLabel(frames),
ignoredFiles: ignored,
issues,
issues: [...issues, ...heatmapReferenceIssues(frames)],
};
}

Expand Down
Loading