Files
buzz/desktop/tests/e2e/animated-avatar.spec.ts
cls 9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
feat: import Chinese-localized Buzz source snapshot
Signed-off-by: cls_宁波本机 <908705107@qq.com>
2026-08-13 18:34:25 +08:00

331 lines
12 KiB
TypeScript

import { expect, test } from "@playwright/test";
import { installMockBridge } from "../helpers/bridge";
import { installFakeCamera } from "../helpers/fakeCamera";
import { openSettings } from "../helpers/settings";
// The review editor (preview + framing + poster strip + backdrop panel) is
// taller than the default 720px viewport — raise it so the whole editor remains visible.
test.use({ viewport: { height: 1280, width: 1280 } });
const UPLOADED_PNG_DESCRIPTOR = {
sha256: "ab".repeat(32),
size: 4096,
type: "image/png",
uploaded: 1700000000,
url: `https://relay.example.com/media/${"ab".repeat(32)}.png`,
};
async function openAnimatedTab(
page: import("@playwright/test").Page,
options: { cameraDelayMs?: number; holdCamera?: boolean } = {},
) {
await installFakeCamera(page, options);
await installMockBridge(page, {
uploadDescriptors: [UPLOADED_PNG_DESCRIPTOR],
});
await page.goto("/");
await openSettings(page, "profile");
await page.getByTestId("profile-avatar-edit").click();
await page.getByRole("tab", { name: "Animated" }).click();
await expect(page.getByTestId("profile-avatar-animated")).toBeVisible();
// Let the tab indicator + content height animations settle.
await page.waitForTimeout(400);
}
test.describe("animated avatar", () => {
test("01 — animated tab idle state", async ({ page }) => {
await openAnimatedTab(page);
await expect(
page.getByTestId("profile-avatar-animated-camera-iphone"),
).toBeVisible();
await expect(
page.getByTestId("profile-avatar-animated-camera-computer"),
).toBeVisible();
await expect(
page.getByTestId("profile-avatar-animated-camera-iphone"),
).toHaveAttribute("aria-pressed", "false");
await expect(
page.getByTestId("profile-avatar-animated-camera-computer"),
).toHaveAttribute("aria-pressed", "false");
await expect(page.getByTestId("profile-avatar-animated-start")).toHaveCount(
0,
);
await expect(
page.getByTestId("profile-avatar-animated-camera-select"),
).toHaveCount(0);
await expect(page.getByTestId("profile-avatar-done")).toHaveCount(0);
});
test("02 — live camera preview", async ({ page }) => {
await openAnimatedTab(page, { holdCamera: true });
const animatedTabHeight = () =>
page
.getByTestId("profile-avatar-animated")
.evaluate((element) =>
Math.round(element.getBoundingClientRect().height),
);
const idleHeight = await animatedTabHeight();
await page.getByTestId("profile-avatar-animated-camera-iphone").click();
await expect
.poll(() =>
page.evaluate(
() =>
(
window as Window & {
__BUZZ_E2E_CAMERA_REQUEST_COUNT__?: number;
}
).__BUZZ_E2E_CAMERA_REQUEST_COUNT__ ?? 0,
),
)
.toBeGreaterThan(0);
const previewSlot = page.getByTestId(
"profile-avatar-animated-preview-slot",
);
await expect(
page.getByRole("status").filter({ hasText: "Starting camera" }),
).toBeVisible();
await expect(
previewSlot.getByRole("status").filter({ hasText: "Starting camera" }),
).toBeVisible();
await expect(page.getByTestId("profile-avatar-preview")).toHaveCount(0);
await expect(
page.getByTestId("profile-avatar-animated-record"),
).toHaveCount(0);
const startingHeight = await animatedTabHeight();
expect(Math.abs(startingHeight - idleHeight)).toBeLessThanOrEqual(1);
await page.evaluate(() => {
(
window as Window & {
__BUZZ_E2E_RELEASE_CAMERA__?: () => void;
}
).__BUZZ_E2E_RELEASE_CAMERA__?.();
});
await expect(
page.getByTestId("profile-avatar-animated-record"),
).toBeVisible({ timeout: 10_000 });
await expect(
previewSlot.getByTestId("profile-avatar-animated-preview"),
).toBeVisible();
const liveHeight = await animatedTabHeight();
expect(Math.abs(liveHeight - idleHeight)).toBeLessThanOrEqual(1);
await expect
.poll(() =>
page.evaluate(
() =>
(
window as Window & {
__BUZZ_E2E_CAMERA_CONSTRAINTS__?: MediaStreamConstraints[];
}
).__BUZZ_E2E_CAMERA_CONSTRAINTS__?.at(-1)?.video,
),
)
.toEqual(
expect.objectContaining({
deviceId: { exact: "iphone-continuity" },
facingMode: { ideal: "user" },
}),
);
await expect(page.getByTestId("profile-avatar-done")).toHaveCount(0);
await page.waitForTimeout(500);
await page.getByTestId("profile-avatar-animated-record").click();
await expect(
page.getByTestId("profile-avatar-animated-sections"),
).toBeVisible({ timeout: 60_000 });
await expect(
page.getByTestId("profile-avatar-animated-size"),
).toHaveAttribute("aria-valuenow", "116");
});
test("03 — recording ring, pop-out review, custom color", async ({
page,
}) => {
test.setTimeout(120_000);
await openAnimatedTab(page);
await page.getByTestId("profile-avatar-animated-camera-computer").click();
await expect(
page.getByTestId("profile-avatar-animated-record"),
).toBeVisible({ timeout: 10_000 });
await page.getByTestId("profile-avatar-animated-record").click();
// Mid-recording: the timer stroke sweeps around the capture circle.
await expect(
page.getByTestId("profile-avatar-animated-timer-ring"),
).toBeVisible();
await page.waitForTimeout(1500);
// Review: the section icon row appears once processing finishes.
await expect(
page.getByTestId("profile-avatar-animated-sections"),
).toBeVisible({ timeout: 60_000 });
await expect(
page.getByTestId("profile-avatar-animated-size"),
).toHaveAttribute("aria-valuenow", "126");
await expect(page.getByTestId("profile-avatar-done")).toBeVisible();
await expect(
page.getByTestId("profile-avatar-animated-framing-readout"),
).toHaveCount(0);
const previewSnapshot = () =>
page
.getByTestId("profile-avatar-animated-review-preview")
.evaluate((canvas) => (canvas as HTMLCanvasElement).toDataURL());
// Color section: pick a deterministic backdrop color (the default is random).
await page.getByTestId("profile-avatar-animated-section-color").click();
await expect(
page.getByTestId("profile-avatar-animated-backdrop-none"),
).toHaveCount(0);
await page
.getByTestId("profile-avatar-animated-backdrop-grid")
.locator("button")
.nth(5)
.click();
// Poster section: selecting a frame updates the (paused) preview.
await page.getByTestId("profile-avatar-animated-section-poster").click();
await expect(
page.getByTestId("profile-avatar-animated-poster-strip"),
).toBeVisible();
await expect(
page.getByTestId("profile-avatar-animated-poster-selector"),
).toBeVisible();
await expect(
page.getByTestId("profile-avatar-animated-review-help"),
).toHaveText("Pick the still shown before hover.");
await expect(
page.getByTestId("profile-avatar-animated-poster-prev"),
).toHaveCount(0);
await expect(
page.getByTestId("profile-avatar-animated-poster-next"),
).toHaveCount(0);
const scrubber = page.getByTestId(
"profile-avatar-animated-poster-scrubber",
);
const scrubberBox = await scrubber.boundingBox();
if (!scrubberBox) {
throw new Error("Animated avatar poster scrubber did not render bounds.");
}
await scrubber.click({
position: {
x: scrubberBox.width * 0.72,
y: scrubberBox.height / 2,
},
});
await expect(scrubber).not.toHaveAttribute("aria-valuenow", "0");
// The Circle tool stays implemented, but it is hidden in the review row for now.
await expect(
page.getByTestId("profile-avatar-animated-section-shape"),
).toHaveCount(0);
// You section: the DialKit-style local slider re-composes the preview.
await page.getByTestId("profile-avatar-animated-section-person").click();
await expect(
page.getByTestId("profile-avatar-animated-review-help"),
).toHaveCount(0);
await expect(
page.getByTestId("profile-avatar-animated-outline-toggle"),
).toHaveAttribute("aria-pressed", "true");
await page.getByTestId("profile-avatar-animated-outline-toggle").click();
await expect(
page.getByTestId("profile-avatar-animated-outline-toggle"),
).toHaveAttribute("aria-pressed", "false");
await page.getByTestId("profile-avatar-animated-outline-toggle").click();
const sizeSlider = page.getByTestId("profile-avatar-animated-size");
const initialSizeValue = await sizeSlider.getAttribute("aria-valuenow");
const sizeSliderBox = await sizeSlider.boundingBox();
if (!sizeSliderBox) {
throw new Error("Animated avatar size slider did not render bounds.");
}
await sizeSlider.click({
position: {
x: sizeSliderBox.width * 0.25,
y: sizeSliderBox.height / 2,
},
});
await expect(sizeSlider).not.toHaveAttribute(
"aria-valuenow",
initialSizeValue ?? "",
);
// Restore defaults so the standard framing remains covered.
await page.getByTestId("profile-avatar-animated-reset-framing").click();
await page.waitForTimeout(400);
// Custom backdrop color panel (shared HSV picker).
await page.getByTestId("profile-avatar-animated-section-color").click();
await page.getByTestId("profile-avatar-animated-backdrop-custom").click();
await expect(
page.getByTestId("profile-avatar-animated-custom-color-spectrum"),
).toBeVisible();
await expect(page.getByTestId("profile-avatar-done")).toHaveCount(0);
const beforeCustomColor = await previewSnapshot();
const customColorSpectrum = page.getByTestId(
"profile-avatar-animated-custom-color-spectrum",
);
const customColorSpectrumBox = await customColorSpectrum.boundingBox();
if (!customColorSpectrumBox) {
throw new Error(
"Animated avatar custom color picker did not render bounds.",
);
}
await customColorSpectrum.click({
position: {
x: customColorSpectrumBox.width * 0.88,
y: customColorSpectrumBox.height * 0.82,
},
});
await page.waitForTimeout(400);
const customColorPreview = await previewSnapshot();
expect(customColorPreview).not.toEqual(beforeCustomColor);
await page.getByTestId("profile-avatar-animated-custom-color-done").click();
await expect(
page.getByTestId("profile-avatar-animated-backdrop-custom"),
).toHaveAttribute("aria-pressed", "true");
expect(await previewSnapshot()).toEqual(customColorPreview);
// Done applies the recording (APNG encode + mocked two-file upload) and
// then saves the profile in one step.
const doneButton = page.getByTestId("profile-avatar-done");
await doneButton.click();
await expect(doneButton).toContainText("Saving", { timeout: 2_000 });
await expect(page.getByRole("tab", { name: "Image" })).toBeDisabled();
await expect(page.getByRole("tab", { name: "Emoji" })).toBeDisabled();
await expect(page.getByRole("tab", { name: "Animated" })).toBeDisabled();
await expect
.poll(
() =>
page.evaluate(
() =>
(
window as Window & { __BUZZ_E2E_COMMANDS__?: string[] }
).__BUZZ_E2E_COMMANDS__?.filter(
(command) => command === "upload_media_bytes",
).length ?? 0,
),
{ timeout: 30_000 },
)
.toBeGreaterThanOrEqual(2);
await expect
.poll(
() =>
page.evaluate(
() =>
(window as Window & { __BUZZ_E2E_COMMANDS__?: string[] })
.__BUZZ_E2E_COMMANDS__ ?? [],
),
{ timeout: 30_000 },
)
.toEqual(expect.arrayContaining(["update_profile"]));
await expect(page.getByTestId("profile-avatar-animated-error")).toHaveCount(
0,
);
await expect(page.getByTestId("profile-avatar-preview")).toBeVisible({
timeout: 10_000,
});
});
});