From 7b09d467dd56f197df7d093a9211f30518a14f8a Mon Sep 17 00:00:00 2001 From: Logan Date: Fri, 21 Aug 2026 23:03:18 -0400 Subject: [PATCH] fix(ui): raise the last controls below the touch floor The rest of #186's second table, and one thing it could not have said. .section-toggle 187x15 autotag .folders-menu-trigger 32x18 autotag .back-button 32x32 artist-details Requests / Downloads tabs 85x34, 96x34 .search-mode-tab 89x26, 79x26 explore explore search input 325x18 in a 36px box **back-button was six controls, not one.** The issue names it in artist-details because that is the view the sweep opened; the same declaration is byte-identical in artist-details, genre-details, playlist-details, smart-playlist-details, explore-artist-details and explore-album-details, 32px in all six. So it is styles/back-button. css.ts now, adopted by each, and a source sweep fails on a seventh copy -- because the failure this invites is not a size changing, it is somebody adding a detail view and writing `.back-button` out again, which no device sweep would catch for the same reason this one did not. That is icon-language.test.ts's shape, and the argument for it here is the inverse of the column arrows': one declaration covering 36 controls is cheap to fix, and six declarations of one control are six chances to miss five. It is a real 44px box rather than padding with the width handed back: a detail header runs no fit pass, and this button has a visible background, so a hit area larger than the circle would be a control bigger than it looks. The size is #55's, reached there for the same reason -- "the way out is 44px on a phone". **The explore search box was two faults.** The row was 36px *and* the input inside it was 18, so eight pixels at each edge were not a target at all: a tap near the top of the box landed on the container and did nothing. The container is 44 and the input stretches to it. **The Downloads tabs take padding rather than a min-size**, because the mark for the selected tab is its bottom border -- a min-size centres the label and leaves the underline 10px beneath it. page-action-check-now (113x29) is in that table and is not here: it is a PageAction, so #195 raised it with the rest of the header's actions and touch-targets.test.ts already covers it. Verified on the device, sweeping each view the way the issue was filed: explore, downloads, autotag and artist-details now report **one** control under the floor apiece, and it is the skip link, which #186 already ruled out as keyboard-only. .search-mode-tab 89x44 and 79x44, the search input 325x44, the Downloads tabs 85x44 and 96x44, .section-toggle 174x44, .folders-menu-trigger 44x44, .back-button 44x44. All 12 new tests fail on main, the source sweep naming all six copies. make ui-test 1041 pass; make e2e 236 pass on chromium, which is half an answer -- CI has the other half. Closes #186 --- .../artist-details/artist-details.ts | 28 +-- .../components/autotag-view/autotag-view.ts | 16 +- .../downloads-view/downloads-view.ts | 10 +- .../explore-album-details.ts | 21 +- .../explore-artist-details.ts | 21 +- .../components/explore-view/explore-view.ts | 18 +- .../components/genre-details/genre-details.ts | 28 +-- .../playlist-details/playlist-details.ts | 27 +- .../smart-playlist-details.ts | 27 +- frontend/src/styles/back-button.css.ts | 54 ++++ .../components/detail-touch-targets.test.ts | 230 ++++++++++++++++++ 11 files changed, 337 insertions(+), 143 deletions(-) create mode 100644 frontend/src/styles/back-button.css.ts create mode 100644 frontend/test/components/detail-touch-targets.test.ts diff --git a/frontend/src/components/artist-details/artist-details.ts b/frontend/src/components/artist-details/artist-details.ts index f360224..12e82d2 100644 --- a/frontend/src/components/artist-details/artist-details.ts +++ b/frontend/src/components/artist-details/artist-details.ts @@ -14,6 +14,7 @@ import { import '@awesome.me/webawesome/dist/components/icon/icon.js'; import '@components/cover-grid/cover-grid.js'; import { designTokens } from '../../styles/tokens.css'; +import { backButton } from '../../styles/back-button.css'; @customElement('artist-details') export class ArtistDetails extends LitElement { @@ -40,7 +41,7 @@ export class ArtistDetails extends LitElement { /** Tracks the store's cached array reference to detect refreshes. */ private lastAlbumsRef: library.Album[] | null = null; - static override styles = [designTokens, css` + static override styles = [designTokens, backButton, css` :host { display: flex; flex-direction: column; @@ -66,31 +67,6 @@ export class ArtistDetails extends LitElement { ); } - .back-button { - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - border: none; - border-radius: 50%; - background: var( - --yj-bg-overlay, - rgba(255, 255, 255, 0.06) - ); - color: var(--yj-text-primary, #fff); - cursor: pointer; - flex-shrink: 0; - transition: background-color 0.15s ease; - } - - .back-button:hover { - background: var( - --yj-bg-hover, - rgba(255, 255, 255, 0.12) - ); - } - .back-button wa-icon { font-size: 16px; /* back button — outside type scale */ } diff --git a/frontend/src/components/autotag-view/autotag-view.ts b/frontend/src/components/autotag-view/autotag-view.ts index 56e2fc4..feed404 100644 --- a/frontend/src/components/autotag-view/autotag-view.ts +++ b/frontend/src/components/autotag-view/autotag-view.ts @@ -250,13 +250,20 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) { } /* Collapsible-section toggle used in the Pending header — - transparent button that inherits the header's type. */ + transparent button that inherits the header's type. + + 187x**15** before this (#186), which was the smallest + control measured anywhere in the app until the column + arrows were counted. It is transparent and full-width + already, so the floor costs it a height and nothing + else. */ .section-toggle { display: flex; align-items: center; gap: 0.35rem; flex: 1; min-width: 0; + min-block-size: 44px; padding: 0; background: transparent; border: 0; @@ -274,6 +281,8 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) { color: var(--yj-text-tertiary, #888); } + /* 32x18, and it has no background until hover -- so the + padding out to a square target is invisible (#186). */ .folders-menu-trigger { background: transparent; border: 0; @@ -281,6 +290,8 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) { font-size: 1.1rem; line-height: 1; padding: 0.1rem 0.4rem; + min-inline-size: 44px; + min-block-size: 44px; border-radius: 3px; cursor: pointer; } @@ -293,7 +304,10 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) { .folders-refresh-trigger { display: flex; align-items: center; + justify-content: center; font-size: 0.95rem; + min-inline-size: 44px; + min-block-size: 44px; } .folders-refresh-trigger:disabled { diff --git a/frontend/src/components/downloads-view/downloads-view.ts b/frontend/src/components/downloads-view/downloads-view.ts index 626e9a0..4f03288 100644 --- a/frontend/src/components/downloads-view/downloads-view.ts +++ b/frontend/src/components/downloads-view/downloads-view.ts @@ -93,8 +93,16 @@ export class DownloadsView extends ViewLifecycleMixin(LitElement) { border-bottom: 1px solid var(--yj-bg-overlay, rgba(255, 255, 255, 0.08)); } + /* 85x34 and 96x34 before this (#186). A tab is the only + route to the panel it names, so it is the last control + that should be hard to hit -- and the underline that + marks the active one is drawn on the bottom border, + which a taller box moves further from the label. That + is why the height goes on padding rather than on a + min-size: the border follows the padding down and the + label stays where the underline is. */ .tab { - padding: 8px 14px; + padding: 13px 14px; font-size: 13px; font-weight: 600; color: var(--yj-text-secondary, #b3b3b3); diff --git a/frontend/src/components/explore-album-details/explore-album-details.ts b/frontend/src/components/explore-album-details/explore-album-details.ts index e6e3758..15bd242 100644 --- a/frontend/src/components/explore-album-details/explore-album-details.ts +++ b/frontend/src/components/explore-album-details/explore-album-details.ts @@ -2,6 +2,7 @@ import { LitElement, html, css, nothing } from 'lit'; import { customElement, property, state, query } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; import { designTokens } from '../../styles/tokens.css'; +import { backButton } from '../../styles/back-button.css'; import { srOnly } from '../../styles/sr-only.css'; import { unownedLabel, unownedStyles } from '@utils/ownership'; import { @@ -355,6 +356,7 @@ export class ExploreAlbumDetails extends LitElement implements ContextMenuHost { static override styles = [ designTokens, + backButton, exploreLinkStyles, contextMenuStyles, srOnly, @@ -379,25 +381,6 @@ export class ExploreAlbumDetails extends LitElement implements ContextMenuHost { var(--yj-border-subtle, rgba(255, 255, 255, 0.06)); } - .back-button { - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - border: none; - border-radius: 50%; - background: var(--yj-bg-overlay, rgba(255, 255, 255, 0.06)); - color: var(--yj-text-primary, #fff); - cursor: pointer; - flex-shrink: 0; - transition: background-color 0.15s ease; - } - - .back-button:hover { - background: var(--yj-bg-hover, rgba(255, 255, 255, 0.12)); - } - .back-button wa-icon { font-size: 16px; } diff --git a/frontend/src/components/explore-artist-details/explore-artist-details.ts b/frontend/src/components/explore-artist-details/explore-artist-details.ts index 5c42394..b750f67 100644 --- a/frontend/src/components/explore-artist-details/explore-artist-details.ts +++ b/frontend/src/components/explore-artist-details/explore-artist-details.ts @@ -3,6 +3,7 @@ import { LitElement, html, css, nothing } from 'lit'; import { customElement, property, state, query } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; import { designTokens } from '../../styles/tokens.css'; +import { backButton } from '../../styles/back-button.css'; import { LookupArtist, BrowseReleaseGroups, @@ -266,6 +267,7 @@ export class ExploreArtistDetails extends LitElement implements ContextMenuHost static override styles = [ designTokens, + backButton, exploreLinkStyles, contextMenuStyles, unownedStyles, @@ -289,25 +291,6 @@ export class ExploreArtistDetails extends LitElement implements ContextMenuHost var(--yj-border-subtle, rgba(255, 255, 255, 0.06)); } - .back-button { - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - border: none; - border-radius: 50%; - background: var(--yj-bg-overlay, rgba(255, 255, 255, 0.06)); - color: var(--yj-text-primary, #fff); - cursor: pointer; - flex-shrink: 0; - transition: background-color 0.15s ease; - } - - .back-button:hover { - background: var(--yj-bg-hover, rgba(255, 255, 255, 0.12)); - } - .back-button wa-icon { font-size: 16px; } diff --git a/frontend/src/components/explore-view/explore-view.ts b/frontend/src/components/explore-view/explore-view.ts index a7bc195..dace829 100644 --- a/frontend/src/components/explore-view/explore-view.ts +++ b/frontend/src/components/explore-view/explore-view.ts @@ -256,15 +256,18 @@ export class ExploreView extends ViewLifecycleMixin(LitElement) implements Conte margin-bottom: 10px; } + /* 89x26 and 79x26 before this (#186). */ .search-mode-tab { display: inline-flex; align-items: center; + justify-content: center; gap: 6px; background: none; border: 1px solid transparent; border-radius: 6px; color: var(--yj-text-tertiary, #888); cursor: pointer; + min-block-size: 44px; padding: 5px 12px; font-size: var(--yj-text-sm); font-family: inherit; @@ -289,7 +292,7 @@ export class ExploreView extends ViewLifecycleMixin(LitElement) implements Conte border-radius: 6px; padding: 0 12px; gap: 8px; - height: 36px; + min-height: 44px; max-width: 520px; transition: border-color 0.15s ease; } @@ -364,8 +367,15 @@ export class ExploreView extends ViewLifecycleMixin(LitElement) implements Conte flex-shrink: 0; } + /* The input measured 325x**18** and the box around it 36, + which is two faults rather than one (#186): the row was + under the floor, and the input did not fill it, so eight + of those pixels were not a target at all. The container + is 44 and the input stretches to it -- a tap anywhere in + the box now lands on the input rather than beside it. */ input { flex: 1; + align-self: stretch; background: none; border: none; outline: none; @@ -379,15 +389,21 @@ export class ExploreView extends ViewLifecycleMixin(LitElement) implements Conte color: var(--yj-text-tertiary, #888); } + /* No background until hover, so the target grows and the + glyph does not. It is inside a 44px box already, hence + the width alone. */ .clear-button { display: flex; align-items: center; justify-content: center; + align-self: stretch; background: none; border: none; color: var(--yj-text-tertiary, #888); cursor: pointer; padding: 0; + min-inline-size: 44px; + margin-inline-end: -12px; font-size: var(--yj-text-sm); flex-shrink: 0; } diff --git a/frontend/src/components/genre-details/genre-details.ts b/frontend/src/components/genre-details/genre-details.ts index 389be91..23322fe 100644 --- a/frontend/src/components/genre-details/genre-details.ts +++ b/frontend/src/components/genre-details/genre-details.ts @@ -15,6 +15,7 @@ import { describeError } from '@utils/describe-error'; import '@awesome.me/webawesome/dist/components/icon/icon.js'; import '@components/track-list/track-list.js'; import { designTokens } from '../../styles/tokens.css'; +import { backButton } from '../../styles/back-button.css'; import { list } from '@utils/binding'; @customElement('genre-details') @@ -37,7 +38,7 @@ export class GenreDetails extends LitElement { private scanCompleteCleanup: (() => void) | null = null; - static override styles = [designTokens, css` + static override styles = [designTokens, backButton, css` :host { display: flex; flex-direction: column; @@ -77,31 +78,6 @@ export class GenreDetails extends LitElement { ); } - .back-button { - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - border: none; - border-radius: 50%; - background: var( - --yj-bg-overlay, - rgba(255, 255, 255, 0.06) - ); - color: var(--yj-text-primary, #fff); - cursor: pointer; - flex-shrink: 0; - transition: background-color 0.15s ease; - } - - .back-button:hover { - background: var( - --yj-bg-hover, - rgba(255, 255, 255, 0.12) - ); - } - .back-button wa-icon { font-size: 16px; /* back button — outside type scale */ } diff --git a/frontend/src/components/playlist-details/playlist-details.ts b/frontend/src/components/playlist-details/playlist-details.ts index 9db18c4..80c222c 100644 --- a/frontend/src/components/playlist-details/playlist-details.ts +++ b/frontend/src/components/playlist-details/playlist-details.ts @@ -71,6 +71,7 @@ import { exploreLinkStyles, } from '@utils/explore-link'; import { designTokens } from '../../styles/tokens.css'; +import { backButton } from '../../styles/back-button.css'; import { list } from '@utils/binding'; import { ICON_PLAYLIST, @@ -954,6 +955,7 @@ export class PlaylistDetails static override styles = [ designTokens, + backButton, contextMenuStyles, exploreLinkStyles, css` @@ -981,31 +983,6 @@ export class PlaylistDetails ); } - .back-button { - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - border: none; - border-radius: 50%; - background: var( - --yj-bg-overlay, - rgba(255, 255, 255, 0.06) - ); - color: var(--yj-text-primary, #fff); - cursor: pointer; - flex-shrink: 0; - transition: background-color 0.15s ease; - } - - .back-button:hover { - background: var( - --yj-bg-hover, - rgba(255, 255, 255, 0.12) - ); - } - .back-button wa-icon { font-size: 16px; } diff --git a/frontend/src/components/smart-playlist-details/smart-playlist-details.ts b/frontend/src/components/smart-playlist-details/smart-playlist-details.ts index 4b9b112..a97f428 100644 --- a/frontend/src/components/smart-playlist-details/smart-playlist-details.ts +++ b/frontend/src/components/smart-playlist-details/smart-playlist-details.ts @@ -62,6 +62,7 @@ import { } from '@utils/explore-link'; import '@components/smart-playlist-editor/smart-playlist-editor.js'; import { designTokens } from '../../styles/tokens.css'; +import { backButton } from '../../styles/back-button.css'; import { list } from '@utils/binding'; import { ICON_PLAYLIST, @@ -242,6 +243,7 @@ export class SmartPlaylistDetails static override styles = [ designTokens, + backButton, contextMenuStyles, exploreLinkStyles, css` @@ -269,31 +271,6 @@ export class SmartPlaylistDetails ); } - .back-button { - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - border: none; - border-radius: 50%; - background: var( - --yj-bg-overlay, - rgba(255, 255, 255, 0.06) - ); - color: var(--yj-text-primary, #fff); - cursor: pointer; - flex-shrink: 0; - transition: background-color 0.15s ease; - } - - .back-button:hover { - background: var( - --yj-bg-hover, - rgba(255, 255, 255, 0.12) - ); - } - .back-button wa-icon { font-size: 16px; } diff --git a/frontend/src/styles/back-button.css.ts b/frontend/src/styles/back-button.css.ts new file mode 100644 index 0000000..73c11b0 --- /dev/null +++ b/frontend/src/styles/back-button.css.ts @@ -0,0 +1,54 @@ +import { css } from 'lit'; + +/** + * The way out of a detail view, at the app's 44px touch floor. + * + * #186's second table names `artist-details`' back button at + * **32x32**. It is the same declaration in **six** components — + * `artist-details`, `genre-details`, `playlist-details`, + * `smart-playlist-details`, `explore-artist-details` and + * `explore-album-details` — byte-identical, 32px in all six, and the + * sweep that filed the issue visited one of them. + * + * That is the argument for this file rather than six edits. A device + * sweep walks the views somebody thought to open, so six copies of a + * control is six chances for the next pass to miss five; the arrows + * and the toggles were each one declaration covering 36 and 29 + * controls, and this is the same shape stated the other way round. + * + * **It is a real 44px box, not padding with the width handed back.** + * The header pass had to grow a hit area past its own layout box + * because `page-header` measures itself for #69's overflow fit; a + * detail view's header does not, so the control can simply be the + * target. It also *should* be — this button has a visible background, + * so a hit area larger than the circle would be a control that is + * bigger than it looks, which is the thing #187 accepts only where a + * thin painted track is the point. + * + * The size is #55's, arrived at for the same reason one component + * over: "the way out is 44px on a phone", when the queue panel's close + * button was 25x21 and, at phone width, the only pointer route off a + * full-screen surface. A detail view has the platform's back gesture + * as well, so this is less severe than the queue was — it is the same + * control wearing the same mistake. + */ +export const backButton = css` + .back-button { + display: flex; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + border: none; + border-radius: 50%; + background: var(--yj-bg-overlay, rgba(255, 255, 255, 0.06)); + color: var(--yj-text-primary, #fff); + cursor: pointer; + flex-shrink: 0; + transition: background-color 0.15s ease; + } + + .back-button:hover { + background: var(--yj-bg-hover, rgba(255, 255, 255, 0.12)); + } +`; diff --git a/frontend/test/components/detail-touch-targets.test.ts b/frontend/test/components/detail-touch-targets.test.ts new file mode 100644 index 0000000..9cc5120 --- /dev/null +++ b/frontend/test/components/detail-touch-targets.test.ts @@ -0,0 +1,230 @@ +/** + * The controls #186's second table found, outside Settings. + * + * Six one-off controls across four surfaces, and the reason they are a + * test rather than six stylesheet edits is `back-button`. The issue + * names it in `artist-details` at **32x32**; it is the same + * declaration, byte-identical, in *six* components — because a device + * sweep walks the views somebody thought to open, and five of them + * were not opened. + * + * So the assertion is over the whole set rather than over the one that + * was measured. That is `icon-language.test.ts`'s shape and it is here + * for the same reason: checking one call site checks one call site. + * + * | control | before | where | + * |---|---|---| + * | `.folders-menu-trigger` | **32x18** | autotag | + * | `.section-toggle` | 187x**15** | autotag | + * | `.back-button` | 32x32 | six detail views | + * | Requests / Downloads tabs | 85x**34**, 96x**34** | downloads | + * | `.search-mode-tab` | 89x**26**, 79x**26** | explore | + * | explore search input | 325x**18** in a 36px box | explore | + * + * `page-action-check-now` (113x29) is in that table and is **not** + * here: it is a `PageAction`, so #195 raised it with the rest of the + * page header's actions, and `touch-targets.test.ts` already covers + * it. Re-asserting it here would be a second statement of one rule. + */ +import { beforeEach, describe, expect, it } from 'vitest'; + +import '@components/artist-details/artist-details'; +import '@components/autotag-view/autotag-view'; +import '@components/downloads-view/downloads-view'; +import '@components/explore-album-details/explore-album-details'; +import '@components/explore-artist-details/explore-artist-details'; +import '@components/explore-view/explore-view'; +import '@components/genre-details/genre-details'; +import '@components/playlist-details/playlist-details'; +import '@components/smart-playlist-details/smart-playlist-details'; + +import { flush, stub } from '@test/support/harness'; +import { fixture, shadow, shadowAll } from '@test/support/render'; + +/** The app's touch floor, from #56. */ +const FLOOR = 44; + +/** + * Every component that draws a back button. + * + * The list is here rather than derived because deriving it means + * reading the source, and this tier renders instead — but it is + * checked against the source by `the back button is one declaration` + * below, so a seventh view cannot join quietly. + */ +const BACK_BUTTON_VIEWS = [ + 'artist-details', + 'genre-details', + 'playlist-details', + 'smart-playlist-details', + 'explore-artist-details', + 'explore-album-details', +] as const; + +function boxOf(el: Element | null | undefined): { w: number; h: number } { + if (!el) return { w: 0, h: 0 }; + + const box = el.getBoundingClientRect(); + + return { w: Math.round(box.width), h: Math.round(box.height) }; +} + +describe('the way out of a detail view', () => { + beforeEach(() => { + for (const path of [ + 'library.Library.GetTracks', + 'library.Library.GetAlbums', + 'library.Library.GetArtists', + 'library.Library.GetGenres', + 'playlist.Service.GetAllPlaylists', + 'playlist.Service.GetAllPlaylistsWithTracks', + ]) { + stub(path, []); + } + }); + + it.each(BACK_BUTTON_VIEWS)('is 44px in <%s>', async (tag) => { + // #55 settled this one component over, when the queue panel's + // close button was 25x21 and, at phone width, the only pointer + // route off a full-screen surface: "the way out is 44px". A detail + // view has the platform's back gesture as well, so it is less + // severe -- and it is the same control wearing the same mistake. + const el = await fixture(tag); + + await flush(); + + const back = shadow(el, '.back-button'); + + expect(back, `${tag} draws a back button`).toBeTruthy(); + expect(boxOf(back)).toEqual({ w: FLOOR, h: FLOOR }); + }); + + it('is one declaration, so a seventh view cannot miss it', async () => { + // The regression this exists for is not a size changing -- it is + // somebody adding a detail view and writing `.back-button` out + // again at 32px, which is exactly how there came to be six copies. + // A sweep of the running app would not catch it either, because a + // sweep visits the views you think to open. + const sources = import.meta.glob('../../src/components/**/*.ts', { + query: '?raw', + import: 'default', + eager: true, + }) as Record; + + expect(Object.keys(sources).length, 'the glob read something').toBeGreaterThan(0); + + const redeclared = Object.entries(sources) + .filter(([, src]) => /^\s*\.back-button\s*(?::[a-z-]+\s*)?\{/m.test(src)) + .map(([path]) => path); + + expect(redeclared).toEqual([]); + }); +}); + +describe('autotag', () => { + it('raises the two smallest controls the sweep found', async () => { + // 187x15 and 32x18. The section toggle was the smallest control + // measured anywhere in the app until the column arrows were + // counted, and autotag is off by default (#25), which is + // presumably why nobody had met either. + const el = await fixture('autotag-view'); + + await flush(); + + for (const selector of ['.section-toggle', '.folders-menu-trigger']) { + const control = shadowAll(el, selector).find( + (c) => c.getBoundingClientRect().height > 0, + ); + + if (!control) continue; + + expect(boxOf(control).h, `${selector} height`).toBeGreaterThanOrEqual(FLOOR); + } + + // The stylesheet is the assertion for whichever of the two this + // fixture does not render -- both are behind state a bare mount + // does not reach, and a test that silently checked nothing is the + // trap icon-language.test.ts's first assertion exists for. + const sheet = (el.constructor as typeof HTMLElement & { styles?: unknown }) + .styles; + + expect(String(sheet)).toContain('min-block-size: 44px'); + }); +}); + +describe('the Downloads tabs', () => { + beforeEach(() => { + stub('download.Service.ListDownloads', []); + stub('download.Service.ListRequests', []); + stub('download.Service.ListProviders', []); + }); + + it('are the only route to their panels, and are 44px', async () => { + const el = await fixture('downloads-view'); + + await flush(); + + const tabs = shadowAll(el, '[role="tab"]'); + + expect(tabs).toHaveLength(2); + + for (const tab of tabs) { + expect(boxOf(tab).h, tab.textContent?.trim()).toBeGreaterThanOrEqual(FLOOR); + } + }); + + it('keeps the active underline against the label', async () => { + // The height is padding rather than a min-size, because the mark + // for the selected tab is the bottom border -- a min-size would + // centre the label and leave the underline 10px below it. + const el = await fixture('downloads-view'); + + await flush(); + + const tab = shadowAll(el, '[role="tab"]')[0]!; + const style = getComputedStyle(tab); + + expect(parseFloat(style.paddingBlockStart)).toBeGreaterThan(8); + expect(style.paddingBlockStart).toBe(style.paddingBlockEnd); + }); +}); + +describe("Explore's own search row", () => { + beforeEach(() => { + stub('explore.Service.GetShelves', { State: 'ready', Shelves: [] }); + stub('explore.Service.GetIndexStatus', {}); + }); + + it('raises the mode tabs', async () => { + const el = await fixture('explore-view'); + + await flush(); + + const tabs = shadowAll(el, '.search-mode-tab'); + + expect(tabs.length).toBeGreaterThan(0); + + for (const tab of tabs) { + expect(boxOf(tab).h, tab.textContent?.trim()).toBeGreaterThanOrEqual(FLOOR); + } + }); + + it('makes the whole search box the input, not the middle 18px of it', async () => { + // Two faults, not one: the row was 36px and the input inside it + // was **18**, so half the box was not a target at all -- a tap + // near the top or bottom edge landed on the container and did + // nothing. The container is 44 and the input stretches to fill it. + const el = await fixture('explore-view'); + + await flush(); + + const box = shadow(el, '.search-container'); + const input = shadow(el, '.search-container input'); + + expect(box, 'the search row renders').toBeTruthy(); + expect(input, 'it holds an input').toBeTruthy(); + + expect(boxOf(box).h).toBeGreaterThanOrEqual(FLOOR); + expect(boxOf(input).h).toBeGreaterThanOrEqual(FLOOR); + }); +});