diff --git a/.circleci/config.yml b/.circleci/config.yml index d16be02e1..e2455caf6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -54,36 +54,6 @@ jobs: name: lint command: yarn lint - test-node-14: - <<: *work-dir - docker: - - image: circleci/node:14 - steps: - - *attach-step - - run: - name: Tests - command: yarn test - - test-node-15: - <<: *work-dir - docker: - - image: circleci/node:15 - steps: - - *attach-step - - run: - name: Tests - command: yarn test - - test-node-LTS: - <<: *work-dir - docker: - - image: circleci/node:latest - steps: - - *attach-step - - run: - name: Tests - command: yarn test - test-node-16: <<: *work-dir docker: @@ -135,20 +105,6 @@ workflows: branches: ignore: gh-pages - - test-node-14: - requires: - - build - filters: - branches: - ignore: gh-pages - - - test-node-15: - requires: - - build - filters: - branches: - ignore: gh-pages - - test-node-16: requires: - build @@ -156,19 +112,9 @@ workflows: branches: ignore: gh-pages - - test-node-LTS: - requires: - - build - filters: - branches: - ignore: gh-pages - - publish-coverage: requires: - - test-node-14 - - test-node-15 - test-node-16 - - test-node-LTS filters: branches: diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 70ea19493..5d902ae07 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -5,15 +5,12 @@ on: [push] jobs: linux: runs-on: ubuntu-latest - strategy: - matrix: - node: [14, 15, 16] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v2 with: - node-version: ${{ matrix.node }} + node-version: 16.x cache: "yarn" - name: Install dependencies @@ -40,15 +37,12 @@ jobs: windows: runs-on: windows-latest - strategy: - matrix: - node: [14, 15, 16] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v2 with: - node-version: ${{ matrix.node }} + node-version: 16.x cache: "yarn" - name: Install dependencies @@ -74,15 +68,12 @@ jobs: mac: runs-on: macos-latest - strategy: - matrix: - node: [14, 15, 16] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v2 with: - node-version: ${{ matrix.node }} + node-version: 16.x cache: "yarn" - name: Install dependencies diff --git a/.nvmrc b/.nvmrc index 3c95b2692..7776cb222 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v16.1.0 +v16.6.0 diff --git a/README.md b/README.md index e2fd6aaad..80c983260 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ The bellow documentation is a resume of what you can find in [Corde's site](http ## 🚀 Getting started -**Node.js 14.0.0 or newer is required** +**Node.js 14.0 or newer is required** Starting to create tests with Corde is simple. First, install it locally with npm `npm i -D corde` or yarn `yarn add -D corde`. You can also install it globally: `npm i -g corde` or `yarn global add corde`. @@ -58,12 +58,12 @@ After installed, add the file `corde.config.json` in the root of your applicatio ```javascript { - "cordeBotToken": "YOUR_TESTING_BOT_TOKEN_HERE", - "botTestId": "YOUR_TESTING_BOT_ID_HERE", - "botToken": "YOUR_BOT_TOKEN_HERE", - "guildId": "THE_GUID_OF_BOT_HERE", - "channelId": "CHANNELS_ID_HERE", - "botPrefix": "+", + "cordeBotToken": "", + "botTestId": "", + "botToken": "", + "guildId": "", + "channelId": "", + "botPrefix": "!", "testMatches": ["./test/**"] } ``` diff --git a/docs/Configuration.mdx b/docs/Configuration.mdx index f5a8c40d0..5b7c3d0a3 100644 --- a/docs/Configuration.mdx +++ b/docs/Configuration.mdx @@ -1,26 +1,13 @@ --- id: configurations -title: Configuring Corde +title: Configurations custom_edit_url: https://github.com/cordejs/corde/blob/master/docs/Configuration.mdx --- Corde's configuration can be defined through a `corde.config.js|json|ts` file. This file must be at the root of your application, or you can declare a custom path for it with the cli option [--config](CLI.mdx#--config). -This is the default config file for Corde: - -```javascript -{ - "cordeBotToken": "YOUR_TESTING_BOT_TOKEN_HERE", - "botTestId": "YOUR_TESTING_BOT_ID_HERE", - "botToken": "YOUR_BOT_TOKEN_HERE", - "guildId": "THE_GUID_OF_BOT_HERE", - "channelId": "CHANNELS_ID_HERE", - "botPrefix": "+", - "testMatches": ["./test/**"], - "timeout": 1000 -} -``` +Configs can also be accessed (in readonly mode) trough `corde.configs`. :::note both `cordeBotToken` and `botToken` can be found in [Discord Developers portal](https://discord.com/developers/applications) @@ -30,129 +17,197 @@ both `cordeBotToken` and `botToken` can be found in [Discord Developers portal]( ### `cordeBotToken` +- Required: `true` +- Type: `string` + The bot token that Corde will use to simulate a user. -**Required: `true`** -**Type: `string`** -**Example: `YaA4MDMiOTY2O4I2MjAwODMy.X2iRwg.Rf3-TqLExWuPQjxnVaDCGv9V7cB`** +```js title="corde.config.js" +module.exports = { + cordeBotToken: "YaA4MDMiOTY2O4I2MjAwODMy.X2iRwg.Rf3-TqLExWuPQjxnVaDCGv9V7cB", // Example value +}; +``` --- ### `botTestId` +- Required: `true` +- Type: `string` + Your bot's id. -**Required: `true`** -**Type: `string`** -**Example: `514212632960122287894`** +```js title="corde.config.js" +module.exports = { + botTestId: "514212632960122287894", // Example value +}; +``` --- ### `botToken` +- Required: `true` +- Type: `string` + Your bot's Token. -**Required: `true`** -**Type: `string`** -**Example: `YaA4MDMiOTY2O4I2MjAwODMy.X2iRwg.Rf3-TqLExWuPQjxnVaDCGv9V7cB`** +```js title="corde.config.js" +module.exports = { + botToken: "YaA4MDMiOTY2O4I2MjAwODMy.X2iRwg.Rf3-TqLExWuPQjxnVaDCGv9V7cB", // Example value +}; +``` --- ### `guildId` +- Required: `true` +- Type: `string` + The id of the `guild` where both bots are. -**Required: `true`** -**Type: `string`** -**Example: `514212632960122287894`** +```js title="corde.config.js" +module.exports = { + guildId: "514212632960122287894", // Example value +}; +``` --- ### `channelId` +- Required: `true` +- Type: `string` + The id of the `channel` where both bots are. -**Required: `true`** -**Type: `string`** -**Example: `514212632960122287894`** +```js title="corde.config.js" +module.exports = { + channelId: "514212632960122287894", // Example value +}; +``` --- ### `botPrefix` -Prefix for call your bot. +- Required: `true` +- Type: `string` -**Required: `true`** -**Type: `string`** -**Example: `!`** +Prefix for call your bot. This value is attached to each command sent by corde. + +```js title="corde.config.js" +module.exports = { + botPrefix: "!", // Example value +}; +``` --- ### `testMatches` +- Required: `true` +- Type: `Array` + Array with the path of test folders | files. -**Required: `true`** -**Type: `string`** -**Example: `['./tests']`** +```js title="corde.config.js" +module.exports = { + testMatches: ["./tests"], // Example value +}; +``` --- ### `timeout` +- Required: `false` +- Type: `number` + Timeout for each test. The value in milliseconds. -**Required: `false`** -**Type: `number`** -**Example: `1000`** -**Default: `5000`** +```js title="corde.config.js" +module.exports = { + timeout: 5000, // Default value +}; +``` --- ### `modulePathIgnorePatterns` +- Required: `false` +- Type: `string[]` + Define the file pattern that corde should ignore. -**Required: `false`** -**Type: `string[]`** -**Example: `['**/tests/\*.fixture.js']`\*\* +```js title="corde.config.js" +module.exports = { + modulePathIgnorePatterns: ["**/tests/*.fixture.js"], // Example value +}; +``` --- ### `rootDir` +- Required: `false` +- Type: `string[]` + Defines root dir of the project. -**Required: `false`** -**Type: `string[]`** -**Default: `process.cwd()`** +:::note +Default value uses **process.cwd()** to pick the `rootDir` +::: + +```js title="corde.config.js" +module.exports = { + rootDir: ".", // Example value. It will be resolved by process.cwd() +}; +``` --- ### `extensions` -**Define file extensions to be loaded** +- Required: `false` +- Type: `Array` + +Define file extensions to be loaded -**Required: `false`** -**Type: `string[]`** -**Default: `[".js",".ts"]`** +```js title="corde.config.js" +module.exports = { + extensions: [".js", ".ts"], // Default value +}; +``` --- ### `exitOnFileReadingError` +- Required: `false` +- Type: `boolean` + Define if corde should stop if any problem occour when importing a test file. -**Required: `false`** -**Type: `boolean`** -**Default: `true`** +```js title="corde.config.js" +module.exports = { + exitOnFileReadingError: true, // Default value +}; +``` --- ### `project` +- Required: `false` +- Type: `string` + Definition of tsconfig path. -**Required: `false`** -**Type: `string`** -**Default: `/tsconfig.json`** +```js title="corde.config.js" +module.exports = { + project: "/tsconfig.json", // Default value +}; +``` diff --git a/docs/CordeBot.mdx b/docs/CordeBot.mdx index d7a11d164..5c99fc4f9 100644 --- a/docs/CordeBot.mdx +++ b/docs/CordeBot.mdx @@ -38,20 +38,34 @@ corde.bot; - [`voiceState`](/docs/cordebot#voicestate) - [`isLoggedIn`](/docs/cordebot#isloggedin) +- [`client`](/docs/cordebot#client) +- [`channel`](/docs/cordebot#channel) +- [`channels`](/docs/cordebot#channels) +- [`guild`](/docs/cordebot#guild) +- [`guildMembers`](/docs/cordebot#guildmembers) +- [`guilds`](/docs/cordebot#guilds) +- [`roles`](/docs/cordebot#roles) #### Methods - [`isMessageAuthor`](/docs/cordebot#ismessageauthormessage) - [`joinVoiceChannel`](/docs/cordebot#joinvoicechannelstring) - [`leaveVoiceChannel`](/docs/cordebot#leavevoicechannel) +- [`getOnlyTextChannels`](/docs/cordebot#getonlytextchannels) - [`isInVoiceChannel`](/docs/cordebot#isinvoicechannel) -- [`fetchChannel`](/docs/cordebot#fetchchannel) -- [`fetchGuild`](/docs/cordebot#fetchguild) -- [`getChannel`](/docs/cordebot#getchannel) -- [`findGuild`](/docs/cordebot#findguild) -- [`send`](/docs/cordebot#send) -- [`createRole`](/docs/cordebot#createrole) -- [`findRole`](/docs/cordebot#findrole) +- [`fetchChannel`](/docs/cordebot#fetchchannelid) +- [`fetchGuild`](/docs/cordebot#fetchguildid) +- [`fetchRole`](/docs/cordebot#fetchroleid) +- [`getChannel`](/docs/cordebot#getchannelid--object) +- [`getGuild`](/docs/cordebot#getguildid--object) +- [`send`](/docs/cordebot#sendstring--number--boolean--object) +- [`createRole`](/docs/cordebot#createrolestring--object) +- [`createGuild`](/docs/cordebot#createguildstring--object) +- [`createChannel`](/docs/cordebot#createchannelstring--object) +- [`createVoiceChannel`](/docs/cordebot#createvoicechannelstring--object) +- [`createTextChannel`](/docs/cordebot#createtextchannelstring--object) +- [`createCategoryChannel`](/docs/cordebot#createcategorychannelstring--object) +- [`getRole`](/docs/cordebot#getrolestring--object) ## Reference @@ -60,56 +74,227 @@ corde.bot; ### `voiceState` Gets the voice channel state that corde's bot is connected in, If it's connected. -This property is filled when `joinVoiceChannel()` connects to a channel. +This property is filled when [joinVoiceChannel()](CordeBot.mdx#joinvoicechannelstring) connects to a channel and +is cleared when [leaveVoiceChannal()](CordeBot.mdx#leavevoicechannel) is called. + +--- ### `isLoggedIn` Checks if corde's bot is connected and ready. +--- + +### `client` + +Client of Discord.js. + +--- + +### `channel` + +- **throws** Error if corde bot is not connected. + +Same of [getChannel()](CordeBot.mdx#getchannelid--object). + +--- + +### `channels` + +- **throws** Error if corde bot is not connected. + +Get all channels in **cache** of the bot. + +--- + +### `guild` + +- **throws** Error if corde bot is not connected. + +Same of [getGuild()](CordeBot.mdx#getguildid--object). + +--- + +### `guildMembers` + +- **throws** Error if corde bot is not connected. + +Members of the guild defined in [configs](configurations). + +--- + +### `guilds` + +- **throws** Error if corde bot is not connected. + +Get all guilds in **cache** of the bot. + +--- + +### `roles` + +- **throws** Error if corde bot is not connected. + +Get all roles in **cache** of the guild defined in [configs](configurations). + +--- + ### `isMessageAuthor(Message)` Checks if a given message was sent by corde's bot. +--- + ### `joinVoiceChannel(string)` +- **throws** Error if corde bot is not connected. + Joins corde's bot to a voice channel. +--- + ### `leaveVoiceChannel` +- **throws** Error if corde bot is not connected. + Leaves a voice channel. +--- + +### `getOnlyTextChannels` + +- **throws** Error if corde bot is not connected. + +From all channels in **cache**, get all that are of type text + +--- + ### `isInVoiceChannel` +- **throws** Error if corde bot is not connected. + Checks if corde's bot is in a voice channel. -### `fetchChannel` +--- + +### `fetchChannel(id)` + +- **throws** Error if corde bot is not connected. Makes a fetch of a channel based on it's `id`. -### `fetchGuild` +--- + +### `fetchGuild(id)` + +- **throws** Error if corde bot is not connected. Makes a fetch of a guild based on it's `id`. -### `getChannel` +--- + +### `fetchRole(id)` + +- **throws** Error if corde bot is not connected. + +Fetch for a role based on it's id, caching it after that. + +--- + +### `getChannel(id | object)` + +- **throws** Error if corde bot is not connected. Gets the channel defined in `configs`. -### `getGuild` +--- + +### `getGuild(id | object)` + +- **throws** Error if corde bot is not connected. Gets the guild defined in `configs`. -### `findGuild` +--- + +### `send(string | number | boolean | object)` + +- **throws** Error if corde bot is not connected. +- **throws** Error If message is invalid. + +Sends a message to the connected [TextChannel](https://discord.js.org/#/docs/main/stable/class/TextChannel). + +--- + +### `createRole(string? | object)` -Search for a guild in guild's cache, based on it's id. +- **throws** Error if corde bot is not connected. -### `send` +Creates a new role to the guild provided in [configs](configurations). + +--- + +### `createGuild(string? | object)` + +- **throws** Error if corde bot is not connected. + +Creates a new `guild` in defined in [configs](configurations). + +--- -Sends a message to the connected textChannel. +### `createChannel(string | object)` -### `createRole` +- **throws** Error if corde bot is not connected. + +Creates a new channel in guild defined in [configs](configurations). + +If the parameter provided be a string or a object without the property [**type**](https://discord.js.org/#/docs/main/stable/typedef/GuildChannelCreateOptions) provided, +then a textChannel will be created. + +--- + +### `createVoiceChannel(string | object)` + +- **throws** Error if corde bot is not connected. + +Creates a new [VoiceChannel](https://discord.js.org/#/docs/main/stable/class/VoiceChannel) in guild defined in [configs](configurations). + +```typescript +client.channels.create("exampleName", { type: "voice" }); +``` + +--- + +### `createTextChannel(string | object)` + +- **throws** Error if corde bot is not connected. + +Creates a new [TextChannel](https://discord.js.org/#/docs/main/stable/class/TextChannel) in guild defined in [configs](configurations). + +Shortcut for: + +```typescript +client.channels.create("exampleName", { type: "text" }); +``` + +--- + +### `createCategoryChannel(string | object)` + +- **throws** Error if corde bot is not connected. + +Creates a new [CategoryChannel](https://discord.js.org/#/docs/main/stable/class/CategoryChannel) in guild defined in [configs](configurations). + +Shortcut for: + +```typescript +client.channels.create("exampleName", { type: "category" }); +``` + +--- -Creates a new role to the guild provided in configs. +### `getRole(string | object)` -### `findRole` +- **throws** Error if corde bot is not connected. -Finds a role in config guild's cache, basing on it's **id** or **name**. +Finds a role in config guild's cache, basing on it's **id** diff --git a/docs/Roadmap.mdx b/docs/Roadmap.mdx new file mode 100644 index 000000000..3f19c05c1 --- /dev/null +++ b/docs/Roadmap.mdx @@ -0,0 +1,17 @@ +--- +id: roadmap +title: Roadmap +custom_edit_url: https://github.com/cordejs/corde/blob/master/docs/Roadmap.mdx +--- + +This page contains futures plans for corde library. + +### v5.0.0 + +- [ ] Add logger for corde, avoiding overrites of others console.log, info etc... calls. +- [ ] Add full configurations to pe passed in CLI. +- [ ] Rename `expect` api to `command` and matchers prefix from `to` to `should`. +- [ ] Node minimal version 16.6.0. +- [ ] Upgrade Discord.js to version 13. +- [ ] Add Discord.js cache support in configs. +- [ ] Assertion library. diff --git a/e2e/__snapshots__/test1.spec.txt b/e2e/__snapshots__/test1.spec.txt index a3590dc04..86dd4bd5b 100644 --- a/e2e/__snapshots__/test1.spec.txt +++ b/e2e/__snapshots__/test1.spec.txt @@ -1,28 +1,14 @@ File: test1.spec.ts exit code: 0 -[dotenv][DEBUG] did not match key and value when parsing line 3: -[dotenv][DEBUG] did not match key and value when parsing line 9: -[dotenv][DEBUG] "TIME_OUT" is already defined in `process.env` and will not be overwritten -[dotenv][DEBUG] "BOT_PREFIX" is already defined in `process.env` and will not be overwritten -[dotenv][DEBUG] "CORDE_TEST_TOKEN" is already defined in `process.env` and will not be overwritten -[dotenv][DEBUG] "BOT_TEST_ID" is already defined in `process.env` and will not be overwritten -[dotenv][DEBUG] "CHANNEL_ID" is already defined in `process.env` and will not be overwritten -[dotenv][DEBUG] "GUILD_ID" is already defined in `process.env` and will not be overwritten -[dotenv][DEBUG] "BOT_TEST_TOKEN" is already defined in `process.env` and will not be overwritten -[dotenv][DEBUG] "CORDE_TEST_TOKEN_LINUX" is already defined in `process.env` and will not be overwritten -[dotenv][DEBUG] "BOT_TEST_ID_LINUX" is already defined in `process.env` and will not be overwritten -[dotenv][DEBUG] "CHANNEL_ID_LINUX" is already defined in `process.env` and will not be overwritten -[dotenv][DEBUG] "GUILD_ID_LINUX" is already defined in `process.env` and will not be overwritten -[dotenv][DEBUG] "BOT_TEST_TOKEN_LINUX" is already defined in `process.env` and will not be overwritten RUNS e2e/toReturn/test1.spec.ts RUNS e2e/toReturn/test1.spec.ts ● Hello command should return... hello!! RUNS e2e/toReturn/test1.spec.ts - ✔ Hello command should return... hello!! 415ms + ✔ Hello command should return... hello!! 375ms - PASS e2e/toReturn/test1.spec.ts 420ms - ✔ Hello command should return... hello!! 415ms + PASS e2e/toReturn/test1.spec.ts 377ms + ✔ Hello command should return... hello!! 375ms Test Files: 1 passed. 1 total Tests: 1 passed. 1 total -Time: 423ms +Time: 378ms diff --git a/e2e/__snapshots__/test2.spec.txt b/e2e/__snapshots__/test2.spec.txt new file mode 100644 index 000000000..fe8fbee41 --- /dev/null +++ b/e2e/__snapshots__/test2.spec.txt @@ -0,0 +1,15 @@ +File: test2.spec.ts +exit code: 0 + RUNS e2e/afterAll/test2.spec.ts + RUNS e2e/afterAll/test2.spec.ts + ● + RUNS e2e/afterAll/test2.spec.ts + ✔ 2ms + + PASS e2e/afterAll/test2.spec.ts 5ms + ✔ 2ms + +Test Files: 1 passed. 1 total +Tests: 1 passed. 1 total +Time: 7ms +test diff --git a/e2e/bot.ts b/e2e/bot.ts index 821aa72dc..3f4441e40 100644 --- a/e2e/bot.ts +++ b/e2e/bot.ts @@ -1,5 +1,4 @@ /* eslint-disable no-console */ -// TODO: In some tests, when a function from another module is called, // Corde reader fail in import the test file because Node.js can not // import the submodule. To avoid the problem, this file is here is root of the project, // but it should be in ./e2e @@ -13,6 +12,8 @@ import * as config from "./corde.config"; export const bot = new Client(); +// TODO: Corde do not recognize optional chaning + export async function sendMessage(message: string) { if (!config.channelId) { return null; @@ -34,14 +35,20 @@ export async function sendMessage(message: string) { */ export function getRole(name: string) { if (config.guildId) { - return bot.guilds.cache.get(config.guildId)?.roles.cache.find((r) => r.name === name); + const guild = bot.guilds.cache.get(config.guildId); + if (guild) { + return guild.roles.cache.find((r) => r.name === name); + } } return null; } export function getRoleManager() { if (config.guildId) { - return bot.guilds.cache.get(config.guildId)?.roles; + const guild = bot.guilds.cache.get(config.guildId); + if (guild) { + return guild.roles; + } } return null; } @@ -66,7 +73,6 @@ export async function login(isDebugMode?: boolean) { }); }); - console.log(config.botToken); const loginPromise = bot.login(config.botToken); await Promise.allSettled([loginPromise, readyPromise]); } @@ -245,17 +251,20 @@ async function deleteRole(msg: Message, roleId: string) { async function sendMultiple(msg: Message, channelId: string) { await msg.channel.send("hello"); - const channel = msg.guild?.channels.cache.get(channelId); - if (channel && channel.isText()) { - await channel.send("hello2"); + + if (msg.guild) { + const channel = msg.guild.channels.cache.get(channelId); + if (channel && channel.isText()) { + await channel.send("hello2"); + } } } /** --------------------- Utility functions --------------------- */ function getRoleById(msg: Message, roleId: string | undefined) { - if (msg && roleId) { - return msg.guild?.roles.cache.get(roleId); + if (msg && roleId && msg.guild) { + return msg.guild.roles.cache.get(roleId); } return null; } diff --git a/e2e/corde.config.ts b/e2e/corde.config.ts index af329c3fd..ab6bd23e1 100644 --- a/e2e/corde.config.ts +++ b/e2e/corde.config.ts @@ -8,7 +8,7 @@ import env from "dotenv"; import testUtils from "./testUtils"; -const result = env.config({ debug: testUtils.isDebug(), path: "./e2e/.env" }); +const result = env.config({ path: "./e2e/.env" }); // Do not throw any error if the project in running inside CI. if (!process.env.CI && result.error) { @@ -29,8 +29,6 @@ export const botPrefix = configs.botPrefix; export const timeout = configs.timeout; export const project = "./tsconfig.json"; -console.log(configs); - if (testUtils.isDebug()) { console.log("Loaded configs: "); console.log(configs); diff --git a/e2e/pipeline.ts b/e2e/pipeline.ts index a815eb9fc..f9b6ea6ec 100644 --- a/e2e/pipeline.ts +++ b/e2e/pipeline.ts @@ -2,23 +2,22 @@ /* eslint-disable no-console */ /** - * @deprecated * package.json script: "e2e": "ts-node ./e2e/pipeline" */ /** * Corde script for end-to-end tests. In comparation with Jest, this script, * runs tests with avarage of ~5 seconds faster. - * usage: ts-node pipeline.ts + * usage: yarn e2e * * This script must be executed outside ./e2e folder witch means, in the root * of corde folder. * + * This script also suport specifics execution, + * just add --tests followed by the test id + * */ -// todo: process.env should be using NODE_ENV -// process.env.ENV = "E2E"; - import chalk from "chalk"; import { login, bot } from "./bot"; import { generator } from "./tests"; @@ -31,20 +30,18 @@ function logoutBot() { async function main() { console.log(`Environment: ${chalk.cyan(testUtils.env())}`); - const testsMeasureName = "tests end"; let exitCode = 0; - // console.time(testsMeasureName); - console.log(chalk.cyanBright("loging example bot...")); + process.stdout.write(chalk.cyanBright("loging example bot...")); await login(); try { - console.log(chalk.green(" Done\n")); + process.stdout.write(chalk.green(" Done\n")); const selectedTests = process.argv .slice(process.argv.indexOf("--tests")) - .filter((el) => Number.isInteger(el)); + .filter((el: any) => !isNaN(el)); for (const [fileObj, testFn] of generator) { if (selectedTests.length === 0 || selectedTests.includes(fileObj.id.toString())) { @@ -71,7 +68,6 @@ async function main() { console.log(`${chalk.bgRed.black(" FAIL ")} ${error}`); exitCode = 1; } finally { - //console.time(testsMeasureName); console.log("\n"); if (exitCode === 0) { console.log(`${chalk.bgGreen(" SUCCESS ")}: All tests passed`); diff --git a/e2e/sendMessage/test1.spec.ts b/e2e/sendMessage/test1.spec.ts index 4da273534..809b690fc 100644 --- a/e2e/sendMessage/test1.spec.ts +++ b/e2e/sendMessage/test1.spec.ts @@ -1,7 +1,5 @@ -/* eslint-disable no-console */ import corde from "../../lib"; corde.it("should send a message", async () => { - const msg = await corde.bot.send("TEST MESSAGE"); - console.log(msg.content); + await corde.bot.send("TEST MESSAGE"); }); diff --git a/e2e/testUtils.ts b/e2e/testUtils.ts index bd347acc4..dabbef2bb 100644 --- a/e2e/testUtils.ts +++ b/e2e/testUtils.ts @@ -1,7 +1,7 @@ +/* eslint-disable no-control-regex */ /* eslint-disable no-console */ import { Message } from "discord.js"; import fs from "fs"; -import { removeANSIColorStyle } from "../tests/testHelper"; import * as childProcess from "child_process"; import { CliOutput, OSEnv } from "./types"; import path from "path"; @@ -122,6 +122,16 @@ namespace testUtils { }); } + // Duplicated code from ../tests/testHelper + // Due to jest types not being found in e2e folder (and can not to avoid types conflict) + + export function removeANSIColorStyle(value: string) { + return value.replace( + /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, + "", + ); + } + export function isDebug() { const args = process.argv.slice(2); return args.includes("debug"); diff --git a/e2e/tests.ts b/e2e/tests.ts index b9939a4db..672364ae5 100644 --- a/e2e/tests.ts +++ b/e2e/tests.ts @@ -89,6 +89,54 @@ const testFiles: ITestFile[] = [ exitCodeExpectation: 0, isRequiredFunction: true, }, + { + id: 12, + folder: "sendMessage", + testFile: "test1.spec.ts", + exitCodeExpectation: 0, + }, + { + id: 13, + folder: "toAddReaction", + testFile: "test1.spec.ts", + exitCodeExpectation: 0, + }, + { + id: 14, + folder: "toAddReaction", + testFile: "test2.spec.ts", + exitCodeExpectation: 1, + }, + // { + // id: 15, + // folder: "toDeleteRole", + // testFile: "test1.spec.ts", + // exitCodeExpectation: 0, + // }, + // { + // id: 16, + // folder: "toDeleteRole", + // testFile: "test2.spec.ts", + // exitCodeExpectation: 1, + // }, + // { + // id: 17, + // folder: "toEditMessage", + // testFile: "test1.spec.ts", + // exitCodeExpectation: 0, + // }, + // { + // id: 18, + // folder: "toEditMessage", + // testFile: "test2.spec.ts", + // exitCodeExpectation: 1, + // }, + // { + // id: 19, + // folder: "toHaveResult", + // testFile: "test1.spec.ts", + // exitCodeExpectation: 0, + // }, ]; function* createTestFunctionsGenerator(): Generator< diff --git a/e2e/toDeleteRole/test1.spec.ts b/e2e/toDeleteRole/test1.spec.ts index 7a3abf9b2..44757be19 100644 --- a/e2e/toDeleteRole/test1.spec.ts +++ b/e2e/toDeleteRole/test1.spec.ts @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -// @ts-nocheck import corde from "../../lib"; @@ -7,16 +6,16 @@ let role = null; const roleName = "role-to-delete"; corde.beforeStart(async () => { - role = corde.getRole({ name: roleName }); + role = corde.bot.getRole({ name: roleName }); if (!role) { - role = await corde.createRole({ + role = await corde.bot.createRole({ name: roleName, }); } }); corde.it("should delete a role", async () => { - role = role || corde.getRole({ name: roleName }); + role = role || corde.bot.getRole({ name: roleName }); if (!role) { throw new Error("Role not found"); @@ -26,7 +25,7 @@ corde.it("should delete a role", async () => { }); corde.afterAll(async () => { - await corde.createRole({ + await corde.bot.createRole({ name: roleName, }); }); diff --git a/e2e/toDeleteRole/test2.spec.ts b/e2e/toDeleteRole/test2.spec.ts index a6120619b..ed3bf8f1f 100644 --- a/e2e/toDeleteRole/test2.spec.ts +++ b/e2e/toDeleteRole/test2.spec.ts @@ -1,6 +1,6 @@ import corde from "../../lib"; corde.it("should fail in delete a role", async () => { - const role = corde.bot.findRole({ name: "role-to-delete" }); + const role = corde.bot.getRole({ name: "role-to-delete" }); corde.expect("deleteRole 1231").toDeleteRole(role.id); }); diff --git a/e2e/toEditMessage/test1.spec.ts b/e2e/toEditMessage/test1.spec.ts index 853952069..4beba4713 100644 --- a/e2e/toEditMessage/test1.spec.ts +++ b/e2e/toEditMessage/test1.spec.ts @@ -1,17 +1,8 @@ import corde from "../../lib"; import { sendMessage } from "../bot"; -import { login, bot } from "../bot"; - -corde.beforeStart(async () => { - await login(); -}, 3000); corde.it("should edit a message", async () => { const msg = await sendMessage("oldValue"); const newValue = "newMessageEdited"; corde.expect(`editMessage ${msg.id} ${newValue}`).toEditMessage(newValue, { id: msg.id }); }); - -corde.afterAll(() => { - bot.destroy(); -}); diff --git a/e2e/toHaveResult/test1.spec.ts b/e2e/toHaveResult/test1.spec.ts index f8344560a..3ebf7d25c 100644 --- a/e2e/toHaveResult/test1.spec.ts +++ b/e2e/toHaveResult/test1.spec.ts @@ -2,6 +2,7 @@ import corde from "../../lib"; corde.describe("testing todoInCascade", () => { corde.it("test should pass", () => { + corde.bot.channels.find((c) => c.isText()); corde .expect("sendMultiple 829873348309155851") .toHaveResult( diff --git a/package.json b/package.json index 073030ed1..4dba45b86 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "release": "release-it", "test": "jest --silent --detectOpenHandles", "build:publish": "bash ./scripts/publish.sh", - "generateStructureDocs": "typedoc --plugin typedoc-plugin-markdown src/api", + "generateDocs": "typedoc --plugin typedoc-plugin-markdown src/api", "prettier-lib": "prettier --write ./lib ./schema", "dependencies": "depcruise --config ./etc/.dependency-cruiser.js src", "watch": "bash tasks.sh watch", @@ -91,7 +91,6 @@ "eslint-config-standard": "^16.0.2", "eslint-import-resolver-typescript": "^2.4.0", "eslint-plugin-import": "^2.22.1", - "eslint-plugin-jsdoc": "^36.0.6", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^3.3.1", "eslint-plugin-promise": "^5.1.0", @@ -121,7 +120,8 @@ "yarn": "^1.22.10" }, "optionalDependencies": { - "dependency-cruiser": "^10.0.1" + "dependency-cruiser": "^10.0.1", + "eslint-plugin-jsdoc": "^36.0.6" }, "dependencies": { "chalk": "^4.1.1", diff --git a/src/api/bot.ts b/src/api/bot.ts deleted file mode 100644 index 6307d84f3..000000000 --- a/src/api/bot.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { Message, Role } from "discord.js"; -import { CordeClientError } from "../errors"; -import { mapper } from "../mapper/messageMapper"; -import { ICordeBot, IMessageEmbed, IRoleData, IRoleIdentifier, Primitive } from "../types"; -import { isPrimitiveValue } from "../utils"; - -export class Bot { - private _bot: ICordeBot; - - /** - * Gets the voice channel state that corde's bot is connected in, If it's connected. - * This property is filled when `joinVoiceChannel()` connects to a channel - * and is cleared when `leaveVoiceChannal()` is called. - */ - get voiceState() { - return this._bot.voiceConnection; - } - - /** - * Checks if corde's bot is connected and ready. - */ - get isLoggedIn() { - return this._bot.isLoggedIn(); - } - - constructor(bot: ICordeBot) { - this._bot = bot; - } - - /** - * Checks if a given message was sent by corde's bot - * @param message Sent message - * @returns If corde's bot is the author of the message - */ - isMessageAuthor(message: Message) { - return message.author.id === this._bot.id; - } - - /** - * Joins corde's bot to a voice channel. - * @param channelId Voice channel to corde's bot connect - * @returns Voice connection state. This property can be get from `bot.voiceState` - */ - async joinVoiceChannel(channelId: string) { - return await this._bot.joinVoiceChannel(channelId); - } - - /** - * Leaves a voice channel. - */ - leaveVoiceChannel() { - this._bot.leaveVoiceChannel(); - } - - /** - * Checks if corde's bot is in a voice channel - */ - isInVoiceChannel() { - return this._bot.isInVoiceChannel(); - } - - /** - * Makes a fetch of a channel based on it's `id`. - * @param id Id of the channel - * @returns Channel if it's found - */ - async fetchChannel(id: string) { - return await this._bot.fetchChannel(id); - } - - /** - * Makes a fetch of a guild based on it's `id`. - * @param id Id of the guild - * @returns Guild if it's found - */ - async fetchGuild(id: string) { - return await this._bot.fetchGuild(id); - } - - /** - * Gets the channel defined in `configs` - */ - getChannel() { - return this._bot.channel; - } - - /** - * Gets the guild defined in `configs` - */ - getGuild() { - return this._bot.guild; - } - - /** - * Search for a guild in guild's cache, based on it's id - * @param id id of the guild - * @returns Guild if the id is valid and the guild is in the cache - */ - findGuild(id: string) { - return this._bot.findGuild(id); - } - - /** - * Sends a message to the connected textChannel. - * - * **This function does not work without a test case** - * - * @param message Message to send - * - * @example - * - * // Works - * test("test 1", () => { - * const message = await sendMessage("msg"); - * expect(`editMessage ${message.id}`).toEditMessage({ id: message.id }, "newValue"); - * }); - * - * // Do not Works - * group("test 1", () => { - * const message = await sendMessage("msg"); - * }); - * - * @throws CordeClienteError - If bot is not connected yet. - * - * @returns null if message is empty, null or undefined. - * Message if **message** is not empty and it was send to Discord. - * - * @since 2.0 - */ - async send(message: string | number | boolean | bigint): Promise; - async send(message: IMessageEmbed): Promise; - async send(message: Primitive | IMessageEmbed): Promise { - if (!message) { - throw new Error("Can not send a empty message"); - } - - if (!this._bot.isLoggedIn()) { - throw new CordeClientError( - "Can not send a directly message to channel because the client is not connected yet", - ); - } - - if (isPrimitiveValue(message)) { - return await this._bot.sendMessage(message); - } - - const embed = mapper.embedInterfaceToMessageEmbed(message); - return await this._bot.sendMessage(embed); - } - - /** - * Creates a new role to the guild provided in configs. - * - * @param roleIdentifier Basic informations about the role. - * @see https://cordejs.org/docs/configurations#guildid - * - * @throws CordeClientError if corde has not yet connect it's bot. - * @returns A promise that return the created role. - * - * @since 2.1 - */ - async createRole(data: IRoleData) { - if (!this._bot.isLoggedIn()) { - throw new CordeClientError("Bot is not connected yet. Can not create a role"); - } - - try { - return await this._bot.roleManager.create({ data }); - } catch (error) { - throw Error("Could not create the role. " + error); - } - } - - // TODO: Rename "findRole" to "getRole" - - /** - * Finds a role in config guild's cache, basing on it's **id** - * - * @param id Id of the role. - * @throws CordeClientError if corde's bot is not connected. - * @returns Role that matches the provided **id** or **name** - */ - findRole(id: string): Role | undefined; - /** - * Finds a role in config guild's cache, basing on it's **id** or **name**. - * - * @param data Data of the role. It can be it's **name** or **id**. - * - * if both informations be provided, and they are from two differents - * roles, the result will correspond to the role that matchs with the parameter - * **id**. - * - * @throws CordeClientError if corde's bot is not connected. - * @returns Role that matches the provided **id** or **name** - */ - findRole(data: IRoleIdentifier): Role | undefined; - findRole(data: string | IRoleIdentifier) { - if (!this._bot.isLoggedIn()) { - throw new CordeClientError("Bot is not connected yet. No role can be searched"); - } - - return this._findRole(data); - } - - private _findRole(data: string | IRoleIdentifier) { - if (typeof data === "string") { - return this._bot.getRoles().find((r) => r.id === data); - } - return this._bot.getRoles().find((r) => r.id === data.id || r.name === data.name); - } -} diff --git a/src/api/botAPI.ts b/src/api/botAPI.ts new file mode 100644 index 000000000..5d9b5b588 --- /dev/null +++ b/src/api/botAPI.ts @@ -0,0 +1,623 @@ +import { + CategoryChannel, + DMChannel, + Guild, + Message, + NewsChannel, + Role, + TextChannel, + VoiceChannel, +} from "discord.js"; +import { CordeClientError } from "../errors"; +import { mapper } from "../mapper/messageMapper"; +import { + IChannelIdentifier, + ICordeBot, + ICreateChannelOptions, + ICreateChannelOptionsSimple, + IGuildCreateOptions, + IGuildIdentifier, + IMessageEmbed, + IRoleData, + IRoleIdentifier, + Primitive, +} from "../types"; +import { isPrimitiveValue } from "../utils"; + +export class BotAPI { + private _bot: ICordeBot; + + /** + * Gets the voice channel state that corde's bot is connected in, If it's connected. + * This property is filled when `joinVoiceChannel()` connects to a channel + * and is cleared when `leaveVoiceChannal()` is called. + */ + get voiceState() { + return this._bot.voiceConnection; + } + + /** + * Client of Discord.js + */ + get client() { + return this._bot.client; + } + + /** + * Same of `this.getChannel()` + * @throws Error if corde bot is not connected. + */ + get channel() { + return this.getChannel(); + } + + /** + * Get all channels in **cache** of the bot. + * @throws Error if corde bot is not connected. + */ + get channels() { + this._throwErrorIfNotLogged(); + return this._bot.client.channels.cache.array(); + } + + /** + * Same of `this.getGuild()` + * @throws Error if corde bot is not connected. + */ + get guild() { + return this.getGuild(); + } + + /** + * Members of the guild defined in configs + * @throws Error if corde bot is not connected. + */ + get guildMembers() { + this._throwErrorIfNotLogged(); + return this.guild.members.cache.array(); + } + + /** + * Get all guilds in **cache** of the bot. + * @throws Error if corde bot is not connected. + */ + get guilds() { + this._throwErrorIfNotLogged(); + return this._bot.client.guilds.cache.array(); + } + + /** + * Get all roles in **cache** of the guild + * defined in configs. + * + * Shortcut for: + * + * ```typescript + * this.getGuild().roles.cache.array() + * ``` + * @throws Error if corde bot is not connected. + */ + get roles() { + this._throwErrorIfNotLogged(); + return this.getGuild().roles.cache.array(); + } + + /** + * Checks if corde's bot is connected and ready. + */ + get isLoggedIn() { + return this._bot.isLoggedIn(); + } + + constructor(bot: ICordeBot) { + this._bot = bot; + } + + /** + * Checks if a given message was sent by corde's bot + * @param message Sent message + * @returns If corde's bot is the author of the message + */ + isMessageAuthor(message: Message) { + return message.author.id === this._bot.id; + } + + /** + * Joins corde's bot to a voice channel. + * @param channelId Voice channel to corde's bot connect + * @throws Error if corde bot is not connected. + * @returns Voice connection state. This property can be get from `bot.voiceState` + */ + joinVoiceChannel(channelId: string) { + this._throwErrorIfNotLogged( + "Can not join a voice channel while corde bot is not connected yet", + ); + return this._bot.joinVoiceChannel(channelId); + } + + /** + * Leaves a voice channel. + * @throws Error if corde bot is not connected. + */ + leaveVoiceChannel() { + this._throwErrorIfNotLogged("Can not leave a voice channel as corde bot is not connected yet"); + this._bot.leaveVoiceChannel(); + } + + /** + * From all channels in **cache**, get all that are of type text + * @throws Error if corde bot is not connected. + */ + getOnlyTextChannels() { + this._throwErrorIfNotLogged(); + return this.channels.filter((c) => c.isText()) as (TextChannel | DMChannel | NewsChannel)[]; + } + + /** + * Checks if corde's bot is in a voice channel + */ + isInVoiceChannel() { + return this._bot.isInVoiceChannel(); + } + + /** + * Makes a fetch of a channel based on it's `id`. + * @param id Id of the channel. + * @throws Error if corde bot is not connected. + * @returns Channel if it's found + */ + fetchChannel(id: string) { + this._throwErrorIfNotLogged(); + return this._bot.fetchChannel(id); + } + + /** + * Makes a fetch of a guild based on it's `id`. + * @param id Id of the guild + * @throws Error if corde bot is not connected. + * @returns Guild if it's found + */ + fetchGuild(id: string) { + this._throwErrorIfNotLogged(); + return this._bot.fetchGuild(id); + } + + /** + * Fetch for a role based on it's id, caching it after that. + * @param roleId Id of the role. + * @throws Error if corde's bot isn't connected yet. + * @returns Fetched Role or undefined. + */ + async fetchRole(roleId: string): Promise; + /** + * Fetch for a role based on it's id, and it's guild's id, caching it after that. + * + * @param roleId Id of the role. + * @param guildId Id of the guild. + * @param fetchGuild Define if the guild should be fetched or searched in cache. + * + * @throws Error if corde's bot isn't connected yet. + * + * @returns Fetched Role or undefined. + */ + async fetchRole(roleId: string, guildId: string, fetchGuild?: boolean): Promise; + async fetchRole(roleId: string, guildId?: string, fetchGuild = false): Promise { + this._throwErrorIfNotLogged("Could not create the guild while corde bot isn't connected yet"); + + if (guildId) { + const guild = await this._getOrFetchGuild(guildId, fetchGuild); + + if (!guild) { + return undefined; + } + + return this._fetchRole(roleId, this.guild); + } + return this._fetchRole(roleId, this.guild); + } + + private _getOrFetchGuild(guildId: string, fetchGuild = false) { + if (fetchGuild) { + return this._bot.client.guilds.fetch(guildId); + } else { + return this._bot.client.guilds.cache.get(guildId); + } + } + + /** + * Gets the channel defined in `configs` + * @throws Error if corde bot is not connected. + */ + getChannel(): TextChannel; + /** + * Gets a channel from `client.channels.cache` based on the channel's id + * + * @param id Channel Id + * @throws Error if corde bot is not connected. + * @return Channel searched by it's id or undefined. + */ + getChannel(id: string): TextChannel | undefined; + /** + * Gets a channel from `client.channels.cache` based on the channel's id or name + * + * @param identifier Channel's identifier + * @throws Error if corde bot is not connected. + * @return Channel searched or undefined. + */ + getChannel(identifier: IChannelIdentifier): TextChannel | undefined; + getChannel(identifier?: string | IChannelIdentifier) { + this._throwErrorIfNotLogged("Corde is not connected yet to fetch any data"); + + if (!identifier) { + return this._bot.channel; + } + + if (typeof identifier === "string") { + return this._bot.client.channels.cache.find((c) => c.id === identifier); + } + + return this._bot.client.channels.cache.find((c) => { + if (c.id === identifier.id) { + return true; + } + + if (c.isText()) { + return (c as TextChannel).name === identifier.name; + } + + return false; + }); + } + + /** + * Gets the guild defined in `configs` + * @throws Error if corde bot is not connected. + */ + getGuild(): Guild; + /** + * Gets a guild from `client.channels.guild` based on the guild's id + * @param id Guild Id + * @throws Error if corde bot is not connected. + * @return Guild searched by it's id or undefined. + */ + getGuild(id: string): Guild | undefined; + /** + * Gets a guild from `client.guild.cache` based on the guild's id or name + * + * @param identifier Guild's identifier + * @throws Error if corde bot is not connected. + * @return Guild searched or undefined. + */ + getGuild(identifier: IGuildIdentifier): Guild | undefined; + getGuild(identifier?: string | IGuildIdentifier) { + this._throwErrorIfNotLogged("Can not get any guild while corde bot is not connected yet"); + + if (!identifier) { + return this._bot.guild; + } + + if (typeof identifier === "string") { + return this._bot.client.guilds.cache.find((c) => c.id === identifier); + } + + return this._bot.client.guilds.cache.find( + (c) => c.id === identifier.id || c.name === identifier.name, + ); + } + + /** + * Sends a message to the connected textChannel. + * + * **This function does not work without a test case** + * + * @param message Message to send + * + * @example + * + * // Works + * test("test 1", () => { + * const message = await corde.bot.send("msg"); + * expect(`editMessage ${message.id}`).toEditMessage({ id: message.id }, "newValue"); + * }); + * + * // Do not Works + * group("test 1", () => { + * const message = await corde.bot.send("msg"); + * }); + * + * @throws Error if corde bot is not connected. + * @throws Error If message is invalid. + * + * @returns null if message is empty, null or undefined. + * Message if **message** is not empty and it was send to Discord. + * + * @since 2.0 + */ + send(message: string | number | boolean | bigint): Promise; + send(message: IMessageEmbed): Promise; + send(message: Primitive | IMessageEmbed): Promise { + if (!message) { + throw new Error("Can not send a empty message"); + } + + this._throwErrorIfNotLogged( + "Can not send a directly message to channel because the client is not connected yet", + ); + + if (isPrimitiveValue(message)) { + return this._bot.sendMessage(message); + } + + const embed = mapper.embedInterfaceToMessageEmbed(message); + return this._bot.sendMessage(embed); + } + + /** + * Creates a new role inside the guild provided in configs. + * + * @param name Name of the role. + * @throws CordeClientError if corde has not yet connect it's bot. + * @returns A promise that return the created role. + * + * @since 2.1 + */ + createRole(name?: string): Promise; + /** + * Creates a new role inside the guild provided in configs. + * + * @param data Basic informations about the role. + * @throws CordeClientError if corde has not yet connect it's bot. + * @returns A promise that return the created role. + * + * @since 2.1 + */ + createRole(data: IRoleData): Promise; + createRole(data?: string | IRoleData) { + this._throwErrorIfNotLogged("Bot is not connected yet. Can not create a role"); + if (typeof data === "string") { + return this._bot.roleManager.create({ data: { name: data } }); + } + return this._bot.roleManager.create({ data }); + } + + private _throwErrorIfInvalidName(name: string, errorMessage: string) { + if (!name || typeof name !== "string" || !name.trim()) { + throw new Error(errorMessage); + } + } + + /** + * Creates a new `guild` in defined in configs. + * + * Shortcut for: + * + * ```typescript + * this.client.guilds.create("nameExample"); + * ``` + * + * @param name Name of the new guild. + * @throws Error if corde's bot isn't connected yet. + * @returns Created guild. + */ + createGuild(name: string): Promise; + /** + * Creates a new `guild` in defined in configs. + * + * Shortcut for: + * + * ```typescript + * this.client.guilds.create("exampleName", { ... }); + * ``` + * + * @param options Informations about the guild. + * @throws Error if corde's bot isn't connected yet. + * @returns Created guild. + */ + createGuild(options: IGuildCreateOptions): Promise; + createGuild(options: string | IGuildCreateOptions) { + this._throwErrorIfNotLogged("Could not create the guild while corde bot isn't connected yet"); + + const errorMessage = "Could not create a guild with an invalid name"; + if (typeof options === "string") { + this._throwErrorIfInvalidName(options, errorMessage); + return this._bot.client.guilds.create(options); + } + + this._throwErrorIfInvalidName(options.name, errorMessage); + return this._bot.client.guilds.create(options.name, options); + } + + /** + * Creates a new channel in guild defined in configs. + * + * Shortcut for: + * + * ```typescript + * this.client.channels.create("exampleName"); + * ``` + * + * @param name Name of the new channel. + * @throws Error if corde's bot isn't connected yet. + * @returns Created channel. + */ + createChannel(name: string): Promise; + /** + * Creates a new channel in guild defined in configs. + * + * Shortcut for: + * + * ```typescript + * this.client.channels.create("exampleName", { ... }); + * ``` + * + * @param options Informations about the channel, including it's type. + * @throws Error if corde's bot isn't connected yet. + * @returns Created channel. + */ + createChannel( + channelOptions: ICreateChannelOptions, + ): Promise; + createChannel(options: string | ICreateChannelOptions) { + this._throwErrorIfNotLogged("Could not create the channel while corde bot isn't connected yet"); + + if (typeof options === "string") { + return this.guild.channels.create(options); + } + return this.guild.channels.create(options.name, options); + } + + /** + * Creates a new **voice** channel in guild defined in configs. + * + * Shortcut for: + * + * ```typescript + * this.client.channels.create("exampleName", { type: "voice" }); + * ``` + * + * @param name Name of the new channel. + * @throws Error if corde's bot isn't connected yet. + * @returns Created channel. + */ + createVoiceChannel(name: string): Promise; + /** + * Creates a new **voice** channel in guild defined in configs. + * + * Shortcut for: + * + * ```typescript + * this.client.channels.create("exampleName", { ..., type: "voice" }); + * ``` + * + * @param options Informations about the channel. + * @throws Error if corde's bot isn't connected yet. + * @returns Created channel. + */ + createVoiceChannel(options: ICreateChannelOptionsSimple): Promise; + createVoiceChannel(options: string | ICreateChannelOptionsSimple) { + return this._createChannel(options, "voice"); + } + + /** + * Creates a new **text** channel in guild defined in configs. + * + * Shortcut for: + * + * ```typescript + * this.client.channels.create("exampleName", { type: "text" }); + * ``` + * + * @param name Name of the new channel. + * @throws Error if corde's bot isn't connected yet. + * @returns Created channel. + */ + createTextChannel(name: string): Promise; + /** + * Creates a new **text** channel in guild defined in configs. + * + * Shortcut for: + * + * ```typescript + * this.client.channels.create("exampleName", { ..., type: "text" }); + * ``` + * + * @param options Informations about the channel. + * @throws Error if corde's bot isn't connected yet. + * @returns Created channel. + */ + createTextChannel(options: ICreateChannelOptionsSimple): Promise; + createTextChannel(options: string | ICreateChannelOptionsSimple) { + return this._createChannel(options, "text"); + } + + /** + * Creates a new **category** channel in guild defined in configs. + * + * Shortcut for: + * + * ```typescript + * this.client.channels.create("exampleName", { type: "category" }); + * ``` + * + * @param name Name of the new channel. + * @throws Error if corde's bot isn't connected yet. + * @returns Created channel. + */ + createCategoryChannel(name: string): Promise; + /** + * Creates a new **category** channel in guild defined in configs. + * + * Shortcut for: + * + * ```typescript + * this.client.channels.create("exampleName", { ..., type: "category" }); + * ``` + * + * @param options Informations about the channel. + * @throws Error if corde's bot isn't connected yet. + * @returns Created channel. + */ + createCategoryChannel(options: ICreateChannelOptionsSimple): Promise; + createCategoryChannel(options: string | ICreateChannelOptionsSimple) { + return this._createChannel(options, "category"); + } + + /** + * Finds a role in config guild's cache, basing on it's **id** + * + * @param id Id of the role. + * @throws CordeClientError if corde's bot is not connected. + * @returns Role that matches the provided **id** or **name** + */ + getRole(id: string): Role | undefined; + /** + * Finds a role in config guild's cache, basing on it's **id** or **name**. + * + * @param data Data of the role. It can be it's **name** or **id**. + * + * if both informations be provided, and they are from two differents + * roles, the result will correspond to the role that matchs with the parameter + * **id**. + * + * @throws CordeClientError if corde's bot is not connected. + * @returns Role that matches the provided **id** or **name** + */ + getRole(data: IRoleIdentifier): Role | undefined; + getRole(data: string | IRoleIdentifier) { + this._throwErrorIfNotLogged("Bot is not connected yet. No role can be searched"); + return this._getRole(data); + } + + private async _fetchRole(roleId: string, guild: Guild) { + const role = await guild.roles.fetch(roleId, true); + if (role) { + return role; + } + return undefined; + } + + private _getRole(data: string | IRoleIdentifier) { + if (typeof data === "string") { + return this._bot.getRoles().find((r) => r.id === data); + } + return this._bot.getRoles().find((r) => r.id === data.id || r.name === data.name); + } + + private _createChannel( + options: string | ICreateChannelOptionsSimple, + type?: ICreateChannelOptions["type"], + ) { + this._throwErrorIfNotLogged("Could not create the channel while corde bot isn't connected yet"); + + if (typeof options === "string") { + return this.guild.channels.create(options, { type }); + } + return this.guild.channels.create(options.name, { ...options, type }); + } + + private _throwErrorIfNotLogged(message = "Corde is not connected yet to fetch any data") { + if (!this.isLoggedIn) { + throw new CordeClientError(message); + } + } +} diff --git a/src/api/configAPI.ts b/src/api/configAPI.ts new file mode 100644 index 000000000..af3562f8b --- /dev/null +++ b/src/api/configAPI.ts @@ -0,0 +1,66 @@ +import { DEFAULT_CONFIG } from "../consts"; +import { IConfigOptions } from "../types"; + +export class ConfigAPI implements Readonly> { + constructor(private _internalConfigs: IConfigOptions) {} + + get cordeBotToken() { + return this._internalConfigs.cordeBotToken; + } + + get botTestId() { + return this._internalConfigs.botTestId; + } + + get botToken() { + return this._internalConfigs.botToken; + } + + get channelId() { + return this._internalConfigs.channelId; + } + + get guildId() { + return this._internalConfigs.guildId; + } + + get timeout() { + return this._internalConfigs.timeout ?? DEFAULT_CONFIG.timeout; + } + + get botPrefix() { + return this._internalConfigs.botPrefix; + } + + get testMatches() { + return [...this._internalConfigs.testMatches]; + } + + get modulePathIgnorePatterns() { + const modulePathIgnorePatterns = this._internalConfigs.modulePathIgnorePatterns; + if (modulePathIgnorePatterns) { + return [...modulePathIgnorePatterns]; + } + return [...DEFAULT_CONFIG.modulePathIgnorePatterns]; + } + + get project() { + return this._internalConfigs.project ?? DEFAULT_CONFIG.project; + } + + get exitOnFileReadingError() { + return this._internalConfigs.exitOnFileReadingError ?? DEFAULT_CONFIG.exitOnFileReadingError; + } + + get extensions() { + const extension = this._internalConfigs.extensions; + if (extension) { + return [...extension]; + } + return [...DEFAULT_CONFIG.extensions]; + } + + get rootDir() { + return this._internalConfigs.rootDir ?? DEFAULT_CONFIG.rootDir; + } +} diff --git a/src/api/index.ts b/src/api/index.ts index 1b9bc2ae0..d63ca9e69 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1 +1,2 @@ -export * from "./bot"; +export * from "./botAPI"; +export * from "./configAPI"; diff --git a/src/cli/init.ts b/src/cli/init.ts index 3a645c948..720399e68 100644 --- a/src/cli/init.ts +++ b/src/cli/init.ts @@ -54,8 +54,6 @@ function getFileFromType(type: ConfigFileType) { if (type === "json") { return convertObjectToFileType(true); } else if (type === "js") { - const temp = { ...DEFAULT_CONFIG }; - delete temp.project; return `module.exports = ${convertObjectToFileType(false)}`; } else if (type === "ts") { return `export = ${convertObjectToFileType(false)}`; diff --git a/src/consts.ts b/src/consts.ts index ab573cb86..87d7bc5ab 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -13,7 +13,7 @@ export const TEXT_PENDING = chalk.yellow; export const TEXT_EMPTY = chalk.yellowBright; export const ROOT_DIR = ""; -export const DEFAULT_CONFIG: IConfigOptions = { +export const DEFAULT_CONFIG: Required = { botPrefix: "", botTestId: "", channelId: "", diff --git a/src/corde.ts b/src/corde.ts index e4d64c091..ae04deb82 100644 --- a/src/corde.ts +++ b/src/corde.ts @@ -7,8 +7,18 @@ import { import { expect as _expect } from "./expect"; import { group as _group, test as _test } from "./closures"; -import { Bot } from "./api"; +import { BotAPI, ConfigAPI } from "./api"; import { runtime } from "./common/runtime"; +import { IConfigOptions } from "./types"; + +function getConfigs(): Readonly> { + return new ConfigAPI(runtime.configs); +} + +function getBot() { + return new BotAPI(runtime.bot); +} + /** * Corde's utility namespace to call it's API functions. * You can also import each function desconstructing in corde lib import @@ -23,5 +33,12 @@ export namespace corde { export const describe = _group; export const it = _test; export const test = _test; - export const bot = new Bot(runtime.bot); + /** + * Corde's bot API + */ + export const bot = getBot(); + /** + * Corde's configs for readonly purpose + */ + export const configs = getConfigs(); } diff --git a/src/types/general.ts b/src/types/general.ts index 35ff482e7..624aa9ba8 100644 --- a/src/types/general.ts +++ b/src/types/general.ts @@ -1,4 +1,4 @@ -import { BitField } from "discord.js"; +import { BitField, GuildCreateChannelOptions, GuildCreateOptions } from "discord.js"; import { Stream } from "stream"; export interface IAuthor { @@ -65,6 +65,26 @@ export interface IRoleIdentifier extends IIdentifier { name?: string; } +export interface IChannelIdentifier extends IIdentifier { + name?: string; +} + +export type ChannelType = "voice" | "text" | "category"; + +export interface IGuildCreateOptions extends GuildCreateOptions { + name: string; +} + +export interface ICreateChannelOptions extends GuildCreateChannelOptions { + name: string; +} + +export interface ICreateChannelOptionsSimple extends Omit {} + +export interface IGuildIdentifier extends IIdentifier { + name?: string; +} + export interface IBaseRole { name?: string; color?: ColorResolvable | Colors; diff --git a/tests/api/bot.test.ts b/tests/api/bot.test.ts deleted file mode 100644 index 47aa55108..000000000 --- a/tests/api/bot.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import corde from "../../src"; -import { Bot } from "../../src/api"; -import { runtime } from "../../src/common/runtime"; -import MockDiscord from "../mocks/mockDiscord"; -import { createCordeBotWithMockedFunctions } from "../testHelper"; - -const mockDiscord = new MockDiscord(); - -describe("testing corde bot API", () => { - it("should call runtime.bot.isLoggedIn", () => { - const spy = jest.spyOn(runtime.bot, "isLoggedIn"); - corde.bot.isLoggedIn; - expect(spy).toBeCalled(); - }); - - it("should call runtime.bot.isLoggedIn", async () => { - const spy = jest - .spyOn(runtime.bot, "sendMessage") - .mockImplementation(() => Promise.resolve(mockDiscord.message)); - - jest.spyOn(runtime.bot, "isLoggedIn").mockReturnValue(true); - - await corde.bot.send("test"); - expect(spy).toBeCalledWith("test"); - }); - - it("should return true for message author be corde's bot", () => { - mockDiscord.message.author.id = runtime.bot.id; - corde.bot.isMessageAuthor(mockDiscord.message); - }); - - it("should call joinVoiceChannel", async () => { - const spy = jest.spyOn(runtime.bot, "joinVoiceChannel").mockImplementation(null); - await corde.bot.joinVoiceChannel(""); - expect(spy).toBeCalledTimes(1); - }); - - it("should call leaveVoiceChannel", async () => { - const spy = jest.spyOn(runtime.bot, "leaveVoiceChannel").mockImplementation(null); - corde.bot.leaveVoiceChannel(); - expect(spy).toBeCalledTimes(1); - }); - - it("should call isInVoiceChannel", async () => { - const spy = jest.spyOn(runtime.bot, "isInVoiceChannel").mockImplementation(null); - corde.bot.isInVoiceChannel(); - expect(spy).toBeCalledTimes(1); - }); - - it("should call fetchChannel", async () => { - const spy = jest.spyOn(runtime.bot, "fetchChannel").mockImplementation(null); - await corde.bot.fetchChannel("1"); - expect(spy).toBeCalledTimes(1); - }); - - it("should call fetchGuild", async () => { - const spy = jest.spyOn(runtime.bot, "fetchGuild").mockImplementation(null); - await corde.bot.fetchGuild("1"); - expect(spy).toBeCalledTimes(1); - }); -}); diff --git a/tests/api/botAPI.test.ts b/tests/api/botAPI.test.ts new file mode 100644 index 000000000..0ec8dd5bc --- /dev/null +++ b/tests/api/botAPI.test.ts @@ -0,0 +1,522 @@ +import { Client } from "discord.js"; +import { BotAPI } from "../../src/api"; +import { mapper } from "../../src/mapper/messageMapper"; +import { ICordeBot, IMessageEmbed } from "../../src/types"; +import MockDiscord from "../mocks/mockDiscord"; +import { initCordeClientWithChannel } from "../testHelper"; + +const mockDiscord = new MockDiscord(); + +let bot: BotAPI; +let cordeBot: ICordeBot; + +beforeEach(() => { + const client = new Client(); + client.readyAt = new Date(); + cordeBot = initCordeClientWithChannel(mockDiscord, client); + client.emit("ready"); + bot = new BotAPI(cordeBot); +}); + +describe("testing corde bot API", () => { + it("should call runtime.bot.isLoggedIn", () => { + const spy = jest.spyOn(cordeBot, "isLoggedIn"); + bot.isLoggedIn; + expect(spy).toBeCalled(); + }); + + it("should call runtime.bot.isLoggedIn", async () => { + const spy = jest + .spyOn(cordeBot, "sendMessage") + .mockImplementation(() => Promise.resolve(mockDiscord.message)); + + await bot.send("test"); + expect(spy).toBeCalledWith("test"); + }); + + it("should throw error due to invalid message", async () => { + try { + await bot.send(undefined); + fail(); + } catch (error) { + expect(error).toBeTruthy(); + } + }); + + it("should send a embed message", async () => { + const spy = jest + .spyOn(cordeBot, "sendMessage") + .mockImplementation(() => Promise.resolve(mockDiscord.message)); + + const embed: IMessageEmbed = { + author: "me", + }; + + await bot.send(embed); + expect(spy).toBeCalledWith(mapper.embedInterfaceToMessageEmbed(embed)); + }); + + it("should return true for message author be corde's bot", () => { + mockDiscord.message.author.id = cordeBot.id; + bot.isMessageAuthor(mockDiscord.message); + }); + + it("should call joinVoiceChannel", async () => { + const spy = jest.spyOn(cordeBot, "joinVoiceChannel").mockImplementation(null); + await bot.joinVoiceChannel(""); + expect(spy).toBeCalledTimes(1); + }); + + it("should call leaveVoiceChannel", async () => { + const spy = jest.spyOn(cordeBot, "leaveVoiceChannel").mockImplementation(null); + bot.leaveVoiceChannel(); + expect(spy).toBeCalledTimes(1); + }); + + it("should call isInVoiceChannel", async () => { + const spy = jest.spyOn(cordeBot, "isInVoiceChannel").mockImplementation(null); + bot.isInVoiceChannel(); + expect(spy).toBeCalledTimes(1); + }); + + it("should call fetchChannel", async () => { + const spy = jest.spyOn(cordeBot, "fetchChannel").mockImplementation(null); + await bot.fetchChannel("1"); + expect(spy).toBeCalledTimes(1); + }); + + it("should call fetchGuild", async () => { + const spy = jest.spyOn(cordeBot, "fetchGuild").mockImplementation(null); + await bot.fetchGuild("1"); + expect(spy).toBeCalledTimes(1); + }); + + it("should get discord.js client", () => { + expect(bot.client).toEqual(cordeBot.client); + }); + + describe("testing bot.channels", () => { + it("should get discord.js client channels cache", () => { + expect(bot.channels).toEqual(cordeBot.client.channels.cache.array()); + }); + + it("should throw error due to not logged bot", () => { + jest.spyOn(cordeBot, "isLoggedIn").mockReturnValueOnce(false); + expect(() => bot.channels).toThrowError(); + }); + }); + + describe("testing bot.channel", () => { + it("should throw error due to not logged bot", () => { + jest.spyOn(cordeBot, "isLoggedIn").mockReturnValueOnce(false); + expect(() => bot.channel).toThrowError(); + }); + + it("should get discord.js client channel", () => { + expect(bot.channel).toEqual(cordeBot.channel); + }); + }); + + describe("testing bot.guild", () => { + it("should throw error due to not logged bot", () => { + jest.spyOn(cordeBot, "isLoggedIn").mockReturnValueOnce(false); + expect(() => bot.guild).toThrowError(); + }); + + it("should get discord.js client guild", () => { + expect(bot.guild).toEqual(cordeBot.guild); + }); + }); + + describe("testing bot.guilds", () => { + it("should throw error due to not logged bot", () => { + jest.spyOn(cordeBot, "isLoggedIn").mockReturnValueOnce(false); + expect(() => bot.guilds).toThrowError(); + }); + + it("should get discord.js client guilds", () => { + expect(bot.guilds).toEqual(cordeBot.client.guilds.cache.array()); + }); + }); + + describe("testing bot.guildMembers", () => { + it("should throw error due to not logged bot", () => { + jest.spyOn(cordeBot, "isLoggedIn").mockReturnValueOnce(false); + expect(() => bot.guildMembers).toThrowError(); + }); + it("should get discord.js client members", () => { + expect(bot.guildMembers).toEqual(cordeBot.guild.members.cache.array()); + }); + }); + + describe("testing bot.roles", () => { + it("should throw error due to not logged bot", () => { + jest.spyOn(cordeBot, "isLoggedIn").mockReturnValueOnce(false); + expect(() => bot.roles).toThrowError(); + }); + + it("should get discord.js client roles", () => { + expect(bot.roles).toEqual(cordeBot.guild.roles.cache.array()); + }); + }); + + it("should get only textChannels", () => { + expect(bot.getOnlyTextChannels()).toHaveLength(0); + cordeBot.client.channels.cache = mockDiscord.textChannelCollection; + expect(bot.getOnlyTextChannels().length).not.toBe(0); + }); + + describe("testing fetchRole", () => { + it("should fail due to bot not logged", async () => { + jest.spyOn(cordeBot, "isLoggedIn").mockReturnValueOnce(false); + try { + await bot.fetchRole("1"); + fail(); + } catch (error) { + expect(error).toBeTruthy(); + } + }); + + it("should fetchRole return undefined based on roleId", async () => { + const spy = jest.spyOn(cordeBot.guild.roles, "fetch").mockImplementation(null); + const response = await bot.fetchRole("1"); + expect(response).toEqual(undefined); + expect(spy).toBeCalled(); + }); + + it("should get based on roleId", async () => { + // Argument of type 'Role' is not assignable to parameter of type 'RoleManager | Promise'. + // Type 'Role' is missing the following properties from type 'RoleManager': everyone, highest, create, fetch, and 6 more. + const spy = jest + .spyOn(cordeBot.guild.roles, "fetch") + .mockResolvedValue(mockDiscord.role as any); + const response = await bot.fetchRole("1"); + expect(response).toEqual(mockDiscord.role); + expect(spy).toBeCalled(); + }); + + it("should get by invalid guildId without fetch", async () => { + cordeBot.client.guilds.cache = mockDiscord.guildCollection; + const spy = jest + .spyOn(cordeBot.guild.roles, "fetch") + .mockResolvedValue(mockDiscord.role as any); + const response = await bot.fetchRole("1", mockDiscord.guildCollection.first().id); + expect(response).toEqual(mockDiscord.role); + expect(spy).toBeCalled(); + }); + + it("should get by invalid guildId with fetch", async () => { + cordeBot.client.guilds.cache = mockDiscord.guildCollection; + + const spy = jest + .spyOn(cordeBot.guild.roles, "fetch") + .mockResolvedValue(mockDiscord.role as any); + + const spyGuildFetch = jest + .spyOn(cordeBot.client.guilds, "fetch") + .mockResolvedValue(mockDiscord.guild as any); + + const response = await bot.fetchRole("1", mockDiscord.guildCollection.first().id, true); + expect(response).toEqual(mockDiscord.role); + expect(spy).toBeCalled(); + expect(spyGuildFetch).toBeCalled(); + }); + + it("should get by invalid guildId, not finding the guild", async () => { + cordeBot.client.guilds.cache = mockDiscord.guildCollection; + jest.spyOn(cordeBot.guild.roles, "fetch").mockResolvedValue(mockDiscord.role as any); + const spyGuildFetch = jest + .spyOn(cordeBot.client.guilds, "fetch") + .mockResolvedValue(undefined); + const response = await bot.fetchRole("1", "1231", true); + expect(response).toEqual(undefined); + expect(spyGuildFetch).toBeCalled(); + }); + }); + + it("should get voiceState", () => { + expect(bot.voiceState).toEqual(cordeBot.voiceConnection); + }); + + describe("testing getChannel", () => { + it("should get channel defined in configs", () => { + cordeBot.client.channels.cache = mockDiscord.textChannelCollection; + expect(bot.getChannel()).toEqual(cordeBot.channel); + }); + + it("should return undefined due to no channel found in cache", () => { + cordeBot.client.channels.cache = mockDiscord.textChannelCollection; + expect(bot.getChannel({ id: "321foo" })).toEqual(undefined); + }); + + it("should get channel by id", () => { + cordeBot.client.channels.cache = mockDiscord.textChannelCollection; + const channel = mockDiscord.textChannelCollection.first(); + expect(bot.getChannel(channel.id)).toEqual(channel); + }); + + it("should get channel by id inside object", () => { + cordeBot.client.channels.cache = mockDiscord.textChannelCollection; + const channel = mockDiscord.textChannelCollection.first(); + expect(bot.getChannel({ id: channel.id })).toEqual(channel); + }); + + it("should get channel by name inside object", () => { + cordeBot.client.channels.cache = mockDiscord.textChannelCollection; + const channel = mockDiscord.textChannelCollection.first(); + expect(bot.getChannel({ name: channel.name })).toEqual(channel); + }); + + it("should return undefined", () => { + cordeBot.client.channels.cache = mockDiscord.textChannelCollection; + expect(bot.getChannel({ id: "aaaa" })).toEqual(undefined); + }); + }); + + describe("testing getGuild", () => { + it("should get guild defined in configs", () => { + cordeBot.client.guilds.cache = mockDiscord.guildCollection; + expect(bot.getGuild()).toEqual(cordeBot.guild); + }); + + it("should get guild by id", () => { + cordeBot.client.guilds.cache = mockDiscord.guildCollection; + const guild = mockDiscord.guildCollection.first(); + expect(bot.getGuild(guild.id)).toEqual(guild); + }); + + it("should get guild by id inside object", () => { + cordeBot.client.guilds.cache = mockDiscord.guildCollection; + const guild = mockDiscord.guildCollection.first(); + expect(bot.getGuild({ id: guild.id })).toEqual(guild); + }); + + it("should get guild by name inside object", () => { + cordeBot.client.guilds.cache = mockDiscord.guildCollection; + const guild = mockDiscord.guildCollection.first(); + expect(bot.getGuild({ name: guild.name })).toEqual(guild); + }); + + it("should return undefined", () => { + cordeBot.client.guilds.cache = mockDiscord.guildCollection; + expect(bot.getGuild({ id: "aaaa" })).toEqual(undefined); + }); + }); + + describe("testing createRole", () => { + it("should call roleManager to create a new role passing object", async () => { + const spy = jest.spyOn(cordeBot.roleManager, "create").mockImplementation(null); + await bot.createRole({}); + expect(spy).toBeCalled(); + }); + + it("should call roleManager to create a new role passing string", async () => { + const spy = jest.spyOn(cordeBot.roleManager, "create").mockImplementation(null); + await bot.createRole("aa"); + expect(spy).toBeCalled(); + }); + + it("should throw exception due to invalid name", async () => { + try { + await bot.createRole(undefined); + fail(); + } catch (error) { + expect(error).toBeTruthy(); + } + }); + + it("should throw exception due to bot not logged in", async () => { + jest.spyOn(cordeBot, "isLoggedIn").mockReturnValueOnce(false); + try { + await bot.createRole({}); + fail(); + } catch (error) { + expect(error).toBeTruthy(); + } + }); + + it("should call guildManager to create a new guild", async () => { + const spy = jest.spyOn(cordeBot.roleManager, "create").mockImplementation(null); + await bot.createRole({ name: "aaa" }); + expect(spy).toBeCalled(); + }); + }); + + describe("testing createGuild", () => { + it("should call guildManager to create a new role passing object", async () => { + const spy = jest.spyOn(cordeBot.client.guilds, "create").mockImplementation(null); + await bot.createGuild({ + name: "test", + }); + expect(spy).toBeCalled(); + }); + + it("should call guildManager to create a new role passing string", async () => { + const spy = jest.spyOn(cordeBot.client.guilds, "create").mockImplementation(null); + await bot.createGuild("aa"); + expect(spy).toBeCalled(); + }); + + it("should throw exception due to invalid name", async () => { + try { + await bot.createGuild(undefined); + fail(); + } catch (error) { + expect(error).toBeTruthy(); + } + }); + + it("should throw exception due to bot not logged in", async () => { + jest.spyOn(cordeBot, "isLoggedIn").mockReturnValueOnce(false); + try { + await bot.createGuild({ + name: "test", + }); + fail(); + } catch (error) { + expect(error).toBeTruthy(); + } + }); + + it("should call guildManager to create a new guild", async () => { + const spy = jest.spyOn(cordeBot.client.guilds, "create").mockImplementation(null); + await bot.createGuild({ name: "aaa" }); + expect(spy).toBeCalled(); + }); + }); + + describe("testing createChannel", () => { + it("should throw exception due to bot not logged in", async () => { + jest.spyOn(cordeBot, "isLoggedIn").mockReturnValueOnce(false); + try { + await bot.createChannel(""); + fail(); + } catch (error) { + expect(error).toBeTruthy(); + } + }); + + it("should call guild.channels.create with a string value", async () => { + const spy = jest.spyOn(cordeBot.guild.channels, "create").mockImplementation(null); + await bot.createChannel("aaa"); + expect(spy).toBeCalled(); + }); + + it("should call guild.channels.create with object", async () => { + const spy = jest.spyOn(cordeBot.guild.channels, "create").mockImplementation(null); + await bot.createChannel({ name: "aaa" }); + expect(spy).toBeCalled(); + }); + }); + + describe("testing createVoiceChannel", () => { + it("should call guild.channels.create passing type = 'voice' (using string name)", async () => { + jest.spyOn(cordeBot, "isLoggedIn").mockReturnValueOnce(false); + try { + await bot.createVoiceChannel(""); + fail(); + } catch (error) { + expect(error).toBeTruthy(); + } + }); + + it("should call guild.channels.create passing type = 'voice' (using string name)", async () => { + const spy = jest.spyOn(cordeBot.guild.channels, "create").mockImplementation(null); + const name = "aaa"; + await bot.createVoiceChannel("aaa"); + expect(spy).toBeCalledWith(name, { type: "voice" }); + }); + + it("should call guild.channels.create passing type = 'voice' (using object name)", async () => { + const spy = jest.spyOn(cordeBot.guild.channels, "create").mockImplementation(null); + const name = "aaa"; + await bot.createVoiceChannel({ name }); + expect(spy).toBeCalledWith(name, { name, type: "voice" }); + }); + }); + + describe("testing createTextChannel", () => { + it("should call guild.channels.create passing type = 'text' (using string name)", async () => { + jest.spyOn(cordeBot, "isLoggedIn").mockReturnValueOnce(false); + try { + await bot.createTextChannel(""); + fail(); + } catch (error) { + expect(error).toBeTruthy(); + } + }); + + it("should call guild.channels.create passing type = 'text' (using string name)", async () => { + const spy = jest.spyOn(cordeBot.guild.channels, "create").mockImplementation(null); + const name = "aaa"; + await bot.createTextChannel("aaa"); + expect(spy).toBeCalledWith(name, { type: "text" }); + }); + + it("should call guild.channels.create passing type = 'text' (using object name)", async () => { + const spy = jest.spyOn(cordeBot.guild.channels, "create").mockImplementation(null); + const name = "aaa"; + await bot.createTextChannel({ name }); + expect(spy).toBeCalledWith(name, { name, type: "text" }); + }); + }); + + describe("testing createCategoryChannel", () => { + it("should call guild.channels.create passing type = 'text' (using string name)", async () => { + jest.spyOn(cordeBot, "isLoggedIn").mockReturnValueOnce(false); + try { + await bot.createCategoryChannel(""); + fail(); + } catch (error) { + expect(error).toBeTruthy(); + } + }); + + it("should call guild.channels.create passing type = 'category' (using string name)", async () => { + const spy = jest.spyOn(cordeBot.guild.channels, "create").mockImplementation(null); + const name = "aaa"; + await bot.createCategoryChannel("aaa"); + expect(spy).toBeCalledWith(name, { type: "category" }); + }); + + it("should call guild.channels.create passing type = 'category' (using object name)", async () => { + const spy = jest.spyOn(cordeBot.guild.channels, "create").mockImplementation(null); + const name = "aaa"; + await bot.createCategoryChannel({ name }); + expect(spy).toBeCalledWith(name, { name, type: "category" }); + }); + }); + + describe("testing getRole", () => { + it("should throw error due to not logged bot", () => { + jest.spyOn(cordeBot, "isLoggedIn").mockReturnValueOnce(false); + try { + bot.getRole(""); + fail(); + } catch (error) { + expect(error).toBeTruthy(); + } + }); + + it("should get role from cache using id (string)", () => { + cordeBot.guild.roles.cache = mockDiscord.roleManager.cache; + const testRole = mockDiscord.roleManager.cache.first(); + const role = bot.getRole(testRole.id); + expect(role).toEqual(testRole); + }); + + it("should get role from cache using id (in object)", () => { + cordeBot.guild.roles.cache = mockDiscord.roleManager.cache; + const testRole = mockDiscord.roleManager.cache.first(); + const role = bot.getRole({ id: testRole.id }); + expect(role).toEqual(testRole); + }); + + it("should get role from cache using name (in object)", () => { + cordeBot.guild.roles.cache = mockDiscord.roleManager.cache; + const testRole = mockDiscord.roleManager.cache.first(); + const role = bot.getRole({ name: testRole.name }); + expect(role).toEqual(testRole); + }); + }); +}); diff --git a/tests/api/configAPI.test.ts b/tests/api/configAPI.test.ts new file mode 100644 index 000000000..f00c27373 --- /dev/null +++ b/tests/api/configAPI.test.ts @@ -0,0 +1,91 @@ +import { ConfigAPI } from "../../src/api"; +import { DEFAULT_CONFIG } from "../../src/consts"; +import { IConfigOptions } from "../../src/types"; + +let configAPI: ConfigAPI; +let config: IConfigOptions; + +beforeEach(() => { + config = DEFAULT_CONFIG; + configAPI = new ConfigAPI(config); + configAPI.extensions; +}); + +describe("testing public configs api", () => { + it("should get value from cordeBotToken", () => { + expect(configAPI.cordeBotToken).toEqual(config.cordeBotToken); + }); + + it("should get value from botTestId", () => { + expect(configAPI.botTestId).toEqual(config.botTestId); + }); + + it("should get value from botToken", () => { + expect(configAPI.botToken).toEqual(config.botToken); + }); + + it("should get value from channelId", () => { + expect(configAPI.channelId).toEqual(config.channelId); + }); + + it("should get value from guildId", () => { + expect(configAPI.guildId).toEqual(config.guildId); + }); + + it("should get value from timeout", () => { + expect(configAPI.timeout).toEqual(config.timeout); + }); + + it("should get value from botPrefix", () => { + expect(configAPI.botPrefix).toEqual(config.botPrefix); + }); + + it("should get value from testMatches", () => { + expect(configAPI.testMatches).toEqual(config.testMatches); + }); + + it("changes in testMatches value should not alter original values", () => { + configAPI.testMatches.push("aaaa"); + expect(configAPI.testMatches).toEqual(config.testMatches); + }); + + it("changes in modulePathIgnorePatterns value should not alter original values", () => { + configAPI.modulePathIgnorePatterns.push("aaaa"); + expect(configAPI.modulePathIgnorePatterns).toEqual(config.modulePathIgnorePatterns); + }); + + it("should get value from modulePathIgnorePatterns", () => { + expect(configAPI.modulePathIgnorePatterns).toEqual(config.modulePathIgnorePatterns); + }); + + it("should get modulePathIgnorePatterns from default configs if no value is provided", () => { + configAPI = new ConfigAPI({} as any); + expect(configAPI.modulePathIgnorePatterns).toEqual(DEFAULT_CONFIG.modulePathIgnorePatterns); + }); + + it("should get value from project", () => { + expect(configAPI.project).toEqual(config.project); + }); + + it("should get value from exitOnFileReadingError", () => { + expect(configAPI.exitOnFileReadingError).toEqual(config.exitOnFileReadingError); + }); + + it("changes in extensions value should not alter original values", () => { + configAPI.extensions.push("aaaa"); + expect(configAPI.extensions).toEqual(config.extensions); + }); + + it("should get extensions from default configs if no value is provided", () => { + configAPI = new ConfigAPI({} as any); + expect(configAPI.extensions).toEqual(DEFAULT_CONFIG.extensions); + }); + + it("should get value from extensions", () => { + expect(configAPI.extensions).toEqual(config.extensions); + }); + + it("should get value from rootDir", () => { + expect(configAPI.rootDir).toEqual(config.rootDir); + }); +}); diff --git a/tests/mocks/mockDiscord.ts b/tests/mocks/mockDiscord.ts index cb825a10c..da595c8e8 100644 --- a/tests/mocks/mockDiscord.ts +++ b/tests/mocks/mockDiscord.ts @@ -22,6 +22,7 @@ import { ActivityType, Activity, VoiceState, + VoiceChannel, } from "discord.js"; import { ToReturn } from "../../src/expect/matches"; import { IMessageEmbed } from "../../src/types"; @@ -109,6 +110,9 @@ export default class MockDiscord { private _messageWithEmbed: Message; private _pinnedMessage: Message; private _unPinnedMessage: Message; + private _textChannelCollection!: Collection; + private _guildCollection!: Collection; + private _channelCollection!: Collection; /** * Initialize all mocks @@ -292,6 +296,9 @@ export default class MockDiscord { return this._role; } + /** + * Role manager with some roles added to cache. + */ get roleManager() { return this._roleManager; } @@ -328,6 +335,18 @@ export default class MockDiscord { return this._unPinnedMessage; } + get textChannelCollection() { + return this._textChannelCollection; + } + + get guildCollection() { + return this._guildCollection; + } + + get channelCollection() { + return this._channelCollection; + } + get, K, V>(collection: T, index: number) { return collection.array()[index]; } @@ -369,6 +388,10 @@ export default class MockDiscord { this._guildMemberCollection = this.createGuildMemberCollection(); this._presence = this.createPresence(); + + this._textChannelCollection = this.createMockTextChannelCollection(); + this._channelCollection = this.createMockChannelCollection(); + this._guildCollection = this.createMockGuildCollection(); } createMockClient() { @@ -690,6 +713,36 @@ export default class MockDiscord { return new VoiceState(this.guild, voiceData); } + createMockTextChannelCollection() { + const channels: [string, TextChannel][] = []; + for (let index = 0; index < 5; index++) { + const channel = this.createMockTextChannel(); + channel.name = "randomName" + index; + channel.id = "123221" + index; + channels.push([channel.id, channel]); + } + + const freezed: Readonly<[string, TextChannel][]> = Object.freeze(channels); + return new Collection(freezed); + } + + createMockGuildCollection() { + const guilds: [string, Guild][] = []; + for (let index = 0; index < 5; index++) { + const guild = this.createMockGuild(); + guild.name = "randomName" + index; + guild.id = "123221" + index; + guilds.push([guild.id, guild]); + } + + const freezed: Readonly<[string, Guild][]> = Object.freeze(guilds); + return new Collection(freezed); + } + + createMockChannelCollection() { + return this.createMockTextChannelCollection() as Collection; + } + createMockRoleManager() { const manager = new RoleManager(this._guild); manager.add(this._role, true); diff --git a/tsconfig.json b/tsconfig.json index 29b6facb8..f1f98b8fa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,7 +23,8 @@ "noFallthroughCasesInSwitch": true, "allowUnusedLabels": false, "allowUnreachableCode": false, - "esModuleInterop": true + "esModuleInterop": true, + "types": ["node"] }, "include": [ "src/**/*", diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index 3e97e81b9..a07d44baa 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -100,15 +100,4 @@ module.exports = { }, ], ], - // plugins: [ - // [ - // "@docusaurus/plugin-sitemap", - // { - // cacheTime: 600 * 1000, // 600 sec - cache purge period - // changefreq: "weekly", - // priority: 0.5, - // trailingSlash: false, - // }, - // ], - // ], }; diff --git a/website/sidebars.js b/website/sidebars.js index 3dc2973fb..06a432043 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -71,6 +71,10 @@ module.exports = { type: "doc", id: "permissions", }, + { + type: "doc", + id: "roadmap", + }, ], }, ], diff --git a/website/yarn.lock b/website/yarn.lock index 420c30aa7..b852007b5 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -4582,6 +4582,11 @@ dns-txt@^2.0.2: dependencies: buffer-indexof "^1.0.0" +docusaurus-plugin-typedoc@^0.15.3: + version "0.15.3" + resolved "https://registry.yarnpkg.com/docusaurus-plugin-typedoc/-/docusaurus-plugin-typedoc-0.15.3.tgz#5076624915e241b3a208f754c8b13368aa57a1f0" + integrity sha512-iS3pWa1whGujrQqhXf/0UnG010n6N43LA/g7bM6b9OQosSB59KJIvNP98dbC+ihIGI3mjEnkVbvre8JEBy5yXg== + dom-converter@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" @@ -5847,7 +5852,7 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@^7.0.0, glob@^7.0.3, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: +glob@^7.0.0, glob@^7.0.3, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.7: version "7.1.7" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== @@ -6062,6 +6067,18 @@ handle-thing@^2.0.0: resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== +handlebars@^4.7.7: + version "4.7.7" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" + integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== + dependencies: + minimist "^1.2.5" + neo-async "^2.6.0" + source-map "^0.6.1" + wordwrap "^1.0.0" + optionalDependencies: + uglify-js "^3.1.4" + har-schema@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" @@ -7189,6 +7206,13 @@ json5@^2.1.2: dependencies: minimist "^1.2.5" +json5@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" + integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA== + dependencies: + minimist "^1.2.5" + jsonfile@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" @@ -7681,6 +7705,11 @@ lru-queue@^0.1.0: dependencies: es5-ext "~0.10.2" +lunr@^2.3.9: + version "2.3.9" + resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1" + integrity sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow== + magic-string@^0.25.1, magic-string@^0.25.2: version "0.25.7" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" @@ -7750,6 +7779,11 @@ marked@^0.7.0: resolved "https://registry.yarnpkg.com/marked/-/marked-0.7.0.tgz#b64201f051d271b1edc10a04d1ae9b74bb8e5c0e" integrity sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg== +marked@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/marked/-/marked-2.1.3.tgz#bd017cef6431724fd4b27e0657f5ceb14bff3753" + integrity sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA== + mdast-squeeze-paragraphs@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/mdast-squeeze-paragraphs/-/mdast-squeeze-paragraphs-4.0.0.tgz#7c4c114679c3bee27ef10b58e2e015be79f1ef97" @@ -7973,7 +8007,7 @@ minimalistic-assert@^1.0.0: resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== -minimatch@3.0.4, minimatch@^3.0.4: +minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== @@ -8175,7 +8209,7 @@ negotiator@0.6.2: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== -neo-async@^2.6.2: +neo-async@^2.6.0, neo-async@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== @@ -8472,6 +8506,13 @@ onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" +onigasm@^2.2.5: + version "2.2.5" + resolved "https://registry.yarnpkg.com/onigasm/-/onigasm-2.2.5.tgz#cc4d2a79a0fa0b64caec1f4c7ea367585a676892" + integrity sha512-F+th54mPc0l1lp1ZcFMyL/jTs2Tlq4SqIHKIXGZOR/VkHkF9A7Fr5rRr5+ZG/lWeRsyrClLYRq7s/yFQ/XhWCA== + dependencies: + lru-cache "^5.1.1" + open@^6.3.0: version "6.4.0" resolved "https://registry.yarnpkg.com/open/-/open-6.4.0.tgz#5c13e96d0dc894686164f18965ecfe889ecfc8a9" @@ -10409,6 +10450,15 @@ shelljs@^0.8.4: interpret "^1.0.0" rechoir "^0.6.2" +shiki@^0.9.3: + version "0.9.6" + resolved "https://registry.yarnpkg.com/shiki/-/shiki-0.9.6.tgz#057d6d451b9c1124107635fdcb5c752560d6abc6" + integrity sha512-h2y5Uq9QEWsEmi97n+BOdPOVxkOUdVunl+jVIzU9EqJ6/QbIX+U6F7TsrWZQ2xqwPgvvQaC9r7/zeegi1b48dQ== + dependencies: + json5 "^2.2.0" + onigasm "^2.2.5" + vscode-textmate "5.2.0" + side-channel@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" @@ -11292,11 +11342,42 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" +typedoc-default-themes@^0.12.10: + version "0.12.10" + resolved "https://registry.yarnpkg.com/typedoc-default-themes/-/typedoc-default-themes-0.12.10.tgz#614c4222fe642657f37693ea62cad4dafeddf843" + integrity sha512-fIS001cAYHkyQPidWXmHuhs8usjP5XVJjWB8oZGqkTowZaz3v7g3KDZeeqE82FBrmkAnIBOY3jgy7lnPnqATbA== + +typedoc-plugin-markdown@^3.10.4: + version "3.10.4" + resolved "https://registry.yarnpkg.com/typedoc-plugin-markdown/-/typedoc-plugin-markdown-3.10.4.tgz#4e0e9c584a1e421beafa4c3666896615f069da6b" + integrity sha512-if9w7S9fXLg73AYi/EoRSEhTOZlg3I8mIP8YEmvzSE33VrNXC9/hA0nVcLEwFVJeQY7ay6z67I6kW0KIv7LjeA== + dependencies: + handlebars "^4.7.7" + +typedoc@^0.21.5: + version "0.21.5" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.21.5.tgz#45643618ede5c3d75e2040b964d05fcffed7ca58" + integrity sha512-uRDRmYheE5Iju9Zz0X50pTASTpBorIHFt02F5Y8Dt4eBt55h3mwk1CBSY2+EfwBxY16N4Xm7f8KXhnfFZ0AmBw== + dependencies: + glob "^7.1.7" + handlebars "^4.7.7" + lunr "^2.3.9" + marked "^2.1.1" + minimatch "^3.0.0" + progress "^2.0.3" + shiki "^0.9.3" + typedoc-default-themes "^0.12.10" + ua-parser-js@^0.7.18: version "0.7.28" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31" integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g== +uglify-js@^3.1.4: + version "3.14.1" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.14.1.tgz#e2cb9fe34db9cb4cf7e35d1d26dfea28e09a7d06" + integrity sha512-JhS3hmcVaXlp/xSo3PKY5R0JqKs5M3IV+exdLHW99qKvKivPO4Z8qbej6mte17SOPqAOVMjt/XGgWacnFSzM3g== + unescape@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/unescape/-/unescape-1.0.1.tgz#956e430f61cad8a4d57d82c518f5e6cc5d0dda96" @@ -11728,6 +11809,11 @@ vlq@^1.0.0: resolved "https://registry.yarnpkg.com/vlq/-/vlq-1.0.1.tgz#c003f6e7c0b4c1edd623fd6ee50bbc0d6a1de468" integrity sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w== +vscode-textmate@5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.2.0.tgz#01f01760a391e8222fe4f33fbccbd1ad71aed74e" + integrity sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ== + wait-on@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-5.3.0.tgz#584e17d4b3fe7b46ac2b9f8e5e102c005c2776c7" @@ -11986,6 +12072,11 @@ word-wrap@~1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== +wordwrap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= + worker-rpc@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/worker-rpc/-/worker-rpc-0.1.1.tgz#cb565bd6d7071a8f16660686051e969ad32f54d5"