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()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()