MVP сайта
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
import os
|
||||
import sys
|
||||
import socket
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
import requests
|
||||
import pytest
|
||||
from playwright.sync_api import sync_playwright,expect
|
||||
|
||||
pytestmark=pytest.mark.skipif(os.environ.get('LAB_BROWSER_TESTS')!='1',reason='Enable LAB_BROWSER_TESTS=1 for real browser tests')
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def live(tmp_path_factory):
|
||||
folder=tmp_path_factory.mktemp('live-server')
|
||||
with socket.socket() as sock:
|
||||
sock.bind(('127.0.0.1',0));port=sock.getsockname()[1]
|
||||
env=dict(os.environ,LAB_TEST_PORT=str(port),LAB_TEST_DATA=str(folder/'data'),PYTHONPATH=str(ROOT/'src'),PYTHONIOENCODING='utf-8')
|
||||
with (folder/'server.log').open('w',encoding='utf-8') as log:
|
||||
process=subprocess.Popen([sys.executable,str(ROOT/'tests/browser_server.py')],env=env,stdout=log,stderr=log)
|
||||
base=f'http://127.0.0.1:{port}'
|
||||
try:
|
||||
deadline=time.monotonic()+25
|
||||
while time.monotonic()<deadline:
|
||||
try:
|
||||
if requests.get(base+'/healthz',timeout=.5).ok: break
|
||||
except requests.RequestException: pass
|
||||
if process.poll() is not None: raise RuntimeError((folder/'server.log').read_text('utf-8'))
|
||||
time.sleep(.1)
|
||||
else: raise RuntimeError('Server startup timeout')
|
||||
yield base,folder
|
||||
finally:
|
||||
process.terminate()
|
||||
try: process.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired: process.kill();process.wait()
|
||||
|
||||
@pytest.fixture
|
||||
def page():
|
||||
with sync_playwright() as p:
|
||||
channel=os.environ.get('PLAYWRIGHT_CHANNEL')
|
||||
browser=p.chromium.launch(headless=True,**({'channel':channel} if channel else {}))
|
||||
yield browser.new_page(viewport={'width':1440,'height':1000})
|
||||
browser.close()
|
||||
|
||||
def press(page,name,target):
|
||||
page.get_by_role('button',name=name,exact=True).click()
|
||||
expect(target).to_be_visible()
|
||||
|
||||
|
||||
def test_teacher_import_and_publish(page,live):
|
||||
base,folder=live
|
||||
page.goto(base+'/login')
|
||||
page.get_by_label('Логин',exact=True).fill('teacher');page.get_by_label('Пароль',exact=True).fill('test-browser-password')
|
||||
page.get_by_role('button',name='Войти в кабинет').click()
|
||||
expect(page.get_by_text('Коллекции данных',exact=True)).to_be_visible()
|
||||
page.get_by_role('link',name='Загрузить данные',exact=True).click()
|
||||
page.locator('input[type=file]').set_input_files({'name':'browser.csv','mimeType':'text/csv','buffer':b'id,answer\n001,7\n002,5'})
|
||||
page.get_by_role('button',name='Проверить файл',exact=True).click()
|
||||
expect(page.locator('#import-save')).to_be_enabled()
|
||||
press(page,'Сохранить и настроить →',page.get_by_text('Включить это представление',exact=True))
|
||||
page.locator('input[type=checkbox]').first.check()
|
||||
expect(page.get_by_text('Поля и подписи',exact=True)).to_be_visible()
|
||||
press(page,'Сохранить черновик',page.get_by_text('Черновик сохранён',exact=True))
|
||||
with page.expect_popup() as opened:
|
||||
page.get_by_role('link',name='Предпросмотр ↗',exact=True).click()
|
||||
expect(opened.value.locator('.record')).to_have_count(2)
|
||||
opened.value.close()
|
||||
press(page,'Опубликовать новую версию',page.get_by_text('Версия 1 · Доступна',exact=True))
|
||||
url=page.get_by_role('link',name='HTML · BeautifulSoup ↗').first.get_attribute('href')
|
||||
page.goto(base+url)
|
||||
expect(page.locator('.record')).to_have_count(2)
|
||||
assert page.locator('.field').all_text_contents()==['001','7','002','5']
|
||||
|
||||
@pytest.mark.parametrize('kind',['table','cards','messages','questionnaire'])
|
||||
def test_views_filters_mobile_and_scrapers(page,live,kind):
|
||||
base,folder=live
|
||||
sys.path.insert(0,str(ROOT/'examples/parsers'))
|
||||
from scrape_html import scrape as html_scrape
|
||||
from common import extract
|
||||
expected=html_scrape(f'{base}/html/browser-study/v/1/{kind}')
|
||||
assert len(expected)==51 and expected[0]['fields'][0]['text']=='Person 001'
|
||||
assert 'DO-NOT-PUBLISH' not in str(expected)
|
||||
page.goto(f'{base}/interactive/browser-study/v/1/{kind}')
|
||||
expect(page.locator('.record')).to_have_count(25)
|
||||
for count in (50,51):
|
||||
for attempt in range(3):
|
||||
page.get_by_role('button',name='Загрузить ещё',exact=True).click()
|
||||
try: expect(page.locator('.record')).to_have_count(count,timeout=1800);break
|
||||
except AssertionError:
|
||||
if attempt==2: raise
|
||||
assert extract(page.content())==expected
|
||||
page.get_by_label('Поиск в коллекции',exact=True).fill('WALK')
|
||||
expect(page.locator('.record')).to_have_count(17)
|
||||
page.set_viewport_size({'width':390,'height':844})
|
||||
page.wait_for_function('document.documentElement.scrollWidth <= innerWidth+1')
|
||||
page.screenshot(path=str(folder/f'{kind}-mobile.png'),full_page=True)
|
||||
assert 'DO-NOT-PUBLISH' not in page.content()
|
||||
|
||||
def test_playwright_example(live):
|
||||
base,_=live
|
||||
sys.path.insert(0,str(ROOT/'examples/parsers'))
|
||||
from scrape_interactive import scrape
|
||||
records=scrape(f'{base}/interactive/browser-study/v/1/table',os.environ.get('PLAYWRIGHT_CHANNEL'))
|
||||
assert len(records)==51 and [r['id'] for r in records]==[str(n) for n in range(1,52)]
|
||||
|
||||
|
||||
def test_live_editor_and_mobile_catalog(page,live):
|
||||
base,folder=live
|
||||
page.goto(base+'/login')
|
||||
page.get_by_label('Логин',exact=True).fill('teacher')
|
||||
page.get_by_label('Пароль',exact=True).fill('test-browser-password')
|
||||
page.get_by_role('button',name='Войти в кабинет').click()
|
||||
expect(page.locator('.catalog-title').filter(has_text='Browser study')).to_be_visible()
|
||||
page.screenshot(path=str(folder/'catalog-desktop.png'),full_page=True)
|
||||
page.get_by_role('link',name='Открыть Browser study',exact=True).click()
|
||||
preview=page.locator('.editor-preview')
|
||||
expect(preview.locator('.record')).to_have_count(5)
|
||||
label=page.get_by_label('Подпись поля',exact=True).first
|
||||
label.fill('Имя участника')
|
||||
label.press('Tab')
|
||||
expect(preview.locator('th').filter(has_text='Имя участника')).to_be_visible()
|
||||
assert preview.evaluate('(e)=>e.getBoundingClientRect().right <= e.closest(".editor-layout").getBoundingClientRect().right + 1')
|
||||
page.screenshot(path=str(folder/'editor-desktop.png'),full_page=True)
|
||||
# Live edits have not changed the public snapshot.
|
||||
response=page.request.get(base+'/html/browser-study/v/1/table')
|
||||
assert response.ok and 'Имя участника' not in response.text()
|
||||
page.set_viewport_size({'width':390,'height':844})
|
||||
page.wait_for_function('document.documentElement.scrollWidth <= innerWidth+1',timeout=5000)
|
||||
page.screenshot(path=str(folder/'editor-mobile.png'),full_page=True)
|
||||
page.once('dialog',lambda dialog:dialog.accept())
|
||||
page.get_by_role('link',name='Мои коллекции',exact=True).click()
|
||||
expect(page.get_by_text('Коллекции данных',exact=True)).to_be_visible()
|
||||
page.wait_for_function('document.documentElement.scrollWidth <= innerWidth+1')
|
||||
page.screenshot(path=str(folder/'catalog-mobile.png'),full_page=True)
|
||||
|
||||
|
||||
def test_collection_home_link_uses_http_page(page,live):
|
||||
base,_=live
|
||||
page.goto(base+'/interactive/browser-study/v/1/table')
|
||||
expect(page.locator('.record')).to_have_count(25)
|
||||
page.get_by_role('link',name='Parser Lab / Коллекции',exact=True).click()
|
||||
expect(page.get_by_role('heading',name='Учебные коллекции данных',exact=True)).to_be_visible()
|
||||
assert page.url==base+'/'
|
||||
page.go_back()
|
||||
expect(page.locator('.record')).to_have_count(25)
|
||||
|
||||
|
||||
def test_logout_opens_http_login_page(page,live):
|
||||
base,_=live
|
||||
page.goto(base+'/login')
|
||||
page.get_by_label('Логин',exact=True).fill('teacher')
|
||||
page.get_by_label('Пароль',exact=True).fill('test-browser-password')
|
||||
page.get_by_role('button',name='Войти в кабинет').click()
|
||||
expect(page.get_by_text('Коллекции данных',exact=True)).to_be_visible()
|
||||
press(page,'Выйти',page.get_by_label('Пароль',exact=True))
|
||||
assert page.url==base+'/login'
|
||||
page.goto(base+'/admin')
|
||||
expect(page.get_by_label('Пароль',exact=True)).to_be_visible()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('width',[1440,390])
|
||||
def test_html_interactive_visual_parity(page,live,width):
|
||||
base,folder=live
|
||||
page.set_viewport_size({'width':width,'height':1000})
|
||||
selectors=['.topbar','.brand','.public-main','.page-heading h1','.version-badge','.collection-meta','.filters','.filters input','.filters select','.record:first-child']
|
||||
def metrics():
|
||||
return page.evaluate('''selectors=>Object.fromEntries(selectors.map(selector=>{
|
||||
const e=document.querySelector(selector);if(!e)return [selector,null];
|
||||
const r=e.getBoundingClientRect(),s=getComputedStyle(e);
|
||||
return [selector,{x:r.x,y:r.y,width:r.width,height:r.height,font:s.fontFamily,size:s.fontSize,color:s.color,background:s.backgroundColor}];
|
||||
}))''',selectors)
|
||||
page.goto(base+'/html/browser-study/v/1/table')
|
||||
page.evaluate('document.fonts.ready')
|
||||
expected=metrics()
|
||||
page.screenshot(path=str(folder/f'html-parity-{width}.png'))
|
||||
page.goto(base+'/interactive/browser-study/v/1/table')
|
||||
expect(page.locator('.record')).to_have_count(25)
|
||||
page.evaluate('document.fonts.ready')
|
||||
actual=metrics()
|
||||
page.screenshot(path=str(folder/f'interactive-parity-{width}.png'))
|
||||
for selector in selectors:
|
||||
assert actual[selector] is not None,selector
|
||||
for key in (('x','y','width') if selector=='.public-main' else ('x','y','width','height')):
|
||||
assert abs(actual[selector][key]-expected[selector][key])<=2,(selector,key,actual[selector],expected[selector])
|
||||
for key in ('font','size','color','background'):
|
||||
assert actual[selector][key]==expected[selector][key],(selector,key)
|
||||
page.get_by_label('Поиск в коллекции',exact=True).fill('walk')
|
||||
expect(page.locator('.record')).to_have_count(17)
|
||||
page.get_by_role('link',name='Сбросить',exact=True).click()
|
||||
expect(page.locator('.record')).to_have_count(25)
|
||||
page.get_by_label('Категория',exact=True).select_option(label='Rest')
|
||||
expect(page.locator('.collection-meta b')).to_have_text('34')
|
||||
expect(page.locator('.record')).to_have_count(25)
|
||||
page.get_by_role('button',name='Загрузить ещё',exact=True).click()
|
||||
expect(page.locator('.record')).to_have_count(34)
|
||||
|
||||
|
||||
def test_json_pointer_and_stale_import_preview(page,live):
|
||||
base,_=live
|
||||
page.goto(base+'/login')
|
||||
page.get_by_label('Логин',exact=True).fill('teacher')
|
||||
page.get_by_label('Пароль',exact=True).fill('test-browser-password')
|
||||
page.get_by_role('button',name='Войти в кабинет').click()
|
||||
page.get_by_role('link',name='Загрузить данные',exact=True).click()
|
||||
upload=page.locator('input[type=file]')
|
||||
upload.set_input_files(str(ROOT/'examples/data/questionnaires.json'))
|
||||
page.locator('#inspect').click()
|
||||
expect(page.locator('#import-save')).to_be_enabled()
|
||||
expect(page.get_by_label('Записи внутри JSON',exact=True)).to_have_value('/participants')
|
||||
page.locator('#import-save').click()
|
||||
expect(page.get_by_label('Представление',exact=True)).to_have_value('questionnaire')
|
||||
expect(page.get_by_label('Структура анкеты',exact=True)).to_have_value('pairs')
|
||||
expect(page.get_by_label('Массив ответов',exact=True)).to_have_value('/answers')
|
||||
expect(page.get_by_label('Поле вопроса внутри ответа',exact=True)).to_have_value('/question')
|
||||
expect(page.get_by_label('Поле ответа',exact=True)).to_have_value('/answer')
|
||||
expect(page.locator('.editor-preview .record')).to_have_count(2)
|
||||
expect(page.locator('.editor-preview .field-pair dd')).to_have_text(['8','Прогулка','true','5','null','false'])
|
||||
expect(page.locator('.editor-preview h2')).to_have_text(['A01','A02'])
|
||||
press(page,'Сохранить черновик',page.get_by_text('Черновик сохранён',exact=True))
|
||||
page.get_by_role('link',name='Импорт файла',exact=True).click()
|
||||
upload.set_input_files({'name':'valid.csv','mimeType':'text/csv','buffer':b'id,value\n1,2'})
|
||||
page.locator('#inspect').click()
|
||||
expect(page.locator('#import-save')).to_be_enabled()
|
||||
upload.set_input_files({'name':'broken.json','mimeType':'application/json','buffer':b'{'})
|
||||
expect(page.locator('#import-save')).to_be_disabled()
|
||||
expect(page.locator('#import-result .record')).to_have_count(0)
|
||||
page.locator('#inspect').click()
|
||||
expect(page.locator('#status')).to_have_class('error')
|
||||
expect(page.locator('#import-save')).to_be_disabled()
|
||||
|
||||
|
||||
def test_dynamic_page_two_starts_from_requested_page(page,live):
|
||||
base,_=live
|
||||
page.goto(base+'/interactive/browser-study/v/1/table?page=2')
|
||||
expect(page.locator('.record').first).to_have_attribute('data-record-id','26')
|
||||
page.get_by_role('button',name='Загрузить ещё',exact=True).click()
|
||||
expect(page.locator('.record')).to_have_count(26)
|
||||
expect(page.locator('.record').last).to_have_attribute('data-record-id','51')
|
||||
|
||||
|
||||
def test_repair_legacy_questionnaire_in_browser(page,live):
|
||||
base,_=live
|
||||
page.goto(base+'/login')
|
||||
page.get_by_label('Логин',exact=True).fill('teacher')
|
||||
page.get_by_label('Пароль',exact=True).fill('test-browser-password')
|
||||
page.get_by_role('button',name='Войти в кабинет').click()
|
||||
csrf=page.locator('meta[name=csrf-token]').get_attribute('content')
|
||||
response=page.request.post(base+'/api/admin/import',headers={'origin':base,'x-csrf-token':csrf},multipart={'file':{'name':'questionnaires.json','mimeType':'application/json','buffer':(ROOT/'examples/data/questionnaires.json').read_bytes()},'root_confirmed':'1','title':'Legacy questionnaire'})
|
||||
assert response.ok
|
||||
page.goto(base+'/admin/datasets/'+response.json()['id'])
|
||||
page.get_by_role('button',name='Настроить анкеты автоматически',exact=True).click()
|
||||
expect(page.locator('.editor-preview .record')).to_have_count(2)
|
||||
expect(page.locator('.editor-preview .field-pair dd')).to_have_text(['8','Прогулка','true','5','null','false'])
|
||||
page.reload()
|
||||
expect(page.locator('.editor-preview h2')).to_have_text(['A01','A02'])
|
||||
page.get_by_role('button',name='Опубликовать новую версию',exact=True).click()
|
||||
expect(page.get_by_text('Версия 1 · Доступна',exact=True)).to_be_visible()
|
||||
Reference in New Issue
Block a user