Skip to content

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

Shell модуля работает внутри Building OS, и всё, что модуль берёт у платформы, ему даёт оболочка. В тесте этого никто не даёт, поэтому то же самое делает сам тест:

В Building OSВ вашем тесте
Шапка публикует выбранное здание, useSelectedBuildingId() его читаетЗапишите такой же выбор по ключу BUILDING_SELECTION_STORAGE_KEY
authorizationHeader() несёт токен вошедшего пользователяТокена нет ({}); подмените fetch
Каждый модуль обёрнут в QueryClientProviderОберните компонент в собственный, с выключенными повторами
Язык задаёт оболочкаПереключите свой экземпляр i18next через changeLanguage()

Страница продолжает шаг 6 раздела Ваш первый модуль и использует его Shell. Код проверен на свежем каркасе --external из SDK 1.15.0.

Подготовка ​

Vitest в каркасе работает в Node, без DOM и без библиотеки для тестирования компонентов. Добавьте их (-w в каркасе обязателен: без него pnpm-workspace.yaml не даёт добавлять пакеты в корень):

bash
pnpm add -D -w @testing-library/react @testing-library/dom @testing-library/jest-dom jsdom

Каждый тестовый файл, который отрисовывает компонент, начинайте со строки // @vitest-environment jsdom и импортируйте @testing-library/jest-dom/vitest — ради матчеров вроде toBeInTheDocument().

Две вспомогательные функции ​

То, что нужно каждому тесту Shell, вынесите в один файл:

tsx
// src/test/render.tsx
import type { ReactElement } from 'react';
import { render } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BUILDING_SELECTION_STORAGE_KEY } from '@tv/extension-sdk/context';

/** Publish a building selection the way the Building OS header does. */
export function selectBuilding(id: string, name = id) {
  localStorage.setItem(
    BUILDING_SELECTION_STORAGE_KEY,
    JSON.stringify({ state: { selectedBuildingId: id, buildings: [{ id, name }] } }),
  );
}

/** Render inside a fresh query client, with retries off so a failure shows at once. */
export function renderWithQueryClient(ui: ReactElement) {
  const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
  return render(<QueryClientProvider client={client}>{ui}</QueryClientProvider>);
}

BUILDING_SELECTION_STORAGE_KEY экспортируется ровно для этого — чтобы записать выбор здания вне оболочки. В коде модуля читайте выбор только через useSelectedBuildingId() или useBuildingSelection(), а не по ключу: ключ может измениться вместе с оболочкой.

Подмените API и проследите за сменой здания ​

vi.stubGlobal('fetch', …) подменяет fetch на время одного теста; отвечайте на каждый адрес, который вызывает модуль. Событие BUILDING_SELECTION_CHANGE_EVENT — то, чем шапка сообщает модулям о смене выбора:

tsx
// @vitest-environment jsdom
import '@testing-library/jest-dom/vitest';
import { afterEach, beforeAll, beforeEach, expect, test, vi } from 'vitest';
import { act, cleanup, screen } from '@testing-library/react';
import { BUILDING_SELECTION_CHANGE_EVENT } from '@tv/extension-sdk/context';
import i18n from './i18n';
import Shell from './Shell';
import { renderWithQueryClient, selectBuilding } from './test/render';

/** Answer /buildings/<id>/spaces with the given totals, anything else with 404. */
function stubSpaces(totals: Record<string, number>) {
  const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
    const match = /\/buildings\/([^/]+)\/spaces$/.exec(String(input));
    const total = match ? totals[decodeURIComponent(match[1])] : undefined;
    return total === undefined
      ? new Response('not found', { status: 404 })
      : Response.json({ data: [], total, limit: 50, offset: 0 });
  });
  vi.stubGlobal('fetch', fetchMock);
  return fetchMock;
}

beforeAll(async () => {
  await i18n.changeLanguage('ru');
});

beforeEach(() => {
  localStorage.clear();
});

afterEach(() => {
  cleanup();
  vi.unstubAllGlobals();
});

test('says so when the API call fails', async () => {
  selectBuilding('b1');
  stubSpaces({});
  renderWithQueryClient(<Shell />);
  expect(await screen.findByText('Не удалось загрузить помещения.')).toBeInTheDocument();
});

