Files
Web-Parser-Constructor/tests/test_browser.py
T

305 lines
17 KiB
Python

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,executable_path=os.environ.get('PLAYWRIGHT_EXECUTABLE_PATH'),**({'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()
def test_independent_detail_fields_and_delete_collection(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()
page.goto(base + '/admin/import')
page.locator('input[type=file]').set_input_files({
'name': 'details.csv', 'mimeType': 'text/csv',
'buffer': b'name,bio,secret\nAlice,BIOGRAPHY,PRIVATE'})
page.get_by_role('button', name='Проверить файл', exact=True).click()
expect(page.locator('#import-save')).to_be_enabled()
page.locator('#import-save').click()
page.get_by_label('Включить это представление').check()
page.get_by_label('bio', exact=True).uncheck()
page.get_by_label('secret', exact=True).uncheck()
expect(page.locator('#live-preview')).not_to_contain_text('BIOGRAPHY')
page.get_by_label('Настройка полей').select_option('detail')
page.get_by_label('Отдельные поля полной записи').check()
page.get_by_label('bio', exact=True).check()
expect(page.locator('#live-preview')).to_contain_text('BIOGRAPHY')
expect(page.locator('#live-preview')).not_to_contain_text('PRIVATE')
page.get_by_role('button', name='Сохранить черновик', exact=True).click()
expect(page.locator('#status')).to_have_text('Черновик сохранён')
page.screenshot(path=str(folder / 'detail-editor.png'), full_page=True)
editor_url = page.url
page.reload()
page.get_by_label('Предпросмотр данных').select_option('detail')
expect(page.locator('#live-preview')).to_contain_text('BIOGRAPHY')
page.get_by_role('button', name='Опубликовать новую версию', exact=True).click()
public_url = page.get_by_role('link', name='HTML · BeautifulSoup ↗').get_attribute('href')
page.goto(base + public_url)
expect(page.locator('.lab-records')).not_to_contain_text('BIOGRAPHY')
page.locator('.detail-link').click()
expect(page.locator('.lab-records')).to_contain_text('BIOGRAPHY')
expect(page.locator('.lab-records')).not_to_contain_text('PRIVATE')
page.goto(editor_url)
page.once('dialog', lambda dialog: dialog.dismiss())
page.get_by_role('button', name='Удалить коллекцию', exact=True).click()
expect(page).to_have_url(editor_url)
page.once('dialog', lambda dialog: dialog.accept())
page.get_by_role('button', name='Удалить коллекцию', exact=True).click()
expect(page).to_have_url(base + '/admin')
assert page.request.get(base + public_url).status == 404