@jant/core 0.6.15 → 0.6.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/commands/import-site.js +7 -0
- package/dist/app-C75Zc8dD.js +6 -0
- package/dist/{app-C-oi_t8W.js → app-C7_4oab9.js} +480 -196
- package/dist/client/.vite/manifest.json +8 -8
- package/dist/client/_assets/chunks/{url-pLre2DM_.js → url-CU9pEbk5.js} +1 -1
- package/dist/client/_assets/{client-DxY5BFe8.js → client-Bq0vre8Y.js} +3 -3
- package/dist/client/_assets/client-D3VZyG4L.css +2 -0
- package/dist/client/_assets/{client-auth-CakPESv9.js → client-auth-F2rb9WfF.js} +1015 -977
- package/dist/{env-OHRKGcMj.js → env-BPYViDJJ.js} +1 -1
- package/dist/{export-CsFx6F_M.js → export-Dl5KCiEs.js} +93 -7
- package/dist/{github-api-BgSiE71w.js → github-api-BNF35Lkx.js} +1 -1
- package/dist/{github-app-BbklkFmU.js → github-app-DdiD-bC4.js} +1 -1
- package/dist/{github-sync-ppWXN3jb.js → github-sync-Bqg62IvV.js} +3 -3
- package/dist/{github-sync-Cq1pzzOI.js → github-sync-HdK2EgvJ.js} +3 -3
- package/dist/index.js +5 -5
- package/dist/node.js +6 -6
- package/dist/{url-BMYO-Zlt.js → url-CbLAtlZe.js} +517 -12
- package/package.json +1 -1
- package/src/__tests__/mise-config.test.ts +22 -0
- package/src/client/__tests__/compose-bridge.test.ts +252 -2
- package/src/client/__tests__/compose-shortcuts.test.ts +20 -0
- package/src/client/__tests__/toast.test.ts +65 -1
- package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
- package/src/client/components/__tests__/jant-command-palette.test.ts +51 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +2015 -318
- package/src/client/components/__tests__/jant-compose-editor.test.ts +53 -62
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +35 -89
- package/src/client/components/compose-format-convert.ts +5 -1
- package/src/client/components/compose-types.ts +47 -3
- package/src/client/components/jant-command-palette.ts +12 -6
- package/src/client/components/jant-compose-dialog.ts +1464 -680
- package/src/client/components/jant-compose-editor.ts +225 -93
- package/src/client/components/jant-compose-fullscreen.ts +3 -5
- package/src/client/components/jant-post-menu.ts +91 -21
- package/src/client/compose-bridge.ts +143 -122
- package/src/client/compose-launch.ts +13 -0
- package/src/client/compose-shortcuts.ts +5 -1
- package/src/client/post-refresh.ts +135 -0
- package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
- package/src/client/tiptap/link-input-rules.ts +4 -4
- package/src/client/toast.ts +130 -38
- package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
- package/src/db/__tests__/thread-activity.test.ts +113 -0
- package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
- package/src/db/migrations/0031_many_ego.sql +3 -0
- package/src/db/migrations/meta/0031_snapshot.json +2562 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
- package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
- package/src/db/migrations/pg/meta/_journal.json +7 -0
- package/src/db/pg/schema.ts +18 -0
- package/src/db/schema.ts +20 -0
- package/src/db/thread-activity.ts +101 -0
- package/src/i18n/locales/public/en.po +89 -21
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +88 -20
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +88 -20
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/lib/__tests__/feed.test.ts +168 -0
- package/src/lib/__tests__/markdown.test.ts +16 -0
- package/src/lib/__tests__/slug.test.ts +18 -0
- package/src/lib/__tests__/translit.test.ts +87 -0
- package/src/lib/__tests__/url.test.ts +53 -0
- package/src/lib/feed.ts +113 -11
- package/src/lib/hugo-markdown.ts +5 -0
- package/src/lib/link-preview.ts +25 -0
- package/src/lib/markdown-manager.ts +6 -1
- package/src/lib/post-display.ts +42 -9
- package/src/lib/timeline.ts +39 -9
- package/src/lib/tiptap-render.ts +4 -2
- package/src/lib/translit.ts +288 -0
- package/src/lib/url.ts +123 -12
- package/src/lib/view.ts +28 -0
- package/src/routes/api/__tests__/palette.test.ts +44 -0
- package/src/routes/api/posts.ts +13 -9
- package/src/routes/api/public/__tests__/posts.test.ts +38 -0
- package/src/routes/api/public/posts.ts +7 -0
- package/src/routes/compose.tsx +6 -2
- package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
- package/src/routes/pages/__tests__/archive-params.test.ts +292 -0
- package/src/routes/pages/__tests__/featured.test.ts +6 -2
- package/src/routes/pages/__tests__/preview.test.ts +3 -1
- package/src/routes/pages/archive.tsx +65 -17
- package/src/routes/pages/page.tsx +5 -2
- package/src/services/__tests__/collection.test.ts +32 -1
- package/src/services/__tests__/post-timeline.test.ts +142 -11
- package/src/services/__tests__/post.test.ts +277 -0
- package/src/services/collection.ts +8 -10
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export.ts +3 -5
- package/src/services/path.ts +4 -1
- package/src/services/post.ts +297 -100
- package/src/services/search.ts +9 -0
- package/src/styles/components.css +0 -14
- package/src/styles/site-media.css +4 -4
- package/src/styles/tokens.css +62 -1
- package/src/styles/ui.css +1363 -1283
- package/src/types/entities.ts +5 -0
- package/src/types/props.ts +12 -0
- package/src/types/views.ts +6 -0
- package/src/ui/compose/ComposeDialog.tsx +17 -17
- package/src/ui/feed/LinkPreview.tsx +2 -7
- package/src/ui/feed/PostStatusBadges.tsx +42 -3
- package/src/ui/pages/ArchivePage.tsx +131 -24
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +63 -0
- package/src/ui/shared/DraftPreviewBar.tsx +25 -9
- package/src/ui/shared/PostFooter.tsx +35 -12
- package/src/ui/shared/custom-icons.ts +5 -0
- package/src/ui/shared/post-article-attributes.ts +6 -0
- package/dist/app-BxCOR3Uc.js +0 -6
- package/dist/client/_assets/client-B_eGKN5p.css +0 -2
|
@@ -69,6 +69,45 @@ async function flushUpdates(el?: JantComposeDialog) {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
async function openPublishPanel(el: JantComposeDialog) {
|
|
73
|
+
requireElement(
|
|
74
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
75
|
+
"expected publish settings toggle",
|
|
76
|
+
).click();
|
|
77
|
+
await flushUpdates(el);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Options-panel rows are found by their title — the drafts entries moved off
|
|
81
|
+
* the title bar and into that panel. */
|
|
82
|
+
function sheetRow(
|
|
83
|
+
el: JantComposeDialog,
|
|
84
|
+
title: string,
|
|
85
|
+
): HTMLButtonElement | null {
|
|
86
|
+
return (
|
|
87
|
+
Array.from(
|
|
88
|
+
el.querySelectorAll<HTMLButtonElement>(
|
|
89
|
+
".compose-publish-panel .compose-sheet-row",
|
|
90
|
+
),
|
|
91
|
+
).find(
|
|
92
|
+
(row) =>
|
|
93
|
+
row.querySelector(".compose-sheet-title")?.textContent?.trim() ===
|
|
94
|
+
title,
|
|
95
|
+
) ?? null
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function sheetRowTitles(el: JantComposeDialog): (string | undefined)[] {
|
|
100
|
+
return Array.from(
|
|
101
|
+
el.querySelectorAll<HTMLElement>(
|
|
102
|
+
".compose-publish-panel .compose-sheet-title",
|
|
103
|
+
),
|
|
104
|
+
).map((n) => n.textContent?.trim());
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function draftsRow(el: JantComposeDialog): HTMLButtonElement | null {
|
|
108
|
+
return sheetRow(el, "Drafts");
|
|
109
|
+
}
|
|
110
|
+
|
|
72
111
|
function deferred<T>() {
|
|
73
112
|
let resolve!: (value: T) => void;
|
|
74
113
|
const promise = new Promise<T>((res) => {
|
|
@@ -211,13 +250,13 @@ const labels: ComposeLabels = {
|
|
|
211
250
|
publishDateInvalid: "Enter a valid date.",
|
|
212
251
|
publishDateFutureError:
|
|
213
252
|
"Choose today or an earlier date, or leave it blank to publish now.",
|
|
214
|
-
publishDateSummaryNow: "
|
|
253
|
+
publishDateSummaryNow: "Now",
|
|
215
254
|
publishDateSummaryAction: "Edit publish date",
|
|
216
255
|
publishSlugLabel: "Custom link",
|
|
217
256
|
publishSlugPlaceholder: "your-post-link",
|
|
218
257
|
publishSlugHint: "Leave blank to generate one automatically.",
|
|
219
258
|
publishSlugAuto: "Generate automatically",
|
|
220
|
-
publishSlugSummaryAuto: "Auto
|
|
259
|
+
publishSlugSummaryAuto: "Auto",
|
|
221
260
|
publishSlugSummaryAction: "Edit custom link",
|
|
222
261
|
publishSlugReset: "Reset link",
|
|
223
262
|
publishSlugSuggested: "Suggested link",
|
|
@@ -233,9 +272,9 @@ const labels: ComposeLabels = {
|
|
|
233
272
|
threadLimitReached: "Threads can include up to 20 posts.",
|
|
234
273
|
showMore: "Show more",
|
|
235
274
|
showLess: "Show less",
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
275
|
+
closeCompose: "Close compose",
|
|
276
|
+
editing: "Editing",
|
|
277
|
+
composeDialogLabel: "Compose",
|
|
239
278
|
slashHint: "Type / for commands",
|
|
240
279
|
tableControls: {
|
|
241
280
|
toolbarLabel: "Table controls",
|
|
@@ -279,6 +318,35 @@ const collections: ComposeCollection[] = [
|
|
|
279
318
|
{ id: "col-2", title: "Movies", slug: "movies" },
|
|
280
319
|
];
|
|
281
320
|
|
|
321
|
+
/**
|
|
322
|
+
* Date and permalink hang off the post itself, not the publish panel. Both
|
|
323
|
+
* fields are open as soon as the panel is, so there is nothing further to
|
|
324
|
+
* expand. Idempotent.
|
|
325
|
+
*/
|
|
326
|
+
async function openPostMeta(el: JantComposeDialog, index = 0) {
|
|
327
|
+
const pill = el.querySelectorAll<HTMLButtonElement>(
|
|
328
|
+
"[data-compose-post-meta-pill]",
|
|
329
|
+
)[index];
|
|
330
|
+
if (pill && pill.getAttribute("aria-expanded") !== "true") pill.click();
|
|
331
|
+
// The slug suggestion is fetched when the panel opens; let it settle.
|
|
332
|
+
await flushUpdates(el);
|
|
333
|
+
await flushUpdates(el);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** The date or permalink as the post's own pill states it. */
|
|
337
|
+
function pillValue(
|
|
338
|
+
el: JantComposeDialog,
|
|
339
|
+
kind: "date" | "slug",
|
|
340
|
+
index = 0,
|
|
341
|
+
): string | undefined {
|
|
342
|
+
const pill = el.querySelectorAll("[data-compose-post-meta-pill]")[index];
|
|
343
|
+
const selector =
|
|
344
|
+
kind === "date"
|
|
345
|
+
? ".compose-post-meta-value:not(.compose-post-meta-value-slug)"
|
|
346
|
+
: ".compose-post-meta-value-slug";
|
|
347
|
+
return pill?.querySelector(selector)?.textContent?.trim();
|
|
348
|
+
}
|
|
349
|
+
|
|
282
350
|
async function createElement(
|
|
283
351
|
cols: ComposeCollection[] = collections,
|
|
284
352
|
): Promise<JantComposeDialog> {
|
|
@@ -331,8 +399,11 @@ describe("JantComposeDialog", () => {
|
|
|
331
399
|
it("renders with collections and labels", async () => {
|
|
332
400
|
const el = await createElement();
|
|
333
401
|
|
|
334
|
-
//
|
|
335
|
-
|
|
402
|
+
// No title bar: the composer opens straight onto the post header row. A new
|
|
403
|
+
// post gets no marker either — an empty composer already says that much.
|
|
404
|
+
expect(el.querySelector(".compose-dialog-header")).toBeNull();
|
|
405
|
+
expect(el.querySelector(".compose-thread-post-header")).not.toBeNull();
|
|
406
|
+
expect(el.querySelector(".compose-post-badge")).toBeNull();
|
|
336
407
|
|
|
337
408
|
// Format buttons present
|
|
338
409
|
const segmentedItems = el.querySelectorAll(".compose-segmented-item");
|
|
@@ -350,7 +421,7 @@ describe("JantComposeDialog", () => {
|
|
|
350
421
|
expect(postBtn.disabled).toBe(true);
|
|
351
422
|
expect(
|
|
352
423
|
requireElement(
|
|
353
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
424
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
354
425
|
"expected publish settings toggle",
|
|
355
426
|
).disabled,
|
|
356
427
|
).toBe(false);
|
|
@@ -382,7 +453,6 @@ describe("JantComposeDialog", () => {
|
|
|
382
453
|
],
|
|
383
454
|
},
|
|
384
455
|
title: "",
|
|
385
|
-
showTitle: false,
|
|
386
456
|
selection: { from: 3, to: 3 },
|
|
387
457
|
replyExpanded: false,
|
|
388
458
|
},
|
|
@@ -490,7 +560,7 @@ describe("JantComposeDialog", () => {
|
|
|
490
560
|
const el = await createElement();
|
|
491
561
|
|
|
492
562
|
requireElement(
|
|
493
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
563
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
494
564
|
"expected publish settings toggle",
|
|
495
565
|
).click();
|
|
496
566
|
await el.updateComplete;
|
|
@@ -498,16 +568,39 @@ describe("JantComposeDialog", () => {
|
|
|
498
568
|
expect(el.querySelector(".compose-publish-panel")).not.toBeNull();
|
|
499
569
|
});
|
|
500
570
|
|
|
501
|
-
it("
|
|
571
|
+
it("offers saving separately from browsing once there is something to save", async () => {
|
|
502
572
|
const el = await createElement();
|
|
573
|
+
await openPublishPanel(el);
|
|
574
|
+
|
|
575
|
+
// Nothing written yet, so there is only a place to go.
|
|
576
|
+
expect(sheetRowTitles(el)).toContain("Drafts");
|
|
577
|
+
expect(sheetRowTitles(el)).not.toContain("Save as draft");
|
|
578
|
+
|
|
579
|
+
const editor = requireElement(
|
|
580
|
+
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
581
|
+
"expected compose editor",
|
|
582
|
+
);
|
|
583
|
+
editor._bodyJson = {
|
|
584
|
+
type: "doc",
|
|
585
|
+
content: [
|
|
586
|
+
{ type: "paragraph", content: [{ type: "text", text: "Hello world" }] },
|
|
587
|
+
],
|
|
588
|
+
};
|
|
589
|
+
await editor.updateComplete;
|
|
590
|
+
await flushUpdates(el);
|
|
503
591
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
592
|
+
// Now both: an action on this post, and the list. Neither label lies about
|
|
593
|
+
// what its row does.
|
|
594
|
+
const titles = sheetRowTitles(el);
|
|
595
|
+
expect(titles).toContain("Save as draft");
|
|
596
|
+
expect(titles).toContain("Drafts");
|
|
597
|
+
expect(titles.indexOf("Save as draft")).toBeLessThan(
|
|
598
|
+
titles.indexOf("Drafts"),
|
|
507
599
|
);
|
|
508
|
-
|
|
509
|
-
expect(draftButton.getAttribute("aria-label")).toBe("Drafts");
|
|
600
|
+
});
|
|
510
601
|
|
|
602
|
+
it("saves straight to a draft from its own row, with no prompt in the way", async () => {
|
|
603
|
+
const el = await createElement();
|
|
511
604
|
const editor = requireElement(
|
|
512
605
|
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
513
606
|
"expected compose editor",
|
|
@@ -519,34 +612,299 @@ describe("JantComposeDialog", () => {
|
|
|
519
612
|
],
|
|
520
613
|
};
|
|
521
614
|
await editor.updateComplete;
|
|
522
|
-
await el
|
|
615
|
+
await flushUpdates(el);
|
|
616
|
+
|
|
617
|
+
let detail: ComposeSubmitDetail | null = null;
|
|
618
|
+
el.addEventListener("jant:compose-submit-deferred", (event) => {
|
|
619
|
+
detail = (event as CustomEvent<ComposeSubmitDetail>).detail;
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
await openPublishPanel(el);
|
|
623
|
+
requireElement(
|
|
624
|
+
sheetRow(el, "Save as draft"),
|
|
625
|
+
"expected save-as-draft row",
|
|
626
|
+
).click();
|
|
627
|
+
await flushUpdates(el);
|
|
628
|
+
|
|
629
|
+
expect(el._confirmPanelOpen).toBe(false);
|
|
630
|
+
expect(el._draftsPanelOpen).toBe(false);
|
|
631
|
+
expect(detail).not.toBeNull();
|
|
632
|
+
expect(detail!.status).toBe("draft");
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it("opens options from its own button, not a chevron on Publish", async () => {
|
|
636
|
+
const el = await createElement();
|
|
637
|
+
|
|
638
|
+
// The split button is gone: Publish is one button, options is another.
|
|
639
|
+
expect(el.querySelector(".compose-publish-toggle")).toBeNull();
|
|
640
|
+
|
|
641
|
+
const trigger = requireElement(
|
|
642
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
643
|
+
"expected options trigger",
|
|
644
|
+
);
|
|
645
|
+
expect(trigger.getAttribute("aria-expanded")).toBe("false");
|
|
646
|
+
expect(el.querySelector(".compose-sheet")).toBeNull();
|
|
647
|
+
|
|
648
|
+
trigger.click();
|
|
649
|
+
await flushUpdates(el);
|
|
650
|
+
|
|
651
|
+
expect(trigger.getAttribute("aria-expanded")).toBe("true");
|
|
652
|
+
expect(el.querySelector(".compose-sheet")).not.toBeNull();
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
it("keeps date and permalink on the post, not in the publish panel", async () => {
|
|
656
|
+
const el = await createElement();
|
|
657
|
+
|
|
658
|
+
// They describe one post, so they hang off the post — not the publish
|
|
659
|
+
// panel, which speaks for the whole submission.
|
|
660
|
+
requireElement(
|
|
661
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
662
|
+
"expected options trigger",
|
|
663
|
+
).click();
|
|
664
|
+
await flushUpdates(el);
|
|
665
|
+
expect(el.querySelector(".compose-publish-date-input")).toBeNull();
|
|
666
|
+
expect(el.querySelector(".compose-publish-slug-input")).toBeNull();
|
|
667
|
+
|
|
668
|
+
// The pill states both values while the panel is shut.
|
|
669
|
+
expect(pillValue(el, "date")).toBe("Now");
|
|
670
|
+
expect(pillValue(el, "slug")).toBe("Auto");
|
|
671
|
+
|
|
672
|
+
// Two settings is not a list to drill through: opening the pill opens both
|
|
673
|
+
// fields at once, with focus on the panel so Tab lands in them.
|
|
674
|
+
await openPostMeta(el);
|
|
675
|
+
expect(el.querySelector(".compose-publish-date-input")).not.toBeNull();
|
|
676
|
+
expect(el.querySelector(".compose-publish-slug-input")).not.toBeNull();
|
|
677
|
+
expect(el.querySelector("[data-compose-post-meta-panel]")).toBe(
|
|
678
|
+
document.activeElement,
|
|
679
|
+
);
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
it("pins the date panel to its pill instead of inside the scroller", async () => {
|
|
683
|
+
const el = await createElement();
|
|
684
|
+
|
|
685
|
+
// The pill lives in `.compose-scroll`, which clips and scrolls. An
|
|
686
|
+
// absolutely positioned panel is taller than that box on an empty post, so
|
|
687
|
+
// it lost its footer, and focusing it scrolled the format switcher off the
|
|
688
|
+
// top to bring it into view. Fixed positioning leaves the scroller behind,
|
|
689
|
+
// which makes placing the panel our job.
|
|
690
|
+
const focusCalls: Array<{
|
|
691
|
+
target: unknown;
|
|
692
|
+
options?: globalThis.FocusOptions;
|
|
693
|
+
}> = [];
|
|
694
|
+
const nativeFocus = globalThis.HTMLElement.prototype.focus;
|
|
695
|
+
globalThis.HTMLElement.prototype.focus = function (
|
|
696
|
+
this: globalThis.HTMLElement,
|
|
697
|
+
options?: globalThis.FocusOptions,
|
|
698
|
+
) {
|
|
699
|
+
focusCalls.push({ target: this, options });
|
|
700
|
+
return nativeFocus.call(this, options);
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
try {
|
|
704
|
+
await openPostMeta(el);
|
|
705
|
+
} finally {
|
|
706
|
+
globalThis.HTMLElement.prototype.focus = nativeFocus;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const panel = requireElement(
|
|
710
|
+
el.querySelector<globalThis.HTMLElement>(
|
|
711
|
+
"[data-compose-post-meta-panel]",
|
|
712
|
+
),
|
|
713
|
+
"expected post meta panel",
|
|
714
|
+
);
|
|
715
|
+
|
|
716
|
+
expect(focusCalls.find((call) => call.target === panel)?.options).toEqual({
|
|
717
|
+
preventScroll: true,
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
// Placed under the pill, and capped at the room the viewport actually has
|
|
721
|
+
// so a short window scrolls the panel rather than cropping it.
|
|
722
|
+
expect(panel.style.top).toBe("6px");
|
|
723
|
+
expect(panel.style.left).not.toBe("");
|
|
724
|
+
expect(
|
|
725
|
+
panel.style.getPropertyValue("--compose-post-meta-panel-max-height"),
|
|
726
|
+
).toBe(`${globalThis.innerHeight - 12 - 6}px`);
|
|
727
|
+
|
|
728
|
+
// Inline coordinates only mean anything if the panel is out of the
|
|
729
|
+
// scroller — `position: absolute` here is the bug, not a style choice.
|
|
730
|
+
const css = readFileSync(resolve("src/styles/ui.css"), "utf8");
|
|
731
|
+
expect(css).toMatch(
|
|
732
|
+
/\.compose-post-meta-panel\s*\{\s*position:\s*fixed;[\s\S]*max-height:\s*var\(--compose-post-meta-panel-max-height,\s*none\);\s*overflow-y:\s*auto;/,
|
|
733
|
+
);
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
it("keeps the date panel on its pill when the composer scrolls", async () => {
|
|
737
|
+
const el = await createElement();
|
|
738
|
+
await openPostMeta(el);
|
|
739
|
+
|
|
740
|
+
const panel = requireElement(
|
|
741
|
+
el.querySelector<globalThis.HTMLElement>(
|
|
742
|
+
"[data-compose-post-meta-panel]",
|
|
743
|
+
),
|
|
744
|
+
"expected post meta panel",
|
|
745
|
+
);
|
|
746
|
+
const pill = requireElement(
|
|
747
|
+
el.querySelector<globalThis.HTMLElement>("[data-compose-post-meta-pill]"),
|
|
748
|
+
"expected post meta pill",
|
|
749
|
+
);
|
|
750
|
+
pill.getBoundingClientRect = () =>
|
|
751
|
+
({
|
|
752
|
+
top: 100,
|
|
753
|
+
bottom: 130,
|
|
754
|
+
left: 300,
|
|
755
|
+
right: 400,
|
|
756
|
+
width: 100,
|
|
757
|
+
height: 30,
|
|
758
|
+
}) as globalThis.DOMRect;
|
|
759
|
+
|
|
760
|
+
// A `scroll` event on the composer's own box never reaches `window` — it
|
|
761
|
+
// doesn't bubble — so a pinned panel would hang in place while the pill it
|
|
762
|
+
// belongs to slid away underneath it.
|
|
763
|
+
const scroller = requireElement(
|
|
764
|
+
el.querySelector(".compose-scroll"),
|
|
765
|
+
"expected compose scroller",
|
|
766
|
+
);
|
|
767
|
+
scroller.dispatchEvent(new globalThis.Event("scroll"));
|
|
768
|
+
|
|
769
|
+
expect(panel.style.top).toBe("136px");
|
|
770
|
+
expect(panel.style.left).toBe("400px");
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
it("opens the calendar from its own button, not the UA glyph", async () => {
|
|
774
|
+
const el = await createElement();
|
|
775
|
+
await openPostMeta(el);
|
|
776
|
+
|
|
777
|
+
const input = requireElement(
|
|
778
|
+
el.querySelector<HTMLInputElement>(".compose-publish-date-input"),
|
|
779
|
+
"expected publish date input",
|
|
780
|
+
);
|
|
781
|
+
// Chrome only opens the picker from its ~13px indicator, which is hidden
|
|
782
|
+
// here — the button has to drive the same picker itself.
|
|
783
|
+
const showPicker = vi.fn();
|
|
784
|
+
(input as HTMLInputElement & { showPicker: () => void }).showPicker =
|
|
785
|
+
showPicker;
|
|
786
|
+
|
|
787
|
+
requireElement(
|
|
788
|
+
el.querySelector<HTMLButtonElement>("[data-compose-date-picker]"),
|
|
789
|
+
"expected date picker button",
|
|
790
|
+
).click();
|
|
791
|
+
await flushUpdates(el);
|
|
792
|
+
|
|
793
|
+
expect(showPicker).toHaveBeenCalledTimes(1);
|
|
794
|
+
expect(input).toBe(document.activeElement);
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
it("keeps the date field usable when showPicker is unsupported", async () => {
|
|
798
|
+
const el = await createElement();
|
|
799
|
+
await openPostMeta(el);
|
|
800
|
+
|
|
801
|
+
const input = requireElement(
|
|
802
|
+
el.querySelector<HTMLInputElement>(".compose-publish-date-input"),
|
|
803
|
+
"expected publish date input",
|
|
804
|
+
);
|
|
805
|
+
// Engines without `showPicker` must still get a focused, typable field
|
|
806
|
+
// rather than an uncaught TypeError from the button.
|
|
807
|
+
expect(
|
|
808
|
+
(input as HTMLInputElement & { showPicker?: () => void }).showPicker,
|
|
809
|
+
).toBeUndefined();
|
|
810
|
+
|
|
811
|
+
expect(() =>
|
|
812
|
+
requireElement(
|
|
813
|
+
el.querySelector<HTMLButtonElement>("[data-compose-date-picker]"),
|
|
814
|
+
"expected date picker button",
|
|
815
|
+
).click(),
|
|
816
|
+
).not.toThrow();
|
|
817
|
+
await flushUpdates(el);
|
|
818
|
+
|
|
819
|
+
expect(input).toBe(document.activeElement);
|
|
820
|
+
});
|
|
523
821
|
|
|
524
|
-
|
|
525
|
-
|
|
822
|
+
it("closes the date panel from its Done button", async () => {
|
|
823
|
+
const el = await createElement();
|
|
824
|
+
await openPostMeta(el);
|
|
825
|
+
|
|
826
|
+
// Escape and an outside click both close it, but neither announces itself,
|
|
827
|
+
// and touch has no Escape key at all.
|
|
828
|
+
requireElement(
|
|
829
|
+
el.querySelector<HTMLButtonElement>("[data-compose-post-meta-done]"),
|
|
830
|
+
"expected Done button",
|
|
831
|
+
).click();
|
|
832
|
+
await flushUpdates(el);
|
|
833
|
+
|
|
834
|
+
expect(el.querySelector("[data-compose-post-meta-panel]")).toBeNull();
|
|
835
|
+
expect(el.querySelector(".compose-publish-date-input")).toBeNull();
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
it("hands focus back to the post when the date panel closes", async () => {
|
|
839
|
+
const el = await createElement();
|
|
840
|
+
await openPostMeta(el);
|
|
841
|
+
expect(el.querySelector("[data-compose-post-meta-panel]")).toBe(
|
|
842
|
+
document.activeElement,
|
|
843
|
+
);
|
|
844
|
+
|
|
845
|
+
// The panel holds focus while it is open, so it would take focus out of the
|
|
846
|
+
// document with it on the way out. The pill is only visible on hover, focus
|
|
847
|
+
// or a non-default value — and on touch there is no hover — so losing focus
|
|
848
|
+
// to `<body>` hides the only way back in.
|
|
849
|
+
const editor = requireElement(
|
|
850
|
+
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
851
|
+
"expected editor",
|
|
852
|
+
);
|
|
853
|
+
const focusInput = vi.spyOn(editor, "focusInput");
|
|
854
|
+
|
|
855
|
+
requireElement(
|
|
856
|
+
el.querySelector<HTMLButtonElement>("[data-compose-post-meta-pill]"),
|
|
857
|
+
"expected post meta pill",
|
|
858
|
+
).click();
|
|
859
|
+
await flushUpdates(el);
|
|
860
|
+
|
|
861
|
+
expect(el.querySelector("[data-compose-post-meta-panel]")).toBeNull();
|
|
862
|
+
expect(focusInput).toHaveBeenCalled();
|
|
526
863
|
});
|
|
527
864
|
|
|
528
|
-
it("
|
|
865
|
+
it("explains every visibility option and checks the selected one", async () => {
|
|
529
866
|
const el = await createElement();
|
|
530
867
|
|
|
531
868
|
requireElement(
|
|
532
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
869
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
533
870
|
"expected publish settings toggle",
|
|
534
871
|
).click();
|
|
535
872
|
await el.updateComplete;
|
|
536
873
|
|
|
537
|
-
//
|
|
874
|
+
// Each row carries its own hint, so the options can be compared before
|
|
875
|
+
// one is picked — a single hint under a chip group could not do that.
|
|
538
876
|
expect(el.textContent).toContain("Appears in Latest.");
|
|
539
|
-
expect(el.textContent).
|
|
877
|
+
expect(el.textContent).toContain("Only visible when signed in.");
|
|
540
878
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
".compose-publish-chip[role='radio']",
|
|
879
|
+
const rows = el.querySelectorAll<HTMLButtonElement>(
|
|
880
|
+
".compose-sheet-row[role='radio']",
|
|
544
881
|
);
|
|
545
|
-
|
|
546
|
-
|
|
882
|
+
expect(rows).toHaveLength(3);
|
|
883
|
+
expect(rows[0].getAttribute("aria-checked")).toBe("true");
|
|
884
|
+
expect(rows[0].querySelector(".compose-sheet-check")).not.toBeNull();
|
|
885
|
+
expect(rows[2].querySelector(".compose-sheet-check")).toBeNull();
|
|
547
886
|
|
|
548
|
-
|
|
549
|
-
|
|
887
|
+
rows[2].click();
|
|
888
|
+
await flushUpdates(el);
|
|
889
|
+
|
|
890
|
+
// Picking is the whole job of this panel, so it dismisses itself — no
|
|
891
|
+
// Done button to confirm a radio selection.
|
|
892
|
+
expect(el._visibility).toBe("private");
|
|
893
|
+
expect(el.querySelector(".compose-sheet")).toBeNull();
|
|
894
|
+
expect(el.querySelector(".compose-publish-done")).toBeNull();
|
|
895
|
+
|
|
896
|
+
// Reopening shows the new choice checked.
|
|
897
|
+
requireElement(
|
|
898
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
899
|
+
"expected publish settings toggle",
|
|
900
|
+
).click();
|
|
901
|
+
await flushUpdates(el);
|
|
902
|
+
const updated = el.querySelectorAll<HTMLButtonElement>(
|
|
903
|
+
".compose-sheet-row[role='radio']",
|
|
904
|
+
);
|
|
905
|
+
expect(updated[2].getAttribute("aria-checked")).toBe("true");
|
|
906
|
+
expect(updated[2].querySelector(".compose-sheet-check")).not.toBeNull();
|
|
907
|
+
expect(updated[0].querySelector(".compose-sheet-check")).toBeNull();
|
|
550
908
|
});
|
|
551
909
|
|
|
552
910
|
it("format switching updates active state", async () => {
|
|
@@ -630,7 +988,7 @@ describe("JantComposeDialog", () => {
|
|
|
630
988
|
);
|
|
631
989
|
}
|
|
632
990
|
|
|
633
|
-
it("shows the format switcher
|
|
991
|
+
it("shows the format switcher above the post while editing", async () => {
|
|
634
992
|
vi.spyOn(globalThis, "requestAnimationFrame").mockImplementation((cb) => {
|
|
635
993
|
cb(0);
|
|
636
994
|
return 1;
|
|
@@ -641,12 +999,21 @@ describe("JantComposeDialog", () => {
|
|
|
641
999
|
await el.openEdit("pst_123");
|
|
642
1000
|
await flushUpdates(el);
|
|
643
1001
|
|
|
644
|
-
|
|
645
|
-
//
|
|
646
|
-
expect(el.querySelector(".compose-
|
|
1002
|
+
// The switcher has exactly one home: above its own post. Editing says so
|
|
1003
|
+
// through the submit label and a marker in the header row, not a title bar.
|
|
1004
|
+
expect(el.querySelector(".compose-thread-post-header")).toBeTruthy();
|
|
1005
|
+
expect(el.querySelector(".compose-dialog-header")).toBeNull();
|
|
1006
|
+
expect(
|
|
1007
|
+
el
|
|
1008
|
+
.querySelector(".compose-thread-post-header .compose-post-badge")
|
|
1009
|
+
?.textContent?.trim(),
|
|
1010
|
+
).toBe(labels.editing);
|
|
1011
|
+
expect(el.querySelector(".compose-publish-main")?.textContent?.trim()).toBe(
|
|
1012
|
+
labels.update,
|
|
1013
|
+
);
|
|
647
1014
|
});
|
|
648
1015
|
|
|
649
|
-
it("shows
|
|
1016
|
+
it("shows the format selector above the post when replying", async () => {
|
|
650
1017
|
const el = await createElement();
|
|
651
1018
|
await el.openReply("019ce8ce-d6d8-7fda-a5df-c2da2bef5ade", {
|
|
652
1019
|
contentHtml: "<p>Parent</p>",
|
|
@@ -654,14 +1021,8 @@ describe("JantComposeDialog", () => {
|
|
|
654
1021
|
});
|
|
655
1022
|
await flushUpdates(el);
|
|
656
1023
|
|
|
657
|
-
expect(el.querySelector(".compose-dialog-
|
|
658
|
-
|
|
659
|
-
);
|
|
660
|
-
// The header center no longer hosts the format selector...
|
|
661
|
-
expect(
|
|
662
|
-
el.querySelector(".compose-dialog-header-center .compose-segmented"),
|
|
663
|
-
).toBeNull();
|
|
664
|
-
// ...it sits inline above the reply editor instead.
|
|
1024
|
+
expect(el.querySelector(".compose-dialog-header")).toBeNull();
|
|
1025
|
+
// The selector sits inline above the reply editor.
|
|
665
1026
|
expect(el.querySelector(".compose-thread-post-header")).not.toBeNull();
|
|
666
1027
|
expect(
|
|
667
1028
|
el.querySelector(".compose-thread-layout.compose-reply-compose-layout"),
|
|
@@ -673,6 +1034,39 @@ describe("JantComposeDialog", () => {
|
|
|
673
1034
|
expect(el.querySelector(".compose-publish-main")).not.toBeNull();
|
|
674
1035
|
});
|
|
675
1036
|
|
|
1037
|
+
it("puts a reply's add-to-thread row on the rail with the other posts", async () => {
|
|
1038
|
+
const el = await createElement();
|
|
1039
|
+
await el.openReply("019ce8ce-d6d8-7fda-a5df-c2da2bef5ade", {
|
|
1040
|
+
contentHtml: "<p>Parent</p>",
|
|
1041
|
+
dateText: "Mar 14",
|
|
1042
|
+
});
|
|
1043
|
+
await flushUpdates(el);
|
|
1044
|
+
|
|
1045
|
+
// Inside the layout, not a sibling of it: out there it misses the rail's
|
|
1046
|
+
// indent and lands a rail's width left of everything above it.
|
|
1047
|
+
const layout = el.querySelector(
|
|
1048
|
+
".compose-thread-layout.compose-reply-compose-layout",
|
|
1049
|
+
);
|
|
1050
|
+
const addRow = layout?.querySelector(":scope > .compose-thread-add-row");
|
|
1051
|
+
expect(addRow).not.toBeNull();
|
|
1052
|
+
expect(el.querySelector(".compose-add-thread-trigger")).toBeNull();
|
|
1053
|
+
// The dashed dot marks where the next post lands, as in thread compose.
|
|
1054
|
+
expect(addRow?.querySelector(".compose-thread-add-dot")).not.toBeNull();
|
|
1055
|
+
// ...and it stays the last row, so the rail still stops at the reply.
|
|
1056
|
+
expect(layout?.lastElementChild).toBe(addRow);
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
it("keeps a single post's add-to-thread row off the rail", async () => {
|
|
1060
|
+
const el = await createElement();
|
|
1061
|
+
await flushUpdates(el);
|
|
1062
|
+
|
|
1063
|
+
// No rail to join, so no dot: the row's position says "next slot" alone.
|
|
1064
|
+
const trigger = el.querySelector(".compose-add-thread-trigger");
|
|
1065
|
+
expect(trigger).not.toBeNull();
|
|
1066
|
+
expect(el.querySelector(".compose-thread-add-dot")).toBeNull();
|
|
1067
|
+
expect(trigger?.querySelector(".compose-thread-add-btn")).not.toBeNull();
|
|
1068
|
+
});
|
|
1069
|
+
|
|
676
1070
|
it("shows an Edit title with the format selector above the post when editing a reply", async () => {
|
|
677
1071
|
vi.spyOn(globalThis, "requestAnimationFrame").mockImplementation((cb) => {
|
|
678
1072
|
cb(0);
|
|
@@ -705,86 +1099,378 @@ describe("JantComposeDialog", () => {
|
|
|
705
1099
|
await el.openEdit("pst_123");
|
|
706
1100
|
await flushUpdates(el);
|
|
707
1101
|
|
|
708
|
-
expect(el.querySelector(".compose-
|
|
709
|
-
|
|
1102
|
+
expect(el.querySelector(".compose-publish-main")?.textContent?.trim()).toBe(
|
|
1103
|
+
labels.update,
|
|
710
1104
|
);
|
|
711
|
-
expect(
|
|
712
|
-
el.querySelector(".compose-dialog-header-center .compose-segmented"),
|
|
713
|
-
).toBeNull();
|
|
1105
|
+
expect(el.querySelector(".compose-dialog-header")).toBeNull();
|
|
714
1106
|
expect(el.querySelector(".compose-thread-post-header")).not.toBeNull();
|
|
715
1107
|
expect(el._collectionIds).toEqual([]);
|
|
716
1108
|
expect(el.querySelector(".compose-collection-trigger")).toBeNull();
|
|
717
1109
|
});
|
|
718
1110
|
|
|
719
|
-
it("
|
|
1111
|
+
it("keeps the format selector above the post in single-post mode", async () => {
|
|
720
1112
|
const el = await createElement();
|
|
721
|
-
await el.openReply("019ce8ce-d6d8-7fda-a5df-c2da2bef5ade", {
|
|
722
|
-
contentHtml: "<p>Parent</p>",
|
|
723
|
-
dateText: "Mar 14",
|
|
724
|
-
});
|
|
725
1113
|
await flushUpdates(el);
|
|
726
1114
|
|
|
1115
|
+
// One selector, one place — above its own post.
|
|
727
1116
|
const items = el.querySelectorAll<HTMLButtonElement>(
|
|
728
1117
|
".compose-thread-post-header .compose-segmented-item",
|
|
729
1118
|
);
|
|
730
|
-
// note / link / quote
|
|
731
1119
|
expect(items.length).toBe(3);
|
|
1120
|
+
|
|
732
1121
|
items[1].click(); // link
|
|
733
1122
|
await flushUpdates(el);
|
|
734
|
-
|
|
735
1123
|
expect(el._format).toBe("link");
|
|
1124
|
+
|
|
1125
|
+
// A single post has no "which one", so no position marker.
|
|
1126
|
+
expect(el.querySelector(".compose-post-position")).toBeNull();
|
|
736
1127
|
});
|
|
737
1128
|
|
|
738
|
-
it("
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
1129
|
+
it("gives every thread post its own date and permalink control", async () => {
|
|
1130
|
+
const el = await createElement();
|
|
1131
|
+
el._threadItems = [
|
|
1132
|
+
{ id: "thread-1", format: "note" },
|
|
1133
|
+
{ id: "thread-2", format: "note" },
|
|
1134
|
+
];
|
|
1135
|
+
await flushUpdates(el);
|
|
1136
|
+
|
|
1137
|
+
// Every post is separately addressable (`path_registry` has a row each),
|
|
1138
|
+
// so every post gets both controls — not just the root.
|
|
1139
|
+
const pills = el.querySelectorAll("[data-compose-post-meta-pill]");
|
|
1140
|
+
expect(pills).toHaveLength(2);
|
|
1141
|
+
for (const pill of pills) {
|
|
1142
|
+
expect(
|
|
1143
|
+
pill.querySelector(".compose-post-meta-value-slug"),
|
|
1144
|
+
).not.toBeNull();
|
|
1145
|
+
}
|
|
1146
|
+
});
|
|
749
1147
|
|
|
1148
|
+
it("sends a reply's own permalink and blocks an invalid one", async () => {
|
|
750
1149
|
const el = await createElement();
|
|
751
|
-
|
|
1150
|
+
el._threadItems = [
|
|
1151
|
+
{ id: "thread-1", format: "note" },
|
|
1152
|
+
{ id: "thread-2", format: "note", slug: "part-two" },
|
|
1153
|
+
];
|
|
752
1154
|
await flushUpdates(el);
|
|
753
1155
|
|
|
754
|
-
const
|
|
755
|
-
el
|
|
756
|
-
|
|
1156
|
+
const build = (
|
|
1157
|
+
el as unknown as {
|
|
1158
|
+
_buildEditorPostDetail: (
|
|
1159
|
+
editor: JantComposeEditor,
|
|
1160
|
+
format: string,
|
|
1161
|
+
index: number,
|
|
1162
|
+
status: string,
|
|
1163
|
+
) => ComposeSubmitDetail;
|
|
1164
|
+
}
|
|
1165
|
+
)._buildEditorPostDetail;
|
|
1166
|
+
const editors = el.querySelectorAll<JantComposeEditor>(
|
|
1167
|
+
"jant-compose-editor",
|
|
757
1168
|
);
|
|
758
|
-
expect(editor._quoteText).toBe("Stay hungry");
|
|
759
1169
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
1170
|
+
expect(build.call(el, editors[1], "note", 1, "published").slug).toBe(
|
|
1171
|
+
"part-two",
|
|
1172
|
+
);
|
|
1173
|
+
expect(build.call(el, editors[0], "note", 0, "published").slug).toBe(
|
|
1174
|
+
undefined,
|
|
1175
|
+
);
|
|
765
1176
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
1177
|
+
// A reply's bad slug blocks publish just like the root's would.
|
|
1178
|
+
el._threadItems = [
|
|
1179
|
+
{ id: "thread-1", format: "note" },
|
|
1180
|
+
{ id: "thread-2", format: "note", slug: "not a slug!" },
|
|
1181
|
+
];
|
|
1182
|
+
await flushUpdates(el);
|
|
1183
|
+
expect(
|
|
1184
|
+
(el as unknown as { _canPublish: () => boolean })._canPublish(),
|
|
1185
|
+
).toBe(false);
|
|
770
1186
|
});
|
|
771
1187
|
|
|
772
|
-
it("
|
|
773
|
-
vi.spyOn(globalThis, "requestAnimationFrame").mockImplementation((cb) => {
|
|
774
|
-
cb(0);
|
|
775
|
-
return 1;
|
|
776
|
-
});
|
|
777
|
-
mockEditPost({ format: "note", title: "Hello", body: null });
|
|
778
|
-
|
|
1188
|
+
it("sends a reply's own date and leaves the rest for the server to inherit", async () => {
|
|
779
1189
|
const el = await createElement();
|
|
780
|
-
|
|
1190
|
+
el._threadItems = [
|
|
1191
|
+
{ id: "thread-1", format: "note" },
|
|
1192
|
+
{ id: "thread-2", format: "note", publishedAtInput: "2024-03-09" },
|
|
1193
|
+
{ id: "thread-3", format: "note" },
|
|
1194
|
+
];
|
|
781
1195
|
await flushUpdates(el);
|
|
782
1196
|
|
|
783
|
-
const
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
1197
|
+
const detail = (
|
|
1198
|
+
el as unknown as {
|
|
1199
|
+
_buildEditorPostDetail: (
|
|
1200
|
+
editor: JantComposeEditor,
|
|
1201
|
+
format: string,
|
|
1202
|
+
index: number,
|
|
1203
|
+
status: string,
|
|
1204
|
+
) => ComposeSubmitDetail;
|
|
1205
|
+
}
|
|
1206
|
+
)._buildEditorPostDetail;
|
|
1207
|
+
const editors = el.querySelectorAll<JantComposeEditor>(
|
|
1208
|
+
"jant-compose-editor",
|
|
1209
|
+
);
|
|
1210
|
+
|
|
1211
|
+
const second = detail.call(el, editors[1], "note", 1, "published");
|
|
1212
|
+
const third = detail.call(el, editors[2], "note", 2, "published");
|
|
1213
|
+
|
|
1214
|
+
expect(second.publishedAt).toBeGreaterThan(0);
|
|
1215
|
+
// Undefined, not "now" — the server fills it from the root.
|
|
1216
|
+
expect(third.publishedAt).toBeUndefined();
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
it("blocks publishing when any thread post has a future date", async () => {
|
|
1220
|
+
const el = await createElement();
|
|
1221
|
+
const future = new Date(Date.now() + 86400000 * 3)
|
|
1222
|
+
.toISOString()
|
|
1223
|
+
.slice(0, 10);
|
|
1224
|
+
el._threadItems = [
|
|
1225
|
+
{ id: "thread-1", format: "note" },
|
|
1226
|
+
{ id: "thread-2", format: "note", publishedAtInput: future },
|
|
1227
|
+
];
|
|
1228
|
+
await flushUpdates(el);
|
|
1229
|
+
|
|
1230
|
+
expect(
|
|
1231
|
+
(el as unknown as { _canPublish: () => boolean })._canPublish(),
|
|
1232
|
+
).toBe(false);
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
/**
|
|
1236
|
+
* Publish reads each row's own answer, so a row that has been removed stops
|
|
1237
|
+
* being asked. These used to fail: the button was computed during render from
|
|
1238
|
+
* the editors still in the DOM — including the one that render was about to
|
|
1239
|
+
* remove.
|
|
1240
|
+
*/
|
|
1241
|
+
describe("publish reflects the rows that are left", () => {
|
|
1242
|
+
const publishButton = (el: JantComposeDialog) =>
|
|
1243
|
+
requireElement(
|
|
1244
|
+
el.querySelector<HTMLButtonElement>(".compose-publish-main"),
|
|
1245
|
+
"expected publish button",
|
|
1246
|
+
);
|
|
1247
|
+
|
|
1248
|
+
async function typeInto(
|
|
1249
|
+
el: JantComposeDialog,
|
|
1250
|
+
index: number,
|
|
1251
|
+
text: string,
|
|
1252
|
+
) {
|
|
1253
|
+
const editor = el.querySelectorAll<JantComposeEditor>(
|
|
1254
|
+
"jant-compose-editor",
|
|
1255
|
+
)[index];
|
|
1256
|
+
requireEditor(
|
|
1257
|
+
requireElement(editor, `expected editor ${index}`),
|
|
1258
|
+
).commands.insertContent(text);
|
|
1259
|
+
await flushUpdates(el);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
async function addPost(el: JantComposeDialog) {
|
|
1263
|
+
requireElement(
|
|
1264
|
+
el.querySelector<HTMLButtonElement>(".compose-thread-add-btn"),
|
|
1265
|
+
"expected add-to-thread button",
|
|
1266
|
+
).click();
|
|
1267
|
+
await flushUpdates(el);
|
|
1268
|
+
await flushUpdates(el);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
async function removePost(el: JantComposeDialog, index: number) {
|
|
1272
|
+
requireElement(
|
|
1273
|
+
el.querySelectorAll<HTMLButtonElement>(".compose-thread-post-remove")[
|
|
1274
|
+
index
|
|
1275
|
+
],
|
|
1276
|
+
`expected remove button ${index}`,
|
|
1277
|
+
).click();
|
|
1278
|
+
await flushUpdates(el);
|
|
1279
|
+
await flushUpdates(el);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
it("re-enables publish when the empty last post is removed", async () => {
|
|
1283
|
+
const el = await createElement();
|
|
1284
|
+
await typeInto(el, 0, "one");
|
|
1285
|
+
await addPost(el);
|
|
1286
|
+
await typeInto(el, 1, "two");
|
|
1287
|
+
await addPost(el);
|
|
1288
|
+
|
|
1289
|
+
expect(publishButton(el).disabled).toBe(true);
|
|
1290
|
+
|
|
1291
|
+
await removePost(el, 2);
|
|
1292
|
+
|
|
1293
|
+
expect(publishButton(el).disabled).toBe(false);
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
it("re-enables publish when the emptied post is the first of two", async () => {
|
|
1297
|
+
const el = await createElement();
|
|
1298
|
+
await typeInto(el, 0, "one");
|
|
1299
|
+
await addPost(el);
|
|
1300
|
+
await typeInto(el, 1, "two");
|
|
1301
|
+
|
|
1302
|
+
requireEditor(
|
|
1303
|
+
el.querySelectorAll<JantComposeEditor>("jant-compose-editor")[0],
|
|
1304
|
+
).commands.clearContent();
|
|
1305
|
+
await flushUpdates(el);
|
|
1306
|
+
expect(publishButton(el).disabled).toBe(true);
|
|
1307
|
+
|
|
1308
|
+
await removePost(el, 0);
|
|
1309
|
+
|
|
1310
|
+
expect(publishButton(el).disabled).toBe(false);
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
it("keeps each post's own text when the middle post is removed", async () => {
|
|
1314
|
+
const el = await createElement();
|
|
1315
|
+
await typeInto(el, 0, "AAA");
|
|
1316
|
+
await addPost(el);
|
|
1317
|
+
await typeInto(el, 1, "BBB");
|
|
1318
|
+
await addPost(el);
|
|
1319
|
+
await typeInto(el, 2, "CCC");
|
|
1320
|
+
|
|
1321
|
+
await removePost(el, 1);
|
|
1322
|
+
|
|
1323
|
+
const text = Array.from(
|
|
1324
|
+
el.querySelectorAll<JantComposeEditor>("jant-compose-editor"),
|
|
1325
|
+
).map((editor) => requireEditor(editor).getText());
|
|
1326
|
+
expect(text).toEqual(["AAA", "CCC"]);
|
|
1327
|
+
});
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
it("numbers each post in a thread", async () => {
|
|
1331
|
+
const el = await createElement();
|
|
1332
|
+
el._threadItems = [
|
|
1333
|
+
{ id: "thread-1", format: "note" },
|
|
1334
|
+
{ id: "thread-2", format: "note" },
|
|
1335
|
+
];
|
|
1336
|
+
await flushUpdates(el);
|
|
1337
|
+
|
|
1338
|
+
const positions = () =>
|
|
1339
|
+
Array.from(el.querySelectorAll(".compose-post-position")).map((n) =>
|
|
1340
|
+
n.textContent?.trim(),
|
|
1341
|
+
);
|
|
1342
|
+
expect(positions()).toEqual(["1/2", "2/2"]);
|
|
1343
|
+
|
|
1344
|
+
(el as unknown as { _addThreadItem: () => void })._addThreadItem();
|
|
1345
|
+
await flushUpdates(el);
|
|
1346
|
+
|
|
1347
|
+
// The total re-renders on every post, not just the new one.
|
|
1348
|
+
expect(positions()).toEqual(["1/3", "2/3", "3/3"]);
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1351
|
+
const positionsIn = (el: JantComposeDialog) =>
|
|
1352
|
+
Array.from(el.querySelectorAll(".compose-post-position")).map((n) =>
|
|
1353
|
+
n.textContent?.trim(),
|
|
1354
|
+
);
|
|
1355
|
+
|
|
1356
|
+
it("numbers a reply from the end of the thread it continues", async () => {
|
|
1357
|
+
// The parent is the second post of a thread, so the reply is the third —
|
|
1358
|
+
// not the first, and not the second.
|
|
1359
|
+
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
1360
|
+
new Response(JSON.stringify({ id: "pst_parent", threadPosition: 2 }), {
|
|
1361
|
+
headers: { "Content-Type": "application/json" },
|
|
1362
|
+
}),
|
|
1363
|
+
);
|
|
1364
|
+
|
|
1365
|
+
const el = await createElement();
|
|
1366
|
+
await el.openReply("pst_parent", {
|
|
1367
|
+
contentHtml: "<p>Parent</p>",
|
|
1368
|
+
dateText: "Mar 14",
|
|
1369
|
+
});
|
|
1370
|
+
await flushUpdates(el);
|
|
1371
|
+
await flushUpdates(el);
|
|
1372
|
+
|
|
1373
|
+
expect(positionsIn(el)).toEqual(["3/3"]);
|
|
1374
|
+
|
|
1375
|
+
(el as unknown as { _addThreadItem: () => void })._addThreadItem();
|
|
1376
|
+
await flushUpdates(el);
|
|
1377
|
+
|
|
1378
|
+
expect(positionsIn(el)).toEqual(["3/4", "4/4"]);
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
it("shows no number for a reply until the parent's position is known", async () => {
|
|
1382
|
+
// Counting from 1 and then correcting to 3 is worse than arriving late, so
|
|
1383
|
+
// the marker stays away while the lookup is in flight — and stays away for
|
|
1384
|
+
// good if it fails.
|
|
1385
|
+
vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("offline"));
|
|
1386
|
+
|
|
1387
|
+
const el = await createElement();
|
|
1388
|
+
await el.openReply("pst_parent", {
|
|
1389
|
+
contentHtml: "<p>Parent</p>",
|
|
1390
|
+
dateText: "Mar 14",
|
|
1391
|
+
});
|
|
1392
|
+
await flushUpdates(el);
|
|
1393
|
+
await flushUpdates(el);
|
|
1394
|
+
|
|
1395
|
+
expect(positionsIn(el)).toEqual([]);
|
|
1396
|
+
});
|
|
1397
|
+
|
|
1398
|
+
it("leaves a lone new post unnumbered — there is no chain to place it in", async () => {
|
|
1399
|
+
const el = await createElement();
|
|
1400
|
+
await flushUpdates(el);
|
|
1401
|
+
|
|
1402
|
+
expect(el.querySelector(".compose-post-position")).toBeNull();
|
|
1403
|
+
});
|
|
1404
|
+
|
|
1405
|
+
it("switches format from the inline selector when replying", async () => {
|
|
1406
|
+
const el = await createElement();
|
|
1407
|
+
await el.openReply("019ce8ce-d6d8-7fda-a5df-c2da2bef5ade", {
|
|
1408
|
+
contentHtml: "<p>Parent</p>",
|
|
1409
|
+
dateText: "Mar 14",
|
|
1410
|
+
});
|
|
1411
|
+
await flushUpdates(el);
|
|
1412
|
+
|
|
1413
|
+
const items = el.querySelectorAll<HTMLButtonElement>(
|
|
1414
|
+
".compose-thread-post-header .compose-segmented-item",
|
|
1415
|
+
);
|
|
1416
|
+
// note / link / quote
|
|
1417
|
+
expect(items.length).toBe(3);
|
|
1418
|
+
items[1].click(); // link
|
|
1419
|
+
await flushUpdates(el);
|
|
1420
|
+
|
|
1421
|
+
expect(el._format).toBe("link");
|
|
1422
|
+
});
|
|
1423
|
+
|
|
1424
|
+
it("edit-mode format switch folds quote fields into the body", async () => {
|
|
1425
|
+
vi.spyOn(globalThis, "requestAnimationFrame").mockImplementation((cb) => {
|
|
1426
|
+
cb(0);
|
|
1427
|
+
return 1;
|
|
1428
|
+
});
|
|
1429
|
+
mockEditPost({
|
|
1430
|
+
format: "quote",
|
|
1431
|
+
quoteText: "Stay hungry",
|
|
1432
|
+
sourceName: "Jobs",
|
|
1433
|
+
body: null,
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
const el = await createElement();
|
|
1437
|
+
await el.openEdit("pst_123");
|
|
1438
|
+
await flushUpdates(el);
|
|
1439
|
+
|
|
1440
|
+
const editor = requireElement(
|
|
1441
|
+
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
1442
|
+
"expected compose editor",
|
|
1443
|
+
);
|
|
1444
|
+
expect(editor._quoteText).toBe("Stay hungry");
|
|
1445
|
+
|
|
1446
|
+
// Click the Note segmented button.
|
|
1447
|
+
el.querySelectorAll<HTMLButtonElement>(
|
|
1448
|
+
".compose-segmented-item",
|
|
1449
|
+
)[0].click();
|
|
1450
|
+
await flushUpdates(el);
|
|
1451
|
+
|
|
1452
|
+
expect(el._format).toBe("note");
|
|
1453
|
+
expect(editor._quoteText).toBe("");
|
|
1454
|
+
expect(editor._quoteAuthor).toBe("");
|
|
1455
|
+
expect(editor._bodyJson?.content?.[0]?.type).toBe("blockquote");
|
|
1456
|
+
});
|
|
1457
|
+
|
|
1458
|
+
it("edit-mode format switch marks the post as having unsaved changes", async () => {
|
|
1459
|
+
vi.spyOn(globalThis, "requestAnimationFrame").mockImplementation((cb) => {
|
|
1460
|
+
cb(0);
|
|
1461
|
+
return 1;
|
|
1462
|
+
});
|
|
1463
|
+
mockEditPost({ format: "note", title: "Hello", body: null });
|
|
1464
|
+
|
|
1465
|
+
const el = await createElement();
|
|
1466
|
+
await el.openEdit("pst_123");
|
|
1467
|
+
await flushUpdates(el);
|
|
1468
|
+
|
|
1469
|
+
const hasUnsaved = (el as unknown as { _hasUnsavedChanges(): boolean })
|
|
1470
|
+
._hasUnsavedChanges;
|
|
1471
|
+
expect(hasUnsaved.call(el)).toBe(false);
|
|
1472
|
+
|
|
1473
|
+
el.querySelectorAll<HTMLButtonElement>(
|
|
788
1474
|
".compose-segmented-item",
|
|
789
1475
|
)[2].click();
|
|
790
1476
|
await flushUpdates(el);
|
|
@@ -1040,13 +1726,13 @@ describe("JantComposeDialog", () => {
|
|
|
1040
1726
|
await editor.updateComplete;
|
|
1041
1727
|
|
|
1042
1728
|
requireElement(
|
|
1043
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
1729
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1044
1730
|
"expected publish settings toggle",
|
|
1045
1731
|
).click();
|
|
1046
1732
|
await el.updateComplete;
|
|
1047
1733
|
|
|
1048
1734
|
const options = el.querySelectorAll<HTMLButtonElement>(
|
|
1049
|
-
".compose-
|
|
1735
|
+
".compose-sheet-row[role='radio']",
|
|
1050
1736
|
);
|
|
1051
1737
|
expect(options).toHaveLength(3);
|
|
1052
1738
|
options[1]?.click();
|
|
@@ -1100,11 +1786,12 @@ describe("JantComposeDialog", () => {
|
|
|
1100
1786
|
await editor.updateComplete;
|
|
1101
1787
|
|
|
1102
1788
|
requireElement(
|
|
1103
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
1789
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1104
1790
|
"expected publish settings toggle",
|
|
1105
1791
|
).click();
|
|
1106
1792
|
await el.updateComplete;
|
|
1107
1793
|
|
|
1794
|
+
await openPostMeta(el);
|
|
1108
1795
|
const publishedAtInput = requireElement(
|
|
1109
1796
|
el.querySelector<HTMLInputElement>(".compose-publish-date-input"),
|
|
1110
1797
|
"expected publish date input",
|
|
@@ -1145,16 +1832,17 @@ describe("JantComposeDialog", () => {
|
|
|
1145
1832
|
}
|
|
1146
1833
|
});
|
|
1147
1834
|
|
|
1148
|
-
it("
|
|
1835
|
+
it("keeps the chosen publish date readable on the post's pill", async () => {
|
|
1149
1836
|
const el = await createElement();
|
|
1150
1837
|
|
|
1151
1838
|
const publishToggle = requireElement(
|
|
1152
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
1839
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1153
1840
|
"expected publish settings toggle",
|
|
1154
1841
|
);
|
|
1155
1842
|
publishToggle.click();
|
|
1156
1843
|
await el.updateComplete;
|
|
1157
1844
|
|
|
1845
|
+
await openPostMeta(el);
|
|
1158
1846
|
const publishedAtInput = requireElement(
|
|
1159
1847
|
el.querySelector<HTMLInputElement>(".compose-publish-date-input"),
|
|
1160
1848
|
"expected publish date input",
|
|
@@ -1163,31 +1851,25 @@ describe("JantComposeDialog", () => {
|
|
|
1163
1851
|
publishedAtInput.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1164
1852
|
await el.updateComplete;
|
|
1165
1853
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
"[data-compose-publish-date-summary]",
|
|
1172
|
-
),
|
|
1173
|
-
"expected publish date summary",
|
|
1174
|
-
);
|
|
1175
|
-
expect(summary.dataset.publishDate).toBe("2024-01-15");
|
|
1176
|
-
expect(summary.textContent).toContain("2024");
|
|
1177
|
-
|
|
1178
|
-
summary.click();
|
|
1179
|
-
await el.updateComplete;
|
|
1854
|
+
// Close the panel: the chosen date stays readable on the pill.
|
|
1855
|
+
requireElement(
|
|
1856
|
+
el.querySelector<HTMLButtonElement>("[data-compose-post-meta-pill]"),
|
|
1857
|
+
"expected post meta pill",
|
|
1858
|
+
).click();
|
|
1180
1859
|
await flushUpdates(el);
|
|
1860
|
+
expect(el.querySelector(".compose-publish-date-input")).toBeNull();
|
|
1861
|
+
expect(pillValue(el, "date")).toContain("2024");
|
|
1181
1862
|
|
|
1863
|
+
await openPostMeta(el);
|
|
1182
1864
|
expect(
|
|
1183
1865
|
requireElement(
|
|
1184
1866
|
el.querySelector<HTMLInputElement>(".compose-publish-date-input"),
|
|
1185
1867
|
"expected publish date input after reopening publish settings",
|
|
1186
|
-
),
|
|
1187
|
-
).toBe(
|
|
1868
|
+
).value,
|
|
1869
|
+
).toBe("2024-01-15");
|
|
1188
1870
|
});
|
|
1189
1871
|
|
|
1190
|
-
it("
|
|
1872
|
+
it("shows the existing publish date on the post pill while editing a post", async () => {
|
|
1191
1873
|
const el = await createElement();
|
|
1192
1874
|
(
|
|
1193
1875
|
el as unknown as {
|
|
@@ -1213,10 +1895,12 @@ describe("JantComposeDialog", () => {
|
|
|
1213
1895
|
el.requestUpdate();
|
|
1214
1896
|
await el.updateComplete;
|
|
1215
1897
|
|
|
1216
|
-
|
|
1898
|
+
await openPostMeta(el);
|
|
1899
|
+
|
|
1900
|
+
expect(pillValue(el, "date")).toContain("2024");
|
|
1217
1901
|
});
|
|
1218
1902
|
|
|
1219
|
-
it("shows
|
|
1903
|
+
it("shows Now on the post date control after clearing the date while editing", async () => {
|
|
1220
1904
|
const el = await createElement();
|
|
1221
1905
|
(
|
|
1222
1906
|
el as unknown as {
|
|
@@ -1242,15 +1926,12 @@ describe("JantComposeDialog", () => {
|
|
|
1242
1926
|
el.requestUpdate();
|
|
1243
1927
|
await el.updateComplete;
|
|
1244
1928
|
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
"expected publish now summary",
|
|
1249
|
-
).textContent,
|
|
1250
|
-
).toContain("Publish now");
|
|
1929
|
+
await openPostMeta(el);
|
|
1930
|
+
|
|
1931
|
+
expect(pillValue(el, "date")).toBe("Now");
|
|
1251
1932
|
});
|
|
1252
1933
|
|
|
1253
|
-
it("
|
|
1934
|
+
it("keeps the custom link readable on the post's pill", async () => {
|
|
1254
1935
|
mockSlugApi((url) => {
|
|
1255
1936
|
if (url.searchParams.get("mode") === "suggest") {
|
|
1256
1937
|
return { body: { slug: "configured-post" } };
|
|
@@ -1263,7 +1944,7 @@ describe("JantComposeDialog", () => {
|
|
|
1263
1944
|
|
|
1264
1945
|
const el = await createElement();
|
|
1265
1946
|
const publishToggle = requireElement(
|
|
1266
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
1947
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1267
1948
|
"expected publish settings toggle",
|
|
1268
1949
|
);
|
|
1269
1950
|
|
|
@@ -1271,6 +1952,7 @@ describe("JantComposeDialog", () => {
|
|
|
1271
1952
|
await el.updateComplete;
|
|
1272
1953
|
await flushUpdates(el);
|
|
1273
1954
|
|
|
1955
|
+
await openPostMeta(el);
|
|
1274
1956
|
const slugInput = requireElement(
|
|
1275
1957
|
el.querySelector<HTMLInputElement>(".compose-publish-slug-input"),
|
|
1276
1958
|
"expected custom link input",
|
|
@@ -1280,31 +1962,25 @@ describe("JantComposeDialog", () => {
|
|
|
1280
1962
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
1281
1963
|
await el.updateComplete;
|
|
1282
1964
|
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
"[data-compose-publish-slug-summary]",
|
|
1289
|
-
),
|
|
1290
|
-
"expected custom link summary",
|
|
1291
|
-
);
|
|
1292
|
-
expect(summary.dataset.publishSlug).toBe("reading-notes");
|
|
1293
|
-
expect(summary.textContent).toContain("/reading-notes");
|
|
1294
|
-
|
|
1295
|
-
summary.click();
|
|
1296
|
-
await el.updateComplete;
|
|
1965
|
+
// Close the panel: the custom link stays readable on the pill.
|
|
1966
|
+
requireElement(
|
|
1967
|
+
el.querySelector<HTMLButtonElement>("[data-compose-post-meta-pill]"),
|
|
1968
|
+
"expected post meta pill",
|
|
1969
|
+
).click();
|
|
1297
1970
|
await flushUpdates(el);
|
|
1971
|
+
expect(el.querySelector(".compose-publish-slug-input")).toBeNull();
|
|
1972
|
+
expect(pillValue(el, "slug")).toBe("/reading-notes");
|
|
1298
1973
|
|
|
1974
|
+
await openPostMeta(el);
|
|
1299
1975
|
expect(
|
|
1300
1976
|
requireElement(
|
|
1301
1977
|
el.querySelector<HTMLInputElement>(".compose-publish-slug-input"),
|
|
1302
1978
|
"expected custom link input after reopening publish settings",
|
|
1303
|
-
),
|
|
1304
|
-
).toBe(
|
|
1979
|
+
).value,
|
|
1980
|
+
).toBe("reading-notes");
|
|
1305
1981
|
});
|
|
1306
1982
|
|
|
1307
|
-
it("
|
|
1983
|
+
it("shows the existing custom link on the post pill while editing a post", async () => {
|
|
1308
1984
|
const el = await createElement();
|
|
1309
1985
|
(
|
|
1310
1986
|
el as unknown as {
|
|
@@ -1330,10 +2006,12 @@ describe("JantComposeDialog", () => {
|
|
|
1330
2006
|
el.requestUpdate();
|
|
1331
2007
|
await el.updateComplete;
|
|
1332
2008
|
|
|
1333
|
-
|
|
2009
|
+
await openPostMeta(el);
|
|
2010
|
+
|
|
2011
|
+
expect(pillValue(el, "slug")).toBe("/reading-notes");
|
|
1334
2012
|
});
|
|
1335
2013
|
|
|
1336
|
-
it("shows
|
|
2014
|
+
it("shows Auto on the post permalink pill after clearing the link while editing", async () => {
|
|
1337
2015
|
const el = await createElement();
|
|
1338
2016
|
(
|
|
1339
2017
|
el as unknown as {
|
|
@@ -1359,12 +2037,9 @@ describe("JantComposeDialog", () => {
|
|
|
1359
2037
|
el.requestUpdate();
|
|
1360
2038
|
await el.updateComplete;
|
|
1361
2039
|
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
"expected auto link summary",
|
|
1366
|
-
).textContent,
|
|
1367
|
-
).toContain("Auto link");
|
|
2040
|
+
await openPostMeta(el);
|
|
2041
|
+
|
|
2042
|
+
expect(pillValue(el, "slug")).toBe("Auto");
|
|
1368
2043
|
});
|
|
1369
2044
|
|
|
1370
2045
|
it("blocks publishing with a future publish date", async () => {
|
|
@@ -1389,11 +2064,12 @@ describe("JantComposeDialog", () => {
|
|
|
1389
2064
|
await editor.updateComplete;
|
|
1390
2065
|
|
|
1391
2066
|
requireElement(
|
|
1392
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
2067
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1393
2068
|
"expected publish settings toggle",
|
|
1394
2069
|
).click();
|
|
1395
2070
|
await el.updateComplete;
|
|
1396
2071
|
|
|
2072
|
+
await openPostMeta(el);
|
|
1397
2073
|
const publishedAtInput = requireElement(
|
|
1398
2074
|
el.querySelector<HTMLInputElement>(".compose-publish-date-input"),
|
|
1399
2075
|
"expected publish date input",
|
|
@@ -1420,13 +2096,13 @@ describe("JantComposeDialog", () => {
|
|
|
1420
2096
|
const el = await createElement();
|
|
1421
2097
|
|
|
1422
2098
|
requireElement(
|
|
1423
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
2099
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1424
2100
|
"expected publish settings toggle",
|
|
1425
2101
|
).click();
|
|
1426
2102
|
await el.updateComplete;
|
|
1427
2103
|
|
|
1428
2104
|
const options = el.querySelectorAll<HTMLButtonElement>(
|
|
1429
|
-
".compose-
|
|
2105
|
+
".compose-sheet-row[role='radio']",
|
|
1430
2106
|
);
|
|
1431
2107
|
expect(options).toHaveLength(3);
|
|
1432
2108
|
options[2]?.click();
|
|
@@ -1444,13 +2120,13 @@ describe("JantComposeDialog", () => {
|
|
|
1444
2120
|
const el = await createElement();
|
|
1445
2121
|
|
|
1446
2122
|
requireElement(
|
|
1447
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
2123
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1448
2124
|
"expected publish settings toggle",
|
|
1449
2125
|
).click();
|
|
1450
2126
|
await el.updateComplete;
|
|
1451
2127
|
|
|
1452
2128
|
const options = el.querySelectorAll<HTMLButtonElement>(
|
|
1453
|
-
".compose-
|
|
2129
|
+
".compose-sheet-row[role='radio']",
|
|
1454
2130
|
);
|
|
1455
2131
|
expect(options).toHaveLength(3);
|
|
1456
2132
|
options[1]?.click();
|
|
@@ -1464,6 +2140,135 @@ describe("JantComposeDialog", () => {
|
|
|
1464
2140
|
).toBe("Post hidden");
|
|
1465
2141
|
});
|
|
1466
2142
|
|
|
2143
|
+
it("flips visibility from the Hide from Latest shortcut", async () => {
|
|
2144
|
+
const el = await createElement();
|
|
2145
|
+
|
|
2146
|
+
const toggle = requireElement(
|
|
2147
|
+
el.querySelector<HTMLInputElement>(
|
|
2148
|
+
".compose-quick-actions-row .compose-publish-quick-toggle-input",
|
|
2149
|
+
),
|
|
2150
|
+
"expected hide-from-latest shortcut",
|
|
2151
|
+
);
|
|
2152
|
+
expect(toggle.checked).toBe(false);
|
|
2153
|
+
|
|
2154
|
+
toggle.checked = true;
|
|
2155
|
+
toggle.dispatchEvent(new globalThis.Event("change"));
|
|
2156
|
+
await el.updateComplete;
|
|
2157
|
+
expect(el._visibility).toBe("latest_hidden");
|
|
2158
|
+
|
|
2159
|
+
toggle.checked = false;
|
|
2160
|
+
toggle.dispatchEvent(new globalThis.Event("change"));
|
|
2161
|
+
await el.updateComplete;
|
|
2162
|
+
expect(el._visibility).toBe("public");
|
|
2163
|
+
});
|
|
2164
|
+
|
|
2165
|
+
it("drops the Hide from Latest shortcut once the post is private", async () => {
|
|
2166
|
+
const el = await createElement();
|
|
2167
|
+
|
|
2168
|
+
requireElement(
|
|
2169
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
2170
|
+
"expected publish settings toggle",
|
|
2171
|
+
).click();
|
|
2172
|
+
await el.updateComplete;
|
|
2173
|
+
|
|
2174
|
+
// A checkbox cannot speak for a third state, so it steps aside.
|
|
2175
|
+
el.querySelectorAll<HTMLButtonElement>(
|
|
2176
|
+
".compose-sheet-row[role='radio']",
|
|
2177
|
+
)[2]?.click();
|
|
2178
|
+
await el.updateComplete;
|
|
2179
|
+
|
|
2180
|
+
expect(el._visibility).toBe("private");
|
|
2181
|
+
expect(el.querySelector(".compose-quick-actions-row")).toBeNull();
|
|
2182
|
+
});
|
|
2183
|
+
|
|
2184
|
+
it("offers the quiet reply shortcut on a reply, in place of visibility", async () => {
|
|
2185
|
+
const el = await createElement();
|
|
2186
|
+
|
|
2187
|
+
await el.openReply("019ce8ce-d6d8-7fda-a5df-c2da2bef5ade", {
|
|
2188
|
+
contentHtml: "<p>Parent</p>",
|
|
2189
|
+
dateText: "Mar 14",
|
|
2190
|
+
});
|
|
2191
|
+
await flushUpdates(el);
|
|
2192
|
+
|
|
2193
|
+
const toggles = el.querySelectorAll<HTMLInputElement>(
|
|
2194
|
+
".compose-quick-actions-row .compose-publish-quick-toggle-input",
|
|
2195
|
+
);
|
|
2196
|
+
// A reply inherits the root's visibility, so only quiet reply is offered.
|
|
2197
|
+
expect(toggles).toHaveLength(1);
|
|
2198
|
+
|
|
2199
|
+
const toggle = requireElement(
|
|
2200
|
+
toggles.item(0),
|
|
2201
|
+
"expected quiet reply shortcut",
|
|
2202
|
+
);
|
|
2203
|
+
toggle.checked = true;
|
|
2204
|
+
toggle.dispatchEvent(new globalThis.Event("change"));
|
|
2205
|
+
await el.updateComplete;
|
|
2206
|
+
|
|
2207
|
+
expect(el._quietReply).toBe(true);
|
|
2208
|
+
expect(
|
|
2209
|
+
requireElement(
|
|
2210
|
+
el.querySelector<HTMLButtonElement>(".compose-publish-main"),
|
|
2211
|
+
"expected publish button",
|
|
2212
|
+
).textContent?.trim(),
|
|
2213
|
+
).toBe("Reply quietly");
|
|
2214
|
+
});
|
|
2215
|
+
|
|
2216
|
+
it("drops the quiet reply switch when editing an existing reply", async () => {
|
|
2217
|
+
const parentId = "019ce8ce-d6d8-7fda-a5df-c2da2bef5ade";
|
|
2218
|
+
vi.spyOn(globalThis, "fetch").mockImplementation((input) => {
|
|
2219
|
+
const url = String(input);
|
|
2220
|
+
const json = url.includes(parentId)
|
|
2221
|
+
? { id: parentId, bodyHtml: "<p>Parent</p>", format: "note" }
|
|
2222
|
+
: {
|
|
2223
|
+
id: "pst_123",
|
|
2224
|
+
format: "note",
|
|
2225
|
+
title: null,
|
|
2226
|
+
body: null,
|
|
2227
|
+
replyToId: parentId,
|
|
2228
|
+
};
|
|
2229
|
+
return Promise.resolve(
|
|
2230
|
+
new Response(JSON.stringify(json), {
|
|
2231
|
+
headers: { "Content-Type": "application/json" },
|
|
2232
|
+
}),
|
|
2233
|
+
);
|
|
2234
|
+
});
|
|
2235
|
+
|
|
2236
|
+
const el = await createElement();
|
|
2237
|
+
await el.openEdit("pst_123");
|
|
2238
|
+
await flushUpdates(el);
|
|
2239
|
+
|
|
2240
|
+
// Saving an edit goes through the update path, which has no create-time
|
|
2241
|
+
// thread bump to skip — so the switch is gone rather than dead.
|
|
2242
|
+
expect(el.querySelector(".compose-quick-actions-row")).toBeNull();
|
|
2243
|
+
|
|
2244
|
+
el._showPublishPanel = true;
|
|
2245
|
+
await flushUpdates(el);
|
|
2246
|
+
expect(
|
|
2247
|
+
Array.from(el.querySelectorAll(".compose-sheet-title")).some(
|
|
2248
|
+
(row) => row.textContent?.trim() === labels.quietReplyLabel,
|
|
2249
|
+
),
|
|
2250
|
+
).toBe(false);
|
|
2251
|
+
});
|
|
2252
|
+
|
|
2253
|
+
it("keeps the quiet reply switch on a thread draft, which saves by recreating", async () => {
|
|
2254
|
+
const el = await createElement();
|
|
2255
|
+
await el.openReply("019ce8ce-d6d8-7fda-a5df-c2da2bef5ade", {
|
|
2256
|
+
contentHtml: "<p>Parent</p>",
|
|
2257
|
+
dateText: "Mar 14",
|
|
2258
|
+
});
|
|
2259
|
+
(el as unknown as { _addThreadItem: () => void })._addThreadItem();
|
|
2260
|
+
// Re-editing a saved thread draft deletes and recreates every post, so the
|
|
2261
|
+
// create-time flag still lands.
|
|
2262
|
+
el._draftSourceId = "pst_draft";
|
|
2263
|
+
await flushUpdates(el);
|
|
2264
|
+
|
|
2265
|
+
expect(
|
|
2266
|
+
el.querySelectorAll(
|
|
2267
|
+
".compose-quick-actions-row .compose-publish-quick-toggle-input",
|
|
2268
|
+
),
|
|
2269
|
+
).toHaveLength(1);
|
|
2270
|
+
});
|
|
2271
|
+
|
|
1467
2272
|
it("opens a new post with the requested collection and keeps the last visibility until refresh", async () => {
|
|
1468
2273
|
const el = await createElement();
|
|
1469
2274
|
|
|
@@ -1474,13 +2279,13 @@ describe("JantComposeDialog", () => {
|
|
|
1474
2279
|
expect(el._visibility).toBe("public");
|
|
1475
2280
|
|
|
1476
2281
|
requireElement(
|
|
1477
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
2282
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1478
2283
|
"expected publish settings toggle",
|
|
1479
2284
|
).click();
|
|
1480
2285
|
await el.updateComplete;
|
|
1481
2286
|
|
|
1482
2287
|
const options = el.querySelectorAll<HTMLButtonElement>(
|
|
1483
|
-
".compose-
|
|
2288
|
+
".compose-sheet-row[role='radio']",
|
|
1484
2289
|
);
|
|
1485
2290
|
options[1]?.click();
|
|
1486
2291
|
await el.updateComplete;
|
|
@@ -1490,23 +2295,264 @@ describe("JantComposeDialog", () => {
|
|
|
1490
2295
|
el.reset();
|
|
1491
2296
|
await el.updateComplete;
|
|
1492
2297
|
|
|
1493
|
-
expect(el._visibility).toBe("latest_hidden");
|
|
2298
|
+
expect(el._visibility).toBe("latest_hidden");
|
|
2299
|
+
|
|
2300
|
+
await el.openNew({ collectionId: "col-1", restoreDraft: false });
|
|
2301
|
+
await el.updateComplete;
|
|
2302
|
+
|
|
2303
|
+
expect(el._collectionIds).toEqual(["col-1"]);
|
|
2304
|
+
expect(el._visibility).toBe("latest_hidden");
|
|
2305
|
+
});
|
|
2306
|
+
|
|
2307
|
+
it("prepends the requested collection even when a local draft is restored", async () => {
|
|
2308
|
+
const el = await createElement();
|
|
2309
|
+
|
|
2310
|
+
globalThis.localStorage.setItem(
|
|
2311
|
+
"jant:compose-draft",
|
|
2312
|
+
JSON.stringify({
|
|
2313
|
+
format: "note",
|
|
2314
|
+
title: "",
|
|
2315
|
+
bodyJson: {
|
|
2316
|
+
type: "doc",
|
|
2317
|
+
content: [
|
|
2318
|
+
{
|
|
2319
|
+
type: "paragraph",
|
|
2320
|
+
content: [{ type: "text", text: "Draft body" }],
|
|
2321
|
+
},
|
|
2322
|
+
],
|
|
2323
|
+
},
|
|
2324
|
+
url: "",
|
|
2325
|
+
quoteText: "",
|
|
2326
|
+
quoteAuthor: "",
|
|
2327
|
+
slug: "",
|
|
2328
|
+
visibility: "public",
|
|
2329
|
+
rating: 0,
|
|
2330
|
+
showRating: false,
|
|
2331
|
+
collectionIds: ["col-1"],
|
|
2332
|
+
attachedTexts: [],
|
|
2333
|
+
attachmentOrder: [],
|
|
2334
|
+
savedAt: Date.now(),
|
|
2335
|
+
}),
|
|
2336
|
+
);
|
|
2337
|
+
|
|
2338
|
+
await el.openNew({ collectionId: "col-2" });
|
|
2339
|
+
await flushUpdates(el);
|
|
2340
|
+
|
|
2341
|
+
expect(el._collectionIds).toEqual(["col-2", "col-1"]);
|
|
2342
|
+
});
|
|
2343
|
+
|
|
2344
|
+
function seedDraftWithMedia() {
|
|
2345
|
+
globalThis.localStorage.setItem(
|
|
2346
|
+
"jant:compose-draft",
|
|
2347
|
+
JSON.stringify({
|
|
2348
|
+
format: "note",
|
|
2349
|
+
title: "",
|
|
2350
|
+
bodyJson: {
|
|
2351
|
+
type: "doc",
|
|
2352
|
+
content: [
|
|
2353
|
+
{
|
|
2354
|
+
type: "paragraph",
|
|
2355
|
+
content: [{ type: "text", text: "Draft with images" }],
|
|
2356
|
+
},
|
|
2357
|
+
],
|
|
2358
|
+
},
|
|
2359
|
+
url: "",
|
|
2360
|
+
quoteText: "",
|
|
2361
|
+
quoteAuthor: "",
|
|
2362
|
+
slug: "",
|
|
2363
|
+
visibility: "public",
|
|
2364
|
+
rating: 0,
|
|
2365
|
+
showTitle: false,
|
|
2366
|
+
showRating: false,
|
|
2367
|
+
collectionIds: [],
|
|
2368
|
+
replyToId: null,
|
|
2369
|
+
attachedTexts: [],
|
|
2370
|
+
attachmentOrder: ["client-a", "client-b"],
|
|
2371
|
+
mediaAttachments: [
|
|
2372
|
+
{
|
|
2373
|
+
clientId: "client-a",
|
|
2374
|
+
mediaId: "med_aaa",
|
|
2375
|
+
url: "/media/aaa.jpg",
|
|
2376
|
+
mimeType: "image/jpeg",
|
|
2377
|
+
name: "aaa.jpg",
|
|
2378
|
+
alt: "first",
|
|
2379
|
+
},
|
|
2380
|
+
],
|
|
2381
|
+
savedAt: Date.now(),
|
|
2382
|
+
}),
|
|
2383
|
+
);
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
it("restores uploaded media from the draft snapshot plus bridge-supplied uploads", async () => {
|
|
2387
|
+
const el = await createElement();
|
|
2388
|
+
seedDraftWithMedia();
|
|
2389
|
+
|
|
2390
|
+
// client-b's upload finished after the dialog closed — only the bridge
|
|
2391
|
+
// knows about it, so it arrives via restoreMedia instead of the snapshot.
|
|
2392
|
+
await el.openNew({
|
|
2393
|
+
restoreDraft: true,
|
|
2394
|
+
restoreToast: false,
|
|
2395
|
+
restoreMedia: [
|
|
2396
|
+
{
|
|
2397
|
+
clientId: "client-b",
|
|
2398
|
+
mediaId: "med_bbb",
|
|
2399
|
+
url: "/media/bbb.png",
|
|
2400
|
+
mimeType: "image/png",
|
|
2401
|
+
name: "bbb.png",
|
|
2402
|
+
},
|
|
2403
|
+
],
|
|
2404
|
+
});
|
|
2405
|
+
await flushUpdates(el);
|
|
2406
|
+
|
|
2407
|
+
const editor = requireElement(
|
|
2408
|
+
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
2409
|
+
"expected compose editor",
|
|
2410
|
+
);
|
|
2411
|
+
const attachments = editor._attachments;
|
|
2412
|
+
expect(attachments.map((a) => a.clientId)).toEqual([
|
|
2413
|
+
"client-a",
|
|
2414
|
+
"client-b",
|
|
2415
|
+
]);
|
|
2416
|
+
expect(attachments.map((a) => a.mediaId)).toEqual(["med_aaa", "med_bbb"]);
|
|
2417
|
+
expect(attachments.every((a) => a.status === "done")).toBe(true);
|
|
2418
|
+
expect(attachments[0].remoteUrl).toBe("/media/aaa.jpg");
|
|
2419
|
+
expect(attachments[0].alt).toBe("first");
|
|
2420
|
+
expect(editor._attachmentOrder).toEqual(["client-a", "client-b"]);
|
|
2421
|
+
});
|
|
2422
|
+
|
|
2423
|
+
it("suppresses the draft-restored toast when restoreToast is false", async () => {
|
|
2424
|
+
const container = document.createElement("div");
|
|
2425
|
+
container.id = "toast-container";
|
|
2426
|
+
document.body.appendChild(container);
|
|
2427
|
+
const el = await createElement();
|
|
2428
|
+
seedDraftWithMedia();
|
|
2429
|
+
|
|
2430
|
+
await el.openNew({ restoreDraft: true, restoreToast: false });
|
|
2431
|
+
await flushUpdates(el);
|
|
2432
|
+
expect(container.querySelector(".toast")).toBeNull();
|
|
2433
|
+
|
|
2434
|
+
el.reset();
|
|
2435
|
+
await el.openNew({ restoreDraft: true });
|
|
2436
|
+
await flushUpdates(el);
|
|
2437
|
+
expect(container.querySelector(".toast span")?.textContent).toBe(
|
|
2438
|
+
"Draft restored.",
|
|
2439
|
+
);
|
|
2440
|
+
});
|
|
2441
|
+
|
|
2442
|
+
const NOTE_TITLE_KEY = "jant:compose-note-title";
|
|
2443
|
+
|
|
2444
|
+
function titleToggle(el: JantComposeDialog, index = 0): HTMLButtonElement {
|
|
2445
|
+
return requireElement(
|
|
2446
|
+
el.querySelectorAll<HTMLButtonElement>(
|
|
2447
|
+
'.compose-tool-btn[title="Title"]',
|
|
2448
|
+
)[index] ?? null,
|
|
2449
|
+
"expected title toggle",
|
|
2450
|
+
);
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
it("remembers a note's title toggle for the next new post", async () => {
|
|
2454
|
+
const el = await createElement();
|
|
2455
|
+
await el.openNew({ restoreDraft: false });
|
|
2456
|
+
await flushUpdates(el);
|
|
2457
|
+
|
|
2458
|
+
expect(el.querySelector(".compose-note-title")).not.toBeNull();
|
|
2459
|
+
|
|
2460
|
+
titleToggle(el).click();
|
|
2461
|
+
await flushUpdates(el);
|
|
2462
|
+
|
|
2463
|
+
expect(globalThis.localStorage.getItem(NOTE_TITLE_KEY)).toBe("0");
|
|
2464
|
+
expect(el.querySelector(".compose-note-title")).toBeNull();
|
|
2465
|
+
|
|
2466
|
+
await el.openNew({ restoreDraft: false });
|
|
2467
|
+
await flushUpdates(el);
|
|
2468
|
+
|
|
2469
|
+
expect(el.querySelector(".compose-note-title")).toBeNull();
|
|
2470
|
+
|
|
2471
|
+
titleToggle(el).click();
|
|
2472
|
+
await flushUpdates(el);
|
|
2473
|
+
|
|
2474
|
+
expect(globalThis.localStorage.getItem(NOTE_TITLE_KEY)).toBe("1");
|
|
2475
|
+
|
|
2476
|
+
await el.openNew({ restoreDraft: false });
|
|
2477
|
+
await flushUpdates(el);
|
|
2478
|
+
|
|
2479
|
+
expect(el.querySelector(".compose-note-title")).not.toBeNull();
|
|
2480
|
+
});
|
|
2481
|
+
|
|
2482
|
+
it("opens a fresh composer without the title field when it was last turned off", async () => {
|
|
2483
|
+
globalThis.localStorage.setItem(NOTE_TITLE_KEY, "0");
|
|
2484
|
+
|
|
2485
|
+
const el = await createElement();
|
|
2486
|
+
|
|
2487
|
+
expect(el.querySelector(".compose-note-title")).toBeNull();
|
|
2488
|
+
});
|
|
2489
|
+
|
|
2490
|
+
it("does not remember the title toggle from an edit", async () => {
|
|
2491
|
+
mockEditPost({ format: "note", title: "Hello", body: null });
|
|
2492
|
+
|
|
2493
|
+
const el = await createElement();
|
|
2494
|
+
await el.openEdit("pst_123");
|
|
2495
|
+
await flushUpdates(el);
|
|
2496
|
+
|
|
2497
|
+
titleToggle(el).click();
|
|
2498
|
+
await flushUpdates(el);
|
|
2499
|
+
|
|
2500
|
+
// Dropping this post's title says nothing about the next post's.
|
|
2501
|
+
expect(globalThis.localStorage.getItem(NOTE_TITLE_KEY)).toBeNull();
|
|
2502
|
+
});
|
|
2503
|
+
|
|
2504
|
+
it("does not remember the title toggle from a reply", async () => {
|
|
2505
|
+
const el = await createElement();
|
|
2506
|
+
await el.openReply("019ce8ce-d6d8-7fda-a5df-c2da2bef5ade", {
|
|
2507
|
+
contentHtml: "<p>Parent</p>",
|
|
2508
|
+
dateText: "Mar 14",
|
|
2509
|
+
});
|
|
2510
|
+
await flushUpdates(el);
|
|
2511
|
+
|
|
2512
|
+
titleToggle(el).click();
|
|
2513
|
+
await flushUpdates(el);
|
|
2514
|
+
|
|
2515
|
+
expect(el.querySelector(".compose-note-title")).not.toBeNull();
|
|
2516
|
+
expect(globalThis.localStorage.getItem(NOTE_TITLE_KEY)).toBeNull();
|
|
2517
|
+
});
|
|
2518
|
+
|
|
2519
|
+
it("does not remember the title toggle from a thread post", async () => {
|
|
2520
|
+
const el = await createElement();
|
|
2521
|
+
(el as unknown as { _addThreadItem: () => void })._addThreadItem();
|
|
2522
|
+
await flushUpdates(el);
|
|
1494
2523
|
|
|
1495
|
-
|
|
1496
|
-
await el
|
|
2524
|
+
titleToggle(el).click();
|
|
2525
|
+
await flushUpdates(el);
|
|
1497
2526
|
|
|
1498
|
-
expect(
|
|
1499
|
-
expect(el._visibility).toBe("latest_hidden");
|
|
2527
|
+
expect(globalThis.localStorage.getItem(NOTE_TITLE_KEY)).toBeNull();
|
|
1500
2528
|
});
|
|
1501
2529
|
|
|
1502
|
-
it("
|
|
2530
|
+
it("does not remember a title the editor reveals on its own", async () => {
|
|
1503
2531
|
const el = await createElement();
|
|
2532
|
+
await el.openNew({ restoreDraft: false });
|
|
2533
|
+
await flushUpdates(el);
|
|
2534
|
+
|
|
2535
|
+
const editor = requireElement(
|
|
2536
|
+
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
2537
|
+
"expected compose editor",
|
|
2538
|
+
);
|
|
2539
|
+
// The fullscreen and H1-promotion paths land here. Neither is the author
|
|
2540
|
+
// stating a habit, so neither writes one down.
|
|
2541
|
+
editor.setEditorState(null, "Promoted", true);
|
|
2542
|
+
await flushUpdates(el);
|
|
1504
2543
|
|
|
2544
|
+
expect(el.querySelector(".compose-note-title")).not.toBeNull();
|
|
2545
|
+
expect(globalThis.localStorage.getItem(NOTE_TITLE_KEY)).toBeNull();
|
|
2546
|
+
});
|
|
2547
|
+
|
|
2548
|
+
it("lets a restored draft's title state win over the remembered default", async () => {
|
|
2549
|
+
globalThis.localStorage.setItem(NOTE_TITLE_KEY, "0");
|
|
1505
2550
|
globalThis.localStorage.setItem(
|
|
1506
2551
|
"jant:compose-draft",
|
|
1507
2552
|
JSON.stringify({
|
|
1508
2553
|
format: "note",
|
|
1509
|
-
title: "",
|
|
2554
|
+
title: "Draft title",
|
|
2555
|
+
showTitle: true,
|
|
1510
2556
|
bodyJson: {
|
|
1511
2557
|
type: "doc",
|
|
1512
2558
|
content: [
|
|
@@ -1522,19 +2568,21 @@ describe("JantComposeDialog", () => {
|
|
|
1522
2568
|
slug: "",
|
|
1523
2569
|
visibility: "public",
|
|
1524
2570
|
rating: 0,
|
|
1525
|
-
showTitle: false,
|
|
1526
2571
|
showRating: false,
|
|
1527
|
-
collectionIds: [
|
|
2572
|
+
collectionIds: [],
|
|
1528
2573
|
attachedTexts: [],
|
|
1529
2574
|
attachmentOrder: [],
|
|
1530
2575
|
savedAt: Date.now(),
|
|
1531
2576
|
}),
|
|
1532
2577
|
);
|
|
1533
2578
|
|
|
1534
|
-
|
|
2579
|
+
const el = await createElement();
|
|
2580
|
+
await el.openNew();
|
|
1535
2581
|
await flushUpdates(el);
|
|
1536
2582
|
|
|
1537
|
-
expect(
|
|
2583
|
+
expect(
|
|
2584
|
+
el.querySelector<HTMLInputElement>(".compose-note-title")?.value,
|
|
2585
|
+
).toBe("Draft title");
|
|
1538
2586
|
});
|
|
1539
2587
|
|
|
1540
2588
|
it("includes a custom slug from the publish settings panel in the submit payload", async () => {
|
|
@@ -1565,13 +2613,14 @@ describe("JantComposeDialog", () => {
|
|
|
1565
2613
|
await editor.updateComplete;
|
|
1566
2614
|
|
|
1567
2615
|
const publishToggle = requireElement(
|
|
1568
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
2616
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1569
2617
|
"expected publish settings toggle",
|
|
1570
2618
|
);
|
|
1571
2619
|
publishToggle.click();
|
|
1572
2620
|
await el.updateComplete;
|
|
1573
2621
|
await flushUpdates(el);
|
|
1574
2622
|
|
|
2623
|
+
await openPostMeta(el);
|
|
1575
2624
|
const slugInput = requireElement(
|
|
1576
2625
|
el.querySelector<HTMLInputElement>(".compose-publish-slug-input"),
|
|
1577
2626
|
"expected custom link input",
|
|
@@ -1611,7 +2660,7 @@ describe("JantComposeDialog", () => {
|
|
|
1611
2660
|
const el = await createElement();
|
|
1612
2661
|
|
|
1613
2662
|
const publishToggle = requireElement(
|
|
1614
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
2663
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1615
2664
|
"expected publish settings toggle",
|
|
1616
2665
|
);
|
|
1617
2666
|
|
|
@@ -1619,6 +2668,7 @@ describe("JantComposeDialog", () => {
|
|
|
1619
2668
|
await el.updateComplete;
|
|
1620
2669
|
await flushUpdates(el);
|
|
1621
2670
|
|
|
2671
|
+
await openPostMeta(el);
|
|
1622
2672
|
const slugInput = requireElement(
|
|
1623
2673
|
el.querySelector<HTMLInputElement>(".compose-publish-slug-input"),
|
|
1624
2674
|
"expected custom link input",
|
|
@@ -1633,6 +2683,7 @@ describe("JantComposeDialog", () => {
|
|
|
1633
2683
|
publishToggle.click();
|
|
1634
2684
|
await el.updateComplete;
|
|
1635
2685
|
|
|
2686
|
+
await openPostMeta(el);
|
|
1636
2687
|
expect(el.querySelector(".compose-publish-slug-input")).not.toBeNull();
|
|
1637
2688
|
});
|
|
1638
2689
|
|
|
@@ -1691,7 +2742,7 @@ describe("JantComposeDialog", () => {
|
|
|
1691
2742
|
} as never);
|
|
1692
2743
|
|
|
1693
2744
|
requireElement(
|
|
1694
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
2745
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1695
2746
|
"expected publish settings toggle",
|
|
1696
2747
|
).click();
|
|
1697
2748
|
await el.updateComplete;
|
|
@@ -1762,7 +2813,7 @@ describe("JantComposeDialog", () => {
|
|
|
1762
2813
|
} as never);
|
|
1763
2814
|
|
|
1764
2815
|
requireElement(
|
|
1765
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
2816
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1766
2817
|
"expected publish settings toggle",
|
|
1767
2818
|
).click();
|
|
1768
2819
|
await el.updateComplete;
|
|
@@ -1784,7 +2835,7 @@ describe("JantComposeDialog", () => {
|
|
|
1784
2835
|
const el = await createElement();
|
|
1785
2836
|
|
|
1786
2837
|
requireElement(
|
|
1787
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
2838
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1788
2839
|
"expected publish settings toggle",
|
|
1789
2840
|
).click();
|
|
1790
2841
|
await el.updateComplete;
|
|
@@ -1812,13 +2863,14 @@ describe("JantComposeDialog", () => {
|
|
|
1812
2863
|
await editor.updateComplete;
|
|
1813
2864
|
|
|
1814
2865
|
const publishToggle = requireElement(
|
|
1815
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
2866
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1816
2867
|
"expected publish settings toggle",
|
|
1817
2868
|
);
|
|
1818
2869
|
publishToggle.click();
|
|
1819
2870
|
await el.updateComplete;
|
|
1820
2871
|
await flushUpdates(el);
|
|
1821
2872
|
|
|
2873
|
+
await openPostMeta(el);
|
|
1822
2874
|
const slugInput = requireElement(
|
|
1823
2875
|
el.querySelector<HTMLInputElement>(".compose-publish-slug-input"),
|
|
1824
2876
|
"expected custom link input",
|
|
@@ -1863,7 +2915,6 @@ describe("JantComposeDialog", () => {
|
|
|
1863
2915
|
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
1864
2916
|
"expected compose editor",
|
|
1865
2917
|
);
|
|
1866
|
-
editor._showTitle = true;
|
|
1867
2918
|
editor._title = "Hello World";
|
|
1868
2919
|
editor._bodyJson = {
|
|
1869
2920
|
type: "doc",
|
|
@@ -1874,13 +2925,14 @@ describe("JantComposeDialog", () => {
|
|
|
1874
2925
|
await editor.updateComplete;
|
|
1875
2926
|
|
|
1876
2927
|
const publishToggle = requireElement(
|
|
1877
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
2928
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1878
2929
|
"expected publish settings toggle",
|
|
1879
2930
|
);
|
|
1880
2931
|
publishToggle.click();
|
|
1881
2932
|
await el.updateComplete;
|
|
1882
2933
|
await flushUpdates(el);
|
|
1883
2934
|
|
|
2935
|
+
await openPostMeta(el);
|
|
1884
2936
|
expect(
|
|
1885
2937
|
el.querySelector(".compose-slug-suggestion-value")?.textContent?.trim(),
|
|
1886
2938
|
).toBe("/hello-world");
|
|
@@ -1901,11 +2953,11 @@ describe("JantComposeDialog", () => {
|
|
|
1901
2953
|
).toBeUndefined();
|
|
1902
2954
|
});
|
|
1903
2955
|
|
|
1904
|
-
it("
|
|
2956
|
+
it("keeps the publish panel to whole-submission settings", async () => {
|
|
1905
2957
|
const el = await createElement();
|
|
1906
2958
|
|
|
1907
2959
|
const publishToggle = requireElement(
|
|
1908
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
2960
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1909
2961
|
"expected publish settings toggle",
|
|
1910
2962
|
);
|
|
1911
2963
|
publishToggle.click();
|
|
@@ -1917,12 +2969,16 @@ describe("JantComposeDialog", () => {
|
|
|
1917
2969
|
);
|
|
1918
2970
|
expect(panel.textContent).not.toContain("Publish settings");
|
|
1919
2971
|
expect(panel.textContent).toContain("Visibility");
|
|
1920
|
-
expect(panel.textContent).toContain("Published on");
|
|
1921
|
-
expect(panel.textContent).toContain("Custom link");
|
|
1922
2972
|
expect(panel.textContent).not.toContain("Save as draft");
|
|
1923
2973
|
expect(panel.textContent).not.toContain("Discard");
|
|
1924
|
-
|
|
1925
|
-
expect(panel.
|
|
2974
|
+
// Post-scoped settings are not in here — they live on the post.
|
|
2975
|
+
expect(panel.textContent).not.toContain("Published on");
|
|
2976
|
+
expect(panel.textContent).not.toContain("Custom link");
|
|
2977
|
+
|
|
2978
|
+
await openPostMeta(el);
|
|
2979
|
+
expect(el.querySelector(".compose-publish-date-input")).not.toBeNull();
|
|
2980
|
+
await openPostMeta(el);
|
|
2981
|
+
expect(el.querySelector(".compose-publish-slug-input")).not.toBeNull();
|
|
1926
2982
|
});
|
|
1927
2983
|
|
|
1928
2984
|
it("clears the custom slug with the reset action", async () => {
|
|
@@ -1938,13 +2994,14 @@ describe("JantComposeDialog", () => {
|
|
|
1938
2994
|
|
|
1939
2995
|
const el = await createElement();
|
|
1940
2996
|
const publishToggle = requireElement(
|
|
1941
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
2997
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1942
2998
|
"expected publish settings toggle",
|
|
1943
2999
|
);
|
|
1944
3000
|
publishToggle.click();
|
|
1945
3001
|
await el.updateComplete;
|
|
1946
3002
|
await flushUpdates(el);
|
|
1947
3003
|
|
|
3004
|
+
await openPostMeta(el);
|
|
1948
3005
|
const slugInput = requireElement(
|
|
1949
3006
|
el.querySelector<HTMLInputElement>(".compose-publish-slug-input"),
|
|
1950
3007
|
"expected custom link input",
|
|
@@ -1964,6 +3021,7 @@ describe("JantComposeDialog", () => {
|
|
|
1964
3021
|
).click();
|
|
1965
3022
|
await flushUpdates(el);
|
|
1966
3023
|
|
|
3024
|
+
await openPostMeta(el);
|
|
1967
3025
|
expect(
|
|
1968
3026
|
requireElement(
|
|
1969
3027
|
el.querySelector<HTMLInputElement>(".compose-publish-slug-input"),
|
|
@@ -1986,7 +3044,7 @@ describe("JantComposeDialog", () => {
|
|
|
1986
3044
|
await el.updateComplete;
|
|
1987
3045
|
|
|
1988
3046
|
const publishToggle = requireElement(
|
|
1989
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
3047
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
1990
3048
|
"expected publish settings toggle",
|
|
1991
3049
|
);
|
|
1992
3050
|
publishToggle.click();
|
|
@@ -2018,18 +3076,18 @@ describe("JantComposeDialog", () => {
|
|
|
2018
3076
|
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
2019
3077
|
"expected compose editor",
|
|
2020
3078
|
);
|
|
2021
|
-
editor._showTitle = true;
|
|
2022
3079
|
editor._title = "Hello World";
|
|
2023
3080
|
await editor.updateComplete;
|
|
2024
3081
|
|
|
2025
3082
|
const publishToggle = requireElement(
|
|
2026
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
3083
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
2027
3084
|
"expected publish settings toggle",
|
|
2028
3085
|
);
|
|
2029
3086
|
publishToggle.click();
|
|
2030
3087
|
await el.updateComplete;
|
|
2031
3088
|
await flushUpdates(el);
|
|
2032
3089
|
|
|
3090
|
+
await openPostMeta(el);
|
|
2033
3091
|
expect(
|
|
2034
3092
|
el.querySelector(".compose-slug-suggestion-value")?.textContent?.trim(),
|
|
2035
3093
|
).toBe("/hello-world");
|
|
@@ -2079,13 +3137,14 @@ describe("JantComposeDialog", () => {
|
|
|
2079
3137
|
await editor.updateComplete;
|
|
2080
3138
|
|
|
2081
3139
|
const publishToggle = requireElement(
|
|
2082
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
3140
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
2083
3141
|
"expected publish settings toggle",
|
|
2084
3142
|
);
|
|
2085
3143
|
publishToggle.click();
|
|
2086
3144
|
await el.updateComplete;
|
|
2087
3145
|
await flushUpdates(el);
|
|
2088
3146
|
|
|
3147
|
+
await openPostMeta(el);
|
|
2089
3148
|
const slugInput = requireElement(
|
|
2090
3149
|
el.querySelector<HTMLInputElement>(".compose-publish-slug-input"),
|
|
2091
3150
|
"expected custom link input",
|
|
@@ -2136,11 +3195,12 @@ describe("JantComposeDialog", () => {
|
|
|
2136
3195
|
await editor.updateComplete;
|
|
2137
3196
|
|
|
2138
3197
|
requireElement(
|
|
2139
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
3198
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
2140
3199
|
"expected publish settings toggle",
|
|
2141
3200
|
).click();
|
|
2142
3201
|
await el.updateComplete;
|
|
2143
3202
|
|
|
3203
|
+
await openPostMeta(el);
|
|
2144
3204
|
const slugInput = requireElement(
|
|
2145
3205
|
el.querySelector<HTMLInputElement>(".compose-publish-slug-input"),
|
|
2146
3206
|
"expected custom link input",
|
|
@@ -2188,11 +3248,12 @@ describe("JantComposeDialog", () => {
|
|
|
2188
3248
|
await editor.updateComplete;
|
|
2189
3249
|
|
|
2190
3250
|
requireElement(
|
|
2191
|
-
el.querySelector<HTMLButtonElement>(".compose-
|
|
3251
|
+
el.querySelector<HTMLButtonElement>(".compose-options-trigger"),
|
|
2192
3252
|
"expected publish settings toggle",
|
|
2193
3253
|
).click();
|
|
2194
3254
|
await el.updateComplete;
|
|
2195
3255
|
|
|
3256
|
+
await openPostMeta(el);
|
|
2196
3257
|
const slugInput = requireElement(
|
|
2197
3258
|
el.querySelector<HTMLInputElement>(".compose-publish-slug-input"),
|
|
2198
3259
|
"expected custom link input",
|
|
@@ -2306,7 +3367,6 @@ describe("JantComposeDialog", () => {
|
|
|
2306
3367
|
slug: "",
|
|
2307
3368
|
visibility: "public",
|
|
2308
3369
|
rating: 0,
|
|
2309
|
-
showTitle: false,
|
|
2310
3370
|
showRating: false,
|
|
2311
3371
|
collectionIds: ["col-stale"],
|
|
2312
3372
|
replyToId,
|
|
@@ -2451,15 +3511,15 @@ describe("JantComposeDialog", () => {
|
|
|
2451
3511
|
];
|
|
2452
3512
|
await el.updateComplete;
|
|
2453
3513
|
|
|
2454
|
-
const
|
|
2455
|
-
el.querySelector<HTMLElement>(".compose-
|
|
2456
|
-
"expected
|
|
3514
|
+
const scroller = requireElement(
|
|
3515
|
+
el.querySelector<HTMLElement>(".compose-scroll"),
|
|
3516
|
+
"expected compose scroll region",
|
|
2457
3517
|
);
|
|
2458
|
-
Object.defineProperty(
|
|
3518
|
+
Object.defineProperty(scroller, "scrollHeight", {
|
|
2459
3519
|
configurable: true,
|
|
2460
3520
|
value: 640,
|
|
2461
3521
|
});
|
|
2462
|
-
|
|
3522
|
+
scroller.scrollTop = 120;
|
|
2463
3523
|
|
|
2464
3524
|
(
|
|
2465
3525
|
el as unknown as {
|
|
@@ -2468,7 +3528,7 @@ describe("JantComposeDialog", () => {
|
|
|
2468
3528
|
)._addThreadItem();
|
|
2469
3529
|
await flushUpdates(el);
|
|
2470
3530
|
|
|
2471
|
-
expect(
|
|
3531
|
+
expect(scroller.scrollTop).toBe(640);
|
|
2472
3532
|
});
|
|
2473
3533
|
|
|
2474
3534
|
it("omits visibility from locked edit submissions", async () => {
|
|
@@ -2512,6 +3572,20 @@ describe("JantComposeDialog", () => {
|
|
|
2512
3572
|
);
|
|
2513
3573
|
});
|
|
2514
3574
|
|
|
3575
|
+
it("gives the collection trigger one glyph and no chevron", async () => {
|
|
3576
|
+
const el = await createElement();
|
|
3577
|
+
const trigger = requireElement(
|
|
3578
|
+
el.querySelector<HTMLButtonElement>(".compose-collection-trigger"),
|
|
3579
|
+
"expected collection trigger",
|
|
3580
|
+
);
|
|
3581
|
+
|
|
3582
|
+
expect(trigger.querySelectorAll("svg").length).toBe(1);
|
|
3583
|
+
expect(
|
|
3584
|
+
trigger.querySelector(".compose-collection-trigger-svg"),
|
|
3585
|
+
).not.toBeNull();
|
|
3586
|
+
expect(trigger.querySelector(".compose-collection-chevron")).toBeNull();
|
|
3587
|
+
});
|
|
3588
|
+
|
|
2515
3589
|
it("collection selector toggles IDs", async () => {
|
|
2516
3590
|
const el = await createElement();
|
|
2517
3591
|
|
|
@@ -3042,13 +4116,10 @@ describe("JantComposeDialog", () => {
|
|
|
3042
4116
|
};
|
|
3043
4117
|
await editor.updateComplete;
|
|
3044
4118
|
|
|
3045
|
-
// Click the
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
);
|
|
3050
|
-
draftBtn.click();
|
|
3051
|
-
await el.updateComplete;
|
|
4119
|
+
// Click the drafts row in the options panel — should show confirm panel
|
|
4120
|
+
await openPublishPanel(el);
|
|
4121
|
+
requireElement(draftsRow(el), "expected drafts row").click();
|
|
4122
|
+
await flushUpdates(el);
|
|
3052
4123
|
|
|
3053
4124
|
expect(el._confirmPanelOpen).toBe(true);
|
|
3054
4125
|
expect(el.querySelector(".compose-confirm-panel")).not.toBeNull();
|
|
@@ -3065,13 +4136,10 @@ describe("JantComposeDialog", () => {
|
|
|
3065
4136
|
}),
|
|
3066
4137
|
);
|
|
3067
4138
|
|
|
3068
|
-
// Click the
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
);
|
|
3073
|
-
draftBtn.click();
|
|
3074
|
-
await el.updateComplete;
|
|
4139
|
+
// Click the drafts row in the options panel — should open drafts panel
|
|
4140
|
+
await openPublishPanel(el);
|
|
4141
|
+
requireElement(draftsRow(el), "expected drafts row").click();
|
|
4142
|
+
await flushUpdates(el);
|
|
3075
4143
|
|
|
3076
4144
|
expect(el._draftsPanelOpen).toBe(true);
|
|
3077
4145
|
|
|
@@ -3132,143 +4200,597 @@ describe("JantComposeDialog", () => {
|
|
|
3132
4200
|
menu.querySelector<HTMLAnchorElement>('a[role="menuitem"]'),
|
|
3133
4201
|
"expected preview link",
|
|
3134
4202
|
);
|
|
3135
|
-
expect(previewLink.getAttribute("href")).toBe("/blog/preview/draft-slug");
|
|
3136
|
-
expect(previewLink.target).toBe("_blank");
|
|
3137
|
-
expect(previewLink.rel).toBe("noopener noreferrer");
|
|
3138
|
-
|
|
3139
|
-
const fetchSpy = vi.spyOn(globalThis, "fetch");
|
|
3140
|
-
previewLink.addEventListener("click", (event) => event.preventDefault());
|
|
3141
|
-
previewLink.click();
|
|
3142
|
-
await el.updateComplete;
|
|
3143
|
-
|
|
3144
|
-
expect(fetchSpy).not.toHaveBeenCalled();
|
|
3145
|
-
expect(el._draftsPanelOpen).toBe(true);
|
|
3146
|
-
expect(el._draftMenuOpenId).toBeNull();
|
|
4203
|
+
expect(previewLink.getAttribute("href")).toBe("/blog/preview/draft-slug");
|
|
4204
|
+
expect(previewLink.target).toBe("_blank");
|
|
4205
|
+
expect(previewLink.rel).toBe("noopener noreferrer");
|
|
4206
|
+
|
|
4207
|
+
const fetchSpy = vi.spyOn(globalThis, "fetch");
|
|
4208
|
+
previewLink.addEventListener("click", (event) => event.preventDefault());
|
|
4209
|
+
previewLink.click();
|
|
4210
|
+
await el.updateComplete;
|
|
4211
|
+
|
|
4212
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
4213
|
+
expect(el._draftsPanelOpen).toBe(true);
|
|
4214
|
+
expect(el._draftMenuOpenId).toBeNull();
|
|
4215
|
+
});
|
|
4216
|
+
|
|
4217
|
+
it("uses title or quote text for draft previews and identifies each format", async () => {
|
|
4218
|
+
const el = await createElement();
|
|
4219
|
+
el._draftsPanelOpen = true;
|
|
4220
|
+
el._draftsLoading = false;
|
|
4221
|
+
el._drafts = [
|
|
4222
|
+
{
|
|
4223
|
+
id: "pst_note",
|
|
4224
|
+
slug: "note-draft",
|
|
4225
|
+
format: "note",
|
|
4226
|
+
title: "A titled note",
|
|
4227
|
+
bodyText: "This body should not be the preview",
|
|
4228
|
+
bodyHtml: "<p>This body should not be the preview</p>",
|
|
4229
|
+
url: null,
|
|
4230
|
+
quoteText: null,
|
|
4231
|
+
replyToId: null,
|
|
4232
|
+
updatedAt: 0,
|
|
4233
|
+
mediaAttachments: [],
|
|
4234
|
+
},
|
|
4235
|
+
{
|
|
4236
|
+
id: "pst_quote",
|
|
4237
|
+
slug: "quote-draft",
|
|
4238
|
+
format: "quote",
|
|
4239
|
+
title: "Quote title",
|
|
4240
|
+
bodyText: "Supporting note",
|
|
4241
|
+
bodyHtml: "<p>Supporting note</p>",
|
|
4242
|
+
url: null,
|
|
4243
|
+
quoteText: "The quoted words come first.",
|
|
4244
|
+
replyToId: null,
|
|
4245
|
+
updatedAt: 0,
|
|
4246
|
+
mediaAttachments: [],
|
|
4247
|
+
},
|
|
4248
|
+
{
|
|
4249
|
+
id: "pst_link",
|
|
4250
|
+
slug: "link-draft",
|
|
4251
|
+
format: "link",
|
|
4252
|
+
title: null,
|
|
4253
|
+
bodyText: "A link note",
|
|
4254
|
+
bodyHtml: "<p>A link note</p>",
|
|
4255
|
+
url: "https://example.com",
|
|
4256
|
+
quoteText: null,
|
|
4257
|
+
replyToId: null,
|
|
4258
|
+
updatedAt: 0,
|
|
4259
|
+
mediaAttachments: [],
|
|
4260
|
+
},
|
|
4261
|
+
];
|
|
4262
|
+
await el.updateComplete;
|
|
4263
|
+
|
|
4264
|
+
const rows = el.querySelectorAll<HTMLElement>(".compose-draft-item");
|
|
4265
|
+
expect(rows).toHaveLength(3);
|
|
4266
|
+
expect(rows[0]?.querySelector(".compose-draft-preview")?.textContent).toBe(
|
|
4267
|
+
"A titled note",
|
|
4268
|
+
);
|
|
4269
|
+
expect(rows[1]?.querySelector(".compose-draft-preview")?.textContent).toBe(
|
|
4270
|
+
"The quoted words come first.",
|
|
4271
|
+
);
|
|
4272
|
+
expect(rows[2]?.querySelector(".compose-draft-preview")?.textContent).toBe(
|
|
4273
|
+
"A link note",
|
|
4274
|
+
);
|
|
4275
|
+
expect(
|
|
4276
|
+
Array.from(el.querySelectorAll(".compose-draft-format")).map(
|
|
4277
|
+
(format) => format.textContent,
|
|
4278
|
+
),
|
|
4279
|
+
).toEqual(["Note", "Quote", "Link"]);
|
|
4280
|
+
});
|
|
4281
|
+
|
|
4282
|
+
it("does not dispatch submit when loading", async () => {
|
|
4283
|
+
const el = await createElement();
|
|
4284
|
+
el._loading = true;
|
|
4285
|
+
await el.updateComplete;
|
|
4286
|
+
|
|
4287
|
+
let dispatched = false;
|
|
4288
|
+
el.addEventListener("jant:compose-submit-deferred", () => {
|
|
4289
|
+
dispatched = true;
|
|
4290
|
+
});
|
|
4291
|
+
|
|
4292
|
+
const postBtn = requireElement(
|
|
4293
|
+
el.querySelector<HTMLButtonElement>(".compose-publish-main"),
|
|
4294
|
+
"expected post button",
|
|
4295
|
+
);
|
|
4296
|
+
postBtn.click();
|
|
4297
|
+
|
|
4298
|
+
expect(dispatched).toBe(false);
|
|
4299
|
+
});
|
|
4300
|
+
|
|
4301
|
+
it("loading state shows spinner in submit button", async () => {
|
|
4302
|
+
const el = await createElement();
|
|
4303
|
+
el._loading = true;
|
|
4304
|
+
await el.updateComplete;
|
|
4305
|
+
|
|
4306
|
+
const spinner = el.querySelector(".compose-publish-main .animate-spin");
|
|
4307
|
+
expect(spinner).not.toBeNull();
|
|
4308
|
+
});
|
|
4309
|
+
|
|
4310
|
+
it("no old media picker dialog is rendered", async () => {
|
|
4311
|
+
const el = await createElement();
|
|
4312
|
+
|
|
4313
|
+
expect(el.querySelector("#compose-media-picker")).toBeNull();
|
|
4314
|
+
expect(el.querySelector(".compose-media-picker")).toBeNull();
|
|
4315
|
+
});
|
|
4316
|
+
|
|
4317
|
+
it("editor renders attachments when present", async () => {
|
|
4318
|
+
const el = await createElement();
|
|
4319
|
+
const editor = requireElement(
|
|
4320
|
+
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
4321
|
+
"expected compose editor",
|
|
4322
|
+
);
|
|
4323
|
+
|
|
4324
|
+
// Simulate adding an attachment
|
|
4325
|
+
const blob = new Blob(["fake-image"], { type: "image/png" });
|
|
4326
|
+
const file = new File([blob], "test.png", { type: "image/png" });
|
|
4327
|
+
const previewUrl = URL.createObjectURL(blob);
|
|
4328
|
+
|
|
4329
|
+
editor._attachments = [
|
|
4330
|
+
{
|
|
4331
|
+
clientId: "test-id-1",
|
|
4332
|
+
file,
|
|
4333
|
+
previewUrl,
|
|
4334
|
+
status: "done",
|
|
4335
|
+
progress: null,
|
|
4336
|
+
mediaId: "media-1",
|
|
4337
|
+
alt: "",
|
|
4338
|
+
error: null,
|
|
4339
|
+
posterUrl: null,
|
|
4340
|
+
remoteUrl: null,
|
|
4341
|
+
summary: null,
|
|
4342
|
+
chars: null,
|
|
4343
|
+
},
|
|
4344
|
+
];
|
|
4345
|
+
editor._attachmentOrder = ["test-id-1"];
|
|
4346
|
+
await editor.updateComplete;
|
|
4347
|
+
|
|
4348
|
+
// Thumbnail strip should be visible
|
|
4349
|
+
expect(editor.querySelector(".compose-attachments")).not.toBeNull();
|
|
4350
|
+
expect(editor.querySelector(".compose-attachment-thumb")).not.toBeNull();
|
|
4351
|
+
// ALT button should be visible
|
|
4352
|
+
expect(editor.querySelector(".compose-attachment-alt")).not.toBeNull();
|
|
4353
|
+
// Media tool button should show inline "Add" label
|
|
4354
|
+
const mediaBtn =
|
|
4355
|
+
editor.querySelector<HTMLButtonElement>(".compose-tool-btn");
|
|
4356
|
+
expect(mediaBtn?.querySelector(".compose-tool-label")?.textContent).toBe(
|
|
4357
|
+
"Add",
|
|
4358
|
+
);
|
|
4359
|
+
|
|
4360
|
+
URL.revokeObjectURL(previewUrl);
|
|
4361
|
+
});
|
|
4362
|
+
|
|
4363
|
+
it("puts the single-post editor straight inside the one scroll region", async () => {
|
|
4364
|
+
const el = await createElement();
|
|
4365
|
+
|
|
4366
|
+
const scroller = requireElement(
|
|
4367
|
+
el.querySelector<HTMLElement>(".compose-scroll"),
|
|
4368
|
+
"expected compose scroll region",
|
|
4369
|
+
);
|
|
4370
|
+
const editor = requireElement(
|
|
4371
|
+
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
4372
|
+
"expected compose editor",
|
|
4373
|
+
);
|
|
4374
|
+
|
|
4375
|
+
// The rules that lay the editor out are keyed on
|
|
4376
|
+
// `.compose-scroll > jant-compose-editor`. Any wrapper breaks them —
|
|
4377
|
+
// including a `display: contents` one, which drops the box but not the DOM
|
|
4378
|
+
// parentage the selector matches on.
|
|
4379
|
+
expect(editor.parentElement).toBe(scroller);
|
|
4380
|
+
// The action row is the composer's pinned floor; it must stay outside.
|
|
4381
|
+
expect(scroller.querySelector(".compose-action-row")).toBeNull();
|
|
4382
|
+
expect(el.querySelector(".compose-action-row")).not.toBeNull();
|
|
4383
|
+
});
|
|
4384
|
+
|
|
4385
|
+
it("scrolls in exactly one place across every compose mode", async () => {
|
|
4386
|
+
const css = readFileSync(resolve("src/styles/ui.css"), "utf8");
|
|
4387
|
+
|
|
4388
|
+
// `.compose-scroll` owns the scrolling for single-post, reply and thread
|
|
4389
|
+
// alike. Anything nested inside it that scrolls on its own is the bug this
|
|
4390
|
+
// replaced: three stacked scrollers, and previews shrunk to fit them.
|
|
4391
|
+
expect(css).toMatch(
|
|
4392
|
+
/\.compose-scroll\s*\{[\s\S]*flex:\s*1;[\s\S]*min-height:\s*0;[\s\S]*overflow-y:\s*auto;/,
|
|
4393
|
+
);
|
|
4394
|
+
expect(css).not.toMatch(/\.compose-body\s*\{[^}]*overflow-y:\s*auto;/);
|
|
4395
|
+
expect(css).not.toMatch(
|
|
4396
|
+
/\.compose-thread-compose-layout\s*\{[^}]*overflow-y:\s*auto;/,
|
|
4397
|
+
);
|
|
4398
|
+
expect(css).not.toMatch(
|
|
4399
|
+
/\.compose-editor-row\s+\.compose-attachments-dock\s*\{[^}]*overflow-y:\s*auto;/,
|
|
4400
|
+
);
|
|
4401
|
+
|
|
4402
|
+
// Nothing between the scroll region and the text may clip or claim a fixed
|
|
4403
|
+
// share of the height — every block is sized by its own content.
|
|
4404
|
+
expect(css).not.toMatch(
|
|
4405
|
+
/\.compose-editor-row\s*\{[^}]*overflow:\s*hidden;/,
|
|
4406
|
+
);
|
|
4407
|
+
expect(css).not.toMatch(
|
|
4408
|
+
/\.compose-thread-layout\s*\{[^}]*overflow:\s*hidden;/,
|
|
4409
|
+
);
|
|
4410
|
+
});
|
|
4411
|
+
|
|
4412
|
+
it("lets attachment previews take the room the pinned toolbar used to need", () => {
|
|
4413
|
+
const css = readFileSync(resolve("src/styles/ui.css"), "utf8");
|
|
4414
|
+
|
|
4415
|
+
expect(css).toMatch(
|
|
4416
|
+
/\.compose-attachment-img\s*\{[\s\S]*max-height:\s*min\(420px,\s*46dvh\);/,
|
|
4417
|
+
);
|
|
4418
|
+
expect(css).toMatch(
|
|
4419
|
+
/\.compose-attachment:only-child\s+\.compose-attachment-img\s*\{[\s\S]*max-height:\s*min\(460px,\s*52dvh\);/,
|
|
4420
|
+
);
|
|
4421
|
+
expect(css).toMatch(
|
|
4422
|
+
/\.compose-attachment:not\(:only-child\)\s+\.compose-attachment-img\s*\{[\s\S]*height:\s*min\(340px,\s*40dvh\);/,
|
|
4423
|
+
);
|
|
4424
|
+
// Reply compose no longer shrinks its previews to 96×72 to spare the
|
|
4425
|
+
// toolbar — one preview size everywhere.
|
|
4426
|
+
expect(css).not.toMatch(/--compose-reply-attachment-width/);
|
|
4427
|
+
});
|
|
4428
|
+
|
|
4429
|
+
it("routes a format change straight off the single-post editor", async () => {
|
|
4430
|
+
const el = await createElement();
|
|
4431
|
+
const editor = requireElement(
|
|
4432
|
+
el.querySelector<JantComposeEditor>("jant-compose-editor"),
|
|
4433
|
+
"expected compose editor",
|
|
4434
|
+
);
|
|
4435
|
+
|
|
4436
|
+
editor.dispatchEvent(
|
|
4437
|
+
new CustomEvent("jant:format-change", {
|
|
4438
|
+
detail: { format: "quote" },
|
|
4439
|
+
bubbles: true,
|
|
4440
|
+
}),
|
|
4441
|
+
);
|
|
4442
|
+
await flushUpdates(el);
|
|
4443
|
+
|
|
4444
|
+
expect(el._format).toBe("quote");
|
|
4445
|
+
});
|
|
4446
|
+
|
|
4447
|
+
it("shows the replied-to post at the same scale as the reply", () => {
|
|
4448
|
+
const css = readFileSync(resolve("src/styles/ui.css"), "utf8");
|
|
4449
|
+
|
|
4450
|
+
// Same ceiling as the composer's own previews — the parent is not a
|
|
4451
|
+
// thumbnail strip.
|
|
4452
|
+
expect(css).toMatch(
|
|
4453
|
+
/\.compose-reply-context-body img,[\s\S]*?video\s*\{[\s\S]*?max-height:\s*min\(420px,\s*46dvh\);/,
|
|
4454
|
+
);
|
|
4455
|
+
// One rule governs every picture in the context. A second one keyed on
|
|
4456
|
+
// `.compose-reply-context-media-img` would sit at lower specificity than
|
|
4457
|
+
// the `…-body img` rule above it and never apply — the media strip is
|
|
4458
|
+
// inside the body.
|
|
4459
|
+
expect(css).not.toMatch(/\.compose-reply-context-media-img\s*\{/);
|
|
4460
|
+
|
|
4461
|
+
// Expanding hands the post its full height; a capped, separately scrolling
|
|
4462
|
+
// box is what left no room for full-size pictures.
|
|
4463
|
+
expect(css).toMatch(
|
|
4464
|
+
/\.compose-reply-context\.expanded\s*\{\s*max-height:\s*none;\s*overflow:\s*visible;/,
|
|
4465
|
+
);
|
|
4466
|
+
// Fullscreen's collapsed cap must not outrank that.
|
|
4467
|
+
expect(css).toMatch(
|
|
4468
|
+
/\.compose-fullscreen-thread-layout\s+\.compose-reply-context:not\(\.expanded\)\s*\{\s*max-height:\s*140px;/,
|
|
4469
|
+
);
|
|
4470
|
+
});
|
|
4471
|
+
|
|
4472
|
+
it("keeps reply compose tools inside the editor's text column", () => {
|
|
4473
|
+
const css = readFileSync(resolve("src/styles/ui.css"), "utf8");
|
|
4474
|
+
|
|
4475
|
+
// `flex: 1` on the editor is horizontal — it claims the width the rail dot
|
|
4476
|
+
// leaves. Height comes from content.
|
|
4477
|
+
expect(css).toMatch(
|
|
4478
|
+
/\.compose-editor-row\s*>\s*jant-compose-editor\s*\{[\s\S]*flex:\s*1;[\s\S]*min-width:\s*0;/,
|
|
4479
|
+
);
|
|
4480
|
+
expect(css).toMatch(
|
|
4481
|
+
/\.compose-reply-compose-layout\s+\.compose-thread-post-header\s*\+\s*\.compose-body\s*\{[\s\S]*padding-top:\s*12px;/,
|
|
4482
|
+
);
|
|
4483
|
+
expect(css).toMatch(
|
|
4484
|
+
/@media \(max-width:\s*760px\),\s*\(hover:\s*none\) and \(pointer:\s*coarse\)\s*\{[\s\S]*\.compose-thread-layout\s*\{[\s\S]*padding-top:\s*0\.75rem;/,
|
|
4485
|
+
);
|
|
4486
|
+
expect(css).toMatch(
|
|
4487
|
+
/\.compose-dialog,[\s\S]*\.compose-page-shell\s*>\s*jant-compose-dialog\s*\{[\s\S]*--compose-quote-input-size:\s*var\(--type-content-subtitle\);[\s\S]*--compose-quote-input-leading:\s*1\.32;/,
|
|
4488
|
+
);
|
|
4489
|
+
// On the page the card's own padding is the text column, so every row
|
|
4490
|
+
// inside it reads a zero gutter and only restates its right.
|
|
4491
|
+
expect(css).toMatch(
|
|
4492
|
+
/\.compose-page-shell\s+\.compose-dialog-inner-page\s*\{[\s\S]*--compose-gutter:\s*0px;/,
|
|
4493
|
+
);
|
|
4494
|
+
expect(css).toMatch(
|
|
4495
|
+
/\.compose-page-shell\s+\.compose-dialog-inner-page\s+\.compose-thread-post-header\s*\{\s*padding-inline-end:\s*0;/,
|
|
4496
|
+
);
|
|
4497
|
+
expect(css).toMatch(
|
|
4498
|
+
/\.compose-quote-text\s*\{[\s\S]*font-size:\s*var\(--compose-quote-input-size\);[\s\S]*line-height:\s*var\(--compose-quote-input-leading\);/,
|
|
4499
|
+
);
|
|
4500
|
+
expect(css).toMatch(
|
|
4501
|
+
/\.compose-reply-compose-layout\s*\{[\s\S]*--compose-title-input-size:\s*calc\(var\(--type-content-body\) \* 1\.2\);[\s\S]*--compose-quote-input-size:\s*calc\(var\(--type-content-body\) \* 1\.06\);[\s\S]*--compose-quote-input-leading:\s*1\.42;[\s\S]*--compose-inline-input-size:\s*var\(--type-base\);/,
|
|
4502
|
+
);
|
|
4503
|
+
expect(css).toMatch(
|
|
4504
|
+
/\.compose-reply-compose-layout\s+\.compose-quote-wrap\s*\{[\s\S]*margin-top:\s*0\.4rem;[\s\S]*padding:\s*24px 18px 18px;[\s\S]*border-radius:\s*0\.95rem;/,
|
|
4505
|
+
);
|
|
4506
|
+
expect(css).toMatch(
|
|
4507
|
+
/\.compose-reply-compose-layout\s+\.compose-link-url-wrap\s*\{[\s\S]*margin-top:\s*0\.4rem;/,
|
|
4508
|
+
);
|
|
4509
|
+
// The reply layout tunes only the lead-in. Height is the shared four-line
|
|
4510
|
+
// floor, which reads this layout's own leading and type size, so a
|
|
4511
|
+
// `min-height` here would just restate what the base rule already knows.
|
|
4512
|
+
expect(css).toMatch(
|
|
4513
|
+
/\.compose-reply-compose-layout\s+\.compose-quote-text\s*\{\s*padding-top:\s*0\.35rem;\s*\}/,
|
|
4514
|
+
);
|
|
4515
|
+
expect(css).not.toMatch(
|
|
4516
|
+
/\.compose-reply-compose-layout\s+\.compose-quote-text\s*\{[^}]*min-height:/,
|
|
4517
|
+
);
|
|
4518
|
+
expect(css).toMatch(
|
|
4519
|
+
/\.compose-reply-compose-layout\s+\.compose-tiptap-body\s+\.tiptap\s*\{[\s\S]*font-size:\s*var\(--type-content-body\);/,
|
|
4520
|
+
);
|
|
3147
4521
|
});
|
|
3148
4522
|
|
|
3149
|
-
it("
|
|
3150
|
-
const
|
|
3151
|
-
el._loading = true;
|
|
3152
|
-
await el.updateComplete;
|
|
3153
|
-
|
|
3154
|
-
let dispatched = false;
|
|
3155
|
-
el.addEventListener("jant:compose-submit-deferred", () => {
|
|
3156
|
-
dispatched = true;
|
|
3157
|
-
});
|
|
4523
|
+
it("opens every compose text surface at a floor stated in lines", () => {
|
|
4524
|
+
const css = readFileSync(resolve("src/styles/ui.css"), "utf8");
|
|
3158
4525
|
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
4526
|
+
// Two lines for the editor, four for the quote — as multiples of the
|
|
4527
|
+
// leading, not pixels, so the floor follows the type instead of going
|
|
4528
|
+
// stale next to it.
|
|
4529
|
+
expect(css).toMatch(
|
|
4530
|
+
/\.compose-tiptap-body \.tiptap\s*\{[\s\S]*min-height:\s*calc\(var\(--type-body-leading\) \* 2em\);/,
|
|
4531
|
+
);
|
|
4532
|
+
expect(css).toMatch(
|
|
4533
|
+
/\.compose-quote-text\s*\{[\s\S]*min-height:\s*calc\(var\(--compose-quote-input-leading\) \* 4em\);/,
|
|
3162
4534
|
);
|
|
3163
|
-
postBtn.click();
|
|
3164
4535
|
|
|
3165
|
-
|
|
4536
|
+
// One floor for every editor: a note's body and the "thoughts" under a
|
|
4537
|
+
// link or a quote all open the same way.
|
|
4538
|
+
expect(css).not.toMatch(/\.compose-tiptap-thoughts/);
|
|
3166
4539
|
});
|
|
3167
4540
|
|
|
3168
|
-
it("
|
|
3169
|
-
const
|
|
3170
|
-
el._loading = true;
|
|
3171
|
-
await el.updateComplete;
|
|
4541
|
+
it("runs the thread rail from the first dot to the last post's, and no further", () => {
|
|
4542
|
+
const css = readFileSync(resolve("src/styles/ui.css"), "utf8");
|
|
3172
4543
|
|
|
3173
|
-
|
|
3174
|
-
|
|
4544
|
+
// Drawn per row rather than as one strip down the layout, which is what
|
|
4545
|
+
// lets both ends be trimmed back to a dot.
|
|
4546
|
+
expect(css).not.toMatch(/\.compose-thread-layout::before\s*\{/);
|
|
4547
|
+
expect(css).toMatch(
|
|
4548
|
+
/\.compose-thread-layout > \*::before\s*\{[\s\S]*top:\s*0;[\s\S]*bottom:\s*0;/,
|
|
4549
|
+
);
|
|
4550
|
+
expect(css).toMatch(
|
|
4551
|
+
/\.compose-thread-layout > :first-child::before\s*\{\s*top:\s*var\(--compose-thread-dot-center\);/,
|
|
4552
|
+
);
|
|
4553
|
+
expect(css).toMatch(
|
|
4554
|
+
/\.compose-editor-row:not\(:has\(~ \.compose-editor-row\)\)::before\s*\{\s*bottom:\s*auto;\s*height:\s*var\(--compose-thread-dot-center\);/,
|
|
4555
|
+
);
|
|
4556
|
+
// The "add" row is a placeholder, not a post — the rail stops before it.
|
|
4557
|
+
expect(css).toMatch(
|
|
4558
|
+
/\.compose-thread-layout > \.compose-thread-add-row::before\s*\{\s*display:\s*none;/,
|
|
4559
|
+
);
|
|
4560
|
+
|
|
4561
|
+
// The dot reads its offset off the same centre the rail is trimmed to, so
|
|
4562
|
+
// resizing the marker cannot leave the line pointing past it.
|
|
4563
|
+
expect(css).toMatch(
|
|
4564
|
+
/\.compose-thread-dot\s*\{[\s\S]*margin-top:\s*calc\(\s*var\(--compose-thread-dot-center\) -\s*var\(--compose-thread-row-padding-top\) -\s*var\(--site-thread-marker-size\) \/ 2\s*\);/,
|
|
4565
|
+
);
|
|
3175
4566
|
});
|
|
3176
4567
|
|
|
3177
|
-
it("
|
|
3178
|
-
const
|
|
4568
|
+
it("starts every compose row's ink on one text column", () => {
|
|
4569
|
+
const css = readFileSync(resolve("src/styles/ui.css"), "utf8");
|
|
3179
4570
|
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
4571
|
+
// One knob for the column itself, read by every row rather than restated
|
|
4572
|
+
// as a literal per row.
|
|
4573
|
+
expect(css).toMatch(
|
|
4574
|
+
/\.compose-dialog,\s*\.compose-page-shell\s*>\s*jant-compose-dialog\s*\{[\s\S]*--compose-gutter:\s*20px;/,
|
|
4575
|
+
);
|
|
4576
|
+
expect(css).toMatch(
|
|
4577
|
+
/@media \(min-width:\s*700px\)\s*\{[\s\S]*--compose-gutter:\s*24px;/,
|
|
4578
|
+
);
|
|
4579
|
+
// Zero wherever the indent already comes from outside the editor.
|
|
4580
|
+
expect(css).toMatch(
|
|
4581
|
+
/\.compose-editor-row\s*\{\s*\/\*[\s\S]*?\*\/\s*--compose-gutter:\s*0px;/,
|
|
4582
|
+
);
|
|
4583
|
+
|
|
4584
|
+
for (const rule of [
|
|
4585
|
+
/\.compose-body\s*\{[^}]*padding:\s*16px 20px 12px var\(--compose-gutter\);/,
|
|
4586
|
+
/\.compose-tools-row\s*\{[\s\S]*padding:\s*6px 10px 6px var\(--compose-gutter\);/,
|
|
4587
|
+
/\.compose-attachments-dock\s*\{\s*padding:\s*8px 20px 4px var\(--compose-gutter\);/,
|
|
4588
|
+
/\.compose-thread-post-header\s*\{[\s\S]*padding:\s*10px 16px 2px var\(--compose-gutter\);/,
|
|
4589
|
+
/\.compose-add-thread-trigger\s*\{\s*padding:\s*1px 10px 4px var\(--compose-gutter\);/,
|
|
4590
|
+
/\.compose-action-row\s*\{\s*padding:\s*10px 12px 14px var\(--compose-gutter\);/,
|
|
4591
|
+
]) {
|
|
4592
|
+
expect(css).toMatch(rule);
|
|
4593
|
+
}
|
|
3183
4594
|
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
4595
|
+
// Each control hangs its box back out of the column by its own optical
|
|
4596
|
+
// inset, so the glyph or the label lands on the edge and not the box.
|
|
4597
|
+
expect(css).toMatch(
|
|
4598
|
+
/\.compose-tools-row\s*\{[\s\S]*--compose-tool-hang:\s*12px;/,
|
|
4599
|
+
);
|
|
4600
|
+
expect(css).toMatch(
|
|
4601
|
+
/\.compose-tools-row > :first-child\s*\{\s*margin-inline-start:\s*calc\(-1 \* var\(--compose-tool-hang\)\);/,
|
|
4602
|
+
);
|
|
4603
|
+
expect(css).toMatch(
|
|
4604
|
+
/\.compose-thread-add-btn\s*\{[\s\S]*padding:\s*5px var\(--compose-add-hang\);\s*margin-inline-start:\s*calc\(-1 \* var\(--compose-add-hang\)\);/,
|
|
4605
|
+
);
|
|
4606
|
+
expect(css).toMatch(
|
|
4607
|
+
/\.compose-collection-select\s*\{[\s\S]*margin-inline-start:\s*calc\(\s*-1 \* \(var\(--compose-collection-pad-icon\) \+ 1px\)\s*\);/,
|
|
3189
4608
|
);
|
|
3190
4609
|
|
|
3191
|
-
//
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
4610
|
+
// The ad-hoc thread-only indents these replaced are gone — they were what
|
|
4611
|
+
// let each mode drift to its own edge in the first place.
|
|
4612
|
+
expect(css).not.toMatch(/margin-inline-start:\s*-10px;/);
|
|
4613
|
+
expect(css).not.toMatch(/\.compose-thread-layout \.compose-tools-row\s*\{/);
|
|
4614
|
+
// One "Add to thread" button, not two rule sets that have to stay in step.
|
|
4615
|
+
expect(css).not.toMatch(/\.compose-add-thread-btn/);
|
|
4616
|
+
});
|
|
3195
4617
|
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
clientId: "test-id-1",
|
|
3199
|
-
file,
|
|
3200
|
-
previewUrl,
|
|
3201
|
-
status: "done",
|
|
3202
|
-
progress: null,
|
|
3203
|
-
mediaId: "media-1",
|
|
3204
|
-
alt: "",
|
|
3205
|
-
error: null,
|
|
3206
|
-
posterUrl: null,
|
|
3207
|
-
summary: null,
|
|
3208
|
-
chars: null,
|
|
3209
|
-
},
|
|
3210
|
-
];
|
|
3211
|
-
editor._attachmentOrder = ["test-id-1"];
|
|
3212
|
-
await editor.updateComplete;
|
|
4618
|
+
it("declares one marker vocabulary for the compose and reading rails", () => {
|
|
4619
|
+
const css = readFileSync(resolve("src/styles/ui.css"), "utf8");
|
|
3213
4620
|
|
|
3214
|
-
//
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
// Media tool button should show inline "Add" label
|
|
3220
|
-
const mediaBtn =
|
|
3221
|
-
editor.querySelector<HTMLButtonElement>(".compose-tool-btn");
|
|
3222
|
-
expect(mediaBtn?.querySelector(".compose-tool-label")?.textContent).toBe(
|
|
3223
|
-
"Add",
|
|
4621
|
+
// Both rails draw the same dot; only the surface behind it differs.
|
|
4622
|
+
// `[^}]*` rather than `[\s\S]*` throughout: a greedy any-char run walks past
|
|
4623
|
+
// the rule's closing brace and happily matches a later rule's declaration.
|
|
4624
|
+
expect(css).toMatch(
|
|
4625
|
+
/\.thread-group,\s*\.compose-thread-layout\s*\{\s*--site-thread-marker-size:\s*7px;[^}]*--site-thread-marker-gap:\s*9px;/,
|
|
3224
4626
|
);
|
|
3225
|
-
|
|
3226
|
-
|
|
4627
|
+
expect(css).toMatch(
|
|
4628
|
+
/\.compose-thread-layout\s*\{\s*--site-thread-marker-surface:\s*var\(--compose-paper-bg\);\s*\}/,
|
|
4629
|
+
);
|
|
4630
|
+
// 7:1.5 marker-to-rail on both, from one token — compose used to hardcode
|
|
4631
|
+
// its own 1.5px while reading ran a 1px hairline under the same 7px dot.
|
|
4632
|
+
expect(css).toMatch(/--site-thread-rail-line-width:\s*1\.5px;/);
|
|
4633
|
+
expect(css).not.toMatch(
|
|
4634
|
+
/\.thread-group-preview,\s*\.thread-group-detail\s*\{[^}]*--site-thread-rail-line-width:/,
|
|
4635
|
+
);
|
|
4636
|
+
expect(css).toMatch(
|
|
4637
|
+
/\.compose-thread-layout > \*::before\s*\{[^}]*width:\s*var\(--site-thread-rail-line-width\);\s*margin-left:\s*calc\(var\(--site-thread-rail-line-width\) \/ -2\);/,
|
|
4638
|
+
);
|
|
4639
|
+
// The reading rail's fade-in gradient stays — it softens where collapsed
|
|
4640
|
+
// ancestor context begins, which compose solves by trimming per row.
|
|
4641
|
+
expect(css).toMatch(
|
|
4642
|
+
/\.thread-group-preview::before,\s*\.thread-group-detail::before\s*\{\s*background:\s*linear-gradient\(/,
|
|
4643
|
+
);
|
|
4644
|
+
// Both rails now stop at the last dot. Reading masks its group-wide line
|
|
4645
|
+
// from that dot down; `> ` keeps the mask off the last ancestor inside
|
|
4646
|
+
// `.thread-context-shell`, which the rail must run straight through.
|
|
4647
|
+
expect(css).toMatch(
|
|
4648
|
+
/\.thread-group > \.thread-item:last-child::after\s*\{[^}]*top:\s*50%;\s*bottom:\s*0;\s*width:\s*var\(--site-thread-rail-line-width\);\s*background-color:\s*var\(--site-thread-marker-surface\);/,
|
|
4649
|
+
);
|
|
4650
|
+
expect(css).not.toMatch(/\.thread-group \.thread-item:last-child::after/);
|
|
4651
|
+
// The ring is reading's alone: the token is declared on `.thread-group`, and
|
|
4652
|
+
// compose's dot rule never reads it. The compose rail is quiet by design —
|
|
4653
|
+
// it already has a card edge, a format switcher and a "1/3" per row.
|
|
4654
|
+
expect(css).toMatch(
|
|
4655
|
+
/\.thread-group\s*\{[^}]*--site-thread-marker-ring-inset:\s*2px;\s*--site-thread-marker-ring-width:\s*2px;/,
|
|
4656
|
+
);
|
|
4657
|
+
expect(css).not.toMatch(
|
|
4658
|
+
/\.compose-thread-layout\s*\{[^}]*--site-thread-marker-ring/,
|
|
4659
|
+
);
|
|
4660
|
+
expect(css).not.toMatch(
|
|
4661
|
+
/\.compose-thread-dot::before\s*\{[^}]*--site-thread-(dot-ring|marker-ring)/,
|
|
4662
|
+
);
|
|
4663
|
+
// The per-dot border and the size-based hero markers stay gone: a border
|
|
4664
|
+
// eats into the fill instead of sitting outside it, and a bigger dot
|
|
4665
|
+
// repeats what the full card beside it already says.
|
|
4666
|
+
for (const dead of [
|
|
4667
|
+
"--site-thread-marker-border-width",
|
|
4668
|
+
"--site-thread-marker-hero-size",
|
|
4669
|
+
"--site-thread-marker-hero-border-width",
|
|
4670
|
+
]) {
|
|
4671
|
+
expect(css).not.toContain(dead);
|
|
4672
|
+
}
|
|
4673
|
+
expect(css).not.toMatch(/\.compose-thread-dot(::before)?\s*\{[^}]*border:/);
|
|
3227
4674
|
});
|
|
3228
4675
|
|
|
3229
|
-
it("
|
|
4676
|
+
it("punches the dot's gap with the surface colour on both rails", () => {
|
|
3230
4677
|
const css = readFileSync(resolve("src/styles/ui.css"), "utf8");
|
|
3231
4678
|
|
|
4679
|
+
// Compose stops at the punch-out; reading layers its ring inside the same
|
|
4680
|
+
// radius, so the outermost layer — the one that breaks the rail line — is
|
|
4681
|
+
// still the gap at its full size on both rails.
|
|
4682
|
+
const punchOut =
|
|
4683
|
+
/0 0 0 var\(--site-thread-marker-gap\)\s*var\(--site-thread-marker-surface\);/;
|
|
3232
4684
|
expect(css).toMatch(
|
|
3233
|
-
|
|
4685
|
+
new RegExp(
|
|
4686
|
+
`\\.compose-thread-dot::before\\s*\\{[^}]*box-shadow:\\s*${punchOut.source}`,
|
|
4687
|
+
),
|
|
3234
4688
|
);
|
|
3235
|
-
expect(css).toMatch(/\.compose-tools-row\s*\{[\s\S]*flex-shrink:\s*0;/);
|
|
3236
4689
|
expect(css).toMatch(
|
|
3237
|
-
|
|
4690
|
+
new RegExp(`\\.thread-item::before\\s*\\{[^}]*${punchOut.source}`),
|
|
3238
4691
|
);
|
|
4692
|
+
// Reading's ring: a band of surface, then the accent tint, both drawn as
|
|
4693
|
+
// spread inside the punch. A border would eat the fill instead.
|
|
3239
4694
|
expect(css).toMatch(
|
|
3240
|
-
/\.
|
|
4695
|
+
/\.thread-item::before\s*\{[^}]*box-shadow:\s*0 0 0 var\(--site-thread-marker-ring-inset\)\s*var\(--site-thread-marker-surface\),\s*0 0 0\s*calc\(\s*var\(--site-thread-marker-ring-inset\) \+\s*var\(--site-thread-marker-ring-width\)\s*\)\s*var\(--site-thread-dot-ring\),/,
|
|
3241
4696
|
);
|
|
4697
|
+
expect(css).not.toMatch(/\.thread-item::before\s*\{[^}]*border:\s/);
|
|
4698
|
+
});
|
|
4699
|
+
|
|
4700
|
+
it("marks the post a view is about with an accent fill, not a bigger dot", () => {
|
|
4701
|
+
const css = readFileSync(resolve("src/styles/ui.css"), "utf8");
|
|
4702
|
+
|
|
4703
|
+
// Feed hero, curated highlight and the opened detail post share one rule,
|
|
4704
|
+
// matching compose's `.is-current`. Size is not a second signal.
|
|
3242
4705
|
expect(css).toMatch(
|
|
3243
|
-
/\.
|
|
4706
|
+
/\.thread-item-hero::before,\s*\.thread-item-curated::before,\s*\.thread-item-current::before\s*\{\s*background-color:\s*var\(--site-thread-marker-current\);\s*\}/,
|
|
3244
4707
|
);
|
|
4708
|
+
// Both rails take the current-post colour from one token, and that token is
|
|
4709
|
+
// a tint — the raw accent made a 7px dot the loudest thing on the page.
|
|
3245
4710
|
expect(css).toMatch(
|
|
3246
|
-
/\.compose-
|
|
4711
|
+
/\.compose-editor-row\.is-current \.compose-thread-dot::before\s*\{\s*background-color:\s*var\(--site-thread-marker-current\);/,
|
|
3247
4712
|
);
|
|
3248
4713
|
expect(css).toMatch(
|
|
3249
|
-
|
|
4714
|
+
/--site-thread-marker-current:\s*color-mix\(\s*in srgb,\s*var\(--site-accent\) 90%,\s*var\(--site-threadline\)\s*\);/,
|
|
3250
4715
|
);
|
|
3251
|
-
expect(css).toMatch(
|
|
3252
|
-
/\.
|
|
4716
|
+
expect(css).not.toMatch(
|
|
4717
|
+
/\.thread-item-current::before\s*\{[^}]*var\(--site-accent\)[^-]/,
|
|
3253
4718
|
);
|
|
4719
|
+
// Every dot's geometry now derives from the shared token.
|
|
3254
4720
|
expect(css).toMatch(
|
|
3255
|
-
/\.
|
|
4721
|
+
/\.thread-item::before\s*\{[^}]*width:\s*var\(--site-thread-marker-size\);/,
|
|
3256
4722
|
);
|
|
4723
|
+
expect(css).not.toMatch(/\.thread-item::before\s*\{[^}]*width:\s*\d+px;/);
|
|
4724
|
+
});
|
|
4725
|
+
|
|
4726
|
+
it("centres every rail child on the line by giving them a shared track", () => {
|
|
4727
|
+
const css = readFileSync(resolve("src/styles/ui.css"), "utf8");
|
|
4728
|
+
|
|
4729
|
+
// The line's x is derived from the track, never from a dot's own size — a
|
|
4730
|
+
// hardcoded half-marker is what put the line down the dot's edge before.
|
|
3257
4731
|
expect(css).toMatch(
|
|
3258
|
-
|
|
4732
|
+
/--compose-thread-rail-left:\s*calc\(\s*var\(--compose-thread-padding-left\) \+ var\(--compose-thread-rail-width\) \/ 2\s*\);/,
|
|
3259
4733
|
);
|
|
4734
|
+
expect(css).not.toMatch(/--compose-thread-rail-left:[^;]*\+ \d/);
|
|
4735
|
+
|
|
4736
|
+
// Post dot: fills the track, centres the circle in it.
|
|
3260
4737
|
expect(css).toMatch(
|
|
3261
|
-
/\.compose-
|
|
4738
|
+
/\.compose-thread-dot\s*\{[\s\S]*justify-content:\s*center;[\s\S]*width:\s*var\(--compose-thread-rail-width\);/,
|
|
3262
4739
|
);
|
|
4740
|
+
// "Add to thread" placeholder: fills the track outright, so no offset.
|
|
3263
4741
|
expect(css).toMatch(
|
|
3264
|
-
/\.compose-
|
|
4742
|
+
/\.compose-thread-add-dot\s*\{[\s\S]*width:\s*var\(--compose-thread-rail-width\);[\s\S]*height:\s*var\(--compose-thread-rail-width\);/,
|
|
3265
4743
|
);
|
|
4744
|
+
expect(css).not.toMatch(/\.compose-thread-add-dot\s*\{[^}]*margin-left:/);
|
|
4745
|
+
// Reply meta hangs off the same track as the rows above it.
|
|
3266
4746
|
expect(css).toMatch(
|
|
3267
|
-
/\.compose-reply-
|
|
4747
|
+
/\.compose-reply-meta\s*\{[\s\S]*var\(--compose-thread-gap\) \+\s*var\(--compose-thread-rail-width\)/,
|
|
3268
4748
|
);
|
|
4749
|
+
});
|
|
4750
|
+
|
|
4751
|
+
it("accents the dot of the post holding the cursor, not every editor row", () => {
|
|
4752
|
+
const css = readFileSync(resolve("src/styles/ui.css"), "utf8");
|
|
4753
|
+
|
|
4754
|
+
// Which selector carries the highlight — the colour itself is pinned by
|
|
4755
|
+
// "marks the post a view is about…", so this stays token-agnostic.
|
|
3269
4756
|
expect(css).toMatch(
|
|
3270
|
-
/\.compose-
|
|
4757
|
+
/\.compose-editor-row\.is-current \.compose-thread-dot::before\s*\{\s*background-color:/,
|
|
4758
|
+
);
|
|
4759
|
+
// The old rule lit every post in a thread at once.
|
|
4760
|
+
expect(css).not.toMatch(
|
|
4761
|
+
/\.compose-editor-row \.compose-thread-dot\s*\{\s*background-color:/,
|
|
4762
|
+
);
|
|
4763
|
+
});
|
|
4764
|
+
|
|
4765
|
+
it("moves the current-post marker as focus moves between thread posts", async () => {
|
|
4766
|
+
const el = await createElement();
|
|
4767
|
+
el._threadItems = [
|
|
4768
|
+
{ id: "thread-1", format: "note" },
|
|
4769
|
+
{ id: "thread-2", format: "note" },
|
|
4770
|
+
{ id: "thread-3", format: "note" },
|
|
4771
|
+
];
|
|
4772
|
+
await flushUpdates(el);
|
|
4773
|
+
|
|
4774
|
+
const currentIndexes = () =>
|
|
4775
|
+
Array.from(el.querySelectorAll<HTMLElement>(".compose-thread-post"))
|
|
4776
|
+
.map((row, index) =>
|
|
4777
|
+
row.classList.contains("is-current") ? index : -1,
|
|
4778
|
+
)
|
|
4779
|
+
.filter((index) => index >= 0);
|
|
4780
|
+
|
|
4781
|
+
// Exactly one at a time — the old rule accented all three.
|
|
4782
|
+
expect(currentIndexes()).toEqual([0]);
|
|
4783
|
+
|
|
4784
|
+
const second = requireElement(
|
|
4785
|
+
el.querySelector<HTMLElement>(
|
|
4786
|
+
'.compose-thread-post[data-thread-index="1"]',
|
|
4787
|
+
),
|
|
4788
|
+
"expected the second thread post",
|
|
3271
4789
|
);
|
|
4790
|
+
second.dispatchEvent(new Event("focusin", { bubbles: true }));
|
|
4791
|
+
await flushUpdates(el);
|
|
4792
|
+
|
|
4793
|
+
expect(currentIndexes()).toEqual([1]);
|
|
3272
4794
|
});
|
|
3273
4795
|
|
|
3274
4796
|
it("keeps passive footnote references quiet until the editor selects them", () => {
|
|
@@ -3304,6 +4826,7 @@ describe("JantComposeDialog", () => {
|
|
|
3304
4826
|
alt: "",
|
|
3305
4827
|
error: null,
|
|
3306
4828
|
posterUrl: null,
|
|
4829
|
+
remoteUrl: null,
|
|
3307
4830
|
summary: null,
|
|
3308
4831
|
chars: null,
|
|
3309
4832
|
},
|
|
@@ -3346,6 +4869,7 @@ describe("JantComposeDialog", () => {
|
|
|
3346
4869
|
alt: "",
|
|
3347
4870
|
error: null,
|
|
3348
4871
|
posterUrl: null,
|
|
4872
|
+
remoteUrl: null,
|
|
3349
4873
|
summary: null,
|
|
3350
4874
|
chars: null,
|
|
3351
4875
|
},
|
|
@@ -3401,6 +4925,7 @@ describe("JantComposeDialog", () => {
|
|
|
3401
4925
|
alt: "A test image",
|
|
3402
4926
|
error: null,
|
|
3403
4927
|
posterUrl: null,
|
|
4928
|
+
remoteUrl: null,
|
|
3404
4929
|
summary: null,
|
|
3405
4930
|
chars: null,
|
|
3406
4931
|
},
|
|
@@ -3471,6 +4996,7 @@ describe("JantComposeDialog", () => {
|
|
|
3471
4996
|
alt: "Alt for pending",
|
|
3472
4997
|
error: null,
|
|
3473
4998
|
posterUrl: null,
|
|
4999
|
+
remoteUrl: null,
|
|
3474
5000
|
summary: null,
|
|
3475
5001
|
chars: null,
|
|
3476
5002
|
},
|
|
@@ -3546,7 +5072,97 @@ describe("JantComposeDialog", () => {
|
|
|
3546
5072
|
);
|
|
3547
5073
|
});
|
|
3548
5074
|
|
|
3549
|
-
// ──
|
|
5075
|
+
// ── Leaving compose ────────────────────────────────────────────────
|
|
5076
|
+
|
|
5077
|
+
it("names the dialog for assistive tech now that no title is drawn", async () => {
|
|
5078
|
+
// The visible title carried the dialog's accessible name; it moves to an
|
|
5079
|
+
// attribute rather than disappearing with the row.
|
|
5080
|
+
const dialog = document.createElement("dialog");
|
|
5081
|
+
document.body.appendChild(dialog);
|
|
5082
|
+
const el = document.createElement(
|
|
5083
|
+
"jant-compose-dialog",
|
|
5084
|
+
) as JantComposeDialog;
|
|
5085
|
+
el.collections = collections;
|
|
5086
|
+
el.labels = labels;
|
|
5087
|
+
dialog.appendChild(el);
|
|
5088
|
+
await flushUpdates(el);
|
|
5089
|
+
|
|
5090
|
+
expect(dialog.getAttribute("aria-label")).toBe(labels.composeDialogLabel);
|
|
5091
|
+
|
|
5092
|
+
// Tear the dialog down here: happy-dom throws when `body.innerHTML = ""`
|
|
5093
|
+
// in the shared beforeEach removes one it did not itself create.
|
|
5094
|
+
el.remove();
|
|
5095
|
+
dialog.remove();
|
|
5096
|
+
});
|
|
5097
|
+
|
|
5098
|
+
it("closes from the × in the post header row", async () => {
|
|
5099
|
+
const el = await createElement();
|
|
5100
|
+
await flushUpdates(el);
|
|
5101
|
+
const requestCloseSpy = vi.spyOn(el, "requestClose");
|
|
5102
|
+
|
|
5103
|
+
const closeBtn = requireElement(
|
|
5104
|
+
el.querySelector<HTMLButtonElement>(
|
|
5105
|
+
".compose-thread-post-header .compose-close-btn",
|
|
5106
|
+
),
|
|
5107
|
+
"expected close button in the post header",
|
|
5108
|
+
);
|
|
5109
|
+
expect(closeBtn.getAttribute("aria-label")).toBe(labels.cancel);
|
|
5110
|
+
|
|
5111
|
+
closeBtn.click();
|
|
5112
|
+
await flushUpdates(el);
|
|
5113
|
+
|
|
5114
|
+
expect(requestCloseSpy).toHaveBeenCalledTimes(1);
|
|
5115
|
+
});
|
|
5116
|
+
|
|
5117
|
+
it("hands the header slot back to per-post remove in a thread, and moves the exit into the options panel", async () => {
|
|
5118
|
+
const el = await createElement();
|
|
5119
|
+
el._threadItems = [
|
|
5120
|
+
{ id: "thread-1", format: "note" },
|
|
5121
|
+
{ id: "thread-2", format: "note" },
|
|
5122
|
+
];
|
|
5123
|
+
await flushUpdates(el);
|
|
5124
|
+
|
|
5125
|
+
// Each post owns its × for removing itself, so no close button competes.
|
|
5126
|
+
expect(el.querySelector(".compose-close-btn")).toBeNull();
|
|
5127
|
+
expect(
|
|
5128
|
+
el.querySelectorAll(".compose-thread-post-remove").length,
|
|
5129
|
+
).toBeGreaterThan(0);
|
|
5130
|
+
|
|
5131
|
+
await openPublishPanel(el);
|
|
5132
|
+
const closeRow = requireElement(
|
|
5133
|
+
Array.from(
|
|
5134
|
+
el.querySelectorAll<HTMLButtonElement>(
|
|
5135
|
+
".compose-publish-panel .compose-sheet-row",
|
|
5136
|
+
),
|
|
5137
|
+
).find(
|
|
5138
|
+
(row) =>
|
|
5139
|
+
row.querySelector(".compose-sheet-title")?.textContent?.trim() ===
|
|
5140
|
+
labels.closeCompose,
|
|
5141
|
+
) ?? null,
|
|
5142
|
+
"expected a close row in the options panel",
|
|
5143
|
+
);
|
|
5144
|
+
|
|
5145
|
+
const requestCloseSpy = vi.spyOn(el, "requestClose");
|
|
5146
|
+
closeRow.click();
|
|
5147
|
+
await flushUpdates(el);
|
|
5148
|
+
|
|
5149
|
+
expect(el._showPublishPanel).toBe(false);
|
|
5150
|
+
expect(requestCloseSpy).toHaveBeenCalledTimes(1);
|
|
5151
|
+
});
|
|
5152
|
+
|
|
5153
|
+
it("keeps the exit out of the options panel when the × already offers one", async () => {
|
|
5154
|
+
const el = await createElement();
|
|
5155
|
+
await openPublishPanel(el);
|
|
5156
|
+
|
|
5157
|
+
const titles = Array.from(
|
|
5158
|
+
el.querySelectorAll<HTMLElement>(
|
|
5159
|
+
".compose-publish-panel .compose-sheet-title",
|
|
5160
|
+
),
|
|
5161
|
+
).map((n) => n.textContent?.trim());
|
|
5162
|
+
|
|
5163
|
+
expect(titles).not.toContain(labels.closeCompose);
|
|
5164
|
+
expect(titles).toContain("Drafts");
|
|
5165
|
+
});
|
|
3550
5166
|
|
|
3551
5167
|
it("requestClose on empty form closes immediately without confirmation", async () => {
|
|
3552
5168
|
const el = await createElement();
|
|
@@ -5008,9 +6624,12 @@ describe("JantComposeDialog", () => {
|
|
|
5008
6624
|
el._format = "note";
|
|
5009
6625
|
await el.updateComplete;
|
|
5010
6626
|
|
|
5011
|
-
// Format switcher should be visible
|
|
6627
|
+
// Format switcher should be visible, and the submit label says this is a
|
|
6628
|
+
// new post rather than an edit.
|
|
5012
6629
|
expect(el.querySelector(".compose-segmented")).not.toBeNull();
|
|
5013
|
-
expect(el.querySelector(".compose-
|
|
6630
|
+
expect(el.querySelector(".compose-publish-main")?.textContent?.trim()).toBe(
|
|
6631
|
+
"Post",
|
|
6632
|
+
);
|
|
5014
6633
|
|
|
5015
6634
|
// Button should say "Post", not "Done"
|
|
5016
6635
|
const postBtn = requireElement(
|
|
@@ -5095,11 +6714,94 @@ describe("JantComposeDialog", () => {
|
|
|
5095
6714
|
expect((receivedDetail as unknown as ComposeSubmitDetail).editPostId).toBe(
|
|
5096
6715
|
"draft789",
|
|
5097
6716
|
);
|
|
6717
|
+
expect(
|
|
6718
|
+
(receivedDetail as unknown as ComposeSubmitDetail).draftSourceId,
|
|
6719
|
+
).toBe("draft789");
|
|
5098
6720
|
expect((receivedDetail as unknown as ComposeSubmitDetail).status).toBe(
|
|
5099
6721
|
"published",
|
|
5100
6722
|
);
|
|
5101
6723
|
});
|
|
5102
6724
|
|
|
6725
|
+
it("opens a thread draft without deleting it and preserves its draft identity", async () => {
|
|
6726
|
+
const rootId = "pst_draft_root";
|
|
6727
|
+
const replyId = "pst_draft_reply";
|
|
6728
|
+
const requests: Array<{ url: string; method: string }> = [];
|
|
6729
|
+
vi.spyOn(globalThis, "fetch").mockImplementation(async (input, init) => {
|
|
6730
|
+
const url = String(input);
|
|
6731
|
+
requests.push({ url, method: init?.method ?? "GET" });
|
|
6732
|
+
|
|
6733
|
+
if (url === `/api/posts/${rootId}`) {
|
|
6734
|
+
return new Response(
|
|
6735
|
+
JSON.stringify({
|
|
6736
|
+
id: rootId,
|
|
6737
|
+
threadId: rootId,
|
|
6738
|
+
format: "note",
|
|
6739
|
+
status: "draft",
|
|
6740
|
+
slug: "draft-thread",
|
|
6741
|
+
title: "Draft thread",
|
|
6742
|
+
body: null,
|
|
6743
|
+
attachments: [],
|
|
6744
|
+
collectionIds: [],
|
|
6745
|
+
}),
|
|
6746
|
+
{ headers: { "Content-Type": "application/json" } },
|
|
6747
|
+
);
|
|
6748
|
+
}
|
|
6749
|
+
|
|
6750
|
+
if (url === "/api/posts?status=draft&limit=50") {
|
|
6751
|
+
return new Response(
|
|
6752
|
+
JSON.stringify({
|
|
6753
|
+
posts: [
|
|
6754
|
+
{
|
|
6755
|
+
id: replyId,
|
|
6756
|
+
threadId: rootId,
|
|
6757
|
+
replyToId: rootId,
|
|
6758
|
+
format: "quote",
|
|
6759
|
+
status: "draft",
|
|
6760
|
+
quoteText: "Draft reply",
|
|
6761
|
+
attachments: [],
|
|
6762
|
+
},
|
|
6763
|
+
],
|
|
6764
|
+
}),
|
|
6765
|
+
{ headers: { "Content-Type": "application/json" } },
|
|
6766
|
+
);
|
|
6767
|
+
}
|
|
6768
|
+
|
|
6769
|
+
throw new Error(`Unexpected fetch: ${url}`);
|
|
6770
|
+
});
|
|
6771
|
+
|
|
6772
|
+
const el = await createElement();
|
|
6773
|
+
await el.openDraft(rootId);
|
|
6774
|
+
await flushUpdates(el);
|
|
6775
|
+
|
|
6776
|
+
expect(el._draftSourceId).toBe(rootId);
|
|
6777
|
+
expect(el.querySelectorAll("jant-compose-editor")).toHaveLength(2);
|
|
6778
|
+
expect(requests).toEqual([
|
|
6779
|
+
{ url: `/api/posts/${rootId}`, method: "GET" },
|
|
6780
|
+
{ url: "/api/posts?status=draft&limit=50", method: "GET" },
|
|
6781
|
+
]);
|
|
6782
|
+
|
|
6783
|
+
let receivedDetail: ComposeSubmitDetail | null = null;
|
|
6784
|
+
el.addEventListener("jant:compose-submit-deferred", (event) => {
|
|
6785
|
+
receivedDetail = (event as CustomEvent<ComposeSubmitDetail>).detail;
|
|
6786
|
+
});
|
|
6787
|
+
requireElement(
|
|
6788
|
+
el.querySelector<HTMLButtonElement>(".compose-publish-main"),
|
|
6789
|
+
"expected publish button",
|
|
6790
|
+
).click();
|
|
6791
|
+
|
|
6792
|
+
expect(receivedDetail).not.toBeNull();
|
|
6793
|
+
expect(
|
|
6794
|
+
(receivedDetail as unknown as ComposeSubmitDetail).draftSourceId,
|
|
6795
|
+
).toBe(rootId);
|
|
6796
|
+
expect((receivedDetail as unknown as ComposeSubmitDetail).editPostId).toBe(
|
|
6797
|
+
rootId,
|
|
6798
|
+
);
|
|
6799
|
+
expect(
|
|
6800
|
+
(receivedDetail as unknown as ComposeSubmitDetail).threadPosts,
|
|
6801
|
+
).toHaveLength(2);
|
|
6802
|
+
expect(requests.every((request) => request.method === "GET")).toBe(true);
|
|
6803
|
+
});
|
|
6804
|
+
|
|
5103
6805
|
it("draft button confirm save dispatches draft then opens drafts panel", async () => {
|
|
5104
6806
|
const el = await createElement();
|
|
5105
6807
|
const editor = requireElement(
|
|
@@ -5129,13 +6831,10 @@ describe("JantComposeDialog", () => {
|
|
|
5129
6831
|
receivedDetail = (event as CustomEvent<ComposeSubmitDetail>).detail;
|
|
5130
6832
|
});
|
|
5131
6833
|
|
|
5132
|
-
// Click
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
);
|
|
5137
|
-
draftBtn.click();
|
|
5138
|
-
await el.updateComplete;
|
|
6834
|
+
// Click the drafts row in the options panel → confirm panel
|
|
6835
|
+
await openPublishPanel(el);
|
|
6836
|
+
requireElement(draftsRow(el), "expected drafts row").click();
|
|
6837
|
+
await flushUpdates(el);
|
|
5139
6838
|
expect(el._confirmPanelOpen).toBe(true);
|
|
5140
6839
|
|
|
5141
6840
|
// Click "Save"
|
|
@@ -5200,13 +6899,10 @@ describe("JantComposeDialog", () => {
|
|
|
5200
6899
|
submitFired = true;
|
|
5201
6900
|
});
|
|
5202
6901
|
|
|
5203
|
-
// Click
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
);
|
|
5208
|
-
draftBtn.click();
|
|
5209
|
-
await el.updateComplete;
|
|
6902
|
+
// Click the drafts row in the options panel → confirm panel
|
|
6903
|
+
await openPublishPanel(el);
|
|
6904
|
+
requireElement(draftsRow(el), "expected drafts row").click();
|
|
6905
|
+
await flushUpdates(el);
|
|
5210
6906
|
|
|
5211
6907
|
// Click "Don't save"
|
|
5212
6908
|
requireElement(
|
|
@@ -5248,6 +6944,7 @@ describe("JantComposeDialog", () => {
|
|
|
5248
6944
|
alt: "",
|
|
5249
6945
|
error: null,
|
|
5250
6946
|
posterUrl: null,
|
|
6947
|
+
remoteUrl: null,
|
|
5251
6948
|
summary: null,
|
|
5252
6949
|
chars: null,
|
|
5253
6950
|
},
|