diff --git a/.circleci/config.yml b/.circleci/config.yml index 867af1f79..b5624d095 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -32,7 +32,8 @@ jobs: . venv/bin/activate pip install -r dev-requirements.txt --quiet git clone --depth 1 git@github.com:plotly/dash.git dash-main - pip install -e ./dash-main[dev] --quiet + pip install -e ./dash-main[dev,testing] --quiet + cd dash-main/dash-renderer && npm run build && pip install -e . && cd ./../.. - run: name: Build diff --git a/.flake8 b/.flake8 new file mode 100644 index 000000000..f994d595b --- /dev/null +++ b/.flake8 @@ -0,0 +1,5 @@ +[flake8] +ignore = E203, E266, E501, E731, W503 +max-line-length = 88 +max-complexity = 18 +select = B,C,E,F,W,T4 \ No newline at end of file diff --git a/dash_table/Format.py b/dash_table/Format.py index 4ae06ace3..8dd12638c 100644 --- a/dash_table/Format.py +++ b/dash_table/Format.py @@ -5,111 +5,97 @@ def get_named_tuple(name, dict): return collections.namedtuple(name, dict.keys())(*dict.values()) -Align = get_named_tuple('align', { - 'default': '', - 'left': '<', - 'right': '>', - 'center': '^', - "right_sign": '=' -}) - -Group = get_named_tuple('group', { - 'no': '', - 'yes': ',' -}) - -Padding = get_named_tuple('padding', { - 'no': '', - 'yes': '0' -}) - -Prefix = get_named_tuple('prefix', { - 'yocto': 10 ** -24, - 'zepto': 10 ** -21, - 'atto': 10 ** -18, - 'femto': 10 ** -15, - 'pico': 10 ** -12, - 'nano': 10 ** -9, - 'micro': 10 ** -6, - 'milli': 10 ** -3, - 'none': None, - 'kilo': 10 ** 3, - 'mega': 10 ** 6, - 'giga': 10 ** 9, - 'tera': 10 ** 12, - 'peta': 10 ** 15, - 'exa': 10 ** 18, - 'zetta': 10 ** 21, - 'yotta': 10 ** 24 -}) - -Scheme = get_named_tuple('scheme', { - 'default': '', - 'decimal': 'r', - 'decimal_integer': 'd', - 'decimal_or_exponent': 'g', - 'decimal_si_prefix': 's', - 'exponent': 'e', - 'fixed': 'f', - 'percentage': '%', - 'percentage_rounded': 'p', - 'binary': 'b', - 'octal': 'o', - 'lower_case_hex': 'x', - 'upper_case_hex': 'X', - 'unicode': 'c' -}) - -Sign = get_named_tuple('sign', { - 'default': '', - 'negative': '-', - 'positive': '+', - 'parantheses': '(', - 'space': ' ' -}) - -Symbol = get_named_tuple('symbol', { - 'no': '', - 'yes': '$', - 'binary': '#b', - 'octal': '#o', - 'hex': '#x' -}) - -Trim = get_named_tuple('trim', { - 'no': '', - 'yes': '~' -}) - - -class Format(): +Align = get_named_tuple( + "align", + {"default": "", "left": "<", "right": ">", "center": "^", "right_sign": "="}, +) + +Group = get_named_tuple("group", {"no": "", "yes": ","}) + +Padding = get_named_tuple("padding", {"no": "", "yes": "0"}) + +Prefix = get_named_tuple( + "prefix", + { + "yocto": 10 ** -24, + "zepto": 10 ** -21, + "atto": 10 ** -18, + "femto": 10 ** -15, + "pico": 10 ** -12, + "nano": 10 ** -9, + "micro": 10 ** -6, + "milli": 10 ** -3, + "none": None, + "kilo": 10 ** 3, + "mega": 10 ** 6, + "giga": 10 ** 9, + "tera": 10 ** 12, + "peta": 10 ** 15, + "exa": 10 ** 18, + "zetta": 10 ** 21, + "yotta": 10 ** 24, + }, +) + +Scheme = get_named_tuple( + "scheme", + { + "default": "", + "decimal": "r", + "decimal_integer": "d", + "decimal_or_exponent": "g", + "decimal_si_prefix": "s", + "exponent": "e", + "fixed": "f", + "percentage": "%", + "percentage_rounded": "p", + "binary": "b", + "octal": "o", + "lower_case_hex": "x", + "upper_case_hex": "X", + "unicode": "c", + }, +) + +Sign = get_named_tuple( + "sign", + {"default": "", "negative": "-", "positive": "+", "parantheses": "(", "space": " "}, +) + +Symbol = get_named_tuple( + "symbol", {"no": "", "yes": "$", "binary": "#b", "octal": "#o", "hex": "#x"} +) + +Trim = get_named_tuple("trim", {"no": "", "yes": "~"}) + + +class Format: def __init__(self, **kwargs): self._locale = {} - self._nully = '' + self._nully = "" self._prefix = Prefix.none self._specifier = { - 'align': Align.default, - 'fill': '', - 'group': Group.no, - 'width': '', - 'padding': Padding.no, - 'precision': '', - 'sign': Sign.default, - 'symbol': Symbol.no, - 'trim': Trim.no, - 'type': Scheme.default + "align": Align.default, + "fill": "", + "group": Group.no, + "width": "", + "padding": Padding.no, + "precision": "", + "sign": Sign.default, + "symbol": Symbol.no, + "trim": Trim.no, + "type": Scheme.default, } valid_methods = [ - m for m in dir(self.__class__) - if m[0] != '_' and m != 'to_plotly_json' + m for m in dir(self.__class__) if m[0] != "_" and m != "to_plotly_json" ] for kw, val in kwargs.items(): if kw not in valid_methods: raise TypeError( - '{0} is not a format method. Expected one of'.format(kw), - str(list(valid_methods)) + "{0} is not a format method. Expected one of".format(kw), + str(list(valid_methods)), ) getattr(self, kw)(val) @@ -118,38 +104,37 @@ def _validate_char(self, value): self._validate_string(value) if len(value) != 1: - raise ValueError('expected value to a string of length one') + raise ValueError("expected value to a string of length one") def _validate_non_negative_integer_or_none(self, value): if value is None: return if not isinstance(value, int): - raise TypeError('expected value to be an integer') + raise TypeError("expected value to be an integer") if value < 0: - raise ValueError('expected value to be non-negative', str(value)) + raise ValueError("expected value to be non-negative", str(value)) def _validate_named(self, value, named_values): if value not in named_values: - raise TypeError('expected value to be one of', - str(list(named_values))) + raise TypeError("expected value to be one of", str(list(named_values))) def _validate_string(self, value): - if not isinstance(value, (str, u''.__class__)): - raise TypeError('expected value to be a string') + if not isinstance(value, (str, u"".__class__)): + raise TypeError("expected value to be a string") # Specifier def align(self, value): self._validate_named(value, Align) - self._specifier['align'] = value + self._specifier["align"] = value return self def fill(self, value): self._validate_char(value) - self._specifier['fill'] = value + self._specifier["fill"] = value return self def group(self, value): @@ -158,7 +143,7 @@ def group(self, value): self._validate_named(value, Group) - self._specifier['group'] = value + self._specifier["group"] = value return self def padding(self, value): @@ -167,39 +152,37 @@ def padding(self, value): self._validate_named(value, Padding) - self._specifier['padding'] = value + self._specifier["padding"] = value return self def padding_width(self, value): self._validate_non_negative_integer_or_none(value) - self._specifier['width'] = value if value is not None else '' + self._specifier["width"] = value if value is not None else "" return self def precision(self, value): self._validate_non_negative_integer_or_none(value) - self._specifier['precision'] = ( - '.{0}'.format(value) if value is not None else '' - ) + self._specifier["precision"] = ".{0}".format(value) if value is not None else "" return self def scheme(self, value): self._validate_named(value, Scheme) - self._specifier['type'] = value + self._specifier["type"] = value return self def sign(self, value): self._validate_named(value, Sign) - self._specifier['sign'] = value + self._specifier["sign"] = value return self def symbol(self, value): self._validate_named(value, Symbol) - self._specifier['symbol'] = value + self._specifier["symbol"] = value return self def trim(self, value): @@ -208,66 +191,66 @@ def trim(self, value): self._validate_named(value, Trim) - self._specifier['trim'] = value + self._specifier["trim"] = value return self # Locale def symbol_prefix(self, value): self._validate_string(value) - if 'symbol' not in self._locale: - self._locale['symbol'] = [value, ''] + if "symbol" not in self._locale: + self._locale["symbol"] = [value, ""] else: - self._locale['symbol'][0] = value + self._locale["symbol"][0] = value return self def symbol_suffix(self, value): self._validate_string(value) - if 'symbol' not in self._locale: - self._locale['symbol'] = ['', value] + if "symbol" not in self._locale: + self._locale["symbol"] = ["", value] else: - self._locale['symbol'][1] = value + self._locale["symbol"][1] = value return self def decimal_delimiter(self, value): self._validate_char(value) - self._locale['decimal'] = value + self._locale["decimal"] = value return self def group_delimiter(self, value): self._validate_char(value) - self._locale['group'] = value + self._locale["group"] = value return self def groups(self, groups): groups = ( - groups if isinstance(groups, list) else - [groups] if isinstance(groups, int) else None + groups + if isinstance(groups, list) + else [groups] + if isinstance(groups, int) + else None ) if not isinstance(groups, list): - raise TypeError( - 'expected groups to be an integer or a list of integers' - ) + raise TypeError("expected groups to be an integer or a list of integers") if len(groups) == 0: raise ValueError( - 'expected groups to be an integer or a list of ' - 'one or more integers' + "expected groups to be an integer or a list of " "one or more integers" ) for group in groups: if not isinstance(group, int): - raise TypeError('expected entry to be an integer') + raise TypeError("expected entry to be an integer") if group <= 0: - raise ValueError('expected entry to be a non-negative integer') + raise ValueError("expected entry to be a non-negative integer") - self._locale['grouping'] = groups + self._locale["grouping"] = groups return self # Nully @@ -284,21 +267,21 @@ def si_prefix(self, value): def to_plotly_json(self): f = {} - f['locale'] = self._locale.copy() - f['nully'] = self._nully - f['prefix'] = self._prefix - aligned = self._specifier['align'] != Align.default - f['specifier'] = '{}{}{}{}{}{}{}{}{}{}'.format( - self._specifier['fill'] if aligned else '', - self._specifier['align'], - self._specifier['sign'], - self._specifier['symbol'], - self._specifier['padding'], - self._specifier['width'], - self._specifier['group'], - self._specifier['precision'], - self._specifier['trim'], - self._specifier['type'] + f["locale"] = self._locale.copy() + f["nully"] = self._nully + f["prefix"] = self._prefix + aligned = self._specifier["align"] != Align.default + f["specifier"] = "{}{}{}{}{}{}{}{}{}{}".format( + self._specifier["fill"] if aligned else "", + self._specifier["align"], + self._specifier["sign"], + self._specifier["symbol"], + self._specifier["padding"], + self._specifier["width"], + self._specifier["group"], + self._specifier["precision"], + self._specifier["trim"], + self._specifier["type"], ) return f diff --git a/dash_table/FormatTemplate.py b/dash_table/FormatTemplate.py index 9991437c7..9c2688ca8 100644 --- a/dash_table/FormatTemplate.py +++ b/dash_table/FormatTemplate.py @@ -7,16 +7,13 @@ def money(decimals, sign=Sign.default): precision=decimals, scheme=Scheme.fixed, sign=sign, - symbol=Symbol.yes + symbol=Symbol.yes, ) def percentage(decimals, rounded=False): if not isinstance(rounded, bool): - raise TypeError('expected rounded to be a boolean') + raise TypeError("expected rounded to be a boolean") rounded = Scheme.percentage_rounded if rounded else Scheme.percentage - return Format( - scheme=rounded, - precision=decimals - ) + return Format(scheme=rounded, precision=decimals) diff --git a/dash_table_base/Format.py b/dash_table_base/Format.py index 4ae06ace3..8dd12638c 100644 --- a/dash_table_base/Format.py +++ b/dash_table_base/Format.py @@ -5,111 +5,97 @@ def get_named_tuple(name, dict): return collections.namedtuple(name, dict.keys())(*dict.values()) -Align = get_named_tuple('align', { - 'default': '', - 'left': '<', - 'right': '>', - 'center': '^', - "right_sign": '=' -}) - -Group = get_named_tuple('group', { - 'no': '', - 'yes': ',' -}) - -Padding = get_named_tuple('padding', { - 'no': '', - 'yes': '0' -}) - -Prefix = get_named_tuple('prefix', { - 'yocto': 10 ** -24, - 'zepto': 10 ** -21, - 'atto': 10 ** -18, - 'femto': 10 ** -15, - 'pico': 10 ** -12, - 'nano': 10 ** -9, - 'micro': 10 ** -6, - 'milli': 10 ** -3, - 'none': None, - 'kilo': 10 ** 3, - 'mega': 10 ** 6, - 'giga': 10 ** 9, - 'tera': 10 ** 12, - 'peta': 10 ** 15, - 'exa': 10 ** 18, - 'zetta': 10 ** 21, - 'yotta': 10 ** 24 -}) - -Scheme = get_named_tuple('scheme', { - 'default': '', - 'decimal': 'r', - 'decimal_integer': 'd', - 'decimal_or_exponent': 'g', - 'decimal_si_prefix': 's', - 'exponent': 'e', - 'fixed': 'f', - 'percentage': '%', - 'percentage_rounded': 'p', - 'binary': 'b', - 'octal': 'o', - 'lower_case_hex': 'x', - 'upper_case_hex': 'X', - 'unicode': 'c' -}) - -Sign = get_named_tuple('sign', { - 'default': '', - 'negative': '-', - 'positive': '+', - 'parantheses': '(', - 'space': ' ' -}) - -Symbol = get_named_tuple('symbol', { - 'no': '', - 'yes': '$', - 'binary': '#b', - 'octal': '#o', - 'hex': '#x' -}) - -Trim = get_named_tuple('trim', { - 'no': '', - 'yes': '~' -}) - - -class Format(): +Align = get_named_tuple( + "align", + {"default": "", "left": "<", "right": ">", "center": "^", "right_sign": "="}, +) + +Group = get_named_tuple("group", {"no": "", "yes": ","}) + +Padding = get_named_tuple("padding", {"no": "", "yes": "0"}) + +Prefix = get_named_tuple( + "prefix", + { + "yocto": 10 ** -24, + "zepto": 10 ** -21, + "atto": 10 ** -18, + "femto": 10 ** -15, + "pico": 10 ** -12, + "nano": 10 ** -9, + "micro": 10 ** -6, + "milli": 10 ** -3, + "none": None, + "kilo": 10 ** 3, + "mega": 10 ** 6, + "giga": 10 ** 9, + "tera": 10 ** 12, + "peta": 10 ** 15, + "exa": 10 ** 18, + "zetta": 10 ** 21, + "yotta": 10 ** 24, + }, +) + +Scheme = get_named_tuple( + "scheme", + { + "default": "", + "decimal": "r", + "decimal_integer": "d", + "decimal_or_exponent": "g", + "decimal_si_prefix": "s", + "exponent": "e", + "fixed": "f", + "percentage": "%", + "percentage_rounded": "p", + "binary": "b", + "octal": "o", + "lower_case_hex": "x", + "upper_case_hex": "X", + "unicode": "c", + }, +) + +Sign = get_named_tuple( + "sign", + {"default": "", "negative": "-", "positive": "+", "parantheses": "(", "space": " "}, +) + +Symbol = get_named_tuple( + "symbol", {"no": "", "yes": "$", "binary": "#b", "octal": "#o", "hex": "#x"} +) + +Trim = get_named_tuple("trim", {"no": "", "yes": "~"}) + + +class Format: def __init__(self, **kwargs): self._locale = {} - self._nully = '' + self._nully = "" self._prefix = Prefix.none self._specifier = { - 'align': Align.default, - 'fill': '', - 'group': Group.no, - 'width': '', - 'padding': Padding.no, - 'precision': '', - 'sign': Sign.default, - 'symbol': Symbol.no, - 'trim': Trim.no, - 'type': Scheme.default + "align": Align.default, + "fill": "", + "group": Group.no, + "width": "", + "padding": Padding.no, + "precision": "", + "sign": Sign.default, + "symbol": Symbol.no, + "trim": Trim.no, + "type": Scheme.default, } valid_methods = [ - m for m in dir(self.__class__) - if m[0] != '_' and m != 'to_plotly_json' + m for m in dir(self.__class__) if m[0] != "_" and m != "to_plotly_json" ] for kw, val in kwargs.items(): if kw not in valid_methods: raise TypeError( - '{0} is not a format method. Expected one of'.format(kw), - str(list(valid_methods)) + "{0} is not a format method. Expected one of".format(kw), + str(list(valid_methods)), ) getattr(self, kw)(val) @@ -118,38 +104,37 @@ def _validate_char(self, value): self._validate_string(value) if len(value) != 1: - raise ValueError('expected value to a string of length one') + raise ValueError("expected value to a string of length one") def _validate_non_negative_integer_or_none(self, value): if value is None: return if not isinstance(value, int): - raise TypeError('expected value to be an integer') + raise TypeError("expected value to be an integer") if value < 0: - raise ValueError('expected value to be non-negative', str(value)) + raise ValueError("expected value to be non-negative", str(value)) def _validate_named(self, value, named_values): if value not in named_values: - raise TypeError('expected value to be one of', - str(list(named_values))) + raise TypeError("expected value to be one of", str(list(named_values))) def _validate_string(self, value): - if not isinstance(value, (str, u''.__class__)): - raise TypeError('expected value to be a string') + if not isinstance(value, (str, u"".__class__)): + raise TypeError("expected value to be a string") # Specifier def align(self, value): self._validate_named(value, Align) - self._specifier['align'] = value + self._specifier["align"] = value return self def fill(self, value): self._validate_char(value) - self._specifier['fill'] = value + self._specifier["fill"] = value return self def group(self, value): @@ -158,7 +143,7 @@ def group(self, value): self._validate_named(value, Group) - self._specifier['group'] = value + self._specifier["group"] = value return self def padding(self, value): @@ -167,39 +152,37 @@ def padding(self, value): self._validate_named(value, Padding) - self._specifier['padding'] = value + self._specifier["padding"] = value return self def padding_width(self, value): self._validate_non_negative_integer_or_none(value) - self._specifier['width'] = value if value is not None else '' + self._specifier["width"] = value if value is not None else "" return self def precision(self, value): self._validate_non_negative_integer_or_none(value) - self._specifier['precision'] = ( - '.{0}'.format(value) if value is not None else '' - ) + self._specifier["precision"] = ".{0}".format(value) if value is not None else "" return self def scheme(self, value): self._validate_named(value, Scheme) - self._specifier['type'] = value + self._specifier["type"] = value return self def sign(self, value): self._validate_named(value, Sign) - self._specifier['sign'] = value + self._specifier["sign"] = value return self def symbol(self, value): self._validate_named(value, Symbol) - self._specifier['symbol'] = value + self._specifier["symbol"] = value return self def trim(self, value): @@ -208,66 +191,66 @@ def trim(self, value): self._validate_named(value, Trim) - self._specifier['trim'] = value + self._specifier["trim"] = value return self # Locale def symbol_prefix(self, value): self._validate_string(value) - if 'symbol' not in self._locale: - self._locale['symbol'] = [value, ''] + if "symbol" not in self._locale: + self._locale["symbol"] = [value, ""] else: - self._locale['symbol'][0] = value + self._locale["symbol"][0] = value return self def symbol_suffix(self, value): self._validate_string(value) - if 'symbol' not in self._locale: - self._locale['symbol'] = ['', value] + if "symbol" not in self._locale: + self._locale["symbol"] = ["", value] else: - self._locale['symbol'][1] = value + self._locale["symbol"][1] = value return self def decimal_delimiter(self, value): self._validate_char(value) - self._locale['decimal'] = value + self._locale["decimal"] = value return self def group_delimiter(self, value): self._validate_char(value) - self._locale['group'] = value + self._locale["group"] = value return self def groups(self, groups): groups = ( - groups if isinstance(groups, list) else - [groups] if isinstance(groups, int) else None + groups + if isinstance(groups, list) + else [groups] + if isinstance(groups, int) + else None ) if not isinstance(groups, list): - raise TypeError( - 'expected groups to be an integer or a list of integers' - ) + raise TypeError("expected groups to be an integer or a list of integers") if len(groups) == 0: raise ValueError( - 'expected groups to be an integer or a list of ' - 'one or more integers' + "expected groups to be an integer or a list of " "one or more integers" ) for group in groups: if not isinstance(group, int): - raise TypeError('expected entry to be an integer') + raise TypeError("expected entry to be an integer") if group <= 0: - raise ValueError('expected entry to be a non-negative integer') + raise ValueError("expected entry to be a non-negative integer") - self._locale['grouping'] = groups + self._locale["grouping"] = groups return self # Nully @@ -284,21 +267,21 @@ def si_prefix(self, value): def to_plotly_json(self): f = {} - f['locale'] = self._locale.copy() - f['nully'] = self._nully - f['prefix'] = self._prefix - aligned = self._specifier['align'] != Align.default - f['specifier'] = '{}{}{}{}{}{}{}{}{}{}'.format( - self._specifier['fill'] if aligned else '', - self._specifier['align'], - self._specifier['sign'], - self._specifier['symbol'], - self._specifier['padding'], - self._specifier['width'], - self._specifier['group'], - self._specifier['precision'], - self._specifier['trim'], - self._specifier['type'] + f["locale"] = self._locale.copy() + f["nully"] = self._nully + f["prefix"] = self._prefix + aligned = self._specifier["align"] != Align.default + f["specifier"] = "{}{}{}{}{}{}{}{}{}{}".format( + self._specifier["fill"] if aligned else "", + self._specifier["align"], + self._specifier["sign"], + self._specifier["symbol"], + self._specifier["padding"], + self._specifier["width"], + self._specifier["group"], + self._specifier["precision"], + self._specifier["trim"], + self._specifier["type"], ) return f diff --git a/dash_table_base/FormatTemplate.py b/dash_table_base/FormatTemplate.py index 9991437c7..9c2688ca8 100644 --- a/dash_table_base/FormatTemplate.py +++ b/dash_table_base/FormatTemplate.py @@ -7,16 +7,13 @@ def money(decimals, sign=Sign.default): precision=decimals, scheme=Scheme.fixed, sign=sign, - symbol=Symbol.yes + symbol=Symbol.yes, ) def percentage(decimals, rounded=False): if not isinstance(rounded, bool): - raise TypeError('expected rounded to be a boolean') + raise TypeError("expected rounded to be a boolean") rounded = Scheme.percentage_rounded if rounded else Scheme.percentage - return Format( - scheme=rounded, - precision=decimals - ) + return Format(scheme=rounded, precision=decimals) diff --git a/dash_table_base/__init__.py b/dash_table_base/__init__.py index 562a31187..b4d71488c 100644 --- a/dash_table_base/__init__.py +++ b/dash_table_base/__init__.py @@ -10,73 +10,82 @@ from ._imports_ import * from ._imports_ import __all__ -if not hasattr(_dash, 'development'): - print('Dash was not successfully imported. ' - 'Make sure you don\'t have a file ' - 'named \n"dash.py" in your current directory.', file=_sys.stderr) +if not hasattr(_dash, "development"): + print( + "Dash was not successfully imported. " + "Make sure you don't have a file " + 'named \n"dash.py" in your current directory.', + file=_sys.stderr, + ) _sys.exit(1) _basepath = _os.path.dirname(__file__) -_filepath = _os.path.abspath(_os.path.join(_basepath, 'package-info.json')) +_filepath = _os.path.abspath(_os.path.join(_basepath, "package-info.json")) with open(_filepath) as f: package = json.load(f) -package_name = package['name'].replace(' ', '_').replace('-', '_') -__version__ = package['version'] +package_name = package["name"].replace(" ", "_").replace("-", "_") +__version__ = package["version"] _current_path = _os.path.dirname(_os.path.abspath(__file__)) _this_module = _sys.modules[__name__] -async_resources = [ - 'export', - 'table', - 'highlight' -] +async_resources = ["export", "table", "highlight"] _js_dist = [] -_js_dist.extend([{ - 'relative_package_path': 'async-{}.js'.format(async_resource), - 'external_url': ( - 'https://unpkg.com/dash-table@{}' - '/dash_table/async-{}.js' - ).format(__version__, async_resource), - 'namespace': package_name, - 'async': True -} for async_resource in async_resources]) - -_js_dist.extend([{ - 'relative_package_path': 'async-{}.js.map'.format(async_resource), - 'external_url': ( - 'https://unpkg.com/dash-table@{}' - '/dash_table/async-{}.js.map' - ).format(__version__, async_resource), - 'namespace': package_name, - 'dynamic': True -} for async_resource in async_resources]) - -_js_dist.extend([ - { - 'relative_package_path': 'bundle.js', - 'external_url': ( - 'https://unpkg.com/dash-table@{}/dash_table/bundle.js' - ).format(__version__), - 'namespace': package_name - }, - { - 'relative_package_path': 'bundle.js.map', - 'external_url': ( - 'https://unpkg.com/dash-table@{}/dash_table/bundle.js.map' - ).format(__version__), - 'namespace': package_name, - 'dynamic': True - } -]) +_js_dist.extend( + [ + { + "relative_package_path": "async-{}.js".format(async_resource), + "external_url": ( + "https://unpkg.com/dash-table@{}" "/dash_table/async-{}.js" + ).format(__version__, async_resource), + "namespace": package_name, + "async": True, + } + for async_resource in async_resources + ] +) + +_js_dist.extend( + [ + { + "relative_package_path": "async-{}.js.map".format(async_resource), + "external_url": ( + "https://unpkg.com/dash-table@{}" "/dash_table/async-{}.js.map" + ).format(__version__, async_resource), + "namespace": package_name, + "dynamic": True, + } + for async_resource in async_resources + ] +) + +_js_dist.extend( + [ + { + "relative_package_path": "bundle.js", + "external_url": ( + "https://unpkg.com/dash-table@{}/dash_table/bundle.js" + ).format(__version__), + "namespace": package_name, + }, + { + "relative_package_path": "bundle.js.map", + "external_url": ( + "https://unpkg.com/dash-table@{}/dash_table/bundle.js.map" + ).format(__version__), + "namespace": package_name, + "dynamic": True, + }, + ] +) _css_dist = [] for _component in __all__: - setattr(locals()[_component], '_js_dist', _js_dist) - setattr(locals()[_component], '_css_dist', _css_dist) + setattr(locals()[_component], "_js_dist", _js_dist) + setattr(locals()[_component], "_css_dist", _css_dist) diff --git a/dev-requirements.txt b/dev-requirements.txt index 260b56bf8..3b2cba63d 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,3 +1,5 @@ +black flake8 pandas +preconditions xlrd \ No newline at end of file diff --git a/index.py b/index.py index 552e10389..392dc8e62 100644 --- a/index.py +++ b/index.py @@ -19,7 +19,8 @@ apps = { filename.replace(".py", "").replace("app_", ""): getattr( getattr( - __import__(".".join(["tests", "integration", filename.replace(".py", "")])), "integration" + __import__(".".join(["tests", "integration", filename.replace(".py", "")])), + "integration", ), filename.replace(".py", ""), ) diff --git a/package.json b/package.json index 0634a9fd7..70bd6ab90 100644 --- a/package.json +++ b/package.json @@ -22,35 +22,20 @@ "private::build:js-test-standalone": "run-s \"private::build -- --mode development --config webpack.test.standalone.config.js\"", "private::build:js-test-watch": "run-s \"private::build -- --mode development --config webpack.test.config.js --watch\"", "private::build:py": "dash-generate-components src/dash-table/dash/DataTable.js dash_table -p package-info.json && cp dash_table_base/** dash_table/ && dash-generate-components src/dash-table/dash/DataTable.js dash_table -p package-info.json --r-prefix 'dash'", - "private::host_dash8081": "python tests/cypress/dash/v_be_page.py", - "private::host_dash8082": "python tests/cypress/dash/v_copy_paste.py", - "private::host_dash8083": "python tests/cypress/dash/v_fe_page.py", - "private::host_dash8084": "python tests/cypress/dash/v_data_loading.py", - "private::host_dash8085": "python tests/cypress/dash/v_default.py", - "private::host_dash8086": "python tests/cypress/dash/v_pagination.py", - "private::host_dash8087": "python tests/cypress/dash/v_markdown.py", "private::host_js": "http-server ./dash_table -c-1 --silent", "private::lint:ts": "tslint --project tsconfig.json --config tslint.json", - "private::lint:py": "flake8 --exclude=DataTable.py,__init__.py,_imports_.py dash_table", - "private::wait_dash8081": "wait-on http://localhost:8081", - "private::wait_dash8082": "wait-on http://localhost:8082", - "private::wait_dash8083": "wait-on http://localhost:8083", - "private::wait_dash8084": "wait-on http://localhost:8084", - "private::wait_dash8085": "wait-on http://localhost:8085", - "private::wait_dash8086": "wait-on http://localhost:8086", - "private::wait_dash8087": "wait-on http://localhost:8087", + "private::lint:py": "flake8 --exclude=DataTable.py,__init__.py,_imports_.py dash_table && black --check .", "private::wait_js": "wait-on http://localhost:8080", "private::opentests": "cypress open", "private::test.python": "python -m unittest tests/unit/format_test.py", "private::test.unit": "cypress run --browser chrome --spec 'tests/cypress/tests/unit/**/*'", - "private::test.server": "cypress run --browser chrome --spec 'tests/cypress/tests/server/**/*'", "private::test.standalone": "cypress run --browser chrome --spec 'tests/cypress/tests/standalone/**/*'", "build.watch": "webpack-dev-server --content-base dash_table --mode development --config webpack.dev.config.js", "build": "run-s private::build:js private::build:py", "postbuild": "es-check es5 dash_table/*.js", - "format": "run-s \"private::lint:ts -- --fix\"", + "format": "run-s \"private::lint:ts -- --fix\" && black .", "lint": "run-s private::lint:*", - "test.server": "run-p --race private::host* private::test.server", + "test.server": "pytest tests/selenium", "test.standalone": "run-p --race private::host_js private::test.standalone", "test.unit": "run-s private::test.python private::test.unit", "test.visual": "build-storybook && percy-storybook", diff --git a/setup.py b/setup.py index 28cfc703f..c85e3db81 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup import json -with open('package.json') as f: +with open("package.json") as f: package = json.load(f) package_name = str(package["name"].replace(" ", "_").replace("-", "_")) @@ -9,10 +9,10 @@ setup( name=package_name, version=package["version"], - author=package['author'], + author=package["author"], packages=[package_name], include_package_data=True, - license=package['license'], - description=package['description'] if 'description' in package else package_name, - install_requires=[] + license=package["license"], + description=package["description"] if "description" in package else package_name, + install_requires=[], ) diff --git a/tests/cypress/dash/v_be_page.py b/tests/cypress/dash/v_be_page.py deleted file mode 100644 index 20775f7f6..000000000 --- a/tests/cypress/dash/v_be_page.py +++ /dev/null @@ -1,105 +0,0 @@ -# pylint: disable=global-statement -import dash -from dash.dependencies import Input, Output -import dash_html_components as html -import os -import pandas as pd -import sys - -sys.path.append( - os.path.abspath( - os.path.join(os.path.dirname(sys.argv[0]), os.pardir, os.pardir, os.pardir) - ) -) -module_names = ["dash_table"] -modules = [__import__(module) for module in module_names] -dash_table = modules[0] - -url = "https://github.com/plotly/datasets/raw/master/" "26k-consumer-complaints.csv" -df = pd.read_csv(url) - -app = dash.Dash() -app.css.config.serve_locally = True -app.scripts.config.serve_locally = True - -app.layout = html.Div( - [ - html.Div(id="container", children="Hello World"), - dash_table.DataTable( - id="table", - data=[], - page_action="custom", - page_current=0, - page_size=250, - columns=[ - {"id": 0, "name": "Complaint ID"}, - {"id": 1, "name": "Product"}, - {"id": 2, "name": "Sub-product"}, - {"id": 3, "name": "Issue"}, - {"id": 4, "name": "Sub-issue"}, - {"id": 5, "name": "State"}, - {"id": 6, "name": "ZIP"}, - {"id": 7, "name": "code"}, - {"id": 8, "name": "Date received"}, - {"id": 9, "name": "Date sent to company"}, - {"id": 10, "name": "Company"}, - {"id": 11, "name": "Company response"}, - {"id": 12, "name": "Timely response?"}, - {"id": 13, "name": "Consumer disputed?"}, - ], - fixed_columns={ 'headers': True, 'data': -1 }, - fixed_rows={ 'headers': True, 'data': -1 }, - row_selectable=True, - row_deletable=True, - sort_action="custom", - filter_action='none', - editable=True, - ), - ] -) - - -@app.callback(Output("table", "data"), [ - Input("table", "page_current"), - Input("table", "page_size"), - Input("table", "sort_by") -]) -def updateData(current_page, page_size, sort_by): - start_index = current_page * page_size - end_index = start_index + page_size - print(str(start_index) + "," + str(end_index)) - print(sort_by) - - if (sort_by is None or len(sort_by) == 0): - sorted_df = df.values - else: - sorted_df = df.sort_index( - axis=sort_by[0]['column_id'], - ascending=(sort_by[0]['direction'] == 'asc') - ).values - - return sorted_df[start_index:end_index] - - -@app.callback( - Output("container", "children"), - [Input("table", "data"), Input("table", "data_previous")], -) -def findModifiedValue(data, previous): - modification = "None" - - if data is None or previous is None: - return modification - - for (y, row) in enumerate(data): - row_prev = previous[y] - - for (x, col) in enumerate(row): - if col != row_prev[x]: - modification = "[{}][{}] = {} -> {}".format(y, x, row_prev[x], col) - - return modification - - -if __name__ == "__main__": - app.run_server(port=8081, debug=False) diff --git a/tests/cypress/dash/v_copy_paste.py b/tests/cypress/dash/v_copy_paste.py deleted file mode 100644 index 4dc7e6650..000000000 --- a/tests/cypress/dash/v_copy_paste.py +++ /dev/null @@ -1,106 +0,0 @@ -# pylint: disable=global-statement -import dash -from dash.dependencies import Input, Output, State -from dash.exceptions import PreventUpdate -import dash_html_components as html -import os -import pandas as pd -import sys - -sys.path.append( - os.path.abspath( - os.path.join(os.path.dirname(sys.argv[0]), os.pardir, os.pardir, os.pardir) - ) -) -module_names = ["dash_table"] -modules = [__import__(x) for x in module_names] -dash_table = modules[0] - -url = "https://github.com/plotly/datasets/raw/master/" "26k-consumer-complaints.csv" -df = pd.read_csv(url) -df = df.values - -app = dash.Dash() -app.css.config.serve_locally = True -app.scripts.config.serve_locally = True - -app.layout = html.Div( - [ - html.Div(id="container", children="Hello World"), - dash_table.DataTable( - id="table", - data=df[0:250], - columns=[ - {"id": 0, "name": "Complaint ID", "hideable": True}, - {"id": 1, "name": "Product"}, - {"id": 2, "name": "Sub-product"}, - {"id": 3, "name": "Issue"}, - {"id": 4, "name": "Sub-issue"}, - {"id": 5, "name": "State"}, - {"id": 6, "name": "ZIP"}, - {"id": 7, "name": "code"}, - {"id": 8, "name": "Date received"}, - {"id": 9, "name": "Date sent to company"}, - {"id": 10, "name": "Company"}, - {"id": 11, "name": "Company response"}, - {"id": 12, "name": "Timely response?"}, - {"id": 13, "name": "Consumer disputed?"}, - ], - editable=True, - sort_action='native', - include_headers_on_copy_paste=True, - - ), - dash_table.DataTable( - id="table2", - data=df[0:10], - columns=[ - {"id": 0, "name": "Complaint ID", "hideable": True}, - {"id": 1, "name": "Product"}, - {"id": 2, "name": "Sub-product"}, - {"id": 3, "name": "Issue"}, - {"id": 4, "name": "Sub-issue"}, - {"id": 5, "name": "State"}, - {"id": 6, "name": "ZIP"}, - {"id": 7, "name": "code"}, - {"id": 8, "name": "Date received"}, - {"id": 9, "name": "Date sent to company"}, - {"id": 10, "name": "Company"}, - {"id": 11, "name": "Company response"}, - {"id": 12, "name": "Timely response?"}, - {"id": 13, "name": "Consumer disputed?"}, - ], - editable=True, - sort_action='native', - include_headers_on_copy_paste=True, - ), - ] -) - - -@app.callback( - Output("table", "data"), - [Input("table", "data_timestamp")], - [State("table", "data"), State("table", "data_previous")], -) -# pylint: disable=unused-argument -def updateData(timestamp, current, previous): - # pylint: enable=unused-argument - if timestamp is None or current is None or previous is None: - raise PreventUpdate - - modified = False - if len(current) == len(previous): - for (i, datum) in enumerate(current): - previous_datum = previous[i] - if datum[0] != previous_datum[0]: - modified = True - datum[1] = "MODIFIED" - - if not modified: - raise PreventUpdate - - return current - -if __name__ == "__main__": - app.run_server(port=8082, debug=False) diff --git a/tests/cypress/dash/v_data_loading.py b/tests/cypress/dash/v_data_loading.py deleted file mode 100644 index 68bccc066..000000000 --- a/tests/cypress/dash/v_data_loading.py +++ /dev/null @@ -1,88 +0,0 @@ -# pylint: disable=global-statement -import dash -from dash.dependencies import Input, Output, State -from dash.exceptions import PreventUpdate -import dash_html_components as html -import dash_core_components as dcc -import os -import pandas as pd -import sys -from time import sleep - -sys.path.append( - os.path.abspath( - os.path.join(os.path.dirname(sys.argv[0]), os.pardir, os.pardir, os.pardir) - ) -) -module_names = ["dash_table"] -modules = [__import__(x) for x in module_names] -dash_table = modules[0] - -url = "https://github.com/plotly/datasets/raw/master/" "26k-consumer-complaints.csv" -df = pd.read_csv(url) -df = df.values - -app = dash.Dash() -app.css.config.serve_locally = True -app.scripts.config.serve_locally = True - -app.layout = html.Div( - [ - dcc.Input(id='change-data-property'), - dcc.Input(id='change-other-property'), - - dash_table.DataTable( - id="table", - data=df[0:250], - columns=[ - {"id": 0, "name": "Complaint ID", "hideable": True}, - {"id": 1, "name": "Product"}, - {"id": 2, "name": "Sub-product"}, - {"id": 3, "name": "Issue"}, - {"id": 4, "name": "Sub-issue"}, - {"id": 5, "name": "State"}, - {"id": 6, "name": "ZIP"}, - {"id": 7, "name": "code"}, - {"id": 8, "name": "Date received"}, - {"id": 9, "name": "Date sent to company"}, - {"id": 10, "name": "Company"}, - {"id": 11, "name": "Company response"}, - {"id": 12, "name": "Timely response?"}, - {"id": 13, "name": "Consumer disputed?"}, - ], - editable=True, - sort_action='native', - include_headers_on_copy_paste=True, - - ) - ] -) - - -@app.callback( - Output("table", "style_cell_conditional"), - [Input("change-other-property", "value")] -) -def dontTriggerWait(to_change): - if to_change != 'dont_change_data': - raise PreventUpdate - - sleep(5) - return [] - - -@app.callback( - Output("table", "data"), - [Input("change-data-property", "value")] -) -# pylint: disable=unused-argument -def triggerWait(to_change): - if to_change != 'change_data': - raise PreventUpdate - - sleep(5) - return df[0:250] - - -if __name__ == "__main__": - app.run_server(port=8084, debug=False) diff --git a/tests/cypress/dash/v_default.py b/tests/cypress/dash/v_default.py deleted file mode 100644 index 994f92c56..000000000 --- a/tests/cypress/dash/v_default.py +++ /dev/null @@ -1,82 +0,0 @@ -# pylint: disable=global-statement -import dash -from dash.dependencies import Input, Output, State -from dash.exceptions import PreventUpdate -import dash_html_components as html -import os -import pandas as pd -import sys - -sys.path.append( - os.path.abspath( - os.path.join(os.path.dirname(sys.argv[0]), os.pardir, os.pardir, os.pardir) - ) -) -module_names = ["dash_table"] -modules = [__import__(x) for x in module_names] -dash_table = modules[0] - -url = "https://github.com/plotly/datasets/raw/master/" "26k-consumer-complaints.csv" -df = pd.read_csv(url) -df = df.values - -app = dash.Dash() -app.css.config.serve_locally = True -app.scripts.config.serve_locally = True - -app.layout = html.Div( - [ - html.Div(id="container", children="Hello World"), - dash_table.DataTable( - id="table", - data=df[0:250], - columns=[ - {"id": 0, "name": "Complaint ID", "hideable": True}, - {"id": 1, "name": "Product"}, - {"id": 2, "name": "Sub-product"}, - {"id": 3, "name": "Issue"}, - {"id": 4, "name": "Sub-issue"}, - {"id": 5, "name": "State"}, - {"id": 6, "name": "ZIP"}, - {"id": 7, "name": "code"}, - {"id": 8, "name": "Date received"}, - {"id": 9, "name": "Date sent to company"}, - {"id": 10, "name": "Company"}, - {"id": 11, "name": "Company response"}, - {"id": 12, "name": "Timely response?"}, - {"id": 13, "name": "Consumer disputed?"}, - ], - editable=True, - sort_action='native', - include_headers_on_copy_paste=True, - - ), - dash_table.DataTable( - id="table2", - data=df[0:10], - columns=[ - {"id": 0, "name": "Complaint ID", "hideable": True}, - {"id": 1, "name": "Product"}, - {"id": 2, "name": "Sub-product"}, - {"id": 3, "name": "Issue"}, - {"id": 4, "name": "Sub-issue"}, - {"id": 5, "name": "State"}, - {"id": 6, "name": "ZIP"}, - {"id": 7, "name": "code"}, - {"id": 8, "name": "Date received"}, - {"id": 9, "name": "Date sent to company"}, - {"id": 10, "name": "Company"}, - {"id": 11, "name": "Company response"}, - {"id": 12, "name": "Timely response?"}, - {"id": 13, "name": "Consumer disputed?"}, - ], - editable=True, - sort_action='native', - include_headers_on_copy_paste=True, - ), - ] -) - - -if __name__ == "__main__": - app.run_server(port=8085, debug=False) diff --git a/tests/cypress/dash/v_fe_page.py b/tests/cypress/dash/v_fe_page.py deleted file mode 100644 index 0f6b97cc0..000000000 --- a/tests/cypress/dash/v_fe_page.py +++ /dev/null @@ -1,95 +0,0 @@ -# pylint: disable=global-statement -import json -import os -import pandas as pd -import sys - -import dash -from dash.dependencies import Input, Output -import dash_html_components as html - -sys.path.append( - os.path.abspath( - os.path.join(os.path.dirname(sys.argv[0]), - os.pardir, os.pardir, os.pardir) - ) -) -module_names = ["dash_table"] -modules = [__import__(module) for module in module_names] -dash_table = modules[0] - -url = ("https://github.com/plotly/datasets/raw/master/" - "26k-consumer-complaints.csv") -df = pd.read_csv(url, nrows=1000) -# add IDs that don't match but are easily derivable from row #s -data = [ - {k: v for k, v in list(enumerate(row)) + [('id', i + 3000)]} - for i, row in enumerate(df.values) -] - -app = dash.Dash() -app.css.config.serve_locally = True -app.scripts.config.serve_locally = True - -app.layout = html.Div( - [ - dash_table.DataTable( - id="table", - data=data, - page_action="native", - page_current=0, - page_size=250, - columns=[ - {"id": 0, "name": "Complaint ID"}, - {"id": 1, "name": "Product"}, - {"id": 2, "name": "Sub-product"}, - {"id": 3, "name": "Issue"}, - {"id": 4, "name": "Sub-issue"}, - {"id": 5, "name": "State"}, - {"id": 6, "name": "ZIP"}, - {"id": 7, "name": "code"}, - {"id": 8, "name": "Date received"}, - {"id": 9, "name": "Date sent to company"}, - {"id": 10, "name": "Company"}, - {"id": 11, "name": "Company response"}, - {"id": 12, "name": "Timely response?"}, - {"id": 13, "name": "Consumer disputed?"}, - ], - fixed_columns={ 'headers': True }, - fixed_rows={ 'headers': True }, - row_selectable=True, - row_deletable=True, - sort_action="native", - filter_action='native', - editable=True, - ), - html.Div(id="props_container") - ] -) - -props = [ - 'active_cell', 'start_cell', 'end_cell', 'selected_cells', - 'selected_rows', 'selected_row_ids', - 'derived_viewport_selected_rows', 'derived_viewport_selected_row_ids', - 'derived_virtual_selected_rows', 'derived_virtual_selected_row_ids', - 'derived_viewport_indices', 'derived_viewport_row_ids', - 'derived_virtual_indices', 'derived_virtual_row_ids' -] - - -@app.callback( - Output("props_container", "children"), - [Input("table", prop) for prop in props] -) -def show_props(*args): - return html.Table([ - html.Tr([ - html.Td(prop), - html.Td(json.dumps(val), id=prop + '_container') - ]) - for prop, val in zip(props, args) - ]) - - -if __name__ == "__main__": - app.run_server(port=8083, debug=False) diff --git a/tests/cypress/dash/v_markdown.py b/tests/cypress/dash/v_markdown.py deleted file mode 100644 index 6e0dc4d79..000000000 --- a/tests/cypress/dash/v_markdown.py +++ /dev/null @@ -1,51 +0,0 @@ -# pylint: disable=global-statement -import dash -import dash_html_components as html -import os -import pandas as pd -import sys - -sys.path.append( - os.path.abspath( - os.path.join(os.path.dirname(sys.argv[0]), os.pardir, os.pardir, os.pardir) - ) -) -module_names = ["dash_table"] -modules = [__import__(x) for x in module_names] -dash_table = modules[0] - -url = "https://github.com/plotly/datasets/raw/master/" "26k-consumer-complaints.csv" -df = pd.read_csv(url) - -df['Complaint ID'] = df['Complaint ID'].map(lambda x: '**' + str(x) + '**') -df['Product'] = df['Product'].map(lambda x: '[' + str(x) + '](plot.ly)') -df['Issue'] = df['Issue'].map(lambda x: '![' + str(x) + '](https://dash.plot.ly/assets/images/logo.png)') -df['State'] = df['State'].map(lambda x: '```python\n"{}"\n```'.format(x)) - -df = df.values - -app = dash.Dash() - -app.layout = html.Div( - [ - html.Div(id="container", children="Hello World"), - dash_table.DataTable( - id="table", - data=df[0:250], - columns=[ - {"id": 1, "name": "Complaint ID", "presentation": "markdown"}, - {"id": 2, "name": "Product", "presentation": "markdown"}, - {"id": 3, "name": "Sub-product"}, - {"id": 4, "name": "Issue", "presentation": "markdown"}, - {"id": 5, "name": "Sub-issue"}, - {"id": 6, "name": "State", "presentation": "markdown"}, - {"id": 7, "name": "ZIP"} - ], - editable=True, - sort_action='native', - include_headers_on_copy_paste=True - ) - ] -) - -app.run_server(debug=False, port=8087) diff --git a/tests/cypress/dash/v_pagination.py b/tests/cypress/dash/v_pagination.py deleted file mode 100644 index 7a8bc89e6..000000000 --- a/tests/cypress/dash/v_pagination.py +++ /dev/null @@ -1,64 +0,0 @@ -import dash -from dash.dependencies import Input, Output -from dash.exceptions import PreventUpdate -import dash_table -import dash_core_components as dcc -import dash_html_components as html -import pandas as pd - - -df = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/gapminder2007.csv') - -df[' index'] = range(1, len(df) + 1) - -app = dash.Dash(__name__) - -PAGE_SIZE = 5 - -app.layout = html.Div([ - dcc.Location(id='url'), - dash_table.DataTable( - id='table', - columns=[ - {'name': i, 'id': i} for i in sorted(df.columns) - ], - page_current=0, - page_size=PAGE_SIZE, - page_count=None - ) -]) - - -@app.callback( - [Output('table', 'data'), - Output('table', 'page_action'), - Output('table', 'page_count')], - [Input('table', 'page_current'), - Input('table', 'page_size'), - Input('url', 'search')] -) -def update_table(page_current, page_size, pagination_mode): - - if not pagination_mode: - raise PreventUpdate - - mode = { - param.split('=')[0]: param.split('=')[1] - for param in pagination_mode.strip('?').split('&') - } - - data = None - - if mode['page_action'] == 'native': - data = df.to_dict('records') - else: - data = df.iloc[ - page_current * page_size: (page_current + 1) * page_size - ].to_dict('records') - - return data, mode.get('page_action', 'native'), \ - int(mode.get('page_count')) if mode.get('page_count') else None - - -if __name__ == '__main__': - app.run_server(port=8086, debug=True) diff --git a/tests/cypress/tests/server/copy_paste_test.ts b/tests/cypress/tests/server/copy_paste_test.ts deleted file mode 100644 index b18e19a6f..000000000 --- a/tests/cypress/tests/server/copy_paste_test.ts +++ /dev/null @@ -1,271 +0,0 @@ -import DashTable, { State } from 'cypress/DashTable'; -import DOM from 'cypress/DOM'; -import Key from 'cypress/Key'; - -describe('copy paste', () => { - beforeEach(() => { - cy.visit('http://localhost:8082'); - cy.wait(1000); - }); - - it('can copy multiple rows', () => { - DashTable.getCell(0, 0).click(); - DOM.focused.type(Key.Shift, { release: false }); - DashTable.getCell(2, 0).click(); - - DOM.focused.type(`${Key.Meta}c`); - DashTable.getCell(3, 0).click(); - DOM.focused.type(`${Key.Meta}v`); - DashTable.getCell(0, 0, State.Any).click(); - - for (let row = 0; row <= 2; ++row) { - DashTable.getCell(row + 3, 0, State.Any).within(() => cy.get('.dash-cell-value').should('have.html', `${row}`)); - } - }); - - it('can copy rows 9 and 10', () => { - DashTable.getCell(9, 0).click(); - DOM.focused.type(`${Key.Shift}${Key.ArrowDown}`); - - DOM.focused.type(`${Key.Meta}c`); - DashTable.getCell(1, 0).click(); - DOM.focused.type(`${Key.Meta}v`); - DashTable.getCell(0, 0, State.Any).click(); - - DashTable.getCell(1, 0, State.Any).within(() => cy.get('.dash-cell-value').should('have.html', '9')); - DashTable.getCell(2, 0, State.Any).within(() => cy.get('.dash-cell-value').should('have.html', '10')); - - }); - - it('can copy multiple rows and columns', () => { - DashTable.getCell(0, 1).click(); - DOM.focused.type(Key.Shift, { release: false }); - DashTable.getCell(2, 2).click(); - - DOM.focused.type(`${Key.Meta}c`); - DashTable.getCell(3, 1).click(); - DOM.focused.type(`${Key.Meta}v`); - DashTable.getCell(0, 0, State.Any).click(); - - for (let row = 0; row <= 2; ++row) { - for (let column = 1; column <= 2; ++column) { - let initialValue: string; - - DashTable.getCell(row, column, State.Any).within(() => cy.get('.dash-cell-value').then($cells => initialValue = $cells[0].innerHTML)); - DashTable.getCell(row + 3, column, State.Any).within(() => cy.get('.dash-cell-value').should('have.html', initialValue)); - } - } - }); - - it('can copy multiple rows and columns from one table and paste to another', () => { - DashTable.getCell(10, 0).click(); - DOM.focused.type(Key.Shift, { release: false }); - DashTable.getCell(13, 3).click(); - - DOM.focused.type(`${Key.Meta}c`); - cy.get(`#table2 tbody tr td.column-${0}`).eq(0).click(); - DOM.focused.type(`${Key.Meta}v`); - cy.get(`#table2 tbody tr td.column-${3}`).eq(3).click(); - - cy.wait(1000); - - DashTable.getCell(14, 0).click(); - DOM.focused.type(Key.Shift, { release: false }); - - for (let row = 10; row <= 13; ++row) { - for (let column = 0; column <= 3; ++column) { - let initialValue: string; - - DashTable.getCell(row, column).within(() => cy.get('.dash-cell-value').then($cells => initialValue = $cells[0].innerHTML)); - cy.get(`#table2 tbody tr td.column-${column}`).eq(row - 10).within(() => cy.get('.dash-cell-value').should('have.html', initialValue)); - } - } - }); - - describe('copy and paste with hideable columns', () => { - it('copy multiple rows and columns within table', () => { - DashTable.hideColumnById(0, '0'); - - DashTable.getCell(0, 0).click(); - DOM.focused.type(Key.Shift, { release: false }); - DashTable.getCell(2, 2).click(); - - DOM.focused.type(`${Key.Meta}c`); - DashTable.getCell(3, 1).click(); - DOM.focused.type(`${Key.Meta}v`); - DashTable.getCell(5, 3, State.Any).click(); - - DashTable.getCell(6, 0, State.Any).click(); - DOM.focused.type(Key.Shift, { release: false }); - - for (let row = 0; row <= 2; ++row) { - for (let column = 0; column <= 2; ++column) { - let initialValue: string; - - DashTable.getCell(row, column, State.Any).within(() => cy.get('.dash-cell-value').then($cells => initialValue = $cells[0].innerHTML)); - DashTable.getCell(row + 3, column + 1, State.Any).within(() => cy.get('.dash-cell-value').should('have.html', initialValue)); - } - } - }); - - it('copy multiple rows and columns from one table to another', () => { - DashTable.hideColumnById(0, '0'); - - DashTable.getCell(10, 0).click(); - DOM.focused.type(Key.Shift, { release: false }); - DashTable.getCell(13, 2).click(); - - DOM.focused.type(`${Key.Meta}c`); - cy.get(`#table2 tbody tr td.column-${0}`).eq(0).click(); - DOM.focused.type(`${Key.Meta}v`); - cy.get(`#table2 tbody tr td.column-${3}`).eq(2).click(); - - DashTable.getCell(16, 6).click(); - DOM.focused.type(Key.Shift, { release: false }); - - for (let row = 10; row <= 13; ++row) { - for (let column = 0; column <= 2; ++column) { - let initialValue: string; - - DashTable.getCell(row, column).within(() => cy.get('.dash-cell-value').then($cells => initialValue = $cells[0].innerHTML)); - cy.get(`#table2 tbody tr td.column-${column}`).eq(row - 10).within(() => cy.get('.dash-cell-value').should('have.html', initialValue)); - } - } - }); - }); - - // Commenting this test as Cypress team is having issues with the copy/paste scenario - // LINK: https://github.com/cypress-io/cypress/issues/2386 - describe('BE roundtrip on copy-paste', () => { - it('on cell modification', () => { - DashTable.getCell(0, 0).click(); - DOM.focused.type(`10${Key.Enter}`); - - cy.wait(1000); - - DashTable - .getCell(0, 0, State.Any) - .within(() => cy.get('.dash-cell-value').should('have.html', '10')) - .then(() => { - DashTable.getCell(0, 1, State.Any) - .within(() => cy.get('.dash-cell-value').should('have.html', 'MODIFIED')); - }); - }); - - it('with unsorted, unfiltered data', () => { - DashTable.getCell(0, 0).click(); - DOM.focused.type(`${Key.Meta}c`); - - DashTable.getCell(1, 0).click(); - DOM.focused.type(`${Key.Meta}v`); - - cy.wait(1000); - - DashTable - .getCell(1, 1, State.Any) - .within(() => cy.get('.dash-cell-value').should('have.html', 'MODIFIED')); - DashTable - .getCell(1, 0, State.Any) - .within(() => cy.get('.dash-cell-value').should('have.value', '0')); - - DashTable.getCell(1, 1).click(); - DOM.focused.type(`${Key.Meta}c`); - - DashTable.getCell(2, 1).click(); - DOM.focused.type(`${Key.Meta}v`); - - cy.wait(1000); - - DashTable - .getCell(2, 1, State.Any) - .within(() => cy.get('.dash-cell-value').should('have.value', 'MODIFIED')); - }); - - it('BE rountrip with sorted, unfiltered data', () => { - cy.get('#table tr th.column-2 .column-header--sort').last().click(); - - DashTable.getCell(0, 0).click(); - DashTable.getCell(0, 0).within(() => cy.get('.dash-cell-value').should('have.value', '11')); - - DOM.focused.type(`${Key.Meta}c`); - - DashTable.getCell(1, 0).click(); - DOM.focused.type(`${Key.Meta}v`); - - cy.wait(1000); - - DashTable - .getCell(1, 1, State.Any) - .within(() => cy.get('.dash-cell-value').should('have.html', 'MODIFIED')); - DashTable - .getCell(1, 0, State.Any) - .within(() => cy.get('.dash-cell-value').should('have.value', '11')); - - DashTable.getCell(1, 1).click(); - DOM.focused.type(`${Key.Meta}c`); - - DashTable.getCell(2, 1).click(); - DOM.focused.type(`${Key.Meta}v`); - - cy.wait(1000); - - DashTable - .getCell(2, 1, State.Any) - .within(() => cy.get('.dash-cell-value').should('have.value', 'MODIFIED')); - DashTable - .getCell(1, 0, State.Any) - .within(() => cy.get('.dash-cell-value').should('have.html', '11')); - }); - }); -}); - -describe('copy/paste behaviour with markdown', () => { - beforeEach(() => { - cy.visit('http://localhost:8087') - }); - - describe('single cell', () => { - it('copy markdown to non-markdown', () => { - DashTable.getCell(0, 3).click(); - DOM.focused.type(`${Key.Meta}c`); - DashTable.getCell(0, 2).click(); - DOM.focused.type(`${Key.Meta}v`); - DashTable.getCell(0, 2).within(() => { - cy.get('.dash-cell-value').should('have.attr', 'value', '![Communication tactics](https://dash.plot.ly/assets/images/logo.png)'); - }); - }); - - it('copy markdown to markdown', () => { - DashTable.getCell(0, 1).click(); - DOM.focused.type(`${Key.Meta}c`); - DashTable.getCell(0, 0).click(); - DOM.focused.type(`${Key.Meta}v`); - DashTable.getCell(0, 0).within(() => { - cy.get('.dash-cell-value > p > a').should('have.html', 'Debt collection'); - cy.get('.dash-cell-value > p > a').should('have.attr', 'href', 'plot.ly'); - }); - }); - - describe('copy non-markdown to markdown', () => { - it('null/empty value', () => { - DashTable.getCell(0, 2).click(); - DOM.focused.type(`${Key.Meta}c`); - DashTable.getCell(0, 1).click(); - DOM.focused.type(`${Key.Meta}v`); - DashTable.getCell(0, 1).within(() => { - cy.get('.dash-cell-value > p').should('have.html', 'null'); - }); - }); - - it('regular value', () => { - DashTable.getCell(1, 2).click(); - DOM.focused.type(`${Key.Meta}c`); - DashTable.getCell(1, 1).click(); - DOM.focused.type(`${Key.Meta}v`); - DashTable.getCell(1, 1).within(() => { - cy.get('.dash-cell-value > p').should('have.html', 'Medical'); - }); - }); - }); - }); -}); diff --git a/tests/cypress/tests/server/dash_test.ts b/tests/cypress/tests/server/dash_test.ts deleted file mode 100644 index b0ba93557..000000000 --- a/tests/cypress/tests/server/dash_test.ts +++ /dev/null @@ -1,150 +0,0 @@ -import DashTable from 'cypress/DashTable'; -import DOM from 'cypress/DOM'; -import Key from 'cypress/Key'; - -describe('dash basic', () => { - beforeEach(() => { - cy.visit('http://localhost:8081'); - }); - - it('can get cell', () => { - DashTable.getCell(0, 0).click(); - DashTable.getCell(0, 0).within(() => cy.get('input').should('have.value', '0')); - - cy.get('button.next-page').click(); - DashTable.getCell(0, 0).click(); - DashTable.getCell(0, 0).within(() => cy.get('input').should('have.value', '250')); - }); - - it('cell click selects all text', () => { - DashTable.getCell(0, 1).click(); - DashTable.getCell(0, 1).within(() => - cy.get('input').then($inputs => { - const $input = $inputs[0]; - - expect($input.selectionStart).to.equal(0); - expect($input.selectionEnd).to.equal($input.value.length); - }) - ); - }); - - // https://github.com/plotly/dash-table/issues/50 - it('can edit last and update data on "enter"', () => { - DashTable.focusCell(249, 0); - DOM.focused.then($input => { - const initialValue = $input.val(); - - DOM.focused.type(`abc${Key.Enter}`); - - cy.get('#container').should($container => { - expect($container.first()[0].innerText).to.equal(`[249][0] = ${initialValue} -> abc`); - }); - }); - }); - - // https://github.com/plotly/dash-table/issues/107 - it('can edit last and update data on "tab"', () => { - DashTable.focusCell(249, 0); - DOM.focused.then($input => { - const initialValue = $input.val(); - - DOM.focused.type(`abc`); - - cy.tab(); - - cy.get('#container').should($container => { - expect($container.first()[0].innerText).to.equal(`[249][0] = ${initialValue} -> abc`); - }); - }); - }); - describe('ArrowKeys navigation', () => { - describe('When active, but not focused', () => { - // https://github.com/plotly/dash-table/issues/141 - it('can edit last, update data on "arrowleft", and move one cell to the left', () => { - const startingCell = [249, 1]; - const targetCell = [249, 0]; - DashTable.focusCell(startingCell[0], startingCell[1]); - DOM.focused.then($input => { - const initialValue = $input.val(); - - DOM.focused.type(`abc${Key.ArrowLeft}`); - - cy.get('#container').should($container => { - expect($container.first()[0].innerText).to.equal(`[249][1] = ${initialValue} -> abc`); - }); - }); - DashTable.getCell(targetCell[0], targetCell[1]).should('have.class', 'focused'); - }); - - // https://github.com/plotly/dash-table/issues/141 - it('can edit last, update data on "arrowup", and move one cell up', () => { - const startingCell = [249, 0]; - const targetCell = [248, 0]; - DashTable.focusCell(startingCell[0], startingCell[1]); - DOM.focused.then($input => { - const initialValue = $input.val(); - - DOM.focused.type(`abc${Key.ArrowUp}`); - - cy.get('#container').should($container => { - expect($container.first()[0].innerText).to.equal(`[249][0] = ${initialValue} -> abc`); - }); - }); - DashTable.getCell(targetCell[0], targetCell[1]).should('have.class', 'focused'); - }); - - // https://github.com/plotly/dash-table/issues/141 - it('can edit last, update data on "arrowright", and move one cell to the right', () => { - const startingCell = [249, 0]; - const targetCell = [249, 1]; - DashTable.focusCell(startingCell[0], startingCell[1]); - DOM.focused.then($input => { - const initialValue = $input.val(); - - DOM.focused.type(`abc${Key.ArrowRight}`); - - cy.get('#container').should($container => { - expect($container.first()[0].innerText).to.equal(`[249][0] = ${initialValue} -> abc`); - }); - }); - DashTable.getCell(targetCell[0], targetCell[1]).should('have.class', 'focused'); - }); - - // https://github.com/plotly/dash-table/issues/141 - it('can edit last, update data on "arrowdown", and move one cell down', () => { - const startingCell = [249, 0]; - const targetCell = [249, 1]; - DashTable.focusCell(startingCell[0], startingCell[1]); - DOM.focused.then($input => { - const initialValue = $input.val(); - - DOM.focused.type(`abc${Key.ArrowRight}`); - - cy.get('#container').should($container => { - expect($container.first()[0].innerText).to.equal(`[249][0] = ${initialValue} -> abc`); - }); - }); - DashTable.getCell(targetCell[0], targetCell[1]).should('have.class', 'focused'); - }); - }); - }); - - it('can edit last and update data when clicking outside of cell', () => { - DashTable.focusCell(249, 0); - DOM.focused.then($input => { - const initialValue = $input.val(); - - DOM.focused.type(`abc`); - DashTable.getCell(248, 0).click(); - - cy.get('#container').should($container => { - expect($container.first()[0].innerText).to.equal(`[249][0] = ${initialValue} -> abc`); - }); - }); - }); - - it('can get cell with double click', () => { - DashTable.getCell(3, 1).within(() => cy.get('div').dblclick()); - DashTable.getCell(3, 1).should('have.class', 'focused'); - }); -}); diff --git a/tests/cypress/tests/server/delete_cells_test.ts b/tests/cypress/tests/server/delete_cells_test.ts deleted file mode 100644 index 3d0cac7ad..000000000 --- a/tests/cypress/tests/server/delete_cells_test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import DashTable from 'cypress/DashTable'; -import DOM from 'cypress/DOM'; -import Key from 'cypress/Key'; - -describe('delete cells', () => { - beforeEach(() => { - cy.visit('http://localhost:8085'); - }); - - describe('unsorted data', () => { - it('can delete single cell', () => { - DashTable.getCell(0, 1).click(); - DashTable.getCell(0, 1).within(() => cy.get('.dash-cell-value').should('not.have.value', '')); - DOM.focused.type(`${Key.Backspace}${Key.ArrowDown}`); - DashTable.getCell(0, 1).within(() => cy.get('.dash-cell-value').should('have.html', '')); - }); - - it('can delete multiple cells', () => { - DashTable.getCell(0, 1).click(); - DOM.focused.type(`${Key.Shift}${Key.ArrowDown}${Key.ArrowRight}`); - DOM.focused.type(`${Key.Backspace}`); - DashTable.getCell(0, 0).click(); - - for (let row = 0; row <= 1; ++row) { - for (let column = 1; column <= 2; ++column) { - DashTable.getCell(row, column).within(() => cy.get('.dash-cell-value').should('have.html', '')); - } - } - }); - }); - - describe('sorted data', () => { - beforeEach(() => { - cy.get('tr th.column-0 .column-header--sort').last().click(); - }); - - it('can delete single cell', () => { - DashTable.getCell(0, 1).click(); - DashTable.getCell(0, 1).within(() => cy.get('.dash-cell-value').should('not.have.value', '')); - DOM.focused.type(`${Key.Backspace}${Key.ArrowDown}`); - DashTable.getCell(0, 1).within(() => cy.get('.dash-cell-value').should('have.html', '')); - }); - - it('can delete multiple cells', () => { - DashTable.getCell(0, 1).click(); - DOM.focused.type(`${Key.Shift}${Key.ArrowDown}${Key.ArrowRight}`); - DOM.focused.type(`${Key.Backspace}`); - DashTable.getCell(0, 0).click(); - - for (let row = 0; row <= 1; ++row) { - for (let column = 1; column <= 2; ++column) { - DashTable.getCell(row, column).within(() => cy.get('.dash-cell-value').should('have.html', '')); - } - } - }); - }); -}); diff --git a/tests/cypress/tests/server/delete_row_test.ts b/tests/cypress/tests/server/delete_row_test.ts deleted file mode 100644 index 488420c4e..000000000 --- a/tests/cypress/tests/server/delete_row_test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import DashTable from 'cypress/DashTable'; - -describe('delete', () => { - beforeEach(() => cy.visit('http://localhost:8081')); - - it('can delete row', () => { - DashTable.getCell(0, 0).within(() => cy.get('.dash-cell-value').should('have.html', '0')); - DashTable.getDelete(0).click(); - DashTable.getCell(0, 0).within(() => cy.get('.dash-cell-value').should('have.html', '1')); - }); - - it('can delete row when sorted', () => { - cy.get('tr th.column-0 .column-header--sort').last().click({ force: true }).click({ force: true }); - DashTable.getCell(0, 0).within(() => cy.get('.dash-cell-value').should('have.html', '28155')); - DashTable.getDelete(0).click(); - DashTable.getCell(0, 0).within(() => cy.get('.dash-cell-value').should('have.html', '28154')); - }); -}); \ No newline at end of file diff --git a/tests/cypress/tests/server/loading_uneditable_test.ts b/tests/cypress/tests/server/loading_uneditable_test.ts deleted file mode 100644 index 494dba2f8..000000000 --- a/tests/cypress/tests/server/loading_uneditable_test.ts +++ /dev/null @@ -1,194 +0,0 @@ -import DashTable, { State } from 'cypress/DashTable'; -import DOM from 'cypress/DOM'; -import Key from 'cypress/Key'; - -describe('loading states uneditable', () => { - - beforeEach(() => { - cy.visit('http://localhost:8084'); - }); - - it('prevents editing while loading', () => { - // Table is editable - DashTable - .getCell(0, 0) - .click() - .find('.dash-input-cell-value-container > input').should('have.length', 1); - - // Trigger data callback - cy.get('#change-data-property').click(); - DOM.focused.type(`change_data${Key.Enter}`); - - // Table is not editable - DashTable - .getCell(0, 0, State.Loading) - .click() - .find('.dash-input-cell-value-container > input').should('have.length', 0); - - DashTable - .getCell(0, 0, State.Any) - .within(() => cy.get('.dash-cell-value').should('not.have.html', 'Hello')); - - cy.get('#change-data-property').should('have.value', 'change_data'); - - cy.wait(5000); - - // Table is editable - DashTable - .getCell(0, 0, State.Ready) - .click() - .find('.dash-input-cell-value-container > input').should('have.length', 1); - - DOM.focused.type(`Hello${Key.Enter}`); - - DashTable - .getCell(0, 0) - .within(() => cy.get('.dash-cell-value') - .should('have.html', 'Hello')); - }); - - it('keeps focus on callback completion', () => { - cy.get('#change-data-property').click(); - DOM.focused.type(`change_data${Key.Enter}`); - - DashTable.getCell(0, 0, State.Loading).click(); - cy.wait(5000); - DashTable.getCell(0, 0, State.Ready); - - DOM.focused.type(`Hello${Key.Enter}`); - DashTable - .getCell(0, 0) - .within(() => cy.get('.dash-cell-value') - .should('have.html', 'Hello')); - }); - - it('does not steal focus on callback completion', () => { - DashTable.getCell(0, 0, State.Ready).click(); - - cy.get('#change-data-property').click(); - DOM.focused.type(`change_data${Key.Enter}`); - - DashTable.getCell(0, 0, State.Loading); - DOM.focused.should('have.id', 'change-data-property'); - - cy.wait(5000); - - DashTable.getCell(0, 0, State.Ready); - DOM.focused.should('have.id', 'change-data-property'); - }); - - it('permits editing when a non-data prop is being changed', () => { - // Table is editable - DashTable - .getCell(0, 0) - .click() - .find('.dash-input-cell-value-container > input').should('have.length', 1); - - // Trigger non-data callback - cy.get('#change-other-property').click(); - DOM.focused.type(`dont_change_data${Key.Enter}`); - - // Table is editable - DashTable - .getCell(0, 0) - .click() - .find('.dash-input-cell-value-container > input').should('have.length', 1); - - DOM.focused.type(`Hello${Key.Enter}`); - DashTable.getCell(1, 0).click(); - - DashTable - .getCell(0, 0) - .within(() => cy.get('.dash-cell-value') - .should('have.html', 'Hello')); - }); - - it('does not permit copy-paste when data are loading', () => { - // Table is editable - DashTable - .getCell(0, 0) - .click() - .find('.dash-input-cell-value-container > input').should('have.length', 1); - - // Trigger data callback - cy.get('#change-data-property').click(); - DOM.focused.type(`change_data${Key.Enter}`); - - // Table is not editable - DashTable - .getCell(0, 0, State.Loading) - .click() - .find('.dash-input-cell-value-container > input').should('have.length', 0); - - DOM.focused.type(`${Key.Meta}c`); - - DashTable - .getCell(0, 1, State.Loading) - .click(); - - DashTable - .getCell(0, 1, State.Loading) - .click() - .find('.dash-input-cell-value-container > input').should('have.length', 0); - - DOM.focused.type(`${Key.Meta}v`); - - DashTable - .getCell(0, 0, State.Loading) - .click(); - - DashTable.getCell(0, 1, State.Loading) - .within(() => cy.get('.dash-cell-value') - .should('not.have.html', '0')); - - cy.wait(5000); - - // Table is editable - DashTable - .getCell(0, 1) - .click(); - - DOM.focused.type(`${Key.Meta}v`); - - DashTable - .getCell(0, 0) - .click(); - - DashTable.getCell(0, 1) - .within(() => cy.get('.dash-cell-value') - .should('have.html', '0')); - }); - - it('permits copy-paste when a non-data prop is loading', () => { - // Table is editable - DashTable - .getCell(0, 0) - .click() - .find('.dash-input-cell-value-container > input').should('have.length', 1); - - // Trigger non-data callback - cy.get('#change-other-property').click(); - DOM.focused.type(`dont_change_data${Key.Enter}`); - - DashTable - .getCell(0, 0) - .click(); - - DOM.focused.type(`${Key.Meta}c`); - - DashTable - .getCell(0, 1) - .click(); - - DOM.focused.type(`${Key.Meta}v`); - - DashTable - .getCell(0, 0) - .click(); - - DashTable.getCell(0, 1) - .within(() => cy.get('.dash-cell-value') - .should('have.html', '0')); - }); - -}); diff --git a/tests/cypress/tests/server/pagination_test.ts b/tests/cypress/tests/server/pagination_test.ts deleted file mode 100644 index e3e691722..000000000 --- a/tests/cypress/tests/server/pagination_test.ts +++ /dev/null @@ -1,254 +0,0 @@ -import DashTable from 'cypress/DashTable'; -import DOM from 'cypress/DOM'; -import Key from 'cypress/Key'; - -const pagination_modes = ['native', 'custom'] - -describe('table pagination', () => { - - pagination_modes.forEach(mode => { - - describe(`can change pages with ${mode} page_action`, () => { - - before(() => { - cy.visit(`http://localhost:8086?page_action=${mode}&page_count=29`); - - // initial state: first page, previous/first buttons disabled - DashTable.getCell(0, 0).within(() => cy.get('.dash-cell-value').should('have.html', '1')); - cy.get('button.first-page').should('be.disabled'); - cy.get('button.previous-page').should('be.disabled'); - cy.get('input.current-page').should('have.attr', 'placeholder', '1'); - cy.get('.last-page').should('have.html', '29'); - cy.get('button.next-page').should('not.be.disabled'); - cy.get('button.last-page').should('not.be.disabled'); - - }); - - describe('with the navigation buttons', () => { - - it('with the next-page navigation button', () => { - - // go forward by five pages - for (let i = 0; i < 5; i++) { - cy.get('button.next-page').click(); - } - - cy.get('button.first-page').should('not.be.disabled'); - cy.get('button.previous-page').should('not.be.disabled'); - cy.get('input.current-page').should('have.attr', 'placeholder', '6'); - cy.get('.last-page').should('have.html', '29'); - cy.get('button.next-page').should('not.be.disabled'); - cy.get('button.last-page').should('not.be.disabled'); - - DashTable.getCell(0, 0).within(() => cy.get('.dash-cell-value').should('have.html', '26')); - - }); - - it('with the previous-page navigation button', () => { - // go back by three pages - for (let i = 0; i < 3; i++) { - cy.get('button.previous-page').click(); - } - - cy.get('button.first-page').should('not.be.disabled'); - cy.get('button.previous-page').should('not.be.disabled'); - cy.get('input.current-page').should('have.attr', 'placeholder', '3'); - cy.get('.last-page').should('have.html', '29'); - cy.get('button.next-page').should('not.be.disabled'); - cy.get('button.last-page').should('not.be.disabled'); - - DashTable.getCell(0, 0).within(() => cy.get('.dash-cell-value').should('have.html', '11')); - - }); - - it('with the first page button', () => { - cy.get('button.first-page').click(); - - cy.get('button.first-page').should('be.disabled'); - cy.get('button.previous-page').should('be.disabled'); - cy.get('input.current-page').should('have.attr', 'placeholder', '1'); - cy.get('.last-page').should('have.html', '29'); - cy.get('button.next-page').should('not.be.disabled'); - cy.get('button.last-page').should('not.be.disabled'); - - DashTable.getCell(0, 0).within(() => cy.get('.dash-cell-value').should('have.html', '1')); - - }); - - it('with the last page button', () => { - cy.get('button.last-page').click(); - - cy.get('button.first-page').should('not.be.disabled'); - cy.get('button.previous-page').should('not.be.disabled'); - cy.get('input.current-page').should('have.attr', 'placeholder', '29'); - cy.get('.last-page').should('have.html', '29'); - cy.get('button.next-page').should('be.disabled'); - cy.get('button.last-page').should('be.disabled'); - - DashTable.getCell(0, 0).within(() => cy.get('.dash-cell-value').should('have.html', '141')); - - }); - }); - - describe('with the text input box', () => { - describe('correctly navigates to the desired page', () => { - it('with unfocus', () => { - cy.get('input.current-page').click(); - DOM.focused.type(`14`); - cy.get('input.current-page').blur(); - - cy.get('button.first-page').should('not.be.disabled'); - cy.get('button.previous-page').should('not.be.disabled'); - cy.get('input.current-page').should('have.attr', 'placeholder', '14'); - cy.get('.last-page').should('have.html', '29'); - cy.get('button.next-page').should('not.be.disabled'); - cy.get('button.last-page').should('not.be.disabled'); - - DashTable.getCell(0, 0).within(() => cy.get('.dash-cell-value').should('have.html', '66')); - }), - it('with enter', () => { - cy.get('input.current-page').click(); - DOM.focused.type(`18${Key.Enter}`); - - cy.get('button.first-page').should('not.be.disabled'); - cy.get('button.previous-page').should('not.be.disabled'); - cy.get('input.current-page').should('have.attr', 'placeholder', '18'); - cy.get('input.current-page').should('not.be.focused'); - cy.get('.last-page').should('have.html', '29'); - cy.get('button.next-page').should('not.be.disabled'); - cy.get('button.last-page').should('not.be.disabled'); - - DashTable.getCell(0, 0).within(() => cy.get('.dash-cell-value').should('have.html', '86')); - }) - }); - - describe('can handle invalid page numbers', () => { - it('zero value', () => { - cy.get('input.current-page').click(); - DOM.focused.type('0'); - cy.get('input.current-page').blur(); - - cy.get('button.first-page').should('be.disabled'); - cy.get('button.previous-page').should('be.disabled'); - cy.get('input.current-page').should('have.attr', 'placeholder', '1'); - cy.get('.last-page').should('have.html', '29'); - cy.get('button.next-page').should('not.be.disabled'); - cy.get('button.last-page').should('not.be.disabled'); - - DashTable.getCell(0, 0).within(() => cy.get('.dash-cell-value').should('have.html', '1')); - - }); - - it('value higher than last page', () => { - cy.get('input.current-page').click(); - DOM.focused.type('100'); - cy.get('input.current-page').blur(); - cy.get('button.first-page').should('not.be.disabled'); - cy.get('button.previous-page').should('not.be.disabled'); - cy.get('input.current-page').should('have.attr', 'placeholder', '29'); - cy.get('.last-page').should('have.html', '29'); - cy.get('button.next-page').should('be.disabled'); - cy.get('button.last-page').should('be.disabled'); - - DashTable.getCell(0, 0).within(() => cy.get('.dash-cell-value').should('have.html', '141')); - }); - - it('negative value', () => { - - cy.get('input.current-page').click(); - DOM.focused.type('-1'); - cy.get('input.current-page').blur(); - - cy.get('button.first-page').should('be.disabled'); - cy.get('button.previous-page').should('be.disabled'); - cy.get('input.current-page').should('have.attr', 'placeholder', '1'); - cy.get('.last-page').should('have.html', '29'); - cy.get('button.next-page').should('not.be.disabled'); - cy.get('button.last-page').should('not.be.disabled'); - - DashTable.getCell(0, 0).within(() => cy.get('.dash-cell-value').should('have.html', '1')); - - }); - - it('non-numerical value', () => { - cy.get('input.current-page').click(); - DOM.focused.type('10'); - cy.get('input.current-page').blur(); - - cy.get('button.first-page').should('not.be.disabled'); - cy.get('button.previous-page').should('not.be.disabled'); - cy.get('input.current-page').should('have.attr', 'placeholder', '10'); - cy.get('.last-page').should('have.html', '29'); - cy.get('button.next-page').should('not.be.disabled'); - cy.get('button.last-page').should('not.be.disabled'); - - DashTable.getCell(0, 0).within(() => cy.get('.dash-cell-value').should('have.html', '46')); - - cy.get('input.current-page').click(); - DOM.focused.type('hello'); - cy.get('input.current-page').blur(); - - cy.get('button.first-page').should('not.be.disabled'); - cy.get('button.previous-page').should('not.be.disabled'); - cy.get('input.current-page').should('have.attr', 'placeholder', '10'); - cy.get('.last-page').should('have.html', '29'); - cy.get('button.next-page').should('not.be.disabled'); - cy.get('button.last-page').should('not.be.disabled'); - - DashTable.getCell(0, 0).within(() => cy.get('.dash-cell-value').should('have.html', '46')); - - }); - }); - }); - }); - }); - - describe('handles other page_count values', () => { - - describe('hides pagination', () => { - it('on single page', () => { - cy.visit(`http://localhost:8086?page_action=custom&page_count=1`); - - cy.get('.previous-next-container').should('not.exist'); - - DashTable.getCell(0, 0).within(() => cy.get('.dash-cell-value').should('have.html', '1')); - - }); - - it('on negative/zero values', () => { - cy.visit(`http://localhost:8086?page_action=custom&page_count=-1`); - - cy.get('.previous-next-container').should('not.exist'); - - DashTable.getCell(0, 0).within(() => cy.get('.dash-cell-value').should('have.html', '1')); - }); - }); - - it('undefined/none', () => { - - cy.visit(`http://localhost:8086?page_action=custom`); - - cy.get('.page-number').children().should('have.length', 1); - cy.get('.current-page').should('exist'); - cy.get('button.next-page').should('not.be.disabled'); - cy.get('button.last-page').should('be.disabled'); - - DashTable.getCell(0, 0).within(() => cy.get('.dash-cell-value').should('have.html', '1')); - - }); - - it('limits pages', () => { - cy.visit(`http://localhost:8086?page_action=custom&page_count=10`); - cy.get('button.last-page').click(); - - cy.get('button.first-page').should('not.be.disabled'); - cy.get('button.previous-page').should('not.be.disabled'); - cy.get('input.current-page').should('have.attr', 'placeholder', '10'); - cy.get('.last-page').should('have.html', '10'); - cy.get('button.next-page').should('be.disabled'); - cy.get('button.last-page').should('be.disabled'); - - DashTable.getCell(0, 0).within(() => cy.get('.dash-cell-value').should('have.html', '46')); - }); - }); -}); diff --git a/tests/cypress/tests/server/select_props_test.ts b/tests/cypress/tests/server/select_props_test.ts deleted file mode 100644 index 43e124ba8..000000000 --- a/tests/cypress/tests/server/select_props_test.ts +++ /dev/null @@ -1,195 +0,0 @@ -import DashTable from 'cypress/DashTable'; -import DOM from 'cypress/DOM'; -import Key from 'cypress/Key'; -import { map, xprod } from 'ramda'; - -function expectArray(selector: string, vals: number[], sorted?: boolean) { - cy.get(selector).should($container => { - const valsOut = JSON.parse($container.text()) as number[]; - if (sorted !== false) { - valsOut.sort(); - } - expect(valsOut).to.deep.equal(vals); - }); -} - -function expectCellSelection( - rows: number[], - rowIds?: number[], - cols?: number[], - colIds?: number[], - activeItem?: number[], // indices within rows/cols, dflt [0,0] - startItem?: number[] // same^ -) { - function makeCell(rc: number[]) { - const [r, c] = rc; - return { - row: rows[r], - row_id: rowIds && rowIds[r], - column: cols && cols[c], - column_id: colIds && colIds[c] - }; - } - - let activeCell: any; - let startCell: any; - let endCell: any; - let selectedCells: any; - if (rows.length && cols) { - activeCell = makeCell(activeItem || [0, 0]); - startCell = makeCell(startItem || [0, 0]); - endCell = makeCell(startItem ? [0, 0] : [rows.length - 1, cols.length - 1]); - selectedCells = map(makeCell, xprod(range(0, rows.length - 1), range(0, cols.length - 1))); - } else { - activeCell = startCell = endCell = selectedCells = null; - } - - cy.get('#active_cell_container').should($container => { - expect(JSON.parse($container.text())).to.deep.equal(activeCell); - }); - cy.get('#start_cell_container').should($container => { - expect(JSON.parse($container.text())).to.deep.equal(startCell); - }); - cy.get('#end_cell_container').should($container => { - expect(JSON.parse($container.text())).to.deep.equal(endCell); - }); - cy.get('#selected_cells_container').should($container => { - if (selectedCells && selectedCells.length) { - expect(JSON.parse($container.text())).to.deep.equal(selectedCells); - } else { - expect($container.text()).to.be.oneOf(['null', '[]']); - } - }); -} - -// NOTE: this function includes both endpoints -// easier to compare with the full arrays that way. -function range(from: number, to: number, step?: number) { - const _step = step || 1; - const out: number[] = []; - for (let v = from; v * _step <= to * _step; v += _step) { - out.push(v); - } - return out; -} - -describe('select row', () => { - describe('be pagination & sort', () => { - beforeEach(() => cy.visit('http://localhost:8081')); - - it('can select row', () => { - DashTable.getSelect(0).within(() => cy.get('input').click()); - DashTable.getSelect(0).within(() => cy.get('input').should('be.checked')); - }); - - it('can select row when sorted', () => { - cy.get('tr th.column-0 .column-header--sort').last().click({ force: true }); - DashTable.getSelect(0).within(() => cy.get('input').click()); - DashTable.getSelect(0).within(() => cy.get('input').should('be.checked')); - }); - - it('select, sort, new row is not selected', () => { - DashTable.getSelect(0).within(() => cy.get('input').click()); - DashTable.getSelect(0).within(() => cy.get('input').should('be.checked')); - cy.get('tr th.column-0 .column-header--sort').last().click({ force: true }); - DashTable.getSelect(0).within(() => cy.get('input').should('not.be.checked')); - }); - }); - - describe('fe pagination & sort', () => { - beforeEach(() => cy.visit('http://localhost:8083')); - - it('selection props are correct, no sort / filter', () => { - DashTable.getSelect(0).within(() => cy.get('input').click()); - DashTable.getSelect(1).within(() => cy.get('input').click()); - - expectCellSelection([]); - - // single cell selection - DashTable.getCell(3, 1).click(); - expectCellSelection([3], [3003], [1], [1]); - - // region up & left - active & start stay at the bottom right - DOM.focused.type(Key.Shift, { release: false }); - DashTable.getCell(1, 0).click(); - expectCellSelection([1, 2, 3], [3001, 3002, 3003], [0, 1], [0, 1], [2, 1], [2, 1]); - - // shrink the selection - DashTable.getCell(2, 1).click(); - expectCellSelection([2, 3], [3002, 3003], [1], [1], [1, 0], [1, 0]); - - // move the active cell without changing the selection - DOM.focused.type(Key.Shift); // and release - DashTable.getCell(2, 1).click(); - expectCellSelection([2, 3], [3002, 3003], [1], [1], [0, 0], [1, 0]); - - expectArray('#selected_rows_container', [0, 1]); - expectArray('#selected_row_ids_container', [3000, 3001]); - expectArray('#derived_viewport_selected_rows_container', [0, 1]); - expectArray('#derived_viewport_selected_row_ids_container', [3000, 3001]); - expectArray('#derived_virtual_selected_rows_container', [0, 1]); - expectArray('#derived_virtual_selected_row_ids_container', [3000, 3001]); - expectArray('#derived_viewport_indices_container', range(0, 249), false); - expectArray('#derived_viewport_row_ids_container', range(3000, 3249), false); - expectArray('#derived_virtual_indices_container', range(0, 999), false); - expectArray('#derived_virtual_row_ids_container', range(3000, 3999), false); - }); - - it('selection props are correct, with filter', () => { - DashTable.getSelect(0).within(() => cy.get('input').click()); - DashTable.getSelect(1).within(() => cy.get('input').click()); - DashTable.getSelect(2).within(() => cy.get('input').click()); - - cy.get('tr th.column-0.dash-filter input').type(`is even${Key.Enter}`); - - // filtered-out data is still selected - expectArray('#selected_rows_container', [0, 1, 2]); - expectArray('#selected_row_ids_container', [3000, 3001, 3002]); - expectArray('#derived_viewport_selected_rows_container', [0, 1]); - expectArray('#derived_viewport_selected_row_ids_container', [3000, 3002]); - expectArray('#derived_virtual_selected_rows_container', [0, 1]); - expectArray('#derived_virtual_selected_row_ids_container', [3000, 3002]); - expectArray('#derived_viewport_indices_container', range(0, 498, 2), false); - expectArray('#derived_viewport_row_ids_container', range(3000, 3498, 2), false); - expectArray('#derived_virtual_indices_container', range(0, 998, 2), false); - expectArray('#derived_virtual_row_ids_container', range(3000, 3998, 2), false); - }); - - it('selection props are correct, with filter & sort', () => { - DashTable.getSelect(0).within(() => cy.get('input').click()); - DashTable.getSelect(1).within(() => cy.get('input').click()); - - DashTable.getCell(3, 1).click(); - expectCellSelection([3], [3003], [1], [1]); - - cy.get('tr th.column-0.dash-filter input').type(`is even${Key.Enter}`); - - expectCellSelection([]); - - DashTable.getCell(3, 1).click(); - expectCellSelection([3], [3006], [1], [1]); - - cy.get('tr th.column-0 .column-header--sort').last().click({ force: true }); - - expectCellSelection([]); - - cy.get('tr th.column-0 .column-header--sort').last().click({ force: true }); - - DashTable.getSelect(0).within(() => cy.get('input').click()); - - DashTable.getCell(3, 1).click(); - expectCellSelection([3], [3992], [1], [1]); - - expectArray('#selected_rows_container', [0, 1, 998]); - expectArray('#selected_row_ids_container', [3000, 3001, 3998]); - expectArray('#derived_viewport_selected_rows_container', [0]); - expectArray('#derived_viewport_selected_row_ids_container', [3998]); - expectArray('#derived_virtual_selected_rows_container', [0, 499]); - expectArray('#derived_virtual_selected_row_ids_container', [3000, 3998]); - expectArray('#derived_viewport_indices_container', range(998, 500, -2), false); - expectArray('#derived_viewport_row_ids_container', range(3998, 3500, -2), false); - expectArray('#derived_virtual_indices_container', range(998, 0, -2), false); - expectArray('#derived_virtual_row_ids_container', range(3998, 3000, -2), false); - }); - }); -}); diff --git a/tests/integration/review_app/test_app_df_backend_paging.py b/tests/integration/review_app/test_app_df_backend_paging.py index bc40a6a85..bd6109eaa 100644 --- a/tests/integration/review_app/test_app_df_backend_paging.py +++ b/tests/integration/review_app/test_app_df_backend_paging.py @@ -29,11 +29,7 @@ def test_rapp001_df_backend_paging(dash_duo): df = pd.read_csv( os.path.realpath( os.path.join( - os.path.dirname(__file__), - "..", - "..", - "assets", - "gapminder.csv", + os.path.dirname(__file__), "..", "..", "assets", "gapminder.csv", ) ) ) @@ -41,8 +37,7 @@ def test_rapp001_df_backend_paging(dash_duo): df["index"] = range(1, len(df) + 1) app = dash.Dash( - __name__, - external_stylesheets=["https://codepen.io/chriddyp/pen/dZVMbK.css"], + __name__, external_stylesheets=["https://codepen.io/chriddyp/pen/dZVMbK.css"], ) app.config.suppress_callback_exceptions = True @@ -55,8 +50,7 @@ def section_title(title): dash_table.DataTable( id=IDS["table"], columns=[ - {"name": i, "id": i, "deletable": True} - for i in sorted(df.columns) + {"name": i, "id": i, "deletable": True} for i in sorted(df.columns) ], page_current=0, page_size=PAGE_SIZE, @@ -83,8 +77,7 @@ def section_title(title): dash_table.DataTable( id=IDS["table-sorting"], columns=[ - {"name": i, "id": i, "deletable": True} - for i in sorted(df.columns) + {"name": i, "id": i, "deletable": True} for i in sorted(df.columns) ], page_current=0, page_size=PAGE_SIZE, @@ -109,8 +102,7 @@ def section_title(title): dash_table.DataTable( id=IDS["table-multi-sorting"], columns=[ - {"name": i, "id": i, "deletable": True} - for i in sorted(df.columns) + {"name": i, "id": i, "deletable": True} for i in sorted(df.columns) ], page_current=0, page_size=PAGE_SIZE, @@ -146,8 +138,7 @@ def section_title(title): dash_table.DataTable( id=IDS["table-filtering"], columns=[ - {"name": i, "id": i, "deletable": True} - for i in sorted(df.columns) + {"name": i, "id": i, "deletable": True} for i in sorted(df.columns) ], page_current=0, page_size=PAGE_SIZE, @@ -155,14 +146,11 @@ def section_title(title): filter_action="custom", filter_query="", ), - section_title( - "Backend Paging with Filtering and Multi-Column Sorting" - ), + section_title("Backend Paging with Filtering and Multi-Column Sorting"), dash_table.DataTable( id=IDS["table-sorting-filtering"], columns=[ - {"name": i, "id": i, "deletable": True} - for i in sorted(df.columns) + {"name": i, "id": i, "deletable": True} for i in sorted(df.columns) ], page_current=0, page_size=PAGE_SIZE, @@ -216,10 +204,7 @@ def section_title(title): @app.callback( Output(IDS["table"], "data"), - [ - Input(IDS["table"], "page_current"), - Input(IDS["table"], "page_size"), - ], + [Input(IDS["table"], "page_current"), Input(IDS["table"], "page_size"),], ) def update_graph(page_current, page_size): return df.iloc[ diff --git a/tests/integration/review_app/test_app_df_graph.py b/tests/integration/review_app/test_app_df_graph.py index 1dd3b0b4a..a7de35c40 100644 --- a/tests/integration/review_app/test_app_df_graph.py +++ b/tests/integration/review_app/test_app_df_graph.py @@ -16,11 +16,7 @@ def test_rapp002_df_graph(dash_duo): df = pd.read_csv( os.path.realpath( os.path.join( - os.path.dirname(__file__), - "..", - "..", - "assets", - "gapminder.csv", + os.path.dirname(__file__), "..", "..", "assets", "gapminder.csv", ) ) ) @@ -28,8 +24,7 @@ def test_rapp002_df_graph(dash_duo): df = df[df["year"] == 2007] app = dash.Dash( - __name__, - external_stylesheets=["https://codepen.io/chriddyp/pen/dZVMbK.css"], + __name__, external_stylesheets=["https://codepen.io/chriddyp/pen/dZVMbK.css"], ) app.layout = html.Div( [ @@ -37,8 +32,7 @@ def test_rapp002_df_graph(dash_duo): dash_table.DataTable( id=IDS["table"], columns=[ - {"name": i, "id": i, "deletable": True} - for i in df.columns + {"name": i, "id": i, "deletable": True} for i in df.columns ], data=df.to_dict("rows"), editable=True, diff --git a/tests/integration/test_table_export_csv.py b/tests/integration/test_table_export_csv.py index e91653eae..59e6bdaf6 100644 --- a/tests/integration/test_table_export_csv.py +++ b/tests/integration/test_table_export_csv.py @@ -17,7 +17,7 @@ def test_tbex001_table_export(dash_duo): export_format="csv", ) dash_duo.start_server(app) - dash_duo.wait_for_element('.export', timeout=1).click() + dash_duo.wait_for_element(".export", timeout=1).click() download = os.path.sep.join((dash_duo.download_path, "Data.csv")) wait.until(lambda: os.path.exists(download), timeout=2) diff --git a/tests/selenium/conftest.py b/tests/selenium/conftest.py new file mode 100644 index 000000000..6d0d29f15 --- /dev/null +++ b/tests/selenium/conftest.py @@ -0,0 +1,321 @@ +import pytest + +from dash.testing.browser import Browser +from preconditions import preconditions +from selenium.webdriver.common.action_chains import ActionChains +from selenium.webdriver.common.by import By +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.wait import WebDriverWait + +_validate_col = lambda col: (isinstance(col, str) and len(col) > 0) or ( + isinstance(col, int) and col >= 0 +) +_validate_col_id = lambda col_id: isinstance(col_id, str) and len(col_id) > 0 +_validate_id = lambda id: isinstance(id, str) and len(id) > 0 +_validate_key = lambda key: isinstance(key, str) and len(key) == 1 +_validate_keys = lambda keys: isinstance(keys, str) and len(keys) > 0 +_validate_mixin = lambda mixin: isinstance(mixin, DataTableMixin) +_validate_row = lambda row: isinstance(row, int) and row >= 0 +_validate_selector = lambda selector: isinstance(selector, str) and len(selector) > 0 +_validate_state = lambda state: state in [_READY, _LOADING, _ANY] +_validate_target = lambda target: isinstance(target, DataTableFacade) + +_READY = ".dash-spreadsheet:not(.dash-loading)" +_LOADING = ".dash-spreadsheet.dash-loading" +_ANY = ".dash-spreadsheet" +_TIMEOUT = 10 + + +class HoldKeyContext: + @preconditions(_validate_mixin, _validate_key) + def __init__(self, mixin, key): + self.mixin = mixin + self.key = key + + def __enter__(self): + ActionChains(self.mixin.driver).key_down(self.key).perform() + + def __exit__(self, type, value, traceback): + ActionChains(self.mixin.driver).key_up(self.key).perform() + + +class DataTableCellFacade(object): + @preconditions( + _validate_id, _validate_mixin, _validate_row, _validate_col, _validate_state + ) + def __init__(self, id, mixin, row, col, state=_ANY): + self.id = id + self.mixin = mixin + self.row = row + self.col = col + self.state = state + + def _get_cell_value(self): + return self.get().find_element_by_css_selector(".dash-cell-value") + + def click(self): + return self.get().click() + + def double_click(self): + ac = ActionChains(self.mixin.driver) + ac.move_to_element(self._get_cell_value()) + ac.pause(1) # sometimes experiencing incorrect behavior on scroll otherwise + ac.double_click() + return ac.perform() + + def get(self): + self.mixin._wait_for_table(self.id, self.state) + + return ( + self.mixin.find_element( + '#{} {} tbody td.dash-cell.column-{}[data-dash-row="{}"]'.format( + self.id, self.state, self.col, self.row + ) + ) + if isinstance(self.col, int) + else self.mixin.find_element( + '#{} {} tbody td.dash-cell[data-dash-column="{}"][data-dash-row="{}"]'.format( + self.id, self.state, self.col, self.row + ) + ) + ) + + def get_text(self): + el = self._get_cell_value() + + value = el.get_attribute("value") + return ( + value + if value is not None and value != "" + else el.get_attribute("innerHTML") + ) + + def is_active(self): + input = self.get().find_element_by_css_selector("input") + + return "focused" in input.get_attribute("class").split(" ") + + def is_focused(self): + cell = self.get() + + return "focused" in cell.get_attribute("class").split(" ") + + +class DataTableColumnFacade(object): + @preconditions(_validate_id, _validate_mixin, _validate_col_id, _validate_state) + def __init__(self, id, mixin, col_id, state=_ANY): + self.id = id + self.mixin = mixin + self.col_id = col_id + self.state = state + + @preconditions(_validate_row) + def get(self, row=0): + self.mixin._wait_for_table(self.id, self.state) + + return self.mixin.find_elements( + '#{} {} tbody tr th.dash-header[data-dash-column="{}"]'.format( + self.id, self.state, self.col_id + ) + )[row] + + @preconditions(_validate_row) + def hide(self, row=0): + self.get(row).find_element_by_css_selector(".column-header--hide").click() + + @preconditions(_validate_row) + def sort(self, row=0): + self.get(row).find_element_by_css_selector(".column-header--sort").click() + + def filter(self): + return self.mixin.find_element( + '#{} {} tbody tr th.dash-filter[data-dash-column="{}"]'.format( + self.id, self.state, self.col_id + ) + ).click() + + +class DataTableRowFacade(object): + @preconditions(_validate_id, _validate_mixin, _validate_row, _validate_state) + def __init__(self, id, mixin, row, state=_ANY): + self.id = id + self.mixin = mixin + self.row = row + self.state = state + + def delete(self): + return self.mixin.find_elements( + "#{} {} tbody tr td.dash-delete-cell".format(self.id, self.state) + )[self.row].click() + + def select(self): + return self.mixin.find_elements( + "#{} {} tbody tr td.dash-select-cell".format(self.id, self.state) + )[self.row].click() + + def is_selected(self): + return ( + self.mixin.find_elements( + "#{} {} tbody tr td.dash-select-cell".format(self.id, self.state) + )[self.row] + .find_element_by_css_selector("input") + .is_selected() + ) + + +class DataTablePagingActionFacade(object): + @preconditions(_validate_id, _validate_mixin, _validate_selector) + def __init__(self, id, mixin, selector): + self.id = id + self.mixin = mixin + self.selector = selector + + def click(self): + self.mixin._wait_for_table(self.id) + + return self.mixin.find_element("#{} {}".format(self.id, self.selector)).click() + + def exists(self): + self.mixin._wait_for_table(self.id) + + el = self.mixin.find_element("#{} {}".format(self.id, self.selector)) + + return el is not None and el.is_enabled() + + +class DataTablePagingCurrentFacade(object): + @preconditions(_validate_id, _validate_mixin) + def __init__(self, id, mixin): + self.id = id + self.mixin = mixin + + def click(self): + self.mixin._wait_for_table(self.id) + + return self.mixin.find_element("#{} input.current-page".format(self.id)).click() + + def get_value(self): + self.mixin._wait_for_table(self.id) + + return self.mixin.find_element( + "#{} input.current-page".format(self.id) + ).get_attribute("placeholder") + + +class DataTablePagingFacade(object): + @preconditions(_validate_id, _validate_mixin) + def __init__(self, id, mixin): + self.id = id + self.mixin = mixin + + self.current = DataTablePagingCurrentFacade(self.id, self.mixin) + self.first = DataTablePagingActionFacade( + self.id, self.mixin, "button.first-page" + ) + self.last = DataTablePagingActionFacade(self.id, self.mixin, "button.last-page") + self.next = DataTablePagingActionFacade(self.id, self.mixin, "button.next-page") + self.previous = DataTablePagingActionFacade( + self.id, self.mixin, "button.previous-page" + ) + + def exists(self): + self.mixin._wait_for_table(self.id) + + return len(self.mixin.find_elements(".previous-next-container")) != 0 + + +class DataTableFacade(object): + @preconditions(_validate_id, _validate_mixin) + def __init__(self, id, mixin): + self.id = id + self.mixin = mixin + + self.paging = DataTablePagingFacade(id, mixin) + + @preconditions(_validate_row, _validate_col, _validate_state) + def cell(self, row, col, state=_ANY): + return DataTableCellFacade(self.id, self.mixin, row, col, state) + + @preconditions(_validate_col_id, _validate_state) + def column(self, col_id, state=_ANY): + return DataTableColumnFacade(self.id, self.mixin, col_id, state) + + @preconditions(_validate_row, _validate_state) + def row(self, row, state=_ANY): + return DataTableRowFacade(self.id, self.mixin, row, state) + + def is_ready(self): + return self.mixin._wait_for_table(self.id, _READY) + + def is_loading(self): + return self.mixin._wait_for_table(self.id, _LOADING) + + +class DataTableMixin(object): + @preconditions(_validate_id, _validate_state) + def _wait_for_table(self, id, state=_ANY): + return WebDriverWait(self.driver, _TIMEOUT).until( + EC.presence_of_element_located( + (By.CSS_SELECTOR, "#{} {}".format(id, state)) + ) + ) + + @preconditions(_validate_id) + def table(self, id): + return DataTableFacade(id, self) + + def copy(self): + with self.hold(Keys.CONTROL): + self.send_keys("c") + + def paste(self): + with self.hold(Keys.CONTROL): + self.send_keys("v") + + @preconditions(_validate_key) + def hold(self, key): + return HoldKeyContext(self, key) + + def get_selected_text(self): + return self.driver.execute_script("return window.getSelection().toString()") + + @preconditions(_validate_keys) + def send_keys(self, keys): + self.driver.switch_to.active_element.send_keys(keys) + + +class DataTableComposite(Browser, DataTableMixin): + def __init__(self, server, **kwargs): + super(DataTableComposite, self).__init__(**kwargs) + self.server = server + + self.READY = _READY + self.LOADING = _LOADING + self.ANY = _ANY + + def start_server(self, app, **kwargs): + """start the local server with app""" + + # start server with app and pass Dash arguments + self.server(app, **kwargs) + + # set the default server_url, it implicitly call wait_for_page + self.server_url = self.server.url + + +@pytest.fixture +def test(request, dash_thread_server, tmpdir): + with DataTableComposite( + dash_thread_server, + browser=request.config.getoption("webdriver"), + remote=request.config.getoption("remote"), + remote_url=request.config.getoption("remote_url"), + headless=request.config.getoption("headless"), + options=request.config.hook.pytest_setup_options(), + download_path=tmpdir.mkdir("download").strpath, + percy_assets_root=request.config.getoption("percy_assets"), + percy_finalize=request.config.getoption("nopercyfinalize"), + percy_run=False, + ) as dc: + yield dc diff --git a/tests/selenium/test_basic_copy_paste.py b/tests/selenium/test_basic_copy_paste.py new file mode 100644 index 000000000..c35cb1d07 --- /dev/null +++ b/tests/selenium/test_basic_copy_paste.py @@ -0,0 +1,237 @@ +import dash +from dash.dependencies import Input, Output, State +from dash.exceptions import PreventUpdate + +import dash_html_components as html +from dash_table import DataTable + +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.common.action_chains import ActionChains + +import pandas as pd + +url = "https://github.com/plotly/datasets/raw/master/" "26k-consumer-complaints.csv" +rawDf = pd.read_csv(url) +df = rawDf.to_dict("rows") + + +def get_app(): + app = dash.Dash(__name__) + + app.layout = html.Div( + [ + DataTable( + id="table", + data=df[0:250], + columns=[ + {"name": i, "id": i, "hideable": i == "Complaint ID"} + for i in rawDf.columns + ], + editable=True, + sort_action="native", + include_headers_on_copy_paste=True, + ), + DataTable( + id="table2", + data=df[0:10], + columns=[ + {"name": i, "id": i, "deletable": True} for i in rawDf.columns + ], + editable=True, + sort_action="native", + include_headers_on_copy_paste=True, + ), + ] + ) + + @app.callback( + Output("table", "data"), + [Input("table", "data_timestamp")], + [State("table", "data"), State("table", "data_previous")], + ) + # pylint: disable=unused-argument + def update_data(timestamp, current, previous): + # pylint: enable=unused-argument + if timestamp is None or current is None or previous is None: + raise PreventUpdate + + modified = False + if len(current) == len(previous): + for (i, datum) in enumerate(current): + previous_datum = previous[i] + + if datum["Unnamed: 0"] != previous_datum["Unnamed: 0"]: + datum["Complaint ID"] = "MODIFIED" + modified = True + + if modified: + return current + else: + raise PreventUpdate + + return app + + +def test_tbcp001_copy_paste_callback(test): + test.start_server(get_app()) + + target = test.table("table") + target.cell(0, 0).click() + + test.copy() + target.cell(1, 0).click() + test.paste() + + assert target.cell(1, 0).get_text() == "0" + assert target.cell(1, 1).get_text() == "MODIFIED" + + +def test_tbcp002_sorted_copy_paste_callback(test): + test.start_server(get_app()) + + target = test.table("table") + target.column(rawDf.columns[2]).sort() + + assert target.cell(0, 0).get_text() == "11" + + target.cell(0, 0).click() + + test.copy() + target.cell(1, 0).click() + test.paste() + + assert target.cell(1, 0).get_text() == "11" + assert target.cell(1, 1).get_text() == "MODIFIED" + + target.cell(1, 1).click() + + test.copy() + target.cell(2, 1).click() + test.paste() + + assert target.cell(1, 0).get_text() == "11" + assert target.cell(2, 1).get_text() == "MODIFIED" + + +def test_tbcp003_copy_multiple_rows(test): + test.start_server(get_app()) + + target = test.table("table") + with test.hold(Keys.SHIFT): + target.cell(0, 0).click() + target.cell(2, 0).click() + + test.copy() + target.cell(3, 0).click() + test.paste() + + for i in range(3): + assert target.cell(i + 3, 0).get_text() == target.cell(i, 0).get_text() + assert target.cell(i + 3, 1).get_text() == "MODIFIED" + + +def test_tbcp004_copy_9_and_10(test): + test.start_server(get_app()) + + source = test.table("table") + target = test.table("table2") + + source.cell(9, 0).click() + with test.hold(Keys.SHIFT): + ActionChains(test.driver).send_keys(Keys.DOWN).perform() + + test.copy() + target.cell(0, 0).click() + test.paste() + + for row in range(2): + for col in range(1): + assert ( + target.cell(row, col).get_text() == source.cell(row + 9, col).get_text() + ) + + +def test_tbcp005_copy_multiple_rows_and_columns(test): + test.start_server(get_app()) + + target = test.table("table") + + target.cell(0, 1).click() + with test.hold(Keys.SHIFT): + target.cell(2, 2).click() + + test.copy() + target.cell(3, 1).click() + test.paste() + + for row in range(3): + for col in range(1, 3): + assert ( + target.cell(row + 3, col).get_text() == target.cell(row, col).get_text() + ) + + +def test_tbcp006_copy_paste_between_tables(test): + test.start_server(get_app()) + + source = test.table("table") + target = test.table("table2") + + source.cell(10, 0).click() + with test.hold(Keys.SHIFT): + source.cell(13, 3).click() + + test.copy() + target.cell(0, 0).click() + test.paste() + + for row in range(4): + for col in range(4): + assert ( + source.cell(row + 10, col).get_text() + == target.cell(row, col).get_text() + ) + + +def test_tbcp007_copy_paste_with_hidden_column(test): + test.start_server(get_app()) + + target = test.table("table") + + target.column("Complaint ID").hide() + target.cell(0, 0).click() + with test.hold(Keys.SHIFT): + target.cell(2, 2).click() + + test.copy() + target.cell(3, 1).click() + test.paste() + + for row in range(3): + for col in range(3): + assert ( + target.cell(row, col).get_text() + == target.cell(row + 3, col + 1).get_text() + ) + + +def test_tbcp008_copy_paste_between_tables_with_hidden_columns(test): + test.start_server(get_app()) + + target = test.table("table") + + target.column("Complaint ID").hide() + target.cell(10, 0).click() + with test.hold(Keys.SHIFT): + target.cell(13, 2).click() + + test.copy() + target.cell(0, 0).click() + test.paste() + + for row in range(4): + for col in range(3): + assert ( + target.cell(row + 10, col).get_text() + == target.cell(row, col).get_text() + ) diff --git a/tests/selenium/test_basic_operations.py b/tests/selenium/test_basic_operations.py new file mode 100644 index 000000000..ed51645c1 --- /dev/null +++ b/tests/selenium/test_basic_operations.py @@ -0,0 +1,343 @@ +import dash + +from dash_table import DataTable + +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.common.action_chains import ActionChains + +import pandas as pd +import pytest + +url = "https://github.com/plotly/datasets/raw/master/" "26k-consumer-complaints.csv" +rawDf = pd.read_csv(url) +df = rawDf.to_dict("rows") + + +def get_app(): + app = dash.Dash(__name__) + + app.layout = DataTable( + id="table", + columns=[{"name": i, "id": i} for i in rawDf.columns], + data=df, + editable=True, + filter_action="native", + fixed_columns={"headers": True}, + fixed_rows={"headers": True}, + page_action="native", + row_deletable=True, + row_selectable=True, + sort_action="native", + ) + + return app + + +def test_tbst001_get_cell(test): + test.start_server(get_app()) + + target = test.table("table") + + assert target.cell(0, 0).get_text() == "0" + target.paging.next.click() + assert target.cell(0, 0).get_text() == "250" + + +def test_tbst002_select_all_text(test): + test.start_server(get_app()) + + target = test.table("table") + + target.cell(0, 1).click() + + assert target.cell(0, 1).get_text() == test.get_selected_text() + + +# https://github.com/plotly/dash-table/issues/50 +def test_tbst003_edit_on_enter(test): + test.start_server(get_app()) + + target = test.table("table") + + target.cell(249, 0).click() + test.send_keys("abc" + Keys.ENTER) + + assert target.cell(249, 0).get_text() == "abc" + + +# https://github.com/plotly/dash-table/issues/107 +def test_tbst004_edit_on_tab(test): + test.start_server(get_app()) + + target = test.table("table") + + target.cell(249, 0).click() + test.send_keys("abc" + Keys.TAB) + + assert target.cell(249, 0).get_text() == "abc" + + +def test_tbst005_edit_last_row_on_click_outside(test): + test.start_server(get_app()) + + target = test.table("table") + + target.cell(249, 0).click() + test.send_keys("abc") + target.cell(248, 0).click() + + assert target.cell(249, 0).get_text() == "abc" + + +# https://github.com/plotly/dash-table/issues/141 +def test_tbst006_focused_arrow_left(test): + test.start_server(get_app()) + + target = test.table("table") + + target.cell(249, 1).click() + test.send_keys("abc" + Keys.LEFT) + + assert target.cell(249, 1).get_text() == "abc" + assert target.cell(249, 0).is_focused() + + +# https://github.com/plotly/dash-table/issues/141 +def test_tbst007_active_focused_arrow_right(test): + test.start_server(get_app()) + + target = test.table("table") + + target.cell(249, 0).click() + test.send_keys("abc" + Keys.RIGHT) + + assert target.cell(249, 0).get_text() == "abc" + assert target.cell(249, 1).is_focused() + + +# https://github.com/plotly/dash-table/issues/141 +def test_tbst008_active_focused_arrow_up(test): + test.start_server(get_app()) + + target = test.table("table") + + target.cell(249, 0).click() + test.send_keys("abc" + Keys.UP) + + assert target.cell(249, 0).get_text() == "abc" + assert target.cell(248, 0).is_focused() + + +# https://github.com/plotly/dash-table/issues/141 +def test_tbst009_active_focused_arrow_down(test): + test.start_server(get_app()) + + target = test.table("table") + + target.cell(249, 0).click() + test.send_keys("abc" + Keys.DOWN) + + assert target.cell(249, 0).get_text() == "abc" + assert target.cell(249, 0).is_focused() + + +def test_tbst010_active_with_dblclick(test): + test.start_server(get_app()) + + target = test.table("table") + + target.cell(0, 0).double_click() + assert target.cell(0, 0).is_active() + assert target.cell(0, 0).get_text() == test.get_selected_text() + + +def test_tbst011_delete_row(test): + test.start_server(get_app()) + + target = test.table("table") + + text01 = target.cell(1, 0).get_text() + target.row(0).delete() + + assert target.cell(0, 0).get_text() == text01 + + +def test_tbst012_delete_sorted_row(test): + test.start_server(get_app()) + + target = test.table("table") + + target.column(rawDf.columns[0]).sort() # None -> ASC + target.column(rawDf.columns[0]).sort() # ASC -> DESC + + text01 = target.cell(1, 0).get_text() + target.row(0).delete() + + assert target.cell(0, 0).get_text() == text01 + + +def test_tbst013_select_row(test): + test.start_server(get_app()) + + target = test.table("table") + + target.row(0).select() + + assert target.row(0).is_selected() + + +def test_tbst014_selected_sorted_row(test): + test.start_server(get_app()) + + target = test.table("table") + + target.column(rawDf.columns[0]).sort() # None -> ASC + target.column(rawDf.columns[0]).sort() # ASC -> DESC + target.row(0).select() + + assert target.row(0).is_selected() + + +def test_tbst015_selected_row_respects_sort(test): + test.start_server(get_app()) + + target = test.table("table") + + target.row(0).select() + + assert target.row(0).is_selected() + + target.column(rawDf.columns[0]).sort() # None -> ASC + target.column(rawDf.columns[0]).sort() # ASC -> DESC + + assert not target.row(0).is_selected() + + target.column(rawDf.columns[0]).sort() # DESC -> None + + assert target.row(0).is_selected() + + +def test_tbst016_delete_cell(test): + test.start_server(get_app()) + + target = test.table("table") + + target.cell(0, 1).click() + test.send_keys(Keys.BACKSPACE) + test.send_keys(Keys.ENTER) + + assert target.cell(0, 1).get_text() == "" + + +@pytest.mark.skip(reason="https://github.com/plotly/dash-table/issues/700") +def test_tbst017_delete_cell_updates_while_selected(test): + test.start_server(get_app()) + + target = test.table("table") + + target.cell(0, 1).click() + test.send_keys(Keys.BACKSPACE) + + assert target.cell(0, 1).get_text() == "" + + +def test_tbst018_delete_multiple_cells(test): + test.start_server(get_app()) + + target = test.table("table") + + target.cell(0, 1).click() + with test.hold(Keys.SHIFT): + ActionChains(test.driver).send_keys(Keys.DOWN).send_keys(Keys.RIGHT).perform() + + ActionChains(test.driver).send_keys(Keys.BACKSPACE).send_keys(Keys.ENTER).perform() + + for row in range(2): + for col in range(1, 3): + assert target.cell(row, col).get_text() == "" + + +@pytest.mark.skip(reason="https://github.com/plotly/dash-table/issues/700") +def test_tbst019_delete_multiple_cells_while_selected(test): + test.start_server(get_app()) + + target = test.table("table") + + target.cell(0, 1).click() + with test.hold(Keys.SHIFT): + ActionChains(test.driver).send_keys(Keys.DOWN).send_keys(Keys.RIGHT).perform() + + ActionChains(test.driver).send_keys(Keys.BACKSPACE).perform() + + for row in range(2): + for col in range(1, 3): + assert target.cell(row, col).get_text() == "" + + +def test_tbst020_sorted_table_delete_cell(test): + test.start_server(get_app()) + + target = test.table("table") + + target.column(rawDf.columns[0]).sort() # None -> ASC + target.column(rawDf.columns[0]).sort() # ASC -> DESC + + target.cell(0, 1).click() + test.send_keys(Keys.BACKSPACE) + test.send_keys(Keys.ENTER) + + assert target.cell(0, 1).get_text() == "" + + +@pytest.mark.skip(reason="https://github.com/plotly/dash-table/issues/700") +def test_tbst021_sorted_table_delete_cell_updates_while_selected(test): + test.start_server(get_app()) + + target = test.table("table") + + target.column(rawDf.columns[0]).sort() # None -> ASC + target.column(rawDf.columns[0]).sort() # ASC -> DESC + + target.cell(0, 1).click() + test.send_keys(Keys.BACKSPACE) + + assert target.cell(0, 1).get_text() == "" + + +def test_tbst022_sorted_table_delete_multiple_cells(test): + test.start_server(get_app()) + + target = test.table("table") + + target.column(rawDf.columns[0]).sort() # None -> ASC + target.column(rawDf.columns[0]).sort() # ASC -> DESC + + target.cell(0, 1).click() + with test.hold(Keys.SHIFT): + ActionChains(test.driver).send_keys(Keys.DOWN).send_keys(Keys.RIGHT).perform() + + ActionChains(test.driver).send_keys(Keys.BACKSPACE).send_keys(Keys.ENTER).perform() + + for row in range(2): + for col in range(1, 3): + assert target.cell(row, col).get_text() == "" + + +@pytest.mark.skip(reason="https://github.com/plotly/dash-table/issues/700") +def test_tbst023_sorted_table_delete_multiple_cells_while_selected(test): + test.start_server(get_app()) + + target = test.table("table") + + target.column(rawDf.columns[0]).sort() # None -> ASC + target.column(rawDf.columns[0]).sort() # ASC -> DESC + + target.cell(0, 1).click() + with test.hold(Keys.SHIFT): + ActionChains(test.driver).send_keys(Keys.DOWN).send_keys(Keys.RIGHT).perform() + + ActionChains(test.driver).send_keys(Keys.BACKSPACE).perform() + + for row in range(2): + for col in range(1, 3): + assert target.cell(row, col).get_text() == "" diff --git a/tests/selenium/test_derived_props.py b/tests/selenium/test_derived_props.py new file mode 100644 index 000000000..4b8465449 --- /dev/null +++ b/tests/selenium/test_derived_props.py @@ -0,0 +1,567 @@ +import dash +from dash.dependencies import Input, Output + +import dash_html_components as html +from dash_table import DataTable + +from selenium.webdriver.common.keys import Keys + +import json +import pandas as pd + +url = "https://github.com/plotly/datasets/raw/master/" "26k-consumer-complaints.csv" +rawDf = pd.read_csv(url, nrows=100) +rawDf["id"] = rawDf.index + 3000 + +df = rawDf.to_dict("rows") + +props = [ + "active_cell", + "start_cell", + "end_cell", + "selected_cells", + "selected_rows", + "selected_row_ids", + "derived_viewport_selected_rows", + "derived_viewport_selected_row_ids", + "derived_virtual_selected_rows", + "derived_virtual_selected_row_ids", + "derived_viewport_indices", + "derived_viewport_row_ids", + "derived_virtual_indices", + "derived_virtual_row_ids", +] + + +def get_app(): + app = dash.Dash(__name__) + + app.layout = html.Div( + [ + DataTable( + id="table", + columns=[{"name": i, "id": i} for i in rawDf.columns], + data=df, + editable=True, + filter_action="native", + fixed_columns={"headers": True}, + fixed_rows={"headers": True}, + page_action="native", + page_size=10, + row_deletable=True, + row_selectable=True, + sort_action="native", + ), + html.Div(id="props_container", children=["Nothing yet"]), + ] + ) + + @app.callback( + Output("props_container", "children"), [Input("table", prop) for prop in props], + ) + def show_props(*args): + # return 'Something yet!' + # print('show props') + return html.Table( + [ + html.Tr( + [ + html.Td(prop), + html.Td( + json.dumps(val) if val is not None else "None", id=prop, + ), + ] + ) + for prop, val in zip(props, args) + ] + ) + + return app + + +def test_tdrp001_select_rows(test): + test.start_server(get_app()) + + target = test.table("table") + + target.row(0).select() + target.row(1).select() + + assert test.find_element("#active_cell").get_attribute("innerHTML") in [ + "None", + json.dumps([]), + ] + assert test.find_element("#start_cell").get_attribute("innerHTML") in [ + "None", + json.dumps([]), + ] + assert test.find_element("#end_cell").get_attribute("innerHTML") in [ + "None", + json.dumps([]), + ] + assert test.find_element("#selected_cells").get_attribute("innerHTML") in [ + "None", + json.dumps([]), + ] + assert test.find_element("#selected_rows").get_attribute("innerHTML") == json.dumps( + list(range(2)) + ) + assert test.find_element("#selected_row_ids").get_attribute( + "innerHTML" + ) == json.dumps(list(range(3000, 3002))) + + assert test.find_element("#derived_viewport_selected_rows").get_attribute( + "innerHTML" + ) == json.dumps(list(range(2))) + assert test.find_element("#derived_viewport_selected_row_ids").get_attribute( + "innerHTML" + ) == json.dumps(list(range(3000, 3002))) + assert test.find_element("#derived_viewport_indices").get_attribute( + "innerHTML" + ) == json.dumps(list(range(10))) + assert test.find_element("#derived_viewport_row_ids").get_attribute( + "innerHTML" + ) == json.dumps(list(range(3000, 3010))) + + assert test.find_element("#derived_virtual_selected_rows").get_attribute( + "innerHTML" + ) == json.dumps(list(range(2))) + assert test.find_element("#derived_virtual_selected_row_ids").get_attribute( + "innerHTML" + ) == json.dumps(list(range(3000, 3002))) + assert test.find_element("#derived_virtual_indices").get_attribute( + "innerHTML" + ) == json.dumps(list(range(100))) + assert test.find_element("#derived_virtual_row_ids").get_attribute( + "innerHTML" + ) == json.dumps(list(range(3000, 3100))) + + +def test_tdrp002_select_cell(test): + test.start_server(get_app()) + + target = test.table("table") + + target.cell(0, 0).click() + + active = dict(row=0, column=0, column_id=rawDf.columns[0], row_id=3000) + + assert test.find_element("#active_cell").get_attribute("innerHTML") == json.dumps( + active + ) + assert test.find_element("#start_cell").get_attribute("innerHTML") == json.dumps( + active + ) + assert test.find_element("#end_cell").get_attribute("innerHTML") == json.dumps( + active + ) + assert test.find_element("#selected_cells").get_attribute( + "innerHTML" + ) == json.dumps([active]) + assert test.find_element("#selected_rows").get_attribute("innerHTML") in [ + "None", + json.dumps([]), + ] + assert test.find_element("#selected_row_ids").get_attribute("innerHTML") in [ + "None", + json.dumps([]), + ] + + assert test.find_element("#derived_viewport_selected_rows").get_attribute( + "innerHTML" + ) in ["None", json.dumps([])] + assert test.find_element("#derived_viewport_selected_row_ids").get_attribute( + "innerHTML" + ) in ["None", json.dumps([])] + assert test.find_element("#derived_viewport_indices").get_attribute( + "innerHTML" + ) == json.dumps(list(range(10))) + assert test.find_element("#derived_viewport_row_ids").get_attribute( + "innerHTML" + ) == json.dumps(list(range(3000, 3010))) + + assert test.find_element("#derived_virtual_selected_rows").get_attribute( + "innerHTML" + ) in ["None", json.dumps([])] + assert test.find_element("#derived_virtual_selected_row_ids").get_attribute( + "innerHTML" + ) in ["None", json.dumps([])] + assert test.find_element("#derived_virtual_indices").get_attribute( + "innerHTML" + ) == json.dumps(list(range(100))) + assert test.find_element("#derived_virtual_row_ids").get_attribute( + "innerHTML" + ) == json.dumps(list(range(3000, 3100))) + + +def test_tdrp003_select_cells(test): + test.start_server(get_app()) + + target = test.table("table") + + target.cell(0, 0).click() + with test.hold(Keys.SHIFT): + test.send_keys(Keys.DOWN + Keys.DOWN + Keys.RIGHT + Keys.RIGHT) + + active = dict(row=0, column=0, column_id=rawDf.columns[0], row_id=3000) + + selected = [] + for row in range(3): + for col in range(3): + selected.append( + dict( + row=row, + column=col, + column_id=rawDf.columns[col], + row_id=row + 3000, + ) + ) + + assert test.find_element("#active_cell").get_attribute("innerHTML") == json.dumps( + active + ) + assert test.find_element("#start_cell").get_attribute("innerHTML") == json.dumps( + selected[0] + ) + assert test.find_element("#end_cell").get_attribute("innerHTML") == json.dumps( + selected[-1] + ) + assert test.find_element("#selected_cells").get_attribute( + "innerHTML" + ) == json.dumps(selected) + assert test.find_element("#selected_rows").get_attribute("innerHTML") in [ + "None", + json.dumps([]), + ] + assert test.find_element("#selected_row_ids").get_attribute("innerHTML") in [ + "None", + json.dumps([]), + ] + + assert test.find_element("#derived_viewport_selected_rows").get_attribute( + "innerHTML" + ) in ["None", json.dumps([])] + assert test.find_element("#derived_viewport_selected_row_ids").get_attribute( + "innerHTML" + ) in ["None", json.dumps([])] + assert test.find_element("#derived_viewport_indices").get_attribute( + "innerHTML" + ) == json.dumps(list(range(10))) + assert test.find_element("#derived_viewport_row_ids").get_attribute( + "innerHTML" + ) == json.dumps(list(range(3000, 3010))) + + assert test.find_element("#derived_virtual_selected_rows").get_attribute( + "innerHTML" + ) in ["None", json.dumps([])] + assert test.find_element("#derived_virtual_selected_row_ids").get_attribute( + "innerHTML" + ) in ["None", json.dumps([])] + assert test.find_element("#derived_virtual_indices").get_attribute( + "innerHTML" + ) == json.dumps(list(range(100))) + assert test.find_element("#derived_virtual_row_ids").get_attribute( + "innerHTML" + ) == json.dumps(list(range(3000, 3100))) + + # reduce selection + with test.hold(Keys.SHIFT): + test.send_keys(Keys.UP + Keys.LEFT) + + selected = [] + for row in range(2): + for col in range(2): + selected.append( + dict( + row=row, + column=col, + column_id=rawDf.columns[col], + row_id=row + 3000, + ) + ) + + assert test.find_element("#active_cell").get_attribute("innerHTML") == json.dumps( + active + ) + assert test.find_element("#start_cell").get_attribute("innerHTML") == json.dumps( + selected[0] + ) + assert test.find_element("#end_cell").get_attribute("innerHTML") == json.dumps( + selected[-1] + ) + assert test.find_element("#selected_cells").get_attribute( + "innerHTML" + ) == json.dumps(selected) + assert test.find_element("#selected_rows").get_attribute("innerHTML") in [ + "None", + json.dumps([]), + ] + assert test.find_element("#selected_row_ids").get_attribute("innerHTML") in [ + "None", + json.dumps([]), + ] + + assert test.find_element("#derived_viewport_selected_rows").get_attribute( + "innerHTML" + ) in ["None", json.dumps([])] + assert test.find_element("#derived_viewport_selected_row_ids").get_attribute( + "innerHTML" + ) in ["None", json.dumps([])] + assert test.find_element("#derived_viewport_indices").get_attribute( + "innerHTML" + ) == json.dumps(list(range(10))) + assert test.find_element("#derived_viewport_row_ids").get_attribute( + "innerHTML" + ) == json.dumps(list(range(3000, 3010))) + + assert test.find_element("#derived_virtual_selected_rows").get_attribute( + "innerHTML" + ) in ["None", json.dumps([])] + assert test.find_element("#derived_virtual_selected_row_ids").get_attribute( + "innerHTML" + ) in ["None", json.dumps([])] + assert test.find_element("#derived_virtual_indices").get_attribute( + "innerHTML" + ) == json.dumps(list(range(100))) + assert test.find_element("#derived_virtual_row_ids").get_attribute( + "innerHTML" + ) == json.dumps(list(range(3000, 3100))) + + +def test_tdrp004_navigate_selected_cells(test): + test.start_server(get_app()) + + target = test.table("table") + + target.cell(0, 0).click() + with test.hold(Keys.SHIFT): + test.send_keys(Keys.DOWN + Keys.DOWN + Keys.RIGHT + Keys.RIGHT) + + selected = [] + for row in range(3): + for col in range(3): + selected.append( + dict( + row=row, + column=col, + column_id=rawDf.columns[col], + row_id=row + 3000, + ) + ) + + for row in range(3): + for col in range(3): + active = dict( + row=row, column=col, column_id=rawDf.columns[col], row_id=row + 3000, + ) + + assert test.find_element("#active_cell").get_attribute( + "innerHTML" + ) == json.dumps(active) + assert test.find_element("#start_cell").get_attribute( + "innerHTML" + ) == json.dumps(selected[0]) + assert test.find_element("#end_cell").get_attribute( + "innerHTML" + ) == json.dumps(selected[-1]) + assert test.find_element("#selected_cells").get_attribute( + "innerHTML" + ) == json.dumps(selected) + assert test.find_element("#selected_rows").get_attribute("innerHTML") in [ + "None", + json.dumps([]), + ] + assert test.find_element("#selected_row_ids").get_attribute( + "innerHTML" + ) in ["None", json.dumps([])] + + assert test.find_element("#derived_viewport_selected_rows").get_attribute( + "innerHTML" + ) in ["None", json.dumps([])] + assert test.find_element( + "#derived_viewport_selected_row_ids" + ).get_attribute("innerHTML") in ["None", json.dumps([])] + assert test.find_element("#derived_viewport_indices").get_attribute( + "innerHTML" + ) == json.dumps(list(range(10))) + assert test.find_element("#derived_viewport_row_ids").get_attribute( + "innerHTML" + ) == json.dumps(list(range(3000, 3010))) + + assert test.find_element("#derived_virtual_selected_rows").get_attribute( + "innerHTML" + ) in ["None", json.dumps([])] + assert test.find_element("#derived_virtual_selected_row_ids").get_attribute( + "innerHTML" + ) in ["None", json.dumps([])] + assert test.find_element("#derived_virtual_indices").get_attribute( + "innerHTML" + ) == json.dumps(list(range(100))) + assert test.find_element("#derived_virtual_row_ids").get_attribute( + "innerHTML" + ) == json.dumps(list(range(3000, 3100))) + + test.send_keys(Keys.TAB) + + +def test_tdrp005_filtered_and_sorted_row_select(test): + test.start_server(get_app()) + + target = test.table("table") + + target.row(0).select() + target.row(1).select() + target.row(2).select() + + assert test.find_element("#active_cell").get_attribute("innerHTML") in [ + "None", + json.dumps([]), + ] + assert test.find_element("#start_cell").get_attribute("innerHTML") in [ + "None", + json.dumps([]), + ] + assert test.find_element("#end_cell").get_attribute("innerHTML") in [ + "None", + json.dumps([]), + ] + assert test.find_element("#selected_cells").get_attribute("innerHTML") in [ + "None", + json.dumps([]), + ] + assert test.find_element("#selected_rows").get_attribute("innerHTML") == json.dumps( + list(range(3)) + ) + assert test.find_element("#selected_row_ids").get_attribute( + "innerHTML" + ) == json.dumps(list(range(3000, 3003))) + + assert test.find_element("#derived_viewport_selected_rows").get_attribute( + "innerHTML" + ) == json.dumps(list(range(3))) + assert test.find_element("#derived_viewport_selected_row_ids").get_attribute( + "innerHTML" + ) == json.dumps(list(range(3000, 3003))) + assert test.find_element("#derived_viewport_indices").get_attribute( + "innerHTML" + ) == json.dumps(list(range(10))) + assert test.find_element("#derived_viewport_row_ids").get_attribute( + "innerHTML" + ) == json.dumps(list(range(3000, 3010))) + + assert test.find_element("#derived_virtual_selected_rows").get_attribute( + "innerHTML" + ) == json.dumps(list(range(3))) + assert test.find_element("#derived_virtual_selected_row_ids").get_attribute( + "innerHTML" + ) == json.dumps(list(range(3000, 3003))) + assert test.find_element("#derived_virtual_indices").get_attribute( + "innerHTML" + ) == json.dumps(list(range(100))) + assert test.find_element("#derived_virtual_row_ids").get_attribute( + "innerHTML" + ) == json.dumps(list(range(3000, 3100))) + + target.column(rawDf.columns[0]).filter() + test.send_keys("is even" + Keys.ENTER) + + assert test.find_element("#active_cell").get_attribute("innerHTML") in [ + "None", + json.dumps([]), + ] + assert test.find_element("#start_cell").get_attribute("innerHTML") in [ + "None", + json.dumps([]), + ] + assert test.find_element("#end_cell").get_attribute("innerHTML") in [ + "None", + json.dumps([]), + ] + assert test.find_element("#selected_cells").get_attribute("innerHTML") in [ + "None", + json.dumps([]), + ] + assert test.find_element("#selected_rows").get_attribute("innerHTML") == json.dumps( + list(range(3)) + ) + assert test.find_element("#selected_row_ids").get_attribute( + "innerHTML" + ) == json.dumps(list(range(3000, 3003))) + + assert test.find_element("#derived_viewport_selected_rows").get_attribute( + "innerHTML" + ) == json.dumps(list(range(0, 2))) + assert test.find_element("#derived_viewport_selected_row_ids").get_attribute( + "innerHTML" + ) == json.dumps(list(range(3000, 3003, 2))) + assert test.find_element("#derived_viewport_indices").get_attribute( + "innerHTML" + ) == json.dumps(list(range(0, 20, 2))) + assert test.find_element("#derived_viewport_row_ids").get_attribute( + "innerHTML" + ) == json.dumps(list(range(3000, 3020, 2))) + + assert test.find_element("#derived_virtual_selected_rows").get_attribute( + "innerHTML" + ) == json.dumps(list(range(0, 2))) + assert test.find_element("#derived_virtual_selected_row_ids").get_attribute( + "innerHTML" + ) == json.dumps(list(range(3000, 3003, 2))) + assert test.find_element("#derived_virtual_indices").get_attribute( + "innerHTML" + ) == json.dumps(list(range(0, 100, 2))) + assert test.find_element("#derived_virtual_row_ids").get_attribute( + "innerHTML" + ) == json.dumps(list(range(3000, 3100, 2))) + + target.column(rawDf.columns[0]).sort() # None -> ASC + target.column(rawDf.columns[0]).sort() # ASC -> DESC + + assert test.find_element("#active_cell").get_attribute("innerHTML") in [ + "None", + json.dumps([]), + ] + assert test.find_element("#start_cell").get_attribute("innerHTML") in [ + "None", + json.dumps([]), + ] + assert test.find_element("#end_cell").get_attribute("innerHTML") in [ + "None", + json.dumps([]), + ] + assert test.find_element("#selected_cells").get_attribute("innerHTML") in [ + "None", + json.dumps([]), + ] + assert test.find_element("#selected_rows").get_attribute("innerHTML") == json.dumps( + list(range(3)) + ) + assert test.find_element("#selected_row_ids").get_attribute( + "innerHTML" + ) == json.dumps(list(range(3000, 3003))) + + assert test.find_element("#derived_viewport_selected_rows").get_attribute( + "innerHTML" + ) in ["None", json.dumps([])] + assert test.find_element("#derived_viewport_selected_row_ids").get_attribute( + "innerHTML" + ) in ["None", json.dumps([])] + assert test.find_element("#derived_viewport_indices").get_attribute( + "innerHTML" + ) == json.dumps(list(range(80, 100, 2))[::-1]) + assert test.find_element("#derived_viewport_row_ids").get_attribute( + "innerHTML" + ) == json.dumps(list(range(3080, 3100, 2))[::-1]) + + assert test.find_element("#derived_virtual_selected_rows").get_attribute( + "innerHTML" + ) == json.dumps(list(range(48, 50))[::-1]) + assert test.find_element("#derived_virtual_selected_row_ids").get_attribute( + "innerHTML" + ) == json.dumps(list(range(3000, 3003, 2))) + assert test.find_element("#derived_virtual_indices").get_attribute( + "innerHTML" + ) == json.dumps(list(range(0, 100, 2))[::-1]) + assert test.find_element("#derived_virtual_row_ids").get_attribute( + "innerHTML" + ) == json.dumps(list(range(3000, 3100, 2))[::-1]) diff --git a/tests/selenium/test_editable.py b/tests/selenium/test_editable.py new file mode 100644 index 000000000..b25b40a76 --- /dev/null +++ b/tests/selenium/test_editable.py @@ -0,0 +1,178 @@ +import dash +from dash.dependencies import Input, Output +from dash.exceptions import PreventUpdate + +import dash_core_components as dcc +import dash_html_components as html +from dash_table import DataTable + +from multiprocessing import Lock +from selenium.webdriver.common.keys import Keys + +import pandas as pd + +url = "https://github.com/plotly/datasets/raw/master/" "26k-consumer-complaints.csv" +rawDf = pd.read_csv(url) +df = rawDf.to_dict("rows") + + +def get_app_and_locks(): + app = dash.Dash(__name__) + + app.layout = html.Div( + [ + dcc.Input(id="input"), + html.Button(["Blocking"], id="blocking"), + html.Button(["Non Blocking"], id="non-blocking"), + DataTable( + id="table", + columns=[{"name": i, "id": i} for i in rawDf.columns], + data=df, + editable=True, + filter_action="native", + fixed_columns={"headers": True}, + fixed_rows={"headers": True}, + page_action="native", + row_deletable=True, + row_selectable=True, + sort_action="native", + ), + ] + ) + + blocking_lock = Lock() + non_blocking_lock = Lock() + + @app.callback( + Output("table", "style_cell_conditional"), [Input("non-blocking", "n_clicks")], + ) + def non_blocking_callback(clicks): + if clicks is None: + raise PreventUpdate + + with non_blocking_lock: + return [] + + @app.callback(Output("table", "data"), [Input("blocking", "n_clicks")]) + def blocking_callback(clicks): + if clicks is None: + raise PreventUpdate + + with blocking_lock: + return df + + return app, blocking_lock, non_blocking_lock + + +def test_tedi001_loading_on_data_change(test): + app, blocking, non_blocking = get_app_and_locks() + + test.start_server(app) + + target = test.table("table") + + with blocking: + test.find_element("#blocking").click() + target.is_loading() + target.cell(0, 0).click() + assert len(target.cell(0, 0).get().find_elements_by_css_selector("input")) == 0 + + target.is_ready() + assert target.cell(0, 0).get().find_element_by_css_selector("input") is not None + + +def test_tedi002_ready_on_non_data_change(test): + app, blocking, non_blocking = get_app_and_locks() + + test.start_server(app) + + target = test.table("table") + + with blocking: + test.find_element("#non-blocking").click() + target.is_ready() + target.cell(0, 0).click() + assert target.cell(0, 0).get().find_element_by_css_selector("input") is not None + + target.is_ready() + assert target.cell(0, 0).get().find_element_by_css_selector("input") is not None + + +def test_tedi003_does_not_steal_focus(test): + app, blocking, non_blocking = get_app_and_locks() + + test.start_server(app) + + target = test.table("table") + + with blocking: + test.find_element("#blocking").click() + test.find_element("#input").click() + assert test.find_element("#input") == test.driver.switch_to.active_element + + target.is_ready() + assert test.find_element("#input") == test.driver.switch_to.active_element + + +def test_tedi004_edit_on_non_blocking(test): + app, blocking, non_blocking = get_app_and_locks() + + test.start_server(app) + + target = test.table("table") + + with blocking: + test.find_element("#non-blocking").click() + target.cell(0, 0).click() + test.send_keys("abc" + Keys.ENTER) + assert target.cell(0, 0).get_text() == "abc" + + +def test_tedi005_prevent_copy_paste_on_blocking(test): + app, blocking, non_blocking = get_app_and_locks() + + test.start_server(app) + + target = test.table("table") + + with blocking: + test.find_element("#blocking").click() + target.cell(0, 0).click() + with test.hold(Keys.SHIFT): + test.send_keys(Keys.DOWN + Keys.RIGHT) + + test.copy() + target.cell(2, 0).click() + test.paste() + + for row in range(2): + for col in range(2): + assert ( + target.cell(row + 2, col).get_text() + != target.cell(row, col).get_text() + ) + + +def test_tedi006_allow_copy_paste_on_non_blocking(test): + app, blocking, non_blocking = get_app_and_locks() + + test.start_server(app) + + target = test.table("table") + + with non_blocking: + test.find_element("#non-blocking").click() + target.cell(0, 0).click() + with test.hold(Keys.SHIFT): + test.send_keys(Keys.DOWN + Keys.RIGHT) + + test.copy() + target.cell(2, 0).click() + test.paste() + + for row in range(2): + for col in range(2): + assert ( + target.cell(row + 2, col).get_text() + == target.cell(row, col).get_text() + ) diff --git a/tests/selenium/test_markdown_copy_paste.py b/tests/selenium/test_markdown_copy_paste.py new file mode 100644 index 000000000..1bcab4443 --- /dev/null +++ b/tests/selenium/test_markdown_copy_paste.py @@ -0,0 +1,105 @@ +import dash +from dash_table import DataTable + +import pandas as pd + +url = "https://github.com/plotly/datasets/raw/master/" "26k-consumer-complaints.csv" +rawDf = pd.read_csv(url) +rawDf["Complaint ID"] = rawDf["Complaint ID"].map(lambda x: "**" + str(x) + "**") +rawDf["Product"] = rawDf["Product"].map(lambda x: "[" + str(x) + "](plot.ly)") +rawDf["Issue"] = rawDf["Issue"].map( + lambda x: "![" + str(x) + "](https://dash.plot.ly/assets/images/logo.png)" +) +rawDf["State"] = rawDf["State"].map(lambda x: '```python\n"{}"\n```'.format(x)) + +df = rawDf.to_dict("rows") + + +def get_app(): + app = dash.Dash(__name__) + + app.layout = DataTable( + id="table", + data=df[0:250], + columns=[ + {"id": "Complaint ID", "name": "Complaint ID", "presentation": "markdown",}, + {"id": "Product", "name": "Product", "presentation": "markdown"}, + {"id": "Sub-product", "name": "Sub-product"}, + {"id": "Issue", "name": "Issue", "presentation": "markdown"}, + {"id": "Sub-issue", "name": "Sub-issue"}, + {"id": "State", "name": "State", "presentation": "markdown"}, + {"id": "ZIP", "name": "ZIP"}, + ], + editable=True, + sort_action="native", + include_headers_on_copy_paste=True, + ) + + return app + + +def test_tmcp001_copy_markdown_to_text(test): + test.start_server(get_app()) + + target = test.table("table") + + target.cell(0, "Issue").click() + + test.copy() + target.cell(0, "Sub-product").click() + test.paste() + + assert target.cell(0, 2).get_text() == df[0].get("Issue") + + +def test_tmcp002_copy_markdown_to_markdown(test): + test.start_server(get_app()) + + target = test.table("table") + + target.cell(0, "Product").click() + + test.copy() + target.cell(0, "Complaint ID").click() + test.paste() + + assert ( + target.cell(0, "Complaint ID").get_text() + == target.cell(0, "Product").get_text() + ) + + +def test_tmcp003_copy_text_to_markdown(test): + test.start_server(get_app()) + + target = test.table("table") + + target.cell(1, "Sub-product").click() + + test.copy() + target.cell(1, "Product").click() + test.paste() + + assert target.cell(1, "Product").get().find_element_by_css_selector( + ".dash-cell-value > p" + ).get_attribute("innerHTML") == df[1].get("Sub-product") + + +def test_tmcp004_copy_null_text_to_markdown(test): + test.start_server(get_app()) + + target = test.table("table") + + target.cell(0, "Sub-product").click() + + test.copy() + target.cell(0, "Product").click() + test.paste() + + assert ( + target.cell(0, "Product") + .get() + .find_element_by_css_selector(".dash-cell-value > p") + .get_attribute("innerHTML") + == "null" + ) diff --git a/tests/selenium/test_pagination.py b/tests/selenium/test_pagination.py new file mode 100644 index 000000000..3d4c046b4 --- /dev/null +++ b/tests/selenium/test_pagination.py @@ -0,0 +1,204 @@ +import dash +from dash.dependencies import Input, Output, State +from dash.exceptions import PreventUpdate + +import dash_core_components as dcc +import dash_html_components as html +from dash_table import DataTable + +import pytest +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.common.action_chains import ActionChains + +import math +import pandas as pd + +url = "https://github.com/plotly/datasets/raw/master/" "26k-consumer-complaints.csv" +rawDf = pd.read_csv(url) +df = rawDf.to_dict("rows") + +PAGE_SIZE = 5 +pages = math.ceil(len(df) / PAGE_SIZE) + + +def get_app(mode, data=df, page_count=None): + app = dash.Dash(__name__) + + if page_count is None: + page_count = math.ceil(len(data) / PAGE_SIZE) + + app.layout = DataTable( + id="table", + columns=[{"name": i, "id": i} for i in rawDf.columns], + data=data if mode == "native" else data[0:PAGE_SIZE], + editable=True, + fixed_columns={"headers": True}, + fixed_rows={"headers": True}, + page_action=mode, + page_count=page_count, + page_size=PAGE_SIZE, + row_deletable=True, + row_selectable=True, + ) + + if mode == "custom": + + @app.callback( + [Output("table", "data")], + [Input("table", "page_current"), Input("table", "page_size")], + ) + def update_table(page_current, page_size): + if page_current is None or page_size is None: + raise PreventUpdate + + return (data[page_current * page_size : (page_current + 1) * page_size],) + + return app + + +@pytest.mark.parametrize("mode", ["custom", "native"]) +def test_tpag001_next_previous(test, mode): + test.start_server(get_app(mode)) + + target = test.table("table") + + assert target.cell(0, 0).get_text() == "0" + assert target.paging.next.exists() + assert not target.paging.previous.exists() + + target.paging.next.click() + + assert target.cell(0, 0).get_text() == "5" + assert target.paging.next.exists() + assert target.paging.previous.exists() + + target.paging.previous.click() + + assert target.cell(0, 0).get_text() == "0" + assert target.paging.next.exists() + assert not target.paging.previous.exists() + + +@pytest.mark.parametrize("mode", ["custom", "native"]) +def test_tpag002_ops_on_first_page(test, mode): + test.start_server(get_app(mode)) + + target = test.table("table") + + assert target.paging.current.get_value() == "1" + assert not target.paging.first.exists() + assert not target.paging.previous.exists() + assert target.paging.next.exists() + assert target.paging.last.exists() + + +@pytest.mark.parametrize("mode", ["custom", "native"]) +def test_tpag003_ops_on_last_page(test, mode): + test.start_server(get_app(mode)) + + target = test.table("table") + + target.paging.last.click() + + assert target.paging.current.get_value() == str(pages) + assert target.paging.first.exists() + assert target.paging.previous.exists() + assert not target.paging.next.exists() + assert not target.paging.last.exists() + + +def test_tpag004_ops_input_with_enter(test): + test.start_server(get_app("native")) + + target = test.table("table") + + text00 = target.cell(0, 0).get_text() + + assert target.paging.current.get_value() == "1" + + target.paging.current.click() + test.send_keys("100" + Keys.ENTER) + + assert target.paging.current.get_value() == "100" + assert target.cell(0, 0).get_text() != text00 + + +def test_tpag005_ops_input_with_unfocus(test): + test.start_server(get_app("native")) + + target = test.table("table") + + text00 = target.cell(0, 0).get_text() + + assert target.paging.current.get_value() == "1" + + target.paging.current.click() + test.send_keys("100") + target.cell(0, 0).click() + + assert target.paging.current.get_value() == "100" + assert target.cell(0, 0).get_text() != text00 + + +@pytest.mark.parametrize( + "value,expected_value", [(0, 1), (-1, 1), ("a", 1), (pages * 2, pages)] +) +def test_tpag006_ops_input_invalid_with_enter(test, value, expected_value): + test.start_server(get_app("native")) + + target = test.table("table") + + text00 = target.cell(0, 0).get_text() + + assert target.paging.current.get_value() == "1" + + target.paging.current.click() + test.send_keys(str(value) + Keys.ENTER) + + assert target.paging.current.get_value() == str(expected_value) + + +@pytest.mark.parametrize( + "value,expected_value", [(0, 1), (-1, 1), ("a", 1), (pages * 2, pages)] +) +def test_tpag007_ops_input_invalid_with_unfocus(test, value, expected_value): + test.start_server(get_app("native")) + + target = test.table("table") + + text00 = target.cell(0, 0).get_text() + + assert target.paging.current.get_value() == "1" + + target.paging.current.click() + test.send_keys(str(value)) + target.cell(0, 0).click() + + assert target.paging.current.get_value() == str(expected_value) + + +@pytest.mark.parametrize("mode", ["custom", "native"]) +def test_tpag008_hide_with_single_page(test, mode): + test.start_server(get_app(mode=mode, data=df[0:PAGE_SIZE])) + + target = test.table("table") + + assert not target.paging.exists() + + +def test_tpag009_hide_with_invalid_page_count(test): + test.start_server(get_app(mode="custom", page_count=-1)) + + target = test.table("table") + + assert not target.paging.exists() + + +def test_tpag010_limits_page(test): + test.start_server(get_app(mode="custom", page_count=10)) + + target = test.table("table") + + target.paging.last.click() + + assert target.paging.current.get_value() == "10" diff --git a/tests/unit/format_test.py b/tests/unit/format_test.py index a31b0e78c..63bc5cd35 100644 --- a/tests/unit/format_test.py +++ b/tests/unit/format_test.py @@ -4,70 +4,89 @@ from dash_table.Format import Format import dash_table.FormatTemplate as FormatTemplate + class FormatTest(unittest.TestCase): def validate_complex(self, res): - self.assertEqual(res['locale']['symbol'][0], 'a') - self.assertEqual(res['locale']['symbol'][1], 'bc') - self.assertEqual(res['locale']['decimal'], 'x') - self.assertEqual(res['locale']['group'], 'y') - self.assertEqual(res['nully'], 'N/A') - self.assertEqual(res['prefix'], None) - self.assertEqual(res['specifier'], '.^($010,.6s') + self.assertEqual(res["locale"]["symbol"][0], "a") + self.assertEqual(res["locale"]["symbol"][1], "bc") + self.assertEqual(res["locale"]["decimal"], "x") + self.assertEqual(res["locale"]["group"], "y") + self.assertEqual(res["nully"], "N/A") + self.assertEqual(res["prefix"], None) + self.assertEqual(res["specifier"], ".^($010,.6s") def test_complex_and_valid_in_ctor(self): res = Format( align=f.Align.center, - fill='.', + fill=".", group=f.Group.yes, padding=True, padding_width=10, precision=6, - scheme='s', + scheme="s", sign=f.Sign.parantheses, symbol=f.Symbol.yes, - symbol_prefix='a', - symbol_suffix='bc', - decimal_delimiter='x', - group_delimiter='y', + symbol_prefix="a", + symbol_suffix="bc", + decimal_delimiter="x", + group_delimiter="y", groups=[2, 2, 2, 3], - nully='N/A', - si_prefix=f.Prefix.none + nully="N/A", + si_prefix=f.Prefix.none, ) self.validate_complex(res.to_plotly_json()) def test_complex_and_valid_in_fluent(self): - res = Format().align(f.Align.center).fill('.').group(f.Group.yes).padding(True).padding_width(10).precision(6).scheme('s').sign(f.Sign.parantheses).symbol(f.Symbol.yes).symbol_prefix('a').symbol_suffix('bc').decimal_delimiter('x').group_delimiter('y').groups([2, 2, 2, 3]).nully('N/A').si_prefix(f.Prefix.none) + res = ( + Format() + .align(f.Align.center) + .fill(".") + .group(f.Group.yes) + .padding(True) + .padding_width(10) + .precision(6) + .scheme("s") + .sign(f.Sign.parantheses) + .symbol(f.Symbol.yes) + .symbol_prefix("a") + .symbol_suffix("bc") + .decimal_delimiter("x") + .group_delimiter("y") + .groups([2, 2, 2, 3]) + .nully("N/A") + .si_prefix(f.Prefix.none) + ) self.validate_complex(res.to_plotly_json()) def test_money_template(self): res = FormatTemplate.money(2).to_plotly_json() - self.assertEqual(res['specifier'], '$,.2f') + self.assertEqual(res["specifier"], "$,.2f") def test_percentage_template(self): res = FormatTemplate.percentage(1).to_plotly_json() - self.assertEqual(res['specifier'], '.1%') + self.assertEqual(res["specifier"], ".1%") def test_valid_align_named(self): Format().align(f.Align.center) def test_valid_align_string(self): - Format().align('=') + Format().align("=") def test_invalid_align_string(self): - self.assertRaises(TypeError, Format().align, 'i') + self.assertRaises(TypeError, Format().align, "i") def test_invalid_align_type(self): self.assertRaises(TypeError, Format().align, 7) def test_valid_fill(self): - Format().fill('.') + Format().fill(".") def test_invalid_fill_length(self): - self.assertRaises(ValueError, Format().fill, 'invalid') + self.assertRaises(ValueError, Format().fill, "invalid") def test_invalid_fill_type(self): self.assertRaises(TypeError, Format().fill, 7) @@ -76,7 +95,7 @@ def test_valid_group_bool(self): Format().group(True) def test_valid_group_string(self): - Format().group(',') + Format().group(",") def test_valid_group_named(self): Format().group(f.Group.no) @@ -85,13 +104,13 @@ def test_invalid_group_type(self): self.assertRaises(TypeError, Format().group, 7) def test_invalid_group_string(self): - self.assertRaises(TypeError, Format().group, 'invalid') + self.assertRaises(TypeError, Format().group, "invalid") def test_valid_padding_bool(self): Format().padding(False) def test_valid_padding_string(self): - Format().padding('0') + Format().padding("0") def test_valid_padding_named(self): Format().padding(f.Padding.no) @@ -100,7 +119,7 @@ def test_invalid_padding_type(self): self.assertRaises(TypeError, Format().padding, 7) def test_invalid_padding_string(self): - self.assertRaises(TypeError, Format().padding, 'invalid') + self.assertRaises(TypeError, Format().padding, "invalid") def test_valid_padding_width(self): Format().padding_width(10) @@ -127,61 +146,61 @@ def test_invalid_precision_type(self): self.assertRaises(TypeError, Format().precision, 7.7) def test_valid_prefix_number(self): - Format().si_prefix(10**-24) + Format().si_prefix(10 ** -24) def test_valid_prefix_named(self): Format().si_prefix(f.Prefix.micro) def test_invalid_prefix_number(self): - self.assertRaises(TypeError, Format().si_prefix, 10**-23) + self.assertRaises(TypeError, Format().si_prefix, 10 ** -23) def test_invalid_prefix_type(self): - self.assertRaises(TypeError, Format().si_prefix, '10**-23') + self.assertRaises(TypeError, Format().si_prefix, "10**-23") def test_valid_scheme_string(self): - Format().scheme('s') + Format().scheme("s") def test_valid_scheme_named(self): Format().scheme(f.Scheme.decimal) def test_invalid_scheme_string(self): - self.assertRaises(TypeError, Format().scheme, 'invalid') + self.assertRaises(TypeError, Format().scheme, "invalid") def test_invalid_scheme_type(self): self.assertRaises(TypeError, Format().scheme, 7) def test_valid_sign_string(self): - Format().sign('+') + Format().sign("+") def test_valid_sign_named(self): Format().sign(f.Sign.space) def test_invalid_sign_string(self): - self.assertRaises(TypeError, Format().sign, 'invalid') + self.assertRaises(TypeError, Format().sign, "invalid") def test_invalid_sign_type(self): self.assertRaises(TypeError, Format().sign, 7) def test_valid_symbol_string(self): - Format().symbol('$') + Format().symbol("$") def test_valid_symbol_named(self): Format().symbol(f.Symbol.hex) def test_invalid_symbol_string(self): - self.assertRaises(TypeError, Format().symbol, 'invalid') + self.assertRaises(TypeError, Format().symbol, "invalid") def test_invalid_symbol_type(self): self.assertRaises(TypeError, Format().symbol, 7) def test_valid_symbol_prefix(self): - Format().symbol_prefix('abc+-') + Format().symbol_prefix("abc+-") def test_invalid_symbol_prefix_type(self): self.assertRaises(TypeError, Format().symbol_prefix, 7) def test_valid_symbol_suffix(self): - Format().symbol_suffix('abc+-') + Format().symbol_suffix("abc+-") def test_invalid_symbol_suffix(self): self.assertRaises(TypeError, Format().symbol_suffix, 7) @@ -190,31 +209,31 @@ def test_valid_trim_boolean(self): Format().trim(False) def test_valid_trim_string(self): - Format().trim('~') + Format().trim("~") def test_valid_trim_named(self): Format().trim(f.Trim.yes) def test_invalid_trim_string(self): - self.assertRaises(TypeError, Format().trim, 'invalid') + self.assertRaises(TypeError, Format().trim, "invalid") def test_invalid_trim_type(self): self.assertRaises(TypeError, Format().trim, 7) def test_valid_decimal_delimiter(self): - Format().decimal_delimiter('x') + Format().decimal_delimiter("x") def test_valid_decimal_delimiter(self): - self.assertRaises(ValueError, Format().decimal_delimiter, 'xyz') + self.assertRaises(ValueError, Format().decimal_delimiter, "xyz") def test_invalid_decimal_delimiter(self): self.assertRaises(TypeError, Format().decimal_delimiter, 7) def test_valid_group_delimiator(self): - Format().group_delimiter('y') + Format().group_delimiter("y") def test_valid_group_delimiator(self): - self.assertRaises(ValueError, Format().group_delimiter, 'xyz') + self.assertRaises(ValueError, Format().group_delimiter, "xyz") def test_invalid_group_delimiter(self): self.assertRaises(TypeError, Format().group_delimiter, 7) @@ -247,4 +266,4 @@ def test_invalid_groups_nested_0(self): self.assertRaises(ValueError, Format().groups, [3, 3, 0]) def test_invalid_groups_nested_negative(self): - self.assertRaises(ValueError, Format().groups, [3, 3, -7]) \ No newline at end of file + self.assertRaises(ValueError, Format().groups, [3, 3, -7])