浏览代码

test: add more test

wangweimin 5 年之前
父节点
当前提交
a770544e81

+ 1 - 1
.github/workflows/test.yml

@@ -13,7 +13,7 @@ jobs:
       - name: Install JS deps
         run: npm install -D @percy/agent
       - name: Install Python deps
-        run: pip3 install -e ".[dev]"
+        run: pip3 install -e ".[dev, flask]"
       - name: Percy Test
         uses: percy/exec-action@v0.2.0
         with:

+ 9 - 108
test/1.basic_output.py

@@ -1,132 +1,33 @@
-import json
 import subprocess
-from functools import partial
-from os import path
 
-import time
-from percy import percySnapshot
 from selenium.webdriver import Chrome
 
 import pywebio
+import template
+import util
 from pywebio import start_server
 from pywebio.output import *
 from pywebio.session import *
 
-proj_dir = path.dirname(path.dirname(path.abspath(__file__)))
 
-
-def basic():
+def target():
     set_auto_scroll_bottom(False)
-    set_anchor('top')
-
-    for i in range(3):
-        put_text('text_%s' % i)
-
-    put_text('测试空格:20空格:[%s]结束' % (' ' * 20))
-
-    for i in range(3):
-        put_text('inline_text_%s' % i, inline=True)
-
-    put_markdown("""### put_markdown 测试
-    `行内代码`
-    
-    无序列表:
-    - 北京
-    - 上海
-    - 天津
-     
-    有序列表:
-    1. 北京
-    2. 上海
-    3. 天津
-    
-    [链接](./#)
-    ~~删除线~~
-    """, strip_indent=4, anchor='put_markdown')
-
-    put_text('put_html("<hr/>")')
-    put_html("<hr/>", anchor='put_html')
-
-    put_code(json.dumps(dict(name='pywebio', author='wangweimin'), indent=4), 'json', anchor='put_code')
-
-    put_table([
-        ['Name', 'Gender', 'Address'],
-        ['Wang', 'M', 'China'],
-        ['Liu', 'W', 'America'],
-    ])
-
-    put_table([
-        ['Wang', 'M', 'China'],
-        ['Liu', 'W', 'America'],
-    ], header=['Name', 'Gender', 'Address'])
-
-    put_table([
-        {"Course": "OS", "Score": "80"},
-        {"Course": "DB", "Score": "93"},
-    ], header=["Course", "Score"], anchor='put_table')
-
-    def edit_row(choice, row):
-        put_text("You click %s button at row %s" % (choice, row), after='table_cell_buttons')
-
-    put_table([
-        ['Idx', 'Actions'],
-        ['1', table_cell_buttons(['edit', 'delete'], onclick=partial(edit_row, row=1))],
-        ['2', table_cell_buttons(['edit', 'delete'], onclick=partial(edit_row, row=2))],
-        ['3', table_cell_buttons(['edit', 'delete'], onclick=partial(edit_row, row=3))],
-    ], anchor='table_cell_buttons')
-
-    put_buttons(['A', 'B', 'C'], onclick=partial(put_text, after='put_buttons'), anchor='put_buttons')
 
-    put_image(open(proj_dir + "/docs/assets/input_1.png", 'rb').read(), anchor='put_image')
+    template.basic_output()
 
-    put_file('hello_word.txt', b'hello word!', anchor='put_file')
-
-    put_markdown('### 锚点')
-
-    put_text('anchor A1', anchor='A1')
-    put_text('new anchor A1', anchor='A1')
-    put_text('anchor A2', anchor='A2')
-    put_text('anchor A3', anchor='A3')
-
-    put_text('after=A1', after='A1')
-    put_text('after=A2', after='A2')
-    put_text('before=A1', before='A1')
-    put_text('before=A3', before='A3')
-    put_text('after=A3', after='A3')
-
-    clear_range('A1', "A2")
-    clear_range('A3', 'A2')
-    clear_after('A3')
-
-    put_text('before=top', before='top')
-    clear_before('top')
-    put_text('before=top again', before='top')
-
-    put_text('to remove', anchor='to_remove')
-    remove('to_remove')
+    template.background_output()
 
     hold()
 
 
-def start_test_server():
-    pywebio.enable_debug()
-    start_server(basic, port=8080, debug=True, auto_open_webbrowser=False)
-
-
 def test(server_proc: subprocess.Popen, browser: Chrome):
-    btns = browser.find_elements_by_css_selector('#pywebio-anchor-put_buttons button')
-    for btn in btns:
-        btn.click()
+    template.test_output(browser)
 
-    tab_btns = browser.find_elements_by_css_selector('#pywebio-anchor-table_cell_buttons button')
-    for btn in tab_btns:
-        btn.click()
 
-    time.sleep(1)
-    percySnapshot(browser=browser, name='basic output')
+def start_test_server():
+    pywebio.enable_debug()
+    start_server(target, port=8080, debug=True, auto_open_webbrowser=False)
 
 
 if __name__ == '__main__':
-    import util
-
     util.run_test(start_test_server, test)

+ 14 - 245
test/2.basic_input.py

@@ -1,268 +1,37 @@
-import json
 import subprocess
-import time
-from percy import percySnapshot
+
 from selenium.webdriver import Chrome
-from selenium.webdriver.support.ui import Select
 
 import pywebio
+import template
+import util
 from pywebio import start_server
-from pywebio.input import *
+from pywebio.input import actions
 from pywebio.output import *
-
-from os import path
-
-here_dir = path.dirname(path.abspath(__file__))
+from pywebio.utils import run_as_function
 
 
-def basic():
+def target():
     set_auto_scroll_bottom(True)
 
