diff --git a/web/app/components/AppSidebar.nuxt.test.ts b/web/app/components/AppSidebar.nuxt.test.ts index 1c660c1d..b1f2bd2f 100644 --- a/web/app/components/AppSidebar.nuxt.test.ts +++ b/web/app/components/AppSidebar.nuxt.test.ts @@ -1034,8 +1034,7 @@ describe("", { concurrent: false }, () => { }) // the suite runs as a non-mac host, so the row prints the keys of the - // "other" variant, one each and without the "then" connector the - // tooltips put between them + // "other" variant, one each; a chord carries no connector word it("spells the shortcut out on the shortcuts row", async ({ expect }) => { stubQueries() const wrapper = await mountSidebar() diff --git a/web/app/components/AppSidebar.vue b/web/app/components/AppSidebar.vue index 6cf011a3..ee678096 100644 --- a/web/app/components/AppSidebar.vue +++ b/web/app/components/AppSidebar.vue @@ -578,6 +578,12 @@ async function installSlack() { {{ val.key }} + + {{ $t("shortcuts.connector") }} + diff --git a/web/app/components/ShortcutModal.nuxt.test.ts b/web/app/components/ShortcutModal.nuxt.test.ts index aeb8e40f..93ecd50f 100644 --- a/web/app/components/ShortcutModal.nuxt.test.ts +++ b/web/app/components/ShortcutModal.nuxt.test.ts @@ -1,5 +1,6 @@ import { mountSuspended } from "@nuxt/test-utils/runtime" import { beforeEach, describe, it } from "vitest" +import { allItems } from "./editor/slash/items" import ShortcutModal from "./ShortcutModal.vue" import { clearTeleportedOverlays, t, teleportedButton } from "./test-helpers" import { SHORTCUT_GROUPS } from "~/utils/shortcuts" @@ -12,6 +13,31 @@ function dialog() { return document.body.querySelector("[data-slot='dialog-content']") } +function row(id: string): Element { + const found = document.body.querySelector(`[data-shortcut-row="${id}"]`) + if (!found) { + throw new Error(`no shortcut row "${id}"`) + } + + return found +} + +// the keys a row prints, one each; a sequence connector would be a +// plain span and is left out +function rowKeys(id: string): string[] { + return Array.from( + row(id).querySelectorAll("kbd[data-slot='kbd-group'] kbd"), + ).map((key) => key.textContent.trim()) +} + +function rowDescription(id: string): string { + return ( + row(id).querySelector(".text-muted-foreground")?.textContent.trim() ?? "" + ) +} + +const entryId = (key: string) => `shortcuts.modal.entries.${key}.label` + // the dialog body is teleported into the shared describe("", { concurrent: false }, () => { beforeEach(() => { @@ -30,63 +56,95 @@ describe("", { concurrent: false }, () => { expect(dialog()?.textContent).toContain(t("shortcuts.modal.title")) }) - it("heads a section for every group, in order", async ({ expect }) => { + it("heads a section for every group, then the slash commands", async ({ + expect, + }) => { await mountModal() const headings = Array.from( document.body.querySelectorAll("[data-slot='dialog-content'] h3"), ).map((heading) => heading.textContent) - expect(headings).toEqual(SHORTCUT_GROUPS.map((group) => t(group.i18nKey))) + expect(headings).toEqual([ + ...SHORTCUT_GROUPS.map((group) => t(group.i18nKey)), + t("shortcuts.groups.slash-commands"), + ]) }) - it("names every grouped shortcut and prints its keys", async ({ expect }) => { + it("names and describes every grouped shortcut", async ({ expect }) => { await mountModal() - const text = dialog()?.textContent ?? "" - SHORTCUT_GROUPS.flatMap((group) => group.shortcuts).forEach((entry) => { - expect(text).toContain(t(entry.i18nKey)) - expect(text).toContain(t(entry.descriptionI18nKey)) + expect(row(entry.i18nKey).textContent).toContain(t(entry.i18nKey)) + expect(rowDescription(entry.i18nKey)).toBe(t(entry.descriptionI18nKey)) }) - - // the suite runs as a non-mac host. Only the keys themselves are - // ; the connector between them is a plain span - const keys = Array.from( - document.body.querySelectorAll( - "[data-slot='dialog-content'] kbd[data-slot='kbd-group'] kbd", - ), - ).map((key) => key.textContent) - - expect(keys).toEqual( - SHORTCUT_GROUPS.flatMap((group) => group.shortcuts).flatMap((entry) => - entry.action.keyboardKey.other.split("+"), - ), - ) }) - it("separates the keys of a shortcut with the connector", async ({ + // the suite runs as a non-mac host, and a chord's keys sit side by side + it("prints an app shortcut and an editor binding as chords", async ({ expect, }) => { await mountModal() - const rendered = Array.from( - document.body.querySelectorAll( - "[data-slot='dialog-content'] kbd[data-slot='kbd-group']", - ), - ).map((group) => - Array.from(group.children).map((child) => child.textContent.trim()), + expect(rowKeys(entryId("toggle-inbox"))).toEqual(["Ctrl", "Shift", "\\"]) + expect(rowKeys(entryId("bold"))).toEqual(["Ctrl", "B"]) + expect(rowKeys(entryId("outdent-list-item"))).toEqual(["Shift", "Tab"]) + }) + + it("prints typed markdown as written", async ({ expect }) => { + await mountModal() + + expect(rowKeys(entryId("md-bold"))).toEqual(["**text**"]) + expect(rowKeys(entryId("md-quote"))).toEqual([ + ">", + t("shortcuts.modal.space-key"), + ]) + }) + + it("lists the markdown of every block the slash menu inserts", async ({ + expect, + }) => { + await mountModal() + + const withMarkdown = allItems.filter((item) => item.shortcut) + + expect(withMarkdown.length).toBeGreaterThan(0) + withMarkdown.forEach((item) => { + const id = `markdown-${item.titleI18nKey}` + const pattern = item.shortcut ?? "" + const keys = pattern.endsWith(" ") + ? [pattern.trimEnd(), t("shortcuts.modal.space-key")] + : [pattern] + + expect(row(id).textContent).toContain(t(item.titleI18nKey)) + expect(rowKeys(id)).toEqual(keys) + expect(rowDescription(id)).toBe( + `${t(item.descriptionI18nKey)} ${t("shortcuts.modal.markdown-block-hint")}`, + ) + }) + }) + + it("lists every slash command by the text that opens it", async ({ + expect, + }) => { + await mountModal() + + const section = document.body.querySelector( + "[data-shortcut-section='slash-commands']", ) - expect(rendered).toEqual( - SHORTCUT_GROUPS.flatMap((group) => group.shortcuts).map((entry) => - entry.action.keyboardKey.other - .split("+") - .flatMap((key, index) => - index === 0 ? [key] : [t("shortcuts.connector"), key], - ), - ), + expect(section?.querySelectorAll("[data-shortcut-row]")).toHaveLength( + allItems.length, ) + allItems.forEach((item) => { + const id = `slash-${item.titleI18nKey}` + + expect(row(id).textContent).toContain(t(item.titleI18nKey)) + expect(rowKeys(id)).toEqual([`/${t(item.titleI18nKey).toLowerCase()}`]) + expect(rowDescription(id)).toBe( + `${t(item.descriptionI18nKey)} ${t("shortcuts.modal.slash-hint")}`, + ) + }) }) it("closes when the close button is pressed", async ({ expect }) => { diff --git a/web/app/components/ShortcutModal.vue b/web/app/components/ShortcutModal.vue index e52cc922..47b5d148 100644 --- a/web/app/components/ShortcutModal.vue +++ b/web/app/components/ShortcutModal.vue @@ -1,12 +1,105 @@