refactor: add ScriptRunner class

Change-Id: I118adb9ec338e88b40321208b84228886bb6b590
diff --git a/jest.config.js b/jest.config.js
index cfbbf85..b394077 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -4,194 +4,9 @@
  */
 
 module.exports = {
-  moduleFileExtensions: ['js', 'mjs'],
-  testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.m?[jt]sx?$",
-  // All imported modules in your tests should be mocked automatically
-  // automock: false,
-
-  // Stop running tests after `n` failures
-  // bail: 0,
-
-  // The directory where Jest should store its cached dependency information
-  // cacheDirectory: "/private/var/folders/dq/pgsjklbj1sx3mndxpcq41swc0000gn/T/jest_dx",
-
-  // Automatically clear mock calls, instances, contexts and results before every test
-  // clearMocks: false,
-
-  // Indicates whether the coverage information should be collected while executing the test
-  // collectCoverage: false,
-
-  // An array of glob patterns indicating a set of files for which coverage information should be collected
-  // collectCoverageFrom: undefined,
-
-  // The directory where Jest should output its coverage files
-  // coverageDirectory: undefined,
-
-  // An array of regexp pattern strings used to skip coverage collection
-  // coveragePathIgnorePatterns: [
-  //   "/node_modules/"
-  // ],
-
-  // Indicates which provider should be used to instrument code for coverage
-  coverageProvider: "v8",
-
-  // A list of reporter names that Jest uses when writing coverage reports
-  // coverageReporters: [
-  //   "json",
-  //   "text",
-  //   "lcov",
-  //   "clover"
-  // ],
-
-  // An object that configures minimum threshold enforcement for coverage results
-  // coverageThreshold: undefined,
-
-  // A path to a custom dependency extractor
-  // dependencyExtractor: undefined,
-
-  // Make calling deprecated APIs throw helpful error messages
-  // errorOnDeprecated: false,
-
-  // The default configuration for fake timers
-  // fakeTimers: {
-  //   "enableGlobally": false
-  // },
-
-  // Force coverage collection from ignored files using an array of glob patterns
-  // forceCoverageMatch: [],
-
-  // A path to a module which exports an async function that is triggered once before all test suites
-  // globalSetup: undefined,
-
-  // A path to a module which exports an async function that is triggered once after all test suites
-  // globalTeardown: undefined,
-
-  // A set of global variables that need to be available in all test environments
-  // globals: {},
-
-  // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
-  // maxWorkers: "50%",
-
-  // An array of directory names to be searched recursively up from the requiring module's location
-  // moduleDirectories: [
-  //   "node_modules"
-  // ],
-
-  // An array of file extensions your modules use
-  // moduleFileExtensions: [
-  //   "js",
-  //   "mjs",
-  //   "cjs",
-  //   "jsx",
-  //   "ts",
-  //   "tsx",
-  //   "json",
-  //   "node"
-  // ],
-
-  // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
-  // moduleNameMapper: {},
-
-  // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
-  // modulePathIgnorePatterns: [],
-
-  // Activates notifications for test results
-  // notify: false,
-
-  // An enum that specifies notification mode. Requires { notify: true }
-  // notifyMode: "failure-change",
-
-  // A preset that is used as a base for Jest's configuration
-  // preset: undefined,
-
-  // Run tests from one or more projects
-  // projects: undefined,
-
-  // Use this configuration option to add custom reporters to Jest
-  // reporters: undefined,
-
-  // Automatically reset mock state before every test
-  // resetMocks: false,
-
-  // Reset the module registry before running each individual test
-  // resetModules: false,
-
-  // A path to a custom resolver
-  // resolver: undefined,
-
-  // Automatically restore mock state and implementation before every test
-  // restoreMocks: false,
-
-  // The root directory that Jest should scan for tests and modules within
-  // rootDir: undefined,
-
-  // A list of paths to directories that Jest should use to search for files in
-  // roots: [
-  //   "<rootDir>"
-  // ],
-
-  // Allows you to use a custom runner instead of Jest's default test runner
-  // runner: "jest-runner",
-
-  // The paths to modules that run some code to configure or set up the testing environment before each test
-  // setupFiles: [],
-
-  // A list of paths to modules that run some code to configure or set up the testing framework before each test
-  // setupFilesAfterEnv: [],
-
-  // The number of seconds after which a test is considered as slow and reported as such in the results.
-  // slowTestThreshold: 5,
-
-  // A list of paths to snapshot serializer modules Jest should use for snapshot testing
-  // snapshotSerializers: [],
-
-  // The test environment that will be used for testing
-  testEnvironment: "jsdom",
-
-  // Options that will be passed to the testEnvironment
-  // testEnvironmentOptions: {},
-
-  // Adds a location field to test results
-  // testLocationInResults: false,
-
-  // The glob patterns Jest uses to detect test files
-  // testMatch: [
-  //   "**/__tests__/**/*.[jt]s?(x)",
-  //   "**/?(*.)+(spec|test).[tj]s?(x)"
-  // ],
-
-  // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
-  // testPathIgnorePatterns: [
-  //   "/node_modules/"
-  // ],
-
-  // The regexp pattern or array of patterns that Jest uses to detect test files
-  // testRegex: [],
-
-  // This option allows the use of a custom results processor
-  // testResultsProcessor: undefined,
-
-  // This option allows use of a custom test runner
-  // testRunner: "jest-circus/runner",
-
-  // A map from regular expressions to paths to transformers
-  // transform: undefined,
-
-  // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
-  // transformIgnorePatterns: [
-  //   "/node_modules/",
-  //   "\\.pnp\\.[^\\/]+$"
-  // ],
-
-  // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
-  // unmockedModulePathPatterns: undefined,
-
-  // Indicates whether each individual test should be reported during the run
-  // verbose: undefined,
-
-  // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
-  // watchPathIgnorePatterns: [],
-
-  // Whether to use watchman for file crawling
-  // watchman: true,
+  preset : 'ts-jest/presets/js-with-ts-esm',
+  moduleFileExtensions : ['js', 'mjs', 'ts'],
+  testRegex : '(/__tests__/.*|(\\.|/)(test|spec))\\.m?[jt]sx?$',
+  coverageProvider : 'v8',
+  testEnvironment : 'jsdom',
 };
