Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement auto preview on error #87

Merged
merged 23 commits into from
May 3, 2022
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# 0.2.0

## Features

- Auto preview on fail (TODO: Polish this changelog)
thanhsonng marked this conversation as resolved.
Show resolved Hide resolved

# 0.1.7

## Fixes
Expand Down
12 changes: 9 additions & 3 deletions demo/__tests__/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react';

import userEvent from '@testing-library/user-event';
import App from '../App';
import preview from '../../dist/index';
import { debug } from '../../dist/index';

describe('App', () => {
it('should work as expected', () => {
Expand All @@ -17,7 +17,13 @@ describe('App', () => {

// Open http://localhost:3336 to see preview
// Require to run `jest-preview` server before
preview.debug();
expect(screen.getByTestId('count')).toContainHTML('6');
// Execute `preview.debug()` or `debug()` to see the UI in a browser
debug();

// Jest Preview automatically preview failed tests without explicitly calling `debug()`
// Try to uncomment the following line to see the count equals to 6
thanhsonng marked this conversation as resolved.
Show resolved Hide resolved
userEvent.click(screen.getByTestId('increase'));

expect(screen.getByTestId('count')).toContainHTML('7');
});
});
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "jest-preview",
"version": "0.1.7-alpha.0",
"version": "0.2.0-alpha.1",
"description": "Preview your HTML code while using Jest",
"keywords": [
"testing",
Expand Down
2 changes: 1 addition & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ function makeBundle({ filePath, dir = 'dist' }) {
format: 'cjs',
},
plugins: [typescript(), terser()],
external: ['path', 'camelcase'],
external: ['path', 'camelcase', 'fs', 'child_process'],
};
}

Expand Down
67 changes: 60 additions & 7 deletions src/configure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,30 @@ import { exec } from 'child_process';

import { CACHE_FOLDER } from './constants';
import { createCacheFolderIfNeeded } from './utils';
import { debug } from './preview';

interface JestPreviewConfigOptions {
externalCss: string[];
externalCss?: string[];
autoPreview?: boolean;
publicFolder?: string;
}

export async function jestPreviewConfigure(
options: JestPreviewConfigOptions = { externalCss: [] },
) {
export async function jestPreviewConfigure({
externalCss = [],
autoPreview = true,
publicFolder,
}: JestPreviewConfigOptions) {
thanhsonng marked this conversation as resolved.
Show resolved Hide resolved
if (autoPreview) {
autoRunPreview();
}

if (!fs.existsSync(CACHE_FOLDER)) {
fs.mkdirSync(CACHE_FOLDER, {
recursive: true,
});
}

options.externalCss?.forEach((cssFile) => {
externalCss?.forEach((cssFile) => {
// Avoid name collision
// Example: src/common/styles.css => cache-src___common___styles.css
const delimiter = '___';
Expand Down Expand Up @@ -65,15 +73,60 @@ export async function jestPreviewConfigure(
// }
});

if (options.publicFolder) {
if (publicFolder) {
createCacheFolderIfNeeded();
fs.writeFileSync(
path.join(CACHE_FOLDER, 'cache-public.config'),
options.publicFolder,
publicFolder,
{
encoding: 'utf-8',
flag: 'w',
},
);
}
}

function autoRunPreview() {
const originalIt = it;
const itWithPreview: jest.It = (name, callback, timeout) => {
let callbackWithPreview = undefined as jest.ProvidesCallback | undefined;
thanhsonng marked this conversation as resolved.
Show resolved Hide resolved
if (!callback) {
callbackWithPreview = undefined;
} else if (callback.constructor.name === 'AsyncFunction') {
console.log(callback.constructor.name);
thanhsonng marked this conversation as resolved.
Show resolved Hide resolved
callbackWithPreview = async function () {
try {
return await (callback as () => Promise<unknown>)();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opportunity to improve: We can write a helper function to detect an async function, that will make TypeScript happy and doesn't require type assertion here. I will add an example below.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thanhsonng I'm waiting for your example. You can commit directly as well if you want.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out it is not easy to achieve. We need to inspect the return value, i.e. we must call the callback function first.
On the other hand, checking callback.constructor.name === 'AsyncFunction' is not enough IMO. We actually want to check if the function return a Promise.

So I propose something like this. We always await for callback's return value, regardless of its signature, and we make callbackWithPreview always return a Promise.

if (!callback) {
  callbackWithPreview = undefined;
} else {
  callbackWithPreview = async (...args: Parameters<jest.ProvidesCallback>) => {
    try {
      // @ts-ignore
      return await callback(...args);
    } catch (error) {
      debug();
      throw error;
    }
  }
}

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checking callback.constructor.name === ‘AsyncFunction’ is not enough IMO. We actually want to check if the function return a Promise.

Can you elaborate why we need to check if the function return a Promise, instead of checking the function is an async function?

So I propose something like this. We always await for callback’s return value, regardless of its signature, and we make callbackWithPreview always return a Promise.

I did try it before this and it works actually (not well tested yet). However, I have a few opening questions I haven’t had answers yet:

  1. Is there any issues regarding the rightness when we always converting sync => async?
  2. Is there any performance issues when we always use async (even when not needed)?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you elaborate why we need to check if the function return a Promise, instead of checking the function is an async function?

An async function is just syntactic sugar for functions that return a promise. Jest allows user to write functions that return a promise without using async/await syntax, so we must account for that case as well. This link is for reference: https://jestjs.io/docs/tutorial-async.

I did try it before this and it works actually (not well tested yet). However, I have a few opening questions I haven’t had answers yet:

  1. Is there any issues regarding the rightness when we always converting sync => async?
  2. Is there any performance issues when we always use async (even when not needed)?

I believe those are non-issues but you shouldn't take my words for it. I will do some research about these questions. 😂

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lemme do a benchmark.

Copy link
Owner Author

@nvh95 nvh95 May 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thanhsonng I did a benchmark and the running time is very similar. So I will go with always async approach.

} catch (error) {
debug();
throw error;
}
};
} else if (callback.constructor.name === 'Function') {
callbackWithPreview = function (
...args: Parameters<jest.ProvidesCallback>
) {
try {
// @ts-expect-error Just foward the args
return callback(...args) as void;
} catch (error) {
debug();
throw error;
}
};
}

return originalIt(name, callbackWithPreview, timeout);
};
itWithPreview.each = originalIt.each;
itWithPreview.only = originalIt.only;
itWithPreview.skip = originalIt.skip;
itWithPreview.todo = originalIt.todo;
itWithPreview.concurrent = originalIt.concurrent;

// Overwrite global it/ test
// Is there any use cases that `it` and `test` is undefined?
it = itWithPreview;
test = itWithPreview;
// TODO: Patch fit
}
28 changes: 28 additions & 0 deletions website/blog/2022-05-03-automatic-mode/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
slug: automatic-mode
title: Announcing Automatic Mode
authors: [nvh95, thanhsonng]
thanhsonng marked this conversation as resolved.
Show resolved Hide resolved
tags: [jest-preview, developer-experience]
# TODO
# image: /img/sass-support.png
---

<!-- Draft -->

We are so happy to annouce that we are launching Jest Preview Automatic Mode. In this mode, you don't have to trigger the `preview.debug()` function by yourself. Jest Preview automatically preview the UI of your app whenever Jest tests fail

Insert image

We believe this is the game changer feature of Jest Preview, which boost the FE productivity dramatically on debugging test. You don't have to move the `preview.debug()` around by yourself anymore. All you need is just a few lines of code

```js
jestPreviewConfigure({
autoPreview: true, // recheck name
});
```

Automatic Mode is in experiement and is an opt-in option. We recommend you to start use it now. Automatic Mode will be default mode in Jest Preview 0.3.0

If you have any trouble with Automatic Mode, opt out by...

Did you use Jest Preview Automatic Mode yet, let's us know by tweeting (insert I use Jest Preview Automatic Mode and it's great! #jest-preview #automatic-mode)
41 changes: 41 additions & 0 deletions website/docs/api/jestPreviewConfigure.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,44 @@ jestPreviewConfigure({
publicFolder: 'your-public-folder-name',
});
```

## externalCss: string[]

Default: `[]`

CSS files outside your Jest rendered app (e.g: CSS from `src/index.js`, `main.jsx`) should be configured via `externalCss` option. They should be path from root of your project. For example:

```js
jestPreviewConfigure({
// Configure external CSS
externalCss: [
'demo/global.css',
'demo/global.scss', // Sass
'node_modules/@your-design-system/css/dist/index.min.css', // css from node_modules
'node_modules/bootstrap/dist/css/bootstrap.min.css',
],
```

## publicFolder: string (default: "public")

Default: `public`.

Name of your public folder from the project root.

You don't have to configure this by yourself if your public folder is `public`. Below you can find a list of public directories which have different names than `public`:

<!-- Thanks msw for the idea https://github.com/mswjs/mswjs.io/blob/9f62d45a3740789cc4308ae1475027598541a007/docs/snippets/public-dir.mdx -->

| Project name | Public directory |
| ------------------------------------ | ---------------- |
| [GatsbyJS](https://www.gatsbyjs.org) | `static` |
| [Angular](https://angular.io/) | `src` |
| [Preact](https://preactjs.com) | `src/static` |

## autoPreview: boolean

Default: `true`

Automatically preview the UI in the external browser when the test fails. You don't need to invoke `preview.debug()` by yourself anymore.

Set to `false` if you experience any error or just want to opt-out.