feat(HEL-251): 按确认稿实现登录页动态小K线人物
在桌面登录门户品牌空白带加入红绿小K线角色,支持焦点、密码遮挡、登录反馈和减少动态效果,且不移动原有文案与行情图。 Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
co-authored by
Cursor
multica-agent
parent
865d8b0516
commit
4a63ccd10f
@@ -229,12 +229,26 @@ async function assertConfirmedSkeleton(page, { width, height }) {
|
||||
expect(title.y).toBeLessThan(height * 0.72);
|
||||
expect(stats.y).toBeGreaterThan(height * 0.55);
|
||||
expect(chart.height).toBeGreaterThan(80);
|
||||
await expect(page.locator("#loginMascots")).toBeVisible();
|
||||
expect(card.width).toBeGreaterThan(380);
|
||||
expect(card.width).toBeLessThan(450);
|
||||
if (width === 1440) {
|
||||
expect(brand.width).toBeGreaterThan(470);
|
||||
expect(brand.width).toBeLessThan(520);
|
||||
expect(brand.height).toBe(height);
|
||||
const mascots = await page.locator("#loginMascots").boundingBox();
|
||||
const kicker = await page.locator(".login-brand-kicker").boundingBox();
|
||||
expect(mascots).toBeTruthy();
|
||||
expect(kicker).toBeTruthy();
|
||||
expect(mascots.y).toBeGreaterThan(header.y + header.height - 4);
|
||||
expect(mascots.y + mascots.height).toBeLessThan(kicker.y + 8);
|
||||
const gapTop = header.y + header.height;
|
||||
const gapBottom = kicker.y;
|
||||
const mid = (gapTop + gapBottom) / 2;
|
||||
const mascotMid = mascots.y + mascots.height / 2;
|
||||
expect(Math.abs(mascotMid - mid)).toBeLessThan(48);
|
||||
expect(title.y).toBeGreaterThan(470);
|
||||
expect(title.y).toBeLessThan(580);
|
||||
} else if (width === 1920) {
|
||||
expect(brand.width).toBeGreaterThan(540);
|
||||
expect(brand.width).toBeLessThan(580);
|
||||
@@ -444,3 +458,99 @@ test("workspace switch-account from a second page also returns to that page", as
|
||||
await page.locator('[data-resume-id="1"]').click();
|
||||
await expect(page).toHaveURL(/view=ladderView/);
|
||||
});
|
||||
|
||||
test("desktop mascots react to account focus, password, toggle, loading, success and failure", async ({ page }) => {
|
||||
await openPortal(page, { theme: "light", width: 1440, height: 900, accounts: [] });
|
||||
const mascots = page.locator("#loginMascots");
|
||||
await expect(mascots).toHaveAttribute("data-mood", "idle");
|
||||
await page.locator("#loginUsername").focus();
|
||||
await expect(mascots).toHaveAttribute("data-mood", "account");
|
||||
await page.locator("#loginPassword").focus();
|
||||
await expect(mascots).toHaveAttribute("data-mood", "password");
|
||||
await expect.poll(async () => (
|
||||
page.locator(".login-mascot.is-red .login-mascot-hand.is-left").evaluate((node) => getComputedStyle(node).opacity)
|
||||
)).toBe("1");
|
||||
await page.locator(".login-password-toggle").click();
|
||||
await expect(page.locator("#loginPassword")).toHaveAttribute("type", "text");
|
||||
await expect(mascots).toHaveAttribute("data-mood", "password");
|
||||
await page.locator(".login-password-toggle").click();
|
||||
await expect(page.locator("#loginPassword")).toHaveAttribute("type", "password");
|
||||
await expect(mascots).toHaveAttribute("data-mood", "password");
|
||||
await page.screenshot({ path: path.join(SHOT_DIR, "mascots-password-light-1440.png"), fullPage: true });
|
||||
});
|
||||
|
||||
test("login loading and failure drive short mascot feedback", async ({ page }) => {
|
||||
await openPortal(page, {
|
||||
theme: "dark",
|
||||
width: 1440,
|
||||
height: 900,
|
||||
accounts: [],
|
||||
loginFails: true,
|
||||
loginDelay: 800,
|
||||
});
|
||||
await page.locator("#loginUsername").fill("baiqizhi");
|
||||
await page.locator("#loginPassword").fill("wrong-password");
|
||||
const submit = page.locator(".login-submit").click();
|
||||
await expect(page.locator("#loginMascots")).toHaveAttribute("data-mood", "busy");
|
||||
await expect(page.locator(".login-error")).toContainText("账号名或密码不正确");
|
||||
await expect(page.locator("#loginMascots")).toHaveAttribute("data-mood", "fail");
|
||||
await page.screenshot({ path: path.join(SHOT_DIR, "mascots-fail-dark-1440.png"), fullPage: true });
|
||||
await submit;
|
||||
});
|
||||
|
||||
test("login success plays a hop before leaving the portal", async ({ page }) => {
|
||||
await openPortal(page, { theme: "light", width: 1440, height: 900, accounts: [] });
|
||||
await page.evaluate(() => {
|
||||
window.location.replace = () => {};
|
||||
});
|
||||
await page.locator("#loginUsername").fill("baiqizhi");
|
||||
await page.locator("#loginPassword").fill("password12");
|
||||
const submit = page.locator(".login-submit").click();
|
||||
await expect(page.locator("#loginMascots")).toHaveAttribute("data-mood", /busy|success/);
|
||||
await expect(page.locator("#loginMascots")).toHaveAttribute("data-mood", "success", { timeout: 4000 });
|
||||
await page.screenshot({ path: path.join(SHOT_DIR, "mascots-success-light-1440.png"), fullPage: true });
|
||||
await submit;
|
||||
});
|
||||
|
||||
test("reduced motion keeps static mascots without mouse tracking", async ({ page }) => {
|
||||
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||
await openPortal(page, { theme: "dark", width: 1440, height: 900, accounts: [] });
|
||||
const mascots = page.locator("#loginMascots");
|
||||
await expect(mascots).toBeVisible();
|
||||
await expect(mascots).toHaveAttribute("data-mood", "idle");
|
||||
const before = await mascots.evaluate((node) => getComputedStyle(node.querySelector(".login-mascot.is-red")).getPropertyValue("--pupil-x"));
|
||||
await page.mouse.move(1200, 120);
|
||||
await page.waitForTimeout(120);
|
||||
const after = await mascots.evaluate((node) => getComputedStyle(node.querySelector(".login-mascot.is-red")).getPropertyValue("--pupil-x"));
|
||||
expect(after).toBe(before);
|
||||
await page.locator("#loginPassword").focus();
|
||||
await expect(mascots).toHaveAttribute("data-mood", "password");
|
||||
await page.screenshot({ path: path.join(SHOT_DIR, "mascots-reduced-dark-1440.png"), fullPage: true });
|
||||
});
|
||||
|
||||
test("mouse follow updates mascot pupils on a fine pointer", async ({ page }) => {
|
||||
await openPortal(page, { theme: "light", width: 1440, height: 900, accounts: [] });
|
||||
const mascots = page.locator("#loginMascots");
|
||||
await page.mouse.move(80, 160);
|
||||
await page.waitForTimeout(180);
|
||||
const left = await mascots.evaluate((node) => getComputedStyle(node.querySelector(".login-mascot.is-red")).getPropertyValue("--pupil-x"));
|
||||
await page.mouse.move(1280, 200);
|
||||
await page.waitForTimeout(180);
|
||||
const right = await mascots.evaluate((node) => getComputedStyle(node.querySelector(".login-mascot.is-red")).getPropertyValue("--pupil-x"));
|
||||
expect(Number.parseFloat(right)).toBeGreaterThan(Number.parseFloat(left));
|
||||
});
|
||||
|
||||
test("narrow screens hide mascots without moving the login card", async ({ page }) => {
|
||||
await openPortal(page, { theme: "light", width: 800, height: 900, accounts: [] });
|
||||
await expect(page.locator("#loginMascots")).toBeHidden();
|
||||
await expect(page.locator(".login-card-title")).toHaveText("欢迎回来");
|
||||
await expect(page.locator(".login-brand-title")).toBeHidden();
|
||||
});
|
||||
|
||||
test("theme toggle keeps mascots in the brand gap", async ({ page }) => {
|
||||
await openPortal(page, { theme: "light", width: 1440, height: 900, accounts: [] });
|
||||
await page.locator("#loginThemeToggle").click();
|
||||
await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
|
||||
await assertConfirmedSkeleton(page, { width: 1440, height: 900 });
|
||||
await page.screenshot({ path: path.join(SHOT_DIR, "mascots-idle-dark-1440.png"), fullPage: true });
|
||||
});
|
||||
|
||||
@@ -44,6 +44,29 @@ class LoginPortalContractTests(unittest.TestCase):
|
||||
self.assertIn("padding-bottom: 0;", AUTH)
|
||||
self.assertNotIn("padding: 0 0 var(--statusbar-height);", AUTH)
|
||||
|
||||
def test_confirmed_mascots_fill_the_brand_gap(self) -> None:
|
||||
for needle in (
|
||||
'id="loginMascots"',
|
||||
'aria-hidden="true"',
|
||||
'class="login-mascot is-red"',
|
||||
'class="login-mascot is-green"',
|
||||
'class="login-mascot-back"',
|
||||
'class="login-mascot-slit"',
|
||||
):
|
||||
self.assertIn(needle, LOGIN)
|
||||
for needle in (
|
||||
"--login-mascot-red: #e8605a;",
|
||||
"--login-mascot-green: #46be93;",
|
||||
"--login-mascot-height: clamp(150px, 15vw, 260px);",
|
||||
):
|
||||
self.assertIn(needle, TOKENS)
|
||||
self.assertIn("flex: 1 1 auto;", AUTH)
|
||||
self.assertIn(".login-mascots {", AUTH)
|
||||
self.assertIn("prefers-reduced-motion: reduce", AUTH)
|
||||
self.assertIn('next === "password"', LOGIN_JS)
|
||||
self.assertIn("toggle-password", LOGIN_JS)
|
||||
self.assertIn("celebrateLogin", LOGIN_JS)
|
||||
|
||||
def test_portal_keeps_account_switch_and_theme_hooks(self) -> None:
|
||||
self.assertIn("data-switch-id", LOGIN_JS)
|
||||
self.assertIn("data-resume-id", LOGIN_JS)
|
||||
|
||||
Reference in New Issue
Block a user