Test SpotlightTile more thoroughly
Catching two accessibility issues along the way: we were putting the wrong accessible labels on the 'expand' button, and even the off-screen pages of the spotlight tile were being exposed to accessibility technologies rather than hidden.
This commit is contained in:
@@ -161,8 +161,8 @@
|
|||||||
"video_tile": {
|
"video_tile": {
|
||||||
"always_show": "Always show",
|
"always_show": "Always show",
|
||||||
"change_fit_contain": "Fit to frame",
|
"change_fit_contain": "Fit to frame",
|
||||||
"exit_full_screen": "Exit full screen",
|
"collapse": "Collapse",
|
||||||
"full_screen": "Full screen",
|
"expand": "Expand",
|
||||||
"mute_for_me": "Mute for me",
|
"mute_for_me": "Mute for me",
|
||||||
"volume": "Volume"
|
"volume": "Volume"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,14 +81,14 @@ test("toggle fit/contain for a participant's video", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("local media remembers whether it should always be shown", async () => {
|
test("local media remembers whether it should always be shown", async () => {
|
||||||
await withLocalMedia(async (vm) =>
|
await withLocalMedia({}, async (vm) =>
|
||||||
withTestScheduler(({ expectObservable, schedule }) => {
|
withTestScheduler(({ expectObservable, schedule }) => {
|
||||||
schedule("-a|", { a: () => vm.setAlwaysShow(false) });
|
schedule("-a|", { a: () => vm.setAlwaysShow(false) });
|
||||||
expectObservable(vm.alwaysShow).toBe("ab", { a: true, b: false });
|
expectObservable(vm.alwaysShow).toBe("ab", { a: true, b: false });
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
// Next local media should start out *not* always shown
|
// Next local media should start out *not* always shown
|
||||||
await withLocalMedia(async (vm) =>
|
await withLocalMedia({}, async (vm) =>
|
||||||
withTestScheduler(({ expectObservable, schedule }) => {
|
withTestScheduler(({ expectObservable, schedule }) => {
|
||||||
schedule("-a|", { a: () => vm.setAlwaysShow(true) });
|
schedule("-a|", { a: () => vm.setAlwaysShow(true) });
|
||||||
expectObservable(vm.alwaysShow).toBe("ab", { a: false, b: true });
|
expectObservable(vm.alwaysShow).toBe("ab", { a: false, b: true });
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { RemoteTrackPublication } from "livekit-client";
|
import { test, expect, vi } from "vitest";
|
||||||
import { test, expect } from "vitest";
|
import { isInaccessible, render, screen } from "@testing-library/react";
|
||||||
import { render, screen } from "@testing-library/react";
|
|
||||||
import { axe } from "vitest-axe";
|
import { axe } from "vitest-axe";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
|
||||||
import { SpotlightTile } from "./SpotlightTile";
|
import { SpotlightTile } from "./SpotlightTile";
|
||||||
import { withRemoteMedia } from "../utils/test";
|
import { withLocalMedia, withRemoteMedia } from "../utils/test";
|
||||||
|
|
||||||
global.IntersectionObserver = class MockIntersectionObserver {
|
global.IntersectionObserver = class MockIntersectionObserver {
|
||||||
public observe(): void {}
|
public observe(): void {}
|
||||||
@@ -33,25 +33,50 @@ test("SpotlightTile is accessible", async () => {
|
|||||||
rawDisplayName: "Alice",
|
rawDisplayName: "Alice",
|
||||||
getMxcAvatarUrl: () => "mxc://adfsg",
|
getMxcAvatarUrl: () => "mxc://adfsg",
|
||||||
},
|
},
|
||||||
|
{},
|
||||||
|
async (vm1) => {
|
||||||
|
await withLocalMedia(
|
||||||
{
|
{
|
||||||
getTrackPublication: () =>
|
rawDisplayName: "Bob",
|
||||||
({}) as Partial<RemoteTrackPublication> as RemoteTrackPublication,
|
getMxcAvatarUrl: () => "mxc://dlskf",
|
||||||
},
|
},
|
||||||
async (vm) => {
|
async (vm2) => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const toggleExpanded = vi.fn();
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<SpotlightTile
|
<SpotlightTile
|
||||||
vms={[vm]}
|
vms={[vm1, vm2]}
|
||||||
targetWidth={300}
|
targetWidth={300}
|
||||||
targetHeight={200}
|
targetHeight={200}
|
||||||
maximised={false}
|
maximised={false}
|
||||||
expanded={false}
|
expanded={false}
|
||||||
onToggleExpanded={() => {}}
|
onToggleExpanded={toggleExpanded}
|
||||||
showIndicators
|
showIndicators
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(await axe(container)).toHaveNoViolations();
|
expect(await axe(container)).toHaveNoViolations();
|
||||||
// Name should be visible
|
// Alice should be in the spotlight, with her name and avatar on the
|
||||||
|
// first page
|
||||||
screen.getByText("Alice");
|
screen.getByText("Alice");
|
||||||
|
const aliceAvatar = screen.getByRole("img");
|
||||||
|
expect(screen.queryByRole("button", { name: "common.back" })).toBe(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
// Bob should be out of the spotlight, and therefore invisible
|
||||||
|
expect(isInaccessible(screen.getByText("Bob"))).toBe(true);
|
||||||
|
// Now navigate to Bob
|
||||||
|
await user.click(screen.getByRole("button", { name: "common.next" }));
|
||||||
|
screen.getByText("Bob");
|
||||||
|
expect(screen.getByRole("img")).not.toBe(aliceAvatar);
|
||||||
|
expect(isInaccessible(screen.getByText("Alice"))).toBe(true);
|
||||||
|
// Can toggle whether the tile is expanded
|
||||||
|
await user.click(
|
||||||
|
screen.getByRole("button", { name: "video_tile.expand" }),
|
||||||
|
);
|
||||||
|
expect(toggleExpanded).toHaveBeenCalled();
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ interface SpotlightItemBaseProps {
|
|||||||
member: RoomMember | undefined;
|
member: RoomMember | undefined;
|
||||||
unencryptedWarning: boolean;
|
unencryptedWarning: boolean;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
|
"aria-hidden"?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SpotlightUserMediaItemBaseProps extends SpotlightItemBaseProps {
|
interface SpotlightUserMediaItemBaseProps extends SpotlightItemBaseProps {
|
||||||
@@ -118,10 +119,21 @@ interface SpotlightItemProps {
|
|||||||
* Whether this item should act as a scroll snapping point.
|
* Whether this item should act as a scroll snapping point.
|
||||||
*/
|
*/
|
||||||
snap: boolean;
|
snap: boolean;
|
||||||
|
"aria-hidden"?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SpotlightItem = forwardRef<HTMLDivElement, SpotlightItemProps>(
|
const SpotlightItem = forwardRef<HTMLDivElement, SpotlightItemProps>(
|
||||||
({ vm, targetWidth, targetHeight, intersectionObserver, snap }, theirRef) => {
|
(
|
||||||
|
{
|
||||||
|
vm,
|
||||||
|
targetWidth,
|
||||||
|
targetHeight,
|
||||||
|
intersectionObserver,
|
||||||
|
snap,
|
||||||
|
"aria-hidden": ariaHidden,
|
||||||
|
},
|
||||||
|
theirRef,
|
||||||
|
) => {
|
||||||
const ourRef = useRef<HTMLDivElement | null>(null);
|
const ourRef = useRef<HTMLDivElement | null>(null);
|
||||||
const ref = useMergedRefs(ourRef, theirRef);
|
const ref = useMergedRefs(ourRef, theirRef);
|
||||||
const displayName = useDisplayName(vm);
|
const displayName = useDisplayName(vm);
|
||||||
@@ -153,6 +165,7 @@ const SpotlightItem = forwardRef<HTMLDivElement, SpotlightItemProps>(
|
|||||||
member: vm.member,
|
member: vm.member,
|
||||||
unencryptedWarning,
|
unencryptedWarning,
|
||||||
displayName,
|
displayName,
|
||||||
|
"aria-hidden": ariaHidden,
|
||||||
};
|
};
|
||||||
|
|
||||||
return vm instanceof ScreenShareViewModel ? (
|
return vm instanceof ScreenShareViewModel ? (
|
||||||
@@ -280,7 +293,12 @@ export const SpotlightTile = forwardRef<HTMLDivElement, Props>(
|
|||||||
targetWidth={targetWidth}
|
targetWidth={targetWidth}
|
||||||
targetHeight={targetHeight}
|
targetHeight={targetHeight}
|
||||||
intersectionObserver={intersectionObserver}
|
intersectionObserver={intersectionObserver}
|
||||||
|
// This is how we get the container to scroll to the right media
|
||||||
|
// when the previous/next buttons are clicked: we temporarily
|
||||||
|
// remove all scroll snap points except for just the one media
|
||||||
|
// that we want to bring into view
|
||||||
snap={scrollToId === null || scrollToId === vm.id}
|
snap={scrollToId === null || scrollToId === vm.id}
|
||||||
|
aria-hidden={(scrollToId ?? visibleId) !== vm.id}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -288,9 +306,7 @@ export const SpotlightTile = forwardRef<HTMLDivElement, Props>(
|
|||||||
<button
|
<button
|
||||||
className={classNames(styles.expand)}
|
className={classNames(styles.expand)}
|
||||||
aria-label={
|
aria-label={
|
||||||
expanded
|
expanded ? t("video_tile.collapse") : t("video_tile.expand")
|
||||||
? t("video_tile.full_screen")
|
|
||||||
: t("video_tile.exit_full_screen")
|
|
||||||
}
|
}
|
||||||
onClick={onToggleExpanded}
|
onClick={onToggleExpanded}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -17,7 +17,12 @@ import { map } from "rxjs";
|
|||||||
import { RunHelpers, TestScheduler } from "rxjs/testing";
|
import { RunHelpers, TestScheduler } from "rxjs/testing";
|
||||||
import { expect, vi } from "vitest";
|
import { expect, vi } from "vitest";
|
||||||
import { RoomMember } from "matrix-js-sdk/src/matrix";
|
import { RoomMember } from "matrix-js-sdk/src/matrix";
|
||||||
import { LocalParticipant, RemoteParticipant } from "livekit-client";
|
import {
|
||||||
|
LocalParticipant,
|
||||||
|
LocalTrackPublication,
|
||||||
|
RemoteParticipant,
|
||||||
|
RemoteTrackPublication,
|
||||||
|
} from "livekit-client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
LocalUserMediaViewModel,
|
LocalUserMediaViewModel,
|
||||||
@@ -66,14 +71,47 @@ export function withTestScheduler(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mockMember(member: Partial<RoomMember>): RoomMember {
|
||||||
|
return {
|
||||||
|
on() {
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
off() {
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
addListener() {
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
removeListener() {
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
...member,
|
||||||
|
} as RoomMember;
|
||||||
|
}
|
||||||
|
|
||||||
export async function withLocalMedia(
|
export async function withLocalMedia(
|
||||||
|
member: Partial<RoomMember>,
|
||||||
continuation: (vm: LocalUserMediaViewModel) => Promise<void>,
|
continuation: (vm: LocalUserMediaViewModel) => Promise<void>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const member = {} as unknown as RoomMember;
|
|
||||||
const vm = new LocalUserMediaViewModel(
|
const vm = new LocalUserMediaViewModel(
|
||||||
"a",
|
"local",
|
||||||
member,
|
mockMember(member),
|
||||||
{} as Partial<LocalParticipant> as LocalParticipant,
|
{
|
||||||
|
getTrackPublication: () =>
|
||||||
|
({}) as Partial<LocalTrackPublication> as LocalTrackPublication,
|
||||||
|
on() {
|
||||||
|
return this as LocalParticipant;
|
||||||
|
},
|
||||||
|
off() {
|
||||||
|
return this as LocalParticipant;
|
||||||
|
},
|
||||||
|
addListener() {
|
||||||
|
return this as LocalParticipant;
|
||||||
|
},
|
||||||
|
removeListener() {
|
||||||
|
return this as LocalParticipant;
|
||||||
|
},
|
||||||
|
} as Partial<LocalParticipant> as LocalParticipant,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
@@ -89,24 +127,12 @@ export async function withRemoteMedia(
|
|||||||
continuation: (vm: RemoteUserMediaViewModel) => Promise<void>,
|
continuation: (vm: RemoteUserMediaViewModel) => Promise<void>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const vm = new RemoteUserMediaViewModel(
|
const vm = new RemoteUserMediaViewModel(
|
||||||
"a",
|
"remote",
|
||||||
{
|
mockMember(member),
|
||||||
on() {
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
off() {
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
addListener() {
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
removeListener() {
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
...member,
|
|
||||||
} as RoomMember,
|
|
||||||
{
|
{
|
||||||
setVolume() {},
|
setVolume() {},
|
||||||
|
getTrackPublication: () =>
|
||||||
|
({}) as Partial<RemoteTrackPublication> as RemoteTrackPublication,
|
||||||
on() {
|
on() {
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user