<commit_before>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

// @flow
import * as React from 'react';
import { Provider } from 'react-redux';
import { ReactLocalization, Localized } from '@fluent/react';

import { render, screen } from '@testing-library/react';
import { lazilyParsedBundles } from 'firefox-profiler/app-logic/l10n';
import { AppLocalizationProvider } from 'firefox-profiler/components/app/AppLocalizationProvider';
import {
  getLocalization,
  getPrimaryLocale,
  getDirection,
} from 'firefox-profiler/selectors/l10n';
import {
  requestL10n,
  receiveL10n,
  togglePseudoStrategy,
} from 'firefox-profiler/actions/l10n';

import { blankStore } from '../fixtures/stores';
import { coerceMatchingShape } from '../../utils/flow';

describe('AppLocalizationProvider', () => {
  afterEach(function () {
    delete window.fetch;
  });

  function setup({
    languages,
    missingTranslation,
  }: $Shape<{| languages: string[], missingTranslation: string[] |}> = {}) {
    languages = languages ?? ['en-US'];
    missingTranslation = missingTranslation ?? [];

    const store = blankStore();
    const { dispatch, getState } = store;

    jest.spyOn(window.navigator, 'languages', 'get').mockReturnValue(languages);

    const translatedText = (language) => `This is ${language} Text`;
    const fetchUrlRe = /^\/locales\/(?<language>[^/]+)\/app.ftl$/;
    const fetch = jest.fn().mockImplementation((fetchUrl: string) => {
      const matchUrlResult = fetchUrlRe.exec(fetchUrl);
      if (matchUrlResult) {
        // $FlowExpectError Our Flow doesn't know about named groups.
        const { language } = matchUrlResult.groups;
        const response = (({
          ok: true,
          status: 200,
          headers: {
            get: () => 'text/plain',
          },
          text: () =>
            Promise.resolve(
              missingTranslation.includes(language)
                ? ``
                : `test-id = ${translatedText(language)}`
            ),
        }: any): Response);

        return Promise.resolve(response);
      }
      return Promise.reject(
        coerceMatchingShape<Response>({
          ok: false,
          status: 404,
          statusText: 'Not found',
        })
      );
    });
    (window: any).fetch = fetch;
    return { store, dispatch, getState, translatedText };
  }

  it('changes the state accordingly when the actions are dispatched', () => {
    const { dispatch, getState } = setup();
    expect(getLocalization(getState())).toEqual(new ReactLocalization([]));
    dispatch(requestL10n(['en-US']));
    const resource = [
      'locale',
      'data-testid = content-test-ApplocalizationProvider',
    ];
    const bundles = lazilyParsedBundles([resource]);
    const testLocalization = new ReactLocalization(bundles);
    dispatch(receiveL10n(testLocalization, 'en-US', 'ltr'));
    expect(getLocalization(getState())).toEqual(testLocalization);
    expect(getPrimaryLocale(getState())).toEqual('en-US');
    expect(getDirection(getState())).toEqual('ltr');
  });

  it(`renders children only once the localization is ready`, async () => {
    const { store } = setup();
    render(
      <Provider store={store}>
        <AppLocalizationProvider>
          <div data-testid="content-test-AppLocalizationProvider" />
        </AppLocalizationProvider>
      </Provider>
    );

    expect(
      screen.queryByTestId('content-test-AppLocalizationProvider')
    ).not.toBeInTheDocument();

    expect(
      await screen.findByTestId('content-test-AppLocalizationProvider')
    ).toBeInTheDocument();
  });

  it('fetches the en-US FTL strings and renders them', async () => {
    const { store, dispatch, translatedText } = setup();
    render(
      <Provider store={store}>
        <AppLocalizationProvider>
          <Localized id="test-id">
            <span>Fallback String</span>
          </Localized>
        </AppLocalizationProvider>
      </Provider>
    );

    expect(
      await screen.findByText(translatedText('en-US'))
    ).toBeInTheDocument();
    expect(document.documentElement).toHaveAttribute('lang', 'en-US');
    // $FlowExpectError Our version of flow doesn't know about document.dir
    expect(document.dir).toBe('ltr');

    // Now we're testing the LTR pseudo-localization.
    dispatch(togglePseudoStrategy('accented'));
    expect(await screen.findByText('Ŧħīş īş ḗḗƞ-ŬŞ Ŧḗḗẋŧ')).toBeInTheDocument();
    expect(document.documentElement).toHaveAttribute('lang', 'en-US');
    // $FlowExpectError Our version of flow doesn't know about document.dir
    expect(document.dir).toBe('ltr');

    // And now the RTL pseudo-localization.
    dispatch(togglePseudoStrategy('bidi'));
    expect(await screen.findByText(/⊥ɥıs ıs ǝu-∩S ⊥ǝxʇ/)).toBeInTheDocument();
    expect(document.documentElement).toHaveAttribute('lang', 'en-US');
    // $FlowExpectError Our version of flow doesn't know about document.dir
    expect(document.dir).toBe('rtl');

    // Back to no pseudo-localization
    dispatch(togglePseudoStrategy(null));
    expect(
      await screen.findByText(translatedText('en-US'))
    ).toBeInTheDocument();
    expect(document.documentElement).toHaveAttribute('lang', 'en-US');
    // $FlowExpectError Our version of flow doesn't know about document.dir
    expect(document.dir).toBe('ltr');
  });

  it('can switch the language', async () => {
    const { store, dispatch, translatedText } = setup();
    render(
      <Provider store={store}>
        <AppLocalizationProvider>
          <Localized id="test-id">
            <span>Fallback String</span>
          </Localized>
        </AppLocalizationProvider>
      </Provider>
    );

    // Start with american english
    expect(
      await screen.findByText(translatedText('en-US'))
    ).toBeInTheDocument();
    expect(document.documentElement).toHaveAttribute('lang', 'en-US');
    // $FlowExpectError Our version of flow doesn't know about document.dir
    expect(document.dir).toBe('ltr');

    // Switch to german
    dispatch(requestL10n(['de']));
    expect(await screen.findByText(translatedText('de'))).toBeInTheDocument();<commit_msg>Wrap 'dispatch' in 'act' calls in the tests for AppLocalizationProvider<commit_after>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

// @flow
import * as React from 'react';
import { Provider } from 'react-redux';
import { ReactLocalization, Localized } from '@fluent/react';

import { render, screen, act } from '@testing-library/react';
import { lazilyParsedBundles } from 'firefox-profiler/app-logic/l10n';
import { AppLocalizationProvider } from 'firefox-profiler/components/app/AppLocalizationProvider';
import {
  getLocalization,
  getPrimaryLocale,
  getDirection,
} from 'firefox-profiler/selectors/l10n';
import {
  requestL10n,
  receiveL10n,
  togglePseudoStrategy,
} from 'firefox-profiler/actions/l10n';

import { blankStore } from '../fixtures/stores';
import { coerceMatchingShape } from '../../utils/flow';

describe('AppLocalizationProvider', () => {
  afterEach(function () {
    delete window.fetch;
  });

  function setup({
    languages,
    missingTranslation,
  }: $Shape<{| languages: string[], missingTranslation: string[] |}> = {}) {
    languages = languages ?? ['en-US'];
    missingTranslation = missingTranslation ?? [];

    const store = blankStore();
    const { dispatch, getState } = store;

    jest.spyOn(window.navigator, 'languages', 'get').mockReturnValue(languages);

    const translatedText = (language) => `This is ${language} Text`;
    const fetchUrlRe = /^\/locales\/(?<language>[^/]+)\/app.ftl$/;
    const fetch = jest.fn().mockImplementation((fetchUrl: string) => {
      const matchUrlResult = fetchUrlRe.exec(fetchUrl);
      if (matchUrlResult) {
        // $FlowExpectError Our Flow doesn't know about named groups.
        const { language } = matchUrlResult.groups;
        const response = (({
          ok: true,
          status: 200,
          headers: {
            get: () => 'text/plain',
          },
          text: () =>
            Promise.resolve(
              missingTranslation.includes(language)
                ? ``
                : `test-id = ${translatedText(language)}`
            ),
        }: any): Response);

        return Promise.resolve(response);
      }
      return Promise.reject(
        coerceMatchingShape<Response>({
          ok: false,
          status: 404,
          statusText: 'Not found',
        })
      );
    });
    (window: any).fetch = fetch;

    function dispatchWithAct(action) {
      // Wrap direct dispatch actions in `act` so that React behaves as if it's
      // a single user interaction.
      return act(() => {
        dispatch(action);
      });
    }

    return { store, dispatch, dispatchWithAct, getState, translatedText };
  }

  it('changes the state accordingly when the actions are dispatched', () => {
    const { dispatch, getState } = setup();
    expect(getLocalization(getState())).toEqual(new ReactLocalization([]));
    dispatch(requestL10n(['en-US']));
    const resource = [
      'locale',
      'data-testid = content-test-ApplocalizationProvider',
    ];
    const bundles = lazilyParsedBundles([resource]);
    const testLocalization = new ReactLocalization(bundles);
    dispatch(receiveL10n(testLocalization, 'en-US', 'ltr'));
    expect(getLocalization(getState())).toEqual(testLocalization);
    expect(getPrimaryLocale(getState())).toEqual('en-US');
    expect(getDirection(getState())).toEqual('ltr');
  });

  it(`renders children only once the localization is ready`, async () => {
    const { store } = setup();
    render(
      <Provider store={store}>
        <AppLocalizationProvider>
          <div data-testid="content-test-AppLocalizationProvider" />
        </AppLocalizationProvider>
      </Provider>
    );

    expect(
      screen.queryByTestId('content-test-AppLocalizationProvider')
    ).not.toBeInTheDocument();

    expect(
      await screen.findByTestId('content-test-AppLocalizationProvider')
    ).toBeInTheDocument();
  });

  it('fetches the en-US FTL strings and renders them', async () => {
    const { store, dispatchWithAct, translatedText } = setup();
    render(
      <Provider store={store}>
        <AppLocalizationProvider>
          <Localized id="test-id">
            <span>Fallback String</span>
          </Localized>
        </AppLocalizationProvider>
      </Provider>
    );

    expect(
      await screen.findByText(translatedText('en-US'))
    ).toBeInTheDocument();
    expect(document.documentElement).toHaveAttribute('lang', 'en-US');
    // $FlowExpectError Our version of flow doesn't know about document.dir
    expect(document.dir).toBe('ltr');

    // Now we're testing the LTR pseudo-localization.
    dispatchWithAct(togglePseudoStrategy('accented'));
    expect(await screen.findByText('Ŧħīş īş ḗḗƞ-ŬŞ Ŧḗḗẋŧ')).toBeInTheDocument();
    expect(document.documentElement).toHaveAttribute('lang', 'en-US');
    // $FlowExpectError Our version of flow doesn't know about document.dir
    expect(document.dir).toBe('ltr');

    // And now the RTL pseudo-localization.
    dispatchWithAct(togglePseudoStrategy('bidi'));
    expect(await screen.findByText(/⊥ɥıs ıs ǝu-∩S ⊥ǝxʇ/)).toBeInTheDocument();
    expect(document.documentElement).toHaveAttribute('lang', 'en-US');
    // $FlowExpectError Our version of flow doesn't know about document.dir
    expect(document.dir).toBe('rtl');

    // Back to no pseudo-localization
    dispatchWithAct(togglePseudoStrategy(null));
    expect(
      await screen.findByText(translatedText('en-US'))
    ).toBeInTheDocument();
    expect(document.documentElement).toHaveAttribute('lang', 'en-US');
    // $FlowExpectError Our version of flow doesn't know about document.dir
    expect(document.dir).toBe('ltr');
  });

  it('can switch the language', async () => {
    const { store, dispatchWithAct, translatedText } = setup();
    render(
      <Provider store={store}>
        <AppLocalizationProvider>
          <Localized id="test-id">
            <span>Fallback String</span>
          </Localized>
        </AppLocalizationProvider>
      </Provider>
    );

    // Start with american english
    expect(
      await screen.findByText(translatedText('en-US'))
    ).toBeInTheDocument();
    expect(document.documentElement).toHaveAttribute('lang', 'en-US');
    // $FlowExpectError Our version of flow doesn't know about document.dir
    expect(document.dir).toBe('ltr');

    // Switch to german
    dispatchWithAct(requestL10n(['de']));
    expect(await screen.findByText(translatedText('de'))).toBeInTheDocument();
{'fraction_non_alphanumeric': 0.14075337157029919, 'fraction_numerical': 0.004030382886374206, 'mean_word_length': 3.2029315960912053, 'pattern_counts': {'":': 0, '<': 51, '<?xml version=': 0, '>': 70, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '366982', 'n_tokens_mistral': 3862, 'n_tokens_neox': 3755, 'n_words': 820}