create-gramstax 0.8.24 → 0.10.0
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/dist/src/index.cjs +1 -1
- package/dist/src/index.js +2 -2
- package/dist/src/templates/README.md +0 -10
- package/dist/src/templates/e2e/blank.test.ts +9 -0
- package/dist/src/templates/e2e/general-status.test.ts +28 -0
- package/dist/src/templates/e2e/help.test.ts +28 -0
- package/dist/src/templates/e2e/random-number.test.ts +28 -0
- package/dist/src/templates/e2e/setup.ts +15 -0
- package/dist/src/templates/e2e/start.test.ts +33 -0
- package/dist/src/templates/e2e/sum-number.test.ts +44 -0
- package/dist/src/templates/e2e/username-notfound.test.ts +24 -0
- package/dist/src/templates/package.json +1 -1
- package/dist/src/templates/src/core/bot.ts +1 -1
- package/dist/src/templates/src/pages/blank.ts +1 -1
- package/dist/src/templates/src/pages/general-status.ts +6 -6
- package/dist/src/templates/src/pages/help.ts +20 -6
- package/dist/src/templates/src/pages/random-number.ts +8 -8
- package/dist/src/templates/src/pages/start.ts +16 -8
- package/dist/src/templates/src/pages/sum-number.ts +20 -20
- package/dist/src/templates/src/pages/username-notfound.ts +6 -6
- package/package.json +1 -1
- package/dist/src/templates/e2e/main.test.ts +0 -287
package/dist/src/index.cjs
CHANGED
|
@@ -10,4 +10,4 @@ $$ | $$ |$$ | $$ __$$ |$$ | $$ | $$ | \\____$$\\ $$ |$$\\ $$ __$$ | $$
|
|
|
10
10
|
\\______/ \\__| \\_______|\\__| \\__| \\__|\\_______/ \\____/ \\_______|\\__/ \\__|
|
|
11
11
|
|
|
12
12
|
`,t=this.version?`v${this.version}`:"v0.0.0";console.log(`${e}${t}
|
|
13
|
-
`)}getPackageManagerCommands(e){return{bun:{install:"bun install",dev:"bun dev",start:"bun start"},npm:{install:"npm install",dev:"npm run dev",start:"npm start"},pnpm:{install:"pnpm install",dev:"pnpm dev",start:"pnpm start"},yarn:{install:"yarn install",dev:"yarn dev",start:"yarn start"}}[e]}copyDirectorySync(e,t,n={}){i.existsSync(t)||i.mkdirSync(t,{recursive:!0});let a=i.readdirSync(e,{withFileTypes:!0});for(let c of a){let m=s.join(e,c.name),l=s.join(t,c.name);if(c.isDirectory())this.copyDirectorySync(m,l,n);else if(c.isFile()){let $=i.readFileSync(m,"utf-8");$=this.replaceTemplateVariables($,n),i.writeFileSync(l,$)}}}replaceTemplateVariables(e,t){let n=e;for(let[a,c]of Object.entries(t))n=n.replace(new RegExp(`{{${a}}}`,"g"),c);return n}async promptForProjectName(){return(await P.default.prompt({type:"input",name:"projectName",message:"What is your project named?",initial:"",validate:p(t=>t===""||/^[a-z0-9-_]+$/i.test(t)?!0:"Project name can only contain letters, numbers, hyphens, and underscores","validate")})).projectName}async promptForConfiguration(){let e=[];e.push({type:"select",name:"packageManager",message:"Which package manager would you like to use?",choices:[{name:"bun",message:"Bun (recommended)",value:"bun"},{name:"npm",message:"npm",value:"npm"},{name:"pnpm",message:"pnpm",value:"pnpm"},{name:"yarn",message:"Yarn",value:"yarn"}],initial:0}),e.push({type:"select",name:"runtime",message:"Which runtime would you like to use?",choices:[{name:"bun",message:"Bun (recommended)",value:"bun"},{name:"node",message:"Node.js",value:"node"}],initial:0}),e.push({type:"confirm",name:"eslint",message:"Would you like to use ESLint?",initial:!0});let t=await P.default.prompt(e);return{packageManager:t.packageManager,runtime:t.runtime,eslint:t.eslint}}async installDependencies(e,t){return await new Promise(n=>{let a=g.resolvePackageManagerCommand(t),m=(0,j.spawn)(a,["install"],{cwd:e,stdio:"inherit",shell:!1});m.on("close",l=>{l===0?n(!0):(r.fail("Dependencies installation failed"),n(!1))}),m.on("error",l=>{r.fail(`Dependencies installation failed: ${l.message}`),n(!1)})})}async gatherProjectConfig(){this.printBanner();let e=this.projectDirectory,t,n=!1,a=g.resolveInitialWorkingDirectory();e?(t=s.resolve(a,e),n=!1):(e=await this.promptForProjectName(),e===""?(e=s.basename(a),t=a,n=!0):(t=s.resolve(a,e),n=!1));let c=await this.promptForConfiguration(),m=c.packageManager,l=c.runtime,$=c.eslint;
|
|
13
|
+
`)}getPackageManagerCommands(e){return{bun:{install:"bun install",dev:"bun dev",start:"bun start"},npm:{install:"npm install",dev:"npm run dev",start:"npm start"},pnpm:{install:"pnpm install",dev:"pnpm dev",start:"pnpm start"},yarn:{install:"yarn install",dev:"yarn dev",start:"yarn start"}}[e]}copyDirectorySync(e,t,n={}){i.existsSync(t)||i.mkdirSync(t,{recursive:!0});let a=i.readdirSync(e,{withFileTypes:!0});for(let c of a){let m=s.join(e,c.name),l=s.join(t,c.name);if(c.isDirectory())this.copyDirectorySync(m,l,n);else if(c.isFile()){let $=i.readFileSync(m,"utf-8");$=this.replaceTemplateVariables($,n),i.writeFileSync(l,$)}}}replaceTemplateVariables(e,t){let n=e;for(let[a,c]of Object.entries(t))n=n.replace(new RegExp(`{{${a}}}`,"g"),c);return n}async promptForProjectName(){return(await P.default.prompt({type:"input",name:"projectName",message:"What is your project named?",initial:"",validate:p(t=>t===""||/^[a-z0-9-_]+$/i.test(t)?!0:"Project name can only contain letters, numbers, hyphens, and underscores","validate")})).projectName}async promptForConfiguration(){let e=[];e.push({type:"select",name:"packageManager",message:"Which package manager would you like to use?",choices:[{name:"bun",message:"Bun (recommended)",value:"bun"},{name:"npm",message:"npm",value:"npm"},{name:"pnpm",message:"pnpm",value:"pnpm"},{name:"yarn",message:"Yarn",value:"yarn"}],initial:0}),e.push({type:"select",name:"runtime",message:"Which runtime would you like to use?",choices:[{name:"bun",message:"Bun (recommended)",value:"bun"},{name:"node",message:"Node.js",value:"node"}],initial:0}),e.push({type:"confirm",name:"eslint",message:"Would you like to use ESLint?",initial:!0});let t=await P.default.prompt(e);return{packageManager:t.packageManager,runtime:t.runtime,eslint:t.eslint}}async installDependencies(e,t){return await new Promise(n=>{let a=g.resolvePackageManagerCommand(t),m=(0,j.spawn)(a,["install"],{cwd:e,stdio:"inherit",shell:!1});m.on("close",l=>{l===0?n(!0):(r.fail("Dependencies installation failed"),n(!1))}),m.on("error",l=>{r.fail(`Dependencies installation failed: ${l.message}`),n(!1)})})}async gatherProjectConfig(){this.printBanner();let e=this.projectDirectory,t,n=!1,a=g.resolveInitialWorkingDirectory();e?(t=s.resolve(a,e),n=!1):(e=await this.promptForProjectName(),e===""?(e=s.basename(a),t=a,n=!0):(t=s.resolve(a,e),n=!1));let c=await this.promptForConfiguration(),m=c.packageManager,l=c.runtime,$=c.eslint;if(r.info(`Project: ${e}`),r.info(`Location: ${t}`),r.info(`Package Manager: ${m}`),r.info(`Runtime: ${l}`),r.info(`ESLint: ${$?"Yes":"No"}`),n){if(!g.isDirectoryEffectivelyEmpty(t))throw r.error("Current directory is not empty!"),new Error("Current directory is not empty")}else if(i.existsSync(t))throw r.error(`Directory "${e}" already exists!`),new Error(`Directory "${e}" already exists`);return{projectName:e,projectPath:t,packageManager:m,runtime:l,setupEslint:$}}async executeProjectGeneration(e){let{projectName:t,projectPath:n,packageManager:a,runtime:c,setupEslint:m}=e;r.info("Creating project directory.."),i.existsSync(n)||i.mkdirSync(n,{recursive:!0}),r.info("Copying template files..");let l=s.join(Y,"templates"),$=this.getPackageManagerCommands(a),I={projectName:t,installCmd:$.install,runDevCmd:$.dev,runStartCmd:$.start};this.copyDirectorySync(l,n,I),r.info("Creating logs directory.."),i.mkdirSync(s.join(n,"logs"),{recursive:!0}),i.writeFileSync(s.join(n,"logs/.gitkeep"),"");let L=s.join(n,".env"),S=s.join(n,".env.example");if(i.existsSync(S)&&(r.info("Creating .env file.."),i.renameSync(S,L)),c!=="bun"){r.info("Removing bunfig.toml for Node.js runtime..");let d=s.join(n,"bunfig.toml");i.existsSync(d)&&i.unlinkSync(d)}if(r.info("Installing dependencies.."),!await this.installDependencies(n,a))throw new Error("Project dependencies installation failed");let u=s.join(n,"eslint.config.ts.template"),T=s.join(n,"eslint.config.ts");if(m){r.info("Installing ESLint dependencies..");let d=g.resolvePackageManagerCommand(a),w=a==="yarn"?["add","-D"]:["install","-D"];w.push("eslint","@eslint/js","globals","typescript-eslint"),await new Promise(b=>{let k=(0,j.spawn)(d,w,{cwd:n,stdio:"inherit",shell:!1});k.on("close",()=>b()),k.on("error",U=>{r.fail(`Failed to install ESLint dependencies: ${U.message}`),b()})}),i.existsSync(u)&&i.renameSync(u,T)}else i.existsSync(u)&&i.unlinkSync(u);r.info("Project created successfully!"),r.info(`Next steps: ${e.projectPath!==process.cwd()?`cd ${t} && `:" "}${$.install} && ${$.dev}`),r.info("Done..")}async createProject(){try{this.config=await this.gatherProjectConfig(),await this.executeProjectGeneration(this.config)}catch(e){e instanceof Error?r.error(`Failed to create project: ${e.message}`):r.error(`Failed to create project: ${String(e)}`),process.exit(1)}}};var N=require("commander"),R=require("fs"),y=require("path"),M=require("url"),z=(0,M.fileURLToPath)(f),H=(0,y.dirname)(z),K=(0,y.join)(H,"../../package.json"),x=JSON.parse((0,R.readFileSync)(K,"utf-8")).version,F=new N.Command;F.name("create-gramstax").description("CLI to create a new Gramstax Telegram bot project").version(x).argument("[project-directory]","Directory to create the project in").action(async o=>{try{await new _(o,{version:x}).createProject()}catch(e){r.error(`Failed to create project: ${String(e)}`),process.exit(1)}});F.parse();
|
package/dist/src/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var
|
|
2
|
+
var C=Object.defineProperty;var g=(p,e)=>C(p,"name",{value:e,configurable:!0});import{LoggingPretty as N}from"logging-pretty";var r=new N;import*as i from"fs";import*as a from"path";import{fileURLToPath as R}from"url";import{readdirSync as F}from"fs";var m=class p{static{g(this,"FsUt")}static DEFAULT_IGNORED_ENTRIES=new Set([".git",".gitignore",".gitkeep",".DS_Store","Thumbs.db"]);static isDirectoryEffectivelyEmpty(e,t=p.DEFAULT_IGNORED_ENTRIES){return F(e).filter(o=>!t.has(o)).length===0}static resolvePackageManagerCommand(e,t=process.platform){return t!=="win32"||e==="bun"?e:`${e}.cmd`}static resolveInitialWorkingDirectory(e=process.cwd(),t=process.env.INIT_CWD){return t??e}};import{spawn as j}from"child_process";import P from"enquirer";var L=R(import.meta.url),T=a.dirname(L),d=class{constructor(e,t){this.projectDirectory=e;this.version=t?.version}projectDirectory;static{g(this,"Create")}config;version;printBanner(){let e=`
|
|
3
3
|
$$$$$$\\ $$\\
|
|
4
4
|
$$ __$$\\ $$ |
|
|
5
5
|
$$ / \\__| $$$$$$\\ $$$$$$\\ $$$$$$\\$$$$\\ $$$$$$$\\ $$$$$$\\ $$$$$$\\ $$\\ $$\\
|
|
@@ -10,4 +10,4 @@ $$ | $$ |$$ | $$ __$$ |$$ | $$ | $$ | \\____$$\\ $$ |$$\\ $$ __$$ | $$
|
|
|
10
10
|
\\______/ \\__| \\_______|\\__| \\__| \\__|\\_______/ \\____/ \\_______|\\__/ \\__|
|
|
11
11
|
|
|
12
12
|
`,t=this.version?`v${this.version}`:"v0.0.0";console.log(`${e}${t}
|
|
13
|
-
`)}getPackageManagerCommands(e){return{bun:{install:"bun install",dev:"bun dev",start:"bun start"},npm:{install:"npm install",dev:"npm run dev",start:"npm start"},pnpm:{install:"pnpm install",dev:"pnpm dev",start:"pnpm start"},yarn:{install:"yarn install",dev:"yarn dev",start:"yarn start"}}[e]}copyDirectorySync(e,t,n={}){i.existsSync(t)||i.mkdirSync(t,{recursive:!0});let s=i.readdirSync(e,{withFileTypes:!0});for(let o of s){let l=a.join(e,o.name),$=a.join(t,o.name);if(o.isDirectory())this.copyDirectorySync(l,$,n);else if(o.isFile()){let c=i.readFileSync(l,"utf-8");c=this.replaceTemplateVariables(c,n),i.writeFileSync($,c)}}}replaceTemplateVariables(e,t){let n=e;for(let[s,o]of Object.entries(t))n=n.replace(new RegExp(`{{${s}}}`,"g"),o);return n}async promptForProjectName(){return(await P.prompt({type:"input",name:"projectName",message:"What is your project named?",initial:"",validate:g(t=>t===""||/^[a-z0-9-_]+$/i.test(t)?!0:"Project name can only contain letters, numbers, hyphens, and underscores","validate")})).projectName}async promptForConfiguration(){let e=[];e.push({type:"select",name:"packageManager",message:"Which package manager would you like to use?",choices:[{name:"bun",message:"Bun (recommended)",value:"bun"},{name:"npm",message:"npm",value:"npm"},{name:"pnpm",message:"pnpm",value:"pnpm"},{name:"yarn",message:"Yarn",value:"yarn"}],initial:0}),e.push({type:"select",name:"runtime",message:"Which runtime would you like to use?",choices:[{name:"bun",message:"Bun (recommended)",value:"bun"},{name:"node",message:"Node.js",value:"node"}],initial:0}),e.push({type:"confirm",name:"eslint",message:"Would you like to use ESLint?",initial:!0});let t=await P.prompt(e);return{packageManager:t.packageManager,runtime:t.runtime,eslint:t.eslint}}async installDependencies(e,t){return await new Promise(n=>{let s=m.resolvePackageManagerCommand(t),l=j(s,["install"],{cwd:e,stdio:"inherit",shell:!1});l.on("close",$=>{$===0?n(!0):(r.fail("Dependencies installation failed"),n(!1))}),l.on("error",$=>{r.fail(`Dependencies installation failed: ${$.message}`),n(!1)})})}async gatherProjectConfig(){this.printBanner();let e=this.projectDirectory,t,n=!1,s=m.resolveInitialWorkingDirectory();e?(t=a.resolve(s,e),n=!1):(e=await this.promptForProjectName(),e===""?(e=a.basename(s),t=s,n=!0):(t=a.resolve(s,e),n=!1));let o=await this.promptForConfiguration(),l=o.packageManager,$=o.runtime,c=o.eslint;
|
|
13
|
+
`)}getPackageManagerCommands(e){return{bun:{install:"bun install",dev:"bun dev",start:"bun start"},npm:{install:"npm install",dev:"npm run dev",start:"npm start"},pnpm:{install:"pnpm install",dev:"pnpm dev",start:"pnpm start"},yarn:{install:"yarn install",dev:"yarn dev",start:"yarn start"}}[e]}copyDirectorySync(e,t,n={}){i.existsSync(t)||i.mkdirSync(t,{recursive:!0});let s=i.readdirSync(e,{withFileTypes:!0});for(let o of s){let l=a.join(e,o.name),$=a.join(t,o.name);if(o.isDirectory())this.copyDirectorySync(l,$,n);else if(o.isFile()){let c=i.readFileSync(l,"utf-8");c=this.replaceTemplateVariables(c,n),i.writeFileSync($,c)}}}replaceTemplateVariables(e,t){let n=e;for(let[s,o]of Object.entries(t))n=n.replace(new RegExp(`{{${s}}}`,"g"),o);return n}async promptForProjectName(){return(await P.prompt({type:"input",name:"projectName",message:"What is your project named?",initial:"",validate:g(t=>t===""||/^[a-z0-9-_]+$/i.test(t)?!0:"Project name can only contain letters, numbers, hyphens, and underscores","validate")})).projectName}async promptForConfiguration(){let e=[];e.push({type:"select",name:"packageManager",message:"Which package manager would you like to use?",choices:[{name:"bun",message:"Bun (recommended)",value:"bun"},{name:"npm",message:"npm",value:"npm"},{name:"pnpm",message:"pnpm",value:"pnpm"},{name:"yarn",message:"Yarn",value:"yarn"}],initial:0}),e.push({type:"select",name:"runtime",message:"Which runtime would you like to use?",choices:[{name:"bun",message:"Bun (recommended)",value:"bun"},{name:"node",message:"Node.js",value:"node"}],initial:0}),e.push({type:"confirm",name:"eslint",message:"Would you like to use ESLint?",initial:!0});let t=await P.prompt(e);return{packageManager:t.packageManager,runtime:t.runtime,eslint:t.eslint}}async installDependencies(e,t){return await new Promise(n=>{let s=m.resolvePackageManagerCommand(t),l=j(s,["install"],{cwd:e,stdio:"inherit",shell:!1});l.on("close",$=>{$===0?n(!0):(r.fail("Dependencies installation failed"),n(!1))}),l.on("error",$=>{r.fail(`Dependencies installation failed: ${$.message}`),n(!1)})})}async gatherProjectConfig(){this.printBanner();let e=this.projectDirectory,t,n=!1,s=m.resolveInitialWorkingDirectory();e?(t=a.resolve(s,e),n=!1):(e=await this.promptForProjectName(),e===""?(e=a.basename(s),t=s,n=!0):(t=a.resolve(s,e),n=!1));let o=await this.promptForConfiguration(),l=o.packageManager,$=o.runtime,c=o.eslint;if(r.info(`Project: ${e}`),r.info(`Location: ${t}`),r.info(`Package Manager: ${l}`),r.info(`Runtime: ${$}`),r.info(`ESLint: ${c?"Yes":"No"}`),n){if(!m.isDirectoryEffectivelyEmpty(t))throw r.error("Current directory is not empty!"),new Error("Current directory is not empty")}else if(i.existsSync(t))throw r.error(`Directory "${e}" already exists!`),new Error(`Directory "${e}" already exists`);return{projectName:e,projectPath:t,packageManager:l,runtime:$,setupEslint:c}}async executeProjectGeneration(e){let{projectName:t,projectPath:n,packageManager:s,runtime:o,setupEslint:l}=e;r.info("Creating project directory.."),i.existsSync(n)||i.mkdirSync(n,{recursive:!0}),r.info("Copying template files..");let $=a.join(T,"templates"),c=this.getPackageManagerCommands(s),S={projectName:t,installCmd:c.install,runDevCmd:c.dev,runStartCmd:c.start};this.copyDirectorySync($,n,S),r.info("Creating logs directory.."),i.mkdirSync(a.join(n,"logs"),{recursive:!0}),i.writeFileSync(a.join(n,"logs/.gitkeep"),"");let b=a.join(n,".env"),y=a.join(n,".env.example");if(i.existsSync(y)&&(r.info("Creating .env file.."),i.renameSync(y,b)),o!=="bun"){r.info("Removing bunfig.toml for Node.js runtime..");let u=a.join(n,"bunfig.toml");i.existsSync(u)&&i.unlinkSync(u)}if(r.info("Installing dependencies.."),!await this.installDependencies(n,s))throw new Error("Project dependencies installation failed");let f=a.join(n,"eslint.config.ts.template"),D=a.join(n,"eslint.config.ts");if(l){r.info("Installing ESLint dependencies..");let u=m.resolvePackageManagerCommand(s),_=s==="yarn"?["add","-D"]:["install","-D"];_.push("eslint","@eslint/js","globals","typescript-eslint"),await new Promise(h=>{let v=j(u,_,{cwd:n,stdio:"inherit",shell:!1});v.on("close",()=>h()),v.on("error",E=>{r.fail(`Failed to install ESLint dependencies: ${E.message}`),h()})}),i.existsSync(f)&&i.renameSync(f,D)}else i.existsSync(f)&&i.unlinkSync(f);r.info("Project created successfully!"),r.info(`Next steps: ${e.projectPath!==process.cwd()?`cd ${t} && `:" "}${c.install} && ${c.dev}`),r.info("Done..")}async createProject(){try{this.config=await this.gatherProjectConfig(),await this.executeProjectGeneration(this.config)}catch(e){e instanceof Error?r.error(`Failed to create project: ${e.message}`):r.error(`Failed to create project: ${String(e)}`),process.exit(1)}}};import{Command as I}from"commander";import{readFileSync as W}from"fs";import{dirname as A,join as G}from"path";import{fileURLToPath as B}from"url";var O=B(import.meta.url),U=A(O),V=G(U,"../../package.json"),k=JSON.parse(W(V,"utf-8")).version,w=new I;w.name("create-gramstax").description("CLI to create a new Gramstax Telegram bot project").version(k).argument("[project-directory]","Directory to create the project in").action(async p=>{try{await new d(p,{version:k}).createProject()}catch(e){r.error(`Failed to create project: ${String(e)}`),process.exit(1)}});w.parse();
|
|
@@ -4,8 +4,6 @@ A Telegram bot built with Gramstax framework.
|
|
|
4
4
|
|
|
5
5
|
## Getting Started
|
|
6
6
|
|
|
7
|
-
### Development
|
|
8
|
-
|
|
9
7
|
1. Install dependencies:
|
|
10
8
|
|
|
11
9
|
```bash
|
|
@@ -22,12 +20,6 @@ TELEGRAM_BOT_TOKEN="<...your_bot_token_here...>"
|
|
|
22
20
|
|
|
23
21
|
3. Run the development server:
|
|
24
22
|
|
|
25
|
-
```bash
|
|
26
|
-
{{runDevCmd}}
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
#### Using Bun directly
|
|
30
|
-
|
|
31
23
|
```bash
|
|
32
24
|
{{runStartCmd}}
|
|
33
25
|
```
|
|
@@ -54,5 +46,3 @@ TELEGRAM_BOT_TOKEN="<...your_bot_token_here...>"
|
|
|
54
46
|
## Learn More
|
|
55
47
|
|
|
56
48
|
- [Gramstax Documentation](https://github.com/gramstax/gramstax)
|
|
57
|
-
- [Grammy Documentation](https://grammy.dev/)
|
|
58
|
-
- [Telegram Bot API](https://core.telegram.org/bots/api)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {describe, it, expect, beforeEach} from "bun:test"
|
|
2
|
+
import {e2e} from "./setup"
|
|
3
|
+
|
|
4
|
+
const intent = `general-status`
|
|
5
|
+
const asUser = {user: {id: 8001, username: `testuser`}, chat: {id: 8001}}
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
e2e.clearCalls()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
describe(`E2E: GeneralStatusPage`, () => {
|
|
12
|
+
it(`show 'Route not found' for unrecognized callback query`, async () => {
|
|
13
|
+
await e2e.sendText(`/start`, asUser)
|
|
14
|
+
e2e.clearCalls()
|
|
15
|
+
|
|
16
|
+
const responses = await e2e.sendCallbackQuery(`nonexistent-callback-data`, asUser)
|
|
17
|
+
const responsesId = await e2e.sendCallbackQuery(`nonexistent-callback-data`, {user: {id: 8002, username: `pengguna`, language_code: `id`}, chat: {id: 8002}})
|
|
18
|
+
|
|
19
|
+
expect(responses.length).toBeGreaterThan(0)
|
|
20
|
+
expect(responsesId.length).toBeGreaterThan(0)
|
|
21
|
+
const reply = responses[0]!
|
|
22
|
+
const replyId = responsesId[0]!
|
|
23
|
+
expect(reply).toBeDefined()
|
|
24
|
+
expect(replyId).toBeDefined()
|
|
25
|
+
expect(reply.body?.text).toBe(e2e.gmsg(intent, `d`, {isNotFound: true}))
|
|
26
|
+
expect(replyId.body?.text).toBe(e2e.gmsg(intent, `d`, {isNotFound: true}, `id`))
|
|
27
|
+
})
|
|
28
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {describe, it, expect, beforeEach} from "bun:test"
|
|
2
|
+
import {e2e} from "./setup"
|
|
3
|
+
|
|
4
|
+
const intent = `help`
|
|
5
|
+
const asUser = {user: {id: 7001, username: `testuser`}, chat: {id: 7001}}
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
e2e.clearCalls()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
describe(`E2E: HelpPage`, () => {
|
|
12
|
+
it(`show help text when navigating from /start -> Help`, async () => {
|
|
13
|
+
await e2e.sendText(`/start`, asUser)
|
|
14
|
+
e2e.clearCalls()
|
|
15
|
+
|
|
16
|
+
const responses = await e2e.sendCallbackQuery(intent, asUser)
|
|
17
|
+
const responsesId = await e2e.sendCallbackQuery(intent, {user: {id: 7002, username: `pengguna`, language_code: `id`}, chat: {id: 7002}})
|
|
18
|
+
|
|
19
|
+
expect(responses.length).toBeGreaterThan(0)
|
|
20
|
+
expect(responsesId.length).toBeGreaterThan(0)
|
|
21
|
+
const edit = responses[0]!
|
|
22
|
+
const editId = responsesId[0]!
|
|
23
|
+
expect(edit.body?.text).toBe(e2e.gmsg(intent, `s1`, {}))
|
|
24
|
+
expect(editId.body?.text).toBe(e2e.gmsg(intent, `s1`, {}, `id`))
|
|
25
|
+
expect(e2e.extractButtonLabels(edit)).toEqual(e2e.gkb(intent, `s1`, `inline`, {}))
|
|
26
|
+
expect(e2e.extractButtonLabels(editId)).toEqual(e2e.gkb(intent, `s1`, `inline`, {}, `id`))
|
|
27
|
+
})
|
|
28
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {describe, it, expect, beforeEach} from "bun:test"
|
|
2
|
+
import {e2e} from "./setup"
|
|
3
|
+
|
|
4
|
+
const intent = `random-number`
|
|
5
|
+
const asUser = {user: {id: 2003, username: `testuser`}, chat: {id: 2003}}
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
e2e.clearCalls()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
describe(`E2E: RandomNumberPage`, () => {
|
|
12
|
+
it(`show generated random number when navigating from /start`, async () => {
|
|
13
|
+
await e2e.sendText(`/start`, asUser)
|
|
14
|
+
e2e.clearCalls()
|
|
15
|
+
|
|
16
|
+
const responses = await e2e.sendCallbackQuery(intent, asUser)
|
|
17
|
+
const responsesId = await e2e.sendCallbackQuery(intent, {user: {id: 6001, username: `pengguna`, language_code: `id`}, chat: {id: 6001}})
|
|
18
|
+
|
|
19
|
+
expect(responses.length).toBeGreaterThan(0)
|
|
20
|
+
expect(responsesId.length).toBeGreaterThan(0)
|
|
21
|
+
const edit = responses[0]!
|
|
22
|
+
const editId = responsesId[0]!
|
|
23
|
+
expect(edit.body?.text).toContain(`Random Number`)
|
|
24
|
+
expect(editId.body?.text).toContain(`Angka Acak`)
|
|
25
|
+
expect(e2e.extractButtonLabels(edit)).toEqual(e2e.gkb(intent, `s1`, `inline`, {}))
|
|
26
|
+
expect(e2e.extractButtonLabels(editId)).toEqual(e2e.gkb(intent, `s1`, `inline`, {}, `id`))
|
|
27
|
+
})
|
|
28
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {join} from "node:path"
|
|
2
|
+
import {BotCore} from "../src/core/bot"
|
|
3
|
+
import {TemplateManager} from "gramstax"
|
|
4
|
+
|
|
5
|
+
export type {ITE2eApiCall, ITE2eUpdateOptions} from "gramstax"
|
|
6
|
+
|
|
7
|
+
const templateManager = new TemplateManager({path: [join(import.meta.dir, `../src/pages`)]})
|
|
8
|
+
export const bot = new BotCore({
|
|
9
|
+
deploy: `test`,
|
|
10
|
+
token: `test`,
|
|
11
|
+
log: false,
|
|
12
|
+
templateManager
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export const e2e = bot.e2e!
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {describe, it, expect, beforeEach} from "bun:test"
|
|
2
|
+
import {e2e} from "./setup"
|
|
3
|
+
|
|
4
|
+
const intent = `start`
|
|
5
|
+
const asUser = {user: {id: 2001, username: `testuser`}, chat: {id: 2001}}
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
e2e.clearCalls()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
describe(`E2E: StartPage`, () => {
|
|
12
|
+
it(`respond to /start command with welcome message and full keyboard`, async () => {
|
|
13
|
+
const responses = await e2e.sendText(`/start`, asUser)
|
|
14
|
+
const responsesId = await e2e.sendText(`/start`, {user: {id: 5001, username: `pengguna`, language_code: `id`}, chat: {id: 5001}})
|
|
15
|
+
|
|
16
|
+
expect(responses.length).toBeGreaterThan(0)
|
|
17
|
+
expect(responsesId.length).toBeGreaterThan(0)
|
|
18
|
+
const reply = responses[0]!
|
|
19
|
+
const replyId = responsesId[0]!
|
|
20
|
+
expect(reply.body?.text).toContain(`Welcome`)
|
|
21
|
+
expect(replyId.body?.text).toContain(`Selamat datang`)
|
|
22
|
+
expect(e2e.extractButtonLabels(reply)).toEqual(e2e.gkb(intent, `s1`, `inline`, {}))
|
|
23
|
+
expect(e2e.extractButtonLabels(replyId)).toEqual(e2e.gkb(intent, `s1`, `inline`, {}, `id`))
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it(`capture outbound requests in e2e.calls`, async () => {
|
|
27
|
+
await e2e.sendText(`/start`, asUser)
|
|
28
|
+
|
|
29
|
+
const sendMessageReq = e2e.lastCall(`sendMessage`)
|
|
30
|
+
expect(sendMessageReq).toBeDefined()
|
|
31
|
+
expect(sendMessageReq?.body?.text).toContain(`Welcome`)
|
|
32
|
+
})
|
|
33
|
+
})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {describe, it, expect, beforeEach} from "bun:test"
|
|
2
|
+
import {e2e} from "./setup"
|
|
3
|
+
|
|
4
|
+
const intent = `sum-number`
|
|
5
|
+
const asUser = {user: {id: 2004, username: `testuser`}, chat: {id: 2004}}
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
e2e.clearCalls()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
describe(`E2E: SumNumberPage`, () => {
|
|
12
|
+
it(`complete two-step sum flow and show result`, async () => {
|
|
13
|
+
await e2e.sendText(`/start`, asUser)
|
|
14
|
+
e2e.clearCalls()
|
|
15
|
+
|
|
16
|
+
const step1 = await e2e.sendCallbackQuery(intent, asUser)
|
|
17
|
+
expect(step1.length).toBeGreaterThan(0)
|
|
18
|
+
expect(step1[0]!.body?.text).toBe(e2e.gmsg(intent, `s1`, {}))
|
|
19
|
+
e2e.clearCalls()
|
|
20
|
+
|
|
21
|
+
const step2 = await e2e.sendText(`23`, asUser)
|
|
22
|
+
expect(step2.length).toBeGreaterThan(0)
|
|
23
|
+
expect(step2[0]!.body?.text).toBe(e2e.gmsg(intent, `s2`, {}))
|
|
24
|
+
e2e.clearCalls()
|
|
25
|
+
|
|
26
|
+
const result = await e2e.sendText(`10`, asUser)
|
|
27
|
+
expect(result.length).toBeGreaterThan(0)
|
|
28
|
+
expect(result[0]!.body?.text).toContain(`Result Sum`)
|
|
29
|
+
expect(result[0]!.body?.text).toContain(`33`)
|
|
30
|
+
expect(e2e.extractButtonLabels(result[0])).toEqual(e2e.gkb(intent, `s`, `inline`, {}))
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it(`re-prompt step 1 on invalid (non-numeric) input`, async () => {
|
|
34
|
+
await e2e.sendText(`/start`, asUser)
|
|
35
|
+
e2e.clearCalls()
|
|
36
|
+
|
|
37
|
+
await e2e.sendCallbackQuery(intent, asUser)
|
|
38
|
+
e2e.clearCalls()
|
|
39
|
+
|
|
40
|
+
const retry = await e2e.sendText(`not-a-number`, asUser)
|
|
41
|
+
expect(retry.length).toBeGreaterThan(0)
|
|
42
|
+
expect(retry[0]!.body?.text).toBe(e2e.gmsg(intent, `s1`, {}))
|
|
43
|
+
})
|
|
44
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {describe, it, expect, beforeEach} from "bun:test"
|
|
2
|
+
import {e2e} from "./setup"
|
|
3
|
+
|
|
4
|
+
const intent = `username-notfound`
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
e2e.clearCalls()
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
describe(`E2E: UserNameNotFoundPage`, () => {
|
|
11
|
+
it(`blocks user without username and shows username required page`, async () => {
|
|
12
|
+
const responses = await e2e.sendText(`/start`, {
|
|
13
|
+
user: {id: 77777, first_name: `NoUsername`},
|
|
14
|
+
chat: {id: 77777, type: `private`, first_name: `NoUsername`}
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
expect(responses.length).toBeGreaterThan(0)
|
|
18
|
+
const reply = responses[0]!
|
|
19
|
+
expect(reply).toBeDefined()
|
|
20
|
+
expect(reply.body?.text).toBe(e2e.gmsg(intent, `s1`, {}))
|
|
21
|
+
expect(reply.body?.text).toContain(`Username Required`)
|
|
22
|
+
expect(e2e.extractButtonLabels(reply)).toEqual(e2e.gkb(intent, `s1`, `inline`, {}))
|
|
23
|
+
})
|
|
24
|
+
})
|
|
@@ -12,7 +12,7 @@ export class BotCore extends Gramstax {
|
|
|
12
12
|
return true
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
async hookErrorPage(ctx: CtxCore, data: Parameters<NonNullable<Gramstax[`hookErrorPage`]>>[1]) {
|
|
15
|
+
async hookErrorPage(ctx: CtxCore, data: NonNullable<Parameters<NonNullable<Gramstax[`hookErrorPage`]>>[1]>) {
|
|
16
16
|
await new GeneralStatusPage(ctx).showError(`${data.listenerName}:hookErrorPage`, data.error, data.isEdit)
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -54,18 +54,18 @@ export class GeneralStatusPage extends PageBase {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
static template = `
|
|
57
|
-
<base id="${Base.dynamic}">
|
|
57
|
+
<g-base id="${Base.dynamic}">
|
|
58
58
|
${this.kbk.home()}
|
|
59
|
-
<message>
|
|
59
|
+
<g-message>
|
|
60
60
|
<b>{~isNotFound ? "🔎 Route Not Found" : "❌ Error Occurred"~}</b>
|
|
61
61
|
|
|
62
62
|
{~isNotFound ? "The requested route does not exist or is unavailable." : "Something went wrong while processing your request. Please try again."~}
|
|
63
|
-
</message>
|
|
64
|
-
<message lang="id">
|
|
63
|
+
</g-message>
|
|
64
|
+
<g-message lang="id">
|
|
65
65
|
<b>{~isNotFound ? "🔎 Rute Tidak Ditemukan" : "❌ Terjadi Kesalahan"~}</b>
|
|
66
66
|
|
|
67
67
|
{~isNotFound ? "Rute yang diminta tidak tersedia atau tidak ada." : "Terjadi kesalahan saat memproses permintaan. Silakan coba lagi."~}
|
|
68
|
-
</message>
|
|
69
|
-
</base>
|
|
68
|
+
</g-message>
|
|
69
|
+
</g-base>
|
|
70
70
|
`
|
|
71
71
|
}
|
|
@@ -32,24 +32,38 @@ export class HelpPage extends PageBase {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
static template = `
|
|
35
|
-
<base id="${Base.step1}">
|
|
35
|
+
<g-base id="${Base.step1}">
|
|
36
36
|
${this.kbk.back()}
|
|
37
|
-
<message>
|
|
37
|
+
<g-message>
|
|
38
38
|
<b>🆘 Help Message</b>
|
|
39
39
|
|
|
40
40
|
Use this command to get help and instructions.
|
|
41
41
|
|
|
42
42
|
<b>🎰 Random Number</b> - Generate a random number
|
|
43
43
|
<b>➕ Sum Number</b> - Add two numbers together
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
|
|
45
|
+
<details><summary>📋 More commands</summary>
|
|
46
|
+
/start - Show welcome message
|
|
47
|
+
/help - Show this help
|
|
48
|
+
/random - Generate a random number
|
|
49
|
+
/sum - Add two numbers
|
|
50
|
+
</details>
|
|
51
|
+
</g-message>
|
|
52
|
+
<g-message lang="id">
|
|
46
53
|
<b>🆘 Pesan Bantuan</b>
|
|
47
54
|
|
|
48
55
|
Gunakan perintah ini untuk melihat bantuan dan petunjuk.
|
|
49
56
|
|
|
50
57
|
<b>🎰 Angka Acak</b> - Menghasilkan angka secara acak
|
|
51
58
|
<b>➕ Jumlah Angka</b> - Menjumlahkan dua angka
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
|
|
60
|
+
<details><summary>📋 Perintah lainnya</summary>
|
|
61
|
+
/start - Tampilkan pesan selamat datang
|
|
62
|
+
/help - Tampilkan bantuan ini
|
|
63
|
+
/random - Hasilkan angka acak
|
|
64
|
+
/sum - Jumlahkan dua angka
|
|
65
|
+
</details>
|
|
66
|
+
</g-message>
|
|
67
|
+
</g-base>
|
|
54
68
|
`
|
|
55
69
|
}
|
|
@@ -36,23 +36,23 @@ export class RandomNumberPage extends PageBase {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
static template = `
|
|
39
|
-
<base id="${Base.step1}">
|
|
40
|
-
<keyboard>🎲 Shake, ${this.kbl.back.default}</keyboard>
|
|
41
|
-
<keyboard lang="id">🎲 Kocok, ${this.kbl.back.id}</keyboard>
|
|
42
|
-
<message>
|
|
39
|
+
<g-base id="${Base.step1}">
|
|
40
|
+
<g-keyboard>🎲 Shake, ${this.kbl.back.default}</g-keyboard>
|
|
41
|
+
<g-keyboard lang="id">🎲 Kocok, ${this.kbl.back.id}</g-keyboard>
|
|
42
|
+
<g-message>
|
|
43
43
|
<b>🎰 Random Number</b>
|
|
44
44
|
|
|
45
45
|
{{result}}
|
|
46
46
|
|
|
47
47
|
Generated automatically based SCALE 18.
|
|
48
|
-
</message>
|
|
49
|
-
<message lang="id">
|
|
48
|
+
</g-message>
|
|
49
|
+
<g-message lang="id">
|
|
50
50
|
<b>🎰 Angka Acak</b>
|
|
51
51
|
|
|
52
52
|
{{result}}
|
|
53
53
|
|
|
54
54
|
Dihasilkan secara otomatis berdasarkan SCALE 18.
|
|
55
|
-
</message>
|
|
56
|
-
</base>
|
|
55
|
+
</g-message>
|
|
56
|
+
</g-base>
|
|
57
57
|
`
|
|
58
58
|
}
|
|
@@ -39,10 +39,10 @@ export class StartPage extends PageBase {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
static template = `
|
|
42
|
-
<base id="${Base.step1}">
|
|
43
|
-
<keyboard>🎰 Random Number, ➕ Sum Number, 🆘 Help</keyboard>
|
|
44
|
-
<keyboard lang="id">🎰 Angka Acak, ➕ Jumlah Angka, 🆘 Bantuan</keyboard>
|
|
45
|
-
<message>
|
|
42
|
+
<g-base id="${Base.step1}">
|
|
43
|
+
<g-keyboard>🎰 Random Number, ➕ Sum Number, 🆘 Help</g-keyboard>
|
|
44
|
+
<g-keyboard lang="id">🎰 Angka Acak, ➕ Jumlah Angka, 🆘 Bantuan</g-keyboard>
|
|
45
|
+
<g-message>
|
|
46
46
|
<b>👋 Welcome to the bot {{userName}}</b>
|
|
47
47
|
|
|
48
48
|
This bot was initially generated using Gramstax as the base setup and starting structure. It provides a clean foundation that can be extended with custom features, commands, and integrations as needed.
|
|
@@ -50,8 +50,12 @@ export class StartPage extends PageBase {
|
|
|
50
50
|
<b>🎰 Random Number</b> - Generate a random number
|
|
51
51
|
<b>➕ Sum Number</b> - Add two numbers together
|
|
52
52
|
<b>❓ Help</b> - View help and available commands
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
|
|
54
|
+
<tg-spoiler>🎁 Hidden tip: try /help for a surprise</tg-spoiler>
|
|
55
|
+
|
|
56
|
+
<a href="https://github.com/gramstax/gramstax">📚 Documentation</a>
|
|
57
|
+
</g-message>
|
|
58
|
+
<g-message lang="id">
|
|
55
59
|
<b>👋 Selamat datang di bot {{userName}}</b>
|
|
56
60
|
|
|
57
61
|
Bot ini dibuat menggunakan Gramstax sebagai generator awal dan struktur dasar. Dari sini, bot dapat dikembangkan lebih lanjut dengan fitur, perintah, dan integrasi sesuai kebutuhan.
|
|
@@ -59,7 +63,11 @@ export class StartPage extends PageBase {
|
|
|
59
63
|
<b>🎰 Angka Acak</b> - Menghasilkan angka secara acak
|
|
60
64
|
<b>➕ Jumlah Angka</b> - Menjumlahkan dua angka
|
|
61
65
|
<b>❓ Bantuan</b> - Lihat bantuan dan daftar perintah
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
|
|
67
|
+
<tg-spoiler>🎁 Tips tersembunyi: coba /help untuk kejutan</tg-spoiler>
|
|
68
|
+
|
|
69
|
+
<a href="https://github.com/gramstax/gramstax">📚 Dokumentasi</a>
|
|
70
|
+
</g-message>
|
|
71
|
+
</g-base>
|
|
64
72
|
`
|
|
65
73
|
}
|
|
@@ -12,8 +12,8 @@ class Helper {
|
|
|
12
12
|
return (ctx.temp.session?.params as {step?: (typeof Base)[keyof typeof Base]; left?: number} | undefined) ?? {}
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
static kb(ctx: SumNumberPage,
|
|
16
|
-
return ctx.inlineKeyboardT([StartPage], `1`,
|
|
15
|
+
static kb(ctx: SumNumberPage, AGBaseId: string) {
|
|
16
|
+
return ctx.inlineKeyboardT([StartPage], `1`, AGBaseId)
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -86,46 +86,46 @@ export class SumNumberPage extends PageBase {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
static template = `
|
|
89
|
-
<base id="${Base.step1}">
|
|
89
|
+
<g-base id="${Base.step1}">
|
|
90
90
|
${this.kbk.cancel()}
|
|
91
|
-
<message>
|
|
91
|
+
<g-message>
|
|
92
92
|
<b>📚 Input Left Number</b>
|
|
93
93
|
|
|
94
94
|
Example: 23
|
|
95
|
-
</message>
|
|
96
|
-
<message lang="id">
|
|
95
|
+
</g-message>
|
|
96
|
+
<g-message lang="id">
|
|
97
97
|
<b>📚 Masukan Angka Kiri</b>
|
|
98
98
|
|
|
99
99
|
Contoh: 23
|
|
100
|
-
</message>
|
|
101
|
-
</base>
|
|
100
|
+
</g-message>
|
|
101
|
+
</g-base>
|
|
102
102
|
|
|
103
|
-
<base id="${Base.step2}">
|
|
103
|
+
<g-base id="${Base.step2}">
|
|
104
104
|
${this.kbk.cancel()}
|
|
105
|
-
<message>
|
|
105
|
+
<g-message>
|
|
106
106
|
<b>📚 Input Right Number</b>
|
|
107
107
|
|
|
108
108
|
Example: 10
|
|
109
|
-
</message>
|
|
110
|
-
<message lang="id">
|
|
109
|
+
</g-message>
|
|
110
|
+
<g-message lang="id">
|
|
111
111
|
<b>📚 Masukan Angka Kanan</b>
|
|
112
112
|
|
|
113
113
|
Contoh: 10
|
|
114
|
-
</message>
|
|
115
|
-
</base>
|
|
114
|
+
</g-message>
|
|
115
|
+
</g-base>
|
|
116
116
|
|
|
117
|
-
<base id="${Base.success}">
|
|
117
|
+
<g-base id="${Base.success}">
|
|
118
118
|
${this.kbk.home()}
|
|
119
|
-
<message>
|
|
119
|
+
<g-message>
|
|
120
120
|
<b>📚 Result Sum</b>
|
|
121
121
|
|
|
122
122
|
{{result}}
|
|
123
|
-
</message>
|
|
124
|
-
<message lang="id">
|
|
123
|
+
</g-message>
|
|
124
|
+
<g-message lang="id">
|
|
125
125
|
<b>📚 Hasil Sum</b>
|
|
126
126
|
|
|
127
127
|
{{result}}
|
|
128
|
-
</message>
|
|
129
|
-
</base>
|
|
128
|
+
</g-message>
|
|
129
|
+
</g-base>
|
|
130
130
|
`
|
|
131
131
|
}
|
|
@@ -28,18 +28,18 @@ export class UserNameNotFoundPage extends PageBase {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
static template = `
|
|
31
|
-
<base id="${Base.step1}">
|
|
31
|
+
<g-base id="${Base.step1}">
|
|
32
32
|
${this.kbk.home()}
|
|
33
|
-
<message>
|
|
33
|
+
<g-message>
|
|
34
34
|
<b>⚠️ Username Required</b>
|
|
35
35
|
|
|
36
36
|
Please set the username first before using the bot.
|
|
37
|
-
</message>
|
|
38
|
-
<message lang="id">
|
|
37
|
+
</g-message>
|
|
38
|
+
<g-message lang="id">
|
|
39
39
|
<b>⚠️ Username Diperlukan</b>
|
|
40
40
|
|
|
41
41
|
Tolong set username dulu sebelum memakai bot.
|
|
42
|
-
</message>
|
|
43
|
-
</base>
|
|
42
|
+
</g-message>
|
|
43
|
+
</g-base>
|
|
44
44
|
`
|
|
45
45
|
}
|
package/package.json
CHANGED
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* E2E Test Flow for Template Bot
|
|
3
|
-
*
|
|
4
|
-
* Setup:
|
|
5
|
-
* 1. MockTelegramServer starts on random port → apiRoot captured
|
|
6
|
-
* 2. BotCore initialized with webhook:http://127.0.0.1:3009/webhook/e2e-template
|
|
7
|
-
* 3. CacheExternal (memory) shared across tests, cleared in beforeEach
|
|
8
|
-
*
|
|
9
|
-
* Test Flow:
|
|
10
|
-
* T1. /start → Welcome message + inline keyboard (Random Number, Sum Number, Help)
|
|
11
|
-
* T2. /start → callback:help → editMessage → Help Message + Back button
|
|
12
|
-
* T3. /start → callback:random-number → editMessage → Random Number + Shake/Back buttons
|
|
13
|
-
* T4. /start → callback:random-number → callback:random-number → editMessage → refreshed number
|
|
14
|
-
* T5. /start → callback:sum-number → text:23 → text:10 → Result Sum: 33 + Home button
|
|
15
|
-
* T6. /start → callback:sum-number → text:not-a-number → re-prompt Input Left Number
|
|
16
|
-
* T7. /start → callback:nonexistent_action → graceful handling (no crash)
|
|
17
|
-
* T8. /start → verify sendMessage request captured in mockServer.requests
|
|
18
|
-
* T9. /start → callback:help → callback:random-number → sequential chain
|
|
19
|
-
* T10. sendPhoto → Route Not Found (no handler for photo)
|
|
20
|
-
* T11. /start with custom user/chat → custom ID propagation verified
|
|
21
|
-
* T12. /start without username → Username Required page (UserGuard blocks)
|
|
22
|
-
* T13. /start → callback:help → callback:random-number → keyboard structure validation (text + callback_data)
|
|
23
|
-
*
|
|
24
|
-
* Cleanup:
|
|
25
|
-
* - botCore.stop() + mockServer.stop() in afterAll
|
|
26
|
-
* - cacheSession.clear() + mockServer.clearRequests() in beforeEach
|
|
27
|
-
*/
|
|
28
|
-
import {join} from "node:path"
|
|
29
|
-
import {BotCore} from "../src/core/bot"
|
|
30
|
-
import {TemplateManager, CacheExternal} from "gramstax"
|
|
31
|
-
import {MockTelegramServer} from "gramstax"
|
|
32
|
-
import {describe, it, expect, beforeAll, afterAll, beforeEach} from "bun:test"
|
|
33
|
-
|
|
34
|
-
let mockServer: MockTelegramServer
|
|
35
|
-
let botCore: BotCore
|
|
36
|
-
let cacheSession: CacheExternal
|
|
37
|
-
|
|
38
|
-
const USER_1 = {
|
|
39
|
-
responseTimeoutMs: undefined, // 9000,
|
|
40
|
-
user: {
|
|
41
|
-
id: 123456789,
|
|
42
|
-
first_name: `Test`,
|
|
43
|
-
last_name: `user`,
|
|
44
|
-
username: `testuser`,
|
|
45
|
-
language_code: `en`,
|
|
46
|
-
is_bot: false
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
beforeAll(async () => {
|
|
51
|
-
mockServer = new MockTelegramServer({log: false})
|
|
52
|
-
await mockServer.start()
|
|
53
|
-
|
|
54
|
-
const templateManager = new TemplateManager({path: [join(import.meta.dir, `../src/pages`)]})
|
|
55
|
-
const cwd = process.cwd()
|
|
56
|
-
process.chdir(join(import.meta.dir, `..`))
|
|
57
|
-
|
|
58
|
-
cacheSession = new CacheExternal(`memory`)
|
|
59
|
-
|
|
60
|
-
botCore = new BotCore({
|
|
61
|
-
deploy: `webhook:http://127.0.0.1:3009/webhook/e2e-template`,
|
|
62
|
-
token: `test-token`,
|
|
63
|
-
log: false,
|
|
64
|
-
templateManager,
|
|
65
|
-
cacheSession,
|
|
66
|
-
botConfig: {client: {apiRoot: mockServer.apiRoot}}
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
process.chdir(cwd)
|
|
70
|
-
|
|
71
|
-
const startedAt = Date.now()
|
|
72
|
-
while (!mockServer.webhookUrl && Date.now() - startedAt < 5000) {
|
|
73
|
-
await new Promise((r) => setTimeout(r, 50))
|
|
74
|
-
}
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
afterAll(async () => {
|
|
78
|
-
await botCore.stop()
|
|
79
|
-
await mockServer.stop()
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
beforeEach(async () => {
|
|
83
|
-
mockServer.clearRequests()
|
|
84
|
-
await cacheSession.clear()
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
describe(`E2E: Template bot via MockTelegramServer (webhook)`, () => {
|
|
88
|
-
it(`responds to /start command with welcome message and navigation buttons`, async () => {
|
|
89
|
-
const responses = await mockServer.sendText(`/start`, USER_1)
|
|
90
|
-
|
|
91
|
-
expect(responses.length).toBeGreaterThan(0)
|
|
92
|
-
const reply = responses[0]!
|
|
93
|
-
expect(reply).toBeDefined()
|
|
94
|
-
expect(reply!.body?.text).toContain(`Welcome to the bot`)
|
|
95
|
-
|
|
96
|
-
const keyboard = reply!.body?.reply_markup?.inline_keyboard
|
|
97
|
-
expect(Array.isArray(keyboard)).toBe(true)
|
|
98
|
-
expect(keyboard.length).toBeGreaterThan(0)
|
|
99
|
-
|
|
100
|
-
const allButtons = keyboard.flat()
|
|
101
|
-
const buttonLabels = allButtons.map((b: any) => b.text)
|
|
102
|
-
expect(buttonLabels).toContain(`🎰 Random Number`)
|
|
103
|
-
expect(buttonLabels).toContain(`➕ Sum Number`)
|
|
104
|
-
expect(buttonLabels).toContain(`🆘 Help`)
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it(`navigates to Help page via callback and shows back button`, async () => {
|
|
108
|
-
await mockServer.sendText(`/start`, USER_1)
|
|
109
|
-
mockServer.clearRequests()
|
|
110
|
-
|
|
111
|
-
const responses = await mockServer.sendCallbackQuery(`help`, USER_1)
|
|
112
|
-
|
|
113
|
-
expect(responses.length).toBeGreaterThan(0)
|
|
114
|
-
const edit = responses[0]!
|
|
115
|
-
expect(edit!.body?.text).toContain(`Help Message`)
|
|
116
|
-
|
|
117
|
-
const keyboard = edit!.body?.reply_markup?.inline_keyboard
|
|
118
|
-
expect(Array.isArray(keyboard)).toBe(true)
|
|
119
|
-
const firstButton = keyboard[0]?.[0]
|
|
120
|
-
expect(firstButton?.text).toContain(`Back`)
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
it(`navigates to Random Number page via callback and shows generated number`, async () => {
|
|
124
|
-
await mockServer.sendText(`/start`, USER_1)
|
|
125
|
-
mockServer.clearRequests()
|
|
126
|
-
|
|
127
|
-
const responses = await mockServer.sendCallbackQuery(`random-number`, USER_1)
|
|
128
|
-
|
|
129
|
-
expect(responses.length).toBeGreaterThan(0)
|
|
130
|
-
const edit = responses[0]!
|
|
131
|
-
expect(edit!.body?.text).toContain(`Random Number`)
|
|
132
|
-
|
|
133
|
-
const keyboard = edit!.body?.reply_markup?.inline_keyboard
|
|
134
|
-
expect(Array.isArray(keyboard)).toBe(true)
|
|
135
|
-
const allButtons = keyboard.flat()
|
|
136
|
-
const buttonLabels = allButtons.map((b: any) => b.text)
|
|
137
|
-
expect(buttonLabels).toContain(`🎲 Shake`)
|
|
138
|
-
expect(buttonLabels).toContain(`⬅️ Back`)
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
it(`refreshes random number via Shake callback`, async () => {
|
|
142
|
-
await mockServer.sendText(`/start`, USER_1)
|
|
143
|
-
mockServer.clearRequests()
|
|
144
|
-
|
|
145
|
-
await mockServer.sendCallbackQuery(`random-number`, USER_1)
|
|
146
|
-
mockServer.clearRequests()
|
|
147
|
-
|
|
148
|
-
const responses = await mockServer.sendCallbackQuery(`random-number`, USER_1)
|
|
149
|
-
|
|
150
|
-
expect(responses.length).toBeGreaterThan(0)
|
|
151
|
-
const edit = responses[0]!
|
|
152
|
-
expect(edit!.body?.text).toContain(`Random Number`)
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
it(`completes Sum Number two-step session flow`, async () => {
|
|
156
|
-
await mockServer.sendText(`/start`, USER_1)
|
|
157
|
-
mockServer.clearRequests()
|
|
158
|
-
|
|
159
|
-
const sumResponses = await mockServer.sendCallbackQuery(`sum-number`, USER_1)
|
|
160
|
-
expect(sumResponses.length).toBeGreaterThan(0)
|
|
161
|
-
expect(sumResponses[0]!.body?.text).toContain(`Input Left Number`)
|
|
162
|
-
|
|
163
|
-
mockServer.clearRequests()
|
|
164
|
-
|
|
165
|
-
const leftResponses = await mockServer.sendText(`23`, USER_1)
|
|
166
|
-
expect(leftResponses.length).toBeGreaterThan(0)
|
|
167
|
-
expect(leftResponses[0]!.body?.text).toContain(`Input Right Number`)
|
|
168
|
-
|
|
169
|
-
mockServer.clearRequests()
|
|
170
|
-
|
|
171
|
-
const resultResponses = await mockServer.sendText(`10`, USER_1)
|
|
172
|
-
expect(resultResponses.length).toBeGreaterThan(0)
|
|
173
|
-
expect(resultResponses[0]!.body?.text).toContain(`Result Sum`)
|
|
174
|
-
expect(resultResponses[0]!.body?.text).toContain(`33`)
|
|
175
|
-
|
|
176
|
-
const keyboard = resultResponses[0]!.body?.reply_markup?.inline_keyboard
|
|
177
|
-
expect(Array.isArray(keyboard)).toBe(true)
|
|
178
|
-
const firstButton = keyboard[0]?.[0]
|
|
179
|
-
expect(firstButton?.text).toContain(`Home`)
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
it(`handles invalid input in Sum Number gracefully`, async () => {
|
|
183
|
-
await mockServer.sendText(`/start`, USER_1)
|
|
184
|
-
mockServer.clearRequests()
|
|
185
|
-
|
|
186
|
-
await mockServer.sendCallbackQuery(`sum-number`, USER_1)
|
|
187
|
-
mockServer.clearRequests()
|
|
188
|
-
|
|
189
|
-
const leftResponses = await mockServer.sendText(`not-a-number`, USER_1)
|
|
190
|
-
expect(leftResponses.length).toBeGreaterThan(0)
|
|
191
|
-
expect(leftResponses[0]!.body?.text).toContain(`Input Left Number`)
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
it(`shows route not found for unrecognized callback`, async () => {
|
|
195
|
-
await mockServer.sendText(`/start`, USER_1)
|
|
196
|
-
mockServer.clearRequests()
|
|
197
|
-
|
|
198
|
-
const responses = await mockServer.sendCallbackQuery(`nonexistent_action_${Math.random()}`, USER_1)
|
|
199
|
-
expect(responses.length).toBeGreaterThanOrEqual(0)
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
it(`captures outbound requests in mockServer.requests`, async () => {
|
|
203
|
-
const responses = await mockServer.sendText(`/start`, USER_1)
|
|
204
|
-
|
|
205
|
-
expect(responses.length).toBeGreaterThan(0)
|
|
206
|
-
const sendMessageReq = mockServer.lastRequest(`sendMessage`)
|
|
207
|
-
expect(sendMessageReq).toBeDefined()
|
|
208
|
-
expect(sendMessageReq?.body?.text).toContain(`Welcome to the bot`)
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
it(`handles multiple sequential interactions`, async () => {
|
|
212
|
-
const res1 = await mockServer.sendText(`/start`, USER_1)
|
|
213
|
-
expect(res1[0]?.body?.text).toContain(`Welcome to the bot`)
|
|
214
|
-
|
|
215
|
-
mockServer.clearRequests()
|
|
216
|
-
|
|
217
|
-
const res2 = await mockServer.sendCallbackQuery(`help`, USER_1)
|
|
218
|
-
expect(res2[0]?.body?.text).toContain(`Help Message`)
|
|
219
|
-
|
|
220
|
-
mockServer.clearRequests()
|
|
221
|
-
|
|
222
|
-
const res3 = await mockServer.sendCallbackQuery(`random-number`, USER_1)
|
|
223
|
-
expect(res3[0]?.body?.text).toContain(`Random Number`)
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
it(`sendPhoto triggers a response`, async () => {
|
|
227
|
-
const responses = await mockServer.sendPhoto({caption: `test photo`, ...USER_1})
|
|
228
|
-
|
|
229
|
-
expect(responses.length).toBeGreaterThan(0)
|
|
230
|
-
expect(responses[0]?.body?.text).toContain(`Route Not Found`)
|
|
231
|
-
})
|
|
232
|
-
|
|
233
|
-
it(`supports custom user and chat options`, async () => {
|
|
234
|
-
const responses = await mockServer.sendText(`/start`, {
|
|
235
|
-
user: {id: 99999, first_name: `CustomUser`, username: `customuser`},
|
|
236
|
-
chat: {id: 88888, type: `private`, first_name: `CustomChat`}
|
|
237
|
-
})
|
|
238
|
-
|
|
239
|
-
expect(responses.length).toBeGreaterThan(0)
|
|
240
|
-
expect(responses[0]?.body?.text).toContain(`Welcome to the bot`)
|
|
241
|
-
|
|
242
|
-
const lastUpdate = mockServer.updates[mockServer.updates.length - 1]
|
|
243
|
-
expect(lastUpdate).toBeDefined()
|
|
244
|
-
const msg = (lastUpdate as any)?.message
|
|
245
|
-
expect(msg?.from?.id).toBe(99999)
|
|
246
|
-
expect(msg?.chat?.id).toBe(88888)
|
|
247
|
-
})
|
|
248
|
-
|
|
249
|
-
it(`blocks user without username and shows username required page`, async () => {
|
|
250
|
-
const responses = await mockServer.sendText(`/start`, {
|
|
251
|
-
user: {id: 77777, first_name: `NoUsername`},
|
|
252
|
-
chat: {id: 77777, type: `private`, first_name: `NoUsername`}
|
|
253
|
-
})
|
|
254
|
-
|
|
255
|
-
expect(responses.length).toBeGreaterThan(0)
|
|
256
|
-
const reply = responses[0]!
|
|
257
|
-
expect(reply.body?.text).toContain(`Username Required`)
|
|
258
|
-
expect(reply.body?.text).toContain(`set the username first`)
|
|
259
|
-
})
|
|
260
|
-
|
|
261
|
-
it(`validates inline_keyboard structure for all navigation pages`, async () => {
|
|
262
|
-
const res1 = await mockServer.sendText(`/start`, USER_1)
|
|
263
|
-
expect(Array.isArray(res1[0]?.body?.reply_markup?.inline_keyboard)).toBe(true)
|
|
264
|
-
|
|
265
|
-
mockServer.clearRequests()
|
|
266
|
-
|
|
267
|
-
const res2 = await mockServer.sendCallbackQuery(`help`, USER_1)
|
|
268
|
-
const kb2 = res2[0]?.body?.reply_markup?.inline_keyboard
|
|
269
|
-
expect(Array.isArray(kb2)).toBe(true)
|
|
270
|
-
expect(kb2!.length).toBeGreaterThan(0)
|
|
271
|
-
expect(kb2![0]?.[0]?.text).toBeDefined()
|
|
272
|
-
expect(kb2![0]?.[0]?.callback_data).toBeDefined()
|
|
273
|
-
|
|
274
|
-
mockServer.clearRequests()
|
|
275
|
-
|
|
276
|
-
const res3 = await mockServer.sendCallbackQuery(`random-number`, USER_1)
|
|
277
|
-
const kb3 = res3[0]?.body?.reply_markup?.inline_keyboard
|
|
278
|
-
expect(Array.isArray(kb3)).toBe(true)
|
|
279
|
-
expect(kb3!.length).toBeGreaterThan(0)
|
|
280
|
-
for (const row of kb3!) {
|
|
281
|
-
for (const btn of row) {
|
|
282
|
-
expect(btn?.text).toBeDefined()
|
|
283
|
-
expect(btn?.callback_data).toBeDefined()
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
})
|
|
287
|
-
})
|