This commit is contained in:
@@ -27,11 +27,13 @@ For development, set `PIGWAY_ADAPTER_PATH` to this repository's absolute `plugin
|
||||
|
||||
## Build
|
||||
|
||||
Current adapter version: **1.0.3**.
|
||||
Current adapter version: **1.0.4**.
|
||||
|
||||
```sh
|
||||
mkdir -p dist
|
||||
python3 package.py plugin dist/pigway-frame-flow-1.0.3.zip
|
||||
python3 package.py plugin dist/pigway-frame-flow-1.0.4.zip
|
||||
```
|
||||
|
||||
[Documentation](docs/README.md)
|
||||
|
||||
Use System configuration → Frame Flow → Frame Flow connection to edit the service URL, API token and optional management URL. The overview link defaults to the service URL. A blank token preserves the existing credential.
|
||||
|
||||
+5
-3
@@ -2,11 +2,11 @@
|
||||
|
||||
简体中文 | [English](README.md)
|
||||
|
||||
独立 ZIP 插件 `pigway-frame-flow` v1.0.3,采用现有 Console adapter protocol v1,仅依赖 Python 标准库。源码在独立仓库 `pigway-console-adapter-frame-flow` 维护,无需修改 Console 核心或部署额外服务。
|
||||
独立 ZIP 插件 `pigway-frame-flow` v1.0.4,采用现有 Console adapter protocol v1,仅依赖 Python 标准库。源码在独立仓库 `pigway-console-adapter-frame-flow` 维护,无需修改 Console 核心或部署额外服务。
|
||||
|
||||
## 安装
|
||||
|
||||
1. 在 Console「机器管理」上传 `dist/pigway-frame-flow-1.0.3.zip`,启用插件。
|
||||
1. 在 Console「机器管理」上传 `dist/pigway-frame-flow-1.0.4.zip`,启用插件。
|
||||
2. 添加设备,选择 PIGWay Frame Flow,填写 Console 能访问的 Frame Flow HTTP(S) 根地址(不含 `/api/v1`)和 **Frame Flow API 令牌**,不是 KoKonna Key 或网页登录密码。
|
||||
3. 若使用受限 API 客户端,授予 `devices:read`、`jobs:read`;默认 `FRAME_FLOW_TOKEN` 也可使用。令牌保存于 Console 敏感字段。跨不可信网络使用 HTTPS。
|
||||
4. 查看通用资源看板、告警与历史;默认每 60 秒采集一次。卡片和告警随 Console 中英文切换,资源显示 API 上报名称或 Console 中设置的显示名称。
|
||||
@@ -26,7 +26,7 @@ python3 -m venv .venv
|
||||
.venv/bin/python -m pip install pytest ruff
|
||||
.venv/bin/python -m pytest -q
|
||||
mkdir -p dist
|
||||
.venv/bin/python package.py plugin dist/pigway-frame-flow-1.0.3.zip
|
||||
.venv/bin/python package.py plugin dist/pigway-frame-flow-1.0.4.zip
|
||||
```
|
||||
|
||||
开发时可将 `PIGWAY_ADAPTER_PATH` 指向本目录 `plugin` 的绝对路径。ZIP 安装默认停用;生产安装与设备登记需在 Console 中执行。本地验证覆盖模拟接口、RPC 和隔离安装。
|
||||
@@ -36,3 +36,5 @@ Frame Flow API 契约测试为可选集成验证,需要另行安装 Frame Flow
|
||||
历史页的新增指标译名需 Console 配套支持(生产已安装)。仅安装插件也可正常采集并显示双语概览卡片。浏览器测试:安装 Playwright 后执行 `node tests/ui.cjs`。
|
||||
|
||||
[文档目录](docs/README.md)
|
||||
|
||||
系统配置 → 选择 Frame Flow → Frame Flow 接入,可修改服务地址、API 令牌和管理入口。首页标题旁的链接默认打开服务地址;管理入口可单独设置,令牌留空保留现有值。
|
||||
|
||||
+5
-3
@@ -1,5 +1,7 @@
|
||||
# pigway-console-adapter-frame-flow v1.0.3
|
||||
# pigway-console-adapter-frame-flow v1.0.4
|
||||
|
||||
统一开源许可证、贡献和安全反馈入口;完善当前版本的文档导航、源码分发和校验和。插件 ZIP 携带许可证。运行功能边界保持项目首页所述。
|
||||
Frame Flow 首页卡片新增管理入口链接,默认打开已登记的服务地址;可在「系统配置 → 选择 Frame Flow → Frame Flow 接入」单独设置管理入口。
|
||||
|
||||
本轮 6 项 Python 测试通过。源码语法、文档入口、打包和校验和检查通过;生产运行状态以实际部署验收为准。
|
||||
新增服务地址、API 令牌和管理入口配置页。令牌留空保留原值;管理入口留空恢复服务地址。链接只允许不带账号密码的 HTTP(S) 地址。采集能力与只读边界不变。
|
||||
|
||||
6 项 Python 测试通过;12 组中英文、深浅主题和桌面/平板/手机浏览器检查通过,并验证默认链接、自定义入口、恢复默认、保存失败和无效 URL 拦截。
|
||||
|
||||
+7
-7
@@ -2,16 +2,16 @@
|
||||
469e82c2812830629864510971466868e94f5e973c7abf9c3f674753a7fae187 .gitignore
|
||||
d2705ae14671873d176e9a306b82566bafe9b7c4504cb2396bc027867e60af71 CONTRIBUTING.md
|
||||
ea01675553f3737ca0f0cefb9459566a7cab8a44f545ec3bdda0a17144681dcd LICENSE
|
||||
281a088726d22cf31f0cfca312289cb1e6d2edd59ad17daa1930aa3e49337796 README.md
|
||||
03ecbd2b6710097aa01f54a82123c72c7aaebe478dbf49385184d9be55018280 README.zh-CN.md
|
||||
2c25fdab1918da78bb51eac81ef8359df0434b736afdde8f296519ee797b682a RELEASE_NOTES.md
|
||||
f581bd3a145e7be4434915233236bd61e557f1eb268a580d210b37f852d99634 README.md
|
||||
716162ae46314bc3ba502214fcf9eeb5d621578ea6305c840d7620d86ba9bb9d README.zh-CN.md
|
||||
2df5c048c1a44f729c1a8be804a62162af1d8ff349d5aeb3c9bd03339d7c639d RELEASE_NOTES.md
|
||||
6cead6c1378cd840f655adc3d467e2cd6fd3ab368d7200a3afbfe85d9ed159be SECURITY.md
|
||||
43d322e37914b73c4d0ca9974137b540bf7ad92afa5d168e22fc278c7eaedf90 docs/README.md
|
||||
416ab1f006fae6d448603e7812f26b196296b8999c8555245bce8b7e36b87fe0 package.py
|
||||
ea01675553f3737ca0f0cefb9459566a7cab8a44f545ec3bdda0a17144681dcd plugin/LICENSE
|
||||
a590a3b3bde8712ee2abe0c49e30f736a4dd40e6487fb708b9b24a0b44371d9d plugin/manifest.json
|
||||
a4f8b2f66e2ae19386ac62edfd2285890a4f48336e7a791098584f53a7b05a64 plugin/ui/index.js
|
||||
d32ba37c71a2417ea99f53a461c6c319c1529d71323c58d0b6d3ba3114585484 plugin/manifest.json
|
||||
eebc45e8d2306d35e8175aad426436414944e5d8b55184928f812c7fd9804d5b plugin/ui/index.js
|
||||
9cb8ae3c212c73b3205a6ba352c15685b83a4f4add88c9b2b493147314dad93c plugin/worker.py
|
||||
721d1d363229b7fee62065ccff86677e5e12c778ef0adfc2de152bc7b35858e2 pyproject.toml
|
||||
6a8cec7b18ca801f5721c4a331e2886858064ba15130b1aa5a1072df7be89b79 pyproject.toml
|
||||
a427151c6e34c78fb0239bdb2f26d5bad685dae37e012477c5b61ec7a8cbb523 tests/test_adapter.py
|
||||
69d4bdd8b4395cfceff573d981e475cd74eadb946c71532e0d9fe084ae8682e3 tests/ui.cjs
|
||||
96a3d9616999aecbf6ae4d3409f02d0d61cc9b292db3e4fafc0da6a8586b251f tests/ui.cjs
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "pigway-frame-flow",
|
||||
"name": "PIGWay Frame Flow",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.4",
|
||||
"protocol_version": 1,
|
||||
"runtime": "python3",
|
||||
"entrypoint": "worker.py",
|
||||
@@ -29,6 +29,14 @@
|
||||
"zh": "Frame Flow API 令牌",
|
||||
"en": "Frame Flow API token"
|
||||
}
|
||||
},
|
||||
"management_url": {
|
||||
"type": "string",
|
||||
"format": "http-origin",
|
||||
"label": {
|
||||
"zh": "管理入口地址(留空使用服务地址)",
|
||||
"en": "Management URL (blank uses service URL)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ui": {
|
||||
|
||||
+55
-2
@@ -1,6 +1,17 @@
|
||||
import { scopeAlertHealth, summaryStatus } from '/assets/js/overview-health.js';
|
||||
import { state } from '/assets/js/state.js';
|
||||
import { esc, pluginText as text } from '/assets/js/shared.js';
|
||||
import { $, esc, pluginText as text } from '/assets/js/shared.js';
|
||||
|
||||
import { get } from '/assets/js/navigation.js';
|
||||
|
||||
function managementLink(host) {
|
||||
try {
|
||||
const url = new URL(host.connection?.management_url || host.connection?.url || host.url);
|
||||
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) return '';
|
||||
const label = text('打开 Frame Flow 管理', 'Open Frame Flow management');
|
||||
return `<a class="docker-icon" style="margin-left: -7px" href="${esc(url.href)}" target="_blank" rel="noopener noreferrer" title="${label}" aria-label="${label}"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 4h6v6M20 4l-10 10M11 4H4v16h16v-7"/></svg></a>`;
|
||||
} catch { return ''; }
|
||||
}
|
||||
|
||||
const metricValue = metric => {
|
||||
if (metric.unit === 'percent') return `${metric.value}%`;
|
||||
@@ -46,5 +57,47 @@ export function hostCard(entry) {
|
||||
});
|
||||
}).join('');
|
||||
const frames = resources.filter(resource => resource.type === 'device');
|
||||
return `<article class="panel host-panel"><div class="host-head"><div><div class="host-title"><h2>${esc(entry.host.name || 'Frame Flow')}</h2></div><div class="device-details">${frames.map(frame => `<span>${esc(frame.name || text('画框', 'Frame'))}</span>`).join('')}</div></div><span class="badge semantic-status" data-semantic="${current ? 'online' : 'unknown'}">${current ? text('在线', 'Online') : text('状态未确认', 'Unconfirmed')}</span></div>${!current ? `<p class="muted">${text('以下为上次采集数据', 'Showing the last collected data')}</p>` : ''}<div class="host-grid">${cards}</div>${(snapshot.alerts || []).length || !current || snapshot.alerts_complete !== true ? `<details class="group" data-overview-details="alerts"><summary class="smart-summary"><strong class="smart-summary-title">${text('已读取范围内的告警', 'Alerts within monitored scope')}</strong>${summaryStatus(scopeAlertHealth(snapshot.alerts, snapshot.alerts_complete, current), state.LANG)}</summary><div style="margin-top:12px">${alertRows || (current && snapshot.alerts_complete ? text('无告警', 'No alerts') : text('状态未确认', 'Unconfirmed'))}</div></details>` : ""}</article>`;
|
||||
return `<article class="panel host-panel"><div class="host-head"><div><div class="host-title"><h2>${esc(entry.host.name || 'Frame Flow')}</h2>${managementLink(entry.host)}</div><div class="device-details">${frames.map(frame => `<span>${esc(frame.name || text('画框', 'Frame'))}</span>`).join('')}</div></div><span class="badge semantic-status" data-semantic="${current ? 'online' : 'unknown'}">${current ? text('在线', 'Online') : text('状态未确认', 'Unconfirmed')}</span></div>${!current ? `<p class="muted">${text('以下为上次采集数据', 'Showing the last collected data')}</p>` : ''}<div class="host-grid">${cards}</div>${(snapshot.alerts || []).length || !current || snapshot.alerts_complete !== true ? `<details class="group" data-overview-details="alerts"><summary class="smart-summary"><strong class="smart-summary-title">${text('已读取范围内的告警', 'Alerts within monitored scope')}</strong>${summaryStatus(scopeAlertHealth(snapshot.alerts, snapshot.alerts_complete, current), state.LANG)}</summary><div style="margin-top:12px">${alertRows || (current && snapshot.alerts_complete ? text('无告警', 'No alerts') : text('状态未确认', 'Unconfirmed'))}</div></details>` : ""}</article>`;
|
||||
}
|
||||
|
||||
|
||||
export function renderConfig() {
|
||||
const host = state.fleetHosts.find(h => h.id === state.currentConfigHost);
|
||||
const connection = host.connection || {};
|
||||
$('mainSavebar').classList.add('hidden');
|
||||
$('configMenu').innerHTML = `<button data-config-section="frame-flow" class="active">${text('Frame Flow 接入', 'Frame Flow connection')}</button>`;
|
||||
$('configRows').innerHTML = `<section class="config-page active" data-config-page="frame-flow"><h2>${text('Frame Flow 接入', 'Frame Flow connection')}</h2><p class="hint">${text('配置保存在 Console;管理入口留空时,首页链接使用 Frame Flow 服务地址。API 令牌留空保留现有值。', 'Settings are stored in Console. Leave the management URL blank to use the Frame Flow service URL on the overview. A blank API token keeps the existing value.')}</p><div class="fields">${[
|
||||
['url', text('Frame Flow 地址', 'Frame Flow URL'), connection.url || host.url || ''],
|
||||
['token', text('API 令牌(留空保留)', 'API token (blank keeps existing)'), ''],
|
||||
['management_url', text('管理入口地址(留空使用服务地址)', 'Management URL (blank uses service URL)'), connection.management_url || ''],
|
||||
].map(([key, label, value]) => `<div class="field"><label>${label}<input data-frame-flow-connection="${key}" type="${key === 'token' ? 'password' : 'url'}" value="${esc(value)}" autocomplete="${key === 'token' ? 'new-password' : 'off'}"></label></div>`).join('')}</div><div class="action-row"><button id="saveFrameFlowConnection">${text('保存接入配置', 'Save connection')}</button><span id="frameFlowConnectionMessage" role="status"></span></div></section>`;
|
||||
const page = $('configRows').querySelector('[data-config-page="frame-flow"]');
|
||||
$('configMenu').onclick = event => {
|
||||
if (!event.target.closest('[data-config-section="frame-flow"]')) return;
|
||||
$('configMenu').querySelectorAll('button').forEach(b => b.classList.toggle('active', b.dataset.configSection === 'frame-flow'));
|
||||
$('configRows').querySelectorAll('[data-config-page]').forEach(p => p.classList.toggle('active', p === page));
|
||||
$('mainSavebar').classList.add('hidden');
|
||||
};
|
||||
$('saveFrameFlowConnection').onclick = async () => {
|
||||
const button = $('saveFrameFlowConnection'), message = $('frameFlowConnectionMessage');
|
||||
const values = Object.fromEntries([...page.querySelectorAll('[data-frame-flow-connection]')].map(input => [input.dataset.frameFlowConnection, input.value.trim()]));
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
if (key === 'token' || (key === 'management_url' && !value)) continue;
|
||||
try {
|
||||
const url = new URL(value);
|
||||
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.pathname !== '/' || url.search || url.hash) throw Error();
|
||||
} catch {
|
||||
message.textContent = text('请使用不含路径或账号密码的 HTTP(S) 根地址', 'Use an HTTP(S) origin without paths or credentials');
|
||||
message.className = 'error'; return;
|
||||
}
|
||||
}
|
||||
button.disabled = true;
|
||||
try {
|
||||
const result = await get('/api/v1/hosts', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ operation: 'save', id: host.id, adapter_id: host.adapter_id, connection: values }) });
|
||||
state.fleetHosts = result.hosts;
|
||||
page.querySelector('[data-frame-flow-connection="token"]').value = '';
|
||||
message.textContent = text('已保存', 'Saved'); message.className = 'success';
|
||||
} catch (error) { message.textContent = error.message; message.className = 'error'; }
|
||||
finally { button.disabled = false; }
|
||||
};
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "pigway-console-adapter-frame-flow"
|
||||
version = "1.0.3"
|
||||
version = "1.0.4"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
readme = "README.md"
|
||||
|
||||
+71
-33
@@ -1,35 +1,73 @@
|
||||
const {chromium}=require('playwright');
|
||||
const {chromium}=require(process.env.PLAYWRIGHT_MODULE||'playwright');
|
||||
const fs=require('node:fs'),path=require('node:path'),assert=require('node:assert/strict');
|
||||
const root=path.resolve(__dirname,'..'),consoleRoot=path.resolve(root,'../pigway-device-console');
|
||||
const manifest=JSON.parse(fs.readFileSync(path.join(root,'plugin/manifest.json')));
|
||||
(async()=>{
|
||||
const browser=await chromium.launch({headless:true,channel:'chrome'});
|
||||
try {
|
||||
const source=fs.readFileSync(path.join(__dirname,'../plugin/ui/index.js'),'utf8').replace(/^import .*;\n/gm,'').replace('export function hostCard','function hostCard');
|
||||
const labels={service_ready:{zh:'服务就绪',en:'Service ready'},device_online:{zh:'画框在线',en:'Frame online'},battery_percent:{zh:'电量',en:'Battery'}};
|
||||
const entry={host:{name:'Frame Flow'},dataCurrent:true,snapshot:{resources:[{id:'service',name:'Frame Flow',type:'service'},{id:'device:living-room',name:'渡口浪人的相框',type:'device'}],metrics:[{resource_id:'service',key:'service_ready',value:1,unit:'boolean',label:labels.service_ready},{resource_id:'device:living-room',key:'device_online',value:1,unit:'boolean',label:labels.device_online},{resource_id:'device:living-room',key:'battery_percent',value:28,unit:'percent',label:labels.battery_percent}],alerts:[]}};
|
||||
entry.snapshot.resources.push({id:'job:slideshow',name:'家庭照片轮播',type:'job'});
|
||||
entry.snapshot.metrics.push({resource_id:'job:slideshow',key:'job_enabled',value:1,unit:'boolean'},{resource_id:'job:slideshow',key:'last_run_success',value:1,unit:'boolean'});
|
||||
for(const lang of ['zh','en'])for(const width of [1440,820,390]) {
|
||||
const page=await browser.newPage({bypassCSP:true,viewport:{width,height:1000}});
|
||||
await page.goto('http://192.168.89.19/');
|
||||
const result=await page.evaluate(async ({source,entry,lang})=>{
|
||||
const {scopeAlertHealth,summaryStatus}=await import('/assets/js/overview-health.js');
|
||||
const state={LANG:lang};const text=(zh,en)=>lang==='zh'?zh:en;
|
||||
const esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
const html=eval(source+'\nhostCard(entry)');
|
||||
document.body.innerHTML=`<main style="max-width:100%;padding:16px">${html}</main>`;
|
||||
return {text:document.body.innerText,overflow:document.documentElement.scrollWidth>innerWidth,subtitle:document.querySelector(".host-head .device-details")?.textContent,nested:document.querySelectorAll(".group .metric, .metric .metric").length,status:document.querySelector(".metric [data-semantic]")?.textContent};
|
||||
},{source,entry,lang});
|
||||
assert(result.text.includes('渡口浪人的相框'));
|
||||
assert(!result.text.includes('living-room'));
|
||||
assert(result.text.includes(lang==='zh'?'服务就绪':'Service ready'));
|
||||
assert.equal(result.overflow,false);
|
||||
assert.equal(result.nested,0);
|
||||
assert.equal(result.subtitle,'渡口浪人的相框');
|
||||
assert.equal(result.status,lang==='zh'?'正常':'Normal');
|
||||
assert(result.text.includes(lang==='zh'?'轮播任务':'Slideshow task'));
|
||||
assert(result.text.includes('家庭照片轮播 · '+(lang==='zh'?'启用':'Enabled')));
|
||||
await page.close();
|
||||
}
|
||||
console.log('6 browser checks passed');
|
||||
} finally {await browser.close()}
|
||||
})().catch(e=>{console.error(e);process.exit(1)});
|
||||
const browser=await chromium.launch({headless:true});
|
||||
try{
|
||||
for(const lang of ['zh','en'])for(const theme of ['light','dark'])for(const width of [1440,820,390]){
|
||||
let host={id:'frame-service',name:'Frame Flow',adapter_id:manifest.id,category:'host',adapter_enabled:true,capabilities:['status'],url:'https://frames.example.test',connection:{url:'https://frames.example.test'}};
|
||||
let writes=[],failSave=false;
|
||||
const page=await browser.newPage({viewport:{width,height:1000}}),errors=[];
|
||||
page.on('pageerror',error=>errors.push(error.message));
|
||||
await page.addInitScript(({lang,theme})=>{localStorage.setItem('pigwayLanguage',lang);localStorage.setItem('pigwayTheme',theme)},{lang,theme});
|
||||
await page.route('http://console.test/**',route=>{
|
||||
const p=new URL(route.request().url()).pathname;
|
||||
if(p.startsWith('/api/')){
|
||||
if(route.request().method()==='PUT'){
|
||||
assert.equal(p,'/api/v1/hosts');const body=route.request().postDataJSON();writes.push(body);
|
||||
assert.equal(body.operation,'save');assert.equal(body.adapter_id,manifest.id);assert.equal(body.connection.token,'');
|
||||
if(failSave)return route.fulfill({status:500,json:{error:'Save failed'}});
|
||||
host={...host,url:body.connection.url,connection:{...body.connection}};
|
||||
return route.fulfill({json:{hosts:[host]}});
|
||||
}
|
||||
const values={
|
||||
'/api/v1/auth/status':{authenticated:true,setup_required:false},
|
||||
'/api/v1/hosts':{hosts:[host]},
|
||||
'/api/v1/adapters':{adapters:[{...manifest,enabled:true,state:'ready'}]},
|
||||
'/api/v1/fleet/status':{hosts:[{host,online:true,snapshot:{identity:{},resources:[{id:'frame',name:'Living room frame',type:'device'},{id:'job:slideshow',name:'Slideshow',type:'job'}],metrics:[{resource_id:'job:slideshow',key:'job_enabled',value:1},{resource_id:'job:slideshow',key:'last_run_success',value:1}],alerts:[],alerts_complete:true}}]},
|
||||
};
|
||||
return route.fulfill({json:values[p]||{}});
|
||||
}
|
||||
const file=p.startsWith('/adapter-assets/')?path.join(root,'plugin/ui/index.js'):path.join(consoleRoot,'web',p==='/'?'index.html':p.replace(/^\/assets\//,''));
|
||||
if(!fs.existsSync(file))return route.fulfill({status:404});
|
||||
return route.fulfill({contentType:file.endsWith('.js')?'text/javascript':file.endsWith('.css')?'text/css':'text/html',body:fs.readFileSync(file)});
|
||||
});
|
||||
const link=()=>page.locator('#hostCards .host-title a');
|
||||
const config=async()=>{
|
||||
await page.locator('[data-tab="config"]').click();
|
||||
await page.locator('[data-config-section="frame-flow"]').click();
|
||||
await page.locator('#saveFrameFlowConnection').waitFor();
|
||||
};
|
||||
await page.goto('http://console.test');await link().waitFor();
|
||||
assert.equal(await link().getAttribute('href'),'https://frames.example.test/');
|
||||
assert.equal(await link().getAttribute('target'),'_blank');
|
||||
assert.equal(await link().getAttribute('rel'),'noopener noreferrer');
|
||||
assert.equal(await link().getAttribute('aria-label'),lang==='zh'?'打开 Frame Flow 管理':'Open Frame Flow management');
|
||||
assert.equal(await page.locator('#hostCards .device-details').textContent(),'Living room frame');
|
||||
await config();
|
||||
assert.equal(await page.locator('[data-frame-flow-connection="url"]').inputValue(),'https://frames.example.test');
|
||||
assert.equal(await page.locator('[data-frame-flow-connection="token"]').inputValue(),'');
|
||||
assert(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth+1));
|
||||
if(lang==='zh'&&theme==='light')await page.locator('#config').screenshot({path:'/tmp/frame-flow-config-'+width+'.png'});
|
||||
if(lang==='zh'&&theme==='light'&&width===1440){
|
||||
const management=page.locator('[data-frame-flow-connection="management_url"]');
|
||||
await management.fill('javascript:alert(1)');await page.locator('#saveFrameFlowConnection').click();
|
||||
await page.locator('#frameFlowConnectionMessage.error').waitFor();assert.equal(writes.length,0);
|
||||
await management.fill('https://manage.example.test');await page.locator('#saveFrameFlowConnection').click();
|
||||
await page.locator('#frameFlowConnectionMessage.success').waitFor();assert.equal(writes.length,1);
|
||||
await page.reload();await link().waitFor();assert.equal(await link().getAttribute('href'),'https://manage.example.test/');
|
||||
await config();failSave=true;
|
||||
await page.locator('[data-frame-flow-connection="management_url"]').fill('https://changed.example.test');await page.locator('#saveFrameFlowConnection').click();
|
||||
await page.getByText('Save failed',{exact:true}).waitFor();
|
||||
assert.equal(await page.locator('[data-frame-flow-connection="management_url"]').inputValue(),'https://changed.example.test');
|
||||
failSave=false;await page.locator('[data-frame-flow-connection="management_url"]').fill('');await page.locator('#saveFrameFlowConnection').click();
|
||||
await page.locator('#frameFlowConnectionMessage.success').waitFor();await page.reload();await link().waitFor();
|
||||
assert.equal(await link().getAttribute('href'),'https://frames.example.test/');
|
||||
host.connection.management_url='javascript:alert(1)';await page.reload();await page.locator('#hostCards h2').waitFor();assert.equal(await link().count(),0);
|
||||
}
|
||||
assert.deepEqual(errors,[]);await page.close();
|
||||
}
|
||||
console.log('PASS Frame Flow management link/configuration: default/custom/reset URL, save failure, unsafe URLs, API-only edits; 12 language/theme/viewport combinations');
|
||||
}finally{await browser.close();}
|
||||
})().catch(error=>{console.error(error);process.exit(1)});
|
||||
|
||||
Reference in New Issue
Block a user