@rstest/browser 0.8.2 → 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -44,6 +44,19 @@ type RsbuildDevServer = rsbuild.RsbuildDevServer;
44
44
  type RsbuildInstance = rsbuild.RsbuildInstance;
45
45
 
46
46
  const __dirname = dirname(fileURLToPath(import.meta.url));
47
+ const OPTIONS_PLACEHOLDER = '__RSTEST_OPTIONS_PLACEHOLDER__';
48
+
49
+ /**
50
+ * Serialize JSON for inline <script> injection.
51
+ * Escapes '<' to prevent accidental </script> break-out.
52
+ * Escapes U+2028/U+2029 to keep script parsing safe.
53
+ */
54
+ const serializeForInlineScript = (value: unknown): string => {
55
+ return JSON.stringify(value)
56
+ .replace(/</g, '\\u003c')
57
+ .replace(/\u2028/g, '\\u2028')
58
+ .replace(/\u2029/g, '\\u2029');
59
+ };
47
60
 
48
61
  // ============================================================================
49
62
  // Type Definitions
@@ -716,6 +729,26 @@ const htmlTemplate = `<!DOCTYPE html>
716
729
  </html>
717
730
  `;
718
731
 
732
+ const fallbackSchedulerHtmlTemplate = `<!DOCTYPE html>
733
+ <html lang="en">
734
+ <head>
735
+ <meta charset="UTF-8" />
736
+ <title>Rstest Browser Scheduler</title>
737
+ <script>
738
+ window.__RSTEST_BROWSER_OPTIONS__ = ${OPTIONS_PLACEHOLDER};
739
+ </script>
740
+ </head>
741
+ <body>
742
+ <script type="module" src="/container-static/js/scheduler.js"></script>
743
+ </body>
744
+ </html>
745
+ `;
746
+
747
+ // Workaround for noisy "removed ..." logs caused by VirtualModulesPlugin.
748
+ // Rsbuild suppresses the removed-file log if all removed paths include "virtual":
749
+ // https://github.com/web-infra-dev/rsbuild/blob/1258fa9dba5c321a4629b591a6dadbd2e26c6963/packages/core/src/createCompiler.ts#L73-L76
750
+ const VIRTUAL_MANIFEST_FILENAME = 'virtual-manifest.ts';
751
+
719
752
  // ============================================================================
720
753
  // Browser Runtime Lifecycle
721
754
  // ============================================================================
@@ -795,22 +828,30 @@ const createBrowserRuntime = async ({
795
828
  [manifestPath]: manifestSource,
796
829
  });
797
830
 
798
- const optionsPlaceholder = '__RSTEST_OPTIONS_PLACEHOLDER__';
799
831
  const containerHtmlTemplate = containerDistPath
800
- ? await fs.readFile(join(containerDistPath, 'container.html'), 'utf-8')
832
+ ? await fs.readFile(join(containerDistPath, 'index.html'), 'utf-8')
833
+ : null;
834
+ const schedulerHtmlTemplate = containerDistPath
835
+ ? await fs
836
+ .readFile(join(containerDistPath, 'scheduler.html'), 'utf-8')
837
+ .catch(() => null)
801
838
  : null;
802
839
 
803
840
  let injectedContainerHtml: string | null = null;
841
+ let injectedSchedulerHtml: string | null = null;
804
842
  let serializedOptions = 'null';
805
843
 
806
844
  const setContainerOptions = (options: BrowserHostConfig): void => {
807
- serializedOptions = JSON.stringify(options).replace(/</g, '\\u003c');
845
+ serializedOptions = serializeForInlineScript(options);
808
846
  if (containerHtmlTemplate) {
809
847
  injectedContainerHtml = containerHtmlTemplate.replace(
810
- optionsPlaceholder,
848
+ OPTIONS_PLACEHOLDER,
811
849
  serializedOptions,
812
850
  );
813
851
  }
852
+ injectedSchedulerHtml = (
853
+ schedulerHtmlTemplate || fallbackSchedulerHtmlTemplate
854
+ ).replace(OPTIONS_PLACEHOLDER, serializedOptions);
814
855
  };
