From 29e37350e561e61f9c530bc28ee8514b44033f6d Mon Sep 17 00:00:00 2001 From: Nikhil Rao Date: Sun, 20 Nov 2022 14:34:25 -0800 Subject: [PATCH] Add basic unit tests (#7) * Unit tests for components, state, and utils --- pynecone/.templates/app/tutorial.py | 7 +- pynecone/app.py | 6 +- pynecone/compiler/templates.py | 4 +- pynecone/components/component.py | 2 +- pynecone/state.py | 13 ++- pynecone/var.py | 4 +- pyproject.toml | 2 +- tests/__init__.py | 1 + tests/compiler/__init__.py | 1 + tests/compiler/test_compiler.py | 72 ++++++++++++ tests/components/__init__.py | 1 + tests/components/test_component.py | 152 +++++++++++++++++++++++++ tests/components/test_tag.py | 108 ++++++------------ tests/test_app.py | 95 +++++++++++++--- tests/test_base.py | 47 ++++++++ tests/test_event.py | 38 +++++-- tests/test_pynecone.py | 0 tests/test_state.py | 29 +++-- tests/test_utils.py | 12 ++ tests/test_var.py | 169 ++++++++++++++++++++++++++++ 20 files changed, 640 insertions(+), 123 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/compiler/__init__.py create mode 100644 tests/compiler/test_compiler.py create mode 100644 tests/components/__init__.py create mode 100644 tests/components/test_component.py create mode 100644 tests/test_base.py delete mode 100644 tests/test_pynecone.py create mode 100644 tests/test_var.py diff --git a/pynecone/.templates/app/tutorial.py b/pynecone/.templates/app/tutorial.py index 2221a6c1a..4a93a3a0a 100644 --- a/pynecone/.templates/app/tutorial.py +++ b/pynecone/.templates/app/tutorial.py @@ -1,5 +1,6 @@ """Welcome to Pynecone! This file outlines the steps to create a basic app.""" import pcconfig + import pynecone as pc docs_url = "https://pynecone.io/docs/getting-started/introduction" @@ -8,6 +9,7 @@ filename = f"{pcconfig.APP_NAME}/{pcconfig.APP_NAME}.py" class State(pc.State): """The app state.""" + pass @@ -21,10 +23,10 @@ def index(): href=docs_url, border="0.1em solid", padding="0.5em", - border_radius="0.5em" + border_radius="0.5em", ), ), - padding="5em" + padding="5em", ) @@ -32,4 +34,3 @@ def index(): app = pc.App(state=State) app.add_page(index) app.compile() - diff --git a/pynecone/app.py b/pynecone/app.py index 17879e582..0d0aa5549 100644 --- a/pynecone/app.py +++ b/pynecone/app.py @@ -1,5 +1,6 @@ """The main Pynecone app.""" +import os import re from typing import Any, Callable, Coroutine, Dict, List, Optional, Tuple, Type, Union @@ -12,7 +13,7 @@ from pynecone.compiler import compiler from pynecone.compiler import utils as compiler_utils from pynecone.components.component import Component, ComponentStyle from pynecone.event import Event -from pynecone.middleware import HydrateMiddleware, LoggingMiddleware, Middleware +from pynecone.middleware import HydrateMiddleware, Middleware from pynecone.model import Model from pynecone.state import DefaultState, Delta, State, StateManager, StateUpdate @@ -56,7 +57,6 @@ class App(Base): # Add middleware. self.middleware.append(HydrateMiddleware()) - self.middleware.append(LoggingMiddleware()) # Set up the state manager. self.state_manager.set(state=self.state) @@ -187,7 +187,7 @@ class App(Base): from pynecone.var import BaseVar - parts = path.split("/") + parts = os.path.split(path) check = re.compile(r"^\[(.+)\]$") args = [] for part in parts: diff --git a/pynecone/compiler/templates.py b/pynecone/compiler/templates.py index 5cc4a384a..d4bb08e54 100644 --- a/pynecone/compiler/templates.py +++ b/pynecone/compiler/templates.py @@ -1,8 +1,8 @@ """Templates to use in the pynecone compiler.""" -from typing import Callable, Optional, Set +from typing import Optional, Set -from pynecone import constants, utils +from pynecone import constants from pynecone.utils import join # Template for the Pynecone config file. diff --git a/pynecone/components/component.py b/pynecone/components/component.py index 5b8959c2b..2d44bc527 100644 --- a/pynecone/components/component.py +++ b/pynecone/components/component.py @@ -327,7 +327,7 @@ class Component(Base, ABC): for child in self.children: child_code = child.get_custom_code() if child_code != "" and child_code not in code: - code += child_code + code += "\n" + child_code return code def _get_imports(self) -> ImportDict: diff --git a/pynecone/state.py b/pynecone/state.py index e2cdef779..bbf62c1cf 100644 --- a/pynecone/state.py +++ b/pynecone/state.py @@ -368,13 +368,16 @@ class State(Base, ABC): Returns: The delta for the state. """ + delta = {} + # Return the dirty vars, as well as all computed vars. - delta = { - self.get_full_name(): { - prop: getattr(self, prop) - for prop in self.dirty_vars | set(self.computed_vars.keys()) - } + subdelta = { + prop: getattr(self, prop) + for prop in self.dirty_vars | set(self.computed_vars.keys()) } + if len(subdelta) > 0: + delta[self.get_full_name()] = subdelta + # Recursively find the substate deltas. for substate in self.dirty_substates: delta.update(self.substates[substate].get_delta()) diff --git a/pynecone/var.py b/pynecone/var.py index cffb48905..a8477b5fc 100644 --- a/pynecone/var.py +++ b/pynecone/var.py @@ -4,7 +4,7 @@ from __future__ import annotations import json from abc import ABC from typing import _GenericAlias # type: ignore -from typing import TYPE_CHECKING, Any, Callable, List, Optional, Type, Union +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Type, Union from plotly.graph_objects import Figure from plotly.io import to_json @@ -153,7 +153,7 @@ class Var(ABC): type_ = utils.get_args(self.type_)[0] else: type_ = Any - elif utils.is_dataframe(self.type_): + elif utils._issubclass(self.type_, Dict) or utils.is_dataframe(self.type_): if isinstance(i, str): i = utils.wrap(i, '"') if isinstance(self.type_, _GenericAlias): diff --git a/pyproject.toml b/pyproject.toml index 345727f69..ee59f1f4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pynecone-io" -version = "0.1.2" +version = "0.1.3" description = "" authors = [ "Nikhil Rao ", diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..b318bba63 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Root directory for tests.""" diff --git a/tests/compiler/__init__.py b/tests/compiler/__init__.py new file mode 100644 index 000000000..c8cd54ad0 --- /dev/null +++ b/tests/compiler/__init__.py @@ -0,0 +1 @@ +"""Compiler tests.""" diff --git a/tests/compiler/test_compiler.py b/tests/compiler/test_compiler.py new file mode 100644 index 000000000..2d03f1cae --- /dev/null +++ b/tests/compiler/test_compiler.py @@ -0,0 +1,72 @@ +from typing import Set + +import pytest + +from pynecone.compiler import utils + + +@pytest.mark.parametrize( + "lib,fields,output", + [ + ("axios", {"axios"}, 'import axios from "axios"'), + ("axios", {"foo", "bar"}, 'import {bar, foo} from "axios"'), + ("axios", {"axios", "foo", "bar"}, 'import axios, {bar, foo} from "axios"'), + ], +) +def test_compile_import_statement(lib: str, fields: Set[str], output: str): + """Test the compile_import_statement function. + + Args: + lib: The library name. + fields: The fields to import. + output: The expected output. + """ + assert utils.compile_import_statement(lib, fields) == output + + +@pytest.mark.parametrize( + "import_dict,output", + [ + ({}, ""), + ({"axios": {"axios"}}, 'import axios from "axios"'), + ({"axios": {"foo", "bar"}}, 'import {bar, foo} from "axios"'), + ( + {"axios": {"axios", "foo", "bar"}, "react": {"react"}}, + 'import axios, {bar, foo} from "axios"\nimport react from "react"', + ), + ({"": {"lib1.js", "lib2.js"}}, 'import "lib1.js"\nimport "lib2.js"'), + ( + {"": {"lib1.js", "lib2.js"}, "axios": {"axios"}}, + 'import "lib1.js"\nimport "lib2.js"\nimport axios from "axios"', + ), + ], +) +def test_compile_imports(import_dict: utils.ImportDict, output: str): + """Test the compile_imports function. + + Args: + import_dict: The import dictionary. + output: The expected output. + """ + assert utils.compile_imports(import_dict) == output + + +@pytest.mark.parametrize( + "name,value,output", + [ + ("foo", "bar", 'const foo = "bar"'), + ("num", 1, "const num = 1"), + ("check", False, "const check = false"), + ("arr", [1, 2, 3], "const arr = [1, 2, 3]"), + ("obj", {"foo": "bar"}, 'const obj = {"foo": "bar"}'), + ], +) +def test_compile_constant_declaration(name: str, value: str, output: str): + """Test the compile_constant_declaration function. + + Args: + name: The name of the constant. + value: The value of the constant. + output: The expected output. + """ + assert utils.compile_constant_declaration(name, value) == output diff --git a/tests/components/__init__.py b/tests/components/__init__.py new file mode 100644 index 000000000..81d14d67c --- /dev/null +++ b/tests/components/__init__.py @@ -0,0 +1 @@ +"""Component tests.""" diff --git a/tests/components/test_component.py b/tests/components/test_component.py new file mode 100644 index 000000000..8e1f81d01 --- /dev/null +++ b/tests/components/test_component.py @@ -0,0 +1,152 @@ +from typing import Type + +import pytest + +from pynecone.components.component import Component, ImportDict +from pynecone.event import EventHandler +from pynecone.style import Style + + +@pytest.fixture +def component1() -> Type[Component]: + """A test component. + + Returns: + A test component. + """ + + class TestComponent1(Component): + def _get_imports(self) -> ImportDict: + return {"react": {"Component"}} + + def _get_custom_code(self) -> str: + return "console.log('component1')" + + return TestComponent1 + + +@pytest.fixture +def component2() -> Type[Component]: + """A test component. + + Returns: + A test component. + """ + + class TestComponent2(Component): + def _get_imports(self) -> ImportDict: + return {"react-redux": {"connect"}} + + def _get_custom_code(self) -> str: + return "console.log('component2')" + + return TestComponent2 + + +@pytest.fixture +def on_click1() -> EventHandler: + """A sample on click function. + + Returns: + A sample on click function. + """ + + def on_click1(): + pass + + return EventHandler(fn=on_click1) + + +@pytest.fixture +def on_click2() -> EventHandler: + """A sample on click function. + + Returns: + A sample on click function. + """ + + def on_click2(): + pass + + return EventHandler(fn=on_click2) + + +def test_set_style_attrs(component1: Type[Component]): + """Test that style attributes are set in the dict. + + Args: + component1: A test component. + """ + component = component1(color="white", text_align="center") + assert component.style["color"] == "white" + assert component.style["textAlign"] == "center" + + +def test_create_component(component1: Type[Component]): + """Test that the component is created correctly. + + Args: + component1: A test component. + """ + children = [component1() for _ in range(3)] + attrs = {"color": "white", "text_align": "center"} + c = component1.create(*children, **attrs) + assert isinstance(c, component1) + assert c.children == children + assert c.style == {"color": "white", "textAlign": "center"} + + +def test_add_style(component1: Type[Component], component2: Type[Component]): + """Test adding a style to a component. + + Args: + component1: A test component. + component2: A test component. + """ + style = { + component1: Style({"color": "white"}), + component2: Style({"color": "black"}), + } + c1 = component1().add_style(style) # type: ignore + c2 = component2().add_style(style) # type: ignore + assert c1.style["color"] == "white" + assert c2.style["color"] == "black" + + +def test_get_imports(component1: Type[Component], component2: Type[Component]): + """Test getting the imports of a component. + + Args: + component1: A test component. + component2: A test component. + """ + c1 = component1.create() + c2 = component2.create(c1) + assert c1.get_imports() == {"react": {"Component"}} + assert c2.get_imports() == {"react-redux": {"connect"}, "react": {"Component"}} + + +def test_get_custom_code(component1: Type[Component], component2: Type[Component]): + """Test getting the custom code of a component. + + Args: + component1: A test component. + component2: A test component. + """ + # Check that the code gets compiled correctly. + c1 = component1.create() + c2 = component2.create() + assert c1.get_custom_code() == "console.log('component1')" + assert c2.get_custom_code() == "console.log('component2')" + + # Check that nesting components compiles both codes. + c1 = component1.create(c2) + assert ( + c1.get_custom_code() == "console.log('component1')\nconsole.log('component2')" + ) + + # Check that code is not duplicated. + c1 = component1.create(c2, c2, c1, c1) + assert ( + c1.get_custom_code() == "console.log('component1')\nconsole.log('component2')" + ) diff --git a/tests/components/test_tag.py b/tests/components/test_tag.py index 200f721da..9d8fda012 100644 --- a/tests/components/test_tag.py +++ b/tests/components/test_tag.py @@ -1,9 +1,9 @@ from typing import Dict -import pydantic import pytest -from pynecone.components.tags import CondTag, Tag +from pynecone.components import Box +from pynecone.components.tags import CondTag, IterTag, Tag from pynecone.event import EventHandler, EventSpec, EventChain from pynecone.var import BaseVar, Var @@ -13,29 +13,7 @@ def mock_event(arg): @pytest.mark.parametrize( - "cond,valid", - [ - (BaseVar(name="p", type_=bool), True), - (BaseVar(name="p", type_=int), False), - (BaseVar(name="p", type_=str), False), - ], -) -def test_validate_cond(cond: BaseVar, valid: bool): - """Test that the cond is a boolean. - - Args: - cond: The cond to test. - valid: The expected validity of the cond. - """ - if not valid: - with pytest.raises(pydantic.ValidationError): - Tag(cond=cond) - else: - assert cond == Tag(cond=cond).cond - - -@pytest.mark.parametrize( - "attr,formatted", + "prop,formatted", [ ("string", '"string"'), ("{wrapped_string}", "{wrapped_string}"), @@ -47,31 +25,35 @@ def test_validate_cond(cond: BaseVar, valid: bool): (["a", "b", "c"], '{["a", "b", "c"]}'), ({"a": 1, "b": 2, "c": 3}, '{{"a": 1, "b": 2, "c": 3}}'), ( - EventSpec(handler=EventHandler(fn=mock_event)), + EventChain(events=[EventSpec(handler=EventHandler(fn=mock_event))]), '{() => Event([E("mock_event", {})])}', ), ( - EventSpec( - handler=EventHandler(fn=mock_event), - local_args=("e",), - args=(("arg", "e.target.value"),), + EventChain( + events=[ + EventSpec( + handler=EventHandler(fn=mock_event), + local_args=("e",), + args=(("arg", "e.target.value"),), + ) + ] ), '{(e) => Event([E("mock_event", {arg:e.target.value})])}', ), ], ) -def test_format_value(attr: Var, formatted: str): - """Test that the formatted value of an attribute is correct. +def test_format_value(prop: Var, formatted: str): + """Test that the formatted value of an prop is correct. Args: - attr: The attribute to test. + prop: The prop to test. formatted: The expected formatted value. """ - assert Tag.format_attr_value(attr) == formatted + assert Tag.format_prop(prop) == formatted @pytest.mark.parametrize( - "attrs,formatted", + "props,formatted", [ ({}, ""), ({"key": 1}, "key={1}"), @@ -79,18 +61,18 @@ def test_format_value(attr: Var, formatted: str): ({"key": True, "key2": "value2"}, 'key={true}\nkey2="value2"'), ], ) -def test_format_attrs(attrs: Dict[str, Var], formatted: str): - """Test that the formatted attributes are correct. +def test_format_props(props: Dict[str, Var], formatted: str): + """Test that the formatted props are correct. Args: - attrs: The attributes to test. - formatted: The expected formatted attributes. + props: The props to test. + formatted: The expected formatted props. """ - assert Tag(attrs=attrs).format_attrs() == formatted + assert Tag(props=props).format_props() == formatted @pytest.mark.parametrize( - "attr,valid", + "prop,valid", [ (1, True), (3.14, True), @@ -101,23 +83,23 @@ def test_format_attrs(attrs: Dict[str, Var], formatted: str): (None, False), ], ) -def test_is_valid_attr(attr: Var, valid: bool): - """Test that the attribute is valid. +def test_is_valid_prop(prop: Var, valid: bool): + """Test that the prop is valid. Args: - attr: The attribute to test. - valid: The expected validity of the attribute. + prop: The prop to test. + valid: The expected validity of the prop. """ - assert Tag.is_valid_attr(attr) == valid + assert Tag.is_valid_prop(prop) == valid def test_add_props(): - """Test that the attributes are added.""" + """Test that the props are added.""" tag = Tag().add_props(key="value", key2=42, invalid=None, invalid2={}) - assert tag.attrs["key"] == Var.create("value") - assert tag.attrs["key2"] == Var.create(42) - assert "invalid" not in tag.attrs - assert "invalid2" not in tag.attrs + assert tag.props["key"] == Var.create("value") + assert tag.props["key2"] == Var.create(42) + assert "invalid" not in tag.props + assert "invalid2" not in tag.props @pytest.mark.parametrize( @@ -128,25 +110,17 @@ def test_add_props(): (Tag(contents="hello"), "<>hello"), (Tag(name="h1", contents="hello"), "

hello

"), ( - Tag(name="box", attrs={"color": "red", "textAlign": "center"}), + Tag(name="box", props={"color": "red", "textAlign": "center"}), '', ), ( Tag( name="box", - attrs={"color": "red", "textAlign": "center"}, + props={"color": "red", "textAlign": "center"}, contents="text", ), 'text', ), - ( - Tag( - name="h1", - contents="hello", - cond=BaseVar(name="logged_in", type_=bool), - ), - '{logged_in ?

hello

: ""}', - ), ], ) def test_format_tag(tag: Tag, expected: str): @@ -167,15 +141,3 @@ def test_format_cond_tag(): cond=BaseVar(name="logged_in", type_=bool), ) assert str(tag) == "{logged_in ?

