diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 48b28a8..2a64431 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,6 +39,27 @@ jobs: set -eux jlpm run test + test_extensions: + runs-on: ubuntu-latest + needs: build_jupyter-chat + name: Python test on extensions + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Base Setup + uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 + + - name: Install dependencies + run: python -m pip install -U "jupyterlab>=4.0.0,<5" + + - name: Build the extensions + run: | + set -eux + ./scripts/install.sh + pytest -vv -r ap --cov + build_extensions: runs-on: ubuntu-latest needs: build_jupyter-chat @@ -57,7 +78,7 @@ jobs: - name: Install dependencies run: python -m pip install -U "jupyterlab>=4.0.0,<5" - - name: Build package + - name: Package the extensions run: | jlpm install jlpm build:${{ matrix.extension }} diff --git a/conftest.py b/conftest.py index 83bd5b7..5987622 100644 --- a/conftest.py +++ b/conftest.py @@ -1,6 +1,5 @@ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. - import pytest pytest_plugins = ("pytest_jupyter.jupyter_server", ) diff --git a/python/jupyterlab-collaborative-chat/conftest.py b/python/jupyterlab-collaborative-chat/conftest.py deleted file mode 100644 index 1eea6b2..0000000 --- a/python/jupyterlab-collaborative-chat/conftest.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) Jupyter Development Team. -# Distributed under the terms of the Modified BSD License. - -import pytest - -pytest_plugins = ("pytest_jupyter.jupyter_server", ) - - -@pytest.fixture -def jp_server_config(jp_server_config): - return {"ServerApp": {"jpserver_extensions": {"jupyterlab_collaborative_chat": True}}} diff --git a/python/jupyterlab-collaborative-chat/jupyterlab_collaborative_chat/tests/test_handlers.py b/python/jupyterlab-collaborative-chat/jupyterlab_collaborative_chat/tests/test_handlers.py deleted file mode 100644 index 902af76..0000000 --- a/python/jupyterlab-collaborative-chat/jupyterlab_collaborative_chat/tests/test_handlers.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) Jupyter Development Team. -# Distributed under the terms of the Modified BSD License. - -### TODO #### diff --git a/python/jupyterlab-collaborative-chat/jupyterlab_collaborative_chat/tests/test_ychat.py b/python/jupyterlab-collaborative-chat/jupyterlab_collaborative_chat/tests/test_ychat.py new file mode 100644 index 0000000..f5934ab --- /dev/null +++ b/python/jupyterlab-collaborative-chat/jupyterlab_collaborative_chat/tests/test_ychat.py @@ -0,0 +1,111 @@ +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. + +# import jupyter_ydoc before YChat to avoid circular error +import jupyter_ydoc + +import pytest +import time +from copy import deepcopy +from uuid import uuid4 +from ..ychat import YChat + +USER = { + "username": str(uuid4()), + "name": "Test user", + "display_name": "Test user" +} + +USER2 = { + "username": str(uuid4()), + "name": "Test user 2", + "display_name": "Test user 2" +} + + +def create_message(): + return { + "type": "msg", + "id": str(uuid4()), + "body": "This is a test message", + "time": time.time(), + "sender": USER["username"] +} + + +def test_initialize_ychat(): + chat = YChat() + assert chat.get_messages() == [] + assert chat.get_users() == {} + assert chat.get_metadata() == {} + + +def test_add_user(): + chat = YChat() + chat.set_user(USER) + assert USER["username"] in chat.get_users().keys() + assert chat.get_users()[USER["username"]] == USER + + +def test_get_user_by_name(): + chat = YChat() + chat.set_user(USER) + chat.set_user(USER2) + assert chat.get_user_by_name(USER["name"]) == USER + assert chat.get_user_by_name(USER2["name"]) == USER2 + assert chat.get_user_by_name(str(uuid4())) == None + + +def test_add_message(): + chat = YChat() + msg = create_message() + chat.add_message(msg) + assert len(chat.get_messages()) == 1 + assert chat.get_messages()[0] == msg + + +def test_set_message_should_add(): + chat = YChat() + msg = create_message() + chat.set_message(msg) + assert len(chat.get_messages()) == 1 + assert chat.get_messages()[0] == msg + + +def test_set_message_should_update(): + chat = YChat() + msg = create_message() + index = chat.add_message(msg) + msg["body"] = "Updated content" + chat.set_message(msg, index) + assert len(chat.get_messages()) == 1 + assert chat.get_messages()[0] == msg + + +def test_set_message_should_add_with_new_id(): + chat = YChat() + msg = create_message() + index = chat.add_message(msg) + new_msg = deepcopy(msg) + new_msg["id"] = str(uuid4()) + new_msg["body"] = "Updated content" + chat.set_message(new_msg, index) + assert len(chat.get_messages()) == 2 + assert chat.get_messages()[0] == msg + assert chat.get_messages()[1] == new_msg + + +def test_set_message_should_update_with_wrong_index(): + chat = YChat() + msg = create_message() + chat.add_message(msg) + new_msg = create_message() + new_msg["body"] = "New content" + index = chat.add_message(new_msg) + assert index == 1 + new_msg["body"] = "Updated content" + chat.set_message(new_msg, 0) + assert len(chat.get_messages()) == 2 + assert chat.get_messages()[0] == msg + assert chat.get_messages()[1] == new_msg + diff --git a/python/jupyterlab-collaborative-chat/jupyterlab_collaborative_chat/ychat.py b/python/jupyterlab-collaborative-chat/jupyterlab_collaborative_chat/ychat.py index ffde8b6..b3cdc17 100644 --- a/python/jupyterlab-collaborative-chat/jupyterlab_collaborative_chat/ychat.py +++ b/python/jupyterlab-collaborative-chat/jupyterlab_collaborative_chat/ychat.py @@ -81,13 +81,13 @@ def set_user(self, user: dict[str, str]) -> None: with self._ydoc.transaction(): self._yusers.update({user["username"]: user}) - def get_message(self, id: str) -> dict | None: + def get_message(self, id: str) -> tuple[dict | None, int | None]: """ - Returns a message from its id, or None + Returns a message and its index from its id, or None """ return next( - (msg for msg in self.get_messages() if msg["id"] == id), - None + ((msg, i) for i, msg in enumerate(self.get_messages()) if msg["id"] == id), + (None, None) ) def get_messages(self) -> list[dict]: @@ -97,12 +97,45 @@ def get_messages(self) -> list[dict]: """ return self._ymessages.to_py() - def add_message(self, message: dict) -> None: + def add_message(self, message: dict) -> int: + """ + Append a message to the document. + """ + timestamp: float = time.time() + message["time"] = timestamp + with self._ydoc.transaction(): + index = len(self._ymessages) - next((i for i, v in enumerate(self.get_messages()[::-1]) if v["time"] < timestamp), len(self._ymessages)) + self._ymessages.insert(index, message) + return index + + def update_message(self, message: dict, index: int, append: bool = False): """ - Appends a message to the document. + Update a message of the document. + If append is True, the content will be append to the previous content. """ with self._ydoc.transaction(): - self._ymessages.append(message) + initial_message = self._ymessages.pop(index) + if append: + message["body"] = initial_message["body"] + message["body"] + self._ymessages.insert(index, message) + + def set_message(self, message: dict, index: int | None = None, append: bool = False): + """ + Update or append a message. + """ + + if index is not None and 0 <= index < len(self._ymessages): + initial_message = self._ymessages[index] + else: + return self.add_message(message) + + if not initial_message["id"] == message["id"]: + initial_message, index = self.get_message(message["id"]) + if initial_message is None: + return self.add_message(message) + + self.update_message(message, index, append) + return index def get_single_metadata(self, name) -> dict: """