create-gramstax 0.13.0 → 0.14.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/templates/e2e/{start.test.ts → start/index.test.ts} +1 -1
- package/dist/src/templates/e2e/start/random/index.test.ts +28 -0
- package/dist/src/templates/e2e/{random-number.test.ts → start/random/number.test.ts} +9 -6
- package/dist/src/templates/e2e/start/random/text.test.ts +31 -0
- package/dist/src/templates/e2e/{general-status.test.ts → status.test.ts} +2 -2
- package/dist/src/templates/package.json +2 -2
- package/dist/src/templates/scripts/check-e2e-mirror.ts +148 -0
- package/dist/src/templates/scripts/generate-e2e-structure.ts +220 -0
- package/dist/src/templates/src/core/bot.ts +3 -3
- package/dist/src/templates/src/guards/user.ts +2 -2
- package/dist/src/templates/src/pages/{start.ts → start/index.ts} +8 -8
- package/dist/src/templates/src/pages/start/random/index.ts +38 -0
- package/dist/src/templates/src/pages/{random-number.ts → start/random/number.ts} +3 -7
- package/dist/src/templates/src/pages/start/random/text.ts +98 -0
- package/dist/src/templates/src/pages/{general-status.ts → status.ts} +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {describe, it, expect, beforeEach} from "bun:test"
|
|
2
|
+
import {e2e} from "../../setup"
|
|
3
|
+
|
|
4
|
+
const intent = `start/random`
|
|
5
|
+
const asUser = {user: {id: 9001, username: `testuser`}, chat: {id: 9001}}
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
e2e.clearCalls()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
describe(`E2E: StartRandomPage (hub)`, () => {
|
|
12
|
+
it(`show hub menu 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: 9002, username: `pengguna`, language_code: `id`}, chat: {id: 9002}})
|
|
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?.rich_message.html).toContain(`Random Generator`)
|
|
24
|
+
expect(editId.body?.rich_message.html).toContain(`Generator Acak`)
|
|
25
|
+
expect(e2e.extractButtonLabels(edit)).toEqual(e2e.gkb(intent, `default`, `inline`, {}))
|
|
26
|
+
expect(e2e.extractButtonLabels(editId)).toEqual(e2e.gkb(intent, `default`, `inline`, {}, `id`))
|
|
27
|
+
})
|
|
28
|
+
})
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import {describe, it, expect, beforeEach} from "bun:test"
|
|
2
|
-
import {e2e} from "
|
|
2
|
+
import {e2e} from "../../setup"
|
|
3
3
|
|
|
4
|
-
const intent = `random
|
|
5
|
-
const asUser = {user: {id:
|
|
4
|
+
const intent = `start/random/number`
|
|
5
|
+
const asUser = {user: {id: 8001, username: `testuser`}, chat: {id: 8001}}
|
|
6
6
|
|
|
7
7
|
beforeEach(() => {
|
|
8
8
|
e2e.clearCalls()
|
|
9
9
|
})
|
|
10
10
|
|
|
11
|
-
describe(`E2E:
|
|
12
|
-
it(`show generated random number when navigating from
|
|
11
|
+
describe(`E2E: StartRandomNumberPage`, () => {
|
|
12
|
+
it(`show generated random number when navigating from hub`, async () => {
|
|
13
13
|
await e2e.sendText(`/start`, asUser)
|
|
14
14
|
e2e.clearCalls()
|
|
15
15
|
|
|
16
|
+
await e2e.sendCallbackQuery(`start/random`, asUser)
|
|
17
|
+
e2e.clearCalls()
|
|
18
|
+
|
|
16
19
|
const responses = await e2e.sendCallbackQuery(intent, asUser)
|
|
17
|
-
const responsesId = await e2e.sendCallbackQuery(intent, {user: {id:
|
|
20
|
+
const responsesId = await e2e.sendCallbackQuery(intent, {user: {id: 8002, username: `pengguna`, language_code: `id`}, chat: {id: 8002}})
|
|
18
21
|
|
|
19
22
|
expect(responses.length).toBeGreaterThan(0)
|
|
20
23
|
expect(responsesId.length).toBeGreaterThan(0)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {describe, it, expect, beforeEach} from "bun:test"
|
|
2
|
+
import {e2e} from "../../setup"
|
|
3
|
+
|
|
4
|
+
const intent = `start/random/text`
|
|
5
|
+
const asUser = {user: {id: 10001, username: `testuser`}, chat: {id: 10001}}
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
e2e.clearCalls()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
describe(`E2E: StartRandomTextPage`, () => {
|
|
12
|
+
it(`show random text when navigating from hub`, async () => {
|
|
13
|
+
await e2e.sendText(`/start`, asUser)
|
|
14
|
+
e2e.clearCalls()
|
|
15
|
+
|
|
16
|
+
await e2e.sendCallbackQuery(`start/random`, asUser)
|
|
17
|
+
e2e.clearCalls()
|
|
18
|
+
|
|
19
|
+
const responses = await e2e.sendCallbackQuery(intent, asUser)
|
|
20
|
+
const responsesId = await e2e.sendCallbackQuery(intent, {user: {id: 10002, username: `pengguna`, language_code: `id`}, chat: {id: 10002}})
|
|
21
|
+
|
|
22
|
+
expect(responses.length).toBeGreaterThan(0)
|
|
23
|
+
expect(responsesId.length).toBeGreaterThan(0)
|
|
24
|
+
const edit = responses[0]!
|
|
25
|
+
const editId = responsesId[0]!
|
|
26
|
+
expect(edit.body?.text).toContain(`Random Text`)
|
|
27
|
+
expect(editId.body?.text).toContain(`Teks Acak`)
|
|
28
|
+
expect(e2e.extractButtonLabels(edit)).toEqual(e2e.gkb(intent, `default`, `inline`, {}))
|
|
29
|
+
expect(e2e.extractButtonLabels(editId)).toEqual(e2e.gkb(intent, `default`, `inline`, {}, `id`))
|
|
30
|
+
})
|
|
31
|
+
})
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import {describe, it, expect, beforeEach} from "bun:test"
|
|
2
2
|
import {e2e} from "./setup"
|
|
3
3
|
|
|
4
|
-
const intent = `
|
|
4
|
+
const intent = `status`
|
|
5
5
|
const asUser = {user: {id: 8001, username: `testuser`}, chat: {id: 8001}}
|
|
6
6
|
|
|
7
7
|
beforeEach(() => {
|
|
8
8
|
e2e.clearCalls()
|
|
9
9
|
})
|
|
10
10
|
|
|
11
|
-
describe(`E2E:
|
|
11
|
+
describe(`E2E: StatusPage`, () => {
|
|
12
12
|
it(`show 'Route not found' for unrecognized callback query`, async () => {
|
|
13
13
|
await e2e.sendText(`/start`, asUser)
|
|
14
14
|
e2e.clearCalls()
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-e2e-mirror.ts
|
|
3
|
+
*
|
|
4
|
+
* Ensures the e2e/ directory structure mirrors src/pages exactly.
|
|
5
|
+
*
|
|
6
|
+
* Convention:
|
|
7
|
+
* src/pages/foo.ts → e2e/foo.test.ts (same relative path, same name)
|
|
8
|
+
* src/pages/foo/bar.ts → e2e/foo/bar.test.ts (same relative path)
|
|
9
|
+
* src/pages/foo/index.ts → e2e/foo/index.test.ts (same relative path)
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* bun run scripts/check-e2e-mirror.ts
|
|
13
|
+
* → Check this project (src/pages vs e2e)
|
|
14
|
+
*
|
|
15
|
+
* bun run scripts/check-e2e-mirror.ts --path ./my-app
|
|
16
|
+
* → Check another project (pages: ./my-app/src/pages, e2e: ./my-app/e2e)
|
|
17
|
+
*
|
|
18
|
+
* bun run scripts/check-e2e-mirror.ts --path ./my-app --fix
|
|
19
|
+
* → Check + auto-create missing e2e test files
|
|
20
|
+
*
|
|
21
|
+
* Exit code: 0 if all OK, 1 if any mismatch.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import {existsSync, readdirSync, mkdirSync, writeFileSync} from "node:fs"
|
|
25
|
+
import {join, dirname} from "node:path"
|
|
26
|
+
|
|
27
|
+
// ── Parse CLI ─────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
const args = process.argv.slice(2)
|
|
30
|
+
|
|
31
|
+
const FIX_MODE = args.includes(`--fix`)
|
|
32
|
+
|
|
33
|
+
const pathIndex = args.indexOf(`--path`)
|
|
34
|
+
const customPath = pathIndex !== -1 && pathIndex + 1 < args.length ? args[pathIndex + 1]! : null
|
|
35
|
+
|
|
36
|
+
// ── Helpers ───────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
function walkPages(dir: string, prefix = ``): Array<{rel: string}> {
|
|
39
|
+
const results: Array<{rel: string}> = []
|
|
40
|
+
for (const entry of readdirSync(dir, {withFileTypes: true})) {
|
|
41
|
+
const full = join(dir, entry.name)
|
|
42
|
+
if (entry.isDirectory()) {
|
|
43
|
+
results.push(...walkPages(full, prefix ? `${prefix}/${entry.name}` : entry.name))
|
|
44
|
+
} else if (entry.isFile() && /\.(ts|js)$/.test(entry.name) && !entry.name.endsWith(`.test`)) {
|
|
45
|
+
results.push({rel: prefix ? `${prefix}/${entry.name}` : entry.name})
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return results
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getTestExt(rel: string): string {
|
|
52
|
+
return rel.endsWith(`.js`) ? `.test.js` : `.test.ts`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getSetupImportPrefix(rel: string): string {
|
|
56
|
+
const depth = rel.split(`/`).length - 1
|
|
57
|
+
if (depth === 0) return `./`
|
|
58
|
+
let prefix = ``
|
|
59
|
+
for (let i = 0; i < depth; i++) prefix += `../`
|
|
60
|
+
return prefix
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function generateTestContent(pageRel: string): string {
|
|
64
|
+
const baseName = pageRel.replace(/\.(ts|js)$/, ``)
|
|
65
|
+
const parts = baseName.split(`/`)
|
|
66
|
+
const pageName = parts[parts.length - 1] || parts[parts.length - 2] || `Page`
|
|
67
|
+
const className = pageName
|
|
68
|
+
.split(/[-_]/)
|
|
69
|
+
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
70
|
+
.join(``) + `Page`
|
|
71
|
+
|
|
72
|
+
return [
|
|
73
|
+
`import {describe, it, expect} from "bun:test"`,
|
|
74
|
+
`import {bot} from "./setup"`,
|
|
75
|
+
``,
|
|
76
|
+
`describe(\`E2E: ${className}\`, () => {`,
|
|
77
|
+
` it(\`load as a registered page in the bot\`, () => {`,
|
|
78
|
+
` const allPages = bot.pages.all`,
|
|
79
|
+
` expect(allPages[\`${baseName}\`]).toBeDefined()`,
|
|
80
|
+
` })`,
|
|
81
|
+
`})`,
|
|
82
|
+
``
|
|
83
|
+
].join(`\n`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function checkProject(label: string, pagesDir: string, e2eDir: string): boolean {
|
|
87
|
+
if (!existsSync(pagesDir)) {
|
|
88
|
+
console.log(` ⏭️ ${label}: pages directory not found at ${pagesDir}`)
|
|
89
|
+
return true
|
|
90
|
+
}
|
|
91
|
+
if (!existsSync(e2eDir)) {
|
|
92
|
+
console.log(` ⏭️ ${label}: e2e directory not found at ${e2eDir}`)
|
|
93
|
+
return true
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const pages = walkPages(pagesDir)
|
|
97
|
+
let ok = true
|
|
98
|
+
|
|
99
|
+
for (const page of pages) {
|
|
100
|
+
const ext = getTestExt(page.rel)
|
|
101
|
+
const expectedE2E = page.rel.replace(/\.(ts|js)$/, ``) + ext
|
|
102
|
+
const e2ePath = join(e2eDir, expectedE2E)
|
|
103
|
+
|
|
104
|
+
if (!existsSync(e2ePath)) {
|
|
105
|
+
console.log(` ❌ ${label}: pages/${page.rel} -> e2e/${expectedE2E} MISSING`)
|
|
106
|
+
ok = false
|
|
107
|
+
|
|
108
|
+
if (FIX_MODE) {
|
|
109
|
+
const content = generateTestContent(page.rel)
|
|
110
|
+
.replace(`"./setup"`, `"${getSetupImportPrefix(expectedE2E)}setup"`)
|
|
111
|
+
mkdirSync(dirname(e2ePath), {recursive: true})
|
|
112
|
+
writeFileSync(e2ePath, content)
|
|
113
|
+
console.log(` ✅ Created e2e/${expectedE2E}`)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (ok) {
|
|
119
|
+
console.log(` ✅ ${label}: ${pages.length} pages, all mirrored`)
|
|
120
|
+
}
|
|
121
|
+
return ok
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Main ──────────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
let allOk = true
|
|
127
|
+
|
|
128
|
+
if (customPath) {
|
|
129
|
+
const label = customPath.replace(/\\/g, `/`).replace(/\/+$/, ``)
|
|
130
|
+
const pagesDir = join(customPath, `src`, `pages`)
|
|
131
|
+
const e2eDir = join(customPath, `e2e`)
|
|
132
|
+
const ok = checkProject(label, pagesDir, e2eDir)
|
|
133
|
+
if (!ok) allOk = false
|
|
134
|
+
} else {
|
|
135
|
+
// Default: check this project's own pages and e2e
|
|
136
|
+
const pagesDir = join(import.meta.dir, `../src/pages`)
|
|
137
|
+
const e2eDir = join(import.meta.dir, `..`)
|
|
138
|
+
const ok = checkProject(`this project`, pagesDir, e2eDir)
|
|
139
|
+
if (!ok) allOk = false
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!allOk && !FIX_MODE) {
|
|
143
|
+
console.log(`\nSome e2e tests are missing. Run with --fix to auto-create them.`)
|
|
144
|
+
process.exit(1)
|
|
145
|
+
} else if (allOk) {
|
|
146
|
+
console.log(`\nAll OK!`)
|
|
147
|
+
process.exit(0)
|
|
148
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* generate-e2e-structure.ts
|
|
3
|
+
*
|
|
4
|
+
* Auto-create e2e test files for every page file that does not yet have
|
|
5
|
+
* a matching e2e test. The e2e structure mirrors src/pages exactly.
|
|
6
|
+
*
|
|
7
|
+
* Convention:
|
|
8
|
+
* src/pages/foo.ts → e2e/foo.test.ts (same relative path)
|
|
9
|
+
* src/pages/foo/bar.ts → e2e/foo/bar.test.ts (same relative path)
|
|
10
|
+
* src/pages/foo/index.ts → e2e/foo/index.test.ts (same relative path)
|
|
11
|
+
*
|
|
12
|
+
* Each generated e2e test contains:
|
|
13
|
+
* - Import setup with the correct relative path
|
|
14
|
+
* - Basic test: "load as a registered page in the bot"
|
|
15
|
+
* - Class name derived from the file name (PascalCase)
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* bun run scripts/generate-e2e-structure.ts
|
|
19
|
+
* → Generate for this project (src/pages → e2e)
|
|
20
|
+
*
|
|
21
|
+
* bun run scripts/generate-e2e-structure.ts --path ./my-app
|
|
22
|
+
* → Generate for another project
|
|
23
|
+
*
|
|
24
|
+
* bun run scripts/generate-e2e-structure.ts --path ./my-app --dry-run
|
|
25
|
+
* → Preview what would be created without writing files
|
|
26
|
+
*
|
|
27
|
+
* bun run scripts/generate-e2e-structure.ts --path ./my-app --force
|
|
28
|
+
* → Overwrite existing e2e tests
|
|
29
|
+
*
|
|
30
|
+
* bun run scripts/generate-e2e-structure.ts --path ./my-app --check
|
|
31
|
+
* → Only check, no file creation
|
|
32
|
+
*
|
|
33
|
+
* Exit code: 0 if OK, 1 if any errors.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import {existsSync, readdirSync, mkdirSync, writeFileSync} from "node:fs"
|
|
37
|
+
import {join, dirname} from "node:path"
|
|
38
|
+
|
|
39
|
+
// ── Parse CLI ─────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
const args = process.argv.slice(2)
|
|
42
|
+
|
|
43
|
+
const DRY_RUN = args.includes(`--dry-run`)
|
|
44
|
+
const FORCE = args.includes(`--force`)
|
|
45
|
+
const CHECK_ONLY = args.includes(`--check`)
|
|
46
|
+
|
|
47
|
+
const pathIndex = args.indexOf(`--path`)
|
|
48
|
+
const customPath = pathIndex !== -1 && pathIndex + 1 < args.length ? args[pathIndex + 1]! : null
|
|
49
|
+
|
|
50
|
+
// ── Helpers ───────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
/** Walk directory recursively, return relative file paths of all pages */
|
|
53
|
+
function walkFiles(dir: string, prefix = ``): string[] {
|
|
54
|
+
const results: string[] = []
|
|
55
|
+
for (const entry of readdirSync(dir, {withFileTypes: true})) {
|
|
56
|
+
const full = join(dir, entry.name)
|
|
57
|
+
if (entry.isDirectory()) {
|
|
58
|
+
results.push(...walkFiles(full, prefix ? `${prefix}/${entry.name}` : entry.name))
|
|
59
|
+
} else if (entry.isFile() && /\.(ts|js)$/.test(entry.name) && !entry.name.endsWith(`.test`)) {
|
|
60
|
+
results.push(prefix ? `${prefix}/${entry.name}` : entry.name)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return results
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Determine test file extension based on page file extension */
|
|
67
|
+
function testExt(rel: string): string {
|
|
68
|
+
return rel.endsWith(`.js`) ? `.test.js` : `.test.ts`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Build correct relative import path from a test file to setup */
|
|
72
|
+
function setupImport(rel: string): string {
|
|
73
|
+
const depth = rel.split(`/`).length - 1
|
|
74
|
+
if (depth === 0) return `./`
|
|
75
|
+
let prefix = ``
|
|
76
|
+
for (let i = 0; i < depth; i++) prefix += `../`
|
|
77
|
+
return prefix
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Convert a filename segment to PascalCase */
|
|
81
|
+
function toClassName(name: string): string {
|
|
82
|
+
return name
|
|
83
|
+
.split(/[-_/]/)
|
|
84
|
+
.filter(Boolean)
|
|
85
|
+
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
86
|
+
.join(` `)
|
|
87
|
+
.replace(/\s+/g, ``)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Generate the content for an e2e test file */
|
|
91
|
+
function generateTestContent(pageRel: string): string {
|
|
92
|
+
const baseName = pageRel.replace(/\.(ts|js)$/, ``)
|
|
93
|
+
const parts = baseName.split(`/`)
|
|
94
|
+
const fileName = parts[parts.length - 1] || `index`
|
|
95
|
+
const className = toClassName(fileName) + `Page`
|
|
96
|
+
const importPath = setupImport(baseName + testExt(pageRel))
|
|
97
|
+
|
|
98
|
+
return [
|
|
99
|
+
`import {describe, it, expect} from "bun:test"`,
|
|
100
|
+
`import {bot} from "${importPath}setup"`,
|
|
101
|
+
``,
|
|
102
|
+
`describe(\`E2E: ${className}\`, () => {`,
|
|
103
|
+
` it(\`load as a registered page in the bot\`, () => {`,
|
|
104
|
+
` const allPages = bot.pages.all`,
|
|
105
|
+
` expect(allPages[\`${baseName}\`]).toBeDefined()`,
|
|
106
|
+
` })`,
|
|
107
|
+
`})`,
|
|
108
|
+
``
|
|
109
|
+
].join(`\n`)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Process one project ───────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
function processProject(
|
|
115
|
+
label: string,
|
|
116
|
+
pagesDir: string,
|
|
117
|
+
e2eDir: string
|
|
118
|
+
): {created: number; skipped: number; errors: number} {
|
|
119
|
+
const result = {created: 0, skipped: 0, errors: 0}
|
|
120
|
+
|
|
121
|
+
if (!existsSync(pagesDir)) {
|
|
122
|
+
console.log(` ⏭️ ${label}: pages directory not found at ${pagesDir}`)
|
|
123
|
+
result.errors++
|
|
124
|
+
return result
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!existsSync(e2eDir)) {
|
|
128
|
+
if (CHECK_ONLY) {
|
|
129
|
+
console.log(` ⏭️ ${label}: e2e directory not found at ${e2eDir}`)
|
|
130
|
+
result.errors++
|
|
131
|
+
return result
|
|
132
|
+
}
|
|
133
|
+
if (!DRY_RUN) {
|
|
134
|
+
mkdirSync(e2eDir, {recursive: true})
|
|
135
|
+
console.log(` 📁 ${label}: created e2e directory`)
|
|
136
|
+
} else {
|
|
137
|
+
console.log(` 📁 ${label}: would create e2e directory`)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const pageFiles = walkFiles(pagesDir)
|
|
142
|
+
|
|
143
|
+
for (const pageRel of pageFiles) {
|
|
144
|
+
const ext = testExt(pageRel)
|
|
145
|
+
const expectedE2E = pageRel.replace(/\.(ts|js)$/, ``) + ext
|
|
146
|
+
const e2ePath = join(e2eDir, expectedE2E)
|
|
147
|
+
|
|
148
|
+
if (existsSync(e2ePath) && !FORCE) {
|
|
149
|
+
result.skipped++
|
|
150
|
+
continue
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (CHECK_ONLY) {
|
|
154
|
+
console.log(` ❌ ${label}: e2e/${expectedE2E} (for pages/${pageRel})`)
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const content = generateTestContent(pageRel)
|
|
159
|
+
|
|
160
|
+
if (DRY_RUN) {
|
|
161
|
+
console.log(` 📝 ${label}: would create e2e/${expectedE2E}`)
|
|
162
|
+
console.log(content.split(`\n`).map((l) => ` | ${l}`).join(`\n`))
|
|
163
|
+
console.log(``)
|
|
164
|
+
result.created++
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
mkdirSync(dirname(e2ePath), {recursive: true})
|
|
170
|
+
writeFileSync(e2ePath, content)
|
|
171
|
+
console.log(` ✅ ${label}: created e2e/${expectedE2E}`)
|
|
172
|
+
result.created++
|
|
173
|
+
} catch (err) {
|
|
174
|
+
console.log(` ❌ ${label}: failed to create e2e/${expectedE2E}: ${err}`)
|
|
175
|
+
result.errors++
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (result.created === 0 && result.errors === 0 && !CHECK_ONLY) {
|
|
180
|
+
console.log(` ✅ ${label}: all ${pageFiles.length} pages already have matching e2e tests`)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return result
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── Main ──────────────────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
let totalCreated = 0
|
|
189
|
+
let totalSkipped = 0
|
|
190
|
+
let totalErrors = 0
|
|
191
|
+
|
|
192
|
+
if (customPath) {
|
|
193
|
+
const label = customPath.replace(/\\/g, `/`).replace(/\/+$/, ``)
|
|
194
|
+
const pagesDir = join(customPath, `src`, `pages`)
|
|
195
|
+
const e2eDir = join(customPath, `e2e`)
|
|
196
|
+
const r = processProject(label, pagesDir, e2eDir)
|
|
197
|
+
totalCreated += r.created
|
|
198
|
+
totalSkipped += r.skipped
|
|
199
|
+
totalErrors += r.errors
|
|
200
|
+
} else {
|
|
201
|
+
// Default: generate for this project's own pages → e2e
|
|
202
|
+
const pagesDir = join(import.meta.dir, `../src/pages`)
|
|
203
|
+
const e2eDir = join(import.meta.dir, `..`)
|
|
204
|
+
const r = processProject(`this project`, pagesDir, e2eDir)
|
|
205
|
+
totalCreated += r.created
|
|
206
|
+
totalSkipped += r.skipped
|
|
207
|
+
totalErrors += r.errors
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── Summary ───────────────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
console.log(``)
|
|
213
|
+
console.log(`Summary:`)
|
|
214
|
+
console.log(` Created: ${totalCreated}`)
|
|
215
|
+
console.log(` Skipped (already exists): ${totalSkipped}`)
|
|
216
|
+
console.log(` Errors: ${totalErrors}`)
|
|
217
|
+
|
|
218
|
+
if (totalErrors > 0) {
|
|
219
|
+
process.exit(1)
|
|
220
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {Gramstax} from "gramstax"
|
|
2
2
|
import {UserGuard} from "~/guards/user"
|
|
3
|
-
import {
|
|
3
|
+
import {StatusPage} from "~/pages/status"
|
|
4
4
|
import type {CtxCore} from "./ctx"
|
|
5
5
|
|
|
6
6
|
export class BotCore extends Gramstax {
|
|
@@ -13,10 +13,10 @@ export class BotCore extends Gramstax {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
async hookErrorPage(ctx: CtxCore, data: NonNullable<Parameters<NonNullable<Gramstax[`hookErrorPage`]>>[1]>) {
|
|
16
|
-
await new
|
|
16
|
+
await new StatusPage(ctx).berror.show(`${data.listenerName}:hookErrorPage`, data.error, data.isEdit)
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
async hookErrorInputNotFoundPage(ctx: CtxCore) {
|
|
20
|
-
await new
|
|
20
|
+
await new StatusPage(ctx).bnotFound.show()
|
|
21
21
|
}
|
|
22
22
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {GuardBase} from "~/base/guard"
|
|
2
|
-
import {
|
|
2
|
+
import {StatusPage} from "~/pages/status"
|
|
3
3
|
import {UserNameNotFoundPage} from "~/pages/username-notfound"
|
|
4
4
|
|
|
5
5
|
export class UserGuard extends GuardBase {
|
|
6
6
|
async _catch(err: any, func: any, isEdit: boolean) {
|
|
7
|
-
await new
|
|
7
|
+
await new StatusPage(this).berror.show(func, err, isEdit)
|
|
8
8
|
return null
|
|
9
9
|
}
|
|
10
10
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {HelpPage} from "
|
|
1
|
+
import {HelpPage} from "../help"
|
|
2
2
|
import {PageBase} from "~/base/page"
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import {SumNumberPage} from "../sum-number"
|
|
4
|
+
import {StartRandomPage} from "./random"
|
|
5
5
|
|
|
6
6
|
export class StartPage extends PageBase {
|
|
7
7
|
async handleCallbackQueryData() {
|
|
@@ -15,7 +15,7 @@ export class StartPage extends PageBase {
|
|
|
15
15
|
bdefault = {
|
|
16
16
|
show: async (isEdit = true) => {
|
|
17
17
|
await this.clearSession()
|
|
18
|
-
const kb = this.inlineKeyboardT([
|
|
18
|
+
const kb = this.inlineKeyboardT([StartRandomPage, SumNumberPage, HelpPage], `2`)
|
|
19
19
|
const da = {userName: this.from?.username}
|
|
20
20
|
await this.renderAutoRich(kb, da, null, null, null, isEdit)
|
|
21
21
|
}
|
|
@@ -23,14 +23,14 @@ export class StartPage extends PageBase {
|
|
|
23
23
|
|
|
24
24
|
static template = `
|
|
25
25
|
<g-base>
|
|
26
|
-
<g-keyboard type="inline"
|
|
27
|
-
<g-keyboard type="inline" lang="id"
|
|
26
|
+
<g-keyboard type="inline">🎲 Random, ➕ Sum Number, 🆘 Help</g-keyboard>
|
|
27
|
+
<g-keyboard type="inline" lang="id">🎲 Acak, ➕ Jumlah Angka, 🆘 Bantuan</g-keyboard>
|
|
28
28
|
<g-message>
|
|
29
29
|
<b>👋 Welcome to the bot {{userName}}</b>
|
|
30
30
|
|
|
31
31
|
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.
|
|
32
32
|
|
|
33
|
-
<b
|
|
33
|
+
<b>🎲 Random</b> - Generate random numbers or text
|
|
34
34
|
<b>➕ Sum Number</b> - Add two numbers together
|
|
35
35
|
<b>❓ Help</b> - View help and available commands
|
|
36
36
|
|
|
@@ -43,7 +43,7 @@ export class StartPage extends PageBase {
|
|
|
43
43
|
|
|
44
44
|
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.
|
|
45
45
|
|
|
46
|
-
<b
|
|
46
|
+
<b>🎲 Acak</b> - Hasilkan angka atau teks acak
|
|
47
47
|
<b>➕ Jumlah Angka</b> - Menjumlahkan dua angka
|
|
48
48
|
<b>❓ Bantuan</b> - Lihat bantuan dan daftar perintah
|
|
49
49
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {PageBase} from "~/base/page"
|
|
2
|
+
import {StartPage} from ".."
|
|
3
|
+
import {StartRandomNumberPage} from "./number"
|
|
4
|
+
import {StartRandomTextPage} from "./text"
|
|
5
|
+
|
|
6
|
+
export class StartRandomPage extends PageBase {
|
|
7
|
+
async handleCallbackQueryData() {
|
|
8
|
+
await this.bdefault.show(true)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async handleCommandText() {
|
|
12
|
+
await this.bdefault.show(false)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
bdefault = {
|
|
16
|
+
show: async (isEdit = true) => {
|
|
17
|
+
const kb = this.inlineKeyboardT([StartRandomNumberPage, StartRandomTextPage, StartPage], `2`)
|
|
18
|
+
await this.renderAutoRich(kb, null, null, null, null, isEdit)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static template = `
|
|
23
|
+
<g-base>
|
|
24
|
+
<g-keyboard type="inline">🎲 Random Number, 📝 Random Text, ${this.kbl.back.default}</g-keyboard>
|
|
25
|
+
<g-keyboard type="inline" lang="id">🎲 Angka Acak, 📝 Teks Acak, ${this.kbl.back.id}</g-keyboard>
|
|
26
|
+
<g-message>
|
|
27
|
+
<b>🎲 Random Generator</b>
|
|
28
|
+
|
|
29
|
+
Choose what kind of random you want to generate.
|
|
30
|
+
</g-message>
|
|
31
|
+
<g-message lang="id">
|
|
32
|
+
<b>🎲 Generator Acak</b>
|
|
33
|
+
|
|
34
|
+
Pilih jenis acak yang ingin dihasilkan.
|
|
35
|
+
</g-message>
|
|
36
|
+
</g-base>
|
|
37
|
+
`
|
|
38
|
+
}
|
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
import {PageBase} from "~/base/page"
|
|
2
|
-
import {
|
|
2
|
+
import {StartRandomPage} from "."
|
|
3
3
|
|
|
4
|
-
export class
|
|
4
|
+
export class StartRandomNumberPage extends PageBase {
|
|
5
5
|
async handleCallbackQueryData() {
|
|
6
6
|
await this.bdefault.show(true)
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
async handleCommandText() {
|
|
10
|
-
await this.bdefault.show(false)
|
|
11
|
-
}
|
|
12
|
-
|
|
13
9
|
bdefault = {
|
|
14
10
|
show: async (isEdit = true) => {
|
|
15
|
-
const kb = this.inlineKeyboardT([this,
|
|
11
|
+
const kb = this.inlineKeyboardT([this, StartRandomPage], `2`)
|
|
16
12
|
const da = {result: Math.random() * 10 ** 18}
|
|
17
13
|
await this.renderAuto(kb, da, null, null, isEdit)
|
|
18
14
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import {PageBase} from "~/base/page"
|
|
2
|
+
import {StartRandomPage} from "."
|
|
3
|
+
|
|
4
|
+
const _WORDS = [
|
|
5
|
+
`innovation`,
|
|
6
|
+
`discovery`,
|
|
7
|
+
`adventure`,
|
|
8
|
+
`knowledge`,
|
|
9
|
+
`wisdom`,
|
|
10
|
+
`curiosity`,
|
|
11
|
+
`imagination`,
|
|
12
|
+
`creation`,
|
|
13
|
+
`exploration`,
|
|
14
|
+
`inspiration`,
|
|
15
|
+
`freedom`,
|
|
16
|
+
`harmony`,
|
|
17
|
+
`courage`,
|
|
18
|
+
`patience`,
|
|
19
|
+
`kindness`,
|
|
20
|
+
`brilliance`,
|
|
21
|
+
`serendipity`,
|
|
22
|
+
`resilience`,
|
|
23
|
+
`ambition`,
|
|
24
|
+
`compassion`,
|
|
25
|
+
`optimism`,
|
|
26
|
+
`integrity`,
|
|
27
|
+
`gratitude`,
|
|
28
|
+
`humility`,
|
|
29
|
+
`perseverance`
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
const _ID_WORDS = [
|
|
33
|
+
`inovasi`,
|
|
34
|
+
`penemuan`,
|
|
35
|
+
`petualangan`,
|
|
36
|
+
`pengetahuan`,
|
|
37
|
+
`kebijaksanaan`,
|
|
38
|
+
`rasa ingin tahu`,
|
|
39
|
+
`imajinasi`,
|
|
40
|
+
`kreasi`,
|
|
41
|
+
`eksplorasi`,
|
|
42
|
+
`inspirasi`,
|
|
43
|
+
`kemerdekaan`,
|
|
44
|
+
`keharmonisan`,
|
|
45
|
+
`keberanian`,
|
|
46
|
+
`kesabaran`,
|
|
47
|
+
`kebaikan`,
|
|
48
|
+
`kecerdasan`,
|
|
49
|
+
`keberuntungan`,
|
|
50
|
+
`ketahanan`,
|
|
51
|
+
`ambisi`,
|
|
52
|
+
`kasih sayang`,
|
|
53
|
+
`optimisme`,
|
|
54
|
+
`integritas`,
|
|
55
|
+
`rasa syukur`,
|
|
56
|
+
`kerendahan hati`,
|
|
57
|
+
`ketekunan`
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
class Helper {
|
|
61
|
+
static pickRandom<T>(arr: T[], count: number): T[] {
|
|
62
|
+
const shuffled = [...arr].sort(() => Math.random() - 0.5)
|
|
63
|
+
return shuffled.slice(0, count)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class StartRandomTextPage extends PageBase {
|
|
68
|
+
async handleCallbackQueryData() {
|
|
69
|
+
await this.bdefault.show(true)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
bdefault = {
|
|
73
|
+
show: async (isEdit = true) => {
|
|
74
|
+
const kb = this.inlineKeyboardT([this, StartRandomPage], `2`)
|
|
75
|
+
const count = Math.floor(Math.random() * 3) + 2 // 2–4 words
|
|
76
|
+
const lang = this.from.language_code
|
|
77
|
+
const da = {res: lang === `en` ? Helper.pickRandom(_WORDS, count).join(` `) : lang === `id` ? Helper.pickRandom(_ID_WORDS, count).join(` `) : Helper.pickRandom(_WORDS, count).join(` `)}
|
|
78
|
+
await this.renderAuto(kb, da, null, null, isEdit)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
static template = `
|
|
83
|
+
<g-base>
|
|
84
|
+
<g-keyboard type="inline">🔁 Regenerate, ${this.kbl.back.default}</g-keyboard>
|
|
85
|
+
<g-keyboard type="inline" lang="id">🔁 Regenerasi, ${this.kbl.back.id}</g-keyboard>
|
|
86
|
+
<g-message>
|
|
87
|
+
<b>📝 Random Text</b>
|
|
88
|
+
|
|
89
|
+
{{res}}
|
|
90
|
+
</g-message>
|
|
91
|
+
<g-message lang="id">
|
|
92
|
+
<b>📝 Teks Acak</b>
|
|
93
|
+
|
|
94
|
+
{{res}}
|
|
95
|
+
</g-message>
|
|
96
|
+
</g-base>
|
|
97
|
+
`
|
|
98
|
+
}
|
|
@@ -2,7 +2,7 @@ import {LogHe} from "~/helpers/log"
|
|
|
2
2
|
import {PageBase} from "~/base/page"
|
|
3
3
|
import {StartPage} from "./start"
|
|
4
4
|
|
|
5
|
-
export class
|
|
5
|
+
export class StatusPage extends PageBase {
|
|
6
6
|
constructorName?: string
|
|
7
7
|
|
|
8
8
|
constructor(p: any) {
|
|
@@ -21,7 +21,7 @@ export class GeneralStatusPage extends PageBase {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
bnotFound = {
|
|
25
25
|
show: async () => {
|
|
26
26
|
const kb = this.inlineKeyboardT([StartPage])
|
|
27
27
|
const da = {isNotFound: true}
|