Files
vfxreview/lib/feedback-parse.test.ts
T
twotalesanimation 842a8fe036
Deploy / deploy (push) Successful in 3m34s
feat(board): paste client feedback from WhatsApp
"Paste feedback" on each board card takes a client's message as it arrived
and splits it into rounds ("FEEDBACK 113 DUE Oct 7 12:00 PM") and per-shot
notes, tolerating WhatsApp copy prefixes, bold markers, wrapped lines,
several codes per note and codes without the show prefix.

A review step shows each note's shot, the version the client was sent,
the player frame and a "move task to Changes" tick (on by default).
Saving creates a FeedbackRound, a player comment per note tagged
source=WHATSAPP (shown as "Client feedback") and a board line under the
round's due time; ticking the line off resolves the comment.

Quoted frames are read as comp frames: shot frameStart or the house 1001,
plus the slate (recorded, else 1 for pipeline previews). Version.duration
is never stored on live, so the length-based slate inference can't be used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 17:31:34 +02:00

117 lines
4.9 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { parseDue, parseFeedback, slateFrames, toPlayerFrame } from "./feedback-parse";
describe("parseFeedback", () => {
it("splits rounds and notes from the doc's own format", () => {
const { rounds, ignored } = parseFeedback(`FEEDBACK 117 DUE Oct 7, 2026 12:00 PM
UNG_117_023_010 The screen comp starts warping, slightly expanding and contracting in the second half of the shot.
UNG_117_024_010 48fps @ 200% Done Dineo seems slightly too small
FEEDBACK 113 DUE Oct 7, 2026 12:00 PM
UNG_113_008_040 The screen is partially peeking under the mask on OZ
UNG_113_018_050 At frame 1018, the mask is still slipping`);
expect(ignored).toEqual([]);
expect(rounds.map((r) => [r.title, r.dueText, r.notes.length])).toEqual([
["FEEDBACK 117", "Oct 7, 2026 12:00 PM", 2],
["FEEDBACK 113", "Oct 7, 2026 12:00 PM", 2],
]);
expect(rounds[1].notes[1]).toEqual({
codes: ["UNG_113_018_050"],
text: "At frame 1018, the mask is still slipping",
frame: 1018,
});
expect(rounds[0].notes[1].frame).toBeNull();
});
it("joins wrapped lines onto the note above, even ones mentioning changes or due", () => {
const { rounds } = parseFeedback(`UNG_108_004_030 Comp needs work.
Visible matte edge on left of "Top Story" banner.
lighting changes, due to the grade`);
expect(rounds).toHaveLength(1);
expect(rounds[0].title).toBe("");
expect(rounds[0].notes[0].text).toBe(
'Comp needs work. Visible matte edge on left of "Top Story" banner. lighting changes, due to the grade',
);
});
it("strips WhatsApp prefixes and formatting, and keeps preamble aside", () => {
const { rounds, ignored } = parseFeedback(`[07/10/2026, 09:12] Stephen Abbott: Hi team, notes below
[07/10/2026, 09:13] Stephen Abbott: *UNG_113_031_010* As the camera pans the screen comp seems to shrink`);
expect(rounds[0].title).toBe("");
expect(rounds[0].notes[0].codes).toEqual(["UNG_113_031_010"]);
expect(rounds[0].notes[0].text).toBe("As the camera pans the screen comp seems to shrink");
expect(ignored).toEqual(["Hi team, notes below"]);
const plain = parseFeedback("Morning!\nUNG_113_031_010 shrink");
expect(plain.ignored).toEqual(["Morning!"]);
});
it("gives one note per shot when several codes lead the line", () => {
const { rounds } = parseFeedback("UNG_113_032_034 / UNG_113_032_036, UNG_113_032_038 same matte fix");
expect(rounds[0].notes.map((n) => [n.codes[0], n.text])).toEqual([
["UNG_113_032_034", "same matte fix"],
["UNG_113_032_036", "same matte fix"],
["UNG_113_032_038", "same matte fix"],
]);
});
it("accepts codes without the show prefix and upper-cases them", () => {
const { rounds } = parseFeedback("- 113_018_040: hard blue edge, fr 1004");
expect(rounds[0].notes[0]).toMatchObject({ codes: ["113_018_040"], text: "hard blue edge, fr 1004", frame: 1004 });
});
});
describe("parseDue", () => {
const now = new Date(2026, 9, 7, 8, 0);
it("reads a full date and time", () => {
const d = parseDue("Oct 7, 2026 12:00 PM", now)!;
expect([d.getFullYear(), d.getMonth(), d.getDate(), d.getHours()]).toEqual([2026, 9, 7, 12]);
});
it("fills in the year", () => {
const d = parseDue("7 Oct 16:00", now)!;
expect([d.getFullYear(), d.getMonth(), d.getDate(), d.getHours()]).toEqual([2026, 9, 7, 16]);
});
it("returns null for what it cannot read", () => {
expect(parseDue("end of day", now)).toBeNull();
expect(parseDue(null, now)).toBeNull();
});
});
describe("slateFrames", () => {
it("uses the recorded slate, else 1 for pipeline previews, else 0", () => {
expect(slateFrames({ pictureStartFrame: 2, frameCount: 120 })).toBe(2);
expect(slateFrames({ pictureStartFrame: 0, frameCount: 120 })).toBe(0);
expect(slateFrames({ pictureStartFrame: null, frameCount: 120 })).toBe(1);
expect(slateFrames({ pictureStartFrame: null, frameCount: null })).toBe(0);
});
});
describe("toPlayerFrame", () => {
it("maps a comp frame through the shot start and slate", () => {
expect(toPlayerFrame(1018, { shotFrameStart: 1001, pictureStart: 1, frameCount: 120 })).toEqual({
frame: 18,
fromComp: true,
});
});
it("assumes the house 1001 start when the shot has none", () => {
expect(toPlayerFrame(1018, { shotFrameStart: null, pictureStart: 1, frameCount: 120 }).frame).toBe(18);
expect(toPlayerFrame(1018, { shotFrameStart: 0, pictureStart: 0, frameCount: null }).frame).toBe(17);
});
it("takes a small number as the player's own frame", () => {
expect(toPlayerFrame(40, { shotFrameStart: 1001, pictureStart: 1, frameCount: 120 })).toEqual({
frame: 40,
fromComp: false,
});
});
it("keeps the frame inside the picture", () => {
expect(toPlayerFrame(1500, { shotFrameStart: 1001, pictureStart: 1, frameCount: 100 }).frame).toBe(100);
expect(toPlayerFrame(500, { shotFrameStart: null, pictureStart: 0, frameCount: 100 }).frame).toBe(99);
});
});