The repeat rules are pinned by the shared fixture
daily/weekly/monthly/yearly was written out in the server (REMINDER_RECURRENCES), the core (recur::RECURRENCES), the web editor's <option>s and Android's picker, with nothing holding them together. grammar.json now has a recurrences list; the server, core and web suites each check theirs against it, and the web's options come from notes/recurrence.ts rather than the template. Android's picker pins the list by hand with its localised labels, as it does the tint: its JVM tests do not read the fixture. DRY pass #2, batch 3 (#5372). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -74,6 +74,19 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use chrono::TimeZone;
|
use chrono::TimeZone;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_rules_are_the_shared_fixtures() {
|
||||||
|
let fixture: serde_json::Value =
|
||||||
|
serde_json::from_str(include_str!("../../testdata/grammar.json")).expect("parses");
|
||||||
|
let want: Vec<&str> = fixture["recurrences"]
|
||||||
|
.as_array()
|
||||||
|
.expect("a list")
|
||||||
|
.iter()
|
||||||
|
.map(|r| r.as_str().expect("a string"))
|
||||||
|
.collect();
|
||||||
|
assert_eq!(RECURRENCES.to_vec(), want);
|
||||||
|
}
|
||||||
|
|
||||||
fn utc(y: i32, m: u32, d: u32, h: u32, min: u32) -> DateTime<Utc> {
|
fn utc(y: i32, m: u32, d: u32, h: u32, min: u32) -> DateTime<Utc> {
|
||||||
Utc.with_ymd_and_hms(y, m, d, h, min, 0).unwrap()
|
Utc.with_ymd_and_hms(y, m, d, h, min, 0).unwrap()
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+3
@@ -5,6 +5,7 @@
|
|||||||
"A difference between any two implementations is a note that changes shape when it syncs. Add a case here, not to one suite.",
|
"A difference between any two implementations is a note that changes shape when it syncs. Add a case here, not to one suite.",
|
||||||
"Lives under core/ because core is the definition, and because core/ is in the desktop, Android and server file sets, so changing a case reruns every suite that reads it.",
|
"Lives under core/ because core is the definition, and because core/ is in the desktop, Android and server file sets, so changing a case reruns every suite that reads it.",
|
||||||
"display_titles: a note's name is its first line that says anything once a task marker is stripped, cut to 200 characters (not bytes). Lines split on \\n alone, as the grammar does everywhere.",
|
"display_titles: a note's name is its first line that says anything once a task marker is stripped, cut to 200 characters (not bytes). Lines split on \\n alone, as the grammar does everywhere.",
|
||||||
|
"recurrences: the repeat rules a reminder can carry, as stored. The server, the core and the web's editor each hold the list; Android's picker pins it by hand, as it does the tint.",
|
||||||
"Tag cases are plain text on purpose. The web's renderer lets `code` and **bold** claim a `#` first, which the derivers do not; that is a rendering choice, not grammar, and is tested where it lives."
|
"Tag cases are plain text on purpose. The web's renderer lets `code` and **bold** claim a `#` first, which the derivers do not; that is a rendering choice, not grammar, and is tested where it lives."
|
||||||
],
|
],
|
||||||
|
|
||||||
@@ -81,6 +82,8 @@
|
|||||||
{ "body": "ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "title": "éééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé" }
|
{ "body": "ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", "title": "éééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé" }
|
||||||
],
|
],
|
||||||
|
|
||||||
|
"recurrences": ["daily", "weekly", "monthly", "yearly"],
|
||||||
|
|
||||||
"tint": {
|
"tint": {
|
||||||
"keys": ["red", "orange", "yellow", "green", "teal", "blue", "purple", "pink", "gray"],
|
"keys": ["red", "orange", "yellow", "green", "teal", "blue", "purple", "pink", "gray"],
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import ReminderActions from "./ReminderActions.vue";
|
|||||||
import LabelPicker from "./LabelPicker.vue";
|
import LabelPicker from "./LabelPicker.vue";
|
||||||
import LinkPreview from "./LinkPreview.vue";
|
import LinkPreview from "./LinkPreview.vue";
|
||||||
import { formatShortDateTime, fromLocalInput, toLocalInput } from "../notes/datetime";
|
import { formatShortDateTime, fromLocalInput, toLocalInput } from "../notes/datetime";
|
||||||
|
import { RECURRENCES } from "../notes/recurrence";
|
||||||
import { takeMorphOrigin } from "../composables/useEditorMorph";
|
import { takeMorphOrigin } from "../composables/useEditorMorph";
|
||||||
import { prefersReducedMotion } from "../composables/useReducedMotion";
|
import { prefersReducedMotion } from "../composables/useReducedMotion";
|
||||||
import type { Note, NoteLabel, NoteRevision } from "../stores/notes";
|
import type { Note, NoteLabel, NoteRevision } from "../stores/notes";
|
||||||
@@ -749,10 +750,7 @@ function revPreview(rev: NoteRevision): string {
|
|||||||
@change="onRecurrenceChange"
|
@change="onRecurrenceChange"
|
||||||
>
|
>
|
||||||
<option value="">Does not repeat</option>
|
<option value="">Does not repeat</option>
|
||||||
<option value="daily">Daily</option>
|
<option v-for="r in RECURRENCES" :key="r.value" :value="r.value">{{ r.label }}</option>
|
||||||
<option value="weekly">Weekly</option>
|
|
||||||
<option value="monthly">Monthly</option>
|
|
||||||
<option value="yearly">Yearly</option>
|
|
||||||
</select>
|
</select>
|
||||||
<ReminderActions :note-id="liveNote.id" />
|
<ReminderActions :note-id="liveNote.id" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { describe, expect, it } from "vitest";
|
|||||||
import fixture from "../../../core/testdata/grammar.json";
|
import fixture from "../../../core/testdata/grammar.json";
|
||||||
import { DERIVED_TINT_KEYS, derivedTint, resolveLabelColor, tintHash } from "./colors";
|
import { DERIVED_TINT_KEYS, derivedTint, resolveLabelColor, tintHash } from "./colors";
|
||||||
import { parseInline, parseTaskLine, renderTaskLine } from "./markdown";
|
import { parseInline, parseTaskLine, renderTaskLine } from "./markdown";
|
||||||
|
import { RECURRENCES } from "./recurrence";
|
||||||
|
|
||||||
describe("checklist lines", () => {
|
describe("checklist lines", () => {
|
||||||
it.each(fixture.task_lines)("$line", ({ line, item }) => {
|
it.each(fixture.task_lines)("$line", ({ line, item }) => {
|
||||||
@@ -52,3 +53,9 @@ describe("derived tag colour", () => {
|
|||||||
expect(resolveLabelColor({ name })).toBe(color);
|
expect(resolveLabelColor({ name })).toBe(color);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("recurrences", () => {
|
||||||
|
it("offers the stored rules every surface holds", () => {
|
||||||
|
expect(RECURRENCES.map((r) => r.value)).toEqual(fixture.recurrences);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// The repeat rules a reminder can carry, as stored, and what the editor calls each.
|
||||||
|
// The server (REMINDER_RECURRENCES) and the core (recur::RECURRENCES) hold the same
|
||||||
|
// list, and all three run the `recurrences` case in core/testdata/grammar.json.
|
||||||
|
export const RECURRENCES: ReadonlyArray<{ value: string; label: string }> = [
|
||||||
|
{ value: "daily", label: "Daily" },
|
||||||
|
{ value: "weekly", label: "Weekly" },
|
||||||
|
{ value: "monthly", label: "Monthly" },
|
||||||
|
{ value: "yearly", label: "Yearly" },
|
||||||
|
];
|
||||||
@@ -14,6 +14,7 @@ import pytest
|
|||||||
|
|
||||||
from inkwell.notes.checklist import parse_items, render_item
|
from inkwell.notes.checklist import parse_items, render_item
|
||||||
from inkwell.notes.helpers import derive_display_title
|
from inkwell.notes.helpers import derive_display_title
|
||||||
|
from inkwell.notes.recurrence import REMINDER_RECURRENCES
|
||||||
from inkwell.notes.tags import parse_tags, split_body_tags
|
from inkwell.notes.tags import parse_tags, split_body_tags
|
||||||
|
|
||||||
FIXTURE = json.loads(
|
FIXTURE = json.loads(
|
||||||
@@ -47,3 +48,7 @@ def test_lifts(case):
|
|||||||
@pytest.mark.parametrize("case", FIXTURE["display_titles"], ids=lambda c: repr(c["body"][:40]))
|
@pytest.mark.parametrize("case", FIXTURE["display_titles"], ids=lambda c: repr(c["body"][:40]))
|
||||||
def test_display_titles(case):
|
def test_display_titles(case):
|
||||||
assert derive_display_title(case["body"]) == case["title"]
|
assert derive_display_title(case["body"]) == case["title"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_recurrences():
|
||||||
|
assert REMINDER_RECURRENCES == set(FIXTURE["recurrences"])
|
||||||
|
|||||||
Reference in New Issue
Block a user