593 lines
16 KiB
TypeScript
593 lines
16 KiB
TypeScript
import { describe, it, expect, beforeEach, beforeAll } from "vitest"
|
|
import { prisma } from "@/lib/prisma"
|
|
|
|
/**
|
|
* Integration tests for collections functionality
|
|
* These tests focus on database operations and data integrity
|
|
*/
|
|
|
|
describe("Collections Database Operations", () => {
|
|
let testUser: any
|
|
let englishLanguage: any
|
|
|
|
beforeEach(async () => {
|
|
// Note: Global beforeEach in vitest.integration.setup.ts clears all data
|
|
// So we need to recreate user and language in each test
|
|
|
|
// Create English language
|
|
englishLanguage = await prisma.language.create({
|
|
data: {
|
|
code: "en",
|
|
name: "English",
|
|
nativeName: "English",
|
|
isActive: true,
|
|
},
|
|
})
|
|
|
|
// Create test user
|
|
testUser = await prisma.user.create({
|
|
data: {
|
|
email: "testcollections@example.com",
|
|
name: "Test User",
|
|
password: "dummy",
|
|
role: "USER",
|
|
isActive: true,
|
|
},
|
|
})
|
|
})
|
|
|
|
describe("Collection CRUD Operations", () => {
|
|
it("should create a collection", async () => {
|
|
const collection = await prisma.collection.create({
|
|
data: {
|
|
name: "Test Collection",
|
|
description: "Test description",
|
|
isPublic: false,
|
|
isGlobal: false,
|
|
createdBy: testUser.id,
|
|
},
|
|
})
|
|
|
|
expect(collection.id).toBeDefined()
|
|
expect(collection.name).toBe("Test Collection")
|
|
expect(collection.createdBy).toBe(testUser.id)
|
|
})
|
|
|
|
it("should update a collection", async () => {
|
|
const collection = await prisma.collection.create({
|
|
data: {
|
|
name: "Test Collection",
|
|
isPublic: false,
|
|
isGlobal: false,
|
|
createdBy: testUser.id,
|
|
},
|
|
})
|
|
|
|
const updated = await prisma.collection.update({
|
|
where: { id: collection.id },
|
|
data: {
|
|
name: "Updated Name",
|
|
description: "New description",
|
|
},
|
|
})
|
|
|
|
expect(updated.name).toBe("Updated Name")
|
|
expect(updated.description).toBe("New description")
|
|
})
|
|
|
|
it("should delete a collection", async () => {
|
|
const collection = await prisma.collection.create({
|
|
data: {
|
|
name: "Test Collection",
|
|
isPublic: false,
|
|
isGlobal: false,
|
|
createdBy: testUser.id,
|
|
},
|
|
})
|
|
|
|
await prisma.collection.delete({
|
|
where: { id: collection.id },
|
|
})
|
|
|
|
const deleted = await prisma.collection.findUnique({
|
|
where: { id: collection.id },
|
|
})
|
|
|
|
expect(deleted).toBeNull()
|
|
})
|
|
|
|
it("should get user collections", async () => {
|
|
await prisma.collection.createMany({
|
|
data: [
|
|
{
|
|
name: "Collection 1",
|
|
isPublic: false,
|
|
isGlobal: false,
|
|
createdBy: testUser.id,
|
|
},
|
|
{
|
|
name: "Collection 2",
|
|
isPublic: true,
|
|
isGlobal: false,
|
|
createdBy: testUser.id,
|
|
},
|
|
],
|
|
})
|
|
|
|
const collections = await prisma.collection.findMany({
|
|
where: { createdBy: testUser.id },
|
|
})
|
|
|
|
expect(collections.length).toBe(2)
|
|
})
|
|
|
|
it("should get global collections", async () => {
|
|
await prisma.collection.create({
|
|
data: {
|
|
name: "HSK 1",
|
|
isPublic: true,
|
|
isGlobal: true,
|
|
createdBy: null,
|
|
},
|
|
})
|
|
|
|
const globalCollections = await prisma.collection.findMany({
|
|
where: { isGlobal: true },
|
|
})
|
|
|
|
expect(globalCollections.length).toBe(1)
|
|
expect(globalCollections[0].name).toBe("HSK 1")
|
|
})
|
|
})
|
|
|
|
describe("Collection Items and OrderIndex", () => {
|
|
let collection: any
|
|
let hanzi1: any
|
|
let hanzi2: any
|
|
let hanzi3: any
|
|
|
|
beforeEach(async () => {
|
|
// Create collection
|
|
collection = await prisma.collection.create({
|
|
data: {
|
|
name: "Test Collection",
|
|
isPublic: false,
|
|
isGlobal: false,
|
|
createdBy: testUser.id,
|
|
},
|
|
})
|
|
|
|
// Create hanzi
|
|
hanzi1 = await prisma.hanzi.create({
|
|
data: { simplified: "好", radical: "女", frequency: 100 },
|
|
})
|
|
hanzi2 = await prisma.hanzi.create({
|
|
data: { simplified: "爱", radical: "爫", frequency: 200 },
|
|
})
|
|
hanzi3 = await prisma.hanzi.create({
|
|
data: { simplified: "你", radical: "人", frequency: 50 },
|
|
})
|
|
})
|
|
|
|
it("should add hanzi to collection with correct orderIndex", async () => {
|
|
await prisma.collectionItem.createMany({
|
|
data: [
|
|
{ collectionId: collection.id, hanziId: hanzi1.id, orderIndex: 0 },
|
|
{ collectionId: collection.id, hanziId: hanzi2.id, orderIndex: 1 },
|
|
{ collectionId: collection.id, hanziId: hanzi3.id, orderIndex: 2 },
|
|
],
|
|
})
|
|
|
|
const items = await prisma.collectionItem.findMany({
|
|
where: { collectionId: collection.id },
|
|
orderBy: { orderIndex: "asc" },
|
|
include: { hanzi: true },
|
|
})
|
|
|
|
expect(items.length).toBe(3)
|
|
expect(items[0].hanzi.simplified).toBe("好")
|
|
expect(items[0].orderIndex).toBe(0)
|
|
expect(items[1].hanzi.simplified).toBe("爱")
|
|
expect(items[1].orderIndex).toBe(1)
|
|
expect(items[2].hanzi.simplified).toBe("你")
|
|
expect(items[2].orderIndex).toBe(2)
|
|
})
|
|
|
|
it("should preserve orderIndex after removing middle item", async () => {
|
|
// Add three items
|
|
await prisma.collectionItem.createMany({
|
|
data: [
|
|
{ collectionId: collection.id, hanziId: hanzi1.id, orderIndex: 0 },
|
|
{ collectionId: collection.id, hanziId: hanzi2.id, orderIndex: 1 },
|
|
{ collectionId: collection.id, hanziId: hanzi3.id, orderIndex: 2 },
|
|
],
|
|
})
|
|
|
|
// Remove middle item
|
|
await prisma.collectionItem.deleteMany({
|
|
where: {
|
|
collectionId: collection.id,
|
|
hanziId: hanzi2.id,
|
|
},
|
|
})
|
|
|
|
const items = await prisma.collectionItem.findMany({
|
|
where: { collectionId: collection.id },
|
|
orderBy: { orderIndex: "asc" },
|
|
include: { hanzi: true },
|
|
})
|
|
|
|
expect(items.length).toBe(2)
|
|
expect(items[0].hanzi.simplified).toBe("好")
|
|
expect(items[0].orderIndex).toBe(0)
|
|
expect(items[1].hanzi.simplified).toBe("你")
|
|
expect(items[1].orderIndex).toBe(2) // Should keep original orderIndex
|
|
})
|
|
|
|
it("should prevent duplicate hanzi in collection (unique constraint)", async () => {
|
|
await prisma.collectionItem.create({
|
|
data: {
|
|
collectionId: collection.id,
|
|
hanziId: hanzi1.id,
|
|
orderIndex: 0,
|
|
},
|
|
})
|
|
|
|
// Try to add same hanzi again
|
|
await expect(
|
|
prisma.collectionItem.create({
|
|
data: {
|
|
collectionId: collection.id,
|
|
hanziId: hanzi1.id,
|
|
orderIndex: 1,
|
|
},
|
|
})
|
|
).rejects.toThrow()
|
|
})
|
|
|
|
it("should allow same hanzi in different collections", async () => {
|
|
const collection2 = await prisma.collection.create({
|
|
data: {
|
|
name: "Second Collection",
|
|
isPublic: false,
|
|
isGlobal: false,
|
|
createdBy: testUser.id,
|
|
},
|
|
})
|
|
|
|
await prisma.collectionItem.createMany({
|
|
data: [
|
|
{ collectionId: collection.id, hanziId: hanzi1.id, orderIndex: 0 },
|
|
{ collectionId: collection2.id, hanziId: hanzi1.id, orderIndex: 0 },
|
|
],
|
|
})
|
|
|
|
const items1 = await prisma.collectionItem.count({
|
|
where: { collectionId: collection.id },
|
|
})
|
|
const items2 = await prisma.collectionItem.count({
|
|
where: { collectionId: collection2.id },
|
|
})
|
|
|
|
expect(items1).toBe(1)
|
|
expect(items2).toBe(1)
|
|
})
|
|
|
|
it("should delete collection items when collection is deleted (cascade)", async () => {
|
|
await prisma.collectionItem.createMany({
|
|
data: [
|
|
{ collectionId: collection.id, hanziId: hanzi1.id, orderIndex: 0 },
|
|
{ collectionId: collection.id, hanziId: hanzi2.id, orderIndex: 1 },
|
|
],
|
|
})
|
|
|
|
await prisma.collection.delete({
|
|
where: { id: collection.id },
|
|
})
|
|
|
|
const items = await prisma.collectionItem.findMany({
|
|
where: { collectionId: collection.id },
|
|
})
|
|
|
|
expect(items.length).toBe(0)
|
|
})
|
|
})
|
|
|
|
describe("Hanzi Search Operations", () => {
|
|
beforeEach(async () => {
|
|
// Create hanzi with forms, transcriptions, and meanings
|
|
const hanzi1 = await prisma.hanzi.create({
|
|
data: { simplified: "好", radical: "女", frequency: 100 },
|
|
})
|
|
const form1 = await prisma.hanziForm.create({
|
|
data: {
|
|
hanziId: hanzi1.id,
|
|
traditional: "好",
|
|
isDefault: true,
|
|
},
|
|
})
|
|
await prisma.hanziTranscription.create({
|
|
data: {
|
|
formId: form1.id,
|
|
type: "pinyin",
|
|
value: "hǎo",
|
|
},
|
|
})
|
|
await prisma.hanziMeaning.create({
|
|
data: {
|
|
formId: form1.id,
|
|
languageId: englishLanguage.id,
|
|
meaning: "good",
|
|
orderIndex: 0,
|
|
},
|
|
})
|
|
|
|
const hanzi2 = await prisma.hanzi.create({
|
|
data: { simplified: "你", radical: "人", frequency: 50 },
|
|
})
|
|
const form2 = await prisma.hanziForm.create({
|
|
data: {
|
|
hanziId: hanzi2.id,
|
|
traditional: "你",
|
|
isDefault: true,
|
|
},
|
|
})
|
|
await prisma.hanziTranscription.create({
|
|
data: {
|
|
formId: form2.id,
|
|
type: "pinyin",
|
|
value: "nǐ",
|
|
},
|
|
})
|
|
await prisma.hanziMeaning.create({
|
|
data: {
|
|
formId: form2.id,
|
|
languageId: englishLanguage.id,
|
|
meaning: "you",
|
|
orderIndex: 0,
|
|
},
|
|
})
|
|
|
|
const hanzi3 = await prisma.hanzi.create({
|
|
data: { simplified: "中国", radical: null, frequency: 300 },
|
|
})
|
|
const form3 = await prisma.hanziForm.create({
|
|
data: {
|
|
hanziId: hanzi3.id,
|
|
traditional: "中國",
|
|
isDefault: true,
|
|
},
|
|
})
|
|
await prisma.hanziTranscription.create({
|
|
data: {
|
|
formId: form3.id,
|
|
type: "pinyin",
|
|
value: "zhōng guó",
|
|
},
|
|
})
|
|
await prisma.hanziMeaning.create({
|
|
data: {
|
|
formId: form3.id,
|
|
languageId: englishLanguage.id,
|
|
meaning: "China",
|
|
orderIndex: 0,
|
|
},
|
|
})
|
|
})
|
|
|
|
it("should search hanzi by simplified character", async () => {
|
|
const results = await prisma.hanzi.findMany({
|
|
where: {
|
|
simplified: { contains: "好" },
|
|
},
|
|
include: {
|
|
forms: {
|
|
where: { isDefault: true },
|
|
include: {
|
|
transcriptions: { where: { type: "pinyin" }, take: 1 },
|
|
meanings: { orderBy: { orderIndex: "asc" }, take: 1 },
|
|
},
|
|
take: 1,
|
|
},
|
|
},
|
|
})
|
|
|
|
expect(results.length).toBeGreaterThan(0)
|
|
expect(results.some((h) => h.simplified === "好")).toBe(true)
|
|
})
|
|
|
|
it("should search hanzi by pinyin transcription", async () => {
|
|
const results = await prisma.hanzi.findMany({
|
|
where: {
|
|
forms: {
|
|
some: {
|
|
transcriptions: {
|
|
some: {
|
|
value: { contains: "hǎo" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
include: {
|
|
forms: {
|
|
where: { isDefault: true },
|
|
include: {
|
|
transcriptions: { where: { type: "pinyin" }, take: 1 },
|
|
},
|
|
take: 1,
|
|
},
|
|
},
|
|
})
|
|
|
|
expect(results.length).toBeGreaterThan(0)
|
|
expect(results.some((h) => h.simplified === "好")).toBe(true)
|
|
})
|
|
|
|
it("should search hanzi by meaning", async () => {
|
|
const results = await prisma.hanzi.findMany({
|
|
where: {
|
|
forms: {
|
|
some: {
|
|
meanings: {
|
|
some: {
|
|
meaning: { contains: "good" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
include: {
|
|
forms: {
|
|
where: { isDefault: true },
|
|
include: {
|
|
meanings: { orderBy: { orderIndex: "asc" }, take: 1 },
|
|
},
|
|
take: 1,
|
|
},
|
|
},
|
|
})
|
|
|
|
expect(results.length).toBeGreaterThan(0)
|
|
expect(results.some((h) => h.simplified === "好")).toBe(true)
|
|
})
|
|
|
|
it("should support multi-character hanzi in database", async () => {
|
|
const multiChar = await prisma.hanzi.findUnique({
|
|
where: { simplified: "中国" },
|
|
include: {
|
|
forms: {
|
|
where: { isDefault: true },
|
|
include: {
|
|
transcriptions: true,
|
|
meanings: true,
|
|
},
|
|
take: 1,
|
|
},
|
|
},
|
|
})
|
|
|
|
expect(multiChar).toBeDefined()
|
|
expect(multiChar?.simplified).toBe("中国")
|
|
expect(multiChar?.forms[0].traditional).toBe("中國")
|
|
expect(multiChar?.forms[0].transcriptions[0].value).toBe("zhōng guó")
|
|
expect(multiChar?.forms[0].meanings[0].meaning).toBe("China")
|
|
})
|
|
})
|
|
|
|
describe("Hanzi List Parsing Logic", () => {
|
|
beforeEach(async () => {
|
|
// Create test hanzi
|
|
await prisma.hanzi.createMany({
|
|
data: [
|
|
{ simplified: "好", radical: "女", frequency: 100 },
|
|
{ simplified: "爱", radical: "爫", frequency: 200 },
|
|
{ simplified: "你", radical: "人", frequency: 50 },
|
|
{ simplified: "中国", radical: null, frequency: 300 },
|
|
],
|
|
})
|
|
})
|
|
|
|
it("should parse newline-separated list", () => {
|
|
const input = "好\n爱\n你"
|
|
const parsed = input
|
|
.split(/[\n,\s]+/)
|
|
.map((s) => s.trim())
|
|
.filter((s) => s.length > 0)
|
|
|
|
expect(parsed).toEqual(["好", "爱", "你"])
|
|
})
|
|
|
|
it("should parse comma-separated list", () => {
|
|
const input = "好, 爱, 你"
|
|
const parsed = input
|
|
.split(/[\n,\s]+/)
|
|
.map((s) => s.trim())
|
|
.filter((s) => s.length > 0)
|
|
|
|
expect(parsed).toEqual(["好", "爱", "你"])
|
|
})
|
|
|
|
it("should parse space-separated list", () => {
|
|
const input = "好 爱 你"
|
|
const parsed = input
|
|
.split(/[\n,\s]+/)
|
|
.map((s) => s.trim())
|
|
.filter((s) => s.length > 0)
|
|
|
|
expect(parsed).toEqual(["好", "爱", "你"])
|
|
})
|
|
|
|
it("should handle multi-character words", () => {
|
|
const input = "好 中国 你"
|
|
const parsed = input
|
|
.split(/[\n,\s]+/)
|
|
.map((s) => s.trim())
|
|
.filter((s) => s.length > 0)
|
|
|
|
expect(parsed).toEqual(["好", "中国", "你"])
|
|
})
|
|
|
|
it("should detect duplicates", () => {
|
|
const input = "好 爱 好 你"
|
|
const chars = input
|
|
.split(/[\n,\s]+/)
|
|
.map((s) => s.trim())
|
|
.filter((s) => s.length > 0)
|
|
|
|
const seen = new Set<string>()
|
|
const duplicates: string[] = []
|
|
const unique = chars.filter((char) => {
|
|
if (seen.has(char)) {
|
|
if (!duplicates.includes(char)) {
|
|
duplicates.push(char)
|
|
}
|
|
return false
|
|
}
|
|
seen.add(char)
|
|
return true
|
|
})
|
|
|
|
expect(unique).toEqual(["好", "爱", "你"])
|
|
expect(duplicates).toEqual(["好"])
|
|
})
|
|
|
|
it("should validate all hanzi exist (strict mode)", async () => {
|
|
const input = ["好", "不存在", "你"]
|
|
|
|
const foundHanzi = await prisma.hanzi.findMany({
|
|
where: {
|
|
simplified: { in: input },
|
|
},
|
|
})
|
|
|
|
const foundSet = new Set(foundHanzi.map((h) => h.simplified))
|
|
const notFound = input.filter((char) => !foundSet.has(char))
|
|
|
|
expect(notFound).toEqual(["不存在"])
|
|
})
|
|
|
|
it("should preserve order from input", async () => {
|
|
const input = ["你", "爱", "好"]
|
|
|
|
const foundHanzi = await prisma.hanzi.findMany({
|
|
where: {
|
|
simplified: { in: input },
|
|
},
|
|
})
|
|
|
|
// Create a map for quick lookup
|
|
const hanziMap = new Map(foundHanzi.map((h) => [h.simplified, h]))
|
|
|
|
// Preserve input order
|
|
const ordered = input
|
|
.map((char) => hanziMap.get(char))
|
|
.filter((h): h is NonNullable<typeof h> => h !== undefined)
|
|
|
|
expect(ordered[0].simplified).toBe("你")
|
|
expect(ordered[1].simplified).toBe("爱")
|
|
expect(ordered[2].simplified).toBe("好")
|
|
})
|
|
})
|
|
})
|