feat: animate console jump to latest button
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
/// <reference lib="dom" />
|
||||||
import { assert, assertEquals } from "@std/assert";
|
import { assert, assertEquals } from "@std/assert";
|
||||||
import { dirname, fromFileUrl, join } from "@std/path";
|
import { dirname, fromFileUrl, join } from "@std/path";
|
||||||
import { chromium } from "playwright";
|
import { chromium } from "playwright";
|
||||||
@@ -9,6 +10,9 @@ const fixtureServer = join(
|
|||||||
"tools/web-ux/browser-tests/console_history_fixture_server.ts",
|
"tools/web-ux/browser-tests/console_history_fixture_server.ts",
|
||||||
);
|
);
|
||||||
const workspaceId = "console-history-review";
|
const workspaceId = "console-history-review";
|
||||||
|
type MotionWindow = Window & {
|
||||||
|
latestMotion: { from: number; to: number; duration: number }[];
|
||||||
|
};
|
||||||
|
|
||||||
async function freePort(): Promise<number> {
|
async function freePort(): Promise<number> {
|
||||||
const listener = Deno.listen({ hostname: "127.0.0.1", port: 0 });
|
const listener = Deno.listen({ hostname: "127.0.0.1", port: 0 });
|
||||||
@@ -73,6 +77,20 @@ async function checkConsoleHistory(viewportHeight: number): Promise<void> {
|
|||||||
});
|
});
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
page.setDefaultTimeout(5_000);
|
page.setDefaultTimeout(5_000);
|
||||||
|
await page.addInitScript(() => {
|
||||||
|
(window as unknown as MotionWindow).latestMotion = [];
|
||||||
|
const animate = Element.prototype.animate;
|
||||||
|
Element.prototype.animate = function (frames, options) {
|
||||||
|
if (this.matches(".console-jump-latest") && Array.isArray(frames) && frames.length > 0) {
|
||||||
|
(window as unknown as MotionWindow).latestMotion.push({
|
||||||
|
from: Number(frames[0].opacity),
|
||||||
|
to: Number(frames.at(-1)?.opacity),
|
||||||
|
duration: Number(typeof options === "number" ? options : options?.duration),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return animate.call(this, frames, options);
|
||||||
|
};
|
||||||
|
});
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
const responses: string[] = [];
|
const responses: string[] = [];
|
||||||
page.on("console", (message) => {
|
page.on("console", (message) => {
|
||||||
@@ -250,6 +268,16 @@ async function checkConsoleHistory(viewportHeight: number): Promise<void> {
|
|||||||
|
|
||||||
const latest = page.getByRole("button", { name: "Jump to latest", exact: true });
|
const latest = page.getByRole("button", { name: "Jump to latest", exact: true });
|
||||||
await latest.waitFor();
|
await latest.waitFor();
|
||||||
|
await latest.evaluate(async (element) => {
|
||||||
|
await Promise.all(element.getAnimations().map((animation) => animation.finished));
|
||||||
|
});
|
||||||
|
await latest.hover();
|
||||||
|
await page.waitForFunction(() => getComputedStyle(document.querySelector(".console-jump-latest")!).translate === "0px -2px");
|
||||||
|
assert((await latest.evaluate((element) => getComputedStyle(element).transitionDuration)).includes("0.14s"));
|
||||||
|
await page.mouse.move(0, 0);
|
||||||
|
await latest.evaluate(async (element) => {
|
||||||
|
await Promise.all(element.getAnimations().map((animation) => animation.finished));
|
||||||
|
});
|
||||||
assertEquals((await latest.textContent())?.trim(), "");
|
assertEquals((await latest.textContent())?.trim(), "");
|
||||||
const viewportBeforeJump = await consoleScroll.boundingBox();
|
const viewportBeforeJump = await consoleScroll.boundingBox();
|
||||||
const latestGeometry = await latest.boundingBox();
|
const latestGeometry = await latest.boundingBox();
|
||||||
@@ -274,6 +302,24 @@ async function checkConsoleHistory(viewportHeight: number): Promise<void> {
|
|||||||
await consoleScroll.evaluate((element) => { element.scrollTop = element.scrollHeight; });
|
await consoleScroll.evaluate((element) => { element.scrollTop = element.scrollHeight; });
|
||||||
await latest.waitFor({ state: "detached" });
|
await latest.waitFor({ state: "detached" });
|
||||||
|
|
||||||
|
const motion = await page.evaluate(() => (window as unknown as MotionWindow).latestMotion);
|
||||||
|
assert(motion.some((animation) => animation.from === 0 && animation.to === 1 && animation.duration === 160), "latest icon must animate in");
|
||||||
|
assert(motion.some((animation) => animation.from === 1 && animation.to === 0 && animation.duration === 160), "latest icon must animate out");
|
||||||
|
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||||
|
await page.evaluate(() => { (window as unknown as MotionWindow).latestMotion = []; });
|
||||||
|
await consoleScroll.evaluate((element) => { element.scrollTop = element.scrollHeight / 2; });
|
||||||
|
await latest.waitFor();
|
||||||
|
await latest.hover();
|
||||||
|
const reducedStyle = await latest.evaluate((element) => {
|
||||||
|
const style = getComputedStyle(element);
|
||||||
|
return { translate: style.translate, duration: style.transitionDuration };
|
||||||
|
});
|
||||||
|
assertEquals(reducedStyle, { translate: "none", duration: "0s" });
|
||||||
|
await latest.click();
|
||||||
|
await latest.waitFor({ state: "detached" });
|
||||||
|
assertEquals(await page.evaluate(() => (window as unknown as MotionWindow).latestMotion), [], "reduced motion must skip enter and exit animations");
|
||||||
|
await page.emulateMedia({ reducedMotion: "no-preference" });
|
||||||
|
|
||||||
const centering = await question12.evaluate((button) => {
|
const centering = await question12.evaluate((button) => {
|
||||||
const turn = button.getBoundingClientRect();
|
const turn = button.getBoundingClientRect();
|
||||||
const bar = button.querySelector(".turn-bar")!.getBoundingClientRect();
|
const bar = button.querySelector(".turn-bar")!.getBoundingClientRect();
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { invalidateAll } from "$app/navigation";
|
import { invalidateAll } from "$app/navigation";
|
||||||
import { tick, untrack, type SvelteComponent } from "svelte";
|
import { tick, untrack, type SvelteComponent } from "svelte";
|
||||||
|
import { prefersReducedMotion } from "svelte/motion";
|
||||||
|
import { fly } from "svelte/transition";
|
||||||
import ConsoleLineItem from "$lib/workspace/console/ConsoleLineItem.svelte";
|
import ConsoleLineItem from "$lib/workspace/console/ConsoleLineItem.svelte";
|
||||||
import ConsoleTurnNavigation from "$lib/workspace/console/ConsoleTurnNavigation.svelte";
|
import ConsoleTurnNavigation from "$lib/workspace/console/ConsoleTurnNavigation.svelte";
|
||||||
import ConsoleDisplayStateView from "$lib/workspace/console/ConsoleDisplayState.svelte";
|
import ConsoleDisplayStateView from "$lib/workspace/console/ConsoleDisplayState.svelte";
|
||||||
@@ -1774,6 +1776,7 @@
|
|||||||
{#if !autoFollowConsole && lines.length > 0 && (consoleDisplayState.kind === "ready" || consoleDisplayState.kind === "stale")}
|
{#if !autoFollowConsole && lines.length > 0 && (consoleDisplayState.kind === "ready" || consoleDisplayState.kind === "stale")}
|
||||||
<button
|
<button
|
||||||
class="console-jump-latest"
|
class="console-jump-latest"
|
||||||
|
transition:fly={{ y: 6, duration: prefersReducedMotion.current ? 0 : 160 }}
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Jump to latest"
|
aria-label="Jump to latest"
|
||||||
title="Jump to latest"
|
title="Jump to latest"
|
||||||
@@ -2244,6 +2247,17 @@
|
|||||||
border-color: var(--tui-cyan);
|
border-color: var(--tui-cyan);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
.console-jump-latest {
|
||||||
|
transition: translate 140ms ease-out, color 140ms ease-out,
|
||||||
|
border-color 140ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-jump-latest:hover {
|
||||||
|
translate: 0 -2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.console-jump-latest:focus-visible {
|
.console-jump-latest:focus-visible {
|
||||||
outline: 2px solid var(--tui-cyan);
|
outline: 2px solid var(--tui-cyan);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
|
|||||||
+3
@@ -19,6 +19,9 @@ import { EditorView } from "@codemirror/view";
|
|||||||
import * as alerts from "$lib/workspace/alerts/store";
|
import * as alerts from "$lib/workspace/alerts/store";
|
||||||
import ConsolePage from "./+page.svelte";
|
import ConsolePage from "./+page.svelte";
|
||||||
|
|
||||||
|
// happy-dom does not implement Web Animations. Motion itself is covered in Chromium.
|
||||||
|
vi.mock("svelte/motion", () => ({ prefersReducedMotion: { current: true } }));
|
||||||
|
|
||||||
const multiplexer = vi.hoisted(() => {
|
const multiplexer = vi.hoisted(() => {
|
||||||
type Listener = {
|
type Listener = {
|
||||||
onFrame(frame: unknown): void;
|
onFrame(frame: unknown): void;
|
||||||
|
|||||||
Reference in New Issue
Block a user