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';
+}