feat(scripts): add sorted scripts provider with new runAfter field

This scripts provider provides scripts sorted by priority and taking
into account the new runAfter field for scripts.

Bug: twpowertools:226
Change-Id: I40e39121f5c18a04eeff932c30dc2c4277993bde
diff --git a/jest.config.js b/jest.config.js
index c468c76..19e70a7 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -8,5 +8,6 @@
   moduleFileExtensions : ['js', 'mjs', 'ts'],
   testRegex : '(/__tests__/.*|(\\.|/)(test|spec))\\.m?[jt]sx?$',
   coverageProvider : 'v8',
+  coverageDirectory : './out/coverage/',
   testEnvironment : 'jsdom',
 };
diff --git a/package.json b/package.json
index 407a421..fc575ca 100644
--- a/package.json
+++ b/package.json
@@ -34,6 +34,7 @@
     "@babel/plugin-transform-modules-commonjs": "7.25.9",
     "@jest/globals": "29.7.0",
     "@lit/localize-tools": "0.8.0",
+    "@testing-library/dom": "^10.4.0",
     "@types/chrome": "0.0.280",
     "@types/eslint__js": "8.42.3",
     "@types/node": "20.17.6",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d9be49c..608e48c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -69,6 +69,9 @@
       '@lit/localize-tools':
         specifier: 0.8.0
         version: 0.8.0
+      '@testing-library/dom':
+        specifier: ^10.4.0
+        version: 10.4.0
       '@types/chrome':
         specifier: 0.0.280
         version: 0.0.280
@@ -161,18 +164,10 @@
     resolution: {integrity: sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==}
     engines: {node: '>=6.0.0'}
 
-  '@babel/code-frame@7.22.13':
-    resolution: {integrity: sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==}
-    engines: {node: '>=6.9.0'}
-
   '@babel/code-frame@7.24.7':
     resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==}
     engines: {node: '>=6.9.0'}
 
-  '@babel/code-frame@7.25.7':
-    resolution: {integrity: sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==}
-    engines: {node: '>=6.9.0'}
-
   '@babel/code-frame@7.26.0':
     resolution: {integrity: sha512-INCKxTtbXtcNbUZ3YXutwMpEleqttcswhAdee7dhuoVrD2cnuc3PqtERBtxkX5nziX9vnBL8WXmSGwv8CuPV6g==}
     engines: {node: '>=6.9.0'}
@@ -247,10 +242,6 @@
     resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==}
     engines: {node: '>=6.9.0'}
 
-  '@babel/helper-validator-identifier@7.25.7':
-    resolution: {integrity: sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==}
-    engines: {node: '>=6.9.0'}
-
   '@babel/helper-validator-identifier@7.25.9':
     resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==}
     engines: {node: '>=6.9.0'}
@@ -263,18 +254,10 @@
     resolution: {integrity: sha512-PBPjs5BppzsGaxHQCDKnZ6Gd9s6xl8bBCluz3vEInLGRJmnZan4F6BYCeqtyXqkk4W5IlPmjK4JlOuZkpJ3xZA==}
     engines: {node: '>=6.9.0'}
 
-  '@babel/highlight@7.22.20':
-    resolution: {integrity: sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==}
-    engines: {node: '>=6.9.0'}
-
   '@babel/highlight@7.24.7':
     resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==}
     engines: {node: '>=6.9.0'}
 
-  '@babel/highlight@7.25.7':
-    resolution: {integrity: sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==}
-    engines: {node: '>=6.9.0'}
-
   '@babel/parser@7.25.7':
     resolution: {integrity: sha512-aZn7ETtQsjjGG5HruveUK06cU3Hljuhd9Iojm4M8WWv3wLE6OkE5PWbDUkItmMgegmccaITudyuW5RPYrYlgWw==}
     engines: {node: '>=6.0.0'}
@@ -945,6 +928,10 @@
     engines: {node: '>=0.10.0', npm: '>2.7.0'}
     os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows]
 
+  '@testing-library/dom@10.4.0':
+    resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==}
+    engines: {node: '>=18'}
+
   '@tootallnate/once@2.0.0':
     resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
     engines: {node: '>= 10'}
