This commit is contained in:
Tutur33
2023-11-24 22:35:41 +01:00
parent 3c0b507a93
commit 7644b2a0f7
45165 changed files with 4803356 additions and 3 deletions
+9
View File
@@ -0,0 +1,9 @@
# The MIT License
Copyright 2022 Harminder Virk, contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+58
View File
@@ -0,0 +1,58 @@
# @japa/client
> API client to test endpoints over HTTP. Uses superagent under the hood
[![github-actions-image]][github-actions-url] [![npm-image]][npm-url] [![license-image]][license-url] [![typescript-image]][typescript-url]
The API client plugin of Japa makes it super simple to test your API endpoints over HTTP. You can use it to test any HTTP endpoint that returns JSON, XML, HTML, or even plain text.
It has out of the box support for:
- Multiple content types including `application/json`, `application/x-www-form-urlencoded` and `multipart`.
- Ability to upload files.
- Read and write cookies with the option to register custom cookies serializer.
- Lifecycle hooks. A great use-case of hooks is to persist and load session data during a request.
- All other common abilities like sending headers, query-string, and following redirects.
- Support for registering custom body serializers and parsers.
#### [Complete API documentation](https://japa.dev/docs/plugins/api-client)
## Installation
Install the package from the npm registry as follows:
```sh
npm i @japa/api-client
yarn add @japa/api-client
```
## Usage
You can use the assertion package with the `@japa/runner` as follows.
```ts
import { apiClient } from '@japa/api-client'
import { configure } from '@japa/runner'
configure({
plugins: [apiClient({ baseURL: 'http://localhost:3333' })]
})
```
Once done, you will be able to access the `client` property from the test context.
```ts
test('test title', ({ client }) => {
const response = await client.get('/')
})
```
[github-actions-url]: https://github.com/japa/api-client/actions/workflows/test.yml
[github-actions-image]: https://img.shields.io/github/actions/workflow/status/japa/api-client/test.yml?style=for-the-badge "github-actions"
[npm-image]: https://img.shields.io/npm/v/@japa/api-client.svg?style=for-the-badge&logo=npm
[npm-url]: https://npmjs.org/package/@japa/api-client "npm"
[license-image]: https://img.shields.io/npm/l/@japa/api-client?color=blueviolet&style=for-the-badge
[license-url]: LICENSE.md "license"
[typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript
[typescript-url]: "typescript"
+18
View File
@@ -0,0 +1,18 @@
import { PluginFn } from '@japa/runner';
import { ApiClient } from './src/client';
export { ApiClient };
export * from './src/types';
export { ApiRequest } from './src/request';
export { ApiResponse } from './src/response';
/**
* API client plugin registers an HTTP request client that
* can be used for testing API endpoints.
*/
export declare function apiClient(options?: string | {
baseURL?: string;
}): PluginFn;
declare module '@japa/runner' {
interface TestContext {
client: ApiClient;
}
}
+44
View File
@@ -0,0 +1,44 @@
"use strict";
/*
* @japa/api-client
*
* (c) Japa.dev
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.apiClient = exports.ApiResponse = exports.ApiRequest = exports.ApiClient = void 0;
const client_1 = require("./src/client");
Object.defineProperty(exports, "ApiClient", { enumerable: true, get: function () { return client_1.ApiClient; } });
__exportStar(require("./src/types"), exports);
var request_1 = require("./src/request");
Object.defineProperty(exports, "ApiRequest", { enumerable: true, get: function () { return request_1.ApiRequest; } });
var response_1 = require("./src/response");
Object.defineProperty(exports, "ApiResponse", { enumerable: true, get: function () { return response_1.ApiResponse; } });
/**
* API client plugin registers an HTTP request client that
* can be used for testing API endpoints.
*/
function apiClient(options) {
return function (_, __, { TestContext }) {
TestContext.getter('client', function () {
return new client_1.ApiClient(typeof options === 'string' ? options : options?.baseURL, this.assert);
}, true);
};
}
exports.apiClient = apiClient;
+89
View File
@@ -0,0 +1,89 @@
import { Macroable } from 'macroable';
import type { Assert } from '@japa/assert';
import { ApiRequest } from './request';
import { SetupHandler, TeardownHandler, CookiesSerializer } from './types';
/**
* ApiClient exposes the API to make HTTP requests in context of
* testing.
*/
export declare class ApiClient extends Macroable {
private baseUrl?;
private assert?;
/**
* Properties required by the Macroable class
*/
static macros: {};
static getters: {};
/**
* Invoked when a new instance of request is created
*/
private static onRequestHandlers;
/**
* Hooks handlers to pass onto the request
*/
private static hooksHandlers;
private static customCookiesSerializer?;
constructor(baseUrl?: string | undefined, assert?: Assert | undefined);
/**
* Remove all globally registered setup hooks
*/
static clearSetupHooks(): typeof ApiClient;
/**
* Remove all globally registered teardown hooks
*/
static clearTeardownHooks(): typeof ApiClient;
/**
* Clear on request handlers registered using "onRequest"
* method
*/
static clearRequestHandlers(): typeof ApiClient;
/**
* Register a handler to be invoked everytime a new request
* instance is created
*/
static onRequest(handler: (request: ApiRequest) => void): typeof ApiClient;
/**
* Register setup hooks. Setup hooks are called before the request
*/
static setup(handler: SetupHandler): typeof ApiClient;
/**
* Register teardown hooks. Teardown hooks are called before the request
*/
static teardown(handler: TeardownHandler): typeof ApiClient;
/**
* Register a custom cookies serializer
*/
static cookiesSerializer(serailizer: CookiesSerializer): typeof ApiClient;
/**
* Create an instance of the request
*/
request(endpoint: string, method: string): ApiRequest;
/**
* Create an instance of the request for GET method
*/
get(endpoint: string): ApiRequest;
/**
* Create an instance of the request for POST method
*/
post(endpoint: string): ApiRequest;
/**
* Create an instance of the request for PUT method
*/
put(endpoint: string): ApiRequest;
/**
* Create an instance of the request for PATCH method
*/
patch(endpoint: string): ApiRequest;
/**
* Create an instance of the request for DELETE method
*/
delete(endpoint: string): ApiRequest;
/**
* Create an instance of the request for HEAD method
*/
head(endpoint: string): ApiRequest;
/**
* Create an instance of the request for OPTIONS method
*/
options(endpoint: string): ApiRequest;
}
+161
View File
@@ -0,0 +1,161 @@
"use strict";
/*
* @japa/api-client
*
* (c) Japa.dev
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ApiClient = void 0;
const macroable_1 = require("macroable");
const request_1 = require("./request");
/**
* ApiClient exposes the API to make HTTP requests in context of
* testing.
*/
class ApiClient extends macroable_1.Macroable {
constructor(baseUrl, assert) {
super();
this.baseUrl = baseUrl;
this.assert = assert;
}
/**
* Remove all globally registered setup hooks
*/
static clearSetupHooks() {
this.hooksHandlers.setup = [];
return this;
}
/**
* Remove all globally registered teardown hooks
*/
static clearTeardownHooks() {
this.hooksHandlers.teardown = [];
return this;
}
/**
* Clear on request handlers registered using "onRequest"
* method
*/
static clearRequestHandlers() {
this.onRequestHandlers = [];
return this;
}
/**
* Register a handler to be invoked everytime a new request
* instance is created
*/
static onRequest(handler) {
this.onRequestHandlers.push(handler);
return this;
}
/**
* Register setup hooks. Setup hooks are called before the request
*/
static setup(handler) {
this.hooksHandlers.setup.push(handler);
return this;
}
/**
* Register teardown hooks. Teardown hooks are called before the request
*/
static teardown(handler) {
this.hooksHandlers.teardown.push(handler);
return this;
}
/**
* Register a custom cookies serializer
*/
static cookiesSerializer(serailizer) {
this.customCookiesSerializer = serailizer;
return this;
}
/**
* Create an instance of the request
*/
request(endpoint, method) {
const hooks = this.constructor.hooksHandlers;
const requestHandlers = this.constructor.onRequestHandlers;
const cookiesSerializer = this.constructor.customCookiesSerializer;
let baseUrl = this.baseUrl;
const envHost = process.env.HOST;
const envPort = process.env.PORT;
/**
* Compute baseUrl from the HOST and the PORT env variables
* when no baseUrl is provided
*/
if (!baseUrl && envHost && envPort) {
baseUrl = `http://${envHost}:${envPort}`;
}
const request = new request_1.ApiRequest({
baseUrl,
method,
endpoint,
hooks,
serializers: { cookie: cookiesSerializer },
}, this.assert);
requestHandlers.forEach((handler) => handler(request));
return request;
}
/**
* Create an instance of the request for GET method
*/
get(endpoint) {
return this.request(endpoint, 'GET');
}
/**
* Create an instance of the request for POST method
*/
post(endpoint) {
return this.request(endpoint, 'POST');
}
/**
* Create an instance of the request for PUT method
*/
put(endpoint) {
return this.request(endpoint, 'PUT');
}
/**
* Create an instance of the request for PATCH method
*/
patch(endpoint) {
return this.request(endpoint, 'PATCH');
}
/**
* Create an instance of the request for DELETE method
*/
delete(endpoint) {
return this.request(endpoint, 'DELETE');
}
/**
* Create an instance of the request for HEAD method
*/
head(endpoint) {
return this.request(endpoint, 'HEAD');
}
/**
* Create an instance of the request for OPTIONS method
*/
options(endpoint) {
return this.request(endpoint, 'OPTIONS');
}
}
exports.ApiClient = ApiClient;
/**
* Properties required by the Macroable class
*/
ApiClient.macros = {};
ApiClient.getters = {};
/**
* Invoked when a new instance of request is created
*/
ApiClient.onRequestHandlers = [];
/**
* Hooks handlers to pass onto the request
*/
ApiClient.hooksHandlers = {
setup: [],
teardown: [],
};
+321
View File
@@ -0,0 +1,321 @@
/// <reference types="node" />
import { Macroable } from 'macroable';
import { Hooks } from '@poppinss/hooks';
import type { Assert } from '@japa/assert';
import superagent from 'superagent';
import { ApiResponse } from './response';
import { SetupHandler, RequestConfig, MultipartValue, RequestCookies, TeardownHandler, SuperAgentParser, SuperAgentSerializer } from './types';
export declare class ApiRequest extends Macroable {
config: RequestConfig;
private assert?;
/**
* Properties required by the Macroable class
*/
static macros: {};
static getters: {};
/**
* Register/remove custom superagent parser
*/
static addParser: (contentType: string, parser: SuperAgentParser) => void;
static removeParser: (contentType: string) => void;
/**
* Register/remove custom superagent serializers
*/
static addSerializer: (contentType: string, serializer: SuperAgentSerializer) => void;
static removeSerializer: (contentType: string) => void;
/**
* Reference to registered hooks
*/
hooks: Hooks;
private setupRunner;
private teardownRunner;
/**
* Dump calls
*/
private valuesToDump;
/**
* The underlying super agent request
*/
request: superagent.SuperAgentRequest;
/**
* Cookies to be sent with the request
*/
cookiesJar: RequestCookies;
constructor(config: RequestConfig, assert?: Assert | undefined);
/**
* Set cookies header
*/
private setCookiesHeader;
/**
* Instantiate hooks runner
*/
private instantiateHooksRunners;
/**
* Run setup hooks
*/
private runSetupHooks;
/**
* Run teardown hooks
*/
private runTeardownHooks;
/**
* Send HTTP request to the server. Errors except the client errors
* are tured into a response object.
*/
private sendRequest;
/**
* Invoke calls calls
*/
private dumpValues;
/**
* Is endpoint a fully qualified URL or not
*/
private isUrl;
/**
* Prepend baseUrl to the endpoint
*/
private prependBaseUrl;
/**
* Creates the request instance for the given HTTP method
*/
private createRequest;
/**
* Register a setup hook. Setup hooks are called before
* making the request
*/
setup(handler: SetupHandler): this;
/**
* Register a teardown hook. Teardown hooks are called after
* making the request
*/
teardown(handler: TeardownHandler): this;
/**
* Set cookie as a key-value pair to be sent to the server
*/
cookie(key: string, value: any): this;
/**
* Set cookies as an object to be sent to the server
*/
cookies(cookies: Record<string, any>): this;
/**
* Define request header as a key-value pair.
*
* @example
* request.header('x-foo', 'bar')
* request.header('x-foo', ['bar', 'baz'])
*/
header(key: string, value: string | string[]): this;
/**
* Define request headers as an object.
*
* @example
* request.headers({ 'x-foo': 'bar' })
* request.headers({ 'x-foo': ['bar', 'baz'] })
*/
headers(headers: Record<string, string | string[]>): this;
/**
* Define the field value for a multipart request.
*
* @note: This method makes a multipart request. See [[this.form]] to
* make HTML style form submissions.
*
* @example
* request.field('name', 'virk')
* request.field('age', 22)
*/
field(name: string, value: MultipartValue | MultipartValue[]): this;
/**
* Define fields as an object for a multipart request
*
* @note: This method makes a multipart request. See [[this.form]] to
* make HTML style form submissions.
*
* @example
* request.fields({'name': 'virk', age: 22})
*/
fields(values: {
[name: string]: MultipartValue | MultipartValue[];
}): this;
/**
* Upload file for a multipart request. Either you can pass path to a
* file, a readable stream, or a buffer
*
* @example
* request.file('avatar', 'absolute/path/to/file')
* request.file('avatar', createReadStream('./path/to/file'))
*/
file(name: string, value: MultipartValue, options?: string | {
filename?: string | undefined;
contentType?: string | undefined;
}): this;
/**
* Set form values. Calling this method will set the content type
* to "application/x-www-form-urlencoded".
*
* @example
* request.form({
* email: 'virk@adonisjs.com',
* password: 'secret'
* })
*/
form(values: string | object): this;
/**
* Set JSON body for the request. Calling this method will set
* the content type to "application/json".
*
* @example
* request.json({
* email: 'virk@adonisjs.com',
* password: 'secret'
* })
*/
json(values: string | object): this;
/**
* Set querystring for the request.
*
* @example
* request.qs('order_by', 'id')
* request.qs({ order_by: 'id' })
*/
qs(key: string, value: any): this;
qs(values: string | object): this;
/**
* Set timeout for the request.
*
* @example
* request.timeout(5000)
* request.timeout({ response: 5000, deadline: 60000 })
*/
timeout(ms: number | {
deadline?: number | undefined;
response?: number | undefined;
}): this;
/**
* Set content-type for the request
*
* @example
* request.type('json')
*/
type(value: string): this;
/**
* Set "accept" header in the request
*
* @example
* request.accept('json')
*/
accept(type: string): this;
/**
* Follow redirects from the response
*
* @example
* request.redirects(3)
*/
redirects(count: number): this;
/**
* Set basic auth header from user and password
*
* @example
* request.basicAuth('foo@bar.com', 'secret')
*/
basicAuth(user: string, password: string): this;
/**
* Pass auth bearer token as authorization header.
*
* @example
* request.apiToken('tokenValue')
*/
bearerToken(token: string): this;
/**
* Set the ca certificates to trust
*/
ca(certificate: string | string[] | Buffer | Buffer[]): this;
/**
* Set the client certificates
*/
cert(certificate: string | string[] | Buffer | Buffer[]): this;
/**
* Set the client private key(s)
*/
privateKey(key: string | string[] | Buffer | Buffer[]): this;
/**
* Set the client PFX or PKCS12 encoded private key and certificate chain
*/
pfx(key: string | string[] | Buffer | Buffer[] | {
pfx: string | Buffer;
passphrase: string;
}): this;
/**
* Does not reject expired or invalid TLS certs. Sets internally rejectUnauthorized=true
*/
disableTLSCerts(): this;
/**
* Trust broken HTTPs connections on localhost
*/
trustLocalhost(trust?: boolean): this;
/**
* Dump request headers
*/
dumpHeaders(): this;
/**
* Dump request cookies
*/
dumpCookies(): this;
/**
* Dump request body
*/
dumpBody(): this;
/**
* Dump request
*/
dump(): this;
/**
* Retry a failing request. Along with the count, you can also define
* a callback to decide how long the request should be retried.
*
* The max count is applied regardless of whether callback is defined
* or not
*
* The following response codes are considered failing.
* - 408
* - 413
* - 429
* - 500
* - 502
* - 503
* - 504
* - 521
* - 522
* - 524
*
* The following error codes are considered failing.
* - 'ETIMEDOUT'
* - 'ECONNRESET'
* - 'EADDRINUSE'
* - 'ECONNREFUSED'
* - 'EPIPE'
* - 'ENOTFOUND'
* - 'ENETUNREACH'
* - 'EAI_AGAIN'
*/
retry(count: number, retryUntilCallback?: (error: any, response: ApiResponse) => boolean): this;
/**
* Make the API request
*/
send(): Promise<ApiResponse>;
/**
* Implementation of `then` for the promise API
*/
then<TResult1 = ApiResponse, TResult2 = never>(resolve?: ((value: ApiResponse) => TResult1 | PromiseLike<TResult1>) | undefined | null, reject?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): Promise<TResult1 | TResult2>;
/**
* Implementation of `catch` for the promise API
*/
catch<TResult = never>(reject?: ((reason: ApiResponse) => TResult | PromiseLike<TResult>) | undefined | null): Promise<ApiResponse | TResult>;
/**
* Implementation of `finally` for the promise API
*/
finally(fullfilled?: (() => void) | undefined | null): Promise<ApiResponse>;
/**
* Required when Promises are extended
*/
get [Symbol.toStringTag](): string;
}
+546
View File
@@ -0,0 +1,546 @@
"use strict";
/*
* @japa/api-client
*
* (c) Japa.dev
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ApiRequest = void 0;
const cookie_1 = __importDefault(require("cookie"));
const macroable_1 = require("macroable");
const hooks_1 = require("@poppinss/hooks");
const superagent_1 = __importDefault(require("superagent"));
const response_1 = require("./response");
const utils_1 = require("./utils");
const DUMP_CALLS = {
request: utils_1.dumpRequest,
body: utils_1.dumpRequestBody,
cookies: utils_1.dumpRequestCookies,
headers: utils_1.dumpRequestHeaders,
};
class ApiRequest extends macroable_1.Macroable {
constructor(config, assert) {
super();
this.config = config;
this.assert = assert;
/**
* Reference to registered hooks
*/
this.hooks = new hooks_1.Hooks();
/**
* Dump calls
*/
this.valuesToDump = new Set();
/**
* The underlying super agent request
*/
this.request = this.createRequest();
/**
* Cookies to be sent with the request
*/
this.cookiesJar = {};
this.config.hooks?.setup.forEach((handler) => this.setup(handler));
this.config.hooks?.teardown.forEach((handler) => this.teardown(handler));
}
/**
* Set cookies header
*/
setCookiesHeader() {
const prepareMethod = this.config.serializers?.cookie?.prepare;
const cookies = Object.keys(this.cookiesJar).map((key) => {
let { name, value } = this.cookiesJar[key];
if (prepareMethod) {
value = prepareMethod(name, value, this);
}
return cookie_1.default.serialize(name, value);
});
if (!cookies.length) {
return;
}
this.header('Cookie', cookies);
}
/**
* Instantiate hooks runner
*/
instantiateHooksRunners() {
this.setupRunner = this.hooks.runner('setup');
this.teardownRunner = this.hooks.runner('teardown');
}
/**
* Run setup hooks
*/
async runSetupHooks() {
try {
await this.setupRunner.run(this);
}
catch (error) {
await this.setupRunner.cleanup(error, this);
throw error;
}
}
/**
* Run teardown hooks
*/
async runTeardownHooks(response) {
try {
await this.teardownRunner.run(response);
}
catch (error) {
await this.teardownRunner.cleanup(error, response);
throw error;
}
await this.teardownRunner.cleanup(null, response);
}
/**
* Send HTTP request to the server. Errors except the client errors
* are tured into a response object.
*/
async sendRequest() {
let response;
try {
this.setCookiesHeader();
this.dumpValues();
response = await this.request.buffer(true);
}
catch (error) {
this.request.abort();
/**
* Call cleanup hooks
*/
if (!error.response) {
await this.setupRunner.cleanup(error, this);
throw error;
}
/**
* Raise exception when received 500 status code from the server
*/
if (error.response.status >= 500) {
await this.setupRunner.cleanup(error, this);
throw (0, utils_1.stackToError)(error.response.text);
}
response = error.response;
}
await this.setupRunner.cleanup(null, this);
return new response_1.ApiResponse(this, response, this.config, this.assert);
}
/**
* Invoke calls calls
*/
dumpValues() {
if (!this.valuesToDump.size) {
return;
}
try {
this.valuesToDump.forEach((key) => {
DUMP_CALLS[key](this);
});
}
catch (error) {
console.log(error);
}
}
/**
* Is endpoint a fully qualified URL or not
*/
isUrl(url) {
return url.startsWith('http://') || url.startsWith('https://');
}
/**
* Prepend baseUrl to the endpoint
*/
prependBaseUrl(url) {
if (!this.config.baseUrl) {
return url;
}
return `${this.config.baseUrl}/${url.replace(/^\//, '')}`;
}
/**
* Creates the request instance for the given HTTP method
*/
createRequest() {
let url = this.config.endpoint;
if (!this.isUrl(url)) {
url = this.prependBaseUrl(url);
}
return (0, superagent_1.default)(this.config.method, url);
}
/**
* Register a setup hook. Setup hooks are called before
* making the request
*/
setup(handler) {
this.hooks.add('setup', handler);
return this;
}
/**
* Register a teardown hook. Teardown hooks are called after
* making the request
*/
teardown(handler) {
this.hooks.add('teardown', handler);
return this;
}
/**
* Set cookie as a key-value pair to be sent to the server
*/
cookie(key, value) {
this.cookiesJar[key] = { name: key, value };
return this;
}
/**
* Set cookies as an object to be sent to the server
*/
cookies(cookies) {
Object.keys(cookies).forEach((key) => this.cookie(key, cookies[key]));
return this;
}
/**
* Define request header as a key-value pair.
*
* @example
* request.header('x-foo', 'bar')
* request.header('x-foo', ['bar', 'baz'])
*/
header(key, value) {
this.headers({ [key]: value });
return this;
}
/**
* Define request headers as an object.
*
* @example
* request.headers({ 'x-foo': 'bar' })
* request.headers({ 'x-foo': ['bar', 'baz'] })
*/
headers(headers) {
this.request.set(headers);
return this;
}
/**
* Define the field value for a multipart request.
*
* @note: This method makes a multipart request. See [[this.form]] to
* make HTML style form submissions.
*
* @example
* request.field('name', 'virk')
* request.field('age', 22)
*/
field(name, value) {
this.request.field(name, value);
return this;
}
/**
* Define fields as an object for a multipart request
*
* @note: This method makes a multipart request. See [[this.form]] to
* make HTML style form submissions.
*
* @example
* request.fields({'name': 'virk', age: 22})
*/
fields(values) {
this.request.field(values);
return this;
}
/**
* Upload file for a multipart request. Either you can pass path to a
* file, a readable stream, or a buffer
*
* @example
* request.file('avatar', 'absolute/path/to/file')
* request.file('avatar', createReadStream('./path/to/file'))
*/
file(name, value, options) {
this.request.attach(name, value, options);
return this;
}
/**
* Set form values. Calling this method will set the content type
* to "application/x-www-form-urlencoded".
*
* @example
* request.form({
* email: 'virk@adonisjs.com',
* password: 'secret'
* })
*/
form(values) {
this.type('form');
this.request.send(values);
return this;
}
/**
* Set JSON body for the request. Calling this method will set
* the content type to "application/json".
*
* @example
* request.json({
* email: 'virk@adonisjs.com',
* password: 'secret'
* })
*/
json(values) {
this.type('json');
this.request.send(values);
return this;
}
qs(key, value) {
if (!value) {
this.request.query(key);
}
else {
this.request.query({ [key]: value });
}
return this;
}
/**
* Set timeout for the request.
*
* @example
* request.timeout(5000)
* request.timeout({ response: 5000, deadline: 60000 })
*/
timeout(ms) {
this.request.timeout(ms);
return this;
}
/**
* Set content-type for the request
*
* @example
* request.type('json')
*/
type(value) {
this.request.type(value);
return this;
}
/**
* Set "accept" header in the request
*
* @example
* request.accept('json')
*/
accept(type) {
this.request.accept(type);
return this;
}
/**
* Follow redirects from the response
*
* @example
* request.redirects(3)
*/
redirects(count) {
this.request.redirects(count);
return this;
}
/**
* Set basic auth header from user and password
*
* @example
* request.basicAuth('foo@bar.com', 'secret')
*/
basicAuth(user, password) {
this.request.auth(user, password, { type: 'basic' });
return this;
}
/**
* Pass auth bearer token as authorization header.
*
* @example
* request.apiToken('tokenValue')
*/
bearerToken(token) {
this.request.auth(token, { type: 'bearer' });
return this;
}
/**
* Set the ca certificates to trust
*/
ca(certificate) {
this.request.ca(certificate);
return this;
}
/**
* Set the client certificates
*/
cert(certificate) {
this.request.cert(certificate);
return this;
}
/**
* Set the client private key(s)
*/
privateKey(key) {
this.request.key(key);
return this;
}
/**
* Set the client PFX or PKCS12 encoded private key and certificate chain
*/
pfx(key) {
this.request.pfx(key);
return this;
}
/**
* Does not reject expired or invalid TLS certs. Sets internally rejectUnauthorized=true
*/
disableTLSCerts() {
this.request.disableTLSCerts();
return this;
}
/**
* Trust broken HTTPs connections on localhost
*/
trustLocalhost(trust = true) {
this.request.trustLocalhost(trust);
return this;
}
/**
* Dump request headers
*/
dumpHeaders() {
this.valuesToDump.add('headers');
return this;
}
/**
* Dump request cookies
*/
dumpCookies() {
this.valuesToDump.add('cookies');
return this;
}
/**
* Dump request body
*/
dumpBody() {
this.valuesToDump.add('body');
return this;
}
/**
* Dump request
*/
dump() {
this.valuesToDump.add('request');
this.dumpCookies();
this.dumpHeaders();
this.dumpBody();
return this;
}
/**
* Retry a failing request. Along with the count, you can also define
* a callback to decide how long the request should be retried.
*
* The max count is applied regardless of whether callback is defined
* or not
*
* The following response codes are considered failing.
* - 408
* - 413
* - 429
* - 500
* - 502
* - 503
* - 504
* - 521
* - 522
* - 524
*
* The following error codes are considered failing.
* - 'ETIMEDOUT'
* - 'ECONNRESET'
* - 'EADDRINUSE'
* - 'ECONNREFUSED'
* - 'EPIPE'
* - 'ENOTFOUND'
* - 'ENETUNREACH'
* - 'EAI_AGAIN'
*/
retry(count, retryUntilCallback) {
if (retryUntilCallback) {
this.request.retry(count, (error, response) => {
return retryUntilCallback(error, new response_1.ApiResponse(this, response, this.config, this.assert));
});
return this;
}
this.request.retry(count);
return this;
}
/**
* Make the API request
*/
async send() {
/**
* Step 1: Instantiate hooks runners
*/
this.instantiateHooksRunners();
/**
* Step 2: Run setup hooks
*/
await this.runSetupHooks();
/**
* Step 3: Make HTTP request
*/
const response = await this.sendRequest();
/**
* Step 4: Run teardown hooks
*/
await this.runTeardownHooks(response);
return response;
}
/**
* Implementation of `then` for the promise API
*/
then(resolve, reject) {
return this.send().then(resolve, reject);
}
/**
* Implementation of `catch` for the promise API
*/
catch(reject) {
return this.send().catch(reject);
}
/**
* Implementation of `finally` for the promise API
*/
finally(fullfilled) {
return this.send().finally(fullfilled);
}
/**
* Required when Promises are extended
*/
get [Symbol.toStringTag]() {
return this.constructor.name;
}
}
exports.ApiRequest = ApiRequest;
/**
* Properties required by the Macroable class
*/
ApiRequest.macros = {};
ApiRequest.getters = {};
/**
* Register/remove custom superagent parser
*/
ApiRequest.addParser = (contentType, parser) => {
superagent_1.default.parse[contentType] = parser;
};
ApiRequest.removeParser = (contentType) => {
delete superagent_1.default.parse[contentType];
};
/**
* Register/remove custom superagent serializers
*/
ApiRequest.addSerializer = (contentType, serializer) => {
superagent_1.default.serialize[contentType] = serializer;
};
ApiRequest.removeSerializer = (contentType) => {
delete superagent_1.default.serialize[contentType];
};
+195
View File
@@ -0,0 +1,195 @@
import { Response } from 'superagent';
import { Macroable } from 'macroable';
import { Assert } from '@japa/assert';
import { ApiRequest } from './request';
import { RequestConfig, ResponseCookie, ResponseCookies, SuperAgentResponseFile } from './types';
export declare class ApiResponse extends Macroable {
request: ApiRequest;
response: Response;
private config;
assert?: Assert | undefined;
static macros: {};
static getters: {};
private valuesDumped;
/**
* Parsed cookies
*/
cookiesJar: ResponseCookies;
constructor(request: ApiRequest, response: Response, config: RequestConfig, assert?: Assert | undefined);
/**
* Parse response header to collect cookies
*/
private parseCookies;
/**
* Process cookies using the serializer
*/
private processCookies;
/**
* Ensure assert plugin is installed
*/
ensureHasAssert(): void;
/**
* Response content-type charset. Undefined if no charset
* is mentioned.
*/
charset(): string | undefined;
/**
* Parsed files from the multipart response.
*/
files<Properties extends string>(): {
[K in Properties]: SuperAgentResponseFile;
};
/**
* Returns an object of links by parsing the "Link" header.
*
* @example
* Link: <https://one.example.com>; rel="preconnect", <https://two.example.com>; rel="preload"
* response.links()
* // {
* // preconnect: 'https://one.example.com',
// preload: 'https://two.example.com',
* // }
*/
links(): Record<string, string>;
/**
* Response status type
*/
statusType(): number;
/**
* Request raw parsed text
*/
text(): string;
/**
* Response body
*/
body(): any;
/**
* Read value for a given response header
*/
header(key: string): string | undefined;
/**
* Get all response headers
*/
headers(): Record<string, string>;
/**
* Get response status
*/
status(): number;
/**
* Get response content-type
*/
type(): string;
/**
* Get redirects URLs the request has followed before
* getting the response
*/
redirects(): string[];
/**
* Find if the response has parsed body. The check is performed
* by inspecting the response content-type and returns true
* when content-type is either one of the following.
*
* - application/json
* - application/x-www-form-urlencoded
* - multipart/form-data
*
* Or when the response body is a buffer.
*/
hasBody(): boolean;
/**
* Find if the response body has files
*/
hasFiles(): boolean;
/**
* Find if response is an error
*/
hasError(): boolean;
/**
* Find if response is an fatal error. Response with >=500
* status code are concerned as fatal errors
*/
hasFatalError(): boolean;
/**
* Find if the request client failed to make the request
*/
hasClientError(): boolean;
/**
* Find if the server responded with an error
*/
hasServerError(): boolean;
/**
* Access to response error
*/
error(): false | import("superagent").HTTPError;
/**
* Get cookie by name
*/
cookie(name: string): ResponseCookie | undefined;
/**
* Parsed response cookies
*/
cookies(): ResponseCookies;
/**
* Dump request headers
*/
dumpHeaders(): this;
/**
* Dump request cookies
*/
dumpCookies(): this;
/**
* Dump request body
*/
dumpBody(): this;
/**
* Dump request body
*/
dumpError(): this;
/**
* Dump request
*/
dump(): this;
/**
* Assert response status to match the expected status
*/
assertStatus(expectedStatus: number): void;
/**
* Assert response body to match the expected body
*/
assertBody(expectedBody: any): void;
/**
* Assert response body to match the subset from the
* expected body
*/
assertBodyContains(expectedBody: any): void;
/**
* Assert response to contain a given cookie and optionally
* has the expected value
*/
assertCookie(name: string, value?: any): void;
/**
* Assert response to not contain a given cookie
*/
assertCookieMissing(name: string): void;
/**
* Assert response to contain a given header and optionally
* has the expected value
*/
assertHeader(name: string, value?: any): void;
/**
* Assert response to not contain a given header
*/
assertHeaderMissing(name: string): void;
/**
* Assert response text to include the expected value
*/
assertTextIncludes(expectedSubset: string): void;
/**
* Assert response body is valid as per the API spec.
*/
assertAgainstApiSpec(): void;
/**
* Assert there is a matching redirect
*/
assertRedirectsTo(pathname: string): void;
}
+355
View File
@@ -0,0 +1,355 @@
"use strict";
/*
* @japa/api-client
*
* (c) Japa.dev
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ApiResponse = void 0;
const macroable_1 = require("macroable");
const set_cookie_parser_1 = __importDefault(require("set-cookie-parser"));
const utils_1 = require("./utils");
class ApiResponse extends macroable_1.Macroable {
constructor(request, response, config, assert) {
super();
this.request = request;
this.response = response;
this.config = config;
this.assert = assert;
this.valuesDumped = new Set();
/**
* Parsed cookies
*/
this.cookiesJar = this.parseCookies();
this.processCookies();
}
/**
* Parse response header to collect cookies
*/
parseCookies() {
const cookieHeader = this.header('set-cookie');
if (!cookieHeader) {
return {};
}
return set_cookie_parser_1.default.parse(cookieHeader, { map: true });
}
/**
* Process cookies using the serializer
*/
processCookies() {
const cookiesSerializer = this.config.serializers?.cookie;
const processMethod = cookiesSerializer?.process;
if (!processMethod) {
return;
}
Object.keys(this.cookiesJar).forEach((key) => {
const cookie = this.cookiesJar[key];
const processedValue = processMethod(cookie.name, cookie.value, this);
if (processedValue !== undefined) {
cookie.value = processedValue;
}
});
}
/**
* Ensure assert plugin is installed
*/
ensureHasAssert() {
if (!this.assert) {
throw new Error('Response assertions are not available. Make sure to install the @japa/assert plugin');
}
}
/**
* Response content-type charset. Undefined if no charset
* is mentioned.
*/
charset() {
return this.response.charset;
}
/**
* Parsed files from the multipart response.
*/
files() {
return this.response.files;
}
/**
* Returns an object of links by parsing the "Link" header.
*
* @example
* Link: <https://one.example.com>; rel="preconnect", <https://two.example.com>; rel="preload"
* response.links()
* // {
* // preconnect: 'https://one.example.com',
// preload: 'https://two.example.com',
* // }
*/
links() {
return this.response.links;
}
/**
* Response status type
*/
statusType() {
return this.response.statusType;
}
/**
* Request raw parsed text
*/
text() {
return this.response.text;
}
/**
* Response body
*/
body() {
return this.response.body;
}
/**
* Read value for a given response header
*/
header(key) {
return this.response.headers[key];
}
/**
* Get all response headers
*/
headers() {
return this.response.headers;
}
/**
* Get response status
*/
status() {
return this.response.status;
}
/**
* Get response content-type
*/
type() {
return this.response.type;
}
/**
* Get redirects URLs the request has followed before
* getting the response
*/
redirects() {
return this.response.redirects;
}
/**
* Find if the response has parsed body. The check is performed
* by inspecting the response content-type and returns true
* when content-type is either one of the following.
*
* - application/json
* - application/x-www-form-urlencoded
* - multipart/form-data
*
* Or when the response body is a buffer.
*/
hasBody() {
return (this.type() === 'application/json' ||
this.type() === 'application/x-www-form-urlencoded' ||
this.type() === 'multipart/form-data' ||
Buffer.isBuffer(this.response.body));
}
/**
* Find if the response body has files
*/
hasFiles() {
return this.files() && Object.keys(this.files()).length > 0;
}
/**
* Find if response is an error
*/
hasError() {
return this.error() ? true : false;
}
/**
* Find if response is an fatal error. Response with >=500
* status code are concerned as fatal errors
*/
hasFatalError() {
return this.status() >= 500;
}
/**
* Find if the request client failed to make the request
*/
hasClientError() {
return this.response.clientError;
}
/**
* Find if the server responded with an error
*/
hasServerError() {
return this.response.serverError;
}
/**
* Access to response error
*/
error() {
return this.response.error;
}
/**
* Get cookie by name
*/
cookie(name) {
return this.cookiesJar[name];
}
/**
* Parsed response cookies
*/
cookies() {
return this.cookiesJar;
}
/**
* Dump request headers
*/
dumpHeaders() {
if (this.valuesDumped.has('headers')) {
return this;
}
this.valuesDumped.add('headers');
(0, utils_1.dumpResponseHeaders)(this);
return this;
}
/**
* Dump request cookies
*/
dumpCookies() {
if (this.valuesDumped.has('cookies')) {
return this;
}
this.valuesDumped.add('cookies');
(0, utils_1.dumpResponseCookies)(this);
return this;
}
/**
* Dump request body
*/
dumpBody() {
if (this.valuesDumped.has('body')) {
return this;
}
this.valuesDumped.add('body');
(0, utils_1.dumpResponseBody)(this);
return this;
}
/**
* Dump request body
*/
dumpError() {
if (this.valuesDumped.has('error')) {
return this;
}
this.valuesDumped.add('error');
(0, utils_1.dumpResponseError)(this);
return this;
}
/**
* Dump request
*/
dump() {
if (this.valuesDumped.has('response')) {
return this;
}
this.valuesDumped.add('response');
(0, utils_1.dumpResponse)(this);
this.dumpCookies();
this.dumpHeaders();
this.dumpBody();
this.dumpError();
return this;
}
/**
* Assert response status to match the expected status
*/
assertStatus(expectedStatus) {
this.ensureHasAssert();
this.assert.equal(this.status(), expectedStatus);
}
/**
* Assert response body to match the expected body
*/
assertBody(expectedBody) {
this.ensureHasAssert();
this.assert.deepEqual(this.body(), expectedBody);
}
/**
* Assert response body to match the subset from the
* expected body
*/
assertBodyContains(expectedBody) {
this.ensureHasAssert();
this.assert.containsSubset(this.body(), expectedBody);
}
/**
* Assert response to contain a given cookie and optionally
* has the expected value
*/
assertCookie(name, value) {
this.ensureHasAssert();
this.assert.property(this.cookies(), name);
if (value !== undefined) {
this.assert.deepEqual(this.cookie(name).value, value);
}
}
/**
* Assert response to not contain a given cookie
*/
assertCookieMissing(name) {
this.ensureHasAssert();
this.assert.notProperty(this.cookies(), name);
}
/**
* Assert response to contain a given header and optionally
* has the expected value
*/
assertHeader(name, value) {
this.ensureHasAssert();
this.assert.property(this.headers(), name);
if (value !== undefined) {
this.assert.deepEqual(this.header(name), value);
}
}
/**
* Assert response to not contain a given header
*/
assertHeaderMissing(name) {
this.ensureHasAssert();
this.assert.notProperty(this.headers(), name);
}
/**
* Assert response text to include the expected value
*/
assertTextIncludes(expectedSubset) {
this.ensureHasAssert();
this.assert.include(this.text(), expectedSubset);
}
/**
* Assert response body is valid as per the API spec.
*/
assertAgainstApiSpec() {
this.ensureHasAssert();
this.assert.isValidApiResponse(this.response);
}
/**
* Assert there is a matching redirect
*/
assertRedirectsTo(pathname) {
this.ensureHasAssert();
const redirects = this.redirects().map((url) => new URL(url).pathname);
this.assert.evaluate(redirects.find((one) => one === pathname), `Expected #{exp} to be one of #{act}`, {
expected: [pathname],
actual: redirects,
operator: 'includes',
});
}
}
exports.ApiResponse = ApiResponse;
ApiResponse.macros = {};
ApiResponse.getters = {};
+103
View File
@@ -0,0 +1,103 @@
/// <reference types="node" />
/// <reference types="node" />
/// <reference types="node" />
import { ReadStream } from 'fs';
import { EventEmitter } from 'events';
import { Response } from 'superagent';
import { ApiRequest } from './request';
import { ApiResponse } from './response';
/**
* The interface is copied from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/formidable/PersistentFile.d.ts, since superagent using formidable for parsing response
* files.
*/
export interface SuperAgentResponseFile extends EventEmitter {
open(): void;
toJSON(): {
length: number;
mimetype: string | null;
mtime: Date | null;
size: number;
filepath: string;
originalFilename: string | null;
hash?: string | null;
};
toString(): string;
write(buffer: string, cb: () => void): void;
end(cb: () => void): void;
destroy(): void;
}
/**
* Superagent response parser callback method. The method
* receives an instance of the Node.js readable stream
*/
export type SuperAgentParser = (res: Response, callback: (err: Error | null, body: any) => void) => void;
/**
* Superagent request serializer. The method receives the
* request body object and must serialize it to a string
*/
export type SuperAgentSerializer = (obj: any) => string;
/**
* Allowed multipart values
*/
export type MultipartValue = Blob | Buffer | ReadStream | string | boolean | number;
/**
* Shape of custom cookies serializer.
*/
export type CookiesSerializer = {
process(key: string, value: any, response: ApiResponse): any;
prepare(key: string, value: any, request: ApiRequest): string;
};
/**
* Config accepted by the API request class
*/
export type RequestConfig = {
method: string;
endpoint: string;
baseUrl?: string;
hooks?: {
setup: SetupHandler[];
teardown: TeardownHandler[];
};
serializers?: {
cookie?: CookiesSerializer;
};
};
/**
* Shape of the parsed response cookie
*/
export type ResponseCookie = {
name: string;
value: any;
path?: string;
domain?: string;
expires?: Date;
maxAge?: number;
secure?: boolean;
httpOnly?: boolean;
sameSite?: string;
};
/**
* Response cookies jar
*/
export type ResponseCookies = Record<string, ResponseCookie>;
/**
* Shape of the cookie accepted by the request
*/
export type RequestCookie = {
name: string;
value: any;
};
/**
* Request cookies jar
*/
export type RequestCookies = Record<string, RequestCookie>;
/**
* Setup handlers
*/
export type SetupCleanupHandler = (error: any | null, request: ApiRequest) => any | Promise<any>;
export type SetupHandler = (request: ApiRequest) => any | SetupCleanupHandler | Promise<any> | Promise<SetupCleanupHandler>;
/**
* Teardown handlers
*/
export type TeardownCleanupHandler = (error: any | null, response: ApiResponse) => any | Promise<any>;
export type TeardownHandler = (response: ApiResponse) => any | TeardownCleanupHandler | Promise<any> | Promise<TeardownCleanupHandler>;
+10
View File
@@ -0,0 +1,10 @@
"use strict";
/*
* @japa/api-client
*
* (c) Japa.dev
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
Object.defineProperty(exports, "__esModule", { value: true });
+45
View File
@@ -0,0 +1,45 @@
import { ApiRequest } from './request';
import { ApiResponse } from './response';
/**
* Convert error stack string to an error object.
*
* It is an expirement to use server error stack and convert
* it to an actual error object.
*/
export declare function stackToError(errorStack: any): string | Error;
/**
* Default implementation to print request errors
*/
export declare function dumpResponseError(response: ApiResponse): void;
/**
* Default implementation to log request cookies
*/
export declare function dumpRequestCookies(request: ApiRequest): void;
/**
* Default implementation to log response cookies
*/
export declare function dumpResponseCookies(response: ApiResponse): void;
/**
* Default implementation to log request headers
*/
export declare function dumpRequestHeaders(request: ApiRequest): void;
/**
* Default implementation to log response headers
*/
export declare function dumpResponseHeaders(response: ApiResponse): void;
/**
* Default implementation to log request body
*/
export declare function dumpRequestBody(request: ApiRequest): void;
/**
* Default implementation to log response body
*/
export declare function dumpResponseBody(response: ApiResponse): void;
/**
* Default implementation to log request
*/
export declare function dumpRequest(request: ApiRequest): void;
/**
* Default implementation to log response
*/
export declare function dumpResponse(response: ApiResponse): void;
+120
View File
@@ -0,0 +1,120 @@
"use strict";
/*
* @japa/api-client
*
* (c) Japa.dev
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.dumpResponse = exports.dumpRequest = exports.dumpResponseBody = exports.dumpRequestBody = exports.dumpResponseHeaders = exports.dumpRequestHeaders = exports.dumpResponseCookies = exports.dumpRequestCookies = exports.dumpResponseError = exports.stackToError = void 0;
const util_1 = require("util");
const INSPECT_OPTIONS = { colors: true, depth: 2, showHidden: false };
/**
* Convert error stack string to an error object.
*
* It is an expirement to use server error stack and convert
* it to an actual error object.
*/
function stackToError(errorStack) {
if (typeof errorStack === 'string' && /^\s*at .*(\S+:\d+|\(native\))/m.test(errorStack)) {
const customError = new Error(errorStack.split('\n')[0]);
customError.stack = errorStack;
return customError;
}
return errorStack;
}
exports.stackToError = stackToError;
/**
* Default implementation to print request errors
*/
function dumpResponseError(response) {
/**
* Attempt to convert error stack to a error object when status >= 500
*/
if (response.status() >= 500 && response.hasError()) {
console.log(`"error" => ${(0, util_1.inspect)(stackToError(response.text()))}`);
return;
}
}
exports.dumpResponseError = dumpResponseError;
/**
* Default implementation to log request cookies
*/
function dumpRequestCookies(request) {
console.log(`"cookies" => ${(0, util_1.inspect)(request.cookiesJar, INSPECT_OPTIONS)}`);
}
exports.dumpRequestCookies = dumpRequestCookies;
/**
* Default implementation to log response cookies
*/
function dumpResponseCookies(response) {
console.log(`"cookies" => ${(0, util_1.inspect)(response.cookies(), INSPECT_OPTIONS)}`);
}
exports.dumpResponseCookies = dumpResponseCookies;
/**
* Default implementation to log request headers
*/
function dumpRequestHeaders(request) {
console.log(`"headers" => ${(0, util_1.inspect)(request.request['header'], INSPECT_OPTIONS)}`);
}
exports.dumpRequestHeaders = dumpRequestHeaders;
/**
* Default implementation to log response headers
*/
function dumpResponseHeaders(response) {
console.log(`"headers" => ${(0, util_1.inspect)(response.headers(), INSPECT_OPTIONS)}`);
}
exports.dumpResponseHeaders = dumpResponseHeaders;
/**
* Default implementation to log request body
*/
function dumpRequestBody(request) {
if (request.request['_data']) {
console.log(`"body" => ${(0, util_1.inspect)(request.request['_data'], INSPECT_OPTIONS)}`);
}
}
exports.dumpRequestBody = dumpRequestBody;
/**
* Default implementation to log response body
*/
function dumpResponseBody(response) {
if (response.status() >= 500) {
return;
}
if (response.hasBody()) {
console.log(`"body" => ${(0, util_1.inspect)(response.body(), INSPECT_OPTIONS)}`);
}
else if (response.text()) {
console.log(`"text" => ${(0, util_1.inspect)(response.text(), INSPECT_OPTIONS)}`);
}
if (response.hasFiles()) {
const files = Object.keys(response.files()).reduce((result, fileName) => {
result[fileName] = response.files()[fileName].toJSON();
return result;
}, {});
console.log(`"files" => ${(0, util_1.inspect)(files, INSPECT_OPTIONS)}`);
}
}
exports.dumpResponseBody = dumpResponseBody;
/**
* Default implementation to log request
*/
function dumpRequest(request) {
console.log(`"request" => ${(0, util_1.inspect)({
method: request.request.method,
endpoint: request.config.endpoint,
}, INSPECT_OPTIONS)}`);
console.log(`"qs" => ${(0, util_1.inspect)(request.request['qs'], INSPECT_OPTIONS)}`);
}
exports.dumpRequest = dumpRequest;
/**
* Default implementation to log response
*/
function dumpResponse(response) {
console.log(`"response" => ${(0, util_1.inspect)({
status: response.status(),
}, INSPECT_OPTIONS)}`);
}
exports.dumpResponse = dumpResponse;
+142
View File
@@ -0,0 +1,142 @@
{
"name": "@japa/api-client",
"version": "1.4.4",
"description": "Browser and API testing client for Japa. Built on top of Playwright",
"main": "build/index.js",
"files": [
"build/src",
"build/index.d.ts",
"build/index.js"
],
"exports": {
".": "./build/index.js"
},
"types": "./build/index.d.ts",
"scripts": {
"mrm": "mrm --preset=@adonisjs/mrm-preset",
"pretest": "npm run lint",
"japa:test": "node -r @adonisjs/require-ts/build/register bin/test.ts",
"test": "npm run japa:test",
"clean": "del build",
"compile": "npm run lint && npm run clean && tsc",
"build": "npm run compile",
"prepublishOnly": "npm run build",
"lint": "eslint . --ext=.ts",
"format": "prettier --write .",
"commit": "git-cz",
"release": "np --message=\"chore(release): %s\"",
"version": "npm run build",
"sync-labels": "github-label-sync --labels ./node_modules/@adonisjs/mrm-preset/gh-labels.json japa/api-client"
},
"keywords": [
"playwright",
"browser-tests",
"tests",
"e2e",
"api-tests"
],
"author": "virk,japa",
"license": "MIT",
"devDependencies": {
"@adonisjs/mrm-preset": "^5.0.3",
"@adonisjs/require-ts": "^2.0.13",
"@japa/assert": "^1.4.1",
"@japa/runner": "^2.5.0",
"@japa/spec-reporter": "^1.3.3",
"@types/cookie": "^0.5.1",
"@types/node": "^18.14.0",
"@types/set-cookie-parser": "^2.4.2",
"cheerio": "^1.0.0-rc.10",
"commitizen": "^4.3.0",
"cz-conventional-changelog": "^3.3.0",
"del-cli": "^5.0.0",
"eslint": "^8.34.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-adonis": "^2.1.1",
"eslint-plugin-prettier": "^4.2.1",
"github-label-sync": "^2.2.0",
"husky": "^8.0.3",
"mrm": "^4.1.13",
"np": "^7.6.3",
"prettier": "^2.8.4",
"typescript": "^4.9.5"
},
"mrmConfig": {
"core": false,
"license": "MIT",
"services": [
"github-actions"
],
"minNodeVersion": "16.13.1",
"probotApps": [
"stale",
"lock"
],
"runGhActionsOnWindows": true
},
"eslintConfig": {
"extends": [
"plugin:adonis/typescriptPackage",
"prettier"
],
"plugins": [
"prettier"
],
"rules": {
"prettier/prettier": [
"error",
{
"endOfLine": "auto"
}
]
}
},
"eslintIgnore": [
"build"
],
"prettier": {
"trailingComma": "es5",
"semi": false,
"singleQuote": true,
"useTabs": false,
"quoteProps": "consistent",
"bracketSpacing": true,
"arrowParens": "always",
"printWidth": 100
},
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
},
"np": {
"contents": ".",
"anyBranch": false
},
"dependencies": {
"@poppinss/hooks": "^6.0.2-0",
"@types/superagent": "^4.1.16",
"cookie": "^0.5.0",
"macroable": "^7.0.2",
"set-cookie-parser": "^2.5.1",
"superagent": "^8.0.9"
},
"peerDependencies": {
"@japa/runner": "^2.2.3"
},
"directories": {
"test": "tests"
},
"repository": {
"type": "git",
"url": "git+https://github.com/japa/api-client.git"
},
"bugs": {
"url": "https://github.com/japa/api-client/issues"
},
"homepage": "https://github.com/japa/api-client#readme",
"publishConfig": {
"access": "public",
"tag": "latest"
}
}
+9
View File
@@ -0,0 +1,9 @@
# The MIT License
Copyright 2022 Harminder Virk, contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+71
View File
@@ -0,0 +1,71 @@
# @japa/assert
> Assertion library built on top of Chai.assert
[![github-actions-image]][github-actions-url] [![npm-image]][npm-url] [![license-image]][license-url] [![typescript-image]][typescript-url]
An assertion library built on top of [Chai.assert](https://www.chaijs.com/guide/styles/#assert) with small tweaks and additional features like assertion planning.
#### [Complete API documentation](https://japa.dev/docs/plugins/assert)
## Installation
Install the package from the npm registry as follows:
```sh
npm i @japa/assert
yarn add @japa/assert
```
## Usage
You can use the assertion package with the `@japa/runner` as follows.
```ts
import { assert } from '@japa/assert'
import { configure } from '@japa/runner'
configure({
plugins: [assert()]
})
```
Once done, you will be able to access the `assert` property on the test context.
```ts
test('test title', ({ assert }) => {
assert.deepEqual({ id: 1 }, { id: 1})
})
```
## Register open API schemas
You can register open API schema and then assert HTTP responses against.
```ts
configure({
plugins: [assert({
openApi: {
schemas: [join(__dirname, '..', 'api-spec.json')]
}
})]
})
```
Validate response as follows.
```ts
test('get users', ({ assert }) => {
const response = await supertest(baseUrl).get('/users')
assert.isValidApiResponse(response)
})
```
[github-actions-url]: https://github.com/japa/assert/actions/workflows/test.yml
[github-actions-image]: https://img.shields.io/github/actions/workflow/status/japa/assert/test.yml?style=for-the-badge "github-actions"
[npm-image]: https://img.shields.io/npm/v/@japa/assert.svg?style=for-the-badge&logo=npm
[npm-url]: https://npmjs.org/package/@japa/assert "npm"
[license-image]: https://img.shields.io/npm/l/@japa/assert?color=blueviolet&style=for-the-badge
[license-url]: LICENSE.md "license"
[typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript
[typescript-url]: "typescript"
+14
View File
@@ -0,0 +1,14 @@
import type { PluginFn } from '@japa/runner';
import type { PluginConfig } from './src/types';
import { Assert } from './src/assert/main';
/**
* Plugin for "@japa/runner"
*/
export declare function assert(options?: PluginConfig): PluginFn;
export * from './src/types';
export { Assert };
declare module '@japa/runner' {
interface TestContext {
assert: Assert;
}
}
+48
View File
@@ -0,0 +1,48 @@
"use strict";
/*
* @japa/assert
*
* (c) Japa.dev
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Assert = exports.assert = void 0;
const main_1 = require("./src/assert/main");
Object.defineProperty(exports, "Assert", { enumerable: true, get: function () { return main_1.Assert; } });
/**
* Plugin for "@japa/runner"
*/
function assert(options) {
if (options?.openApi) {
main_1.Assert.registerApiSpecs(options.openApi.schemas, {
exportCoverage: options.openApi.exportCoverage,
reportCoverage: options.openApi.reportCoverage,
});
}
return function (_, __, { TestContext, Test }) {
TestContext.getter('assert', () => new main_1.Assert(), true);
Test.dispose(function (test, hasError) {
if (!hasError) {
test.context.assert.assertions.validate();
}
});
};
}
exports.assert = assert;
__exportStar(require("./src/types"), exports);
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+7
View File
@@ -0,0 +1,7 @@
/**
* Copy of https://github.com/debitoor/chai-subset/blob/master/lib/chai-subset.js
*
* The package is not maintained anymore, hence it makes more sense
* to own the small piece of code and evolve it as required.
*/
export declare function subsetCompare(expected: any, actual: any): any;
+71
View File
@@ -0,0 +1,71 @@
"use strict";
/*
* @japa/assert
*
* (c) Japa.dev
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.subsetCompare = void 0;
/**
* Copy of https://github.com/debitoor/chai-subset/blob/master/lib/chai-subset.js
*
* The package is not maintained anymore, hence it makes more sense
* to own the small piece of code and evolve it as required.
*/
function subsetCompare(expected, actual) {
if (expected === actual) {
return true;
}
if (typeof actual !== typeof expected) {
return false;
}
if (typeof expected !== 'object' || expected === null) {
return expected === actual;
}
if (!!expected && !actual) {
return false;
}
/**
* Handling arrays
*/
if (Array.isArray(expected)) {
if (typeof actual.length !== 'number') {
return false;
}
const aa = Array.prototype.slice.call(actual);
return expected.every(function (exp) {
return aa.some(function (act) {
return subsetCompare(exp, act);
});
});
}
/**
* Handling date instances
*/
if (expected instanceof Date) {
if (actual instanceof Date) {
return expected.getTime() === actual.getTime();
}
else {
return false;
}
}
/**
* Handling objects
*/
return Object.keys(expected).every(function (key) {
const eo = expected[key];
const ao = actual[key];
if (typeof eo === 'object' && eo !== null && ao !== null) {
return subsetCompare(eo, ao);
}
if (typeof eo === 'function') {
return eo(ao);
}
return ao === eo;
});
}
exports.subsetCompare = subsetCompare;
+18
View File
@@ -0,0 +1,18 @@
import { assert } from 'chai';
/**
* Unnecessary similar methods have been removed
*/
export type ChaiAssert = {
[K in keyof typeof assert]: (typeof assert)[K];
};
/**
* Assert contract
*/
export type AssertContract = Omit<ChaiAssert, 'deepStrictEqual' | 'nestedInclude' | 'notNestedInclude' | 'deepNestedInclude' | 'notDeepNestedInclude' | 'ifError' | 'changes' | 'changesBy' | 'doesNotChange' | 'changesButNotBy' | 'increases' | 'increasesBy' | 'doesNotIncrease' | 'increasesButNotBy' | 'decreases' | 'decreasesBy' | 'doesNotDecrease' | 'doesNotDecreaseBy' | 'decreasesButNotBy' | 'extensible' | 'isExtensible' | 'notExtensible' | 'isNotExtensible' | 'deepProperty' | 'notDeepProperty' | 'nestedProperty' | 'nestedPropertyVal' | 'notNestedProperty' | 'notNestedPropertyVal' | 'deepNestedProperty' | 'notDeepNestedProperty' | 'deepNestedPropertyVal' | 'notDeepNestedPropertyVal' | 'hasAnyKeys' | 'hasAllKeys' | 'containsAllKeys' | 'doesNotHaveAnyKeys' | 'doesNotHaveAllKeys' | 'throw' | 'Throw' | 'doesNotThrow' | 'hasAnyDeepKeys' | 'hasAllDeepKeys' | 'containsAllDeepKeys' | 'doesNotHaveAnyDeepKeys' | 'doesNotHaveAllDeepKeys' | 'closeTo' | 'operator' | 'oneOf' | 'ownInclude' | 'notOwnInclude' | 'deepOwnInclude' | 'notDeepOwnInclude'>;
export type PluginConfig = {
openApi?: {
schemas: string[];
reportCoverage?: boolean;
exportCoverage?: boolean;
};
};
+10
View File
@@ -0,0 +1,10 @@
"use strict";
/*
* @japa/assert
*
* (c) Japa.dev
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
Object.defineProperty(exports, "__esModule", { value: true });
+136
View File
@@ -0,0 +1,136 @@
{
"name": "@japa/assert",
"version": "1.4.1",
"description": "Assertion module for Japa",
"main": "build/index.js",
"files": [
"build/src",
"build/index.d.ts",
"build/index.js"
],
"exports": {
".": "./build/index.js"
},
"scripts": {
"mrm": "mrm --preset=@adonisjs/mrm-preset",
"pretest": "npm run lint",
"test": "node .bin/test.js",
"clean": "del build",
"compile": "npm run lint && npm run clean && tsc",
"build": "npm run compile",
"prepublishOnly": "npm run build",
"lint": "eslint . --ext=.ts",
"format": "prettier --write .",
"commit": "git-cz",
"release": "np --message=\"chore(release): %s\"",
"version": "npm run build",
"sync-labels": "github-label-sync --labels ./node_modules/@adonisjs/mrm-preset/gh-labels.json japa/assert"
},
"keywords": [
"assert",
"chai",
"japa"
],
"author": "virk,japa",
"license": "MIT",
"peerDependencies": {
"@japa/runner": "^2.1.1"
},
"devDependencies": {
"@adonisjs/mrm-preset": "^5.0.3",
"@adonisjs/require-ts": "^2.0.13",
"@japa/core": "^7.3.1",
"@japa/runner": "^2.3.0",
"@types/luxon": "^3.2.0",
"@types/node": "^18.13.0",
"commitizen": "^4.3.0",
"cz-conventional-changelog": "^3.3.0",
"del-cli": "^5.0.0",
"eslint": "^8.33.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-adonis": "^2.1.1",
"eslint-plugin-prettier": "^4.2.1",
"github-label-sync": "^2.2.0",
"husky": "^8.0.3",
"japa": "^4.0.0",
"luxon": "^3.2.1",
"mrm": "^4.1.13",
"np": "^7.6.3",
"prettier": "^2.8.4",
"typescript": "^4.9.5"
},
"mrmConfig": {
"core": false,
"license": "MIT",
"services": [
"github-actions"
],
"minNodeVersion": "16.13.1",
"probotApps": [
"stale",
"lock"
],
"runGhActionsOnWindows": false
},
"eslintConfig": {
"extends": [
"plugin:adonis/typescriptPackage",
"prettier"
],
"plugins": [
"prettier"
],
"rules": {
"prettier/prettier": [
"error",
{
"endOfLine": "auto"
}
]
}
},
"eslintIgnore": [
"build"
],
"prettier": {
"trailingComma": "es5",
"semi": false,
"singleQuote": true,
"useTabs": false,
"quoteProps": "consistent",
"bracketSpacing": true,
"arrowParens": "always",
"printWidth": 100
},
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
},
"np": {
"contents": ".",
"anyBranch": false
},
"dependencies": {
"@types/chai": "^4.3.4",
"api-contract-validator": "^2.2.8",
"chai": "^4.3.7",
"macroable": "^7.0.2"
},
"publishConfig": {
"access": "public",
"tag": "latest"
},
"types": "./build/index.d.ts",
"directories": {
"test": "test"
},
"repository": {
"type": "git",
"url": "git+https://github.com/japa/assert.git"
},
"bugs": {
"url": "https://github.com/japa/assert/issues"
},
"homepage": "https://github.com/japa/assert#readme"
}
+9
View File
@@ -0,0 +1,9 @@
# The MIT License
Copyright 2022 Harminder Virk, contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+132
View File
@@ -0,0 +1,132 @@
Japa Base Reporter
> Base reporter to create customized testing reporters for Japa
The Base reporter abstracts the repetitive parts of creating a tests reporters.
[![github-actions-image]][github-actions-url] [![npm-image]][npm-url] [![license-image]][license-url] [![typescript-image]][typescript-url]
## Setup
Install the package from npm registry as follows:
```sh
npm i @japa/base-reporter
# Yarn lovers
yarn add @japa/base-reporter
```
```ts
import { BaseReporter } from '@japa/base-reporter'
class MyReporter extends BaseReporter {}
export const reporterFn = (myReporterOptions = {}) => {
const myReporter = new MyReporter(myReporterOptions)
return myReporter.boot.bind(reporter)
}
```
## Handlers
The Base reporter invokes following methods as it receives the events from the runner. You can implement these methods to display the tests progress.
```ts
import type {
TestEndNode,
SuiteEndNode,
GroupEndNode,
TestStartNode,
RunnerEndNode,
GroupStartNode,
SuiteStartNode,
RunnerStartNode,
} from '@japa/core'
class SpecReporter extends BaseReporter {
protected onTestStart(payload: TestStartNode) {
console.log('test started')
}
protected onTestEnd(payload: TestEndNode) {
console.log('test endeded')
}
protected onGroupStart(payload: GroupStartNode) {
console.log('group started')
}
protected onGroupEnd(payload: GroupEndNode) {
console.log('group ended')
}
protected onSuiteStart(payload: SuiteStartNode) {
console.log('suite started')
}
protected onSuiteEnd(payload: SuiteEndNode) {
console.log('suite ended')
}
protected async start(payload: RunnerStartNode) {
console.log('test runner started. You can run async operations here')
}
protected async end(payload: RunnerEndNode) {
console.log('test runner ended. You can run async operations here')
}
}
```
## Inherited properties
The following properties are available on the BaseReporter. These properties are available only after the boot method is called.
### runner
Reference to underlying tests runner instance.
```ts
this.runner
```
### currentFileName
Reference to the file name for which tests are getting executed. The filename is only available inside the test or group handlers.
```ts
this.currentFileName
```
### currentSuiteName
Reference to the suite name for which tests are getting executed. The suite name is only available after the `onSuiteStart` handler call.
```ts
this.currentSuiteName
```
### uncaughtExceptions
Uncaught exceptions collected while tests are running. We rely on `process.on('uncaughtException')` event to collect uncaught exceptions and display them with their stack trace at the end.
## Printing tests summary
After all the tests have been finished, you can call the `printSummary` method to print a detailed summary of all tests alongside pretty diffs and pretty error stack trace.
You should call the `printSummary` method from the `end` handler.
```ts
class SpecReporter extends BaseReporter {
protected async end() {
const summary = await this.runner.getSummary()
await this.printSummary(summary)
}
}
```
[github-actions-image]: https://img.shields.io/github/actions/workflow/status/japa/base-reporter/test.yml?style=for-the-badge
[github-actions-url]: https://github.com/japa/base-reporter/actions/workflows/test.yml 'github-actions'
[npm-image]: https://img.shields.io/npm/v/@japa/base-reporter.svg?style=for-the-badge&logo=npm
[npm-url]: https://npmjs.org/package/@japa/base-reporter 'npm'
[license-image]: https://img.shields.io/npm/l/@japa/base-reporter?color=blueviolet&style=for-the-badge
[license-url]: LICENSE.md 'license'
[typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript
[typescript-url]: "typescript"
+2
View File
@@ -0,0 +1,2 @@
export { BaseReporter } from './src/base_reporter';
export type { BaseReporterOptions } from './src/types';
+5
View File
@@ -0,0 +1,5 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BaseReporter = void 0;
var base_reporter_1 = require("./src/base_reporter");
Object.defineProperty(exports, "BaseReporter", { enumerable: true, get: function () { return base_reporter_1.BaseReporter; } });
+26
View File
@@ -0,0 +1,26 @@
import type { BaseReporterOptions } from './types';
import type { Emitter, Runner, TestEndNode, SuiteEndNode, GroupEndNode, TestStartNode, RunnerEndNode, GroupStartNode, SuiteStartNode, RunnerStartNode } from '@japa/core';
export declare abstract class BaseReporter {
private options;
runner: Runner<any>;
currentFileName?: string;
currentSuiteName?: string;
uncaughtExceptions: {
phase: 'test';
error: Error;
}[];
constructor(options?: Partial<BaseReporterOptions>);
private printKeyValuePair;
protected onTestStart(_: TestStartNode): void;
protected onTestEnd(_: TestEndNode): void;
protected onGroupStart(_: GroupStartNode): void;
protected onGroupEnd(_: GroupEndNode): void;
protected onSuiteStart(_: SuiteStartNode): void;
protected onSuiteEnd(_: SuiteEndNode): void;
protected start(_: RunnerStartNode): Promise<void>;
protected end(_: RunnerEndNode): Promise<void>;
private printAggregates;
private printErrors;
printSummary(summary: ReturnType<Runner<any>['getSummary']>): Promise<void>;
boot(runner: Runner<any>, emitter: Emitter): void;
}
+132
View File
@@ -0,0 +1,132 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BaseReporter = void 0;
const ms_1 = __importDefault(require("ms"));
const cliui_1 = require("@poppinss/cliui");
const errors_printer_1 = require("@japa/errors-printer");
class BaseReporter {
options;
runner;
currentFileName;
currentSuiteName;
uncaughtExceptions = [];
constructor(options = {}) {
this.options = {
stackLinesCount: options.stackLinesCount || 5,
};
}
printKeyValuePair(key, value, whitespaceLength) {
console.log(`${cliui_1.logger.colors.dim(`${key.padEnd(whitespaceLength + 2)} : `)}${value}`);
}
onTestStart(_) { }
onTestEnd(_) { }
onGroupStart(_) { }
onGroupEnd(_) { }
onSuiteStart(_) { }
onSuiteEnd(_) { }
async start(_) { }
async end(_) { }
printAggregates(summary) {
const [tests, time] = [[], []];
time.push(cliui_1.logger.colors.dim((0, ms_1.default)(summary.duration)));
if (summary.aggregates.passed) {
tests.push(cliui_1.logger.colors.green(`${summary.aggregates.passed} passed`));
}
if (summary.aggregates.failed) {
tests.push(cliui_1.logger.colors.red(`${summary.aggregates.failed} failed`));
}
if (summary.aggregates.todo) {
tests.push(cliui_1.logger.colors.cyan(`${summary.aggregates.todo} todo`));
}
if (summary.aggregates.skipped) {
tests.push(cliui_1.logger.colors.yellow(`${summary.aggregates.skipped} skipped`));
}
if (summary.aggregates.regression) {
tests.push(cliui_1.logger.colors.magenta(`${summary.aggregates.regression} regression`));
}
const keysPadding = summary.aggregates.uncaughtExceptions ? 19 : 5;
this.printKeyValuePair('Tests', `${tests.join(', ')} ${cliui_1.logger.colors.dim(`(${summary.aggregates.total})`)}`, keysPadding);
this.printKeyValuePair('Time', time.join(''), keysPadding);
if (summary.aggregates.uncaughtExceptions) {
this.printKeyValuePair('Uncaught exceptions', cliui_1.logger.colors.red(String(summary.aggregates.uncaughtExceptions)), keysPadding);
}
}
async printErrors(summary) {
if (summary.failureTree.length || this.uncaughtExceptions.length) {
console.log('');
console.log('');
}
const errorPrinter = new errors_printer_1.ErrorsPrinter({
stackLinesCount: this.options.stackLinesCount,
});
for (let suite of summary.failureTree) {
await errorPrinter.printErrors(suite.name, suite.errors);
for (let testOrGroup of suite.children) {
if (testOrGroup.type === 'group') {
await errorPrinter.printErrors(testOrGroup.name, testOrGroup.errors);
for (let test of testOrGroup.children) {
await errorPrinter.printErrors(test.title, test.errors);
}
}
else {
await errorPrinter.printErrors(testOrGroup.title, testOrGroup.errors);
}
}
}
await errorPrinter.printErrors('Uncaught exception', this.uncaughtExceptions);
}
async printSummary(summary) {
console.log('');
if (summary.aggregates.total === 0 && !summary.hasError) {
console.log(cliui_1.logger.colors.bgYellow().black(' NO TESTS EXECUTED '));
return;
}
if (summary.hasError) {
console.log(cliui_1.logger.colors.bgRed().black(' FAILED '));
}
else {
console.log(cliui_1.logger.colors.bgGreen().black(' PASSED '));
}
console.log('');
this.printAggregates(summary);
await this.printErrors(summary);
}
boot(runner, emitter) {
this.runner = runner;
emitter.on('test:start', (payload) => {
this.currentFileName = payload.meta.fileName;
this.onTestStart(payload);
});
emitter.on('test:end', (payload) => {
this.onTestEnd(payload);
});
emitter.on('group:start', (payload) => {
this.currentFileName = payload.meta.fileName;
this.onGroupStart(payload);
});
emitter.on('group:end', (payload) => {
this.onGroupEnd(payload);
});
emitter.on('suite:start', (payload) => {
this.currentSuiteName = payload.name;
this.onSuiteStart(payload);
});
emitter.on('suite:end', (payload) => {
this.currentSuiteName = undefined;
this.onSuiteEnd(payload);
});
emitter.on('uncaught:exception', async (error) => {
this.uncaughtExceptions.push({ phase: 'test', error });
});
emitter.on('runner:start', async (payload) => {
await this.start(payload);
});
emitter.on('runner:end', async (payload) => {
await this.end(payload);
});
}
}
exports.BaseReporter = BaseReporter;
+3
View File
@@ -0,0 +1,3 @@
export interface BaseReporterOptions {
stackLinesCount?: number;
}
+2
View File
@@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
+115
View File
@@ -0,0 +1,115 @@
{
"name": "@japa/base-reporter",
"version": "1.1.2",
"description": "Base reporter to create customized testing reporters for Japa",
"type": "commonjs",
"main": "./build/index.js",
"files": [
"build/src",
"build/index.js",
"build/index.d.ts"
],
"types": "./build/index.d.ts",
"exports": {
".": "./build/index.js"
},
"scripts": {
"pretest": "npm run lint",
"test": "node --require=@adonisjs/require-ts/build/register bin/test.ts",
"clean": "del-cli build",
"compile": "npm run lint && npm run clean && tsc",
"build": "npm run compile",
"release": "np --message=\"chore(release): %s\"",
"version": "npm run build",
"prepublishOnly": "npm run build",
"lint": "eslint . --ext=.ts",
"format": "prettier --write .",
"sync-labels": "github-label-sync --labels .github/labels.json @japa/base-reporter"
},
"publishConfig": {
"access": "public",
"tag": "latest"
},
"keywords": [],
"author": "japa,virk",
"license": "MIT",
"devDependencies": {
"@adonisjs/require-ts": "^2.0.13",
"@commitlint/cli": "^17.4.4",
"@commitlint/config-conventional": "^17.4.4",
"@japa/assert": "^1.4.1",
"@japa/core": "^7.3.2",
"@japa/run-failed-tests": "^1.1.1",
"@japa/runner": "^2.5.1",
"@japa/spec-reporter": "^1.3.3",
"@types/ms": "^0.7.31",
"@types/node": "^18.14.5",
"del-cli": "^5.0.0",
"eslint": "^8.35.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-adonis": "^3.0.3",
"eslint-plugin-prettier": "^4.0.0",
"github-label-sync": "^2.2.0",
"husky": "^8.0.3",
"np": "^7.6.3",
"prettier": "^2.8.4",
"typescript": "^4.9.5"
},
"eslintConfig": {
"extends": [
"plugin:adonis/typescriptPackage",
"prettier"
],
"plugins": [
"prettier"
],
"rules": {
"prettier/prettier": [
"error",
{
"endOfLine": "auto"
}
]
}
},
"eslintIgnore": [
"build"
],
"prettier": {
"trailingComma": "es5",
"semi": false,
"singleQuote": true,
"useTabs": false,
"quoteProps": "consistent",
"bracketSpacing": true,
"arrowParens": "always",
"printWidth": 100
},
"commitlint": {
"extends": [
"@commitlint/config-conventional"
]
},
"np": {
"message": "chore(release): %s",
"tag": "latest",
"branch": "main",
"anyBranch": false
},
"dependencies": {
"@japa/errors-printer": "^2.1.0",
"@poppinss/cliui": "^3.0.5",
"ms": "^2.1.3"
},
"directories": {
"test": "tests"
},
"repository": {
"type": "git",
"url": "git+https://github.com/japa/base-reporter.git"
},
"bugs": {
"url": "https://github.com/japa/base-reporter/issues"
},
"homepage": "https://github.com/japa/base-reporter#readme"
}
+9
View File
@@ -0,0 +1,9 @@
# The MIT License
Copyright 2021 Harminder Virk, contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+26
View File
@@ -0,0 +1,26 @@
# @japa/core
> The core of the Japa tests runners
[![github-actions-image]][github-actions-url] [![npm-image]][npm-url] [![license-image]][license-url] [![typescript-image]][typescript-url]
This repo contains the code for the core of the japa tests runner. You can use it create your tests runner, just like `@japa/runner`.
You can view the documentation on [https://japa.dev](https://japa.dev)
[github-actions-image]: https://img.shields.io/github/actions/workflow/status/japa/core/test.yml?style=for-the-badge
[github-actions-url]: https://github.com/japa/core/actions/workflows/test.yml "github-actions"
[npm-image]: https://img.shields.io/npm/v/@japa/core.svg?style=for-the-badge&logo=npm
[npm-url]: https://npmjs.org/package/@japa/core "npm"
[license-image]: https://img.shields.io/npm/l/@japa/core?color=blueviolet&style=for-the-badge
[license-url]: LICENSE.md "license"
[typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript
[typescript-url]: "typescript"
<br />
<hr>
![](https://raw.githubusercontent.com/thetutlage/static/main/sponsorkit/sponsors.png)
+9
View File
@@ -0,0 +1,9 @@
export * from './src/types';
export { Runner } from './src/runner';
export { Test } from './src/test/main';
export { Emitter } from './src/emitter';
export { Refiner } from './src/refiner';
export { Tracker } from './src/tracker';
export { Suite } from './src/suite/main';
export { Group } from './src/group/main';
export { TestContext } from './src/test_context';
+42
View File
@@ -0,0 +1,42 @@
"use strict";
/*
* @japa/core
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TestContext = exports.Group = exports.Suite = exports.Tracker = exports.Refiner = exports.Emitter = exports.Test = exports.Runner = void 0;
__exportStar(require("./src/types"), exports);
var runner_1 = require("./src/runner");
Object.defineProperty(exports, "Runner", { enumerable: true, get: function () { return runner_1.Runner; } });
var main_1 = require("./src/test/main");
Object.defineProperty(exports, "Test", { enumerable: true, get: function () { return main_1.Test; } });
var emitter_1 = require("./src/emitter");
Object.defineProperty(exports, "Emitter", { enumerable: true, get: function () { return emitter_1.Emitter; } });
var refiner_1 = require("./src/refiner");
Object.defineProperty(exports, "Refiner", { enumerable: true, get: function () { return refiner_1.Refiner; } });
var tracker_1 = require("./src/tracker");
Object.defineProperty(exports, "Tracker", { enumerable: true, get: function () { return tracker_1.Tracker; } });
var main_2 = require("./src/suite/main");
Object.defineProperty(exports, "Suite", { enumerable: true, get: function () { return main_2.Suite; } });
var main_3 = require("./src/group/main");
Object.defineProperty(exports, "Group", { enumerable: true, get: function () { return main_3.Group; } });
var test_context_1 = require("./src/test_context");
Object.defineProperty(exports, "TestContext", { enumerable: true, get: function () { return test_context_1.TestContext; } });
+3
View File
@@ -0,0 +1,3 @@
/// <reference types="node" />
declare const _default: import("util").DebugLogger;
export default _default;
+12
View File
@@ -0,0 +1,12 @@
"use strict";
/*
* @japa/core
*
* (c) Japa.dev
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
Object.defineProperty(exports, "__esModule", { value: true });
const util_1 = require("util");
exports.default = (0, util_1.debuglog)('japa:core');
+16
View File
@@ -0,0 +1,16 @@
import Emittery from 'emittery';
import { RunnerEvents } from './types';
/**
* Runner emitter
*/
export declare class Emitter extends Emittery<RunnerEvents> {
private errorHandler?;
/**
* Define onError handler invoked when `emit` fails
*/
onError(errorHandler: (error: any) => void | Promise<void>): void;
/**
* Emit event
*/
emit<Name extends keyof RunnerEvents>(eventName: Name, eventData?: RunnerEvents[Name], allowMetaEvents?: boolean): Promise<void>;
}
+43
View File
@@ -0,0 +1,43 @@
"use strict";
/*
* @japa/core
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Emitter = void 0;
const emittery_1 = __importDefault(require("emittery"));
/**
* Runner emitter
*/
class Emitter extends emittery_1.default {
/**
* Define onError handler invoked when `emit` fails
*/
onError(errorHandler) {
this.errorHandler = errorHandler;
}
/**
* Emit event
*/
async emit(eventName, eventData, allowMetaEvents) {
try {
await super.emit(eventName, eventData, allowMetaEvents);
}
catch (error) {
if (this.errorHandler) {
await this.errorHandler(error);
}
else {
throw error;
}
}
}
}
exports.Emitter = Emitter;
+77
View File
@@ -0,0 +1,77 @@
import { Macroable } from 'macroable';
import { Test } from '../test/main';
import { Emitter } from '../emitter';
import { Refiner } from '../refiner';
import { GroupHooksHandler, TestHooksHandler, GroupOptions } from '../types';
/**
* Group class exposes an API to group multiple tests together
* and bulk configure them.
*
* NOTE: Nested groups are not supported on purpose.
*
* @example
* const group = new Group('addition', emitter, refiner)
* const test = new Test('2 + 2 = 4', emitter, refiner)
*
* group.add(test)
* await group.exec()
*/
export declare class Group<Context extends Record<any, any>> extends Macroable {
title: string;
private emitter;
private refiner;
static macros: {};
static getters: {};
/**
* Reference to registered hooks
*/
private hooks;
/**
* Callbacks to invoke on each test
*/
private tapsCallbacks;
/**
* Properties to configure on every test
*/
private testsTimeout?;
private testsRetries?;
private testSetupHooks;
private testTeardownHooks;
options: GroupOptions;
/**
* An array of tests registered under the given group
*/
tests: Test<Context, any>[];
/**
* Shortcut methods to configure tests
*/
each: {
setup: (handler: TestHooksHandler<Context>) => void;
teardown: (handler: TestHooksHandler<Context>) => void;
timeout: (timeout: number) => void;
retry: (retries: number) => void;
disableTimeout: () => void;
};
constructor(title: string, emitter: Emitter, refiner: Refiner);
/**
* Add a test to the group. Adding a test to the group
* mutates the test properties
*/
add(test: Test<Context, any>): this;
/**
* Tap into each test and configure it
*/
tap(callback: (test: Test<Context, any>) => void): this;
/**
* Define setup hook for the group
*/
setup(handler: GroupHooksHandler<Context>): this;
/**
* Define teardown hook for the group
*/
teardown(handler: GroupHooksHandler<Context>): this;
/**
* Execute group hooks and tests
*/
exec(): Promise<void>;
}
+156
View File
@@ -0,0 +1,156 @@
"use strict";
/*
* @japa/core
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Group = void 0;
const macroable_1 = require("macroable");
const hooks_1 = require("@poppinss/hooks");
const debug_1 = __importDefault(require("../debug"));
const runner_1 = require("./runner");
/**
* Group class exposes an API to group multiple tests together
* and bulk configure them.
*
* NOTE: Nested groups are not supported on purpose.
*
* @example
* const group = new Group('addition', emitter, refiner)
* const test = new Test('2 + 2 = 4', emitter, refiner)
*
* group.add(test)
* await group.exec()
*/
class Group extends macroable_1.Macroable {
constructor(title, emitter, refiner) {
super();
this.title = title;
this.emitter = emitter;
this.refiner = refiner;
/**
* Reference to registered hooks
*/
this.hooks = new hooks_1.Hooks();
/**
* Callbacks to invoke on each test
*/
this.tapsCallbacks = [];
this.testSetupHooks = [];
this.testTeardownHooks = [];
this.options = {
title: this.title,
meta: {},
};
/**
* An array of tests registered under the given group
*/
this.tests = [];
/**
* Shortcut methods to configure tests
*/
this.each = {
/**
* Define setup hook for all tests inside the group
*/
setup: (handler) => {
this.testSetupHooks.push(handler);
},
/**
* Define teardown hook for all tests inside the group
*/
teardown: (handler) => {
this.testTeardownHooks.push(handler);
},
/**
* Define timeout for all tests inside the group
*/
timeout: (timeout) => {
this.testsTimeout = timeout;
},
/**
* Disable timeout for all tests inside the group
*/
disableTimeout: () => {
this.testsTimeout = 0;
},
/**
* Define retries for all tests inside the group
*/
retry: (retries) => {
this.testsRetries = retries;
},
};
}
/**
* Add a test to the group. Adding a test to the group
* mutates the test properties
*/
add(test) {
(0, debug_1.default)('adding "%s" test to "%s" group', test.title, this.title);
/**
* Bulk configure
*/
if (this.testsTimeout !== undefined) {
test.timeout(this.testsTimeout);
}
if (this.testsRetries !== undefined) {
test.retry(this.testsRetries);
}
if (this.testSetupHooks.length) {
this.testSetupHooks.forEach((handler) => test.setup(handler));
}
if (this.testTeardownHooks.length) {
this.testTeardownHooks.forEach((handler) => test.teardown(handler));
}
/**
* Invoke tap callback passing test to each callback
*/
this.tapsCallbacks.forEach((callback) => callback(test));
this.tests.push(test);
return this;
}
/**
* Tap into each test and configure it
*/
tap(callback) {
this.tapsCallbacks.push(callback);
return this;
}
/**
* Define setup hook for the group
*/
setup(handler) {
(0, debug_1.default)('registering "%s" group setup hook %s', this.title, handler);
this.hooks.add('setup', handler);
return this;
}
/**
* Define teardown hook for the group
*/
teardown(handler) {
(0, debug_1.default)('registering "%s" group teardown hook %s', this.title, handler);
this.hooks.add('teardown', handler);
return this;
}
/**
* Execute group hooks and tests
*/
async exec() {
if (!this.refiner.allows(this)) {
(0, debug_1.default)('group skipped by refined %s', this.title);
return;
}
await new runner_1.GroupRunner(this, this.hooks, this.emitter).run();
}
}
exports.Group = Group;
Group.macros = {};
Group.getters = {};
+56
View File
@@ -0,0 +1,56 @@
import { Hooks } from '@poppinss/hooks';
import { Group } from './main';
import { Emitter } from '../emitter';
/**
* Run all tests for a given group
*/
export declare class GroupRunner {
private group;
private hooks;
private emitter;
/**
* Reference to the startup runner
*/
private setupRunner;
/**
* Reference to the cleanup runner
*/
private teardownRunner;
/**
* Test errors
*/
private errors;
/**
* Track if test has any errors
*/
private hasError;
constructor(group: Group<any>, hooks: Hooks, emitter: Emitter);
/**
* Notify the reporter about the group start
*/
private notifyStart;
/**
* Notify the reporter about the group end
*/
private notifyEnd;
/**
* Running setup hooks
*/
private runSetupHooks;
/**
* Running teardown hooks
*/
private runTeardownHooks;
/**
* Running setup cleanup functions
*/
private runSetupCleanupFunctions;
/**
* Running teardown cleanup functions
*/
private runTeardownCleanupFunctions;
/**
* Run the test
*/
run(): Promise<void>;
}
+152
View File
@@ -0,0 +1,152 @@
"use strict";
/*
* @japa/core
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GroupRunner = void 0;
const debug_1 = __importDefault(require("../debug"));
/**
* Run all tests for a given group
*/
class GroupRunner {
constructor(group, hooks, emitter) {
this.group = group;
this.hooks = hooks;
this.emitter = emitter;
/**
* Reference to the startup runner
*/
this.setupRunner = this.hooks.runner('setup');
/**
* Reference to the cleanup runner
*/
this.teardownRunner = this.hooks.runner('teardown');
/**
* Test errors
*/
this.errors = [];
/**
* Track if test has any errors
*/
this.hasError = false;
}
/**
* Notify the reporter about the group start
*/
notifyStart() {
const startOptions = { ...this.group.options };
this.emitter.emit('group:start', startOptions);
}
/**
* Notify the reporter about the group end
*/
notifyEnd() {
const endOptions = {
...this.group.options,
hasError: this.hasError,
errors: this.errors,
};
this.emitter.emit('group:end', endOptions);
}
/**
* Running setup hooks
*/
async runSetupHooks() {
try {
(0, debug_1.default)('running "%s" group setup hooks', this.group.title);
await this.setupRunner.run(this.group);
}
catch (error) {
(0, debug_1.default)('group setup hooks failed, group: %s, error: %O', this.group.title, error);
this.hasError = true;
this.errors.push({ phase: 'setup', error });
}
}
/**
* Running teardown hooks
*/
async runTeardownHooks() {
try {
(0, debug_1.default)('running "%s" group teardown hooks', this.group.title);
await this.teardownRunner.run(this.group);
}
catch (error) {
(0, debug_1.default)('group teardown hooks failed, group: %s, error: %O', this.group.title, error);
this.hasError = true;
this.errors.push({ phase: 'teardown', error });
}
}
/**
* Running setup cleanup functions
*/
async runSetupCleanupFunctions() {
try {
(0, debug_1.default)('running "%s" group setup cleanup functions', this.group.title);
await this.setupRunner.cleanup(this.hasError, this.group);
}
catch (error) {
(0, debug_1.default)('group setup cleanup function failed, group: %s, error: %O', this.group.title, error);
this.hasError = true;
this.errors.push({ phase: 'setup:cleanup', error });
}
}
/**
* Running teardown cleanup functions
*/
async runTeardownCleanupFunctions() {
try {
(0, debug_1.default)('running "%s" group teardown cleanup functions', this.group.title);
await this.teardownRunner.cleanup(this.hasError, this.group);
}
catch (error) {
(0, debug_1.default)('group teardown cleanup function failed, group: %s, error: %O', this.group.title, error);
this.hasError = true;
this.errors.push({ phase: 'teardown:cleanup', error });
}
}
/**
* Run the test
*/
async run() {
(0, debug_1.default)('starting to run "%s" group', this.group.title);
this.notifyStart();
/**
* Run setup hooks and exit early when one of the hooks
* fails
*/
await this.runSetupHooks();
if (this.hasError) {
await this.runSetupCleanupFunctions();
this.notifyEnd();
return;
}
/**
* Run the test executor
*/
for (let test of this.group.tests) {
await test.exec();
}
/**
* Cleanup setup hooks
*/
await this.runSetupCleanupFunctions();
/**
* Run + cleanup teardown hooks
*/
await this.runTeardownHooks();
await this.runTeardownCleanupFunctions();
/**
* Notify test end
*/
this.notifyEnd();
}
}
exports.GroupRunner = GroupRunner;
+7
View File
@@ -0,0 +1,7 @@
/**
* A simple function interpolate values inside curly braces.
*
* @example
* interpolate('hello { username }', { username: 'virk' })
*/
export declare function interpolate(input: string, data: any, index: number): string;
+53
View File
@@ -0,0 +1,53 @@
"use strict";
/*
* @japa/core
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.interpolate = void 0;
function uncurryThis(fn) {
return function (...args) {
return Function.call.apply(fn, args);
};
}
const hasOwnProperty = uncurryThis(Object.prototype.hasOwnProperty);
/**
* Parses prop
*/
function parseProp(data, key) {
const tokens = key.split('.');
while (tokens.length) {
if (data === null || typeof data !== 'object') {
return;
}
const token = tokens.shift();
data = hasOwnProperty(data, token) ? data[token] : undefined;
}
return data;
}
/**
* A simple function interpolate values inside curly braces.
*
* @example
* interpolate('hello { username }', { username: 'virk' })
*/
function interpolate(input, data, index) {
return input.replace(/(\\)?{(.*?)}/g, (_, escapeChar, key) => {
if (escapeChar) {
return `{${key}}`;
}
key = key.trim();
if (key === '$i') {
return index;
}
if (key === '$self') {
return data;
}
return parseProp(data, key);
});
}
exports.interpolate = interpolate;
+63
View File
@@ -0,0 +1,63 @@
import { Test } from './test/main';
import { Group } from './group/main';
import { FilteringOptions } from './types';
/**
* Exposes the API to refine unwanted tests based upon applied
* filters.
*
* @example
* const refiner = new Refiner({ tags: ['@slow'] })
* refiner.allows('tags', ['@slow']) // true
* refiner.allows('tags', ['@regression']) // false
*
* const refiner = new Refiner({ tags: [] })
* refiner.allows('tags', ['@slow']) // true
* refiner.allows('tags', ['@regression']) // true
*/
export declare class Refiner {
/**
* A set of pinned tests
*/
private pinnedTests;
/**
* Available filters
*/
private filters;
constructor(filters?: FilteringOptions);
/**
* Find if the group is allowed to execute its tests.
*/
private isGroupAllowed;
/**
* Find if the test is allowed to be executed by checking
* for the test title filter
*/
private isTestTitleAllowed;
/**
* Find if test is allowed by the negated tags filter
*/
private allowedByNegatedTags;
/**
* Test if the test is allowed by the tags filter
*/
private allowedByTags;
private areTestTagsAllowed;
private isAllowedByPinnedTest;
/**
* Pin a test to be executed.
*/
pinTest(test: Test<any, any>): void;
/**
* Find if a test is pinned
*/
isPinned(test: Test<any, any>): boolean;
/**
* Add a filter
*/
add(layer: 'tests' | 'tags' | 'groups', values: string[]): void;
/**
* Check if refiner allows a specific test or group to run by looking
* at the applied filters
*/
allows(testOrGroup: Test<any, any> | Group<any>): boolean;
}
+204
View File
@@ -0,0 +1,204 @@
"use strict";
/*
* @japa/core
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.Refiner = void 0;
const main_1 = require("./group/main");
/**
* Exposes the API to refine unwanted tests based upon applied
* filters.
*
* @example
* const refiner = new Refiner({ tags: ['@slow'] })
* refiner.allows('tags', ['@slow']) // true
* refiner.allows('tags', ['@regression']) // false
*
* const refiner = new Refiner({ tags: [] })
* refiner.allows('tags', ['@slow']) // true
* refiner.allows('tags', ['@regression']) // true
*/
class Refiner {
constructor(filters = {}) {
/**
* A set of pinned tests
*/
this.pinnedTests = new Set();
/**
* Available filters
*/
this.filters = {
tags: [],
tests: [],
groups: [],
negateTags: [],
};
if (filters.tags) {
this.add('tags', filters.tags);
}
if (filters.tests) {
this.add('tests', filters.tests);
}
if (filters.groups) {
this.add('groups', filters.groups);
}
}
/**
* Find if the group is allowed to execute its tests.
*/
isGroupAllowed(group) {
const groupFilters = this.filters.groups;
/**
* Group filters exists and group title is not within the filters
* list, then return false right away
*/
if (groupFilters.length && !groupFilters.includes(group.title)) {
return false;
}
/**
* By default the group is not allowed to be executed. However,
* we go through all the tests within that group and if
* one or more tests are allowed to run, then we will
* allow the group to run as well.
*
* Basically, we are checking the children to find if the group
* should run or not.
*/
let allowGroup = false;
for (let test of group.tests) {
allowGroup = this.allows(test);
if (allowGroup) {
break;
}
}
return allowGroup;
}
/**
* Find if the test is allowed to be executed by checking
* for the test title filter
*/
isTestTitleAllowed(test) {
/**
* All tests are allowed, when no filters are applied
* on the test title
*/
if (!this.filters.tests.length) {
return true;
}
return this.filters.tests.includes(test.title);
}
/**
* Find if test is allowed by the negated tags filter
*/
allowedByNegatedTags(test) {
if (!this.filters.negateTags.length) {
return true;
}
/**
* There should be zero matching negated tags
*/
return this.filters.negateTags.every((tag) => !test.options.tags.includes(tag));
}
/**
* Test if the test is allowed by the tags filter
*/
allowedByTags(test) {
if (!this.filters.tags.length) {
return true;
}
/**
* Find one or more matching tags
*/
return this.filters.tags.some((tag) => test.options.tags.includes(tag));
}
/*
* Find if the test is allowed to be executed by checking
* for the test tags
*/
areTestTagsAllowed(test) {
return this.allowedByTags(test) && this.allowedByNegatedTags(test);
}
/*
* Find if the test is allowed to be executed by checking
* for the pinned tests
*/
isAllowedByPinnedTest(test) {
/**
* All tests are allowed, when no tests are pinned
*/
if (!this.pinnedTests.size) {
return true;
}
return this.pinnedTests.has(test);
}
/**
* Pin a test to be executed.
*/
pinTest(test) {
this.pinnedTests.add(test);
}
/**
* Find if a test is pinned
*/
isPinned(test) {
return this.pinnedTests.has(test);
}
/**
* Add a filter
*/
add(layer, values) {
if (layer === 'tags') {
values.forEach((tag) => {
if (tag.startsWith('!')) {
this.filters.negateTags.push(tag.slice(1));
}
else {
this.filters.tags.push(tag);
}
});
}
else {
this.filters[layer].push(...values);
}
}
/**
* Check if refiner allows a specific test or group to run by looking
* at the applied filters
*/
allows(testOrGroup) {
if (testOrGroup instanceof main_1.Group) {
return this.isGroupAllowed(testOrGroup);
}
/**
* Do not run lone tests when group filter is applied. It is responsibility
* of the runner to attach groups to tests.
*/
if (this.filters.groups.length && !testOrGroup.parent) {
return false;
}
/**
* Layer 1
*/
const isTestTitleAllowed = this.isTestTitleAllowed(testOrGroup);
if (!isTestTitleAllowed) {
return false;
}
/**
* Layer 2
*/
const areTestTagsAllowed = this.areTestTagsAllowed(testOrGroup);
if (!areTestTagsAllowed) {
return false;
}
/**
* Layer 3
*/
return this.isAllowedByPinnedTest(testOrGroup);
}
}
exports.Refiner = Refiner;
+88
View File
@@ -0,0 +1,88 @@
import { Macroable } from 'macroable';
import { Suite } from './suite/main';
import { Emitter } from './emitter';
import { ReporterContract, RunnerSummary } from './types';
/**
* The Runner class exposes the API to register test suites and execute
* them sequentially.
*
* @example
* const runner = new Runner(emitter)
* const suite = new Suite('unit', emitter)
*
* runner.add(suite)
* runner.registerReporter(reporters.list)
*
* await runner.exec()
*/
export declare class Runner<Context extends Record<any, any>> extends Macroable {
private emitter;
static macros: {};
static getters: {};
/**
* Callbacks to invoke on every suite
*/
private configureSuiteCallbacks;
/**
* Reference to tests tracker
*/
private tracker;
/**
* Handler to listen for uncaughtException
*/
private uncaughtExceptionHandler?;
/**
* A collection of suites
*/
suites: Suite<Context>[];
/**
* Registered tests reporter
*/
reporters: Set<ReporterContract>;
constructor(emitter: Emitter);
/**
* Notify the reporter about the runner start
*/
private notifyStart;
/**
* Notify the reporter about the runner end
*/
private notifyEnd;
/**
* Boot the runner
*/
private boot;
/**
* Add a suite to the runner
*/
add(suite: Suite<Context>): this;
/**
* Tap into each suite and configure it
*/
onSuite(callback: (suite: Suite<Context>) => void): this;
/**
* Register a tests reporter
*/
registerReporter(reporter: ReporterContract): this;
/**
* Manage unhandled exceptions occurred during tests
*/
manageUnHandledExceptions(): this;
/**
* Get tests summary
*/
getSummary(): RunnerSummary;
/**
* Start the test runner process. The method emits
* "runner:start" event
*/
start(): Promise<void>;
/**
* Execute runner suites
*/
exec(): Promise<void>;
/**
* End the runner process. Emits "runner:end" event
*/
end(): Promise<void>;
}
+148
View File
@@ -0,0 +1,148 @@
"use strict";
/*
* @japa/core
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Runner = void 0;
const macroable_1 = require("macroable");
const debug_1 = __importDefault(require("./debug"));
const tracker_1 = require("./tracker");
/**
* The Runner class exposes the API to register test suites and execute
* them sequentially.
*
* @example
* const runner = new Runner(emitter)
* const suite = new Suite('unit', emitter)
*
* runner.add(suite)
* runner.registerReporter(reporters.list)
*
* await runner.exec()
*/
class Runner extends macroable_1.Macroable {
constructor(emitter) {
super();
this.emitter = emitter;
/**
* Callbacks to invoke on every suite
*/
this.configureSuiteCallbacks = [];
/**
* A collection of suites
*/
this.suites = [];
/**
* Registered tests reporter
*/
this.reporters = new Set();
}
/**
* Notify the reporter about the runner start
*/
notifyStart() {
return this.emitter.emit('runner:start', {});
}
/**
* Notify the reporter about the runner end
*/
notifyEnd() {
return this.emitter.emit('runner:end', {});
}
/**
* Boot the runner
*/
boot() {
this.tracker = new tracker_1.Tracker();
this.emitter.on('uncaught:exception', (payload) => this.tracker.processEvent('uncaught:exception', payload));
this.emitter.on('runner:start', (payload) => this.tracker.processEvent('runner:start', payload));
this.emitter.on('runner:end', (payload) => this.tracker.processEvent('runner:end', payload));
this.emitter.on('suite:start', (payload) => this.tracker.processEvent('suite:start', payload));
this.emitter.on('suite:end', (payload) => this.tracker.processEvent('suite:end', payload));
this.emitter.on('group:start', (payload) => this.tracker.processEvent('group:start', payload));
this.emitter.on('group:end', (payload) => this.tracker.processEvent('group:end', payload));
this.emitter.on('test:start', (payload) => this.tracker.processEvent('test:start', payload));
this.emitter.on('test:end', (payload) => this.tracker.processEvent('test:end', payload));
}
/**
* Add a suite to the runner
*/
add(suite) {
this.configureSuiteCallbacks.forEach((callback) => callback(suite));
this.suites.push(suite);
(0, debug_1.default)('registering suite %s', suite.name);
return this;
}
/**
* Tap into each suite and configure it
*/
onSuite(callback) {
this.configureSuiteCallbacks.push(callback);
return this;
}
/**
* Register a tests reporter
*/
registerReporter(reporter) {
this.reporters.add(reporter);
return this;
}
/**
* Manage unhandled exceptions occurred during tests
*/
manageUnHandledExceptions() {
if (!this.uncaughtExceptionHandler) {
this.uncaughtExceptionHandler = (error) => this.emitter.emit('uncaught:exception', error);
process.on('uncaughtException', this.uncaughtExceptionHandler);
}
return this;
}
/**
* Get tests summary
*/
getSummary() {
return this.tracker.getSummary();
}
/**
* Start the test runner process. The method emits
* "runner:start" event
*/
async start() {
this.boot();
(0, debug_1.default)('starting to run tests');
for (let reporter of this.reporters) {
if (typeof reporter === 'function') {
await reporter(this, this.emitter);
}
else {
await reporter.handler(this, this.emitter);
}
}
await this.notifyStart();
}
/**
* Execute runner suites
*/
async exec() {
for (let suite of this.suites) {
await suite.exec();
}
}
/**
* End the runner process. Emits "runner:end" event
*/
async end() {
await this.notifyEnd();
}
}
exports.Runner = Runner;
Runner.macros = {};
Runner.getters = {};
+71
View File
@@ -0,0 +1,71 @@
import { Macroable } from 'macroable';
import { Emitter } from '../emitter';
import { Test } from '../test/main';
import { Refiner } from '../refiner';
import { Group } from '../group/main';
import { SuiteHooksHandler } from '../types';
/**
* The Suite class exposes the API to run a group of tests
* or independent tests together as part of a suite.
*
* You can think of suites as
* - unit tests suite
* - e2e tests suites
* - and so on
*
* @example
* const suite = new Suite('unit', emitter)
* const group = new Group('addition', emitter, refiner)
* const test = new Test('2 + 2 = 4', emitter, refiner)
*
* suite.add(group)
* group.add(test)
*
* // Runs all the tests inside the registered group
* await suite.exec()
*/
export declare class Suite<Context extends Record<any, any>> extends Macroable {
name: string;
private emitter;
private refiner;
static macros: {};
static getters: {};
/**
* Reference to registered hooks
*/
private hooks;
/**
* Callbacks to invoke on each test and group
*/
private configureTestCallbacks;
private configureGroupCallbacks;
/**
* A collection of tests and groups both
*/
stack: (Test<Context, any> | Group<Context>)[];
constructor(name: string, emitter: Emitter, refiner: Refiner);
/**
* Add a test or a group to the execution stack
*/
add(testOrGroup: Test<Context, any> | Group<Context>): this;
/**
* Tap into each test and configure it
*/
onTest(callback: (test: Test<Context, any>) => void): this;
/**
* Tap into each group and configure it
*/
onGroup(callback: (group: Group<Context>) => void): this;
/**
* Register a test setup function
*/
setup(handler: SuiteHooksHandler<Context>): this;
/**
* Register a test teardown function
*/
teardown(handler: SuiteHooksHandler<Context>): this;
/**
* Execute suite groups, tests and hooks
*/
exec(): Promise<void>;
}
+133
View File
@@ -0,0 +1,133 @@
"use strict";
/*
* @japa/core
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Suite = void 0;
const macroable_1 = require("macroable");
const hooks_1 = require("@poppinss/hooks");
const debug_1 = __importDefault(require("../debug"));
const main_1 = require("../test/main");
const main_2 = require("../group/main");
const runner_1 = require("./runner");
/**
* The Suite class exposes the API to run a group of tests
* or independent tests together as part of a suite.
*
* You can think of suites as
* - unit tests suite
* - e2e tests suites
* - and so on
*
* @example
* const suite = new Suite('unit', emitter)
* const group = new Group('addition', emitter, refiner)
* const test = new Test('2 + 2 = 4', emitter, refiner)
*
* suite.add(group)
* group.add(test)
*
* // Runs all the tests inside the registered group
* await suite.exec()
*/
class Suite extends macroable_1.Macroable {
constructor(name, emitter, refiner) {
super();
this.name = name;
this.emitter = emitter;
this.refiner = refiner;
/**
* Reference to registered hooks
*/
this.hooks = new hooks_1.Hooks();
/**
* Callbacks to invoke on each test and group
*/
this.configureTestCallbacks = [];
this.configureGroupCallbacks = [];
/**
* A collection of tests and groups both
*/
this.stack = [];
}
/**
* Add a test or a group to the execution stack
*/
add(testOrGroup) {
if (testOrGroup instanceof main_2.Group) {
this.configureGroupCallbacks.forEach((callback) => callback(testOrGroup));
}
if (testOrGroup instanceof main_1.Test) {
this.configureTestCallbacks.forEach((callback) => callback(testOrGroup));
}
this.stack.push(testOrGroup);
return this;
}
/**
* Tap into each test and configure it
*/
onTest(callback) {
this.configureTestCallbacks.push(callback);
return this;
}
/**
* Tap into each group and configure it
*/
onGroup(callback) {
this.configureGroupCallbacks.push(callback);
return this;
}
/**
* Register a test setup function
*/
setup(handler) {
(0, debug_1.default)('registering suite setup hook %s', handler);
this.hooks.add('setup', handler);
return this;
}
/**
* Register a test teardown function
*/
teardown(handler) {
(0, debug_1.default)('registering suite teardown hook %s', handler);
this.hooks.add('teardown', handler);
return this;
}
/**
* Execute suite groups, tests and hooks
*/
async exec() {
/**
* By default a suite is not allowed to be executed. However, we go
* through all the tests/ groups within the suite and if one
* or more tests/groups are allowed to run, then we will
* allow the suite to run as well.
*
* Basically, we are checking the children to find if the suite
* should run or not.
*/
let allowSuite = false;
for (let item of this.stack) {
allowSuite = this.refiner.allows(item);
if (allowSuite) {
break;
}
}
if (!allowSuite) {
(0, debug_1.default)('suite disabled by refiner %s', this.name);
return;
}
await new runner_1.SuiteRunner(this, this.hooks, this.emitter).run();
}
}
exports.Suite = Suite;
Suite.macros = {};
Suite.getters = {};
+56
View File
@@ -0,0 +1,56 @@
import { Hooks } from '@poppinss/hooks';
import { Suite } from './main';
import { Emitter } from '../emitter';
/**
* Run all groups or tests inside the suite stack
*/
export declare class SuiteRunner {
private suite;
private hooks;
private emitter;
/**
* Reference to the startup runner
*/
private setupRunner;
/**
* Reference to the cleanup runner
*/
private teardownRunner;
/**
* Test errors
*/
private errors;
/**
* Track if test has any errors
*/
private hasError;
constructor(suite: Suite<any>, hooks: Hooks, emitter: Emitter);
/**
* Notify the reporter about the suite start
*/
private notifyStart;
/**
* Notify the reporter about the suite end
*/
private notifyEnd;
/**
* Running setup hooks
*/
private runSetupHooks;
/**
* Running teardown hooks
*/
private runTeardownHooks;
/**
* Running setup cleanup functions
*/
private runSetupCleanupFunctions;
/**
* Running teardown cleanup functions
*/
private runTeardownCleanupFunctions;
/**
* Run the test
*/
run(): Promise<void>;
}
+152
View File
@@ -0,0 +1,152 @@
"use strict";
/*
* @japa/core
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SuiteRunner = void 0;
const debug_1 = __importDefault(require("../debug"));
/**
* Run all groups or tests inside the suite stack
*/
class SuiteRunner {
constructor(suite, hooks, emitter) {
this.suite = suite;
this.hooks = hooks;
this.emitter = emitter;
/**
* Reference to the startup runner
*/
this.setupRunner = this.hooks.runner('setup');
/**
* Reference to the cleanup runner
*/
this.teardownRunner = this.hooks.runner('teardown');
/**
* Test errors
*/
this.errors = [];
/**
* Track if test has any errors
*/
this.hasError = false;
}
/**
* Notify the reporter about the suite start
*/
notifyStart() {
const startOptions = { name: this.suite.name };
this.emitter.emit('suite:start', startOptions);
}
/**
* Notify the reporter about the suite end
*/
notifyEnd() {
const endOptions = {
name: this.suite.name,
hasError: this.hasError,
errors: this.errors,
};
this.emitter.emit('suite:end', endOptions);
}
/**
* Running setup hooks
*/
async runSetupHooks() {
(0, debug_1.default)('running "%s" suite setup hooks', this.suite.name);
try {
await this.setupRunner.run(this.suite);
}
catch (error) {
(0, debug_1.default)('suite setup hooks failed, suite: %s, error: %O', this.suite.name, error);
this.hasError = true;
this.errors.push({ phase: 'setup', error });
}
}
/**
* Running teardown hooks
*/
async runTeardownHooks() {
(0, debug_1.default)('running "%s" suite teardown hooks', this.suite.name);
try {
await this.teardownRunner.run(this.suite);
}
catch (error) {
(0, debug_1.default)('suite teardown hooks failed, suite: %s, error: %O', this.suite.name, error);
this.hasError = true;
this.errors.push({ phase: 'teardown', error });
}
}
/**
* Running setup cleanup functions
*/
async runSetupCleanupFunctions() {
(0, debug_1.default)('running "%s" suite setup cleanup functions', this.suite.name);
try {
await this.setupRunner.cleanup(this.hasError, this.suite);
}
catch (error) {
(0, debug_1.default)('suite setup cleanup functions failed, suite: %s, error: %O', this.suite.name, error);
this.hasError = true;
this.errors.push({ phase: 'setup:cleanup', error });
}
}
/**
* Running teardown cleanup functions
*/
async runTeardownCleanupFunctions() {
(0, debug_1.default)('running "%s" suite teardown cleanup functions', this.suite.name);
try {
await this.teardownRunner.cleanup(this.hasError, this.suite);
}
catch (error) {
(0, debug_1.default)('suite teardown cleanup functions failed, suite: %s, error: %O', this.suite.name, error);
this.hasError = true;
this.errors.push({ phase: 'teardown:cleanup', error });
}
}
/**
* Run the test
*/
async run() {
(0, debug_1.default)('starting to run "%s" suite', this.suite.name);
this.notifyStart();
/**
* Run setup hooks and exit early when one of the hooks
* fails
*/
await this.runSetupHooks();
if (this.hasError) {
await this.runSetupCleanupFunctions();
this.notifyEnd();
return;
}
/**
* Run the test executor
*/
for (let groupOrTest of this.suite.stack) {
await groupOrTest.exec();
}
/**
* Cleanup setup hooks
*/
await this.runSetupCleanupFunctions();
/**
* Run + cleanup teardown hooks
*/
await this.runTeardownHooks();
await this.runTeardownCleanupFunctions();
/**
* Notify test end
*/
this.notifyEnd();
}
}
exports.SuiteRunner = SuiteRunner;
+150
View File
@@ -0,0 +1,150 @@
import { Macroable } from 'macroable';
import { Group } from '../group/main';
import { Emitter } from '../emitter';
import { Refiner } from '../refiner';
import { DataSetNode, TestEndNode, TestOptions, TestExecutor, TestHooksHandler, TestHooksCleanupHandler } from '../types';
/**
* Test class exposes a self contained API to configure and run
* tests along with its hooks.
*
* @example
* const test = new Test('2 + 2 = 4', emitter, refiner)
*
* test.run(async ({ assert }) => {
* assert.equal(2 + 2 , 4)
* })
*/
export declare class Test<Context extends Record<any, any>, TestData extends DataSetNode = undefined> extends Macroable {
title: string;
private emitter;
private refiner;
parent?: Group<Context> | undefined;
static macros: {};
static getters: {};
/**
* Methods to call before disposing the test
*/
static disposeCallbacks: ((test: Test<any, any>, hasError: boolean, errors: TestEndNode['errors']) => void)[];
/**
* Find if the test has already been executed
*/
private executed;
/**
* Reference to registered hooks
*/
private hooks;
/**
* Test options
*/
options: TestOptions;
/**
* Reference to the test dataset
*/
dataset?: any[];
/**
* Reference to the test context. Available at the time
* of running the test
*/
context: Context;
/**
* Find if the test is pinned
*/
get isPinned(): boolean;
/**
* The function for creating the test context
*/
private contextAccumlator?;
/**
* The function for computing if test should
* be skipped or not
*/
private skipAccumulator?;
/**
* The function that returns the test data set
*/
private datasetAccumlator?;
constructor(title: string, context: Context | ((test: Test<Context, TestData>) => Context | Promise<Context>), emitter: Emitter, refiner: Refiner, parent?: Group<Context> | undefined);
/**
* Find if test should be skipped
*/
private computeShouldSkip;
/**
* Find if test is a todo
*/
private computeisTodo;
/**
* Returns the dataset array or undefined
*/
private computeDataset;
/**
* Get context instance for the test
*/
private computeContext;
/**
* Skip the test conditionally
*/
skip(skip?: boolean | (() => Promise<boolean> | boolean), skipReason?: string): this;
/**
* Expect the test to fail. Helpful in creating test cases
* to showcase bugs
*/
fails(failReason?: string): this;
/**
* Define custom timeout for the test
*/
timeout(timeout: number): this;
/**
* Disable test timeout. It is same as calling `test.timeout(0)`
*/
disableTimeout(): this;
/**
* Assign tags to the test. Later you can use the tags to run
* specific tests
*/
tags(tags: string[], strategy?: 'replace' | 'append' | 'prepend'): this;
/**
* Configure the number of times this test should be retried
* when failing.
*/
retry(retries: number): this;
/**
* Wait for the test executor to call done method
*/
waitForDone(): this;
/**
* Pin current test. Pinning a test will only run the
* pinned tests.
*/
pin(): this;
/**
* Define a dispose callback.
*
* Do note: Async methods are not allowed
*/
static dispose(callback: (test: Test<any, any>, hasError: boolean, errors: TestEndNode['errors']) => void): void;
/**
* Define the dataset for the test. The test executor will be invoked
* for all the items inside the dataset array
*/
with<Dataset extends DataSetNode>(dataset: Dataset): Test<Context, Dataset>;
/**
* Define the test executor function
*/
run(executor: TestExecutor<Context, TestData>): this;
/**
* Register a test setup function
*/
setup(handler: TestHooksHandler<Context>): this;
/**
* Register a test teardown function
*/
teardown(handler: TestHooksHandler<Context>): this;
/**
* Register a cleanup hook from within the test
*/
cleanup(handler: TestHooksCleanupHandler<Context>): this;
/**
* Execute test
*/
exec(): Promise<void>;
}
+300
View File
@@ -0,0 +1,300 @@
"use strict";
/*
* @japa/core
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Test = void 0;
const macroable_1 = require("macroable");
const hooks_1 = require("@poppinss/hooks");
const debug_1 = __importDefault(require("../debug"));
const runner_1 = require("./runner");
/**
* Test class exposes a self contained API to configure and run
* tests along with its hooks.
*
* @example
* const test = new Test('2 + 2 = 4', emitter, refiner)
*
* test.run(async ({ assert }) => {
* assert.equal(2 + 2 , 4)
* })
*/
class Test extends macroable_1.Macroable {
/**
* Find if the test is pinned
*/
get isPinned() {
return this.refiner.isPinned(this);
}
constructor(title, context, emitter, refiner, parent) {
super();
this.title = title;
this.emitter = emitter;
this.refiner = refiner;
this.parent = parent;
/**
* Find if the test has already been executed
*/
this.executed = false;
/**
* Reference to registered hooks
*/
this.hooks = new hooks_1.Hooks();
/**
* Test options
*/
this.options = {
title: this.title,
tags: [],
timeout: 2000,
meta: {},
};
/**
* Make sure the instantiated class has its own property "disposeCalls"
*/
if (!this.constructor.hasOwnProperty('disposeCallbacks')) {
throw new Error(`Define static property "disposeCallbacks = []" on ${this.constructor.name} class`);
}
if (typeof context === 'function') {
this.contextAccumlator = context;
}
else {
this.context = context;
}
}
/**
* Find if test should be skipped
*/
async computeShouldSkip() {
if (this.skipAccumulator) {
this.options.isSkipped = await this.skipAccumulator();
}
}
/**
* Find if test is a todo
*/
computeisTodo() {
this.options.isTodo = !this.options.executor;
}
/**
* Returns the dataset array or undefined
*/
async computeDataset() {
if (typeof this.datasetAccumlator === 'function') {
this.dataset = await this.datasetAccumlator();
}
return this.dataset;
}
/**
* Get context instance for the test
*/
async computeContext() {
if (typeof this.contextAccumlator === 'function') {
this.context = await this.contextAccumlator(this);
}
return this.context;
}
/**
* Skip the test conditionally
*/
skip(skip = true, skipReason) {
if (typeof skip === 'function') {
this.skipAccumulator = skip;
}
else {
this.options.isSkipped = skip;
}
this.options.skipReason = skipReason;
return this;
}
/**
* Expect the test to fail. Helpful in creating test cases
* to showcase bugs
*/
fails(failReason) {
this.options.isFailing = true;
this.options.failReason = failReason;
return this;
}
/**
* Define custom timeout for the test
*/
timeout(timeout) {
this.options.timeout = timeout;
return this;
}
/**
* Disable test timeout. It is same as calling `test.timeout(0)`
*/
disableTimeout() {
return this.timeout(0);
}
/**
* Assign tags to the test. Later you can use the tags to run
* specific tests
*/
tags(tags, strategy = 'replace') {
if (strategy === 'replace') {
this.options.tags = tags;
return this;
}
if (strategy === 'prepend') {
this.options.tags = tags.concat(this.options.tags);
return this;
}
this.options.tags = this.options.tags.concat(tags);
return this;
}
/**
* Configure the number of times this test should be retried
* when failing.
*/
retry(retries) {
this.options.retries = retries;
return this;
}
/**
* Wait for the test executor to call done method
*/
waitForDone() {
this.options.waitsForDone = true;
return this;
}
/**
* Pin current test. Pinning a test will only run the
* pinned tests.
*/
pin() {
this.refiner.pinTest(this);
return this;
}
/**
* Define a dispose callback.
*
* Do note: Async methods are not allowed
*/
static dispose(callback) {
this.disposeCallbacks.push(callback);
}
/**
* Define the dataset for the test. The test executor will be invoked
* for all the items inside the dataset array
*/
with(dataset) {
if (Array.isArray(dataset)) {
this.dataset = dataset;
return this;
}
if (typeof dataset === 'function') {
this.datasetAccumlator = dataset;
return this;
}
throw new Error('dataset must be an array or a function that returns an array');
}
/**
* Define the test executor function
*/
run(executor) {
this.options.executor = executor;
return this;
}
/**
* Register a test setup function
*/
setup(handler) {
(0, debug_1.default)('registering "%s" test setup hook %s', this.title, handler);
this.hooks.add('setup', handler);
return this;
}
/**
* Register a test teardown function
*/
teardown(handler) {
(0, debug_1.default)('registering "%s" test teardown hook %s', this.title, handler);
this.hooks.add('teardown', handler);
return this;
}
/**
* Register a cleanup hook from within the test
*/
cleanup(handler) {
(0, debug_1.default)('registering "%s" test cleanup function %s', this.title, handler);
this.hooks.add('cleanup', handler);
return this;
}
/**
* Execute test
*/
async exec() {
/**
* Return early, if there are pinned test and the current test is not
* pinned.
*
* However, the pinned test check is only applied when there
* is no filter on the test title.
*/
if (!this.refiner.allows(this)) {
(0, debug_1.default)('test "%s" skipped by refiner', this.title);
return;
}
/**
* Avoid re-running the same test multiple times
*/
if (this.executed) {
return;
}
this.executed = true;
/**
* Do not run tests without executor function
*/
this.computeisTodo();
if (this.options.isTodo) {
(0, debug_1.default)('skipping todo test "%s"', this.title);
new runner_1.DummyRunner(this, this.emitter).run();
return;
}
/**
* Do not run test meant to be skipped
*/
await this.computeShouldSkip();
if (this.options.isSkipped) {
(0, debug_1.default)('skipping test "%s", reason (%s)', this.title, this.options.skipReason || 'Skipped using .skip method');
new runner_1.DummyRunner(this, this.emitter).run();
return;
}
/**
* Run for each row inside dataset
*/
await this.computeDataset();
if (Array.isArray(this.dataset) && this.dataset.length) {
let index = 0;
// eslint-disable-next-line @typescript-eslint/naming-convention
for (let _ of this.dataset) {
await this.computeContext();
await new runner_1.TestRunner(this, this.hooks, this.emitter, this.constructor.disposeCallbacks, index).run();
index++;
}
return;
}
/**
* Run when no dataset is used
*/
await this.computeContext();
await new runner_1.TestRunner(this, this.hooks, this.emitter, this.constructor.disposeCallbacks).run();
}
}
exports.Test = Test;
Test.macros = {};
Test.getters = {};
/**
* Methods to call before disposing the test
*/
Test.disposeCallbacks = [];
+115
View File
@@ -0,0 +1,115 @@
import { Hooks } from '@poppinss/hooks';
import { Test } from './main';
import { Emitter } from '../emitter';
import { TestEndNode } from '../types';
/**
* Dummy test runner that just emits the required events
*/
export declare class DummyRunner {
private test;
private emitter;
constructor(test: Test<any, any>, emitter: Emitter);
/**
* Notify the reporter about the test start
*/
private notifyStart;
/**
* Notify the reporter about the test start
*/
private notifyEnd;
/**
* Run test
*/
run(): void;
}
/**
* Run an instance of test
*/
export declare class TestRunner {
private test;
private hooks;
private emitter;
private disposeCalls;
private datasetCurrentIndex?;
/**
* Time tracker to find test duration
*/
private timeTracker;
/**
* Reference to the startup runner
*/
private setupRunner;
/**
* Reference to the cleanup runner
*/
private teardownRunner;
/**
* Test errors
*/
private errors;
/**
* Track if test has any errors
*/
private hasError;
private uncaughtExceptionHandler?;
constructor(test: Test<any, any>, hooks: Hooks, emitter: Emitter, disposeCalls: ((test: Test<any, any>, hasError: boolean, errors: TestEndNode['errors']) => void)[], datasetCurrentIndex?: number | undefined);
/**
* Returns the dataset node for the test events
*/
private getDatasetNode;
/**
* Get the title node for the test
*/
private getTitle;
/**
* Notify the reporter about the test start
*/
private notifyStart;
/**
* Notify the reporter about the test start
*/
private notifyEnd;
/**
* Running setup hooks
*/
private runSetupHooks;
/**
* Running teardown hooks
*/
private runTeardownHooks;
/**
* Running test cleanup functions
*/
private runTestCleanupFunctions;
/**
* Running setup cleanup functions
*/
private runSetupCleanupFunctions;
/**
* Running teardown cleanup functions
*/
private runTeardownCleanupFunctions;
/**
* Run the test executor. The method takes care of passing
* dataset row to the test method
*/
private runTest;
/**
* Run the test executor that relies on the done method. The test will
* timeout if done isn't called.
*/
private runTestWithDone;
/**
* Run the test executor and make sure it times out after the configured
* timeout.
*/
private wrapTestInTimeout;
/**
* Runs the test with retries in place
*/
private wrapTestInRetries;
/**
* Run the test
*/
run(): Promise<void>;
}
+367
View File
@@ -0,0 +1,367 @@
"use strict";
/*
* @japa/core
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TestRunner = exports.DummyRunner = void 0;
const async_retry_1 = __importDefault(require("async-retry"));
const time_span_1 = __importDefault(require("time-span"));
const debug_1 = __importDefault(require("../debug"));
const interpolate_1 = require("../interpolate");
/**
* Dummy test runner that just emits the required events
*/
class DummyRunner {
constructor(test, emitter) {
this.test = test;
this.emitter = emitter;
}
/**
* Notify the reporter about the test start
*/
notifyStart() {
const startOptions = {
...this.test.options,
title: {
original: this.test.options.title,
expanded: this.test.options.title,
toString() {
return this.original;
},
},
isPinned: this.test.isPinned,
};
this.emitter.emit('test:start', startOptions);
}
/**
* Notify the reporter about the test start
*/
notifyEnd() {
const endOptions = {
...this.test.options,
title: {
original: this.test.options.title,
expanded: this.test.options.title,
toString() {
return this.original;
},
},
isPinned: this.test.isPinned,
hasError: false,
duration: 0,
errors: [],
};
this.emitter.emit('test:end', endOptions);
}
/**
* Run test
*/
run() {
this.notifyStart();
this.notifyEnd();
}
}
exports.DummyRunner = DummyRunner;
/**
* Run an instance of test
*/
class TestRunner {
constructor(test, hooks, emitter, disposeCalls, datasetCurrentIndex) {
this.test = test;
this.hooks = hooks;
this.emitter = emitter;
this.disposeCalls = disposeCalls;
this.datasetCurrentIndex = datasetCurrentIndex;
/**
* Reference to the startup runner
*/
this.setupRunner = this.hooks.runner('setup');
/**
* Reference to the cleanup runner
*/
this.teardownRunner = this.hooks.runner('teardown');
/**
* Test errors
*/
this.errors = [];
/**
* Track if test has any errors
*/
this.hasError = false;
}
/**
* Returns the dataset node for the test events
*/
getDatasetNode() {
if (this.datasetCurrentIndex !== undefined && this.test.dataset) {
return {
dataset: {
row: this.test.dataset[this.datasetCurrentIndex],
index: this.datasetCurrentIndex,
size: this.test.dataset.length,
},
};
}
}
/**
* Get the title node for the test
*/
getTitle(dataset) {
const title = this.test.options.title;
return {
original: title,
expanded: dataset ? (0, interpolate_1.interpolate)(title, dataset.row, dataset.index + 1) : title,
toString() {
return this.original;
},
};
}
/**
* Notify the reporter about the test start
*/
notifyStart() {
this.timeTracker = (0, time_span_1.default)();
const dataset = this.getDatasetNode();
const startOptions = {
...this.test.options,
...dataset,
isPinned: this.test.isPinned,
title: this.getTitle(dataset ? dataset.dataset : undefined),
};
this.emitter.emit('test:start', startOptions);
}
/**
* Notify the reporter about the test start
*/
notifyEnd() {
const dataset = this.getDatasetNode();
const endOptions = {
...this.test.options,
...dataset,
isPinned: this.test.isPinned,
title: this.getTitle(dataset ? dataset.dataset : undefined),
hasError: this.hasError,
errors: this.errors,
retryAttempt: this.test.options.retryAttempt,
duration: this.timeTracker.rounded(),
};
this.emitter.emit('test:end', endOptions);
}
/**
* Running setup hooks
*/
async runSetupHooks() {
try {
(0, debug_1.default)('running "%s" test setup hooks', this.test.title);
await this.setupRunner.run(this.test);
}
catch (error) {
(0, debug_1.default)('test setup hooks failed, test: %s, error: %O', this.test.title, error);
this.hasError = true;
this.errors.push({ phase: 'setup', error });
}
}
/**
* Running teardown hooks
*/
async runTeardownHooks() {
try {
(0, debug_1.default)('running "%s" test teardown hooks', this.test.title);
await this.teardownRunner.run(this.test);
}
catch (error) {
(0, debug_1.default)('test teardown hooks failed, test: %s, error: %O', this.test.title, error);
this.hasError = true;
this.errors.push({ phase: 'teardown', error });
}
}
/**
* Running test cleanup functions
*/
async runTestCleanupFunctions() {
try {
(0, debug_1.default)('running "%s" test cleanup functions', this.test.title);
await this.hooks.runner('cleanup').run(this.errors.length > 0, this.test);
}
catch (error) {
(0, debug_1.default)('test cleanup functions failed, test: %s, error: %O', this.test.title, error);
this.hasError = true;
this.errors.push({ phase: 'test:cleanup', error });
}
}
/**
* Running setup cleanup functions
*/
async runSetupCleanupFunctions() {
try {
(0, debug_1.default)('running "%s" test setup cleanup functions', this.test.title);
await this.setupRunner.cleanup(this.errors.length > 0, this.test);
}
catch (error) {
(0, debug_1.default)('test setup cleanup functions failed, test: %s, error: %O', this.test.title, error);
this.hasError = true;
this.errors.push({ phase: 'setup:cleanup', error });
}
}
/**
* Running teardown cleanup functions
*/
async runTeardownCleanupFunctions() {
try {
(0, debug_1.default)('running "%s" test teardown cleanup functions', this.test.title);
await this.teardownRunner.cleanup(this.errors.length > 0, this.test);
}
catch (error) {
(0, debug_1.default)('test teardown cleanup functions failed, test: %s, error: %O', this.test.title, error);
this.hasError = true;
this.errors.push({ phase: 'teardown:cleanup', error });
}
}
/**
* Run the test executor. The method takes care of passing
* dataset row to the test method
*/
async runTest(done) {
const datasetRow = this.datasetCurrentIndex !== undefined && this.test.dataset
? this.test.dataset[this.datasetCurrentIndex]
: undefined;
return datasetRow !== undefined
? this.test.options.executor(this.test.context, datasetRow, done)
: this.test.options.executor(this.test.context, done);
}
/**
* Run the test executor that relies on the done method. The test will
* timeout if done isn't called.
*/
runTestWithDone() {
return new Promise((resolve, reject) => {
const done = (error) => {
if (error) {
reject(error);
}
else {
resolve();
}
};
/**
* Done style tests the primary source of uncaught exceptions. Hence
* we make an extra efforts to related uncaught exceptions with
* them
*/
if (!this.uncaughtExceptionHandler) {
this.uncaughtExceptionHandler = (error) => {
reject(error);
};
process.on('uncaughtException', this.uncaughtExceptionHandler);
}
(0, debug_1.default)('running test "%s" and waiting for done method call', this.test.title);
this.runTest(done).catch(reject);
});
}
/**
* Run the test executor and make sure it times out after the configured
* timeout.
*/
async wrapTestInTimeout() {
if (!this.test.options.timeout) {
return this.test.options.waitsForDone ? this.runTestWithDone() : this.runTest();
}
let timeoutTimer = null;
const timeout = () => {
return new Promise((_, reject) => {
timeoutTimer = setTimeout(() => reject(new Error('Test timeout')), this.test.options.timeout);
});
};
try {
await Promise.race([
this.test.options.waitsForDone ? this.runTestWithDone() : this.runTest(),
timeout(),
]);
}
finally {
if (timeoutTimer) {
clearTimeout(timeoutTimer);
}
}
}
/**
* Runs the test with retries in place
*/
wrapTestInRetries() {
if (!this.test.options.retries) {
return this.wrapTestInTimeout();
}
return (0, async_retry_1.default)((_, attempt) => {
this.test.options.retryAttempt = attempt;
return this.wrapTestInTimeout();
}, { retries: this.test.options.retries, factor: 1 });
}
/**
* Run the test
*/
async run() {
(0, debug_1.default)('starting to run "%s" test', this.test.title);
this.notifyStart();
/**
* Run setup hooks and exit early when one of the hooks
* fails
*/
await this.runSetupHooks();
if (this.hasError) {
await this.runSetupCleanupFunctions();
this.notifyEnd();
return;
}
/**
* Run the test executor
*/
try {
await this.wrapTestInRetries();
}
catch (error) {
this.hasError = true;
this.errors.push({ phase: 'test', error });
}
finally {
if (this.uncaughtExceptionHandler) {
process.removeListener('uncaughtException', this.uncaughtExceptionHandler);
}
}
/**
* Run dispose callbacks
*/
try {
this.disposeCalls.forEach((callback) => callback(this.test, this.hasError, this.errors));
}
catch (error) {
this.hasError = true;
this.errors.push({ phase: 'test', error });
}
/**
* Run test cleanup hooks
*/
await this.runTestCleanupFunctions();
/**
* Cleanup setup hooks
*/
await this.runSetupCleanupFunctions();
/**
* Run + cleanup teardown hooks
*/
await this.runTeardownHooks();
await this.runTeardownCleanupFunctions();
/**
* Notify test end
*/
this.notifyEnd();
}
}
exports.TestRunner = TestRunner;
+11
View File
@@ -0,0 +1,11 @@
/// <reference types="node" />
import { inspect } from 'util';
import { Macroable } from 'macroable';
/**
* A fresh copy of test context is shared with all the tests
*/
export declare class TestContext extends Macroable {
static macros: {};
static getters: {};
[inspect.custom](): string;
}
+24
View File
@@ -0,0 +1,24 @@
"use strict";
/*
* @japa/core
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.TestContext = void 0;
const util_1 = require("util");
const macroable_1 = require("macroable");
/**
* A fresh copy of test context is shared with all the tests
*/
class TestContext extends macroable_1.Macroable {
[util_1.inspect.custom]() {
return (0, util_1.inspect)(this, { showHidden: false, depth: 1, colors: true, customInspect: false });
}
}
exports.TestContext = TestContext;
TestContext.macros = {};
TestContext.getters = {};
+81
View File
@@ -0,0 +1,81 @@
import { RunnerEvents, RunnerSummary } from './types';
/**
* Tracks the tests events to generate a summary report. Failing tests are further tracked
* for complete hierarchy
*/
export declare class Tracker {
/**
* Time tracker to find runner duration
*/
private timeTracker;
/**
* Currently active suite
*/
private currentSuite?;
/**
* Currently active group
*/
private currentGroup?;
/**
* If the entire run cycle has one or more errors
*/
private hasError;
/**
* Storing state if current suite and group has errors. These
* errors are not directly from the suite and groups, but
* instead from their children.
*
* For example: If a test fails, it marks both current group
* and suite has errors.
*/
private currentSuiteHasError;
private currentGroupHasError;
private aggregates;
private duration;
/**
* A tree of suites/groups and tests that have failed. They are always nested inside
* other unless the test groups where used, then suites contains a list of tests
* directly.
*/
private failureTree;
private failedTestsTitles;
/**
* Set reference for the current suite
*/
private onSuiteStart;
/**
* Move suite to the failure tree when the suite
* has errors
*/
private onSuiteEnd;
/**
* Set reference for the current group
*/
private onGroupStart;
/**
* Move suite to the failure tree when the suite
* has errors
*/
private onGroupEnd;
/**
* In case of failure, track the test inside the current group
* or the current suite.
*/
private onTestEnd;
/**
* Mark test as failed
*/
private markTestAsFailed;
/**
* Increment the count of uncaught exceptions
*/
private onUncaughtException;
/**
* Process the tests events
*/
processEvent<Event extends keyof RunnerEvents>(event: keyof RunnerEvents, payload: RunnerEvents[Event]): void;
/**
* Returns the tests runner summary
*/
getSummary(): RunnerSummary;
}
+240
View File
@@ -0,0 +1,240 @@
"use strict";
/*
* @japa/core
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Tracker = void 0;
const time_span_1 = __importDefault(require("time-span"));
/**
* Tracks the tests events to generate a summary report. Failing tests are further tracked
* for complete hierarchy
*/
class Tracker {
constructor() {
/**
* If the entire run cycle has one or more errors
*/
this.hasError = false;
/**
* Storing state if current suite and group has errors. These
* errors are not directly from the suite and groups, but
* instead from their children.
*
* For example: If a test fails, it marks both current group
* and suite has errors.
*/
this.currentSuiteHasError = false;
this.currentGroupHasError = false;
this.aggregates = {
total: 0,
failed: 0,
passed: 0,
regression: 0,
skipped: 0,
todo: 0,
uncaughtExceptions: 0,
};
this.duration = 0;
/**
* A tree of suites/groups and tests that have failed. They are always nested inside
* other unless the test groups where used, then suites contains a list of tests
* directly.
*/
this.failureTree = [];
this.failedTestsTitles = [];
}
/**
* Set reference for the current suite
*/
onSuiteStart(payload) {
this.currentSuiteHasError = false;
this.currentSuite = {
name: payload.name,
type: 'suite',
errors: [],
children: [],
};
}
/**
* Move suite to the failure tree when the suite
* has errors
*/
onSuiteEnd(payload) {
if (payload.hasError) {
this.hasError = true;
this.currentSuiteHasError = true;
this.currentSuite.errors = payload.errors;
}
if (this.currentSuiteHasError) {
this.failureTree.push(this.currentSuite);
}
}
/**
* Set reference for the current group
*/
onGroupStart(payload) {
this.currentGroupHasError = false;
this.currentGroup = {
name: payload.title,
type: 'group',
errors: [],
children: [],
};
}
/**
* Move suite to the failure tree when the suite
* has errors
*/
onGroupEnd(payload) {
if (payload.hasError) {
this.hasError = true;
this.currentGroupHasError = true;
this.currentGroup.errors = payload.errors;
}
if (this.currentGroupHasError) {
this.currentSuiteHasError = true;
this.currentSuite.children.push(this.currentGroup);
}
}
/**
* In case of failure, track the test inside the current group
* or the current suite.
*/
onTestEnd(payload) {
/**
* Bumping aggregates
*/
this.aggregates.total++;
/**
* Test was skipped
*/
if (payload.isSkipped) {
this.aggregates.skipped++;
return;
}
/**
* Test was a todo
*/
if (payload.isTodo) {
this.aggregates.todo++;
return;
}
/**
* Regression test. Mark test as failed, when there is no error
* Because, we expect regression tests to have errors.
*
* However, there is no need to move anything to the failure
* tree, since there is no real error
*/
if (payload.isFailing) {
if (!payload.hasError) {
this.aggregates.failed++;
this.hasError = true;
}
else {
this.aggregates.regression++;
}
return;
}
/**
* Test completed successfully
*/
if (!payload.hasError) {
this.aggregates.passed++;
return;
}
this.markTestAsFailed(payload);
}
/**
* Mark test as failed
*/
markTestAsFailed(payload) {
/**
* Bump failed count
*/
this.aggregates.failed++;
this.hasError = true;
/**
* Test payload
*/
const testPayload = {
type: 'test',
title: payload.title.expanded,
errors: payload.errors,
};
/**
* Track test inside the current group or suite
*/
if (this.currentGroup) {
this.currentGroupHasError = true;
this.currentGroup.children.push(testPayload);
}
else if (this.currentSuite) {
this.currentSuiteHasError = true;
this.currentSuite.children.push(testPayload);
}
/**
* Push title to the failedTestsTitles array
*/
this.failedTestsTitles.push(payload.title.original);
}
/**
* Increment the count of uncaught exceptions
*/
onUncaughtException() {
this.aggregates.uncaughtExceptions++;
this.hasError = true;
}
/**
* Process the tests events
*/
processEvent(event, payload) {
switch (event) {
case 'uncaught:exception':
this.onUncaughtException();
break;
case 'suite:start':
this.onSuiteStart(payload);
break;
case 'suite:end':
this.onSuiteEnd(payload);
break;
case 'group:start':
this.onGroupStart(payload);
break;
case 'group:end':
this.onGroupEnd(payload);
break;
case 'test:end':
this.onTestEnd(payload);
break;
case 'runner:start':
this.timeTracker = (0, time_span_1.default)();
break;
case 'runner:end':
this.duration = this.timeTracker.rounded();
break;
}
}
/**
* Returns the tests runner summary
*/
getSummary() {
return {
aggregates: this.aggregates,
hasError: this.hasError,
duration: this.duration,
failureTree: this.failureTree,
failedTestsTitles: this.failedTestsTitles,
};
}
}
exports.Tracker = Tracker;
+222
View File
@@ -0,0 +1,222 @@
import type { Runner } from './runner';
import type { Test } from './test/main';
import type { Emitter } from './emitter';
import type { Group } from './group/main';
import type { Suite } from './suite/main';
/**
* Shape of test data set. Should be an array of a function that
* returns an array
*/
export type DataSetNode = undefined | any[] | (() => any[] | Promise<any[]>);
/**
* The cleanup function for test hooks
*/
export type TestHooksCleanupHandler<Context extends Record<any, any>> = (error: null | any, test: Test<Context, any>) => Promise<any> | any;
/**
* The function that can be registered as a test hook
*/
export type TestHooksHandler<Context extends Record<any, any>> = (test: Test<Context, any>) => Promise<any> | any | TestHooksCleanupHandler<Context> | Promise<TestHooksCleanupHandler<Context>>;
/**
* The cleanup function for group hooks
*/
export type GroupHooksCleanupHandler<Context extends Record<any, any>> = (error: null | any, group: Group<Context>) => Promise<any> | any;
/**
* The function that can be registered as a group hook
*/
export type GroupHooksHandler<Context extends Record<any, any>> = (group: Group<Context>) => Promise<any> | any | GroupHooksCleanupHandler<Context> | Promise<GroupHooksCleanupHandler<Context>>;
/**
* The cleanup function for suite hooks
*/
export type SuiteHooksCleanupHandler<Context extends Record<any, any>> = (error: null | any, suite: Suite<Context>) => Promise<any> | any;
/**
* The function that can be registered as a suite hook
*/
export type SuiteHooksHandler<Context extends Record<any, any>> = (suite: Suite<Context>) => Promise<any> | any | SuiteHooksCleanupHandler<Context> | Promise<SuiteHooksCleanupHandler<Context>>;
/**
* The function to execute the test
*/
export type TestExecutor<Context, DataSet> = DataSet extends any[] ? (context: Context, value: DataSet[number], done: (error?: any) => void) => void | Promise<void> : DataSet extends () => infer A ? (context: Context, value: Awaited<A> extends any[] ? Awaited<A>[number] : Awaited<A>, done?: (error?: any) => void) => void | Promise<void> : (context: Context, done: (error?: any) => void) => void | Promise<void>;
/**
* Test configuration options.
*/
export type TestOptions = {
title: string;
tags: string[];
timeout: number;
waitsForDone?: boolean;
executor?: TestExecutor<any, any>;
isTodo?: boolean;
isSkipped?: boolean;
isFailing?: boolean;
skipReason?: string;
failReason?: string;
retries?: number;
retryAttempt?: number;
meta: Record<string, any>;
};
/**
* Data shared during "test:start" event
*/
export type TestStartNode = Omit<TestOptions, 'title'> & {
title: {
original: string;
expanded: string;
toString(): string;
};
isPinned: boolean;
dataset?: {
size: number;
index: number;
row: any;
};
};
/**
* Data shared during "test:end" event
*/
export type TestEndNode = Omit<TestOptions, 'title'> & {
title: {
original: string;
expanded: string;
toString(): string;
};
isPinned: boolean;
duration: number;
hasError: boolean;
errors: {
phase: 'setup' | 'test' | 'setup:cleanup' | 'teardown' | 'teardown:cleanup' | 'test:cleanup';
error: Error;
}[];
retryAttempt?: number;
dataset?: {
size: number;
index: number;
row: any;
};
};
/**
* Group options
*/
export type GroupOptions = {
title: string;
meta: Record<string, any>;
};
/**
* Data shared with "group:start" event
*/
export type GroupStartNode = GroupOptions;
/**
* Data shared with "group:end" event
*/
export type GroupEndNode = GroupOptions & {
hasError: boolean;
errors: {
phase: 'setup' | 'setup:cleanup' | 'teardown' | 'teardown:cleanup';
error: Error;
}[];
};
/**
* Data shared with "suite:start" event
*/
export type SuiteStartNode = {
name: string;
};
/**
* Data shared with "suite:end" event
*/
export type SuiteEndNode = {
name: string;
hasError: boolean;
errors: {
phase: 'setup' | 'setup:cleanup' | 'teardown' | 'teardown:cleanup';
error: Error;
}[];
};
/**
* Data shared with "runner:start" event
*/
export type RunnerStartNode = {};
/**
* Data shared with "runner:end" event
*/
export type RunnerEndNode = {};
/**
* Events emitted by the runner emitter. These can be extended as well
*/
export interface RunnerEvents {
'test:start': TestStartNode;
'test:end': TestEndNode;
'group:start': GroupStartNode;
'group:end': GroupEndNode;
'suite:start': SuiteStartNode;
'suite:end': SuiteEndNode;
'runner:start': RunnerStartNode;
'runner:end': RunnerEndNode;
'uncaught:exception': Error;
}
/**
* Options for filtering and running on selected tests
*/
export type FilteringOptions = {
tags?: string[];
groups?: string[];
tests?: string[];
};
/**
* Type for the reporter handler function
*/
export type ReporterHandlerContract = (runner: Runner<any>, emitter: Emitter) => void | Promise<void>;
/**
* Type for a named reporter object.
*/
export type NamedReporterContract = {
readonly name: string;
handler: ReporterHandlerContract;
};
/**
* Test reporters must adhere to the following contract
*/
export type ReporterContract = ReporterHandlerContract | NamedReporterContract;
/**
* The test node inside the failure tree
*/
export type FailureTreeTestNode = {
title: string;
type: 'test';
errors: TestEndNode['errors'];
};
/**
* The group node inside the failure tree
*/
export type FailureTreeGroupNode = {
name: string;
type: 'group';
errors: GroupEndNode['errors'];
children: FailureTreeTestNode[];
};
/**
* The suite node inside the failure tree
*/
export type FailureTreeSuiteNode = {
name: string;
type: 'suite';
errors: SuiteEndNode['errors'];
children: (FailureTreeTestNode | FailureTreeGroupNode)[];
};
/**
* Runner summary properties
*/
export type RunnerSummary = {
aggregates: {
total: number;
failed: number;
passed: number;
regression: number;
skipped: number;
todo: number;
uncaughtExceptions: number;
};
duration: number;
hasError: boolean;
failureTree: FailureTreeSuiteNode[];
failedTestsTitles: string[];
};
+10
View File
@@ -0,0 +1,10 @@
"use strict";
/*
* @japa/core
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
Object.defineProperty(exports, "__esModule", { value: true });
+134
View File
@@ -0,0 +1,134 @@
{
"name": "@japa/core",
"version": "7.3.3",
"description": "The core of Japa testing framework",
"main": "build/index.js",
"files": [
"build/src",
"build/index.d.ts",
"build/index.js"
],
"exports": {
".": "./build/index.js",
"./types": "./build/src/types.js"
},
"scripts": {
"mrm": "mrm --preset=@adonisjs/mrm-preset",
"pretest": "npm run lint",
"test": "cross-env NODE_DEBUG=japa:core nyc --reporter html node .bin/test.js",
"clean": "del build",
"compile": "npm run lint && npm run clean && tsc",
"build": "npm run compile",
"prepublishOnly": "npm run build",
"lint": "eslint . --ext=.ts",
"format": "prettier --write .",
"commit": "git-cz",
"release": "np --message=\"chore(release): %s\"",
"version": "npm run build",
"sync-labels": "npx github-label-sync --labels ./node_modules/@adonisjs/mrm-preset/gh-labels.json thetutlage/japa"
},
"repository": {
"type": "git",
"url": "git+https://github.com/thetutlage/japa.git"
},
"keywords": [
"japa",
"testing",
"tests"
],
"author": "virk,japa",
"license": "MIT",
"bugs": {
"url": "https://github.com/thetutlage/japa/issues"
},
"homepage": "https://github.com/thetutlage/japa#readme",
"devDependencies": {
"@adonisjs/mrm-preset": "^5.0.3",
"@adonisjs/require-ts": "^2.0.13",
"@types/node": "^18.14.5",
"commitizen": "^4.3.0",
"cross-env": "^7.0.3",
"cz-conventional-changelog": "^3.3.0",
"del-cli": "^5.0.0",
"eslint": "^8.35.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-adonis": "^2.1.1",
"eslint-plugin-prettier": "^4.2.1",
"husky": "^8.0.3",
"japa": "^4.0.0",
"mrm": "^4.1.13",
"np": "^7.6.3",
"nyc": "^15.1.0",
"prettier": "^2.8.4",
"typescript": "^4.9.5"
},
"mrmConfig": {
"core": false,
"license": "MIT",
"services": [
"github-actions"
],
"minNodeVersion": "16.13.1",
"probotApps": [
"stale",
"lock"
],
"runGhActionsOnWindows": true
},
"eslintConfig": {
"extends": [
"plugin:adonis/typescriptPackage",
"prettier"
],
"plugins": [
"prettier"
],
"rules": {
"prettier/prettier": [
"error",
{
"endOfLine": "auto"
}
]
}
},
"eslintIgnore": [
"build"
],
"prettier": {
"trailingComma": "es5",
"semi": false,
"singleQuote": true,
"useTabs": false,
"quoteProps": "consistent",
"bracketSpacing": true,
"arrowParens": "always",
"printWidth": 100
},
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
},
"np": {
"contents": ".",
"anyBranch": false
},
"dependencies": {
"@poppinss/hooks": "^6.0.2-0",
"async-retry": "^1.3.3",
"emittery": "^0.13.1",
"macroable": "^7.0.2",
"time-span": "^4.0.0"
},
"nyc": {
"exclude": [
"test",
"test-helpers"
]
},
"publishConfig": {
"access": "public",
"tag": "latest"
}
}
+9
View File
@@ -0,0 +1,9 @@
# The MIT License
Copyright 2022 Harminder Virk, contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+145
View File
@@ -0,0 +1,145 @@
# @japa/errors-printer
> Print errors produced by the Japa tests runner
[![github-actions-image]][github-actions-url] [![npm-image]][npm-url] [![license-image]][license-url] [![typescript-image]][typescript-url]
## Installation
Install the package from the npm registry as follows:
```sh
npm i @japa/errors-printer
# yarn
yarn add @japa/errors-printer
```
## Usage
You can print errors produced by japa test runner as follows.
```ts
import { ErrorsPrinter } from '@japa/errors-printer'
const printer = new ErrorsPrinter()
const error = new Error('boom')
await printer.printError(error)
```
Most of the times, you will find yourself printing errors using the Japa test summary. Here is how you can go about doing it.
```ts
import { ErrorsPrinter } from '@japa/errors-printer'
const printer = new ErrorsPrinter()
// assuming you have the runner instance
const summary = runner.getSummary()
/**
* Printing all the errors inside the failure tree
*/
for (let suite in summary.failureTree) {
await printer.printErrors(suite.name, suite.errors)
for (let groupOrTest in suite.children) {
if (groupOrTest.type === 'test') {
await printer.printErrors(groupOrTest.title, groupOrTest.errors)
} else {
await printer.printErrors(groupOrTest.title, groupOrTest.errors)
for (let group in groupOrTest.children) {
await printer.printErrors(group.title, group.errors)
}
}
}
}
```
## API
Following are the available methods.
### printError()
Accepts error as the only argument. If the error is an assertion error, then the diff will be displayed. Otherwise, the error stack is printed.
**Assertion diff**
```ts
import { Assert } from '@japa/assert'
import { ErrorsPrinter } from '@japa/errors-printer'
const printer = new ErrorsPrinter()
try {
new Assert().deepEqual({ id: 1 }, { id: 2 })
} catch (error) {
await printer.printError(error)
}
```
![](assets/assertion-error.png)
**Jest error**
```ts
import expect from 'expect'
import { ErrorsPrinter } from '@japa/errors-printer'
const printer = new ErrorsPrinter()
try {
expect({ bar: 'baz' }).toEqual(expect.not.objectContaining({ bar: 'baz' }))
} catch (error) {
await printer.printError(error)
}
```
![](assets/jest-error.png)
**Error stack**
```ts
import { ErrorsPrinter } from '@japa/errors-printer'
const printer = new ErrorsPrinter()
await printer.printError(new Error('boom'))
```
![](assets/error-stack.png)
### printErrors
Print an array of errors produced by the Japa test runner summary. The method accepts the following arguments.
- `label`: The error label to print. Usually, it will be test title or the group title.
- `errors`: An array of errors in the following format.
```ts
{
phase: string,
error: Error
}
```
```ts
await printer.printErrors('test 1', [
{
phase: 'test',
error: new Error('test failed')
},
{
phase: 'teardown',
error: new Error('teardown failed')
}
])
```
[github-actions-image]: https://img.shields.io/github/actions/workflow/status/japa/errors-printer/test.yml?style=for-the-badge
[github-actions-url]: https://github.com/japa/errors-printer/actions/workflows/test.yml "github-actions"
[npm-image]: https://img.shields.io/npm/v/@japa/errors-printer.svg?style=for-the-badge&logo=npm
[npm-url]: https://npmjs.org/package/@japa/errors-printer "npm"
[license-image]: https://img.shields.io/npm/l/@japa/errors-printer?color=blueviolet&style=for-the-badge
[license-url]: LICENSE.md "license"
[typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript
[typescript-url]: "typescript"
+1
View File
@@ -0,0 +1 @@
export { ErrorsPrinter } from './src/printer';
+13
View File
@@ -0,0 +1,13 @@
"use strict";
/*
* @japa/errors-printer
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ErrorsPrinter = void 0;
var printer_1 = require("./src/printer");
Object.defineProperty(exports, "ErrorsPrinter", { enumerable: true, get: function () { return printer_1.ErrorsPrinter; } });
+42
View File
@@ -0,0 +1,42 @@
/**
* Print test runner errors
*/
export declare class ErrorsPrinter {
private stackLinesCount;
private framesMaxLimit;
constructor(options?: {
stackLinesCount?: number;
framesMaxLimit?: number;
});
/**
* Get Youch's JSON report of the given error
*/
private getYouchJson;
/**
* Returns human readable message for error phase
*/
private getPhaseTitle;
/**
* Displays the error stack for a given error
*/
private displayErrorStack;
/**
* Display chai assertion error
*/
private displayAssertionError;
/**
* Display jest assertion error
*/
private displayJestError;
/**
* Pretty print the error to the console
*/
printError(error: any): Promise<void>;
/**
* Print summary errors
*/
printErrors(label: string, errors: {
phase: string;
error: any;
}[]): Promise<void>;
}
+152
View File
@@ -0,0 +1,152 @@
"use strict";
/*
* @japa/errors-printer
*
* (c) Japa.dev
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ErrorsPrinter = void 0;
const os_1 = require("os");
const youch_1 = __importDefault(require("youch"));
const youch_terminal_1 = __importDefault(require("youch-terminal"));
const jest_diff_1 = require("jest-diff");
const cliui_1 = require("@poppinss/cliui");
/**
* Print test runner errors
*/
class ErrorsPrinter {
constructor(options) {
this.stackLinesCount = options?.stackLinesCount || 5;
this.framesMaxLimit = options?.framesMaxLimit || 3;
}
/**
* Get Youch's JSON report of the given error
*/
async getYouchJson(error) {
const youch = new youch_1.default(error, {}, {
postLines: this.stackLinesCount,
preLines: this.stackLinesCount,
});
return youch.toJSON();
}
/**
* Returns human readable message for error phase
*/
getPhaseTitle(phase) {
switch (phase) {
case 'setup':
return 'Setup hook';
case 'setup:cleanup':
return 'Setup hook cleanup function';
case 'teardown':
return 'Teardown hook';
case 'teardown:cleanup':
return 'Teardown hook cleanup function';
}
}
/**
* Displays the error stack for a given error
*/
async displayErrorStack(error) {
const jsonResponse = await this.getYouchJson(error);
console.log((0, youch_terminal_1.default)(jsonResponse, {
displayShortPath: true,
framesMaxLimit: this.framesMaxLimit,
displayMainFrameOnly: false,
}));
}
/**
* Display chai assertion error
*/
async displayAssertionError(error) {
/**
* Display diff
*/
console.log();
console.log(` Assertion Error: ${error.message}`);
console.log();
if (error.showDiff) {
const { actual, expected } = error;
const diff = (0, jest_diff_1.diff)(expected, actual, {
expand: true,
includeChangeCounts: true,
});
console.log(diff);
}
/**
* Display error stack with the main frame only
*/
const jsonResponse = await this.getYouchJson(error);
console.log((0, youch_terminal_1.default)(jsonResponse, {
hideErrorTitle: true,
hideMessage: true,
displayShortPath: true,
displayMainFrameOnly: true,
}));
}
/**
* Display jest assertion error
*/
async displayJestError(error) {
/**
* Display diff
*/
console.log();
console.log(` Assertion Error:${error.message
.split(os_1.EOL)
.map((line) => ` ${line}`)
.join(os_1.EOL)}`);
console.log();
/**
* Display error stack with the main frame only
*/
const jsonResponse = await this.getYouchJson(error);
console.log((0, youch_terminal_1.default)(jsonResponse, {
hideErrorTitle: true,
hideMessage: true,
displayShortPath: true,
displayMainFrameOnly: true,
}));
}
/**
* Pretty print the error to the console
*/
async printError(error) {
/**
* Values are not object objects are printed as it is.
*/
if (error === null || Array.isArray(error) || typeof error !== 'object') {
console.log(`Error: ${error}`);
return;
}
if ('actual' in error && 'expected' in error) {
await this.displayAssertionError(error);
return;
}
if ('matcherResult' in error) {
await this.displayJestError(error);
return;
}
await this.displayErrorStack(error);
}
/**
* Print summary errors
*/
async printErrors(label, errors) {
for (let { phase, error } of errors) {
console.log(cliui_1.logger.colors.red(`${cliui_1.icons.cross} ${label}`));
if (phase !== 'test') {
console.log(` ${cliui_1.logger.colors.red(`(${this.getPhaseTitle(phase)})`)}`);
}
await this.printError(error);
console.log();
}
}
}
exports.ErrorsPrinter = ErrorsPrinter;
+128
View File
@@ -0,0 +1,128 @@
{
"name": "@japa/errors-printer",
"version": "2.1.0",
"description": "Reusable package to pretty print test runner summary errors",
"main": "build/index.js",
"files": [
"build/src",
"build/index.d.ts",
"build/index.js"
],
"exports": {
".": "./build/index.js"
},
"scripts": {
"mrm": "mrm --preset=@adonisjs/mrm-preset",
"pretest": "npm run lint",
"test": "node .bin/test.js",
"clean": "del-cli build",
"compile": "npm run lint && npm run clean && tsc",
"build": "npm run compile",
"prepublishOnly": "npm run build",
"lint": "eslint . --ext=.ts",
"format": "prettier --write .",
"commit": "git-cz",
"release": "np --message=\"chore(release): %s\"",
"version": "npm run build",
"sync-labels": "github-label-sync --labels ./node_modules/@adonisjs/mrm-preset/gh-labels.json japa/errors-printer"
},
"keywords": [
"japa",
"test",
"printer"
],
"author": "virk,japa",
"license": "MIT",
"devDependencies": {
"@adonisjs/mrm-preset": "^5.0.3",
"@adonisjs/require-ts": "^2.0.13",
"@japa/assert": "^1.3.6",
"@types/node": "^18.11.19",
"commitizen": "^4.3.0",
"cz-conventional-changelog": "^3.3.0",
"del-cli": "^5.0.0",
"eslint": "^8.33.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-adonis": "^3.0.3",
"eslint-plugin-prettier": "^4.2.1",
"expect": "^29.4.1",
"github-label-sync": "^2.2.0",
"husky": "^8.0.3",
"japa": "^4.0.0",
"mrm": "^4.1.13",
"np": "^7.6.3",
"prettier": "^2.8.3",
"typescript": "^4.9.5"
},
"mrmConfig": {
"core": false,
"license": "MIT",
"services": [
"github-actions"
],
"minNodeVersion": "16.13.1",
"probotApps": [
"stale",
"lock"
],
"runGhActionsOnWindows": false
},
"eslintConfig": {
"extends": [
"plugin:adonis/typescriptPackage",
"prettier"
],
"plugins": [
"prettier"
],
"rules": {
"unicorn/prefer-node-protocol": "off",
"prettier/prettier": [
"error",
{
"endOfLine": "auto"
}
]
}
},
"eslintIgnore": [
"build"
],
"prettier": {
"trailingComma": "es5",
"semi": false,
"singleQuote": true,
"useTabs": false,
"quoteProps": "consistent",
"bracketSpacing": true,
"arrowParens": "always",
"printWidth": 100
},
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
},
"np": {
"contents": ".",
"anyBranch": false
},
"repository": {
"type": "git",
"url": "git+https://github.com/japa/errors-printer.git"
},
"bugs": {
"url": "https://github.com/japa/errors-printer/issues"
},
"homepage": "https://github.com/japa/errors-printer#readme",
"dependencies": {
"@poppinss/cliui": "^3.0.5",
"jest-diff": "^29.4.1",
"youch": "^3.2.3",
"youch-terminal": "^2.2.0"
},
"publishConfig": {
"access": "public",
"tag": "latest"
}
}
+9
View File
@@ -0,0 +1,9 @@
# The MIT License
Copyright 2022 Harminder Virk, contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+38
View File
@@ -0,0 +1,38 @@
# AdonisJS Preset
> Bundled with opinionated plugins and reporters.
[![github-actions-image]][github-actions-url] [![npm-image]][npm-url] [![license-image]][license-url] [![typescript-image]][typescript-url]
Instead of installing individual plugins and reporters, this preset bundles them within a single package.
## Installation
Install the package from the npm registry as follows:
```sh
npm i @japa/preset-adonis
```
## Usage
Use the plugins as follows.
```ts
import { assert, specReporter, runFailedTests } from '@japa/preset-adonis'
configure({
plugins: [assert(), runFailedTests()],
reporters: [specReporter()]
})
```
[github-actions-image]: https://github.com/japa/preset-adonis/actions/workflows/test.yml
[github-actions-url]: https://img.shields.io/github/workflow/status/japa/preset-adonis/test?style=for-the-badge "github-actions"
[npm-image]: https://img.shields.io/npm/v/@japa/preset-adonis.svg?style=for-the-badge&logo=npm
[npm-url]: https://npmjs.org/package/@japa/preset-adonis "npm"
[license-image]: https://img.shields.io/npm/l/@japa/preset-adonis?color=blueviolet&style=for-the-badge
[license-url]: LICENSE.md "license"
[typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript
[typescript-url]: "typescript"
@@ -0,0 +1,13 @@
declare module '@ioc:Adonis/Core/Application' {
import type { Assert } from '@japa/assert';
import type { Test, TestContext } from '@japa/runner';
import type { ApiRequest, ApiResponse, ApiClient } from '@japa/api-client';
interface ContainerBindings {
'Japa/Preset/Test': typeof Test;
'Japa/Preset/TestContext': typeof TestContext;
'Japa/Preset/Assert': typeof Assert;
'Japa/Preset/ApiRequest': typeof ApiRequest;
'Japa/Preset/ApiClient': typeof ApiClient;
'Japa/Preset/ApiResponse': typeof ApiResponse;
}
}
@@ -0,0 +1,8 @@
/*
* @japa/preset-adonis
*
* (c) Japa.dev
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
@@ -0,0 +1,2 @@
/// <reference path="container.d.ts" />
/// <reference path="tests.d.ts" />
+10
View File
@@ -0,0 +1,10 @@
/*
* @japa/preset-adonis
*
* (c) Japa.dev
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/// <reference path="./container.ts" />
/// <reference path="./tests.ts" />
+13
View File
@@ -0,0 +1,13 @@
/// <reference types="@adonisjs/http-server/build/adonis-typings" />
import '@japa/runner';
import { MakeUrlOptions } from '@ioc:Adonis/Core/Route';
declare module '@japa/runner' {
interface TestContext {
route(routeIdentifier: string, params?: Record<string, any> | any[], options?: MakeUrlOptions): string;
}
}
declare module '@japa/api-client' {
interface ApiResponse {
assertRedirectsToRoute(routeIdentifier: string, params?: Record<string, any> | any[], options?: MakeUrlOptions): string;
}
}
+11
View File
@@ -0,0 +1,11 @@
"use strict";
/*
* @japa/preset-adonis
*
* (c) Japa.dev
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
Object.defineProperty(exports, "__esModule", { value: true });
require("@japa/runner");
+4
View File
@@ -0,0 +1,4 @@
export { assert } from '@japa/assert';
export { apiClient } from '@japa/api-client';
export { specReporter } from '@japa/spec-reporter';
export { runFailedTests } from '@japa/run-failed-tests';
+19
View File
@@ -0,0 +1,19 @@
"use strict";
/*
* @japa/preset-adonis
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.runFailedTests = exports.specReporter = exports.apiClient = exports.assert = void 0;
var assert_1 = require("@japa/assert");
Object.defineProperty(exports, "assert", { enumerable: true, get: function () { return assert_1.assert; } });
var api_client_1 = require("@japa/api-client");
Object.defineProperty(exports, "apiClient", { enumerable: true, get: function () { return api_client_1.apiClient; } });
var spec_reporter_1 = require("@japa/spec-reporter");
Object.defineProperty(exports, "specReporter", { enumerable: true, get: function () { return spec_reporter_1.specReporter; } });
var run_failed_tests_1 = require("@japa/run-failed-tests");
Object.defineProperty(exports, "runFailedTests", { enumerable: true, get: function () { return run_failed_tests_1.runFailedTests; } });
@@ -0,0 +1,11 @@
/// <reference types="@adonisjs/application/build/adonis-typings" />
import type { ApplicationContract } from '@ioc:Adonis/Core/Application';
/**
* AdonisJS provider for registering japa class to the container
*/
export default class TestsProvider {
protected app: ApplicationContract;
constructor(app: ApplicationContract);
register(): void;
boot(): void;
}
@@ -0,0 +1,55 @@
"use strict";
/*
* @japa/preset-adonis
*
* (c) Japa.dev
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
Object.defineProperty(exports, "__esModule", { value: true });
/**
* AdonisJS provider for registering japa class to the container
*/
class TestsProvider {
constructor(app) {
this.app = app;
}
register() {
this.app.container.bind('Japa/Preset/Test', () => {
const { Test } = require('@japa/runner');
return Test;
});
this.app.container.bind('Japa/Preset/TestContext', () => {
const { TestContext } = require('@japa/runner');
return TestContext;
});
this.app.container.bind('Japa/Preset/Assert', () => {
const { Assert } = require('@japa/assert');
return Assert;
});
this.app.container.bind('Japa/Preset/ApiClient', () => {
const { ApiClient } = require('@japa/api-client');
return ApiClient;
});
this.app.container.bind('Japa/Preset/ApiRequest', () => {
const { ApiRequest } = require('@japa/api-client');
return ApiRequest;
});
this.app.container.bind('Japa/Preset/ApiResponse', () => {
const { ApiResponse } = require('@japa/api-client');
return ApiResponse;
});
}
boot() {
this.app.container.withBindings(['Adonis/Core/Route', 'Japa/Preset/TestContext', 'Japa/Preset/ApiResponse'], (Route, TestContext, Response) => {
TestContext.macro('route', function (routeIdentifier, params, options) {
return Route.makeUrl(routeIdentifier, params, options);
});
Response.macro('assertRedirectsToRoute', function (routeIdentifier, params, options) {
return this.assertRedirectsTo(Route.makeUrl(routeIdentifier, params, options));
});
});
}
}
exports.default = TestsProvider;
+149
View File
@@ -0,0 +1,149 @@
{
"name": "@japa/preset-adonis",
"version": "1.2.0",
"description": "Preset for AdonisJS",
"main": "build/index.js",
"files": [
"build/index.d.ts",
"build/index.js",
"build/providers",
"build/adonis-typings"
],
"exports": {
".": {
"types": "./build/index.d.ts",
"require": "./build/index.js"
},
"./TestsProvider": {
"types": [
"./build/adonis-typings/index.d.ts"
],
"require": "./build/providers/TestsProvider/index.js"
},
"./*": {
"types": [
"./build/adonis-typings/index.d.ts"
],
"require": "./*"
}
},
"scripts": {
"mrm": "mrm --preset=@adonisjs/mrm-preset",
"pretest": "npm run lint",
"test": "",
"clean": "del-cli build",
"compile": "npm run lint && npm run clean && tsc",
"build": "npm run compile",
"prepublishOnly": "npm run build",
"lint": "eslint . --ext=.ts",
"format": "prettier --write .",
"commit": "git-cz",
"release": "np --message=\"chore(release): %s\"",
"version": "npm run build",
"sync-labels": "github-label-sync --labels ./node_modules/@adonisjs/mrm-preset/gh-labels.json null"
},
"keywords": [
"adonisjs",
"adonis-preset",
"preset"
],
"author": "virk,japa",
"license": "MIT",
"devDependencies": {
"@adonisjs/core": "^5.8.6",
"@adonisjs/mrm-preset": "^5.0.3",
"@adonisjs/require-ts": "^2.0.12",
"@japa/runner": "^2.1.3",
"@types/node": "^18.7.16",
"commitizen": "^4.2.5",
"cz-conventional-changelog": "^3.3.0",
"del-cli": "^5.0.0",
"eslint": "^8.23.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-adonis": "^2.1.0",
"eslint-plugin-prettier": "^4.2.1",
"github-label-sync": "^2.2.0",
"husky": "^8.0.1",
"mrm": "^4.1.0",
"np": "^7.6.2",
"prettier": "^2.7.1",
"typescript": "^4.8.2"
},
"mrmConfig": {
"core": false,
"license": "MIT",
"services": [
"github-actions"
],
"minNodeVersion": "16.13.1",
"probotApps": [
"stale",
"lock"
],
"runGhActionsOnWindows": false
},
"eslintConfig": {
"extends": [
"plugin:adonis/typescriptPackage",
"prettier"
],
"plugins": [
"prettier"
],
"rules": {
"prettier/prettier": [
"error",
{
"endOfLine": "auto"
}
]
}
},
"eslintIgnore": [
"build"
],
"prettier": {
"trailingComma": "es5",
"semi": false,
"singleQuote": true,
"useTabs": false,
"quoteProps": "consistent",
"bracketSpacing": true,
"arrowParens": "always",
"printWidth": 100
},
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
},
"np": {
"contents": ".",
"anyBranch": false
},
"publishConfig": {
"access": "public",
"tag": "latest"
},
"dependencies": {
"@japa/api-client": "^1.4.2",
"@japa/assert": "^1.3.6",
"@japa/run-failed-tests": "^1.1.0",
"@japa/spec-reporter": "^1.3.0"
},
"peerDependencies": {
"@adonisjs/core": "^5.0.0",
"@japa/runner": "^2.0.0"
},
"repository": {
"type": "git",
"url": "git+https://github.com/japa/preset-adonis.git"
},
"bugs": {
"url": "https://github.com/japa/preset-adonis/issues"
},
"homepage": "https://github.com/japa/preset-adonis#readme",
"adonisjs": {
"types": "@japa/preset-adonis/build/adonis-typings/index"
}
}
+9
View File
@@ -0,0 +1,9 @@
# The MIT License
Copyright 2022 Harminder Virk, contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+36
View File
@@ -0,0 +1,36 @@
# @japa/run-failed-tests
> Japa plugin to run only failed tests
[![github-actions-image]][github-actions-url] [![npm-image]][npm-url] [![license-image]][license-url] [![typescript-image]][typescript-url]
The `@japa/run-failed-tests` plugin runs only the failed tests on subsequent runs.
Here's how it works under the hood.
- You ran the tests suite, and a couple of tests failed.
- On the next run, only the failed test will run.
- If all tests are green, the next run will execute all the tests.
```sh
npm i @japa/run-failed-tests
```
```ts
import { runFailedTests } from '@japa/run-failed-tests'
configure({
plugins: [runFailedTests()]
})
```
[github-actions-image]: https://img.shields.io/github/actions/workflow/status/japa/run-failed-tests/test.yml?style=for-the-badge
[github-actions-url]: https://github.com/japa/run-failed-tests/actions/workflows/test.yml "github-actions"
[npm-image]: https://img.shields.io/npm/v/@japa/run-failed-tests.svg?style=for-the-badge&logo=npm
[npm-url]: https://npmjs.org/package/@japa/run-failed-tests "npm"
[license-image]: https://img.shields.io/npm/l/@japa/run-failed-tests?color=blueviolet&style=for-the-badge
[license-url]: LICENSE.md "license"
[typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript
[typescript-url]: "typescript"
+8
View File
@@ -0,0 +1,8 @@
import { PluginFn } from '@japa/runner';
/**
* Plugin function to run failed tests only. Only for "@japa/runner"
*/
export declare function runFailedTests(options?: {
summaryFilePath?: string;
ignoreFilesFilter?: boolean;
}): PluginFn;
+89
View File
@@ -0,0 +1,89 @@
"use strict";
/*
* @japa/run-failed-tests
*
* (c) Japa.dev
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.runFailedTests = void 0;
const path_1 = require("path");
const find_cache_dir_1 = __importDefault(require("find-cache-dir"));
const cliui_1 = require("@poppinss/cliui");
const fs_extra_1 = require("fs-extra");
/**
* Returns the summary from the summary file
*/
async function getSummary(summaryFilePath) {
try {
return await (0, fs_extra_1.readJson)(summaryFilePath);
}
catch (error) {
if (error.code === 'ENOENT') {
return {};
}
throw new Error(`"@japa/run-failed-tests": ${error.message}`);
}
}
/**
* Perists summary with failing tests to the disk
*/
async function writeSummary(summaryFilePath, contents) {
await (0, fs_extra_1.outputJson)(summaryFilePath, contents);
}
/**
* Plugin function to run failed tests only. Only for "@japa/runner"
*/
function runFailedTests(options) {
options = Object.assign({}, options);
return async function (config) {
/**
* Do not overwrite existing filters
*/
if (config.filters.tests?.length) {
return;
}
/**
* Use the default path when no explicit path is provided
*/
if (!options.summaryFilePath) {
options.summaryFilePath = (0, path_1.join)((0, find_cache_dir_1.default)({ name: '@japa/run-failed-tests' }, 'summary.json'));
}
/**
* Get summary and look for failed tests
*/
const summary = await getSummary(options.summaryFilePath);
/**
* Apply the filter when there are one or more failed tests
*/
if (summary.tests?.length) {
config.filters.tests = summary.tests;
(0, cliui_1.sticker)()
.heading('"@japa/run-failed-tests"')
.add('')
.add(`${summary.tests.length} failed test(s) found`)
.add('Applying filter to run only failed tests')
.render();
/**
* Empty out files filter when "ignoreFilesFilter" is set to true
*/
if (options.ignoreFilesFilter) {
config.filters.files = [];
}
}
/**
* Persist file during teardown
*/
config.teardown.push(async (runner) => {
await writeSummary(options.summaryFilePath, {
tests: runner.getSummary().failedTestsTitles,
});
});
};
}
exports.runFailedTests = runFailedTests;
@@ -0,0 +1,15 @@
(The MIT License)
Copyright (c) 2011-2017 JP Richardson
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,292 @@
Node.js: fs-extra
=================
`fs-extra` adds file system methods that aren't included in the native `fs` module and adds promise support to the `fs` methods. It also uses [`graceful-fs`](https://github.com/isaacs/node-graceful-fs) to prevent `EMFILE` errors. It should be a drop in replacement for `fs`.
[![npm Package](https://img.shields.io/npm/v/fs-extra.svg)](https://www.npmjs.org/package/fs-extra)
[![License](https://img.shields.io/npm/l/fs-extra.svg)](https://github.com/jprichardson/node-fs-extra/blob/master/LICENSE)
[![build status](https://img.shields.io/github/actions/workflow/status/jprichardson/node-fs-extra/ci.yml?branch=master)](https://github.com/jprichardson/node-fs-extra/actions/workflows/ci.yml?query=branch%3Amaster)
[![downloads per month](http://img.shields.io/npm/dm/fs-extra.svg)](https://www.npmjs.org/package/fs-extra)
[![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com)
Why?
----
I got tired of including `mkdirp`, `rimraf`, and `ncp` in most of my projects.
Installation
------------
npm install fs-extra
Usage
-----
### CommonJS
`fs-extra` is a drop in replacement for native `fs`. All methods in `fs` are attached to `fs-extra`. All `fs` methods return promises if the callback isn't passed.
You don't ever need to include the original `fs` module again:
```js
const fs = require('fs') // this is no longer necessary
```
you can now do this:
```js
const fs = require('fs-extra')
```
or if you prefer to make it clear that you're using `fs-extra` and not `fs`, you may want
to name your `fs` variable `fse` like so:
```js
const fse = require('fs-extra')
```
you can also keep both, but it's redundant:
```js
const fs = require('fs')
const fse = require('fs-extra')
```
### ESM
There is also an `fs-extra/esm` import, that supports both default and named exports. However, note that `fs` methods are not included in `fs-extra/esm`; you still need to import `fs` and/or `fs/promises` seperately:
```js
import { readFileSync } from 'fs'
import { readFile } from 'fs/promises'
import { outputFile, outputFileSync } from 'fs-extra/esm'
```
Default exports are supported:
```js
import fs from 'fs'
import fse from 'fs-extra/esm'
// fse.readFileSync is not a function; must use fs.readFileSync
```
but you probably want to just use regular `fs-extra` instead of `fs-extra/esm` for default exports:
```js
import fs from 'fs-extra'
// both fs and fs-extra methods are defined
```
Sync vs Async vs Async/Await
-------------
Most methods are async by default. All async methods will return a promise if the callback isn't passed.
Sync methods on the other hand will throw if an error occurs.
Also Async/Await will throw an error if one occurs.
Example:
```js
const fs = require('fs-extra')
// Async with promises:
fs.copy('/tmp/myfile', '/tmp/mynewfile')
.then(() => console.log('success!'))
.catch(err => console.error(err))
// Async with callbacks:
fs.copy('/tmp/myfile', '/tmp/mynewfile', err => {
if (err) return console.error(err)
console.log('success!')
})
// Sync:
try {
fs.copySync('/tmp/myfile', '/tmp/mynewfile')
console.log('success!')
} catch (err) {
console.error(err)
}
// Async/Await:
async function copyFiles () {
try {
await fs.copy('/tmp/myfile', '/tmp/mynewfile')
console.log('success!')
} catch (err) {
console.error(err)
}
}
copyFiles()
```
Methods
-------
### Async
- [copy](docs/copy.md)
- [emptyDir](docs/emptyDir.md)
- [ensureFile](docs/ensureFile.md)
- [ensureDir](docs/ensureDir.md)
- [ensureLink](docs/ensureLink.md)
- [ensureSymlink](docs/ensureSymlink.md)
- [mkdirp](docs/ensureDir.md)
- [mkdirs](docs/ensureDir.md)
- [move](docs/move.md)
- [outputFile](docs/outputFile.md)
- [outputJson](docs/outputJson.md)
- [pathExists](docs/pathExists.md)
- [readJson](docs/readJson.md)
- [remove](docs/remove.md)
- [writeJson](docs/writeJson.md)
### Sync
- [copySync](docs/copy-sync.md)
- [emptyDirSync](docs/emptyDir-sync.md)
- [ensureFileSync](docs/ensureFile-sync.md)
- [ensureDirSync](docs/ensureDir-sync.md)
- [ensureLinkSync](docs/ensureLink-sync.md)
- [ensureSymlinkSync](docs/ensureSymlink-sync.md)
- [mkdirpSync](docs/ensureDir-sync.md)
- [mkdirsSync](docs/ensureDir-sync.md)
- [moveSync](docs/move-sync.md)
- [outputFileSync](docs/outputFile-sync.md)
- [outputJsonSync](docs/outputJson-sync.md)
- [pathExistsSync](docs/pathExists-sync.md)
- [readJsonSync](docs/readJson-sync.md)
- [removeSync](docs/remove-sync.md)
- [writeJsonSync](docs/writeJson-sync.md)
**NOTE:** You can still use the native Node.js methods. They are promisified and copied over to `fs-extra`. See [notes on `fs.read()`, `fs.write()`, & `fs.writev()`](docs/fs-read-write-writev.md)
### What happened to `walk()` and `walkSync()`?
They were removed from `fs-extra` in v2.0.0. If you need the functionality, `walk` and `walkSync` are available as separate packages, [`klaw`](https://github.com/jprichardson/node-klaw) and [`klaw-sync`](https://github.com/manidlou/node-klaw-sync).
Third Party
-----------
### CLI
[fse-cli](https://www.npmjs.com/package/@atao60/fse-cli) allows you to run `fs-extra` from a console or from [npm](https://www.npmjs.com) scripts.
### TypeScript
If you like TypeScript, you can use `fs-extra` with it: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/fs-extra
### File / Directory Watching
If you want to watch for changes to files or directories, then you should use [chokidar](https://github.com/paulmillr/chokidar).
### Obtain Filesystem (Devices, Partitions) Information
[fs-filesystem](https://github.com/arthurintelligence/node-fs-filesystem) allows you to read the state of the filesystem of the host on which it is run. It returns information about both the devices and the partitions (volumes) of the system.
### Misc.
- [fs-extra-debug](https://github.com/jdxcode/fs-extra-debug) - Send your fs-extra calls to [debug](https://npmjs.org/package/debug).
- [mfs](https://github.com/cadorn/mfs) - Monitor your fs-extra calls.
Hacking on fs-extra
-------------------
Wanna hack on `fs-extra`? Great! Your help is needed! [fs-extra is one of the most depended upon Node.js packages](http://nodei.co/npm/fs-extra.png?downloads=true&downloadRank=true&stars=true). This project
uses [JavaScript Standard Style](https://github.com/feross/standard) - if the name or style choices bother you,
you're gonna have to get over it :) If `standard` is good enough for `npm`, it's good enough for `fs-extra`.
[![js-standard-style](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard)
What's needed?
- First, take a look at existing issues. Those are probably going to be where the priority lies.
- More tests for edge cases. Specifically on different platforms. There can never be enough tests.
- Improve test coverage.
Note: If you make any big changes, **you should definitely file an issue for discussion first.**
### Running the Test Suite
fs-extra contains hundreds of tests.
- `npm run lint`: runs the linter ([standard](http://standardjs.com/))
- `npm run unit`: runs the unit tests
- `npm run unit-esm`: runs tests for `fs-extra/esm` exports
- `npm test`: runs the linter and all tests
When running unit tests, set the environment variable `CROSS_DEVICE_PATH` to the absolute path of an empty directory on another device (like a thumb drive) to enable cross-device move tests.
### Windows
If you run the tests on the Windows and receive a lot of symbolic link `EPERM` permission errors, it's
because on Windows you need elevated privilege to create symbolic links. You can add this to your Windows's
account by following the instructions here: http://superuser.com/questions/104845/permission-to-make-symbolic-links-in-windows-7
However, I didn't have much luck doing this.
Since I develop on Mac OS X, I use VMWare Fusion for Windows testing. I create a shared folder that I map to a drive on Windows.
I open the `Node.js command prompt` and run as `Administrator`. I then map the network drive running the following command:
net use z: "\\vmware-host\Shared Folders"
I can then navigate to my `fs-extra` directory and run the tests.
Naming
------
I put a lot of thought into the naming of these functions. Inspired by @coolaj86's request. So he deserves much of the credit for raising the issue. See discussion(s) here:
* https://github.com/jprichardson/node-fs-extra/issues/2
* https://github.com/flatiron/utile/issues/11
* https://github.com/ryanmcgrath/wrench-js/issues/29
* https://github.com/substack/node-mkdirp/issues/17
First, I believe that in as many cases as possible, the [Node.js naming schemes](http://nodejs.org/api/fs.html) should be chosen. However, there are problems with the Node.js own naming schemes.
For example, `fs.readFile()` and `fs.readdir()`: the **F** is capitalized in *File* and the **d** is not capitalized in *dir*. Perhaps a bit pedantic, but they should still be consistent. Also, Node.js has chosen a lot of POSIX naming schemes, which I believe is great. See: `fs.mkdir()`, `fs.rmdir()`, `fs.chown()`, etc.
We have a dilemma though. How do you consistently name methods that perform the following POSIX commands: `cp`, `cp -r`, `mkdir -p`, and `rm -rf`?
My perspective: when in doubt, err on the side of simplicity. A directory is just a hierarchical grouping of directories and files. Consider that for a moment. So when you want to copy it or remove it, in most cases you'll want to copy or remove all of its contents. When you want to create a directory, if the directory that it's suppose to be contained in does not exist, then in most cases you'll want to create that too.
So, if you want to remove a file or a directory regardless of whether it has contents, just call `fs.remove(path)`. If you want to copy a file or a directory whether it has contents, just call `fs.copy(source, destination)`. If you want to create a directory regardless of whether its parent directories exist, just call `fs.mkdirs(path)` or `fs.mkdirp(path)`.
Credit
------
`fs-extra` wouldn't be possible without using the modules from the following authors:
- [Isaac Shlueter](https://github.com/isaacs)
- [Charlie McConnel](https://github.com/avianflu)
- [James Halliday](https://github.com/substack)
- [Andrew Kelley](https://github.com/andrewrk)
License
-------
Licensed under MIT
Copyright (c) 2011-2017 [JP Richardson](https://github.com/jprichardson)
[1]: http://nodejs.org/docs/latest/api/fs.html
[jsonfile]: https://github.com/jprichardson/node-jsonfile
@@ -0,0 +1,161 @@
'use strict'
const fs = require('graceful-fs')
const path = require('path')
const mkdirsSync = require('../mkdirs').mkdirsSync
const utimesMillisSync = require('../util/utimes').utimesMillisSync
const stat = require('../util/stat')
function copySync (src, dest, opts) {
if (typeof opts === 'function') {
opts = { filter: opts }
}
opts = opts || {}
opts.clobber = 'clobber' in opts ? !!opts.clobber : true // default to true for now
opts.overwrite = 'overwrite' in opts ? !!opts.overwrite : opts.clobber // overwrite falls back to clobber
// Warn about using preserveTimestamps on 32-bit node
if (opts.preserveTimestamps && process.arch === 'ia32') {
process.emitWarning(
'Using the preserveTimestamps option in 32-bit node is not recommended;\n\n' +
'\tsee https://github.com/jprichardson/node-fs-extra/issues/269',
'Warning', 'fs-extra-WARN0002'
)
}
const { srcStat, destStat } = stat.checkPathsSync(src, dest, 'copy', opts)
stat.checkParentPathsSync(src, srcStat, dest, 'copy')
if (opts.filter && !opts.filter(src, dest)) return
const destParent = path.dirname(dest)
if (!fs.existsSync(destParent)) mkdirsSync(destParent)
return getStats(destStat, src, dest, opts)
}
function getStats (destStat, src, dest, opts) {
const statSync = opts.dereference ? fs.statSync : fs.lstatSync
const srcStat = statSync(src)
if (srcStat.isDirectory()) return onDir(srcStat, destStat, src, dest, opts)
else if (srcStat.isFile() ||
srcStat.isCharacterDevice() ||
srcStat.isBlockDevice()) return onFile(srcStat, destStat, src, dest, opts)
else if (srcStat.isSymbolicLink()) return onLink(destStat, src, dest, opts)
else if (srcStat.isSocket()) throw new Error(`Cannot copy a socket file: ${src}`)
else if (srcStat.isFIFO()) throw new Error(`Cannot copy a FIFO pipe: ${src}`)
throw new Error(`Unknown file: ${src}`)
}
function onFile (srcStat, destStat, src, dest, opts) {
if (!destStat) return copyFile(srcStat, src, dest, opts)
return mayCopyFile(srcStat, src, dest, opts)
}
function mayCopyFile (srcStat, src, dest, opts) {
if (opts.overwrite) {
fs.unlinkSync(dest)
return copyFile(srcStat, src, dest, opts)
} else if (opts.errorOnExist) {
throw new Error(`'${dest}' already exists`)
}
}
function copyFile (srcStat, src, dest, opts) {
fs.copyFileSync(src, dest)
if (opts.preserveTimestamps) handleTimestamps(srcStat.mode, src, dest)
return setDestMode(dest, srcStat.mode)
}
function handleTimestamps (srcMode, src, dest) {
// Make sure the file is writable before setting the timestamp
// otherwise open fails with EPERM when invoked with 'r+'
// (through utimes call)
if (fileIsNotWritable(srcMode)) makeFileWritable(dest, srcMode)
return setDestTimestamps(src, dest)
}
function fileIsNotWritable (srcMode) {
return (srcMode & 0o200) === 0
}
function makeFileWritable (dest, srcMode) {
return setDestMode(dest, srcMode | 0o200)
}
function setDestMode (dest, srcMode) {
return fs.chmodSync(dest, srcMode)
}
function setDestTimestamps (src, dest) {
// The initial srcStat.atime cannot be trusted
// because it is modified by the read(2) system call
// (See https://nodejs.org/api/fs.html#fs_stat_time_values)
const updatedSrcStat = fs.statSync(src)
return utimesMillisSync(dest, updatedSrcStat.atime, updatedSrcStat.mtime)
}
function onDir (srcStat, destStat, src, dest, opts) {
if (!destStat) return mkDirAndCopy(srcStat.mode, src, dest, opts)
return copyDir(src, dest, opts)
}
function mkDirAndCopy (srcMode, src, dest, opts) {
fs.mkdirSync(dest)
copyDir(src, dest, opts)
return setDestMode(dest, srcMode)
}
function copyDir (src, dest, opts) {
fs.readdirSync(src).forEach(item => copyDirItem(item, src, dest, opts))
}
function copyDirItem (item, src, dest, opts) {
const srcItem = path.join(src, item)
const destItem = path.join(dest, item)
if (opts.filter && !opts.filter(srcItem, destItem)) return
const { destStat } = stat.checkPathsSync(srcItem, destItem, 'copy', opts)
return getStats(destStat, srcItem, destItem, opts)
}
function onLink (destStat, src, dest, opts) {
let resolvedSrc = fs.readlinkSync(src)
if (opts.dereference) {
resolvedSrc = path.resolve(process.cwd(), resolvedSrc)
}
if (!destStat) {
return fs.symlinkSync(resolvedSrc, dest)
} else {
let resolvedDest
try {
resolvedDest = fs.readlinkSync(dest)
} catch (err) {
// dest exists and is a regular file or directory,
// Windows may throw UNKNOWN error. If dest already exists,
// fs throws error anyway, so no need to guard against it here.
if (err.code === 'EINVAL' || err.code === 'UNKNOWN') return fs.symlinkSync(resolvedSrc, dest)
throw err
}
if (opts.dereference) {
resolvedDest = path.resolve(process.cwd(), resolvedDest)
}
if (stat.isSrcSubdir(resolvedSrc, resolvedDest)) {
throw new Error(`Cannot copy '${resolvedSrc}' to a subdirectory of itself, '${resolvedDest}'.`)
}
// prevent copy if src is a subdir of dest since unlinking
// dest in this case would result in removing src contents
// and therefore a broken symlink would be created.
if (stat.isSrcSubdir(resolvedDest, resolvedSrc)) {
throw new Error(`Cannot overwrite '${resolvedDest}' with '${resolvedSrc}'.`)
}
return copyLink(resolvedSrc, dest)
}
}
function copyLink (resolvedSrc, dest) {
fs.unlinkSync(dest)
return fs.symlinkSync(resolvedSrc, dest)
}
module.exports = copySync
@@ -0,0 +1,238 @@
'use strict'
const fs = require('graceful-fs')
const path = require('path')
const mkdirs = require('../mkdirs').mkdirs
const pathExists = require('../path-exists').pathExists
const utimesMillis = require('../util/utimes').utimesMillis
const stat = require('../util/stat')
function copy (src, dest, opts, cb) {
if (typeof opts === 'function' && !cb) {
cb = opts
opts = {}
} else if (typeof opts === 'function') {
opts = { filter: opts }
}
cb = cb || function () {}
opts = opts || {}
opts.clobber = 'clobber' in opts ? !!opts.clobber : true // default to true for now
opts.overwrite = 'overwrite' in opts ? !!opts.overwrite : opts.clobber // overwrite falls back to clobber
// Warn about using preserveTimestamps on 32-bit node
if (opts.preserveTimestamps && process.arch === 'ia32') {
process.emitWarning(
'Using the preserveTimestamps option in 32-bit node is not recommended;\n\n' +
'\tsee https://github.com/jprichardson/node-fs-extra/issues/269',
'Warning', 'fs-extra-WARN0001'
)
}
stat.checkPaths(src, dest, 'copy', opts, (err, stats) => {
if (err) return cb(err)
const { srcStat, destStat } = stats
stat.checkParentPaths(src, srcStat, dest, 'copy', err => {
if (err) return cb(err)
runFilter(src, dest, opts, (err, include) => {
if (err) return cb(err)
if (!include) return cb()
checkParentDir(destStat, src, dest, opts, cb)
})
})
})
}
function checkParentDir (destStat, src, dest, opts, cb) {
const destParent = path.dirname(dest)
pathExists(destParent, (err, dirExists) => {
if (err) return cb(err)
if (dirExists) return getStats(destStat, src, dest, opts, cb)
mkdirs(destParent, err => {
if (err) return cb(err)
return getStats(destStat, src, dest, opts, cb)
})
})
}
function runFilter (src, dest, opts, cb) {
if (!opts.filter) return cb(null, true)
Promise.resolve(opts.filter(src, dest))
.then(include => cb(null, include), error => cb(error))
}
function getStats (destStat, src, dest, opts, cb) {
const stat = opts.dereference ? fs.stat : fs.lstat
stat(src, (err, srcStat) => {
if (err) return cb(err)
if (srcStat.isDirectory()) return onDir(srcStat, destStat, src, dest, opts, cb)
else if (srcStat.isFile() ||
srcStat.isCharacterDevice() ||
srcStat.isBlockDevice()) return onFile(srcStat, destStat, src, dest, opts, cb)
else if (srcStat.isSymbolicLink()) return onLink(destStat, src, dest, opts, cb)
else if (srcStat.isSocket()) return cb(new Error(`Cannot copy a socket file: ${src}`))
else if (srcStat.isFIFO()) return cb(new Error(`Cannot copy a FIFO pipe: ${src}`))
return cb(new Error(`Unknown file: ${src}`))
})
}
function onFile (srcStat, destStat, src, dest, opts, cb) {
if (!destStat) return copyFile(srcStat, src, dest, opts, cb)
return mayCopyFile(srcStat, src, dest, opts, cb)
}
function mayCopyFile (srcStat, src, dest, opts, cb) {
if (opts.overwrite) {
fs.unlink(dest, err => {
if (err) return cb(err)
return copyFile(srcStat, src, dest, opts, cb)
})
} else if (opts.errorOnExist) {
return cb(new Error(`'${dest}' already exists`))
} else return cb()
}
function copyFile (srcStat, src, dest, opts, cb) {
fs.copyFile(src, dest, err => {
if (err) return cb(err)
if (opts.preserveTimestamps) return handleTimestampsAndMode(srcStat.mode, src, dest, cb)
return setDestMode(dest, srcStat.mode, cb)
})
}
function handleTimestampsAndMode (srcMode, src, dest, cb) {
// Make sure the file is writable before setting the timestamp
// otherwise open fails with EPERM when invoked with 'r+'
// (through utimes call)
if (fileIsNotWritable(srcMode)) {
return makeFileWritable(dest, srcMode, err => {
if (err) return cb(err)
return setDestTimestampsAndMode(srcMode, src, dest, cb)
})
}
return setDestTimestampsAndMode(srcMode, src, dest, cb)
}
function fileIsNotWritable (srcMode) {
return (srcMode & 0o200) === 0
}
function makeFileWritable (dest, srcMode, cb) {
return setDestMode(dest, srcMode | 0o200, cb)
}
function setDestTimestampsAndMode (srcMode, src, dest, cb) {
setDestTimestamps(src, dest, err => {
if (err) return cb(err)
return setDestMode(dest, srcMode, cb)
})
}
function setDestMode (dest, srcMode, cb) {
return fs.chmod(dest, srcMode, cb)
}
function setDestTimestamps (src, dest, cb) {
// The initial srcStat.atime cannot be trusted
// because it is modified by the read(2) system call
// (See https://nodejs.org/api/fs.html#fs_stat_time_values)
fs.stat(src, (err, updatedSrcStat) => {
if (err) return cb(err)
return utimesMillis(dest, updatedSrcStat.atime, updatedSrcStat.mtime, cb)
})
}
function onDir (srcStat, destStat, src, dest, opts, cb) {
if (!destStat) return mkDirAndCopy(srcStat.mode, src, dest, opts, cb)
return copyDir(src, dest, opts, cb)
}
function mkDirAndCopy (srcMode, src, dest, opts, cb) {
fs.mkdir(dest, err => {
if (err) return cb(err)
copyDir(src, dest, opts, err => {
if (err) return cb(err)
return setDestMode(dest, srcMode, cb)
})
})
}
function copyDir (src, dest, opts, cb) {
fs.readdir(src, (err, items) => {
if (err) return cb(err)
return copyDirItems(items, src, dest, opts, cb)
})
}
function copyDirItems (items, src, dest, opts, cb) {
const item = items.pop()
if (!item) return cb()
return copyDirItem(items, item, src, dest, opts, cb)
}
function copyDirItem (items, item, src, dest, opts, cb) {
const srcItem = path.join(src, item)
const destItem = path.join(dest, item)
runFilter(srcItem, destItem, opts, (err, include) => {
if (err) return cb(err)
if (!include) return copyDirItems(items, src, dest, opts, cb)
stat.checkPaths(srcItem, destItem, 'copy', opts, (err, stats) => {
if (err) return cb(err)
const { destStat } = stats
getStats(destStat, srcItem, destItem, opts, err => {
if (err) return cb(err)
return copyDirItems(items, src, dest, opts, cb)
})
})
})
}
function onLink (destStat, src, dest, opts, cb) {
fs.readlink(src, (err, resolvedSrc) => {
if (err) return cb(err)
if (opts.dereference) {
resolvedSrc = path.resolve(process.cwd(), resolvedSrc)
}
if (!destStat) {
return fs.symlink(resolvedSrc, dest, cb)
} else {
fs.readlink(dest, (err, resolvedDest) => {
if (err) {
// dest exists and is a regular file or directory,
// Windows may throw UNKNOWN error. If dest already exists,
// fs throws error anyway, so no need to guard against it here.
if (err.code === 'EINVAL' || err.code === 'UNKNOWN') return fs.symlink(resolvedSrc, dest, cb)
return cb(err)
}
if (opts.dereference) {
resolvedDest = path.resolve(process.cwd(), resolvedDest)
}
if (stat.isSrcSubdir(resolvedSrc, resolvedDest)) {
return cb(new Error(`Cannot copy '${resolvedSrc}' to a subdirectory of itself, '${resolvedDest}'.`))
}
// do not copy if src is a subdir of dest since unlinking
// dest in this case would result in removing src contents
// and therefore a broken symlink would be created.
if (stat.isSrcSubdir(resolvedDest, resolvedSrc)) {
return cb(new Error(`Cannot overwrite '${resolvedDest}' with '${resolvedSrc}'.`))
}
return copyLink(resolvedSrc, dest, cb)
})
}
})
}
function copyLink (resolvedSrc, dest, cb) {
fs.unlink(dest, err => {
if (err) return cb(err)
return fs.symlink(resolvedSrc, dest, cb)
})
}
module.exports = copy
@@ -0,0 +1,7 @@
'use strict'
const u = require('universalify').fromCallback
module.exports = {
copy: u(require('./copy')),
copySync: require('./copy-sync')
}
@@ -0,0 +1,39 @@
'use strict'
const u = require('universalify').fromPromise
const fs = require('../fs')
const path = require('path')
const mkdir = require('../mkdirs')
const remove = require('../remove')
const emptyDir = u(async function emptyDir (dir) {
let items
try {
items = await fs.readdir(dir)
} catch {
return mkdir.mkdirs(dir)
}
return Promise.all(items.map(item => remove.remove(path.join(dir, item))))
})
function emptyDirSync (dir) {
let items
try {
items = fs.readdirSync(dir)
} catch {
return mkdir.mkdirsSync(dir)
}
items.forEach(item => {
item = path.join(dir, item)
remove.removeSync(item)
})
}
module.exports = {
emptyDirSync,
emptydirSync: emptyDirSync,
emptyDir,
emptydir: emptyDir
}
@@ -0,0 +1,69 @@
'use strict'
const u = require('universalify').fromCallback
const path = require('path')
const fs = require('graceful-fs')
const mkdir = require('../mkdirs')
function createFile (file, callback) {
function makeFile () {
fs.writeFile(file, '', err => {
if (err) return callback(err)
callback()
})
}
fs.stat(file, (err, stats) => { // eslint-disable-line handle-callback-err
if (!err && stats.isFile()) return callback()
const dir = path.dirname(file)
fs.stat(dir, (err, stats) => {
if (err) {
// if the directory doesn't exist, make it
if (err.code === 'ENOENT') {
return mkdir.mkdirs(dir, err => {
if (err) return callback(err)
makeFile()
})
}
return callback(err)
}
if (stats.isDirectory()) makeFile()
else {
// parent is not a directory
// This is just to cause an internal ENOTDIR error to be thrown
fs.readdir(dir, err => {
if (err) return callback(err)
})
}
})
})
}
function createFileSync (file) {
let stats
try {
stats = fs.statSync(file)
} catch {}
if (stats && stats.isFile()) return
const dir = path.dirname(file)
try {
if (!fs.statSync(dir).isDirectory()) {
// parent is not a directory
// This is just to cause an internal ENOTDIR error to be thrown
fs.readdirSync(dir)
}
} catch (err) {
// If the stat call above failed because the directory doesn't exist, create it
if (err && err.code === 'ENOENT') mkdir.mkdirsSync(dir)
else throw err
}
fs.writeFileSync(file, '')
}
module.exports = {
createFile: u(createFile),
createFileSync
}
@@ -0,0 +1,23 @@
'use strict'
const { createFile, createFileSync } = require('./file')
const { createLink, createLinkSync } = require('./link')
const { createSymlink, createSymlinkSync } = require('./symlink')
module.exports = {
// file
createFile,
createFileSync,
ensureFile: createFile,
ensureFileSync: createFileSync,
// link
createLink,
createLinkSync,
ensureLink: createLink,
ensureLinkSync: createLinkSync,
// symlink
createSymlink,
createSymlinkSync,
ensureSymlink: createSymlink,
ensureSymlinkSync: createSymlinkSync
}

Some files were not shown because too many files have changed in this diff Show More