fix: note list shows incomplete relationships when opening from sidebar
Two root causes: 1. noteListHooks used a stale selection.entry to build relationship groups — now looks up the fresh entry from the entries array so relationship updates propagate immediately. 2. frontmatterToEntryPatch didn't update entry.relationships — added RelationshipPatch support so frontmatter changes also update the relationships map in state. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -486,6 +486,70 @@ References:
|
||||
);
|
||||
}
|
||||
|
||||
// --- large relationship array (regression: No Code note with 32 Notes) ---
|
||||
|
||||
#[test]
|
||||
fn test_parse_large_notes_relationship_array() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let content = r#"---
|
||||
type: Topic
|
||||
Referred by Data:
|
||||
- "[[michele-sampieri|Michele Sampieri]]"
|
||||
- "[[varun-anand|Varun Anand]]"
|
||||
Belongs to:
|
||||
- "[[engineering|Engineering]]"
|
||||
aliases:
|
||||
- No Code
|
||||
Notes:
|
||||
- "[[8020-we-help-companies-move-faster-without-code|8020 | We help companies move faster without code.]]"
|
||||
- "[[airdev-build-hub|Airdev Build Hub]]"
|
||||
- "[[airdev-leader-in-bubble-and-no-code-development|AirDev | Leader in Bubble and No-Code Development]]"
|
||||
- "[[budibase-internal-tools-made-easy|Budibase - Internal tools made easy]]"
|
||||
- "[[bullet-launch-bubble-boilerplate|Bullet Launch Bubble Boilerplate]]"
|
||||
- "[[canvas-base-template-for-bubble|Canvas • Base Template for Bubble]]"
|
||||
- "[[chameleon-microsurveys|Chameleon | Microsurveys]]"
|
||||
- "[[felt-the-best-way-to-make-maps-on-the-internet|Felt – The best way to make maps on the internet]]"
|
||||
- "[[flutterflow-build-native-apps-visually|FlutterFlow | Build Native Apps Visually]]"
|
||||
- "[[framer-ai-generate-and-publish-your-site-with-ai-in-seconds|Framer AI — Generate and publish your site with AI in seconds.]]"
|
||||
- "[[jumpstart-pro-the-best-ruby-on-rails-saas-template|Jumpstart Pro | The best Ruby on Rails SaaS Template]]"
|
||||
- "[[mailparser-email-parser-software-workflow-automation|MailParser • Email Parser Software & Workflow Automation]]"
|
||||
- "[[make-work-the-way-you-imagine|Make | Work the way you imagine]]"
|
||||
- "[[michele-sampieri|Michele Sampieri]]"
|
||||
- "[[n8nio-a-powerful-workflow-automation-tool|n8n.io - a powerful workflow automation tool]]"
|
||||
- "[[n8nio-ai-workflow-automation-tool|n8n.io - AI workflow automation tool]]"
|
||||
- "[[nocodey-find-best-nocoder|Nocodey • Find Best Nocoder]]"
|
||||
- "[[outseta-software-for-subscription-start-ups|Outseta | Software for subscription start-ups]]"
|
||||
- "[[payments-tax-subscriptions-for-software-companies-lemon-squeezy|Payments, tax & subscriptions for software companies • Lemon Squeezy]]"
|
||||
- "[[retool-portals-custom-client-portal-software|Retool Portals • Custom Client Portal Software]]"
|
||||
- "[[rise-of-the-no-code-economy-report-formstack|Rise of the No-Code Economy Report | Formstack]]"
|
||||
- "[[scene-the-smart-way-to-build-websites|Scene • The smart way to build websites]]"
|
||||
- "[[scrapingbee-the-best-web-scraping-api|ScrapingBee • the best web scraping API]]"
|
||||
- "[[softr-build-a-website-web-app-or-portal-on-airtable-without-code|Softr | Build a website, web app or portal on Airtable without code]]"
|
||||
- "[[superblocks-build-modern-internal-apps-in-days-not-months|Superblocks • Build modern internal apps in days, not months]]"
|
||||
- "[[superwall-quickly-deploy-paywalls|Superwall • Quickly deploy paywalls]]"
|
||||
- "[[tails-tailwind-css-page-creator|Tails | Tailwind CSS Page Creator]]"
|
||||
- "[[the-open-source-firebase-alternative-supabase|The Open Source Firebase Alternative | Supabase]]"
|
||||
- "[[varun-anand|Varun Anand]]"
|
||||
- "[[xano-the-fastest-no-code-backend-development-platform|Xano - The Fastest No Code Backend Development Platform]]"
|
||||
- "[[directus-open-data-platform-for-headless-content-management|{'Directus': 'Open Data Platform for Headless Content Management'}]]"
|
||||
- "[[framer-design-beautiful-websites-in-minutes|{'Framer': 'Design beautiful websites in minutes'}]]"
|
||||
title: No Code
|
||||
---
|
||||
# No Code
|
||||
"#;
|
||||
create_test_file(dir.path(), "no-code.md", content);
|
||||
let entry = parse_md_file(&dir.path().join("no-code.md")).unwrap();
|
||||
|
||||
let notes = entry.relationships.get("Notes").expect("Notes relationship should exist");
|
||||
assert_eq!(notes.len(), 32, "All 32 Notes entries should be parsed");
|
||||
|
||||
let referred = entry.relationships.get("Referred by Data").expect("Referred by Data should exist");
|
||||
assert_eq!(referred.len(), 2);
|
||||
|
||||
let belongs = entry.relationships.get("Belongs to").expect("Belongs to should exist");
|
||||
assert_eq!(belongs.len(), 1);
|
||||
}
|
||||
|
||||
// --- type from frontmatter only (no folder inference) ---
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -56,7 +56,10 @@ export function useNoteListData({ entries, selection, query, listSort, listDirec
|
||||
|
||||
const searchedGroups = useMemo(() => {
|
||||
if (!isEntityView) return []
|
||||
const groups = buildRelationshipGroups(selection.entry, entries)
|
||||
// Look up the fresh entry from the entries array to pick up relationship
|
||||
// updates that happened after the selection was captured.
|
||||
const freshEntry = entries.find((e) => e.path === selection.entry.path) ?? selection.entry
|
||||
const groups = buildRelationshipGroups(freshEntry, entries)
|
||||
return filterGroupsByQuery(groups, query)
|
||||
}, [isEntityView, selection, entries, query])
|
||||
|
||||
|
||||
@@ -13,12 +13,38 @@ const ENTRY_DELETE_MAP: Record<string, Partial<VaultEntry>> = {
|
||||
template: { template: null }, sort: { sort: null }, visible: { visible: null },
|
||||
}
|
||||
|
||||
/** Check if a string contains a wikilink pattern `[[...]]`. */
|
||||
function isWikilink(s: string): boolean {
|
||||
return s.startsWith('[[') && s.includes(']]')
|
||||
}
|
||||
|
||||
/** Extract wikilink strings from a FrontmatterValue. Returns empty array if none. */
|
||||
function extractWikilinks(value: FrontmatterValue): string[] {
|
||||
if (typeof value === 'string') return isWikilink(value) ? [value] : []
|
||||
if (Array.isArray(value)) return value.filter((v): v is string => typeof v === 'string' && isWikilink(v))
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship patch: a partial update to merge into `entry.relationships`.
|
||||
* Keys map to their new ref arrays. A `null` value means "remove this key".
|
||||
*/
|
||||
export type RelationshipPatch = Record<string, string[] | null>
|
||||
|
||||
export interface EntryPatchResult {
|
||||
patch: Partial<VaultEntry>
|
||||
relationshipPatch: RelationshipPatch | null
|
||||
}
|
||||
|
||||
/** Map a frontmatter key+value to the corresponding VaultEntry field(s). */
|
||||
export function frontmatterToEntryPatch(
|
||||
op: 'update' | 'delete', key: string, value?: FrontmatterValue,
|
||||
): Partial<VaultEntry> {
|
||||
): EntryPatchResult {
|
||||
const k = key.toLowerCase().replace(/\s+/g, '_')
|
||||
if (op === 'delete') return ENTRY_DELETE_MAP[k] ?? {}
|
||||
if (op === 'delete') {
|
||||
const relPatch: RelationshipPatch = { [key]: null }
|
||||
return { patch: ENTRY_DELETE_MAP[k] ?? {}, relationshipPatch: relPatch }
|
||||
}
|
||||
const str = value != null ? String(value) : null
|
||||
const arr = Array.isArray(value) ? value.map(String) : []
|
||||
const updates: Record<string, Partial<VaultEntry>> = {
|
||||
@@ -32,7 +58,11 @@ export function frontmatterToEntryPatch(
|
||||
view: { view: str },
|
||||
visible: { visible: value === false ? false : null },
|
||||
}
|
||||
return updates[k] ?? {}
|
||||
// Also update the relationships map for wikilink-containing values
|
||||
const wikilinks = value != null ? extractWikilinks(value) : []
|
||||
const relationshipPatch: RelationshipPatch | null =
|
||||
wikilinks.length > 0 ? { [key]: wikilinks } : null
|
||||
return { patch: updates[k] ?? {}, relationshipPatch }
|
||||
}
|
||||
|
||||
async function invokeFrontmatter(command: string, args: Record<string, unknown>): Promise<string> {
|
||||
@@ -65,18 +95,42 @@ export interface FrontmatterOpOptions {
|
||||
silent?: boolean
|
||||
}
|
||||
|
||||
/** Apply a relationship patch by merging into the existing relationships map. */
|
||||
export function applyRelationshipPatch(
|
||||
existing: Record<string, string[]>, relPatch: RelationshipPatch,
|
||||
): Record<string, string[]> {
|
||||
const merged = { ...existing }
|
||||
for (const [k, v] of Object.entries(relPatch)) {
|
||||
if (v === null) delete merged[k]
|
||||
else merged[k] = v
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
/** Run a frontmatter update/delete and apply the result to state.
|
||||
* Returns the new file content on success, or undefined on failure. */
|
||||
export async function runFrontmatterAndApply(
|
||||
op: 'update' | 'delete', path: string, key: string, value: FrontmatterValue | undefined,
|
||||
callbacks: { updateTab: (p: string, c: string) => void; updateEntry: (p: string, patch: Partial<VaultEntry>) => void; toast: (m: string | null) => void },
|
||||
callbacks: {
|
||||
updateTab: (p: string, c: string) => void
|
||||
updateEntry: (p: string, patch: Partial<VaultEntry>) => void
|
||||
toast: (m: string | null) => void
|
||||
getEntry?: (p: string) => VaultEntry | undefined
|
||||
},
|
||||
options?: FrontmatterOpOptions,
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
const newContent = await executeFrontmatterOp(op, path, key, value)
|
||||
callbacks.updateTab(path, newContent)
|
||||
const patch = frontmatterToEntryPatch(op, key, value)
|
||||
if (Object.keys(patch).length > 0) callbacks.updateEntry(path, patch)
|
||||
const { patch, relationshipPatch } = frontmatterToEntryPatch(op, key, value)
|
||||
const fullPatch = { ...patch }
|
||||
if (relationshipPatch && callbacks.getEntry) {
|
||||
const current = callbacks.getEntry(path)
|
||||
if (current) {
|
||||
fullPatch.relationships = applyRelationshipPatch(current.relationships, relationshipPatch)
|
||||
}
|
||||
}
|
||||
if (Object.keys(fullPatch).length > 0) callbacks.updateEntry(path, fullPatch)
|
||||
if (!options?.silent) callbacks.toast(op === 'update' ? 'Property updated' : 'Property deleted')
|
||||
return newContent
|
||||
} catch (err) {
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
resolveTemplate,
|
||||
} from './useNoteCreation'
|
||||
import { needsRenameOnSave } from './useNoteRename'
|
||||
import { frontmatterToEntryPatch } from './frontmatterOps'
|
||||
import { frontmatterToEntryPatch, applyRelationshipPatch } from './frontmatterOps'
|
||||
import { useNoteActions } from './useNoteActions'
|
||||
import type { NoteActionsConfig } from './useNoteActions'
|
||||
|
||||
@@ -345,25 +345,45 @@ describe('frontmatterToEntryPatch', () => {
|
||||
] as [string, unknown, Partial<VaultEntry>][])(
|
||||
'maps %s update to correct entry field',
|
||||
(key, value, expected) => {
|
||||
expect(frontmatterToEntryPatch('update', key, value as never)).toEqual(expected)
|
||||
expect(frontmatterToEntryPatch('update', key, value as never).patch).toEqual(expected)
|
||||
},
|
||||
)
|
||||
|
||||
it('maps aliases update with array value', () => {
|
||||
expect(frontmatterToEntryPatch('update', 'aliases', ['A', 'B'])).toEqual({ aliases: ['A', 'B'] })
|
||||
expect(frontmatterToEntryPatch('update', 'aliases', ['A', 'B']).patch).toEqual({ aliases: ['A', 'B'] })
|
||||
})
|
||||
|
||||
it('maps belongs_to update with array value', () => {
|
||||
expect(frontmatterToEntryPatch('update', 'belongs_to', ['[[parent]]'])).toEqual({ belongsTo: ['[[parent]]'] })
|
||||
const result = frontmatterToEntryPatch('update', 'belongs_to', ['[[parent]]'])
|
||||
expect(result.patch).toEqual({ belongsTo: ['[[parent]]'] })
|
||||
// Also produces a relationship patch for the wikilink
|
||||
expect(result.relationshipPatch).toEqual({ 'belongs_to': ['[[parent]]'] })
|
||||
})
|
||||
|
||||
it('handles case-insensitive keys with spaces (e.g. "Is A", "Belongs to")', () => {
|
||||
expect(frontmatterToEntryPatch('update', 'Is A', 'Experiment')).toEqual({ isA: 'Experiment' })
|
||||
expect(frontmatterToEntryPatch('update', 'Belongs to', ['[[x]]'])).toEqual({ belongsTo: ['[[x]]'] })
|
||||
expect(frontmatterToEntryPatch('update', 'Is A', 'Experiment').patch).toEqual({ isA: 'Experiment' })
|
||||
const result = frontmatterToEntryPatch('update', 'Belongs to', ['[[x]]'])
|
||||
expect(result.patch).toEqual({ belongsTo: ['[[x]]'] })
|
||||
expect(result.relationshipPatch).toEqual({ 'Belongs to': ['[[x]]'] })
|
||||
})
|
||||
|
||||
it('returns empty object for unknown keys', () => {
|
||||
expect(frontmatterToEntryPatch('update', 'custom_field', 'value')).toEqual({})
|
||||
it('returns empty patch for unknown non-wikilink keys', () => {
|
||||
const result = frontmatterToEntryPatch('update', 'custom_field', 'value')
|
||||
expect(result.patch).toEqual({})
|
||||
// Non-wikilink value → no relationship change
|
||||
expect(result.relationshipPatch).toBeNull()
|
||||
})
|
||||
|
||||
it('produces relationship patch for wikilink values on unknown keys', () => {
|
||||
const result = frontmatterToEntryPatch('update', 'Notes', ['[[note-a]]', '[[note-b|Note B]]'])
|
||||
expect(result.patch).toEqual({})
|
||||
expect(result.relationshipPatch).toEqual({ Notes: ['[[note-a]]', '[[note-b|Note B]]'] })
|
||||
})
|
||||
|
||||
it('produces relationship patch for single wikilink string', () => {
|
||||
const result = frontmatterToEntryPatch('update', 'Owner', '[[person/alice]]')
|
||||
expect(result.patch).toEqual({})
|
||||
expect(result.relationshipPatch).toEqual({ Owner: ['[[person/alice]]'] })
|
||||
})
|
||||
|
||||
it.each([
|
||||
@@ -377,12 +397,46 @@ describe('frontmatterToEntryPatch', () => {
|
||||
] as [string, Partial<VaultEntry>][])(
|
||||
'maps delete of %s to null/default',
|
||||
(key, expected) => {
|
||||
expect(frontmatterToEntryPatch('delete', key)).toEqual(expected)
|
||||
expect(frontmatterToEntryPatch('delete', key).patch).toEqual(expected)
|
||||
},
|
||||
)
|
||||
|
||||
it('returns empty object for unknown key on delete', () => {
|
||||
expect(frontmatterToEntryPatch('delete', 'unknown_key')).toEqual({})
|
||||
it('returns empty patch for unknown key on delete, with relationship removal', () => {
|
||||
const result = frontmatterToEntryPatch('delete', 'unknown_key')
|
||||
expect(result.patch).toEqual({})
|
||||
expect(result.relationshipPatch).toEqual({ unknown_key: null })
|
||||
})
|
||||
|
||||
it('delete of known key also produces relationship removal', () => {
|
||||
const result = frontmatterToEntryPatch('delete', 'status')
|
||||
expect(result.patch).toEqual({ status: null })
|
||||
expect(result.relationshipPatch).toEqual({ status: null })
|
||||
})
|
||||
})
|
||||
|
||||
describe('applyRelationshipPatch', () => {
|
||||
it('adds new relationship key', () => {
|
||||
const existing = { 'Belongs to': ['[[eng]]'] }
|
||||
const result = applyRelationshipPatch(existing, { Notes: ['[[note-a]]', '[[note-b]]'] })
|
||||
expect(result).toEqual({ 'Belongs to': ['[[eng]]'], Notes: ['[[note-a]]', '[[note-b]]'] })
|
||||
})
|
||||
|
||||
it('overwrites existing relationship key', () => {
|
||||
const existing = { Notes: ['[[old]]'] }
|
||||
const result = applyRelationshipPatch(existing, { Notes: ['[[new-a]]', '[[new-b]]'] })
|
||||
expect(result).toEqual({ Notes: ['[[new-a]]', '[[new-b]]'] })
|
||||
})
|
||||
|
||||
it('removes relationship key when value is null', () => {
|
||||
const existing = { Notes: ['[[a]]'], Owner: ['[[alice]]'] }
|
||||
const result = applyRelationshipPatch(existing, { Notes: null })
|
||||
expect(result).toEqual({ Owner: ['[[alice]]'] })
|
||||
})
|
||||
|
||||
it('does not mutate the original map', () => {
|
||||
const existing = { Notes: ['[[a]]'] }
|
||||
applyRelationshipPatch(existing, { Notes: ['[[b]]'] })
|
||||
expect(existing).toEqual({ Notes: ['[[a]]'] })
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -117,8 +117,8 @@ export function useNoteActions(config: NoteActionsConfig) {
|
||||
|
||||
const runFrontmatterOp = useCallback(
|
||||
(op: 'update' | 'delete', path: string, key: string, value?: FrontmatterValue, options?: FrontmatterOpOptions) =>
|
||||
runFrontmatterAndApply(op, path, key, value, { updateTab: updateTabContent, updateEntry, toast: setToastMessage }, options),
|
||||
[updateTabContent, updateEntry, setToastMessage],
|
||||
runFrontmatterAndApply(op, path, key, value, { updateTab: updateTabContent, updateEntry, toast: setToastMessage, getEntry: (p) => entries.find((e) => e.path === p) }, options),
|
||||
[updateTabContent, updateEntry, setToastMessage, entries],
|
||||
)
|
||||
|
||||
return {
|
||||
|
||||
@@ -324,6 +324,50 @@ describe('buildRelationshipGroups', () => {
|
||||
const groups = buildRelationshipGroups(entity, [entity, linker])
|
||||
expect(groups.find((g) => g.label === 'Backlinks')!.entries[0].title).toBe('Linker')
|
||||
})
|
||||
|
||||
it('resolves all entries in a large Notes relationship (regression: No Code)', () => {
|
||||
// Simulates the No Code topic note with 32 Notes, 2 Referred by Data, 1 Belongs to
|
||||
const noteRefs = [
|
||||
'8020', 'airdev-build-hub', 'airdev-leader', 'budibase', 'bullet-launch',
|
||||
'canvas', 'chameleon', 'felt', 'flutterflow', 'framer-ai',
|
||||
'jumpstart', 'mailparser', 'make', 'michele-sampieri', 'n8n-a',
|
||||
'n8n-ai', 'nocodey', 'outseta', 'lemon-squeezy', 'retool',
|
||||
'rise-no-code', 'scene', 'scrapingbee', 'softr', 'superblocks',
|
||||
'superwall', 'tails', 'supabase', 'varun-anand', 'xano',
|
||||
'directus', 'framer-design',
|
||||
]
|
||||
const noteEntries = noteRefs.map((slug, i) => makeEntry({
|
||||
path: `/Laputa/${slug}.md`, filename: `${slug}.md`, title: `Title ${slug}`,
|
||||
modifiedAt: 1700000000 - i * 100,
|
||||
}))
|
||||
const engineering = makeEntry({
|
||||
path: '/Laputa/engineering.md', filename: 'engineering.md', title: 'Engineering',
|
||||
modifiedAt: 1700000000,
|
||||
})
|
||||
const entity = makeEntry({
|
||||
path: '/Laputa/no-code.md', filename: 'no-code.md', title: 'No Code',
|
||||
isA: 'Topic',
|
||||
relationships: {
|
||||
'Belongs to': ['[[engineering|Engineering]]'],
|
||||
Notes: noteRefs.map((slug) => `[[${slug}|Title ${slug}]]`),
|
||||
'Referred by Data': ['[[michele-sampieri|Michele Sampieri]]', '[[varun-anand|Varun Anand]]'],
|
||||
},
|
||||
})
|
||||
const allEntries = [entity, engineering, ...noteEntries]
|
||||
const groups = buildRelationshipGroups(entity, allEntries)
|
||||
|
||||
const belongsGroup = groups.find((g) => g.label === 'Belongs to')
|
||||
expect(belongsGroup).toBeDefined()
|
||||
expect(belongsGroup!.entries).toHaveLength(1)
|
||||
|
||||
const notesGroup = groups.find((g) => g.label === 'Notes')
|
||||
expect(notesGroup).toBeDefined()
|
||||
expect(notesGroup!.entries).toHaveLength(32)
|
||||
|
||||
// michele-sampieri and varun-anand already consumed by Notes → Referred by Data has 0 new
|
||||
const referredGroup = groups.find((g) => g.label === 'Referred by Data')
|
||||
expect(referredGroup).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getSortComparator — custom properties', () => {
|
||||
|
||||
103
tests/smoke/note-list-incomplete-relationships.spec.ts
Normal file
103
tests/smoke/note-list-incomplete-relationships.spec.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { sendShortcut } from './helpers'
|
||||
|
||||
test.describe('Note list shows complete relationships when opening from sidebar', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1600, height: 900 })
|
||||
await page.goto('/')
|
||||
await page.waitForLoadState('networkidle')
|
||||
})
|
||||
|
||||
test('entity view shows relationship groups with correct counts after sidebar click', async ({ page }) => {
|
||||
// Navigate to Responsibilities type to find entity notes in the sidebar
|
||||
// First, click on a sidebar type that contains entity notes
|
||||
// We'll use the sidebar search to filter to "Sponsorships"
|
||||
const searchToggle = page.locator('[data-testid="search-toggle"]')
|
||||
if (await searchToggle.isVisible()) {
|
||||
await searchToggle.click()
|
||||
}
|
||||
|
||||
// Type in sidebar search to find Sponsorships
|
||||
const sidebarSearch = page.locator('[data-testid="note-list-search"]')
|
||||
if (await sidebarSearch.isVisible()) {
|
||||
await sidebarSearch.fill('Sponsorships')
|
||||
await page.waitForTimeout(500)
|
||||
}
|
||||
|
||||
// Click the Sponsorships note in the note list to trigger entity view
|
||||
const sponsorshipsItem = page.locator('[data-entry-path*="sponsorships"]').first()
|
||||
if (await sponsorshipsItem.isVisible({ timeout: 3000 }).catch(() => false)) {
|
||||
await sponsorshipsItem.click()
|
||||
await page.waitForTimeout(1000)
|
||||
} else {
|
||||
// Fallback: open via quick open — then click in sidebar
|
||||
await page.locator('body').click()
|
||||
await sendShortcut(page, 'p', ['Control'])
|
||||
const searchInput = page.locator('input[placeholder="Search notes..."]')
|
||||
await expect(searchInput).toBeVisible()
|
||||
await searchInput.fill('Sponsorships')
|
||||
await page.waitForTimeout(500)
|
||||
await page.keyboard.press('Enter')
|
||||
await page.waitForTimeout(1000)
|
||||
}
|
||||
|
||||
// The inspector panel (right side) should show relationship labels with font-mono-overline
|
||||
// This verifies the note loaded and relationships were parsed correctly
|
||||
const measuresLabel = page.locator('span.font-mono-overline').filter({ hasText: 'Has Measures' })
|
||||
await expect(measuresLabel).toBeVisible({ timeout: 5000 })
|
||||
|
||||
const proceduresLabel = page.locator('span.font-mono-overline').filter({ hasText: 'Has Procedures' })
|
||||
await expect(proceduresLabel).toBeVisible({ timeout: 5000 })
|
||||
|
||||
// Verify the relationships panel shows the correct number of items
|
||||
// Has Measures should have 2 entries (measure-sponsorship-mrr, measure-close-rate)
|
||||
const measuresSection = measuresLabel.locator('xpath=ancestor::div[1]')
|
||||
const measureItems = measuresSection.locator('a, button').filter({ hasText: /measure|Measure/ })
|
||||
// At least 1 resolved relationship entry
|
||||
const hasItems = await measureItems.count()
|
||||
expect(hasItems).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
|
||||
test('relationship labels persist after navigating away and back', async ({ page }) => {
|
||||
// Open Sponsorships via quick open
|
||||
await page.locator('body').click()
|
||||
await sendShortcut(page, 'p', ['Control'])
|
||||
const searchInput = page.locator('input[placeholder="Search notes..."]')
|
||||
await expect(searchInput).toBeVisible()
|
||||
await searchInput.fill('Sponsorships')
|
||||
await page.waitForTimeout(500)
|
||||
await page.keyboard.press('Enter')
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// Verify Has Measures relationship appears in inspector
|
||||
const measuresLabel = page.locator('span.font-mono-overline').filter({ hasText: 'Has Measures' })
|
||||
await expect(measuresLabel).toBeVisible({ timeout: 5000 })
|
||||
|
||||
// Navigate to different note
|
||||
await page.locator('body').click()
|
||||
await sendShortcut(page, 'p', ['Control'])
|
||||
const searchInput2 = page.locator('input[placeholder="Search notes..."]')
|
||||
await expect(searchInput2).toBeVisible()
|
||||
await searchInput2.fill('Start Laputa App')
|
||||
await page.waitForTimeout(500)
|
||||
await page.keyboard.press('Enter')
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// Navigate back to Sponsorships
|
||||
await page.locator('body').click()
|
||||
await sendShortcut(page, 'p', ['Control'])
|
||||
const searchInput3 = page.locator('input[placeholder="Search notes..."]')
|
||||
await expect(searchInput3).toBeVisible()
|
||||
await searchInput3.fill('Sponsorships')
|
||||
await page.waitForTimeout(500)
|
||||
await page.keyboard.press('Enter')
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// Relationships should still be visible — not stale or incomplete
|
||||
const measuresLabelAgain = page.locator('span.font-mono-overline').filter({ hasText: 'Has Measures' })
|
||||
await expect(measuresLabelAgain).toBeVisible({ timeout: 5000 })
|
||||
|
||||
const proceduresLabelAgain = page.locator('span.font-mono-overline').filter({ hasText: 'Has Procedures' })
|
||||
await expect(proceduresLabelAgain).toBeVisible({ timeout: 5000 })
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user