mirror of
https://github.com/vector-im/element-call.git
synced 2026-09-25 22:35:49 +00:00
- Cut placement, surrounding context, design measurements and decision history from comments this branch added, across code, styles, tests and stories. - Correct one that claimed most Firefox builds can't route audio. - Drop a comment left duplicated in the footer story. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
157 lines
5.0 KiB
TypeScript
157 lines
5.0 KiB
TypeScript
/*
|
|
Copyright 2026 Element Creations Ltd.
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
Please see LICENSE in the repository root for full details.
|
|
*/
|
|
|
|
import { expect, within } from "storybook/test";
|
|
import { type JSX } from "react";
|
|
|
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
import {
|
|
MicrophoneLevelMeter,
|
|
type MicrophoneLevelMeterProps,
|
|
} from "./MicrophoneLevelMeter";
|
|
import styles from "./MicrophoneLevelMeter.module.css";
|
|
import { LEVEL_SCALE } from "../state/MicrophoneLevel";
|
|
|
|
/** Roughly the menu's width. It only decides how many bars fit. */
|
|
const STORY_WIDTH = 256;
|
|
|
|
const meta = {
|
|
component: MicrophoneLevelMeter,
|
|
decorators: [
|
|
(Story): JSX.Element => (
|
|
<div style={{ inlineSize: STORY_WIDTH }}>
|
|
<Story />
|
|
</div>
|
|
),
|
|
],
|
|
argTypes: {
|
|
state: {
|
|
description:
|
|
"What the selected microphone can say about itself: a level, or a reason there is none.",
|
|
},
|
|
},
|
|
} satisfies Meta<typeof MicrophoneLevelMeter>;
|
|
|
|
export default meta;
|
|
type Story = StoryObj<typeof meta>;
|
|
|
|
/** A quiet room: hiss below the noise floor lights nothing. */
|
|
export const Silent: Story = {
|
|
args: { state: { type: "level", level: 0 } },
|
|
play: async ({ canvasElement }) => {
|
|
await expect(litSegments(canvasElement)).toBe(0);
|
|
},
|
|
};
|
|
|
|
export const QuietSpeech: Story = {
|
|
args: { state: { type: "level", level: 5 } },
|
|
};
|
|
|
|
export const NormalSpeech: Story = {
|
|
args: { state: { type: "level", level: 12 } },
|
|
play: async ({ canvasElement }) => {
|
|
// A floor, not a count: the count follows from the design's bar and gap sizes.
|
|
await expect(
|
|
canvasElement.getElementsByClassName(styles.segment).length,
|
|
).toBeGreaterThanOrEqual(15);
|
|
},
|
|
};
|
|
|
|
export const LoudSpeech: Story = {
|
|
args: { state: { type: "level", level: LEVEL_SCALE } },
|
|
};
|
|
|
|
/** The three volumes differ in how many bars are lit, not only in colour. */
|
|
export const VolumesAreDistinguishable: Story = {
|
|
args: { state: { type: "level", level: 5 } },
|
|
play: async ({ canvasElement, mount }) => {
|
|
const lit: number[] = [];
|
|
for (const level of [5, 12, LEVEL_SCALE]) {
|
|
await mount(<MicrophoneLevelMeter state={{ type: "level", level }} />);
|
|
lit.push(litSegments(canvasElement));
|
|
await expect(within(canvasElement).getByRole("meter")).toHaveAttribute(
|
|
"aria-valuenow",
|
|
String(level),
|
|
);
|
|
}
|
|
await expect(new Set(lit).size).toBe(lit.length);
|
|
await expect(lit).toEqual([...lit].sort((a, b) => a - b));
|
|
},
|
|
};
|
|
|
|
/** Permission refused: a message with a next action, not a still meter. */
|
|
export const PermissionDenied: Story = {
|
|
args: { state: { type: "permission-denied" } },
|
|
play: async ({ canvasElement }) => {
|
|
const canvas = within(canvasElement);
|
|
await expect(canvas.queryByRole("meter")).toBeNull();
|
|
await expect(
|
|
canvas.getByText(/Microphone access is blocked/),
|
|
).toBeVisible();
|
|
},
|
|
};
|
|
|
|
/** No input device, told apart from a refusal. */
|
|
export const NoDevice: Story = {
|
|
args: { state: { type: "no-device" } },
|
|
play: async ({ canvasElement }) => {
|
|
const canvas = within(canvasElement);
|
|
await expect(canvas.queryByRole("meter")).toBeNull();
|
|
await expect(canvas.getByText(/No microphone found/)).toBeVisible();
|
|
|
|
// The icon sits on the middle of the text, however many lines it runs to.
|
|
const middle = (element: Element): number => {
|
|
const box = element.getBoundingClientRect();
|
|
return box.top + box.height / 2;
|
|
};
|
|
const icon = canvasElement.getElementsByClassName(styles.icon)[0];
|
|
const words = canvasElement.getElementsByClassName(styles.message)[0];
|
|
await expect(Math.abs(middle(icon) - middle(words))).toBeLessThanOrEqual(1);
|
|
},
|
|
};
|
|
|
|
/** The same meter at two widths: the bars keep their size and only their count changes. */
|
|
export const ShapeStaysTheSameAtAnyWidth: Story = {
|
|
args: { state: { type: "level", level: 12 } },
|
|
play: async ({ mount, args }) => {
|
|
const narrow = await measureAt(mount, args, 180);
|
|
const wide = await measureAt(mount, args, 400);
|
|
|
|
await expect(narrow.bar).toBe(wide.bar);
|
|
await expect(narrow.gap).toBe(wide.gap);
|
|
await expect(narrow.count).toBeLessThan(wide.count);
|
|
// The bars are still separate, not one run of colour.
|
|
await expect(narrow.gap).toBeGreaterThan(0);
|
|
},
|
|
};
|
|
|
|
/** How many bars are lit. */
|
|
function litSegments(canvasElement: HTMLElement): number {
|
|
return canvasElement.getElementsByClassName(styles.segmentLit).length;
|
|
}
|
|
|
|
/** Renders the meter at one width and reports its bars' shape. */
|
|
async function measureAt(
|
|
mount: (ui: JSX.Element) => Promise<unknown>,
|
|
args: MicrophoneLevelMeterProps,
|
|
width: number,
|
|
): Promise<{ count: number; bar: number; gap: number }> {
|
|
await mount(
|
|
<div style={{ inlineSize: width }}>
|
|
<MicrophoneLevelMeter {...args} />
|
|
</div>,
|
|
);
|
|
const bars = Array.from(document.body.getElementsByClassName(styles.segment));
|
|
const first = bars[0].getBoundingClientRect();
|
|
const second = bars[1].getBoundingClientRect();
|
|
return {
|
|
count: bars.length,
|
|
bar: first.width,
|
|
gap: second.left - first.right,
|
|
};
|
|
}
|