Track outbound device traffic deltas
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
export const HARBOR_VERSIONS = Object.freeze({
|
export const HARBOR_VERSIONS = Object.freeze({
|
||||||
macClient: '0.25.7',
|
macClient: '0.25.8',
|
||||||
gatewayClient: '0.26.6',
|
gatewayClient: '0.26.7',
|
||||||
gatewayBackend: '0.26.5',
|
gatewayBackend: '0.26.5',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -724,7 +724,6 @@ export function ClientOverviewPage({
|
|||||||
servers={profile.servers}
|
servers={profile.servers}
|
||||||
selectedServerId={pickerState.selectedServerId}
|
selectedServerId={pickerState.selectedServerId}
|
||||||
disabled={serverApplyBlocked || pickerState.disabled}
|
disabled={serverApplyBlocked || pickerState.disabled}
|
||||||
prompt={!pickerState.selectedServerId}
|
|
||||||
leaving={pickerState.leaving}
|
leaving={pickerState.leaving}
|
||||||
revealVersion={pickerState.revealVersion}
|
revealVersion={pickerState.revealVersion}
|
||||||
anchorServerId={pickerState.anchorServerId}
|
anchorServerId={pickerState.anchorServerId}
|
||||||
|
|||||||
@@ -29,6 +29,18 @@ interface TrafficDelta {
|
|||||||
gateway?: string;
|
gateway?: string;
|
||||||
proxy?: string;
|
proxy?: string;
|
||||||
total?: string;
|
total?: string;
|
||||||
|
vpn?: string;
|
||||||
|
direct?: string;
|
||||||
|
unknown?: string;
|
||||||
|
outboundTotal?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TrafficTotals {
|
||||||
|
gateway: bigint;
|
||||||
|
proxy: bigint;
|
||||||
|
vpn: bigint;
|
||||||
|
direct: bigint;
|
||||||
|
unknown: bigint;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PinCollapse {
|
interface PinCollapse {
|
||||||
@@ -95,7 +107,7 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
|||||||
const previousPositions = useRef(new Map<string, DOMRect>());
|
const previousPositions = useRef(new Map<string, DOMRect>());
|
||||||
const previousScrollTop = useRef(0);
|
const previousScrollTop = useRef(0);
|
||||||
const movementAnimations = useRef(new Map<string, Animation>());
|
const movementAnimations = useRef(new Map<string, Animation>());
|
||||||
const previousTraffic = useRef(new Map<string, { gateway: bigint; proxy: bigint }>());
|
const previousTraffic = useRef(new Map<string, TrafficTotals>());
|
||||||
const aliasBaseline = useRef({ id: '', value: '' });
|
const aliasBaseline = useRef({ id: '', value: '' });
|
||||||
const copyTimers = useRef(new Map<string, ReturnType<typeof setTimeout>>());
|
const copyTimers = useRef(new Map<string, ReturnType<typeof setTimeout>>());
|
||||||
const copyAttempts = useRef(new Map<string, object>());
|
const copyAttempts = useRef(new Map<string, object>());
|
||||||
@@ -131,19 +143,38 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const next = new Map<string, { gateway: bigint; proxy: bigint }>();
|
const next = new Map<string, TrafficTotals>();
|
||||||
const deltas: Record<string, TrafficDelta> = {};
|
const deltas: Record<string, TrafficDelta> = {};
|
||||||
for (const device of snapshot?.devices || []) {
|
for (const device of snapshot?.devices || []) {
|
||||||
const gateway = byteString(device.downloadBytes) + byteString(device.uploadBytes);
|
const gateway = byteString(device.downloadBytes) + byteString(device.uploadBytes);
|
||||||
const proxy = byteString(device.proxyDownloadBytes) + byteString(device.proxyUploadBytes);
|
const proxy = byteString(device.proxyDownloadBytes) + byteString(device.proxyUploadBytes);
|
||||||
|
const vpn = byteString(device.outboundTraffic?.vpnBytes);
|
||||||
|
const direct = byteString(device.outboundTraffic?.directTrackedBytes)
|
||||||
|
+ byteString(device.outboundTraffic?.directIpv4Bytes);
|
||||||
|
const unknown = byteString(device.outboundTraffic?.unknownBytes);
|
||||||
const previous = previousTraffic.current.get(device.id);
|
const previous = previousTraffic.current.get(device.id);
|
||||||
next.set(device.id, { gateway, proxy });
|
next.set(device.id, { gateway, proxy, vpn, direct, unknown });
|
||||||
if (!previous) continue;
|
if (!previous) continue;
|
||||||
const gatewayDelta = positiveByteDelta(previous.gateway, gateway);
|
const gatewayDelta = positiveByteDelta(previous.gateway, gateway);
|
||||||
const proxyDelta = positiveByteDelta(previous.proxy, proxy);
|
const proxyDelta = positiveByteDelta(previous.proxy, proxy);
|
||||||
if (!gatewayDelta && !proxyDelta) continue;
|
const vpnDelta = positiveByteDelta(previous.vpn, vpn);
|
||||||
|
const directDelta = positiveByteDelta(previous.direct, direct);
|
||||||
|
const unknownDelta = positiveByteDelta(previous.unknown, unknown);
|
||||||
|
if (!gatewayDelta && !proxyDelta && !vpnDelta && !directDelta && !unknownDelta) continue;
|
||||||
const totalDelta = positiveByteDelta(previous.gateway + previous.proxy, gateway + proxy);
|
const totalDelta = positiveByteDelta(previous.gateway + previous.proxy, gateway + proxy);
|
||||||
deltas[device.id] = { gateway: gatewayDelta, proxy: proxyDelta, total: totalDelta };
|
const outboundTotalDelta = positiveByteDelta(
|
||||||
|
previous.vpn + previous.direct + previous.unknown,
|
||||||
|
vpn + direct + unknown,
|
||||||
|
);
|
||||||
|
deltas[device.id] = {
|
||||||
|
gateway: gatewayDelta,
|
||||||
|
proxy: proxyDelta,
|
||||||
|
total: totalDelta,
|
||||||
|
vpn: vpnDelta,
|
||||||
|
direct: directDelta,
|
||||||
|
unknown: unknownDelta,
|
||||||
|
outboundTotal: outboundTotalDelta,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
previousTraffic.current = next;
|
previousTraffic.current = next;
|
||||||
if (!Object.keys(deltas).length) return;
|
if (!Object.keys(deltas).length) return;
|
||||||
@@ -343,7 +374,7 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
|||||||
<button
|
<button
|
||||||
className="client-devices-reset"
|
className="client-devices-reset"
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Сбросить вход и выход всех устройств"
|
aria-label="Обнулить трафик устройств"
|
||||||
disabled={!snapshot || resetting}
|
disabled={!snapshot || resetting}
|
||||||
onClick={requestTrafficReset}
|
onClick={requestTrafficReset}
|
||||||
>
|
>
|
||||||
@@ -351,7 +382,7 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
|||||||
<path d="M4 12a8 8 0 1 0 2.3-5.7M4 5v7h7" />
|
<path d="M4 12a8 8 0 1 0 2.3-5.7M4 5v7h7" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<Tooltip>Сбросить вход и выход всех устройств</Tooltip>
|
<Tooltip>Обнулить трафик устройств</Tooltip>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 id="client-devices-title">Устройства</h2>
|
<h2 id="client-devices-title">Устройства</h2>
|
||||||
@@ -579,13 +610,13 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
|
|||||||
aria-label={trafficAriaLabel}
|
aria-label={trafficAriaLabel}
|
||||||
>
|
>
|
||||||
<span className="client-device-traffic-total" aria-hidden="true">
|
<span className="client-device-traffic-total" aria-hidden="true">
|
||||||
<b>{trafficLabel}</b><TrafficValue value={displayedTotal} delta={trafficView === 'inbound' ? trafficDelta.total : undefined} />
|
<b>{trafficLabel}</b><TrafficValue value={displayedTotal} delta={trafficView === 'outbound' ? trafficDelta.outboundTotal : trafficDelta.total} />
|
||||||
</span>
|
</span>
|
||||||
<span className="client-device-traffic-breakdown" aria-hidden="true">
|
<span className="client-device-traffic-breakdown" aria-hidden="true">
|
||||||
{trafficView === 'outbound' ? <>
|
{trafficView === 'outbound' ? <>
|
||||||
<span className="is-vpn"><b>VPN</b><TrafficValue value={outboundAvailable ? formatByteString(vpnTotal.toString()) : '—'} /></span>
|
<span className="is-vpn"><b>VPN</b><TrafficValue value={outboundAvailable ? formatByteString(vpnTotal.toString()) : '—'} delta={trafficDelta.vpn} /></span>
|
||||||
<span className="is-direct-total"><b>Direct</b><TrafficValue value={outboundAvailable ? formatByteString(directTotal.toString()) : '—'} /></span>
|
<span className="is-direct-total"><b>Direct</b><TrafficValue value={outboundAvailable ? formatByteString(directTotal.toString()) : '—'} delta={trafficDelta.direct} /></span>
|
||||||
{unknownTotal > 0n && <span className="is-unknown"><b>Другое</b><TrafficValue value={formatByteString(unknownTotal.toString())} /></span>}
|
{unknownTotal > 0n && <span className="is-unknown"><b>Другое</b><TrafficValue value={formatByteString(unknownTotal.toString())} delta={trafficDelta.unknown} /></span>}
|
||||||
</> : <>
|
</> : <>
|
||||||
<span><b>Gateway</b><TrafficValue value={gatewayTraffic} delta={trafficDelta.gateway} /></span>
|
<span><b>Gateway</b><TrafficValue value={gatewayTraffic} delta={trafficDelta.gateway} /></span>
|
||||||
{hasProxyTraffic && <span className="is-proxy"><b>Прокси</b><TrafficValue value={proxyTraffic} delta={trafficDelta.proxy} /></span>}
|
{hasProxyTraffic && <span className="is-proxy"><b>Прокси</b><TrafficValue value={proxyTraffic} delta={trafficDelta.proxy} /></span>}
|
||||||
|
|||||||
@@ -164,7 +164,6 @@ interface ServerPickerProps {
|
|||||||
servers: PickerServer[];
|
servers: PickerServer[];
|
||||||
selectedServerId: string;
|
selectedServerId: string;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
prompt: boolean;
|
|
||||||
leaving: boolean;
|
leaving: boolean;
|
||||||
revealVersion: number;
|
revealVersion: number;
|
||||||
anchorServerId?: string;
|
anchorServerId?: string;
|
||||||
@@ -177,7 +176,6 @@ export function ServerPicker({
|
|||||||
servers,
|
servers,
|
||||||
selectedServerId,
|
selectedServerId,
|
||||||
disabled,
|
disabled,
|
||||||
prompt,
|
|
||||||
leaving,
|
leaving,
|
||||||
revealVersion,
|
revealVersion,
|
||||||
anchorServerId = '',
|
anchorServerId = '',
|
||||||
@@ -278,7 +276,6 @@ export function ServerPicker({
|
|||||||
|
|
||||||
if (servers.length === 1) {
|
if (servers.length === 1) {
|
||||||
return <section className="client-servers" aria-label="Выберите сервер">
|
return <section className="client-servers" aria-label="Выберите сервер">
|
||||||
{prompt && <span className="client-server-prompt">Выберите сервер</span>}
|
|
||||||
<div className="client-server-toolbar is-single">
|
<div className="client-server-toolbar is-single">
|
||||||
<span className="client-server-toolbar-title">Список серверов</span>
|
<span className="client-server-toolbar-title">Список серверов</span>
|
||||||
<ServerCheckButton checking={checking} onClick={checkVisible} />
|
<ServerCheckButton checking={checking} onClick={checkVisible} />
|
||||||
@@ -317,7 +314,6 @@ export function ServerPicker({
|
|||||||
];
|
];
|
||||||
|
|
||||||
return <section className={`client-servers is-scalable${simpleServers.length <= INLINE_SERVER_ROWS ? ' is-short' : ''}`} aria-label="Выберите сервер">
|
return <section className={`client-servers is-scalable${simpleServers.length <= INLINE_SERVER_ROWS ? ' is-short' : ''}`} aria-label="Выберите сервер">
|
||||||
{prompt && <span className="client-server-prompt">Выберите сервер</span>}
|
|
||||||
<div className="client-server-toolbar">
|
<div className="client-server-toolbar">
|
||||||
<span className="client-server-toolbar-title">Список серверов</span>
|
<span className="client-server-toolbar-title">Список серверов</span>
|
||||||
<ServerCheckButton checking={checking} disabled={!servers.length} onClick={checkVisible} />
|
<ServerCheckButton checking={checking} disabled={!servers.length} onClick={checkVisible} />
|
||||||
|
|||||||
@@ -475,8 +475,7 @@ function ProfileGroup({
|
|||||||
inert={!expanded ? true : undefined}
|
inert={!expanded ? true : undefined}
|
||||||
>
|
>
|
||||||
<div className="client-profile-body-inner">
|
<div className="client-profile-body-inner">
|
||||||
{!visibleServerId && <p className="client-profile-server-hint">Выберите сервер этой подписки</p>}
|
{!localStatus && renderServerPicker(profile, {
|
||||||
{renderServerPicker(profile, {
|
|
||||||
disabled: controlsBlocked,
|
disabled: controlsBlocked,
|
||||||
leaving: false,
|
leaving: false,
|
||||||
revealVersion: feature.revealVersions[profile.id] || 0,
|
revealVersion: feature.revealVersions[profile.id] || 0,
|
||||||
|
|||||||
@@ -124,6 +124,7 @@
|
|||||||
.client-devices-reset-wrap > .client-tooltip {
|
.client-devices-reset-wrap > .client-tooltip {
|
||||||
right: 0;
|
right: 0;
|
||||||
left: auto;
|
left: auto;
|
||||||
|
white-space: nowrap;
|
||||||
transform: translate(0, 2px);
|
transform: translate(0, 2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,6 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-server-prompt {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
color: var(--client-muted);
|
|
||||||
font: var(--type-label);
|
|
||||||
letter-spacing: var(--type-label-tracking);
|
|
||||||
text-transform: var(--type-label-transform);
|
|
||||||
text-align: center;
|
|
||||||
animation: client-state-reveal 700ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-server-grid {
|
.client-server-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
@@ -385,7 +385,6 @@
|
|||||||
from { opacity: 0; }
|
from { opacity: 0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-profile-server-hint,
|
|
||||||
.client-profile-local-status {
|
.client-profile-local-status {
|
||||||
min-height: 18px;
|
min-height: 18px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@@ -114,7 +114,9 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
|
|||||||
assert.match(chart, /hovered\.proxy - hovered\.directIpv4 > 0n && hovered\.directIpv4 > 0n && <>[\s\S]*через sing-box[\s\S]*мимо sing-box · IPv4/);
|
assert.match(chart, /hovered\.proxy - hovered\.directIpv4 > 0n && hovered\.directIpv4 > 0n && <>[\s\S]*через sing-box[\s\S]*мимо sing-box · IPv4/);
|
||||||
assert.match(chart, /client-device-traffic-legend[\s\S]*valueKey === 'gateway'[\s\S]*>VPN<\/span>[\s\S]*valueKey === 'proxy'[\s\S]*>Direct<\/span>[\s\S]*valueKey === 'unknown'[\s\S]*>Другое<\/span>/);
|
assert.match(chart, /client-device-traffic-legend[\s\S]*valueKey === 'gateway'[\s\S]*>VPN<\/span>[\s\S]*valueKey === 'proxy'[\s\S]*>Direct<\/span>[\s\S]*valueKey === 'unknown'[\s\S]*>Другое<\/span>/);
|
||||||
assert.doesNotMatch(chart, /Direct ≈|>\?<\/span>/);
|
assert.doesNotMatch(chart, /Direct ≈|>\?<\/span>/);
|
||||||
assert.match(panel, /positiveByteDelta\(previous\.gateway, gateway\)[\s\S]*positiveByteDelta\(previous\.proxy, proxy\)/);
|
assert.match(panel, /positiveByteDelta\(previous\.gateway, gateway\)[\s\S]*positiveByteDelta\(previous\.proxy, proxy\)[\s\S]*positiveByteDelta\(previous\.vpn, vpn\)[\s\S]*positiveByteDelta\(previous\.direct, direct\)[\s\S]*positiveByteDelta\(previous\.unknown, unknown\)/);
|
||||||
|
assert.match(panel, /trafficView === 'outbound' \? trafficDelta\.outboundTotal : trafficDelta\.total/);
|
||||||
|
assert.match(panel, /<b>VPN<\/b><TrafficValue[^>]*delta=\{trafficDelta\.vpn\}[\s\S]*<b>Direct<\/b><TrafficValue[^>]*delta=\{trafficDelta\.direct\}[\s\S]*<b>Другое<\/b><TrafficValue[^>]*delta=\{trafficDelta\.unknown\}/);
|
||||||
assert.match(panel, /setTimeout\(\(\) => setTrafficDeltas\(\{\}\), TRAFFIC_DELTA_MS\)/);
|
assert.match(panel, /setTimeout\(\(\) => setTrafficDeltas\(\{\}\), TRAFFIC_DELTA_MS\)/);
|
||||||
assert.doesNotMatch(panel, /client-device-traffic client-tooltip-anchor|<Tooltip>\{trafficLabel\}<\/Tooltip>/);
|
assert.doesNotMatch(panel, /client-device-traffic client-tooltip-anchor|<Tooltip>\{trafficLabel\}<\/Tooltip>/);
|
||||||
assert.match(panel, /source\?\.traffic\?\.proxy\?\.error/);
|
assert.match(panel, /source\?\.traffic\?\.proxy\?\.error/);
|
||||||
@@ -200,10 +202,11 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
|
|||||||
assert.match(styles, /\.client-device-traffic-plot svg \{[\s\S]*height: 100%;[\s\S]*overflow: hidden/);
|
assert.match(styles, /\.client-device-traffic-plot svg \{[\s\S]*height: 100%;[\s\S]*overflow: hidden/);
|
||||||
assert.match(styles, /\.client-devices-sort:hover \.client-devices-sort-icon[\s\S]*rotate\(360deg\)/);
|
assert.match(styles, /\.client-devices-sort:hover \.client-devices-sort-icon[\s\S]*rotate\(360deg\)/);
|
||||||
assert.match(styles, /\.client-devices-refresh-ring circle[\s\S]*client-devices-refresh-progress 15s/);
|
assert.match(styles, /\.client-devices-refresh-ring circle[\s\S]*client-devices-refresh-progress 15s/);
|
||||||
assert.match(panel, /client-devices-reset-wrap client-tooltip-anchor[\s\S]*aria-label="Сбросить вход и выход всех устройств"[\s\S]*<Tooltip>Сбросить вход и выход всех устройств<\/Tooltip>/);
|
assert.match(panel, /client-devices-reset-wrap client-tooltip-anchor[\s\S]*aria-label="Обнулить трафик устройств"[\s\S]*<Tooltip>Обнулить трафик устройств<\/Tooltip>/);
|
||||||
assert.doesNotMatch(panel, /className="client-devices-reset"[\s\S]{0,320}<span>/);
|
assert.doesNotMatch(panel, /className="client-devices-reset"[\s\S]{0,320}<span>/);
|
||||||
assert.match(styles, /\.client-devices-kicker \{[^}]*flex-wrap: nowrap/);
|
assert.match(styles, /\.client-devices-kicker \{[^}]*flex-wrap: nowrap/);
|
||||||
assert.match(styles, /\.client-devices-reset-wrap \{[^}]*margin-left: auto/);
|
assert.match(styles, /\.client-devices-reset-wrap \{[^}]*margin-left: auto/);
|
||||||
|
assert.match(styles, /\.client-devices-reset-wrap > \.client-tooltip \{[^}]*white-space: nowrap/);
|
||||||
assert.match(styles, /\.client-devices-reset \{[\s\S]*oklch\(0\.68 0\.15 28\)[\s\S]*\.client-devices-reset:hover:not\(:disabled\) svg[\s\S]*rotate\(-360deg\)/);
|
assert.match(styles, /\.client-devices-reset \{[\s\S]*oklch\(0\.68 0\.15 28\)[\s\S]*\.client-devices-reset:hover:not\(:disabled\) svg[\s\S]*rotate\(-360deg\)/);
|
||||||
assert.match(styles, /@media \(prefers-reduced-motion: reduce\)[\s\S]*\.client-device-policy svg[\s\S]*\.client-device-alias-trigger[\s\S]*\.client-device-identity-details[\s\S]*\.client-device-identity-copy[\s\S]*\.client-device-traffic-value > span[\s\S]*\.client-device-traffic-breakdown[\s\S]*\.client-device-traffic-lines[\s\S]*\.client-text-morph-value[\s\S]*transition: none;[\s\S]*animation: none/);
|
assert.match(styles, /@media \(prefers-reduced-motion: reduce\)[\s\S]*\.client-device-policy svg[\s\S]*\.client-device-alias-trigger[\s\S]*\.client-device-identity-details[\s\S]*\.client-device-identity-copy[\s\S]*\.client-device-traffic-value > span[\s\S]*\.client-device-traffic-breakdown[\s\S]*\.client-device-traffic-lines[\s\S]*\.client-text-morph-value[\s\S]*transition: none;[\s\S]*animation: none/);
|
||||||
assert.match(styles, /\.client-drawer \{[\s\S]*z-index: 50;[\s\S]*box-shadow:/);
|
assert.match(styles, /\.client-drawer \{[\s\S]*z-index: 50;[\s\S]*box-shadow:/);
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ test('server picker has one public feature owner without legacy shims', () => {
|
|||||||
assert.match(overview, /const selectedServerId = desiredProfile\?\.desiredServerId \|\| ''/);
|
assert.match(overview, /const selectedServerId = desiredProfile\?\.desiredServerId \|\| ''/);
|
||||||
assert.match(overview, /function selectServer\(profile: ProfileSnapshot, serverId: string\)[\s\S]*if \(connected && !gatewayDirect\)[\s\S]*onApply\(profile\.id, serverId\)[\s\S]*onSelectProfileServer\(profile\.id, serverId\)/);
|
assert.match(overview, /function selectServer\(profile: ProfileSnapshot, serverId: string\)[\s\S]*if \(connected && !gatewayDirect\)[\s\S]*onApply\(profile\.id, serverId\)[\s\S]*onSelectProfileServer\(profile\.id, serverId\)/);
|
||||||
assert.match(overview, /selectedServerId=\{pickerState\.selectedServerId\}/);
|
assert.match(overview, /selectedServerId=\{pickerState\.selectedServerId\}/);
|
||||||
|
assert.doesNotMatch([picker, overview, styles].join('\n'), /client-server-prompt|prompt=\{|prompt: boolean/);
|
||||||
});
|
});
|
||||||
|
|
||||||
const fixtures = (count) => Array.from({ length: count }, (_, index) => ({
|
const fixtures = (count) => Array.from({ length: count }, (_, index) => ({
|
||||||
|
|||||||
@@ -37,26 +37,26 @@ const expectedImports = [
|
|||||||
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
|
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
|
||||||
const acceptedLedger = {
|
const acceptedLedger = {
|
||||||
counts: {
|
counts: {
|
||||||
cascadeEdges: 823,
|
cascadeEdges: 822,
|
||||||
customProperties: 103,
|
customProperties: 103,
|
||||||
declarations: 3306,
|
declarations: 3299,
|
||||||
important: 0,
|
important: 0,
|
||||||
keyframes: 56,
|
keyframes: 56,
|
||||||
media: 13,
|
media: 13,
|
||||||
rules: 950,
|
rules: 949,
|
||||||
variableReferences: 812,
|
variableReferences: 808,
|
||||||
},
|
},
|
||||||
hashes: {
|
hashes: {
|
||||||
cascadeEdges: '52014b4f05da82ed3e1404ac7e81e2e7dd0960431828eb22fcdcd99ea4009683',
|
cascadeEdges: '03bede7036ed12ad1ccb5ee41188834934f8d9acd2d02296c5945dfc195bac71',
|
||||||
customProperties: 'ef70fb1d9b61b177f1939f811babe1116bee631de26f5eac0aa0d0009ca5a1fb',
|
customProperties: 'ef70fb1d9b61b177f1939f811babe1116bee631de26f5eac0aa0d0009ca5a1fb',
|
||||||
declarations: '568631ecf7ea61652c5fbb5f6d947e8a08fff1a179a72fb2dd6439e0e9cac950',
|
declarations: 'fc35daf13ffc5a0754f4e6f4982a5ff0c09c8664dfd86fd144e12c02fb006794',
|
||||||
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
|
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
|
||||||
duplicateSelectors: '257eff4727dab9ce25f4e1f9320e89bf2270eb29feae921b96601f103ad7f036',
|
duplicateSelectors: '0fa66e370695261a2a5c6a189752c07d518d80d68d3e79a3ba4bb34407893c0e',
|
||||||
keyframes: 'd4378751f36eccc87923c12540315462055032a5d34978a0f45ecada0a5cc1ce',
|
keyframes: 'd4378751f36eccc87923c12540315462055032a5d34978a0f45ecada0a5cc1ce',
|
||||||
ruleDeclarationSequences: 'a53ac1a62e6c88fd923971802e9ec5238c8d5d285b6fbecce1d54bbd38d91e2c',
|
ruleDeclarationSequences: 'af072317748091c47aaf33de5d124249367ca37e5037dd58eab869ca0fc26130',
|
||||||
selectors: '190b90a75c4dfedc9ea66e37046fbceba70b0adbbdf89872db8f83ce93119114',
|
selectors: '84116a78a2c71322a4e5ea45a7cdec788ca1d0d02c46767eb1c642b7e0755754',
|
||||||
variableReferences: 'd3a740a583156df0b42ce0f52687617a5b5507b394251070c63d355560921f03',
|
variableReferences: '664c093241c4076ca05a9dda06ae88d4e7024c926c4636109790201df8212df4',
|
||||||
witnesses: 'ae24f63221c956797fcd18706d3f318f15d75ffded0297fdbeb81487e6c7875e',
|
witnesses: 'b31a875d6be7980f6bb8eb46dac2d9665f6fe9afaa7775a4f994a0156109734d',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -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, 808);
|
assert.equal(witnesses.length, 805);
|
||||||
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-B8MBAMBM.css']);
|
assert.deepEqual(assets, ['index-9maKdwRg.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, 127884);
|
assert.equal(built.byteLength, 127604);
|
||||||
assert.equal(sha256(built), 'ec9b242f2a01e75970ec6dee43a037764835c9032a1ea6cfa97df3c7735f76ba');
|
assert.equal(sha256(built), '0ba84e80e387ee62c33379b3674b1849ba65857cf45dac1257f4d87605008d04');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -108,6 +108,8 @@ 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, /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.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, /\{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 \|\| ''/);
|
||||||
|
|||||||
Reference in New Issue
Block a user