815
856
 
816
857
  // Get user Rsbuild config from the first browser project
@@ -991,7 +1032,7 @@ const createBrowserRuntime = async ({
991
1032
  const serveContainer = containerDistPath
992
1033
  ? sirv(containerDistPath, {
993
1034
  dev: false,
994
- single: 'container.html',
1035
+ single: 'index.html',
995
1036
  })
996
1037
  : null;
997
1038
 
@@ -1015,7 +1056,7 @@ const createBrowserRuntime = async ({
1015
1056
  }
1016
1057
 
1017
1058
  let html = await response.text();
1018
- html = html.replace(optionsPlaceholder, serializedOptions);
1059
+ html = html.replace(OPTIONS_PLACEHOLDER, serializedOptions);
1019
1060
 
1020
1061
  res.statusCode = response.status;
1021
1062
  response.headers.forEach((value, key) => {
@@ -1092,14 +1133,26 @@ const createBrowserRuntime = async ({
1092
1133
  }
1093
1134
  return;
1094
1135
  }
1095
- if (url.pathname === '/') {
1136
+ if (url.pathname === '/' || url.pathname === '/scheduler.html') {
1096
1137
  if (await respondWithDevServerHtml(url, res)) {
1097
1138
  return;
1098
1139
  }
1099
1140
 
1141
+ if (url.pathname === '/scheduler.html') {
1142
+ res.setHeader('Content-Type', 'text/html');
1143
+ res.end(
1144
+ injectedSchedulerHtml ||
1145
+ (schedulerHtmlTemplate || fallbackSchedulerHtmlTemplate).replace(
1146
+ OPTIONS_PLACEHOLDER,
1147
+ 'null',
1148
+ ),
1149
+ );
1150
+ return;
1151
+ }
1152
+
1100
1153
  const html =
1101
1154
  injectedContainerHtml ||
1102
- containerHtmlTemplate?.replace(optionsPlaceholder, 'null');
1155
+ containerHtmlTemplate?.replace(OPTIONS_PLACEHOLDER, 'null');
1103
1156
 
1104
1157
  if (html) {
1105
1158
  res.setHeader('Content-Type', 'text/html');
@@ -1227,6 +1280,56 @@ export const runBrowserController = async (
1227
1280
  ): Promise<BrowserTestRunResult | void> => {
1228
1281
  const { skipOnTestRunEnd = false } = options ?? {};
1229
1282
  const buildStart = Date.now();
1283
+ const browserProjects = getBrowserProjects(context);
1284
+ const useSchedulerPage = browserProjects.every(
1285
+ (project) => project.normalizedConfig.browser.headless,
1286
+ );
1287
+
1288
+ /**
1289
+ * Build an error BrowserTestRunResult and call onTestRunEnd if needed.
1290
+ * Used for early-exit error paths to ensure errors reach the summary report.
1291
+ */
1292
+ const buildErrorResult = async (
1293
+ error: Error,
1294
+ ): Promise<BrowserTestRunResult> => {
1295
+ const elapsed = Math.max(0, Date.now() - buildStart);
1296
+ const errorResult: BrowserTestRunResult = {
1297
+ results: [],
1298
+ testResults: [],
1299
+ duration: { totalTime: elapsed, buildTime: elapsed, testTime: 0 },
1300
+ hasFailure: true,
1301
+ unhandledErrors: [error],
1302
+ };
1303
+
1304
+ if (!skipOnTestRunEnd) {
1305
+ for (const reporter of context.reporters) {
1306
+ await (reporter as Reporter).onTestRunEnd?.({
1307
+ results: [],
1308
+ testResults: [],
1309
+ duration: errorResult.duration,
1310
+ snapshotSummary: context.snapshotManager.summary,
1311
+ getSourcemap: async () => null,
1312
+ unhandledErrors: errorResult.unhandledErrors,
1313
+ });
1314
+ }
1315
+ }
1316
+
1317
+ return errorResult;
1318
+ };
1319
+
1320
+ const toError = (error: unknown): Error => {
1321
+ return error instanceof Error ? error : new Error(String(error));
1322
+ };
1323
+
1324
+ const failWithError = async (
1325
+ error: unknown,
1326
+ cleanup?: () => Promise<void>,
1327
+ ): Promise<BrowserTestRunResult> => {
1328
+ ensureProcessExitCode(1);
1329
+ await cleanup?.();
1330
+ return buildErrorResult(toError(error));
1331
+ };
1332
+
1230
1333
  const containerDevServerEnv = process.env.RSTEST_CONTAINER_DEV_SERVER;
1231
1334
  let containerDevServer: string | undefined;
1232
1335
  let containerDistPath: string | undefined;
@@ -1238,13 +1341,9 @@ export const runBrowserController = async (
1238
1341
  `[Browser UI] Using dev server for container: ${containerDevServer}`,
1239
1342
  );
1240
1343
  } catch (error) {
1241
- logger.error(
1242
- color.red(
1243
- `Invalid RSTEST_CONTAINER_DEV_SERVER value: ${String(error)}`,
1244
- ),
1245
- );
1246
- ensureProcessExitCode(1);
1247
- return;
1344
+ const originalError = toError(error);
1345
+ originalError.message = `Invalid RSTEST_CONTAINER_DEV_SERVER value: ${originalError.message}`;
1346
+ return failWithError(originalError);
1248
1347
  }
1249
1348
  }
1250
1349
 
@@ -1252,9 +1351,7 @@ export const runBrowserController = async (
1252
1351
  try {
1253
1352
  containerDistPath = resolveContainerDist();
1254
1353
  } catch (error) {
1255
- logger.error(color.red(String(error)));
1256
- ensureProcessExitCode(1);
1257
- return;
1354
+ return failWithError(error);
1258
1355
  }
1259
1356
  }
1260
1357
 
@@ -1296,8 +1393,8 @@ export const runBrowserController = async (
1296
1393
  'browser',
1297
1394
  Date.now().toString(),
1298
1395
  );
1299
- const manifestPath = join(tempDir, 'manifest.ts');
1300
1396
 
1397
+ const manifestPath = join(tempDir, VIRTUAL_MANIFEST_FILENAME);
1301
1398
  const manifestSource = generateManifestModule({
1302
1399
  manifestPath,
1303
1400
  entries: projectEntries,
@@ -1335,10 +1432,9 @@ export const runBrowserController = async (
1335
1432
  containerDevServer,
1336
1433
  });
1337
1434
  } catch (error) {
1338
- logger.error(error instanceof Error ? error : new Error(String(error)));
1339
- ensureProcessExitCode(1);
1340
- await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
1341
- return;
1435
+ return failWithError(error, async () => {
1436
+ await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
1437
+ });
1342
1438
  }
1343
1439
 
1344
1440
  if (isWatchMode) {
@@ -1361,20 +1457,19 @@ export const runBrowserController = async (
1361
1457
 
1362
1458
  // Only include browser mode projects in runtime configs
1363
1459
  // Normalize projectRoot to posix format for cross-platform compatibility
1364
- const browserProjectsForRuntime = getBrowserProjects(context);
1365
- const projectRuntimeConfigs: BrowserProjectRuntime[] =
1366
- browserProjectsForRuntime.map((project: ProjectContext) => ({
1460
+ const projectRuntimeConfigs: BrowserProjectRuntime[] = browserProjects.map(
1461
+ (project: ProjectContext) => ({
1367
1462
  name: project.name,
1368
1463
  environmentName: project.environmentName,
1369
1464
  projectRoot: normalize(project.rootPath),
1370
1465
  runtimeConfig: serializableConfig(getRuntimeConfigFromProject(project)),
1371
- }));
1466
+ viewport: project.normalizedConfig.browser.viewport,
1467
+ }),
1468
+ );
1372
1469
 
1373
1470
  // Get max testTimeout from all browser projects for RPC timeout
1374
1471
  const maxTestTimeoutForRpc = Math.max(
1375
- ...browserProjectsForRuntime.map(
1376
- (p) => p.normalizedConfig.testTimeout ?? 5000,
1377
- ),
1472
+ ...browserProjects.map((p) => p.normalizedConfig.testTimeout ?? 5000),
1378
1473
  );
1379
1474
 
1380
1475
  const hostOptions: BrowserHostConfig = {
@@ -1438,7 +1533,11 @@ export const runBrowserController = async (
1438
1533
  // Forward browser console to terminal
1439
1534
  containerPage.on('console', (msg: ConsoleMessage) => {
1440
1535
  const text = msg.text();
1441
- if (text.includes('[Container]') || text.includes('[Runner]')) {
1536
+ if (
1537
+ text.startsWith('[Container]') ||
1538
+ text.startsWith('[Runner]') ||
1539
+ text.startsWith('[Scheduler]')
1540
+ ) {
1442
1541
  logger.log(color.gray(`[Browser Console] ${text}`));
1443
1542
  }
1444
1543
  });
@@ -1579,21 +1678,28 @@ export const runBrowserController = async (
1579
1678
 
1580
1679
  // Only navigate on first creation
1581
1680
  if (isNewPage) {
1582
- await containerPage.goto(`http://localhost:${port}/`, {
1681
+ const pagePath = useSchedulerPage ? '/scheduler.html' : '/';
1682
+ if (useSchedulerPage) {
1683
+ const serializedOptions = serializeForInlineScript(hostOptions);
1684
+ await containerPage.addInitScript(
1685
+ `window.__RSTEST_BROWSER_OPTIONS__ = ${serializedOptions};`,
1686
+ );
1687
+ }
1688
+ await containerPage.goto(`http://localhost:${port}${pagePath}`, {
1583
1689
  waitUntil: 'load',
1584
1690
  });
1585
1691
 
1586
1692
  logger.log(
1587
- color.cyan(`\nBrowser mode opened at http://localhost:${port}/\n`),
1693
+ color.cyan(
1694
+ `\nBrowser mode opened at http://localhost:${port}${pagePath}\n`,
1695
+ ),
1588
1696
  );
1589
1697
  }
1590
1698
 
1591
1699
  // Wait for all tests to complete
1592
1700
  // Calculate total timeout based on config: max testTimeout * file count + buffer
1593
1701
  const maxTestTimeout = Math.max(
1594
- ...browserProjectsForRuntime.map(
1595
- (p) => p.normalizedConfig.testTimeout ?? 5000,
1596
- ),
1702
+ ...browserProjects.map((p) => p.normalizedConfig.testTimeout ?? 5000),
1597
1703
  );
1598
1704
  const totalTimeoutMs = maxTestTimeout * allTestFiles.length + 30_000;
1599
1705
 
@@ -1665,15 +1771,21 @@ export const runBrowserController = async (
1665
1771
  }
1666
1772
 
1667
1773
  if (!isWatchMode) {
1774
+ try {
1775
+ await containerPage.close();
1776
+ } catch {
1777
+ // ignore
1778
+ }
1779
+ try {
1780
+ await containerContext.close();
1781
+ } catch {
1782
+ // ignore
1783
+ }
1668
1784
  await destroyBrowserRuntime(runtime);
1669
1785
  }
1670
1786
 
1671
1787
  if (fatalError) {
1672
- logger.error(
1673
- color.red(`Browser test run failed: ${(fatalError as Error).message}`),
1674
- );
1675
- ensureProcessExitCode(1);
1676
- return;
1788
+ return failWithError(fatalError);
1677
1789
  }
1678
1790
 
1679
1791
  const duration = {
@@ -1769,8 +1881,8 @@ export const listBrowserTests = async (
1769
1881
  'browser',
1770
1882
  `list-${Date.now()}`,
1771
1883
  );
1772
- const manifestPath = join(tempDir, 'manifest.ts');
1773
1884
 
1885
+ const manifestPath = join(tempDir, VIRTUAL_MANIFEST_FILENAME);
1774
1886
  const manifestSource = generateManifestModule({
1775
1887
  manifestPath,
1776
1888
  entries: projectEntries,
@@ -1810,6 +1922,7 @@ export const listBrowserTests = async (
1810
1922
  environmentName: project.environmentName,
1811
1923
  projectRoot: normalize(project.rootPath),
1812
1924
  runtimeConfig: serializableConfig(getRuntimeConfigFromProject(project)),
1925
+ viewport: project.normalizedConfig.browser.viewport,
1813
1926
  }),
1814
1927
  );
1815
1928
 
@@ -1890,10 +2003,7 @@ export const listBrowserTests = async (
1890
2003
  );
1891
2004
 
1892
2005
  // Inject host options before navigation so the runner can access them
1893
- const serializedOptions = JSON.stringify(hostOptions).replace(
1894
- /</g,
1895
- '\\u003c',
1896
- );
2006
+ const serializedOptions = serializeForInlineScript(hostOptions);
1897
2007
  await page.addInitScript(
1898
2008
  `window.__RSTEST_BROWSER_OPTIONS__ = ${serializedOptions};`,
1899
2009
  );
package/src/index.ts CHANGED
@@ -9,6 +9,14 @@ import {
9
9
  runBrowserController,
10
10
  } from './hostController';
11
11
 
12
+ export { validateBrowserConfig } from './configValidation';
13
+
14
+ export {
15
+ BROWSER_VIEWPORT_PRESET_DIMENSIONS,
16
+ BROWSER_VIEWPORT_PRESET_IDS,
17
+ resolveBrowserViewportPreset,
18
+ } from './viewportPresets';
19
+
12
20
  export async function runBrowserTests(
13
21
  context: Rstest,
14
22
  options?: BrowserTestRunOptions,
package/src/protocol.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { DevicePreset } from '@rstest/core/browser';
1
2
  import type {
2
3
  RuntimeConfig,
3
4
  Test,
@@ -8,11 +9,19 @@ import type { SnapshotUpdateState } from '@vitest/snapshot';
8
9
 
9
10
  export type SerializedRuntimeConfig = RuntimeConfig;
10
11
 
12
+ export type BrowserViewport =
13
+ | {
14
+ width: number;
15
+ height: number;
16
+ }
17
+ | DevicePreset;
18
+
11
19
  export type BrowserProjectRuntime = {
12
20
  name: string;
13
21
  environmentName: string;
14
22
  projectRoot: string;
15
23
  runtimeConfig: SerializedRuntimeConfig;
24
+ viewport?: BrowserViewport;
16
25
  };
17
26
 
18
27
  /**
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Runtime source of truth for browser viewport presets.
3
+ *
4
+ * IMPORTANT: Keep this list/map in sync with `DevicePreset` typing in
5
+ * `@rstest/core` (`packages/core/src/types/config.ts`) so `defineConfig`
6
+ * autocomplete and runtime validation stay consistent.
7
+ */
8
+ export const BROWSER_VIEWPORT_PRESET_IDS = [
9
+ 'iPhoneSE',
10
+ 'iPhoneXR',
11
+ 'iPhone12Pro',
12
+ 'iPhone14ProMax',
13
+ 'Pixel7',
14
+ 'SamsungGalaxyS8Plus',
15
+ 'SamsungGalaxyS20Ultra',
16
+ 'iPadMini',
17
+ 'iPadAir',
18
+ 'iPadPro',
19
+ 'SurfacePro7',
20
+ 'SurfaceDuo',
21
+ 'GalaxyZFold5',
22
+ 'AsusZenbookFold',
23
+ 'SamsungGalaxyA51A71',
24
+ 'NestHub',
25
+ 'NestHubMax',
26
+ ] as const;
27
+
28
+ type BrowserViewportPresetId = (typeof BROWSER_VIEWPORT_PRESET_IDS)[number];
29
+
30
+ type BrowserViewportSize = {
31
+ width: number;
32
+ height: number;
33
+ };
34
+
35
+ export const BROWSER_VIEWPORT_PRESET_DIMENSIONS: Record<
36
+ BrowserViewportPresetId,
37
+ BrowserViewportSize
38
+ > = {
39
+ iPhoneSE: { width: 375, height: 667 },
40
+ iPhoneXR: { width: 414, height: 896 },
41
+ iPhone12Pro: { width: 390, height: 844 },
42
+ iPhone14ProMax: { width: 430, height: 932 },
43
+ Pixel7: { width: 412, height: 915 },
44
+ SamsungGalaxyS8Plus: { width: 360, height: 740 },
45
+ SamsungGalaxyS20Ultra: { width: 412, height: 915 },
46
+ iPadMini: { width: 768, height: 1024 },
47
+ iPadAir: { width: 820, height: 1180 },
48
+ iPadPro: { width: 1024, height: 1366 },
49
+ SurfacePro7: { width: 912, height: 1368 },
50
+ SurfaceDuo: { width: 540, height: 720 },
51
+ GalaxyZFold5: { width: 344, height: 882 },
52
+ AsusZenbookFold: { width: 853, height: 1280 },
53
+ SamsungGalaxyA51A71: { width: 412, height: 914 },
54
+ NestHub: { width: 1024, height: 600 },
55
+ NestHubMax: { width: 1280, height: 800 },
56
+ };
57
+
58
+ export const resolveBrowserViewportPreset = (
59
+ presetId: string,
60
+ ): BrowserViewportSize | null => {
61
+ const size =
62
+ BROWSER_VIEWPORT_PRESET_DIMENSIONS[presetId as BrowserViewportPresetId];
63
+ return size ?? null;
64
+ };
@@ -1 +0,0 @@
1
- @import "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap";@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after{--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-duration:initial;--tw-ease:initial}::backdrop{--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-duration:initial;--tw-ease:initial}}}@layer theme{:root,:host{--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-zinc-950:#09090b;--color-black:#000;--color-white:#fff;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--font-weight-light:300;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wider:.05em;--tracking-widest:.1em;--radius-md:.375rem;--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4,0,.6,1)infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1)}@supports (color:color(display-p3 0 0 0)){:root,:host{--color-zinc-950:color(display-p3 .0353716 .0353595 .0435539)}}@supports (color:lab(0% 0 0)){:root,:host{--color-zinc-950:lab(2.51107% .242703 -.886115)}}}@layer base,components;@layer utilities{.absolute{position:absolute}.relative{position:relative}.inset-0{inset:calc(var(--spacing)*0)}.inset-x-0{inset-inline:calc(var(--spacing)*0)}.inset-y-0{inset-block:calc(var(--spacing)*0)}.bottom-0{bottom:calc(var(--spacing)*0)}.left-0{left:calc(var(--spacing)*0)}.z-10{z-index:10}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.m-0{margin:calc(var(--spacing)*0)}.ml-1{margin-left:calc(var(--spacing)*1)}.block{display:block}.flex{display:flex}.grid{display:grid}.inline{display:inline}.inline-flex{display:inline-flex}.h-2{height:calc(var(--spacing)*2)}.h-5{height:calc(var(--spacing)*5)}.h-6{height:calc(var(--spacing)*6)}.h-8{height:calc(var(--spacing)*8)}.h-\[1px\]{height:1px}.h-\[48px\]{height:48px}.h-full{height:100%}.h-screen{height:100vh}.min-h-0{min-height:calc(var(--spacing)*0)}.w-2{width:calc(var(--spacing)*2)}.w-5{width:calc(var(--spacing)*5)}.w-8{width:calc(var(--spacing)*8)}.w-\[16px\]{width:16px}.w-full{width:100%}.max-w-\[400px\]{max-width:400px}.flex-1{flex:1}.shrink-0{flex-shrink:0}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-default{cursor:default}.grid-cols-\[auto_minmax\(0\,1fr\)_auto\]{grid-template-columns:auto minmax(0,1fr) auto}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:calc(var(--spacing)*1)}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-hidden{overflow-x:hidden}.overflow-y-auto{overflow-y:auto}.rounded-full{border-radius:3.40282e38px}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.bg-black\/\[0\.02\]{background-color:#00000005}@supports (color:color-mix(in lab, red, red)){.bg-black\/\[0\.02\]{background-color:color-mix(in oklab,var(--color-black)2%,transparent)}}.bg-transparent{background-color:#0000}.bg-zinc-950{background-color:var(--color-zinc-950)}.p-0{padding:calc(var(--spacing)*0)}.p-3{padding:calc(var(--spacing)*3)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-4{padding-inline:calc(var(--spacing)*4)}.py-0{padding-block:calc(var(--spacing)*0)}.py-0\.5{padding-block:calc(var(--spacing)*.5)}.py-1\.5{padding-block:calc(var(--spacing)*1.5)}.py-3{padding-block:calc(var(--spacing)*3)}.font-mono{font-family:var(--font-mono)}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.text-\[12px\]{font-size:12px}.text-\[13px\]{font-size:13px}.leading-none{--tw-leading:1;line-height:1}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-light{--tw-font-weight:var(--font-weight-light);font-weight:var(--font-weight-light)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.text-\(--accents-3\){color:var(--accents-3)}.text-\(--accents-4\){color:var(--accents-4)}.text-\(--accents-5\){color:var(--accents-5)}.text-\(--accents-7\){color:var(--accents-7)}.text-\(--muted-foreground\){color:var(--muted-foreground)}.text-\(--warning\){color:var(--warning)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal, )var(--tw-slashed-zero, )var(--tw-numeric-figure, )var(--tw-numeric-spacing, )var(--tw-numeric-fraction, )}.opacity-0{opacity:0}.opacity-40{opacity:.4}.opacity-70{opacity:.7}.opacity-80{opacity:.8}.opacity-90{opacity:.9}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-500{--tw-duration:.5s;transition-duration:.5s}.ease-\[cubic-bezier\(0\.4\,0\,0\.2\,1\)\]{--tw-ease:cubic-bezier(.4,0,.2,1);transition-timing-function:cubic-bezier(.4,0,.2,1)}.select-none{-webkit-user-select:none;user-select:none}@media (hover:hover){.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}.hover\:bg-\(--accents-1\):hover{background-color:var(--accents-1)}.hover\:text-\(--accents-6\):hover{color:var(--accents-6)}}}.ant-typography.font-mono{font-family:var(--font-mono)}:root{--font-mono:"JetBrains Mono",ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--background:#fff;--foreground:#000;--ds-gray-100:#fafafa;--ds-gray-200:#eaeaea;--ds-gray-300:#999;--ds-gray-400:#888;--ds-gray-500:#666;--ds-gray-600:#444;--ds-gray-700:#333;--ds-gray-800:#111;--ds-gray-900:#000;--ds-gray-1000:#000;--ds-red-100:#fff0f0;--ds-red-200:#ffebeb;--ds-red-300:#ffe6e6;--ds-red-700:#e5484d;--ds-red-800:#da2f35;--ds-red-900:#cb2a2f;--ds-green-100:#effbef;--ds-green-200:#ebfaeb;--ds-green-300:#daf6da;--ds-green-700:#45a557;--ds-green-800:#398e4a;--ds-green-900:#297a3a;--ds-amber-100:#fff6e6;--ds-amber-200:#fff4d6;--ds-amber-300:#fef0cd;--ds-amber-700:#ffb224;--ds-amber-800:#ff990a;--ds-amber-900:#a35200;--ds-blue-700:#0070f3;--accents-1:var(--ds-gray-100);--accents-2:var(--ds-gray-200);--accents-3:var(--ds-gray-300);--accents-4:var(--ds-gray-400);--accents-5:var(--ds-gray-500);--accents-6:var(--ds-gray-600);--accents-7:var(--ds-gray-700);--accents-8:var(--ds-gray-800);--border:var(--accents-2);--muted:var(--accents-1);--muted-foreground:var(--accents-5);--primary:var(--foreground);--success:var(--ds-green-700);--error:var(--ds-red-800);--warning:var(--ds-amber-700);--lightningcss-light:initial;--lightningcss-dark: ;--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light}[data-theme=dark]{--background:#000;--foreground:#fff;--ds-gray-100:#111;--ds-gray-200:#333;--ds-gray-300:#444;--ds-gray-400:#666;--ds-gray-500:#888;--ds-gray-600:#999;--ds-gray-700:#eaeaea;--ds-gray-800:#fafafa;--ds-gray-900:#fff;--ds-gray-1000:#fff;--ds-red-100:#2a1314;--ds-red-200:#3c1618;--ds-red-300:#561a1e;--ds-red-700:#e5484d;--ds-red-800:#d93036;--ds-red-900:#ff6166;--ds-green-100:#0b2212;--ds-green-200:#0f2e18;--ds-green-300:#12361b;--ds-green-700:#45a557;--ds-green-800:#398e4a;--ds-green-900:#62c073;--ds-amber-100:#291800;--ds-amber-200:#331b00;--ds-amber-300:#4d2a00;--ds-amber-700:#ffb224;--ds-amber-800:#ff990a;--ds-amber-900:#f2a20d;--ds-blue-700:#0070f3;--accents-1:var(--ds-gray-100);--accents-2:var(--ds-gray-200);--accents-3:var(--ds-gray-300);--accents-4:var(--ds-gray-400);--accents-5:var(--ds-gray-500);--accents-6:var(--ds-gray-600);--accents-7:var(--ds-gray-700);--accents-8:var(--ds-gray-800);--border:var(--accents-2);--muted:var(--accents-1);--muted-foreground:var(--accents-5);--primary:var(--foreground);--success:var(--ds-green-700);--error:var(--ds-red-800);--warning:var(--ds-amber-700);--lightningcss-light: ;--lightningcss-dark:initial;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}*{box-sizing:border-box}body{background:var(--background);min-height:100vh;color:var(--foreground);margin:0;font-family:Inter,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Noto Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif;overflow:hidden}.ant-input-affix-wrapper:focus,.ant-input-affix-wrapper-focused{border-color:var(--foreground);box-shadow:0 0 0 1px var(--foreground)}.ant-tree .ant-tree-node-content-wrapper.ant-tree-node-selected{transition:none}@keyframes status-flash{0%{filter:brightness()}5%{filter:brightness(2.5)}15%{filter:brightness(1.8)}40%{filter:brightness(1.3)}to{filter:brightness()}}.status-icon-flash{animation:1.5s ease-out status-flash}@keyframes progress-segment-flash{0%{filter:brightness();box-shadow:none}10%{filter:brightness(2.5);box-shadow:0 0 8px 1px}to{filter:brightness();box-shadow:none}}.progress-flash-active{animation:1s cubic-bezier(.4,0,.2,1) progress-segment-flash}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}}
@@ -1 +0,0 @@
1
- /*! For license information please see 837.e631233e.js.LICENSE.txt */
@@ -1 +0,0 @@
1
- /*! For license information please see lib-react.48335539.js.LICENSE.txt */