Compare commits
3
Commits
5fbe1d9923
...
3a47070d3f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a47070d3f | ||
|
|
6cca57f229 | ||
|
|
ea3edde697 |
@@ -1462,6 +1462,32 @@ vary) wins, ours being told from theirs by **identity** rather than
|
||||
that ends the gesture is swallowed, keyed on the gesture rather than on
|
||||
a time window so the first tap on the menu it opened is not eaten too.
|
||||
|
||||
**A control revealed by `:hover` is gated on the device having hover,
|
||||
and which way round depends on whether it is the only route to its
|
||||
action.** The gate itself is not optional: a touch long-press
|
||||
synthesises a hover state in the WebView, so every one of these flashed
|
||||
into view during the 500 ms hold above — a control appearing because
|
||||
the user was reaching for a different one. Where the action is reachable
|
||||
another way the control is **absent** on a touch device (the home card's
|
||||
play button, #68; the queue row's remove, which the row's bottom-sheet
|
||||
menu carries since #60), and that is `display: none` outside
|
||||
`(hover: hover) and (pointer: fine)` rather than `opacity: 0` or
|
||||
`visibility: hidden`, both of which leave a button holding its hit area
|
||||
and its place in the accessibility tree. Where the control is the
|
||||
**only** route it is instead always visible under
|
||||
`@media not all and (hover: hover)` — `track-details`'s cover-art
|
||||
overlay and remove, `shortcut-capture`'s reset (#137) — because hiding
|
||||
it takes the action away entirely.
|
||||
|
||||
One thing to know before checking either: **no tier here can render as a
|
||||
touch device.** CDP's `Emulation.setEmulatedMedia` does not reach the
|
||||
component tier's iframe, and the e2e projects are Desktop Chrome and
|
||||
Desktop Safari, neither of which has touch. So
|
||||
`hover-affordance.test.ts` asserts the *parsed stylesheet* — which rule
|
||||
sits inside which media query — and says so; the regression it exists
|
||||
for is someone hoisting a rule out of its query as a tidy-up, which
|
||||
nothing on a desktop renders differently.
|
||||
|
||||
Three lists had no focused row to open a menu *from* — the queue panel
|
||||
and both playlist detail views — and gained a roving tab stop through
|
||||
`utils/roving-rows.ts`. **`track-list` deliberately does not use it**:
|
||||
@@ -2489,6 +2515,31 @@ missing half; `catalogFailed` is the only route to `unavailable` now,
|
||||
and the timer is a 60 s backstop for a genuine hang rather than the
|
||||
verdict.
|
||||
|
||||
**On a phone that page is one scroll container, and the header is in
|
||||
it** (#66). It was built as a fixed header over a scrolling tracklist,
|
||||
which is the desktop arrangement: at the reference device's 424×439 the
|
||||
header owned **253 of the panel's 318px** and the list scrolled inside
|
||||
the 64 that were left. Below 600px the *host* is the scroller and
|
||||
`.content` stops being one, so the whole page moves together — which is
|
||||
only available because this tracklist is plain DOM rather than a
|
||||
virtualizer, and because `.main-panel > *` already gives the host a
|
||||
definite height.
|
||||
|
||||
Three things about it are load-bearing. **Another `min-width: 0` was
|
||||
not the fix**: `.album-info` carries one and was shrinking exactly as
|
||||
asked, to 112px beside a 200px cover — so the title drew as `G…` and
|
||||
"Shuffle album" ended at x=443 inside a 424px box, clipped by the
|
||||
component's own `overflow: hidden` and reachable by no gesture. A row
|
||||
with a fixed-size sibling has to **stack** at that width, or the column
|
||||
that must shrink has nothing to be wide with. **`layout-overflow.spec.ts`
|
||||
cannot see any of this** — `body.scrollWidth` equalled the viewport
|
||||
throughout, because the overflow was *inside* a component; the spec
|
||||
measures each header control against the host's own box, which is
|
||||
`top-bar-fit.spec.ts`'s shape for the same reason. And **the phone block
|
||||
is last in the stylesheet**, on `index.css`'s rule: a media query adds
|
||||
no specificity, so written above the plain rules it overrides every
|
||||
declaration in it is silently dead.
|
||||
|
||||
**Activating a row plays the list the row is in, from that row.** A
|
||||
double-click — and Play on a single row's context menu — queues the
|
||||
list as *displayed* with `startIndex` on that row, not a queue of one
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
import { test, expect } from '../support/fixtures.js';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* The album page on a phone (#66).
|
||||
*
|
||||
* Two faults, and neither was visible to `layout-overflow.spec.ts`:
|
||||
* that spec asserts the *shell* needs no sideways scrolling, and the
|
||||
* shell was correct throughout — `body.scrollWidth === clientWidth`
|
||||
* while `explore-album-details` itself measured 443 inside a 424px box
|
||||
* and clipped two of the album's three primary actions with its own
|
||||
* `overflow: hidden`. So the measurement here is **per control against
|
||||
* the component's box**, which is the same shape `top-bar-fit.spec.ts`
|
||||
* needed for the same reason.
|
||||
*
|
||||
* The other half is the scroll: the page was a fixed header over a
|
||||
* scrolling tracklist, so at the reference device's 424x439 the header
|
||||
* owned 253 of the panel's 318px and the list scrolled in the 64 that
|
||||
* were left. It is one scroll container below 600px, which is a
|
||||
* property of the *host* rather than of `.content`.
|
||||
*
|
||||
* The engine is the caveat this tier cannot close: the reference device
|
||||
* renders in Chrome 113 and this is Chromium/WebKit. A flex direction
|
||||
* and a scroll container are nowhere near that engine's documented gaps
|
||||
* (relaxed nesting, the Popover API, `light-dark()`), but "it renders
|
||||
* at that size in Chromium" is not evidence about the phone.
|
||||
*/
|
||||
|
||||
/** The phone this was measured on, in CSS pixels. */
|
||||
const DEVICE = { width: 424, height: 439 };
|
||||
|
||||
const details = (page: Page) => page.locator('explore-album-details');
|
||||
|
||||
/** The page's own boxes, read from inside its shadow root. */
|
||||
const geometry = (page: Page) =>
|
||||
page.evaluate(() => {
|
||||
const host = document.querySelector('explore-album-details');
|
||||
const sr = host?.shadowRoot;
|
||||
|
||||
if (!host || !sr) return null;
|
||||
|
||||
const box = (sel: string) => {
|
||||
const el = sr.querySelector(sel);
|
||||
|
||||
if (!el) return null;
|
||||
|
||||
const r = el.getBoundingClientRect();
|
||||
|
||||
return { width: Math.round(r.width), right: Math.round(r.right) };
|
||||
};
|
||||
|
||||
const content = sr.querySelector('.content');
|
||||
|
||||
return {
|
||||
hostWidth: host.clientWidth,
|
||||
hostScrollWidth: host.scrollWidth,
|
||||
// The host is the scroller below 600px, so the page is taller
|
||||
// than its box rather than the tracklist being a window inside it.
|
||||
hostScrolls: host.scrollHeight > host.clientHeight,
|
||||
contentScrolls: content
|
||||
? content.scrollHeight > content.clientHeight
|
||||
: null,
|
||||
header: box('.album-header'),
|
||||
play: box('[data-testid="album-play"]'),
|
||||
shuffle: box('[data-testid="album-shuffle"]'),
|
||||
queue: box('[data-testid="album-queue"]'),
|
||||
title: (() => {
|
||||
const el = sr.querySelector('.album-title-text');
|
||||
|
||||
return el ? el.scrollWidth <= el.clientWidth + 1 : null;
|
||||
})(),
|
||||
};
|
||||
});
|
||||
|
||||
test.describe('the album page on a phone', () => {
|
||||
test.beforeEach(async ({ app }) => {
|
||||
await app.setViewportSize(DEVICE);
|
||||
await openFirstAlbum(app);
|
||||
});
|
||||
|
||||
test.afterEach(async ({ app }) => {
|
||||
await app.setViewportSize({ width: 1440, height: 900 });
|
||||
await app.getByTestId('nav-tracks').click();
|
||||
});
|
||||
|
||||
test('keeps every action inside its own box', async ({ app }) => {
|
||||
const geo = await geometry(app);
|
||||
|
||||
expect(geo).not.toBeNull();
|
||||
// "Shuffle album" ended at x=443 in a 424px component and could not
|
||||
// be reached by any gesture; "Add to queue" at 440.
|
||||
for (const action of ['play', 'shuffle', 'queue'] as const) {
|
||||
expect(
|
||||
geo?.[action],
|
||||
`${action} is rendered`,
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
geo?.[action]?.right ?? 0,
|
||||
`${action} ends inside the page`,
|
||||
).toBeLessThanOrEqual(geo?.hostWidth ?? 0);
|
||||
}
|
||||
|
||||
expect(geo?.hostScrollWidth).toBe(geo?.hostWidth);
|
||||
expect(geo?.header?.width).toBe(geo?.hostWidth);
|
||||
});
|
||||
|
||||
test('gives the title the row rather than one glyph of it', async ({
|
||||
app,
|
||||
}) => {
|
||||
// `.album-info` was squeezed to 112px beside the art, so an album
|
||||
// called *Glass Harbour* drew as `G…`. It carries `min-width: 0`
|
||||
// and was shrinking as asked — the row had to stack.
|
||||
expect(await geometry(app).then((g) => g?.title)).toBe(true);
|
||||
});
|
||||
|
||||
test('scrolls as one page, with the header scrolling away', async ({
|
||||
app,
|
||||
}) => {
|
||||
const before = await geometry(app);
|
||||
|
||||
expect(before?.hostScrolls).toBe(true);
|
||||
expect(before?.contentScrolls).toBe(false);
|
||||
|
||||
const headerTop = () =>
|
||||
app.evaluate(
|
||||
() =>
|
||||
document
|
||||
.querySelector('explore-album-details')
|
||||
?.shadowRoot?.querySelector('.album-header')
|
||||
?.getBoundingClientRect().top ?? 0,
|
||||
);
|
||||
|
||||
expect(await headerTop()).toBeGreaterThanOrEqual(0);
|
||||
|
||||
// A wheel gesture, not `scrollTop`: `overflow: hidden` still permits
|
||||
// programmatic scrolling, so a probe that assigns it passes on the
|
||||
// build this exists to fail.
|
||||
await details(app).hover();
|
||||
await app.mouse.wheel(0, 250);
|
||||
|
||||
await expect.poll(headerTop).toBeLessThan(-100);
|
||||
});
|
||||
|
||||
test('is the desktop arrangement again above the breakpoint', async ({
|
||||
app,
|
||||
}) => {
|
||||
await app.setViewportSize({ width: 1024, height: 800 });
|
||||
|
||||
// The same element, re-laid-out: one component with two
|
||||
// arrangements, not a phone-only copy.
|
||||
await expect
|
||||
.poll(async () => (await geometry(app))?.hostScrolls)
|
||||
.toBe(false);
|
||||
|
||||
const arrangement = await app.evaluate(() => {
|
||||
const sr = document.querySelector('explore-album-details')?.shadowRoot;
|
||||
const header = sr?.querySelector('.album-header');
|
||||
const content = sr?.querySelector('.content');
|
||||
|
||||
return {
|
||||
direction: header ? getComputedStyle(header).flexDirection : null,
|
||||
contentOverflow: content ? getComputedStyle(content).overflowY : null,
|
||||
};
|
||||
});
|
||||
|
||||
expect(arrangement.direction).toBe('row');
|
||||
expect(arrangement.contentOverflow).toBe('auto');
|
||||
});
|
||||
});
|
||||
|
||||
/** Albums → the second card, which navigates to the album page. */
|
||||
async function openFirstAlbum(app: Page): Promise<void> {
|
||||
// Below 600px the sidebar is gone; the tab bar is the navigation.
|
||||
await app.getByTestId('tab-albums').click();
|
||||
await expect(app.getByTestId('main-content')).toHaveAttribute(
|
||||
'data-active-view',
|
||||
'albums',
|
||||
);
|
||||
|
||||
await expect.poll(() => cardCount(app)).toBeGreaterThan(1);
|
||||
|
||||
// Dispatched rather than clicked: the card lives in a virtualizer
|
||||
// inside a shadow root, and Enter expands the dropdown instead.
|
||||
await app.evaluate(() => {
|
||||
document
|
||||
.querySelector('cover-grid')
|
||||
?.shadowRoot?.querySelectorAll('.album-card')[1]
|
||||
?.dispatchEvent(
|
||||
new MouseEvent('click', { bubbles: true, composed: true }),
|
||||
);
|
||||
});
|
||||
|
||||
await expect(app.getByTestId('main-content')).toHaveAttribute(
|
||||
'data-active-view',
|
||||
'explore-album-details',
|
||||
);
|
||||
await expect(
|
||||
details(app).locator('[data-testid="album-play"]'),
|
||||
).toBeVisible();
|
||||
}
|
||||
|
||||
async function cardCount(app: Page): Promise<number> {
|
||||
return app.evaluate(
|
||||
() =>
|
||||
document
|
||||
.querySelector('cover-grid')
|
||||
?.shadowRoot?.querySelectorAll('.album-card').length ?? 0,
|
||||
);
|
||||
}
|
||||
@@ -79,6 +79,17 @@ export class ShortcutCapture extends LitElement {
|
||||
.reset-btn:hover {
|
||||
color: var(--yj-accent-text, #ffd43b);
|
||||
}
|
||||
/*
|
||||
* Reset is the only way to put a rebound shortcut back, so where
|
||||
* the device has no hover it is always visible rather than an
|
||||
* invisible button holding its hit area. The inverse of #68's
|
||||
* rule, which applies where the hover control is redundant.
|
||||
*/
|
||||
@media not all and (hover: hover) {
|
||||
.reset-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
private handleClick = () => {
|
||||
|
||||
@@ -892,6 +892,67 @@ export class ExploreAlbumDetails extends LitElement implements ContextMenuHost {
|
||||
.track-row .track-request {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── The phone (#66) ──
|
||||
*
|
||||
* **This block is last on purpose**, for index.css's
|
||||
* reason: a media query adds no specificity, so a rule
|
||||
* written above the plain one it overrides loses to it and
|
||||
* every declaration here is silently dead.
|
||||
*
|
||||
* Two faults, one shape. The page is a fixed header over a
|
||||
* scrolling tracklist — the desktop arrangement — so at the
|
||||
* reference device's 424x439 the header owned 253 of the
|
||||
* panel's 318px and the tracklist scrolled inside the 64px
|
||||
* that were left. And the header's flex row squeezed
|
||||
* .album-info to 112px, so the title drew as one ellipsised
|
||||
* glyph and two of the album's three primary actions were
|
||||
* clipped by the host's own overflow: Shuffle album ended
|
||||
* at x=443 in a 424px box, unreachable by any gesture.
|
||||
*
|
||||
* .album-info carries min-width: 0 and was shrinking as
|
||||
* asked, so another one is not the fix — the row has to
|
||||
* stack, or the info column has nothing to be wide with.
|
||||
*
|
||||
* The scroller moves to the host and .content stops being
|
||||
* one, which is what makes the header scroll away; the
|
||||
* tracklist is plain DOM rather than a virtualizer, so
|
||||
* nothing inside wants a scroll window of its own. */
|
||||
@media (max-width: 599px) {
|
||||
:host {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.album-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
/* Stacked, the art is the whole of the header's width
|
||||
* budget and its 200px square is 45% of the reference
|
||||
* device's height. It is still what identifies the
|
||||
* album, so it shrinks rather than going. */
|
||||
.cover-art-container {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
/* A column flex item takes its content's width from
|
||||
* align-items: flex-start above, which would leave the
|
||||
* actions wrapping inside a box narrower than the row
|
||||
* they now have to themselves. */
|
||||
.album-info {
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 0 0 auto;
|
||||
overflow-y: visible;
|
||||
padding: 16px 16px 24px;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
|
||||
@@ -639,23 +639,42 @@ export class QueuePanel
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/*
|
||||
* The per-row remove is a hover affordance, and on a device
|
||||
* without hover it is redundant rather than missing: the row's
|
||||
* context menu is a bottom sheet since #60 and carries "Remove
|
||||
* from Queue", so the action is one long-press away. An
|
||||
* always-visible X would instead spend part of a 424px row on
|
||||
* something already reachable. #68's treatment, for #68's reason.
|
||||
*
|
||||
* display:none outside the query rather than visibility:hidden:
|
||||
* a hidden button still occupies its hit area and is still in
|
||||
* the accessibility tree, so a phone would keep a target for a
|
||||
* control it can never see.
|
||||
*/
|
||||
.remove-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--yj-text-tertiary, #888);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
visibility: hidden;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.track-item:hover .remove-button {
|
||||
visibility: visible;
|
||||
}
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.remove-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--yj-text-tertiary, #888);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.remove-button:hover {
|
||||
color: var(--yj-error-text, #ff8787);
|
||||
.track-item:hover .remove-button {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.remove-button:hover {
|
||||
color: var(--yj-error-text, #ff8787);
|
||||
}
|
||||
}
|
||||
|
||||
.list-area.drag-over {
|
||||
|
||||
@@ -529,6 +529,21 @@ export class TrackDetails extends LitElement {
|
||||
background: var(--yj-error, #e03131);
|
||||
}
|
||||
|
||||
/*
|
||||
* Both are the *only* route to changing or removing a track's
|
||||
* cover art, so where the device has no hover they are always
|
||||
* visible rather than hidden — the inverse of #68's rule, which
|
||||
* applies where the hover control is redundant. Revealed by
|
||||
* opacity, so what is on screen is what the desktop reveal shows
|
||||
* and nothing about the layout moves.
|
||||
*/
|
||||
@media not all and (hover: hover) {
|
||||
.cover-art-overlay,
|
||||
.cover-art-remove {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Error message */
|
||||
.error-message {
|
||||
flex: 1;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/**
|
||||
* A hover affordance is gated on the device having hover.
|
||||
* A hover affordance is gated on the device having hover — in whichever
|
||||
* direction keeps the action reachable.
|
||||
*
|
||||
* The home page's cover cards reveal a play button on :hover. A touch
|
||||
* long-press synthesises a hover state in the WebView, so on a phone
|
||||
@@ -7,6 +8,15 @@
|
||||
* utils/long-press.ts is measuring for a context menu — a control
|
||||
* appearing because the user was reaching for a different one.
|
||||
*
|
||||
* #137 is the same sweep with the opposite answer for two of its three
|
||||
* cases. Where the revealed control is the *only* route to its action,
|
||||
* hiding it removes the action, so it is always visible where there is
|
||||
* no hover: `track-details`'s cover-art overlay and remove, and
|
||||
* `shortcut-capture`'s reset. The queue's per-row remove is the third,
|
||||
* and is the redundant kind — since #60 the row's context menu is a
|
||||
* bottom sheet carrying "Remove from Queue" — so it takes #68's
|
||||
* treatment here.
|
||||
*
|
||||
* This is asserted against the *parsed stylesheet* rather than by
|
||||
* emulating a touch device, and that is a limitation worth stating
|
||||
* rather than hiding. CDP's Emulation.setEmulatedMedia does not reach
|
||||
@@ -24,6 +34,9 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import '@components/home-view/home-view';
|
||||
import '@components/queue-panel/queue-panel';
|
||||
import '@components/track-details/track-details';
|
||||
import '@components/config-page/shortcut-capture';
|
||||
import { fixture } from '@test/support/render';
|
||||
|
||||
/** Every rule in the element's own adopted stylesheets, flattened. */
|
||||
@@ -83,3 +96,91 @@ describe('the home card play button', () => {
|
||||
expect(unconditional.some((r) => /display:\s*none/.test(r.text))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the queue row's remove button", () => {
|
||||
it('is absent where the device has no hover, the menu carrying the action', async () => {
|
||||
const el = await fixture('queue-panel', {});
|
||||
const rules = rulesOf(el);
|
||||
|
||||
expect(rules.length).toBeGreaterThan(0);
|
||||
|
||||
// visibility:hidden alone would leave an invisible button holding
|
||||
// its hit area on a phone, which is the trap #68's commit names.
|
||||
const unconditional = rules.filter(
|
||||
(r) => r.condition === null && r.text.startsWith('.remove-button'),
|
||||
);
|
||||
|
||||
expect(unconditional.length).toBeGreaterThan(0);
|
||||
expect(unconditional.some((r) => /display:\s*none/.test(r.text))).toBe(true);
|
||||
|
||||
const reveals = rules.filter(
|
||||
(r) =>
|
||||
r.text.includes('.remove-button') && /visibility:\s*visible/.test(r.text),
|
||||
);
|
||||
|
||||
expect(reveals.length).toBeGreaterThan(0);
|
||||
|
||||
for (const rule of reveals) {
|
||||
expect(rule.condition).toMatch(/hover:\s*hover/);
|
||||
expect(rule.condition).toMatch(/pointer:\s*fine/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* The two affordances that are the only route to their action.
|
||||
*
|
||||
* Asserted as "there is a rule showing it, and its condition is a
|
||||
* *negated* hover query" — the same stylesheet reading as above, for
|
||||
* the same reason: this tier's iframe cannot be emulated as a touch
|
||||
* device, and the regression worth catching is someone folding the rule
|
||||
* away as redundant on the desktop it does nothing on.
|
||||
*/
|
||||
describe('an affordance with no other route', () => {
|
||||
const cases: Array<[string, string, string[]]> = [
|
||||
['track-details', 'track-details', ['.cover-art-overlay', '.cover-art-remove']],
|
||||
['shortcut-capture', 'shortcut-capture', ['.reset-btn']],
|
||||
];
|
||||
|
||||
for (const [name, tag, selectors] of cases) {
|
||||
it(`${name} shows it where the device has no hover`, async () => {
|
||||
const el = await fixture(tag, {});
|
||||
const rules = rulesOf(el);
|
||||
|
||||
expect(rules.length).toBeGreaterThan(0);
|
||||
|
||||
for (const selector of selectors) {
|
||||
const shown = rules.filter(
|
||||
(r) =>
|
||||
r.condition !== null &&
|
||||
r.text.includes(selector) &&
|
||||
/opacity:\s*1/.test(r.text),
|
||||
);
|
||||
|
||||
const touch = shown.filter((r) => /not[\s\S]*hover:\s*hover/.test(r.condition!));
|
||||
|
||||
expect(touch.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// The one half this tier can measure rather than read: the query is
|
||||
// negated, so on the hover-capable browser running these tests the
|
||||
// control must still be revealed by hover and by nothing else. A rule
|
||||
// written without the `not` would show it here, permanently, on every
|
||||
// desktop.
|
||||
it('leaves the desktop reveal alone, where the device does have hover', async () => {
|
||||
expect(matchMedia('(hover: hover)').matches).toBe(true);
|
||||
|
||||
const el = await fixture('shortcut-capture', {
|
||||
action: 'player.next',
|
||||
label: 'Next Track',
|
||||
currentKey: 'X',
|
||||
defaultKey: 'N',
|
||||
});
|
||||
const btn = el.shadowRoot?.querySelector('.reset-btn');
|
||||
|
||||
expect(btn).not.toBeNull();
|
||||
expect(getComputedStyle(btn!).opacity).toBe('0');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user