mirror of
https://github.com/zadam/trilium.git
synced 2025-11-11 17:08:58 +01:00
503 lines
21 KiB
TypeScript
503 lines
21 KiB
TypeScript
import { test, expect } from "@playwright/test";
|
|
import App from "./support/app";
|
|
|
|
const BASE_URL = "http://127.0.0.1:8082";
|
|
|
|
/**
|
|
* E2E tests for exact search functionality using the leading "=" operator.
|
|
*
|
|
* These tests validate the GitHub issue:
|
|
* - Searching for "pagio" returns many false positives (e.g., "page", "pages")
|
|
* - Searching for "=pagio" should return ONLY exact matches for "pagio"
|
|
*/
|
|
|
|
test.describe("Exact Search with Leading = Operator", () => {
|
|
let csrfToken: string;
|
|
let createdNoteIds: string[] = [];
|
|
|
|
test.beforeEach(async ({ page, context }) => {
|
|
const app = new App(page, context);
|
|
await app.goto();
|
|
|
|
// Get CSRF token
|
|
csrfToken = await page.evaluate(() => {
|
|
return (window as any).glob.csrfToken;
|
|
});
|
|
|
|
expect(csrfToken).toBeTruthy();
|
|
|
|
// Create test notes with specific content patterns
|
|
// Note 1: Contains exactly "pagio" in title
|
|
const note1 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
|
headers: { "x-csrf-token": csrfToken },
|
|
data: {
|
|
title: "Test Note with pagio",
|
|
content: "This note contains the word pagio in the content.",
|
|
type: "text"
|
|
}
|
|
});
|
|
expect(note1.ok()).toBeTruthy();
|
|
const note1Data = await note1.json();
|
|
createdNoteIds.push(note1Data.note.noteId);
|
|
|
|
// Note 2: Contains "page" (not exact match)
|
|
const note2 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
|
headers: { "x-csrf-token": csrfToken },
|
|
data: {
|
|
title: "Test Note with page",
|
|
content: "This note contains the word page in the content.",
|
|
type: "text"
|
|
}
|
|
});
|
|
expect(note2.ok()).toBeTruthy();
|
|
const note2Data = await note2.json();
|
|
createdNoteIds.push(note2Data.note.noteId);
|
|
|
|
// Note 3: Contains "pages" (plural, not exact match)
|
|
const note3 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
|
headers: { "x-csrf-token": csrfToken },
|
|
data: {
|
|
title: "Test Note with pages",
|
|
content: "This note contains the word pages in the content.",
|
|
type: "text"
|
|
}
|
|
});
|
|
expect(note3.ok()).toBeTruthy();
|
|
const note3Data = await note3.json();
|
|
createdNoteIds.push(note3Data.note.noteId);
|
|
|
|
// Note 4: Contains "homepage" (contains "page", not exact match)
|
|
const note4 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
|
headers: { "x-csrf-token": csrfToken },
|
|
data: {
|
|
title: "Homepage Note",
|
|
content: "This note is about homepage content.",
|
|
type: "text"
|
|
}
|
|
});
|
|
expect(note4.ok()).toBeTruthy();
|
|
const note4Data = await note4.json();
|
|
createdNoteIds.push(note4Data.note.noteId);
|
|
|
|
// Note 5: Another note with exact "pagio" in content
|
|
const note5 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
|
headers: { "x-csrf-token": csrfToken },
|
|
data: {
|
|
title: "Another pagio Note",
|
|
content: "This is another note with pagio content for testing exact matches.",
|
|
type: "text"
|
|
}
|
|
});
|
|
expect(note5.ok()).toBeTruthy();
|
|
const note5Data = await note5.json();
|
|
createdNoteIds.push(note5Data.note.noteId);
|
|
|
|
// Note 6: Contains "pagio" in title only
|
|
const note6 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
|
headers: { "x-csrf-token": csrfToken },
|
|
data: {
|
|
title: "pagio",
|
|
content: "This note has pagio as the title.",
|
|
type: "text"
|
|
}
|
|
});
|
|
expect(note6.ok()).toBeTruthy();
|
|
const note6Data = await note6.json();
|
|
createdNoteIds.push(note6Data.note.noteId);
|
|
|
|
// Wait a bit for indexing
|
|
await page.waitForTimeout(500);
|
|
});
|
|
|
|
test.afterEach(async ({ page }) => {
|
|
// Clean up created notes
|
|
for (const noteId of createdNoteIds) {
|
|
try {
|
|
const taskId = `cleanup-${Math.random().toString(36).substr(2, 9)}`;
|
|
await page.request.delete(`${BASE_URL}/api/notes/${noteId}?taskId=${taskId}&last=true`, {
|
|
headers: { "x-csrf-token": csrfToken }
|
|
});
|
|
} catch (e) {
|
|
console.error(`Failed to delete note ${noteId}:`, e);
|
|
}
|
|
}
|
|
createdNoteIds = [];
|
|
});
|
|
|
|
test("Quick search without = operator returns all partial matches", async ({ page }) => {
|
|
// Test the /quick-search endpoint without the = operator
|
|
const response = await page.request.get(`${BASE_URL}/api/quick-search/pag`, {
|
|
headers: { "x-csrf-token": csrfToken }
|
|
});
|
|
|
|
expect(response.ok()).toBeTruthy();
|
|
const data = await response.json();
|
|
|
|
// Should return multiple notes including "page", "pages", "homepage"
|
|
expect(data.searchResultNoteIds).toBeDefined();
|
|
expect(data.searchResults).toBeDefined();
|
|
|
|
// Filter to only our test notes
|
|
const testResults = data.searchResults.filter((result: any) =>
|
|
result.noteTitle.includes("page") ||
|
|
result.noteTitle.includes("pagio") ||
|
|
result.noteTitle.includes("Homepage")
|
|
);
|
|
|
|
// Should find at least "page", "pages", "homepage", and "pagio" notes
|
|
expect(testResults.length).toBeGreaterThanOrEqual(4);
|
|
|
|
console.log("Quick search 'pag' found:", testResults.length, "matching notes");
|
|
console.log("Note titles:", testResults.map((r: any) => r.noteTitle));
|
|
});
|
|
|
|
test("Quick search with = operator returns only exact matches", async ({ page }) => {
|
|
// Test the /quick-search endpoint WITH the = operator
|
|
const response = await page.request.get(`${BASE_URL}/api/quick-search/=pagio`, {
|
|
headers: { "x-csrf-token": csrfToken }
|
|
});
|
|
|
|
expect(response.ok()).toBeTruthy();
|
|
const data = await response.json();
|
|
|
|
// Should return only notes with exact "pagio" match
|
|
expect(data.searchResultNoteIds).toBeDefined();
|
|
expect(data.searchResults).toBeDefined();
|
|
|
|
// Filter to only our test notes
|
|
const testResults = data.searchResults.filter((result: any) =>
|
|
createdNoteIds.includes(result.notePath.split("/").pop() || "")
|
|
);
|
|
|
|
console.log("Quick search '=pagio' found:", testResults.length, "matching notes");
|
|
console.log("Note titles:", testResults.map((r: any) => r.noteTitle));
|
|
|
|
// Should find exactly 3 notes: "Test Note with pagio", "Another pagio Note", "pagio"
|
|
expect(testResults.length).toBe(3);
|
|
|
|
// Verify that none of the results contain "page" or "pages" (only "pagio")
|
|
for (const result of testResults) {
|
|
const title = result.noteTitle.toLowerCase();
|
|
const hasPageNotPagio = (title.includes("page") && !title.includes("pagio"));
|
|
expect(hasPageNotPagio).toBe(false);
|
|
}
|
|
});
|
|
|
|
test("Full search API without = operator returns partial matches", async ({ page }) => {
|
|
// Test the /search endpoint without the = operator
|
|
const response = await page.request.get(`${BASE_URL}/api/search/pag`, {
|
|
headers: { "x-csrf-token": csrfToken }
|
|
});
|
|
|
|
expect(response.ok()).toBeTruthy();
|
|
const data = await response.json();
|
|
|
|
// Should return an array of note IDs
|
|
expect(Array.isArray(data)).toBe(true);
|
|
|
|
// Filter to only our test notes
|
|
const testNoteIds = data.filter((id: string) => createdNoteIds.includes(id));
|
|
|
|
console.log("Full search 'pag' found:", testNoteIds.length, "matching notes from our test set");
|
|
|
|
// Should find at least 4 notes
|
|
expect(testNoteIds.length).toBeGreaterThanOrEqual(4);
|
|
});
|
|
|
|
test("Full search API with = operator returns only exact matches", async ({ page }) => {
|
|
// Test the /search endpoint WITH the = operator
|
|
const response = await page.request.get(`${BASE_URL}/api/search/=pagio`, {
|
|
headers: { "x-csrf-token": csrfToken }
|
|
});
|
|
|
|
expect(response.ok()).toBeTruthy();
|
|
const data = await response.json();
|
|
|
|
// Should return an array of note IDs
|
|
expect(Array.isArray(data)).toBe(true);
|
|
|
|
// Filter to only our test notes
|
|
const testNoteIds = data.filter((id: string) => createdNoteIds.includes(id));
|
|
|
|
console.log("Full search '=pagio' found:", testNoteIds.length, "matching notes from our test set");
|
|
|
|
// Should find exactly 3 notes with exact "pagio" match
|
|
expect(testNoteIds.length).toBe(3);
|
|
});
|
|
|
|
test("Exact search operator works with content search", async ({ page }) => {
|
|
// Create a note with "test" in title but different content
|
|
const noteWithTest = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
|
headers: { "x-csrf-token": csrfToken },
|
|
data: {
|
|
title: "Testing Content",
|
|
content: "This note contains the exact word test in content.",
|
|
type: "text"
|
|
}
|
|
});
|
|
expect(noteWithTest.ok()).toBeTruthy();
|
|
const noteWithTestData = await noteWithTest.json();
|
|
const testNoteId = noteWithTestData.note.noteId;
|
|
createdNoteIds.push(testNoteId);
|
|
|
|
// Create a note with "testing" (not exact match)
|
|
const noteWithTesting = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
|
headers: { "x-csrf-token": csrfToken },
|
|
data: {
|
|
title: "Testing More",
|
|
content: "This note has testing in the content.",
|
|
type: "text"
|
|
}
|
|
});
|
|
expect(noteWithTesting.ok()).toBeTruthy();
|
|
const noteWithTestingData = await noteWithTesting.json();
|
|
createdNoteIds.push(noteWithTestingData.note.noteId);
|
|
|
|
await page.waitForTimeout(500);
|
|
|
|
// Search with exact operator
|
|
const response = await page.request.get(`${BASE_URL}/api/quick-search/=test`, {
|
|
headers: { "x-csrf-token": csrfToken }
|
|
});
|
|
|
|
expect(response.ok()).toBeTruthy();
|
|
const data = await response.json();
|
|
|
|
const ourTestNotes = data.searchResults.filter((result: any) => {
|
|
const noteId = result.notePath.split("/").pop();
|
|
return noteId === testNoteId || noteId === noteWithTestingData.note.noteId;
|
|
});
|
|
|
|
console.log("Exact search '=test' found our test notes:", ourTestNotes.length);
|
|
console.log("Note titles:", ourTestNotes.map((r: any) => r.noteTitle));
|
|
|
|
// Should find the note with exact "test" match, but not "testing"
|
|
// Note: This test may fail if the implementation doesn't properly handle exact matching in content
|
|
expect(ourTestNotes.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("Exact search is case-insensitive", async ({ page }) => {
|
|
// Create notes with different case variations
|
|
const noteUpper = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
|
headers: { "x-csrf-token": csrfToken },
|
|
data: {
|
|
title: "EXACT MATCH",
|
|
content: "This note has EXACT in uppercase.",
|
|
type: "text"
|
|
}
|
|
});
|
|
expect(noteUpper.ok()).toBeTruthy();
|
|
const noteUpperData = await noteUpper.json();
|
|
createdNoteIds.push(noteUpperData.note.noteId);
|
|
|
|
const noteLower = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
|
headers: { "x-csrf-token": csrfToken },
|
|
data: {
|
|
title: "exact match",
|
|
content: "This note has exact in lowercase.",
|
|
type: "text"
|
|
}
|
|
});
|
|
expect(noteLower.ok()).toBeTruthy();
|
|
const noteLowerData = await noteLower.json();
|
|
createdNoteIds.push(noteLowerData.note.noteId);
|
|
|
|
await page.waitForTimeout(500);
|
|
|
|
// Search with exact operator in lowercase
|
|
const response = await page.request.get(`${BASE_URL}/api/quick-search/=exact`, {
|
|
headers: { "x-csrf-token": csrfToken }
|
|
});
|
|
|
|
expect(response.ok()).toBeTruthy();
|
|
const data = await response.json();
|
|
|
|
const ourTestNotes = data.searchResults.filter((result: any) => {
|
|
const noteId = result.notePath.split("/").pop();
|
|
return noteId === noteUpperData.note.noteId || noteId === noteLowerData.note.noteId;
|
|
});
|
|
|
|
console.log("Case-insensitive exact search found:", ourTestNotes.length, "notes");
|
|
|
|
// Should find both uppercase and lowercase versions
|
|
expect(ourTestNotes.length).toBe(2);
|
|
});
|
|
|
|
test("Exact phrase matching with multi-word searches", async ({ page }) => {
|
|
// Create notes with various phrase patterns
|
|
const note1 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
|
headers: { "x-csrf-token": csrfToken },
|
|
data: {
|
|
title: "exact phrase",
|
|
content: "This note contains the exact phrase.",
|
|
type: "text"
|
|
}
|
|
});
|
|
expect(note1.ok()).toBeTruthy();
|
|
const note1Data = await note1.json();
|
|
createdNoteIds.push(note1Data.note.noteId);
|
|
|
|
const note2 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
|
headers: { "x-csrf-token": csrfToken },
|
|
data: {
|
|
title: "exact phrase match",
|
|
content: "This note has exact phrase followed by more words.",
|
|
type: "text"
|
|
}
|
|
});
|
|
expect(note2.ok()).toBeTruthy();
|
|
const note2Data = await note2.json();
|
|
createdNoteIds.push(note2Data.note.noteId);
|
|
|
|
const note3 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
|
headers: { "x-csrf-token": csrfToken },
|
|
data: {
|
|
title: "phrase exact",
|
|
content: "This note has the words in reverse order.",
|
|
type: "text"
|
|
}
|
|
});
|
|
expect(note3.ok()).toBeTruthy();
|
|
const note3Data = await note3.json();
|
|
createdNoteIds.push(note3Data.note.noteId);
|
|
|
|
const note4 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
|
headers: { "x-csrf-token": csrfToken },
|
|
data: {
|
|
title: "this exact and that phrase",
|
|
content: "Words are separated but both present.",
|
|
type: "text"
|
|
}
|
|
});
|
|
expect(note4.ok()).toBeTruthy();
|
|
const note4Data = await note4.json();
|
|
createdNoteIds.push(note4Data.note.noteId);
|
|
|
|
await page.waitForTimeout(500);
|
|
|
|
// Search for exact phrase "exact phrase"
|
|
const response = await page.request.get(`${BASE_URL}/api/quick-search/='exact phrase'`, {
|
|
headers: { "x-csrf-token": csrfToken }
|
|
});
|
|
|
|
expect(response.ok()).toBeTruthy();
|
|
const data = await response.json();
|
|
|
|
const ourTestNotes = data.searchResults.filter((result: any) => {
|
|
const noteId = result.notePath.split("/").pop();
|
|
return [note1Data.note.noteId, note2Data.note.noteId, note3Data.note.noteId, note4Data.note.noteId].includes(noteId || "");
|
|
});
|
|
|
|
console.log("Exact phrase search '=\"exact phrase\"' found:", ourTestNotes.length, "notes");
|
|
console.log("Note titles:", ourTestNotes.map((r: any) => r.noteTitle));
|
|
|
|
// Should find only notes 1 and 2 (consecutive "exact phrase")
|
|
// Should NOT find note 3 (reversed order) or note 4 (words separated)
|
|
expect(ourTestNotes.length).toBe(2);
|
|
|
|
const foundTitles = ourTestNotes.map((r: any) => r.noteTitle);
|
|
expect(foundTitles).toContain("exact phrase");
|
|
expect(foundTitles).toContain("exact phrase match");
|
|
expect(foundTitles).not.toContain("phrase exact");
|
|
expect(foundTitles).not.toContain("this exact and that phrase");
|
|
});
|
|
|
|
test("Exact phrase matching respects word order", async ({ page }) => {
|
|
// Create notes to test word order sensitivity
|
|
const noteForward = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
|
headers: { "x-csrf-token": csrfToken },
|
|
data: {
|
|
title: "Testing Order",
|
|
content: "This is a test sentence for verification.",
|
|
type: "text"
|
|
}
|
|
});
|
|
expect(noteForward.ok()).toBeTruthy();
|
|
const noteForwardData = await noteForward.json();
|
|
createdNoteIds.push(noteForwardData.note.noteId);
|
|
|
|
const noteReverse = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
|
headers: { "x-csrf-token": csrfToken },
|
|
data: {
|
|
title: "Order Testing",
|
|
content: "A sentence test is this for verification.",
|
|
type: "text"
|
|
}
|
|
});
|
|
expect(noteReverse.ok()).toBeTruthy();
|
|
const noteReverseData = await noteReverse.json();
|
|
createdNoteIds.push(noteReverseData.note.noteId);
|
|
|
|
await page.waitForTimeout(500);
|
|
|
|
// Search for exact phrase "test sentence"
|
|
const response = await page.request.get(`${BASE_URL}/api/quick-search/='test sentence'`, {
|
|
headers: { "x-csrf-token": csrfToken }
|
|
});
|
|
|
|
expect(response.ok()).toBeTruthy();
|
|
const data = await response.json();
|
|
|
|
const ourTestNotes = data.searchResults.filter((result: any) => {
|
|
const noteId = result.notePath.split("/").pop();
|
|
return noteId === noteForwardData.note.noteId || noteId === noteReverseData.note.noteId;
|
|
});
|
|
|
|
console.log("Exact phrase search '=\"test sentence\"' found:", ourTestNotes.length, "notes");
|
|
console.log("Note titles:", ourTestNotes.map((r: any) => r.noteTitle));
|
|
|
|
// Should find only the forward order note
|
|
expect(ourTestNotes.length).toBe(1);
|
|
expect(ourTestNotes[0].noteTitle).toBe("Testing Order");
|
|
});
|
|
|
|
test("Multi-word exact search without quotes", async ({ page }) => {
|
|
// Test that multi-word search with = but without quotes also does exact phrase matching
|
|
const notePhrase = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
|
headers: { "x-csrf-token": csrfToken },
|
|
data: {
|
|
title: "Quick Test Note",
|
|
content: "A simple note for multi word testing.",
|
|
type: "text"
|
|
}
|
|
});
|
|
expect(notePhrase.ok()).toBeTruthy();
|
|
const notePhraseData = await notePhrase.json();
|
|
createdNoteIds.push(notePhraseData.note.noteId);
|
|
|
|
const noteScattered = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
|
headers: { "x-csrf-token": csrfToken },
|
|
data: {
|
|
title: "Word Multi Testing",
|
|
content: "Words are multi scattered in this testing example.",
|
|
type: "text"
|
|
}
|
|
});
|
|
expect(noteScattered.ok()).toBeTruthy();
|
|
const noteScatteredData = await noteScattered.json();
|
|
createdNoteIds.push(noteScatteredData.note.noteId);
|
|
|
|
await page.waitForTimeout(500);
|
|
|
|
// Search for "=multi word" without quotes (parser tokenizes as two words)
|
|
const response = await page.request.get(`${BASE_URL}/api/quick-search/=multi word`, {
|
|
headers: { "x-csrf-token": csrfToken }
|
|
});
|
|
|
|
expect(response.ok()).toBeTruthy();
|
|
const data = await response.json();
|
|
|
|
const ourTestNotes = data.searchResults.filter((result: any) => {
|
|
const noteId = result.notePath.split("/").pop();
|
|
return noteId === notePhraseData.note.noteId || noteId === noteScatteredData.note.noteId;
|
|
});
|
|
|
|
console.log("Multi-word exact search '=multi word' found:", ourTestNotes.length, "notes");
|
|
console.log("Note titles:", ourTestNotes.map((r: any) => r.noteTitle));
|
|
|
|
// Should find only the note with consecutive "multi word" phrase
|
|
expect(ourTestNotes.length).toBe(1);
|
|
expect(ourTestNotes[0].noteTitle).toBe("Quick Test Note");
|
|
});
|
|
});
|