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

WIP: TEST: Добавить тесты #1152

Merged
merged 29 commits into from
Dec 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
9530c86
TEST: Add jest for testing
thoughtspile Nov 18, 2020
dee9735
TEST: Add danger
thoughtspile Nov 18, 2020
84aed82
TEST: Add danger-jest
thoughtspile Nov 19, 2020
001fa26
TEST: Add yarn install cache
thoughtspile Nov 19, 2020
9cd5929
TEST: Add danger eslint
thoughtspile Nov 19, 2020
1625126
TEST: Prevent bundling tests into dist
thoughtspile Nov 19, 2020
05f2fbe
TEST: Add danger coverage report
thoughtspile Nov 19, 2020
cde4a7a
TEST: Setup react testing library
thoughtspile Nov 19, 2020
0e1aeec
WIP: TEST: Add playwright screen test
thoughtspile Nov 20, 2020
918ecba
TEST: Tighter jest / playwright integration
thoughtspile Nov 23, 2020
cf7e503
TEST: Optimize e2e watch
thoughtspile Nov 24, 2020
af5c5ad
TEST: Multi-project jest for e2e + unit
thoughtspile Nov 24, 2020
d341829
TEST: Trim screenshots lower than viewport
thoughtspile Nov 24, 2020
e274143
TEST: Use docker for screenshots
thoughtspile Nov 25, 2020
a46f7eb
TEST: Fix coverage in test:code
thoughtspile Nov 25, 2020
f42aa3b
TEST: Use roboto on all platforms for screenshots
thoughtspile Nov 24, 2020
2ba7bfb
TEST: Upload failed screens to cloud
thoughtspile Nov 25, 2020
578a16e
TEST: Add screen fuzz testing helpers & enable LFS
thoughtspile Nov 27, 2020
e020d72
TEST: Cleanup testing code & scripts
thoughtspile Nov 30, 2020
614ef2d
TEST: Add docs on testing
thoughtspile Nov 30, 2020
3590e7b
TEST: Wait for fonts to load before screenshot
thoughtspile Dec 1, 2020
52ae212
TEST: Force compact vkcom + dont fail on screen test error
thoughtspile Dec 1, 2020
a4655f2
TEST: Avoid github caching diff images - append hash
thoughtspile Dec 1, 2020
b07df66
TEST: Enable caches for faster pre-commit runs
thoughtspile Dec 2, 2020
613618d
TEST: Fix eslint & ban jest globals in non-test files
thoughtspile Dec 9, 2020
53b4c53
TEST: Fix screenshot fuzz params
thoughtspile Dec 9, 2020
d2f965d
TEST: Add manual workflow for updating screenshots
thoughtspile Dec 11, 2020
cbc4be4
TEST: Warn abount failed & modified screenshots in PR
thoughtspile Dec 11, 2020
64a2e7f
TEST: Silently skip screenshot reporting on missing S3 credentials
thoughtspile Dec 14, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
"extends": ["@vkontakte/eslint-config/typescript/react"],
"parserOptions": {
"project": "./tsconfig.json",
"ecmaVersion": 2018, // Allows for the parsing of modern ECMAScript features
"sourceType": "module", // Allows for the use of imports
"ecmaVersion": 2018, // Allows for the parsing of modern ECMAScript features
"sourceType": "module", // Allows for the use of imports
"ecmaFeatures": {
"jsx": true, // Allows for the parsing of JSX
"jsx": true, // Allows for the parsing of JSX
"restParams": true,
"spread": true
}
Expand All @@ -25,5 +25,17 @@
"@typescript-eslint/no-unused-vars": ["error", {
"ignoreRestSiblings": true
}]
}
},
"overrides": [
{
"files": ["src/**/*.{test,spec,e2e}.{ts,tsx}", "e2e/**/*", "src/testing/**/*"],
"env": {
"jest": true
}
},
{
"files": ["src/**/*.e2e.{ts,tsx}", "e2e/**/*", "src/testing/**/*"],
"extends": ["plugin:jest-playwright/recommended"]
}
]
}
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
src/**/__image_snapshots__/*.png filter=lfs diff=lfs merge=lfs -text
21 changes: 18 additions & 3 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,34 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
lfs: true
- name: Checkout LFS objects
run: git lfs checkout
- uses: actions/setup-node@v1
with:
node-version: 12
- run: yarn install --frozen-lockfile
- run: yarn test
# cached yarn install
- uses: bahmutov/npm-install@v1
- name: Run tests
run: yarn test:ci
- name: FYI visual tests
run: yarn test:e2e
continue-on-error: true
- name: Create test report
if: always()
ArthurStam marked this conversation as resolved.
Show resolved Hide resolved
run: yarn danger ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
styleguide:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
- run: yarn install --frozen-lockfile
# cached yarn install
- uses: bahmutov/npm-install@v1
- run: yarn styleguide:build
- name: Uploading styleguide artifact
uses: actions/upload-artifact@v2
Expand Down
26 changes: 26 additions & 0 deletions .github/workflows/update_screens.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: 'Update screenshots'

on:
workflow_dispatch

jobs:
update_screens:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
lfs: true
- name: Checkout LFS objects
run: git lfs checkout
- uses: actions/setup-node@v1
with:
node-version: 12
# cached yarn install
- uses: bahmutov/npm-install@v1
- name: Update screenshots
run: yarn test:e2e -u
- name: Push updated screenshots
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: "CHORE: Update screenshots"
file_pattern: './**/*.png'
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ coverage
.env
npm-debug.log
stats.json
dist/
dist/*
!e2e/dist/index.html
src/styles/generated/
PUBLISHING.md
.idea/workspace.xml
Expand All @@ -13,3 +14,8 @@ PUBLISHING.md
.idea/markdown-navigator.xml
docs/
.vscode/
test-results.json
lint-results.json
.nyc_output
__diff_output__
.cache
11 changes: 5 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ ReactDOM.render(<App />, document.getElementById('root'));
Сейчас библиотека не гарантирует стабильную работу при интеграции в [vk.com](vk.com) и [m.vk.com](m.vk.com). В vk.com есть нерешенные проблемы с элементами ввода типа клавиатуры и мышки. В m.vk.com есть несоответствия в списках поддерживаемых браузеров.

### Десктопный UI
В данный момент ведется [активная разработка](https://github.com/VKCOM/VKUI/pull/665) адаптивного UI, который будет работать на десктопных и планшетных браузерах. Stay tuned.
В данный момент ведется [активная разработка](https://github.com/VKCOM/VKUI/pull/665) адаптивного UI, который будет работать на десктопных и планшетных браузерах. Stay tuned.

## Тестирование

Мы работаем над качеством библиотеки и подвозим тесты. `yarn test` запускает юниты, проверяет типы и линтит. `yarn test:unit` запускает только юниты и поддерживает интерактивный режим с флагом `--watch`. Также мы поддерживаем скриншотные тесты — смотрите наш [гайд по тестированию](./TESTING.md).

## Документация
В [документации](https://vkcom.github.io/VKUI/) вы сможете найти информацию об использовании компонентов и утилит.
Expand All @@ -59,8 +63,3 @@ ReactDOM.render(<App />, document.getElementById('root'));
## Contributing
Мы очень радуемся, когда пользователи библиотеки работают над её улучшением. Для того, чтобы оставить след в
истории, сделайте форк проекта, внесите изменения и отправьте нам [pull request](https://github.com/VKCOM/VKUI/pulls).





49 changes: 49 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
Мы используем три вида тестов:

__Юнит-тесты__ на jest в файлах `moduleName.test.ts` [пример](./src/helpers/getClassName.test.ts)

__Компонентные тесты__ на jest + [react-testing-library](https://testing-library.com/docs/react-testing-library/example-intro) + [jest-dom](https://github.com/testing-library/jest-dom#table-of-contents) в `ComponentName.test.tsx` [пример](./src/components/Checkbox/Checkbox.test.tsx)

__Скриншотные тесты__ на jest + [playwright](https://playwright.dev/#?path=docs/api.md) + [jest-playwright](https://github.com/playwright-community/jest-playwright) в `ComponentName.e2e.tsx` [пример](./src/components/Checkbox/Checkbox.e2e.tsx).

Чтобы запускать скриншотные тесты локально, нужно установить:
- [git-lfs](https://git-lfs.github.com) — чтобы от эталонных скриншотов не раздувало репозиторий
- [докер](https://www.docker.com/products/docker-desktop) — чтобы платформа не влияла на рендеринг шрифтов. На линуксе докер не нужен.

## Команды

- `yarn test:unit` — только быстрые юниты + компонентные;
- `yarn test:unit:quick` — только юниты + компонентные тесты на незакоммиченные изменения;
- `yarn test:e2e` — только браузерные тесты;
- `yarn test:code` — все тесты.

Все команды работают в интерактивном режиме с опцией `--watch` (`yarn test:unit --watch`).

Чтобы обновить скриншоты, установите докер и git-lfs и запустите `yarn test:e2e -u`.

## `describeScreenshotFuzz`

Функция `describeScreenshotFuzz` из [`/testing/utils`]('./src/testing/utils.ts) помогает быстро заскриншотить все состояния компонента в разных темах и на разных платформах:

```ts
describe('Button', () => {
describeScreenshotFuzz(
// Можем передать компонент или вот так добавить дефолтные пропы для скриншотов
(props: ButtonProps) => <Button {...props}>Кнопка</Button>,
// Описание комбинаций пропов — по элементу массива на независимый набор
[
// mode и disabled влияют на цвет, но не на размер
{ mode: ['primary', 'secondary'], disabled: [undefined, true] },
// size влияет на размер, но не на цвет
{ size: ['s', 'm', 'l'] }
]);
// всего 2 * 2 + 3 = 7 состояний:
// mode="primary"
// mode="primary" disabled
// mode="secondary"
// mode="secondary" disabled
// size="s"
// size="m"
// size="l"
});
```
21 changes: 19 additions & 2 deletions babel.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
const { NODE_ENV } = process.env;
const isProduction = NODE_ENV === 'production';
const isDevelopment = NODE_ENV === 'development';
const useModules = isProduction || isDevelopment;

const testFiles = [
'./src/**/*.test.ts', './src/**/*.test.tsx',
'./src/**/*.spec.ts', './src/**/*.spec.tsx',
'./src/**/*.e2e.ts', './src/**/*.e2e.tsx',
'./e2e/', './src/testing/',
];