diff --git a/package-lock.json b/package-lock.json
index 28b88de..bda2678 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -29,6 +29,7 @@
       },
       "devDependencies": {
         "@babel/plugin-transform-modules-commonjs": "7.24.1",
+        "@jest/globals": "^29.7.0",
         "@lit/localize-tools": "0.7.2",
         "copy-webpack-plugin": "12.0.2",
         "css-loader": "7.1.1",
@@ -42,6 +43,7 @@
         "sass": "1.77.0",
         "sass-loader": "14.2.1",
         "style-loader": "4.0.0",
+        "ts-jest": "^29.1.2",
         "ts-loader": "^9.5.1",
         "ts-node": "^10.9.2",
         "typescript": "^5.4.5",
@@ -342,9 +344,9 @@
       }
     },
     "node_modules/@babel/helper-plugin-utils": {
-      "version": "7.24.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz",
-      "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==",
+      "version": "7.24.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.5.tgz",
+      "integrity": "sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==",
       "dev": true,
       "engines": {
         "node": ">=6.9.0"
@@ -363,39 +365,39 @@
       }
     },
     "node_modules/@babel/helper-split-export-declaration": {
-      "version": "7.22.6",
-      "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
-      "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
+      "version": "7.24.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz",
+      "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==",
       "dev": true,
       "dependencies": {
-        "@babel/types": "^7.22.5"
+        "@babel/types": "^7.24.5"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helper-string-parser": {
-      "version": "7.22.5",
-      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
-      "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==",
+      "version": "7.24.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz",
+      "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==",
       "dev": true,
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helper-validator-identifier": {
-      "version": "7.22.20",
-      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
-      "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
+      "version": "7.24.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz",
+      "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==",
       "dev": true,
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/helper-validator-option": {
-      "version": "7.18.6",
-      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz",
-      "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==",
+      "version": "7.23.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz",
+      "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==",
       "dev": true,
       "engines": {
         "node": ">=6.9.0"
@@ -573,12 +575,12 @@
       }
     },
     "node_modules/@babel/plugin-syntax-jsx": {
-      "version": "7.23.3",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz",
-      "integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==",
+      "version": "7.24.1",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.1.tgz",
+      "integrity": "sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA==",
       "dev": true,
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.22.5"
+        "@babel/helper-plugin-utils": "^7.24.0"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -675,12 +677,12 @@
       }
     },
     "node_modules/@babel/plugin-syntax-typescript": {
-      "version": "7.23.3",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz",
-      "integrity": "sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==",
+      "version": "7.24.1",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.1.tgz",
+      "integrity": "sha512-Yhnmvy5HZEnHUty6i++gcfH1/l68AHnItFHnaCv6hn9dNh0hQvvQJsxpi4BMBFN5DLeHBuucT/0DgzXif/OyRw==",
       "dev": true,
       "dependencies": {
-        "@babel/helper-plugin-utils": "^7.22.5"
+        "@babel/helper-plugin-utils": "^7.24.0"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -751,13 +753,13 @@
       }
     },
     "node_modules/@babel/types": {
-      "version": "7.23.0",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
-      "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
+      "version": "7.24.5",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz",
+      "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==",
       "dev": true,
       "dependencies": {
-        "@babel/helper-string-parser": "^7.22.5",
-        "@babel/helper-validator-identifier": "^7.22.20",
+        "@babel/helper-string-parser": "^7.24.1",
+        "@babel/helper-validator-identifier": "^7.24.5",
         "to-fast-properties": "^2.0.0"
       },
       "engines": {
@@ -3620,6 +3622,18 @@
         "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
       }
     },
