refactor: add ScriptRunner class

Change-Id: I118adb9ec338e88b40321208b84228886bb6b590
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();