@@ -961,6 +948,9 @@
   '@tsconfig/node16@1.0.4':
     resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
 
+  '@types/aria-query@5.0.4':
+    resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
+
   '@types/babel__core@7.20.5':
     resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
 
@@ -1327,6 +1317,9 @@
   argparse@2.0.1:
     resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
 
+  aria-query@5.3.0:
+    resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
+
   array-differ@4.0.0:
     resolution: {integrity: sha512-Q6VPTLMsmXZ47ENG3V+wQyZS1ZxXMxFyYzA+Z/GMrJ6yIutAIEf9wTyroTzmGjNfox9/h3GdGBCVh43GVFx4Uw==}
     engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@@ -1739,6 +1732,10 @@
     resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
     engines: {node: '>=0.4.0'}
 
+  dequal@2.0.3:
+    resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
+    engines: {node: '>=6'}
+
   detect-libc@1.0.3:
     resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
     engines: {node: '>=0.10'}
@@ -1760,6 +1757,9 @@
     resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
     engines: {node: '>=6.0.0'}
 
+  dom-accessibility-api@0.5.16:
+    resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
+
   dom-converter@0.2.0:
     resolution: {integrity: sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==}
 
@@ -2779,6 +2779,10 @@
   lru-cache@5.1.1:
     resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
 
+  lz-string@1.5.0:
+    resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
+    hasBin: true
+
   make-dir@4.0.0:
     resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
     engines: {node: '>=10'}
@@ -3123,6 +3127,10 @@
   pretty-error@4.0.0:
     resolution: {integrity: sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==}
 
+  pretty-format@27.5.1:
+    resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
+    engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
+
   pretty-format@29.7.0:
     resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -3193,6 +3201,9 @@
     resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
     hasBin: true
 
+  react-is@17.0.2:
+    resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
+
   react-is@18.2.0:
     resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
 
@@ -3967,33 +3978,23 @@
       '@jridgewell/gen-mapping': 0.1.1
       '@jridgewell/trace-mapping': 0.3.25
 
-  '@babel/code-frame@7.22.13':
-    dependencies:
-      '@babel/highlight': 7.22.20
-      chalk: 2.4.2
-
   '@babel/code-frame@7.24.7':
     dependencies:
       '@babel/highlight': 7.24.7
-      picocolors: 1.0.0
-
-  '@babel/code-frame@7.25.7':
-    dependencies:
-      '@babel/highlight': 7.25.7
-      picocolors: 1.0.0
+      picocolors: 1.1.1
 
   '@babel/code-frame@7.26.0':
     dependencies:
       '@babel/helper-validator-identifier': 7.25.9
       js-tokens: 4.0.0
-      picocolors: 1.0.0
+      picocolors: 1.1.1
 
   '@babel/compat-data@7.20.10': {}
 
   '@babel/core@7.20.12':
     dependencies:
       '@ampproject/remapping': 2.2.0
-      '@babel/code-frame': 7.25.7
+      '@babel/code-frame': 7.26.0
       '@babel/generator': 7.25.7
       '@babel/helper-compilation-targets': 7.20.7(@babel/core@7.20.12)
       '@babel/helper-module-transforms': 7.25.7(@babel/core@7.20.12)
@@ -4060,7 +4061,7 @@
       '@babel/core': 7.20.12
       '@babel/helper-module-imports': 7.25.7
       '@babel/helper-simple-access': 7.25.7
-      '@babel/helper-validator-identifier': 7.25.7
+      '@babel/helper-validator-identifier': 7.25.9
       '@babel/traverse': 7.25.7
     transitivePeerDependencies:
       - supports-color
@@ -4096,8 +4097,6 @@
 
   '@babel/helper-string-parser@7.25.9': {}
 
-  '@babel/helper-validator-identifier@7.25.7': {}
-
   '@babel/helper-validator-identifier@7.25.9': {}
 
   '@babel/helper-validator-option@7.23.5': {}
@@ -4110,25 +4109,12 @@
     transitivePeerDependencies:
       - supports-color
 
