Skip to content

Commit

Permalink
feat(Codebytes): add tests for codebytes package disc 399 (#21)
Browse files Browse the repository at this point in the history
* working version

* update to include gamut styles

* add simple code editor

* add editor container styles

* fix prettier issues

* pass onCopy from parent

* use isIframeProp

* pass down snippets base url from parent

* simple monaco editor

* update tslint for scale

* incorporate review feedback

* fix prettier issues

* default snippets endpoint to empty string

* use rem

* update example text

* use language prop

* add language selection comp

* use colors gray

* remove env file

* run prettier

* change function name to onEdit

* update story names

* comment on change handlers

* update stories

* fix prettier

* pairing session - new props, types, and passing on handler

* use monaco-editor/react package

* update monaco editor package version

* remove comments

* add additional story for language and text not provided

* update text

* update stories

* update stories

* fix typings

* remove gitignore

* use theme navy

* fix linting issues

* update dependencies

* slight change in editorOnMount naming

* fix linting issues

* rename env

* remove eslint comment

* fix lint

* migrate codebytes tracking

* Added types file

* Remove Pick

* update select

* Added default for on

* fix linter error

* Remove TODO

* changed type to interface

* Removing "on" param and embedding tracking directly in like static sites

* Remove eslint diable

* lint fix

* added dependency to package.json

* use pascal case for language option

* fix lint

* some quick fixes with bana

* codebytes tracking

* add editor tests

* create shared mock file

* add track user impression tests

* add tracking tests

* add language selection test

* add helpers tests

* update config

* ci test

* try adding tracking lib back

* adding onCopy prop back in

* change to interface

* 1 empty line after import statement

* call track user impression only if called from forums

* use isForums prop for tests

* fix identifier

* format types

* try yarn focus

* try running codebytes command

* rename to sibling dependencies

* replace useEffect for initialText with function

* clear merge conflicts

* change to is iframe

Co-authored-by: Hailey <[email protected]>
  • Loading branch information
BandanaKM and Hailey authored Jan 31, 2022
1 parent 152f23d commit df3f780
Show file tree
Hide file tree
Showing 16 changed files with 566 additions and 22 deletions.
1 change: 1 addition & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ jobs:
- *set_npm_token
- *restore_yarn_cache
- run: yarn --production=false --frozen-lockfile
- run: yarn run install:sibling-dependencies
- *save_yarn_cache
- *save_node_modules

Expand Down
5 changes: 5 additions & 0 deletions jest.config.base.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ module.exports = (packageName) => ({
snapshotSerializers: ['enzyme-to-json/serializer'],
moduleDirectories: ['node_modules'],
collectCoverageFrom: ['<rootDir>/**/*.{js,jsx,ts,tsx}'],
moduleNameMapper: {
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'<rootDir>/../../script/jest/fileMock',
'\\.(css|scss)$': '<rootDir>/../../script/jest/styleMock',
},
coveragePathIgnorePatterns: [
'/node_modules/',
'/stories/',
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"build-all": "lerna run lernaBuildTask",
"build-changed": "lerna run lernaBuildTask --since --include-dependencies",
"start:storybook": "cd ./packages/styleguide && yarn start",
"install:sibling-dependencies": "cd ./packages/codebytes && yarn --focus",
"start": "yarn && yarn start:storybook"
},
"lint-staged": {
Expand Down
1 change: 1 addition & 0 deletions packages/codebytes/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('../../jest.config.base')('codebytes');
12 changes: 10 additions & 2 deletions packages/codebytes/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@
"@emotion/styled": "^11.3.0",
"@monaco-editor/react": "4.3.1",
"monaco-editor": ">= 0.25.0 < 1",
"react-resize-observer": "1.1.1"
"react-resize-observer": "1.1.1",
"js-base64": "^3.6.0",
"jsuri": "^1.3.1",
"@codecademy/tracking": "0.18.0"
},
"scripts": {
"verify": "tsc --noEmit",
Expand All @@ -45,7 +48,12 @@
"@emotion/jest": "^11.3.0",
"@testing-library/dom": "^7.31.2",
"@testing-library/react": "^11.0.4",
"@types/loadable__component": "^5.13.2"
"@types/loadable__component": "^5.13.2",
"@types/jsuri": "^1.3.30",
"@testing-library/react-hooks": "3.2.1",
"@testing-library/user-event": "13.1.1",
"monaco-editor-webpack-plugin": "1.9.1",
"@codecademy/gamut-tests": "*"
},
"publishConfig": {
"access": "public"
Expand Down
197 changes: 197 additions & 0 deletions packages/codebytes/src/__tests__/codebyte-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import './mocks';

import { setupRtl } from '@codecademy/gamut-tests';
import userEvent from '@testing-library/user-event';
import { encode } from 'js-base64';
import React from 'react';

import { CodeByteEditor } from '..';
import { helloWorld, validLanguages } from '../consts';
import { trackClick } from '../helpers';
import { trackUserImpression } from '../libs/eventTracking';
import { CodeByteEditorProps } from '../types';

const mockEditorTestId = 'mock-editor-test-id';

// This is a super simplified mock capable of render value and trigger onChange.
jest.mock('../MonacoEditor', () => ({
SimpleMonacoEditor: ({
value,
onChange,
}: {
value: string;
onChange?: (value: string) => void;
}) => (
<>
{value}
<input
data-testid={mockEditorTestId}
type="text"
onChange={(e) => {
onChange?.(e.target.value);
}}
value={value}
/>
</>
),
}));

const renderWrapper = setupRtl(CodeByteEditor, {});

type RenderWrapperWithProps = CodeByteEditorProps & { mode?: string };

const renderWrapperWith = ({ mode, ...rest }: RenderWrapperWithProps) => {
const url = new URL(window.location.href);

const { text, language } = rest;
if (text) {
url.searchParams.set('text', encode(text));
}
if (language) {
url.searchParams.set('lang', language);
}
url.searchParams.set('client-name', 'forum');
url.searchParams.set(
'page',
'https://discuss.codecademy.com/some-interesting/post'
);
if (mode) {
url.searchParams.set('mode', mode);
}
window.history.replaceState({}, '', url.toString());

return renderWrapper(rest);
};

describe('CodeBytes', () => {
const initialUrl = window.location.href;

afterEach(() => {
window.history.replaceState(null, '', initialUrl);
(trackClick as any).mockReset();
(trackUserImpression as any).mockReset();
});

it('has a language-specific "hello world" program defined for each language', () => {
validLanguages.forEach((language) => {
expect(helloWorld[language]).toBeDefined();
});
});

it('initializes with a language-specific "hello world" program when there is no language prop', () => {
const { view } = renderWrapper();
const selectedLanguage = view.getByRole('combobox') as Element;
userEvent.selectOptions(selectedLanguage, ['javascript']);
view.getByText(helloWorld.javascript);
});

it('initializes with a language-specific "hello world" program when there is a language prop but no text prop', () => {
const { view } = renderWrapper({ language: 'javascript' });
view.getByText(helloWorld.javascript);
});

it('initializes with deserialized text when there is a text prop but no language prop', () => {
const testString = 'yes hello';
const { view } = renderWrapper({ text: testString });
const selectedLanguage = view.getByRole('combobox') as Element;
userEvent.selectOptions(selectedLanguage, ['javascript']);
view.getByText(testString);
});

it('initializes with deserialized text when there is both a language and text prop', () => {
const testString = 'yes hello';
const { view } = renderWrapper({
text: testString,
language: 'javascript',
});
view.getByText(testString);
});

describe('Change Handlers', () => {
it('triggers onEdit on text edit', () => {
const onEdit = jest.fn();
const { view } = renderWrapper({
text: '',
language: 'javascript',
onEdit,
});

const editor = view.getByTestId(mockEditorTestId);
userEvent.type(editor, 'dog');

expect(onEdit).toHaveBeenCalledTimes(3);
expect(onEdit).toHaveBeenLastCalledWith('dog', 'javascript');
});

it('triggers onLanguageChange on language selection', () => {
const onLanguageChange = jest.fn();
const { view } = renderWrapper({
onLanguageChange,
});

const selectedLanguage = view.getByRole('combobox') as Element;
userEvent.selectOptions(selectedLanguage, ['javascript']);

expect(onLanguageChange).toHaveBeenCalledWith(
"console.log('Hello world!');",
'javascript'
);
});
});

describe('Tracking', () => {
it('triggers trackClick on clicking the logo', () => {
const { view } = renderWrapper({});
const logo = view.getByLabelText('visit codecademy.com');
userEvent.click(logo);
expect(trackClick).toHaveBeenCalledWith('logo');
});

it('triggers trackClick on language selection', () => {
const { view } = renderWrapper();
const selectedLanguage = view.getByRole('combobox') as Element;
userEvent.selectOptions(selectedLanguage, ['javascript']);
expect(trackClick).toHaveBeenCalledWith('lang_select');
});

it('triggers trackClick for the first edit in view mode', () => {
const testString = 'original-value';
const { view } = renderWrapper({
text: testString,
language: 'javascript',
});

const editor = view.getByTestId(mockEditorTestId);
userEvent.type(editor, 'd');

expect(trackClick).toHaveBeenCalledWith('edit');
});

it('triggers trackUserImpression for view mode', () => {
renderWrapperWith({
text: 'some-value',
language: 'javascript',
});

expect(trackUserImpression).toHaveBeenCalledWith({
page_name: 'forum',
context: 'https://discuss.codecademy.com/some-interesting/post',
target: 'codebyte',
});
});

it('triggers trackUserImpression for compose mode', () => {
renderWrapperWith({
text: 'some-value',
language: 'javascript',
mode: 'compose',
});

expect(trackUserImpression).toHaveBeenCalledWith({
page_name: 'forum_compose',
context: 'https://discuss.codecademy.com/some-interesting/post',
target: 'codebyte',
});
});
});
});
113 changes: 113 additions & 0 deletions packages/codebytes/src/__tests__/editor-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import './mocks';

import { setupRtl } from '@codecademy/gamut-tests';
import { act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';

import { Editor } from '../editor';
import { trackClick } from '../helpers';

jest.mock('../MonacoEditor', () => ({
SimpleMonacoEditor: ({ value }: { value: string }) => <>{value}</>,
}));

const renderWrapper = setupRtl(Editor, {
hideCopyButton: false,
language: 'javascript',
text: 'hello world',
onChange: jest.fn(),
snippetsBaseUrl: '',
});

Object.defineProperty(navigator, 'clipboard', {
value: {
writeText: jest.fn().mockImplementation(() => Promise.resolve()),
},
});

describe('Editor', () => {
(global as any).fetch = jest.fn();
afterEach(() => {
(global as any).fetch.mockClear();
});

it('shows a prompt tooltip when the CodeByte has __not__ been copied via the button', () => {
const { view } = renderWrapper();
expect(view.queryByTestId('copy-confirmation-tooltip')).toBeFalsy();
view.getByTestId('copy-prompt-tooltip');
});

it('shows a confirmation tooltip when the CodeByte has been copied via the button', () => {
const { view } = renderWrapper();
const copyBtn = view.getByTestId('copy-codebyte-btn');
userEvent.click(copyBtn as HTMLButtonElement);
expect(view.queryByTestId('copy-prompt-tooltip')).toBeFalsy();
view.getByTestId('copy-confirmation-tooltip');
});

it('hides the copy codebyte button if hideCopyButton prop is true"', () => {
const { view } = renderWrapper({
hideCopyButton: true,
});
expect(view.queryByTestId('copy-codebyte-btn')).toBeNull();
});

it('shows the copy codebyte button if hideCopyButton prop is not set', () => {
const { view } = renderWrapper();

view.getByTestId('copy-codebyte-btn');
});

describe('Change Handlers', () => {
it('triggers onCopy upon clicking the copy button', () => {
const onCopy = jest.fn();
const { view } = renderWrapper({
onCopy,
});

const copyButton = view.getByTestId('copy-codebyte-btn');
userEvent.click(copyButton);

expect(onCopy).toHaveBeenCalled();
});
});

describe('Tracking', () => {
it('tracks clicks on the run button', async () => {
(global as any).fetch.mockResolvedValue({
json: () =>
Promise.resolve({
stderr: [],
exit_code: 0,
stdout: '',
}),
});
const { view } = renderWrapper({
onChange: jest.fn(),
text: 'test',
language: 'javascript',
});

const runButton = view.getByText('Run');
await act(async () => {
userEvent.click(runButton);
});

expect(trackClick).toHaveBeenCalledWith('run');
});

it('tracks clicks on the copy codebyte button', () => {
const { view } = renderWrapper({
onChange: jest.fn(),
text: 'test',
language: 'javascript',
});

const copyButton = view.getByTestId('copy-codebyte-btn');
userEvent.click(copyButton);

expect(trackClick).toHaveBeenCalledWith('copy');
});
});
});
Loading

0 comments on commit df3f780

Please sign in to comment.