@crvouga/atlas 0.5.0 → 0.6.0
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.
- package/CHANGELOG.md +6 -0
- package/README.md +30 -11
- package/dist/core/src/cli.d.ts +1 -1
- package/dist/core/src/cli.js +8 -3
- package/dist/core/src/config.d.ts +7 -0
- package/dist/core/src/config.js +48 -14
- package/dist/core/src/run/execute.d.ts +6 -0
- package/dist/core/src/run/execute.js +4 -2
- package/dist/reports/src/doctor.d.ts +1 -1
- package/dist/reports/src/doctor.js +3 -1
- package/dist/reports/src/http-store.d.ts +4 -0
- package/dist/reports/src/http-store.js +6 -3
- package/dist/reports/src/init.js +6 -1
- package/dist/reports/src/live-reporter.d.ts +34 -0
- package/dist/reports/src/live-reporter.js +122 -0
- package/dist/reports/src/report-cli.d.ts +1 -1
- package/dist/reports/src/report-cli.js +3 -2
- package/dist/reports/src/store-url.js +1 -1
- package/dist/runners/src/codecept.d.ts +14 -0
- package/dist/runners/src/codecept.js +16 -0
- package/dist/runners/src/jest.d.ts +50 -0
- package/dist/runners/src/jest.js +52 -0
- package/dist/runners/src/mocha.d.ts +40 -0
- package/dist/runners/src/mocha.js +39 -0
- package/dist/runners/src/node-test.d.ts +28 -0
- package/dist/runners/src/node-test.js +44 -0
- package/dist/runners/src/publish.d.ts +30 -0
- package/dist/runners/src/publish.js +68 -0
- package/dist/schema/src/api.d.ts +1 -0
- package/dist/schema/src/api.js +1 -1
- package/dist/schema/src/index.d.ts +2 -0
- package/dist/schema/src/index.js +2 -0
- package/dist/schema/src/live-run.d.ts +123 -0
- package/dist/schema/src/live-run.js +30 -0
- package/dist/schema/src/manifest.d.ts +2 -2
- package/dist/schema/src/project.d.ts +20 -0
- package/dist/schema/src/project.js +10 -0
- package/dist/schema/src/report-sources.d.ts +1 -0
- package/dist/schema/src/report-sources.js +2 -1
- package/dist/schema/src/runs-index.d.ts +1 -1
- package/dist/schema/src/server-config.d.ts +84 -0
- package/dist/schema/src/server-config.js +12 -0
- package/dist/server/src/app.d.ts +10 -0
- package/dist/server/src/app.js +148 -10
- package/dist/server/src/config.d.ts +1 -1
- package/dist/server/src/config.js +20 -3
- package/dist/server/src/dev.d.ts +3 -3
- package/dist/server/src/dev.js +22 -13
- package/dist/server/src/events.d.ts +3 -3
- package/dist/server/src/events.js +1 -1
- package/dist/server/src/index.d.ts +2 -0
- package/dist/server/src/index.js +2 -0
- package/dist/server/src/live-runs.d.ts +52 -0
- package/dist/server/src/live-runs.js +229 -0
- package/dist/server/src/main.d.ts +1 -1
- package/dist/server/src/main.js +7 -5
- package/dist/server/src/openapi.d.ts +2 -5
- package/dist/server/src/openapi.js +71 -7
- package/dist/server/src/projects.d.ts +3 -0
- package/dist/server/src/projects.js +154 -0
- package/dist/server/src/settings-page.d.ts +9 -0
- package/dist/server/src/settings-page.js +93 -0
- package/dist/server/src/settings.d.ts +23 -0
- package/dist/server/src/settings.js +72 -0
- package/dist/server/src/site.d.ts +8 -6
- package/dist/server/src/site.js +45 -10
- package/dist/server/src/store-api.js +43 -22
- package/docs/deploying.md +19 -19
- package/docs/dogfooding.md +2 -2
- package/docs/extending.md +3 -3
- package/docs/integrating.md +9 -9
- package/docs/reference/cli.md +9 -7
- package/docs/reference/core.md +6 -0
- package/docs/reference/http-api.md +498 -2
- package/docs/reference/reports.md +55 -1
- package/docs/reference/runners.md +169 -0
- package/docs/reference/schema.md +111 -0
- package/docs/reference/server.md +154 -14
- package/docs/report-backends.md +8 -8
- package/docs/report-contract.md +7 -7
- package/docs/reporting-issues.md +3 -3
- package/docs/runners.md +69 -0
- package/docs/server.md +61 -25
- package/docs/statecharts.md +22 -20
- package/llms-full.txt +713 -105
- package/llms.txt +3 -1
- package/package.json +31 -9
- package/packages/runners/README.md +6 -0
- package/packages/runners/compat/codecept.cjs +2 -0
- package/packages/runners/compat/jest.cjs +2 -0
- package/packages/runners/compat/mocha.cjs +2 -0
- package/packages/schema/schemas/atlas-change.schema.json +4 -0
- package/packages/schema/schemas/hello-event.schema.json +4 -0
- package/packages/schema/schemas/live-run-start.schema.json +42 -0
- package/packages/schema/schemas/live-run-update.schema.json +93 -0
- package/packages/schema/schemas/live-run.schema.json +225 -0
- package/packages/schema/schemas/project-slug.schema.json +8 -0
- package/packages/schema/schemas/project.schema.json +22 -0
- package/packages/schema/schemas/projects.schema.json +33 -0
- package/packages/schema/schemas/server-config.schema.json +14 -0
- package/packages/schema/schemas/server-editable-settings.schema.json +132 -0
- package/packages/schema/schemas/server-settings-snapshot.schema.json +285 -0
- package/packages/server/openapi.json +5101 -779
- package/packages/visualizer/README.md +13 -13
- package/packages/visualizer/bin/atlas-client.mjs +176 -0
- package/packages/visualizer/bin/atlas-visualizer.mjs +2 -175
- package/packages/visualizer/src/app/AppShell.tsx +26 -17
- package/packages/visualizer/src/app/report-sources.tsx +2 -1
- package/packages/visualizer/src/components/ActivityIndicator.module.css +4 -0
- package/packages/visualizer/src/components/ActivityIndicator.tsx +6 -0
- package/packages/visualizer/src/components/Header.module.css +101 -227
- package/packages/visualizer/src/components/Header.tsx +79 -41
- package/packages/visualizer/src/components/ProjectPicker.tsx +64 -0
- package/packages/visualizer/src/data/adapters/report-backend.test.ts +3 -2
- package/packages/visualizer/src/data/adapters/report-backend.ts +1 -1
- package/packages/visualizer/src/data/queries.ts +14 -7
- package/packages/visualizer/src/evidence/api.ts +2 -1
- package/packages/visualizer/src/evidence/check-detail.tsx +19 -4
- package/packages/visualizer/src/evidence/document-page.tsx +6 -2
- package/packages/visualizer/src/evidence/evidence-context.tsx +17 -12
- package/packages/visualizer/src/evidence/evidence.module.css +11 -1
- package/packages/visualizer/src/evidence/feature-page.tsx +7 -3
- package/packages/visualizer/src/evidence/model-page.tsx +6 -2
- package/packages/visualizer/src/evidence/project-home.tsx +45 -0
- package/packages/visualizer/src/evidence/recording-page.tsx +4 -1
- package/packages/visualizer/src/evidence/spec-index.tsx +2 -1
- package/packages/visualizer/src/evidence/test-filter.tsx +21 -0
- package/packages/visualizer/src/live/live-query.ts +21 -0
- package/packages/visualizer/src/live/live-runs.tsx +69 -0
- package/packages/visualizer/src/live/live-stream.ts +30 -9
- package/packages/visualizer/src/live/report-progress.ts +62 -0
- package/packages/visualizer/src/live/test-progress.tsx +34 -0
- package/packages/visualizer/src/runtime.ts +17 -1
- package/packages/visualizer/src/state/ui-store.ts +3 -2
- package/packages/visualizer/src/views/ChartPage.tsx +2 -0
- package/packages/visualizer/src/views/ChartTests.tsx +43 -0
- package/packages/visualizer/src/views/SidePanel.tsx +2 -0
- package/packages/visualizer/vite/atlas-files.test.ts +4 -4
- package/packages/visualizer/vite/atlas-files.ts +1 -5
- package/packages/visualizer/vite/atlas-server-proxy.ts +2 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { addSummaries, summarizeSlots, type CheckSlot, type EvidenceView, type SpecEvidence } from '@crvouga/atlas/reports/evidence-view';
|
|
2
|
+
import type { LiveTest } from './live-runs';
|
|
3
|
+
|
|
4
|
+
type LiveIndex = ReadonlyMap<string, readonly LiveTest[]>;
|
|
5
|
+
const testKey = (id: string, file: string | null) => JSON.stringify([file, id]);
|
|
6
|
+
|
|
7
|
+
/** Build once per server update so rendering a row only looks up that test's executions. */
|
|
8
|
+
export function indexLiveTests(tests: readonly LiveTest[]): LiveIndex {
|
|
9
|
+
const index = new Map<string, LiveTest[]>();
|
|
10
|
+
for (const test of tests) {
|
|
11
|
+
const key = testKey(test.check.id, test.check.specPath ?? null);
|
|
12
|
+
const values = index.get(key);
|
|
13
|
+
if (values) values.push(test);
|
|
14
|
+
else index.set(key, [test]);
|
|
15
|
+
}
|
|
16
|
+
return index;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Match identity and spec, never a display title: different specs can use the same test name. */
|
|
20
|
+
export function testsForSlot(index: LiveIndex, slot: Pick<CheckSlot, 'id' | 'specPath' | 'latestAtMs'>) {
|
|
21
|
+
const seen = new Set<string>();
|
|
22
|
+
return (index.get(testKey(slot.id, slot.specPath)) ?? []).filter(({ run }) => {
|
|
23
|
+
const identity = JSON.stringify([run.producer, run.environment, run.branch]);
|
|
24
|
+
if (seen.has(identity)) return false;
|
|
25
|
+
seen.add(identity);
|
|
26
|
+
return run.status === 'running' || (run.finishedAtMs ?? run.lastProgressAtMs) >= slot.latestAtMs;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Display current execution beside history without changing immutable layers or report composition. */
|
|
31
|
+
export function withLiveProgress(view: EvidenceView, tests: readonly LiveTest[]): EvidenceView {
|
|
32
|
+
if (!tests.length) return view;
|
|
33
|
+
const specs = new Map(view.specs);
|
|
34
|
+
const index = indexLiveTests(tests);
|
|
35
|
+
const bySpec = new Map<string | null, Map<string, CheckSlot>>();
|
|
36
|
+
for (const { check } of tests) {
|
|
37
|
+
const file = check.specPath ?? null;
|
|
38
|
+
if (!bySpec.has(file)) bySpec.set(file, new Map((file ? specs.get(file)?.slots ?? [] : view.unassigned).map(slot => [slot.id, slot])));
|
|
39
|
+
const slots = bySpec.get(file)!;
|
|
40
|
+
if (!slots.has(check.id)) slots.set(check.id, { key: testKey(check.id, file), id: check.id, title: check.title, specPath: file,
|
|
41
|
+
status: check.status, outcomes: [], diagnostics: [], latestAtMs: 0, live: false });
|
|
42
|
+
}
|
|
43
|
+
const update = (slots: CheckSlot[]) => slots.map(slot => {
|
|
44
|
+
const matches = testsForSlot(index, slot);
|
|
45
|
+
if (!matches.length) return slot;
|
|
46
|
+
const running = matches.some(({ run, check }) => run.status === 'running' && check.status === 'running');
|
|
47
|
+
const latest = matches[0]!;
|
|
48
|
+
const status = running ? 'running' : slot.outcomes.length ? slot.status : latest.check.status === 'running' ? 'not-run' : latest.check.status;
|
|
49
|
+
const interrupted = matches.some(({ run, check }) => run.status === 'interrupted' && check.status === 'not-run');
|
|
50
|
+
return { ...slot, status: interrupted && !running ? 'not-run' as const : status, live: running,
|
|
51
|
+
latestAtMs: Math.max(slot.latestAtMs, ...matches.map(({ run }) => run.lastProgressAtMs)) };
|
|
52
|
+
});
|
|
53
|
+
let unassigned = view.unassigned;
|
|
54
|
+
for (const [file, slots] of bySpec) {
|
|
55
|
+
const updated = update([...slots.values()]);
|
|
56
|
+
if (file) {
|
|
57
|
+
const entry: SpecEvidence = { slots: updated, summary: summarizeSlots(updated) };
|
|
58
|
+
specs.set(file, entry);
|
|
59
|
+
} else unassigned = updated;
|
|
60
|
+
}
|
|
61
|
+
return { specs, unassigned, total: addSummaries([...specs.values()].map(entry => entry.summary)) };
|
|
62
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { CheckSlot } from '@crvouga/atlas/reports/evidence-view';
|
|
2
|
+
import { StatusMark } from '../evidence/marks';
|
|
3
|
+
import styles from '../evidence/evidence.module.css';
|
|
4
|
+
import { useLiveTests, type LiveTest } from './live-runs';
|
|
5
|
+
import { testsForSlot } from './report-progress';
|
|
6
|
+
|
|
7
|
+
export function useTestProgress(slot: CheckSlot | null) {
|
|
8
|
+
const { index, error } = useLiveTests();
|
|
9
|
+
return { tests: slot ? testsForSlot(index, slot) : [], error };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** A test's own progress log. Historical evidence stays available below it. */
|
|
13
|
+
export function TestProgress({ tests, error }: { tests: readonly LiveTest[]; error?: string | null }) {
|
|
14
|
+
if (!tests.length) return null;
|
|
15
|
+
return <div aria-label="Test progress">
|
|
16
|
+
{tests.map(({ run, check }) => {
|
|
17
|
+
const running = run.status === 'running' && check.status === 'running';
|
|
18
|
+
const interrupted = run.status === 'interrupted' && check.status === 'not-run';
|
|
19
|
+
const status = running ? 'running' : interrupted ? 'not-run' : check.status === 'running' ? 'not-run' : check.status;
|
|
20
|
+
return <div key={`${run.id}:${check.id}`}>
|
|
21
|
+
<p className={styles.detailMeta} role="status"><StatusMark status={status} />
|
|
22
|
+
<strong>{running ? 'Running' : interrupted ? 'Interrupted' : status === 'passed' ? 'Passed' : status === 'failed' ? 'Failed' : 'Not run'}</strong>
|
|
23
|
+
<span>{run.producer}{run.environment ? ` · ${run.environment}` : ''}</span>
|
|
24
|
+
</p>
|
|
25
|
+
{check.detail && check.detail !== run.reason && <pre className={styles.pre}>{check.detail}</pre>}
|
|
26
|
+
{!!check.steps?.length && <ol className={styles.steps} aria-label="Progress log">{check.steps.map((step, index) => <li key={index}>
|
|
27
|
+
<div className={styles.stepRow}><StatusMark status={step.status === 'passed' || step.status === 'failed' || step.status === 'running' ? step.status : 'none'} size={14} /><span className={styles.stepText}>{step.name}</span></div>
|
|
28
|
+
</li>)}</ol>}
|
|
29
|
+
{interrupted && <p className={styles.notice} role="status">{run.reason ?? 'The test run was interrupted.'} No completed outcome was received for this test.</p>}
|
|
30
|
+
</div>;
|
|
31
|
+
})}
|
|
32
|
+
{error && <p className={styles.notice} role="alert">{error}</p>}
|
|
33
|
+
</div>;
|
|
34
|
+
}
|
|
@@ -9,9 +9,25 @@ export type RuntimeConfig = {
|
|
|
9
9
|
readonly dataUrl?: string;
|
|
10
10
|
readonly eventsUrl?: string;
|
|
11
11
|
readonly effective?: boolean;
|
|
12
|
+
readonly projectsUrl?: string;
|
|
12
13
|
};
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
/** The project selected in the page address. A full navigation on change isolates caches and connections. */
|
|
16
|
+
export const currentProject = () => typeof window === 'undefined' ? null : new URL(window.location.href).searchParams.get('project');
|
|
17
|
+
|
|
18
|
+
export const runtimeConfig = (): RuntimeConfig | undefined => {
|
|
19
|
+
const config = (globalThis as { __ATLAS_CONFIG__?: RuntimeConfig }).__ATLAS_CONFIG__;
|
|
20
|
+
const project = currentProject();
|
|
21
|
+
if (!config?.api || !project || !/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(project) || project.length > 64) return config;
|
|
22
|
+
const api = new URL(config.api, window.location.href).href.replace(/\/$/, '');
|
|
23
|
+
const scoped = `${api}/projects/${project}`;
|
|
24
|
+
const scope = (address: string | undefined) => {
|
|
25
|
+
if (!address) return address;
|
|
26
|
+
const absolute = new URL(address, window.location.href).href;
|
|
27
|
+
return absolute === api || absolute.startsWith(`${api}/`) ? scoped + absolute.slice(api.length) : absolute;
|
|
28
|
+
};
|
|
29
|
+
return { ...config, api: scoped, reportsUrl: scope(config.reportsUrl), dataUrl: scope(config.dataUrl), eventsUrl: scope(config.eventsUrl) };
|
|
30
|
+
};
|
|
15
31
|
|
|
16
32
|
/** An address from the config, resolved against the page, as an absolute URL. */
|
|
17
33
|
export function runtimeUrl(value: string | undefined): string | undefined {
|
|
@@ -2,9 +2,10 @@ import { ALL_SOURCES, type SourceSelection } from '@crvouga/atlas/reports/eviden
|
|
|
2
2
|
import type { Viewport } from '@xyflow/react';
|
|
3
3
|
import { create } from 'zustand';
|
|
4
4
|
import { createJSONStorage, persist } from 'zustand/middleware';
|
|
5
|
+
import { currentProject } from '../runtime';
|
|
5
6
|
|
|
6
7
|
/** Which specs the index lists, by what has been recorded for them. */
|
|
7
|
-
export type ResultFilter = 'all' | 'results' | 'failed' | 'none';
|
|
8
|
+
export type ResultFilter = 'all' | 'results' | 'running' | 'failed' | 'none';
|
|
8
9
|
|
|
9
10
|
type UiState = {
|
|
10
11
|
viewports: Record<string, Viewport>;
|
|
@@ -36,7 +37,7 @@ export const useUiStore = create<UiState>()(
|
|
|
36
37
|
setExcluded: (excluded) => set({ excluded })
|
|
37
38
|
}),
|
|
38
39
|
{
|
|
39
|
-
name: 'atlas-ui',
|
|
40
|
+
name: currentProject() ? `atlas-ui:${currentProject()}` : 'atlas-ui',
|
|
40
41
|
storage: createJSONStorage(() => sessionStorage),
|
|
41
42
|
partialize: (s) => ({ viewports: s.viewports, excluded: s.excluded, resultFilter: s.resultFilter }),
|
|
42
43
|
merge: (persisted, current) => {
|
|
@@ -5,6 +5,7 @@ import { mapFrame } from '../layout/graph';
|
|
|
5
5
|
import { ChartMap } from '../map/ChartMap';
|
|
6
6
|
import { SidePanel } from './SidePanel';
|
|
7
7
|
import styles from './views.module.css';
|
|
8
|
+
import { ChartTests } from './ChartTests';
|
|
8
9
|
|
|
9
10
|
export function ChartPage() {
|
|
10
11
|
const { chartId } = useParams({ from: '/chart/$chartId' });
|
|
@@ -47,6 +48,7 @@ export function ChartPage() {
|
|
|
47
48
|
<div className={styles.mapArea}>
|
|
48
49
|
<ChartMap key={`${chartId}:${search.group ?? ''}`} view={view} chartId={chartId} selection={selection} group={search.group} />
|
|
49
50
|
</div>
|
|
51
|
+
<ChartTests view={view} chartId={chartId} compact />
|
|
50
52
|
</div>
|
|
51
53
|
);
|
|
52
54
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { lazy, Suspense, useState } from 'react';
|
|
2
|
+
import type { AtlasView } from '../data/model';
|
|
3
|
+
import { useEvidence } from '../evidence/evidence-context';
|
|
4
|
+
import { CheckDetail, ResultRow, type CheckSelection } from '../evidence/check-detail';
|
|
5
|
+
import { TestFilter, useRunningFilter } from '../evidence/test-filter';
|
|
6
|
+
import { Bounded } from '../evidence/marks';
|
|
7
|
+
import { outlinePath } from '../spec-kind';
|
|
8
|
+
import styles from '../evidence/evidence.module.css';
|
|
9
|
+
|
|
10
|
+
const RecordingPage = lazy(() => import('../evidence/recording-page').then(module => ({ default: module.RecordingPage })));
|
|
11
|
+
|
|
12
|
+
/** Statechart test reports stay in the chart, including while their recordings are still arriving. */
|
|
13
|
+
export function ChartTests({ view, chartId, compact = false }: { view: AtlasView; chartId: string; compact?: boolean }) {
|
|
14
|
+
const evidence = useEvidence();
|
|
15
|
+
const runningOnly = useRunningFilter();
|
|
16
|
+
const [selection, setSelection] = useState<CheckSelection>({});
|
|
17
|
+
const [selectedKey, setSelectedKey] = useState<string | null>(null);
|
|
18
|
+
const [recording, setRecording] = useState(false);
|
|
19
|
+
const [moment, setMoment] = useState<number | null>(null);
|
|
20
|
+
const files = new Set<string>();
|
|
21
|
+
let chart = view.charts.get(chartId);
|
|
22
|
+
while (chart && !files.has(outlinePath(chart.file))) {
|
|
23
|
+
files.add(outlinePath(chart.file));
|
|
24
|
+
chart = chart.parent ? view.charts.get(chart.parent.chartId) : undefined;
|
|
25
|
+
}
|
|
26
|
+
const slots = [...files].flatMap(file => evidence.view.specs.get(file)?.slots ?? []);
|
|
27
|
+
const visible = slots.filter(slot => !runningOnly || slot.status === 'running');
|
|
28
|
+
if (!slots.length) return null;
|
|
29
|
+
return <section aria-label="Chart tests" className={styles.block} style={compact ? { maxHeight: '40vh', overflowY: 'auto', flexShrink: 0 } : undefined}>
|
|
30
|
+
<h3 className={styles.blockTitle}>Tests</h3>
|
|
31
|
+
<TestFilter slots={slots} />
|
|
32
|
+
<ul className={styles.results}><Bounded items={visible} size={40} more="More tests">{slot =>
|
|
33
|
+
<ResultRow key={slot.key} label={slot.title} slot={slot} open={selectedKey === slot.key}
|
|
34
|
+
onToggle={() => { setSelectedKey(selectedKey === slot.key ? null : slot.key); setSelection({ check: slot.id }); setRecording(false); setMoment(null); }}>
|
|
35
|
+
{recording ? <Suspense fallback={<p role="status">Loading the recording…</p>}><RecordingPage slot={slot} layerId={selection.layer} label={slot.title} back="Test report" moment={moment} onMoment={setMoment} onClose={() => setRecording(false)} /></Suspense>
|
|
36
|
+
: <CheckDetail slot={slot} layerId={selection.layer} reads="steps" select={patch => {
|
|
37
|
+
setSelection({ check: patch.check ?? slot.id, layer: patch.layer ?? undefined });
|
|
38
|
+
if (patch.view === 'recording') { setRecording(true); setMoment(null); }
|
|
39
|
+
}} />}
|
|
40
|
+
</ResultRow>
|
|
41
|
+
}</Bounded></ul>
|
|
42
|
+
</section>;
|
|
43
|
+
}
|
|
@@ -12,6 +12,7 @@ import { Stage } from '../looks/Stage';
|
|
|
12
12
|
import { mapScope } from '../layout/graph';
|
|
13
13
|
import type { MapSelection } from '../map/ChartMap';
|
|
14
14
|
import styles from './views.module.css';
|
|
15
|
+
import { ChartTests } from './ChartTests';
|
|
15
16
|
|
|
16
17
|
function Section({ title, children }: { title: string; children: ReactNode }) {
|
|
17
18
|
return (
|
|
@@ -314,6 +315,7 @@ export function SidePanel({ view, chartId, selection }: { view: AtlasView; chart
|
|
|
314
315
|
return (
|
|
315
316
|
<article className={styles.plateScroll} aria-label={label}>
|
|
316
317
|
{body}
|
|
318
|
+
<ChartTests view={view} chartId={chartId} />
|
|
317
319
|
<Link
|
|
318
320
|
to="/chart/$chartId"
|
|
319
321
|
params={{ chartId }}
|
|
@@ -4,7 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
|
|
5
5
|
import { afterAll, describe, expect, it } from 'bun:test';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { FIXTURES_ROOT, isSpecFile, listSpecFiles, publicReportSources, resolveReportSources, resolveRoots, safeJoin, summarizeRun } from './atlas-files';
|
|
8
8
|
|
|
9
9
|
const tmp = mkdtempSync(path.join(os.tmpdir(), 'atlas-files-'));
|
|
10
10
|
afterAll(() => rmSync(tmp, { recursive: true, force: true }));
|
|
@@ -31,10 +31,10 @@ describe('resolveRoots', () => {
|
|
|
31
31
|
expect(resolveRoots({ ATLAS_SPECS_ROOT: 'x' }, project).specs).toBe(path.join(project, 'x'));
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
it('
|
|
34
|
+
it('keeps an unconfigured project empty instead of loading a bundled example', () => {
|
|
35
35
|
const roots = resolveRoots({ INIT_CWD: path.join(tmp, 'empty') });
|
|
36
|
-
expect(roots.specs).toBe(path.join(
|
|
37
|
-
expect(roots.label).
|
|
36
|
+
expect(roots.specs).toBe(path.join(tmp, 'empty', 'specs'));
|
|
37
|
+
expect(roots.label).toBe('configured');
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
it('reads a named fixture and rejects an unknown one', () => {
|
|
@@ -53,17 +53,13 @@ export function fixtureRoots(name: string): AtlasRoots {
|
|
|
53
53
|
* (`ATLAS_FIXTURE`), explicit roots (`ATLAS_SPECS_ROOT`, `ATLAS_RUNS_ROOT`), then `specs/` and
|
|
54
54
|
* `atlas-runs/` where the command was run. Relative paths resolve against `INIT_CWD` (where pnpm
|
|
55
55
|
* was run) or the current directory. With nothing configured and no `specs/` there, the bundled
|
|
56
|
-
*
|
|
56
|
+
* project stays empty. Bundled examples require an explicit fixture selection.
|
|
57
57
|
*/
|
|
58
58
|
export function resolveRoots(env: NodeJS.ProcessEnv = process.env, cwd = process.cwd()): AtlasRoots {
|
|
59
59
|
if (env.ATLAS_FIXTURE) return fixtureRoots(env.ATLAS_FIXTURE);
|
|
60
60
|
const base = env.INIT_CWD || cwd;
|
|
61
|
-
const specsSet = Boolean(env.ATLAS_SPECS_ROOT);
|
|
62
61
|
const specs = path.resolve(base, env.ATLAS_SPECS_ROOT || DEFAULT_SPECS_DIR);
|
|
63
62
|
const runs = path.resolve(base, env.ATLAS_RUNS_ROOT || DEFAULT_RUNS_DIR);
|
|
64
|
-
if (!specsSet && !env.ATLAS_RUNS_ROOT && !existsSync(specs)) {
|
|
65
|
-
return { ...fixtureRoots(DEFAULT_FIXTURE), label: `example (no ${DEFAULT_SPECS_DIR}/ in ${base}; set ATLAS_SPECS_ROOT)` };
|
|
66
|
-
}
|
|
67
63
|
return { specs, runs, label: 'configured' };
|
|
68
64
|
}
|
|
69
65
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Plugin, ProxyOptions } from 'vite';
|
|
2
2
|
|
|
3
3
|
/** What the viewer is told about the server it is served with: the same words `atlas serve` writes into its page. */
|
|
4
|
-
export const serverPageConfig = { api: './api', reportsUrl: './api/reports', dataUrl: './api/data/', eventsUrl: './api/events', effective: true } as const;
|
|
4
|
+
export const serverPageConfig = { api: './api', reportsUrl: './api/reports', dataUrl: './api/data/', eventsUrl: './api/events', projectsUrl: './api/projects', effective: true } as const;
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Put the Atlas server behind the dev server. The browser talks to one host; `/api` goes to the server, so the
|
|
@@ -10,6 +10,7 @@ export const serverPageConfig = { api: './api', reportsUrl: './api/reports', dat
|
|
|
10
10
|
export function atlasServerProxy(target: string): { readonly proxy: Record<string, ProxyOptions> } {
|
|
11
11
|
return {
|
|
12
12
|
proxy: {
|
|
13
|
+
'/admin': { target, changeOrigin: false },
|
|
13
14
|
'/api': {
|
|
14
15
|
target,
|
|
15
16
|
changeOrigin: false,
|