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
68 changes: 68 additions & 0 deletions src/formatters/displayFormatters-1579-field-placeholder.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { describe, expect, it } from "vitest";
import type { App } from "obsidian";
import { FileNameDisplayFormatter } from "./fileNameDisplayFormatter";
import { FormatDisplayFormatter } from "./formatDisplayFormatter";
import type QuickAdd from "../main";

/**
* Issue #1579. Both preview formatters built the `{{FIELD:...}}` placeholder out
* of the token's WHOLE inner text, filters included, so the more precisely you
* filtered the less the preview looked like a value:
* `{{FIELD:status|folder:Work}}` previewed `status|folder:Work_field_value`.
* The field is `status`.
*/
const mockApp = {
workspace: { getActiveFile: () => null },
vault: { getMarkdownFiles: () => [], getAbstractFileByPath: () => null },
metadataCache: { getFileCache: () => null, getAllPropertyInfos: () => ({}) },
} as unknown as App;

const plugin = {
settings: { globalVariables: {}, choices: [] },
getTemplateFiles: () => [],
} as unknown as QuickAdd;

const formatters = [
["file name", () => new FileNameDisplayFormatter(mockApp, plugin)],
["format", () => new FormatDisplayFormatter(mockApp, plugin)],
] as const;

describe.each(formatters)("the %s preview names the FIELD", (_label, make) => {
it.each([
["no filters", "{{FIELD:status}}", "status_field_value"],
["one filter", "{{FIELD:status|folder:Work}}", "status_field_value"],
[
"several filters",
"{{FIELD:status|folder:Work|exclude-tag:archive|multi}}",
"status_field_value",
],
// Not a case: `{{FIELD:status,Work}}` previews `status,Work_field_value`,
// and that is faithful - FieldSuggestionParser splits on `|` only, so the
// run looks up a property literally named "status,Work" too.
])("%s", async (_case, input, expected) => {
expect(await make().format(input)).toBe(expected);
});

it("says something neutral when the field name is missing", async () => {
// Reachable on every keystroke of `{{FIELD:|folder:x}}`. Echoing the raw
// specifier back here would reprint the filters, which is the bug.
expect(await make().format("{{FIELD:|folder:Work}}")).toBe("field_value");
});
});

describe("#1579 the variable KEY still carries the whole specifier", () => {
it("keeps two differently filtered {{FIELD:status}} tokens apart", async () => {
// They are different prompts at run time, so they must not collapse onto
// one variable - even though they now PREVIEW identically.
const formatter = new FileNameDisplayFormatter(mockApp, plugin);
await formatter.format("{{FIELD:status|folder:Work}} {{FIELD:status}}");

const keys = [
...(formatter as unknown as { variables: Map<string, unknown> }).variables.keys(),
];
expect(keys).toEqual([
"FIELD:status|folder:Work",
"FIELD:status",
]);
});
});
28 changes: 26 additions & 2 deletions src/formatters/fileNameDisplayFormatter-1563-normalize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ describe("the file-name preview mirrors the run's name normalizer", () => {
expect(problems).toEqual([
{
severity: "error",
kind: "path",
message: 'File path cannot contain "." or ".." path segments.',
},
]);
Expand All @@ -70,6 +71,7 @@ describe("the file-name preview mirrors the run's name normalizer", () => {
expect(problems).toEqual([
{
severity: "error",
kind: "path",
message: "File path contains an empty path segment after formatting.",
},
]);
Expand All @@ -87,9 +89,31 @@ describe("the file-name preview mirrors the run's name normalizer", () => {
"tag:#inbox",
"Daily/2026-07-27.md",
]) {
const { out, problems } = await preview(target);
const { out } = await preview(target);
expect(out).toBe(target);
expect(problems).toEqual([]);
}
});

it("does report the colon in a picker target, which the builder hides", async () => {
// This formatter previews FILE NAMES. `property:x=y` and `tag:#inbox` are
// capture-target syntax, not paths, and a colon in an actual name really
// is fatal - so the rule is right and the SURFACE is what knows the
// difference: CaptureTargetSetting.svelte renders no preview row at all
// while the field holds recognised picker syntax
// (`{#if !usesPickerTargetSyntax}`), and that gate is pinned by
// CaptureTargetSetting-1578-picker-preview.test.ts.
//
// Teaching the formatter capture semantics would be the wrong layer: the
// same class previews a Template choice's file name, where a literal
// `property:x=y` IS a path and the colon IS the problem.
const { problems } = await preview("property:status=done");
expect(problems).toEqual([
{
severity: "error",
kind: "path",
message:
'A file or folder name cannot contain ":", so this choice would fail at run time. Check your own text and tokens like {{TIME}}, which is HH:mm.',
},
]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,14 @@ describe("#1563 the file-name preview resolves {{TEMPLATE:}}", () => {
message:
'Template "Body.md" is 5 lines; a file name is one line, so they are joined with spaces.',
},
{
// The frontmatter's "title: x" is now IN the name, so the name has a
// colon in it and Obsidian would refuse it (#1578).
severity: "error",
kind: "path",
message:
'A file or folder name cannot contain ":", so this choice would fail at run time. Check your own text and tokens like {{TIME}}, which is HH:mm.',
},
]);
});

Expand Down
Loading