Handle expired subscriptions in client profile UI
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
export const HARBOR_VERSIONS = Object.freeze({
|
export const HARBOR_VERSIONS = Object.freeze({
|
||||||
macClient: '0.25.8',
|
macClient: '0.25.9',
|
||||||
gatewayClient: '0.26.7',
|
gatewayClient: '0.26.8',
|
||||||
gatewayBackend: '0.26.5',
|
gatewayBackend: '0.26.5',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -482,12 +482,17 @@ export function ClientOverviewPage({
|
|||||||
&& (canonicalSwitch || localSwitch)
|
&& (canonicalSwitch || localSwitch)
|
||||||
&& Boolean(operationProfile && operationServer)
|
&& Boolean(operationProfile && operationServer)
|
||||||
&& (operationProfile?.id !== appliedProfile?.id || operationServer?.id !== appliedServer?.id);
|
&& (operationProfile?.id !== appliedProfile?.id || operationServer?.id !== appliedServer?.id);
|
||||||
|
const subscriptionError = error?.context === 'subscription'
|
||||||
|
&& profiles.some((profile) => profile.id === error.profileId
|
||||||
|
&& profile.subscription.errorCode === 'SUBSCRIPTION_EXPIRED')
|
||||||
|
? null
|
||||||
|
: error;
|
||||||
const subscriptionFeature = useSubscriptionFeature({
|
const subscriptionFeature = useSubscriptionFeature({
|
||||||
profiles,
|
profiles,
|
||||||
selection: state.selection,
|
selection: state.selection,
|
||||||
connected,
|
connected,
|
||||||
operations: visibleOperations,
|
operations: visibleOperations,
|
||||||
error,
|
error: subscriptionError,
|
||||||
isGateway,
|
isGateway,
|
||||||
gatewayDirect,
|
gatewayDirect,
|
||||||
onAdd: onAddProfile,
|
onAdd: onAddProfile,
|
||||||
@@ -716,7 +721,7 @@ export function ClientOverviewPage({
|
|||||||
<SubscriptionPanel
|
<SubscriptionPanel
|
||||||
feature={subscriptionFeature}
|
feature={subscriptionFeature}
|
||||||
statusSlot={<>
|
statusSlot={<>
|
||||||
<InlineError error={subscriptionFeature.error || error} context="subscription" />
|
<InlineError error={subscriptionFeature.error} context="subscription" />
|
||||||
</>}
|
</>}
|
||||||
renderServerPicker={(profile, pickerState) => <ServerPicker
|
renderServerPicker={(profile, pickerState) => <ServerPicker
|
||||||
profileId={profile.id}
|
profileId={profile.id}
|
||||||
|
|||||||
@@ -378,7 +378,8 @@ function ProfileGroup({
|
|||||||
profile: ProfileSnapshot;
|
profile: ProfileSnapshot;
|
||||||
renderServerPicker: (profile: ProfileSnapshot, state: ServerPickerRenderState) => ReactNode;
|
renderServerPicker: (profile: ProfileSnapshot, state: ServerPickerRenderState) => ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const expanded = feature.expanded.includes(profile.id);
|
const expired = profile.subscription.errorCode === 'SUBSCRIPTION_EXPIRED';
|
||||||
|
const expanded = !expired && feature.expanded.includes(profile.id);
|
||||||
const closing = feature.closing.includes(profile.id);
|
const closing = feature.closing.includes(profile.id);
|
||||||
const visited = feature.visited.includes(profile.id);
|
const visited = feature.visited.includes(profile.id);
|
||||||
const applied = feature.connected
|
const applied = feature.connected
|
||||||
@@ -401,18 +402,29 @@ function ProfileGroup({
|
|||||||
const visibleServerLocation = [visibleServer?.city, visibleServer?.country]
|
const visibleServerLocation = [visibleServer?.city, visibleServer?.country]
|
||||||
.filter((value): value is string => typeof value === 'string' && Boolean(value))
|
.filter((value): value is string => typeof value === 'string' && Boolean(value))
|
||||||
.join(' · ');
|
.join(' · ');
|
||||||
const localStatus = profileError?.message || (profile.subscription.status === 'stale'
|
const localStatus = !expired && (profileError?.message || (profile.subscription.status === 'stale'
|
||||||
? `Последнее обновление не удалось. Данные от ${profile.subscription.fetchedAt
|
? `Последнее обновление не удалось. Данные от ${profile.subscription.fetchedAt
|
||||||
? new Date(profile.subscription.fetchedAt).toLocaleString('ru-RU', { dateStyle: 'short', timeStyle: 'short' })
|
? new Date(profile.subscription.fetchedAt).toLocaleString('ru-RU', { dateStyle: 'short', timeStyle: 'short' })
|
||||||
: 'предыдущей загрузки'}.`
|
: 'предыдущей загрузки'}.`
|
||||||
: '');
|
: ''));
|
||||||
const profileDomain = subscriptionDomain(profile.subscription.host);
|
const profileDomain = subscriptionDomain(profile.subscription.host);
|
||||||
const profileDetails = `Серверов: ${profile.servers.length}${profile.subscription.fetchedAt
|
const profileDetails = expired
|
||||||
? ` · обновлено ${new Date(profile.subscription.fetchedAt).toLocaleDateString('ru-RU')}`
|
? 'Срок действия подписки истёк'
|
||||||
: ''}`;
|
: `Серверов: ${profile.servers.length}${profile.subscription.fetchedAt
|
||||||
return <section className={`client-profile-group${expanded ? ' is-expanded' : ''}${applied ? ' is-active' : ''}`}>
|
? ` · обновлено ${new Date(profile.subscription.fetchedAt).toLocaleDateString('ru-RU')}`
|
||||||
|
: ''}`;
|
||||||
|
const profileTitle = <span className="client-profile-title">
|
||||||
|
<span>
|
||||||
|
<strong>{profileDomain}</strong>
|
||||||
|
{(applied || desired) && <em>{applied ? 'АКТИВНА' : 'ВЫБРАНА'}</em>}
|
||||||
|
</span>
|
||||||
|
<small>{profileDetails}</small>
|
||||||
|
</span>;
|
||||||
|
return <section className={`client-profile-group${expanded ? ' is-expanded' : ''}${applied ? ' is-active' : ''}${expired ? ' is-expired' : ''}`}>
|
||||||
<div className="client-profile-header">
|
<div className="client-profile-header">
|
||||||
<button
|
{expired ? <div className="client-profile-disclosure is-static">
|
||||||
|
{profileTitle}
|
||||||
|
</div> : <button
|
||||||
className="client-profile-disclosure"
|
className="client-profile-disclosure"
|
||||||
type="button"
|
type="button"
|
||||||
aria-expanded={expanded}
|
aria-expanded={expanded}
|
||||||
@@ -420,15 +432,9 @@ function ProfileGroup({
|
|||||||
onClick={() => feature.toggleProfile(profile.id)}
|
onClick={() => feature.toggleProfile(profile.id)}
|
||||||
>
|
>
|
||||||
<span className="client-profile-chevron" aria-hidden="true">›</span>
|
<span className="client-profile-chevron" aria-hidden="true">›</span>
|
||||||
<span className="client-profile-title">
|
{profileTitle}
|
||||||
<span>
|
</button>}
|
||||||
<strong>{profileDomain}</strong>
|
{!expired && (applied || desired) && <ProfileUsage profile={profile} />}
|
||||||
{(applied || desired) && <em>{applied ? 'АКТИВНА' : 'ВЫБРАНА'}</em>}
|
|
||||||
</span>
|
|
||||||
<small>{profileDetails}</small>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
{(applied || desired) && <ProfileUsage profile={profile} />}
|
|
||||||
<button
|
<button
|
||||||
className={`client-profile-refresh${refreshing ? ' is-refreshing' : ''}`}
|
className={`client-profile-refresh${refreshing ? ' is-refreshing' : ''}`}
|
||||||
type="button"
|
type="button"
|
||||||
@@ -456,7 +462,7 @@ function ProfileGroup({
|
|||||||
|
|
||||||
{localStatus && <div className="client-profile-local-status" role="status">{localStatus}</div>}
|
{localStatus && <div className="client-profile-local-status" role="status">{localStatus}</div>}
|
||||||
|
|
||||||
{visibleServer && <button
|
{!expired && visibleServer && <button
|
||||||
className="client-profile-selected-server"
|
className="client-profile-selected-server"
|
||||||
type="button"
|
type="button"
|
||||||
aria-expanded={expanded}
|
aria-expanded={expanded}
|
||||||
@@ -468,7 +474,7 @@ function ProfileGroup({
|
|||||||
{visibleServerLocation && <small>{visibleServerLocation}</small>}
|
{visibleServerLocation && <small>{visibleServerLocation}</small>}
|
||||||
</button>}
|
</button>}
|
||||||
|
|
||||||
{visited && <div
|
{!expired && visited && <div
|
||||||
id={`client-profile-${profile.id}`}
|
id={`client-profile-${profile.id}`}
|
||||||
className={`client-profile-body${expanded ? ' is-open' : ''}${closing ? ' is-closing' : ''}`}
|
className={`client-profile-body${expanded ? ' is-open' : ''}${closing ? ' is-closing' : ''}`}
|
||||||
aria-hidden={!expanded}
|
aria-hidden={!expanded}
|
||||||
|
|||||||
@@ -651,7 +651,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.client-profile-body.is-open .client-server-toolbar {
|
.client-profile-body.is-open .client-server-toolbar {
|
||||||
animation: client-profile-ping-in 300ms 620ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
animation: client-profile-ping-in 420ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-profile-body.is-closing .client-server-toolbar {
|
.client-profile-body.is-closing .client-server-toolbar {
|
||||||
|
|||||||
@@ -105,6 +105,14 @@
|
|||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.client-profile-disclosure.is-static {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-profile-disclosure.is-static .client-profile-title {
|
||||||
|
grid-column: 2;
|
||||||
|
}
|
||||||
|
|
||||||
.client-profile-title {
|
.client-profile-title {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -139,6 +147,10 @@
|
|||||||
text-transform: var(--type-label-transform);
|
text-transform: var(--type-label-transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.client-profile-group.is-expired .client-profile-title small {
|
||||||
|
color: oklch(0.68 0.14 72);
|
||||||
|
}
|
||||||
|
|
||||||
.client-profile-title em {
|
.client-profile-title em {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
color: var(--client-accent);
|
color: var(--client-accent);
|
||||||
@@ -276,7 +288,7 @@
|
|||||||
.client-profile-body-inner {
|
.client-profile-body-inner {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding-bottom: 4px;
|
padding: 0 0 4px 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-profile-body .client-servers {
|
.client-profile-body .client-servers {
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ test('server picker starts simple and reveals advanced controls on demand', () =
|
|||||||
assert.match(styles, /\.client-profile-body \.client-server-check-label \{[\s\S]*?display: inline;/);
|
assert.match(styles, /\.client-profile-body \.client-server-check-label \{[\s\S]*?display: inline;/);
|
||||||
assert.match(picker, /'--server-index': Math\.min\(index, 7\)[\s\S]*'--server-exit-delay'/);
|
assert.match(picker, /'--server-index': Math\.min\(index, 7\)[\s\S]*'--server-exit-delay'/);
|
||||||
assert.match(styles, /\.client-profile-body\.is-open \.client-server-row \{[\s\S]*client-profile-server-in[\s\S]*var\(--server-index\)/);
|
assert.match(styles, /\.client-profile-body\.is-open \.client-server-row \{[\s\S]*client-profile-server-in[\s\S]*var\(--server-index\)/);
|
||||||
assert.match(styles, /\.client-profile-body\.is-open \.client-server-toolbar \{[\s\S]*client-profile-ping-in 300ms 620ms/);
|
assert.match(styles, /\.client-profile-body\.is-open \.client-server-toolbar \{[\s\S]*client-profile-ping-in 420ms cubic-bezier\(0\.16, 1, 0\.3, 1\) both/);
|
||||||
assert.match(styles, /\.client-profile-body\.is-closing \.client-server-toolbar \{[\s\S]*client-profile-ping-out[\s\S]*\.client-profile-body\.is-closing \.client-server-row \{[\s\S]*var\(--server-exit-delay\)/);
|
assert.match(styles, /\.client-profile-body\.is-closing \.client-server-toolbar \{[\s\S]*client-profile-ping-out[\s\S]*\.client-profile-body\.is-closing \.client-server-row \{[\s\S]*var\(--server-exit-delay\)/);
|
||||||
assert.doesNotMatch(styles, /\.client-profile-body \.client-server-check::after/);
|
assert.doesNotMatch(styles, /\.client-profile-body \.client-server-check::after/);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -39,24 +39,24 @@ const acceptedLedger = {
|
|||||||
counts: {
|
counts: {
|
||||||
cascadeEdges: 822,
|
cascadeEdges: 822,
|
||||||
customProperties: 103,
|
customProperties: 103,
|
||||||
declarations: 3299,
|
declarations: 3302,
|
||||||
important: 0,
|
important: 0,
|
||||||
keyframes: 56,
|
keyframes: 56,
|
||||||
media: 13,
|
media: 13,
|
||||||
rules: 949,
|
rules: 952,
|
||||||
variableReferences: 808,
|
variableReferences: 808,
|
||||||
},
|
},
|
||||||
hashes: {
|
hashes: {
|
||||||
cascadeEdges: '03bede7036ed12ad1ccb5ee41188834934f8d9acd2d02296c5945dfc195bac71',
|
cascadeEdges: '183435ac9241f0507dd2d82eda73e71eaaee8f8dcbdc0747b83cf8e17e2ce9f7',
|
||||||
customProperties: 'ef70fb1d9b61b177f1939f811babe1116bee631de26f5eac0aa0d0009ca5a1fb',
|
customProperties: 'ef70fb1d9b61b177f1939f811babe1116bee631de26f5eac0aa0d0009ca5a1fb',
|
||||||
declarations: 'fc35daf13ffc5a0754f4e6f4982a5ff0c09c8664dfd86fd144e12c02fb006794',
|
declarations: '07ab8e4c442a88368e6727f0665b29a2d27a882d64c38647250a0f4f0c069f6e',
|
||||||
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
|
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
|
||||||
duplicateSelectors: '0fa66e370695261a2a5c6a189752c07d518d80d68d3e79a3ba4bb34407893c0e',
|
duplicateSelectors: '0fa66e370695261a2a5c6a189752c07d518d80d68d3e79a3ba4bb34407893c0e',
|
||||||
keyframes: 'd4378751f36eccc87923c12540315462055032a5d34978a0f45ecada0a5cc1ce',
|
keyframes: 'd4378751f36eccc87923c12540315462055032a5d34978a0f45ecada0a5cc1ce',
|
||||||
ruleDeclarationSequences: 'af072317748091c47aaf33de5d124249367ca37e5037dd58eab869ca0fc26130',
|
ruleDeclarationSequences: '864dd1f50b04e6e07e30be6e4aa3b72d02d5425dbecf74a28afa6280ce9dfd73',
|
||||||
selectors: '84116a78a2c71322a4e5ea45a7cdec788ca1d0d02c46767eb1c642b7e0755754',
|
selectors: 'fa5bb2e06dcbb72802e49d67e2312504f27e87e855427498526ea24f143f84c8',
|
||||||
variableReferences: '664c093241c4076ca05a9dda06ae88d4e7024c926c4636109790201df8212df4',
|
variableReferences: '664c093241c4076ca05a9dda06ae88d4e7024c926c4636109790201df8212df4',
|
||||||
witnesses: 'b31a875d6be7980f6bb8eb46dac2d9665f6fe9afaa7775a4f994a0156109734d',
|
witnesses: 'ac771c94e7f2c409c3d317de610c2dae5722eb1d6f0b6626a4e1aa986c83fd94',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -209,7 +209,7 @@ test('client typography uses the shared semantic scale outside the token owner',
|
|||||||
|
|
||||||
test('accepted stylesheet has pinned declaration, selector, keyframe, variable, and cascade ledgers', () => {
|
test('accepted stylesheet has pinned declaration, selector, keyframe, variable, and cascade ledgers', () => {
|
||||||
const witnesses = readStyleWitnesses(root);
|
const witnesses = readStyleWitnesses(root);
|
||||||
assert.equal(witnesses.length, 805);
|
assert.equal(witnesses.length, 811);
|
||||||
assert.equal(witnesses.filter((witness) => witness.unknown || witness.ancestorUnknown).length, 0);
|
assert.equal(witnesses.filter((witness) => witness.unknown || witness.ancestorUnknown).length, 0);
|
||||||
const ledger = createStyleLedger(readStyleSource(root), { witnesses });
|
const ledger = createStyleLedger(readStyleSource(root), { witnesses });
|
||||||
assert.deepEqual(ledger.counts, acceptedLedger.counts);
|
assert.deepEqual(ledger.counts, acceptedLedger.counts);
|
||||||
@@ -405,8 +405,8 @@ test('main owns one public stylesheet and the regrouped production CSS is determ
|
|||||||
assert.equal((main.match(/import ['"][^'"]+\.css['"]/g) || []).length, 1);
|
assert.equal((main.match(/import ['"][^'"]+\.css['"]/g) || []).length, 1);
|
||||||
|
|
||||||
const assets = fs.readdirSync(path.join(root, 'dist/assets')).filter((file) => file.endsWith('.css'));
|
const assets = fs.readdirSync(path.join(root, 'dist/assets')).filter((file) => file.endsWith('.css'));
|
||||||
assert.deepEqual(assets, ['index-9maKdwRg.css']);
|
assert.deepEqual(assets, ['index-DbgBuebh.css']);
|
||||||
const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0]));
|
const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0]));
|
||||||
assert.equal(built.byteLength, 127604);
|
assert.equal(built.byteLength, 127802);
|
||||||
assert.equal(sha256(built), '0ba84e80e387ee62c33379b3674b1849ba65857cf45dac1257f4d87605008d04');
|
assert.equal(sha256(built), '19995a47de1ae634f29b26fb4e484f6d9fb048519cc42ad0a186d11904e2bfb6');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -107,11 +107,16 @@ test('feature keeps exact slots, truthy closes and subscription DOM order', () =
|
|||||||
test('profiles render as flat accordion groups with scoped controls', () => {
|
test('profiles render as flat accordion groups with scoped controls', () => {
|
||||||
assert.match(feature, /feature\.profiles\.map\(\(profile\) => <ProfileGroup/);
|
assert.match(feature, /feature\.profiles\.map\(\(profile\) => <ProfileGroup/);
|
||||||
assert.match(feature, /aria-expanded=\{expanded\}/);
|
assert.match(feature, /aria-expanded=\{expanded\}/);
|
||||||
|
assert.match(feature, /const expired = profile\.subscription\.errorCode === 'SUBSCRIPTION_EXPIRED'/);
|
||||||
|
assert.match(feature, /profileDetails = expired[\s\S]*'Срок действия подписки истёк'[\s\S]*`Серверов:/);
|
||||||
|
assert.match(feature, /expired \? <div className="client-profile-disclosure is-static">[\s\S]*: <button[\s\S]*client-profile-chevron/);
|
||||||
|
assert.match(feature, /\{!expired && visited && <div/);
|
||||||
|
assert.match(page, /const subscriptionError = error\?\.context === 'subscription'[\s\S]*profile\.id === error\.profileId[\s\S]*profile\.subscription\.errorCode === 'SUBSCRIPTION_EXPIRED'[\s\S]*error: subscriptionError/);
|
||||||
assert.match(feature, /profile\.subscription\.status === 'stale'[\s\S]*profile\.subscription\.fetchedAt/);
|
assert.match(feature, /profile\.subscription\.status === 'stale'[\s\S]*profile\.subscription\.fetchedAt/);
|
||||||
assert.match(feature, /\{!localStatus && renderServerPicker\(profile/);
|
assert.match(feature, /\{!localStatus && renderServerPicker\(profile/);
|
||||||
assert.doesNotMatch(feature, /Выберите сервер этой подписки|client-profile-server-hint/);
|
assert.doesNotMatch(feature, /Выберите сервер этой подписки|client-profile-server-hint/);
|
||||||
assert.match(feature, /feature\.operations\.profileRefresh\?\.target === profile\.id/);
|
assert.match(feature, /feature\.operations\.profileRefresh\?\.target === profile\.id/);
|
||||||
assert.match(feature, /\{visibleServer && <button[\s\S]*client-profile-selected-server[\s\S]*aria-expanded=\{expanded\}/);
|
assert.match(feature, /\{!expired && visibleServer && <button[\s\S]*client-profile-selected-server[\s\S]*aria-expanded=\{expanded\}/);
|
||||||
assert.match(feature, /anchorServerId: visibleServer\?\.id \|\| ''/);
|
assert.match(feature, /anchorServerId: visibleServer\?\.id \|\| ''/);
|
||||||
assert.match(feature, /const visibleServerId = applied[\s\S]*feature\.selection\.appliedServerId[\s\S]*desired \? profile\.desiredServerId : ''/);
|
assert.match(feature, /const visibleServerId = applied[\s\S]*feature\.selection\.appliedServerId[\s\S]*desired \? profile\.desiredServerId : ''/);
|
||||||
assert.match(feature, /selectedServerId: visibleServerId/);
|
assert.match(feature, /selectedServerId: visibleServerId/);
|
||||||
@@ -119,11 +124,12 @@ test('profiles render as flat accordion groups with scoped controls', () => {
|
|||||||
assert.doesNotMatch(page, /onActivateProfile/);
|
assert.doesNotMatch(page, /onActivateProfile/);
|
||||||
assert.match(feature, /const desired = !feature\.connected[\s\S]*&& !feature\.gatewayDirect/);
|
assert.match(feature, /const desired = !feature\.connected[\s\S]*&& !feature\.gatewayDirect/);
|
||||||
assert.match(feature, /const profileDomain = subscriptionDomain\(profile\.subscription\.host\)/);
|
assert.match(feature, /const profileDomain = subscriptionDomain\(profile\.subscription\.host\)/);
|
||||||
assert.match(feature, /const profileDetails = `Серверов: \$\{profile\.servers\.length\}/);
|
|
||||||
assert.doesNotMatch(feature, /<strong>\{profile\.label\}<\/strong>|Переименовать|client-profile-menu/);
|
assert.doesNotMatch(feature, /<strong>\{profile\.label\}<\/strong>|Переименовать|client-profile-menu/);
|
||||||
assert.match(styles, /\.client-profile-group/);
|
assert.match(styles, /\.client-profile-group/);
|
||||||
assert.match(styles, /\.client-profile-body/);
|
assert.match(styles, /\.client-profile-body/);
|
||||||
assert.match(styles, /\.client-profile-body \{[\s\S]*grid-template-rows: 0fr[\s\S]*\.client-profile-body\.is-open \{[\s\S]*grid-template-rows: 1fr/);
|
assert.match(styles, /\.client-profile-body \{[\s\S]*grid-template-rows: 0fr[\s\S]*\.client-profile-body\.is-open \{[\s\S]*grid-template-rows: 1fr/);
|
||||||
|
assert.match(styles, /\.client-profile-body-inner \{[\s\S]*padding: 0 0 4px 24px/);
|
||||||
|
assert.match(styles, /\.client-profile-group\.is-expired \.client-profile-title small \{[\s\S]*color: oklch\(0\.68 0\.14 72\)/);
|
||||||
assert.match(styles, /@media \(prefers-reduced-motion: reduce\)[\s\S]*\.client-profile-body \*/);
|
assert.match(styles, /@media \(prefers-reduced-motion: reduce\)[\s\S]*\.client-profile-body \*/);
|
||||||
assert.match(styles, /\.client-profile-refresh\.is-refreshing/);
|
assert.match(styles, /\.client-profile-refresh\.is-refreshing/);
|
||||||
assert.match(styles, /\.client-profile-refresh,[\s\S]*\.client-profile-delete\s*\{[\s\S]*width:\s*36px;[\s\S]*height:\s*36px;[\s\S]*display:\s*grid;[\s\S]*place-items:\s*center/);
|
assert.match(styles, /\.client-profile-refresh,[\s\S]*\.client-profile-delete\s*\{[\s\S]*width:\s*36px;[\s\S]*height:\s*36px;[\s\S]*display:\s*grid;[\s\S]*place-items:\s*center/);
|
||||||
|
|||||||
Reference in New Issue
Block a user