From 8455b0d88b60a4cfc808fc00b7bb6769ad488194 Mon Sep 17 00:00:00 2001 From: andrei Date: Mon, 14 Jul 2025 16:23:39 +0200 Subject: [PATCH 01/59] feat: tests for the unescapeHTML utils function --- vis/test/utils/unescapeHTMLentities.test.ts | 39 +++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 vis/test/utils/unescapeHTMLentities.test.ts diff --git a/vis/test/utils/unescapeHTMLentities.test.ts b/vis/test/utils/unescapeHTMLentities.test.ts new file mode 100644 index 000000000..c68f2cb93 --- /dev/null +++ b/vis/test/utils/unescapeHTMLentities.test.ts @@ -0,0 +1,39 @@ +import { describe } from "vitest"; +import { unescapeHTML } from "../../js/utils/unescapeHTMLentities"; + +describe("Unescape HTML entities function", () => { + it("Single HTML entities decoded", () => { + expect(unescapeHTML("&")).toBe("&"); + expect(unescapeHTML("<")).toBe("<"); + expect(unescapeHTML(">")).toBe(">"); + expect(unescapeHTML(""")).toBe('"'); + expect(unescapeHTML(""")).toBe('"'); + expect(unescapeHTML("'")).toBe("'"); + expect(unescapeHTML("/")).toBe("/"); + expect(unescapeHTML("`")).toBe("`"); + expect(unescapeHTML("=")).toBe("="); + }); + + it("Multiple entities in a single string decoded", () => { + const input = + "<div class="test">Hello & Welcome/</div>"; + const expected = '
Hello & Welcome/
'; + expect(unescapeHTML(input)).toBe(expected); + }); + + it("String unchanged if no entities present", () => { + const input = "Plain text with no HTML entities"; + expect(unescapeHTML(input)).toBe(input); + }); + + it("Empty string input handled", () => { + expect(unescapeHTML("")).toBe(""); + }); + + it("Non-string input converted to string", () => { + expect(unescapeHTML(123 as any as string)).toBe("123"); + expect(unescapeHTML(null as any)).toBe("null"); + expect(unescapeHTML(undefined as any)).toBe("undefined"); + expect(unescapeHTML(true as any)).toBe("true"); + }); +}); From 51392982a1675d6df152cfc11cd983f39eec4741 Mon Sep 17 00:00:00 2001 From: andrei Date: Wed, 16 Jul 2025 13:59:19 +0200 Subject: [PATCH 02/59] feat: tests for the createTransition function --- vis/test/utils/transition.test.ts | 57 +++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 vis/test/utils/transition.test.ts diff --git a/vis/test/utils/transition.test.ts b/vis/test/utils/transition.test.ts new file mode 100644 index 000000000..e97b1d23e --- /dev/null +++ b/vis/test/utils/transition.test.ts @@ -0,0 +1,57 @@ +import { describe, it, expect, vi } from "vitest"; + +vi.mock("d3-transition", () => { + const mockDuration = vi.fn().mockReturnThis(); + const mockEase = vi.fn().mockReturnThis(); + const mockOn = vi.fn().mockReturnThis(); + + const mockTransition = { + duration: mockDuration, + ease: mockEase, + on: mockOn, + }; + + return { + transition: vi.fn(() => mockTransition), + __esModule: true, + _mocks: { + mockTransition, + mockDuration, + mockEase, + mockOn, + }, + }; +}); + +vi.mock("d3-ease", () => { + const mockExponent = vi.fn(() => "mocked-easing-fn"); + return { + easePolyInOut: { exponent: mockExponent }, + __esModule: true, + _mocks: { + mockExponent, + }, + }; +}); + +import { createTransition } from "../../js/utils/transition"; + +describe("The createTransition function", async () => { + it("creates a transition with specified duration, easing and end callback", async () => { + const transitionModule = await import("d3-transition"); + const tMocks = (transitionModule as any)._mocks; + + const easeModule = await import("d3-ease"); + const eMocks = (easeModule as any)._mocks; + + const mockCallback = vi.fn(); + const result = createTransition(400, mockCallback); + + expect(transitionModule.transition).toHaveBeenCalled(); + expect(tMocks.mockDuration).toHaveBeenCalledWith(400); + expect(eMocks.mockExponent).toHaveBeenCalledWith(3); + expect(tMocks.mockEase).toHaveBeenCalledWith("mocked-easing-fn"); + expect(tMocks.mockOn).toHaveBeenCalledWith("end", mockCallback); + expect(result).toEqual(tMocks.mockTransition); + }); +}); From 9016f964fb93111d571d376ac12da64f73af7e04 Mon Sep 17 00:00:00 2001 From: andrei Date: Wed, 16 Jul 2025 14:00:08 +0200 Subject: [PATCH 03/59] refactor: formatting problems in the transition.ts file --- vis/js/utils/transition.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/vis/js/utils/transition.ts b/vis/js/utils/transition.ts index a27b404d6..5d7b01c61 100644 --- a/vis/js/utils/transition.ts +++ b/vis/js/utils/transition.ts @@ -2,15 +2,16 @@ import { transition } from "d3-transition"; import { easePolyInOut } from "d3-ease"; /** - * Returns d3 transition object that is passed as a parameter + * Returns d3 transition object that is passed as a parameter * to each component transition. - * + * * @param {Function} callback callback triggered at the end */ export const createTransition = (duration: number, callback: () => void) => { const newTransition = transition() .duration(duration) .ease(easePolyInOut.exponent(3)); + newTransition.on("end", callback); return newTransition; }; From a435399e9a9f058287cf29eb679c1c51ff2e027c Mon Sep 17 00:00:00 2001 From: andrei Date: Wed, 16 Jul 2025 14:02:59 +0200 Subject: [PATCH 04/59] refactor: transfer the file from .js to .ts --- vis/test/utils/{data.test.js => data.test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename vis/test/utils/{data.test.js => data.test.ts} (100%) diff --git a/vis/test/utils/data.test.js b/vis/test/utils/data.test.ts similarity index 100% rename from vis/test/utils/data.test.js rename to vis/test/utils/data.test.ts From ce854ebd012535e2fe7186b6237503750d9ad709 Mon Sep 17 00:00:00 2001 From: andrei Date: Wed, 16 Jul 2025 14:08:32 +0200 Subject: [PATCH 05/59] refactor: transfer the file from .js to .ts --- vis/test/utils/{paper.test.js => paper.test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename vis/test/utils/{paper.test.js => paper.test.ts} (100%) diff --git a/vis/test/utils/paper.test.js b/vis/test/utils/paper.test.ts similarity index 100% rename from vis/test/utils/paper.test.js rename to vis/test/utils/paper.test.ts From 0865167f8bf7035ff14301881f04fdf2f4bb7b43 Mon Sep 17 00:00:00 2001 From: andrei Date: Thu, 17 Jul 2025 14:12:51 +0200 Subject: [PATCH 06/59] feat: tests for the CommUtils class --- .../classes/headstart/library/tests/README.md | 27 +++++++++ .../tests/configuration/.phpunit.result.cache | 1 + .../library/tests/configuration/phpunit.xml | 14 +++++ .../headstart/library/tests/docker/Dockerfile | 27 +++++++++ .../library/tests/functions/CommUtilsTest.php | 57 +++++++++++++++++++ 5 files changed, 126 insertions(+) create mode 100644 server/classes/headstart/library/tests/README.md create mode 100644 server/classes/headstart/library/tests/configuration/.phpunit.result.cache create mode 100644 server/classes/headstart/library/tests/configuration/phpunit.xml create mode 100644 server/classes/headstart/library/tests/docker/Dockerfile create mode 100644 server/classes/headstart/library/tests/functions/CommUtilsTest.php diff --git a/server/classes/headstart/library/tests/README.md b/server/classes/headstart/library/tests/README.md new file mode 100644 index 000000000..c0b7e9a86 --- /dev/null +++ b/server/classes/headstart/library/tests/README.md @@ -0,0 +1,27 @@ +# About this folder + +This folder contains tests and their configurations for PHP scripts in the `library` folder. + +It contains three main folders: + +1. `functions` - folder with the tests; +2. `docker` - folder with the dockerfile; +3. `configuration` - folder for storing test configurations. + +Tests implemented using [`PHPUnit`](https://phpunit.de/index.html). All tests are run in the docker container. + +## How to run tests + +To run tests, you need to follow steps below (run them from the root folder level of the project): + +1. Build a docker container: + + ``` + docker build -t php-test server/classes/headstart/library/tests/docker + ``` + +2. Run the container with tests: + + ``` + docker run --rm -v $(pwd)/server/classes/headstart/library:/app php-test phpunit --configuration tests/configuration/phpunit.xml + ``` diff --git a/server/classes/headstart/library/tests/configuration/.phpunit.result.cache b/server/classes/headstart/library/tests/configuration/.phpunit.result.cache new file mode 100644 index 000000000..9299af4a4 --- /dev/null +++ b/server/classes/headstart/library/tests/configuration/.phpunit.result.cache @@ -0,0 +1 @@ +{"version":1,"defects":[],"times":{"headstart\\library\\tests\\functions\\CommUtilsTest::testEchoOrCallbackWithCallback":0.001,"headstart\\library\\tests\\functions\\CommUtilsTest::testEchoOrCallbackWithoutCallback":0,"headstart\\library\\tests\\functions\\CommUtilsTest::testGetParameterExists":0,"headstart\\library\\tests\\functions\\CommUtilsTest::testGetParameterNotExists":0}} \ No newline at end of file diff --git a/server/classes/headstart/library/tests/configuration/phpunit.xml b/server/classes/headstart/library/tests/configuration/phpunit.xml new file mode 100644 index 000000000..c69695696 --- /dev/null +++ b/server/classes/headstart/library/tests/configuration/phpunit.xml @@ -0,0 +1,14 @@ + + + + + ../functions + + + \ No newline at end of file diff --git a/server/classes/headstart/library/tests/docker/Dockerfile b/server/classes/headstart/library/tests/docker/Dockerfile new file mode 100644 index 000000000..cbcf1d1f0 --- /dev/null +++ b/server/classes/headstart/library/tests/docker/Dockerfile @@ -0,0 +1,27 @@ +# Setup PHP using the project version +FROM php:8.2-cli + +# Installing dependencies +RUN apt-get update && apt-get install -y \ + git \ + unzip \ + curl \ + libcurl4-openssl-dev \ + pkg-config \ + libssl-dev \ + libxml2-dev \ + file + +# Installing composer and PHPUnit +RUN curl -sS https://getcomposer.org/installer | php && mv composer.phar /usr/local/bin/composer +RUN composer global require phpunit/phpunit +ENV PATH="/root/.composer/vendor/bin:$PATH" + +# Defining working directory inside the container +WORKDIR /app + +# Creation of the output-file for saving .pdf files (mocking them) +RUN mkdir -p /path/to/paper_preview/ +RUN touch /path/to/paper_preview/test-file.pdf + +CMD ["phpunit"] \ No newline at end of file diff --git a/server/classes/headstart/library/tests/functions/CommUtilsTest.php b/server/classes/headstart/library/tests/functions/CommUtilsTest.php new file mode 100644 index 000000000..8d3f90d5a --- /dev/null +++ b/server/classes/headstart/library/tests/functions/CommUtilsTest.php @@ -0,0 +1,57 @@ + 'myFunc']; + + $this->expectOutputString('myFunc({"status":"ok"});'); + CommUtils::echoOrCallback($data, $params); + } + + /** + * Tests that raw data is echoed when the 'jsoncallback' parameter + * is not provided. + */ + public function testEchoOrCallbackWithoutCallback(): void { + $data = '{"status":"ok"}'; + $params = []; + + $this->expectOutputString('{"status":"ok"}'); + CommUtils::echoOrCallback($data, $params); + } + + /** + * Tests that the correct value is returned for a parameter that exists + * in the input array. + */ + public function testGetParameterExists(): void { + $params = ['id' => '123', 'query' => 'test']; + + $this->assertEquals('123', CommUtils::getParameter($params, 'id')); + $this->assertEquals('test', CommUtils::getParameter($params, 'query')); + } + + /** + * Tests that an Exception is thrown when a requested parameter + * does not exist in the input array. + */ + public function testGetParameterNotExists(): void { + $params = ['id' => '123']; + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('The following parameter is not set: query'); + CommUtils::getParameter($params, 'query'); + } +} \ No newline at end of file From 19a891a08f473b3bcaf433a2606f0391430adf81 Mon Sep 17 00:00:00 2001 From: andrei Date: Fri, 18 Jul 2025 17:11:44 +0200 Subject: [PATCH 07/59] feat: tests for the Toolkit class --- server/classes/headstart/library/Toolkit.php | 2 +- .../tests/configuration/.phpunit.result.cache | 2 +- .../headstart/library/tests/docker/Dockerfile | 2 +- .../tests/functions/ToolkitFileSystemTest.php | 112 ++++++++++++++ .../library/tests/functions/ToolkitTest.php | 138 ++++++++++++++++++ 5 files changed, 253 insertions(+), 3 deletions(-) create mode 100644 server/classes/headstart/library/tests/functions/ToolkitFileSystemTest.php create mode 100644 server/classes/headstart/library/tests/functions/ToolkitTest.php diff --git a/server/classes/headstart/library/Toolkit.php b/server/classes/headstart/library/Toolkit.php index 3482798df..08c6229f2 100644 --- a/server/classes/headstart/library/Toolkit.php +++ b/server/classes/headstart/library/Toolkit.php @@ -100,7 +100,7 @@ public static function openOrCreateFile($file) { //now, we should be good, let's open or create the file $handle = fopen($file, "w+"); if ($handle == false) - throw new Exception("There was an error while opening/creating the following file: " . $file); + throw new \Exception("There was an error while opening/creating the following file: " . $file); return $handle; } diff --git a/server/classes/headstart/library/tests/configuration/.phpunit.result.cache b/server/classes/headstart/library/tests/configuration/.phpunit.result.cache index 9299af4a4..9069600b1 100644 --- a/server/classes/headstart/library/tests/configuration/.phpunit.result.cache +++ b/server/classes/headstart/library/tests/configuration/.phpunit.result.cache @@ -1 +1 @@ -{"version":1,"defects":[],"times":{"headstart\\library\\tests\\functions\\CommUtilsTest::testEchoOrCallbackWithCallback":0.001,"headstart\\library\\tests\\functions\\CommUtilsTest::testEchoOrCallbackWithoutCallback":0,"headstart\\library\\tests\\functions\\CommUtilsTest::testGetParameterExists":0,"headstart\\library\\tests\\functions\\CommUtilsTest::testGetParameterNotExists":0}} \ No newline at end of file +{"version":1,"defects":{"headstart\\library\\tests\\functions\\ToolkitTest::testAddOrInitiatlizeArrayKeyNumericalDoesNotAffectOtherKeys":8,"headstart\\library\\tests\\functions\\ToolkitTest::testThatKeyWithValueWillBeUpdated":8,"headstart\\library\\tests\\functions\\ToolkitFileSystemTest::testCreatesFileInRoot":8,"headstart\\library\\tests\\functions\\ToolkitFileSystemTest::testThrowsExceptionOnFailure":7,"headstart\\library\\tests\\functions\\ToolkitFileSystemTest::testThrowsExceptionIfFileDoesNotExist":8,"headstart\\library\\tests\\functions\\ToolkitFileSystemTest::testThrowsExceptionIfFileIsNotReadable":8},"times":{"headstart\\library\\tests\\functions\\CommUtilsTest::testEchoOrCallbackWithCallback":0.001,"headstart\\library\\tests\\functions\\CommUtilsTest::testEchoOrCallbackWithoutCallback":0,"headstart\\library\\tests\\functions\\CommUtilsTest::testGetParameterExists":0,"headstart\\library\\tests\\functions\\CommUtilsTest::testGetParameterNotExists":0,"headstart\\library\\tests\\functions\\ToolkitTest::testAddOrInitiatlizeArrayKeyNumericalAddsNewKeyWithValueOne":0.001,"headstart\\library\\tests\\functions\\ToolkitTest::testThatKeyWillBeAdded":0,"headstart\\library\\tests\\functions\\ToolkitTest::testThatKeyCountWillBeIncrementedCOrrectly":0,"headstart\\library\\tests\\functions\\ToolkitTest::testThatKeyCountWillBeIncrementedCorrectlyWhenItIsEqualsToZero":0,"headstart\\library\\tests\\functions\\ToolkitTest::testThatKeyCountWillBeIncrementedCorrectly":0,"headstart\\library\\tests\\functions\\ToolkitTest::testAddOrInitiatlizeArrayKeyNumericalHandlesNullValueAsNew":0,"headstart\\library\\tests\\functions\\ToolkitTest::testThatFunctionHandlesNullValueAsNew":0,"headstart\\library\\tests\\functions\\ToolkitTest::testAddOrInitiatlizeArrayKeyNumericalDoesNotAffectOtherKeys":0,"headstart\\library\\tests\\functions\\ToolkitTest::testThatKeyWithValueWillBeAdded":0,"headstart\\library\\tests\\functions\\ToolkitTest::testThatKeyWithValueWillBeUpdated":0.003,"headstart\\library\\tests\\functions\\ToolkitTest::testAddsValueToExistingKey":0,"headstart\\library\\tests\\functions\\ToolkitTest::testAddsMultipleValues":0,"headstart\\library\\tests\\functions\\ToolkitTest::testAddsMultipleValuesToExistingKey":0,"headstart\\library\\tests\\functions\\ToolkitFileSystemTest::testCreatesFileInRoot":0,"headstart\\library\\tests\\functions\\ToolkitFileSystemTest::testCreatesDirectoryPathAndFile":0,"headstart\\library\\tests\\functions\\ToolkitFileSystemTest::testThrowsExceptionOnFailure":0,"headstart\\library\\tests\\functions\\ToolkitFileSystemTest::testOpensExistingAndReadableFileSuccessfully":0,"headstart\\library\\tests\\functions\\ToolkitFileSystemTest::testThrowsExceptionIfFileDoesNotExist":0,"headstart\\library\\tests\\functions\\ToolkitFileSystemTest::testThrowsExceptionIfFileIsNotReadable":0}} \ No newline at end of file diff --git a/server/classes/headstart/library/tests/docker/Dockerfile b/server/classes/headstart/library/tests/docker/Dockerfile index cbcf1d1f0..5dabbeafd 100644 --- a/server/classes/headstart/library/tests/docker/Dockerfile +++ b/server/classes/headstart/library/tests/docker/Dockerfile @@ -14,7 +14,7 @@ RUN apt-get update && apt-get install -y \ # Installing composer and PHPUnit RUN curl -sS https://getcomposer.org/installer | php && mv composer.phar /usr/local/bin/composer -RUN composer global require phpunit/phpunit +RUN composer global require phpunit/phpunit mikey179/vfsstream ENV PATH="/root/.composer/vendor/bin:$PATH" # Defining working directory inside the container diff --git a/server/classes/headstart/library/tests/functions/ToolkitFileSystemTest.php b/server/classes/headstart/library/tests/functions/ToolkitFileSystemTest.php new file mode 100644 index 000000000..ac2407c30 --- /dev/null +++ b/server/classes/headstart/library/tests/functions/ToolkitFileSystemTest.php @@ -0,0 +1,112 @@ +root = vfsStream::setup('root'); + } + + /** + * Testing method: openOrCreateFile. + * + * File creation test. + */ + public function testCreatesFileInRoot(): void { + $filename = vfsStream::url('root/test.txt'); + + $handle = Toolkit::openOrCreateFile($filename); + + $this->assertIsResource($handle); + fclose($handle); + $this->assertTrue($this->root->hasChild('test.txt')); + } + + /** + * Testing method: openOrCreateFile. + * + * File creation with the path to it. + */ + public function testCreatesDirectoryPathAndFile(): void { + $filename = vfsStream::url('root/new/path/file.log'); + + $this->assertFalse($this->root->hasChild('new')); + + $handle = Toolkit::openOrCreateFile($filename); + $this->assertIsResource($handle); + fclose($handle); + $this->assertTrue($this->root->hasChild('new/path/file.log')); + } + + /** + * Testing method: openOrCreateFile. + * + * Exception will be thrown if some error occurs. + */ + public function testThrowsExceptionOnFailure(): void { + vfsStream::newDirectory('readonly_dir', 0400)->at($this->root); + $filename = vfsStream::url('root/readonly_dir/data'); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage("There was an error while opening/creating the following file: " . $filename); + + Toolkit::openOrCreateFile($filename); + } + + /** + * Testing method: openFileForReading. + * + * An existing file opening. + */ + public function testOpensExistingAndReadableFileSuccessfully(): void { + $content = "Hello, world!"; + vfsStream::newFile('document.txt') + ->withContent($content) + ->at($this->root); + $filePath = vfsStream::url('root/document.txt'); + + $handle = Toolkit::openFileForReading($filePath); + + $this->assertIsResource($handle); + $this->assertSame($content, fread($handle, strlen($content))); + + fclose($handle); + } + + /** + * Testing method: openFileForReading. + * + * Exception will be thrown if the file does not exists. + */ + public function testThrowsExceptionIfFileDoesNotExist(): void { + $nonExistentFile = vfsStream::url('root/file.log'); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage("There was an error while opening/creating the following file: " . $nonExistentFile); + Toolkit::openFileForReading($nonExistentFile); + } + + /** + * Testing method: openFileForReading. + * + * Exception will be thrown if the file cannot be changes (no rights for editing). + */ + public function testThrowsExceptionIfFileIsNotReadable(): void { + $unreadableFile = vfsStream::newFile('secret', 0200) + ->at($this->root); + $filePath = $unreadableFile->url(); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage("There was an error while opening/creating the following file: " . $filePath); + Toolkit::openFileForReading($filePath); + } +} \ No newline at end of file diff --git a/server/classes/headstart/library/tests/functions/ToolkitTest.php b/server/classes/headstart/library/tests/functions/ToolkitTest.php new file mode 100644 index 000000000..6bea6f36d --- /dev/null +++ b/server/classes/headstart/library/tests/functions/ToolkitTest.php @@ -0,0 +1,138 @@ +assertArrayHasKey($key, $data, 'The key should be added to the array.'); + $this->assertSame(1, $data[$key], 'The new key should have a value of 1.'); + } + + /** + * Testing method: addOrInitiatlizeArrayKeyNumerical. + * + * Tests the edge case when key already exists and has 0 value. + */ + public function testThatKeyCountWillBeIncrementedCorrectlyWhenItIsEqualsToZero(): void { + $data = ["key" => 0]; + $key = 'key'; + + Toolkit::addOrInitiatlizeArrayKeyNumerical($data, $key); + + $this->assertArrayHasKey($key, $data, 'The key should be added to the array.'); + $this->assertSame(1, $data[$key], 'The new key should have a value of 1.'); + } + + /** + * Testing method: addOrInitiatlizeArrayKeyNumerical. + * + * Tests that an existing numerical key's value is incremented by 1. + */ + public function testThatKeyCountWillBeIncrementedCorrectly(): void { + $key = 'key'; + $initialValue = 10; + $data = [$key => $initialValue]; + + Toolkit::addOrInitiatlizeArrayKeyNumerical($data, $key); + $this->assertSame($initialValue + 1, $data[$key], 'The existing key value should be incremented.'); + } + + /** + * Testing method: addOrInitiatlizeArrayKeyNumerical. + * + * Tests the behavior when a key exists but its value is null. + * `isset()` returns false for null values, so it should be treated as a new key. + */ + public function testThatFunctionHandlesNullValueAsNew(): void { + $key = 'key'; + $data = [$key => null]; + + Toolkit::addOrInitiatlizeArrayKeyNumerical($data, $key); + $this->assertSame(1, $data[$key], 'A key with a null value should be re-initialized to 1.'); + } + + /** + * Testing method: addOrInitiatlizeArrayKeyNumerical. + * + * Tests that the function does not affect other keys in the array. + */ + public function testAddOrInitiatlizeArrayKeyNumericalDoesNotAffectOtherKeys(): void { + $data = [ + 'unrelated_key' => 'some string', + 'another_value' => 123 + ]; + $keyToIncrement = 'key'; + $expectedUnrelatedValue = 'some string'; + $expectedAnotherValue = 123; + + Toolkit::addOrInitiatlizeArrayKeyNumerical($data, $keyToIncrement); + + $this->assertSame(1, $data[$keyToIncrement]); + + $this->assertArrayHasKey('unrelated_key', $data); + $this->assertSame($expectedUnrelatedValue, $data['unrelated_key']); + + $this->assertArrayHasKey('another_value', $data); + $this->assertSame($expectedAnotherValue, $data['another_value']); + + $this->assertCount(3, $data); + } + + /** + * Testing method: addOrInitiatlizeArrayKey. + * + * Tests that a new key and its value will be added in the array. + */ + public function testThatKeyWithValueWillBeAdded(): void { + $array = []; + $key = 'key'; + $value = 'value'; + + Toolkit::addOrInitiatlizeArrayKey($array, $key, $value); + + $this->assertArrayHasKey($key, $array, 'The key should be added to the array.'); + $this->assertSame(["value"], $array[$key], 'The new key should have a value of 1.'); + } + + /** + * Testing method: addOrInitiatlizeArrayKey. + * + * Tests that an existing key will be found and its value will be updated. + */ + public function testAddsValueToExistingKey(): void { + $array = ['foo' => ['bar']]; + Toolkit::addOrInitiatlizeArrayKey($array, 'foo', 'baz'); + + $this->assertEquals(['bar', 'baz'], $array['foo']); + } + + /** + * Testing method: addOrInitiatlizeArrayKey. + * + * Tests that an existing key will be found and its value will be updated + * with multiple values. + */ + public function testAddsMultipleValuesToExistingKey(): void { + $array = []; + Toolkit::addOrInitiatlizeArrayKey($array, 'items', 1); + Toolkit::addOrInitiatlizeArrayKey($array, 'items', 2); + Toolkit::addOrInitiatlizeArrayKey($array, 'items', 3); + + $this->assertEquals([1, 2, 3], $array['items']); + } +} \ No newline at end of file From ede936e42789e611b3b25246f24e438d37dea8ed Mon Sep 17 00:00:00 2001 From: andrei Date: Fri, 18 Jul 2025 17:12:31 +0200 Subject: [PATCH 08/59] feat: phpunit tests cache in the gitignore --- .gitignore | 1 + .../headstart/library/tests/configuration/.phpunit.result.cache | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 server/classes/headstart/library/tests/configuration/.phpunit.result.cache diff --git a/.gitignore b/.gitignore index c9fbeab53..ae72d260a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ dist/ .cache coverage/ .vscode/ +*/.phpunit.result.cache # local deployment files /deploy.sh diff --git a/server/classes/headstart/library/tests/configuration/.phpunit.result.cache b/server/classes/headstart/library/tests/configuration/.phpunit.result.cache deleted file mode 100644 index 9069600b1..000000000 --- a/server/classes/headstart/library/tests/configuration/.phpunit.result.cache +++ /dev/null @@ -1 +0,0 @@ -{"version":1,"defects":{"headstart\\library\\tests\\functions\\ToolkitTest::testAddOrInitiatlizeArrayKeyNumericalDoesNotAffectOtherKeys":8,"headstart\\library\\tests\\functions\\ToolkitTest::testThatKeyWithValueWillBeUpdated":8,"headstart\\library\\tests\\functions\\ToolkitFileSystemTest::testCreatesFileInRoot":8,"headstart\\library\\tests\\functions\\ToolkitFileSystemTest::testThrowsExceptionOnFailure":7,"headstart\\library\\tests\\functions\\ToolkitFileSystemTest::testThrowsExceptionIfFileDoesNotExist":8,"headstart\\library\\tests\\functions\\ToolkitFileSystemTest::testThrowsExceptionIfFileIsNotReadable":8},"times":{"headstart\\library\\tests\\functions\\CommUtilsTest::testEchoOrCallbackWithCallback":0.001,"headstart\\library\\tests\\functions\\CommUtilsTest::testEchoOrCallbackWithoutCallback":0,"headstart\\library\\tests\\functions\\CommUtilsTest::testGetParameterExists":0,"headstart\\library\\tests\\functions\\CommUtilsTest::testGetParameterNotExists":0,"headstart\\library\\tests\\functions\\ToolkitTest::testAddOrInitiatlizeArrayKeyNumericalAddsNewKeyWithValueOne":0.001,"headstart\\library\\tests\\functions\\ToolkitTest::testThatKeyWillBeAdded":0,"headstart\\library\\tests\\functions\\ToolkitTest::testThatKeyCountWillBeIncrementedCOrrectly":0,"headstart\\library\\tests\\functions\\ToolkitTest::testThatKeyCountWillBeIncrementedCorrectlyWhenItIsEqualsToZero":0,"headstart\\library\\tests\\functions\\ToolkitTest::testThatKeyCountWillBeIncrementedCorrectly":0,"headstart\\library\\tests\\functions\\ToolkitTest::testAddOrInitiatlizeArrayKeyNumericalHandlesNullValueAsNew":0,"headstart\\library\\tests\\functions\\ToolkitTest::testThatFunctionHandlesNullValueAsNew":0,"headstart\\library\\tests\\functions\\ToolkitTest::testAddOrInitiatlizeArrayKeyNumericalDoesNotAffectOtherKeys":0,"headstart\\library\\tests\\functions\\ToolkitTest::testThatKeyWithValueWillBeAdded":0,"headstart\\library\\tests\\functions\\ToolkitTest::testThatKeyWithValueWillBeUpdated":0.003,"headstart\\library\\tests\\functions\\ToolkitTest::testAddsValueToExistingKey":0,"headstart\\library\\tests\\functions\\ToolkitTest::testAddsMultipleValues":0,"headstart\\library\\tests\\functions\\ToolkitTest::testAddsMultipleValuesToExistingKey":0,"headstart\\library\\tests\\functions\\ToolkitFileSystemTest::testCreatesFileInRoot":0,"headstart\\library\\tests\\functions\\ToolkitFileSystemTest::testCreatesDirectoryPathAndFile":0,"headstart\\library\\tests\\functions\\ToolkitFileSystemTest::testThrowsExceptionOnFailure":0,"headstart\\library\\tests\\functions\\ToolkitFileSystemTest::testOpensExistingAndReadableFileSuccessfully":0,"headstart\\library\\tests\\functions\\ToolkitFileSystemTest::testThrowsExceptionIfFileDoesNotExist":0,"headstart\\library\\tests\\functions\\ToolkitFileSystemTest::testThrowsExceptionIfFileIsNotReadable":0}} \ No newline at end of file From aac66e171dd53abb15352ea9765410748ac05eba Mon Sep 17 00:00:00 2001 From: andrei Date: Mon, 21 Jul 2025 13:56:31 +0200 Subject: [PATCH 09/59] refactor: remove tests for not used methods and add for loadIni --- .../tests/configuration/.phpunit.result.cache | 1 + .../tests/functions/ToolkitFileSystemTest.php | 112 -------------- .../library/tests/functions/ToolkitTest.php | 146 +++++------------- 3 files changed, 40 insertions(+), 219 deletions(-) create mode 100644 server/classes/headstart/library/tests/configuration/.phpunit.result.cache delete mode 100644 server/classes/headstart/library/tests/functions/ToolkitFileSystemTest.php diff --git a/server/classes/headstart/library/tests/configuration/.phpunit.result.cache b/server/classes/headstart/library/tests/configuration/.phpunit.result.cache new file mode 100644 index 000000000..40c68d10b --- /dev/null +++ b/server/classes/headstart/library/tests/configuration/.phpunit.result.cache @@ -0,0 +1 @@ +{"version":1,"defects":{"headstart\\library\\tests\\functions\\ToolkitTest::testLoadIniHandlesPathWithoutTrailingSlash":7},"times":{"headstart\\library\\tests\\functions\\CommUtilsTest::testEchoOrCallbackWithCallback":0.001,"headstart\\library\\tests\\functions\\CommUtilsTest::testEchoOrCallbackWithoutCallback":0,"headstart\\library\\tests\\functions\\CommUtilsTest::testGetParameterExists":0,"headstart\\library\\tests\\functions\\CommUtilsTest::testGetParameterNotExists":0,"headstart\\library\\tests\\functions\\ToolkitFileSystemTest::testCreatesFileInRoot":0,"headstart\\library\\tests\\functions\\ToolkitFileSystemTest::testCreatesDirectoryPathAndFile":0,"headstart\\library\\tests\\functions\\ToolkitFileSystemTest::testThrowsExceptionOnFailure":0,"headstart\\library\\tests\\functions\\ToolkitFileSystemTest::testOpensExistingAndReadableFileSuccessfully":0,"headstart\\library\\tests\\functions\\ToolkitFileSystemTest::testThrowsExceptionIfFileDoesNotExist":0,"headstart\\library\\tests\\functions\\ToolkitFileSystemTest::testThrowsExceptionIfFileIsNotReadable":0,"headstart\\library\\tests\\ToolkitTest::it_loads_local_config_when_it_exists":0,"headstart\\library\\tests\\functions\\ToolkitTest::testLoadIniLoadsLocalConfigWhenExists":0,"headstart\\library\\tests\\functions\\ToolkitTest::testLoadIniLoadsDefaultConfigWhenLocalIsMissing":0,"headstart\\library\\tests\\functions\\ToolkitTest::testLoadIniReturnsFalseWhenNoConfigFilesExist":0.002,"headstart\\library\\tests\\functions\\ToolkitTest::testLoadIniHandlesPathWithoutTrailingSlash":0}} \ No newline at end of file diff --git a/server/classes/headstart/library/tests/functions/ToolkitFileSystemTest.php b/server/classes/headstart/library/tests/functions/ToolkitFileSystemTest.php deleted file mode 100644 index ac2407c30..000000000 --- a/server/classes/headstart/library/tests/functions/ToolkitFileSystemTest.php +++ /dev/null @@ -1,112 +0,0 @@ -root = vfsStream::setup('root'); - } - - /** - * Testing method: openOrCreateFile. - * - * File creation test. - */ - public function testCreatesFileInRoot(): void { - $filename = vfsStream::url('root/test.txt'); - - $handle = Toolkit::openOrCreateFile($filename); - - $this->assertIsResource($handle); - fclose($handle); - $this->assertTrue($this->root->hasChild('test.txt')); - } - - /** - * Testing method: openOrCreateFile. - * - * File creation with the path to it. - */ - public function testCreatesDirectoryPathAndFile(): void { - $filename = vfsStream::url('root/new/path/file.log'); - - $this->assertFalse($this->root->hasChild('new')); - - $handle = Toolkit::openOrCreateFile($filename); - $this->assertIsResource($handle); - fclose($handle); - $this->assertTrue($this->root->hasChild('new/path/file.log')); - } - - /** - * Testing method: openOrCreateFile. - * - * Exception will be thrown if some error occurs. - */ - public function testThrowsExceptionOnFailure(): void { - vfsStream::newDirectory('readonly_dir', 0400)->at($this->root); - $filename = vfsStream::url('root/readonly_dir/data'); - - $this->expectException(\Exception::class); - $this->expectExceptionMessage("There was an error while opening/creating the following file: " . $filename); - - Toolkit::openOrCreateFile($filename); - } - - /** - * Testing method: openFileForReading. - * - * An existing file opening. - */ - public function testOpensExistingAndReadableFileSuccessfully(): void { - $content = "Hello, world!"; - vfsStream::newFile('document.txt') - ->withContent($content) - ->at($this->root); - $filePath = vfsStream::url('root/document.txt'); - - $handle = Toolkit::openFileForReading($filePath); - - $this->assertIsResource($handle); - $this->assertSame($content, fread($handle, strlen($content))); - - fclose($handle); - } - - /** - * Testing method: openFileForReading. - * - * Exception will be thrown if the file does not exists. - */ - public function testThrowsExceptionIfFileDoesNotExist(): void { - $nonExistentFile = vfsStream::url('root/file.log'); - - $this->expectException(\Exception::class); - $this->expectExceptionMessage("There was an error while opening/creating the following file: " . $nonExistentFile); - Toolkit::openFileForReading($nonExistentFile); - } - - /** - * Testing method: openFileForReading. - * - * Exception will be thrown if the file cannot be changes (no rights for editing). - */ - public function testThrowsExceptionIfFileIsNotReadable(): void { - $unreadableFile = vfsStream::newFile('secret', 0200) - ->at($this->root); - $filePath = $unreadableFile->url(); - - $this->expectException(\Exception::class); - $this->expectExceptionMessage("There was an error while opening/creating the following file: " . $filePath); - Toolkit::openFileForReading($filePath); - } -} \ No newline at end of file diff --git a/server/classes/headstart/library/tests/functions/ToolkitTest.php b/server/classes/headstart/library/tests/functions/ToolkitTest.php index 6bea6f36d..ca003cebf 100644 --- a/server/classes/headstart/library/tests/functions/ToolkitTest.php +++ b/server/classes/headstart/library/tests/functions/ToolkitTest.php @@ -3,136 +3,68 @@ namespace headstart\library\tests\functions; use PHPUnit\Framework\TestCase; +use org\bovigo\vfs\vfsStream; +use org\bovigo\vfs\vfsStreamDirectory; use headstart\library\Toolkit; require_once __DIR__ . "/../../Toolkit.php"; class ToolkitTest extends TestCase { - /** - * Testing method: addOrInitiatlizeArrayKeyNumerical. - * - * Tests that a new key is added with a value of 1 when it does not exist in the array. - */ - public function testThatKeyWillBeAdded(): void { - $data = []; - $key = 'key'; - - Toolkit::addOrInitiatlizeArrayKeyNumerical($data, $key); - - $this->assertArrayHasKey($key, $data, 'The key should be added to the array.'); - $this->assertSame(1, $data[$key], 'The new key should have a value of 1.'); - } - - /** - * Testing method: addOrInitiatlizeArrayKeyNumerical. - * - * Tests the edge case when key already exists and has 0 value. - */ - public function testThatKeyCountWillBeIncrementedCorrectlyWhenItIsEqualsToZero(): void { - $data = ["key" => 0]; - $key = 'key'; - - Toolkit::addOrInitiatlizeArrayKeyNumerical($data, $key); - - $this->assertArrayHasKey($key, $data, 'The key should be added to the array.'); - $this->assertSame(1, $data[$key], 'The new key should have a value of 1.'); - } - - /** - * Testing method: addOrInitiatlizeArrayKeyNumerical. - * - * Tests that an existing numerical key's value is incremented by 1. - */ - public function testThatKeyCountWillBeIncrementedCorrectly(): void { - $key = 'key'; - $initialValue = 10; - $data = [$key => $initialValue]; - - Toolkit::addOrInitiatlizeArrayKeyNumerical($data, $key); - $this->assertSame($initialValue + 1, $data[$key], 'The existing key value should be incremented.'); - } - - /** - * Testing method: addOrInitiatlizeArrayKeyNumerical. - * - * Tests the behavior when a key exists but its value is null. - * `isset()` returns false for null values, so it should be treated as a new key. - */ - public function testThatFunctionHandlesNullValueAsNew(): void { - $key = 'key'; - $data = [$key => null]; + private vfsStreamDirectory $root; - Toolkit::addOrInitiatlizeArrayKeyNumerical($data, $key); - $this->assertSame(1, $data[$key], 'A key with a null value should be re-initialized to 1.'); + protected function setUp(): void { + $this->root = vfsStream::setup('root'); } /** - * Testing method: addOrInitiatlizeArrayKeyNumerical. + * Testing function - loadIni. * - * Tests that the function does not affect other keys in the array. + * Loads a `config_local.ini` correctly when it is exists + * and ignore a `config.ini`. */ - public function testAddOrInitiatlizeArrayKeyNumericalDoesNotAffectOtherKeys(): void { - $data = [ - 'unrelated_key' => 'some string', - 'another_value' => 123 + public function testLoadIniLoadsLocalConfigWhenExists(): void { + $configContent = "[database]\nhost = default_db_host"; + $localConfigContent = "[database]\nhost = local_db_host"; + + vfsStream::newFile('config.ini')->withContent($configContent)->at($this->root); + vfsStream::newFile('config_local.ini')->withContent($localConfigContent)->at($this->root); + + $result = Toolkit::loadIni($this->root->url() . '/'); + $expected = [ + 'database' => [ + 'host' => 'local_db_host' + ] ]; - $keyToIncrement = 'key'; - $expectedUnrelatedValue = 'some string'; - $expectedAnotherValue = 123; - - Toolkit::addOrInitiatlizeArrayKeyNumerical($data, $keyToIncrement); - - $this->assertSame(1, $data[$keyToIncrement]); - - $this->assertArrayHasKey('unrelated_key', $data); - $this->assertSame($expectedUnrelatedValue, $data['unrelated_key']); - - $this->assertArrayHasKey('another_value', $data); - $this->assertSame($expectedAnotherValue, $data['another_value']); - $this->assertCount(3, $data); + $this->assertEquals($expected, $result); } /** - * Testing method: addOrInitiatlizeArrayKey. + * Testing function - loadIni. * - * Tests that a new key and its value will be added in the array. + * Function returns false if no configuration files were found. */ - public function testThatKeyWithValueWillBeAdded(): void { - $array = []; - $key = 'key'; - $value = 'value'; - - Toolkit::addOrInitiatlizeArrayKey($array, $key, $value); - - $this->assertArrayHasKey($key, $array, 'The key should be added to the array.'); - $this->assertSame(["value"], $array[$key], 'The new key should have a value of 1.'); + public function testLoadIniReturnsFalseWhenNoConfigFilesExist(): void { + $result = @Toolkit::loadIni($this->root->url() . '/'); + $this->assertFalse($result); } /** - * Testing method: addOrInitiatlizeArrayKey. + * Testing function - loadIni. * - * Tests that an existing key will be found and its value will be updated. + * Function loads a `config.ini` by default if a `config_local.ini` is missing. */ - public function testAddsValueToExistingKey(): void { - $array = ['foo' => ['bar']]; - Toolkit::addOrInitiatlizeArrayKey($array, 'foo', 'baz'); - - $this->assertEquals(['bar', 'baz'], $array['foo']); - } - - /** - * Testing method: addOrInitiatlizeArrayKey. - * - * Tests that an existing key will be found and its value will be updated - * with multiple values. - */ - public function testAddsMultipleValuesToExistingKey(): void { - $array = []; - Toolkit::addOrInitiatlizeArrayKey($array, 'items', 1); - Toolkit::addOrInitiatlizeArrayKey($array, 'items', 2); - Toolkit::addOrInitiatlizeArrayKey($array, 'items', 3); + public function testLoadIniLoadsDefaultConfigWhenLocalIsMissing(): void { + $configContent = "[server]\nurl = https://some-example.com"; + vfsStream::newFile('config.ini')->withContent($configContent)->at($this->root); + + $result = Toolkit::loadIni($this->root->url() . '/'); + $expected = [ + 'server' => [ + 'url' => 'https://some-example.com' + ] + ]; - $this->assertEquals([1, 2, 3], $array['items']); + $this->assertEquals($expected, $result); } } \ No newline at end of file From 8dcb3dde8a2d74f0650a9d77690d0b285bfa85bc Mon Sep 17 00:00:00 2001 From: andrei Date: Mon, 21 Jul 2025 13:57:46 +0200 Subject: [PATCH 10/59] refactor: remove tests cache --- .../headstart/library/tests/configuration/.phpunit.result.cache | 1 - 1 file changed, 1 deletion(-) delete mode 100644 server/classes/headstart/library/tests/configuration/.phpunit.result.cache diff --git a/server/classes/headstart/library/tests/configuration/.phpunit.result.cache b/server/classes/headstart/library/tests/configuration/.phpunit.result.cache deleted file mode 100644 index 40c68d10b..000000000 --- a/server/classes/headstart/library/tests/configuration/.phpunit.result.cache +++ /dev/null @@ -1 +0,0 @@ -{"version":1,"defects":{"headstart\\library\\tests\\functions\\ToolkitTest::testLoadIniHandlesPathWithoutTrailingSlash":7},"times":{"headstart\\library\\tests\\functions\\CommUtilsTest::testEchoOrCallbackWithCallback":0.001,"headstart\\library\\tests\\functions\\CommUtilsTest::testEchoOrCallbackWithoutCallback":0,"headstart\\library\\tests\\functions\\CommUtilsTest::testGetParameterExists":0,"headstart\\library\\tests\\functions\\CommUtilsTest::testGetParameterNotExists":0,"headstart\\library\\tests\\functions\\ToolkitFileSystemTest::testCreatesFileInRoot":0,"headstart\\library\\tests\\functions\\ToolkitFileSystemTest::testCreatesDirectoryPathAndFile":0,"headstart\\library\\tests\\functions\\ToolkitFileSystemTest::testThrowsExceptionOnFailure":0,"headstart\\library\\tests\\functions\\ToolkitFileSystemTest::testOpensExistingAndReadableFileSuccessfully":0,"headstart\\library\\tests\\functions\\ToolkitFileSystemTest::testThrowsExceptionIfFileDoesNotExist":0,"headstart\\library\\tests\\functions\\ToolkitFileSystemTest::testThrowsExceptionIfFileIsNotReadable":0,"headstart\\library\\tests\\ToolkitTest::it_loads_local_config_when_it_exists":0,"headstart\\library\\tests\\functions\\ToolkitTest::testLoadIniLoadsLocalConfigWhenExists":0,"headstart\\library\\tests\\functions\\ToolkitTest::testLoadIniLoadsDefaultConfigWhenLocalIsMissing":0,"headstart\\library\\tests\\functions\\ToolkitTest::testLoadIniReturnsFalseWhenNoConfigFilesExist":0.002,"headstart\\library\\tests\\functions\\ToolkitTest::testLoadIniHandlesPathWithoutTrailingSlash":0}} \ No newline at end of file From e6767f1259619ca147ad4b7b2f841bc187aea7d2 Mon Sep 17 00:00:00 2001 From: andrei Date: Mon, 21 Jul 2025 13:58:53 +0200 Subject: [PATCH 11/59] feat: .phpunit.result.cache in the git ignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ae72d260a..0785459ae 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,7 @@ dist/ .cache coverage/ .vscode/ -*/.phpunit.result.cache +.phpunit.result.cache # local deployment files /deploy.sh From b8c1ce496de42117446ebc1eb680c57219f1db43 Mon Sep 17 00:00:00 2001 From: andrei Date: Wed, 13 Aug 2025 10:10:22 +0200 Subject: [PATCH 12/59] feat: tests for the scale.ts functions --- vis/test/utils/scale.test.ts | 99 ++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 vis/test/utils/scale.test.ts diff --git a/vis/test/utils/scale.test.ts b/vis/test/utils/scale.test.ts new file mode 100644 index 000000000..103a253eb --- /dev/null +++ b/vis/test/utils/scale.test.ts @@ -0,0 +1,99 @@ +import { + getCoordsScale, + getDiameterScale, + getInitialCoordsScale, + getRadiusScale, + getResizedScale, + getZoomScale, +} from "../../js/utils/scale"; + +describe("Scale functions", () => { + describe("getCoordsScale", () => { + it("Scale coordinates correctly", () => { + const scaleFn = getCoordsScale([0, 100], 500, { + maxAreaSize: 50, + referenceSize: 500, + bubbleMaxScale: 1, + }); + + const result = scaleFn(50); + + expect(typeof result).toBe("number"); + expect(result).toBeGreaterThan(0); + expect(result).toBeLessThan(500); + }); + }); + + describe("getRadiusScale", () => { + it("Scale radius correctly", () => { + const scaleFn = getRadiusScale([0, 10], 400, { + minAreaSize: 10, + maxAreaSize: 50, + referenceSize: 400, + bubbleMinScale: 1, + bubbleMaxScale: 2, + }); + + const small = scaleFn(0); + const big = scaleFn(10); + + expect(big).toBeGreaterThan(small); + }); + }); + + describe("getDiameterScale", () => { + it("Scale diameter correctly", () => { + const scaleFn = getDiameterScale([1, 5], 300, { + referenceSize: 300, + minDiameterSize: 10, + maxDiameterSize: 30, + paperMinScale: 1, + paperMaxScale: 2, + }); + + const small = scaleFn(1); + const big = scaleFn(5); + + expect(big).toBeGreaterThan(small); + }); + }); + + describe("getInitialCoordsScale", () => { + it("Scale initial coordinates correctly", () => { + const scaleFn = getInitialCoordsScale([0, 100], 200); + + expect(scaleFn(0)).toBeGreaterThan(0); + expect(scaleFn(100)).toBeLessThan(200); + }); + }); + + describe("getResizedScale", () => { + it("Resize coordinates from old size to new size", () => { + const scaleFn = getResizedScale(100, 200); + + expect(scaleFn(0)).toBe(0); + expect(scaleFn(100)).toBe(200); + expect(scaleFn(50)).toBe(100); + }); + }); + + describe("getZoomScale", () => { + it("Zoom coordinates correctly for bubbles", () => { + const scaleFn = getZoomScale(100, 50, 400, "bubble"); + const val = scaleFn(100); + + expect(typeof val).toBe("number"); + expect(val).toBeGreaterThan(0); + expect(val).toBeLessThan(400); + }); + + it("Zoom coordinates correctly for papers", () => { + const scaleFn = getZoomScale(200, 80, 500, "paper"); + const val = scaleFn(250); + + expect(typeof val).toBe("number"); + expect(val).toBeGreaterThan(0); + expect(val).toBeLessThan(500); + }); + }); +}); From 6e8c20cfa90ed02a450ca11bd7c3c606e88d565b Mon Sep 17 00:00:00 2001 From: andrei Date: Fri, 19 Sep 2025 13:35:42 +0200 Subject: [PATCH 13/59] feat: tests for the OrcidMetrics component --- local_dev/searchflow-container/Dockerfile | 2 +- .../list-entry/OrcidMetrics.test.tsx | 184 ++++++++++++++++++ 2 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 vis/test/component/templates/list-entry/OrcidMetrics.test.tsx diff --git a/local_dev/searchflow-container/Dockerfile b/local_dev/searchflow-container/Dockerfile index b5df97e18..24e640f6a 100644 --- a/local_dev/searchflow-container/Dockerfile +++ b/local_dev/searchflow-container/Dockerfile @@ -1,4 +1,4 @@ -FROM php:8.2-apache +FROM php:8.2-apache-bookworm LABEL maintainer="Chris Kittel " diff --git a/vis/test/component/templates/list-entry/OrcidMetrics.test.tsx b/vis/test/component/templates/list-entry/OrcidMetrics.test.tsx new file mode 100644 index 000000000..ad1c06398 --- /dev/null +++ b/vis/test/component/templates/list-entry/OrcidMetrics.test.tsx @@ -0,0 +1,184 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import OrcidMetrics from "../../../../js/templates/listentry/OrcidMetrics"; + +describe("OrcidMetrics in the ListEntry component", () => { + type PossibleMetricsTypes = null | undefined | number | string; + + const setup = ( + citations: PossibleMetricsTypes, + socialMedia: PossibleMetricsTypes, + referencesOutsideAcademia: PossibleMetricsTypes, + baseUnit: null | string, + ) => { + const { container } = render( + , + ); + + return container; + }; + + describe("Citations", () => { + it('Showing "n/a" if citations are not defined', () => { + const container = setup(undefined, "n/a", null, null); + + const spansWithMetrics = container.querySelectorAll(".list_metrics_item"); + const spanWithCitationsMetricSpan = spansWithMetrics[0]; + + expect(spanWithCitationsMetricSpan).toBeInTheDocument(); + expect(spanWithCitationsMetricSpan).toHaveTextContent("n/a"); + }); + + it('Showing "n/a" if citations are equals to null', () => { + const container = setup(null, "n/a", null, null); + + const spansWithMetrics = container.querySelectorAll(".list_metrics_item"); + const spanWithCitationsMetricSpan = spansWithMetrics[0]; + + expect(spanWithCitationsMetricSpan).toBeInTheDocument(); + expect(spanWithCitationsMetricSpan).toHaveTextContent("n/a"); + }); + + it("Shows citations when they are defined and presented as number", () => { + const container = setup(100, "n/a", null, null); + + const spansWithMetrics = container.querySelectorAll(".list_metrics_item"); + const spanWithCitationsMetricSpan = spansWithMetrics[0]; + + expect(spanWithCitationsMetricSpan).toBeInTheDocument(); + expect(spanWithCitationsMetricSpan).toHaveTextContent("100"); + }); + + it("Shows citations when they are defined and presented as string", () => { + const container = setup("100", "n/a", null, null); + + const spansWithMetrics = container.querySelectorAll(".list_metrics_item"); + const spanWithCitationsMetricSpan = spansWithMetrics[0]; + + expect(spanWithCitationsMetricSpan).toBeInTheDocument(); + expect(spanWithCitationsMetricSpan).toHaveTextContent("100"); + }); + + it('Apply additional styles if "citations" is a base unit', () => { + const container = setup("100", "n/a", null, "citations"); + + const spansWithMetrics = container.querySelectorAll(".list_metrics_item"); + const spanWithCitationsMetricSpan = spansWithMetrics[0]; + + expect(spanWithCitationsMetricSpan).toBeInTheDocument(); + expect(spanWithCitationsMetricSpan).toHaveTextContent("100"); + expect(spanWithCitationsMetricSpan).toHaveClass("scaled-metric"); + }); + }); + + describe("Social media", () => { + it('Showing "n/a" if social media metrics are not defined', () => { + const container = setup(100, undefined, null, null); + + const spansWithMetrics = container.querySelectorAll(".list_metrics_item"); + const spanWithCitationsMetricSpan = spansWithMetrics[1]; + + expect(spanWithCitationsMetricSpan).toBeInTheDocument(); + expect(spanWithCitationsMetricSpan).toHaveTextContent("n/a"); + }); + + it('Showing "n/a" if social media metrics are equals to null', () => { + const container = setup(100, null, null, null); + + const spansWithMetrics = container.querySelectorAll(".list_metrics_item"); + const spanWithCitationsMetricSpan = spansWithMetrics[1]; + + expect(spanWithCitationsMetricSpan).toBeInTheDocument(); + expect(spanWithCitationsMetricSpan).toHaveTextContent("n/a"); + }); + + it("Shows social media metrics if they are defined and presented as number", () => { + const container = setup(100, 101, null, null); + + const spansWithMetrics = container.querySelectorAll(".list_metrics_item"); + const spanWithCitationsMetricSpan = spansWithMetrics[1]; + + expect(spanWithCitationsMetricSpan).toBeInTheDocument(); + expect(spanWithCitationsMetricSpan).toHaveTextContent("101"); + }); + + it("Shows social media metrics if they are defined and presented as string", () => { + const container = setup(100, "101", null, null); + + const spansWithMetrics = container.querySelectorAll(".list_metrics_item"); + const spanWithCitationsMetricSpan = spansWithMetrics[1]; + + expect(spanWithCitationsMetricSpan).toBeInTheDocument(); + expect(spanWithCitationsMetricSpan).toHaveTextContent("101"); + }); + + it('Apply additional styles if "social" is a base unit', () => { + const container = setup(100, "101", null, "social"); + + const spansWithMetrics = container.querySelectorAll(".list_metrics_item"); + const spanWithCitationsMetricSpan = spansWithMetrics[1]; + + expect(spanWithCitationsMetricSpan).toBeInTheDocument(); + expect(spanWithCitationsMetricSpan).toHaveTextContent("101"); + expect(spanWithCitationsMetricSpan).toHaveClass("scaled-metric"); + }); + }); + + describe("References outside academia", () => { + it('Showing "n/a" if references outside academia are not defined', () => { + const container = setup(100, 101, undefined, null); + + const spansWithMetrics = container.querySelectorAll(".list_metrics_item"); + const spanWithCitationsMetricSpan = spansWithMetrics[2]; + + expect(spanWithCitationsMetricSpan).toBeInTheDocument(); + expect(spanWithCitationsMetricSpan).toHaveTextContent("n/a"); + }); + + it('Showing "n/a" if references outside academia are equals to null', () => { + const container = setup(100, 101, null, null); + + const spansWithMetrics = container.querySelectorAll(".list_metrics_item"); + const spanWithCitationsMetricSpan = spansWithMetrics[2]; + + expect(spanWithCitationsMetricSpan).toBeInTheDocument(); + expect(spanWithCitationsMetricSpan).toHaveTextContent("n/a"); + }); + + it("Shows references outside academia metrics if they are defined and presented as number", () => { + const container = setup(100, 101, 102, null); + + const spansWithMetrics = container.querySelectorAll(".list_metrics_item"); + const spanWithCitationsMetricSpan = spansWithMetrics[2]; + + expect(spanWithCitationsMetricSpan).toBeInTheDocument(); + expect(spanWithCitationsMetricSpan).toHaveTextContent("102"); + }); + + it("Shows references outside academia metrics if they are defined and presented as string", () => { + const container = setup(100, 101, "102", null); + + const spansWithMetrics = container.querySelectorAll(".list_metrics_item"); + const spanWithCitationsMetricSpan = spansWithMetrics[2]; + + expect(spanWithCitationsMetricSpan).toBeInTheDocument(); + expect(spanWithCitationsMetricSpan).toHaveTextContent("102"); + }); + + it('Apply additional styles if "references" is a base unit', () => { + const container = setup(100, 101, 102, "references"); + + const spansWithMetrics = container.querySelectorAll(".list_metrics_item"); + const spanWithCitationsMetricSpan = spansWithMetrics[2]; + + expect(spanWithCitationsMetricSpan).toBeInTheDocument(); + expect(spanWithCitationsMetricSpan).toHaveTextContent("102"); + expect(spanWithCitationsMetricSpan).toHaveClass("scaled-metric"); + }); + }); +}); From 4ce7f4b56d8f92844a299d5fd110ccf11634fb1c Mon Sep 17 00:00:00 2001 From: andrei Date: Fri, 19 Sep 2025 13:49:25 +0200 Subject: [PATCH 14/59] feat: tests for the Link component --- .../templates/list-entry/Link.test.tsx | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 vis/test/component/templates/list-entry/Link.test.tsx diff --git a/vis/test/component/templates/list-entry/Link.test.tsx b/vis/test/component/templates/list-entry/Link.test.tsx new file mode 100644 index 000000000..9ad43931c --- /dev/null +++ b/vis/test/component/templates/list-entry/Link.test.tsx @@ -0,0 +1,58 @@ +import { render } from "@testing-library/react"; +import Link from "../../../../js/templates/listentry/Link"; +import React from "react"; +import LocalizationProvider from "../../../../js/components/LocalizationProvider"; +import { Localization } from "../../../../js/i18n/localization"; + +describe("Link in the ListEntry component", () => { + const setup = (isDoi: boolean, url?: string | null) => { + const LOCALIZATION_OBJECT_MOCK = { + link: "link", + notAvailable: "n/a", + } as Localization; + + const { container } = render( + + , + , + ); + + return container; + }; + + it('Link named as "doi"', () => { + const container = setup(true, ""); + + expect(container).toHaveTextContent("doi"); + }); + + it('Link named as "link"', () => { + const container = setup(false, ""); + + expect(container).toHaveTextContent("link"); + }); + + it('Link named as "link" and displays url correctly', () => { + const container = setup(false, "https://some.url.com/"); + + expect(container).toHaveTextContent("[link]: https://some.url.com/"); + }); + + it('Link named as "doi" and displays url correctly', () => { + const container = setup(true, "https://some.url.com/"); + + expect(container).toHaveTextContent("[doi]: https://some.url.com/"); + }); + + it('Link named as "doi" and displays "not available" message', () => { + const container = setup(true, undefined); + + expect(container).toHaveTextContent("[doi]: n/a"); + }); + + it('Link named as "link" and displays "not available" message', () => { + const container = setup(false, undefined); + + expect(container).toHaveTextContent("[link]: n/a"); + }); +}); From 3173a669541746429bb49cfc726f984459f26065 Mon Sep 17 00:00:00 2001 From: andrei Date: Wed, 24 Sep 2025 15:16:09 +0200 Subject: [PATCH 15/59] feat: tests for the Abstract component --- .../templates/list-entry/Abstract.test.tsx | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 vis/test/component/templates/list-entry/Abstract.test.tsx diff --git a/vis/test/component/templates/list-entry/Abstract.test.tsx b/vis/test/component/templates/list-entry/Abstract.test.tsx new file mode 100644 index 000000000..eae1de361 --- /dev/null +++ b/vis/test/component/templates/list-entry/Abstract.test.tsx @@ -0,0 +1,62 @@ +import { render } from "@testing-library/react"; +import { Localization } from "../../../../js/i18n/localization"; +import LocalizationProvider from "../../../../js/components/LocalizationProvider"; +import Abstract from "../../../../js/templates/listentry/Abstract"; +import React from "react"; +import { Provider } from "react-redux"; +import configureStore from "redux-mock-store"; + +const mockStore = configureStore([]); + +describe("Abstract in the ListEntry component", () => { + const setup = (isSelectedPaper: boolean, abstractText?: string) => { + const LOCALIZATION_OBJECT_MOCK: Pick = { + default_abstract: "No abstract available", + }; + + const STORE_DATA_MOCK = { + selectedPaper: isSelectedPaper, + list: { searchValue: "" }, + query: { parsedTerms: "" }, + }; + + const STORE_MOCK = mockStore(STORE_DATA_MOCK); + + const { container } = render( + + + + + , + ); + + return container; + }; + + it("Shows abstract text if it is presented", () => { + const container = setup(true, "Some cool abstract text."); + + const paragraphWithAbstract = container.querySelector("#list_abstract"); + expect(paragraphWithAbstract).toBeInTheDocument(); + expect(paragraphWithAbstract).toHaveTextContent("Some cool abstract text."); + }); + + it("Shows abstract text with additional styling if it is presented and document is not selected", () => { + const container = setup(false, "Some cool abstract text."); + + const paragraphWithAbstract = container.querySelector("#list_abstract"); + expect(paragraphWithAbstract).toBeInTheDocument(); + expect(paragraphWithAbstract).toHaveClass("short"); + expect(paragraphWithAbstract).toHaveTextContent("Some cool abstract text."); + }); + + it("Shows template text from localization if abstract is not presented", () => { + const container = setup(false, undefined); + + const paragraphWithAbstract = container.querySelector("#list_abstract"); + expect(paragraphWithAbstract).toBeInTheDocument(); + expect(paragraphWithAbstract).toHaveTextContent("No abstract available"); + }); +}); From 39499310f3d646b20125284f6f51b504b1a2f92f Mon Sep 17 00:00:00 2001 From: andrei Date: Wed, 24 Sep 2025 15:24:30 +0200 Subject: [PATCH 16/59] feat: tests for the Citations component --- .../templates/list-entry/Citations.test.tsx | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 vis/test/component/templates/list-entry/Citations.test.tsx diff --git a/vis/test/component/templates/list-entry/Citations.test.tsx b/vis/test/component/templates/list-entry/Citations.test.tsx new file mode 100644 index 000000000..375121cd7 --- /dev/null +++ b/vis/test/component/templates/list-entry/Citations.test.tsx @@ -0,0 +1,32 @@ +import { render } from "@testing-library/react"; +import Citations from "../../../../js/templates/listentry/Citations"; +import React from "react"; + +describe("Citations in the ListEntry component", () => { + const setup = (numberOfCitation: number, labelOfCitations: string) => { + const { container } = render( + , + ); + + return container; + }; + + it("Shows citations in the correct order (numberOfCitation and labelOfCitations)", () => { + const NUMBER_OF_CITATIONS = 15; + const LABEL_OF_CITATIONS = "citations"; + + const container = setup(NUMBER_OF_CITATIONS, LABEL_OF_CITATIONS); + + const citationsContainer = container.querySelector(".list_readers"); + expect(citationsContainer).toBeInTheDocument(); + + const spanWithLabelOfCitations = citationsContainer?.querySelector( + ".list_readers_entity", + ); + expect(spanWithLabelOfCitations).toBeInTheDocument(); + + expect(citationsContainer).toHaveTextContent( + `${NUMBER_OF_CITATIONS} ${LABEL_OF_CITATIONS}`, + ); + }); +}); From 502c6af573db16f56e211c0aa617afb3feca78fd Mon Sep 17 00:00:00 2001 From: andrei Date: Wed, 24 Sep 2025 17:10:54 +0200 Subject: [PATCH 17/59] feat: test for the Keywords component --- .../templates/list-entry/Keywords.test.tsx | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 vis/test/component/templates/list-entry/Keywords.test.tsx diff --git a/vis/test/component/templates/list-entry/Keywords.test.tsx b/vis/test/component/templates/list-entry/Keywords.test.tsx new file mode 100644 index 000000000..421ba7c39 --- /dev/null +++ b/vis/test/component/templates/list-entry/Keywords.test.tsx @@ -0,0 +1,69 @@ +import { render } from "@testing-library/react"; +import Keywords from "../../../../js/templates/listentry/Keywords"; +import React from "react"; +import LocalizationProvider from "../../../../js/components/LocalizationProvider"; +import { Localization } from "../../../../js/i18n/localization"; +import { Provider } from "react-redux"; +import configureStore from "redux-mock-store"; + +const mockStore = configureStore([]); + +describe("Keywords in the ListEntry component", () => { + const setup = (keywords: string, query: string) => { + const LOCALIZATION_OBJECT_MOCK = { + keywords: "Keywords", + } as Localization; + + const STORE_DATA_MOCK = { + list: { searchValue: "" }, + query: { + parsedTerms: [query], + highlightTerms: true, + useLookBehind: true, + }, + }; + + const STORE_MOCK = mockStore(STORE_DATA_MOCK); + + const { container } = render( + + + {keywords} + + , + ); + + return container; + }; + + it("Shows keywords", () => { + const KEYWORDS_TEXT = "Higher education is a distinct context..."; + const HIGHLIGHTED_WORD = ""; + + const container = setup(KEYWORDS_TEXT, HIGHLIGHTED_WORD); + + const keywordsContainer = container.querySelector(".list_row"); + expect(keywordsContainer).toBeInTheDocument(); + + const keywordsLabel = keywordsContainer?.firstChild; + expect(keywordsLabel).toHaveTextContent("Keywords:"); + + const keywordsContent = keywordsContainer?.lastChild; + expect(keywordsContent).toHaveTextContent(KEYWORDS_TEXT); + }); + + it("Shows keywords and highlight query words in them", () => { + const KEYWORDS_TEXT = "Higher education is a distinct context..."; + const HIGHLIGHTED_WORD = "education"; + + const container = setup(KEYWORDS_TEXT, HIGHLIGHTED_WORD); + + const keywordsContainer = container.querySelector(".list_row"); + expect(keywordsContainer).toBeInTheDocument(); + + const highlightedWord = keywordsContainer?.querySelector( + ".query_term_highlight ", + ); + expect(highlightedWord).toBeInTheDocument(); + }); +}); From 4b20cff42ad061391316b166229d6f1b84979c77 Mon Sep 17 00:00:00 2001 From: andrei Date: Mon, 16 Mar 2026 14:44:42 +0100 Subject: [PATCH 18/59] feat: prototype configuration --- .gitignore | 6 + e2e/example.spec.ts | 10 ++ .../knowledgeMap/toolbar/toolbar.spec.ts | 90 ++++++++++++ e2e/resources.spec.ts | 138 ++++++++++++++++++ package-lock.json | 60 ++++++++ package.json | 7 +- playwright.config.ts | 26 ++++ 7 files changed, 336 insertions(+), 1 deletion(-) create mode 100644 e2e/example.spec.ts create mode 100644 e2e/interactions/knowledgeMap/toolbar/toolbar.spec.ts create mode 100644 e2e/resources.spec.ts create mode 100644 playwright.config.ts diff --git a/.gitignore b/.gitignore index 87d9aa519..f06d6d8f6 100644 --- a/.gitignore +++ b/.gitignore @@ -67,5 +67,11 @@ server/preprocessing/other-scripts/renv local_dev/config_local_headstart.ini local_dev/config_local_searchflow.ini +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ + # mac os .DS_Store diff --git a/e2e/example.spec.ts b/e2e/example.spec.ts new file mode 100644 index 000000000..9bb32c2fe --- /dev/null +++ b/e2e/example.spec.ts @@ -0,0 +1,10 @@ +import { test, expect } from "@playwright/test"; + +test.describe("Headstart base example", () => { + test("Loads search box on landing page", async ({ page }) => { + await page.goto("/"); + + const searchForm = page.locator("#searchform"); + await expect(searchForm).toBeVisible(); + }); +}); diff --git a/e2e/interactions/knowledgeMap/toolbar/toolbar.spec.ts b/e2e/interactions/knowledgeMap/toolbar/toolbar.spec.ts new file mode 100644 index 000000000..55166eacc --- /dev/null +++ b/e2e/interactions/knowledgeMap/toolbar/toolbar.spec.ts @@ -0,0 +1,90 @@ +import { test, expect } from "@playwright/test"; + +test.describe("Toolbar component interactions", () => { + const VISUALISATION_SEARCH_URL = + "/search?type=get&vis_type=overview&q=digital%20education&service=base&sorting=most-relevant&document_types%5B%5D=121&lang_id%5B%5D=all-lang&min_descsize=300"; + + const prepareVisualisation = async (page) => { + await page.goto(VISUALISATION_SEARCH_URL); + + const contextLine = page.getByTestId("context"); + await expect(contextLine).toBeVisible({ timeout: 60_000 }); + + await expect + .poll( + async () => + ((await contextLine.textContent()) ?? "").replace(/\s+/g, " ").trim(), + { timeout: 60_000, intervals: [500, 1000, 2000, 5000] }, + ) + .not.toBe(""); + }; + + test("Share with email", async ({ page }) => { + await prepareVisualisation(page); + + const EMAIL_BUTTON_TITLE = "Share this visualization via email"; + const emailButton = page.getByTitle(EMAIL_BUTTON_TITLE); + + await expect(emailButton).toBeVisible(); + }); + + test("Embed visualisation", async ({ page }) => { + await prepareVisualisation(page); + + const EMBED_BUTTON_TITLE = "Embed this visualization on other websites"; + const embedButton = page.getByTitle(EMBED_BUTTON_TITLE); + await expect(embedButton).toBeVisible(); + + const MODAL_TITLE = "embed visualization"; + const MODAL_DESCRIPTION = + "You can use this code to embed the visualization on your own website or in a dashboard."; + const IFRAME_CONTENT_START_WITH = '