Skip to content

Commit

Permalink
Merge branch 'main' into use-mark-tag-for-highlights
Browse files Browse the repository at this point in the history
  • Loading branch information
staxly[bot] authored Jun 17, 2024
2 parents fb9690c + 1580b99 commit b8cb8e7
Show file tree
Hide file tree
Showing 11 changed files with 323 additions and 23 deletions.
12 changes: 1 addition & 11 deletions public/rex/activity.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,9 @@
"activities":{
"reading":{
"icon":{"url":"/rex/icons/reading.svg"},
"launch":{"url":"/apps/rex/assigned/:id","navigation":true,"xapi":false},
"launch":{"url":"/apps/rex/assigned/:id","navigation":true,"xapi":false,"textSize":true},
"objectIRI":"https://openstax.org/orn/reading/:id",
"objectTypeIRI":"http://id.tincanapi.com/activitytype/school-assignment"
},
"youtube_video":{
"launch":{"url":"https://www.youtube.com/embed/:id","navigation":false,"xapi":false},
"objectIRI":"https://openstax.org/orn/video-youtube/:id",
"objectTypeIRI":"http://adlnet.gov/expapi/activities/media"
},
"pressbooks_interactive": {
"launch":{"url":"https://ecampusontario.pressbooks.pub/apinteractives/wp-admin/admin-ajax.php","navigation":false,"xapi":false},
"objectIRI":"https://openstax.org/orn/interactives-pressbooks/:id",
"objectTypeIRI":"http://adlnet.gov/expapi/activities/media"
}
}
}
109 changes: 109 additions & 0 deletions src/app/content/components/AssignedTopBar.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,17 @@ import { MiddlewareAPI, Store } from '../../types';
import { setTextSize } from '../actions';
import { LinkedArchiveTreeSection } from '../types';
import { AssignedTopBar } from './AssignedTopBar';
import { assertDocument, assertWindow } from '../../utils/browser-assertions';

jest.mock('react', () => {
const react = (jest as any).requireActual('react');
return { ...react, useEffect: react.useLayoutEffect };
});

