feat(scripts): add ScriptRunner and NodeWatcherScript

This will allow us to initialize and use scripts and node watcher
handlers in the entrypoints instead of using the legacy
EntrypointScriptRunner.

Bug: twpowertools:226
Change-Id: Ic7e2f36a758555210dc0197fd10b1da7d9f424e9
diff --git a/src/infrastructure/presentation/scripts/NodeWatcherScript.adapter.test.ts b/src/infrastructure/presentation/scripts/NodeWatcherScript.adapter.test.ts
new file mode 100644
index 0000000..8647e3b
--- /dev/null
+++ b/src/infrastructure/presentation/scripts/NodeWatcherScript.adapter.test.ts
@@ -0,0 +1,40 @@
+import { beforeEach, describe, expect, it, jest } from '@jest/globals';
+import NodeWatcherScriptAdapter from './NodeWatcherScript.adapter';
+import { NodeWatcherPort } from '../../../presentation/nodeWatcher/NodeWatcher.port';
+import { NodeWatcherHandler } from '../../../presentation/nodeWatcher/NodeWatcherHandler';
+
+describe('NodeWatcherScriptAdapter', () => {
+  const fakeNodeWatcher: NodeWatcherPort = {
+    start: jest.fn<NodeWatcherPort['start']>(),
+    pause: jest.fn<NodeWatcherPort['pause']>(),
+    setHandler: jest.fn<NodeWatcherPort['setHandler']>(),
+    removeHandler: jest.fn<NodeWatcherPort['removeHandler']>(),
+  };
+
+  beforeEach(() => {
+    jest.resetAllMocks();
+  });
+
+  describe('When the script is executed', () => {
+    it('should start the NodeWatcher', () => {
+      const sut = new NodeWatcherScriptAdapter(fakeNodeWatcher, new Map([]));
+      sut.execute();
+
+      expect(fakeNodeWatcher.start).toBeCalledTimes(1);
+    });
+
+    it('should add the handlers to NodeWatcher', () => {
+      const key = 'test-handler';
+      const handler: NodeWatcherHandler = {
+        nodeFilter: () => false,
+        onMutatedNode: () => undefined,
+      };
+      const handlers = new Map([[key, handler]]);
+      const sut = new NodeWatcherScriptAdapter(fakeNodeWatcher, handlers);
+      sut.execute();
+
+      expect(fakeNodeWatcher.setHandler).toHaveBeenCalledTimes(1);
+      expect(fakeNodeWatcher.setHandler).toHaveBeenCalledWith(key, handler);
+    });
+  });
+});
diff --git a/src/infrastructure/presentation/scripts/NodeWatcherScript.adapter.ts b/src/infrastructure/presentation/scripts/NodeWatcherScript.adapter.ts
new file mode 100644
index 0000000..29dbd1f
--- /dev/null
+++ b/src/infrastructure/presentation/scripts/NodeWatcherScript.adapter.ts
@@ -0,0 +1,24 @@
+import Script from "../../../common/architecture/scripts/Script";
+import { NodeWatcherHandler } from "../../../presentation/nodeWatcher/NodeWatcherHandler";
+import { NodeWatcherPort } from "../../../presentation/nodeWatcher/NodeWatcher.port";
+
+export default class NodeWatcherScriptAdapter extends Script {
+  // TODO: Delete this once these properties are removed from Script.
+  page: never;
+  environment: never;
+  runPhase: never;
+
+  constructor(
+    private nodeWatcher: NodeWatcherPort,
+    private handlers: Map<string, NodeWatcherHandler>,
+  ) {
+    super();
+  }
+
+  execute() {
+    this.nodeWatcher.start();
+    for (const [key, handler] of this.handlers) {
+      this.nodeWatcher.setHandler(key, handler);
+    }
+  }
+}
diff --git a/src/infrastructure/presentation/scripts/ScriptRunner.ts b/src/infrastructure/presentation/scripts/ScriptRunner.ts
new file mode 100644
index 0000000..15e70bb
--- /dev/null
+++ b/src/infrastructure/presentation/scripts/ScriptRunner.ts
@@ -0,0 +1,11 @@
+import Script from '../../../common/architecture/scripts/Script';
+
+export default class ScriptRunner {
+  constructor(private scripts: Script[]) {}
+
+  run() {
+    for (const script of this.scripts) {
+      script.execute();
+    }
+  }
+}