module.exports = {
presets: [['@babel/preset-env', { modules: false }], '@babel/preset-react', '@babel/preset-typescript'],
presets: [
['@babel/preset-env', { modules: useModules ? false : 'commonjs' }],
'@babel/preset-react',
'@babel/preset-typescript'
],
plugins: ['@babel/plugin-proposal-class-properties', '@babel/plugin-proposal-object-rest-spread', '@babel/plugin-transform-runtime'],
ignore: ['./src/vkui.js'],
ignore: ['./src/vkui.js'].concat(
isProduction ? ['./src/**/*.test.ts', './src/**/*.test.tsx', './src/**/*.spec.ts', './src/**/*.spec.tsx', './e2e', './src/testing'] : []),
};
131 changes: 131 additions & 0 deletions dangerfile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { fail, message, warn, danger, markdown } from 'danger';
const dangerJest = require('danger-plugin-jest').default;
const { readFile } = require('fs').promises;
const md5 = require('md5');
const path = require('path');
const glob = require('glob');
const { promisify } = require('util')
const pglob = promisify(glob);
const AWS = require('aws-sdk/global');
const S3 = require('aws-sdk/clients/s3');

const { AWS_ACCESS_KEY_ID, AWS_SECRET_KEY, AWS_ENDPOINT } = process.env;