test('follows the building the user picks in the header', async () => {
  selectBuilding('b1');
  stubSpaces({ b1: 3, b2: 5 });
  renderWithQueryClient(<Shell />);
  expect(await screen.findByText('3 помещения')).toBeInTheDocument();

  act(() => {
    selectBuilding('b2');
    window.dispatchEvent(new Event(BUILDING_SELECTION_CHANGE_EVENT));
  });
  expect(await screen.findByText('5 помещений')).toBeInTheDocument();
});

i18n.changeLanguage('ru') делает тест независимым от содержимого хранилища. Для тестов английского каталога переключайтесь на 'en'.

createMockPlatformContext ​

createMockPlatformContext() из @tv/extension-sdk/testing собирает PlatformContext целиком, с реалистичными значениями по умолчанию: менеджер эксплуатации ТРЦ смотрит своё здание на английском в светлой теме. src/main.tsx каркаса использует его для автономного превью.

В тестах модуля он нужен редко. Собственный код модуля не должен читать контекст: usePlatformContext() и построенные на нём хуки бросают исключение внутри Building OS (см. PlatformContext). Поэтому тест, который оборачивает Shell в <PlatformProvider>, ничего не доказывает про оболочку: useSelectedBuildingId() провайдер не читает.

Переопределения поверхностные

createMockPlatformContext(overrides) принимает Partial<PlatformContext> — поверхностный, а не глубокий. Какой бы ключ верхнего уровня вы ни переопределяли, передавайте его целиком. Переопределение building требует всех полей PlatformBuilding (id, slug, name, type, timezone); частичное — ошибка типизации.

Чтобы поменять одно поле, разложите значение по умолчанию вместо ручной сборки:

ts
import { createMockPlatformContext } from '@tv/extension-sdk/testing';

const base = createMockPlatformContext();
export const ctx = createMockPlatformContext({
  building: { ...base.building!, name: 'Demo Mall' },
  user: { ...base.user, roles: ['manager'] },
});

Чек-лист самодиагностики ​

Прежде чем писать нам, пройдите по нему — большинство проблем решается здесь:

  1. Какая версия SDK? cat node_modules/@tv/extension-sdk/package.json | jq .version
  2. Манифест валиден? npx @tv/extension-sdk validate module-manifest.json
  3. Экспорт федерации верный? npx @tv/extension-sdk check-exposes module-manifest.json
  4. Подписки совместимы с издателями? npx @tv/extension-sdk check-pact module-manifest.json
  5. Используете символ @experimental? Просмотрите импорты — такие символы могут меняться между релизами.
  6. Сообщается ли о работоспособности (heartbeat)? Проверьте точку вашего модуля на боковой панели Building OS.
  7. Тесты отрисовывают Shell так же, как Building OS? Выбор здания записан, fetch подменён, клиент запросов есть, и код не зависит от <PlatformProvider>.

Если все семь пунктов в порядке, а проблема остаётся, напишите контакту, указанному в приветственном сообщении, на почту или в Telegram и приложите вывод чек-листа; адрес developers@tango.vision доходит до тех же людей.

Когда автономно работает, а в оболочке ломается ​

Это отдельный класс отказов, и ни одна из семи проверок выше его не видит: ваши тесты, сборка и образ зелены по построению — автономному модулю просто не с чем конфликтовать. Три вопроса по порядку:

  1. Упоминается ли в ошибке useSyncExternalStore или null-диспетчер? Ваш remote увозит вторую копию React. У вас не проставлен алиас use-sync-external-store/shim.
  2. Показывает ли оболочка сырые ключи i18n (structure.title), пока ваш модуль отрисовывается нормально? Вы поделились react-i18next без i18next и заменили экземпляр по умолчанию у хоста.
  3. Бросает ли исключение usePlatformContext()? Он не пересекает границу федерации — см. PlatformContext.

Все три случая разобраны в разделе Федерация и оболочка. Внутри Tango Vision первые два ловит CI-проверка shell-smoke ещё до попадания на стенд. Она лежит в закрытом репозитории, и ваш CI её запустить не может: загружайте модуль в оболочку своей песочницы после каждого изменения vite.config.ts, настройки i18n или общей зависимости.

Создано на платформе Tango Vision. Вопросы? developers@tango.vision