-    age = input("How old are you?", type=NUMBER)
-    put_markdown(f'`{repr(age)}`')
-
-    password = input("Input password", type=PASSWORD)
-    put_markdown(f'`{repr(password)}`')
-
-    # 下拉选择框
-    gift = select('Which gift you want?', ['keyboard', 'ipad'])
-    put_markdown(f'`{repr(gift)}`')
-
-    # CheckBox
-    agree = checkbox("用户协议", options=['I agree to terms and conditions'])
-    put_markdown(f'`{repr(agree)}`')
-
-    # Text Area
-    text = textarea('Text Area', rows=3, placeholder='Some text')
-    put_markdown(f'`{repr(text)}`')
-
-    # 文件上传
-    img = file_upload("Select a image:", accept="image/*")
-    put_markdown(f'`{repr(img)}`')
-
-    # 输入参数
-    res = input('This is label', type=TEXT, placeholder='This is placeholder,required=True',
-                help_text='This is help text', required=True)
-    put_markdown(f'`{repr(res)}`')
-
-    # 校验函数
-    def check_age(p):  # 检验函数校验通过时返回None,否则返回错误消息
-        if p < 10:
-            return 'Too young!!'
-        if p > 60:
-            return 'Too old!!'
-
-    age = input("How old are you?", type=NUMBER, valid_func=check_age, help_text='age in [10, 60]')
-    put_markdown(f'`{repr(age)}`')
-
-    # Codemirror
-    code = textarea('Code Edit', code={
-        'mode': "python",  # 编辑区代码语言
-        'theme': 'darcula',  # 编辑区darcula主题, Visit https://codemirror.net/demo/theme.html#cobalt to get more themes
-    }, value='import something\n# Write your python code')
-    put_markdown(f'`{repr(code)}`')
-
-    # 输入组
-    info = input_group("Cancelable", [
-        input('Input your name', name='name'),
-        input('Input your age', name='age', type=NUMBER, valid_func=check_age, help_text='age in [10, 60]')
-    ], cancelable=True)
-    put_markdown(f'`{repr(info)}`')
-
-    def check_form(data):  # 检验函数校验通过时返回None,否则返回 (input name,错误消息)
-        if len(data['password']) > 6:
-            return ('password', 'password太长!')
-
-    check_item_data = []
-
-    def check_item(data):
-        check_item_data.append(repr(data))
-
-    info = input_group('Input group', [
-        input('Text', type=TEXT, datalist=['data-%s' % i for i in range(10)], name='text',
-              required=True, help_text='required=True', valid_func=check_item),
-        input('Number', type=NUMBER, value="42", name='number', valid_func=check_item),
-        input('Float', type=FLOAT, name='float', valid_func=check_item),
-        input('Password', type=PASSWORD, name='password', valid_func=check_item),
-
-        textarea('Textarea', rows=3, maxlength=20, name='textarea',
-                 help_text='rows=3, maxlength=20', valid_func=check_item),
-
-        textarea('Code', name='code', code={
-            'lineNumbers': False,
-            'indentUnit': 2,
-        }, value='import something\n# Write your python code', valid_func=check_item),
-
-        select('select-multiple', [
-            {'label': '标签0,selected', 'value': '0', 'selected': True},
-            {'label': '标签1,disabled', 'value': '1', 'disabled': True},
-            ('标签2,selected', '2', True),
-            ('标签3', '3'),
-            ('标签4,disabled', '4', False, True),
-            '标签5,selected',
-        ], name='select-multiple', multiple=True, value=['标签5,selected'], required=True,
-               help_text='required至少选择一项', valid_func=check_item),
-
-        select('select', [
-            {'label': '标签0', 'value': '0', 'selected': False},
-            {'label': '标签1,disabled', 'value': '1', 'disabled': True},
-            ('标签2', '2', False),
-            ('标签3', '3'),
-            ('标签4,disabled', '4', False, True),
-            '标签5,selected',
-        ], name='select', value=['标签5,selected'], valid_func=check_item),
+    template.set_defer_call()
 
-        checkbox('checkbox-inline', [
-            {'label': '标签0,selected', 'value': '0', 'selected': False},
-            {'label': '标签1,disabled', 'value': '1', 'disabled': True},
-            ('标签2,selected', '2', True),
-            ('标签3', '3'),
-            ('标签4,disabled', '4', False, True),
-            '标签5,selected',
-        ], inline=True, name='checkbox-inline', value=['标签5,selected', '标签0', '标签0,selected'], valid_func=check_item),
+    run_as_function(template.basic_input())
 
-        checkbox('checkbox', [
-            {'label': '标签0,selected', 'value': '0', 'selected': True},
-            {'label': '标签1,disabled', 'value': '1', 'disabled': True},
-            ('标签2,selected', '2', True),
-            ('标签3', '3'),
-            ('标签4,disabled', '4', False, True),
-            '标签5',
-        ], name='checkbox', valid_func=check_item),
+    actions(buttons=['Continue'])
 
-        radio('radio-inline', [
-            {'label': '标签0', 'value': '0', 'selected': False},
-            {'label': '标签1,disabled', 'value': '1', 'disabled': True},
-            ('标签2', '2', False),
-            ('标签3', '3'),
-            ('标签4,disabled', '4', False, True),
-            '标签5,selected',
-        ], inline=True, name='radio-inline', value='标签5,selected', valid_func=check_item),
+    template.background_input()
 
-        radio('radio', [
-            {'label': '标签0', 'value': '0', 'selected': False},
-            {'label': '标签1,disabled', 'value': '1', 'disabled': True},
-            ('标签2', '2', False),
-            ('标签3', '3'),
-            ('标签4,disabled', '4', False, True),
-            '标签5,selected',
-        ], inline=False, name='radio', value='标签5,selected', valid_func=check_item),
 
-        file_upload('file_upload', name='file_upload'),
-
-        actions('actions', [
-            {'label': '提交', 'value': 'submit'},
-            ('提交2', 'submit2'),
-            '提交3',
-            {'label': 'disabled', 'disabled': True},
-            ('重置', 'reset', 'reset'),
-            {'label': '取消', 'type': 'cancel'},
-        ], name='actions', help_text='actions'),
-
-    ], valid_func=check_form)
-
-    put_text('`valid_func()` log:')
-    put_code(json.dumps(sorted(check_item_data), indent=4, ensure_ascii=False), 'json')
-
-    put_text('Form result:')
-    if info:
-        put_code(json.dumps([repr(i) for i in sorted(info.items())], indent=4, ensure_ascii=False), 'json')
+def test(server_proc: subprocess.Popen, browser: Chrome):
+    template.test_input(browser)
+    template.test_defer_call()
 
 
 def start_test_server():
     pywebio.enable_debug()
