842a8fe036
Deploy / deploy (push) Successful in 3m34s
"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>
117 lines
4.9 KiB
TypeScript
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);
|
|
});
|
|
});
|