@testing-library/react#queryByText TypeScript Examples

The following examples show how to use @testing-library/react#queryByText. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: Settings.spec.tsx    From apps with GNU Affero General Public License v3.0 6 votes vote down vote up
it('should mutate feed sorting enabled setting', () =>
  testSettingsMutation({ sortingEnabled: true }, async () => {
    const checkboxes = await screen.findAllByRole('checkbox');
    const checkbox = checkboxes.find((el) =>
      // eslint-disable-next-line testing-library/no-node-access, testing-library/prefer-screen-queries
      queryByText(el.parentElement, 'Show feed sorting menu'),
    ) as HTMLInputElement;
    fireEvent.click(checkbox);
  }));
Example #2
Source File: UMLEditor.test.tsx    From legend-studio with Apache License 2.0 6 votes vote down vote up
test(integrationTest('Enumeration editor'), async () => {
  await TEST__openElementFromExplorerTree('ui::TestEnumeration', renderResult);
  const editPanelHeader = renderResult.getByTestId(
    LEGEND_STUDIO_TEST_ID.EDIT_PANEL__HEADER_TABS,
  );
  expect(getByText(editPanelHeader, 'TestEnumeration')).not.toBeNull();
  const enumerationEditor = renderResult.getByTestId(
    LEGEND_STUDIO_TEST_ID.ENUMERATION_EDITOR,
  );
  const enums = ['enumA', 'enumB', 'enumC'];
  enums.forEach((e) => getByDisplayValue(enumerationEditor, e));
  fireEvent.click(getByText(enumerationEditor, 'Tagged Values'));
  await waitFor(() => getByText(enumerationEditor, 'ProfileTest'));
  getByDisplayValue(enumerationEditor, 'Enumeration Tag');
  fireEvent.click(getByText(enumerationEditor, 'Stereotypes'));
  await waitFor(() => getByText(enumerationEditor, 'stereotype2'));
  fireEvent.click(getByText(enumerationEditor, 'Values'));
  await waitFor(() => getByDisplayValue(enumerationEditor, 'enumA'));
  const enumB = getByDisplayValue(enumerationEditor, 'enumA');
  const parentElement = enumB.parentElement?.parentElement as HTMLElement;
  const buttons = queryAllByRole(parentElement, 'button');
  expect(buttons).toHaveLength(2);
  fireEvent.click(guaranteeNonNullable(buttons[0])); // navigate
  await waitFor(() => getByText(enumerationEditor, 'enum'));
  const subPropertyPanel = getByTestId(
    enumerationEditor,
    LEGEND_STUDIO_TEST_ID.PANEL,
  );
  getByDisplayValue(subPropertyPanel, 'enumATag');
  fireEvent.click(getByText(subPropertyPanel, 'Stereotypes'));
  await waitFor(() => getByText(subPropertyPanel, 'stereotype1'));
  fireEvent.click(
    guaranteeNonNullable(queryAllByRole(subPropertyPanel, 'button')[0]),
  );
  fireEvent.click(guaranteeNonNullable(buttons[1])); // delete
  expect(queryByText(enumerationEditor, 'enumA')).toBeNull();
});
Example #3
Source File: Settings.spec.tsx    From apps with GNU Affero General Public License v3.0 6 votes vote down vote up
it('should mutate Show custom shortcuts setting in extension', async () => {
  process.env.TARGET_BROWSER = 'chrome';
  renderComponent();

  let mutationCalled = false;
  mockGraphQL({
    request: {
      query: UPDATE_USER_SETTINGS_MUTATION,
      variables: { data: { ...defaultSettings, showTopSites: false } },
    },
    result: () => {
      mutationCalled = true;
      return { data: { updateUserSettings: { updatedAt: new Date(0) } } };
    },
  });

  const checkboxes = await screen.findAllByRole('checkbox');
  const checkbox = checkboxes.find((el) =>
    // eslint-disable-next-line testing-library/no-node-access, testing-library/prefer-screen-queries
    queryByText(el.parentElement, 'Show custom shortcuts'),
  ) as HTMLInputElement;

  await waitFor(() => expect(checkbox).toBeInTheDocument());
  await waitFor(() => expect(checkbox).toBeChecked());
  fireEvent.click(checkbox);

  await waitFor(() => expect(mutationCalled).toBeTruthy());
});
Example #4
Source File: Settings.spec.tsx    From apps with GNU Affero General Public License v3.0 6 votes vote down vote up
it('should mutate show weekly goals widget setting', () =>
  testSettingsMutation({ optOutWeeklyGoal: false }, async () => {
    const checkboxes = await screen.findAllByRole('checkbox');
    const checkbox = checkboxes.find((el) =>
      // eslint-disable-next-line testing-library/no-node-access, testing-library/prefer-screen-queries
      queryByText(el.parentElement, 'Show Weekly Goal widget'),
    ) as HTMLInputElement;
    fireEvent.click(checkbox);
  }));
Example #5
Source File: Settings.spec.tsx    From apps with GNU Affero General Public License v3.0 6 votes vote down vote up
it('should mutate open links in new tab setting', () =>
  testSettingsMutation({ openNewTab: true }, async () => {
    const checkboxes = await screen.findAllByRole('checkbox');
    const checkbox = checkboxes.find((el) =>
      // eslint-disable-next-line testing-library/no-node-access, testing-library/prefer-screen-queries
      queryByText(el.parentElement, 'Open links in new tab'),
    ) as HTMLInputElement;
    fireEvent.click(checkbox);
  }));
Example #6
Source File: Settings.spec.tsx    From apps with GNU Affero General Public License v3.0 6 votes vote down vote up
it('should mutate hide read posts setting', () =>
  testSettingsMutation({ showOnlyUnreadPosts: false }, async () => {
    const checkboxes = await screen.findAllByRole('checkbox');
    const checkbox = checkboxes.find((el) =>
      // eslint-disable-next-line testing-library/no-node-access, testing-library/prefer-screen-queries
      queryByText(el.parentElement, 'Hide read posts'),
    ) as HTMLInputElement;
    fireEvent.click(checkbox);
  }));
Example #7
Source File: Settings.spec.tsx    From apps with GNU Affero General Public License v3.0 6 votes vote down vote up
it('should set light to dark mode setting', () =>
  testSettingsMutation(
    { theme: 'bright' },
    async () => {
      const radios = await screen.findAllByRole('radio');
      const radio = radios.find((el) =>
        // eslint-disable-next-line testing-library/no-node-access, testing-library/prefer-screen-queries
        queryByText(el.parentElement, 'Light'),
      ) as HTMLInputElement;
      fireEvent.click(radio);
    },
    { ...defaultSettings, theme: 'darcula' },
  ));
Example #8
Source File: Settings.spec.tsx    From apps with GNU Affero General Public License v3.0 6 votes vote down vote up
it('should set theme to dark mode setting', () =>
  testSettingsMutation({ theme: 'darcula' }, async () => {
    const radios = await screen.findAllByRole('radio');
    const radio = radios.find((el) =>
      // eslint-disable-next-line testing-library/no-node-access, testing-library/prefer-screen-queries
      queryByText(el.parentElement, 'Dark'),
    ) as HTMLInputElement;
    fireEvent.click(radio);
  }));
Example #9
Source File: Settings.spec.tsx    From apps with GNU Affero General Public License v3.0 6 votes vote down vote up
it('should utilize local cache settings for anonymous users', async () => {
  const localBootData = {
    ...defaultBootData,
    settings: { ...defaultBootData.settings, theme: 'cozy' },
  };
  localStorage.setItem(BOOT_LOCAL_KEY, JSON.stringify(localBootData));
  renderBootProvider(defaultBootData);

  const radio = await screen.findAllByRole('radio');
  await waitFor(() =>
    expect(
      // eslint-disable-next-line testing-library/no-node-access, testing-library/prefer-screen-queries
      radio.find((el) => queryByText(el.parentElement, 'Cozy')),
    ).toBeChecked(),
  );
});
Example #10
Source File: Settings.spec.tsx    From apps with GNU Affero General Public License v3.0 6 votes vote down vote up
it('should utilize front-end default settings for first time users', async () => {
  renderBootProvider();

  const radio = await screen.findAllByRole('radio');
  await waitFor(() =>
    expect(
      // eslint-disable-next-line testing-library/no-node-access, testing-library/prefer-screen-queries
      radio.find((el) => queryByText(el.parentElement, 'Eco')),
    ).toBeChecked(),
  );
});
Example #11
Source File: Settings.spec.tsx    From apps with GNU Affero General Public License v3.0 6 votes vote down vote up
testSettingsMutation = async (
  settings: Partial<RemoteSettings>,
  updateFunc: () => Promise<void>,
  initialSettings = defaultSettings,
): Promise<void> => {
  renderComponent(defaultUser, initialSettings);

  let mutationCalled = false;
  mockGraphQL({
    request: {
      query: UPDATE_USER_SETTINGS_MUTATION,
      variables: { data: { ...defaultSettings, ...settings } },
    },
    result: () => {
      mutationCalled = true;
      return { data: { updateUserSettings: { updatedAt: new Date(0) } } };
    },
  });

  if (initialSettings.theme === 'bright') {
    const radio = await screen.findAllByRole('radio');
    await waitFor(() =>
      expect(
        // eslint-disable-next-line testing-library/no-node-access, testing-library/prefer-screen-queries
        radio.find((el) => queryByText(el.parentElement, 'Light')),
      ).toBeChecked(),
    );
  }

  await updateFunc();
  await waitFor(() => expect(mutationCalled).toBeTruthy());
}
Example #12
Source File: AsyncDualSelectWidget.test.tsx    From ke with MIT License 6 votes vote down vote up
test('It should move the item from the left list to the right list', async () => {
  const screen = render(<AsyncDualSelectWidget {...(defaultProps as any)} />)

  await waitFor(() => screen.findByText('test'))

  const leftList = screen.queryByTestId('ds-left-list')
  await waitFor(() => findByText(leftList as HTMLElement, 'test'))
  expect(leftList?.textContent).toContain('test')

  const testOption = screen.getByText('test')
  const selectButton = screen.getByText('SELECT')
  fireEvent.click(testOption)
  fireEvent.click(selectButton)

  const rightList = screen.queryByTestId('ds-right-list')
  expect(getByText(rightList as HTMLElement, 'test')).not.toBeNull()
  expect(queryByText(leftList as HTMLElement, 'test')).toBeNull()
})
Example #13
Source File: Settings.spec.tsx    From apps with GNU Affero General Public License v3.0 5 votes vote down vote up
it('should mutate density setting', () =>
  testSettingsMutation({ spaciness: 'cozy' }, async () => {
    const radio = await screen.findAllByRole('radio');
    // eslint-disable-next-line testing-library/no-node-access, testing-library/prefer-screen-queries
    const cozy = radio.find((el) => queryByText(el.parentElement, 'Cozy'));
    fireEvent.click(cozy);
  }));