-    start_server(basic, port=8080, debug=False, auto_open_webbrowser=False)
-
-
-def test(server_proc: subprocess.Popen, browser: Chrome):
-    browser.find_element_by_css_selector('input').send_keys("22")
-    browser.find_element_by_tag_name('form').submit()
-
-    time.sleep(0.5)
-    browser.find_element_by_css_selector('input').send_keys("secret")
-    browser.find_element_by_tag_name('form').submit()
-
-    time.sleep(0.5)
-    browser.find_element_by_tag_name('form').submit()
-
-    # checkbox
-    time.sleep(0.5)
-    browser.find_element_by_css_selector('input').click()
-    browser.find_element_by_tag_name('form').submit()
-
-    # Text Area
-    time.sleep(0.5)
-    browser.find_element_by_css_selector('textarea').send_keys(" ".join(str(i) for i in range(20)))
-    browser.find_element_by_tag_name('form').submit()
-
-    # file
-    time.sleep(0.5)
-    img_path = path.join(here_dir, 'assets', 'img.png')
-    browser.find_element_by_css_selector('input').send_keys(img_path)
-    browser.find_element_by_tag_name('form').submit()
-
-    # text
-    time.sleep(0.5)
-    browser.find_element_by_css_selector('input').send_keys("text")
-    browser.find_element_by_tag_name('form').submit()
-
-    # valid func, age in [10, 60]
-    time.sleep(0.5)
-    browser.find_element_by_css_selector('input').send_keys("1")
-    browser.find_element_by_tag_name('form').submit()
-    time.sleep(0.5)
-    browser.find_element_by_css_selector('input').clear()
-    browser.find_element_by_css_selector('input').send_keys("90")
-    browser.find_element_by_tag_name('form').submit()
-    time.sleep(0.5)
-    browser.find_element_by_css_selector('input').clear()
-    browser.find_element_by_css_selector('input').send_keys("23")
-    browser.find_element_by_tag_name('form').submit()
-
-    # code
-    time.sleep(0.5)
-    # browser.find_element_by_css_selector('textarea').send_keys(" ".join(str(i) for i in range(20)))
-    browser.find_element_by_tag_name('form').submit()
-
-    # Cancelable from group
-    time.sleep(0.5)
-    browser.find_element_by_css_selector('input[name="name"]').send_keys("name")
-    browser.find_element_by_css_selector('input[name="age"]').send_keys("90")
-    browser.find_element_by_tag_name('form').submit()
-    percySnapshot(browser=browser, name='input group invalid')
-    browser.find_element_by_css_selector('input[name="age"]').clear()
-    browser.find_element_by_css_selector('input[name="age"]').send_keys("23")
-    browser.find_element_by_tag_name('form').submit()
-
-    # Input group
-    time.sleep(0.5)
-    percySnapshot(browser=browser, name='input group all')
-    browser.find_element_by_css_selector('input[name="text"]').send_keys("name")
-    browser.find_element_by_css_selector('input[name="number"]').send_keys("20")
-    browser.find_element_by_css_selector('input[name="float"]').send_keys("3.1415")
-    browser.find_element_by_css_selector('input[name="password"]').send_keys("password")
-    browser.find_element_by_css_selector('textarea[name="textarea"]').send_keys(" ".join(str(i) for i in range(20)))
-    # browser.find_element_by_css_selector('[name="code"]').send_keys(" ".join(str(i) for i in range(10)))
-    Select(browser.find_element_by_css_selector('select[name="select-multiple"]')).select_by_index(0)
-    # browser. find_element_by_css_selector('[name="select"]'). send_keys("name")
-    # browser. find_element_by_css_selector('[name="checkbox-inline"]'). send_keys("name")
-    # browser. find_element_by_css_selector('[name="checkbox"]'). send_keys("name")
-    # browser. find_element_by_css_selector('[name="radio-inline"]'). send_keys("name")
-    # browser. find_element_by_css_selector('[name="radio"]'). send_keys("name")
-    browser.find_element_by_css_selector('input[name="file_upload"]').send_keys(img_path)
-
-    browser.find_element_by_css_selector('button[value="submit2"]').click()
-    time.sleep(0.5)
-    percySnapshot(browser=browser, name='input group all invalid')
-
-    browser.find_element_by_css_selector('input[name="password"]').clear()
-    browser.find_element_by_css_selector('input[name="password"]').send_keys("123")
-    browser.find_element_by_css_selector('button[value="submit2"]').click()
-    time.sleep(0.5)
-    percySnapshot(browser=browser, name='input group all submit')
+    start_server(target, port=8080, debug=False, auto_open_webbrowser=False)
 
 
 if __name__ == '__main__':
-    import util
-
     util.run_test(start_test_server, test)

+ 47 - 0
test/3.script_mode.py

@@ -0,0 +1,47 @@
+import os
+import subprocess
+
+import time
+from percy import percySnapshot
+from selenium.webdriver import Chrome
+
+import template
+import util
+from pywebio.input import *
+from pywebio.output import *
+from pywebio.utils import run_as_function
+
+
+def target():
+    set_auto_scroll_bottom(True)
+
+    template.basic_output()
+
+    template.background_output()
+
+    run_as_function(template.basic_input())
+
+    actions(buttons=['Continue'])
+
+    template.background_input()
+
+
+def test(server_proc: subprocess.Popen, browser: Chrome):
+    percy_prefix = '[script_mode]'
+
+    template.test_output(browser, percy_prefix=percy_prefix)
+
+    time.sleep(1)
+
+    template.test_input(browser, percy_prefix=percy_prefix)
+
+    # script mode 下,此时 server 应停止
+    server_proc.wait(timeout=8)
+    percySnapshot(browser=browser, name=percy_prefix + 'over')
+
+
+if __name__ == '__main__':
+    # 设置监听端口,并关闭自动打开浏览器
+    os.environ["PYWEBIO_SCRIPT_MODE_PORT"] = "8080"
+
+    util.run_test(target, test)

