修复固定试玩误判祖先指针穿透样式(#547) #583
@@ -422,7 +422,8 @@ pub(super) const PROBE_PLAYTEST_CONTROL_SCRIPT: &str = r#"function() {
|
||||
}
|
||||
|
||||
let stylesVisible = true;
|
||||
let pointerBlocked = false;
|
||||
// 后代可用 auto 覆盖祖先的 none;未覆盖时计算样式仍会继承 none。
|
||||
const pointerBlocked = getComputedStyle(this).pointerEvents === 'none';
|
||||
for (let current = this; current instanceof HTMLElement; current = current.parentElement) {
|
||||
const style = getComputedStyle(current);
|
||||
const opacity = Number.parseFloat(style.opacity);
|
||||
@@ -431,7 +432,6 @@ pub(super) const PROBE_PLAYTEST_CONTROL_SCRIPT: &str = r#"function() {
|
||||
&& style.visibility !== 'hidden'
|
||||
&& style.visibility !== 'collapse'
|
||||
&& (Number.isNaN(opacity) || opacity > 0);
|
||||
pointerBlocked ||= style.pointerEvents === 'none';
|
||||
}
|
||||
|
||||
const rect = this.getBoundingClientRect();
|
||||
|
||||
@@ -270,16 +270,12 @@ fn playtest_scenario_fingerprints_are_fixed_lowercase_sha256_values() {
|
||||
let tetris = browser_playtest_scenario_fingerprint(BrowserPlaytestScenario::TetrisV1);
|
||||
let lane = browser_playtest_scenario_fingerprint(BrowserPlaytestScenario::LaneDefenseV1);
|
||||
assert_eq!(
|
||||
generic,
|
||||
"e5e1b4d01e39cb7b0dd2fd5a56982c3ddf825a926738d99152875ee1ff412a8d"
|
||||
);
|
||||
assert_eq!(
|
||||
tetris,
|
||||
"b3c49f43f70291ea5c9431a21bb751fead2e244c8e97fecec20ee3e38215a59c"
|
||||
);
|
||||
assert_eq!(
|
||||
lane,
|
||||
"6a24072ce7a570dd29edac0ca3fa905546140e44ca4ab6412d8fc7fe1239aa5a"
|
||||
[generic.as_str(), tetris.as_str(), lane.as_str()],
|
||||
[
|
||||
"85e7ec677411745ad3a7385db67a2c89441cb2ccc12a7a1853bb87ba74f15e0d",
|
||||
"60c202a9fe6313dd414365db52df8b5b63110971f89708ac1563d531f510f4ff",
|
||||
"a3120c8cf585f2e06091229c3b7c3a5bd8aa08821704064edc611297ee54f466",
|
||||
]
|
||||
);
|
||||
assert!(generic.bytes().all(|byte| byte.is_ascii_hexdigit()));
|
||||
assert!(tetris.bytes().all(|byte| byte.is_ascii_hexdigit()));
|
||||
@@ -1354,7 +1350,7 @@ async fn real_chrome_generic_playtest_rejects_one_frame_playing_state() {
|
||||
#[tokio::test]
|
||||
#[ignore = "requires an installed Chrome/Chromium/Edge and explicit local browser execution"]
|
||||
async fn real_chrome_generic_playtest_accepts_earlier_window_capture_stop_immediate_flow() {
|
||||
let result = run_stable_generic_viewport_fixture(false).await;
|
||||
let result = run_stable_generic_viewport_fixture(false, None).await;
|
||||
assert!(
|
||||
result.passed,
|
||||
"diagnostics={:#?}\nviewports={:#?}",
|
||||
@@ -1388,7 +1384,7 @@ async fn real_chrome_generic_playtest_accepts_earlier_window_capture_stop_immedi
|
||||
#[tokio::test]
|
||||
#[ignore = "requires an installed Chrome/Chromium/Edge and explicit local browser execution"]
|
||||
async fn real_chrome_generic_playtest_rejects_mobile_only_unwired_control() {
|
||||
let result = run_stable_generic_viewport_fixture(true).await;
|
||||
let result = run_stable_generic_viewport_fixture(true, None).await;
|
||||
assert!(
|
||||
!result.passed,
|
||||
"a successful desktop cannot hide mobile gameplay failure"
|
||||
@@ -1410,8 +1406,117 @@ async fn real_chrome_generic_playtest_rejects_mobile_only_unwired_control() {
|
||||
.any(|message| message.starts_with("playtest mobile:")));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires an installed Chrome/Chromium/Edge and explicit local browser execution"]
|
||||
async fn real_chrome_generic_playtest_pointer_events_allows_auto_under_none() {
|
||||
let result = run_stable_generic_viewport_fixture(
|
||||
false,
|
||||
Some(
|
||||
r#"<div style="pointer-events:none;padding:20px">
|
||||
<button type="button" data-playtest-id="start" style="pointer-events:auto;padding:0">
|
||||
<span style="display:block;padding:20px">Start</span>
|
||||
</button>
|
||||
</div>"#,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
assert!(result.passed, "{:?}", result.diagnostics);
|
||||
for viewport in REQUIRED_VIEWPORTS {
|
||||
let playtest = &result
|
||||
.viewport_playtests
|
||||
.iter()
|
||||
.find(|entry| entry.viewport == viewport)
|
||||
.expect("each viewport must have gameplay evidence")
|
||||
.result;
|
||||
assert!(playtest.passed, "{viewport:?}: {:?}", playtest.diagnostics);
|
||||
assert_eq!(playtest.initial_sequence, Some(0));
|
||||
assert_eq!(playtest.final_sequence, Some(3));
|
||||
assert_eq!(playtest.final_phase, Some(BrowserPlaytestPhase::Ready));
|
||||
assert!(playtest.assertions.iter().all(|assertion| assertion.passed));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires an installed Chrome/Chromium/Edge and explicit local browser execution"]
|
||||
async fn real_chrome_generic_playtest_pointer_events_keeps_unavailable_controls_blocked() {
|
||||
for (case, markup) in [
|
||||
(
|
||||
"inherited-none",
|
||||
r#"<div style="pointer-events:none"><button data-playtest-id="start">Start</button></div>"#,
|
||||
),
|
||||
(
|
||||
"own-none",
|
||||
r#"<button data-playtest-id="start" style="pointer-events:none">Start</button>"#,
|
||||
),
|
||||
(
|
||||
"own-none-child-auto",
|
||||
r#"<button data-playtest-id="start" style="pointer-events:none;padding:0"><span style="pointer-events:auto;display:block;padding:20px">Start</span></button>"#,
|
||||
),
|
||||
(
|
||||
"disabled",
|
||||
r#"<button data-playtest-id="start" disabled>Start</button>"#,
|
||||
),
|
||||
(
|
||||
"disabled-fieldset",
|
||||
r#"<fieldset disabled><button data-playtest-id="start">Start</button></fieldset>"#,
|
||||
),
|
||||
(
|
||||
"aria-disabled",
|
||||
r#"<button data-playtest-id="start" aria-disabled="true">Start</button>"#,
|
||||
),
|
||||
(
|
||||
"inert",
|
||||
r#"<div inert><button data-playtest-id="start" style="pointer-events:auto">Start</button></div>"#,
|
||||
),
|
||||
(
|
||||
"hidden",
|
||||
r#"<div style="display:none"><button data-playtest-id="start">Start</button></div>"#,
|
||||
),
|
||||
(
|
||||
"covered",
|
||||
r#"<div style="position:relative;width:max-content"><button data-playtest-id="start">Start</button><div style="position:absolute;inset:0;background:black"></div></div>"#,
|
||||
),
|
||||
] {
|
||||
let result = run_stable_generic_viewport_fixture(false, Some(markup)).await;
|
||||
assert!(!result.passed, "{case} must fail");
|
||||
for viewport in REQUIRED_VIEWPORTS {
|
||||
let playtest = &result
|
||||
.viewport_playtests
|
||||
.iter()
|
||||
.find(|entry| entry.viewport == viewport)
|
||||
.expect("each viewport must have gameplay evidence")
|
||||
.result;
|
||||
assert!(!playtest.passed, "{case} / {viewport:?} must fail");
|
||||
assert!(
|
||||
playtest
|
||||
.diagnostics
|
||||
.iter()
|
||||
.any(|message| message.contains("固定试玩控件 start")),
|
||||
"{case} / {viewport:?}: {:?}",
|
||||
playtest.diagnostics
|
||||
);
|
||||
assert!(
|
||||
playtest.assertions.iter().any(|assertion| {
|
||||
assertion.name == "start-control-clicked" && !assertion.passed
|
||||
}),
|
||||
"{case} / {viewport:?} must be rejected before input"
|
||||
);
|
||||
if case == "own-none-child-auto" {
|
||||
assert!(
|
||||
playtest.diagnostics.iter().any(|message| {
|
||||
message == "固定试玩控件 start 处于 disabled 状态"
|
||||
}),
|
||||
"{viewport:?}: child hit must not enable the pointer-blocked target: {:?}",
|
||||
playtest.diagnostics
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_stable_generic_viewport_fixture(
|
||||
mobile_action_broken: bool,
|
||||
start_markup: Option<&str>,
|
||||
) -> BrowserValidationResult {
|
||||
use std::io::{Read, Write};
|
||||
use std::net::TcpListener;
|
||||
@@ -1482,6 +1587,11 @@ async fn run_stable_generic_viewport_fixture(
|
||||
</body>
|
||||
</html>"#;
|
||||
let html = String::from_utf8_lossy(html)
|
||||
.replace(
|
||||
r#"<button type="button" data-playtest-id="start">Start</button>"#,
|
||||
start_markup
|
||||
.unwrap_or(r#"<button type="button" data-playtest-id="start">Start</button>"#),
|
||||
)
|
||||
.replace(
|
||||
"const mobileActionBroken = false;",
|
||||
if mobile_action_broken {
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
这里只记录对当前开发仍有用的症状、根因、排查方法和风险边界。同一事实保留一个当前口径;退役对象的专属过程与单轮测试结果由 Git 历史追溯。遇到旧路径或版本时,以现行代码和专题文档为准。
|
||||
|
||||
## 2026-10-02 固定试玩误判祖先的指针穿透样式
|
||||
|
||||
- `pointer-events:none` 不会强制禁用整棵子树;后代显式 `auto` 可以恢复命中。控件探针只检查目标的计算样式,继承未覆盖的 `none` 仍拒绝;可见性、遮挡、disabled 与 inert 保留各自检查。
|
||||
- 修复和回归必须经过生产输入入口及可信事件驱动的状态变化,不能用程序化点击证明真实可玩。双视口 generic 回归与真实触摸验收需要区分,详见 AGC 实施计划“固定试玩控件的指针命中边界”。
|
||||
|
||||
## 2026-10-01 Rust 分片编译失败只剩汇总错误
|
||||
|
||||
- **原因**:`--message-format=json` 把编译诊断写到 stdout;只读取 `compiler-artifact` 的运行器会丢弃 `compiler-message`,CI 只能看到「due to 1 previous error」。
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# AI 游戏创作智能体 App 实施计划
|
||||
|
||||
## 2026-10-02 固定试玩控件的指针命中边界
|
||||
|
||||
固定试玩只按目标控件自身的计算样式判断 `pointer-events:none`,不累计祖先的 `none`。覆盖层为 `none`、按钮显式恢复 `auto` 是合法布局;按钮未覆盖时仍会继承 `none` 并被拒绝。保留祖先可见性、中心点 `elementFromPoint` 命中、原生 disabled、aria-disabled 和 inert 检查;目标自身为 `none` 时,即使内部子元素恢复 `auto`,也不放行目标控件。
|
||||
|
||||
继续使用现有真实鼠标/触摸输入及游戏状态变化验收,不以 JS 强制点击或改写游戏样式绕过宿主检查。公共探针源码已进入场景指纹,修改后旧指纹证据按现行 stale 规则处理。定向回归入口为 `real_chrome_generic_playtest_pointer_events_`:在 desktop/mobile 两个视口验证祖先穿透、子元素命中和开始/主要操作/重开状态推进,并验证继承禁用、目标禁用、原生禁用、aria-disabled、inert、隐藏及遮挡仍拒绝。generic 的移动视口沿用鼠标输入,不将其表述为真实触摸验收;现场游戏开始流程需另行复核。
|
||||
|
||||
## 2026-10-01 项目文件列举的范围与并发边界
|
||||
|
||||
`agc_list_project_files` 的项目内 `path` 在遍历时限制到目标文件、目标目录及其必要祖先;不递归无关子树,也不读取无关条目的元数据。Agent 的控制面、敏感文件和快照排除规则在读取条目元数据之前生效;通用 UI 文件树保持原有可见范围。引擎生成目录仍按项目根身份排除,不能因定向列举改变判定基准。
|
||||
|
||||
Reference in New Issue
Block a user