Explorar o código

test(web): add unit test framework

Paul Armstrong %!s(int64=4) %!d(string=hai) anos
pai
achega
a803ab8577

+ 14 - 0
.github/workflows/pull_request.yml

@@ -30,3 +30,17 @@ jobs:
       - name: Build
         run: npm run build
         working-directory: ./web
+
+  web_test:
+    name: Web - Test
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@master
+      - uses: actions/setup-node@master
+        with:
+          node-version: 14.x
+      - run: npm install
+        working-directory: ./web
+      - name: Test
+        run: npm run test
+        working-directory: ./web

+ 1 - 0
.gitignore

@@ -9,3 +9,4 @@ models
 frigate/version.py
 web/build
 web/node_modules
+web/coverage

+ 17 - 2
web/.eslintrc.js

@@ -9,8 +9,14 @@ module.exports = {
     },
   },
 
-  extends: ['prettier', 'preact', 'plugin:import/react'],
-  plugins: ['import'],
+  extends: [
+    'prettier',
+    'preact',
+    'plugin:import/react',
+    'plugin:testing-library/recommended',
+    'plugin:jest/recommended',
+  ],
+  plugins: ['import', 'testing-library', 'jest'],
 
   env: {
     es6: true,
@@ -113,6 +119,15 @@ module.exports = {
     'import/no-unresolved': 'error',
 
     'react-hooks/exhaustive-deps': 'error',
+
+    'jest/consistent-test-it': ['error', { fn: 'test' }],
+    'jest/no-test-prefixes': 'error',
+    'jest/no-restricted-matchers': [
+      'error',
+      { toMatchSnapshot: 'Use `toMatchInlineSnapshot()` and ensure you only snapshot very small elements' },
+    ],
+    'jest/valid-describe': 'error',
+    'jest/valid-expect-in-promise': 'error',
   },
 
   settings: {

+ 1 - 1
web/babel.config.js

@@ -1,4 +1,4 @@
 module.exports = {
   presets: ['@babel/preset-env'],
-  plugins: [['@babel/plugin-transform-react-jsx', { pragma: 'preact.h' }]],
+  plugins: [['@babel/plugin-transform-react-jsx', { pragma: 'h' }]],
 };

+ 14 - 0
web/config/setupTests.js

@@ -0,0 +1,14 @@
+import 'regenerator-runtime/runtime';
+import '@testing-library/jest-dom/extend-expect';
+
+Object.defineProperty(window, 'matchMedia', {
+  writable: true,
+  value: (query) => ({
+    matches: false,
+    media: query,
+    onchange: null,
+    addEventListener: jest.fn(),
+    removeEventListener: jest.fn(),
+    dispatchEvent: jest.fn(),
+  }),
+});

+ 9 - 0
web/jest.config.js

@@ -0,0 +1,9 @@
+module.exports = {
+  moduleFileExtensions: ['js', 'jsx'],
+  name: 'react-component-benchmark',
+  resetMocks: true,
+  roots: ['<rootDir>'],
+  setupFilesAfterEnv: ['<rootDir>/config/setupTests.js'],
+  testEnvironment: 'jsdom',
+  timers: 'fake',
+};

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 1210 - 753
web/package-lock.json


+ 7 - 1
web/package.json

@@ -6,7 +6,8 @@
     "prebuild": "rimraf build",
     "build": "cross-env NODE_ENV=production SNOWPACK_MODE=production SNOWPACK_PUBLIC_API_HOST='' snowpack build",
     "lint": "npm run lint:cmd -- --fix",
-    "lint:cmd": "eslint ./ --ext .jsx,.js"
+    "lint:cmd": "eslint ./ --ext .jsx,.js",
+    "test": "jest"
   },
   "dependencies": {
     "idb-keyval": "^5.0.2",
@@ -21,12 +22,17 @@
     "@babel/preset-env": "^7.12.13",
     "@prefresh/snowpack": "^3.0.1",
     "@snowpack/plugin-postcss": "^1.1.0",
+    "@testing-library/jest-dom": "^5.11.9",
+    "@testing-library/preact": "^2.0.1",
     "autoprefixer": "^10.2.1",
     "cross-env": "^7.0.3",
     "eslint": "^7.19.0",
     "eslint-config-preact": "^1.1.3",
     "eslint-config-prettier": "^7.2.0",
     "eslint-plugin-import": "^2.22.1",
+    "eslint-plugin-jest": "^24.1.3",
+    "eslint-plugin-testing-library": "^3.10.1",
+    "jest": "^26.6.3",
     "postcss": "^8.2.2",
     "postcss-cli": "^8.3.1",
     "prettier": "^2.2.1",

+ 217 - 0
web/src/context/__tests__/index.test.jsx

@@ -0,0 +1,217 @@
+import { h } from 'preact';
+import * as IDB from 'idb-keyval';
+import { DarkModeProvider, useDarkMode, usePersistence } from '..';
+import { fireEvent, render, screen } from '@testing-library/preact';
+import { useCallback } from 'preact/hooks';
+
+function DarkModeChecker() {
+  const { currentMode } = useDarkMode();
+  return <div data-testid={currentMode}>{currentMode}</div>;
+}
+
+describe('DarkMode', () => {
+  let MockIDB;
+  beforeEach(() => {
+    MockIDB = {
+      get: jest.spyOn(IDB, 'get').mockImplementation(() => Promise.resolve(undefined)),
+      set: jest.spyOn(IDB, 'set').mockImplementation(() => Promise.resolve(true)),
+    };
+  });
+
+  test('uses media by default', async () => {
+    render(
+      <DarkModeProvider>
+        <DarkModeChecker />
+      </DarkModeProvider>
+    );
+    const el = await screen.findByTestId('media');
+    expect(el).toBeInTheDocument();
+  });
+
+  test('uses the mode stored in idb - dark', async () => {
+    MockIDB.get.mockResolvedValue('dark');
+    render(
+      <DarkModeProvider>
+        <DarkModeChecker />
+      </DarkModeProvider>
+    );
+    const el = await screen.findByTestId('dark');
+    expect(el).toBeInTheDocument();
+    expect(document.body.classList.contains('dark')).toBe(true);
+  });
+
+  test('uses the mode stored in idb - light', async () => {
+    MockIDB.get.mockResolvedValue('light');
+    render(
+      <DarkModeProvider>
+        <DarkModeChecker />
+      </DarkModeProvider>
+    );
+    const el = await screen.findByTestId('light');
+    expect(el).toBeInTheDocument();
+    expect(document.body.classList.contains('dark')).toBe(false);
+  });
+
+  test('allows updating the mode', async () => {
+    MockIDB.get.mockResolvedValue('dark');
+
+    function Updater() {
+      const { setDarkMode } = useDarkMode();
+      const handleClick = useCallback(() => {
+        setDarkMode('light');
+      }, [setDarkMode]);
+      return <div onClick={handleClick}>click me</div>;
+    }
+
+    render(
+      <DarkModeProvider>
+        <DarkModeChecker />
+        <Updater />
+      </DarkModeProvider>
+    );
+
+    const dark = await screen.findByTestId('dark');
+    expect(dark).toBeInTheDocument();
+    expect(document.body.classList.contains('dark')).toBe(true);
+
+    const button = await screen.findByText('click me');
+    fireEvent.click(button);
+
+    const light = await screen.findByTestId('light');
+    expect(light).toBeInTheDocument();
+    expect(document.body.classList.contains('dark')).toBe(false);
+  });
+
+  test('when using media, matches on preference', async () => {
+    MockIDB.get.mockResolvedValue('media');
+    jest.spyOn(window, 'matchMedia').mockImplementation((query) => {
+      if (query === '(prefers-color-scheme: dark)') {
+        return { matches: true, addEventListener: jest.fn(), removeEventListener: jest.fn() };
+      }
+
+      throw new Error(`Unexpected query to matchMedia: ${query}`);
+    });
+    render(
+      <DarkModeProvider>
+        <DarkModeChecker />
+      </DarkModeProvider>
+    );
+
+    const el = await screen.findByTestId('dark');
+    expect(el).toBeInTheDocument();
+    expect(document.body.classList.contains('dark')).toBe(true);
+  });
+});
+
+describe('usePersistence', () => {
+  let MockIDB;
+  beforeEach(() => {
+    MockIDB = {
+      get: jest.spyOn(IDB, 'get').mockImplementation(() => Promise.resolve(undefined)),
+      set: jest.spyOn(IDB, 'set').mockImplementation(() => Promise.resolve(true)),
+    };
+  });
+
+  test('returns a defaultValue initially', async () => {
+    MockIDB.get.mockImplementationOnce(
+      () =>
+        new Promise((resolve) => {
+          setTimeout(() => {
+            resolve('foo');
+          }, 1);
+        })
+    );
+
+    function Component() {
+      const [value, , loaded] = usePersistence('tacos', 'my-default');
+      return (
+        <div>
+          <div data-testid="loaded">{loaded ? 'loaded' : 'not loaded'}</div>
+          <div data-testid="value">{value}</div>
+        </div>
+      );
+    }
+
+    render(<Component />);
+
+    expect(screen.getByTestId('loaded')).toMatchInlineSnapshot(`
+      <div
+        data-testid="loaded"
+      >
+        not loaded
+      </div>
+    `);
+    expect(screen.getByTestId('value')).toMatchInlineSnapshot(`
+      <div
+        data-testid="value"
+      >
+        my-default
+      </div>
+    `);
+
+    jest.runAllTimers();
+  });
+
+  test('updates with the previously-persisted value', async () => {
+    MockIDB.get.mockResolvedValue('are delicious');
+
+    function Component() {
+      const [value, , loaded] = usePersistence('tacos', 'my-default');
+      return (
+        <div>
+          <div data-testid="loaded">{loaded ? 'loaded' : 'not loaded'}</div>
+          <div data-testid="value">{value}</div>
+        </div>
+      );
+    }
+
+    render(<Component />);
+
+    await screen.findByText('loaded');
+
+    expect(screen.getByTestId('loaded')).toMatchInlineSnapshot(`
+      <div
+        data-testid="loaded"
+      >
+        loaded
+      </div>
+    `);
+    expect(screen.getByTestId('value')).toMatchInlineSnapshot(`
+      <div
+        data-testid="value"
+      >
+        are delicious
+      </div>
+    `);
+  });
+
+  test('can be updated manually', async () => {
+    MockIDB.get.mockResolvedValue('are delicious');
+
+    function Component() {
+      const [value, setValue] = usePersistence('tacos', 'my-default');
+      const handleClick = useCallback(() => {
+        setValue('super delicious');
+      }, [setValue]);
+      return (
+        <div>
+          <div onClick={handleClick}>click me</div>
+          <div data-testid="value">{value}</div>
+        </div>
+      );
+    }
+
+    render(<Component />);
+
+    const button = await screen.findByText('click me');
+    fireEvent.click(button);
+
+    expect(screen.getByTestId('value')).toMatchInlineSnapshot(`
+      <div
+        data-testid="value"
+      >
+        super delicious
+      </div>
+    `);
+  });
+});

+ 1 - 3
web/src/context/index.jsx

@@ -12,9 +12,7 @@ export function DarkModeProvider({ children }) {
     (value) => {
       setPersistedMode(value);
       setData('darkmode', value);
-      if (value !== 'media') {
-        setCurrentMode(value);
-      }
+      setCurrentMode(value);
     },
     [setPersistedMode]
   );

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio