feat(accounts): add precise service filtering

Add an explicit service selector to the account creation dialog while keeping the text field focused on access types such as API and web login. Preserve the result area dimensions during filtering and allow Enter to choose the first visible account type. Cover filtering, keyboard selection, translations, and stable list geometry with focused regression tests.
This commit is contained in:
Sucukdeluxe
2026-08-15 18:46:18 +02:00
parent a3b4b2323f
commit 5747a50932
7 changed files with 147 additions and 56 deletions
+12 -28
View File
@@ -40,8 +40,7 @@ import {
} from "../shared/provider-daily-limits"; } from "../shared/provider-daily-limits";
import { preservePackageOrderForDisplay, sortPackageOrderByName } from "./package-order"; import { preservePackageOrderForDisplay, sortPackageOrderByName } from "./package-order";
import { pruneSelection, releaseAccountSelectionFocus, resolveEscapeSelectionScope, shouldClearDownloadSelection } from "./selection"; import { pruneSelection, releaseAccountSelectionFocus, resolveEscapeSelectionScope, shouldClearDownloadSelection } from "./selection";
import { buildConfiguredProviderOrder, buildScopedAccountEnabledState, getAccountDialogSelectableOptions, matchesAccountModeFilter, pruneAccountRowSelections, resolveAccountStatusState, resolveAccountUsername, resolveVisibleAccountKind, runOptimisticAccountUpdate, updateAccountRowSelection } from "./account-ui"; import { buildConfiguredProviderOrder, buildScopedAccountEnabledState, filterAccountDialogOptions, getAccountDialogSelectableOptions, pruneAccountRowSelections, resolveAccountStatusState, resolveAccountUsername, resolveVisibleAccountKind, runOptimisticAccountUpdate, updateAccountRowSelection } from "./account-ui";
import type { AccountModeFilter } from "./account-ui";
import { buildAccountDeleteCommand, buildAccountReplaceCommand, buildAccountSecretRequest, createAccountEditState, validateAccountEdit } from "./account-edit"; import { buildAccountDeleteCommand, buildAccountReplaceCommand, buildAccountSecretRequest, createAccountEditState, validateAccountEdit } from "./account-edit";
import type { AccountEditState, AccountEditTarget, AccountKind, AccountService, SingleAccountKind } from "./account-edit"; import type { AccountEditState, AccountEditTarget, AccountKind, AccountService, SingleAccountKind } from "./account-edit";
import { ACCOUNT_SERVICE_ICONS } from "./account-service-icons"; import { ACCOUNT_SERVICE_ICONS } from "./account-service-icons";
@@ -1614,7 +1613,7 @@ export function App(): ReactElement {
const [accountEditSecretVisible, setAccountEditSecretVisible] = useState<Record<string, boolean>>({}); const [accountEditSecretVisible, setAccountEditSecretVisible] = useState<Record<string, boolean>>({});
const [accountEditSecretBusy, setAccountEditSecretBusy] = useState<string | null>(null); const [accountEditSecretBusy, setAccountEditSecretBusy] = useState<string | null>(null);
const [accountDialogSearch, setAccountDialogSearch] = useState(""); const [accountDialogSearch, setAccountDialogSearch] = useState("");
const [accountDialogModeFilter, setAccountDialogModeFilter] = useState<AccountModeFilter>("all"); const [accountDialogServiceFilter, setAccountDialogServiceFilter] = useState("all");
const [keyStatsPopup, setKeyStatsPopup] = useState<string | null>(null); const [keyStatsPopup, setKeyStatsPopup] = useState<string | null>(null);
const [debridLinkHostLimits, setDebridLinkHostLimits] = useState<Record<string, DebridLinkHostLimitInfo>>({}); const [debridLinkHostLimits, setDebridLinkHostLimits] = useState<Record<string, DebridLinkHostLimitInfo>>({});
const [debridLinkHostLimitsLoading, setDebridLinkHostLimitsLoading] = useState(false); const [debridLinkHostLimitsLoading, setDebridLinkHostLimitsLoading] = useState(false);
@@ -2432,28 +2431,12 @@ export function App(): ReactElement {
accountDialog.service accountDialog.service
); );
}, [accountDialog, availableAccountOptions]); }, [accountDialog, availableAccountOptions]);
const accountDialogSearchQuery = accountDialogSearch.trim().toLowerCase(); const accountDialogServiceFilters = useMemo(() => (
[...new Set(accountDialogSelectableOptions.map((option) => option.serviceLabel))]
), [accountDialogSelectableOptions]);
const filteredAccountDialogOptions = useMemo(() => ( const filteredAccountDialogOptions = useMemo(() => (
accountDialogSelectableOptions.filter((option) => { filterAccountDialogOptions(accountDialogSelectableOptions, accountDialogSearch, accountDialogServiceFilter)
const matchesMode = accountDialogModeFilter === "all" ), [accountDialogSearch, accountDialogSelectableOptions, accountDialogServiceFilter]);
|| (accountDialogModeFilter === "api" && option.modeLabel === "API")
|| (accountDialogModeFilter === "web" && (option.modeLabel.startsWith("Web") || option.kind === "ddownload-login" || option.kind === "linksnappy-login"));
if (!matchesMode) {
return false;
}
if (!accountDialogSearchQuery) {
return true;
}
const haystack = [
option.title,
option.serviceLabel,
option.modeLabel,
option.pickerDescription,
getAccountPickerFunctionLabel(option)
].join(" ").toLowerCase();
return haystack.includes(accountDialogSearchQuery);
})
), [accountDialogModeFilter, accountDialogSearchQuery, accountDialogSelectableOptions]);
const handleUpdateResult = async ( const handleUpdateResult = async (
result: UpdateCheckResult, result: UpdateCheckResult,
source: "manual" | "startup", source: "manual" | "startup",
@@ -2686,7 +2669,7 @@ export function App(): ReactElement {
const openCreateAccountDialog = (): void => { const openCreateAccountDialog = (): void => {
setAccountDialogSearch(""); setAccountDialogSearch("");
setAccountDialogModeFilter("all"); setAccountDialogServiceFilter("all");
setAccountDialog(createAccountDialogState("create", availableAccountOptions[0]?.kind ?? null, settingsDraft)); setAccountDialog(createAccountDialogState("create", availableAccountOptions[0]?.kind ?? null, settingsDraft));
}; };
@@ -2725,7 +2708,7 @@ export function App(): ReactElement {
const closeAccountDialog = useCallback((): void => { const closeAccountDialog = useCallback((): void => {
setAccountDialog(null); setAccountDialog(null);
setAccountDialogSearch(""); setAccountDialogSearch("");
setAccountDialogModeFilter("all"); setAccountDialogServiceFilter("all");
}, []); }, []);
const closeAccountEditDialog = useCallback((): void => { const closeAccountEditDialog = useCallback((): void => {
@@ -5186,7 +5169,7 @@ export function App(): ReactElement {
<AccountAddDialog <AccountAddDialog
actions={{ actions={{
onQueryChange: setAccountDialogSearch, onQueryChange: setAccountDialogSearch,
onFilterChange: setAccountDialogModeFilter, onServiceFilterChange: setAccountDialogServiceFilter,
onOptionSelect: (optionId) => updateAccountDialogKind(optionId as AccountKind), onOptionSelect: (optionId) => updateAccountDialogKind(optionId as AccountKind),
onFieldChange: (fieldId, value) => setAccountDialog((current) => current ? { ...current, [fieldId]: value } : current), onFieldChange: (fieldId, value) => setAccountDialog((current) => current ? { ...current, [fieldId]: value } : current),
onClose: closeAccountDialog, onClose: closeAccountDialog,
@@ -5198,7 +5181,8 @@ export function App(): ReactElement {
model={{ model={{
open: Boolean(accountDialog), open: Boolean(accountDialog),
query: accountDialogSearch, query: accountDialogSearch,
filter: accountDialogModeFilter, serviceFilter: accountDialogServiceFilter,
serviceFilters: accountDialogServiceFilters,
options: accountAddOptions, options: accountAddOptions,
selectedOptionId: accountDialog?.kind ?? null, selectedOptionId: accountDialog?.kind ?? null,
fields: accountAddFields, fields: accountAddFields,
+21
View File
@@ -16,6 +16,27 @@ export function matchesAccountModeFilter(option: AccountModeOption, filter: Acco
return option.modeLabel.startsWith("Web"); return option.modeLabel.startsWith("Web");
} }
export function filterAccountDialogOptions<T extends {
serviceLabel: string;
title: string;
modeLabel: string;
pickerDescription: string;
}>(options: readonly T[], query: string, serviceFilter: string): T[] {
const normalizedQuery = query.trim().toLocaleLowerCase("de-DE");
return options.filter((option) => {
if (serviceFilter !== "all" && option.serviceLabel !== serviceFilter) {
return false;
}
if (!normalizedQuery) {
return true;
}
return [option.title, option.modeLabel, option.pickerDescription]
.join(" ")
.toLocaleLowerCase("de-DE")
.includes(normalizedQuery);
});
}
export function buildConfiguredProviderOrder( export function buildConfiguredProviderOrder(
currentOrder: readonly DebridProvider[], currentOrder: readonly DebridProvider[],
configuredProviders: readonly DebridProvider[] configuredProviders: readonly DebridProvider[]
+1 -1
View File
@@ -75,7 +75,7 @@ const pairs = [
["Füge einen Account hinzu, um Downloads über einen Anbieter zu starten.", "Add an account to start downloads through a provider."], ["Noch keine Accounts", "No accounts yet"], ["Keine Provider konfiguriert.", "No providers configured."], ["Füge einen Account hinzu, um Downloads über einen Anbieter zu starten.", "Add an account to start downloads through a provider."], ["Noch keine Accounts", "No accounts yet"], ["Keine Provider konfiguriert.", "No providers configured."],
["Keine eigenen Zuordnungen.", "No custom assignments."], ["Hoster-Routing hinzufügen", "Add hoster routing"], ["Hoster hinzufügen…", "Add hoster…"], ["Eigener Hoster…", "Custom hoster…"], ["Noch keine Rotations-Ereignisse.", "No rotation events yet."], ["Keine eigenen Zuordnungen.", "No custom assignments."], ["Hoster-Routing hinzufügen", "Add hoster routing"], ["Hoster hinzufügen…", "Add hoster…"], ["Eigener Hoster…", "Custom hoster…"], ["Noch keine Rotations-Ereignisse.", "No rotation events yet."],
["Prüfen und speichern", "Check and save"], ["Wähle einen Dienst und trage die passenden Zugangsdaten ein.", "Choose a service and enter the matching credentials."], ["Accounts durchsuchen", "Search accounts"], ["Prüfen und speichern", "Check and save"], ["Wähle einen Dienst und trage die passenden Zugangsdaten ein.", "Choose a service and enter the matching credentials."], ["Accounts durchsuchen", "Search accounts"],
["Dienst / Zugangstyp", "Service / access type"], ["Dienst", "Service"], ["Typ/Funktion", "Type/function"], ["Dienst oder Zugangstyp suchen", "Search service or access type"], ["Account-Typ filtern", "Filter account type"], ["Verfügbare Account-Typen", "Available account types"], ["Keine passenden Account-Typen.", "No matching account types."], ["Dienst / Zugangstyp", "Service / access type"], ["Dienst", "Service"], ["Typ/Funktion", "Type/function"], ["Dienst oder Zugangstyp suchen", "Search service or access type"], ["Zugangstyp suchen, z. B. API oder Web", "Search access type, e.g. API or web"], ["Dienst filtern", "Filter service"], ["Alle Dienste", "All services"], ["Account-Typ filtern", "Filter account type"], ["Verfügbare Account-Typen", "Available account types"], ["Keine passenden Account-Typen.", "No matching account types."],
["Prüfen", "Check"], ["Bearbeite ausschließlich den ausgewählten Account.", "Edit only the selected account."], ["Account bearbeiten", "Edit account"], ["Account aktiviert", "Account enabled"], ["Prüfen", "Check"], ["Bearbeite ausschließlich den ausgewählten Account.", "Edit only the selected account."], ["Account bearbeiten", "Edit account"], ["Account aktiviert", "Account enabled"],
["Immer erste Tonspur", "Always first audio track"], ["Pro Download", "Per download"], ["Keine Archive löschen", "Do not delete archives"], ["Archive in Papierkorb", "Move archives to recycle bin"], ["Archive löschen", "Delete archives"], ["Immer erste Tonspur", "Always first audio track"], ["Pro Download", "Per download"], ["Keine Archive löschen", "Do not delete archives"], ["Archive in Papierkorb", "Move archives to recycle bin"], ["Archive löschen", "Delete archives"],
["Accounts und Verwendungsregeln.", "Accounts and usage rules."], ["Premium Account", "Premium account"], ["Zugang ungültig", "Invalid access"], ["Prüft…", "Checking…"], ["Geschützter Zugang", "Protected access"], ["Accounts und Verwendungsregeln.", "Accounts and usage rules."], ["Premium Account", "Premium account"], ["Zugang ungültig", "Invalid access"], ["Prüft…", "Checking…"], ["Geschützter Zugang", "Protected access"],
@@ -23,7 +23,6 @@ import {
getAccountTableMinWidth, getAccountTableMinWidth,
getSettingsSelectNavigationIndex, getSettingsSelectNavigationIndex,
resizeAccountTableColumn, resizeAccountTableColumn,
type AccountAddFilter,
type AccountAddOption, type AccountAddOption,
type AccountRowViewModel, type AccountRowViewModel,
type AccountTableColumnId, type AccountTableColumnId,
@@ -176,10 +175,11 @@ export interface AccountDialogField {
secretBusy?: boolean; secretBusy?: boolean;
} }
export interface AccountAddDialogModel { export interface AccountAddDialogModel {
open: boolean; open: boolean;
query: string; query: string;
filter: AccountAddFilter; serviceFilter: string;
serviceFilters: readonly string[];
options: readonly AccountAddOption[]; options: readonly AccountAddOption[];
selectedOptionId: string | null; selectedOptionId: string | null;
fields: readonly AccountDialogField[]; fields: readonly AccountDialogField[];
@@ -187,9 +187,9 @@ export interface AccountAddDialogModel {
busy: boolean; busy: boolean;
} }
export interface AccountAddDialogActions { export interface AccountAddDialogActions {
onQueryChange: (value: string) => void; onQueryChange: (value: string) => void;
onFilterChange: (filter: AccountAddFilter) => void; onServiceFilterChange: (service: string) => void;
onOptionSelect: (optionId: string) => void; onOptionSelect: (optionId: string) => void;
onFieldChange: (fieldId: string, value: string) => void; onFieldChange: (fieldId: string, value: string) => void;
onClose: () => void; onClose: () => void;
@@ -667,16 +667,34 @@ export function AccountAddDialog({
> >
<div className="settings-account-picker-selector"> <div className="settings-account-picker-selector">
<span>Dienst / Zugangstyp</span> <span>Dienst / Zugangstyp</span>
<input <div className="settings-account-picker-controls">
aria-controls={model.options.length === 0 ? "settings-account-picker-empty" : "settings-account-picker-results"} <input
aria-describedby={model.options.length === 0 ? "settings-account-picker-empty" : undefined} aria-controls={model.options.length === 0 ? "settings-account-picker-empty" : "settings-account-picker-results"}
aria-label="Dienst oder Zugangstyp suchen" aria-describedby={model.options.length === 0 ? "settings-account-picker-empty" : undefined}
className="settings-control" aria-label="Dienst oder Zugangstyp suchen"
onChange={(event) => actions.onQueryChange(event.target.value)} className="settings-control"
placeholder="Dienst oder Zugangstyp suchen" onChange={(event) => actions.onQueryChange(event.target.value)}
type="search" onKeyDown={(event) => {
value={model.query} if (event.key !== "Enter" || !model.options[0]) {
/> return;
}
event.preventDefault();
actions.onOptionSelect(model.options[0].id);
}}
placeholder="Zugangstyp suchen, z. B. API oder Web"
type="search"
value={model.query}
/>
<select
aria-label="Dienst filtern"
className="settings-control settings-account-picker-service-filter"
onChange={(event) => actions.onServiceFilterChange(event.target.value)}
value={model.serviceFilter}
>
<option value="all">Alle Dienste</option>
{model.serviceFilters.map((service) => <option key={service} value={service}>{service}</option>)}
</select>
</div>
</div> </div>
<div className="settings-account-picker-table"> <div className="settings-account-picker-table">
<div aria-hidden="true" className="settings-account-picker-header"> <div aria-hidden="true" className="settings-account-picker-header">
+14 -2
View File
@@ -936,6 +936,16 @@
line-height: 18px; line-height: 18px;
} }
.settings-account-picker-controls {
display: grid;
grid-template-columns: minmax(0, 1fr) 190px;
gap: 8px;
}
.settings-account-picker-service-filter {
cursor: pointer;
}
.settings-account-picker-table { .settings-account-picker-table {
overflow: hidden; overflow: hidden;
border: 1px solid var(--ui-border); border: 1px solid var(--ui-border);
@@ -968,13 +978,14 @@
} }
.settings-account-picker-list { .settings-account-picker-list {
max-height: 190px; height: 190px;
overflow-y: auto; overflow-y: auto;
scrollbar-gutter: stable;
} }
.settings-account-picker-empty { .settings-account-picker-empty {
display: grid; display: grid;
min-height: 72px; height: 190px;
padding: 16px; padding: 16px;
place-items: center; place-items: center;
color: var(--ui-text-muted); color: var(--ui-text-muted);
@@ -1159,6 +1170,7 @@
} }
.settings-theme-options, .settings-theme-options,
.settings-account-picker-controls,
.settings-account-picker-header, .settings-account-picker-header,
.settings-account-picker-row { .settings-account-picker-row {
grid-template-columns: minmax(0, 1fr); grid-template-columns: minmax(0, 1fr);
+14
View File
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { import {
buildConfiguredProviderOrder, buildConfiguredProviderOrder,
filterAccountDialogOptions,
getAccountDialogSelectableOptions, getAccountDialogSelectableOptions,
isAccountRowSelectionKey, isAccountRowSelectionKey,
matchesAccountModeFilter, matchesAccountModeFilter,
@@ -22,6 +23,19 @@ describe("account mode filter", () => {
}); });
}); });
describe("account dialog filter", () => {
const options = [
{ id: "rd-api", serviceLabel: "Real-Debrid", title: "Real-Debrid API", modeLabel: "API", pickerDescription: "API-Token" },
{ id: "rd-web", serviceLabel: "Real-Debrid", title: "Real-Debrid Web-Login", modeLabel: "Web-Login", pickerDescription: "Browserfenster" },
{ id: "md-api", serviceLabel: "Mega-Debrid", title: "Mega-Debrid API", modeLabel: "API", pickerDescription: "Login:Passwort" }
];
it("combines an exact service choice with an access-type search", () => {
expect(filterAccountDialogOptions(options, "web", "Real-Debrid").map((option) => option.id)).toEqual(["rd-web"]);
expect(filterAccountDialogOptions(options, "api", "all").map((option) => option.id)).toEqual(["rd-api", "md-api"]);
});
});
describe("account provider order", () => { describe("account provider order", () => {
it("preserves the custom provider order while accounts are disabled", () => { it("preserves the custom provider order while accounts are disabled", () => {
expect(buildConfiguredProviderOrder( expect(buildConfiguredProviderOrder(
+49 -7
View File
@@ -885,7 +885,7 @@ describe("account workspace", () => {
<AccountAddDialog <AccountAddDialog
actions={{ actions={{
onQueryChange: () => {}, onQueryChange: () => {},
onFilterChange: () => {}, onServiceFilterChange: () => {},
onOptionSelect: () => {}, onOptionSelect: () => {},
onFieldChange: () => {}, onFieldChange: () => {},
onClose: () => {}, onClose: () => {},
@@ -894,7 +894,8 @@ describe("account workspace", () => {
model={{ model={{
open: true, open: true,
query: "", query: "",
filter: "all", serviceFilter: "all",
serviceFilters: ["Real-Debrid", "DDownload", "Mega-Debrid", "Debrid-Link"],
options, options,
selectedOptionId: "megadebrid-api", selectedOptionId: "megadebrid-api",
fields: [ fields: [
@@ -938,7 +939,9 @@ describe("account workspace", () => {
expect(addHtml).toContain("Account hinzufügen"); expect(addHtml).toContain("Account hinzufügen");
expect(addHtml).toContain("Prüfen und speichern"); expect(addHtml).toContain("Prüfen und speichern");
expect(count(addHtml, "<select")).toBe(0); expect(count(addHtml, "<select")).toBe(1);
expect(addHtml).toContain('aria-label="Dienst filtern"');
expect(addHtml).toContain("Alle Dienste");
expect(addHtml).toContain('aria-label="Dienst oder Zugangstyp suchen"'); expect(addHtml).toContain('aria-label="Dienst oder Zugangstyp suchen"');
expect(addHtml).toContain('role="listbox"'); expect(addHtml).toContain('role="listbox"');
expect(addHtml).toContain('class="settings-account-picker-header"'); expect(addHtml).toContain('class="settings-account-picker-header"');
@@ -970,7 +973,7 @@ describe("account workspace", () => {
const tree = AccountAddDialog({ const tree = AccountAddDialog({
actions: { actions: {
onQueryChange: () => {}, onQueryChange: () => {},
onFilterChange: () => {}, onServiceFilterChange: () => {},
onOptionSelect: (optionId) => selected.push(optionId), onOptionSelect: (optionId) => selected.push(optionId),
onFieldChange: () => {}, onFieldChange: () => {},
onClose: () => {}, onClose: () => {},
@@ -979,7 +982,8 @@ describe("account workspace", () => {
model: { model: {
open: true, open: true,
query: "", query: "",
filter: "all", serviceFilter: "all",
serviceFilters: ["Real-Debrid", "DDownload", "Mega-Debrid", "Debrid-Link"],
options: accountOptions(), options: accountOptions(),
selectedOptionId: "megadebrid-api", selectedOptionId: "megadebrid-api",
fields: [], fields: [],
@@ -994,12 +998,47 @@ describe("account workspace", () => {
expect(selected).toEqual(["debridlink-api"]); expect(selected).toEqual(["debridlink-api"]);
}); });
it("filters by service and selects the first visible option with Enter", () => {
const selected: string[] = [];
const serviceFilters: string[] = [];
const options = accountOptions().filter((option) => option.title === "Mega-Debrid");
const tree = AccountAddDialog({
actions: {
onQueryChange: () => {},
onServiceFilterChange: (service) => serviceFilters.push(service),
onOptionSelect: (optionId) => selected.push(optionId),
onFieldChange: () => {},
onClose: () => {},
onSubmit: () => {}
},
model: {
open: true,
query: "api",
serviceFilter: "Mega-Debrid",
serviceFilters: ["Real-Debrid", "Mega-Debrid"],
options,
selectedOptionId: options[0].id,
fields: [],
error: "",
busy: false
}
});
const serviceSelect = findElement(tree, (element) => element.props["aria-label"] === "Dienst filtern");
const search = findElement(tree, (element) => element.props["aria-label"] === "Dienst oder Zugangstyp suchen");
serviceSelect.props.onChange({ target: { value: "Real-Debrid" } });
search.props.onKeyDown({ key: "Enter", preventDefault: () => selected.push("prevented") });
expect(serviceFilters).toEqual(["Real-Debrid"]);
expect(selected).toEqual(["prevented", "megadebrid-api"]);
});
it("shows an accessible empty result when account search has no matches", () => { it("shows an accessible empty result when account search has no matches", () => {
const html = renderToStaticMarkup( const html = renderToStaticMarkup(
<AccountAddDialog <AccountAddDialog
actions={{ actions={{
onQueryChange: () => {}, onQueryChange: () => {},
onFilterChange: () => {}, onServiceFilterChange: () => {},
onOptionSelect: () => {}, onOptionSelect: () => {},
onFieldChange: () => {}, onFieldChange: () => {},
onClose: () => {}, onClose: () => {},
@@ -1008,7 +1047,8 @@ describe("account workspace", () => {
model={{ model={{
open: true, open: true,
query: "nicht vorhanden", query: "nicht vorhanden",
filter: "all", serviceFilter: "all",
serviceFilters: ["Real-Debrid"],
options: [], options: [],
selectedOptionId: null, selectedOptionId: null,
fields: [], fields: [],
@@ -1149,6 +1189,8 @@ describe("settings geometry", () => {
it("keeps the specified form, table, switch, overflow and selection geometry", () => { it("keeps the specified form, table, switch, overflow and selection geometry", () => {
const css = readFileSync(new URL("../src/renderer/views/settings/settings.css", import.meta.url), "utf8"); const css = readFileSync(new URL("../src/renderer/views/settings/settings.css", import.meta.url), "utf8");
expect(css).toMatch(/\.settings-account-picker-list\s*{[^}]*height:\s*190px;[^}]*scrollbar-gutter:\s*stable;/s);
expect(css).toMatch(/\.settings-account-picker-empty\s*{[^}]*height:\s*190px;/s);
expect(css).toMatch(/\.settings-content\s*{[^}]*padding:\s*24px;/s); expect(css).toMatch(/\.settings-content\s*{[^}]*padding:\s*24px;/s);
expect(css).toMatch( expect(css).toMatch(
/\.md-runtime-view-content\s*>\s*\.settings-content\s*{[^}]*height:\s*100%;[^}]*padding:\s*24px;/s /\.md-runtime-view-content\s*>\s*\.settings-content\s*{[^}]*height:\s*100%;[^}]*padding:\s*24px;/s