diff --git a/apps/simplex-calculator-bot/README.md b/apps/simplex-calculator-bot/README.md index 3d97c1eb53..b7110bffa8 100644 --- a/apps/simplex-calculator-bot/README.md +++ b/apps/simplex-calculator-bot/README.md @@ -1,48 +1,29 @@ # SimpleX Calculator -A pocket calculator in a chat, built with the [SimpleX Chat Node.js library](../../packages/simplex-chat-nodejs/). +A calculator bot built with the [SimpleX Chat Node.js library](../../packages/simplex-chat-nodejs/). -Each user who connects via the bot's business address gets their own chat with a calculator message. The keys are bot commands: tapping a key sends it to the bot, and the bot replaces the calculator message with the updated one. +Each user who connects via the bot's business address receives a calculator message with keys as commands. The bot replaces this message after each input. -## How it works - -- Keys are applied left to right, without operator priority: `2 + 3 × 4 =` gives 20. -- `%` follows Apple's calculator: `100 + 15 %` shows 15, and `=` gives 115; `50 × 10 %` shows 0.1, and `=` gives 5. -- `C` clears the current number; the second `C` clears the whole calculation. -- The display shows up to 15 digits, without exponent; larger results show `Error`. -- After `=`, the bot sends the calculation as a message, e.g. `2 + 2 = 4`. -- Tapped keys are deleted from the chat; the calculation messages stay as a log. -- Users can also send messages instead of tapping keys: - - a number or an expression, e.g. `12 × 3 + 4`, is computed and entered as the current number, so `25`, `/add`, `25`, `=` gives 50. - - a key, e.g. `+`, `=`, `x`, `*`, `/`, `add` or `c`, works as the tapped key. -- The calculator turns off after 10 minutes without use: the number is removed from the display, the keys stay. Any key, number or `/calc` turns it on. -- Apps that support commands made of symbols (chat protocol version 21) get keys like `/+`; older apps get keys like `/add`. - -## Install & build - -```bash -cd apps/simplex-calculator-bot -npm install -npm run build -``` - -The bot requires a `simplex-chat` library version that passes the chat API to event handlers (7.1.0-beta.4.1 or later). To run against the in-tree library: - -```bash -# In packages/simplex-chat-nodejs -npm link - -# In apps/simplex-calculator-bot -npm link simplex-chat -``` +- Keys are applied left to right, so `2 + 3 × 4 =` equals 20. +- `%` is computed as in Apple's calculator: `100 + 15 % =` equals 115. +- Tap `C` to clear the number, and `C` again to clear the calculation. +- Numbers are shown without exponent, up to 15 digits; larger results are shown as `Error`. +- You can also send numbers, expressions like `12 × 3 + 4`, which are entered as one number, and keys like `+`, `=`, `add` or `c`. +- The bot deletes tapped keys and sends each calculation after `=`. +- After 10 minutes without input, the bot turns the calculator off and discards the calculation; tap any key to turn it on. +- The bot sends keys like `/+` to apps with chat protocol version 21 or later, and keys like `/add` to older apps. ## Run ```bash +npm install +npm run build npm start ``` -The bot prints its address on start. The database is stored in `./data`. +The bot prints its address and keeps its data in `./data`. + +To use the library from this repository, build it and run `npm install --no-save ../../packages/simplex-chat-nodejs` instead of `npm install`. ## Test @@ -50,8 +31,4 @@ The bot prints its address on start. The database is stored in `./data`. npm test ``` -The end-to-end test runs a local SMP server with the TLS certificates from `tests/fixtures/tls`. On Linux, the test downloads `smp-server` from [simplexmq releases](https://github.com/simplex-chat/simplexmq/releases) to `node_modules/.cache` on the first run. To use another build, or on other systems, pass its path in `SMP_SERVER` variable: - -```bash -SMP_SERVER=/path/to/smp-server npm test -``` +A local SMP server is started for the end-to-end test. On Linux, `smp-server` is downloaded from [simplexmq releases](https://github.com/simplex-chat/simplexmq/releases); to use another build, or on other systems, set `SMP_SERVER` to the path of `smp-server`. diff --git a/apps/simplex-calculator-bot/calculator.test.ts b/apps/simplex-calculator-bot/calculator.test.ts index a8350fee40..7c5d15ac8a 100644 --- a/apps/simplex-calculator-bot/calculator.test.ts +++ b/apps/simplex-calculator-bot/calculator.test.ts @@ -134,6 +134,8 @@ describe("typed messages", () => { }) describe("calculator text", () => { + const nbsp = String.fromCharCode(0xa0) + test("symbol keys", () => { expect(calculatorText(initialCalc, true)).toBe([ "*0*", @@ -147,12 +149,12 @@ describe("calculator text", () => { test("switched off: keys without number", () => { const [displayLine, ...rows] = calculatorText(undefined, true).split("\n") - expect(displayLine).toBe("* *") + expect(displayLine).toBe(`*${nbsp}*`) expect(rows).toEqual(calculatorText(initialCalc, true).split("\n").slice(1)) }) test("word keys padded to equal width", () => { - const pad = (n: number) => `\`${" ".repeat(n)}\`` + const pad = (n: number) => `\`${nbsp.repeat(n)}\`` const [displayLine, firstRow] = calculatorText(initialCalc, false).split("\n") expect(displayLine).toBe("*0*") expect(firstRow).toBe(`/C ${pad(4)}/neg ${pad(2)}/pct ${pad(2)}/div ${pad(2)}`) diff --git a/apps/simplex-calculator-bot/calculatorBot.test.ts b/apps/simplex-calculator-bot/calculatorBot.test.ts index d92390ed16..8d43e90fe1 100644 --- a/apps/simplex-calculator-bot/calculatorBot.test.ts +++ b/apps/simplex-calculator-bot/calculatorBot.test.ts @@ -2,7 +2,7 @@ import {afterAll, beforeAll, test, expect} from "vitest" import {mkdtempSync, rmSync} from "fs" import {tmpdir} from "os" import {join} from "path" -import {CEvt, T} from "@simplex-chat/types" +import {T} from "@simplex-chat/types" import {api, bot, util} from "simplex-chat" import {runCalculatorBot} from "./src/calculatorBot.js" import {SmpServer, startSmpServer} from "./test/smpServer.js" @@ -25,12 +25,11 @@ async function prepareBotDatabase(dbOpts: bot.BotDbOpts): Promise { await chat.close() } -const isCalculator = (display: string) => (ci: T.AChatItem) => ci.chatItem.meta.itemText.startsWith(`*${display}*\n`) +type ItemCheck = (ci: T.AChatItem) => boolean -const calculatorShows = (display: string) => ({chatItems}: CEvt.NewChatItems) => chatItems.some(isCalculator(display)) +const calculatorShows = (display: string): ItemCheck => ci => ci.chatItem.meta.itemText.startsWith(`*${display}*\n`) -const hasText = (text: string) => ({chatItems}: CEvt.NewChatItems) => - chatItems.some(ci => ci.chatItem.meta.itemText === text) +const hasText = (text: string): ItemCheck => ci => ci.chatItem.meta.itemText === text test("calculator in business chat", async () => { const dir = mkdtempSync(join(tmpdir(), "calculator-bot-")) @@ -41,29 +40,22 @@ test("calculator in business chat", async () => { const aliceUser = await alice.apiCreateActiveUser({displayName: "alice", fullName: ""}) await alice.startChat() await useSmpServer(alice) + const receives = (check: ItemCheck) => alice.wait("newChatItems", ({chatItems}) => chatItems.some(check), 30000) try { const [_plan, link] = await alice.apiConnectPlan(aliceUser.userId, util.contactAddressStr(address!.connLinkContact)) - const firstCalculator = alice.wait("newChatItems", calculatorShows("0"), 30000) + const firstCalculator = receives(calculatorShows("0")) await alice.apiConnect(aliceUser.userId, false, link) - const calculatorItem = (await firstCalculator)?.chatItems.find(isCalculator("0")) + const calculatorItem = (await firstCalculator)?.chatItems.find(calculatorShows("0")) expect(calculatorItem?.chatInfo.type).toBe(T.ChatType.Group) const groupId = (calculatorItem!.chatInfo as T.ChatInfo.Group).groupInfo.groupId - - const logLine = alice.wait("newChatItems", hasText("2 + 2 = 4"), 30000) - const four = alice.wait("newChatItems", calculatorShows("4"), 30000) - for (const key of ["/2", "/+", "/2", "/="]) await alice.apiSendTextMessage([T.ChatType.Group, groupId], key) - expect(await logLine).toBeDefined() - expect(await four).toBeDefined() - - const forty = alice.wait("newChatItems", calculatorShows("40"), 30000) - await alice.apiSendTextMessage([T.ChatType.Group, groupId], "12 × 3 + 4") - expect(await forty).toBeDefined() - - const typedLogLine = alice.wait("newChatItems", hasText("25 + 25 = 50"), 30000) - const fifty = alice.wait("newChatItems", calculatorShows("50"), 30000) - for (const text of ["25", "+", "25", "="]) await alice.apiSendTextMessage([T.ChatType.Group, groupId], text) - expect(await typedLogLine).toBeDefined() - expect(await fifty).toBeDefined() + const send = async (texts: string[], ...checks: ItemCheck[]) => { + const received = checks.map(receives) + for (const text of texts) await alice.apiSendTextMessage([T.ChatType.Group, groupId], text) + for (const event of received) expect(await event).toBeDefined() + } + await send(["/2", "/+", "/2", "/="], hasText("2 + 2 = 4"), calculatorShows("4")) + await send(["12 × 3 + 4"], calculatorShows("40")) + await send(["25", "+", "25", "="], hasText("25 + 25 = 50"), calculatorShows("50")) } finally { await alice.close() await calculator.close() diff --git a/apps/simplex-calculator-bot/src/calculator.ts b/apps/simplex-calculator-bot/src/calculator.ts index 189f2a956c..f9e52ec9af 100644 --- a/apps/simplex-calculator-bot/src/calculator.ts +++ b/apps/simplex-calculator-bot/src/calculator.ts @@ -1,5 +1,5 @@ -export type Digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" -export type Operator = "+" | "-" | "×" | "÷" +type Digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" +type Operator = "+" | "-" | "×" | "÷" export type Key = Digit | Operator | "C" | "±" | "%" | "√" | "." | "=" export type Update = (calc: Calc) => [Calc, string?] @@ -13,7 +13,7 @@ export interface Calc { export const initialCalc: Calc = {display: "0", operand: "0", terms: [], mode: "typing"} -export const keypad: Key[][] = [ +const keypad: Key[][] = [ ["C", "±", "%", "÷"], ["7", "8", "9", "×"], ["4", "5", "6", "-"], @@ -33,7 +33,7 @@ const keyWords: Partial> = { "=": "eq", } -export function keyWord(key: Key): string { +function keyWord(key: Key): string { return keyWords[key] ?? key } @@ -49,18 +49,23 @@ const errorDisplay = "Error" export function press(calc: Calc, key: Key): [Calc, string?] { switch (key) { - case "C": return [calc.display === "0" || calc.display === errorDisplay ? initialCalc : {...calc, display: "0", operand: "0", mode: "typing"}] + case "C": return [clear(calc)] case "=": return equals(calc) case "+": case "-": case "×": case "÷": return [operator(calc, key)] - case "%": return [result(calc, percent(calc), `${calc.operand}%`)] - case "√": return [result(calc, Math.sqrt(value(calc)), `√${calc.operand}`)] + case "%": return [showNumber(calc, percent(calc), `${calc.operand}%`)] + case "√": return [showNumber(calc, Math.sqrt(value(calc)), `√${calc.operand}`)] case "±": return [negate(calc)] default: return [enter(calc, key)] } } +function clear(calc: Calc): Calc { + if (calc.display === "0" || calc.display === errorDisplay) return initialCalc + return {...calc, display: "0", operand: "0", mode: "typing"} +} + function enter(calc: Calc, key: Digit | "."): Calc { - const display = calc.mode === "typing" ? append(calc.display, key) : key === "." ? "0." : key + const display = append(calc.mode === "typing" ? calc.display : "0", key) return {...calc, display, operand: display, mode: "typing"} } @@ -71,7 +76,7 @@ function append(display: string, key: Digit | "."): string { } function negate(calc: Calc): Calc { - if (calc.mode !== "typing") return enterNumber(calc, -value(calc)) + if (calc.mode !== "typing") return showNumber(calc, -value(calc)) const display = calc.display.startsWith("-") ? calc.display.slice(1) : `-${calc.display}` return {...calc, display, operand: display} } @@ -82,12 +87,9 @@ function percent(calc: Calc): number { return pending && (pending.op === "+" || pending.op === "-") ? pending.acc * b / 100 : b / 100 } -function enterNumber(calc: Calc, n: number): Calc { - return result(calc, n, format(n)) -} - -function result(calc: Calc, n: number, operand: string): Calc { - return {...calc, display: format(n), operand, mode: "result"} +function showNumber(calc: Calc, n: number, operand?: string): Calc { + const display = format(n) + return {...calc, display, operand: operand ?? display, mode: "result"} } function operator(calc: Calc, op: Operator): Calc { @@ -132,7 +134,7 @@ export function textInput(text: string): Update | undefined { const n = expressionValue(input.replace(/=$/, "")) if (n !== undefined) { return calc => { - const entered = enterNumber(calc, n) + const entered = showNumber(calc, n) return input.endsWith("=") ? press(entered, "=") : [entered] } } @@ -150,13 +152,13 @@ function expressionValue(expression: string): number | undefined { if (!valid) return undefined const calc = terms.reduce((current, [, op, sign, number, percentSign]) => { const withOperator = op ? press(current, keyAliases[op] ?? op as Key)[0] : current - const entered = enterNumber(withOperator, sign ? -Number(number) : Number(number)) + const entered = showNumber(withOperator, sign ? -Number(number) : Number(number)) return percentSign ? press(entered, "%")[0] : entered }, initialCalc) return value(press(calc, "=")[0]) } -const nbsp = " " +const nbsp = String.fromCharCode(0xa0) const wordWidth = Math.max(...keypad.flat().map(key => keyWord(key).length)) export function calculatorText(calc: Calc | undefined, symbolKeys: boolean): string { diff --git a/apps/simplex-calculator-bot/src/calculatorBot.ts b/apps/simplex-calculator-bot/src/calculatorBot.ts index c62d82acff..1c0daa9499 100644 --- a/apps/simplex-calculator-bot/src/calculatorBot.ts +++ b/apps/simplex-calculator-bot/src/calculatorBot.ts @@ -6,13 +6,14 @@ import {calculatorIcon} from "./icon.js" const anyTextCommandsVersion = 21 const idleMinutes = 10 -const welcomeMessage = `Tap the keys, or send numbers, keys like + or =, and expressions like 12 × 3 + 4.\nKeys are applied left to right, as on a pocket calculator.\nThe calculator turns off after ${idleMinutes} minutes, any key turns it on.` +const welcomeMessage = `Tap the keys or send numbers, keys like + and =, or expressions like 12 × 3 + 4. +Keys are applied left to right, as on a pocket calculator. +The calculator turns off after ${idleMinutes} minutes; any key turns it on.` const hint = "Send a number, a key like + or =, or an expression like 12 × 3 + 4." interface Session { calc?: Calc itemId: number - symbolKeys: boolean timer?: NodeJS.Timeout } @@ -29,17 +30,13 @@ function groupSender({chatInfo, chatItem}: T.AChatItem): Sender | undefined { : undefined } -function currentCalc(groupId: number): Calc { - return sessions.get(groupId)?.calc ?? initialCalc -} - -async function showCalculator(chat: api.ChatApi, groupId: number, member: T.GroupMember, calc: Calc): Promise { +async function showCalculator(chat: api.ChatApi, {groupId, member}: Sender, calc: Calc): Promise { const symbolKeys = member.memberChatVRange.maxVersion >= anyTextCommandsVersion const [sent] = await chat.apiSendTextMessage([T.ChatType.Group, groupId], calculatorText(calc, symbolKeys)) const itemId = sent.chatItem.meta.itemId const previous = sessions.get(groupId) const timer = setTimeout(() => turnOff(chat, groupId, itemId, symbolKeys), idleMinutes * 60_000) - sessions.set(groupId, {calc, itemId, symbolKeys, timer}) + sessions.set(groupId, {calc, itemId, timer}) if (previous) { clearTimeout(previous.timer) await chat.apiDeleteChatItems(T.ChatType.Group, groupId, [previous.itemId], T.CIDeleteMode.Broadcast) @@ -47,15 +44,15 @@ async function showCalculator(chat: api.ChatApi, groupId: number, member: T.Grou } function turnOff(chat: api.ChatApi, groupId: number, itemId: number, symbolKeys: boolean): void { - sessions.set(groupId, {itemId, symbolKeys}) + sessions.set(groupId, {itemId}) chat.apiUpdateChatItem(T.ChatType.Group, groupId, itemId, {type: "text", text: calculatorText(undefined, symbolKeys)}, false) .catch(e => console.log("error turning calculator off", e)) } -async function updateCalculator(chat: api.ChatApi, {groupId, member}: Sender, update: Update): Promise { - const [calc, logLine] = update(currentCalc(groupId)) - if (logLine) await chat.apiSendTextMessage([T.ChatType.Group, groupId], logLine) - await showCalculator(chat, groupId, member, calc) +async function updateCalculator(chat: api.ChatApi, sender: Sender, update: Update): Promise { + const [calc, logLine] = update(sessions.get(sender.groupId)?.calc ?? initialCalc) + if (logLine) await chat.apiSendTextMessage([T.ChatType.Group, sender.groupId], logLine) + await showCalculator(chat, sender, calc) } function tapCommand(update: Update) { @@ -92,7 +89,7 @@ export function runCalculatorBot(dbOpts: bot.BotDbOpts): Promise<[api.ChatApi, T "": async (ci, _command, chat) => { await chat.apiSendTextReply(ci, hint) }, }, events: { - joinedGroupMember: ({groupInfo, member}, chat) => showCalculator(chat, groupInfo.groupId, member, initialCalc), + joinedGroupMember: ({groupInfo, member}, chat) => showCalculator(chat, {groupId: groupInfo.groupId, member}, initialCalc), }, }) } diff --git a/apps/simplex-calculator-bot/test/smpServer.ts b/apps/simplex-calculator-bot/test/smpServer.ts index c42bee517f..c7f0d87ec7 100644 --- a/apps/simplex-calculator-bot/test/smpServer.ts +++ b/apps/simplex-calculator-bot/test/smpServer.ts @@ -28,20 +28,22 @@ export async function startSmpServer(): Promise { env: {...process.env, SMP_SERVER_CFG_PATH: configDir, SMP_SERVER_LOG_PATH: join(dir, "logs")}, stdio: ["ignore", "ignore", "inherit"], }) - let spawnError: Error | undefined - server.on("error", e => { spawnError = e }) - await waitForServer(server, port, () => spawnError) - return { - address: `smp://${fingerprint}@localhost:${port}`, - stop: async () => { - if (server.exitCode === null) { - const exited = once(server, "exit") - server.kill() - await exited - } - rmSync(dir, {recursive: true, force: true}) - }, + const stop = async () => { + if (server.exitCode === null) { + const exited = once(server, "exit") + server.kill() + await exited + } + rmSync(dir, {recursive: true, force: true}) } + try { + await once(server, "spawn") + await waitForServer(server, port) + } catch (e) { + await stop() + throw e + } + return {address: `smp://${fingerprint}@localhost:${port}`, stop} } async function smpServerExecutable(): Promise { @@ -54,7 +56,7 @@ async function smpServerExecutable(): Promise { async function downloadSmpServer(path: string): Promise { const arch = releaseArch[process.arch] if (process.platform !== "linux" || !arch) { - throw new Error("smp-server release binaries are only available for Linux, set SMP_SERVER to the smp-server executable") + throw new Error(`smp-server release binaries are not available for ${process.platform} ${process.arch}, set SMP_SERVER to the smp-server path`) } const url = `https://github.com/simplex-chat/simplexmq/releases/download/${smpServerRelease}/smp-server-ubuntu-22_04-${arch}` const response = await fetch(url) @@ -80,12 +82,10 @@ function freePort(): Promise { }) } -async function waitForServer(server: ChildProcess, port: number, spawnError: () => Error | undefined): Promise { +async function waitForServer(server: ChildProcess, port: number): Promise { const deadline = Date.now() + 15_000 while (!(await canConnect(port))) { - if (server.pid === undefined || server.exitCode !== null || Date.now() > deadline) { - throw new Error(`smp-server did not start on port ${port}: ${spawnError()?.message ?? "no error"}`) - } + if (server.exitCode !== null || Date.now() > deadline) throw new Error(`smp-server did not start on port ${port}`) await new Promise(resolve => setTimeout(resolve, 100)) } } diff --git a/packages/simplex-chat-nodejs/README.md b/packages/simplex-chat-nodejs/README.md index c70c7a194c..05c99eca5c 100644 --- a/packages/simplex-chat-nodejs/README.md +++ b/packages/simplex-chat-nodejs/README.md @@ -62,7 +62,7 @@ There is an example with more options in [./examples/squaring-bot.ts](./examples You can run it with: `npx ts-node ./examples/squaring-bot.ts` -A complete bot using business chats, command keys and message updates: [SimpleX Calculator](../../apps/simplex-calculator-bot/). +A larger example, with a business address and commands as keys: [SimpleX Calculator](../../apps/simplex-calculator-bot/). ## PostgreSQL backend diff --git a/packages/simplex-chat-nodejs/docs/api.TypeAlias.EventSubscriberFunc.md b/packages/simplex-chat-nodejs/docs/api.TypeAlias.EventSubscriberFunc.md index bf40b3165c..13efcec7dd 100644 --- a/packages/simplex-chat-nodejs/docs/api.TypeAlias.EventSubscriberFunc.md +++ b/packages/simplex-chat-nodejs/docs/api.TypeAlias.EventSubscriberFunc.md @@ -6,7 +6,7 @@ # Type Alias: EventSubscriberFunc\ -> **EventSubscriberFunc**\<`K`\> = (`event`) => `void` \| `Promise`\<`void`\> +> **EventSubscriberFunc**\<`K`\> = (`event`, `chat`) => `void` \| `Promise`\<`void`\> Defined in: [src/api.ts:50](../src/api.ts#L50) @@ -22,6 +22,10 @@ Defined in: [src/api.ts:50](../src/api.ts#L50) `ChatEvent` & `object` +### chat + +[`ChatApi`](api.Class.ChatApi.md) + ## Returns `void` \| `Promise`\<`void`\> diff --git a/packages/simplex-chat-nodejs/docs/bot.Function.subscribeChatItems.md b/packages/simplex-chat-nodejs/docs/bot.Function.subscribeChatItems.md index 5397cf3aeb..8fbb699f80 100644 --- a/packages/simplex-chat-nodejs/docs/bot.Function.subscribeChatItems.md +++ b/packages/simplex-chat-nodejs/docs/bot.Function.subscribeChatItems.md @@ -18,7 +18,7 @@ Defined in: [src/bot.ts:108](../src/bot.ts#L108) ### onMessage -((`chatItem`, `content`) => `void` \| `Promise`\<`void`\>) \| `undefined` +((`chatItem`, `content`, `chat`) => `void` \| `Promise`\<`void`\>) \| `undefined` ### commands diff --git a/packages/simplex-chat-nodejs/docs/bot.Interface.BotConfig.md b/packages/simplex-chat-nodejs/docs/bot.Interface.BotConfig.md index f763f414da..3f7757041b 100644 --- a/packages/simplex-chat-nodejs/docs/bot.Interface.BotConfig.md +++ b/packages/simplex-chat-nodejs/docs/bot.Interface.BotConfig.md @@ -34,13 +34,13 @@ Defined in: [src/bot.ts:42](../src/bot.ts#L42) #### Index Signature -\[`key`: `string`\]: ((`chatItem`, `command`) => `void` \| `Promise`\<`void`\>) \| `undefined` +\[`key`: `string`\]: ((`chatItem`, `command`, `chat`) => `void` \| `Promise`\<`void`\>) \| `undefined` *** ### onMessage? -> `optional` **onMessage?**: (`chatItem`, `content`) => `void` \| `Promise`\<`void`\> +> `optional` **onMessage?**: (`chatItem`, `content`, `chat`) => `void` \| `Promise`\<`void`\> Defined in: [src/bot.ts:40](../src/bot.ts#L40) @@ -54,6 +54,10 @@ Defined in: [src/bot.ts:40](../src/bot.ts#L40) `MsgContent` +##### chat + +[`ChatApi`](api.Class.ChatApi.md) + #### Returns `void` \| `Promise`\<`void`\> diff --git a/tests/MarkdownTests.hs b/tests/MarkdownTests.hs index ffb1588c23..903e3b3773 100644 --- a/tests/MarkdownTests.hs +++ b/tests/MarkdownTests.hs @@ -407,10 +407,7 @@ textWithCommands = describe "text with commands" do "send /'filter 1'." <==> "send " <> command "filter 1" "/'filter 1'" <> "." "send /'filter 1.'!" <==> "send " <> command "filter 1." "/'filter 1.'" <> "!" "send /he?lp" <==> "send " <> command "he?lp" "/he?lp" - "/+" <==> command "+" "/+" "/-" <==> command "-" "/-" - "/." <==> command "." "/." - "/√ /÷" <==> command "√" "/√" <> " " <> command "÷" "/÷" "send /+." <==> "send " <> command "+" "/+" <> "." "/'+'" <==> command "+" "/'+'" it "calculator keys" do