-  '@babel/highlight@7.22.20':
-    dependencies:
-      '@babel/helper-validator-identifier': 7.25.7
-      chalk: 2.4.2
-      js-tokens: 4.0.0
-
   '@babel/highlight@7.24.7':
     dependencies:
-      '@babel/helper-validator-identifier': 7.25.7
+      '@babel/helper-validator-identifier': 7.25.9
       chalk: 2.4.2
       js-tokens: 4.0.0
-      picocolors: 1.0.0
-
-  '@babel/highlight@7.25.7':
-    dependencies:
-      '@babel/helper-validator-identifier': 7.25.7
-      chalk: 2.4.2
-      js-tokens: 4.0.0
-      picocolors: 1.0.0
+      picocolors: 1.1.1
 
   '@babel/parser@7.25.7':
     dependencies:
@@ -4223,7 +4209,7 @@
 
   '@babel/template@7.25.7':
     dependencies:
-      '@babel/code-frame': 7.25.7
+      '@babel/code-frame': 7.26.0
       '@babel/parser': 7.25.7
       '@babel/types': 7.25.7
 
@@ -4235,7 +4221,7 @@
 
   '@babel/traverse@7.25.7':
     dependencies:
-      '@babel/code-frame': 7.25.7
+      '@babel/code-frame': 7.26.0
       '@babel/generator': 7.25.7
       '@babel/parser': 7.25.7
       '@babel/template': 7.25.7
@@ -4260,7 +4246,7 @@
   '@babel/types@7.25.7':
     dependencies:
       '@babel/helper-string-parser': 7.25.7
-      '@babel/helper-validator-identifier': 7.25.7
+      '@babel/helper-validator-identifier': 7.25.9
       to-fast-properties: 2.0.0
 
   '@babel/types@7.26.0':
@@ -5094,6 +5080,17 @@
       '@stdlib/assert-has-tostringtag-support': 0.2.2
       '@stdlib/symbol-ctor': 0.2.2
 
+  '@testing-library/dom@10.4.0':
+    dependencies:
+      '@babel/code-frame': 7.26.0
+      '@babel/runtime': 7.25.6
+      '@types/aria-query': 5.0.4
+      aria-query: 5.3.0
+      chalk: 4.1.2
+      dom-accessibility-api: 0.5.16
+      lz-string: 1.5.0
+      pretty-format: 27.5.1
+
   '@tootallnate/once@2.0.0': {}
 
   '@tsconfig/node10@1.0.11': {}
@@ -5104,6 +5101,8 @@
 
   '@tsconfig/node16@1.0.4': {}
 
+  '@types/aria-query@5.0.4': {}
+
   '@types/babel__core@7.20.5':
     dependencies:
       '@babel/parser': 7.25.7
@@ -5537,6 +5536,10 @@
 
   argparse@2.0.1: {}
 
+  aria-query@5.3.0:
+    dependencies:
+      dequal: 2.0.3
+
   array-differ@4.0.0: {}
 
   array-union@3.0.1: {}
@@ -5970,6 +5973,8 @@
 
   delayed-stream@1.0.0: {}
 
+  dequal@2.0.3: {}
+
   detect-libc@1.0.3:
     optional: true
 
@@ -5983,6 +5988,8 @@
     dependencies:
       esutils: 2.0.3
 
+  dom-accessibility-api@0.5.16: {}
+
   dom-converter@0.2.0:
     dependencies:
       utila: 0.4.0
@@ -6894,7 +6901,7 @@
 
   jest-message-util@29.7.0:
     dependencies:
-      '@babel/code-frame': 7.22.13
+      '@babel/code-frame': 7.26.0
       '@jest/types': 29.6.3
       '@types/stack-utils': 2.0.3
       chalk: 4.1.2
@@ -7259,6 +7266,8 @@
     dependencies:
       yallist: 3.1.1
 
+  lz-string@1.5.0: {}
+
   make-dir@4.0.0:
     dependencies:
       semver: 7.6.3
@@ -7473,7 +7482,7 @@
 
   parse-json@5.2.0:
     dependencies:
