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
99 changes: 99 additions & 0 deletions src/setScriptSrc.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**
* @jest-environment node
*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { jest } from "@jest/globals";

type TrustedTypesStub = {
createPolicy: jest.Mock;
};

declare global {
// Node does not provide this, but tests attach a stub.
var trustedTypes: TrustedTypesStub | undefined;
}

describe("setScriptSrc()", () => {
beforeEach(() => {
jest.resetModules();
jest.clearAllMocks();

// make sure these tests are running without a window-instance
if (typeof window !== "undefined") {
throw new Error("Expected Node test environment without window.");
}
});

afterEach(() => {
globalThis.trustedTypes = undefined;
});

it("falls back to a plain string when Trusted Types are not available", async () => {
globalThis.trustedTypes = undefined;

const { setScriptSrc } = await import("./setScriptSrc.js");
const script = { src: "" } as HTMLScriptElement;

setScriptSrc(script, "https://maps.googleapis.com/maps/api/js");

expect(script.src).toBe("https://maps.googleapis.com/maps/api/js");
});

it("creates and uses a Trusted Types policy when available", async () => {
const createScriptURL = jest.fn((url: string) => `tt:${url}`);
const createPolicy = jest.fn(() => ({ createScriptURL }));

globalThis.trustedTypes = {
createPolicy,
};

const { setScriptSrc } = await import("./setScriptSrc.js");
const script = { src: "" } as HTMLScriptElement;

setScriptSrc(script, "https://maps.googleapis.com/maps/api/js");

expect(createPolicy).toHaveBeenCalledTimes(1);
expect(createPolicy).toHaveBeenCalledWith(
"@googlemaps/js-api-loader",
expect.objectContaining({
createScriptURL: expect.any(Function),
})
);
expect(createScriptURL).toHaveBeenCalledWith(
"https://maps.googleapis.com/maps/api/js"
);
expect(script.src).toBe("tt:https://maps.googleapis.com/maps/api/js");
});

it("falls back when policy creation fails", async () => {
const createPolicy = jest.fn(() => {
throw new Error("policy denied");
});

globalThis.trustedTypes = {
createPolicy,
};

const { setScriptSrc } = await import("./setScriptSrc.js");
const script = { src: "" } as HTMLScriptElement;

setScriptSrc(script, "https://maps.googleapis.com/maps/api/js");

expect(createPolicy).toHaveBeenCalledTimes(1);
expect(script.src).toBe("https://maps.googleapis.com/maps/api/js");
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's also get some coverage of what the expected output is, both for supported Trusted Types envs and unsupported envs?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also added tests for the different new and old behaviors of setScriptSrc

});
39 changes: 26 additions & 13 deletions src/setScriptSrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,34 @@ import type { TrustedTypePolicyFactory } from "trusted-types";
import { logDevWarning, MSG_TRUSTED_TYPES_POLICY_FAILED } from "./messages.js";

const TRUSTED_TYPES_POLICY_NAME = "@googlemaps/js-api-loader";
type TrustedTypesWindow = Window & {
interface TrustedTypesGlobals {
trustedTypes?: TrustedTypePolicyFactory;
};

// Try to create a Trusted Types policy when supported. Falls back to a string
// passthrough when Trusted Types is unsupported, blocked by CSP, or already
// registered.
}

let policy: {
type Policy = {
createScriptURL: (url: string) => string | TrustedScriptURL;
};

const trustedTypes = (window as TrustedTypesWindow).trustedTypes;
const fallbackPolicy: Policy = { createScriptURL: (url: string) => url };

let policy: Policy | undefined;

/*
* Tries to create a Trusted Types policy when supported. Falls back to a string passthrough
* when Trusted Types is unsupported, blocked by CSP, or already registered.
*/
function getPolicy(): Policy {
if (policy) {
return policy;
}

const trustedTypes = (globalThis as TrustedTypesGlobals).trustedTypes;

if (!trustedTypes) {
policy = fallbackPolicy;
return policy;
}

if (!trustedTypes) {
policy = { createScriptURL: (url: string) => url };
} else {
try {
policy = trustedTypes.createPolicy(TRUSTED_TYPES_POLICY_NAME, {
createScriptURL: (url: string) => url,
Expand All @@ -33,10 +44,12 @@ if (!trustedTypes) {
logDevWarning(
MSG_TRUSTED_TYPES_POLICY_FAILED(TRUSTED_TYPES_POLICY_NAME, e)
);
policy = { createScriptURL: (url: string) => url };
policy = fallbackPolicy;
}

return policy;
}

export function setScriptSrc(script: HTMLScriptElement, src: string): void {
script.src = policy.createScriptURL(src) as string;
script.src = getPolicy().createScriptURL(src) as string;
}
Loading