diff --git a/Gruntfile.cjs b/Gruntfile.cjs
index e568c534..50e3c184 100644
--- a/Gruntfile.cjs
+++ b/Gruntfile.cjs
@@ -6,45 +6,7 @@ module.exports = function (grunt) {
pkg: grunt.file.readJSON("package.json"),
- clean: ["<%= properties.webappBuildDir %>", "<%= properties.umdBuildDir %>"],
-
- sync: {
- all: {
- files:
- [
- // copy tests
- { expand: true, cwd: "<%= properties.webappTestDir %>", src: "**", dest: "<%= properties.webappBuildDir %>" },
-
- // copy tests resources
- { expand: true, cwd: "<%= properties.unitTestsResourcesDir %>", src: "imsc-tests/imsc1/ttml/**", dest: "<%= properties.webappBuildDir %>" },
- { expand: true, cwd: "<%= properties.unitTestsResourcesDir %>", src: "imsc-tests/imsc1/tests.json", dest: "<%= properties.webappBuildDir %>" },
- { expand: true, cwd: "<%= properties.unitTestsResourcesDir %>", src: "imsc-tests/imsc1_1/ttml/**", dest: "<%= properties.webappBuildDir %>" },
- { expand: true, cwd: "<%= properties.unitTestsResourcesDir %>", src: "imsc-tests/imsc1_1/tests.json", dest: "<%= properties.webappBuildDir %>" },
- { expand: true, cwd: "<%= properties.unitTestsResourcesDir %>", src: "unit-tests/**", dest: "<%= properties.webappBuildDir %>" },
- ],
- },
-
- release: {
- src: "<%= properties.umdBuildDir %>/<%= properties.umdMinName %>",
- dest: "<%= properties.webappBuildDir %>/libs/imsc.js",
- },
-
- debug: {
- src: "<%= properties.umdBuildDir %>/<%= properties.umdDebugName %>",
- dest: "<%= properties.webappBuildDir %>/libs/imsc.js",
- },
- },
-
- npmcopy: {
- default: {
- files: {
- "<%= properties.webappBuildDir %>/libs/": [
- "filesaver.js-npm:main",
- "jszip/dist/jszip.js",
- ],
- },
- },
- },
+ clean: ["<%= properties.umdBuildDir %>"],
exec: {
compile: {
@@ -65,18 +27,10 @@ module.exports = function (grunt) {
grunt.loadNpmTasks("grunt-contrib-clean");
- grunt.loadNpmTasks("grunt-npmcopy");
-
- grunt.loadNpmTasks("grunt-sync");
-
grunt.loadNpmTasks("grunt-exec");
grunt.registerTask("lint", ["exec:lint"]);
- grunt.registerTask("build:release", ["lint", "exec:compile", "exec:bundle", "sync:all", "sync:release", "npmcopy"]);
-
- grunt.registerTask("build:debug", ["lint", "exec:compile", "exec:bundle", "sync:all", "sync:debug", "npmcopy"]);
-
- grunt.registerTask("build", ["build:debug"]);
+ grunt.registerTask("build", ["lint", "exec:compile", "exec:bundle"]);
};
diff --git a/README.md b/README.md
index c1e9d26e..4858185f 100644
--- a/README.md
+++ b/README.md
@@ -39,10 +39,10 @@ Rendering to HTML5 requires a browser environment, but parsing an IMSC document
* run the `build` target defined in [Gruntfile.cjs](./Gruntfile.cjs) using [grunt](http://gruntjs.com/).
-* the resulting `imsc.js` file at `build/public_html/libs` is the imscJS library. For example, it can be included in a web page as follows:
+* the resulting `dist/imsc.debug.js` (non-minified) or `dist/imsc.min.js` (minified) file is the imscJS library. For example, it can be included in a web page as follows:
```html
-
+
```
See BUILD ARTIFACTS for a full list of build artifacts, and TESTS AND SAMPLES for a list of samples and tests available.
@@ -80,7 +80,7 @@ imscJS consists of the following ES modules at [src/main/js](src/main/js), which
## Build
-imscJS is built using the `build:release` or `build:debug` Grunt tasks -- the `build` task is an alias of `build:debug`.
+imscJS is built using the `build` Grunt task.
The `dist` directory contains the following build artifacts:
@@ -88,11 +88,6 @@ The `dist` directory contains the following build artifacts:
* `imsc.min.js`: Minified UMD build.
* `main/`: ES modules and TypeScript type declarations, used when the library is imported as an NPM package.
-The `build/public_html/libs/imsc.js` file is identical to:
-
-* `imsc.debug.js`, if the `build:debug` task is executed.
-* `imsc.min.js`, if the `build:release` task is executed.
-
## Releases
imscJS is released as an NPM package under [imsc](https://www.npmjs.com/package/imsc). The `dev` distribution tag indicates pre-releases.
@@ -105,7 +100,7 @@ To access the latest builds, please consult the [release page](https://github.co
### W3C Test Suite
-The `gen-renders.html` web app or the headless script [src/test/script/gen-render-package.mjs](src/test/script/gen-render-package.mjs) can be used to generate PNG renderings as well as intermediary files (JSON document, ISD documents and HTML documents) from the [W3C IMSC test suite](https://github.com/w3c/imsc-tests).
+The headless script [src/test/script/gen-render-package.mjs](src/test/script/gen-render-package.mjs) can be used to generate PNG renderings as well as intermediary files (JSON document, ISD documents and HTML documents) from the [W3C IMSC test suite](https://github.com/w3c/imsc-tests).
```sh
npm run gen-imsc1
@@ -157,8 +152,4 @@ npm run gen-reference-files
* [src/test](src/test): Test files
-* [src/test/webapp](src/test/webapp): Web app, used to generate PNG renderings and intermediary files from the W3C IMSC test suite
-
* [dist](dist): Built libraries
-
-* [build/public_html](build/public_html): Test web applications
diff --git a/eslint.config.js b/eslint.config.js
index 54f53d1d..98ee1e2f 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -4,7 +4,6 @@ import globals from "globals";
export default [
{
ignores: [
- "src/test/webapp/**/*",
"dist",
"build",
],
@@ -30,6 +29,7 @@ export default [
},
{
files: ["src/test/script/**/*"],
+ ignores: ["src/test/script/**/*.browser.js"],
languageOptions: {
globals: {
...globals.node,
diff --git a/package-lock.json b/package-lock.json
index cdc4820c..ac7bdc02 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,13 +18,10 @@
"@rollup/plugin-node-resolve": "16.0.3",
"@rollup/plugin-terser": "1.0.0",
"eslint": "10.10.0",
- "filesaver.js-npm": "1.0.1",
"globals": "17.12.0",
"grunt": "1.6.3",
"grunt-contrib-clean": "2.0.1",
"grunt-exec": "3.0.0",
- "grunt-npmcopy": "0.2.0",
- "grunt-sync": "0.8.2",
"jszip": "3.10.1",
"rollup": "4.63.1",
"rollup-plugin-polyfill-node": "0.13.0",
@@ -1260,13 +1257,6 @@
"flat-cache": "^6.1.23"
}
},
- "node_modules/filesaver.js-npm": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/filesaver.js-npm/-/filesaver.js-npm-1.0.1.tgz",
- "integrity": "sha512-UvDxCSp2+i5oav5BEswN8nxnQiH3gzXGqtC4NDaFqFKhRLHqc0FGHceC9m8fV1WArsTnTNSJ+EyZFCHD/0zzJg==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -1382,18 +1372,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/fs-extra": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.1.tgz",
- "integrity": "sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "graceful-fs": "^4.1.2",
- "jsonfile": "^4.0.0",
- "universalify": "^0.1.0"
- }
- },
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -1534,13 +1512,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/graceful-fs": {
- "version": "4.2.11",
- "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
- "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
- "dev": true,
- "license": "ISC"
- },
"node_modules/grunt": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/grunt/-/grunt-1.6.3.tgz",
@@ -1677,35 +1648,6 @@
"node": ">=10"
}
},
- "node_modules/grunt-npmcopy": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/grunt-npmcopy/-/grunt-npmcopy-0.2.0.tgz",
- "integrity": "sha512-iiIpifpz5UCWPYtskdgCbDjKOoSjmwdEj9hvIHZFHVWjjfehXN6VNoeBp9c6v8XuD81BnRLRf8z/rkwmmmxvqA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "glob": "^7.1.3",
- "lodash": "^4.17.11"
- },
- "engines": {
- "node": ">= 0.10.0"
- }
- },
- "node_modules/grunt-sync": {
- "version": "0.8.2",
- "resolved": "https://registry.npmjs.org/grunt-sync/-/grunt-sync-0.8.2.tgz",
- "integrity": "sha512-PB+xKI9YPyZn3NZQXpKHfZVlxHdf1L8GMl+Wi0mLhYypWuOdZPW2EzTmSuhhFbXjkb0aIOxvII1zdZZEl9zqGg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fs-extra": "^6.0.1",
- "glob": "^7.0.5",
- "md5-file": "^2.0.3"
- },
- "engines": {
- "node": ">=6 <7 || >=8"
- }
- },
"node_modules/grunt/node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
@@ -2042,16 +1984,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/jsonfile": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
- "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
- "dev": true,
- "license": "MIT",
- "optionalDependencies": {
- "graceful-fs": "^4.1.6"
- }
- },
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
@@ -2213,13 +2145,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/md5-file": {
- "version": "2.0.7",
- "resolved": "https://registry.npmjs.org/md5-file/-/md5-file-2.0.7.tgz",
- "integrity": "sha512-kWAICpJv8fIY0Ka/90iOX9nCJ407Fgj82ceWwcxi2HvVkKGHRMS/Y4caqBaju5skNYXiQohGUjwGZ7rVgzUhRw==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
@@ -3000,16 +2925,6 @@
"node": "*"
}
},
- "node_modules/universalify": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
- "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 4.0.0"
- }
- },
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
diff --git a/package.json b/package.json
index 548d4a9b..b8d90d7c 100644
--- a/package.json
+++ b/package.json
@@ -46,8 +46,7 @@
"./package.json": "./package.json"
},
"scripts": {
- "prepublishOnly": "grunt build:release",
- "dev": "npx http-server build/public_html",
+ "prepublishOnly": "grunt build",
"gen-imsc1": "node src/test/script/gen-render-package.mjs imsc-tests/imsc1 build/renders-imsc1.zip",
"gen-imsc1_1": "node src/test/script/gen-render-package.mjs imsc-tests/imsc1_1 build/renders-imsc1_1.zip",
"gen-reference-files": "node src/test/script/gen-reference-files.mjs",
@@ -67,13 +66,10 @@
"@rollup/plugin-node-resolve": "16.0.3",
"@rollup/plugin-terser": "1.0.0",
"eslint": "10.10.0",
- "filesaver.js-npm": "1.0.1",
"globals": "17.12.0",
"grunt": "1.6.3",
"grunt-contrib-clean": "2.0.1",
"grunt-exec": "3.0.0",
- "grunt-npmcopy": "0.2.0",
- "grunt-sync": "0.8.2",
"jszip": "3.10.1",
"rollup": "4.63.1",
"rollup-plugin-polyfill-node": "0.13.0",
diff --git a/properties.json b/properties.json
index 39e4f924..8e08b282 100644
--- a/properties.json
+++ b/properties.json
@@ -1,7 +1,5 @@
{
"webappSrcDir" : "src/main/webapp",
- "webappTestDir" : "src/test/webapp",
- "webappBuildDir" : "build/public_html",
"unitTestsResourcesDir" : "src/test/resources",
"umdBuildDir" : "dist",
"umdDebugName" : "imsc.debug.js",
diff --git a/src/test/js/GenerateReferenceFilesTest.js b/src/test/js/GenerateReferenceFilesTest.js
index c6a3ef4e..8f5cde6a 100644
--- a/src/test/js/GenerateReferenceFilesTest.js
+++ b/src/test/js/GenerateReferenceFilesTest.js
@@ -3,7 +3,7 @@ import fs from "node:fs/promises";
import path from "node:path";
import { test } from "node:test";
import { fileURLToPath } from "node:url";
-import { renderTTMLInBrowser } from "../script/gen-renders-page.mjs";
+import { renderTestSuite } from "../script/render-harness.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REFERENCE_FILES_ROOT = path.resolve(__dirname, "..", "resources", "reference-files");
@@ -21,41 +21,38 @@ async function listFilesRecursively(dir) {
async function generateAndCompare(browserProduct) {
- await renderTTMLInBrowser(browserProduct, async (page) => {
+ const results = [];
- for (const reffilesRoot of REFFILES_ROOTS) {
+ for (const reffilesRoot of REFFILES_ROOTS) {
+ results.push([reffilesRoot, await renderTestSuite(browserProduct, reffilesRoot)]);
+ }
- const generatedFiles = await page.evaluate(async (root) => {
- // eslint-disable-next-line no-undef -- injected by gen-renders.js in the page context
- return await generateReferenceFiles(root);
- }, reffilesRoot);
+ for (const [reffilesRoot, generatedFiles] of results) {
- const referenceDir = path.join(REFERENCE_FILES_ROOT, path.basename(reffilesRoot));
+ const referenceDir = path.join(REFERENCE_FILES_ROOT, path.basename(reffilesRoot));
- const generatedPaths = Object.keys(generatedFiles).sort();
- const referencePaths = await listFilesRecursively(referenceDir);
+ const generatedPaths = Object.keys(generatedFiles).map((p) => p.replace(/^generated\//, "")).sort();
+ const referencePaths = await listFilesRecursively(referenceDir);
- assert.deepStrictEqual(
- generatedPaths,
- referencePaths,
- `generated files for ${reffilesRoot} do not match the set of reference files at ${referenceDir}`,
- );
-
- for (const relativePath of generatedPaths) {
+ assert.deepStrictEqual(
+ generatedPaths,
+ referencePaths,
+ `generated files for ${reffilesRoot} do not match the set of reference files at ${referenceDir}`,
+ );
- const referenceContents = await fs.readFile(path.join(referenceDir, relativePath), "utf8");
+ for (const relativePath of generatedPaths) {
- assert.strictEqual(
- generatedFiles[relativePath],
- referenceContents,
- `${reffilesRoot}/${relativePath} does not match its reference file`,
- );
+ const referenceContents = await fs.readFile(path.join(referenceDir, relativePath), "utf8");
- }
+ assert.strictEqual(
+ generatedFiles["generated/" + relativePath],
+ referenceContents,
+ `${reffilesRoot}/${relativePath} does not match its reference file`,
+ );
}
- });
+ }
}
diff --git a/src/test/script/gen-reference-files.mjs b/src/test/script/gen-reference-files.mjs
index 36cd1438..1a154adf 100644
--- a/src/test/script/gen-reference-files.mjs
+++ b/src/test/script/gen-reference-files.mjs
@@ -29,14 +29,16 @@
* Generates reference files for the IMSC 1 and IMSC 1.1 test suites, writing
* them to src/test/resources/reference-files//.
*
+ * Always uses Firefox, so the committed reference files come from one
+ * canonical, reproducible renderer.
+ *
* Usage: node src/test/script/gen-reference-files.mjs
- * [--browser=chrome|firefox]
*/
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
-import { renderTTMLInBrowser } from "./gen-renders-page.mjs";
+import { renderTestSuite } from "./render-harness.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const OUTPUT_ROOT = path.resolve(__dirname, "..", "resources", "reference-files");
@@ -44,31 +46,28 @@ const OUTPUT_ROOT = path.resolve(__dirname, "..", "resources", "reference-files"
const REFFILES_ROOTS = ["imsc-tests/imsc1", "imsc-tests/imsc1_1"];
async function main() {
- const browserArg = process.argv.find((a) => a.startsWith("--browser="));
- const browserProduct = browserArg ? browserArg.split("=")[1] : "firefox";
-
- await renderTTMLInBrowser(browserProduct, async (page) => {
- for (const reffilesRoot of REFFILES_ROOTS) {
- console.log(`Generating reference files for "${reffilesRoot}"...`);
+ const browserProduct = "firefox";
- const files = await page.evaluate(async (root) => {
- // eslint-disable-next-line no-undef -- injected by gen-renders.js in the page context
- return await generateReferenceFiles(root);
- }, reffilesRoot);
+ const results = [];
- const destDir = path.join(OUTPUT_ROOT, path.basename(reffilesRoot));
+ for (const reffilesRoot of REFFILES_ROOTS) {
+ console.log(`Generating reference files for "${reffilesRoot}"...`);
+ results.push([reffilesRoot, await renderTestSuite(browserProduct, reffilesRoot)]);
+ }
- fs.rmSync(destDir, { recursive: true, force: true });
+ for (const [reffilesRoot, files] of results) {
+ const destDir = path.join(OUTPUT_ROOT, path.basename(reffilesRoot));
- for (const [relativePath, contents] of Object.entries(files)) {
- const filePath = path.join(destDir, relativePath);
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
- fs.writeFileSync(filePath, contents);
- }
+ fs.rmSync(destDir, { recursive: true, force: true });
- console.log(`Wrote ${Object.keys(files).length} files to ${destDir}`);
+ for (const [relativePath, contents] of Object.entries(files)) {
+ const filePath = path.join(destDir, relativePath.replace(/^generated\//, ""));
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
+ fs.writeFileSync(filePath, contents);
}
- });
+
+ console.log(`Wrote ${Object.keys(files).length} files to ${destDir}`);
+ }
}
main().catch((err) => {
diff --git a/src/test/script/gen-render-package.mjs b/src/test/script/gen-render-package.mjs
index 104709d9..ebe5344e 100644
--- a/src/test/script/gen-render-package.mjs
+++ b/src/test/script/gen-render-package.mjs
@@ -34,7 +34,8 @@
import fs from "node:fs";
import path from "node:path";
-import { renderTTMLInBrowser } from "./gen-renders-page.mjs";
+import JSZip from "jszip";
+import { renderTestSuite } from "./render-harness.mjs";
async function main() {
const args = process.argv.slice(2).filter((a) => !a.startsWith("--"));
@@ -46,19 +47,15 @@ async function main() {
console.log(`Generating renders for "${reffilesRoot}"...`);
- const base64Zip = await renderTTMLInBrowser(browserProduct, (page) => page.evaluate(async (root) => {
- // eslint-disable-next-line no-undef -- injected by gen-renders.js in the page context
- const blob = await generateRenderPackageAsZip(root);
+ const files = await renderTestSuite(browserProduct, reffilesRoot, true);
- return new Promise((resolve, reject) => {
- const reader = new FileReader();
- reader.onload = () => resolve(reader.result.split(",")[1]);
- reader.onerror = () => reject(reader.error);
- reader.readAsDataURL(blob);
- });
- }, reffilesRoot));
+ const zip = new JSZip();
- fs.writeFileSync(outFile, Buffer.from(base64Zip, "base64"));
+ for (const [name, contents] of Object.entries(files)) {
+ zip.file(name, contents);
+ }
+
+ fs.writeFileSync(outFile, await zip.generateAsync({ type: "nodebuffer" }));
console.log(`Wrote ${outFile}`);
}
diff --git a/src/test/script/gen-renders-page.mjs b/src/test/script/gen-renders-page.mjs
deleted file mode 100644
index 3ccccca1..00000000
--- a/src/test/script/gen-renders-page.mjs
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * Copyright (c) Sandflow Consulting LLC
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * * Redistributions of source code must retain the above copyright notice, this
- * list of conditions and the following disclaimer.
- * * Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
-/*
- * Render TTML files in browser
- */
-
-import fs from "node:fs";
-import path from "node:path";
-import { fileURLToPath } from "node:url";
-import puppeteer from "puppeteer";
-
-const __dirname = path.dirname(fileURLToPath(import.meta.url));
-export const PUBLIC_DIR = path.resolve(__dirname, "..", "..", "..", "build", "public_html");
-
-const MIME_TYPES = {
- ".html": "text/html",
- ".js": "text/javascript",
- ".css": "text/css",
- ".json": "application/json",
- ".ttml": "application/xml",
- ".xml": "application/xml",
- ".png": "image/png",
-};
-
-/**
- * Renders a TTML file within the `gen-renders.html` page using puppeteer
- *
- * @param {string} browserProduct Puppeteer browser product to launch, e.g. "chrome" or "firefox"
- * @param {(page: import("puppeteer").Page) => Promise<*>} fn Callback invoked with the Puppeteer page, once gen-renders.html has loaded
- * @returns {Promise<*>} Whatever fn's returned promise resolves to
- */
-export async function renderTTMLInBrowser(browserProduct, fn) {
- if (!fs.existsSync(PUBLIC_DIR)) {
- throw new Error(`${PUBLIC_DIR} does not exist. Run "grunt build" first.`);
- }
-
- const browser = await puppeteer.launch({
- browser: browserProduct,
- headless: true,
- args: browserProduct === "chrome" ? ["--no-sandbox", "--disable-setuid-sandbox"] : [],
- });
-
- try {
- const page = await browser.newPage();
-
- page.on("console", (msg) => console.log(`[page] ${msg.text()}`));
- page.on("pageerror", (err) => console.error(`[page error] ${err}`));
-
- // Answers every request against a fake origin straight from
- // build/public_html, so no real HTTP server needs to be listening.
- await page.setRequestInterception(true);
-
- page.on("request", (request) => {
- // data: URIs (embedded images, generated SVGs) are self-contained
- // and don't need serving from disk; let the browser handle them.
- if (!request.url().startsWith("http://gen-renders.local")) {
- request.continue();
- return;
- }
-
- const urlPath = decodeURIComponent(new URL(request.url()).pathname);
- const filePath = path.join(PUBLIC_DIR, urlPath);
-
- if (!filePath.startsWith(PUBLIC_DIR)) {
- console.error(`[serve] 403 ${request.url()} (outside ${PUBLIC_DIR})`);
- request.respond({ status: 403 });
- return;
- }
-
- fs.readFile(filePath, (err, data) => {
- if (err) {
- console.error(`[serve] 404 ${request.url()} -> ${filePath}`);
- request.respond({ status: 404 });
- return;
- }
-
- const ext = path.extname(filePath).toLowerCase();
- request.respond({
- status: 200,
- contentType: MIME_TYPES[ext] || "application/octet-stream",
- body: data,
- });
- });
- });
-
- await page.goto("http://gen-renders.local/gen-renders.html", { waitUntil: "load" });
-
- return await fn(page);
- } finally {
- await browser.close();
- }
-}
diff --git a/src/test/script/png-diff.sh b/src/test/script/png-diff.sh
deleted file mode 100644
index 0c78c20f..00000000
--- a/src/test/script/png-diff.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/sh
-#compare $2 $1 png:- | montage -geometry +4+4 $2 - $1 png:- | imdisplay -title "$1" -
-compare $2 $1 png:- | montage -geometry +4+4 $2 - $1 show:
\ No newline at end of file
diff --git a/src/test/script/refpngcompare.py b/src/test/script/refpngcompare.py
deleted file mode 100644
index d0d9aaed..00000000
--- a/src/test/script/refpngcompare.py
+++ /dev/null
@@ -1,56 +0,0 @@
-import argparse
-import subprocess
-import os
-import fnmatch
-import re
-import sys
-
-parser = argparse.ArgumentParser()
-
-parser.add_argument("ref_dir", help = "Path of the Reference directory")
-
-parser.add_argument("render_dir", help = "Path of the Generated directory")
-
-parser.add_argument("-d", help = "Specifies whether a diff of images is output")
-
-parser.add_argument("-t", type=float, help = "Specifies the difference threshold", default=0.00001)
-
-args = parser.parse_args()
-
-for dir, dirs, files in os.walk(args.ref_dir):
- for file in files:
- reffile = os.path.join(dir, file)
- destdir = os.path.join(args.render_dir, os.path.relpath(dir, args.ref_dir))
- genfile = os.path.join(destdir, file)
-
- if not os.path.exists(genfile):
- print("File " + genfile + " does not exist")
- continue
-
- if fnmatch.fnmatch(file, '*.png'):
- try:
- subprocess.check_output(["magick", "compare", '-metric', 'mse', reffile, genfile, 'null:'], stderr=subprocess.STDOUT, universal_newlines=True)
- except subprocess.CalledProcessError as err:
- m = re.search('([^\(]+)\(([^\)]+)', err.output)
- r = float(m.group(2))
- if (r > args.t) :
- print(reffile + ": " + str(r))
- if args.d is not None:
- diffdir = args.d
- #diffdir = os.path.join(args.d, os.path.relpath(dir, args.ref_dir))
- if not os.path.exists(diffdir):
- os.makedirs(diffdir)
- difffile = os.path.join(args.d, os.path.relpath(dir, args.ref_dir) + "-" + file)
- p1 = subprocess.Popen(["magick", "compare", reffile, genfile, "png:-"], stdout=subprocess.PIPE)
- p2 = subprocess.Popen(["magick", "montage", "-mode", "concatenate", reffile, "-", genfile, difffile], stdin=p1.stdout)
- p1.stdout.close()
- p2.communicate()
-
-
-# elif not fnmatch.fnmatch(file, 'manifest.json'):
-# try:
-# subprocess.check_output(["diff", '-w', reffile, genfile], stderr=subprocess.STDOUT, universal_newlines=True)
-# except subprocess.CalledProcessError as err:
-# print(err.output)
-
-
diff --git a/src/test/script/render-harness.browser.js b/src/test/script/render-harness.browser.js
new file mode 100644
index 00000000..b3af67a2
--- /dev/null
+++ b/src/test/script/render-harness.browser.js
@@ -0,0 +1,214 @@
+/*
+ * Copyright (c) Sandflow Consulting LLC
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/*
+ * Browser-side rendering helpers for the IMSC test suite.
+ * the global `imsc` is injected alongside this script.
+ */
+
+/* global imsc */
+
+const errorHandler = {
+ info: function (msg) {
+ console.log("info: " + msg);
+ return false;
+ },
+ warn: function (msg) {
+ console.log("warn: " + msg);
+ return false;
+ },
+ error: function (msg) {
+ console.log("error: " + msg);
+ return false;
+ },
+ fatal: function (msg) {
+ console.log("fatal: " + msg);
+ return false;
+ },
+};
+
+async function asyncLoadFile(url) {
+
+ const response = await fetch(url);
+
+ if (!response.ok) {
+ throw {
+ status: response.status,
+ statusText: response.statusText,
+ };
+ }
+
+ return response.text();
+}
+
+function getTestListPath(reffiles_root) {
+ return reffiles_root + "/tests.json";
+}
+
+function getReferenceFilePath(reffiles_root, reffile_path) {
+ return reffiles_root + "/ttml/" + reffile_path;
+}
+
+function getReferenceFileDirectory(reffiles_root, reffile_path) {
+ return reffiles_root + "/ttml/" + reffile_path.substring(0, Math.max(reffile_path.lastIndexOf("/"), reffile_path.lastIndexOf("\\")) + 1);
+}
+
+function getTestName(reffile_path) {
+ return reffile_path.split("\\").pop().split("/").pop().split(".")[0];
+}
+
+function customReplace(k, v) {
+ if (k === "end" && v === Number.POSITIVE_INFINITY) return "Infinity";
+ return v;
+}
+
+function getRenderDiv() {
+
+ let vdiv = document.getElementById("render-div");
+
+ if (!vdiv) {
+ vdiv = document.createElement("div");
+ vdiv.id = "render-div";
+ vdiv.style.position = "relative";
+ vdiv.style.background = "#A9A9A9";
+ document.body.appendChild(vdiv);
+ }
+
+ return vdiv;
+}
+
+// Set by openTTMLFile(), read by renderEvent(): the page keeps at most one
+// parsed document "open" at a time, rendered sequentially event by event.
+let currentDoc = null;
+let currentReffileDir = "";
+
+/**
+ * Fetches and parses tests.json at reffiles_root, returning the list of test
+ * file descriptors it contains.
+ */
+// eslint-disable-next-line no-unused-vars -- called from Node via page.evaluate
+async function loadTestList(reffiles_root) {
+ const contents = await asyncLoadFile(getTestListPath(reffiles_root));
+ return JSON.parse(contents);
+}
+
+/**
+ * Loads and parses a single reference file, making it the current document
+ * that renderEvent() renders events from. Returns its name, its media time
+ * events, and its JSON document (already stringified, so that "Infinity"
+ * survives the trip back to Node -- see customReplace()).
+ */
+// eslint-disable-next-line no-unused-vars -- called from Node via page.evaluate
+async function openTTMLFile(reffiles_root, finfo) {
+
+ const contents = await asyncLoadFile(getReferenceFilePath(reffiles_root, finfo.path));
+
+ currentDoc = imsc.fromXML(contents.replace(/\r\n/g, "\n"), errorHandler);
+ currentReffileDir = getReferenceFileDirectory(reffiles_root, finfo.path);
+
+ return {
+ "name": finfo.name || getTestName(finfo.path),
+ "events": currentDoc.getMediaTimeEvents(),
+ "docJson": JSON.stringify(currentDoc, customReplace, 2),
+ };
+}
+
+/**
+ * Renders the current document (see openTTMLFile()) at offset into
+ * #render-div, sized width x height, resolving any images it references
+ * first. Returns the ISD (already stringified, as with openTTMLFile()) and
+ * the rendered HTML; the caller is expected to screenshot #render-div itself
+ * to obtain a PNG.
+ */
+// eslint-disable-next-line no-unused-vars -- called from Node via page.evaluate
+async function renderEvent(offset, params, width, height) {
+
+ const isd = imsc.generateISD(currentDoc, offset);
+
+ const vdiv = getRenderDiv();
+
+ vdiv.style.height = height + "px";
+ vdiv.style.width = width + "px";
+
+ while (vdiv.firstChild) {
+ vdiv.removeChild(vdiv.firstChild);
+ }
+
+ /* resolve images referenced by the ISD so they are embedded in the HTML */
+
+ const imgs = [];
+
+ const imgr = function (uri, img) {
+ const p = (async function () {
+
+ const url = await new Promise(function (resolve) {
+
+ const png = new Image();
+
+ png.onload = function () {
+ const canvas = document.createElement("canvas");
+ canvas.width = this.naturalWidth;
+ canvas.height = this.naturalHeight;
+
+ const ctx = canvas.getContext("2d");
+
+ ctx.drawImage(this, 0, 0);
+
+ resolve(canvas.toDataURL("image/png"));
+ };
+
+ png.src = currentReffileDir + uri;
+
+ });
+
+ img.src = url;
+
+ })();
+
+ imgs.push(p);
+
+ return null;
+ };
+
+ imsc.renderHTML(
+ isd,
+ vdiv,
+ imgr,
+ height,
+ width,
+ params.displayForcedOnlyMode === true,
+ errorHandler,
+ );
+
+ await Promise.all(imgs);
+
+ const html = vdiv.innerHTML.replace(/>\n<");
+
+ return {
+ "isdJson": JSON.stringify(isd, customReplace, 2),
+ "html": html,
+ };
+}
diff --git a/src/test/script/render-harness.mjs b/src/test/script/render-harness.mjs
new file mode 100644
index 00000000..ca0a7ce6
--- /dev/null
+++ b/src/test/script/render-harness.mjs
@@ -0,0 +1,192 @@
+/*
+ * Copyright (c) Sandflow Consulting LLC
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/*
+ * Render TTML files in browser
+ */
+
+import fs from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+import puppeteer from "puppeteer";
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+export const RESOURCES_DIR = path.resolve(__dirname, "..", "resources");
+export const IMSC_BUNDLE_PATH = path.resolve(__dirname, "..", "..", "..", "dist", "imsc.debug.js");
+const HARNESS_PATH = path.resolve(__dirname, "render-harness.browser.js");
+
+/* We are not loading from an HTTP server, but instead intecerpting all load calls */
+const FAKE_ORIGIN = "http://gen-renders.local";
+
+/* Dimensions of the rendered visuals */
+const RENDER_WIDTH = 640;
+const RENDER_HEIGHT = 360;
+
+const MIME_TYPES = {
+ ".json": "application/json",
+ ".ttml": "application/xml",
+ ".xml": "application/xml",
+ ".png": "image/png",
+};
+
+/**
+ * Renders an IMSC test suite, returning a list containing file names and
+ * corresponding file contents. JSON/HTML paths are nested under "generated/"
+ * and, when includeRenders is true, PNGs under "png/" -- matching the layout
+ * expected in a render package.
+ *
+ * @param {string} browserProduct Puppeteer browser product to launch, e.g.
+ * "chrome" or "firefox"
+ * @param {string} reffilesRoot e.g. "imsc-tests/imsc1"
+ * @param {boolean} includeRenders Whether to also render/screenshot each event
+ * @returns {Promise