Add DNS diagnostics across dataplane and client
Build and Deploy Gateway / build-and-push (push) Successful in 31s
Build and Deploy Gateway / deploy (push) Successful in 13s

This commit is contained in:
2026-09-01 04:16:15 +03:00
parent e0bdafd25e
commit 76a99f098a
23 changed files with 1888 additions and 35 deletions
+29 -2
View File
@@ -192,7 +192,15 @@ test('connectivity use case keeps applied priority, selected fallback and error
test('diagnostics settings validate and replace one canonical revision', () => {
let state = {
revision: 7,
diagnostics: { configured: false, customServices: [], hiddenServiceIds: [] },
diagnostics: {
configured: false,
customServices: [],
hiddenServiceIds: [],
customDnsResolvers: [{
id: 'custom-dns-office', label: 'Office', kind: 'dns', endpoint: '192.168.1.1',
}],
customDnsDomains: [{ id: 'custom-domain-office', label: 'Office', hostname: 'office.example.com' }],
},
};
const useCase = createConnectivityDiagnosticsUseCase({
state: {
@@ -212,6 +220,10 @@ test('diagnostics settings validate and replace one canonical revision', () => {
configured: true,
customServices: [{ id: 'custom-status', label: 'Status', url: 'https://example.com/status' }],
hiddenServiceIds: ['speedtest'],
customDnsResolvers: [{
id: 'custom-dns-office', label: 'Office', kind: 'dns', endpoint: '192.168.1.1',
}],
customDnsDomains: [{ id: 'custom-domain-office', label: 'Office', hostname: 'office.example.com' }],
});
assert.equal(state.revision, 8);
assert.throws(() => useCase.updateSettings({ customServices: [], hiddenServiceIds: [] }, 7), {
@@ -245,6 +257,11 @@ test('connectivity route preserves exact URL, defaults and raw response', async
calls.push(args);
return { checkedAt: 'now', vpn: { server: null } };
},
dnsCatalog: async () => ({ resolvers: [{ id: 'google-dns' }], domains: [] }),
runDns: async (...args) => {
calls.push(['dns', ...args]);
return { checkedAt: 'dns-now', results: [] };
},
updateSettings: (...args) => calls.push(['settings', ...args]),
},
readBody: async () => {
@@ -276,6 +293,16 @@ test('connectivity route preserves exact URL, defaults and raw response', async
assert.deepEqual(calls.at(-1), ['settings', body.settings, 4]);
assert.equal(settingsResponse.payload.success, true);
const catalogResponse = routeResponse();
assert.equal(await route.handle({ method: 'GET', url: '/api/diagnostics/dns' }, catalogResponse), true);
assert.deepEqual(catalogResponse.payload, { resolvers: [{ id: 'google-dns' }], domains: [] });
body = { domainId: 'youtube', resolverId: 'google-dns' };
const dnsResponse = routeResponse();
assert.equal(await route.handle({ method: 'POST', url: '/api/diagnostics/dns' }, dnsResponse), true);
assert.deepEqual(calls.at(-1), ['dns', 'youtube', 'google-dns']);
assert.deepEqual(dnsResponse.payload, { checkedAt: 'dns-now', results: [] });
for (const [method, url] of [
['GET', '/api/diagnostics/connectivity'],
['POST', '/api/diagnostics/connectivity?target=all'],
@@ -284,7 +311,7 @@ test('connectivity route preserves exact URL, defaults and raw response', async
]) {
assert.equal(await route.handle({ method, url }, routeResponse()), false);
}
assert.equal(bodyReads, 3);
assert.equal(bodyReads, 4);
});
test('a targeted IP row uses three samples and keeps the majority address', async () => {
+16 -4
View File
@@ -34,6 +34,8 @@ test('control uses the dataplane socket protocol', async () => {
[{ id: 'custom-test', url: 'https://example.com' }],
'site:custom-test',
);
await client.getDnsDiagnosticsCatalog([{ id: 'dns' }], [{ id: 'domain' }]);
await client.runDnsDiagnostics([{ id: 'dns' }], [{ id: 'domain' }], 'youtube', 'google-dns');
await client.checkConfig({ outbounds: [] });
await client.runFailoverProbe('primary', [], 'site:youtube', 9_000);
await client.readFailoverSelector();
@@ -53,6 +55,8 @@ test('control uses the dataplane socket protocol', async () => {
'GET /device-policy /run/dataplane.sock',
'PUT /device-policy /run/dataplane.sock',
'POST /diagnostics/connectivity /run/dataplane.sock',
'POST /diagnostics/dns/catalog /run/dataplane.sock',
'POST /diagnostics/dns /run/dataplane.sock',
'POST /config/check /run/dataplane.sock',
'POST /failover/probe /run/dataplane.sock',
'GET /failover/selector /run/dataplane.sock',
@@ -68,10 +72,18 @@ test('control uses the dataplane socket protocol', async () => {
target: 'site:custom-test',
});
assert.equal(requests[8].timeoutMs, 25_000);
assert.deepEqual(requests[9].body, { config: { outbounds: [] } });
assert.deepEqual(requests[10].body, { role: 'primary', services: [], target: 'site:youtube', timeoutMs: 9_000 });
assert.equal(requests[10].timeoutMs, 19_000);
assert.deepEqual(requests[12].body, { role: 'reserve' });
assert.deepEqual(requests[9].body, {
customResolvers: [{ id: 'dns' }], customDomains: [{ id: 'domain' }],
});
assert.deepEqual(requests[10].body, {
customResolvers: [{ id: 'dns' }], customDomains: [{ id: 'domain' }],
domainId: 'youtube', resolverId: 'google-dns',
});
assert.equal(requests[10].timeoutMs, 40_000);
assert.deepEqual(requests[11].body, { config: { outbounds: [] } });
assert.deepEqual(requests[12].body, { role: 'primary', services: [], target: 'site:youtube', timeoutMs: 9_000 });
assert.equal(requests[12].timeoutMs, 19_000);
assert.deepEqual(requests[14].body, { role: 'reserve' });
});
test('connectivity diagnostics expose a retryable domain error', async () => {
+208
View File
@@ -0,0 +1,208 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import test from 'node:test';
import {
buildDnsQuery,
createDnsDiagnosticsService,
parseDnsResponse,
} from '../../dist/server/services/dnsDiagnosticsService.js';
function wireResponse(query, { rcode = 0, truncated = false, address = null } = {}) {
const header = Buffer.alloc(12);
query.copy(header, 0, 0, 2);
header.writeUInt16BE(0x8180 | rcode | (truncated ? 0x0200 : 0), 2);
header.writeUInt16BE(1, 4);
header.writeUInt16BE(address ? 1 : 0, 6);
const question = query.subarray(12);
if (!address) return Buffer.concat([header, question]);
const type = query.readUInt16BE(query.length - 4);
const data = type === 1
? Buffer.from(address.split('.').map(Number))
: Buffer.from(address.split(':').flatMap((part) => {
const value = Number.parseInt(part || '0', 16);
return [value >> 8, value & 0xff];
}));
const answer = Buffer.alloc(12);
answer.writeUInt16BE(0xc00c, 0);
answer.writeUInt16BE(type, 2);
answer.writeUInt16BE(1, 4);
answer.writeUInt32BE(60, 6);
answer.writeUInt16BE(data.length, 10);
return Buffer.concat([header, question, answer, data]);
}
function addressFor(packet, path = 'direct') {
return packet.readUInt16BE(packet.length - 4) === 1
? path === 'direct' ? '8.8.8.8' : '9.9.9.9'
: path === 'direct' ? '2001:db8:0:0:0:0:0:1' : '2001:db8:0:0:0:0:0:2';
}
test('DNS wire codec parses compressed A/AAAA and rejects malformed packets', () => {
const aQuery = buildDnsQuery('www.youtube.com', 'A', 17);
const aaaaQuery = buildDnsQuery('www.youtube.com', 'AAAA', 18);
assert.deepEqual(parseDnsResponse(wireResponse(aQuery, { address: '8.8.8.8' }), 17), {
rcode: 'NOERROR', truncated: false, ipv4: ['8.8.8.8'], ipv6: [],
});
assert.deepEqual(parseDnsResponse(wireResponse(aaaaQuery, {
address: '2001:db8:0:0:0:0:0:1',
}), 18).ipv6, ['2001:db8:0:0:0:0:0:1']);
assert.equal(parseDnsResponse(wireResponse(aQuery, { rcode: 3 }), 17).rcode, 'NXDOMAIN');
assert.equal(parseDnsResponse(wireResponse(aQuery, { rcode: 2 }), 17).rcode, 'SERVFAIL');
assert.throws(() => parseDnsResponse(Buffer.from([0, 1]), 1), /malformed/);
assert.throws(() => parseDnsResponse(wireResponse(aQuery), 99), /unexpected/);
});
test('DNS checker keeps Direct and VPN independent and never probes VPN when it is off', async () => {
const calls = [];
const classicExchange = async ({ path: route, resolver, packet }) => {
calls.push([route, resolver.endpoint, packet.readUInt16BE(packet.length - 4)]);
return {
response: wireResponse(packet, { address: addressFor(packet, route) }),
transport: 'udp',
latencyMs: route === 'direct' ? 10 : 20,
};
};
const service = createDnsDiagnosticsService({
proxyPort: 18080, getServers: () => [], classicExchange,
now: () => '2026-09-01T00:00:00.000Z',
});
const running = await service.run({ vpnAvailable: true, domainId: 'youtube', resolverId: 'google-dns' });
assert.deepEqual(calls.map(([route]) => route).sort(), ['direct', 'direct', 'vpn', 'vpn']);
assert.equal(running.results[0].comparison, 'different');
assert.deepEqual(running.results[0].direct.ipv4, ['8.8.8.8']);
assert.deepEqual(running.results[0].vpn.ipv6, ['2001:db8:0:0:0:0:0:2']);
calls.length = 0;
const stopped = await service.run({ vpnAvailable: false, domainId: 'youtube', resolverId: 'google-dns' });
assert.deepEqual(calls.map(([route]) => route), ['direct', 'direct']);
assert.equal(stopped.results[0].vpn.status, 'vpn-off');
});
test('DNS checker retries timeout once and uses TCP fallback on a truncated response', async () => {
let attempts = 0;
const timedOut = createDnsDiagnosticsService({
proxyPort: 18080,
getServers: () => [],
classicExchange: async () => {
attempts += 1;
const error = new Error('timeout');
error.code = 'ETIMEDOUT';
throw error;
},
});
const timeoutResult = await timedOut.run({
vpnAvailable: false, domainId: 'youtube', resolverId: 'google-dns',
});
assert.equal(attempts, 4);
assert.equal(timeoutResult.results[0].direct.status, 'timeout');
const fallbackCalls = [];
const fallback = createDnsDiagnosticsService({
proxyPort: 18080,
getServers: () => [],
classicExchange: async ({ packet }) => ({
response: wireResponse(packet, { truncated: true }), transport: 'udp', latencyMs: 2,
}),
tcpExchange: async ({ path: route, packet }) => {
fallbackCalls.push(route);
return {
response: wireResponse(packet, { address: addressFor(packet, route) }),
transport: 'tcp', latencyMs: 4,
};
},
});
const fallbackResult = await fallback.run({
vpnAvailable: true, domainId: 'youtube', resolverId: 'google-dns',
});
assert.deepEqual(fallbackCalls.sort(), ['direct', 'direct', 'vpn', 'vpn']);
assert.equal(fallbackResult.results[0].direct.transport, 'tcp');
assert.equal(fallbackResult.results[0].vpn.transport, 'tcp');
});
test('custom DoH is pinned only to public bootstrap addresses', async () => {
const resolver = {
id: 'custom-dns-office', label: 'Office DoH', kind: 'doh', endpoint: 'https://dns.example/dns-query',
};
const calls = [];
const service = createDnsDiagnosticsService({
proxyPort: 18080,
getServers: () => [],
lookup: async () => [{ address: '93.184.216.34', family: 4 }],
dohExchange: async ({ path: route, resolver: pinned, packet }) => {
calls.push([route, pinned.bootstrap]);
return {
response: wireResponse(packet, { address: addressFor(packet, route) }),
transport: 'doh', latencyMs: 7,
};
},
});
const result = await service.run({
vpnAvailable: true, customResolvers: [resolver], domainId: 'chatgpt', resolverId: resolver.id,
});
assert.deepEqual(calls, [
['direct', '93.184.216.34'], ['direct', '93.184.216.34'],
['vpn', '93.184.216.34'], ['vpn', '93.184.216.34'],
]);
assert.equal(result.results[0].direct.transport, 'doh');
let requests = 0;
const blocked = createDnsDiagnosticsService({
proxyPort: 18080,
getServers: () => [],
lookup: async () => [{ address: '127.0.0.1', family: 4 }],
dohExchange: async () => { requests += 1; throw new Error('unexpected'); },
});
const blockedResult = await blocked.run({
vpnAvailable: true, customResolvers: [resolver], domainId: 'chatgpt', resolverId: resolver.id,
});
assert.equal(requests, 0);
assert.equal(blockedResult.results[0].direct.error, 'DoH endpoint is not public');
});
test('full DNS run includes system and built-in rows with at most three resolver workers', async () => {
const active = new Map();
let maximumResolvers = 0;
const exchange = async ({ path: route, resolver, packet }) => {
active.set(resolver.id, (active.get(resolver.id) || 0) + 1);
maximumResolvers = Math.max(maximumResolvers, active.size);
await new Promise((resolve) => setTimeout(resolve, 2));
const count = active.get(resolver.id) - 1;
if (count) active.set(resolver.id, count);
else active.delete(resolver.id);
return {
response: wireResponse(packet, { address: addressFor(packet, route) }),
transport: resolver.kind === 'doh' ? 'doh' : 'udp',
latencyMs: 2,
};
};
const service = createDnsDiagnosticsService({
proxyPort: 18080,
getServers: () => ['192.168.1.1'],
classicExchange: exchange,
dohExchange: exchange,
});
const catalog = await service.catalog();
assert.equal(catalog.resolvers[0].system, true);
assert.equal(catalog.resolvers.length, 7);
assert.deepEqual(catalog.domains.map(({ hostname }) => hostname), ['www.youtube.com', 'chatgpt.com']);
const result = await service.run({ vpnAvailable: false, domainId: 'youtube' });
assert.equal(result.results.length, 7);
assert.ok(maximumResolvers <= 3);
});
test('default transports enforce direct bypass, VPN proxy, binary DoH and no redirects', () => {
const source = fs.readFileSync(
path.resolve(import.meta.dirname, '../../src/server/services/dnsDiagnosticsService.ts'),
'utf8',
);
assert.match(source, /path === 'direct'[\s\S]*udpExchange\(address, port/);
assert.match(source, /socksHandshake\(3,[\s\S]*socksHandshake\(1,/);
assert.match(source, /'--noproxy', '\*'/);
assert.match(source, /'--proxy', `http:\/\/127\.0\.0\.1:\$\{proxyPort\}`/);
assert.match(source, /'--data-binary', '@-'/);
assert.match(source, /'--resolve'/);
assert.doesNotMatch(source, /--location/);
});
+31
View File
@@ -102,6 +102,8 @@ test('schema v5 migrates rules and diagnostics settings with an exact backup', (
configured: false,
customServices: [],
hiddenServiceIds: [],
customDnsResolvers: [],
customDnsDomains: [],
});
assert.equal(migrated.routeRulesRevision, 7);
assert.deepEqual(migrated.routeRules.map(({ outbound }) => outbound), ['direct', 'direct']);
@@ -149,6 +151,35 @@ test('schema v7 migrates failover disabled without losing canonical state', (t)
assert.deepEqual(JSON.parse(fs.readFileSync(store.migration.backupPath, 'utf8')), legacy);
});
test('schema v9 adds empty DNS settings atomically and preserves the exact backup', (t) => {
const filePath = fixture(t);
const legacy = {
schemaVersion: 9,
revision: 23,
routeRules: [],
appliedRouteRules: [],
diagnostics: {
configured: true,
customServices: [{ id: 'custom-status', label: 'Status', url: 'https://example.com/' }],
hiddenServiceIds: ['youtube'],
},
};
const bytes = JSON.stringify(legacy);
fs.writeFileSync(filePath, bytes);
const store = createStateStore(filePath, {
now: () => new Date('2026-09-01T12:00:00.000Z'),
});
const migrated = store.read();
assert.equal(migrated.schemaVersion, 10);
assert.equal(migrated.revision, 23);
assert.deepEqual(migrated.diagnostics.customDnsResolvers, []);
assert.deepEqual(migrated.diagnostics.customDnsDomains, []);
assert.equal(store.migration.fromVersion, 9);
assert.equal(fs.readFileSync(store.migration.backupPath, 'utf8'), bytes);
});
test('schema v6 rejects missing or unknown outbound without rewriting source bytes', (t) => {
for (const [name, rule] of [
['missing', { type: 'domain', value: 'example.com', enabled: true }],
@@ -12,6 +12,7 @@ const panel = fs.readFileSync(path.join(root, 'src/web/features/diagnostics/Conn
const model = fs.readFileSync(path.join(root, 'src/web/features/diagnostics/connectivityResult.ts'), 'utf8');
const customServiceAction = fs.readFileSync(path.join(root, 'src/web/features/diagnostics/customServiceAction.ts'), 'utf8');
const boundary = fs.readFileSync(path.join(root, 'src/web/features/diagnostics/index.ts'), 'utf8');
const dns = fs.readFileSync(path.join(root, 'src/web/features/diagnostics/DnsDiagnosticsSection.tsx'), 'utf8');
const ip = { source: 'cloudflare', address: '198.51.100.10', extra: true };
const site = { id: 'google', status: 'available', httpStatus: 204, latencyMs: 120 };
@@ -122,3 +123,19 @@ test('network identity is one stable compact row for Direct and VPN', () => {
assert.match(panel, /else if \(path\.network == null\) status = \['is-muted', '—'\][\s\S]*if \(!status && !identity && !location\) status = \['is-error', 'Нет ответа'\]/);
assert.doesNotMatch(panel, /traceroute|tracepath/);
});
test('DNS diagnostics reuse the drawer owner, canonical settings and independent row refresh', () => {
assert.match(panel, /<DnsDiagnosticsSection[\s\S]*settings=\{settings\}[\s\S]*updateSettings=\{updateSettings\}/);
assert.match(panel, /<DnsDiagnosticsSection[\s\S]*loadCatalog=\{loadDnsDiagnosticsCatalog\}[\s\S]*runDiagnostics=\{runDnsDiagnostics\}/);
assert.match(dns, /<select[\s\S]*id="client-dns-domain"[\s\S]*value=\{activeDomainId\}/);
assert.match(dns, /setCache\(\(current\)[\s\S]*mergeRow\(previous\[row\.resolver\.id\], row\)/);
assert.match(dns, /function preserveAddresses[\s\S]*ipv4: previous\.ipv4, ipv6: previous\.ipv6/);
assert.match(dns, /aria-busy=\{running\}/);
assert.match(dns, /<Refresh label=\{`Проверить: \$\{item\.label\}`\}[\s\S]*onClick=\{\(\) => void run\(item\.id\)\}/);
assert.match(dns, /updateSettings\([\s\S]*customDnsDomains/);
assert.match(dns, /updateSettings\([\s\S]*customDnsResolvers/);
assert.match(dns, /settings\.customDnsDomains\.length >= MAX_CUSTOM_DNS_DOMAINS/);
assert.match(dns, /settings\.customDnsResolvers\.length >= MAX_CUSTOM_DNS_RESOLVERS/);
assert.match(dns, /<b>A<\/b>[\s\S]*<b>AAAA<\/b>/);
assert.match(dns, /Разные ответы могут быть нормой для CDN/);
});
@@ -15,6 +15,7 @@ const routing = fs.readFileSync(path.join(root, 'src/web/features/routing/Routin
const devices = fs.readFileSync(path.join(root, 'src/web/features/devices/DevicesFeature.tsx'), 'utf8');
const diagnosticsFeature = fs.readFileSync(path.join(root, 'src/web/features/diagnostics/DiagnosticsFeature.tsx'), 'utf8');
const diagnostics = fs.readFileSync(path.join(root, 'src/web/features/diagnostics/ConnectivityDiagnosticsPanel.tsx'), 'utf8');
const dnsDiagnostics = fs.readFileSync(path.join(root, 'src/web/features/diagnostics/DnsDiagnosticsSection.tsx'), 'utf8');
const instructions = fs.readFileSync(path.join(root, 'src/web/features/instructions/InstructionsFeature.tsx'), 'utf8');
function rule(selector, source = styles) {
@@ -264,6 +265,15 @@ test('responsive motion has a reduced-motion fallback', () => {
assert.doesNotMatch(styles, /\.client-rail-action:active:not\(:disabled\) svg/);
});
test('DNS keeps three result columns while its add forms stack on narrow screens', () => {
assert.match(styles, /\.client-dns-row \{[\s\S]*min-height:\s*92px/);
assert.match(styles, /\.client-dns-result \{[\s\S]*min-height:\s*72px/);
assert.match(styles, /\.client-dns-header,\s*\.client-dns-row \{[\s\S]*grid-template-columns:\s*35% minmax\(0, 1fr\) minmax\(0, 1fr\) 28px/);
assert.match(styles, /@media \(max-width: 560px\)[\s\S]*\.client-dns-inline-form \{[\s\S]*grid-template-columns:\s*minmax\(0, 1fr\) 44px/);
assert.match(styles, /@media \(max-width: 560px\)[\s\S]*\.client-dns-row \{[\s\S]*grid-template-columns:\s*32% minmax\(0, 1fr\) minmax\(0, 1fr\) 44px/);
assert.match(dnsDiagnostics, /role="table" aria-busy=\{running\}/);
});
test('tooltips stay opaque, above adjacent content, and do not stick after pointer clicks', () => {
const tooltip = rule('.client-tooltip');
+15 -19
View File
@@ -40,26 +40,26 @@ const expectedImports = [
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
const acceptedLedger = {
counts: {
cascadeEdges: 1163,
cascadeEdges: 1188,
customProperties: 115,
declarations: 4746,
declarations: 4862,
important: 0,
keyframes: 55,
media: 23,
rules: 1288,
variableReferences: 1237,
rules: 1329,
variableReferences: 1270,
},
hashes: {
cascadeEdges: 'c1b3eaa3d0ad08e3393cf90878f90e17a5401d79eed9072d06f39b7841d8bbfd',
cascadeEdges: '3d02c10ab8fd95f7eed306048e9b59ec391a0febaa9e3e235badcff3b042eef7',
customProperties: '81d1e70737ae5a8e4b20d4c1be1b24685975a404863444da4a8821be1f0d5097',
declarations: 'e668191e548093c02463e57b81eb6c12b826991a5e10f5191d87b73e682cd852',
declarations: '0f31e2cdeeab02b9e0a845a6b50ff7e3f035a441217a418e7b5465b5a296ae16',
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
duplicateSelectors: '8982145dba05b33bf1c93b2d54cfbe76305b276ff3c657aa020144016ada5848',
keyframes: 'b9b0bf3fad92e69b93ec0c24f01da15e78bbe89a095597d64ce4f7ef5e272fdd',
ruleDeclarationSequences: '2cc97550fecd8bbf46217dc378cc490fd41d977abcdcc4c93a18c7593ba42f53',
selectors: 'c7f4752d81bbeb04612eb6c79a42bcde875c762d81591b2cdf2fb2bbd7399ea2',
variableReferences: '69c03c059f9ce107bc8be0397cff90e1b8b3749b8e37ba8e231e2d6cdcd61e46',
witnesses: '99713805eb7a5c67427ace32775f0cc503b3b2ac63240db717e71b36d09ddd68',
ruleDeclarationSequences: '33a0dff2428a8e712776d40d796ea05d169544eb1af02ad350759061fb2a3c3c',
selectors: '5734c69a504c59025e2e6b1637cfb6d1905ca1ae358783d0916a694a973eea22',
variableReferences: '49ad724b2812128f5344fdd55b177aa96ea9b30eb4d878dc86f2e4f3fa898182',
witnesses: '4e357ee4f7310d60399baa3099bd8f4ed320debc1e3c9a09f55b892a1178049c',
},
};
@@ -212,7 +212,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', () => {
const witnesses = readStyleWitnesses(root);
assert.equal(witnesses.length, 1312);
assert.equal(witnesses.length, 1375);
assert.equal(witnesses.filter((witness) => witness.unknown || witness.ancestorUnknown).length, 0);
const ledger = createStyleLedger(readStyleSource(root), { witnesses });
assert.deepEqual(ledger.counts, acceptedLedger.counts);
@@ -221,11 +221,7 @@ test('accepted stylesheet has pinned declaration, selector, keyframe, variable,
test('every live production selector has an expanded DOM witness', () => {
const unmatched = selectorsWithoutWitness(readStyleSource(root), readStyleWitnesses(root));
assert.deepEqual(unmatched, [
'.client-diagnostics-section-title button',
'.client-diagnostics-section-title button:disabled',
'.client-diagnostics-section-title button:focus-visible',
]);
assert.deepEqual(unmatched, []);
});
test('JSX witness expansion follows cross-file components, render props, ReactNode slots, portals, and imperative classes', () => {
@@ -409,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);
const assets = fs.readdirSync(path.join(root, 'dist/assets')).filter((file) => file.endsWith('.css'));
assert.deepEqual(assets, ['index-BLRRtFYh.css']);
assert.deepEqual(assets, ['index-fllV-PfI.css']);
const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0]));
assert.equal(built.byteLength, 180634);
assert.equal(sha256(built), '6bf3cb9c724758de39567c606c939e6041edde82837f826e8d2d4e31ebd1d8a8');
assert.equal(built.byteLength, 184958);
assert.equal(sha256(built), '3a9ed6fdf6c5d81234de9aa0abd995bc9db1cfe298194162654edcbfe3ae7c62');
});
+1 -1
View File
@@ -32,7 +32,7 @@ test('all repeated client primitive consumers use the shared owners', () => {
.map(({ source }) => source)
.join('\n');
assert.equal((production.match(/<Tooltip\b/g) || []).length, 16);
assert.equal((production.match(/<Tooltip\b/g) || []).length, 17);
assert.equal((production.match(/<CopyButton\b/g) || []).length, 2);
assert.equal((production.match(/<RailAction\b/g) || []).length, 8);
assert.equal((production.match(/<Drawer\b/g) || []).length, 8);