Example #14
Source File: Settings.spec.tsx    From apps with GNU Affero General Public License v3.0 5 votes vote down vote up
it('should fetch remote settings', async () => {
  renderComponent();

  const radio = await screen.findAllByRole('radio');
  await waitFor(() =>
    expect(
      // eslint-disable-next-line testing-library/no-node-access, testing-library/prefer-screen-queries
      radio.find((el) => queryByText(el.parentElement, 'Roomy')),
    ).toBeChecked(),
  );
  await waitFor(() =>
    expect(
      // eslint-disable-next-line testing-library/no-node-access, testing-library/prefer-screen-queries
      radio.find((el) => queryByText(el.parentElement, 'Light')),
    ).toBeChecked(),
  );

  const checkbox = await screen.findAllByRole('checkbox');

  await waitFor(() =>
    expect(
      // eslint-disable-next-line testing-library/no-node-access, testing-library/prefer-screen-queries
      checkbox.find((el) => queryByText(el.parentElement, 'Hide read posts')),
    ).toBeChecked(),
  );
  await waitFor(() =>
    expect(
      checkbox.find((el) =>
        // eslint-disable-next-line testing-library/no-node-access, testing-library/prefer-screen-queries
        queryByText(el.parentElement, 'Open links in new tab'),
      ),
    ).not.toBeChecked(),
  );

  await waitFor(() =>
    expect(
      checkbox.find((el) =>
        // eslint-disable-next-line testing-library/no-node-access, testing-library/prefer-screen-queries
        queryByText(el.parentElement, 'Show feed sorting menu'),
      ),
    ).not.toBeChecked(),
  );
});
Example #15
Source File: Settings.spec.tsx    From apps with GNU Affero General Public License v3.0 5 votes vote down vote up
it('should not have the Show custom shortcuts switch in the webapp', async () => {
  renderComponent(null);
  const checkbox = screen.queryByText('Show custom shortcuts');
  expect(checkbox).not.toBeInTheDocument();
});
Example #16
Source File: SharedBookmarksModal.spec.tsx    From apps with GNU Affero General Public License v3.0 5 votes vote down vote up
it('should enable public mode on toggle click', async () => {
  let mutationCalled = false;
  renderComponent([
    {
      request: {
        query: BOOKMARK_SHARING_QUERY,
      },
      result: {
        data: {
          bookmarksSharing: {
            enabled: false,
            slug: '',
            rssUrl: '',
          },
        },
      },
    },
    {
      request: {
        query: BOOKMARK_SHARING_MUTATION,
        variables: { enabled: true },
      },
      result: () => {
        mutationCalled = true;
        return {
          data: {
            bookmarksSharing: {
              enabled: true,
              slug: '619f6044-c02b-486b-8234-9a46ad1bb604',
              rssUrl:
                'http://localhost:4000/rss/b/619f6044-c02b-486b-8234-9a46ad1bb604',
            },
          },
        };
      },
    },
  ]);
  // eslint-disable-next-line testing-library/no-node-access, testing-library/prefer-screen-queries
  const checkboxes = await screen.findAllByRole('checkbox');
  const checkbox = checkboxes.find((el) =>
    // eslint-disable-next-line testing-library/no-node-access, testing-library/prefer-screen-queries
    queryByText(el.parentElement, 'Public mode'),
  ) as HTMLInputElement;
  fireEvent.click(checkbox);
  await waitFor(() => expect(mutationCalled).toBeTruthy());
  const input = screen.getByDisplayValue(
    'http://localhost:4000/rss/b/619f6044-c02b-486b-8234-9a46ad1bb604',
  );
  await waitFor(() => expect(input).toBeInTheDocument());
});
Example #17
Source File: EditPanel.test.tsx    From legend-studio with Apache License 2.0 5 votes vote down vote up
test(integrationTest('Test navigation between element states'), async () => {
  // Test opening multiple elements
  await TEST__openElementFromExplorerTree('ui::test1::Animal', renderResult);
  const packageExplorer = renderResult.getByTestId(
    LEGEND_STUDIO_TEST_ID.EXPLORER_TREES,
  );
  fireEvent.click(getByText(packageExplorer, 'TestClass'));
  fireEvent.click(getByText(packageExplorer, 'TestEnumeration'));
  fireEvent.click(getByText(packageExplorer, 'Anyone'));
  fireEvent.click(getByText(packageExplorer, 'Dog'));
  fireEvent.click(getByText(packageExplorer, 'Something'));
  fireEvent.click(getByText(packageExplorer, 'ProfileTest'));
  const editPanelHeader = renderResult.getByTestId(
    LEGEND_STUDIO_TEST_ID.EDIT_PANEL__HEADER_TABS,
  );
  await waitFor(() => getByText(editPanelHeader, 'ProfileTest'));

  const openElements = [
    'TestEnumeration',
    'TestClass',
    'Anyone',
    'Dog',
    'Something',
    'ProfileTest',
  ];
  openElements.forEach((openEl) => getByText(editPanelHeader, openEl));

  // navigate through visit buttons
  fireEvent.click(getByText(editPanelHeader, 'TestClass'));
  await waitFor(() => renderResult.getByText('founder'));
  const navigateToClass = async (className: string): Promise<void> => {
    const classForm = renderResult.getByTestId(
      LEGEND_STUDIO_TEST_ID.CLASS_FORM_EDITOR,
    );
    const property = await waitFor(() => getByText(classForm, className));
    const propertyBasicEditor = property.parentElement as HTMLElement;
    const navigateButton = getByTestId(
      propertyBasicEditor,
      LEGEND_STUDIO_TEST_ID.TYPE_VISIT,
    );
    fireEvent.click(navigateButton);
    await waitFor(() => getByText(editPanelHeader, className));
  };

  await navigateToClass('Person');
  await navigateToClass('Firm');
  await navigateToClass('Person');
  await navigateToClass('Degree');
  const newOpened = ['Firm', 'Degree', 'Person'];
  openElements
    .concat(newOpened)
    .forEach((openElement) => getByText(editPanelHeader, openElement));

  // test closing of tabs
  const closeTabs = ['Firm', 'Degree', 'TestEnumeration'];
  closeTabs.forEach((tab) => {
    const text = getByText(editPanelHeader, tab);
    const parent = text.parentElement as HTMLElement;
    const deleteButton = getByTitle(parent, 'Close');
    fireEvent.click(deleteButton);
  });
  closeTabs.forEach((tab) =>
    expect(queryByText(editPanelHeader, tab)).toBeNull(),
  );
  // TODO Add Diff Element States
});
Example #18
Source File: OwnershipCard.test.tsx    From backstage with Apache License 2.0 4 votes vote down vote up
describe('OwnershipCard', () => {
  const groupEntity: GroupEntity = {
    apiVersion: 'backstage.io/v1alpha1',
    kind: 'Group',
    metadata: {
      name: 'my-team',
    },
    spec: {
      type: 'team',
      children: [],
    },
    relations: [
      {
        type: 'memberOf',
        targetRef: 'group:default/examplegroup',
      },
    ],
  };

  it('displays entity counts', async () => {
    const catalogApi: jest.Mocked<CatalogApi> = {
      getEntities: jest.fn(),
    } as any;

    catalogApi.getEntities.mockImplementation(getEntitiesMock);

    const { getByText } = await renderInTestApp(
      <TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
        <EntityProvider entity={groupEntity}>
          <OwnershipCard />
        </EntityProvider>
      </TestApiProvider>,
      {
        mountedRoutes: {
          '/create': catalogIndexRouteRef,
        },
      },
    );

    expect(catalogApi.getEntities).toHaveBeenCalledWith({
      filter: [
        {
          kind: ['Component', 'API', 'System'],
          'relations.ownedBy': ['group:default/my-team'],
        },
      ],
      fields: [
        'kind',
        'metadata.name',
        'metadata.namespace',
        'spec.type',
        'relations',
      ],
    });

    expect(getByText('OPENAPI')).toBeInTheDocument();
    expect(
      queryByText(getByText('OPENAPI').parentElement!, '1'),
    ).toBeInTheDocument();
    expect(getByText('SERVICE')).toBeInTheDocument();
    expect(
      queryByText(getByText('SERVICE').parentElement!, '1'),
    ).toBeInTheDocument();
    expect(getByText('LIBRARY')).toBeInTheDocument();
    expect(
      queryByText(getByText('LIBRARY').parentElement!, '1'),
    ).toBeInTheDocument();
    expect(getByText('SYSTEM')).toBeInTheDocument();
    expect(
      queryByText(getByText('SYSTEM').parentElement!, '1'),
    ).toBeInTheDocument();
  });

  it('applies CustomFilterDefinition', async () => {
    const catalogApi: jest.Mocked<CatalogApi> = {
      getEntities: jest.fn(),
    } as any;

    catalogApi.getEntities.mockImplementation(getEntitiesMock);

    const { getByText } = await renderInTestApp(
      <TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
        <EntityProvider entity={groupEntity}>
          <OwnershipCard entityFilterKind={['API', 'System']} />
        </EntityProvider>
      </TestApiProvider>,
      {
        mountedRoutes: {
          '/create': catalogIndexRouteRef,
        },
      },
    );

    expect(getByText('SYSTEM')).toBeInTheDocument();
    expect(
      queryByText(getByText('SYSTEM').parentElement!, '1'),
    ).toBeInTheDocument();
    expect(getByText('OPENAPI')).toBeInTheDocument();
    expect(
      queryByText(getByText('OPENAPI').parentElement!, '1'),
    ).toBeInTheDocument();
    expect(() => getByText('LIBRARY')).toThrowError();
  });

  it('links to the catalog with the group filter', async () => {
    const catalogApi: jest.Mocked<CatalogApi> = {
      getEntities: jest.fn(),
    } as any;

    catalogApi.getEntities.mockImplementation(getEntitiesMock);

    const { getByText } = await renderInTestApp(
      <TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
        <EntityProvider entity={groupEntity}>
          <OwnershipCard />
        </EntityProvider>
      </TestApiProvider>,
      {
        mountedRoutes: {
          '/create': catalogIndexRouteRef,
        },
      },
    );

    expect(getByText('OPENAPI').closest('a')).toHaveAttribute(
      'href',
      '/create/?filters%5Bkind%5D=API&filters%5Btype%5D=openapi&filters%5Bowners%5D=my-team&filters%5Buser%5D=all',
    );
  });

  it('links to the catalog with the user and groups filters from an user profile', async () => {
    const userEntity: UserEntity = {
      apiVersion: 'backstage.io/v1alpha1',
      kind: 'User',
      metadata: {
        name: 'the-user',
      },
      spec: {
        memberOf: ['my-team'],
      },
      relations: [
        {
          type: 'memberOf',
          targetRef: 'group:default/my-team',
        },
        {
          type: 'memberOf',
          targetRef: 'group:custom/some-team',
        },
      ],
    };
    const catalogApi: jest.Mocked<CatalogApi> = {
      getEntities: jest.fn(),
    } as any;

    catalogApi.getEntities.mockImplementation(getEntitiesMock);

    const { getByText } = await renderInTestApp(
      <TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
        <EntityProvider entity={userEntity}>
          <OwnershipCard />
        </EntityProvider>
      </TestApiProvider>,
      {
        mountedRoutes: {
          '/create': catalogIndexRouteRef,
        },
      },
    );

    expect(getByText('OPENAPI').closest('a')).toHaveAttribute(
      'href',
      '/create/?filters%5Bkind%5D=API&filters%5Btype%5D=openapi&filters%5Bowners%5D=user%3Athe-user&filters%5Bowners%5D=my-team&filters%5Bowners%5D=custom%2Fsome-team&filters%5Buser%5D=all',
    );
  });

  describe('OwnershipCard relations', () => {
    it('shows relations toggle', async () => {
      const catalogApi: jest.Mocked<CatalogApi> = {
        getEntities: jest.fn(),
      } as any;

      catalogApi.getEntities.mockImplementation(getEntitiesMock);

      const { getByTitle } = await renderInTestApp(
        <TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
          <EntityProvider entity={groupEntity}>
            <OwnershipCard />
          </EntityProvider>
        </TestApiProvider>,
        {
          mountedRoutes: {
            '/create': catalogIndexRouteRef,
          },
        },
      );

      expect(getByTitle('Direct Relations')).toBeInTheDocument();
    });

    it('hides relations toggle', async () => {
      const catalogApi: jest.Mocked<CatalogApi> = {
        getEntities: jest.fn(),
      } as any;

      catalogApi.getEntities.mockImplementation(getEntitiesMock);

      const rendered = await renderInTestApp(
        <TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
          <EntityProvider entity={groupEntity}>
            <OwnershipCard hideRelationsToggle />
          </EntityProvider>
        </TestApiProvider>,
        {
          mountedRoutes: {
            '/create': catalogIndexRouteRef,
          },
        },
      );

      expect(rendered.queryByText('Direct Relations')).toBeNull();
    });
    it('overrides relation type', async () => {
      const catalogApi: jest.Mocked<CatalogApi> = {
        getEntities: jest.fn(),
      } as any;

      catalogApi.getEntities.mockImplementation(getEntitiesMock);

      const { getByTitle } = await renderInTestApp(
        <TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
          <EntityProvider entity={groupEntity}>
            <OwnershipCard relationsType="aggregated" />
          </EntityProvider>
        </TestApiProvider>,
        {
          mountedRoutes: {
            '/create': catalogIndexRouteRef,
          },
        },
      );

      expect(getByTitle('Aggregated Relations')).toBeInTheDocument();
    });
  });
});
Example #19
Source File: textWysiwyg.test.tsx    From excalidraw with MIT License 4 votes vote down vote up
describe("textWysiwyg", () => {
  describe("start text editing", () => {
    const { h } = window;
    beforeEach(async () => {
      await render(<ExcalidrawApp />);
      h.elements = [];
    });

    it("should prefer editing selected text element (non-bindable container present)", async () => {
      const line = API.createElement({
        type: "line",
        width: 100,
        height: 0,
        points: [
          [0, 0],
          [100, 0],
        ],
      });
      const textSize = 20;
      const text = API.createElement({
        type: "text",
        text: "ola",
        x: line.width / 2 - textSize / 2,
        y: -textSize / 2,
        width: textSize,
        height: textSize,
      });
      h.elements = [text, line];

      API.setSelectedElements([text]);

      Keyboard.keyPress(KEYS.ENTER);

      expect(h.state.editingElement?.id).toBe(text.id);
      expect(
        (h.state.editingElement as ExcalidrawTextElement).containerId,
      ).toBe(null);
    });

    it("should prefer editing selected text element (bindable container present)", async () => {
      const container = API.createElement({
        type: "rectangle",
        width: 100,
        boundElements: [],
      });
      const textSize = 20;

      const boundText = API.createElement({
        type: "text",
        text: "ola",
        x: container.width / 2 - textSize / 2,
        y: container.height / 2 - textSize / 2,
        width: textSize,
        height: textSize,
        containerId: container.id,
      });

      const boundText2 = API.createElement({
        type: "text",
        text: "ola",
        x: container.width / 2 - textSize / 2,
        y: container.height / 2 - textSize / 2,
        width: textSize,
        height: textSize,
        containerId: container.id,
      });

      h.elements = [container, boundText, boundText2];

      mutateElement(container, {
        boundElements: [{ type: "text", id: boundText.id }],
      });

      API.setSelectedElements([boundText2]);

      Keyboard.keyPress(KEYS.ENTER);

      expect(h.state.editingElement?.id).toBe(boundText2.id);
    });

    it("should not create bound text on ENTER if text exists at container center", () => {
      const container = API.createElement({
        type: "rectangle",
        width: 100,
      });
      const textSize = 20;
      const text = API.createElement({
        type: "text",
        text: "ola",
        x: container.width / 2 - textSize / 2,
        y: container.height / 2 - textSize / 2,
        width: textSize,
        height: textSize,
        containerId: container.id,
      });

      h.elements = [container, text];

      API.setSelectedElements([container]);

      Keyboard.keyPress(KEYS.ENTER);

      expect(h.state.editingElement?.id).toBe(text.id);
    });

    it("should edit existing bound text on ENTER even if higher z-index unbound text exists at container center", () => {
      const container = API.createElement({
        type: "rectangle",
        width: 100,
        boundElements: [],
      });
      const textSize = 20;

      const boundText = API.createElement({
        type: "text",
        text: "ola",
        x: container.width / 2 - textSize / 2,
        y: container.height / 2 - textSize / 2,
        width: textSize,
        height: textSize,
        containerId: container.id,
      });

      const boundText2 = API.createElement({
        type: "text",
        text: "ola",
        x: container.width / 2 - textSize / 2,
        y: container.height / 2 - textSize / 2,
        width: textSize,
        height: textSize,
        containerId: container.id,
      });

      h.elements = [container, boundText, boundText2];

      mutateElement(container, {
        boundElements: [{ type: "text", id: boundText.id }],
      });

      API.setSelectedElements([container]);

      Keyboard.keyPress(KEYS.ENTER);

      expect(h.state.editingElement?.id).toBe(boundText.id);
    });

    it("should edit text under cursor when clicked with text tool", () => {
      const text = API.createElement({
        type: "text",
        text: "ola",
        x: 60,
        y: 0,
        width: 100,
        height: 100,
      });

      h.elements = [text];
      UI.clickTool("text");

      mouse.clickAt(text.x + 50, text.y + 50);

      const editor = document.querySelector(
        ".excalidraw-textEditorContainer > textarea",
      ) as HTMLTextAreaElement;

      expect(editor).not.toBe(null);
      expect(h.state.editingElement?.id).toBe(text.id);
      expect(h.elements.length).toBe(1);
    });

    it("should edit text under cursor when double-clicked with selection tool", () => {
      const text = API.createElement({
        type: "text",
        text: "ola",
        x: 60,
        y: 0,
        width: 100,
        height: 100,
      });

      h.elements = [text];
      UI.clickTool("selection");

      mouse.doubleClickAt(text.x + 50, text.y + 50);

      const editor = document.querySelector(
        ".excalidraw-textEditorContainer > textarea",
      ) as HTMLTextAreaElement;

      expect(editor).not.toBe(null);
      expect(h.state.editingElement?.id).toBe(text.id);
      expect(h.elements.length).toBe(1);
    });
  });

  describe("Test container-unbound text", () => {
    const { h } = window;

    let textarea: HTMLTextAreaElement;
    let textElement: ExcalidrawTextElement;
    beforeEach(async () => {
      await render(<ExcalidrawApp />);

      textElement = UI.createElement("text");

      mouse.clickOn(textElement);
      textarea = document.querySelector(
        ".excalidraw-textEditorContainer > textarea",
      )!;
    });

    it("should add a tab at the start of the first line", () => {
      const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
      textarea.value = "Line#1\nLine#2";
      // cursor: "|Line#1\nLine#2"
      textarea.selectionStart = 0;
      textarea.selectionEnd = 0;
      textarea.dispatchEvent(event);

      expect(textarea.value).toEqual(`${tab}Line#1\nLine#2`);
      // cursor: "    |Line#1\nLine#2"
      expect(textarea.selectionStart).toEqual(4);
      expect(textarea.selectionEnd).toEqual(4);
    });

    it("should add a tab at the start of the second line", () => {
      const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
      textarea.value = "Line#1\nLine#2";
      // cursor: "Line#1\nLin|e#2"
      textarea.selectionStart = 10;
      textarea.selectionEnd = 10;

      textarea.dispatchEvent(event);

      expect(textarea.value).toEqual(`Line#1\n${tab}Line#2`);

      // cursor: "Line#1\n    Lin|e#2"
      expect(textarea.selectionStart).toEqual(14);
      expect(textarea.selectionEnd).toEqual(14);
    });

    it("should add a tab at the start of the first and second line", () => {
      const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
      textarea.value = "Line#1\nLine#2\nLine#3";
      // cursor: "Li|ne#1\nLi|ne#2\nLine#3"
      textarea.selectionStart = 2;
      textarea.selectionEnd = 9;

      textarea.dispatchEvent(event);

      expect(textarea.value).toEqual(`${tab}Line#1\n${tab}Line#2\nLine#3`);

      // cursor: "    Li|ne#1\n    Li|ne#2\nLine#3"
      expect(textarea.selectionStart).toEqual(6);
      expect(textarea.selectionEnd).toEqual(17);
    });

    it("should remove a tab at the start of the first line", () => {
      const event = new KeyboardEvent("keydown", {
        key: KEYS.TAB,
        shiftKey: true,
      });
      textarea.value = `${tab}Line#1\nLine#2`;
      // cursor: "|    Line#1\nLine#2"
      textarea.selectionStart = 0;
      textarea.selectionEnd = 0;

      textarea.dispatchEvent(event);

      expect(textarea.value).toEqual(`Line#1\nLine#2`);

      // cursor: "|Line#1\nLine#2"
      expect(textarea.selectionStart).toEqual(0);
      expect(textarea.selectionEnd).toEqual(0);
    });

    it("should remove a tab at the start of the second line", () => {
      const event = new KeyboardEvent("keydown", {
        key: KEYS.TAB,
        shiftKey: true,
      });
      // cursor: "Line#1\n    Lin|e#2"
      textarea.value = `Line#1\n${tab}Line#2`;
      textarea.selectionStart = 15;
      textarea.selectionEnd = 15;

      textarea.dispatchEvent(event);

      expect(textarea.value).toEqual(`Line#1\nLine#2`);
      // cursor: "Line#1\nLin|e#2"
      expect(textarea.selectionStart).toEqual(11);
      expect(textarea.selectionEnd).toEqual(11);
    });

    it("should remove a tab at the start of the first and second line", () => {
      const event = new KeyboardEvent("keydown", {
        key: KEYS.TAB,
        shiftKey: true,
      });
      // cursor: "    Li|ne#1\n    Li|ne#2\nLine#3"
      textarea.value = `${tab}Line#1\n${tab}Line#2\nLine#3`;
      textarea.selectionStart = 6;
      textarea.selectionEnd = 17;

      textarea.dispatchEvent(event);

      expect(textarea.value).toEqual(`Line#1\nLine#2\nLine#3`);
      // cursor: "Li|ne#1\nLi|ne#2\nLine#3"
      expect(textarea.selectionStart).toEqual(2);
      expect(textarea.selectionEnd).toEqual(9);
    });

    it("should remove a tab at the start of the second line and cursor stay on this line", () => {
      const event = new KeyboardEvent("keydown", {
        key: KEYS.TAB,
        shiftKey: true,
      });
      // cursor: "Line#1\n  |  Line#2"
      textarea.value = `Line#1\n${tab}Line#2`;
      textarea.selectionStart = 9;
      textarea.selectionEnd = 9;
      textarea.dispatchEvent(event);

      // cursor: "Line#1\n|Line#2"
      expect(textarea.selectionStart).toEqual(7);
      // expect(textarea.selectionEnd).toEqual(7);
    });

    it("should remove partial tabs", () => {
      const event = new KeyboardEvent("keydown", {
        key: KEYS.TAB,
        shiftKey: true,
      });
      // cursor: "Line#1\n  Line#|2"
      textarea.value = `Line#1\n  Line#2`;
      textarea.selectionStart = 15;
      textarea.selectionEnd = 15;
      textarea.dispatchEvent(event);

      expect(textarea.value).toEqual(`Line#1\nLine#2`);
    });

    it("should remove nothing", () => {
      const event = new KeyboardEvent("keydown", {
        key: KEYS.TAB,
        shiftKey: true,
      });
      // cursor: "Line#1\n  Li|ne#2"
      textarea.value = `Line#1\nLine#2`;
      textarea.selectionStart = 9;
      textarea.selectionEnd = 9;
      textarea.dispatchEvent(event);

      expect(textarea.value).toEqual(`Line#1\nLine#2`);
    });

    it("should resize text via shortcuts while in wysiwyg", () => {
      textarea.value = "abc def";
      const origFontSize = textElement.fontSize;
      textarea.dispatchEvent(
        new KeyboardEvent("keydown", {
          key: KEYS.CHEVRON_RIGHT,
          ctrlKey: true,
          shiftKey: true,
        }),
      );
      expect(textElement.fontSize).toBe(origFontSize * 1.1);

      textarea.dispatchEvent(
        new KeyboardEvent("keydown", {
          key: KEYS.CHEVRON_LEFT,
          ctrlKey: true,
          shiftKey: true,
        }),
      );
      expect(textElement.fontSize).toBe(origFontSize);
    });

    it("zooming via keyboard should zoom canvas", () => {
      expect(h.state.zoom.value).toBe(1);
      textarea.dispatchEvent(
        new KeyboardEvent("keydown", {
          code: CODES.MINUS,
          ctrlKey: true,
        }),
      );
      expect(h.state.zoom.value).toBe(0.9);
      textarea.dispatchEvent(
        new KeyboardEvent("keydown", {
          code: CODES.NUM_SUBTRACT,
          ctrlKey: true,
        }),
      );
      expect(h.state.zoom.value).toBe(0.8);
      textarea.dispatchEvent(
        new KeyboardEvent("keydown", {
          code: CODES.NUM_ADD,
          ctrlKey: true,
        }),
      );
      expect(h.state.zoom.value).toBe(0.9);
      textarea.dispatchEvent(
        new KeyboardEvent("keydown", {
          code: CODES.EQUAL,
          ctrlKey: true,
        }),
      );
      expect(h.state.zoom.value).toBe(1);
    });
  });

  describe("Test container-bound text", () => {
    let rectangle: any;
    const { h } = window;

    const DUMMY_HEIGHT = 240;
    const DUMMY_WIDTH = 160;
    const APPROX_LINE_HEIGHT = 25;
    const INITIAL_WIDTH = 10;

    beforeAll(() => {
      jest
        .spyOn(textElementUtils, "getApproxLineHeight")
        .mockReturnValue(APPROX_LINE_HEIGHT);
    });

    beforeEach(async () => {
      await render(<ExcalidrawApp />);
      h.elements = [];

      rectangle = UI.createElement("rectangle", {
        x: 10,
        y: 20,
        width: 90,
        height: 75,
      });
    });

    it("should bind text to container when double clicked on center", async () => {
      expect(h.elements.length).toBe(1);
      expect(h.elements[0].id).toBe(rectangle.id);

      mouse.doubleClickAt(
        rectangle.x + rectangle.width / 2,
        rectangle.y + rectangle.height / 2,
      );
      expect(h.elements.length).toBe(2);

      const text = h.elements[1] as ExcalidrawTextElementWithContainer;
      expect(text.type).toBe("text");
      expect(text.containerId).toBe(rectangle.id);
      mouse.down();
      const editor = document.querySelector(
        ".excalidraw-textEditorContainer > textarea",
      ) as HTMLTextAreaElement;

      fireEvent.change(editor, { target: { value: "Hello World!" } });

      await new Promise((r) => setTimeout(r, 0));
      editor.blur();
      expect(rectangle.boundElements).toStrictEqual([
        { id: text.id, type: "text" },
      ]);
    });

    it("should bind text to container when clicked on container and enter pressed", async () => {
      expect(h.elements.length).toBe(1);
      expect(h.elements[0].id).toBe(rectangle.id);

      Keyboard.withModifierKeys({}, () => {
        Keyboard.keyPress(KEYS.ENTER);
      });

      expect(h.elements.length).toBe(2);

      const text = h.elements[1] as ExcalidrawTextElementWithContainer;
      expect(text.type).toBe("text");
      expect(text.containerId).toBe(rectangle.id);
      const editor = document.querySelector(
        ".excalidraw-textEditorContainer > textarea",
      ) as HTMLTextAreaElement;

      await new Promise((r) => setTimeout(r, 0));

      fireEvent.change(editor, { target: { value: "Hello World!" } });
      editor.blur();
      expect(rectangle.boundElements).toStrictEqual([
        { id: text.id, type: "text" },
      ]);
    });

    it("shouldn't bind to non-text-bindable containers", async () => {
      const line = API.createElement({
        type: "line",
        width: 100,
        height: 0,
        points: [
          [0, 0],
          [100, 0],
        ],
      });
      h.elements = [line];

      UI.clickTool("text");

      mouse.clickAt(line.x + line.width / 2, line.y + line.height / 2);

      const editor = document.querySelector(
        ".excalidraw-textEditorContainer > textarea",
      ) as HTMLTextAreaElement;

      fireEvent.change(editor, {
        target: {
          value: "Hello World!",
        },
      });
      fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
      editor.dispatchEvent(new Event("input"));

      expect(line.boundElements).toBe(null);
      expect(h.elements[1].type).toBe("text");
      expect((h.elements[1] as ExcalidrawTextElement).containerId).toBe(null);
    });

    it("should'nt bind text to container when not double clicked on center", async () => {
      expect(h.elements.length).toBe(1);
      expect(h.elements[0].id).toBe(rectangle.id);

      // clicking somewhere on top left
      mouse.doubleClickAt(rectangle.x + 20, rectangle.y + 20);
      expect(h.elements.length).toBe(2);

      const text = h.elements[1] as ExcalidrawTextElementWithContainer;
      expect(text.type).toBe("text");
      expect(text.containerId).toBe(null);
      mouse.down();
      const editor = document.querySelector(
        ".excalidraw-textEditorContainer > textarea",
      ) as HTMLTextAreaElement;

      fireEvent.change(editor, { target: { value: "Hello World!" } });

      await new Promise((r) => setTimeout(r, 0));
      editor.blur();
      expect(rectangle.boundElements).toBe(null);
    });

    it("should update font family correctly on undo/redo by selecting bounded text when font family was updated", async () => {
      expect(h.elements.length).toBe(1);

      mouse.doubleClickAt(
        rectangle.x + rectangle.width / 2,
        rectangle.y + rectangle.height / 2,
      );
      mouse.down();

      const text = h.elements[1] as ExcalidrawTextElementWithContainer;
      let editor = document.querySelector(
        ".excalidraw-textEditorContainer > textarea",
      ) as HTMLTextAreaElement;

      await new Promise((r) => setTimeout(r, 0));
      fireEvent.change(editor, { target: { value: "Hello World!" } });
      editor.blur();
      expect(text.fontFamily).toEqual(FONT_FAMILY.Virgil);
      UI.clickTool("text");

      mouse.clickAt(
        rectangle.x + rectangle.width / 2,
        rectangle.y + rectangle.height / 2,
      );
      mouse.down();
      editor = document.querySelector(
        ".excalidraw-textEditorContainer > textarea",
      ) as HTMLTextAreaElement;

      editor.select();
      fireEvent.click(screen.getByTitle(/code/i));

      await new Promise((r) => setTimeout(r, 0));
      editor.blur();
      expect(
        (h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
      ).toEqual(FONT_FAMILY.Cascadia);

      //undo
      Keyboard.withModifierKeys({ ctrl: true }, () => {
        Keyboard.keyPress(KEYS.Z);
      });
      expect(
        (h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
      ).toEqual(FONT_FAMILY.Virgil);

      //redo
      Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
        Keyboard.keyPress(KEYS.Z);
      });
      expect(
        (h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
      ).toEqual(FONT_FAMILY.Cascadia);
    });

    it("should wrap text and vertcially center align once text submitted", async () => {
      jest
        .spyOn(textElementUtils, "measureText")
        .mockImplementation((text, font, maxWidth) => {
          let width = INITIAL_WIDTH;
          let height = APPROX_LINE_HEIGHT;
          let baseline = 10;
          if (!text) {
            return {
              width,
              height,
              baseline,
            };
          }
          baseline = 30;
          width = DUMMY_WIDTH;
          if (text === "Hello \nWorld!") {
            height = APPROX_LINE_HEIGHT * 2;
          }
          if (maxWidth) {
            width = maxWidth;
            // To capture cases where maxWidth passed is initial width
            // due to which the text is not wrapped correctly
            if (maxWidth === INITIAL_WIDTH) {
              height = DUMMY_HEIGHT;
            }
          }
          return {
            width,
            height,
            baseline,
          };
        });

      expect(h.elements.length).toBe(1);

      Keyboard.keyDown(KEYS.ENTER);
      let text = h.elements[1] as ExcalidrawTextElementWithContainer;
      let editor = document.querySelector(
        ".excalidraw-textEditorContainer > textarea",
      ) as HTMLTextAreaElement;

      // mock scroll height
      jest
        .spyOn(editor, "scrollHeight", "get")
        .mockImplementation(() => APPROX_LINE_HEIGHT * 2);

      fireEvent.change(editor, {
        target: {
          value: "Hello World!",
        },
      });

      editor.dispatchEvent(new Event("input"));

      await new Promise((cb) => setTimeout(cb, 0));
      editor.blur();
      text = h.elements[1] as ExcalidrawTextElementWithContainer;
      expect(text.text).toBe("Hello \nWorld!");
      expect(text.originalText).toBe("Hello World!");
      expect(text.y).toBe(
        rectangle.y + rectangle.height / 2 - (APPROX_LINE_HEIGHT * 2) / 2,
      );
      expect(text.x).toBe(rectangle.x + BOUND_TEXT_PADDING);
      expect(text.height).toBe(APPROX_LINE_HEIGHT * 2);
      expect(text.width).toBe(rectangle.width - BOUND_TEXT_PADDING * 2);

      // Edit and text by removing second line and it should
      // still vertically align correctly
      mouse.select(rectangle);
      Keyboard.withModifierKeys({}, () => {
        Keyboard.keyPress(KEYS.ENTER);
      });
      editor = document.querySelector(
        ".excalidraw-textEditorContainer > textarea",
      ) as HTMLTextAreaElement;

      fireEvent.change(editor, {
        target: {
          value: "Hello",
        },
      });

      // mock scroll height
      jest
        .spyOn(editor, "scrollHeight", "get")
        .mockImplementation(() => APPROX_LINE_HEIGHT);
      editor.style.height = "25px";
      editor.dispatchEvent(new Event("input"));

      await new Promise((r) => setTimeout(r, 0));

      editor.blur();
      text = h.elements[1] as ExcalidrawTextElementWithContainer;

      expect(text.text).toBe("Hello");
      expect(text.originalText).toBe("Hello");
      expect(text.y).toBe(
        rectangle.y + rectangle.height / 2 - APPROX_LINE_HEIGHT / 2,
      );
      expect(text.x).toBe(rectangle.x + BOUND_TEXT_PADDING);
      expect(text.height).toBe(APPROX_LINE_HEIGHT);
      expect(text.width).toBe(rectangle.width - BOUND_TEXT_PADDING * 2);
    });

    it("should unbind bound text when unbind action from context menu is triggered", async () => {
      expect(h.elements.length).toBe(1);
      expect(h.elements[0].id).toBe(rectangle.id);

      Keyboard.withModifierKeys({}, () => {
        Keyboard.keyPress(KEYS.ENTER);
      });

      expect(h.elements.length).toBe(2);

      const text = h.elements[1] as ExcalidrawTextElementWithContainer;
      expect(text.containerId).toBe(rectangle.id);

      const editor = document.querySelector(
        ".excalidraw-textEditorContainer > textarea",
      ) as HTMLTextAreaElement;

      await new Promise((r) => setTimeout(r, 0));

      fireEvent.change(editor, { target: { value: "Hello World!" } });
      editor.blur();
      expect(rectangle.boundElements).toStrictEqual([
        { id: text.id, type: "text" },
      ]);
      mouse.reset();
      UI.clickTool("selection");
      mouse.clickAt(10, 20);
      mouse.down();
      mouse.up();
      fireEvent.contextMenu(GlobalTestState.canvas, {
        button: 2,
        clientX: 20,
        clientY: 30,
      });
      const contextMenu = document.querySelector(".context-menu");
      fireEvent.click(queryByText(contextMenu as HTMLElement, "Unbind text")!);
      expect(h.elements[0].boundElements).toEqual([]);
      expect((h.elements[1] as ExcalidrawTextElement).containerId).toEqual(
        null,
      );
    });
    it("shouldn't bind to container if container has bound text", async () => {
      expect(h.elements.length).toBe(1);

      Keyboard.withModifierKeys({}, () => {
        Keyboard.keyPress(KEYS.ENTER);
      });

      expect(h.elements.length).toBe(2);

      // Bind first text
      let text = h.elements[1] as ExcalidrawTextElementWithContainer;
      expect(text.containerId).toBe(rectangle.id);
      let editor = document.querySelector(
        ".excalidraw-textEditorContainer > textarea",
      ) as HTMLTextAreaElement;
      await new Promise((r) => setTimeout(r, 0));
      fireEvent.change(editor, { target: { value: "Hello World!" } });
      editor.blur();
      expect(rectangle.boundElements).toStrictEqual([
        { id: text.id, type: "text" },
      ]);

      // Attempt to bind another text
      UI.clickTool("text");
      mouse.clickAt(
        rectangle.x + rectangle.width / 2,
        rectangle.y + rectangle.height / 2,
      );
      mouse.down();
      expect(h.elements.length).toBe(3);
      text = h.elements[2] as ExcalidrawTextElementWithContainer;
      editor = document.querySelector(
        ".excalidraw-textEditorContainer > textarea",
      ) as HTMLTextAreaElement;
      await new Promise((r) => setTimeout(r, 0));
      fireEvent.change(editor, { target: { value: "Whats up?" } });
      editor.blur();
      expect(rectangle.boundElements).toStrictEqual([
        { id: h.elements[1].id, type: "text" },
      ]);
      expect(text.containerId).toBe(null);
    });
  });
});
Example #20
Source File: excalidraw.test.tsx    From excalidraw with MIT License 4 votes vote down vote up
describe("<Excalidraw/>", () => {
  describe("Test zenModeEnabled prop", () => {
    it('should show exit zen mode button when zen mode is set and zen mode option in context menu when zenModeEnabled is "undefined"', async () => {
      const { container } = await render(<Excalidraw />);
      expect(
        container.getElementsByClassName("disable-zen-mode--visible").length,
      ).toBe(0);
      expect(h.state.zenModeEnabled).toBe(false);

      fireEvent.contextMenu(GlobalTestState.canvas, {
        button: 2,
        clientX: 1,
        clientY: 1,
      });
      const contextMenu = document.querySelector(".context-menu");
      fireEvent.click(queryByText(contextMenu as HTMLElement, "Zen mode")!);
      expect(h.state.zenModeEnabled).toBe(true);
      expect(
        container.getElementsByClassName("disable-zen-mode--visible").length,
      ).toBe(1);
    });

    it("should not show exit zen mode button and zen mode option in context menu when zenModeEnabled is set", async () => {
      const { container } = await render(<Excalidraw zenModeEnabled={true} />);
      expect(
        container.getElementsByClassName("disable-zen-mode--visible").length,
      ).toBe(0);
      expect(h.state.zenModeEnabled).toBe(true);

      fireEvent.contextMenu(GlobalTestState.canvas, {
        button: 2,
        clientX: 1,
        clientY: 1,
      });
      const contextMenu = document.querySelector(".context-menu");
      expect(queryByText(contextMenu as HTMLElement, "Zen mode")).toBe(null);
      expect(h.state.zenModeEnabled).toBe(true);
      expect(
        container.getElementsByClassName("disable-zen-mode--visible").length,
      ).toBe(0);
    });
  });

  describe("Test gridModeEnabled prop", () => {
    it('should show grid mode in context menu when gridModeEnabled is "undefined"', async () => {
      const { container } = await render(<Excalidraw />);
      expect(h.state.gridSize).toBe(null);

      expect(
        container.getElementsByClassName("disable-zen-mode--visible").length,
      ).toBe(0);
      fireEvent.contextMenu(GlobalTestState.canvas, {
        button: 2,
        clientX: 1,
        clientY: 1,
      });
      const contextMenu = document.querySelector(".context-menu");
      fireEvent.click(queryByText(contextMenu as HTMLElement, "Show grid")!);
      expect(h.state.gridSize).toBe(GRID_SIZE);
    });

    it('should not show grid mode in context menu when gridModeEnabled is not "undefined"', async () => {
      const { container } = await render(
        <Excalidraw gridModeEnabled={false} />,
      );
      expect(h.state.gridSize).toBe(null);

      expect(
        container.getElementsByClassName("disable-zen-mode--visible").length,
      ).toBe(0);
      fireEvent.contextMenu(GlobalTestState.canvas, {
        button: 2,
        clientX: 1,
        clientY: 1,
      });
      const contextMenu = document.querySelector(".context-menu");
      expect(queryByText(contextMenu as HTMLElement, "Show grid")).toBe(null);
      expect(h.state.gridSize).toBe(null);
    });
  });

  describe("Test theme prop", () => {
    it('should show the dark mode toggle when the theme prop is "undefined"', async () => {
      const { container } = await render(<Excalidraw />);
      expect(h.state.theme).toBe(THEME.LIGHT);

      const darkModeToggle = queryByTestId(container, "toggle-dark-mode");

      expect(darkModeToggle).toBeTruthy();
    });

    it('should not show the dark mode toggle when the theme prop is not "undefined"', async () => {
      const { container } = await render(<Excalidraw theme="dark" />);
      expect(h.state.theme).toBe(THEME.DARK);

      expect(queryByTestId(container, "toggle-dark-mode")).toBe(null);
    });
  });

  describe("Test name prop", () => {
    it('should allow editing name when the name prop is "undefined"', async () => {
      const { container } = await render(<Excalidraw />);

      fireEvent.click(queryByTestId(container, "image-export-button")!);
      const textInput: HTMLInputElement | null = document.querySelector(
        ".ExportDialog .ProjectName .TextInput",
      );
      expect(textInput?.value).toContain(`${t("labels.untitled")}`);
      expect(textInput?.nodeName).toBe("INPUT");
    });

    it('should set the name and not allow editing when the name prop is present"', async () => {
      const name = "test";
      const { container } = await render(<Excalidraw name={name} />);

      await fireEvent.click(queryByTestId(container, "image-export-button")!);
      const textInput = document.querySelector(
        ".ExportDialog .ProjectName .TextInput--readonly",
      );
      expect(textInput?.textContent).toEqual(name);
      expect(textInput?.nodeName).toBe("SPAN");
    });
  });

  describe("Test UIOptions prop", () => {
    it('should not hide any UI element when the UIOptions prop is "undefined"', async () => {
      await render(<Excalidraw />);

      const canvasActions = document.querySelector(
        'section[aria-labelledby="test-id-canvasActions-title"]',
      );

      expect(canvasActions).toMatchSnapshot();
    });

    describe("Test canvasActions", () => {
      it('should not hide any UI element when canvasActions is "undefined"', async () => {
        await render(<Excalidraw UIOptions={{}} />);
        const canvasActions = document.querySelector(
          'section[aria-labelledby="test-id-canvasActions-title"]',
        );
        expect(canvasActions).toMatchSnapshot();
      });

      it("should hide clear canvas button when clearCanvas is false", async () => {
        const { container } = await render(
          <Excalidraw UIOptions={{ canvasActions: { clearCanvas: false } }} />,
        );

        expect(queryByTestId(container, "clear-canvas-button")).toBeNull();
      });

      it("should hide export button when export is false", async () => {
        const { container } = await render(
          <Excalidraw UIOptions={{ canvasActions: { export: false } }} />,
        );

        expect(queryByTestId(container, "json-export-button")).toBeNull();
      });

      it("should hide 'Save as image' button when 'saveAsImage' is false", async () => {
        const { container } = await render(
          <Excalidraw UIOptions={{ canvasActions: { saveAsImage: false } }} />,
        );

        expect(queryByTestId(container, "image-export-button")).toBeNull();
      });

      it("should hide load button when loadScene is false", async () => {
        const { container } = await render(
          <Excalidraw UIOptions={{ canvasActions: { loadScene: false } }} />,
        );

        expect(queryByTestId(container, "load-button")).toBeNull();
      });

      it("should hide save as button when saveFileToDisk is false", async () => {
        const { container } = await render(
          <Excalidraw
            UIOptions={{ canvasActions: { export: { saveFileToDisk: false } } }}
          />,
        );

        expect(queryByTestId(container, "save-as-button")).toBeNull();
      });

      it("should hide save button when saveToActiveFile is false", async () => {
        const { container } = await render(
          <Excalidraw
            UIOptions={{ canvasActions: { saveToActiveFile: false } }}
          />,
        );

        expect(queryByTestId(container, "save-button")).toBeNull();
      });

      it("should hide the canvas background picker when changeViewBackgroundColor is false", async () => {
        const { container } = await render(
          <Excalidraw
            UIOptions={{ canvasActions: { changeViewBackgroundColor: false } }}
          />,
        );

        expect(queryByTestId(container, "canvas-background-picker")).toBeNull();
      });

      it("should hide the theme toggle when theme is false", async () => {
        const { container } = await render(
          <Excalidraw UIOptions={{ canvasActions: { theme: false } }} />,
        );

        expect(queryByTestId(container, "toggle-dark-mode")).toBeNull();
      });
    });
  });

  describe("Test autoFocus prop", () => {
    it("should not focus when autoFocus is false", async () => {
      const { container } = await render(<Excalidraw />);

      expect(
        container.querySelector(".excalidraw") === document.activeElement,
      ).toBe(false);
    });

    it("should focus when autoFocus is true", async () => {
      const { container } = await render(<Excalidraw autoFocus={true} />);

      expect(
        container.querySelector(".excalidraw") === document.activeElement,
      ).toBe(true);
    });
  });
});
Example #21
Source File: regressionTests.test.tsx    From excalidraw-embed with MIT License 4 votes vote down vote up
describe("regression tests", () => {
  it("draw every type of shape", () => {
    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(10, 10);

    clickTool("diamond");
    mouse.down(10, -10);
    mouse.up(10, 10);

    clickTool("ellipse");
    mouse.down(10, -10);
    mouse.up(10, 10);

    clickTool("arrow");
    mouse.down(10, -10);
    mouse.up(10, 10);

    clickTool("line");
    mouse.down(10, -10);
    mouse.up(10, 10);

    clickTool("arrow");
    mouse.click(10, -10);
    mouse.click(10, 10);
    mouse.click(-10, 10);
    hotkeyPress("ENTER");

    clickTool("line");
    mouse.click(10, -20);
    mouse.click(10, 10);
    mouse.click(-10, 10);
    hotkeyPress("ENTER");

    clickTool("draw");
    mouse.down(10, -20);
    mouse.up(10, 10);

    expect(h.elements.map((element) => element.type)).toEqual([
      "rectangle",
      "diamond",
      "ellipse",
      "arrow",
      "line",
      "arrow",
      "line",
      "draw",
    ]);
  });

  it("click to select a shape", () => {
    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(10, 10);

    const firstRectPos = mouse.getPosition();

    clickTool("rectangle");
    mouse.down(10, -10);
    mouse.up(10, 10);

    const prevSelectedId = getSelectedElement().id;
    mouse.restorePosition(...firstRectPos);
    mouse.click();

    expect(getSelectedElement().id).not.toEqual(prevSelectedId);
  });

  for (const [keys, shape] of [
    ["2r", "rectangle"],
    ["3d", "diamond"],
    ["4e", "ellipse"],
    ["5a", "arrow"],
    ["6l", "line"],
    ["7x", "draw"],
  ] as [string, ExcalidrawElement["type"]][]) {
    for (const key of keys) {
      it(`hotkey ${key} selects ${shape} tool`, () => {
        keyPress(key);

        mouse.down(10, 10);
        mouse.up(10, 10);

        expect(getSelectedElement().type).toBe(shape);
      });
    }
  }

  it("change the properties of a shape", () => {
    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(10, 10);

    clickLabeledElement("Background");
    clickLabeledElement("#fa5252");
    clickLabeledElement("Stroke");
    clickLabeledElement("#5f3dc4");
    expect(getSelectedElement().backgroundColor).toBe("#fa5252");
    expect(getSelectedElement().strokeColor).toBe("#5f3dc4");
  });

  it("resize an element, trying every resize handle", () => {
    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(10, 10);

    const resizeHandles = getResizeHandles("mouse");
    delete resizeHandles.rotation; // exclude rotation handle
    for (const handlePos in resizeHandles) {
      const [x, y] = resizeHandles[handlePos as keyof typeof resizeHandles];
      const { width: prevWidth, height: prevHeight } = getSelectedElement();
      mouse.restorePosition(x, y);
      mouse.down();
      mouse.up(-5, -5);

      const {
        width: nextWidthNegative,
        height: nextHeightNegative,
      } = getSelectedElement();
      expect(
        prevWidth !== nextWidthNegative || prevHeight !== nextHeightNegative,
      ).toBeTruthy();
      checkpoint(`resize handle ${handlePos} (-5, -5)`);

      mouse.down();
      mouse.up(5, 5);

      const { width, height } = getSelectedElement();
      expect(width).toBe(prevWidth);
      expect(height).toBe(prevHeight);
      checkpoint(`unresize handle ${handlePos} (-5, -5)`);

      mouse.restorePosition(x, y);
      mouse.down();
      mouse.up(5, 5);

      const {
        width: nextWidthPositive,
        height: nextHeightPositive,
      } = getSelectedElement();
      expect(
        prevWidth !== nextWidthPositive || prevHeight !== nextHeightPositive,
      ).toBeTruthy();
      checkpoint(`resize handle ${handlePos} (+5, +5)`);

      mouse.down();
      mouse.up(-5, -5);

      const { width: finalWidth, height: finalHeight } = getSelectedElement();
      expect(finalWidth).toBe(prevWidth);
      expect(finalHeight).toBe(prevHeight);

      checkpoint(`unresize handle ${handlePos} (+5, +5)`);
    }
  });

  it("click on an element and drag it", () => {
    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(10, 10);

    const { x: prevX, y: prevY } = getSelectedElement();
    mouse.down(-10, -10);
    mouse.up(10, 10);

    const { x: nextX, y: nextY } = getSelectedElement();
    expect(nextX).toBeGreaterThan(prevX);
    expect(nextY).toBeGreaterThan(prevY);

    checkpoint("dragged");

    mouse.down();
    mouse.up(-10, -10);

    const { x, y } = getSelectedElement();
    expect(x).toBe(prevX);
    expect(y).toBe(prevY);
  });

  it("alt-drag duplicates an element", () => {
    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(10, 10);

    expect(
      h.elements.filter((element) => element.type === "rectangle").length,
    ).toBe(1);

    withModifierKeys({ alt: true }, () => {
      mouse.down(-10, -10);
      mouse.up(10, 10);
    });

    expect(
      h.elements.filter((element) => element.type === "rectangle").length,
    ).toBe(2);
  });

  it("click-drag to select a group", () => {
    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(10, 10);

    clickTool("rectangle");
    mouse.down(10, -10);
    mouse.up(10, 10);

    const finalPosition = mouse.getPosition();

    clickTool("rectangle");
    mouse.down(10, -10);
    mouse.up(10, 10);

    mouse.restorePosition(0, 0);
    mouse.down();
    mouse.restorePosition(...finalPosition);
    mouse.up(5, 5);

    expect(
      h.elements.filter((element) => h.state.selectedElementIds[element.id])
        .length,
    ).toBe(2);
  });

  it("shift-click to multiselect, then drag", () => {
    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(10, 10);

    clickTool("rectangle");
    mouse.down(10, -10);
    mouse.up(10, 10);

    const prevRectsXY = h.elements
      .filter((element) => element.type === "rectangle")
      .map((element) => ({ x: element.x, y: element.y }));

    mouse.reset();
    mouse.click(10, 10);
    withModifierKeys({ shift: true }, () => {
      mouse.click(20, 0);
    });

    mouse.down();
    mouse.up(10, 10);

    h.elements
      .filter((element) => element.type === "rectangle")
      .forEach((element, i) => {
        expect(element.x).toBeGreaterThan(prevRectsXY[i].x);
        expect(element.y).toBeGreaterThan(prevRectsXY[i].y);
      });
  });

  it("pinch-to-zoom works", () => {
    expect(h.state.zoom).toBe(1);
    finger1.down(50, 50);
    finger2.down(60, 50);
    finger1.move(-10, 0);
    expect(h.state.zoom).toBeGreaterThan(1);
    const zoomed = h.state.zoom;
    finger1.move(5, 0);
    finger2.move(-5, 0);
    expect(h.state.zoom).toBeLessThan(zoomed);
  });

  it("two-finger scroll works", () => {
    const startScrollY = h.state.scrollY;
    finger1.down(50, 50);
    finger2.down(60, 50);

    finger1.up(0, -10);
    finger2.up(0, -10);
    expect(h.state.scrollY).toBeLessThan(startScrollY);

    const startScrollX = h.state.scrollX;

    finger1.restorePosition(50, 50);
    finger2.restorePosition(50, 60);
    finger1.down();
    finger2.down();
    finger1.up(10, 0);
    finger2.up(10, 0);
    expect(h.state.scrollX).toBeGreaterThan(startScrollX);
  });

  it("spacebar + drag scrolls the canvas", () => {
    const { scrollX: startScrollX, scrollY: startScrollY } = h.state;
    hotkeyDown("SPACE");
    mouse.down(50, 50);
    mouse.up(60, 60);
    hotkeyUp("SPACE");
    const { scrollX, scrollY } = h.state;
    expect(scrollX).not.toEqual(startScrollX);
    expect(scrollY).not.toEqual(startScrollY);
  });

  it("arrow keys", () => {
    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(10, 10);
    hotkeyPress("ARROW_LEFT");
    hotkeyPress("ARROW_LEFT");
    hotkeyPress("ARROW_RIGHT");
    hotkeyPress("ARROW_UP");
    hotkeyPress("ARROW_UP");
    hotkeyPress("ARROW_DOWN");
    expect(h.elements[0].x).toBe(9);
    expect(h.elements[0].y).toBe(9);
  });

  it("undo/redo drawing an element", () => {
    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(10, 10);

    clickTool("rectangle");
    mouse.down(10, -10);
    mouse.up(10, 10);

    clickTool("arrow");
    mouse.click(10, -10);
    mouse.click(10, 10);
    mouse.click(-10, 10);
    hotkeyPress("ENTER");

    expect(h.elements.filter((element) => !element.isDeleted).length).toBe(3);
    withModifierKeys({ ctrl: true }, () => {
      keyPress("z");
      keyPress("z");
    });
    expect(h.elements.filter((element) => !element.isDeleted).length).toBe(2);
    withModifierKeys({ ctrl: true }, () => {
      keyPress("z");
    });
    expect(h.elements.filter((element) => !element.isDeleted).length).toBe(1);
    withModifierKeys({ ctrl: true, shift: true }, () => {
      keyPress("z");
    });
    expect(h.elements.filter((element) => !element.isDeleted).length).toBe(2);
  });

  it("noop interaction after undo shouldn't create history entry", () => {
    // NOTE: this will fail if this test case is run in isolation. There's
    //  some leaking state or race conditions in initialization/teardown
    //  (couldn't figure out)
    expect(getStateHistory().length).toBe(0);

    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(10, 10);

    const firstElementEndPoint = mouse.getPosition();

    clickTool("rectangle");
    mouse.down(10, -10);
    mouse.up(10, 10);

    const secondElementEndPoint = mouse.getPosition();

    expect(getStateHistory().length).toBe(2);

    withModifierKeys({ ctrl: true }, () => {
      keyPress("z");
    });

    expect(getStateHistory().length).toBe(1);

    // clicking an element shouldn't add to history
    mouse.restorePosition(...firstElementEndPoint);
    mouse.click();
    expect(getStateHistory().length).toBe(1);

    withModifierKeys({ shift: true, ctrl: true }, () => {
      keyPress("z");
    });

    expect(getStateHistory().length).toBe(2);

    // clicking an element shouldn't add to history
    mouse.click();
    expect(getStateHistory().length).toBe(2);

    const firstSelectedElementId = getSelectedElement().id;

    // same for clicking the element just redo-ed
    mouse.restorePosition(...secondElementEndPoint);
    mouse.click();
    expect(getStateHistory().length).toBe(2);

    expect(getSelectedElement().id).not.toEqual(firstSelectedElementId);
  });

  it("zoom hotkeys", () => {
    expect(h.state.zoom).toBe(1);
    fireEvent.keyDown(document, { code: "Equal", ctrlKey: true });
    fireEvent.keyUp(document, { code: "Equal", ctrlKey: true });
    expect(h.state.zoom).toBeGreaterThan(1);
    fireEvent.keyDown(document, { code: "Minus", ctrlKey: true });
    fireEvent.keyUp(document, { code: "Minus", ctrlKey: true });
    expect(h.state.zoom).toBe(1);
  });

  it("rerenders UI on language change", async () => {
    // select rectangle tool to show properties menu
    clickTool("rectangle");
    // english lang should display `hachure` label
    expect(screen.queryByText(/hachure/i)).not.toBeNull();
    fireEvent.change(document.querySelector(".dropdown-select__language")!, {
      target: { value: "de-DE" },
    });
    // switching to german, `hachure` label should no longer exist
    await waitFor(() => expect(screen.queryByText(/hachure/i)).toBeNull());
    // reset language
    fireEvent.change(document.querySelector(".dropdown-select__language")!, {
      target: { value: "en" },
    });
    // switching back to English
    await waitFor(() => expect(screen.queryByText(/hachure/i)).not.toBeNull());
  });

  it("make a group and duplicate it", () => {
    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(10, 10);

    clickTool("rectangle");
    mouse.down(10, -10);
    mouse.up(10, 10);

    clickTool("rectangle");
    mouse.down(10, -10);
    mouse.up(10, 10);
    const end = mouse.getPosition();

    mouse.reset();
    mouse.down();
    mouse.restorePosition(...end);
    mouse.up();

    expect(h.elements.length).toBe(3);
    for (const element of h.elements) {
      expect(element.groupIds.length).toBe(0);
      expect(h.state.selectedElementIds[element.id]).toBe(true);
    }

    withModifierKeys({ ctrl: true }, () => {
      keyPress("g");
    });

    for (const element of h.elements) {
      expect(element.groupIds.length).toBe(1);
    }

    withModifierKeys({ alt: true }, () => {
      mouse.restorePosition(...end);
      mouse.down();
      mouse.up(10, 10);
    });

    expect(h.elements.length).toBe(6);
    const groups = new Set();
    for (const element of h.elements) {
      for (const groupId of element.groupIds) {
        groups.add(groupId);
      }
    }

    expect(groups.size).toBe(2);
  });

  it("double click to edit a group", () => {
    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(10, 10);

    clickTool("rectangle");
    mouse.down(10, -10);
    mouse.up(10, 10);

    clickTool("rectangle");
    mouse.down(10, -10);
    mouse.up(10, 10);

    withModifierKeys({ ctrl: true }, () => {
      keyPress("a");
      keyPress("g");
    });

    expect(getSelectedElements().length).toBe(3);
    expect(h.state.editingGroupId).toBe(null);
    mouse.doubleClick();
    expect(getSelectedElements().length).toBe(1);
    expect(h.state.editingGroupId).not.toBe(null);
  });

  it("adjusts z order when grouping", () => {
    const positions = [];

    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(10, 10);
    positions.push(mouse.getPosition());

    clickTool("rectangle");
    mouse.down(10, -10);
    mouse.up(10, 10);
    positions.push(mouse.getPosition());

    clickTool("rectangle");
    mouse.down(10, -10);
    mouse.up(10, 10);
    positions.push(mouse.getPosition());

    const ids = h.elements.map((element) => element.id);

    mouse.restorePosition(...positions[0]);
    mouse.click();
    mouse.restorePosition(...positions[2]);
    withModifierKeys({ shift: true }, () => {
      mouse.click();
    });
    withModifierKeys({ ctrl: true }, () => {
      keyPress("g");
    });

    expect(h.elements.map((element) => element.id)).toEqual([
      ids[1],
      ids[0],
      ids[2],
    ]);
  });

  it("supports nested groups", () => {
    const positions: number[][] = [];

    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(10, 10);
    positions.push(mouse.getPosition());

    clickTool("rectangle");
    mouse.down(10, -10);
    mouse.up(10, 10);
    positions.push(mouse.getPosition());

    clickTool("rectangle");
    mouse.down(10, -10);
    mouse.up(10, 10);
    positions.push(mouse.getPosition());

    withModifierKeys({ ctrl: true }, () => {
      keyPress("a");
      keyPress("g");
    });

    mouse.doubleClick();
    withModifierKeys({ shift: true }, () => {
      mouse.restorePosition(...positions[0]);
      mouse.click();
    });
    withModifierKeys({ ctrl: true }, () => {
      keyPress("g");
    });

    const groupIds = h.elements[2].groupIds;
    expect(groupIds.length).toBe(2);
    expect(h.elements[1].groupIds).toEqual(groupIds);
    expect(h.elements[0].groupIds).toEqual(groupIds.slice(1));

    mouse.click(50, 50);
    expect(getSelectedElements().length).toBe(0);
    mouse.restorePosition(...positions[0]);
    mouse.click();
    expect(getSelectedElements().length).toBe(3);
    expect(h.state.editingGroupId).toBe(null);

    mouse.doubleClick();
    expect(getSelectedElements().length).toBe(2);
    expect(h.state.editingGroupId).toBe(groupIds[1]);

    mouse.doubleClick();
    expect(getSelectedElements().length).toBe(1);
    expect(h.state.editingGroupId).toBe(groupIds[0]);

    // click out of the group
    mouse.restorePosition(...positions[1]);
    mouse.click();
    expect(getSelectedElements().length).toBe(0);
    mouse.click();
    expect(getSelectedElements().length).toBe(3);
    mouse.doubleClick();
    expect(getSelectedElements().length).toBe(1);
  });

  it("updates fontSize & fontFamily appState", () => {
    clickTool("text");
    expect(h.state.currentItemFontFamily).toEqual(1); // Virgil
    fireEvent.click(screen.getByText(/code/i));
    expect(h.state.currentItemFontFamily).toEqual(3); // Cascadia
  });

  it("shows context menu for canvas", () => {
    fireEvent.contextMenu(canvas, { button: 2, clientX: 1, clientY: 1 });
    const contextMenu = document.querySelector(".context-menu");
    const options = contextMenu?.querySelectorAll(".context-menu-option");
    const expectedOptions = ["Select all", "Toggle grid mode"];

    expect(contextMenu).not.toBeNull();
    expect(options?.length).toBe(2);
    expect(options?.item(0).textContent).toBe(expectedOptions[0]);
  });

  it("shows context menu for element", () => {
    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(20, 20);

    fireEvent.contextMenu(canvas, { button: 2, clientX: 1, clientY: 1 });
    const contextMenu = document.querySelector(".context-menu");
    const options = contextMenu?.querySelectorAll(".context-menu-option");
    const expectedOptions = [
      "Copy styles",
      "Paste styles",
      "Delete",
      "Add to library",
      "Send backward",
      "Bring forward",
      "Send to back",
      "Bring to front",
      "Duplicate",
    ];

    expect(contextMenu).not.toBeNull();
    expect(contextMenu?.children.length).toBe(9);
    options?.forEach((opt, i) => {
      expect(opt.textContent).toBe(expectedOptions[i]);
    });
  });

  it("shows 'Group selection' in context menu for multiple selected elements", () => {
    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(10, 10);

    clickTool("rectangle");
    mouse.down(10, -10);
    mouse.up(10, 10);

    mouse.reset();
    mouse.click(10, 10);
    withModifierKeys({ shift: true }, () => {
      mouse.click(20, 0);
    });

    fireEvent.contextMenu(canvas, { button: 2, clientX: 1, clientY: 1 });

    const contextMenu = document.querySelector(".context-menu");
    const options = contextMenu?.querySelectorAll(".context-menu-option");
    const expectedOptions = [
      "Copy styles",
      "Paste styles",
      "Delete",
      "Group selection",
      "Add to library",
      "Send backward",
      "Bring forward",
      "Send to back",
      "Bring to front",
      "Duplicate",
    ];

    expect(contextMenu).not.toBeNull();
    expect(contextMenu?.children.length).toBe(10);
    options?.forEach((opt, i) => {
      expect(opt.textContent).toBe(expectedOptions[i]);
    });
  });

  it("shows 'Ungroup selection' in context menu for group inside selected elements", () => {
    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(10, 10);

    clickTool("rectangle");
    mouse.down(10, -10);
    mouse.up(10, 10);

    mouse.reset();
    mouse.click(10, 10);
    withModifierKeys({ shift: true }, () => {
      mouse.click(20, 0);
    });

    withModifierKeys({ ctrl: true }, () => {
      keyPress("g");
    });

    fireEvent.contextMenu(canvas, { button: 2, clientX: 1, clientY: 1 });

    const contextMenu = document.querySelector(".context-menu");
    const options = contextMenu?.querySelectorAll(".context-menu-option");
    const expectedOptions = [
      "Copy styles",
      "Paste styles",
      "Delete",
      "Ungroup selection",
      "Add to library",
      "Send backward",
      "Bring forward",
      "Send to back",
      "Bring to front",
      "Duplicate",
    ];

    expect(contextMenu).not.toBeNull();
    expect(contextMenu?.children.length).toBe(10);
    options?.forEach((opt, i) => {
      expect(opt.textContent).toBe(expectedOptions[i]);
    });
  });

  it("selecting 'Copy styles' in context menu copies styles", () => {
    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(20, 20);

    fireEvent.contextMenu(canvas, { button: 2, clientX: 1, clientY: 1 });
    const contextMenu = document.querySelector(".context-menu");
    expect(copiedStyles).toBe("{}");
    fireEvent.click(queryByText(contextMenu as HTMLElement, "Copy styles")!);
    expect(copiedStyles).not.toBe("{}");
    const element = JSON.parse(copiedStyles);
    expect(element).toEqual(getSelectedElement());
  });

  it("selecting 'Paste styles' in context menu pastes styles", () => {
    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(20, 20);

    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(20, 20);

    // Change some styles of second rectangle
    clickLabeledElement("Stroke");
    clickLabeledElement("#c92a2a");
    clickLabeledElement("Background");
    clickLabeledElement("#e64980");
    // Fill style
    fireEvent.click(screen.getByLabelText("Cross-hatch"));
    // Stroke width
    fireEvent.click(screen.getByLabelText("Bold"));
    // Stroke style
    fireEvent.click(screen.getByLabelText("Dotted"));
    // Roughness
    fireEvent.click(screen.getByLabelText("Cartoonist"));
    // Opacity
    fireEvent.change(screen.getByLabelText("Opacity"), {
      target: { value: "60" },
    });

    mouse.reset();
    // Copy styles of second rectangle
    fireEvent.contextMenu(canvas, { button: 2, clientX: 40, clientY: 40 });
    let contextMenu = document.querySelector(".context-menu");
    fireEvent.click(queryByText(contextMenu as HTMLElement, "Copy styles")!);
    const secondRect = JSON.parse(copiedStyles);
    expect(secondRect.id).toBe(h.elements[1].id);

    mouse.reset();
    // Paste styles to first rectangle
    fireEvent.contextMenu(canvas, { button: 2, clientX: 10, clientY: 10 });
    contextMenu = document.querySelector(".context-menu");
    fireEvent.click(queryByText(contextMenu as HTMLElement, "Paste styles")!);

    const firstRect = getSelectedElement();
    expect(firstRect.id).toBe(h.elements[0].id);
    expect(firstRect.strokeColor).toBe("#c92a2a");
    expect(firstRect.backgroundColor).toBe("#e64980");
    expect(firstRect.fillStyle).toBe("cross-hatch");
    expect(firstRect.strokeWidth).toBe(2); // Bold: 2
    expect(firstRect.strokeStyle).toBe("dotted");
    expect(firstRect.roughness).toBe(2); // Cartoonist: 2
    expect(firstRect.opacity).toBe(60);
  });

  it("selecting 'Delete' in context menu deletes element", () => {
    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(20, 20);

    fireEvent.contextMenu(canvas, { button: 2, clientX: 1, clientY: 1 });
    const contextMenu = document.querySelector(".context-menu");
    fireEvent.click(queryByText(contextMenu as HTMLElement, "Delete")!);
    expect(getSelectedElements()).toHaveLength(0);
    expect(h.elements[0].isDeleted).toBe(true);
  });

  it("selecting 'Add to library' in context menu adds element to library", async () => {
    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(20, 20);

    fireEvent.contextMenu(canvas, { button: 2, clientX: 1, clientY: 1 });
    const contextMenu = document.querySelector(".context-menu");
    fireEvent.click(queryByText(contextMenu as HTMLElement, "Add to library")!);

    await waitFor(() => {
      const library = localStorage.getItem("excalidraw-library");
      expect(library).not.toBeNull();
      const addedElement = JSON.parse(library!)[0][0];
      expect(addedElement).toEqual(h.elements[0]);
    });
  });

  it("selecting 'Duplicate' in context menu duplicates element", () => {
    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(20, 20);

    fireEvent.contextMenu(canvas, { button: 2, clientX: 1, clientY: 1 });
    const contextMenu = document.querySelector(".context-menu");
    fireEvent.click(queryByText(contextMenu as HTMLElement, "Duplicate")!);
    expect(h.elements).toHaveLength(2);
    const { id: _id0, seed: _seed0, x: _x0, y: _y0, ...rect1 } = h.elements[0];
    const { id: _id1, seed: _seed1, x: _x1, y: _y1, ...rect2 } = h.elements[1];
    expect(rect1).toEqual(rect2);
  });

  it("selecting 'Send backward' in context menu sends element backward", () => {
    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(20, 20);

    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(20, 20);

    mouse.reset();
    fireEvent.contextMenu(canvas, { button: 2, clientX: 40, clientY: 40 });
    const contextMenu = document.querySelector(".context-menu");
    const elementsBefore = h.elements;
    fireEvent.click(queryByText(contextMenu as HTMLElement, "Send backward")!);
    expect(elementsBefore[0].id).toEqual(h.elements[1].id);
    expect(elementsBefore[1].id).toEqual(h.elements[0].id);
  });

  it("selecting 'Bring forward' in context menu brings element forward", () => {
    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(20, 20);

    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(20, 20);

    mouse.reset();
    fireEvent.contextMenu(canvas, { button: 2, clientX: 10, clientY: 10 });
    const contextMenu = document.querySelector(".context-menu");
    const elementsBefore = h.elements;
    fireEvent.click(queryByText(contextMenu as HTMLElement, "Bring forward")!);
    expect(elementsBefore[0].id).toEqual(h.elements[1].id);
    expect(elementsBefore[1].id).toEqual(h.elements[0].id);
  });

  it("selecting 'Send to back' in context menu sends element to back", () => {
    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(20, 20);

    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(20, 20);

    mouse.reset();
    fireEvent.contextMenu(canvas, { button: 2, clientX: 40, clientY: 40 });
    const contextMenu = document.querySelector(".context-menu");
    const elementsBefore = h.elements;
    fireEvent.click(queryByText(contextMenu as HTMLElement, "Send to back")!);
    expect(elementsBefore[1].id).toEqual(h.elements[0].id);
  });

  it("selecting 'Bring to front' in context menu brings element to front", () => {
    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(20, 20);

    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(20, 20);

    mouse.reset();
    fireEvent.contextMenu(canvas, { button: 2, clientX: 10, clientY: 10 });
    const contextMenu = document.querySelector(".context-menu");
    const elementsBefore = h.elements;
    fireEvent.click(queryByText(contextMenu as HTMLElement, "Bring to front")!);
    expect(elementsBefore[0].id).toEqual(h.elements[1].id);
  });

  it("selecting 'Group selection' in context menu groups selected elements", () => {
    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(20, 20);

    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(20, 20);

    mouse.reset();
    withModifierKeys({ shift: true }, () => {
      mouse.click(10, 10);
    });

    fireEvent.contextMenu(canvas, { button: 2, clientX: 1, clientY: 1 });
    const contextMenu = document.querySelector(".context-menu");
    fireEvent.click(
      queryByText(contextMenu as HTMLElement, "Group selection")!,
    );
    const selectedGroupIds = Object.keys(h.state.selectedGroupIds);
    expect(h.elements[0].groupIds).toEqual(selectedGroupIds);
    expect(h.elements[1].groupIds).toEqual(selectedGroupIds);
  });

  it("selecting 'Ungroup selection' in context menu ungroups selected group", () => {
    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(20, 20);

    clickTool("rectangle");
    mouse.down(10, 10);
    mouse.up(20, 20);

    mouse.reset();
    withModifierKeys({ shift: true }, () => {
      mouse.click(10, 10);
    });

    withModifierKeys({ ctrl: true }, () => {
      keyPress("g");
    });

    fireEvent.contextMenu(canvas, { button: 2, clientX: 1, clientY: 1 });
    const contextMenu = document.querySelector(".context-menu");
    fireEvent.click(
      queryByText(contextMenu as HTMLElement, "Ungroup selection")!,
    );

    const selectedGroupIds = Object.keys(h.state.selectedGroupIds);
    expect(selectedGroupIds).toHaveLength(0);
    expect(h.elements[0].groupIds).toHaveLength(0);
    expect(h.elements[1].groupIds).toHaveLength(0);
  });
});
Example #22
Source File: UMLEditor.test.tsx    From legend-studio with Apache License 2.0 4 votes vote down vote up
test(
  integrationTest('Class editor without constraints and derived properties'),
  async () => {
    await TEST__openElementFromExplorerTree('ui::TestClass', renderResult);
    const editPanelHeader = renderResult.getByTestId(
      LEGEND_STUDIO_TEST_ID.EDIT_PANEL__HEADER_TABS,
    );
    expect(getByText(editPanelHeader, 'TestClass')).not.toBeNull();
    const classForm = renderResult.getByTestId(
      LEGEND_STUDIO_TEST_ID.CLASS_FORM_EDITOR,
    );
    // Normal properties
    const classProperties = ['a', 'b', 'name', 'person'];
    classProperties.forEach((t) =>
      expect(getByDisplayValue(classForm, t)).not.toBeNull(),
    );
    // Supertype propertes
    const superTypeProperties = [
      'legs',
      'arms',
      'planet',
      'description',
      'founder',
    ];
    superTypeProperties.forEach((superTypeProperty) => {
      // input fields for super type property name are not present/disabled
      expect(queryByDisplayValue(classForm, superTypeProperty)).toBeNull();
      expect(queryByText(classForm, superTypeProperty)).not.toBeNull();
    });
    // Association properties
    const associationProperties = ['testClassSibling'];
    associationProperties.forEach((associationProperty) => {
      // input fields for association property name are not present/disabled
      expect(queryByDisplayValue(classForm, associationProperty)).toBeNull();
      expect(queryByText(classForm, associationProperty)).not.toBeNull();
    });
    // SuperTypes
    fireEvent.click(getByText(classForm, 'Super Types'));
    await waitFor(() => getByText(classForm, 'Animal'));
    // TaggedValues
    fireEvent.click(getByText(classForm, 'Tagged Values'));
    await waitFor(() => getByText(classForm, 'ProfileTest'));
    expect(getByText(classForm, 'tag1')).not.toBeNull();
    expect(getByDisplayValue(classForm, 'test')).not.toBeNull();
    // Stereotypes
    fireEvent.click(getByText(classForm, 'Stereotypes'));
    await waitFor(() => getByText(classForm, 'ProfileTest'));
    expect(getByText(classForm, 'stereotype1')).not.toBeNull();
    // Back to properties. Test more rigorous
    fireEvent.click(getByText(classForm, 'Properties'));
    await waitFor(() => getByText(classForm, 'founder'));
    const inputA = getByDisplayValue(classForm, 'a');
    const propertyA = inputA.parentElement?.parentElement
      ?.parentElement as HTMLElement;
    fireEvent.change(inputA, { target: { value: 'abcdefg' } });
    await waitFor(() => getByDisplayValue(classForm, 'abcdefg'));
    expect(getAllByDisplayValue(propertyA, '1')).toHaveLength(2);
    expect(getByText(propertyA, 'String')).not.toBeNull();
    expect(getAllByRole(propertyA, 'button')).toHaveLength(2);
    fireEvent.click(guaranteeNonNullable(getAllByRole(propertyA, 'button')[1]));
    expect(queryByDisplayValue(classForm, 'abcdefg')).toBeNull();
    // Sub Panel Property
    const inputB = getByDisplayValue(classForm, 'b');
    const propertyB = inputB.parentElement?.parentElement
      ?.parentElement as HTMLElement;
    const buttons = getAllByRole(propertyB, 'button');
    expect(buttons).toHaveLength(2);
    expect(queryByDisplayValue(classForm, 'ProfileTest')).toBeNull();
    const navigateToPropertyButton = guaranteeNonNullable(buttons[0]);
    fireEvent.click(navigateToPropertyButton);
    await waitFor(() => getByText(classForm, 'property'));
    const subPropertyPanel = getByTestId(
      classForm,
      LEGEND_STUDIO_TEST_ID.PANEL,
    );
    expect(
      getByDisplayValue(subPropertyPanel, 'lets write a tag'),
    ).not.toBeNull();
    expect(getAllByText(subPropertyPanel, 'tag2')).not.toBeNull();
    expect(getByText(subPropertyPanel, 'ProfileTest')).not.toBeNull();
    fireEvent.click(getByText(subPropertyPanel, 'Stereotypes'));
    await waitFor(() => getByText(subPropertyPanel, 'stereotype1'));
    fireEvent.click(
      guaranteeNonNullable(getAllByRole(subPropertyPanel, 'button')[0]),
    );
    expect(queryByRole(classForm, 'panel')).toBeNull();
  },
);