Files
Immich/web/test/medium/mock/helpers.ts
T
Jason Rasmussen e14fcd0a52 feat: web medium tests (#32817)
* feat: web medium tests

* chore: pr feedback

* fix: merge
2026-11-03 19:39:30 +03:00

121 lines
4.8 KiB
TypeScript

import type { AssetResponseDto } from '@immich/sdk';
import { expect } from '@playwright/test';
type Entry<T> = { value: T; seen: boolean; expected: boolean };
const minimalMp4Base64 =
'AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAAAIZnJlZQAAAr9tZGF0AAACoAYF//+c' +
'3EXpvebZSLeWLNgg2SPu73gyNjQgLSBjb3JlIDEyNSAtIEguMjY0L01QRUctNCBBVkMgY29kZWMg' +
'LSBDb3B5bGVmdCAyMDAzLTIwMTIgLSBodHRwOi8vd3d3LnZpZGVvbGFuLm9yZy94MjY0Lmh0bWwg' +
'LSBvcHRpb25zOiBjYWJhYz0xIHJlZj0zIGRlYmxvY2s9MTowOjAgYW5hbHlzZT0weDM6MHgxMTMg' +
'bWU9aGV4IHN1Ym1lPTcgcHN5PTEgcHN5X3JkPTEuMDA6MC4wMCBtaXhlZF9yZWY9MSBtZV9yYW5n' +
'ZT0xNiBjaHJvbWFfbWU9MSB0cmVsbGlzPTEgOHg4ZGN0PTEgY3FtPTAgZGVhZHpvbmU9MjEsMTEg' +
'ZmFzdF9wc2tpcD0xIGNocm9tYV9xcF9vZmZzZXQ9LTIgdGhyZWFkcz02IGxvb2thaGVhZF90aHJl' +
'YWRzPTEgc2xpY2VkX3RocmVhZHM9MCBucj0wIGRlY2ltYXRlPTEgaW50ZXJsYWNlZD0wIGJsdXJh' +
'eV9jb21wYXQ9MCBjb25zdHJhaW5lZF9pbnRyYT0wIGJmcmFtZXM9MyBiX3B5cmFtaWQ9MiBiX2Fk' +
'YXB0PTEgYl9iaWFzPTAgZGlyZWN0PTEgd2VpZ2h0Yj0xIG9wZW5fZ29wPTAgd2VpZ2h0cD0yIGtl' +
'eWludD0yNTAga2V5aW50X21pbj0yNCBzY2VuZWN1dD00MCBpbnRyYV9yZWZyZXNoPTAgcmNfbG9v' +
'a2FoZWFkPTQwIHJjPWNyZiBtYnRyZWU9MSBjcmY9MjMuMCBxY29tcD0wLjYwIHFwbWluPTAgcXBt' +
'YXg9NjkgcXBzdGVwPTQgaXBfcmF0aW89MS40MCBhcT0xOjEuMDAAgAAAAA9liIQAV/0TAAYdeBTX' +
'zg8AAALvbW9vdgAAAGxtdmhkAAAAAAAAAAAAAAAAAAAD6AAAACoAAQAAAQAAAAAAAAAAAAAAAAEAAAAA' +
'AAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAA' +
'Ahl0cmFrAAAAXHRraGQAAAAPAAAAAAAAAAAAAAABAAAAAAAAACoAAAAAAAAAAAAAAAAAAAAAAAEAAAAA' +
'AAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAgAAAAIAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAA' +
'AAEAAAAqAAAAAAABAAAAAAGRbWRpYQAAACBtZGhkAAAAAAAAAAAAAAAAAAAwAAAAAgBVxAAAAAAA' +
'LWhkbHIAAAAAAAAAAHZpZGUAAAAAAAAAAAAAAABWaWRlb0hhbmRsZXIAAAABPG1pbmYAAAAUdm1oZAAA' +
'AAEAAAAAAAAAAAAAACRkaW5mAAAAHGRyZWYAAAAAAAAAAQAAAAx1cmwgAAAAAQAAAPxzdGJsAAAAmHN0' +
'c2QAAAAAAAAAAQAAAIhhdmMxAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAgACABIAAAASAAAAAAAAAAB' +
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGP//AAAAMmF2Y0MBZAAK/+EAGWdkAAqs' +
'2V+WXAWyAAADAAIAAAMAYB4kSywBAAZo6+PLIsAAAAAYc3R0cwAAAAAAAAABAAAAAQAAAgAAAAAcc3Rz' +
'YwAAAAAAAAABAAAAAQAAAAEAAAABAAAAFHN0c3oAAAAAAAACtwAAAAEAAAAUc3RjbwAAAAAAAAABAAAA' +
'MAAAAGJ1ZHRhAAAAWm1ldGEAAAAAAAAAIWhkbHIAAAAAAAAAAG1kaXJhcHBsAAAAAAAAAAAAAAAALWls' +
'c3QAAAAlqXRvbwAAAB1kYXRhAAAAAQAAAABMYXZmNTQuNjMuMTA0';
export const minimalMp4 = Buffer.from(minimalMp4Base64, 'base64');
const hueOf = (value: string) => {
let hash = 0;
for (const char of value) {
hash = (hash * 31 + (char.codePointAt(0) ?? 0)) % 360;
}
return hash;
};
export const placeholderImage = (asset: AssetResponseDto | undefined, id: string) => {
const width = asset?.width ?? 1200;
const height = asset?.height ?? 800;
const fontSize = Math.round(Math.min(width, height) / 12);
const label = asset?.originalFileName ?? id;
return [
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`,
`<rect width="100%" height="100%" fill="hsl(${hueOf(id)} 60% 70%)"/>`,
`<text x="50%" y="50%" font-family="sans-serif" font-size="${fontSize}" text-anchor="middle" dominant-baseline="middle" fill="#222">${label}</text>`,
'</svg>',
].join('');
};
export class Spy<TCall, TExpected = TCall> {
private readonly entries: Entry<TCall>[] = [];
constructor(readonly name: string) {}
get calls(): ReadonlyArray<TCall> {
return this.entries.map((entry) => entry.value);
}
record(value: TCall, { expected = false } = {}) {
this.entries.push({ value, seen: false, expected });
}
matches(value: TCall, expected: TExpected) {
try {
expect(value as unknown).toEqual(expected);
return true;
} catch {
return false;
}
}
markSeen(value: TCall) {
for (const entry of this.entries) {
if (entry.value === value) {
entry.seen = true;
}
}
}
unexpected() {
return this.entries.filter((entry) => entry.expected && !entry.seen).map((entry) => entry.value);
}
}
export class SeededRandom {
constructor(private seed: number) {}
next() {
this.seed = (this.seed * 1_664_525 + 1_013_904_223) % 2_147_483_647;
return this.seed / 2_147_483_647;
}
nextInt(min: number, max: number) {
return Math.floor(this.next() * (max - min)) + min;
}
pick<T>(items: readonly T[]): T {
if (items.length === 0) {
throw new Error('Cannot pick from an empty list');
}
return items[this.nextInt(0, items.length)];
}
pickMany<T>(items: readonly T[], count: number): T[] {
if (count > items.length) {
throw new Error(`Cannot pick ${count} items from a list of ${items.length}`);
}
const indexes = new Set<number>();
while (indexes.size < count) {
indexes.add(this.nextInt(0, items.length));
}
return [...indexes].map((index) => items[index]);
}
}