True content

:

False content

}" - - -def test_format_iter_tag(): - """Test that the formatted iter tag is correct.""" - # def render_todo(todo: str): - # return Tag(name="Text", contents=todo) - - # tag = IterTag( - # iterable=BaseVar(name="todos", type_=list), - # render_fn=render_todo - # ) - # assert str(tag) == '{state.todos.map(render_todo)}' diff --git a/tests/test_app.py b/tests/test_app.py index 84ea47a2a..fa299670d 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,9 +1,12 @@ +from typing import Type + import pytest -from pynecone.base import Base from pynecone.app import App, DefaultState from pynecone.middleware import HydrateMiddleware from pynecone.components import Box +from pynecone.state import State +from pynecone.style import Style @pytest.fixture @@ -36,25 +39,32 @@ def about_page(): return about -def test_default_state(app: App) -> None: - """Test creating an app with no state. +@pytest.fixture() +def TestState() -> Type[State]: + """A default state. + + Returns: + A default state. + """ + + class TestState(State): + var: int + + return TestState + + +def test_default_app(app: App): + """Test creating an app with no args. Args: app: The app to test. """ assert app.state() == DefaultState() - - -def test_default_middleware(app: App) -> None: - """Test creating an app with no middleware. - - Args: - app: The app to test. - """ assert app.middleware == [HydrateMiddleware()] + assert app.style == Style() -def test_add_page_default_route(app: App, index_page, about_page) -> None: +def test_add_page_default_route(app: App, index_page, about_page): """Test adding a page to an app. Args: @@ -69,7 +79,7 @@ def test_add_page_default_route(app: App, index_page, about_page) -> None: assert set(app.pages.keys()) == {"index", "about"} -def test_add_page_set_route(app: App, index_page) -> None: +def test_add_page_set_route(app: App, index_page): """Test adding a page to an app. Args: @@ -79,3 +89,62 @@ def test_add_page_set_route(app: App, index_page) -> None: assert app.pages == {} app.add_page(index_page, path="/test") assert set(app.pages.keys()) == {"test"} + + +def test_add_page_set_route_nested(app: App, index_page): + """Test adding a page to an app. + + Args: + app: The app to test. + index_page: The index page. + """ + assert app.pages == {} + app.add_page(index_page, path="/test/nested") + assert set(app.pages.keys()) == {"test/nested"} + + +def test_initialize_with_state(TestState: Type[State]): + """Test setting the state of an app. + + Args: + DefaultState: The default state. + """ + app = App(state=TestState) + assert app.state == TestState + + # Get a state for a given token. + token = "token" + state = app.get_state(token) + assert isinstance(state, TestState) + assert state.var == 0 + + +def test_set_and_get_state(TestState: Type[State]): + """Test setting and getting the state of an app with different tokens. + + Args: + DefaultState: The default state. + """ + app = App(state=TestState) + + # Create two tokens. + token1 = "token1" + token2 = "token2" + + # Get the default state for each token. + state1 = app.get_state(token1) + state2 = app.get_state(token2) + assert state1.var == 0 + assert state2.var == 0 + + # Set the vars to different values. + state1.var = 1 + state2.var = 2 + app.set_state(token1, state1) + app.set_state(token2, state2) + + # Get the states again and check the values. + state1 = app.get_state(token1) + state2 = app.get_state(token2) + assert state1.var == 1 + assert state2.var == 2 diff --git a/tests/test_base.py b/tests/test_base.py new file mode 100644 index 000000000..250eb1e92 --- /dev/null +++ b/tests/test_base.py @@ -0,0 +1,47 @@ +import pytest + +from pynecone.base import Base + + +@pytest.fixture +def child() -> Base: + """A child class. + + Returns: + A child class. + """ + + class Child(Base): + num: float + key: str + + return Child(num=3.14, key="pi") + + +def test_get_fields(child): + """Test that the fields are set correctly. + + Args: + child: A child class. + """ + assert child.get_fields().keys() == {"num", "key"} + + +def test_set(child): + """Test setting fields. + + Args: + child: A child class. + """ + child.set(num=1, key="a") + assert child.num == 1 + assert child.key == "a" + + +def test_json(child): + """Test converting to json. + + Args: + child: A child class. + """ + assert child.json().replace(" ", "") == '{"num":3.14,"key":"pi"}' diff --git a/tests/test_event.py b/tests/test_event.py index 005da6e5b..582ebfad9 100644 --- a/tests/test_event.py +++ b/tests/test_event.py @@ -1,15 +1,35 @@ -from datetime import datetime +import pytest -from pynecone.event import Event +from pynecone.event import Event, EventHandler, EventSpec -# def test_event_default_date(): -# """Test that that the default date is set.""" -# t1 = datetime.now() +def test_create_event(): + """Test creating an event.""" + event = Event(token="token", name="state.do_thing", payload={"arg": "value"}) + assert event.token == "token" + assert event.name == "state.do_thing" + assert event.payload == {"arg": "value"} -# e1 = Event(token="t", name="e1") -# e2 = Event(token="t", name="e2") -# t2 = datetime.now() +def test_call_event_handler(): + """Test that calling an event handler creates an event spec.""" -# assert t1 < e1.date < e2.date < t2 + def test_fn(): + pass + + def test_fn_with_args(_, arg1, arg2): + pass + + handler = EventHandler(fn=test_fn) + event_spec = handler() + + assert event_spec.handler == handler + assert event_spec.local_args == () + assert event_spec.args == () + + handler = EventHandler(fn=test_fn_with_args) + event_spec = handler("first", "second") + + assert event_spec.handler == handler + assert event_spec.local_args == () + assert event_spec.args == (("arg1", "first"), ("arg2", "second")) diff --git a/tests/test_pynecone.py b/tests/test_pynecone.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/test_state.py b/tests/test_state.py index 815f882fc..751476465 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -643,14 +643,15 @@ async def test_process_event_simple(TestState): test_state = TestState() assert test_state.num1 == 0 - event = Event(token="t", name="set_num1", payload={"num1": 69}) - delta = await test_state.process(event) + event = Event(token="t", name="set_num1", payload={"value": 69}) + update = await test_state.process(event) # The event should update the value. assert test_state.num1 == 69 # The delta should contain the changes, including computed vars. - assert delta == {"test_state": {"num1": 69, "sum": 72.14, "upper": ""}} + assert update.delta == {"test_state": {"num1": 69, "sum": 72.14, "upper": ""}} + assert update.events == [] @pytest.mark.asyncio @@ -672,10 +673,13 @@ async def test_process_event_substate(TestState, ChildState, GrandchildState): event = Event( token="t", name="child_state.change_both", payload={"value": "hi", "count": 12} ) - delta = await test_state.process(event) + update = await test_state.process(event) assert child_state.value == "HI" assert child_state.count == 24 - assert delta == {"test_state.child_state": {"value": "HI", "count": 24}} + assert update.delta == { + "test_state.child_state": {"value": "HI", "count": 24}, + "test_state": {"sum": 3.14, "upper": ""}, + } test_state.clean() # Test with the granchild state. @@ -683,11 +687,14 @@ async def test_process_event_substate(TestState, ChildState, GrandchildState): event = Event( token="t", name="child_state.grandchild_state.set_value2", - payload={"value2": "new"}, + payload={"value": "new"}, ) - delta = await test_state.process(event) + update = await test_state.process(event) assert grandchild_state.value2 == "new" - assert delta == {"test_state.child_state.grandchild_state": {"value2": "new"}} + assert update.delta == { + "test_state.child_state.grandchild_state": {"value2": "new"}, + "test_state": {"sum": 3.14, "upper": ""}, + } @pytest.mark.asyncio @@ -699,7 +706,7 @@ async def test_process_event_substate_set_parent_state(TestState, ChildState): ChildState: The child state class. """ test_state = TestState() - event = Event(token="t", name="child_state.set_num1", payload={"num1": 69}) - delta = await test_state.process(event) + event = Event(token="t", name="child_state.set_num1", payload={"value": 69}) + update = await test_state.process(event) assert test_state.num1 == 69 - assert delta == {"test_state": {"num1": 69, "sum": 72.14, "upper": ""}} + assert update.delta == {"test_state": {"num1": 69, "sum": 72.14, "upper": ""}} diff --git a/tests/test_utils.py b/tests/test_utils.py index dc8e7d152..3853080f7 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -11,6 +11,8 @@ from pynecone import utils ("Hello", "hello"), ("camelCase", "camel_case"), ("camelTwoHumps", "camel_two_humps"), + ("_start_with_underscore", "_start_with_underscore"), + ("__start_with_double_underscore", "__start_with_double_underscore"), ], ) def test_to_snake_case(input: str, output: str): @@ -169,3 +171,13 @@ def test_format_cond(condition: str, true_value: str, false_value: str, expected expected: The expected output string. """ assert utils.format_cond(condition, true_value, false_value) == expected + + +def test_merge_imports(): + """Test that imports are merged correctly.""" + d1 = {"react": {"Component"}} + d2 = {"react": {"Component"}, "react-dom": {"render"}} + d = utils.merge_imports(d1, d2) + assert set(d.keys()) == {"react", "react-dom"} + assert set(d["react"]) == {"Component"} + assert set(d["react-dom"]) == {"render"} diff --git a/tests/test_var.py b/tests/test_var.py new file mode 100644 index 000000000..49eba20e6 --- /dev/null +++ b/tests/test_var.py @@ -0,0 +1,169 @@ +import pytest + +from pynecone.base import Base +from pynecone.var import BaseVar, Var + +test_vars = [ + BaseVar(name="prop1", type_=int), + BaseVar(name="key", type_=str), + BaseVar(name="value", type_=str, state="state"), + BaseVar(name="local", type_=str, state="state", is_local=True), + BaseVar(name="local2", type_=str, is_local=True), +] + + +@pytest.fixture +def TestObj(): + class TestObj(Base): + foo: int + bar: str + + return TestObj + + +@pytest.mark.parametrize( + "prop,expected", + zip( + test_vars, + [ + "prop1", + "key", + "state.value", + "state.local", + "local2", + ], + ), +) +def test_full_name(prop, expected): + """Test that the full name of a var is correct. + + Args: + prop: The var to test. + expected: The expected full name. + """ + assert prop.full_name == expected + + +@pytest.mark.parametrize( + "prop,expected", + zip( + test_vars, + ["{prop1}", "{key}", "{state.value}", "state.local", "local2"], + ), +) +def test_str(prop, expected): + """Test that the string representation of a var is correct. + + Args: + prop: The var to test. + expected: The expected string representation. + """ + assert str(prop) == expected + + +@pytest.mark.parametrize( + "prop,expected", + [ + (BaseVar(name="p", type_=int), 0), + (BaseVar(name="p", type_=float), 0.0), + (BaseVar(name="p", type_=str), ""), + (BaseVar(name="p", type_=bool), False), + (BaseVar(name="p", type_=list), []), + (BaseVar(name="p", type_=dict), {}), + (BaseVar(name="p", type_=tuple), ()), + (BaseVar(name="p", type_=set), set()), + ], +) +def test_default_value(prop, expected): + """Test that the default value of a var is correct. + + Args: + prop: The var to test. + expected: The expected default value. + """ + assert prop.get_default_value() == expected + + +@pytest.mark.parametrize( + "prop,expected", + zip( + test_vars, + [ + "set_prop1", + "set_key", + "state.set_value", + "state.set_local", + "set_local2", + ], + ), +) +def test_get_setter(prop, expected): + """Test that the name of the setter function of a var is correct. + + Args: + prop: The var to test. + expected: The expected name of the setter function. + """ + assert prop.get_setter_name() == expected + + +@pytest.mark.parametrize( + "value,expected", + [ + (None, None), + (1, BaseVar(name="1", type_=int, is_local=True)), + ("key", BaseVar(name="key", type_=str, is_local=True)), + (3.14, BaseVar(name="3.14", type_=float, is_local=True)), + ([1, 2, 3], BaseVar(name="[1, 2, 3]", type_=list, is_local=True)), + ( + {"a": 1, "b": 2}, + BaseVar(name='{"a": 1, "b": 2}', type_=dict, is_local=True), + ), + ], +) +def test_create(value, expected): + """Test the var create function. + + Args: + value: The value to create a var from. + expected: The expected name of the setter function. + """ + prop = Var.create(value) + if value is None: + assert prop == expected + else: + assert prop.equals(expected) # type: ignore + + +def test_basic_operations(TestObj): + """Test the var operations. + + Args: + TestObj: The test object. + """ + + def v(value) -> Var: + val = Var.create(value) + assert val is not None + return val + + assert str(v(1) == v(2)) == "{(1 == 2)}" + assert str(v(1) != v(2)) == "{(1 != 2)}" + assert str(v(1) < v(2)) == "{(1 < 2)}" + assert str(v(1) <= v(2)) == "{(1 <= 2)}" + assert str(v(1) > v(2)) == "{(1 > 2)}" + assert str(v(1) >= v(2)) == "{(1 >= 2)}" + assert str(v(1) + v(2)) == "{(1 + 2)}" + assert str(v(1) - v(2)) == "{(1 - 2)}" + assert str(v(1) * v(2)) == "{(1 * 2)}" + assert str(v(1) / v(2)) == "{(1 / 2)}" + assert str(v(1) // v(2)) == "{Math.floor(1 / 2)}" + assert str(v(1) % v(2)) == "{(1 % 2)}" + assert str(v(1) ** v(2)) == "{Math.pow(1 , 2)}" + assert str(v(1) & v(2)) == "{(1 && 2)}" + assert str(v(1) | v(2)) == "{(1 || 2)}" + assert str(v([1, 2, 3])[v(0)]) == "{[1, 2, 3][0]}" + assert str(v({"a": 1, "b": 2})["a"]) == '{{"a": 1, "b": 2}["a"]}' + assert ( + str(BaseVar(name="foo", state="state", type_=TestObj).bar) == "{state.foo.bar}" + )