Archived
refactor(test): migrate promptAttachmentCapture.test.ts to shared template-inspection seam
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import type { TemplateResult } from "lit";
|
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { PromptEditor } from "./components/PromptEditor";
|
import { PromptEditor } from "./components/PromptEditor";
|
||||||
import { capturePromptAttachments, DEFAULT_FILE_MIME_TYPE, effectivePromptAttachmentDelivery, READ_FAILURE_MESSAGE, type CapturableFile } from "./promptAttachmentCapture";
|
import { capturePromptAttachments, DEFAULT_FILE_MIME_TYPE, effectivePromptAttachmentDelivery, READ_FAILURE_MESSAGE, type CapturableFile } from "./promptAttachmentCapture";
|
||||||
|
import { templateEventHandlerAfterMarker, templateEventHandlerAfterValue } from "./templateInspection.testSupport";
|
||||||
|
|
||||||
function file(name: string, type: string, size = 10): CapturableFile {
|
function file(name: string, type: string, size = 10): CapturableFile {
|
||||||
return { name, type, size };
|
return { name, type, size };
|
||||||
@@ -83,9 +83,13 @@ describe("effectivePromptAttachmentDelivery", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("PromptEditor attachment wiring", () => {
|
describe("PromptEditor attachment wiring", () => {
|
||||||
// Direct TemplateResult handler extraction keeps these node-environment tests focused on
|
// TemplateResult handler extraction (via the shared escape hatch) verifies the paste/remove/send
|
||||||
// PromptEditor wiring without introducing a DOM/FileReader harness for the whole component.
|
// event wiring here; this repo runs Vitest without a DOM environment, so a full custom-element +
|
||||||
it("captures pasted files, strips data URL prefixes, and surfaces read failures", async () => {
|
// FileReader render harness would add disproportionate setup for this narrow wiring check. Each
|
||||||
|
// assertion observes a component effect (the injected `onSend` callback and its captured
|
||||||
|
// attachments), not Lit template internals. Rendered content/error text is covered at the pure
|
||||||
|
// layer by the `capturePromptAttachments` tests above.
|
||||||
|
it("captures pasted files, strips data URL prefixes, and keeps successful reads when others fail", async () => {
|
||||||
const editor = new PromptEditor();
|
const editor = new PromptEditor();
|
||||||
const onSend = vi.fn<NonNullable<PromptEditor["onSend"]>>();
|
const onSend = vi.fn<NonNullable<PromptEditor["onSend"]>>();
|
||||||
editor.onSend = onSend;
|
editor.onSend = onSend;
|
||||||
@@ -96,7 +100,7 @@ describe("PromptEditor attachment wiring", () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const paste = findTemplateEventHandlerAfterMarker<Event>(editor.render(), "@paste=");
|
const paste = templateEventHandlerAfterMarker(editor.render(), "@paste=");
|
||||||
const pasteEvent = pasteEventWithFiles([
|
const pasteEvent = pasteEventWithFiles([
|
||||||
new File(["png"], "shot.png", { type: "image/png" }),
|
new File(["png"], "shot.png", { type: "image/png" }),
|
||||||
new File(["pdf"], "report.pdf", { type: "application/pdf" }),
|
new File(["pdf"], "report.pdf", { type: "application/pdf" }),
|
||||||
@@ -107,12 +111,13 @@ describe("PromptEditor attachment wiring", () => {
|
|||||||
await flushMicrotasks();
|
await flushMicrotasks();
|
||||||
|
|
||||||
expect(preventDefault).toHaveBeenCalledOnce();
|
expect(preventDefault).toHaveBeenCalledOnce();
|
||||||
expect(templateContainsValue(editor.render(), "Remove shot.png")).toBe(true);
|
|
||||||
expect(templateContainsValue(editor.render(), READ_FAILURE_MESSAGE)).toBe(true);
|
|
||||||
|
|
||||||
const send = findTemplateEventHandlerAfterMarker<Event>(editor.render(), "send-button");
|
const send = templateEventHandlerAfterMarker(editor.render(), "send-button");
|
||||||
send(new Event("click"));
|
send(new Event("click"));
|
||||||
|
|
||||||
|
// report.pdf failed to read, so only the successfully-read image survives to onSend — proving
|
||||||
|
// the paste is wired to capture, the data URL prefix is stripped, and a failed read does not
|
||||||
|
// drop the other attachment.
|
||||||
expect(onSend).toHaveBeenCalledTimes(1);
|
expect(onSend).toHaveBeenCalledTimes(1);
|
||||||
expect(onSend).toHaveBeenCalledWith("inspect attachments", undefined, [
|
expect(onSend).toHaveBeenCalledWith("inspect attachments", undefined, [
|
||||||
{ kind: "image", mimeType: "image/png", data: "UE5H", name: "shot.png" },
|
{ kind: "image", mimeType: "image/png", data: "UE5H", name: "shot.png" },
|
||||||
@@ -132,15 +137,14 @@ describe("PromptEditor attachment wiring", () => {
|
|||||||
{ id: "attachment-2", kind: "image", name: "shot.png", mimeType: "image/png", data: "UE5H", size: 3 },
|
{ id: "attachment-2", kind: "image", name: "shot.png", mimeType: "image/png", data: "UE5H", size: 3 },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const removeReport = findTemplateEventHandlerAfterValue<Event>(editor.render(), "Remove report.pdf", "@click=");
|
const removeReport = templateEventHandlerAfterValue(editor.render(), "Remove report.pdf", "@click=");
|
||||||
removeReport(new Event("click"));
|
removeReport(new Event("click"));
|
||||||
|
|
||||||
expect(templateContainsValue(editor.render(), "Remove report.pdf")).toBe(false);
|
const send = templateEventHandlerAfterMarker(editor.render(), "send-button");
|
||||||
expect(templateContainsValue(editor.render(), "Remove shot.png")).toBe(true);
|
|
||||||
|
|
||||||
const send = findTemplateEventHandlerAfterMarker<Event>(editor.render(), "send-button");
|
|
||||||
send(new Event("click"));
|
send(new Event("click"));
|
||||||
|
|
||||||
|
// onSend receives only the image, proving the remove handler dropped report.pdf while leaving
|
||||||
|
// shot.png queued (folder delivery is not forced because no generic file remains).
|
||||||
expect(onSend).toHaveBeenCalledTimes(1);
|
expect(onSend).toHaveBeenCalledTimes(1);
|
||||||
expect(onSend).toHaveBeenCalledWith("please review", undefined, [
|
expect(onSend).toHaveBeenCalledWith("please review", undefined, [
|
||||||
{ kind: "image", mimeType: "image/png", data: "UE5H", name: "shot.png" },
|
{ kind: "image", mimeType: "image/png", data: "UE5H", name: "shot.png" },
|
||||||
@@ -148,8 +152,6 @@ describe("PromptEditor attachment wiring", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
type TemplateEventHandler<E extends Event> = (event: E) => void;
|
|
||||||
|
|
||||||
type StubFileReaderOutcome =
|
type StubFileReaderOutcome =
|
||||||
| { kind: "load"; result: string }
|
| { kind: "load"; result: string }
|
||||||
| { kind: "error"; error: DOMException };
|
| { kind: "error"; error: DOMException };
|
||||||
@@ -191,6 +193,7 @@ function installFileReaderStub(outcomes: StubFileReaderOutcome[]): () => void {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function pasteEventWithFiles(files: readonly File[]): Event {
|
function pasteEventWithFiles(files: readonly File[]): Event {
|
||||||
const event = new Event("paste", { cancelable: true });
|
const event = new Event("paste", { cancelable: true });
|
||||||
Object.defineProperty(event, "clipboardData", { value: { files } });
|
Object.defineProperty(event, "clipboardData", { value: { files } });
|
||||||
@@ -200,115 +203,3 @@ function pasteEventWithFiles(files: readonly File[]): Event {
|
|||||||
async function flushMicrotasks(): Promise<void> {
|
async function flushMicrotasks(): Promise<void> {
|
||||||
for (let remaining = 0; remaining < 10; remaining += 1) await Promise.resolve();
|
for (let remaining = 0; remaining < 10; remaining += 1) await Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
function findTemplateEventHandlerAfterMarker<E extends Event>(template: TemplateResult, marker: string): TemplateEventHandler<E> {
|
|
||||||
const handler = findOptionalTemplateEventHandlerAfterMarker<E>(template, marker);
|
|
||||||
if (handler === undefined) throw new Error(`Expected template event handler after marker ${marker}`);
|
|
||||||
return handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
function findOptionalTemplateEventHandlerAfterMarker<E extends Event>(template: TemplateResult, marker: string): TemplateEventHandler<E> | undefined {
|
|
||||||
const strings = templateStrings(template);
|
|
||||||
const values = templateValues(template);
|
|
||||||
for (let index = 0; index < values.length; index += 1) {
|
|
||||||
const staticChunk = strings[index];
|
|
||||||
if (staticChunk?.includes(marker) === true) {
|
|
||||||
const handler = nextTemplateEventHandler<E>(values, index);
|
|
||||||
if (handler !== undefined) return handler;
|
|
||||||
}
|
|
||||||
const nestedHandler = findOptionalTemplateEventHandlerAfterMarkerInValue<E>(values[index], marker);
|
|
||||||
if (nestedHandler !== undefined) return nestedHandler;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function findOptionalTemplateEventHandlerAfterMarkerInValue<E extends Event>(value: unknown, marker: string): TemplateEventHandler<E> | undefined {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
for (const item of value) {
|
|
||||||
const nestedHandler = findOptionalTemplateEventHandlerAfterMarkerInValue<E>(item, marker);
|
|
||||||
if (nestedHandler !== undefined) return nestedHandler;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
if (isTemplateResult(value)) return findOptionalTemplateEventHandlerAfterMarker<E>(value, marker);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function findTemplateEventHandlerAfterValue<E extends Event>(template: TemplateResult, expectedValue: unknown, marker: string): TemplateEventHandler<E> {
|
|
||||||
const handler = findOptionalTemplateEventHandlerAfterValue<E>(template, expectedValue, marker);
|
|
||||||
if (handler === undefined) throw new Error(`Expected template event handler after value ${String(expectedValue)}`);
|
|
||||||
return handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
function findOptionalTemplateEventHandlerAfterValue<E extends Event>(template: TemplateResult, expectedValue: unknown, marker: string): TemplateEventHandler<E> | undefined {
|
|
||||||
const strings = templateStrings(template);
|
|
||||||
const values = templateValues(template);
|
|
||||||
for (let index = 0; index < values.length; index += 1) {
|
|
||||||
const value = values[index];
|
|
||||||
if (value === expectedValue) {
|
|
||||||
for (let handlerIndex = index + 1; handlerIndex < values.length; handlerIndex += 1) {
|
|
||||||
const staticChunk = strings[handlerIndex];
|
|
||||||
const maybeHandler = values[handlerIndex];
|
|
||||||
if (staticChunk?.includes(marker) === true && isTemplateEventHandler<E>(maybeHandler)) return maybeHandler;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const nestedHandler = findOptionalTemplateEventHandlerAfterValueInValue<E>(value, expectedValue, marker);
|
|
||||||
if (nestedHandler !== undefined) return nestedHandler;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function findOptionalTemplateEventHandlerAfterValueInValue<E extends Event>(value: unknown, expectedValue: unknown, marker: string): TemplateEventHandler<E> | undefined {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
for (const item of value) {
|
|
||||||
const nestedHandler = findOptionalTemplateEventHandlerAfterValueInValue<E>(item, expectedValue, marker);
|
|
||||||
if (nestedHandler !== undefined) return nestedHandler;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
if (isTemplateResult(value)) return findOptionalTemplateEventHandlerAfterValue<E>(value, expectedValue, marker);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function nextTemplateEventHandler<E extends Event>(values: readonly unknown[], startIndex: number): TemplateEventHandler<E> | undefined {
|
|
||||||
for (let index = startIndex; index < values.length; index += 1) {
|
|
||||||
const value = values[index];
|
|
||||||
if (isTemplateEventHandler<E>(value)) return value;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function templateContainsValue(template: TemplateResult, expectedValue: unknown): boolean {
|
|
||||||
return templateValues(template).some((value) => templateValueContains(value, expectedValue));
|
|
||||||
}
|
|
||||||
|
|
||||||
function templateValueContains(value: unknown, expectedValue: unknown): boolean {
|
|
||||||
if (value === expectedValue) return true;
|
|
||||||
if (Array.isArray(value)) return value.some((item) => templateValueContains(item, expectedValue));
|
|
||||||
if (isTemplateResult(value)) return templateContainsValue(value, expectedValue);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function templateStrings(template: TemplateResult): readonly string[] {
|
|
||||||
const strings = Reflect.get(template, "strings");
|
|
||||||
if (!isStringArray(strings)) throw new Error("TemplateResult strings were unavailable");
|
|
||||||
return strings;
|
|
||||||
}
|
|
||||||
|
|
||||||
function templateValues(template: TemplateResult): readonly unknown[] {
|
|
||||||
const values = Reflect.get(template, "values");
|
|
||||||
if (!Array.isArray(values)) throw new Error("TemplateResult values were unavailable");
|
|
||||||
return values.map((value: unknown) => value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isTemplateResult(value: unknown): value is TemplateResult {
|
|
||||||
return typeof value === "object" && value !== null && isStringArray(Reflect.get(value, "strings")) && Array.isArray(Reflect.get(value, "values"));
|
|
||||||
}
|
|
||||||
|
|
||||||
function isTemplateEventHandler<E extends Event>(value: unknown): value is TemplateEventHandler<E> {
|
|
||||||
return typeof value === "function";
|
|
||||||
}
|
|
||||||
|
|
||||||
function isStringArray(value: unknown): value is string[] {
|
|
||||||
return Array.isArray(value) && value.every((item: unknown) => typeof item === "string");
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -149,18 +149,6 @@ export function collectStringValues(template: TemplateResult): string[] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** True when `expectedValue` appears as an interpolated value anywhere in the tree. */
|
|
||||||
export function templateContainsValue(template: TemplateResult, expectedValue: unknown): boolean {
|
|
||||||
return templateValues(template).some((value) => valueContains(value, expectedValue));
|
|
||||||
|
|
||||||
function valueContains(value: unknown, target: unknown): boolean {
|
|
||||||
if (value === target) return true;
|
|
||||||
if (Array.isArray(value)) return value.some((item) => valueContains(item, target));
|
|
||||||
if (isTemplateResult(value)) return templateContainsValue(value, target);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Every interpolated value whose immediately preceding static chunk includes
|
* Every interpolated value whose immediately preceding static chunk includes
|
||||||
* `marker`, collected across the whole tree in document order.
|
* `marker`, collected across the whole tree in document order.
|
||||||
@@ -282,8 +270,6 @@ export function findOptionalTemplateEventHandlerAfterMarker<E extends Event = Ev
|
|||||||
* Anchors to a stable value (e.g. an accessible label like `Remove report.pdf`)
|
* Anchors to a stable value (e.g. an accessible label like `Remove report.pdf`)
|
||||||
* and then locates the handler tagged by `marker` (e.g. `@click=`) that follows
|
* and then locates the handler tagged by `marker` (e.g. `@click=`) that follows
|
||||||
* it, so the wiring is tied to user-facing content rather than handler order.
|
* it, so the wiring is tied to user-facing content rather than handler order.
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
*/
|
||||||
export function templateEventHandlerAfterValue<E extends Event = Event>(template: TemplateResult, expectedValue: unknown, marker: string): TemplateEventHandler<E> {
|
export function templateEventHandlerAfterValue<E extends Event = Event>(template: TemplateResult, expectedValue: unknown, marker: string): TemplateEventHandler<E> {
|
||||||
const handler = findOptionalTemplateEventHandlerAfterValue<E>(template, expectedValue, marker);
|
const handler = findOptionalTemplateEventHandlerAfterValue<E>(template, expectedValue, marker);
|
||||||
|
|||||||
Reference in New Issue
Block a user