Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
15 changes: 13 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 12 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
"lint": "eslint src",
"test": "vitest",
"test:coverage": "vitest run --coverage",
"test:search": "node scripts/test-search.js",
"test:search:interactive": "node scripts/test-search.js -i",
"end2end": "npx playwright test",
"dev": "vite dev",
"emu": "npm run env && (cd functions && npm run build) && firebase emulators:start --project=demo-wordplay",
Expand Down Expand Up @@ -49,10 +51,6 @@
"devDependencies": {
"@google-cloud/translate": "^9",
"@playwright/test": "^1",
"svelte": "^5",
"svelte-check": "^4",
"svelte-jester": "^5",
"svelte-preprocess": "^6.0",
"@sveltejs/adapter-static": "^3",
"@sveltejs/kit": "^2.20.6",
"@sveltejs/vite-plugin-svelte": "^5",
Expand All @@ -71,6 +69,10 @@
"prettier": "^3",
"prettier-plugin-svelte": "^3",
"run-script-os": "^1",
"svelte": "^5",
"svelte-check": "^4",
"svelte-jester": "^5",
"svelte-preprocess": "^6.0",
"ts-jest": "^29",
"ts-json-schema-generator": "^2",
"tslib": "^2",
Expand All @@ -90,19 +92,23 @@
"firebase-admin": "^13",
"firebase-functions": "^6",
"firebase-functions-test": "^3",
"fuse.js": "^7.1.0",
"graphemer": "^1.4.0",
"matter-js": "^0.20",
"pitchy": "^4.1.0",
"recoverable-random": "^1.0.5",
"shared-types": "file:./functions/src/shared",
"uuid": "^11",
"zod": "^3.23.8",
"shared-types": "file:./functions/src/shared"
"zod": "^3.23.8"
},
"browserslist": [
"defaults",
"not op_mini all"
],
"bundledDependencies": [
"shared-types"
],
"bundleDependencies": [
"shared-types"
]
}
197 changes: 197 additions & 0 deletions scripts/test-search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
#!/usr/bin/env node

/**
* Console Testing Script for Search Functionality
*
* This script allows you to test the search functionality from the command line
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test should be integrated into the vitest infrastructure we have, rather than creating a standalone script. Write this using the test APIs so it's included in the standard test suite.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that there is a vitest test. What is this for then? Is this redundant? If so, remove it.

* without needing to run the full application.
*/

import Fuse from 'fuse.js';

// Mock project data for testing
const mockProjects = [
{
project: { getName: () => 'Object Oriented Programming', getSources: () => [] },
name: 'Object Oriented Programming',
files: []
},
{
project: { getName: () => 'Data Structures & Algorithms', getSources: () => [] },
name: 'Data Structures & Algorithms',
files: []
},
{
project: { getName: () => 'Test Project', getSources: () => [] },
name: 'Test Project',
files: []
},
{
project: {
getName: () => 'Main Project',
getSources: () => [
{ getPreferredName: () => 'main.wp' },
{ getPreferredName: () => 'utils.js' }
]
},
name: 'Main Project',
files: [
{ name: 'main.wp' },
{ name: 'utils.js' }
]
},
{
project: { getName: () => 'React Tutorial', getSources: () => [] },
name: 'React Tutorial',
files: []
},
{
project: { getName: () => 'JavaScript Basics', getSources: () => [] },
name: 'JavaScript Basics',
files: []
},
// Add archived projects for testing
{
project: { getName: () => 'Archived Math Project', getSources: () => [] },
name: 'Archived Math Project',
files: []
},
{
project: { getName: () => 'Old Science Experiment', getSources: () => [] },
name: 'Old Science Experiment',
files: []
}
];

// Fuse.js configuration (same as in the app)
const fuseOptions = {
includeScore: true,
threshold: 0.4,
ignoreLocation: true,
keys: ['name', 'files.name']
};

const fuse = new Fuse(mockProjects, fuseOptions);

// Test cases
const testCases = [
// Exact matches
'Object Oriented Programming',
'Test Project',
'main.wp',

// Fuzzy matches with typos
'Objct',
'algoritm',
'projct',
'Testt',

// Partial matches
'Object',
'Test',
'main',
'React',

// Case insensitive
'object',
'TEST',
'MAIN',

// Archived project searches
'Archived',
'Math',
'Science',
'Experiment',

// No matches
'nonexistent',
'xyz123',
'',

// Special characters
'test@',
'test#',
'test$'
];

