Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/components/canvas/shotstack-canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Edit } from "@core/edit-session";
import { InternalEvent } from "@core/events/edit-events";
import { ms } from "@core/timing/types";
import type { UIController } from "@core/ui/ui-controller";
import { checkWebGLSupport } from "@core/webgl-support";
import { checkWebGLSupport, WebGLUnsupportedError } from "@core/webgl-support";
import { type Size } from "@layouts/geometry";
import { AudioLoadParser } from "@loaders/audio-load-parser";
import { FontLoadParser } from "@loaders/font-load-parser";
Expand Down Expand Up @@ -90,7 +90,7 @@ export class Canvas {
const webglSupport = checkWebGLSupport();
if (!webglSupport.supported) {
createWebGLErrorOverlay(root);
return;
throw new WebGLUnsupportedError(webglSupport.reason);
}

const rect = root.getBoundingClientRect();
Expand Down Expand Up @@ -268,6 +268,9 @@ export class Canvas {
}

public resize(): void {
// Renderer is undefined until load() initialises it and null after dispose().
if (!this.application.renderer) return;

const root = document.querySelector<HTMLDivElement>(Canvas.CanvasSelector);
if (!root) return;

Expand Down
15 changes: 15 additions & 0 deletions src/core/webgl-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,21 @@ export interface WebGLSupportResult {
reason?: "webgl-unavailable" | "webgl-error";
}

/**
* Thrown by `Canvas.load()` when WebGL is unavailable, so hosts can show
* their own error state instead of treating the editor as loaded.
*/
export class WebGLUnsupportedError extends Error {
constructor(reason?: WebGLSupportResult["reason"]) {
super(
reason === "webgl-error"
? "WebGL initialisation failed. The editor cannot render in this browser."
: "WebGL is not available in this browser. Enable hardware acceleration or use a browser that supports WebGL."
);
this.name = "WebGLUnsupportedError";
}
}

/**
* Check if WebGL is available in the current browser.
* Tests both WebGL2 and WebGL1 for maximum compatibility.
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export { Controls } from "@core/inputs/controls";
export { VideoExporter } from "@core/export";
export { Timeline } from "@timeline/index";
export { UIController } from "@core/ui/ui-controller";
export { WebGLUnsupportedError } from "@core/webgl-support";

export type { UIControllerOptions, ToolbarButtonConfig } from "@core/ui/ui-controller";
export type { EditConfig } from "@core/schemas";
Expand Down
64 changes: 64 additions & 0 deletions tests/webgl-support.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* @jest-environment jsdom
*
* WebGL Support Tests
*
* jsdom has no WebGL, so the unsupported path is the natural one here:
* load() must throw WebGLUnsupportedError (not silently succeed), and
* resize() must be a no-op before the renderer exists.
*/

import { Canvas } from "@canvas/shotstack-canvas";
import { checkWebGLSupport, WebGLUnsupportedError } from "@core/webgl-support";

import type { Edit } from "@core/edit-session";

// pixi.js ships untransformed ESM that jest can't parse; the paths under test
// (constructor + the WebGL guard at the top of load/resize) never reach a real renderer.
jest.mock("pixi.js", () => ({
Application: jest.fn(),
Container: jest.fn(),
Graphics: jest.fn(),
Rectangle: jest.fn()
}));
jest.mock("pixi.js/app", () => ({}));
jest.mock("pixi.js/events", () => ({}));
jest.mock("pixi.js/graphics", () => ({}));
jest.mock("pixi.js/text", () => ({}));
jest.mock("pixi.js/text-html", () => ({}));
jest.mock("pixi.js/sprite-tiling", () => ({}));
jest.mock("pixi.js/filters", () => ({}));
jest.mock("pixi.js/mesh", () => ({}));

const makeEditStub = (): Edit =>
({
setCanvas: () => {},
size: { width: 1280, height: 720 }
}) as unknown as Edit;

describe("WebGL support", () => {
beforeEach(() => {
document.body.innerHTML = `<div data-shotstack-studio></div>`;
});

it("reports unsupported in jsdom", () => {
expect(checkWebGLSupport().supported).toBe(false);
});

it("load() throws WebGLUnsupportedError when WebGL is unavailable", async () => {
const canvas = new Canvas(makeEditStub());
await expect(canvas.load()).rejects.toBeInstanceOf(WebGLUnsupportedError);
});

it("still renders the error overlay for the user", async () => {
const canvas = new Canvas(makeEditStub());
await canvas.load().catch(() => {});
const root = document.querySelector("[data-shotstack-studio]");
expect(root?.childElementCount).toBeGreaterThan(0);
});

it("resize() is a no-op before the renderer is initialised", () => {
const canvas = new Canvas(makeEditStub());
expect(() => canvas.resize()).not.toThrow();
});
});
Loading