const lintPath = path.join(__dirname, 'lint-results.json');
function lint() {
return readFile(lintPath).then(file => {
const lintReport = JSON.parse(file)
for (const { messages, filePath } of lintReport) {
const relPath = path.relative(__dirname, filePath)
for (const message of messages) {
const text = `${message.message} \`${message.ruleId}\``
if (message.severity === 2) {
fail(text, relPath, message.line);
} else if (message.severity === 1) {
warn(text, relPath, message.line);
}
}
}
});
}

function coverage() {
return readFile(path.join(__dirname, 'coverage', 'coverage-summary.json')).then(file => {
const { total } = JSON.parse(file)
const formatCoverage = (kind, { covered, total, pct }) => `${covered} / ${total} ${kind} (${pct}%)`
message(`Code coverage: ${
Object.entries(total).map(([kind, cov]) => formatCoverage(kind, cov)).join(', ')
}`)
})
}

const updateScreensActionLink = `https://github.com/VKCOM/VKUI/actions?query=workflow%3A"Update+screenshots"`;
const UPLOAD_BUCKET = 'vkui-screenshots';
const awsHost = `${UPLOAD_BUCKET}.${AWS_ENDPOINT}`;
async function uploadFailedScreenshots() {
const diffDir = '__diff_output__'
const { github } = danger;
const pathPrefix = github ? github.pr.number : 'local';
const failedScreens = await pglob(path.join(__dirname, '**', diffDir, '*.png'));

if (failedScreens.length) {
warn(`${failedScreens.length} changed screenshots found — review & update them via ["Update Screenshots" action](${updateScreensActionLink}) before merging.`);
}

if (!AWS_ENDPOINT || !AWS_ACCESS_KEY_ID || !AWS_SECRET_KEY) {
// Silently skip screenshot reporting if credentials missing
console.log('AWS credentials missing - skip screenshot reporting');
return;
}

let s3;
try {
s3 = new AWS.S3({
apiVersion: '2006-03-01',
endpoint: AWS_ENDPOINT,
accessKeyId: AWS_ACCESS_KEY_ID,
secretAccessKey: AWS_SECRET_KEY,
});
} catch (err) {
warn(`Could not create S3 client - aborting screen test reporter.`);
return;
}

try {
await removeDiffs(s3, `${pathPrefix}/`);
} catch (err) {
warn(`Could not purge old screenshots from S3: "${err.message}"`);
}

for (const failedScreen of failedScreens) {
const screenName = path.parse(failedScreen).name;
const fileContents = await readFile(failedScreen);
const key = `${pathPrefix}/${screenName}-${md5(fileContents)}.png`;
try {
await s3.putObject({
Body: fileContents,
Bucket: UPLOAD_BUCKET,
Key: key,
ContentType: 'image/png',
ACL: 'public-read',
}).promise();
markdown(`
<details>
<summary>Screenshot <code>${screenName}</code> failed</summary>
<img src="https://${awsHost}/${key}">
</details>
`.replace(/(^|\n) +/g, ''));
} catch (err) {
warn(`Counld not upload screenshot diff ${screenName}: ${err.message}`);
}
}
}