function testSearch(searchTerm) {
console.log(`\n🔍 Testing: "${searchTerm}"`);
console.log('─'.repeat(50));

const results = fuse.search(searchTerm);

if (results.length === 0) {
console.log('❌ No results found');
return;
}

console.log(`✅ Found ${results.length} result(s):`);

results.forEach((result, index) => {
const score = result.score ? result.score.toFixed(3) : 'N/A';
console.log(` ${index + 1}. ${result.item.name} (score: ${score})`);

if (result.item.files.length > 0) {
console.log(` Files: ${result.item.files.map(f => f.name).join(', ')}`);
}
});
}

function runAllTests() {
console.log('🧪 Search Functionality Test Suite');
console.log('='.repeat(50));
console.log(`Testing ${testCases.length} search terms...`);

testCases.forEach(testSearch);

console.log('\n🎉 All tests completed!');
}

function interactiveMode() {
console.log('🎮 Interactive Search Testing Mode');
console.log('Type search terms to test (or "quit" to exit)');
console.log('─'.repeat(50));

const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});

const askQuestion = () => {
rl.question('🔍 Enter search term: ', (searchTerm) => {
if (searchTerm.toLowerCase() === 'quit') {
console.log('👋 Goodbye!');
rl.close();
return;
}

testSearch(searchTerm);
askQuestion();
});
};

askQuestion();
}

// Main execution
const args = process.argv.slice(2);

if (args.includes('--interactive') || args.includes('-i')) {
interactiveMode();
} else if (args.includes('--help') || args.includes('-h')) {
console.log(`
Search Testing Script
Usage:
node test-search.js # Run all test cases
node test-search.js -i # Interactive mode
node test-search.js --help # Show this help
Options:
-i, --interactive Run in interactive mode
-h, --help Show this help message
`);
} else {
runAllTests();
}
35 changes: 33 additions & 2 deletions src/components/app/ProjectPreview.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
children?: import('svelte').Snippet;
anonymize?: boolean;
showCollaborators?: boolean;
/** Search term for highlighting matches in project names */
searchTerm?: string;
}

function findCharacterName(value: Value): string | null {
Expand Down Expand Up @@ -71,6 +73,7 @@
children,
anonymize = true,
showCollaborators = false,
searchTerm = '',
}: Props = $props();

// Clone the project and get its initial value, then stop the project's evaluator.
Expand Down Expand Up @@ -166,6 +169,26 @@
const user = getUser();

let path = $derived(link ?? project.getLink(true));

// Highlight matching text in search results
function highlightText(text: string, searchTerm: string): string {
if (!searchTerm.trim()) return text;

const searchLower = searchTerm.toLowerCase();
const textLower = text.toLowerCase();

// First try exact substring match for highlighting
const index = textLower.indexOf(searchLower);
if (index !== -1) {
const before = text.substring(0, index);
const match = text.substring(index, index + searchTerm.length);
const after = text.substring(index + searchTerm.length);
return `${before}<mark class="search-highlight">${match}</mark>${after}`;
}

// If no exact match, don't highlight (fuzzy matches are found but not highlighted)
return text;
}
/** See if this is a public project being viewed by someone who isn't a creator or collaborator */
let audience = $derived(isAudience($user, project));

Expand Down Expand Up @@ -223,14 +246,14 @@
{#if name}
<div class="name">
{#if action}
{project.getName()}
{@html highlightText(project.getName(), searchTerm)}
{:else}
<Link to={path}>
{#if project.getName().length === 0}<em class="untitled"
>&mdash;</em
>
{:else}
{project.getName()}{/if}</Link
{@html highlightText(project.getName(), searchTerm)}{/if}</Link
>
{#if navigating && `${navigating.to?.url.pathname}${navigating.to?.url.search}` === path}
<Spinning />{:else}{@render children?.()}
Expand Down Expand Up @@ -353,4 +376,12 @@
gap: var(--wordplay-spacing);
row-gap: var(--wordplay-spacing);
}

:global(.search-highlight) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use global CSS for a local style class. Instead of returning raw HTML and. using an @html directive, just use a Svelte 5 snippet.

background-color: #ffffff;
color: #1f2937;
padding: 0 2px;
border-radius: 2px;
font-weight: 600;
}
</style>
Loading