Skip to content

Commit

Permalink
Merge pull request #118 from graasp/88/websockets
Browse files Browse the repository at this point in the history
Add React hooks calls and integration tests for real-time updates (#88)
  • Loading branch information
Alexandre Chau authored Jun 30, 2021
2 parents fa95a22 + 127d79a commit cb8335c
Show file tree
Hide file tree
Showing 8 changed files with 481 additions and 26 deletions.
47 changes: 47 additions & 0 deletions cypress/integration/ws/mock-ws.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* Mock WebSocket class for Graasp WS protocol
*/

// eslint-disable-next-line import/prefer-default-export
export class WebSocket {
constructor() {
this.CLOSED = 0;
this.OPEN = 1;

this.readyState = this.OPEN;

this.send = this.send.bind(this);
this.receive = this.receive.bind(this);
this.addEventListener = this.addEventListener.bind(this);
}

send(msg) {
const req = JSON.parse(msg);
// acknowledge request
if (req.action.includes('subscribe')) {
const res = {
data: JSON.stringify({
realm: 'notif',
type: 'response',
status: 'success',
request: req,
}),
};
this.onmessage(res);
}
}

receive(msg) {
const event = {
data: JSON.stringify(msg),
};
this.onmessage(event);
}

addEventListener(event, handler) {
this[`on${event}`] = handler;
if (event === 'open') {
handler();
}
}
}
131 changes: 131 additions & 0 deletions cypress/integration/ws/ws.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { buildItemPath, SHARED_ITEMS_PATH } from '../../../src/config/paths';
import { buildItemsTableRowId } from '../../../src/config/selectors';
import { SAMPLE_ITEMS } from '../../fixtures/items';
import { CURRENT_USER } from '../../fixtures/members';
import { WebSocket } from './mock-ws';

describe('Websocket interactions', () => {
let client;

beforeEach(() => {
client = new WebSocket();
});

// paramaterized before, to be called in each test or in beforeEach
const beforeWs = (visitRoute, sampleData) => {
cy.setUpApi(sampleData);
cy.visit(visitRoute, {
onBeforeLoad: (win) => {
cy.stub(win, 'WebSocket', () => client);
},
});
};

describe('sharedWith me items updates', () => {
it('displays sharedWith create update', () => {
beforeWs(SHARED_ITEMS_PATH, { items: [] });

const item = SAMPLE_ITEMS.items[0];
cy.wait('@getSharedItems').then(() => {
// send mock sharedItem create update
client.receive({
realm: 'notif',
type: 'update',
channel: CURRENT_USER.id,
body: {
entity: 'member',
kind: 'sharedWith',
op: 'create',
value: item,
},
});
});

// assert item is in list
cy.get(`#${buildItemsTableRowId(item.id)}`).should('exist');
});

it('displays sharedWith delete update', () => {
// create items that do not belong to current user
const items = SAMPLE_ITEMS.items.map((i) => ({
...i,
creator: 'someoneElse',
}));
const item = items[0];
beforeWs(SHARED_ITEMS_PATH, { items });

cy.get(`#${buildItemsTableRowId(item.id)}`).then(() => {
// send mock sharedItem delete update
client.receive({
realm: 'notif',
type: 'update',
channel: CURRENT_USER.id,
body: {
entity: 'member',
kind: 'sharedWith',
op: 'delete',
value: item,
},
});
});

// assert item is not in list anymore
cy.get(`#${buildItemsTableRowId(item.id)}`).should('not.exist');
});
});

describe('childItem updates', () => {
const { id } = SAMPLE_ITEMS.items[0];

beforeEach(() => {
beforeWs(buildItemPath(id), SAMPLE_ITEMS);

// should get children
cy.wait('@getChildren').then(({ response: { body } }) => {
// check item is created and displayed
for (const item of body) {
cy.get(`#${buildItemsTableRowId(item.id)}`).should('exist');
}
});
});

it('displays childItem create update', () => {
const item = { ...SAMPLE_ITEMS.items[0], id: 'child0' };
// send mock childItem create update
client.receive({
realm: 'notif',
type: 'update',
channel: id,
body: {
entity: 'item',
kind: 'childItem',
op: 'create',
value: item,
},
});

// assert item is in list
cy.get(`#${buildItemsTableRowId(item.id)}`).should('exist');
});

it('displays childItem delete update', () => {
// this item MUST be a child of id above
const item = SAMPLE_ITEMS.items[2];
// send mock childItem delete update
client.receive({
realm: 'notif',
type: 'update',
channel: id,
body: {
entity: 'item',
kind: 'childItem',
op: 'delete',
value: item,
},
});

// assert item is not in list
cy.get(`#${buildItemsTableRowId(item.id)}`).should('not.exist');
});
});
});
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"@commitlint/config-conventional": "11.0.0",
"@cypress/code-coverage": "3.9.2",
"@cypress/instrument-cra": "1.4.0",
"@graasp/websockets": "git://github.com/graasp/graasp-websockets.git#master",
"@testing-library/jest-dom": "^5.11.6",
"@testing-library/react": "^11.2.2",
"@testing-library/user-event": "^12.6.0",
Expand Down
6 changes: 4 additions & 2 deletions src/components/SharedItems.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,21 @@ import {
import ItemHeader from './item/header/ItemHeader';
import ErrorAlert from './common/ErrorAlert';
import Items from './main/Items';
import { hooks } from '../config/queryClient';
import { hooks, ws } from '../config/queryClient';
import Loader from './common/Loader';
import Main from './main/Main';

const SharedItems = () => {
const { t } = useTranslation();
const { data: sharedItems, isLoading, isError } = hooks.useSharedItems();
const { data: user, isUserLoading } = hooks.useCurrentMember();
ws.hooks.useSharedItemsUpdates(user?.get('id'));

if (isError) {
return <ErrorAlert id={SHARED_ITEMS_ERROR_ALERT_ID} />;
}

if (isLoading) {
if (isLoading || isUserLoading) {
return <Loader />;
}

Expand Down
3 changes: 2 additions & 1 deletion src/components/item/ItemContent.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
AppItem,
} from '@graasp/ui';
import { MUTATION_KEYS } from '@graasp/query-client';
import { hooks, useMutation } from '../../config/queryClient';
import { hooks, useMutation, ws } from '../../config/queryClient';
import {
buildFileItemId,
buildS3FileItemId,
Expand Down Expand Up @@ -53,6 +53,7 @@ const ItemContent = ({ item }) => {

// display children
const { data: children, isLoading: isLoadingChildren } = useChildren(itemId);
ws.hooks.useChildrenUpdates(itemId);
const id = item?.get(ITEM_KEYS.ID);

const { data: content, isLoading: isLoadingFileContent } = useFileContent(
Expand Down
8 changes: 4 additions & 4 deletions src/components/main/ItemScreen.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Loader } from '@graasp/ui';
import React, { useContext } from 'react';
import { useParams } from 'react-router';
import { Loader } from '@graasp/ui';
import { hooks } from '../../config/queryClient';
import ItemMain from '../item/ItemMain';
import ErrorAlert from '../common/ErrorAlert';
import { LayoutContext } from '../context/LayoutContext';
import Main from './Main';
import ItemContent from '../item/ItemContent';
import ItemMain from '../item/ItemMain';
import ItemSettings from '../item/settings/ItemSettings';
import ErrorAlert from '../common/ErrorAlert';
import Main from './Main';

const { useItem } = hooks;

Expand Down
3 changes: 3 additions & 0 deletions src/config/queryClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,21 @@ const {
queryClient,
QueryClientProvider,
hooks,
ws,
useMutation,
ReactQueryDevtools,
API_ROUTES,
} = configureQueryClient({
API_HOST,
notifier,
enableWebsocket: true,
});

export {
queryClient,
QueryClientProvider,
hooks,
ws,
useMutation,
ReactQueryDevtools,
API_ROUTES,
Expand Down
Loading

0 comments on commit cb8335c

Please sign in to comment.