Skip to content

Commit 2db1e08

Browse files
authored
fix: avoid unconditional use of window in policy creation logic (#1234)
* fix: avoid unconditional use of window in policy creation logic fixes #1233 * test: add unit tests for `setScriptSrc` with Trusted Types support
1 parent 2b0a90a commit 2db1e08

2 files changed

Lines changed: 125 additions & 13 deletions

File tree

src/setScriptSrc.test.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/**
2+
* @jest-environment node
3+
*
4+
* Copyright 2026 Google LLC
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
import { jest } from "@jest/globals";
20+
21+
type TrustedTypesStub = {
22+
createPolicy: jest.Mock;
23+
};
24+
25+
declare global {
26+
// Node does not provide this, but tests attach a stub.
27+
var trustedTypes: TrustedTypesStub | undefined;
28+
}
29+
30+
describe("setScriptSrc()", () => {
31+
beforeEach(() => {
32+
jest.resetModules();
33+
jest.clearAllMocks();
34+
35+
// make sure these tests are running without a window-instance
36+
if (typeof window !== "undefined") {
37+
throw new Error("Expected Node test environment without window.");
38+
}
39+
});
40+
41+
afterEach(() => {
42+
globalThis.trustedTypes = undefined;
43+
});
44+
45+
it("falls back to a plain string when Trusted Types are not available", async () => {
46+
globalThis.trustedTypes = undefined;
47+
48+
const { setScriptSrc } = await import("./setScriptSrc.js");
49+
const script = { src: "" } as HTMLScriptElement;
50+
51+
setScriptSrc(script, "https://maps.googleapis.com/maps/api/js");
52+
53+
expect(script.src).toBe("https://maps.googleapis.com/maps/api/js");
54+
});
55+
56+
it("creates and uses a Trusted Types policy when available", async () => {
57+
const createScriptURL = jest.fn((url: string) => `tt:${url}`);
58+
const createPolicy = jest.fn(() => ({ createScriptURL }));
59+
60+
globalThis.trustedTypes = {
61+
createPolicy,
62+
};
63+
64+
const { setScriptSrc } = await import("./setScriptSrc.js");
65+
const script = { src: "" } as HTMLScriptElement;
66+
67+
setScriptSrc(script, "https://maps.googleapis.com/maps/api/js");
68+
69+
expect(createPolicy).toHaveBeenCalledTimes(1);
70+
expect(createPolicy).toHaveBeenCalledWith(
71+
"@googlemaps/js-api-loader",
72+
expect.objectContaining({
73+
createScriptURL: expect.any(Function),
74+
})
75+
);
76+
expect(createScriptURL).toHaveBeenCalledWith(
77+
"https://maps.googleapis.com/maps/api/js"
78+
);
79+
expect(script.src).toBe("tt:https://maps.googleapis.com/maps/api/js");
80+
});
81+
82+
it("falls back when policy creation fails", async () => {
83+
const createPolicy = jest.fn(() => {
84+
throw new Error("policy denied");
85+
});
86+
87+
globalThis.trustedTypes = {
88+
createPolicy,
89+
};
90+
91+
const { setScriptSrc } = await import("./setScriptSrc.js");
92+
const script = { src: "" } as HTMLScriptElement;
93+
94+
setScriptSrc(script, "https://maps.googleapis.com/maps/api/js");
95+
96+
expect(createPolicy).toHaveBeenCalledTimes(1);
97+
expect(script.src).toBe("https://maps.googleapis.com/maps/api/js");
98+
});
99+
});

src/setScriptSrc.ts

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,34 @@ import type { TrustedTypePolicyFactory } from "trusted-types";
88
import { logDevWarning, MSG_TRUSTED_TYPES_POLICY_FAILED } from "./messages.js";
99

1010
const TRUSTED_TYPES_POLICY_NAME = "@googlemaps/js-api-loader";
11-
type TrustedTypesWindow = Window & {
11+
interface TrustedTypesGlobals {
1212
trustedTypes?: TrustedTypePolicyFactory;
13-
};
14-
15-
// Try to create a Trusted Types policy when supported. Falls back to a string
16-
// passthrough when Trusted Types is unsupported, blocked by CSP, or already
17-
// registered.
13+
}
1814

19-
let policy: {
15+
type Policy = {
2016
createScriptURL: (url: string) => string | TrustedScriptURL;
2117
};
2218

23-
const trustedTypes = (window as TrustedTypesWindow).trustedTypes;
19+
const fallbackPolicy: Policy = { createScriptURL: (url: string) => url };
20+
21+
let policy: Policy | undefined;
22+
23+
/*
24+
* Tries to create a Trusted Types policy when supported. Falls back to a string passthrough
25+
* when Trusted Types is unsupported, blocked by CSP, or already registered.
26+
*/
27+
function getPolicy(): Policy {
28+
if (policy) {
29+
return policy;
30+
}
31+
32+
const trustedTypes = (globalThis as TrustedTypesGlobals).trustedTypes;
33+
34+
if (!trustedTypes) {
35+
policy = fallbackPolicy;
36+
return policy;
37+
}
2438

25-
if (!trustedTypes) {
26-
policy = { createScriptURL: (url: string) => url };
27-
} else {
2839
try {
2940
policy = trustedTypes.createPolicy(TRUSTED_TYPES_POLICY_NAME, {
3041
createScriptURL: (url: string) => url,
@@ -33,10 +44,12 @@ if (!trustedTypes) {
3344
logDevWarning(
3445
MSG_TRUSTED_TYPES_POLICY_FAILED(TRUSTED_TYPES_POLICY_NAME, e)
3546
);
36-
policy = { createScriptURL: (url: string) => url };
47+
policy = fallbackPolicy;
3748
}
49+
50+
return policy;
3851
}
3952

4053
export function setScriptSrc(script: HTMLScriptElement, src: string): void {
41-
script.src = policy.createScriptURL(src) as string;
54+
script.src = getPolicy().createScriptURL(src) as string;
4255
}

0 commit comments

Comments
 (0)