@editframe/elements 0.42.4 → 0.42.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-FULL.md +148 -0
- package/dist/elements/EFTemporal.js +3 -1
- package/dist/elements/EFTemporal.js.map +1 -1
- package/dist/elements/EFText.js +1 -0
- package/dist/elements/EFText.js.map +1 -1
- package/dist/elements/EFTimegroup.js +11 -0
- package/dist/elements/EFTimegroup.js.map +1 -1
- package/dist/elements/updateAnimations.js +7 -5
- package/dist/elements/updateAnimations.js.map +1 -1
- package/dist/gui/timeline/EFTimeline.js +18 -1
- package/dist/gui/timeline/EFTimeline.js.map +1 -1
- package/dist/preview/renderTimegroupToCanvas.js +9 -0
- package/dist/preview/renderTimegroupToCanvas.js.map +1 -1
- package/package.json +8 -3
- package/scripts/build-css.js +0 -44
- package/test/__cache__/GET__api_v1_transcode_audio_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__32da3954ba60c96ad732020c65a08ebc/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__32da3954ba60c96ad732020c65a08ebc/metadata.json +0 -16
- package/test/__cache__/GET__api_v1_transcode_audio_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__b0b2b07efcf607de8ee0f650328c32f7/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__b0b2b07efcf607de8ee0f650328c32f7/metadata.json +0 -16
- package/test/__cache__/GET__api_v1_transcode_audio_3_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a75c2252b542e0c152c780e9a8d7b154/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_3_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a75c2252b542e0c152c780e9a8d7b154/metadata.json +0 -16
- package/test/__cache__/GET__api_v1_transcode_audio_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a64ff1cfb1b52cae14df4b5dfa1e222b/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a64ff1cfb1b52cae14df4b5dfa1e222b/metadata.json +0 -16
- package/test/__cache__/GET__api_v1_transcode_audio_5_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__91e8a522f950809b9f09f4173113b4b0/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_5_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__91e8a522f950809b9f09f4173113b4b0/metadata.json +0 -16
- package/test/__cache__/GET__api_v1_transcode_audio_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__e66d2c831d951e74ad0aeaa6489795d0/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__e66d2c831d951e74ad0aeaa6489795d0/metadata.json +0 -16
- package/test/__cache__/GET__api_v1_transcode_high_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__26197f6f7c46cacb0a71134131c3f775/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_high_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__26197f6f7c46cacb0a71134131c3f775/metadata.json +0 -16
- package/test/__cache__/GET__api_v1_transcode_high_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__4cb6774cd3650ccf59c8f8dc6678c0b9/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_high_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__4cb6774cd3650ccf59c8f8dc6678c0b9/metadata.json +0 -16
- package/test/__cache__/GET__api_v1_transcode_high_3_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0b3b2b1c8933f7fcf8a9ecaa88d58b41/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_high_3_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0b3b2b1c8933f7fcf8a9ecaa88d58b41/metadata.json +0 -16
- package/test/__cache__/GET__api_v1_transcode_high_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a6fb05a22b18d850f7f2950bbcdbdeed/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_high_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a6fb05a22b18d850f7f2950bbcdbdeed/metadata.json +0 -16
- package/test/__cache__/GET__api_v1_transcode_high_5_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a50058c7c3602e90879fe3428ed891f4/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_high_5_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a50058c7c3602e90879fe3428ed891f4/metadata.json +0 -16
- package/test/__cache__/GET__api_v1_transcode_high_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0798c479b44aaeef850609a430f6e613/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_high_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0798c479b44aaeef850609a430f6e613/metadata.json +0 -16
- package/test/__cache__/GET__api_v1_transcode_manifest_json_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__3be92a0437de726b431ed5af2369158a/data.bin +0 -1
- package/test/__cache__/GET__api_v1_transcode_manifest_json_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__3be92a0437de726b431ed5af2369158a/metadata.json +0 -17
- package/test/__cache__/GET__api_v1_transcode_scrub_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__6ff5127ebeda578a679474347fbd6137/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_scrub_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__6ff5127ebeda578a679474347fbd6137/metadata.json +0 -16
- package/test/__cache__/GET__api_v1_transcode_scrub_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__f6d4793fc9ff854ee9a738917fb64a53/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_scrub_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__f6d4793fc9ff854ee9a738917fb64a53/metadata.json +0 -16
- package/test/cache-integration-verification.browsertest.ts +0 -84
- package/test/constants.ts +0 -8
- package/test/createJitTestClips.ts +0 -425
- package/test/profilingPlugin.ts +0 -221
- package/test/recordReplayProxyPlugin.js +0 -428
- package/test/setup.ts +0 -71
- package/test/useAssetMSW.ts +0 -53
- package/test/useMSW.ts +0 -40
- package/test/useTranscodeMSW.ts +0 -191
- package/test/visualRegressionUtils.ts +0 -300
- package/tsdown.config.ts +0 -65
package/test/setup.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Global test setup for all browser tests
|
|
3
|
-
* This runs before every test to ensure clean state
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { beforeEach, afterAll } from "vitest";
|
|
7
|
-
import {
|
|
8
|
-
globalRequestDeduplicator,
|
|
9
|
-
mediaCache,
|
|
10
|
-
} from "../src/elements/EFMedia/CachedFetcher.js";
|
|
11
|
-
import { globalURLTokenDeduplicator } from "../src/transcoding/cache/URLTokenDeduplicator.js";
|
|
12
|
-
import { TEST_SERVER_PORT } from "./constants.js";
|
|
13
|
-
|
|
14
|
-
// Type declarations for test environment
|
|
15
|
-
declare global {
|
|
16
|
-
interface Window {
|
|
17
|
-
__CI_MODE__?: boolean;
|
|
18
|
-
__PROFILER_STOP_REQUESTED__?: boolean;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Get the correct API host for the current environment.
|
|
24
|
-
* In local dev with Traefik, returns the Traefik URL (e.g., http://main.localhost:4322).
|
|
25
|
-
* In CI or direct access, returns the current location (e.g., http://localhost:63315).
|
|
26
|
-
*/
|
|
27
|
-
export function getApiHost(): string {
|
|
28
|
-
const host = window.location.host;
|
|
29
|
-
const protocol = window.location.protocol;
|
|
30
|
-
|
|
31
|
-
// Check if CI mode was injected by server
|
|
32
|
-
const isCI = (window as any).__CI_MODE__ === true;
|
|
33
|
-
|
|
34
|
-
if (isCI) {
|
|
35
|
-
// CI mode: always use localhost directly
|
|
36
|
-
return `${protocol}//${host}`;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (host === `localhost:${TEST_SERVER_PORT}`) {
|
|
40
|
-
// Check if we have a Traefik referrer (local dev)
|
|
41
|
-
const traefikReferrer = document.referrer.match(/\/\/([^:]+):4322/)?.[1];
|
|
42
|
-
if (traefikReferrer) {
|
|
43
|
-
// Local dev: use Traefik URL
|
|
44
|
-
return `${protocol}//${traefikReferrer}:4322`;
|
|
45
|
-
}
|
|
46
|
-
// No Traefik referrer but not explicitly CI: use localhost directly
|
|
47
|
-
return `${protocol}//${host}`;
|
|
48
|
-
}
|
|
49
|
-
// Already on Traefik URL or other configuration
|
|
50
|
-
return `${protocol}//${host}`;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Clear global caches before each test to ensure isolation
|
|
54
|
-
beforeEach(() => {
|
|
55
|
-
globalRequestDeduplicator.clear();
|
|
56
|
-
mediaCache.clear();
|
|
57
|
-
globalURLTokenDeduplicator.clear();
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
// Signal profiler to stop before tests finish
|
|
61
|
-
afterAll(() => {
|
|
62
|
-
if (typeof window !== "undefined") {
|
|
63
|
-
// Always set the flag, creating it if it doesn't exist
|
|
64
|
-
// This handles both profiled and non-profiled test runs
|
|
65
|
-
(window as any).__PROFILER_STOP_REQUESTED__ = true;
|
|
66
|
-
|
|
67
|
-
// Give profiler time to detect the signal and retrieve profile data
|
|
68
|
-
// Poll interval is 50ms, so wait longer to ensure detection + retrieval
|
|
69
|
-
return new Promise((resolve) => setTimeout(resolve, 500));
|
|
70
|
-
}
|
|
71
|
-
});
|
package/test/useAssetMSW.ts
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { HttpResponse, http } from "msw";
|
|
2
|
-
|
|
3
|
-
export const assetMSWHandlers = [
|
|
4
|
-
http.get("/api/v1/files/:id/index", async () => {
|
|
5
|
-
const mockIndex = {
|
|
6
|
-
0: {
|
|
7
|
-
duration: 10000,
|
|
8
|
-
timescale: 1000,
|
|
9
|
-
fragments: [
|
|
10
|
-
{
|
|
11
|
-
offset: 0,
|
|
12
|
-
size: 1024,
|
|
13
|
-
timestamp: 0,
|
|
14
|
-
duration: 10000,
|
|
15
|
-
},
|
|
16
|
-
],
|
|
17
|
-
},
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
return HttpResponse.json(mockIndex, {
|
|
21
|
-
headers: {
|
|
22
|
-
"Content-Type": "application/json",
|
|
23
|
-
},
|
|
24
|
-
});
|
|
25
|
-
}),
|
|
26
|
-
|
|
27
|
-
http.get("/api/v1/files/:id/tracks/:trackId", async ({ request }) => {
|
|
28
|
-
const rangeHeader = request.headers.get("range");
|
|
29
|
-
|
|
30
|
-
if (rangeHeader) {
|
|
31
|
-
const mockData = new ArrayBuffer(1024);
|
|
32
|
-
return new HttpResponse(mockData, {
|
|
33
|
-
status: 206,
|
|
34
|
-
headers: {
|
|
35
|
-
"Content-Type": "video/mp4",
|
|
36
|
-
"Accept-Ranges": "bytes",
|
|
37
|
-
"Content-Range": rangeHeader,
|
|
38
|
-
"Content-Length": "1024",
|
|
39
|
-
},
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const mockData = new ArrayBuffer(1024);
|
|
44
|
-
return new HttpResponse(mockData, {
|
|
45
|
-
status: 200,
|
|
46
|
-
headers: {
|
|
47
|
-
"Content-Type": "video/mp4",
|
|
48
|
-
"Accept-Ranges": "bytes",
|
|
49
|
-
"Content-Length": "1024",
|
|
50
|
-
},
|
|
51
|
-
});
|
|
52
|
-
}),
|
|
53
|
-
];
|
package/test/useMSW.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MSW integration for Vitest Browser Mode
|
|
3
|
-
* Based on: https://mswjs.io/docs/recipes/vitest-browser-mode/
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { setupWorker } from "msw/browser";
|
|
7
|
-
import { test as testBase } from "vitest";
|
|
8
|
-
import { transcodeMSWHandlers } from "./useTranscodeMSW.js";
|
|
9
|
-
|
|
10
|
-
// Create the worker instance that will be shared across tests
|
|
11
|
-
const worker = setupWorker();
|
|
12
|
-
|
|
13
|
-
// Start the worker once when this module is loaded
|
|
14
|
-
let workerStarted = false;
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Extended test with MSW worker integration for Vitest Browser Mode
|
|
18
|
-
* This follows the official MSW recommendation for Vitest Browser Mode
|
|
19
|
-
*/
|
|
20
|
-
export const test = testBase.extend<{
|
|
21
|
-
worker: typeof worker;
|
|
22
|
-
}>({
|
|
23
|
-
worker: [
|
|
24
|
-
async ({ expect: _expect }, use) => {
|
|
25
|
-
// Only start the worker once
|
|
26
|
-
if (!workerStarted) {
|
|
27
|
-
await worker.start({
|
|
28
|
-
onUnhandledRequest: "bypass", // Allow unhandled requests to pass through
|
|
29
|
-
});
|
|
30
|
-
// Set up default handlers for transcode API endpoints
|
|
31
|
-
worker.use(...transcodeMSWHandlers);
|
|
32
|
-
workerStarted = true;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Use the worker in the test
|
|
36
|
-
await use(worker);
|
|
37
|
-
},
|
|
38
|
-
{ scope: "test" },
|
|
39
|
-
],
|
|
40
|
-
});
|
package/test/useTranscodeMSW.ts
DELETED
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Transcode API MSW handlers for testing
|
|
3
|
-
* Provides handlers for transcode API endpoints including URL signing
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { HttpResponse, http } from "msw";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* MSW handlers for transcode API endpoints
|
|
10
|
-
* These handlers mock the API responses needed for tests
|
|
11
|
-
*/
|
|
12
|
-
export const transcodeMSWHandlers = [
|
|
13
|
-
// URL signing endpoint handler
|
|
14
|
-
// This mocks the /@ef-sign-url endpoint used by the Vite plugin
|
|
15
|
-
http.post("/@ef-sign-url", async () => {
|
|
16
|
-
// Return a mock JWT token
|
|
17
|
-
// The token format is: header.payload.signature
|
|
18
|
-
// We create a simple mock token that will pass basic validation
|
|
19
|
-
const mockToken =
|
|
20
|
-
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1cmwiOiJodHRwOi8vd2ViOjMwMDAvaGVhZC1tb292LTQ4MHAubXA0IiwiZXhwIjo5OTk5OTk5OTk5fQ.mock-signature";
|
|
21
|
-
|
|
22
|
-
return HttpResponse.json(
|
|
23
|
-
{ token: mockToken },
|
|
24
|
-
{
|
|
25
|
-
status: 200,
|
|
26
|
-
headers: {
|
|
27
|
-
"Content-Type": "application/json",
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
);
|
|
31
|
-
}),
|
|
32
|
-
|
|
33
|
-
// URL token endpoint handler (for proxied requests from vite plugin)
|
|
34
|
-
// The vite plugin proxies /@ef-sign-url to /api/v1/url-token
|
|
35
|
-
http.post("/api/v1/url-token", async () => {
|
|
36
|
-
// Return the same mock JWT token as /@ef-sign-url
|
|
37
|
-
const mockToken =
|
|
38
|
-
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1cmwiOiJodHRwOi8vd2ViOjMwMDAvaGVhZC1tb292LTQ4MHAubXA0IiwiZXhwIjo5OTk5OTk5OTk5fQ.mock-signature";
|
|
39
|
-
|
|
40
|
-
return HttpResponse.json(
|
|
41
|
-
{ token: mockToken },
|
|
42
|
-
{
|
|
43
|
-
status: 200,
|
|
44
|
-
headers: {
|
|
45
|
-
"Content-Type": "application/json",
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
);
|
|
49
|
-
}),
|
|
50
|
-
|
|
51
|
-
// Transcode manifest endpoint handler
|
|
52
|
-
// This mocks the manifest.json endpoint used by JitMediaEngine
|
|
53
|
-
http.get("/api/v1/transcode/manifest.json", async ({ request }) => {
|
|
54
|
-
const url = new URL(request.url);
|
|
55
|
-
const sourceUrl = url.searchParams.get("url");
|
|
56
|
-
|
|
57
|
-
if (!sourceUrl) {
|
|
58
|
-
return HttpResponse.json(
|
|
59
|
-
{ error: "url parameter is required" },
|
|
60
|
-
{ status: 400 },
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Return a mock manifest response
|
|
65
|
-
const manifest = {
|
|
66
|
-
version: "1.0",
|
|
67
|
-
type: "cmaf",
|
|
68
|
-
duration: 10,
|
|
69
|
-
durationMs: 10000,
|
|
70
|
-
segmentDuration: 2000,
|
|
71
|
-
baseUrl: `${url.origin}/api/v1/transcode`,
|
|
72
|
-
sourceUrl: sourceUrl,
|
|
73
|
-
audioRenditions: [
|
|
74
|
-
{
|
|
75
|
-
id: "audio",
|
|
76
|
-
src: sourceUrl,
|
|
77
|
-
segmentDurationMs: 2000,
|
|
78
|
-
},
|
|
79
|
-
],
|
|
80
|
-
videoRenditions: [
|
|
81
|
-
{
|
|
82
|
-
id: "high",
|
|
83
|
-
src: sourceUrl,
|
|
84
|
-
segmentDurationMs: 2000,
|
|
85
|
-
},
|
|
86
|
-
],
|
|
87
|
-
endpoints: {
|
|
88
|
-
initSegment: `${url.origin}/api/v1/transcode/{rendition}/init.m4s?url=${encodeURIComponent(sourceUrl)}`,
|
|
89
|
-
mediaSegment: `${url.origin}/api/v1/transcode/{rendition}/{segmentId}.m4s?url=${encodeURIComponent(sourceUrl)}`,
|
|
90
|
-
},
|
|
91
|
-
jitInfo: {
|
|
92
|
-
parallelTranscodingSupported: true,
|
|
93
|
-
expectedTranscodeLatency: 1000,
|
|
94
|
-
segmentCount: 5,
|
|
95
|
-
},
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
return HttpResponse.json(manifest, {
|
|
99
|
-
status: 200,
|
|
100
|
-
headers: {
|
|
101
|
-
"Content-Type": "application/json",
|
|
102
|
-
},
|
|
103
|
-
});
|
|
104
|
-
}),
|
|
105
|
-
|
|
106
|
-
// Transcode init segment endpoint handler
|
|
107
|
-
http.get("/api/v1/transcode/:rendition/init.m4s", async () => {
|
|
108
|
-
// Return a minimal valid MP4 init segment
|
|
109
|
-
// This is a very basic ftyp + moov box structure
|
|
110
|
-
const initSegment = new Uint8Array([
|
|
111
|
-
// ftyp box
|
|
112
|
-
0x00,
|
|
113
|
-
0x00,
|
|
114
|
-
0x00,
|
|
115
|
-
0x20, // box size
|
|
116
|
-
0x66,
|
|
117
|
-
0x74,
|
|
118
|
-
0x79,
|
|
119
|
-
0x70, // 'ftyp'
|
|
120
|
-
0x69,
|
|
121
|
-
0x73,
|
|
122
|
-
0x6f,
|
|
123
|
-
0x6d, // major brand 'isom'
|
|
124
|
-
0x00,
|
|
125
|
-
0x00,
|
|
126
|
-
0x02,
|
|
127
|
-
0x00, // minor version
|
|
128
|
-
0x69,
|
|
129
|
-
0x73,
|
|
130
|
-
0x6f,
|
|
131
|
-
0x6d, // compatible brand 'isom'
|
|
132
|
-
0x69,
|
|
133
|
-
0x73,
|
|
134
|
-
0x6f,
|
|
135
|
-
0x32, // compatible brand 'iso2'
|
|
136
|
-
0x6d,
|
|
137
|
-
0x70,
|
|
138
|
-
0x34,
|
|
139
|
-
0x31, // compatible brand 'mp41'
|
|
140
|
-
// moov box (minimal)
|
|
141
|
-
0x00,
|
|
142
|
-
0x00,
|
|
143
|
-
0x00,
|
|
144
|
-
0x08, // box size
|
|
145
|
-
0x6d,
|
|
146
|
-
0x6f,
|
|
147
|
-
0x6f,
|
|
148
|
-
0x76, // 'moov'
|
|
149
|
-
]);
|
|
150
|
-
|
|
151
|
-
return HttpResponse.arrayBuffer(initSegment.buffer, {
|
|
152
|
-
status: 200,
|
|
153
|
-
headers: {
|
|
154
|
-
"Content-Type": "video/mp4",
|
|
155
|
-
},
|
|
156
|
-
});
|
|
157
|
-
}),
|
|
158
|
-
|
|
159
|
-
// Transcode media segment endpoint handler
|
|
160
|
-
http.get("/api/v1/transcode/:rendition/:segmentId.m4s", async () => {
|
|
161
|
-
// Return a minimal valid MP4 media segment
|
|
162
|
-
// This is a very basic moof + mdat box structure
|
|
163
|
-
const mediaSegment = new Uint8Array([
|
|
164
|
-
// moof box (minimal)
|
|
165
|
-
0x00,
|
|
166
|
-
0x00,
|
|
167
|
-
0x00,
|
|
168
|
-
0x08, // box size
|
|
169
|
-
0x6d,
|
|
170
|
-
0x6f,
|
|
171
|
-
0x6f,
|
|
172
|
-
0x66, // 'moof'
|
|
173
|
-
// mdat box (minimal)
|
|
174
|
-
0x00,
|
|
175
|
-
0x00,
|
|
176
|
-
0x00,
|
|
177
|
-
0x08, // box size
|
|
178
|
-
0x6d,
|
|
179
|
-
0x64,
|
|
180
|
-
0x61,
|
|
181
|
-
0x74, // 'mdat'
|
|
182
|
-
]);
|
|
183
|
-
|
|
184
|
-
return HttpResponse.arrayBuffer(mediaSegment.buffer, {
|
|
185
|
-
status: 200,
|
|
186
|
-
headers: {
|
|
187
|
-
"Content-Type": "video/mp4",
|
|
188
|
-
},
|
|
189
|
-
});
|
|
190
|
-
}),
|
|
191
|
-
];
|
|
@@ -1,300 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Visual regression utilities for browser tests.
|
|
3
|
-
*
|
|
4
|
-
* These utilities enable capturing canvas/element snapshots in browser tests
|
|
5
|
-
* and comparing them against baseline images using odiff for pixel-perfect
|
|
6
|
-
* visual regression testing.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
export interface SnapshotComparisonResult {
|
|
10
|
-
match: boolean;
|
|
11
|
-
diffCount?: number;
|
|
12
|
-
diffPercentage?: number;
|
|
13
|
-
baselineCreated?: boolean;
|
|
14
|
-
error?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Capture a canvas or image as a data URL.
|
|
19
|
-
* Uses JPEG format with configurable quality for smaller file sizes.
|
|
20
|
-
*/
|
|
21
|
-
export function captureCanvasAsDataUrl(
|
|
22
|
-
source: CanvasImageSource | HTMLCanvasElement,
|
|
23
|
-
format: "image/png" | "image/jpeg" = "image/jpeg",
|
|
24
|
-
quality: number = 0.85,
|
|
25
|
-
): string {
|
|
26
|
-
// If it's already a canvas, use toDataURL directly
|
|
27
|
-
if (source instanceof HTMLCanvasElement) {
|
|
28
|
-
return source.toDataURL(format, quality);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Otherwise, draw to temp canvas first
|
|
32
|
-
const canvas = document.createElement("canvas");
|
|
33
|
-
canvas.width = (source as any).width as number;
|
|
34
|
-
canvas.height = (source as any).height as number;
|
|
35
|
-
const ctx = canvas.getContext("2d");
|
|
36
|
-
if (!ctx) {
|
|
37
|
-
throw new Error("Failed to get canvas 2d context");
|
|
38
|
-
}
|
|
39
|
-
ctx.drawImage(source, 0, 0);
|
|
40
|
-
return canvas.toDataURL(format, quality);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Capture an element to canvas, then return as data URL.
|
|
45
|
-
* Uses the same technique as renderToImage but returns raw data.
|
|
46
|
-
*/
|
|
47
|
-
export async function captureElementAsDataUrl(
|
|
48
|
-
element: HTMLElement,
|
|
49
|
-
width: number,
|
|
50
|
-
height: number,
|
|
51
|
-
): Promise<string> {
|
|
52
|
-
const canvas = document.createElement("canvas");
|
|
53
|
-
canvas.width = width;
|
|
54
|
-
canvas.height = height;
|
|
55
|
-
|
|
56
|
-
const ctx = canvas.getContext("2d");
|
|
57
|
-
if (!ctx) {
|
|
58
|
-
throw new Error("Failed to get canvas 2d context");
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Use html2canvas-style rendering through SVG foreignObject
|
|
62
|
-
const clone = element.cloneNode(true) as HTMLElement;
|
|
63
|
-
|
|
64
|
-
// Create wrapper with XHTML namespace
|
|
65
|
-
const wrapper = document.createElement("div");
|
|
66
|
-
wrapper.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
|
|
67
|
-
wrapper.setAttribute(
|
|
68
|
-
"style",
|
|
69
|
-
`width:${width}px;height:${height}px;overflow:hidden;position:relative;`,
|
|
70
|
-
);
|
|
71
|
-
wrapper.appendChild(clone);
|
|
72
|
-
|
|
73
|
-
// Serialize to XHTML
|
|
74
|
-
const xmlSerializer = new XMLSerializer();
|
|
75
|
-
const serialized = xmlSerializer.serializeToString(wrapper);
|
|
76
|
-
|
|
77
|
-
// Wrap in SVG foreignObject
|
|
78
|
-
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
|
|
79
|
-
<foreignObject width="100%" height="100%">
|
|
80
|
-
${serialized}
|
|
81
|
-
</foreignObject>
|
|
82
|
-
</svg>`;
|
|
83
|
-
|
|
84
|
-
// Convert to data URL
|
|
85
|
-
const base64 = btoa(unescape(encodeURIComponent(svg)));
|
|
86
|
-
const svgDataUri = `data:image/svg+xml;base64,${base64}`;
|
|
87
|
-
|
|
88
|
-
// Draw to canvas
|
|
89
|
-
const img = await new Promise<HTMLImageElement>((resolve, reject) => {
|
|
90
|
-
const image = new Image();
|
|
91
|
-
image.onload = () => resolve(image);
|
|
92
|
-
image.onerror = reject;
|
|
93
|
-
image.src = svgDataUri;
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
ctx.drawImage(img, 0, 0);
|
|
97
|
-
return canvas.toDataURL("image/png");
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Write a snapshot image to the file system via the test server.
|
|
102
|
-
* This sends the PNG data to the server which writes it to disk.
|
|
103
|
-
*/
|
|
104
|
-
export async function writeSnapshot(
|
|
105
|
-
testName: string,
|
|
106
|
-
snapshotName: string,
|
|
107
|
-
dataUrl: string,
|
|
108
|
-
isBaseline: boolean = false,
|
|
109
|
-
): Promise<void> {
|
|
110
|
-
const response = await fetch("/@ef-write-snapshot", {
|
|
111
|
-
method: "POST",
|
|
112
|
-
headers: {
|
|
113
|
-
"Content-Type": "application/json",
|
|
114
|
-
},
|
|
115
|
-
body: JSON.stringify({
|
|
116
|
-
testName,
|
|
117
|
-
snapshotName,
|
|
118
|
-
dataUrl,
|
|
119
|
-
isBaseline,
|
|
120
|
-
}),
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
if (!response.ok) {
|
|
124
|
-
const error = await response.text();
|
|
125
|
-
throw new Error(`Failed to write snapshot: ${error}`);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Compare a snapshot against its baseline using odiff.
|
|
131
|
-
* Returns comparison results including diff percentage.
|
|
132
|
-
*/
|
|
133
|
-
export async function compareSnapshot(
|
|
134
|
-
testName: string,
|
|
135
|
-
snapshotName: string,
|
|
136
|
-
dataUrl: string,
|
|
137
|
-
options: {
|
|
138
|
-
threshold?: number;
|
|
139
|
-
antialiasing?: boolean;
|
|
140
|
-
acceptableDiffPercentage?: number;
|
|
141
|
-
} = {},
|
|
142
|
-
): Promise<SnapshotComparisonResult> {
|
|
143
|
-
const {
|
|
144
|
-
threshold = 0.1,
|
|
145
|
-
antialiasing = true,
|
|
146
|
-
acceptableDiffPercentage = 1.0,
|
|
147
|
-
} = options;
|
|
148
|
-
|
|
149
|
-
const response = await fetch("/@ef-compare-snapshot", {
|
|
150
|
-
method: "POST",
|
|
151
|
-
headers: {
|
|
152
|
-
"Content-Type": "application/json",
|
|
153
|
-
},
|
|
154
|
-
body: JSON.stringify({
|
|
155
|
-
testName,
|
|
156
|
-
snapshotName,
|
|
157
|
-
dataUrl,
|
|
158
|
-
threshold,
|
|
159
|
-
antialiasing,
|
|
160
|
-
acceptableDiffPercentage,
|
|
161
|
-
}),
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
if (!response.ok) {
|
|
165
|
-
const error = await response.text();
|
|
166
|
-
throw new Error(`Failed to compare snapshot: ${error}`);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return response.json();
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* High-level function to capture and compare a canvas snapshot.
|
|
174
|
-
* Creates baseline if it doesn't exist, otherwise compares.
|
|
175
|
-
*/
|
|
176
|
-
export async function assertCanvasSnapshot(
|
|
177
|
-
canvas: HTMLCanvasElement,
|
|
178
|
-
testName: string,
|
|
179
|
-
snapshotName: string,
|
|
180
|
-
options: {
|
|
181
|
-
threshold?: number;
|
|
182
|
-
acceptableDiffPercentage?: number;
|
|
183
|
-
} = {},
|
|
184
|
-
): Promise<SnapshotComparisonResult> {
|
|
185
|
-
// Use PNG format for consistent snapshot comparison (odiff works best with PNG)
|
|
186
|
-
const dataUrl = captureCanvasAsDataUrl(canvas, "image/png");
|
|
187
|
-
return compareSnapshot(testName, snapshotName, dataUrl, options);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Assert that a snapshot matches its baseline.
|
|
192
|
-
* Throws an assertion error if the diff exceeds the acceptable threshold.
|
|
193
|
-
*/
|
|
194
|
-
export async function expectCanvasToMatchSnapshot(
|
|
195
|
-
source: CanvasImageSource | HTMLCanvasElement,
|
|
196
|
-
testName: string,
|
|
197
|
-
snapshotName: string,
|
|
198
|
-
options: {
|
|
199
|
-
threshold?: number;
|
|
200
|
-
acceptableDiffPercentage?: number;
|
|
201
|
-
} = {},
|
|
202
|
-
): Promise<void> {
|
|
203
|
-
const result = await assertCanvasSnapshot(
|
|
204
|
-
source as unknown as HTMLCanvasElement,
|
|
205
|
-
testName,
|
|
206
|
-
snapshotName,
|
|
207
|
-
options,
|
|
208
|
-
);
|
|
209
|
-
|
|
210
|
-
if (result.baselineCreated) {
|
|
211
|
-
console.log(`✅ Created baseline: ${testName}/${snapshotName}`);
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
if (!result.match) {
|
|
216
|
-
const diffInfo =
|
|
217
|
-
result.diffPercentage !== undefined
|
|
218
|
-
? `${result.diffPercentage.toFixed(2)}% different`
|
|
219
|
-
: result.error || "comparison failed";
|
|
220
|
-
throw new Error(
|
|
221
|
-
`Visual regression detected for ${testName}/${snapshotName}: ${diffInfo}`,
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Compare two canvases or images directly against each other.
|
|
228
|
-
* Returns comparison results including diff percentage.
|
|
229
|
-
*/
|
|
230
|
-
export async function compareTwoCanvases(
|
|
231
|
-
source1: CanvasImageSource | HTMLCanvasElement,
|
|
232
|
-
source2: CanvasImageSource | HTMLCanvasElement,
|
|
233
|
-
testName: string,
|
|
234
|
-
comparisonName: string,
|
|
235
|
-
options: {
|
|
236
|
-
threshold?: number;
|
|
237
|
-
acceptableDiffPercentage?: number;
|
|
238
|
-
} = {},
|
|
239
|
-
): Promise<SnapshotComparisonResult> {
|
|
240
|
-
const { threshold = 0.1, acceptableDiffPercentage = 1.0 } = options;
|
|
241
|
-
|
|
242
|
-
// Use PNG format for consistent comparison (odiff works best with PNG)
|
|
243
|
-
const dataUrl1 = captureCanvasAsDataUrl(source1, "image/png");
|
|
244
|
-
const dataUrl2 = captureCanvasAsDataUrl(source2, "image/png");
|
|
245
|
-
|
|
246
|
-
const response = await fetch("/@ef-compare-two-images", {
|
|
247
|
-
method: "POST",
|
|
248
|
-
headers: {
|
|
249
|
-
"Content-Type": "application/json",
|
|
250
|
-
},
|
|
251
|
-
body: JSON.stringify({
|
|
252
|
-
testName,
|
|
253
|
-
comparisonName,
|
|
254
|
-
dataUrl1,
|
|
255
|
-
dataUrl2,
|
|
256
|
-
threshold,
|
|
257
|
-
acceptableDiffPercentage,
|
|
258
|
-
}),
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
if (!response.ok) {
|
|
262
|
-
const error = await response.text();
|
|
263
|
-
throw new Error(`Failed to compare canvases: ${error}`);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
return response.json();
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Assert that two canvases match within acceptable threshold.
|
|
271
|
-
* Throws an assertion error if the diff exceeds the acceptable threshold.
|
|
272
|
-
*/
|
|
273
|
-
export async function expectCanvasesToMatch(
|
|
274
|
-
canvas1: HTMLCanvasElement,
|
|
275
|
-
canvas2: HTMLCanvasElement,
|
|
276
|
-
testName: string,
|
|
277
|
-
comparisonName: string,
|
|
278
|
-
options: {
|
|
279
|
-
threshold?: number;
|
|
280
|
-
acceptableDiffPercentage?: number;
|
|
281
|
-
} = {},
|
|
282
|
-
): Promise<void> {
|
|
283
|
-
const result = await compareTwoCanvases(
|
|
284
|
-
canvas1,
|
|
285
|
-
canvas2,
|
|
286
|
-
testName,
|
|
287
|
-
comparisonName,
|
|
288
|
-
options,
|
|
289
|
-
);
|
|
290
|
-
|
|
291
|
-
if (!result.match) {
|
|
292
|
-
const diffInfo =
|
|
293
|
-
result.diffPercentage !== undefined
|
|
294
|
-
? `${result.diffPercentage.toFixed(2)}% different`
|
|
295
|
-
: result.error || "comparison failed";
|
|
296
|
-
throw new Error(
|
|
297
|
-
`Canvas comparison failed for ${testName}/${comparisonName}: ${diffInfo}`,
|
|
298
|
-
);
|
|
299
|
-
}
|
|
300
|
-
}
|