1 parent
aa4eb2f8ad
commit
e28fbbd9da
3 files changed
+144
-10
No files matched your search
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in new issue
Block a user