修复新闻工作台换号联动流程
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
15 files changed
+203
-34
No files matched your search
+20
-1
@@ -12,7 +12,7 @@ test('终止后较早的 heartbeat 响应不能恢复流程',async()=>{
|
||||
requests[1].onload({status:200,responseText:JSON.stringify({running:false,epoch:2})});await stop;
|
||||
});
|
||||
|
||||
test('悬浮窗跳过按钮发送页面事件并防止忙碌时重复点击',()=>{
|
||||
test('悬浮窗开始、暂停、跳过与开始响应竞态',async()=>{
|
||||
const handlers=new Map(),elements=new Map(),emitted=[],requests=[];
|
||||
const element=()=>({style:{},before(child){elements.set(child.id,child);}});
|
||||
for(const id of ['halt','port','token','save','status'])elements.set(id,element());
|
||||
@@ -33,4 +33,23 @@ test('悬浮窗跳过按钮发送页面事件并防止忙碌时重复点击',()=
|
||||
doc.dispatchEvent(new Event('dww11:local-request',{detail:JSON.stringify({id:'skip-test',route:'skip',body:{id:'job'}})}));
|
||||
assert.equal(requests[0].url,'http://127.0.0.1:8770/bridge/skip');
|
||||
requests[0].onload({status:200,responseText:'{"ok":true}'});
|
||||
const start=elements.get('start'),pause=elements.get('pause');
|
||||
assert.equal(start.textContent,'开始 / 继续');assert.equal(pause.textContent,'暂停');
|
||||
let started=start.onclick();assert.equal(start.disabled,true);
|
||||
assert.ok(requests.at(-1).url.endsWith('/bridge/start'));
|
||||
requests.at(-1).onload({status:200,responseText:'{"running":true}'});await started;
|
||||
doc.dispatchEvent(new Event('dww11:run-state',{detail:true}));assert.equal(start.disabled,true);
|
||||
const paused=pause.onclick();assert.equal(emitted.at(-1).type,'dww11:auto-stop');
|
||||
assert.ok(requests.at(-1).url.endsWith('/bridge/pause'));
|
||||
requests.at(-1).onload({status:200,responseText:'{"running":false}'});await paused;
|
||||
doc.dispatchEvent(new Event('dww11:run-state',{detail:false}));assert.equal(start.disabled,false);
|
||||
// Pause while Start is still in flight: a late Start must be followed by Pause.
|
||||
started=start.onclick();const lateStart=requests.at(-1);
|
||||
const pausing=pause.onclick();requests.at(-1).onload({status:200,responseText:'{"running":false}'});await pausing;
|
||||
lateStart.onload({status:200,responseText:'{"running":true}'});
|
||||
await new Promise(resolve=>setImmediate(resolve));
|
||||
assert.ok(requests.at(-1).url.endsWith('/bridge/pause'));
|
||||
requests.at(-1).onload({status:200,responseText:'{"running":false}'});await started;
|
||||
assert.match(elements.get('status').textContent,/已暂停/);
|
||||
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
'use strict';
|
||||
const {test}=require('node:test');
|
||||
const assert=require('node:assert/strict');
|
||||
const fs=require('node:fs');
|
||||
const path=require('node:path');
|
||||
const vm=require('node:vm');
|
||||
const flush=async()=>{for(let i=0;i<12;i++)await new Promise(resolve=>setImmediate(resolve));};
|
||||
|
||||
function fixture({holdStart=false,holdResume=false,flow=null,helloError=false}={}) {
|
||||
const elements=[],handlers=new Map(),routes=[];
|
||||
let finishStart,finishResume;
|
||||
function element(){
|
||||
const attrs=new Map();
|
||||
const el={style:{},hidden:false,disabled:false,children:[],
|
||||
append(...nodes){this.children.push(...nodes);},prepend(){},before(){},remove(){},
|
||||
addEventListener(type,fn){this[type]=fn;},removeAttribute(key){attrs.delete(key);},
|
||||
setAttribute(key,value){attrs.set(key,value);},getAttribute:key=>attrs.get(key)??null,
|
||||
attachShadow:()=>element()};
|
||||
elements.push(el);return el;
|
||||
}
|
||||
const doc={body:element(),createElement:element,querySelector:()=>null,querySelectorAll:()=>[],
|
||||
addEventListener(type,fn){handlers.set(type,fn);}};
|
||||
const context={document:doc,location:{origin:'https://www.dww11.com',hostname:'www.dww11.com',href:'https://www.dww11.com/'},
|
||||
URL,Node:{TEXT_NODE:3},MutationObserver:class{observe(){}},setTimeout,
|
||||
chrome:{runtime:{sendMessage:async({route})=>{
|
||||
routes.push(route);
|
||||
if(route==='/hello')return helloError?{ok:false,error:'unavailable'}:{ok:true,data:{flow,total:1}};
|
||||
if(route==='/start'&&holdStart)return new Promise(resolve=>{finishStart=()=>resolve({ok:true,data:{flow:{id:'test',phase:'logout',paused:false}}});});
|
||||
if(route==='/resume'&&holdResume)return new Promise(resolve=>{finishResume=()=>resolve({ok:true,data:{flow:{id:'test',phase:'logout',paused:false}}});});
|
||||
if(route==='/suspend')return {ok:true,data:{}};
|
||||
throw new Error('Unexpected route '+route);
|
||||
}}}};
|
||||
vm.runInNewContext(fs.readFileSync(path.join(__dirname,'../../dww11-account-switch-browser-extension/content.js'),'utf8'),context);
|
||||
return {routes,state:()=>JSON.parse(elements.find(e=>e.id==='dww11-extension-panel').getAttribute('data-auto-news-state')),
|
||||
click:async(text)=>{elements.find(e=>e.textContent===text).click();await flush();},
|
||||
stop:()=>handlers.get('dww11:auto-stop')(),finishStart:()=>finishStart(),finishResume:()=>finishResume()};
|
||||
}
|
||||
|
||||
test('插件公开生命周期状态,不包含账号和凭据',async()=>{
|
||||
const f=fixture();await flush();
|
||||
assert.deepEqual(f.state(),{ready:true,idle:true,paused:false});
|
||||
const failed=fixture({helloError:true});await flush();
|
||||
assert.deepEqual(failed.state(),{ready:false,idle:false,paused:false});
|
||||
});
|
||||
test('抽号响应返回前终止,保留该次抽号并暂停,不执行登录',async()=>{
|
||||
const f=fixture({holdStart:true});await flush();await f.click('登陆下一帐号');
|
||||
assert.equal(f.state().idle,false);f.stop();f.finishStart();await flush();
|
||||
assert.deepEqual(f.routes,['/hello','/start','/suspend']);
|
||||
assert.deepEqual(f.state(),{ready:true,idle:false,paused:true});
|
||||
});
|
||||
test('恢复响应返回前终止,不执行登录或再次抽号',async()=>{
|
||||
const f=fixture({holdResume:true,flow:{id:'test',paused:true,phase:'logout'}});await flush();
|
||||
await f.click('继续未完成流程');f.stop();f.finishResume();await flush();
|
||||
assert.deepEqual(f.routes,['/hello','/resume','/suspend']);
|
||||
assert.equal(f.state().paused,true);
|
||||
});
|
||||
@@ -182,6 +182,11 @@ test('真实站点 aid 属性:上传成功但图片响应失败不得报告完
|
||||
await assert.rejects(h.api.importPrepared({automatic:true}),/网站附件图片无法显示/);
|
||||
assert.equal(h.ids.get('subject').value,'');assert.equal(h.api.getState().result,null);
|
||||
});
|
||||
test('自动流程关闭图片检查后,不等待或拒绝无法显示的图片', async () => {
|
||||
const h=harness({brokenImages:true});await h.api.stageFiles(articleFiles());
|
||||
const result=await h.api.importPrepared({automatic:true,verifyImages:false});
|
||||
assert.ok(result.attachmentOrder.length);assert.ok(h.ids.get('subject').value);
|
||||
});
|
||||
test('链接内图片、多行图文链接均参与上传并按原位置渲染', () => {
|
||||
const md='# 标题\n\n[\n\n### 宣传标题\n\n说明](https://example.com/promo)\n\n';
|
||||
const article=core.compile(md,'a.md',new Map([['image004.jpg',new Blob(['x'])],['image001.jpg',new Blob(['y'])]]));
|
||||
|
||||
@@ -49,10 +49,11 @@ function fixture({holdImport=false,holdHeartbeat=false,rejectSkip=false,result=n
|
||||
location:{href:'https://www.dww11.com/forum.php?mod=post&action=newthread&fid=125',assign:url=>navigations.push(url)},
|
||||
setInterval:fn=>{tick=fn;},setTimeout:(fn,ms)=>{const t=setTimeout(fn,ms);timers.add(t);return t;},clearTimeout:t=>{clearTimeout(t);timers.delete(t);}
|
||||
};
|
||||
context.window={DWW11Results:result?{inspect:()=>result,clean:v=>String(v||'').trim()}:null,DWW11Importer:{stagePackage:async()=>({}),importPrepared:async({signal})=>{
|
||||
context.window={DWW11Results:result?{inspect:()=>result,clean:v=>String(v||'').trim()}:null,DWW11Importer:{stagePackage:async()=>({}),importPrepared:async({signal,verifyImages})=>{
|
||||
assert.equal(verifyImages,false);
|
||||
if(holdImport)await new Promise((resolve,reject)=>{finishImport=()=>{rolledBack=true;reject(signal.reason);};signal.addEventListener('abort',()=>{}, {once:true});});
|
||||
signal.throwIfAborted();subject.value='标题';return {attachmentOrder:[]};
|
||||
},verifyEditorImages:async()=>{}},DWW11Formatter:{format(){}}};
|
||||
},verifyEditorImages:async()=>{throw new Error("自动流程不应检查图片");}},DWW11Formatter:{format(source,options){assert.equal(options.verifyImages,false);}}};
|
||||
vm.runInNewContext(fs.readFileSync(path.join(__dirname,'../browser/runner.js'),'utf8'),context);
|
||||
return {calls,navigations,job,run,storage,doc,Event,tick:()=>tick(),skip:()=>doc.dispatchEvent(new Event('dww11:skip-article')),
|
||||
finishImport:()=>finishImport(),finishHeartbeat:()=>finishHeartbeat(),rolledBack:()=>rolledBack,
|
||||
|
||||
@@ -70,6 +70,16 @@ class ControllerTests(unittest.TestCase):
|
||||
self.controller.scan(self.controller.manager.root)
|
||||
self.assertEqual(len(self.controller.store.state()['jobs']), 1)
|
||||
|
||||
def test_bridge_start_and_pause(self):
|
||||
store = self.controller.store
|
||||
with self.assertRaises(ValueError):
|
||||
self.controller.bridge('start', {})
|
||||
store.configure(dict(fid=95, minimum=1, maximum=1, interval=0, history_checked=True))
|
||||
for action, expected in [('start', True), ('pause', False), ('start', True)]:
|
||||
code, body = self.request('/bridge/' + action, {}, token=self.controller.bridge_token)
|
||||
self.assertEqual(code, 200)
|
||||
self.assertEqual(json.loads(body)['running'], expected)
|
||||
|
||||
def test_bridge_skip_continues_queue(self):
|
||||
store = self.controller.store
|
||||
store.enqueue(ROOT / 'test/fixtures/article')
|
||||
|
||||
+29
-1
@@ -22,7 +22,7 @@ class QueueTests(unittest.TestCase):
|
||||
self.md.write_text('# 测试新闻\n\n正文内容。\n\n来源:测试来源,如有侵权,请联系删除。\n')
|
||||
self.store = Store(self.root / 'queue.sqlite3')
|
||||
self.store.configure(dict(fid=125, minimum=1, maximum=1, interval=0, history_checked=True))
|
||||
self.owner = dict(owner='tab-a', account=dict(uid='123', name='测试账号'), extension={'idle': True}, url='https://www.dww11.com/')
|
||||
self.owner = dict(owner='tab-a', account=dict(uid='123', name='测试账号'), extension={'ready': True, 'idle': True}, url='https://www.dww11.com/')
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
@@ -321,6 +321,34 @@ class QueueTests(unittest.TestCase):
|
||||
self.assertFalse(self.store.run['running'])
|
||||
self.assertEqual(self.store.run['switch']['state'], 'clicked')
|
||||
|
||||
def pending_switch(self):
|
||||
import time
|
||||
job = self.begin(); self.submit(job); self.accept(job)
|
||||
other = self.root / 'other'; shutil.copytree(self.article, other)
|
||||
(other / 'a.md').write_text(self.md.read_text().replace('正文内容', '另一篇内容'))
|
||||
self.store.enqueue(other)
|
||||
self.assertEqual(self.store.next(self.owner)['action'], 'switch')
|
||||
self.store.run['switch']['created_at'] = time.time() - 3600
|
||||
|
||||
def test_old_pending_switch_does_not_timeout_before_click(self):
|
||||
self.pending_switch()
|
||||
self.store.control('pause'); self.store.control('start')
|
||||
self.assertEqual(self.store.next(self.owner)['action'], 'switch')
|
||||
self.assertTrue(self.store.run['running'])
|
||||
self.assertNotIn('clicked_at', self.store.run['switch'])
|
||||
|
||||
def test_missing_bridge_explains_problem_and_preserves_pending_switch(self):
|
||||
self.pending_switch()
|
||||
changed = {**self.owner, 'account': {'uid': '456', 'name': '手动换号后的账号'}, 'extension': {}}
|
||||
self.assertEqual(self.store.next(changed)['action'], 'idle')
|
||||
self.assertIn('联动接口未就绪', self.store.run['reason'])
|
||||
self.assertEqual(self.store.run['switch']['state'], 'pending')
|
||||
self.assertEqual(self.store.run['batch']['count'], 1)
|
||||
self.store.control('start')
|
||||
self.assertEqual(self.store.next({**changed, 'extension': {'ready': True, 'idle': True}})['action'], 'prepare')
|
||||
self.assertIsNone(self.store.run['switch'])
|
||||
self.assertEqual(self.store.run['batch']['count'], 0)
|
||||
|
||||
def test_rejected_submission_requires_manual_resolution(self):
|
||||
job = self.begin(); self.submit(job)
|
||||
self.transition(job, 'rejected', error='标题超限')
|
||||
|
||||
Reference in new issue
Block a user