fix(android): usesCleartextTraffic=true — release APK blocked HTTP APIs (Android 9+ default)
release 构建默认禁明文 HTTP(debug 由 Flutter 自动放行)——服务暂时不可用 即此因。当前测试服务器 API/观众页均为 http://,需显式允许;上 https 后移除。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
<application
|
||||
android:usesCleartextTraffic="true"
|
||||
android:label="photo_live"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
|
||||
@@ -313,20 +313,26 @@ test.describe.serial('E04 interactions: pause, click lock, keyboard, destroy', (
|
||||
await openHost(page, { width: 1440, height: 900 });
|
||||
const frame = page.locator('iframe[src*="/embed/"][src*="v=2"]');
|
||||
await frame.waitFor();
|
||||
const inside = page.frameLocator('iframe[src*="/embed/"][src*="v=2"]');
|
||||
// 先悬停冻结(避免点击瞬间目标位移——点击锁定的正是防错图语义)。
|
||||
|
||||
// 悬停冻结 + 坐标直击当前命中的照片(.first() 可能已滚出视口——flaky 源)。
|
||||
const box = await frame.boundingBox();
|
||||
await page.mouse.move((box!.x ?? 0) + 90, (box!.y ?? 0) + 140);
|
||||
const cx = (box!.x ?? 0) + 90;
|
||||
const cy = (box!.y ?? 0) + 140;
|
||||
await page.mouse.move(cx, cy);
|
||||
await page.waitForTimeout(400);
|
||||
const target = inside.locator('a.stream-photo').first();
|
||||
const photoId = await target.getAttribute('data-photo-id');
|
||||
expect(photoId).toBeTruthy();
|
||||
const embedFrame0 = page.frames().find((f) => f.url().includes('v=2'));
|
||||
expect(embedFrame0).toBeDefined();
|
||||
const photoId = await embedFrame0!.evaluate(([_x, _y]) => {
|
||||
const el = document.elementFromPoint(_x, _y)?.closest('a.stream-photo');
|
||||
return el?.getAttribute('data-photo-id') ?? '';
|
||||
}, [90, 140] as const);
|
||||
expect(photoId).not.toBe('');
|
||||
// 宿主内弹框(用户决策 2026-09-23):不新开标签,宿主页覆盖层 + 详情 iframe。
|
||||
let popupOpened = false;
|
||||
page.once('popup', () => {
|
||||
popupOpened = true;
|
||||
});
|
||||
await target.click();
|
||||
await page.mouse.click(cx, cy);
|
||||
const modalFrame = page.locator('iframe[title="PhotoLive photo detail"]');
|
||||
await modalFrame.waitFor({ timeout: 5000 });
|
||||
expect(popupOpened).toBe(false);
|
||||
@@ -432,8 +438,12 @@ test.describe.serial('E05 realtime hearts: real burst drives particles', () => {
|
||||
await page.waitForTimeout(500);
|
||||
const embedFrame = page.frames().find((f) => f.url().includes('v=2'));
|
||||
expect(embedFrame).toBeDefined();
|
||||
const photoId = await embedFrame!.locator('a.stream-photo').first().getAttribute('data-photo-id');
|
||||
expect(photoId).toBeTruthy();
|
||||
// 坐标命中当前可见照片(.first() 可能已滚出视口——事件心只对可见照片出泡)。
|
||||
const photoId = await embedFrame!.evaluate(() => {
|
||||
const el = document.elementFromPoint(90, 140)?.closest('a.stream-photo');
|
||||
return el?.getAttribute('data-photo-id') ?? '';
|
||||
});
|
||||
expect(photoId).not.toBe('');
|
||||
// 真实公开路径点赞(ov 域):服务端广播 likes.burst → 订阅出泡。
|
||||
const like = await page.request.post(
|
||||
`http://127.0.0.1:8081/v1/public/albums/${albumToken}/photos/${photoId}/likes`,
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import { createServer } from 'node:http';
|
||||
import { chromium } from '@playwright/test';
|
||||
const server = createServer((_q, res) => {
|
||||
res.setHeader('content-type', 'text/html; charset=utf-8');
|
||||
res.end(`<!doctype html><html><body style="min-height:900px">
|
||||
<script src="http://219.157.255.213:32006/embed/v2/loader.js" data-slot="se1bt8mqvmeese4jx9gixnj4" defer></script></body></html>`);
|
||||
});
|
||||
await new Promise((r) => server.listen(3980, '127.0.0.1', r));
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
|
||||
await page.goto('http://127.0.0.1:3980');
|
||||
const frame = page.locator('iframe[src*="/embed/"][src*="v=2"]');
|
||||
await frame.waitFor({ timeout: 20000 });
|
||||
await page.waitForTimeout(3000);
|
||||
const f = page.frames().find((fr) => fr.url().includes('v=2'));
|
||||
const offset = async () => {
|
||||
const t = await f.locator('.stream-track').evaluate((el) => el.style.transform);
|
||||
return Number.parseFloat((/translateY\((-?[\d.]+)px\)/.exec(t) ?? ['0', '0'])[1]);
|
||||
};
|
||||
const box = await frame.boundingBox();
|
||||
const cx = (box ? box.x : 0) + 90, cy = (box ? box.y : 0) + 150;
|
||||
const scrolling = async (ms = 1000) => { const a = await offset(); await page.waitForTimeout(ms); return a !== (await offset()); };
|
||||
|
||||
console.log('1. baseline =', await scrolling());
|
||||
|
||||
// 窗内拖动后,鼠标移开再验证(窗内 up 后鼠标停在窗内属正常 hover 暂停)
|
||||
await page.mouse.move(cx, cy); await page.mouse.down();
|
||||
await page.mouse.move(cx, cy + 40, { steps: 4 }); await page.mouse.up();
|
||||
await page.mouse.move(30, 30); await page.waitForTimeout(1500);
|
||||
console.log('2. after in-frame drag (pointer away) =', await scrolling());
|
||||
|
||||
// 窗外释放
|
||||
await page.mouse.move(cx, cy); await page.mouse.down();
|
||||
await page.mouse.move(cx + 120, cy + 200, { steps: 4 }); await page.mouse.up();
|
||||
await page.waitForTimeout(3500);
|
||||
console.log('3. after outside-release =', await scrolling());
|
||||
|
||||
// 长跑 20s
|
||||
const s1 = await offset(); await page.waitForTimeout(20000); const s2 = await offset();
|
||||
console.log('4. 20s moved =', Math.abs(s2 - s1).toFixed(0), 'px');
|
||||
|
||||
// hover 语义仍正常
|
||||
await page.mouse.move(cx, cy); await page.waitForTimeout(800);
|
||||
const h1 = await offset(); await page.waitForTimeout(800); const h2 = await offset();
|
||||
await page.mouse.move(30, 30); await page.waitForTimeout(1500);
|
||||
const h3 = await offset(); await page.waitForTimeout(800); const h4 = await offset();
|
||||
console.log('5. hover pause =', h1 === h2, '; leave resume =', h3 !== h4);
|
||||
server.close(); await browser.close();
|
||||
@@ -217,40 +217,30 @@ function scheduleResume(): void {
|
||||
|
||||
function onPointerDown(event: PointerEvent): void {
|
||||
if (event.pointerType === 'mouse' && event.button !== 0) return;
|
||||
lastPointerTs = Date.now();
|
||||
pointerDownY = event.clientY;
|
||||
pointerMoved = 0;
|
||||
const anchor = (event.target as HTMLElement).closest<HTMLElement>('a.stream-photo');
|
||||
lockedTarget = anchor === null ? null : (anchor.dataset.photoId ?? null);
|
||||
setReason('pointer', true);
|
||||
// 指针捕获 + 看门狗(滚动卡死修复 2026-09-23):按下后拖出窗外松开时,
|
||||
// pointerup 发生在宿主文档——iframe 内任何监听都收不到(capture 不跨文档),
|
||||
// 'pointer' 暂停原因会永久残留 → 滚动停死。看门狗 2s 强制恢复是确定性兜底;
|
||||
// capture/window 监听让 iframe 内的释放更快恢复。
|
||||
try {
|
||||
viewportEl.value?.setPointerCapture(event.pointerId);
|
||||
} catch {
|
||||
/* 指针已释放等:忽略 */
|
||||
}
|
||||
// 看门狗(滚动卡死修复 2026-09-23):按下后拖出窗外松开时,pointerup 发生
|
||||
// 在宿主文档——iframe 内任何监听都收不到,'pointer'/'manual'/'hover' 暂停
|
||||
// 原因会永久残留 → 滚动停死。2s 强制恢复是确定性兜底。注意不能用
|
||||
// setPointerCapture:捕获会把 pointerup/click 重定向到视口元素——照片 <a>
|
||||
// 的 click 处理收不到点击(曾致详情弹框打不开),已移除。
|
||||
if (pointerWatchdog !== null) clearTimeout(pointerWatchdog);
|
||||
pointerWatchdog = setTimeout(() => {
|
||||
pointerWatchdog = null;
|
||||
pointerDownY = null;
|
||||
// 统一清指针族原因(滚动卡死第三层修复 2026-09-23):pointer capture 挂起
|
||||
// 期间 pointerleave 不派发,而窗外释放使 up 永不送达 iframe → capture 不
|
||||
// 释放 → hover 原因同样永久卡住。看门狗兜底:释放捕获 + 清
|
||||
// pointer/manual/hover 三因(窗内正常路径由各事件自行清理,不受影响)。
|
||||
try {
|
||||
const el = viewportEl.value;
|
||||
if (el !== null && el.hasPointerCapture(event.pointerId)) {
|
||||
el.releasePointerCapture(event.pointerId);
|
||||
}
|
||||
} catch {
|
||||
/* 指针已不在:忽略 */
|
||||
}
|
||||
// 统一清指针族原因(滚动卡死第三层修复 2026-09-23):拖出窗外后 leave
|
||||
// 可能未派发(焦点/边界事件在 iframe 间不可靠),hover 原因同样可能卡住。
|
||||
// 看门狗兜底清 pointer/manual/hover/focus 四因(窗内正常路径由各事件
|
||||
// 自行清理,不受影响)。
|
||||
hoverPointerIsMouse = false;
|
||||
setReason('pointer', false);
|
||||
setReason('manual', false);
|
||||
setReason('hover', false);
|
||||
setReason('focus', false);
|
||||
scheduleResume();
|
||||
}, POINTER_WATCHDOG_MS);
|
||||
}
|
||||
@@ -363,7 +353,14 @@ function onPointerLeave(event: PointerEvent): void {
|
||||
scheduleResume();
|
||||
}, RESUME_DELAY_MS);
|
||||
}
|
||||
/** 最近一次指针交互时刻(focusin 归因用)。 */
|
||||
let lastPointerTs = 0;
|
||||
|
||||
function onFocusIn(): void {
|
||||
// 鼠标点击/拖动会把焦点带到窗内元素(focusin 副作用)——此时暂停会永久
|
||||
// 卡住(鼠标移开不清键盘焦点,滚动停死根因 2026-09-23)。仅键盘导航
|
||||
// (Tab/方向键进入,非指针后短窗)才触发 focus 暂停(§4.2 本意)。
|
||||
if (Date.now() - lastPointerTs < 500) return;
|
||||
setReason('focus', true);
|
||||
}
|
||||
function onFocusOut(): void {
|
||||
@@ -387,6 +384,7 @@ function onWindowBlur(): void {
|
||||
pointerDownY = null;
|
||||
setReason('pointer', false);
|
||||
setReason('manual', false);
|
||||
setReason('focus', false);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 109 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
Reference in New Issue
Block a user