Spaces:
Running on Zero
Running on Zero
Make example switching reuse preloaded images and sessions instead of re-encoding and downloading
Browse files- .gitattributes +2 -0
- app.py +27 -5
- examples/chinese-preview.webp +3 -0
- examples/english-preview.webp +3 -0
- static/app.js +29 -6
- static/index.html +3 -1
- tests/test_api.py +22 -0
- tests/test_examples.mjs +86 -0
.gitattributes
CHANGED
|
@@ -35,3 +35,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
examples/chinese.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
examples/english.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
examples/chinese.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
examples/english.png filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
examples/chinese-preview.webp filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
examples/english-preview.webp filter=lfs diff=lfs merge=lfs -text
|
app.py
CHANGED
|
@@ -18,6 +18,7 @@ import urllib.request
|
|
| 18 |
import uuid
|
| 19 |
import zipfile
|
| 20 |
from concurrent.futures import ThreadPoolExecutor
|
|
|
|
| 21 |
from pathlib import Path
|
| 22 |
|
| 23 |
from fastapi import FastAPI, File, HTTPException, UploadFile
|
|
@@ -53,7 +54,23 @@ if os.environ.get("PIXTEXT_GRADIO") == "1":
|
|
| 53 |
else:
|
| 54 |
app = FastAPI(title="PixText Studio", docs_url=None, redoc_url=None)
|
| 55 |
app.mount("/static", StaticFiles(directory=ROOT / "static"), name="static")
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
|
| 59 |
class ExampleRequest(BaseModel):
|
|
@@ -109,14 +126,19 @@ def make_session(image: Image.Image, example: str | None = None):
|
|
| 109 |
sid = uuid.uuid4().hex
|
| 110 |
directory = RUNTIME / sid
|
| 111 |
directory.mkdir()
|
| 112 |
-
|
| 113 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
SESSIONS[sid] = {
|
| 115 |
"directory": directory, "size": image.size, "example": example,
|
| 116 |
"touched": time.monotonic(), "job": None,
|
| 117 |
}
|
| 118 |
return {"id": sid, "width": image.width, "height": image.height,
|
| 119 |
-
"image": f"/api/sessions/{sid}/preview.webp",
|
|
|
|
| 120 |
|
| 121 |
|
| 122 |
def update_job(jid: str, **values):
|
|
@@ -320,7 +342,7 @@ def example_session(request: ExampleRequest):
|
|
| 320 |
if request.example not in EXAMPLES:
|
| 321 |
raise HTTPException(404, "Example not found")
|
| 322 |
with Image.open(ROOT / "examples" / f"{request.example}.png") as image:
|
| 323 |
-
session = make_session(image
|
| 324 |
return {**session, **example_preset(request.example)}
|
| 325 |
|
| 326 |
|
|
|
|
| 18 |
import uuid
|
| 19 |
import zipfile
|
| 20 |
from concurrent.futures import ThreadPoolExecutor
|
| 21 |
+
from functools import lru_cache
|
| 22 |
from pathlib import Path
|
| 23 |
|
| 24 |
from fastapi import FastAPI, File, HTTPException, UploadFile
|
|
|
|
| 54 |
else:
|
| 55 |
app = FastAPI(title="PixText Studio", docs_url=None, redoc_url=None)
|
| 56 |
app.mount("/static", StaticFiles(directory=ROOT / "static"), name="static")
|
| 57 |
+
|
| 58 |
+
class ExampleFiles(StaticFiles):
|
| 59 |
+
async def get_response(self, path, scope):
|
| 60 |
+
response = await super().get_response(path, scope)
|
| 61 |
+
if path.endswith("-preview.webp") and response.status_code in (200, 304):
|
| 62 |
+
response.headers["Cache-Control"] = "public, max-age=31536000, immutable"
|
| 63 |
+
return response
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
app.mount("/examples", ExampleFiles(directory=ROOT / "examples"), name="examples")
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
@lru_cache(maxsize=2)
|
| 70 |
+
def example_preview(example: str):
|
| 71 |
+
path = ROOT / "examples" / f"{example}-preview.webp"
|
| 72 |
+
version = hashlib.sha256(path.read_bytes()).hexdigest()[:12]
|
| 73 |
+
return f"/examples/{path.name}?v={version}"
|
| 74 |
|
| 75 |
|
| 76 |
class ExampleRequest(BaseModel):
|
|
|
|
| 126 |
sid = uuid.uuid4().hex
|
| 127 |
directory = RUNTIME / sid
|
| 128 |
directory.mkdir()
|
| 129 |
+
if example:
|
| 130 |
+
shutil.copyfile(ROOT / "examples" / f"{example}.png", directory / "source.png")
|
| 131 |
+
shutil.copyfile(ROOT / "examples" / f"{example}-preview.webp", directory / "preview.webp")
|
| 132 |
+
else:
|
| 133 |
+
image.save(directory / "source.png")
|
| 134 |
+
image.save(directory / "preview.webp", quality=94, method=4)
|
| 135 |
SESSIONS[sid] = {
|
| 136 |
"directory": directory, "size": image.size, "example": example,
|
| 137 |
"touched": time.monotonic(), "job": None,
|
| 138 |
}
|
| 139 |
return {"id": sid, "width": image.width, "height": image.height,
|
| 140 |
+
"image": example_preview(example) if example else f"/api/sessions/{sid}/preview.webp",
|
| 141 |
+
"example": example}
|
| 142 |
|
| 143 |
|
| 144 |
def update_job(jid: str, **values):
|
|
|
|
| 342 |
if request.example not in EXAMPLES:
|
| 343 |
raise HTTPException(404, "Example not found")
|
| 344 |
with Image.open(ROOT / "examples" / f"{request.example}.png") as image:
|
| 345 |
+
session = make_session(image, request.example)
|
| 346 |
return {**session, **example_preset(request.example)}
|
| 347 |
|
| 348 |
|
examples/chinese-preview.webp
ADDED
|
Git LFS Details
|
examples/english-preview.webp
ADDED
|
Git LFS Details
|
static/app.js
CHANGED
|
@@ -6,6 +6,22 @@ let gesture = null;
|
|
| 6 |
const clone = (x) => structuredClone(x);
|
| 7 |
const escape = (s) => String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
| 8 |
const endpoint = (name) => `/api/sessions/${state.session.id}/${name}`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
async function api(url, body, isForm=false) {
|
| 11 |
const response = await fetch(url, body === undefined ? {} : {method:'POST', headers:isForm?{}:{'Content-Type':'application/json'}, body:isForm?body:JSON.stringify(body)});
|
|
@@ -28,13 +44,13 @@ function changed() {
|
|
| 28 |
updateControls();
|
| 29 |
}
|
| 30 |
function selected() {return state.regions.find(r=>r.id===state.selected);}
|
| 31 |
-
function setBusy(busy) {
|
| 32 |
state.busy=busy;
|
| 33 |
document.querySelectorAll('[data-example],[data-model],#uploadButton,#exampleTargets,#resetButton,#drawTool,#undoButton,#targetText,#enableRegion,#deleteRegion,.coords input,.settings input,.region-row input').forEach(e=>e.disabled=busy);
|
| 34 |
$('regionCanvas').style.pointerEvents=busy?'none':'';
|
| 35 |
$('jobProgress').hidden=!busy;
|
| 36 |
$('jobProgress').classList.toggle('waiting',busy);
|
| 37 |
-
if(busy){$('jobMessage').textContent=
|
| 38 |
updateControls();
|
| 39 |
}
|
| 40 |
function updateControls() {
|
|
@@ -63,7 +79,8 @@ function applySettings(settings) {
|
|
| 63 |
if(settings[key]!=null)$(id).value=settings[key];
|
| 64 |
}
|
| 65 |
}
|
| 66 |
-
async function loadSession(session) {
|
|
|
|
| 67 |
state.session=session;state.regions=clone(session.regions||[]);state.selected=state.regions[0]?.id||null;state.history=[];state.result=null;state.stale=false;state.tool='select';
|
| 68 |
if(session.settings)applySettings(session.settings);
|
| 69 |
$('regionResults').hidden=true;$('compareView').disabled=true;
|
|
@@ -72,14 +89,18 @@ async function loadSession(session) {
|
|
| 72 |
const image=$('sourceImage'); image.setAttribute('href',session.image); image.setAttribute('width',session.width);image.setAttribute('height',session.height);
|
| 73 |
$('imageSize').textContent=`${session.width} × ${session.height}`;
|
| 74 |
$('wipe').style.aspectRatio=`${session.width} / ${session.height}`;
|
| 75 |
-
state.source=
|
| 76 |
-
await state.source.decode();
|
| 77 |
document.querySelectorAll('[data-example]').forEach(e=>e.classList.toggle('active',e.dataset.example===session.example));
|
| 78 |
switchView('edit');setTool('select');render();notice();
|
| 79 |
}
|
| 80 |
async function loadExample(example) {
|
| 81 |
if(state.busy)return;
|
| 82 |
-
try{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
}
|
| 84 |
async function upload(file) {
|
| 85 |
if(!file||state.busy)return;
|
|
@@ -272,6 +293,8 @@ async function openStudio(){
|
|
| 272 |
}catch(e){notice('This preview has expired. Choose an example to start a new image.');}
|
| 273 |
finally{setBusy(false);}
|
| 274 |
}
|
|
|
|
|
|
|
| 275 |
await loadExample('english');
|
| 276 |
}
|
| 277 |
openStudio();
|
|
|
|
| 6 |
const clone = (x) => structuredClone(x);
|
| 7 |
const escape = (s) => String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
| 8 |
const endpoint = (name) => `/api/sessions/${state.session.id}/${name}`;
|
| 9 |
+
const exampleCache = new Map();
|
| 10 |
+
|
| 11 |
+
function prepareExample(example) {
|
| 12 |
+
const cached = exampleCache.get(example);
|
| 13 |
+
if(cached && cached.expires > Date.now()) return cached.promise;
|
| 14 |
+
const entry = {expires:Date.now()+45*60*1000};
|
| 15 |
+
entry.promise = (async()=>{
|
| 16 |
+
const session = await api('/api/session',{example});
|
| 17 |
+
const image = new Image(); image.src=session.image;
|
| 18 |
+
await image.decode();
|
| 19 |
+
return {session,image};
|
| 20 |
+
})();
|
| 21 |
+
exampleCache.set(example,entry);
|
| 22 |
+
entry.promise.catch(()=>{if(exampleCache.get(example)===entry)exampleCache.delete(example);});
|
| 23 |
+
return entry.promise;
|
| 24 |
+
}
|
| 25 |
|
| 26 |
async function api(url, body, isForm=false) {
|
| 27 |
const response = await fetch(url, body === undefined ? {} : {method:'POST', headers:isForm?{}:{'Content-Type':'application/json'}, body:isForm?body:JSON.stringify(body)});
|
|
|
|
| 44 |
updateControls();
|
| 45 |
}
|
| 46 |
function selected() {return state.regions.find(r=>r.id===state.selected);}
|
| 47 |
+
function setBusy(busy, message='Waiting for the model') {
|
| 48 |
state.busy=busy;
|
| 49 |
document.querySelectorAll('[data-example],[data-model],#uploadButton,#exampleTargets,#resetButton,#drawTool,#undoButton,#targetText,#enableRegion,#deleteRegion,.coords input,.settings input,.region-row input').forEach(e=>e.disabled=busy);
|
| 50 |
$('regionCanvas').style.pointerEvents=busy?'none':'';
|
| 51 |
$('jobProgress').hidden=!busy;
|
| 52 |
$('jobProgress').classList.toggle('waiting',busy);
|
| 53 |
+
if(busy){$('jobMessage').textContent=message;$('jobPercent').textContent='';$('progressFill').style.width='0%';}
|
| 54 |
updateControls();
|
| 55 |
}
|
| 56 |
function updateControls() {
|
|
|
|
| 79 |
if(settings[key]!=null)$(id).value=settings[key];
|
| 80 |
}
|
| 81 |
}
|
| 82 |
+
async function loadSession(session, source=null) {
|
| 83 |
+
if(!source){source=new Image();source.src=session.image;await source.decode();}
|
| 84 |
state.session=session;state.regions=clone(session.regions||[]);state.selected=state.regions[0]?.id||null;state.history=[];state.result=null;state.stale=false;state.tool='select';
|
| 85 |
if(session.settings)applySettings(session.settings);
|
| 86 |
$('regionResults').hidden=true;$('compareView').disabled=true;
|
|
|
|
| 89 |
const image=$('sourceImage'); image.setAttribute('href',session.image); image.setAttribute('width',session.width);image.setAttribute('height',session.height);
|
| 90 |
$('imageSize').textContent=`${session.width} × ${session.height}`;
|
| 91 |
$('wipe').style.aspectRatio=`${session.width} / ${session.height}`;
|
| 92 |
+
state.source=source;
|
|
|
|
| 93 |
document.querySelectorAll('[data-example]').forEach(e=>e.classList.toggle('active',e.dataset.example===session.example));
|
| 94 |
switchView('edit');setTool('select');render();notice();
|
| 95 |
}
|
| 96 |
async function loadExample(example) {
|
| 97 |
if(state.busy)return;
|
| 98 |
+
try{
|
| 99 |
+
setBusy(true,'Loading example');
|
| 100 |
+
const prepared=await prepareExample(example);
|
| 101 |
+
await loadSession(clone(prepared.session),prepared.image);
|
| 102 |
+
history.replaceState(null,'',location.pathname);
|
| 103 |
+
}catch(e){notice(e.message);}finally{setBusy(false);}
|
| 104 |
}
|
| 105 |
async function upload(file) {
|
| 106 |
if(!file||state.busy)return;
|
|
|
|
| 293 |
}catch(e){notice('This preview has expired. Choose an example to start a new image.');}
|
| 294 |
finally{setBusy(false);}
|
| 295 |
}
|
| 296 |
+
// Prepare both examples together; switching reuses decoded images and untouched presets.
|
| 297 |
+
prepareExample('chinese').catch(()=>{});
|
| 298 |
await loadExample('english');
|
| 299 |
}
|
| 300 |
openStudio();
|
static/index.html
CHANGED
|
@@ -5,7 +5,9 @@
|
|
| 5 |
<meta name="theme-color" content="#faf9f6">
|
| 6 |
<title>PixText · Text repair studio</title>
|
| 7 |
<link rel="stylesheet" href="/static/style.css">
|
| 8 |
-
<link rel="
|
|
|
|
|
|
|
| 9 |
<script defer src="/static/app.js"></script>
|
| 10 |
</head>
|
| 11 |
<body>
|
|
|
|
| 5 |
<meta name="theme-color" content="#faf9f6">
|
| 6 |
<title>PixText · Text repair studio</title>
|
| 7 |
<link rel="stylesheet" href="/static/style.css">
|
| 8 |
+
<link rel="preload" as="image" href="/examples/english-preview.webp?v=8a15b66d2fd9">
|
| 9 |
+
<link rel="preload" as="image" href="/examples/chinese-preview.webp?v=dfbb3fb24330">
|
| 10 |
+
<link rel="modulepreload" href="/static/gradio-client.js">
|
| 11 |
<script defer src="/static/app.js"></script>
|
| 12 |
</head>
|
| 13 |
<body>
|
tests/test_api.py
CHANGED
|
@@ -92,3 +92,25 @@ def test_sessions_and_jobs_expire_together(client, monkeypatch):
|
|
| 92 |
new_session(client)
|
| 93 |
assert not directory.exists()
|
| 94 |
assert client.get(f"/api/sessions/{sid}/source.png").status_code == 404
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
new_session(client)
|
| 93 |
assert not directory.exists()
|
| 94 |
assert client.get(f"/api/sessions/{sid}/source.png").status_code == 404
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
@pytest.mark.parametrize('example', ['english', 'chinese'])
|
| 98 |
+
def test_example_selection_reuses_preview_without_reencoding(client, monkeypatch, example):
|
| 99 |
+
def unexpected_encoding(*args, **kwargs):
|
| 100 |
+
raise AssertionError('Example selection is re-encoding images on the request path')
|
| 101 |
+
|
| 102 |
+
with monkeypatch.context() as patch:
|
| 103 |
+
patch.setattr(Image.Image, 'save', unexpected_encoding)
|
| 104 |
+
first = client.post('/api/session', json={'example': example}).json()
|
| 105 |
+
second = client.post('/api/session', json={'example': example}).json()
|
| 106 |
+
assert first['id'] != second['id']
|
| 107 |
+
assert first['image'] == second['image']
|
| 108 |
+
preview = client.get(first['image'])
|
| 109 |
+
assert preview.status_code == 200
|
| 110 |
+
assert 'public' in preview.headers['cache-control']
|
| 111 |
+
assert 'immutable' in preview.headers['cache-control']
|
| 112 |
+
assert client.get(first['image'], headers={'If-None-Match': preview.headers['etag']}).status_code == 304
|
| 113 |
+
assert first['image'] in client.get('/').text
|
| 114 |
+
assert Image.open(io.BytesIO(preview.content)).size == (first['width'], first['height'])
|
| 115 |
+
original = client.get(f"/api/sessions/{first['id']}/source.png")
|
| 116 |
+
assert original.headers['cache-control'] == 'private, no-store'
|
tests/test_examples.mjs
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import assert from 'node:assert/strict';
|
| 2 |
+
import {readFileSync} from 'node:fs';
|
| 3 |
+
import test from 'node:test';
|
| 4 |
+
import vm from 'node:vm';
|
| 5 |
+
|
| 6 |
+
const script = readFileSync(new URL('../static/app.js', import.meta.url), 'utf8');
|
| 7 |
+
const presets = Object.fromEntries(['english', 'chinese'].map(name => [name,
|
| 8 |
+
JSON.parse(readFileSync(new URL(`../examples/${name}.json`, import.meta.url), 'utf8'))]));
|
| 9 |
+
|
| 10 |
+
// Run the real editor against a small DOM/fetch adapter; no models or browser packages needed.
|
| 11 |
+
function editor() {
|
| 12 |
+
const elements = new Map(), requests = [], decoded = [];
|
| 13 |
+
let nextId = 0, failExample = null;
|
| 14 |
+
const element = id => {
|
| 15 |
+
if(!elements.has(id)) elements.set(id, {value:'', dataset:{}, style:{setProperty(){}},
|
| 16 |
+
classList:{toggle(){},add(){},remove(){}}, setAttribute(){}, addEventListener(){},
|
| 17 |
+
getContext(){return {drawImage(){}};}, children:[], focus(){}});
|
| 18 |
+
return elements.get(id);
|
| 19 |
+
};
|
| 20 |
+
const examples = ['english','chinese'].map(example => ({...element(example),dataset:{example}}));
|
| 21 |
+
const models = ['meanflow','teacher'].map(model => ({...element(model),dataset:{model}}));
|
| 22 |
+
const context = vm.createContext({structuredClone, URLSearchParams, console, setTimeout,
|
| 23 |
+
document:{getElementById:element,addEventListener(){},querySelectorAll(selector){
|
| 24 |
+
if(selector==='[data-example]') return examples;
|
| 25 |
+
if(selector==='[data-model]') return models;
|
| 26 |
+
return [];
|
| 27 |
+
}},
|
| 28 |
+
Image:class {async decode(){decoded.push(this.src);this.complete=true;}},
|
| 29 |
+
location:{pathname:'/',hash:''}, history:{replaceState(){}},
|
| 30 |
+
fetch:async(url,options) => {
|
| 31 |
+
if(url==='/api/config') return {ok:true,json:async()=>({transport:'http'})};
|
| 32 |
+
assert.equal(url,'/api/session');
|
| 33 |
+
const {example}=JSON.parse(options.body);requests.push(example);
|
| 34 |
+
if(example===failExample){failExample=null;throw new Error('Network unavailable');}
|
| 35 |
+
return {ok:true,json:async()=>({id:`session-${++nextId}`,example,width:1024,height:1024,
|
| 36 |
+
image:`/examples/${example}-preview.webp?v=release`,
|
| 37 |
+
regions:presets[example].map((r,i)=>({...r,id:`example-${i}`,enabled:true,origin:'example'})),
|
| 38 |
+
settings:{variant:'meanflow',steps:4,seed:42,guidance:2.6,text_threshold:.5}})};
|
| 39 |
+
}});
|
| 40 |
+
vm.runInContext(script,context);
|
| 41 |
+
const run=code=>vm.runInContext(code,context);
|
| 42 |
+
return {run,requests,decoded,element,failNext(example){failExample=example;}};
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
async function ready(app) {
|
| 46 |
+
await app.run("Promise.all([prepareExample('english'),prepareExample('chinese')])");
|
| 47 |
+
await new Promise(resolve=>setImmediate(resolve));
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
test('switching and restoring example edits reuse prepared sessions and decoded images', async()=>{
|
| 51 |
+
const app=editor();await ready(app);
|
| 52 |
+
assert.equal(app.requests.length,2);
|
| 53 |
+
assert.equal(app.decoded.length,2);
|
| 54 |
+
for(const example of ['chinese','english','chinese','english']) {
|
| 55 |
+
await app.run(`loadExample('${example}')`);
|
| 56 |
+
assert.equal(app.run('state.regions.length'),presets[example].length);
|
| 57 |
+
assert.equal(app.element('guidance').value,2.6);
|
| 58 |
+
assert.equal(app.element('refineButton').disabled,false);
|
| 59 |
+
app.run("state.regions[0].text='edited'; state.regions[0].bbox[0]=0; state.regions[0].enabled=false;");
|
| 60 |
+
}
|
| 61 |
+
await app.run("loadExample('english')");
|
| 62 |
+
assert.equal(app.run('state.regions[0].text'),presets.english[0].text);
|
| 63 |
+
assert.equal(app.run('state.regions[0].bbox[0]'),presets.english[0].bbox[0]);
|
| 64 |
+
assert.equal(app.run('state.regions[0].enabled'),true);
|
| 65 |
+
assert.equal(app.requests.length,2);
|
| 66 |
+
assert.equal(app.decoded.length,2);
|
| 67 |
+
});
|
| 68 |
+
|
| 69 |
+
test('concurrent preparation shares one request and an expired session is renewed',async()=>{
|
| 70 |
+
const app=editor();await ready(app);
|
| 71 |
+
app.run("exampleCache.get('chinese').expires=0");
|
| 72 |
+
await app.run("Promise.all([prepareExample('chinese'),prepareExample('chinese')])");
|
| 73 |
+
assert.equal(app.requests.filter(x=>x==='chinese').length,2);
|
| 74 |
+
});
|
| 75 |
+
|
| 76 |
+
test('failed preparation is retryable without replacing the current editor',async()=>{
|
| 77 |
+
const app=editor();await ready(app);
|
| 78 |
+
await app.run("loadExample('english')");
|
| 79 |
+
const current=app.run('state.session.id');
|
| 80 |
+
app.run("exampleCache.get('chinese').expires=0");app.failNext('chinese');
|
| 81 |
+
await app.run("loadExample('chinese')");
|
| 82 |
+
assert.equal(app.run('state.session.id'),current);
|
| 83 |
+
assert.equal(app.run('state.busy'),false);
|
| 84 |
+
await app.run("loadExample('chinese')");
|
| 85 |
+
assert.equal(app.run('state.regions.length'),13);
|
| 86 |
+
});
|