Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
610 changes: 610 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"scripts": {
"dev": "vite --host 0.0.0.0",
"build": " vite build",
"test": "vitest run",
"lint": "eslint .",
"preview": "vite preview",
"domain": "echo \"127.0.0.1 local.jhnara.asuscomm.com\" | sudo tee -a /etc/hosts"
Expand All @@ -32,6 +33,7 @@
"devDependencies": {
"@ckeditor/ckeditor5-inspector": "^4.1.0",
"@eslint/js": "^9.19.0",
"@testing-library/react": "^16.3.0",
"@types/ckeditor": "^4.9.10",
"@types/deep-diff": "^1.0.5",
"@types/node": "^22.13.1",
Expand All @@ -42,6 +44,7 @@
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.18",
"globals": "^15.14.0",
"jsdom": "^26.1.0",
"typescript": "~5.7.2",
"typescript-eslint": "^8.22.0",
"vite": "^6.3.2",
Expand Down
91 changes: 91 additions & 0 deletions src/shared/stores/DocumentManager.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { DocumentManager } from "./DocumentManager";
import { HtmlProcessor } from "@/utils/HtmlProcessor";
import { AiService } from "@/utils/AiService";
import { DocumentService } from "@/shared/services/DocumentService";
import deepDiff from "deep-diff";

// Mock all dependencies
vi.mock("@/utils/HtmlProcessor");
vi.mock("@/utils/AiService");
vi.mock("@/shared/services/DocumentService");
vi.mock("deep-diff", () => ({
default: {
diff: vi.fn(),
},
}));

describe("DocumentManager", () => {
beforeEach(() => {
// Reset the static property to break the singleton behavior for tests
(DocumentManager as any).documentService = undefined;
vi.clearAllMocks();
});

it("should initialize DocumentService on first creation", () => {
new DocumentManager();
expect(DocumentService).toHaveBeenCalledTimes(1);
});

describe("handleDocumentModifications", () => {
it("should do nothing if no differences are found", async () => {
vi.mocked(deepDiff.diff).mockReturnValue(undefined);
const documentManager = new DocumentManager();
await documentManager.handleDocumentModifications("<p>test</p>");
expect(AiService.fetchAiRefinementsLocal).not.toHaveBeenCalled();
});

it("should process modifications and call AI service if differences are found", async () => {
// Arrange
const mockDiffs = [{ kind: "E", path: [0], lhs: "a", rhs: "b" }];
const mockModifiedElements = ["<p>b</p>"];
const mockProcessedDoc = ["b"];

vi.mocked(deepDiff.diff).mockReturnValue(mockDiffs as any);

const mockProcessor = vi.mocked(HtmlProcessor.prototype);
mockProcessor.processHtmlDocument.mockReturnValue(mockProcessedDoc);
mockProcessor.extractModifiedElements.mockReturnValue(mockModifiedElements);

const mockAiGenerator = async function*() { yield { target_id: '1', errors: [] }; }
vi.mocked(AiService.fetchAiRefinementsLocal).mockReturnValue(mockAiGenerator());

const setPrevDocSpy = vi.fn();
const handleAiRefinementSpy = vi.fn();
vi.spyOn(DocumentService.prototype, 'getPreviousDocuments').mockReturnValue(["a"]);
vi.spyOn(DocumentService.prototype, 'setPreviousDocuments').mockImplementation(setPrevDocSpy);
vi.spyOn(DocumentService.prototype, 'handleAiRefinement').mockImplementation(handleAiRefinementSpy);

// Act
const documentManager = new DocumentManager();
await documentManager.handleDocumentModifications("<p>b</p>");

// Assert
expect(mockProcessor.extractModifiedElements).toHaveBeenCalledWith(mockDiffs, mockProcessedDoc);
expect(setPrevDocSpy).toHaveBeenCalledWith(mockProcessedDoc);
expect(AiService.fetchAiRefinementsLocal).toHaveBeenCalledWith(mockModifiedElements);
expect(handleAiRefinementSpy).toHaveBeenCalled();
});
});

it("should delegate subscribe to DocumentService", () => {
const listener = () => {};
const unsubscribe = () => {};
const subscribeSpy = vi.spyOn(DocumentService.prototype, 'subscribe').mockReturnValue(unsubscribe);

const documentManager = new DocumentManager();
const result = documentManager.subscribe(listener);

expect(subscribeSpy).toHaveBeenCalledWith(listener);
expect(result).toBe(unsubscribe);
});

it("should delegate getErrorParagraphs to DocumentService", () => {
const getErrorsSpy = vi.spyOn(DocumentService.prototype, 'getErrorParagraphs').mockReturnValue([]);

const documentManager = new DocumentManager();
documentManager.getErrorParagraphs();

expect(getErrorsSpy).toHaveBeenCalled();
});
});
77 changes: 77 additions & 0 deletions src/shared/stores/useDocument.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { describe, it, expect, vi, afterEach, beforeEach } from "vitest";
import { useFileStore } from "./useDocument";
import { act } from "@testing-library/react";

describe("useFileStore", () => {
const initialState = useFileStore.getState();

beforeEach(() => {
// Reset store to initial state before each test
useFileStore.setState(initialState);
// Mock global fetch
global.fetch = vi.fn();
});

afterEach(() => {
vi.restoreAllMocks();
});

it("should fetch files and update the store", async () => {
const mockFiles = [
{ title: "file1", updated_at: "2024-01-01 10:00:00", hashed_id: "1" },
{ title: "file2", updated_at: "2024-01-02 11:00:00", hashed_id: "2" },
];
const mockResponse = {
ok: true,
json: () => Promise.resolve(mockFiles),
};
vi.mocked(global.fetch).mockResolvedValue(mockResponse as Response);

// Using act to ensure state updates are processed
await act(async () => {
await useFileStore.getState().fetchFiles();
});

const state = useFileStore.getState();
expect(state.files).toHaveLength(2);
expect(state.files[0].title).toBe("file1");
// Check if date was correctly transformed
expect(state.files[0].updated_at).toBe(new Date("2024-01-01T10:00:00").toISOString());
});

it("should handle fetch errors and set files to an empty array", async () => {
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
vi.mocked(global.fetch).mockRejectedValue(new Error("Network Error"));

await act(async () => {
await useFileStore.getState().fetchFiles();
});

const state = useFileStore.getState();
expect(state.files).toEqual([]);
expect(consoleSpy).toHaveBeenCalled();
});

it("should not update state if fetched data is the same", async () => {
const mockFiles = [{ title: "file1", updated_at: "2024-01-01 10:00:00", hashed_id: "1" }];
const mockResponse = { ok: true, json: () => Promise.resolve(mockFiles) };
vi.mocked(global.fetch).mockResolvedValue(mockResponse as Response);

// Initial fetch
await act(async () => {
await useFileStore.getState().fetchFiles();
});

const stateAfterFirstFetch = useFileStore.getState();

// Second fetch with the same data
await act(async () => {
await useFileStore.getState().fetchFiles();
});

const stateAfterSecondFetch = useFileStore.getState();

// The object reference should be the same if no update happened
expect(stateAfterFirstFetch.files).toBe(stateAfterSecondFetch.files);
});
});
104 changes: 104 additions & 0 deletions src/utils/AiService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { describe, it, expect, vi, afterEach, beforeEach } from "vitest";
import { AiService } from "./AiService";
import { refineForeign } from "./ai/refineForeign";

// Mock the refineForeign dependency
vi.mock("./ai/refineForeign", () => ({
refineForeign: vi.fn(),
}));

describe("AiService", () => {
afterEach(() => {
vi.restoreAllMocks();
});

describe("getAiRefinements", () => {
it("should call fetchAiRefinements when VITE_USE_MOCK_AI is not 'true'", async () => {
vi.stubEnv("VITE_USE_MOCK_AI", "false");
const fetchSpy = vi.spyOn(AiService, "fetchAiRefinements").mockResolvedValue([]);

await AiService.getAiRefinements([]);

expect(fetchSpy).toHaveBeenCalled();
});

it("should call fetchAiRefinementsMock when VITE_USE_MOCK_AI is 'true'", async () => {
vi.stubEnv("VITE_USE_MOCK_AI", "true");
const mockFetchSpy = vi.spyOn(AiService, "fetchAiRefinementsMock").mockResolvedValue([]);

await AiService.getAiRefinements([]);

expect(mockFetchSpy).toHaveBeenCalled();
});
});

describe("fetchAiRefinementsMock", () => {
it("should return mock data based on input elements", async () => {
const modifiedElements = ['<p data-unique="p-1">data one</p>', '<p data-unique="p-2">info two</p>'];
const result = await AiService.fetchAiRefinementsMock(modifiedElements);

expect(result).toHaveLength(2);
expect(result[0].target_id).toBe("p-1");
expect(result[0].errors[0].origin_word).toBe("data");
expect(result[0].errors[0].refine_word).toEqual(["자료", "정보"]);
expect(result[1].target_id).toBe("p-2");
expect(result[1].errors[0].origin_word).toBe("info");
});
});

describe("fetchAiRefinements", () => {
beforeEach(() => {
// Mock global fetch
global.fetch = vi.fn();
});

it("should call fetch with the correct parameters and return data", async () => {
const mockResponse = { data: "test" };
const mockJsonPromise = Promise.resolve(mockResponse);
const mockFetchPromise = Promise.resolve({
json: () => mockJsonPromise,
});
vi.mocked(global.fetch).mockReturnValue(mockFetchPromise as Promise<Response>);

const modifiedElements = ["<p>hello</p>"];
const result = await AiService.fetchAiRefinements(modifiedElements);

expect(global.fetch).toHaveBeenCalledWith(
`${import.meta.env.VITE_AI_API_URL}/ai/refine`,
expect.objectContaining({
method: "POST",
body: JSON.stringify({
title: '<p data-unique="e-0">http</p>',
content: modifiedElements,
}),
})
);
expect(result).toEqual(mockResponse);
});
});

describe("fetchAiRefinementsLocal", () => {
it("should yield refined data from the local AI generator", async () => {
const mockGenerator = async function* () {
yield { target_id: "p-1", errors: [{ origin_word: "word", refine_word: ["단어"], index: 0, code: 0 }] };
yield { target_id: "p-2", errors: [{ origin_word: "data", refine_word: ["자료"], index: 0, code: 0 }] };
};

vi.mocked(refineForeign).mockReturnValue(mockGenerator());

const results = [];
const generator = AiService.fetchAiRefinementsLocal([]);
for await (const result of generator) {
results.push(result);
}

expect(results).toHaveLength(2);
expect(results[0].target_id).toBe("p-1");
expect(results[0].errors[0].origin_word).toBe("word");
expect(results[0].errors[0]).toHaveProperty("error_id"); // check that an id was added
expect(results[1].target_id).toBe("p-2");
expect(results[1].errors[0].origin_word).toBe("data");
expect(results[1].errors[0]).toHaveProperty("error_id");
});
});
});
Loading