+ 44 - 0
test/4.flask_backend.py

@@ -0,0 +1,44 @@
+import subprocess
+
+import time
+from percy import percySnapshot
+from selenium.webdriver import Chrome
+
+import pywebio
+import template
+import util
+from pywebio.input import *
+from pywebio.output import *
+from pywebio.utils import run_as_function
+from pywebio.platform.flask import start_server
+
+
+def target():
+    set_auto_scroll_bottom(False)
+
+    template.basic_output()
+
+    template.background_output()
+
+    run_as_function(template.basic_input())
+
+    actions(buttons=['Continue'])
+
+    template.background_input()
+
+
+def test(server_proc: subprocess.Popen, browser: Chrome):
+    template.test_output(browser, percy_prefix='[flask]')
+
+    time.sleep(1)
+
+    template.test_input(browser, percy_prefix='[flask]')
+
+
+def start_test_server():
+    pywebio.enable_debug()
+    start_server(target, port=8080)
+
+
+if __name__ == '__main__':
+    util.run_test(start_test_server, test)

+ 44 - 0
test/5.coroutine_based_session.py

@@ -0,0 +1,44 @@
+import subprocess
+
+import time
+from percy import percySnapshot
+from selenium.webdriver import Chrome
+
+import pywebio
+import template
+import util
+from pywebio.input import *
+from pywebio.output import *
+from pywebio.utils import to_coroutine
+from pywebio import start_server
+
+
+async def target():
+    set_auto_scroll_bottom(True)
+
+    template.basic_output()
+
+    await template.coro_background_output()
+
+    await to_coroutine(template.basic_input())
+
+    await actions(buttons=['Continue'])
+
+    await template.coro_background_input()
+
+
+def test(server_proc: subprocess.Popen, browser: Chrome):
+    template.test_output(browser, percy_prefix='[coro]')
+
+    time.sleep(1)
+
+    template.test_input(browser, percy_prefix='[coro]')
+
+
+def start_test_server():
+    pywebio.enable_debug()
+    start_server(target, port=8080, debug=True)
+
+
+if __name__ == '__main__':
+    util.run_test(start_test_server, test)

+ 43 - 0
test/6.flask_coroutine.py

@@ -0,0 +1,43 @@
+import subprocess
+
+import time
+from selenium.webdriver import Chrome
+
+import pywebio
+import template
+import util
+from pywebio.input import *
+from pywebio.output import *
+from pywebio.platform.flask import start_server
+from pywebio.utils import to_coroutine
+
+
+async def target():
+    set_auto_scroll_bottom(True)
+
+    template.basic_output()
+
+    await template.coro_background_output()
+
+    await to_coroutine(template.basic_input())
+
+    await actions(buttons=['Continue'])
+
+    await template.flask_coro_background_input()
+
+
+def test(server_proc: subprocess.Popen, browser: Chrome):
+    template.test_output(browser, percy_prefix='[flask coro]')
+
+    time.sleep(1)
+
+    template.test_input(browser, percy_prefix='[flask coro]')
+
+
+def start_test_server():
+    pywebio.enable_debug()
+    start_server(target, port=8080)
+
+
+if __name__ == '__main__':
+    util.run_test(start_test_server, test)

+ 78 - 0
test/7.multiple_session_impliment.py

@@ -0,0 +1,78 @@
+import subprocess
+
+from selenium.webdriver import Chrome
+
+import pywebio
+import template
+import util, time
+from pywebio.input import *
+from pywebio.output import *
+from pywebio.utils import to_coroutine, run_as_function
+
+
+def target():
+    set_auto_scroll_bottom(True)
+
+    template.basic_output()
+
+    template.background_output()
+
+    run_as_function(template.basic_input())
+
+    actions(buttons=['Continue'])
+
+    template.background_input()
+
+
+async def async_target():
+    set_auto_scroll_bottom(True)
+
+    template.basic_output()
+
+    await template.coro_background_output()
+
+    await to_coroutine(template.basic_input())
+
+    await actions(buttons=['Continue'])
+
+    await template.coro_background_input()
+
+
+def test(server_proc: subprocess.Popen, browser: Chrome):
+    template.test_output(browser, percy_prefix='[multi tornado coro]')
+
+    time.sleep(1)
+
+    template.test_input(browser, percy_prefix='[multi tornado coro]')
+
+    time.sleep(3)
+
+    browser.get('http://localhost:8080?_pywebio_debug=1&pywebio_api=io2')
+
+    template.test_output(browser, percy_prefix='[multi tornado thread]')
+
+    time.sleep(1)
+
+    template.test_input(browser, percy_prefix='[multi tornado thread]')
+
+
+def start_test_server():
+    pywebio.enable_debug()
+
+    import tornado.ioloop
+    import tornado.web
+    from pywebio.platform.tornado import webio_handler
+    from pywebio import STATIC_PATH
+
+    application = tornado.web.Application([
+        (r"/io", webio_handler(async_target)),
+        (r"/io2", webio_handler(target)),
+        (r"/(.*)", tornado.web.StaticFileHandler,
+         {"path": STATIC_PATH, 'default_filename': 'index.html'})
+    ])
+    application.listen(port=8080, address='localhost')
+    tornado.ioloop.IOLoop.current().start()
+
+
+if __name__ == '__main__':
+    util.run_test(start_test_server, test)

+ 86 - 0
test/8.flask_multiple_session_impliment.py