+    "node_modules/bs-logger": {
+      "version": "0.2.6",
+      "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz",
+      "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==",
+      "dev": true,
+      "dependencies": {
+        "fast-json-stable-stringify": "2.x"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/bser": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz",
@@ -6303,6 +6317,12 @@
         "node": ">=8"
       }
     },
+    "node_modules/lodash.memoize": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
+      "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==",
+      "dev": true
+    },
     "node_modules/lodash.merge": {
       "version": "4.6.2",
       "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -7680,6 +7700,49 @@
         "node": ">=12"
       }
     },
+    "node_modules/ts-jest": {
+      "version": "29.1.2",
+      "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.2.tgz",
+      "integrity": "sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==",
+      "dev": true,
+      "dependencies": {
+        "bs-logger": "0.x",
+        "fast-json-stable-stringify": "2.x",
+        "jest-util": "^29.0.0",
+        "json5": "^2.2.3",
+        "lodash.memoize": "4.x",
+        "make-error": "1.x",
+        "semver": "^7.5.3",
+        "yargs-parser": "^21.0.1"
+      },
+      "bin": {
+        "ts-jest": "cli.js"
+      },
+      "engines": {
+        "node": "^16.10.0 || ^18.0.0 || >=20.0.0"
+      },
+      "peerDependencies": {
+        "@babel/core": ">=7.0.0-beta.0 <8",
+        "@jest/types": "^29.0.0",
+        "babel-jest": "^29.0.0",
+        "jest": "^29.0.0",
+        "typescript": ">=4.3 <6"
+      },
+      "peerDependenciesMeta": {
+        "@babel/core": {
+          "optional": true
+        },
+        "@jest/types": {
+          "optional": true
+        },
+        "babel-jest": {
+          "optional": true
+        },
+        "esbuild": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/ts-loader": {
       "version": "9.5.1",
       "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz",
@@ -8520,9 +8583,9 @@
       }
     },
     "@babel/helper-plugin-utils": {
-      "version": "7.24.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz",
-      "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==",
+      "version": "7.24.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.5.tgz",
+      "integrity": "sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==",
       "dev": true
     },
     "@babel/helper-simple-access": {
@@ -8535,30 +8598,30 @@
       }
     },
     "@babel/helper-split-export-declaration": {
-      "version": "7.22.6",
-      "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
-      "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
+      "version": "7.24.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz",
+      "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==",
       "dev": true,
       "requires": {
-        "@babel/types": "^7.22.5"
+        "@babel/types": "^7.24.5"
       }
     },
     "@babel/helper-string-parser": {
-      "version": "7.22.5",
-      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
-      "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==",
+      "version": "7.24.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz",
+      "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==",
       "dev": true
     },
     "@babel/helper-validator-identifier": {
-      "version": "7.22.20",
-      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
-      "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
+      "version": "7.24.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz",
+      "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==",
       "dev": true
     },
     "@babel/helper-validator-option": {
-      "version": "7.18.6",
-      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz",
-      "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==",
+      "version": "7.23.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz",
+      "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==",
       "dev": true
     },
     "@babel/helpers": {
@@ -8693,12 +8756,12 @@
       }
     },
     "@babel/plugin-syntax-jsx": {
-      "version": "7.23.3",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz",
-      "integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==",
+      "version": "7.24.1",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.1.tgz",
+      "integrity": "sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA==",
       "dev": true,
       "requires": {
-        "@babel/helper-plugin-utils": "^7.22.5"
+        "@babel/helper-plugin-utils": "^7.24.0"
       }
     },
     "@babel/plugin-syntax-logical-assignment-operators": {
@@ -8765,12 +8828,12 @@
       }
     },
     "@babel/plugin-syntax-typescript": {
-      "version": "7.23.3",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz",
-      "integrity": "sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==",
+      "version": "7.24.1",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.1.tgz",
+      "integrity": "sha512-Yhnmvy5HZEnHUty6i++gcfH1/l68AHnItFHnaCv6hn9dNh0hQvvQJsxpi4BMBFN5DLeHBuucT/0DgzXif/OyRw==",
       "dev": true,
       "requires": {
-        "@babel/helper-plugin-utils": "^7.22.5"
+        "@babel/helper-plugin-utils": "^7.24.0"
       }
     },
     "@babel/plugin-transform-modules-commonjs": {
@@ -8822,13 +8885,13 @@
       }
     },
     "@babel/types": {
-      "version": "7.23.0",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
-      "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
+      "version": "7.24.5",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz",
+      "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==",
       "dev": true,
       "requires": {
-        "@babel/helper-string-parser": "^7.22.5",
-        "@babel/helper-validator-identifier": "^7.22.20",
+        "@babel/helper-string-parser": "^7.24.1",
+        "@babel/helper-validator-identifier": "^7.24.5",
         "to-fast-properties": "^2.0.0"
       }
     },
