"""Run with Vite on port 5198 and Firefox installed; no API required.""" import json, pathlib, socket, subprocess, tempfile, time profile=pathlib.Path(tempfile.mkdtemp(prefix='home-dossier-')) with socket.socket() as s: s.bind(('127.0.0.1',0));port=s.getsockname()[1] (profile/'user.js').write_text(f'user_pref("marionette.port", {port});\nuser_pref("browser.shell.checkDefaultBrowser", false);') log=(profile/'firefox.log').open('w') firefox=subprocess.Popen(['firefox','--headless','--no-remote','--profile',str(profile),'--marionette','--remote-allow-system-access','about:blank'],stdout=log,stderr=log) sock=None try: for _ in range(100): try: sock=socket.create_connection(('127.0.0.1',port),timeout=1);break except OSError: time.sleep(.1) assert sock is not None, 'Firefox did not start' sock.settimeout(20) def read(): size=b'' while not size.endswith(b':'): size+=sock.recv(1) data=b'';length=int(size[:-1]) while len(data)e.children[0].tagName==="H2" && e.children[1].matches(".component-sample, .n-config-provider") && e.children[3].matches("pre") && e.children[3].querySelector("code"))'), 'Each component needs a heading, sample and usage code' assert js('return [...document.querySelectorAll("[id]")].map(e=>e.id).length===new Set([...document.querySelectorAll("[id]")].map(e=>e.id)).size'), 'Duplicate DOM IDs' assert js('return document.querySelectorAll(".catalog .status-stamp").length===2'), 'Optional folder status' assert js('return !document.querySelectorAll(".catalog .folder-action")[0].disabled && document.querySelectorAll(".catalog .folder-action")[1].disabled'), 'Status controls availability' ids=js('return [...document.querySelectorAll(".catalog .clue-toggle")].map(e=>e.getAttribute("aria-controls"))') assert len(set(ids))==2 js('document.querySelectorAll(".catalog .clue-toggle")[0].click();return true') wait('document.querySelectorAll(".catalog .clue-toggle")[0].getAttribute("aria-expanded")==="true"') assert js('return document.querySelectorAll(".catalog .clue-toggle")[1].getAttribute("aria-expanded")')=='false' js('document.querySelectorAll(".catalog .clue-toggle")[1].click();return true') wait('document.querySelectorAll(".catalog .clue-toggle")[1].getAttribute("aria-expanded")==="true"') js('document.querySelectorAll(".catalog .clue-toggle")[0].click();return true') wait('document.querySelectorAll(".catalog .clue-toggle")[0].getAttribute("aria-expanded")==="false"') assert js('return document.querySelectorAll(".catalog .clue-toggle")[1].getAttribute("aria-expanded")')=='true' assert js('return document.querySelector(".catalog .case-board").textContent.includes("Найден у двери")') assert js('return document.querySelector(".catalog [data-component=ObjectCover] .object-cover").textContent.includes("Чердак")') js('document.querySelector(".catalog form").dispatchEvent(new Event("submit",{bubbles:true,cancelable:true}));return true') wait('document.querySelector(".catalog").textContent.includes("Укажите название команды")') js('const e=document.querySelector("#catalog-team");e.value="Сыщики";e.dispatchEvent(new Event("input",{bubbles:true}));return true') js('document.querySelector(".catalog form").dispatchEvent(new Event("submit",{bubbles:true,cancelable:true}));return true') wait('document.querySelector(".catalog").textContent.includes("Сыщики» готова")') js('document.querySelector(".catalog .folder-action").focus();document.querySelector(".catalog .folder-action").click();return true') wait('!!document.querySelector("[role=dialog]")') assert js('return document.querySelector("[role=dialog]").getAttribute("aria-label")')=='Повторное дело' key('\ue00c') wait('!document.querySelector("[role=dialog]")') wait('document.activeElement===document.querySelector(".catalog .folder-action")') assert js('return document.querySelector(".catalog .buttons button[disabled]")!==null && document.querySelector(".catalog .buttons .n-button--loading")!==null') assert js('return getComputedStyle(document.querySelector(".catalog .buttons button")).backgroundColor')=='rgb(255, 212, 59)' js('const e=document.querySelector(".game-input-run");e.value="Сад";e.dispatchEvent(new Event("input",{bubbles:true}));return true') wait('!document.querySelector(".game-button-run").disabled') js('document.querySelector(".game-button-run").click();return true') wait('!!document.querySelector(".action-result")') assert js('return getComputedStyle(document.querySelector(".action-result")).getPropertyValue("--n-icon-color").trim()')!='#ffd43b' js('document.querySelector("#tab-components").focus();return true') key('\ue014'); wait('!!document.querySelector(".site-preview")') key('\ue012'); wait('!!document.querySelector(".catalog")') key('\ue011'); wait('!!document.querySelector(".site-preview")') key('\ue014'); wait('!!document.querySelector(".game-preview")') key('\ue014'); wait('!!document.querySelector(".catalog")') key('\ue011'); wait('!!document.querySelector(".site-preview")') print(f'PASS {width}px: three tabs, quotes, IDs, data, states, form, open, modal, themes', flush=True) command('Marionette:SetContext', {'value':'chrome'}) js('Services.prefs.setIntPref("ui.prefersReducedMotion",1);return true') command('Marionette:SetContext', {'value':'content'}) wait('matchMedia("(prefers-reduced-motion: reduce)").matches') js('document.querySelector(".clue-toggle").focus();return true') assert js('return getComputedStyle(document.querySelector(".case-folder")).transform')=='none' assert js('return getComputedStyle(document.querySelector(".case-folder")).transitionDuration')=='0s' assert js('return getComputedStyle(document.querySelector(".clues"),"::before").transitionDuration')=='0s' print('PASS reduced motion', flush=True) finally: if sock:sock.close() firefox.terminate();firefox.wait(timeout=10);log.close()