@@ -0,0 +1,86 @@
+import subprocess
+
+from selenium.webdriver import Chrome
+
+import pywebio
+import template
+import time
+import util
+from pywebio.input import *
+from pywebio.output import *
+from pywebio.utils import to_coroutine, run_as_function
+
+
+def target():
+    set_auto_scroll_bottom(False)
+
+    template.basic_output()
+
+    template.background_output()
+
+    run_as_function(template.basic_input())
+
+    actions(buttons=['Continue'])
+
+    template.background_input()
+
+
+async def async_target():
+    set_auto_scroll_bottom(False)
+
+    template.basic_output()
+
+    await template.coro_background_output()
+
+    await to_coroutine(template.basic_input())
+
+    await actions(buttons=['Continue'])
+
+    await template.coro_background_input()
+
+
+def test(server_proc: subprocess.Popen, browser: Chrome):
+
+    template.test_output(browser, percy_prefix='[multi flask coro]')
+
+    time.sleep(1)
+
+    template.test_input(browser, percy_prefix='[multi flask coro]')
+
+    time.sleep(3)
+
+    browser.get('http://localhost:8080?_pywebio_debug=1&pywebio_api=io2')
+
+    template.test_output(browser, percy_prefix='[multi flask thread]')
+
+    time.sleep(1)
+
+    template.test_input(browser, percy_prefix='[multi flask thread]')
+
+
+def start_test_server():
+    pywebio.enable_debug()
+    from flask import Flask, send_from_directory
+    from pywebio.platform.flask import webio_view, run_event_loop
+    from pywebio import STATIC_PATH
+    import threading
+    import logging
+
+    app = Flask(__name__)
+    app.add_url_rule('/io', 'webio_view', webio_view(target), methods=['GET', 'POST', 'OPTIONS'])
+    app.add_url_rule('/io2', 'webio_view_async_target', webio_view(async_target), methods=['GET', 'POST', 'OPTIONS'])
+
+    @app.route('/')
+    @app.route('/<path:static_file>')
+    def serve_static_file(static_file='index.html'):
+        return send_from_directory(STATIC_PATH, static_file)
+
+    threading.Thread(target=run_event_loop, daemon=True).start()
+
+    logging.getLogger('werkzeug').setLevel(logging.WARNING)
+
+    app.run(port=8080)
+
+
+if __name__ == '__main__':
+    util.run_test(start_test_server, test)

+ 6 - 1
test/run_all.sh

@@ -3,4 +3,9 @@
 cd test
 
 python3 1.basic_output.py auto
-python3 2.basic_input.py auto
+python3 2.basic_input.py auto
+python3 3.script_mode.py auto
+python3 4.flask_backend.py auto
+python3 5.coroutine_based_session.py auto
+python3 6.flask_coroutine.py auto
+python3 7.multiple_session_impliment.py auto

+ 488 - 0
test/template.py

