Rebuild downloads, link collection, settings, history, and statistics around a responsive desktop shell with compact account and queue tables, contextual navigation, persistent update affordances, unified overlays, and accessible keyboard interactions. Add safe history-folder reveal IPC, responsive 2560/1920/1366/1120 coverage, deterministic visual fixtures, focused component regressions, and release-tree exclusions for internal working files. Bump the public application version to 2.0.13.
306 lines
10 KiB
TypeScript
306 lines
10 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { describe, expect, it } from "vitest";
|
|
import { VISUAL_SCENARIOS } from "./visual/fixtures";
|
|
import {
|
|
prepareVisualCapture,
|
|
validateVisualCaptureManifest,
|
|
type VisualCapture
|
|
} from "./visual/ui-driver";
|
|
|
|
const validCapture: VisualCapture = {
|
|
name: "region-contract",
|
|
scenario: "dense",
|
|
viewport: { width: 2560, height: 1369 },
|
|
activeView: "downloads",
|
|
interactions: [],
|
|
assertions: [{ type: "visible", region: "downloads-table-body" }]
|
|
};
|
|
|
|
interface FakeElementOptions {
|
|
role?: string;
|
|
name?: string;
|
|
region?: string;
|
|
className?: string;
|
|
visible?: boolean;
|
|
current?: boolean;
|
|
}
|
|
|
|
class FakeElement {
|
|
readonly nodeType = 1;
|
|
readonly tagName = "DIV";
|
|
readonly dataset: Record<string, string> = {};
|
|
readonly style = { display: "", visibility: "", opacity: "", zIndex: "auto" };
|
|
readonly className: string;
|
|
readonly role?: string;
|
|
readonly name?: string;
|
|
readonly region?: string;
|
|
readonly visible: boolean;
|
|
readonly current: boolean;
|
|
textContent = "content";
|
|
hidden = false;
|
|
readonly classList: { contains: (name: string) => boolean };
|
|
|
|
constructor(options: FakeElementOptions) {
|
|
this.role = options.role;
|
|
this.name = options.name;
|
|
this.region = options.region;
|
|
this.className = options.className ?? "";
|
|
this.visible = options.visible ?? true;
|
|
this.current = options.current ?? false;
|
|
this.classList = {
|
|
contains: (name: string): boolean => this.className.split(/\s+/).includes(name)
|
|
};
|
|
if (this.region) {
|
|
this.dataset.visualRegion = this.region;
|
|
}
|
|
}
|
|
|
|
getAttribute(name: string): string | null {
|
|
if (name === "role") return this.role ?? null;
|
|
if (name === "aria-label") return this.name ?? null;
|
|
if (name === "data-visual-region") return this.region ?? null;
|
|
if (name === "aria-hidden") return this.visible ? null : "true";
|
|
if (name === "aria-current" && this.current) return "page";
|
|
return null;
|
|
}
|
|
|
|
hasAttribute(name: string): boolean {
|
|
return name === "hidden" ? this.hidden : this.getAttribute(name) !== null;
|
|
}
|
|
|
|
getClientRects(): { length: number } {
|
|
return { length: this.visible ? 1 : 0 };
|
|
}
|
|
|
|
querySelectorAll(): FakeElement[] {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function createFakeDocument(
|
|
elements: FakeElement[],
|
|
navigationName = "Downloads"
|
|
): Document {
|
|
const navigation = new FakeElement({
|
|
role: "button",
|
|
name: navigationName,
|
|
className: "tab",
|
|
current: true
|
|
});
|
|
Object.assign(navigation, {
|
|
tagName: "BUTTON",
|
|
click(): void {
|
|
return undefined;
|
|
},
|
|
focus(): void {
|
|
return undefined;
|
|
},
|
|
dispatchEvent(): boolean {
|
|
return true;
|
|
}
|
|
});
|
|
const all = [navigation, ...elements];
|
|
class FakeEvent {
|
|
constructor(readonly type: string) {}
|
|
}
|
|
return {
|
|
defaultView: {
|
|
Event: FakeEvent,
|
|
MouseEvent: FakeEvent,
|
|
KeyboardEvent: FakeEvent,
|
|
requestAnimationFrame(callback: FrameRequestCallback): number {
|
|
callback(0);
|
|
return 1;
|
|
},
|
|
getComputedStyle(element: FakeElement) {
|
|
return {
|
|
display: element.visible ? "block" : "none",
|
|
visibility: element.visible ? "visible" : "hidden",
|
|
opacity: element.visible ? "1" : "0",
|
|
zIndex: element.style.zIndex
|
|
};
|
|
}
|
|
},
|
|
querySelectorAll(selector: string): FakeElement[] {
|
|
if (selector === "[data-visual-region]") {
|
|
return all.filter((element) => element.region !== undefined);
|
|
}
|
|
return all;
|
|
},
|
|
querySelector(): FakeElement | null {
|
|
return null;
|
|
}
|
|
} as unknown as Document;
|
|
}
|
|
|
|
describe("reference capture manifest", () => {
|
|
const manifest = JSON.parse(readFileSync(new URL("./visual/capture-manifest.json", import.meta.url), "utf8"));
|
|
|
|
it("defines executable dense, collector, avatar, context and info captures", () => {
|
|
expect(VISUAL_SCENARIOS).toEqual(["empty", "dense", "update"]);
|
|
expect(validateVisualCaptureManifest(manifest)).toEqual([]);
|
|
expect(manifest.map((entry: { name: string }) => entry.name)).toEqual(expect.arrayContaining([
|
|
"downloads-dense",
|
|
"app-navigation-current",
|
|
"collector-dense",
|
|
"settings-dense",
|
|
"history-dense",
|
|
"statistics-dense",
|
|
"avatar-menu",
|
|
"avatar-update-tooltip",
|
|
"context-downloads",
|
|
"context-collector",
|
|
"context-settings",
|
|
"context-history",
|
|
"context-statistics",
|
|
"info-closed",
|
|
"info-open",
|
|
"info-absent"
|
|
]));
|
|
const collector = manifest.find((entry: { name: string }) => entry.name === "collector-dense");
|
|
expect(collector.interactions).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({ type: "fill", role: "textbox", name: "Links" })
|
|
]));
|
|
expect(collector.assertions).toContainEqual(expect.objectContaining({
|
|
type: "minimum-row-count",
|
|
region: "collector-table-body",
|
|
value: 2
|
|
}));
|
|
const avatarUpdate = manifest.find((entry: { name: string }) => entry.name === "avatar-update-tooltip");
|
|
expect(avatarUpdate.assertions).toContainEqual({
|
|
type: "visible",
|
|
role: "button",
|
|
name: "Update verfügbar"
|
|
});
|
|
});
|
|
|
|
it("defines representative responsive captures before the final matrix", () => {
|
|
expect(manifest).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({
|
|
name: "responsive-downloads-1920",
|
|
viewport: { width: 1920, height: 1080 }
|
|
}),
|
|
expect.objectContaining({
|
|
name: "responsive-collector-1366",
|
|
viewport: { width: 1366, height: 768 }
|
|
}),
|
|
expect.objectContaining({
|
|
name: "responsive-settings-1120",
|
|
viewport: { width: 1120, height: 760 }
|
|
})
|
|
]));
|
|
const minimumSettings = manifest.find((entry: { name: string }) => entry.name === "responsive-settings-1120");
|
|
expect(minimumSettings.interactions.slice(0, 2)).toEqual([
|
|
{ type: "click", role: "button", name: "Seitenleiste ausklappen" },
|
|
{ type: "click", role: "button", name: "Accounts" }
|
|
]);
|
|
});
|
|
|
|
it("covers every primary view at every supported viewport exactly once", () => {
|
|
const primaryViews = ["downloads", "collector", "settings", "history", "statistics"];
|
|
const supportedViewports = [
|
|
{ width: 2560, height: 1369 },
|
|
{ width: 1920, height: 1080 },
|
|
{ width: 1366, height: 768 },
|
|
{ width: 1120, height: 760 }
|
|
];
|
|
const expectedCells = primaryViews.flatMap((activeView) =>
|
|
supportedViewports.map((viewport) => `${activeView}@${viewport.width}x${viewport.height}`)
|
|
);
|
|
const matrixEntries = manifest.filter((entry: {
|
|
name: string;
|
|
activeView: string;
|
|
viewport: { width: number; height: number };
|
|
}) => primaryViews.includes(entry.activeView) && supportedViewports.some((viewport) =>
|
|
viewport.width === entry.viewport.width && viewport.height === entry.viewport.height
|
|
) && (
|
|
entry.name.endsWith("-dense") ||
|
|
entry.name.startsWith("responsive-")
|
|
));
|
|
const actualCells = matrixEntries.map((entry: {
|
|
activeView: string;
|
|
viewport: { width: number; height: number };
|
|
}) => `${entry.activeView}@${entry.viewport.width}x${entry.viewport.height}`);
|
|
|
|
expect(matrixEntries).toHaveLength(20);
|
|
expect(new Set(matrixEntries.map((entry: { name: string }) => entry.name)).size).toBe(20);
|
|
expect([...actualCells].sort()).toEqual([...expectedCells].sort());
|
|
});
|
|
|
|
it("reports every invalid required field with its manifest path", () => {
|
|
expect(validateVisualCaptureManifest([{}])).toEqual([
|
|
"$[0].name must be a nonempty string",
|
|
"$[0].scenario must be one of empty, dense, update",
|
|
"$[0].viewport must be an object",
|
|
"$[0].activeView must be one of downloads, collector, settings, history, statistics",
|
|
"$[0].interactions must be an array",
|
|
"$[0].assertions must be an array"
|
|
]);
|
|
});
|
|
|
|
it("rejects region values outside the exact marker-name pattern", () => {
|
|
expect(validateVisualCaptureManifest([{
|
|
...validCapture,
|
|
assertions: [{ type: "visible", region: "Downloads_Table" }]
|
|
}])).toContain('$[0].assertions[0].region must match ^[a-z0-9]+(?:-[a-z0-9]+)*$');
|
|
});
|
|
|
|
it("does not use a same-named CSS class as a region fallback", async () => {
|
|
const document = createFakeDocument([
|
|
new FakeElement({ className: "downloads-table-body" })
|
|
]);
|
|
|
|
await expect(prepareVisualCapture(validCapture, document)).rejects.toThrow(
|
|
'region marker "downloads-table-body" is missing'
|
|
);
|
|
});
|
|
|
|
it("resolves one visible exact region marker", async () => {
|
|
const document = createFakeDocument([
|
|
new FakeElement({ region: "downloads-table-body" }),
|
|
new FakeElement({ region: "downloads-table" })
|
|
]);
|
|
|
|
await expect(prepareVisualCapture(validCapture, document)).resolves.toBeUndefined();
|
|
});
|
|
|
|
it("rejects duplicate visible exact region markers as ambiguous", async () => {
|
|
const document = createFakeDocument([
|
|
new FakeElement({ region: "downloads-table-body" }),
|
|
new FakeElement({ region: "downloads-table-body" })
|
|
]);
|
|
|
|
await expect(prepareVisualCapture(validCapture, document)).rejects.toThrow(
|
|
'region marker "downloads-table-body" is ambiguous'
|
|
);
|
|
});
|
|
|
|
it("rejects duplicate region markers before evaluating absence", async () => {
|
|
const document = createFakeDocument([
|
|
new FakeElement({ region: "downloads-table-body" }),
|
|
new FakeElement({ region: "downloads-table-body" })
|
|
]);
|
|
const capture: VisualCapture = {
|
|
...validCapture,
|
|
assertions: [{ type: "absent", region: "downloads-table-body" }]
|
|
};
|
|
|
|
await expect(prepareVisualCapture(capture, document)).rejects.toThrow(
|
|
'region marker "downloads-table-body" is ambiguous'
|
|
);
|
|
});
|
|
|
|
it("selects the view tab when another button has the same accessible name", async () => {
|
|
const document = createFakeDocument([
|
|
new FakeElement({ role: "button", name: "Einstellungen", className: "menu-bar-trigger" })
|
|
], "Einstellungen");
|
|
const capture: VisualCapture = {
|
|
...validCapture,
|
|
activeView: "settings",
|
|
assertions: [{ type: "active-view", value: "settings" }]
|
|
};
|
|
|
|
await expect(prepareVisualCapture(capture, document)).resolves.toBeUndefined();
|
|
});
|
|
});
|