refactor: 프론트 템플릿 리펙토링
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type {
|
||||
BrowserDataResult,
|
||||
IndexedDbRepositoryPort,
|
||||
} from "../../../src/application/ports/browser-file-storage/index.ts";
|
||||
import {
|
||||
createLocalDraftStore,
|
||||
type LocalDraft,
|
||||
} from "./fixtures/local-draft-feature.ts";
|
||||
|
||||
function success<Value>(value: Value): BrowserDataResult<Value> {
|
||||
return Object.freeze({ ok: true as const, value });
|
||||
}
|
||||
|
||||
function repositoryFixture(): IndexedDbRepositoryPort<LocalDraft, never> {
|
||||
let stored: Readonly<{ value: LocalDraft; revision: number }> | null = null;
|
||||
|
||||
const repository: IndexedDbRepositoryPort<LocalDraft, never> = {
|
||||
async open() {
|
||||
return success(undefined);
|
||||
},
|
||||
async read(key) {
|
||||
if (stored === null || stored.value.draftId !== key) {
|
||||
return success(null);
|
||||
}
|
||||
return success(stored);
|
||||
},
|
||||
async query() {
|
||||
return success(Object.freeze({ items: [], nextCursor: null }));
|
||||
},
|
||||
async compareAndSwap(input) {
|
||||
const currentRevision = stored?.revision ?? null;
|
||||
if (currentRevision !== input.expectedRevision) {
|
||||
return Object.freeze({
|
||||
ok: false as const,
|
||||
error: Object.freeze({
|
||||
code: "CONFLICT" as const,
|
||||
operation: "INDEXEDDB_WRITE" as const,
|
||||
retryable: false,
|
||||
recovery: "RECONCILE" as const,
|
||||
}),
|
||||
});
|
||||
}
|
||||
const revision = (currentRevision ?? 0) + 1;
|
||||
stored = Object.freeze({ value: input.value, revision });
|
||||
return success(
|
||||
Object.freeze({
|
||||
key: input.key,
|
||||
revision,
|
||||
replayed: false,
|
||||
}),
|
||||
);
|
||||
},
|
||||
async remove(input) {
|
||||
if (stored === null || stored.revision !== input.expectedRevision) {
|
||||
return Object.freeze({
|
||||
ok: false as const,
|
||||
error: Object.freeze({
|
||||
code: "CONFLICT" as const,
|
||||
operation: "INDEXEDDB_WRITE" as const,
|
||||
retryable: false,
|
||||
recovery: "RECONCILE" as const,
|
||||
}),
|
||||
});
|
||||
}
|
||||
stored = null;
|
||||
return success(
|
||||
Object.freeze({
|
||||
key: input.key,
|
||||
revision: input.expectedRevision + 1,
|
||||
replayed: false,
|
||||
}),
|
||||
);
|
||||
},
|
||||
async enforceLifecycleBatch() {
|
||||
stored = null;
|
||||
return success(
|
||||
Object.freeze({
|
||||
state: "COMPLETE" as const,
|
||||
scannedRows: 0,
|
||||
deletedRows: 0,
|
||||
budgetExhausted: false,
|
||||
}),
|
||||
);
|
||||
},
|
||||
getStatus() {
|
||||
return Object.freeze({ kind: "READY" as const, schemaVersion: 1 });
|
||||
},
|
||||
subscribeStatus() {
|
||||
return () => {};
|
||||
},
|
||||
close() {},
|
||||
};
|
||||
return Object.freeze(repository);
|
||||
}
|
||||
|
||||
describe("IndexedDB local-draft consumer experience", () => {
|
||||
it("implements save/find/remove through the public application port", async () => {
|
||||
const store = createLocalDraftStore(repositoryFixture());
|
||||
const draft = Object.freeze({
|
||||
draftId: "draft-1",
|
||||
title: "Architecture notes",
|
||||
body: "Feature code owns the draft model.",
|
||||
});
|
||||
|
||||
await expect(
|
||||
store.save({
|
||||
draft,
|
||||
expectedRevision: null,
|
||||
idempotencyKey: "draft-save-0001",
|
||||
}),
|
||||
).resolves.toEqual({ ok: true, value: { revision: 1 } });
|
||||
|
||||
await expect(store.find("draft-1")).resolves.toEqual({
|
||||
ok: true,
|
||||
value: { draft, revision: 1 },
|
||||
});
|
||||
|
||||
await expect(
|
||||
store.remove({
|
||||
draftId: "draft-1",
|
||||
expectedRevision: 1,
|
||||
idempotencyKey: "draft-remove-0001",
|
||||
}),
|
||||
).resolves.toEqual({ ok: true, value: undefined });
|
||||
|
||||
await expect(store.find("draft-1")).resolves.toEqual({
|
||||
ok: true,
|
||||
value: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps native IndexedDB and runtime internals out of feature-owned code", async () => {
|
||||
const source = await readFile(
|
||||
new URL("./fixtures/local-draft-feature.ts", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const importLines = source
|
||||
.split("\n")
|
||||
.filter((line) => line.startsWith("import "));
|
||||
expect(importLines).toHaveLength(1);
|
||||
expect(source).toContain(
|
||||
"src/application/ports/browser-file-storage/index.ts",
|
||||
);
|
||||
|
||||
for (const forbidden of [
|
||||
"src/adapters/storage/indexeddb",
|
||||
"globalThis.indexedDB",
|
||||
"IDBFactory",
|
||||
"IDBDatabase",
|
||||
"IDBTransaction",
|
||||
"IDBObjectStore",
|
||||
]) {
|
||||
expect(source).not.toContain(forbidden);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user