diff --git a/test/components/simple-viewer.html b/test/components/simple-viewer.html
new file mode 100644
index 000000000..49703137d
--- /dev/null
+++ b/test/components/simple-viewer.html
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+ PDF.js — Simple viewer
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/components/simple-viewer.js b/test/components/simple-viewer.js
new file mode 100644
index 000000000..a8db16b0a
--- /dev/null
+++ b/test/components/simple-viewer.js
@@ -0,0 +1,106 @@
+/* Copyright 2026 Mozilla Foundation
+ *
+ * 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 { getDocument, GlobalWorkerOptions } from "pdfjs-lib";
+import { EventBus } from "../../web/event_utils.js";
+import { GenericL10n } from "../../web/genericl10n.js";
+import { PDFFindController } from "../../web/pdf_find_controller.js";
+import { PDFLinkService } from "../../web/pdf_link_service.js";
+import { PDFScriptingManager } from "../../web/pdf_scripting_manager.js";
+import { PDFViewer } from "../../web/pdf_viewer.js";
+
+// The workerSrc property shall be specified.
+//
+GlobalWorkerOptions.workerSrc =
+ typeof PDFJSDev === "undefined"
+ ? "../../src/pdf.worker.js"
+ : "../../build/pdf.worker.mjs";
+
+// Some PDFs need external cmaps.
+//
+const CMAP_URL =
+ typeof PDFJSDev === "undefined"
+ ? "../../external/bcmaps/"
+ : "../../web/cmaps/";
+
+const DEFAULT_URL = "../../web/compressed.tracemonkey-pldi-09.pdf";
+
+const ENABLE_XFA = true;
+const SEARCH_FOR = ""; // try "Mozilla";
+
+const SANDBOX_BUNDLE_SRC = new URL(
+ typeof PDFJSDev === "undefined"
+ ? "../../src/pdf.sandbox.js"
+ : "../../build/pdf.sandbox.mjs",
+ window.location
+);
+
+const fileUrl = new URLSearchParams(location.search).get("file") ?? DEFAULT_URL;
+
+const container = document.getElementById("viewerContainer");
+
+const eventBus = new EventBus();
+
+// (Optionally) enable hyperlinks within PDF files.
+const pdfLinkService = new PDFLinkService({
+ eventBus,
+});
+
+// (Optionally) enable find controller.
+const pdfFindController = new PDFFindController({
+ eventBus,
+ linkService: pdfLinkService,
+});
+
+// (Optionally) enable scripting support.
+const pdfScriptingManager = new PDFScriptingManager({
+ eventBus,
+ sandboxBundleSrc: SANDBOX_BUNDLE_SRC,
+});
+
+const pdfViewer = new PDFViewer({
+ container,
+ eventBus,
+ l10n: new GenericL10n(navigator.language),
+ linkService: pdfLinkService,
+ findController: pdfFindController,
+ scriptingManager: pdfScriptingManager,
+});
+pdfLinkService.setViewer(pdfViewer);
+pdfScriptingManager.setViewer(pdfViewer);
+
+eventBus.on("pagesinit", function () {
+ // We can use pdfViewer now, e.g. let's change default scale.
+ pdfViewer.currentScaleValue = "page-width";
+
+ // We can try searching for things.
+ if (SEARCH_FOR) {
+ eventBus.dispatch("find", { type: "", query: SEARCH_FOR });
+ }
+});
+
+// Loading document.
+const loadingTask = getDocument({
+ url: fileUrl,
+ cMapUrl: CMAP_URL,
+ enableXfa: ENABLE_XFA,
+});
+
+const pdfDocument = await loadingTask.promise;
+// Document loaded, specifying document for the viewer and
+// the (optional) linkService.
+pdfViewer.setDocument(pdfDocument);
+
+pdfLinkService.setDocument(pdfDocument, null);
diff --git a/test/integration/jasmine-boot.js b/test/integration/jasmine-boot.js
index 59e599ee8..26e1ce134 100644
--- a/test/integration/jasmine-boot.js
+++ b/test/integration/jasmine-boot.js
@@ -40,6 +40,7 @@ async function runTests(results) {
"reorganize_pages_spec.mjs",
"scripting_spec.mjs",
"signature_editor_spec.mjs",
+ "simple_viewer_spec.mjs",
"stamp_editor_spec.mjs",
"text_field_spec.mjs",
"text_layer_spec.mjs",
diff --git a/test/integration/simple_viewer_spec.mjs b/test/integration/simple_viewer_spec.mjs
new file mode 100644
index 000000000..34dc6e293
--- /dev/null
+++ b/test/integration/simple_viewer_spec.mjs
@@ -0,0 +1,58 @@
+/* Copyright 2026 Mozilla Foundation
+ *
+ * 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.
+ */
+
+// Integration tests for the simple viewer (test/components/).
+
+describe("Simple viewer", () => {
+ describe("TextLayerBuilder without abortSignal", () => {
+ let pages;
+
+ beforeEach(async () => {
+ const origin = new URL(global.integrationBaseUrl).origin;
+ pages = await Promise.all(
+ global.integrationSessions.map(async session => {
+ const page = await session.browser.newPage();
+ await page.goto(
+ `${origin}/test/components/simple-viewer.html` +
+ `?file=/test/pdfs/tracemonkey.pdf`
+ );
+ await page.bringToFront();
+ await page.waitForSelector(
+ "[data-page-number='1'] .textLayer .endOfContent"
+ );
+ await page.waitForSelector(
+ "[data-page-number='2'] .textLayer .endOfContent"
+ );
+ return [session.name, page];
+ })
+ );
+ });
+
+ afterEach(async () => {
+ await Promise.all(pages.map(([, page]) => page.close()));
+ });
+
+ it("must produce text spans in the text layer", async () => {
+ await Promise.all(
+ pages.map(async ([browserName, page]) => {
+ const count = await page.evaluate(
+ () => document.querySelectorAll(".textLayer span").length
+ );
+ expect(count).withContext(`In ${browserName}`).toBeGreaterThan(0);
+ })
+ );
+ });
+ });
+});