describe('AssignedTopBar', () => {
const windowBack = assertWindow();
const addEventListenerBackup = windowBack.addEventListener;
let addEventListener: jest.SpyInstance;
let store: Store;
let services: ReturnType<typeof createTestServices> & MiddlewareAPI;

Expand All @@ -24,9 +33,15 @@ describe('AssignedTopBar', () => {
getState: store.getState,
};

addEventListener = jest.spyOn(windowBack, 'addEventListener');

store.dispatch(setTextSize(0));
});

afterEach(() => {
windowBack.addEventListener = addEventListenerBackup;
});

it('renders', async() => {
const dispatch = jest.fn();
jest.spyOn(redux, 'useDispatch').mockImplementation(() => dispatch);
Expand Down Expand Up @@ -57,4 +72,98 @@ describe('AssignedTopBar', () => {

tree.unmount();
});

it('renders null with textSize integration', async() => {
services.launchToken = {tokenString: '', tokenData: {textSize: 2}};

const section = { title: '1.1 Section Title' } as LinkedArchiveTreeSection;
const intl = await createIntl('en');

const tree = renderer.create(
<Provider store={store}>
<RawIntlProvider value={intl}>
<Services.Provider value={services}>
<AssignedTopBar section={section} />
</Services.Provider>
</RawIntlProvider>
</Provider>
);

expect(tree.toJSON()).toMatchSnapshot();

tree.unmount();
});

it('handles font size from postmessage', async() => {
const dispatch = jest.fn();
Object.defineProperty(assertWindow(), 'parent', {value: {...assertWindow()}});
Object.defineProperty(assertDocument(), 'referrer', {value: assertWindow().location.toString()});
services.launchToken = {tokenString: '', tokenData: {textSize: 2}};
jest.spyOn(redux, 'useDispatch').mockImplementation(() => dispatch);

const section = { title: '1.1 Section Title' } as LinkedArchiveTreeSection;
const intl = await createIntl('en');

const tree = renderer.create(
<Provider store={store}>
<RawIntlProvider value={intl}>
<Services.Provider value={services}>
<AssignedTopBar section={section} />
</Services.Provider>
</RawIntlProvider>
</Provider>
);

renderer.act(() => {
addEventListener.mock.calls.forEach(([event, handler]) => {
if (event === 'message') {
handler({
data: {type: 'TextSizeUpdate', value: 2},
origin: assertWindow().location.origin,
});
}
});
});

expect(dispatch).toHaveBeenCalledTimes(1);
expect(dispatch).toHaveBeenCalledWith(setTextSize(2));

tree.unmount();
});

it('ignores postmessages from weird origins', async() => {
const dispatch = jest.fn();
Object.defineProperty(assertWindow(), 'parent', {value: {...assertWindow()}});
Object.defineProperty(assertDocument(), 'referrer', {value: assertWindow().location.toString()});
services.launchToken = {tokenString: '', tokenData: {textSize: 2}};
jest.spyOn(redux, 'useDispatch').mockImplementation(() => dispatch);

const section = { title: '1.1 Section Title' } as LinkedArchiveTreeSection;
const intl = await createIntl('en');

const tree = renderer.create(
<Provider store={store}>
<RawIntlProvider value={intl}>
<Services.Provider value={services}>
<AssignedTopBar section={section} />
</Services.Provider>
</RawIntlProvider>
</Provider>
);

renderer.act(() => {
addEventListener.mock.calls.forEach(([event, handler]) => {
if (event === 'message') {
handler({
data: {type: 'TextSizeUpdate', value: 2},
origin: 'https://google.com',
});
}
});
});

expect(dispatch).not.toHaveBeenCalled();

tree.unmount();
});
});
44 changes: 42 additions & 2 deletions src/app/content/components/AssignedTopBar.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import styled, { css } from 'styled-components';
import { MessageEvent } from '@openstax/types/lib.dom';
import theme from '../../theme';
import { setTextSize } from '../actions';
import * as selectContent from '../selectors';
import { LinkedArchiveTreeSection } from '../types';
import { shadow, TopBarWrapper } from './Topbar/styled';
import { TextResizer } from './Topbar/TextResizer';
import { topbarDesktopHeight, topbarMobileHeight } from './constants';
import { TextResizerValue, textResizerValues } from '../constants';
import { useLaunchToken } from '../launchToken';

// tslint:disable-next-line:variable-name
const StyledTopBarWrapper = styled(TopBarWrapper)`
Expand Down Expand Up @@ -39,21 +42,58 @@ const StyledSectionTitle = styled.h2`
`)}
`;

const useTextResizeIntegration = (handleChange: (value: TextResizerValue) => void) => {
const launchToken = useLaunchToken();

React.useEffect(() => {
if (typeof window === 'undefined' || !window.parent || window.parent === window) {
return;
}

const win = window;
const referrer = new URL(win.document.referrer);

const handler = (event: MessageEvent<any>) => {
if (
event.data.type === 'TextSizeUpdate' &&
event.origin === referrer.origin &&
textResizerValues.includes(event.data.value as TextResizerValue)
) {
handleChange(event.data.value);
}
};

win.addEventListener('message', handler);
return () => win.removeEventListener('message', handler);
}, [handleChange]);

return typeof launchToken.textSize === 'number';
};

// tslint:disable-next-line:variable-name
export const AssignedTopBar = (props: {
section: LinkedArchiveTreeSection;
}) => {

const bookTheme = useSelector(selectContent.bookTheme);
const textSize = useSelector(selectContent.textSize);
const dispatch = useDispatch();

const handleValueChange = React.useCallback((value: TextResizerValue) => {
dispatch(setTextSize(value));
}, [dispatch]);

const hasIntegration = useTextResizeIntegration(handleValueChange);

if (hasIntegration) {
return null;
}

return (
<StyledTopBarWrapper>
<StyledSectionTitle dangerouslySetInnerHTML={{ __html: props.section?.title }} />
<TextResizer
bookTheme={bookTheme}
setTextSize={(value) => dispatch(setTextSize(value))}
setTextSize={handleValueChange}
textSize={textSize}
data-testid='text-resizer'
mobileToolbarOpen={false}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,5 @@ exports[`AssignedTopBar renders 1`] = `
</div>
</div>
`;

exports[`AssignedTopBar renders null with textSize integration 1`] = `null`;
9 changes: 8 additions & 1 deletion src/app/content/hooks/storeTextSize.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ describe('loadStoredTextSize', () => {
expect(storeDispatch).toHaveBeenCalledWith(setTextSize(2));
});

it('loads the value from launchToken', async() => {
helpers.launchToken = {tokenString: '', tokenData: {textSize: 2}};
await hook();
expect(assertWindow().localStorage.getItem).toHaveBeenCalled();
expect(storeDispatch).toHaveBeenCalledWith(setTextSize(2));
});

it('noops if textSize is already set', async() => {
mockLocalStorage(2);
store.dispatch(setTextSize(3));
Expand All @@ -97,7 +104,7 @@ describe('loadStoredTextSize', () => {
global.window = window;
});

Object.entries(invalidValues).map((entry) => {
Object.entries(invalidValues).forEach((entry) => {
it(`uses the default if loaded value is ${entry[0]}`, async() => {
mockLocalStorage(entry[1]);
await hook();
Expand Down
19 changes: 10 additions & 9 deletions src/app/content/hooks/storeTextSize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import {
textResizerDefaultValue,
textResizerStorageKey,
TextResizerValue,
textResizerValues
textResizerValues,
} from '../constants';
import { textSize } from '../selectors';

export const loadStoredTextSize = (services: MiddlewareAPI & AppServices) => async() => {
const { getState, dispatch } = services;
const { getState, dispatch, launchToken } = services;
const state = getState();
let storedTextSize;
let value: TextResizerValue = textResizerDefaultValue;
Expand All @@ -18,19 +18,20 @@ export const loadStoredTextSize = (services: MiddlewareAPI & AppServices) => asy
return;
}

if (typeof window !== 'undefined') {
if (typeof launchToken?.tokenData.textSize === 'number') {
storedTextSize = launchToken.tokenData.textSize;
}

if (storedTextSize === undefined && typeof window !== 'undefined') {
try {
storedTextSize = window.localStorage.getItem(textResizerStorageKey);
storedTextSize = parseInt(window.localStorage.getItem(textResizerStorageKey) ?? '', 10);
} catch {
// They have blocked access to localStorage; ignore it
}
}

if (storedTextSize) {
const parsedValue = parseInt(storedTextSize, 10) as TextResizerValue;
if (!isNaN(parsedValue) && textResizerValues.includes(parsedValue)) {
value = parsedValue;
}
if (storedTextSize && textResizerValues.includes(storedTextSize as TextResizerValue)) {
value = storedTextSize as TextResizerValue;
}

dispatch(setTextSize(value));
Expand Down
Loading

0 comments on commit b8cb8e7

Please sign in to comment.