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); }); });