Files
memohanzi/src/actions/collections.integration.test.ts
2025-11-21 09:51:16 +01:00

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("好")
})
})
})