@@ -11199,6 +11262,15 @@
         "update-browserslist-db": "^1.0.13"
       }
     },
+    "bs-logger": {
+      "version": "0.2.6",
+      "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz",
+      "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==",
+      "dev": true,
+      "requires": {
+        "fast-json-stable-stringify": "2.x"
+      }
+    },
     "bser": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz",
@@ -13219,6 +13291,12 @@
         "p-locate": "^4.1.0"
       }
     },
+    "lodash.memoize": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
+      "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==",
+      "dev": true
+    },
     "lodash.merge": {
       "version": "4.6.2",
       "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -14154,6 +14232,22 @@
         "punycode": "^2.1.1"
       }
     },
+    "ts-jest": {
+      "version": "29.1.2",
+      "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.2.tgz",
+      "integrity": "sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==",
+      "dev": true,
+      "requires": {
+        "bs-logger": "0.x",
+        "fast-json-stable-stringify": "2.x",
+        "jest-util": "^29.0.0",
+        "json5": "^2.2.3",
+        "lodash.memoize": "4.x",
+        "make-error": "1.x",
+        "semver": "^7.5.3",
+        "yargs-parser": "^21.0.1"
+      }
+    },
     "ts-loader": {
       "version": "9.5.1",
       "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz",
diff --git a/package.json b/package.json
index bcfe8a9..c9f0839 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,7 @@
   ],
   "devDependencies": {
     "@babel/plugin-transform-modules-commonjs": "7.24.1",
+    "@jest/globals": "^29.7.0",
     "@lit/localize-tools": "0.7.2",
     "copy-webpack-plugin": "12.0.2",
     "css-loader": "7.1.1",
@@ -39,6 +40,7 @@
     "sass": "1.77.0",
     "sass-loader": "14.2.1",
     "style-loader": "4.0.0",
+    "ts-jest": "^29.1.2",
     "ts-loader": "^9.5.1",
     "ts-node": "^10.9.2",
     "typescript": "^5.4.5",
diff --git a/src/common/architecture/scripts/ScriptRunner.test.ts b/src/common/architecture/scripts/ScriptRunner.test.ts
new file mode 100644
index 0000000..7435c8e
--- /dev/null
+++ b/src/common/architecture/scripts/ScriptRunner.test.ts
@@ -0,0 +1,83 @@
+import Script, {
+  ScriptEnvironment,
+  ScriptPage,
+  ScriptRunPhase,
+} from './Script';
+import { beforeEach, expect, it, jest } from '@jest/globals';
+import ScriptRunner from './ScriptRunner';
+
+interface FakeScriptOptions {
+  id: string;
+  priority: number;
+}
+
+class FakeScript extends Script {
+  id: string;
+  priority: number;
+  page: ScriptPage.CommunityConsole;
+  environment: ScriptEnvironment.ContentScript;
+  runPhase: ScriptRunPhase.Main;
+
+  constructor(options: FakeScriptOptions) {
+    super();
+    this.id = options.id;
+    this.priority = options.priority;
+  }
+
+  execute() {}
+}
+
+const fakeScriptMock = jest
+  .spyOn(FakeScript.prototype, 'execute')
+  .mockImplementation(function() {
+    return (this as FakeScript).id;
+  });
+
+beforeEach(() => {
+  jest.clearAllMocks();
+});
+
+it('scripts run in the correct order based on priority', () => {
+  const scriptsConfig = [
+    {
+      script: new FakeScript({
+        id: '1',
+        priority: 1,
+      }),
+      expectedRunPosition: 2,
+    },
+    {
+      script: new FakeScript({
+        id: '2',
+        priority: 1000,
+      }),
+      expectedRunPosition: 3,
+    },
+    {
+      script: new FakeScript({
+        id: '3',
+        priority: 0,
+      }),
+      expectedRunPosition: 1,
+    },
+    {
+      script: new FakeScript({
+        id: '4',
+        priority: 2 ** 31,
+      }),
+      expectedRunPosition: 4,
+    },
+  ];
+
+  const scriptRunner = new ScriptRunner();
+  scriptRunner.add(...scriptsConfig.map((config) => config.script));
+  scriptRunner.run();
+
+  expect(fakeScriptMock).toHaveBeenCalledTimes(scriptsConfig.length);
+  for (const config of scriptsConfig) {
+    expect(fakeScriptMock).toHaveNthReturnedWith(
+      config.expectedRunPosition,
+      config.script.id,
+    );
+  }
+});
diff --git a/src/common/architecture/scripts/ScriptRunner.ts b/src/common/architecture/scripts/ScriptRunner.ts
new file mode 100644
index 0000000..4a2ed2d
--- /dev/null
+++ b/src/common/architecture/scripts/ScriptRunner.ts
@@ -0,0 +1,20 @@
+import Script from './Script';
+
+export default class ScriptRunner {
+  private scripts: Script[] = [];
+
+  add(...scripts: Script[]) {
+    this.scripts.push(...scripts);
+  }
+
+  run() {
+    this.scripts.sort((a, b) => {
+      if (a.priority < b.priority) return -1;
+      if (a.priority > b.priority) return 1;
+      return 0;
+    });
+    for (const script of this.scripts) {
+      script.execute();
+    }
+  }
+}
diff --git a/src/features/Features.ts b/src/features/Features.ts
index 98a4b96..8a13700 100644
--- a/src/features/Features.ts
+++ b/src/features/Features.ts
@@ -4,6 +4,7 @@
   ScriptPage,
   ScriptRunPhase,
 } from '../common/architecture/scripts/Script';
