Skip to content

Commit 3344559

Browse files
authored
Merge pull request #141 from shotstack/derk/webgl-load-error
fix: throw WebGLUnsupportedError when WebGL is unavailable
2 parents 0ce557c + 179640d commit 3344559

4 files changed

Lines changed: 85 additions & 2 deletions

File tree

src/components/canvas/shotstack-canvas.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Edit } from "@core/edit-session";
55
import { InternalEvent } from "@core/events/edit-events";
66
import { ms } from "@core/timing/types";
77
import type { UIController } from "@core/ui/ui-controller";
8-
import { checkWebGLSupport } from "@core/webgl-support";
8+
import { checkWebGLSupport, WebGLUnsupportedError } from "@core/webgl-support";
99
import { type Size } from "@layouts/geometry";
1010
import { AudioLoadParser } from "@loaders/audio-load-parser";
1111
import { FontLoadParser } from "@loaders/font-load-parser";
@@ -90,7 +90,7 @@ export class Canvas {
9090
const webglSupport = checkWebGLSupport();
9191
if (!webglSupport.supported) {
9292
createWebGLErrorOverlay(root);
93-
return;
93+
throw new WebGLUnsupportedError(webglSupport.reason);
9494
}
9595

9696
const rect = root.getBoundingClientRect();
@@ -268,6 +268,9 @@ export class Canvas {
268268
}
269269

270270
public resize(): void {
271+
// Renderer is undefined until load() initialises it and null after dispose().
272+
if (!this.application.renderer) return;
273+
271274
const root = document.querySelector<HTMLDivElement>(Canvas.CanvasSelector);
272275
if (!root) return;
273276

src/core/webgl-support.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,21 @@ export interface WebGLSupportResult {
1010
reason?: "webgl-unavailable" | "webgl-error";
1111
}
1212

13+
/**
14+
* Thrown by `Canvas.load()` when WebGL is unavailable, so hosts can show
15+
* their own error state instead of treating the editor as loaded.
16+
*/
17+
export class WebGLUnsupportedError extends Error {
18+
constructor(reason?: WebGLSupportResult["reason"]) {
19+
super(
20+
reason === "webgl-error"
21+
? "WebGL initialisation failed. The editor cannot render in this browser."
22+
: "WebGL is not available in this browser. Enable hardware acceleration or use a browser that supports WebGL."
23+
);
24+
this.name = "WebGLUnsupportedError";
25+
}
26+
}
27+
1328
/**
1429
* Check if WebGL is available in the current browser.
1530
* Tests both WebGL2 and WebGL1 for maximum compatibility.

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export { Controls } from "@core/inputs/controls";
66
export { VideoExporter } from "@core/export";
77
export { Timeline } from "@timeline/index";
88
export { UIController } from "@core/ui/ui-controller";
9+
export { WebGLUnsupportedError } from "@core/webgl-support";
910

1011
export type { UIControllerOptions, ToolbarButtonConfig } from "@core/ui/ui-controller";
1112
export type { EditConfig } from "@core/schemas";

tests/webgl-support.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* @jest-environment jsdom
3+
*
4+
* WebGL Support Tests
5+
*
6+
* jsdom has no WebGL, so the unsupported path is the natural one here:
7+
* load() must throw WebGLUnsupportedError (not silently succeed), and
8+
* resize() must be a no-op before the renderer exists.
9+
*/
10+
11+
import { Canvas } from "@canvas/shotstack-canvas";
12+
import { checkWebGLSupport, WebGLUnsupportedError } from "@core/webgl-support";
13+
14+
import type { Edit } from "@core/edit-session";
15+
16+
// pixi.js ships untransformed ESM that jest can't parse; the paths under test
17+
// (constructor + the WebGL guard at the top of load/resize) never reach a real renderer.
18+
jest.mock("pixi.js", () => ({
19+
Application: jest.fn(),
20+
Container: jest.fn(),
21+
Graphics: jest.fn(),
22+
Rectangle: jest.fn()
23+
}));
24+
jest.mock("pixi.js/app", () => ({}));
25+
jest.mock("pixi.js/events", () => ({}));
26+
jest.mock("pixi.js/graphics", () => ({}));
27+
jest.mock("pixi.js/text", () => ({}));
28+
jest.mock("pixi.js/text-html", () => ({}));
29+
jest.mock("pixi.js/sprite-tiling", () => ({}));
30+
jest.mock("pixi.js/filters", () => ({}));
31+
jest.mock("pixi.js/mesh", () => ({}));
32+
33+
const makeEditStub = (): Edit =>
34+
({
35+
setCanvas: () => {},
36+
size: { width: 1280, height: 720 }
37+
}) as unknown as Edit;
38+
39+
describe("WebGL support", () => {
40+
beforeEach(() => {
41+
document.body.innerHTML = `<div data-shotstack-studio></div>`;
42+
});
43+
44+
it("reports unsupported in jsdom", () => {
45+
expect(checkWebGLSupport().supported).toBe(false);
46+
});
47+
48+
it("load() throws WebGLUnsupportedError when WebGL is unavailable", async () => {
49+
const canvas = new Canvas(makeEditStub());
50+
await expect(canvas.load()).rejects.toBeInstanceOf(WebGLUnsupportedError);
51+
});
52+
53+
it("still renders the error overlay for the user", async () => {
54+
const canvas = new Canvas(makeEditStub());
55+
await canvas.load().catch(() => {});
56+
const root = document.querySelector("[data-shotstack-studio]");
57+
expect(root?.childElementCount).toBeGreaterThan(0);
58+
});
59+
60+
it("resize() is a no-op before the renderer is initialised", () => {
61+
const canvas = new Canvas(makeEditStub());
62+
expect(() => canvas.resize()).not.toThrow();
63+
});
64+
});

0 commit comments

Comments
 (0)