@@ -0,0 +1,488 @@
+import asyncio
+import json
+import os
+import threading
+from functools import partial
+from os import path
+
+import time
+from percy import percySnapshot
+from selenium.webdriver import Chrome
+from selenium.webdriver.support.ui import Select
+
+from pywebio.input import *
+from pywebio.output import *
+from pywebio.session import *
+
+here_dir = path.dirname(path.abspath(__file__))
+
+
+def get_visible_form(browser):
+    forms = browser.find_elements_by_css_selector('#input-container > div')
+    for f in forms:
+        if f.is_displayed():
+            return f
+
+
+def basic_output():
+    set_anchor('top')
+
+    for i in range(3):
+        put_text('text_%s' % i)
+
+    put_text('测试空格:20空格:[%s]结束' % (' ' * 20))
+
+    for i in range(3):
+        put_text('inline_text_%s' % i, inline=True)
+
+    put_markdown("""### put_markdown 测试
+    `行内代码`
+
+    无序列表:
+    - 北京
+    - 上海
+    - 天津
+
+    有序列表:
+    1. 北京
+    2. 上海
+    3. 天津
+
+    [链接](./#)
+    ~~删除线~~
+    """, lstrip=True, anchor='put_markdown')
+
+    put_text('<hr/>:')
+    put_html("<hr/>", anchor='put_html')
+
+    put_text('code:')
+    put_code(json.dumps(dict(name='pywebio', author='wangweimin'), indent=4), 'json', anchor='put_code')
+
+    put_text('table:')
+    put_table([
+        ['Name', 'Gender', 'Address'],
+        ['Wang', 'M', 'China'],
+        ['Liu', 'W', 'America'],
+    ])
+
+    put_table([
+        ['Wang', 'M', 'China'],
+        ['Liu', 'W', 'America'],
+    ], header=['Name', 'Gender', 'Address'])
+
+    put_table([
+        {"Course": "OS", "Score": "80"},
+        {"Course": "DB", "Score": "93"},
+    ], header=["Course", "Score"], anchor='put_table')
+
+    def edit_row(choice, row):
+        put_text("You click %s button at row %s" % (choice, row), after='table_cell_buttons')
+
+    put_table([
+        ['Idx', 'Actions'],
+        ['1', table_cell_buttons(['edit', 'delete'], onclick=partial(edit_row, row=1))],
+        ['2', table_cell_buttons(['edit', 'delete'], onclick=partial(edit_row, row=2))],
+        ['3', table_cell_buttons(['edit', 'delete'], onclick=partial(edit_row, row=3))],
+    ], anchor='table_cell_buttons')
+
+    put_buttons(['A', 'B', 'C'], onclick=partial(put_text, after='put_buttons'), anchor='put_buttons')
+
+    img_data = open(path.join(here_dir, 'assets', 'img.png'), 'rb').read()
+    put_image(img_data, anchor='put_image1')
+    put_image(img_data, width="30px", anchor='put_image2')
+    put_image(img_data, height="50px", anchor='put_image3')
+
+    put_file('hello_word.txt', b'hello word!', anchor='put_file')
+
+    put_markdown('### 锚点')
+
+    put_text('anchor A1', anchor='A1')
+    put_text('new anchor A1', anchor='A1')
+    put_text('anchor A2', anchor='A2')
+    put_text('anchor A3', anchor='A3')
+
+    put_text('after=A1', after='A1')
+    put_text('after=A2', after='A2')
+    put_text('before=A1', before='A1')
+    put_text('before=A3', before='A3')
+    put_text('after=A3', after='A3')
+
+    clear_range('A1', "A2")
+    clear_range('A3', 'A2')
+    clear_after('A3')
+
+    put_text('before=top', before='top')
+    clear_before('top')
+    put_text('before=top again', before='top')
+
+    put_text('to remove', anchor='to_remove')
+    remove('to_remove')
+
+
+def background_output():
+    put_text("Background output", anchor='background')
+
+    def background():
+        for i in range(20):
+            put_text('%s ' % i, inline=True, after='background')
+
+    t = threading.Thread(target=background)
+    register_thread(t)
+    t.start()
+
+
+async def coro_background_output():
+    put_text("Background output", anchor='background')
+
+    async def background():
+        for i in range(20):
+            put_text('%s ' % i, inline=True, after='background')
+
+    return run_async(background())
+
+
+def test_output(browser: Chrome, percy_prefix=''):
+    """测试输出::
+
+        template.basic_output()
+        template.background_output() # 或者 await template.coro_background_output()
+        hold()
+
+    """
+    time.sleep(1)  # 等待输出完毕
+
+    if percy_prefix:
+        percy_prefix = percy_prefix + ' '
+
+    tab_btns = browser.find_elements_by_css_selector('#pywebio-anchor-table_cell_buttons button')
+    for btn in tab_btns:
+        time.sleep(0.5)
+        btn.click()
+
+    btns = browser.find_elements_by_css_selector('#pywebio-anchor-put_buttons button')
+    for btn in btns:
+        time.sleep(0.5)
+        btn.click()
+
+    time.sleep(1)
+    percySnapshot(browser=browser, name=percy_prefix + 'basic output')
+
+
+def basic_input():
+    age = yield input("How old are you?", type=NUMBER)
+    put_markdown(f'`{repr(age)}`')
+
+    password = yield input("Input password", type=PASSWORD)
+    put_markdown(f'`{repr(password)}`')
+
+    # 下拉选择框
+    gift = yield select('Which gift you want?', ['keyboard', 'ipad'])
+    put_markdown(f'`{repr(gift)}`')
+
+    # CheckBox
+    agree = yield checkbox("用户协议", options=['I agree to terms and conditions'])
+    put_markdown(f'`{repr(agree)}`')
+
+    # Text Area
+    text = yield textarea('Text Area', rows=3, placeholder='Some text')
+    put_markdown(f'`{repr(text)}`')
+
+    # 文件上传
+    img = yield file_upload("Select a image:", accept="image/*")
+    put_markdown(f'`{repr(img)}`')
+
+    # 输入参数
+    res = yield input('This is label', type=TEXT, placeholder='This is placeholder,required=True',
+                      help_text='This is help text', required=True)
+    put_markdown(f'`{repr(res)}`')
+
+    # 校验函数
+    def check_age(p):  # 检验函数校验通过时返回None,否则返回错误消息
+        if p < 10:
+            return 'Too young!!'
+        if p > 60:
+            return 'Too old!!'
+
+    age = yield input("How old are you?", type=NUMBER, valid_func=check_age, help_text='age in [10, 60]')
+    put_markdown(f'`{repr(age)}`')
+
+    # Codemirror
+    code = yield textarea('Code Edit', code={
+        'mode': "python",  # 编辑区代码语言
+        'theme': 'darcula',  # 编辑区darcula主题, Visit https://codemirror.net/demo/theme.html#cobalt to get more themes
+    }, value='import something\n# Write your python code')
+    put_markdown(f'`{repr(code)}`')
+
+    # 输入组
+    info = yield input_group("Cancelable", [
+        input('Input your name', name='name'),
+        input('Input your age', name='age', type=NUMBER, valid_func=check_age, help_text='age in [10, 60]')
+    ], cancelable=True)
+    put_markdown(f'`{repr(info)}`')
+
+    def check_form(data):  # 检验函数校验通过时返回None,否则返回 (input name,错误消息)
+        if len(data['password']) > 6:
+            return ('password', 'password太长!')
+
+    check_item_data = []
+
+    def check_item(data):
+        check_item_data.append(repr(data))
+
+    info = yield input_group('Input group', [
+        input('Text', type=TEXT, datalist=['data-%s' % i for i in range(10)], name='text',
+              required=True, help_text='required=True', valid_func=check_item),
+        input('Number', type=NUMBER, value="42", name='number', valid_func=check_item),
+        input('Float', type=FLOAT, name='float', valid_func=check_item),
+        input('Password', type=PASSWORD, name='password', valid_func=check_item),
+
+        textarea('Textarea', rows=3, maxlength=20, name='textarea',
+                 help_text='rows=3, maxlength=20', valid_func=check_item),
+
+        textarea('Code', name='code', code={
+            'lineNumbers': False,
+            'indentUnit': 2,
+        }, value='import something\n# Write your python code', valid_func=check_item),
+
+        select('select-multiple', [
+            {'label': '标签0,selected', 'value': '0', 'selected': True},
+            {'label': '标签1,disabled', 'value': '1', 'disabled': True},
+            ('标签2,selected', '2', True),
+            ('标签3', '3'),
+            ('标签4,disabled', '4', False, True),
+            '标签5,selected',
+        ], name='select-multiple', multiple=True, value=['标签5,selected'], required=True,
+               help_text='required至少选择一项', valid_func=check_item),
+
+        select('select', [
+            {'label': '标签0', 'value': '0', 'selected': False},
+            {'label': '标签1,disabled', 'value': '1', 'disabled': True},
+            ('标签2', '2', False),
+            ('标签3', '3'),
+            ('标签4,disabled', '4', False, True),
+            '标签5,selected',
+        ], name='select', value=['标签5,selected'], valid_func=check_item),
+
+        checkbox('checkbox-inline', [
+            {'label': '标签0,selected', 'value': '0', 'selected': False},
+            {'label': '标签1,disabled', 'value': '1', 'disabled': True},
+            ('标签2,selected', '2', True),
+            ('标签3', '3'),
+            ('标签4,disabled', '4', False, True),
+            '标签5,selected',
+        ], inline=True, name='checkbox-inline', value=['标签5,selected', '标签0', '标签0,selected'], valid_func=check_item),
+
+        checkbox('checkbox', [
+            {'label': '标签0,selected', 'value': '0', 'selected': True},
+            {'label': '标签1,disabled', 'value': '1', 'disabled': True},
+            ('标签2,selected', '2', True),
+            ('标签3', '3'),
+            ('标签4,disabled', '4', False, True),
+            '标签5',
+        ], name='checkbox', valid_func=check_item),
+
+        radio('radio-inline', [
+            {'label': '标签0', 'value': '0', 'selected': False},
+            {'label': '标签1,disabled', 'value': '1', 'disabled': True},
+            ('标签2', '2', False),
+            ('标签3', '3'),
+            ('标签4,disabled', '4', False, True),
+            '标签5,selected',
+        ], inline=True, name='radio-inline', value='标签5,selected', valid_func=check_item),
+
+        radio('radio', [
+            {'label': '标签0', 'value': '0', 'selected': False},
+            {'label': '标签1,disabled', 'value': '1', 'disabled': True},
+            ('标签2', '2', False),
+            ('标签3', '3'),
+            ('标签4,disabled', '4', False, True),
+            '标签5,selected',
+        ], inline=False, name='radio', value='标签5,selected', valid_func=check_item),
+
+        file_upload('file_upload', name='file_upload'),
+
+        actions('actions', [
+            {'label': '提交', 'value': 'submit'},
+            ('提交2', 'submit2'),
+            '提交3',
+            {'label': 'disabled', 'disabled': True},
+            ('重置', 'reset', 'reset'),
+            {'label': '取消', 'type': 'cancel'},
+        ], name='actions', help_text='actions'),
+
+    ], valid_func=check_form)
+
+    put_text('`valid_func()` log:')
+    put_code(json.dumps(sorted(check_item_data), indent=4, ensure_ascii=False), 'json')
+
+    put_text('Form result:')
+    if info:
+        put_code(json.dumps([repr(i) for i in sorted(info.items())], indent=4, ensure_ascii=False), 'json')
+
+    # yield actions(['Continue'])
+
+
+def background_input():
+    def background():
+        time.sleep(1)
+        res = input('background')
+        put_markdown(f'`background: {repr(res)}`')
+
+    t = threading.Thread(target=background)
+    register_thread(t)
+    t.start()
+
+    res = input('front')
+    put_markdown(f'`front: {repr(res)}`')
+
+
+async def coro_background_input():
+    async def background():
+        await asyncio.sleep(1)
+        res = await input('background')
+        put_markdown(f'`background: {repr(res)}`')
+
+    run_async(background())
+
+    res = await input('front')
+    put_markdown(f'`front: {repr(res)}`')
+
+
+async def flask_coro_background_input():
+    async def background():
+        await run_asyncio_coroutine(asyncio.sleep(1))
+        res = await input('background')
+        put_markdown(f'`background: {repr(res)}`')
+
+    run_async(background())
+
+    res = await input('front')
+    put_markdown(f'`front: {repr(res)}`')
+
+
+def test_input(browser: Chrome, percy_prefix=''):
+    """测试输入::
+
+        template.basic_input()
+        actions(['Continue'])
+        template.background_input() # 或者 await template.coro_background_input() / flask_coro_background_input
+
+    """
+    if percy_prefix:
+        percy_prefix = percy_prefix + ' '
+
+    browser.find_element_by_css_selector('input').send_keys("22")
+    browser.find_element_by_tag_name('form').submit()
+
+    time.sleep(0.5)
+    browser.find_element_by_css_selector('input').send_keys("secret")
+    browser.find_element_by_tag_name('form').submit()
+
+    time.sleep(0.5)
+    browser.find_element_by_tag_name('form').submit()
+
+    # checkbox
+    time.sleep(0.5)
+    browser.find_element_by_css_selector('input').click()
+    browser.find_element_by_tag_name('form').submit()
+
+    # Text Area
+    time.sleep(0.5)
+    browser.find_element_by_css_selector('textarea').send_keys(" ".join(str(i) for i in range(20)))
+    browser.find_element_by_tag_name('form').submit()
+
+    # file
+    time.sleep(0.5)
+    img_path = path.join(here_dir, 'assets', 'img.png')
+    browser.find_element_by_css_selector('input').send_keys(img_path)
+    browser.find_element_by_tag_name('form').submit()
+
+    # text
+    time.sleep(0.5)
+    browser.find_element_by_css_selector('input').send_keys("text")
+    browser.find_element_by_tag_name('form').submit()
+
+    # valid func, age in [10, 60]
+    time.sleep(0.5)
+    browser.find_element_by_css_selector('input').send_keys("1")
+    browser.find_element_by_tag_name('form').submit()
+    time.sleep(0.5)
+    browser.find_element_by_css_selector('input').clear()
+    browser.find_element_by_css_selector('input').send_keys("90")
+    browser.find_element_by_tag_name('form').submit()
+    time.sleep(0.5)
+    browser.find_element_by_css_selector('input').clear()
+    browser.find_element_by_css_selector('input').send_keys("23")
+    browser.find_element_by_tag_name('form').submit()
+
+    # code
+    time.sleep(0.5)
+    # browser.find_element_by_css_selector('textarea').send_keys(" ".join(str(i) for i in range(20)))
+    browser.find_element_by_tag_name('form').submit()
+
+    # Cancelable from group
+    time.sleep(0.5)
+    browser.find_element_by_css_selector('input[name="name"]').send_keys("name")
+    browser.find_element_by_css_selector('input[name="age"]').send_keys("90")
+    browser.find_element_by_tag_name('form').submit()
+    percySnapshot(browser=browser, name=percy_prefix + 'input group invalid')
+    browser.find_element_by_css_selector('input[name="age"]').clear()
+    browser.find_element_by_css_selector('input[name="age"]').send_keys("23")
+    browser.find_element_by_tag_name('form').submit()
+
+    # Input group
+    time.sleep(0.5)
+    percySnapshot(browser=browser, name=percy_prefix + 'input group all')
+    browser.find_element_by_css_selector('input[name="text"]').send_keys("name")
+    browser.find_element_by_css_selector('input[name="number"]').send_keys("20")
+    browser.find_element_by_css_selector('input[name="float"]').send_keys("3.1415")
+    browser.find_element_by_css_selector('input[name="password"]').send_keys("password")
+    browser.find_element_by_css_selector('textarea[name="textarea"]').send_keys(" ".join(str(i) for i in range(20)))
+    # browser.find_element_by_css_selector('[name="code"]').send_keys(" ".join(str(i) for i in range(10)))
+    Select(browser.find_element_by_css_selector('select[name="select-multiple"]')).select_by_index(0)
+    # browser. find_element_by_css_selector('[name="select"]'). send_keys("name")
+    # browser. find_element_by_css_selector('[name="checkbox-inline"]'). send_keys("name")
+    # browser. find_element_by_css_selector('[name="checkbox"]'). send_keys("name")
+    # browser. find_element_by_css_selector('[name="radio-inline"]'). send_keys("name")
+    # browser. find_element_by_css_selector('[name="radio"]'). send_keys("name")
+    browser.find_element_by_css_selector('input[name="file_upload"]').send_keys(img_path)
+
+    browser.find_element_by_css_selector('button[value="submit2"]').click()
+    time.sleep(0.5)
+    percySnapshot(browser=browser, name=percy_prefix + 'input group all invalid')
+
+    browser.find_element_by_css_selector('input[name="password"]').clear()
+    browser.find_element_by_css_selector('input[name="password"]').send_keys("123")
+    browser.find_element_by_css_selector('button[value="submit2"]').click()
+    time.sleep(0.5)
+    percySnapshot(browser=browser, name=percy_prefix + 'input group all submit')
+
+    browser.find_element_by_css_selector('form').submit()
+
+    # background
+    time.sleep(3)
+    get_visible_form(browser).find_element_by_css_selector('input').send_keys("background")
+    get_visible_form(browser).find_element_by_tag_name('form').submit()
+    # front
+    time.sleep(0.5)
+    get_visible_form(browser).find_element_by_css_selector('input').send_keys("front")
+    get_visible_form(browser).find_element_by_tag_name('form').submit()
+
+
+def set_defer_call():
+    def deferred_1():
+        open('test_defer.tmp', 'w').write('deferred_1')
+
+    def deferred_2():
+        open('test_defer.tmp', 'a').write('deferred_2')
+
+    defer_call(deferred_1)
+    defer_call(deferred_2)
+
+
+def test_defer_call():
+    output = open('test_defer.tmp').read()
+    assert "deferred_1" in output
+    assert "deferred_2" in output
+
+    os.remove('test_defer.tmp')