async function checkUpdatedScreenshots() {
if (danger.git.modified_files.some(file => /__image_snapshots__/.test(file))) {
warn('Some screenshots were modified in this PR');
}
}

async function removeDiffs(s3, prefix) {
const list = (await s3.listObjects({
Bucket: UPLOAD_BUCKET,
Prefix: prefix,
}).promise()).Contents;
if (list && list.length !== 0) {
await s3.deleteObjects({
Bucket: UPLOAD_BUCKET,
Delete: {
Objects: list.map(obj => ({ Key: obj.Key })),
},
}).promise();
}
}

Promise.all([
dangerJest(),
lint(),
coverage(),
uploadFailedScreenshots(),
checkUpdatedScreenshots(),
]);
43 changes: 43 additions & 0 deletions e2e/browser/jest-mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
if (!window.Proxy) {
throw new Error('The environment needs to support window.Proxy!');
}

function makeAny(): any {
// callable
return new Proxy(() => makeAny(), {
// and with any property
get: () => makeAny(),
});
}

window.jest = makeAny();
(window as any)['page'] = makeAny();
(window as any)['expect'] = makeAny();

const notImplemented = (name: string) => () => {
throw new Error(`${name} is not supported in react / playwright browser runtime`);
};
const noop = () => {/* do nothing */};

window.beforeAll = notImplemented('beforeAll');
window.afterAll = notImplemented('afterAll');
window.beforeEach = notImplemented('beforeEach');
window.afterEach = notImplemented('afterEach');

const path: string[] = [];
const withPath = (name: string, fun: jest.ProvidesCallback) => {
path.push(name);
fun(Object.assign(noop, { fail: noop }));
path.pop();
};
const fakeTest = Object.assign(withPath, {
each: notImplemented('describe.each'),
only: withPath,
skip: noop as any,
}) as any as typeof test;

window.describe = fakeTest;
window.test = fakeTest;
window.it = fakeTest;

export const currentPath = () => path.join(' ');
Loading