Migrate from Jest to Vitest (#3173)
* first part of transition * second part of transition * remove jest * bump up node version and playwright version * update all screenshots because of the new playwright version * fix tests * one more screenshot update
@@ -34,7 +34,7 @@ jobs:
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22.11.0
|
||||
node-version: 22.14.0
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Cache node_modules
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22.11.0
|
||||
node-version: 22.14.0
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Cache node_modules
|
||||
@@ -133,7 +133,7 @@ jobs:
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22.11.0
|
||||
node-version: 22.14.0
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Cache node_modules
|
||||
@@ -163,8 +163,8 @@ jobs:
|
||||
echo "exitcode=$exitcode" >> $GITHUB_OUTPUT
|
||||
exit "$exitcode"
|
||||
|
||||
jest_tests:
|
||||
name: Jest tests
|
||||
vitest_tests:
|
||||
name: Unit tests
|
||||
needs: [ code_quality, envs_validation ]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -176,7 +176,7 @@ jobs:
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22.11.0
|
||||
node-version: 22.14.0
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Cache node_modules
|
||||
@@ -195,8 +195,8 @@ jobs:
|
||||
if: steps.cache-node-modules.outputs.cache-hit == 'true'
|
||||
run: yarn chakra:typegen
|
||||
|
||||
- name: Run Jest
|
||||
run: yarn test:jest ${{ github.event_name == 'pull_request' && '--changedSince=origin/main' || '' }} --passWithNoTests
|
||||
- name: Run Vitest
|
||||
run: yarn test:vitest ${{ github.event_name == 'pull_request' && '--changed=origin/main' || '' }} --passWithNoTests
|
||||
|
||||
pw_affected_tests:
|
||||
name: Resolve affected Playwright tests
|
||||
@@ -212,7 +212,7 @@ jobs:
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22.11.0
|
||||
node-version: 22.14.0
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Cache node_modules
|
||||
@@ -254,7 +254,7 @@ jobs:
|
||||
(needs.pw_affected_tests.result == 'success' || needs.pw_affected_tests.result == 'skipped')
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.49.0-noble
|
||||
image: mcr.microsoft.com/playwright:v1.57.0-noble
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -273,7 +273,7 @@ jobs:
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22.11.0
|
||||
node-version: 22.14.0
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Cache node_modules
|
||||
|
||||
@@ -9,7 +9,7 @@ on:
|
||||
- '.husky/**'
|
||||
- '.vscode/**'
|
||||
- 'docs/**'
|
||||
- 'jest/**'
|
||||
- 'vitest/**'
|
||||
- 'mocks/**'
|
||||
- 'playwright/**'
|
||||
- 'stubs/**'
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22.11.0'
|
||||
node-version: '22.14.0'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
# Ensure npm 11.5.1 is installed
|
||||
|
||||
@@ -3,23 +3,5 @@
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Jest: watch current file",
|
||||
"program": "${workspaceFolder}/node_modules/jest/bin/jest",
|
||||
"args": [
|
||||
"${fileBasename}",
|
||||
"--runInBand",
|
||||
"--verbose",
|
||||
"-i",
|
||||
"--no-cache",
|
||||
"--watchAll",
|
||||
"--testTimeout=1000000000",
|
||||
],
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen"
|
||||
}
|
||||
]
|
||||
"configurations": [ ]
|
||||
}
|
||||
@@ -198,34 +198,13 @@
|
||||
},
|
||||
},
|
||||
|
||||
// JEST TESTS
|
||||
// VITEST TESTS
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "test:jest",
|
||||
"script": "test:vitest",
|
||||
"problemMatcher": [],
|
||||
"label": "jest",
|
||||
"detail": "run jest tests",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "shared",
|
||||
"focus": true,
|
||||
"close": false,
|
||||
"revealProblems": "onProblem",
|
||||
},
|
||||
"icon": {
|
||||
"color": "terminal.ansiBlue",
|
||||
"id": "beaker"
|
||||
},
|
||||
"runOptions": {
|
||||
"instanceLimit": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "test:jest:watch",
|
||||
"problemMatcher": [],
|
||||
"label": "jest: watch all",
|
||||
"detail": "run jest tests in watch mode",
|
||||
"label": "vitest",
|
||||
"detail": "run unit tests",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "shared",
|
||||
@@ -243,10 +222,10 @@
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"command": "yarn test:jest ${relativeFileDirname}/${fileBasename} --watch",
|
||||
"command": "yarn test:vitest ${relativeFileDirname}/${fileBasename}",
|
||||
"problemMatcher": [],
|
||||
"label": "jest: watch",
|
||||
"detail": "run jest tests in watch mode for current file",
|
||||
"label": "vitest: file",
|
||||
"detail": "run unit tests for current file",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "shared",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# *****************************
|
||||
# *** STAGE 1: Dependencies ***
|
||||
# *****************************
|
||||
FROM node:22.11.0-alpine AS deps
|
||||
FROM node:22.14.0-alpine AS deps
|
||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||
RUN apk add --no-cache libc6-compat python3 make g++
|
||||
RUN ln -sf /usr/bin/python3 /usr/bin/python
|
||||
@@ -67,7 +67,7 @@ RUN yarn --frozen-lockfile --network-timeout 100000
|
||||
# *****************************
|
||||
# ****** STAGE 2: Build *******
|
||||
# *****************************
|
||||
FROM node:22.11.0-alpine AS builder
|
||||
FROM node:22.14.0-alpine AS builder
|
||||
RUN apk add --no-cache --upgrade libc6-compat bash jq
|
||||
|
||||
# pass build args to env variables
|
||||
@@ -144,7 +144,7 @@ RUN cd ./deploy/tools/llms-txt-generator && yarn build
|
||||
# ******* STAGE 3: Run ********
|
||||
# *****************************
|
||||
# Production image, copy all the files and run next
|
||||
FROM node:22.11.0-alpine AS runner
|
||||
FROM node:22.14.0-alpine AS runner
|
||||
RUN apk add --no-cache --upgrade bash curl jq unzip
|
||||
|
||||
### APP
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Set of ENVs for Jest unit tests
|
||||
# Set of ENVs for Vitest unit tests
|
||||
|
||||
# app configuration
|
||||
NEXT_PUBLIC_APP_PROTOCOL=http
|
||||
@@ -37,7 +37,7 @@ NEXT_PUBLIC_GIT_TAG=v1.0.11
|
||||
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Bitquery','baseUrl':'https://explorer.bitquery.io/','paths':{'tx':'/goerli/tx','address':'/goerli/address','token':'/goerli/token','block':'/goerli/block'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]
|
||||
|
||||
# app features
|
||||
NEXT_PUBLIC_APP_INSTANCE=jest
|
||||
NEXT_PUBLIC_APP_INSTANCE=vitest
|
||||
NEXT_PUBLIC_APP_ENV=testing
|
||||
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://localhost:3000/marketplace-config.json
|
||||
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://localhost:3000/marketplace-submit-form
|
||||
@@ -47,7 +47,7 @@ We are using following technology stack in the project
|
||||
- [Next.js](https://nextjs.org/) as application framework
|
||||
- [Chakra](https://chakra-ui.com/) as component library; our theme customization can be found in `/theme` folder
|
||||
- [TanStack Query](https://tanstack.com/query/v4/docs/react/overview/) for fetching, caching and updating data from the API
|
||||
- [Jest](https://jestjs.io/) as JavaScript testing framework
|
||||
- [Vitest](https://vitest.dev/) as JavaScript testing framework
|
||||
- [Playwright](https://playwright.dev/) as a tool for components visual testing
|
||||
|
||||
And of course our premier language is [Typescript](https://www.typescriptlang.org/).
|
||||
@@ -119,13 +119,13 @@ These are the steps that you have to follow to make everything work:
|
||||
|
||||
Every feature or bugfix should be accompanied by tests, either unit tests or component visual tests, or both, except from trivial fixes (for example, typo fix). All commands for running tests you can find [below](./CONTRIBUTING.md#command-list).
|
||||
|
||||
### Jest unit tests
|
||||
### Vitest unit tests
|
||||
|
||||
If your changes are only related to the logic of the app and not to its visual presentation, then try to write unit tests using [Jest](https://jestjs.io/) framework and [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/). In general these tests are "cheaper" and faster than Playwright ones. Use them for testing your utilities and React hooks, as well as the whole components logic.
|
||||
If your changes are only related to the logic of the app and not to its visual presentation, then try to write unit tests using [Vitest](https://vitest.dev/) framework and [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/). In general these tests are "cheaper" and faster than Playwright ones. Use them for testing your utilities and React hooks, as well as the whole components logic.
|
||||
|
||||
Place your test suites in `.test.ts` or `.test.tsx` files. You can find or add some mocks or other helpful utilities for these tests purposes in the `/jest` folder.
|
||||
Place your test suites in `.spec.ts` or `.spec.tsx` files. You can find or add some mocks or other helpful utilities for these tests purposes in the `/vitest` folder.
|
||||
|
||||
*Note*, that we are using custom renderer and wrapper in all tests for React components, so please do not import package `@testing-library/react` directly in your test suites, instead use imports from `jest/lib` utility.
|
||||
*Note*, that we are using custom renderer and wrapper in all tests for React components, so please do not import package `@testing-library/react` directly in your test suites, instead use imports from `vitest/lib` utility.
|
||||
|
||||
### Playwright components tests
|
||||
|
||||
@@ -198,8 +198,7 @@ We have 3 pre-configured projects. You can run your test with the desired projec
|
||||
| `yarn svg:format` | format and optimize SVG icons in the `/icons` folder using SVGO tool |
|
||||
| `yarn svg:build-sprite` | build SVG icons sprite |
|
||||
| **Testing** |
|
||||
| `yarn test:jest` | run all Jest unit tests |
|
||||
| `yarn test:jest:watch` | run all Jest unit tests in watch mode |
|
||||
| `yarn test:vitest` | run all Vitest unit tests |
|
||||
| `yarn test:pw:local` | run Playwright component tests locally |
|
||||
| `yarn test:pw:docker` | run Playwright component tests in docker container |
|
||||
| `yarn test:pw:ci` | run Playwright component tests in CI |
|
||||
@@ -214,6 +213,4 @@ We have 3 pre-configured projects. You can run your test with the desired projec
|
||||
|
||||
There are some predefined tasks for all commands described above. You can see the full list by pressing <kbd>cmd + shift + P</kbd> and using command `Task: Run task`
|
||||
|
||||
Also there is a Jest test launch configuration for debugging and running current test file in the watch mode.
|
||||
|
||||
And you may find the Dev Container setup useful too.
|
||||
|
||||
@@ -6,13 +6,13 @@ import reactQueryPlugin from '@tanstack/eslint-plugin-query';
|
||||
import consistentDefaultExportNamePlugin from 'eslint-plugin-consistent-default-export-name';
|
||||
import importPlugin from 'eslint-plugin-import';
|
||||
import importHelpersPlugin from 'eslint-plugin-import-helpers';
|
||||
import jestPlugin from 'eslint-plugin-jest';
|
||||
import jsxA11yPlugin from 'eslint-plugin-jsx-a11y';
|
||||
import noCyrillicStringPlugin from 'eslint-plugin-no-cyrillic-string';
|
||||
import playwrightPlugin from 'eslint-plugin-playwright';
|
||||
import reactPlugin from 'eslint-plugin-react';
|
||||
import reactHooksPlugin from 'eslint-plugin-react-hooks';
|
||||
import * as regexpPlugin from 'eslint-plugin-regexp';
|
||||
import vitestPlugin from 'eslint-plugin-vitest';
|
||||
import globals from 'globals';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -79,14 +79,12 @@ export default tseslint.config(
|
||||
{
|
||||
plugins: {
|
||||
'@typescript-eslint': tseslint.plugin,
|
||||
jest: jestPlugin,
|
||||
},
|
||||
languageOptions: {
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
},
|
||||
globals: jestPlugin.environments.globals.globals,
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/array-type': [ 'error', {
|
||||
@@ -261,10 +259,21 @@ export default tseslint.config(
|
||||
},
|
||||
|
||||
{
|
||||
files: [ '**/*.test.{ts,js,jsx,tsx}' ],
|
||||
plugins: { jest: jestPlugin },
|
||||
files: [ '**/*.spec.{ts,js,jsx,tsx}' ],
|
||||
plugins: { vitest: vitestPlugin },
|
||||
rules: {
|
||||
...vitestPlugin.configs.recommended.rules,
|
||||
},
|
||||
settings: {
|
||||
vitest: {
|
||||
typecheck: true,
|
||||
},
|
||||
},
|
||||
languageOptions: {
|
||||
globals: jestPlugin.environments.globals.globals,
|
||||
globals: {
|
||||
...vitestPlugin.environments.env.globals,
|
||||
fetchMock: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -297,7 +306,6 @@ export default tseslint.config(
|
||||
'/^data/',
|
||||
'/^deploy/',
|
||||
'/^icons/',
|
||||
'/^jest/',
|
||||
'/^lib/',
|
||||
'/^mocks/',
|
||||
'/^pages/',
|
||||
@@ -306,6 +314,7 @@ export default tseslint.config(
|
||||
'/^theme/',
|
||||
'/^toolkit/',
|
||||
'/^ui/',
|
||||
'/^vitest/',
|
||||
],
|
||||
[ 'parent', 'sibling', 'index' ],
|
||||
],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { EssentialDappsChainConfig } from 'types/client/marketplace';
|
||||
import type { MultichainConfig } from 'types/multichain';
|
||||
import type { WalletProvider } from 'types/web3';
|
||||
import 'vitest-fetch-mock';
|
||||
|
||||
type CPreferences = {
|
||||
zone: string;
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import type { JestConfigWithTsJest } from 'ts-jest';
|
||||
|
||||
/*
|
||||
* For a detailed explanation regarding each configuration property and type check, visit:
|
||||
* https://jestjs.io/docs/configuration
|
||||
*/
|
||||
|
||||
const config: JestConfigWithTsJest = {
|
||||
clearMocks: true,
|
||||
coverageProvider: 'v8',
|
||||
globalSetup: '<rootDir>/jest/global-setup.ts',
|
||||
moduleDirectories: [
|
||||
'node_modules',
|
||||
__dirname,
|
||||
],
|
||||
moduleNameMapper: {
|
||||
'^jest/(.*)': '<rootDir>/jest/$1',
|
||||
'^nextjs-routes$': '<rootDir>/jest/mocks/nextjs-routes.js',
|
||||
'\\.svg$': '<rootDir>/jest/mocks/svg.js',
|
||||
'^@uidotdev/usehooks$': '<rootDir>/jest/mocks/usehooks.js',
|
||||
},
|
||||
modulePathIgnorePatterns: [
|
||||
'node_modules_linux',
|
||||
],
|
||||
preset: 'ts-jest',
|
||||
reporters: [ 'default', 'github-actions' ],
|
||||
setupFiles: [
|
||||
'<rootDir>/jest/setup.ts',
|
||||
],
|
||||
testEnvironment: 'jsdom',
|
||||
transform: {
|
||||
// '^.+\\.[tj]sx?$' to process js/ts with `ts-jest`
|
||||
// '^.+\\.m?[tj]sx?$' to process js/ts/mjs/mts with `ts-jest`
|
||||
'^.+\\.tsx?$': [
|
||||
'ts-jest',
|
||||
{
|
||||
tsconfig: 'tsconfig.jest.json',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,5 +0,0 @@
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
export default async function globalSetup() {
|
||||
dotenv.config({ path: './configs/envs/.env.jest' });
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { NextRouter } from 'next/router';
|
||||
|
||||
export const router = {
|
||||
query: {},
|
||||
push: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
export const useRouter = jest.fn<unknown, Array<Partial<NextRouter>>>(() => (router));
|
||||
|
||||
export const mockUseRouter = (params?: Partial<NextRouter>) => {
|
||||
return {
|
||||
useRouter: jest.fn(() => ({
|
||||
...router,
|
||||
...params,
|
||||
})),
|
||||
};
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
module.exports = {
|
||||
route: jest.fn((opts) => {
|
||||
const pathname = opts?.pathname;
|
||||
const query = opts?.query || {};
|
||||
|
||||
if (pathname === '/address/[hash]') {
|
||||
return `/address/${ query.hash || 'test-hash' }`;
|
||||
}
|
||||
if (pathname === '/tx/[hash]') {
|
||||
return `/tx/${ query.hash || 'test-hash' }`;
|
||||
}
|
||||
if (pathname === '/name-services/clusters/[name]') {
|
||||
return `/name-services/clusters/${ query.name || 'test-cluster' }`;
|
||||
}
|
||||
|
||||
return pathname || '/';
|
||||
}),
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
module.exports = 'test-file-stub';
|
||||
@@ -1,12 +0,0 @@
|
||||
module.exports = {
|
||||
useClickAway: jest.fn(() => jest.fn()),
|
||||
useEventListener: jest.fn(),
|
||||
useLocalStorage: jest.fn(() => [ '', jest.fn() ]),
|
||||
useSessionStorage: jest.fn(() => [ '', jest.fn() ]),
|
||||
useToggle: jest.fn(() => [ false, jest.fn() ]),
|
||||
useDebounce: jest.fn((value) => value),
|
||||
useThrottle: jest.fn((value) => value),
|
||||
usePrevious: jest.fn(),
|
||||
useCounter: jest.fn(() => ({ count: 0, increment: jest.fn(), decrement: jest.fn(), reset: jest.fn() })),
|
||||
useCopyToClipboard: jest.fn(() => [ '', jest.fn() ]),
|
||||
};
|
||||
@@ -1,55 +0,0 @@
|
||||
import dotenv from 'dotenv';
|
||||
import { TextEncoder, TextDecoder } from 'util';
|
||||
|
||||
import fetchMock from 'jest-fetch-mock';
|
||||
|
||||
fetchMock.enableMocks();
|
||||
|
||||
const envs = dotenv.config({ path: './configs/envs/.env.jest' });
|
||||
|
||||
Object.assign(global, { TextDecoder, TextEncoder });
|
||||
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation(query => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: jest.fn(),
|
||||
removeListener: jest.fn(),
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
Object.defineProperty(window, '__envs', {
|
||||
writable: true,
|
||||
value: envs.parsed || {},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
const consoleError = console.error;
|
||||
|
||||
global.console = {
|
||||
...console,
|
||||
error: (...args) => {
|
||||
// silence some irrelevant errors
|
||||
if (args.some((arg) => typeof arg === 'string' && arg.includes('Using kebab-case for css properties'))) {
|
||||
return;
|
||||
}
|
||||
consoleError(...args);
|
||||
},
|
||||
};
|
||||
|
||||
// Polyfill for structuredClone
|
||||
if (typeof structuredClone === 'undefined') {
|
||||
global.structuredClone = <T>(obj: T): T => {
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(obj)) as T;
|
||||
} catch (error) {
|
||||
// Fallback for circular references and other special cases
|
||||
return obj;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { expect, test, describe } from 'vitest';
|
||||
|
||||
import buildUrl from './buildUrl';
|
||||
|
||||
test('builds URL for resource without path params', () => {
|
||||
@@ -1,3 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import {
|
||||
shouldShowClearButton,
|
||||
shouldDisableViewToggle,
|
||||
@@ -1,3 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import {
|
||||
filterOwnedClusters,
|
||||
getTotalRecordsDisplay,
|
||||
@@ -1,4 +1,5 @@
|
||||
import { isEvmAddress } from 'lib/address/isEvmAddress';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import { detectInputType } from './detectInputType';
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ClustersOrderBy } from 'types/api/clusters';
|
||||
import type { ClustersDirectoryObject } from 'types/api/clusters';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import {
|
||||
getViewModeOrderBy,
|
||||
shouldShowDirectoryView,
|
||||
@@ -1,32 +1,47 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { renderHook } from 'vitest/lib';
|
||||
|
||||
import { useAddressClusters } from './useAddressClusters';
|
||||
|
||||
jest.mock('lib/api/useApiQuery', () => ({
|
||||
__esModule: true,
|
||||
'default': jest.fn(),
|
||||
const { mockUseApiQuery } = vi.hoisted(() => ({
|
||||
mockUseApiQuery: vi.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('configs/app', () => ({
|
||||
features: {
|
||||
nameServices: {
|
||||
isEnabled: true,
|
||||
ens: { isEnabled: true },
|
||||
clusters: { isEnabled: true },
|
||||
vi.mock('lib/api/useApiQuery', () => ({
|
||||
'default': mockUseApiQuery,
|
||||
}));
|
||||
|
||||
vi.mock('configs/app', async() => {
|
||||
return {
|
||||
'default': {
|
||||
UI: {
|
||||
colorTheme: {},
|
||||
homepage: {},
|
||||
fonts: {},
|
||||
},
|
||||
features: {
|
||||
nameServices: {
|
||||
isEnabled: true,
|
||||
ens: { isEnabled: true },
|
||||
clusters: { isEnabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const mockUseApiQuery = require('lib/api/useApiQuery').default;
|
||||
};
|
||||
});
|
||||
|
||||
describe('useAddressClusters', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
mockUseApiQuery.mockReturnValue({ data: null, isLoading: false });
|
||||
});
|
||||
|
||||
it('should call API with correct parameters', () => {
|
||||
const addressHash = '0x1234567890123456789012345678901234567890';
|
||||
|
||||
useAddressClusters(addressHash);
|
||||
renderHook(() => useAddressClusters(addressHash));
|
||||
|
||||
expect(mockUseApiQuery).toHaveBeenCalledWith('clusters:get_clusters_by_address', {
|
||||
queryParams: {
|
||||
@@ -41,7 +56,7 @@ describe('useAddressClusters', () => {
|
||||
});
|
||||
|
||||
it('should be disabled when addressHash is empty', () => {
|
||||
useAddressClusters('');
|
||||
renderHook(() => useAddressClusters(''));
|
||||
|
||||
expect(mockUseApiQuery).toHaveBeenCalledWith('clusters:get_clusters_by_address', {
|
||||
queryParams: {
|
||||
@@ -58,7 +73,7 @@ describe('useAddressClusters', () => {
|
||||
it('should handle isEnabled parameter', () => {
|
||||
const addressHash = '0x1234567890123456789012345678901234567890';
|
||||
|
||||
useAddressClusters(addressHash, false);
|
||||
renderHook(() => useAddressClusters(addressHash, false));
|
||||
|
||||
expect(mockUseApiQuery).toHaveBeenCalledWith('clusters:get_clusters_by_address', {
|
||||
queryParams: {
|
||||
@@ -1,25 +1,30 @@
|
||||
import { renderHook, act } from 'jest/lib';
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import { useQueryParams } from 'lib/router/useQueryParams';
|
||||
import type { Mock } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { renderHook, act } from 'vitest/lib';
|
||||
|
||||
import { useClusterPagination } from '../clusters/useClusterPagination';
|
||||
|
||||
jest.mock('next/router', () => ({
|
||||
useRouter: jest.fn(),
|
||||
vi.mock('next/router', () => ({
|
||||
useRouter: vi.fn(),
|
||||
}));
|
||||
jest.mock('lib/router/useQueryParams');
|
||||
jest.mock('lib/router/getQueryParamString');
|
||||
vi.mock('lib/router/useQueryParams');
|
||||
vi.mock('lib/router/getQueryParamString');
|
||||
|
||||
const { useRouter } = require('next/router');
|
||||
const mockUseRouter = useRouter as jest.MockedFunction<typeof useRouter>;
|
||||
const mockUseQueryParams = useQueryParams as jest.MockedFunction<typeof useQueryParams>;
|
||||
const mockGetQueryParamString = getQueryParamString as jest.MockedFunction<typeof getQueryParamString>;
|
||||
const mockUseRouter = useRouter as Mock<typeof useRouter>;
|
||||
const mockUseQueryParams = useQueryParams as Mock<typeof useQueryParams>;
|
||||
const mockGetQueryParamString = getQueryParamString as Mock<typeof getQueryParamString>;
|
||||
|
||||
describe('useClusterPagination', () => {
|
||||
const mockUpdateQuery = jest.fn();
|
||||
const mockUpdateQuery = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
mockUseRouter.mockReturnValue({
|
||||
query: {},
|
||||
} as unknown as ReturnType<typeof useRouter>);
|
||||
@@ -1,46 +1,50 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import { renderHook } from 'jest/lib';
|
||||
import type { Mock } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { renderHook } from 'vitest/lib';
|
||||
|
||||
import { useClusterSearch } from './useClusterSearch';
|
||||
|
||||
jest.mock('next/router', () => ({
|
||||
useRouter: jest.fn(),
|
||||
vi.mock('next/router', () => ({
|
||||
useRouter: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockUseRouter = useRouter as jest.MockedFunction<typeof useRouter>;
|
||||
const mockUseRouter = useRouter as Mock<typeof useRouter>;
|
||||
|
||||
describe('useClusterSearch', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return search term from router query', () => {
|
||||
it('should return search term from router query', async() => {
|
||||
mockUseRouter.mockReturnValue({
|
||||
query: { q: 'test-search' },
|
||||
} as unknown as ReturnType<typeof useRouter>);
|
||||
|
||||
const { result } = renderHook(() => useClusterSearch());
|
||||
const { result } = await renderHook(() => useClusterSearch());
|
||||
|
||||
expect(result.current.searchTerm).toBe('test-search');
|
||||
});
|
||||
|
||||
it('should debounce search term', () => {
|
||||
it('should debounce search term', async() => {
|
||||
mockUseRouter.mockReturnValue({
|
||||
query: { q: 'test' },
|
||||
} as unknown as ReturnType<typeof useRouter>);
|
||||
|
||||
const { result } = renderHook(() => useClusterSearch());
|
||||
const { result } = await renderHook(() => useClusterSearch());
|
||||
|
||||
expect(result.current.debouncedSearchTerm).toBe('test');
|
||||
});
|
||||
|
||||
it('should handle empty query', () => {
|
||||
it('should handle empty query', async() => {
|
||||
mockUseRouter.mockReturnValue({
|
||||
query: {},
|
||||
} as unknown as ReturnType<typeof useRouter>);
|
||||
|
||||
const { result } = renderHook(() => useClusterSearch());
|
||||
const { result } = await renderHook(() => useClusterSearch());
|
||||
|
||||
expect(result.current.searchTerm).toBe('');
|
||||
expect(result.current.debouncedSearchTerm).toBe('');
|
||||
@@ -1,23 +1,28 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import useApiQuery from 'lib/api/useApiQuery';
|
||||
import { detectInputType } from 'lib/clusters/detectInputType';
|
||||
import type { Mock } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
|
||||
import { useClustersData } from './useClustersData';
|
||||
|
||||
jest.mock('lib/api/useApiQuery');
|
||||
const mockUseApiQuery = useApiQuery as jest.MockedFunction<typeof useApiQuery>;
|
||||
vi.mock('lib/api/useApiQuery');
|
||||
const mockUseApiQuery = useApiQuery as Mock<typeof useApiQuery>;
|
||||
|
||||
type MockQueryResult = ReturnType<typeof useApiQuery>;
|
||||
|
||||
jest.mock('lib/clusters/detectInputType', () => ({
|
||||
detectInputType: jest.fn(),
|
||||
vi.mock('lib/clusters/detectInputType', () => ({
|
||||
detectInputType: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockDetectInputType = require('lib/clusters/detectInputType').detectInputType;
|
||||
const mockDetectInputType = detectInputType as Mock<typeof detectInputType>;
|
||||
|
||||
describe('useClustersData', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
|
||||
mockUseApiQuery.mockReturnValue({
|
||||
data: null,
|
||||
@@ -183,7 +188,7 @@ describe('useClustersData', () => {
|
||||
{ initialProps: { searchTerm: 'search' } },
|
||||
);
|
||||
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
|
||||
rerender({ searchTerm: 'search' });
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`generates correct metadata for: dynamic route 1`] = `
|
||||
exports[`generates correct metadata for: > dynamic route 1`] = `
|
||||
{
|
||||
"canonical": undefined,
|
||||
"description": "View transaction 0x12345 on Blockscout (Blockscout) Explorer",
|
||||
@@ -14,7 +14,7 @@ exports[`generates correct metadata for: dynamic route 1`] = `
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`generates correct metadata for: dynamic route with API data 1`] = `
|
||||
exports[`generates correct metadata for: > dynamic route with API data 1`] = `
|
||||
{
|
||||
"canonical": undefined,
|
||||
"description": "0x12345, balances and analytics on the Blockscout (Blockscout) Explorer",
|
||||
@@ -45,7 +45,7 @@ exports[`generates correct metadata for: dynamic route with API data 1`] = `
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`generates correct metadata for: static route 1`] = `
|
||||
exports[`generates correct metadata for: > static route 1`] = `
|
||||
{
|
||||
"canonical": "http://localhost:3000/txs",
|
||||
"description": "Open-source block explorer by Blockscout. Search transactions, verify smart contracts, analyze addresses, and track network activity. Complete blockchain data and APIs for the Blockscout (Blockscout) Explorer network.",
|
||||
@@ -2,6 +2,8 @@ import type { ApiData } from './types';
|
||||
|
||||
import type { Route } from 'nextjs-routes';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import generate from './generate';
|
||||
|
||||
interface TestCase<Pathname extends Route['pathname']> {
|
||||
@@ -4,8 +4,8 @@
|
||||
"private": false,
|
||||
"homepage": "https://github.com/blockscout/frontend#readme",
|
||||
"engines": {
|
||||
"node": "22.11.0",
|
||||
"npm": "10.9.0"
|
||||
"node": ">=22.14.0",
|
||||
"npm": ">=10.9.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "./tools/scripts/dev.sh",
|
||||
@@ -28,12 +28,11 @@
|
||||
"svg:build-sprite": "./deploy/scripts/build_sprite.sh",
|
||||
"test:pw": "./tools/scripts/pw.sh",
|
||||
"test:pw:local": "export NODE_PATH=$(pwd)/node_modules && yarn test:pw",
|
||||
"test:pw:docker": "docker run --rm --ipc=host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.49.0-noble ./tools/scripts/pw.docker.sh",
|
||||
"test:pw:docker:deps": "docker run --rm --ipc=host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.49.0-noble ./tools/scripts/pw.docker.deps.sh",
|
||||
"test:pw:docker": "docker run --rm --ipc=host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.57.0-noble ./tools/scripts/pw.docker.sh",
|
||||
"test:pw:docker:deps": "docker run --rm --ipc=host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.57.0-noble ./tools/scripts/pw.docker.deps.sh",
|
||||
"test:pw:ci": "yarn test:pw --project=$PW_PROJECT",
|
||||
"test:pw:detect-affected": "node ./deploy/tools/affected-tests/index.js",
|
||||
"test:jest": "jest",
|
||||
"test:jest:watch": "jest --watch",
|
||||
"test:vitest": "vitest",
|
||||
"favicon:generate:dev": "./tools/scripts/favicon-generator.dev.sh",
|
||||
"og-image:generate:dev": "./tools/scripts/og-image-generator.dev.sh",
|
||||
"sitemap:generate:dev": "./tools/scripts/sitemap-generator.dev.sh",
|
||||
@@ -96,7 +95,7 @@
|
||||
"dayjs": "^1.11.5",
|
||||
"dom-to-image": "^2.6.0",
|
||||
"es-toolkit": "1.39.10",
|
||||
"esbuild": "0.21.5",
|
||||
"esbuild": "0.25.12",
|
||||
"focus-visible": "^5.2.0",
|
||||
"gradient-avatar": "git+https://github.com/blockscout/gradient-avatar.git",
|
||||
"graphiql": "^4.1.2",
|
||||
@@ -137,12 +136,12 @@
|
||||
"xss": "^1.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chakra-ui/cli": "3.15.0",
|
||||
"@chakra-ui/cli": "3.30.0",
|
||||
"@eslint/compat": "1.2.2",
|
||||
"@eslint/js": "9.14.0",
|
||||
"@next/eslint-plugin-next": "15.0.3",
|
||||
"@playwright/experimental-ct-react": "1.49.0",
|
||||
"@playwright/test": "1.49.0",
|
||||
"@playwright/experimental-ct-react": "1.57.0",
|
||||
"@playwright/test": "1.57.0",
|
||||
"@stylistic/eslint-plugin": "5.2.3",
|
||||
"@svgr/webpack": "^6.5.1",
|
||||
"@tanstack/eslint-plugin-query": "5.60.1",
|
||||
@@ -152,7 +151,6 @@
|
||||
"@types/csp-dev": "^1.0.0",
|
||||
"@types/d3": "^7.4.0",
|
||||
"@types/dom-to-image": "^2.6.4",
|
||||
"@types/jest": "29.2.1",
|
||||
"@types/js-cookie": "^3.0.2",
|
||||
"@types/mixpanel-browser": "^2.38.1",
|
||||
"@types/node": "20.16.7",
|
||||
@@ -172,29 +170,28 @@
|
||||
"eslint-plugin-consistent-default-export-name": "^0.0.15",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-import-helpers": "2.0.1",
|
||||
"eslint-plugin-jest": "28.9.0",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-no-cyrillic-string": "^1.0.5",
|
||||
"eslint-plugin-playwright": "2.0.1",
|
||||
"eslint-plugin-react": "7.37.2",
|
||||
"eslint-plugin-react-hooks": "5.0.0",
|
||||
"eslint-plugin-regexp": "2.6.0",
|
||||
"eslint-plugin-vitest": "0.5.4",
|
||||
"globals": "15.12.0",
|
||||
"husky": "^8.0.0",
|
||||
"jest": "^29.2.1",
|
||||
"jest-environment-jsdom": "^29.2.1",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"jsdom": "27.3.0",
|
||||
"lint-staged": ">=10",
|
||||
"mockdate": "^3.0.5",
|
||||
"style-loader": "^3.3.1",
|
||||
"svg-icons-cli": "0.0.8",
|
||||
"svgo": "^2.8.0",
|
||||
"ts-jest": "^29.0.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "5.9.2",
|
||||
"typescript-eslint": "8.41.0",
|
||||
"vite-plugin-svgr": "^2.2.2",
|
||||
"vite-tsconfig-paths": "4.3.2",
|
||||
"vite-tsconfig-paths": "5.1.4",
|
||||
"vitest": "4.0.15",
|
||||
"vitest-fetch-mock": "0.4.5",
|
||||
"ws": "^8.17.1"
|
||||
},
|
||||
"lint-staged": {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { PlaywrightTestConfig } from '@playwright/experimental-ct-react';
|
||||
import { devices, defineConfig } from '@playwright/experimental-ct-react';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import type { Plugin } from 'esbuild';
|
||||
import svgr from 'vite-plugin-svgr';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
|
||||
@@ -61,7 +62,7 @@ const config: PlaywrightTestConfig = defineConfig({
|
||||
svgr({
|
||||
exportAsDefault: true,
|
||||
}),
|
||||
],
|
||||
] as unknown as Array<Plugin>,
|
||||
build: {
|
||||
// it actually frees some memory that vite needs a lot
|
||||
// https://github.com/storybookjs/builder-vite/issues/409#issuecomment-1152848986
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
"vite-tsconfig-paths": "5.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.11.0"
|
||||
"node": ">=22.14.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"baseUrl": ".",
|
||||
"types": ["node", "jest"],
|
||||
"types": ["node"],
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.node.ts", "**/*.tsx", "**/*.pw.tsx", "decs.d.ts", "global.d.ts"],
|
||||
"exclude": ["node_modules", "node_modules_linux", "./deploy/tools/envs-validator", "./deploy/tools/favicon-generator", "./deploy/tools/multichain-config-generator", "./deploy/tools/essential-dapps-chains-config-generator", "./toolkit/package", "./deploy/tools/llms-txt-generator"],
|
||||
|
||||
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 18 KiB |