+import ScriptRunner from '../common/architecture/scripts/ScriptRunner';
 import AutoRefreshFeature from './autoRefresh/autoRefresh.feature';
 import InfiniteScrollFeature from './infiniteScroll/infiniteScroll.feature';
 
@@ -22,13 +23,11 @@
   ];
   private initializedFeatures: Feature[];
 
-  runScripts(context: Context) {
-    const scripts = this.getScripts(context).sort((a, b) =>
-      a.priority < b.priority ? -1 : 1,
-    );
-    for (const script of scripts) {
-      script.execute();
-    }
+  getScriptRunner(context: Context) {
+    const scripts = this.getScripts(context);
+    const scriptRunner = new ScriptRunner();
+    scriptRunner.add(...scripts);
+    return scriptRunner;
   }
 
   getScripts(context: Context) {
diff --git a/src/platforms/communityConsole/entryPoints/main.ts b/src/platforms/communityConsole/entryPoints/main.ts
index c7d223b..03d5eba 100644
--- a/src/platforms/communityConsole/entryPoints/main.ts
+++ b/src/platforms/communityConsole/entryPoints/main.ts
@@ -4,13 +4,18 @@
   ScriptRunPhase,
 } from '../../../common/architecture/scripts/Script';
 import Features from '../../../features/Features';
+import ScriptRunner from '../../../common/architecture/scripts/ScriptRunner';
 
 // Run legacy Javascript entry point
 import '../../../contentScripts/communityConsole/main';
 
 const features = new Features();
-features.runScripts({
+const scripts = features.getScripts({
   page: ScriptPage.CommunityConsole,
   environment: ScriptEnvironment.ContentScript,
   runPhase: ScriptRunPhase.Main,
 });
+
+const scriptRunner = new ScriptRunner();
+scriptRunner.add(...scripts);
+scriptRunner.run();
diff --git a/src/platforms/communityConsole/entryPoints/start.ts b/src/platforms/communityConsole/entryPoints/start.ts
index b7cb6eb..2410f2c 100644
--- a/src/platforms/communityConsole/entryPoints/start.ts
+++ b/src/platforms/communityConsole/entryPoints/start.ts
@@ -4,13 +4,18 @@
   ScriptRunPhase,
 } from '../../../common/architecture/scripts/Script';
 import Features from '../../../features/Features';
+import ScriptRunner from '../../../common/architecture/scripts/ScriptRunner';
 
 // Run legacy Javascript entry point
 import '../../../contentScripts/communityConsole/start';
 
 const features = new Features();
-features.runScripts({
+const scripts = features.getScripts({
   page: ScriptPage.CommunityConsole,
   environment: ScriptEnvironment.ContentScript,
   runPhase: ScriptRunPhase.Start,
 });
+
+const scriptRunner = new ScriptRunner();
+scriptRunner.add(...scripts);
+scriptRunner.run();
diff --git a/tsconfig.json b/tsconfig.json
index 363c4b1..ae31377 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -7,6 +7,7 @@
     "module": "es6",
     "target": "es2019",
     "moduleResolution": "bundler",
+    "esModuleInterop": true,
     "allowJs": true
   },
   "ts-node": {