Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8198886dab |
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"schemaVersion": 1,
|
|
||||||
"layers": {
|
|
||||||
"domain": {
|
|
||||||
"root": "src/domain",
|
|
||||||
"mayImport": ["src/domain"]
|
|
||||||
},
|
|
||||||
"application": {
|
|
||||||
"root": "src/application",
|
|
||||||
"mayImport": ["src/application", "src/domain", "src/contracts"]
|
|
||||||
},
|
|
||||||
"presentation": {
|
|
||||||
"root": "src/presentation",
|
|
||||||
"mayImport": ["src/presentation", "src/application", "src/domain", "src/contracts"]
|
|
||||||
},
|
|
||||||
"adapters": {
|
|
||||||
"root": "src/adapters",
|
|
||||||
"mayImport": ["src/adapters", "src/application", "src/domain", "src/contracts"]
|
|
||||||
},
|
|
||||||
"bootstrap": {
|
|
||||||
"root": "src/bootstrap",
|
|
||||||
"mayImport": ["src"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"forbidden": [
|
|
||||||
["domain", "application"],
|
|
||||||
["domain", "presentation"],
|
|
||||||
["domain", "adapters"],
|
|
||||||
["domain", "bootstrap"],
|
|
||||||
["application", "presentation"],
|
|
||||||
["application", "adapters"],
|
|
||||||
["application", "bootstrap"],
|
|
||||||
["presentation", "adapters"],
|
|
||||||
["presentation", "bootstrap"],
|
|
||||||
["adapters", "presentation"],
|
|
||||||
["adapters", "bootstrap"]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
# Clean Architecture layer contract
|
|
||||||
|
|
||||||
The import direction is `domain <- application <- presentation`; concrete
|
|
||||||
adapters implement application-owned ports and are assembled only in
|
|
||||||
`src/bootstrap`.
|
|
||||||
|
|
||||||
| Layer | Owns | May depend on |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `domain` | framework-neutral models and pure policies | domain siblings |
|
|
||||||
| `application` | use cases, ports, orchestration, view-models | domain and application siblings |
|
|
||||||
| `presentation` | routes, components, user interaction and view state | application public API and shared UI |
|
|
||||||
| `adapters` | browser and third-party implementations of application ports | application ports and limited domain values |
|
|
||||||
| `bootstrap` | runtime configuration, adapter construction and React mount | all selected runtime modules |
|
|
||||||
|
|
||||||
The following edges are forbidden:
|
|
||||||
|
|
||||||
- domain to application, presentation, adapters, bootstrap, React, or browser globals
|
|
||||||
- application to presentation, concrete adapters, bootstrap, React, or browser globals
|
|
||||||
- presentation to concrete adapters, raw DTO schemas, or storage implementations
|
|
||||||
- an adapter to presentation, bootstrap internals, or another concrete adapter
|
|
||||||
|
|
||||||
`bootstrap` contains composition only. Business rules and page-specific
|
|
||||||
orchestration belong to domain/application.
|
|
||||||
|
|
||||||
Architecture reports use this shape:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"schemaVersion": 1,
|
|
||||||
"generatedAt": "ISO-8601",
|
|
||||||
"rules": [{ "name": "rule-id", "severity": "error", "violations": 0 }],
|
|
||||||
"summary": { "errors": 0, "warnings": 0 }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# Test and evidence taxonomy
|
|
||||||
|
|
||||||
Each gate is blocking in its declared scope. Failures are not downgraded with
|
|
||||||
`continue-on-error` or warning-only scripts.
|
|
||||||
|
|
||||||
| Level | Command | Evidence |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| runtime schema | `pnpm test:runtime-schema` | `artifacts/tests/runtime-schema.xml` |
|
|
||||||
| unit | `pnpm test:unit` | `artifacts/tests/unit.xml` |
|
|
||||||
| component | `pnpm test:component` | `artifacts/tests/component.xml` |
|
|
||||||
| integration | `pnpm test:integration` | `artifacts/tests/integration.xml` |
|
|
||||||
| end-to-end | `pnpm test:e2e` | `artifacts/tests/e2e/` |
|
|
||||||
| accessibility | `pnpm test:a11y` | `artifacts/tests/a11y.json` |
|
|
||||||
|
|
||||||
A control is verified only when a positive fixture passes and its deliberately
|
|
||||||
failing negative fixture is rejected. Generated evidence is retained by CI;
|
|
||||||
the repository tracks only the evidence directory structure.
|
|
||||||
|
|
||||||
Promotion is an AND graph:
|
|
||||||
|
|
||||||
1. merge gates
|
|
||||||
2. merge gates plus release gates
|
|
||||||
3. release gates plus rollback/runbook drills
|
|
||||||
4. production promotion plus eligible field Web Vitals evidence
|
|
||||||
+2
-17
@@ -13,33 +13,18 @@
|
|||||||
"build": "vite build && node scripts/generate-build-manifest.mjs",
|
"build": "vite build && node scripts/generate-build-manifest.mjs",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"check:types": "tsc --allowJs --checkJs --noEmit",
|
"check:types": "tsc --allowJs --checkJs --noEmit",
|
||||||
"check:types:fixture": "tsc --allowJs --checkJs --noEmit --target ES2022 --module NodeNext --moduleResolution NodeNext tests/fixtures/typecheck/invalid-port-call.js",
|
"check:types:fixture": "tsc --ignoreConfig --allowJs --checkJs --noEmit --target ES2022 --module NodeNext --moduleResolution NodeNext tests/fixtures/typecheck/invalid-port-call.js"
|
||||||
"test:runtime-schema": "vitest run tests/runtime-schema --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/runtime-schema.xml --passWithNoTests",
|
|
||||||
"test:unit": "vitest run tests/unit --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/unit.xml",
|
|
||||||
"test:component": "vitest run tests/component --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/component.xml",
|
|
||||||
"test:integration": "vitest run tests/integration --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/integration.xml",
|
|
||||||
"test:e2e": "playwright test",
|
|
||||||
"test:a11y": "playwright test --grep @a11y",
|
|
||||||
"test:all": "pnpm test:runtime-schema && pnpm test:unit && pnpm test:component && pnpm test:integration"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "19.2.8",
|
"react": "19.2.8",
|
||||||
"react-dom": "19.2.8"
|
"react-dom": "19.2.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@axe-core/playwright": "4.12.1",
|
|
||||||
"@playwright/test": "1.62.0",
|
|
||||||
"@testing-library/jest-dom": "7.0.0",
|
|
||||||
"@testing-library/react": "16.3.2",
|
|
||||||
"@testing-library/user-event": "14.6.1",
|
|
||||||
"@types/node": "24.13.3",
|
"@types/node": "24.13.3",
|
||||||
"@types/react": "19.2.8",
|
"@types/react": "19.2.8",
|
||||||
"@types/react-dom": "19.2.3",
|
"@types/react-dom": "19.2.3",
|
||||||
"@vitejs/plugin-react": "6.0.4",
|
"@vitejs/plugin-react": "6.0.4",
|
||||||
"jsdom": "29.1.1",
|
|
||||||
"msw": "2.15.0",
|
|
||||||
"typescript": "7.0.2",
|
"typescript": "7.0.2",
|
||||||
"vite": "8.1.5",
|
"vite": "8.1.5"
|
||||||
"vitest": "4.1.10"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import { defineConfig, devices } from "@playwright/test";
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
testDir: "./tests/e2e",
|
|
||||||
outputDir: "./artifacts/tests/e2e/results",
|
|
||||||
reporter: [
|
|
||||||
["list"],
|
|
||||||
["html", { outputFolder: "./artifacts/tests/e2e/report", open: "never" }],
|
|
||||||
],
|
|
||||||
use: {
|
|
||||||
baseURL: "http://127.0.0.1:5173",
|
|
||||||
trace: "retain-on-failure",
|
|
||||||
screenshot: "only-on-failure",
|
|
||||||
},
|
|
||||||
webServer: {
|
|
||||||
command: "pnpm dev --host 127.0.0.1",
|
|
||||||
url: "http://127.0.0.1:5173",
|
|
||||||
reuseExistingServer: !process.env.CI,
|
|
||||||
},
|
|
||||||
projects: [
|
|
||||||
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
Generated
-1231
File diff suppressed because it is too large
Load Diff
@@ -1,6 +0,0 @@
|
|||||||
allowBuilds:
|
|
||||||
msw: true
|
|
||||||
minimumReleaseAgeExclude:
|
|
||||||
- '@playwright/test@1.62.0'
|
|
||||||
- playwright-core@1.62.0
|
|
||||||
- playwright@1.62.0
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
/**
|
|
||||||
* Application facade factory. Concrete dependencies are supplied by bootstrap.
|
|
||||||
*
|
|
||||||
* @param {{
|
|
||||||
* resources: {
|
|
||||||
* query: import("./ports/resource-ports.js").ResourceQueryPort<unknown, unknown>,
|
|
||||||
* command: import("./ports/resource-ports.js").ResourceCommandPort<unknown, unknown>
|
|
||||||
* },
|
|
||||||
* cache: import("./ports/query-cache-port.js").QueryCachePort,
|
|
||||||
* storage: import("./ports/storage-port.js").StoragePort,
|
|
||||||
* telemetry: import("./ports/telemetry-port.js").TelemetryPort
|
|
||||||
* }} ports
|
|
||||||
*/
|
|
||||||
export function createApplication(ports) {
|
|
||||||
/**
|
|
||||||
* @param {unknown} query
|
|
||||||
* @param {import("./ports/resource-ports.js").RequestContext} [context]
|
|
||||||
*/
|
|
||||||
function queryResources(query, context) {
|
|
||||||
return ports.resources.query.execute(query, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {unknown} command
|
|
||||||
* @param {import("./ports/resource-ports.js").RequestContext} [context]
|
|
||||||
*/
|
|
||||||
function commandResources(command, context) {
|
|
||||||
return ports.resources.command.execute(command, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.freeze({
|
|
||||||
resources: Object.freeze({
|
|
||||||
query: queryResources,
|
|
||||||
command: commandResources,
|
|
||||||
}),
|
|
||||||
cache: ports.cache,
|
|
||||||
storage: ports.storage,
|
|
||||||
telemetry: ports.telemetry,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
/**
|
|
||||||
* @typedef {"authenticated" | "unauthenticated" | "recovery-pending" | "integration-failed"} SessionState
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The session is opaque: credentials are attached without exposing tokens.
|
|
||||||
*
|
|
||||||
* @typedef {{
|
|
||||||
* getState(): SessionState,
|
|
||||||
* attach(request: Request): Promise<Request>,
|
|
||||||
* recover(): Promise<"restored" | "no-session">,
|
|
||||||
* onUnauthenticated(): void
|
|
||||||
* }} AuthSessionPort
|
|
||||||
*/
|
|
||||||
|
|
||||||
export {};
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
/**
|
|
||||||
* @typedef {{
|
|
||||||
* now(): number,
|
|
||||||
* sleep(milliseconds: number, signal?: AbortSignal): Promise<void>
|
|
||||||
* }} ClockPort
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** @type {ClockPort} */
|
|
||||||
export const systemClock = Object.freeze({
|
|
||||||
now: () => Date.now(),
|
|
||||||
sleep(milliseconds, signal) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (signal?.aborted) {
|
|
||||||
reject(signal.reason);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timer = setTimeout(resolve, milliseconds);
|
|
||||||
signal?.addEventListener(
|
|
||||||
"abort",
|
|
||||||
() => {
|
|
||||||
clearTimeout(timer);
|
|
||||||
reject(signal.reason);
|
|
||||||
},
|
|
||||||
{ once: true },
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
/**
|
|
||||||
* @typedef {{
|
|
||||||
* read(key: readonly unknown[]): unknown,
|
|
||||||
* write(key: readonly unknown[], value: unknown): void,
|
|
||||||
* invalidate(namespace: readonly unknown[]): Promise<void>
|
|
||||||
* }} QueryCachePort
|
|
||||||
*/
|
|
||||||
|
|
||||||
export {};
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
/**
|
|
||||||
* @typedef {{
|
|
||||||
* getCurrent(): Promise<{
|
|
||||||
* buildId: string,
|
|
||||||
* configSchemaVersion: string,
|
|
||||||
* apiContractVersion: string,
|
|
||||||
* assetManifestHash: string,
|
|
||||||
* releaseId: string
|
|
||||||
* }>
|
|
||||||
* }} ReleaseInfoPort
|
|
||||||
*/
|
|
||||||
|
|
||||||
export {};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
/**
|
|
||||||
* @template Query
|
|
||||||
* @template Model
|
|
||||||
* @typedef {{ execute(query: Query, context?: RequestContext): Promise<Result<Model>> }} ResourceQueryPort
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @template Command
|
|
||||||
* @template Model
|
|
||||||
* @typedef {{ execute(command: Command, context?: RequestContext): Promise<Result<Model>> }} ResourceCommandPort
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {{
|
|
||||||
* operationId: string,
|
|
||||||
* routeId: string,
|
|
||||||
* signal?: AbortSignal,
|
|
||||||
* idempotencyKey?: string
|
|
||||||
* }} RequestContext
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @template Value
|
|
||||||
* @typedef {{ ok: true, value: Value, meta?: Record<string, unknown> } |
|
|
||||||
* { ok: false, error: unknown }} Result
|
|
||||||
*/
|
|
||||||
|
|
||||||
export {};
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
/**
|
|
||||||
* @typedef {{
|
|
||||||
* read(logicalName: string): { ok: true, value: unknown } | { ok: false, error: unknown },
|
|
||||||
* write(logicalName: string, value: unknown): { ok: true } | { ok: false, error: unknown },
|
|
||||||
* remove(logicalName: string): { ok: true } | { ok: false, error: unknown }
|
|
||||||
* }} StoragePort
|
|
||||||
*/
|
|
||||||
|
|
||||||
export {};
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
/**
|
|
||||||
* @typedef {{ emit(eventName: string, attributes: Record<string, unknown>): void }} TelemetryPort
|
|
||||||
*/
|
|
||||||
|
|
||||||
export {};
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { createApplication } from "../application/create-application.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is the only module allowed to join concrete adapters to application
|
|
||||||
* ports. Boot phases are explicit so failures can stop before product mount.
|
|
||||||
*
|
|
||||||
* @param {{
|
|
||||||
* loadConfig(): Promise<Record<string, unknown>>,
|
|
||||||
* loadRelease(config: Record<string, unknown>): Promise<Record<string, unknown>>,
|
|
||||||
* createAdapters(context: {
|
|
||||||
* config: Record<string, unknown>,
|
|
||||||
* release: Record<string, unknown>
|
|
||||||
* }): Promise<Parameters<typeof createApplication>[0]>
|
|
||||||
* }} factories
|
|
||||||
*/
|
|
||||||
export async function createCompositionRoot(factories) {
|
|
||||||
const config = await factories.loadConfig();
|
|
||||||
const release = await factories.loadRelease(config);
|
|
||||||
const ports = await factories.createAdapters({ config, release });
|
|
||||||
const application = createApplication(ports);
|
|
||||||
|
|
||||||
return Object.freeze({ config, release, ports, application });
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
// @vitest-environment jsdom
|
|
||||||
|
|
||||||
import { render, screen } from "@testing-library/react";
|
|
||||||
import { describe, expect, it } from "vitest";
|
|
||||||
|
|
||||||
function TestShell() {
|
|
||||||
return <main aria-label="application shell">ready</main>;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("component test level", () => {
|
|
||||||
it("renders an accessible application shell", () => {
|
|
||||||
render(<TestShell />);
|
|
||||||
expect(screen.getByRole("main", { name: "application shell" })).toHaveTextContent(
|
|
||||||
"ready",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { expect, test } from "@playwright/test";
|
|
||||||
|
|
||||||
test("boots the public app shell", async ({ page }) => {
|
|
||||||
await page.goto("/");
|
|
||||||
await expect(page.getByRole("heading", { level: 1 })).toHaveText(
|
|
||||||
"Clean Architecture Frontend",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { HttpResponse, http } from "msw";
|
|
||||||
import { setupServer } from "msw/node";
|
|
||||||
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
||||||
|
|
||||||
const server = setupServer(
|
|
||||||
http.get("https://example.test/health", () =>
|
|
||||||
HttpResponse.json({ success: true, data: { status: "ok" } }),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
|
|
||||||
afterEach(() => server.resetHandlers());
|
|
||||||
afterAll(() => server.close());
|
|
||||||
|
|
||||||
describe("integration test level", () => {
|
|
||||||
it("uses MSW to isolate the HTTP boundary", async () => {
|
|
||||||
const response = await fetch("https://example.test/health");
|
|
||||||
await expect(response.json()).resolves.toEqual({
|
|
||||||
success: true,
|
|
||||||
data: { status: "ok" },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import "@testing-library/jest-dom/vitest";
|
|
||||||
import { cleanup } from "@testing-library/react";
|
|
||||||
import { afterEach } from "vitest";
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
cleanup();
|
|
||||||
});
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
import { systemClock } from "../../src/application/ports/clock-port.js";
|
|
||||||
|
|
||||||
describe("systemClock", () => {
|
|
||||||
it("resolves after the requested duration", async () => {
|
|
||||||
vi.useFakeTimers();
|
|
||||||
const sleeper = systemClock.sleep(250);
|
|
||||||
await vi.advanceTimersByTimeAsync(250);
|
|
||||||
await expect(sleeper).resolves.toBeUndefined();
|
|
||||||
vi.useRealTimers();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { defineConfig } from "vitest/config";
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
test: {
|
|
||||||
globals: false,
|
|
||||||
setupFiles: ["./tests/setup.js"],
|
|
||||||
restoreMocks: true,
|
|
||||||
clearMocks: true,
|
|
||||||
mockReset: true,
|
|
||||||
testTimeout: 10_000,
|
|
||||||
coverage: {
|
|
||||||
reporter: ["text", "json-summary"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user