+ 29 - 22
test/util.py

@@ -1,9 +1,11 @@
+import asyncio
+import signal
 import subprocess
-import sys, os, signal
+import sys
 
 from selenium import webdriver
+
 from pywebio.utils import wait_host_port
-import asyncio
 
 default_chrome_options = webdriver.ChromeOptions()
 default_chrome_options.add_argument('--no-sandbox')
@@ -16,7 +18,7 @@ python {name}
     启动PyWebIO服务器
 
 python {name} auto
-    使用无头浏览器自动化测试
+    使用无头浏览器进行自动化测试,并使用coverage检测代码覆盖率
 
 python {name} debug
     使用带界面的浏览器自动化测试
@@ -34,7 +36,10 @@ def run_test(server_func, test_func, port=8080, chrome_options=None):
         return
 
     if len(sys.argv) != 2:
-        server_func()
+        try:
+            server_func()
+        except KeyboardInterrupt:
+            pass
         sys.exit()
 
     if chrome_options is None:
@@ -42,23 +47,25 @@ def run_test(server_func, test_func, port=8080, chrome_options=None):
 
     if sys.argv[-1] == 'auto':
         default_chrome_options.add_argument('--headless')
-
-    if sys.argv[-1] in ('auto', 'debug'):
         proc = subprocess.Popen(['coverage', 'run', '--source', 'pywebio',
-                                 sys.argv[0]], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
-        browser = None
-        try:
-            browser = webdriver.Chrome(chrome_options=chrome_options)
-            asyncio.run(wait_host_port('localhost', port))
-            browser.get('http://localhost:%s' % port)
-            browser.implicitly_wait(10)
-            test_func(proc, browser)
-        finally:
-            if browser:
-                browser.quit()
+                                 sys.argv[0]], stdout=sys.stdout, stderr=subprocess.STDOUT, text=True)
+    elif sys.argv[-1] == 'debug':
+        proc = subprocess.Popen(['python3', sys.argv[0]], stdout=sys.stdout, stderr=subprocess.STDOUT, text=True)
 
-            # 不要使用 proc.terminate() ,因为coverage会无法保存分析数据
-            proc.send_signal(signal.SIGINT)
-            print("Closed browser and PyWebIO server")
-    else:
-        print(USAGE.format(name=sys.argv[0]))
+    browser = None
+    try:
+        browser = webdriver.Chrome(chrome_options=chrome_options)
+        asyncio.run(wait_host_port('localhost', port))
+        browser.get('http://localhost:%s?_pywebio_debug=1' % port)
+        browser.implicitly_wait(10)
+        test_func(proc, browser)
+    finally:
+        if browser:
+            if sys.argv[-1] == 'debug':
+                input('press ENTER to exit')
+
+            browser.quit()
+
+        # 不要使用 proc.terminate() ,因为coverage会无法保存分析数据
+        proc.send_signal(signal.SIGINT)
+        print("Closed browser and PyWebIO server")