-      '@babel/code-frame': 7.25.7
+      '@babel/code-frame': 7.26.0
       error-ex: 1.3.2
       json-parse-even-better-errors: 2.3.1
       lines-and-columns: 1.2.4
@@ -7616,6 +7625,12 @@
       lodash: 4.17.21
       renderkid: 3.0.0
 
+  pretty-format@27.5.1:
+    dependencies:
+      ansi-regex: 5.0.1
+      ansi-styles: 5.2.0
+      react-is: 17.0.2
+
   pretty-format@29.7.0:
     dependencies:
       '@jest/schemas': 29.6.3
@@ -7683,6 +7698,8 @@
       minimist: 1.2.8
       strip-json-comments: 2.0.1
 
+  react-is@17.0.2: {}
+
   react-is@18.2.0: {}
 
   readable-stream@2.3.8:
@@ -8129,7 +8146,7 @@
     dependencies:
       browserslist: 4.22.3
       escalade: 3.1.1
-      picocolors: 1.0.0
+      picocolors: 1.1.1
 
   update-browserslist-db@1.1.1(browserslist@4.24.2):
     dependencies:
diff --git a/src/common/architecture/scripts/FakeScript.ts b/src/common/architecture/scripts/FakeScript.ts
new file mode 100644
index 0000000..8f3f504
--- /dev/null
+++ b/src/common/architecture/scripts/FakeScript.ts
@@ -0,0 +1,12 @@
+import Script, { ConcreteScript } from './Script';
+
+export class FakeScript extends Script {
+  runAfter: ConcreteScript[] = [];
+  priority: number = 2 ** 31;
+  page: never;
+  environment: never;
+  runPhase: never;
+
+  /* istanbul ignore next */
+  execute: () => undefined;
+}
diff --git a/src/common/architecture/scripts/Script.ts b/src/common/architecture/scripts/Script.ts
index 0e33e7e..8a8e9de 100644
--- a/src/common/architecture/scripts/Script.ts
+++ b/src/common/architecture/scripts/Script.ts
@@ -53,6 +53,12 @@
 
 export default abstract class Script {
   /**
+   * Used to indicate that this script should run after the members of the
+   * provided array.
+   */
+  readonly runAfter: ConcreteScript[] = [];
+
+  /**
    * Priority with which the script is executed. Scripts with a lower value are
    * executed first.
    */
diff --git a/src/infrastructure/presentation/scripts/ScriptSorter.adapter.test.ts b/src/infrastructure/presentation/scripts/ScriptSorter.adapter.test.ts
new file mode 100644
index 0000000..8d9eb39
--- /dev/null
+++ b/src/infrastructure/presentation/scripts/ScriptSorter.adapter.test.ts
@@ -0,0 +1,176 @@
+import { describe, expect, it } from '@jest/globals';
+import { FakeScript } from '../../../common/architecture/scripts/FakeScript';
+import ScriptSorterAdapter from './ScriptSorter.adapter';
+import Script, {
+  ConcreteScript,
+} from '../../../common/architecture/scripts/Script';
+import ScriptSorterCycleDetectedError from '../../../presentation/scripts/errors/ScriptSorterCycleDetected.error';
+import ScriptSorterRepeatedScriptError from '../../../presentation/scripts/errors/ScriptSorterRepeatedScript.error';
+
+describe('ScriptSorterAdapter', () => {
+  const checkSort = (scripts: Script[], expectedOrder: ConcreteScript[]) => {
+    const sut = new ScriptSorterAdapter();
+    const result = sut.sort(scripts);
+
+    expect(result.map((script) => script.constructor)).toEqual(expectedOrder);
+  };
+
+  describe('Regarding only script priority', () => {
+    it('should sort scripts by priority in ascending order', () => {
+      class A extends FakeScript {
+        priority = 1;
+      }
+      class B extends FakeScript {
+        priority = 1000;
+      }
+      class C extends FakeScript {
+        priority = 0;
+      }
+      class D extends FakeScript {
+        priority = 2 ** 31;
+      }
+
+      const scripts = [new A(), new B(), new C(), new D()];
+      const expectedOrder = [C, A, B, D];
+      checkSort(scripts, expectedOrder);
+    });
+
+    it('should not reorder scripts which have the same priority', () => {
+      class A extends FakeScript {
+        priority = 1;
+        runAfter: ConcreteScript[] = [];
+      }
+      class B extends FakeScript {
+        priority = 10;
+        runAfter: ConcreteScript[] = [];
+      }
+      class C extends FakeScript {
+        priority = 1;
+        runAfter: ConcreteScript[] = [];
+      }
+
+      const scripts = [new A(), new B(), new C()];
+      const expectedOrder = [A, C, B];
+      checkSort(scripts, expectedOrder);
+    });
+  });
+
+  describe('Regarding only runAfter', () => {
+    it('should place scripts which appear in runAfter before the script which includes them, in the order set in runAfter', () => {
+      // Dependency tree (arrows symbolize scripts in `runAfter`):
+      // A
+      // --> B
+      // --> E
+      // C
+      // D
+      // --> F
+      //     --> G
+      //         --> H
+      class A extends FakeScript {
+        runAfter = [B, E];
+      }
+      class B extends FakeScript {}
+      class C extends FakeScript {}
+      class D extends FakeScript {
+        runAfter = [F];
+      }
+      class E extends FakeScript {}
+      class F extends FakeScript {
+        runAfter = [G];
+      }
+      class G extends FakeScript {
+        runAfter = [H];
+      }
+      class H extends FakeScript {}
+
+      const scripts = [
+        new A(),
+        new B(),
+        new C(),
+        new D(),
+        new E(),
+        new F(),
+        new G(),
+        new H(),
+      ];
+      const expectedOrder = [B, E, A, C, H, G, F, D];
+      checkSort(scripts, expectedOrder);
+    });
+
+    it('should not return a script multiple times when it is included in runAfter in multiple scripts', () => {
+      // Dependency tree (arrows symbolize scripts in `runAfter`):
+      // A
+      // --> C
+      // B
+      // --> C
+      class C extends FakeScript {}
+      class A extends FakeScript {
+        runAfter = [C];
+      }
+      class B extends FakeScript {
+        runAfter = [C];
+      }
+
+      const scripts = [new A(), new B(), new C()];
+      const expectedOrder = [C, A, B];
+      checkSort(scripts, expectedOrder);
+    });
+
+    it("should return ScriptSorterCycleDetectedError when there's a cycle", () => {
+      // Dependency tree (arrows symbolize scripts in `runAfter`):
+      // A
+      // --> B
+      //     --> A
+      //         --> ...
+      class A extends FakeScript {
+        runAfter = [B];
+      }
+      class B extends FakeScript {
+        runAfter = [A];
+      }
+
+      const scripts = [new A(), new B()];
+
+      const sut = new ScriptSorterAdapter();
+      expect(() => sut.sort(scripts)).toThrow(ScriptSorterCycleDetectedError);
+    });
+  });
+
+  describe('Combining priority and runAfter configurations', () => {
+    it('should order by priority first and then alter this order when a script needs to run after other scripts which, according to their priority, should have been ran later', () => {
+      class A extends FakeScript {
+        priority = 1;
+        runAfter = [B];
+      }
+      class B extends FakeScript {
+        priority = 2;
+      }
+      class C extends FakeScript {
+        priority = 0;
+      }
+
+      const scripts = [new A(), new B(), new C()];
+      const expectedOrder = [C, B, A];
+      checkSort(scripts, expectedOrder);
+    });
+  });
+
+  describe('Regarding edge cases', () => {
+    it('should not throw an error when there are multiple script instances from the same class', () => {
+      class A extends FakeScript {}
+      const scripts = [new A(), new A()];
+
+      const sut = new ScriptSorterAdapter();
+      expect(() => sut.sort(scripts)).not.toThrow();
+    });
+
+    it('should throw an error when a script instance is repeated', () => {
+      class A extends FakeScript {}
+      const script = new A();
+      const scripts = [script, script];
+
+      const sut = new ScriptSorterAdapter();
+      expect(() => sut.sort(scripts)).toThrow(ScriptSorterRepeatedScriptError);
+    });
+  });
+});
diff --git a/src/infrastructure/presentation/scripts/ScriptSorter.adapter.ts b/src/infrastructure/presentation/scripts/ScriptSorter.adapter.ts
new file mode 100644
index 0000000..f34426a
--- /dev/null
+++ b/src/infrastructure/presentation/scripts/ScriptSorter.adapter.ts
@@ -0,0 +1,106 @@
+import Script from '../../../common/architecture/scripts/Script';
+import ScriptSorterCycleDetectedError from '../../../presentation/scripts/errors/ScriptSorterCycleDetected.error';
+import ScriptSorterRepeatedScriptError from '../../../presentation/scripts/errors/ScriptSorterRepeatedScript.error';
+import { ScriptSorterPort } from '../../../presentation/scripts/ScriptSorter.port';
+
+export default class ScriptSorterAdapter implements ScriptSorterPort {
+  /**
+   * Sorts scripts based on the `runAfter` and `priority` fields.
+   *
+   * It initially sorts scripts based on the `priority` field, and then looks at
+   * script dependencies set in the `runAfter` field. If there are any, it places
+   * them before the including script (if they didn't appear before) in the order
+   * they appear in the `runAfter` field.
+   *
+   * Take a look at ScriptSorter.adapter.test.ts for the full spec of how the
+   * scripts are sorted.
+   */
+  sort(scripts: Script[]): Script[] {
+    scripts = this.sortByPriority(scripts);
+
+    const childrenMap = this.getChildrenMap(scripts);
+    if (hasCycles(childrenMap)) {
+      throw new ScriptSorterCycleDetectedError();
+    }
+
+    const sortedScripts: Script[] = [];
+    for (const script of scripts) {
+      this.traverse(script, childrenMap, sortedScripts);
+    }
+    return sortedScripts;
+  }
+
+  private sortByPriority(scripts: Script[]): Script[] {
+    return scripts.sort((a, b) => {
+      if (a.priority < b.priority) {
+        return -1;
+      } else if (a.priority > b.priority) {
+        return 1;
+      } else {
+        return 0;
+      }
+    });
+  }
+
+  getChildrenMap(scripts: Script[]): Map<Script, Script[]> {
+    const childrenMap = new Map<Script, Script[]>();
+    for (const script of scripts) {
+      if (childrenMap.has(script)) {
+        throw new ScriptSorterRepeatedScriptError();
+      }
+      childrenMap.set(script, this.getChildren(script, scripts));
+    }
+    return childrenMap;
+  }
+
+  private getChildren(script: Script, scripts: Script[]): Script[] {
+    const children: Script[] = [];
+    for (const childConstructor of script.runAfter) {
+      const runAfterScripts = scripts.filter(
+        (it) => it.constructor === childConstructor,
+      );
+      for (const it of runAfterScripts) {
+        if (!children.includes(it)) {
+          children.push(it);
+        }
+      }
+    }
+    return children;
+  }
+
+  private traverse(
+    script: Script,
+    childrenMap: Map<Script, Script[]>,
+    sortedScripts: Script[],
+  ) {
+    if (sortedScripts.includes(script)) return;
+
+    for (const child of childrenMap.get(script)) {
+      this.traverse(child, childrenMap, sortedScripts);
+    }
+    sortedScripts.push(script);
+  }
+}
+
+function hasCycles<T>(childrenMap: Map<T, T[]>): boolean {
+  const visited = new Set<T>();
+  for (const node of childrenMap.keys()) {
+    const hasCycles = dfs(node, visited, childrenMap);
+    if (hasCycles) return true;
+  }
+
+  return false;
+}
+
+function dfs<T>(node: T, visited: Set<T>, childrenMap: Map<T, T[]>) {
+  if (visited.has(node)) return true;
+
+  visited.add(node);
+  for (const child of childrenMap.get(node)) {
+    const hasCycles = dfs(child, visited, childrenMap);
+    if (hasCycles) return true;
+  }
+  visited.delete(node);
+
+  return false;
+}
diff --git a/src/infrastructure/presentation/scripts/SortedScriptsProvider.adapter.test.ts b/src/infrastructure/presentation/scripts/SortedScriptsProvider.adapter.test.ts
new file mode 100644
index 0000000..8d41ea9
--- /dev/null
+++ b/src/infrastructure/presentation/scripts/SortedScriptsProvider.adapter.test.ts
@@ -0,0 +1,17 @@
+import { describe, expect, it } from "@jest/globals";
+import { FakeScript } from "../../../common/architecture/scripts/FakeScript";
+import { SortedScriptsProviderAdapter } from "./SortedScriptsProvider.adapter";
+import ScriptSorterAdapter from "./ScriptSorter.adapter";
+
+describe('SortedScriptsProvider', () => {
+  describe('getScripts', () => {
+    it('should not throw', () => {
+      class A extends FakeScript {}
+      const scripts = [new A()];
+
+      const sut = new SortedScriptsProviderAdapter(scripts, new ScriptSorterAdapter());
+
+      expect(() => sut.getScripts()).not.toThrow();
+    });
+  });
+});
diff --git a/src/infrastructure/presentation/scripts/SortedScriptsProvider.adapter.ts b/src/infrastructure/presentation/scripts/SortedScriptsProvider.adapter.ts
new file mode 100644
index 0000000..f93cc1c
--- /dev/null
+++ b/src/infrastructure/presentation/scripts/SortedScriptsProvider.adapter.ts
@@ -0,0 +1,19 @@
+import Script from '../../../common/architecture/scripts/Script';
+import { ScriptProviderPort } from '../../../presentation/scripts/ScriptProvider.port';
+import { ScriptSorterPort } from '../../../presentation/scripts/ScriptSorter.port';
+
+export class SortedScriptsProviderAdapter implements ScriptProviderPort {
+  private sortedScripts: Script[] | undefined;
+
+  constructor(
+    private scripts: Script[],
+    private scriptSorter: ScriptSorterPort,
+  ) {}
+
+  getScripts() {
+    if (this.sortedScripts === undefined) {
+      this.sortedScripts = this.scriptSorter.sort(this.scripts);
+    }
+    return this.sortedScripts;
+  }
+}
diff --git a/src/presentation/scripts/ScriptProvider.port.ts b/src/presentation/scripts/ScriptProvider.port.ts
new file mode 100644
index 0000000..be45215
--- /dev/null
+++ b/src/presentation/scripts/ScriptProvider.port.ts
@@ -0,0 +1,5 @@
+import Script from "../../common/architecture/scripts/Script";
+
+export interface ScriptProviderPort {
+  getScripts(): Script[];
+}
diff --git a/src/presentation/scripts/ScriptSorter.port.ts b/src/presentation/scripts/ScriptSorter.port.ts
new file mode 100644
index 0000000..74db45c
--- /dev/null
+++ b/src/presentation/scripts/ScriptSorter.port.ts
@@ -0,0 +1,5 @@
+import Script from "../../common/architecture/scripts/Script";
+
+export interface ScriptSorterPort {
+  sort(scripts: Script[]): Script[];
+}
diff --git a/src/presentation/scripts/errors/ScriptSorterCycleDetected.error.ts b/src/presentation/scripts/errors/ScriptSorterCycleDetected.error.ts
new file mode 100644
index 0000000..3289541
--- /dev/null
+++ b/src/presentation/scripts/errors/ScriptSorterCycleDetected.error.ts
@@ -0,0 +1,3 @@
+export default class ScriptSorterCycleDetectedError extends Error {
+  name: 'script-sorter-cycle-detected-error';
+}
diff --git a/src/presentation/scripts/errors/ScriptSorterRepeatedScript.error.ts b/src/presentation/scripts/errors/ScriptSorterRepeatedScript.error.ts
new file mode 100644
index 0000000..8e9a219
--- /dev/null
+++ b/src/presentation/scripts/errors/ScriptSorterRepeatedScript.error.ts
@@ -0,0 +1,3 @@
+export default class ScriptSorterRepeatedScriptError extends Error {
+  name: 'script-sorter-repeated-script-error';
+}