修复密码显隐按钮遮挡文字编辑区
前端验证 / verify (push) Failing after 2m33s

This commit is contained in:
cieanyau committed 2026-11-06 05:23:50 +08:00
1 parent aa4eb2f8ad
commit e28fbbd9da
3 files changed
+144 -10

No files matched your search

-7
View File
@@ -92,17 +92,10 @@
.auth-card .p-password { .auth-card .p-password {
font-size: 1em; font-size: 1em;
} }
.auth-card .p-password-input {
padding-inline-end: 44px;
}
.auth-card .p-inputtext::placeholder { .auth-card .p-inputtext::placeholder {
color: var(--p-text-muted-color); color: var(--p-text-muted-color);
opacity: 1; opacity: 1;
} }
.auth-card .p-password-toggle-mask-icon {
width: 18px;
height: 18px;
}
.auth-card .p-button { .auth-card .p-button {
min-height: 44px; min-height: 44px;
font-size: 1em; font-size: 1em;
+12 -3
View File
@@ -68,15 +68,24 @@ async function togglePassword(callback) {
</template> </template>
<style scoped> <style scoped>
.password-field {
--password-toggle-size: 44px;
}
/* PrimeVue公开的输入样式类位于子组件内;操作宽度与逻辑末端留白必须同源,避免文字被按钮截获。 */
.password-field:has(.password-toggle) :deep(.p-password-input) {
padding-inline-end: calc(
var(--password-toggle-size) + var(--p-form-field-padding-x)
);
}
.password-field .password-toggle { .password-field .password-toggle {
position: absolute; position: absolute;
inset-block: 0; inset-block: 0;
inset-inline-end: 0; inset-inline-end: 0;
display: grid; display: grid;
place-items: center; place-items: center;
width: 44px; inline-size: var(--password-toggle-size);
height: 100%; block-size: 100%;
min-height: 0; min-block-size: 0;
margin: 0; margin: 0;
padding: 0; padding: 0;
color: var(--p-text-muted-color); color: var(--p-text-muted-color);
+132
View File
@@ -7,6 +7,8 @@ import {
eventually, eventually,
fixtureRequest, fixtureRequest,
login, login,
loginAccount,
loginPassword,
scenario, scenario,
} from "./helpers/console-browser.mjs"; } from "./helpers/console-browser.mjs";
@@ -120,6 +122,136 @@ test(
}), }),
); );
for (const width of [320, 390]) {
test(
`password text remains editable beside its visibility button at ${width}px`,
{ timeout: 25000 },
(t) =>
scenario(t, {}, async (page) => {
await page.setViewportSize({ width, height: 844 });
await page.goto(`${appOrigin}/register`);
const field = page.getByLabel("密码 *", { exact: true });
const password = "long-local-password-".repeat(4);
const observations = [];
for (const direction of ["ltr", "rtl"]) {
// dir是浏览器渲染环境,不宣称产品新增了RTL语言入口或全站RTL支持。
await page.locator("html").evaluate((element, dir) => {
element.dir = dir;
}, direction);
await field.fill(password);
await field.press("End");
const point = await field.evaluate((input) => {
const rect = input.getBoundingClientRect();
const style = getComputedStyle(input);
// 长密码滚到末端;点击实际文字编辑区末端,而不是假设按钮应有多少padding。
return {
x:
style.direction === "rtl"
? rect.left +
parseFloat(style.borderLeftWidth) +
parseFloat(style.paddingLeft) +
1
: rect.right -
parseFloat(style.borderRightWidth) -
parseFloat(style.paddingRight) -
1,
y: rect.top + rect.height / 2,
overflowing: input.scrollWidth > input.clientWidth,
};
});
assert.equal(
point.overflowing,
true,
"Exercise a scrolled long password",
);
await page.mouse.click(point.x, point.y);
assert.equal(
await field.getAttribute("type"),
"password",
`${direction} text editing must not activate the visibility button`,
);
assert.equal(
await field.evaluate((input) => input === document.activeElement),
true,
"A real click in the text editor must keep native input focus",
);
await page.keyboard.type("Z");
const edited = await field.inputValue();
assert.equal(edited.length, password.length + 1);
assert.equal(edited.replace("Z", ""), password);
assert.equal(
await page.evaluate(
() => document.documentElement.scrollWidth <= window.innerWidth,
),
true,
);
observations.push({ direction, textClickEditsWithoutToggling: true });
}
return { viewportWidth: width, editing: observations };
}),
);
}
test(
"pending login disables password editing and visibility then restores native attributes",
{ timeout: 25000 },
(t) =>
scenario(t, {}, async (page) => {
let release;
let received = false;
const pending = new Promise((resolve) => {
release = resolve;
});
await page
.context()
.route(`${backendOrigin}/console/v1/login`, async (route) => {
received = true;
await pending;
await route.fulfill({
json: { status: 400, msg: "本地验证失败", data: {} },
headers: { "Access-Control-Allow-Origin": "*" },
});
});
try {
await page.goto(`${appOrigin}/login`);
const field = page.getByLabel("密码", { exact: true });
await page.getByLabel("账号", { exact: true }).fill(loginAccount);
await field.fill(loginPassword);
const toggle = field.locator("..").getByRole("button", {
name: "显示密码",
exact: true,
});
await page.getByRole("button", { name: "登录", exact: true }).click();
await eventually(
() => received,
Boolean,
"Login must reach the HTTP boundary",
);
assert.equal(await field.isDisabled(), true);
assert.equal(await toggle.isDisabled(), true);
const box = await toggle.boundingBox();
await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
await page.keyboard.type("Z");
assert.equal(await field.getAttribute("type"), "password");
assert.equal(await field.inputValue(), loginPassword);
release();
await eventually(
() => field.isEnabled(),
Boolean,
"Rejected login unlocks the form",
);
assert.equal(await toggle.isEnabled(), true);
assert.equal(
await field.getAttribute("aria-describedby"),
"login-error",
);
return { disabledInputAndToggle: true, nativeAriaRestored: true };
} finally {
release();
}
}),
);
async function contrast(locator) { async function contrast(locator) {
return locator.evaluate((element) => { return locator.evaluate((element) => {
const style = getComputedStyle(element); const style = getComputedStyle(element);