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
Signed-off-by: cls_宁波本机 <908705107@qq.com>
331 lines
12 KiB
TypeScript
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,
|
|
});
|
|
});
|
|
});
|