Skip to content
Merged
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
3 changes: 3 additions & 0 deletions e2e/pageobjects/canvas.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ export const canvas = {

noResults: () => $(testid('canvas-no-results')),

/** What the search field says instead of its shortcut hint while filtering. */
matchedCount: () => $(testid('search-matched')).getText(),

async openTagManager(): Promise<void> {
await $(testid('tag-manage')).click();
await $(testid('tag-manager-close')).waitForExist({ timeout: 10_000 });
Expand Down
69 changes: 68 additions & 1 deletion e2e/specs/06-search-and-filters.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect } from '@wdio/globals';
import { $, expect } from '@wdio/globals';

import { canvas } from '../pageobjects/canvas.page.js';
import { cursorOf, reloadCanvas, testid } from '../support/app.js';
Expand Down Expand Up @@ -82,6 +82,73 @@ describe('Search, filters and facets', () => {
expect(await canvas.noResults().isExisting()).toBe(true);
});

/**
* The count was already computed in Rust and thrown away on arrival; the excerpt is
* new. Both answer the same question — how big is this result, and why is that card
* in it — which a canvas full of first-three-lines previews could not.
*/
describe('what a search says about itself', () => {
/**
* ⚠️ Mocha runs a suite's own tests **before** its nested suites, so this block is
* the last thing in the file whatever its position in it — and one process serves
* the whole run, so a search left in the field is a search `07-checklists` inherits.
* It did, and every one of its scenarios failed on a canvas holding one card.
*/
after(async () => {
await canvas.clearSearch();
});

it('counts the results, and says zero rather than going quiet', async () => {
await canvas.search('Docker');
// Against what is on screen rather than a number written down here: the corpus is
// shared with every spec file that ran before this one.
expect(await canvas.matchedCount()).toContain(String((await canvas.titles()).length));

await canvas.search('nothing matches this');
expect(await canvas.matchedCount()).toContain('0');
});

it('hides the count again once nothing is being filtered', async () => {
await canvas.clearSearch();
expect(await $(testid('search-matched')).isExisting()).toBe(false);
});

it('quotes the line that matched rather than the head of the body', async () => {
const spaceId = await homeSpaceId();
await bridge.createNote(
draft({
spaceId,
title: 'Deployment runbook',
// The needle is on the last line: a preview of the first three explains nothing.
content: ['# preamble', 'nothing to see', 'still nothing', ' helm upgrade gateway'].join(
String.fromCharCode(10),
),
language: 'sh',
}),
);
await reloadCanvas();

await canvas.search('helm');
const card = await canvas.cardWithTitle('Deployment runbook');
// Trimmed of its indentation: a card shows one line and it starts with the code.
expect(await card.$('.card-snippet').getText()).toBe('helm upgrade gateway');
});

it('quotes the tag when that is what matched, since no preview ever showed it', async () => {
await canvas.search('db');
const card = await canvas.cardWithTitle('Étape de migration');
expect(await card.$(testid('note-card-hit')).getText()).toContain('db');
});

it('quotes nothing when the title is what matched, the card showing it already', async () => {
await canvas.search('Docker');
const card = await canvas.cardWithTitle('Docker compose');
expect(await card.$(testid('note-card-hit')).isExisting()).toBe(false);
// Back to the head of the body, which is what the card shows outside a search.
expect(await card.$('.card-snippet').getText()).toContain('docker compose up -d');
});
});

it('goes back to the chronological sections when the search is cleared', async () => {
await canvas.clearSearch();
const keys = await canvas.sectionKeys();
Expand Down
8 changes: 8 additions & 0 deletions src-tauri/src/notes/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ use specta::Type;
use super::checklist::{self, ChecklistItem, NoteKind};
use super::language::{self, Language};
use super::placeholder::{self, Placeholder};
// The search vocabulary lives with the matching, in `view`. The two modules name each
// other, which inside one feature is a reference rather than a dependency.
use super::view::SearchHit;

#[derive(Debug, Clone, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase")]
Expand Down Expand Up @@ -204,6 +207,10 @@ pub struct DisplayNote {
/// list travels as the Markdown of its items, a snippet as `None`. Decided here
/// so `checklist::to_markdown` stays the only place the `- [x] ` syntax exists.
pub copy_text: Option<String>,
/// Why this note is in the results, when the card is not already showing it.
/// Filled in afterwards by `view::build`, like the attachment count above it —
/// `None` outside a search, and for a note found by its own title.
pub search_hit: Option<SearchHit>,
}

impl std::ops::Deref for DisplayNote {
Expand All @@ -220,6 +227,7 @@ pub fn decorate(note: Note, now: DateTime<Utc>) -> DisplayNote {
expiring_soon: expires_soon(&note, now),
placeholders: placeholder::parse(&note.content, &note.placeholder_values),
attachment_count: 0,
search_hit: None,
copy_text: match note.kind {
NoteKind::Checklist => Some(checklist::to_markdown(&note.items)),
NoteKind::Snippet => None,
Expand Down
Loading